Datalink - UWARG/PICpilot GitHub Wiki

The datalink is one of the most important peripheral components in an unmanned system. It provides information on the status of the aircraft (telemetry) and provides the crucial functionality of an uplink, in order to be able to communicate with the system and make changes to its overall functionality.

Make note that the details of this system can change frequently due to the requirements of each user. The telemetry data may change from the ones present in this document. Likewise, uplink commands may change based on newly implemented features and requirements. Although, this document should be updated whenever there is a change, this may not always occur. Therefore, be careful when referencing data in this section.

Telemetry (Downlink) Data

Data Programming Variable Format Description
Latitude Lat Long double (64bit floating point) The latitude location of the airplane in degrees.
Longitude Lon Long double (64bit floating point) The longitude location of the airplane in degrees.
Time Time Float (32 bit floating point) The time as a UTC time stamp.
Pitch Pitch Float The current state estimation of the pitch of the aircraft in degrees.
Roll Roll Float The current state estimation of the roll of the aircraft in degrees.
Yaw Yaw Float The current state estimation of the yaw of the aircraft according to the magnetometer in degrees.
Pitch Rate Pitch_rate Float The gyroscope sensor data. The rate of rotation of the aircraft in radians per second.
Roll Rate Roll_rate Float The gyroscope sensor data. The rate of rotation of the aircraft in radians per second.
Yaw Rate Yaw_rate Float The gyroscope sensor data. The rate of rotation of the aircraft in radians per second.
Derivative Gain Kd_gain Float A predetermined gain value for a PID loop. Typically used for tuning and debugging purposes.
Proportional Gain Kp_gain Float A predetermined gain value for a PID loop. Typically used for tuning and debugging purposes.
Integral Gain Ki_gain Float A predetermined gain value for a PID loop. Typically used for tuning and debugging purposes.
Heading Heading Float The GPS heading of the aircraft in degrees ranging from 0 to 360.
Ground Speed Ground_speed Float The ground speed of the aircraft in meters per second.
Pitch Setpoint Pitch_setpoint Float The autopilot-controlled setpoint for the pitch angle in degrees.
Roll Setpoint Roll_setpoint Float The autopilot-controlled setpoint for the Roll angle in degrees.
Heading Setpoint heading_setpoint Float The autopilot-controlled setpoint for the heading angle in degrees.
Throttle Setpoint Throttle_setpoint Float The autopilot-controlled setpoint for the propeller speed in terms of percentage (0-100%)
Altitude Setpoint altitude_setpoint Float The autopilot-controlled setpoint for the altitude above mean sea level. (in meters)
Altitude altitude Float The altitude of the plane above the mean sea level (in meters)
Pitch Setpoint (Controller) int_pitch_setpoint Float The user input for the Pitch angle in arbitrary timer tick units.
Roll Setpoint(Controller) int_roll_setpoint Float The user input for the Roll angle in arbitrary timer tick units.
Yaw Setpoint (Controller) int_yaw_setpoint Float The user input for the yaw angle in arbitrary timer tick units.
Last Wireless Command Sent&Received lastCommandSent int This is a combination of the command number (commands.h) multiplied by 100. For every subsequent call, the number is incremented by 1.For example, if the return home command was called 5 times, this variable would be equal to: 4105
Error Codes errorCodes Unsigned int Signals any problems that may be occurring or have occurred.This value is retrieved from StartupErrorCodes.c. The possible values are (and any binary combination):0b0000000000000000: No Errors0b0000000000000001:Power on reset occurred.0b0000000000000010:Brown out reset occurred.0b0000000000000100:Idle Mode Reset Occurred.0b0000000000001000:Sleep Mode Reset Occurred.0b0000000000010000:Software Watch Dog Timer Reset Occurred.0b0000000000100000:Software Reset Occurred.0b0000000001000000:External Reset Occurred.0b0000000010000000:Voltage Regulator Reset Occurred.0b0000000100000000:Illegal Opcode Reset Occurred.0b0000001000000000:Trap Reset Occurred.0b1000000000000000:UHF Switch is ON (Can be used to indicate joystick controller connection)
Camera Counter cameraCounter Unsigned Int Every time the camera is triggered, this value increases by one. This allows one to keep track which picture corresponds to what data.
Waypoint Index waypointIndex Char Indicates what waypoint the vehicle is attempting to get to.-1 - indicates that the vehicle is going "HOME"Any other value indicates the waypoint in the order that it was added.
Controller Status Indicator Editing_gain Char An indicator that depicts which gain values are currently being changed.0x00 = Manual Mode0x01 = Yaw0x02 = Pitch0x03 = Roll0x04 = Heading0x05 = Altitude0x06 = Throttle
GPS Status Indicator Gps_status Char An indicator that depicts the number of satellites connected, as well as the status of the gps fix. Format:0x For example:0x00 = No GPS Fix, 0 Satellites0x1A = GPS Fix, 10 Satellites0x24 = DGPS Fix, 4 Satellites
Battery Level Indicator batteryLevel Char This indicator provides the battery level as a percentage (%) of the original battery capacity specified in the voltageSensor.c/h files.

