Hi everyone. My two previous posts on this forum (#1, #2) are related to a project I'm working on that involves controlling a variety of actuators using an Arduino Mega, which is triggered by commands sent serially from a Raspberry Pi.
It was clear from the responses to #2 that my approach is flawed. I'm taking a step back and reevaluating the structure of the project.
Background:
The Arduino will be responsible for the control of a range of actuators, including linear actuators and motors. The motors have their own controllers that I can communicate through a serial connection, and the linear actuators are controlled with relays -- but the actuator control isn't the main focus of this post.
Multitasking:
In post #1 I inquired about how to best handle tasks psuedo-simultaneously and the best advice I received was in post 6 - bascially to create a state machine for each actuator and have the main loop iterate over the state machines, changing their behavior based on whether they have achieved their target positions / speeds / etc. This process of checking and updating would happen quickly enough to achieve good-enough multitasking functionality.
My current plan is to represent each actuator with a struct like this:
struct actuator {
char* name;
unsigned int timer; // Current timer count
unsigned int timeout; // Timeout set by command
int current_position; // Actual position read from encoder
int target_position; // Target position to try and reach
unsigned int state; // To set the required behavior of the actuator
}
Communication:
I'd like to be able to send packets of commands of a set structure (see below) from a Raspberry Pi to the Arduino over serial connection. A packet can contain one or more command, with no more than one command for each actuator. The Arduino will ingest and parse each packet for individual commands. I'm not sure what the most efficient way to handle the next step is -- but basically I need to iterate over the individual commands making the necessary changes to the relevant and then (or in the same for loop?) check the status of the actuators that weren't referenced in any of the commands to make sure they're on track.
This is how I envision the packet structure:
&XXYYZZZZ!
Where:
-
&signals the start of a new command -
XXis the ID of the target actuator (i.e.01formotor_1) -
YYis the ID of the command type (i.e.03forrotate_to_angle(arg)) -
ZZZZis the argumentargfrom the previous bullet -
!signals the end of a packet, so it's possible to string together multiple commands like this:
&XXYYZZZZ&XXYYZZZZ!
My current approach:
This is a minimal (but compilable! as per @david_2018's advice in post #2) example of how I'm receiving the packets of serial data (note - this is without parsing the packet for individual commands):
// Max length of packet is max number of actuators - let's say 5 for this example
const unsigned int MAX_PACKET_LENGTH = 5;
// This flag lets us know when a full packet has been received
int PACKET_READY_FLAG = 0;
void setup() {
// Open serial connection
Serial.begin(9600);
}
void loop() {
// Get incoming packet
char *packet = get_bytes();
if (PACKET_READY_FLAG == 1) {
Serial.println("New packet ready.");
}
}
char *get_bytes() {
// This runs as long as there are available bytes in the serial buffer
while (Serial.available() > 0) {
// This is the position in the packet being written to
static unsigned int packet_position = 0;
// Here we initialize the packet (character array)
static char packet[MAX_PACKET_LENGTH] = {0};
// Read next incoming byte
char in_byte = Serial.read();
// No error handling here yet - this writes to and increments the position in the packet
if (packet_position < MAX_PACKET_LENGTH) {
packet[packet_position] = in_byte;
packet_position++;
}
// This triggers the end of a packet
if (packet[packet_position - 1] == '!') {
PACKET_READY_FLAG = 1;
return packet;
}
}
}
Please note that packet is not meant to be written to / adjusted from outside the function get_bytes(), but will be read by other functions. This is mainly in response to this post of @J-M-L's (thank you for the input J-M-L!) As far as I know this presents a healthy use of static variables.
With that said hopefully my post #2 is now more clear. I need to parse packet for individual commands. My plan is to iterate over packet while copying the contents to a 2D "array of arrays", where each sub-array is an individual command. This is the function I wrote but I think there are a number of problems with it (you can paste this after the code I wrote above and it will still compile):
// The length of a command is 9 including "&" at the start
const unsigned int MAX_COMMAND_LENGTH = 9;
// This function parses the packet in static memory for individual commands
char *parse_commands(char *packet) {
// Set up 2D array of commands
static char commands[MAX_PACKET_LENGTH][MAX_COMMAND_LENGTH];
static unsigned int commands_position = 0;
// Iterate over packet in static memory and
// look for "&" for the start of a new command, incrementing
// commands_position when each new command is discovered
for (int i=0; i<MAX_PACKET_LENGTH; i++) {
if (packet[i] == '&') {
// Write to command sub-array
for (int j=i; j<MAX_COMMAND_LENGTH; j++) {
// Is this the right way to copy a single character?
commands[commands_position][j] = packet[j];
}
// Increment command array position
commands_position++;
}
// This trips when end of packet is reached
else if (packet[i] == '!') {
// ---> Return
return *commands;
}
}
}
Bonus imaginary points for whoever can answer this:
Regarding "---> Return", I originally tried to simply put "return commands;" without * like I did with packets in get_bytes(), but it didn't compile (received the error cannot convert 'char (*)[9]' to 'char*' in return). I don't understand why. Shouldn't it simply return a pointer to the address of the first item in the 2D commands array?
That's enough for now.
To wrap up, my questions are:
- How can I improve my overall strategy?
- How can I improve
get_bytes()? - Am I on track with
parse_commands()? What's a more efficient (but still readable) way to parsepacket?
Thank you! I'm learning a lot from this forum.
Edit 1: Added error received when trying to compile without * in parse_commands().