training-labs

Prelab 3b: C++ Datatype Introduction

Remember from the numbers intro that we can define densely-packed data structures? Data structures will help you with sending data over the internet.

//Put this code outside of all functions, in the *global scope*
// Define a structure 'pkt_fmt' with a packed attribute to ensure there is no padding between its members.
 // This is important when dealing with binary data formats or hardware communication, where exact size and layout matter.

  struct __attribute__((__packed__)) pkt_fmt {
      uint32_t totalPackets; // A 32-bit unsigned integer to store the total number of packets.

      uint8_t myString[6];   // A fixed-size array of 6 bytes to store a short string or byte sequence.

      float illumination;    // A 32-bit floating-point value to store illumination data.
  };
   
  // Declare a global variable 'myPkt' of type 'pkt_fmt'.
 // This variable can be accessed and modified by any function in the program.
 pkt_fmt myPkt;

That code defines a densely-packed packet format and one instance called myPkt that you can send over LoRaWAN. In the lines reproduced below

myLoRaWAN.SendBuffer(messageBuffer, bufferLength, myStatusCallback, NULL, false, 1);

you now want to send bytes starting at (uint8_t *) &myPkt instead of messageBuffer. There are now sizeof(myPkt) bytes to send instead of bufferLength bytes to send.

Please don’t copy this exact data structure in your assignment.

Making groups of bytes on TTN

The example functions for working with strings on TTN assume you pass them a list of bytes. You can use input.bytes.slice(begin, end) and that will give you a list of bytes in [begin, end) (set open to the right: includes begin, but does not include end). So, we can get the illumination from bytes 10, 11, 12, and 13 by using

var exampleFloat = floatFromBytes(input.bytes.slice(10,14));

We know that our float is at bytes 10-13 because totalPackets resides in bytes 0-3, and our 6-uint8_t string resides in six bytes, bytes 4-9. We have to use attribute((__packed)) to guarantee that, though.

How do ints work?

Recall from the numbers intro that there are standard integer data types available in C++ on Arduino. They are uint8_t, int8_t, uint16_t, int16_t, uint32_t, and int32_t. U stands for unsigned, and signed just means that the most significant bit has a negative weight.

On Arduino

Simply add fixed-width integer types to your packet struct. You could also copy the bytes directly into a messagebuffer with memcpy.

On TTN

You need to reassemble ints from bytes. You can start decoding both signed and unsigned ints in a similar way. However, for bytes that represent a signed integer, you must assign a negative weight to the most significant bit.

 // Function to build an unsigned integer from an array of bytes
 // This function treats the bytes as an unsigned integer in little-endian format

  function build_uint_from_bytes(bytes) {
    var built_up_uint = 0; // Initialize the result as 0
    for (var i = 0; i < bytes.length; i++) {
      // Add the value of each byte shifted to its        appropriate position
      // '<<' shifts the byte value by (i * 8) bits to its place in the integer
      built_up_uint = built_up_uint + (bytes[i] << i * 8);
    }
   return built_up_uint; // Return the constructed unsigned integer
 }
 
 // Function to build a signed integer from an array of bytes
 // This function interprets the bytes as a signed integer in little-endian format

 function build_int_from_bytes(bytes) {
   var built_up_int = 0; // Initialize the result as 0
   var cap = bytes.length - 1; // Determine the index of the most significant byte
  
   // Process all bytes except the most significant byte
   for (var i = 0; i < cap; i++) {
     // Add the value of each byte shifted to its appropriate position
     built_up_int = built_up_int + (bytes[i] << i * 8);
   }
  
   // Handle the most significant byte separately to account for its signedness
   var lastByte = bytes[cap]; // Get the most significant byte

   if (lastByte & 0b10000000) { 
     // Check if the most significant bit (MSB) is set, indicating a negative value
     lastByte = lastByte - 256; 
     // Convert the byte to a signed value (e.g., 0xFF becomes -1)
     built_up_int = built_up_int + (lastByte << cap * 8); 
     // Add the signed value to the total, shifted to its proper position
   }
  
   return built_up_int; // Return the constructed signed integer
 } 

Example of Function Inputs and Outputs

Unsigned Integer Function

Input: [0x01, 0x02, 0x03, 0x04]

Function: build_uint_from_bytes

Explanation: Each byte contributes to the final value based on its position.