Note that all telemetry data must be visible within the scope of the AttitudeManager.c file.

Command (Uplink) Data

Every command that is sent to the UAV must be predefined with an ID, as well as an associated function. Some commands only change variable values, whereas some call functions with the associated data as a parameter.

Command ID Socket Command Associated Function Format Description
0 debug: UART1_SendString() Char Array The debugging command, which writes to the UART1 port.
1 set_pitchKDGain: setGain(PITCH, KD_GAIN, ) Float The command to set the derivative gain for pitch control.
2 set_rollKDGain: setGain(ROLL, KD_GAIN, ) Float The command to set the derivative gain for roll control.
3 set_yawKDGain: setGain(YAW, KD_GAIN, ) Float The command to set the derivative gain for yaw control.
4 set_pitchKPGain: setGain(PITCH, KP_GAIN, ) Float The command to set the proportional gain for pitch control.
5 set_rollKPGain: setGain(ROLL, KP_GAIN, ) Float The command to set the proportional gain for roll control.
6 set_yawKPGain: setGain(YAW, KP_GAIN, ) Float The command to set the proportional gain for yaw control.
7 set_pitchKIGain: setGain(PITCH, KI_GAIN, ) Float The command to set the integral gain for pitch control.
8 set_rollKIGain: setGain(ROLL, KI_GAIN, ) Float The command to set the proportional gain for roll control.
9 set_yawKIGain: setGain(YAW, KI_GAIN, ) Float The command to set the proportional gain for yaw control.
10 set_headingKDGain: setGain(HEADING, KD_GAIN, ) Float The command to set the derivative gain for heading control.
11 set_headingKPGain: setGain(HEADING, KP_GAIN, ) Float The command to set the proportional gain for heading control.
12 set_headingKIGain: setGain(HEADING, KI_GAIN, ) Float The command to set the integral gain for heading control.
13 set_altitudeKDGain: setGain(ALTITUDE, KD_GAIN, ) Float The command to set the derivative gain for altitude control.
14 set_altitudeKPGain: setGain(ALTITUDE, KP_GAIN, ) Float The command to set the proportional gain for altitude control.
15 set_altitudeKIGain: setGain(ALTITUDE, KI_GAIN, ) Float The command to set the integral gain for altitude control.
16 set_throttleKDGain: setGain(THROTTLE, KD_GAIN, ) Float The command to set the derivative gain for throttle control (speed).
17 set_throttleKPGain: setGain(THROTTLE, KP_GAIN, ) Float The command to set the derivative gain for throttle control (speed).
18 set_throttleKIGain: setGain(THROTTLE, KI_GAIN, ) Float The command to set the integral gain for throttle control (speed).
19 set_pathGain: <Currently Unsupported, but will not cause errors if set> Float The command used to set the gain that scales lateral positional control around a path.
20 set_orbitGain: <Currently Unsupported, but will not cause errors if set> Float The command used to set the gain that scales orbital convergence.
21 set_showGain: displayGain = Char The command used to switch the output between multiple gain types:0x00 = Yaw0x01 = Pitch0x02 = Roll0x03 = Heading0x04 = Altitude0x05 = Throttle*Note that there is no support for the path gain or orbital gain yet.
22 set_pitchRate: sp_PitchRate = Int The user input for the pitch rate in PWM timer tick units. (Normal values range from 470 to 941 [dependent on setup]). Note you must set command 32 greater than 4 to use this.
23 set_rollRate: sp_RollRate = Int The user input for the roll rate in PWM timer tick units. (Normal values range from 470 to 941 [dependent on setup]) Note you must set command 32 greater than 4 to use this.
24 set_yawRate: sp_YawRate = Int The user input for the roll rate in PWM timer tick units. (Normal values range from 470 to 941 [dependent on setup]) Note you must set command 32 greater than 4 to use this.
25 set_pitchAngle: sp_PitchAngle = Float The user input for the pitch angle in degrees. Note you must set command 32 greater than 5 to use this.
26 set_rollAngle: sp_RollAngle = Float The user input for the roll angle in degrees. Note you must set command 32 greater than 5 to use this.
27 set_yawAngle: <Currently Unsupported, but will not cause errors if set>sp_YawAngle = Float The user input for the yaw angle in degrees. WILL LIKELY BE REMOVED IN THE FUTURE IF UNEEDED. Note you must set command 32 greater than 5 to use this.
28 set_altitude: sp_Altitude = Float The user input for the altitude in meters above sea level. Note you must set command 32 greater than 6 to use this.
29 set_heading: <sp_Heading> = Float The user input for the heading in standard compass bearing degrees. Note you must set command 32 greater than 7 to use this.
30 set_throttle: <sp_Throttle> = Int The user input for the throttle as a percentage. Note you must set command 32 to 8 to use this.
31 set_autonomousLevel: controlLevel = Int This sets the source of control input between the autopilot, the remote control, and the ground stations:0b00000000 = Full manual control (default)0b00000001 = Set Pitch Rate(0), Pitch Angle(1)0b00000010 = Pitch Control Source: Controller(0), Ground Station(1) 0b00000100 = Roll Control Type: Roll Rate(0), Roll Angle(1)0b00001000 = Roll Control Sources: Controller(0), Ground Station(1)0b00110000 = Throttle control source: Controller(0), Ground Station(1), Autopilot(2) 0b01000000 = Altitude Source: Ground Station(0), Autopilot(1)0b10000000 = Altitude Control On(1) or Off(0)0b100000000 = Heading control source: Ground Station(0), Autopilot(1)0b1000000000= To fly with Ground Station Control of the Pitch Rate and Roll Angle:set_autonomousLevel:14To fly with Ground Station Control of the Pitch Rate, Roll Angle, and Throttle:set_autonomousLevel:30To fly with Ground Station Control of Altitude, and Throttle (Roll controlled by controller):set_autonomousLevel:134To fly with Ground Station Control of Altitude, Throttle, Roll Angle:set_autonomousLevel:156To reset everything and fly with controller:set_autonomousLevel:0
32 set_angularWalkVariance: setAngularWalkVariance() Float Sets the Kalman Filter parameter that determines how fast the gyro bias estimates converge.
33 Set_gyroVariance: setGyroVariance() Float Sets the Kalman filter parameter that determines the weighting of the gryo in the attitude estimates of the plane.
34 set_magneticVariance: setMagneticVariance() Float Sets the Kalman filter parameter that determines the weighting of the magnetometers in the attitude estimates of the plane.
35 set_accelVariance: setAccelVariance() Float Sets the Kalman filter parameter that determines the weighting of the accelerometers in the attitude estimates of the plane.
36 set_scaleFactor: pitchScaleFactor = Float Sets the value for the feed-forward term of pitch, when the aircraft is turning. In other words, when the aircraft is turning, this proportion is added to the elevators to prevent the airplane from losing altitude.
37 calibrate_altimeter: amData.calibrationHeight = amData.command = PM_CALIBRATE_ALTIMETER Float This sets the reference height on the altimeter to a predefined value. This allows one to choose a relative value for the height aircraft. For example, one may set 0m to refer to the starting or landing terrain height.
38 clear_waypoints: amData.waypoint.id = amData.command = PM_CLEAR_WAYPOINTS Byte This command clears ALL waypoints. The is just a dummy variable.
39 remove_waypoint: amData.waypoint.id = amData.command = PM_REMOVE_WAYPOINT Byte This command removes a specific waypoint given a specific ID as the parameter.
40 set_targetWaypoint: amData.waypoint.id = amData.command = PM_SET_TARGET_WAYPOINT Byte The target waypoint is the waypoint which the UAV is trying to currently get to. If this command is called, it can be used to skip waypoints, or return to waypoints. The is the specified ID for the new target.
41 return_home: amData.command = PM_RETURN_HOME Byte This tells the plane to go to the "home" coordinates. The is just a dummy variable.
42 cancel_returnHome: amData.command = PM_CANCEL_RETURN_HOME Byte This tells the plane to return back to its original path after being called to the "home" coordinates.
43 send_heartbeat: heartbeatTimer = time Byte This sends a "heartbeat" (verification ping) to the plane to tell it that a data connection is still present. If this command is not received after a certain amount of time, emergency maneuvers will be used.
44 trigger_camera: triggerCamera() Int This manually triggers the camera via a "fake" PWM signal. The is the integer value of the PWM signal.
45 set_triggerDistance: setTriggerDistance() Float This sets the trigger distance (how often a picture is taken based on distance). This is a value in meters.
46 set_gimbleOffset: setGimbleOffset() Int This provides an offset to the gimbal. If the gimbal is misaligned on start up, this function can correct it.
47 kill_plane: if ( == 1234) killingPlane = 1; Int This crashes the plane into the ground (in emergencies). This requires a password ( = "1234") to ensure this isn't an accident.
48 unkill_plane: if ( == 1234) killingPlane = 0; Int This changes the state of the plane from "I'm crashing" to "Nevermind, this was just a test". This requires a password ( = "1234") to ensure this isn't an accident.
128 new_waypoint:,,, amData.command = PM_NEW_WAYPOINTamData.waypoint = 4 floats This uploads and appends a waypoint to the aircraft based on corresponding gps coordinates and path instructions. The format is (longitude, latitude, altitude,radius).
129 insert_Waypoint: amData.command = PM_INSERT_WAYPOINT amData.waypoint = 4 floats followed by 2 Bytes This uploads and inserts a waypoint to the aircraft based on corresponding gps coordinates and path instructions. The format is (longitude, latitude, altitude,radius,nextID,previousID).
130 set_ReturnHomeCoordinates: amData.command = PM_SET_RETURN_HOME_COORDINATESamData.waypoint = 3 Floats This sets the home coordinates, to which the plane will return in case of an emergency. The format is (longitude, latitude, altitude).
131 tare_IMU:,, adjustVNOrientationMatrix(float* adjustment); 3 Floats This adds a bias adjustment to the matrix based on the last setting. The 3 data values are the x,y,z components of the aircraft.
132 Set_IMU:,, setVNOrientationMatrix(float* adjustment); 3 Floats This is used to set the reference frame of the aircraft's IMU unit. The input values are the x,y,z values of the IMU's rotation respectively.

