Transferring Multi-Byte Values:
So far we've only been discussing how to send a packet of individual, single byte values inside our packets. However, many users need to be able to send data with greater precision per value than can be achieved with 8 bits.
Regular (16-bit) Ints:
This is the easiest example to show since we only have 2 bytes to deal with. In this case, we can specify a packet payload of 2 bytes to send the entire int. We can then use bit-masking and bit-shifting to place the MSB (Most Significant Byte) in one of the indexes of the payload and the LSB (Least Significant Byte) in the other index of the payload.
For instance, let's say our transmitting Arduino has a sensor, who's current output is 1400 and we want to send that sensor's value to a receiving Arduino for datalogging. Since we can't fit the value 1400 into a single byte, we'll have to use a 16-bit int. Whether the MSB or LSB comes first in the payload is arbitrary as long as both the transmitter and receiver both agree on the order.
Note: if the MSB is first the order is called Big Endian and Little Endian if the LSB is first. Also note that Arduinos are Little Endian.
For this example, let's define the byte order as Little Endian. We then define the packet anatomy as the following (ignoring COBS for simplicity):
Start-Byte | Sensor-LSB | Sensor-MSB | End-Byte
We can then stuff the payload fields using code such as the following:
uint16_t sensor = 1400;
uint8_t msb = (sensor >> 8) & 0xFF;
uint8_t lsb = sensor & 0xFF;
uint8_t packet[4] = {0x7E, lsb, msb, 0x81};
We can do the same thing in Python:
def msb(val):
return byte_val(val, num_bytes(val) - 1)
def lsb(val):
return byte_val(val, 0)
def byte_val(val, pos):
return int.from_bytes(((val >> (pos * 8)) & 0xFF).to_bytes(2, 'big'), 'big')
def num_bytes(val):
num_bits = val.bit_length()
num_bytes = num_bits // 8
if num_bits % 8:
num_bytes += 1
if not num_bytes:
num_bytes = 1
return num_bytes
if __name__ == '__main__':
sensor = 1400
msb = msb(sensor)
lsb = lsb(sensor)
packet = [0x7E, lsb, msb, 0x81]
print(packet)
When receiving the integer, we simply apply the bit-masking/shifting in the opposite order:
C++:
uint8_t packet[4] = {0x7E, 0x78, 0x5, 0x81};
uint8_t msb = packet[2];
uint8_t lsb = packet[1];
uint16_t sensor = (msb << 8) | lsb;
Python:
packet = [0x7E, 0x78, 0x5, 0x81]
msb = packet[2]
lsb = packet[1]
sensor = (msb << 8) | lsb
print(sensor)
Floats:
Sometimes users need to transfer floating point numbers. For example, an Arduino collecting telemetry on a model rocket might need to send GPS coordinates to a ground station for datalogging/display on an LCD screen. What you can do in this case is multiply the sensor value by a given amount, transfer the value as a 16-bit integer (as described above), reconstruct the int on the receiving Arduino, and then divide by the same amount while saving to a float variable. Do be aware that you will lose accuracy with this method! If you want to retain full accuracy of the float during transmission, you will have to use the "Generalized Technique" as described in the next section!
Note: IEEE standard for representing floats in memory.
Generalized Technique: (C++ only - not Python)
A more elegant solution for transferring multi-byte values is to simply copy the bytes directly from memory into the packet's payload. We can do this easily with 2 pointers - one pointer to keep track of the current byte of the value we're copying over and one pointer to keep track of where we're at in the packet's payload (where we're saving the value to).
Here's an example function you can use to copy over the values from any object (except for "S"trings) to the packet payload:
/*
void txObj(T &val, uint8_t len, uint8_t index)
Description:
------------
* Stuffs "len" number of bytes of an arbitrary object (byte, int,
float, double, struct, etc...) into the transmit buffer (txBuff)
starting at the index as specified by the argument "index"
Inputs:
-------
* T &val - Pointer to the object to be copied to the
transmit buffer (txBuff)
* uint8_t len - Number of bytes of the object "val" to transmit
* uint8_t index - Starting index of the object within the
transmit buffer (txBuff)
Return:
-------
* bool - Whether or not the specified index is valid
*/
template <typename T>
bool txObj(T &val, uint8_t len, uint8_t index=0)
{
if (index < (MAX_PACKET_SIZE - len + 1))
{
uint8_t* ptr = (uint8_t*)&val;
for (byte i = index; i < (len + index); i++)
{
txBuff[i] = *ptr;
ptr++;
}
return true;
}
return false;
}
We can then use pointers in a similar way on the receiving end to save an object (such as a full 32-bit float) directly to memory using a function like this:
/*
void rxObj(T &val, uint8_t len, uint8_t index)
Description:
------------
* Reads "len" number of bytes from the receive buffer (rxBuff)
starting at the index as specified by the argument "index"
into an arbitrary object (byte, int, float, double, struct, etc...)
Inputs:
-------
* T &val - Pointer to the object to be copied into from the
receive buffer (rxBuff)
* uint8_t len - Number of bytes in the object "val" received
* uint8_t index - Starting index of the object within the
receive buffer (txBuff)
Return:
-------
* bool - Whether or not the specified index is valid
*/
template <typename T>
bool rxObj(T &val, uint8_t len, uint8_t index=0)
{
if (index < (MAX_PACKET_SIZE - len + 1))
{
uint8_t* ptr = (uint8_t*)&val;
for (byte i = index; i < (len + index); i++)
{
*ptr = rxBuff[i];
ptr++;
}
return true;
}
return false;
}
Next we'll go over how to parse packets without blocking code via a FSM (Finite State Machine).