Since we’re using LoRa to transmit and receive data, we have to keep in mind what that technology was designed for: transmitting small amount of data over long distances. This means that all our commands have to be as brief as possible as every byte will count. We could just send some string, e.g. “motor left 50” to set left motors to 50% speed, but that is incredibly wasteful. This string takes up 14 bytes of data just to pass two three chunks of information: what subsystem the command is meant for (“motor”) are which motors will be affected (“left”) and the percentage of maximum speed will they be set to (“50”). Safe to say, we can do better than that. The advantage is that this type of commands is very easy to read, but then again, as long as the system is working correctly, there should be no human interaction on the command level.
Instead of passing around long strings, I decided to develop a simple command system that only uses a maximum of 4 bytes for a command packet. The first byte in the packet is called header. It is further divided in half: the upper 4 bits contain the classification of command from the control system. The lower 4 bits are used for the response from the robot; 0 is success, non-zero values are error codes with known meaning.
1st byte | Command description |
0x00 | Stop all motors |
0x10 | Set speed and direction of all motors |
0x20 | Set speed and direction of left side motors |
0x30 | Set speed and direction of right side motors |
0x40 | Set camera tilt |
0x50 | Set camera pan |
0x60 | Take a picture with camera |
0x70 | Start JPEG transmission |
0x80 | Force new sensor measurement |
0x90 | Get latest sensor data |
0xA0 | Resend last packet |
0xB0 | Set LoRa modem configuration |
0xC0 | Get all currently active errors |
0xD0 | unused |
0xE0 | unused |
0xF0 | unused |
The next part of the packet is the payload – this is where all additional information for the command in header is stored. This part is a bit less straightforward than the header, because its contents depend on the type of command: some commands don’t require any additional info – 0x00 (stop both motors), for example. These commands have empty payload. However, some require up to three bytes of extra data, take for example command 0xD0 (LoRa configuration), which requires one byte for each of the main settings i.e. bandwidth, spreading factor, and coding rate (see the tutorial on LoRaLib or the GitHub wiki for details on LoRa). The following table describes structure of all packets.
1st byte | 2nd byte | 3rd byte | 4th byte |
0x00 | – | – | – |
0x10 | left side speed PWM | right side speed PWM | direction |
0x20 | left side speed PWM | direction | – |
0x30 | right side speed PWM | direction | – |
0x40 | tilt position degrees | – | – |
0x50 | pan position degrees | – | – |
0x60 | – | – | – |
0x70 | picture number | – | – |
0x80 | sensor ID(s) | – | – |
0x90 | sensor ID(s) | – | – |
0xA0 | – | – | – |
0xB0 | bandwidth | spreading factor | coding rate |
0xC0 | – | – | – |
0xD0 | – | – | – |
0xE0 | – | – | – |
0xF0 | – | – | – |
Of course, these are only the command packets transmitted by the control system and received by the robot. We also want the robot to respond. For some commands, this response can be very simple, just something to let the control system know that the command was either executed successfully or failed. If you take a closer look at the command table above, you will notice that all the commands only use the upper 4 bits of the 1st command byte. That leaves the bottom four bits which can be used for the response. This approach has two significant advantages: first, there’s no way to lose track which response belongs to a given command, because the command bits are always a part of the response. And second, there’s enough space for 16 different responses for each of the commands.
Let’s illustrate with an example. Say we want to take a picture with the camera, so we send command 0x60. The robot then has 16 different ways to respond. If the response is 0x60, it means that the command was executed successfully. All the other responses 0x61 to 0x6F mean something has gone wrong. Not only do we know that the command failed, we also have an idea of what’s wrong and how to fix it. Of course, the responses don’t always have to be just a single byte long. Some of the commands require the robot to send back additional information,like sensor data. The following table shows all the response packets for different commands.
Command | 1st byte | 2nd byte | 3rd byte | 4th – 240th byte |
0x00 | 0x00 | – | – | – |
0x10 | 0x10 | – | – | – |
0x20 | 0x20 | – | – | – |
0x30 | 0x30 | – | – | – |
0x40 | 0x40 | – | – | – |
0x50 | 0x50 | – | – | – |
0x60 | 0x6_ | – | – | – |
0x70 | 0x7_ | image data | image data | image data |
0x80 | 0x8_ | sensor(s) that failed | sensor data | sensor data |
0x90 | 0x9_ | sensor(s) that failed | sensor data | sensor data |
0xA0 | 0xA0 | – | – | – |
0xB0 | 0xB0 | – | – | – |
0xC0 | 0xC_ | error flags | error flags | – |
0xD0 | 0xD0 | – | – | – |
0xE0 | 0xE0 | – | – | – |
0xF0 | 0xF0 | – | – | – |
You can notice that some of the commands – mainly those related to motors and servos – can only return 0 (success). That’s because the current version of on-board electronics has no way to tell if those command was executed successfully. To check that, we would have to add new equipment to test whether the motors are actually moving, or if the servo is physically in the correct position. While that shouldn’t be too difficult, let’s leave it for now and take a look at commands that can result in fail:
With all that explained, let’s return to the original example. We wanted to set the left side motors speed to 50%. We already know that the header will be 0x20. So according to the above tables, we can fill in the rest of the bytes and the resulting packet sent from the controller will look like this:
0x20 0x7F 0x00 0x00
The first byte is obviously the header – set left motor speed. The next one is the speed. The motor driver changes speed of the motor by PWM modulation, so the number 0xF7 (127 in decadic format) corresponds to 50% duty cycle or 50% speed. The third byte is the direction – 0x00 means forward, 0x01 means backward. The last byte is just padding – it’s added so that the command packets are always four bytes long. Once the command is received and successfully executed by the robot, it will respond with the following packet:
0x20
The implementation of the command system in Arduino sketch that is running on the robot is on my GitHub. Usually, this is the place where I would post the actual code, unfortunately, it is far too big to fit into this article, so if you’re interested in details of the implementation, see the comments inside the actual code. Now, let’s move onto something more interesting, which is the remote control app!