In the code

Prior to usage, the datalink must be initialized. This is done so in _main.c _using initDataLink(). This simply initializes the UART2 interface (see UART section) for appropriate usage with the datalink.

After initialization, the data link can be used. The interface used to queue data to the datalink is present in the AttitudeManager.c file.

In the code – Downlink/Telemetry

Data is exported to the data link at a certain frequency (according to a clock). This is done by calling writeDatalink(frequency), where frequency is the time between packets. This subroutine creates a structure (defined in net.h) which contains memory locations for every variable. This data is then pushed to be processed in net_outbound.c.

if (time - lastTime > frequency) {

    lastTime = time;

    struct telem_block* statusData = createTelemetryBlock();

    statusData->lat = gps_Latitude;

    statusData->lon = gps_Longitude;

    ...

return pushOutboundTelemetryQueue(statusData);

}

When all the data is assembled in the struct, pushOutboundTelemetryQueue(statusData) is called. This pushes the data onto a queue to be processed later:

int pushOutboundTelemetryQueue(struct telem_block *telem) {

    if (getOutboundQueueLength() >= OUTBOUND_QUEUE_SIZE) {

        return -1;

    }

    outBuffer[outbuff_end] = telem;

    outbuff_end++;

    outbuff_end = outbuff_end % OUTBOUND_QUEUE_SIZE;

    return getOutboundQueueLength();

}