built_up_uint = (0x01 « 0) + (0x02 « 8) + (0x03 « 16) + (0x04 « 24) = 0x04030201 = 67,305,985 (decimal)

Output: 67,305,985 (or 0x04030201)


Signed Integer Function with Positive Value

Input: [0xFF, 0xFF, 0xFF, 0x7F]

Function: build_int_from_bytes

Explanation: Most significant byte (MSB) is 0x7F (MSB bit 0, positive number).

built_up_int = (0xFF « 0) + (0xFF « 8) + (0xFF « 16) + (0x7F « 24) = 0x7FFFFFFF = 2,147,483,647 (decimal)

Output: 2,147,483,647 (or 0x7FFFFFFF).


Signed Integer Function with Negative Value

Input: [0xFF, 0xFF, 0xFF, 0xFF]

Function: build_int_from_bytes

Explanation: MSB is 0xFF (MSB bit 1, negative number). Adjust 0xFF to signed value: 0xFF - 256 = -1.

built_up_int = (0xFF « 0) + (0xFF « 8) + (0xFF « 16) + (-1 « 24) = -1

Output: -1


You may copy and paste these payload bytes to confirm your understanding of these data types.

Payload Bytes Payload
72d3e901 uint32_t 32101234
8e2c16fe int32_t -32101234

How do strings work?

A string is an ordered collection of characters (char or uint8_t), such as “Hello”.

On Arduino

There is a String datatype, but we will use C strings. Define a C string as an array of chars or uint8_ts. Use memcpy to copy into your buffer. Every string ends with a terminating zero (also called a null terminator): using double quotes gives the null terminator automatically.

The following are equivalent:

struct __attribute__((__packed__)) pkt_fmt{
  uint32_t totalPackets;
  uint8_t myString[6];
  float illumination;
};

pkt_fmt myPkt;

String myString = "Hello";
uint8_t myCString[6] = "Hello";
uint8_t myCString2[6] = {'H', 'e', 'l', 'l', 'o', 0};


setup(){
  memcpy(myPkt.myString, myString.c_str(), 6);
  memcpy(myPkt.myString, myCString, 6);
  memcpy(myPkt.myString, myCString2, 6);

};

They all copy a string into an example packet.

On TTN

You can use the static method String.fromCharCode with Javascript’s spread operator ... as in the following function:

function stringFromBytes(bytes){
  return String.fromCharCode(...bytes);
}

You may copy and paste these payload bytes to confirm your understanding of these data types.

Payload Bytes Payload
68656c6c6f00 “hello”
72d3e90168656c6c6f00 uint32_t 32101234, then “hello”

The null terminator is included and appears as “\u0000” on TTN.

How does a float work?

float is a four byte datatype for floating-point arithmetic specified in the IEEE 754 standard. Wikipedia Article on Floats Its most significant bit (bit 31, or bit 7 of byte 3) is its sign bit. There are then 8 bits that are a biased exponent, and the remaining 23 bits are the fractional part. Check the article for more details. For lab 2, you may simply cite the bytesToFloat function given below and trust that it works.

On Arduino

Simply add a float to your packet struct. You could also copy the bytes directly into a messagebuffer with memcpy.

On TTN

You can pass in a list of four little-endian bytes that represent a float and this function will turn them in to a Javascript float.

function floatFromBytes(bytes) {
    // JavaScript bitwise operators yield a 32 bit integer, not a float.
    // Assume LSB (least significant byte first).
    var bits = bytes[3]<<24 | bytes[2]<<16 | bytes[1]<<8 | bytes[0];
    // >>> is unsigned right shift. Zeros are shifted in at all times from the left
    var sign = (bits>>>31 === 0) ? 1.0 : -1.0;
    var e = bits>>>23 & 0xff;
    // Can code explicit leading 0 with 0 exponent. Otherwise, there is a
    // leading 1 implied.
    // Since we have constructed our significand/mantissa like we constructed
    // our integers, it is a factor of 2^23 too large (decimal point all the way)
    // right.
    // So, we correct that when we build the final number.
    var m = (e === 0) ? (bits & 0x7fffff)<<1 : (bits & 0x7fffff) | 0x800000;
    var f = sign * m * Math.pow(2, e - 127-23);
    return f;
  }

You may copy and paste these payload bytes to confirm your understanding of these data types.

Payload Bytes Payload
ae474940 3.145
72d3e90168656c6c6f00ae474940 uint32_t 32101234, then “hello”, then 3.145