Note that this is a circular buffer. When the buffer reaches the OUTBOUND_QUEUE_SIZE, the outbuff_end variable starts from 0 and overwrites the old data.

Every once in a while, the data accumulated must be processed. As a result, every iteration of the program runs a subroutine to maintain and cleanup the circular buffer. For the outgoing buffer, this method is outboundBufferMaintenance():

if ( stagingBuffer.sendIndex >= PACKET_LENGTH ) {

    destroyTelemetryBlock(stagingBuffer.telemetry.asStruct);

    if ( getOutboundQueueLength() ) {

        stageTelemetryBlock(popOutboundTelemetryQueue());

    }

} else if ( stagingBuffer.telemetry.asStruct == 0 && getOutboundQueueLength() ) {

    stageTelemetryBlock(popOutboundTelemetryQueue());

}

Note that the structure of stagingBuffer is as follows:

struct telem_buffer {

    unsigned int sendIndex;             // index into telemetry to send

    unsigned char header[API_HEADER_LENGTH];    // The header for the telem

    union {

        struct telem_block *asStruct;   // The telemetry block being sent

        unsigned char *asArray;         // The telemetry intepreted as an array

    } telemetry;

    unsigned char checksum;             // The checksum so far

};

Note that the _stagingBuffer _converts the data into a data link friendly format. The data link hardware requires that each data packet must be sent with a header, the data, and a checksum (For more specification see the XBEE section). These are 3 components of the telem_buffer structure. The 4th component is the sendIndex variable. This value is used to keep track (index) what data has already been sent or still needs to be sent.

After sufficient error checking (making sure sendIndex is less than the allowed packet size), stageTelemetryBlock(popOutboundTelemetryQueue()) is called. This method takes (pops) the next struct of data and stages it to be sent. stageTelemetryBlock() is responsible for converting the telemetry data into a telem_buffer structure.

void stageTelemetryBlock(struct telem_block *telem) {

    stagingBuffer.telemetry.asStruct = telem;

    generateApiHeader(stagingBuffer.header, 0);

    stagingBuffer.checksum = 0;

    // Send index should be reset last for reasons

    stagingBuffer.sendIndex = 0;

    sendNextByte();

}

The first line of the subroutine adds the data into the packet. The second line (generateApiHeader(stagingBuffer.header,0)) creates an appropriate header in the stagingBuffer.header memory address with a data frame of 0. (See the XBEE section for the datasheet). The API header includes information involving which device the packet should be sent to, the length of the packet, as well as acknowledgement options, and packet types (data packet, configuration packet, status packet). After the checksum and sendIndex are explicitly reset, the sending process begins with sendNextByte():

void sendNextByte(void) {

    unsigned char sendByte; // The byte to send

    if ( stagingBuffer.sendIndex < API_HEADER_LENGTH ) {

        //while (U2STAbits.TRMT == 0);

        sendByte = stagingBuffer.header[stagingBuffer.sendIndex] & 0xFF;

        // Compute checksum

        if (stagingBuffer.sendIndex >= 3) {

            stagingBuffer.checksum += sendByte & 0xFF;

        }

    } else if ( stagingBuffer.sendIndex < PACKET_LENGTH - 1 ) {

        sendByte = stagingBuffer.telemetry.asArray[stagingBuffer.sendIndex - API_HEADER_LENGTH] & 0xFF;

        stagingBuffer.checksum += sendByte & 0xFF;

    } else if ( stagingBuffer.sendIndex == PACKET_LENGTH - 1) {

        sendByte = 0xFF - (stagingBuffer.checksum & 0xFF);

    } else {

        IFS1bits.U2TXIF = 0;

        return;

    }

    stagingBuffer.sendIndex++;

    IFS1bits.U2TXIF = 0;

    U2TXREG = sendByte;

}

All the "if" statements above, compile the header, the data and the checksum together. Note that the checksum is the bitwise inverse of the actual sum: sendByte = 0xFF - (stagingBuffer.checksum & 0xFF). The most important part of this process is the last line, where each byte is sent to the UART transmit buffer. Since the UART transmit process is interrupt-based, each interrupt keeps calling sendNextByte(), until there is no more data left:

void __attribute__((__interrupt__, no_auto_psv)) _U2TXInterrupt(void) {

    // Short circuit if nothing in the staging area yet

    if ( stagingBuffer.telemetry.asStruct == 0 ) {

        IFS1bits.U2TXIF = 0;

        return;

    }

    sendNextByte();

}

The process can be described through this flowchart:

Downlink Flowchart

In the code – Uplink

Once every iteration, a command is read from the uplink queue. This is done by calling readDatalink(). The command popCommand() is called. If any new commands have been received, popCommand() will return a command struct (defined in net.h):

struct command {

    unsigned char cmd;

    unsigned char data\_length;

    unsigned char data[101];

};

It is fairly straight forward. The structure contains a _cmd.cmd _which indicates the command ID. This ID corresponds to a certain function that needs to be completed. Following the pop command are a series of case statements (one for each command ID). For instance, if the command ID is 30, the following command is run (in net_inbound.c):

struct command* cmd = popCommand();

//TODO: Add rudimentary input validation

if ( cmd ) {

    if (lastCommandSentCode == cmd->cmd){

        lastCommandSentCode++;

    }

    else{

        lastCommandSentCode = cmd->cmd * 100;

    }

    switch (cmd->cmd) {

        ...

        case SET_THROTTLE:

            sp_ThrottleRate = (int)(*(int*)(&cmd->data) / 100.0  * (890 - 454) + 454);

            break;

        ...
    }

}

The throttle ends up being set to the value indicated in the cmd.data location. In addition, the last command read is stored and sent to the ground station as verification that the command was received.

The _popCommand() _function waits and reads the next available command from a circular buffer (note the INBOUND_QUEUE_SIZE variable). If the command doesn't exist, it exits the function.

In order for the command structure to exist, the U2RXInterrupt must have been triggered. This occurs when new data is sent.

void __attribute__((__interrupt__, no_auto_psv)) _U2RXInterrupt(void) {

    unsigned char data = U2RXREG;

    if ( rawPacketStatus[packetPos] != BUSY ) {    // no buffer available to write

        packetPos = ( packetPos + 1  ) % RAW_PACKET_BUFFER_SIZE;

        IFS1bits.U2RXIF = 0;

        return;

    }

    switch ( payloadPos ) {

        case 0:

            if ( data != START_DELIMITER ) {

                IFS1bits.U2RXIF = 0;

                return;

            }

            break;

        case 1:

            if ( data != 0 ) {

                payloadPos = 0;

                IFS1bits.U2RXIF = 0;

                return;                 // packet length < 100 bytes, so msb == 0

            }

            break;

        case 2:

            payloadLength[packetPos] = data;

            break;

        default:        // Normally, don't do anything special

            break;

    }

    rawPackets[packetPos][payloadPos++] = data;

    if ( payloadPos && payloadPos == payloadLength[packetPos] + 3 + 1) {   // at end of packet

        rawPacketStatus[packetPos] = READY;

        payloadPos = 0;

        packetPos = ( packetPos + 1  ) % RAW_PACKET_BUFFER_SIZE;

        if ( rawPacketStatus[packetPos] == EMPTY ) {

            rawPacketStatus[packetPos] = BUSY;

        }

    }

    IFS1bits.U2RXIF = 0;

}

The first thing that occurs when new data arrives is a check to see if there is enough memory to store the data. A if statement is used to check if the buffer (rawPacketStatus) is busy or not. Note that the interrupt will only record the data, if the current packetPos marked busy.

If the buffer is full, the next buffer location is checked. Otherwise, the packet is parsed byte by byte.

Firstly, the start delimiter is looked for using a case statement. Until the start delimiter is found, nothing happens. Secondly, for case 1 and 2, the length of the packet is check and recorded. Once the length of the packet is known, the data is read into a 2d array called rawPackets. This array contains each byte of every packet in the circular buffer. Once all the data is copied into the array, the packet is marked as READY, and the next one is marked BUSY if it is EMPTY, and the processing of the data begins on the next maintenance cycle when inboundBufferMaintenance() is called from main.c:

void inboundBufferMaintenance(void) {

    int i;

    for ( i = 0; i < RAW_PACKET_BUFFER_SIZE; i++ ) {

        if ( rawPacketStatus[i] == READY && checkPacket(rawPackets[i]) ) {

            struct command\* cmd = createCommand( rawPackets[i] );

            if ( cmd ) {            // create command was successful ?

                pushCommand( cmd ); // queue it up

                rawPacketStatus[i] = EMPTY;         // buffer is now good for writing another packet

            }

        }

    }

    if ( rawPacketStatus[0] == EMPTY ) {

        rawPacketStatus[0] = BUSY;

    }

}

This subroutine iterates through each buffer location and checks for any READY packets. If one is found, and it has been verified through a checksum, the command structure is created from the data using the createCommand(rawPackets[i]) method. Once this is done, the previous spot is marked EMPTY. The first buffer is always primed if it is empty.

Uplink Flowchart

⚠️ **GitHub.com Fallback** ⚠️