From b1d23a3b44e607b951af595386ab06e3e3ab9251 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 08:42:19 -0600 Subject: [PATCH 001/594] Add bad packet testing --- Firmware/LoRaSerial_Firmware/States.ino | 11 ++++++++++- Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 89005e1a..2539c2a9 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -366,7 +366,11 @@ void updateRadioState() default: triggerEvent(TRIGGER_UNKNOWN_PACKET); returnToReceiving(); - changeState(RADIO_P2P_LINK_UP); + break; + + case PACKET_BAD: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); break; case DATAGRAM_PING: @@ -592,6 +596,11 @@ void updateRadioState() returnToReceiving(); break; + case PACKET_BAD: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + case DATAGRAM_PING: //Break the link v2BreakLink(); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 4ca2b2f0..84b5eccb 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -148,6 +148,7 @@ enum TRIGGER_UNKNOWN_PACKET, TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND, TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, + TRIGGER_BAD_PACKET, TRIGGER_ACK_PROCESSED, From 48633b11d0f71f9a86458200b7ec2ba02f0d454c Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 10:15:44 -0600 Subject: [PATCH 002/594] Correct datagram count handling rxAckNumber was being overwritten in rcvDatagram() regardless of gram type. Reduce number of counters to two (ack and everything else). --- .../LoRaSerial_Firmware.ino | 5 +- Firmware/LoRaSerial_Firmware/RadioV2.ino | 52 +++++++++++-------- Firmware/LoRaSerial_Firmware/States.ino | 22 +++++--- 3 files changed, 45 insertions(+), 34 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 065d16fe..c240804b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -204,8 +204,7 @@ uint8_t txDatagramSize; //Point-to-Point unsigned long datagramTimer; -uint8_t expectedTxAck; -uint8_t txAckNumber; +uint8_t expectedDatagramNumber; uint16_t ackAirTime; uint16_t datagramAirTime; uint16_t overheadTime = 10; //ms added to ack and datagram times before ACK timeout occurs @@ -216,7 +215,7 @@ uint16_t heartbeatRandomTime; uint8_t incomingBuffer[MAX_PACKET_SIZE]; uint8_t minDatagramSize; uint8_t maxDatagramSize; -uint8_t rxAckNumber; +uint8_t expectedAckNumber; uint8_t * rxData; uint8_t rxDataBytes; unsigned long heartbeatTimer; diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d6916888..509a5b5c 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -134,8 +134,8 @@ void xmitDatagramP2PCommand() */ txControl.datagramType = DATAGRAM_REMOTE_COMMAND; - txControl.ackNumber = txAckNumber; - txAckNumber = (txAckNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; + txControl.ackNumber = expectedDatagramNumber; + expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; packetSent = 0; //This is the first time this packet is being sent transmitDatagram(); } @@ -155,8 +155,8 @@ void xmitDatagramP2PCommandResponse() */ txControl.datagramType = DATAGRAM_REMOTE_COMMAND_RESPONSE; - txControl.ackNumber = txAckNumber; - txAckNumber = (txAckNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; + txControl.ackNumber = expectedDatagramNumber; + expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; packetSent = 0; //This is the first time this packet is being sent transmitDatagram(); } @@ -176,8 +176,8 @@ void xmitDatagramP2PData() */ txControl.datagramType = DATAGRAM_DATA; - txControl.ackNumber = txAckNumber; - txAckNumber = (txAckNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; + txControl.ackNumber = expectedDatagramNumber; + expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; packetSent = 0; //This is the first time this packet is being sent transmitDatagram(); } @@ -197,8 +197,8 @@ void xmitDatagramP2PHeartbeat() */ txControl.datagramType = DATAGRAM_HEARTBEAT; - txControl.ackNumber = txAckNumber; - txAckNumber = (txAckNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; + txControl.ackNumber = expectedDatagramNumber; + expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; packetSent = 0; //This is the first time this packet is being sent transmitDatagram(); } @@ -222,7 +222,8 @@ void xmitDatagramP2PAck() */ txControl.datagramType = DATAGRAM_DATA_ACK; - txControl.ackNumber = rxAckNumber; + txControl.ackNumber = expectedAckNumber; + expectedAckNumber = (expectedAckNumber + 1) & 3; transmitDatagram(); } @@ -257,7 +258,6 @@ void xmitDatagramMpDatagram() PacketType rcvDatagram() { PacketType datagramType; - static uint8_t expectedRxAck; uint8_t receivedNetID; CONTROL_U8 rxControl; @@ -375,7 +375,7 @@ PacketType rcvDatagram() //Get the control byte rxControl = *((CONTROL_U8 *)rxData++); datagramType = rxControl.datagramType; - rxAckNumber = rxControl.ackNumber; + uint8_t packetNumber = rxControl.ackNumber; if (settings.debugReceive) printControl(*((uint8_t *)&rxControl)); if (datagramType >= MAX_DATAGRAM_TYPE) @@ -415,9 +415,7 @@ PacketType rcvDatagram() } rxDataBytes -= minDatagramSize; - //Verify the ACK number last so that the expected ACK number can be updated - // txAckNumber ----> rxAckNumber == expectedTxAck - // expectedTxAck == rxAckNumber <---- txAckNumber + //Verify the packet number last so that the expected datagram or ACK number can be updated if (settings.pointToPoint) { switch (datagramType) @@ -426,21 +424,21 @@ PacketType rcvDatagram() break; case DATAGRAM_DATA_ACK: - if (rxAckNumber != expectedTxAck) + if (packetNumber != expectedAckNumber) { if (settings.debugReceive) { systemPrintTimestamp(); systemPrint("Invalid ACK number, received "); - systemPrint(rxAckNumber); + systemPrint(packetNumber); systemPrint(" expecting "); - systemPrintln(expectedTxAck); + systemPrintln(expectedAckNumber); } return (PACKET_BAD); } //Increment the expected ACK number - expectedTxAck = (expectedTxAck + 1) & 3; + expectedAckNumber = (expectedAckNumber + 1) & 3; break; case DATAGRAM_DATA: @@ -448,21 +446,29 @@ PacketType rcvDatagram() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (rxAckNumber != expectedRxAck) + if (packetNumber != expectedDatagramNumber) { //Determine if this is a duplicate datagram - if (rxAckNumber == ((expectedRxAck - 1) & 3)) + if (packetNumber == ((expectedDatagramNumber - 1) & 3)) { linkDownTimer = millis(); return PACKET_DUPLICATE; } //Not a duplicate + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Invalid datagram number, received "); + systemPrint(packetNumber); + systemPrint(" expecting "); + systemPrintln(expectedDatagramNumber); + } return PACKET_BAD; } - //Receive this data packet and set the next expected RX ACK number - expectedRxAck = (expectedRxAck + 1) & 3; + //Receive this data packet and set the next expected datagram number + expectedDatagramNumber = (expectedDatagramNumber + 1) & 3; break; } } @@ -515,7 +521,7 @@ PacketType rcvDatagram() if (settings.pointToPoint) { systemPrint(" (ACK #"); - systemPrint(rxAckNumber); + systemPrint(packetNumber); systemPrint(")"); } systemPrintln(); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 2539c2a9..1d79a8bb 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -29,7 +29,6 @@ void updateRadioState() //Set all of the ACK numbers to zero *(uint8_t *)(&txControl) = 0; - *(uint8_t *)(&expectedTxAck) = 0; //Determine the components of the frame header headerBytes = 0; @@ -399,7 +398,11 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); + + if (expectedAckNumber == 0) expectedAckNumber = 3; //Reduce eAN by one to align with remote's count + else expectedAckNumber--; xmitDatagramP2PAck(); //Transmit ACK + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); break; @@ -715,9 +718,9 @@ void updateRadioState() } if (receiveInProcess() == false) { - retransmitDatagram(); - packetSent++; - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + retransmitDatagram(); + packetSent++; + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } } else @@ -793,7 +796,7 @@ void updateRadioState() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= case RADIO_MP_STANDBY: -timeToHop = false; + timeToHop = false; if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived { triggerEvent(TRIGGER_BROADCAST_PACKET_RECEIVED); @@ -843,7 +846,7 @@ timeToHop = false; break; case RADIO_MP_WAIT_TX_DONE: -timeToHop = false; + timeToHop = false; if (transactionComplete == true) //If dio0ISR has fired, we are done transmitting { transactionComplete = false; //Reset ISR flag @@ -857,7 +860,7 @@ timeToHop = false; case RADIO_MP_RECEIVE: { -timeToHop = false; + timeToHop = false; //Decode the received datagram PacketType packetType = rcvDatagram(); @@ -1845,9 +1848,12 @@ void v2EnterLinkUp() triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); startChannelTimer(); hopChannel(); //Leave home + heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% //Synchronize the ACK numbers - expectedTxAck = txControl.ackNumber; + txControl.ackNumber = 0; + expectedAckNumber = 0; + expectedDatagramNumber = 0; //Discard any previous data rxTail = rxHead; From da90168693e9225879cd14afcb4c2363219167a2 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 10:17:49 -0600 Subject: [PATCH 003/594] Add heartbeat update after clock sync After successful clock sync, push out next heartbeat. This happens after a heartbeat ack, but now pushes out the next heartbeat after anything that generates an ack (data, etc). --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 509a5b5c..9dfe7852 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -819,4 +819,7 @@ void syncChannelTimer() channelTimer.disableTimer(); channelTimer.setInterval_MS(settings.maxDwellTime - channelTimerElapsed, channelTimerHandler); //Shorten our hardware timer to match our mate's channelTimer.enableTimer(); + + //Extend time before next heartbeat + heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% } From e10b77c40085df7f10c9e8b7e2855d8899bd953d Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 10:17:59 -0600 Subject: [PATCH 004/594] Remove test code --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 11 ----------- Firmware/LoRaSerial_Firmware/States.ino | 3 ++- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index c240804b..fe6a1651 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -280,17 +280,6 @@ void setup() systemPrintTimestamp(); systemPrintln("LRS Setup Complete"); - //Testing - settings.triggerEnable = 0xFFFFFFFF; //Enable all - settings.debugTrigger = true; - settings.debug = true; - //settings.printFrequency = true; - //settings.debugTransmit = true; - //settings.debugReceive = true; - settings.triggerWidth = 25; - settings.useV2 = true; - //settings.printTimestamp = true; - triggerEvent(TRIGGER_RADIO_RESET); } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 1d79a8bb..56de0b8d 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -625,10 +625,11 @@ void updateRadioState() systemPrintln(); } - triggerEvent(TRIGGER_LINK_ACK_RECEIVED); packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + triggerEvent(TRIGGER_LINK_ACK_RECEIVED); returnToReceiving(); changeState(RADIO_P2P_LINK_UP); break; From 38c5224111f68f036bb30cf95dd0dc285fcb3052 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 7 Oct 2022 08:10:10 -1000 Subject: [PATCH 005/594] V2: Properly save and restore the last transmit state --- Firmware/LoRaSerial_Firmware/States.ino | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 56de0b8d..ca516b9a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -6,6 +6,7 @@ void updateRadioState() uint8_t radioSeed; static uint8_t rexmtBuffer[MAX_PACKET_SIZE]; static CONTROL_U8 rexmtControl; + static uint8_t rexmtLength; switch (radioState) { @@ -661,6 +662,7 @@ void updateRadioState() petWDT(); memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); rexmtControl = txControl; + rexmtLength = txDatagramSize; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); xmitDatagramP2PAck(); //Transmit ACK @@ -674,13 +676,6 @@ void updateRadioState() else if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + overheadTime)) //Set at end of transmit, measures ACK timeout { - // systemPrint("millis: "); - // systemPrintln(millis()); - // systemPrint("datagramTimer: "); - // systemPrintln(datagramTimer); - // systemPrint("ackAirTime: "); - // systemPrintln(ackAirTime); - if (settings.debugDatagrams) { systemPrintTimestamp(); @@ -751,6 +746,7 @@ void updateRadioState() { memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); txControl = rexmtControl; + txDatagramSize = rexmtLength; if (settings.debugDatagrams) { systemPrintTimestamp(); From 465784f16fe77f306700ff077a2269246711f663 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 7 Oct 2022 08:48:16 -1000 Subject: [PATCH 006/594] V2: Move overheadTime into settings --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/States.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/settings.h | 4 ++-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 1fa1e80d..9902118f 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -505,7 +505,7 @@ const COMMAND_ENTRY commands[] = {45, 0, 1, 0, TYPE_BOOL, valInt, "UseV2", &settings.useV2}, {46, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, {47, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, - {48, 5, 1000, 0, TYPE_U16, valInt, "TxAckMillis", &settings.txAckMillis}, + {48, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, //Define any user parameters starting at 255 decrementing towards 0 }; diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index fe6a1651..a0f7e097 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -207,7 +207,6 @@ unsigned long datagramTimer; uint8_t expectedDatagramNumber; uint16_t ackAirTime; uint16_t datagramAirTime; -uint16_t overheadTime = 10; //ms added to ack and datagram times before ACK timeout occurs uint16_t pingRandomTime; uint16_t heartbeatRandomTime; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 56de0b8d..66950a25 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -230,7 +230,7 @@ void updateRadioState() } else { - if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + overheadTime)) + if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + settings.overheadTime)) { if (settings.debugDatagrams) { @@ -274,7 +274,7 @@ void updateRadioState() } else { - if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + overheadTime)) + if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + settings.overheadTime)) { if (settings.debugDatagrams) { @@ -402,7 +402,7 @@ void updateRadioState() if (expectedAckNumber == 0) expectedAckNumber = 3; //Reduce eAN by one to align with remote's count else expectedAckNumber--; xmitDatagramP2PAck(); //Transmit ACK - + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); break; @@ -671,7 +671,7 @@ void updateRadioState() } //Check for ACK timeout - else if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + overheadTime)) + else if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + settings.overheadTime)) //Set at end of transmit, measures ACK timeout { // systemPrint("millis: "); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 84b5eccb..1fb36bac 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -149,7 +149,7 @@ enum TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND, TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, TRIGGER_BAD_PACKET, - + TRIGGER_ACK_PROCESSED, TRIGGER_DATA_SEND, @@ -274,7 +274,7 @@ typedef struct struct_settings { bool useV2 = false; //Use the V2 protocol bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds bool debugDatagrams = false; //Print the datagrams - uint16_t txAckMillis = 100; //Number of milliseconds to delay for ACK + uint16_t overheadTime = 10; ////ms added to ack and datagram times before ACK timeout occurs } Settings; Settings settings; From 78a522f26c35daf49528019f4873780f78b572ef Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 14:06:50 -0600 Subject: [PATCH 007/594] Trim clock sync overhead time Brings units to ~0.5ms offset --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index fe6a1651..737b0b50 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -119,7 +119,7 @@ const int trainWithDefaultsButtonTime = 5000; //ms press and hold before enterin SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 unsigned long timerStart = 0; //Tracks how long our timer has been running since last hop bool partialTimer = false; //After an ACK we reset and run a partial timer to sync units -const int SYNC_PROCESSING_OVERHEAD = 4; //Number of milliseconds it takes to compute clock deltas before sync'ing clocks +const int SYNC_PROCESSING_OVERHEAD = -5; //Number of milliseconds it takes to compute clock deltas before sync'ing clocks //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Global variables - Serial From b0bc2e864f4f982a1b881be2668f333c7071ab28 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 14:07:52 -0600 Subject: [PATCH 008/594] Force reset of timer This fixes a rare timer hang seen when link is re-established --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 9dfe7852..d16ea706 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -791,6 +791,8 @@ void retransmitDatagram() void startChannelTimer() { + channelTimer.disableTimer(); + channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); channelTimer.enableTimer(); timerStart = millis(); //ISR normally takes care of this but allow for correct ACK sync before first ISR triggerEvent(TRIGGER_HOP_TIMER_START); From fd4e0baa328352f1d6c5ca3c1707d3491da70eef Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 14:08:13 -0600 Subject: [PATCH 009/594] Whitespace --- Firmware/LoRaSerial_Firmware/States.ino | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 56de0b8d..677bc803 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -402,7 +402,7 @@ void updateRadioState() if (expectedAckNumber == 0) expectedAckNumber = 3; //Reduce eAN by one to align with remote's count else expectedAckNumber--; xmitDatagramP2PAck(); //Transmit ACK - + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); break; @@ -537,13 +537,16 @@ void updateRadioState() else if (heartbeatTimeout) { triggerEvent(TRIGGER_HEARTBEAT); - xmitDatagramP2PHeartbeat(); + if (receiveInProcess() == false) + { + xmitDatagramP2PHeartbeat(); - heartbeatTimer = millis(); - heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% + heartbeatTimer = millis(); + heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% - //Wait for heartbeat to transmit - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + //Wait for heartbeat to transmit + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } } else if ((millis() - linkDownTimer) >= (3 * settings.heartbeatTimeout)) //Break the link From 15dbe9eda311c6090209dc1fdd4b21ab6b184a5b Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 15:25:00 -0600 Subject: [PATCH 010/594] Reset heartbeat after data or sync --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 9 +++++++-- Firmware/LoRaSerial_Firmware/States.ino | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d16ea706..9f973a5e 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -821,7 +821,12 @@ void syncChannelTimer() channelTimer.disableTimer(); channelTimer.setInterval_MS(settings.maxDwellTime - channelTimerElapsed, channelTimerHandler); //Shorten our hardware timer to match our mate's channelTimer.enableTimer(); +} - //Extend time before next heartbeat - heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% +//This function resets the heartbeat time and re-rolls the random time +//Call when something has happened (ACK received, etc) where clocks have been sync'd +void resetHeartbeat() +{ + heartbeatTimer = millis(); + heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //20-100% } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 677bc803..30b1c327 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -425,6 +425,9 @@ void updateRadioState() packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + resetHeartbeat(); + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); xmitDatagramP2PAck(); //Transmit ACK changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); @@ -459,10 +462,13 @@ void updateRadioState() txHead += rxDataBytes - length; txHead %= sizeof(serialTransmitBuffer); - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + resetHeartbeat(); + + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); xmitDatagramP2PAck(); //Transmit ACK changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); break; @@ -537,12 +543,11 @@ void updateRadioState() else if (heartbeatTimeout) { triggerEvent(TRIGGER_HEARTBEAT); - if (receiveInProcess() == false) + if (receiveInProcess() == false && transactionComplete == false) //Avoid race condition { xmitDatagramP2PHeartbeat(); - heartbeatTimer = millis(); - heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% + resetHeartbeat(); //Wait for heartbeat to transmit changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); @@ -615,6 +620,8 @@ void updateRadioState() case DATAGRAM_DATA_ACK: syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + resetHeartbeat(); //Extend time before next heartbeat + //Display the signal strength if (settings.displayPacketQuality == true) { @@ -1852,7 +1859,7 @@ void v2EnterLinkUp() triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); startChannelTimer(); hopChannel(); //Leave home - heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% + resetHeartbeat(); //Synchronize the ACK numbers txControl.ackNumber = 0; From e79c8023759dce3081c684768a3ac4626eafb950 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 7 Oct 2022 15:46:43 -0600 Subject: [PATCH 011/594] Increase pings times during handshake --- Firmware/LoRaSerial_Firmware/States.ino | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b1b5c022..ff25aed1 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -26,7 +26,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING heartbeatTimer = millis(); - pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); + pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); //Fast ping //Set all of the ACK numbers to zero *(uint8_t *)(&txControl) = 0; @@ -243,7 +243,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK1_TIMEOUT); heartbeatTimer = millis(); - pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); + pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); //Slow ping changeState(RADIO_P2P_LINK_DOWN); } } @@ -286,7 +286,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK2_TIMEOUT); heartbeatTimer = millis(); - pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); + pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); //Slow ping changeState(RADIO_P2P_LINK_DOWN); } } @@ -792,7 +792,7 @@ void updateRadioState() if (settings.debugDatagrams) systemPrintln("---------- Link DOWN ----------"); heartbeatTimer = millis(); - pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); + pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); //Fast ping changeState(RADIO_P2P_LINK_DOWN); } } From 7d2bc1e4ac935a0cfbcdfda0a8968e83f63738ec Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 8 Oct 2022 06:35:13 -1000 Subject: [PATCH 012/594] V2: Add CRC-16 support Add enableCRC16 setting and ATS49 command --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 139 +++++++++++++++--- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/System.ino | 18 +++ Firmware/LoRaSerial_Firmware/settings.h | 2 +- Firmware/Tools/Command_Validation_Script.txt | 2 +- .../Results/Command_Validation_Results.txt | 20 ++- 7 files changed, 154 insertions(+), 30 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9902118f..ae47e1b3 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -506,6 +506,7 @@ const COMMAND_ENTRY commands[] = {46, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, {47, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, {48, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, + {49, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &settings.enableCRC16}, //Define any user parameters starting at 255 decrementing towards 0 }; diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 9f973a5e..01f9d854 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1,3 +1,39 @@ +const uint16_t crc16Table[256] = +{ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, + 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, + 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, + 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, + 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, + 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, + 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, + 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, + 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, + 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, + 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, + 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, + 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, + 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, + 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, + 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, + 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, + 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, + 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, + 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, + 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, + 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, + 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 +}; + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Point-To-Point: Bring up the link // @@ -64,7 +100,7 @@ void xmitDatagramP2PPing() +--------+---------+----------+ | | | | | NET ID | Control | Trailer | - | 8 bits | 8 bits | 8 bits | + | 8 bits | 8 bits | n Bytes | +--------+---------+----------+ */ @@ -87,7 +123,7 @@ void xmitDatagramP2PAck1() +--------+---------+----------+----------+ | | | Channel | Optional | | NET ID | Control | Timer | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | + | 8 bits | 8 bits | 2 bytes | n Bytes | +--------+---------+----------+----------+ */ @@ -106,7 +142,7 @@ void xmitDatagramP2PAck2() +--------+---------+----------+ | | | | | NET ID | Control | Trailer | - | 8 bits | 8 bits | 8 bits | + | 8 bits | 8 bits | n Bytes | +--------+---------+----------+ */ @@ -129,7 +165,7 @@ void xmitDatagramP2PCommand() +--------+---------+--- ... ---+----------+ | | | | Optional | | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | n bytes | n Bytes | +--------+---------+-------------+----------+ */ @@ -150,7 +186,7 @@ void xmitDatagramP2PCommandResponse() +--------+---------+--- ... ---+----------+ | | | | Optional | | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | n bytes | n Bytes | +--------+---------+-------------+----------+ */ @@ -171,7 +207,7 @@ void xmitDatagramP2PData() +--------+---------+--- ... ---+----------+ | | | | Optional | | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | n bytes | n Bytes | +--------+---------+-------------+----------+ */ @@ -192,7 +228,7 @@ void xmitDatagramP2PHeartbeat() +--------+---------+----------+ | | | Optional | | NET ID | Control | Trailer | - | 8 bits | 8 bits | 8 bits | + | 8 bits | 8 bits | n Bytes | +--------+---------+----------+ */ @@ -217,7 +253,7 @@ void xmitDatagramP2PAck() +--------+---------+----------+----------+ | | | Channel | Optional | | NET ID | Control | Timer | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | + | 8 bits | 8 bits | 2 bytes | n Bytes | +--------+---------+----------+----------+ */ @@ -241,7 +277,7 @@ void xmitDatagramMpDatagram() +----------+---------+--- ... ---+----------+ | Optional | | | Optional | | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | n bytes | n Bytes | +----------+---------+-------------+----------+ */ @@ -272,7 +308,7 @@ PacketType rcvDatagram() +----------+----------+------------+--- ... ---+----------+ | Optional | | Optional | | Optional | | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | +----------+----------+------------+-------------+----------+ ^ | @@ -325,15 +361,13 @@ PacketType rcvDatagram() +----------+----------+------------+--- ... ---+----------+ | Optional | | Optional | | Optional | | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | +----------+----------+------------+-------------+----------+ ^ | '---- rxData */ - //Process the trailer - //Verify the netID if necessary if (settings.pointToPoint || settings.verifyRxNetID) { @@ -359,13 +393,44 @@ PacketType rcvDatagram() } } + //Process the trailer + petWDT(); + if (settings.enableCRC16) + { + uint16_t crc; + uint8_t * data; + + //Compute the CRC-16 value + crc = 0xffff; + for (data = incomingBuffer; data < &incomingBuffer[rxDataBytes - 2]; data++) + crc = crc16Table[*data ^ (uint8_t)(crc >> (16 - 8))] ^ (crc << 8); + if ((incomingBuffer[rxDataBytes - 2] != (crc >> 8)) + && (incomingBuffer[rxDataBytes - 1] != (crc & 0xff))) + { + //Display the packet contents + if (settings.printPktData || settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("RX: Bad CRC-16, received 0x"); + systemPrint(incomingBuffer[rxDataBytes - 2], HEX); + systemPrint(incomingBuffer[rxDataBytes - 1], HEX); + systemPrint(" expected 0x"); + systemPrintln(crc, HEX); + petWDT(); + if (settings.printRfData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + } + return (PACKET_BAD); + } + } + /* |<---------------------- rxDataBytes ---------------------->| | | +----------+----------+------------+--- ... ---+----------+ | Optional | | Optional | | Optional | | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | +----------+----------+------------+-------------+----------+ ^ | @@ -381,13 +446,22 @@ PacketType rcvDatagram() if (datagramType >= MAX_DATAGRAM_TYPE) return (PACKET_BAD); + //Display the CRC + if (settings.enableCRC16 && (settings.printPktData || settings.debugReceive)) + { + systemPrintTimestamp(); + systemPrint(" CRC-16: 0x"); + systemPrint(incomingBuffer[rxDataBytes - 2], HEX); + systemPrintln(incomingBuffer[rxDataBytes - 1], HEX); + } + /* |<---------------------- rxDataBytes ---------------------->| | | +----------+----------+------------+--- ... ---+----------+ | Optional | | Optional | | Optional | | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | +----------+----------+------------+-------------+----------+ ^ | @@ -479,7 +553,7 @@ PacketType rcvDatagram() +----------+----------+------------+------ ... ------+----------+ | Optional | | Optional | | Optional | | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | +----------+----------+------------+-------------------+----------+ ^ | @@ -585,7 +659,7 @@ void transmitDatagram() +----------+----------+------------+--- ... ---+----------+ | Optional | | Optional | | Optional | | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | +----------+----------+------------+-------------+----------+ | | | | |<- Length -->| @@ -658,6 +732,9 @@ void transmitDatagram() } /* + endOfTxData ---. + | + V +----------+----------+------------+--- ... ---+ | Optional | | Optional | | | NET ID | Control | SF6 Length | Data | @@ -668,6 +745,18 @@ void transmitDatagram() */ //Add the datagram trailer + if (settings.enableCRC16) + { + uint16_t crc; + uint8_t * txData; + + //Compute the CRC-16 value + crc = 0xffff; + for (txData = outgoingPacket; txData < endOfTxData; txData++) + crc = crc16Table[*txData ^ (uint8_t)(crc >> (16 - 8))] ^ (crc << 8); + *endOfTxData++ = (uint8_t)(crc >> 8); + *endOfTxData++ = (uint8_t)(crc & 0xff); + } txDatagramSize += trailerBytes; //Display the trailer @@ -679,13 +768,25 @@ void transmitDatagram() systemPrint(" (0x"); systemPrint(trailerBytes); systemPrintln(") bytes"); + + //Display the CRC + if (settings.enableCRC16 && (settings.printPktData || settings.debugReceive)) + { + systemPrintTimestamp(); + systemPrint(" CRC-16: 0x"); + systemPrint(endOfTxData[-2], HEX); + systemPrintln(endOfTxData[-1], HEX); + } } /* + endOfTxData ---. + | + V +----------+----------+------------+--- ... ---+----------+ | Optional | | Optional | | Optional | | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | +----------+----------+------------+-------------+----------+ | | |<-------------------- txDatagramSize --------------------->| @@ -737,7 +838,7 @@ void retransmitDatagram() +----------+----------+------------+--- ... ---+----------+ | Optional | | Optional | | Optional | | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | 8 bits | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | +----------+----------+------------+-------------+----------+ | | |<-------------------- txDatagramSize --------------------->| diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ff25aed1..b41009e5 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -49,7 +49,7 @@ void updateRadioState() endOfTxData = &outgoingPacket[headerBytes]; //Determine the size of the trailer - trailerBytes = 0; + trailerBytes = (settings.enableCRC16 ? 2 : 0); radioSeed = radio.randomByte(); //Puts radio into standy-by state randomSeed(radioSeed); diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index e0052a9f..20a12e14 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -64,6 +64,24 @@ void systemPrintln(uint8_t value, uint8_t printType) systemPrint("\r\n"); } +void systemPrint(uint16_t value, uint8_t printType) +{ + char temp[20]; + + if (printType == HEX) + sprintf(temp, "%04X", value); + else if (printType == DEC) + sprintf(temp, "%d", value); + + systemPrint(temp); +} + +void systemPrintln(uint16_t value, uint8_t printType) +{ + systemPrint(value, printType); + systemPrint("\r\n"); +} + void systemPrint(float value, uint8_t decimals) { char temp[20]; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 1fb36bac..918e3f78 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -150,7 +150,6 @@ enum TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, TRIGGER_BAD_PACKET, - TRIGGER_ACK_PROCESSED, TRIGGER_DATA_SEND, TRIGGER_RTR_2BYTE, @@ -275,6 +274,7 @@ typedef struct struct_settings { bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds bool debugDatagrams = false; //Print the datagrams uint16_t overheadTime = 10; ////ms added to ack and datagram times before ACK timeout occurs + bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive } Settings; Settings settings; diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 97951799..926075eb 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -442,7 +442,7 @@ ats48=1 ats48=2 ats48=0 -# displayRealMillis +# enableCRC16 ats49=1 ats49=2 ats49=0 diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index 66d6d67c..ab4d55fe 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -18,6 +18,8 @@ Command summary: ATT - Enter training mode ATX - Stop the training server ATZ - Reboot the radio + AT&F - Restore factory settings + AT&W - Save current settings to NVM ati? ATI0 - Show user settable parameters ATI1 - Show board variant @@ -32,15 +34,15 @@ ati? ERROR ati OK -SparkFun LoRaSerial SAMD21 1W 915MHz v1.1 +SparkFun LoRaSerial SAMD21 1W 915MHz v2.0 ati1 SparkFun LoRaSerial SAMD21 1W 915MHz ati2 -1.1 +2.0 ati3 -157 ati4 -109 +154 ati5 506 ati6 @@ -101,7 +103,7 @@ ATS45:UseV2=0 ATS46:PrintTimestamp=0 ATS47:DebugDatagrams=0 ATS48:TxAckMillis=100 -ATS49:DisplayRealMillis=0 +ATS49:EnableCRC16=0 ats28=0 OK # SerialSpeed @@ -893,15 +895,15 @@ ats48=2 ERROR ats48=0 ERROR -# displayRealMillis +# enableCRC16 ERROR ats49=1 -DisplayRealMillis=1 +EnableCRC16=1 OK ats49=2 ERROR ats49=0 -DisplayRealMillis=0 +EnableCRC16=0 OK # Invalid commands ERROR @@ -928,8 +930,8 @@ ATS32:DebugTraining=0 ATS43:DebugTransmit=0 ATS33:DebugTrigger=0 ATS25:DisplayPacketQuality=0 -ATS49:DisplayRealMillis=0 ATS21:Echo=0 +ATS49:EnableCRC16=0 ATS4:EncryptData=1 ATS5:EncryptionKey=37782141A665734E4475672AE6308308 ATS23:FlowControl=0 @@ -968,3 +970,5 @@ at&w OK atz OK +LRS +LRS Setup Complete From 8245d9ff505807d8d75c1bb4c6ff69419b2a11a0 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 8 Oct 2022 08:41:34 -1000 Subject: [PATCH 013/594] Display space in ASCII dump --- Firmware/LoRaSerial_Firmware/System.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index e0052a9f..bafb1a10 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -412,7 +412,7 @@ void dumpBuffer(uint8_t * data, int length) data -= bytes; for (index = 0; index < bytes; index++) { byte[0] = *data++; - systemPrint(((byte[0] <= ' ') || (byte[0] >= 0x7f)) ? "." : byte); + systemPrint(((byte[0] < ' ') || (byte[0] >= 0x7f)) ? "." : byte); } systemPrintln(); petWDT(); From 63aef1ae9ea70915a15f384c3184451c2a26f7f1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 8 Oct 2022 10:00:08 -1000 Subject: [PATCH 014/594] Compute an offset which will synchronize the timestamps Synchronize the timestamp offsets during receive of: * PING * ACK 1 * ACK 2 * HEARTBEAT --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 + .../LoRaSerial_Firmware.ino | 6 ++ Firmware/LoRaSerial_Firmware/RadioV2.ino | 86 +++++++++++-------- Firmware/LoRaSerial_Firmware/States.ino | 79 +++++++++++++++++ Firmware/LoRaSerial_Firmware/System.ino | 4 + Firmware/LoRaSerial_Firmware/settings.h | 1 + Firmware/Tools/Command_Validation_Script.txt | 9 +- .../Results/Command_Validation_Results.txt | 31 +++++-- 8 files changed, 173 insertions(+), 45 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index ae47e1b3..19e9b837 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -508,6 +508,8 @@ const COMMAND_ENTRY commands[] = {48, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, {49, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &settings.enableCRC16}, + {50, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &settings.displayRealMillis}, + //Define any user parameters starting at 255 decrementing towards 0 }; diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 4c6d7f17..d2e49b86 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -220,6 +220,12 @@ uint8_t rxDataBytes; unsigned long heartbeatTimer; unsigned long linkDownTimer; +//Clock synchronization +unsigned long rcvTimeMillis; +unsigned long xmitTimeMillis; +unsigned long timestampOffset; +unsigned long roundTripMillis; + //Transmit control const int datagramsExpectingAcks = 0 | (1 << DATAGRAM_DATA) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 01f9d854..8d8669ac 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -93,15 +93,19 @@ const uint16_t crc16Table[256] = //First packet in the three way handshake to bring up the link void xmitDatagramP2PPing() { + unsigned long currentMillis = millis(); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(unsigned long); + /* - endOfTxData ---. - | - V - +--------+---------+----------+ - | | | | - | NET ID | Control | Trailer | - | 8 bits | 8 bits | n Bytes | - +--------+---------+----------+ + endOfTxData ---. + | + V + +--------+---------+---------+----------+ + | | | | | + | NET ID | Control | Millis | Trailer | + | 8 bits | 8 bits | 4 bytes | n Bytes | + +--------+---------+---------+----------+ */ txControl.datagramType = DATAGRAM_PING; @@ -112,19 +116,19 @@ void xmitDatagramP2PPing() //Second packet in the three way handshake to bring up the link void xmitDatagramP2PAck1() { - uint16_t channelTimerElapsed = millis() - timerStart; - memcpy(endOfTxData, &channelTimerElapsed, sizeof(channelTimerElapsed)); - endOfTxData += sizeof(channelTimerElapsed); + unsigned long currentMillis = millis(); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(unsigned long); /* - endOfTxData ---. - | - V - +--------+---------+----------+----------+ - | | | Channel | Optional | - | NET ID | Control | Timer | Trailer | - | 8 bits | 8 bits | 2 bytes | n Bytes | - +--------+---------+----------+----------+ + endOfTxData ---. + | + V + +--------+---------+---------+----------+ + | | | | Optional | + | NET ID | Control | Millis | Trailer | + | 8 bits | 8 bits | 4 bytes | n Bytes | + +--------+---------+---------+----------+ */ txControl.datagramType = DATAGRAM_ACK_1; @@ -135,15 +139,19 @@ void xmitDatagramP2PAck1() //Last packet in the three way handshake to bring up the link void xmitDatagramP2PAck2() { + unsigned long currentMillis = millis(); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(unsigned long); + /* - endOfTxData ---. - | - V - +--------+---------+----------+ - | | | | - | NET ID | Control | Trailer | - | 8 bits | 8 bits | n Bytes | - +--------+---------+----------+ + endOfTxData ---. + | + V + +--------+---------+---------+----------+ + | | | | | + | NET ID | Control | Millis | Trailer | + | 8 bits | 8 bits | 4 bytes | n Bytes | + +--------+---------+---------+----------+ */ txControl.datagramType = DATAGRAM_ACK_2; @@ -221,15 +229,19 @@ void xmitDatagramP2PData() //Heartbeat packet to keep the link up void xmitDatagramP2PHeartbeat() { + unsigned long currentMillis = millis(); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(currentMillis); + /* - endOfTxData ---. - | - V - +--------+---------+----------+ - | | | Optional | - | NET ID | Control | Trailer | - | 8 bits | 8 bits | n Bytes | - +--------+---------+----------+ + endOfTxData ---. + | + V + +--------+---------+---------+----------+ + | | | | Optional | + | NET ID | Control | Millis | Trailer | + | 8 bits | 8 bits | 4 Bytes | n Bytes | + +--------+---------+---------+----------+ */ txControl.datagramType = DATAGRAM_HEARTBEAT; @@ -297,6 +309,9 @@ PacketType rcvDatagram() uint8_t receivedNetID; CONTROL_U8 rxControl; + //Save the receive time + rcvTimeMillis = millis(); + //Get the received datagram radio.readData(incomingBuffer, MAX_PACKET_SIZE); rxDataBytes = radio.getPacketLength(); @@ -864,6 +879,7 @@ void retransmitDatagram() int state = radio.startTransmit(outgoingPacket, txDatagramSize); if (state == RADIOLIB_ERR_NONE) { + xmitTimeMillis = millis(); packetAirTime = calcAirTime(txDatagramSize); //Calculate packet air size while we're transmitting in the background uint16_t responseDelay = packetAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond if (settings.debugTransmit) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b41009e5..bdb76d65 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1,5 +1,8 @@ void updateRadioState() { + unsigned long clockOffset; + unsigned long currentMillis; + unsigned long deltaMillis; uint8_t * header = outgoingPacket; bool heartbeatTimeout; uint16_t length; @@ -186,6 +189,16 @@ void updateRadioState() returnToReceiving(); else { + //Received PING + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; + //Acknowledge the PING triggerEvent(TRIGGER_SEND_ACK1); xmitDatagramP2PAck1(); @@ -214,6 +227,16 @@ void updateRadioState() PacketType packetType = rcvDatagram(); if (packetType == DATAGRAM_PING) { + //Received PING + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; + //Acknowledge the PING triggerEvent(TRIGGER_SEND_ACK1); xmitDatagramP2PAck1(); @@ -223,6 +246,16 @@ void updateRadioState() returnToReceiving(); else { + //Received ACK 1 + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; + //Acknowledge the ACK1 triggerEvent(TRIGGER_SEND_ACK2); xmitDatagramP2PAck2(); @@ -270,8 +303,20 @@ void updateRadioState() if (packetType != DATAGRAM_ACK_2) returnToReceiving(); else + { + //Received ACK 2 + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; + //Bring up the link v2EnterLinkUp(); + } } else { @@ -344,6 +389,26 @@ void updateRadioState() datagramTimer: Set at end of transmit, measures ACK timeout heartbeatTimer: Set upon entry to P2P_LINK_UP, reset upon HEARTBEAT transmit, measures time to send next HEARTBEAT + + Timestamp offset synchronization: + + System A System B + + PING ----> Update timestampOffset + + Update timestampOffset <---- ACK 1 + + ACK 2 ----> Update timestampOffset + + HEARTBEAT 0 ----> Update timestampOffset + + Update timestampOffset <---- HEARTBEAT 0 + + HEARTBEAT 1 --X + + Update timestampOffset <---- HEARTBEAT 1 + + HEARTBEAT 1 ----> Update timestampOffset */ case RADIO_P2P_LINK_UP: @@ -409,6 +474,13 @@ void updateRadioState() case DATAGRAM_HEARTBEAT: //Received heartbeat while link was idle. Send ack to sync clocks. + //Adjust the timestamp offset + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; //Display the signal strength if (settings.displayPacketQuality == true) @@ -647,6 +719,13 @@ void updateRadioState() case DATAGRAM_HEARTBEAT: //Received heartbeat while waiting for ack. + //Adjust the timestamp offset + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; //Display the signal strength if (settings.displayPacketQuality == true) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 20a12e14..6575f66d 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -128,6 +128,10 @@ void systemPrintTimestamp() //Get the clock value milliseconds = millis(); + //Offset the value for display + if (!settings.displayRealMillis) + milliseconds += timestampOffset; + //Compute the values for display seconds = milliseconds / 1000; minutes = seconds / 60; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 918e3f78..ca0b8030 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -275,6 +275,7 @@ typedef struct struct_settings { bool debugDatagrams = false; //Print the datagrams uint16_t overheadTime = 10; ////ms added to ack and datagram times before ACK timeout occurs bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive + bool displayRealMillis = false; //true = Display the millis value without offset, false = use offset } Settings; Settings settings; diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 926075eb..11ab57b9 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -447,9 +447,14 @@ ats49=1 ats49=2 ats49=0 -# Invalid commands +# DisplayRealMillis ats50=1 -ats50? +ats50=2 +ats50=0 + +# Invalid commands +ats51=1 +ats51? ats255=1 ats255? diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index ab4d55fe..eb5847ff 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -42,7 +42,7 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati3 -157 ati4 -154 +248 ati5 506 ati6 @@ -102,8 +102,9 @@ ATS44:PrintTxErrors=0 ATS45:UseV2=0 ATS46:PrintTimestamp=0 ATS47:DebugDatagrams=0 -ATS48:TxAckMillis=100 +ATS48:OverHeadtime=10 ATS49:EnableCRC16=0 +ATS50:DisplayRealMillis=0 ats28=0 OK # SerialSpeed @@ -890,11 +891,14 @@ OK # txAckMillis ERROR ats48=1 -ERROR +OverHeadtime=1 +OK ats48=2 -ERROR +OverHeadtime=2 +OK ats48=0 -ERROR +OverHeadtime=0 +OK # enableCRC16 ERROR ats49=1 @@ -905,11 +909,21 @@ ERROR ats49=0 EnableCRC16=0 OK -# Invalid commands +# DisplayRealMillis ERROR ats50=1 +DisplayRealMillis=1 +OK +ats50=2 +ERROR +ats50=0 +DisplayRealMillis=0 +OK +# Invalid commands +ERROR +ats51=1 ERROR -ats50? +ats51? ERROR ats255=1 ERROR @@ -930,6 +944,7 @@ ATS32:DebugTraining=0 ATS43:DebugTransmit=0 ATS33:DebugTrigger=0 ATS25:DisplayPacketQuality=0 +ATS50:DisplayRealMillis=0 ATS21:Echo=0 ATS49:EnableCRC16=0 ATS4:EncryptData=1 @@ -945,6 +960,7 @@ ATS12:MaxDwellTime=400 ATS26:MaxResends=2 ATS2:netID=192 ATS10:NumberOfChannels=50 +ATS48:OverHeadtime=0 ATS3:PointToPoint=1 ATS17:PreambleLength=8 ATS29:PrintFrequency=0 @@ -961,7 +977,6 @@ ATS40:TriggerEnable: 31-0=-1 ATS41:TriggerEnable: 63-32=-1 ATS38:TriggerWidth=25 ATS39:TriggerWidthIsMultiplier=1 -ATS48:TxAckMillis=100 ATS7:TxPower=30 ATS34:UsbSerialWait=0 ATS45:UseV2=0 From e7caaecfc88228d01f41edc3773b425078d54702 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 9 Oct 2022 06:43:14 -1000 Subject: [PATCH 015/594] Convert unique ID from uint32_t to uint8_t --- Firmware/LoRaSerial_Firmware/Arch_ESP32.h | 9 +++------ Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 14 +++++++++----- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/System.ino | 7 +++---- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h index d0cfcf09..033c3c75 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h +++ b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h @@ -107,13 +107,10 @@ void esp32SystemReset() ESP.restart(); } -void esp32UniqueID(uint32_t * unique128_BitID) +void esp32UniqueID(uint8_t * unique128_BitID) { - unique128_BitID[0] = 0; - unique128_BitID[1] = 0; - unique128_BitID[2] = 0; - unique128_BitID[3] = 0; - esp_read_mac((uint8_t *)unique128_BitID, ESP_MAC_WIFI_STA); + memset(unique128_BitID, 0, UNIQUE_ID_BYTES); + esp_read_mac(unique128_BitID, ESP_MAC_WIFI_STA); } const ARCH_TABLE arch = { diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 579ccfc8..394560ce 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -208,13 +208,17 @@ void samdSystemReset() NVIC_SystemReset(); } -void samdUniqueID(uint32_t * unique128_BitID) +void samdUniqueID(uint8_t * unique128_BitID) { + uint32_t id[UNIQUE_ID_BYTES/4]; + //Read the CPU's unique ID value - unique128_BitID[0] = *(uint32_t *)0x0080a00c; - unique128_BitID[1] = *(uint32_t *)0x0080a040; - unique128_BitID[2] = *(uint32_t *)0x0080a044; - unique128_BitID[3] = *(uint32_t *)0x0080a048; + id[0] = *(uint32_t *)0x0080a00c; + id[1] = *(uint32_t *)0x0080a040; + id[2] = *(uint32_t *)0x0080a044; + id[3] = *(uint32_t *)0x0080a048; + + memcpy(unique128_BitID, id, UNIQUE_ID_BYTES); } const ARCH_TABLE arch = { diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9902118f..870e6c20 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -126,7 +126,7 @@ bool commandAT(const char * commandString) //ATIx commands else if (commandString[2] == 'I' && commandLength == 4) { - uint32_t uniqueID[4]; + uint8_t uniqueID[UNIQUE_ID_BYTES]; switch (commandString[3]) { diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 4c6d7f17..56c91806 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -57,6 +57,7 @@ const int FIRMWARE_VERSION_MINOR = 0; #define LRS_IDENTIFIER (FIRMWARE_VERSION_MAJOR * 0x10 + FIRMWARE_VERSION_MINOR) #define MAX_PACKET_SIZE 255 //Limited by SX127x +#define UNIQUE_ID_BYTES 16 //Number of bytes in the unique ID #include "settings.h" diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index e0052a9f..e34c5997 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -154,14 +154,13 @@ void systemPrintTimestamp() } } -void systemPrintUniqueID(uint32_t * uniqueID) +void systemPrintUniqueID(uint8_t * uniqueID) { - uint8_t * id = (uint8_t *)uniqueID; int index; //Display in the same byte order as dump output - for (index = 0; index < 16; index++) - systemPrint(id[index], HEX); + for (index = 0; index < UNIQUE_ID_BYTES; index++) + systemPrint(uniqueID[index], HEX); } void systemWrite(uint8_t value) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 1fb36bac..0013f1e8 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -299,7 +299,7 @@ typedef void (* ARCH_SERIAL_PRINT)(const char * value); typedef uint8_t (* ARCH_SERIAL_READ)(); typedef void (* ARCH_SERIAL_WRITE)(uint8_t value); typedef void (* ARCH_SYSTEM_RESET)(); -typedef void (* ARCH_UNIQUE_ID)(uint32_t * unique128_BitID); +typedef void (* ARCH_UNIQUE_ID)(uint8_t * unique128_BitID); typedef struct _ARCH_TABLE { From 14cff45261736d3f383084dc88242d8fc1d8878d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 9 Oct 2022 07:26:29 -1000 Subject: [PATCH 016/594] Verify AES encryption key and block counter lengths Replace magic numbers with defined constants. Verify the defined constants. --- .../LoRaSerial_Firmware.ino | 8 +++--- Firmware/LoRaSerial_Firmware/Radio.ino | 26 ++++++++++++++++--- Firmware/LoRaSerial_Firmware/System.ino | 4 +-- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 56c91806..e9f2c304 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -57,7 +57,9 @@ const int FIRMWARE_VERSION_MINOR = 0; #define LRS_IDENTIFIER (FIRMWARE_VERSION_MAJOR * 0x10 + FIRMWARE_VERSION_MINOR) #define MAX_PACKET_SIZE 255 //Limited by SX127x -#define UNIQUE_ID_BYTES 16 //Number of bytes in the unique ID +#define AES_IV_BYTES 12 //Number of bytes for AESiv +#define AES_KEY_BYTES 16 //Number of bytes in the encryption key +#define UNIQUE_ID_BYTES 16 //Number of bytes in the unique ID #include "settings.h" @@ -102,7 +104,7 @@ uint8_t channelNumber = 0; #include GCM gcm; -uint8_t AESiv[12] = {0}; //Set during hop table generation +uint8_t AESiv[AES_IV_BYTES] = {0}; //Set during hop table generation //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Buttons - Interrupt driven and debounce @@ -187,7 +189,7 @@ unsigned long lastTrainBlink = 0; //Controls LED during training Settings originalSettings; //Create a duplicate of settings during training so that we can resort as needed uint8_t trainNetID; //New netID passed during training -uint8_t trainEncryptionKey[16]; //New AES key passed during training +uint8_t trainEncryptionKey[AES_KEY_BYTES]; //New AES key passed during training bool inCommandMode = false; //Normal data is prevented from entering serial output when in command mode uint8_t commandLength = 0; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 7573c63f..176e10ca 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1042,8 +1042,26 @@ void generateHopTable() //'Randomly' shuffle list based on our specific seed shuffle(channels, settings.numberOfChannels); + //Verify the AES IV length + if (AES_IV_BYTES != gcm.ivSize()) + { + systemPrint("ERROR - Wrong AES IV size in bytes, please set AES_IV_BYTES = "); + systemPrintln(gcm.ivSize()); + while (1) + petWDT(); + } + + //Verify the AES key length + if (AES_KEY_BYTES != gcm.keySize()) + { + systemPrint("ERROR - Wrong AES key size in bytes, please set AES_KEY_BYTES = "); + systemPrintln(gcm.keySize()); + while (1) + petWDT(); + } + //Set new initial values for AES using settings based random seed - for (uint8_t x = 0 ; x < 12 ; x++) + for (uint8_t x = 0 ; x < sizeof(AESiv) ; x++) AESiv[x] = myRand(); if ((settings.debug == true) || (settings.debugRadio == true)) @@ -1061,7 +1079,7 @@ void generateHopTable() } systemPrint("AES IV:"); - for (uint8_t i = 0 ; i < 12 ; i++) + for (uint8_t i = 0 ; i < sizeof(AESiv) ; i++) { systemPrint(" 0x"); systemPrint(AESiv[i], HEX); @@ -1143,11 +1161,11 @@ void hopChannel() bool receiveInProcess() { //triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_START); - + uint8_t radioStatus = radio.getModemStatus(); if (radioStatus & 0b1011) return (true); //If any bits are set there is a receive in progress return false; - + //A remote unit may have started transmitting but this unit has not received enough preamble to detect it. //Wait X * symbol time for clear air. //This was found by sending two nearly simultaneous packets and using a logic analyzer to establish the point at which diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index e34c5997..0c534dd1 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -251,7 +251,7 @@ void systemReset() //Encrypt a given array in place void encryptBuffer(uint8_t *bufferToEncrypt, uint8_t arraySize) { - gcm.setKey(settings.encryptionKey, gcm.keySize()); + gcm.setKey(settings.encryptionKey, sizeof(settings.encryptionKey)); gcm.setIV(AESiv, sizeof(AESiv)); gcm.encrypt(bufferToEncrypt, bufferToEncrypt, arraySize); @@ -260,7 +260,7 @@ void encryptBuffer(uint8_t *bufferToEncrypt, uint8_t arraySize) //Decrypt a given array in place void decryptBuffer(uint8_t *bufferToDecrypt, uint8_t arraySize) { - gcm.setKey(settings.encryptionKey, gcm.keySize()); + gcm.setKey(settings.encryptionKey, sizeof(settings.encryptionKey)); gcm.setIV(AESiv, sizeof(AESiv)); gcm.decrypt(bufferToDecrypt, bufferToDecrypt, arraySize); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 0013f1e8..28eeeb40 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -231,7 +231,7 @@ typedef struct struct_settings { uint8_t netID = 192; //Both radios must share a network ID bool pointToPoint = true; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. bool encryptData = true; //AES encrypt each packet - uint8_t encryptionKey[16] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; + uint8_t encryptionKey[AES_KEY_BYTES] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; bool dataScrambling = false; //Use IBM Data Whitening to reduce DC bias uint8_t radioBroadcastPower_dbm = 30; //Transmit power in dBm. Max is 30dBm (1W), min is 14dBm (25mW). float frequencyMin = 902.0; //MHz From ce0578e0eb4608f8dfddabb4b36ab4bf5df83301 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 10 Oct 2022 19:23:01 -1000 Subject: [PATCH 017/594] Display encrypted and unencrypted data for TX and RX Better handle packetSent counter --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 76 +++++++++++++++++++----- Firmware/LoRaSerial_Firmware/States.ino | 3 + 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 9f973a5e..942e0793 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -136,7 +136,6 @@ void xmitDatagramP2PCommand() txControl.datagramType = DATAGRAM_REMOTE_COMMAND; txControl.ackNumber = expectedDatagramNumber; expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; - packetSent = 0; //This is the first time this packet is being sent transmitDatagram(); } @@ -157,7 +156,6 @@ void xmitDatagramP2PCommandResponse() txControl.datagramType = DATAGRAM_REMOTE_COMMAND_RESPONSE; txControl.ackNumber = expectedDatagramNumber; expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; - packetSent = 0; //This is the first time this packet is being sent transmitDatagram(); } @@ -178,7 +176,6 @@ void xmitDatagramP2PData() txControl.datagramType = DATAGRAM_DATA; txControl.ackNumber = expectedDatagramNumber; expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; - packetSent = 0; //This is the first time this packet is being sent transmitDatagram(); } @@ -199,7 +196,6 @@ void xmitDatagramP2PHeartbeat() txControl.datagramType = DATAGRAM_HEARTBEAT; txControl.ackNumber = expectedDatagramNumber; expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; - packetSent = 0; //This is the first time this packet is being sent transmitDatagram(); } @@ -284,7 +280,9 @@ PacketType rcvDatagram() { systemPrintln("----------"); systemPrintTimestamp(); - systemPrint("RX: Data "); + systemPrint("RX: "); + systemPrint((settings.dataScrambling || settings.encryptData) ? "Encrypted " : "Unencrypted "); + systemPrint("Frame "); systemPrint(rxDataBytes); systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); @@ -300,6 +298,21 @@ PacketType rcvDatagram() if (settings.encryptData == true) decryptBuffer(incomingBuffer, rxDataBytes); + //Display the received data bytes + if ((settings.dataScrambling || settings.encryptData) + && (settings.printRfData || settings.debugReceive)) + { + systemPrintTimestamp(); + systemPrint("RX: Unencrypted Frame "); + systemPrint(rxDataBytes); + systemPrint(" (0x"); + systemPrint(rxDataBytes, HEX); + systemPrintln(") bytes"); + petWDT(); + if (settings.printRfData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + } + //All packets must include the 2-byte control header if (rxDataBytes < minDatagramSize) { @@ -307,7 +320,7 @@ PacketType rcvDatagram() if (settings.printPktData || settings.debugReceive) { systemPrintTimestamp(); - systemPrint("RX: Bad packet "); + systemPrint("RX: Bad Frame "); systemPrint(rxDataBytes); systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); @@ -490,7 +503,7 @@ PacketType rcvDatagram() if (settings.printPktData || settings.debugReceive) { systemPrintTimestamp(); - systemPrint("RX: Packet data "); + systemPrint("RX: Datagram "); systemPrint(rxDataBytes); systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); @@ -596,7 +609,7 @@ void transmitDatagram() if (settings.printPktData || settings.debugTransmit) { systemPrintTimestamp(); - systemPrint("TX: Packet data "); + systemPrint("TX: Datagram "); systemPrint(length); systemPrint(" (0x"); systemPrint(length, HEX); @@ -691,6 +704,20 @@ void transmitDatagram() |<-------------------- txDatagramSize --------------------->| */ + //Display the transmitted packet bytes + if (settings.printRfData || settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: Unencrypted Frame "); + systemPrint(txDatagramSize); + systemPrint(" (0x"); + systemPrint(txDatagramSize, HEX); + systemPrintln(") bytes"); + petWDT(); + if (settings.printRfData) + dumpBuffer(outgoingPacket, txDatagramSize); + } + //Encrypt the datagram if (settings.encryptData == true) encryptBuffer(outgoingPacket, txDatagramSize); @@ -699,6 +726,21 @@ void transmitDatagram() if (settings.dataScrambling == true) radioComputeWhitening(outgoingPacket, txDatagramSize); + //Display the transmitted packet bytes + if ((settings.printRfData || settings.debugTransmit) + && (settings.encryptData || settings.dataScrambling)) + { + systemPrintTimestamp(); + systemPrint("TX: Encrypted Frame "); + systemPrint(txDatagramSize); + systemPrint(" (0x"); + systemPrint(txDatagramSize, HEX); + systemPrintln(") bytes"); + petWDT(); + if (settings.printRfData) + dumpBuffer(outgoingPacket, txDatagramSize); + } + //If we are trainsmitting at high data rates the receiver is often not ready for new data. Pause for a few ms (measured with logic analyzer). if (settings.airSpeed == 28800 || settings.airSpeed == 38400) delay(2); @@ -710,6 +752,7 @@ void transmitDatagram() datagramAirTime = calcAirTime(txDatagramSize); //Transmit this datagram + packetSent = 0; //This is the first time this packet is being sent retransmitDatagram(); } @@ -726,7 +769,13 @@ void printControl(uint8_t value) systemPrintln(value & 3); systemPrintTimestamp(); systemPrint(" datagramType "); - systemPrintln(v2DatagramType[control->datagramType]); + if (control->datagramType < MAX_DATAGRAM_TYPE) + systemPrintln(v2DatagramType[control->datagramType]); + else + { + systemPrint("Unknown "); + systemPrintln(control->datagramType); + } petWDT(); } @@ -744,13 +793,12 @@ void retransmitDatagram() */ //Display the transmitted packet bytes - if (settings.printRfData || settings.debugTransmit) + if (packetSent && (settings.printRfData || settings.debugTransmit)) { systemPrintTimestamp(); - systemPrint("TX: "); - if (packetSent) - systemPrint("Retransmit "); - systemPrint ("Data "); + systemPrint("TX: Retransmit "); + systemPrint((settings.encryptData || settings.dataScrambling) ? "Encrypted " : "Unencrypted "); + systemPrint("Frame "); systemPrint(txDatagramSize); systemPrint(" (0x"); systemPrint(txDatagramSize, HEX); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ff25aed1..cd474322 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -7,6 +7,7 @@ void updateRadioState() static uint8_t rexmtBuffer[MAX_PACKET_SIZE]; static CONTROL_U8 rexmtControl; static uint8_t rexmtLength; + static uint8_t rexmtPacketSent; switch (radioState) { @@ -673,6 +674,7 @@ void updateRadioState() memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); rexmtControl = txControl; rexmtLength = txDatagramSize; + rexmtPacketSent = packetSent; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); xmitDatagramP2PAck(); //Transmit ACK @@ -757,6 +759,7 @@ void updateRadioState() memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); txControl = rexmtControl; txDatagramSize = rexmtLength; + packetSent = rexmtPacketSent; if (settings.debugDatagrams) { systemPrintTimestamp(); From ec0090b0b8cfe91dcf53deabc981ac41ae2d5ab9 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 10 Oct 2022 19:27:13 -1000 Subject: [PATCH 018/594] Only frequency HOP when frequency hopping is enabled --- Firmware/LoRaSerial_Firmware/Begin.ino | 7 +++++-- Firmware/LoRaSerial_Firmware/States.ino | 3 --- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 80f7957e..4785875f 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -104,6 +104,9 @@ void channelTimerHandler() channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); } - triggerEvent(TRIGGER_CHANNEL_TIMER_ISR); - timeToHop = true; + if (settings.frequencyHop) + { + triggerEvent(TRIGGER_CHANNEL_TIMER_ISR); + timeToHop = true; + } } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ff25aed1..de1b63a2 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -803,7 +803,6 @@ void updateRadioState() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= case RADIO_MP_STANDBY: - timeToHop = false; if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived { triggerEvent(TRIGGER_BROADCAST_PACKET_RECEIVED); @@ -853,7 +852,6 @@ void updateRadioState() break; case RADIO_MP_WAIT_TX_DONE: - timeToHop = false; if (transactionComplete == true) //If dio0ISR has fired, we are done transmitting { transactionComplete = false; //Reset ISR flag @@ -867,7 +865,6 @@ void updateRadioState() case RADIO_MP_RECEIVE: { - timeToHop = false; //Decode the received datagram PacketType packetType = rcvDatagram(); From c7cd899ea9ddaa19c8bf0b139460da58f407709b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 11 Oct 2022 04:53:44 -1000 Subject: [PATCH 019/594] V2: Remove excess state from multi-point datagram exchange --- Firmware/LoRaSerial_Firmware/States.ino | 125 ++++++++++++------------ Firmware/LoRaSerial_Firmware/settings.h | 1 - 2 files changed, 62 insertions(+), 64 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index de1b63a2..9d452212 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -803,68 +803,16 @@ void updateRadioState() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= case RADIO_MP_STANDBY: - if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived - { - triggerEvent(TRIGGER_BROADCAST_PACKET_RECEIVED); - transactionComplete = false; //Reset ISR flag - changeState(RADIO_MP_RECEIVE); - } - - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency + //Hop channels when required + if (timeToHop == true) hopChannel(); - else //Process waiting serial - { - //If the radio is available, send any data in the serial buffer over the radio - if (receiveInProcess() == false) - { - if (availableRXBytes()) //If we have bytes - { - if (processWaitingSerial(false) == true) //If we've hit a frame size or frame-timed-out - { - triggerEvent(TRIGGER_BROADCAST_DATA_PACKET); - xmitDatagramMpDatagram(); - sendDataPacket(); - changeState(RADIO_MP_WAIT_TX_DONE); - } - } - } - } //End processWaitingSerial - - //Toggle 2 LEDs if we have recently transmitted - if (millis() - packetTimestamp < 5000) - { - if (millis() - lastLinkBlink > 250) //Blink at 4Hz - { - lastLinkBlink = millis(); - if (digitalRead(pin_rssi2LED) == HIGH) - setRSSI(0b0001); - else - setRSSI(0b0010); - } - } - else if (millis() - lastPacketReceived < 5000) - updateRSSI(); //Adjust LEDs to RSSI level - - //Turn off RSSI after 5 seconds of no activity - else - setRSSI(0); - break; - - case RADIO_MP_WAIT_TX_DONE: - if (transactionComplete == true) //If dio0ISR has fired, we are done transmitting + //Process the receive packet + if (transactionComplete == true) { + triggerEvent(TRIGGER_BROADCAST_PACKET_RECEIVED); transactionComplete = false; //Reset ISR flag - returnToReceiving(); - changeState(RADIO_MP_STANDBY); - setRSSI(0b0001); - } - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - break; - case RADIO_MP_RECEIVE: - { //Decode the received datagram PacketType packetType = rcvDatagram(); @@ -887,7 +835,6 @@ void updateRadioState() //We should not be receiving these datagrams, but if we do, just ignore frequencyCorrection += radio.getFrequencyError() / 1000000.0; returnToReceiving(); - changeState(RADIO_MP_STANDBY); break; case DATAGRAM_DATAGRAM: @@ -921,12 +868,65 @@ void updateRadioState() updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; returnToReceiving(); //No response when in broadcasting mode - changeState(RADIO_MP_STANDBY); lastPacketReceived = millis(); //Update timestamp for Link LED break; } } + + else //Process waiting serial + { + //If the radio is available, send any data in the serial buffer over the radio + if (receiveInProcess() == false) + { + if (availableRXBytes()) //If we have bytes + { + if (processWaitingSerial(false) == true) //If we've hit a frame size or frame-timed-out + { + triggerEvent(TRIGGER_BROADCAST_DATA_PACKET); + xmitDatagramMpDatagram(); + sendDataPacket(); + changeState(RADIO_MP_WAIT_TX_DONE); + } + } + } + } //End processWaitingSerial + + //Toggle 2 LEDs if we have recently transmitted + if (millis() - packetTimestamp < 5000) + { + if (millis() - lastLinkBlink > 250) //Blink at 4Hz + { + lastLinkBlink = millis(); + if (digitalRead(pin_rssi2LED) == HIGH) + setRSSI(0b0001); + else + setRSSI(0b0010); + } + } + else if (millis() - lastPacketReceived < 5000) + updateRSSI(); //Adjust LEDs to RSSI level + + //Turn off RSSI after 5 seconds of no activity + else + setRSSI(0); + break; + + case RADIO_MP_WAIT_TX_DONE: + //Hop channels when required + if (timeToHop == true) + hopChannel(); + + //If transmit is complete then start receiving + if (transactionComplete == true) + { + transactionComplete = false; //Reset ISR flag + returnToReceiving(); + changeState(RADIO_MP_STANDBY); + setRSSI(0b0001); + } + else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency + hopChannel(); break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1615,7 +1615,7 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_TRAINING_ACK_WAIT, "TRAINING_ACK_WAIT", "[Training] Ack Wait"}, //14 {RADIO_TRAINING_RECEIVED_PACKET, "TRAINING_RECEIVED_PACKET", "[Training] RX Packet"}, //15 - //V2 - Point to Point link handshake + //V2 - Point-to-Point link handshake // State Name Description {RADIO_P2P_LINK_DOWN, "P2P_LINK_DOWN", "V2 P2P: [No Link] Waiting for Ping"}, //16 {RADIO_P2P_WAIT_TX_PING_DONE, "P2P_WAIT_TX_PING_DONE", "V2 P2P: [No Link] Wait Ping TX Done"},//17 @@ -1624,7 +1624,7 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_P2P_WAIT_ACK_2, "P2P_WAIT_ACK_2", "V2 P2P: [No Link] Waiting for ACK2"}, //20 {RADIO_P2P_WAIT_TX_ACK_2_DONE, "P2P_WAIT_TX_ACK_2_DONE", "V2 P2P: [No Link] Wait ACK2 TX Done"},//21 - //V2 - Point to Point, link up, data exchange + //V2 - Point-to-Point, link up, data exchange // State Name Description {RADIO_P2P_LINK_UP, "P2P_LINK_UP", "V2 P2P: Receiving Standby"}, //22 {RADIO_P2P_LINK_UP_WAIT_ACK_DONE, "P2P_LINK_UP_WAIT_ACK_DONE", "V2 P2P: Waiting ACK TX Done"}, //23 @@ -1632,11 +1632,10 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_P2P_LINK_UP_WAIT_ACK, "P2P_LINK_UP_WAIT_ACK", "V2 P2P: Waiting for ACK"}, //25 {RADIO_P2P_LINK_UP_HB_ACK_REXMT, "P2P_LINK_UP_HB_ACK_REXMT", "V2 P2P: Heartbeat ACK ReXmt"}, //26 - //V2 - Multi Point data exchange + //V2 - Multi-Point data exchange // State Name Description {RADIO_MP_STANDBY, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //27 {RADIO_MP_WAIT_TX_DONE, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //28 - {RADIO_MP_RECEIVE, "MP_RECEIVE", "V2 MP: Received datagram"}, //29 }; void verifyRadioStateTable() diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 1fb36bac..54ebffc6 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -41,7 +41,6 @@ typedef enum // Multi-Point: Datagrams RADIO_MP_STANDBY, RADIO_MP_WAIT_TX_DONE, - RADIO_MP_RECEIVE, RADIO_MAX_STATE, } RadioStates; From a69b19511b00350c852c2c33acfcdb5051a92a19 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 11 Oct 2022 05:47:17 -1000 Subject: [PATCH 020/594] Remove F() --- Firmware/LoRaSerial_Firmware/Begin.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 80f7957e..2cca60ff 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -87,7 +87,7 @@ void petWDT() void beginChannelTimer() { if (channelTimer.attachInterruptInterval_MS(settings.maxDwellTime, channelTimerHandler) == false) - Serial.println(F("Error starting ChannelTimer!")); + Serial.println("Error starting ChannelTimer!"); stopChannelTimer(); //Start timer only after link is up } From 9b635b38a2d0c36081b807a24653f159f430af94 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 11 Oct 2022 05:50:27 -1000 Subject: [PATCH 021/594] Display NVM operations during debug --- Firmware/LoRaSerial_Firmware/NVM.ino | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index 218d48c9..d768edf6 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -6,7 +6,8 @@ void loadSettings() uint32_t testRead = 0; if (EEPROM.get(0, testRead) == 0xFFFFFFFF) { - //systemPrintln("EEPROM is blank. Default settings applied."); + if (settings.debug) + systemPrintln("EEPROM is blank."); recordSystemSettings(); //Record default settings to EEPROM. At power on, settings are in default state } @@ -16,7 +17,8 @@ void loadSettings() EEPROM.get(0, tempSize); //Load the sizeOfSettings if (tempSize != sizeof(settings)) { - //systemPrintln("Settings wrong size. Default settings applied."); + if (settings.debug) + systemPrintln("Settings wrong size."); recordSystemSettings(); //Record default settings to EEPROM. At power on, settings are in default state } @@ -25,11 +27,14 @@ void loadSettings() EEPROM.get(sizeof(tempSize), tempIdentifier); //Load the identifier from the EEPROM location after sizeOfSettings (int) if (tempIdentifier != LRS_IDENTIFIER) { - //systemPrint("Settings are not valid for this variant of LoRaSerial. Default settings applied."); + if (settings.debug) + systemPrint("Settings are not valid for this variant of LoRaSerial."); recordSystemSettings(); //Record default settings to EEPROM. At power on, settings are in default state } //Read current settings + if (settings.debug) + systemPrintln("Reading the settings from EEPROM"); EEPROM.get(0, settings); recordSystemSettings(); @@ -38,6 +43,8 @@ void loadSettings() //Record the current settings struct to EEPROM void recordSystemSettings() { + if (settings.debug) + systemPrintln("Writing settings to EEPROM"); settings.sizeOfSettings = sizeof(settings); EEPROM.put(0, settings); @@ -46,6 +53,8 @@ void recordSystemSettings() void eepromErase() { + if (settings.debug) + systemPrintln("Erasing the EEPROM"); for (uint16_t i = 0 ; i < EEPROM.length() ; i++) { petWDT(); From 568de7b014d449bbfcca95ee4ce84f544e82f6f6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 11 Oct 2022 05:58:06 -1000 Subject: [PATCH 022/594] Add myUniqueId --- Firmware/LoRaSerial_Firmware/Commands.ino | 3 +-- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9902118f..adabe041 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -172,8 +172,7 @@ bool commandAT(const char * commandString) systemPrintln(channelNumber); break; case ('8'): //ATI8 - Display the unique ID - arch.uniqueID(uniqueID); - systemPrintUniqueID(uniqueID); + systemPrintUniqueID(myUniqueId); systemPrintln(); break; default: diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 4c6d7f17..a4c0cd10 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -230,6 +230,8 @@ const int datagramsExpectingAcks = 0 uint8_t * endOfTxData; CONTROL_U8 txControl; +uint8_t myUniqueId[UNIQUE_ID_BYTES]; // Unique ID of this system + volatile bool clearDIO1 = true; //Clear the DIO1 hop ISR when possible //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -261,11 +263,13 @@ void setup() beginSerial(settings.serialSpeed); - verifyRadioStateTable(); //Verify that the state table contains all of the states in increasing order - systemPrintTimestamp(); systemPrintln("LRS"); + verifyRadioStateTable(); //Verify that the state table contains all of the states in increasing order + + arch.uniqueID(myUniqueId); //Get the unique ID + beginBoard(); //Determine what hardware platform we are running on. beginLoRa(); //Start radio From 78417ff1ffadeabd96e8220978935c6b4ed70df6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 11 Oct 2022 06:05:59 -1000 Subject: [PATCH 023/594] V2: Use common routine to determine header and trailer length --- Firmware/LoRaSerial_Firmware/States.ino | 54 ++++++++++++++----------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b89addb3..c9b54b83 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -32,26 +32,10 @@ void updateRadioState() //Set all of the ACK numbers to zero *(uint8_t *)(&txControl) = 0; - //Determine the components of the frame header - headerBytes = 0; - - //Add the netID to the header - if (settings.pointToPoint || settings.verifyRxNetID) - headerBytes += 1; - - //Add the control byte to the header - headerBytes += 1; - - //Determine the maximum frame size - if (settings.radioSpreadFactor == 6) - headerBytes += 1; - - //Set the beginning of the data portion of the transmit buffer - endOfTxData = &outgoingPacket[headerBytes]; - - //Determine the size of the trailer - trailerBytes = (settings.enableCRC16 ? 2 : 0); + //Determine the components of the frame header and trailer + selectHeaderAndTrailerBytes(); + //Initialize the radio radioSeed = radio.randomByte(); //Puts radio into standy-by state randomSeed(radioSeed); if ((settings.debug == true) || (settings.debugRadio == true)) @@ -72,10 +56,6 @@ void updateRadioState() //Start the link between the radios if (settings.useV2) { - //Determine the minimum and maximum datagram sizes - minDatagramSize = headerBytes + trailerBytes; - maxDatagramSize = sizeof(outgoingPacket) - minDatagramSize; - //Start the V2 protocol if (settings.pointToPoint == true) changeState(RADIO_P2P_LINK_DOWN); @@ -1583,6 +1563,34 @@ void updateRadioState() } } +//Compute the number of header and trailer bytes +void selectHeaderAndTrailerBytes() +{ + //Determine the components of the frame header + headerBytes = 0; + + //Add the netID to the header + if (settings.pointToPoint || settings.verifyRxNetID) + headerBytes += 1; + + //Add the control byte to the header + headerBytes += 1; + + //Determine the maximum frame size + if (settings.radioSpreadFactor == 6) + headerBytes += 1; + + //Set the beginning of the data portion of the transmit buffer + endOfTxData = &outgoingPacket[headerBytes]; + + //Determine the size of the trailer + trailerBytes = (settings.enableCRC16 ? 2 : 0); + + //Determine the minimum and maximum datagram sizes + minDatagramSize = headerBytes + trailerBytes; + maxDatagramSize = sizeof(outgoingPacket) - minDatagramSize; +} + //Return true if the radio is in a linked state //This is used for determining if we can do remote AT commands or not bool isLinked() From cf9ae2e0dbecfacf96ab1b06b0bbdcced25da3d7 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 11 Oct 2022 06:51:49 -1000 Subject: [PATCH 024/594] V2: Add client/server training support Verified with 3 clients using the following server script: +++ at&f ats28=1 ats1=9600 ats2=83 ats3=0 ats5=537061726b46756e3230323231303130 ats6=1 ats7=16 ats11=0 ats35=1 ats36=1 ats37=1 ats45=1 ats47=1 ats51=1 at&w att The following client script was used: +++ at&f ats28=1 ats3=0 ats45=1 att --- Firmware/LoRaSerial_Firmware/Commands.ino | 25 +- .../LoRaSerial_Firmware.ino | 5 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 175 +++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 239 ++++++++++++++++++ Firmware/LoRaSerial_Firmware/Train.ino | 135 +++++++++- Firmware/LoRaSerial_Firmware/settings.h | 40 ++- Firmware/Tools/Command_Validation_Script.txt | 29 ++- .../Results/Command_Validation_Results.txt | 68 ++++- 8 files changed, 706 insertions(+), 10 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index f50744ee..3315de1a 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -111,7 +111,24 @@ bool commandAT(const char * commandString) break; case ('T'): //Enter training mode reportOK(); - beginTraining(); + if (settings.useV2 && (!settings.pointToPoint)) + { + if (settings.trainingServer) + beginTrainingServer(); + else + beginTrainingClient(); + } + else + beginTraining(); + break; + case ('X'): //Stop the training server + if (trainingServerRunning && (!settings.pointToPoint) && settings.trainingServer) + { + endClientServerTraining(TRIGGER_TRAINING_SERVER_STOPPED); + reportOK(); + } + else + reportERROR(); break; case ('Z'): //Reboots the radio reportOK(); @@ -508,6 +525,12 @@ const COMMAND_ENTRY commands[] = {49, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &settings.enableCRC16}, {50, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &settings.displayRealMillis}, + {51, 0, 1, 0, TYPE_BOOL, valInt, "TrainingServer", &settings.trainingServer}, + {52, 1, 255, 0, TYPE_U8, valInt, "ClientRetryInterval", &settings.clientPingRetryInterval}, + {53, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &settings.copyDebug}, + {54, 0, 1, 0, TYPE_BOOL, valInt, "CopySerial", &settings.copySerial}, + + {55, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &settings.copyTriggers}, //Define any user parameters starting at 255 decrementing towards 0 }; diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 5d29fb72..a36594e2 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -239,6 +239,11 @@ const int datagramsExpectingAcks = 0 uint8_t * endOfTxData; CONTROL_U8 txControl; +//Multi-point Training +bool trainingServerRunning; //Training server is running +bool trainingPreviousRxInProgress = false; //Previous RX status +float originalChannel; //Original channel from HOP table while training is in progress +uint8_t trainingPartnerID[UNIQUE_ID_BYTES]; //Unique ID of the training partner uint8_t myUniqueId[UNIQUE_ID_BYTES]; // Unique ID of this system volatile bool clearDIO1 = true; //Clear the DIO1 hop ISR when possible diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d9ebfd39..8287be17 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -294,6 +294,181 @@ void xmitDatagramMpDatagram() transmitDatagram(); } +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Multi-Point Client Training +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Build the client ping packet used for training +void xmitDatagramTrainingPing() +{ + //Add the source (server) ID + memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + /* + endOfTxData ---. + | + V + +----------+---------+-----------+----------+ + | Optional | | | Optional | + | NET ID | Control | Client ID | Trailer | + | 8 bits | 8 bits | 16 Bytes | n Bytes | + +----------+---------+-----------+----------+ + */ + + txControl.datagramType = DATAGRAM_TRAINING_PING; + txControl.ackNumber = 0; + transmitDatagram(); +} + +//Build the client ACK packet used for training +void xmitDatagramTrainingAck(uint8_t * serverID) +{ + //Add the destination (server) ID + memcpy(endOfTxData, serverID, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + //Add the source (client) ID + memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + /* + endOfTxData ---. + | + V + +----------+---------+-----------+-----------+----------+ + | Optional | | | | Optional | + | NET ID | Control | Server ID | Client ID | Trailer | + | 8 bits | 8 bits | 16 Bytes | 16 Bytes | n Bytes | + +----------+---------+-----------+-----------+----------+ + */ + + txControl.datagramType = DATAGRAM_TRAINING_ACK; + txControl.ackNumber = 0; + transmitDatagram(); +} + +void updateRadioParameters(uint8_t * rxData) +{ + Settings params; + + //Get the parameters + memcpy(¶ms, rxData, sizeof(params)); + + //Update the radio parameters + originalSettings.airSpeed = params.airSpeed; + originalSettings.netID = params.netID; + originalSettings.pointToPoint = false; + originalSettings.encryptData = params.encryptData; + memcpy(originalSettings.encryptionKey, params.encryptionKey, sizeof(originalSettings.encryptionKey)); + originalSettings.dataScrambling = params.dataScrambling; + originalSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; + originalSettings.frequencyMin = params.frequencyMin; + originalSettings.frequencyMax = params.frequencyMax; + originalSettings.numberOfChannels = params.numberOfChannels; + originalSettings.frequencyHop = params.frequencyHop; + originalSettings.maxDwellTime = params.maxDwellTime; + originalSettings.radioBandwidth = params.radioBandwidth; + originalSettings.radioSpreadFactor = params.radioSpreadFactor; + originalSettings.radioCodingRate = params.radioCodingRate; + originalSettings.radioSyncWord = params.radioSyncWord; + originalSettings.radioPreambleLength = params.radioPreambleLength; + originalSettings.frameSize = params.frameSize; + originalSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; + originalSettings.heartbeatTimeout = params.heartbeatTimeout; + originalSettings.autoTuneFrequency = params.autoTuneFrequency; + originalSettings.maxResends = params.maxResends; + originalSettings.verifyRxNetID = params.verifyRxNetID; + originalSettings.useV2 = params.useV2; + originalSettings.overheadTime = params.overheadTime; + originalSettings.enableCRC16 = params.enableCRC16; + originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; + + //Update the API parameters + originalSettings.sortParametersByName = params.sortParametersByName; + originalSettings.printParameterName = params.printParameterName; + + //Update the debug parameters + if (params.copyDebug) + { + originalSettings.debug = params.debug; + originalSettings.displayPacketQuality = params.displayPacketQuality; + originalSettings.printFrequency = params.printFrequency; + originalSettings.debugRadio = params.debugRadio; + originalSettings.debugStates = params.debugStates; + originalSettings.debugTraining = params.debugTraining; + originalSettings.debugTrigger = params.debugTrigger; + originalSettings.printRfData = params.printRfData; + originalSettings.printPktData = params.printPktData; + originalSettings.debugReceive = params.debugReceive; + originalSettings.debugTransmit = params.debugTransmit; + originalSettings.printTxErrors = params.printTxErrors; + originalSettings.printTimestamp = params.printTimestamp; + originalSettings.debugDatagrams = params.debugDatagrams; + originalSettings.displayRealMillis = params.displayRealMillis; + } + + //Update the serial parameters + if (params.copySerial) + { + originalSettings.serialSpeed = params.serialSpeed; + originalSettings.echo = params.echo; + originalSettings.flowControl = params.flowControl; + originalSettings.usbSerialWait = params.usbSerialWait; + } + + //Update the trigger parameters + if (params.copyTriggers) + { + originalSettings.triggerWidth = params.triggerWidth; + originalSettings.triggerWidthIsMultiplier = params.triggerWidthIsMultiplier; + originalSettings.triggerEnable = params.triggerEnable; + originalSettings.triggerEnable2 = params.triggerEnable2; + } +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Multi-Point Server Training +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Build the server parameters packet used for training +void xmitDatagramRadioParameters(const uint8_t * clientID) +{ + Settings params; + + //Initialize the radio parameters + memcpy(¶ms, &originalSettings, sizeof(settings)); + params.pointToPoint = false; + params.trainingServer = false; + + //Add the destination (client) ID + memcpy(endOfTxData, clientID, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + //Add the source (server) ID + memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + //Add the radio parameters + memcpy(endOfTxData, ¶ms, sizeof(params)); + endOfTxData += sizeof(params); + + /* + endOfTxData ---. + | + V + +----------+---------+-----------+-----------+--- ... ---+----------+ + | Optional | | | | Radio | Optional | + | NET ID | Control | Client ID | Server ID | Parameters | Trailer | + | 8 bits | 8 bits | 16 Bytes | 16 Bytes | n bytes | n Bytes | + +----------+---------+-----------+-----------+-------------+----------+ + */ + + txControl.datagramType = DATAGRAM_TRAINING_PARAMS; + txControl.ackNumber = 0; + transmitDatagram(); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Datagram reception //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 44a05c3e..206880b3 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -991,6 +991,234 @@ void updateRadioState() hopChannel(); break; + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //V2 - Multi-Point Client Training + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + /* + beginTrainingClient + | + | Save current settings + | + V + +<--------------------------------. + | | + | Send client ping | + | | + V | + RADIO_MP_WAIT_TX_TRAINING_PING_DONE | + | | + V | Timeout + RADIO_MP_WAIT_RX_RADIO_PARAMETERS --------' + | + | Update settings + | Send client ACK + | + V + RADIO_MP_WAIT_TX_PARAM_ACK_DONE + | + V + endTrainingClientServer + | + | Restore settings + | + V + */ + + case RADIO_MP_WAIT_TX_TRAINING_PING_DONE: + updateCylonLEDs(); + + //If dio0ISR has fired, we are done transmitting + if (transactionComplete == true) + { + transactionComplete = false; + + //Indicate that the receive is complete + triggerEvent(TRIGGER_TRAINING_CLIENT_TX_PING_DONE); + + //Start the receive operation + returnToReceiving(); + + //Set the next state + changeState(RADIO_MP_WAIT_RX_RADIO_PARAMETERS); + } + break; + + case RADIO_MP_WAIT_RX_RADIO_PARAMETERS: + updateCylonLEDs(); + + //If dio0ISR has fired, a packet has arrived + if (transactionComplete == true) + { + transactionComplete = false; + trainingPreviousRxInProgress = false; + + //Decode the received datagram + PacketType packetType = rcvDatagram(); + + //Process the received datagram + switch (packetType) + { + default: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_TRAINING_PARAMS: + //Verify the IDs + if ((memcmp(rxData, myUniqueId, UNIQUE_ID_BYTES) != 0) + && (memcmp(rxData, myUniqueId, UNIQUE_ID_BYTES) != 0)) + { + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + } + + //Save the training partner ID + memcpy(trainingPartnerID, &rxData[UNIQUE_ID_BYTES], UNIQUE_ID_BYTES); + + //Get the radio parameters + updateRadioParameters(&rxData[UNIQUE_ID_BYTES * 2]); + + //Acknowledge the radio parameters + xmitDatagramTrainingAck(&rxData[UNIQUE_ID_BYTES]); + changeState(RADIO_MP_WAIT_TX_PARAM_ACK_DONE); + break; + } + } + + //Determine if a receive is in progress + else if (receiveInProcess()) + { + if (!trainingPreviousRxInProgress) + { + trainingPreviousRxInProgress = true; + triggerEvent(TRIGGER_TRAINING_CLIENT_RX_PARAMS); + } + } + + //Check for a receive timeout + else if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) + xmitDatagramTrainingPing(); + break; + + case RADIO_MP_WAIT_TX_PARAM_ACK_DONE: + updateCylonLEDs(); + + //If dio0ISR has fired, we are done transmitting + if (transactionComplete == true) + { + transactionComplete = false; + endClientServerTraining(TRIGGER_TRAINING_CLIENT_TX_ACK_DONE); + } + break; + + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //V2 - Multi-Point Server Training + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + /* + beginTrainingServer + | + | Save current settings + | + V + +<--------------------------------. + | | + V | + .------ RADIO_MP_WAIT_FOR_TRAINING_PING | + | | | + | | Send client ping | + | | | + | V | + | RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE --------' + | + | + `---------------. + | Stop training command + | + V + endTrainingClientServer + | + | Restore settings + | + V + */ + + case RADIO_MP_WAIT_FOR_TRAINING_PING: + updateCylonLEDs(); + + //If dio0ISR has fired, a packet has arrived + if (transactionComplete == true) + { + transactionComplete = false; //Reset ISR flag + trainingPreviousRxInProgress = false; + + //Decode the received datagram + PacketType packetType = rcvDatagram(); + + //Process the received datagram + switch (packetType) + { + default: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_TRAINING_PING: + //Save the client ID + memcpy(trainingPartnerID, rxData, UNIQUE_ID_BYTES); + xmitDatagramRadioParameters(trainingPartnerID); + + //Wait for the transmit to complete + changeState(RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE); + break; + + case DATAGRAM_TRAINING_ACK: + //Verify the client ID + if (memcmp(trainingPartnerID, &rxData[UNIQUE_ID_BYTES], UNIQUE_ID_BYTES) == 0) + { + //Don't respond to the client ACK, just start another receive operation + triggerEvent(TRIGGER_TRAINING_SERVER_RX_ACK); + + //Print the client trained message + systemPrint("Client "); + systemPrintUniqueID(trainingPartnerID); + systemPrintln(" Trained"); + } + returnToReceiving(); + break; + } + } + + //Determine if a receive is in progress + else if (receiveInProcess()) + if (!trainingPreviousRxInProgress) + { + trainingPreviousRxInProgress = true; + triggerEvent(TRIGGER_TRAINING_SERVER_RX); + } + break; + + case RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE: + updateCylonLEDs(); + + //If dio0ISR has fired, we are done transmitting + if (transactionComplete == true) + { + transactionComplete = false; + + //Indicate that the receive is complete + triggerEvent(TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE); + + //Start the receive operation + returnToReceiving(); + + //Set the next state + changeState(RADIO_MP_WAIT_FOR_TRAINING_PING); + } + break; + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //V1 - No Link //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1726,6 +1954,17 @@ const RADIO_STATE_ENTRY radioStateTable[] = // State Name Description {RADIO_MP_STANDBY, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //27 {RADIO_MP_WAIT_TX_DONE, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //28 + + //V2 - Multi-Point training client states + // State Name Description + {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, "MP_WAIT_TX_TRAINING_PING_DONE", "V2 MP: Wait TX training PING done"}, //29 + {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, "MP_WAIT_RX_RADIO_PARAMETERS", "V2 MP: Wait for radio parameters"}, //30 + {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, "MP_WAIT_TX_PARAM_ACK_DONE", "V2 MP: Wait for TX param ACK done"}, //31 + + //V2 - Multi-Point training server states + // State Name Description + {RADIO_MP_WAIT_FOR_TRAINING_PING, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //32 + {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //33 }; void verifyRadioStateTable() diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 67e20097..e62774e0 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -2,7 +2,7 @@ void beginTraining() { if ((settings.debug == true) || (settings.debugTraining == true)) - systemPrintln("Begin training"); + systemPrintln("Begin point-to-point training"); originalSettings = settings; //Make copy of current settings @@ -257,3 +257,136 @@ void updateCylonLEDs() } } +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//V2 Multi-Point Client/Server Training +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void beginTrainingClient() +{ + systemPrintln("Multipoint client training"); + + //Common initialization + commonTrainingInitialization(); + + //Transmit client ping to the training server + xmitDatagramTrainingPing(); + + //Set the next state + changeState(RADIO_MP_WAIT_TX_TRAINING_PING_DONE); +} + +void beginTrainingServer() +{ + trainingServerRunning = true; + + //Display the values to be used for the client/server training + systemPrintln("Multipoint server training"); + systemPrintln("Using:"); + systemPrint(" netID: "); + systemPrintln(settings.netID); + systemPrint(" Encryption key: "); + displayEncryptionKey(settings.encryptionKey); + systemPrintln(); + + //Common initialization + commonTrainingInitialization(); + settings.trainingServer = true; //52: Operate as the training server + + //Start the receive operation + returnToReceiving(); + + //Set the next state + changeState(RADIO_MP_WAIT_FOR_TRAINING_PING); +} + +//Perform the common training initialization +void commonTrainingInitialization() +{ + //Save the current settings + originalSettings = settings; + + //Use common radio settings between the client and server for training + settings = defaultSettings; + settings.pointToPoint = false; // 3: Disable netID checking + settings.encryptData = true; // 4: Enable packet encryption + settings.dataScrambling = true; // 6: Scramble the data + settings.radioBroadcastPower_dbm = 14; // 7: Minimum, assume radios are near each other + settings.frequencyHop = false; //11: Stay on the training frequency + settings.printParameterName = true; //28: Print the parameter names + settings.verifyRxNetID = false; //37: Disable netID checking + settings.enableCRC16 = true; //49: Use CRC-16 + + //Determine the components of the frame header and trailer + selectHeaderAndTrailerBytes(); + + //Debug training if requested + if (originalSettings.debugTraining) + { + settings.debug = originalSettings.debug; + settings.displayPacketQuality = originalSettings.displayPacketQuality; + settings.printParameterName = originalSettings.printParameterName; + settings.printFrequency = originalSettings.printFrequency; + + settings.debugRadio = originalSettings.debugRadio; + settings.debugStates = originalSettings.debugStates; + settings.debugTraining = originalSettings.debugTraining; + settings.debugTrigger = originalSettings.debugTrigger; + + settings.printRfData = originalSettings.printRfData; + settings.printPktData = originalSettings.printPktData; + settings.triggerWidth = originalSettings.triggerWidth; + settings.triggerWidthIsMultiplier = originalSettings.triggerWidthIsMultiplier; + + settings.triggerEnable = originalSettings.triggerEnable; + settings.triggerEnable = originalSettings.triggerEnable; + settings.debugReceive = originalSettings.debugReceive; + settings.debugTransmit = originalSettings.debugTransmit; + settings.printTxErrors = originalSettings.printTxErrors; + + settings.printTimestamp = originalSettings.printTimestamp; + settings.debugDatagrams = originalSettings.debugDatagrams; + settings.displayRealMillis = originalSettings.displayRealMillis; + } + + //Reset cylon variables + startCylonLEDs(); + + //Select the training frequency, a multiple of channels down from the maximum + petWDT(); + float channelSpacing = (settings.frequencyMax - settings.frequencyMin) / (float)(settings.numberOfChannels + 2); + float trainFrequency = settings.frequencyMax - (channelSpacing * (FIRMWARE_VERSION_MAJOR % settings.numberOfChannels)); + originalChannel = channels[0]; //Remember the original channel + channels[0] = trainFrequency; //Inject this frequency into the channel table + + //Use only the first channel of the previously allocated channel table + configureRadio(); //Setup radio with settings +} + +//Upon successful exchange of parameters, switch to the new radio settings +void endClientServerTraining(uint8_t event) +{ + triggerEvent(event); + settings = originalSettings; //Return to original radio settings + + if (settings.debugTraining) + displayParameters(); + + if (!settings.trainingServer) + { + //Record the new client settings + recordSystemSettings(); + + systemPrint("Link trained from "); + systemPrintUniqueID(trainingPartnerID); + systemPrintln(); + } + + //Done with training + trainingServerRunning = false; + + //Reboot the radio with the new parameters + petWDT(); + systemFlush(); + systemReset(); +} + diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index dfd6788b..d2ac8775 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -42,6 +42,15 @@ typedef enum RADIO_MP_STANDBY, RADIO_MP_WAIT_TX_DONE, + //Training client states + RADIO_MP_WAIT_TX_TRAINING_PING_DONE, + RADIO_MP_WAIT_RX_RADIO_PARAMETERS, + RADIO_MP_WAIT_TX_PARAM_ACK_DONE, + + //Training server states + RADIO_MP_WAIT_FOR_TRAINING_PING, + RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, + RADIO_MAX_STATE, } RadioStates; RadioStates radioState = RADIO_NO_LINK_RECEIVING_STANDBY; @@ -73,6 +82,11 @@ typedef enum //V2: Multi-Point data exchange DATAGRAM_DATAGRAM, + //V2: Multi-Point training exchange + DATAGRAM_TRAINING_PING, + DATAGRAM_TRAINING_PARAMS, + DATAGRAM_TRAINING_ACK, + //Add new V2 datagram types before this line MAX_DATAGRAM_TYPE, @@ -97,8 +111,10 @@ typedef enum const char * const v2DatagramType[] = {// 0 1 2 3 4 5 6 "PING", "ACK-1", "ACK-2", "DATA", "SF6-DATA", "DATA-ACK", "HEARTBEAT", - // 7 8 9 - "RMT-CMD", "RMT_RESP", "DATAGRAM_DATAGRAM" + // 7 8 9 10 + "RMT-CMD", "RMT_RESP", "DATAGRAM_DATAGRAM", "TRAINING_PING", + // 11 12 + "TRAINING_PARAMS", "TRAINING_ACK" }; //Train button states @@ -189,6 +205,21 @@ enum TRIGGER_COMMAND_SENT_ACK_PACKET, TRIGGER_COMMAND_PACKET_RESEND, TRIGGER_PACKET_COMMAND_DATA, + + //Training client triggers + TRIGGER_TRAINING_CLIENT_TX_PING, //34, 875us + TRIGGER_TRAINING_CLIENT_TX_PING_DONE, //35, 900us + TRIGGER_TRAINING_CLIENT_RX_PARAMS, //38, 975us + TRIGGER_TRAINING_CLIENT_TX_ACK, //39, 1000us + TRIGGER_TRAINING_CLIENT_TX_ACK_DONE, //40, 1025us + TRIGGER_TRAINING_COMPLETE, //41, 1050us + + //Training server triggers + TRIGGER_TRAINING_SERVER_RX, //42, 1075us + TRIGGER_TRAINING_SERVER_TX_PARAMS, //43, 1100us + TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE, //44, 1125us + TRIGGER_TRAINING_SERVER_RX_ACK, //45, 1150us + TRIGGER_TRAINING_SERVER_STOPPED, //46, 1175us }; //Control where to print command output @@ -275,6 +306,11 @@ typedef struct struct_settings { uint16_t overheadTime = 10; ////ms added to ack and datagram times before ACK timeout occurs bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive bool displayRealMillis = false; //true = Display the millis value without offset, false = use offset + bool trainingServer = false; //Default to being a client + uint8_t clientPingRetryInterval = 3; //Number of seconds before retransmiting the client PING + bool copyDebug = true; //Copy the debug parameters to the training client + bool copySerial = true; //Copy the serial parameters to the training client + bool copyTriggers = true; //Copy the trigger parameters to the training client } Settings; Settings settings; diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 11ab57b9..d36cd5b0 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -452,9 +452,34 @@ ats50=1 ats50=2 ats50=0 -# Invalid commands +# TrainingServer ats51=1 -ats51? +ats51=2 +ats51=0 + +# ClientRetryInterval +ats52=1 +ats52=2 +ats52=0 + +# CopyDebug +ats53=1 +ats53=2 +ats53=0 + +# CopySerial +ats54=1 +ats54=2 +ats54=0 + +# CopyTriggers +ats55=1 +ats55=2 +ats55=0 + +# Invalid commands +ats56=1 +ats56? ats255=1 ats255? diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index eb5847ff..0335e8c4 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -42,7 +42,7 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati3 -157 ati4 -248 +15 ati5 506 ati6 @@ -50,7 +50,7 @@ ati6 ati7 0 ati8 -B2A99BF24A34555020312E34162815FF +E5D60B0B4A34555020312E34212815FF ati9 ERROR ati0 @@ -105,6 +105,11 @@ ATS47:DebugDatagrams=0 ATS48:OverHeadtime=10 ATS49:EnableCRC16=0 ATS50:DisplayRealMillis=0 +ATS51:TrainingServer=0 +ATS52:ClientRetryInterval=3 +ATS53:CopyDebug=1 +ATS54:CopySerial=1 +ATS55:CopyTriggers=1 ats28=0 OK # SerialSpeed @@ -919,11 +924,61 @@ ERROR ats50=0 DisplayRealMillis=0 OK -# Invalid commands +# TrainingServer ERROR ats51=1 +TrainingServer=1 +OK +ats51=2 +ERROR +ats51=0 +TrainingServer=0 +OK +# ClientRetryInterval +ERROR +ats52=1 +ClientRetryInterval=1 +OK +ats52=2 +ClientRetryInterval=2 +OK +ats52=0 +ERROR +# CopyDebug +ERROR +ats53=1 +CopyDebug=1 +OK +ats53=2 +ERROR +ats53=0 +CopyDebug=0 +OK +# CopySerial +ERROR +ats54=1 +CopySerial=1 +OK +ats54=2 +ERROR +ats54=0 +CopySerial=0 +OK +# CopyTriggers +ERROR +ats55=1 +CopyTriggers=1 +OK +ats55=2 +ERROR +ats55=0 +CopyTriggers=0 +OK +# Invalid commands +ERROR +ats56=1 ERROR -ats51? +ats56? ERROR ats255=1 ERROR @@ -933,7 +988,11 @@ ati0 ATS1:AirSpeed=4800 ATS24:AutoTune=0 ATS13:Bandwidth=500.00 +ATS52:ClientRetryInterval=2 ATS15:CodingRate=8 +ATS53:CopyDebug=0 +ATS54:CopySerial=0 +ATS55:CopyTriggers=0 ATS6:DataScrambling=0 ATS20:Debug=0 ATS47:DebugDatagrams=0 @@ -973,6 +1032,7 @@ ATS0:SerialSpeed=57600 ATS27:SortParametersByName=1 ATS14:SpreadFactor=9 ATS16:SyncWord=18 +ATS51:TrainingServer=0 ATS40:TriggerEnable: 31-0=-1 ATS41:TriggerEnable: 63-32=-1 ATS38:TriggerWidth=25 From 78a61f1cce0310c9e19ae86aea544187a002e80f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 11 Oct 2022 10:06:59 -1000 Subject: [PATCH 025/594] V2: Add training encryption key --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 +++-- Firmware/LoRaSerial_Firmware/Train.ino | 5 +++-- Firmware/LoRaSerial_Firmware/settings.h | 1 + Firmware/Tools/Command_Validation_Script.txt | 11 ++++++++--- .../Results/Command_Validation_Results.txt | 19 +++++++++++++++---- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 3315de1a..25d0a16c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -531,6 +531,7 @@ const COMMAND_ENTRY commands[] = {54, 0, 1, 0, TYPE_BOOL, valInt, "CopySerial", &settings.copySerial}, {55, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &settings.copyTriggers}, + {56, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &settings.trainingKey}, //Define any user parameters starting at 255 decrementing towards 0 }; @@ -665,8 +666,8 @@ bool commandSet(const char * commandString) case TYPE_KEY: valid = command->validate((void *)buffer, command->minValue, command->maxValue); if (valid) - for (uint32_t x = 0; x < (2 * sizeof(settings.encryptionKey)); x += 2) - settings.encryptionKey[x / 2] = charHexToDec(buffer[x], buffer[x + 1]); + for (uint32_t x = 0; x < (2 * AES_KEY_BYTES); x += 2) + ((uint8_t *)command->setting)[x / 2] = charHexToDec(buffer[x], buffer[x + 1]); break; case TYPE_SPEED_AIR: case TYPE_SPEED_SERIAL: diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index e62774e0..0f9181cb 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -284,12 +284,13 @@ void beginTrainingServer() systemPrintln("Using:"); systemPrint(" netID: "); systemPrintln(settings.netID); - systemPrint(" Encryption key: "); - displayEncryptionKey(settings.encryptionKey); + systemPrint(" Training key: "); + displayEncryptionKey(settings.trainingKey); systemPrintln(); //Common initialization commonTrainingInitialization(); + memcpy(&settings.trainingKey, &originalSettings.trainingKey, AES_KEY_BYTES); settings.trainingServer = true; //52: Operate as the training server //Start the receive operation diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index d2ac8775..6f2624cb 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -311,6 +311,7 @@ typedef struct struct_settings { bool copyDebug = true; //Copy the debug parameters to the training client bool copySerial = true; //Copy the serial parameters to the training client bool copyTriggers = true; //Copy the trigger parameters to the training client + uint8_t trainingKey[AES_KEY_BYTES] = { 0x53, 0x70, 0x61, 0x72, 0x6b, 0x46, 0x75, 0x6E, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67 }; } Settings; Settings settings; diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index d36cd5b0..8b35e23a 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -477,15 +477,20 @@ ats55=1 ats55=2 ats55=0 +# TrainingKey +ats56=01020304050607080910111213141516 +ats56=0102030405060708091011121314151 +ats56=010203040506070809101112131415160 + # Invalid commands -ats56=1 -ats56? +ats57=1 +ats57? ats255=1 ats255? ati0 -at&w +at&f atz diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index 0335e8c4..c66581a0 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -42,7 +42,7 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati3 -157 ati4 -15 +175 ati5 506 ati6 @@ -110,6 +110,7 @@ ATS52:ClientRetryInterval=3 ATS53:CopyDebug=1 ATS54:CopySerial=1 ATS55:CopyTriggers=1 +ATS56:TrainingKey=537061726B46756E547261696E696E67 ats28=0 OK # SerialSpeed @@ -974,11 +975,20 @@ ERROR ats55=0 CopyTriggers=0 OK +# TrainingKey +ERROR +ats56=01020304050607080910111213141516 +TrainingKey=01020304050607080910111213141516 +OK +ats56=0102030405060708091011121314151 +ERROR +ats56=010203040506070809101112131415160 +ERROR # Invalid commands ERROR -ats56=1 +ats57=1 ERROR -ats56? +ats57? ERROR ats255=1 ERROR @@ -1032,6 +1042,7 @@ ATS0:SerialSpeed=57600 ATS27:SortParametersByName=1 ATS14:SpreadFactor=9 ATS16:SyncWord=18 +ATS56:TrainingKey=01020304050607080910111213141516 ATS51:TrainingServer=0 ATS40:TriggerEnable: 31-0=-1 ATS41:TriggerEnable: 63-32=-1 @@ -1041,7 +1052,7 @@ ATS7:TxPower=30 ATS34:UsbSerialWait=0 ATS45:UseV2=0 ATS37:VerifyRxNetID=0 -at&w +at&f OK atz OK From dd95ffbed5521a1dccb78a2312bdba74af88d816 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 12 Oct 2022 07:43:12 -1000 Subject: [PATCH 026/594] Only enable one protocol version at a time Switch useV2 to protocolVersion to support future protocols Add comments to end of settings as reminders to update code --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 ++-- Firmware/LoRaSerial_Firmware/RadioV2.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 6 +++++- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 25d0a16c..085d5663 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -111,7 +111,7 @@ bool commandAT(const char * commandString) break; case ('T'): //Enter training mode reportOK(); - if (settings.useV2 && (!settings.pointToPoint)) + if ((settings.protocolVersion >= 2) && (!settings.pointToPoint)) { if (settings.trainingServer) beginTrainingServer(); @@ -518,7 +518,7 @@ const COMMAND_ENTRY commands[] = {43, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, {44, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, - {45, 0, 1, 0, TYPE_BOOL, valInt, "UseV2", &settings.useV2}, + {45, 1, 2, 0, TYPE_U8, valInt, "protocolVersion", &settings.protocolVersion}, {46, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, {47, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, {48, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 8287be17..1586f7cf 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -379,7 +379,7 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.autoTuneFrequency = params.autoTuneFrequency; originalSettings.maxResends = params.maxResends; originalSettings.verifyRxNetID = params.verifyRxNetID; - originalSettings.useV2 = params.useV2; + originalSettings.protocolVersion = params.protocolVersion; originalSettings.overheadTime = params.overheadTime; originalSettings.enableCRC16 = params.enableCRC16; originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 206880b3..f78a04a1 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -57,7 +57,7 @@ void updateRadioState() returnToReceiving(); //Start receiving //Start the link between the radios - if (settings.useV2) + if (settings.protocolVersion >= 2) { //Start the V2 protocol if (settings.pointToPoint == true) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 6f2624cb..845b3dda 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -300,7 +300,7 @@ typedef struct struct_settings { bool debugReceive = false; //Print receive processing bool debugTransmit = false; //Print transmit processing bool printTxErrors = false; //Print any transmit errors - bool useV2 = false; //Use the V2 protocol + uint8_t protocolVersion = 1; //Select the radio protocol bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds bool debugDatagrams = false; //Print the datagrams uint16_t overheadTime = 10; ////ms added to ack and datagram times before ACK timeout occurs @@ -312,6 +312,10 @@ typedef struct struct_settings { bool copySerial = true; //Copy the serial parameters to the training client bool copyTriggers = true; //Copy the trigger parameters to the training client uint8_t trainingKey[AES_KEY_BYTES] = { 0x53, 0x70, 0x61, 0x72, 0x6b, 0x46, 0x75, 0x6E, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67 }; + + //Add new parameters immediately before this line + //-- Add commands to set the parameters + //-- Add parameters to routine updateRadioParameters } Settings; Settings settings; From e2cac760d0ff86c89305fdb81139abbbde9a8e2d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 12 Oct 2022 07:55:34 -1000 Subject: [PATCH 027/594] Add setting linkUpDown and command ATS57 to print link status --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 6 ++++-- Firmware/LoRaSerial_Firmware/settings.h | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 085d5663..b109cb44 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -532,6 +532,7 @@ const COMMAND_ENTRY commands[] = {55, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &settings.copyTriggers}, {56, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &settings.trainingKey}, + {57, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &settings.printLinkUpDown}, //Define any user parameters starting at 255 decrementing towards 0 }; diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 1586f7cf..b59f17d0 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -415,6 +415,7 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.echo = params.echo; originalSettings.flowControl = params.flowControl; originalSettings.usbSerialWait = params.usbSerialWait; + originalSettings.printLinkUpDown = params.printLinkUpDown; } //Update the trigger parameters diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index f78a04a1..f0abfaef 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2169,7 +2169,8 @@ void changeState(RadioStates newState) void v2BreakLink() { //Break the link - systemPrintln("--------- Link DOWN ---------"); + if (settings.printLinkUpDown) + systemPrintln("--------- Link DOWN ---------"); triggerEvent(TRIGGER_RADIO_RESET); changeState(RADIO_RESET); } @@ -2196,5 +2197,6 @@ void v2EnterLinkUp() //Start the receiver returnToReceiving(); changeState(RADIO_P2P_LINK_UP); - systemPrintln("========== Link UP =========="); + if (settings.printLinkUpDown) + systemPrintln("========== Link UP =========="); } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 845b3dda..970e7e25 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -312,6 +312,7 @@ typedef struct struct_settings { bool copySerial = true; //Copy the serial parameters to the training client bool copyTriggers = true; //Copy the trigger parameters to the training client uint8_t trainingKey[AES_KEY_BYTES] = { 0x53, 0x70, 0x61, 0x72, 0x6b, 0x46, 0x75, 0x6E, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67 }; + bool printLinkUpDown = false; //Print the link up and link down messages //Add new parameters immediately before this line //-- Add commands to set the parameters From 789ab1149a97f7417df5609388c07afb3ad040e6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 06:55:32 -1000 Subject: [PATCH 028/594] Add selectTraining routine Pass in parameter to determining the set of parameters to use (current or default). --- Firmware/LoRaSerial_Firmware/Commands.ino | 23 +-- Firmware/LoRaSerial_Firmware/States.ino | 8 +- Firmware/LoRaSerial_Firmware/System.ino | 4 +- Firmware/LoRaSerial_Firmware/Train.ino | 219 ++++++++++++---------- 4 files changed, 130 insertions(+), 124 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index b109cb44..1e76db47 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -46,6 +46,8 @@ typedef struct bool commandAT(const char * commandString) { + bool defaultTraining = false; + //'AT' if (commandLength == 2) reportOK(); @@ -71,10 +73,6 @@ bool commandAT(const char * commandString) systemPrintln(" AT&F - Restore factory settings"); systemPrintln(" AT&W - Save current settings to NVM"); break; - case ('F'): //Enter training mode and return to factory defaults - reportOK(); - beginDefaultTraining(); - break; case ('I'): //Shows the radio version reportOK(); @@ -109,17 +107,16 @@ bool commandAT(const char * commandString) reportOK(); } break; + case ('F'): //Enter training mode and return to factory defaults + defaultTraining = true; + //Fall through + // | + // | + // | + // V case ('T'): //Enter training mode reportOK(); - if ((settings.protocolVersion >= 2) && (!settings.pointToPoint)) - { - if (settings.trainingServer) - beginTrainingServer(); - else - beginTrainingClient(); - } - else - beginTraining(); + selectTraining(defaultTraining); break; case ('X'): //Stop the training server if (trainingServerRunning && (!settings.pointToPoint) && settings.trainingServer) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index f0abfaef..81c0f22c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1756,13 +1756,9 @@ void updateRadioState() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= /* - beginTraining beginDefaultTraining - | Save current settings | Save default settings - V | - +<-------------------------------’ + beginTraining | - V - moveToTrainingFreq + | Save settings | V RADIO_TRAINING_TRANSMITTING diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index c0d328bb..612a40cd 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -216,7 +216,7 @@ void updateButton() { setRSSI(0b1111); - beginTraining(); + selectTraining(false); trainState = TRAIN_NO_PRESS; } @@ -228,7 +228,7 @@ void updateButton() { setRSSI(0b1111); - beginDefaultTraining(); + selectTraining(true); trainState = TRAIN_NO_PRESS; } diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 0f9181cb..c73f2d03 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -1,113 +1,31 @@ - -void beginTraining() -{ - if ((settings.debug == true) || (settings.debugTraining == true)) - systemPrintln("Begin point-to-point training"); - - originalSettings = settings; //Make copy of current settings - - moveToTrainingFreq(); -} - -void beginDefaultTraining() -{ - if ((settings.debug == true) || (settings.debugTraining == true)) - systemPrintln("Begin default training"); - - originalSettings = defaultSettings; //Upon completion we will return to default settings - - moveToTrainingFreq(); -} - -//Upon successful exchange of keys, go back to original settings -void endTraining(bool newTrainingAvailable) +//Select the training protocol +void selectTraining(bool defaultTraining) { - settings = originalSettings; //Return to original radio settings - - //Apply new netID and AES if available - if (newTrainingAvailable) + if (settings.protocolVersion >= 2) { - if (lastPacketSize == sizeof(settings.encryptionKey) + 1) //Error check, should be AES key + NetID { - //Move training data into settings - for (uint8_t x = 0 ; x < sizeof(settings.encryptionKey); x++) - settings.encryptionKey[x] = lastPacket[x]; - - settings.netID = lastPacket[lastPacketSize - 1]; //Last spot in array is netID - - if ((settings.debug == true) || (settings.debugTraining == true)) - { - systemPrint("New ID: "); - systemPrintln(settings.netID); - - systemPrint("New Key: "); - for (uint8_t i = 0 ; i < 16 ; i++) - { - systemPrint(settings.encryptionKey[i], HEX); - systemPrint(" "); - } - systemPrintln(); - } - } - else - { - //If the packet was marked as training but was not valid training data, then give up. Return to normal radio mode with pre-existing settings. + if (settings.trainingServer) + beginTrainingServer(); + else + beginTrainingClient(); } } + else if (settings.protocolVersion == 1) + beginTraining(defaultTraining); else { - //We transmitted the training data, move the local training data into settings - for (uint8_t x = 0 ; x < sizeof(settings.encryptionKey); x++) - settings.encryptionKey[x] = trainEncryptionKey[x]; - - settings.netID = trainNetID; //Last spot in array is netID + //Handle unknown future versions + systemPrint("Unknown protocol version: "); + systemPrintln(settings.protocolVersion); + while (1) + petWDT(); } - - recordSystemSettings(); - - generateHopTable(); //Generate frequency table based on current settings - - configureRadio(); //Setup radio with settings - - returnToReceiving(); - - //Blink LEDs to indicate training success - setRSSI(0b0000); - delayWDT(100); - - setRSSI(0b1001); - delayWDT(500); - - setRSSI(0b0110); - delayWDT(500); - - setRSSI(0b1111); - delayWDT(500); - - setRSSI(0b0000); - delayWDT(500); - - setRSSI(0b1111); - delayWDT(1500); - - setRSSI(0); - delayWDT(2000); - - changeState(RADIO_RESET); - - sentFirstPing = false; //Send ping as soon as we exit - - systemPrintln("LINK TRAINED"); } /* - beginTraining beginDefaultTraining - | Save current settings | Save default settings - V | - +<-------------------------------’ + beginTraining | - V - moveToTrainingFreq + | Save settings | V RADIO_TRAINING_TRANSMITTING @@ -127,7 +45,7 @@ void endTraining(bool newTrainingAvailable) endTraining - moveToTrainingFreq + beginTraining 1. Disable point-to-point 2. Disable frequency hopping @@ -146,11 +64,24 @@ void endTraining(bool newTrainingAvailable) 3. Set net ID */ -//Change to known training frequency based on available freq and current major firmware version -//This will allow different minor versions to continue to train to each other -//Send special packet with train = 1, then wait for response -void moveToTrainingFreq() +void beginTraining(bool defaultTraining) { + if ((settings.debug == true) || (settings.debugTraining == true)) + { + systemPrint("Begin "); + if (defaultTraining) + systemPrint("default "); + systemPrintln("point-to-point training"); + } + + //Save the parameters + if (defaultTraining) + originalSettings = defaultSettings; //Upon completion we will return to default settings + else + originalSettings = settings; //Make copy of current settings + + //Change to known training frequency based on available freq and current major firmware version + //This will allow different minor versions to continue to train to each other //During training use default radio settings. This ensures both radios are at known good settings. settings = defaultSettings; //Move to default settings @@ -184,6 +115,7 @@ void moveToTrainingFreq() channels[0] = trainFrequency; //Inject this frequency into the channel table //Transmit general ping packet to see if anyone else is sitting on the training channel + //Send special packet with train = 1, then wait for response sendTrainingPingPacket(); //Recalculate packetAirTime because we need to wait not for a 2-byte response, but a 19 byte response @@ -195,6 +127,87 @@ void moveToTrainingFreq() changeState(RADIO_TRAINING_TRANSMITTING); } +//Upon successful exchange of keys, go back to original settings +void endTraining(bool newTrainingAvailable) +{ + settings = originalSettings; //Return to original radio settings + + //Apply new netID and AES if available + if (newTrainingAvailable) + { + if (lastPacketSize == sizeof(settings.encryptionKey) + 1) //Error check, should be AES key + NetID + { + //Move training data into settings + for (uint8_t x = 0 ; x < sizeof(settings.encryptionKey); x++) + settings.encryptionKey[x] = lastPacket[x]; + + settings.netID = lastPacket[lastPacketSize - 1]; //Last spot in array is netID + + if ((settings.debug == true) || (settings.debugTraining == true)) + { + systemPrint("New ID: "); + systemPrintln(settings.netID); + + systemPrint("New Key: "); + for (uint8_t i = 0 ; i < 16 ; i++) + { + systemPrint(settings.encryptionKey[i], HEX); + systemPrint(" "); + } + systemPrintln(); + } + } + else + { + //If the packet was marked as training but was not valid training data, then give up. Return to normal radio mode with pre-existing settings. + } + } + else + { + //We transmitted the training data, move the local training data into settings + for (uint8_t x = 0 ; x < sizeof(settings.encryptionKey); x++) + settings.encryptionKey[x] = trainEncryptionKey[x]; + + settings.netID = trainNetID; //Last spot in array is netID + } + + recordSystemSettings(); + + generateHopTable(); //Generate frequency table based on current settings + + configureRadio(); //Setup radio with settings + + returnToReceiving(); + + //Blink LEDs to indicate training success + setRSSI(0b0000); + delayWDT(100); + + setRSSI(0b1001); + delayWDT(500); + + setRSSI(0b0110); + delayWDT(500); + + setRSSI(0b1111); + delayWDT(500); + + setRSSI(0b0000); + delayWDT(500); + + setRSSI(0b1111); + delayWDT(1500); + + setRSSI(0); + delayWDT(2000); + + changeState(RADIO_RESET); + + sentFirstPing = false; //Send ping as soon as we exit + + systemPrintln("LINK TRAINED"); +} + //Generate new netID/AES key to share //We assume the user needs to maintain their settings (airSpeed, numberOfChannels, freq min/max, bandwidth/spread/hop) //but need to be on a different netID/AES key. From cd7e1721c08f1785add4a5498378f789066fd15e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 07:02:30 -1000 Subject: [PATCH 029/594] Add routine to verify the V2DatagramTable --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 ++ Firmware/LoRaSerial_Firmware/States.ino | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index a36594e2..8c494b3b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -282,6 +282,8 @@ void setup() verifyRadioStateTable(); //Verify that the state table contains all of the states in increasing order + verifyV2DatagramType(); //Verify that the datagram type table contains all of the datagram types + arch.uniqueID(myUniqueId); //Get the unique ID beginBoard(); //Determine what hardware platform we are running on. diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 81c0f22c..1e2d2a41 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2136,6 +2136,17 @@ void verifyRadioStateTable() } } +//Verify the datagram type table +void verifyV2DatagramType() +{ + if ((sizeof(v2DatagramType) / sizeof(v2DatagramType[0])) != MAX_DATAGRAM_TYPE) + { + systemPrintln("ERROR - Please update the v2DatagramTable"); + while (1) + petWDT(); + } +} + //Change states and print the new state void changeState(RadioStates newState) { From ea75d1a99d7d6dabf7257c3dcecfbbbc6d9104a6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 07:21:33 -1000 Subject: [PATCH 030/594] Add Mp to multi-point training routines --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 6 +++--- Firmware/LoRaSerial_Firmware/States.ino | 6 +++--- Firmware/LoRaSerial_Firmware/Train.ino | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index b59f17d0..c66ea091 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -299,7 +299,7 @@ void xmitDatagramMpDatagram() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Build the client ping packet used for training -void xmitDatagramTrainingPing() +void xmitDatagramMpTrainingPing() { //Add the source (server) ID memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); @@ -322,7 +322,7 @@ void xmitDatagramTrainingPing() } //Build the client ACK packet used for training -void xmitDatagramTrainingAck(uint8_t * serverID) +void xmitDatagramMpTrainingAck(uint8_t * serverID) { //Add the destination (server) ID memcpy(endOfTxData, serverID, UNIQUE_ID_BYTES); @@ -433,7 +433,7 @@ void updateRadioParameters(uint8_t * rxData) //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Build the server parameters packet used for training -void xmitDatagramRadioParameters(const uint8_t * clientID) +void xmitDatagramMpRadioParameters(const uint8_t * clientID) { Settings params; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 1e2d2a41..fe935821 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1081,7 +1081,7 @@ void updateRadioState() updateRadioParameters(&rxData[UNIQUE_ID_BYTES * 2]); //Acknowledge the radio parameters - xmitDatagramTrainingAck(&rxData[UNIQUE_ID_BYTES]); + xmitDatagramMpTrainingAck(&rxData[UNIQUE_ID_BYTES]); changeState(RADIO_MP_WAIT_TX_PARAM_ACK_DONE); break; } @@ -1099,7 +1099,7 @@ void updateRadioState() //Check for a receive timeout else if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) - xmitDatagramTrainingPing(); + xmitDatagramMpTrainingPing(); break; case RADIO_MP_WAIT_TX_PARAM_ACK_DONE: @@ -1168,7 +1168,7 @@ void updateRadioState() case DATAGRAM_TRAINING_PING: //Save the client ID memcpy(trainingPartnerID, rxData, UNIQUE_ID_BYTES); - xmitDatagramRadioParameters(trainingPartnerID); + xmitDatagramMpRadioParameters(trainingPartnerID); //Wait for the transmit to complete changeState(RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE); diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index c73f2d03..a23db4fa 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -282,7 +282,7 @@ void beginTrainingClient() commonTrainingInitialization(); //Transmit client ping to the training server - xmitDatagramTrainingPing(); + xmitDatagramMpTrainingPing(); //Set the next state changeState(RADIO_MP_WAIT_TX_TRAINING_PING_DONE); From 3b4d9604228f4473736a76ed0e68eae52bbbc9f1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 07:28:51 -1000 Subject: [PATCH 031/594] Pet the angry watchdog --- Firmware/LoRaSerial_Firmware/Radio.ino | 3 +++ Firmware/LoRaSerial_Firmware/Train.ino | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 176e10ca..657ddf12 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1066,12 +1066,14 @@ void generateHopTable() if ((settings.debug == true) || (settings.debugRadio == true)) { + petWDT(); systemPrint("channelSpacing: "); systemPrintln(channelSpacing, 3); systemPrintln("Channel table:"); for (int x = 0 ; x < settings.numberOfChannels ; x++) { + petWDT(); systemPrint(x); systemPrint(": "); systemPrint(channels[x], 3); @@ -1081,6 +1083,7 @@ void generateHopTable() systemPrint("AES IV:"); for (uint8_t i = 0 ; i < sizeof(AESiv) ; i++) { + petWDT(); systemPrint(" 0x"); systemPrint(AESiv[i], HEX); } diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index a23db4fa..aa484f8a 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -130,6 +130,7 @@ void beginTraining(bool defaultTraining) //Upon successful exchange of keys, go back to original settings void endTraining(bool newTrainingAvailable) { + petWDT(); settings = originalSettings; //Return to original radio settings //Apply new netID and AES if available @@ -171,8 +172,10 @@ void endTraining(bool newTrainingAvailable) settings.netID = trainNetID; //Last spot in array is netID } + petWDT(); recordSystemSettings(); + petWDT(); generateHopTable(); //Generate frequency table based on current settings configureRadio(); //Setup radio with settings @@ -373,6 +376,7 @@ void commonTrainingInitialization() channels[0] = trainFrequency; //Inject this frequency into the channel table //Use only the first channel of the previously allocated channel table + petWDT(); configureRadio(); //Setup radio with settings } @@ -388,6 +392,7 @@ void endClientServerTraining(uint8_t event) if (!settings.trainingServer) { //Record the new client settings + petWDT(); recordSystemSettings(); systemPrint("Link trained from "); From 9a65c1c3142fec4c191b92d75770b6960830363d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 07:31:46 -1000 Subject: [PATCH 032/594] V2: Add pointToPoint parameter to updateRadioParameters --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 4 ++-- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index c66ea091..a6592810 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -348,7 +348,7 @@ void xmitDatagramMpTrainingAck(uint8_t * serverID) transmitDatagram(); } -void updateRadioParameters(uint8_t * rxData) +void updateRadioParameters(uint8_t * rxData, bool pointToPoint) { Settings params; @@ -358,7 +358,7 @@ void updateRadioParameters(uint8_t * rxData) //Update the radio parameters originalSettings.airSpeed = params.airSpeed; originalSettings.netID = params.netID; - originalSettings.pointToPoint = false; + originalSettings.pointToPoint = pointToPoint; originalSettings.encryptData = params.encryptData; memcpy(originalSettings.encryptionKey, params.encryptionKey, sizeof(originalSettings.encryptionKey)); originalSettings.dataScrambling = params.dataScrambling; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index fe935821..06aecf36 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1078,7 +1078,7 @@ void updateRadioState() memcpy(trainingPartnerID, &rxData[UNIQUE_ID_BYTES], UNIQUE_ID_BYTES); //Get the radio parameters - updateRadioParameters(&rxData[UNIQUE_ID_BYTES * 2]); + updateRadioParameters(&rxData[UNIQUE_ID_BYTES * 2], false); //Acknowledge the radio parameters xmitDatagramMpTrainingAck(&rxData[UNIQUE_ID_BYTES]); From ca9aca0a52810fa55168ded4719dde9acff7022c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 07:42:01 -1000 Subject: [PATCH 033/594] V2: Add point-to-point training --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 52 ++++++++ Firmware/LoRaSerial_Firmware/States.ino | 153 ++++++++++++++++++++--- Firmware/LoRaSerial_Firmware/Train.ino | 38 +++++- Firmware/LoRaSerial_Firmware/settings.h | 65 ++++++---- 4 files changed, 263 insertions(+), 45 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index a6592810..da4cecc8 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -34,6 +34,58 @@ const uint16_t crc16Table[256] = 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 }; +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Point-To-Point Training +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Ping the other radio in the point-to-point configuration +void xmitDatagramP2PTrainingPing() +{ + /* + endOfTxData ---. + | + V + +---------+----------+ + | | | + | Control | Trailer | + | 8 bits | n Bytes | + +---------+----------+ + */ + + txControl.datagramType = DATAGRAM_P2P_TRAINING_PING; + txControl.ackNumber = 0; + transmitDatagram(); +} + +//Build the parameters packet used for training +void xmitDatagramP2pTrainingParams() +{ + Settings params; + + //Initialize the radio parameters + memcpy(¶ms, &originalSettings, sizeof(settings)); + params.pointToPoint = true; + + //Add the radio parameters + memcpy(endOfTxData, ¶ms, sizeof(params)); + endOfTxData += sizeof(params); + + /* + endOfTxData ---. + | + V + +----------+---------+--- ... ---+----------+ + | Optional | | Radio | Optional | + | NET ID | Control | Parameters | Trailer | + | 8 bits | 8 bits | n bytes | n Bytes | + +----------+---------+-------------+----------+ + */ + + txControl.datagramType = DATAGRAM_P2P_TRAINING_PARAMS; + txControl.ackNumber = 0; + transmitDatagram(); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Point-To-Point: Bring up the link // diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 06aecf36..10bb1431 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -83,6 +83,117 @@ void updateRadioState() } break; + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //V2 - Point-to-Point Training + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + /* + beginTrainingPointToPoint + | + | Save settings + | + | TX DATAGRAM_P2P_TRAINING_PING + | + V + RADIO_P2P_TRAINING_WAIT_PING_DONE + | + V RX DATAGRAM_P2P_TRAINING_PARAMS + RADIO_P2P_WAIT_FOR_TRAINING_PARAMS -----------. + | | + | RX DATAGRAM_P2P_TRAINING_PING | + | TX DATAGRAM_P2P_TRAINING_PARAMS | + | | + V | + RADIO_P2P_WAIT_TRAINING_ACK_DONE | + | | + +<------------------------------------’ + | + V + RADIO_RESET + */ + + //Wait for the PING to complete transmission + case RADIO_P2P_TRAINING_WAIT_PING_DONE: + if (transactionComplete) + { + transactionComplete = false; //Reset ISR flag + returnToReceiving(); + changeState(RADIO_P2P_WAIT_FOR_TRAINING_PARAMS); + } + break; + + case RADIO_P2P_WAIT_FOR_TRAINING_PARAMS: + updateRSSI(); + + //Check for a received datagram + if (transactionComplete == true) + { + transactionComplete = false; //Reset ISR flag + + //Decode the received datagram + PacketType packetType = rcvDatagram(); + + //Process the received datagram + switch (packetType) + { + default: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_P2P_TRAINING_PING: + //Display the signal strength + if (settings.displayPacketQuality == true) + { + systemPrintln(); + systemPrint("R:"); + systemPrint(radio.getRSSI()); + systemPrint("\tS:"); + systemPrint(radio.getSNR()); + systemPrint("\tfE:"); + systemPrint(radio.getFrequencyError()); + systemPrintln(); + } + + triggerEvent(TRIGGER_TRAINING_CONTROL_PACKET); + + //Send the parameters + xmitDatagramP2pTrainingParams(); + changeState(RADIO_P2P_WAIT_TRAINING_PARAMS_DONE); + break; + + case DATAGRAM_P2P_TRAINING_PARAMS: + triggerEvent(TRIGGER_TRAINING_DATA_PACKET); + + //Update the parameters + updateRadioParameters(rxData, true); + endPointToPointTraining(true); + changeState(RADIO_RESET); + } + } + + //If the radio is available, send any data in the serial buffer over the radio + else if (receiveInProcess() == false) + { + //Check for a receive timeout + if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) + { + triggerEvent(TRIGGER_TRAINING_NO_ACK); + retransmitDatagram(); + changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); + } + } + break; + + case RADIO_P2P_WAIT_TRAINING_PARAMS_DONE: + if (transactionComplete) + { + transactionComplete = false; //Reset ISR flag + endPointToPointTraining(false); + changeState(RADIO_RESET); + } + break; + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //V2 - No Link //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1929,38 +2040,44 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_TRAINING_ACK_WAIT, "TRAINING_ACK_WAIT", "[Training] Ack Wait"}, //14 {RADIO_TRAINING_RECEIVED_PACKET, "TRAINING_RECEIVED_PACKET", "[Training] RX Packet"}, //15 + //V2 - Point-to-Point Training + // State Name Description + {RADIO_P2P_TRAINING_WAIT_PING_DONE, "P2P_TRAINING_WAIT_PING_DONE", "V2 P2P: Wait TX Training Ping Done"}, //16 + {RADIO_P2P_WAIT_FOR_TRAINING_PARAMS, "P2P_WAIT_FOR_TRAINING_PARAMS", "V2 P2P: Wait for Training params"}, //17 + {RADIO_P2P_WAIT_TRAINING_PARAMS_DONE, "P2P_WAIT_TRAINING_PARAMS_DONE", "V2 P2P: Wait training params done"}, //18 + //V2 - Point-to-Point link handshake // State Name Description - {RADIO_P2P_LINK_DOWN, "P2P_LINK_DOWN", "V2 P2P: [No Link] Waiting for Ping"}, //16 - {RADIO_P2P_WAIT_TX_PING_DONE, "P2P_WAIT_TX_PING_DONE", "V2 P2P: [No Link] Wait Ping TX Done"},//17 - {RADIO_P2P_WAIT_ACK_1, "P2P_WAIT_ACK_1", "V2 P2P: [No Link] Waiting for ACK1"}, //18 - {RADIO_P2P_WAIT_TX_ACK_1_DONE, "P2P_WAIT_TX_ACK_1_DONE", "V2 P2P: [No Link] Wait ACK1 TX Done"},//19 - {RADIO_P2P_WAIT_ACK_2, "P2P_WAIT_ACK_2", "V2 P2P: [No Link] Waiting for ACK2"}, //20 - {RADIO_P2P_WAIT_TX_ACK_2_DONE, "P2P_WAIT_TX_ACK_2_DONE", "V2 P2P: [No Link] Wait ACK2 TX Done"},//21 + {RADIO_P2P_LINK_DOWN, "P2P_LINK_DOWN", "V2 P2P: [No Link] Waiting for Ping"}, //19 + {RADIO_P2P_WAIT_TX_PING_DONE, "P2P_WAIT_TX_PING_DONE", "V2 P2P: [No Link] Wait Ping TX Done"},//20 + {RADIO_P2P_WAIT_ACK_1, "P2P_WAIT_ACK_1", "V2 P2P: [No Link] Waiting for ACK1"}, //21 + {RADIO_P2P_WAIT_TX_ACK_1_DONE, "P2P_WAIT_TX_ACK_1_DONE", "V2 P2P: [No Link] Wait ACK1 TX Done"},//22 + {RADIO_P2P_WAIT_ACK_2, "P2P_WAIT_ACK_2", "V2 P2P: [No Link] Waiting for ACK2"}, //23 + {RADIO_P2P_WAIT_TX_ACK_2_DONE, "P2P_WAIT_TX_ACK_2_DONE", "V2 P2P: [No Link] Wait ACK2 TX Done"},//24 //V2 - Point-to-Point, link up, data exchange // State Name Description - {RADIO_P2P_LINK_UP, "P2P_LINK_UP", "V2 P2P: Receiving Standby"}, //22 - {RADIO_P2P_LINK_UP_WAIT_ACK_DONE, "P2P_LINK_UP_WAIT_ACK_DONE", "V2 P2P: Waiting ACK TX Done"}, //23 - {RADIO_P2P_LINK_UP_WAIT_TX_DONE, "P2P_LINK_UP_WAIT_TX_DONE", "V2 P2P: Waiting TX done"}, //24 - {RADIO_P2P_LINK_UP_WAIT_ACK, "P2P_LINK_UP_WAIT_ACK", "V2 P2P: Waiting for ACK"}, //25 - {RADIO_P2P_LINK_UP_HB_ACK_REXMT, "P2P_LINK_UP_HB_ACK_REXMT", "V2 P2P: Heartbeat ACK ReXmt"}, //26 + {RADIO_P2P_LINK_UP, "P2P_LINK_UP", "V2 P2P: Receiving Standby"}, //25 + {RADIO_P2P_LINK_UP_WAIT_ACK_DONE, "P2P_LINK_UP_WAIT_ACK_DONE", "V2 P2P: Waiting ACK TX Done"}, //26 + {RADIO_P2P_LINK_UP_WAIT_TX_DONE, "P2P_LINK_UP_WAIT_TX_DONE", "V2 P2P: Waiting TX done"}, //27 + {RADIO_P2P_LINK_UP_WAIT_ACK, "P2P_LINK_UP_WAIT_ACK", "V2 P2P: Waiting for ACK"}, //28 + {RADIO_P2P_LINK_UP_HB_ACK_REXMT, "P2P_LINK_UP_HB_ACK_REXMT", "V2 P2P: Heartbeat ACK ReXmt"}, //29 //V2 - Multi-Point data exchange // State Name Description - {RADIO_MP_STANDBY, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //27 - {RADIO_MP_WAIT_TX_DONE, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //28 + {RADIO_MP_STANDBY, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //30 + {RADIO_MP_WAIT_TX_DONE, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //31 //V2 - Multi-Point training client states // State Name Description - {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, "MP_WAIT_TX_TRAINING_PING_DONE", "V2 MP: Wait TX training PING done"}, //29 - {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, "MP_WAIT_RX_RADIO_PARAMETERS", "V2 MP: Wait for radio parameters"}, //30 - {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, "MP_WAIT_TX_PARAM_ACK_DONE", "V2 MP: Wait for TX param ACK done"}, //31 + {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, "MP_WAIT_TX_TRAINING_PING_DONE", "V2 MP: Wait TX training PING done"}, //32 + {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, "MP_WAIT_RX_RADIO_PARAMETERS", "V2 MP: Wait for radio parameters"}, //33 + {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, "MP_WAIT_TX_PARAM_ACK_DONE", "V2 MP: Wait for TX param ACK done"}, //34 //V2 - Multi-Point training server states // State Name Description - {RADIO_MP_WAIT_FOR_TRAINING_PING, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //32 - {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //33 + {RADIO_MP_WAIT_FOR_TRAINING_PING, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //35 + {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //36 }; void verifyRadioStateTable() diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index aa484f8a..c296bc38 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -3,6 +3,9 @@ void selectTraining(bool defaultTraining) { if (settings.protocolVersion >= 2) { + if (settings.pointToPoint) + beginTrainingPointToPoint(defaultTraining); + else { if (settings.trainingServer) beginTrainingServer(); @@ -273,6 +276,36 @@ void updateCylonLEDs() } } +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//V2 Point-To-Point Training +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void beginTrainingPointToPoint(bool defaultTraining) +{ + if (defaultTraining) + { + settings = defaultSettings; //Upon completion we will return to default settings + systemPrint("Default "); + } + systemPrintln("Point-to-point training"); + + //Common initialization + commonTrainingInitialization(); + + //Transmit general ping packet to see if anyone else is sitting on the training channel + xmitDatagramP2PTrainingPing(); + + //Set the next state + changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); +} + +void endPointToPointTraining(bool saveParams) +{ + memcpy(&settings, &originalSettings, sizeof(settings)); + if (saveParams) + recordSystemSettings(); +} + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //V2 Multi-Point Client/Server Training //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -306,7 +339,6 @@ void beginTrainingServer() //Common initialization commonTrainingInitialization(); - memcpy(&settings.trainingKey, &originalSettings.trainingKey, AES_KEY_BYTES); settings.trainingServer = true; //52: Operate as the training server //Start the receive operation @@ -332,6 +364,7 @@ void commonTrainingInitialization() settings.printParameterName = true; //28: Print the parameter names settings.verifyRxNetID = false; //37: Disable netID checking settings.enableCRC16 = true; //49: Use CRC-16 + memcpy(&settings.trainingKey, &originalSettings.trainingKey, AES_KEY_BYTES); //56: Common training key //Determine the components of the frame header and trailer selectHeaderAndTrailerBytes(); @@ -368,6 +401,9 @@ void commonTrainingInitialization() //Reset cylon variables startCylonLEDs(); + petWDT(); + generateHopTable(); //Generate frequency table based on current settings + //Select the training frequency, a multiple of channels down from the maximum petWDT(); float channelSpacing = (settings.frequencyMax - settings.frequencyMin) / (float)(settings.numberOfChannels + 2); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 970e7e25..ad6f546e 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -2,7 +2,7 @@ typedef enum { RADIO_RESET = 0, - // V1 + //V1 RADIO_NO_LINK_RECEIVING_STANDBY, RADIO_NO_LINK_TRANSMITTING, RADIO_NO_LINK_ACK_WAIT, @@ -22,8 +22,13 @@ typedef enum RADIO_TRAINING_ACK_WAIT, RADIO_TRAINING_RECEIVED_PACKET, - // V2 - // Point-To-Point: Bring up the link + //V2 + //Point-to-Point Training + RADIO_P2P_TRAINING_WAIT_PING_DONE, + RADIO_P2P_WAIT_FOR_TRAINING_PARAMS, + RADIO_P2P_WAIT_TRAINING_PARAMS_DONE, + + //Point-To-Point: Bring up the link RADIO_P2P_LINK_DOWN, RADIO_P2P_WAIT_TX_PING_DONE, RADIO_P2P_WAIT_ACK_1, @@ -31,23 +36,23 @@ typedef enum RADIO_P2P_WAIT_ACK_2, RADIO_P2P_WAIT_TX_ACK_2_DONE, - // Point-to-Point: Link up, data exchange + //Point-to-Point: Link up, data exchange RADIO_P2P_LINK_UP, RADIO_P2P_LINK_UP_WAIT_ACK_DONE, RADIO_P2P_LINK_UP_WAIT_TX_DONE, RADIO_P2P_LINK_UP_WAIT_ACK, RADIO_P2P_LINK_UP_HB_ACK_REXMT, - // Multi-Point: Datagrams + //Multi-Point: Datagrams RADIO_MP_STANDBY, RADIO_MP_WAIT_TX_DONE, - //Training client states + //Multi-Point Training client states RADIO_MP_WAIT_TX_TRAINING_PING_DONE, RADIO_MP_WAIT_RX_RADIO_PARAMETERS, RADIO_MP_WAIT_TX_PARAM_ACK_DONE, - //Training server states + //Multi-Point Training server states RADIO_MP_WAIT_FOR_TRAINING_PING, RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, @@ -66,26 +71,30 @@ typedef struct _RADIO_STATE_ENTRY typedef enum { //V2 packet types must start at zero + //V2: Point-to-Point training + DATAGRAM_P2P_TRAINING_PING = 0, // 0 + DATAGRAM_P2P_TRAINING_PARAMS, // 1 + //V2: Link establishment handshake - DATAGRAM_PING = 0, - DATAGRAM_ACK_1, - DATAGRAM_ACK_2, + DATAGRAM_PING, // 3 + DATAGRAM_ACK_1, // 4 + DATAGRAM_ACK_2, // 5 //V2: Point-to-Point data exchange - DATAGRAM_DATA, - DATAGRAM_SF6_DATA, - DATAGRAM_DATA_ACK, - DATAGRAM_HEARTBEAT, - DATAGRAM_REMOTE_COMMAND, - DATAGRAM_REMOTE_COMMAND_RESPONSE, + DATAGRAM_DATA, // 6 + DATAGRAM_SF6_DATA, // 7 + DATAGRAM_DATA_ACK, // 8 + DATAGRAM_HEARTBEAT, // 9 + DATAGRAM_REMOTE_COMMAND, //10 + DATAGRAM_REMOTE_COMMAND_RESPONSE, //11 //V2: Multi-Point data exchange - DATAGRAM_DATAGRAM, + DATAGRAM_DATAGRAM, //12 //V2: Multi-Point training exchange - DATAGRAM_TRAINING_PING, - DATAGRAM_TRAINING_PARAMS, - DATAGRAM_TRAINING_ACK, + DATAGRAM_TRAINING_PING, //13 + DATAGRAM_TRAINING_PARAMS, //14 + DATAGRAM_TRAINING_ACK, //15 //Add new V2 datagram types before this line MAX_DATAGRAM_TYPE, @@ -109,12 +118,16 @@ typedef enum } PacketType; const char * const v2DatagramType[] = -{// 0 1 2 3 4 5 6 - "PING", "ACK-1", "ACK-2", "DATA", "SF6-DATA", "DATA-ACK", "HEARTBEAT", - // 7 8 9 10 - "RMT-CMD", "RMT_RESP", "DATAGRAM_DATAGRAM", "TRAINING_PING", - // 11 12 - "TRAINING_PARAMS", "TRAINING_ACK" +{// 0 1 + "P2P_TRAINING_PING", "P2P_TRAINING_PARAMS", + // 3 4 5 + "PING", "ACK-1", "ACK-2", + // 6 7 8 9 10 11 + "DATA", "SF6-DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", + // 12 + "DATAGRAM_DATAGRAM", + // 13 14 15 + "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK" }; //Train button states From 2934fd022fd9fe862f42632e8efb9d58279ec7ae Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 14 Oct 2022 09:48:54 -0600 Subject: [PATCH 034/594] Add ackAirTime debug printing --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 7573c63f..c7a9f41a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -474,6 +474,8 @@ void configureRadio() systemPrintln(hoppingPeriod); systemPrint("controlPacketAirTime: "); systemPrintln(controlPacketAirTime); + systemPrint("ackAirTime: "); + systemPrintln(ackAirTime); } if (success == false) From 1ddd097276e27010c7afd7f6477721e0175f1339 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 14 Oct 2022 11:00:16 -0600 Subject: [PATCH 035/594] Fix triggerEnable2 typos --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- Firmware/LoRaSerial_Firmware/Train.ino | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 1e76db47..e5caf9b4 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -510,7 +510,7 @@ const COMMAND_ENTRY commands[] = {39, 0, 1, 0, TYPE_BOOL, valInt, "TriggerWidthIsMultiplier", &settings.triggerWidthIsMultiplier}, {40, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable: 31-0", &settings.triggerEnable}, - {41, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable: 63-32", &settings.triggerEnable}, + {41, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable2: 63-32", &settings.triggerEnable2}, {42, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &settings.debugReceive}, {43, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, {44, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index c296bc38..012d9457 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -388,7 +388,7 @@ void commonTrainingInitialization() settings.triggerWidthIsMultiplier = originalSettings.triggerWidthIsMultiplier; settings.triggerEnable = originalSettings.triggerEnable; - settings.triggerEnable = originalSettings.triggerEnable; + settings.triggerEnable2 = originalSettings.triggerEnable2; settings.debugReceive = originalSettings.debugReceive; settings.debugTransmit = originalSettings.debugTransmit; settings.printTxErrors = originalSettings.printTxErrors; @@ -444,4 +444,3 @@ void endClientServerTraining(uint8_t event) systemFlush(); systemReset(); } - From 436b437048b2a9914109a157ab467207fc0e366a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 10:11:11 -1000 Subject: [PATCH 036/594] Add missing the ATSx? commands to the validation script --- Firmware/Tools/Command_Validation_Script.txt | 32 +++++++- .../Results/Command_Validation_Results.txt | 74 ++++++++++++++++--- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 8b35e23a..2c1dec2b 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -393,99 +393,125 @@ ats38=1 ats38=255 ats38=256 ats38=25 +ats38? # TriggerWidthIsMultiplier ats39=0 ats39=2 ats39=1 +ats39? # TriggerEnable: 31-0 ats40=0 ats40=4294967295 +ats40? # TriggerEnable: 63-32 ats41=0 ats41=4294967295 +ats41? # DebugReceive ats42=1 ats42=2 ats42=0 +ats42? # DebugTransmit ats43=1 ats43=2 ats43=0 +ats43? # PrintTxErrors ats44=1 ats44=2 ats44=0 +ats44? -# useV2 +# protocolVersion +ats45=0 ats45=1 ats45=2 -ats45=0 +ats45=3 +ats45? # PrintTimestamp ats46=1 ats46=2 ats46=0 +ats46? # debugDatagrams ats47=1 ats47=2 ats47=0 +ats47? # txAckMillis ats48=1 ats48=2 ats48=0 +ats48? # enableCRC16 ats49=1 ats49=2 ats49=0 +ats49? # DisplayRealMillis ats50=1 ats50=2 ats50=0 +ats50? # TrainingServer ats51=1 ats51=2 ats51=0 +ats51? # ClientRetryInterval ats52=1 ats52=2 ats52=0 +ats52? # CopyDebug ats53=1 ats53=2 ats53=0 +ats53? # CopySerial ats54=1 ats54=2 ats54=0 +ats54? # CopyTriggers ats55=1 ats55=2 ats55=0 +ats55? # TrainingKey ats56=01020304050607080910111213141516 ats56=0102030405060708091011121314151 ats56=010203040506070809101112131415160 +ats56? -# Invalid commands +# PrintLinkUpDown ats57=1 +ats57=2 +ats57=0 ats57? +# Invalid commands +ats58=1 +ats58? + ats255=1 ats255? diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index c66581a0..07fc4f3b 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -42,7 +42,7 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati3 -157 ati4 -175 +167 ati5 506 ati6 @@ -50,7 +50,7 @@ ati6 ati7 0 ati8 -E5D60B0B4A34555020312E34212815FF +CB0C64334A34555020312E34242815FF ati9 ERROR ati0 @@ -99,7 +99,7 @@ ATS41:TriggerEnable: 63-32=-1 ATS42:DebugReceive=0 ATS43:DebugTransmit=0 ATS44:PrintTxErrors=0 -ATS45:UseV2=0 +ATS45:protocolVersion=1 ATS46:PrintTimestamp=0 ATS47:DebugDatagrams=0 ATS48:OverHeadtime=10 @@ -111,6 +111,7 @@ ATS53:CopyDebug=1 ATS54:CopySerial=1 ATS55:CopyTriggers=1 ATS56:TrainingKey=537061726B46756E547261696E696E67 +ATS57:PrintLinkUpDown=0 ats28=0 OK # SerialSpeed @@ -808,6 +809,8 @@ ERROR ats38=25 TriggerWidth=25 OK +ats38? +TriggerWidth=25 # TriggerWidthIsMultiplier ERROR ats39=0 @@ -818,6 +821,8 @@ ERROR ats39=1 TriggerWidthIsMultiplier=1 OK +ats39? +TriggerWidthIsMultiplier=1 # TriggerEnable: 31-0 ERROR ats40=0 @@ -826,6 +831,8 @@ OK ats40=4294967295 TriggerEnable: 31-0=-1 OK +ats40? +TriggerEnable: 31-0=-1 # TriggerEnable: 63-32 ERROR ats41=0 @@ -834,6 +841,8 @@ OK ats41=4294967295 TriggerEnable: 63-32=-1 OK +ats41? +TriggerEnable: 63-32=-1 # DebugReceive ERROR ats42=1 @@ -844,6 +853,8 @@ ERROR ats42=0 DebugReceive=0 OK +ats42? +DebugReceive=0 # DebugTransmit ERROR ats43=1 @@ -854,6 +865,8 @@ ERROR ats43=0 DebugTransmit=0 OK +ats43? +DebugTransmit=0 # PrintTxErrors ERROR ats44=1 @@ -864,16 +877,22 @@ ERROR ats44=0 PrintTxErrors=0 OK -# useV2 +ats44? +PrintTxErrors=0 +# protocolVersion +ERROR +ats45=0 ERROR ats45=1 -UseV2=1 +protocolVersion=1 OK ats45=2 -ERROR -ats45=0 -UseV2=0 +protocolVersion=2 OK +ats45=3 +ERROR +ats45? +protocolVersion=2 # PrintTimestamp ERROR ats46=1 @@ -884,6 +903,8 @@ ERROR ats46=0 PrintTimestamp=0 OK +ats46? +PrintTimestamp=0 # debugDatagrams ERROR ats47=1 @@ -894,6 +915,8 @@ ERROR ats47=0 DebugDatagrams=0 OK +ats47? +DebugDatagrams=0 # txAckMillis ERROR ats48=1 @@ -905,6 +928,8 @@ OK ats48=0 OverHeadtime=0 OK +ats48? +OverHeadtime=0 # enableCRC16 ERROR ats49=1 @@ -915,6 +940,8 @@ ERROR ats49=0 EnableCRC16=0 OK +ats49? +EnableCRC16=0 # DisplayRealMillis ERROR ats50=1 @@ -925,6 +952,8 @@ ERROR ats50=0 DisplayRealMillis=0 OK +ats50? +DisplayRealMillis=0 # TrainingServer ERROR ats51=1 @@ -935,6 +964,8 @@ ERROR ats51=0 TrainingServer=0 OK +ats51? +TrainingServer=0 # ClientRetryInterval ERROR ats52=1 @@ -945,6 +976,8 @@ ClientRetryInterval=2 OK ats52=0 ERROR +ats52? +ClientRetryInterval=2 # CopyDebug ERROR ats53=1 @@ -955,6 +988,8 @@ ERROR ats53=0 CopyDebug=0 OK +ats53? +CopyDebug=0 # CopySerial ERROR ats54=1 @@ -965,6 +1000,8 @@ ERROR ats54=0 CopySerial=0 OK +ats54? +CopySerial=0 # CopyTriggers ERROR ats55=1 @@ -975,6 +1012,8 @@ ERROR ats55=0 CopyTriggers=0 OK +ats55? +CopyTriggers=0 # TrainingKey ERROR ats56=01020304050607080910111213141516 @@ -984,11 +1023,25 @@ ats56=0102030405060708091011121314151 ERROR ats56=010203040506070809101112131415160 ERROR -# Invalid commands +ats56? +TrainingKey=01020304050607080910111213141516 +# PrintLinkUpDown ERROR ats57=1 +PrintLinkUpDown=1 +OK +ats57=2 ERROR +ats57=0 +PrintLinkUpDown=0 +OK ats57? +PrintLinkUpDown=0 +# Invalid commands +ERROR +ats58=1 +ERROR +ats58? ERROR ats255=1 ERROR @@ -1033,11 +1086,13 @@ ATS48:OverHeadtime=0 ATS3:PointToPoint=1 ATS17:PreambleLength=8 ATS29:PrintFrequency=0 +ATS57:PrintLinkUpDown=0 ATS28:PrintParameterName=1 ATS36:PrintPktData=0 ATS35:PrintRfData=0 ATS46:PrintTimestamp=0 ATS44:PrintTxErrors=0 +ATS45:protocolVersion=2 ATS0:SerialSpeed=57600 ATS27:SortParametersByName=1 ATS14:SpreadFactor=9 @@ -1050,7 +1105,6 @@ ATS38:TriggerWidth=25 ATS39:TriggerWidthIsMultiplier=1 ATS7:TxPower=30 ATS34:UsbSerialWait=0 -ATS45:UseV2=0 ATS37:VerifyRxNetID=0 at&f OK From e787b2941142bfc01e0c60baf0a643c7e669f971 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 10:23:26 -1000 Subject: [PATCH 037/594] Add ATG command to generate new netID and encryption key --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 1e76db47..c2b1d488 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -61,6 +61,7 @@ bool commandAT(const char * commandString) systemPrintln("Command summary:"); systemPrintln(" AT? - Print the command summary"); systemPrintln(" ATF - Enter training mode and return to factory defaults"); + systemPrintln(" ATG - Generate new netID and encryption key"); systemPrintln(" ATI - Display the radio version"); systemPrintln(" ATI? - Display the information commands"); systemPrintln(" ATIn - Display system information"); @@ -73,6 +74,10 @@ bool commandAT(const char * commandString) systemPrintln(" AT&F - Restore factory settings"); systemPrintln(" AT&W - Save current settings to NVM"); break; + case ('G'): //Generate a new netID and encryption key + generateTrainingSettings(); + reportOK(); + break; case ('I'): //Shows the radio version reportOK(); From 7074adc22e0346457943098e68bb8053eb0b06e3 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 10:49:28 -1000 Subject: [PATCH 038/594] Initial state is RADIO_RESET --- Firmware/LoRaSerial_Firmware/Commands.ino | 7 +------ Firmware/LoRaSerial_Firmware/settings.h | 3 ++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index c2b1d488..aec59a98 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -102,14 +102,9 @@ bool commandAT(const char * commandString) configureRadio(); //Apply any new settings setRSSI(0); //Turn off LEDs - if (settings.pointToPoint == true) - changeState(RADIO_NO_LINK_RECEIVING_STANDBY); - else - changeState(RADIO_BROADCASTING_RECEIVING_STANDBY); - inCommandMode = false; //Return to printing normal RF serial data - reportOK(); + changeState(RADIO_RESET); } break; case ('F'): //Enter training mode and return to factory defaults diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index ad6f546e..cd4311ad 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -58,7 +58,8 @@ typedef enum RADIO_MAX_STATE, } RadioStates; -RadioStates radioState = RADIO_NO_LINK_RECEIVING_STANDBY; + +RadioStates radioState = RADIO_RESET; typedef struct _RADIO_STATE_ENTRY { From 9ee02432d638be03bdff55c5204b903921415df9 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 11:56:16 -1000 Subject: [PATCH 039/594] V2: Multipoint remove V1 references --- Firmware/LoRaSerial_Firmware/States.ino | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 10bb1431..24ae9c82 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1058,7 +1058,6 @@ void updateRadioState() { triggerEvent(TRIGGER_BROADCAST_DATA_PACKET); xmitDatagramMpDatagram(); - sendDataPacket(); changeState(RADIO_MP_WAIT_TX_DONE); } } @@ -1066,7 +1065,7 @@ void updateRadioState() } //End processWaitingSerial //Toggle 2 LEDs if we have recently transmitted - if (millis() - packetTimestamp < 5000) + if (millis() - datagramTimer < 5000) { if (millis() - lastLinkBlink > 250) //Blink at 4Hz { From fc43fca05877110cf55807178f18b4db10d0be0e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 13 Oct 2022 15:35:02 -1000 Subject: [PATCH 040/594] Remove V1 Radio Protocol Switch to using V2 by default Remove V1 states from state machine Remove V1 training Remove V1 radio protocol routines Remove V1 states Regroup global variables Remove unused V1 variables Rename PACKET_ to DATAGRAM_ and add V2 in MAX_V2_DATAGRAM_TYPE Add link quality metrics Rename packetSent to frameSentCount Fix counting of transmitted frames. Rename controlPacketAirTime to ackAirTime Rename packetAirTime and datagramAirTime to frameAirTime --- Firmware/LoRaSerial_Firmware/Commands.ino | 41 +- .../LoRaSerial_Firmware.ino | 79 +- Firmware/LoRaSerial_Firmware/Radio.ino | 767 +---------------- Firmware/LoRaSerial_Firmware/RadioV2.ino | 55 +- Firmware/LoRaSerial_Firmware/States.ino | 768 +----------------- Firmware/LoRaSerial_Firmware/Train.ino | 202 +---- Firmware/LoRaSerial_Firmware/settings.h | 93 ++- Firmware/Tools/Command_Validation_Script.txt | 7 + .../Results/Command_Validation_Results.txt | 30 +- 9 files changed, 227 insertions(+), 1815 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index aec59a98..2790a655 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -92,7 +92,6 @@ bool commandAT(const char * commandString) if (printerEndpoint == PRINT_TO_RF) { //If we are pointed at the RF link, send ok and wait for response ACK before applying settings - confirmDeliveryBeforeRadioConfig = true; reportOK(); } else @@ -140,8 +139,6 @@ bool commandAT(const char * commandString) //ATIx commands else if (commandString[2] == 'I' && commandLength == 4) { - uint8_t uniqueID[UNIQUE_ID_BYTES]; - switch (commandString[3]) { case ('?'): //ATI? - Display the information commands @@ -154,6 +151,13 @@ bool commandAT(const char * commandString) systemPrintln(" ATI6 - Display AES key"); systemPrintln(" ATI7 - Show current FHSS channel"); systemPrintln(" ATI8 - Display unique ID"); + systemPrintln(" ATI9 - Display the total datagrams sent"); + systemPrintln(" ATI10 - Display the total datagrams received"); + systemPrintln(" ATI11 - Display the total frames sent"); + systemPrintln(" ATI12 - Display the total frames received"); + systemPrintln(" ATI13 - Display the total bad frames received"); + systemPrintln(" ATI14 - Display the total duplicate frames received"); + systemPrintln(" ATI15 - Display the total lost TX frames"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(); @@ -189,8 +193,37 @@ bool commandAT(const char * commandString) systemPrintUniqueID(myUniqueId); systemPrintln(); break; + case ('9'): //ATI9 - Display the toal datagrams sent + systemPrintln(datagramsSent); + break; + default: + return false; + } + } + else if ((commandString[2] == 'I') && (commandString[3] == '1') && (commandLength == 5)) + { + switch (commandString[4]) + { default: return false; + case ('0'): //ATI10 - Display the total datagrams received + systemPrintln(datagramsReceived); + break; + case ('1'): //ATI11 - Display the total frames received + systemPrintln(framesReceived); + break; + case ('2'): //ATI12 - Display the total frames received + systemPrintln(framesReceived); + break; + case ('3'): //ATI13 - Display the total bad frames received + systemPrintln(badFrames); + break; + case ('4'): //ATI14 - Display the total duplicate frames received + systemPrintln(duplicateFrames); + break; + case ('5'): //ATI15 - Display the total lost TX frames + systemPrintln(lostFrames); + break; } } @@ -515,7 +548,7 @@ const COMMAND_ENTRY commands[] = {43, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, {44, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, - {45, 1, 2, 0, TYPE_U8, valInt, "protocolVersion", &settings.protocolVersion}, + {45, 2, 2, 0, TYPE_U8, valInt, "protocolVersion", &settings.protocolVersion}, {46, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, {47, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, {48, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 8c494b3b..723d1835 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -123,6 +123,9 @@ SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 unsigned long timerStart = 0; //Tracks how long our timer has been running since last hop bool partialTimer = false; //After an ACK we reset and run a partial timer to sync units const int SYNC_PROCESSING_OVERHEAD = -5; //Number of milliseconds it takes to compute clock deltas before sync'ing clocks + +uint16_t petTimeoutHalf = 0; //Half the amount of time before WDT. Helps reduce amount of time spent petting. +unsigned long lastPet = 0; //Remebers time of last WDT pet. //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Global variables - Serial @@ -140,6 +143,11 @@ uint16_t txTail = 0; uint16_t rxHead = 0; uint16_t rxTail = 0; +unsigned long lastByteReceived_ms = 0; //Track when last transmission was. Send partial buffer once time has expired. +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Global variables - Command Processing +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- char commandBuffer[100]; //Received serial gets stored into buffer until \r or \n is received uint8_t commandRXBuffer[100]; //Bytes received from remote, waiting for printing or AT parsing uint8_t commandTXBuffer[1024 * 4]; //Bytes waiting to be transmitted to the remote unit @@ -148,56 +156,53 @@ uint16_t commandTXTail = 0; uint16_t commandRXHead = 0; uint16_t commandRXTail = 0; -unsigned long lastByteReceived_ms = 0; //Track when last transmission was. Send partial buffer once time has expired. - -char platformPrefix[25]; //Used for printing platform specific device name, ie "SAMD21 1W 915MHz" uint8_t escapeCharsReceived = 0; //Used to enter command mode unsigned long lastEscapeReceived_ms = 0; //Tracks end of serial traffic const long minEscapeTime_ms = 2000; //Serial traffic must stop this amount before an escape char is recognized -uint16_t petTimeoutHalf = 0; //Half the amount of time before WDT. Helps reduce amount of time spent petting. -unsigned long lastPet = 0; //Remebers time of last WDT pet. +bool inCommandMode = false; //Normal data is prevented from entering serial output when in command mode +uint8_t commandLength = 0; +bool remoteCommandResponse; +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//Global variables - LEDs +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +int trainCylonNumber = 0b0001; +int trainCylonDirection = -1; + +unsigned long lastTrainBlink = 0; //Controls LED during training //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//Global variables - Radio +//Global variables - Radio (General) //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver uint8_t packetSize = 0; //Tracks how much data + control trailer -uint16_t packetAirTime = 0; //Recalc'd with each new packet transmission -uint16_t controlPacketAirTime = 0; //Recalc'd with each change of settings -uint8_t packetSent = 0; //Increases each time packet is sent -unsigned long packetTimestamp = 0; -uint16_t packetsLost = 0; //Used to determine if radio link is down -uint16_t packetsResent = 0; //Keep metrics of link quality -uint16_t totalPacketsLost = 0; //Keep metrics of link quality - -uint8_t lastPacket[MAX_PACKET_SIZE]; //Contains the last data received. Used for duplicate testing. -uint8_t lastPacketSize = 0; //Tracks the last packet size we received +uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission +uint16_t ackAirTime = 0; //Recalc'd with each change of settings +uint8_t frameSentCount = 0; //Increases each time a frame is sent + unsigned long lastPacketReceived = 0; //Controls link LED in broadcast mode unsigned long lastLinkBlink = 0; //Controls link LED in broadcast mode -#define MAX_LOST_PACKET_BEFORE_LINKLOST 2 -bool sentFirstPing = false; //Force a ping to link at POR - volatile bool transactionComplete = false; //Used in dio0ISR volatile bool timeToHop = false; //Used in dio1ISR bool expectingAck = false; //Used by various send packet functions float frequencyCorrection = 0; //Adjust receive freq based on the last packet received freqError -unsigned long lastTrainBlink = 0; //Controls LED during training - -Settings originalSettings; //Create a duplicate of settings during training so that we can resort as needed -uint8_t trainNetID; //New netID passed during training -uint8_t trainEncryptionKey[AES_KEY_BYTES]; //New AES key passed during training - -bool inCommandMode = false; //Normal data is prevented from entering serial output when in command mode -uint8_t commandLength = 0; -bool remoteCommandResponse; +volatile bool clearDIO1 = true; //Clear the DIO1 hop ISR when possible +//Link quality metrics +uint32_t datagramsSent; //Total number of datagrams sent +uint32_t datagramsReceived; //Total number of datagrams received +uint32_t framesSent; //Total number of frames sent +uint32_t framesReceived; //Total number of frames received +uint32_t badFrames; //Total number of bad frames received +uint32_t duplicateFrames; //Total number of duplicate frames received +uint32_t lostFrames; //Total number of lost TX frames //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//V2 +//Global variables - V2 Protocol //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Frame size values @@ -208,8 +213,6 @@ uint8_t txDatagramSize; //Point-to-Point unsigned long datagramTimer; uint8_t expectedDatagramNumber; -uint16_t ackAirTime; -uint16_t datagramAirTime; uint16_t pingRandomTime; uint16_t heartbeatRandomTime; @@ -245,22 +248,14 @@ bool trainingPreviousRxInProgress = false; //Previous RX status float originalChannel; //Original channel from HOP table while training is in progress uint8_t trainingPartnerID[UNIQUE_ID_BYTES]; //Unique ID of the training partner uint8_t myUniqueId[UNIQUE_ID_BYTES]; // Unique ID of this system - -volatile bool clearDIO1 = true; //Clear the DIO1 hop ISR when possible - //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -long startTime = 0; //Used for air time of TX frames -long stopTime = 0; - -bool confirmDeliveryBeforeRadioConfig = false; //Goes true when we have remotely configured a radio - -int trainCylonNumber = 0b0001; -int trainCylonDirection = -1; - const Settings defaultSettings; +Settings originalSettings; //Create a duplicate of settings during training so that we can resort as needed + +char platformPrefix[25]; //Used for printing platform specific device name, ie "SAMD21 1W 915MHz" //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Architecture variables diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 06ab0553..45c9f85c 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1,324 +1,3 @@ -//Determine the type of packet received and print data as necessary -//The LoRa radio handles CRC and FHSS for us so we presume the packet was meant for us -//We check the packet netID for validity -//If the packet is a short/ping packet, ack -//If the packet is marked as resend, check if it's a duplicate -//Move any data to incomingPacket buffer -PacketType identifyPacketType() -{ - //Receive the data packet - radio.readData(incomingBuffer, MAX_PACKET_SIZE); - uint8_t receivedBytes = radio.getPacketLength(); - - /* - +-------------------------+------------+--------+---------+ - | | Optional | | | - | data | SF6 length | NET ID | Control | - | 0 - n bytes | 8 bits | 8 bits | 8 bits | - +-------------------------+------------+--------+---------+ - | | - |<-------------------- receivedBytes -------------------->| - */ - - //Display the received data bytes - if (settings.printRfData || settings.debugReceive) - { - petWDT(); - systemPrintln("----------"); - systemPrint("RX: Data "); - systemPrint(receivedBytes); - systemPrint(" (0x"); - systemPrint(receivedBytes, HEX); - systemPrintln(") bytes"); - petWDT(); - if (settings.printRfData && receivedBytes) - dumpBuffer(incomingBuffer, receivedBytes); - } - - if (settings.dataScrambling == true) - radioComputeWhitening(incomingBuffer, receivedBytes); - - if (settings.encryptData == true) - decryptBuffer(incomingBuffer, receivedBytes); - - //All packets must include the 2-byte control header - if (receivedBytes < 2) - { - //Display the packet contents - if (settings.printPktData || settings.debugReceive) - { - petWDT(); - systemPrint("RX: Bad packet"); - systemPrint(receivedBytes); - systemPrint(" (0x"); - systemPrint(receivedBytes, HEX); - systemPrintln(") bytes"); - petWDT(); - if (settings.printRfData && receivedBytes) - dumpBuffer(incomingBuffer, receivedBytes); - } - return (PACKET_BAD); - } - - /* - +-------------------------+------------+--------+---------+ - | | Optional | | | - | data | SF6 length | NET ID | Control | - | n bytes | 8 bits | 8 bits | 8 bits | - +-------------------------+------------+--------+---------+ - | | - |<---------- receivedBytes ----------->| - */ - - //Pull out control header - uint8_t receivedNetID = incomingBuffer[receivedBytes - 2]; - *(uint8_t *)&receiveTrailer = incomingBuffer[receivedBytes - 1]; - receivedBytes -= 2; //Remove control bytes - - //Display the control header - if (settings.debugReceive) - { - petWDT(); - systemPrint("RX: NetID "); - systemPrint(receivedNetID); - systemPrint(" (0x"); - systemPrint(receivedNetID, HEX); - systemPrintln(")"); - - petWDT(); - systemPrint("RX: Control"); - systemPrint(" 0x"); - systemPrintln(*(uint8_t *)&receiveTrailer, HEX); - if (*(uint8_t *)&receiveTrailer) - { - if (receiveTrailer.resend) systemPrintln(" 0x01: resend"); - if (receiveTrailer.ack) systemPrintln(" 0x02: ack"); - if (receiveTrailer.remoteCommand) systemPrintln(" 0x04: remoteCommand"); - if (receiveTrailer.remoteCommandResponse) systemPrintln(" 0x08: remoteCommandResponse"); - if (receiveTrailer.train) systemPrintln(" 0x10: train"); - } - petWDT(); - } - - /* - +-------------------------+------------+ - | | Optional | - | data | SF6 length | - | n bytes | 8 bits | - +-------------------------+------------+ - | | - |<---------- receivedBytes ----------->| - */ - - //SF6 requires an implicit header which means there is no dataLength in the header - //Instead, we manually store it after the data and before NetID - if (settings.radioSpreadFactor == 6) - { - //We've either received a control packet (2 bytes) or a data packet - if (receivedBytes) - { - receivedBytes -= 1; //Remove the manual packetSize byte from consideration - receivedBytes = incomingBuffer[receivedBytes]; //Obtain actual packet data length - } - } - - /* - +-------------------------+ - | data | - | n bytes | - +-------------------------+ - | | - |<---- receivedBytes ---->| - */ - - //Display the packet contents - if (settings.printPktData || settings.debugReceive) - { - petWDT(); - systemPrint("RX: Packet data "); - systemPrint(receivedBytes); - systemPrint(" (0x"); - systemPrint(receivedBytes, HEX); - systemPrintln(") bytes"); - petWDT(); - if (settings.printPktData && receivedBytes) - dumpBuffer(incomingBuffer, receivedBytes); - } - - if ((receivedNetID != settings.netID) - && ((settings.pointToPoint == true) || (settings.verifyRxNetID == true))) - { - if (settings.debugReceive) - { - petWDT(); - systemPrint("RX: netID "); - systemPrint(receivedNetID); - systemPrint(" expecting "); - systemPrintln(settings.netID); - } - return (PACKET_NETID_MISMATCH); - } - - //---------- - //Handle ACKs and duplicate packets - //---------- - - if ((receiveTrailer.ack == 1) - && (receiveTrailer.remoteCommand == 0) - && (receiveTrailer.remoteCommandResponse == 0)) - { - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Ack packet"); - } - return (PACKET_ACK); - } - - if (receiveTrailer.resend == 1) - { - //We've received a packet that is marked as a retransmit - //Determine if this is a duplicate of a previous packet - if (receivedBytes == lastPacketSize) - { - //Check packet contents - if (memcmp(lastPacket, incomingBuffer, lastPacketSize) == 0) - { - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Duplicate received. Acking again."); - } - return (PACKET_DUPLICATE); //It's a duplicate. Ack then ignore - } - } - else - { - //We've received a resend but it's different from the lastPacket - //Go ahead and print it - } - } - - //---------- - //Handle control packets - //---------- - - //We have empty data packet, this is a control packet used for pinging/scanning - if (receivedBytes == 0) - { - //If this packet is marked as training data, someone is sending training ping - if (receiveTrailer.train == 1) - { - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Training Control Packet"); - } - return (PACKET_TRAINING_PING); - } - - //If this packet is marked as a remote command, it's either an ack or a zero length packet (not known) - if (receiveTrailer.remoteCommand == 1) - { - if (receiveTrailer.ack == 1) - { - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Command Ack"); - } - return (PACKET_COMMAND_ACK); - } - - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Unknown Command"); - } - return (PACKET_BAD); - } - - //If this packet is marked as a remote command response, it's either an ack or a zero length packet (not known) - if (receiveTrailer.remoteCommandResponse == 1) - { - if (receiveTrailer.ack == 1) - { - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Command Response Ack"); - } - return (PACKET_COMMAND_RESPONSE_ACK); - } - - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Unknown Response Command"); - } - return (PACKET_BAD); - } - - //Not training, not command packet, just a ping - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Control Packet"); - } - return (PACKET_PING); - } - - //---------- - //Handle data packets - //---------- - - //Update lastPacket details with current packet - memcpy(lastPacket, incomingBuffer, receivedBytes); - lastPacketSize = receivedBytes; - - //If this packet is marked as training data, - //payload contains new AES key and netID which will be processed externally - if (receiveTrailer.train == 1) - { - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Training Data"); - } - return (PACKET_TRAINING_DATA); - } - - if (receiveTrailer.remoteCommand == 1) - { - //New data from remote - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Command Data"); - } - return (PACKET_COMMAND_DATA); - } - - if (receiveTrailer.remoteCommandResponse == 1) - { - //New response data from remote - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Command Response Data"); - } - return (PACKET_COMMAND_RESPONSE_DATA); - } - - //Return the data to the user - if (settings.debugReceive) - { - petWDT(); - systemPrintln("RX: Return user data"); - } - return (PACKET_DATA); -} - //Apply settings to radio //Called after begin() and once user exits from command interface void configureRadio() @@ -447,9 +126,9 @@ void configureRadio() if (radio.setFHSSHoppingPeriod(hoppingPeriod) != RADIOLIB_ERR_NONE) success = false; - controlPacketAirTime = calcAirTime(2); //Used for response timeout during RADIO_LINKED_ACK_WAIT - uint16_t responseDelay = controlPacketAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond - controlPacketAirTime += responseDelay; + ackAirTime = calcAirTime(2); //Used for response timeout during ACK + uint16_t responseDelay = ackAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond + ackAirTime += responseDelay; //Precalculate the ACK packet time ackAirTime = calcAirTime(4); //We assume all ACKs have max of 4 bytes @@ -472,8 +151,6 @@ void configureRadio() systemPrintln(calcSymbolTime()); systemPrint("HoppingPeriod: "); systemPrintln(hoppingPeriod); - systemPrint("controlPacketAirTime: "); - systemPrintln(controlPacketAirTime); systemPrint("ackAirTime: "); systemPrintln(ackAirTime); } @@ -537,431 +214,6 @@ void returnToReceiving() } } -//Create short packet of 2 control bytes - query remote radio for proof of life (ack) -void sendPingPacket() -{ - /* - +--------+---------+ - | NET ID | Trailer | - | 8 bits | 8 bits | - +--------+---------+ - | | - |<-- packetSize -->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Ping "); - responseTrailer.ack = 0; //This is not an ACK to a previous transmission - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 0; //This is not a training packet - responseTrailer.remoteCommand = 0; //This is not a remote command packet - responseTrailer.remoteCommandResponse = 0; //This is not a response to a previous remote command - - packetSize = 2; - packetSent = 0; //Reset the number of times we've sent this packet - - //SF6 requires an implicit header which means there is no dataLength in the header - //Because we cannot predict when a ping packet will be received, the receiver will always - //expecting 255 bytes. Pings must be increased to 255 bytes. ACKs are still 2 bytes. - if (settings.radioSpreadFactor == 6) - { - //Manually store actual data length 3 bytes from the end (before NetID) - //Manual packet size is whatever has been processed + 1 for the manual packetSize byte - outgoingPacket[maxDatagramSize] = packetSize + 1; - packetSize = MAX_PACKET_SIZE; //We're now going to transmit 255 bytes - } - - expectingAck = true; //We expect destination to ack - sendPacket(); -} - -//Create packet of current data + control bytes - expect ACK from recipient -void sendDataPacket() -{ - /* - +--- ... ---+--------+---------+ - | Data | NET ID | Trailer | - | n bytes | 8 bits | 8 bits | - +-------------+--------+---------+ - | | - |<--------- packetSize --------->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Data "); - responseTrailer.ack = 0; //This is not an ACK to a previous transmission - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 0; //This is not a training packet - responseTrailer.remoteCommand = 0; //This is not a remote command packet - responseTrailer.remoteCommandResponse = 0; //This is not a response to a previous remote command - - packetSize += 2; //Make room for control bytes - packetSent = 0; //Reset the number of times we've sent this packet - - //SF6 requires an implicit header which means there is no dataLength in the header - if (settings.radioSpreadFactor == 6) - { - //Manually store actual data length 3 bytes from the end (before NetID) - //Manual packet size is whatever has been processed + 1 for the manual packetSize byte - outgoingPacket[maxDatagramSize] = packetSize + 1; - packetSize = MAX_PACKET_SIZE; //We're now going to transmit 255 bytes - } - - expectingAck = true; //We expect destination to ack - sendPacket(); -} - -//Create packet of current data + control bytes - expect ACK from recipient -void sendResendPacket() -{ - /* - +--- ... ---+--------+---------+ - | Data | NET ID | Trailer | - | n bytes | 8 bits | 8 bits | - +-------------+--------+---------+ - | | - |<--------- packetSize --------->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Resend "); - responseTrailer.ack = 0; //This is not an ACK to a previous transmission - responseTrailer.resend = 1; //This is a resend - responseTrailer.train = 0; //This is not a training packet - //responseTrailer.remoteCommand = ; //Don't modify the remoteCommand bit; we may be resending a command packet - //responseTrailer.remoteCommandResponse = ; //Don't modify the remoteCommand bit; we may be resending a command packet - - //packetSize += 2; //Don't adjust the packet size - //packetSent = 0; //Don't reset - expectingAck = true; //We expect destination to ack - sendPacket(); -} - -//Create short packet of 2 control bytes - do not expect ack -void sendAckPacket() -{ - /* - +--------+---------+ - | NET ID | Trailer | - | 8 bits | 8 bits | - +--------+---------+ - | | - |<-- packetSize -->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Ack "); - responseTrailer.ack = 1; //This is an ACK to a previous reception - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 0; //This is not a training packet - responseTrailer.remoteCommand = 0; //This is not a remote command packet - responseTrailer.remoteCommandResponse = 0; //This is not a response to a previous remote command - - packetSize = 2; - packetSent = 0; //Reset the number of times we've sent this packet - expectingAck = false; //We do not expect destination to ack - sendPacket(); -} - -//Create short packet of 2 control bytes with train = 1 -void sendTrainingPingPacket() -{ - /* - +--------+---------+ - | NET ID | Trailer | - | 8 bits | 8 bits | - +--------+---------+ - | | - |<-- packetSize -->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Training Ping "); - responseTrailer.ack = 0; //This is not an ACK to a previous transmission - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 1; //This is a training packet - responseTrailer.remoteCommand = 0; //This is not a remote command packet - responseTrailer.remoteCommandResponse = 0; //This is not a response to a previous remote command - - packetSize = 2; - packetSent = 0; //Reset the number of times we've sent this packet - - //SF6 is not used during training - - expectingAck = true; //We expect destination to ack - sendPacket(); -} - -//Create packet of AES + netID with training = 1 -void sendTrainingDataPacket() -{ - /* - +----------------+------------+--------+---------+ - | Encryption Key | New NET ID | NET ID | Trailer | - | 16 bytes | 8 bits | 8 bits | 8 bits | - +----------------+------------+--------+---------+ - | | - |<----------------- packetSize ----------------->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Training Data "); - responseTrailer.ack = 0; //This is not an ACK to a previous transmission - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 1; //This is training packet - responseTrailer.remoteCommand = 0; //This is not a remote command packet - responseTrailer.remoteCommandResponse = 0; //This is not a response to a previous remote command - - packetSize = sizeof(trainEncryptionKey) + sizeof(trainNetID); - - for (uint8_t x = 0 ; x < sizeof(trainEncryptionKey) ; x++) - outgoingPacket[x] = trainEncryptionKey[x]; - outgoingPacket[packetSize - 1] = trainNetID; - - packetSize += 2; - packetSent = 0; //Reset the number of times we've sent this packet - - //During training we do not use spread factor 6 - - expectingAck = false; //We do not expect destination to ack - sendPacket(); -} - -//Create short packet of 2 control bytes - do not expect ack -void sendCommandAckPacket() -{ - /* - +--------+---------+ - | NET ID | Trailer | - | 8 bits | 8 bits | - +--------+---------+ - | | - |<-- packetSize -->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Command Ack "); - responseTrailer.ack = 1; //This is an ACK to a previous reception - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 0; //This is not a training packet - responseTrailer.remoteCommand = 1; //This is a remote command packet - responseTrailer.remoteCommandResponse = 0; //This is not a response to a previous command - - packetSize = 2; - packetSent = 0; //Reset the number of times we've sent this packet - expectingAck = false; //We do not expect destination to ack - sendPacket(); -} - -//Create packet of serial command with remote command = 1, ack = 0 -void sendCommandDataPacket() -{ - /* - +---------+--------+---------+ - | Command | NET ID | Trailer | - | n bytes | 8 bits | 8 bits | - +---------+--------+---------+ - | | - |<------- packetSize ------->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Command Data "); - responseTrailer.ack = 0; //This is not an ACK to a previous transmission. - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 0; //This is not training packet - responseTrailer.remoteCommand = 1; //This is a remote control packet - responseTrailer.remoteCommandResponse = 0; //This is not a response to a previous command - - packetSize += 2; //Make room for control bytes - packetSent = 0; //Reset the number of times we've sent this packet - - //SF6 requires an implicit header which means there is no dataLength in the header - if (settings.radioSpreadFactor == 6) - { - //Manually store actual data length 3 bytes from the end (before NetID) - //Manual packet size is whatever has been processed + 1 for the manual packetSize byte - outgoingPacket[maxDatagramSize] = packetSize + 1; - packetSize = MAX_PACKET_SIZE; //We're now going to transmit 255 bytes - } - - expectingAck = true; //We expect destination to ack - sendPacket(); -} - -//Create short packet of 2 control bytes - do not expect ack -void sendCommandResponseAckPacket() -{ - /* - +--------+---------+ - | NET ID | Trailer | - | 8 bits | 8 bits | - +--------+---------+ - | | - |<-- packetSize -->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Command Response Ack "); - responseTrailer.ack = 1; //This is an ACK to a previous reception - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 0; //This is not a training packet - responseTrailer.remoteCommand = 0; //This is not a remote command packet - responseTrailer.remoteCommandResponse = 1; //This is a response to a previous command - - packetSize = 2; - packetSent = 0; //Reset the number of times we've sent this packet - expectingAck = false; //We do not expect destination to ack - sendPacket(); -} - -//Create packet of serial command with remote command = 1, ack = 0 -void sendCommandResponseDataPacket() -{ - /* - +----------+--------+---------+ - | Response | NET ID | Trailer | - | n bytes | 8 bits | 8 bits | - +----------+--------+---------+ - | | - |<-------- packetSize ------->| - */ - - if (settings.debugDatagrams) - systemPrintln("TX: Command Response Data "); - responseTrailer.ack = 0; //This is not an ACK to a previous transmission. - responseTrailer.resend = 0; //This is not a resend - responseTrailer.train = 0; //This is not training packet - responseTrailer.remoteCommand = 0; //This is a remote control packet - responseTrailer.remoteCommandResponse = 1; //This is a response to a previous command - - packetSize += 2; //Make room for control bytes - packetSent = 0; //Reset the number of times we've sent this packet - - //SF6 requires an implicit header which means there is no dataLength in the header - if (settings.radioSpreadFactor == 6) - { - //Manually store actual data length 3 bytes from the end (before NetID) - //Manual packet size is whatever has been processed + 1 for the manual packetSize byte - outgoingPacket[maxDatagramSize] = packetSize + 1; - packetSize = MAX_PACKET_SIZE; //We're now going to transmit 255 bytes - } - - expectingAck = true; //We expect destination to ack - sendPacket(); -} - -//Push the outgoing packet to the air -void sendPacket() -{ - /* - +--- ... ---+------------+--------+---------+ - | | Optional | | | - | Data | SF6 Length | NET ID | Trailer | - | n bytes | 8 bits | 8 bits | 8 bits | - +-------------+------------+--------+---------+ - | | - |<--------------- packetSize ---------------->| - */ - - //Attach netID and control byte to end of packet - outgoingPacket[packetSize - 2] = settings.netID; - outgoingPacket[packetSize - 1] = *(uint8_t *)&responseTrailer; - - //Display the packet contents - if (settings.printPktData || settings.debugTransmit) - { - petWDT(); - systemPrint("TX: Control"); - systemPrint(" 0x"); - systemPrintln(*(uint8_t *)&receiveTrailer, HEX); - if (*(uint8_t *)&receiveTrailer) - { - if (receiveTrailer.resend) systemPrintln(" 0x01: resend"); - if (receiveTrailer.ack) systemPrintln(" 0x02: ack"); - if (receiveTrailer.remoteCommand) systemPrintln(" 0x04: remoteCommand"); - if (receiveTrailer.remoteCommandResponse) systemPrintln(" 0x08: remoteCommandResponse"); - if (receiveTrailer.train) systemPrintln(" 0x10: train"); - } - petWDT(); - systemPrint("TX: Packet data "); - systemPrint(packetSize); - systemPrint(" (0x"); - systemPrint(packetSize, HEX); - systemPrintln(") bytes"); - petWDT(); - if (settings.printPktData) - dumpBuffer(outgoingPacket, packetSize); - } - - //Apply AES and whitening only to new packets, not resends - if (responseTrailer.resend == 0) - { - if (settings.encryptData == true) - encryptBuffer(outgoingPacket, packetSize); - - if (settings.dataScrambling == true) - radioComputeWhitening(outgoingPacket, packetSize); - } - - //Display the transmitted packet bytes - if (settings.printPktData || settings.debugTransmit) - { - petWDT(); - systemPrint("TX: Data "); - systemPrint(packetSize); - systemPrint(" (0x"); - systemPrint(packetSize, HEX); - systemPrintln(") bytes"); - petWDT(); - if (settings.printPktData) - dumpBuffer(outgoingPacket, packetSize); - } - - //If we are trainsmitting at high data rates the receiver is often not ready for new data. Pause for a few ms (measured with logic analyzer). - if (settings.airSpeed == 28800 || settings.airSpeed == 38400) - delay(2); - - setRadioFrequency(false); //Return home before every transmission - - //Display the transmit frequency - if (settings.debugTransmit) - { - systemPrint("TX: Transmitting @ "); - systemPrint(channels[channelNumber], 3); - systemPrintln(" MHz"); - } - - int state = radio.startTransmit(outgoingPacket, packetSize); - if (state == RADIOLIB_ERR_NONE) - { - if (timeToHop) hopChannel(); - - packetAirTime = calcAirTime(packetSize); //Calculate packet air size while we're transmitting in the background - uint16_t responseDelay = packetAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond - packetAirTime += responseDelay; - - packetSent++; - - if (settings.debugTransmit) - { - systemPrint("TX: PacketAirTime "); - systemPrintln(packetAirTime); - systemPrint("TX: responseDelay: "); - systemPrintln(responseDelay); - } - - if (timeToHop) hopChannel(); - } - else - { - if (settings.debugTransmit || settings.printTxErrors) - systemPrintln("Error: TX"); - } - - packetTimestamp = millis(); //Move timestamp even if error -} - //ISR when DIO0 goes low //Called when transmission is complete or when RX is received void dio0ISR(void) @@ -1114,16 +366,6 @@ uint16_t myRand() return myRandSeed = (myRandSeed >> 1) | (myRandBit << 15); } -//If we have lost too many packets -//or have not had a successful heart beat within a determined amount of time -//the link is considered lost -bool linkLost() -{ - if (packetsLost > MAX_LOST_PACKET_BEFORE_LINKLOST) - return (true); - return (false); -} - //Move to the next channel //This is called when the FHSS interrupt is received //at the beginning and during of a transmission or reception @@ -1139,8 +381,7 @@ void hopChannel() float frequency; if (settings.autoTuneFrequency == true) { - if (radioState == RADIO_LINKED_RECEIVING_STANDBY || radioState == RADIO_LINKED_ACK_WAIT - || radioState == RADIO_BROADCASTING_RECEIVING_STANDBY) //Only adjust frequency on RX. Not TX. + if (radioStateTable[radioState].rxState) frequency = channels[channelNumber] - frequencyCorrection; else frequency = channels[channelNumber]; diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index da4cecc8..0faaaf66 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -537,6 +537,7 @@ PacketType rcvDatagram() rcvTimeMillis = millis(); //Get the received datagram + framesReceived++; radio.readData(incomingBuffer, MAX_PACKET_SIZE); rxDataBytes = radio.getPacketLength(); rxData = incomingBuffer; @@ -608,7 +609,8 @@ PacketType rcvDatagram() if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); } - return (PACKET_BAD); + badFrames++; + return (DATAGRAM_BAD); } /* @@ -643,7 +645,7 @@ PacketType rcvDatagram() petWDT(); if (settings.printPktData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); - return (PACKET_NETID_MISMATCH); + return (DATAGRAM_NETID_MISMATCH); } systemPrintln(); } @@ -676,7 +678,8 @@ PacketType rcvDatagram() if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); } - return (PACKET_BAD); + badFrames++; + return (DATAGRAM_BAD); } } @@ -699,8 +702,11 @@ PacketType rcvDatagram() uint8_t packetNumber = rxControl.ackNumber; if (settings.debugReceive) printControl(*((uint8_t *)&rxControl)); - if (datagramType >= MAX_DATAGRAM_TYPE) - return (PACKET_BAD); + if (datagramType >= MAX_V2_DATAGRAM_TYPE) + { + badFrames++; + return (DATAGRAM_BAD); + } //Display the CRC if (settings.enableCRC16 && (settings.printPktData || settings.debugReceive)) @@ -740,7 +746,8 @@ PacketType rcvDatagram() systemPrint((int)rxDataBytes - minDatagramSize); systemPrintln(" received bytes"); } - return (PACKET_BAD); + badFrames++; + return (DATAGRAM_BAD); } } rxDataBytes -= minDatagramSize; @@ -764,7 +771,8 @@ PacketType rcvDatagram() systemPrint(" expecting "); systemPrintln(expectedAckNumber); } - return (PACKET_BAD); + badFrames++; + return (DATAGRAM_BAD); } //Increment the expected ACK number @@ -782,7 +790,8 @@ PacketType rcvDatagram() if (packetNumber == ((expectedDatagramNumber - 1) & 3)) { linkDownTimer = millis(); - return PACKET_DUPLICATE; + duplicateFrames++; + return DATAGRAM_DUPLICATE; } //Not a duplicate @@ -794,7 +803,8 @@ PacketType rcvDatagram() systemPrint(" expecting "); systemPrintln(expectedDatagramNumber); } - return PACKET_BAD; + badFrames++; + return DATAGRAM_BAD; } //Receive this data packet and set the next expected datagram number @@ -860,6 +870,7 @@ PacketType rcvDatagram() } //Process the packet + datagramsReceived++; linkDownTimer = millis(); return datagramType; } @@ -876,6 +887,7 @@ void transmitDatagram() uint8_t length; //Determine the packet size + datagramsSent++; txDatagramSize = endOfTxData - outgoingPacket; length = txDatagramSize - headerBytes; @@ -1092,11 +1104,11 @@ void transmitDatagram() //Reset the buffer data pointer for the next transmit operation endOfTxData = &outgoingPacket[headerBytes]; - //Compute the time needed for this packet. Part of ACK timeout. - datagramAirTime = calcAirTime(txDatagramSize); + //Compute the time needed for this frame. Part of ACK timeout. + frameAirTime = calcAirTime(txDatagramSize); //Transmit this datagram - packetSent = 0; //This is the first time this packet is being sent + frameSentCount = 0; //This is the first time this frame is being sent retransmitDatagram(); } @@ -1113,7 +1125,7 @@ void printControl(uint8_t value) systemPrintln(value & 3); systemPrintTimestamp(); systemPrint(" datagramType "); - if (control->datagramType < MAX_DATAGRAM_TYPE) + if (control->datagramType < MAX_V2_DATAGRAM_TYPE) systemPrintln(v2DatagramType[control->datagramType]); else { @@ -1136,8 +1148,8 @@ void retransmitDatagram() |<-------------------- txDatagramSize --------------------->| */ - //Display the transmitted packet bytes - if (packetSent && (settings.printRfData || settings.debugTransmit)) + //Display the transmitted frame bytes + if (frameSentCount && (settings.printRfData || settings.debugTransmit)) { systemPrintTimestamp(); systemPrint("TX: Retransmit "); @@ -1152,17 +1164,20 @@ void retransmitDatagram() dumpBuffer(outgoingPacket, txDatagramSize); } + //Transmit this frame int state = radio.startTransmit(outgoingPacket, txDatagramSize); if (state == RADIOLIB_ERR_NONE) { + frameSentCount++; + framesSent++; xmitTimeMillis = millis(); - packetAirTime = calcAirTime(txDatagramSize); //Calculate packet air size while we're transmitting in the background - uint16_t responseDelay = packetAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond + frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background + uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond if (settings.debugTransmit) { systemPrintTimestamp(); - systemPrint("TX: PacketAirTime "); - systemPrint(packetAirTime); + systemPrint("TX: frameAirTime "); + systemPrint(frameAirTime); systemPrintln(" mSec"); systemPrintTimestamp(); @@ -1170,7 +1185,7 @@ void retransmitDatagram() systemPrint(responseDelay); systemPrintln(" mSec"); } - packetAirTime += responseDelay; + frameAirTime += responseDelay; } else if (settings.debugTransmit) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 24ae9c82..a401fd65 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -10,7 +10,7 @@ void updateRadioState() static uint8_t rexmtBuffer[MAX_PACKET_SIZE]; static CONTROL_U8 rexmtControl; static uint8_t rexmtLength; - static uint8_t rexmtPacketSent; + static uint8_t rexmtFrameSentCount; switch (radioState) { @@ -18,6 +18,8 @@ void updateRadioState() { systemPrint("Unknown state: "); systemPrintln(radioState); + while (1) + petWDT(); } break; @@ -65,22 +67,6 @@ void updateRadioState() else changeState(RADIO_MP_STANDBY); } - else - { - //V1 - SF6 length, netID and control are at the end of the datagram - trailerBytes = headerBytes; - headerBytes = 0; - - //Determine the minimum and maximum datagram sizes - minDatagramSize = headerBytes + trailerBytes; - maxDatagramSize = sizeof(outgoingPacket) - minDatagramSize; - - //Start the V1 protocol - if (settings.pointToPoint == true) - changeState(RADIO_NO_LINK_RECEIVING_STANDBY); - else - changeState(RADIO_BROADCASTING_RECEIVING_STANDBY); - } break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -180,6 +166,7 @@ void updateRadioState() { triggerEvent(TRIGGER_TRAINING_NO_ACK); retransmitDatagram(); + lostFrames++; changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); } } @@ -356,7 +343,7 @@ void updateRadioState() } else { - if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + settings.overheadTime)) + if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime)) { if (settings.debugDatagrams) { @@ -412,7 +399,7 @@ void updateRadioState() } else { - if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + settings.overheadTime)) + if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime)) { if (settings.debugDatagrams) { @@ -525,7 +512,7 @@ void updateRadioState() returnToReceiving(); break; - case PACKET_BAD: + case DATAGRAM_BAD: triggerEvent(TRIGGER_BAD_PACKET); returnToReceiving(); break; @@ -537,7 +524,7 @@ void updateRadioState() changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); break; - case PACKET_DUPLICATE: + case DATAGRAM_DUPLICATE: //Display the signal strength if (settings.displayPacketQuality == true) { @@ -551,7 +538,6 @@ void updateRadioState() systemPrintln(); } - packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -587,7 +573,6 @@ void updateRadioState() systemPrintln(); } - packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -627,7 +612,6 @@ void updateRadioState() txHead += rxDataBytes - length; txHead %= sizeof(serialTransmitBuffer); - packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -654,7 +638,6 @@ void updateRadioState() commandRXHead += rxDataBytes - length; commandRXHead %= sizeof(commandRXBuffer); - packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); @@ -667,7 +650,6 @@ void updateRadioState() for (int x = 0 ; x < rxDataBytes ; x++) Serial.write(rxData[x]); - packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -772,7 +754,7 @@ void updateRadioState() returnToReceiving(); break; - case PACKET_BAD: + case DATAGRAM_BAD: triggerEvent(TRIGGER_BAD_PACKET); returnToReceiving(); break; @@ -800,7 +782,6 @@ void updateRadioState() systemPrintln(); } - packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -832,7 +813,6 @@ void updateRadioState() systemPrintln(); } - packetsLost = 0; //Reset, used for linkLost testing updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -844,7 +824,7 @@ void updateRadioState() memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); rexmtControl = txControl; rexmtLength = txDatagramSize; - rexmtPacketSent = packetSent; + rexmtFrameSentCount = frameSentCount; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); xmitDatagramP2PAck(); //Transmit ACK @@ -855,7 +835,7 @@ void updateRadioState() } //Check for ACK timeout - else if ((millis() - datagramTimer) >= (datagramAirTime + ackAirTime + settings.overheadTime)) + else if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime)) //Set at end of transmit, measures ACK timeout { if (settings.debugDatagrams) @@ -865,14 +845,14 @@ void updateRadioState() } //Retransmit the packet - if (packetSent < settings.maxResends) + if (frameSentCount < settings.maxResends) { triggerEvent(TRIGGER_LINK_RETRANSMIT); if (settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("TX: Retransmit "); - systemPrint(packetSent); + systemPrint(frameSentCount); systemPrint(", "); systemPrint(v2DatagramType[txControl.datagramType]); switch (txControl.datagramType) @@ -897,7 +877,7 @@ void updateRadioState() if (receiveInProcess() == false) { retransmitDatagram(); - packetSent++; + lostFrames++; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } } @@ -924,17 +904,17 @@ void updateRadioState() transactionComplete = false; //Reset ISR flag //Retransmit the packet - if (packetSent++ < settings.maxResends) + if (rexmtFrameSentCount < settings.maxResends) { memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); txControl = rexmtControl; txDatagramSize = rexmtLength; - packetSent = rexmtPacketSent; + frameSentCount = rexmtFrameSentCount; if (settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("TX: Retransmit "); - systemPrint(packetSent); + systemPrint(frameSentCount); systemPrint(", "); systemPrint(v2DatagramType[txControl.datagramType]); switch (txControl.datagramType) @@ -957,6 +937,7 @@ void updateRadioState() } } retransmitDatagram(); + lostFrames++; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } else @@ -1328,651 +1309,6 @@ void updateRadioState() changeState(RADIO_MP_WAIT_FOR_TRAINING_PING); } break; - - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V1 - No Link - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - case RADIO_NO_LINK_RECEIVING_STANDBY: - { - if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived - { - transactionComplete = false; //Reset ISR flag - changeState(RADIO_NO_LINK_RECEIVED_PACKET); - } - - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - - //Check to see if we need to send a ping - else if ( (millis() - packetTimestamp) > (unsigned int)(settings.heartbeatTimeout + random(0, 1000)) //Avoid pinging each other at same time - || sentFirstPing == false) //Immediately send pings at POR - { - if (receiveInProcess() == false) - { - triggerEvent(TRIGGER_NOLINK_SEND_PING); - sentFirstPing = true; - sendPingPacket(); - transactionComplete = false; //Reset ISR flag - changeState(RADIO_NO_LINK_TRANSMITTING); - } - else if (settings.debugRadio) - systemPrintln("NO_LINK_RECEIVING_STANDBY: RX In Progress"); - } - } - break; - - case RADIO_NO_LINK_TRANSMITTING: - { - if (transactionComplete == true) //If dio0ISR has fired, we are done transmitting - { - transactionComplete = false; //Reset ISR flag - - if (expectingAck == false) - { - triggerEvent(TRIGGER_NOLINK_SEND_ACK_PACKET); - //We're done transmitting our ack packet - //Yay! Return to normal communication - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level. We will soon be linked. - returnToReceiving(); - changeState(RADIO_LINKED_RECEIVING_STANDBY); - } - else - { - returnToReceiving(); - changeState(RADIO_NO_LINK_ACK_WAIT); - } - } - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - } - break; - - case RADIO_NO_LINK_ACK_WAIT: - { - if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived - { - transactionComplete = false; //Reset ISR flag - changeState(RADIO_NO_LINK_RECEIVED_PACKET); - } - - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - - else if ((millis() - packetTimestamp) > (packetAirTime + controlPacketAirTime)) //Wait for xmit of packet and ACK response - { - //Give up. No ACK recevied. - randomSeed(radio.randomByte()); //Reseed the random delay between heartbeats - triggerEvent(TRIGGER_NOLINK_NO_ACK_GIVEUP); - returnToReceiving(); - changeState(RADIO_NO_LINK_RECEIVING_STANDBY); - } - } - break; - - case RADIO_NO_LINK_RECEIVED_PACKET: - { - PacketType packetType = identifyPacketType(); //Look at the packet we just received - - if (packetType == PACKET_BAD || packetType == PACKET_NETID_MISMATCH) - { - returnToReceiving(); //Ignore - changeState(RADIO_NO_LINK_RECEIVING_STANDBY); - } - else if (packetType == PACKET_ACK) - { - //Yay! Return to normal communication - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level - returnToReceiving(); - changeState(RADIO_LINKED_RECEIVING_STANDBY); - } - else if (packetType == PACKET_DUPLICATE) - { - sendAckPacket(); //It's a duplicate. Ack then ignore. - changeState(RADIO_NO_LINK_TRANSMITTING); - } - else if (packetType == PACKET_PING) - { - triggerEvent(TRIGGER_NOLINK_IDENT_PACKET); - updateRSSI(); //Adjust LEDs to RSSI level. We will soon be linked. - sendAckPacket(); //Someone is pinging us - changeState(RADIO_NO_LINK_TRANSMITTING); - } - else if (packetType == PACKET_DATA) - { - ; //No data packets allowed when not linked - } - } - break; - - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V1 - Link - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - case RADIO_LINKED_RECEIVING_STANDBY: - { - if (linkLost()) - { - setRSSI(0); - - //Return to home channel and begin linking process - returnToReceiving(); - changeState(RADIO_NO_LINK_RECEIVING_STANDBY); - } - - else if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived - { - triggerEvent(TRIGGER_LINK_PACKET_RECEIVED); - transactionComplete = false; //Reset ISR flag - changeState(RADIO_LINKED_RECEIVED_PACKET); - } - - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - - else if ((millis() - packetTimestamp) > (unsigned int)(settings.heartbeatTimeout + random(0, 1000))) //Avoid pinging each other at same time - { - if (receiveInProcess() == false) - { - randomSeed(radio.randomByte()); //Takes 10ms. Reseed the random delay between heartbeats - triggerEvent(TRIGGER_LINK_SEND_PING); - sendPingPacket(); - changeState(RADIO_LINKED_TRANSMITTING); - } - else if (settings.debugRadio) - systemPrintln("RECEIVING_STANDBY: RX In Progress"); - } - - else //Process any waiting serial or commands - { - //If the radio is available, send any data in the serial buffer over the radio - if (receiveInProcess() == false) - { - if (availableRXBytes()) //If we have bytes - { - if (processWaitingSerial(false) == true) //If we've hit a frame size or frame-timed-out - { - triggerEvent(TRIGGER_LINK_DATA_PACKET); - sendDataPacket(); - changeState(RADIO_LINKED_TRANSMITTING); - } - } - else if (availableTXCommandBytes()) //If we have command bytes to send out - { - //Load command bytes into outgoing packet - readyOutgoingCommandPacket(); - - triggerEvent(TRIGGER_LINK_DATA_PACKET); - - //Serial.print("Sending Command/Response: "); - //for (int x = 0 ; x < packetSize ; x++) - // Serial.write(outgoingPacket[x]); - //Serial.println(); - - //We now have the commandTXBuffer loaded. But we need to send an remoteCommandResponse if we are pointed at PRINT_TO_RF. - if (remoteCommandResponse) - sendCommandResponseDataPacket(); - else - sendCommandDataPacket(); - - if (availableTXCommandBytes() == 0) - printerEndpoint = PRINT_TO_SERIAL; //Once the response is received, we need to print it to serial - - changeState(RADIO_LINKED_TRANSMITTING); - } - } - } //End processWaitingSerial - } - break; - - case RADIO_LINKED_TRANSMITTING: - { - if (transactionComplete == true) //If dio0ISR has fired, we are done transmitting - { - transactionComplete = false; //Reset ISR flag - - if (expectingAck == true) - { - returnToReceiving(); - changeState(RADIO_LINKED_ACK_WAIT); - } - else - { - triggerEvent(TRIGGER_LINK_SENT_ACK_PACKET); - returnToReceiving(); - changeState(RADIO_LINKED_RECEIVING_STANDBY); - } - } - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - } - break; - - case RADIO_LINKED_ACK_WAIT: - { - if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived - { - transactionComplete = false; //Reset ISR flag - changeState(RADIO_LINKED_RECEIVED_PACKET); - } - - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - - //Check to see if we need to retransmit - if ((millis() - packetTimestamp) > (packetAirTime + controlPacketAirTime)) //Wait for xmit of packet and ACK response - { - if (packetSent > settings.maxResends) - { - if (settings.debugRadio) - systemPrintln("Packet Lost"); - packetsLost++; - totalPacketsLost++; - returnToReceiving(); - changeState(RADIO_LINKED_RECEIVING_STANDBY); - } - else - { - if (receiveInProcess() == false) - { - triggerEvent(TRIGGER_LINK_PACKET_RESEND); - packetsResent++; - sendResendPacket(); - changeState(RADIO_LINKED_TRANSMITTING); - } - else - { - if (settings.debugRadio) - systemPrintln("ACK_WAIT: RX In Progress"); - triggerEvent(TRIGGER_RX_IN_PROGRESS); - } - } - } - } - break; - - case RADIO_LINKED_RECEIVED_PACKET: - { - PacketType packetType = identifyPacketType(); //Look at the packet we just received - - if (packetType == PACKET_ACK || packetType == PACKET_COMMAND_ACK || packetType == PACKET_COMMAND_RESPONSE_ACK) - { - //This packet is an ack. Return to receiving. - triggerEvent(TRIGGER_ACK_PROCESSED); //Trigger for transmission timing - - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - returnToReceiving(); - changeState(RADIO_LINKED_RECEIVING_STANDBY); - } - else if (packetType == PACKET_DUPLICATE) - { - //It's a duplicate. Ack then throw data away. - triggerEvent(TRIGGER_LINK_DUPLICATE_PACKET); - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - sendAckPacket(); - changeState(RADIO_LINKED_TRANSMITTING); - } - else if (packetType == PACKET_PING) - { - //Someone is pinging us. Ack back. - triggerEvent(TRIGGER_LINK_CONTROL_PACKET); - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - sendAckPacket(); - changeState(RADIO_LINKED_TRANSMITTING); - } - else if (packetType == PACKET_DATA) - { - //Pull data from packet and move into outbound serial buffer - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } - - //Move this packet into the tx buffer - //We cannot directly print here because Serial.print is blocking - for (int x = 0 ; x < lastPacketSize ; x++) - { - serialTransmitBuffer[txHead++] = lastPacket[x]; - txHead %= sizeof(serialTransmitBuffer); - } - - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - sendAckPacket(); //Transmit ACK - changeState(RADIO_LINKED_TRANSMITTING); - } - - else if (packetType == PACKET_COMMAND_DATA) - { - //Serial.print("Received Command Data: "); - //for (int x = 0 ; x < lastPacketSize ; x++) - // Serial.write(lastPacket[x]); - //Serial.println(); - - //Move this packet into the command RX buffer - for (int x = 0 ; x < lastPacketSize ; x++) - { - commandRXBuffer[commandRXHead++] = lastPacket[x]; - commandRXHead %= sizeof(commandRXBuffer); - } - - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - sendCommandAckPacket(); //Transmit ACK - changeState(RADIO_LINKED_TRANSMITTING); - } - - else if (packetType == PACKET_COMMAND_RESPONSE_DATA) - { - //Print received data. This is blocking but we do not use the serialTransmitBuffer because we're in command mode (and it's not much data to print). - for (int x = 0 ; x < lastPacketSize ; x++) - Serial.write(lastPacket[x]); - - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - sendCommandResponseAckPacket(); //Transmit ACK - changeState(RADIO_LINKED_TRANSMITTING); - } - else if (packetType == PACKET_COMMAND_RESPONSE_ACK) - { - //If we are waiting for ack before radio config, apply settings - if (confirmDeliveryBeforeRadioConfig == true) - { - confirmDeliveryBeforeRadioConfig = false; - - //Apply settings - generateHopTable(); //Generate freq with new settings - configureRadio(); //Apply any new settings - - setRSSI(0); //Turn off RSSI LEDs - changeState(RADIO_RESET); - } - else //It was just an ACK - { - packetsLost = 0; //Reset, used for linkLost testing - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - returnToReceiving(); - changeState(RADIO_LINKED_RECEIVING_STANDBY); - } - } - else //packetType == PACKET_BAD, packetType == PACKET_NETID_MISMATCH - { - //Packet type not supported in this state - triggerEvent(TRIGGER_LINK_BAD_PACKET); - returnToReceiving(); - changeState(RADIO_LINKED_RECEIVING_STANDBY); - } - } - break; - - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - case RADIO_BROADCASTING_RECEIVING_STANDBY: - { - if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived - { - triggerEvent(TRIGGER_BROADCAST_PACKET_RECEIVED); - transactionComplete = false; //Reset ISR flag - changeState(RADIO_BROADCASTING_RECEIVED_PACKET); - } - - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - - else //Process waiting serial - { - //If the radio is available, send any data in the serial buffer over the radio - if (receiveInProcess() == false) - { - if (availableRXBytes()) //If we have bytes - { - if (processWaitingSerial(false) == true) //If we've hit a frame size or frame-timed-out - { - triggerEvent(TRIGGER_BROADCAST_DATA_PACKET); - sendDataPacket(); - changeState(RADIO_BROADCASTING_TRANSMITTING); - } - } - } - } //End processWaitingSerial - - //Toggle 2 LEDs if we have recently transmitted - if (millis() - packetTimestamp < 5000) - { - if (millis() - lastLinkBlink > 250) //Blink at 4Hz - { - lastLinkBlink = millis(); - if (digitalRead(pin_rssi2LED) == HIGH) - setRSSI(0b0001); - else - setRSSI(0b0010); - } - } - else if (millis() - lastPacketReceived < 5000) - { - updateRSSI(); //Adjust LEDs to RSSI level - } - - //Turn off RSSI after 5 seconds of no activity - else - setRSSI(0); - - } - break; - - case RADIO_BROADCASTING_TRANSMITTING: - { - if (transactionComplete == true) //If dio0ISR has fired, we are done transmitting - { - transactionComplete = false; //Reset ISR flag - returnToReceiving(); - changeState(RADIO_BROADCASTING_RECEIVING_STANDBY); //No ack response when in broadcasting mode - setRSSI(0b0001); - } - - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); - } - break; - case RADIO_BROADCASTING_RECEIVED_PACKET: - { - PacketType packetType = identifyPacketType(); //Look at the packet we just received - - if (packetType == PACKET_ACK) - { - //We should not be receiving ack packets, but if we do, just ignore - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - returnToReceiving(); - changeState(RADIO_BROADCASTING_RECEIVING_STANDBY); - } - else if (packetType == PACKET_DUPLICATE || packetType == PACKET_PING) - { - //We should not be receiving control packets, but if we do, just ignore - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - returnToReceiving(); //No response when in broadcasting mode - changeState(RADIO_BROADCASTING_RECEIVING_STANDBY); - } - else if (packetType == PACKET_DATA) - { - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } - - //Move this packet into the tx buffer - //We cannot directly print here because Serial.print is blocking - for (int x = 0 ; x < lastPacketSize ; x++) - { - serialTransmitBuffer[txHead++] = lastPacket[x]; - txHead %= sizeof(serialTransmitBuffer); - } - - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - returnToReceiving(); //No response when in broadcasting mode - changeState(RADIO_BROADCASTING_RECEIVING_STANDBY); - - lastPacketReceived = millis(); //Update timestamp for Link LED - } - else //PACKET_BAD, PACKET_NETID_MISMATCH - { - //This packet type is not supported in this state - returnToReceiving(); - changeState(RADIO_BROADCASTING_RECEIVING_STANDBY); - } - - } - break; - - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V1 - Point-to-Point Training - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - /* - beginTraining - | - | Save settings - | - V - RADIO_TRAINING_TRANSMITTING - | - V - RADIO_TRAINING_ACK_WAIT --------------. - | | - V | - RADIO_TRAINING_RECEIVING_HERE_FIRST | - | | - +<----------------------------’ - | - V - RADIO_TRAINING_RECEIVED_PACKET - | - V - endTraining - */ - - case RADIO_TRAINING_TRANSMITTING: - { - if (transactionComplete == true) //If dio0ISR has fired, we are done transmitting - { - transactionComplete = false; //Reset ISR flag - returnToReceiving(); - changeState(RADIO_TRAINING_ACK_WAIT); - } - } - break; - case RADIO_TRAINING_ACK_WAIT: - { - //If we receive an ACK, absorb training data - if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived - { - transactionComplete = false; //Reset ISR flag - changeState(RADIO_TRAINING_RECEIVED_PACKET); - } - - //If timeout, create new link data and return to receive, and wait for training ping from remote - if ((millis() - packetTimestamp) > (packetAirTime + controlPacketAirTime)) //Wait for xmit of packet and ACK response - { - triggerEvent(TRIGGER_TRAINING_NO_ACK); - generateTrainingSettings(); - returnToReceiving(); - changeState(RADIO_TRAINING_RECEIVING_HERE_FIRST); - } - } - break; - case RADIO_TRAINING_RECEIVING_HERE_FIRST: - { - //Wait for ping. Once received, transmit training data - if (transactionComplete == true) //If dio0ISR has fired, a packet has arrived - { - transactionComplete = false; //Reset ISR flag - changeState(RADIO_TRAINING_RECEIVED_PACKET); - } - updateCylonLEDs(); - } - break; - - case RADIO_TRAINING_RECEIVED_PACKET: - { - PacketType packetType = identifyPacketType(); //Look at the packet we just received - - if (packetType == PACKET_TRAINING_PING) - { - triggerEvent(TRIGGER_TRAINING_CONTROL_PACKET); - packetsLost = 0; //Reset, used for linkLost testing - sendTrainingDataPacket(); //Someone is pinging us, send training data back - - //Wait for transmission to complete before ending training - while (transactionComplete == false) //If dio0ISR has fired, a packet has arrived - { - if ((millis() - packetTimestamp) > (packetAirTime + controlPacketAirTime)) //Wait for xmit of packet and ACK response - { - if (settings.debugRadio) - systemPrintln("Timeout"); - break; - } - } - if (settings.debugRadio) - systemPrintln("Ending Training"); - - endTraining(false); //We do not have data to apply to settings - } - - else if (packetType == PACKET_TRAINING_DATA) - { - //The waiting node has responded with a data packet - //Absorb training data and then return to normal operation - triggerEvent(TRIGGER_TRAINING_DATA_PACKET); - packetsLost = 0; //Reset, used for linkLost testing - endTraining(true); //Apply data from packet to settings - } - - //During training, only training packets are valid - else //PACKET_BAD, PACKET_NETID_MISMATCH, PACKET_ACK, PACKET_PING, PACKET_DATA - { - triggerEvent(TRIGGER_TRAINING_BAD_PACKET); - returnToReceiving(); - changeState(RADIO_TRAINING_RECEIVING_HERE_FIRST); - } - } - break; } } @@ -2008,77 +1344,13 @@ void selectHeaderAndTrailerBytes() //This is used for determining if we can do remote AT commands or not bool isLinked() { - if (((radioState >= RADIO_P2P_LINK_UP) + if ((radioState >= RADIO_P2P_LINK_UP) && (radioState <= RADIO_P2P_LINK_UP_WAIT_ACK)) - || ((radioState >= RADIO_LINKED_RECEIVING_STANDBY) - && (radioState <= RADIO_LINKED_RECEIVED_PACKET))) return (true); return (false); } -const RADIO_STATE_ENTRY radioStateTable[] = -{ - {RADIO_RESET, "RESET", NULL}, // 0 - - //V1 - // State Name Description - {RADIO_NO_LINK_RECEIVING_STANDBY, "NO_LINK_RECEIVING_STANDBY", "[No Link] Receiving Standby"},// 1 - {RADIO_NO_LINK_TRANSMITTING, "NO_LINK_TRANSMITTING", "[No Link] Transmitting"}, // 2 - {RADIO_NO_LINK_ACK_WAIT, "NO_LINK_ACK_WAIT", "[No Link] Ack Wait"}, // 3 - {RADIO_NO_LINK_RECEIVED_PACKET, "NO_LINK_RECEIVED_PACKET", "[No Link] Received Packet"}, // 4 - {RADIO_LINKED_RECEIVING_STANDBY, "LINKED_RECEIVING_STANDBY", "Receiving Standby "}, // 5 - {RADIO_LINKED_TRANSMITTING, "LINKED_TRANSMITTING", "Transmitting "}, // 6 - {RADIO_LINKED_ACK_WAIT, "LINKED_ACK_WAIT", "Ack Wait "}, // 7 - {RADIO_LINKED_RECEIVED_PACKET, "LINKED_RECEIVED_PACKET", "Received Packet "}, // 8 - {RADIO_BROADCASTING_RECEIVING_STANDBY, "BROADCASTING_RECEIVING_STANDBY", "B-Receiving Standby "}, // 9 - {RADIO_BROADCASTING_TRANSMITTING, "BROADCASTING_TRANSMITTING", "B-Transmitting "}, //10 - {RADIO_BROADCASTING_RECEIVED_PACKET, "BROADCASTING_RECEIVED_PACKET", "B-Received Packet "}, //11 - {RADIO_TRAINING_RECEIVING_HERE_FIRST, "TRAINING_RECEIVING_HERE_FIRST", "[Training] RX Here First"}, //12 - {RADIO_TRAINING_TRANSMITTING, "TRAINING_TRANSMITTING", "[Training] TX"}, //13 - {RADIO_TRAINING_ACK_WAIT, "TRAINING_ACK_WAIT", "[Training] Ack Wait"}, //14 - {RADIO_TRAINING_RECEIVED_PACKET, "TRAINING_RECEIVED_PACKET", "[Training] RX Packet"}, //15 - - //V2 - Point-to-Point Training - // State Name Description - {RADIO_P2P_TRAINING_WAIT_PING_DONE, "P2P_TRAINING_WAIT_PING_DONE", "V2 P2P: Wait TX Training Ping Done"}, //16 - {RADIO_P2P_WAIT_FOR_TRAINING_PARAMS, "P2P_WAIT_FOR_TRAINING_PARAMS", "V2 P2P: Wait for Training params"}, //17 - {RADIO_P2P_WAIT_TRAINING_PARAMS_DONE, "P2P_WAIT_TRAINING_PARAMS_DONE", "V2 P2P: Wait training params done"}, //18 - - //V2 - Point-to-Point link handshake - // State Name Description - {RADIO_P2P_LINK_DOWN, "P2P_LINK_DOWN", "V2 P2P: [No Link] Waiting for Ping"}, //19 - {RADIO_P2P_WAIT_TX_PING_DONE, "P2P_WAIT_TX_PING_DONE", "V2 P2P: [No Link] Wait Ping TX Done"},//20 - {RADIO_P2P_WAIT_ACK_1, "P2P_WAIT_ACK_1", "V2 P2P: [No Link] Waiting for ACK1"}, //21 - {RADIO_P2P_WAIT_TX_ACK_1_DONE, "P2P_WAIT_TX_ACK_1_DONE", "V2 P2P: [No Link] Wait ACK1 TX Done"},//22 - {RADIO_P2P_WAIT_ACK_2, "P2P_WAIT_ACK_2", "V2 P2P: [No Link] Waiting for ACK2"}, //23 - {RADIO_P2P_WAIT_TX_ACK_2_DONE, "P2P_WAIT_TX_ACK_2_DONE", "V2 P2P: [No Link] Wait ACK2 TX Done"},//24 - - //V2 - Point-to-Point, link up, data exchange - // State Name Description - {RADIO_P2P_LINK_UP, "P2P_LINK_UP", "V2 P2P: Receiving Standby"}, //25 - {RADIO_P2P_LINK_UP_WAIT_ACK_DONE, "P2P_LINK_UP_WAIT_ACK_DONE", "V2 P2P: Waiting ACK TX Done"}, //26 - {RADIO_P2P_LINK_UP_WAIT_TX_DONE, "P2P_LINK_UP_WAIT_TX_DONE", "V2 P2P: Waiting TX done"}, //27 - {RADIO_P2P_LINK_UP_WAIT_ACK, "P2P_LINK_UP_WAIT_ACK", "V2 P2P: Waiting for ACK"}, //28 - {RADIO_P2P_LINK_UP_HB_ACK_REXMT, "P2P_LINK_UP_HB_ACK_REXMT", "V2 P2P: Heartbeat ACK ReXmt"}, //29 - - //V2 - Multi-Point data exchange - // State Name Description - {RADIO_MP_STANDBY, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //30 - {RADIO_MP_WAIT_TX_DONE, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //31 - - //V2 - Multi-Point training client states - // State Name Description - {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, "MP_WAIT_TX_TRAINING_PING_DONE", "V2 MP: Wait TX training PING done"}, //32 - {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, "MP_WAIT_RX_RADIO_PARAMETERS", "V2 MP: Wait for radio parameters"}, //33 - {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, "MP_WAIT_TX_PARAM_ACK_DONE", "V2 MP: Wait for TX param ACK done"}, //34 - - //V2 - Multi-Point training server states - // State Name Description - {RADIO_MP_WAIT_FOR_TRAINING_PING, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //35 - {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //36 -}; - void verifyRadioStateTable() { int expectedState; @@ -2255,7 +1527,7 @@ void verifyRadioStateTable() //Verify the datagram type table void verifyV2DatagramType() { - if ((sizeof(v2DatagramType) / sizeof(v2DatagramType[0])) != MAX_DATAGRAM_TYPE) + if ((sizeof(v2DatagramType) / sizeof(v2DatagramType[0])) != MAX_V2_DATAGRAM_TYPE) { systemPrintln("ERROR - Please update the v2DatagramTable"); while (1) diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index c296bc38..c8e7a94f 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -13,8 +13,6 @@ void selectTraining(bool defaultTraining) beginTrainingClient(); } } - else if (settings.protocolVersion == 1) - beginTraining(defaultTraining); else { //Handle unknown future versions @@ -25,195 +23,6 @@ void selectTraining(bool defaultTraining) } } -/* - beginTraining - | - | Save settings - | - V - RADIO_TRAINING_TRANSMITTING - | - V - RADIO_TRAINING_ACK_WAIT --------------. - | | - V | - RADIO_TRAINING_RECEIVING_HERE_FIRST | - | | - +<----------------------------’ - | - V - RADIO_TRAINING_RECEIVED_PACKET - | - V - endTraining - - - beginTraining - - 1. Disable point-to-point - 2. Disable frequency hopping - 3. Reduce power to minimum - 4. Generate HOP table - 5. Compute channel spacing - 6. Set training frequency - 7. Configure the radio - 8. Send training ping - 9. Set state RADIO_TRAINING_TRANSMITTING - - endTraining - - 1. Restore original settings - 2. Update encryption key - 3. Set net ID -*/ - -void beginTraining(bool defaultTraining) -{ - if ((settings.debug == true) || (settings.debugTraining == true)) - { - systemPrint("Begin "); - if (defaultTraining) - systemPrint("default "); - systemPrintln("point-to-point training"); - } - - //Save the parameters - if (defaultTraining) - originalSettings = defaultSettings; //Upon completion we will return to default settings - else - originalSettings = settings; //Make copy of current settings - - //Change to known training frequency based on available freq and current major firmware version - //This will allow different minor versions to continue to train to each other - //During training use default radio settings. This ensures both radios are at known good settings. - settings = defaultSettings; //Move to default settings - - //Disable hopping - settings.frequencyHop = false; - - //Disable NetID checking - settings.pointToPoint = false; - settings.verifyRxNetID = false; - - //Debug training if requested - if (originalSettings.debugTraining) - { - settings.debugTraining = originalSettings.debugTraining; - settings.printPktData = originalSettings.printPktData; - settings.printRfData = originalSettings.printRfData; - } - - //Turn power as low as possible. We assume two units will be near each other. - settings.radioBroadcastPower_dbm = 14; - - generateHopTable(); //Generate frequency table based on current settings - - configureRadio(); //Setup radio with settings - - //Move to frequency that is not part of the hop table - //In normal operation we move 1/2 a channel away from min. In training, we move a full channel away + major firmware version. - float channelSpacing = (settings.frequencyMax - settings.frequencyMin) / (float)(settings.numberOfChannels + 2); - float trainFrequency = settings.frequencyMin + (channelSpacing * (FIRMWARE_VERSION_MAJOR % settings.numberOfChannels)); - - channels[0] = trainFrequency; //Inject this frequency into the channel table - - //Transmit general ping packet to see if anyone else is sitting on the training channel - //Send special packet with train = 1, then wait for response - sendTrainingPingPacket(); - - //Recalculate packetAirTime because we need to wait not for a 2-byte response, but a 19 byte response - packetAirTime = calcAirTime(sizeof(trainEncryptionKey) + sizeof(trainNetID) + 2); - - //Reset cylon variables - startCylonLEDs(); - - changeState(RADIO_TRAINING_TRANSMITTING); -} - -//Upon successful exchange of keys, go back to original settings -void endTraining(bool newTrainingAvailable) -{ - petWDT(); - settings = originalSettings; //Return to original radio settings - - //Apply new netID and AES if available - if (newTrainingAvailable) - { - if (lastPacketSize == sizeof(settings.encryptionKey) + 1) //Error check, should be AES key + NetID - { - //Move training data into settings - for (uint8_t x = 0 ; x < sizeof(settings.encryptionKey); x++) - settings.encryptionKey[x] = lastPacket[x]; - - settings.netID = lastPacket[lastPacketSize - 1]; //Last spot in array is netID - - if ((settings.debug == true) || (settings.debugTraining == true)) - { - systemPrint("New ID: "); - systemPrintln(settings.netID); - - systemPrint("New Key: "); - for (uint8_t i = 0 ; i < 16 ; i++) - { - systemPrint(settings.encryptionKey[i], HEX); - systemPrint(" "); - } - systemPrintln(); - } - } - else - { - //If the packet was marked as training but was not valid training data, then give up. Return to normal radio mode with pre-existing settings. - } - } - else - { - //We transmitted the training data, move the local training data into settings - for (uint8_t x = 0 ; x < sizeof(settings.encryptionKey); x++) - settings.encryptionKey[x] = trainEncryptionKey[x]; - - settings.netID = trainNetID; //Last spot in array is netID - } - - petWDT(); - recordSystemSettings(); - - petWDT(); - generateHopTable(); //Generate frequency table based on current settings - - configureRadio(); //Setup radio with settings - - returnToReceiving(); - - //Blink LEDs to indicate training success - setRSSI(0b0000); - delayWDT(100); - - setRSSI(0b1001); - delayWDT(500); - - setRSSI(0b0110); - delayWDT(500); - - setRSSI(0b1111); - delayWDT(500); - - setRSSI(0b0000); - delayWDT(500); - - setRSSI(0b1111); - delayWDT(1500); - - setRSSI(0); - delayWDT(2000); - - changeState(RADIO_RESET); - - sentFirstPing = false; //Send ping as soon as we exit - - systemPrintln("LINK TRAINED"); -} - //Generate new netID/AES key to share //We assume the user needs to maintain their settings (airSpeed, numberOfChannels, freq min/max, bandwidth/spread/hop) //but need to be on a different netID/AES key. @@ -226,24 +35,24 @@ void generateTrainingSettings() randomSeed(radio.randomByte()); //Generate new NetID - trainNetID = random(0, 256); //Inclusive, exclusive + settings.netID = random(0, 256); //Inclusive, exclusive //Generate new AES Key. User may not be using AES but we still need both radios to have the same key in case they do enable AES. for (int x = 0 ; x < 16 ; x++) - trainEncryptionKey[x] = random(0, 256); //Inclusive, exclusive + settings.encryptionKey[x] = random(0, 256); //Inclusive, exclusive //We do not generate new AES Initial Values here. Those are generated during generateHopTable() based on the unit's settings. if ((settings.debug == true) || (settings.debugTraining == true)) { systemPrint("Select new NetID: "); - systemPrintln(trainNetID); + systemPrintln(settings.netID); systemPrint("Select new Encryption Key:"); for (uint8_t i = 0 ; i < 16 ; i++) { systemPrint(" "); - systemPrint(trainEncryptionKey[i], HEX); + systemPrint(settings.encryptionKey[i], HEX); } systemPrintln(); } @@ -289,6 +98,9 @@ void beginTrainingPointToPoint(bool defaultTraining) } systemPrintln("Point-to-point training"); + //Generate new netID and encryption key + generateTrainingSettings(); + //Common initialization commonTrainingInitialization(); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index cd4311ad..1a0a4a1b 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -2,26 +2,6 @@ typedef enum { RADIO_RESET = 0, - //V1 - RADIO_NO_LINK_RECEIVING_STANDBY, - RADIO_NO_LINK_TRANSMITTING, - RADIO_NO_LINK_ACK_WAIT, - RADIO_NO_LINK_RECEIVED_PACKET, - - RADIO_LINKED_RECEIVING_STANDBY, - RADIO_LINKED_TRANSMITTING, - RADIO_LINKED_ACK_WAIT, - RADIO_LINKED_RECEIVED_PACKET, - - RADIO_BROADCASTING_RECEIVING_STANDBY, - RADIO_BROADCASTING_TRANSMITTING, - RADIO_BROADCASTING_RECEIVED_PACKET, - - RADIO_TRAINING_RECEIVING_HERE_FIRST, - RADIO_TRAINING_TRANSMITTING, - RADIO_TRAINING_ACK_WAIT, - RADIO_TRAINING_RECEIVED_PACKET, - //V2 //Point-to-Point Training RADIO_P2P_TRAINING_WAIT_PING_DONE, @@ -64,10 +44,55 @@ RadioStates radioState = RADIO_RESET; typedef struct _RADIO_STATE_ENTRY { RadioStates state; + bool rxState; const char * name; const char * description; } RADIO_STATE_ENTRY; +const RADIO_STATE_ENTRY radioStateTable[] = +{ + {RADIO_RESET, 0, "RESET", NULL}, // 0 + + //V2 - Point-to-Point Training + // State RX Name Description + {RADIO_P2P_TRAINING_WAIT_PING_DONE, 0, "P2P_TRAINING_WAIT_PING_DONE", "V2 P2P: Wait TX Training Ping Done"}, // 1 + {RADIO_P2P_WAIT_FOR_TRAINING_PARAMS, 1, "P2P_WAIT_FOR_TRAINING_PARAMS", "V2 P2P: Wait for Training params"}, // 2 + {RADIO_P2P_WAIT_TRAINING_PARAMS_DONE, 0, "P2P_WAIT_TRAINING_PARAMS_DONE", "V2 P2P: Wait training params done"}, // 3 + + //V2 - Point-to-Point link handshake + // State RX Name Description + {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "V2 P2P: [No Link] Waiting for Ping"}, // 4 + {RADIO_P2P_WAIT_TX_PING_DONE, 0, "P2P_WAIT_TX_PING_DONE", "V2 P2P: [No Link] Wait Ping TX Done"},// 5 + {RADIO_P2P_WAIT_ACK_1, 1, "P2P_WAIT_ACK_1", "V2 P2P: [No Link] Waiting for ACK1"}, // 6 + {RADIO_P2P_WAIT_TX_ACK_1_DONE, 0, "P2P_WAIT_TX_ACK_1_DONE", "V2 P2P: [No Link] Wait ACK1 TX Done"},// 7 + {RADIO_P2P_WAIT_ACK_2, 1, "P2P_WAIT_ACK_2", "V2 P2P: [No Link] Waiting for ACK2"}, // 8 + {RADIO_P2P_WAIT_TX_ACK_2_DONE, 0, "P2P_WAIT_TX_ACK_2_DONE", "V2 P2P: [No Link] Wait ACK2 TX Done"},// 9 + + //V2 - Point-to-Point, link up, data exchange + // State RX Name Description + {RADIO_P2P_LINK_UP, 1, "P2P_LINK_UP", "V2 P2P: Receiving Standby"}, //10 + {RADIO_P2P_LINK_UP_WAIT_ACK_DONE, 0, "P2P_LINK_UP_WAIT_ACK_DONE", "V2 P2P: Waiting ACK TX Done"}, //11 + {RADIO_P2P_LINK_UP_WAIT_TX_DONE, 0, "P2P_LINK_UP_WAIT_TX_DONE", "V2 P2P: Waiting TX done"}, //12 + {RADIO_P2P_LINK_UP_WAIT_ACK, 1, "P2P_LINK_UP_WAIT_ACK", "V2 P2P: Waiting for ACK"}, //13 + {RADIO_P2P_LINK_UP_HB_ACK_REXMT, 0, "P2P_LINK_UP_HB_ACK_REXMT", "V2 P2P: Heartbeat ACK ReXmt"}, //14 + + //V2 - Multi-Point data exchange + // State RX Name Description + {RADIO_MP_STANDBY, 1, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //15 + {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //16 + + //V2 - Multi-Point training client states + // State RX Name Description + {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, 0, "MP_WAIT_TX_TRAINING_PING_DONE", "V2 MP: Wait TX training PING done"}, //17 + {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, 1, "MP_WAIT_RX_RADIO_PARAMETERS", "V2 MP: Wait for radio parameters"}, //18 + {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, 0, "MP_WAIT_TX_PARAM_ACK_DONE", "V2 MP: Wait for TX param ACK done"}, //19 + + //V2 - Multi-Point training server states + // State RX Name Description + {RADIO_MP_WAIT_FOR_TRAINING_PING, 1, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //20 + {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, 0, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //21 +}; + //Possible types of packets received typedef enum { @@ -98,24 +123,14 @@ typedef enum DATAGRAM_TRAINING_ACK, //15 //Add new V2 datagram types before this line - MAX_DATAGRAM_TYPE, - - //V1 packet types are below - PACKET_BAD, - PACKET_NETID_MISMATCH, - PACKET_ACK, //ack = 1 - PACKET_DUPLICATE, - PACKET_PING, //ack = 0, len = 0 - PACKET_DATA, - - PACKET_COMMAND_ACK, //remoteCommand = 1, ack = 1 - PACKET_COMMAND_DATA, //remoteCommand = 1 + MAX_V2_DATAGRAM_TYPE, - PACKET_COMMAND_RESPONSE_ACK, //remoteCommand = 1, remoteCommandResponse = 1, ack = 1 - PACKET_COMMAND_RESPONSE_DATA, //remoteCommand = 1, remoteCommandResponse = 1, + //Add new protocol datagrams above this line - PACKET_TRAINING_PING, - PACKET_TRAINING_DATA, + //Common datagram types + DATAGRAM_BAD, + DATAGRAM_NETID_MISMATCH, + DATAGRAM_DUPLICATE, } PacketType; const char * const v2DatagramType[] = @@ -125,8 +140,8 @@ const char * const v2DatagramType[] = "PING", "ACK-1", "ACK-2", // 6 7 8 9 10 11 "DATA", "SF6-DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", - // 12 - "DATAGRAM_DATAGRAM", + // 12 + "DATAGRAM", // 13 14 15 "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK" }; @@ -314,7 +329,7 @@ typedef struct struct_settings { bool debugReceive = false; //Print receive processing bool debugTransmit = false; //Print transmit processing bool printTxErrors = false; //Print any transmit errors - uint8_t protocolVersion = 1; //Select the radio protocol + uint8_t protocolVersion = 2; //Select the radio protocol bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds bool debugDatagrams = false; //Print the datagrams uint16_t overheadTime = 10; ////ms added to ack and datagram times before ACK timeout occurs diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 2c1dec2b..e53a8f22 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -17,6 +17,13 @@ ati6 ati7 ati8 ati9 +ati10 +ati11 +ati12 +ati13 +ati14 +ati15 +ati16 ati0 ats28=0 diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index 07fc4f3b..5ab8c6a6 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -9,6 +9,7 @@ at? Command summary: AT? - Print the command summary ATF - Enter training mode and return to factory defaults + ATG - Generate new netID and encryption key ATI - Display the radio version ATI? - Display the information commands ATIn - Display system information @@ -30,6 +31,13 @@ ati? ATI6 - Display AES key ATI7 - Show current FHSS channel ATI8 - Display unique ID + ATI9 - Display the total datagrams sent + ATI10 - Display the total datagrams received + ATI11 - Display the total frames sent + ATI12 - Display the total frames received + ATI13 - Display the total bad frames received + ATI14 - Display the total duplicate frames received + ATI15 - Display the total lost TX frames # Allow leading white space (space, tab, cr, lf) ERROR ati @@ -40,18 +48,32 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati2 2.0 ati3 --157 +-68 ati4 -167 +255 ati5 506 ati6 37782141A665734E4475672AE6308308 ati7 -0 +11 ati8 CB0C64334A34555020312E34242815FF ati9 +4 +ati10 +2 +ati11 +2 +ati12 +2 +ati13 +0 +ati14 +0 +ati15 +0 +ati16 ERROR ati0 ATS0:SerialSpeed=57600 @@ -99,7 +121,7 @@ ATS41:TriggerEnable: 63-32=-1 ATS42:DebugReceive=0 ATS43:DebugTransmit=0 ATS44:PrintTxErrors=0 -ATS45:protocolVersion=1 +ATS45:protocolVersion=2 ATS46:PrintTimestamp=0 ATS47:DebugDatagrams=0 ATS48:OverHeadtime=10 From bcd6b92dbf9e030458a93b9105c0fb43aa204a36 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 14 Oct 2022 12:34:30 -0600 Subject: [PATCH 041/594] Consolidate triggers --- Firmware/LoRaSerial_Firmware/settings.h | 60 +++++-------------------- 1 file changed, 11 insertions(+), 49 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 1a0a4a1b..e553b41c 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -193,62 +193,24 @@ enum TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND, TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, TRIGGER_BAD_PACKET, - - TRIGGER_ACK_PROCESSED, - TRIGGER_DATA_SEND, TRIGGER_RTR_2BYTE, TRIGGER_RTR_255BYTE, - - TRIGGER_LINK_SEND_PING, - TRIGGER_LINK_SENT_ACK_PACKET, - TRIGGER_LINK_NOISE_TRIGGERED_HOP, - TRIGGER_LINK_NOISE_TRIGGERED_HOP_ACK_WAIT, - TRIGGER_LINK_NO_ACK_GIVEUP, - TRIGGER_LINK_PACKET_RESEND, - TRIGGER_LINK_DATA_PACKET, - TRIGGER_LINK_PACKET_RECEIVED, - - TRIGGER_NOLINK_SEND_PING, - TRIGGER_NOLINK_SEND_ACK_PACKET, - TRIGGER_NOLINK_NOISE_TRIGGERED_HOP, - TRIGGER_NOLINK_NO_ACK_GIVEUP, - TRIGGER_NOLINK_IDENT_PACKET, - TRIGGER_BROADCAST_DATA_PACKET, TRIGGER_BROADCAST_PACKET_RECEIVED, - - TRIGGER_RX_IN_PROGRESS, - TRIGGER_LINK_BAD_PACKET, - TRIGGER_LINK_DUPLICATE_PACKET, - TRIGGER_LINK_CONTROL_PACKET, - - TRIGGER_TRAINING_BAD_PACKET, TRIGGER_TRAINING_CONTROL_PACKET, TRIGGER_TRAINING_DATA_PACKET, TRIGGER_TRAINING_NO_ACK, - - TRIGGER_COMMAND_CONTROL_PACKET, - TRIGGER_COMMAND_CONTROL_PACKET_ACK, - TRIGGER_COMMAND_DATA_PACKET_ACK, - TRIGGER_COMMAND_PACKET_RECEIVED, - TRIGGER_COMMAND_SENT_ACK_PACKET, - TRIGGER_COMMAND_PACKET_RESEND, - TRIGGER_PACKET_COMMAND_DATA, - - //Training client triggers - TRIGGER_TRAINING_CLIENT_TX_PING, //34, 875us - TRIGGER_TRAINING_CLIENT_TX_PING_DONE, //35, 900us - TRIGGER_TRAINING_CLIENT_RX_PARAMS, //38, 975us - TRIGGER_TRAINING_CLIENT_TX_ACK, //39, 1000us - TRIGGER_TRAINING_CLIENT_TX_ACK_DONE, //40, 1025us - TRIGGER_TRAINING_COMPLETE, //41, 1050us - - //Training server triggers - TRIGGER_TRAINING_SERVER_RX, //42, 1075us - TRIGGER_TRAINING_SERVER_TX_PARAMS, //43, 1100us - TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE, //44, 1125us - TRIGGER_TRAINING_SERVER_RX_ACK, //45, 1150us - TRIGGER_TRAINING_SERVER_STOPPED, //46, 1175us + TRIGGER_TRAINING_CLIENT_TX_PING, + TRIGGER_TRAINING_CLIENT_TX_PING_DONE, + TRIGGER_TRAINING_CLIENT_RX_PARAMS, + TRIGGER_TRAINING_CLIENT_TX_ACK, + TRIGGER_TRAINING_CLIENT_TX_ACK_DONE, + TRIGGER_TRAINING_COMPLETE, + TRIGGER_TRAINING_SERVER_RX, + TRIGGER_TRAINING_SERVER_TX_PARAMS, + TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE, + TRIGGER_TRAINING_SERVER_RX_ACK, + TRIGGER_TRAINING_SERVER_STOPPED, }; //Control where to print command output From 6c36df5e63d5066f9cab6198a52f6a027930454b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 14 Oct 2022 08:37:05 -1000 Subject: [PATCH 042/594] Remove the DATAGRAM_SF6_DATA frame definition --- .../LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/RadioV2.ino | 7 ++---- Firmware/LoRaSerial_Firmware/States.ino | 2 -- Firmware/LoRaSerial_Firmware/settings.h | 25 +++++++++---------- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 723d1835..23ed9d6f 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -235,7 +235,6 @@ unsigned long roundTripMillis; //Transmit control const int datagramsExpectingAcks = 0 | (1 << DATAGRAM_DATA) - | (1 << DATAGRAM_SF6_DATA) | (1 << DATAGRAM_REMOTE_COMMAND) | (1 << DATAGRAM_REMOTE_COMMAND_RESPONSE) | (1 << DATAGRAM_HEARTBEAT); diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 0faaaf66..6de5205e 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -731,7 +731,7 @@ PacketType rcvDatagram() */ //Get the spread factor 6 length - if (datagramType == DATAGRAM_SF6_DATA) + if (settings.radioSpreadFactor == 6) { if (rxDataBytes >= (*rxData + minDatagramSize)) rxDataBytes = *rxData++; @@ -780,7 +780,6 @@ PacketType rcvDatagram() break; case DATAGRAM_DATA: - case DATAGRAM_SF6_DATA: case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: @@ -854,7 +853,6 @@ PacketType rcvDatagram() case DATAGRAM_DATA: case DATAGRAM_DATA_ACK: - case DATAGRAM_SF6_DATA: case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: @@ -905,7 +903,6 @@ void transmitDatagram() case DATAGRAM_DATA: case DATAGRAM_DATA_ACK: - case DATAGRAM_SF6_DATA: case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: @@ -987,7 +984,7 @@ void transmitDatagram() printControl(control); //Add the spread factor 6 length is required - if (txControl.datagramType == DATAGRAM_SF6_DATA) + if (settings.radioSpreadFactor == 6) { *header++ = length; txDatagramSize = MAX_PACKET_SIZE - trailerBytes; //We're now going to transmit a full size datagram diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index a401fd65..bd3b6fab 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -863,7 +863,6 @@ void updateRadioState() case DATAGRAM_DATA: case DATAGRAM_DATA_ACK: - case DATAGRAM_SF6_DATA: case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: @@ -925,7 +924,6 @@ void updateRadioState() case DATAGRAM_DATA: case DATAGRAM_DATA_ACK: - case DATAGRAM_SF6_DATA: case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index e553b41c..06dc8ec8 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -108,19 +108,18 @@ typedef enum //V2: Point-to-Point data exchange DATAGRAM_DATA, // 6 - DATAGRAM_SF6_DATA, // 7 - DATAGRAM_DATA_ACK, // 8 - DATAGRAM_HEARTBEAT, // 9 - DATAGRAM_REMOTE_COMMAND, //10 - DATAGRAM_REMOTE_COMMAND_RESPONSE, //11 + DATAGRAM_DATA_ACK, // 7 + DATAGRAM_HEARTBEAT, // 8 + DATAGRAM_REMOTE_COMMAND, // 9 + DATAGRAM_REMOTE_COMMAND_RESPONSE, //10 //V2: Multi-Point data exchange - DATAGRAM_DATAGRAM, //12 + DATAGRAM_DATAGRAM, //11 //V2: Multi-Point training exchange - DATAGRAM_TRAINING_PING, //13 - DATAGRAM_TRAINING_PARAMS, //14 - DATAGRAM_TRAINING_ACK, //15 + DATAGRAM_TRAINING_PING, //12 + DATAGRAM_TRAINING_PARAMS, //13 + DATAGRAM_TRAINING_ACK, //14 //Add new V2 datagram types before this line MAX_V2_DATAGRAM_TYPE, @@ -138,11 +137,11 @@ const char * const v2DatagramType[] = "P2P_TRAINING_PING", "P2P_TRAINING_PARAMS", // 3 4 5 "PING", "ACK-1", "ACK-2", - // 6 7 8 9 10 11 - "DATA", "SF6-DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", - // 12 + // 6 7 8 9 10 + "DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", + // 11 "DATAGRAM", - // 13 14 15 + // 12 13 14 "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK" }; From e4fa9bde201f0c4898cf5eec759e6bbc0e9f12a2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 14 Oct 2022 09:00:06 -1000 Subject: [PATCH 043/594] Remove setting.frameSize --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 +- Firmware/LoRaSerial_Firmware/Radio.ino | 4 +- Firmware/LoRaSerial_Firmware/RadioV2.ino | 1 - Firmware/LoRaSerial_Firmware/Serial.ino | 6 +-- Firmware/LoRaSerial_Firmware/settings.h | 1 - Firmware/Tools/Command_Validation_Script.txt | 8 +--- .../Results/Command_Validation_Results.txt | 48 ++++++++----------- 7 files changed, 30 insertions(+), 43 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9eacf0b0..6d5e1a81 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -158,6 +158,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI13 - Display the total bad frames received"); systemPrintln(" ATI14 - Display the total duplicate frames received"); systemPrintln(" ATI15 - Display the total lost TX frames"); + systemPrintln(" ATI16 - Display the maximum datagram size"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(); @@ -224,6 +225,9 @@ bool commandAT(const char * commandString) case ('5'): //ATI15 - Display the total lost TX frames systemPrintln(lostFrames); break; + case ('6'): //ATI16 - Display the maximum datagram size + systemPrintln(maxDatagramSize); + break; } } @@ -515,7 +519,6 @@ const COMMAND_ENTRY commands[] = {15, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &settings.radioCodingRate}, {16, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &settings.radioSyncWord}, {17, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &settings.radioPreambleLength}, - {18, 16, 254, 0, TYPE_U8, valInt, "FrameSize", &settings.frameSize}, {19, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &settings.serialTimeoutBeforeSendingFrame_ms}, {20, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 45c9f85c..c84c19eb 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -254,8 +254,8 @@ float calcSymbolTime() //Given spread factor, bandwidth, coding rate and frame size, return most bytes we can push per second uint16_t calcMaxThroughput() { - uint8_t mostFramesPerSecond = 1000 / calcAirTime(settings.frameSize); - uint16_t mostBytesPerSecond = settings.frameSize * mostFramesPerSecond; + uint8_t mostFramesPerSecond = 1000 / calcAirTime(MAX_PACKET_SIZE); + uint16_t mostBytesPerSecond = maxDatagramSize * mostFramesPerSecond; return (mostBytesPerSecond); } diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 6de5205e..43cce001 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -425,7 +425,6 @@ void updateRadioParameters(uint8_t * rxData, bool pointToPoint) originalSettings.radioCodingRate = params.radioCodingRate; originalSettings.radioSyncWord = params.radioSyncWord; originalSettings.radioPreambleLength = params.radioPreambleLength; - originalSettings.frameSize = params.frameSize; originalSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; originalSettings.heartbeatTimeout = params.heartbeatTimeout; originalSettings.autoTuneFrequency = params.autoTuneFrequency; diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index f725cfbf..2e332749 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -228,7 +228,7 @@ bool isCTS() bool processWaitingSerial(bool sendNow) { //Push any available data out - if (availableRXBytes() >= settings.frameSize) + if (availableRXBytes() >= maxDatagramSize) { if (settings.debugRadio) systemPrintln("Sending max frame"); @@ -252,7 +252,7 @@ void readyOutgoingPacket() { uint16_t length; uint16_t bytesToSend = availableRXBytes(); - if (bytesToSend > settings.frameSize) bytesToSend = settings.frameSize; + if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; //SF6 requires an implicit header which means there is no dataLength in the header if (settings.radioSpreadFactor == 6) @@ -284,7 +284,7 @@ void readyOutgoingCommandPacket() { uint16_t length; uint16_t bytesToSend = availableTXCommandBytes(); - if (bytesToSend > settings.frameSize) bytesToSend = settings.frameSize; + if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; //SF6 requires an implicit header which means there is no dataLength in the header if (settings.radioSpreadFactor == 6) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 06dc8ec8..0f6058b9 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -263,7 +263,6 @@ typedef struct struct_settings { uint8_t radioCodingRate = 8; //5 to 8. Higher coding rates ensure less packets dropped. uint8_t radioSyncWord = 18; //18 = 0x12 is default for custom/private networks. Different sync words does *not* guarantee a remote radio will not get packet. uint16_t radioPreambleLength = 8; //Number of symbols. Different lengths does *not* guarantee a remote radio privacy. 8 to 11 works. 8 to 15 drops some. 8 to 20 is silent. - uint8_t frameSize = MAX_PACKET_SIZE - 2; //Send batches of bytes through LoRa link, max (255 - control trailer) = 253. uint16_t serialTimeoutBeforeSendingFrame_ms = 50; //Send partial buffer if time expires bool debug = false; //Print basic events: ie, radio state changes bool echo = false; //Print locally inputted serial diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index e53a8f22..4e571f7b 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -24,6 +24,7 @@ ati13 ati14 ati15 ati16 +ati17 ati0 ats28=0 @@ -267,12 +268,7 @@ ats17=8 ats17? # FrameSize -ats18=15 -ats18=16 -ats18=256 -ats18=255 -ats18=254 -ats18=253 +ats18=0 ats18? # FrameTimeout diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index 5ab8c6a6..c16509bb 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -38,6 +38,7 @@ ati? ATI13 - Display the total bad frames received ATI14 - Display the total duplicate frames received ATI15 - Display the total lost TX frames + ATI16 - Display the maximum datagram size # Allow leading white space (space, tab, cr, lf) ERROR ati @@ -48,25 +49,25 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati2 2.0 ati3 --68 +-157 ati4 -255 +42 ati5 506 ati6 37782141A665734E4475672AE6308308 ati7 -11 +0 ati8 -CB0C64334A34555020312E34242815FF +E5D60B0B4A34555020312E34212815FF ati9 -4 +54 ati10 -2 +0 ati11 -2 +0 ati12 -2 +0 ati13 0 ati14 @@ -74,6 +75,8 @@ ati14 ati15 0 ati16 +253 +ati17 ERROR ati0 ATS0:SerialSpeed=57600 @@ -94,7 +97,6 @@ ATS14:SpreadFactor=9 ATS15:CodingRate=8 ATS16:SyncWord=18 ATS17:PreambleLength=8 -ATS18:FrameSize=253 ATS19:FrameTimeout=50 ATS20:Debug=0 ATS21:Echo=0 @@ -117,7 +119,7 @@ ATS37:VerifyRxNetID=0 ATS38:TriggerWidth=25 ATS39:TriggerWidthIsMultiplier=1 ATS40:TriggerEnable: 31-0=-1 -ATS41:TriggerEnable: 63-32=-1 +ATS41:TriggerEnable2: 63-32=-1 ATS42:DebugReceive=0 ATS43:DebugTransmit=0 ATS44:PrintTxErrors=0 @@ -583,20 +585,10 @@ ats17? 8 # FrameSize ERROR -ats18=15 +ats18=0 ERROR -ats18=16 -OK -ats18=256 -ERROR -ats18=255 -ERROR -ats18=254 -OK -ats18=253 -OK ats18? -253 +ERROR # FrameTimeout ERROR ats19=9 @@ -858,13 +850,13 @@ TriggerEnable: 31-0=-1 # TriggerEnable: 63-32 ERROR ats41=0 -TriggerEnable: 63-32=0 +TriggerEnable2: 63-32=0 OK ats41=4294967295 -TriggerEnable: 63-32=-1 +TriggerEnable2: 63-32=-1 OK ats41? -TriggerEnable: 63-32=-1 +TriggerEnable2: 63-32=-1 # DebugReceive ERROR ats42=1 @@ -906,8 +898,7 @@ ERROR ats45=0 ERROR ats45=1 -protocolVersion=1 -OK +ERROR ats45=2 protocolVersion=2 OK @@ -1094,7 +1085,6 @@ ATS49:EnableCRC16=0 ATS4:EncryptData=1 ATS5:EncryptionKey=37782141A665734E4475672AE6308308 ATS23:FlowControl=0 -ATS18:FrameSize=253 ATS19:FrameTimeout=50 ATS11:FrequencyHop=1 ATS9:FrequencyMax=928.000 @@ -1121,8 +1111,8 @@ ATS14:SpreadFactor=9 ATS16:SyncWord=18 ATS56:TrainingKey=01020304050607080910111213141516 ATS51:TrainingServer=0 +ATS41:TriggerEnable2: 63-32=-1 ATS40:TriggerEnable: 31-0=-1 -ATS41:TriggerEnable: 63-32=-1 ATS38:TriggerWidth=25 ATS39:TriggerWidthIsMultiplier=1 ATS7:TxPower=30 From cc9bc365d4a6a331613d375abac428037cbe15fc Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 14 Oct 2022 16:11:15 -1000 Subject: [PATCH 044/594] Fix DATAGRAM numbers --- Firmware/LoRaSerial_Firmware/settings.h | 32 ++++++++++++------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 0f6058b9..b3285ede 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -102,24 +102,24 @@ typedef enum DATAGRAM_P2P_TRAINING_PARAMS, // 1 //V2: Link establishment handshake - DATAGRAM_PING, // 3 - DATAGRAM_ACK_1, // 4 - DATAGRAM_ACK_2, // 5 + DATAGRAM_PING, // 2 + DATAGRAM_ACK_1, // 3 + DATAGRAM_ACK_2, // 4 //V2: Point-to-Point data exchange - DATAGRAM_DATA, // 6 - DATAGRAM_DATA_ACK, // 7 - DATAGRAM_HEARTBEAT, // 8 - DATAGRAM_REMOTE_COMMAND, // 9 - DATAGRAM_REMOTE_COMMAND_RESPONSE, //10 + DATAGRAM_DATA, // 5 + DATAGRAM_DATA_ACK, // 6 + DATAGRAM_HEARTBEAT, // 7 + DATAGRAM_REMOTE_COMMAND, // 8 + DATAGRAM_REMOTE_COMMAND_RESPONSE, // 9 //V2: Multi-Point data exchange - DATAGRAM_DATAGRAM, //11 + DATAGRAM_DATAGRAM, //10 //V2: Multi-Point training exchange - DATAGRAM_TRAINING_PING, //12 - DATAGRAM_TRAINING_PARAMS, //13 - DATAGRAM_TRAINING_ACK, //14 + DATAGRAM_TRAINING_PING, //11 + DATAGRAM_TRAINING_PARAMS, //12 + DATAGRAM_TRAINING_ACK, //13 //Add new V2 datagram types before this line MAX_V2_DATAGRAM_TYPE, @@ -135,13 +135,13 @@ typedef enum const char * const v2DatagramType[] = {// 0 1 "P2P_TRAINING_PING", "P2P_TRAINING_PARAMS", - // 3 4 5 + // 2 3 4 "PING", "ACK-1", "ACK-2", - // 6 7 8 9 10 + // 5 6 7 8 9 "DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", - // 11 + // 10 "DATAGRAM", - // 12 13 14 + // 11 12 13 "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK" }; From 9c9285ab68d8f104850002e7df56fe8168e7ef22 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 17 Oct 2022 07:31:17 -1000 Subject: [PATCH 045/594] Simplify and document the ACK number management --- .../LoRaSerial_Firmware.ino | 39 +++++++++++--- Firmware/LoRaSerial_Firmware/RadioV2.ino | 51 ++++++++----------- Firmware/LoRaSerial_Firmware/States.ino | 9 ++-- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 23ed9d6f..6c218e0d 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -212,15 +212,45 @@ uint8_t txDatagramSize; //Point-to-Point unsigned long datagramTimer; -uint8_t expectedDatagramNumber; uint16_t pingRandomTime; uint16_t heartbeatRandomTime; +/* ACK Number Management + + System A System B + + txAckNumber + | + V + Tx DATA Frame -----------------------> Rx DATA Frame + | + V + AckNumber == rmtTxAckNumber + | + | yes + V + rxAckNumber = rmtTxAckNumber++ + | + V + Rx DATA_Ack Frame <--------------------- Tx DATA_ACK Frame + | + V + ackNumber == txAckNumber + | + | yes + V + txAckNumber++ +*/ + +//ACK management +uint8_t rmtTxAckNumber; +uint8_t rxAckNumber; +uint8_t txAckNumber; + //Receive control uint8_t incomingBuffer[MAX_PACKET_SIZE]; uint8_t minDatagramSize; uint8_t maxDatagramSize; -uint8_t expectedAckNumber; uint8_t * rxData; uint8_t rxDataBytes; unsigned long heartbeatTimer; @@ -233,11 +263,6 @@ unsigned long timestampOffset; unsigned long roundTripMillis; //Transmit control -const int datagramsExpectingAcks = 0 - | (1 << DATAGRAM_DATA) - | (1 << DATAGRAM_REMOTE_COMMAND) - | (1 << DATAGRAM_REMOTE_COMMAND_RESPONSE) - | (1 << DATAGRAM_HEARTBEAT); uint8_t * endOfTxData; CONTROL_U8 txControl; diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 43cce001..7b23bc37 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -53,7 +53,6 @@ void xmitDatagramP2PTrainingPing() */ txControl.datagramType = DATAGRAM_P2P_TRAINING_PING; - txControl.ackNumber = 0; transmitDatagram(); } @@ -82,7 +81,6 @@ void xmitDatagramP2pTrainingParams() */ txControl.datagramType = DATAGRAM_P2P_TRAINING_PARAMS; - txControl.ackNumber = 0; transmitDatagram(); } @@ -161,7 +159,6 @@ void xmitDatagramP2PPing() */ txControl.datagramType = DATAGRAM_PING; - txControl.ackNumber = 0; transmitDatagram(); } @@ -184,7 +181,6 @@ void xmitDatagramP2PAck1() */ txControl.datagramType = DATAGRAM_ACK_1; - txControl.ackNumber = 0; transmitDatagram(); } @@ -207,7 +203,6 @@ void xmitDatagramP2PAck2() */ txControl.datagramType = DATAGRAM_ACK_2; - txControl.ackNumber = 0; transmitDatagram(); } @@ -230,8 +225,6 @@ void xmitDatagramP2PCommand() */ txControl.datagramType = DATAGRAM_REMOTE_COMMAND; - txControl.ackNumber = expectedDatagramNumber; - expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; transmitDatagram(); } @@ -250,8 +243,6 @@ void xmitDatagramP2PCommandResponse() */ txControl.datagramType = DATAGRAM_REMOTE_COMMAND_RESPONSE; - txControl.ackNumber = expectedDatagramNumber; - expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; transmitDatagram(); } @@ -270,8 +261,6 @@ void xmitDatagramP2PData() */ txControl.datagramType = DATAGRAM_DATA; - txControl.ackNumber = expectedDatagramNumber; - expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; transmitDatagram(); } @@ -294,8 +283,6 @@ void xmitDatagramP2PHeartbeat() */ txControl.datagramType = DATAGRAM_HEARTBEAT; - txControl.ackNumber = expectedDatagramNumber; - expectedDatagramNumber = (expectedDatagramNumber + ((datagramsExpectingAcks & (1 << txControl.datagramType)) != 0)) & 3; transmitDatagram(); } @@ -318,8 +305,6 @@ void xmitDatagramP2PAck() */ txControl.datagramType = DATAGRAM_DATA_ACK; - txControl.ackNumber = expectedAckNumber; - expectedAckNumber = (expectedAckNumber + 1) & 3; transmitDatagram(); } @@ -342,7 +327,6 @@ void xmitDatagramMpDatagram() */ txControl.datagramType = DATAGRAM_DATAGRAM; - txControl.ackNumber = 0; transmitDatagram(); } @@ -369,7 +353,6 @@ void xmitDatagramMpTrainingPing() */ txControl.datagramType = DATAGRAM_TRAINING_PING; - txControl.ackNumber = 0; transmitDatagram(); } @@ -396,7 +379,6 @@ void xmitDatagramMpTrainingAck(uint8_t * serverID) */ txControl.datagramType = DATAGRAM_TRAINING_ACK; - txControl.ackNumber = 0; transmitDatagram(); } @@ -517,7 +499,6 @@ void xmitDatagramMpRadioParameters(const uint8_t * clientID) */ txControl.datagramType = DATAGRAM_TRAINING_PARAMS; - txControl.ackNumber = 0; transmitDatagram(); } @@ -528,6 +509,7 @@ void xmitDatagramMpRadioParameters(const uint8_t * clientID) //Determine the type of datagram received PacketType rcvDatagram() { + uint8_t ackNumber; PacketType datagramType; uint8_t receivedNetID; CONTROL_U8 rxControl; @@ -698,7 +680,7 @@ PacketType rcvDatagram() //Get the control byte rxControl = *((CONTROL_U8 *)rxData++); datagramType = rxControl.datagramType; - uint8_t packetNumber = rxControl.ackNumber; + ackNumber = rxControl.ackNumber; if (settings.debugReceive) printControl(*((uint8_t *)&rxControl)); if (datagramType >= MAX_V2_DATAGRAM_TYPE) @@ -760,32 +742,32 @@ PacketType rcvDatagram() break; case DATAGRAM_DATA_ACK: - if (packetNumber != expectedAckNumber) + if (ackNumber != txAckNumber) { if (settings.debugReceive) { systemPrintTimestamp(); systemPrint("Invalid ACK number, received "); - systemPrint(packetNumber); + systemPrint(ackNumber); systemPrint(" expecting "); - systemPrintln(expectedAckNumber); + systemPrintln(txAckNumber); } badFrames++; return (DATAGRAM_BAD); } - //Increment the expected ACK number - expectedAckNumber = (expectedAckNumber + 1) & 3; + //Set the next TX ACK number + txAckNumber = (txAckNumber + 1) & 3; break; case DATAGRAM_DATA: case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (packetNumber != expectedDatagramNumber) + if (ackNumber != rmtTxAckNumber) { //Determine if this is a duplicate datagram - if (packetNumber == ((expectedDatagramNumber - 1) & 3)) + if (ackNumber == ((rmtTxAckNumber - 1) & 3)) { linkDownTimer = millis(); duplicateFrames++; @@ -797,16 +779,17 @@ PacketType rcvDatagram() { systemPrintTimestamp(); systemPrint("Invalid datagram number, received "); - systemPrint(packetNumber); + systemPrint(ackNumber); systemPrint(" expecting "); - systemPrintln(expectedDatagramNumber); + systemPrintln(rmtTxAckNumber); } badFrames++; return DATAGRAM_BAD; } //Receive this data packet and set the next expected datagram number - expectedDatagramNumber = (expectedDatagramNumber + 1) & 3; + rxAckNumber = rmtTxAckNumber; + rmtTxAckNumber = (rmtTxAckNumber + 1) & 3; break; } } @@ -858,7 +841,7 @@ PacketType rcvDatagram() if (settings.pointToPoint) { systemPrint(" (ACK #"); - systemPrint(packetNumber); + systemPrint(ackNumber); systemPrint(")"); } systemPrintln(); @@ -888,6 +871,12 @@ void transmitDatagram() txDatagramSize = endOfTxData - outgoingPacket; length = txDatagramSize - headerBytes; + //Select the ACK number + if (txControl.datagramType == DATAGRAM_DATA_ACK) + txControl.ackNumber = rxAckNumber; + else + txControl.ackNumber = txAckNumber; + //Process the packet if (settings.debugDatagrams) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bd3b6fab..b57626a6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -542,9 +542,6 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); - - if (expectedAckNumber == 0) expectedAckNumber = 3; //Reduce eAN by one to align with remote's count - else expectedAckNumber--; xmitDatagramP2PAck(); //Transmit ACK changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); @@ -1577,9 +1574,9 @@ void v2EnterLinkUp() resetHeartbeat(); //Synchronize the ACK numbers - txControl.ackNumber = 0; - expectedAckNumber = 0; - expectedDatagramNumber = 0; + rmtTxAckNumber = 0; + rxAckNumber = 0; + txAckNumber = 0; //Discard any previous data rxTail = rxHead; From fcf3859a4b9b1ba006473f9ffac65b274c97b6f7 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 17 Oct 2022 08:11:35 -1000 Subject: [PATCH 046/594] Count and display total link failures --- Firmware/LoRaSerial_Firmware/Commands.ino | 13 +++++++++++++ .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 6 ++---- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 6d5e1a81..a89ebeee 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -159,6 +159,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI14 - Display the total duplicate frames received"); systemPrintln(" ATI15 - Display the total lost TX frames"); systemPrintln(" ATI16 - Display the maximum datagram size"); + systemPrintln(" ATI17 - Display the total link failures"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(); @@ -195,6 +196,7 @@ bool commandAT(const char * commandString) systemPrintln(); break; case ('9'): //ATI9 - Display the toal datagrams sent + systemPrint("Total datagrams sent: "); systemPrintln(datagramsSent); break; default: @@ -208,26 +210,37 @@ bool commandAT(const char * commandString) default: return false; case ('0'): //ATI10 - Display the total datagrams received + systemPrint("Total datagrams received: "); systemPrintln(datagramsReceived); break; case ('1'): //ATI11 - Display the total frames received + systemPrint("Total frames sent: "); systemPrintln(framesReceived); break; case ('2'): //ATI12 - Display the total frames received + systemPrint("Total frames sent: "); systemPrintln(framesReceived); break; case ('3'): //ATI13 - Display the total bad frames received + systemPrint("Total bad frames received: "); systemPrintln(badFrames); break; case ('4'): //ATI14 - Display the total duplicate frames received + systemPrint("Total duplicate frames received: "); systemPrintln(duplicateFrames); break; case ('5'): //ATI15 - Display the total lost TX frames + systemPrint("Total lost TX frames: "); systemPrintln(lostFrames); break; case ('6'): //ATI16 - Display the maximum datagram size + systemPrint("Maximum datagram size: "); systemPrintln(maxDatagramSize); break; + case ('7'): //ATI17 - Display the total link failures + systemPrint("Total link failures: "); + systemPrintln(linkFailures); + break; } } diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 23ed9d6f..74261d6e 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -200,6 +200,7 @@ uint32_t framesReceived; //Total number of frames received uint32_t badFrames; //Total number of bad frames received uint32_t duplicateFrames; //Total number of duplicate frames received uint32_t lostFrames; //Total number of lost TX frames +uint32_t linkFailures; //Total number of link failures //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - V2 Protocol diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bd3b6fab..f71e405d 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -518,10 +518,7 @@ void updateRadioState() break; case DATAGRAM_PING: - //Acknowledge the PING - triggerEvent(TRIGGER_SEND_ACK1); - xmitDatagramP2PAck1(); - changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); + v2BreakLink(); break; case DATAGRAM_DUPLICATE: @@ -1562,6 +1559,7 @@ void changeState(RadioStates newState) void v2BreakLink() { //Break the link + linkFailures++; if (settings.printLinkUpDown) systemPrintln("--------- Link DOWN ---------"); triggerEvent(TRIGGER_RADIO_RESET); From b2ce35e74dd17e5461af1690889c974d259e4ee6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 08:11:33 -1000 Subject: [PATCH 047/594] Rename protocolVersion to radioProtocolVersion --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- Firmware/LoRaSerial_Firmware/RadioV2.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/Train.ino | 4 ++-- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index a89ebeee..1f50675c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -564,7 +564,7 @@ const COMMAND_ENTRY commands[] = {43, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, {44, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, - {45, 2, 2, 0, TYPE_U8, valInt, "protocolVersion", &settings.protocolVersion}, + {45, 2, 2, 0, TYPE_U8, valInt, "radioProtocolVersion", &settings.radioProtocolVersion}, {46, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, {47, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, {48, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 7b23bc37..de04eb85 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -412,7 +412,7 @@ void updateRadioParameters(uint8_t * rxData, bool pointToPoint) originalSettings.autoTuneFrequency = params.autoTuneFrequency; originalSettings.maxResends = params.maxResends; originalSettings.verifyRxNetID = params.verifyRxNetID; - originalSettings.protocolVersion = params.protocolVersion; + originalSettings.radioProtocolVersion = params.radioProtocolVersion; originalSettings.overheadTime = params.overheadTime; originalSettings.enableCRC16 = params.enableCRC16; originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 09e1eefb..e9428a97 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -59,7 +59,7 @@ void updateRadioState() returnToReceiving(); //Start receiving //Start the link between the radios - if (settings.protocolVersion >= 2) + if (settings.radioProtocolVersion >= 2) { //Start the V2 protocol if (settings.pointToPoint == true) diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 566cd794..4cbe14ef 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -1,7 +1,7 @@ //Select the training protocol void selectTraining(bool defaultTraining) { - if (settings.protocolVersion >= 2) + if (settings.radioProtocolVersion >= 2) { if (settings.pointToPoint) beginTrainingPointToPoint(defaultTraining); @@ -17,7 +17,7 @@ void selectTraining(bool defaultTraining) { //Handle unknown future versions systemPrint("Unknown protocol version: "); - systemPrintln(settings.protocolVersion); + systemPrintln(settings.radioProtocolVersion); while (1) petWDT(); } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index b3285ede..8cbfe5d4 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -289,7 +289,7 @@ typedef struct struct_settings { bool debugReceive = false; //Print receive processing bool debugTransmit = false; //Print transmit processing bool printTxErrors = false; //Print any transmit errors - uint8_t protocolVersion = 2; //Select the radio protocol + uint8_t radioProtocolVersion = 2; //Select the radio protocol bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds bool debugDatagrams = false; //Print the datagrams uint16_t overheadTime = 10; ////ms added to ack and datagram times before ACK timeout occurs From 9c3f9c47f2e0157d590e35ab11ed780e24ff6f77 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 08:25:24 -1000 Subject: [PATCH 048/594] Rename pointToPoint to operatingMode --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 +++-- Firmware/LoRaSerial_Firmware/Radio.ino | 4 ++-- Firmware/LoRaSerial_Firmware/RadioV2.ino | 18 +++++++++--------- Firmware/LoRaSerial_Firmware/States.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/Train.ino | 4 ++-- Firmware/LoRaSerial_Firmware/settings.h | 8 +++++++- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index a89ebeee..24915c55 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -118,7 +118,8 @@ bool commandAT(const char * commandString) selectTraining(defaultTraining); break; case ('X'): //Stop the training server - if (trainingServerRunning && (!settings.pointToPoint) && settings.trainingServer) + if (trainingServerRunning && settings.trainingServer + && (settings.operatingMode == MODE_DATAGRAM)) { endClientServerTraining(TRIGGER_TRAINING_SERVER_STOPPED); reportOK(); @@ -514,7 +515,7 @@ const COMMAND_ENTRY commands[] = {0, 0, 0, 0, TYPE_SPEED_SERIAL, valSpeedSerial, "SerialSpeed", &settings.serialSpeed}, {1, 0, 0, 0, TYPE_SPEED_AIR, valSpeedAir, "AirSpeed", &settings.airSpeed}, {2, 0, 255, 0, TYPE_U8, valInt, "netID", &settings.netID}, - {3, 0, 1, 0, TYPE_BOOL, valInt, "PointToPoint", &settings.pointToPoint}, + {3, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &settings.operatingMode}, {4, 0, 1, 0, TYPE_BOOL, valInt, "EncryptData", &settings.encryptData}, {5, 0, 0, 0, TYPE_KEY, valKey, "EncryptionKey", &settings.encryptionKey}, diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index c84c19eb..5ce8b9cd 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -190,7 +190,7 @@ void returnToReceiving() } else { - if (expectingAck && settings.pointToPoint == true) + if (expectingAck && (settings.operatingMode == MODE_POINT_TO_POINT)) { radio.implicitHeader(2); state = radio.startReceive(2); //Expect a control packet @@ -281,7 +281,7 @@ void generateHopTable() //Feed random number generator with our specific platform settings //Use settings that must be identical to have a functioning link. //For example, we do not use coding rate because two radios can communicate with different coding rate values - myRandSeed = settings.airSpeed + settings.netID + settings.pointToPoint + settings.encryptData + myRandSeed = settings.airSpeed + settings.netID + settings.operatingMode + settings.encryptData + settings.dataScrambling + (uint16_t)settings.frequencyMin + (uint16_t)settings.frequencyMax + settings.numberOfChannels + settings.frequencyHop + settings.maxDwellTime diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 7b23bc37..e0e274bc 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -63,7 +63,7 @@ void xmitDatagramP2pTrainingParams() //Initialize the radio parameters memcpy(¶ms, &originalSettings, sizeof(settings)); - params.pointToPoint = true; + params.operatingMode = MODE_POINT_TO_POINT; //Add the radio parameters memcpy(endOfTxData, ¶ms, sizeof(params)); @@ -382,7 +382,7 @@ void xmitDatagramMpTrainingAck(uint8_t * serverID) transmitDatagram(); } -void updateRadioParameters(uint8_t * rxData, bool pointToPoint) +void updateRadioParameters(uint8_t * rxData) { Settings params; @@ -392,7 +392,7 @@ void updateRadioParameters(uint8_t * rxData, bool pointToPoint) //Update the radio parameters originalSettings.airSpeed = params.airSpeed; originalSettings.netID = params.netID; - originalSettings.pointToPoint = pointToPoint; + originalSettings.operatingMode = params.operatingMode; originalSettings.encryptData = params.encryptData; memcpy(originalSettings.encryptionKey, params.encryptionKey, sizeof(originalSettings.encryptionKey)); originalSettings.dataScrambling = params.dataScrambling; @@ -472,7 +472,7 @@ void xmitDatagramMpRadioParameters(const uint8_t * clientID) //Initialize the radio parameters memcpy(¶ms, &originalSettings, sizeof(settings)); - params.pointToPoint = false; + params.operatingMode = MODE_DATAGRAM; params.trainingServer = false; //Add the destination (client) ID @@ -608,7 +608,7 @@ PacketType rcvDatagram() */ //Verify the netID if necessary - if (settings.pointToPoint || settings.verifyRxNetID) + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) { receivedNetID = *rxData++; if (settings.debugReceive) @@ -734,7 +734,7 @@ PacketType rcvDatagram() rxDataBytes -= minDatagramSize; //Verify the packet number last so that the expected datagram or ACK number can be updated - if (settings.pointToPoint) + if (settings.operatingMode == MODE_POINT_TO_POINT) { switch (datagramType) { @@ -838,7 +838,7 @@ PacketType rcvDatagram() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (settings.pointToPoint) + if (settings.operatingMode == MODE_POINT_TO_POINT) { systemPrint(" (ACK #"); systemPrint(ackNumber); @@ -894,7 +894,7 @@ void transmitDatagram() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (settings.pointToPoint) + if (settings.operatingMode == MODE_POINT_TO_POINT) { systemPrint(" (ACK #"); systemPrint(txControl.ackNumber); @@ -946,7 +946,7 @@ void transmitDatagram() } //Add the netID if necessary - if (settings.pointToPoint || settings.verifyRxNetID) + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) { *header++ = settings.netID; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 09e1eefb..c6f48141 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -62,7 +62,7 @@ void updateRadioState() if (settings.protocolVersion >= 2) { //Start the V2 protocol - if (settings.pointToPoint == true) + if (settings.operatingMode == MODE_POINT_TO_POINT) changeState(RADIO_P2P_LINK_DOWN); else changeState(RADIO_MP_STANDBY); @@ -152,7 +152,7 @@ void updateRadioState() triggerEvent(TRIGGER_TRAINING_DATA_PACKET); //Update the parameters - updateRadioParameters(rxData, true); + updateRadioParameters(rxData); endPointToPointTraining(true); changeState(RADIO_RESET); } @@ -1161,7 +1161,7 @@ void updateRadioState() memcpy(trainingPartnerID, &rxData[UNIQUE_ID_BYTES], UNIQUE_ID_BYTES); //Get the radio parameters - updateRadioParameters(&rxData[UNIQUE_ID_BYTES * 2], false); + updateRadioParameters(&rxData[UNIQUE_ID_BYTES * 2]); //Acknowledge the radio parameters xmitDatagramMpTrainingAck(&rxData[UNIQUE_ID_BYTES]); @@ -1311,7 +1311,7 @@ void selectHeaderAndTrailerBytes() headerBytes = 0; //Add the netID to the header - if (settings.pointToPoint || settings.verifyRxNetID) + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) headerBytes += 1; //Add the control byte to the header diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 566cd794..7736c19a 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -3,7 +3,7 @@ void selectTraining(bool defaultTraining) { if (settings.protocolVersion >= 2) { - if (settings.pointToPoint) + if (settings.operatingMode == MODE_POINT_TO_POINT) beginTrainingPointToPoint(defaultTraining); else { @@ -168,7 +168,7 @@ void commonTrainingInitialization() //Use common radio settings between the client and server for training settings = defaultSettings; - settings.pointToPoint = false; // 3: Disable netID checking + settings.operatingMode = MODE_DATAGRAM; // 3: Use datagrams settings.encryptData = true; // 4: Enable packet encryption settings.dataScrambling = true; // 6: Scramble the data settings.radioBroadcastPower_dbm = 14; // 7: Minimum, assume radios are near each other diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index b3285ede..a654de6a 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -220,6 +220,12 @@ typedef enum } PrinterEndpoints; PrinterEndpoints printerEndpoint = PRINT_TO_SERIAL; +//Select the operating mode +typedef enum +{ + MODE_DATAGRAM = 0, + MODE_POINT_TO_POINT, +} OPERATING_MODE; struct ControlTrailer { @@ -248,7 +254,7 @@ typedef struct struct_settings { uint32_t serialSpeed = 57600; //Default to 57600bps to match RTK Surveyor default firmware uint32_t airSpeed = 4800; //Default to ~523 bytes per second to support RTCM. Overrides spread, bandwidth, and coding uint8_t netID = 192; //Both radios must share a network ID - bool pointToPoint = true; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. + uint8_t operatingMode = MODE_POINT_TO_POINT; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. bool encryptData = true; //AES encrypt each packet uint8_t encryptionKey[AES_KEY_BYTES] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; bool dataScrambling = false; //Use IBM Data Whitening to reduce DC bias From 95a9193121f4c09b323c5436fa988aee229ea402 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 08:31:52 -1000 Subject: [PATCH 049/594] Define P2P_LINK_BREAK_MULTIPLIER --- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 09e1eefb..501679d7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -694,7 +694,7 @@ void updateRadioState() changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } } - else if ((millis() - linkDownTimer) >= (3 * settings.heartbeatTimeout)) + else if ((millis() - linkDownTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)) //Break the link v2BreakLink(); } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index b3285ede..a015900b 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -41,6 +41,8 @@ typedef enum RadioStates radioState = RADIO_RESET; +#define P2P_LINK_BREAK_MULTIPLIER 3 + typedef struct _RADIO_STATE_ENTRY { RadioStates state; From 0dc2264c2c0db8d81ade8cfccff970661f3c160f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 08:37:25 -1000 Subject: [PATCH 050/594] Add SAVE_TX_BUFFER and RESTORE_TX_BUFFER macros --- Firmware/LoRaSerial_Firmware/States.ino | 26 +++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 09e1eefb..512883a6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1,3 +1,19 @@ +#define SAVE_TX_BUFFER() \ +{ \ + memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); \ + rexmtControl = txControl; \ + rexmtLength = txDatagramSize; \ + rexmtFrameSentCount = frameSentCount; \ +} + +#define RESTORE_TX_BUFFER() \ +{ \ + memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ + txControl = rexmtControl; \ + txDatagramSize = rexmtLength; \ + frameSentCount = rexmtFrameSentCount; \ +} + void updateRadioState() { unsigned long clockOffset; @@ -815,10 +831,7 @@ void updateRadioState() //and ACK the heartbeat. Later perform the retransmission for the //datagram that was lost. petWDT(); - memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); - rexmtControl = txControl; - rexmtLength = txDatagramSize; - rexmtFrameSentCount = frameSentCount; + SAVE_TX_BUFFER(); triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); xmitDatagramP2PAck(); //Transmit ACK @@ -899,10 +912,7 @@ void updateRadioState() //Retransmit the packet if (rexmtFrameSentCount < settings.maxResends) { - memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); - txControl = rexmtControl; - txDatagramSize = rexmtLength; - frameSentCount = rexmtFrameSentCount; + RESTORE_TX_BUFFER(); if (settings.debugDatagrams) { systemPrintTimestamp(); From e66cb5445b277af59a725be055dee21c309ae3f6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 08:47:20 -1000 Subject: [PATCH 051/594] Serial.ino reorganization --- Firmware/LoRaSerial_Firmware/Serial.ino | 213 +++++++++++++----------- 1 file changed, 117 insertions(+), 96 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 2e332749..ed7abeb2 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -1,3 +1,7 @@ +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Serial RX - Data arriving at the USB or serial port +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //Return number of bytes sitting in the serial receive buffer uint16_t availableRXBytes() { @@ -5,6 +9,75 @@ uint16_t availableRXBytes() return (sizeof(serialReceiveBuffer) - rxTail + rxHead); } +//Returns true if CTS is asserted (high = host says it's ok to send data) +bool isCTS() +{ + if (pin_cts == PIN_UNDEFINED) return (true); //CTS not implmented on this board + if (settings.flowControl == false) return (true); //CTS turned off + if (digitalRead(pin_cts) == HIGH) return (true); + return (false); +} + +//If we have data to send, get the packet ready +//Return true if new data is ready to be sent +bool processWaitingSerial(bool sendNow) +{ + //Push any available data out + if (availableRXBytes() >= maxDatagramSize) + { + if (settings.debugRadio) + systemPrintln("Sending max frame"); + readyOutgoingPacket(); + return (true); + } + + //Check if we should send out a partial frame + else if (sendNow || (availableRXBytes() > 0 && (millis() - lastByteReceived_ms) >= settings.serialTimeoutBeforeSendingFrame_ms)) + { + if (settings.debugRadio) + systemPrintln("Sending partial frame"); + readyOutgoingPacket(); + return (true); + } + return (false); +} + +//Send a portion of the serialReceiveBuffer to outgoingPacket +void readyOutgoingPacket() +{ + uint16_t length; + uint16_t bytesToSend = availableRXBytes(); + if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; + + //SF6 requires an implicit header which means there is no dataLength in the header + if (settings.radioSpreadFactor == 6) + { + if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; //We are going to transmit 255 bytes no matter what + } + + packetSize = bytesToSend; + + //Determine the number of bytes to send + length = 0; + if ((rxTail + packetSize) > sizeof(serialReceiveBuffer)) + { + //Copy the first portion of the buffer + length = sizeof(serialReceiveBuffer) - rxTail; + memcpy(&outgoingPacket[headerBytes], &serialReceiveBuffer[rxTail], length); + rxTail = 0; + } + + //Copy the remaining portion of the buffer + memcpy(&outgoingPacket[headerBytes + length], &serialReceiveBuffer[rxTail], packetSize - length); + rxTail += packetSize - length; + rxTail %= sizeof(serialReceiveBuffer); + endOfTxData += packetSize; +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Serial TX - Data being sent to the USB or serial port +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //Return number of bytes sitting in the serial transmit buffer uint16_t availableTXBytes() { @@ -14,6 +87,10 @@ uint16_t availableTXBytes() return (sizeof(serialTransmitBuffer) - txTail + txHead); } +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Command RX - Remote command data received from a remote system +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //Return number of bytes sitting in the serial receive buffer uint16_t availableRXCommandBytes() { @@ -21,6 +98,10 @@ uint16_t availableRXCommandBytes() return (sizeof(commandRXBuffer) - commandRXTail + commandRXHead); } +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Command TX - Remote command data or command response data to be sent to the remote system +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //Return number of bytes sitting in the serial transmit buffer uint16_t availableTXCommandBytes() { @@ -28,6 +109,42 @@ uint16_t availableTXCommandBytes() return (sizeof(commandTXBuffer) - commandTXTail + commandTXHead); } +//Send a portion of the commandTXBuffer to outgoingPacket +void readyOutgoingCommandPacket() +{ + uint16_t length; + uint16_t bytesToSend = availableTXCommandBytes(); + if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; + + //SF6 requires an implicit header which means there is no dataLength in the header + if (settings.radioSpreadFactor == 6) + { + if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; //We are going to transmit 255 bytes no matter what + } + + packetSize = bytesToSend; + + //Determine the number of bytes to send + length = 0; + if ((commandTXTail + packetSize) > sizeof(commandTXBuffer)) + { + //Copy the first portion of the buffer + length = sizeof(commandTXBuffer) - commandTXTail; + memcpy(&outgoingPacket[headerBytes], &commandTXBuffer[commandTXTail], length); + commandTXTail = 0; + } + + //Copy the remaining portion of the buffer + memcpy(&outgoingPacket[headerBytes + length], &commandTXBuffer[commandTXTail], packetSize - length); + commandTXTail += packetSize - length; + commandTXTail %= sizeof(commandTXBuffer); + endOfTxData += packetSize; +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Move serial data through the system +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //See if there's any serial from the remote radio that needs printing //Record any characters to the receive buffer //Scan for escape characters @@ -214,99 +331,3 @@ void updateSerial() } } -//Returns true if CTS is asserted (high = host says it's ok to send data) -bool isCTS() -{ - if (pin_cts == PIN_UNDEFINED) return (true); //CTS not implmented on this board - if (settings.flowControl == false) return (true); //CTS turned off - if (digitalRead(pin_cts) == HIGH) return (true); - return (false); -} - -//If we have data to send, get the packet ready -//Return true if new data is ready to be sent -bool processWaitingSerial(bool sendNow) -{ - //Push any available data out - if (availableRXBytes() >= maxDatagramSize) - { - if (settings.debugRadio) - systemPrintln("Sending max frame"); - readyOutgoingPacket(); - return (true); - } - - //Check if we should send out a partial frame - else if (sendNow || (availableRXBytes() > 0 && (millis() - lastByteReceived_ms) >= settings.serialTimeoutBeforeSendingFrame_ms)) - { - if (settings.debugRadio) - systemPrintln("Sending partial frame"); - readyOutgoingPacket(); - return (true); - } - return (false); -} - -//Send a portion of the serialReceiveBuffer to outgoingPacket -void readyOutgoingPacket() -{ - uint16_t length; - uint16_t bytesToSend = availableRXBytes(); - if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; - - //SF6 requires an implicit header which means there is no dataLength in the header - if (settings.radioSpreadFactor == 6) - { - if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; //We are going to transmit 255 bytes no matter what - } - - packetSize = bytesToSend; - - //Determine the number of bytes to send - length = 0; - if ((rxTail + packetSize) > sizeof(serialReceiveBuffer)) - { - //Copy the first portion of the buffer - length = sizeof(serialReceiveBuffer) - rxTail; - memcpy(&outgoingPacket[headerBytes], &serialReceiveBuffer[rxTail], length); - rxTail = 0; - } - - //Copy the remaining portion of the buffer - memcpy(&outgoingPacket[headerBytes + length], &serialReceiveBuffer[rxTail], packetSize - length); - rxTail += packetSize - length; - rxTail %= sizeof(serialReceiveBuffer); - endOfTxData += packetSize; -} - -//Send a portion of the commandTXBuffer to outgoingPacket -void readyOutgoingCommandPacket() -{ - uint16_t length; - uint16_t bytesToSend = availableTXCommandBytes(); - if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; - - //SF6 requires an implicit header which means there is no dataLength in the header - if (settings.radioSpreadFactor == 6) - { - if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; //We are going to transmit 255 bytes no matter what - } - - packetSize = bytesToSend; - - //Determine the number of bytes to send - length = 0; - if ((commandTXTail + packetSize) > sizeof(commandTXBuffer)) - { - //Copy the first portion of the buffer - length = sizeof(commandTXBuffer) - commandTXTail; - memcpy(&outgoingPacket[headerBytes], &commandTXBuffer[commandTXTail], length); - commandTXTail = 0; - } - - //Copy the remaining portion of the buffer - memcpy(&outgoingPacket[headerBytes + length], &commandTXBuffer[commandTXTail], packetSize - length); - commandTXTail += packetSize - length; - commandTXTail %= sizeof(commandTXBuffer); - endOfTxData += packetSize; -} From a71ea93adf023961c57472e5abb329010a491b4a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 08:55:12 -1000 Subject: [PATCH 052/594] Move default to top of switch statements --- Firmware/LoRaSerial_Firmware/Commands.ino | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index a89ebeee..6b86f197 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -57,6 +57,8 @@ bool commandAT(const char * commandString) { switch (commandString[2]) { + default: + return false; case ('?'): //Display the command help systemPrintln("Command summary:"); systemPrintln(" AT? - Print the command summary"); @@ -131,8 +133,6 @@ bool commandAT(const char * commandString) systemFlush(); systemReset(); break; - default: - return false; } } @@ -141,6 +141,8 @@ bool commandAT(const char * commandString) { switch (commandString[3]) { + default: + return false; case ('?'): //ATI? - Display the information commands systemPrintln(" ATI0 - Show user settable parameters"); systemPrintln(" ATI1 - Show board variant"); @@ -199,8 +201,6 @@ bool commandAT(const char * commandString) systemPrint("Total datagrams sent: "); systemPrintln(datagramsSent); break; - default: - return false; } } else if ((commandString[2] == 'I') && (commandString[3] == '1') && (commandLength == 5)) From 949b9b4dc79a68338378fe2015d8817bc2ce045a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 09:03:12 -1000 Subject: [PATCH 053/594] Don't use fall through case for default training --- Firmware/LoRaSerial_Firmware/Commands.ino | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index a89ebeee..82a9a5a5 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -46,8 +46,6 @@ typedef struct bool commandAT(const char * commandString) { - bool defaultTraining = false; - //'AT' if (commandLength == 2) reportOK(); @@ -74,6 +72,10 @@ bool commandAT(const char * commandString) systemPrintln(" AT&F - Restore factory settings"); systemPrintln(" AT&W - Save current settings to NVM"); break; + case ('F'): //Enter training mode and return to factory defaults + reportOK(); + selectTraining(true); + break; case ('G'): //Generate a new netID and encryption key generateTrainingSettings(); reportOK(); @@ -106,16 +108,9 @@ bool commandAT(const char * commandString) changeState(RADIO_RESET); } break; - case ('F'): //Enter training mode and return to factory defaults - defaultTraining = true; - //Fall through - // | - // | - // | - // V case ('T'): //Enter training mode reportOK(); - selectTraining(defaultTraining); + selectTraining(false); break; case ('X'): //Stop the training server if (trainingServerRunning && (!settings.pointToPoint) && settings.trainingServer) From 3c253a3099a54d8f684ce57ef75f3191d8b3e383 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 09:10:58 -1000 Subject: [PATCH 054/594] Add define for ACK_BYTES and verify value --- .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index c471941f..693a49b5 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -56,6 +56,7 @@ const int FIRMWARE_VERSION_MINOR = 0; // the minor firmware version #define LRS_IDENTIFIER (FIRMWARE_VERSION_MAJOR * 0x10 + FIRMWARE_VERSION_MINOR) +#define ACK_BYTES 2 //Length of the ACK in bytes #define MAX_PACKET_SIZE 255 //Limited by SX127x #define AES_IV_BYTES 12 //Number of bytes for AESiv #define AES_KEY_BYTES 16 //Number of bytes in the encryption key diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 7b23bc37..d12cb892 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -289,10 +289,23 @@ void xmitDatagramP2PHeartbeat() //Create short packet of 2 control bytes - do not expect ack void xmitDatagramP2PAck() { + int ackLength; + + uint8_t * ackStart = endOfTxData; uint16_t channelTimerElapsed = millis() - timerStart; memcpy(endOfTxData, &channelTimerElapsed, sizeof(channelTimerElapsed)); endOfTxData += sizeof(channelTimerElapsed); + //Verify the ACK length + ackLength = endOfTxData - ackStart; + if (ackLength != ACK_BYTES) + { + systemPrint("ERROR - Please define ACK_BYTES = "); + systemPrintln(ackLength); + while (1) + petWDT(); + } + /* endOfTxData ---. | From 40ac2f8826aaee1e8ad1983606e3a25ad3ab4704 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 09:14:50 -1000 Subject: [PATCH 055/594] Enable CRC-16 header display with printPktData --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 7b23bc37..5a953756 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -690,7 +690,7 @@ PacketType rcvDatagram() } //Display the CRC - if (settings.enableCRC16 && (settings.printPktData || settings.debugReceive)) + if (settings.enableCRC16 && settings.debugReceive) { systemPrintTimestamp(); systemPrint(" CRC-16: 0x"); From 82bf309dba4d081e423d4f3c0b331b7d343cf37c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 09:20:56 -1000 Subject: [PATCH 056/594] Add serialBufferOutput function --- Firmware/LoRaSerial_Firmware/Serial.ino | 20 ++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 32 ++++--------------------- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index ed7abeb2..5049a95b 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -87,6 +87,26 @@ uint16_t availableTXBytes() return (sizeof(serialTransmitBuffer) - txTail + txHead); } +//Add serial data to the output buffer +void serialBufferOutput(uint8_t * data, uint16_t dataLength) +{ + int length; + + length = 0; + if ((txHead + dataLength) > sizeof(serialTransmitBuffer)) + { + //Copy the first portion of the received datagram into the buffer + length = sizeof(serialTransmitBuffer) - txHead; + memcpy(&serialTransmitBuffer[txHead], data, length); + txHead = 0; + } + + //Copy the remaining portion of the received datagram into the buffer + memcpy(&serialTransmitBuffer[txHead], &data[length], dataLength - length); + txHead += dataLength - length; + txHead %= sizeof(serialTransmitBuffer); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Command RX - Remote command data received from a remote system //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 09e1eefb..b617233b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -591,20 +591,8 @@ void updateRadioState() systemPrintln(); } - //Determine the number of bytes received - length = 0; - if ((txHead + rxDataBytes) > sizeof(serialTransmitBuffer)) - { - //Copy the first portion of the received datagram into the buffer - length = sizeof(serialTransmitBuffer) - txHead; - memcpy(&serialTransmitBuffer[txHead], rxData, length); - txHead = 0; - } - - //Copy the remaining portion of the received datagram into the buffer - memcpy(&serialTransmitBuffer[txHead], &rxData[length], rxDataBytes - length); - txHead += rxDataBytes - length; - txHead %= sizeof(serialTransmitBuffer); + //Place the data in the serial output buffer + serialBufferOutput(rxData, rxDataBytes); updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -996,20 +984,8 @@ void updateRadioState() systemPrintln(); } - //Determine the number of bytes received - length = 0; - if ((txHead + rxDataBytes) > sizeof(serialTransmitBuffer)) - { - //Copy the first portion of the received datagram into the buffer - length = sizeof(serialTransmitBuffer) - txHead; - memcpy(&serialTransmitBuffer[txHead], rxData, length); - txHead = 0; - } - - //Copy the remaining portion of the received datagram into the buffer - memcpy(&serialTransmitBuffer[txHead], &rxData[length], rxDataBytes - length); - txHead += rxDataBytes - length; - txHead %= sizeof(serialTransmitBuffer); + //Place the data in the serial output buffer + serialBufferOutput(rxData, rxDataBytes); updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; From f96b64446bffe225ec84c84e71bda1cf40a976b3 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 09:27:09 -1000 Subject: [PATCH 057/594] Add offset parameter to syncChannelTimer --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 4 ++-- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 7b23bc37..6246b5ed 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1199,12 +1199,12 @@ void stopChannelTimer() //Given the remote unit's amount of channelTimer that has elapsed, //adjust our own channelTimer interrupt to be synchronized with the remote unit -void syncChannelTimer() +void syncChannelTimer(int offset) { triggerEvent(TRIGGER_SYNC_CHANNEL); uint16_t channelTimerElapsed; - memcpy(&channelTimerElapsed, rxData, sizeof(channelTimerElapsed)); + memcpy(&channelTimerElapsed, &rxData[offset], sizeof(channelTimerElapsed)); channelTimerElapsed += ackAirTime; channelTimerElapsed += SYNC_PROCESSING_OVERHEAD; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 09e1eefb..000eb15c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -759,7 +759,7 @@ void updateRadioState() break; case DATAGRAM_DATA_ACK: - syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + syncChannelTimer(0); //Adjust freq hop ISR based on remote's remaining clock resetHeartbeat(); //Extend time before next heartbeat From ecbf2cabfe854228e326c15198f1d52d44aa0e48 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 09:35:43 -1000 Subject: [PATCH 058/594] Add bytesToSend parameter to readyOutgoingPacket --- Firmware/LoRaSerial_Firmware/Serial.ino | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index ed7abeb2..105d8c34 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -27,7 +27,7 @@ bool processWaitingSerial(bool sendNow) { if (settings.debugRadio) systemPrintln("Sending max frame"); - readyOutgoingPacket(); + readyOutgoingPacket(availableRXBytes()); return (true); } @@ -36,30 +36,21 @@ bool processWaitingSerial(bool sendNow) { if (settings.debugRadio) systemPrintln("Sending partial frame"); - readyOutgoingPacket(); + readyOutgoingPacket(availableRXBytes()); return (true); } return (false); } //Send a portion of the serialReceiveBuffer to outgoingPacket -void readyOutgoingPacket() +void readyOutgoingPacket(uint16_t bytesToSend) { uint16_t length; - uint16_t bytesToSend = availableRXBytes(); if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; - //SF6 requires an implicit header which means there is no dataLength in the header - if (settings.radioSpreadFactor == 6) - { - if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; //We are going to transmit 255 bytes no matter what - } - - packetSize = bytesToSend; - //Determine the number of bytes to send length = 0; - if ((rxTail + packetSize) > sizeof(serialReceiveBuffer)) + if ((rxTail + bytesToSend) > sizeof(serialReceiveBuffer)) { //Copy the first portion of the buffer length = sizeof(serialReceiveBuffer) - rxTail; @@ -68,10 +59,10 @@ void readyOutgoingPacket() } //Copy the remaining portion of the buffer - memcpy(&outgoingPacket[headerBytes + length], &serialReceiveBuffer[rxTail], packetSize - length); - rxTail += packetSize - length; + memcpy(&outgoingPacket[headerBytes + length], &serialReceiveBuffer[rxTail], bytesToSend - length); + rxTail += bytesToSend - length; rxTail %= sizeof(serialReceiveBuffer); - endOfTxData += packetSize; + endOfTxData += bytesToSend; } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= From ac4338fc3bf6c2397b1dfd28f2b924323905f803 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 10:19:25 -1000 Subject: [PATCH 059/594] Properly compute the ackAirTime --- Firmware/LoRaSerial_Firmware/Radio.ino | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index c84c19eb..4d68d5ea 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -126,12 +126,10 @@ void configureRadio() if (radio.setFHSSHoppingPeriod(hoppingPeriod) != RADIOLIB_ERR_NONE) success = false; - ackAirTime = calcAirTime(2); //Used for response timeout during ACK - uint16_t responseDelay = ackAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond - ackAirTime += responseDelay; - //Precalculate the ACK packet time - ackAirTime = calcAirTime(4); //We assume all ACKs have max of 4 bytes + ackAirTime = calcAirTime(headerBytes + ACK_BYTES + trailerBytes); //Used for response timeout during ACK + if (settings.radioSpreadFactor == 6) + ackAirTime = calcAirTime(MAX_PACKET_SIZE); if ((settings.debug == true) || (settings.debugRadio == true)) { From 2f871038b9fcb65d8631966cd60b47987d7ddd57 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 21 Oct 2022 07:25:15 -1000 Subject: [PATCH 060/594] SF6: Fix end of data so that the CRC gets computed correctly --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d12cb892..a02ceaf2 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -989,6 +989,7 @@ void transmitDatagram() { *header++ = length; txDatagramSize = MAX_PACKET_SIZE - trailerBytes; //We're now going to transmit a full size datagram + endOfTxData = &outgoingPacket[txDatagramSize]; if (settings.debugTransmit) { systemPrintTimestamp(); From 5e56b7e4e867110c649e79238ce897ed6e144113 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 19 Oct 2022 09:43:40 -1000 Subject: [PATCH 061/594] VC: Add basis virtual circuit support (point-to-point) CH 0 --- Firmware/LoRaSerial_Firmware/Commands.ino | 108 +++++ .../LoRaSerial_Firmware.ino | 12 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 172 ++++++- Firmware/LoRaSerial_Firmware/Serial.ino | 98 ++++ Firmware/LoRaSerial_Firmware/States.ino | 448 +++++++++++++++++- Firmware/LoRaSerial_Firmware/settings.h | 153 +++++- 6 files changed, 983 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 432e00df..d96b1e0e 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -46,6 +46,9 @@ typedef struct bool commandAT(const char * commandString) { + VIRTUAL_CIRCUIT * vc; + unsigned long timer; + //'AT' if (commandLength == 2) reportOK(); @@ -66,6 +69,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI? - Display the information commands"); systemPrintln(" ATIn - Display system information"); systemPrintln(" ATO - Exit command mode"); + systemPrintln(" ATR - VC link reset"); systemPrintln(" ATSn=xxx - Set parameter n's value to xxx"); systemPrintln(" ATSn? - Print parameter n's current value"); systemPrintln(" ATT - Enter training mode"); @@ -110,6 +114,17 @@ bool commandAT(const char * commandString) changeState(RADIO_RESET); } break; + case ('R'): //VC link reset + reportOK(); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Idle the system to break the link + //This is required on the server system which does not request an VC number assignment + timer = millis(); + while ((millis() - timer) < ((VC_LINK_BREAK_MULTIPLIER + 2) * settings.heartbeatTimeout)) + petWDT(); + } + break; case ('T'): //Enter training mode reportOK(); selectTraining(false); @@ -158,6 +173,13 @@ bool commandAT(const char * commandString) systemPrintln(" ATI15 - Display the total lost TX frames"); systemPrintln(" ATI16 - Display the maximum datagram size"); systemPrintln(" ATI17 - Display the total link failures"); + systemPrintln(" ATI18 - Display the VC frames sent"); + systemPrintln(" ATI19 - Display the VC frames received"); + systemPrintln(" ATI20 - Display the VC messages sent"); + systemPrintln(" ATI21 - Display the VC messages received"); + systemPrintln(" ATI22 - Display the VC bad length received"); + systemPrintln(" ATI23 - Display the VC link failures"); + systemPrintln(" ATI24 - Display the VC details"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(); @@ -237,6 +259,91 @@ bool commandAT(const char * commandString) systemPrint("Total link failures: "); systemPrintln(linkFailures); break; + case ('8'): //ATI18 - Display the VC frames sent + systemPrint("VC "); + systemPrint(cmdVc); + systemPrint(" frames sent: "); + systemPrintln(virtualCircuitList[cmdVc].framesSent); + break; + case ('9'): //ATI19 - Display the VC frames received + systemPrint("VC "); + systemPrint(cmdVc); + systemPrint(" frames received: "); + systemPrintln(virtualCircuitList[cmdVc].framesReceived); + break; + } + } + else if ((commandString[2] == 'I') && (commandString[3] == '2') && (commandLength == 5)) + { + switch (commandString[4]) + { + default: + return false; + case ('0'): //ATI20 - Display the VC messages sent + systemPrint("VC "); + systemPrint(cmdVc); + systemPrint(" messages sent: "); + systemPrintln(virtualCircuitList[cmdVc].messagesSent); + break; + case ('1'): //ATI21 - Display the VC messages received + systemPrint("VC "); + systemPrint(cmdVc); + systemPrint(" messages received: "); + systemPrintln(virtualCircuitList[cmdVc].messagesReceived); + break; + case ('2'): //ATI22 - Display the VC bad length received + systemPrint("VC "); + systemPrint(cmdVc); + systemPrint(" bad length received: "); + systemPrintln(virtualCircuitList[cmdVc].badLength); + break; + case ('3'): //ATI23 - Display the VC link failures + systemPrint("VC "); + systemPrint(cmdVc); + systemPrint(" link failures: "); + systemPrintln(virtualCircuitList[cmdVc].linkFailures); + break; + case ('4'): //ATI24 - Display the VC details + vc = &virtualCircuitList[cmdVc]; + systemPrint("VC "); + systemPrint(cmdVc); + systemPrint(":"); + if (!vc->valid) + systemPrintln(" Not valid!"); + else + { + petWDT(); + systemPrint(" Link: "); + systemPrintln(vc->linkUp ? "Up" : "Down"); + systemPrint(" Unique ID: "); + systemPrintUniqueID(vc->uniqueId); + + systemPrintln(" Metrics:"); + systemPrint(" Link Failures: "); + systemPrintln(vc->linkFailures); + systemPrint(" Frames Sent: "); + systemPrintln(vc->framesSent); + systemPrint(" Frames Received: "); + systemPrintln(vc->framesReceived); + systemPrint(" Messages Sent: "); + systemPrintln(vc->messagesSent); + systemPrint(" Messages Received: "); + systemPrintln(vc->messagesReceived); + systemPrint(" Bad Lengths Received: "); + systemPrintln(vc->badLength); + + systemPrintln(" ACK Management:"); + systemPrint(" Last RX ACK number: "); + systemPrintln(vc->rxAckNumber); + systemPrint(" Next RX ACK number: "); + systemPrintln(vc->rmtTxAckNumber); + systemPrint(" Last TX ACK number: "); + systemPrintln(vc->txAckNumber); + + systemPrint(" Last HEARTBEAT millis: "); + systemPrintln(vc->lastHeartbeatMillis); + } + break; } } @@ -528,6 +635,7 @@ const COMMAND_ENTRY commands[] = {15, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &settings.radioCodingRate}, {16, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &settings.radioSyncWord}, {17, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &settings.radioPreambleLength}, + {18, 0, MAX_VC, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, {19, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &settings.serialTimeoutBeforeSendingFrame_ms}, {20, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 693a49b5..0d140966 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -263,6 +263,7 @@ unsigned long rcvTimeMillis; unsigned long xmitTimeMillis; unsigned long timestampOffset; unsigned long roundTripMillis; +unsigned long vcTxHeartbeatMillis; //Transmit control uint8_t * endOfTxData; @@ -274,6 +275,17 @@ bool trainingPreviousRxInProgress = false; //Previous RX status float originalChannel; //Original channel from HOP table while training is in progress uint8_t trainingPartnerID[UNIQUE_ID_BYTES]; //Unique ID of the training partner uint8_t myUniqueId[UNIQUE_ID_BYTES]; // Unique ID of this system + +//Virtual-Circuit +int8_t cmdVc; //VC index for ATI commands only +int8_t myVc; +int8_t rxDestVc; +int8_t rxSrcVc; +uint8_t *rxVcData; +int8_t txDestVc; +unsigned long vcAckTimer; +VIRTUAL_CIRCUIT virtualCircuitList[MAX_VC]; + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 52b9ea5f..0ae9e90d 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -515,6 +515,50 @@ void xmitDatagramMpRadioParameters(const uint8_t * clientID) transmitDatagram(); } +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Virtual Circuit frames +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +void xmitVcHeartbeat(int8_t addr, uint8_t * id) +{ + uint8_t * txData; + + uint32_t currentMillis = millis(); + txData = endOfTxData; + *endOfTxData++ = 0; //Reserve for length + *endOfTxData++ = VC_BROADCAST; + *endOfTxData++ = addr; + memcpy(endOfTxData, id, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(currentMillis); + + //Set the length field + *txData = (uint8_t)(endOfTxData - txData); + + /* + endOfTxData ---. + | + V + +----------+---------+--------+----------+---------+----------+---------+----------+ + | Optional | | | | | | | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Src ID | millis | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes || n Bytes | + +----------+---------+--------+----------+---------+----------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_VC_HEARTBEAT; + txControl.ackNumber = 0; + transmitDatagram(); + + //Determine the time that it took to pass this frame to the radio + //This time is used to adjust the time offset + vcTxHeartbeatMillis = millis() - currentMillis; + + //Select a random for the next heartbeat + resetHeartbeat(); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Datagram reception //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -526,6 +570,7 @@ PacketType rcvDatagram() PacketType datagramType; uint8_t receivedNetID; CONTROL_U8 rxControl; + VIRTUAL_CIRCUIT * vc; //Save the receive time rcvTimeMillis = millis(); @@ -747,6 +792,7 @@ PacketType rcvDatagram() rxDataBytes -= minDatagramSize; //Verify the packet number last so that the expected datagram or ACK number can be updated + rxVcData = rxData; if (settings.operatingMode == MODE_POINT_TO_POINT) { switch (datagramType) @@ -807,6 +853,87 @@ PacketType rcvDatagram() } } + //Verify the Virtual-Circuit length + else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Verify that the virtual circuit header is present + if (rxDataBytes < 3) + { + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Missing VC header bytes, received only "); + systemPrint(rxDataBytes); + systemPrintln(" bytes, expecting at least 3 bytes"); + } + badFrames++; + return DATAGRAM_BAD; + } + + //Parse the virtual circuit header + rxDestVc = rxData[1]; + rxSrcVc = rxData[2]; + rxVcData = &rxData[3]; + + //Validate the source VC + vc = NULL; + if (rxSrcVc != VC_UNASSIGNED) + { + if ((uint8_t)rxSrcVc >= MAX_VC) + { + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Invalid source VC: "); + systemPrintln(rxSrcVc); + } + badFrames++; + return DATAGRAM_BAD; + } + vc = &virtualCircuitList[rxSrcVc]; + } + + //Validate the length + if (*rxData != rxDataBytes) + { + systemPrintTimestamp(); + systemPrint("Invalid VC length, received "); + systemPrint(*rxData); + systemPrint(" expecting "); + systemPrintln(rxDataBytes); + badFrames++; + if (vc) + vc->badLength++; + return DATAGRAM_BAD; + } + + //Account for this frame + if (vc) + { + vc->framesReceived++; + if (datagramType == DATAGRAM_DATA) + vc->messagesReceived++; + } + + //Display the virtual circuit header + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint(" VC Length: "); + systemPrintln(*rxData); + systemPrint(" DestAddr: "); + if (rxDestVc == VC_BROADCAST) + systemPrintln("Broadcast"); + else + systemPrintln(rxDestVc); + systemPrint(" SrcAddr: "); + if (rxSrcVc == VC_UNASSIGNED) + systemPrintln("Unassigned"); + else + systemPrintln(rxSrcVc); + } + } + /* |<-- rxDataBytes -->| | | @@ -878,6 +1005,25 @@ void transmitDatagram() uint8_t control; uint8_t * header; uint8_t length; + int8_t srcVc; + uint8_t * vcData; + uint8_t vcLength; + VIRTUAL_CIRCUIT * vc; + + //Parse the virtual circuit header + vc = NULL; + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + vcData = &outgoingPacket[headerBytes]; + vcLength = *vcData++; + txDestVc = *vcData++; + srcVc = *vcData++; + if ((uint8_t)srcVc <= MAX_VC) + { + vc = &virtualCircuitList[srcVc]; + vc->messagesSent++; + } + } //Determine the packet size datagramsSent++; @@ -998,6 +1144,24 @@ void transmitDatagram() } } + //Verify the Virtual-Circuit length + if (settings.debugTransmit && (settings.operatingMode == MODE_VIRTUAL_CIRCUIT)) + { + systemPrintTimestamp(); + systemPrint(" Length: "); + systemPrintln(vcLength); + systemPrint(" DestAddr: "); + if (txDestVc == VC_BROADCAST) + systemPrintln("Broadcast"); + else + systemPrintln(txDestVc); + systemPrint(" SrcAddr: "); + if (srcVc == VC_UNASSIGNED) + systemPrintln("Unassigned"); + else + systemPrintln(srcVc); + } + /* endOfTxData ---. | @@ -1108,7 +1272,7 @@ void transmitDatagram() //Transmit this datagram frameSentCount = 0; //This is the first time this frame is being sent - retransmitDatagram(); + retransmitDatagram(vc); } //Print the control byte value @@ -1135,7 +1299,7 @@ void printControl(uint8_t value) } //The previous transmission was not received, retransmit the datagram -void retransmitDatagram() +void retransmitDatagram(VIRTUAL_CIRCUIT * vc) { /* +----------+----------+------------+--- ... ---+----------+ @@ -1168,6 +1332,8 @@ void retransmitDatagram() if (state == RADIOLIB_ERR_NONE) { frameSentCount++; + if (vc) + vc->framesSent++; framesSent++; xmitTimeMillis = millis(); frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background @@ -1218,7 +1384,7 @@ void syncChannelTimer(int offset) triggerEvent(TRIGGER_SYNC_CHANNEL); uint16_t channelTimerElapsed; - memcpy(&channelTimerElapsed, &rxData[offset], sizeof(channelTimerElapsed)); + memcpy(&channelTimerElapsed, &rxVcData[offset], sizeof(channelTimerElapsed)); channelTimerElapsed += ackAirTime; channelTimerElapsed += SYNC_PROCESSING_OVERHEAD; diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 8076acd6..efa21d41 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -342,3 +342,101 @@ void updateSerial() } } +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Virtual-Circuit support +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Get the message length byte from the serial buffer +uint8_t vcSerialMsgGetLengthByte() +{ + //Get the length byte for the received serial message + return serialReceiveBuffer[rxTail]; +} + +//Get the destination virtual circuit byte from the serial buffer +uint8_t vcSerialMsgGetVcDest() +{ + uint16_t index; + + //Get the destination address byte + index = rxTail + 1; + if (index >= sizeof(serialReceiveBuffer)) + index -= sizeof(serialReceiveBuffer); + return serialReceiveBuffer[index]; +} + +//Determine if received serial data may be sent to the remote system +bool vcSerialMessageReceived() +{ + int8_t vcDest; + uint8_t msgLength; + + do + { + //Determine if the radio is idle + if (receiveInProcess()) + //The radio is busy, wait until it is idle + break; + + //Wait until at least one byte is available + if (!availableRXBytes()) + //No data available + break; + + //Verify that the entire message is in the serial buffer + msgLength = vcSerialMsgGetLengthByte(); + if (availableRXBytes() < msgLength) + //The entire message is not in the buffer + break; + + //Determine if the message is too large + vcDest = vcSerialMsgGetVcDest(); + if (msgLength > maxDatagramSize) + { + //Discard this message, it is too long to transmit over the radio link + rxTail += msgLength; + if (rxTail >= sizeof(serialReceiveBuffer)) + rxTail -= sizeof(serialReceiveBuffer); + + //Nothing to do for invalid addresses or the broadcast address + if ((vcDest >= MAX_VC) || (vcDest == VC_BROADCAST)) + break; + + //Break the link to this host + vcBreakLink(vcDest); + break; + } + + //Validate the destination VC + if ((vcDest < VC_BROADCAST) || (vcDest >= MAX_VC)) + { + if (settings.debugTransmit) + { + systemPrint("ERROR: Invalid vcDest "); + systemPrint(vcDest); + systemPrintln(", discarding message!"); + } + + //Discard this message + rxTail += msgLength; + if (rxTail >= sizeof(serialReceiveBuffer)) + rxTail -= sizeof(serialReceiveBuffer); + break; + } + + //If sending to ourself, just place the data in the serial output buffer + readyOutgoingPacket(msgLength); + if (vcDest == myVc) + { + serialBufferOutput(outgoingPacket, msgLength); + endOfTxData -= msgLength; + break; + } + + //Send this message + return true; + } while (0); + + //Nothing to send at this time + return false; +} diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index cbb1d54c..c3bdfa3a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -16,17 +16,21 @@ void updateRadioState() { + int8_t addressByte; unsigned long clockOffset; unsigned long currentMillis; unsigned long deltaMillis; uint8_t * header = outgoingPacket; bool heartbeatTimeout; + int index; uint16_t length; uint8_t radioSeed; static uint8_t rexmtBuffer[MAX_PACKET_SIZE]; static CONTROL_U8 rexmtControl; static uint8_t rexmtLength; static uint8_t rexmtFrameSentCount; + static uint8_t rexmtTxDestVc; + VIRTUAL_CIRCUIT * vc; switch (radioState) { @@ -80,6 +84,19 @@ void updateRadioState() //Start the V2 protocol if (settings.operatingMode == MODE_POINT_TO_POINT) changeState(RADIO_P2P_LINK_DOWN); + else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + if (settings.trainingServer) + //Reserve the server's address (0) + myVc = vcIdToAddressByte(VC_SERVER, myUniqueId); + else + //Unknown client address + myVc = VC_UNASSIGNED; + + //Start sending heartbeats + xmitVcHeartbeat(myVc, myUniqueId); + changeState(RADIO_VC_WAIT_TX_DONE); + } else changeState(RADIO_MP_STANDBY); } @@ -181,7 +198,7 @@ void updateRadioState() if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) { triggerEvent(TRIGGER_TRAINING_NO_ACK); - retransmitDatagram(); + retransmitDatagram(NULL); lostFrames++; changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); } @@ -870,7 +887,7 @@ void updateRadioState() } if (receiveInProcess() == false) { - retransmitDatagram(); + retransmitDatagram(NULL); lostFrames++; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } @@ -926,7 +943,7 @@ void updateRadioState() break; } } - retransmitDatagram(); + retransmitDatagram(NULL); lostFrames++; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } @@ -1287,9 +1304,288 @@ void updateRadioState() changeState(RADIO_MP_WAIT_FOR_TRAINING_PING); } break; + + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //V2 - Virtual Circuit States + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + + /* + RADIO_RESET + | + V + +<-------------------------. + | | + | Send VC_HEARTBEAT | + | | + V | + RADIO_VC_WAIT_TX_DONE | + | | + V | Heartbeat timeout + .-----> RADIO_VC_WAIT_RECEIVE ---------------' + | | + | | Receive serial data + | | Send DATA + | | + | V + | Receive ACK +<----------------+<----------------------. + | | ^ | + | V | | Send ACK + | RADIO_VC_WAIT_TC_DONE_ACK | Send VC_HEARTBEAT | + | | | | Receive DATA + | V | Heartbeat timeout | + '-------- RADIO_VC_WAIT_ACK --------+-----------------------' + + + */ + + case RADIO_VC_WAIT_TX_DONE: + //If dio0ISR has fired, we are done transmitting + if (transactionComplete == true) + { + transactionComplete = false; + + //Indicate that the receive is complete + triggerEvent(TRIGGER_VC_TX_DONE); + + //Start the receive operation + returnToReceiving(); + + //Set the next state + changeState(RADIO_VC_WAIT_RECEIVE); + } + break; + + case RADIO_VC_WAIT_RECEIVE: + //If dio0ISR has fired, a packet has arrived + currentMillis = millis(); + if (transactionComplete == true) + { + transactionComplete = false; //Reset ISR flag + trainingPreviousRxInProgress = false; + + //Decode the received datagram + PacketType packetType = rcvDatagram(); + + //Process the received datagram + switch (packetType) + { + default: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_VC_HEARTBEAT: + vcReceiveHeartbeat(RADIO_VC_WAIT_TX_DONE, millis() - currentMillis); + break; + + case DATAGRAM_DATA: + //Move the data into the serial output buffer + serialBufferOutput(rxData, rxDataBytes); + + //Acknowledge the data frame + *endOfTxData++ = VC_HEADER_BYTES + ACK_BYTES; + *endOfTxData++ = rxSrcVc; + *endOfTxData++ = myVc; + xmitDatagramP2PAck(); + changeState(RADIO_VC_WAIT_TX_DONE); + break; + + case DATAGRAM_DATA_ACK: + vcAckTimer = 0; + returnToReceiving(); + break; + } + } + + //Transmit a HEARTBEAT if necessary + else if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime) + && (receiveInProcess() == false)) + { + //Send another heartbeat + xmitVcHeartbeat(myVc, myUniqueId); + changeState(RADIO_VC_WAIT_TX_DONE); + } + + //Check for data to send + else if ((receiveInProcess() == false) && (vcSerialMessageReceived())) + { + //Transmit the packet + triggerEvent(TRIGGER_VC_TX_DATA); + xmitDatagramP2PData(); + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; + + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + changeState(RADIO_VC_WAIT_TX_DONE_ACK); + } + + //Check for link timeout + else + { + for (index = 0; index < MAX_VC; index++) + { + //Don't timeout the connection to myself + if (index == myVc) + continue; + + //Determine if the link has timed out + vc = &virtualCircuitList[index]; + if (vc->linkUp && ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) + vcBreakLink(index); + } + } + break; + + case RADIO_VC_WAIT_TX_DONE_ACK: + //If dio0ISR has fired, we are done transmitting + if (transactionComplete == true) + { + transactionComplete = false; + + //Indicate that the receive is complete + triggerEvent(TRIGGER_VC_TX_DONE); + + //Start the receive operation + returnToReceiving(); + + //Set the next state + changeState(RADIO_VC_WAIT_ACK); + } + break; + + case RADIO_VC_WAIT_ACK: + //If dio0ISR has fired, a packet has arrived + currentMillis = millis(); + if (transactionComplete == true) + { + transactionComplete = false; //Reset ISR flag + trainingPreviousRxInProgress = false; + + //Decode the received datagram + PacketType packetType = rcvDatagram(); + + //Process the received datagram + switch (packetType) + { + default: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_VC_HEARTBEAT: + vcReceiveHeartbeat(RADIO_VC_WAIT_TX_DONE_ACK, millis() - currentMillis); + break; + + case DATAGRAM_DATA: + //Move the data into the serial output buffer + serialBufferOutput(rxData, rxDataBytes); + + //Acknowledge the data frame + *endOfTxData++ = VC_HEADER_BYTES + ACK_BYTES; + *endOfTxData++ = rxSrcVc; + *endOfTxData++ = myVc; + xmitDatagramP2PAck(); + changeState(RADIO_VC_WAIT_TX_DONE_ACK); + break; + + case DATAGRAM_DATA_ACK: + vcAckTimer = 0; + returnToReceiving(); + changeState(RADIO_VC_WAIT_RECEIVE); + break; + } + } + + //Transmit a HEARTBEAT if necessary + else if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime) + && (receiveInProcess() == false)) + { + //Send another heartbeat + xmitVcHeartbeat(myVc, myUniqueId); + changeState(RADIO_VC_WAIT_TX_DONE_ACK); + } + + //Check for retransmit needed + else if (vcAckTimer && ((currentMillis - vcAckTimer) >= (frameAirTime + ackAirTime + settings.overheadTime))) + { + //Determine if another retransmit is allowed + txDestVc = rexmtTxDestVc; + if (rexmtFrameSentCount < settings.maxResends) + { + rexmtFrameSentCount++; + + //Restore the message for retransmission + RESTORE_TX_BUFFER(); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("TX: Retransmit "); + systemPrint(frameSentCount); + systemPrint(", "); + systemPrint(v2DatagramType[txControl.datagramType]); + switch (txControl.datagramType) + { + default: + systemPrintln(); + break; + + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + case DATAGRAM_HEARTBEAT: + systemPrint(" (ACK #"); + systemPrint(txControl.ackNumber); + systemPrint(")"); + systemPrintln(); + break; + } + } + + //Retransmit the packet + retransmitDatagram(((uint8_t)txDestVc <= MAX_VC) ? &virtualCircuitList[txDestVc] : NULL); + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; + lostFrames++; + changeState(RADIO_VC_WAIT_TX_DONE_ACK); + } + else + { + //Failed to reach the other system, break the link + vcAckTimer = 0; + vcBreakLink(txDestVc); + } + } + + //Check for link timeout + else + { + for (index = 0; index < MAX_VC; index++) + { + //Don't timeout the connection to myself + if (index == myVc) + continue; + + //Determine if the link has timed out + vc = &virtualCircuitList[index]; + if (vc->linkUp && ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) + { + vcBreakLink(index); + if (index == rexmtTxDestVc) + changeState(RADIO_VC_WAIT_RECEIVE); + } + } + } + break; } } +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //Compute the number of header and trailer bytes void selectHeaderAndTrailerBytes() { @@ -1574,3 +1870,149 @@ void v2EnterLinkUp() if (settings.printLinkUpDown) systemPrintln("========== Link UP =========="); } + +//Break the virtual-circuit link +void vcBreakLink(int8_t vcIndex) +{ + VIRTUAL_CIRCUIT * vc; + + //Get the virtual circuit data structure + if ((vcIndex >= 0) && (vcIndex != myVc) && ( vcIndex < MAX_VC)) + { + //Account for the link failure + vc = &virtualCircuitList[vcIndex]; + vc->linkFailures++; + vc->linkUp = false; + } + linkFailures++; + + //Display the link failure + if (settings.printLinkUpDown) + { + systemPrint("--------- Link "); + systemPrint(vcIndex); + systemPrintln(" Down ---------"); + } +} + +int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) +{ + int8_t index; + VIRTUAL_CIRCUIT * vc; + + //Determine if the address is already in the list + for (index = 0; index < MAX_VC; index++) + { + //Verify that an address is present + vc = &virtualCircuitList[index]; + if (!vc->valid) + continue; + + //Compare the unique ID values + if (memcmp(vc->uniqueId, id, UNIQUE_ID_BYTES) == 0) + { + if (!vc->linkUp) + { + if (settings.printLinkUpDown) + { + systemPrint("========== Link "); + systemPrint(srcAddr); + systemPrintln(" up =========="); + } + } + vc->linkUp = true; + vc->lastHeartbeatMillis = millis(); + return index; + } + } + + //The unique ID is not in the list + //Fill in clients that were already running + index = srcAddr; + vc = &virtualCircuitList[index]; + + //Only the server can assign the address bytes + if ((srcAddr == VC_UNASSIGNED) && (!settings.trainingServer)) + return -1; + + //Assign an address if necessary + if (srcAddr == VC_UNASSIGNED) + { + //Unknown client ID + //Determine if there is a free address + for (index = 0; index < MAX_VC; index++) + { + vc = &virtualCircuitList[index]; + if (!virtualCircuitList[index].valid) + break; + } + if (index >= MAX_VC) + { + systemPrintln("ERROR: Too many clients, no free addresses!\n"); + return -2; + } + } + + //Check for an address conflict + if (vc->valid) + { + systemPrint("ERROR: Unknown ID with pre-assigned conflicting address: "); + systemPrintln(srcAddr); + systemPrint("Received ID: "); + for (int i = 0; i < UNIQUE_ID_BYTES; i++) + systemPrint(id[i], HEX); + systemPrintln(); + systemPrint("Assigned ID: "); + for (int i = 0; i < UNIQUE_ID_BYTES; i++) + systemPrint(vc->uniqueId[i], HEX); + systemPrintln(); + return -3; + } + + //Mark this link as up + vc->valid = true; + vc->linkUp = true; + vc->lastHeartbeatMillis = millis(); + memcpy(&vc->uniqueId, id, UNIQUE_ID_BYTES); + if (settings.printLinkUpDown) + { + systemPrint("========== Link "); + systemPrint(index); + systemPrintln(" up =========="); + } + + //Returned the assigned address + return index; +} + +void vcReceiveHeartbeat(RadioStates nextState, uint32_t rxMillis) +{ + uint32_t deltaMillis; + int vcSrc; + + //Update the timestamp offset + if (rxSrcVc == VC_SERVER) + { + //Assume client and server are running the same level of debugging, + //then the delay from reading the millisecond value on the server should + //get offset by the transmit setup time and the receive overhead time. + memcpy(×tampOffset, &rxVcData[UNIQUE_ID_BYTES], sizeof(timestampOffset)); + timestampOffset += vcTxHeartbeatMillis + rxMillis - millis(); + } + + //Save our address + if ((myVc == VC_UNASSIGNED) && (memcmp(myUniqueId, rxVcData, UNIQUE_ID_BYTES) == 0)) + myVc = rxSrcVc; + + //Translate the unique ID into an address byte + vcSrc = vcIdToAddressByte(rxSrcVc, rxVcData); + + if (settings.trainingServer && (rxSrcVc == VC_UNASSIGNED) && (vcSrc >= 0)) + { + //Assign the address to the client + xmitVcHeartbeat(vcSrc, rxVcData); + changeState(nextState); + } + else + returnToReceiving(); +} diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 510aaed5..9ea4b0e9 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -36,6 +36,12 @@ typedef enum RADIO_MP_WAIT_FOR_TRAINING_PING, RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, + //Virtual-Circuit states + RADIO_VC_WAIT_TX_DONE, + RADIO_VC_WAIT_RECEIVE, + RADIO_VC_WAIT_TX_DONE_ACK, + RADIO_VC_WAIT_ACK, + RADIO_MAX_STATE, } RadioStates; @@ -93,6 +99,12 @@ const RADIO_STATE_ENTRY radioStateTable[] = // State RX Name Description {RADIO_MP_WAIT_FOR_TRAINING_PING, 1, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //20 {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, 0, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //21 + + //V2 - Virtual circuit states + {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "V2 VC: Wait for TX done"}, //22 + {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "V2 VC: Wait for receive"}, //23 + {RADIO_VC_WAIT_TX_DONE_ACK, 0, "VC_WAIT_TX_DONE_ACK", "V2 VC: Wait for TX done then ACK"}, //24 + {RADIO_VC_WAIT_ACK, 1, "VC_WAIT_ACK", "V2 VC: Wait for ACK"}, //25 }; //Possible types of packets received @@ -123,6 +135,9 @@ typedef enum DATAGRAM_TRAINING_PARAMS, //12 DATAGRAM_TRAINING_ACK, //13 + //V2: Virtual-Circuit (VC) exchange + DATAGRAM_VC_HEARTBEAT, //14 + //Add new V2 datagram types before this line MAX_V2_DATAGRAM_TYPE, @@ -143,10 +158,141 @@ const char * const v2DatagramType[] = "DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", // 10 "DATAGRAM", - // 11 12 13 - "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK" + // 11 12 13 + "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK", + // 14 + "VC_HEARTBEAT", }; +typedef struct _VIRTUAL_CIRCUIT +{ + uint8_t uniqueId[UNIQUE_ID_BYTES]; + unsigned long lastHeartbeatMillis; + + //Link quality metrics + uint32_t framesSent; //Total number of frames sent + uint32_t framesReceived; //Total number of frames received + uint32_t messagesSent; //Total number of messages sent + uint32_t messagesReceived; //Total number of messages received + uint32_t badLength; //Total number of bad lengths received + uint32_t linkFailures; //Total number of link failures + + //Link management + bool valid; //Unique ID is valid + bool linkUp; //Link is up, received a recent HEARTBEAT datagram + + /* ACK number management + + System A System B + + txAckNumber + | + V + Tx DATA Frame -----------------------> Rx DATA Frame + | + V + AckNumber == rmtTxAckNumber + | + | yes + V + rxAckNumber = rmtTxAckNumber++ + | + V + Rx DATA_Ack Frame <--------------------- Tx DATA_ACK Frame + | + V + ackNumber == txAckNumber + | + | yes + V + txAckNumber++ + */ + + uint8_t rmtTxAckNumber; //Next expected ACK # from remote system in DATA frame, + //incremented upon match to received DATA frame ACK number, + //indicates frame was received and processed + //Duplicate frame if received ACK # == (rmtTxAckNumber -1) + uint8_t rxAckNumber; //Received ACK # of the most recent acknowledged DATA frame, + //does not get incremented, used to ACK the data frame + uint8_t txAckNumber; //# of next ACK to be sent by the local system in DATA frame, + //incremented when successfully acknowledged via DATA_ACK frame +} VIRTUAL_CIRCUIT; + +typedef struct _VC_MESSAGE_HEADER +{ + uint8_t length; //Length in bytes of the VC message + int8_t destVc; //Destination VC + int8_t srcVc; //Source VC +} VC_MESSAGE_HEADER; + +#define VC_HEADER_BYTES (sizeof(VC_MESSAGE_HEADER)) //Length of the VC header in bytes + +//Virtual-Circuit source and destination index values +#define MAX_VC 8 +#define VC_SERVER 0 +#define VC_BROADCAST -1 +#define VC_UNASSIGNED -2 + +//Source and destinations reserved for the local host +#define VC_LINK_RESET -3 //Force link reset +#define VC_LINK_STATUS -4 //Asynchronous link status output +#define VC_COMMAND -5 //Command input and command response +#define VC_DEBUG -6 //Debug input and output + +/* +Host Interaction using Virtual-Circuits + + Host A LoRa A LoRa B Host B + +All output goes to serial . + . + +++ ----> . + <---- OK + . + . + . + <---- Command responses + <---- Debug message + + Mode: VC ----> + <---- OK + + (VC debug) <---- Debug message + + CMD: AT&W ----> + (VC command) <---- OK + + CMD: LINK_RESET ----> + HEARTBEAT -2 ----> + (VC status) <---- Link A up Link A Up ----> (link status) + + (VC debug) <---- Debug message + + <---- HEARTBEAT B + (VC status) <---- Link B Up + + (VC debug) <---- Debug message + + MSG: Data for B ----> + Data for B ----> + <---- ACK + Data for B ----> MSG: Data for B + <---- MSG: Resp for A + <---- Resp for A + ACK ----> + MSG: Resp for A <---- Resp for A + +*/ + +//Field offsets in the VC HEARTBEAT frame +#define VC_HB_UNIQUE_ID 0 +#define VC_HB_MILLIS (VC_HB_UNIQUE_ID + UNIQUE_ID_BYTES) +#define VC_HB_CHANNEL_TIMER (VC_HB_MILLIS + sizeof(uint32_t)) +#define VC_HB_CHANNEL (VC_HB_CHANNEL_TIMER + sizeof(uint16_t)) +#define VC_HB_END (VC_HB_CHANNEL + sizeof(uint8_t)) + +#define VC_LINK_BREAK_MULTIPLIER 3 //Number of missing HEARTBEAT timeouts + //Train button states typedef enum { @@ -212,6 +358,8 @@ enum TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE, TRIGGER_TRAINING_SERVER_RX_ACK, TRIGGER_TRAINING_SERVER_STOPPED, + TRIGGER_VC_TX_DONE, + TRIGGER_VC_TX_DATA, }; //Control where to print command output @@ -227,6 +375,7 @@ typedef enum { MODE_DATAGRAM = 0, MODE_POINT_TO_POINT, + MODE_VIRTUAL_CIRCUIT, } OPERATING_MODE; struct ControlTrailer From efc3f645c7c5ad36b5baff45bd6130d32287adc1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 14 Oct 2022 16:16:30 -1000 Subject: [PATCH 062/594] VC: Add basis virtual circuit support (P2P, VC0<->VC1) Freq 0 Testing with the following scripts: //Virtual-Circuit - Server, noHOP, netId, CRC, Debug +++ at&f ats3=2 ats51=1 ats11=0 ats37=1 ats49=1 ats7=16 ats29=1 ats30=1 ats31=1 ats35=1 ats36=1 ats42=1 ats43=1 ats46=1 ats48=300 ats57=1 at&w atz //Virtual-Circuit - Client, noHOP, netId, CRC, No Debug +++ at&f ats3=2 ats51=0 ats11=0 ats37=1 ats49=1 ats7=16 ats29=1 ats30=1 ats31=1 ats35=1 ats36=1 ats42=1 ats43=1 ats46=1 ats48=300 ats57=1 at&w atz --- .gitignore | 41 ++-- Firmware/Tools/Client.c | 26 +++ Firmware/Tools/Command_Validation_Script.txt | 13 +- Firmware/Tools/RadioV2.c | 121 ++++++++++ .../Results/Command_Validation_Results.txt | 66 ++++-- Firmware/Tools/Server.c | 24 ++ Firmware/Tools/Sockets.c | 56 +++++ Firmware/Tools/States.c | 143 ++++++++++++ Firmware/Tools/System.c | 194 +++++++++++++++ Firmware/Tools/Terminal.c | 102 ++++++++ Firmware/Tools/VcServerTest.c | 220 ++++++++++++++++++ Firmware/Tools/makefile | 71 ++++++ Firmware/Tools/settings.h | 146 ++++++++++++ 13 files changed, 1189 insertions(+), 34 deletions(-) create mode 100644 Firmware/Tools/Client.c create mode 100644 Firmware/Tools/RadioV2.c create mode 100644 Firmware/Tools/Server.c create mode 100644 Firmware/Tools/Sockets.c create mode 100644 Firmware/Tools/States.c create mode 100644 Firmware/Tools/System.c create mode 100644 Firmware/Tools/Terminal.c create mode 100644 Firmware/Tools/VcServerTest.c create mode 100644 Firmware/Tools/makefile create mode 100644 Firmware/Tools/settings.h diff --git a/.gitignore b/.gitignore index e110aaf1..99e427e8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,16 +27,31 @@ $RECYCLE.BIN/ # OSX # ========================= -.DS_Store -.AppleDouble -.LSOverride - -# Icon must ends with two \r. -Icon - -# Thumbnails -._* - -# Files that might appear on external disk -.Spotlight-V100 -.Trashes +.DS_Store +.AppleDouble +.LSOverride + +# Icon must ends with two \r. +Icon + + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Linux +# ========================= + +# Object files +*.o + +# Archives (Libraries) +*.a + +# Executables +Firmware/Tools/Client +Firmware/Tools/Server +Firmware/Tools/VcServerTest diff --git a/Firmware/Tools/Client.c b/Firmware/Tools/Client.c new file mode 100644 index 00000000..0d54a2a4 --- /dev/null +++ b/Firmware/Tools/Client.c @@ -0,0 +1,26 @@ +#include "settings.h" + +int +main ( + int argc, + char ** argv +) +{ + int status; + + if (argc == 2) + //Open the terminal + status = openTty(argv[1]); + else + //Open the socket + status = openSocket(); + if (status) + return status; + + //Set the ID + myUniqueId[0] = 1; + + //Implement the protocol + defaultSettings(&settings); + return processData(); +} diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 4e571f7b..32eb84a6 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -25,6 +25,14 @@ ati14 ati15 ati16 ati17 +ati18 +ati19 +ati20 +ati21 +ati22 +ati23 +ati24 +ati25 ati0 ats28=0 @@ -114,9 +122,10 @@ ats2=255 ats2=192 ats2? -# PointToPoint +# OperatingMode ats3=0 ats3=2 +ats3=3 ats3=1 ats3? @@ -432,7 +441,7 @@ ats44=2 ats44=0 ats44? -# protocolVersion +# radioProtocolVersion ats45=0 ats45=1 ats45=2 diff --git a/Firmware/Tools/RadioV2.c b/Firmware/Tools/RadioV2.c new file mode 100644 index 00000000..a7e475a2 --- /dev/null +++ b/Firmware/Tools/RadioV2.c @@ -0,0 +1,121 @@ +#include "settings.h" + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Constants +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +const char * frameTypeTable[] = +{ + "VC_HEARTBEAT", + "ADDRESS_BYTE", +}; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Frames +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void xmitVcHeartbeat(int8_t addr, uint8_t * id) +{ + txData = txBuffer; + *txData++ = FRAME_VC_HEARTBEAT; + *txData++ = 0; //Reserve for length + *txData++ = ADDR_BROADCAST; + *txData++ = addr; + memcpy(txData, id, UNIQUE_ID_BYTES); + txData += UNIQUE_ID_BYTES; + transmitDatagram((const struct sockaddr *)&broadcastAddr, sizeof(broadcastAddr)); +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Datagram Receive +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +FRAME_TYPE rcvDatagram() +{ + FRAME_TYPE frameType; + uint8_t length; + + //Receive a datagram from the client + if (usingTerminal) + rxBytes = readTty(rxBuffer, sizeof(rxBuffer)); + else + { + remoteAddrLength = sizeof(remoteAddr); + memset(&remoteAddr, 0, remoteAddrLength); + rxBytes = recvfrom(tty, rxBuffer, sizeof(rxBuffer), MSG_WAITALL, + (struct sockaddr *) &remoteAddr, &remoteAddrLength); + if (rxBytes < 0) + { + perror("Failed call to recvfrom!"); + exit(rxBytes); + } + } + + //Remove the header + rxData = rxBuffer; + frameType = (FRAME_TYPE)*rxData++; + length = rxData[0]; + destAddr = rxData[1]; + srcAddr = rxData[2]; + + //Display the received data + printf("Received Frame: %d --> %d\n", srcAddr, destAddr); + printf("Header:\n"); + printf(" FrameType: %s\n", frameTypeTable[frameType]); + printf(" Length: %d\n", length); + printf(" Destination Address: %d\n", destAddr); + printf(" Source Address: %d\n", srcAddr); + dumpBuffer(rxBuffer, rxBytes); + rxBytes -= 1; + + //Return the frame type + return frameType; +} + +void returnToReceiving() +{ +} + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Datagram Transmit +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void transmitDatagram(const struct sockaddr * addr, int addrLen) +{ + int bytesSent; + uint8_t * data; + FRAME_TYPE frameType; + uint8_t length; + + //Get the buffer length + txBytes = txData - txBuffer; + length = txBytes - 1; + txBuffer[1] = length; + + //Decode the frame + data = txBuffer; + frameType = (FRAME_TYPE)*data++; + length = *data++; + destAddr = *data++; + srcAddr = *data++; + + //Display this datagram + printf("Transmitting Frame: %d --> %d\n", srcAddr, destAddr); + printf("Header:\n"); + printf(" FrameType: %s\n", frameTypeTable[frameType]); + printf(" Length: %d\n", length); + printf(" Destination Address: %d\n", destAddr); + printf(" Source Address: %d\n", srcAddr); + dumpBuffer(txBuffer, txBytes); + + //Send this datagram + if (usingTerminal) + bytesSent = write(tty, (const void *)txBuffer, txBytes); + else + bytesSent = sendto(tty, (const char *)txBuffer, txBytes, MSG_CONFIRM, + addr, addrLen); + if (bytesSent < 0) + perror("Failed to send datagram!"); + else + datagramTimer = millis(); +} diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index c16509bb..d3878270 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -39,6 +39,14 @@ ati? ATI14 - Display the total duplicate frames received ATI15 - Display the total lost TX frames ATI16 - Display the maximum datagram size + ATI17 - Display the total link failures + ATI18 - Display the VC frames sent + ATI19 - Display the VC frames received + ATI20 - Display the VC messages sent + ATI21 - Display the VC messages received + ATI22 - Display the VC bad length received + ATI23 - Display the VC link failures + ATI24 - Display the VC details # Allow leading white space (space, tab, cr, lf) ERROR ati @@ -51,7 +59,7 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati3 -157 ati4 -42 +218 ati5 506 ati6 @@ -61,28 +69,44 @@ ati7 ati8 E5D60B0B4A34555020312E34212815FF ati9 -54 +Total datagrams sent: 24 ati10 -0 +Total datagrams received: 0 ati11 -0 +Total frames sent: 0 ati12 -0 +Total frames sent: 0 ati13 -0 +Total bad frames received: 0 ati14 -0 +Total duplicate frames received: 0 ati15 -0 +Total lost TX frames: 0 ati16 -253 +Maximum datagram size: 253 ati17 +Total link failures: 0 +ati18 +VC 0 frames sent: 0 +ati19 +VC 0 frames received: 0 +ati20 +VC 0 messages sent: 0 +ati21 +VC 0 messages received: 0 +ati22 +VC 0 bad length received: 0 +ati23 +VC 0 link failures: 0 +ati24 +VC 0: Not valid! +ati25 ERROR ati0 ATS0:SerialSpeed=57600 ATS1:AirSpeed=4800 ATS2:netID=192 -ATS3:PointToPoint=1 +ATS3:OperatingMode=1 ATS4:EncryptData=1 ATS5:EncryptionKey=37782141A665734E4475672AE6308308 ATS6:DataScrambling=0 @@ -97,6 +121,7 @@ ATS14:SpreadFactor=9 ATS15:CodingRate=8 ATS16:SyncWord=18 ATS17:PreambleLength=8 +ATS18:CmdVC=0 ATS19:FrameTimeout=50 ATS20:Debug=0 ATS21:Echo=0 @@ -123,7 +148,7 @@ ATS41:TriggerEnable2: 63-32=-1 ATS42:DebugReceive=0 ATS43:DebugTransmit=0 ATS44:PrintTxErrors=0 -ATS45:protocolVersion=2 +ATS45:radioProtocolVersion=2 ATS46:PrintTimestamp=0 ATS47:DebugDatagrams=0 ATS48:OverHeadtime=10 @@ -302,11 +327,13 @@ ats2=192 OK ats2? 192 -# PointToPoint +# OperatingMode ERROR ats3=0 OK ats3=2 +OK +ats3=3 ERROR ats3=1 OK @@ -586,9 +613,9 @@ ats17? # FrameSize ERROR ats18=0 -ERROR +OK ats18? -ERROR +0 # FrameTimeout ERROR ats19=9 @@ -893,19 +920,19 @@ PrintTxErrors=0 OK ats44? PrintTxErrors=0 -# protocolVersion +# radioProtocolVersion ERROR ats45=0 ERROR ats45=1 ERROR ats45=2 -protocolVersion=2 +radioProtocolVersion=2 OK ats45=3 ERROR ats45? -protocolVersion=2 +radioProtocolVersion=2 # PrintTimestamp ERROR ats46=1 @@ -1065,6 +1092,7 @@ ATS1:AirSpeed=4800 ATS24:AutoTune=0 ATS13:Bandwidth=500.00 ATS52:ClientRetryInterval=2 +ATS18:CmdVC=0 ATS15:CodingRate=8 ATS53:CopyDebug=0 ATS54:CopySerial=0 @@ -1094,8 +1122,8 @@ ATS12:MaxDwellTime=400 ATS26:MaxResends=2 ATS2:netID=192 ATS10:NumberOfChannels=50 +ATS3:OperatingMode=1 ATS48:OverHeadtime=0 -ATS3:PointToPoint=1 ATS17:PreambleLength=8 ATS29:PrintFrequency=0 ATS57:PrintLinkUpDown=0 @@ -1104,7 +1132,7 @@ ATS36:PrintPktData=0 ATS35:PrintRfData=0 ATS46:PrintTimestamp=0 ATS44:PrintTxErrors=0 -ATS45:protocolVersion=2 +ATS45:radioProtocolVersion=2 ATS0:SerialSpeed=57600 ATS27:SortParametersByName=1 ATS14:SpreadFactor=9 diff --git a/Firmware/Tools/Server.c b/Firmware/Tools/Server.c new file mode 100644 index 00000000..8a7e69f7 --- /dev/null +++ b/Firmware/Tools/Server.c @@ -0,0 +1,24 @@ +#include "settings.h" + +int +main ( + int argc, + char ** argv +) +{ + int status; + + if (argc == 2) + //Open the terminal + status = openTty(argv[1]); + else + //Open the socket + status = openSocket(); + if (status) + return status; + + //Implement the protocol + defaultSettings(&settings); + settings.trainingServer = true; + return processData(); +} diff --git a/Firmware/Tools/Sockets.c b/Firmware/Tools/Sockets.c new file mode 100644 index 00000000..f79f2a94 --- /dev/null +++ b/Firmware/Tools/Sockets.c @@ -0,0 +1,56 @@ +#include "settings.h" + +int openSocket() +{ + const int broadcastEnable = 1; + const int non_blocking = 1; + int status; + + //Open the socket + tty = socket(AF_INET, SOCK_DGRAM, 0); + if (tty < 0) + { + status = errno; + perror("Failed to open the socket!"); + return status; + } + + //Set the broadcast IPv4 address + memset(&broadcastAddr, 0, sizeof(broadcastAddr)); + broadcastAddr.sin_family = AF_INET; + broadcastAddr.sin_addr.s_addr = BROADCAST_IPV4_ADDRESS; + broadcastAddr.sin_port = htons(PORT); + + //Enable reception via any IPv4 address + memset(&localAddr, 0, sizeof(localAddr)); + localAddr.sin_family = AF_INET; + localAddr.sin_addr.s_addr = INADDR_ANY; + localAddr.sin_port = htons(PORT); + + // Bind the socket with the server address + status = bind(tty, (const struct sockaddr *)&localAddr, sizeof(localAddr)); + if (status < 0 ) + { + perror("Failed to bind the socket!"); + return status; + } + + //Support broadcasting + status = setsockopt (tty, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable)); + if (status < 0) + { + perror("setsockopt failed"); + close(tty); + return status; + } + + //Set the socket to non-blocking + status = ioctl(tty, FIONBIO, (char *)&non_blocking); + if (status < 0) + { + perror("ioctl failed"); + close(tty); + return status; + } + return 0; +} diff --git a/Firmware/Tools/States.c b/Firmware/Tools/States.c new file mode 100644 index 00000000..6e3eb74a --- /dev/null +++ b/Firmware/Tools/States.c @@ -0,0 +1,143 @@ +#include "settings.h" + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Constants +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +const STATE_ENTRY stateList[] = +{ + {STATE_RESET, "RESET"}, // 0 + {STATE_WAIT_TX_DONE, "STATE_WAIT_TX_DONE"}, // 1 + {STATE_WAIT_RECEIVE, "STATE_WAIT_RECEIVE"}, // 2 +}; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Main loop +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void loop() +{ + int8_t addressByte; + FRAME_TYPE frameType; + + switch (state) + { + default: + printf("Unknown state"); + while(1) + petWDT(); + break; + + case STATE_RESET: + if (settings.trainingServer) + //Reserve the server's address (0) + myAddr = idToAddressByte(ADDR_SERVER, myUniqueId); + else + //Unknown client address + myAddr = ADDR_UNASSIGNED; + + //Start sending heartbeats + xmitVcHeartbeat(myAddr, myUniqueId); + changeState(STATE_WAIT_TX_DONE); + break; + + case STATE_WAIT_TX_DONE: + returnToReceiving(); + changeState(STATE_WAIT_RECEIVE); + break; + + case STATE_WAIT_RECEIVE: + if (transactionComplete) + { + transactionComplete = false; + + //Receive a datagram from the client + frameType = rcvDatagram(); + + /* + .------- rxData + | + V + +--------+------+-----+--- ... ---+ + | Length | Dest | Src | Data | + +--------+------+-----+--- ... ---+ + | | + |<----------- rxBytes ----------->| + */ + + //Process the datagram + switch(frameType) + { + default: + break; + + case FRAME_VC_HEARTBEAT: + //Save our address + if ((myAddr == ADDR_UNASSIGNED) && (memcmp(myUniqueId, &rxData[3], UNIQUE_ID_BYTES) == 0)) + { + myAddr = srcAddr; +printf("myAddr: %d\n", myAddr); + } + + //Translate the unique ID into an address byte + addressByte = idToAddressByte(srcAddr, &rxData[3]); + + if (settings.trainingServer && (srcAddr == ADDR_UNASSIGNED) && (addressByte >= 0)) + //Assign the address to the client + xmitVcHeartbeat(addressByte, &rxData[3]); + + changeState(STATE_WAIT_TX_DONE); + break; + + case FRAME_VC_DATA: + if ((destAddr != myAddr) && (destAddr != ADDR_BROADCAST)) + returnToReceiving(); + else + { + //place in serial output buffer + } + break; + } + } + //Process serial data length bytes at a time +// else if ((serialAvailable() && (available data >= length byte)) +// { +// if ((length <= maxFrameSize) && ((dest == broadcast) or (dest == cmd) or (link up for dest))) +// { +// if (dest == cmd) +// { +// Send to command processor +// Package response with length, dest=myAddr, src=cmd +// Place packaged response in output serial buffer +// } +// else if (dest == myAddr) +// { +// Place in output serial buffer +// } +// else +// { +// xmitVcData(); +// changeState(STATE_WAIT_TX_DONE); +// } +// } +// else +// { +// //Discard the frame +// } +// } + else if ((millis() - datagramTimer) >= settings.heartbeatTimeout) + { +printf("deltaTime: %d\n", millis() - datagramTimer); + //Send another heartbeat + xmitVcHeartbeat(myAddr, myUniqueId); + changeState(STATE_WAIT_TX_DONE); + } + break; + } +} + +void changeState(int newState) +{ + printf("State: %s\n", stateList[newState].name); + state = newState; +} diff --git a/Firmware/Tools/System.c b/Firmware/Tools/System.c new file mode 100644 index 00000000..971738e5 --- /dev/null +++ b/Firmware/Tools/System.c @@ -0,0 +1,194 @@ +#include "settings.h" + +void defaultSettings(Settings * settings) +{ + settings->trainingServer = false; + settings->heartbeatTimeout = 5000; +} + +void dumpBuffer(uint8_t * data, int length) +{ + char byte; + int bytes; + uint8_t * dataEnd; + uint8_t * dataStart; + const int displayWidth = 16; + int index; + + dataStart = data; + dataEnd = &data[length]; + while (data < dataEnd) + { + // Display the offset + printf(" 0x%02x: ", (unsigned int)(data - dataStart)); + + // Determine the number of bytes to display + bytes = dataEnd - data; + if (bytes > displayWidth) + bytes = displayWidth; + + // Display the data bytes in hex + for (index = 0; index < bytes; index++) + printf(" %02x", *data++); + + // Space over to the ASCII display + for (; index < displayWidth; index++) + printf(" "); + printf(" "); + + // Display the ASCII bytes + data -= bytes; + for (index = 0; index < bytes; index++) { + byte = *data++; + printf("%c", ((byte < ' ') || (byte >= 0x7f)) ? '.' : byte); + } + printf("\n"); + } +} + +uint32_t millis() +{ + return currentTime; +} + +int8_t idToAddressByte(int8_t srcAddr, uint8_t * id) +{ + int8_t index; + RESERVED_ADDRESS * addr; + + //Determine if the address is already in the list + for (index = 0; index < MAX_CLIENT_ADDRESS; index++) + { + //Verify that an address is present + if ((freeAddresses & (1 << index)) == 0) + continue; + + //Compare the unique ID values + if (memcmp(addressList[index]->uniqueId, id, UNIQUE_ID_BYTES) == 0) + { +printf ("Address %d already assigned to ", index); +for (int i = 0; i < UNIQUE_ID_BYTES; i++) +printf("%02x", id[i]); +printf("\n"); + if ((linkUp & (1 << index)) == 0) + { + linkUp |= 1 << index; + printf("========== Link %d up ==========\n", srcAddr); + } + return index; + } + } + + //Fill in clients that were already running + index = srcAddr; + + //Only the server can assign the address bytes + if ((srcAddr == ADDR_UNASSIGNED) && (!settings.trainingServer)) + return -1; + + //Assign an address if necessary + if (srcAddr == ADDR_UNASSIGNED) + { + //Unknown client ID + //Determine if there is a free address + if (freeAddresses == (ADDRESS_MASK)(-1)) + { + printf ("ERROR: Too many clients, no free addresses!\n"); + return -2; + } + + //Look for the next free address byte + for (index = 0; index < MAX_CLIENT_ADDRESS; index++) + if ((freeAddresses & (1 << index)) == 0) + break; + +printf ("Server assigning address %d to : ", index); +for (int i = 0; i < UNIQUE_ID_BYTES; i++) +printf("%02x", id[i]); +printf("\n"); + } + + //Check for an address conflict + if (addressList[index]) + { + printf("ERROR: Unknown ID with pre-assigned conflicting address: %d\n", srcAddr); + printf("Received ID: "); + for (int i = 0; i < UNIQUE_ID_BYTES; i++) + printf("%02x", id[i]); + printf("\n"); + printf("Assigned ID: "); + for (int i = 0; i < UNIQUE_ID_BYTES; i++) + printf("%02x", addressList[srcAddr]->uniqueId[i]); + printf("\n"); + return -3; + } + + //Allocate the address structure + addr = malloc(sizeof(*addr)); + if (!addr) + { + printf ("ERROR: No free memory, malloc of RESERVED_ADDRESS failed!\n"); + return -4; + } + + //Reserve this address + freeAddresses |= 1 << index; + addr->addressByte = index; + memcpy(&addr->uniqueId, id, UNIQUE_ID_BYTES); + addressList[index] = addr; + + //Mark this link as up + linkUp |= 1 << index; + printf("========== Link %d up ==========\n", index); + + //Returned the assigned address + return index; +} + +void petWDT() +{ +} + +int processData() +{ + int deltaMillis; + struct timeval start; + int status; + struct timeval stop; + struct timeval timeout; + + //Initialize the fd_sets + FD_ZERO(&exceptfds); + FD_ZERO(&readfds); + FD_ZERO(&writefds); + + //Implement the protocol + changeState(STATE_RESET); + gettimeofday(&start, NULL); + while (1) + { + //Set the timeout + timeout.tv_sec = 0; + timeout.tv_usec = 1000; + + //Wait for receive data or timeout + FD_SET(tty, &readfds); + status = select(tty + 1, &readfds, &writefds, &exceptfds, &timeout); + + //Update the milliseconds since started + gettimeofday(&stop, NULL); + deltaMillis = ((stop.tv_sec - start.tv_sec) * 1000000) + stop.tv_usec - start.tv_usec; + deltaMillis /= 1000; + if (deltaMillis) + { + currentTime += deltaMillis; + start = stop; + } + + //Indicate receive interrupt + if (status == 1) + transactionComplete = 1; + loop(); + } + return 0; +} diff --git a/Firmware/Tools/Terminal.c b/Firmware/Tools/Terminal.c new file mode 100644 index 00000000..1db8b382 --- /dev/null +++ b/Firmware/Tools/Terminal.c @@ -0,0 +1,102 @@ +#include "settings.h" + +int openTty(const char *ttyName) +{ + struct termios params; + int status; + + tty = open (ttyName, O_RDWR); + if (tty < 0) + { + perror ("ERROR: Failed to open the tty"); + return errno; + } + + if (tcgetattr(tty, ¶ms) != 0) { + perror("ERROR: tcgetattr failed!"); + return errno; + } + + //Initialize the terminal parameters + params.c_cflag &= ~(CSIZE | CSTOPB | PARENB); + params.c_cflag |= CS8 | CRTSCTS | CREAD | CLOCAL; + + params.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHONL | ISIG); + + params.c_iflag &= ~(IXON | IXOFF | IXANY | IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); + + params.c_oflag &= ~(OPOST | ONLCR); + + cfsetispeed(¶ms, B57600); + cfsetospeed(¶ms, B57600); + + if (tcsetattr(tty, TCSANOW, ¶ms) != 0) { + perror("ERROR: tcsetattr failed!"); + return errno; + } + + usingTerminal = true; + return 0; +} + +int readTty(uint8_t * buffer, int bufferLength) +{ + int bytes; + int length; + + //Read the frame type + bytes = read(tty, buffer, 1); + if (bytes < 0) + { + perror("Failed to read frame type!"); + exit(bytes); + } + rxBytes = bytes; + + //Read the length in bytes + bytes = read(tty, &buffer[rxBytes], 1); + if (bytes < 0) + { + perror("Failed to read length byte!"); + exit(bytes); + } + + //Get the length in bytes + length = buffer[rxBytes]; + rxBytes += bytes; + + //Read the length in bytes + while (length - rxBytes) + { + bytes = read(tty, &buffer[rxBytes], length - rxBytes); + if (bytes < 0) + { + perror("Failed to read remaining data!"); + exit(bytes); + } + rxBytes += bytes; + } + + return rxBytes; +} + +int updateTerm(int fd) +{ + struct termios params; + int status; + + if (tcgetattr(fd, ¶ms) != 0) { + perror("ERROR: STDIN tcgetattr failed!"); + return errno; + } + + //Initialize the terminal parameters + params.c_iflag &= ~(IXON | IXOFF | ISTRIP); + + if (tcsetattr(fd, TCSANOW, ¶ms) != 0) { + perror("ERROR: STDIN tcsetattr failed!"); + return errno; + } + + return 0; +} diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c new file mode 100644 index 00000000..96196817 --- /dev/null +++ b/Firmware/Tools/VcServerTest.c @@ -0,0 +1,220 @@ +#include "settings.h" + +#define BUFFER_SIZE 2048 +#define MAX_MESSAGE_SIZE 32 +#define STDIN 0 +#define STDOUT 1 + +int myVcAddr; +int remoteVcAddr; +uint8_t inputBuffer[BUFFER_SIZE + 3]; +uint8_t outputBuffer[BUFFER_SIZE + 3]; + +int stdinToRadio() +{ + int bytesRead; + int bytesSent; + int bytesToSend; + int maxfds; + int status; + struct timeval timeout; + uint8_t * vcData; + + status = 0; + do + { + //Read the console input data into the local buffer. + vcData = inputBuffer; + bytesRead = read(STDIN, &inputBuffer[3], BUFFER_SIZE); + if (bytesRead < 0) + { + perror("ERROR: Read from stdin failed!"); + status = bytesRead; + break; + } + + //Send this data over the VC + bytesSent = 0; + while (bytesSent < bytesRead) + { + //Break up the data if necessary + bytesToSend = bytesRead - bytesSent; + if (bytesToSend > MAX_MESSAGE_SIZE) + bytesToSend = MAX_MESSAGE_SIZE; + + //Send the data + vcData[0] = bytesToSend + 3; + vcData[1] = remoteVcAddr; + vcData[2] = myVcAddr; + bytesToSend = write(tty, vcData, *vcData); + if (bytesToSend < 0) + { + perror("ERROR: Write to radio failed!"); + status = bytesToSend; + break; + } + + //Account for the bytes written + bytesSent += bytesToSend; + } + } while (0); + return status; +} + +int radioToStdout() +{ + int bytesRead; + int bytesSent; + int bytesToSend; + int8_t destAddr; + uint8_t length; + int maxfds; + int status; + int8_t srcAddr; + struct timeval timeout; + uint8_t * vcData; + + status = 0; + do + { + //Read the virtual circuit header into the local buffer. + vcData = outputBuffer; +bytesRead = sizeof(outputBuffer); + bytesRead = read(tty, outputBuffer, bytesRead); + if (bytesRead < 0) + { + perror("ERROR: Read from radio failed!"); + status = bytesRead; + break; + } + +/* + //Display the VC header + length = vcData[0]; + destAddr = vcData[1]; + srcAddr = vcData[2]; + printf("length: %d\n", length); + printf("destAddr: %d\n", destAddr); + printf("srcAddr: %d\n", srcAddr); + + //Read the message + vcData = outputBuffer; + bytesRead = read(STDIN, outputBuffer, length); + if (bytesRead < 0) + { + perror("ERROR: Read from radio failed!"); + status = bytesRead; + break; + } +*/ + + //Send this data over the VC + bytesSent = 0; + while (bytesSent < bytesRead) + { + //Send the data + bytesToSend = bytesRead - bytesSent; + if (bytesToSend > MAX_MESSAGE_SIZE) + bytesToSend = MAX_MESSAGE_SIZE; + bytesToSend = write(STDOUT, &vcData[bytesSent], bytesToSend); + if (bytesToSend < 0) + { + perror("ERROR: Write to radio failed!"); + status = bytesToSend; + break; + } + + //Account for the bytes written + bytesSent += bytesToSend; + } + } while (0); + return status; +} + +int +main ( + int argc, + char ** argv +) +{ + int maxfds; + int status; + struct timeval timeout; + uint8_t * vcData; + + status = 0; + do + { + //Display the help text if necessary + if ((argc != 2) || (sscanf(argv[1], "/dev/ttyACM%d", &myVcAddr) != 1)) + { + printf("%s terminal\n", argv[0]); + printf("\n"); + printf("terminal - Name or path to the terminal device for the radio\n"); + status = -1; + break; + } + + //Determine the remote VC + if (myVcAddr) + remoteVcAddr = 0; + else + remoteVcAddr = 1; + + //Update STDIN and STDOUT +/* + status = updateTerm(STDIN); + if (status) + break; + status = updateTerm(STDOUT); + if (status) + break; +*/ + + //Open the terminal + maxfds = STDIN; + status = openTty(argv[1]); + if (status) + break; + if (maxfds < tty) + maxfds = tty; + + //Display myVCAddr + printf("myVcAddr: %d\n", myVcAddr); + + //Initialize the fd_sets + FD_ZERO(&exceptfds); + FD_ZERO(&readfds); + FD_ZERO(&writefds); + + while (1) + { + //Set the timeout + timeout.tv_sec = 0; + timeout.tv_usec = 1000; + + //Wait for receive data or timeout + FD_SET(STDIN, &readfds); + FD_SET(tty, &readfds); + status = select(maxfds + 1, &readfds, &writefds, &exceptfds, &timeout); + + //Determine if console input is available + if (FD_ISSET(STDIN, &readfds)) + { + //Send the console input to the radio + status = stdinToRadio(); + if (status) + break; + } + + if (FD_ISSET(tty, &readfds)) + { + //Write the radio data to console output + status = radioToStdout(); + if (status) + break; + } + } + } while (0); + return status; +} diff --git a/Firmware/Tools/makefile b/Firmware/Tools/makefile new file mode 100644 index 00000000..2738fd7f --- /dev/null +++ b/Firmware/Tools/makefile @@ -0,0 +1,71 @@ +###################################################################### +# makefile +# +# Builds the LoRaSerial support programs +###################################################################### + +########## +# Source files +########## + +EXECUTABLES = Client +EXECUTABLES += Server +EXECUTABLES += VcServerTest + +INCLUDES = settings.h + +COMMON_LIB = Common_Lib.a + +LIB_OBJS = RadioV2.o Sockets.o States.o System.o Terminal.o + +########## +# Buid tools and rules +########## + +GCC = gcc +CFLAGS = -flto -O3 -Wpedantic -pedantic-errors -Wall -Wextra -Werror -Wno-unused-variable -Wno-unused-parameter +CC = $(GCC) $(CFLAGS) + +%.o: %.c $(INCLUDES) + $(CC) -c -o $@ $< + +%: %.c $(INCLUDES) + $(CC) $(CFLAGS) -o $@ $< + +########## +# Buid all the sources - must be first +########## + +.PHONY: all + +all: $(EXECUTABLES) + +########## +# Buid the libraries +########## + +$(COMMON_LIB): $(LIB_OBJS) + gcc --version + ar rvs $@ $^ + +########## +# Build the executables +########## + +Client: Client.c $(COMMON_LIB) + $(CC) -o $@ $^ + +Server: Server.c $(COMMON_LIB) + $(CC) -o $@ $^ + +VcServerTest: VcServerTest.c $(COMMON_LIB) + $(CC) -o $@ $^ + +######## +# Clean the build directory +########## + +.PHONY: clean + +clean: + rm -f *.o *.a $(EXECUTABLES) diff --git a/Firmware/Tools/settings.h b/Firmware/Tools/settings.h new file mode 100644 index 00000000..c0e102e0 --- /dev/null +++ b/Firmware/Tools/settings.h @@ -0,0 +1,146 @@ +#ifndef __settings_h__ +#define __settings_h__ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Defines +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +#define ADDR_SERVER 0 +#define ADDR_BROADCAST -1 +#define ADDR_UNASSIGNED -2 + +#define PORT 7272 +#define UNIQUE_ID_BYTES 16 + +#define false 0 +#define true 1 + +#define BROADCAST_IPV4_ADDRESS inet_addr("192.168.86.255") //INADDR_BROADCAST + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Enums +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +typedef enum { + FRAME_VC_HEARTBEAT = 0, + FRAME_VC_DATA, +} FRAME_TYPE; + +typedef enum +{ + STATE_RESET = 0, // 0 + STATE_WAIT_TX_DONE, // 1 + STATE_WAIT_RECEIVE, // 2 +} STATE; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Types +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +typedef uint8_t ADDRESS_MASK; + +typedef uint8_t bool; + +typedef struct _RESERVED_ADDRESS +{ + uint8_t uniqueId[UNIQUE_ID_BYTES]; + uint8_t addressByte; +} RESERVED_ADDRESS; + +typedef struct _STATE_ENTRY +{ + STATE state; + const char * name; +} STATE_ENTRY; + +typedef struct _SETTINGS +{ + uint32_t heartbeatTimeout; + bool trainingServer; +} Settings; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Globals +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +uint32_t currentTime; +uint32_t datagramTimer; +int8_t destAddr; +fd_set exceptfds; +int8_t myAddr; +uint8_t myUniqueId[UNIQUE_ID_BYTES]; +fd_set readfds; +uint8_t rxBuffer[255]; +int rxBytes; +uint8_t * rxData; +Settings settings; +int8_t srcAddr; +int state; +bool transactionComplete; +uint8_t txBuffer[255]; +int txBytes; +uint8_t * txData; +int tty; +bool usingTerminal; +fd_set writefds; + +struct sockaddr_in localAddr; +struct sockaddr_in remoteAddr; +struct sockaddr_in broadcastAddr; +socklen_t remoteAddrLength; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Address Byte Management +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +ADDRESS_MASK freeAddresses; +ADDRESS_MASK linkUp; +#define MAX_CLIENT_ADDRESS ((uint8_t)(sizeof(freeAddresses) * 8)) +RESERVED_ADDRESS * addressList[MAX_CLIENT_ADDRESS]; + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Forward Declarations +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//RadioV2 +FRAME_TYPE rcvDatagram(); +void returnToReceiving(); +void transmitDatagram(const struct sockaddr * addr, int addrLen); +void xmitVcHeartbeat(int8_t addr, uint8_t * id); + +//Sockets +int openSocket(); + +//States +void changeState(int newState); +void loop(); + +//Systems +void defaultSettings(Settings * settings); +void dumpBuffer(uint8_t * data, int length); +int8_t idToAddressByte(int8_t srcAddr, uint8_t * id); +uint32_t millis(); +void petWDT(); +int processData(); + +//Terminal +int openTty(const char *ttyName); +int readTty(uint8_t * buffer, int bufferLength); +int updateTerm(int fd); + +#endif // __settings_h__ From 435afcd99dddf4d57dd361fb8a077d1128d7b813 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 26 Oct 2022 09:14:25 -1000 Subject: [PATCH 063/594] Add RX Flow_Control test sketch Test case: States.ino --> USB in --> USB out --> st.txt Results: The files matched, except the following lines were added to the output file: Delay time: 495522 mSec RX Buffer Full Count: 135 Test case: States.ino --> Digilent Pmod USBUart --> Serial1 --> USB Out -->st2.txt Results: The files matched, except the following lines were added to the output file: Delay time: 495400 mSec RX Buffer Full Count: 133 These lines indicated that the output took over 8 minutes to print and that the USB serial receive buffer was full over 65K times! Therefore the SAMD21 was applying flow control back pressure on the PC limiting how fast USB serial data was allowed to leave the PC and enter the SAMD21. --- .../Test_Sketches/Flow_Control/Arch_SAMD.h | 122 ++++++++++++++++ .../Flow_Control/Flow_Control.ino | 95 +++++++++++++ .../Test_Sketches/Flow_Control/Serial.ino | 132 ++++++++++++++++++ 3 files changed, 349 insertions(+) create mode 100644 Firmware/Test_Sketches/Flow_Control/Arch_SAMD.h create mode 100644 Firmware/Test_Sketches/Flow_Control/Flow_Control.ino create mode 100644 Firmware/Test_Sketches/Flow_Control/Serial.ino diff --git a/Firmware/Test_Sketches/Flow_Control/Arch_SAMD.h b/Firmware/Test_Sketches/Flow_Control/Arch_SAMD.h new file mode 100644 index 00000000..659ad130 --- /dev/null +++ b/Firmware/Test_Sketches/Flow_Control/Arch_SAMD.h @@ -0,0 +1,122 @@ +#if defined(ARDUINO_ARCH_SAMD) +#ifndef __SAMD_H__ + +/* + Data flow + +--------------+ + | SAMD | + | | + TTL Serial <-->| Serial1 | +--------------+ + | SPI |<----->| SX1276 Radio |<---> Antenna + USB Serial <-->| Serial | +--------------+ ^ + +--------------+ | + | + +--------------+ | + | SAMD | | + | | | + TTL Serial <-->| Serial1 | +--------------+ V + | SPI |<----->| SX1276 Radio |<---> Antenna + USB Serial <-->| Serial | +--------------+ + +--------------+ + + +--------------+ + | SAMD | + | | + | PB02 A5 |---> rxLED + | PB23 31 |---> txLED + | | + | PA07 9 |---> rssi4LED + | PA06 8 |---> rssi3LED + | PA05 A4 |---> rssi2LED + | PA04 A3 |---> rssi1LED + | | + USB_D- <------------->| 28 PA24 | + USB_D+ <------------->| 29 PA25 | + | | + | PA15 5 |---> cs -----> LORA_CS/ + | PA17 13 |---> SCLK ---> SPI_SCK + | PA16 11 |---> MOSI ---> SPI_PICO + | PA19 12 |<--- MISO ---> SPI_POCI + | | + RTS-0 <------ rts <---| 38 PA13 | + TX-0 <------- tx <----| 0 PA10 | + RX-I_LV <---- rx ---->| 1 PA11 | + CTS-I_LV <--- cts --->| 30 PB22 | + | | + | PA09 3 |---> rxen ---> LORA_RXEN + | PA14 2 |---> txen ---> LORA_TXEN + | | + | PA18 10 |<--- dio1 <--- LORA_D1 (Freq Change) + | PA21 7 |<--- dio0 <--- LORA_D0 (TX Done) + | | + | PA20 6 |---> rst ----> LORA_RST/ + | | + | PA23 21 |---> SCL + | PA22 20 |---> SDA + +--------------+ +*/ + +void samdBeginBoard() +{ + pin_cts = 30; + pin_rts = 38; + pin_txLED = 31; + pin_rxLED = A5; + + //Flow control + pinMode(pin_rts, OUTPUT); + digitalWrite(pin_rts, INVERT_RTS_CTS ? HIGH : LOW); //Don't give me more + + pinMode(pin_cts, INPUT_PULLUP); + + //LEDs + pinMode(pin_txLED, OUTPUT); + digitalWrite(pin_txLED, LOW); + pinMode(pin_rxLED, OUTPUT); + digitalWrite(pin_rxLED, LOW); +} + +void samdBeginSerial(uint16_t serialSpeed) +{ + //Wait for serial to come online for debug printing + SerialUSB.begin(serialSpeed); + while (!SerialUSB); + + Serial1.begin(serialSpeed); +} + +bool samdSerialAvailable() +{ + return (SerialUSB.available() || Serial1.available()); +} + +void samdSerialFlush() +{ + SerialUSB.flush(); + Serial1.flush(); +} + +void samdSerialPrint(const char * value) +{ + SerialUSB.print(value); + Serial1.print(value); +} + +uint8_t samdSerialRead() +{ + byte incoming = 0; + if (SerialUSB.available()) + incoming = SerialUSB.read(); + else if (Serial1.available()) + incoming = Serial1.read(); + return (incoming); +} + +void samdSerialWrite(uint8_t value) +{ + SerialUSB.write(value); + Serial1.write(value); +} + +#endif //__SAMD_H__ +#endif //ARDUINO_ARCH_SAMD diff --git a/Firmware/Test_Sketches/Flow_Control/Flow_Control.ino b/Firmware/Test_Sketches/Flow_Control/Flow_Control.ino new file mode 100644 index 00000000..3736e3f6 --- /dev/null +++ b/Firmware/Test_Sketches/Flow_Control/Flow_Control.ino @@ -0,0 +1,95 @@ +/* + October 26 2022 + SparkFun Electronics + Lee Leahy + + Verify flow control operation + + Compiled with Arduino v1.8.15 +*/ + +#define UNUSED(x) (void)(x) + +#define INVERT_RTS_CTS 1 + +//Hardware connections +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//These pins are set in beginBoard() +#define PIN_UNDEFINED 255 + +uint8_t pin_rts = PIN_UNDEFINED; +uint8_t pin_cts = PIN_UNDEFINED; +uint8_t pin_txLED = PIN_UNDEFINED; +uint8_t pin_rxLED = PIN_UNDEFINED; +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//Global variables - Serial +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Buffer to store bytes incoming from serial before broadcasting over LoRa +uint8_t serialTransmitBuffer[1024 * 1]; //Bytes received from RF waiting to be printed out UART. Buffer up to 1s of bytes at 4k + +uint16_t txHead = 0; +uint16_t txTail = 0; + +//When RTS is asserted, host says it's ok to send data +bool rtsAsserted; + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +/* Data Flow + + USB or UART + | + | Flow control: RTS for UART + | Off: Buffer full + | On: Buffer drops below half full + V + serialTransmitBuffer + | + | Flow control: CTS for UART + | + V + USB or UART + +*/ + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//Architecture variables +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#include "Arch_SAMD.h" +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +void setup() +{ + samdBeginSerial(57600); + samdBeginBoard(); + randomSeed(5); + + //Enable the flow of data from the remote serial port into the SAMD + updateRTS(true); +} + +void loop() +{ + updateSerial(); //Store incoming and print outgoing +} + +void systemPrint(int value) +{ + char string[20]; + sprintf(string, "%d", value); + SerialUSB.print(string); +} + +void systemPrint(const char * string) +{ + SerialUSB.print(string); +} + +void systemPrintln() +{ + SerialUSB.print("\r\n"); +} diff --git a/Firmware/Test_Sketches/Flow_Control/Serial.ino b/Firmware/Test_Sketches/Flow_Control/Serial.ino new file mode 100644 index 00000000..3e7020d0 --- /dev/null +++ b/Firmware/Test_Sketches/Flow_Control/Serial.ino @@ -0,0 +1,132 @@ +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Serial RX - Data arriving at the USB or serial port +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Returns true if CTS is asserted (host says it's ok to send data) +bool isCTS() +{ + return (digitalRead(pin_cts) == (INVERT_RTS_CTS ? LOW : HIGH)); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Serial TX - Data being sent to the USB or serial port +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Return number of bytes sitting in the serial transmit buffer +uint16_t availableTXBytes() +{ + if (txHead >= txTail) + return (txHead - txTail); + return (sizeof(serialTransmitBuffer) - txTail + txHead); +} + +//Returns true if CTS is asserted (host says it's ok to send data) +void updateRTS(bool assertRTS) +{ + rtsAsserted = assertRTS; + digitalWrite(pin_rts, assertRTS ^ INVERT_RTS_CTS); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Move serial data through the system +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//See if there's any serial from the remote radio that needs printing +//Record any characters to the receive buffer +//Scan for escape characters +void updateSerial() +{ + bool bufferFull; + static uint32_t bufferFullCount; + static bool copyStarted; + static uint32_t delayMillis; + int x; + static uint32_t lastTime; + static uint32_t totalDelayMillis; + + //Determine if bytea are available for transmission + while (availableTXBytes() && isCTS() && ((millis() - lastTime) >= delayMillis)) + { + //Account for the delay time. + lastTime = millis(); + totalDelayMillis += delayMillis; + delayMillis = random(1, 15); + copyStarted = true; + + //Turn on LED during serial transmissions + txLED(true); + + //Send a byte to the host + samdSerialWrite(serialTransmitBuffer[txTail++]); + txTail %= sizeof(serialTransmitBuffer); + + //Assert RTS when enough space is available + if (availableTXBytes() <= (sizeof(serialTransmitBuffer) / 2)) + updateRTS(true); //Ok to send more + + //Since the UART does not have RTS and CTS, prevent loss of serial data + //by sending one character at a time + samdSerialFlush(); + + //Turn off LED + txLED(false); + } + + //Look for local incoming serial + if (samdSerialAvailable() && rtsAsserted) + { + do + { + rxLED(true); //Turn on LED during serial reception + + //Deassert RTS when the buffer becomes full + if (availableTXBytes() >= (sizeof(serialTransmitBuffer) - 32)) + { + updateRTS(false); //Don't give me more + bufferFullCount += 1; + } + + //Get the next character + serialTransmitBuffer[txHead++] = samdSerialRead(); + txHead %= sizeof(serialTransmitBuffer); + + rxLED(false); //Turn off LED + } while (samdSerialAvailable() && rtsAsserted); + } + + if (copyStarted && ((millis() - lastTime) >= (5 * 1000))) + { + copyStarted = false; + systemPrint("Delay time: "); + systemPrint(totalDelayMillis); + systemPrint(" mSec"); + systemPrintln(); + systemPrint("RX Buffer Full Count: "); + systemPrint(bufferFullCount); + systemPrintln(); + bufferFullCount = 0; + totalDelayMillis = 0; + } +} + +void txLED(bool illuminate) +{ + if (pin_txLED != PIN_UNDEFINED) + { + if (illuminate == true) + digitalWrite(pin_txLED, HIGH); + else + digitalWrite(pin_txLED, LOW); + } +} + +void rxLED(bool illuminate) +{ + if (pin_rxLED != PIN_UNDEFINED) + { + if (illuminate == true) + digitalWrite(pin_rxLED, HIGH); + else + digitalWrite(pin_rxLED, LOW); + } +} From 28cd1238baa09db8173063b97027d24273e3a842 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 27 Oct 2022 06:13:18 -1000 Subject: [PATCH 064/594] Add ATS58 and ATS59 commands to invert CTS and RTS Add invertCts and invertRts to settings. Move setting of RTS into a subroutine to properly handle inversion. Replace the direct writes to the RTS pin with calls to the subroutine. Prevent serial input until the end of setup. --- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 2 +- Firmware/LoRaSerial_Firmware/Commands.ino | 2 ++ .../LoRaSerial_Firmware.ino | 6 +++++ Firmware/LoRaSerial_Firmware/Serial.ino | 22 +++++++++---------- Firmware/LoRaSerial_Firmware/settings.h | 2 ++ 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 394560ce..78d665ce 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -86,7 +86,7 @@ void samdBeginBoard() //Flow control pinMode(pin_rts, OUTPUT); - digitalWrite(pin_rts, HIGH); + updateRTS(false); //Disable serial input until the radio starts pinMode(pin_cts, INPUT_PULLUP); diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index d96b1e0e..83ae6857 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -683,6 +683,8 @@ const COMMAND_ENTRY commands[] = {55, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &settings.copyTriggers}, {56, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &settings.trainingKey}, {57, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &settings.printLinkUpDown}, + {58, 0, 1, 0, TYPE_BOOL, valInt, "InvertCts", &settings.invertCts}, + {59, 0, 1, 0, TYPE_BOOL, valInt, "InvertRts", &settings.invertRts}, //Define any user parameters starting at 255 decrementing towards 0 }; diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 0d140966..edf5731e 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -164,6 +164,8 @@ const long minEscapeTime_ms = 2000; //Serial traffic must stop this amount befor bool inCommandMode = false; //Normal data is prevented from entering serial output when in command mode uint8_t commandLength = 0; bool remoteCommandResponse; + +bool rtsAsserted; //When RTS is asserted, host says it's ok to send data //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - LEDs @@ -298,6 +300,8 @@ char platformPrefix[25]; //Used for printing platform specific device name, ie " //Architecture variables //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +void updateRTS(bool assertRTS); + #include "Arch_ESP32.h" #include "Arch_SAMD.h" //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -329,6 +333,8 @@ void setup() arch.beginWDT(); //Start watchdog timer + updateRTS(true); //Enable serial input + systemPrintTimestamp(); systemPrintln("LRS Setup Complete"); diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index efa21d41..587b53f1 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -14,8 +14,7 @@ bool isCTS() { if (pin_cts == PIN_UNDEFINED) return (true); //CTS not implmented on this board if (settings.flowControl == false) return (true); //CTS turned off - if (digitalRead(pin_cts) == HIGH) return (true); - return (false); + return (digitalRead(pin_cts) == HIGH) ^ settings.invertCts; } //If we have data to send, get the packet ready @@ -98,6 +97,14 @@ void serialBufferOutput(uint8_t * data, uint16_t dataLength) txHead %= sizeof(serialTransmitBuffer); } +//Update the output of the RTS pin (host says it's ok to send data when assertRTS = true) +void updateRTS(bool assertRTS) +{ + rtsAsserted = assertRTS; + if (settings.flowControl && (pin_rts != PIN_UNDEFINED)) + digitalWrite(pin_rts, assertRTS ^ settings.invertRts); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Command RX - Remote command data received from a remote system //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -215,16 +222,9 @@ void updateSerial() //Handle RTS if (availableRXBytes() == sizeof(serialReceiveBuffer) - 1) - { - //Buffer full! - if (pin_rts != PIN_UNDEFINED && settings.flowControl == true) - digitalWrite(pin_rts, LOW); //Don't give me more - } + updateRTS(false); //Buffer full! else - { - if (pin_rts != PIN_UNDEFINED && settings.flowControl == true) - digitalWrite(pin_rts, HIGH); //Ok to send more - } + updateRTS(true); //Ok to send more byte incoming = systemRead(); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 9ea4b0e9..02092beb 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -459,6 +459,8 @@ typedef struct struct_settings { bool copyTriggers = true; //Copy the trigger parameters to the training client uint8_t trainingKey[AES_KEY_BYTES] = { 0x53, 0x70, 0x61, 0x72, 0x6b, 0x46, 0x75, 0x6E, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67 }; bool printLinkUpDown = false; //Print the link up and link down messages + bool invertCts = false; //Invert the input of CTS + bool invertRts = false; //Invert the output of RTS //Add new parameters immediately before this line //-- Add commands to set the parameters From 3e51fa30692494a2596f6cb917a3d8bc17b2642e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 26 Oct 2022 05:41:58 -1000 Subject: [PATCH 065/594] Add USB/UART data flow diagram --- .../LoRaSerial_Firmware.ino | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index edf5731e..b71a77a3 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -166,6 +166,61 @@ uint8_t commandLength = 0; bool remoteCommandResponse; bool rtsAsserted; //When RTS is asserted, host says it's ok to send data + +/* Data Flow - Point-to-Point and Multi-Point + + USB or UART + | + | inCommandMode? + | + true V false + .-------------+--------------------------. + | | + V v + commandBuffer serialReceiveBuffer + | | + | Remote Command? v + | outgoingPacket + false V true | + .-------------+-------------. V + | | Send to remote system + | V | + | outgoingPacket V + | | incomingBuffer + | V | + | Send to remote system V + | | serialTransmitBuffer + | V | + | incomingBuffer | + | | | + | V | + | commandRXBuffer | + | | | + | V | + | Command processing | + | checkCommand | + | | | + | V | + | commandTXBuffer | + | | | + | V | + | outgoingPacket | + | | | + | V | + | Send back to local system | + | | | + | V | + | incomingBuffer | + | | | + | V | + `-------------------------->+<-------------------------' + | + | + V + USB or UART + +*/ + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - LEDs From d71636bc017d51b7d8d2b7cb5e0dd7e5bdb779a2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 27 Oct 2022 17:36:29 -1000 Subject: [PATCH 066/594] Flush the buffers and any serial input when the link breaks --- Firmware/LoRaSerial_Firmware/Serial.ino | 41 +++++++++++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 6 ++++ 2 files changed, 47 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 587b53f1..894f2df6 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -440,3 +440,44 @@ bool vcSerialMessageReceived() //Nothing to send at this time return false; } + +void resetSerial() +{ + uint32_t delayTime; + uint32_t lastCharacterReceived; + + //Determine the amount of time needed to receive a character + delayTime = 200; + if (settings.airSpeed) + { + delayTime = (1000 * 8 * 2) / settings.airSpeed; + if (delayTime < 200) + delayTime = 200; + } + + //Enable RTS + updateRTS(true); + + //Flush the incoming serial + lastCharacterReceived = millis(); + do + { + //Discard any incoming serial data + while (arch.serialAvailable()) + { + petWDT(); + systemRead(); + lastCharacterReceived = millis(); + } + petWDT(); + + //Wait enough time to receive any remaining data from the host + } while ((millis() - lastCharacterReceived) < delayTime); + + //Empty the buffers + rxHead = rxTail; + txHead = txTail; + commandRXHead = commandRXTail; + commandTXHead = commandTXTail; + endOfTxData = &outgoingPacket[headerBytes]; +} diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c3bdfa3a..6414a626 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1842,6 +1842,9 @@ void v2BreakLink() if (settings.printLinkUpDown) systemPrintln("--------- Link DOWN ---------"); triggerEvent(TRIGGER_RADIO_RESET); + + //Flush the buffers + resetSerial(); changeState(RADIO_RESET); } @@ -1893,6 +1896,9 @@ void vcBreakLink(int8_t vcIndex) systemPrint(vcIndex); systemPrintln(" Down ---------"); } + + //Flush the buffers + resetSerial(); } int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) From 5b9e5474d6cc446e078c96d8945ecaee156aa848 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 27 Oct 2022 17:46:49 -1000 Subject: [PATCH 067/594] Timeout retransmission attempts using the link break timeout If settings.maxResends is set to zero, allow retransmits continue until the link break timeout occurs, even if HEARTBEATS are being received. --- .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 27 ++++++++++++++++--- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b71a77a3..f210542a 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -325,6 +325,7 @@ unsigned long vcTxHeartbeatMillis; //Transmit control uint8_t * endOfTxData; CONTROL_U8 txControl; +uint32_t transmitTimer; //Multi-point Training bool trainingServerRunning; //Training server is running diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 6414a626..37610664 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -78,6 +78,9 @@ void updateRadioState() returnToReceiving(); //Start receiving + //Stop the transmit timer + transmitTimer = 0; + //Start the link between the radios if (settings.radioProtocolVersion >= 2) { @@ -685,6 +688,7 @@ void updateRadioState() { triggerEvent(TRIGGER_LINK_DATA_XMIT); xmitDatagramP2PData(); + transmitTimer = datagramTimer; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } else if (availableTXCommandBytes()) //If we have command bytes to send out @@ -782,6 +786,9 @@ void updateRadioState() case DATAGRAM_DATA_ACK: syncChannelTimer(0); //Adjust freq hop ISR based on remote's remaining clock + //Stop the transmit timer + transmitTimer = 0; + resetHeartbeat(); //Extend time before next heartbeat //Display the signal strength @@ -857,7 +864,7 @@ void updateRadioState() } //Retransmit the packet - if (frameSentCount < settings.maxResends) + if ((!settings.maxResends) || (frameSentCount < settings.maxResends)) { triggerEvent(TRIGGER_LINK_RETRANSMIT); if (settings.debugDatagrams) @@ -901,6 +908,11 @@ void updateRadioState() v2BreakLink(); } } + + //Retransmits are not getting through in a rational time + else if (transmitTimer && ((millis() - transmitTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) + //Break the link + v2BreakLink(); break; case RADIO_P2P_LINK_UP_HB_ACK_REXMT: @@ -915,7 +927,7 @@ void updateRadioState() transactionComplete = false; //Reset ISR flag //Retransmit the packet - if (rexmtFrameSentCount < settings.maxResends) + if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { RESTORE_TX_BUFFER(); if (settings.debugDatagrams) @@ -1513,7 +1525,7 @@ void updateRadioState() { //Determine if another retransmit is allowed txDestVc = rexmtTxDestVc; - if (rexmtFrameSentCount < settings.maxResends) + if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { rexmtFrameSentCount++; @@ -1843,6 +1855,9 @@ void v2BreakLink() systemPrintln("--------- Link DOWN ---------"); triggerEvent(TRIGGER_RADIO_RESET); + //Stop the transmit timer + transmitTimer = 0; + //Flush the buffers resetSerial(); changeState(RADIO_RESET); @@ -1867,6 +1882,9 @@ void v2EnterLinkUp() commandRXTail = commandRXHead; commandTXTail = commandTXHead; + //Stop the transmit timer + transmitTimer = 0; + //Start the receiver returnToReceiving(); changeState(RADIO_P2P_LINK_UP); @@ -1897,6 +1915,9 @@ void vcBreakLink(int8_t vcIndex) systemPrintln(" Down ---------"); } + //Stop the transmit timer + transmitTimer = 0; + //Flush the buffers resetSerial(); } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 02092beb..0e276061 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -427,7 +427,7 @@ typedef struct struct_settings { bool flowControl = false; //Enable the use of CTS/RTS flow control signals bool autoTuneFrequency = false; //Based on the last packets frequency error, adjust our next transaction frequency bool displayPacketQuality = false; //Print RSSI, SNR, and freqError for received packets - uint8_t maxResends = 2; //Attempt resends up to this number. + uint8_t maxResends = 0; //Attempt resends up to this number, 0 = infinite retries bool sortParametersByName = false; //Sort the parameter list (ATI0) by parameter name bool printParameterName = false; //Print the parameter name in the ATSx? response bool printFrequency = false; //Print the updated frequency From 78abd2d92f371c51a38199573e873fb534c2325c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 27 Oct 2022 18:02:07 -1000 Subject: [PATCH 068/594] Support serial data flow control across the radio link Only read data from the serial port or USB when there is space in the serialReceiveBuffer. Deassert RTS when serialReceiveBuffer is full. Assert RTS when serialReceiveBuffer drops to half full and there is lots of space in the transmit buffer. Testing done by sending States.ino across the radio link and verifying the resulting file. This has been successful a couple of times, but the link is likely to break during the transfer. Add ATI25 command to display the number of times there was insufficient receive buffer space at the radio layer. --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 ++ .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 45 +++++++++++- Firmware/LoRaSerial_Firmware/Serial.ino | 69 +++++++------------ Firmware/Tools/Command_Validation_Script.txt | 15 +++- .../Results/Command_Validation_Results.txt | 56 +++++++++++---- 6 files changed, 131 insertions(+), 60 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 83ae6857..68f9885d 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -180,6 +180,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI22 - Display the VC bad length received"); systemPrintln(" ATI23 - Display the VC link failures"); systemPrintln(" ATI24 - Display the VC details"); + systemPrintln(" ATI25 - Display the total insufficient buffer count"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(); @@ -344,6 +345,10 @@ bool commandAT(const char * commandString) systemPrintln(vc->lastHeartbeatMillis); } break; + case ('5'): //ATI25 - Display the total insufficient buffer count + systemPrint("Total insufficient buffer count: "); + systemPrintln(insufficientSpace); + break; } } diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index f210542a..b0a57e51 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -259,6 +259,7 @@ uint32_t badFrames; //Total number of bad frames received uint32_t duplicateFrames; //Total number of duplicate frames received uint32_t lostFrames; //Total number of lost TX frames uint32_t linkFailures; //Total number of link failures +uint32_t insufficientSpace; //Total number of times the buffer did not have enough space //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - V2 Protocol diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 0ae9e90d..68b3d5e3 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -819,7 +819,6 @@ PacketType rcvDatagram() txAckNumber = (txAckNumber + 1) & 3; break; - case DATAGRAM_DATA: case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: @@ -846,6 +845,50 @@ PacketType rcvDatagram() return DATAGRAM_BAD; } + //Receive this data packet and set the next expected datagram number + rxAckNumber = rmtTxAckNumber; + rmtTxAckNumber = (rmtTxAckNumber + 1) & 3; + break; + + case DATAGRAM_DATA: + if (ackNumber != rmtTxAckNumber) + { + //Determine if this is a duplicate datagram + if (ackNumber == ((rmtTxAckNumber - 1) & 3)) + { + linkDownTimer = millis(); + duplicateFrames++; + return DATAGRAM_DUPLICATE; + } + + //Not a duplicate + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Invalid datagram number, received "); + systemPrint(ackNumber); + systemPrint(" expecting "); + systemPrintln(rmtTxAckNumber); + } + badFrames++; + return DATAGRAM_BAD; + } + + //Verify that there is sufficient space in the serialTransmitBuffer + if ((sizeof(serialTransmitBuffer) - availableTXBytes()) < rxDataBytes) + { + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrintln("Insufficient space in the serialTransmitBuffer"); + } + insufficientSpace++; + + //Apply back pressure to the other radio by dropping this packet and + //forcing the other radio to retransmit the packet. + return DATAGRAM_BAD; + } + //Receive this data packet and set the next expected datagram number rxAckNumber = rmtTxAckNumber; rmtTxAckNumber = (rmtTxAckNumber + 1) & 3; diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 894f2df6..6be821fd 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -168,51 +168,34 @@ void readyOutgoingCommandPacket() //Scan for escape characters void updateSerial() { + int dataBytes; int x; + //Assert RTS when there is enough space in the receive buffer + if ((!rtsAsserted) && (availableRXBytes() < (sizeof(serialReceiveBuffer) / 2)) + && (availableTXBytes() < (sizeof(serialTransmitBuffer) / 4))) + updateRTS(true); + //Forget printing if there are ISRs to attend to - if (transactionComplete == false && timeToHop == false) + dataBytes = availableTXBytes(); + while (dataBytes-- && isCTS() && (!transactionComplete)) { - if (availableTXBytes()) - { - if (isCTS()) - { - txLED(true); //Turn on LED during serial transmissions + txLED(true); //Turn on LED during serial transmissions - //Print data to both ports - for (int x = 0 ; x < availableTXBytes() ; x++) - { - //Take a break if there are ISRs to attend to - petWDT(); - if (transactionComplete == true) break; - if (timeToHop == true) hopChannel(); - if (isCTS() == false) break; - - // int bytesToSend = availableTXBytes(); - // - // if (txTail + bytesToSend > sizeof(serialTransmitBuffer)) - // bytesToSend = sizeof(serialTransmitBuffer) - txTail; - // - // //TODO this may introduce delays when we should be checking ISRs - // Serial.write(&serialTransmitBuffer[txTail], bytesToSend); - // Serial1.write(&serialTransmitBuffer[txTail], bytesToSend); - // txTail += bytesToSend; - // txTail %= sizeof(serialTransmitBuffer); - - systemWrite(serialTransmitBuffer[txTail]); - systemFlush(); //Prevent serial hardware from blocking more than this one write - - txTail++; - txTail %= sizeof(serialTransmitBuffer); - } + //Take a break if there are ISRs to attend to + petWDT(); + if (timeToHop == true) hopChannel(); - txLED(false); //Turn off LED - } - } + systemWrite(serialTransmitBuffer[txTail]); + systemFlush(); //Prevent serial hardware from blocking more than this one write + + txTail++; + txTail %= sizeof(serialTransmitBuffer); } + txLED(false); //Turn off LED //Look for local incoming serial - while (arch.serialAvailable() && transactionComplete == false) + while (rtsAsserted && arch.serialAvailable() && (transactionComplete == false)) { rxLED(true); //Turn on LED during serial reception @@ -220,11 +203,9 @@ void updateSerial() petWDT(); if (timeToHop == true) hopChannel(); - //Handle RTS - if (availableRXBytes() == sizeof(serialReceiveBuffer) - 1) - updateRTS(false); //Buffer full! - else - updateRTS(true); //Ok to send more + //Deassert RTS when the buffer gets full + if (rtsAsserted && (sizeof(serialReceiveBuffer) - availableRXBytes()) < 32) + updateRTS(false); byte incoming = systemRead(); @@ -305,15 +286,11 @@ void updateSerial() if (settings.echo == true) systemWrite(incoming); - //We must always read in characters to avoid causing the host computer blocking USB from sending more - //If the buffer is full, we will overwrite oldest data first serialReceiveBuffer[rxHead++] = incoming; //Push char to holding buffer rxHead %= sizeof(serialReceiveBuffer); } //End process rx buffer - - rxLED(false); //Turn off LED - } //End Serial.available() + rxLED(false); //Turn off LED //Process any remote commands sitting in buffer if (availableRXCommandBytes() && inCommandMode == false) diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 32eb84a6..3b4f3fc1 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -33,6 +33,7 @@ ati22 ati23 ati24 ati25 +ati26 ati0 ats28=0 @@ -520,10 +521,22 @@ ats57=2 ats57=0 ats57? -# Invalid commands +# InvertCts ats58=1 +ats58=2 +ats58=0 ats58? +# InvertRts +ats59=1 +ats59=2 +ats59=0 +ats59? + +# Invalid commands +ats60=1 +ats60? + ats255=1 ats255? diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index d3878270..8258a712 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -14,6 +14,7 @@ Command summary: ATI? - Display the information commands ATIn - Display system information ATO - Exit command mode + ATR - VC link reset ATSn=xxx - Set parameter n's value to xxx ATSn? - Print parameter n's current value ATT - Enter training mode @@ -47,6 +48,7 @@ ati? ATI22 - Display the VC bad length received ATI23 - Display the VC link failures ATI24 - Display the VC details + ATI25 - Display the total insufficient buffer count # Allow leading white space (space, tab, cr, lf) ERROR ati @@ -57,35 +59,35 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati2 2.0 ati3 --157 +-105 ati4 -218 +29 ati5 506 ati6 37782141A665734E4475672AE6308308 ati7 -0 +2 ati8 -E5D60B0B4A34555020312E34212815FF +FB25B80B5730305020312E50021A0AFF ati9 -Total datagrams sent: 24 +Total datagrams sent: 36 ati10 -Total datagrams received: 0 +Total datagrams received: 34 ati11 -Total frames sent: 0 +Total frames sent: 34 ati12 -Total frames sent: 0 +Total frames sent: 34 ati13 Total bad frames received: 0 ati14 Total duplicate frames received: 0 ati15 -Total lost TX frames: 0 +Total lost TX frames: 201 ati16 Maximum datagram size: 253 ati17 -Total link failures: 0 +Total link failures: 1 ati18 VC 0 frames sent: 0 ati19 @@ -101,6 +103,8 @@ VC 0 link failures: 0 ati24 VC 0: Not valid! ati25 +Total insufficient buffer count: 0 +ati26 ERROR ati0 ATS0:SerialSpeed=57600 @@ -129,7 +133,7 @@ ATS22:HeartBeatTimeout=5000 ATS23:FlowControl=0 ATS24:AutoTune=0 ATS25:DisplayPacketQuality=0 -ATS26:MaxResends=2 +ATS26:MaxResends=0 ATS27:SortParametersByName=0 ATS28:PrintParameterName=0 ATS29:PrintFrequency=0 @@ -161,6 +165,8 @@ ATS54:CopySerial=1 ATS55:CopyTriggers=1 ATS56:TrainingKey=537061726B46756E547261696E696E67 ATS57:PrintLinkUpDown=0 +ATS58:InvertCts=0 +ATS59:InvertRts=0 ats28=0 OK # SerialSpeed @@ -1077,11 +1083,35 @@ PrintLinkUpDown=0 OK ats57? PrintLinkUpDown=0 -# Invalid commands +# InvertCts ERROR ats58=1 +InvertCts=1 +OK +ats58=2 ERROR +ats58=0 +InvertCts=0 +OK ats58? +InvertCts=0 +# InvertRts +ERROR +ats59=1 +InvertRts=1 +OK +ats59=2 +ERROR +ats59=0 +InvertRts=0 +OK +ats59? +InvertRts=0 +# Invalid commands +ERROR +ats60=1 +ERROR +ats60? ERROR ats255=1 ERROR @@ -1118,6 +1148,8 @@ ATS11:FrequencyHop=1 ATS9:FrequencyMax=928.000 ATS8:FrequencyMin=902.000 ATS22:HeartBeatTimeout=5000 +ATS58:InvertCts=0 +ATS59:InvertRts=0 ATS12:MaxDwellTime=400 ATS26:MaxResends=2 ATS2:netID=192 From 8404fd1e263598738a6c2048a88a8af5224e4b00 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 28 Oct 2022 05:59:05 -1000 Subject: [PATCH 069/594] Add more checks for timeToHop in receive and transmit paths --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 88 ++++++++++++++++++++++++ Firmware/LoRaSerial_Firmware/System.ino | 8 +++ 2 files changed, 96 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 68b3d5e3..cf2fdaa8 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -606,6 +606,8 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); @@ -615,7 +617,11 @@ PacketType rcvDatagram() radioComputeWhitening(incomingBuffer, rxDataBytes); if (settings.encryptData == true) + { decryptBuffer(incomingBuffer, rxDataBytes); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } //Display the received data bytes if ((settings.dataScrambling || settings.encryptData) @@ -627,6 +633,8 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); @@ -644,6 +652,8 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); @@ -677,6 +687,8 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(receivedNetID, HEX); systemPrint(")"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); if (receivedNetID != settings.netID) { systemPrint(" expecting "); @@ -699,8 +711,12 @@ PacketType rcvDatagram() //Compute the CRC-16 value crc = 0xffff; + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); for (data = incomingBuffer; data < &incomingBuffer[rxDataBytes - 2]; data++) crc = crc16Table[*data ^ (uint8_t)(crc >> (16 - 8))] ^ (crc << 8); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); if ((incomingBuffer[rxDataBytes - 2] != (crc >> 8)) && (incomingBuffer[rxDataBytes - 1] != (crc & 0xff))) { @@ -713,6 +729,8 @@ PacketType rcvDatagram() systemPrint(incomingBuffer[rxDataBytes - 1], HEX); systemPrint(" expected 0x"); systemPrintln(crc, HEX); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); @@ -754,6 +772,8 @@ PacketType rcvDatagram() systemPrint(" CRC-16: 0x"); systemPrint(incomingBuffer[rxDataBytes - 2], HEX); systemPrintln(incomingBuffer[rxDataBytes - 1], HEX); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } /* @@ -784,6 +804,8 @@ PacketType rcvDatagram() systemPrint(" > "); systemPrint((int)rxDataBytes - minDatagramSize); systemPrintln(" received bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } badFrames++; return (DATAGRAM_BAD); @@ -810,6 +832,8 @@ PacketType rcvDatagram() systemPrint(ackNumber); systemPrint(" expecting "); systemPrintln(txAckNumber); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } badFrames++; return (DATAGRAM_BAD); @@ -840,6 +864,8 @@ PacketType rcvDatagram() systemPrint(ackNumber); systemPrint(" expecting "); systemPrintln(rmtTxAckNumber); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } badFrames++; return DATAGRAM_BAD; @@ -869,6 +895,8 @@ PacketType rcvDatagram() systemPrint(ackNumber); systemPrint(" expecting "); systemPrintln(rmtTxAckNumber); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } badFrames++; return DATAGRAM_BAD; @@ -908,6 +936,8 @@ PacketType rcvDatagram() systemPrint("Missing VC header bytes, received only "); systemPrint(rxDataBytes); systemPrintln(" bytes, expecting at least 3 bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } badFrames++; return DATAGRAM_BAD; @@ -929,6 +959,8 @@ PacketType rcvDatagram() systemPrintTimestamp(); systemPrint("Invalid source VC: "); systemPrintln(rxSrcVc); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } badFrames++; return DATAGRAM_BAD; @@ -944,6 +976,8 @@ PacketType rcvDatagram() systemPrint(*rxData); systemPrint(" expecting "); systemPrintln(rxDataBytes); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); badFrames++; if (vc) vc->badLength++; @@ -974,6 +1008,8 @@ PacketType rcvDatagram() systemPrintln("Unassigned"); else systemPrintln(rxSrcVc); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } } @@ -999,6 +1035,8 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printPktData && rxDataBytes) dumpBuffer(rxData, rxDataBytes); @@ -1031,6 +1069,8 @@ PacketType rcvDatagram() break; } } + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); //Process the packet datagramsReceived++; @@ -1053,6 +1093,9 @@ void transmitDatagram() uint8_t vcLength; VIRTUAL_CIRCUIT * vc; + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + //Parse the virtual circuit header vc = NULL; if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) @@ -1105,6 +1148,8 @@ void transmitDatagram() systemPrintln(); break; } + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } /* @@ -1130,6 +1175,8 @@ void transmitDatagram() systemPrint(" (0x"); systemPrint(length, HEX); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printPktData) dumpBuffer(&endOfTxData[-length], length); @@ -1145,6 +1192,8 @@ void transmitDatagram() systemPrint(" (0x"); systemPrint(headerBytes); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } //Add the netID if necessary @@ -1161,6 +1210,8 @@ void transmitDatagram() systemPrint(" (0x"); systemPrint(settings.netID, HEX); systemPrintln(")"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); } } @@ -1184,6 +1235,8 @@ void transmitDatagram() systemPrintTimestamp(); systemPrint(" SF6 Length: "); systemPrintln(length); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } } @@ -1203,6 +1256,8 @@ void transmitDatagram() systemPrintln("Unassigned"); else systemPrintln(srcVc); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } /* @@ -1226,12 +1281,16 @@ void transmitDatagram() //Compute the CRC-16 value crc = 0xffff; + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); for (txData = outgoingPacket; txData < endOfTxData; txData++) crc = crc16Table[*txData ^ (uint8_t)(crc >> (16 - 8))] ^ (crc << 8); *endOfTxData++ = (uint8_t)(crc >> 8); *endOfTxData++ = (uint8_t)(crc & 0xff); } txDatagramSize += trailerBytes; + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); //Display the trailer if (trailerBytes && settings.debugTransmit) @@ -1242,6 +1301,8 @@ void transmitDatagram() systemPrint(" (0x"); systemPrint(trailerBytes); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); //Display the CRC if (settings.enableCRC16 && (settings.printPktData || settings.debugReceive)) @@ -1250,6 +1311,8 @@ void transmitDatagram() systemPrint(" CRC-16: 0x"); systemPrint(endOfTxData[-2], HEX); systemPrintln(endOfTxData[-1], HEX); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } } @@ -1275,6 +1338,8 @@ void transmitDatagram() systemPrint(" (0x"); systemPrint(txDatagramSize, HEX); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printRfData) dumpBuffer(outgoingPacket, txDatagramSize); @@ -1282,7 +1347,11 @@ void transmitDatagram() //Encrypt the datagram if (settings.encryptData == true) + { encryptBuffer(outgoingPacket, txDatagramSize); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } //Scramble the datagram if (settings.dataScrambling == true) @@ -1298,6 +1367,8 @@ void transmitDatagram() systemPrint(" (0x"); systemPrint(txDatagramSize, HEX); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printRfData) dumpBuffer(outgoingPacket, txDatagramSize); @@ -1326,9 +1397,13 @@ void printControl(uint8_t value) systemPrintTimestamp(); systemPrint(" Control: 0x"); systemPrintln(value, HEX); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); systemPrintTimestamp(); systemPrint(" ACK # "); systemPrintln(value & 3); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); systemPrintTimestamp(); systemPrint(" datagramType "); if (control->datagramType < MAX_V2_DATAGRAM_TYPE) @@ -1338,6 +1413,8 @@ void printControl(uint8_t value) systemPrint("Unknown "); systemPrintln(control->datagramType); } + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); } @@ -1354,6 +1431,9 @@ void retransmitDatagram(VIRTUAL_CIRCUIT * vc) |<-------------------- txDatagramSize --------------------->| */ + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + //Display the transmitted frame bytes if (frameSentCount && (settings.printRfData || settings.debugTransmit)) { @@ -1365,12 +1445,16 @@ void retransmitDatagram(VIRTUAL_CIRCUIT * vc) systemPrint(" (0x"); systemPrint(txDatagramSize, HEX); systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); if (settings.printRfData) dumpBuffer(outgoingPacket, txDatagramSize); } //Transmit this frame + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); int state = radio.startTransmit(outgoingPacket, txDatagramSize); if (state == RADIOLIB_ERR_NONE) { @@ -1387,11 +1471,15 @@ void retransmitDatagram(VIRTUAL_CIRCUIT * vc) systemPrint("TX: frameAirTime "); systemPrint(frameAirTime); systemPrintln(" mSec"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); systemPrintTimestamp(); systemPrint("TX: responseDelay "); systemPrint(responseDelay); systemPrintln(" mSec"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); } frameAirTime += responseDelay; } diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 612a40cd..9c27f3f1 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -299,6 +299,8 @@ void radioComputeWhitening(uint8_t *buffer, uint16_t bufferSize) for (uint16_t j = 0 ; j < bufferSize ; j++) { + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); buffer[j] ^= WhiteningKeyLSB; for (uint8_t i = 0 ; i < 8 ; i++) @@ -418,6 +420,8 @@ void dumpBuffer(uint8_t * data, int length) { systemPrint(" "); systemPrint(*data++, HEX); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); } @@ -425,6 +429,8 @@ void dumpBuffer(uint8_t * data, int length) for (; index < displayWidth; index++) { systemPrint(" "); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); } systemPrint(" "); @@ -436,6 +442,8 @@ void dumpBuffer(uint8_t * data, int length) systemPrint(((byte[0] < ' ') || (byte[0] >= 0x7f)) ? "." : byte); } systemPrintln(); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); petWDT(); } } From 164226003036dc3cae0f7b10481e7d65df16e01b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 28 Oct 2022 07:39:28 -1000 Subject: [PATCH 070/594] Force link break timeout when waiting for ACK after HEARTBEAT --- Firmware/LoRaSerial_Firmware/States.ino | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 37610664..053bea26 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -714,6 +714,7 @@ void updateRadioState() xmitDatagramP2PHeartbeat(); resetHeartbeat(); + transmitTimer = datagramTimer; //Wait for heartbeat to transmit changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); @@ -909,6 +910,10 @@ void updateRadioState() } } + else if ((millis() - linkDownTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)) + //Break the link + v2BreakLink(); + //Retransmits are not getting through in a rational time else if (transmitTimer && ((millis() - transmitTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) //Break the link From 34990faf6806767de72a89145b93ef3575923e5f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 28 Oct 2022 07:43:15 -1000 Subject: [PATCH 071/594] In P2P always call v2BreakLink for consist behavior --- Firmware/LoRaSerial_Firmware/States.ino | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 053bea26..f5c0d937 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -965,14 +965,8 @@ void updateRadioState() changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } else - { //Failed to reach the other system, break the link - if (settings.debugDatagrams) - systemPrintln("---------- Link DOWN ----------"); - heartbeatTimer = millis(); - pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); //Fast ping - changeState(RADIO_P2P_LINK_DOWN); - } + v2BreakLink(); } break; From ba98db8b60fe94a0021a3032e430560d4d68785e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 28 Oct 2022 07:45:13 -1000 Subject: [PATCH 072/594] Always call resetHeartbeat instead of setting heartbeatTimer directly --- Firmware/LoRaSerial_Firmware/States.ino | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index f5c0d937..007aafab 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -51,7 +51,7 @@ void updateRadioState() petWDT(); //Start the TX timer: time to delay before transmitting the PING - heartbeatTimer = millis(); + resetHeartbeat(); pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); //Fast ping //Set all of the ACK numbers to zero @@ -390,7 +390,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK1_TIMEOUT); - heartbeatTimer = millis(); + resetHeartbeat(); pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); //Slow ping changeState(RADIO_P2P_LINK_DOWN); } @@ -445,7 +445,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK2_TIMEOUT); - heartbeatTimer = millis(); + resetHeartbeat(); pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); //Slow ping changeState(RADIO_P2P_LINK_DOWN); } From 3bc7009779012836eb29d8a1a21bc077ecb52c29 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 26 Oct 2022 05:55:25 -1000 Subject: [PATCH 073/594] Add virtual-circuit data flow Change data flow to support flow control through the radio stack. --- .../LoRaSerial_Firmware.ino | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b0a57e51..048d7189 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -221,6 +221,72 @@ bool rtsAsserted; //When RTS is asserted, host says it's ok to send data */ +/* Data Flow - Virtual Circuit + + USB or UART + | + | Flow control: RTS for UART + | Off: Buffer full + | On: Buffer drops below half full + V + serialReceiveBuffer + | + | Destination VC? + | + V Other + .-------------+--------------->+---------------->+------> Discard + VC_COMMAND | myVc | >= 0 | + | | VC_BROADCAST | + | | | + V | v + commandBuffer | radioTxBuffer + | | | + | Remote Command? | v + | | outgoingPacket + false V true | | + .-------------+-------------. | V + | | | Send to remote system + | V | | + | outgoingPacket | V + | | | incomingBuffer + | V | | + | Send to remote system | | + | | | | + | V | | + | incomingBuffer | | + | | | | + | V | | + | commandRXBuffer | | + | | | | + | V | | + | Command processing | | + | checkCommand | | + | | | | + | V | | + | commandTXBuffer | | + | | | | + | V | | + | outgoingPacket | | + | | | | + | V | | + | Send back to local system | | + | | | | + | V | | + | incomingBuffer | | + | | | | + | V V | + `-------------------------->+<---------------+<----------------' + | + V + serialTransmitBuffer + | + | Flow control: CTS for UART + | + V + USB or UART + +*/ + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - LEDs From 7e44ff3dddd1e811c278dc7bda1ae5867f333520 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 31 Oct 2022 12:49:10 -0600 Subject: [PATCH 074/594] Add linkUpTime to debug printing --- .../LoRaSerial_Firmware.ino | 2 ++ Firmware/LoRaSerial_Firmware/States.ino | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b0a57e51..c58dbb1e 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -260,6 +260,8 @@ uint32_t duplicateFrames; //Total number of duplicate frames received uint32_t lostFrames; //Total number of lost TX frames uint32_t linkFailures; //Total number of link failures uint32_t insufficientSpace; //Total number of times the buffer did not have enough space + +unsigned long lastLinkUpTime = 0; //Mark when link was first established //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - V2 Protocol diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 007aafab..f15829ac 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1840,10 +1840,27 @@ void changeState(RadioStates newState) else { if (radioStateTable[radioState].description) - systemPrintln(radioStateTable[radioState].description); + systemPrint(radioStateTable[radioState].description); else - systemPrintln(radioStateTable[radioState].name); + systemPrint(radioStateTable[radioState].name); } + + if (newState == RADIO_P2P_LINK_UP) + { + int seconds = (millis() - lastLinkUpTime) / 1000; + int minutes = seconds / 60; + seconds -= (minutes * 60); + + systemPrint(" LinkUptime: "); + + if (minutes < 10) systemPrint("0"); + systemPrint(minutes); + systemPrint(":"); + if (seconds < 10) systemPrint("0"); + systemPrint(seconds); + } + + systemPrintln(); } void v2BreakLink() @@ -1884,6 +1901,9 @@ void v2EnterLinkUp() //Stop the transmit timer transmitTimer = 0; + //Mark start time for uptime calculation + lastLinkUpTime = millis(); + //Start the receiver returnToReceiving(); changeState(RADIO_P2P_LINK_UP); From 5235c4d1a2da55a1ecc3176527a3d6dffa964e03 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 31 Oct 2022 14:13:38 -0600 Subject: [PATCH 075/594] Add short/long heartbeat Prevent pair from having even random heartbeat at same time. --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 22 ++++++---- Firmware/LoRaSerial_Firmware/States.ino | 52 ++++++++++++------------ 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index cf2fdaa8..d07ac61f 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -556,7 +556,7 @@ void xmitVcHeartbeat(int8_t addr, uint8_t * id) vcTxHeartbeatMillis = millis() - currentMillis; //Select a random for the next heartbeat - resetHeartbeat(); + setHeartbeatLong(); //Those who send a heartbeat or data have long time before next heartbeat. Those who send ACKs, have short wait to next heartbeat. } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -625,7 +625,7 @@ PacketType rcvDatagram() //Display the received data bytes if ((settings.dataScrambling || settings.encryptData) - && (settings.printRfData || settings.debugReceive)) + && (settings.printRfData || settings.debugReceive)) { systemPrintTimestamp(); systemPrint("RX: Unencrypted Frame "); @@ -718,7 +718,7 @@ PacketType rcvDatagram() if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); if ((incomingBuffer[rxDataBytes - 2] != (crc >> 8)) - && (incomingBuffer[rxDataBytes - 1] != (crc & 0xff))) + && (incomingBuffer[rxDataBytes - 1] != (crc & 0xff))) { //Display the packet contents if (settings.printPktData || settings.debugReceive) @@ -733,7 +733,7 @@ PacketType rcvDatagram() hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); + dumpBuffer(incomingBuffer, rxDataBytes); } badFrames++; return (DATAGRAM_BAD); @@ -1359,7 +1359,7 @@ void transmitDatagram() //Display the transmitted packet bytes if ((settings.printRfData || settings.debugTransmit) - && (settings.encryptData || settings.dataScrambling)) + && (settings.encryptData || settings.dataScrambling)) { systemPrintTimestamp(); systemPrint("TX: Encrypted Frame "); @@ -1529,8 +1529,16 @@ void syncChannelTimer(int offset) //This function resets the heartbeat time and re-rolls the random time //Call when something has happened (ACK received, etc) where clocks have been sync'd -void resetHeartbeat() +//Short/long times set to avoid two radios attempting to xmit heartbeat at same time +//Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. +void setHeartbeatShort() +{ + heartbeatTimer = millis(); + heartbeatRandomTime = random(settings.heartbeatTimeout * 2 / 10, settings.heartbeatTimeout / 2); //20-50% +} + +void setHeartbeatLong() { heartbeatTimer = millis(); - heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //20-100% + heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index f15829ac..86cf7cdb 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1,18 +1,18 @@ #define SAVE_TX_BUFFER() \ -{ \ - memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); \ - rexmtControl = txControl; \ - rexmtLength = txDatagramSize; \ - rexmtFrameSentCount = frameSentCount; \ -} + { \ + memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); \ + rexmtControl = txControl; \ + rexmtLength = txDatagramSize; \ + rexmtFrameSentCount = frameSentCount; \ + } #define RESTORE_TX_BUFFER() \ -{ \ - memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ - txControl = rexmtControl; \ - txDatagramSize = rexmtLength; \ - frameSentCount = rexmtFrameSentCount; \ -} + { \ + memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ + txControl = rexmtControl; \ + txDatagramSize = rexmtLength; \ + frameSentCount = rexmtFrameSentCount; \ + } void updateRadioState() { @@ -51,7 +51,7 @@ void updateRadioState() petWDT(); //Start the TX timer: time to delay before transmitting the PING - resetHeartbeat(); + setHeartbeatShort(); //Both radios start with short heartbeat period pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); //Fast ping //Set all of the ACK numbers to zero @@ -390,7 +390,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK1_TIMEOUT); - resetHeartbeat(); + setHeartbeatShort(); pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); //Slow ping changeState(RADIO_P2P_LINK_DOWN); } @@ -445,7 +445,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK2_TIMEOUT); - resetHeartbeat(); + setHeartbeatShort(); pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); //Slow ping changeState(RADIO_P2P_LINK_DOWN); } @@ -606,7 +606,7 @@ void updateRadioState() updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; - resetHeartbeat(); + setHeartbeatShort(); //We ack'd this heartbeat so be responsible for sending the next heartbeat triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); xmitDatagramP2PAck(); //Transmit ACK @@ -633,7 +633,7 @@ void updateRadioState() updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; - resetHeartbeat(); + setHeartbeatShort(); //We ack'd this data, so be responsible for sending the next heartbeat triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); xmitDatagramP2PAck(); //Transmit ACK @@ -713,7 +713,7 @@ void updateRadioState() { xmitDatagramP2PHeartbeat(); - resetHeartbeat(); + setHeartbeatLong(); //We're sending a heartbeat, so don't be the first to send next heartbeat transmitTimer = datagramTimer; //Wait for heartbeat to transmit @@ -785,12 +785,12 @@ void updateRadioState() break; case DATAGRAM_DATA_ACK: - syncChannelTimer(0); //Adjust freq hop ISR based on remote's remaining clock + syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock //Stop the transmit timer transmitTimer = 0; - resetHeartbeat(); //Extend time before next heartbeat + setHeartbeatLong(); //Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. //Display the signal strength if (settings.displayPacketQuality == true) @@ -1115,7 +1115,7 @@ void updateRadioState() RADIO_MP_WAIT_TX_PARAM_ACK_DONE | V - endTrainingClientServer + endTrainingClientServer | | Restore settings | @@ -1164,7 +1164,7 @@ void updateRadioState() case DATAGRAM_TRAINING_PARAMS: //Verify the IDs if ((memcmp(rxData, myUniqueId, UNIQUE_ID_BYTES) != 0) - && (memcmp(rxData, myUniqueId, UNIQUE_ID_BYTES) != 0)) + && (memcmp(rxData, myUniqueId, UNIQUE_ID_BYTES) != 0)) { triggerEvent(TRIGGER_BAD_PACKET); returnToReceiving(); @@ -1410,7 +1410,7 @@ void updateRadioState() //Transmit a HEARTBEAT if necessary else if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime) - && (receiveInProcess() == false)) + && (receiveInProcess() == false)) { //Send another heartbeat xmitVcHeartbeat(myVc, myUniqueId); @@ -1512,7 +1512,7 @@ void updateRadioState() //Transmit a HEARTBEAT if necessary else if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime) - && (receiveInProcess() == false)) + && (receiveInProcess() == false)) { //Send another heartbeat xmitVcHeartbeat(myVc, myUniqueId); @@ -1630,7 +1630,7 @@ void selectHeaderAndTrailerBytes() bool isLinked() { if ((radioState >= RADIO_P2P_LINK_UP) - && (radioState <= RADIO_P2P_LINK_UP_WAIT_ACK)) + && (radioState <= RADIO_P2P_LINK_UP_WAIT_ACK)) return (true); return (false); @@ -1885,7 +1885,7 @@ void v2EnterLinkUp() triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); startChannelTimer(); hopChannel(); //Leave home - resetHeartbeat(); + setHeartbeatLong(); //Start link with long heartbeat //Synchronize the ACK numbers rmtTxAckNumber = 0; From 3bfccca79132836eaab6782bc26092ad40c50ac0 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 31 Oct 2022 14:15:21 -0600 Subject: [PATCH 076/594] Fix short hop bug Radios were getting out of channel sync when a heartbeat had a hopChannel within it. --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 27 ++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d07ac61f..6cd8d43f 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1510,20 +1510,39 @@ void stopChannelTimer() //Given the remote unit's amount of channelTimer that has elapsed, //adjust our own channelTimer interrupt to be synchronized with the remote unit -void syncChannelTimer(int offset) +void syncChannelTimer() { triggerEvent(TRIGGER_SYNC_CHANNEL); uint16_t channelTimerElapsed; - memcpy(&channelTimerElapsed, &rxVcData[offset], sizeof(channelTimerElapsed)); + memcpy(&channelTimerElapsed, &rxVcData[0], sizeof(channelTimerElapsed)); channelTimerElapsed += ackAirTime; channelTimerElapsed += SYNC_PROCESSING_OVERHEAD; - if (channelTimerElapsed > settings.maxDwellTime) channelTimerElapsed -= settings.maxDwellTime; + int16_t remoteRemainingTime = settings.maxDwellTime - channelTimerElapsed; + + int16_t localRemainingTime = settings.maxDwellTime - (millis() - timerStart); //The amount of time we think we have left on this channel + + //If we have just hopped channels, and a sync comes in that is very small, it will incorrectly + //cause us to hop again, causing the clocks to be sync'd, but the channels to be one ahead. + //So, if our localRemainingTime is very large, and remoteRemainingTime is very small, then add + //the remoteRemainingTime to our localRemainingTime + if (remoteRemainingTime < (settings.maxDwellTime / 16) + && localRemainingTime > (settings.maxDwellTime - (settings.maxDwellTime / 16)) ) + { +// systemPrint("remoteRemainingTime: "); +// systemPrint(remoteRemainingTime); +// systemPrint(" localRemainingTime: "); +// systemPrint(localRemainingTime); +// systemPrintln(); + + triggerEvent(TRIGGER_SYNC_CHANNEL); //Double trigger + remoteRemainingTime = remoteRemainingTime + localRemainingTime; + } partialTimer = true; channelTimer.disableTimer(); - channelTimer.setInterval_MS(settings.maxDwellTime - channelTimerElapsed, channelTimerHandler); //Shorten our hardware timer to match our mate's + channelTimer.setInterval_MS(remoteRemainingTime, channelTimerHandler); //Adjust our hardware timer to match our mate's channelTimer.enableTimer(); } From 5908d478fbb51a3a0fb9da6d93b09b0d0feba239 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 31 Oct 2022 14:34:39 -0600 Subject: [PATCH 077/594] Fix negative in remaining channel time --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 26 ++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 6cd8d43f..ad0f3fd0 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1519,10 +1519,10 @@ void syncChannelTimer() channelTimerElapsed += ackAirTime; channelTimerElapsed += SYNC_PROCESSING_OVERHEAD; - int16_t remoteRemainingTime = settings.maxDwellTime - channelTimerElapsed; + int16_t remoteRemainingTime = settings.maxDwellTime - channelTimerElapsed; int16_t localRemainingTime = settings.maxDwellTime - (millis() - timerStart); //The amount of time we think we have left on this channel - + //If we have just hopped channels, and a sync comes in that is very small, it will incorrectly //cause us to hop again, causing the clocks to be sync'd, but the channels to be one ahead. //So, if our localRemainingTime is very large, and remoteRemainingTime is very small, then add @@ -1530,16 +1530,26 @@ void syncChannelTimer() if (remoteRemainingTime < (settings.maxDwellTime / 16) && localRemainingTime > (settings.maxDwellTime - (settings.maxDwellTime / 16)) ) { -// systemPrint("remoteRemainingTime: "); -// systemPrint(remoteRemainingTime); -// systemPrint(" localRemainingTime: "); -// systemPrint(localRemainingTime); -// systemPrintln(); - + // systemPrint("remoteRemainingTime: "); + // systemPrint(remoteRemainingTime); + // systemPrint(" localRemainingTime: "); + // systemPrint(localRemainingTime); + // systemPrintln(); + triggerEvent(TRIGGER_SYNC_CHANNEL); //Double trigger remoteRemainingTime = remoteRemainingTime + localRemainingTime; } + if (remoteRemainingTime < 0) + { + // systemPrint(" channelTimerElapsed: "); + // systemPrint(channelTimerElapsed); + // systemPrint("remoteRemainingTime: "); + // systemPrint(remoteRemainingTime); + // systemPrintln(); + remoteRemainingTime = 0; + } + partialTimer = true; channelTimer.disableTimer(); channelTimer.setInterval_MS(remoteRemainingTime, channelTimerHandler); //Adjust our hardware timer to match our mate's From 7713e25310745d493242d0a52f76dc9dbd4520c5 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 31 Oct 2022 14:35:07 -0600 Subject: [PATCH 078/594] Update sync processing overhead time --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index c58dbb1e..9842879e 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -123,7 +123,7 @@ const int trainWithDefaultsButtonTime = 5000; //ms press and hold before enterin SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 unsigned long timerStart = 0; //Tracks how long our timer has been running since last hop bool partialTimer = false; //After an ACK we reset and run a partial timer to sync units -const int SYNC_PROCESSING_OVERHEAD = -5; //Number of milliseconds it takes to compute clock deltas before sync'ing clocks +const int SYNC_PROCESSING_OVERHEAD = 3; //Number of milliseconds it takes to compute clock deltas before sync'ing clocks uint16_t petTimeoutHalf = 0; //Half the amount of time before WDT. Helps reduce amount of time spent petting. unsigned long lastPet = 0; //Remebers time of last WDT pet. From 2f81d66e052c8d88b76c77525a53950699e1788a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 31 Oct 2022 17:20:07 -1000 Subject: [PATCH 079/594] Remove unused trigger values --- Firmware/LoRaSerial_Firmware/settings.h | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 0e276061..72d518bb 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -309,14 +309,12 @@ enum // triggerEnable = 0xffffffff TRIGGER_CHANNEL_TIMER_ISR, //0 - TRIGGER_COMPLETE, TRIGGER_RADIO_RESET, TRIGGER_HOP_TIMER_START, TRIGGER_HOP_TIMER_STOP, TRIGGER_HEARTBEAT, TRIGGER_FREQ_CHANGE, TRIGGER_SYNC_CHANNEL, - TRIGGER_LINK_HEARTBEAT_DO_NOTHING, TRIGGER_LINK_SEND_ACK_FOR_DATA, TRIGGER_LINK_SEND_ACK_FOR_DUP, TRIGGER_LINK_RETRANSMIT, @@ -347,14 +345,10 @@ enum TRIGGER_TRAINING_CONTROL_PACKET, TRIGGER_TRAINING_DATA_PACKET, TRIGGER_TRAINING_NO_ACK, - TRIGGER_TRAINING_CLIENT_TX_PING, TRIGGER_TRAINING_CLIENT_TX_PING_DONE, TRIGGER_TRAINING_CLIENT_RX_PARAMS, - TRIGGER_TRAINING_CLIENT_TX_ACK, TRIGGER_TRAINING_CLIENT_TX_ACK_DONE, - TRIGGER_TRAINING_COMPLETE, TRIGGER_TRAINING_SERVER_RX, - TRIGGER_TRAINING_SERVER_TX_PARAMS, TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE, TRIGGER_TRAINING_SERVER_RX_ACK, TRIGGER_TRAINING_SERVER_STOPPED, From bdf0ec6f544ca4babc89153160444ea8d8dcefdf Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 28 Oct 2022 19:04:36 -1000 Subject: [PATCH 080/594] Add setting to enable alternate LED usage Add badCrc value to count bad CRC frames Add ATI26 to display the badCrc count Add ATS60 to select alternate LED usage rssi1LED - RX radio data rssi2LED - Radio link up rssi3LED - RSSI value (pulse width modulated) rssi4LED - TX radio data txLED (blue) - Bad frames rxLED (yellow) - Bad CRC frames --- Firmware/LoRaSerial_Firmware/Commands.ino | 7 ++ .../LoRaSerial_Firmware.ino | 23 ++++ Firmware/LoRaSerial_Firmware/RadioV2.ino | 10 +- Firmware/LoRaSerial_Firmware/States.ino | 1 + Firmware/LoRaSerial_Firmware/System.ino | 109 ++++++++++++++++-- Firmware/LoRaSerial_Firmware/settings.h | 1 + Firmware/Tools/Command_Validation_Script.txt | 9 +- .../Results/Command_Validation_Results.txt | 39 +++++-- 8 files changed, 179 insertions(+), 20 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 68f9885d..b66f21ca 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -181,6 +181,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI23 - Display the VC link failures"); systemPrintln(" ATI24 - Display the VC details"); systemPrintln(" ATI25 - Display the total insufficient buffer count"); + systemPrintln(" ATI26 - Display the total number of bad CRC frames"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(); @@ -349,6 +350,10 @@ bool commandAT(const char * commandString) systemPrint("Total insufficient buffer count: "); systemPrintln(insufficientSpace); break; + case ('6'): //ATI26 - Display the total number of bad CRC frames + systemPrint("Total number of bad CRC frames: "); + systemPrintln(badCrc); + break; } } @@ -691,6 +696,8 @@ const COMMAND_ENTRY commands[] = {58, 0, 1, 0, TYPE_BOOL, valInt, "InvertCts", &settings.invertCts}, {59, 0, 1, 0, TYPE_BOOL, valInt, "InvertRts", &settings.invertRts}, + {60, 0, 1, 0, TYPE_BOOL, valInt, "AlternateLedUsage", &settings.alternateLedUsage}, + //Define any user parameters starting at 255 decrementing towards 0 }; diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b0a57e51..df2cb7af 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -87,6 +87,18 @@ uint8_t pin_rssi4LED = PIN_UNDEFINED; uint8_t pin_boardID = PIN_UNDEFINED; uint8_t pin_trigger = PIN_UNDEFINED; + +#define ALT_LED_RX_DATA pin_rssi1LED //Green +#define ALT_LED_RADIO_LINK pin_rssi2LED //Green +#define ALT_LED_RSSI pin_rssi3LED //Green +#define ALT_LED_TX_DATA pin_rssi4LED //Green +#define ALT_LED_BAD_FRAMES pin_txLED //Blue +#define ALT_LED_BAD_CRC pin_rxLED //Yellow + +#define LED_ON HIGH +#define LED_OFF LOW + +#define ALT_LED_BLINK_MILLIS 15 //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Radio Library @@ -250,6 +262,13 @@ float frequencyCorrection = 0; //Adjust receive freq based on the last packet re volatile bool clearDIO1 = true; //Clear the DIO1 hop ISR when possible +//RSSI must be above these negative numbers for LED to illuminate +const int rssiLevelLow = -150; +const int rssiLevelMed = -70; +const int rssiLevelHigh = -50; +const int rssiLevelMax = -20; +int rssi; //Signal level + //Link quality metrics uint32_t datagramsSent; //Total number of datagrams sent uint32_t datagramsReceived; //Total number of datagrams received @@ -260,6 +279,7 @@ uint32_t duplicateFrames; //Total number of duplicate frames received uint32_t lostFrames; //Total number of lost TX frames uint32_t linkFailures; //Total number of link failures uint32_t insufficientSpace; //Total number of times the buffer did not have enough space +uint32_t badCrc; //Total number of bad CRC frames //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - V2 Protocol @@ -406,6 +426,9 @@ void loop() updateSerial(); //Store incoming and print outgoing + if (settings.alternateLedUsage) + updateLeds(); + updateRadioState(); //Ping/ack/send packets as needed if (clearDIO1) //Allow DIO1 hop ISR but use it only for debug diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index cf2fdaa8..8817f8dd 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -735,7 +735,7 @@ PacketType rcvDatagram() if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); } - badFrames++; + badCrc++; return (DATAGRAM_BAD); } } @@ -1075,6 +1075,10 @@ PacketType rcvDatagram() //Process the packet datagramsReceived++; linkDownTimer = millis(); + + //BLink the RX LED + if (settings.alternateLedUsage) + digitalWrite(ALT_LED_RX_DATA, LED_ON); return datagramType; } @@ -1491,6 +1495,10 @@ void retransmitDatagram(VIRTUAL_CIRCUIT * vc) } datagramTimer = millis(); //Move timestamp even if error + + //BLink the RX LED + if (settings.alternateLedUsage) + digitalWrite(ALT_LED_TX_DATA, LED_ON); } void startChannelTimer() diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 007aafab..34cc1d83 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -61,6 +61,7 @@ void updateRadioState() selectHeaderAndTrailerBytes(); //Initialize the radio + rssi = -200; radioSeed = radio.randomByte(); //Puts radio into standy-by state randomSeed(radioSeed); if ((settings.debug == true) || (settings.debugRadio == true)) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 9c27f3f1..83587913 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -450,13 +450,10 @@ void dumpBuffer(uint8_t * data, int length) void updateRSSI() { - //RSSI must be above these negative numbers for LED to illuminate - const int rssiLevelLow = -150; - const int rssiLevelMed = -70; - const int rssiLevelHigh = -50; - const int rssiLevelMax = -20; - - int rssi = radio.getRSSI(); + //Get the current signal level + rssi = radio.getRSSI(); + if (settings.alternateLedUsage) + return; //Set LEDs according to RSSI level if (rssi > rssiLevelLow) @@ -471,6 +468,9 @@ void updateRSSI() void setRSSI(uint8_t ledBits) { + if (settings.alternateLedUsage) + return; + if (ledBits & 0b0001) digitalWrite(pin_rssi1LED, HIGH); else @@ -494,6 +494,8 @@ void setRSSI(uint8_t ledBits) void txLED(bool illuminate) { + if (settings.alternateLedUsage) + return; if (pin_txLED != PIN_UNDEFINED) { if (illuminate == true) @@ -505,6 +507,8 @@ void txLED(bool illuminate) void rxLED(bool illuminate) { + if (settings.alternateLedUsage) + return; if (pin_rxLED != PIN_UNDEFINED) { if (illuminate == true) @@ -514,6 +518,97 @@ void rxLED(bool illuminate) } } +void updateLeds() +{ + uint32_t currentMillis; + static uint32_t previousMillis; + static uint32_t previousBadFrames; + static uint32_t badFramesMillis; + static uint32_t previousBadCrc; + static uint32_t badCrcMillis; + static int8_t rssiCount = 127; + static int8_t rssiValue; + + //Turn off the RX LED to end the blink + currentMillis = millis(); + if ((currentMillis - linkDownTimer) >= ALT_LED_BLINK_MILLIS) + digitalWrite(ALT_LED_RX_DATA, LED_OFF); + + //Turn off the TX LED to end the blink + currentMillis = millis(); + if ((currentMillis - datagramTimer) >= ALT_LED_BLINK_MILLIS) + digitalWrite(ALT_LED_TX_DATA, LED_OFF); + + //Blink the bad frames LED + if (badFrames != previousBadFrames) + { + previousBadFrames = badFrames; + badFramesMillis = currentMillis; + digitalWrite(ALT_LED_BAD_FRAMES, LED_ON); + } + else if (badFramesMillis && ((currentMillis - badFramesMillis) >= ALT_LED_BLINK_MILLIS)) + { + badFramesMillis = 0; + digitalWrite(ALT_LED_BAD_FRAMES, LED_OFF); + } + + //Blink the bad CRC or duplicate frames LED + if (settings.enableCRC16 && (badCrc != previousBadCrc)) + { + previousBadCrc = badCrc; + badCrcMillis = currentMillis; + digitalWrite(ALT_LED_BAD_CRC, LED_ON); + } + if ((!settings.enableCRC16) && (duplicateFrames != previousBadCrc)) + { + previousBadCrc = duplicateFrames; + badCrcMillis = currentMillis; + digitalWrite(ALT_LED_BAD_CRC, LED_ON); + } + else if (badCrcMillis && ((currentMillis - badCrcMillis) >= ALT_LED_BLINK_MILLIS)) + { + badCrcMillis = 0; + digitalWrite(ALT_LED_BAD_CRC, LED_OFF); + } + + //Update the link LED + switch (radioState) + { + //Turn off the LED + default: + digitalWrite(ALT_LED_RADIO_LINK, LED_OFF); + break; + + //Turn on the LED + case RADIO_P2P_LINK_UP: + case RADIO_P2P_LINK_UP_WAIT_ACK_DONE: + case RADIO_P2P_LINK_UP_WAIT_TX_DONE: + case RADIO_P2P_LINK_UP_WAIT_ACK: + case RADIO_P2P_LINK_UP_HB_ACK_REXMT: + digitalWrite(ALT_LED_RADIO_LINK, LED_ON); + break; + } + + //Update the RSSI LED + if (currentMillis != previousMillis) + { + if (rssiCount >= 16) + { + rssiValue = (150 + rssi) / 10; + rssiCount = 0; + if (rssiValue >= rssiCount) + digitalWrite(ALT_LED_RSSI, LED_ON); + } + + //Turn off the RSSI LED + else if (rssiValue < rssiCount++) + digitalWrite(ALT_LED_RSSI, LED_OFF); + } + + //Save the last millis value + previousMillis = currentMillis; +} + int stricmp(const char * str1, const char * str2) { char char1; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 0e276061..a978a7f1 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -461,6 +461,7 @@ typedef struct struct_settings { bool printLinkUpDown = false; //Print the link up and link down messages bool invertCts = false; //Invert the input of CTS bool invertRts = false; //Invert the output of RTS + bool alternateLedUsage = false; //Enable alternate LED usage //Add new parameters immediately before this line //-- Add commands to set the parameters diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 3b4f3fc1..7e9aa401 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -34,6 +34,7 @@ ati23 ati24 ati25 ati26 +ati27 ati0 ats28=0 @@ -533,10 +534,16 @@ ats59=2 ats59=0 ats59? -# Invalid commands +# AlternateLedUsage ats60=1 +ats60=2 +ats60=0 ats60? +# Invalid commands +ats61=1 +ats61? + ats255=1 ats255? diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index 8258a712..cc35aad6 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -49,6 +49,7 @@ ati? ATI23 - Display the VC link failures ATI24 - Display the VC details ATI25 - Display the total insufficient buffer count + ATI26 - Display the total number of bad CRC frames # Allow leading white space (space, tab, cr, lf) ERROR ati @@ -59,35 +60,35 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati2 2.0 ati3 --105 +-4 ati4 -29 +122 ati5 506 ati6 37782141A665734E4475672AE6308308 ati7 -2 +10 ati8 -FB25B80B5730305020312E50021A0AFF +E5D60B0B4A34555020312E34212815FF ati9 -Total datagrams sent: 36 +Total datagrams sent: 3 ati10 -Total datagrams received: 34 +Total datagrams received: 1 ati11 -Total frames sent: 34 +Total frames sent: 1 ati12 -Total frames sent: 34 +Total frames sent: 1 ati13 Total bad frames received: 0 ati14 Total duplicate frames received: 0 ati15 -Total lost TX frames: 201 +Total lost TX frames: 1 ati16 Maximum datagram size: 253 ati17 -Total link failures: 1 +Total link failures: 0 ati18 VC 0 frames sent: 0 ati19 @@ -105,6 +106,8 @@ VC 0: Not valid! ati25 Total insufficient buffer count: 0 ati26 +Total number of bad CRC frames: 0 +ati27 ERROR ati0 ATS0:SerialSpeed=57600 @@ -167,6 +170,7 @@ ATS56:TrainingKey=537061726B46756E547261696E696E67 ATS57:PrintLinkUpDown=0 ATS58:InvertCts=0 ATS59:InvertRts=0 +ATS60:AlternateLedUsage=0 ats28=0 OK # SerialSpeed @@ -1107,11 +1111,23 @@ InvertRts=0 OK ats59? InvertRts=0 -# Invalid commands +# AlternateLedUsage ERROR ats60=1 +AlternateLedUsage=1 +OK +ats60=2 ERROR +ats60=0 +AlternateLedUsage=0 +OK ats60? +AlternateLedUsage=0 +# Invalid commands +ERROR +ats61=1 +ERROR +ats61? ERROR ats255=1 ERROR @@ -1119,6 +1135,7 @@ ats255? ERROR ati0 ATS1:AirSpeed=4800 +ATS60:AlternateLedUsage=0 ATS24:AutoTune=0 ATS13:Bandwidth=500.00 ATS52:ClientRetryInterval=2 From ddabca6046f93a0cbed7179e64ec40f8f6224d64 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 29 Oct 2022 13:10:31 -1000 Subject: [PATCH 081/594] Rename serialReceiveBuffer to radioTxBuffer --- .../LoRaSerial_Firmware.ino | 14 +- Firmware/LoRaSerial_Firmware/Serial.ino | 152 +++++++++--------- Firmware/LoRaSerial_Firmware/States.ino | 6 +- 3 files changed, 91 insertions(+), 81 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index df2cb7af..644a131b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -147,14 +147,16 @@ const uint8_t escapeCharacter = '+'; const uint8_t maxEscapeCharacters = 3; //Number of characters needed to enter command mode const uint8_t responseDelayDivisor = 4; //Add on to max response time after packet has been sent. Factor of 2. 8 is ok. 4 is good. A smaller number increases the delay. -//Buffer to store bytes incoming from serial before broadcasting over LoRa -uint8_t serialReceiveBuffer[1024 * 4]; //Bytes received from UART waiting to be RF transmitted. Buffer up to 1s of bytes at 4k -uint8_t serialTransmitBuffer[1024 * 4]; //Bytes received from RF waiting to be printed out UART. Buffer up to 1s of bytes at 4k +//Buffer to store bytes for transmission via the long range radio +uint16_t radioTxHead = 0; +uint16_t radioTxTail = 0; +uint8_t radioTxBuffer[1024 * 3]; + +//Buffer to store bytes to be sent to the USB or serial ports uint16_t txHead = 0; uint16_t txTail = 0; -uint16_t rxHead = 0; -uint16_t rxTail = 0; +uint8_t serialTransmitBuffer[1024 * 4]; //Bytes received from RF waiting to be printed out UART. Buffer up to 1s of bytes at 4k unsigned long lastByteReceived_ms = 0; //Track when last transmission was. Send partial buffer once time has expired. //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -189,7 +191,7 @@ bool rtsAsserted; //When RTS is asserted, host says it's ok to send data .-------------+--------------------------. | | V v - commandBuffer serialReceiveBuffer + commandBuffer radioTxBuffer | | | Remote Command? v | outgoingPacket diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 6be821fd..061c3baf 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -2,13 +2,6 @@ //Serial RX - Data arriving at the USB or serial port //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Return number of bytes sitting in the serial receive buffer -uint16_t availableRXBytes() -{ - if (rxHead >= rxTail) return (rxHead - rxTail); - return (sizeof(serialReceiveBuffer) - rxTail + rxHead); -} - //Returns true if CTS is asserted (high = host says it's ok to send data) bool isCTS() { @@ -17,53 +10,6 @@ bool isCTS() return (digitalRead(pin_cts) == HIGH) ^ settings.invertCts; } -//If we have data to send, get the packet ready -//Return true if new data is ready to be sent -bool processWaitingSerial(bool sendNow) -{ - //Push any available data out - if (availableRXBytes() >= maxDatagramSize) - { - if (settings.debugRadio) - systemPrintln("Sending max frame"); - readyOutgoingPacket(availableRXBytes()); - return (true); - } - - //Check if we should send out a partial frame - else if (sendNow || (availableRXBytes() > 0 && (millis() - lastByteReceived_ms) >= settings.serialTimeoutBeforeSendingFrame_ms)) - { - if (settings.debugRadio) - systemPrintln("Sending partial frame"); - readyOutgoingPacket(availableRXBytes()); - return (true); - } - return (false); -} - -//Send a portion of the serialReceiveBuffer to outgoingPacket -void readyOutgoingPacket(uint16_t bytesToSend) -{ - uint16_t length; - if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; - - //Determine the number of bytes to send - length = 0; - if ((rxTail + bytesToSend) > sizeof(serialReceiveBuffer)) - { - //Copy the first portion of the buffer - length = sizeof(serialReceiveBuffer) - rxTail; - memcpy(&outgoingPacket[headerBytes], &serialReceiveBuffer[rxTail], length); - rxTail = 0; - } - - //Copy the remaining portion of the buffer - memcpy(&outgoingPacket[headerBytes + length], &serialReceiveBuffer[rxTail], bytesToSend - length); - rxTail += bytesToSend - length; - rxTail %= sizeof(serialReceiveBuffer); - endOfTxData += bytesToSend; -} - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Serial TX - Data being sent to the USB or serial port //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -105,6 +51,67 @@ void updateRTS(bool assertRTS) digitalWrite(pin_rts, assertRTS ^ settings.invertRts); } +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Radio TX - Data to provide to the long range radio +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Return number of bytes sitting in the radio TX buffer +uint16_t availableRadioTXBytes() +{ + if (radioTxHead >= radioTxTail) return (radioTxHead - radioTxTail); + return (sizeof(radioTxBuffer) - radioTxTail + radioTxHead); +} + +//If we have data to send, get the packet ready +//Return true if new data is ready to be sent +bool processWaitingSerial(bool sendNow) +{ + uint16_t dataBytes; + + //Push any available data out + dataBytes = availableRadioTXBytes(); + if (dataBytes >= maxDatagramSize) + { + if (settings.debugRadio) + systemPrintln("Sending max frame"); + readyOutgoingPacket(dataBytes); + return (true); + } + + //Check if we should send out a partial frame + else if (sendNow || (dataBytes > 0 && (millis() - lastByteReceived_ms) >= settings.serialTimeoutBeforeSendingFrame_ms)) + { + if (settings.debugRadio) + systemPrintln("Sending partial frame"); + readyOutgoingPacket(dataBytes); + return (true); + } + return (false); +} + +//Send a portion of the radioTxBuffer to outgoingPacket +void readyOutgoingPacket(uint16_t bytesToSend) +{ + uint16_t length; + if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; + + //Determine the number of bytes to send + length = 0; + if ((radioTxTail + bytesToSend) > sizeof(radioTxBuffer)) + { + //Copy the first portion of the buffer + length = sizeof(radioTxBuffer) - radioTxTail; + memcpy(&outgoingPacket[headerBytes], &radioTxBuffer[radioTxTail], length); + radioTxTail = 0; + } + + //Copy the remaining portion of the buffer + memcpy(&outgoingPacket[headerBytes + length], &radioTxBuffer[radioTxTail], bytesToSend - length); + radioTxTail += bytesToSend - length; + radioTxTail %= sizeof(radioTxBuffer); + endOfTxData += bytesToSend; +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Command RX - Remote command data received from a remote system //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -172,7 +179,7 @@ void updateSerial() int x; //Assert RTS when there is enough space in the receive buffer - if ((!rtsAsserted) && (availableRXBytes() < (sizeof(serialReceiveBuffer) / 2)) + if ((!rtsAsserted) && (availableRadioTXBytes() < (sizeof(radioTxBuffer) / 2)) && (availableTXBytes() < (sizeof(serialTransmitBuffer) / 4))) updateRTS(true); @@ -204,7 +211,7 @@ void updateSerial() if (timeToHop == true) hopChannel(); //Deassert RTS when the buffer gets full - if (rtsAsserted && (sizeof(serialReceiveBuffer) - availableRXBytes()) < 32) + if (rtsAsserted && (sizeof(radioTxBuffer) - availableRadioTXBytes()) < 32) updateRTS(false); byte incoming = systemRead(); @@ -286,8 +293,9 @@ void updateSerial() if (settings.echo == true) systemWrite(incoming); - serialReceiveBuffer[rxHead++] = incoming; //Push char to holding buffer - rxHead %= sizeof(serialReceiveBuffer); + //This data byte will be sent over the long range radio + radioTxBuffer[radioTxHead++] = incoming; + radioTxHead %= sizeof(radioTxBuffer); } //End process rx buffer } //End Serial.available() rxLED(false); //Turn off LED @@ -327,7 +335,7 @@ void updateSerial() uint8_t vcSerialMsgGetLengthByte() { //Get the length byte for the received serial message - return serialReceiveBuffer[rxTail]; + return radioTxBuffer[radioTxTail]; } //Get the destination virtual circuit byte from the serial buffer @@ -336,10 +344,10 @@ uint8_t vcSerialMsgGetVcDest() uint16_t index; //Get the destination address byte - index = rxTail + 1; - if (index >= sizeof(serialReceiveBuffer)) - index -= sizeof(serialReceiveBuffer); - return serialReceiveBuffer[index]; + index = radioTxTail + 1; + if (index >= sizeof(radioTxBuffer)) + index -= sizeof(radioTxBuffer); + return radioTxBuffer[index]; } //Determine if received serial data may be sent to the remote system @@ -356,13 +364,13 @@ bool vcSerialMessageReceived() break; //Wait until at least one byte is available - if (!availableRXBytes()) + if (!availableRadioTXBytes()) //No data available break; //Verify that the entire message is in the serial buffer msgLength = vcSerialMsgGetLengthByte(); - if (availableRXBytes() < msgLength) + if (availableRadioTXBytes() < msgLength) //The entire message is not in the buffer break; @@ -371,9 +379,9 @@ bool vcSerialMessageReceived() if (msgLength > maxDatagramSize) { //Discard this message, it is too long to transmit over the radio link - rxTail += msgLength; - if (rxTail >= sizeof(serialReceiveBuffer)) - rxTail -= sizeof(serialReceiveBuffer); + radioTxTail += msgLength; + if (radioTxTail >= sizeof(radioTxBuffer)) + radioTxTail -= sizeof(radioTxBuffer); //Nothing to do for invalid addresses or the broadcast address if ((vcDest >= MAX_VC) || (vcDest == VC_BROADCAST)) @@ -395,9 +403,9 @@ bool vcSerialMessageReceived() } //Discard this message - rxTail += msgLength; - if (rxTail >= sizeof(serialReceiveBuffer)) - rxTail -= sizeof(serialReceiveBuffer); + radioTxTail += msgLength; + if (radioTxTail >= sizeof(radioTxBuffer)) + radioTxTail -= sizeof(radioTxBuffer); break; } @@ -452,7 +460,7 @@ void resetSerial() } while ((millis() - lastCharacterReceived) < delayTime); //Empty the buffers - rxHead = rxTail; + radioTxHead = radioTxTail; txHead = txTail; commandRXHead = commandRXTail; commandTXHead = commandTXTail; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 34cc1d83..3b7329ef 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -685,7 +685,7 @@ void updateRadioState() heartbeatTimeout = ((millis() - heartbeatTimer) > heartbeatRandomTime); //Check for time to send serial data - if (availableRXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) + if (availableRadioTXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) { triggerEvent(TRIGGER_LINK_DATA_XMIT); xmitDatagramP2PData(); @@ -1040,7 +1040,7 @@ void updateRadioState() //If the radio is available, send any data in the serial buffer over the radio if (receiveInProcess() == false) { - if (availableRXBytes()) //If we have bytes + if (availableRadioTXBytes()) //If we have bytes { if (processWaitingSerial(false) == true) //If we've hit a frame size or frame-timed-out { @@ -1877,7 +1877,7 @@ void v2EnterLinkUp() txAckNumber = 0; //Discard any previous data - rxTail = rxHead; + radioTxTail = radioTxHead; txTail = txHead; commandRXTail = commandRXHead; commandTXTail = commandTXHead; From e734c9c7d8b9cc99b020bcc2677cf543c246e84f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 29 Oct 2022 13:59:08 -1000 Subject: [PATCH 082/594] Use common input buffering and flow control Add serialReceiveBuffer to buffer incoming serial data --- .../LoRaSerial_Firmware.ino | 10 +++++ Firmware/LoRaSerial_Firmware/Serial.ino | 38 ++++++++++++++----- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 644a131b..d880dda6 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -147,6 +147,10 @@ const uint8_t escapeCharacter = '+'; const uint8_t maxEscapeCharacters = 3; //Number of characters needed to enter command mode const uint8_t responseDelayDivisor = 4; //Add on to max response time after packet has been sent. Factor of 2. 8 is ok. 4 is good. A smaller number increases the delay. +//Buffer to receive serial data from the USB or serial ports +uint16_t rxHead = 0; +uint16_t rxTail = 0; +uint8_t serialReceiveBuffer[1024]; //Buffer to store bytes for transmission via the long range radio uint16_t radioTxHead = 0; @@ -184,6 +188,12 @@ bool rtsAsserted; //When RTS is asserted, host says it's ok to send data /* Data Flow - Point-to-Point and Multi-Point USB or UART + | + | Flow control: RTS for UART + | Off: Buffer full + | On: Buffer drops below half full + V + serialReceiveBuffer | | inCommandMode? | diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 061c3baf..b2ac9e87 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -2,6 +2,13 @@ //Serial RX - Data arriving at the USB or serial port //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Return number of bytes sitting in the serial receive buffer +uint16_t availableRXBytes() +{ + if (rxHead >= rxTail) return (rxHead - rxTail); + return (sizeof(serialReceiveBuffer) - rxTail + rxHead); +} + //Returns true if CTS is asserted (high = host says it's ok to send data) bool isCTS() { @@ -179,7 +186,7 @@ void updateSerial() int x; //Assert RTS when there is enough space in the receive buffer - if ((!rtsAsserted) && (availableRadioTXBytes() < (sizeof(radioTxBuffer) / 2)) + if ((!rtsAsserted) && (availableRXBytes() < (sizeof(serialReceiveBuffer) / 2)) && (availableTXBytes() < (sizeof(serialTransmitBuffer) / 4))) updateRTS(true); @@ -211,11 +218,30 @@ void updateSerial() if (timeToHop == true) hopChannel(); //Deassert RTS when the buffer gets full - if (rtsAsserted && (sizeof(radioTxBuffer) - availableRadioTXBytes()) < 32) + if (rtsAsserted && (sizeof(serialReceiveBuffer) - availableRXBytes()) < 32) updateRTS(false); byte incoming = systemRead(); + if (settings.echo == true) + systemWrite(incoming); + + serialReceiveBuffer[rxHead++] = incoming; //Push char to holding buffer + rxHead %= sizeof(serialReceiveBuffer); + } //End Serial.available() + rxLED(false); //Turn off LED + + //Process the serial data + while (availableRXBytes() && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - 1)) + && (transactionComplete == false)) + { + //Take a break if there are ISRs to attend to + petWDT(); + if (timeToHop == true) hopChannel(); + + byte incoming = serialReceiveBuffer[rxTail++]; + rxTail %= sizeof(serialReceiveBuffer); + //Process serial into either rx buffer or command buffer if (inCommandMode == true) { @@ -266,9 +292,6 @@ void updateSerial() escapeCharsReceived++; if (escapeCharsReceived == maxEscapeCharacters) { - if (settings.echo == true) - systemWrite(incoming); - systemPrintln("\r\nOK"); inCommandMode = true; //Allow AT parsing. Prevent received RF data from being printed. @@ -290,15 +313,11 @@ void updateSerial() escapeCharsReceived = 0; //Update timeout check for escape char and partial frame } - if (settings.echo == true) - systemWrite(incoming); - //This data byte will be sent over the long range radio radioTxBuffer[radioTxHead++] = incoming; radioTxHead %= sizeof(radioTxBuffer); } //End process rx buffer } //End Serial.available() - rxLED(false); //Turn off LED //Process any remote commands sitting in buffer if (availableRXCommandBytes() && inCommandMode == false) @@ -460,6 +479,7 @@ void resetSerial() } while ((millis() - lastCharacterReceived) < delayTime); //Empty the buffers + rxHead = rxTail; radioTxHead = radioTxTail; txHead = txTail; commandRXHead = commandRXTail; From 9beee4878bc5920802c4ac90ac485b8a19a606eb Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Tue, 1 Nov 2022 20:37:52 -0600 Subject: [PATCH 083/594] Fix hops when clock syncing. Print link uptime during debug. --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 95 ++++++++++++++++-------- Firmware/LoRaSerial_Firmware/States.ino | 18 ++++- 2 files changed, 78 insertions(+), 35 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index ad0f3fd0..3587fa5c 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -292,9 +292,9 @@ void xmitDatagramP2PAck() int ackLength; uint8_t * ackStart = endOfTxData; - uint16_t channelTimerElapsed = millis() - timerStart; - memcpy(endOfTxData, &channelTimerElapsed, sizeof(channelTimerElapsed)); - endOfTxData += sizeof(channelTimerElapsed); + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); //Verify the ACK length ackLength = endOfTxData - ackStart; @@ -1508,52 +1508,81 @@ void stopChannelTimer() triggerEvent(TRIGGER_HOP_TIMER_STOP); } -//Given the remote unit's amount of channelTimer that has elapsed, +//Given the remote unit's number of ms before its next hop, //adjust our own channelTimer interrupt to be synchronized with the remote unit void syncChannelTimer() { - triggerEvent(TRIGGER_SYNC_CHANNEL); + int16_t msToNextHopRemote; + memcpy(&msToNextHopRemote, &rxVcData[0], sizeof(msToNextHopRemote)); + msToNextHopRemote -= ackAirTime; + msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; //Can be negative - uint16_t channelTimerElapsed; - memcpy(&channelTimerElapsed, &rxVcData[0], sizeof(channelTimerElapsed)); - channelTimerElapsed += ackAirTime; - channelTimerElapsed += SYNC_PROCESSING_OVERHEAD; + int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - timerStart); - int16_t remoteRemainingTime = settings.maxDwellTime - channelTimerElapsed; + //Precalculate large/small time amounts + uint16_t smallAmount = settings.maxDwellTime / 8; + uint16_t largeAmount = settings.maxDwellTime - smallAmount; - int16_t localRemainingTime = settings.maxDwellTime - (millis() - timerStart); //The amount of time we think we have left on this channel + int16_t msToNextHop = msToNextHopRemote; //By default, we will adjust our clock to match our mate's - //If we have just hopped channels, and a sync comes in that is very small, it will incorrectly - //cause us to hop again, causing the clocks to be sync'd, but the channels to be one ahead. - //So, if our localRemainingTime is very large, and remoteRemainingTime is very small, then add - //the remoteRemainingTime to our localRemainingTime - if (remoteRemainingTime < (settings.maxDwellTime / 16) - && localRemainingTime > (settings.maxDwellTime - (settings.maxDwellTime / 16)) ) + //Below are the edge cases that occur when a hop occurs near ACK reception + + //msToNextHopLocal is small and msToNextHopRemote is negative + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in negative then the remote has hopped + //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + if (msToNextHopLocal < smallAmount && msToNextHopRemote <= 0) + { + hopChannel(); + msToNextHop = msToNextHopRemote + settings.maxDwellTime; + } + + //msToNextHopLocal is large and msToNextHopRemote is negative + //If we just hopped (msToNextHopLocal is large), and msToNextHopRemote comes in negative then the remote has hopped + //No need to hop. Adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + else if (msToNextHopLocal > largeAmount && msToNextHopRemote <= 0) + { + msToNextHop = msToNextHopRemote + settings.maxDwellTime; + } + + //msToNextHopLocal is small and msToNextHopRemote is large + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in large then the remote has hopped + //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote) + else if (msToNextHopLocal < smallAmount && msToNextHopRemote > largeAmount) { - // systemPrint("remoteRemainingTime: "); - // systemPrint(remoteRemainingTime); - // systemPrint(" localRemainingTime: "); - // systemPrint(localRemainingTime); - // systemPrintln(); - - triggerEvent(TRIGGER_SYNC_CHANNEL); //Double trigger - remoteRemainingTime = remoteRemainingTime + localRemainingTime; + hopChannel(); + msToNextHop = msToNextHopRemote; + } + + //msToNextHopLocal is large and msToNextHopRemote is large + //If we just hopped (msToNextHopLocal is large), and a msToNextHopRemote comes in large then the remote has hopped + //Then adjust our clock to the remote's next hop (msToNextHopRemote) + else if (msToNextHopLocal > largeAmount && msToNextHopRemote > largeAmount) + { + msToNextHop = msToNextHopRemote; } - if (remoteRemainingTime < 0) + //msToNextHopLocal is large and msToNextHopRemote is small + //If we just hopped (msToNextHopLocal is large), and a msToNextHopRemote comes in small then the remote is about to hop + //Then adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + else if (msToNextHopLocal > largeAmount && msToNextHopRemote < smallAmount) { - // systemPrint(" channelTimerElapsed: "); - // systemPrint(channelTimerElapsed); - // systemPrint("remoteRemainingTime: "); - // systemPrint(remoteRemainingTime); - // systemPrintln(); - remoteRemainingTime = 0; + msToNextHop = msToNextHopRemote + settings.maxDwellTime; + } + + //msToNextHopLocal is small and msToNextHopRemote is small + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in small then the remote is about to hop + //Then adjust our clock to the remote's next hop (msToNextHopRemote) + else if (msToNextHopLocal < smallAmount && msToNextHopRemote < smallAmount) + { + msToNextHop = msToNextHopRemote; } partialTimer = true; channelTimer.disableTimer(); - channelTimer.setInterval_MS(remoteRemainingTime, channelTimerHandler); //Adjust our hardware timer to match our mate's + channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's channelTimer.enableTimer(); + + triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug } //This function resets the heartbeat time and re-rolls the random time diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 86cf7cdb..a392debd 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1847,12 +1847,26 @@ void changeState(RadioStates newState) if (newState == RADIO_P2P_LINK_UP) { - int seconds = (millis() - lastLinkUpTime) / 1000; - int minutes = seconds / 60; + unsigned int seconds = (millis() - lastLinkUpTime) / 1000; + unsigned int minutes = seconds / 60; seconds -= (minutes * 60); + unsigned int hours = minutes / 60; + minutes -= (hours * 60); + unsigned int days = hours / 24; + hours -= (days * 24); systemPrint(" LinkUptime: "); + if (days < 10) systemPrint(" "); + if (days) + systemPrint(days); + else + systemPrint(" "); + systemPrint(" "); + + if (hours < 10) systemPrint(" "); + systemPrint(hours); + systemPrint(":"); if (minutes < 10) systemPrint("0"); systemPrint(minutes); systemPrint(":"); From 06ae4f34aeecefc341ae48c6b87d5e8c4bc6a8b5 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 3 Nov 2022 13:41:24 -0600 Subject: [PATCH 084/594] Enable hardware CRC by default --- Firmware/LoRaSerial_Firmware/Radio.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index eaf4913c..776e17bc 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -99,6 +99,9 @@ void configureRadio() if (radio.setPreambleLength(settings.radioPreambleLength) == RADIOLIB_ERR_INVALID_PREAMBLE_LENGTH) success = false; + if (radio.setCRC(true) == RADIOLIB_ERR_INVALID_CRC_CONFIGURATION) //Enable hardware CRC + success = false; + //SF6 requires an implicit header. We will transmit 255 bytes for most packets and 2 bytes for ACK packets. if (settings.radioSpreadFactor == 6) { From 870b8d0c2dabfb5efb2f17ed2335d68592727e15 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 3 Nov 2022 13:42:35 -0600 Subject: [PATCH 085/594] Prevent double hop if ISR triggers during clock sync The hop ISR may occur while we are forcing a hop (case A and C). Reset timeToHop as needed. --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 8 ++++++++ Firmware/LoRaSerial_Firmware/settings.h | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index b21e9ac1..27982925 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1533,6 +1533,8 @@ void syncChannelTimer() int16_t msToNextHop = msToNextHopRemote; //By default, we will adjust our clock to match our mate's + bool resetHop = false; //The hop ISR may occur while we are forcing a hop (case A and C). Reset timeToHop as needed. + //Below are the edge cases that occur when a hop occurs near ACK reception //msToNextHopLocal is small and msToNextHopRemote is negative @@ -1542,6 +1544,7 @@ void syncChannelTimer() { hopChannel(); msToNextHop = msToNextHopRemote + settings.maxDwellTime; + resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } //msToNextHopLocal is large and msToNextHopRemote is negative @@ -1559,6 +1562,7 @@ void syncChannelTimer() { hopChannel(); msToNextHop = msToNextHopRemote; + resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } //msToNextHopLocal is large and msToNextHopRemote is large @@ -1588,6 +1592,10 @@ void syncChannelTimer() partialTimer = true; channelTimer.disableTimer(); channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's + + if(resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. + timeToHop = false; + channelTimer.enableTimer(); triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 1a6346ea..dee40048 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -443,7 +443,7 @@ typedef struct struct_settings { uint8_t radioProtocolVersion = 2; //Select the radio protocol bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds bool debugDatagrams = false; //Print the datagrams - uint16_t overheadTime = 10; ////ms added to ack and datagram times before ACK timeout occurs + uint16_t overheadTime = 10; //ms added to ack and datagram times before ACK timeout occurs bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive bool displayRealMillis = false; //true = Display the millis value without offset, false = use offset bool trainingServer = false; //Default to being a client From 1beb9db476ddb71a8b6a0d07cf8d45be000af3c3 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 3 Nov 2022 13:45:14 -0600 Subject: [PATCH 086/594] Set bootloader protect fuse bits in batch program --- Binaries/Bootloader_Combined/batch_program.bat | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Binaries/Bootloader_Combined/batch_program.bat b/Binaries/Bootloader_Combined/batch_program.bat index 3a7e9293..8bacc213 100644 --- a/Binaries/Bootloader_Combined/batch_program.bat +++ b/Binaries/Bootloader_Combined/batch_program.bat @@ -10,10 +10,13 @@ if [%1]==[] goto usage atprogram.exe -t atmelice -i swd -cl 20mhz -d atsamd21g18a chiperase program -f %1 --verify +rem Lock bootloader +atprogram.exe -t atmelice -i swd -cl 20mhz -d atsamd21g18a write --fuses --offset 0x804000 --values 0xFAC7E0D8 + @echo Done programming! Ready for next board. @pause goto loop :usage -@echo Missing the hex file. Ex: batch_program.bat LoRaSerial_Firmware_v10_Combined.hex +@echo Missing the hex file. Ex: batch_program.bat LoRaSerial_Firmware_v10_Combined.hex \ No newline at end of file From 3ba0a302414d3503bd0c3f9d889ce1eac56685fe Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 4 Nov 2022 16:43:04 -0600 Subject: [PATCH 087/594] Relate pingtime to ackTime For lower airspeeds, pings can happen faster than an ack has time to be received. This makes ping times related to ack times. --- Firmware/LoRaSerial_Firmware/States.ino | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b92fbbcb..21a33c91 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -52,7 +52,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING setHeartbeatShort(); //Both radios start with short heartbeat period - pingRandomTime = random(settings.maxDwellTime / 10, settings.maxDwellTime / 2); //Fast ping + pingRandomTime = random(ackAirTime, ackAirTime * 2); //Fast ping //Set all of the ACK numbers to zero *(uint8_t *)(&txControl) = 0; @@ -392,7 +392,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK1_TIMEOUT); setHeartbeatShort(); - pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); //Slow ping + pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); //Slow ping changeState(RADIO_P2P_LINK_DOWN); } } @@ -447,7 +447,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK2_TIMEOUT); setHeartbeatShort(); - pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); //Slow ping + pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); //Slow ping changeState(RADIO_P2P_LINK_DOWN); } } From a5af8ead16344ad72bc7b1035ef47d7025bd451f Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 4 Nov 2022 16:43:34 -0600 Subject: [PATCH 088/594] Allow reverse hop table walking --- Firmware/LoRaSerial_Firmware/Radio.ino | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 776e17bc..03e0fc36 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -207,7 +207,7 @@ void returnToReceiving() } if (state != RADIOLIB_ERR_NONE) { - if ((settings.debug == true) || (settings.debugRadio == true)) + if ((settings.debug == true) || (settings.debugRadio == true)) { systemPrint("Receive failed: "); systemPrintln(state); @@ -370,13 +370,32 @@ uint16_t myRand() //Move to the next channel //This is called when the FHSS interrupt is received //at the beginning and during of a transmission or reception + void hopChannel() +{ + hopChannel(true); //Move forward +} + +void hopChannelReverse() +{ + hopChannel(false); //Move backward +} + +void hopChannel(bool moveForwardThroughTable) { timeToHop = false; triggerEvent(TRIGGER_FREQ_CHANGE); - channelNumber++; - channelNumber %= settings.numberOfChannels; + if (moveForwardThroughTable) + { + channelNumber++; + channelNumber %= settings.numberOfChannels; + } + else + { + if (channelNumber == 0) channelNumber = settings.numberOfChannels; + channelNumber--; + } //Select the new frequency float frequency; From fa32798430d93e65700aab6b6570c2cee17148d2 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 5 Nov 2022 16:20:15 -0600 Subject: [PATCH 089/594] Add CRC checking to radio receive --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 33 +++++++++++++++++++----- Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 27982925..d3c776fe 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -577,7 +577,28 @@ PacketType rcvDatagram() //Get the received datagram framesReceived++; - radio.readData(incomingBuffer, MAX_PACKET_SIZE); + int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); + + if (state == RADIOLIB_ERR_NONE) + { + //Do nothing + } + else if (state == RADIOLIB_ERR_CRC_MISMATCH) + { + if (setting.debug == true) + systemPrintln("Receive CRC error!"); + return (DATAGRAM_CRC_ERROR); + } + else + { + if (settings.debug == true) + { + systemPrint("Receive error: ")); + systemPrintln(state); + } + return (DATAGRAM_BAD); + } + rxDataBytes = radio.getPacketLength(); rxData = incomingBuffer; @@ -736,7 +757,7 @@ PacketType rcvDatagram() dumpBuffer(incomingBuffer, rxDataBytes); } badCrc++; - return (DATAGRAM_BAD); + return (DATAGRAM_CRC_ERROR); } } @@ -1533,8 +1554,8 @@ void syncChannelTimer() int16_t msToNextHop = msToNextHopRemote; //By default, we will adjust our clock to match our mate's - bool resetHop = false; //The hop ISR may occur while we are forcing a hop (case A and C). Reset timeToHop as needed. - + bool resetHop = false; //The hop ISR may occur while we are forcing a hop (case A and C). Reset timeToHop as needed. + //Below are the edge cases that occur when a hop occurs near ACK reception //msToNextHopLocal is small and msToNextHopRemote is negative @@ -1593,9 +1614,9 @@ void syncChannelTimer() channelTimer.disableTimer(); channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's - if(resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. + if (resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. timeToHop = false; - + channelTimer.enableTimer(); triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index dee40048..2e341c5f 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -145,6 +145,7 @@ typedef enum //Common datagram types DATAGRAM_BAD, + DATAGRAM_CRC_ERROR, DATAGRAM_NETID_MISMATCH, DATAGRAM_DUPLICATE, } PacketType; From d731fd4f430e73afbbe1ca43062678dd6dbca638 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 5 Nov 2022 16:24:05 -0600 Subject: [PATCH 090/594] Move packet quality printing to subfunction --- Firmware/LoRaSerial_Firmware/States.ino | 93 +++---------------------- Firmware/LoRaSerial_Firmware/System.ino | 15 ++++ 2 files changed, 25 insertions(+), 83 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 21a33c91..ce90160c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -165,18 +165,7 @@ void updateRadioState() break; case DATAGRAM_P2P_TRAINING_PING: - //Display the signal strength - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } + printPacketQuality(); triggerEvent(TRIGGER_TRAINING_CONTROL_PACKET); @@ -559,18 +548,7 @@ void updateRadioState() break; case DATAGRAM_DUPLICATE: - //Display the signal strength - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } + printPacketQuality(); updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -591,18 +569,7 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - //Display the signal strength - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } + printPacketQuality(); updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -615,18 +582,7 @@ void updateRadioState() break; case DATAGRAM_DATA: - //Display the signal strength - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } + printPacketQuality(); //Place the data in the serial output buffer serialBufferOutput(rxData, rxDataBytes); @@ -793,18 +749,8 @@ void updateRadioState() setHeartbeatLong(); //Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. - //Display the signal strength - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } + printPacketQuality(); + updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -824,18 +770,8 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - //Display the signal strength - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } + printPacketQuality(); + updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -1011,17 +947,8 @@ void updateRadioState() break; case DATAGRAM_DATAGRAM: - if (settings.displayPacketQuality == true) - { - systemPrintln(); - systemPrint("R:"); - systemPrint(radio.getRSSI()); - systemPrint("\tS:"); - systemPrint(radio.getSNR()); - systemPrint("\tfE:"); - systemPrint(radio.getFrequencyError()); - systemPrintln(); - } + printPacketQuality(); + //Place the data in the serial output buffer serialBufferOutput(rxData, rxDataBytes); diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 83587913..66ee4d80 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -623,3 +623,18 @@ int stricmp(const char * str1, const char * str2) //Return the difference between the two strings return char1 - char2; } + +void printPacketQuality() +{ + if (settings.displayPacketQuality == true) + { + systemPrintln(); + systemPrint("R:"); + systemPrint(radio.getRSSI()); + systemPrint("\tS:"); + systemPrint(radio.getSNR()); + systemPrint("\tfE:"); + systemPrint(radio.getFrequencyError()); + systemPrintln(); + } +} From 84ffc6de89d14006873bcfde8ea8b28863d21486 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 8 Nov 2022 07:54:41 -1000 Subject: [PATCH 091/594] Add AT-* commands to support commands by parameter name Example: at-echo=1 or at-echo? The parameter name that follows at- is converted to uppercase and compared with the uppercase of the parameter names in the command tables. --- Firmware/LoRaSerial_Firmware/Commands.ino | 103 ++-- Firmware/LoRaSerial_Firmware/System.ino | 15 + Firmware/LoRaSerial_Firmware/settings.h | 14 + Firmware/Tools/Command_Validation_Script.txt | 269 ++++++----- .../Results/Command_Validation_Results.txt | 449 +++++++++--------- 5 files changed, 471 insertions(+), 379 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index b66f21ca..339d9efd 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -19,20 +19,6 @@ enum { TYPE_U32, }; -typedef bool (* VALIDATION_ROUTINE)(void * value, uint32_t valMin, uint32_t valMax); - -typedef struct _COMMAND_ENTRY -{ - uint8_t number; - uint32_t minValue; - uint32_t maxValue; - uint8_t digits; - uint8_t type; - VALIDATION_ROUTINE validate; - const char * name; - void * setting; -} COMMAND_ENTRY; - typedef bool (* COMMAND_ROUTINE)(const char * commandString); typedef struct { @@ -75,6 +61,8 @@ bool commandAT(const char * commandString) systemPrintln(" ATT - Enter training mode"); systemPrintln(" ATX - Stop the training server"); systemPrintln(" ATZ - Reboot the radio"); + systemPrintln(" AT-Param=xxx - Set parameter's value to xxx by name (Param)"); + systemPrintln(" AT-Param? - Print parameter's current value by name (Param)"); systemPrintln(" AT&F - Restore factory settings"); systemPrintln(" AT&W - Save current settings to NVM"); break; @@ -411,6 +399,7 @@ bool sendRemoteCommand(const char * commandString) const COMMAND_PREFIX prefixTable[] = { {"ATS", commandSet}, + {"AT-", commandSetByName}, {"AT", commandAT}, {"RT", sendRemoteCommand}, }; @@ -645,7 +634,7 @@ const COMMAND_ENTRY commands[] = {15, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &settings.radioCodingRate}, {16, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &settings.radioSyncWord}, {17, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &settings.radioPreambleLength}, - {18, 0, MAX_VC, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, + {18, 0, MAX_VC-1,0, TYPE_U8, valInt, "CmdVC", &cmdVc}, {19, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &settings.serialTimeoutBeforeSendingFrame_ms}, {20, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, @@ -672,8 +661,8 @@ const COMMAND_ENTRY commands[] = {38, 1, 255, 0, TYPE_U8, valInt, "TriggerWidth", &settings.triggerWidth}, {39, 0, 1, 0, TYPE_BOOL, valInt, "TriggerWidthIsMultiplier", &settings.triggerWidthIsMultiplier}, - {40, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable: 31-0", &settings.triggerEnable}, - {41, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable2: 63-32", &settings.triggerEnable2}, + {40, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_31-0", &settings.triggerEnable}, + {41, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_63-32", &settings.triggerEnable2}, {42, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &settings.debugReceive}, {43, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, {44, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, @@ -778,27 +767,87 @@ void commandDisplay(uint8_t number, bool printName) systemPrintln(); } +//Set or display the parameter by name +bool commandSetByName(const char * commandString) +{ + const char * buffer; + const COMMAND_ENTRY * command; + int index; + int nameLength; + int number; + const char * param; + int paramLength; + int table; + + //Determine the parameter name length + param = &commandString[3]; + buffer = param; + paramLength = 0; + while (*buffer && (*buffer != '=') && (*buffer != '?')) + { + buffer++; + paramLength += 1; + } + + command = NULL; + for (index = 0; index < commandCount; index++) + { + nameLength = strlen(commands[index].name); + if (nameLength == paramLength) + { + //Compare the parameter names + if (strnicmp(param, commands[index].name, nameLength) == 0) + { + command = &commands[index]; + break; + } + } + } + + //Verify that the parameter was found + if (!command) + //Report the error + return false; + + //Process this command + return commandSetOrDisplayValue(command, buffer); +} + //Set or display the command bool commandSet(const char * commandString) { const char * buffer; const COMMAND_ENTRY * command; - double doubleSettingValue; int index; uint32_t number; + + //Validate the command number + buffer = commandGetNumber(&commandString[3], &number); + command = NULL; + for (index = 0; index < commandCount; index++) + if (number == commands[index].number) + { + command = &commands[index]; + break; + } + + //Verify that the parameter was found + if (!command) + //Report the error + return false; + + //Process this command + return commandSetOrDisplayValue(command, buffer); +} + +//Set or display the command +bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer) +{ + double doubleSettingValue; uint32_t settingValue; bool valid; do { - //Validate the command number - buffer = commandGetNumber(&commandString[3], &number); - for (index = 0; index < commandCount; index++) - if (number == commands[index].number) - break; - if (index >= commandCount) - break; - command = &commands[index]; - //Is this a display request if (strcmp(buffer, "?") == 0) { diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 66ee4d80..09e2b651 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -624,6 +624,21 @@ int stricmp(const char * str1, const char * str2) return char1 - char2; } +int strnicmp(const char * str1, const char * str2, int length) +{ + char char1; + char char2; + + //Do a case insensitive comparison between the two strings + do { + char1 = toupper(*str1++); + char2 = toupper(*str2++); + } while (char1 && (char1 == char2) && --length); + + //Return the difference between the two strings + return char1 - char2; +} + void printPacketQuality() { if (settings.displayPacketQuality == true) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 2e341c5f..0c863fce 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -392,6 +392,20 @@ typedef struct _CONTROL_U8 uint8_t filler : 2; } CONTROL_U8; +typedef bool (* VALIDATION_ROUTINE)(void * value, uint32_t valMin, uint32_t valMax); + +typedef struct _COMMAND_ENTRY +{ + uint8_t number; + uint32_t minValue; + uint32_t maxValue; + uint8_t digits; + uint8_t type; + VALIDATION_ROUTINE validate; + const char * name; + void * setting; +} COMMAND_ENTRY; + //These are all the settings that can be set on Serial Terminal Radio. It's recorded to NVM. typedef struct struct_settings { uint16_t sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 7e9aa401..08169f84 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -37,10 +37,9 @@ ati26 ati27 ati0 -ats28=0 +at-PrintParameterName=0 -# SerialSpeed -ats0=0 +at-SerialSpeed=0 ats0=1 ats0=40 ats0=50 @@ -51,7 +50,7 @@ ats0=300 ats0=400 ats0=600 ats0=1200 -ats0=2400 +at-SerialSpeed=2400 ats0=4800 ats0=9600 ats0=14400 @@ -78,10 +77,10 @@ ats0=3686400 ats0=4000000 ats0=57600 ats0? +at-SerialSpeed? -# AirSpeed ats1=1 -ats1=40 +at-AirSpeed=40 ats1=50 ats1=110 ats1=134 @@ -117,50 +116,50 @@ ats1=3686400 ats1=4000000 ats1=0 ats1? +at-AirSpeed? -# NetID -ats2=0 +at-NetID=0 ats2=255 ats2=192 ats2? +at-NetID? -# OperatingMode -ats3=0 +at-OperatingMode=0 ats3=2 ats3=3 ats3=1 ats3? +at-OperatingMode? -# EncryptData -ats4=0 +at-EncryptData=0 ats4=2 ats4=1 ats4? +at-EncryptData? -# EncryptionKey -ats5=00112233445566778899aabbccddeeff +at-EncryptionKey=00112233445566778899aabbccddeeff ats5? ats5=00112233445566778899aabbccddeef ats5=00112233445566778899aabbccddeefff ats5=00112233445566778899aabbccddeefg ats5=37782141A665734E4475672AE6308308 ats5? +at-EncryptionKey? -# DataScrambling -ats6=1 +at-DataScrambling=1 ats6=2 ats6=0 ats6? +at-DataScrambling? -# TxPower ats7=13 -ats7=14 +at-TxPower=14 ats7=31 ats7=30 ats7? +at-TxPower? -# FrequencyMin -ats9=928 +at-FrequencyMin=928 ats8=0 ats8=928.1 ats8=928 @@ -168,43 +167,43 @@ ats8=927.9 ats8=901.9 ats8=902 ats8? +at-FrequencyMin? -# FrequencyMax ats9=0 ats9=901.9 -ats9=902 +at-FrequencyMin=902 ats9=902.1 ats9=927.9 ats9=928.1 ats9=928 ats9? +at-FrequencyMin? -# NumberOfChannels ats10=0 -ats10=1 +at-NumberOfChannels=1 ats10=255 ats10=256 ats10=50 ats10? +at-NumberOfChannels? -# FrequencyHop -ats11=0 +at-FrequencyHop=0 ats11=2 ats11=1 ats11? +at-FrequencyHop? -# MaxDwellTime ats12=9 -ats12=10 +at-MaxDwellTime=10 ats12=65535 ast12=65536 ats12=400 ats12? +at-MaxDwellTime? -# Bandwidth -ats1=4800 +at-AirSpeed=4800 -ats13=0 +at-Bandwidth=0 ats13=10.4 ats13=15.6 ats13=20 @@ -215,7 +214,7 @@ ats13=125 ats13=250 ats13=500 -ats1=0 +at-AirSpeed=0 ats13=0 ats13=10.4 @@ -228,317 +227,320 @@ ats13=125 ats13=250 ats13=500 ats13? +at-Bandwidth? -ats1=4800 +at-AirSpeed=4800 -# SpreadFactor -ats14=5 +at-SpreadFactor=5 ats14=6 ats14=12 ats14=13 ats14=9 -ats1=0 +at-AirSpeed=0 ats14=5 -ats14=6 +at-SpreadFactor=6 ats14=12 ats14=13 ats14=9 ats14? +at-SpreadFactor? -# CodingRate ats15=4 -ats15=5 +at-CodingRate=5 ats15=9 ats15=8 -ats1=4800 +at-AirSpeed=4800 ats15=4 ats15=5 ats15=9 ats15=8 ats15? +at-CodingRate? -# SyncWord -ats16=0 +at-SyncWord=0 ats16=255 ats16=256 ats16=18 ats16? +at-SyncWord? -# PreambleLength ats17=5 -ats17=6 +at-PreambleLength=6 ats17=255 ats17=256 ats17=65535 ats17=65536 ats17=8 ats17? +at-PreambleLength? -# FrameSize -ats18=0 +at-CmdVC=0 +ats18=7; +ats18=8; ats18? +at-CmdVC? -# FrameTimeout ats19=9 -ats19=10 +at-FrameTimeout=10 ats19=2000 ats19=2001 ats19=50 ats19? +at-FrameTimeout? -# Debug -ats20=1 +at-Debug=1 ats20=2 ats20=0 ats20? +at-Debug? -# Echo -ats21=1 +at-Echo=1 ats21=2 ats21=0 ats21? +at-Echo? -# HeartBeatTimeout ats22=249 -ats22=250 +at-HeartBeatTimeout=250 ats22=65535 ats22=65536 ats22=5000 ats22? +at-HeartBeatTimeout? -# FlowControl -ats23=1 +at-FlowControl=1 ats23=2 ats23=0 ats23? +at-FlowControl? -# AutoTune -ats24=1 +at-AutoTune=1 ats24=2 ats24=0 ats24? +at-AutoTune? -# DisplayPacketQuality -ats25=1 +at-DisplayPacketQuality=1 ats25=2 ats25=0 ats25? +at-DisplayPacketQuality? -# MaxResends -ats26=0 +at-MaxResends=0 ats26=255 ats26=256 ats26=2 ats26? +at-MaxResends? -# SortParametersByName -ats27=0 +at-SortParametersByName=0 ats27=2 ats27=1 ats27? +at-SortParametersByName? -# ParameterName -ats28=0 +at-PrintParameterName=0 ats28=2 ats28=1 ats28? +at-PrintParameterName? -# PrintFrequency -ats29=1 +at-PrintFrequency=1 ats29=2 ats29=0 ats29? +at-PrintFrequency? -# DebugRadio -ats30=1 +at-DebugRadio=1 ats30=2 ats30=0 ats30? +at-DebugRadio? -# DebugStates -ats31=1 +at-DebugStates=1 ats31=2 ats31=0 ats31? +at-DebugStates? -# DebugTraining -ats32=1 +at-DebugTraining=1 ats32=2 ats32=0 ats32? +at-DebugTraining? -# DebugTrigger -ats33=1 +at-DebugTrigger=1 ats33=2 ats33=0 ats33? +at-DebugTrigger? -# UsbSerialWait -ats34=1 +at-UsbSerialWait=1 ats34=2 ats34=0 ats34? +at-UsbSerialWait? -# PrintRfData -ats35=1 +at-PrintRfData=1 ats35=2 ats35=0 ats35? +at-PrintRfData? -# PrintPktData -ats36=1 +at-PrintPktData=1 ats36=2 ats36=0 ats36? +at-PrintPktData? -# VerifyRxNetID -ats37=1 +at-VerifyRxNetID=1 ats37=2 ats37=0 ats37? +at-VerifyRxNetID? -# TriggerWidth ats38=0 -ats38=1 +at-TriggerWidth=1 ats38=255 ats38=256 ats38=25 ats38? +at-TriggerWidth? -# TriggerWidthIsMultiplier -ats39=0 +at-TriggerWidthIsMultiplier=0 ats39=2 ats39=1 ats39? +at-TriggerWidthIsMultiplier? -# TriggerEnable: 31-0 -ats40=0 +at-TriggerEnable_31-0=0 ats40=4294967295 ats40? +at-TriggerEnable_31-0? -# TriggerEnable: 63-32 -ats41=0 +at-TriggerEnable_63-32=0 ats41=4294967295 ats41? +at-TriggerEnable_63-32? -# DebugReceive -ats42=1 +at-DebugReceive=1 ats42=2 ats42=0 ats42? +at-DebugReceive? -# DebugTransmit -ats43=1 +at-DebugTransmit=1 ats43=2 ats43=0 ats43? +at-DebugTransmit? -# PrintTxErrors -ats44=1 +at-PrintTxErrors=1 ats44=2 ats44=0 ats44? +at-PrintTxErrors? -# radioProtocolVersion ats45=0 ats45=1 -ats45=2 +at-radioProtocolVersion=2 ats45=3 ats45? +at-radioProtocolVersion? -# PrintTimestamp -ats46=1 +at-PrintTimestamp=1 ats46=2 ats46=0 ats46? +at-PrintTimestamp? -# debugDatagrams -ats47=1 +at-debugDatagrams=1 ats47=2 ats47=0 ats47? +at-debugDatagrams? -# txAckMillis -ats48=1 -ats48=2 -ats48=0 +at-OverHeadtime=0 +ats48=1000 +ats48=1001 ats48? +at-OverHeadtime? -# enableCRC16 -ats49=1 +at-enableCRC16=1 ats49=2 ats49=0 ats49? +at-enableCRC16? -# DisplayRealMillis -ats50=1 +at-DisplayRealMillis=1 ats50=2 ats50=0 ats50? +at-DisplayRealMillis? -# TrainingServer -ats51=1 +at-TrainingServer=1 ats51=2 ats51=0 ats51? +at-TrainingServer? -# ClientRetryInterval -ats52=1 +at-ClientRetryInterval=1 ats52=2 ats52=0 ats52? +at-ClientRetryInterval? -# CopyDebug -ats53=1 +at-CopyDebug=1 ats53=2 ats53=0 ats53? +at-CopyDebug? -# CopySerial -ats54=1 +at-CopySerial=1 ats54=2 ats54=0 ats54? +at-CopySerial? -# CopyTriggers -ats55=1 +at-CopyTriggers=1 ats55=2 ats55=0 ats55? +at-CopyTriggers? -# TrainingKey -ats56=01020304050607080910111213141516 +at-TrainingKey=01020304050607080910111213141516 ats56=0102030405060708091011121314151 ats56=010203040506070809101112131415160 ats56? +at-TrainingKey? -# PrintLinkUpDown -ats57=1 +at-PrintLinkUpDown=1 ats57=2 ats57=0 ats57? +at-PrintLinkUpDown? -# InvertCts -ats58=1 +at-InvertCts=1 ats58=2 ats58=0 ats58? +at-InvertCts? -# InvertRts -ats59=1 +at-InvertRts=1 ats59=2 ats59=0 ats59? +at-InvertRts? -# AlternateLedUsage -ats60=1 +at-AlternateLedUsage=1 ats60=2 ats60=0 ats60? +at-AlternateLedUsage? # Invalid commands ats61=1 @@ -547,6 +549,9 @@ ats61? ats255=1 ats255? +at-foobar=1 +at-foobar? + ati0 at&f diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index cc35aad6..9009488c 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -20,6 +20,8 @@ Command summary: ATT - Enter training mode ATX - Stop the training server ATZ - Reboot the radio + AT-Param=xxx - Set parameter's value to xxx by name (Param) + AT-Param? - Print parameter's current value by name (Param) AT&F - Restore factory settings AT&W - Save current settings to NVM ati? @@ -60,31 +62,31 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati2 2.0 ati3 --4 +-157 ati4 -122 +205 ati5 506 ati6 37782141A665734E4475672AE6308308 ati7 -10 +0 ati8 -E5D60B0B4A34555020312E34212815FF +B2A99BF24A34555020312E34162815FF ati9 -Total datagrams sent: 3 +Total datagrams sent: 678 ati10 -Total datagrams received: 1 +Total datagrams received: 0 ati11 -Total frames sent: 1 +Total frames sent: 0 ati12 -Total frames sent: 1 +Total frames sent: 0 ati13 Total bad frames received: 0 ati14 Total duplicate frames received: 0 ati15 -Total lost TX frames: 1 +Total lost TX frames: 0 ati16 Maximum datagram size: 253 ati17 @@ -150,8 +152,8 @@ ATS36:PrintPktData=0 ATS37:VerifyRxNetID=0 ATS38:TriggerWidth=25 ATS39:TriggerWidthIsMultiplier=1 -ATS40:TriggerEnable: 31-0=-1 -ATS41:TriggerEnable2: 63-32=-1 +ATS40:TriggerEnable_31-0=-1 +ATS41:TriggerEnable_63-32=-1 ATS42:DebugReceive=0 ATS43:DebugTransmit=0 ATS44:PrintTxErrors=0 @@ -171,11 +173,9 @@ ATS57:PrintLinkUpDown=0 ATS58:InvertCts=0 ATS59:InvertRts=0 ATS60:AlternateLedUsage=0 -ats28=0 +at-PrintParameterName=0 OK -# SerialSpeed -ERROR -ats0=0 +at-SerialSpeed=0 ERROR ats0=1 ERROR @@ -197,7 +197,7 @@ ats0=600 ERROR ats0=1200 ERROR -ats0=2400 +at-SerialSpeed=2400 OK ats0=4800 OK @@ -251,11 +251,11 @@ ats0=57600 OK ats0? 57600 -# AirSpeed -ERROR +at-SerialSpeed? +57600 ats1=1 ERROR -ats1=40 +at-AirSpeed=40 OK ats1=50 ERROR @@ -327,9 +327,9 @@ ats1=0 OK ats1? 0 -# NetID -ERROR -ats2=0 +at-AirSpeed? +0 +at-NetID=0 OK ats2=255 OK @@ -337,9 +337,9 @@ ats2=192 OK ats2? 192 -# OperatingMode -ERROR -ats3=0 +at-NetID? +192 +at-OperatingMode=0 OK ats3=2 OK @@ -349,9 +349,9 @@ ats3=1 OK ats3? 1 -# EncryptData -ERROR -ats4=0 +at-OperatingMode? +1 +at-EncryptData=0 OK ats4=2 ERROR @@ -359,9 +359,9 @@ ats4=1 OK ats4? 1 -# EncryptionKey -ERROR -ats5=00112233445566778899aabbccddeeff +at-EncryptData? +1 +at-EncryptionKey=00112233445566778899aabbccddeeff OK ats5? 00112233445566778899AABBCCDDEEFF @@ -375,9 +375,9 @@ ats5=37782141A665734E4475672AE6308308 OK ats5? 37782141A665734E4475672AE6308308 -# DataScrambling -ERROR -ats6=1 +at-EncryptionKey? +37782141A665734E4475672AE6308308 +at-DataScrambling=1 OK ats6=2 ERROR @@ -385,11 +385,11 @@ ats6=0 OK ats6? 0 -# TxPower -ERROR +at-DataScrambling? +0 ats7=13 ERROR -ats7=14 +at-TxPower=14 OK ats7=31 ERROR @@ -397,9 +397,9 @@ ats7=30 OK ats7? 30 -# FrequencyMin -ERROR -ats9=928 +at-TxPower? +30 +at-FrequencyMin=928 OK ats8=0 ERROR @@ -415,13 +415,13 @@ ats8=902 OK ats8? 902.000 -# FrequencyMax -ERROR +at-FrequencyMin? +902.000 ats9=0 ERROR ats9=901.9 ERROR -ats9=902 +at-FrequencyMin=902 OK ats9=902.1 OK @@ -433,11 +433,11 @@ ats9=928 OK ats9? 928.000 -# NumberOfChannels -ERROR +at-FrequencyMin? +902.000 ats10=0 ERROR -ats10=1 +at-NumberOfChannels=1 OK ats10=255 OK @@ -447,9 +447,9 @@ ats10=50 OK ats10? 50 -# FrequencyHop -ERROR -ats11=0 +at-NumberOfChannels? +50 +at-FrequencyHop=0 OK ats11=2 ERROR @@ -457,11 +457,11 @@ ats11=1 OK ats11? 1 -# MaxDwellTime -ERROR +at-FrequencyHop? +1 ats12=9 ERROR -ats12=10 +at-MaxDwellTime=10 OK ats12=65535 OK @@ -471,12 +471,12 @@ ats12=400 OK ats12? 400 -# Bandwidth -ERROR -ats1=4800 +at-MaxDwellTime? +400 +at-AirSpeed=4800 Warning: AirSpeed override of bandwidth, spread factor, and coding rate OK -ats13=0 +at-Bandwidth=0 OK ats13=10.4 AirSpeed is overriding @@ -505,7 +505,7 @@ ERROR ats13=500 AirSpeed is overriding ERROR -ats1=0 +at-AirSpeed=0 OK ats13=0 ERROR @@ -529,12 +529,12 @@ ats13=500 OK ats13? 500.00 -ats1=4800 +at-Bandwidth? +500.00 +at-AirSpeed=4800 Warning: AirSpeed override of bandwidth, spread factor, and coding rate OK -# SpreadFactor -ERROR -ats14=5 +at-SpreadFactor=5 AirSpeed is overriding ERROR ats14=6 @@ -549,11 +549,11 @@ ERROR ats14=9 AirSpeed is overriding ERROR -ats1=0 +at-AirSpeed=0 OK ats14=5 ERROR -ats14=6 +at-SpreadFactor=6 OK ats14=12 OK @@ -563,17 +563,17 @@ ats14=9 OK ats14? 9 -# CodingRate -ERROR +at-SpreadFactor? +9 ats15=4 ERROR -ats15=5 +at-CodingRate=5 OK ats15=9 ERROR ats15=8 OK -ats1=4800 +at-AirSpeed=4800 Warning: AirSpeed override of bandwidth, spread factor, and coding rate OK ats15=4 @@ -590,9 +590,9 @@ AirSpeed is overriding ERROR ats15? 8 -# SyncWord -ERROR -ats16=0 +at-CodingRate? +8 +at-SyncWord=0 OK ats16=255 OK @@ -602,11 +602,11 @@ ats16=18 OK ats16? 18 -# PreambleLength -ERROR +at-SyncWord? +18 ats17=5 ERROR -ats17=6 +at-PreambleLength=6 OK ats17=255 OK @@ -620,17 +620,21 @@ ats17=8 OK ats17? 8 -# FrameSize -ERROR -ats18=0 +at-PreambleLength? +8 +at-CmdVC=0 OK -ats18? -0 -# FrameTimeout +ats18=7; +OK +ats18=8; ERROR +ats18? +7 +at-CmdVC? +7 ats19=9 ERROR -ats19=10 +at-FrameTimeout=10 OK ats19=2000 OK @@ -640,9 +644,9 @@ ats19=50 OK ats19? 50 -# Debug -ERROR -ats20=1 +at-FrameTimeout? +50 +at-Debug=1 OK ats20=2 ERROR @@ -650,9 +654,9 @@ ats20=0 OK ats20? 0 -# Echo -ERROR -ats21=1 +at-Debug? +0 +at-Echo=1 OK ats21=2 ERROR @@ -660,11 +664,11 @@ ats21=0 OK ats21? 0 -# HeartBeatTimeout -ERROR +at-Echo? +0 ats22=249 ERROR -ats22=250 +at-HeartBeatTimeout=250 OK ats22=65535 OK @@ -674,9 +678,9 @@ ats22=5000 OK ats22? 5000 -# FlowControl -ERROR -ats23=1 +at-HeartBeatTimeout? +5000 +at-FlowControl=1 OK ats23=2 ERROR @@ -684,9 +688,9 @@ ats23=0 OK ats23? 0 -# AutoTune -ERROR -ats24=1 +at-FlowControl? +0 +at-AutoTune=1 OK ats24=2 ERROR @@ -694,9 +698,9 @@ ats24=0 OK ats24? 0 -# DisplayPacketQuality -ERROR -ats25=1 +at-AutoTune? +0 +at-DisplayPacketQuality=1 OK ats25=2 ERROR @@ -704,9 +708,9 @@ ats25=0 OK ats25? 0 -# MaxResends -ERROR -ats26=0 +at-DisplayPacketQuality? +0 +at-MaxResends=0 OK ats26=255 OK @@ -716,9 +720,9 @@ ats26=2 OK ats26? 2 -# SortParametersByName -ERROR -ats27=0 +at-MaxResends? +2 +at-SortParametersByName=0 OK ats27=2 ERROR @@ -726,9 +730,9 @@ ats27=1 OK ats27? 1 -# ParameterName -ERROR -ats28=0 +at-SortParametersByName? +1 +at-PrintParameterName=0 OK ats28=2 ERROR @@ -737,9 +741,9 @@ PrintParameterName=1 OK ats28? PrintParameterName=1 -# PrintFrequency -ERROR -ats29=1 +at-PrintParameterName? +PrintParameterName=1 +at-PrintFrequency=1 PrintFrequency=1 OK ats29=2 @@ -749,9 +753,9 @@ PrintFrequency=0 OK ats29? PrintFrequency=0 -# DebugRadio -ERROR -ats30=1 +at-PrintFrequency? +PrintFrequency=0 +at-DebugRadio=1 DebugRadio=1 OK ats30=2 @@ -761,9 +765,9 @@ DebugRadio=0 OK ats30? DebugRadio=0 -# DebugStates -ERROR -ats31=1 +at-DebugRadio? +DebugRadio=0 +at-DebugStates=1 DebugStates=1 OK ats31=2 @@ -773,9 +777,9 @@ DebugStates=0 OK ats31? DebugStates=0 -# DebugTraining -ERROR -ats32=1 +at-DebugStates? +DebugStates=0 +at-DebugTraining=1 DebugTraining=1 OK ats32=2 @@ -785,9 +789,9 @@ DebugTraining=0 OK ats32? DebugTraining=0 -# DebugTrigger -ERROR -ats33=1 +at-DebugTraining? +DebugTraining=0 +at-DebugTrigger=1 DebugTrigger=1 OK ats33=2 @@ -797,9 +801,9 @@ DebugTrigger=0 OK ats33? DebugTrigger=0 -# UsbSerialWait -ERROR -ats34=1 +at-DebugTrigger? +DebugTrigger=0 +at-UsbSerialWait=1 UsbSerialWait=1 OK ats34=2 @@ -809,9 +813,9 @@ UsbSerialWait=0 OK ats34? UsbSerialWait=0 -# PrintRfData -ERROR -ats35=1 +at-UsbSerialWait? +UsbSerialWait=0 +at-PrintRfData=1 PrintRfData=1 OK ats35=2 @@ -821,9 +825,9 @@ PrintRfData=0 OK ats35? PrintRfData=0 -# PrintPktData -ERROR -ats36=1 +at-PrintRfData? +PrintRfData=0 +at-PrintPktData=1 PrintPktData=1 OK ats36=2 @@ -833,9 +837,9 @@ PrintPktData=0 OK ats36? PrintPktData=0 -# VerifyRxNetID -ERROR -ats37=1 +at-PrintPktData? +PrintPktData=0 +at-VerifyRxNetID=1 VerifyRxNetID=1 OK ats37=2 @@ -845,11 +849,11 @@ VerifyRxNetID=0 OK ats37? VerifyRxNetID=0 -# TriggerWidth -ERROR +at-VerifyRxNetID? +VerifyRxNetID=0 ats38=0 ERROR -ats38=1 +at-TriggerWidth=1 TriggerWidth=1 OK ats38=255 @@ -862,9 +866,9 @@ TriggerWidth=25 OK ats38? TriggerWidth=25 -# TriggerWidthIsMultiplier -ERROR -ats39=0 +at-TriggerWidth? +TriggerWidth=25 +at-TriggerWidthIsMultiplier=0 TriggerWidthIsMultiplier=0 OK ats39=2 @@ -874,29 +878,29 @@ TriggerWidthIsMultiplier=1 OK ats39? TriggerWidthIsMultiplier=1 -# TriggerEnable: 31-0 -ERROR -ats40=0 -TriggerEnable: 31-0=0 +at-TriggerWidthIsMultiplier? +TriggerWidthIsMultiplier=1 +at-TriggerEnable_31-0=0 +TriggerEnable_31-0=0 OK ats40=4294967295 -TriggerEnable: 31-0=-1 +TriggerEnable_31-0=-1 OK ats40? -TriggerEnable: 31-0=-1 -# TriggerEnable: 63-32 -ERROR -ats41=0 -TriggerEnable2: 63-32=0 +TriggerEnable_31-0=-1 +at-TriggerEnable_31-0? +TriggerEnable_31-0=-1 +at-TriggerEnable_63-32=0 +TriggerEnable_63-32=0 OK ats41=4294967295 -TriggerEnable2: 63-32=-1 +TriggerEnable_63-32=-1 OK ats41? -TriggerEnable2: 63-32=-1 -# DebugReceive -ERROR -ats42=1 +TriggerEnable_63-32=-1 +at-TriggerEnable_63-32? +TriggerEnable_63-32=-1 +at-DebugReceive=1 DebugReceive=1 OK ats42=2 @@ -906,9 +910,9 @@ DebugReceive=0 OK ats42? DebugReceive=0 -# DebugTransmit -ERROR -ats43=1 +at-DebugReceive? +DebugReceive=0 +at-DebugTransmit=1 DebugTransmit=1 OK ats43=2 @@ -918,9 +922,9 @@ DebugTransmit=0 OK ats43? DebugTransmit=0 -# PrintTxErrors -ERROR -ats44=1 +at-DebugTransmit? +DebugTransmit=0 +at-PrintTxErrors=1 PrintTxErrors=1 OK ats44=2 @@ -930,22 +934,22 @@ PrintTxErrors=0 OK ats44? PrintTxErrors=0 -# radioProtocolVersion -ERROR +at-PrintTxErrors? +PrintTxErrors=0 ats45=0 ERROR ats45=1 ERROR -ats45=2 +at-radioProtocolVersion=2 radioProtocolVersion=2 OK ats45=3 ERROR ats45? radioProtocolVersion=2 -# PrintTimestamp -ERROR -ats46=1 +at-radioProtocolVersion? +radioProtocolVersion=2 +at-PrintTimestamp=1 PrintTimestamp=1 OK ats46=2 @@ -955,9 +959,9 @@ PrintTimestamp=0 OK ats46? PrintTimestamp=0 -# debugDatagrams -ERROR -ats47=1 +at-PrintTimestamp? +PrintTimestamp=0 +at-debugDatagrams=1 DebugDatagrams=1 OK ats47=2 @@ -967,22 +971,21 @@ DebugDatagrams=0 OK ats47? DebugDatagrams=0 -# txAckMillis -ERROR -ats48=1 -OverHeadtime=1 -OK -ats48=2 -OverHeadtime=2 -OK -ats48=0 +at-debugDatagrams? +DebugDatagrams=0 +at-OverHeadtime=0 OverHeadtime=0 OK -ats48? -OverHeadtime=0 -# enableCRC16 +ats48=1000 +OverHeadtime=1000 +OK +ats48=1001 ERROR -ats49=1 +ats48? +OverHeadtime=1000 +at-OverHeadtime? +OverHeadtime=1000 +at-enableCRC16=1 EnableCRC16=1 OK ats49=2 @@ -992,9 +995,9 @@ EnableCRC16=0 OK ats49? EnableCRC16=0 -# DisplayRealMillis -ERROR -ats50=1 +at-enableCRC16? +EnableCRC16=0 +at-DisplayRealMillis=1 DisplayRealMillis=1 OK ats50=2 @@ -1004,9 +1007,9 @@ DisplayRealMillis=0 OK ats50? DisplayRealMillis=0 -# TrainingServer -ERROR -ats51=1 +at-DisplayRealMillis? +DisplayRealMillis=0 +at-TrainingServer=1 TrainingServer=1 OK ats51=2 @@ -1016,9 +1019,9 @@ TrainingServer=0 OK ats51? TrainingServer=0 -# ClientRetryInterval -ERROR -ats52=1 +at-TrainingServer? +TrainingServer=0 +at-ClientRetryInterval=1 ClientRetryInterval=1 OK ats52=2 @@ -1028,9 +1031,9 @@ ats52=0 ERROR ats52? ClientRetryInterval=2 -# CopyDebug -ERROR -ats53=1 +at-ClientRetryInterval? +ClientRetryInterval=2 +at-CopyDebug=1 CopyDebug=1 OK ats53=2 @@ -1040,9 +1043,9 @@ CopyDebug=0 OK ats53? CopyDebug=0 -# CopySerial -ERROR -ats54=1 +at-CopyDebug? +CopyDebug=0 +at-CopySerial=1 CopySerial=1 OK ats54=2 @@ -1052,9 +1055,9 @@ CopySerial=0 OK ats54? CopySerial=0 -# CopyTriggers -ERROR -ats55=1 +at-CopySerial? +CopySerial=0 +at-CopyTriggers=1 CopyTriggers=1 OK ats55=2 @@ -1064,9 +1067,9 @@ CopyTriggers=0 OK ats55? CopyTriggers=0 -# TrainingKey -ERROR -ats56=01020304050607080910111213141516 +at-CopyTriggers? +CopyTriggers=0 +at-TrainingKey=01020304050607080910111213141516 TrainingKey=01020304050607080910111213141516 OK ats56=0102030405060708091011121314151 @@ -1075,9 +1078,9 @@ ats56=010203040506070809101112131415160 ERROR ats56? TrainingKey=01020304050607080910111213141516 -# PrintLinkUpDown -ERROR -ats57=1 +at-TrainingKey? +TrainingKey=01020304050607080910111213141516 +at-PrintLinkUpDown=1 PrintLinkUpDown=1 OK ats57=2 @@ -1087,9 +1090,9 @@ PrintLinkUpDown=0 OK ats57? PrintLinkUpDown=0 -# InvertCts -ERROR -ats58=1 +at-PrintLinkUpDown? +PrintLinkUpDown=0 +at-InvertCts=1 InvertCts=1 OK ats58=2 @@ -1099,9 +1102,9 @@ InvertCts=0 OK ats58? InvertCts=0 -# InvertRts -ERROR -ats59=1 +at-InvertCts? +InvertCts=0 +at-InvertRts=1 InvertRts=1 OK ats59=2 @@ -1111,9 +1114,9 @@ InvertRts=0 OK ats59? InvertRts=0 -# AlternateLedUsage -ERROR -ats60=1 +at-InvertRts? +InvertRts=0 +at-AlternateLedUsage=1 AlternateLedUsage=1 OK ats60=2 @@ -1123,6 +1126,8 @@ AlternateLedUsage=0 OK ats60? AlternateLedUsage=0 +at-AlternateLedUsage? +AlternateLedUsage=0 # Invalid commands ERROR ats61=1 @@ -1133,13 +1138,17 @@ ats255=1 ERROR ats255? ERROR +at-foobar=1 +ERROR +at-foobar? +ERROR ati0 ATS1:AirSpeed=4800 ATS60:AlternateLedUsage=0 ATS24:AutoTune=0 ATS13:Bandwidth=500.00 ATS52:ClientRetryInterval=2 -ATS18:CmdVC=0 +ATS18:CmdVC=7 ATS15:CodingRate=8 ATS53:CopyDebug=0 ATS54:CopySerial=0 @@ -1172,7 +1181,7 @@ ATS26:MaxResends=2 ATS2:netID=192 ATS10:NumberOfChannels=50 ATS3:OperatingMode=1 -ATS48:OverHeadtime=0 +ATS48:OverHeadtime=1000 ATS17:PreambleLength=8 ATS29:PrintFrequency=0 ATS57:PrintLinkUpDown=0 @@ -1188,8 +1197,8 @@ ATS14:SpreadFactor=9 ATS16:SyncWord=18 ATS56:TrainingKey=01020304050607080910111213141516 ATS51:TrainingServer=0 -ATS41:TriggerEnable2: 63-32=-1 -ATS40:TriggerEnable: 31-0=-1 +ATS40:TriggerEnable_31-0=-1 +ATS41:TriggerEnable_63-32=-1 ATS38:TriggerWidth=25 ATS39:TriggerWidthIsMultiplier=1 ATS7:TxPower=30 From 9b17626fdbc2bcc8180a0ba8a0b86f61bfb9df98 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 8 Nov 2022 07:57:54 -1000 Subject: [PATCH 092/594] Fix build error --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d3c776fe..f20c4ba9 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -585,7 +585,7 @@ PacketType rcvDatagram() } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { - if (setting.debug == true) + if (settings.debug == true) systemPrintln("Receive CRC error!"); return (DATAGRAM_CRC_ERROR); } @@ -593,7 +593,7 @@ PacketType rcvDatagram() { if (settings.debug == true) { - systemPrint("Receive error: ")); + systemPrint("Receive error: "); systemPrintln(state); } return (DATAGRAM_BAD); From 654d3b3add6c60b4560200fec1a4e354ac5d087f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 3 Nov 2022 10:33:09 -1000 Subject: [PATCH 093/594] Remove the debugTrigger setting and simplify the trigger logic --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- Firmware/LoRaSerial_Firmware/RadioV2.ino | 1 - Firmware/LoRaSerial_Firmware/System.ino | 31 ++++++++++------------- Firmware/LoRaSerial_Firmware/Train.ino | 1 - Firmware/LoRaSerial_Firmware/settings.h | 5 ++-- 5 files changed, 17 insertions(+), 23 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index b66f21ca..8e0b16b8 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -663,7 +663,7 @@ const COMMAND_ENTRY commands[] = {30, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &settings.debugRadio}, {31, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &settings.debugStates}, {32, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &settings.debugTraining}, - {33, 0, 1, 0, TYPE_BOOL, valInt, "DebugTrigger", &settings.debugTrigger}, + //33 {34, 0, 1, 0, TYPE_BOOL, valInt, "UsbSerialWait", &settings.usbSerialWait}, {35, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &settings.printRfData}, diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d3c776fe..92fc2e69 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -443,7 +443,6 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.debugRadio = params.debugRadio; originalSettings.debugStates = params.debugStates; originalSettings.debugTraining = params.debugTraining; - originalSettings.debugTrigger = params.debugTrigger; originalSettings.printRfData = params.printRfData; originalSettings.printPktData = params.printPktData; originalSettings.debugReceive = params.debugReceive; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 66ee4d80..2107a794 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -319,6 +319,10 @@ void triggerEvent(uint8_t triggerNumber) uint32_t triggerEnable; uint16_t triggerWidth; + //Determine if the trigger pin is enabled + if (pin_trigger != PIN_UNDEFINED) + return; + //Determine which trigger enable to use triggerBitNumber = triggerNumber; triggerEnable = settings.triggerEnable; @@ -329,24 +333,17 @@ void triggerEvent(uint8_t triggerNumber) } //Determine if the trigger is enabled - if ((pin_trigger != PIN_UNDEFINED) && (triggerEnable & (1 << triggerBitNumber))) + if (triggerEnable & (1 << triggerBitNumber)) { - //Determine if the trigger pin is enabled - if (pin_trigger != PIN_UNDEFINED) - { - if ((settings.debug == true) || (settings.debugTrigger == true)) - { - //Determine the trigger pulse width - triggerWidth = settings.triggerWidth; - if (settings.triggerWidthIsMultiplier) - triggerWidth *= (triggerNumber + 1); - - //Output the trigger pulse - digitalWrite(pin_trigger, LOW); - delayMicroseconds(triggerWidth); - digitalWrite(pin_trigger, HIGH); - } - } + //Determine the trigger pulse width + triggerWidth = settings.triggerWidth; + if (settings.triggerWidthIsMultiplier) + triggerWidth *= (triggerNumber + 1); + + //Output the trigger pulse + digitalWrite(pin_trigger, LOW); + delayMicroseconds(triggerWidth); + digitalWrite(pin_trigger, HIGH); } } diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 3030d2c5..6272b95d 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -192,7 +192,6 @@ void commonTrainingInitialization() settings.debugRadio = originalSettings.debugRadio; settings.debugStates = originalSettings.debugStates; settings.debugTraining = originalSettings.debugTraining; - settings.debugTrigger = originalSettings.debugTrigger; settings.printRfData = originalSettings.printRfData; settings.printPktData = originalSettings.printPktData; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 2e341c5f..b4646ef2 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -429,15 +429,14 @@ typedef struct struct_settings { bool debugRadio = false; //Print radio info bool debugStates = false; //Print state changes bool debugTraining = false; //Print training info - bool debugTrigger = false; //Print triggers bool usbSerialWait = false; //Wait for USB serial initialization bool printRfData = false; //Print RX and TX data bool printPktData = false; //Print data, before encryption and after decryption bool verifyRxNetID = false; //Verify RX netID value when not operating in point-to-point mode uint8_t triggerWidth = 25; //Trigger width in microSeconds or multipler for trigger width bool triggerWidthIsMultiplier = true; //Use the trigger width as a multiplier - uint32_t triggerEnable = 0xffffffff; //Determine which triggers are enabled: 31 - 0 - uint32_t triggerEnable2 = 0xffffffff; //Determine which triggers are enabled: 63 - 32 + uint32_t triggerEnable = 0; //Determine which triggers are enabled: 31 - 0 + uint32_t triggerEnable2 = 0; //Determine which triggers are enabled: 63 - 32 bool debugReceive = false; //Print receive processing bool debugTransmit = false; //Print transmit processing bool printTxErrors = false; //Print any transmit errors From 27d34a5161ac57edd16b199e095bb15fc302a658 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 3 Nov 2022 10:53:39 -1000 Subject: [PATCH 094/594] Generate different random seeds with different radio parameters Use a common routine to update the parameters based upon the air speed. Make sure that the current parameters are in use before generating the random seed that selects the frequency hop table. --- Firmware/LoRaSerial_Firmware/Radio.ino | 166 ++++++++++++++----------- 1 file changed, 94 insertions(+), 72 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 03e0fc36..259cc12e 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -4,73 +4,8 @@ void configureRadio() { bool success = true; - //Determine if we are using AirSpeed or custom settings - if (settings.airSpeed != 0) - { - switch (settings.airSpeed) - { - case (0): - //Custom settings - use settings without modification - break; - case (40): - settings.radioSpreadFactor = 11; - settings.radioBandwidth = 62.5; - settings.radioCodingRate = 8; - break; - case (150): - settings.radioSpreadFactor = 10; - settings.radioBandwidth = 62.5; - settings.radioCodingRate = 8; - break; - case (400): - settings.radioSpreadFactor = 10; - settings.radioBandwidth = 125; - settings.radioCodingRate = 8; - break; - case (1200): - settings.radioSpreadFactor = 9; - settings.radioBandwidth = 125; - settings.radioCodingRate = 8; - break; - case (2400): - settings.radioSpreadFactor = 10; - settings.radioBandwidth = 500; - settings.radioCodingRate = 8; - break; - case (4800): - settings.radioSpreadFactor = 9; - settings.radioBandwidth = 500; - settings.radioCodingRate = 8; - break; - case (9600): - settings.radioSpreadFactor = 8; - settings.radioBandwidth = 500; - settings.radioCodingRate = 7; - break; - case (19200): - settings.radioSpreadFactor = 7; - settings.radioBandwidth = 500; - settings.radioCodingRate = 7; - break; - case (28800): - settings.radioSpreadFactor = 6; - settings.radioBandwidth = 500; - settings.radioCodingRate = 6; - break; - case (38400): - settings.radioSpreadFactor = 6; - settings.radioBandwidth = 500; - settings.radioCodingRate = 5; - break; - default: - if ((settings.debug == true) || (settings.debugRadio == true)) - { - systemPrint("Unknown airSpeed: "); - systemPrintln(settings.airSpeed); - } - break; - } - } + //Update the settings based upon the air speed + convertAirSpeedToSettings(); if (radio.setFrequency(channels[0]) == RADIOLIB_ERR_INVALID_FREQUENCY) success = false; @@ -165,6 +100,78 @@ void configureRadio() systemPrintln("Radio configured"); } +//Update the settings based upon the airSpeed value +void convertAirSpeedToSettings() +{ + //Determine if we are using AirSpeed or custom settings + if (settings.airSpeed != 0) + { + switch (settings.airSpeed) + { + case (0): + //Custom settings - use settings without modification + break; + case (40): + settings.radioSpreadFactor = 11; + settings.radioBandwidth = 62.5; + settings.radioCodingRate = 8; + break; + case (150): + settings.radioSpreadFactor = 10; + settings.radioBandwidth = 62.5; + settings.radioCodingRate = 8; + break; + case (400): + settings.radioSpreadFactor = 10; + settings.radioBandwidth = 125; + settings.radioCodingRate = 8; + break; + case (1200): + settings.radioSpreadFactor = 9; + settings.radioBandwidth = 125; + settings.radioCodingRate = 8; + break; + case (2400): + settings.radioSpreadFactor = 10; + settings.radioBandwidth = 500; + settings.radioCodingRate = 8; + break; + case (4800): + settings.radioSpreadFactor = 9; + settings.radioBandwidth = 500; + settings.radioCodingRate = 8; + break; + case (9600): + settings.radioSpreadFactor = 8; + settings.radioBandwidth = 500; + settings.radioCodingRate = 7; + break; + case (19200): + settings.radioSpreadFactor = 7; + settings.radioBandwidth = 500; + settings.radioCodingRate = 7; + break; + case (28800): + settings.radioSpreadFactor = 6; + settings.radioBandwidth = 500; + settings.radioCodingRate = 6; + break; + case (38400): + settings.radioSpreadFactor = 6; + settings.radioBandwidth = 500; + settings.radioCodingRate = 5; + break; + default: + if ((settings.debug == true) || (settings.debugRadio == true)) + { + systemPrint("Unknown airSpeed: "); + systemPrintln(settings.airSpeed); + } + break; + } + } +} + //Set radio frequency void setRadioFrequency(bool rxAdjust) { @@ -270,6 +277,9 @@ void generateHopTable() if (channels != NULL) free(channels); channels = (float *)malloc(settings.numberOfChannels * sizeof(float)); + //Update the settings based upon the air speed + convertAirSpeedToSettings(); + float channelSpacing = (settings.frequencyMax - settings.frequencyMin) / (float)(settings.numberOfChannels + 2); //Keep away from edge of available spectrum @@ -282,11 +292,23 @@ void generateHopTable() //Feed random number generator with our specific platform settings //Use settings that must be identical to have a functioning link. //For example, we do not use coding rate because two radios can communicate with different coding rate values - myRandSeed = settings.airSpeed + settings.netID + settings.operatingMode + settings.encryptData - + settings.dataScrambling - + (uint16_t)settings.frequencyMin + (uint16_t)settings.frequencyMax - + settings.numberOfChannels + settings.frequencyHop + settings.maxDwellTime - + (uint16_t)settings.radioBandwidth + settings.radioSpreadFactor; + myRandSeed = settings.airSpeed + + settings.netID + + settings.operatingMode + + settings.encryptData + + settings.dataScrambling + + (uint16_t)settings.frequencyMin + + (uint16_t)settings.frequencyMax + + settings.numberOfChannels + + settings.frequencyHop + + settings.maxDwellTime + + (uint16_t)settings.radioBandwidth + + settings.radioSpreadFactor + + settings.verifyRxNetID + + settings.radioProtocolVersion + + settings.overheadTime + + settings.enableCRC16 + + settings.clientPingRetryInterval; if (settings.encryptData == true) { From 13d40b34562954d250cecf2b3b035bf5690a1735 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 3 Nov 2022 11:19:17 -1000 Subject: [PATCH 095/594] Verify that a receive is not in progress before transmitting --- Firmware/LoRaSerial_Firmware/States.ino | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ce90160c..17af7819 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -274,17 +274,8 @@ void updateRadioState() radio.setFrequency(channels[channelNumber]); } - //Is it time to send the PING to the remote system - if ((millis() - heartbeatTimer) >= pingRandomTime) - { - //Transmit the PING - triggerEvent(TRIGGER_HANDSHAKE_SEND_PING); - xmitDatagramP2PPing(); - changeState(RADIO_P2P_WAIT_TX_PING_DONE); - } - //Determine if a PING was received - else if (transactionComplete) + if (transactionComplete) { transactionComplete = false; //Reset ISR flag @@ -310,6 +301,15 @@ void updateRadioState() changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); } } + + //Is it time to send the PING to the remote system + else if ((receiveInProcess() == false) && ((millis() - heartbeatTimer) >= pingRandomTime)) + { + //Transmit the PING + triggerEvent(TRIGGER_HANDSHAKE_SEND_PING); + xmitDatagramP2PPing(); + changeState(RADIO_P2P_WAIT_TX_PING_DONE); + } break; case RADIO_P2P_WAIT_TX_PING_DONE: @@ -791,9 +791,9 @@ void updateRadioState() } } - //Check for ACK timeout - else if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime)) - //Set at end of transmit, measures ACK timeout + //Check for ACK timeout, set at end of transmit, measures ACK timeout + else if ((receiveInProcess() == false) + && ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime))) { if (settings.debugDatagrams) { @@ -1778,7 +1778,7 @@ void changeState(RadioStates newState) unsigned int seconds = (millis() - lastLinkUpTime) / 1000; unsigned int minutes = seconds / 60; seconds -= (minutes * 60); - unsigned int hours = minutes / 60; + unsigned int hours = minutes / 60; minutes -= (hours * 60); unsigned int days = hours / 24; hours -= (days * 24); From ec56aab3f7fa2a22786dcdd2baeed88341c23d9b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 8 Nov 2022 07:43:24 -1000 Subject: [PATCH 096/594] Only echo during input processing and only in one spot --- Firmware/LoRaSerial_Firmware/Serial.ino | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index b2ac9e87..d09005e8 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -223,9 +223,6 @@ void updateSerial() byte incoming = systemRead(); - if (settings.echo == true) - systemWrite(incoming); - serialReceiveBuffer[rxHead++] = incoming; //Push char to holding buffer rxHead %= sizeof(serialReceiveBuffer); } //End Serial.available() @@ -242,6 +239,9 @@ void updateSerial() byte incoming = serialReceiveBuffer[rxTail++]; rxTail %= sizeof(serialReceiveBuffer); + if ((settings.echo == true) || (inCommandMode == true)) + systemWrite(incoming); + //Process serial into either rx buffer or command buffer if (inCommandMode == true) { @@ -272,8 +272,6 @@ void updateSerial() } else { - systemWrite(incoming); //Always echo during command mode - //Move this character into the command buffer commandBuffer[commandLength++] = toupper(incoming); commandLength %= sizeof(commandBuffer); From d76f3a6b15683db355a54ed294cd2c30a03f875b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 3 Nov 2022 12:03:47 -1000 Subject: [PATCH 097/594] Add command ATS33 to set the trainingTimeout in minutes --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 20 +++++++++++++++++-- Firmware/LoRaSerial_Firmware/Train.ino | 2 ++ Firmware/LoRaSerial_Firmware/settings.h | 1 + 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 8e0b16b8..8ca07c36 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -663,7 +663,7 @@ const COMMAND_ENTRY commands[] = {30, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &settings.debugRadio}, {31, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &settings.debugStates}, {32, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &settings.debugTraining}, - //33 + {33, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &settings.trainingTimeout}, {34, 0, 1, 0, TYPE_BOOL, valInt, "UsbSerialWait", &settings.usbSerialWait}, {35, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &settings.printRfData}, diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index fbb6eb9b..8c5d0b33 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -434,6 +434,7 @@ bool trainingPreviousRxInProgress = false; //Previous RX status float originalChannel; //Original channel from HOP table while training is in progress uint8_t trainingPartnerID[UNIQUE_ID_BYTES]; //Unique ID of the training partner uint8_t myUniqueId[UNIQUE_ID_BYTES]; // Unique ID of this system +unsigned long trainingTimer; //Virtual-Circuit int8_t cmdVc; //VC index for ATI commands only diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ce90160c..47c2ab26 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -137,6 +137,7 @@ void updateRadioState() //Wait for the PING to complete transmission case RADIO_P2P_TRAINING_WAIT_PING_DONE: + updateCylonLEDs(); if (transactionComplete) { transactionComplete = false; //Reset ISR flag @@ -146,7 +147,7 @@ void updateRadioState() break; case RADIO_P2P_WAIT_FOR_TRAINING_PARAMS: - updateRSSI(); + updateCylonLEDs(); //Check for a received datagram if (transactionComplete == true) @@ -180,6 +181,8 @@ void updateRadioState() //Update the parameters updateRadioParameters(rxData); endPointToPointTraining(true); + if (settings.debugTraining) + systemPrintln("Training successful, received parameters!"); changeState(RADIO_RESET); } } @@ -195,14 +198,27 @@ void updateRadioState() lostFrames++; changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); } + + //Check for done the training + else if ((millis() - trainingTimer) > (settings.trainingTimeout * 60 * 1000)) + { + //Failed to complete the training + if (settings.debugTraining) + systemPrintln("Training timeout, returning to previous mode!"); + endPointToPointTraining(false); + changeState(RADIO_RESET); + } } break; case RADIO_P2P_WAIT_TRAINING_PARAMS_DONE: + updateCylonLEDs(); if (transactionComplete) { transactionComplete = false; //Reset ISR flag endPointToPointTraining(false); + if (settings.debugTraining) + systemPrintln("Training successful, sent parameters!"); changeState(RADIO_RESET); } break; @@ -1778,7 +1794,7 @@ void changeState(RadioStates newState) unsigned int seconds = (millis() - lastLinkUpTime) / 1000; unsigned int minutes = seconds / 60; seconds -= (minutes * 60); - unsigned int hours = minutes / 60; + unsigned int hours = minutes / 60; minutes -= (hours * 60); unsigned int days = hours / 24; hours -= (days * 24); diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 6272b95d..05178991 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -106,6 +106,7 @@ void beginTrainingPointToPoint(bool defaultTraining) //Transmit general ping packet to see if anyone else is sitting on the training channel xmitDatagramP2PTrainingPing(); + trainingTimer = millis(); //Set the next state changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); @@ -131,6 +132,7 @@ void beginTrainingClient() //Transmit client ping to the training server xmitDatagramMpTrainingPing(); + trainingTimer = millis(); //Set the next state changeState(RADIO_MP_WAIT_TX_TRAINING_PING_DONE); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index b4646ef2..eb7c3668 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -456,6 +456,7 @@ typedef struct struct_settings { bool invertCts = false; //Invert the input of CTS bool invertRts = false; //Invert the output of RTS bool alternateLedUsage = false; //Enable alternate LED usage + uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training //Add new parameters immediately before this line //-- Add commands to set the parameters From 0b384f16003e18a14b1335832cb5ffbbf181d6f1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 8 Nov 2022 08:43:17 -1000 Subject: [PATCH 098/594] Move virtual circuit definitions into Virtual_Circuit_Protocol.h This provides a .h file for C or C++ programs to ensure that both the firmware and host programs are using the same values. --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 30 ++--- Firmware/LoRaSerial_Firmware/States.ino | 19 +-- .../Virtual_Circuit_Protocol.h | 116 ++++++++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 75 +---------- 4 files changed, 145 insertions(+), 95 deletions(-) create mode 100644 Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d3c776fe..95ed3606 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -571,6 +571,7 @@ PacketType rcvDatagram() uint8_t receivedNetID; CONTROL_U8 rxControl; VIRTUAL_CIRCUIT * vc; + VC_RADIO_MESSAGE_HEADER * vcHeader; //Save the receive time rcvTimeMillis = millis(); @@ -585,7 +586,7 @@ PacketType rcvDatagram() } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { - if (setting.debug == true) + if (settings.debug == true) systemPrintln("Receive CRC error!"); return (DATAGRAM_CRC_ERROR); } @@ -593,7 +594,7 @@ PacketType rcvDatagram() { if (settings.debug == true) { - systemPrint("Receive error: ")); + systemPrint("Receive error: "); systemPrintln(state); } return (DATAGRAM_BAD); @@ -965,8 +966,9 @@ PacketType rcvDatagram() } //Parse the virtual circuit header - rxDestVc = rxData[1]; - rxSrcVc = rxData[2]; + vcHeader = (VC_RADIO_MESSAGE_HEADER *)&rxData[1]; + rxDestVc = vcHeader->destVc; + rxSrcVc = vcHeader->srcVc; rxVcData = &rxData[3]; //Validate the source VC @@ -990,11 +992,11 @@ PacketType rcvDatagram() } //Validate the length - if (*rxData != rxDataBytes) + if (vcHeader->length != rxDataBytes) { systemPrintTimestamp(); systemPrint("Invalid VC length, received "); - systemPrint(*rxData); + systemPrint(vcHeader->length); systemPrint(" expecting "); systemPrintln(rxDataBytes); if (timeToHop == true) //If the channelTimer has expired, move to next frequency @@ -1018,7 +1020,7 @@ PacketType rcvDatagram() { systemPrintTimestamp(); systemPrint(" VC Length: "); - systemPrintln(*rxData); + systemPrintln(vcHeader->length); systemPrint(" DestAddr: "); if (rxDestVc == VC_BROADCAST) systemPrintln("Broadcast"); @@ -1115,8 +1117,8 @@ void transmitDatagram() uint8_t length; int8_t srcVc; uint8_t * vcData; - uint8_t vcLength; VIRTUAL_CIRCUIT * vc; + VC_RADIO_MESSAGE_HEADER * vcHeader; if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -1125,15 +1127,15 @@ void transmitDatagram() vc = NULL; if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { - vcData = &outgoingPacket[headerBytes]; - vcLength = *vcData++; - txDestVc = *vcData++; - srcVc = *vcData++; - if ((uint8_t)srcVc <= MAX_VC) + vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; + txDestVc = vcHeader->destVc; + srcVc = vcHeader->srcVc; + if ((uint8_t)vcHeader->srcVc <= MAX_VC) { vc = &virtualCircuitList[srcVc]; vc->messagesSent++; } + vcData = (uint8_t *)&vcHeader[1]; } //Determine the packet size @@ -1270,7 +1272,7 @@ void transmitDatagram() { systemPrintTimestamp(); systemPrint(" Length: "); - systemPrintln(vcLength); + systemPrintln(vcHeader->length); systemPrint(" DestAddr: "); if (txDestVc == VC_BROADCAST) systemPrintln("Broadcast"); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ce90160c..970ccc3c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -31,6 +31,7 @@ void updateRadioState() static uint8_t rexmtFrameSentCount; static uint8_t rexmtTxDestVc; VIRTUAL_CIRCUIT * vc; + VC_RADIO_MESSAGE_HEADER * vcHeader; switch (radioState) { @@ -1322,9 +1323,11 @@ void updateRadioState() serialBufferOutput(rxData, rxDataBytes); //Acknowledge the data frame - *endOfTxData++ = VC_HEADER_BYTES + ACK_BYTES; - *endOfTxData++ = rxSrcVc; - *endOfTxData++ = myVc; + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; + vcHeader->length = VC_RADIO_HEADER_BYTES + ACK_BYTES; + vcHeader->destVc = rxSrcVc; + vcHeader->srcVc = myVc; + endOfTxData += VC_RADIO_HEADER_BYTES; xmitDatagramP2PAck(); changeState(RADIO_VC_WAIT_TX_DONE); break; @@ -1423,9 +1426,11 @@ void updateRadioState() serialBufferOutput(rxData, rxDataBytes); //Acknowledge the data frame - *endOfTxData++ = VC_HEADER_BYTES + ACK_BYTES; - *endOfTxData++ = rxSrcVc; - *endOfTxData++ = myVc; + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; + vcHeader->length = VC_RADIO_HEADER_BYTES + ACK_BYTES; + vcHeader->destVc = rxSrcVc; + vcHeader->srcVc = myVc; + endOfTxData += VC_RADIO_HEADER_BYTES; xmitDatagramP2PAck(); changeState(RADIO_VC_WAIT_TX_DONE_ACK); break; @@ -1778,7 +1783,7 @@ void changeState(RadioStates newState) unsigned int seconds = (millis() - lastLinkUpTime) / 1000; unsigned int minutes = seconds / 60; seconds -= (minutes * 60); - unsigned int hours = minutes / 60; + unsigned int hours = minutes / 60; minutes -= (hours * 60); unsigned int days = hours / 24; hours -= (days * 24); diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h new file mode 100644 index 00000000..fd391e3d --- /dev/null +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -0,0 +1,116 @@ +#ifndef __VIRTUAL_CIRCUIT_PROTOCOL_H__ +#define __VIRTUAL_CIRCUIT_PROTOCOL_H__ + +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +//Virtual-Circuit source and destination index values +#define MAX_VC 8 +#define VC_SERVER 0 +#define VC_BROADCAST -1 +#define VC_UNASSIGNED -2 + +//Source and destinations reserved for the local radio +#define VC_LINK_RESET -3 //Force link reset +#define VC_COMMAND -4 //Command input and command response + +//Source and destinations reserved for the local host +#define PC_COMMAND -17 //Command input and command response +#define PC_LINK_STATUS -18 //Asynchronous link status output + +//Field offsets in the VC HEARTBEAT frame +#define VC_HB_UNIQUE_ID 0 +#define VC_HB_MILLIS (VC_HB_UNIQUE_ID + UNIQUE_ID_BYTES) +#define VC_HB_CHANNEL_TIMER (VC_HB_MILLIS + sizeof(uint32_t)) +#define VC_HB_CHANNEL (VC_HB_CHANNEL_TIMER + sizeof(uint16_t)) +#define VC_HB_END (VC_HB_CHANNEL + sizeof(uint8_t)) + +#define VC_LINK_BREAK_MULTIPLIER 3 //Number of missing HEARTBEAT timeouts + +//ASCII characters +#define START_OF_HEADING 0x01 //From ASCII table + +//------------------------------------------------------------------------------ +// Protocol Exchanges +//------------------------------------------------------------------------------ +/* +Host Interaction using Virtual-Circuits + + Host A LoRa A LoRa B Host B + +All output goes to serial . + . + +++ ----> . + <---- OK + . + . + . + <---- Command responses + <---- Debug message + + Mode: VC ----> + <---- OK + + (VC debug) <---- Debug message + + CMD: AT&W ----> + (VC command) <---- OK + + CMD: LINK_RESET ----> + HEARTBEAT -2 ----> + HEARTBEAT # <---- From server + + (VC status) <---- Link self up + + HEARTBEAT # ----> + + (VC status) <---- Link A up Link A Up ----> (link status) + + (VC debug) <---- Debug message + + <---- HEARTBEAT B + (VC status) <---- Link B Up + + (VC debug) <---- Debug message + + MSG: Data for B ----> + Data for B ----> + <---- ACK + Data for B ----> MSG: Data for B + <---- MSG: Resp for A + <---- Resp for A + ACK ----> + MSG: Resp for A <---- Resp for A + +*/ +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +typedef struct _VC_RADIO_MESSAGE_HEADER +{ + uint8_t length; //Length in bytes of the VC message + int8_t destVc; //Destination VC + int8_t srcVc; //Source VC +} VC_RADIO_MESSAGE_HEADER; + +#define VC_RADIO_HEADER_BYTES (sizeof(VC_RADIO_MESSAGE_HEADER)) //Length of the radio VC header in bytes + +typedef struct _VC_SERIAL_MESSAGE_HEADER +{ + uint8_t start; //START_OF_HEADING + VC_RADIO_MESSAGE_HEADER radio; +} VC_SERIAL_MESSAGE_HEADER; + +#define VC_SERIAL_HEADER_BYTES (sizeof(VC_SERIAL_MESSAGE_HEADER)) //Length of the serial VC header in bytes + +typedef struct _VC_LINK_STATUS_MESSAGE +{ + uint8_t linkStatus; //Link status +} VC_LINK_STATUS_MESSAGE; + +#define LINK_DOWN 0 +#define LINK_UP 1 + +#endif //__VIRTUAL_CIRCUIT_PROTOCOL_H__ diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 2e341c5f..57910457 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -219,80 +219,7 @@ typedef struct _VIRTUAL_CIRCUIT //incremented when successfully acknowledged via DATA_ACK frame } VIRTUAL_CIRCUIT; -typedef struct _VC_MESSAGE_HEADER -{ - uint8_t length; //Length in bytes of the VC message - int8_t destVc; //Destination VC - int8_t srcVc; //Source VC -} VC_MESSAGE_HEADER; - -#define VC_HEADER_BYTES (sizeof(VC_MESSAGE_HEADER)) //Length of the VC header in bytes - -//Virtual-Circuit source and destination index values -#define MAX_VC 8 -#define VC_SERVER 0 -#define VC_BROADCAST -1 -#define VC_UNASSIGNED -2 - -//Source and destinations reserved for the local host -#define VC_LINK_RESET -3 //Force link reset -#define VC_LINK_STATUS -4 //Asynchronous link status output -#define VC_COMMAND -5 //Command input and command response -#define VC_DEBUG -6 //Debug input and output - -/* -Host Interaction using Virtual-Circuits - - Host A LoRa A LoRa B Host B - -All output goes to serial . - . - +++ ----> . - <---- OK - . - . - . - <---- Command responses - <---- Debug message - - Mode: VC ----> - <---- OK - - (VC debug) <---- Debug message - - CMD: AT&W ----> - (VC command) <---- OK - - CMD: LINK_RESET ----> - HEARTBEAT -2 ----> - (VC status) <---- Link A up Link A Up ----> (link status) - - (VC debug) <---- Debug message - - <---- HEARTBEAT B - (VC status) <---- Link B Up - - (VC debug) <---- Debug message - - MSG: Data for B ----> - Data for B ----> - <---- ACK - Data for B ----> MSG: Data for B - <---- MSG: Resp for A - <---- Resp for A - ACK ----> - MSG: Resp for A <---- Resp for A - -*/ - -//Field offsets in the VC HEARTBEAT frame -#define VC_HB_UNIQUE_ID 0 -#define VC_HB_MILLIS (VC_HB_UNIQUE_ID + UNIQUE_ID_BYTES) -#define VC_HB_CHANNEL_TIMER (VC_HB_MILLIS + sizeof(uint32_t)) -#define VC_HB_CHANNEL (VC_HB_CHANNEL_TIMER + sizeof(uint16_t)) -#define VC_HB_END (VC_HB_CHANNEL + sizeof(uint8_t)) - -#define VC_LINK_BREAK_MULTIPLIER 3 //Number of missing HEARTBEAT timeouts +#include "Virtual_Circuit_Protocol.h" //Train button states typedef enum From bc15f07d8a4db79e6159d9706122cfd1b9bb8f86 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 8 Nov 2022 09:51:01 -1000 Subject: [PATCH 099/594] Add vcSendLinkStatus to send the VC link status to the host --- Firmware/LoRaSerial_Firmware/States.ino | 64 +++++++++++++++++-------- Firmware/LoRaSerial_Firmware/System.ino | 10 ++++ 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 970ccc3c..a3201ee9 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1858,6 +1858,40 @@ void v2EnterLinkUp() systemPrintln("========== Link UP =========="); } +void vcSendLinkStatus(bool linkUp, int8_t srcVc) +{ + //Build the message + VC_LINK_STATUS_MESSAGE message; + message.linkStatus = linkUp ? LINK_UP : LINK_DOWN; + + //Build the message header + VC_SERIAL_MESSAGE_HEADER header; + header.start = START_OF_HEADING; + header.radio.length = sizeof(header) + sizeof(message); + header.radio.destVc = PC_LINK_STATUS; + header.radio.srcVc = srcVc; + + //Send the message + systemWrite((uint8_t *)&header, sizeof(header)); + systemWrite((uint8_t *)&message, sizeof(message)); + + if (settings.printLinkUpDown) + { + if (linkUp) + { + systemPrint("========== Link "); + systemPrint(srcVc); + systemPrintln(" UP =========="); + } + else + { + systemPrint("--------- Link "); + systemPrint(srcVc); + systemPrintln(" Down ---------"); + } + } +} + //Break the virtual-circuit link void vcBreakLink(int8_t vcIndex) { @@ -1873,13 +1907,8 @@ void vcBreakLink(int8_t vcIndex) } linkFailures++; - //Display the link failure - if (settings.printLinkUpDown) - { - systemPrint("--------- Link "); - systemPrint(vcIndex); - systemPrintln(" Down ---------"); - } + //Send the status message + vcSendLinkStatus(false, myVc); //Stop the transmit timer transmitTimer = 0; @@ -1905,14 +1934,10 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) if (memcmp(vc->uniqueId, id, UNIQUE_ID_BYTES) == 0) { if (!vc->linkUp) - { - if (settings.printLinkUpDown) - { - systemPrint("========== Link "); - systemPrint(srcAddr); - systemPrintln(" up =========="); - } - } + //Send the status message + vcSendLinkStatus(true, myVc); + + //Update the link status vc->linkUp = true; vc->lastHeartbeatMillis = millis(); return index; @@ -1967,12 +1992,9 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) vc->linkUp = true; vc->lastHeartbeatMillis = millis(); memcpy(&vc->uniqueId, id, UNIQUE_ID_BYTES); - if (settings.printLinkUpDown) - { - systemPrint("========== Link "); - systemPrint(index); - systemPrintln(" up =========="); - } + + //Send the status message + vcSendLinkStatus(true, myVc); //Returned the assigned address return index; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 66ee4d80..5753634c 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -190,6 +190,16 @@ void systemWrite(uint8_t value) arch.serialWrite(value); } +void systemWrite(uint8_t * buffer, uint16_t length) +{ + uint8_t * end; + + //Output the entire buffer ignoring contents + end = &buffer[length]; + while (buffer < end) + arch.serialWrite(*buffer); +} + void systemFlush() { arch.serialFlush(); From 80ff5ddfec4ab9fa591242b29d58f337a30b49e5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 8 Nov 2022 07:47:48 -1000 Subject: [PATCH 100/594] Add AT-* commands to support commands by parameter name Example: at-echo=1 or at-echo? The parameter name that follows at- is converted to uppercase and compared with the uppercase of the parameter names in the command tables. --- Firmware/Tools/Command_Validation_Script.txt | 6 ++-- .../Results/Command_Validation_Results.txt | 30 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index 08169f84..a54cd67b 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -373,11 +373,11 @@ ats32=0 ats32? at-DebugTraining? -at-DebugTrigger=1 -ats33=2 +at-TrainingTimeout=1 +ats33=255 ats33=0 ats33? -at-DebugTrigger? +at-TrainingTimeout? at-UsbSerialWait=1 ats34=2 diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index 9009488c..372a0dec 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -64,7 +64,7 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati3 -157 ati4 -205 +151 ati5 506 ati6 @@ -74,7 +74,7 @@ ati7 ati8 B2A99BF24A34555020312E34162815FF ati9 -Total datagrams sent: 678 +Total datagrams sent: 63 ati10 Total datagrams received: 0 ati11 @@ -145,15 +145,15 @@ ATS29:PrintFrequency=0 ATS30:DebugRadio=0 ATS31:DebugStates=0 ATS32:DebugTraining=0 -ATS33:DebugTrigger=0 +ATS33:TrainingTimeout=1 ATS34:UsbSerialWait=0 ATS35:PrintRfData=0 ATS36:PrintPktData=0 ATS37:VerifyRxNetID=0 ATS38:TriggerWidth=25 ATS39:TriggerWidthIsMultiplier=1 -ATS40:TriggerEnable_31-0=-1 -ATS41:TriggerEnable_63-32=-1 +ATS40:TriggerEnable_31-0=0 +ATS41:TriggerEnable_63-32=0 ATS42:DebugReceive=0 ATS43:DebugTransmit=0 ATS44:PrintTxErrors=0 @@ -791,18 +791,18 @@ ats32? DebugTraining=0 at-DebugTraining? DebugTraining=0 -at-DebugTrigger=1 -DebugTrigger=1 +at-TrainingTimeout=1 +TrainingTimeout=1 OK -ats33=2 -ERROR -ats33=0 -DebugTrigger=0 +ats33=255 +TrainingTimeout=255 OK +ats33=0 +ERROR ats33? -DebugTrigger=0 -at-DebugTrigger? -DebugTrigger=0 +TrainingTimeout=255 +at-TrainingTimeout? +TrainingTimeout=255 at-UsbSerialWait=1 UsbSerialWait=1 OK @@ -1161,7 +1161,6 @@ ATS42:DebugReceive=0 ATS31:DebugStates=0 ATS32:DebugTraining=0 ATS43:DebugTransmit=0 -ATS33:DebugTrigger=0 ATS25:DisplayPacketQuality=0 ATS50:DisplayRealMillis=0 ATS21:Echo=0 @@ -1197,6 +1196,7 @@ ATS14:SpreadFactor=9 ATS16:SyncWord=18 ATS56:TrainingKey=01020304050607080910111213141516 ATS51:TrainingServer=0 +ATS33:TrainingTimeout=255 ATS40:TriggerEnable_31-0=-1 ATS41:TriggerEnable_63-32=-1 ATS38:TriggerWidth=25 From cb546ede309c8edd73a50464dca30321ddc4691f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 8 Nov 2022 11:25:12 -1000 Subject: [PATCH 101/594] Place output data into serialTransmitBuffer This change includes: * Remove arch.serialPrint * Add serialOutputByte called by systemPrint and systemWrite to place data into serialTransmitBuffer * Add outputSerialData to remove bytes from serialTransmitBuffer and write them to the serial port * Add a few calls to outputSerialData to force serial output * Don't receive data when in command mode Verified using: * States.ino over the radio link * Command_Validation_Script.txt --- Firmware/LoRaSerial_Firmware/Arch_ESP32.h | 6 -- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 7 --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 2 +- Firmware/LoRaSerial_Firmware/Serial.ino | 58 ++++++++++++------- Firmware/LoRaSerial_Firmware/System.ino | 14 +++-- Firmware/LoRaSerial_Firmware/settings.h | 2 - .../Results/Command_Validation_Results.txt | 4 +- 9 files changed, 53 insertions(+), 43 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h index 033c3c75..fca52d85 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h +++ b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h @@ -87,11 +87,6 @@ void esp32SerialFlush() Serial.flush(); } -void esp32SerialPrint(const char * value) -{ - Serial.print(value); -} - uint8_t esp32SerialRead() { return (Serial.read()); @@ -123,7 +118,6 @@ const ARCH_TABLE arch = { esp32Radio, //radio esp32SerialAvailable, //serialAvailable esp32SerialFlush, //serialFlush - esp32SerialPrint, //serialPrint esp32SerialRead, //serialRead esp32SerialWrite, //serialWrite esp32SystemReset, //systemReset diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 78d665ce..8ad6c100 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -181,12 +181,6 @@ void samdSerialFlush() Serial1.flush(); } -void samdSerialPrint(const char * value) -{ - Serial.print(value); - Serial1.print(value); -} - uint8_t samdSerialRead() { byte incoming = 0; @@ -231,7 +225,6 @@ const ARCH_TABLE arch = { samdRadio, //radio samdSerialAvailable, //serialAvailable samdSerialFlush, //serialFlush - samdSerialPrint, //serialPrint samdSerialRead, //serialRead samdSerialWrite, //serialWrite samdSystemReset, //systemReset diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index c911b86b..8271ec8e 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -129,6 +129,7 @@ bool commandAT(const char * commandString) break; case ('Z'): //Reboots the radio reportOK(); + outputSerialData(true); systemFlush(); systemReset(); break; diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 8c5d0b33..d9c5f77a 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -474,6 +474,7 @@ void setup() systemPrintTimestamp(); systemPrintln("LRS"); + outputSerialData(true); verifyRadioStateTable(); //Verify that the state table contains all of the states in increasing order @@ -495,6 +496,7 @@ void setup() systemPrintTimestamp(); systemPrintln("LRS Setup Complete"); + outputSerialData(true); triggerEvent(TRIGGER_RADIO_RESET); } diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 480672d3..31dba17e 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -924,7 +924,7 @@ PacketType rcvDatagram() } //Verify that there is sufficient space in the serialTransmitBuffer - if ((sizeof(serialTransmitBuffer) - availableTXBytes()) < rxDataBytes) + if (inCommandMode || ((sizeof(serialTransmitBuffer) - availableTXBytes()) < rxDataBytes)) { if (settings.debugReceive) { diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index d09005e8..168c3abc 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -24,8 +24,6 @@ bool isCTS() //Return number of bytes sitting in the serial transmit buffer uint16_t availableTXBytes() { - if (inCommandMode == true) return (0); //If we are in command mode, block printing of data from the TXBuffer - if (txHead >= txTail) return (txHead - txTail); return (sizeof(serialTransmitBuffer) - txTail + txHead); } @@ -50,6 +48,13 @@ void serialBufferOutput(uint8_t * data, uint16_t dataLength) txHead %= sizeof(serialTransmitBuffer); } +//Add a single byte to the output buffer +void serialOutputByte(uint8_t data) +{ + serialTransmitBuffer[txHead++] = data; + txHead %= sizeof(serialTransmitBuffer); +} + //Update the output of the RTS pin (host says it's ok to send data when assertRTS = true) void updateRTS(bool assertRTS) { @@ -182,7 +187,6 @@ void readyOutgoingCommandPacket() //Scan for escape characters void updateSerial() { - int dataBytes; int x; //Assert RTS when there is enough space in the receive buffer @@ -190,23 +194,8 @@ void updateSerial() && (availableTXBytes() < (sizeof(serialTransmitBuffer) / 4))) updateRTS(true); - //Forget printing if there are ISRs to attend to - dataBytes = availableTXBytes(); - while (dataBytes-- && isCTS() && (!transactionComplete)) - { - txLED(true); //Turn on LED during serial transmissions - - //Take a break if there are ISRs to attend to - petWDT(); - if (timeToHop == true) hopChannel(); - - systemWrite(serialTransmitBuffer[txTail]); - systemFlush(); //Prevent serial hardware from blocking more than this one write - - txTail++; - txTail %= sizeof(serialTransmitBuffer); - } - txLED(false); //Turn off LED + //Attempt to empty the serialTransmitBuffer + outputSerialData(false); //Look for local incoming serial while (rtsAsserted && arch.serialAvailable() && (transactionComplete == false)) @@ -240,7 +229,10 @@ void updateSerial() rxTail %= sizeof(serialReceiveBuffer); if ((settings.echo == true) || (inCommandMode == true)) + { systemWrite(incoming); + outputSerialData(true); + } //Process serial into either rx buffer or command buffer if (inCommandMode == true) @@ -250,6 +242,7 @@ void updateSerial() printerEndpoint = PRINT_TO_SERIAL; systemPrintln(); checkCommand(); //Process command buffer + outputSerialData(true); } else if (incoming == '\n') ; //Do nothing @@ -269,6 +262,7 @@ void updateSerial() } else systemWrite(7); + outputSerialData(true); } else { @@ -291,6 +285,7 @@ void updateSerial() if (escapeCharsReceived == maxEscapeCharacters) { systemPrintln("\r\nOK"); + outputSerialData(true); inCommandMode = true; //Allow AT parsing. Prevent received RF data from being printed. @@ -344,6 +339,29 @@ void updateSerial() } } +void outputSerialData(bool ignoreISR) +{ + int dataBytes; + + //Forget printing if there are ISRs to attend to + dataBytes = availableTXBytes(); + while (dataBytes-- && isCTS() && (ignoreISR || (!transactionComplete))) + { + txLED(true); //Turn on LED during serial transmissions + + //Take a break if there are ISRs to attend to + petWDT(); + if (timeToHop == true) hopChannel(); + + arch.serialWrite(serialTransmitBuffer[txTail]); + systemFlush(); //Prevent serial hardware from blocking more than this one write + + txTail++; + txTail %= sizeof(serialTransmitBuffer); + } + txLED(false); //Turn off LED +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Virtual-Circuit support //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index cb525e47..afa226b1 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -1,15 +1,19 @@ -void systemPrint(const char* value) +void systemPrint(const char* string) { + uint16_t length; + + length = strlen(string); if (printerEndpoint == PRINT_TO_SERIAL) { - arch.serialPrint(value); + for (uint16_t x = 0 ; x < length ; x++) + serialOutputByte(string[x]); } else if (printerEndpoint == PRINT_TO_RF) { //Move these characters into the transmit buffer - for (uint16_t x = 0 ; x < strlen(value) ; x++) + for (uint16_t x = 0 ; x < length ; x++) { - commandTXBuffer[commandTXHead++] = value[x]; + commandTXBuffer[commandTXHead++] = string[x]; commandTXHead %= sizeof(commandTXBuffer); } } @@ -187,7 +191,7 @@ void systemPrintUniqueID(uint8_t * uniqueID) void systemWrite(uint8_t value) { - arch.serialWrite(value); + serialOutputByte(value); } void systemWrite(uint8_t * buffer, uint16_t length) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index d17a7826..30ab45f8 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -422,7 +422,6 @@ typedef void (* ARCH_PET_WDT)(); typedef Module * (* ARCH_RADIO)(); typedef bool (* ARCH_SERIAL_AVAILABLE)(); typedef void (* ARCH_SERIAL_FLUSH)(); -typedef void (* ARCH_SERIAL_PRINT)(const char * value); typedef uint8_t (* ARCH_SERIAL_READ)(); typedef void (* ARCH_SERIAL_WRITE)(uint8_t value); typedef void (* ARCH_SYSTEM_RESET)(); @@ -439,7 +438,6 @@ typedef struct _ARCH_TABLE ARCH_RADIO radio; //Initialize the radio ARCH_SERIAL_AVAILABLE serialAvailable; //Determine if serial data is available ARCH_SERIAL_FLUSH serialFlush; //Flush the serial port - ARCH_SERIAL_PRINT serialPrint; //Print the specified string value ARCH_SERIAL_READ serialRead; //Read a byte from the serial port ARCH_SERIAL_WRITE serialWrite; //Print the specified character ARCH_SYSTEM_RESET systemReset; //Reset the system diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index 372a0dec..28ac9738 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -64,7 +64,7 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati3 -157 ati4 -151 +44 ati5 506 ati6 @@ -74,7 +74,7 @@ ati7 ati8 B2A99BF24A34555020312E34162815FF ati9 -Total datagrams sent: 63 +Total datagrams sent: 70 ati10 Total datagrams received: 0 ati11 From cbe9994ee6723ffd884ec622e89c9ac4a02cc78c Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 10 Nov 2022 10:42:55 -0700 Subject: [PATCH 102/594] Add CRC Error cases to radio states --- Firmware/LoRaSerial_Firmware/States.ino | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ce90160c..53d2a473 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -543,6 +543,11 @@ void updateRadioState() returnToReceiving(); break; + case DATAGRAM_CRC_ERROR: + triggerEvent(TRIGGER_CRC_ERROR); + returnToReceiving(); + break; + case DATAGRAM_PING: v2BreakLink(); break; @@ -736,6 +741,11 @@ void updateRadioState() returnToReceiving(); break; + case DATAGRAM_CRC_ERROR: + triggerEvent(TRIGGER_CRC_ERROR); + returnToReceiving(); + break; + case DATAGRAM_PING: //Break the link v2BreakLink(); From 4628aafe8d363c84dabc44fdb26b56326cb030ea Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 10 Nov 2022 10:47:42 -0700 Subject: [PATCH 103/594] Update settings.h --- Firmware/LoRaSerial_Firmware/settings.h | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 30ab45f8..313aa56f 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -266,6 +266,7 @@ enum TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND, TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, TRIGGER_BAD_PACKET, + TRIGGER_CRC_ERROR, TRIGGER_RTR_2BYTE, TRIGGER_RTR_255BYTE, TRIGGER_BROADCAST_DATA_PACKET, From 45340c991eaa98bcd2b8de4d57a3bfce9c355a15 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 10 Nov 2022 10:52:41 -0700 Subject: [PATCH 104/594] Rename ISRs --- Firmware/LoRaSerial_Firmware/Radio.ino | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 259cc12e..3dd3b26d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -49,8 +49,8 @@ void configureRadio() success = false; } - radio.setDio0Action(dio0ISR); //Called when transmission is finished - radio.setDio1Action(dio1ISR); //Called after a transmission has started, so we can move to next freq + radio.setDio0Action(transactionCompleteISR); //Called when transmission is finished + radio.setDio1Action(hopISR); //Called after a transmission has started, so we can move to next freq if (pin_rxen != PIN_UNDEFINED) radio.setRfSwitchPins(pin_rxen, pin_txen); @@ -224,7 +224,7 @@ void returnToReceiving() //ISR when DIO0 goes low //Called when transmission is complete or when RX is received -void dio0ISR(void) +void transactionCompleteISR(void) { transactionComplete = true; } @@ -232,7 +232,7 @@ void dio0ISR(void) //ISR when DIO1 goes low //Called when FhssChangeChannel interrupt occurs (at regular HoppingPeriods) //We do not use SX based channel hopping, and instead use a synchronized hardware timer -void dio1ISR(void) +void hopISR(void) { clearDIO1 = true; } From cf1f36094e77bde9dd1204511d9016f2ff85caf1 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 10 Nov 2022 11:58:07 -0700 Subject: [PATCH 105/594] Fix triggers --- Firmware/LoRaSerial_Firmware/System.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index afa226b1..944fd3f1 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -334,7 +334,7 @@ void triggerEvent(uint8_t triggerNumber) uint16_t triggerWidth; //Determine if the trigger pin is enabled - if (pin_trigger != PIN_UNDEFINED) + if (pin_trigger == PIN_UNDEFINED) return; //Determine which trigger enable to use From 661e983ef4acda3a3c9fc8b779c4e267ed45613b Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 10 Nov 2022 21:22:38 -0700 Subject: [PATCH 106/594] Implement frequency hopping for multipoint --- .../LoRaSerial_Firmware.ino | 9 +- Firmware/LoRaSerial_Firmware/Radio.ino | 30 +- Firmware/LoRaSerial_Firmware/RadioV2.ino | 108 +++++++- Firmware/LoRaSerial_Firmware/States.ino | 260 +++++++++++++++--- Firmware/LoRaSerial_Firmware/System.ino | 8 + Firmware/LoRaSerial_Firmware/settings.h | 51 ++-- 6 files changed, 384 insertions(+), 82 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index d9c5f77a..9feb4c29 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -338,7 +338,7 @@ bool expectingAck = false; //Used by various send packet functions float frequencyCorrection = 0; //Adjust receive freq based on the last packet received freqError -volatile bool clearDIO1 = true; //Clear the DIO1 hop ISR when possible +volatile bool hop = true; //Clear the DIO1 hop ISR when possible //RSSI must be above these negative numbers for LED to illuminate const int rssiLevelLow = -150; @@ -446,6 +446,9 @@ int8_t txDestVc; unsigned long vcAckTimer; VIRTUAL_CIRCUIT virtualCircuitList[MAX_VC]; +unsigned int multipointChannelLoops = 0; //Count the number of times Multipoint scanning has traversed the table +unsigned int multipointAttempts = 0; //Throttle back scanning when a server is not detected + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables @@ -514,9 +517,9 @@ void loop() updateRadioState(); //Ping/ack/send packets as needed - if (clearDIO1) //Allow DIO1 hop ISR but use it only for debug + if (hop) //Allow DIO1 hop ISR but use it only for debug { - clearDIO1 = false; + hop = false; radio.clearFHSSInt(); //Clear the interrupt } } diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 3dd3b26d..2c8750de 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -222,21 +222,6 @@ void returnToReceiving() } } -//ISR when DIO0 goes low -//Called when transmission is complete or when RX is received -void transactionCompleteISR(void) -{ - transactionComplete = true; -} - -//ISR when DIO1 goes low -//Called when FhssChangeChannel interrupt occurs (at regular HoppingPeriods) -//We do not use SX based channel hopping, and instead use a synchronized hardware timer -void hopISR(void) -{ - clearDIO1 = true; -} - //Given spread factor, bandwidth, coding rate and number of bytes, return total Air Time in ms for packet uint16_t calcAirTime(uint8_t bytesToSend) { @@ -529,3 +514,18 @@ void printSX1276Registers () } #endif //RADIOLIB_LOW_LEVEL + +//ISR when DIO0 goes low +//Called when transmission is complete or when RX is received +void transactionCompleteISR(void) +{ + transactionComplete = true; +} + +//ISR when DIO1 goes low +//Called when FhssChangeChannel interrupt occurs (at regular HoppingPeriods) +//We do not use SX based channel hopping, and instead use a synchronized hardware timer +void hopISR(void) +{ + hop = true; +} diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 31dba17e..b158d3f8 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -325,21 +325,95 @@ void xmitDatagramP2PAck() // Multi-Point Data Exchange //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Send a data datagram to the remote system -void xmitDatagramMpDatagram() +//Send a data datagram to the remote system, including sync data +void xmitDatagramMpData() { + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); + /* - endOfTxData ---. - | - V - +----------+---------+--- ... ---+----------+ - | Optional | | | Optional | - | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +----------+---------+-------------+----------+ + endOfTxData ---. + | + V + +--------+---------+--- ... ---+----------+----------+ + | | | | Channel | Optional | + | NET ID | Control | Data | Timer | Trailer | + | 8 bits | 8 bits | n bytes | 2 bytes | n Bytes | + +--------+---------+-------------+----------+----------+ */ - txControl.datagramType = DATAGRAM_DATAGRAM; + + + txControl.datagramType = DATAGRAM_DATA; + transmitDatagram(); +} + +//Heartbeat packet to sync other units in multipoint mode +void xmitDatagramMpHeartbeat() +{ + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); + + /* + endOfTxData ---. + | + V + +--------+---------+----------+----------+ + | | | Channel | Optional | + | NET ID | Control | Timer | Trailer | + | 8 bits | 8 bits | 2 bytes | n Bytes | + +--------+---------+----------+----------+ + */ + + txControl.datagramType = DATAGRAM_HEARTBEAT; + transmitDatagram(); +} + +//Ack packet sent by server in response the client ping, includes sync data and channel number +//During Multipoint scanning, it's possible for the client to get an ack but be 500kHz off +//The channel Number ensures that the client gets the next hop correct +void xmitDatagramMpAck() +{ + memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); + endOfTxData += sizeof(channelNumber); + + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); + + + /* + endOfTxData ---. + | + V + +--------+---------+---------+----------+----------+ + | | | Channel | Channel | Optional | + | NET ID | Control | Number | Timer | Trailer | + | 8 bits | 8 bits | 1 byte | 2 bytes | n Bytes | + +--------+---------+---------+----------+----------+ + */ + + txControl.datagramType = DATAGRAM_ACK_1; + transmitDatagram(); +} + +//Ping packet sent during scanning +void xmitDatagramMpPing() +{ + /* + endOfTxData ---. + | + V + +--------+---------+----------+ + | | | Optional | + | NET ID | Control | Trailer | + | 8 bits | 8 bits | n Bytes | + +--------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_PING; transmitDatagram(); } @@ -1098,7 +1172,7 @@ PacketType rcvDatagram() datagramsReceived++; linkDownTimer = millis(); - //BLink the RX LED + //Blink the RX LED if (settings.alternateLedUsage) digitalWrite(ALT_LED_RX_DATA, LED_ON); return datagramType; @@ -1543,7 +1617,7 @@ void stopChannelTimer() void syncChannelTimer() { int16_t msToNextHopRemote; - memcpy(&msToNextHopRemote, &rxVcData[0], sizeof(msToNextHopRemote)); + memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); msToNextHopRemote -= ackAirTime; msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; //Can be negative @@ -1638,3 +1712,11 @@ void setHeartbeatLong() heartbeatTimer = millis(); heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% } + +//Only the server sends heartbeats in multipoint mode +//Not random, just the straight timeout +void setHeartbeatMultipoint() +{ + heartbeatTimer = millis(); + heartbeatRandomTime = settings.heartbeatTimeout; +} diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ba9db51f..360aba19 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -103,7 +103,16 @@ void updateRadioState() changeState(RADIO_VC_WAIT_TX_DONE); } else - changeState(RADIO_MP_STANDBY); + { + if (settings.multipointServer == true) + { + startChannelTimer(); //Start hopping + changeState(RADIO_MP_STANDBY); + } + else + changeState(RADIO_MP_BEGIN_SCAN); + } + } break; @@ -820,7 +829,7 @@ void updateRadioState() //Check for ACK timeout, set at end of transmit, measures ACK timeout else if ((receiveInProcess() == false) - && ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime))) + && ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime))) { if (settings.debugDatagrams) { @@ -938,6 +947,135 @@ void updateRadioState() //V2 - Multi-Point Data Exchange //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + case RADIO_MP_BEGIN_SCAN: + stopChannelTimer(); //Stop hopping + + multipointChannelLoops = 0; + multipointAttempts = 0; + + triggerEvent(TRIGGER_MP_SCAN); + changeState(RADIO_MP_SCANNING); + break; + + //Walk through channel table transmitting a Ping and looking for an Ack + case RADIO_MP_SCANNING: + + if (transactionComplete) + { + transactionComplete = false; //Reset ISR flag + + //Decode the received datagram + PacketType packetType = rcvDatagram(); + + //Process the received datagram + switch (packetType) + { + default: + triggerEvent(TRIGGER_UNKNOWN_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_BAD: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_CRC_ERROR: + triggerEvent(TRIGGER_CRC_ERROR); + returnToReceiving(); + break; + + case DATAGRAM_ACK_2: + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_PING: //Clients do not respond to pings, only the server + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + //We should not be receiving these datagrams, but if we do, just ignore + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_ACK_1: + //Server has responded with ack + syncChannelTimer(); //Start and adjust freq hop ISR based on remote's remaining clock + + channelNumber = rxData[0]; //Change to the server's channel number + + printPacketQuality(); + + updateRSSI(); //Adjust LEDs to RSSI level + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + lastPacketReceived = millis(); //Reset + + triggerEvent(TRIGGER_LINK_ACK_RECEIVED); + returnToReceiving(); + changeState(RADIO_MP_STANDBY); + break; + } + } + + //Nothing received + else if (receiveInProcess() == false) + { + //Check for a receive timeout + if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime)) + { + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrintln("MP: ACK1 Timeout"); + } + + //Move to previous channel in table + hopChannelReverse(); + + if (channelNumber == 0) multipointChannelLoops++; + + if (multipointChannelLoops > 10) + { + multipointChannelLoops = 0; + + multipointAttempts++; + //Throttle the scanning. Stop transmitting for 60s times the number of attempts. + unsigned long startTime = millis(); + while ((millis() - startTime) < (60 * 1000 * multipointAttempts)) + { + delay(10); + petWDT(); + } + } + + //Send ping + xmitDatagramMpPing(); + changeState(RADIO_MP_WAIT_TX_PING_DONE); + } + } + + break; + + //Wait for the PING to complete transmission + case RADIO_MP_WAIT_TX_PING_DONE: + if (transactionComplete) + { + transactionComplete = false; //Reset ISR flag + returnToReceiving(); + changeState(RADIO_MP_SCANNING); + } + break; + + //Wait for the ACK to complete transmission + case RADIO_MP_WAIT_TX_ACK_DONE: + if (transactionComplete) + { + transactionComplete = false; //Reset ISR flag + returnToReceiving(); + changeState(RADIO_MP_STANDBY); + } + break; + case RADIO_MP_STANDBY: //Hop channels when required if (timeToHop == true) @@ -946,7 +1084,7 @@ void updateRadioState() //Process the receive packet if (transactionComplete == true) { - triggerEvent(TRIGGER_BROADCAST_PACKET_RECEIVED); + triggerEvent(TRIGGER_MP_PACKET_RECEIVED); transactionComplete = false; //Reset ISR flag //Decode the received datagram @@ -956,55 +1094,120 @@ void updateRadioState() switch (packetType) { default: + triggerEvent(TRIGGER_UNKNOWN_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_BAD: + triggerEvent(TRIGGER_BAD_PACKET); + returnToReceiving(); + break; + + case DATAGRAM_CRC_ERROR: + triggerEvent(TRIGGER_CRC_ERROR); returnToReceiving(); - changeState(RADIO_MP_STANDBY); break; case DATAGRAM_ACK_1: case DATAGRAM_ACK_2: - case DATAGRAM_DATA: + case DATAGRAM_DATAGRAM: case DATAGRAM_DATA_ACK: - case DATAGRAM_HEARTBEAT: - case DATAGRAM_PING: case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: //We should not be receiving these datagrams, but if we do, just ignore frequencyCorrection += radio.getFrequencyError() / 1000000.0; + triggerEvent(TRIGGER_BAD_PACKET); returnToReceiving(); break; - case DATAGRAM_DATAGRAM: + case DATAGRAM_PING: + //A new radio is saying hello + if (settings.multipointServer == true) + { + //Ack their ping with sync data + xmitDatagramMpAck(); + changeState(RADIO_MP_WAIT_TX_ACK_DONE); + } + else + { + returnToReceiving(); + changeState(RADIO_MP_STANDBY); + } + break; + + case DATAGRAM_HEARTBEAT: + //Received data or heartbeat. Sync clock, do not ack. + syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + printPacketQuality(); + updateRSSI(); //Adjust LEDs to RSSI level + frequencyCorrection += radio.getFrequencyError() / 1000000.0; - //Place the data in the serial output buffer - serialBufferOutput(rxData, rxDataBytes); + lastPacketReceived = millis(); //Update timestamp for Link LED + + returnToReceiving(); //No ack when in multipoint mode + changeState(RADIO_MP_STANDBY); + break; + + case DATAGRAM_DATA: + //Received data or heartbeat. Sync clock, do not ack. + syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + + setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer + + printPacketQuality(); + + //Place any available data in the serial output buffer + serialBufferOutput(rxData, rxDataBytes - sizeof(uint16_t)); //Remove the two bytes of sync data updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; - returnToReceiving(); //No response when in broadcasting mode + triggerEvent(TRIGGER_MP_DATA_PACKET); lastPacketReceived = millis(); //Update timestamp for Link LED + + returnToReceiving(); //No ack when in multipoint mode + changeState(RADIO_MP_STANDBY); break; } } - else //Process waiting serial + //If the radio is available, send any data in the serial buffer over the radio + else if (receiveInProcess() == false) { - //If the radio is available, send any data in the serial buffer over the radio - if (receiveInProcess() == false) + heartbeatTimeout = ((millis() - heartbeatTimer) > heartbeatRandomTime); + + //Check for time to send serial data + if (availableRadioTXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) { - if (availableRadioTXBytes()) //If we have bytes + triggerEvent(TRIGGER_MP_DATA_PACKET); + xmitDatagramMpData(); + setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer + changeState(RADIO_MP_WAIT_TX_DONE); + } + + //Only the server transmits heartbeats + else if (settings.multipointServer == true) + { + if (heartbeatTimeout) { - if (processWaitingSerial(false) == true) //If we've hit a frame size or frame-timed-out - { - triggerEvent(TRIGGER_BROADCAST_DATA_PACKET); - xmitDatagramMpDatagram(); - changeState(RADIO_MP_WAIT_TX_DONE); - } + triggerEvent(TRIGGER_HEARTBEAT); + xmitDatagramMpHeartbeat(); + setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer + changeState(RADIO_MP_WAIT_TX_DONE); //Wait for heartbeat to transmit } } - } //End processWaitingSerial + + //If the client hasn't received a packet in too long, return to scanning + else if (settings.multipointServer == false) + { + if ((millis() - lastPacketReceived) > (settings.heartbeatTimeout * 3)) + { + changeState(RADIO_MP_BEGIN_SCAN); + } + } + } //Toggle 2 LEDs if we have recently transmitted if (millis() - datagramTimer < 5000) @@ -1018,12 +1221,9 @@ void updateRadioState() setRSSI(0b0010); } } - else if (millis() - lastPacketReceived < 5000) - updateRSSI(); //Adjust LEDs to RSSI level + else if (millis() - lastPacketReceived > 5000) + setRSSI(0); //Turn off RSSI after 5 seconds of no new packets received - //Turn off RSSI after 5 seconds of no activity - else - setRSSI(0); break; case RADIO_MP_WAIT_TX_DONE: @@ -1035,12 +1235,10 @@ void updateRadioState() if (transactionComplete == true) { transactionComplete = false; //Reset ISR flag + setRSSI(0b0001); returnToReceiving(); changeState(RADIO_MP_STANDBY); - setRSSI(0b0001); } - else if (timeToHop == true) //If the dio1ISR has fired, move to next frequency - hopChannel(); break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1960,7 +2158,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) if (memcmp(vc->uniqueId, id, UNIQUE_ID_BYTES) == 0) { if (!vc->linkUp) - //Send the status message + //Send the status message vcSendLinkStatus(true, myVc); //Update the link status diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 944fd3f1..99f869dc 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -664,3 +664,11 @@ void printPacketQuality() systemPrintln(); } } + +//Toggle a pin. Used for logic analyzer debugging. +void triggerFrequency(uint16_t frequency) +{ + digitalWrite(pin_trigger, LOW); + delayMicroseconds(frequency); + digitalWrite(pin_trigger, HIGH); +} diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 30ab45f8..b047bc00 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -24,6 +24,10 @@ typedef enum RADIO_P2P_LINK_UP_HB_ACK_REXMT, //Multi-Point: Datagrams + RADIO_MP_BEGIN_SCAN, + RADIO_MP_SCANNING, + RADIO_MP_WAIT_TX_PING_DONE, + RADIO_MP_WAIT_TX_ACK_DONE, RADIO_MP_STANDBY, RADIO_MP_WAIT_TX_DONE, @@ -86,25 +90,29 @@ const RADIO_STATE_ENTRY radioStateTable[] = //V2 - Multi-Point data exchange // State RX Name Description - {RADIO_MP_STANDBY, 1, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //15 - {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //16 + {RADIO_MP_BEGIN_SCAN, 0, "MP_BEGIN_SCAN", "V2 MP: Setup for CAD Scanning"}, //15 + {RADIO_MP_SCANNING, 0, "MP_SCANNING", "V2 MP: Scanning for activity"}, //16 + {RADIO_MP_WAIT_TX_PING_DONE, 0, "MP_WAIT_TX_PING_DONE", "V2 MP: Wait for ping to xmit"}, //17 + {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "V2 MP: Wait for ACK to xmit"}, //18 + {RADIO_MP_STANDBY, 1, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //19 + {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //20 //V2 - Multi-Point training client states // State RX Name Description - {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, 0, "MP_WAIT_TX_TRAINING_PING_DONE", "V2 MP: Wait TX training PING done"}, //17 - {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, 1, "MP_WAIT_RX_RADIO_PARAMETERS", "V2 MP: Wait for radio parameters"}, //18 - {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, 0, "MP_WAIT_TX_PARAM_ACK_DONE", "V2 MP: Wait for TX param ACK done"}, //19 + {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, 0, "MP_WAIT_TX_TRAINING_PING_DONE", "V2 MP: Wait TX training PING done"}, //21 + {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, 1, "MP_WAIT_RX_RADIO_PARAMETERS", "V2 MP: Wait for radio parameters"}, //22 + {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, 0, "MP_WAIT_TX_PARAM_ACK_DONE", "V2 MP: Wait for TX param ACK done"}, //23 //V2 - Multi-Point training server states // State RX Name Description - {RADIO_MP_WAIT_FOR_TRAINING_PING, 1, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //20 - {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, 0, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //21 + {RADIO_MP_WAIT_FOR_TRAINING_PING, 1, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //24 + {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, 0, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //25 //V2 - Virtual circuit states - {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "V2 VC: Wait for TX done"}, //22 - {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "V2 VC: Wait for receive"}, //23 - {RADIO_VC_WAIT_TX_DONE_ACK, 0, "VC_WAIT_TX_DONE_ACK", "V2 VC: Wait for TX done then ACK"}, //24 - {RADIO_VC_WAIT_ACK, 1, "VC_WAIT_ACK", "V2 VC: Wait for ACK"}, //25 + {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "V2 VC: Wait for TX done"}, //26 + {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "V2 VC: Wait for receive"}, //27 + {RADIO_VC_WAIT_TX_DONE_ACK, 0, "VC_WAIT_TX_DONE_ACK", "V2 VC: Wait for TX done then ACK"}, //28 + {RADIO_VC_WAIT_ACK, 1, "VC_WAIT_ACK", "V2 VC: Wait for ACK"}, //29 }; //Possible types of packets received @@ -151,7 +159,7 @@ typedef enum } PacketType; const char * const v2DatagramType[] = -{// 0 1 +{ // 0 1 "P2P_TRAINING_PING", "P2P_TRAINING_PARAMS", // 2 3 4 "PING", "ACK-1", "ACK-2", @@ -210,13 +218,13 @@ typedef struct _VIRTUAL_CIRCUIT */ uint8_t rmtTxAckNumber; //Next expected ACK # from remote system in DATA frame, - //incremented upon match to received DATA frame ACK number, - //indicates frame was received and processed - //Duplicate frame if received ACK # == (rmtTxAckNumber -1) + //incremented upon match to received DATA frame ACK number, + //indicates frame was received and processed + //Duplicate frame if received ACK # == (rmtTxAckNumber -1) uint8_t rxAckNumber; //Received ACK # of the most recent acknowledged DATA frame, - //does not get incremented, used to ACK the data frame + //does not get incremented, used to ACK the data frame uint8_t txAckNumber; //# of next ACK to be sent by the local system in DATA frame, - //incremented when successfully acknowledged via DATA_ACK frame + //incremented when successfully acknowledged via DATA_ACK frame } VIRTUAL_CIRCUIT; #include "Virtual_Circuit_Protocol.h" @@ -250,6 +258,9 @@ enum TRIGGER_LINK_WAIT_FOR_ACK, TRIGGER_LINK_DATA_XMIT, TRIGGER_LINK_RETRANSMIT_FAIL, + TRIGGER_MP_SCAN, + TRIGGER_MP_DATA_PACKET, + TRIGGER_MP_PACKET_RECEIVED, TRIGGER_HANDSHAKE_ACK1_TIMEOUT, TRIGGER_HANDSHAKE_SEND_PING, TRIGGER_HANDSHAKE_SEND_PING_COMPLETE, @@ -266,10 +277,9 @@ enum TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND, TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, TRIGGER_BAD_PACKET, + TRIGGER_CRC_ERROR, TRIGGER_RTR_2BYTE, TRIGGER_RTR_255BYTE, - TRIGGER_BROADCAST_DATA_PACKET, - TRIGGER_BROADCAST_PACKET_RECEIVED, TRIGGER_TRAINING_CONTROL_PACKET, TRIGGER_TRAINING_DATA_PACKET, TRIGGER_TRAINING_NO_ACK, @@ -398,7 +408,8 @@ typedef struct struct_settings { bool invertRts = false; //Invert the output of RTS bool alternateLedUsage = false; //Enable alternate LED usage uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training - + bool multipointServer = false; //Only one radio can be the server in multipoint mode + //Add new parameters immediately before this line //-- Add commands to set the parameters //-- Add parameters to routine updateRadioParameters From cf9dbd3144bfd26c6d92da677109f43e35edced9 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 10 Nov 2022 21:22:57 -0700 Subject: [PATCH 107/594] Update programming batch file --- Binaries/Bootloader_Combined/batch_program.bat | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Binaries/Bootloader_Combined/batch_program.bat b/Binaries/Bootloader_Combined/batch_program.bat index 8bacc213..f1b7b0a4 100644 --- a/Binaries/Bootloader_Combined/batch_program.bat +++ b/Binaries/Bootloader_Combined/batch_program.bat @@ -8,9 +8,13 @@ if [%1]==[] goto usage @echo - @echo Programming binary: %1 +@echo Unlock bootloader +atprogram.exe -t atmelice -i swd -cl 20mhz -d atsamd21g18a write --fuses --offset 0x804000 --values 0xFFC7E0D8 + +@echo Programming firmware atprogram.exe -t atmelice -i swd -cl 20mhz -d atsamd21g18a chiperase program -f %1 --verify -rem Lock bootloader +@echo Lock bootloader atprogram.exe -t atmelice -i swd -cl 20mhz -d atsamd21g18a write --fuses --offset 0x804000 --values 0xFAC7E0D8 @echo Done programming! Ready for next board. From e6f7bdcf64ec2e179b2b99a3011b11d1c5117cb4 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 10 Nov 2022 21:41:11 -0700 Subject: [PATCH 108/594] Add multipoint server command --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 8271ec8e..b548eec4 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -687,6 +687,7 @@ const COMMAND_ENTRY commands[] = {59, 0, 1, 0, TYPE_BOOL, valInt, "InvertRts", &settings.invertRts}, {60, 0, 1, 0, TYPE_BOOL, valInt, "AlternateLedUsage", &settings.alternateLedUsage}, + {61, 0, 1, 0, TYPE_BOOL, valInt, "MultipointServer", &settings.multipointServer}, //Define any user parameters starting at 255 decrementing towards 0 }; From 613067110870d07de7386caf790c72ad3bcabc1c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 8 Nov 2022 15:47:15 -1000 Subject: [PATCH 109/594] Remove command numbers and a few settings, add new commands New and updated commands: * ATD - Display debug settings * ATP - Display probe trigger settings * ATR - Display radio settings * ATS - Display serial settings * AT-? - Display radio and serial settings * ATI0 - Display all settings Removed the following settings and corresponding commands: * radioProtocolVersion * printParameterName * sortParametersByName --- Firmware/LoRaSerial_Firmware/Commands.ino | 326 ++-- Firmware/LoRaSerial_Firmware/Radio.ino | 1 - Firmware/LoRaSerial_Firmware/RadioV2.ino | 5 - Firmware/LoRaSerial_Firmware/States.ino | 45 +- Firmware/LoRaSerial_Firmware/Train.ino | 27 +- Firmware/LoRaSerial_Firmware/settings.h | 6 +- Firmware/Tools/Command_Validation_Script.txt | 787 +++++----- .../Results/Command_Validation_Results.txt | 1368 +++++++---------- 8 files changed, 1114 insertions(+), 1451 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index b548eec4..b43b278c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -49,20 +49,24 @@ bool commandAT(const char * commandString) case ('?'): //Display the command help systemPrintln("Command summary:"); systemPrintln(" AT? - Print the command summary"); + systemPrintln(" ATD - Display the debug settings"); systemPrintln(" ATF - Enter training mode and return to factory defaults"); systemPrintln(" ATG - Generate new netID and encryption key"); systemPrintln(" ATI - Display the radio version"); systemPrintln(" ATI? - Display the information commands"); systemPrintln(" ATIn - Display system information"); + systemPrintln(" ATL - VC link reset"); systemPrintln(" ATO - Exit command mode"); - systemPrintln(" ATR - VC link reset"); + systemPrintln(" ATP - Display probe trigger settings"); systemPrintln(" ATSn=xxx - Set parameter n's value to xxx"); systemPrintln(" ATSn? - Print parameter n's current value"); systemPrintln(" ATT - Enter training mode"); + systemPrintln(" ATV - Display virtual circuit settings"); systemPrintln(" ATX - Stop the training server"); systemPrintln(" ATZ - Reboot the radio"); systemPrintln(" AT-Param=xxx - Set parameter's value to xxx by name (Param)"); systemPrintln(" AT-Param? - Print parameter's current value by name (Param)"); + systemPrintln(" AT-? - Display the setting values"); systemPrintln(" AT&F - Restore factory settings"); systemPrintln(" AT&W - Save current settings to NVM"); break; @@ -173,7 +177,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI26 - Display the total number of bad CRC frames"); break; case ('0'): //ATI0 - Show user settable parameters - displayParameters(); + displayParameters(0, true); break; case ('1'): //ATI1 - Show board variant systemPrint("SparkFun LoRaSerial "); @@ -399,7 +403,12 @@ bool sendRemoteCommand(const char * commandString) //---------------------------------------- const COMMAND_PREFIX prefixTable[] = { - {"ATS", commandSet}, + {"ATD", commandDisplayDebug}, + {"ATP", commandDisplayProbe}, + {"ATR", commandDisplayRadio}, + {"ATS", commandDisplaySerial}, + {"ATV", commandDisplayVirtualCircuit}, + {"AT-?", commandDisplayAll}, {"AT-", commandSetByName}, {"AT", commandAT}, {"RT", sendRemoteCommand}, @@ -469,6 +478,42 @@ char * trimCommand() return commandString; } +bool commandDisplayAll(const char * commandString) +{ + displayParameters(0, false); + return true; +} + +bool commandDisplayDebug(const char * commandString) +{ + displayParameters('D', false); + return true; +} + +bool commandDisplayProbe(const char * commandString) +{ + displayParameters('P', false); + return true; +} + +bool commandDisplayRadio(const char * commandString) +{ + displayParameters('R', false); + return true; +} + +bool commandDisplaySerial(const char * commandString) +{ + displayParameters('S', false); + return true; +} + +bool commandDisplayVirtualCircuit(const char * commandString) +{ + displayParameters('V', false); + return true; +} + //---------------------------------------- // Data validation routines //---------------------------------------- @@ -613,83 +658,86 @@ bool valSpeedSerial (void * value, uint32_t valMin, uint32_t valMax) //---------------------------------------- const COMMAND_ENTRY commands[] = -{ //#, min, max, digits, type, validation, name, setting addr - {0, 0, 0, 0, TYPE_SPEED_SERIAL, valSpeedSerial, "SerialSpeed", &settings.serialSpeed}, - {1, 0, 0, 0, TYPE_SPEED_AIR, valSpeedAir, "AirSpeed", &settings.airSpeed}, - {2, 0, 255, 0, TYPE_U8, valInt, "netID", &settings.netID}, - {3, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &settings.operatingMode}, - {4, 0, 1, 0, TYPE_BOOL, valInt, "EncryptData", &settings.encryptData}, - - {5, 0, 0, 0, TYPE_KEY, valKey, "EncryptionKey", &settings.encryptionKey}, - {6, 0, 1, 0, TYPE_BOOL, valInt, "DataScrambling", &settings.dataScrambling}, - {7, 14, 30, 0, TYPE_U8, valInt, "TxPower", &settings.radioBroadcastPower_dbm}, - {8, 902, 0, 3, TYPE_FLOAT, valFreqMin, "FrequencyMin", &settings.frequencyMin}, - {9, 0, 928, 3, TYPE_FLOAT, valFreqMax, "FrequencyMax", &settings.frequencyMax}, - - {10, 1, 255, 0, TYPE_U8, valInt, "NumberOfChannels", &settings.numberOfChannels}, - {11, 0, 1, 0, TYPE_BOOL, valInt, "FrequencyHop", &settings.frequencyHop}, - {12, 10, 65535, 0, TYPE_U16, valInt, "MaxDwellTime", &settings.maxDwellTime}, - {13, 0, 0, 2, TYPE_FLOAT, valBandwidth, "Bandwidth", &settings.radioBandwidth}, - {14, 6, 12, 0, TYPE_U8, valOverride, "SpreadFactor", &settings.radioSpreadFactor}, - - {15, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &settings.radioCodingRate}, - {16, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &settings.radioSyncWord}, - {17, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &settings.radioPreambleLength}, - {18, 0, MAX_VC-1,0, TYPE_U8, valInt, "CmdVC", &cmdVc}, - {19, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &settings.serialTimeoutBeforeSendingFrame_ms}, - - {20, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, - {21, 0, 1, 0, TYPE_BOOL, valInt, "Echo", &settings.echo}, - {22, 250, 65535, 0, TYPE_U16, valInt, "HeartBeatTimeout", &settings.heartbeatTimeout}, - {23, 0, 1, 0, TYPE_BOOL, valInt, "FlowControl", &settings.flowControl}, - {24, 0, 1, 0, TYPE_BOOL, valInt, "AutoTune", &settings.autoTuneFrequency}, - - {25, 0, 1, 0, TYPE_BOOL, valInt, "DisplayPacketQuality", &settings.displayPacketQuality}, - {26, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &settings.maxResends}, - {27, 0, 1, 0, TYPE_BOOL, valInt, "SortParametersByName", &settings.sortParametersByName}, - {28, 0, 1, 0, TYPE_BOOL, valInt, "PrintParameterName", &settings.printParameterName}, - {29, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &settings.printFrequency}, - - {30, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &settings.debugRadio}, - {31, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &settings.debugStates}, - {32, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &settings.debugTraining}, - {33, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &settings.trainingTimeout}, - {34, 0, 1, 0, TYPE_BOOL, valInt, "UsbSerialWait", &settings.usbSerialWait}, - - {35, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &settings.printRfData}, - {36, 0, 1, 0, TYPE_BOOL, valInt, "PrintPktData", &settings.printPktData}, - {37, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &settings.verifyRxNetID}, - {38, 1, 255, 0, TYPE_U8, valInt, "TriggerWidth", &settings.triggerWidth}, - {39, 0, 1, 0, TYPE_BOOL, valInt, "TriggerWidthIsMultiplier", &settings.triggerWidthIsMultiplier}, - - {40, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_31-0", &settings.triggerEnable}, - {41, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_63-32", &settings.triggerEnable2}, - {42, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &settings.debugReceive}, - {43, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, - {44, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, - - {45, 2, 2, 0, TYPE_U8, valInt, "radioProtocolVersion", &settings.radioProtocolVersion}, - {46, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, - {47, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, - {48, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, - {49, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &settings.enableCRC16}, - - {50, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &settings.displayRealMillis}, - {51, 0, 1, 0, TYPE_BOOL, valInt, "TrainingServer", &settings.trainingServer}, - {52, 1, 255, 0, TYPE_U8, valInt, "ClientRetryInterval", &settings.clientPingRetryInterval}, - {53, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &settings.copyDebug}, - {54, 0, 1, 0, TYPE_BOOL, valInt, "CopySerial", &settings.copySerial}, - - {55, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &settings.copyTriggers}, - {56, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &settings.trainingKey}, - {57, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &settings.printLinkUpDown}, - {58, 0, 1, 0, TYPE_BOOL, valInt, "InvertCts", &settings.invertCts}, - {59, 0, 1, 0, TYPE_BOOL, valInt, "InvertRts", &settings.invertRts}, - - {60, 0, 1, 0, TYPE_BOOL, valInt, "AlternateLedUsage", &settings.alternateLedUsage}, - {61, 0, 1, 0, TYPE_BOOL, valInt, "MultipointServer", &settings.multipointServer}, - - //Define any user parameters starting at 255 decrementing towards 0 +{ + /*Debug parameters + Ltr, All, min, max, digits, type, validation, name, setting addr */ + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "AlternateLedUsage", &settings.alternateLedUsage}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &settings.copyDebug}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &settings.debugRadio}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &settings.debugReceive}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &settings.debugStates}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &settings.debugTraining}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayPacketQuality", &settings.displayPacketQuality}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &settings.displayRealMillis}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &settings.printFrequency}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &settings.printLinkUpDown}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintPktData", &settings.printPktData}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &settings.printRfData}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, + + /*Radio parameters + Ltr, All, min, max, digits, type, validation, name, setting addr */ + {'R', 0, 0, 0, 0, TYPE_SPEED_AIR, valSpeedAir, "AirSpeed", &settings.airSpeed}, + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "AutoTune", &settings.autoTuneFrequency}, + {'R', 0, 0, 0, 2, TYPE_FLOAT, valBandwidth, "Bandwidth", &settings.radioBandwidth}, + {'R', 0, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &settings.radioCodingRate}, + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "FrequencyHop", &settings.frequencyHop}, + {'R', 0, 0, 928, 3, TYPE_FLOAT, valFreqMax, "FrequencyMax", &settings.frequencyMax}, + {'R', 0, 902, 0, 3, TYPE_FLOAT, valFreqMin, "FrequencyMin", &settings.frequencyMin}, + {'R', 0, 10, 65535, 0, TYPE_U16, valInt, "MaxDwellTime", &settings.maxDwellTime}, + {'R', 0, 1, 255, 0, TYPE_U8, valInt, "NumberOfChannels", &settings.numberOfChannels}, + {'R', 0, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &settings.radioPreambleLength}, + {'R', 0, 6, 12, 0, TYPE_U8, valOverride, "SpreadFactor", &settings.radioSpreadFactor}, + {'R', 0, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &settings.radioSyncWord}, + {'R', 0, 14, 30, 0, TYPE_U8, valInt, "TxPower", &settings.radioBroadcastPower_dbm}, + + /*Radio protocol parameters + Ltr, All, min, max, digits, type, validation, name, setting addr */ + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "DataScrambling", &settings.dataScrambling}, + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &settings.enableCRC16}, + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "EncryptData", &settings.encryptData}, + {'R', 0, 0, 0, 0, TYPE_KEY, valKey, "EncryptionKey", &settings.encryptionKey}, + {'R', 0, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &settings.serialTimeoutBeforeSendingFrame_ms}, + {'R', 0, 250, 65535, 0, TYPE_U16, valInt, "HeartBeatTimeout", &settings.heartbeatTimeout}, + {'R', 0, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &settings.maxResends}, + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "MultipointServer", &settings.multipointServer}, + {'R', 0, 0, 255, 0, TYPE_U8, valInt, "NetID", &settings.netID}, + {'R', 0, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &settings.operatingMode}, + {'R', 0, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &settings.verifyRxNetID}, + + /*Serial parameters + Ltr, All, min, max, digits, type, validation, name, setting addr */ + {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "CopySerial", &settings.copySerial}, + {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "Echo", &settings.echo}, + {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "FlowControl", &settings.flowControl}, + {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "InvertCts", &settings.invertCts}, + {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "InvertRts", &settings.invertRts}, + {'S', 0, 0, 0, 0, TYPE_SPEED_SERIAL, valSpeedSerial, "SerialSpeed", &settings.serialSpeed}, + {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "UsbSerialWait", &settings.usbSerialWait}, + + /*Training parameters + Ltr, All, min, max, digits, type, validation, name, setting addr */ + {'R', 0, 1, 255, 0, TYPE_U8, valInt, "ClientPingRetryInterval", &settings.clientPingRetryInterval}, + {'R', 0, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &settings.trainingKey}, + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "TrainingServer", &settings.trainingServer}, + {'R', 0, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &settings.trainingTimeout}, + + /*Trigger parameters + Ltr, All, min, max, digits, type, validation, name, setting addr */ + {'P', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &settings.copyTriggers}, + {'P', 1, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_31-0", &settings.triggerEnable}, + {'P', 1, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_63-32", &settings.triggerEnable2}, + {'P', 1, 1, 255, 0, TYPE_U8, valInt, "TriggerWidth", &settings.triggerWidth}, + {'P', 1, 0, 1, 0, TYPE_BOOL, valInt, "TriggerWidthIsMultiplier", &settings.triggerWidthIsMultiplier}, + + /*Virtual circuit parameters + Ltr, All, min, max, digits, type, validation, name, setting addr */ + {'V', 0, 0, MAX_VC-1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, }; const int commandCount = sizeof(commands) / sizeof(commands[0]); @@ -698,50 +746,8 @@ const int commandCount = sizeof(commands) / sizeof(commands[0]); // ATSxx routines //---------------------------------------- -const char * commandGetNumber(const char * buffer, uint32_t * value) +void commandDisplay(const COMMAND_ENTRY * command) { - int number; - - //Assume an invalid number - number = -1; - if ((*buffer >= '0') && (*buffer <= '9')) - { - //Get the number - number = 0; - while ((*buffer >= '0') && (*buffer <= '9')) - number = (number * 10) + *buffer++ - '0'; - } - - //Return the command number and the pointer to the next character - *value = number; - return buffer; -} - -void commandDisplay(uint8_t number, bool printName) -{ - const COMMAND_ENTRY * command; - const COMMAND_ENTRY * commandEnd; - - //Locate the command - command = &commands[0]; - commandEnd = &commands[commandCount]; - while (command < commandEnd) - if (command->number == number) - break; - else - command++; - - //Verify the command number - if (command >= commandEnd) - return; - - //Print the setting name - if (printName) - { - systemPrint(command->name); - systemPrint("="); - } - //Print the setting value switch (command->type) { @@ -815,33 +821,6 @@ bool commandSetByName(const char * commandString) return commandSetOrDisplayValue(command, buffer); } -//Set or display the command -bool commandSet(const char * commandString) -{ - const char * buffer; - const COMMAND_ENTRY * command; - int index; - uint32_t number; - - //Validate the command number - buffer = commandGetNumber(&commandString[3], &number); - command = NULL; - for (index = 0; index < commandCount; index++) - if (number == commands[index].number) - { - command = &commands[index]; - break; - } - - //Verify that the parameter was found - if (!command) - //Report the error - return false; - - //Process this command - return commandSetOrDisplayValue(command, buffer); -} - //Set or display the command bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer) { @@ -853,7 +832,7 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer //Is this a display request if (strcmp(buffer, "?") == 0) { - commandDisplay(command->number, settings.printParameterName); + commandDisplay(command); return true; } @@ -906,10 +885,6 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer if (valid == false) break; - //Display the parameter if requested - if (settings.printParameterName) - commandDisplay(command->number, true); - //The parameter was successfully set reportOK(); return true; @@ -927,7 +902,7 @@ void displayEncryptionKey(uint8_t * key) } //Show current settings in user friendly way -void displayParameters() +void displayParameters(char letter, bool displayAll) { int index; uint8_t sortOrder[commandCount]; @@ -939,29 +914,32 @@ void displayParameters() sortOrder[index] = index; //Perform a bubble sort if requested - if (settings.sortParametersByName) - for (index = 0; index < commandCount; index++) - for (x = index + 1; x < commandCount; x++) - if (stricmp(commands[sortOrder[index]].name, commands[sortOrder[x]].name) > 0) - { - temp = sortOrder[index]; - sortOrder[index] = sortOrder[x]; - sortOrder[x] = temp; - } + for (index = 0; index < commandCount; index++) + for (x = index + 1; x < commandCount; x++) + if (stricmp(commands[sortOrder[index]].name, commands[sortOrder[x]].name) > 0) + { + temp = sortOrder[index]; + sortOrder[index] = sortOrder[x]; + sortOrder[x] = temp; + } //Print the parameters for (index = 0; index < commandCount; index++) { - petWDT(); //Printing may take longer than WDT at 9600, so pet the WDT. + if (displayAll || (letter == commands[sortOrder[index]].letter) + || ((letter == 0) && (!commands[sortOrder[index]].requireAll))) + { + petWDT(); //Printing may take longer than WDT at 9600, so pet the WDT. - if (printerEndpoint == PRINT_TO_RF) - systemPrint("R"); //If someone is asking for our settings over RF, respond with 'R' style settings - else - systemPrint("A"); + if (printerEndpoint == PRINT_TO_RF) + systemPrint("R"); //If someone is asking for our settings over RF, respond with 'R' style settings + else + systemPrint("A"); - systemPrint("TS"); - systemPrint(commands[sortOrder[index]].number); - systemPrint(":"); - commandDisplay(commands[sortOrder[index]].number, true); + systemPrint("T-"); + systemPrint(commands[sortOrder[index]].name); + systemPrint("="); + commandDisplay(&commands[sortOrder[index]]); + } } } diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 2c8750de..b1a8f21f 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -290,7 +290,6 @@ void generateHopTable() + (uint16_t)settings.radioBandwidth + settings.radioSpreadFactor + settings.verifyRxNetID - + settings.radioProtocolVersion + settings.overheadTime + settings.enableCRC16 + settings.clientPingRetryInterval; diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index b158d3f8..18290c98 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -499,15 +499,10 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.autoTuneFrequency = params.autoTuneFrequency; originalSettings.maxResends = params.maxResends; originalSettings.verifyRxNetID = params.verifyRxNetID; - originalSettings.radioProtocolVersion = params.radioProtocolVersion; originalSettings.overheadTime = params.overheadTime; originalSettings.enableCRC16 = params.enableCRC16; originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; - //Update the API parameters - originalSettings.sortParametersByName = params.sortParametersByName; - originalSettings.printParameterName = params.printParameterName; - //Update the debug parameters if (params.copyDebug) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 360aba19..1acba3a7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -84,35 +84,30 @@ void updateRadioState() transmitTimer = 0; //Start the link between the radios - if (settings.radioProtocolVersion >= 2) + if (settings.operatingMode == MODE_POINT_TO_POINT) + changeState(RADIO_P2P_LINK_DOWN); + else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { - //Start the V2 protocol - if (settings.operatingMode == MODE_POINT_TO_POINT) - changeState(RADIO_P2P_LINK_DOWN); - else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - { - if (settings.trainingServer) - //Reserve the server's address (0) - myVc = vcIdToAddressByte(VC_SERVER, myUniqueId); - else - //Unknown client address - myVc = VC_UNASSIGNED; - - //Start sending heartbeats - xmitVcHeartbeat(myVc, myUniqueId); - changeState(RADIO_VC_WAIT_TX_DONE); - } + if (settings.trainingServer) + //Reserve the server's address (0) + myVc = vcIdToAddressByte(VC_SERVER, myUniqueId); else + //Unknown client address + myVc = VC_UNASSIGNED; + + //Start sending heartbeats + xmitVcHeartbeat(myVc, myUniqueId); + changeState(RADIO_VC_WAIT_TX_DONE); + } + else + { + if (settings.multipointServer == true) { - if (settings.multipointServer == true) - { - startChannelTimer(); //Start hopping - changeState(RADIO_MP_STANDBY); - } - else - changeState(RADIO_MP_BEGIN_SCAN); + startChannelTimer(); //Start hopping + changeState(RADIO_MP_STANDBY); } - + else + changeState(RADIO_MP_BEGIN_SCAN); } break; diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 05178991..8593ec7f 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -1,25 +1,14 @@ //Select the training protocol void selectTraining(bool defaultTraining) { - if (settings.radioProtocolVersion >= 2) - { - if (settings.operatingMode == MODE_POINT_TO_POINT) - beginTrainingPointToPoint(defaultTraining); - else - { - if (settings.trainingServer) - beginTrainingServer(); - else - beginTrainingClient(); - } - } + if (settings.operatingMode == MODE_POINT_TO_POINT) + beginTrainingPointToPoint(defaultTraining); else { - //Handle unknown future versions - systemPrint("Unknown protocol version: "); - systemPrintln(settings.radioProtocolVersion); - while (1) - petWDT(); + if (settings.trainingServer) + beginTrainingServer(); + else + beginTrainingClient(); } } @@ -175,7 +164,6 @@ void commonTrainingInitialization() settings.dataScrambling = true; // 6: Scramble the data settings.radioBroadcastPower_dbm = 14; // 7: Minimum, assume radios are near each other settings.frequencyHop = false; //11: Stay on the training frequency - settings.printParameterName = true; //28: Print the parameter names settings.verifyRxNetID = false; //37: Disable netID checking settings.enableCRC16 = true; //49: Use CRC-16 memcpy(&settings.trainingKey, &originalSettings.trainingKey, AES_KEY_BYTES); //56: Common training key @@ -188,7 +176,6 @@ void commonTrainingInitialization() { settings.debug = originalSettings.debug; settings.displayPacketQuality = originalSettings.displayPacketQuality; - settings.printParameterName = originalSettings.printParameterName; settings.printFrequency = originalSettings.printFrequency; settings.debugRadio = originalSettings.debugRadio; @@ -236,7 +223,7 @@ void endClientServerTraining(uint8_t event) settings = originalSettings; //Return to original radio settings if (settings.debugTraining) - displayParameters(); + displayParameters(0, settings.copyDebug || settings.copyTriggers); if (!settings.trainingServer) { diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index b047bc00..528e6966 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -333,7 +333,8 @@ typedef bool (* VALIDATION_ROUTINE)(void * value, uint32_t valMin, uint32_t valM typedef struct _COMMAND_ENTRY { - uint8_t number; + char letter; + char requireAll; uint32_t minValue; uint32_t maxValue; uint8_t digits; @@ -374,8 +375,6 @@ typedef struct struct_settings { bool autoTuneFrequency = false; //Based on the last packets frequency error, adjust our next transaction frequency bool displayPacketQuality = false; //Print RSSI, SNR, and freqError for received packets uint8_t maxResends = 0; //Attempt resends up to this number, 0 = infinite retries - bool sortParametersByName = false; //Sort the parameter list (ATI0) by parameter name - bool printParameterName = false; //Print the parameter name in the ATSx? response bool printFrequency = false; //Print the updated frequency bool debugRadio = false; //Print radio info bool debugStates = false; //Print state changes @@ -391,7 +390,6 @@ typedef struct struct_settings { bool debugReceive = false; //Print receive processing bool debugTransmit = false; //Print transmit processing bool printTxErrors = false; //Print any transmit errors - uint8_t radioProtocolVersion = 2; //Select the radio protocol bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds bool debugDatagrams = false; //Print the datagrams uint16_t overheadTime = 10; //ms added to ack and datagram times before ACK timeout occurs diff --git a/Firmware/Tools/Command_Validation_Script.txt b/Firmware/Tools/Command_Validation_Script.txt index a54cd67b..7b5bf449 100644 --- a/Firmware/Tools/Command_Validation_Script.txt +++ b/Firmware/Tools/Command_Validation_Script.txt @@ -37,518 +37,441 @@ ati26 ati27 ati0 -at-PrintParameterName=0 - at-SerialSpeed=0 -ats0=1 -ats0=40 -ats0=50 -ats0=110 -ats0=134 -ats0=150 -ats0=300 -ats0=400 -ats0=600 -ats0=1200 +at-SerialSpeed=1 +at-SerialSpeed=40 +at-SerialSpeed=50 +at-SerialSpeed=110 +at-SerialSpeed=134 +at-SerialSpeed=150 +at-SerialSpeed=300 +at-SerialSpeed=400 +at-SerialSpeed=600 +at-SerialSpeed=1200 at-SerialSpeed=2400 -ats0=4800 -ats0=9600 -ats0=14400 -ats0=19200 -ats0=28800 -ats0=38400 -ats0=57600 -ats0=76800 -ats0=115200 -ats0=128000 -ats0=230400 -ats0=250000 -ats0=256000 -ats0=460800 -ats0=500000 -ats0=512000 -ats0=576000 -ats0=921600 -ats0=1000000 -ats0=1843200 -ats0=2000000 -ats0=3000000 -ats0=3686400 -ats0=4000000 -ats0=57600 -ats0? +at-SerialSpeed=4800 +at-SerialSpeed=9600 +at-SerialSpeed=14400 +at-SerialSpeed=19200 +at-SerialSpeed=28800 +at-SerialSpeed=38400 +at-SerialSpeed=57600 +at-SerialSpeed=76800 +at-SerialSpeed=115200 +at-SerialSpeed=128000 +at-SerialSpeed=230400 +at-SerialSpeed=250000 +at-SerialSpeed=256000 +at-SerialSpeed=460800 +at-SerialSpeed=500000 +at-SerialSpeed=512000 +at-SerialSpeed=576000 +at-SerialSpeed=921600 +at-SerialSpeed=1000000 +at-SerialSpeed=1843200 +at-SerialSpeed=2000000 +at-SerialSpeed=3000000 +at-SerialSpeed=3686400 +at-SerialSpeed=4000000 +at-SerialSpeed=57600 at-SerialSpeed? -ats1=1 +at-AirSpeed=1 at-AirSpeed=40 -ats1=50 -ats1=110 -ats1=134 -ats1=150 -ats1=300 -ats1=400 -ats1=600 -ats1=1200 -ats1=2400 -ats1=4800 -ats1=9600 -ats1=14400 -ats1=19200 -ats1=28800 -ats1=38400 -ats1=57600 -ats1=76800 -ats1=115200 -ats1=128000 -ats1=230400 -ats1=250000 -ats1=256000 -ats1=460800 -ats1=500000 -ats1=512000 -ats1=576000 -ats1=921600 -ats1=1000000 -ats1=1843200 -ats1=2000000 -ats1=3000000 -ats1=3686400 -ats1=4000000 -ats1=0 -ats1? +at-AirSpeed=50 +at-AirSpeed=110 +at-AirSpeed=134 +at-AirSpeed=150 +at-AirSpeed=300 +at-AirSpeed=400 +at-AirSpeed=600 +at-AirSpeed=1200 +at-AirSpeed=2400 +at-AirSpeed=4800 +at-AirSpeed=9600 +at-AirSpeed=14400 +at-AirSpeed=19200 +at-AirSpeed=28800 +at-AirSpeed=38400 +at-AirSpeed=57600 +at-AirSpeed=76800 +at-AirSpeed=115200 +at-AirSpeed=128000 +at-AirSpeed=230400 +at-AirSpeed=250000 +at-AirSpeed=256000 +at-AirSpeed=460800 +at-AirSpeed=500000 +at-AirSpeed=512000 +at-AirSpeed=576000 +at-AirSpeed=921600 +at-AirSpeed=1000000 +at-AirSpeed=1843200 +at-AirSpeed=2000000 +at-AirSpeed=3000000 +at-AirSpeed=3686400 +at-AirSpeed=4000000 +at-AirSpeed=0 at-AirSpeed? -at-NetID=0 -ats2=255 -ats2=192 -ats2? -at-NetID? - -at-OperatingMode=0 -ats3=2 -ats3=3 -ats3=1 -ats3? -at-OperatingMode? - -at-EncryptData=0 -ats4=2 -ats4=1 -ats4? -at-EncryptData? - -at-EncryptionKey=00112233445566778899aabbccddeeff -ats5? -ats5=00112233445566778899aabbccddeef -ats5=00112233445566778899aabbccddeefff -ats5=00112233445566778899aabbccddeefg -ats5=37782141A665734E4475672AE6308308 -ats5? -at-EncryptionKey? +at-AlternateLedUsage=1 +at-AlternateLedUsage=2 +at-AlternateLedUsage=0 +at-AlternateLedUsage? -at-DataScrambling=1 -ats6=2 -ats6=0 -ats6? -at-DataScrambling? +at-AutoTune=1 +at-AutoTune=2 +at-AutoTune=0 +at-AutoTune? -ats7=13 -at-TxPower=14 -ats7=31 -ats7=30 -ats7? -at-TxPower? +at-AirSpeed=4800 -at-FrequencyMin=928 -ats8=0 -ats8=928.1 -ats8=928 -ats8=927.9 -ats8=901.9 -ats8=902 -ats8? -at-FrequencyMin? +at-Bandwidth=0 +at-Bandwidth=10.4 +at-Bandwidth=15.6 +at-Bandwidth=20 +at-Bandwidth=31.25 +at-Bandwidth=41.7 +at-Bandwidth=62.5 +at-Bandwidth=125 +at-Bandwidth=250 +at-Bandwidth=500 -ats9=0 -ats9=901.9 -at-FrequencyMin=902 -ats9=902.1 -ats9=927.9 -ats9=928.1 -ats9=928 -ats9? -at-FrequencyMin? +at-AirSpeed=0 -ats10=0 -at-NumberOfChannels=1 -ats10=255 -ats10=256 -ats10=50 -ats10? -at-NumberOfChannels? +at-Bandwidth=0 +at-Bandwidth=10.4 +at-Bandwidth=15.6 +at-Bandwidth=20.8 +at-Bandwidth=31.25 +at-Bandwidth=41.7 +at-Bandwidth=62.5 +at-Bandwidth=125 +at-Bandwidth=250 +at-Bandwidth=500 +at-Bandwidth? -at-FrequencyHop=0 -ats11=2 -ats11=1 -ats11? -at-FrequencyHop? +at-ClientRetryInterval=1 +at-ClientRetryInterval=2 +at-ClientRetryInterval=0 +at-ClientRetryInterval? -ats12=9 -at-MaxDwellTime=10 -ats12=65535 -ast12=65536 -ats12=400 -ats12? -at-MaxDwellTime? +at-CmdVC=0 +at-CmdVC=7; +at-CmdVC=8; +at-CmdVC? at-AirSpeed=4800 -at-Bandwidth=0 -ats13=10.4 -ats13=15.6 -ats13=20 -ats13=31.25 -ats13=41.7 -ats13=62.5 -ats13=125 -ats13=250 -ats13=500 +at-CodingRate=4 +at-CodingRate=5 +at-CodingRate=9 +at-CodingRate=8 at-AirSpeed=0 -ats13=0 -ats13=10.4 -ats13=15.6 -ats13=20.8 -ats13=31.25 -ats13=41.7 -ats13=62.5 -ats13=125 -ats13=250 -ats13=500 -ats13? -at-Bandwidth? +at-CodingRate=4 +at-CodingRate=5 +at-CodingRate=9 +at-CodingRate=8 +at-CodingRate? -at-AirSpeed=4800 +at-CopyDebug=1 +at-CopyDebug=2 +at-CopyDebug=0 +at-CopyDebug? -at-SpreadFactor=5 -ats14=6 -ats14=12 -ats14=13 -ats14=9 +at-CopySerial=1 +at-CopySerial=2 +at-CopySerial=0 +at-CopySerial? -at-AirSpeed=0 +at-CopyTriggers=1 +at-CopyTriggers=2 +at-CopyTriggers=0 +at-CopyTriggers? -ats14=5 -at-SpreadFactor=6 -ats14=12 -ats14=13 -ats14=9 -ats14? -at-SpreadFactor? +at-DataScrambling=1 +at-DataScrambling=2 +at-DataScrambling=0 +at-DataScrambling? -ats15=4 -at-CodingRate=5 -ats15=9 -ats15=8 +at-Debug=1 +at-Debug=2 +at-Debug=0 +at-Debug? -at-AirSpeed=4800 +at-DebugDatagrams=1 +at-DebugDatagrams=2 +at-DebugDatagrams=0 +at-DebugDatagrams? -ats15=4 -ats15=5 -ats15=9 -ats15=8 -ats15? -at-CodingRate? +at-DebugRadio=1 +at-DebugRadio=2 +at-DebugRadio=0 +at-DebugRadio? -at-SyncWord=0 -ats16=255 -ats16=256 -ats16=18 -ats16? -at-SyncWord? +at-DebugStates=1 +at-DebugStates=2 +at-DebugStates=0 +at-DebugStates? -ats17=5 -at-PreambleLength=6 -ats17=255 -ats17=256 -ats17=65535 -ats17=65536 -ats17=8 -ats17? -at-PreambleLength? +at-DebugTraining=1 +at-DebugTraining=2 +at-DebugTraining=0 +at-DebugTraining? -at-CmdVC=0 -ats18=7; -ats18=8; -ats18? -at-CmdVC? +at-DebugReceive=1 +at-DebugReceive=2 +at-DebugReceive=0 +at-DebugReceive? -ats19=9 -at-FrameTimeout=10 -ats19=2000 -ats19=2001 -ats19=50 -ats19? -at-FrameTimeout? +at-DebugTransmit=1 +at-DebugTransmit=2 +at-DebugTransmit=0 +at-DebugTransmit? -at-Debug=1 -ats20=2 -ats20=0 -ats20? -at-Debug? +at-DisplayPacketQuality=1 +at-DisplayPacketQuality=2 +at-DisplayPacketQuality=0 +at-DisplayPacketQuality? + +at-DisplayRealMillis=1 +at-DisplayRealMillis=2 +at-DisplayRealMillis=0 +at-DisplayRealMillis? at-Echo=1 -ats21=2 -ats21=0 -ats21? +at-Echo=2 +at-Echo=0 at-Echo? -ats22=249 -at-HeartBeatTimeout=250 -ats22=65535 -ats22=65536 -ats22=5000 -ats22? -at-HeartBeatTimeout? +at-EnableCRC16=1 +at-EnableCRC16=2 +at-EnableCRC16=0 +at-EnableCRC16? + +at-EncryptData=0 +at-EncryptData=2 +at-EncryptData=1 +at-EncryptData? + +at-EncryptionKey=00112233445566778899aabbccddeeff +at-EncryptionKey? +at-EncryptionKey=00112233445566778899aabbccddeef +at-EncryptionKey=00112233445566778899aabbccddeefff +at-EncryptionKey=00112233445566778899aabbccddeefg +at-EncryptionKey=37782141A665734E4475672AE6308308 +at-EncryptionKey? at-FlowControl=1 -ats23=2 -ats23=0 -ats23? +at-FlowControl=2 +at-FlowControl=0 at-FlowControl? -at-AutoTune=1 -ats24=2 -ats24=0 -ats24? -at-AutoTune? - -at-DisplayPacketQuality=1 -ats25=2 -ats25=0 -ats25? -at-DisplayPacketQuality? +at-FrameTimeout=9 +at-FrameTimeout=10 +at-FrameTimeout=2000 +at-FrameTimeout=2001 +at-FrameTimeout=50 +at-FrameTimeout? -at-MaxResends=0 -ats26=255 -ats26=256 -ats26=2 -ats26? -at-MaxResends? +at-FrequencyHop=0 +at-FrequencyHop=2 +at-FrequencyHop=1 +at-FrequencyHop? -at-SortParametersByName=0 -ats27=2 -ats27=1 -ats27? -at-SortParametersByName? +at-FrequencyMin=0 +at-FrequencyMin=901.9 +at-FrequencyMin=902 +at-FrequencyMin=902.1 +at-FrequencyMin=927.9 +at-FrequencyMin=928.1 +at-FrequencyMin=928 +at-FrequencyMin? -at-PrintParameterName=0 -ats28=2 -ats28=1 -ats28? -at-PrintParameterName? +at-FrequencyMin=928 +at-FrequencyMin=0 +at-FrequencyMin=928.1 +at-FrequencyMin=928 +at-FrequencyMin=927.9 +at-FrequencyMin=901.9 +at-FrequencyMin=902 +at-FrequencyMin? -at-PrintFrequency=1 -ats29=2 -ats29=0 -ats29? -at-PrintFrequency? +at-HeartBeatTimeout=249 +at-HeartBeatTimeout=250 +at-HeartBeatTimeout=65535 +at-HeartBeatTimeout=65536 +at-HeartBeatTimeout=5000 +at-HeartBeatTimeout? -at-DebugRadio=1 -ats30=2 -ats30=0 -ats30? -at-DebugRadio? +at-InvertCts=1 +at-InvertCts=2 +at-InvertCts=0 +at-InvertCts? -at-DebugStates=1 -ats31=2 -ats31=0 -ats31? -at-DebugStates? +at-InvertRts=1 +at-InvertRts=2 +at-InvertRts=0 +at-InvertRts? -at-DebugTraining=1 -ats32=2 -ats32=0 -ats32? -at-DebugTraining? +at-MaxDwellTime=9 +at-MaxDwellTime=10 +at-MaxDwellTime=65535 +at-MaxDwellTime=65536 +at-MaxDwellTime=400 +at-MaxDwellTime? -at-TrainingTimeout=1 -ats33=255 -ats33=0 -ats33? -at-TrainingTimeout? +at-MaxResends=0 +at-MaxResends=255 +at-MaxResends=256 +at-MaxResends=2 +at-MaxResends? -at-UsbSerialWait=1 -ats34=2 -ats34=0 -ats34? -at-UsbSerialWait? +at-NetID=0 +at-NetID=255 +at-NetID=192 +at-NetID? -at-PrintRfData=1 -ats35=2 -ats35=0 -ats35? -at-PrintRfData? +at-NumberOfChannels=0 +at-NumberOfChannels=1 +at-NumberOfChannels=255 +at-NumberOfChannels=256 +at-NumberOfChannels=50 +at-NumberOfChannels? -at-PrintPktData=1 -ats36=2 -ats36=0 -ats36? -at-PrintPktData? +at-OperatingMode=0 +at-OperatingMode=2 +at-OperatingMode=3 +at-OperatingMode=1 +at-OperatingMode? -at-VerifyRxNetID=1 -ats37=2 -ats37=0 -ats37? -at-VerifyRxNetID? +at-OverHeadtime=0 +at-OverHeadtime=1000 +at-OverHeadtime=1001 +at-OverHeadtime? -ats38=0 -at-TriggerWidth=1 -ats38=255 -ats38=256 -ats38=25 -ats38? -at-TriggerWidth? +at-PreambleLength=5 +at-PreambleLength=6 +at-PreambleLength=255 +at-PreambleLength=256 +at-PreambleLength=65535 +at-PreambleLength=65536 +at-PreambleLength=8 +at-PreambleLength? -at-TriggerWidthIsMultiplier=0 -ats39=2 -ats39=1 -ats39? -at-TriggerWidthIsMultiplier? +at-PrintFrequency=1 +at-PrintFrequency=2 +at-PrintFrequency=0 +at-PrintFrequency? -at-TriggerEnable_31-0=0 -ats40=4294967295 -ats40? -at-TriggerEnable_31-0? +at-PrintLinkUpDown=1 +at-PrintLinkUpDown=2 +at-PrintLinkUpDown=0 +at-PrintLinkUpDown? -at-TriggerEnable_63-32=0 -ats41=4294967295 -ats41? -at-TriggerEnable_63-32? +at-PrintPktData=1 +at-PrintPktData=2 +at-PrintPktData=0 +at-PrintPktData? -at-DebugReceive=1 -ats42=2 -ats42=0 -ats42? -at-DebugReceive? +at-PrintRfData=1 +at-PrintRfData=2 +at-PrintRfData=0 +at-PrintRfData? -at-DebugTransmit=1 -ats43=2 -ats43=0 -ats43? -at-DebugTransmit? +at-PrintTimestamp=1 +at-PrintTimestamp=2 +at-PrintTimestamp=0 +at-PrintTimestamp? at-PrintTxErrors=1 -ats44=2 -ats44=0 -ats44? +at-PrintTxErrors=2 +at-PrintTxErrors=0 at-PrintTxErrors? -ats45=0 -ats45=1 -at-radioProtocolVersion=2 -ats45=3 -ats45? -at-radioProtocolVersion? +at-RadioProtocolVersion=0 +at-RadioProtocolVersion=1 +at-RadioProtocolVersion=2 +at-RadioProtocolVersion=3 +at-RadioProtocolVersion? -at-PrintTimestamp=1 -ats46=2 -ats46=0 -ats46? -at-PrintTimestamp? +at-AirSpeed=4800 -at-debugDatagrams=1 -ats47=2 -ats47=0 -ats47? -at-debugDatagrams? +at-SpreadFactor=5 +at-SpreadFactor=6 +at-SpreadFactor=12 +at-SpreadFactor=13 +at-SpreadFactor=9 -at-OverHeadtime=0 -ats48=1000 -ats48=1001 -ats48? -at-OverHeadtime? +at-AirSpeed=0 -at-enableCRC16=1 -ats49=2 -ats49=0 -ats49? -at-enableCRC16? +at-SpreadFactor=5 +at-SpreadFactor=6 +at-SpreadFactor=12 +at-SpreadFactor=13 +at-SpreadFactor=9 +at-SpreadFactor? -at-DisplayRealMillis=1 -ats50=2 -ats50=0 -ats50? -at-DisplayRealMillis? +at-SyncWord=0 +at-SyncWord=255 +at-SyncWord=256 +at-SyncWord=18 +at-SyncWord? + +at-TrainingKey=01020304050607080910111213141516 +at-TrainingKey=0102030405060708091011121314151 +at-TrainingKey=010203040506070809101112131415160 +at-TrainingKey? at-TrainingServer=1 -ats51=2 -ats51=0 -ats51? +at-TrainingServer=2 +at-TrainingServer=0 at-TrainingServer? -at-ClientRetryInterval=1 -ats52=2 -ats52=0 -ats52? -at-ClientRetryInterval? - -at-CopyDebug=1 -ats53=2 -ats53=0 -ats53? -at-CopyDebug? +at-TrainingTimeout=1 +at-TrainingTimeout=255 +at-TrainingTimeout=0 +at-TrainingTimeout? -at-CopySerial=1 -ats54=2 -ats54=0 -ats54? -at-CopySerial? +at-TriggerEnable_31-0=0 +at-TriggerEnable_31-0=4294967295 +at-TriggerEnable_31-0? -at-CopyTriggers=1 -ats55=2 -ats55=0 -ats55? -at-CopyTriggers? +at-TriggerEnable_63-32=0 +at-TriggerEnable_63-32=4294967295 +at-TriggerEnable_63-32? -at-TrainingKey=01020304050607080910111213141516 -ats56=0102030405060708091011121314151 -ats56=010203040506070809101112131415160 -ats56? -at-TrainingKey? +at-TriggerWidth=0 +at-TriggerWidth=1 +at-TriggerWidth=255 +at-TriggerWidth=256 +at-TriggerWidth=25 +at-TriggerWidth? -at-PrintLinkUpDown=1 -ats57=2 -ats57=0 -ats57? -at-PrintLinkUpDown? +at-TriggerWidthIsMultiplier=0 +at-TriggerWidthIsMultiplier=2 +at-TriggerWidthIsMultiplier=1 +at-TriggerWidthIsMultiplier? -at-InvertCts=1 -ats58=2 -ats58=0 -ats58? -at-InvertCts? +at-TxPower=13 +at-TxPower=14 +at-TxPower=31 +at-TxPower=30 +at-TxPower? -at-InvertRts=1 -ats59=2 -ats59=0 -ats59? -at-InvertRts? +at-UsbSerialWait=1 +at-UsbSerialWait=2 +at-UsbSerialWait=0 +at-UsbSerialWait? -at-AlternateLedUsage=1 -ats60=2 -ats60=0 -ats60? -at-AlternateLedUsage? +at-VerifyRxNetID=1 +at-VerifyRxNetID=2 +at-VerifyRxNetID=0 +at-VerifyRxNetID? # Invalid commands -ats61=1 -ats61? - -ats255=1 -ats255? - at-foobar=1 at-foobar? diff --git a/Firmware/Tools/Results/Command_Validation_Results.txt b/Firmware/Tools/Results/Command_Validation_Results.txt index 28ac9738..127c064f 100644 --- a/Firmware/Tools/Results/Command_Validation_Results.txt +++ b/Firmware/Tools/Results/Command_Validation_Results.txt @@ -8,20 +8,24 @@ ERROR at? Command summary: AT? - Print the command summary + ATD - Display the debug settings ATF - Enter training mode and return to factory defaults ATG - Generate new netID and encryption key ATI - Display the radio version ATI? - Display the information commands ATIn - Display system information + ATL - VC link reset ATO - Exit command mode - ATR - VC link reset + ATP - Display probe trigger settings ATSn=xxx - Set parameter n's value to xxx ATSn? - Print parameter n's current value ATT - Enter training mode + ATV - Display virtual circuit settings ATX - Stop the training server ATZ - Reboot the radio AT-Param=xxx - Set parameter's value to xxx by name (Param) AT-Param? - Print parameter's current value by name (Param) + AT-? - Display the setting values AT&F - Restore factory settings AT&W - Save current settings to NVM ati? @@ -64,7 +68,7 @@ SparkFun LoRaSerial SAMD21 1W 915MHz ati3 -157 ati4 -44 +139 ati5 506 ati6 @@ -74,7 +78,7 @@ ati7 ati8 B2A99BF24A34555020312E34162815FF ati9 -Total datagrams sent: 70 +Total datagrams sent: 102 ati10 Total datagrams received: 0 ati11 @@ -112,1098 +116,882 @@ Total number of bad CRC frames: 0 ati27 ERROR ati0 -ATS0:SerialSpeed=57600 -ATS1:AirSpeed=4800 -ATS2:netID=192 -ATS3:OperatingMode=1 -ATS4:EncryptData=1 -ATS5:EncryptionKey=37782141A665734E4475672AE6308308 -ATS6:DataScrambling=0 -ATS7:TxPower=30 -ATS8:FrequencyMin=902.000 -ATS9:FrequencyMax=928.000 -ATS10:NumberOfChannels=50 -ATS11:FrequencyHop=1 -ATS12:MaxDwellTime=400 -ATS13:Bandwidth=500.00 -ATS14:SpreadFactor=9 -ATS15:CodingRate=8 -ATS16:SyncWord=18 -ATS17:PreambleLength=8 -ATS18:CmdVC=0 -ATS19:FrameTimeout=50 -ATS20:Debug=0 -ATS21:Echo=0 -ATS22:HeartBeatTimeout=5000 -ATS23:FlowControl=0 -ATS24:AutoTune=0 -ATS25:DisplayPacketQuality=0 -ATS26:MaxResends=0 -ATS27:SortParametersByName=0 -ATS28:PrintParameterName=0 -ATS29:PrintFrequency=0 -ATS30:DebugRadio=0 -ATS31:DebugStates=0 -ATS32:DebugTraining=0 -ATS33:TrainingTimeout=1 -ATS34:UsbSerialWait=0 -ATS35:PrintRfData=0 -ATS36:PrintPktData=0 -ATS37:VerifyRxNetID=0 -ATS38:TriggerWidth=25 -ATS39:TriggerWidthIsMultiplier=1 -ATS40:TriggerEnable_31-0=0 -ATS41:TriggerEnable_63-32=0 -ATS42:DebugReceive=0 -ATS43:DebugTransmit=0 -ATS44:PrintTxErrors=0 -ATS45:radioProtocolVersion=2 -ATS46:PrintTimestamp=0 -ATS47:DebugDatagrams=0 -ATS48:OverHeadtime=10 -ATS49:EnableCRC16=0 -ATS50:DisplayRealMillis=0 -ATS51:TrainingServer=0 -ATS52:ClientRetryInterval=3 -ATS53:CopyDebug=1 -ATS54:CopySerial=1 -ATS55:CopyTriggers=1 -ATS56:TrainingKey=537061726B46756E547261696E696E67 -ATS57:PrintLinkUpDown=0 -ATS58:InvertCts=0 -ATS59:InvertRts=0 -ATS60:AlternateLedUsage=0 -at-PrintParameterName=0 -OK +AT-AirSpeed=4800 +AT-AlternateLedUsage=0 +AT-AutoTune=0 +AT-Bandwidth=500.00 +AT-ClientPingRetryInterval=3 +AT-CmdVC=0 +AT-CodingRate=8 +AT-CopyDebug=1 +AT-CopySerial=1 +AT-CopyTriggers=1 +AT-DataScrambling=0 +AT-Debug=0 +AT-DebugDatagrams=0 +AT-DebugRadio=0 +AT-DebugReceive=0 +AT-DebugStates=0 +AT-DebugTraining=0 +AT-DebugTransmit=0 +AT-DisplayPacketQuality=0 +AT-DisplayRealMillis=0 +AT-Echo=0 +AT-EnableCRC16=0 +AT-EncryptData=1 +AT-EncryptionKey=37782141A665734E4475672AE6308308 +AT-FlowControl=0 +AT-FrameTimeout=50 +AT-FrequencyHop=1 +AT-FrequencyMax=928.000 +AT-FrequencyMin=902.000 +AT-HeartBeatTimeout=5000 +AT-InvertCts=0 +AT-InvertRts=0 +AT-MaxDwellTime=400 +AT-MaxResends=0 +AT-netID=192 +AT-NumberOfChannels=50 +AT-OperatingMode=1 +AT-OverHeadtime=10 +AT-PreambleLength=8 +AT-PrintFrequency=0 +AT-PrintLinkUpDown=0 +AT-PrintPktData=0 +AT-PrintRfData=0 +AT-PrintTimestamp=0 +AT-PrintTxErrors=0 +AT-SerialSpeed=57600 +AT-SpreadFactor=9 +AT-SyncWord=18 +AT-TrainingKey=537061726B46756E547261696E696E67 +AT-TrainingServer=0 +AT-TrainingTimeout=1 +AT-TriggerEnable_31-0=0 +AT-TriggerEnable_63-32=0 +AT-TriggerWidth=25 +AT-TriggerWidthIsMultiplier=1 +AT-TxPower=30 +AT-UsbSerialWait=0 +AT-VerifyRxNetID=0 at-SerialSpeed=0 ERROR -ats0=1 +at-SerialSpeed=1 ERROR -ats0=40 +at-SerialSpeed=40 ERROR -ats0=50 +at-SerialSpeed=50 ERROR -ats0=110 +at-SerialSpeed=110 ERROR -ats0=134 +at-SerialSpeed=134 ERROR -ats0=150 +at-SerialSpeed=150 ERROR -ats0=300 +at-SerialSpeed=300 ERROR -ats0=400 +at-SerialSpeed=400 ERROR -ats0=600 +at-SerialSpeed=600 ERROR -ats0=1200 +at-SerialSpeed=1200 ERROR at-SerialSpeed=2400 OK -ats0=4800 +at-SerialSpeed=4800 OK -ats0=9600 +at-SerialSpeed=9600 OK -ats0=14400 +at-SerialSpeed=14400 OK -ats0=19200 +at-SerialSpeed=19200 OK -ats0=28800 +at-SerialSpeed=28800 ERROR -ats0=38400 +at-SerialSpeed=38400 OK -ats0=57600 +at-SerialSpeed=57600 OK -ats0=76800 +at-SerialSpeed=76800 ERROR -ats0=115200 +at-SerialSpeed=115200 OK -ats0=128000 +at-SerialSpeed=128000 ERROR -ats0=230400 +at-SerialSpeed=230400 ERROR -ats0=250000 +at-SerialSpeed=250000 ERROR -ats0=256000 +at-SerialSpeed=256000 ERROR -ats0=460800 +at-SerialSpeed=460800 ERROR -ats0=500000 +at-SerialSpeed=500000 ERROR -ats0=512000 +at-SerialSpeed=512000 ERROR -ats0=576000 +at-SerialSpeed=576000 ERROR -ats0=921600 +at-SerialSpeed=921600 ERROR -ats0=1000000 +at-SerialSpeed=1000000 ERROR -ats0=1843200 +at-SerialSpeed=1843200 ERROR -ats0=2000000 +at-SerialSpeed=2000000 ERROR -ats0=3000000 +at-SerialSpeed=3000000 ERROR -ats0=3686400 +at-SerialSpeed=3686400 ERROR -ats0=4000000 +at-SerialSpeed=4000000 ERROR -ats0=57600 +at-SerialSpeed=57600 OK -ats0? -57600 at-SerialSpeed? 57600 -ats1=1 +at-AirSpeed=1 ERROR at-AirSpeed=40 OK -ats1=50 +at-AirSpeed=50 ERROR -ats1=110 +at-AirSpeed=110 ERROR -ats1=134 +at-AirSpeed=134 ERROR -ats1=150 +at-AirSpeed=150 OK -ats1=300 +at-AirSpeed=300 ERROR -ats1=400 +at-AirSpeed=400 OK -ats1=600 +at-AirSpeed=600 ERROR -ats1=1200 +at-AirSpeed=1200 OK -ats1=2400 +at-AirSpeed=2400 OK -ats1=4800 +at-AirSpeed=4800 OK -ats1=9600 +at-AirSpeed=9600 OK -ats1=14400 +at-AirSpeed=14400 ERROR -ats1=19200 +at-AirSpeed=19200 OK -ats1=28800 +at-AirSpeed=28800 OK -ats1=38400 +at-AirSpeed=38400 OK -ats1=57600 +at-AirSpeed=57600 ERROR -ats1=76800 +at-AirSpeed=76800 ERROR -ats1=115200 +at-AirSpeed=115200 ERROR -ats1=128000 +at-AirSpeed=128000 ERROR -ats1=230400 +at-AirSpeed=230400 ERROR -ats1=250000 +at-AirSpeed=250000 ERROR -ats1=256000 +at-AirSpeed=256000 ERROR -ats1=460800 +at-AirSpeed=460800 ERROR -ats1=500000 +at-AirSpeed=500000 ERROR -ats1=512000 +at-AirSpeed=512000 ERROR -ats1=576000 +at-AirSpeed=576000 ERROR -ats1=921600 +at-AirSpeed=921600 ERROR -ats1=1000000 +at-AirSpeed=1000000 ERROR -ats1=1843200 +at-AirSpeed=1843200 ERROR -ats1=2000000 +at-AirSpeed=2000000 ERROR -ats1=3000000 +at-AirSpeed=3000000 ERROR -ats1=3686400 +at-AirSpeed=3686400 ERROR -ats1=4000000 +at-AirSpeed=4000000 ERROR -ats1=0 +at-AirSpeed=0 OK -ats1? -0 at-AirSpeed? 0 -at-NetID=0 -OK -ats2=255 -OK -ats2=192 -OK -ats2? -192 -at-NetID? -192 -at-OperatingMode=0 -OK -ats3=2 -OK -ats3=3 -ERROR -ats3=1 -OK -ats3? -1 -at-OperatingMode? -1 -at-EncryptData=0 -OK -ats4=2 -ERROR -ats4=1 -OK -ats4? -1 -at-EncryptData? -1 -at-EncryptionKey=00112233445566778899aabbccddeeff -OK -ats5? -00112233445566778899AABBCCDDEEFF -ats5=00112233445566778899aabbccddeef -ERROR -ats5=00112233445566778899aabbccddeefff -ERROR -ats5=00112233445566778899aabbccddeefg -ERROR -ats5=37782141A665734E4475672AE6308308 -OK -ats5? -37782141A665734E4475672AE6308308 -at-EncryptionKey? -37782141A665734E4475672AE6308308 -at-DataScrambling=1 +at-AlternateLedUsage=1 OK -ats6=2 +at-AlternateLedUsage=2 ERROR -ats6=0 +at-AlternateLedUsage=0 OK -ats6? -0 -at-DataScrambling? +at-AlternateLedUsage? 0 -ats7=13 -ERROR -at-TxPower=14 -OK -ats7=31 -ERROR -ats7=30 -OK -ats7? -30 -at-TxPower? -30 -at-FrequencyMin=928 -OK -ats8=0 -ERROR -ats8=928.1 -ERROR -ats8=928 -OK -ats8=927.9 -OK -ats8=901.9 -ERROR -ats8=902 -OK -ats8? -902.000 -at-FrequencyMin? -902.000 -ats9=0 -ERROR -ats9=901.9 -ERROR -at-FrequencyMin=902 -OK -ats9=902.1 -OK -ats9=927.9 -OK -ats9=928.1 -ERROR -ats9=928 -OK -ats9? -928.000 -at-FrequencyMin? -902.000 -ats10=0 -ERROR -at-NumberOfChannels=1 -OK -ats10=255 -OK -ats10=256 -ERROR -ats10=50 -OK -ats10? -50 -at-NumberOfChannels? -50 -at-FrequencyHop=0 -OK -ats11=2 -ERROR -ats11=1 -OK -ats11? -1 -at-FrequencyHop? -1 -ats12=9 -ERROR -at-MaxDwellTime=10 -OK -ats12=65535 +at-AutoTune=1 OK -ast12=65536 +at-AutoTune=2 ERROR -ats12=400 +at-AutoTune=0 OK -ats12? -400 -at-MaxDwellTime? -400 +at-AutoTune? +0 at-AirSpeed=4800 Warning: AirSpeed override of bandwidth, spread factor, and coding rate OK at-Bandwidth=0 OK -ats13=10.4 +at-Bandwidth=10.4 AirSpeed is overriding ERROR -ats13=15.6 +at-Bandwidth=15.6 AirSpeed is overriding ERROR -ats13=20 +at-Bandwidth=20 AirSpeed is overriding ERROR -ats13=31.25 +at-Bandwidth=31.25 AirSpeed is overriding ERROR -ats13=41.7 +at-Bandwidth=41.7 AirSpeed is overriding ERROR -ats13=62.5 +at-Bandwidth=62.5 AirSpeed is overriding ERROR -ats13=125 +at-Bandwidth=125 AirSpeed is overriding ERROR -ats13=250 +at-Bandwidth=250 AirSpeed is overriding ERROR -ats13=500 +at-Bandwidth=500 AirSpeed is overriding ERROR at-AirSpeed=0 OK -ats13=0 +at-Bandwidth=0 ERROR -ats13=10.4 +at-Bandwidth=10.4 OK -ats13=15.6 +at-Bandwidth=15.6 OK -ats13=20.8 +at-Bandwidth=20.8 OK -ats13=31.25 +at-Bandwidth=31.25 OK -ats13=41.7 +at-Bandwidth=41.7 OK -ats13=62.5 +at-Bandwidth=62.5 OK -ats13=125 +at-Bandwidth=125 OK -ats13=250 +at-Bandwidth=250 OK -ats13=500 +at-Bandwidth=500 OK -ats13? -500.00 at-Bandwidth? 500.00 -at-AirSpeed=4800 -Warning: AirSpeed override of bandwidth, spread factor, and coding rate -OK -at-SpreadFactor=5 -AirSpeed is overriding -ERROR -ats14=6 -AirSpeed is overriding -ERROR -ats14=12 -AirSpeed is overriding +at-ClientRetryInterval=1 ERROR -ats14=13 -AirSpeed is overriding +at-ClientRetryInterval=2 ERROR -ats14=9 -AirSpeed is overriding +at-ClientRetryInterval=0 ERROR -at-AirSpeed=0 -OK -ats14=5 +at-ClientRetryInterval? ERROR -at-SpreadFactor=6 -OK -ats14=12 +at-CmdVC=0 OK -ats14=13 -ERROR -ats14=9 +at-CmdVC=7; OK -ats14? -9 -at-SpreadFactor? -9 -ats15=4 +at-CmdVC=8; ERROR -at-CodingRate=5 -OK -ats15=9 -ERROR -ats15=8 -OK +at-CmdVC? +7 at-AirSpeed=4800 Warning: AirSpeed override of bandwidth, spread factor, and coding rate OK -ats15=4 +at-CodingRate=4 AirSpeed is overriding ERROR -ats15=5 +at-CodingRate=5 AirSpeed is overriding ERROR -ats15=9 +at-CodingRate=9 AirSpeed is overriding ERROR -ats15=8 +at-CodingRate=8 AirSpeed is overriding ERROR -ats15? -8 -at-CodingRate? -8 -at-SyncWord=0 -OK -ats16=255 +at-AirSpeed=0 OK -ats16=256 +at-CodingRate=4 ERROR -ats16=18 +at-CodingRate=5 OK -ats16? -18 -at-SyncWord? -18 -ats17=5 +at-CodingRate=9 ERROR -at-PreambleLength=6 +at-CodingRate=8 OK -ats17=255 +at-CodingRate? +8 +at-CopyDebug=1 OK -ats17=256 +at-CopyDebug=2 +ERROR +at-CopyDebug=0 OK -ats17=65535 +at-CopyDebug? +0 +at-CopySerial=1 OK -ats17=65536 +at-CopySerial=2 ERROR -ats17=8 +at-CopySerial=0 OK -ats17? -8 -at-PreambleLength? -8 -at-CmdVC=0 -OK -ats18=7; +at-CopySerial? +0 +at-CopyTriggers=1 OK -ats18=8; +at-CopyTriggers=2 ERROR -ats18? -7 -at-CmdVC? -7 -ats19=9 -ERROR -at-FrameTimeout=10 +at-CopyTriggers=0 OK -ats19=2000 +at-CopyTriggers? +0 +at-DataScrambling=1 OK -ats19=2001 +at-DataScrambling=2 ERROR -ats19=50 +at-DataScrambling=0 OK -ats19? -50 -at-FrameTimeout? -50 +at-DataScrambling? +0 at-Debug=1 OK -ats20=2 +at-Debug=2 ERROR -ats20=0 +at-Debug=0 OK -ats20? -0 at-Debug? 0 -at-Echo=1 +at-DebugDatagrams=1 OK -ats21=2 +at-DebugDatagrams=2 ERROR -ats21=0 +at-DebugDatagrams=0 OK -ats21? -0 -at-Echo? +at-DebugDatagrams? 0 -ats22=249 +at-DebugRadio=1 +OK +at-DebugRadio=2 ERROR -at-HeartBeatTimeout=250 +at-DebugRadio=0 OK -ats22=65535 +at-DebugRadio? +0 +at-DebugStates=1 OK -ats22=65536 +at-DebugStates=2 ERROR -ats22=5000 +at-DebugStates=0 OK -ats22? -5000 -at-HeartBeatTimeout? -5000 -at-FlowControl=1 +at-DebugStates? +0 +at-DebugTraining=1 OK -ats23=2 +at-DebugTraining=2 ERROR -ats23=0 +at-DebugTraining=0 OK -ats23? -0 -at-FlowControl? +at-DebugTraining? 0 -at-AutoTune=1 +at-DebugReceive=1 OK -ats24=2 +at-DebugReceive=2 ERROR -ats24=0 +at-DebugReceive=0 OK -ats24? +at-DebugReceive? 0 -at-AutoTune? +at-DebugTransmit=1 +OK +at-DebugTransmit=2 +ERROR +at-DebugTransmit=0 +OK +at-DebugTransmit? 0 at-DisplayPacketQuality=1 OK -ats25=2 +at-DisplayPacketQuality=2 ERROR -ats25=0 +at-DisplayPacketQuality=0 OK -ats25? -0 at-DisplayPacketQuality? 0 -at-MaxResends=0 +at-DisplayRealMillis=1 +OK +at-DisplayRealMillis=2 +ERROR +at-DisplayRealMillis=0 OK -ats26=255 +at-DisplayRealMillis? +0 +at-Echo=1 OK -ats26=256 +at-Echo=2 ERROR -ats26=2 +at-Echo=0 OK -ats26? -2 -at-MaxResends? -2 -at-SortParametersByName=0 +at-Echo? +0 +at-EnableCRC16=1 OK -ats27=2 +at-EnableCRC16=2 ERROR -ats27=1 +at-EnableCRC16=0 OK -ats27? -1 -at-SortParametersByName? +at-EnableCRC16? +0 +at-EncryptData=0 +OK +at-EncryptData=2 +ERROR +at-EncryptData=1 +OK +at-EncryptData? 1 -at-PrintParameterName=0 +at-EncryptionKey=00112233445566778899aabbccddeeff OK -ats28=2 +at-EncryptionKey? +00112233445566778899AABBCCDDEEFF +at-EncryptionKey=00112233445566778899aabbccddeef +ERROR +at-EncryptionKey=00112233445566778899aabbccddeefff +ERROR +at-EncryptionKey=00112233445566778899aabbccddeefg ERROR -ats28=1 -PrintParameterName=1 +at-EncryptionKey=37782141A665734E4475672AE6308308 OK -ats28? -PrintParameterName=1 -at-PrintParameterName? -PrintParameterName=1 -at-PrintFrequency=1 -PrintFrequency=1 +at-EncryptionKey? +37782141A665734E4475672AE6308308 +at-FlowControl=1 OK -ats29=2 +at-FlowControl=2 ERROR -ats29=0 -PrintFrequency=0 +at-FlowControl=0 OK -ats29? -PrintFrequency=0 -at-PrintFrequency? -PrintFrequency=0 -at-DebugRadio=1 -DebugRadio=1 +at-FlowControl? +0 +at-FrameTimeout=9 +ERROR +at-FrameTimeout=10 +OK +at-FrameTimeout=2000 OK -ats30=2 +at-FrameTimeout=2001 ERROR -ats30=0 -DebugRadio=0 +at-FrameTimeout=50 OK -ats30? -DebugRadio=0 -at-DebugRadio? -DebugRadio=0 -at-DebugStates=1 -DebugStates=1 +at-FrameTimeout? +50 +at-FrequencyHop=0 OK -ats31=2 +at-FrequencyHop=2 ERROR -ats31=0 -DebugStates=0 +at-FrequencyHop=1 OK -ats31? -DebugStates=0 -at-DebugStates? -DebugStates=0 -at-DebugTraining=1 -DebugTraining=1 +at-FrequencyHop? +1 +at-FrequencyMin=0 +ERROR +at-FrequencyMin=901.9 +ERROR +at-FrequencyMin=902 +OK +at-FrequencyMin=902.1 OK -ats32=2 +at-FrequencyMin=927.9 +OK +at-FrequencyMin=928.1 ERROR -ats32=0 -DebugTraining=0 +at-FrequencyMin=928 OK -ats32? -DebugTraining=0 -at-DebugTraining? -DebugTraining=0 -at-TrainingTimeout=1 -TrainingTimeout=1 +at-FrequencyMin? +928.000 +at-FrequencyMin=928 OK -ats33=255 -TrainingTimeout=255 +at-FrequencyMin=0 +ERROR +at-FrequencyMin=928.1 +ERROR +at-FrequencyMin=928 OK -ats33=0 +at-FrequencyMin=927.9 +OK +at-FrequencyMin=901.9 ERROR -ats33? -TrainingTimeout=255 -at-TrainingTimeout? -TrainingTimeout=255 -at-UsbSerialWait=1 -UsbSerialWait=1 +at-FrequencyMin=902 OK -ats34=2 +at-FrequencyMin? +902.000 +at-HeartBeatTimeout=249 ERROR -ats34=0 -UsbSerialWait=0 +at-HeartBeatTimeout=250 OK -ats34? -UsbSerialWait=0 -at-UsbSerialWait? -UsbSerialWait=0 -at-PrintRfData=1 -PrintRfData=1 +at-HeartBeatTimeout=65535 OK -ats35=2 +at-HeartBeatTimeout=65536 ERROR -ats35=0 -PrintRfData=0 +at-HeartBeatTimeout=5000 OK -ats35? -PrintRfData=0 -at-PrintRfData? -PrintRfData=0 -at-PrintPktData=1 -PrintPktData=1 +at-HeartBeatTimeout? +5000 +at-InvertCts=1 OK -ats36=2 +at-InvertCts=2 ERROR -ats36=0 -PrintPktData=0 +at-InvertCts=0 OK -ats36? -PrintPktData=0 -at-PrintPktData? -PrintPktData=0 -at-VerifyRxNetID=1 -VerifyRxNetID=1 +at-InvertCts? +0 +at-InvertRts=1 OK -ats37=2 +at-InvertRts=2 ERROR -ats37=0 -VerifyRxNetID=0 +at-InvertRts=0 OK -ats37? -VerifyRxNetID=0 -at-VerifyRxNetID? -VerifyRxNetID=0 -ats38=0 +at-InvertRts? +0 +at-MaxDwellTime=9 ERROR -at-TriggerWidth=1 -TriggerWidth=1 +at-MaxDwellTime=10 OK -ats38=255 -TriggerWidth=255 +at-MaxDwellTime=65535 OK -ats38=256 +at-MaxDwellTime=65536 ERROR -ats38=25 -TriggerWidth=25 +at-MaxDwellTime=400 OK -ats38? -TriggerWidth=25 -at-TriggerWidth? -TriggerWidth=25 -at-TriggerWidthIsMultiplier=0 -TriggerWidthIsMultiplier=0 +at-MaxDwellTime? +400 +at-MaxResends=0 OK -ats39=2 +at-MaxResends=255 +OK +at-MaxResends=256 ERROR -ats39=1 -TriggerWidthIsMultiplier=1 +at-MaxResends=2 OK -ats39? -TriggerWidthIsMultiplier=1 -at-TriggerWidthIsMultiplier? -TriggerWidthIsMultiplier=1 -at-TriggerEnable_31-0=0 -TriggerEnable_31-0=0 +at-MaxResends? +2 +at-NetID=0 OK -ats40=4294967295 -TriggerEnable_31-0=-1 +at-NetID=255 OK -ats40? -TriggerEnable_31-0=-1 -at-TriggerEnable_31-0? -TriggerEnable_31-0=-1 -at-TriggerEnable_63-32=0 -TriggerEnable_63-32=0 +at-NetID=192 OK -ats41=4294967295 -TriggerEnable_63-32=-1 +at-NetID? +192 +at-NumberOfChannels=0 +ERROR +at-NumberOfChannels=1 OK -ats41? -TriggerEnable_63-32=-1 -at-TriggerEnable_63-32? -TriggerEnable_63-32=-1 -at-DebugReceive=1 -DebugReceive=1 +at-NumberOfChannels=255 OK -ats42=2 +at-NumberOfChannels=256 ERROR -ats42=0 -DebugReceive=0 +at-NumberOfChannels=50 OK -ats42? -DebugReceive=0 -at-DebugReceive? -DebugReceive=0 -at-DebugTransmit=1 -DebugTransmit=1 +at-NumberOfChannels? +50 +at-OperatingMode=0 +OK +at-OperatingMode=2 OK -ats43=2 +at-OperatingMode=3 ERROR -ats43=0 -DebugTransmit=0 +at-OperatingMode=1 OK -ats43? -DebugTransmit=0 -at-DebugTransmit? -DebugTransmit=0 -at-PrintTxErrors=1 -PrintTxErrors=1 +at-OperatingMode? +1 +at-OverHeadtime=0 OK -ats44=2 -ERROR -ats44=0 -PrintTxErrors=0 +at-OverHeadtime=1000 OK -ats44? -PrintTxErrors=0 -at-PrintTxErrors? -PrintTxErrors=0 -ats45=0 +at-OverHeadtime=1001 ERROR -ats45=1 +at-OverHeadtime? +1000 +at-PreambleLength=5 ERROR -at-radioProtocolVersion=2 -radioProtocolVersion=2 +at-PreambleLength=6 OK -ats45=3 -ERROR -ats45? -radioProtocolVersion=2 -at-radioProtocolVersion? -radioProtocolVersion=2 -at-PrintTimestamp=1 -PrintTimestamp=1 +at-PreambleLength=255 OK -ats46=2 -ERROR -ats46=0 -PrintTimestamp=0 +at-PreambleLength=256 OK -ats46? -PrintTimestamp=0 -at-PrintTimestamp? -PrintTimestamp=0 -at-debugDatagrams=1 -DebugDatagrams=1 +at-PreambleLength=65535 OK -ats47=2 +at-PreambleLength=65536 ERROR -ats47=0 -DebugDatagrams=0 +at-PreambleLength=8 OK -ats47? -DebugDatagrams=0 -at-debugDatagrams? -DebugDatagrams=0 -at-OverHeadtime=0 -OverHeadtime=0 +at-PreambleLength? +8 +at-PrintFrequency=1 OK -ats48=1000 -OverHeadtime=1000 +at-PrintFrequency=2 +ERROR +at-PrintFrequency=0 OK -ats48=1001 +at-PrintFrequency? +0 +at-PrintLinkUpDown=1 +OK +at-PrintLinkUpDown=2 ERROR -ats48? -OverHeadtime=1000 -at-OverHeadtime? -OverHeadtime=1000 -at-enableCRC16=1 -EnableCRC16=1 +at-PrintLinkUpDown=0 +OK +at-PrintLinkUpDown? +0 +at-PrintPktData=1 OK -ats49=2 +at-PrintPktData=2 ERROR -ats49=0 -EnableCRC16=0 +at-PrintPktData=0 OK -ats49? -EnableCRC16=0 -at-enableCRC16? -EnableCRC16=0 -at-DisplayRealMillis=1 -DisplayRealMillis=1 +at-PrintPktData? +0 +at-PrintRfData=1 OK -ats50=2 +at-PrintRfData=2 ERROR -ats50=0 -DisplayRealMillis=0 +at-PrintRfData=0 OK -ats50? -DisplayRealMillis=0 -at-DisplayRealMillis? -DisplayRealMillis=0 -at-TrainingServer=1 -TrainingServer=1 +at-PrintRfData? +0 +at-PrintTimestamp=1 OK -ats51=2 +at-PrintTimestamp=2 ERROR -ats51=0 -TrainingServer=0 +at-PrintTimestamp=0 OK -ats51? -TrainingServer=0 -at-TrainingServer? -TrainingServer=0 -at-ClientRetryInterval=1 -ClientRetryInterval=1 +at-PrintTimestamp? +0 +at-PrintTxErrors=1 OK -ats52=2 -ClientRetryInterval=2 +at-PrintTxErrors=2 +ERROR +at-PrintTxErrors=0 +OK +at-PrintTxErrors? +0 +at-RadioProtocolVersion=0 +ERROR +at-RadioProtocolVersion=1 +ERROR +at-RadioProtocolVersion=2 +ERROR +at-RadioProtocolVersion=3 +ERROR +at-RadioProtocolVersion? +ERROR +at-AirSpeed=4800 +Warning: AirSpeed override of bandwidth, spread factor, and coding rate OK -ats52=0 +at-SpreadFactor=5 +AirSpeed is overriding ERROR -ats52? -ClientRetryInterval=2 -at-ClientRetryInterval? -ClientRetryInterval=2 -at-CopyDebug=1 -CopyDebug=1 +at-SpreadFactor=6 +AirSpeed is overriding +ERROR +at-SpreadFactor=12 +AirSpeed is overriding +ERROR +at-SpreadFactor=13 +AirSpeed is overriding +ERROR +at-SpreadFactor=9 +AirSpeed is overriding +ERROR +at-AirSpeed=0 OK -ats53=2 +at-SpreadFactor=5 ERROR -ats53=0 -CopyDebug=0 +at-SpreadFactor=6 OK -ats53? -CopyDebug=0 -at-CopyDebug? -CopyDebug=0 -at-CopySerial=1 -CopySerial=1 +at-SpreadFactor=12 OK -ats54=2 +at-SpreadFactor=13 ERROR -ats54=0 -CopySerial=0 +at-SpreadFactor=9 OK -ats54? -CopySerial=0 -at-CopySerial? -CopySerial=0 -at-CopyTriggers=1 -CopyTriggers=1 +at-SpreadFactor? +9 +at-SyncWord=0 OK -ats55=2 +at-SyncWord=255 +OK +at-SyncWord=256 ERROR -ats55=0 -CopyTriggers=0 +at-SyncWord=18 OK -ats55? -CopyTriggers=0 -at-CopyTriggers? -CopyTriggers=0 +at-SyncWord? +18 at-TrainingKey=01020304050607080910111213141516 -TrainingKey=01020304050607080910111213141516 OK -ats56=0102030405060708091011121314151 +at-TrainingKey=0102030405060708091011121314151 ERROR -ats56=010203040506070809101112131415160 +at-TrainingKey=010203040506070809101112131415160 ERROR -ats56? -TrainingKey=01020304050607080910111213141516 at-TrainingKey? -TrainingKey=01020304050607080910111213141516 -at-PrintLinkUpDown=1 -PrintLinkUpDown=1 +01020304050607080910111213141516 +at-TrainingServer=1 OK -ats57=2 +at-TrainingServer=2 ERROR -ats57=0 -PrintLinkUpDown=0 +at-TrainingServer=0 OK -ats57? -PrintLinkUpDown=0 -at-PrintLinkUpDown? -PrintLinkUpDown=0 -at-InvertCts=1 -InvertCts=1 +at-TrainingServer? +0 +at-TrainingTimeout=1 +OK +at-TrainingTimeout=255 OK -ats58=2 +at-TrainingTimeout=0 ERROR -ats58=0 -InvertCts=0 +at-TrainingTimeout? +255 +at-TriggerEnable_31-0=0 OK -ats58? -InvertCts=0 -at-InvertCts? -InvertCts=0 -at-InvertRts=1 -InvertRts=1 +at-TriggerEnable_31-0=4294967295 OK -ats59=2 +at-TriggerEnable_31-0? +-1 +at-TriggerEnable_63-32=0 +OK +at-TriggerEnable_63-32=4294967295 +OK +at-TriggerEnable_63-32? +-1 +at-TriggerWidth=0 ERROR -ats59=0 -InvertRts=0 +at-TriggerWidth=1 OK -ats59? -InvertRts=0 -at-InvertRts? -InvertRts=0 -at-AlternateLedUsage=1 -AlternateLedUsage=1 +at-TriggerWidth=255 OK -ats60=2 +at-TriggerWidth=256 ERROR -ats60=0 -AlternateLedUsage=0 +at-TriggerWidth=25 OK -ats60? -AlternateLedUsage=0 -at-AlternateLedUsage? -AlternateLedUsage=0 -# Invalid commands +at-TriggerWidth? +25 +at-TriggerWidthIsMultiplier=0 +OK +at-TriggerWidthIsMultiplier=2 ERROR -ats61=1 +at-TriggerWidthIsMultiplier=1 +OK +at-TriggerWidthIsMultiplier? +1 +at-TxPower=13 +ERROR +at-TxPower=14 +OK +at-TxPower=31 ERROR -ats61? +at-TxPower=30 +OK +at-TxPower? +30 +at-UsbSerialWait=1 +OK +at-UsbSerialWait=2 ERROR -ats255=1 +at-UsbSerialWait=0 +OK +at-UsbSerialWait? +0 +at-VerifyRxNetID=1 +OK +at-VerifyRxNetID=2 ERROR -ats255? +at-VerifyRxNetID=0 +OK +at-VerifyRxNetID? +0 +# Invalid commands ERROR at-foobar=1 ERROR at-foobar? ERROR ati0 -ATS1:AirSpeed=4800 -ATS60:AlternateLedUsage=0 -ATS24:AutoTune=0 -ATS13:Bandwidth=500.00 -ATS52:ClientRetryInterval=2 -ATS18:CmdVC=7 -ATS15:CodingRate=8 -ATS53:CopyDebug=0 -ATS54:CopySerial=0 -ATS55:CopyTriggers=0 -ATS6:DataScrambling=0 -ATS20:Debug=0 -ATS47:DebugDatagrams=0 -ATS30:DebugRadio=0 -ATS42:DebugReceive=0 -ATS31:DebugStates=0 -ATS32:DebugTraining=0 -ATS43:DebugTransmit=0 -ATS25:DisplayPacketQuality=0 -ATS50:DisplayRealMillis=0 -ATS21:Echo=0 -ATS49:EnableCRC16=0 -ATS4:EncryptData=1 -ATS5:EncryptionKey=37782141A665734E4475672AE6308308 -ATS23:FlowControl=0 -ATS19:FrameTimeout=50 -ATS11:FrequencyHop=1 -ATS9:FrequencyMax=928.000 -ATS8:FrequencyMin=902.000 -ATS22:HeartBeatTimeout=5000 -ATS58:InvertCts=0 -ATS59:InvertRts=0 -ATS12:MaxDwellTime=400 -ATS26:MaxResends=2 -ATS2:netID=192 -ATS10:NumberOfChannels=50 -ATS3:OperatingMode=1 -ATS48:OverHeadtime=1000 -ATS17:PreambleLength=8 -ATS29:PrintFrequency=0 -ATS57:PrintLinkUpDown=0 -ATS28:PrintParameterName=1 -ATS36:PrintPktData=0 -ATS35:PrintRfData=0 -ATS46:PrintTimestamp=0 -ATS44:PrintTxErrors=0 -ATS45:radioProtocolVersion=2 -ATS0:SerialSpeed=57600 -ATS27:SortParametersByName=1 -ATS14:SpreadFactor=9 -ATS16:SyncWord=18 -ATS56:TrainingKey=01020304050607080910111213141516 -ATS51:TrainingServer=0 -ATS33:TrainingTimeout=255 -ATS40:TriggerEnable_31-0=-1 -ATS41:TriggerEnable_63-32=-1 -ATS38:TriggerWidth=25 -ATS39:TriggerWidthIsMultiplier=1 -ATS7:TxPower=30 -ATS34:UsbSerialWait=0 -ATS37:VerifyRxNetID=0 +AT-AirSpeed=0 +AT-AlternateLedUsage=0 +AT-AutoTune=0 +AT-Bandwidth=500.00 +AT-ClientPingRetryInterval=3 +AT-CmdVC=7 +AT-CodingRate=8 +AT-CopyDebug=0 +AT-CopySerial=0 +AT-CopyTriggers=0 +AT-DataScrambling=0 +AT-Debug=0 +AT-DebugDatagrams=0 +AT-DebugRadio=0 +AT-DebugReceive=0 +AT-DebugStates=0 +AT-DebugTraining=0 +AT-DebugTransmit=0 +AT-DisplayPacketQuality=0 +AT-DisplayRealMillis=0 +AT-Echo=0 +AT-EnableCRC16=0 +AT-EncryptData=1 +AT-EncryptionKey=37782141A665734E4475672AE6308308 +AT-FlowControl=0 +AT-FrameTimeout=50 +AT-FrequencyHop=1 +AT-FrequencyMax=928.000 +AT-FrequencyMin=902.000 +AT-HeartBeatTimeout=5000 +AT-InvertCts=0 +AT-InvertRts=0 +AT-MaxDwellTime=400 +AT-MaxResends=2 +AT-netID=192 +AT-NumberOfChannels=50 +AT-OperatingMode=1 +AT-OverHeadtime=1000 +AT-PreambleLength=8 +AT-PrintFrequency=0 +AT-PrintLinkUpDown=0 +AT-PrintPktData=0 +AT-PrintRfData=0 +AT-PrintTimestamp=0 +AT-PrintTxErrors=0 +AT-SerialSpeed=57600 +AT-SpreadFactor=9 +AT-SyncWord=18 +AT-TrainingKey=01020304050607080910111213141516 +AT-TrainingServer=0 +AT-TrainingTimeout=255 +AT-TriggerEnable_31-0=-1 +AT-TriggerEnable_63-32=-1 +AT-TriggerWidth=25 +AT-TriggerWidthIsMultiplier=1 +AT-TxPower=30 +AT-UsbSerialWait=0 +AT-VerifyRxNetID=0 at&f OK atz From e16a14dcce47a34cc30bc008a75fef902de72842 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 09:16:14 -1000 Subject: [PATCH 110/594] Fix the virtual circuit header alignment --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 18290c98..bb12209f 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1034,7 +1034,7 @@ PacketType rcvDatagram() } //Parse the virtual circuit header - vcHeader = (VC_RADIO_MESSAGE_HEADER *)&rxData[1]; + vcHeader = (VC_RADIO_MESSAGE_HEADER *)rxData; rxDestVc = vcHeader->destVc; rxSrcVc = vcHeader->srcVc; rxVcData = &rxData[3]; @@ -1052,6 +1052,8 @@ PacketType rcvDatagram() systemPrintln(rxSrcVc); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); + if (settings.printRfData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); } badFrames++; return DATAGRAM_BAD; From 974253e4bfccb1b5abc31f5fcd22e9e3f731d4c8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 07:47:30 -1000 Subject: [PATCH 111/594] Display the frequency after setting it. --- Firmware/LoRaSerial_Firmware/Radio.ino | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index b1a8f21f..fe77547f 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2,14 +2,24 @@ //Called after begin() and once user exits from command interface void configureRadio() { + float frequency; bool success = true; //Update the settings based upon the air speed convertAirSpeedToSettings(); - if (radio.setFrequency(channels[0]) == RADIOLIB_ERR_INVALID_FREQUENCY) + frequency = channels[0]; + if (radio.setFrequency(frequency) == RADIOLIB_ERR_INVALID_FREQUENCY) success = false; + //Print the frequency if requested + if (settings.printFrequency) + { + systemPrintTimestamp(); + systemPrint(frequency); + systemPrintln(" MHz"); + } + channelNumber = 0; //The SX1276 and RadioLib accepts a value of 2 to 17, with 20 enabling the power amplifier @@ -180,13 +190,15 @@ void setRadioFrequency(bool rxAdjust) frequency = channels[channelNumber]; if (rxAdjust) frequency -= frequencyCorrection; + radio.setFrequency(frequency); + + //Print the frequency if requested if (settings.printFrequency) { systemPrintTimestamp(); systemPrint(frequency); systemPrintln(" MHz"); } - radio.setFrequency(frequency); } void returnToReceiving() From 73298f8264e955450809e1f457d0d60008657544 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 09:25:45 -1000 Subject: [PATCH 112/594] Fix the netID verification path Bug: Only returned DATAGRAM_NETID_MISMATCH if debugging Fix: Rework the logic to test the error first and then display any resulting error --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 37 +++++++++++++----------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index bb12209f..65258144 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -769,26 +769,29 @@ PacketType rcvDatagram() if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) { receivedNetID = *rxData++; - if (settings.debugReceive) + if (receivedNetID != settings.netID) { - systemPrintTimestamp(); - systemPrint("RX: NetID "); - systemPrint(receivedNetID); - systemPrint(" (0x"); - systemPrint(receivedNetID, HEX); - systemPrint(")"); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - if (receivedNetID != settings.netID) + if (settings.debugReceive) { - systemPrint(" expecting "); - systemPrintln(settings.netID); - petWDT(); - if (settings.printPktData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - return (DATAGRAM_NETID_MISMATCH); + systemPrintTimestamp(); + systemPrint("RX: NetID "); + systemPrint(receivedNetID); + systemPrint(" (0x"); + systemPrint(receivedNetID, HEX); + systemPrint(")"); + if (receivedNetID != settings.netID) + { + systemPrint(" expecting "); + systemPrint(settings.netID); + } + systemPrintln(); } - systemPrintln(); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.debugReceive && settings.printPktData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + return (DATAGRAM_NETID_MISMATCH); } } From ee3055cd669f42d32189de69192e7ea20589f905 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 09:27:49 -1000 Subject: [PATCH 113/594] Add more debug prints enabled by debugReceive --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 65258144..38bb07c0 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -854,6 +854,12 @@ PacketType rcvDatagram() printControl(*((uint8_t *)&rxControl)); if (datagramType >= MAX_V2_DATAGRAM_TYPE) { + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("RX: Invalid datagram type "); + systemPrintln(datagramType); + } badFrames++; return (DATAGRAM_BAD); } @@ -1067,11 +1073,14 @@ PacketType rcvDatagram() //Validate the length if (vcHeader->length != rxDataBytes) { - systemPrintTimestamp(); - systemPrint("Invalid VC length, received "); - systemPrint(vcHeader->length); - systemPrint(" expecting "); - systemPrintln(rxDataBytes); + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Invalid VC length, received "); + systemPrint(vcHeader->length); + systemPrint(" expecting "); + systemPrintln(rxDataBytes); + } if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); badFrames++; From 034250ae2281dd30f703719083937b9164907c6d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 11:48:11 -1000 Subject: [PATCH 114/594] Fix systemWrite with length Bug: System rebooting when the link up message is received Fix: Properly increment the buffer address in systemWrite with length --- Firmware/LoRaSerial_Firmware/System.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 99f869dc..da531b31 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -201,7 +201,7 @@ void systemWrite(uint8_t * buffer, uint16_t length) //Output the entire buffer ignoring contents end = &buffer[length]; while (buffer < end) - arch.serialWrite(*buffer); + serialOutputByte(*buffer++); } void systemFlush() From 23c7fe795df67c75b7eb690fa8f97b3dd50b24d4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 11:50:29 -1000 Subject: [PATCH 115/594] Use 2 (start of text) to indicate beginning of VC serial data --- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 1acba3a7..b8bcc2d8 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2085,7 +2085,7 @@ void vcSendLinkStatus(bool linkUp, int8_t srcVc) //Build the message header VC_SERIAL_MESSAGE_HEADER header; - header.start = START_OF_HEADING; + header.start = START_OF_VC_SERIAL; header.radio.length = sizeof(header) + sizeof(message); header.radio.destVc = PC_LINK_STATUS; header.radio.srcVc = srcVc; diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index fd391e3d..0c3e36b4 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -29,7 +29,7 @@ #define VC_LINK_BREAK_MULTIPLIER 3 //Number of missing HEARTBEAT timeouts //ASCII characters -#define START_OF_HEADING 0x01 //From ASCII table +#define START_OF_VC_SERIAL 0x02 //From ASCII table - Start of Text //------------------------------------------------------------------------------ // Protocol Exchanges From b7c1acf881aa06be3a55ef64e57bb44355730897 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 11:57:10 -1000 Subject: [PATCH 116/594] Tools: Switch to using Virtual_Circuit_Header.h --- Firmware/Tools/RadioV2.c | 2 +- Firmware/Tools/States.c | 10 +++++----- Firmware/Tools/System.c | 4 ++-- Firmware/Tools/makefile | 2 +- Firmware/Tools/settings.h | 13 +++++-------- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/Firmware/Tools/RadioV2.c b/Firmware/Tools/RadioV2.c index a7e475a2..9cda0e7f 100644 --- a/Firmware/Tools/RadioV2.c +++ b/Firmware/Tools/RadioV2.c @@ -19,7 +19,7 @@ void xmitVcHeartbeat(int8_t addr, uint8_t * id) txData = txBuffer; *txData++ = FRAME_VC_HEARTBEAT; *txData++ = 0; //Reserve for length - *txData++ = ADDR_BROADCAST; + *txData++ = VC_BROADCAST; *txData++ = addr; memcpy(txData, id, UNIQUE_ID_BYTES); txData += UNIQUE_ID_BYTES; diff --git a/Firmware/Tools/States.c b/Firmware/Tools/States.c index 6e3eb74a..9b3ca946 100644 --- a/Firmware/Tools/States.c +++ b/Firmware/Tools/States.c @@ -31,10 +31,10 @@ void loop() case STATE_RESET: if (settings.trainingServer) //Reserve the server's address (0) - myAddr = idToAddressByte(ADDR_SERVER, myUniqueId); + myAddr = idToAddressByte(VC_SERVER, myUniqueId); else //Unknown client address - myAddr = ADDR_UNASSIGNED; + myAddr = VC_UNASSIGNED; //Start sending heartbeats xmitVcHeartbeat(myAddr, myUniqueId); @@ -73,7 +73,7 @@ void loop() case FRAME_VC_HEARTBEAT: //Save our address - if ((myAddr == ADDR_UNASSIGNED) && (memcmp(myUniqueId, &rxData[3], UNIQUE_ID_BYTES) == 0)) + if ((myAddr == VC_UNASSIGNED) && (memcmp(myUniqueId, &rxData[3], UNIQUE_ID_BYTES) == 0)) { myAddr = srcAddr; printf("myAddr: %d\n", myAddr); @@ -82,7 +82,7 @@ printf("myAddr: %d\n", myAddr); //Translate the unique ID into an address byte addressByte = idToAddressByte(srcAddr, &rxData[3]); - if (settings.trainingServer && (srcAddr == ADDR_UNASSIGNED) && (addressByte >= 0)) + if (settings.trainingServer && (srcAddr == VC_UNASSIGNED) && (addressByte >= 0)) //Assign the address to the client xmitVcHeartbeat(addressByte, &rxData[3]); @@ -90,7 +90,7 @@ printf("myAddr: %d\n", myAddr); break; case FRAME_VC_DATA: - if ((destAddr != myAddr) && (destAddr != ADDR_BROADCAST)) + if ((destAddr != myAddr) && (destAddr != VC_BROADCAST)) returnToReceiving(); else { diff --git a/Firmware/Tools/System.c b/Firmware/Tools/System.c index 971738e5..53f38747 100644 --- a/Firmware/Tools/System.c +++ b/Firmware/Tools/System.c @@ -83,11 +83,11 @@ printf("\n"); index = srcAddr; //Only the server can assign the address bytes - if ((srcAddr == ADDR_UNASSIGNED) && (!settings.trainingServer)) + if ((srcAddr == VC_UNASSIGNED) && (!settings.trainingServer)) return -1; //Assign an address if necessary - if (srcAddr == ADDR_UNASSIGNED) + if (srcAddr == VC_UNASSIGNED) { //Unknown client ID //Determine if there is a free address diff --git a/Firmware/Tools/makefile b/Firmware/Tools/makefile index 2738fd7f..f53940f0 100644 --- a/Firmware/Tools/makefile +++ b/Firmware/Tools/makefile @@ -12,7 +12,7 @@ EXECUTABLES = Client EXECUTABLES += Server EXECUTABLES += VcServerTest -INCLUDES = settings.h +INCLUDES = settings.h ../LoRaSerial_Firmware/Virtual_Circuit_Protocol.h COMMON_LIB = Common_Lib.a diff --git a/Firmware/Tools/settings.h b/Firmware/Tools/settings.h index c0e102e0..847ff8f4 100644 --- a/Firmware/Tools/settings.h +++ b/Firmware/Tools/settings.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -16,19 +17,17 @@ #include #include +#include "../LoRaSerial_Firmware/Virtual_Circuit_Protocol.h" + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Defines //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -#define ADDR_SERVER 0 -#define ADDR_BROADCAST -1 -#define ADDR_UNASSIGNED -2 - #define PORT 7272 #define UNIQUE_ID_BYTES 16 -#define false 0 -#define true 1 +//#define false 0 +//#define true 1 #define BROADCAST_IPV4_ADDRESS inet_addr("192.168.86.255") //INADDR_BROADCAST @@ -54,8 +53,6 @@ typedef enum typedef uint8_t ADDRESS_MASK; -typedef uint8_t bool; - typedef struct _RESERVED_ADDRESS { uint8_t uniqueId[UNIQUE_ID_BYTES]; From 06827e3ddc6de585f94526c0f0defcd0acee1f46 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 16:46:15 -1000 Subject: [PATCH 117/594] Add debugSerial setting --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + Firmware/LoRaSerial_Firmware/Serial.ino | 17 ++++++++++++++++- Firmware/LoRaSerial_Firmware/settings.h | 3 ++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index b43b278c..506adf45 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -670,6 +670,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &settings.debugStates}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &settings.debugTraining}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &settings.debugSerial}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayPacketQuality", &settings.displayPacketQuality}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &settings.displayRealMillis}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &settings.printFrequency}, diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 168c3abc..f4ae9720 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -406,13 +406,20 @@ bool vcSerialMessageReceived() //Verify that the entire message is in the serial buffer msgLength = vcSerialMsgGetLengthByte(); if (availableRadioTXBytes() < msgLength) + { //The entire message is not in the buffer + if (settings.debugSerial) + systemPrintln("VC serial RX: Waiting for entire buffer"); break; + } //Determine if the message is too large vcDest = vcSerialMsgGetVcDest(); if (msgLength > maxDatagramSize) { + if (settings.debugSerial || settings.debugTransmit) + systemPrintln("VC serial RX: Message too long, discarded"); + //Discard this message, it is too long to transmit over the radio link radioTxTail += msgLength; if (radioTxTail >= sizeof(radioTxBuffer)) @@ -430,7 +437,7 @@ bool vcSerialMessageReceived() //Validate the destination VC if ((vcDest < VC_BROADCAST) || (vcDest >= MAX_VC)) { - if (settings.debugTransmit) + if (settings.debugSerial || settings.debugTransmit) { systemPrint("ERROR: Invalid vcDest "); systemPrint(vcDest); @@ -445,9 +452,17 @@ bool vcSerialMessageReceived() } //If sending to ourself, just place the data in the serial output buffer + if (settings.debugSerial) + { + systemPrint("Readying "); + systemPrint(msgLength); + systemPrintln(" byte for transmission"); + } readyOutgoingPacket(msgLength); if (vcDest == myVc) { + if (settings.debugSerial) + systemPrintln("VC serial RX: Sending to ourself"); serialBufferOutput(outgoingPacket, msgLength); endOfTxData -= msgLength; break; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 528e6966..8fd9847c 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -407,7 +407,8 @@ typedef struct struct_settings { bool alternateLedUsage = false; //Enable alternate LED usage uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training bool multipointServer = false; //Only one radio can be the server in multipoint mode - + bool debugSerial = false; //Debug the serial input + //Add new parameters immediately before this line //-- Add commands to set the parameters //-- Add parameters to routine updateRadioParameters From 7f33a7e724cbd05fe35d1720015def1002b6a321 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 16:48:17 -1000 Subject: [PATCH 118/594] Properly display the virtual circuit link number --- Firmware/LoRaSerial_Firmware/States.ino | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b8bcc2d8..1d3c117c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2154,7 +2154,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) { if (!vc->linkUp) //Send the status message - vcSendLinkStatus(true, myVc); + vcSendLinkStatus(true, index); //Update the link status vc->linkUp = true; @@ -2188,6 +2188,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) systemPrintln("ERROR: Too many clients, no free addresses!\n"); return -2; } + srcAddr = index; } //Check for an address conflict @@ -2213,7 +2214,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) memcpy(&vc->uniqueId, id, UNIQUE_ID_BYTES); //Send the status message - vcSendLinkStatus(true, myVc); + vcSendLinkStatus(true, index); //Returned the assigned address return index; From fa0acba33d7aa7e3a54aaf4479130134a54ef916 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 16:51:00 -1000 Subject: [PATCH 119/594] Add the serial start byte to the virtual circuit header for serial Tested using the following parameters entered with a terminal emulator program (minicom) and then using the Firmware/Tools/VcServerTest program: //Server +++ at&f at-AlternateLedUsage=1 at-EnableCRC16=1 at-FrequencyHop=0 at-MaxResends=2 at-NetID=56 at-OperatingMode=2 at-PrintLinkUpDown=1 at-TxPower=14 at-VerifyRxNetID=1 at-TrainingServer=1 at&w atz //Virtual-Circuit - Client, noHOP, netId, CRC, No Debug +++ at&f at-AlternateLedUsage=1 at-EnableCRC16=1 at-FrequencyHop=0 at-MaxResends=2 at-NetID=56 at-OperatingMode=2 at-PrintLinkUpDown=1 at-TxPower=14 at-VerifyRxNetID=1 at&w atz --- Firmware/LoRaSerial_Firmware/Serial.ino | 47 ++++++++++++++++++++----- Firmware/Tools/VcServerTest.c | 20 +++++++---- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index f4ae9720..a3bad37a 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -367,10 +367,33 @@ void outputSerialData(bool ignoreISR) //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Get the message length byte from the serial buffer -uint8_t vcSerialMsgGetLengthByte() +bool vcSerialMsgGetLengthByte(uint8_t * msgLength) { - //Get the length byte for the received serial message - return radioTxBuffer[radioTxTail]; + uint16_t dataBytes; + uint16_t index; + + //Discard any garbage bytes + dataBytes = availableRadioTXBytes(); + while (dataBytes && (radioTxBuffer[radioTxTail] != START_OF_VC_SERIAL)) + { + radioTxTail = (radioTxTail + 1) % sizeof(radioTxBuffer); + dataBytes -= 1; + } + + //The second byte is the length which does not include the start byte + if (dataBytes < 2) + return false; + + //The first byte is the start serial byte + if (availableRadioTXBytes() < 2) + return false; + + //Get the length byte for the received serial message, account for the start byte + index = radioTxTail + 1; + if (index >= sizeof(radioTxBuffer)) + index -= sizeof(radioTxBuffer); + *msgLength = radioTxBuffer[index] + 1; + return true; } //Get the destination virtual circuit byte from the serial buffer @@ -379,7 +402,7 @@ uint8_t vcSerialMsgGetVcDest() uint16_t index; //Get the destination address byte - index = radioTxTail + 1; + index = radioTxTail + 2; if (index >= sizeof(radioTxBuffer)) index -= sizeof(radioTxBuffer); return radioTxBuffer[index]; @@ -388,8 +411,8 @@ uint8_t vcSerialMsgGetVcDest() //Determine if received serial data may be sent to the remote system bool vcSerialMessageReceived() { - int8_t vcDest; uint8_t msgLength; + int8_t vcDest; do { @@ -398,13 +421,12 @@ bool vcSerialMessageReceived() //The radio is busy, wait until it is idle break; - //Wait until at least one byte is available - if (!availableRadioTXBytes()) - //No data available + //Wait until the length byte is available + if (!vcSerialMsgGetLengthByte(&msgLength)) + //The length byte is not available break; //Verify that the entire message is in the serial buffer - msgLength = vcSerialMsgGetLengthByte(); if (availableRadioTXBytes() < msgLength) { //The entire message is not in the buffer @@ -451,6 +473,12 @@ bool vcSerialMessageReceived() break; } + //Skip over the start byte + radioTxTail += 1; + if (radioTxTail >= sizeof(radioTxBuffer)) + radioTxTail -= sizeof(radioTxBuffer); + msgLength -= 1; + //If sending to ourself, just place the data in the serial output buffer if (settings.debugSerial) { @@ -463,6 +491,7 @@ bool vcSerialMessageReceived() { if (settings.debugSerial) systemPrintln("VC serial RX: Sending to ourself"); + serialOutputByte(START_OF_VC_SERIAL); serialBufferOutput(outgoingPacket, msgLength); endOfTxData -= msgLength; break; diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 96196817..7cbd7c0e 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -7,8 +7,8 @@ int myVcAddr; int remoteVcAddr; -uint8_t inputBuffer[BUFFER_SIZE + 3]; -uint8_t outputBuffer[BUFFER_SIZE + 3]; +uint8_t inputBuffer[VC_SERIAL_HEADER_BYTES + BUFFER_SIZE]; +uint8_t outputBuffer[VC_SERIAL_HEADER_BYTES + BUFFER_SIZE]; int stdinToRadio() { @@ -25,7 +25,7 @@ int stdinToRadio() { //Read the console input data into the local buffer. vcData = inputBuffer; - bytesRead = read(STDIN, &inputBuffer[3], BUFFER_SIZE); + bytesRead = read(STDIN, &inputBuffer[VC_SERIAL_HEADER_BYTES], BUFFER_SIZE); if (bytesRead < 0) { perror("ERROR: Read from stdin failed!"); @@ -42,11 +42,17 @@ int stdinToRadio() if (bytesToSend > MAX_MESSAGE_SIZE) bytesToSend = MAX_MESSAGE_SIZE; + //Build the virtual circuit serial header + vcData[0] = START_OF_VC_SERIAL; + vcData[1] = bytesToSend + VC_RADIO_HEADER_BYTES; + vcData[2] = remoteVcAddr; + vcData[3] = myVcAddr; + //Send the data - vcData[0] = bytesToSend + 3; - vcData[1] = remoteVcAddr; - vcData[2] = myVcAddr; - bytesToSend = write(tty, vcData, *vcData); +printf("Sending the message:\n"); +dumpBuffer(vcData, vcData[1] + 1); + bytesToSend = write(tty, vcData, vcData[1] + 1); +printf("bytesToSend: %d\n", bytesToSend); if (bytesToSend < 0) { perror("ERROR: Write to radio failed!"); From 5d5f88a2ea20bf08a23656dbc7890e67ea1c30f5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 16:58:31 -1000 Subject: [PATCH 120/594] Add the waitForever routine to flush the serial data and pet the dog --- Firmware/LoRaSerial_Firmware/Radio.ino | 6 ++---- Firmware/LoRaSerial_Firmware/RadioV2.ino | 3 +-- Firmware/LoRaSerial_Firmware/States.ino | 11 ++++------- Firmware/LoRaSerial_Firmware/System.ino | 13 +++++++++++++ 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index fe77547f..55469835 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -320,8 +320,7 @@ void generateHopTable() { systemPrint("ERROR - Wrong AES IV size in bytes, please set AES_IV_BYTES = "); systemPrintln(gcm.ivSize()); - while (1) - petWDT(); + waitForever(); } //Verify the AES key length @@ -329,8 +328,7 @@ void generateHopTable() { systemPrint("ERROR - Wrong AES key size in bytes, please set AES_KEY_BYTES = "); systemPrintln(gcm.keySize()); - while (1) - petWDT(); + waitForever(); } //Set new initial values for AES using settings based random seed diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 38bb07c0..8f98438e 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -302,8 +302,7 @@ void xmitDatagramP2PAck() { systemPrint("ERROR - Please define ACK_BYTES = "); systemPrintln(ackLength); - while (1) - petWDT(); + waitForever(); } /* diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 1d3c117c..f9d86c5b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -39,8 +39,7 @@ void updateRadioState() { systemPrint("Unknown state: "); systemPrintln(radioState); - while (1) - petWDT(); + waitForever(); } break; @@ -1818,7 +1817,7 @@ void verifyRadioStateTable() if (!order) { systemPrintln("ERROR - Failed to allocate the order table from the heap!"); - while (1); + waitForever(); } //Assume the table is in the correct order @@ -1954,10 +1953,9 @@ void verifyRadioStateTable() systemPrintln(expectedState++); } systemPrintln("};"); - systemFlush(); //Wait forever - while (1); + waitForever(); } } @@ -1967,8 +1965,7 @@ void verifyV2DatagramType() if ((sizeof(v2DatagramType) / sizeof(v2DatagramType[0])) != MAX_V2_DATAGRAM_TYPE) { systemPrintln("ERROR - Please update the v2DatagramTable"); - while (1) - petWDT(); + waitForever(); } } diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index da531b31..5bdcff41 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -284,6 +284,19 @@ void systemReset() arch.systemReset(); } +void waitForever() +{ + //Output the remaining serial data + outputSerialData(true); + + //Empty the USB serial device + systemFlush(); + + //Wait forever + while (1) + petWDT(); +} + //Encrypt a given array in place void encryptBuffer(uint8_t *bufferToEncrypt, uint8_t arraySize) { From ab334c1a6ceb1621b817304a12c21e841e511655 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 9 Nov 2022 18:37:07 -1000 Subject: [PATCH 121/594] Remove debug prints from VcServerTest.c --- Firmware/Tools/VcServerTest.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 7cbd7c0e..7ae66ec0 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -49,10 +49,7 @@ int stdinToRadio() vcData[3] = myVcAddr; //Send the data -printf("Sending the message:\n"); -dumpBuffer(vcData, vcData[1] + 1); bytesToSend = write(tty, vcData, vcData[1] + 1); -printf("bytesToSend: %d\n", bytesToSend); if (bytesToSend < 0) { perror("ERROR: Write to radio failed!"); @@ -85,7 +82,7 @@ int radioToStdout() { //Read the virtual circuit header into the local buffer. vcData = outputBuffer; -bytesRead = sizeof(outputBuffer); + bytesRead = sizeof(outputBuffer); bytesRead = read(tty, outputBuffer, bytesRead); if (bytesRead < 0) { From 435e49b4e0294a4156524f1c5cacc561d3e0ccab Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 07:50:01 -1000 Subject: [PATCH 122/594] Fix help for ATR and ATS commands --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 506adf45..6e7d9ab0 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -58,8 +58,8 @@ bool commandAT(const char * commandString) systemPrintln(" ATL - VC link reset"); systemPrintln(" ATO - Exit command mode"); systemPrintln(" ATP - Display probe trigger settings"); - systemPrintln(" ATSn=xxx - Set parameter n's value to xxx"); - systemPrintln(" ATSn? - Print parameter n's current value"); + systemPrintln(" ATR - Display radio settings"); + systemPrintln(" ATS - Display the serial settings"); systemPrintln(" ATT - Enter training mode"); systemPrintln(" ATV - Display virtual circuit settings"); systemPrintln(" ATX - Stop the training server"); From d8e7678c05cafb09b25ea1dd21466cef11d1924c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 07:53:53 -1000 Subject: [PATCH 123/594] Fix letter for ATL command --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 6e7d9ab0..6e843678 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -106,7 +106,7 @@ bool commandAT(const char * commandString) changeState(RADIO_RESET); } break; - case ('R'): //VC link reset + case ('L'): //VC link reset reportOK(); if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { From 013c3e4c50c165fdf3a36a39333403332e6d24e5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 07:56:01 -1000 Subject: [PATCH 124/594] Remove CR and LF for all commands --- Firmware/LoRaSerial_Firmware/Commands.ino | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 6e843678..0bb35d47 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -430,6 +430,12 @@ void checkCommand() //Verify the command length commandString = trimCommand(); //Remove any leading whitespace + + //Remove trailing CR and LF + while ((commandLength > 0) && ((commandString[commandLength - 1] == '\r') + || (commandString[commandLength -1] == '\n'))) + commandLength -= 1; + commandString[commandLength] = '\0'; //Terminate buffer if (commandLength < 2) //Too short reportERROR(); From 4e66e3869bcc9bb5c601a77ed380a782c94abcb4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 19:33:26 -1000 Subject: [PATCH 125/594] Add dumpCircularBuffer routine --- Firmware/LoRaSerial_Firmware/System.ino | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 5bdcff41..c8dd16be 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -472,6 +472,63 @@ void dumpBuffer(uint8_t * data, int length) } } +void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, int length) +{ + int bytes; + uint8_t data; + const int displayWidth = 16; + uint16_t i; + int index; + uint16_t offset; + + offset = tail; + while (length > 0) + { + // Display the offset + systemPrint(" 0x"); + systemPrint((uint16_t)(offset), HEX); + systemPrint(": "); + + // Determine the number of bytes to display + bytes = length; + if (bytes > displayWidth) + bytes = displayWidth; + + // Display the data bytes in hex + for (index = 0; index < bytes; index++) + { + systemWrite(' '); + data = buffer[(offset + index) % bufferLength]; + systemPrint(data, HEX); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + } + + // Space over to the ASCII display + for (; index < displayWidth; index++) + { + systemPrint(" "); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + } + systemPrint(" "); + + // Display the ASCII bytes + for (index = 0; index < bytes; index++) { + data = buffer[(offset + index) % bufferLength]; + systemWrite(((data < ' ') || (data >= 0x7f)) ? '.' : data); + } + systemPrintln(); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + offset += bytes; + length -= bytes; + } +} + void updateRSSI() { //Get the current signal level From 17d289950bd633f2962848ae1750c8ad1a2542fc Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 19:35:01 -1000 Subject: [PATCH 126/594] VC: Change VC_COMMAND to -2, remove VC_LINK_RESET --- Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 0c3e36b4..300a5a3b 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -9,11 +9,8 @@ #define MAX_VC 8 #define VC_SERVER 0 #define VC_BROADCAST -1 -#define VC_UNASSIGNED -2 - -//Source and destinations reserved for the local radio -#define VC_LINK_RESET -3 //Force link reset -#define VC_COMMAND -4 //Command input and command response +#define VC_COMMAND -2 //Command input and command response +#define VC_UNASSIGNED -3 //Source and destinations reserved for the local host #define PC_COMMAND -17 //Command input and command response From 028745453f1ffc661991f396b784191d6e6ef647 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 19:36:36 -1000 Subject: [PATCH 127/594] Add discardPreviousData routine --- Firmware/LoRaSerial_Firmware/States.ino | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index f9d86c5b..437f1183 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -50,6 +50,9 @@ void updateRadioState() case RADIO_RESET: petWDT(); + //Empty the buffers + discardPreviousData(); + //Start the TX timer: time to delay before transmitting the PING setHeartbeatShort(); //Both radios start with short heartbeat period pingRandomTime = random(ackAirTime, ackAirTime * 2); //Fast ping @@ -2056,10 +2059,7 @@ void v2EnterLinkUp() txAckNumber = 0; //Discard any previous data - radioTxTail = radioTxHead; - txTail = txHead; - commandRXTail = commandRXHead; - commandTXTail = commandTXHead; + discardPreviousData(); //Stop the transmit timer transmitTimer = 0; @@ -2074,6 +2074,18 @@ void v2EnterLinkUp() systemPrintln("========== Link UP =========="); } +void discardPreviousData() +{ + //Output any debug messages + outputSerialData(true); + + //Discard any previous data + radioTxTail = radioTxHead; + txTail = txHead; + commandRXTail = commandRXHead; + commandTXTail = commandTXHead; +} + void vcSendLinkStatus(bool linkUp, int8_t srcVc) { //Build the message From 51e260d596abbd31b0c0e9ceb42906ea180df8fd Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 19:47:50 -1000 Subject: [PATCH 128/594] Add more serial debug code --- Firmware/LoRaSerial_Firmware/Serial.ino | 55 ++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index a3bad37a..5989c4db 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -187,6 +187,8 @@ void readyOutgoingCommandPacket() //Scan for escape characters void updateSerial() { + uint16_t previousHead; + uint16_t radioHead; int x; //Assert RTS when there is enough space in the receive buffer @@ -198,6 +200,7 @@ void updateSerial() outputSerialData(false); //Look for local incoming serial + previousHead = rxHead; while (rtsAsserted && arch.serialAvailable() && (transactionComplete == false)) { rxLED(true); //Turn on LED during serial reception @@ -217,7 +220,18 @@ void updateSerial() } //End Serial.available() rxLED(false); //Turn off LED + //Print the number of bytes received via serial + if (settings.debugSerial && (rxHead - previousHead)) + { + systemPrint("updateSerial moved "); + systemPrint(rxHead - previousHead); + systemPrintln(" bytes into serialReceiveBuffer"); + outputSerialData(true); + petWDT(); + } + //Process the serial data + radioHead = radioTxHead; while (availableRXBytes() && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - 1)) && (transactionComplete == false)) { @@ -312,6 +326,15 @@ void updateSerial() } //End process rx buffer } //End Serial.available() + //Print the number of bytes placed into the rxTxBuffer + if (settings.debugSerial && (radioTxHead != radioHead)) + { + systemPrint("processSerialInput moved "); + systemPrint((radioTxHead - radioHead) % sizeof(radioTxBuffer)); + systemPrintln(" bytes from serialReceiveBuffer into radioTxBuffer"); + outputSerialData(true); + } + //Process any remote commands sitting in buffer if (availableRXCommandBytes() && inCommandMode == false) { @@ -323,6 +346,16 @@ void updateSerial() commandRXTail %= sizeof(commandRXBuffer); } + //Print the number of bytes moved into the command buffer + if (settings.debugSerial && commandLength) + { + systemPrint("updateSerial moved "); + systemPrint(commandLength); + systemPrintln(" bytes from commandRXBuffer into commandBuffer"); + outputSerialData(true); + petWDT(); + } + if (commandBuffer[0] == 'R') //Error check { commandBuffer[0] = 'A'; //Convert this RT command to an AT command for local consumption @@ -426,12 +459,25 @@ bool vcSerialMessageReceived() //The length byte is not available break; + //Print the message length and availableRadioTXBytes + if (settings.debugSerial) + { + systemPrint("msgLength: "); + systemPrintln(msgLength); + systemPrint("availableRadioTXBytes(): "); + systemPrintln(availableRadioTXBytes()); + outputSerialData(true); + } + //Verify that the entire message is in the serial buffer if (availableRadioTXBytes() < msgLength) { //The entire message is not in the buffer if (settings.debugSerial) + { systemPrintln("VC serial RX: Waiting for entire buffer"); + outputSerialData(true); + } break; } @@ -440,7 +486,10 @@ bool vcSerialMessageReceived() if (msgLength > maxDatagramSize) { if (settings.debugSerial || settings.debugTransmit) + { systemPrintln("VC serial RX: Message too long, discarded"); + outputSerialData(true); + } //Discard this message, it is too long to transmit over the radio link radioTxTail += msgLength; @@ -464,6 +513,7 @@ bool vcSerialMessageReceived() systemPrint("ERROR: Invalid vcDest "); systemPrint(vcDest); systemPrintln(", discarding message!"); + outputSerialData(true); } //Discard this message @@ -485,14 +535,15 @@ bool vcSerialMessageReceived() systemPrint("Readying "); systemPrint(msgLength); systemPrintln(" byte for transmission"); + outputSerialData(true); } readyOutgoingPacket(msgLength); if (vcDest == myVc) { if (settings.debugSerial) systemPrintln("VC serial RX: Sending to ourself"); - serialOutputByte(START_OF_VC_SERIAL); - serialBufferOutput(outgoingPacket, msgLength); + systemWrite(START_OF_VC_SERIAL); + systemWrite(outgoingPacket, msgLength); endOfTxData -= msgLength; break; } From 512ce79be33797df9e413ba6c50db353f19cd838 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 19:53:47 -1000 Subject: [PATCH 129/594] Add processSerialInput routine --- Firmware/LoRaSerial_Firmware/Serial.ino | 83 ++++++++++++++----------- 1 file changed, 45 insertions(+), 38 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 5989c4db..2d9a42ce 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -188,7 +188,6 @@ void readyOutgoingCommandPacket() void updateSerial() { uint16_t previousHead; - uint16_t radioHead; int x; //Assert RTS when there is enough space in the receive buffer @@ -230,6 +229,50 @@ void updateSerial() petWDT(); } + //Process the serial data + processSerialInput(); + + //Process any remote commands sitting in buffer + if (availableRXCommandBytes() && inCommandMode == false) + { + commandLength = availableRXCommandBytes(); + + for (x = 0 ; x < commandLength ; x++) + { + commandBuffer[x] = commandRXBuffer[commandRXTail++]; + commandRXTail %= sizeof(commandRXBuffer); + } + + //Print the number of bytes moved into the command buffer + if (settings.debugSerial && commandLength) + { + systemPrint("updateSerial moved "); + systemPrint(commandLength); + systemPrintln(" bytes from commandRXBuffer into commandBuffer"); + outputSerialData(true); + petWDT(); + } + + if (commandBuffer[0] == 'R') //Error check + { + commandBuffer[0] = 'A'; //Convert this RT command to an AT command for local consumption + printerEndpoint = PRINT_TO_RF; //Send prints to RF link + checkCommand(); //Parse the command buffer + printerEndpoint = PRINT_TO_SERIAL; + remoteCommandResponse = true; + } + else + { + if (settings.debugRadio) + systemPrintln("Corrupt remote command received"); + } + } +} + +void processSerialInput() +{ + uint16_t radioHead; + //Process the serial data radioHead = radioTxHead; while (availableRXBytes() && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - 1)) @@ -324,7 +367,7 @@ void updateSerial() radioTxBuffer[radioTxHead++] = incoming; radioTxHead %= sizeof(radioTxBuffer); } //End process rx buffer - } //End Serial.available() + } //Print the number of bytes placed into the rxTxBuffer if (settings.debugSerial && (radioTxHead != radioHead)) @@ -334,42 +377,6 @@ void updateSerial() systemPrintln(" bytes from serialReceiveBuffer into radioTxBuffer"); outputSerialData(true); } - - //Process any remote commands sitting in buffer - if (availableRXCommandBytes() && inCommandMode == false) - { - commandLength = availableRXCommandBytes(); - - for (x = 0 ; x < commandLength ; x++) - { - commandBuffer[x] = commandRXBuffer[commandRXTail++]; - commandRXTail %= sizeof(commandRXBuffer); - } - - //Print the number of bytes moved into the command buffer - if (settings.debugSerial && commandLength) - { - systemPrint("updateSerial moved "); - systemPrint(commandLength); - systemPrintln(" bytes from commandRXBuffer into commandBuffer"); - outputSerialData(true); - petWDT(); - } - - if (commandBuffer[0] == 'R') //Error check - { - commandBuffer[0] = 'A'; //Convert this RT command to an AT command for local consumption - printerEndpoint = PRINT_TO_RF; //Send prints to RF link - checkCommand(); //Parse the command buffer - printerEndpoint = PRINT_TO_SERIAL; - remoteCommandResponse = true; - } - else - { - if (settings.debugRadio) - systemPrintln("Corrupt remote command received"); - } - } } void outputSerialData(bool ignoreISR) From fb9c50d59437e3cef27acc31d9823a87e311ad07 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 10 Nov 2022 20:08:35 -1000 Subject: [PATCH 130/594] Add VC routing for commands and data Tested a terminal emulator (minicom) on Linux (Ubuntu) to set the following parameters: //Server +++ at&f at-AlternateLedUsage=1 at-EnableCRC16=1 at-FrequencyHop=0 at-MaxResends=2 at-NetID=56 at-OperatingMode=2 at-PrintLinkUpDown=1 at-TxPower=14 at-VerifyRxNetID=1 at-TrainingServer=1 at&w atz //Client +++ at&f at-AlternateLedUsage=1 at-EnableCRC16=1 at-FrequencyHop=0 at-MaxResends=2 at-NetID=56 at-OperatingMode=2 at-PrintLinkUpDown=1 at-TxPower=14 at-VerifyRxNetID=1 at&w atz After the parameters are set, exit the terminal emulator and start the VcServerTest program. //Server - Broadcast, send data to all clients ./VcServerTest /dev/ttyACM0 0 -1 //Server - Send commands to the local radio ./VcServerTest /dev/ttyACM0 0 -2 //Server - Send data to client 1 ./VcServerTest /dev/ttyACM0 0 1 //Server - local echo ./VcServerTest /dev/ttyACM0 0 0 //Client - send data to the server (0) ./VcServerTest /dev/ttyACM1 1 0 --- .../LoRaSerial_Firmware.ino | 9 +- Firmware/LoRaSerial_Firmware/Serial.ino | 238 +++++++++++++++--- Firmware/Tools/VcServerTest.c | 51 +++- 3 files changed, 253 insertions(+), 45 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 9feb4c29..98394624 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -192,8 +192,11 @@ bool rtsAsserted; //When RTS is asserted, host says it's ok to send data | Flow control: RTS for UART | Off: Buffer full | On: Buffer drops below half full + | updateSerial V serialReceiveBuffer + | + | processSerialInput | | inCommandMode? | @@ -252,11 +255,13 @@ bool rtsAsserted; //When RTS is asserted, host says it's ok to send data | Flow control: RTS for UART | Off: Buffer full | On: Buffer drops below half full + | updateSerial V serialReceiveBuffer | - | Destination VC? + | vcProcessSerialInput | + | Destination VC? V Other .-------------+--------------->+---------------->+------> Discard VC_COMMAND | myVc | >= 0 | @@ -445,6 +450,7 @@ uint8_t *rxVcData; int8_t txDestVc; unsigned long vcAckTimer; VIRTUAL_CIRCUIT virtualCircuitList[MAX_VC]; +uint8_t serialOperatingMode; unsigned int multipointChannelLoops = 0; //Count the number of times Multipoint scanning has traversed the table unsigned int multipointAttempts = 0; //Throttle back scanning when a server is not detected @@ -472,6 +478,7 @@ void setup() beginSerial(57600); //Default for debug messages before board begins loadSettings(); //Load settings from EEPROM + serialOperatingMode = settings.operatingMode; beginSerial(settings.serialSpeed); diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 2d9a42ce..284c43fe 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -230,7 +230,10 @@ void updateSerial() } //Process the serial data - processSerialInput(); + if (serialOperatingMode == MODE_VIRTUAL_CIRCUIT) + vcProcessSerialInput(); + else + processSerialInput(); //Process any remote commands sitting in buffer if (availableRXCommandBytes() && inCommandMode == false) @@ -409,30 +412,8 @@ void outputSerialData(bool ignoreISR) //Get the message length byte from the serial buffer bool vcSerialMsgGetLengthByte(uint8_t * msgLength) { - uint16_t dataBytes; - uint16_t index; - - //Discard any garbage bytes - dataBytes = availableRadioTXBytes(); - while (dataBytes && (radioTxBuffer[radioTxTail] != START_OF_VC_SERIAL)) - { - radioTxTail = (radioTxTail + 1) % sizeof(radioTxBuffer); - dataBytes -= 1; - } - - //The second byte is the length which does not include the start byte - if (dataBytes < 2) - return false; - - //The first byte is the start serial byte - if (availableRadioTXBytes() < 2) - return false; - //Get the length byte for the received serial message, account for the start byte - index = radioTxTail + 1; - if (index >= sizeof(radioTxBuffer)) - index -= sizeof(radioTxBuffer); - *msgLength = radioTxBuffer[index] + 1; + *msgLength = radioTxBuffer[radioTxTail]; return true; } @@ -442,7 +423,7 @@ uint8_t vcSerialMsgGetVcDest() uint16_t index; //Get the destination address byte - index = radioTxTail + 2; + index = radioTxTail + 1; if (index >= sizeof(radioTxBuffer)) index -= sizeof(radioTxBuffer); return radioTxBuffer[index]; @@ -451,6 +432,7 @@ uint8_t vcSerialMsgGetVcDest() //Determine if received serial data may be sent to the remote system bool vcSerialMessageReceived() { + uint16_t dataBytes; uint8_t msgLength; int8_t vcDest; @@ -461,11 +443,14 @@ bool vcSerialMessageReceived() //The radio is busy, wait until it is idle break; - //Wait until the length byte is available - if (!vcSerialMsgGetLengthByte(&msgLength)) - //The length byte is not available + //Check for some data bytes + dataBytes = availableRadioTXBytes(); + if (!dataBytes) break; + //Wait until the length byte is available + msgLength = radioTxBuffer[radioTxTail]; + //Print the message length and availableRadioTXBytes if (settings.debugSerial) { @@ -530,13 +515,7 @@ bool vcSerialMessageReceived() break; } - //Skip over the start byte - radioTxTail += 1; - if (radioTxTail >= sizeof(radioTxBuffer)) - radioTxTail -= sizeof(radioTxBuffer); - msgLength -= 1; - - //If sending to ourself, just place the data in the serial output buffer + //Print the data ready for transmission if (settings.debugSerial) { systemPrint("Readying "); @@ -544,6 +523,8 @@ bool vcSerialMessageReceived() systemPrintln(" byte for transmission"); outputSerialData(true); } + + //If sending to ourself, just place the data in the serial output buffer readyOutgoingPacket(msgLength); if (vcDest == myVc) { @@ -563,6 +544,193 @@ bool vcSerialMessageReceived() return false; } +void vcProcessSerialInput() +{ + char * cmd; + uint8_t data; + uint16_t dataBytes; + uint16_t index; + int8_t vcDest; + int8_t vcSrc; + uint8_t length; + + //Process the serial data while there is space in radioTxBuffer + dataBytes = availableRXBytes(); + while (dataBytes && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - 256)) + && (transactionComplete == false)) + { + //Take a break if there are ISRs to attend to + petWDT(); + if (timeToHop == true) hopChannel(); + + //Skip any garbage in the input stream + data = serialReceiveBuffer[rxTail++]; + rxTail %= sizeof(radioTxBuffer); + if (data != START_OF_VC_SERIAL) + { + if (settings.debugSerial) + { + systemPrint("vcProcessSerialInput discarding 0x"); + systemPrint(data, HEX); + systemPrintln(); + outputSerialData(true); + } + + //Discard this data byte + dataBytes = availableRXBytes(); + continue; + } + + //Get the virtual circuit header + //The start byte has already been removed + length = serialReceiveBuffer[rxTail]; + if (length <= VC_SERIAL_HEADER_BYTES) + { + if (settings.debugSerial) + { + systemPrint("ERROR - Invalid length "); + systemPrint(length); + systemPrint(", discarding 0x"); + systemPrint(data, HEX); + systemPrintln(); + outputSerialData(true); + } + + //Invalid message length, discard the START_OF_VC_SERIAL + dataBytes = availableRXBytes(); + continue; + } + + //Skip if there is not enough data + if (dataBytes < length) + { + if (settings.debugSerial) + { + systemPrint("ERROR - Invalid length "); + systemPrint(length); + systemPrint(", discarding 0x"); + systemPrint(data, HEX); + systemPrintln(); + outputSerialData(true); + } + dataBytes = availableRXBytes(); + continue; + } + + //Get the source and destination virtual circuits + index = (rxTail + 1) % sizeof(serialReceiveBuffer); + vcDest = serialReceiveBuffer[index]; + index = (rxTail + 2) % sizeof(serialReceiveBuffer); + vcSrc = serialReceiveBuffer[index]; + + //Process this message + switch ((uint8_t)vcDest) + { + //Send data over the radio link + default: + //Validate the source virtual circuit + if ((vcSrc != myVc) || (myVc == VC_UNASSIGNED)) + { + if (settings.debugSerial) + { + systemPrint("ERROR: Invalid myVc "); + systemPrint(myVc); + systemPrintln(" for data message, discarding message!"); + outputSerialData(true); + } + + //Discard this message + rxTail = (rxTail + length) % sizeof(radioTxBuffer); + break; + } + + //Verify the destination virtual circuit + if ((vcDest < VC_BROADCAST) || (vcDest >= MAX_VC)) + { + if (settings.debugSerial) + { + systemPrint("ERROR: Invalid vcDest "); + systemPrint(vcDest); + systemPrintln(" for data message, discarding message!"); + outputSerialData(true); + } + + //Discard this message + rxTail = (rxTail + length) % sizeof(radioTxBuffer); + break; + } + + if (settings.debugSerial) + { + systemPrint("vcProcessSerialInput moving "); + systemPrint(length); + systemPrintln(" bytes into radioTxBuffer"); + dumpCircularBuffer(serialReceiveBuffer, rxTail, sizeof(serialReceiveBuffer), length); + outputSerialData(true); + } + + //Place the data in radioTxBuffer + for (; length > 0; length--) + { + radioTxBuffer[radioTxHead++] = serialReceiveBuffer[rxTail++]; + radioTxHead %= sizeof(radioTxBuffer); + rxTail %= sizeof(serialReceiveBuffer); + } + break; + + //Process this command + case (uint8_t)VC_COMMAND: + //Validate the source virtual circuit + if ((vcSrc != PC_COMMAND) || (length > (sizeof(commandBuffer) - 1))) + { + if (settings.debugSerial) + { + systemPrint("ERROR: Invalid vcSrc "); + systemPrint(myVc); + systemPrintln(" for command message, discarding message!"); + outputSerialData(true); + } + + //Discard this message + rxTail = (rxTail + length) % sizeof(radioTxBuffer); + break; + } + + //Discard the VC header + length -= VC_RADIO_HEADER_BYTES; + rxTail = (rxTail + VC_RADIO_HEADER_BYTES) % sizeof(radioTxBuffer); + + //Move this message into the command buffer + for (cmd = commandBuffer; length > 0; length--) + { + *cmd++ = toupper(serialReceiveBuffer[rxTail++]); + rxTail %= sizeof(serialReceiveBuffer); + } + commandLength = cmd - commandBuffer; + + if (settings.debugSerial) + { + systemPrint("vcProcessSerialInput moving "); + systemPrint(commandLength); + systemPrintln(" bytes into commandBuffer"); + outputSerialData(true); + dumpBuffer((uint8_t *)commandBuffer, commandLength); + } + + //Process this command + checkCommand(); + break; + } + + //Determine how much data is left in the buffer + dataBytes = availableRXBytes(); + } + + //Take a break if there are ISRs to attend to + petWDT(); + if (timeToHop == true) hopChannel(); +} + void resetSerial() { uint32_t delayTime; diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 7ae66ec0..98db90de 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -46,7 +46,7 @@ int stdinToRadio() vcData[0] = START_OF_VC_SERIAL; vcData[1] = bytesToSend + VC_RADIO_HEADER_BYTES; vcData[2] = remoteVcAddr; - vcData[3] = myVcAddr; + vcData[3] = (remoteVcAddr == VC_COMMAND) ? PC_COMMAND : myVcAddr; //Send the data bytesToSend = write(tty, vcData, vcData[1] + 1); @@ -142,6 +142,7 @@ main ( { int maxfds; int status; + char * terminal; struct timeval timeout; uint8_t * vcData; @@ -149,20 +150,52 @@ main ( do { //Display the help text if necessary - if ((argc != 2) || (sscanf(argv[1], "/dev/ttyACM%d", &myVcAddr) != 1)) + if (argc != 4) { - printf("%s terminal\n", argv[0]); + printf("%s terminal my_VC target_VC\n", argv[0]); printf("\n"); printf("terminal - Name or path to the terminal device for the radio\n"); + printf("my_VC:\n"); + printf(" Server: 0\n"); + printf(" Client: 1 - %d\n", MAX_VC - 1); + printf("target_VC:\n"); + printf(" Server: 0\n"); + printf(" Client: 1 - %d\n", MAX_VC - 1); + printf(" Loopback: my_VC\n"); + printf(" Broadcast: %d\n", VC_BROADCAST); + printf(" Command: %d\n", VC_COMMAND); status = -1; break; } - //Determine the remote VC - if (myVcAddr) - remoteVcAddr = 0; - else - remoteVcAddr = 1; + //Get the path to the terminal + terminal = argv[1]; + + //Determine the local VC address + if ((sscanf(argv[2], "%d", &myVcAddr) != 1) + || ((myVcAddr < VC_SERVER) || (myVcAddr >= MAX_VC))) + { + fprintf(stderr, "ERROR: Invalid my VC address, please use one of the following:\n"); + fprintf(stderr, " Server: 0\n"); + fprintf(stderr, " Client: 1 - %d\n", MAX_VC - 1); + status = -1; + break; + } + + //Determine the remote VC address + if ((sscanf(argv[3], "%d", &remoteVcAddr) != 1) + || (remoteVcAddr < VC_COMMAND) || (remoteVcAddr >= MAX_VC)) + { + fprintf(stderr, "ERROR: Invalid target VC address, please use one of the following:\n"); + if (myVcAddr) + fprintf(stderr, " Server: 0\n"); + fprintf(stderr, " Client: 1 - %d\n", MAX_VC - 1); + fprintf(stderr, " Loopback: my_VC\n"); + fprintf(stderr, " Broadcast: %d\n", VC_BROADCAST); + fprintf(stderr, " Command: %d\n", VC_COMMAND); + status = -1; + break; + } //Update STDIN and STDOUT /* @@ -176,7 +209,7 @@ main ( //Open the terminal maxfds = STDIN; - status = openTty(argv[1]); + status = openTty(terminal); if (status) break; if (maxfds < tty) From c58e867a3f8e02b95ca229302388f635c3cd68fe Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 11 Nov 2022 15:58:33 -0700 Subject: [PATCH 131/594] Get RSSI during reception then average --- .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 8 ++++++-- Firmware/LoRaSerial_Firmware/Radio.ino | 5 +++++ Firmware/LoRaSerial_Firmware/States.ino | 4 ---- Firmware/LoRaSerial_Firmware/System.ino | 13 ++++++++++--- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 9feb4c29..7ac5d81d 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -339,13 +339,15 @@ bool expectingAck = false; //Used by various send packet functions float frequencyCorrection = 0; //Adjust receive freq based on the last packet received freqError volatile bool hop = true; //Clear the DIO1 hop ISR when possible +int hopCount = 0; //Used to average the RSSI measured during hops //RSSI must be above these negative numbers for LED to illuminate const int rssiLevelLow = -150; const int rssiLevelMed = -70; const int rssiLevelHigh = -50; const int rssiLevelMax = -20; -int rssi; //Signal level +int rssi; //Average signal level, measured during reception of a packet +int avgRssi; //Average signal level, measured during reception of a packet //Link quality metrics uint32_t datagramsSent; //Total number of datagrams sent @@ -517,9 +519,11 @@ void loop() updateRadioState(); //Ping/ack/send packets as needed - if (hop) //Allow DIO1 hop ISR but use it only for debug + if (hop) //If the hop ISR has triggered, measure RSSI during reception { hop = false; radio.clearFHSSInt(); //Clear the interrupt + rssi = radio.getRSSI(); + hopCount++; } } diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 2c8750de..a6e5a0bc 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -220,6 +220,10 @@ void returnToReceiving() systemPrintln(state); } } + + //Reset RSSI measurements + rssi = 0; + hopCount = 0; } //Given spread factor, bandwidth, coding rate and number of bytes, return total Air Time in ms for packet @@ -525,6 +529,7 @@ void transactionCompleteISR(void) //ISR when DIO1 goes low //Called when FhssChangeChannel interrupt occurs (at regular HoppingPeriods) //We do not use SX based channel hopping, and instead use a synchronized hardware timer +//We use the hop ISR to measure RSSI during reception void hopISR(void) { hop = true; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 360aba19..3648f431 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -543,8 +543,6 @@ void updateRadioState() */ case RADIO_P2P_LINK_UP: - updateRSSI(); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -787,7 +785,6 @@ void updateRadioState() printPacketQuality(); - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -808,7 +805,6 @@ void updateRadioState() printPacketQuality(); - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 99f869dc..806ee6a7 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -461,8 +461,15 @@ void dumpBuffer(uint8_t * data, int length) void updateRSSI() { - //Get the current signal level - rssi = radio.getRSSI(); + //Calculate the average RSSI if possible + if(hopCount > 0) + { + rssi /= hopCount; + hopCount = 0; + } + else + rssi = radio.getRSSI(); + if (settings.alternateLedUsage) return; @@ -656,7 +663,7 @@ void printPacketQuality() { systemPrintln(); systemPrint("R:"); - systemPrint(radio.getRSSI()); + systemPrint(rssi); systemPrint("\tS:"); systemPrint(radio.getSNR()); systemPrint("\tfE:"); From 86e217a1e4bfb4efb116a8d6153dd3752eb69bee Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 11 Nov 2022 07:25:44 -1000 Subject: [PATCH 132/594] Tools: Remove Client.c and Server.c --- .gitignore | 2 -- Firmware/Tools/Client.c | 26 -------------------------- Firmware/Tools/Server.c | 24 ------------------------ Firmware/Tools/makefile | 8 -------- 4 files changed, 60 deletions(-) delete mode 100644 Firmware/Tools/Client.c delete mode 100644 Firmware/Tools/Server.c diff --git a/.gitignore b/.gitignore index 99e427e8..01f377f7 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,4 @@ Icon *.a # Executables -Firmware/Tools/Client -Firmware/Tools/Server Firmware/Tools/VcServerTest diff --git a/Firmware/Tools/Client.c b/Firmware/Tools/Client.c deleted file mode 100644 index 0d54a2a4..00000000 --- a/Firmware/Tools/Client.c +++ /dev/null @@ -1,26 +0,0 @@ -#include "settings.h" - -int -main ( - int argc, - char ** argv -) -{ - int status; - - if (argc == 2) - //Open the terminal - status = openTty(argv[1]); - else - //Open the socket - status = openSocket(); - if (status) - return status; - - //Set the ID - myUniqueId[0] = 1; - - //Implement the protocol - defaultSettings(&settings); - return processData(); -} diff --git a/Firmware/Tools/Server.c b/Firmware/Tools/Server.c deleted file mode 100644 index 8a7e69f7..00000000 --- a/Firmware/Tools/Server.c +++ /dev/null @@ -1,24 +0,0 @@ -#include "settings.h" - -int -main ( - int argc, - char ** argv -) -{ - int status; - - if (argc == 2) - //Open the terminal - status = openTty(argv[1]); - else - //Open the socket - status = openSocket(); - if (status) - return status; - - //Implement the protocol - defaultSettings(&settings); - settings.trainingServer = true; - return processData(); -} diff --git a/Firmware/Tools/makefile b/Firmware/Tools/makefile index f53940f0..808895d7 100644 --- a/Firmware/Tools/makefile +++ b/Firmware/Tools/makefile @@ -8,8 +8,6 @@ # Source files ########## -EXECUTABLES = Client -EXECUTABLES += Server EXECUTABLES += VcServerTest INCLUDES = settings.h ../LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -52,12 +50,6 @@ $(COMMON_LIB): $(LIB_OBJS) # Build the executables ########## -Client: Client.c $(COMMON_LIB) - $(CC) -o $@ $^ - -Server: Server.c $(COMMON_LIB) - $(CC) -o $@ $^ - VcServerTest: VcServerTest.c $(COMMON_LIB) $(CC) -o $@ $^ From eb798eceb6428411711086b0f2959d930e4f945d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 11 Nov 2022 09:43:21 -1000 Subject: [PATCH 133/594] Fix length in link status message --- Firmware/LoRaSerial_Firmware/States.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 437f1183..bd276dc7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2095,7 +2095,7 @@ void vcSendLinkStatus(bool linkUp, int8_t srcVc) //Build the message header VC_SERIAL_MESSAGE_HEADER header; header.start = START_OF_VC_SERIAL; - header.radio.length = sizeof(header) + sizeof(message); + header.radio.length = VC_RADIO_HEADER_BYTES + sizeof(message); header.radio.destVc = PC_LINK_STATUS; header.radio.srcVc = srcVc; @@ -2136,7 +2136,7 @@ void vcBreakLink(int8_t vcIndex) linkFailures++; //Send the status message - vcSendLinkStatus(false, myVc); + vcSendLinkStatus(false, vcIndex); //Stop the transmit timer transmitTimer = 0; From 7957b7857f4d0af8707d2e6c47936e37948051cf Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 11 Nov 2022 13:09:13 -1000 Subject: [PATCH 134/594] Rename ATL command to ATB (break link) Add debug output to the command Call break link routine to notify caller that the link is down Only reset ring buffers when the local host's link breaks --- Firmware/LoRaSerial_Firmware/Commands.ino | 55 +++++++++++++++++------ Firmware/LoRaSerial_Firmware/Serial.ino | 4 ++ Firmware/LoRaSerial_Firmware/States.ino | 4 +- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 0bb35d47..5954900f 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -32,8 +32,9 @@ typedef struct bool commandAT(const char * commandString) { - VIRTUAL_CIRCUIT * vc; + uint32_t delayMillis; unsigned long timer; + VIRTUAL_CIRCUIT * vc; //'AT' if (commandLength == 2) @@ -49,13 +50,13 @@ bool commandAT(const char * commandString) case ('?'): //Display the command help systemPrintln("Command summary:"); systemPrintln(" AT? - Print the command summary"); + systemPrintln(" ATB - Break the link"); systemPrintln(" ATD - Display the debug settings"); systemPrintln(" ATF - Enter training mode and return to factory defaults"); systemPrintln(" ATG - Generate new netID and encryption key"); systemPrintln(" ATI - Display the radio version"); systemPrintln(" ATI? - Display the information commands"); systemPrintln(" ATIn - Display system information"); - systemPrintln(" ATL - VC link reset"); systemPrintln(" ATO - Exit command mode"); systemPrintln(" ATP - Display probe trigger settings"); systemPrintln(" ATR - Display radio settings"); @@ -70,6 +71,44 @@ bool commandAT(const char * commandString) systemPrintln(" AT&F - Restore factory settings"); systemPrintln(" AT&W - Save current settings to NVM"); break; + case ('B'): //Break the link + reportOK(); + + //Compute the time delay + delayMillis = (VC_LINK_BREAK_MULTIPLIER + 2) * settings.heartbeatTimeout; + + //Warn the user of the delay + if (settings.printLinkUpDown) + { + systemPrint("Delaying "); + systemPrint(delayMillis / 1000); + systemPrintln(" seconds to break the link"); + outputSerialData(true); + } + + //Flag the links as broken + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + for (int i=0; i < MAX_VC; i++) + if (virtualCircuitList[i].linkUp && (i != myVc)) + vcBreakLink(i); + } + else if (settings.operatingMode == MODE_POINT_TO_POINT) + { + v2BreakLink(); + outputSerialData(true); + } + + //Idle the system to break the link + //This is required on the server system which does not request an VC number assignment + timer = millis(); + while ((millis() - timer) < ((VC_LINK_BREAK_MULTIPLIER + 2) * settings.heartbeatTimeout)) + petWDT(); + + //Display the end of the delay + if (settings.printLinkUpDown) + systemPrintln("Delay done"); + break; case ('F'): //Enter training mode and return to factory defaults reportOK(); selectTraining(true); @@ -106,17 +145,6 @@ bool commandAT(const char * commandString) changeState(RADIO_RESET); } break; - case ('L'): //VC link reset - reportOK(); - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - { - //Idle the system to break the link - //This is required on the server system which does not request an VC number assignment - timer = millis(); - while ((millis() - timer) < ((VC_LINK_BREAK_MULTIPLIER + 2) * settings.heartbeatTimeout)) - petWDT(); - } - break; case ('T'): //Enter training mode reportOK(); selectTraining(false); @@ -457,6 +485,7 @@ void checkCommand() //Print the command failure if (!success) reportERROR(); + outputSerialData(true); commandLength = 0; //Get ready for next command } diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 284c43fe..21fffe8a 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -736,6 +736,9 @@ void resetSerial() uint32_t delayTime; uint32_t lastCharacterReceived; + //Display any debug output + outputSerialData(true); + //Determine the amount of time needed to receive a character delayTime = 200; if (settings.airSpeed) @@ -771,4 +774,5 @@ void resetSerial() commandRXHead = commandRXTail; commandTXHead = commandTXTail; endOfTxData = &outgoingPacket[headerBytes]; + commandLength = 0; } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bd276dc7..af4404b7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2142,7 +2142,9 @@ void vcBreakLink(int8_t vcIndex) transmitTimer = 0; //Flush the buffers - resetSerial(); + outputSerialData(true); + if (vcIndex == myVc) + resetSerial(); } int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) From 97ead3c8a258d03e275436766b88cc34ffb714a9 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 11 Nov 2022 16:11:09 -1000 Subject: [PATCH 135/594] Separate debug serial from VC traffic such as link status --- Firmware/Tools/VcServerTest.c | 183 +++++++++++++++++++++++----------- 1 file changed, 126 insertions(+), 57 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 98db90de..520a13c7 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -15,16 +15,17 @@ int stdinToRadio() int bytesRead; int bytesSent; int bytesToSend; + VC_SERIAL_MESSAGE_HEADER * header; + int length; int maxfds; int status; struct timeval timeout; - uint8_t * vcData; status = 0; do { //Read the console input data into the local buffer. - vcData = inputBuffer; + header = (VC_SERIAL_MESSAGE_HEADER *)inputBuffer; bytesRead = read(STDIN, &inputBuffer[VC_SERIAL_HEADER_BYTES], BUFFER_SIZE); if (bytesRead < 0) { @@ -43,13 +44,17 @@ int stdinToRadio() bytesToSend = MAX_MESSAGE_SIZE; //Build the virtual circuit serial header - vcData[0] = START_OF_VC_SERIAL; - vcData[1] = bytesToSend + VC_RADIO_HEADER_BYTES; - vcData[2] = remoteVcAddr; - vcData[3] = (remoteVcAddr == VC_COMMAND) ? PC_COMMAND : myVcAddr; + length = VC_RADIO_HEADER_BYTES + bytesToSend; + header->start = START_OF_VC_SERIAL; + header->radio.length = length; + header->radio.destVc = remoteVcAddr; + header->radio.srcVc = (remoteVcAddr == VC_COMMAND) ? PC_COMMAND : myVcAddr; + + //Display the data being sent to the radio +// dumpBuffer((uint8_t *)header, VC_SERIAL_HEADER_BYTES + bytesToSend); //Send the data - bytesToSend = write(tty, vcData, vcData[1] + 1); + bytesToSend = write(tty, header, length + 1); if (bytesToSend < 0) { perror("ERROR: Write to radio failed!"); @@ -64,73 +69,147 @@ int stdinToRadio() return status; } -int radioToStdout() +int hostToStdout(uint8_t * data, uint8_t bytesToSend) +{ + int bytesSent; + int bytesWritten; + int status; + + //Write this data to stdout + bytesSent = 0; + while (bytesSent < bytesToSend) + { + bytesWritten = write(STDOUT, &data[bytesSent], bytesToSend - bytesSent); + if (bytesWritten < 0) + { + perror("ERROR: Write to stdout!"); + status = bytesWritten; + break; + } + + //Account for the bytes written + bytesSent += bytesWritten; + } + return status; +} + +void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) +{ + VC_LINK_STATUS_MESSAGE * linkStatus; + + linkStatus = (VC_LINK_STATUS_MESSAGE *)&header[1]; + if (linkStatus->linkStatus == LINK_UP) + printf("========== Link %d UP ==========\n", header->radio.srcVc); + else + printf("--------- Link %d DOWN ---------\n", header->radio.srcVc); +} + +int radioToHost() { int bytesRead; int bytesSent; + int bytesToRead; int bytesToSend; + static uint8_t * data = outputBuffer; + static uint8_t * dataStart = outputBuffer; + static uint8_t * dataEnd = outputBuffer; int8_t destAddr; + static VC_SERIAL_MESSAGE_HEADER * header = (VC_SERIAL_MESSAGE_HEADER *)outputBuffer; uint8_t length; int maxfds; int status; int8_t srcAddr; struct timeval timeout; - uint8_t * vcData; status = 0; do { //Read the virtual circuit header into the local buffer. - vcData = outputBuffer; - bytesRead = sizeof(outputBuffer); - bytesRead = read(tty, outputBuffer, bytesRead); + bytesToRead = &outputBuffer[sizeof(outputBuffer)] - dataEnd; + bytesRead = read(tty, dataEnd, bytesToRead); + if (bytesRead == 0) + break; if (bytesRead < 0) { perror("ERROR: Read from radio failed!"); status = bytesRead; break; } + dataEnd += bytesRead; -/* - //Display the VC header - length = vcData[0]; - destAddr = vcData[1]; - srcAddr = vcData[2]; - printf("length: %d\n", length); - printf("destAddr: %d\n", destAddr); - printf("srcAddr: %d\n", srcAddr); - - //Read the message - vcData = outputBuffer; - bytesRead = read(STDIN, outputBuffer, length); - if (bytesRead < 0) + //Display the data received from the radio +// if (bytesRead) dumpBuffer(dataStart, dataEnd - dataStart); + + //The data read is a mix of debug serial output and virtual circuit messages + //Any data before the VC_SERIAL_MESSAGE_HEADER is considered debug serial output + data = dataStart; + while ((data < dataEnd) && (*data != START_OF_VC_SERIAL)) + data++; + + //Process any debug data + length = data - dataStart; + if (length) + //Output the debug data + hostToStdout(dataStart, length); + + //Determine if this is the beginning of a virtual circuit message + length = dataEnd - data; + if (length <= 0) { - perror("ERROR: Read from radio failed!"); - status = bytesRead; + //Refill the empty buffer + dataStart = outputBuffer; + data = dataStart; + dataEnd = data; break; } -*/ - //Send this data over the VC - bytesSent = 0; - while (bytesSent < bytesRead) + //This is the beginning of a virtual circuit message + //Move it to the beginning of the buffer to make things easier + if (data != outputBuffer) + memcpy(outputBuffer, data, length); + dataEnd = &outputBuffer[length]; + dataStart = outputBuffer; + data = dataStart; + + //Determine if the VC header is in the buffer + if (length < VC_SERIAL_HEADER_BYTES) + //Need more data + break; + + //Determine if the entire message is in the buffer + if (length < (header->radio.length + 1)) + //Need more data + break; + + //Set the beginning of the message + data = &outputBuffer[VC_SERIAL_HEADER_BYTES]; + length = header->radio.length - VC_RADIO_HEADER_BYTES; + + //Process the message + switch (header->radio.destVc) { - //Send the data - bytesToSend = bytesRead - bytesSent; - if (bytesToSend > MAX_MESSAGE_SIZE) - bytesToSend = MAX_MESSAGE_SIZE; - bytesToSend = write(STDOUT, &vcData[bytesSent], bytesToSend); - if (bytesToSend < 0) - { - perror("ERROR: Write to radio failed!"); - status = bytesToSend; - break; - } + default: + //Display the VC header and message + printf("VC Header:\n"); + printf(" length: %d\n", header->radio.length); + printf(" destVc: %d\n", header->radio.destVc); + printf(" srcVc: %d\n", header->radio.srcVc); + if (length > 0) + dumpBuffer(data, length); - //Account for the bytes written - bytesSent += bytesToSend; + //Discard this message + break; + + case PC_LINK_STATUS: + radioToPcLinkStatus(header, VC_SERIAL_HEADER_BYTES + length); + break; } - } while (0); + + //Continue processing the rest of the data in the buffer + if (length > 0) + data += length; + dataStart = data; + } while(0); return status; } @@ -197,16 +276,6 @@ main ( break; } - //Update STDIN and STDOUT -/* - status = updateTerm(STDIN); - if (status) - break; - status = updateTerm(STDOUT); - if (status) - break; -*/ - //Open the terminal maxfds = STDIN; status = openTty(terminal); @@ -245,8 +314,8 @@ main ( if (FD_ISSET(tty, &readfds)) { - //Write the radio data to console output - status = radioToStdout(); + //Process the incoming data from the radio + status = radioToHost(); if (status) break; } From 10e71614f43b4fe2baacd964d181388cce7e4b94 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 11 Nov 2022 16:26:56 -1000 Subject: [PATCH 136/594] Set usbSerialWait based upon if ENABLE_DEVELOPER is defined When ENABLE_DEVELOPER is defined, set usbSerialWait to true by default. Allow the user to override this value by setting usbSerialWait to false and saving the value to NVM. --- Firmware/LoRaSerial_Firmware/Arch_ESP32.h | 2 -- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 2 -- Firmware/LoRaSerial_Firmware/settings.h | 7 ++++++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h index fca52d85..4efd73ac 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h +++ b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h @@ -44,9 +44,7 @@ void esp32BeginBoard() void esp32BeginSerial(uint16_t serialSpeed) { -#if !defined(ENABLE_DEVELOPER) if (settings.usbSerialWait) -#endif //ENABLE_DEVELOPER //Wait for serial to come online for debug printing delay(500); } diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 8ad6c100..f310ae43 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -134,9 +134,7 @@ void samdBeginBoard() void samdBeginSerial(uint16_t serialSpeed) { -#if !defined(ENABLE_DEVELOPER) if (settings.usbSerialWait) -#endif //ENABLE_DEVELOPER //Wait for serial to come online for debug printing while (!Serial); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 8fd9847c..6e0d315a 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -379,7 +379,12 @@ typedef struct struct_settings { bool debugRadio = false; //Print radio info bool debugStates = false; //Print state changes bool debugTraining = false; //Print training info - bool usbSerialWait = false; //Wait for USB serial initialization +#if defined(ENABLE_DEVELOPER) +#define WAIT_SERIAL_DEFAULT true +#else //ENABLE_DEVELOPER +#define WAIT_SERIAL_DEFAULT false +#endif //ENABLE_DEVELOPER + bool usbSerialWait = WAIT_SERIAL_DEFAULT; //Wait for USB serial initialization bool printRfData = false; //Print RX and TX data bool printPktData = false; //Print data, before encryption and after decryption bool verifyRxNetID = false; //Verify RX netID value when not operating in point-to-point mode From 846f177b65342d93685eaa9ec0abdd985a25040b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 11 Nov 2022 17:20:01 -1000 Subject: [PATCH 137/594] VcTestServer: Force the link to break upon start This allows the program to see all of the link up messages without testing each of the virtual circuit connections. --- Firmware/Tools/VcServerTest.c | 156 +++++++++++++++++++++++++++------- 1 file changed, 125 insertions(+), 31 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 520a13c7..90c0fea4 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -5,17 +5,110 @@ #define STDIN 0 #define STDOUT 1 -int myVcAddr; -int remoteVcAddr; -uint8_t inputBuffer[VC_SERIAL_HEADER_BYTES + BUFFER_SIZE]; +#define LINK_RESET_COMMAND "atb" + +#define DEBUG_PC_TO_RADIO 0 +#define DEBUG_RADIO_TO_PC 0 + +int myVc; +int remoteVc; +uint8_t inputBuffer[BUFFER_SIZE]; uint8_t outputBuffer[VC_SERIAL_HEADER_BYTES + BUFFER_SIZE]; +int cmdToRadio(uint8_t * buffer, int length) +{ + int bytesSent; + int bytesWritten; + VC_SERIAL_MESSAGE_HEADER header; + + //Build the virtual circuit serial header + header.start = START_OF_VC_SERIAL; + header.radio.length = VC_RADIO_HEADER_BYTES + length; + header.radio.destVc = VC_COMMAND; + header.radio.srcVc = PC_COMMAND; + + //Display the data being sent to the radio + if (DEBUG_PC_TO_RADIO) + { + dumpBuffer((uint8_t *)&header, VC_SERIAL_HEADER_BYTES); + dumpBuffer(buffer, length); + } + + //Send the header + bytesWritten = write(tty, (uint8_t *)&header, VC_SERIAL_HEADER_BYTES); + if (bytesWritten < (int)VC_SERIAL_HEADER_BYTES) + { + perror("ERROR: Write of header to radio failed!"); + return -1; + } + + //Send the message + bytesSent = 0; + while (bytesSent < length) + { + bytesWritten = write(tty, buffer, length); + if (bytesWritten < 0) + { + perror("ERROR: Write of data to radio failed!"); + return bytesWritten; + } + bytesSent += bytesWritten; + } + + //Return the amount of the buffer that was sent + return length; +} + +int hostToRadio(uint8_t destVc, uint8_t * buffer, int length) +{ + int bytesSent; + int bytesWritten; + VC_SERIAL_MESSAGE_HEADER header; + + //Build the virtual circuit serial header + header.start = START_OF_VC_SERIAL; + header.radio.length = VC_RADIO_HEADER_BYTES + length; + header.radio.destVc = destVc; + header.radio.srcVc = myVc; + + //Display the data being sent to the radio + if (DEBUG_PC_TO_RADIO) + { + dumpBuffer((uint8_t *)&header, VC_SERIAL_HEADER_BYTES); + dumpBuffer(buffer, length); + } + + //Send the header + bytesWritten = write(tty, (uint8_t *)&header, VC_SERIAL_HEADER_BYTES); + if (bytesWritten < (int)VC_SERIAL_HEADER_BYTES) + { + perror("ERROR: Write of header to radio failed!"); + return -1; + } + + //Send the message + bytesSent = 0; + while (bytesSent < length) + { + bytesWritten = write(tty, buffer, length); + if (bytesWritten < 0) + { + perror("ERROR: Write of data to radio failed!"); + return bytesWritten; + } + bytesSent += bytesWritten; + } + + //Return the amount of the buffer that was sent + return length; +} + int stdinToRadio() { int bytesRead; int bytesSent; int bytesToSend; - VC_SERIAL_MESSAGE_HEADER * header; + int bytesWritten; int length; int maxfds; int status; @@ -25,8 +118,7 @@ int stdinToRadio() do { //Read the console input data into the local buffer. - header = (VC_SERIAL_MESSAGE_HEADER *)inputBuffer; - bytesRead = read(STDIN, &inputBuffer[VC_SERIAL_HEADER_BYTES], BUFFER_SIZE); + bytesRead = read(STDIN, inputBuffer, BUFFER_SIZE); if (bytesRead < 0) { perror("ERROR: Read from stdin failed!"); @@ -43,27 +135,20 @@ int stdinToRadio() if (bytesToSend > MAX_MESSAGE_SIZE) bytesToSend = MAX_MESSAGE_SIZE; - //Build the virtual circuit serial header - length = VC_RADIO_HEADER_BYTES + bytesToSend; - header->start = START_OF_VC_SERIAL; - header->radio.length = length; - header->radio.destVc = remoteVcAddr; - header->radio.srcVc = (remoteVcAddr == VC_COMMAND) ? PC_COMMAND : myVcAddr; - - //Display the data being sent to the radio -// dumpBuffer((uint8_t *)header, VC_SERIAL_HEADER_BYTES + bytesToSend); - //Send the data - bytesToSend = write(tty, header, length + 1); - if (bytesToSend < 0) + if (remoteVc == VC_COMMAND) + bytesWritten = cmdToRadio(&inputBuffer[bytesSent], bytesToSend); + else + bytesWritten = hostToRadio(remoteVc, &inputBuffer[bytesSent], bytesToSend); + if (bytesWritten < 0) { perror("ERROR: Write to radio failed!"); - status = bytesToSend; + status = bytesWritten; break; } //Account for the bytes written - bytesSent += bytesToSend; + bytesSent += bytesWritten; } } while (0); return status; @@ -185,18 +270,21 @@ int radioToHost() data = &outputBuffer[VC_SERIAL_HEADER_BYTES]; length = header->radio.length - VC_RADIO_HEADER_BYTES; - //Process the message - switch (header->radio.destVc) + //Display the VC header and message + if (DEBUG_RADIO_TO_PC) { - default: - //Display the VC header and message printf("VC Header:\n"); printf(" length: %d\n", header->radio.length); printf(" destVc: %d\n", header->radio.destVc); printf(" srcVc: %d\n", header->radio.srcVc); if (length > 0) dumpBuffer(data, length); + } + //Process the message + switch (header->radio.destVc) + { + default: //Discard this message break; @@ -251,8 +339,8 @@ main ( terminal = argv[1]; //Determine the local VC address - if ((sscanf(argv[2], "%d", &myVcAddr) != 1) - || ((myVcAddr < VC_SERVER) || (myVcAddr >= MAX_VC))) + if ((sscanf(argv[2], "%d", &myVc) != 1) + || ((myVc < VC_SERVER) || (myVc >= MAX_VC))) { fprintf(stderr, "ERROR: Invalid my VC address, please use one of the following:\n"); fprintf(stderr, " Server: 0\n"); @@ -262,11 +350,11 @@ main ( } //Determine the remote VC address - if ((sscanf(argv[3], "%d", &remoteVcAddr) != 1) - || (remoteVcAddr < VC_COMMAND) || (remoteVcAddr >= MAX_VC)) + if ((sscanf(argv[3], "%d", &remoteVc) != 1) + || (remoteVc < VC_COMMAND) || (remoteVc >= MAX_VC)) { fprintf(stderr, "ERROR: Invalid target VC address, please use one of the following:\n"); - if (myVcAddr) + if (myVc) fprintf(stderr, " Server: 0\n"); fprintf(stderr, " Client: 1 - %d\n", MAX_VC - 1); fprintf(stderr, " Loopback: my_VC\n"); @@ -284,8 +372,14 @@ main ( if (maxfds < tty) maxfds = tty; - //Display myVCAddr - printf("myVcAddr: %d\n", myVcAddr); + //Display myVc + printf("myVc: %d\n", myVc); + + //Delay a while to let the radio complete its reset operation + sleep(2); + + //Break the links to this node + cmdToRadio((uint8_t *)LINK_RESET_COMMAND, strlen(LINK_RESET_COMMAND)); //Initialize the fd_sets FD_ZERO(&exceptfds); From 39532840814e000c88531861ad7d3b7a8e07f2d1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 14 Nov 2022 16:30:39 -1000 Subject: [PATCH 138/594] Finish support for net ID mismatch Add counter for net ID mismatch Add ATI27 command to display counter for net ID mismatch Add trigger for net ID mismatch Add cases to handle the net ID mismatch and generate the trigger --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 ++++ .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 24 +++++++++++++++++-- Firmware/LoRaSerial_Firmware/settings.h | 1 + 5 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 5954900f..dec04093 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -203,6 +203,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI24 - Display the VC details"); systemPrintln(" ATI25 - Display the total insufficient buffer count"); systemPrintln(" ATI26 - Display the total number of bad CRC frames"); + systemPrintln(" ATI27 - Display the total number of net ID mismatch frames"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(0, true); @@ -375,6 +376,10 @@ bool commandAT(const char * commandString) systemPrint("Total number of bad CRC frames: "); systemPrintln(badCrc); break; + case ('7'): //ATI27 - Display the total number of net ID mismatch frames + systemPrint("Total number of net ID mismatch frames: "); + systemPrintln(netIdMismatch); + break; } } diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 98394624..c3b5a818 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -363,6 +363,7 @@ uint32_t lostFrames; //Total number of lost TX frames uint32_t linkFailures; //Total number of link failures uint32_t insufficientSpace; //Total number of times the buffer did not have enough space uint32_t badCrc; //Total number of bad CRC frames +uint32_t netIdMismatch; //Total number of mismatched Net ID frames unsigned long lastLinkUpTime = 0; //Mark when link was first established //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 8f98438e..3c1641c6 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -790,6 +790,7 @@ PacketType rcvDatagram() petWDT(); if (settings.debugReceive && settings.printPktData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); + netIdMismatch++; return (DATAGRAM_NETID_MISMATCH); } } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index af4404b7..2a25dbcd 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1,4 +1,4 @@ -#define SAVE_TX_BUFFER() \ +#define SAVE_TX_BUFFER() \ { \ memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); \ rexmtControl = txControl; \ @@ -6,7 +6,7 @@ rexmtFrameSentCount = frameSentCount; \ } -#define RESTORE_TX_BUFFER() \ +#define RESTORE_TX_BUFFER() \ { \ memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ txControl = rexmtControl; \ @@ -571,6 +571,11 @@ void updateRadioState() returnToReceiving(); break; + case DATAGRAM_NETID_MISMATCH: + triggerEvent(TRIGGER_NETID_MISMATCH); + returnToReceiving(); + break; + case DATAGRAM_PING: v2BreakLink(); break; @@ -769,6 +774,11 @@ void updateRadioState() returnToReceiving(); break; + case DATAGRAM_NETID_MISMATCH: + triggerEvent(TRIGGER_NETID_MISMATCH); + returnToReceiving(); + break; + case DATAGRAM_PING: //Break the link v2BreakLink(); @@ -982,6 +992,11 @@ void updateRadioState() returnToReceiving(); break; + case DATAGRAM_NETID_MISMATCH: + triggerEvent(TRIGGER_NETID_MISMATCH); + returnToReceiving(); + break; + case DATAGRAM_ACK_2: case DATAGRAM_DATA: case DATAGRAM_DATA_ACK: @@ -1105,6 +1120,11 @@ void updateRadioState() returnToReceiving(); break; + case DATAGRAM_NETID_MISMATCH: + triggerEvent(TRIGGER_NETID_MISMATCH); + returnToReceiving(); + break; + case DATAGRAM_ACK_1: case DATAGRAM_ACK_2: case DATAGRAM_DATAGRAM: diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 6e0d315a..3ed92c82 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -278,6 +278,7 @@ enum TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, TRIGGER_BAD_PACKET, TRIGGER_CRC_ERROR, + TRIGGER_NETID_MISMATCH, TRIGGER_RTR_2BYTE, TRIGGER_RTR_255BYTE, TRIGGER_TRAINING_CONTROL_PACKET, From e5073cdad63a3fbf40554ebf0e1a452935e18149 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 14 Nov 2022 13:58:55 -0700 Subject: [PATCH 139/594] Adjust timing to improve airspeed 150 --- Firmware/LoRaSerial_Firmware/Radio.ino | 51 ++++++++++++++++-------- Firmware/LoRaSerial_Firmware/RadioV2.ino | 12 +++++- Firmware/LoRaSerial_Firmware/States.ino | 5 ++- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 1503feb4..b095a41b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -84,7 +84,7 @@ void configureRadio() systemPrint("Freq: "); systemPrintln(channels[0], 3); systemPrint("radioBandwidth: "); - systemPrintln(settings.radioBandwidth); + systemPrintln(settings.radioBandwidth, 3); systemPrint("radioSpreadFactor: "); systemPrintln(settings.radioSpreadFactor); systemPrint("radioCodingRate: "); @@ -294,21 +294,22 @@ void generateHopTable() //Use settings that must be identical to have a functioning link. //For example, we do not use coding rate because two radios can communicate with different coding rate values myRandSeed = settings.airSpeed - + settings.netID - + settings.operatingMode - + settings.encryptData - + settings.dataScrambling - + (uint16_t)settings.frequencyMin - + (uint16_t)settings.frequencyMax - + settings.numberOfChannels - + settings.frequencyHop - + settings.maxDwellTime - + (uint16_t)settings.radioBandwidth - + settings.radioSpreadFactor - + settings.verifyRxNetID - + settings.overheadTime - + settings.enableCRC16 - + settings.clientPingRetryInterval; + + settings.netID + + settings.operatingMode + + settings.encryptData + + settings.dataScrambling + + (uint16_t)settings.frequencyMin + + (uint16_t)settings.frequencyMax + + settings.numberOfChannels + + settings.frequencyHop + + settings.maxDwellTime + + (uint16_t)settings.radioBandwidth + + settings.radioSpreadFactor + + settings.verifyRxNetID + + settings.radioProtocolVersion + + settings.overheadTime + + settings.enableCRC16 + + settings.clientPingRetryInterval; if (settings.encryptData == true) { @@ -543,3 +544,21 @@ void hopISR(void) { hop = true; } + +//As we complete linkup, different airspeeds exit at different rates +//We adjust the initial clock setup as needed +int16_t getLinkOffset() +{ + int linkOffset = settings.maxDwellTime; + + if (settings.airSpeed == 150) + { + linkOffset -= 13; + } + + partialTimer = true; + + Serial.print("linkOffset: "); + Serial.println(linkOffset); + return(linkOffset); +} diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 3c1641c6..5c73093b 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1607,9 +1607,14 @@ void retransmitDatagram(VIRTUAL_CIRCUIT * vc) } void startChannelTimer() +{ + startChannelTimer(settings.maxDwellTime); +} + +void startChannelTimer(int16_t startAmount) { channelTimer.disableTimer(); - channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); + channelTimer.setInterval_MS(startAmount, channelTimerHandler); channelTimer.enableTimer(); timerStart = millis(); //ISR normally takes care of this but allow for correct ACK sync before first ISR triggerEvent(TRIGGER_HOP_TIMER_START); @@ -1630,6 +1635,11 @@ void syncChannelTimer() msToNextHopRemote -= ackAirTime; msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; //Can be negative + if (settings.airSpeed == 150) + { + msToNextHopRemote -= 145; + } + int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - timerStart); //Precalculate large/small time amounts diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index e3bea22c..2d773e2e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -442,6 +442,8 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; + startChannelTimer(getLinkOffset()); //We are exiting the link last so adjust our starting Timer + //Bring up the link v2EnterLinkUp(); } @@ -471,6 +473,8 @@ void updateRadioState() { transactionComplete = false; //Reset ISR flag + startChannelTimer(); //We are exiting the link first so do not adjust our starting Timer + //Bring up the link v2EnterLinkUp(); } @@ -2065,7 +2069,6 @@ void v2EnterLinkUp() { //Bring up the link triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); - startChannelTimer(); hopChannel(); //Leave home setHeartbeatLong(); //Start link with long heartbeat From 61623097963c3198d4dcf2b9538cc678e0dd8a26 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 16 Nov 2022 14:33:49 -0700 Subject: [PATCH 140/594] Prevent a rexmit from corrupting a restored buffer --- Firmware/LoRaSerial_Firmware/States.ino | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 2d773e2e..31a21c0f 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -4,14 +4,19 @@ rexmtControl = txControl; \ rexmtLength = txDatagramSize; \ rexmtFrameSentCount = frameSentCount; \ + bufferRestored = false; \ } #define RESTORE_TX_BUFFER() \ { \ - memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ - txControl = rexmtControl; \ - txDatagramSize = rexmtLength; \ - frameSentCount = rexmtFrameSentCount; \ + if(bufferRestored == false) \ + { \ + memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ + txControl = rexmtControl; \ + txDatagramSize = rexmtLength; \ + frameSentCount = rexmtFrameSentCount; \ + bufferRestored = true; \ + } \ } void updateRadioState() @@ -30,6 +35,7 @@ void updateRadioState() static uint8_t rexmtLength; static uint8_t rexmtFrameSentCount; static uint8_t rexmtTxDestVc; + static bool bufferRestored = false; VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; From e1decd5403d12956b6e62b7c011ebc346afd2922 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 16 Nov 2022 14:38:50 -0700 Subject: [PATCH 141/594] Abort transmit if receive is detected --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 91 +++++++----- Firmware/LoRaSerial_Firmware/States.ino | 182 ++++++++++++++--------- Firmware/LoRaSerial_Firmware/Train.ino | 20 ++- Firmware/LoRaSerial_Firmware/settings.h | 1 + 4 files changed, 177 insertions(+), 117 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 5c73093b..d42d597b 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -39,7 +39,7 @@ const uint16_t crc16Table[256] = //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Ping the other radio in the point-to-point configuration -void xmitDatagramP2PTrainingPing() +bool xmitDatagramP2PTrainingPing() { /* endOfTxData ---. @@ -53,11 +53,11 @@ void xmitDatagramP2PTrainingPing() */ txControl.datagramType = DATAGRAM_P2P_TRAINING_PING; - transmitDatagram(); + return (transmitDatagram()); } //Build the parameters packet used for training -void xmitDatagramP2pTrainingParams() +bool xmitDatagramP2pTrainingParams() { Settings params; @@ -81,7 +81,7 @@ void xmitDatagramP2pTrainingParams() */ txControl.datagramType = DATAGRAM_P2P_TRAINING_PARAMS; - transmitDatagram(); + return (transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -141,7 +141,7 @@ void xmitDatagramP2pTrainingParams() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //First packet in the three way handshake to bring up the link -void xmitDatagramP2PPing() +bool xmitDatagramP2PPing() { unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); @@ -159,11 +159,11 @@ void xmitDatagramP2PPing() */ txControl.datagramType = DATAGRAM_PING; - transmitDatagram(); + return (transmitDatagram()); } //Second packet in the three way handshake to bring up the link -void xmitDatagramP2PAck1() +bool xmitDatagramP2PAck1() { unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); @@ -181,11 +181,11 @@ void xmitDatagramP2PAck1() */ txControl.datagramType = DATAGRAM_ACK_1; - transmitDatagram(); + return (transmitDatagram()); } //Last packet in the three way handshake to bring up the link -void xmitDatagramP2PAck2() +bool xmitDatagramP2PAck2() { unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); @@ -203,7 +203,7 @@ void xmitDatagramP2PAck2() */ txControl.datagramType = DATAGRAM_ACK_2; - transmitDatagram(); + return (transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -211,7 +211,7 @@ void xmitDatagramP2PAck2() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Send a command datagram to the remote system -void xmitDatagramP2PCommand() +bool xmitDatagramP2PCommand() { /* endOfTxData ---. @@ -225,11 +225,11 @@ void xmitDatagramP2PCommand() */ txControl.datagramType = DATAGRAM_REMOTE_COMMAND; - transmitDatagram(); + return (transmitDatagram()); } //Send a command response datagram to the remote system -void xmitDatagramP2PCommandResponse() +bool xmitDatagramP2PCommandResponse() { /* endOfTxData ---. @@ -243,11 +243,11 @@ void xmitDatagramP2PCommandResponse() */ txControl.datagramType = DATAGRAM_REMOTE_COMMAND_RESPONSE; - transmitDatagram(); + return (transmitDatagram()); } //Send a data datagram to the remote system -void xmitDatagramP2PData() +bool xmitDatagramP2PData() { /* endOfTxData ---. @@ -261,11 +261,11 @@ void xmitDatagramP2PData() */ txControl.datagramType = DATAGRAM_DATA; - transmitDatagram(); + return (transmitDatagram()); } //Heartbeat packet to keep the link up -void xmitDatagramP2PHeartbeat() +bool xmitDatagramP2PHeartbeat() { unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); @@ -283,11 +283,11 @@ void xmitDatagramP2PHeartbeat() */ txControl.datagramType = DATAGRAM_HEARTBEAT; - transmitDatagram(); + return (transmitDatagram()); } //Create short packet of 2 control bytes - do not expect ack -void xmitDatagramP2PAck() +bool xmitDatagramP2PAck() { int ackLength; @@ -317,7 +317,7 @@ void xmitDatagramP2PAck() */ txControl.datagramType = DATAGRAM_DATA_ACK; - transmitDatagram(); + return(transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -325,7 +325,7 @@ void xmitDatagramP2PAck() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Send a data datagram to the remote system, including sync data -void xmitDatagramMpData() +bool xmitDatagramMpData() { uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); @@ -345,11 +345,11 @@ void xmitDatagramMpData() txControl.datagramType = DATAGRAM_DATA; - transmitDatagram(); + return(transmitDatagram()); } //Heartbeat packet to sync other units in multipoint mode -void xmitDatagramMpHeartbeat() +bool xmitDatagramMpHeartbeat() { uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); @@ -367,13 +367,13 @@ void xmitDatagramMpHeartbeat() */ txControl.datagramType = DATAGRAM_HEARTBEAT; - transmitDatagram(); + return(transmitDatagram()); } //Ack packet sent by server in response the client ping, includes sync data and channel number //During Multipoint scanning, it's possible for the client to get an ack but be 500kHz off //The channel Number ensures that the client gets the next hop correct -void xmitDatagramMpAck() +bool xmitDatagramMpAck() { memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); endOfTxData += sizeof(channelNumber); @@ -395,11 +395,11 @@ void xmitDatagramMpAck() */ txControl.datagramType = DATAGRAM_ACK_1; - transmitDatagram(); + return(transmitDatagram()); } //Ping packet sent during scanning -void xmitDatagramMpPing() +bool xmitDatagramMpPing() { /* endOfTxData ---. @@ -413,7 +413,7 @@ void xmitDatagramMpPing() */ txControl.datagramType = DATAGRAM_PING; - transmitDatagram(); + return(transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -421,7 +421,7 @@ void xmitDatagramMpPing() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Build the client ping packet used for training -void xmitDatagramMpTrainingPing() +bool xmitDatagramMpTrainingPing() { //Add the source (server) ID memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); @@ -439,11 +439,11 @@ void xmitDatagramMpTrainingPing() */ txControl.datagramType = DATAGRAM_TRAINING_PING; - transmitDatagram(); + return(transmitDatagram()); } //Build the client ACK packet used for training -void xmitDatagramMpTrainingAck(uint8_t * serverID) +bool xmitDatagramMpTrainingAck(uint8_t * serverID) { //Add the destination (server) ID memcpy(endOfTxData, serverID, UNIQUE_ID_BYTES); @@ -465,7 +465,7 @@ void xmitDatagramMpTrainingAck(uint8_t * serverID) */ txControl.datagramType = DATAGRAM_TRAINING_ACK; - transmitDatagram(); + return(transmitDatagram()); } void updateRadioParameters(uint8_t * rxData) @@ -546,7 +546,7 @@ void updateRadioParameters(uint8_t * rxData) //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Build the server parameters packet used for training -void xmitDatagramMpRadioParameters(const uint8_t * clientID) +bool xmitDatagramMpRadioParameters(const uint8_t * clientID) { Settings params; @@ -579,14 +579,14 @@ void xmitDatagramMpRadioParameters(const uint8_t * clientID) */ txControl.datagramType = DATAGRAM_TRAINING_PARAMS; - transmitDatagram(); + return(transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Virtual Circuit frames //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -void xmitVcHeartbeat(int8_t addr, uint8_t * id) +bool xmitVcHeartbeat(int8_t addr, uint8_t * id) { uint8_t * txData; @@ -616,7 +616,6 @@ void xmitVcHeartbeat(int8_t addr, uint8_t * id) txControl.datagramType = DATAGRAM_VC_HEARTBEAT; txControl.ackNumber = 0; - transmitDatagram(); //Determine the time that it took to pass this frame to the radio //This time is used to adjust the time offset @@ -624,6 +623,8 @@ void xmitVcHeartbeat(int8_t addr, uint8_t * id) //Select a random for the next heartbeat setHeartbeatLong(); //Those who send a heartbeat or data have long time before next heartbeat. Those who send ACKs, have short wait to next heartbeat. + + return(transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1192,7 +1193,8 @@ PacketType rcvDatagram() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Push the outgoing packet to the air -void transmitDatagram() +//Returns false if we could not start tranmission due to packet received or RX in process +bool transmitDatagram() { uint8_t control; uint8_t * header; @@ -1495,7 +1497,7 @@ void transmitDatagram() //Transmit this datagram frameSentCount = 0; //This is the first time this frame is being sent - retransmitDatagram(vc); + return (retransmitDatagram(vc)); } //Print the control byte value @@ -1528,7 +1530,8 @@ void printControl(uint8_t value) } //The previous transmission was not received, retransmit the datagram -void retransmitDatagram(VIRTUAL_CIRCUIT * vc) +//Returns false if we could not start tranmission due to packet received or RX in process +bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) { /* +----------+----------+------------+--- ... ---+----------+ @@ -1564,7 +1567,15 @@ void retransmitDatagram(VIRTUAL_CIRCUIT * vc) //Transmit this frame if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); + + if (receiveInProcess() == true || transactionComplete == true) + { + triggerEvent(TRIGGER_TRANSMIT_CANCELED); + return (false); //Do not start transmit while RX is or has occured + } + int state = radio.startTransmit(outgoingPacket, txDatagramSize); + if (state == RADIOLIB_ERR_NONE) { frameSentCount++; @@ -1604,6 +1615,8 @@ void retransmitDatagram(VIRTUAL_CIRCUIT * vc) //BLink the RX LED if (settings.alternateLedUsage) digitalWrite(ALT_LED_TX_DATA, LED_ON); + + return(true); //Tranmission has started } void startChannelTimer() diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 31a21c0f..3c031d7e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -326,8 +326,8 @@ void updateRadioState() //Acknowledge the PING triggerEvent(TRIGGER_SEND_ACK1); - xmitDatagramP2PAck1(); - changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); + if (xmitDatagramP2PAck1() == true) + changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); } } @@ -336,8 +336,8 @@ void updateRadioState() { //Transmit the PING triggerEvent(TRIGGER_HANDSHAKE_SEND_PING); - xmitDatagramP2PPing(); - changeState(RADIO_P2P_WAIT_TX_PING_DONE); + if (xmitDatagramP2PPing() == true) + changeState(RADIO_P2P_WAIT_TX_PING_DONE); } break; @@ -373,8 +373,8 @@ void updateRadioState() //Acknowledge the PING triggerEvent(TRIGGER_SEND_ACK1); - xmitDatagramP2PAck1(); - changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); + if (xmitDatagramP2PAck1() == true) + changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); } else if (packetType != DATAGRAM_ACK_1) returnToReceiving(); @@ -392,8 +392,8 @@ void updateRadioState() //Acknowledge the ACK1 triggerEvent(TRIGGER_SEND_ACK2); - xmitDatagramP2PAck2(); - changeState(RADIO_P2P_WAIT_TX_ACK_2_DONE); + if (xmitDatagramP2PAck2() == true) + changeState(RADIO_P2P_WAIT_TX_ACK_2_DONE); } } else @@ -595,9 +595,11 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); - xmitDatagramP2PAck(); //Transmit ACK - - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + if (xmitDatagramP2PAck() == true) //Transmit ACK + { + setHeartbeatShort(); //We ack'd the packet (again) so be responsible for sending the next heartbeat + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + } break; case DATAGRAM_HEARTBEAT: @@ -615,11 +617,12 @@ void updateRadioState() updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; - setHeartbeatShort(); //We ack'd this heartbeat so be responsible for sending the next heartbeat - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); - xmitDatagramP2PAck(); //Transmit ACK - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + if (xmitDatagramP2PAck() == true) //Transmit ACK + { + setHeartbeatShort(); //We ack'd this heartbeat so be responsible for sending the next heartbeat + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + } break; case DATAGRAM_DATA: @@ -631,11 +634,12 @@ void updateRadioState() updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; - setHeartbeatShort(); //We ack'd this data, so be responsible for sending the next heartbeat - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); - xmitDatagramP2PAck(); //Transmit ACK - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + if (xmitDatagramP2PAck() == true) //Transmit ACK + { + setHeartbeatShort(); //We ack'd this data, so be responsible for sending the next heartbeat + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + } break; case DATAGRAM_REMOTE_COMMAND: @@ -656,9 +660,13 @@ void updateRadioState() updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); - xmitDatagramP2PAck(); //Transmit ACK - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + if (xmitDatagramP2PAck() == true) //Transmit ACK + { + setHeartbeatShort(); //We ack'd the packet so be responsible for sending the next heartbeat + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + } break; case DATAGRAM_REMOTE_COMMAND_RESPONSE: @@ -670,8 +678,11 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE); - xmitDatagramP2PAck(); //Transmit ACK - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + if (xmitDatagramP2PAck() == true) //Transmit ACK + { + setHeartbeatShort(); //We ack'd the packet so be responsible for sending the next heartbeat + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); + } break; } } @@ -685,9 +696,13 @@ void updateRadioState() if (availableRadioTXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) { triggerEvent(TRIGGER_LINK_DATA_XMIT); - xmitDatagramP2PData(); - transmitTimer = datagramTimer; - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + + if (xmitDatagramP2PData() == true) + { + setHeartbeatLong(); //We're sending data, so don't be the first to send next heartbeat + transmitTimer = datagramTimer; + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } } else if (availableTXCommandBytes()) //If we have command bytes to send out { @@ -698,20 +713,31 @@ void updateRadioState() //We now have the commandTXBuffer loaded if (remoteCommandResponse) - xmitDatagramP2PCommandResponse(); + { + if (xmitDatagramP2PCommandResponse() == true) + { + setHeartbeatLong(); //We're sending command, so don't be the first to send next heartbeat + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } + } else - xmitDatagramP2PCommand(); + { + if (xmitDatagramP2PCommand() == true) + { + setHeartbeatLong(); //We're sending command, so don't be the first to send next heartbeat + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } + } - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } else if (heartbeatTimeout) { triggerEvent(TRIGGER_HEARTBEAT); - if (receiveInProcess() == false && transactionComplete == false) //Avoid race condition - { - xmitDatagramP2PHeartbeat(); + if (xmitDatagramP2PHeartbeat() == true) + { setHeartbeatLong(); //We're sending a heartbeat, so don't be the first to send next heartbeat + transmitTimer = datagramTimer; //Wait for heartbeat to transmit @@ -826,15 +852,18 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; //An ACK was expected for a previous transmission that must have been - //lost. Save the current transmit buffer for later retransmission - //and ACK the heartbeat. Later perform the retransmission for the + //lost. Save the current transmit buffer for later retransmission + //and ACK the heartbeat. Later perform the retransmission for the //datagram that was lost. petWDT(); SAVE_TX_BUFFER(); triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); - xmitDatagramP2PAck(); //Transmit ACK - changeState(RADIO_P2P_LINK_UP_HB_ACK_REXMT); + if (xmitDatagramP2PAck() == true) //Transmit ACK + { + setHeartbeatShort(); //We ack'd the packet so be responsible for sending the next heartbeat + changeState(RADIO_P2P_LINK_UP_HB_ACK_REXMT); + } break; } @@ -911,16 +940,16 @@ void updateRadioState() hopChannel(); //An ACK was expected for a previous transmission that must have been - //lost. A heartbeat was received instead which was ACKed. Once the ACK + //lost. A heartbeat was received instead which was ACKed. Once the ACK //completes transmission, retransmit the previously lost datagram. if (transactionComplete) { transactionComplete = false; //Reset ISR flag - //Retransmit the packet if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { - RESTORE_TX_BUFFER(); + + RESTORE_TX_BUFFER(); //This should only be called once otherwise corruption occurs if (settings.debugDatagrams) { systemPrintTimestamp(); @@ -946,9 +975,15 @@ void updateRadioState() break; } } - retransmitDatagram(NULL); - lostFrames++; - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + + triggerEvent(TRIGGER_LINK_HB_ACK_REXMIT); + + if (retransmitDatagram(NULL) == true) + { + setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat + lostFrames++; + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } } else //Failed to reach the other system, break the link @@ -1067,8 +1102,8 @@ void updateRadioState() } //Send ping - xmitDatagramMpPing(); - changeState(RADIO_MP_WAIT_TX_PING_DONE); + if (xmitDatagramMpPing() == true) + changeState(RADIO_MP_WAIT_TX_PING_DONE); } } @@ -1148,8 +1183,8 @@ void updateRadioState() if (settings.multipointServer == true) { //Ack their ping with sync data - xmitDatagramMpAck(); - changeState(RADIO_MP_WAIT_TX_ACK_DONE); + if (xmitDatagramMpAck() == true) + changeState(RADIO_MP_WAIT_TX_ACK_DONE); } else { @@ -1205,9 +1240,11 @@ void updateRadioState() if (availableRadioTXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) { triggerEvent(TRIGGER_MP_DATA_PACKET); - xmitDatagramMpData(); - setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer - changeState(RADIO_MP_WAIT_TX_DONE); + if (xmitDatagramMpData() == true) + { + setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer + changeState(RADIO_MP_WAIT_TX_DONE); + } } //Only the server transmits heartbeats @@ -1216,9 +1253,11 @@ void updateRadioState() if (heartbeatTimeout) { triggerEvent(TRIGGER_HEARTBEAT); - xmitDatagramMpHeartbeat(); - setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer - changeState(RADIO_MP_WAIT_TX_DONE); //Wait for heartbeat to transmit + if (xmitDatagramMpHeartbeat() == true) + { + setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer + changeState(RADIO_MP_WAIT_TX_DONE); //Wait for heartbeat to transmit + } } } @@ -1354,8 +1393,8 @@ void updateRadioState() updateRadioParameters(&rxData[UNIQUE_ID_BYTES * 2]); //Acknowledge the radio parameters - xmitDatagramMpTrainingAck(&rxData[UNIQUE_ID_BYTES]); - changeState(RADIO_MP_WAIT_TX_PARAM_ACK_DONE); + if (xmitDatagramMpTrainingAck(&rxData[UNIQUE_ID_BYTES]) == true) + changeState(RADIO_MP_WAIT_TX_PARAM_ACK_DONE); break; } } @@ -1372,7 +1411,9 @@ void updateRadioState() //Check for a receive timeout else if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) + { xmitDatagramMpTrainingPing(); + } break; case RADIO_MP_WAIT_TX_PARAM_ACK_DONE: @@ -1441,10 +1482,9 @@ void updateRadioState() case DATAGRAM_TRAINING_PING: //Save the client ID memcpy(trainingPartnerID, rxData, UNIQUE_ID_BYTES); - xmitDatagramMpRadioParameters(trainingPartnerID); - //Wait for the transmit to complete - changeState(RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE); + if (xmitDatagramMpRadioParameters(trainingPartnerID) == true) + changeState(RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE); break; case DATAGRAM_TRAINING_ACK: @@ -1575,8 +1615,8 @@ void updateRadioState() vcHeader->destVc = rxSrcVc; vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; - xmitDatagramP2PAck(); - changeState(RADIO_VC_WAIT_TX_DONE); + if (xmitDatagramP2PAck() == true) + changeState(RADIO_VC_WAIT_TX_DONE); break; case DATAGRAM_DATA_ACK: @@ -1600,15 +1640,17 @@ void updateRadioState() { //Transmit the packet triggerEvent(TRIGGER_VC_TX_DATA); - xmitDatagramP2PData(); - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; - - //Save the message for retransmission - SAVE_TX_BUFFER(); - rexmtTxDestVc = txDestVc; - changeState(RADIO_VC_WAIT_TX_DONE_ACK); + if (xmitDatagramP2PData() == true) + { + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; + + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + changeState(RADIO_VC_WAIT_TX_DONE_ACK); + } } //Check for link timeout @@ -1678,8 +1720,8 @@ void updateRadioState() vcHeader->destVc = rxSrcVc; vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; - xmitDatagramP2PAck(); - changeState(RADIO_VC_WAIT_TX_DONE_ACK); + if (xmitDatagramP2PAck() == true) + changeState(RADIO_VC_WAIT_TX_DONE_ACK); break; case DATAGRAM_DATA_ACK: diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 8593ec7f..d52fc9bf 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -94,11 +94,13 @@ void beginTrainingPointToPoint(bool defaultTraining) commonTrainingInitialization(); //Transmit general ping packet to see if anyone else is sitting on the training channel - xmitDatagramP2PTrainingPing(); - trainingTimer = millis(); + if (xmitDatagramP2PTrainingPing() == true) + { + trainingTimer = millis(); - //Set the next state - changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); + //Set the next state + changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); + } } void endPointToPointTraining(bool saveParams) @@ -120,11 +122,13 @@ void beginTrainingClient() commonTrainingInitialization(); //Transmit client ping to the training server - xmitDatagramMpTrainingPing(); - trainingTimer = millis(); + if (xmitDatagramMpTrainingPing() == true) + { + trainingTimer = millis(); - //Set the next state - changeState(RADIO_MP_WAIT_TX_TRAINING_PING_DONE); + //Set the next state + changeState(RADIO_MP_WAIT_TX_TRAINING_PING_DONE); + } } void beginTrainingServer() diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 3ed92c82..54c89e4f 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -261,6 +261,7 @@ enum TRIGGER_MP_SCAN, TRIGGER_MP_DATA_PACKET, TRIGGER_MP_PACKET_RECEIVED, + TRIGGER_TRANSMIT_CANCELED, TRIGGER_HANDSHAKE_ACK1_TIMEOUT, TRIGGER_HANDSHAKE_SEND_PING, TRIGGER_HANDSHAKE_SEND_PING_COMPLETE, From a0bb81dbe5ab65246a3c089b7fbfd9328c587a8a Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 16 Nov 2022 14:39:53 -0700 Subject: [PATCH 142/594] Correct heartbeat timeouts as we link up --- Firmware/LoRaSerial_Firmware/States.ino | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3c031d7e..88a0b7a7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -450,6 +450,8 @@ void updateRadioState() startChannelTimer(getLinkOffset()); //We are exiting the link last so adjust our starting Timer + setHeartbeatLong(); //We sent ACK1 and they sent ACK2, so don't be the first to send heartbeat + //Bring up the link v2EnterLinkUp(); } @@ -481,6 +483,8 @@ void updateRadioState() startChannelTimer(); //We are exiting the link first so do not adjust our starting Timer + setHeartbeatShort(); //We sent the last ack so be responsible for sending the next heartbeat + //Bring up the link v2EnterLinkUp(); } From 23d1e4e0997794bb7a4c29e8e3ac8abd166bd544 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 16 Nov 2022 14:41:17 -0700 Subject: [PATCH 143/594] Increase heartbeat timeout by frame transmit time This allows heartbeat lengths to be correctly started once the transmission is complete. --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d42d597b..ad86f8c8 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1737,12 +1737,20 @@ void setHeartbeatShort() { heartbeatTimer = millis(); heartbeatRandomTime = random(settings.heartbeatTimeout * 2 / 10, settings.heartbeatTimeout / 2); //20-50% + + //Slow datarates can have significant ack transmission times + //Add the amount of time it takes to send an ack + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime; } void setHeartbeatLong() { heartbeatTimer = millis(); heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% + + //Slow datarates can have significant ack transmission times + //Add the amount of time it takes to send an ack + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime; } //Only the server sends heartbeats in multipoint mode @@ -1751,4 +1759,8 @@ void setHeartbeatMultipoint() { heartbeatTimer = millis(); heartbeatRandomTime = settings.heartbeatTimeout; + + //Slow datarates can have significant ack transmission times + //Add the amount of time it takes to send an ack + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime; } From fa5ff4ad57b50ab5ae0c1950b4d02527a1e8df3d Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 16 Nov 2022 14:43:50 -0700 Subject: [PATCH 144/594] Throttle retransmits Adds a global called retransmitTimeout. This var is calculated at transmit and is atleast ackAirTime, but may be as large as frameTime+ackAirTime. For each failed transmission, retransmit is delayed by this random amount * failed attempts. --- .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 61 ++++++++++--------- Firmware/LoRaSerial_Firmware/settings.h | 1 + 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index db0ae84b..cae3fe4b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -458,6 +458,8 @@ uint8_t serialOperatingMode; unsigned int multipointChannelLoops = 0; //Count the number of times Multipoint scanning has traversed the table unsigned int multipointAttempts = 0; //Throttle back scanning when a server is not detected +unsigned long retransmitTimeout = 0; //Throttle back re-transmits + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index ad86f8c8..a47ece66 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1611,6 +1611,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) } datagramTimer = millis(); //Move timestamp even if error + retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime); //Wait this number of ms between retransmits. Increases with each re-transmit. //BLink the RX LED if (settings.alternateLedUsage) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 88a0b7a7..5a4fecd3 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -886,38 +886,44 @@ void updateRadioState() //Retransmit the packet if ((!settings.maxResends) || (frameSentCount < settings.maxResends)) { - triggerEvent(TRIGGER_LINK_RETRANSMIT); - if (settings.debugDatagrams) + //Throttle back retransmits based on the number of retransmits we've attempted + //retransmitTimeout is a random number, set when the first datagram is sent + if (millis() - datagramTimer > (frameSentCount * retransmitTimeout)) { - systemPrintTimestamp(); - systemPrint("TX: Retransmit "); - systemPrint(frameSentCount); - systemPrint(", "); - systemPrint(v2DatagramType[txControl.datagramType]); - switch (txControl.datagramType) + triggerEvent(TRIGGER_LINK_RETRANSMIT); + if (settings.debugDatagrams) { - default: - systemPrintln(); - break; + systemPrintTimestamp(); + systemPrint("TX: Retransmit "); + systemPrint(frameSentCount); + systemPrint(", "); + systemPrint(v2DatagramType[txControl.datagramType]); + switch (txControl.datagramType) + { + default: + systemPrintln(); + break; + + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + case DATAGRAM_HEARTBEAT: + systemPrint(" (ACK #"); + systemPrint(txControl.ackNumber); + systemPrint(")"); + systemPrintln(); + break; + } + } - case DATAGRAM_DATA: - case DATAGRAM_DATA_ACK: - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - systemPrint(" (ACK #"); - systemPrint(txControl.ackNumber); - systemPrint(")"); - systemPrintln(); - break; + if (retransmitDatagram(NULL) == true) + { + setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat + lostFrames++; + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } } - if (receiveInProcess() == false) - { - retransmitDatagram(NULL); - lostFrames++; - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - } } else { @@ -2122,7 +2128,6 @@ void v2EnterLinkUp() //Bring up the link triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); hopChannel(); //Leave home - setHeartbeatLong(); //Start link with long heartbeat //Synchronize the ACK numbers rmtTxAckNumber = 0; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 54c89e4f..7a68b14c 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -274,6 +274,7 @@ enum TRIGGER_HANDSHAKE_ACK2_TIMEOUT, TRIGGER_RECEIVE_IN_PROCESS_START, TRIGGER_RECEIVE_IN_PROCESS_END, + TRIGGER_LINK_HB_ACK_REXMIT, TRIGGER_UNKNOWN_PACKET, TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND, TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, From c5cfb7be4d3fc74fb9197150d8c9496fff0f5e74 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 16 Nov 2022 14:47:12 -0700 Subject: [PATCH 145/594] Add debug setting for clock sync Add edge case for slow air speeds --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 24 ++++++++++++++++++++---- Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index a47ece66..21e248df 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1644,10 +1644,10 @@ void stopChannelTimer() //adjust our own channelTimer interrupt to be synchronized with the remote unit void syncChannelTimer() { - int16_t msToNextHopRemote; + int16_t msToNextHopRemote; //Can be negative memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); msToNextHopRemote -= ackAirTime; - msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; //Can be negative + msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; if (settings.airSpeed == 150) { @@ -1668,8 +1668,10 @@ void syncChannelTimer() //msToNextHopLocal is small and msToNextHopRemote is negative //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in negative then the remote has hopped + //msToNextHopLocal is small and msToNextHopRemote is negative (and small) + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in negative (and small) then the remote has hopped //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) - if (msToNextHopLocal < smallAmount && msToNextHopRemote <= 0) + if (msToNextHopLocal < smallAmount && (msToNextHopRemote <= 0 && msToNextHopRemote >= (smallAmount * -1))) { hopChannel(); msToNextHop = msToNextHopRemote + settings.maxDwellTime; @@ -1685,7 +1687,7 @@ void syncChannelTimer() } //msToNextHopLocal is small and msToNextHopRemote is large - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in large then the remote has hopped + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in large then the remote has hopped recently //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote) else if (msToNextHopLocal < smallAmount && msToNextHopRemote > largeAmount) { @@ -1718,6 +1720,20 @@ void syncChannelTimer() msToNextHop = msToNextHopRemote; } + //Insure against negative times + while (msToNextHop < 0) msToNextHop += settings.maxDwellTime; + + if (settings.debugSync) + { + systemPrint("msToNextHopRemote: "); + systemPrint(msToNextHopRemote); + systemPrint(" msToNextHopLocal: "); + systemPrint(msToNextHopLocal); + systemPrint(" msToNextHop: "); + systemPrint(msToNextHop); + systemPrintln(); + } + partialTimer = true; channelTimer.disableTimer(); channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 7a68b14c..77aab17f 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -416,6 +416,7 @@ typedef struct struct_settings { uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training bool multipointServer = false; //Only one radio can be the server in multipoint mode bool debugSerial = false; //Debug the serial input + bool debugSync = false; //Print clock sync processing //Add new parameters immediately before this line //-- Add commands to set the parameters From 4e2e1e7d6378f037931bdd0de8a4dccfee42dc38 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 17 Nov 2022 14:38:55 -0700 Subject: [PATCH 146/594] Add getReceiveCompletionOffset for different airSpeeds --- Firmware/LoRaSerial_Firmware/Begin.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 15 +---- Firmware/LoRaSerial_Firmware/RadioV2.ino | 59 ++++++++++++------ Firmware/LoRaSerial_Firmware/States.ino | 78 ++++++++++++++++++++---- Firmware/LoRaSerial_Firmware/System.ino | 32 +++++++++- 5 files changed, 141 insertions(+), 44 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index a8d8135f..5b6cbd18 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -23,6 +23,7 @@ void beginBoard() digitalWrite(pin_rxLED, LOW); delay(50); } + } void beginLoRa() diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index b095a41b..01b5f809 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -547,18 +547,9 @@ void hopISR(void) //As we complete linkup, different airspeeds exit at different rates //We adjust the initial clock setup as needed -int16_t getLinkOffset() +int16_t getLinkupOffset() { - int linkOffset = settings.maxDwellTime; + partialTimer = true; //Mark timer so that it runs only once with less than dwell time - if (settings.airSpeed == 150) - { - linkOffset -= 13; - } - - partialTimer = true; - - Serial.print("linkOffset: "); - Serial.println(linkOffset); - return(linkOffset); + return (settings.maxDwellTime - getReceiveCompletionOffset()); //Reduce the default window by the offset } diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 21e248df..5921e2f5 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -317,7 +317,7 @@ bool xmitDatagramP2PAck() */ txControl.datagramType = DATAGRAM_DATA_ACK; - return(transmitDatagram()); + return (transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -345,7 +345,7 @@ bool xmitDatagramMpData() txControl.datagramType = DATAGRAM_DATA; - return(transmitDatagram()); + return (transmitDatagram()); } //Heartbeat packet to sync other units in multipoint mode @@ -367,7 +367,7 @@ bool xmitDatagramMpHeartbeat() */ txControl.datagramType = DATAGRAM_HEARTBEAT; - return(transmitDatagram()); + return (transmitDatagram()); } //Ack packet sent by server in response the client ping, includes sync data and channel number @@ -395,7 +395,7 @@ bool xmitDatagramMpAck() */ txControl.datagramType = DATAGRAM_ACK_1; - return(transmitDatagram()); + return (transmitDatagram()); } //Ping packet sent during scanning @@ -413,7 +413,7 @@ bool xmitDatagramMpPing() */ txControl.datagramType = DATAGRAM_PING; - return(transmitDatagram()); + return (transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -439,7 +439,7 @@ bool xmitDatagramMpTrainingPing() */ txControl.datagramType = DATAGRAM_TRAINING_PING; - return(transmitDatagram()); + return (transmitDatagram()); } //Build the client ACK packet used for training @@ -465,7 +465,7 @@ bool xmitDatagramMpTrainingAck(uint8_t * serverID) */ txControl.datagramType = DATAGRAM_TRAINING_ACK; - return(transmitDatagram()); + return (transmitDatagram()); } void updateRadioParameters(uint8_t * rxData) @@ -579,7 +579,7 @@ bool xmitDatagramMpRadioParameters(const uint8_t * clientID) */ txControl.datagramType = DATAGRAM_TRAINING_PARAMS; - return(transmitDatagram()); + return (transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -624,7 +624,7 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) //Select a random for the next heartbeat setHeartbeatLong(); //Those who send a heartbeat or data have long time before next heartbeat. Those who send ACKs, have short wait to next heartbeat. - return(transmitDatagram()); + return (transmitDatagram()); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1617,7 +1617,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) if (settings.alternateLedUsage) digitalWrite(ALT_LED_TX_DATA, LED_ON); - return(true); //Tranmission has started + return (true); //Tranmission has started } void startChannelTimer() @@ -1649,11 +1649,23 @@ void syncChannelTimer() msToNextHopRemote -= ackAirTime; msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; - if (settings.airSpeed == 150) + //Different airspeeds complete the transmitComplete ISR at different rates + //We adjust the clock setup as needed + switch (settings.airSpeed) { - msToNextHopRemote -= 145; + default: + break; + case (40): + msToNextHopRemote -= getReceiveCompletionOffset(); + break; + case (150): + msToNextHopRemote -= 145; + break; } + //Calculate the remote's absolute distance to its next hop + //A remote hop may be very negative for + int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - timerStart); //Precalculate large/small time amounts @@ -1666,8 +1678,6 @@ void syncChannelTimer() //Below are the edge cases that occur when a hop occurs near ACK reception - //msToNextHopLocal is small and msToNextHopRemote is negative - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in negative then the remote has hopped //msToNextHopLocal is small and msToNextHopRemote is negative (and small) //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in negative (and small) then the remote has hopped //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) @@ -1718,10 +1728,21 @@ void syncChannelTimer() else if (msToNextHopLocal < smallAmount && msToNextHopRemote < smallAmount) { msToNextHop = msToNextHopRemote; + + //If we have a negative remote hop time that is larger than a dwell time then the remote has hopped again + //This is seen at lower air speeds + //Hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + if (msToNextHop < (settings.maxDwellTime * -1)) //-402 < -400 + { + hopChannel(); + msToNextHop += settings.maxDwellTime; + resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. + } } - //Insure against negative times - while (msToNextHop < 0) msToNextHop += settings.maxDwellTime; + //Insure against negative timer values + while (msToNextHop < 0) + msToNextHop += settings.maxDwellTime; if (settings.debugSync) { @@ -1757,7 +1778,7 @@ void setHeartbeatShort() //Slow datarates can have significant ack transmission times //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime; + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); } void setHeartbeatLong() @@ -1767,7 +1788,7 @@ void setHeartbeatLong() //Slow datarates can have significant ack transmission times //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime; + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); } //Only the server sends heartbeats in multipoint mode @@ -1779,5 +1800,5 @@ void setHeartbeatMultipoint() //Slow datarates can have significant ack transmission times //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime; + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 5a4fecd3..9ef063aa 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -398,7 +398,7 @@ void updateRadioState() } else { - if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime)) + if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { if (settings.debugDatagrams) { @@ -448,7 +448,7 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - startChannelTimer(getLinkOffset()); //We are exiting the link last so adjust our starting Timer + startChannelTimer(getLinkupOffset()); //We are exiting the link last so adjust our starting Timer setHeartbeatLong(); //We sent ACK1 and they sent ACK2, so don't be the first to send heartbeat @@ -458,7 +458,7 @@ void updateRadioState() } else { - if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime)) + if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { if (settings.debugDatagrams) { @@ -570,6 +570,13 @@ void updateRadioState() { default: triggerEvent(TRIGGER_UNKNOWN_PACKET); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("LinkUp: Unhandled packet type "); + systemPrint(v2DatagramType[packetType]); + systemPrintln(); + } returnToReceiving(); break; @@ -799,6 +806,13 @@ void updateRadioState() { default: triggerEvent(TRIGGER_UNKNOWN_PACKET); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("RX: Unhandled packet type "); + systemPrint(v2DatagramType[packetType]); + systemPrintln(); + } returnToReceiving(); break; @@ -823,6 +837,7 @@ void updateRadioState() break; case DATAGRAM_DATA_ACK: + //The datagram we are expecting syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock //Stop the transmit timer @@ -870,19 +885,38 @@ void updateRadioState() } break; + + case DATAGRAM_DATA: + //Received data while waiting for ack. + printPacketQuality(); + + //Place the data in the serial output buffer + serialBufferOutput(rxData, rxDataBytes); + + updateRSSI(); //Adjust LEDs to RSSI level + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + //An ACK was expected for a previous transmission that must have been + //lost. Save the current transmit buffer for later retransmission + //and ACK the data. Later perform the retransmission for the + //datagram that was lost. + petWDT(); + SAVE_TX_BUFFER(); + + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); + if (xmitDatagramP2PAck() == true) //Transmit ACK + { + setHeartbeatShort(); //We ack'd this data, so be responsible for sending the next heartbeat + changeState(RADIO_P2P_LINK_UP_HB_ACK_REXMT); + } + break; } } //Check for ACK timeout, set at end of transmit, measures ACK timeout else if ((receiveInProcess() == false) - && ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime))) + && ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset()))) { - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrintln("RX: ACK Timeout"); - } - //Retransmit the packet if ((!settings.maxResends) || (frameSentCount < settings.maxResends)) { @@ -890,6 +924,12 @@ void updateRadioState() //retransmitTimeout is a random number, set when the first datagram is sent if (millis() - datagramTimer > (frameSentCount * retransmitTimeout)) { + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrintln("RX: ACK Timeout"); + } + triggerEvent(TRIGGER_LINK_RETRANSMIT); if (settings.debugDatagrams) { @@ -1030,6 +1070,13 @@ void updateRadioState() { default: triggerEvent(TRIGGER_UNKNOWN_PACKET); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Scan: Unhandled packet type "); + systemPrint(v2DatagramType[packetType]); + systemPrintln(); + } returnToReceiving(); break; @@ -1084,7 +1131,7 @@ void updateRadioState() else if (receiveInProcess() == false) { //Check for a receive timeout - if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime)) + if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { if (settings.debugDatagrams) { @@ -1158,6 +1205,13 @@ void updateRadioState() { default: triggerEvent(TRIGGER_UNKNOWN_PACKET); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("MP Standby: Unhandled packet type "); + systemPrint(v2DatagramType[packetType]); + systemPrintln(); + } returnToReceiving(); break; @@ -1752,7 +1806,7 @@ void updateRadioState() } //Check for retransmit needed - else if (vcAckTimer && ((currentMillis - vcAckTimer) >= (frameAirTime + ackAirTime + settings.overheadTime))) + else if (vcAckTimer && ((currentMillis - vcAckTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset()))) { //Determine if another retransmit is allowed txDestVc = rexmtTxDestVc; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index e0ecdf78..a759d574 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -532,7 +532,7 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, void updateRSSI() { //Calculate the average RSSI if possible - if(hopCount > 0) + if (hopCount > 0) { rssi /= hopCount; hopCount = 0; @@ -749,3 +749,33 @@ void triggerFrequency(uint16_t frequency) delayMicroseconds(frequency); digitalWrite(pin_trigger, HIGH); } + +//The difference between when a transmitter is finished and when the receiver is finished +//is different between airSpeeds and affects things like ACK timeouts at lower airSpeeds +//Offsets were found using a logic analyzer but it looks like (1 * Tsym) or calcSymbolTime() +int16_t getReceiveCompletionOffset() +{ + switch (settings.airSpeed) + { + default: + break; + case (40): + return(26); //Tsym: 32. Measured: 26 ms between a TX complete and the RX complete + break; + case (150): + return(12); //Tsym: 16 + break; + case (400): + return(0); //Tsym: 8 + break; + case (1200): + return(0); //Tsym: 4 + break; + case (2400): + return(0); //Tsym: 2 + break; + case (4800): + return(0); //Tsym: 1 + break; + } +} From ef39b7b55a956e05a2c97a13e105452c04bf7b51 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 17 Nov 2022 14:46:45 -0700 Subject: [PATCH 147/594] Set RSSI LEDs on linkup/down --- Firmware/LoRaSerial_Firmware/States.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9ef063aa..d8b4c9d9 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -71,6 +71,7 @@ void updateRadioState() //Initialize the radio rssi = -200; + setRSSI(0b0000); //Turn off LEDs radioSeed = radio.randomByte(); //Puts radio into standy-by state randomSeed(radioSeed); if ((settings.debug == true) || (settings.debugRadio == true)) @@ -2183,6 +2184,8 @@ void v2EnterLinkUp() triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); hopChannel(); //Leave home + updateRSSI(); + //Synchronize the ACK numbers rmtTxAckNumber = 0; rxAckNumber = 0; From 714ba3cdddf616c703e3659f59684f770c29c7b6 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 17 Nov 2022 15:14:41 -0700 Subject: [PATCH 148/594] Add offsets for other airspeeds --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 9 +++++++++ Firmware/LoRaSerial_Firmware/System.ino | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 5921e2f5..75ed323a 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1661,6 +1661,15 @@ void syncChannelTimer() case (150): msToNextHopRemote -= 145; break; + case (400): + msToNextHopRemote -= getReceiveCompletionOffset(); + break; + case (1200): + msToNextHopRemote -= getReceiveCompletionOffset(); + break; + case (2400): + msToNextHopRemote -= getReceiveCompletionOffset(); + break; } //Calculate the remote's absolute distance to its next hop diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index a759d574..538fdae1 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -766,13 +766,13 @@ int16_t getReceiveCompletionOffset() return(12); //Tsym: 16 break; case (400): - return(0); //Tsym: 8 + return(6); //Tsym: 8 break; case (1200): - return(0); //Tsym: 4 + return(3); //Tsym: 4 break; case (2400): - return(0); //Tsym: 2 + return(1); //Tsym: 2 break; case (4800): return(0); //Tsym: 1 From dbbe465b657961d7258a444bd1894d64d8d5372b Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 17 Nov 2022 15:32:18 -0700 Subject: [PATCH 149/594] Fix merge bug --- Firmware/LoRaSerial_Firmware/Radio.ino | 1 - 1 file changed, 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 01b5f809..6c9221d7 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -306,7 +306,6 @@ void generateHopTable() + (uint16_t)settings.radioBandwidth + settings.radioSpreadFactor + settings.verifyRxNetID - + settings.radioProtocolVersion + settings.overheadTime + settings.enableCRC16 + settings.clientPingRetryInterval; From f0cdf4207a02f57c434e7f2aaf11221543a7de8e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 16 Nov 2022 06:12:16 -1000 Subject: [PATCH 150/594] Always save the TX data frame after transmission --- Firmware/LoRaSerial_Firmware/States.ino | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d8b4c9d9..e8dff3a4 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -713,6 +713,12 @@ void updateRadioState() { setHeartbeatLong(); //We're sending data, so don't be the first to send next heartbeat transmitTimer = datagramTimer; + + //Save the previous transmit in case the previous ACK was lost or a + //HEARTBEAT must be transmitted. Restore the buffer when a retransmission + //is necessary. + petWDT(); + SAVE_TX_BUFFER(); changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); } } @@ -871,13 +877,6 @@ void updateRadioState() updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; - //An ACK was expected for a previous transmission that must have been - //lost. Save the current transmit buffer for later retransmission - //and ACK the heartbeat. Later perform the retransmission for the - //datagram that was lost. - petWDT(); - SAVE_TX_BUFFER(); - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); if (xmitDatagramP2PAck() == true) //Transmit ACK { @@ -897,13 +896,6 @@ void updateRadioState() updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; - //An ACK was expected for a previous transmission that must have been - //lost. Save the current transmit buffer for later retransmission - //and ACK the data. Later perform the retransmission for the - //datagram that was lost. - petWDT(); - SAVE_TX_BUFFER(); - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); if (xmitDatagramP2PAck() == true) //Transmit ACK { @@ -999,8 +991,7 @@ void updateRadioState() //Retransmit the packet if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { - - RESTORE_TX_BUFFER(); //This should only be called once otherwise corruption occurs + RESTORE_TX_BUFFER(); if (settings.debugDatagrams) { systemPrintTimestamp(); From 5bf6731bb26bbaa50929205fc4fdecf47fbab96e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 18 Nov 2022 08:52:30 -0800 Subject: [PATCH 151/594] Default to TX_POWER of 14 when ENABLE_DEVELOPER is defined --- Firmware/LoRaSerial_Firmware/settings.h | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 77aab17f..3b19b107 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -359,7 +359,12 @@ typedef struct struct_settings { bool encryptData = true; //AES encrypt each packet uint8_t encryptionKey[AES_KEY_BYTES] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; bool dataScrambling = false; //Use IBM Data Whitening to reduce DC bias - uint8_t radioBroadcastPower_dbm = 30; //Transmit power in dBm. Max is 30dBm (1W), min is 14dBm (25mW). +#if defined(ENABLE_DEVELOPER) +#define TX_POWER_DB 14 +#else //ENABLE_DEVELOPER +#define TX_POWER_DB 30 +#endif //ENABLE_DEVELOPER + uint8_t radioBroadcastPower_dbm = TX_POWER_DB; //Transmit power in dBm. Max is 30dBm (1W), min is 14dBm (25mW). float frequencyMin = 902.0; //MHz float frequencyMax = 928.0; //MHz uint8_t numberOfChannels = 50; //Divide the min/max freq band into this number of channels and hop between. From dcadce0634539dc1ce02e00bfaa2fc0aa79fc0a8 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 18 Nov 2022 13:25:13 -0700 Subject: [PATCH 152/594] Determine settings before selecting header bytes Partial fix for SF6 --- Firmware/LoRaSerial_Firmware/Radio.ino | 7 +------ Firmware/LoRaSerial_Firmware/States.ino | 22 ++++++++++++---------- Firmware/LoRaSerial_Firmware/System.ino | 11 +++++++++++ Firmware/LoRaSerial_Firmware/Train.ino | 2 ++ 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6c9221d7..71f4487a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -5,9 +5,6 @@ void configureRadio() float frequency; bool success = true; - //Update the settings based upon the air speed - convertAirSpeedToSettings(); - frequency = channels[0]; if (radio.setFrequency(frequency) == RADIOLIB_ERR_INVALID_FREQUENCY) success = false; @@ -278,9 +275,6 @@ void generateHopTable() if (channels != NULL) free(channels); channels = (float *)malloc(settings.numberOfChannels * sizeof(float)); - //Update the settings based upon the air speed - convertAirSpeedToSettings(); - float channelSpacing = (settings.frequencyMax - settings.frequencyMin) / (float)(settings.numberOfChannels + 2); //Keep away from edge of available spectrum @@ -430,6 +424,7 @@ void hopChannel(bool moveForwardThroughTable) frequency = channels[channelNumber]; radio.setFrequency(frequency); + //triggerFrequency(frequency); //Print the frequency if requested if (settings.printFrequency) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d8b4c9d9..ccbbd2a7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -59,16 +59,9 @@ void updateRadioState() //Empty the buffers discardPreviousData(); - //Start the TX timer: time to delay before transmitting the PING - setHeartbeatShort(); //Both radios start with short heartbeat period - pingRandomTime = random(ackAirTime, ackAirTime * 2); //Fast ping - //Set all of the ACK numbers to zero *(uint8_t *)(&txControl) = 0; - //Determine the components of the frame header and trailer - selectHeaderAndTrailerBytes(); - //Initialize the radio rssi = -200; setRSSI(0b0000); //Turn off LEDs @@ -80,12 +73,21 @@ void updateRadioState() systemPrintln(radioSeed); } - generateHopTable(); //Generate frequency table based on user settings + convertAirSpeedToSettings(); //Update the settings based upon the air speed + + generateHopTable(); //Generate frequency table based on user settings. + + selectHeaderAndTrailerBytes(); //Determine the components of the frame header and trailer stopChannelTimer(); //Prevent radio from frequency hopping + configureRadio(); //Setup radio, set freq to channel 0, calculate air times + + //Start the TX timer: time to delay before transmitting the PING + setHeartbeatShort(); //Both radios start with short heartbeat period + pingRandomTime = random(ackAirTime, ackAirTime * 2); //Fast ping + petWDT(); - configureRadio(); //Setup radio, set freq to channel 0 returnToReceiving(); //Start receiving @@ -1897,7 +1899,7 @@ void selectHeaderAndTrailerBytes() //Add the control byte to the header headerBytes += 1; - //Determine the maximum frame size + //Add the byte containing the frame size (only needed in SF6) if (settings.radioSpreadFactor == 6) headerBytes += 1; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 538fdae1..6f306ab3 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -472,6 +472,17 @@ void dumpBuffer(uint8_t * data, int length) } } +void dumpBufferRaw(uint8_t * data, int length) +{ + systemPrint("0x "); + for(int x = 0 ; x < length ; x++) + { + systemPrint(data[x], HEX); + systemPrint(" "); + } + systemPrintln(); +} + void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, int length) { int bytes; diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index d52fc9bf..4006df5f 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -206,6 +206,8 @@ void commonTrainingInitialization() startCylonLEDs(); petWDT(); + convertAirSpeedToSettings(); //Update the settings based upon the air speed + generateHopTable(); //Generate frequency table based on current settings //Select the training frequency, a multiple of channels down from the maximum From 76ee0352bebd75d3103322798553937ecf23450c Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 18 Nov 2022 13:25:56 -0700 Subject: [PATCH 153/594] Avoid rxDataByte adjustment for SF6 Partial fix for SF6 --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 75ed323a..821fd352 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -893,7 +893,9 @@ PacketType rcvDatagram() if (settings.radioSpreadFactor == 6) { if (rxDataBytes >= (*rxData + minDatagramSize)) + { rxDataBytes = *rxData++; + } else { if (settings.debugReceive) @@ -911,7 +913,8 @@ PacketType rcvDatagram() return (DATAGRAM_BAD); } } - rxDataBytes -= minDatagramSize; + else + rxDataBytes -= minDatagramSize; //Verify the packet number last so that the expected datagram or ACK number can be updated rxVcData = rxData; @@ -1335,7 +1338,7 @@ bool transmitDatagram() if (settings.debugTransmit) printControl(control); - //Add the spread factor 6 length is required + //Add the spread factor 6 length if required if (settings.radioSpreadFactor == 6) { *header++ = length; From effc3092f078719a9c1d6138263cf9368b9afadb Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 18 Nov 2022 13:26:22 -0700 Subject: [PATCH 154/594] Add offsets for higher air speeds --- Firmware/LoRaSerial_Firmware/Radio.ino | 40 +++++++++++++++++++++++- Firmware/LoRaSerial_Firmware/RadioV2.ino | 13 ++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 71f4487a..9ac0d7a0 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -545,5 +545,43 @@ int16_t getLinkupOffset() { partialTimer = true; //Mark timer so that it runs only once with less than dwell time - return (settings.maxDwellTime - getReceiveCompletionOffset()); //Reduce the default window by the offset + int linkupOffset = 0; + + switch (settings.airSpeed) + { + default: + break; + case (40): + linkupOffset = 0; + break; + case (150): + linkupOffset = 0; + break; + case (400): + linkupOffset = 0; + break; + case (1200): + linkupOffset = 0; + break; + case (2400): + linkupOffset = 0; + break; + case (4800): + linkupOffset = 0; + break; + case (9600): + linkupOffset = 0; + break; + case (19200): + linkupOffset = 0; + break; + case (28800): + linkupOffset = 6; + break; + case (38400): + linkupOffset = 6; + break; + } + + return (settings.maxDwellTime - getReceiveCompletionOffset() - linkupOffset); //Reduce the default window by the offset } diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 821fd352..fc3db3d2 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1673,6 +1673,19 @@ void syncChannelTimer() case (2400): msToNextHopRemote -= getReceiveCompletionOffset(); break; + case (4800): + break; + case (9600): + break; + case (19200): + break; + case (28800): + msToNextHopRemote -= 17; + break; + case (38400): + //msToNextHopRemote -= 0; //Unit sending HB is 16ms behind + msToNextHopRemote -= 16; //Unit sending HB is + break; } //Calculate the remote's absolute distance to its next hop From 0a00f0ae6792435ee6ccc45461a7eaec31c53786 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 18 Nov 2022 15:32:27 -0700 Subject: [PATCH 155/594] Remove unnecessary bufferRestored gate --- Firmware/LoRaSerial_Firmware/States.ino | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bbd1cc2c..4084ccc0 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -4,19 +4,14 @@ rexmtControl = txControl; \ rexmtLength = txDatagramSize; \ rexmtFrameSentCount = frameSentCount; \ - bufferRestored = false; \ } #define RESTORE_TX_BUFFER() \ { \ - if(bufferRestored == false) \ - { \ - memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ - txControl = rexmtControl; \ - txDatagramSize = rexmtLength; \ - frameSentCount = rexmtFrameSentCount; \ - bufferRestored = true; \ - } \ + memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ + txControl = rexmtControl; \ + txDatagramSize = rexmtLength; \ + frameSentCount = rexmtFrameSentCount; \ } void updateRadioState() @@ -35,7 +30,6 @@ void updateRadioState() static uint8_t rexmtLength; static uint8_t rexmtFrameSentCount; static uint8_t rexmtTxDestVc; - static bool bufferRestored = false; VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; From 5391fc37f6757d07bd04c8ddc05fc7737c6c4fa2 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 18 Nov 2022 15:40:34 -0700 Subject: [PATCH 156/594] Add hardware CRC output to alternate LED --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index fc3db3d2..d9996cfd 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -656,6 +656,7 @@ PacketType rcvDatagram() { if (settings.debug == true) systemPrintln("Receive CRC error!"); + badCrc++; return (DATAGRAM_CRC_ERROR); } else From 9863e6efb4416c39779d654b053742418ff437d9 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 18 Nov 2022 16:09:59 -0700 Subject: [PATCH 157/594] Move return to receive directly after data is read from radio Remove duplicate returnToReceiving(). --- Firmware/LoRaSerial_Firmware/Radio.ino | 6 +-- Firmware/LoRaSerial_Firmware/RadioV2.ino | 3 ++ Firmware/LoRaSerial_Firmware/States.ino | 48 +++--------------------- Firmware/LoRaSerial_Firmware/System.ino | 26 +++++++------ 4 files changed, 25 insertions(+), 58 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 9ac0d7a0..4625d39c 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -200,6 +200,8 @@ void setRadioFrequency(bool rxAdjust) void returnToReceiving() { + if(receiveInProcess() == true) return; //Do not touch the radio if it is already receiving + int state; if (settings.radioSpreadFactor > 6) { @@ -229,10 +231,6 @@ void returnToReceiving() systemPrintln(state); } } - - //Reset RSSI measurements - rssi = 0; - hopCount = 0; } //Given spread factor, bandwidth, coding rate and number of bytes, return total Air Time in ms for packet diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index d9996cfd..27598424 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -670,6 +670,9 @@ PacketType rcvDatagram() } rxDataBytes = radio.getPacketLength(); + + returnToReceiving(); //Immediately begin listening while we process new data + rxData = incomingBuffer; /* diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 4084ccc0..69022970 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -172,7 +172,6 @@ void updateRadioState() { default: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_P2P_TRAINING_PING: @@ -307,9 +306,7 @@ void updateRadioState() //Decode the received packet PacketType packetType = rcvDatagram(); - if (packetType != DATAGRAM_PING) - returnToReceiving(); - else + if (packetType == DATAGRAM_PING) { //Received PING //Compute the common clock @@ -373,9 +370,7 @@ void updateRadioState() if (xmitDatagramP2PAck1() == true) changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); } - else if (packetType != DATAGRAM_ACK_1) - returnToReceiving(); - else + else if (packetType == DATAGRAM_ACK_1) { //Received ACK 1 //Compute the common clock @@ -402,7 +397,6 @@ void updateRadioState() systemPrintTimestamp(); systemPrintln("RX: ACK1 Timeout"); } - returnToReceiving(); //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK1_TIMEOUT); @@ -431,9 +425,7 @@ void updateRadioState() //Decode the received packet PacketType packetType = rcvDatagram(); - if (packetType != DATAGRAM_ACK_2) - returnToReceiving(); - else + if (packetType == DATAGRAM_ACK_2) { //Received ACK 2 //Compute the common clock @@ -574,22 +566,18 @@ void updateRadioState() systemPrint(v2DatagramType[packetType]); systemPrintln(); } - returnToReceiving(); break; case DATAGRAM_BAD: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_CRC_ERROR: triggerEvent(TRIGGER_CRC_ERROR); - returnToReceiving(); break; case DATAGRAM_NETID_MISMATCH: triggerEvent(TRIGGER_NETID_MISMATCH); - returnToReceiving(); break; case DATAGRAM_PING: @@ -816,22 +804,18 @@ void updateRadioState() systemPrint(v2DatagramType[packetType]); systemPrintln(); } - returnToReceiving(); break; case DATAGRAM_BAD: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_CRC_ERROR: triggerEvent(TRIGGER_CRC_ERROR); - returnToReceiving(); break; case DATAGRAM_NETID_MISMATCH: triggerEvent(TRIGGER_NETID_MISMATCH); - returnToReceiving(); break; case DATAGRAM_PING: @@ -854,7 +838,6 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_ACK_RECEIVED); - returnToReceiving(); changeState(RADIO_P2P_LINK_UP); break; @@ -983,7 +966,6 @@ void updateRadioState() //completes transmission, retransmit the previously lost datagram. if (transactionComplete) { - transactionComplete = false; //Reset ISR flag //Retransmit the packet if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { @@ -1018,6 +1000,8 @@ void updateRadioState() if (retransmitDatagram(NULL) == true) { + transactionComplete = false; //Reset ISR flag + setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat lostFrames++; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); @@ -1065,22 +1049,18 @@ void updateRadioState() systemPrint(v2DatagramType[packetType]); systemPrintln(); } - returnToReceiving(); break; case DATAGRAM_BAD: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_CRC_ERROR: triggerEvent(TRIGGER_CRC_ERROR); - returnToReceiving(); break; case DATAGRAM_NETID_MISMATCH: triggerEvent(TRIGGER_NETID_MISMATCH); - returnToReceiving(); break; case DATAGRAM_ACK_2: @@ -1092,7 +1072,6 @@ void updateRadioState() //We should not be receiving these datagrams, but if we do, just ignore frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_ACK_1: @@ -1109,7 +1088,6 @@ void updateRadioState() lastPacketReceived = millis(); //Reset triggerEvent(TRIGGER_LINK_ACK_RECEIVED); - returnToReceiving(); changeState(RADIO_MP_STANDBY); break; } @@ -1200,22 +1178,18 @@ void updateRadioState() systemPrint(v2DatagramType[packetType]); systemPrintln(); } - returnToReceiving(); break; case DATAGRAM_BAD: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_CRC_ERROR: triggerEvent(TRIGGER_CRC_ERROR); - returnToReceiving(); break; case DATAGRAM_NETID_MISMATCH: triggerEvent(TRIGGER_NETID_MISMATCH); - returnToReceiving(); break; case DATAGRAM_ACK_1: @@ -1227,7 +1201,6 @@ void updateRadioState() //We should not be receiving these datagrams, but if we do, just ignore frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_PING: @@ -1240,7 +1213,6 @@ void updateRadioState() } else { - returnToReceiving(); changeState(RADIO_MP_STANDBY); } break; @@ -1256,7 +1228,6 @@ void updateRadioState() lastPacketReceived = millis(); //Update timestamp for Link LED - returnToReceiving(); //No ack when in multipoint mode changeState(RADIO_MP_STANDBY); break; @@ -1277,7 +1248,6 @@ void updateRadioState() triggerEvent(TRIGGER_MP_DATA_PACKET); lastPacketReceived = millis(); //Update timestamp for Link LED - returnToReceiving(); //No ack when in multipoint mode changeState(RADIO_MP_STANDBY); break; } @@ -1425,7 +1395,6 @@ void updateRadioState() { default: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_TRAINING_PARAMS: @@ -1434,7 +1403,6 @@ void updateRadioState() && (memcmp(rxData, myUniqueId, UNIQUE_ID_BYTES) != 0)) { triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; } @@ -1528,7 +1496,6 @@ void updateRadioState() { default: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_TRAINING_PING: @@ -1551,7 +1518,6 @@ void updateRadioState() systemPrintUniqueID(trainingPartnerID); systemPrintln(" Trained"); } - returnToReceiving(); break; } } @@ -1650,7 +1616,6 @@ void updateRadioState() { default: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_VC_HEARTBEAT: @@ -1673,7 +1638,6 @@ void updateRadioState() case DATAGRAM_DATA_ACK: vcAckTimer = 0; - returnToReceiving(); break; } } @@ -1755,7 +1719,6 @@ void updateRadioState() { default: triggerEvent(TRIGGER_BAD_PACKET); - returnToReceiving(); break; case DATAGRAM_VC_HEARTBEAT: @@ -1778,7 +1741,6 @@ void updateRadioState() case DATAGRAM_DATA_ACK: vcAckTimer = 0; - returnToReceiving(); changeState(RADIO_VC_WAIT_RECEIVE); break; } diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 6f306ab3..688e5b33 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -475,7 +475,7 @@ void dumpBuffer(uint8_t * data, int length) void dumpBufferRaw(uint8_t * data, int length) { systemPrint("0x "); - for(int x = 0 ; x < length ; x++) + for (int x = 0 ; x < length ; x++) { systemPrint(data[x], HEX); systemPrint(" "); @@ -544,10 +544,7 @@ void updateRSSI() { //Calculate the average RSSI if possible if (hopCount > 0) - { rssi /= hopCount; - hopCount = 0; - } else rssi = radio.getRSSI(); @@ -563,6 +560,13 @@ void updateRSSI() setRSSI(0b0111); if (rssi > rssiLevelMax) setRSSI(0b1111); + + if (hopCount > 0) + { + //Reset RSSI measurements + hopCount = 0; + rssi = 0; + } } void setRSSI(uint8_t ledBits) @@ -761,7 +765,7 @@ void triggerFrequency(uint16_t frequency) digitalWrite(pin_trigger, HIGH); } -//The difference between when a transmitter is finished and when the receiver is finished +//The difference between when a transmitter is finished and when the receiver is finished //is different between airSpeeds and affects things like ACK timeouts at lower airSpeeds //Offsets were found using a logic analyzer but it looks like (1 * Tsym) or calcSymbolTime() int16_t getReceiveCompletionOffset() @@ -771,22 +775,22 @@ int16_t getReceiveCompletionOffset() default: break; case (40): - return(26); //Tsym: 32. Measured: 26 ms between a TX complete and the RX complete + return (26); //Tsym: 32. Measured: 26 ms between a TX complete and the RX complete break; case (150): - return(12); //Tsym: 16 + return (12); //Tsym: 16 break; case (400): - return(6); //Tsym: 8 + return (6); //Tsym: 8 break; case (1200): - return(3); //Tsym: 4 + return (3); //Tsym: 4 break; case (2400): - return(1); //Tsym: 2 + return (1); //Tsym: 2 break; case (4800): - return(0); //Tsym: 1 + return (0); //Tsym: 1 break; } } From 7c8724e4f159f6d462e012f2eb9892e9177a6d91 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 14 Nov 2022 16:01:55 -1000 Subject: [PATCH 158/594] VC: Only receive messages targeting my VC --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 16 ++++++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 17 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 27598424..3b9c280b 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1097,6 +1097,22 @@ PacketType rcvDatagram() return DATAGRAM_BAD; } + //Validate the destination VC + if ((rxDestVc != VC_BROADCAST) && (rxDestVc != myVc)) + { + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Not my VC: "); + systemPrintln(rxDestVc); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + if (settings.printPktData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + } + return DATAGRAM_NOT_MINE; + } + //Account for this frame if (vc) { diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 3b19b107..4f7d2cbd 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -156,6 +156,7 @@ typedef enum DATAGRAM_CRC_ERROR, DATAGRAM_NETID_MISMATCH, DATAGRAM_DUPLICATE, + DATAGRAM_NOT_MINE, } PacketType; const char * const v2DatagramType[] = From ace5b6537621a118ad1a28471cedf6fbf04fb922 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 21 Nov 2022 09:22:10 -0800 Subject: [PATCH 159/594] Output the debug serial data --- Firmware/LoRaSerial_Firmware/Commands.ino | 3 ++ Firmware/LoRaSerial_Firmware/NVM.ino | 18 ++++++++++ Firmware/LoRaSerial_Firmware/Radio.ino | 10 ++++++ Firmware/LoRaSerial_Firmware/RadioV2.ino | 43 +++++++++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 32 +++++++++++++++++ Firmware/LoRaSerial_Firmware/Train.ino | 7 ++++ 6 files changed, 113 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index dec04093..44489d85 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -107,7 +107,10 @@ bool commandAT(const char * commandString) //Display the end of the delay if (settings.printLinkUpDown) + { systemPrintln("Delay done"); + outputSerialData(true); + } break; case ('F'): //Enter training mode and return to factory defaults reportOK(); diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index d768edf6..c1e32304 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -7,7 +7,10 @@ void loadSettings() if (EEPROM.get(0, testRead) == 0xFFFFFFFF) { if (settings.debug) + { systemPrintln("EEPROM is blank."); + outputSerialData(true); + } recordSystemSettings(); //Record default settings to EEPROM. At power on, settings are in default state } @@ -18,7 +21,10 @@ void loadSettings() if (tempSize != sizeof(settings)) { if (settings.debug) + { systemPrintln("Settings wrong size."); + outputSerialData(true); + } recordSystemSettings(); //Record default settings to EEPROM. At power on, settings are in default state } @@ -28,13 +34,19 @@ void loadSettings() if (tempIdentifier != LRS_IDENTIFIER) { if (settings.debug) + { systemPrint("Settings are not valid for this variant of LoRaSerial."); + outputSerialData(true); + } recordSystemSettings(); //Record default settings to EEPROM. At power on, settings are in default state } //Read current settings if (settings.debug) + { systemPrintln("Reading the settings from EEPROM"); + outputSerialData(true); + } EEPROM.get(0, settings); recordSystemSettings(); @@ -44,7 +56,10 @@ void loadSettings() void recordSystemSettings() { if (settings.debug) + { systemPrintln("Writing settings to EEPROM"); + outputSerialData(true); + } settings.sizeOfSettings = sizeof(settings); EEPROM.put(0, settings); @@ -54,7 +69,10 @@ void recordSystemSettings() void eepromErase() { if (settings.debug) + { systemPrintln("Erasing the EEPROM"); + outputSerialData(true); + } for (uint16_t i = 0 ; i < EEPROM.length() ; i++) { petWDT(); diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4625d39c..1b8f571c 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -15,6 +15,7 @@ void configureRadio() systemPrintTimestamp(); systemPrint(frequency); systemPrintln(" MHz"); + outputSerialData(true); } channelNumber = 0; @@ -96,6 +97,7 @@ void configureRadio() systemPrintln(hoppingPeriod); systemPrint("ackAirTime: "); systemPrintln(ackAirTime); + outputSerialData(true); } if (success == false) @@ -104,7 +106,10 @@ void configureRadio() systemPrintln("Radio init failed. Check settings."); } if ((settings.debug == true) || (settings.debugRadio == true)) + { systemPrintln("Radio configured"); + outputSerialData(true); + } } //Update the settings based upon the airSpeed value @@ -173,6 +178,7 @@ void convertAirSpeedToSettings() { systemPrint("Unknown airSpeed: "); systemPrintln(settings.airSpeed); + outputSerialData(true); } break; } @@ -195,6 +201,7 @@ void setRadioFrequency(bool rxAdjust) systemPrintTimestamp(); systemPrint(frequency); systemPrintln(" MHz"); + outputSerialData(true); } } @@ -229,6 +236,7 @@ void returnToReceiving() { systemPrint("Receive failed: "); systemPrintln(state); + outputSerialData(true); } } } @@ -355,6 +363,7 @@ void generateHopTable() systemPrint(AESiv[i], HEX); } systemPrintln(); + outputSerialData(true); } } @@ -430,6 +439,7 @@ void hopChannel(bool moveForwardThroughTable) systemPrintTimestamp(); systemPrint(frequency, 3); systemPrintln(" MHz"); + outputSerialData(true); } } diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 3b9c280b..dac6c66c 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -700,11 +700,13 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); } if (settings.dataScrambling == true) @@ -727,11 +729,13 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); } //All packets must include the 2-byte control header @@ -746,11 +750,13 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); } badFrames++; return (DATAGRAM_BAD); @@ -789,12 +795,14 @@ PacketType rcvDatagram() systemPrint(settings.netID); } systemPrintln(); + outputSerialData(true); } if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); if (settings.debugReceive && settings.printPktData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); netIdMismatch++; return (DATAGRAM_NETID_MISMATCH); } @@ -827,11 +835,13 @@ PacketType rcvDatagram() systemPrint(incomingBuffer[rxDataBytes - 1], HEX); systemPrint(" expected 0x"); systemPrintln(crc, HEX); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); } badCrc++; return (DATAGRAM_CRC_ERROR); @@ -864,6 +874,7 @@ PacketType rcvDatagram() systemPrintTimestamp(); systemPrint("RX: Invalid datagram type "); systemPrintln(datagramType); + outputSerialData(true); } badFrames++; return (DATAGRAM_BAD); @@ -876,6 +887,7 @@ PacketType rcvDatagram() systemPrint(" CRC-16: 0x"); systemPrint(incomingBuffer[rxDataBytes - 2], HEX); systemPrintln(incomingBuffer[rxDataBytes - 1], HEX); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -910,6 +922,7 @@ PacketType rcvDatagram() systemPrint(" > "); systemPrint((int)rxDataBytes - minDatagramSize); systemPrintln(" received bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -939,6 +952,7 @@ PacketType rcvDatagram() systemPrint(ackNumber); systemPrint(" expecting "); systemPrintln(txAckNumber); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -971,6 +985,7 @@ PacketType rcvDatagram() systemPrint(ackNumber); systemPrint(" expecting "); systemPrintln(rmtTxAckNumber); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1002,6 +1017,7 @@ PacketType rcvDatagram() systemPrint(ackNumber); systemPrint(" expecting "); systemPrintln(rmtTxAckNumber); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1043,6 +1059,7 @@ PacketType rcvDatagram() systemPrint("Missing VC header bytes, received only "); systemPrint(rxDataBytes); systemPrintln(" bytes, expecting at least 3 bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1067,10 +1084,12 @@ PacketType rcvDatagram() systemPrintTimestamp(); systemPrint("Invalid source VC: "); systemPrintln(rxSrcVc); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); if (settings.printRfData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); } badFrames++; return DATAGRAM_BAD; @@ -1088,6 +1107,7 @@ PacketType rcvDatagram() systemPrint(vcHeader->length); systemPrint(" expecting "); systemPrintln(rxDataBytes); + outputSerialData(true); } if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -1105,10 +1125,12 @@ PacketType rcvDatagram() systemPrintTimestamp(); systemPrint("Not my VC: "); systemPrintln(rxDestVc); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); if (settings.printPktData && rxDataBytes) dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); } return DATAGRAM_NOT_MINE; } @@ -1137,6 +1159,7 @@ PacketType rcvDatagram() systemPrintln("Unassigned"); else systemPrintln(rxSrcVc); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1164,6 +1187,7 @@ PacketType rcvDatagram() systemPrint(" (0x"); systemPrint(rxDataBytes, HEX); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); @@ -1197,6 +1221,7 @@ PacketType rcvDatagram() systemPrintln(); break; } + outputSerialData(true); } if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -1282,6 +1307,7 @@ bool transmitDatagram() systemPrintln(); break; } + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1314,6 +1340,7 @@ bool transmitDatagram() petWDT(); if (settings.printPktData) dumpBuffer(&endOfTxData[-length], length); + outputSerialData(true); } //Build the datagram header @@ -1326,6 +1353,7 @@ bool transmitDatagram() systemPrint(" (0x"); systemPrint(headerBytes); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1344,6 +1372,7 @@ bool transmitDatagram() systemPrint(" (0x"); systemPrint(settings.netID, HEX); systemPrintln(")"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); @@ -1369,6 +1398,7 @@ bool transmitDatagram() systemPrintTimestamp(); systemPrint(" SF6 Length: "); systemPrintln(length); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1390,6 +1420,7 @@ bool transmitDatagram() systemPrintln("Unassigned"); else systemPrintln(srcVc); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1435,6 +1466,7 @@ bool transmitDatagram() systemPrint(" (0x"); systemPrint(trailerBytes); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -1445,6 +1477,7 @@ bool transmitDatagram() systemPrint(" CRC-16: 0x"); systemPrint(endOfTxData[-2], HEX); systemPrintln(endOfTxData[-1], HEX); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1472,11 +1505,13 @@ bool transmitDatagram() systemPrint(" (0x"); systemPrint(txDatagramSize, HEX); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); if (settings.printRfData) dumpBuffer(outgoingPacket, txDatagramSize); + outputSerialData(true); } //Encrypt the datagram @@ -1501,11 +1536,13 @@ bool transmitDatagram() systemPrint(" (0x"); systemPrint(txDatagramSize, HEX); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); if (settings.printRfData) dumpBuffer(outgoingPacket, txDatagramSize); + outputSerialData(true); } //If we are trainsmitting at high data rates the receiver is often not ready for new data. Pause for a few ms (measured with logic analyzer). @@ -1547,6 +1584,7 @@ void printControl(uint8_t value) systemPrint("Unknown "); systemPrintln(control->datagramType); } + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); @@ -1580,11 +1618,13 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) systemPrint(" (0x"); systemPrint(txDatagramSize, HEX); systemPrintln(") bytes"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); if (settings.printRfData) dumpBuffer(outgoingPacket, txDatagramSize); + outputSerialData(true); } //Transmit this frame @@ -1614,6 +1654,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) systemPrint("TX: frameAirTime "); systemPrint(frameAirTime); systemPrintln(" mSec"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -1621,6 +1662,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) systemPrint("TX: responseDelay "); systemPrint(responseDelay); systemPrintln(" mSec"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } @@ -1631,6 +1673,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) systemPrintTimestamp(); systemPrint("TX: Transmit error, state "); systemPrintln(state); + outputSerialData(true); } datagramTimer = millis(); //Move timestamp even if error diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 69022970..c5b13420 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -65,6 +65,7 @@ void updateRadioState() { systemPrint("RadioSeed: "); systemPrintln(radioSeed); + outputSerialData(true); } convertAirSpeedToSettings(); //Update the settings based upon the air speed @@ -191,7 +192,10 @@ void updateRadioState() updateRadioParameters(rxData); endPointToPointTraining(true); if (settings.debugTraining) + { systemPrintln("Training successful, received parameters!"); + outputSerialData(true); + } changeState(RADIO_RESET); } } @@ -213,7 +217,10 @@ void updateRadioState() { //Failed to complete the training if (settings.debugTraining) + { systemPrintln("Training timeout, returning to previous mode!"); + outputSerialData(true); + } endPointToPointTraining(false); changeState(RADIO_RESET); } @@ -227,7 +234,10 @@ void updateRadioState() transactionComplete = false; //Reset ISR flag endPointToPointTraining(false); if (settings.debugTraining) + { systemPrintln("Training successful, sent parameters!"); + outputSerialData(true); + } changeState(RADIO_RESET); } break; @@ -396,6 +406,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrintln("RX: ACK1 Timeout"); + outputSerialData(true); } //Start the TX timer: time to delay before transmitting the PING @@ -453,6 +464,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrintln("RX: ACK2 Timeout"); + outputSerialData(true); } //Start the TX timer: time to delay before transmitting the PING @@ -565,6 +577,7 @@ void updateRadioState() systemPrint("LinkUp: Unhandled packet type "); systemPrint(v2DatagramType[packetType]); systemPrintln(); + outputSerialData(true); } break; @@ -803,6 +816,7 @@ void updateRadioState() systemPrint("RX: Unhandled packet type "); systemPrint(v2DatagramType[packetType]); systemPrintln(); + outputSerialData(true); } break; @@ -900,6 +914,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrintln("RX: ACK Timeout"); + outputSerialData(true); } triggerEvent(TRIGGER_LINK_RETRANSMIT); @@ -927,6 +942,7 @@ void updateRadioState() systemPrintln(); break; } + outputSerialData(true); } if (retransmitDatagram(NULL) == true) @@ -977,6 +993,7 @@ void updateRadioState() systemPrint(frameSentCount); systemPrint(", "); systemPrint(v2DatagramType[txControl.datagramType]); + outputSerialData(true); switch (txControl.datagramType) { default: @@ -992,6 +1009,7 @@ void updateRadioState() systemPrint(txControl.ackNumber); systemPrint(")"); systemPrintln(); + outputSerialData(true); break; } } @@ -1048,6 +1066,7 @@ void updateRadioState() systemPrint("Scan: Unhandled packet type "); systemPrint(v2DatagramType[packetType]); systemPrintln(); + outputSerialData(true); } break; @@ -1103,6 +1122,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrintln("MP: ACK1 Timeout"); + outputSerialData(true); } //Move to previous channel in table @@ -1177,6 +1197,7 @@ void updateRadioState() systemPrint("MP Standby: Unhandled packet type "); systemPrint(v2DatagramType[packetType]); systemPrintln(); + outputSerialData(true); } break; @@ -1790,6 +1811,7 @@ void updateRadioState() systemPrintln(); break; } + outputSerialData(true); } //Retransmit the packet @@ -2109,6 +2131,7 @@ void changeState(RadioStates newState) } systemPrintln(); + outputSerialData(true); } void v2BreakLink() @@ -2116,7 +2139,10 @@ void v2BreakLink() //Break the link linkFailures++; if (settings.printLinkUpDown) + { systemPrintln("--------- Link DOWN ---------"); + outputSerialData(true); + } triggerEvent(TRIGGER_RADIO_RESET); //Stop the transmit timer @@ -2153,7 +2179,10 @@ void v2EnterLinkUp() returnToReceiving(); changeState(RADIO_P2P_LINK_UP); if (settings.printLinkUpDown) + { systemPrintln("========== Link UP =========="); + outputSerialData(true); + } } void discardPreviousData() @@ -2199,6 +2228,7 @@ void vcSendLinkStatus(bool linkUp, int8_t srcVc) systemPrint(srcVc); systemPrintln(" Down ---------"); } + outputSerialData(true); } } @@ -2279,6 +2309,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) if (index >= MAX_VC) { systemPrintln("ERROR: Too many clients, no free addresses!\n"); + outputSerialData(true); return -2; } srcAddr = index; @@ -2297,6 +2328,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) for (int i = 0; i < UNIQUE_ID_BYTES; i++) systemPrint(vc->uniqueId[i], HEX); systemPrintln(); + outputSerialData(true); return -3; } diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 4006df5f..9c94b9d2 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -18,7 +18,10 @@ void selectTraining(bool defaultTraining) void generateTrainingSettings() { if ((settings.debug == true) || (settings.debugTraining == true)) + { systemPrintln("Generate New Training Settings"); + outputSerialData(true); + } //Seed random number based on RF noise. We use Arduino random() because platform specific generation does not matter randomSeed(radio.randomByte()); @@ -44,6 +47,7 @@ void generateTrainingSettings() systemPrint(settings.encryptionKey[i], HEX); } systemPrintln(); + outputSerialData(true); } } @@ -229,7 +233,10 @@ void endClientServerTraining(uint8_t event) settings = originalSettings; //Return to original radio settings if (settings.debugTraining) + { displayParameters(0, settings.copyDebug || settings.copyTriggers); + outputSerialData(true); + } if (!settings.trainingServer) { From a53e1000cae9c679037a2f3ea9b26decabe996b0 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 21 Nov 2022 09:47:56 -0800 Subject: [PATCH 160/594] Debug the NVM operation Change settings.debug to settings.debugNvm --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + Firmware/LoRaSerial_Firmware/NVM.ino | 12 ++++++------ Firmware/LoRaSerial_Firmware/settings.h | 1 + 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 44489d85..9865e5f2 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -708,6 +708,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &settings.copyDebug}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugNvm", &settings.debugNvm}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &settings.debugRadio}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &settings.debugReceive}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &settings.debugStates}, diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index c1e32304..5b718367 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -6,7 +6,7 @@ void loadSettings() uint32_t testRead = 0; if (EEPROM.get(0, testRead) == 0xFFFFFFFF) { - if (settings.debug) + if (settings.debugNvm) { systemPrintln("EEPROM is blank."); outputSerialData(true); @@ -20,7 +20,7 @@ void loadSettings() EEPROM.get(0, tempSize); //Load the sizeOfSettings if (tempSize != sizeof(settings)) { - if (settings.debug) + if (settings.debugNvm) { systemPrintln("Settings wrong size."); outputSerialData(true); @@ -33,7 +33,7 @@ void loadSettings() EEPROM.get(sizeof(tempSize), tempIdentifier); //Load the identifier from the EEPROM location after sizeOfSettings (int) if (tempIdentifier != LRS_IDENTIFIER) { - if (settings.debug) + if (settings.debugNvm) { systemPrint("Settings are not valid for this variant of LoRaSerial."); outputSerialData(true); @@ -42,7 +42,7 @@ void loadSettings() } //Read current settings - if (settings.debug) + if (settings.debugNvm) { systemPrintln("Reading the settings from EEPROM"); outputSerialData(true); @@ -55,7 +55,7 @@ void loadSettings() //Record the current settings struct to EEPROM void recordSystemSettings() { - if (settings.debug) + if (settings.debugNvm) { systemPrintln("Writing settings to EEPROM"); outputSerialData(true); @@ -68,7 +68,7 @@ void recordSystemSettings() void eepromErase() { - if (settings.debug) + if (settings.debugNvm) { systemPrintln("Erasing the EEPROM"); outputSerialData(true); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 4f7d2cbd..beafc794 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -423,6 +423,7 @@ typedef struct struct_settings { bool multipointServer = false; //Only one radio can be the server in multipoint mode bool debugSerial = false; //Debug the serial input bool debugSync = false; //Print clock sync processing + bool debugNvm = false; //Debug NVM operation //Add new parameters immediately before this line //-- Add commands to set the parameters From 595cbbd0e967d6389fd79377f3b4b530fe95e6a1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 21 Nov 2022 14:17:48 -0800 Subject: [PATCH 161/594] Remove extra calls to returnToReceiving --- Firmware/LoRaSerial_Firmware/States.ino | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c5b13420..d1cb8de2 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1019,7 +1019,7 @@ void updateRadioState() if (retransmitDatagram(NULL) == true) { transactionComplete = false; //Reset ISR flag - + setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat lostFrames++; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); @@ -2176,7 +2176,6 @@ void v2EnterLinkUp() lastLinkUpTime = millis(); //Start the receiver - returnToReceiving(); changeState(RADIO_P2P_LINK_UP); if (settings.printLinkUpDown) { @@ -2373,6 +2372,4 @@ void vcReceiveHeartbeat(RadioStates nextState, uint32_t rxMillis) xmitVcHeartbeat(vcSrc, rxVcData); changeState(nextState); } - else - returnToReceiving(); } From 54c97b764ea8717c508ca28584b1420cfc9e2bdf Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 21 Nov 2022 09:58:38 -0800 Subject: [PATCH 162/594] Add setting to print ACK number updates --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + Firmware/LoRaSerial_Firmware/RadioV2.ino | 40 +++++++++++++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 1 + 3 files changed, 42 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9865e5f2..9027f2d7 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -717,6 +717,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &settings.debugSerial}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayPacketQuality", &settings.displayPacketQuality}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &settings.displayRealMillis}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintAckNumbers", &settings.printAckNumbers}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &settings.printFrequency}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &settings.printLinkUpDown}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintPktData", &settings.printPktData}, diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index dac6c66c..db8acb85 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -961,7 +961,18 @@ PacketType rcvDatagram() } //Set the next TX ACK number + if (settings.printAckNumbers) + { + systemPrint("txAckNumber: "); + systemPrint(txAckNumber); + systemPrint(" --> "); + } txAckNumber = (txAckNumber + 1) & 3; + if (settings.printAckNumbers) + { + systemPrintln(txAckNumber); + outputSerialData(true); + } break; case DATAGRAM_REMOTE_COMMAND: @@ -994,8 +1005,26 @@ PacketType rcvDatagram() } //Receive this data packet and set the next expected datagram number + if (settings.printAckNumbers) + { + systemPrint("rxAckNumber: "); + systemPrint(rxAckNumber); + systemPrint(" --> "); + } rxAckNumber = rmtTxAckNumber; + if (settings.printAckNumbers) + { + systemPrintln(rxAckNumber); + systemPrint("rmtTxAckNumber: "); + systemPrint(rmtTxAckNumber); + systemPrint(" --> "); + } rmtTxAckNumber = (rmtTxAckNumber + 1) & 3; + if (settings.printAckNumbers) + { + systemPrintln(rmtTxAckNumber); + outputSerialData(true); + } break; case DATAGRAM_DATA: @@ -1041,7 +1070,18 @@ PacketType rcvDatagram() } //Receive this data packet and set the next expected datagram number + if (settings.printAckNumbers) + { + systemPrint("rxAckNumber: "); + systemPrint(rxAckNumber); + systemPrint(" --> "); + } rxAckNumber = rmtTxAckNumber; + if (settings.printAckNumbers) + { + systemPrintln(rxAckNumber); + outputSerialData(true); + } rmtTxAckNumber = (rmtTxAckNumber + 1) & 3; break; } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index beafc794..de6680ae 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -424,6 +424,7 @@ typedef struct struct_settings { bool debugSerial = false; //Debug the serial input bool debugSync = false; //Print clock sync processing bool debugNvm = false; //Debug NVM operation + bool printAckNumbers = false; //Print the ACK numbers //Add new parameters immediately before this line //-- Add commands to set the parameters From 87ba02627802efc062efaa05d4eceafef922c8a7 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 21 Nov 2022 10:37:45 -0800 Subject: [PATCH 163/594] Update the list of training parameters --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 74 +++++++++++++++--------- Firmware/LoRaSerial_Firmware/Train.ino | 32 +++++----- 2 files changed, 64 insertions(+), 42 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index db8acb85..4b083996 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -477,67 +477,85 @@ void updateRadioParameters(uint8_t * rxData) //Update the radio parameters originalSettings.airSpeed = params.airSpeed; - originalSettings.netID = params.netID; - originalSettings.operatingMode = params.operatingMode; - originalSettings.encryptData = params.encryptData; - memcpy(originalSettings.encryptionKey, params.encryptionKey, sizeof(originalSettings.encryptionKey)); - originalSettings.dataScrambling = params.dataScrambling; - originalSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; - originalSettings.frequencyMin = params.frequencyMin; - originalSettings.frequencyMax = params.frequencyMax; - originalSettings.numberOfChannels = params.numberOfChannels; + originalSettings.autoTuneFrequency = params.autoTuneFrequency; + originalSettings.radioBandwidth = params.radioBandwidth; + originalSettings.radioCodingRate = params.radioCodingRate; originalSettings.frequencyHop = params.frequencyHop; + originalSettings.frequencyMax = params.frequencyMax; + originalSettings.frequencyMin = params.frequencyMin; originalSettings.maxDwellTime = params.maxDwellTime; - originalSettings.radioBandwidth = params.radioBandwidth; + originalSettings.numberOfChannels = params.numberOfChannels; + originalSettings.radioPreambleLength = params.radioPreambleLength; originalSettings.radioSpreadFactor = params.radioSpreadFactor; - originalSettings.radioCodingRate = params.radioCodingRate; originalSettings.radioSyncWord = params.radioSyncWord; - originalSettings.radioPreambleLength = params.radioPreambleLength; + originalSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; + + //Update the radio protocol parameters + originalSettings.dataScrambling = params.dataScrambling; + originalSettings.enableCRC16 = params.enableCRC16; + originalSettings.encryptData = params.encryptData; + memcpy(originalSettings.encryptionKey, params.encryptionKey, sizeof(originalSettings.encryptionKey)); originalSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; originalSettings.heartbeatTimeout = params.heartbeatTimeout; - originalSettings.autoTuneFrequency = params.autoTuneFrequency; originalSettings.maxResends = params.maxResends; - originalSettings.verifyRxNetID = params.verifyRxNetID; + originalSettings.multipointServer = params.multipointServer; + originalSettings.netID = params.netID; + originalSettings.operatingMode = params.operatingMode; originalSettings.overheadTime = params.overheadTime; - originalSettings.enableCRC16 = params.enableCRC16; - originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; + originalSettings.verifyRxNetID = params.verifyRxNetID; //Update the debug parameters if (params.copyDebug) { originalSettings.debug = params.debug; - originalSettings.displayPacketQuality = params.displayPacketQuality; - originalSettings.printFrequency = params.printFrequency; + originalSettings.alternateLedUsage = params.alternateLedUsage; + originalSettings.copyDebug = params.copyDebug; + originalSettings.debug = params.debug; + originalSettings.debugDatagrams = params.debugDatagrams; + originalSettings.debugNvm = params.debugNvm; originalSettings.debugRadio = params.debugRadio; + originalSettings.debugReceive = params.debugReceive; originalSettings.debugStates = params.debugStates; originalSettings.debugTraining = params.debugTraining; - originalSettings.printRfData = params.printRfData; - originalSettings.printPktData = params.printPktData; - originalSettings.debugReceive = params.debugReceive; originalSettings.debugTransmit = params.debugTransmit; - originalSettings.printTxErrors = params.printTxErrors; - originalSettings.printTimestamp = params.printTimestamp; - originalSettings.debugDatagrams = params.debugDatagrams; + originalSettings.debugSerial = params.debugSerial; + originalSettings.displayPacketQuality = params.displayPacketQuality; originalSettings.displayRealMillis = params.displayRealMillis; + originalSettings.printAckNumbers = params.printAckNumbers; + originalSettings.printFrequency = params.printFrequency; + originalSettings.printLinkUpDown = params.printLinkUpDown; + originalSettings.printPktData = params.printPktData; + originalSettings.printRfData = params.printRfData; + originalSettings.printTimestamp = params.printTimestamp; + originalSettings.printTxErrors = params.printTxErrors; } //Update the serial parameters if (params.copySerial) { - originalSettings.serialSpeed = params.serialSpeed; + originalSettings.copySerial = params.copySerial; originalSettings.echo = params.echo; originalSettings.flowControl = params.flowControl; + originalSettings.invertCts = params.invertCts; + originalSettings.invertRts = params.invertRts; + originalSettings.serialSpeed = params.serialSpeed; originalSettings.usbSerialWait = params.usbSerialWait; - originalSettings.printLinkUpDown = params.printLinkUpDown; } + //Update the training values + originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; + //The trainingKey is already the same + originalSettings.trainingServer = false; + originalSettings.trainingTimeout = params.trainingTimeout; + //Update the trigger parameters if (params.copyTriggers) { - originalSettings.triggerWidth = params.triggerWidth; - originalSettings.triggerWidthIsMultiplier = params.triggerWidthIsMultiplier; + originalSettings.copyTriggers = params.copyTriggers; originalSettings.triggerEnable = params.triggerEnable; originalSettings.triggerEnable2 = params.triggerEnable2; + originalSettings.triggerWidth = params.triggerWidth; + originalSettings.triggerWidthIsMultiplier = params.triggerWidthIsMultiplier; } } diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 9c94b9d2..0e5febfb 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -182,28 +182,32 @@ void commonTrainingInitialization() //Debug training if requested if (originalSettings.debugTraining) { + settings.alternateLedUsage = originalSettings.alternateLedUsage; + //Ignore copyDebug settings.debug = originalSettings.debug; - settings.displayPacketQuality = originalSettings.displayPacketQuality; - settings.printFrequency = originalSettings.printFrequency; - + settings.debugDatagrams = originalSettings.debugDatagrams; + settings.debugNvm = originalSettings.debugNvm; settings.debugRadio = originalSettings.debugRadio; + settings.debugReceive = originalSettings.debugReceive; settings.debugStates = originalSettings.debugStates; settings.debugTraining = originalSettings.debugTraining; - - settings.printRfData = originalSettings.printRfData; + settings.debugTransmit = originalSettings.debugTransmit; + settings.debugSerial = originalSettings.debugSerial; + settings.displayPacketQuality = originalSettings.displayPacketQuality; + settings.displayRealMillis = originalSettings.displayRealMillis; + settings.printAckNumbers = originalSettings.printAckNumbers; + settings.printFrequency = originalSettings.printFrequency; + settings.printLinkUpDown = originalSettings.printLinkUpDown; settings.printPktData = originalSettings.printPktData; - settings.triggerWidth = originalSettings.triggerWidth; - settings.triggerWidthIsMultiplier = originalSettings.triggerWidthIsMultiplier; + settings.printRfData = originalSettings.printRfData; + settings.printTimestamp = originalSettings.printTimestamp; + settings.printTxErrors = originalSettings.printTxErrors; + //Ignore copyTriggers settings.triggerEnable = originalSettings.triggerEnable; settings.triggerEnable2 = originalSettings.triggerEnable2; - settings.debugReceive = originalSettings.debugReceive; - settings.debugTransmit = originalSettings.debugTransmit; - settings.printTxErrors = originalSettings.printTxErrors; - - settings.printTimestamp = originalSettings.printTimestamp; - settings.debugDatagrams = originalSettings.debugDatagrams; - settings.displayRealMillis = originalSettings.displayRealMillis; + settings.triggerWidth = originalSettings.triggerWidth; + settings.triggerWidthIsMultiplier = originalSettings.triggerWidthIsMultiplier; } //Reset cylon variables From fd6d5624def3bd235d34e0f0ae58f51b1cabffaa Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 21 Nov 2022 11:10:06 -0800 Subject: [PATCH 164/594] Merge multipointServer and trainingServer into server --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 ++--- Firmware/LoRaSerial_Firmware/RadioV2.ino | 5 ++--- Firmware/LoRaSerial_Firmware/States.ino | 14 +++++++------- Firmware/LoRaSerial_Firmware/Train.ino | 6 +++--- Firmware/LoRaSerial_Firmware/settings.h | 3 +-- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9027f2d7..53de5935 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -153,7 +153,7 @@ bool commandAT(const char * commandString) selectTraining(false); break; case ('X'): //Stop the training server - if (trainingServerRunning && settings.trainingServer + if (trainingServerRunning && settings.server && (settings.operatingMode == MODE_DATAGRAM)) { endClientServerTraining(TRIGGER_TRAINING_SERVER_STOPPED); @@ -750,10 +750,10 @@ const COMMAND_ENTRY commands[] = {'R', 0, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &settings.serialTimeoutBeforeSendingFrame_ms}, {'R', 0, 250, 65535, 0, TYPE_U16, valInt, "HeartBeatTimeout", &settings.heartbeatTimeout}, {'R', 0, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &settings.maxResends}, - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "MultipointServer", &settings.multipointServer}, {'R', 0, 0, 255, 0, TYPE_U8, valInt, "NetID", &settings.netID}, {'R', 0, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &settings.operatingMode}, {'R', 0, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, + {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "Server", &settings.server}, {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &settings.verifyRxNetID}, /*Serial parameters @@ -770,7 +770,6 @@ const COMMAND_ENTRY commands[] = Ltr, All, min, max, digits, type, validation, name, setting addr */ {'R', 0, 1, 255, 0, TYPE_U8, valInt, "ClientPingRetryInterval", &settings.clientPingRetryInterval}, {'R', 0, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &settings.trainingKey}, - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "TrainingServer", &settings.trainingServer}, {'R', 0, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &settings.trainingTimeout}, /*Trigger parameters diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 4b083996..8c27f678 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -498,10 +498,10 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; originalSettings.heartbeatTimeout = params.heartbeatTimeout; originalSettings.maxResends = params.maxResends; - originalSettings.multipointServer = params.multipointServer; originalSettings.netID = params.netID; originalSettings.operatingMode = params.operatingMode; originalSettings.overheadTime = params.overheadTime; + originalSettings.server = params.server; originalSettings.verifyRxNetID = params.verifyRxNetID; //Update the debug parameters @@ -545,7 +545,6 @@ void updateRadioParameters(uint8_t * rxData) //Update the training values originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; //The trainingKey is already the same - originalSettings.trainingServer = false; originalSettings.trainingTimeout = params.trainingTimeout; //Update the trigger parameters @@ -571,7 +570,7 @@ bool xmitDatagramMpRadioParameters(const uint8_t * clientID) //Initialize the radio parameters memcpy(¶ms, &originalSettings, sizeof(settings)); params.operatingMode = MODE_DATAGRAM; - params.trainingServer = false; + params.server = false; //Add the destination (client) ID memcpy(endOfTxData, clientID, UNIQUE_ID_BYTES); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d1cb8de2..e81a298a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -94,7 +94,7 @@ void updateRadioState() changeState(RADIO_P2P_LINK_DOWN); else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { - if (settings.trainingServer) + if (settings.server) //Reserve the server's address (0) myVc = vcIdToAddressByte(VC_SERVER, myUniqueId); else @@ -107,7 +107,7 @@ void updateRadioState() } else { - if (settings.multipointServer == true) + if (settings.server == true) { startChannelTimer(); //Start hopping changeState(RADIO_MP_STANDBY); @@ -1226,7 +1226,7 @@ void updateRadioState() case DATAGRAM_PING: //A new radio is saying hello - if (settings.multipointServer == true) + if (settings.server == true) { //Ack their ping with sync data if (xmitDatagramMpAck() == true) @@ -1291,7 +1291,7 @@ void updateRadioState() } //Only the server transmits heartbeats - else if (settings.multipointServer == true) + else if (settings.server == true) { if (heartbeatTimeout) { @@ -1305,7 +1305,7 @@ void updateRadioState() } //If the client hasn't received a packet in too long, return to scanning - else if (settings.multipointServer == false) + else if (settings.server == false) { if ((millis() - lastPacketReceived) > (settings.heartbeatTimeout * 3)) { @@ -2291,7 +2291,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) vc = &virtualCircuitList[index]; //Only the server can assign the address bytes - if ((srcAddr == VC_UNASSIGNED) && (!settings.trainingServer)) + if ((srcAddr == VC_UNASSIGNED) && (!settings.server)) return -1; //Assign an address if necessary @@ -2366,7 +2366,7 @@ void vcReceiveHeartbeat(RadioStates nextState, uint32_t rxMillis) //Translate the unique ID into an address byte vcSrc = vcIdToAddressByte(rxSrcVc, rxVcData); - if (settings.trainingServer && (rxSrcVc == VC_UNASSIGNED) && (vcSrc >= 0)) + if (settings.server && (rxSrcVc == VC_UNASSIGNED) && (vcSrc >= 0)) { //Assign the address to the client xmitVcHeartbeat(vcSrc, rxVcData); diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 0e5febfb..a517bffd 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -5,7 +5,7 @@ void selectTraining(bool defaultTraining) beginTrainingPointToPoint(defaultTraining); else { - if (settings.trainingServer) + if (settings.server) beginTrainingServer(); else beginTrainingClient(); @@ -150,7 +150,7 @@ void beginTrainingServer() //Common initialization commonTrainingInitialization(); - settings.trainingServer = true; //52: Operate as the training server + settings.server = true; //52: Operate as the training server //Start the receive operation returnToReceiving(); @@ -242,7 +242,7 @@ void endClientServerTraining(uint8_t event) outputSerialData(true); } - if (!settings.trainingServer) + if (!settings.server) { //Record the new client settings petWDT(); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index de6680ae..8f343df7 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -409,7 +409,7 @@ typedef struct struct_settings { uint16_t overheadTime = 10; //ms added to ack and datagram times before ACK timeout occurs bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive bool displayRealMillis = false; //true = Display the millis value without offset, false = use offset - bool trainingServer = false; //Default to being a client + bool server = false; //Default to being a client, enable server for multipoint, VC and training uint8_t clientPingRetryInterval = 3; //Number of seconds before retransmiting the client PING bool copyDebug = true; //Copy the debug parameters to the training client bool copySerial = true; //Copy the serial parameters to the training client @@ -420,7 +420,6 @@ typedef struct struct_settings { bool invertRts = false; //Invert the output of RTS bool alternateLedUsage = false; //Enable alternate LED usage uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training - bool multipointServer = false; //Only one radio can be the server in multipoint mode bool debugSerial = false; //Debug the serial input bool debugSync = false; //Print clock sync processing bool debugNvm = false; //Debug NVM operation From fa990358f39ecc7f5b1f1cd9b7d94e0d1eb71090 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 21 Nov 2022 11:30:34 -0800 Subject: [PATCH 165/594] Remove point-to-point training Use client/server training for all training cases to ensure radio parameters are set to known values. This commit makes the following changes: * Remove ATF command and long button hold for default factory setting training * Remove ATX command, use ATZ to exit server training * Don't force MODE_DATAGRAM for clients, support P2P training * Ensure that the training state is entered * Save server settings used for training to ensure proper operation after training * Display radio settings for netID and encryption key * Don't copy debug, serial or trigger values by default. --- Firmware/LoRaSerial_Firmware/Commands.ino | 18 +-- .../LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/RadioV2.ino | 1 - Firmware/LoRaSerial_Firmware/States.ino | 125 ------------------ Firmware/LoRaSerial_Firmware/System.ino | 29 +--- Firmware/LoRaSerial_Firmware/Train.ino | 75 +++-------- Firmware/LoRaSerial_Firmware/settings.h | 22 +-- 7 files changed, 30 insertions(+), 241 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 53de5935..71c46cc6 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -52,7 +52,6 @@ bool commandAT(const char * commandString) systemPrintln(" AT? - Print the command summary"); systemPrintln(" ATB - Break the link"); systemPrintln(" ATD - Display the debug settings"); - systemPrintln(" ATF - Enter training mode and return to factory defaults"); systemPrintln(" ATG - Generate new netID and encryption key"); systemPrintln(" ATI - Display the radio version"); systemPrintln(" ATI? - Display the information commands"); @@ -63,7 +62,6 @@ bool commandAT(const char * commandString) systemPrintln(" ATS - Display the serial settings"); systemPrintln(" ATT - Enter training mode"); systemPrintln(" ATV - Display virtual circuit settings"); - systemPrintln(" ATX - Stop the training server"); systemPrintln(" ATZ - Reboot the radio"); systemPrintln(" AT-Param=xxx - Set parameter's value to xxx by name (Param)"); systemPrintln(" AT-Param? - Print parameter's current value by name (Param)"); @@ -112,10 +110,6 @@ bool commandAT(const char * commandString) outputSerialData(true); } break; - case ('F'): //Enter training mode and return to factory defaults - reportOK(); - selectTraining(true); - break; case ('G'): //Generate a new netID and encryption key generateTrainingSettings(); reportOK(); @@ -150,17 +144,7 @@ bool commandAT(const char * commandString) break; case ('T'): //Enter training mode reportOK(); - selectTraining(false); - break; - case ('X'): //Stop the training server - if (trainingServerRunning && settings.server - && (settings.operatingMode == MODE_DATAGRAM)) - { - endClientServerTraining(TRIGGER_TRAINING_SERVER_STOPPED); - reportOK(); - } - else - reportERROR(); + selectTraining(); break; case ('Z'): //Reboots the radio reportOK(); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index cae3fe4b..e352aad4 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -126,7 +126,6 @@ uint8_t AESiv[AES_IV_BYTES] = {0}; //Set during hop table generation Button *trainBtn = NULL; //We can't instantiate the button here because we don't yet know what pin number to use const int trainButtonTime = 2000; //ms press and hold before entering training -const int trainWithDefaultsButtonTime = 5000; //ms press and hold before entering training //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Hardware Timers diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 8c27f678..90ea57a5 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -569,7 +569,6 @@ bool xmitDatagramMpRadioParameters(const uint8_t * clientID) //Initialize the radio parameters memcpy(¶ms, &originalSettings, sizeof(settings)); - params.operatingMode = MODE_DATAGRAM; params.server = false; //Add the destination (client) ID diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index e81a298a..58e97874 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -117,131 +117,6 @@ void updateRadioState() } break; - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V2 - Point-to-Point Training - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - - /* - beginTrainingPointToPoint - | - | Save settings - | - | TX DATAGRAM_P2P_TRAINING_PING - | - V - RADIO_P2P_TRAINING_WAIT_PING_DONE - | - V RX DATAGRAM_P2P_TRAINING_PARAMS - RADIO_P2P_WAIT_FOR_TRAINING_PARAMS -----------. - | | - | RX DATAGRAM_P2P_TRAINING_PING | - | TX DATAGRAM_P2P_TRAINING_PARAMS | - | | - V | - RADIO_P2P_WAIT_TRAINING_ACK_DONE | - | | - +<------------------------------------’ - | - V - RADIO_RESET - */ - - //Wait for the PING to complete transmission - case RADIO_P2P_TRAINING_WAIT_PING_DONE: - updateCylonLEDs(); - if (transactionComplete) - { - transactionComplete = false; //Reset ISR flag - returnToReceiving(); - changeState(RADIO_P2P_WAIT_FOR_TRAINING_PARAMS); - } - break; - - case RADIO_P2P_WAIT_FOR_TRAINING_PARAMS: - updateCylonLEDs(); - - //Check for a received datagram - if (transactionComplete == true) - { - transactionComplete = false; //Reset ISR flag - - //Decode the received datagram - PacketType packetType = rcvDatagram(); - - //Process the received datagram - switch (packetType) - { - default: - triggerEvent(TRIGGER_BAD_PACKET); - break; - - case DATAGRAM_P2P_TRAINING_PING: - printPacketQuality(); - - triggerEvent(TRIGGER_TRAINING_CONTROL_PACKET); - - //Send the parameters - xmitDatagramP2pTrainingParams(); - changeState(RADIO_P2P_WAIT_TRAINING_PARAMS_DONE); - break; - - case DATAGRAM_P2P_TRAINING_PARAMS: - triggerEvent(TRIGGER_TRAINING_DATA_PACKET); - - //Update the parameters - updateRadioParameters(rxData); - endPointToPointTraining(true); - if (settings.debugTraining) - { - systemPrintln("Training successful, received parameters!"); - outputSerialData(true); - } - changeState(RADIO_RESET); - } - } - - //If the radio is available, send any data in the serial buffer over the radio - else if (receiveInProcess() == false) - { - //Check for a receive timeout - if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) - { - triggerEvent(TRIGGER_TRAINING_NO_ACK); - retransmitDatagram(NULL); - lostFrames++; - changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); - } - - //Check for done the training - else if ((millis() - trainingTimer) > (settings.trainingTimeout * 60 * 1000)) - { - //Failed to complete the training - if (settings.debugTraining) - { - systemPrintln("Training timeout, returning to previous mode!"); - outputSerialData(true); - } - endPointToPointTraining(false); - changeState(RADIO_RESET); - } - } - break; - - case RADIO_P2P_WAIT_TRAINING_PARAMS_DONE: - updateCylonLEDs(); - if (transactionComplete) - { - transactionComplete = false; //Reset ISR flag - endPointToPointTraining(false); - if (settings.debugTraining) - { - systemPrintln("Training successful, sent parameters!"); - outputSerialData(true); - } - changeState(RADIO_RESET); - } - break; - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //V2 - No Link //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 688e5b33..7a8147a8 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -217,6 +217,8 @@ uint8_t systemRead() //Check the train button and change state accordingly void updateButton() { + static TrainStates trainState = TRAIN_NO_PRESS; + if (trainBtn != NULL) { trainBtn->read(); @@ -230,19 +232,7 @@ void updateButton() { setRSSI(0b1111); - selectTraining(false); - - trainState = TRAIN_NO_PRESS; - } - else if (trainState == TRAIN_PRESSED_2S && trainBtn->pressedFor(trainWithDefaultsButtonTime)) - { - trainState = TRAIN_PRESSED_5S; - } - else if (trainState == TRAIN_PRESSED_5S && trainBtn->wasReleased()) - { - setRSSI(0b1111); - - selectTraining(true); + selectTraining(); trainState = TRAIN_NO_PRESS; } @@ -255,19 +245,6 @@ void updateButton() Serial.println("Train Blinking LEDs"); lastTrainBlink = millis(); - //Toggle RSSI LEDs - if (digitalRead(pin_rssi1LED) == HIGH) - setRSSI(0); - else - setRSSI(0b1111); - } - } - else if (trainState == TRAIN_PRESSED_5S) - { - if (millis() - lastTrainBlink > 100) //Fast blink - { - lastTrainBlink = millis(); - //Toggle RSSI LEDs if (digitalRead(pin_rssi1LED) == HIGH) setRSSI(0); diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index a517bffd..de7c5b69 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -1,15 +1,10 @@ //Select the training protocol -void selectTraining(bool defaultTraining) +void selectTraining() { - if (settings.operatingMode == MODE_POINT_TO_POINT) - beginTrainingPointToPoint(defaultTraining); + if (settings.server) + beginTrainingServer(); else - { - if (settings.server) - beginTrainingServer(); - else - beginTrainingClient(); - } + beginTrainingClient(); } //Generate new netID/AES key to share @@ -78,42 +73,6 @@ void updateCylonLEDs() } } -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//V2 Point-To-Point Training -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -void beginTrainingPointToPoint(bool defaultTraining) -{ - if (defaultTraining) - { - settings = defaultSettings; //Upon completion we will return to default settings - systemPrint("Default "); - } - systemPrintln("Point-to-point training"); - - //Generate new netID and encryption key - generateTrainingSettings(); - - //Common initialization - commonTrainingInitialization(); - - //Transmit general ping packet to see if anyone else is sitting on the training channel - if (xmitDatagramP2PTrainingPing() == true) - { - trainingTimer = millis(); - - //Set the next state - changeState(RADIO_P2P_TRAINING_WAIT_PING_DONE); - } -} - -void endPointToPointTraining(bool saveParams) -{ - memcpy(&settings, &originalSettings, sizeof(settings)); - if (saveParams) - recordSystemSettings(); -} - //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //V2 Multi-Point Client/Server Training //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -127,22 +86,24 @@ void beginTrainingClient() //Transmit client ping to the training server if (xmitDatagramMpTrainingPing() == true) - { - trainingTimer = millis(); - //Set the next state changeState(RADIO_MP_WAIT_TX_TRAINING_PING_DONE); - } + else + changeState(RADIO_MP_WAIT_RX_RADIO_PARAMETERS); + trainingTimer = millis(); } void beginTrainingServer() { trainingServerRunning = true; + //Record the settings used for training + petWDT(); + recordSystemSettings(); + //Display the values to be used for the client/server training - systemPrintln("Multipoint server training"); - systemPrintln("Using:"); - systemPrint(" netID: "); + systemPrintln("Server training parameters"); + systemPrint(" Training netID: "); systemPrintln(settings.netID); systemPrint(" Training key: "); displayEncryptionKey(settings.trainingKey); @@ -152,6 +113,14 @@ void beginTrainingServer() commonTrainingInitialization(); settings.server = true; //52: Operate as the training server + systemPrintln("Server radio protocol parameters"); + systemPrint(" netID: "); + systemPrintln(originalSettings.netID); + systemPrint(" Encryption key: "); + displayEncryptionKey(originalSettings.encryptionKey); + systemPrintln(); + outputSerialData(true); + //Start the receive operation returnToReceiving(); @@ -189,10 +158,10 @@ void commonTrainingInitialization() settings.debugNvm = originalSettings.debugNvm; settings.debugRadio = originalSettings.debugRadio; settings.debugReceive = originalSettings.debugReceive; + settings.debugSerial = originalSettings.debugSerial; settings.debugStates = originalSettings.debugStates; settings.debugTraining = originalSettings.debugTraining; settings.debugTransmit = originalSettings.debugTransmit; - settings.debugSerial = originalSettings.debugSerial; settings.displayPacketQuality = originalSettings.displayPacketQuality; settings.displayRealMillis = originalSettings.displayRealMillis; settings.printAckNumbers = originalSettings.printAckNumbers; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 8f343df7..239eeb7e 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -2,12 +2,6 @@ typedef enum { RADIO_RESET = 0, - //V2 - //Point-to-Point Training - RADIO_P2P_TRAINING_WAIT_PING_DONE, - RADIO_P2P_WAIT_FOR_TRAINING_PARAMS, - RADIO_P2P_WAIT_TRAINING_PARAMS_DONE, - //Point-To-Point: Bring up the link RADIO_P2P_LINK_DOWN, RADIO_P2P_WAIT_TX_PING_DONE, @@ -65,12 +59,6 @@ const RADIO_STATE_ENTRY radioStateTable[] = { {RADIO_RESET, 0, "RESET", NULL}, // 0 - //V2 - Point-to-Point Training - // State RX Name Description - {RADIO_P2P_TRAINING_WAIT_PING_DONE, 0, "P2P_TRAINING_WAIT_PING_DONE", "V2 P2P: Wait TX Training Ping Done"}, // 1 - {RADIO_P2P_WAIT_FOR_TRAINING_PARAMS, 1, "P2P_WAIT_FOR_TRAINING_PARAMS", "V2 P2P: Wait for Training params"}, // 2 - {RADIO_P2P_WAIT_TRAINING_PARAMS_DONE, 0, "P2P_WAIT_TRAINING_PARAMS_DONE", "V2 P2P: Wait training params done"}, // 3 - //V2 - Point-to-Point link handshake // State RX Name Description {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "V2 P2P: [No Link] Waiting for Ping"}, // 4 @@ -234,10 +222,8 @@ typedef struct _VIRTUAL_CIRCUIT typedef enum { TRAIN_NO_PRESS = 0, - TRAIN_PRESSED_2S, - TRAIN_PRESSED_5S, + TRAIN_PRESSED_2S } TrainStates; -TrainStates trainState = TRAIN_NO_PRESS; enum { //#, Width - Computed with: @@ -411,9 +397,9 @@ typedef struct struct_settings { bool displayRealMillis = false; //true = Display the millis value without offset, false = use offset bool server = false; //Default to being a client, enable server for multipoint, VC and training uint8_t clientPingRetryInterval = 3; //Number of seconds before retransmiting the client PING - bool copyDebug = true; //Copy the debug parameters to the training client - bool copySerial = true; //Copy the serial parameters to the training client - bool copyTriggers = true; //Copy the trigger parameters to the training client + bool copyDebug = false; //Copy the debug parameters to the training client + bool copySerial = false; //Copy the serial parameters to the training client + bool copyTriggers = false; //Copy the trigger parameters to the training client uint8_t trainingKey[AES_KEY_BYTES] = { 0x53, 0x70, 0x61, 0x72, 0x6b, 0x46, 0x75, 0x6E, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67 }; bool printLinkUpDown = false; //Print the link up and link down messages bool invertCts = false; //Invert the input of CTS From 0b5e98d03c4e317eff4656f7a04f5f1d53835bc1 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 21 Nov 2022 15:57:53 -0700 Subject: [PATCH 166/594] Implement variable packets for SF6 Reduce ACK/ACK1/ACK2/PING from 255 to variable 5/7/7/7 bytes. --- .../LoRaSerial_Firmware.ino | 5 +- Firmware/LoRaSerial_Firmware/Radio.ino | 33 +-- Firmware/LoRaSerial_Firmware/RadioV2.ino | 52 ++++- Firmware/LoRaSerial_Firmware/States.ino | 221 ++++++++++++------ Firmware/LoRaSerial_Firmware/settings.h | 2 +- 5 files changed, 219 insertions(+), 94 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index cae3fe4b..32240808 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -56,7 +56,8 @@ const int FIRMWARE_VERSION_MINOR = 0; // the minor firmware version #define LRS_IDENTIFIER (FIRMWARE_VERSION_MAJOR * 0x10 + FIRMWARE_VERSION_MINOR) -#define ACK_BYTES 2 //Length of the ACK in bytes +#define CLOCK_SYNC_BYTES sizeof(uint16_t) //Number of bytes used within in ACK packet for clock sync (uint16_t msToNextHop) +#define CLOCK_MILLIS_BYTES sizeof(unsigned long) //Number of bytes used within in various packets for system timestamps sync (unsigned long currentMillis) #define MAX_PACKET_SIZE 255 //Limited by SX127x #define AES_IV_BYTES 12 //Number of bytes for AESiv #define AES_KEY_BYTES 16 //Number of bytes in the encryption key @@ -339,7 +340,7 @@ unsigned long lastLinkBlink = 0; //Controls link LED in broadcast mode volatile bool transactionComplete = false; //Used in dio0ISR volatile bool timeToHop = false; //Used in dio1ISR -bool expectingAck = false; //Used by various send packet functions +uint8_t sf6ExpectedSize = MAX_PACKET_SIZE; //Used during SF6 operation to reduce packet size when needed float frequencyCorrection = 0; //Adjust receive freq based on the last packet received freqError diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4625d39c..e851d1e2 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -72,9 +72,7 @@ void configureRadio() success = false; //Precalculate the ACK packet time - ackAirTime = calcAirTime(headerBytes + ACK_BYTES + trailerBytes); //Used for response timeout during ACK - if (settings.radioSpreadFactor == 6) - ackAirTime = calcAirTime(MAX_PACKET_SIZE); + ackAirTime = calcAirTime(headerBytes + CLOCK_SYNC_BYTES + trailerBytes); //Used for response timeout during ACK if ((settings.debug == true) || (settings.debugRadio == true)) { @@ -200,8 +198,8 @@ void setRadioFrequency(bool rxAdjust) void returnToReceiving() { - if(receiveInProcess() == true) return; //Do not touch the radio if it is already receiving - + if (receiveInProcess() == true) return; //Do not touch the radio if it is already receiving + int state; if (settings.radioSpreadFactor > 6) { @@ -209,18 +207,21 @@ void returnToReceiving() } else { - if (expectingAck && (settings.operatingMode == MODE_POINT_TO_POINT)) - { - radio.implicitHeader(2); - state = radio.startReceive(2); //Expect a control packet - triggerEvent(TRIGGER_RTR_2BYTE); - expectingAck = false; //Do not return to this receiving configuration if something goes wrong - } - else + if (settings.operatingMode == MODE_POINT_TO_POINT) { - radio.implicitHeader(MAX_PACKET_SIZE); - state = radio.startReceive(MAX_PACKET_SIZE); //Expect a full data packet - triggerEvent(TRIGGER_RTR_255BYTE); + + radio.implicitHeader(sf6ExpectedSize); + radio.setCRC(true); + + state = radio.startReceive(sf6ExpectedSize); //Set the size we expect to see + radio.setCRC(true); + + if (sf6ExpectedSize < MAX_PACKET_SIZE) + triggerEvent(TRIGGER_RTR_SHORT_PACKET); + else + triggerEvent(TRIGGER_RTR_255BYTE); + + sf6ExpectedSize = MAX_PACKET_SIZE; //Always return to expecing a full data packet } } diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 27598424..14be05f1 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -298,9 +298,9 @@ bool xmitDatagramP2PAck() //Verify the ACK length ackLength = endOfTxData - ackStart; - if (ackLength != ACK_BYTES) + if (ackLength != CLOCK_SYNC_BYTES) { - systemPrint("ERROR - Please define ACK_BYTES = "); + systemPrint("ERROR - Please define CLOCK_SYNC_BYTES = "); systemPrintln(ackLength); waitForever(); } @@ -657,6 +657,7 @@ PacketType rcvDatagram() if (settings.debug == true) systemPrintln("Receive CRC error!"); badCrc++; + returnToReceiving(); //Return to listening return (DATAGRAM_CRC_ERROR); } else @@ -666,6 +667,7 @@ PacketType rcvDatagram() systemPrint("Receive error: "); systemPrintln(state); } + returnToReceiving(); //Return to listening return (DATAGRAM_BAD); } @@ -717,6 +719,12 @@ PacketType rcvDatagram() hopChannel(); } + if (settings.debugTransmit) + { + systemPrint("in: "); + dumpBufferRaw(incomingBuffer, 14); //Print only the first few bytes when debugging packets + } + //Display the received data bytes if ((settings.dataScrambling || settings.encryptData) && (settings.printRfData || settings.debugReceive)) @@ -905,7 +913,7 @@ PacketType rcvDatagram() if (settings.debugReceive) { systemPrintTimestamp(); - systemPrint("Invalid SF6 length, received SF6 length"); + systemPrint("Invalid SF6 length, received SF6 length "); systemPrint(*rxData); systemPrint(" > "); systemPrint((int)rxDataBytes - minDatagramSize); @@ -1346,16 +1354,40 @@ bool transmitDatagram() if (settings.radioSpreadFactor == 6) { *header++ = length; - txDatagramSize = MAX_PACKET_SIZE - trailerBytes; //We're now going to transmit a full size datagram + + //Send either a short ACK or full length packet + switch (txControl.datagramType) + { + default: + txDatagramSize = MAX_PACKET_SIZE - trailerBytes; //We're now going to transmit a full size datagram + break; + + case DATAGRAM_PING: + case DATAGRAM_ACK_1: + case DATAGRAM_ACK_2: + txDatagramSize = headerBytes + CLOCK_MILLIS_BYTES; //Short packet is 3 + 4 + break; + + case DATAGRAM_DATA_ACK: + txDatagramSize = headerBytes + CLOCK_SYNC_BYTES; //Short ACK packet is 3 + 2 + break; + } + + + radio.implicitHeader(txDatagramSize); //Set header size so that hardware CRC is calculated correctly + endOfTxData = &outgoingPacket[txDatagramSize]; if (settings.debugTransmit) { systemPrintTimestamp(); systemPrint(" SF6 Length: "); systemPrintln(length); + systemPrint(" SF6 TX Header Size: "); + systemPrintln(txDatagramSize); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } + } //Verify the Virtual-Circuit length @@ -1463,6 +1495,13 @@ bool transmitDatagram() dumpBuffer(outgoingPacket, txDatagramSize); } + //Print before encryption + if (settings.debugTransmit) + { + systemPrint("out: "); + dumpBufferRaw(outgoingPacket, 14); + } + //Encrypt the datagram if (settings.encryptData == true) { @@ -1684,11 +1723,10 @@ void syncChannelTimer() case (19200): break; case (28800): - msToNextHopRemote -= 17; + msToNextHopRemote -= 2; break; case (38400): - //msToNextHopRemote -= 0; //Unit sending HB is 16ms behind - msToNextHopRemote -= 16; //Unit sending HB is + msToNextHopRemote -= 3; break; } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 69022970..c0b62ff6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -81,6 +81,8 @@ void updateRadioState() setHeartbeatShort(); //Both radios start with short heartbeat period pingRandomTime = random(ackAirTime, ackAirTime * 2); //Fast ping + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive PING packet + petWDT(); returnToReceiving(); //Start receiving @@ -306,22 +308,48 @@ void updateRadioState() //Decode the received packet PacketType packetType = rcvDatagram(); - if (packetType == DATAGRAM_PING) + + //Process the received datagram + switch (packetType) { - //Received PING - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; - - //Acknowledge the PING - triggerEvent(TRIGGER_SEND_ACK1); - if (xmitDatagramP2PAck1() == true) - changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); + default: + triggerEvent(TRIGGER_UNKNOWN_PACKET); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Scan: Unhandled packet type "); + systemPrint(v2DatagramType[packetType]); + systemPrintln(); + } + break; + + case DATAGRAM_BAD: + triggerEvent(TRIGGER_BAD_PACKET); + break; + + case DATAGRAM_CRC_ERROR: + triggerEvent(TRIGGER_CRC_ERROR); + break; + + case DATAGRAM_PING: + //Received PING + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; + + //Acknowledge the PING + triggerEvent(TRIGGER_SEND_ACK1); + if (xmitDatagramP2PAck1() == true) + { + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK2 to contain millis info + changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); + } + break; } } @@ -331,7 +359,10 @@ void updateRadioState() //Transmit the PING triggerEvent(TRIGGER_HANDSHAKE_SEND_PING); if (xmitDatagramP2PPing() == true) + { + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK1 to contain millis info changeState(RADIO_P2P_WAIT_TX_PING_DONE); + } } break; @@ -353,39 +384,68 @@ void updateRadioState() //Decode the received packet PacketType packetType = rcvDatagram(); - if (packetType == DATAGRAM_PING) - { - //Received PING - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; - - //Acknowledge the PING - triggerEvent(TRIGGER_SEND_ACK1); - if (xmitDatagramP2PAck1() == true) - changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); - } - else if (packetType == DATAGRAM_ACK_1) + + //Process the received datagram + switch (packetType) { - //Received ACK 1 - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; - - //Acknowledge the ACK1 - triggerEvent(TRIGGER_SEND_ACK2); - if (xmitDatagramP2PAck2() == true) - changeState(RADIO_P2P_WAIT_TX_ACK_2_DONE); + default: + triggerEvent(TRIGGER_UNKNOWN_PACKET); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Scan: Unhandled packet type "); + systemPrint(v2DatagramType[packetType]); + systemPrintln(); + } + break; + + case DATAGRAM_BAD: + triggerEvent(TRIGGER_BAD_PACKET); + break; + + case DATAGRAM_CRC_ERROR: + triggerEvent(TRIGGER_CRC_ERROR); + break; + + case DATAGRAM_PING: + //Received PING + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; + + //Acknowledge the PING + triggerEvent(TRIGGER_SEND_ACK1); + if (xmitDatagramP2PAck1() == true) + { + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK2 to contain millis info + changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); + } + break; + + case DATAGRAM_ACK_1: + //Received ACK 1 + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; + + //Acknowledge the ACK1 + triggerEvent(TRIGGER_SEND_ACK2); + if (xmitDatagramP2PAck2() == true) + { + sf6ExpectedSize = MAX_PACKET_SIZE; //Tell SF6 to return to max packet length + changeState(RADIO_P2P_WAIT_TX_ACK_2_DONE); + } + break; } } else @@ -425,24 +485,47 @@ void updateRadioState() //Decode the received packet PacketType packetType = rcvDatagram(); - if (packetType == DATAGRAM_ACK_2) + + //Process the received datagram + switch (packetType) { - //Received ACK 2 - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; - - startChannelTimer(getLinkupOffset()); //We are exiting the link last so adjust our starting Timer - - setHeartbeatLong(); //We sent ACK1 and they sent ACK2, so don't be the first to send heartbeat - - //Bring up the link - v2EnterLinkUp(); + default: + triggerEvent(TRIGGER_UNKNOWN_PACKET); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Scan: Unhandled packet type "); + systemPrint(v2DatagramType[packetType]); + systemPrintln(); + } + break; + + case DATAGRAM_BAD: + triggerEvent(TRIGGER_BAD_PACKET); + break; + + case DATAGRAM_CRC_ERROR: + triggerEvent(TRIGGER_CRC_ERROR); + break; + + case DATAGRAM_ACK_2: + //Received ACK 2 + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; + + startChannelTimer(getLinkupOffset()); //We are exiting the link last so adjust our starting Timer + + setHeartbeatLong(); //We sent ACK1 and they sent ACK2, so don't be the first to send heartbeat + + //Bring up the link + v2EnterLinkUp(); + break; } } else @@ -773,6 +856,8 @@ void updateRadioState() if (transactionComplete) { + sf6ExpectedSize = headerBytes + CLOCK_SYNC_BYTES + trailerBytes; //Tell SF6 to receive ACK packet + triggerEvent(TRIGGER_LINK_WAIT_FOR_ACK); transactionComplete = false; //Reset ISR flag returnToReceiving(); @@ -966,6 +1051,8 @@ void updateRadioState() //completes transmission, retransmit the previously lost datagram. if (transactionComplete) { + transactionComplete = false; //Reset ISR flag + //Retransmit the packet if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { @@ -1000,8 +1087,6 @@ void updateRadioState() if (retransmitDatagram(NULL) == true) { - transactionComplete = false; //Reset ISR flag - setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat lostFrames++; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); @@ -1628,7 +1713,7 @@ void updateRadioState() //Acknowledge the data frame vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; - vcHeader->length = VC_RADIO_HEADER_BYTES + ACK_BYTES; + vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; vcHeader->destVc = rxSrcVc; vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; @@ -1731,7 +1816,7 @@ void updateRadioState() //Acknowledge the data frame vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; - vcHeader->length = VC_RADIO_HEADER_BYTES + ACK_BYTES; + vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; vcHeader->destVc = rxSrcVc; vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 3b19b107..368ee53e 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -281,7 +281,7 @@ enum TRIGGER_BAD_PACKET, TRIGGER_CRC_ERROR, TRIGGER_NETID_MISMATCH, - TRIGGER_RTR_2BYTE, + TRIGGER_RTR_SHORT_PACKET, TRIGGER_RTR_255BYTE, TRIGGER_TRAINING_CONTROL_PACKET, TRIGGER_TRAINING_DATA_PACKET, From 6d7b30d5ee022989a5540b445ce17793b8b0fe86 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 21 Nov 2022 15:59:15 -0700 Subject: [PATCH 167/594] Reduce ping rate for higher airspeeds --- Firmware/LoRaSerial_Firmware/Radio.ino | 10 +++------- Firmware/LoRaSerial_Firmware/States.ino | 22 ++++++++++++++++++++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e851d1e2..a9767b66 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -209,12 +209,8 @@ void returnToReceiving() { if (settings.operatingMode == MODE_POINT_TO_POINT) { - radio.implicitHeader(sf6ExpectedSize); - radio.setCRC(true); - state = radio.startReceive(sf6ExpectedSize); //Set the size we expect to see - radio.setCRC(true); if (sf6ExpectedSize < MAX_PACKET_SIZE) triggerEvent(TRIGGER_RTR_SHORT_PACKET); @@ -545,7 +541,7 @@ int16_t getLinkupOffset() partialTimer = true; //Mark timer so that it runs only once with less than dwell time int linkupOffset = 0; - + switch (settings.airSpeed) { default: @@ -575,10 +571,10 @@ int16_t getLinkupOffset() linkupOffset = 0; break; case (28800): - linkupOffset = 6; + linkupOffset = 0; break; case (38400): - linkupOffset = 6; + linkupOffset = 0; break; } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c0b62ff6..9840dc4a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -461,7 +461,16 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK1_TIMEOUT); setHeartbeatShort(); - pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); //Slow ping + + //Slow down pings + if (ackAirTime < settings.maxDwellTime) + pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); + else + pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); + + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive PING packet + returnToReceiving(); + changeState(RADIO_P2P_LINK_DOWN); } } @@ -541,7 +550,16 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the PING triggerEvent(TRIGGER_HANDSHAKE_ACK2_TIMEOUT); setHeartbeatShort(); - pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); //Slow ping + + //Slow down pings + if (ackAirTime < settings.maxDwellTime) + pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); + else + pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); + + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive PING packet + returnToReceiving(); + changeState(RADIO_P2P_LINK_DOWN); } } From 054d2131fa811a4a83a2e4534515c717b75c7ad4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 21 Nov 2022 13:48:26 -0800 Subject: [PATCH 168/594] Move ACK values into VC0 for point-to-point operation --- .../LoRaSerial_Firmware.ino | 32 ----------- Firmware/LoRaSerial_Firmware/RadioV2.ino | 54 ++++++++++--------- Firmware/LoRaSerial_Firmware/States.ino | 9 ++-- 3 files changed, 35 insertions(+), 60 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 49207f20..446a7b58 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -383,38 +383,6 @@ unsigned long datagramTimer; uint16_t pingRandomTime; uint16_t heartbeatRandomTime; -/* ACK Number Management - - System A System B - - txAckNumber - | - V - Tx DATA Frame -----------------------> Rx DATA Frame - | - V - AckNumber == rmtTxAckNumber - | - | yes - V - rxAckNumber = rmtTxAckNumber++ - | - V - Rx DATA_Ack Frame <--------------------- Tx DATA_ACK Frame - | - V - ackNumber == txAckNumber - | - | yes - V - txAckNumber++ -*/ - -//ACK management -uint8_t rmtTxAckNumber; -uint8_t rxAckNumber; -uint8_t txAckNumber; - //Receive control uint8_t incomingBuffer[MAX_PACKET_SIZE]; uint8_t minDatagramSize; diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 17c8a26d..1df2238c 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -692,6 +692,8 @@ PacketType rcvDatagram() returnToReceiving(); //Immediately begin listening while we process new data rxData = incomingBuffer; + vc = &virtualCircuitList[0]; + vcHeader = NULL; /* |<---------------------- rxDataBytes ---------------------->| @@ -967,7 +969,7 @@ PacketType rcvDatagram() break; case DATAGRAM_DATA_ACK: - if (ackNumber != txAckNumber) + if (ackNumber != vc->txAckNumber) { if (settings.debugReceive) { @@ -975,7 +977,7 @@ PacketType rcvDatagram() systemPrint("Invalid ACK number, received "); systemPrint(ackNumber); systemPrint(" expecting "); - systemPrintln(txAckNumber); + systemPrintln(vc->txAckNumber); outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -988,13 +990,13 @@ PacketType rcvDatagram() if (settings.printAckNumbers) { systemPrint("txAckNumber: "); - systemPrint(txAckNumber); + systemPrint(vc->txAckNumber); systemPrint(" --> "); } - txAckNumber = (txAckNumber + 1) & 3; + vc->txAckNumber = (vc->txAckNumber + 1) & 3; if (settings.printAckNumbers) { - systemPrintln(txAckNumber); + systemPrintln(vc->txAckNumber); outputSerialData(true); } break; @@ -1002,10 +1004,10 @@ PacketType rcvDatagram() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (ackNumber != rmtTxAckNumber) + if (ackNumber != vc->rmtTxAckNumber) { //Determine if this is a duplicate datagram - if (ackNumber == ((rmtTxAckNumber - 1) & 3)) + if (ackNumber == ((vc->rmtTxAckNumber - 1) & 3)) { linkDownTimer = millis(); duplicateFrames++; @@ -1019,7 +1021,7 @@ PacketType rcvDatagram() systemPrint("Invalid datagram number, received "); systemPrint(ackNumber); systemPrint(" expecting "); - systemPrintln(rmtTxAckNumber); + systemPrintln(vc->rmtTxAckNumber); outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -1032,30 +1034,30 @@ PacketType rcvDatagram() if (settings.printAckNumbers) { systemPrint("rxAckNumber: "); - systemPrint(rxAckNumber); + systemPrint(vc->rxAckNumber); systemPrint(" --> "); } - rxAckNumber = rmtTxAckNumber; + vc->rxAckNumber = vc->rmtTxAckNumber; if (settings.printAckNumbers) { - systemPrintln(rxAckNumber); + systemPrintln(vc->rxAckNumber); systemPrint("rmtTxAckNumber: "); - systemPrint(rmtTxAckNumber); + systemPrint(vc->rmtTxAckNumber); systemPrint(" --> "); } - rmtTxAckNumber = (rmtTxAckNumber + 1) & 3; + vc->rmtTxAckNumber = (vc->rmtTxAckNumber + 1) & 3; if (settings.printAckNumbers) { - systemPrintln(rmtTxAckNumber); + systemPrintln(vc->rmtTxAckNumber); outputSerialData(true); } break; case DATAGRAM_DATA: - if (ackNumber != rmtTxAckNumber) + if (ackNumber != vc->rmtTxAckNumber) { //Determine if this is a duplicate datagram - if (ackNumber == ((rmtTxAckNumber - 1) & 3)) + if (ackNumber == ((vc->rmtTxAckNumber - 1) & 3)) { linkDownTimer = millis(); duplicateFrames++; @@ -1069,7 +1071,7 @@ PacketType rcvDatagram() systemPrint("Invalid datagram number, received "); systemPrint(ackNumber); systemPrint(" expecting "); - systemPrintln(rmtTxAckNumber); + systemPrintln(vc->rmtTxAckNumber); outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -1096,17 +1098,17 @@ PacketType rcvDatagram() //Receive this data packet and set the next expected datagram number if (settings.printAckNumbers) { - systemPrint("rxAckNumber: "); - systemPrint(rxAckNumber); + systemPrint("vc->rxAckNumber: "); + systemPrint(vc->rxAckNumber); systemPrint(" --> "); } - rxAckNumber = rmtTxAckNumber; + vc->rxAckNumber = vc->rmtTxAckNumber; if (settings.printAckNumbers) { - systemPrintln(rxAckNumber); + systemPrintln(vc->rxAckNumber); outputSerialData(true); } - rmtTxAckNumber = (rmtTxAckNumber + 1) & 3; + vc->rmtTxAckNumber = (vc->rmtTxAckNumber + 1) & 3; break; } } @@ -1320,9 +1322,11 @@ bool transmitDatagram() hopChannel(); //Parse the virtual circuit header - vc = NULL; + vc = &virtualCircuitList[0]; + vcHeader = NULL; if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { + vc = NULL; vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; txDestVc = vcHeader->destVc; srcVc = vcHeader->srcVc; @@ -1341,9 +1345,9 @@ bool transmitDatagram() //Select the ACK number if (txControl.datagramType == DATAGRAM_DATA_ACK) - txControl.ackNumber = rxAckNumber; + txControl.ackNumber = vc->rxAckNumber; else - txControl.ackNumber = txAckNumber; + txControl.ackNumber = vc->txAckNumber; //Process the packet if (settings.debugDatagrams) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 2d824d0b..4164b5ae 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2132,6 +2132,8 @@ void v2BreakLink() void v2EnterLinkUp() { + VIRTUAL_CIRCUIT * vc; + //Bring up the link triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); hopChannel(); //Leave home @@ -2139,9 +2141,10 @@ void v2EnterLinkUp() updateRSSI(); //Synchronize the ACK numbers - rmtTxAckNumber = 0; - rxAckNumber = 0; - txAckNumber = 0; + vc = &virtualCircuitList[0]; + vc->rmtTxAckNumber = 0; + vc->rxAckNumber = 0; + vc->txAckNumber = 0; //Discard any previous data discardPreviousData(); From 03ad5687349f4758bcea9d6c4cd4868fb0947197 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 23 Nov 2022 13:21:54 -0700 Subject: [PATCH 169/594] Allow exit from training by button press. Print output before training end. --- Firmware/LoRaSerial_Firmware/System.ino | 13 +++++++++++-- Firmware/LoRaSerial_Firmware/Train.ino | 4 +++- Firmware/LoRaSerial_Firmware/settings.h | 3 ++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 7a8147a8..bc6ead72 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -234,7 +234,17 @@ void updateButton() selectTraining(); - trainState = TRAIN_NO_PRESS; + trainState = TRAIN_IN_PROCESS; + } + else if (trainState == TRAIN_IN_PROCESS && trainBtn->wasReleased()) + { + //Exiting training + setRSSI(0b0000); //Turn off LEDs + + //Reboot the radio + petWDT(); + systemFlush(); + systemReset(); } //Blink LEDs according to our state while we wait for user to release button @@ -242,7 +252,6 @@ void updateButton() { if (millis() - lastTrainBlink > 500) //Slow blink { - Serial.println("Train Blinking LEDs"); lastTrainBlink = millis(); //Toggle RSSI LEDs diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index de7c5b69..20ec64b2 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -79,7 +79,7 @@ void updateCylonLEDs() void beginTrainingClient() { - systemPrintln("Multipoint client training"); + systemPrintln("Begin client training"); //Common initialization commonTrainingInitialization(); @@ -220,6 +220,8 @@ void endClientServerTraining(uint8_t event) systemPrint("Link trained from "); systemPrintUniqueID(trainingPartnerID); systemPrintln(); + + outputSerialData(true); } //Done with training diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 9dbee5f1..b5547012 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -222,7 +222,8 @@ typedef struct _VIRTUAL_CIRCUIT typedef enum { TRAIN_NO_PRESS = 0, - TRAIN_PRESSED_2S + TRAIN_PRESSED_2S, + TRAIN_IN_PROCESS, } TrainStates; enum From 31b90ecffabc96505a125f3ea0938221dd866811 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 23 Nov 2022 13:25:53 -0700 Subject: [PATCH 170/594] Increase watchdog timeout from 250ms to 2s on SAMD21. --- Firmware/LoRaSerial_Firmware/Arch_ESP32.h | 2 +- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 4 ++-- Firmware/LoRaSerial_Firmware/Begin.ino | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h index 4efd73ac..affe336a 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h +++ b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h @@ -51,7 +51,7 @@ void esp32BeginSerial(uint16_t serialSpeed) void esp32BeginWDT() { - petTimeoutHalf = 1000 / 2; + petTimeout = 1000 / 2; } void esp32EepromBegin() diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index f310ae43..03ab9df1 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -143,8 +143,8 @@ void samdBeginSerial(uint16_t serialSpeed) void samdBeginWDT() { - myWatchDog.setup(WDT_HARDCYCLE250m); // Initialize WDT with 250ms timeout - petTimeoutHalf = 250 / 2; + myWatchDog.setup(WDT_HARDCYCLE2S); // Initialize WDT with 2s timeout + petTimeout = 1800; } void samdEepromBegin() diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 5b6cbd18..00d26d52 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -76,9 +76,8 @@ void beginSerial(uint16_t serialSpeed) void petWDT() { - //Petting the dog takes a long time so its only done after we've passed the - //half way point - if (millis() - lastPet > petTimeoutHalf) + //Petting the dog takes a long time (~4.5ms on SAMD21) so it's only done after we've passed the timeout + if (millis() - lastPet > petTimeout) { lastPet = millis(); arch.petWDT(); From 2d6fa333327ef3479f558ad7e87222ae16a3309a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 23 Nov 2022 10:05:41 -0800 Subject: [PATCH 171/594] VcServerTest: Find myVc, Add options for reset and break --- Firmware/Tools/VcServerTest.c | 125 +++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 25 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 90c0fea4..a12979bf 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -5,11 +5,15 @@ #define STDIN 0 #define STDOUT 1 -#define LINK_RESET_COMMAND "atb" +#define BREAK_LINKS_COMMAND "atb" +#define GET_MY_VC_ADDRESS "atI28" +#define LINK_RESET_COMMAND "atz" +#define MY_VC_ADDRESS "myVc: " #define DEBUG_PC_TO_RADIO 0 #define DEBUG_RADIO_TO_PC 0 +bool findMyVc = true; int myVc; int remoteVc; uint8_t inputBuffer[BUFFER_SIZE]; @@ -156,9 +160,57 @@ int stdinToRadio() int hostToStdout(uint8_t * data, uint8_t bytesToSend) { + uint8_t * buffer; + uint8_t * bufferEnd; int bytesSent; int bytesWritten; + static uint8_t compareBuffer[4 * BUFFER_SIZE]; + static int offset; int status; + int vcNumber; + + //Locate myVc if necessary + if (findMyVc) + { + //Place the data into the compare buffer + buffer = compareBuffer; + memcpy(&compareBuffer[offset], data, bytesToSend); + offset += bytesToSend; + bufferEnd = &buffer[offset]; + + //Walk through the buffer + while (buffer < bufferEnd) + { + if ((strncmp((const char *)buffer, MY_VC_ADDRESS, strlen(MY_VC_ADDRESS)) == 0) + && (&buffer[strlen(MY_VC_ADDRESS) + 3] <= bufferEnd)) + { + if ((sscanf((const char *)&buffer[strlen(MY_VC_ADDRESS)], "%d", &vcNumber) == 1) + && ((uint8_t)vcNumber < MAX_VC)) + { + findMyVc = false; + myVc = (int8_t)vcNumber; + break; + } + } + + //Skip to the end of the line + while ((buffer < bufferEnd) && (*buffer != '\n')) + buffer++; + + if ((buffer < bufferEnd) && (*buffer == '\n')) + { + //Skip to the next line + while ((buffer < bufferEnd) && (*buffer == '\n')) + buffer++; + + //Move this data to the beginning of the buffer + offset = bufferEnd - buffer; + memcpy(compareBuffer, buffer, offset); + buffer = compareBuffer; + bufferEnd = &buffer[offset]; + } + } + } //Write this data to stdout bytesSent = 0; @@ -307,30 +359,33 @@ main ( char ** argv ) { + bool breakLinks; int maxfds; + bool reset; int status; char * terminal; struct timeval timeout; uint8_t * vcData; + maxfds = STDIN; status = 0; do { //Display the help text if necessary - if (argc != 4) + if (argc < 3) { - printf("%s terminal my_VC target_VC\n", argv[0]); + printf("%s terminal target_VC [options]\n", argv[0]); printf("\n"); printf("terminal - Name or path to the terminal device for the radio\n"); - printf("my_VC:\n"); - printf(" Server: 0\n"); - printf(" Client: 1 - %d\n", MAX_VC - 1); printf("target_VC:\n"); printf(" Server: 0\n"); printf(" Client: 1 - %d\n", MAX_VC - 1); printf(" Loopback: my_VC\n"); printf(" Broadcast: %d\n", VC_BROADCAST); printf(" Command: %d\n", VC_COMMAND); + printf("Options:\n"); + printf(" --reset Reset the LoRaSerial radio and break the links\n"); + printf(" --break Use ATB command to break the links\n"); status = -1; break; } @@ -338,19 +393,8 @@ main ( //Get the path to the terminal terminal = argv[1]; - //Determine the local VC address - if ((sscanf(argv[2], "%d", &myVc) != 1) - || ((myVc < VC_SERVER) || (myVc >= MAX_VC))) - { - fprintf(stderr, "ERROR: Invalid my VC address, please use one of the following:\n"); - fprintf(stderr, " Server: 0\n"); - fprintf(stderr, " Client: 1 - %d\n", MAX_VC - 1); - status = -1; - break; - } - //Determine the remote VC address - if ((sscanf(argv[3], "%d", &remoteVc) != 1) + if ((sscanf(argv[2], "%d", &remoteVc) != 1) || (remoteVc < VC_COMMAND) || (remoteVc >= MAX_VC)) { fprintf(stderr, "ERROR: Invalid target VC address, please use one of the following:\n"); @@ -365,23 +409,54 @@ main ( } //Open the terminal - maxfds = STDIN; status = openTty(terminal); if (status) break; if (maxfds < tty) maxfds = tty; - //Display myVc - printf("myVc: %d\n", myVc); + //Determine the options + reset = false; + if ((argc == 4) && (strcmp("--reset", argv[3]) == 0)) + reset = true; - //Delay a while to let the radio complete its reset operation - sleep(2); + breakLinks = reset; + if ((argc == 4) && (strcmp("--break", argv[3]) == 0)) + breakLinks = true; - //Break the links to this node - cmdToRadio((uint8_t *)LINK_RESET_COMMAND, strlen(LINK_RESET_COMMAND)); + //Reset the LoRaSerial radio if requested + if (reset) + { + //Delay a while to let the radio complete its reset operation + sleep(2); + + //Break the links to this node + cmdToRadio((uint8_t *)LINK_RESET_COMMAND, strlen(LINK_RESET_COMMAND)); + + //Allow the device to reset + close(tty); + do + { + sleep(1); + + //Open the terminal + status = openTty(terminal); + } while (status); + + //Delay a while to let the radio complete its reset operation + sleep(2); + } + + //Get myVc address + cmdToRadio((uint8_t *)GET_MY_VC_ADDRESS, strlen(GET_MY_VC_ADDRESS)); + + //Break the links if requested + if (breakLinks) + cmdToRadio((uint8_t *)BREAK_LINKS_COMMAND, strlen(BREAK_LINKS_COMMAND)); //Initialize the fd_sets + if (maxfds < tty) + maxfds = tty; FD_ZERO(&exceptfds); FD_ZERO(&readfds); FD_ZERO(&writefds); From 1d53ed5ce4e0bc00bb98047c08d2dc93baa43c9c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 09:22:12 -0800 Subject: [PATCH 172/594] Add ATI28 to display myVc value --- Firmware/LoRaSerial_Firmware/Commands.ino | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 71c46cc6..4d352be8 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -191,6 +191,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI25 - Display the total insufficient buffer count"); systemPrintln(" ATI26 - Display the total number of bad CRC frames"); systemPrintln(" ATI27 - Display the total number of net ID mismatch frames"); + systemPrintln(" ATI28 - Return myVc value"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(0, true); @@ -367,6 +368,12 @@ bool commandAT(const char * commandString) systemPrint("Total number of net ID mismatch frames: "); systemPrintln(netIdMismatch); break; + case ('8'): //ATI28 - Return myVc value + systemPrintln(); + systemPrint("myVc: "); + systemPrintln(myVc); + reportOK(); + break; } } From c8c9c92ea27d765c777a28a7fdd0fd15fc4b4fa5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 09:30:25 -0800 Subject: [PATCH 173/594] Move clearing of transactionComplete into rcvDatagram --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 3 +++ Firmware/LoRaSerial_Firmware/States.ino | 17 ----------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 1df2238c..0d777b8d 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -657,6 +657,9 @@ PacketType rcvDatagram() VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; + //Acknowledge the receive interrupt + transactionComplete = false; + //Save the receive time rcvTimeMillis = millis(); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 4164b5ae..fad76e37 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -189,8 +189,6 @@ void updateRadioState() //Determine if a PING was received if (transactionComplete) { - transactionComplete = false; //Reset ISR flag - //Decode the received packet PacketType packetType = rcvDatagram(); @@ -265,8 +263,6 @@ void updateRadioState() case RADIO_P2P_WAIT_ACK_1: if (transactionComplete) { - transactionComplete = false; //Reset ISR flag - //Decode the received packet PacketType packetType = rcvDatagram(); @@ -376,8 +372,6 @@ void updateRadioState() case RADIO_P2P_WAIT_ACK_2: if (transactionComplete == true) { - transactionComplete = false; //Reset ISR flag - //Decode the received packet PacketType packetType = rcvDatagram(); @@ -537,8 +531,6 @@ void updateRadioState() //Check for a received datagram if (transactionComplete == true) { - transactionComplete = false; //Reset ISR flag - //Decode the received datagram PacketType packetType = rcvDatagram(); @@ -778,8 +770,6 @@ void updateRadioState() if (transactionComplete) { - transactionComplete = false; //Reset ISR flag - //Decode the received datagram PacketType packetType = rcvDatagram(); @@ -1027,8 +1017,6 @@ void updateRadioState() if (transactionComplete) { - transactionComplete = false; //Reset ISR flag - //Decode the received datagram PacketType packetType = rcvDatagram(); @@ -1158,7 +1146,6 @@ void updateRadioState() if (transactionComplete == true) { triggerEvent(TRIGGER_MP_PACKET_RECEIVED); - transactionComplete = false; //Reset ISR flag //Decode the received datagram PacketType packetType = rcvDatagram(); @@ -1382,7 +1369,6 @@ void updateRadioState() //If dio0ISR has fired, a packet has arrived if (transactionComplete == true) { - transactionComplete = false; trainingPreviousRxInProgress = false; //Decode the received datagram @@ -1483,7 +1469,6 @@ void updateRadioState() //If dio0ISR has fired, a packet has arrived if (transactionComplete == true) { - transactionComplete = false; //Reset ISR flag trainingPreviousRxInProgress = false; //Decode the received datagram @@ -1603,7 +1588,6 @@ void updateRadioState() currentMillis = millis(); if (transactionComplete == true) { - transactionComplete = false; //Reset ISR flag trainingPreviousRxInProgress = false; //Decode the received datagram @@ -1706,7 +1690,6 @@ void updateRadioState() currentMillis = millis(); if (transactionComplete == true) { - transactionComplete = false; //Reset ISR flag trainingPreviousRxInProgress = false; //Decode the received datagram From f69f7f60e9c4c677bd1552b2b969d46e617f00f8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 09:57:03 -0800 Subject: [PATCH 174/594] Add xmitVcAckFrame to send the VC ACK to a data frame Start the received data with the START_OF_VC_SERIAL byte when sending it over the serial link --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 26 ++++++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 30 ++++++++++++++---------- 2 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 0d777b8d..ca6fd284 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -643,6 +643,32 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) return (transmitDatagram()); } +//Build the ACK frame +bool xmitVcAckFrame(int8_t destVc) +{ + VC_RADIO_MESSAGE_HEADER * vcHeader; + + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; + vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; + vcHeader->destVc = destVc; + vcHeader->srcVc = myVc; + endOfTxData += VC_RADIO_HEADER_BYTES; + + /* + endOfTxData ---. + | + V + +--------+---------+--------+----------+---------+----------+----------+ + | | | | | | Channel | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Timer | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 2 bytes | n Bytes | + +--------+---------+--------+----------+---------+----------+----------+ + */ + + //Finish building the ACK frame + return xmitDatagramP2PAck(); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Datagram reception //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index fad76e37..f8dff4d6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1606,15 +1606,18 @@ void updateRadioState() case DATAGRAM_DATA: //Move the data into the serial output buffer + if (settings.debugSerial) + { + systemPrint("updateRadioState moving "); + systemPrint(rxDataBytes); + systemPrintln(" bytes from inputBuffer into serialTransmitBuffer"); + outputSerialData(true); + } + systemWrite(START_OF_VC_SERIAL); serialBufferOutput(rxData, rxDataBytes); //Acknowledge the data frame - vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; - vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; - vcHeader->destVc = rxSrcVc; - vcHeader->srcVc = myVc; - endOfTxData += VC_RADIO_HEADER_BYTES; - if (xmitDatagramP2PAck() == true) + if (xmitVcAckFrame(rxSrcVc)) changeState(RADIO_VC_WAIT_TX_DONE); break; @@ -1708,15 +1711,18 @@ void updateRadioState() case DATAGRAM_DATA: //Move the data into the serial output buffer + if (settings.debugSerial) + { + systemPrint("updateRadioState moving "); + systemPrint(rxDataBytes); + systemPrintln(" bytes from inputBuffer into serialTransmitBuffer"); + outputSerialData(true); + } + systemWrite(START_OF_VC_SERIAL); serialBufferOutput(rxData, rxDataBytes); //Acknowledge the data frame - vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; - vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; - vcHeader->destVc = rxSrcVc; - vcHeader->srcVc = myVc; - endOfTxData += VC_RADIO_HEADER_BYTES; - if (xmitDatagramP2PAck() == true) + if (xmitVcAckFrame(rxSrcVc)) changeState(RADIO_VC_WAIT_TX_DONE_ACK); break; From 48d05a3556df2a8aa7c4edf1e297a99e68c07dfe Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 10:07:41 -0800 Subject: [PATCH 175/594] Update the debugging for VCs This check-in includes the following changes: * Always increment badFrames before returning DATAGRAM_BAD * Display SF6 length during receive * Display the VC header during receive * Summarize the VC communication * Display the VC ACK number --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 64 +++++++++++++++++++++--- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index ca6fd284..ad35f422 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -713,6 +713,7 @@ PacketType rcvDatagram() systemPrintln(state); } returnToReceiving(); //Return to listening + badFrames++; return (DATAGRAM_BAD); } @@ -964,9 +965,7 @@ PacketType rcvDatagram() if (settings.radioSpreadFactor == 6) { if (rxDataBytes >= (*rxData + minDatagramSize)) - { rxDataBytes = *rxData++; - } else { if (settings.debugReceive) @@ -984,6 +983,15 @@ PacketType rcvDatagram() badFrames++; return (DATAGRAM_BAD); } + if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint(" SF6 Length: "); + systemPrintln(rxDataBytes); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } } else rxDataBytes -= minDatagramSize; @@ -1168,6 +1176,27 @@ PacketType rcvDatagram() rxSrcVc = vcHeader->srcVc; rxVcData = &rxData[3]; + //Display the virtual circuit header + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint(" VC Length: "); + systemPrintln(vcHeader->length); + systemPrint(" DestAddr: "); + if (rxDestVc == VC_BROADCAST) + systemPrintln("Broadcast"); + else + systemPrintln(rxDestVc); + systemPrint(" SrcAddr: "); + if (rxSrcVc == VC_UNASSIGNED) + systemPrintln("Unassigned"); + else + systemPrintln(rxSrcVc); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + //Validate the source VC vc = NULL; if (rxSrcVc != VC_UNASSIGNED) @@ -1206,9 +1235,9 @@ PacketType rcvDatagram() } if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); - badFrames++; if (vc) vc->badLength++; + badFrames++; return DATAGRAM_BAD; } @@ -1295,6 +1324,13 @@ PacketType rcvDatagram() { systemPrintTimestamp(); systemPrint("RX: "); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + systemPrint((uint8_t)rxDestVc); + systemPrint(" <-- "); + systemPrint((uint8_t)rxSrcVc); + systemWrite(' '); + } systemPrint(v2DatagramType[datagramType]); switch (datagramType) { @@ -1307,9 +1343,12 @@ PacketType rcvDatagram() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (settings.operatingMode == MODE_POINT_TO_POINT) + if (settings.operatingMode != MODE_DATAGRAM) { - systemPrint(" (ACK #"); + systemPrint(" ("); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + systemPrint("VC "); + systemPrint("ACK #"); systemPrint(ackNumber); systemPrint(")"); } @@ -1383,6 +1422,13 @@ bool transmitDatagram() { systemPrintTimestamp(); systemPrint("TX: "); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + systemPrint((uint8_t)srcVc); + systemPrint(" --> "); + systemPrint((uint8_t)txDestVc); + systemWrite(' '); + } systemPrint(v2DatagramType[txControl.datagramType]); switch (txControl.datagramType) { @@ -1395,9 +1441,12 @@ bool transmitDatagram() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (settings.operatingMode == MODE_POINT_TO_POINT) + if (settings.operatingMode != MODE_DATAGRAM) { - systemPrint(" (ACK #"); + systemPrint(" ("); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + systemPrint("VC "); + systemPrint("ACK #"); systemPrint(txControl.ackNumber); systemPrint(")"); } @@ -1522,7 +1571,6 @@ bool transmitDatagram() if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } - } //Verify the Virtual-Circuit length From 68e0f87b670ccf82fb6c8b3fafa4da16390d605e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 10:11:29 -0800 Subject: [PATCH 176/594] Always clear transactionComplete when set --- Firmware/LoRaSerial_Firmware/States.ino | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index f8dff4d6..d12cd780 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -950,6 +950,8 @@ void updateRadioState() //completes transmission, retransmit the previously lost datagram. if (transactionComplete) { + transactionComplete = false; //Reset ISR flag + //Retransmit the packet if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { @@ -986,7 +988,6 @@ void updateRadioState() if (retransmitDatagram(NULL) == true) { - transactionComplete = false; //Reset ISR flag setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat lostFrames++; changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); From 21701c58f88172725b26525e9f1cc6ad28675f77 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 10:16:27 -0800 Subject: [PATCH 177/594] VC: Handle duplicate datagrams --- Firmware/LoRaSerial_Firmware/States.ino | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d12cd780..17f2dd48 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1625,6 +1625,17 @@ void updateRadioState() case DATAGRAM_DATA_ACK: vcAckTimer = 0; break; + + case DATAGRAM_DUPLICATE: + printPacketQuality(); + + updateRSSI(); //Adjust LEDs to RSSI level + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); + if (xmitVcAckFrame(rxSrcVc)) + changeState(RADIO_VC_WAIT_TX_DONE); + break; } } @@ -1731,6 +1742,17 @@ void updateRadioState() vcAckTimer = 0; changeState(RADIO_VC_WAIT_RECEIVE); break; + + case DATAGRAM_DUPLICATE: + printPacketQuality(); + updateRSSI(); //Adjust LEDs to RSSI level + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + //Acknowledge the data frame + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); + if (xmitVcAckFrame(rxSrcVc)) + changeState(RADIO_VC_WAIT_TX_DONE); + break; } } From 54aa5a1338c6d0e2f24bc80364a8fa29328bce4b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 10:20:46 -0800 Subject: [PATCH 178/594] VC: Only wait for TX complete is the datagram was sent --- Firmware/LoRaSerial_Firmware/States.ino | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 17f2dd48..d2dba0f8 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1644,18 +1644,21 @@ void updateRadioState() && (receiveInProcess() == false)) { //Send another heartbeat - xmitVcHeartbeat(myVc, myUniqueId); - changeState(RADIO_VC_WAIT_TX_DONE); + if (xmitVcHeartbeat(myVc, myUniqueId)) + changeState(RADIO_VC_WAIT_TX_DONE); } //Check for data to send else if ((receiveInProcess() == false) && (vcSerialMessageReceived())) { + //No need to add the VC header since the header is in the radioTxBuffer //Transmit the packet triggerEvent(TRIGGER_VC_TX_DATA); if (xmitDatagramP2PData() == true) { vcAckTimer = datagramTimer; + + //Since vcAckTimer is off when equal to zero, force it to a non-zero value if (!vcAckTimer) vcAckTimer = 1; @@ -1761,8 +1764,8 @@ void updateRadioState() && (receiveInProcess() == false)) { //Send another heartbeat - xmitVcHeartbeat(myVc, myUniqueId); - changeState(RADIO_VC_WAIT_TX_DONE_ACK); + if (xmitVcHeartbeat(myVc, myUniqueId)) + changeState(RADIO_VC_WAIT_TX_DONE_ACK); } //Check for retransmit needed From a369db0dee7ccd3210df85e872652855957f6003 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 10:25:29 -0800 Subject: [PATCH 179/594] VC: Add vcLinkUp routine Only reset ACK counters if link was down --- Firmware/LoRaSerial_Firmware/States.ino | 40 ++++++++++++++----------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d2dba0f8..885c5bf6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2253,6 +2253,27 @@ void vcBreakLink(int8_t vcIndex) resetSerial(); } +int8_t vcLinkUp(int8_t index) +{ + VIRTUAL_CIRCUIT * vc = &virtualCircuitList[index]; + + //Send the status message + if (!vc->linkUp) + { + vcSendLinkStatus(true, index); + + //Reset the ACK counters + vc->txAckNumber = 0; + vc->rmtTxAckNumber = 0; + vc->rxAckNumber = 0; + } + + //Update the link status + vc->linkUp = true; + vc->lastHeartbeatMillis = millis(); + return index; +} + int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) { int8_t index; @@ -2268,16 +2289,8 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) //Compare the unique ID values if (memcmp(vc->uniqueId, id, UNIQUE_ID_BYTES) == 0) - { - if (!vc->linkUp) - //Send the status message - vcSendLinkStatus(true, index); - //Update the link status - vc->linkUp = true; - vc->lastHeartbeatMillis = millis(); - return index; - } + return vcLinkUp(index); } //The unique ID is not in the list @@ -2328,15 +2341,8 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) //Mark this link as up vc->valid = true; - vc->linkUp = true; - vc->lastHeartbeatMillis = millis(); memcpy(&vc->uniqueId, id, UNIQUE_ID_BYTES); - - //Send the status message - vcSendLinkStatus(true, index); - - //Returned the assigned address - return index; + return vcLinkUp(index); } void vcReceiveHeartbeat(RadioStates nextState, uint32_t rxMillis) From 665071293be75a0c49006166350b9615268b40a0 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 10:31:02 -0800 Subject: [PATCH 180/594] VC: Use ACK numbers in the VIRTUAL_CIRCUIT structure --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 393 +++++++++++------------ Firmware/LoRaSerial_Firmware/settings.h | 13 +- 2 files changed, 187 insertions(+), 219 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index ad35f422..a1b4dccb 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -996,162 +996,9 @@ PacketType rcvDatagram() else rxDataBytes -= minDatagramSize; - //Verify the packet number last so that the expected datagram or ACK number can be updated + //Get the Virtual-Circuit header rxVcData = rxData; - if (settings.operatingMode == MODE_POINT_TO_POINT) - { - switch (datagramType) - { - default: - break; - - case DATAGRAM_DATA_ACK: - if (ackNumber != vc->txAckNumber) - { - if (settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint("Invalid ACK number, received "); - systemPrint(ackNumber); - systemPrint(" expecting "); - systemPrintln(vc->txAckNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - badFrames++; - return (DATAGRAM_BAD); - } - - //Set the next TX ACK number - if (settings.printAckNumbers) - { - systemPrint("txAckNumber: "); - systemPrint(vc->txAckNumber); - systemPrint(" --> "); - } - vc->txAckNumber = (vc->txAckNumber + 1) & 3; - if (settings.printAckNumbers) - { - systemPrintln(vc->txAckNumber); - outputSerialData(true); - } - break; - - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - if (ackNumber != vc->rmtTxAckNumber) - { - //Determine if this is a duplicate datagram - if (ackNumber == ((vc->rmtTxAckNumber - 1) & 3)) - { - linkDownTimer = millis(); - duplicateFrames++; - return DATAGRAM_DUPLICATE; - } - - //Not a duplicate - if (settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint("Invalid datagram number, received "); - systemPrint(ackNumber); - systemPrint(" expecting "); - systemPrintln(vc->rmtTxAckNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - badFrames++; - return DATAGRAM_BAD; - } - - //Receive this data packet and set the next expected datagram number - if (settings.printAckNumbers) - { - systemPrint("rxAckNumber: "); - systemPrint(vc->rxAckNumber); - systemPrint(" --> "); - } - vc->rxAckNumber = vc->rmtTxAckNumber; - if (settings.printAckNumbers) - { - systemPrintln(vc->rxAckNumber); - systemPrint("rmtTxAckNumber: "); - systemPrint(vc->rmtTxAckNumber); - systemPrint(" --> "); - } - vc->rmtTxAckNumber = (vc->rmtTxAckNumber + 1) & 3; - if (settings.printAckNumbers) - { - systemPrintln(vc->rmtTxAckNumber); - outputSerialData(true); - } - break; - - case DATAGRAM_DATA: - if (ackNumber != vc->rmtTxAckNumber) - { - //Determine if this is a duplicate datagram - if (ackNumber == ((vc->rmtTxAckNumber - 1) & 3)) - { - linkDownTimer = millis(); - duplicateFrames++; - return DATAGRAM_DUPLICATE; - } - - //Not a duplicate - if (settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint("Invalid datagram number, received "); - systemPrint(ackNumber); - systemPrint(" expecting "); - systemPrintln(vc->rmtTxAckNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - badFrames++; - return DATAGRAM_BAD; - } - - //Verify that there is sufficient space in the serialTransmitBuffer - if (inCommandMode || ((sizeof(serialTransmitBuffer) - availableTXBytes()) < rxDataBytes)) - { - if (settings.debugReceive) - { - systemPrintTimestamp(); - systemPrintln("Insufficient space in the serialTransmitBuffer"); - } - insufficientSpace++; - - //Apply back pressure to the other radio by dropping this packet and - //forcing the other radio to retransmit the packet. - return DATAGRAM_BAD; - } - - //Receive this data packet and set the next expected datagram number - if (settings.printAckNumbers) - { - systemPrint("vc->rxAckNumber: "); - systemPrint(vc->rxAckNumber); - systemPrint(" --> "); - } - vc->rxAckNumber = vc->rmtTxAckNumber; - if (settings.printAckNumbers) - { - systemPrintln(vc->rxAckNumber); - outputSerialData(true); - } - vc->rmtTxAckNumber = (vc->rmtTxAckNumber + 1) & 3; - break; - } - } - - //Verify the Virtual-Circuit length - else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { //Verify that the virtual circuit header is present if (rxDataBytes < 3) @@ -1258,35 +1105,82 @@ PacketType rcvDatagram() } return DATAGRAM_NOT_MINE; } + } - //Account for this frame - if (vc) + //Verify the packet number last so that the expected datagram or ACK number can be updated + if (vc && (settings.operatingMode != MODE_DATAGRAM)) + { + switch (datagramType) { - vc->framesReceived++; - if (datagramType == DATAGRAM_DATA) + default: + break; + + case DATAGRAM_DATA_ACK: + if (ackNumber != vc->txAckNumber) + { + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Invalid ACK number, received "); + systemPrint(ackNumber); + systemPrint(" expecting "); + systemPrintln(vc->txAckNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + badFrames++; + return (DATAGRAM_BAD); + } + + //Set the next TX ACK number + if (settings.printAckNumbers) + { + systemPrint("txAckNumber: "); + systemPrint(vc->txAckNumber); + systemPrint(" --> "); + } + vc->txAckNumber = (vc->txAckNumber + 1) & 3; + if (settings.printAckNumbers) + { + systemPrintln(vc->txAckNumber); + outputSerialData(true); + } + break; + + case DATAGRAM_HEARTBEAT: + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + break; + datagramType = validateDatagram(vc, datagramType, ackNumber, rxDataBytes); + if (datagramType != DATAGRAM_HEARTBEAT) + return datagramType; + break; + + case DATAGRAM_REMOTE_COMMAND: + datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(commandRXBuffer) + - availableRXCommandBytes()); + if (datagramType != DATAGRAM_REMOTE_COMMAND) + return datagramType; + break; + + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(serialTransmitBuffer) + - availableTXBytes()); + if (datagramType != DATAGRAM_REMOTE_COMMAND_RESPONSE) + return datagramType; + break; + + case DATAGRAM_DATA: + datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(serialTransmitBuffer) + - availableTXBytes()); + if (datagramType != DATAGRAM_DATA) + return datagramType; vc->messagesReceived++; + break; } - //Display the virtual circuit header - if (settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint(" VC Length: "); - systemPrintln(vcHeader->length); - systemPrint(" DestAddr: "); - if (rxDestVc == VC_BROADCAST) - systemPrintln("Broadcast"); - else - systemPrintln(rxDestVc); - systemPrint(" SrcAddr: "); - if (rxSrcVc == VC_UNASSIGNED) - systemPrintln("Unassigned"); - else - systemPrintln(rxSrcVc); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } + //Account for this frame + vc->framesReceived++; } /* @@ -1370,6 +1264,74 @@ PacketType rcvDatagram() return datagramType; } +PacketType validateDatagram(VIRTUAL_CIRCUIT * vc, PacketType datagramType, uint8_t ackNumber, uint16_t freeBytes) +{ + if (ackNumber != vc->rmtTxAckNumber) + { + //Determine if this is a duplicate datagram + if (ackNumber == ((vc->rmtTxAckNumber - 1) & 3)) + { + linkDownTimer = millis(); + duplicateFrames++; + return DATAGRAM_DUPLICATE; + } + + //Not a duplicate + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Invalid datagram number, received "); + systemPrint(ackNumber); + systemPrint(" expecting "); + systemPrintln(vc->rmtTxAckNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + badFrames++; + return DATAGRAM_BAD; + } + + //Verify that there is sufficient space in the serialTransmitBuffer + if (inCommandMode || ((sizeof(serialTransmitBuffer) - availableTXBytes()) < rxDataBytes)) + { + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrintln("Insufficient space in the serialTransmitBuffer"); + } + insufficientSpace++; + + //Apply back pressure to the other radio by dropping this packet and + //forcing the other radio to retransmit the packet. + badFrames++; + return DATAGRAM_BAD; + } + + //Receive this data packet and set the next expected datagram number + if (settings.printAckNumbers) + { + systemPrint("rxAckNumber: "); + systemPrint(vc->rxAckNumber); + systemPrint(" --> "); + } + vc->rxAckNumber = vc->rmtTxAckNumber; + if (settings.printAckNumbers) + { + systemPrintln(vc->rxAckNumber); + systemPrint("rmtTxAckNumber: "); + systemPrint(vc->rmtTxAckNumber); + systemPrint(" --> "); + } + vc->rmtTxAckNumber = (vc->rmtTxAckNumber + 1) & 3; + if (settings.printAckNumbers) + { + systemPrintln(vc->rmtTxAckNumber); + outputSerialData(true); + } + return datagramType; +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Datagram transmission //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1398,9 +1360,9 @@ bool transmitDatagram() vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; txDestVc = vcHeader->destVc; srcVc = vcHeader->srcVc; - if ((uint8_t)vcHeader->srcVc <= MAX_VC) + if ((uint8_t)vcHeader->destVc <= MAX_VC) { - vc = &virtualCircuitList[srcVc]; + vc = &virtualCircuitList[txDestVc]; vc->messagesSent++; } vcData = (uint8_t *)&vcHeader[1]; @@ -1807,51 +1769,56 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); + //Drop this datagram if the receiver is active + frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background + uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond if (receiveInProcess() == true || transactionComplete == true) { triggerEvent(TRIGGER_TRANSMIT_CANCELED); + if (settings.debugReceive || settings.debugDatagrams) + systemPrintln(receiveInProcess() ? "RXIP" : "RXTC"); return (false); //Do not start transmit while RX is or has occured } + else + { - int state = radio.startTransmit(outgoingPacket, txDatagramSize); + int state = radio.startTransmit(outgoingPacket, txDatagramSize); - if (state == RADIOLIB_ERR_NONE) - { - frameSentCount++; - if (vc) - vc->framesSent++; - framesSent++; - xmitTimeMillis = millis(); - frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background - uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond - if (settings.debugTransmit) + if (state == RADIOLIB_ERR_NONE) { - systemPrintTimestamp(); - systemPrint("TX: frameAirTime "); - systemPrint(frameAirTime); - systemPrintln(" mSec"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); + frameSentCount++; + if (vc) + vc->framesSent++; + framesSent++; + xmitTimeMillis = millis(); + if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: frameAirTime "); + systemPrint(frameAirTime); + systemPrintln(" mSec"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + systemPrintTimestamp(); + systemPrint("TX: responseDelay "); + systemPrint(responseDelay); + systemPrintln(" mSec"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + } + else if (settings.debugTransmit) + { systemPrintTimestamp(); - systemPrint("TX: responseDelay "); - systemPrint(responseDelay); - systemPrintln(" mSec"); + systemPrint("TX: Transmit error, state "); + systemPrintln(state); outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); } - frameAirTime += responseDelay; } - else if (settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint("TX: Transmit error, state "); - systemPrintln(state); - outputSerialData(true); - } - + frameAirTime += responseDelay; datagramTimer = millis(); //Move timestamp even if error retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime); //Wait this number of ms between retransmits. Increases with each re-transmit. diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 9dbee5f1..d82b693a 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -168,12 +168,12 @@ typedef struct _VIRTUAL_CIRCUIT unsigned long lastHeartbeatMillis; //Link quality metrics - uint32_t framesSent; //Total number of frames sent - uint32_t framesReceived; //Total number of frames received - uint32_t messagesSent; //Total number of messages sent - uint32_t messagesReceived; //Total number of messages received - uint32_t badLength; //Total number of bad lengths received - uint32_t linkFailures; //Total number of link failures + uint32_t framesSent; //myVc --> VC, Total number of frames sent + uint32_t framesReceived; //myVc <-- VC, Total number of frames received + uint32_t messagesSent; //myVc --> VC, Total number of messages sent + uint32_t messagesReceived; //myVc <-- VC, Total number of messages received + uint32_t badLength; //myVc <-- VC, Total number of bad lengths received + uint32_t linkFailures; //myVc <-> VC, Total number of link failures //Link management bool valid; //Unique ID is valid @@ -182,6 +182,7 @@ typedef struct _VIRTUAL_CIRCUIT /* ACK number management System A System B + (in destVc) (in srcVc) txAckNumber | From 40b4cdc82c937f3554064f6fe634f0385642b536 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 12:06:41 -0800 Subject: [PATCH 181/594] VcServerTest: Output data messages --- Firmware/Tools/VcServerTest.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index a12979bf..8d331b87 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -214,6 +214,7 @@ int hostToStdout(uint8_t * data, uint8_t bytesToSend) //Write this data to stdout bytesSent = 0; + status = 0; while (bytesSent < bytesToSend) { bytesWritten = write(STDOUT, &data[bytesSent], bytesToSend - bytesSent); @@ -337,7 +338,9 @@ int radioToHost() switch (header->radio.destVc) { default: - //Discard this message + if ((header->radio.destVc == myVc) || (header->radio.destVc == VC_BROADCAST)) + //Output this message + status = hostToStdout(data, length); break; case PC_LINK_STATUS: From f13b5e97b355b5aafcc1af86e7088a6d9b6f4b2d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 12:11:24 -0800 Subject: [PATCH 182/594] Output error cases when using debugDatagrams --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 41 ++++++++++++++++-------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index a1b4dccb..c35f7a18 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -699,18 +699,22 @@ PacketType rcvDatagram() } else if (state == RADIOLIB_ERR_CRC_MISMATCH) { - if (settings.debug == true) + if (settings.debug || settings.debugDatagrams || settings.debugReceive) + { systemPrintln("Receive CRC error!"); + outputSerialData(true); + } badCrc++; returnToReceiving(); //Return to listening return (DATAGRAM_CRC_ERROR); } else { - if (settings.debug == true) + if (settings.debug || settings.debugDatagrams || settings.debugReceive) { systemPrint("Receive error: "); systemPrintln(state); + outputSerialData(true); } returnToReceiving(); //Return to listening badFrames++; @@ -769,7 +773,7 @@ PacketType rcvDatagram() hopChannel(); } - if (settings.debugTransmit) + if (settings.debugReceive) { systemPrint("in: "); dumpBufferRaw(incomingBuffer, 14); //Print only the first few bytes when debugging packets @@ -798,7 +802,7 @@ PacketType rcvDatagram() if (rxDataBytes < minDatagramSize) { //Display the packet contents - if (settings.printPktData || settings.debugReceive) + if (settings.printPktData || settings.debugDatagrams || settings.debugReceive) { systemPrintTimestamp(); systemPrint("RX: Bad Frame "); @@ -837,7 +841,7 @@ PacketType rcvDatagram() receivedNetID = *rxData++; if (receivedNetID != settings.netID) { - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("RX: NetID "); @@ -883,7 +887,7 @@ PacketType rcvDatagram() && (incomingBuffer[rxDataBytes - 1] != (crc & 0xff))) { //Display the packet contents - if (settings.printPktData || settings.debugReceive) + if (settings.printPktData || settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("RX: Bad CRC-16, received 0x"); @@ -925,7 +929,7 @@ PacketType rcvDatagram() printControl(*((uint8_t *)&rxControl)); if (datagramType >= MAX_V2_DATAGRAM_TYPE) { - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("RX: Invalid datagram type "); @@ -1003,7 +1007,7 @@ PacketType rcvDatagram() //Verify that the virtual circuit header is present if (rxDataBytes < 3) { - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("Missing VC header bytes, received only "); @@ -1050,7 +1054,7 @@ PacketType rcvDatagram() { if ((uint8_t)rxSrcVc >= MAX_VC) { - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("Invalid source VC: "); @@ -1071,7 +1075,7 @@ PacketType rcvDatagram() //Validate the length if (vcHeader->length != rxDataBytes) { - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("Invalid VC length, received "); @@ -1091,7 +1095,7 @@ PacketType rcvDatagram() //Validate the destination VC if ((rxDestVc != VC_BROADCAST) && (rxDestVc != myVc)) { - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("Not my VC: "); @@ -1118,7 +1122,7 @@ PacketType rcvDatagram() case DATAGRAM_DATA_ACK: if (ackNumber != vc->txAckNumber) { - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("Invalid ACK number, received "); @@ -1271,13 +1275,22 @@ PacketType validateDatagram(VIRTUAL_CIRCUIT * vc, PacketType datagramType, uint8 //Determine if this is a duplicate datagram if (ackNumber == ((vc->rmtTxAckNumber - 1) & 3)) { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Duplicate datagram received, ACK "); + systemPrintln(ackNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } linkDownTimer = millis(); duplicateFrames++; return DATAGRAM_DUPLICATE; } //Not a duplicate - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrint("Invalid datagram number, received "); @@ -1295,7 +1308,7 @@ PacketType validateDatagram(VIRTUAL_CIRCUIT * vc, PacketType datagramType, uint8 //Verify that there is sufficient space in the serialTransmitBuffer if (inCommandMode || ((sizeof(serialTransmitBuffer) - availableTXBytes()) < rxDataBytes)) { - if (settings.debugReceive) + if (settings.debugReceive || settings.debugDatagrams) { systemPrintTimestamp(); systemPrintln("Insufficient space in the serialTransmitBuffer"); From 7fe88821741ec6e9fc85a6c837d7131f17d736c4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 28 Nov 2022 10:30:47 -0800 Subject: [PATCH 183/594] VcServerTest: Get the correct myVc value --- Firmware/Tools/VcServerTest.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 8d331b87..2b48d042 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -13,11 +13,12 @@ #define DEBUG_PC_TO_RADIO 0 #define DEBUG_RADIO_TO_PC 0 -bool findMyVc = true; -int myVc; +bool findMyVc; +int myVc = VC_UNASSIGNED; int remoteVc; uint8_t inputBuffer[BUFFER_SIZE]; uint8_t outputBuffer[VC_SERIAL_HEADER_BYTES + BUFFER_SIZE]; +int timeoutCount; int cmdToRadio(uint8_t * buffer, int length) { @@ -451,6 +452,7 @@ main ( } //Get myVc address + findMyVc = true; cmdToRadio((uint8_t *)GET_MY_VC_ADDRESS, strlen(GET_MY_VC_ADDRESS)); //Break the links if requested @@ -475,6 +477,18 @@ main ( FD_SET(tty, &readfds); status = select(maxfds + 1, &readfds, &writefds, &exceptfds, &timeout); + //Check for timeout + if ((status == 0) && (timeoutCount++ >= 1000)) + { + timeoutCount = 0; + if (myVc == VC_UNASSIGNED) + { + //Get myVc address + findMyVc = true; + cmdToRadio((uint8_t *)GET_MY_VC_ADDRESS, strlen(GET_MY_VC_ADDRESS)); + } + } + //Determine if console input is available if (FD_ISSET(STDIN, &readfds)) { From bb659dcad9509d408ee2bae45891fa398ab91654 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 26 Nov 2022 12:40:58 -0800 Subject: [PATCH 184/594] VC: Wait for the server to come up --- Firmware/LoRaSerial_Firmware/States.ino | 67 ++++++++++++++++++++++--- Firmware/LoRaSerial_Firmware/settings.h | 11 ++-- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 885c5bf6..3143febf 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -30,6 +30,7 @@ void updateRadioState() static uint8_t rexmtLength; static uint8_t rexmtFrameSentCount; static uint8_t rexmtTxDestVc; + bool serverLinkBroken; VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; @@ -1567,6 +1568,32 @@ void updateRadioState() */ + case RADIO_VC_WAIT_SERVER: + if (myVc == VC_SERVER) + { + changeState(RADIO_VC_WAIT_RECEIVE); + break; + } + + //If dio0ISR has fired, a packet has arrived + currentMillis = millis(); + if (transactionComplete == true) + { + //Decode the received datagram + PacketType packetType = rcvDatagram(); + + //Process the received datagram + if ((packetType == DATAGRAM_VC_HEARTBEAT) && (rxSrcVc == VC_SERVER)) + { + vcReceiveHeartbeat(RADIO_VC_WAIT_TX_DONE, millis() - currentMillis); + changeState(RADIO_VC_WAIT_RECEIVE); + } + else + //Ignore this datagram + triggerEvent(TRIGGER_BAD_PACKET); + } + break; + case RADIO_VC_WAIT_TX_DONE: //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) @@ -1589,8 +1616,6 @@ void updateRadioState() currentMillis = millis(); if (transactionComplete == true) { - trainingPreviousRxInProgress = false; - //Decode the received datagram PacketType packetType = rcvDatagram(); @@ -1672,6 +1697,7 @@ void updateRadioState() //Check for link timeout else { + serverLinkBroken = false; for (index = 0; index < MAX_VC; index++) { //Don't timeout the connection to myself @@ -1680,8 +1706,16 @@ void updateRadioState() //Determine if the link has timed out vc = &virtualCircuitList[index]; - if (vc->linkUp && ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) + if (vc->linkUp && (serverLinkBroken + || ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) + { + if (index == VC_SERVER) + { + serverLinkBroken = true; + changeState(RADIO_VC_WAIT_SERVER); + } vcBreakLink(index); + } } } break; @@ -1708,8 +1742,6 @@ void updateRadioState() currentMillis = millis(); if (transactionComplete == true) { - trainingPreviousRxInProgress = false; - //Decode the received datagram PacketType packetType = rcvDatagram(); @@ -1819,12 +1851,28 @@ void updateRadioState() //Failed to reach the other system, break the link vcAckTimer = 0; vcBreakLink(txDestVc); + if (txDestVc != VC_SERVER) + changeState(RADIO_VC_WAIT_RECEIVE); + else + { + for (index = 0; index < MAX_VC; index++) + { + //Don't timeout the connection to myself + if (index == myVc) + continue; + + //Break all of the links + vcBreakLink(index); + } + changeState(RADIO_VC_WAIT_SERVER); + } } } //Check for link timeout else { + serverLinkBroken = false; for (index = 0; index < MAX_VC; index++) { //Don't timeout the connection to myself @@ -1833,13 +1881,18 @@ void updateRadioState() //Determine if the link has timed out vc = &virtualCircuitList[index]; - if (vc->linkUp && ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) + if (vc->linkUp && (serverLinkBroken + || ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) { + if (index == VC_SERVER) + serverLinkBroken = true; vcBreakLink(index); - if (index == rexmtTxDestVc) + if ((index == rexmtTxDestVc) && (!serverLinkBroken)) changeState(RADIO_VC_WAIT_RECEIVE); } } + if (serverLinkBroken) + changeState(RADIO_VC_WAIT_SERVER); } break; } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index d82b693a..1fc70a40 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -35,6 +35,7 @@ typedef enum RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, //Virtual-Circuit states + RADIO_VC_WAIT_SERVER, RADIO_VC_WAIT_TX_DONE, RADIO_VC_WAIT_RECEIVE, RADIO_VC_WAIT_TX_DONE_ACK, @@ -97,10 +98,12 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, 0, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //25 //V2 - Virtual circuit states - {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "V2 VC: Wait for TX done"}, //26 - {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "V2 VC: Wait for receive"}, //27 - {RADIO_VC_WAIT_TX_DONE_ACK, 0, "VC_WAIT_TX_DONE_ACK", "V2 VC: Wait for TX done then ACK"}, //28 - {RADIO_VC_WAIT_ACK, 1, "VC_WAIT_ACK", "V2 VC: Wait for ACK"}, //29 + // State RX Name Description + {RADIO_VC_WAIT_SERVER, 1, "VC_WAIT_SERVER", "V2 VC: Wait for the server"}, //26 + {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "V2 VC: Wait for TX done"}, //27 + {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "V2 VC: Wait for receive"}, //28 + {RADIO_VC_WAIT_TX_DONE_ACK, 0, "VC_WAIT_TX_DONE_ACK", "V2 VC: Wait for TX done then ACK"}, //29 + {RADIO_VC_WAIT_ACK, 1, "VC_WAIT_ACK", "V2 VC: Wait for ACK"}, //30 }; //Possible types of packets received From 4470d269e95f6a0ae6b738069308363de253b37a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 28 Nov 2022 11:25:28 -0800 Subject: [PATCH 185/594] VC: Simplify the state machine by prioritizing receive processing Receive priority: 1: Hello messages - Send HELLO frames to keep the links up 2: Retransmission - Get an acknowledgement for the last message or break the link 3: Remote command responses - Finish the last command 4: Data - Send data to the remote system --- Firmware/LoRaSerial_Firmware/States.ino | 300 ++++++++---------------- Firmware/LoRaSerial_Firmware/settings.h | 4 - 2 files changed, 95 insertions(+), 209 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3143febf..2ad17937 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1417,9 +1417,7 @@ void updateRadioState() //Check for a receive timeout else if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) - { xmitDatagramMpTrainingPing(); - } break; case RADIO_MP_WAIT_TX_PARAM_ACK_DONE: @@ -1486,6 +1484,7 @@ void updateRadioState() case DATAGRAM_TRAINING_PING: //Save the client ID memcpy(trainingPartnerID, rxData, UNIQUE_ID_BYTES); + //Wait for the transmit to complete if (xmitDatagramMpRadioParameters(trainingPartnerID) == true) changeState(RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE); @@ -1543,28 +1542,26 @@ void updateRadioState() RADIO_RESET | V + RADIO_VC_WAIT_SERVER + | + V +<-------------------------. | | | Send VC_HEARTBEAT | | | V | - RADIO_VC_WAIT_TX_DONE | - | | - V | Heartbeat timeout - .-----> RADIO_VC_WAIT_RECEIVE ---------------' + .-----> RADIO_VC_WAIT_TX_DONE | + | | | + | V | Heartbeat timeout + | RADIO_VC_WAIT_RECEIVE ---------------' | | - | | Receive serial data - | | Send DATA + | ACK Timeout | Receive serial data + | Retransmit | Send DATA | | - | V - | Receive ACK +<----------------+<----------------------. - | | ^ | - | V | | Send ACK - | RADIO_VC_WAIT_TC_DONE_ACK | Send VC_HEARTBEAT | - | | | | Receive DATA - | V | Heartbeat timeout | - '-------- RADIO_VC_WAIT_ACK --------+-----------------------' - + | Remote Cmd Rsp | Remote Cmd + | Send Cmd Rsp | Send remote Cmd + | | + '-----------------' */ @@ -1585,7 +1582,7 @@ void updateRadioState() //Process the received datagram if ((packetType == DATAGRAM_VC_HEARTBEAT) && (rxSrcVc == VC_SERVER)) { - vcReceiveHeartbeat(RADIO_VC_WAIT_TX_DONE, millis() - currentMillis); + vcReceiveHeartbeat(millis() - currentMillis); changeState(RADIO_VC_WAIT_RECEIVE); } else @@ -1627,7 +1624,7 @@ void updateRadioState() break; case DATAGRAM_VC_HEARTBEAT: - vcReceiveHeartbeat(RADIO_VC_WAIT_TX_DONE, millis() - currentMillis); + vcReceiveHeartbeat(millis() - currentMillis); break; case DATAGRAM_DATA: @@ -1664,7 +1661,9 @@ void updateRadioState() } } + //---------- //Transmit a HEARTBEAT if necessary + //---------- else if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime) && (receiveInProcess() == false)) { @@ -1673,203 +1672,92 @@ void updateRadioState() changeState(RADIO_VC_WAIT_TX_DONE); } - //Check for data to send - else if ((receiveInProcess() == false) && (vcSerialMessageReceived())) + //---------- + //Wait for an outstanding ACK until it is received, don't transmit any other data + //---------- + else if (vcAckTimer) { - //No need to add the VC header since the header is in the radioTxBuffer - //Transmit the packet - triggerEvent(TRIGGER_VC_TX_DATA); - if (xmitDatagramP2PData() == true) + //Check for retransmit needed + if ((currentMillis - vcAckTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { - vcAckTimer = datagramTimer; - - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - if (!vcAckTimer) - vcAckTimer = 1; - - //Save the message for retransmission - SAVE_TX_BUFFER(); - rexmtTxDestVc = txDestVc; - changeState(RADIO_VC_WAIT_TX_DONE_ACK); - } - } - - //Check for link timeout - else - { - serverLinkBroken = false; - for (index = 0; index < MAX_VC; index++) - { - //Don't timeout the connection to myself - if (index == myVc) - continue; - - //Determine if the link has timed out - vc = &virtualCircuitList[index]; - if (vc->linkUp && (serverLinkBroken - || ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) + //Determine if another retransmit is allowed + txDestVc = rexmtTxDestVc; + if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { - if (index == VC_SERVER) - { - serverLinkBroken = true; - changeState(RADIO_VC_WAIT_SERVER); - } - vcBreakLink(index); - } - } - } - break; - - case RADIO_VC_WAIT_TX_DONE_ACK: - //If dio0ISR has fired, we are done transmitting - if (transactionComplete == true) - { - transactionComplete = false; - - //Indicate that the receive is complete - triggerEvent(TRIGGER_VC_TX_DONE); - - //Start the receive operation - returnToReceiving(); - - //Set the next state - changeState(RADIO_VC_WAIT_ACK); - } - break; - - case RADIO_VC_WAIT_ACK: - //If dio0ISR has fired, a packet has arrived - currentMillis = millis(); - if (transactionComplete == true) - { - //Decode the received datagram - PacketType packetType = rcvDatagram(); + rexmtFrameSentCount++; - //Process the received datagram - switch (packetType) - { - default: - triggerEvent(TRIGGER_BAD_PACKET); - break; - - case DATAGRAM_VC_HEARTBEAT: - vcReceiveHeartbeat(RADIO_VC_WAIT_TX_DONE_ACK, millis() - currentMillis); - break; - - case DATAGRAM_DATA: - //Move the data into the serial output buffer - if (settings.debugSerial) + //Restore the message for retransmission + RESTORE_TX_BUFFER(); + if (settings.debugDatagrams) { - systemPrint("updateRadioState moving "); - systemPrint(rxDataBytes); - systemPrintln(" bytes from inputBuffer into serialTransmitBuffer"); + systemPrintTimestamp(); + systemPrint("TX: Retransmit "); + systemPrint(frameSentCount); + systemPrint(", "); + systemPrint(v2DatagramType[txControl.datagramType]); + switch (txControl.datagramType) + { + default: + systemPrintln(); + break; + + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + case DATAGRAM_HEARTBEAT: + systemPrint(" (ACK #"); + systemPrint(txControl.ackNumber); + systemPrint(")"); + systemPrintln(); + break; + } outputSerialData(true); } - systemWrite(START_OF_VC_SERIAL); - serialBufferOutput(rxData, rxDataBytes); - //Acknowledge the data frame - if (xmitVcAckFrame(rxSrcVc)) - changeState(RADIO_VC_WAIT_TX_DONE_ACK); - break; + //Retransmit the packet + if (retransmitDatagram(((uint8_t)txDestVc <= MAX_VC) ? &virtualCircuitList[txDestVc] : NULL)) + changeState(RADIO_VC_WAIT_TX_DONE); - case DATAGRAM_DATA_ACK: + //Since vcAckTimer is off when equal to zero, force it to a non-zero value + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; + lostFrames++; + } + else + { + //Failed to reach the other system, break the link vcAckTimer = 0; - changeState(RADIO_VC_WAIT_RECEIVE); - break; - - case DATAGRAM_DUPLICATE: - printPacketQuality(); - updateRSSI(); //Adjust LEDs to RSSI level - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - //Acknowledge the data frame - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); - if (xmitVcAckFrame(rxSrcVc)) - changeState(RADIO_VC_WAIT_TX_DONE); - break; + vcBreakLink(txDestVc); + } } } - //Transmit a HEARTBEAT if necessary - else if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime) - && (receiveInProcess() == false)) - { - //Send another heartbeat - if (xmitVcHeartbeat(myVc, myUniqueId)) - changeState(RADIO_VC_WAIT_TX_DONE_ACK); - } - - //Check for retransmit needed - else if (vcAckTimer && ((currentMillis - vcAckTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset()))) + //---------- + //Check for data to send + //---------- + else if ((receiveInProcess() == false) && (vcSerialMessageReceived())) { - //Determine if another retransmit is allowed - txDestVc = rexmtTxDestVc; - if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) - { - rexmtFrameSentCount++; - - //Restore the message for retransmission - RESTORE_TX_BUFFER(); - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("TX: Retransmit "); - systemPrint(frameSentCount); - systemPrint(", "); - systemPrint(v2DatagramType[txControl.datagramType]); - switch (txControl.datagramType) - { - default: - systemPrintln(); - break; - - case DATAGRAM_DATA: - case DATAGRAM_DATA_ACK: - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - systemPrint(" (ACK #"); - systemPrint(txControl.ackNumber); - systemPrint(")"); - systemPrintln(); - break; - } - outputSerialData(true); - } + //No need to add the VC header since the header is in the radioTxBuffer + //Transmit the packet + triggerEvent(TRIGGER_VC_TX_DATA); + if (xmitDatagramP2PData() == true) + changeState(RADIO_VC_WAIT_TX_DONE); + vcAckTimer = datagramTimer; - //Retransmit the packet - retransmitDatagram(((uint8_t)txDestVc <= MAX_VC) ? &virtualCircuitList[txDestVc] : NULL); - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; - lostFrames++; - changeState(RADIO_VC_WAIT_TX_DONE_ACK); - } - else - { - //Failed to reach the other system, break the link - vcAckTimer = 0; - vcBreakLink(txDestVc); - if (txDestVc != VC_SERVER) - changeState(RADIO_VC_WAIT_RECEIVE); - else - { - for (index = 0; index < MAX_VC; index++) - { - //Don't timeout the connection to myself - if (index == myVc) - continue; + //Since vcAckTimer is off when equal to zero, force it to a non-zero value + if (!vcAckTimer) + vcAckTimer = 1; - //Break all of the links - vcBreakLink(index); - } - changeState(RADIO_VC_WAIT_SERVER); - } - } + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; } + //---------- //Check for link timeout + //---------- else { serverLinkBroken = false; @@ -1885,14 +1773,13 @@ void updateRadioState() || ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) { if (index == VC_SERVER) + { serverLinkBroken = true; + changeState(RADIO_VC_WAIT_SERVER); + } vcBreakLink(index); - if ((index == rexmtTxDestVc) && (!serverLinkBroken)) - changeState(RADIO_VC_WAIT_RECEIVE); } } - if (serverLinkBroken) - changeState(RADIO_VC_WAIT_SERVER); } break; } @@ -2398,7 +2285,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) return vcLinkUp(index); } -void vcReceiveHeartbeat(RadioStates nextState, uint32_t rxMillis) +void vcReceiveHeartbeat(uint32_t rxMillis) { uint32_t deltaMillis; int vcSrc; @@ -2419,11 +2306,14 @@ void vcReceiveHeartbeat(RadioStates nextState, uint32_t rxMillis) //Translate the unique ID into an address byte vcSrc = vcIdToAddressByte(rxSrcVc, rxVcData); + if (vcSrc < 0) + return; - if (settings.server && (rxSrcVc == VC_UNASSIGNED) && (vcSrc >= 0)) + //When the client does not know its address, it is assigned by the server + if (settings.server && (rxSrcVc == VC_UNASSIGNED)) { //Assign the address to the client - xmitVcHeartbeat(vcSrc, rxVcData); - changeState(nextState); + if (xmitVcHeartbeat(vcSrc, rxVcData)) + changeState(RADIO_VC_WAIT_TX_DONE); } } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 1fc70a40..6f081e8d 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -38,8 +38,6 @@ typedef enum RADIO_VC_WAIT_SERVER, RADIO_VC_WAIT_TX_DONE, RADIO_VC_WAIT_RECEIVE, - RADIO_VC_WAIT_TX_DONE_ACK, - RADIO_VC_WAIT_ACK, RADIO_MAX_STATE, } RadioStates; @@ -102,8 +100,6 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_VC_WAIT_SERVER, 1, "VC_WAIT_SERVER", "V2 VC: Wait for the server"}, //26 {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "V2 VC: Wait for TX done"}, //27 {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "V2 VC: Wait for receive"}, //28 - {RADIO_VC_WAIT_TX_DONE_ACK, 0, "VC_WAIT_TX_DONE_ACK", "V2 VC: Wait for TX done then ACK"}, //29 - {RADIO_VC_WAIT_ACK, 1, "VC_WAIT_ACK", "V2 VC: Wait for ACK"}, //30 }; //Possible types of packets received From 341381a78351467dd08407bae850404085244955 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 28 Nov 2022 20:11:03 -0700 Subject: [PATCH 186/594] Add petTimeout --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 49207f20..1fcac1e8 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -137,7 +137,7 @@ unsigned long timerStart = 0; //Tracks how long our timer has been running since bool partialTimer = false; //After an ACK we reset and run a partial timer to sync units const int SYNC_PROCESSING_OVERHEAD = 3; //Number of milliseconds it takes to compute clock deltas before sync'ing clocks -uint16_t petTimeoutHalf = 0; //Half the amount of time before WDT. Helps reduce amount of time spent petting. +uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps reduce amount of time spent petting. unsigned long lastPet = 0; //Remebers time of last WDT pet. //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= From 4d52322a279661aa080e91e0659d020ed99766cc Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 06:02:15 -1000 Subject: [PATCH 187/594] VC: Modify protocol to support remote commands and responses --- .../Virtual_Circuit_Protocol.h | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 300a5a3b..9215a76d 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -5,16 +5,43 @@ // Constants //------------------------------------------------------------------------------ -//Virtual-Circuit source and destination index values -#define MAX_VC 8 +//Define the virtual circuit address byte +#define VCAB_NUMBER_BITS 5 +#define MAX_VC (1 << VCAB_NUMBER_BITS) +#define VCAB_NUMBER_MASK (MAX_VC - 1) +#define VCAB_CHANNEL_BITS (8 - VCAB_NUMBER_BITS) +#define VCAB_CHANNEL_MASK ((1 << VCAB_CHANNEL_BITS) - 1) + +//Communications over the virtual circuit are broken down into the following +//address spaces: +// +// 0 - Data communications +// 7 - Special virtual circuits + +//The following addresses are reserved for data communications: 0 - VCAB_NUMBER_MASK +//Address zero is reserved for the server #define VC_SERVER 0 -#define VC_BROADCAST -1 -#define VC_COMMAND -2 //Command input and command response -#define VC_UNASSIGNED -3 + +//Address space 7 is reserved for the following special addresses: + +#define VC_RSVD_SPECIAL_VCS ((int8_t)(VCAB_CHANNEL_MASK << VCAB_NUMBER_BITS)) +#define VC_BROADCAST ((int8_t)(VC_RSVD_SPECIAL_VCS | VCAB_NUMBER_MASK)) +#define VC_COMMAND (VC_BROADCAST - 1) //Command input and command response +#define VC_UNASSIGNED (VC_COMMAND - 1) //Source and destinations reserved for the local host -#define PC_COMMAND -17 //Command input and command response -#define PC_LINK_STATUS -18 //Asynchronous link status output +#define PC_COMMAND VC_RSVD_SPECIAL_VCS //Command input and command response +#define PC_LINK_STATUS (PC_COMMAND + 1) //Asynchronous link status output + +//Address space 1 and 2 are reserved for the host PC interface to support remote +//command processing. The radio removes these bits and converts them to the +//appropriate datagram type. Upon reception of one of these messages the bit is +//added back into the VC header and the message is delivered to the host PC. +//As a result, any host may send commands to any other host! +#define PC_REMOTE_COMMAND ((int8_t)(1 << VCAB_NUMBER_BITS)) +#define PC_REMOTE_RESPONSE ((int8_t)(2 << VCAB_NUMBER_BITS)) + +//Address spaces 3 - 6 are not currently defined //Field offsets in the VC HEARTBEAT frame #define VC_HB_UNIQUE_ID 0 @@ -110,4 +137,11 @@ typedef struct _VC_LINK_STATUS_MESSAGE #define LINK_DOWN 0 #define LINK_UP 1 +//------------------------------------------------------------------------------ +// Macros +//------------------------------------------------------------------------------ + +#define DATA_BYTES(vc_message_length) (vc_message_length - VC_RADIO_HEADER_BYTES) +#define DATA_BUFFER(data) (data + VC_RADIO_HEADER_BYTES) + #endif //__VIRTUAL_CIRCUIT_PROTOCOL_H__ From 56a99b0b868b3c17fc7ec6fd8fc787bb667e4e8e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 08:59:57 -1000 Subject: [PATCH 188/594] Alway convert the command to lower case --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 ++++ Firmware/LoRaSerial_Firmware/Serial.ino | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 4d352be8..acf2f7db 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -463,6 +463,10 @@ void checkCommand() || (commandString[commandLength -1] == '\n'))) commandLength -= 1; + //Upper case the command + for (index = 0; index < commandLength; index++) + commandBuffer[index] = toupper(commandBuffer[index]); + commandString[commandLength] = '\0'; //Terminate buffer if (commandLength < 2) //Too short reportERROR(); diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 21fffe8a..b34019b7 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -327,7 +327,7 @@ void processSerialInput() else { //Move this character into the command buffer - commandBuffer[commandLength++] = toupper(incoming); + commandBuffer[commandLength++] = incoming; commandLength %= sizeof(commandBuffer); } } From 3adf65b0ae2739a8712d0e1475030121ff820c00 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 06:03:22 -1000 Subject: [PATCH 189/594] VcServerTest: Modify to support remote commands --- Firmware/Tools/VcServerTest.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 2b48d042..0f400bd8 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -336,17 +336,17 @@ int radioToHost() } //Process the message - switch (header->radio.destVc) + if (header->radio.destVc == PC_LINK_STATUS) + radioToPcLinkStatus(header, VC_SERIAL_HEADER_BYTES + length); + + if (header->radio.destVc == (PC_REMOTE_RESPONSE | myVc)) + status = hostToStdout(data, length); + + else { - default: if ((header->radio.destVc == myVc) || (header->radio.destVc == VC_BROADCAST)) //Output this message status = hostToStdout(data, length); - break; - - case PC_LINK_STATUS: - radioToPcLinkStatus(header, VC_SERIAL_HEADER_BYTES + length); - break; } //Continue processing the rest of the data in the buffer @@ -399,7 +399,7 @@ main ( //Determine the remote VC address if ((sscanf(argv[2], "%d", &remoteVc) != 1) - || (remoteVc < VC_COMMAND) || (remoteVc >= MAX_VC)) + || ((remoteVc > PC_LINK_STATUS) && (remoteVc < VC_COMMAND))) { fprintf(stderr, "ERROR: Invalid target VC address, please use one of the following:\n"); if (myVc) From 05c4bdbb37746def80dcd137cfe335d93146579a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 07:03:23 -1000 Subject: [PATCH 190/594] VC: Don't send data when the link is down --- Firmware/LoRaSerial_Firmware/RadioV2.ino | 14 +++++++++++-- Firmware/LoRaSerial_Firmware/Serial.ino | 19 +++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 26 ++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index c35f7a18..9918bcde 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1785,11 +1785,21 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) //Drop this datagram if the receiver is active frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond - if (receiveInProcess() == true || transactionComplete == true) + if ((receiveInProcess() == true) || (transactionComplete == true) + || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].linkUp == false))) { triggerEvent(TRIGGER_TRANSMIT_CANCELED); if (settings.debugReceive || settings.debugDatagrams) - systemPrintln(receiveInProcess() ? "RXIP" : "RXTC"); + { + if (transactionComplete) + systemPrintln("RXTC"); + else if (receiveInProcess()) + systemPrintln("RXIP"); + else + systemPrintln("VC link down"); + outputSerialData(true); + } return (false); //Do not start transmit while RX is or has occured } else diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 21fffe8a..6fe7557b 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -515,6 +515,25 @@ bool vcSerialMessageReceived() break; } + //Verify that the destination link is up + if ((vcDest != VC_BROADCAST) + && (virtualCircuitList[vcDest & VCAB_NUMBER_MASK].linkUp == false)) + { + if (settings.debugSerial || settings.debugTransmit) + { + systemPrint("Link down "); + systemPrint((vcDest == VC_BROADCAST) ? vcDest : vcDest & VCAB_NUMBER_MASK); + systemPrintln(", discarding message!"); + outputSerialData(true); + } + + //Discard this message + radioTxTail += msgLength; + if (radioTxTail >= sizeof(radioTxBuffer)) + radioTxTail -= sizeof(radioTxBuffer); + break; + } + //Print the data ready for transmission if (settings.debugSerial) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 2ad17937..2f6bbdbb 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1677,11 +1677,19 @@ void updateRadioState() //---------- else if (vcAckTimer) { + //Verify that the link is still up + txDestVc = rexmtTxDestVc; + if ((txDestVc != VC_BROADCAST) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].linkUp == false)) + { + //Stop the retransmits + vcAckTimer = 0; + } + //Check for retransmit needed - if ((currentMillis - vcAckTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + else if ((currentMillis - vcAckTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { //Determine if another retransmit is allowed - txDestVc = rexmtTxDestVc; if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { rexmtFrameSentCount++; @@ -1772,11 +1780,20 @@ void updateRadioState() if (vc->linkUp && (serverLinkBroken || ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) { + //When the server connection breaks, break all other connections and + //wait for the server if (index == VC_SERVER) { serverLinkBroken = true; changeState(RADIO_VC_WAIT_SERVER); } + + //If waiting for an ACK and the link breaks, stop the retransmissions + //by stopping the ACK timer. + if (vcAckTimer && (index == rexmtTxDestVc)) + vcAckTimer = 0; + + //Break the link vcBreakLink(index); } } @@ -2171,6 +2188,11 @@ void vcBreakLink(int8_t vcIndex) { VIRTUAL_CIRCUIT * vc; + //Only handle real VCs + if ((vcIndex >= PC_COMMAND) && (vcIndex <= VC_BROADCAST)) + return; + vcIndex &= VCAB_NUMBER_MASK; + //Get the virtual circuit data structure if ((vcIndex >= 0) && (vcIndex != myVc) && ( vcIndex < MAX_VC)) { From ab730dd257d296f21dd8e6f3796f40a37e37cee4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 08:59:57 -1000 Subject: [PATCH 191/594] Alway convert the command to lower case --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 ++++ Firmware/LoRaSerial_Firmware/Serial.ino | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 4d352be8..acf2f7db 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -463,6 +463,10 @@ void checkCommand() || (commandString[commandLength -1] == '\n'))) commandLength -= 1; + //Upper case the command + for (index = 0; index < commandLength; index++) + commandBuffer[index] = toupper(commandBuffer[index]); + commandString[commandLength] = '\0'; //Terminate buffer if (commandLength < 2) //Too short reportERROR(); diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 6fe7557b..ec4d2cf7 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -327,7 +327,7 @@ void processSerialInput() else { //Move this character into the command buffer - commandBuffer[commandLength++] = toupper(incoming); + commandBuffer[commandLength++] = incoming; commandLength %= sizeof(commandBuffer); } } From 362d3c189ad3d9b50ec9896973920d5b054daa41 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 06:05:31 -1000 Subject: [PATCH 192/594] VC: Remote command support any VC to any VC This commit includes the following changes: * Modify serial to pass remote commands to the transmit code * Modify radio code to display the proper VC numbers * Leave all remote commands in the outgoingPacket * Process the remote command locally - Place response in commandTXBuffer - Divide up response and place in serialTransmitBuffer * Place the remote command into the command buffer and process it * Transmit command response to VC that issued the command * Receive the command response --- .../LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/RadioV2.ino | 9 +- Firmware/LoRaSerial_Firmware/Serial.ino | 76 ++++++-- Firmware/LoRaSerial_Firmware/States.ino | 176 +++++++++++++++++- .../Virtual_Circuit_Protocol.h | 1 + 5 files changed, 233 insertions(+), 30 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 9d01e349..07bd8cf0 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -329,7 +329,6 @@ unsigned long lastTrainBlink = 0; //Controls LED during training //Global variables - Radio (General) //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver -uint8_t packetSize = 0; //Tracks how much data + control trailer uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission uint16_t ackAirTime = 0; //Recalc'd with each change of settings uint8_t frameSentCount = 0; //Increases each time a frame is sent diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino index 9918bcde..c2f14d9f 100644 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ b/Firmware/LoRaSerial_Firmware/RadioV2.ino @@ -1224,9 +1224,9 @@ PacketType rcvDatagram() systemPrint("RX: "); if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { - systemPrint((uint8_t)rxDestVc); + systemPrint((uint8_t)((rxDestVc == VC_BROADCAST) ? rxDestVc : rxDestVc & VCAB_NUMBER_MASK)); systemPrint(" <-- "); - systemPrint((uint8_t)rxSrcVc); + systemPrint((uint8_t)((rxSrcVc == VC_UNASSIGNED) ? rxSrcVc : rxSrcVc & VCAB_NUMBER_MASK)); systemWrite(' '); } systemPrint(v2DatagramType[datagramType]); @@ -1399,9 +1399,9 @@ bool transmitDatagram() systemPrint("TX: "); if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { - systemPrint((uint8_t)srcVc); + systemPrint((uint8_t)((srcVc == VC_UNASSIGNED) ? srcVc : srcVc & VCAB_NUMBER_MASK)); systemPrint(" --> "); - systemPrint((uint8_t)txDestVc); + systemPrint((uint8_t)((txDestVc == VC_BROADCAST) ? txDestVc : txDestVc & VCAB_NUMBER_MASK)); systemWrite(' '); } systemPrint(v2DatagramType[txControl.datagramType]); @@ -1792,6 +1792,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) triggerEvent(TRIGGER_TRANSMIT_CANCELED); if (settings.debugReceive || settings.debugDatagrams) { + systemPrint("TX failed: "); if (transactionComplete) systemPrintln("RXTC"); else if (receiveInProcess()) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index ec4d2cf7..1841b350 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -146,36 +146,73 @@ uint16_t availableTXCommandBytes() return (sizeof(commandTXBuffer) - commandTXTail + commandTXHead); } -//Send a portion of the commandTXBuffer to outgoingPacket -void readyOutgoingCommandPacket() +//Send a portion of the commandTXBuffer to serialTransmitBuffer +void readyLocalCommandPacket() { + uint16_t bytesToSend; uint16_t length; - uint16_t bytesToSend = availableTXCommandBytes(); - if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; + uint16_t maxLength; + + bytesToSend = availableTXCommandBytes(); + maxLength = maxDatagramSize; + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + maxLength -= VC_RADIO_HEADER_BYTES; + if (bytesToSend > maxLength) + bytesToSend = maxLength; + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Build the VC header + serialOutputByte(START_OF_VC_SERIAL); + serialOutputByte(bytesToSend + VC_RADIO_HEADER_BYTES); + serialOutputByte(PC_REMOTE_RESPONSE | myVc); + serialOutputByte(myVc); + if (settings.debugSerial) + systemPrintln("Built VC header in serialTransmitBuffer"); + } - //SF6 requires an implicit header which means there is no dataLength in the header - if (settings.radioSpreadFactor == 6) + //Place the command response bytes into serialTransmitBuffer + if (settings.debugSerial) + { + systemPrint("Moving "); + systemPrint(bytesToSend); + systemPrintln(" bytes from commandTXBuffer into serialTransmitBuffer"); + outputSerialData(true); + } + for (length = 0; length < bytesToSend; length++) { - if (bytesToSend > maxDatagramSize) bytesToSend = maxDatagramSize; //We are going to transmit 255 bytes no matter what + serialOutputByte(commandTXBuffer[commandTXTail++]); + commandTXTail %= sizeof(commandTXBuffer); } +} + +//Send a portion of the commandTXBuffer to outgoingPacket +uint8_t readyOutgoingCommandPacket(uint16_t offset) +{ + uint16_t bytesToSend = availableTXCommandBytes(); + uint16_t length; + uint16_t maxLength; - packetSize = bytesToSend; + maxLength = maxDatagramSize - offset; + bytesToSend = availableTXCommandBytes(); + if (bytesToSend > maxLength) + bytesToSend = maxLength; //Determine the number of bytes to send length = 0; - if ((commandTXTail + packetSize) > sizeof(commandTXBuffer)) + if ((commandTXTail + bytesToSend) > sizeof(commandTXBuffer)) { //Copy the first portion of the buffer length = sizeof(commandTXBuffer) - commandTXTail; - memcpy(&outgoingPacket[headerBytes], &commandTXBuffer[commandTXTail], length); + memcpy(&outgoingPacket[headerBytes + offset], &commandTXBuffer[commandTXTail], length); commandTXTail = 0; } //Copy the remaining portion of the buffer - memcpy(&outgoingPacket[headerBytes + length], &commandTXBuffer[commandTXTail], packetSize - length); - commandTXTail += packetSize - length; + memcpy(&outgoingPacket[headerBytes + offset + length], &commandTXBuffer[commandTXTail], bytesToSend - length); + commandTXTail += bytesToSend - length; commandTXTail %= sizeof(commandTXBuffer); - endOfTxData += packetSize; + endOfTxData += bytesToSend; + return (uint8_t)bytesToSend; } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -432,6 +469,7 @@ uint8_t vcSerialMsgGetVcDest() //Determine if received serial data may be sent to the remote system bool vcSerialMessageReceived() { + int8_t channel; uint16_t dataBytes; uint8_t msgLength; int8_t vcDest; @@ -498,7 +536,8 @@ bool vcSerialMessageReceived() } //Validate the destination VC - if ((vcDest < VC_BROADCAST) || (vcDest >= MAX_VC)) + //None of the local host targets belong in the radioTxBuffer + if ((vcDest >= PC_COMMAND) && (vcDest < VC_BROADCAST)) { if (settings.debugSerial || settings.debugTransmit) { @@ -545,10 +584,12 @@ bool vcSerialMessageReceived() //If sending to ourself, just place the data in the serial output buffer readyOutgoingPacket(msgLength); - if (vcDest == myVc) + channel = GET_CHANNEL_NUMBER(vcDest); + if ((vcDest != VC_BROADCAST) && ((vcDest & VCAB_NUMBER_MASK) == myVc) + && (channel == 0)) { if (settings.debugSerial) - systemPrintln("VC serial RX: Sending to ourself"); + systemPrintln("VC: Sending data to ourself"); systemWrite(START_OF_VC_SERIAL); systemWrite(outgoingPacket, msgLength); endOfTxData -= msgLength; @@ -648,6 +689,7 @@ void vcProcessSerialInput() //Send data over the radio link default: //Validate the source virtual circuit + //Data that is being transmitted should always use myVC if ((vcSrc != myVc) || (myVc == VC_UNASSIGNED)) { if (settings.debugSerial) @@ -664,7 +706,7 @@ void vcProcessSerialInput() } //Verify the destination virtual circuit - if ((vcDest < VC_BROADCAST) || (vcDest >= MAX_VC)) + if ((vcDest > PC_LINK_STATUS) && (vcDest < VC_BROADCAST)) { if (settings.debugSerial) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 2f6bbdbb..5591523a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -17,6 +17,7 @@ void updateRadioState() { int8_t addressByte; + uint8_t channel; unsigned long clockOffset; unsigned long currentMillis; unsigned long deltaMillis; @@ -25,6 +26,7 @@ void updateRadioState() int index; uint16_t length; uint8_t radioSeed; + static uint8_t rmtCmdVc; static uint8_t rexmtBuffer[MAX_PACKET_SIZE]; static CONTROL_U8 rexmtControl; static uint8_t rexmtLength; @@ -691,7 +693,7 @@ void updateRadioState() else if (availableTXCommandBytes()) //If we have command bytes to send out { //Load command bytes into outgoing packet - readyOutgoingCommandPacket(); + readyOutgoingCommandPacket(0); triggerEvent(TRIGGER_LINK_DATA_XMIT); @@ -1615,6 +1617,7 @@ void updateRadioState() { //Decode the received datagram PacketType packetType = rcvDatagram(); + vcHeader = (VC_RADIO_MESSAGE_HEADER *)rxData; //Process the received datagram switch (packetType) @@ -1658,6 +1661,73 @@ void updateRadioState() if (xmitVcAckFrame(rxSrcVc)) changeState(RADIO_VC_WAIT_TX_DONE); break; + + case DATAGRAM_REMOTE_COMMAND: + rmtCmdVc = rxSrcVc; + + //Copy the command into the command buffer + commandLength = rxDataBytes - VC_RADIO_HEADER_BYTES; + memcpy(commandBuffer, &rxData[VC_RADIO_HEADER_BYTES], commandLength); + if (settings.debugSerial) + { + systemPrint("RX: Moving "); + systemPrint(commandLength); + systemPrintln(" bytes into commandBuffer"); + outputSerialData(true); + dumpBuffer((uint8_t *)commandBuffer, commandLength); + } + petWDT(); + + updateRSSI(); //Adjust LEDs to RSSI level + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); + + //Transmit ACK + if (xmitVcAckFrame(rxSrcVc)) + changeState(RADIO_VC_WAIT_TX_DONE); + + //Process the command + petWDT(); + printerEndpoint = PRINT_TO_RF; //Send prints to RF link + checkCommand(); //Parse the command buffer + petWDT(); + printerEndpoint = PRINT_TO_SERIAL; + length = availableTXCommandBytes(); + if (settings.debugSerial) + { + systemPrint("RX: checkCommand placed "); + systemPrint(commandLength); + systemPrintln(" bytes into commandTXBuffer"); + dumpCircularBuffer(commandTXBuffer, commandTXTail, sizeof(commandTXBuffer), length); + outputSerialData(true); + petWDT(); + } + break; + + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + //Debug the serial path + if (settings.debugSerial) + { + systemPrint("Moving "); + systemPrint(rxDataBytes); + systemPrintln(" from incomingBuffer to serialTransmitBuffer"); + dumpBuffer(rxData, rxDataBytes); + outputSerialData(true); + } + + //Place the data in to the serialTransmitBuffer + serialOutputByte(START_OF_VC_SERIAL); + vcHeader->destVc |= PC_REMOTE_RESPONSE; + serialBufferOutput(rxData, rxDataBytes); + + updateRSSI(); //Adjust LEDs to RSSI level + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + //ACK the command response + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE); + if (xmitVcAckFrame(rxSrcVc)) //Transmit ACK + changeState(RADIO_VC_WAIT_TX_DONE); + break; } } @@ -1743,18 +1813,22 @@ void updateRadioState() } //---------- - //Check for data to send + //Prioritize sending the command response higher than sending data //---------- - else if ((receiveInProcess() == false) && (vcSerialMessageReceived())) + else if (availableTXCommandBytes()) { - //No need to add the VC header since the header is in the radioTxBuffer - //Transmit the packet - triggerEvent(TRIGGER_VC_TX_DATA); - if (xmitDatagramP2PData() == true) + //Send the next portion of the command response + vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; + endOfTxData += VC_RADIO_HEADER_BYTES; + vcHeader->destVc = rmtCmdVc; + vcHeader->srcVc = myVc; + vcHeader->length = readyOutgoingCommandPacket(VC_RADIO_HEADER_BYTES) + + VC_RADIO_HEADER_BYTES; + if (xmitDatagramP2PCommandResponse()) changeState(RADIO_VC_WAIT_TX_DONE); - vcAckTimer = datagramTimer; //Since vcAckTimer is off when equal to zero, force it to a non-zero value + vcAckTimer = datagramTimer; if (!vcAckTimer) vcAckTimer = 1; @@ -1763,6 +1837,92 @@ void updateRadioState() rexmtTxDestVc = txDestVc; } + //---------- + //Check for data to send + //---------- + else if ((receiveInProcess() == false) && (vcSerialMessageReceived())) + { + //No need to add the VC header since the header is in the radioTxBuffer + //Get the VC header + vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; + channel = (vcHeader->destVc >> VCAB_NUMBER_BITS) & VCAB_CHANNEL_MASK; + switch (channel) + { + case 0: //Data packets + //Transmit the packet + triggerEvent(TRIGGER_VC_TX_DATA); + if (xmitDatagramP2PData() == true) + changeState(RADIO_VC_WAIT_TX_DONE); + + //Since vcAckTimer is off when equal to zero, force it to a non-zero value + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; + + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + break; + + case 1: //Remote command packets + if ((vcHeader->destVc & VCAB_NUMBER_MASK) == myVc) + { + //Copy the command into the command buffer + commandLength = endOfTxData - &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES]; + memcpy(commandBuffer, &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES], commandLength); + if (settings.debugSerial) + { + systemPrint("RX: Moving "); + systemPrint(commandLength); + systemPrintln(" bytes into commandBuffer"); + outputSerialData(true); + dumpBuffer((uint8_t *)commandBuffer, commandLength); + } + petWDT(); + + //Reset the buffer data pointer for the next transmit operation + endOfTxData = &outgoingPacket[headerBytes]; + + //Process the command + petWDT(); + printerEndpoint = PRINT_TO_RF; //Send prints to RF link + checkCommand(); //Parse the command buffer + petWDT(); + printerEndpoint = PRINT_TO_SERIAL; + length = availableTXCommandBytes(); + if (settings.debugSerial) + { + systemPrint("RX: checkCommand placed "); + systemPrint(length); + systemPrintln(" bytes into commandTXBuffer"); + dumpCircularBuffer(commandTXBuffer, commandTXTail, sizeof(commandTXBuffer), length); + outputSerialData(true); + petWDT(); + } + + //Break up the command response + while (availableTXCommandBytes()) + readyLocalCommandPacket(); + break; + } + + //Send the remote command + vcHeader->destVc &= VCAB_NUMBER_MASK; + if (xmitDatagramP2PCommand() == true) + changeState(RADIO_VC_WAIT_TX_DONE); + + //Since vcAckTimer is off when equal to zero, force it to a non-zero value + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; + + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + break; + } + } + //---------- //Check for link timeout //---------- diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 9215a76d..4752e28f 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -143,5 +143,6 @@ typedef struct _VC_LINK_STATUS_MESSAGE #define DATA_BYTES(vc_message_length) (vc_message_length - VC_RADIO_HEADER_BYTES) #define DATA_BUFFER(data) (data + VC_RADIO_HEADER_BYTES) +#define GET_CHANNEL_NUMBER(vc) (vc & (VCAB_CHANNEL_MASK << VCAB_NUMBER_BITS)) #endif //__VIRTUAL_CIRCUIT_PROTOCOL_H__ From 41d520b597882886745d9677b18886dcb72b7317 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 17:49:53 -1000 Subject: [PATCH 193/594] Remove the V2 protocol indicator --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- .../LoRaSerial_Firmware.ino | 4 +- Firmware/LoRaSerial_Firmware/Radio.ino | 2063 +++++++++++++++++ Firmware/LoRaSerial_Firmware/RadioV2.ino | 2058 ---------------- Firmware/LoRaSerial_Firmware/States.ino | 60 +- Firmware/LoRaSerial_Firmware/Train.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 86 +- 7 files changed, 2140 insertions(+), 2135 deletions(-) delete mode 100644 Firmware/LoRaSerial_Firmware/RadioV2.ino diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index acf2f7db..bd1a9249 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -93,7 +93,7 @@ bool commandAT(const char * commandString) } else if (settings.operatingMode == MODE_POINT_TO_POINT) { - v2BreakLink(); + breakLink(); outputSerialData(true); } diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 07bd8cf0..c52bfccc 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -369,7 +369,7 @@ uint32_t netIdMismatch; //Total number of mismatched Net ID frames unsigned long lastLinkUpTime = 0; //Mark when link was first established //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//Global variables - V2 Protocol +//Global variables - Radio Protocol //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Frame size values @@ -460,7 +460,7 @@ void setup() verifyRadioStateTable(); //Verify that the state table contains all of the states in increasing order - verifyV2DatagramType(); //Verify that the datagram type table contains all of the datagram types + verifyRadioDatagramType(); //Verify that the datagram type table contains all of the datagram types arch.uniqueID(myUniqueId); //Get the unique ID diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e6093fe7..71ff8872 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -590,3 +590,2066 @@ int16_t getLinkupOffset() return (settings.maxDwellTime - getReceiveCompletionOffset() - linkupOffset); //Reduce the default window by the offset } + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Table for use in calculating the software CRC-16 +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +const uint16_t crc16Table[256] = +{ + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, + 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, + 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, + 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, + 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, + 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, + 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, + 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, + 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, + 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, + 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, + 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, + 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, + 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, + 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, + 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, + 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, + 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, + 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, + 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, + 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, + 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, + 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 +}; + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Point-To-Point Training +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Ping the other radio in the point-to-point configuration +bool xmitDatagramP2PTrainingPing() +{ + /* + endOfTxData ---. + | + V + +---------+----------+ + | | | + | Control | Trailer | + | 8 bits | n Bytes | + +---------+----------+ + */ + + txControl.datagramType = DATAGRAM_P2P_TRAINING_PING; + return (transmitDatagram()); +} + +//Build the parameters packet used for training +bool xmitDatagramP2pTrainingParams() +{ + Settings params; + + //Initialize the radio parameters + memcpy(¶ms, &originalSettings, sizeof(settings)); + params.operatingMode = MODE_POINT_TO_POINT; + + //Add the radio parameters + memcpy(endOfTxData, ¶ms, sizeof(params)); + endOfTxData += sizeof(params); + + /* + endOfTxData ---. + | + V + +----------+---------+--- ... ---+----------+ + | Optional | | Radio | Optional | + | NET ID | Control | Parameters | Trailer | + | 8 bits | 8 bits | n bytes | n Bytes | + +----------+---------+-------------+----------+ + */ + + txControl.datagramType = DATAGRAM_P2P_TRAINING_PARAMS; + return (transmitDatagram()); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Point-To-Point: Bring up the link +// +//A three way handshake is used to get both systems to agree that data can flow in both +//directions. This handshake is also used to synchronize the HOP timer. +/* + System A System B + + RESET RESET + | | + Channel 0 | | Channel 0 + V V + .----> P2P_NO_LINK P2P_NO_LINK + | | Tx PING | + | Timeout | | + | V | + | P2P_WAIT_TX_PING_DONE | + | | | + | | Tx Complete - - - - - > | Rx PING + | | Start Rx | + | | MAX_PACKET_SIZE | + | V V + `---- P2P_WAIT_ACK_1 +<----------------------. + | | Tx PING ACK1 | + | V | + | P2P_WAIT_TX_ACK_1_DONE | + | | | + Rx PING ACK1 | < - - - - - - - - - - - | Tx Complete | + | | Start Rx | + | | MAX_PACKET_SIZE | + | | | + V V Timeout | + .---------->+ P2P_WAIT_ACK_2 -------------->+ + | TX PING | | ^ + | ACK2 | | | + | V | | + | P2P_WAIT_TX_ACK_2_DONE | | + | | Tx Complete - - - - - > | Rx PING ACK2 | + | Stop | Start HOP timer | Start HOP Timer | Stop + | HOP | Start Rx | Start Rx | HOP + | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | Timer + | | | | + | Rx | | | + | PING ACK V V Rx PING | + `----- P2P_LINK_UP P2P_LINK_UP -----------------’ + | | + | Rx Data | Rx Data + | | + V V + + Two timers are in use: + datagramTimer: Set at end of transmit, measures ACK timeout + heartbeatTimer: Set upon entry to P2P_NO_LINK, measures time to send next PING +*/ +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//First packet in the three way handshake to bring up the link +bool xmitDatagramP2PPing() +{ + unsigned long currentMillis = millis(); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(unsigned long); + + /* + endOfTxData ---. + | + V + +--------+---------+---------+----------+ + | | | | | + | NET ID | Control | Millis | Trailer | + | 8 bits | 8 bits | 4 bytes | n Bytes | + +--------+---------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_PING; + return (transmitDatagram()); +} + +//Second packet in the three way handshake to bring up the link +bool xmitDatagramP2PAck1() +{ + unsigned long currentMillis = millis(); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(unsigned long); + + /* + endOfTxData ---. + | + V + +--------+---------+---------+----------+ + | | | | Optional | + | NET ID | Control | Millis | Trailer | + | 8 bits | 8 bits | 4 bytes | n Bytes | + +--------+---------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_ACK_1; + return (transmitDatagram()); +} + +//Last packet in the three way handshake to bring up the link +bool xmitDatagramP2PAck2() +{ + unsigned long currentMillis = millis(); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(unsigned long); + + /* + endOfTxData ---. + | + V + +--------+---------+---------+----------+ + | | | | | + | NET ID | Control | Millis | Trailer | + | 8 bits | 8 bits | 4 bytes | n Bytes | + +--------+---------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_ACK_2; + return (transmitDatagram()); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Point-to-Point Data Exchange +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Send a command datagram to the remote system +bool xmitDatagramP2PCommand() +{ + /* + endOfTxData ---. + | + V + +--------+---------+--- ... ---+----------+ + | | | | Optional | + | NET ID | Control | Data | Trailer | + | 8 bits | 8 bits | n bytes | n Bytes | + +--------+---------+-------------+----------+ + */ + + txControl.datagramType = DATAGRAM_REMOTE_COMMAND; + return (transmitDatagram()); +} + +//Send a command response datagram to the remote system +bool xmitDatagramP2PCommandResponse() +{ + /* + endOfTxData ---. + | + V + +--------+---------+--- ... ---+----------+ + | | | | Optional | + | NET ID | Control | Data | Trailer | + | 8 bits | 8 bits | n bytes | n Bytes | + +--------+---------+-------------+----------+ + */ + + txControl.datagramType = DATAGRAM_REMOTE_COMMAND_RESPONSE; + return (transmitDatagram()); +} + +//Send a data datagram to the remote system +bool xmitDatagramP2PData() +{ + /* + endOfTxData ---. + | + V + +--------+---------+--- ... ---+----------+ + | | | | Optional | + | NET ID | Control | Data | Trailer | + | 8 bits | 8 bits | n bytes | n Bytes | + +--------+---------+-------------+----------+ + */ + + txControl.datagramType = DATAGRAM_DATA; + return (transmitDatagram()); +} + +//Heartbeat packet to keep the link up +bool xmitDatagramP2PHeartbeat() +{ + unsigned long currentMillis = millis(); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(currentMillis); + + /* + endOfTxData ---. + | + V + +--------+---------+---------+----------+ + | | | | Optional | + | NET ID | Control | Millis | Trailer | + | 8 bits | 8 bits | 4 Bytes | n Bytes | + +--------+---------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_HEARTBEAT; + return (transmitDatagram()); +} + +//Create short packet of 2 control bytes - do not expect ack +bool xmitDatagramP2PAck() +{ + int ackLength; + + uint8_t * ackStart = endOfTxData; + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); + + //Verify the ACK length + ackLength = endOfTxData - ackStart; + if (ackLength != CLOCK_SYNC_BYTES) + { + systemPrint("ERROR - Please define CLOCK_SYNC_BYTES = "); + systemPrintln(ackLength); + waitForever(); + } + + /* + endOfTxData ---. + | + V + +--------+---------+----------+----------+ + | | | Channel | Optional | + | NET ID | Control | Timer | Trailer | + | 8 bits | 8 bits | 2 bytes | n Bytes | + +--------+---------+----------+----------+ + */ + + txControl.datagramType = DATAGRAM_DATA_ACK; + return (transmitDatagram()); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +// Multi-Point Data Exchange +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Send a data datagram to the remote system, including sync data +bool xmitDatagramMpData() +{ + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); + + /* + endOfTxData ---. + | + V + +--------+---------+--- ... ---+----------+----------+ + | | | | Channel | Optional | + | NET ID | Control | Data | Timer | Trailer | + | 8 bits | 8 bits | n bytes | 2 bytes | n Bytes | + +--------+---------+-------------+----------+----------+ + */ + + + + txControl.datagramType = DATAGRAM_DATA; + return (transmitDatagram()); +} + +//Heartbeat packet to sync other units in multipoint mode +bool xmitDatagramMpHeartbeat() +{ + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); + + /* + endOfTxData ---. + | + V + +--------+---------+----------+----------+ + | | | Channel | Optional | + | NET ID | Control | Timer | Trailer | + | 8 bits | 8 bits | 2 bytes | n Bytes | + +--------+---------+----------+----------+ + */ + + txControl.datagramType = DATAGRAM_HEARTBEAT; + return (transmitDatagram()); +} + +//Ack packet sent by server in response the client ping, includes sync data and channel number +//During Multipoint scanning, it's possible for the client to get an ack but be 500kHz off +//The channel Number ensures that the client gets the next hop correct +bool xmitDatagramMpAck() +{ + memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); + endOfTxData += sizeof(channelNumber); + + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); + + + /* + endOfTxData ---. + | + V + +--------+---------+---------+----------+----------+ + | | | Channel | Channel | Optional | + | NET ID | Control | Number | Timer | Trailer | + | 8 bits | 8 bits | 1 byte | 2 bytes | n Bytes | + +--------+---------+---------+----------+----------+ + */ + + txControl.datagramType = DATAGRAM_ACK_1; + return (transmitDatagram()); +} + +//Ping packet sent during scanning +bool xmitDatagramMpPing() +{ + /* + endOfTxData ---. + | + V + +--------+---------+----------+ + | | | Optional | + | NET ID | Control | Trailer | + | 8 bits | 8 bits | n Bytes | + +--------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_PING; + return (transmitDatagram()); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Multi-Point Client Training +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Build the client ping packet used for training +bool xmitDatagramMpTrainingPing() +{ + //Add the source (server) ID + memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + /* + endOfTxData ---. + | + V + +----------+---------+-----------+----------+ + | Optional | | | Optional | + | NET ID | Control | Client ID | Trailer | + | 8 bits | 8 bits | 16 Bytes | n Bytes | + +----------+---------+-----------+----------+ + */ + + txControl.datagramType = DATAGRAM_TRAINING_PING; + return (transmitDatagram()); +} + +//Build the client ACK packet used for training +bool xmitDatagramMpTrainingAck(uint8_t * serverID) +{ + //Add the destination (server) ID + memcpy(endOfTxData, serverID, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + //Add the source (client) ID + memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + /* + endOfTxData ---. + | + V + +----------+---------+-----------+-----------+----------+ + | Optional | | | | Optional | + | NET ID | Control | Server ID | Client ID | Trailer | + | 8 bits | 8 bits | 16 Bytes | 16 Bytes | n Bytes | + +----------+---------+-----------+-----------+----------+ + */ + + txControl.datagramType = DATAGRAM_TRAINING_ACK; + return (transmitDatagram()); +} + +void updateRadioParameters(uint8_t * rxData) +{ + Settings params; + + //Get the parameters + memcpy(¶ms, rxData, sizeof(params)); + + //Update the radio parameters + originalSettings.airSpeed = params.airSpeed; + originalSettings.autoTuneFrequency = params.autoTuneFrequency; + originalSettings.radioBandwidth = params.radioBandwidth; + originalSettings.radioCodingRate = params.radioCodingRate; + originalSettings.frequencyHop = params.frequencyHop; + originalSettings.frequencyMax = params.frequencyMax; + originalSettings.frequencyMin = params.frequencyMin; + originalSettings.maxDwellTime = params.maxDwellTime; + originalSettings.numberOfChannels = params.numberOfChannels; + originalSettings.radioPreambleLength = params.radioPreambleLength; + originalSettings.radioSpreadFactor = params.radioSpreadFactor; + originalSettings.radioSyncWord = params.radioSyncWord; + originalSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; + + //Update the radio protocol parameters + originalSettings.dataScrambling = params.dataScrambling; + originalSettings.enableCRC16 = params.enableCRC16; + originalSettings.encryptData = params.encryptData; + memcpy(originalSettings.encryptionKey, params.encryptionKey, sizeof(originalSettings.encryptionKey)); + originalSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; + originalSettings.heartbeatTimeout = params.heartbeatTimeout; + originalSettings.maxResends = params.maxResends; + originalSettings.netID = params.netID; + originalSettings.operatingMode = params.operatingMode; + originalSettings.overheadTime = params.overheadTime; + originalSettings.server = params.server; + originalSettings.verifyRxNetID = params.verifyRxNetID; + + //Update the debug parameters + if (params.copyDebug) + { + originalSettings.debug = params.debug; + originalSettings.alternateLedUsage = params.alternateLedUsage; + originalSettings.copyDebug = params.copyDebug; + originalSettings.debug = params.debug; + originalSettings.debugDatagrams = params.debugDatagrams; + originalSettings.debugNvm = params.debugNvm; + originalSettings.debugRadio = params.debugRadio; + originalSettings.debugReceive = params.debugReceive; + originalSettings.debugStates = params.debugStates; + originalSettings.debugTraining = params.debugTraining; + originalSettings.debugTransmit = params.debugTransmit; + originalSettings.debugSerial = params.debugSerial; + originalSettings.displayPacketQuality = params.displayPacketQuality; + originalSettings.displayRealMillis = params.displayRealMillis; + originalSettings.printAckNumbers = params.printAckNumbers; + originalSettings.printFrequency = params.printFrequency; + originalSettings.printLinkUpDown = params.printLinkUpDown; + originalSettings.printPktData = params.printPktData; + originalSettings.printRfData = params.printRfData; + originalSettings.printTimestamp = params.printTimestamp; + originalSettings.printTxErrors = params.printTxErrors; + } + + //Update the serial parameters + if (params.copySerial) + { + originalSettings.copySerial = params.copySerial; + originalSettings.echo = params.echo; + originalSettings.flowControl = params.flowControl; + originalSettings.invertCts = params.invertCts; + originalSettings.invertRts = params.invertRts; + originalSettings.serialSpeed = params.serialSpeed; + originalSettings.usbSerialWait = params.usbSerialWait; + } + + //Update the training values + originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; + //The trainingKey is already the same + originalSettings.trainingTimeout = params.trainingTimeout; + + //Update the trigger parameters + if (params.copyTriggers) + { + originalSettings.copyTriggers = params.copyTriggers; + originalSettings.triggerEnable = params.triggerEnable; + originalSettings.triggerEnable2 = params.triggerEnable2; + originalSettings.triggerWidth = params.triggerWidth; + originalSettings.triggerWidthIsMultiplier = params.triggerWidthIsMultiplier; + } +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Multi-Point Server Training +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Build the server parameters packet used for training +bool xmitDatagramMpRadioParameters(const uint8_t * clientID) +{ + Settings params; + + //Initialize the radio parameters + memcpy(¶ms, &originalSettings, sizeof(settings)); + params.server = false; + + //Add the destination (client) ID + memcpy(endOfTxData, clientID, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + //Add the source (server) ID + memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + + //Add the radio parameters + memcpy(endOfTxData, ¶ms, sizeof(params)); + endOfTxData += sizeof(params); + + /* + endOfTxData ---. + | + V + +----------+---------+-----------+-----------+--- ... ---+----------+ + | Optional | | | | Radio | Optional | + | NET ID | Control | Client ID | Server ID | Parameters | Trailer | + | 8 bits | 8 bits | 16 Bytes | 16 Bytes | n bytes | n Bytes | + +----------+---------+-----------+-----------+-------------+----------+ + */ + + txControl.datagramType = DATAGRAM_TRAINING_PARAMS; + return (transmitDatagram()); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Virtual Circuit frames +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +bool xmitVcHeartbeat(int8_t addr, uint8_t * id) +{ + uint8_t * txData; + + uint32_t currentMillis = millis(); + txData = endOfTxData; + *endOfTxData++ = 0; //Reserve for length + *endOfTxData++ = VC_BROADCAST; + *endOfTxData++ = addr; + memcpy(endOfTxData, id, UNIQUE_ID_BYTES); + endOfTxData += UNIQUE_ID_BYTES; + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(currentMillis); + + //Set the length field + *txData = (uint8_t)(endOfTxData - txData); + + /* + endOfTxData ---. + | + V + +----------+---------+--------+----------+---------+----------+---------+----------+ + | Optional | | | | | | | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Src ID | millis | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes || n Bytes | + +----------+---------+--------+----------+---------+----------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_VC_HEARTBEAT; + txControl.ackNumber = 0; + + //Determine the time that it took to pass this frame to the radio + //This time is used to adjust the time offset + vcTxHeartbeatMillis = millis() - currentMillis; + + //Select a random for the next heartbeat + setHeartbeatLong(); //Those who send a heartbeat or data have long time before next heartbeat. Those who send ACKs, have short wait to next heartbeat. + + return (transmitDatagram()); +} + +//Build the ACK frame +bool xmitVcAckFrame(int8_t destVc) +{ + VC_RADIO_MESSAGE_HEADER * vcHeader; + + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; + vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; + vcHeader->destVc = destVc; + vcHeader->srcVc = myVc; + endOfTxData += VC_RADIO_HEADER_BYTES; + + /* + endOfTxData ---. + | + V + +--------+---------+--------+----------+---------+----------+----------+ + | | | | | | Channel | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Timer | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 2 bytes | n Bytes | + +--------+---------+--------+----------+---------+----------+----------+ + */ + + //Finish building the ACK frame + return xmitDatagramP2PAck(); +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Datagram reception +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Determine the type of datagram received +PacketType rcvDatagram() +{ + uint8_t ackNumber; + PacketType datagramType; + uint8_t receivedNetID; + CONTROL_U8 rxControl; + VIRTUAL_CIRCUIT * vc; + VC_RADIO_MESSAGE_HEADER * vcHeader; + + //Acknowledge the receive interrupt + transactionComplete = false; + + //Save the receive time + rcvTimeMillis = millis(); + + //Get the received datagram + framesReceived++; + int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); + + if (state == RADIOLIB_ERR_NONE) + { + //Do nothing + } + else if (state == RADIOLIB_ERR_CRC_MISMATCH) + { + if (settings.debug || settings.debugDatagrams || settings.debugReceive) + { + systemPrintln("Receive CRC error!"); + outputSerialData(true); + } + badCrc++; + returnToReceiving(); //Return to listening + return (DATAGRAM_CRC_ERROR); + } + else + { + if (settings.debug || settings.debugDatagrams || settings.debugReceive) + { + systemPrint("Receive error: "); + systemPrintln(state); + outputSerialData(true); + } + returnToReceiving(); //Return to listening + badFrames++; + return (DATAGRAM_BAD); + } + + rxDataBytes = radio.getPacketLength(); + + returnToReceiving(); //Immediately begin listening while we process new data + + rxData = incomingBuffer; + vc = &virtualCircuitList[0]; + vcHeader = NULL; + + /* + |<---------------------- rxDataBytes ---------------------->| + | | + +----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | | Optional | + | NET ID | Control | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | + +----------+----------+------------+-------------+----------+ + ^ + | + '---- rxData + */ + + //Display the received data bytes + if (settings.printRfData || settings.debugReceive) + { + systemPrintln("----------"); + systemPrintTimestamp(); + systemPrint("RX: "); + systemPrint((settings.dataScrambling || settings.encryptData) ? "Encrypted " : "Unencrypted "); + systemPrint("Frame "); + systemPrint(rxDataBytes); + systemPrint(" (0x"); + systemPrint(rxDataBytes, HEX); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printRfData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); + } + + if (settings.dataScrambling == true) + radioComputeWhitening(incomingBuffer, rxDataBytes); + + if (settings.encryptData == true) + { + decryptBuffer(incomingBuffer, rxDataBytes); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + if (settings.debugReceive) + { + systemPrint("in: "); + dumpBufferRaw(incomingBuffer, 14); //Print only the first few bytes when debugging packets + } + + //Display the received data bytes + if ((settings.dataScrambling || settings.encryptData) + && (settings.printRfData || settings.debugReceive)) + { + systemPrintTimestamp(); + systemPrint("RX: Unencrypted Frame "); + systemPrint(rxDataBytes); + systemPrint(" (0x"); + systemPrint(rxDataBytes, HEX); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printRfData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); + } + + //All packets must include the 2-byte control header + if (rxDataBytes < minDatagramSize) + { + //Display the packet contents + if (settings.printPktData || settings.debugDatagrams || settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("RX: Bad Frame "); + systemPrint(rxDataBytes); + systemPrint(" (0x"); + systemPrint(rxDataBytes, HEX); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printRfData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); + } + badFrames++; + return (DATAGRAM_BAD); + } + + /* + |<---------------------- rxDataBytes ---------------------->| + | | + +----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | | Optional | + | NET ID | Control | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | + +----------+----------+------------+-------------+----------+ + ^ + | + '---- rxData + */ + + //Verify the netID if necessary + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) + { + receivedNetID = *rxData++; + if (receivedNetID != settings.netID) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("RX: NetID "); + systemPrint(receivedNetID); + systemPrint(" (0x"); + systemPrint(receivedNetID, HEX); + systemPrint(")"); + if (receivedNetID != settings.netID) + { + systemPrint(" expecting "); + systemPrint(settings.netID); + } + systemPrintln(); + outputSerialData(true); + } + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.debugReceive && settings.printPktData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); + netIdMismatch++; + return (DATAGRAM_NETID_MISMATCH); + } + } + + //Process the trailer + petWDT(); + if (settings.enableCRC16) + { + uint16_t crc; + uint8_t * data; + + //Compute the CRC-16 value + crc = 0xffff; + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + for (data = incomingBuffer; data < &incomingBuffer[rxDataBytes - 2]; data++) + crc = crc16Table[*data ^ (uint8_t)(crc >> (16 - 8))] ^ (crc << 8); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + if ((incomingBuffer[rxDataBytes - 2] != (crc >> 8)) + && (incomingBuffer[rxDataBytes - 1] != (crc & 0xff))) + { + //Display the packet contents + if (settings.printPktData || settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("RX: Bad CRC-16, received 0x"); + systemPrint(incomingBuffer[rxDataBytes - 2], HEX); + systemPrint(incomingBuffer[rxDataBytes - 1], HEX); + systemPrint(" expected 0x"); + systemPrintln(crc, HEX); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printRfData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); + } + badCrc++; + return (DATAGRAM_CRC_ERROR); + } + } + + /* + |<---------------------- rxDataBytes ---------------------->| + | | + +----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | | Optional | + | NET ID | Control | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | + +----------+----------+------------+-------------+----------+ + ^ + | + '---- rxData + */ + + //Get the control byte + rxControl = *((CONTROL_U8 *)rxData++); + datagramType = rxControl.datagramType; + ackNumber = rxControl.ackNumber; + if (settings.debugReceive) + printControl(*((uint8_t *)&rxControl)); + if (datagramType >= MAX_DATAGRAM_TYPE) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("RX: Invalid datagram type "); + systemPrintln(datagramType); + outputSerialData(true); + } + badFrames++; + return (DATAGRAM_BAD); + } + + //Display the CRC + if (settings.enableCRC16 && settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint(" CRC-16: 0x"); + systemPrint(incomingBuffer[rxDataBytes - 2], HEX); + systemPrintln(incomingBuffer[rxDataBytes - 1], HEX); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + /* + |<---------------------- rxDataBytes ---------------------->| + | | + +----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | | Optional | + | NET ID | Control | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | + +----------+----------+------------+-------------+----------+ + ^ + | + '---- rxData + */ + + //Get the spread factor 6 length + if (settings.radioSpreadFactor == 6) + { + if (rxDataBytes >= (*rxData + minDatagramSize)) + rxDataBytes = *rxData++; + else + { + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("Invalid SF6 length, received SF6 length "); + systemPrint(*rxData); + systemPrint(" > "); + systemPrint((int)rxDataBytes - minDatagramSize); + systemPrintln(" received bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + badFrames++; + return (DATAGRAM_BAD); + } + if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint(" SF6 Length: "); + systemPrintln(rxDataBytes); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + } + else + rxDataBytes -= minDatagramSize; + + //Get the Virtual-Circuit header + rxVcData = rxData; + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Verify that the virtual circuit header is present + if (rxDataBytes < 3) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Missing VC header bytes, received only "); + systemPrint(rxDataBytes); + systemPrintln(" bytes, expecting at least 3 bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + badFrames++; + return DATAGRAM_BAD; + } + + //Parse the virtual circuit header + vcHeader = (VC_RADIO_MESSAGE_HEADER *)rxData; + rxDestVc = vcHeader->destVc; + rxSrcVc = vcHeader->srcVc; + rxVcData = &rxData[3]; + + //Display the virtual circuit header + if (settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint(" VC Length: "); + systemPrintln(vcHeader->length); + systemPrint(" DestAddr: "); + if (rxDestVc == VC_BROADCAST) + systemPrintln("Broadcast"); + else + systemPrintln(rxDestVc); + systemPrint(" SrcAddr: "); + if (rxSrcVc == VC_UNASSIGNED) + systemPrintln("Unassigned"); + else + systemPrintln(rxSrcVc); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + //Validate the source VC + vc = NULL; + if (rxSrcVc != VC_UNASSIGNED) + { + if ((uint8_t)rxSrcVc >= MAX_VC) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Invalid source VC: "); + systemPrintln(rxSrcVc); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + if (settings.printRfData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); + } + badFrames++; + return DATAGRAM_BAD; + } + vc = &virtualCircuitList[rxSrcVc]; + } + + //Validate the length + if (vcHeader->length != rxDataBytes) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Invalid VC length, received "); + systemPrint(vcHeader->length); + systemPrint(" expecting "); + systemPrintln(rxDataBytes); + outputSerialData(true); + } + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + if (vc) + vc->badLength++; + badFrames++; + return DATAGRAM_BAD; + } + + //Validate the destination VC + if ((rxDestVc != VC_BROADCAST) && (rxDestVc != myVc)) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Not my VC: "); + systemPrintln(rxDestVc); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + if (settings.printPktData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); + } + return DATAGRAM_NOT_MINE; + } + } + + //Verify the packet number last so that the expected datagram or ACK number can be updated + if (vc && (settings.operatingMode != MODE_DATAGRAM)) + { + switch (datagramType) + { + default: + break; + + case DATAGRAM_DATA_ACK: + if (ackNumber != vc->txAckNumber) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Invalid ACK number, received "); + systemPrint(ackNumber); + systemPrint(" expecting "); + systemPrintln(vc->txAckNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + badFrames++; + return (DATAGRAM_BAD); + } + + //Set the next TX ACK number + if (settings.printAckNumbers) + { + systemPrint("txAckNumber: "); + systemPrint(vc->txAckNumber); + systemPrint(" --> "); + } + vc->txAckNumber = (vc->txAckNumber + 1) & 3; + if (settings.printAckNumbers) + { + systemPrintln(vc->txAckNumber); + outputSerialData(true); + } + break; + + case DATAGRAM_HEARTBEAT: + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + break; + datagramType = validateDatagram(vc, datagramType, ackNumber, rxDataBytes); + if (datagramType != DATAGRAM_HEARTBEAT) + return datagramType; + break; + + case DATAGRAM_REMOTE_COMMAND: + datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(commandRXBuffer) + - availableRXCommandBytes()); + if (datagramType != DATAGRAM_REMOTE_COMMAND) + return datagramType; + break; + + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(serialTransmitBuffer) + - availableTXBytes()); + if (datagramType != DATAGRAM_REMOTE_COMMAND_RESPONSE) + return datagramType; + break; + + case DATAGRAM_DATA: + datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(serialTransmitBuffer) + - availableTXBytes()); + if (datagramType != DATAGRAM_DATA) + return datagramType; + vc->messagesReceived++; + break; + } + + //Account for this frame + vc->framesReceived++; + } + + /* + |<-- rxDataBytes -->| + | | + +----------+----------+------------+------ ... ------+----------+ + | Optional | | Optional | | Optional | + | NET ID | Control | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | + +----------+----------+------------+-------------------+----------+ + ^ + | + '---- rxData + */ + + //Display the packet contents + if (settings.printPktData || settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint("RX: Datagram "); + systemPrint(rxDataBytes); + systemPrint(" (0x"); + systemPrint(rxDataBytes, HEX); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printPktData && rxDataBytes) + dumpBuffer(rxData, rxDataBytes); + } + + //Display the datagram type + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("RX: "); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + systemPrint((uint8_t)((rxDestVc == VC_BROADCAST) ? rxDestVc : rxDestVc & VCAB_NUMBER_MASK)); + systemPrint(" <-- "); + systemPrint((uint8_t)((rxSrcVc == VC_UNASSIGNED) ? rxSrcVc : rxSrcVc & VCAB_NUMBER_MASK)); + systemWrite(' '); + } + systemPrint(radioDatagramType[datagramType]); + switch (datagramType) + { + default: + systemPrintln(); + break; + + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + case DATAGRAM_HEARTBEAT: + if (settings.operatingMode != MODE_DATAGRAM) + { + systemPrint(" ("); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + systemPrint("VC "); + systemPrint("ACK #"); + systemPrint(ackNumber); + systemPrint(")"); + } + systemPrintln(); + break; + } + outputSerialData(true); + } + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + + //Process the packet + datagramsReceived++; + linkDownTimer = millis(); + + //Blink the RX LED + if (settings.alternateLedUsage) + digitalWrite(ALT_LED_RX_DATA, LED_ON); + return datagramType; +} + +PacketType validateDatagram(VIRTUAL_CIRCUIT * vc, PacketType datagramType, uint8_t ackNumber, uint16_t freeBytes) +{ + if (ackNumber != vc->rmtTxAckNumber) + { + //Determine if this is a duplicate datagram + if (ackNumber == ((vc->rmtTxAckNumber - 1) & 3)) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Duplicate datagram received, ACK "); + systemPrintln(ackNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + linkDownTimer = millis(); + duplicateFrames++; + return DATAGRAM_DUPLICATE; + } + + //Not a duplicate + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Invalid datagram number, received "); + systemPrint(ackNumber); + systemPrint(" expecting "); + systemPrintln(vc->rmtTxAckNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + badFrames++; + return DATAGRAM_BAD; + } + + //Verify that there is sufficient space in the serialTransmitBuffer + if (inCommandMode || ((sizeof(serialTransmitBuffer) - availableTXBytes()) < rxDataBytes)) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrintln("Insufficient space in the serialTransmitBuffer"); + } + insufficientSpace++; + + //Apply back pressure to the other radio by dropping this packet and + //forcing the other radio to retransmit the packet. + badFrames++; + return DATAGRAM_BAD; + } + + //Receive this data packet and set the next expected datagram number + if (settings.printAckNumbers) + { + systemPrint("rxAckNumber: "); + systemPrint(vc->rxAckNumber); + systemPrint(" --> "); + } + vc->rxAckNumber = vc->rmtTxAckNumber; + if (settings.printAckNumbers) + { + systemPrintln(vc->rxAckNumber); + systemPrint("rmtTxAckNumber: "); + systemPrint(vc->rmtTxAckNumber); + systemPrint(" --> "); + } + vc->rmtTxAckNumber = (vc->rmtTxAckNumber + 1) & 3; + if (settings.printAckNumbers) + { + systemPrintln(vc->rmtTxAckNumber); + outputSerialData(true); + } + return datagramType; +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Datagram transmission +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +//Push the outgoing packet to the air +//Returns false if we could not start tranmission due to packet received or RX in process +bool transmitDatagram() +{ + uint8_t control; + uint8_t * header; + uint8_t length; + int8_t srcVc; + uint8_t * vcData; + VIRTUAL_CIRCUIT * vc; + VC_RADIO_MESSAGE_HEADER * vcHeader; + + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + + //Parse the virtual circuit header + vc = &virtualCircuitList[0]; + vcHeader = NULL; + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + vc = NULL; + vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; + txDestVc = vcHeader->destVc; + srcVc = vcHeader->srcVc; + if ((uint8_t)vcHeader->destVc <= MAX_VC) + { + vc = &virtualCircuitList[txDestVc]; + vc->messagesSent++; + } + vcData = (uint8_t *)&vcHeader[1]; + } + + //Determine the packet size + datagramsSent++; + txDatagramSize = endOfTxData - outgoingPacket; + length = txDatagramSize - headerBytes; + + //Select the ACK number + if (txControl.datagramType == DATAGRAM_DATA_ACK) + txControl.ackNumber = vc->rxAckNumber; + else + txControl.ackNumber = vc->txAckNumber; + + //Process the packet + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("TX: "); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + systemPrint((uint8_t)((srcVc == VC_UNASSIGNED) ? srcVc : srcVc & VCAB_NUMBER_MASK)); + systemPrint(" --> "); + systemPrint((uint8_t)((txDestVc == VC_BROADCAST) ? txDestVc : txDestVc & VCAB_NUMBER_MASK)); + systemWrite(' '); + } + systemPrint(radioDatagramType[txControl.datagramType]); + switch (txControl.datagramType) + { + default: + systemPrintln(); + break; + + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + case DATAGRAM_HEARTBEAT: + if (settings.operatingMode != MODE_DATAGRAM) + { + systemPrint(" ("); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + systemPrint("VC "); + systemPrint("ACK #"); + systemPrint(txControl.ackNumber); + systemPrint(")"); + } + systemPrintln(); + break; + } + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + /* + endOfTxData ---. + | + V + +----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | | Optional | + | NET ID | Control | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | + +----------+----------+------------+-------------+----------+ + | | | + | |<- Length -->| + |<--------- txDatagramSize --------------------->| + */ + + //Display the packet contents + if (settings.printPktData || settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: Datagram "); + systemPrint(length); + systemPrint(" (0x"); + systemPrint(length, HEX); + systemPrintln(") bytes"); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printPktData) + dumpBuffer(&endOfTxData[-length], length); + outputSerialData(true); + } + + //Build the datagram header + header = outgoingPacket; + if (headerBytes && settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: Header"); + systemPrint(headerBytes); + systemPrint(" (0x"); + systemPrint(headerBytes); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + //Add the netID if necessary + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) + { + *header++ = settings.netID; + + //Display the netID value + if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint(" NetID: "); + systemPrint(settings.netID); + systemPrint(" (0x"); + systemPrint(settings.netID, HEX); + systemPrintln(")"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + } + } + + //Add the control byte + control = *(uint8_t *)&txControl; + *header++ = control; + + //Display the control value + if (settings.debugTransmit) + printControl(control); + + //Add the spread factor 6 length if required + if (settings.radioSpreadFactor == 6) + { + *header++ = length; + + //Send either a short ACK or full length packet + switch (txControl.datagramType) + { + default: + txDatagramSize = MAX_PACKET_SIZE - trailerBytes; //We're now going to transmit a full size datagram + break; + + case DATAGRAM_PING: + case DATAGRAM_ACK_1: + case DATAGRAM_ACK_2: + txDatagramSize = headerBytes + CLOCK_MILLIS_BYTES; //Short packet is 3 + 4 + break; + + case DATAGRAM_DATA_ACK: + txDatagramSize = headerBytes + CLOCK_SYNC_BYTES; //Short ACK packet is 3 + 2 + break; + } + + + radio.implicitHeader(txDatagramSize); //Set header size so that hardware CRC is calculated correctly + + endOfTxData = &outgoingPacket[txDatagramSize]; + if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint(" SF6 Length: "); + systemPrintln(length); + systemPrint(" SF6 TX Header Size: "); + systemPrintln(txDatagramSize); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + } + + //Verify the Virtual-Circuit length + if (settings.debugTransmit && (settings.operatingMode == MODE_VIRTUAL_CIRCUIT)) + { + systemPrintTimestamp(); + systemPrint(" Length: "); + systemPrintln(vcHeader->length); + systemPrint(" DestAddr: "); + if (txDestVc == VC_BROADCAST) + systemPrintln("Broadcast"); + else + systemPrintln(txDestVc); + systemPrint(" SrcAddr: "); + if (srcVc == VC_UNASSIGNED) + systemPrintln("Unassigned"); + else + systemPrintln(srcVc); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + /* + endOfTxData ---. + | + V + +----------+----------+------------+--- ... ---+ + | Optional | | Optional | | + | NET ID | Control | SF6 Length | Data | + | 8 bits | 8 bits | 8 bits | n bytes | + +----------+----------+------------+-------------+ + | | + |<--------------- txDatagramSize --------------->| + */ + + //Add the datagram trailer + if (settings.enableCRC16) + { + uint16_t crc; + uint8_t * txData; + + //Compute the CRC-16 value + crc = 0xffff; + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + for (txData = outgoingPacket; txData < endOfTxData; txData++) + crc = crc16Table[*txData ^ (uint8_t)(crc >> (16 - 8))] ^ (crc << 8); + *endOfTxData++ = (uint8_t)(crc >> 8); + *endOfTxData++ = (uint8_t)(crc & 0xff); + } + txDatagramSize += trailerBytes; + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + + //Display the trailer + if (trailerBytes && settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: Trailer "); + systemPrint(trailerBytes); + systemPrint(" (0x"); + systemPrint(trailerBytes); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + + //Display the CRC + if (settings.enableCRC16 && (settings.printPktData || settings.debugReceive)) + { + systemPrintTimestamp(); + systemPrint(" CRC-16: 0x"); + systemPrint(endOfTxData[-2], HEX); + systemPrintln(endOfTxData[-1], HEX); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + } + + /* + endOfTxData ---. + | + V + +----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | | Optional | + | NET ID | Control | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | + +----------+----------+------------+-------------+----------+ + | | + |<-------------------- txDatagramSize --------------------->| + */ + + //Display the transmitted packet bytes + if (settings.printRfData || settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: Unencrypted Frame "); + systemPrint(txDatagramSize); + systemPrint(" (0x"); + systemPrint(txDatagramSize, HEX); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printRfData) + dumpBuffer(outgoingPacket, txDatagramSize); + outputSerialData(true); + } + + //Print before encryption + if (settings.debugTransmit) + { + systemPrint("out: "); + dumpBufferRaw(outgoingPacket, 14); + } + + //Encrypt the datagram + if (settings.encryptData == true) + { + encryptBuffer(outgoingPacket, txDatagramSize); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + //Scramble the datagram + if (settings.dataScrambling == true) + radioComputeWhitening(outgoingPacket, txDatagramSize); + + //Display the transmitted packet bytes + if ((settings.printRfData || settings.debugTransmit) + && (settings.encryptData || settings.dataScrambling)) + { + systemPrintTimestamp(); + systemPrint("TX: Encrypted Frame "); + systemPrint(txDatagramSize); + systemPrint(" (0x"); + systemPrint(txDatagramSize, HEX); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printRfData) + dumpBuffer(outgoingPacket, txDatagramSize); + outputSerialData(true); + } + + //If we are trainsmitting at high data rates the receiver is often not ready for new data. Pause for a few ms (measured with logic analyzer). + if (settings.airSpeed == 28800 || settings.airSpeed == 38400) + delay(2); + + //Reset the buffer data pointer for the next transmit operation + endOfTxData = &outgoingPacket[headerBytes]; + + //Compute the time needed for this frame. Part of ACK timeout. + frameAirTime = calcAirTime(txDatagramSize); + + //Transmit this datagram + frameSentCount = 0; //This is the first time this frame is being sent + return (retransmitDatagram(vc)); +} + +//Print the control byte value +void printControl(uint8_t value) +{ + CONTROL_U8 * control = (CONTROL_U8 *)&value; + + systemPrintTimestamp(); + systemPrint(" Control: 0x"); + systemPrintln(value, HEX); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + systemPrintTimestamp(); + systemPrint(" ACK # "); + systemPrintln(value & 3); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + systemPrintTimestamp(); + systemPrint(" datagramType "); + if (control->datagramType < MAX_DATAGRAM_TYPE) + systemPrintln(radioDatagramType[control->datagramType]); + else + { + systemPrint("Unknown "); + systemPrintln(control->datagramType); + } + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); +} + +//The previous transmission was not received, retransmit the datagram +//Returns false if we could not start tranmission due to packet received or RX in process +bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) +{ + /* + +----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | | Optional | + | NET ID | Control | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | + +----------+----------+------------+-------------+----------+ + | | + |<-------------------- txDatagramSize --------------------->| + */ + + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + + //Display the transmitted frame bytes + if (frameSentCount && (settings.printRfData || settings.debugTransmit)) + { + systemPrintTimestamp(); + systemPrint("TX: Retransmit "); + systemPrint((settings.encryptData || settings.dataScrambling) ? "Encrypted " : "Unencrypted "); + systemPrint("Frame "); + systemPrint(txDatagramSize); + systemPrint(" (0x"); + systemPrint(txDatagramSize, HEX); + systemPrintln(") bytes"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + if (settings.printRfData) + dumpBuffer(outgoingPacket, txDatagramSize); + outputSerialData(true); + } + + //Transmit this frame + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + + //Drop this datagram if the receiver is active + frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background + uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond + if ((receiveInProcess() == true) || (transactionComplete == true) + || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].linkUp == false))) + { + triggerEvent(TRIGGER_TRANSMIT_CANCELED); + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrint("TX failed: "); + if (transactionComplete) + systemPrintln("RXTC"); + else if (receiveInProcess()) + systemPrintln("RXIP"); + else + systemPrintln("VC link down"); + outputSerialData(true); + } + return (false); //Do not start transmit while RX is or has occured + } + else + { + + int state = radio.startTransmit(outgoingPacket, txDatagramSize); + + if (state == RADIOLIB_ERR_NONE) + { + frameSentCount++; + if (vc) + vc->framesSent++; + framesSent++; + xmitTimeMillis = millis(); + if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: frameAirTime "); + systemPrint(frameAirTime); + systemPrintln(" mSec"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + + systemPrintTimestamp(); + systemPrint("TX: responseDelay "); + systemPrint(responseDelay); + systemPrintln(" mSec"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + } + else if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: Transmit error, state "); + systemPrintln(state); + outputSerialData(true); + } + } + frameAirTime += responseDelay; + datagramTimer = millis(); //Move timestamp even if error + retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime); //Wait this number of ms between retransmits. Increases with each re-transmit. + + //BLink the RX LED + if (settings.alternateLedUsage) + digitalWrite(ALT_LED_TX_DATA, LED_ON); + + return (true); //Tranmission has started +} + +void startChannelTimer() +{ + startChannelTimer(settings.maxDwellTime); +} + +void startChannelTimer(int16_t startAmount) +{ + channelTimer.disableTimer(); + channelTimer.setInterval_MS(startAmount, channelTimerHandler); + channelTimer.enableTimer(); + timerStart = millis(); //ISR normally takes care of this but allow for correct ACK sync before first ISR + triggerEvent(TRIGGER_HOP_TIMER_START); +} + +void stopChannelTimer() +{ + channelTimer.disableTimer(); + triggerEvent(TRIGGER_HOP_TIMER_STOP); +} + +//Given the remote unit's number of ms before its next hop, +//adjust our own channelTimer interrupt to be synchronized with the remote unit +void syncChannelTimer() +{ + int16_t msToNextHopRemote; //Can be negative + memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); + msToNextHopRemote -= ackAirTime; + msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; + + //Different airspeeds complete the transmitComplete ISR at different rates + //We adjust the clock setup as needed + switch (settings.airSpeed) + { + default: + break; + case (40): + msToNextHopRemote -= getReceiveCompletionOffset(); + break; + case (150): + msToNextHopRemote -= 145; + break; + case (400): + msToNextHopRemote -= getReceiveCompletionOffset(); + break; + case (1200): + msToNextHopRemote -= getReceiveCompletionOffset(); + break; + case (2400): + msToNextHopRemote -= getReceiveCompletionOffset(); + break; + case (4800): + break; + case (9600): + break; + case (19200): + break; + case (28800): + msToNextHopRemote -= 2; + break; + case (38400): + msToNextHopRemote -= 3; + break; + } + + //Calculate the remote's absolute distance to its next hop + //A remote hop may be very negative for + + int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - timerStart); + + //Precalculate large/small time amounts + uint16_t smallAmount = settings.maxDwellTime / 8; + uint16_t largeAmount = settings.maxDwellTime - smallAmount; + + int16_t msToNextHop = msToNextHopRemote; //By default, we will adjust our clock to match our mate's + + bool resetHop = false; //The hop ISR may occur while we are forcing a hop (case A and C). Reset timeToHop as needed. + + //Below are the edge cases that occur when a hop occurs near ACK reception + + //msToNextHopLocal is small and msToNextHopRemote is negative (and small) + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in negative (and small) then the remote has hopped + //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + if (msToNextHopLocal < smallAmount && (msToNextHopRemote <= 0 && msToNextHopRemote >= (smallAmount * -1))) + { + hopChannel(); + msToNextHop = msToNextHopRemote + settings.maxDwellTime; + resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. + } + + //msToNextHopLocal is large and msToNextHopRemote is negative + //If we just hopped (msToNextHopLocal is large), and msToNextHopRemote comes in negative then the remote has hopped + //No need to hop. Adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + else if (msToNextHopLocal > largeAmount && msToNextHopRemote <= 0) + { + msToNextHop = msToNextHopRemote + settings.maxDwellTime; + } + + //msToNextHopLocal is small and msToNextHopRemote is large + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in large then the remote has hopped recently + //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote) + else if (msToNextHopLocal < smallAmount && msToNextHopRemote > largeAmount) + { + hopChannel(); + msToNextHop = msToNextHopRemote; + resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. + } + + //msToNextHopLocal is large and msToNextHopRemote is large + //If we just hopped (msToNextHopLocal is large), and a msToNextHopRemote comes in large then the remote has hopped + //Then adjust our clock to the remote's next hop (msToNextHopRemote) + else if (msToNextHopLocal > largeAmount && msToNextHopRemote > largeAmount) + { + msToNextHop = msToNextHopRemote; + } + + //msToNextHopLocal is large and msToNextHopRemote is small + //If we just hopped (msToNextHopLocal is large), and a msToNextHopRemote comes in small then the remote is about to hop + //Then adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + else if (msToNextHopLocal > largeAmount && msToNextHopRemote < smallAmount) + { + msToNextHop = msToNextHopRemote + settings.maxDwellTime; + } + + //msToNextHopLocal is small and msToNextHopRemote is small + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in small then the remote is about to hop + //Then adjust our clock to the remote's next hop (msToNextHopRemote) + else if (msToNextHopLocal < smallAmount && msToNextHopRemote < smallAmount) + { + msToNextHop = msToNextHopRemote; + + //If we have a negative remote hop time that is larger than a dwell time then the remote has hopped again + //This is seen at lower air speeds + //Hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + if (msToNextHop < (settings.maxDwellTime * -1)) //-402 < -400 + { + hopChannel(); + msToNextHop += settings.maxDwellTime; + resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. + } + } + + //Insure against negative timer values + while (msToNextHop < 0) + msToNextHop += settings.maxDwellTime; + + if (settings.debugSync) + { + systemPrint("msToNextHopRemote: "); + systemPrint(msToNextHopRemote); + systemPrint(" msToNextHopLocal: "); + systemPrint(msToNextHopLocal); + systemPrint(" msToNextHop: "); + systemPrint(msToNextHop); + systemPrintln(); + } + + partialTimer = true; + channelTimer.disableTimer(); + channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's + + if (resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. + timeToHop = false; + + channelTimer.enableTimer(); + + triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug +} + +//This function resets the heartbeat time and re-rolls the random time +//Call when something has happened (ACK received, etc) where clocks have been sync'd +//Short/long times set to avoid two radios attempting to xmit heartbeat at same time +//Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. +void setHeartbeatShort() +{ + heartbeatTimer = millis(); + heartbeatRandomTime = random(settings.heartbeatTimeout * 2 / 10, settings.heartbeatTimeout / 2); //20-50% + + //Slow datarates can have significant ack transmission times + //Add the amount of time it takes to send an ack + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); +} + +void setHeartbeatLong() +{ + heartbeatTimer = millis(); + heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% + + //Slow datarates can have significant ack transmission times + //Add the amount of time it takes to send an ack + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); +} + +//Only the server sends heartbeats in multipoint mode +//Not random, just the straight timeout +void setHeartbeatMultipoint() +{ + heartbeatTimer = millis(); + heartbeatRandomTime = settings.heartbeatTimeout; + + //Slow datarates can have significant ack transmission times + //Add the amount of time it takes to send an ack + heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); +} diff --git a/Firmware/LoRaSerial_Firmware/RadioV2.ino b/Firmware/LoRaSerial_Firmware/RadioV2.ino deleted file mode 100644 index c2f14d9f..00000000 --- a/Firmware/LoRaSerial_Firmware/RadioV2.ino +++ /dev/null @@ -1,2058 +0,0 @@ -const uint16_t crc16Table[256] = -{ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, - 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6, - 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, - 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4, - 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, - 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B, - 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12, - 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, - 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, - 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, - 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, - 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, - 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F, - 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, - 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D, - 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, - 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, - 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, - 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, - 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, - 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, - 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, - 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 -}; - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Point-To-Point Training -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Ping the other radio in the point-to-point configuration -bool xmitDatagramP2PTrainingPing() -{ - /* - endOfTxData ---. - | - V - +---------+----------+ - | | | - | Control | Trailer | - | 8 bits | n Bytes | - +---------+----------+ - */ - - txControl.datagramType = DATAGRAM_P2P_TRAINING_PING; - return (transmitDatagram()); -} - -//Build the parameters packet used for training -bool xmitDatagramP2pTrainingParams() -{ - Settings params; - - //Initialize the radio parameters - memcpy(¶ms, &originalSettings, sizeof(settings)); - params.operatingMode = MODE_POINT_TO_POINT; - - //Add the radio parameters - memcpy(endOfTxData, ¶ms, sizeof(params)); - endOfTxData += sizeof(params); - - /* - endOfTxData ---. - | - V - +----------+---------+--- ... ---+----------+ - | Optional | | Radio | Optional | - | NET ID | Control | Parameters | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +----------+---------+-------------+----------+ - */ - - txControl.datagramType = DATAGRAM_P2P_TRAINING_PARAMS; - return (transmitDatagram()); -} - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Point-To-Point: Bring up the link -// -//A three way handshake is used to get both systems to agree that data can flow in both -//directions. This handshake is also used to synchronize the HOP timer. -/* - System A System B - - RESET RESET - | | - Channel 0 | | Channel 0 - V V - .----> P2P_NO_LINK P2P_NO_LINK - | | Tx PING | - | Timeout | | - | V | - | P2P_WAIT_TX_PING_DONE | - | | | - | | Tx Complete - - - - - > | Rx PING - | | Start Rx | - | | MAX_PACKET_SIZE | - | V V - `---- P2P_WAIT_ACK_1 +<----------------------. - | | Tx PING ACK1 | - | V | - | P2P_WAIT_TX_ACK_1_DONE | - | | | - Rx PING ACK1 | < - - - - - - - - - - - | Tx Complete | - | | Start Rx | - | | MAX_PACKET_SIZE | - | | | - V V Timeout | - .---------->+ P2P_WAIT_ACK_2 -------------->+ - | TX PING | | ^ - | ACK2 | | | - | V | | - | P2P_WAIT_TX_ACK_2_DONE | | - | | Tx Complete - - - - - > | Rx PING ACK2 | - | Stop | Start HOP timer | Start HOP Timer | Stop - | HOP | Start Rx | Start Rx | HOP - | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | Timer - | | | | - | Rx | | | - | PING ACK V V Rx PING | - `----- P2P_LINK_UP P2P_LINK_UP -----------------’ - | | - | Rx Data | Rx Data - | | - V V - - Two timers are in use: - datagramTimer: Set at end of transmit, measures ACK timeout - heartbeatTimer: Set upon entry to P2P_NO_LINK, measures time to send next PING -*/ -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//First packet in the three way handshake to bring up the link -bool xmitDatagramP2PPing() -{ - unsigned long currentMillis = millis(); - memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); - endOfTxData += sizeof(unsigned long); - - /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+ - | | | | | - | NET ID | Control | Millis | Trailer | - | 8 bits | 8 bits | 4 bytes | n Bytes | - +--------+---------+---------+----------+ - */ - - txControl.datagramType = DATAGRAM_PING; - return (transmitDatagram()); -} - -//Second packet in the three way handshake to bring up the link -bool xmitDatagramP2PAck1() -{ - unsigned long currentMillis = millis(); - memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); - endOfTxData += sizeof(unsigned long); - - /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+ - | | | | Optional | - | NET ID | Control | Millis | Trailer | - | 8 bits | 8 bits | 4 bytes | n Bytes | - +--------+---------+---------+----------+ - */ - - txControl.datagramType = DATAGRAM_ACK_1; - return (transmitDatagram()); -} - -//Last packet in the three way handshake to bring up the link -bool xmitDatagramP2PAck2() -{ - unsigned long currentMillis = millis(); - memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); - endOfTxData += sizeof(unsigned long); - - /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+ - | | | | | - | NET ID | Control | Millis | Trailer | - | 8 bits | 8 bits | 4 bytes | n Bytes | - +--------+---------+---------+----------+ - */ - - txControl.datagramType = DATAGRAM_ACK_2; - return (transmitDatagram()); -} - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -// Point-to-Point Data Exchange -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Send a command datagram to the remote system -bool xmitDatagramP2PCommand() -{ - /* - endOfTxData ---. - | - V - +--------+---------+--- ... ---+----------+ - | | | | Optional | - | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +--------+---------+-------------+----------+ - */ - - txControl.datagramType = DATAGRAM_REMOTE_COMMAND; - return (transmitDatagram()); -} - -//Send a command response datagram to the remote system -bool xmitDatagramP2PCommandResponse() -{ - /* - endOfTxData ---. - | - V - +--------+---------+--- ... ---+----------+ - | | | | Optional | - | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +--------+---------+-------------+----------+ - */ - - txControl.datagramType = DATAGRAM_REMOTE_COMMAND_RESPONSE; - return (transmitDatagram()); -} - -//Send a data datagram to the remote system -bool xmitDatagramP2PData() -{ - /* - endOfTxData ---. - | - V - +--------+---------+--- ... ---+----------+ - | | | | Optional | - | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +--------+---------+-------------+----------+ - */ - - txControl.datagramType = DATAGRAM_DATA; - return (transmitDatagram()); -} - -//Heartbeat packet to keep the link up -bool xmitDatagramP2PHeartbeat() -{ - unsigned long currentMillis = millis(); - memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); - endOfTxData += sizeof(currentMillis); - - /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+ - | | | | Optional | - | NET ID | Control | Millis | Trailer | - | 8 bits | 8 bits | 4 Bytes | n Bytes | - +--------+---------+---------+----------+ - */ - - txControl.datagramType = DATAGRAM_HEARTBEAT; - return (transmitDatagram()); -} - -//Create short packet of 2 control bytes - do not expect ack -bool xmitDatagramP2PAck() -{ - int ackLength; - - uint8_t * ackStart = endOfTxData; - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - - //Verify the ACK length - ackLength = endOfTxData - ackStart; - if (ackLength != CLOCK_SYNC_BYTES) - { - systemPrint("ERROR - Please define CLOCK_SYNC_BYTES = "); - systemPrintln(ackLength); - waitForever(); - } - - /* - endOfTxData ---. - | - V - +--------+---------+----------+----------+ - | | | Channel | Optional | - | NET ID | Control | Timer | Trailer | - | 8 bits | 8 bits | 2 bytes | n Bytes | - +--------+---------+----------+----------+ - */ - - txControl.datagramType = DATAGRAM_DATA_ACK; - return (transmitDatagram()); -} - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -// Multi-Point Data Exchange -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Send a data datagram to the remote system, including sync data -bool xmitDatagramMpData() -{ - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - - /* - endOfTxData ---. - | - V - +--------+---------+--- ... ---+----------+----------+ - | | | | Channel | Optional | - | NET ID | Control | Data | Timer | Trailer | - | 8 bits | 8 bits | n bytes | 2 bytes | n Bytes | - +--------+---------+-------------+----------+----------+ - */ - - - - txControl.datagramType = DATAGRAM_DATA; - return (transmitDatagram()); -} - -//Heartbeat packet to sync other units in multipoint mode -bool xmitDatagramMpHeartbeat() -{ - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - - /* - endOfTxData ---. - | - V - +--------+---------+----------+----------+ - | | | Channel | Optional | - | NET ID | Control | Timer | Trailer | - | 8 bits | 8 bits | 2 bytes | n Bytes | - +--------+---------+----------+----------+ - */ - - txControl.datagramType = DATAGRAM_HEARTBEAT; - return (transmitDatagram()); -} - -//Ack packet sent by server in response the client ping, includes sync data and channel number -//During Multipoint scanning, it's possible for the client to get an ack but be 500kHz off -//The channel Number ensures that the client gets the next hop correct -bool xmitDatagramMpAck() -{ - memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); - endOfTxData += sizeof(channelNumber); - - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - - - /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+----------+ - | | | Channel | Channel | Optional | - | NET ID | Control | Number | Timer | Trailer | - | 8 bits | 8 bits | 1 byte | 2 bytes | n Bytes | - +--------+---------+---------+----------+----------+ - */ - - txControl.datagramType = DATAGRAM_ACK_1; - return (transmitDatagram()); -} - -//Ping packet sent during scanning -bool xmitDatagramMpPing() -{ - /* - endOfTxData ---. - | - V - +--------+---------+----------+ - | | | Optional | - | NET ID | Control | Trailer | - | 8 bits | 8 bits | n Bytes | - +--------+---------+----------+ - */ - - txControl.datagramType = DATAGRAM_PING; - return (transmitDatagram()); -} - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Multi-Point Client Training -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Build the client ping packet used for training -bool xmitDatagramMpTrainingPing() -{ - //Add the source (server) ID - memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); - endOfTxData += UNIQUE_ID_BYTES; - - /* - endOfTxData ---. - | - V - +----------+---------+-----------+----------+ - | Optional | | | Optional | - | NET ID | Control | Client ID | Trailer | - | 8 bits | 8 bits | 16 Bytes | n Bytes | - +----------+---------+-----------+----------+ - */ - - txControl.datagramType = DATAGRAM_TRAINING_PING; - return (transmitDatagram()); -} - -//Build the client ACK packet used for training -bool xmitDatagramMpTrainingAck(uint8_t * serverID) -{ - //Add the destination (server) ID - memcpy(endOfTxData, serverID, UNIQUE_ID_BYTES); - endOfTxData += UNIQUE_ID_BYTES; - - //Add the source (client) ID - memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); - endOfTxData += UNIQUE_ID_BYTES; - - /* - endOfTxData ---. - | - V - +----------+---------+-----------+-----------+----------+ - | Optional | | | | Optional | - | NET ID | Control | Server ID | Client ID | Trailer | - | 8 bits | 8 bits | 16 Bytes | 16 Bytes | n Bytes | - +----------+---------+-----------+-----------+----------+ - */ - - txControl.datagramType = DATAGRAM_TRAINING_ACK; - return (transmitDatagram()); -} - -void updateRadioParameters(uint8_t * rxData) -{ - Settings params; - - //Get the parameters - memcpy(¶ms, rxData, sizeof(params)); - - //Update the radio parameters - originalSettings.airSpeed = params.airSpeed; - originalSettings.autoTuneFrequency = params.autoTuneFrequency; - originalSettings.radioBandwidth = params.radioBandwidth; - originalSettings.radioCodingRate = params.radioCodingRate; - originalSettings.frequencyHop = params.frequencyHop; - originalSettings.frequencyMax = params.frequencyMax; - originalSettings.frequencyMin = params.frequencyMin; - originalSettings.maxDwellTime = params.maxDwellTime; - originalSettings.numberOfChannels = params.numberOfChannels; - originalSettings.radioPreambleLength = params.radioPreambleLength; - originalSettings.radioSpreadFactor = params.radioSpreadFactor; - originalSettings.radioSyncWord = params.radioSyncWord; - originalSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; - - //Update the radio protocol parameters - originalSettings.dataScrambling = params.dataScrambling; - originalSettings.enableCRC16 = params.enableCRC16; - originalSettings.encryptData = params.encryptData; - memcpy(originalSettings.encryptionKey, params.encryptionKey, sizeof(originalSettings.encryptionKey)); - originalSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; - originalSettings.heartbeatTimeout = params.heartbeatTimeout; - originalSettings.maxResends = params.maxResends; - originalSettings.netID = params.netID; - originalSettings.operatingMode = params.operatingMode; - originalSettings.overheadTime = params.overheadTime; - originalSettings.server = params.server; - originalSettings.verifyRxNetID = params.verifyRxNetID; - - //Update the debug parameters - if (params.copyDebug) - { - originalSettings.debug = params.debug; - originalSettings.alternateLedUsage = params.alternateLedUsage; - originalSettings.copyDebug = params.copyDebug; - originalSettings.debug = params.debug; - originalSettings.debugDatagrams = params.debugDatagrams; - originalSettings.debugNvm = params.debugNvm; - originalSettings.debugRadio = params.debugRadio; - originalSettings.debugReceive = params.debugReceive; - originalSettings.debugStates = params.debugStates; - originalSettings.debugTraining = params.debugTraining; - originalSettings.debugTransmit = params.debugTransmit; - originalSettings.debugSerial = params.debugSerial; - originalSettings.displayPacketQuality = params.displayPacketQuality; - originalSettings.displayRealMillis = params.displayRealMillis; - originalSettings.printAckNumbers = params.printAckNumbers; - originalSettings.printFrequency = params.printFrequency; - originalSettings.printLinkUpDown = params.printLinkUpDown; - originalSettings.printPktData = params.printPktData; - originalSettings.printRfData = params.printRfData; - originalSettings.printTimestamp = params.printTimestamp; - originalSettings.printTxErrors = params.printTxErrors; - } - - //Update the serial parameters - if (params.copySerial) - { - originalSettings.copySerial = params.copySerial; - originalSettings.echo = params.echo; - originalSettings.flowControl = params.flowControl; - originalSettings.invertCts = params.invertCts; - originalSettings.invertRts = params.invertRts; - originalSettings.serialSpeed = params.serialSpeed; - originalSettings.usbSerialWait = params.usbSerialWait; - } - - //Update the training values - originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; - //The trainingKey is already the same - originalSettings.trainingTimeout = params.trainingTimeout; - - //Update the trigger parameters - if (params.copyTriggers) - { - originalSettings.copyTriggers = params.copyTriggers; - originalSettings.triggerEnable = params.triggerEnable; - originalSettings.triggerEnable2 = params.triggerEnable2; - originalSettings.triggerWidth = params.triggerWidth; - originalSettings.triggerWidthIsMultiplier = params.triggerWidthIsMultiplier; - } -} - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Multi-Point Server Training -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Build the server parameters packet used for training -bool xmitDatagramMpRadioParameters(const uint8_t * clientID) -{ - Settings params; - - //Initialize the radio parameters - memcpy(¶ms, &originalSettings, sizeof(settings)); - params.server = false; - - //Add the destination (client) ID - memcpy(endOfTxData, clientID, UNIQUE_ID_BYTES); - endOfTxData += UNIQUE_ID_BYTES; - - //Add the source (server) ID - memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); - endOfTxData += UNIQUE_ID_BYTES; - - //Add the radio parameters - memcpy(endOfTxData, ¶ms, sizeof(params)); - endOfTxData += sizeof(params); - - /* - endOfTxData ---. - | - V - +----------+---------+-----------+-----------+--- ... ---+----------+ - | Optional | | | | Radio | Optional | - | NET ID | Control | Client ID | Server ID | Parameters | Trailer | - | 8 bits | 8 bits | 16 Bytes | 16 Bytes | n bytes | n Bytes | - +----------+---------+-----------+-----------+-------------+----------+ - */ - - txControl.datagramType = DATAGRAM_TRAINING_PARAMS; - return (transmitDatagram()); -} - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Virtual Circuit frames -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -bool xmitVcHeartbeat(int8_t addr, uint8_t * id) -{ - uint8_t * txData; - - uint32_t currentMillis = millis(); - txData = endOfTxData; - *endOfTxData++ = 0; //Reserve for length - *endOfTxData++ = VC_BROADCAST; - *endOfTxData++ = addr; - memcpy(endOfTxData, id, UNIQUE_ID_BYTES); - endOfTxData += UNIQUE_ID_BYTES; - memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); - endOfTxData += sizeof(currentMillis); - - //Set the length field - *txData = (uint8_t)(endOfTxData - txData); - - /* - endOfTxData ---. - | - V - +----------+---------+--------+----------+---------+----------+---------+----------+ - | Optional | | | | | | | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Src ID | millis | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes || n Bytes | - +----------+---------+--------+----------+---------+----------+---------+----------+ - */ - - txControl.datagramType = DATAGRAM_VC_HEARTBEAT; - txControl.ackNumber = 0; - - //Determine the time that it took to pass this frame to the radio - //This time is used to adjust the time offset - vcTxHeartbeatMillis = millis() - currentMillis; - - //Select a random for the next heartbeat - setHeartbeatLong(); //Those who send a heartbeat or data have long time before next heartbeat. Those who send ACKs, have short wait to next heartbeat. - - return (transmitDatagram()); -} - -//Build the ACK frame -bool xmitVcAckFrame(int8_t destVc) -{ - VC_RADIO_MESSAGE_HEADER * vcHeader; - - vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; - vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; - vcHeader->destVc = destVc; - vcHeader->srcVc = myVc; - endOfTxData += VC_RADIO_HEADER_BYTES; - - /* - endOfTxData ---. - | - V - +--------+---------+--------+----------+---------+----------+----------+ - | | | | | | Channel | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Timer | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 2 bytes | n Bytes | - +--------+---------+--------+----------+---------+----------+----------+ - */ - - //Finish building the ACK frame - return xmitDatagramP2PAck(); -} - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Datagram reception -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Determine the type of datagram received -PacketType rcvDatagram() -{ - uint8_t ackNumber; - PacketType datagramType; - uint8_t receivedNetID; - CONTROL_U8 rxControl; - VIRTUAL_CIRCUIT * vc; - VC_RADIO_MESSAGE_HEADER * vcHeader; - - //Acknowledge the receive interrupt - transactionComplete = false; - - //Save the receive time - rcvTimeMillis = millis(); - - //Get the received datagram - framesReceived++; - int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); - - if (state == RADIOLIB_ERR_NONE) - { - //Do nothing - } - else if (state == RADIOLIB_ERR_CRC_MISMATCH) - { - if (settings.debug || settings.debugDatagrams || settings.debugReceive) - { - systemPrintln("Receive CRC error!"); - outputSerialData(true); - } - badCrc++; - returnToReceiving(); //Return to listening - return (DATAGRAM_CRC_ERROR); - } - else - { - if (settings.debug || settings.debugDatagrams || settings.debugReceive) - { - systemPrint("Receive error: "); - systemPrintln(state); - outputSerialData(true); - } - returnToReceiving(); //Return to listening - badFrames++; - return (DATAGRAM_BAD); - } - - rxDataBytes = radio.getPacketLength(); - - returnToReceiving(); //Immediately begin listening while we process new data - - rxData = incomingBuffer; - vc = &virtualCircuitList[0]; - vcHeader = NULL; - - /* - |<---------------------- rxDataBytes ---------------------->| - | | - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - ^ - | - '---- rxData - */ - - //Display the received data bytes - if (settings.printRfData || settings.debugReceive) - { - systemPrintln("----------"); - systemPrintTimestamp(); - systemPrint("RX: "); - systemPrint((settings.dataScrambling || settings.encryptData) ? "Encrypted " : "Unencrypted "); - systemPrint("Frame "); - systemPrint(rxDataBytes); - systemPrint(" (0x"); - systemPrint(rxDataBytes, HEX); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printRfData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - outputSerialData(true); - } - - if (settings.dataScrambling == true) - radioComputeWhitening(incomingBuffer, rxDataBytes); - - if (settings.encryptData == true) - { - decryptBuffer(incomingBuffer, rxDataBytes); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - - if (settings.debugReceive) - { - systemPrint("in: "); - dumpBufferRaw(incomingBuffer, 14); //Print only the first few bytes when debugging packets - } - - //Display the received data bytes - if ((settings.dataScrambling || settings.encryptData) - && (settings.printRfData || settings.debugReceive)) - { - systemPrintTimestamp(); - systemPrint("RX: Unencrypted Frame "); - systemPrint(rxDataBytes); - systemPrint(" (0x"); - systemPrint(rxDataBytes, HEX); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printRfData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - outputSerialData(true); - } - - //All packets must include the 2-byte control header - if (rxDataBytes < minDatagramSize) - { - //Display the packet contents - if (settings.printPktData || settings.debugDatagrams || settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint("RX: Bad Frame "); - systemPrint(rxDataBytes); - systemPrint(" (0x"); - systemPrint(rxDataBytes, HEX); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printRfData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - outputSerialData(true); - } - badFrames++; - return (DATAGRAM_BAD); - } - - /* - |<---------------------- rxDataBytes ---------------------->| - | | - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - ^ - | - '---- rxData - */ - - //Verify the netID if necessary - if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) - { - receivedNetID = *rxData++; - if (receivedNetID != settings.netID) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("RX: NetID "); - systemPrint(receivedNetID); - systemPrint(" (0x"); - systemPrint(receivedNetID, HEX); - systemPrint(")"); - if (receivedNetID != settings.netID) - { - systemPrint(" expecting "); - systemPrint(settings.netID); - } - systemPrintln(); - outputSerialData(true); - } - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.debugReceive && settings.printPktData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - outputSerialData(true); - netIdMismatch++; - return (DATAGRAM_NETID_MISMATCH); - } - } - - //Process the trailer - petWDT(); - if (settings.enableCRC16) - { - uint16_t crc; - uint8_t * data; - - //Compute the CRC-16 value - crc = 0xffff; - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - for (data = incomingBuffer; data < &incomingBuffer[rxDataBytes - 2]; data++) - crc = crc16Table[*data ^ (uint8_t)(crc >> (16 - 8))] ^ (crc << 8); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - if ((incomingBuffer[rxDataBytes - 2] != (crc >> 8)) - && (incomingBuffer[rxDataBytes - 1] != (crc & 0xff))) - { - //Display the packet contents - if (settings.printPktData || settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("RX: Bad CRC-16, received 0x"); - systemPrint(incomingBuffer[rxDataBytes - 2], HEX); - systemPrint(incomingBuffer[rxDataBytes - 1], HEX); - systemPrint(" expected 0x"); - systemPrintln(crc, HEX); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printRfData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - outputSerialData(true); - } - badCrc++; - return (DATAGRAM_CRC_ERROR); - } - } - - /* - |<---------------------- rxDataBytes ---------------------->| - | | - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - ^ - | - '---- rxData - */ - - //Get the control byte - rxControl = *((CONTROL_U8 *)rxData++); - datagramType = rxControl.datagramType; - ackNumber = rxControl.ackNumber; - if (settings.debugReceive) - printControl(*((uint8_t *)&rxControl)); - if (datagramType >= MAX_V2_DATAGRAM_TYPE) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("RX: Invalid datagram type "); - systemPrintln(datagramType); - outputSerialData(true); - } - badFrames++; - return (DATAGRAM_BAD); - } - - //Display the CRC - if (settings.enableCRC16 && settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint(" CRC-16: 0x"); - systemPrint(incomingBuffer[rxDataBytes - 2], HEX); - systemPrintln(incomingBuffer[rxDataBytes - 1], HEX); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - - /* - |<---------------------- rxDataBytes ---------------------->| - | | - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - ^ - | - '---- rxData - */ - - //Get the spread factor 6 length - if (settings.radioSpreadFactor == 6) - { - if (rxDataBytes >= (*rxData + minDatagramSize)) - rxDataBytes = *rxData++; - else - { - if (settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint("Invalid SF6 length, received SF6 length "); - systemPrint(*rxData); - systemPrint(" > "); - systemPrint((int)rxDataBytes - minDatagramSize); - systemPrintln(" received bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - badFrames++; - return (DATAGRAM_BAD); - } - if (settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint(" SF6 Length: "); - systemPrintln(rxDataBytes); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - } - else - rxDataBytes -= minDatagramSize; - - //Get the Virtual-Circuit header - rxVcData = rxData; - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - { - //Verify that the virtual circuit header is present - if (rxDataBytes < 3) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("Missing VC header bytes, received only "); - systemPrint(rxDataBytes); - systemPrintln(" bytes, expecting at least 3 bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - badFrames++; - return DATAGRAM_BAD; - } - - //Parse the virtual circuit header - vcHeader = (VC_RADIO_MESSAGE_HEADER *)rxData; - rxDestVc = vcHeader->destVc; - rxSrcVc = vcHeader->srcVc; - rxVcData = &rxData[3]; - - //Display the virtual circuit header - if (settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint(" VC Length: "); - systemPrintln(vcHeader->length); - systemPrint(" DestAddr: "); - if (rxDestVc == VC_BROADCAST) - systemPrintln("Broadcast"); - else - systemPrintln(rxDestVc); - systemPrint(" SrcAddr: "); - if (rxSrcVc == VC_UNASSIGNED) - systemPrintln("Unassigned"); - else - systemPrintln(rxSrcVc); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - - //Validate the source VC - vc = NULL; - if (rxSrcVc != VC_UNASSIGNED) - { - if ((uint8_t)rxSrcVc >= MAX_VC) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("Invalid source VC: "); - systemPrintln(rxSrcVc); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - if (settings.printRfData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - outputSerialData(true); - } - badFrames++; - return DATAGRAM_BAD; - } - vc = &virtualCircuitList[rxSrcVc]; - } - - //Validate the length - if (vcHeader->length != rxDataBytes) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("Invalid VC length, received "); - systemPrint(vcHeader->length); - systemPrint(" expecting "); - systemPrintln(rxDataBytes); - outputSerialData(true); - } - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - if (vc) - vc->badLength++; - badFrames++; - return DATAGRAM_BAD; - } - - //Validate the destination VC - if ((rxDestVc != VC_BROADCAST) && (rxDestVc != myVc)) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("Not my VC: "); - systemPrintln(rxDestVc); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - if (settings.printPktData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - outputSerialData(true); - } - return DATAGRAM_NOT_MINE; - } - } - - //Verify the packet number last so that the expected datagram or ACK number can be updated - if (vc && (settings.operatingMode != MODE_DATAGRAM)) - { - switch (datagramType) - { - default: - break; - - case DATAGRAM_DATA_ACK: - if (ackNumber != vc->txAckNumber) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("Invalid ACK number, received "); - systemPrint(ackNumber); - systemPrint(" expecting "); - systemPrintln(vc->txAckNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - badFrames++; - return (DATAGRAM_BAD); - } - - //Set the next TX ACK number - if (settings.printAckNumbers) - { - systemPrint("txAckNumber: "); - systemPrint(vc->txAckNumber); - systemPrint(" --> "); - } - vc->txAckNumber = (vc->txAckNumber + 1) & 3; - if (settings.printAckNumbers) - { - systemPrintln(vc->txAckNumber); - outputSerialData(true); - } - break; - - case DATAGRAM_HEARTBEAT: - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - break; - datagramType = validateDatagram(vc, datagramType, ackNumber, rxDataBytes); - if (datagramType != DATAGRAM_HEARTBEAT) - return datagramType; - break; - - case DATAGRAM_REMOTE_COMMAND: - datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(commandRXBuffer) - - availableRXCommandBytes()); - if (datagramType != DATAGRAM_REMOTE_COMMAND) - return datagramType; - break; - - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(serialTransmitBuffer) - - availableTXBytes()); - if (datagramType != DATAGRAM_REMOTE_COMMAND_RESPONSE) - return datagramType; - break; - - case DATAGRAM_DATA: - datagramType = validateDatagram(vc, datagramType, ackNumber, sizeof(serialTransmitBuffer) - - availableTXBytes()); - if (datagramType != DATAGRAM_DATA) - return datagramType; - vc->messagesReceived++; - break; - } - - //Account for this frame - vc->framesReceived++; - } - - /* - |<-- rxDataBytes -->| - | | - +----------+----------+------------+------ ... ------+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------------+----------+ - ^ - | - '---- rxData - */ - - //Display the packet contents - if (settings.printPktData || settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint("RX: Datagram "); - systemPrint(rxDataBytes); - systemPrint(" (0x"); - systemPrint(rxDataBytes, HEX); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printPktData && rxDataBytes) - dumpBuffer(rxData, rxDataBytes); - } - - //Display the datagram type - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("RX: "); - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - { - systemPrint((uint8_t)((rxDestVc == VC_BROADCAST) ? rxDestVc : rxDestVc & VCAB_NUMBER_MASK)); - systemPrint(" <-- "); - systemPrint((uint8_t)((rxSrcVc == VC_UNASSIGNED) ? rxSrcVc : rxSrcVc & VCAB_NUMBER_MASK)); - systemWrite(' '); - } - systemPrint(v2DatagramType[datagramType]); - switch (datagramType) - { - default: - systemPrintln(); - break; - - case DATAGRAM_DATA: - case DATAGRAM_DATA_ACK: - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - if (settings.operatingMode != MODE_DATAGRAM) - { - systemPrint(" ("); - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - systemPrint("VC "); - systemPrint("ACK #"); - systemPrint(ackNumber); - systemPrint(")"); - } - systemPrintln(); - break; - } - outputSerialData(true); - } - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - //Process the packet - datagramsReceived++; - linkDownTimer = millis(); - - //Blink the RX LED - if (settings.alternateLedUsage) - digitalWrite(ALT_LED_RX_DATA, LED_ON); - return datagramType; -} - -PacketType validateDatagram(VIRTUAL_CIRCUIT * vc, PacketType datagramType, uint8_t ackNumber, uint16_t freeBytes) -{ - if (ackNumber != vc->rmtTxAckNumber) - { - //Determine if this is a duplicate datagram - if (ackNumber == ((vc->rmtTxAckNumber - 1) & 3)) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("Duplicate datagram received, ACK "); - systemPrintln(ackNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - linkDownTimer = millis(); - duplicateFrames++; - return DATAGRAM_DUPLICATE; - } - - //Not a duplicate - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("Invalid datagram number, received "); - systemPrint(ackNumber); - systemPrint(" expecting "); - systemPrintln(vc->rmtTxAckNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - badFrames++; - return DATAGRAM_BAD; - } - - //Verify that there is sufficient space in the serialTransmitBuffer - if (inCommandMode || ((sizeof(serialTransmitBuffer) - availableTXBytes()) < rxDataBytes)) - { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrintln("Insufficient space in the serialTransmitBuffer"); - } - insufficientSpace++; - - //Apply back pressure to the other radio by dropping this packet and - //forcing the other radio to retransmit the packet. - badFrames++; - return DATAGRAM_BAD; - } - - //Receive this data packet and set the next expected datagram number - if (settings.printAckNumbers) - { - systemPrint("rxAckNumber: "); - systemPrint(vc->rxAckNumber); - systemPrint(" --> "); - } - vc->rxAckNumber = vc->rmtTxAckNumber; - if (settings.printAckNumbers) - { - systemPrintln(vc->rxAckNumber); - systemPrint("rmtTxAckNumber: "); - systemPrint(vc->rmtTxAckNumber); - systemPrint(" --> "); - } - vc->rmtTxAckNumber = (vc->rmtTxAckNumber + 1) & 3; - if (settings.printAckNumbers) - { - systemPrintln(vc->rmtTxAckNumber); - outputSerialData(true); - } - return datagramType; -} - -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Datagram transmission -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Push the outgoing packet to the air -//Returns false if we could not start tranmission due to packet received or RX in process -bool transmitDatagram() -{ - uint8_t control; - uint8_t * header; - uint8_t length; - int8_t srcVc; - uint8_t * vcData; - VIRTUAL_CIRCUIT * vc; - VC_RADIO_MESSAGE_HEADER * vcHeader; - - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - //Parse the virtual circuit header - vc = &virtualCircuitList[0]; - vcHeader = NULL; - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - { - vc = NULL; - vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; - txDestVc = vcHeader->destVc; - srcVc = vcHeader->srcVc; - if ((uint8_t)vcHeader->destVc <= MAX_VC) - { - vc = &virtualCircuitList[txDestVc]; - vc->messagesSent++; - } - vcData = (uint8_t *)&vcHeader[1]; - } - - //Determine the packet size - datagramsSent++; - txDatagramSize = endOfTxData - outgoingPacket; - length = txDatagramSize - headerBytes; - - //Select the ACK number - if (txControl.datagramType == DATAGRAM_DATA_ACK) - txControl.ackNumber = vc->rxAckNumber; - else - txControl.ackNumber = vc->txAckNumber; - - //Process the packet - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("TX: "); - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - { - systemPrint((uint8_t)((srcVc == VC_UNASSIGNED) ? srcVc : srcVc & VCAB_NUMBER_MASK)); - systemPrint(" --> "); - systemPrint((uint8_t)((txDestVc == VC_BROADCAST) ? txDestVc : txDestVc & VCAB_NUMBER_MASK)); - systemWrite(' '); - } - systemPrint(v2DatagramType[txControl.datagramType]); - switch (txControl.datagramType) - { - default: - systemPrintln(); - break; - - case DATAGRAM_DATA: - case DATAGRAM_DATA_ACK: - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - if (settings.operatingMode != MODE_DATAGRAM) - { - systemPrint(" ("); - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - systemPrint("VC "); - systemPrint("ACK #"); - systemPrint(txControl.ackNumber); - systemPrint(")"); - } - systemPrintln(); - break; - } - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - - /* - endOfTxData ---. - | - V - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - | | | - | |<- Length -->| - |<--------- txDatagramSize --------------------->| - */ - - //Display the packet contents - if (settings.printPktData || settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint("TX: Datagram "); - systemPrint(length); - systemPrint(" (0x"); - systemPrint(length, HEX); - systemPrintln(") bytes"); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printPktData) - dumpBuffer(&endOfTxData[-length], length); - outputSerialData(true); - } - - //Build the datagram header - header = outgoingPacket; - if (headerBytes && settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint("TX: Header"); - systemPrint(headerBytes); - systemPrint(" (0x"); - systemPrint(headerBytes); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - - //Add the netID if necessary - if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) - { - *header++ = settings.netID; - - //Display the netID value - if (settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint(" NetID: "); - systemPrint(settings.netID); - systemPrint(" (0x"); - systemPrint(settings.netID, HEX); - systemPrintln(")"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - } - } - - //Add the control byte - control = *(uint8_t *)&txControl; - *header++ = control; - - //Display the control value - if (settings.debugTransmit) - printControl(control); - - //Add the spread factor 6 length if required - if (settings.radioSpreadFactor == 6) - { - *header++ = length; - - //Send either a short ACK or full length packet - switch (txControl.datagramType) - { - default: - txDatagramSize = MAX_PACKET_SIZE - trailerBytes; //We're now going to transmit a full size datagram - break; - - case DATAGRAM_PING: - case DATAGRAM_ACK_1: - case DATAGRAM_ACK_2: - txDatagramSize = headerBytes + CLOCK_MILLIS_BYTES; //Short packet is 3 + 4 - break; - - case DATAGRAM_DATA_ACK: - txDatagramSize = headerBytes + CLOCK_SYNC_BYTES; //Short ACK packet is 3 + 2 - break; - } - - - radio.implicitHeader(txDatagramSize); //Set header size so that hardware CRC is calculated correctly - - endOfTxData = &outgoingPacket[txDatagramSize]; - if (settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint(" SF6 Length: "); - systemPrintln(length); - systemPrint(" SF6 TX Header Size: "); - systemPrintln(txDatagramSize); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - } - - //Verify the Virtual-Circuit length - if (settings.debugTransmit && (settings.operatingMode == MODE_VIRTUAL_CIRCUIT)) - { - systemPrintTimestamp(); - systemPrint(" Length: "); - systemPrintln(vcHeader->length); - systemPrint(" DestAddr: "); - if (txDestVc == VC_BROADCAST) - systemPrintln("Broadcast"); - else - systemPrintln(txDestVc); - systemPrint(" SrcAddr: "); - if (srcVc == VC_UNASSIGNED) - systemPrintln("Unassigned"); - else - systemPrintln(srcVc); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - - /* - endOfTxData ---. - | - V - +----------+----------+------------+--- ... ---+ - | Optional | | Optional | | - | NET ID | Control | SF6 Length | Data | - | 8 bits | 8 bits | 8 bits | n bytes | - +----------+----------+------------+-------------+ - | | - |<--------------- txDatagramSize --------------->| - */ - - //Add the datagram trailer - if (settings.enableCRC16) - { - uint16_t crc; - uint8_t * txData; - - //Compute the CRC-16 value - crc = 0xffff; - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - for (txData = outgoingPacket; txData < endOfTxData; txData++) - crc = crc16Table[*txData ^ (uint8_t)(crc >> (16 - 8))] ^ (crc << 8); - *endOfTxData++ = (uint8_t)(crc >> 8); - *endOfTxData++ = (uint8_t)(crc & 0xff); - } - txDatagramSize += trailerBytes; - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - //Display the trailer - if (trailerBytes && settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint("TX: Trailer "); - systemPrint(trailerBytes); - systemPrint(" (0x"); - systemPrint(trailerBytes); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - //Display the CRC - if (settings.enableCRC16 && (settings.printPktData || settings.debugReceive)) - { - systemPrintTimestamp(); - systemPrint(" CRC-16: 0x"); - systemPrint(endOfTxData[-2], HEX); - systemPrintln(endOfTxData[-1], HEX); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - } - - /* - endOfTxData ---. - | - V - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - | | - |<-------------------- txDatagramSize --------------------->| - */ - - //Display the transmitted packet bytes - if (settings.printRfData || settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint("TX: Unencrypted Frame "); - systemPrint(txDatagramSize); - systemPrint(" (0x"); - systemPrint(txDatagramSize, HEX); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printRfData) - dumpBuffer(outgoingPacket, txDatagramSize); - outputSerialData(true); - } - - //Print before encryption - if (settings.debugTransmit) - { - systemPrint("out: "); - dumpBufferRaw(outgoingPacket, 14); - } - - //Encrypt the datagram - if (settings.encryptData == true) - { - encryptBuffer(outgoingPacket, txDatagramSize); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - - //Scramble the datagram - if (settings.dataScrambling == true) - radioComputeWhitening(outgoingPacket, txDatagramSize); - - //Display the transmitted packet bytes - if ((settings.printRfData || settings.debugTransmit) - && (settings.encryptData || settings.dataScrambling)) - { - systemPrintTimestamp(); - systemPrint("TX: Encrypted Frame "); - systemPrint(txDatagramSize); - systemPrint(" (0x"); - systemPrint(txDatagramSize, HEX); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printRfData) - dumpBuffer(outgoingPacket, txDatagramSize); - outputSerialData(true); - } - - //If we are trainsmitting at high data rates the receiver is often not ready for new data. Pause for a few ms (measured with logic analyzer). - if (settings.airSpeed == 28800 || settings.airSpeed == 38400) - delay(2); - - //Reset the buffer data pointer for the next transmit operation - endOfTxData = &outgoingPacket[headerBytes]; - - //Compute the time needed for this frame. Part of ACK timeout. - frameAirTime = calcAirTime(txDatagramSize); - - //Transmit this datagram - frameSentCount = 0; //This is the first time this frame is being sent - return (retransmitDatagram(vc)); -} - -//Print the control byte value -void printControl(uint8_t value) -{ - CONTROL_U8 * control = (CONTROL_U8 *)&value; - - systemPrintTimestamp(); - systemPrint(" Control: 0x"); - systemPrintln(value, HEX); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - systemPrintTimestamp(); - systemPrint(" ACK # "); - systemPrintln(value & 3); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - systemPrintTimestamp(); - systemPrint(" datagramType "); - if (control->datagramType < MAX_V2_DATAGRAM_TYPE) - systemPrintln(v2DatagramType[control->datagramType]); - else - { - systemPrint("Unknown "); - systemPrintln(control->datagramType); - } - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); -} - -//The previous transmission was not received, retransmit the datagram -//Returns false if we could not start tranmission due to packet received or RX in process -bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) -{ - /* - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - | | - |<-------------------- txDatagramSize --------------------->| - */ - - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - //Display the transmitted frame bytes - if (frameSentCount && (settings.printRfData || settings.debugTransmit)) - { - systemPrintTimestamp(); - systemPrint("TX: Retransmit "); - systemPrint((settings.encryptData || settings.dataScrambling) ? "Encrypted " : "Unencrypted "); - systemPrint("Frame "); - systemPrint(txDatagramSize); - systemPrint(" (0x"); - systemPrint(txDatagramSize, HEX); - systemPrintln(") bytes"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - petWDT(); - if (settings.printRfData) - dumpBuffer(outgoingPacket, txDatagramSize); - outputSerialData(true); - } - - //Transmit this frame - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - //Drop this datagram if the receiver is active - frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background - uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond - if ((receiveInProcess() == true) || (transactionComplete == true) - || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) - && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].linkUp == false))) - { - triggerEvent(TRIGGER_TRANSMIT_CANCELED); - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrint("TX failed: "); - if (transactionComplete) - systemPrintln("RXTC"); - else if (receiveInProcess()) - systemPrintln("RXIP"); - else - systemPrintln("VC link down"); - outputSerialData(true); - } - return (false); //Do not start transmit while RX is or has occured - } - else - { - - int state = radio.startTransmit(outgoingPacket, txDatagramSize); - - if (state == RADIOLIB_ERR_NONE) - { - frameSentCount++; - if (vc) - vc->framesSent++; - framesSent++; - xmitTimeMillis = millis(); - if (settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint("TX: frameAirTime "); - systemPrint(frameAirTime); - systemPrintln(" mSec"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - systemPrintTimestamp(); - systemPrint("TX: responseDelay "); - systemPrint(responseDelay); - systemPrintln(" mSec"); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - } - else if (settings.debugTransmit) - { - systemPrintTimestamp(); - systemPrint("TX: Transmit error, state "); - systemPrintln(state); - outputSerialData(true); - } - } - frameAirTime += responseDelay; - datagramTimer = millis(); //Move timestamp even if error - retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime); //Wait this number of ms between retransmits. Increases with each re-transmit. - - //BLink the RX LED - if (settings.alternateLedUsage) - digitalWrite(ALT_LED_TX_DATA, LED_ON); - - return (true); //Tranmission has started -} - -void startChannelTimer() -{ - startChannelTimer(settings.maxDwellTime); -} - -void startChannelTimer(int16_t startAmount) -{ - channelTimer.disableTimer(); - channelTimer.setInterval_MS(startAmount, channelTimerHandler); - channelTimer.enableTimer(); - timerStart = millis(); //ISR normally takes care of this but allow for correct ACK sync before first ISR - triggerEvent(TRIGGER_HOP_TIMER_START); -} - -void stopChannelTimer() -{ - channelTimer.disableTimer(); - triggerEvent(TRIGGER_HOP_TIMER_STOP); -} - -//Given the remote unit's number of ms before its next hop, -//adjust our own channelTimer interrupt to be synchronized with the remote unit -void syncChannelTimer() -{ - int16_t msToNextHopRemote; //Can be negative - memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); - msToNextHopRemote -= ackAirTime; - msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; - - //Different airspeeds complete the transmitComplete ISR at different rates - //We adjust the clock setup as needed - switch (settings.airSpeed) - { - default: - break; - case (40): - msToNextHopRemote -= getReceiveCompletionOffset(); - break; - case (150): - msToNextHopRemote -= 145; - break; - case (400): - msToNextHopRemote -= getReceiveCompletionOffset(); - break; - case (1200): - msToNextHopRemote -= getReceiveCompletionOffset(); - break; - case (2400): - msToNextHopRemote -= getReceiveCompletionOffset(); - break; - case (4800): - break; - case (9600): - break; - case (19200): - break; - case (28800): - msToNextHopRemote -= 2; - break; - case (38400): - msToNextHopRemote -= 3; - break; - } - - //Calculate the remote's absolute distance to its next hop - //A remote hop may be very negative for - - int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - timerStart); - - //Precalculate large/small time amounts - uint16_t smallAmount = settings.maxDwellTime / 8; - uint16_t largeAmount = settings.maxDwellTime - smallAmount; - - int16_t msToNextHop = msToNextHopRemote; //By default, we will adjust our clock to match our mate's - - bool resetHop = false; //The hop ISR may occur while we are forcing a hop (case A and C). Reset timeToHop as needed. - - //Below are the edge cases that occur when a hop occurs near ACK reception - - //msToNextHopLocal is small and msToNextHopRemote is negative (and small) - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in negative (and small) then the remote has hopped - //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) - if (msToNextHopLocal < smallAmount && (msToNextHopRemote <= 0 && msToNextHopRemote >= (smallAmount * -1))) - { - hopChannel(); - msToNextHop = msToNextHopRemote + settings.maxDwellTime; - resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. - } - - //msToNextHopLocal is large and msToNextHopRemote is negative - //If we just hopped (msToNextHopLocal is large), and msToNextHopRemote comes in negative then the remote has hopped - //No need to hop. Adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) - else if (msToNextHopLocal > largeAmount && msToNextHopRemote <= 0) - { - msToNextHop = msToNextHopRemote + settings.maxDwellTime; - } - - //msToNextHopLocal is small and msToNextHopRemote is large - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in large then the remote has hopped recently - //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote) - else if (msToNextHopLocal < smallAmount && msToNextHopRemote > largeAmount) - { - hopChannel(); - msToNextHop = msToNextHopRemote; - resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. - } - - //msToNextHopLocal is large and msToNextHopRemote is large - //If we just hopped (msToNextHopLocal is large), and a msToNextHopRemote comes in large then the remote has hopped - //Then adjust our clock to the remote's next hop (msToNextHopRemote) - else if (msToNextHopLocal > largeAmount && msToNextHopRemote > largeAmount) - { - msToNextHop = msToNextHopRemote; - } - - //msToNextHopLocal is large and msToNextHopRemote is small - //If we just hopped (msToNextHopLocal is large), and a msToNextHopRemote comes in small then the remote is about to hop - //Then adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) - else if (msToNextHopLocal > largeAmount && msToNextHopRemote < smallAmount) - { - msToNextHop = msToNextHopRemote + settings.maxDwellTime; - } - - //msToNextHopLocal is small and msToNextHopRemote is small - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in small then the remote is about to hop - //Then adjust our clock to the remote's next hop (msToNextHopRemote) - else if (msToNextHopLocal < smallAmount && msToNextHopRemote < smallAmount) - { - msToNextHop = msToNextHopRemote; - - //If we have a negative remote hop time that is larger than a dwell time then the remote has hopped again - //This is seen at lower air speeds - //Hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) - if (msToNextHop < (settings.maxDwellTime * -1)) //-402 < -400 - { - hopChannel(); - msToNextHop += settings.maxDwellTime; - resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. - } - } - - //Insure against negative timer values - while (msToNextHop < 0) - msToNextHop += settings.maxDwellTime; - - if (settings.debugSync) - { - systemPrint("msToNextHopRemote: "); - systemPrint(msToNextHopRemote); - systemPrint(" msToNextHopLocal: "); - systemPrint(msToNextHopLocal); - systemPrint(" msToNextHop: "); - systemPrint(msToNextHop); - systemPrintln(); - } - - partialTimer = true; - channelTimer.disableTimer(); - channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's - - if (resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. - timeToHop = false; - - channelTimer.enableTimer(); - - triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug -} - -//This function resets the heartbeat time and re-rolls the random time -//Call when something has happened (ACK received, etc) where clocks have been sync'd -//Short/long times set to avoid two radios attempting to xmit heartbeat at same time -//Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. -void setHeartbeatShort() -{ - heartbeatTimer = millis(); - heartbeatRandomTime = random(settings.heartbeatTimeout * 2 / 10, settings.heartbeatTimeout / 2); //20-50% - - //Slow datarates can have significant ack transmission times - //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); -} - -void setHeartbeatLong() -{ - heartbeatTimer = millis(); - heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% - - //Slow datarates can have significant ack transmission times - //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); -} - -//Only the server sends heartbeats in multipoint mode -//Not random, just the straight timeout -void setHeartbeatMultipoint() -{ - heartbeatTimer = millis(); - heartbeatRandomTime = settings.heartbeatTimeout; - - //Slow datarates can have significant ack transmission times - //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); -} diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 5591523a..d69d5cda 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -123,7 +123,7 @@ void updateRadioState() break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V2 - No Link + //No Link //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Point-To-Point: Bring up the link // @@ -204,7 +204,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrint("Scan: Unhandled packet type "); - systemPrint(v2DatagramType[packetType]); + systemPrint(radioDatagramType[packetType]); systemPrintln(); } break; @@ -278,7 +278,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrint("Scan: Unhandled packet type "); - systemPrint(v2DatagramType[packetType]); + systemPrint(radioDatagramType[packetType]); systemPrintln(); } break; @@ -387,7 +387,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrint("Scan: Unhandled packet type "); - systemPrint(v2DatagramType[packetType]); + systemPrint(radioDatagramType[packetType]); systemPrintln(); } break; @@ -416,7 +416,7 @@ void updateRadioState() setHeartbeatLong(); //We sent ACK1 and they sent ACK2, so don't be the first to send heartbeat //Bring up the link - v2EnterLinkUp(); + enterLinkUp(); break; } } @@ -460,12 +460,12 @@ void updateRadioState() setHeartbeatShort(); //We sent the last ack so be responsible for sending the next heartbeat //Bring up the link - v2EnterLinkUp(); + enterLinkUp(); } break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V2 - Link Up + //Link Up //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Point-To-Point: Data Exchange @@ -546,7 +546,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrint("LinkUp: Unhandled packet type "); - systemPrint(v2DatagramType[packetType]); + systemPrint(radioDatagramType[packetType]); systemPrintln(); outputSerialData(true); } @@ -565,7 +565,7 @@ void updateRadioState() break; case DATAGRAM_PING: - v2BreakLink(); + breakLink(); break; case DATAGRAM_DUPLICATE: @@ -732,7 +732,7 @@ void updateRadioState() } else if ((millis() - linkDownTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)) //Break the link - v2BreakLink(); + breakLink(); } break; @@ -785,7 +785,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrint("RX: Unhandled packet type "); - systemPrint(v2DatagramType[packetType]); + systemPrint(radioDatagramType[packetType]); systemPrintln(); outputSerialData(true); } @@ -805,7 +805,7 @@ void updateRadioState() case DATAGRAM_PING: //Break the link - v2BreakLink(); + breakLink(); break; case DATAGRAM_DATA_ACK: @@ -895,7 +895,7 @@ void updateRadioState() systemPrint("TX: Retransmit "); systemPrint(frameSentCount); systemPrint(", "); - systemPrint(v2DatagramType[txControl.datagramType]); + systemPrint(radioDatagramType[txControl.datagramType]); switch (txControl.datagramType) { default: @@ -930,18 +930,18 @@ void updateRadioState() triggerEvent(TRIGGER_LINK_RETRANSMIT_FAIL); //Break the link - v2BreakLink(); + breakLink(); } } else if ((millis() - linkDownTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)) //Break the link - v2BreakLink(); + breakLink(); //Retransmits are not getting through in a rational time else if (transmitTimer && ((millis() - transmitTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) //Break the link - v2BreakLink(); + breakLink(); break; case RADIO_P2P_LINK_UP_HB_ACK_REXMT: @@ -965,7 +965,7 @@ void updateRadioState() systemPrint("TX: Retransmit "); systemPrint(frameSentCount); systemPrint(", "); - systemPrint(v2DatagramType[txControl.datagramType]); + systemPrint(radioDatagramType[txControl.datagramType]); outputSerialData(true); switch (txControl.datagramType) { @@ -998,12 +998,12 @@ void updateRadioState() } else //Failed to reach the other system, break the link - v2BreakLink(); + breakLink(); } break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V2 - Multi-Point Data Exchange + //Multi-Point Data Exchange //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= case RADIO_MP_BEGIN_SCAN: @@ -1033,7 +1033,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrint("Scan: Unhandled packet type "); - systemPrint(v2DatagramType[packetType]); + systemPrint(radioDatagramType[packetType]); systemPrintln(); outputSerialData(true); } @@ -1163,7 +1163,7 @@ void updateRadioState() { systemPrintTimestamp(); systemPrint("MP Standby: Unhandled packet type "); - systemPrint(v2DatagramType[packetType]); + systemPrint(radioDatagramType[packetType]); systemPrintln(); outputSerialData(true); } @@ -1315,7 +1315,7 @@ void updateRadioState() break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V2 - Multi-Point Client Training + //Multi-Point Client Training //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= /* @@ -1434,7 +1434,7 @@ void updateRadioState() break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V2 - Multi-Point Server Training + //Multi-Point Server Training //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= /* @@ -1537,7 +1537,7 @@ void updateRadioState() break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //V2 - Virtual Circuit States + //Virtual Circuit States //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= /* @@ -1772,7 +1772,7 @@ void updateRadioState() systemPrint("TX: Retransmit "); systemPrint(frameSentCount); systemPrint(", "); - systemPrint(v2DatagramType[txControl.datagramType]); + systemPrint(radioDatagramType[txControl.datagramType]); switch (txControl.datagramType) { default: @@ -2176,11 +2176,11 @@ void verifyRadioStateTable() } //Verify the datagram type table -void verifyV2DatagramType() +void verifyRadioDatagramType() { - if ((sizeof(v2DatagramType) / sizeof(v2DatagramType[0])) != MAX_V2_DATAGRAM_TYPE) + if ((sizeof(radioDatagramType) / sizeof(radioDatagramType[0])) != MAX_DATAGRAM_TYPE) { - systemPrintln("ERROR - Please update the v2DatagramTable"); + systemPrintln("ERROR - Please update the radioDatagramTable"); waitForever(); } } @@ -2243,7 +2243,7 @@ void changeState(RadioStates newState) outputSerialData(true); } -void v2BreakLink() +void breakLink() { //Break the link linkFailures++; @@ -2262,7 +2262,7 @@ void v2BreakLink() changeState(RADIO_RESET); } -void v2EnterLinkUp() +void enterLinkUp() { VIRTUAL_CIRCUIT * vc; diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 20ec64b2..f579e4bd 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -74,7 +74,7 @@ void updateCylonLEDs() } //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//V2 Multi-Point Client/Server Training +//Multi-Point Client/Server Training //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- void beginTrainingClient() diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index c80f4335..585890e2 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -58,83 +58,83 @@ const RADIO_STATE_ENTRY radioStateTable[] = { {RADIO_RESET, 0, "RESET", NULL}, // 0 - //V2 - Point-to-Point link handshake + //Point-to-Point link handshake // State RX Name Description - {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "V2 P2P: [No Link] Waiting for Ping"}, // 4 - {RADIO_P2P_WAIT_TX_PING_DONE, 0, "P2P_WAIT_TX_PING_DONE", "V2 P2P: [No Link] Wait Ping TX Done"},// 5 - {RADIO_P2P_WAIT_ACK_1, 1, "P2P_WAIT_ACK_1", "V2 P2P: [No Link] Waiting for ACK1"}, // 6 - {RADIO_P2P_WAIT_TX_ACK_1_DONE, 0, "P2P_WAIT_TX_ACK_1_DONE", "V2 P2P: [No Link] Wait ACK1 TX Done"},// 7 - {RADIO_P2P_WAIT_ACK_2, 1, "P2P_WAIT_ACK_2", "V2 P2P: [No Link] Waiting for ACK2"}, // 8 - {RADIO_P2P_WAIT_TX_ACK_2_DONE, 0, "P2P_WAIT_TX_ACK_2_DONE", "V2 P2P: [No Link] Wait ACK2 TX Done"},// 9 - - //V2 - Point-to-Point, link up, data exchange + {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "P2P: [No Link] Waiting for Ping"}, // 4 + {RADIO_P2P_WAIT_TX_PING_DONE, 0, "P2P_WAIT_TX_PING_DONE", "P2P: [No Link] Wait Ping TX Done"},// 5 + {RADIO_P2P_WAIT_ACK_1, 1, "P2P_WAIT_ACK_1", "P2P: [No Link] Waiting for ACK1"}, // 6 + {RADIO_P2P_WAIT_TX_ACK_1_DONE, 0, "P2P_WAIT_TX_ACK_1_DONE", "P2P: [No Link] Wait ACK1 TX Done"},// 7 + {RADIO_P2P_WAIT_ACK_2, 1, "P2P_WAIT_ACK_2", "P2P: [No Link] Waiting for ACK2"}, // 8 + {RADIO_P2P_WAIT_TX_ACK_2_DONE, 0, "P2P_WAIT_TX_ACK_2_DONE", "P2P: [No Link] Wait ACK2 TX Done"},// 9 + + //Point-to-Point, link up, data exchange // State RX Name Description - {RADIO_P2P_LINK_UP, 1, "P2P_LINK_UP", "V2 P2P: Receiving Standby"}, //10 - {RADIO_P2P_LINK_UP_WAIT_ACK_DONE, 0, "P2P_LINK_UP_WAIT_ACK_DONE", "V2 P2P: Waiting ACK TX Done"}, //11 - {RADIO_P2P_LINK_UP_WAIT_TX_DONE, 0, "P2P_LINK_UP_WAIT_TX_DONE", "V2 P2P: Waiting TX done"}, //12 - {RADIO_P2P_LINK_UP_WAIT_ACK, 1, "P2P_LINK_UP_WAIT_ACK", "V2 P2P: Waiting for ACK"}, //13 - {RADIO_P2P_LINK_UP_HB_ACK_REXMT, 0, "P2P_LINK_UP_HB_ACK_REXMT", "V2 P2P: Heartbeat ACK ReXmt"}, //14 + {RADIO_P2P_LINK_UP, 1, "P2P_LINK_UP", "P2P: Receiving Standby"}, //10 + {RADIO_P2P_LINK_UP_WAIT_ACK_DONE, 0, "P2P_LINK_UP_WAIT_ACK_DONE", "P2P: Waiting ACK TX Done"}, //11 + {RADIO_P2P_LINK_UP_WAIT_TX_DONE, 0, "P2P_LINK_UP_WAIT_TX_DONE", "P2P: Waiting TX done"}, //12 + {RADIO_P2P_LINK_UP_WAIT_ACK, 1, "P2P_LINK_UP_WAIT_ACK", "P2P: Waiting for ACK"}, //13 + {RADIO_P2P_LINK_UP_HB_ACK_REXMT, 0, "P2P_LINK_UP_HB_ACK_REXMT", "P2P: Heartbeat ACK ReXmt"}, //14 - //V2 - Multi-Point data exchange + //Multi-Point data exchange // State RX Name Description - {RADIO_MP_BEGIN_SCAN, 0, "MP_BEGIN_SCAN", "V2 MP: Setup for CAD Scanning"}, //15 - {RADIO_MP_SCANNING, 0, "MP_SCANNING", "V2 MP: Scanning for activity"}, //16 - {RADIO_MP_WAIT_TX_PING_DONE, 0, "MP_WAIT_TX_PING_DONE", "V2 MP: Wait for ping to xmit"}, //17 - {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "V2 MP: Wait for ACK to xmit"}, //18 - {RADIO_MP_STANDBY, 1, "MP_STANDBY", "V2 MP: Wait for TX or RX"}, //19 - {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "V2 MP: Waiting for TX done"}, //20 - - //V2 - Multi-Point training client states + {RADIO_MP_BEGIN_SCAN, 0, "MP_BEGIN_SCAN", "MP: Setup for CAD Scanning"}, //15 + {RADIO_MP_SCANNING, 0, "MP_SCANNING", "MP: Scanning for activity"}, //16 + {RADIO_MP_WAIT_TX_PING_DONE, 0, "MP_WAIT_TX_PING_DONE", "MP: Wait for ping to xmit"}, //17 + {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //18 + {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //19 + {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //20 + + //Multi-Point training client states // State RX Name Description - {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, 0, "MP_WAIT_TX_TRAINING_PING_DONE", "V2 MP: Wait TX training PING done"}, //21 - {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, 1, "MP_WAIT_RX_RADIO_PARAMETERS", "V2 MP: Wait for radio parameters"}, //22 - {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, 0, "MP_WAIT_TX_PARAM_ACK_DONE", "V2 MP: Wait for TX param ACK done"}, //23 + {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, 0, "MP_WAIT_TX_TRAINING_PING_DONE", "MP: Wait TX training PING done"}, //21 + {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, 1, "MP_WAIT_RX_RADIO_PARAMETERS", "MP: Wait for radio parameters"}, //22 + {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, 0, "MP_WAIT_TX_PARAM_ACK_DONE", "MP: Wait for TX param ACK done"}, //23 - //V2 - Multi-Point training server states + //Multi-Point training server states // State RX Name Description - {RADIO_MP_WAIT_FOR_TRAINING_PING, 1, "MP_WAIT_FOR_TRAINING_PING", "V2 MP: Wait for training PING"}, //24 - {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, 0, "MP_WAIT_TX_RADIO_PARAMS_DONE", "V2 MP: Wait for TX params done"}, //25 + {RADIO_MP_WAIT_FOR_TRAINING_PING, 1, "MP_WAIT_FOR_TRAINING_PING", "MP: Wait for training PING"}, //24 + {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, 0, "MP_WAIT_TX_RADIO_PARAMS_DONE", "MP: Wait for TX params done"}, //25 - //V2 - Virtual circuit states + //Virtual circuit states // State RX Name Description - {RADIO_VC_WAIT_SERVER, 1, "VC_WAIT_SERVER", "V2 VC: Wait for the server"}, //26 - {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "V2 VC: Wait for TX done"}, //27 - {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "V2 VC: Wait for receive"}, //28 + {RADIO_VC_WAIT_SERVER, 1, "VC_WAIT_SERVER", "VC: Wait for the server"}, //26 + {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "VC: Wait for TX done"}, //27 + {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "VC: Wait for receive"}, //28 }; //Possible types of packets received typedef enum { - //V2 packet types must start at zero - //V2: Point-to-Point training + //Packet types must start at zero + //Point-to-Point training DATAGRAM_P2P_TRAINING_PING = 0, // 0 DATAGRAM_P2P_TRAINING_PARAMS, // 1 - //V2: Link establishment handshake + //Link establishment handshake DATAGRAM_PING, // 2 DATAGRAM_ACK_1, // 3 DATAGRAM_ACK_2, // 4 - //V2: Point-to-Point data exchange + //Point-to-Point data exchange DATAGRAM_DATA, // 5 DATAGRAM_DATA_ACK, // 6 DATAGRAM_HEARTBEAT, // 7 DATAGRAM_REMOTE_COMMAND, // 8 DATAGRAM_REMOTE_COMMAND_RESPONSE, // 9 - //V2: Multi-Point data exchange + //Multi-Point data exchange DATAGRAM_DATAGRAM, //10 - //V2: Multi-Point training exchange + //Multi-Point training exchange DATAGRAM_TRAINING_PING, //11 DATAGRAM_TRAINING_PARAMS, //12 DATAGRAM_TRAINING_ACK, //13 - //V2: Virtual-Circuit (VC) exchange + //Virtual-Circuit (VC) exchange DATAGRAM_VC_HEARTBEAT, //14 - //Add new V2 datagram types before this line - MAX_V2_DATAGRAM_TYPE, + //Add new datagram types before this line + MAX_DATAGRAM_TYPE, //Add new protocol datagrams above this line @@ -146,7 +146,7 @@ typedef enum DATAGRAM_NOT_MINE, } PacketType; -const char * const v2DatagramType[] = +const char * const radioDatagramType[] = { // 0 1 "P2P_TRAINING_PING", "P2P_TRAINING_PARAMS", // 2 3 4 From 164acbada7f308e9c36c0c6e33b5c9ba41114441 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 19:16:45 -1000 Subject: [PATCH 194/594] VC: Save the unique ID to VC address translation in NVM --- Firmware/LoRaSerial_Firmware/Arch_ESP32.h | 3 ++ Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 3 ++ .../LoRaSerial_Firmware.ino | 10 +++++ Firmware/LoRaSerial_Firmware/NVM.ino | 44 +++++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 6 ++- 5 files changed, 65 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h index affe336a..00f96a98 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h +++ b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h @@ -4,6 +4,9 @@ #include #define EEPROM_SIZE 1024 //ESP32 emulates EEPROM in non-volatile storage (external flash IC). Max is 508k. +#define NVM_ERASE_VALUE 0xff +#define NVM_UNIQUE_ID_OFFSET (EEPROM_SIZE - (MAX_VC * UNIQUE_ID_BYTES)) + /* Data flow +--------+ diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 03ab9df1..a2b31f34 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -5,6 +5,9 @@ #include //https://github.com/javos65/WDTZero WDTZero myWatchDog; +#define NVM_ERASE_VALUE 0xff +#define NVM_UNIQUE_ID_OFFSET (EEPROM_EMULATION_SIZE - (MAX_VC * UNIQUE_ID_BYTES)) + /* Data flow +--------------+ diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index c52bfccc..ce7963cd 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -447,6 +447,8 @@ void updateRTS(bool assertRTS); void setup() { + int index; + beginSerial(57600); //Default for debug messages before board begins loadSettings(); //Load settings from EEPROM @@ -462,6 +464,14 @@ void setup() verifyRadioDatagramType(); //Verify that the datagram type table contains all of the datagram types + //Load the unique IDs for the virtual circuits + //Always hand out the same VC number for a given unique ID + if (settings.server) + { + for (index = 0; index < MAX_VC; index++) + nvmLoadVcUniqueId(index); + } + arch.uniqueID(myUniqueId); //Get the unique ID beginBoard(); //Determine what hardware platform we are running on. diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index 5b718367..7087a219 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -82,3 +82,47 @@ void eepromErase() arch.eepromCommit(); } } + +//Copy the unique ID for the VC from NVM into the virtualCircuitList entry +void nvmLoadVcUniqueId(int8_t vc) +{ + uint8_t id[UNIQUE_ID_BYTES]; + int index; + int offset; + + //Read the ID from the flash + offset = NVM_UNIQUE_ID_OFFSET + (vc * UNIQUE_ID_BYTES); + EEPROM.get(offset, id); + + //Verify that the value was saved + for (index = 0; index < sizeof(id); index++) + { + //Determine if this entry was set to a value + if (id[index] != NVM_ERASE_VALUE) + { + //The unique ID was set, copy it into the VC structure + memcpy(virtualCircuitList[vc].uniqueId, id, sizeof(id)); + virtualCircuitList[vc].valid = true; + break; + } + } +} + +//Save the unique ID from the virtualCircuitList entry into the NVM +void nvmSaveVcUniqueId(int8_t vc) +{ + uint8_t id[UNIQUE_ID_BYTES]; + int index; + int offset; + + //Read the ID from the flash + offset = NVM_UNIQUE_ID_OFFSET + (vc * UNIQUE_ID_BYTES); + EEPROM.get(offset, id); + + //Write the ID into the flash + if (memcmp(id, virtualCircuitList[vc].uniqueId, sizeof(id)) != 0) + { + EEPROM.put(offset, virtualCircuitList[vc].uniqueId); + arch.eepromCommit(); + } +} diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d69d5cda..793b5f36 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2461,9 +2461,13 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) return -3; } + //Save the unique ID to VC assignment in NVM + memcpy(&vc->uniqueId, id, UNIQUE_ID_BYTES); + if (settings.server) + nvmSaveVcUniqueId(index); + //Mark this link as up vc->valid = true; - memcpy(&vc->uniqueId, id, UNIQUE_ID_BYTES); return vcLinkUp(index); } From f8335cfa7df4e19ecec694d4313535e4a26a2e87 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 30 Nov 2022 21:42:54 -1000 Subject: [PATCH 195/594] VC: Support broadcast address for datagrams --- Firmware/LoRaSerial_Firmware/Radio.ino | 18 ++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 44 ++++++++++++++++++++++++- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 71ff8872..189a3c77 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1199,6 +1199,24 @@ bool xmitDatagramMpRadioParameters(const uint8_t * clientID) //Virtual Circuit frames //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +bool xmitVcDatagram() +{ + /* + endOfTxData ---. + | + V + +----------+---------+--------+----------+---------+---------+----------+ + | Optional | | | | | | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Data | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | n Bytes | n Bytes | + +----------+---------+--------+----------+---------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_DATAGRAM; + txControl.ackNumber = 0; + return (transmitDatagram()); +} + bool xmitVcHeartbeat(int8_t addr, uint8_t * id) { uint8_t * txData; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 793b5f36..c881552a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1647,6 +1647,21 @@ void updateRadioState() changeState(RADIO_VC_WAIT_TX_DONE); break; + case DATAGRAM_DATAGRAM: + //Move the data into the serial output buffer + if (settings.debugSerial) + { + systemPrint("updateRadioState moving "); + systemPrint(rxDataBytes); + systemPrintln(" bytes from inputBuffer into serialTransmitBuffer"); + outputSerialData(true); + } + systemWrite(START_OF_VC_SERIAL); + serialBufferOutput(rxData, rxDataBytes); + + //Datagrams do NOT get ACKed + break; + case DATAGRAM_DATA_ACK: vcAckTimer = 0; break; @@ -1845,10 +1860,22 @@ void updateRadioState() //No need to add the VC header since the header is in the radioTxBuffer //Get the VC header vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; - channel = (vcHeader->destVc >> VCAB_NUMBER_BITS) & VCAB_CHANNEL_MASK; + if (vcHeader->destVc == VC_BROADCAST) + channel = 0; + else + channel = (vcHeader->destVc >> VCAB_NUMBER_BITS) & VCAB_CHANNEL_MASK; switch (channel) { case 0: //Data packets + //Check for datagram transmission + if (vcHeader->destVc == VC_BROADCAST) + { + //Broadcast this data to all VCs, no ACKs will be received + triggerEvent(TRIGGER_VC_TX_DATA); + xmitVcDatagram(); + break; + } + //Transmit the packet triggerEvent(TRIGGER_VC_TX_DATA); if (xmitDatagramP2PData() == true) @@ -1865,6 +1892,21 @@ void updateRadioState() break; case 1: //Remote command packets + //Remote commands must not be broadcast + if (vcHeader->destVc == VC_BROADCAST) + { + if (settings.debugSerial || settings.debugTransmit) + { + systemPrintln("ERROR: Remote commands may not be broadcast!"); + outputSerialData(true); + } + + //Discard this message + endOfTxData = &outgoingPacket[headerBytes]; + break; + } + + //Determine if this remote command gets processed on the local node if ((vcHeader->destVc & VCAB_NUMBER_MASK) == myVc) { //Copy the command into the command buffer diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 585890e2..118a2e0c 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -315,8 +315,8 @@ struct ControlTrailer responseTrailer; typedef struct _CONTROL_U8 { - uint8_t ackNumber : 2; PacketType datagramType: 4; + uint8_t ackNumber : 2; uint8_t filler : 2; } CONTROL_U8; From 8cd289aa8b0aa565c266ce7d8cb13dc6bbb0f453 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 1 Dec 2022 08:09:25 -1000 Subject: [PATCH 196/594] Add/verify HOP checks at beginning of P2P, MP and VC states Add state documentation Highlight beginning of states --- Firmware/LoRaSerial_Firmware/States.ino | 76 +++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c881552a..3725aabf 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -527,6 +527,15 @@ void updateRadioState() HEARTBEAT 1 ----> Update timestampOffset */ + //==================== + //Wait for the next operation (listed in priority order): + // * Frame received + // * Time to send HEARTBEAT + // * Time to retransmit previous frame + // * Remote command response to send + // * Data to send + // * Link timeout + //==================== case RADIO_P2P_LINK_UP: if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -736,7 +745,9 @@ void updateRadioState() } break; + //==================== //Wait for the ACK or HEARTBEAT to finish transmission + //==================== case RADIO_P2P_LINK_UP_WAIT_ACK_DONE: if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -750,7 +761,9 @@ void updateRadioState() } break; + //==================== //Wait for the data transmission to complete + //==================== case RADIO_P2P_LINK_UP_WAIT_TX_DONE: if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -766,7 +779,9 @@ void updateRadioState() } break; + //==================== //Wait for the ACK to be received + //==================== case RADIO_P2P_LINK_UP_WAIT_ACK: if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -944,6 +959,10 @@ void updateRadioState() breakLink(); break; + //==================== + //Wait for the HEARTBEAT frame to complete transmission then wait for ACK + //and retransmit previous data frame if necessary + //==================== case RADIO_P2P_LINK_UP_HB_ACK_REXMT: if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -1006,6 +1025,9 @@ void updateRadioState() //Multi-Point Data Exchange //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //==================== + //Start searching for other radios + //==================== case RADIO_MP_BEGIN_SCAN: stopChannelTimer(); //Stop hopping @@ -1016,7 +1038,9 @@ void updateRadioState() changeState(RADIO_MP_SCANNING); break; + //==================== //Walk through channel table transmitting a Ping and looking for an Ack + //==================== case RADIO_MP_SCANNING: if (transactionComplete) @@ -1121,7 +1145,9 @@ void updateRadioState() break; + //==================== //Wait for the PING to complete transmission + //==================== case RADIO_MP_WAIT_TX_PING_DONE: if (transactionComplete) { @@ -1131,7 +1157,9 @@ void updateRadioState() } break; + //==================== //Wait for the ACK to complete transmission + //==================== case RADIO_MP_WAIT_TX_ACK_DONE: if (transactionComplete) { @@ -1141,6 +1169,13 @@ void updateRadioState() } break; + //==================== + //Wait for the next operation (listed in priority order): + // * Frame received + // * Data to send + // * Time to send HEARTBEAT + // * Link timeout + //==================== case RADIO_MP_STANDBY: //Hop channels when required if (timeToHop == true) @@ -1299,6 +1334,9 @@ void updateRadioState() break; + //==================== + //Wait for the frame transmission to complete + //==================== case RADIO_MP_WAIT_TX_DONE: //Hop channels when required if (timeToHop == true) @@ -1348,6 +1386,9 @@ void updateRadioState() V */ + //==================== + //Wait for the PING to complete transmission + //==================== case RADIO_MP_WAIT_TX_TRAINING_PING_DONE: updateCylonLEDs(); @@ -1367,6 +1408,9 @@ void updateRadioState() } break; + //==================== + //Wait to receive the radio parameters + //==================== case RADIO_MP_WAIT_RX_RADIO_PARAMETERS: updateCylonLEDs(); @@ -1422,6 +1466,9 @@ void updateRadioState() xmitDatagramMpTrainingPing(); break; + //==================== + //Wait for the ACK frame to complete transmission + //==================== case RADIO_MP_WAIT_TX_PARAM_ACK_DONE: updateCylonLEDs(); @@ -1465,6 +1512,9 @@ void updateRadioState() V */ + //==================== + //Wait for a PING frame from a client + //==================== case RADIO_MP_WAIT_FOR_TRAINING_PING: updateCylonLEDs(); @@ -1517,6 +1567,9 @@ void updateRadioState() } break; + //==================== + //Wait for the radio parameters to complete transmission + //==================== case RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE: updateCylonLEDs(); @@ -1567,6 +1620,9 @@ void updateRadioState() */ + //==================== + //Wait for a HEARTBEAT from the server + //==================== case RADIO_VC_WAIT_SERVER: if (myVc == VC_SERVER) { @@ -1593,7 +1649,14 @@ void updateRadioState() } break; + //==================== + //Wait for the transmission to complete + //==================== case RADIO_VC_WAIT_TX_DONE: + //Hop channels when required + if (timeToHop == true) + hopChannel(); + //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) { @@ -1610,7 +1673,20 @@ void updateRadioState() } break; + //==================== + //Wait for the next operation (listed in priority order): + // * Frame received + // * Time to send HEARTBEAT + // * Time to retransmit previous frame + // * Remote command response to send + // * Data to send + // * Link timeout + //==================== case RADIO_VC_WAIT_RECEIVE: + //Hop channels when required + if (timeToHop == true) + hopChannel(); + //If dio0ISR has fired, a packet has arrived currentMillis = millis(); if (transactionComplete == true) From 5a6cbffb078589b387654c2085a10aec6b561ca8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 1 Dec 2022 14:48:47 -1000 Subject: [PATCH 197/594] Add AT-DebugSync command to modify settings.debugSync --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index bd1a9249..66641676 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -707,6 +707,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &settings.debugRadio}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &settings.debugReceive}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &settings.debugStates}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugSync", &settings.debugSync}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &settings.debugTraining}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &settings.debugSerial}, From 442720468e902bc1acbc0a555c9f0d94defbd732 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 1 Dec 2022 15:01:30 -1000 Subject: [PATCH 198/594] Display the milliseconds until CH0 is reached again Use common routine to change the frequency --- .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 64 +++++++------------ 2 files changed, 24 insertions(+), 41 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index ce7963cd..ce3fff55 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -397,6 +397,7 @@ unsigned long xmitTimeMillis; unsigned long timestampOffset; unsigned long roundTripMillis; unsigned long vcTxHeartbeatMillis; +unsigned long nextChannelZeroTimeInMillis; //Transmit control uint8_t * endOfTxData; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 189a3c77..c67a6c14 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2,23 +2,11 @@ //Called after begin() and once user exits from command interface void configureRadio() { - float frequency; bool success = true; - frequency = channels[0]; - if (radio.setFrequency(frequency) == RADIOLIB_ERR_INVALID_FREQUENCY) - success = false; - - //Print the frequency if requested - if (settings.printFrequency) - { - systemPrintTimestamp(); - systemPrint(frequency); - systemPrintln(" MHz"); - outputSerialData(true); - } - channelNumber = 0; + if (!setRadioFrequency(false)) + success = false; //The SX1276 and RadioLib accepts a value of 2 to 17, with 20 enabling the power amplifier //Measuring actual power output the radio will output 14dBm (25mW) to 27.9dBm (617mW) in constant transmission @@ -184,23 +172,36 @@ void convertAirSpeedToSettings() } //Set radio frequency -void setRadioFrequency(bool rxAdjust) +bool setRadioFrequency(bool rxAdjust) { float frequency; + //Determine the frequency to use frequency = channels[channelNumber]; - if (rxAdjust) + if (rxAdjust && settings.autoTuneFrequency) frequency -= frequencyCorrection; - radio.setFrequency(frequency); + + //Set the new frequency + if (radio.setFrequency(frequency) == RADIOLIB_ERR_INVALID_FREQUENCY) + return false; + //triggerFrequency(frequency); + + //Determine the time in milliseconds when channel zero is reached again + nextChannelZeroTimeInMillis = millis() + ((settings.numberOfChannels - channelNumber) * settings.maxDwellTime); //Print the frequency if requested if (settings.printFrequency) { systemPrintTimestamp(); - systemPrint(frequency); - systemPrintln(" MHz"); + systemPrint(channelNumber); + systemPrint(": "); + systemPrint(frequency, 3); + systemPrint(" MHz, Ch 0 in "); + systemPrint(nextChannelZeroTimeInMillis - millis()); + systemPrintln(" mSec"); outputSerialData(true); } + return true; } void returnToReceiving() @@ -416,28 +417,7 @@ void hopChannel(bool moveForwardThroughTable) } //Select the new frequency - float frequency; - if (settings.autoTuneFrequency == true) - { - if (radioStateTable[radioState].rxState) - frequency = channels[channelNumber] - frequencyCorrection; - else - frequency = channels[channelNumber]; - } - else - frequency = channels[channelNumber]; - - radio.setFrequency(frequency); - //triggerFrequency(frequency); - - //Print the frequency if requested - if (settings.printFrequency) - { - systemPrintTimestamp(); - systemPrint(frequency, 3); - systemPrintln(" MHz"); - outputSerialData(true); - } + setRadioFrequency(radioStateTable[radioState].rxState); } //Returns true if the radio indicates we have an ongoing reception @@ -2634,6 +2614,8 @@ void syncChannelTimer() channelTimer.enableTimer(); triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug + if (settings.debugSync) + outputSerialData(true); } //This function resets the heartbeat time and re-rolls the random time From 5e290345315c79f38a38aa06a3e5acc54dd7d68e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 1 Dec 2022 15:50:47 -1000 Subject: [PATCH 199/594] VC: Randomize the HEARTBEAT intervals Randomize the HEARTBEAT interval until it gets close to channel 0. Choose the last couple of intervals to force a HEARTBEAT in channel 0. HEARTBEATs in channel 0 enable the clients to come online. Add AT-DebugHeartBeat command to display HEARTBEAT timing values --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 64 ++++++++++++++++++++++- Firmware/LoRaSerial_Firmware/settings.h | 1 + 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 66641676..e54d2efa 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -703,6 +703,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &settings.copyDebug}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugHeartbeat", &settings.debugHeartbeat}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugNvm", &settings.debugNvm}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &settings.debugRadio}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &settings.debugReceive}, diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index c67a6c14..b85eb55f 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1089,10 +1089,12 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.copyDebug = params.copyDebug; originalSettings.debug = params.debug; originalSettings.debugDatagrams = params.debugDatagrams; + originalSettings.debugHeartbeat = params.debugHeartbeat; originalSettings.debugNvm = params.debugNvm; originalSettings.debugRadio = params.debugRadio; originalSettings.debugReceive = params.debugReceive; originalSettings.debugStates = params.debugStates; + originalSettings.debugSync = params.debugSync; originalSettings.debugTraining = params.debugTraining; originalSettings.debugTransmit = params.debugTransmit; originalSettings.debugSerial = params.debugSerial; @@ -1233,8 +1235,7 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) vcTxHeartbeatMillis = millis() - currentMillis; //Select a random for the next heartbeat - setHeartbeatLong(); //Those who send a heartbeat or data have long time before next heartbeat. Those who send ACKs, have short wait to next heartbeat. - + setVcHeartbeatTimer(); return (transmitDatagram()); } @@ -2653,3 +2654,62 @@ void setHeartbeatMultipoint() //Add the amount of time it takes to send an ack heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); } + +void setVcHeartbeatTimer() +{ + long deltaMillis; + + /* + * The goal of this routine is to randomize the placement of the HEARTBEAT + * messages, allowing traffic to flow normally. However since clients are + * waiting in channel zero (0) for a HEARTBEAT, the last couple of invervals + * are adjusted for the server to ensure that a HEARTBEAT is sent in channel + * zero. + * + * dwellTime: 400 mSec + * heartbeatTimeout: 3000 mSec + * 50% heartbeatTimeout: 1500 mSec + * + * channel 38 39 40 41 42 43 44 45 46 47 48 49 0 1 2 + * dwellTime | | | | | | | | | | | | | | | + * seconds | . | . | . | . | . | . | + * case 1: > 4.5, Use random, then remaining + * ^--------------^^^^^^^^^^^^^^^^---------------^ + * case 2: 4.5 >= X >= 3, Use half, then second half + * ^^^^^^^^^^^^^^^^-------^^^^^^^^--------------^ + * case 3: X < 3, Use remaining + * ^----------------------------^ + */ + petWDT(); + + //Determine the delay before channel zero is reached + heartbeatTimer = millis(); + deltaMillis = nextChannelZeroTimeInMillis - heartbeatTimer; + if (deltaMillis <= 0) + { + nextChannelZeroTimeInMillis = heartbeatTimer + ((settings.numberOfChannels - channelNumber) * settings.maxDwellTime); + deltaMillis = nextChannelZeroTimeInMillis - heartbeatTimer; + } + + //Determine the delay before the next HEARTBEAT frame + if ((!settings.server) || (deltaMillis > ((3 * settings.heartbeatTimeout) / 2)) + || (deltaMillis <= 0)) + //Use the random interval: 50% - 100% + heartbeatRandomTime = random(settings.heartbeatTimeout / 2, + settings.heartbeatTimeout); + else if (deltaMillis >= settings.heartbeatTimeout) + heartbeatRandomTime = deltaMillis / 2; + else + heartbeatRandomTime = deltaMillis; + + //Display the next HEARTBEAT time interval + if (settings.debugHeartbeat) + { + systemPrint("deltaMillis: "); + systemPrintln(deltaMillis); + systemPrint("heartbeatRandomTime: "); + systemPrintln(heartbeatRandomTime); + outputSerialData(true); + petWDT(); + } +} diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 118a2e0c..83d21a69 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -411,6 +411,7 @@ typedef struct struct_settings { bool debugSync = false; //Print clock sync processing bool debugNvm = false; //Debug NVM operation bool printAckNumbers = false; //Print the ACK numbers + bool debugHeartbeat = false; //Print the HEARTBEAT timing values //Add new parameters immediately before this line //-- Add commands to set the parameters From cbb72f32b817bb0d03a23837028978ee3ce417af Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 1 Dec 2022 15:56:31 -1000 Subject: [PATCH 200/594] VC: Add channel timer to the HEARTBEAT frame --- Firmware/LoRaSerial_Firmware/Radio.ino | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index b85eb55f..5ec5e159 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1201,30 +1201,40 @@ bool xmitVcDatagram() bool xmitVcHeartbeat(int8_t addr, uint8_t * id) { + uint32_t currentMillis = millis(); uint8_t * txData; - uint32_t currentMillis = millis(); + //Build the VC header txData = endOfTxData; *endOfTxData++ = 0; //Reserve for length *endOfTxData++ = VC_BROADCAST; *endOfTxData++ = addr; + + //Add this radio's unique ID memcpy(endOfTxData, id, UNIQUE_ID_BYTES); endOfTxData += UNIQUE_ID_BYTES; + + //Add the current time for timestamp synchronization memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(currentMillis); + //Add the channel timer for HOP synchronization + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); + endOfTxData += sizeof(msToNextHop); + //Set the length field *txData = (uint8_t)(endOfTxData - txData); /* - endOfTxData ---. - | - V - +----------+---------+--------+----------+---------+----------+---------+----------+ - | Optional | | | | | | | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Src ID | millis | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes || n Bytes | - +----------+---------+--------+----------+---------+----------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+--------+----------+---------+----------+---------+----------+----------+ + | Optional | | | | | | | Channel | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Src ID | millis | Timer | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes | 2 bytes || n Bytes | + +----------+---------+--------+----------+---------+----------+---------+----------+----------+ */ txControl.datagramType = DATAGRAM_VC_HEARTBEAT; From 78c681d0181c5225215dadc21ebcca9b1af2fd68 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 07:15:55 -1000 Subject: [PATCH 201/594] VC: Notify the PC of the data ACK --- .../LoRaSerial_Firmware.ino | 8 +++++++ Firmware/LoRaSerial_Firmware/Radio.ino | 14 +++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 6 ----- .../Virtual_Circuit_Protocol.h | 6 +++++ Firmware/Tools/VcServerTest.c | 24 +++++++++++++++++-- 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index ce3fff55..2cfd754e 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -404,6 +404,14 @@ uint8_t * endOfTxData; CONTROL_U8 txControl; uint32_t transmitTimer; +//Retransmit support +uint8_t rmtCmdVc; +uint8_t rexmtBuffer[MAX_PACKET_SIZE]; +CONTROL_U8 rexmtControl; +uint8_t rexmtLength; +uint8_t rexmtFrameSentCount; +uint8_t rexmtTxDestVc; + //Multi-point Training bool trainingServerRunning; //Training server is running bool trainingPreviousRxInProgress = false; //Previous RX status diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 5ec5e159..d9535abb 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1756,6 +1756,20 @@ PacketType rcvDatagram() systemPrintln(vc->txAckNumber); outputSerialData(true); } + + //Notify the PC when an ACK is received for a DATA frame + if ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + && (rexmtControl.datagramType == DATAGRAM_DATA)) + { + //Build the VC header + serialOutputByte(START_OF_VC_SERIAL); + serialOutputByte(sizeof(VC_DATA_ACK_MESSAGE) + VC_RADIO_HEADER_BYTES); //Length + serialOutputByte(PC_DATA_ACK); //Destination + serialOutputByte(myVc); //Source + + //Build the VC_DATA_ACK_MESSAGE + serialOutputByte(rexmtTxDestVc);//Message destination VC + } break; case DATAGRAM_HEARTBEAT: diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3725aabf..116bdc82 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -26,12 +26,6 @@ void updateRadioState() int index; uint16_t length; uint8_t radioSeed; - static uint8_t rmtCmdVc; - static uint8_t rexmtBuffer[MAX_PACKET_SIZE]; - static CONTROL_U8 rexmtControl; - static uint8_t rexmtLength; - static uint8_t rexmtFrameSentCount; - static uint8_t rexmtTxDestVc; bool serverLinkBroken; VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 4752e28f..edda6ba7 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -32,6 +32,7 @@ //Source and destinations reserved for the local host #define PC_COMMAND VC_RSVD_SPECIAL_VCS //Command input and command response #define PC_LINK_STATUS (PC_COMMAND + 1) //Asynchronous link status output +#define PC_DATA_ACK (PC_LINK_STATUS + 1)//Indicate successful delivery of the data //Address space 1 and 2 are reserved for the host PC interface to support remote //command processing. The radio removes these bits and converts them to the @@ -134,6 +135,11 @@ typedef struct _VC_LINK_STATUS_MESSAGE uint8_t linkStatus; //Link status } VC_LINK_STATUS_MESSAGE; +typedef struct _VC_DATA_ACK_MESSAGE +{ + uint8_t msgDestVc; //message destination VC +} VC_DATA_ACK_MESSAGE; + #define LINK_DOWN 0 #define LINK_UP 1 diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 0f400bd8..c443d1a0 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -12,6 +12,7 @@ #define DEBUG_PC_TO_RADIO 0 #define DEBUG_RADIO_TO_PC 0 +#define DISPLAY_DATA_ACK 1 bool findMyVc; int myVc = VC_UNASSIGNED; @@ -243,6 +244,15 @@ void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) printf("--------- Link %d DOWN ---------\n", header->radio.srcVc); } +void radioDataAck(uint8_t * data, uint8_t length) +{ + VC_DATA_ACK_MESSAGE * ack; + + ack = (VC_DATA_ACK_MESSAGE *)data; + if (DISPLAY_DATA_ACK) + printf("ACK from VC %d\n", ack->msgDestVc); +} + int radioToHost() { int bytesRead; @@ -335,13 +345,23 @@ int radioToHost() dumpBuffer(data, length); } + //------------------------------ //Process the message + //------------------------------ + + //Display link status if (header->radio.destVc == PC_LINK_STATUS) radioToPcLinkStatus(header, VC_SERIAL_HEADER_BYTES + length); - if (header->radio.destVc == (PC_REMOTE_RESPONSE | myVc)) - status = hostToStdout(data, length); + //Display remote command response + else if (header->radio.destVc == (PC_REMOTE_RESPONSE | myVc)) + status = hostToStdout(data, length); + + //Display ACKs for transmitted messages + else if (header->radio.destVc == PC_DATA_ACK) + radioDataAck(data, length); + //Display received messages else { if ((header->radio.destVc == myVc) || (header->radio.destVc == VC_BROADCAST)) From 82519e3dbda4a000068217a6c0e5e023343d7ae3 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 07:37:01 -1000 Subject: [PATCH 202/594] VC: Save the time the link came up --- Firmware/LoRaSerial_Firmware/States.ino | 1 + Firmware/LoRaSerial_Firmware/settings.h | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 116bdc82..fce4e0cd 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2494,6 +2494,7 @@ int8_t vcLinkUp(int8_t index) //Send the status message if (!vc->linkUp) { + vc->firstHeartbeatMillis = millis(); vcSendLinkStatus(true, index); //Reset the ACK counters diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 83d21a69..a7530fc1 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -164,7 +164,8 @@ const char * const radioDatagramType[] = typedef struct _VIRTUAL_CIRCUIT { uint8_t uniqueId[UNIQUE_ID_BYTES]; - unsigned long lastHeartbeatMillis; + unsigned long firstHeartbeatMillis; //Time VC link came up + unsigned long lastHeartbeatMillis; //Last time a HEARTBEAT was received, last time link was up //Link quality metrics uint32_t framesSent; //myVc --> VC, Total number of frames sent From c9dff869e8acd99f94ec99708d344d38e155877c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 08:13:36 -1000 Subject: [PATCH 203/594] VC: Display the contents of the VC structure --- Firmware/LoRaSerial_Firmware/Commands.ino | 47 ++++++------ Firmware/LoRaSerial_Firmware/System.ino | 90 ++++++++++++----------- 2 files changed, 74 insertions(+), 63 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index e54d2efa..f3c4a763 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -34,7 +34,7 @@ bool commandAT(const char * commandString) { uint32_t delayMillis; unsigned long timer; - VIRTUAL_CIRCUIT * vc; + VIRTUAL_CIRCUIT * vc = &virtualCircuitList[cmdVc]; //'AT' if (commandLength == 2) @@ -275,13 +275,13 @@ bool commandAT(const char * commandString) systemPrint("VC "); systemPrint(cmdVc); systemPrint(" frames sent: "); - systemPrintln(virtualCircuitList[cmdVc].framesSent); + systemPrintln(vc->framesSent); break; case ('9'): //ATI19 - Display the VC frames received systemPrint("VC "); systemPrint(cmdVc); systemPrint(" frames received: "); - systemPrintln(virtualCircuitList[cmdVc].framesReceived); + systemPrintln(vc->framesReceived); break; } } @@ -295,44 +295,49 @@ bool commandAT(const char * commandString) systemPrint("VC "); systemPrint(cmdVc); systemPrint(" messages sent: "); - systemPrintln(virtualCircuitList[cmdVc].messagesSent); + systemPrintln(vc->messagesSent); break; case ('1'): //ATI21 - Display the VC messages received systemPrint("VC "); systemPrint(cmdVc); systemPrint(" messages received: "); - systemPrintln(virtualCircuitList[cmdVc].messagesReceived); + systemPrintln(vc->messagesReceived); break; case ('2'): //ATI22 - Display the VC bad length received systemPrint("VC "); systemPrint(cmdVc); systemPrint(" bad length received: "); - systemPrintln(virtualCircuitList[cmdVc].badLength); + systemPrintln(vc->badLength); break; case ('3'): //ATI23 - Display the VC link failures systemPrint("VC "); systemPrint(cmdVc); systemPrint(" link failures: "); - systemPrintln(virtualCircuitList[cmdVc].linkFailures); + systemPrintln(vc->linkFailures); break; case ('4'): //ATI24 - Display the VC details - vc = &virtualCircuitList[cmdVc]; systemPrint("VC "); systemPrint(cmdVc); - systemPrint(":"); + systemPrint(": "); if (!vc->valid) - systemPrintln(" Not valid!"); + systemPrintln("Down, Not valid"); else { - petWDT(); - systemPrint(" Link: "); systemPrintln(vc->linkUp ? "Up" : "Down"); - systemPrint(" Unique ID: "); + systemPrint(" ID: "); systemPrintUniqueID(vc->uniqueId); - - systemPrintln(" Metrics:"); - systemPrint(" Link Failures: "); - systemPrintln(vc->linkFailures); + systemPrintln(vc->valid ? " (Valid)" : " (Invalid)"); + systemPrintln(" Heartbeats"); + systemPrint(" Last: "); + systemPrintTimestamp(vc->lastHeartbeatMillis); + systemPrintln(); + systemPrint(" First: "); + systemPrintTimestamp(vc->firstHeartbeatMillis); + systemPrintln(); + systemPrint(" Up Time: "); + systemPrintTimestamp(vc->lastHeartbeatMillis - vc->firstHeartbeatMillis); + systemPrintln(); + systemPrintln(" Metrics"); systemPrint(" Frames Sent: "); systemPrintln(vc->framesSent); systemPrint(" Frames Received: "); @@ -343,17 +348,15 @@ bool commandAT(const char * commandString) systemPrintln(vc->messagesReceived); systemPrint(" Bad Lengths Received: "); systemPrintln(vc->badLength); - - systemPrintln(" ACK Management:"); + systemPrint(" Link Failures: "); + systemPrintln(linkFailures); + systemPrintln(" ACK Management"); systemPrint(" Last RX ACK number: "); systemPrintln(vc->rxAckNumber); systemPrint(" Next RX ACK number: "); systemPrintln(vc->rmtTxAckNumber); systemPrint(" Last TX ACK number: "); systemPrintln(vc->txAckNumber); - - systemPrint(" Last HEARTBEAT millis: "); - systemPrintln(vc->lastHeartbeatMillis); } break; case ('5'): //ATI25 - Display the total insufficient buffer count diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index bc6ead72..47692804 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -117,9 +117,8 @@ void systemPrintln() systemPrint("\r\n"); } -void systemPrintTimestamp() +void systemPrintTimestamp(unsigned int milliseconds) { - unsigned int milliseconds; unsigned int seconds; unsigned int minutes; unsigned int hours; @@ -127,6 +126,53 @@ void systemPrintTimestamp() unsigned int total; petWDT(); + + //Compute the values for display + seconds = milliseconds / 1000; + minutes = seconds / 60; + hours = minutes / 60; + days = hours / 24; + + total = days * 24; + hours -= total; + + total = (total + hours) * 60; + minutes -= total; + + total = (total + minutes) * 60; + seconds -= total; + + total = (total + seconds) * 1000; + milliseconds -= total; + + //Print the days + if (days < 10) systemPrint(" "); + if (days) + systemPrint(days); + else + systemPrint(" "); + systemPrint(" "); + + //Print the time + if (hours < 10) systemPrint(" "); + systemPrint(hours); + systemPrint(":"); + if (minutes < 10) systemPrint("0"); + systemPrint(minutes); + systemPrint(":"); + if (seconds < 10) systemPrint("0"); + systemPrint(seconds); + systemPrint("."); + if (milliseconds < 100) systemPrint("0"); + if (milliseconds < 10) systemPrint("0"); + systemPrint(milliseconds); + petWDT(); +} + +void systemPrintTimestamp() +{ + unsigned int milliseconds; + if (settings.printTimestamp) { //Get the clock value @@ -136,47 +182,9 @@ void systemPrintTimestamp() if (!settings.displayRealMillis) milliseconds += timestampOffset; - //Compute the values for display - seconds = milliseconds / 1000; - minutes = seconds / 60; - hours = minutes / 60; - days = hours / 24; - - total = days * 24; - hours -= total; - - total = (total + hours) * 60; - minutes -= total; - - total = (total + minutes) * 60; - seconds -= total; - - total = (total + seconds) * 1000; - milliseconds -= total; - - //Print the days - if (days < 10) systemPrint(" "); - if (days) - systemPrint(days); - else - systemPrint(" "); - systemPrint(" "); - //Print the time - if (hours < 10) systemPrint(" "); - systemPrint(hours); - systemPrint(":"); - if (minutes < 10) systemPrint("0"); - systemPrint(minutes); - systemPrint(":"); - if (seconds < 10) systemPrint("0"); - systemPrint(seconds); - systemPrint("."); - if (milliseconds < 100) systemPrint("0"); - if (milliseconds < 10) systemPrint("0"); - systemPrint(milliseconds); + systemPrintTimestamp(milliseconds); systemPrint(": "); - petWDT(); } } From 73fdca8098e967a0d1076658fcd0abfaaab79300 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 08:13:36 -1000 Subject: [PATCH 204/594] VC: Display the next HEARTBEAT time in ATI24 command --- Firmware/LoRaSerial_Firmware/Commands.ino | 17 +++++++++++++++-- Firmware/LoRaSerial_Firmware/States.ino | 4 ++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index f3c4a763..892b52f1 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -320,14 +320,27 @@ bool commandAT(const char * commandString) systemPrint(cmdVc); systemPrint(": "); if (!vc->valid) - systemPrintln("Down, Not valid"); + { + systemPrint("Down, Not valid @ "); + systemPrintTimestamp(millis()); + systemPrintln(); + } else { - systemPrintln(vc->linkUp ? "Up" : "Down"); + systemPrint(vc->linkUp ? "Up" : "Down"); + systemPrint(" @ "); + systemPrintTimestamp(millis()); + systemPrintln(); systemPrint(" ID: "); systemPrintUniqueID(vc->uniqueId); systemPrintln(vc->valid ? " (Valid)" : " (Invalid)"); systemPrintln(" Heartbeats"); + if (cmdVc == myVc) + { + systemPrint(" Next TX: "); + systemPrintTimestamp(heartbeatTimer + heartbeatRandomTime); + systemPrintln(); + } systemPrint(" Last: "); systemPrintTimestamp(vc->lastHeartbeatMillis); systemPrintln(); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index fce4e0cd..496d6a9b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1824,7 +1824,11 @@ void updateRadioState() { //Send another heartbeat if (xmitVcHeartbeat(myVc, myUniqueId)) + { + if (((uint8_t)myVc) < MAX_VC) + virtualCircuitList[myVc].lastHeartbeatMillis = millis(); changeState(RADIO_VC_WAIT_TX_DONE); + } } //---------- From 4f19a8d27d8640664ccb0470da6b059f815075d6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 10:30:54 -1000 Subject: [PATCH 205/594] Add command to display the radio metrics --- Firmware/LoRaSerial_Firmware/Commands.ino | 42 +++++++++++++++++++ .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/Radio.ino | 5 +++ 3 files changed, 49 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 892b52f1..c62b861f 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -192,6 +192,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI26 - Display the total number of bad CRC frames"); systemPrintln(" ATI27 - Display the total number of net ID mismatch frames"); systemPrintln(" ATI28 - Return myVc value"); + systemPrintln(" ATI29 - Display metrics"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(0, true); @@ -390,6 +391,47 @@ bool commandAT(const char * commandString) systemPrintln(myVc); reportOK(); break; + case ('9'): //ATI29 - Display metrics + systemPrint("Radio Metrics @ "); + systemPrintTimestamp(millis()); + systemPrintln(); + if (settings.operatingMode == MODE_POINT_TO_POINT) + { + systemPrint(" Link Status: "); + systemPrintln((radioState >= RADIO_P2P_LINK_UP) ? "Up" : "Down"); + systemPrint(" Last RX: "); + systemPrintTimestamp(lastRxDatagram); + systemPrintln(); + systemPrint(" First RX: "); + systemPrintTimestamp(lastLinkUpTime); + systemPrintln(); + systemPrint(" Up Time: "); + systemPrintTimestamp(lastRxDatagram - lastLinkUpTime); + systemPrintln(); + } + systemPrintln(" Sent"); + systemPrint(" Datagrams: "); + systemPrintln(datagramsSent); + systemPrint(" Frames: "); + systemPrintln(framesSent); + systemPrint(" Lost Frames: "); + systemPrintln(lostFrames); + systemPrintln(" Received"); + systemPrint(" Datagrams: "); + systemPrintln(datagramsReceived); + systemPrint(" Frames: "); + systemPrintln(framesReceived); + systemPrint(" Bad CRC: "); + systemPrintln(badCrc); + systemPrint(" Bad Frames: "); + systemPrintln(badFrames); + systemPrint(" Duplicate Frames: "); + systemPrintln(duplicateFrames); + systemPrint(" Insufficient Space: "); + systemPrintln(insufficientSpace); + systemPrint(" Net ID Mismatch: "); + systemPrintln(netIdMismatch); + break; } } diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 2cfd754e..ea9366fa 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -367,6 +367,8 @@ uint32_t badCrc; //Total number of bad CRC frames uint32_t netIdMismatch; //Total number of mismatched Net ID frames unsigned long lastLinkUpTime = 0; //Mark when link was first established +unsigned long lastRxDatagram; //Remember last valid receive + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - Radio Protocol diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index d9535abb..1635b29a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1770,6 +1770,7 @@ PacketType rcvDatagram() //Build the VC_DATA_ACK_MESSAGE serialOutputByte(rexmtTxDestVc);//Message destination VC } + lastRxDatagram = rcvTimeMillis; break; case DATAGRAM_HEARTBEAT: @@ -1778,6 +1779,7 @@ PacketType rcvDatagram() datagramType = validateDatagram(vc, datagramType, ackNumber, rxDataBytes); if (datagramType != DATAGRAM_HEARTBEAT) return datagramType; + lastRxDatagram = rcvTimeMillis; break; case DATAGRAM_REMOTE_COMMAND: @@ -1785,6 +1787,7 @@ PacketType rcvDatagram() - availableRXCommandBytes()); if (datagramType != DATAGRAM_REMOTE_COMMAND) return datagramType; + lastRxDatagram = rcvTimeMillis; break; case DATAGRAM_REMOTE_COMMAND_RESPONSE: @@ -1792,6 +1795,7 @@ PacketType rcvDatagram() - availableTXBytes()); if (datagramType != DATAGRAM_REMOTE_COMMAND_RESPONSE) return datagramType; + lastRxDatagram = rcvTimeMillis; break; case DATAGRAM_DATA: @@ -1799,6 +1803,7 @@ PacketType rcvDatagram() - availableTXBytes()); if (datagramType != DATAGRAM_DATA) return datagramType; + lastRxDatagram = rcvTimeMillis; vc->messagesReceived++; break; } From 29730300e113bc022738c1e065b77232d26f0704 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 10:57:12 -1000 Subject: [PATCH 206/594] Reduce the number of ATI commands --- Firmware/LoRaSerial_Firmware/Commands.ino | 229 +++++++--------------- Firmware/Tools/VcServerTest.c | 2 +- 2 files changed, 70 insertions(+), 161 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index c62b861f..877c7ac0 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -172,27 +172,10 @@ bool commandAT(const char * commandString) systemPrintln(" ATI6 - Display AES key"); systemPrintln(" ATI7 - Show current FHSS channel"); systemPrintln(" ATI8 - Display unique ID"); - systemPrintln(" ATI9 - Display the total datagrams sent"); - systemPrintln(" ATI10 - Display the total datagrams received"); - systemPrintln(" ATI11 - Display the total frames sent"); - systemPrintln(" ATI12 - Display the total frames received"); - systemPrintln(" ATI13 - Display the total bad frames received"); - systemPrintln(" ATI14 - Display the total duplicate frames received"); - systemPrintln(" ATI15 - Display the total lost TX frames"); - systemPrintln(" ATI16 - Display the maximum datagram size"); - systemPrintln(" ATI17 - Display the total link failures"); - systemPrintln(" ATI18 - Display the VC frames sent"); - systemPrintln(" ATI19 - Display the VC frames received"); - systemPrintln(" ATI20 - Display the VC messages sent"); - systemPrintln(" ATI21 - Display the VC messages received"); - systemPrintln(" ATI22 - Display the VC bad length received"); - systemPrintln(" ATI23 - Display the VC link failures"); - systemPrintln(" ATI24 - Display the VC details"); - systemPrintln(" ATI25 - Display the total insufficient buffer count"); - systemPrintln(" ATI26 - Display the total number of bad CRC frames"); - systemPrintln(" ATI27 - Display the total number of net ID mismatch frames"); - systemPrintln(" ATI28 - Return myVc value"); - systemPrintln(" ATI29 - Display metrics"); + systemPrintln(" ATI9 - Display the maximum datagram size"); + systemPrintln(" ATI10 - Display radio metrics"); + systemPrintln(" ATI11 - Return myVc value"); + systemPrintln(" ATI12 - Display the VC details"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(0, true); @@ -228,9 +211,9 @@ bool commandAT(const char * commandString) systemPrintUniqueID(myUniqueId); systemPrintln(); break; - case ('9'): //ATI9 - Display the toal datagrams sent - systemPrint("Total datagrams sent: "); - systemPrintln(datagramsSent); + case ('9'): //ATI9 - Display the maximum datagram size + systemPrint("Maximum datagram size: "); + systemPrintln(maxDatagramSize); break; } } @@ -240,83 +223,66 @@ bool commandAT(const char * commandString) { default: return false; - case ('0'): //ATI10 - Display the total datagrams received - systemPrint("Total datagrams received: "); + case ('0'): //ATI10 - Display radio metrics + systemPrint("Radio Metrics @ "); + systemPrintTimestamp(millis()); + systemPrintln(); + if (settings.operatingMode == MODE_POINT_TO_POINT) + { + systemPrint(" Link Status: "); + systemPrintln((radioState >= RADIO_P2P_LINK_UP) ? "Up" : "Down"); + systemPrint(" Last RX: "); + systemPrintTimestamp(lastRxDatagram); + systemPrintln(); + systemPrint(" First RX: "); + systemPrintTimestamp(lastLinkUpTime); + systemPrintln(); + systemPrint(" Up Time: "); + systemPrintTimestamp(lastRxDatagram - lastLinkUpTime); + systemPrintln(); + } + systemPrintln(" Sent"); + systemPrint(" Datagrams: "); + systemPrintln(datagramsSent); + systemPrint(" Frames: "); + systemPrintln(framesSent); + systemPrint(" Lost Frames: "); + systemPrintln(lostFrames); + systemPrintln(" Received"); + systemPrint(" Datagrams: "); systemPrintln(datagramsReceived); - break; - case ('1'): //ATI11 - Display the total frames received - systemPrint("Total frames sent: "); - systemPrintln(framesReceived); - break; - case ('2'): //ATI12 - Display the total frames received - systemPrint("Total frames sent: "); + systemPrint(" Frames: "); systemPrintln(framesReceived); - break; - case ('3'): //ATI13 - Display the total bad frames received - systemPrint("Total bad frames received: "); + systemPrint(" Bad CRC: "); + systemPrintln(badCrc); + systemPrint(" Bad Frames: "); systemPrintln(badFrames); - break; - case ('4'): //ATI14 - Display the total duplicate frames received - systemPrint("Total duplicate frames received: "); + systemPrint(" Duplicate Frames: "); systemPrintln(duplicateFrames); + systemPrint(" Insufficient Space: "); + systemPrintln(insufficientSpace); + systemPrint(" Net ID Mismatch: "); + systemPrintln(netIdMismatch); + if (settings.operatingMode == MODE_POINT_TO_POINT) + { + vc = &virtualCircuitList[0]; + systemPrintln(" ACK Management"); + systemPrint(" Last RX ACK number: "); + systemPrintln(vc->rxAckNumber); + systemPrint(" Next RX ACK number: "); + systemPrintln(vc->rmtTxAckNumber); + systemPrint(" Last TX ACK number: "); + systemPrintln(vc->txAckNumber); + } + reportOK(); break; - case ('5'): //ATI15 - Display the total lost TX frames - systemPrint("Total lost TX frames: "); - systemPrintln(lostFrames); - break; - case ('6'): //ATI16 - Display the maximum datagram size - systemPrint("Maximum datagram size: "); - systemPrintln(maxDatagramSize); - break; - case ('7'): //ATI17 - Display the total link failures - systemPrint("Total link failures: "); - systemPrintln(linkFailures); - break; - case ('8'): //ATI18 - Display the VC frames sent - systemPrint("VC "); - systemPrint(cmdVc); - systemPrint(" frames sent: "); - systemPrintln(vc->framesSent); - break; - case ('9'): //ATI19 - Display the VC frames received - systemPrint("VC "); - systemPrint(cmdVc); - systemPrint(" frames received: "); - systemPrintln(vc->framesReceived); - break; - } - } - else if ((commandString[2] == 'I') && (commandString[3] == '2') && (commandLength == 5)) - { - switch (commandString[4]) - { - default: - return false; - case ('0'): //ATI20 - Display the VC messages sent - systemPrint("VC "); - systemPrint(cmdVc); - systemPrint(" messages sent: "); - systemPrintln(vc->messagesSent); - break; - case ('1'): //ATI21 - Display the VC messages received - systemPrint("VC "); - systemPrint(cmdVc); - systemPrint(" messages received: "); - systemPrintln(vc->messagesReceived); - break; - case ('2'): //ATI22 - Display the VC bad length received - systemPrint("VC "); - systemPrint(cmdVc); - systemPrint(" bad length received: "); - systemPrintln(vc->badLength); - break; - case ('3'): //ATI23 - Display the VC link failures - systemPrint("VC "); - systemPrint(cmdVc); - systemPrint(" link failures: "); - systemPrintln(vc->linkFailures); + case ('1'): //ATI11 - Return myVc value + systemPrintln(); + systemPrint("myVc: "); + systemPrintln(myVc); + reportOK(); break; - case ('4'): //ATI24 - Display the VC details + case ('2'): //ATI12 - Display the VC details systemPrint("VC "); systemPrint(cmdVc); systemPrint(": "); @@ -351,16 +317,17 @@ bool commandAT(const char * commandString) systemPrint(" Up Time: "); systemPrintTimestamp(vc->lastHeartbeatMillis - vc->firstHeartbeatMillis); systemPrintln(); - systemPrintln(" Metrics"); - systemPrint(" Frames Sent: "); + systemPrintln(" Sent"); + systemPrint(" Frames: "); systemPrintln(vc->framesSent); - systemPrint(" Frames Received: "); - systemPrintln(vc->framesReceived); - systemPrint(" Messages Sent: "); + systemPrint(" Messages: "); systemPrintln(vc->messagesSent); - systemPrint(" Messages Received: "); + systemPrintln(" Received"); + systemPrint(" Frames: "); + systemPrintln(vc->framesReceived); + systemPrint(" Messages: "); systemPrintln(vc->messagesReceived); - systemPrint(" Bad Lengths Received: "); + systemPrint(" Bad Lengths: "); systemPrintln(vc->badLength); systemPrint(" Link Failures: "); systemPrintln(linkFailures); @@ -372,66 +339,8 @@ bool commandAT(const char * commandString) systemPrint(" Last TX ACK number: "); systemPrintln(vc->txAckNumber); } - break; - case ('5'): //ATI25 - Display the total insufficient buffer count - systemPrint("Total insufficient buffer count: "); - systemPrintln(insufficientSpace); - break; - case ('6'): //ATI26 - Display the total number of bad CRC frames - systemPrint("Total number of bad CRC frames: "); - systemPrintln(badCrc); - break; - case ('7'): //ATI27 - Display the total number of net ID mismatch frames - systemPrint("Total number of net ID mismatch frames: "); - systemPrintln(netIdMismatch); - break; - case ('8'): //ATI28 - Return myVc value - systemPrintln(); - systemPrint("myVc: "); - systemPrintln(myVc); reportOK(); break; - case ('9'): //ATI29 - Display metrics - systemPrint("Radio Metrics @ "); - systemPrintTimestamp(millis()); - systemPrintln(); - if (settings.operatingMode == MODE_POINT_TO_POINT) - { - systemPrint(" Link Status: "); - systemPrintln((radioState >= RADIO_P2P_LINK_UP) ? "Up" : "Down"); - systemPrint(" Last RX: "); - systemPrintTimestamp(lastRxDatagram); - systemPrintln(); - systemPrint(" First RX: "); - systemPrintTimestamp(lastLinkUpTime); - systemPrintln(); - systemPrint(" Up Time: "); - systemPrintTimestamp(lastRxDatagram - lastLinkUpTime); - systemPrintln(); - } - systemPrintln(" Sent"); - systemPrint(" Datagrams: "); - systemPrintln(datagramsSent); - systemPrint(" Frames: "); - systemPrintln(framesSent); - systemPrint(" Lost Frames: "); - systemPrintln(lostFrames); - systemPrintln(" Received"); - systemPrint(" Datagrams: "); - systemPrintln(datagramsReceived); - systemPrint(" Frames: "); - systemPrintln(framesReceived); - systemPrint(" Bad CRC: "); - systemPrintln(badCrc); - systemPrint(" Bad Frames: "); - systemPrintln(badFrames); - systemPrint(" Duplicate Frames: "); - systemPrintln(duplicateFrames); - systemPrint(" Insufficient Space: "); - systemPrintln(insufficientSpace); - systemPrint(" Net ID Mismatch: "); - systemPrintln(netIdMismatch); - break; } } diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index c443d1a0..b6afddba 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -6,7 +6,7 @@ #define STDOUT 1 #define BREAK_LINKS_COMMAND "atb" -#define GET_MY_VC_ADDRESS "atI28" +#define GET_MY_VC_ADDRESS "atI11" #define LINK_RESET_COMMAND "atz" #define MY_VC_ADDRESS "myVc: " From 109afe59107cdc74e5b8dba085865712b5150754 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 11:16:36 -1000 Subject: [PATCH 207/594] Redefine ATI6 command to display current state Previous command was duplicate of AT-EncryptionKey? --- Firmware/LoRaSerial_Firmware/Commands.ino | 6 ++---- Firmware/LoRaSerial_Firmware/States.ino | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 877c7ac0..9b08202c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -199,10 +199,8 @@ bool commandAT(const char * commandString) case ('5'): //ATI5 - Show max possible bytes per second systemPrintln(calcMaxThroughput()); break; - case ('6'): //ATI6 - Display AES key - for (uint8_t i = 0 ; i < 16 ; i++) - systemPrint(settings.encryptionKey[i], HEX); - systemPrintln(); + case ('6'): //ATI6 - Display currentState + displayState(radioState); break; case ('7'): //ATI7 - Show current FHSS channel systemPrintln(channelNumber); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 496d6a9b..58a6cb5f 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2309,6 +2309,12 @@ void changeState(RadioStates newState) if ((settings.debug == false) && (settings.debugStates == false)) return; + displayState(newState); + outputSerialData(true); +} + +void displayState(RadioStates newState) +{ //Debug print if (settings.printTimestamp) systemPrintTimestamp(); @@ -2356,7 +2362,6 @@ void changeState(RadioStates newState) } systemPrintln(); - outputSerialData(true); } void breakLink() From 29de8b76ffdf41a097b19eddc182a63802cebdb8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 11:59:35 -1000 Subject: [PATCH 208/594] Rename alternateLedUsage to selectLedUse and change it to uint8_t --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- .../LoRaSerial_Firmware.ino | 5 +-- Firmware/LoRaSerial_Firmware/Radio.ino | 6 +-- Firmware/LoRaSerial_Firmware/System.ino | 45 +++++++++++-------- Firmware/LoRaSerial_Firmware/Train.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 11 ++++- 6 files changed, 43 insertions(+), 28 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9b08202c..5f8b257b 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -664,7 +664,6 @@ const COMMAND_ENTRY commands[] = { /*Debug parameters Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "AlternateLedUsage", &settings.alternateLedUsage}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &settings.copyDebug}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, @@ -686,6 +685,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &settings.printRfData}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, + {'D', 1, 0, 255, 0, TYPE_U8, valInt, "SelectLedUse", &settings.selectLedUse}, /*Radio parameters Ltr, All, min, max, digits, type, validation, name, setting addr */ diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index ea9366fa..7788f771 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -512,11 +512,10 @@ void loop() updateSerial(); //Store incoming and print outgoing - if (settings.alternateLedUsage) - updateLeds(); - updateRadioState(); //Ping/ack/send packets as needed + updateLeds(); //Update the LEDs on the board + if (hop) //If the hop ISR has triggered, measure RSSI during reception { hop = false; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 1635b29a..861522fe 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1085,7 +1085,6 @@ void updateRadioParameters(uint8_t * rxData) if (params.copyDebug) { originalSettings.debug = params.debug; - originalSettings.alternateLedUsage = params.alternateLedUsage; originalSettings.copyDebug = params.copyDebug; originalSettings.debug = params.debug; originalSettings.debugDatagrams = params.debugDatagrams; @@ -1107,6 +1106,7 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.printRfData = params.printRfData; originalSettings.printTimestamp = params.printTimestamp; originalSettings.printTxErrors = params.printTxErrors; + originalSettings.selectLedUse = params.selectLedUse; } //Update the serial parameters @@ -1888,7 +1888,7 @@ PacketType rcvDatagram() linkDownTimer = millis(); //Blink the RX LED - if (settings.alternateLedUsage) + if (settings.selectLedUse == LEDS_RADIO_USE) digitalWrite(ALT_LED_RX_DATA, LED_ON); return datagramType; } @@ -2472,7 +2472,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime); //Wait this number of ms between retransmits. Increases with each re-transmit. //BLink the RX LED - if (settings.alternateLedUsage) + if (settings.selectLedUse == LEDS_RADIO_USE) digitalWrite(ALT_LED_TX_DATA, LED_ON); return (true); //Tranmission has started diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 47692804..ae8396d3 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -542,19 +542,6 @@ void updateRSSI() else rssi = radio.getRSSI(); - if (settings.alternateLedUsage) - return; - - //Set LEDs according to RSSI level - if (rssi > rssiLevelLow) - setRSSI(0b0001); - if (rssi > rssiLevelMed) - setRSSI(0b0011); - if (rssi > rssiLevelHigh) - setRSSI(0b0111); - if (rssi > rssiLevelMax) - setRSSI(0b1111); - if (hopCount > 0) { //Reset RSSI measurements @@ -565,9 +552,6 @@ void updateRSSI() void setRSSI(uint8_t ledBits) { - if (settings.alternateLedUsage) - return; - if (ledBits & 0b0001) digitalWrite(pin_rssi1LED, HIGH); else @@ -591,7 +575,7 @@ void setRSSI(uint8_t ledBits) void txLED(bool illuminate) { - if (settings.alternateLedUsage) + if (settings.selectLedUse != LEDS_RSSI) return; if (pin_txLED != PIN_UNDEFINED) { @@ -604,7 +588,7 @@ void txLED(bool illuminate) void rxLED(bool illuminate) { - if (settings.alternateLedUsage) + if (settings.selectLedUse != LEDS_RSSI) return; if (pin_rxLED != PIN_UNDEFINED) { @@ -615,7 +599,7 @@ void rxLED(bool illuminate) } } -void updateLeds() +void radioLeds() { uint32_t currentMillis; static uint32_t previousMillis; @@ -706,6 +690,29 @@ void updateLeds() previousMillis = currentMillis; } +void updateLeds() +{ + switch (settings.selectLedUse) + { + //Set LEDs according to RSSI level + default: + case 0: + if (rssi > rssiLevelLow) + setRSSI(0b0001); + if (rssi > rssiLevelMed) + setRSSI(0b0011); + if (rssi > rssiLevelHigh) + setRSSI(0b0111); + if (rssi > rssiLevelMax) + setRSSI(0b1111); + break; + + case 1: + radioLeds(); + break; + } +} + int stricmp(const char * str1, const char * str2) { char char1; diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index f579e4bd..edd2a509 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -151,7 +151,7 @@ void commonTrainingInitialization() //Debug training if requested if (originalSettings.debugTraining) { - settings.alternateLedUsage = originalSettings.alternateLedUsage; + settings.selectLedUse = originalSettings.selectLedUse; //Ignore copyDebug settings.debug = originalSettings.debug; settings.debugDatagrams = originalSettings.debugDatagrams; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index a7530fc1..5cd80559 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -336,6 +336,15 @@ typedef struct _COMMAND_ENTRY void * setting; } COMMAND_ENTRY; +typedef enum +{ + LEDS_RSSI = 0, //Green: RSSI, Blue: Serial TX, Yellow: Serial RX + LEDS_RADIO_USE, //Green1: RX, Green2: Link, Green3: RSSI, Green4: TX + //Blue: Bad frames, Yellow: Bad CRC + + //Add user LED types from 255 working down +} LEDS_USE_TYPE; + //These are all the settings that can be set on Serial Terminal Radio. It's recorded to NVM. typedef struct struct_settings { uint16_t sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int @@ -406,7 +415,7 @@ typedef struct struct_settings { bool printLinkUpDown = false; //Print the link up and link down messages bool invertCts = false; //Invert the input of CTS bool invertRts = false; //Invert the output of RTS - bool alternateLedUsage = false; //Enable alternate LED usage + bool selectLedUse = 0; //Select LED use uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training bool debugSerial = false; //Debug the serial input bool debugSync = false; //Print clock sync processing From 26dec60e06ab1afda2dcf6e716366de0a9db9ed5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 15:51:30 -1000 Subject: [PATCH 209/594] Remove more of the V1 radio protocol --- Firmware/LoRaSerial_Firmware/settings.h | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 5cd80559..c3490e66 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -302,18 +302,6 @@ typedef enum MODE_VIRTUAL_CIRCUIT, } OPERATING_MODE; -struct ControlTrailer -{ - uint8_t resend : 1; - uint8_t ack : 1; - uint8_t remoteCommand : 1; - uint8_t remoteCommandResponse : 1; - uint8_t train : 1; - uint8_t filler : 3; -}; -struct ControlTrailer receiveTrailer; -struct ControlTrailer responseTrailer; - typedef struct _CONTROL_U8 { PacketType datagramType: 4; From e95ca85e230e6ef599b4461a3893aa115c0f9324 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 18:15:15 -1000 Subject: [PATCH 210/594] Add missing routine comments --- Firmware/LoRaSerial_Firmware/Arch_ESP32.h | 13 ++++++ Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 13 ++++++ Firmware/LoRaSerial_Firmware/Begin.ino | 6 +++ Firmware/LoRaSerial_Firmware/Commands.ino | 18 ++++++++ .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/NVM.ino | 2 + Firmware/LoRaSerial_Firmware/Radio.ino | 17 +++++++- Firmware/LoRaSerial_Firmware/Serial.ino | 8 ++++ Firmware/LoRaSerial_Firmware/States.ino | 11 ++++- Firmware/LoRaSerial_Firmware/System.ino | 43 +++++++++++++++++++ Firmware/LoRaSerial_Firmware/Train.ino | 2 + 11 files changed, 133 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h index 00f96a98..c81b8dcc 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h +++ b/Firmware/LoRaSerial_Firmware/Arch_ESP32.h @@ -26,6 +26,7 @@ +--------+ */ +//Initialize the LoRaSerial board void esp32BeginBoard() { //Lower power boards @@ -45,6 +46,7 @@ void esp32BeginBoard() strcpy(platformPrefix, "ESP32 100mW"); } +//Initialize the USB serial port void esp32BeginSerial(uint16_t serialSpeed) { if (settings.usbSerialWait) @@ -52,16 +54,19 @@ void esp32BeginSerial(uint16_t serialSpeed) delay(500); } +//Initialize the watch dog timer void esp32BeginWDT() { petTimeout = 1000 / 2; } +//Initilaize the EEPROM controller or simulation void esp32EepromBegin() { EEPROM.begin(EEPROM_SIZE); } +//Write any remaining data to EEPROM void esp32EepromCommit() { EEPROM.commit(); @@ -73,42 +78,50 @@ void esp32PetWDT() delay(1); } +//Initialize the radio module Module * esp32Radio() { return new Module(pin_cs, pin_dio0, pin_rst, pin_dio1); } +//Determine if serial input data is available bool esp32SerialAvailable() { return Serial.available(); } +//Ensure that all serial output data has been sent over USB void esp32SerialFlush() { Serial.flush(); } +//Read in the serial input data uint8_t esp32SerialRead() { return (Serial.read()); } +//Provide the serial output data to the USB layer or the UART TX FIFO void esp32SerialWrite(uint8_t value) { Serial.write(value); } +//Reset the CPU void esp32SystemReset() { ESP.restart(); } +//Get the CPU's unique ID value void esp32UniqueID(uint8_t * unique128_BitID) { memset(unique128_BitID, 0, UNIQUE_ID_BYTES); esp_read_mac(unique128_BitID, ESP_MAC_WIFI_STA); } +//Provide the hardware abstraction layer (HAL) interface const ARCH_TABLE arch = { esp32BeginBoard, //beginBoard esp32BeginSerial, //beginSerial diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index a2b31f34..960008be 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -63,6 +63,7 @@ WDTZero myWatchDog; +--------------+ */ +//Initialize the LoRaSerial board void samdBeginBoard() { //Use ADC to check resistor divider @@ -135,6 +136,7 @@ void samdBeginBoard() } } +//Initialize the USB serial port and the UART void samdBeginSerial(uint16_t serialSpeed) { if (settings.usbSerialWait) @@ -144,16 +146,19 @@ void samdBeginSerial(uint16_t serialSpeed) Serial1.begin(serialSpeed); } +//Initialize the watch dog timer void samdBeginWDT() { myWatchDog.setup(WDT_HARDCYCLE2S); // Initialize WDT with 2s timeout petTimeout = 1800; } +//Initilaize the EEPROM controller or simulation void samdEepromBegin() { } +//Write any remaining data to EEPROM void samdEepromCommit() { EEPROM.commit(); @@ -166,22 +171,26 @@ void samdPetWDT() myWatchDog.clear(); } +//Initialize the radio module Module * samdRadio() { return new Module(pin_cs, pin_dio0, pin_rst, pin_dio1); } +//Determine if serial input data is available bool samdSerialAvailable() { return (Serial.available() || Serial1.available()); } +//Ensure that all serial output data has been sent over USB and via the UART void samdSerialFlush() { Serial.flush(); Serial1.flush(); } +//Read in the serial input data uint8_t samdSerialRead() { byte incoming = 0; @@ -192,17 +201,20 @@ uint8_t samdSerialRead() return (incoming); } +//Provide the serial output data to the USB layer or the UART TX FIFO void samdSerialWrite(uint8_t value) { Serial.write(value); Serial1.write(value); } +//Reset the CPU void samdSystemReset() { NVIC_SystemReset(); } +//Get the CPU's unique ID value void samdUniqueID(uint8_t * unique128_BitID) { uint32_t id[UNIQUE_ID_BYTES/4]; @@ -216,6 +228,7 @@ void samdUniqueID(uint8_t * unique128_BitID) memcpy(unique128_BitID, id, UNIQUE_ID_BYTES); } +//Provide the hardware abstraction layer (HAL) interface const ARCH_TABLE arch = { samdBeginBoard, //beginBoard samdBeginSerial, //beginSerial diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 00d26d52..46d1b6a9 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -26,6 +26,7 @@ void beginBoard() } +//Initialize the radio layer void beginLoRa() { radio = arch.radio(); @@ -48,6 +49,7 @@ void beginLoRa() changeState(RADIO_RESET); } +//Initialize the button driver void beginButton() { if (pin_trainButton != PIN_UNDEFINED) @@ -68,12 +70,14 @@ void delayWDT(uint16_t delayAmount) } } +//Initialize the serial drivers void beginSerial(uint16_t serialSpeed) { Serial.begin(serialSpeed); arch.beginSerial(serialSpeed); } +//Ensure the watch dog timer does not fire which would cause a CPU hardware reset void petWDT() { //Petting the dog takes a long time (~4.5ms on SAMD21) so it's only done after we've passed the timeout @@ -84,6 +88,8 @@ void petWDT() } } +//Start the timer measuring the dwell interval and indicating that it is time to +//hop channels void beginChannelTimer() { if (channelTimer.attachInterruptInterval_MS(settings.maxDwellTime, channelTimerHandler) == false) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 5f8b257b..d461c94c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -30,6 +30,7 @@ typedef struct // Command prefix routines //---------------------------------------- +//Process the AT commands bool commandAT(const char * commandString) { uint32_t delayMillis; @@ -458,11 +459,13 @@ void checkCommand() commandLength = 0; //Get ready for next command } +//Indicate successful command completion void reportOK() { systemPrintln("OK"); } +//Indicate command failure void reportERROR() { systemPrintln("ERROR"); @@ -481,36 +484,42 @@ char * trimCommand() return commandString; } +//Display all of the commands bool commandDisplayAll(const char * commandString) { displayParameters(0, false); return true; } +//Display only the debugging commands bool commandDisplayDebug(const char * commandString) { displayParameters('D', false); return true; } +//Display only the trigger probe commands bool commandDisplayProbe(const char * commandString) { displayParameters('P', false); return true; } +//Display only the radio commands bool commandDisplayRadio(const char * commandString) { displayParameters('R', false); return true; } +//Display only the serial commands bool commandDisplaySerial(const char * commandString) { displayParameters('S', false); return true; } +//Display only the virtual circuit commands bool commandDisplayVirtualCircuit(const char * commandString) { displayParameters('V', false); @@ -521,6 +530,7 @@ bool commandDisplayVirtualCircuit(const char * commandString) // Data validation routines //---------------------------------------- +//Validate a bandwidth value bool valBandwidth (void * value, uint32_t valMin, uint32_t valMax) { double doubleSettingValue = *(double *)value; @@ -547,6 +557,7 @@ bool valBandwidth (void * value, uint32_t valMin, uint32_t valMax) || (doubleSettingValue == 500.0)); } +//Validate a maximum frequency value bool valFreqMax (void * value, uint32_t valMin, uint32_t valMax) { double doubleSettingValue = *(double *)value; @@ -556,6 +567,7 @@ bool valFreqMax (void * value, uint32_t valMin, uint32_t valMax) return ((doubleSettingValue >= settings.frequencyMin) && (doubleSettingValue <= (double)valMax)); } +//Validate a minimum frequency value bool valFreqMin (void * value, uint32_t valMin, uint32_t valMax) { double doubleSettingValue = *(double *)value; @@ -565,6 +577,7 @@ bool valFreqMin (void * value, uint32_t valMin, uint32_t valMax) return ((doubleSettingValue >= (double)valMin) && (doubleSettingValue <= settings.frequencyMax)); } +//Validate an integer value bool valInt (void * value, uint32_t valMin, uint32_t valMax) { uint32_t settingValue = *(uint32_t *)value; @@ -572,6 +585,7 @@ bool valInt (void * value, uint32_t valMin, uint32_t valMax) return ((settingValue >= valMin) && (settingValue <= valMax)); } +//Validate an encryption key value bool valKey (void * value, uint32_t valMin, uint32_t valMax) { unsigned int length; @@ -602,6 +616,7 @@ bool valKey (void * value, uint32_t valMin, uint32_t valMax) return false; } +//Determine if the AirSpeed value is overriding the parameter value bool valOverride (void * value, uint32_t valMin, uint32_t valMax) { uint32_t settingValue = *(uint32_t *)value; @@ -615,6 +630,7 @@ bool valOverride (void * value, uint32_t valMin, uint32_t valMax) return ((settingValue >= valMin) && (settingValue <= valMax)); } +//Validate the AirSpeed value bool valSpeedAir (void * value, uint32_t valMin, uint32_t valMax) { bool valid; @@ -639,6 +655,7 @@ bool valSpeedAir (void * value, uint32_t valMin, uint32_t valMax) return valid; } +//Validate the SerialSpeed value bool valSpeedSerial (void * value, uint32_t valMin, uint32_t valMax) { uint32_t settingValue = *(uint32_t *)value; @@ -753,6 +770,7 @@ const int commandCount = sizeof(commands) / sizeof(commands[0]); // ATSxx routines //---------------------------------------- +//Display a command void commandDisplay(const COMMAND_ENTRY * command) { //Print the setting value diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 7788f771..20b99ba6 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -456,6 +456,7 @@ void updateRTS(bool assertRTS); #include "Arch_SAMD.h" //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Initial entrypoint following any runtime library initialization void setup() { int index; @@ -504,6 +505,7 @@ void setup() triggerEvent(TRIGGER_RADIO_RESET); } +//Idle loop for the CPU void loop() { petWDT(); diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index 7087a219..4ed28f1e 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -1,3 +1,4 @@ +//Read the settings from NVM into the settings structure void loadSettings() { arch.eepromBegin(); @@ -66,6 +67,7 @@ void recordSystemSettings() arch.eepromCommit(); } +//Erase the EEPROM void eepromErase() { if (settings.debugNvm) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 861522fe..7371a8a9 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -204,6 +204,7 @@ bool setRadioFrequency(bool rxAdjust) return true; } +//Place the radio in receive mode void returnToReceiving() { if (receiveInProcess() == true) return; //Do not touch the radio if it is already receiving @@ -389,17 +390,18 @@ uint16_t myRand() //Move to the next channel //This is called when the FHSS interrupt is received //at the beginning and during of a transmission or reception - void hopChannel() { hopChannel(true); //Move forward } +//Hop to the previous channel in the frequency list void hopChannelReverse() { hopChannel(false); //Move backward } +//Set the next radio frequency given the hop direction and frequency table void hopChannel(bool moveForwardThroughTable) { timeToHop = false; @@ -477,6 +479,7 @@ uint8_t covertdBmToSetting(uint8_t userSetting) } #ifdef RADIOLIB_LOW_LEVEL +//Read a register from the SX1276 chip uint8_t readSX1276Register(uint8_t reg) { return radio._mod->SPIreadRegister(reg); @@ -1045,6 +1048,8 @@ bool xmitDatagramMpTrainingAck(uint8_t * serverID) return (transmitDatagram()); } +//Copy the training parameters received from the server into the settings structure +//that will eventually be written into the NVM void updateRadioParameters(uint8_t * rxData) { Settings params; @@ -1181,6 +1186,7 @@ bool xmitDatagramMpRadioParameters(const uint8_t * clientID) //Virtual Circuit frames //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Broadcast a datagram to all of the VCs bool xmitVcDatagram() { /* @@ -1199,6 +1205,7 @@ bool xmitVcDatagram() return (transmitDatagram()); } +//Broadcast a HEARTBEAT to all of the VCs bool xmitVcHeartbeat(int8_t addr, uint8_t * id) { uint32_t currentMillis = millis(); @@ -1893,6 +1900,10 @@ PacketType rcvDatagram() return datagramType; } +//Determine what PacketType value should be returned to the receiving code, options are: +// * Received datagramType +// * DATAGRAM_DUPLICATE +// * DATAGRAM_BAD PacketType validateDatagram(VIRTUAL_CIRCUIT * vc, PacketType datagramType, uint8_t ackNumber, uint16_t freeBytes) { if (ackNumber != vc->rmtTxAckNumber) @@ -2478,11 +2489,13 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) return (true); //Tranmission has started } +//Use the maximum dwell setting to start the timer that indicates when to hop channels void startChannelTimer() { startChannelTimer(settings.maxDwellTime); } +//Use the specified value to start the timer that indicates when to hop channels void startChannelTimer(int16_t startAmount) { channelTimer.disableTimer(); @@ -2492,6 +2505,7 @@ void startChannelTimer(int16_t startAmount) triggerEvent(TRIGGER_HOP_TIMER_START); } +//Stop the channel (hop) timer void stopChannelTimer() { channelTimer.disableTimer(); @@ -2684,6 +2698,7 @@ void setHeartbeatMultipoint() heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); } +//Determine the delay for the next VC HEARTBEAT void setVcHeartbeatTimer() { long deltaMillis; diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 1841b350..549fcbe6 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -309,6 +309,7 @@ void updateSerial() } } +//Process serial input for point-to-point and multi-point modes void processSerialInput() { uint16_t radioHead; @@ -419,6 +420,7 @@ void processSerialInput() } } +//Move the serial data from serialTransmitBuffer to the USB or serial port void outputSerialData(bool ignoreISR) { int dataBytes; @@ -604,6 +606,7 @@ bool vcSerialMessageReceived() return false; } +//Process serial input when running in MODE_VIRTUAL_CIRCUIT void vcProcessSerialInput() { char * cmd; @@ -792,6 +795,11 @@ void vcProcessSerialInput() if (timeToHop == true) hopChannel(); } +//Display any serial data for output, discard: +// * Serial input data +// * Radio transmit data +// * Received remote command data +// * Remote command response data void resetSerial() { uint32_t delayTime; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 58a6cb5f..756ff95d 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -14,6 +14,7 @@ frameSentCount = rexmtFrameSentCount; \ } +//Process the radio states void updateRadioState() { int8_t addressByte; @@ -2291,7 +2292,7 @@ void verifyRadioStateTable() } } -//Verify the datagram type table +//Verify the PacketType enums against the radioDatagramType void verifyRadioDatagramType() { if ((sizeof(radioDatagramType) / sizeof(radioDatagramType[0])) != MAX_DATAGRAM_TYPE) @@ -2313,6 +2314,7 @@ void changeState(RadioStates newState) outputSerialData(true); } +//Display the state transition void displayState(RadioStates newState) { //Debug print @@ -2364,6 +2366,7 @@ void displayState(RadioStates newState) systemPrintln(); } +//Break a point-to-point link void breakLink() { //Break the link @@ -2383,6 +2386,7 @@ void breakLink() changeState(RADIO_RESET); } +//Point-to-point link is now up, following a 3-way handshake void enterLinkUp() { VIRTUAL_CIRCUIT * vc; @@ -2417,6 +2421,7 @@ void enterLinkUp() } } +//Empty the remote command receive buffer void discardPreviousData() { //Output any debug messages @@ -2429,6 +2434,7 @@ void discardPreviousData() commandTXTail = commandTXHead; } +//Output VC link status void vcSendLinkStatus(bool linkUp, int8_t srcVc) { //Build the message @@ -2496,6 +2502,7 @@ void vcBreakLink(int8_t vcIndex) resetSerial(); } +//Place VC in LINK-UP state since it is receiving HEARTBEATs from the remote radio int8_t vcLinkUp(int8_t index) { VIRTUAL_CIRCUIT * vc = &virtualCircuitList[index]; @@ -2518,6 +2525,7 @@ int8_t vcLinkUp(int8_t index) return index; } +//Translate the UNIQUE ID value into a VC number to reduce the communications overhead int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) { int8_t index; @@ -2593,6 +2601,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) return vcLinkUp(index); } +//Process a received HEARTBEAT frame from a VC void vcReceiveHeartbeat(uint32_t rxMillis) { uint32_t deltaMillis; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index ae8396d3..633e3f32 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -1,3 +1,4 @@ +//Copy the string into the serialTransmitBuffer or command response buffer (commandTXBuffer) void systemPrint(const char* string) { uint16_t length; @@ -19,12 +20,14 @@ void systemPrint(const char* string) } } +//Print a string with a carriage return and linefeed void systemPrintln(const char* value) { systemPrint(value); systemPrint("\r\n"); } +//Print an integer value void systemPrint(int value) { char temp[20]; @@ -32,6 +35,7 @@ void systemPrint(int value) systemPrint(temp); } +//Print an integer value as HEX or decimal void systemPrint(int value, uint8_t printType) { char temp[20]; @@ -44,12 +48,14 @@ void systemPrint(int value, uint8_t printType) systemPrint(temp); } +//Print an integer value with a carriage return and line feed void systemPrintln(int value) { systemPrint(value); systemPrint("\r\n"); } +//Print an 8-bit value as HEX or decimal void systemPrint(uint8_t value, uint8_t printType) { char temp[20]; @@ -62,12 +68,14 @@ void systemPrint(uint8_t value, uint8_t printType) systemPrint(temp); } +//Print an 8-bit value as HEX or decimal with a carriage return and linefeed void systemPrintln(uint8_t value, uint8_t printType) { systemPrint(value, printType); systemPrint("\r\n"); } +//Print a 16-bit value as HEX or decimal void systemPrint(uint16_t value, uint8_t printType) { char temp[20]; @@ -80,12 +88,14 @@ void systemPrint(uint16_t value, uint8_t printType) systemPrint(temp); } +//Print a 16-bit value as HEX or decimal with a carriage return and linefeed void systemPrintln(uint16_t value, uint8_t printType) { systemPrint(value, printType); systemPrint("\r\n"); } +//Print a floating point value with a specified number of decimal places void systemPrint(float value, uint8_t decimals) { char temp[20]; @@ -93,12 +103,16 @@ void systemPrint(float value, uint8_t decimals) systemPrint(temp); } +//Print a floating point value with a specified number of decimal places and a +//carriage return and linefeed void systemPrintln(float value, uint8_t decimals) { systemPrint(value, decimals); systemPrint("\r\n"); } +//Print a double precision floating point value with a specified number of decimal +//places void systemPrint(double value, uint8_t decimals) { char temp[300]; @@ -106,17 +120,21 @@ void systemPrint(double value, uint8_t decimals) systemPrint(temp); } +//Print a double precision floating point value with a specified number of decimal +//places and a carriage return and linefeed void systemPrintln(double value, uint8_t decimals) { systemPrint(value, decimals); systemPrint("\r\n"); } +//Print a carriage return and linefeed void systemPrintln() { systemPrint("\r\n"); } +//Print a timestamp value: days hours:minutes:seconds.milliseconds void systemPrintTimestamp(unsigned int milliseconds) { unsigned int seconds; @@ -169,6 +187,7 @@ void systemPrintTimestamp(unsigned int milliseconds) petWDT(); } +//Print a timestamp value with an offset void systemPrintTimestamp() { unsigned int milliseconds; @@ -188,6 +207,7 @@ void systemPrintTimestamp() } } +//Print the unique ID value void systemPrintUniqueID(uint8_t * uniqueID) { int index; @@ -197,11 +217,13 @@ void systemPrintUniqueID(uint8_t * uniqueID) systemPrint(uniqueID[index], HEX); } +//Output a byte to the serial port void systemWrite(uint8_t value) { serialOutputByte(value); } +//Output a buffer of the specified length to the serial port void systemWrite(uint8_t * buffer, uint16_t length) { uint8_t * end; @@ -212,11 +234,13 @@ void systemWrite(uint8_t * buffer, uint16_t length) serialOutputByte(*buffer++); } +//Ensure all serial output has been transmitted, FIFOs are empty void systemFlush() { arch.serialFlush(); } +//Read a byte from the serial port uint8_t systemRead() { return (arch.serialRead()); @@ -278,6 +302,7 @@ void systemReset() arch.systemReset(); } +//Display any debug serial data and then loop forever void waitForever() { //Output the remaining serial data @@ -409,6 +434,7 @@ uint8_t charHexToDec(char a, char b) return ((a << 4) | b); } +//Dump a buffer with offset from buffer start, 16 bytes per row, displaying hex and ASCII void dumpBuffer(uint8_t * data, int length) { char byte[2]; @@ -466,6 +492,7 @@ void dumpBuffer(uint8_t * data, int length) } } +//Dump a buffer assuming that it contains text void dumpBufferRaw(uint8_t * data, int length) { systemPrint("0x "); @@ -477,6 +504,7 @@ void dumpBufferRaw(uint8_t * data, int length) systemPrintln(); } +//Dump a circular buffer with offset from buffer start, 16 bytes per row, displaying hex and ASCII void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, int length) { int bytes; @@ -534,6 +562,7 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, } } +//Compute the RSSI value void updateRSSI() { //Calculate the average RSSI if possible @@ -550,6 +579,7 @@ void updateRSSI() } } +//Update the state of the 4 green LEDs void setRSSI(uint8_t ledBits) { if (ledBits & 0b0001) @@ -573,6 +603,7 @@ void setRSSI(uint8_t ledBits) digitalWrite(pin_rssi4LED, LOW); } +//Set the serial TX (blue) LED value void txLED(bool illuminate) { if (settings.selectLedUse != LEDS_RSSI) @@ -586,6 +617,7 @@ void txLED(bool illuminate) } } +//Set the serial RX (yellow) LED value void rxLED(bool illuminate) { if (settings.selectLedUse != LEDS_RSSI) @@ -599,6 +631,13 @@ void rxLED(bool illuminate) } } +//Radio LED display +// Green1: Radio RX data received +// Green2: Receiving HEARTBEATs (link up) +// Green3: RSSI level +// Green4: Radio TX data sent +// Blue: Bad frame received +// Yellow: Bad CRC received void radioLeds() { uint32_t currentMillis; @@ -690,6 +729,7 @@ void radioLeds() previousMillis = currentMillis; } +//Update the LED values depending upon the selected display void updateLeds() { switch (settings.selectLedUse) @@ -713,6 +753,7 @@ void updateLeds() } } +//Case independent string comparison int stricmp(const char * str1, const char * str2) { char char1; @@ -728,6 +769,7 @@ int stricmp(const char * str1, const char * str2) return char1 - char2; } +//Case independent string comparison with specified maximum length int strnicmp(const char * str1, const char * str2, int length) { char char1; @@ -743,6 +785,7 @@ int strnicmp(const char * str1, const char * str2, int length) return char1 - char2; } +//Display the RSSI, SNR and frequency error values void printPacketQuality() { if (settings.displayPacketQuality == true) diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index edd2a509..36508ef2 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -77,6 +77,7 @@ void updateCylonLEDs() //Multi-Point Client/Server Training //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Start the multi-point training in client mode void beginTrainingClient() { systemPrintln("Begin client training"); @@ -93,6 +94,7 @@ void beginTrainingClient() trainingTimer = millis(); } +//Start the multi-point training in server mode void beginTrainingServer() { trainingServerRunning = true; From 1040697c6a6268dddd6e7c04ca7a541960cb96b6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 19:23:09 -1000 Subject: [PATCH 211/594] Add verifyTables routine --- .../LoRaSerial_Firmware.ino | 4 +--- Firmware/LoRaSerial_Firmware/States.ino | 21 +++++++++++-------- Firmware/LoRaSerial_Firmware/System.ino | 20 ++++++++++++++++++ 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 20b99ba6..feca41b0 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -472,9 +472,7 @@ void setup() systemPrintln("LRS"); outputSerialData(true); - verifyRadioStateTable(); //Verify that the state table contains all of the states in increasing order - - verifyRadioDatagramType(); //Verify that the datagram type table contains all of the datagram types + verifyTables(); //Verify that the enum counts match the name table lengths //Load the unique IDs for the virtual circuits //Always hand out the same VC number for a given unique ID diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 756ff95d..1eb5ed08 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2120,7 +2120,8 @@ bool isLinked() return (false); } -void verifyRadioStateTable() +//Verify the radio state definitions against the radioStateTable +bool verifyRadioStateTable() { int expectedState; unsigned int i; @@ -2131,8 +2132,10 @@ void verifyRadioStateTable() int * order; unsigned int tableEntries; int temp; + int valid; //Verify that all the entries are in the state table + valid = true; tableEntries = sizeof(radioStateTable) / sizeof(radioStateTable[0]); for (index = 0; index < tableEntries; index++) { @@ -2286,20 +2289,20 @@ void verifyRadioStateTable() systemPrintln(expectedState++); } systemPrintln("};"); - - //Wait forever - waitForever(); + valid = false; } + return valid; } //Verify the PacketType enums against the radioDatagramType -void verifyRadioDatagramType() +bool verifyRadioDatagramType() { - if ((sizeof(radioDatagramType) / sizeof(radioDatagramType[0])) != MAX_DATAGRAM_TYPE) - { + bool valid; + + valid = ((sizeof(radioDatagramType) / sizeof(radioDatagramType[0])) == MAX_DATAGRAM_TYPE); + if (!valid) systemPrintln("ERROR - Please update the radioDatagramTable"); - waitForever(); - } + return valid; } //Change states and print the new state diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 633e3f32..098535aa 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -838,3 +838,23 @@ int16_t getReceiveCompletionOffset() break; } } + +//Verify the enums .vs. name tables, stop on failure to force software fix +void verifyTables() +{ + bool valid; + + valid = true; + + //Verify that the state table contains all of the states in increasing order + valid &= verifyRadioStateTable(); + + //Verify that the datagram type table contains all of the datagram types + valid &= verifyRadioDatagramType(); + + if (!valid) + { + outputSerialData(true); + waitForever(); + } +} From 38210e592f588aaa634d5d5358139ef992205619 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 4 Dec 2022 19:30:42 -1000 Subject: [PATCH 212/594] VC: Rename linkUp to vcState which is uint8_t --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 +- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/Serial.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 40 +++++++++++++------ Firmware/LoRaSerial_Firmware/System.ino | 15 +++++++ .../Virtual_Circuit_Protocol.h | 13 ++++++ Firmware/LoRaSerial_Firmware/settings.h | 9 ++++- 7 files changed, 68 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index d461c94c..0796fc81 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -89,7 +89,7 @@ bool commandAT(const char * commandString) if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { for (int i=0; i < MAX_VC; i++) - if (virtualCircuitList[i].linkUp && (i != myVc)) + if ((virtualCircuitList[i].vcState != VC_STATE_LINK_DOWN) && (i != myVc)) vcBreakLink(i); } else if (settings.operatingMode == MODE_POINT_TO_POINT) @@ -293,7 +293,7 @@ bool commandAT(const char * commandString) } else { - systemPrint(vc->linkUp ? "Up" : "Down"); + systemPrint(vcStateNames[vc->vcState]); systemPrint(" @ "); systemPrintTimestamp(millis()); systemPrintln(); diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 7371a8a9..49e844bb 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2423,7 +2423,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond if ((receiveInProcess() == true) || (transactionComplete == true) || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) - && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].linkUp == false))) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN))) { triggerEvent(TRIGGER_TRANSMIT_CANCELED); if (settings.debugReceive || settings.debugDatagrams) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 549fcbe6..22249ded 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -558,7 +558,7 @@ bool vcSerialMessageReceived() //Verify that the destination link is up if ((vcDest != VC_BROADCAST) - && (virtualCircuitList[vcDest & VCAB_NUMBER_MASK].linkUp == false)) + && (virtualCircuitList[vcDest & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { if (settings.debugSerial || settings.debugTransmit) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 1eb5ed08..3d0a035b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1589,6 +1589,9 @@ void updateRadioState() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= /* + + Radio States: + RADIO_RESET | V @@ -1613,6 +1616,20 @@ void updateRadioState() | | '-----------------' + 3-way Handshake to zero ACKs: + + TX PING --> RX ACK1 --> TX ACK2 + or + RX PING --> TX ACK1 --> RX ACK2 + + VC States: + + .-------------> VC_STATE_LINK_DOWN + | | + | | HEARTBEAT received + | HB Timeout v + +<----------- VC_STATE_LINK_ALIVE + */ //==================== @@ -1818,7 +1835,7 @@ void updateRadioState() } //---------- - //Transmit a HEARTBEAT if necessary + //Priority 1: Transmit a HEARTBEAT if necessary //---------- else if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime) && (receiveInProcess() == false)) @@ -1833,14 +1850,15 @@ void updateRadioState() } //---------- - //Wait for an outstanding ACK until it is received, don't transmit any other data + //Priority 2: Wait for an outstanding ACK until it is received, don't + //transmit any other data //---------- else if (vcAckTimer) { //Verify that the link is still up txDestVc = rexmtTxDestVc; if ((txDestVc != VC_BROADCAST) - && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].linkUp == false)) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { //Stop the retransmits vcAckTimer = 0; @@ -1903,7 +1921,8 @@ void updateRadioState() } //---------- - //Prioritize sending the command response higher than sending data + //Priority 3: Send the entire command response, toggle between waiting for + //ACK above and transmitting the command response //---------- else if (availableTXCommandBytes()) { @@ -1928,7 +1947,7 @@ void updateRadioState() } //---------- - //Check for data to send + //Lowest Priority: Check for data to send //---------- else if ((receiveInProcess() == false) && (vcSerialMessageReceived())) { @@ -2054,7 +2073,7 @@ void updateRadioState() //Determine if the link has timed out vc = &virtualCircuitList[index]; - if (vc->linkUp && (serverLinkBroken + if ((vc->vcState != VC_STATE_LINK_DOWN) && (serverLinkBroken || ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) { //When the server connection breaks, break all other connections and @@ -2489,13 +2508,10 @@ void vcBreakLink(int8_t vcIndex) //Account for the link failure vc = &virtualCircuitList[vcIndex]; vc->linkFailures++; - vc->linkUp = false; + vc->vcState = VC_STATE_LINK_DOWN; } linkFailures++; - //Send the status message - vcSendLinkStatus(false, vcIndex); - //Stop the transmit timer transmitTimer = 0; @@ -2511,7 +2527,7 @@ int8_t vcLinkUp(int8_t index) VIRTUAL_CIRCUIT * vc = &virtualCircuitList[index]; //Send the status message - if (!vc->linkUp) + if (vc->vcState == VC_STATE_LINK_DOWN) { vc->firstHeartbeatMillis = millis(); vcSendLinkStatus(true, index); @@ -2523,7 +2539,7 @@ int8_t vcLinkUp(int8_t index) } //Update the link status - vc->linkUp = true; + vc->vcState = VC_STATE_LINK_ALIVE; vc->lastHeartbeatMillis = millis(); return index; } diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 098535aa..92d521ec 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -839,6 +839,18 @@ int16_t getReceiveCompletionOffset() } } +//Verify the VC_STATE_TYPE enums against the vcStateNames table +bool verifyVcStateNames() +{ + bool valid; + + //Verify the length of the vcStateNames table + valid = (VC_STATE_MAX == (sizeof(vcStateNames) / sizeof(vcStateNames[0]))); + if (!valid) + systemPrintln("ERROR: Fix difference between VC_STATE_TYPE and vcStateNames"); + return valid; +} + //Verify the enums .vs. name tables, stop on failure to force software fix void verifyTables() { @@ -852,6 +864,9 @@ void verifyTables() //Verify that the datagram type table contains all of the datagram types valid &= verifyRadioDatagramType(); + //Verify the VC state name table + valid &= verifyVcStateNames(); + if (!valid) { outputSerialData(true); diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index edda6ba7..3195f3c7 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -135,6 +135,19 @@ typedef struct _VC_LINK_STATUS_MESSAGE uint8_t linkStatus; //Link status } VC_LINK_STATUS_MESSAGE; +typedef enum +{ + VC_STATE_LINK_DOWN = 0, //0: HEARTBEATs not received + VC_STATE_LINK_ALIVE, //1: Receiving HEARTBEATs, waiting for PING + VC_STATE_SEND_PING, //2: ATC command received, sending PING + VC_STATE_WAIT_FOR_ACK1 ,//3: PING sent, waiting for ACK1 + VC_STATE_WAIT_FOR_ACK2 ,//4: ACK1 sent, waiting for ACK2 + VC_STATE_CONNECTED, //5: ACK2 received, ACKs cleared, ready to send data + + //Insert new states before this line + VC_STATE_MAX +} VC_STATE_TYPE; + typedef struct _VC_DATA_ACK_MESSAGE { uint8_t msgDestVc; //message destination VC diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index c3490e66..b5b2c9ef 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -161,6 +161,13 @@ const char * const radioDatagramType[] = "VC_HEARTBEAT", }; +const char * const vcStateNames[] = +{ // 0 1 + "LINK-DOWN", "LINK-ALIVE", + // 2 3 4 5 + "SEND-PING", "WAIT-ACK1", "WAIT-ACK2", "CONNECTED", +}; + typedef struct _VIRTUAL_CIRCUIT { uint8_t uniqueId[UNIQUE_ID_BYTES]; @@ -177,7 +184,7 @@ typedef struct _VIRTUAL_CIRCUIT //Link management bool valid; //Unique ID is valid - bool linkUp; //Link is up, received a recent HEARTBEAT datagram + uint8_t vcState; //State of VC /* ACK number management From 719fc99fdc3e56cc88c1bcc0089b17e519a083ad Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 17:52:37 -1000 Subject: [PATCH 213/594] VC: Continue sending data until its all done --- Firmware/LoRaSerial_Firmware/States.ino | 179 ++++++++++++------------ 1 file changed, 91 insertions(+), 88 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3d0a035b..c80b5980 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1949,113 +1949,116 @@ void updateRadioState() //---------- //Lowest Priority: Check for data to send //---------- - else if ((receiveInProcess() == false) && (vcSerialMessageReceived())) + else if (vcSerialMessageReceived()) { - //No need to add the VC header since the header is in the radioTxBuffer - //Get the VC header - vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; - if (vcHeader->destVc == VC_BROADCAST) - channel = 0; - else - channel = (vcHeader->destVc >> VCAB_NUMBER_BITS) & VCAB_CHANNEL_MASK; - switch (channel) + if (receiveInProcess() == false) { - case 0: //Data packets - //Check for datagram transmission + //No need to add the VC header since the header is in the radioTxBuffer + //Get the VC header + vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; if (vcHeader->destVc == VC_BROADCAST) + channel = 0; + else + channel = (vcHeader->destVc >> VCAB_NUMBER_BITS) & VCAB_CHANNEL_MASK; + switch (channel) { - //Broadcast this data to all VCs, no ACKs will be received - triggerEvent(TRIGGER_VC_TX_DATA); - xmitVcDatagram(); - break; - } + case 0: //Data packets + //Check for datagram transmission + if (vcHeader->destVc == VC_BROADCAST) + { + //Broadcast this data to all VCs, no ACKs will be received + triggerEvent(TRIGGER_VC_TX_DATA); + xmitVcDatagram(); + break; + } - //Transmit the packet - triggerEvent(TRIGGER_VC_TX_DATA); - if (xmitDatagramP2PData() == true) - changeState(RADIO_VC_WAIT_TX_DONE); + //Transmit the packet + triggerEvent(TRIGGER_VC_TX_DATA); + if (xmitDatagramP2PData() == true) + changeState(RADIO_VC_WAIT_TX_DONE); - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; + //Since vcAckTimer is off when equal to zero, force it to a non-zero value + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; - //Save the message for retransmission - SAVE_TX_BUFFER(); - rexmtTxDestVc = txDestVc; - break; + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + break; - case 1: //Remote command packets - //Remote commands must not be broadcast - if (vcHeader->destVc == VC_BROADCAST) - { - if (settings.debugSerial || settings.debugTransmit) + case 1: //Remote command packets + //Remote commands must not be broadcast + if (vcHeader->destVc == VC_BROADCAST) { - systemPrintln("ERROR: Remote commands may not be broadcast!"); - outputSerialData(true); - } + if (settings.debugSerial || settings.debugTransmit) + { + systemPrintln("ERROR: Remote commands may not be broadcast!"); + outputSerialData(true); + } - //Discard this message - endOfTxData = &outgoingPacket[headerBytes]; - break; - } + //Discard this message + endOfTxData = &outgoingPacket[headerBytes]; + break; + } - //Determine if this remote command gets processed on the local node - if ((vcHeader->destVc & VCAB_NUMBER_MASK) == myVc) - { - //Copy the command into the command buffer - commandLength = endOfTxData - &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES]; - memcpy(commandBuffer, &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES], commandLength); - if (settings.debugSerial) + //Determine if this remote command gets processed on the local node + if ((vcHeader->destVc & VCAB_NUMBER_MASK) == myVc) { - systemPrint("RX: Moving "); - systemPrint(commandLength); - systemPrintln(" bytes into commandBuffer"); - outputSerialData(true); - dumpBuffer((uint8_t *)commandBuffer, commandLength); - } - petWDT(); + //Copy the command into the command buffer + commandLength = endOfTxData - &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES]; + memcpy(commandBuffer, &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES], commandLength); + if (settings.debugSerial) + { + systemPrint("RX: Moving "); + systemPrint(commandLength); + systemPrintln(" bytes into commandBuffer"); + outputSerialData(true); + dumpBuffer((uint8_t *)commandBuffer, commandLength); + } + petWDT(); - //Reset the buffer data pointer for the next transmit operation - endOfTxData = &outgoingPacket[headerBytes]; + //Reset the buffer data pointer for the next transmit operation + endOfTxData = &outgoingPacket[headerBytes]; - //Process the command - petWDT(); - printerEndpoint = PRINT_TO_RF; //Send prints to RF link - checkCommand(); //Parse the command buffer - petWDT(); - printerEndpoint = PRINT_TO_SERIAL; - length = availableTXCommandBytes(); - if (settings.debugSerial) - { - systemPrint("RX: checkCommand placed "); - systemPrint(length); - systemPrintln(" bytes into commandTXBuffer"); - dumpCircularBuffer(commandTXBuffer, commandTXTail, sizeof(commandTXBuffer), length); - outputSerialData(true); + //Process the command petWDT(); - } + printerEndpoint = PRINT_TO_RF; //Send prints to RF link + checkCommand(); //Parse the command buffer + petWDT(); + printerEndpoint = PRINT_TO_SERIAL; + length = availableTXCommandBytes(); + if (settings.debugSerial) + { + systemPrint("RX: checkCommand placed "); + systemPrint(length); + systemPrintln(" bytes into commandTXBuffer"); + dumpCircularBuffer(commandTXBuffer, commandTXTail, sizeof(commandTXBuffer), length); + outputSerialData(true); + petWDT(); + } - //Break up the command response - while (availableTXCommandBytes()) - readyLocalCommandPacket(); - break; - } + //Break up the command response + while (availableTXCommandBytes()) + readyLocalCommandPacket(); + break; + } - //Send the remote command - vcHeader->destVc &= VCAB_NUMBER_MASK; - if (xmitDatagramP2PCommand() == true) - changeState(RADIO_VC_WAIT_TX_DONE); + //Send the remote command + vcHeader->destVc &= VCAB_NUMBER_MASK; + if (xmitDatagramP2PCommand() == true) + changeState(RADIO_VC_WAIT_TX_DONE); - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; + //Since vcAckTimer is off when equal to zero, force it to a non-zero value + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; - //Save the message for retransmission - SAVE_TX_BUFFER(); - rexmtTxDestVc = txDestVc; - break; + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + break; + } } } From 72943bfa8b94b0b22525c678d6d376aa266b67ce Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 09:45:37 -1000 Subject: [PATCH 214/594] VC: Rename index to VC index --- Firmware/LoRaSerial_Firmware/States.ino | 34 ++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c80b5980..cc4640ea 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2525,15 +2525,15 @@ void vcBreakLink(int8_t vcIndex) } //Place VC in LINK-UP state since it is receiving HEARTBEATs from the remote radio -int8_t vcLinkUp(int8_t index) +int8_t vcLinkUp(int8_t vcIndex) { - VIRTUAL_CIRCUIT * vc = &virtualCircuitList[index]; + VIRTUAL_CIRCUIT * vc = &virtualCircuitList[vcIndex]; //Send the status message if (vc->vcState == VC_STATE_LINK_DOWN) { vc->firstHeartbeatMillis = millis(); - vcSendLinkStatus(true, index); + vcSendLinkStatus(true, vcIndex); //Reset the ACK counters vc->txAckNumber = 0; @@ -2544,33 +2544,33 @@ int8_t vcLinkUp(int8_t index) //Update the link status vc->vcState = VC_STATE_LINK_ALIVE; vc->lastHeartbeatMillis = millis(); - return index; + return vcIndex; } //Translate the UNIQUE ID value into a VC number to reduce the communications overhead int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) { - int8_t index; VIRTUAL_CIRCUIT * vc; + int8_t vcIndex; //Determine if the address is already in the list - for (index = 0; index < MAX_VC; index++) + for (vcIndex = 0; vcIndex < MAX_VC; vcIndex++) { //Verify that an address is present - vc = &virtualCircuitList[index]; + vc = &virtualCircuitList[vcIndex]; if (!vc->valid) continue; //Compare the unique ID values if (memcmp(vc->uniqueId, id, UNIQUE_ID_BYTES) == 0) //Update the link status - return vcLinkUp(index); + return vcLinkUp(vcIndex); } //The unique ID is not in the list //Fill in clients that were already running - index = srcAddr; - vc = &virtualCircuitList[index]; + vcIndex = srcAddr; + vc = &virtualCircuitList[vcIndex]; //Only the server can assign the address bytes if ((srcAddr == VC_UNASSIGNED) && (!settings.server)) @@ -2581,19 +2581,19 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) { //Unknown client ID //Determine if there is a free address - for (index = 0; index < MAX_VC; index++) + for (vcIndex = 0; vcIndex < MAX_VC; vcIndex++) { - vc = &virtualCircuitList[index]; - if (!virtualCircuitList[index].valid) + vc = &virtualCircuitList[vcIndex]; + if (!virtualCircuitList[vcIndex].valid) break; } - if (index >= MAX_VC) + if (vcIndex >= MAX_VC) { systemPrintln("ERROR: Too many clients, no free addresses!\n"); outputSerialData(true); return -2; } - srcAddr = index; + srcAddr = vcIndex; } //Check for an address conflict @@ -2616,11 +2616,11 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) //Save the unique ID to VC assignment in NVM memcpy(&vc->uniqueId, id, UNIQUE_ID_BYTES); if (settings.server) - nvmSaveVcUniqueId(index); + nvmSaveVcUniqueId(vcIndex); //Mark this link as up vc->valid = true; - return vcLinkUp(index); + return vcLinkUp(vcIndex); } //Process a received HEARTBEAT frame from a VC From 64e83bfd8422bb0a10b41d0e5171bfa65c4518d2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 09:47:03 -1000 Subject: [PATCH 215/594] VC: Rename vcLinkUp to vcLinkAlive --- Firmware/LoRaSerial_Firmware/States.ino | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index cc4640ea..774d363a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2525,7 +2525,7 @@ void vcBreakLink(int8_t vcIndex) } //Place VC in LINK-UP state since it is receiving HEARTBEATs from the remote radio -int8_t vcLinkUp(int8_t vcIndex) +int8_t vcLinkAlive(int8_t vcIndex) { VIRTUAL_CIRCUIT * vc = &virtualCircuitList[vcIndex]; @@ -2564,7 +2564,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) //Compare the unique ID values if (memcmp(vc->uniqueId, id, UNIQUE_ID_BYTES) == 0) //Update the link status - return vcLinkUp(vcIndex); + return vcLinkAlive(vcIndex); } //The unique ID is not in the list @@ -2620,7 +2620,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) //Mark this link as up vc->valid = true; - return vcLinkUp(vcIndex); + return vcLinkAlive(vcIndex); } //Process a received HEARTBEAT frame from a VC From 182820e43ea3f28e12fc0cc144d0abcf833f5718 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 09:52:48 -1000 Subject: [PATCH 216/594] VC: Send VC state to PC --- Firmware/LoRaSerial_Firmware/States.ino | 80 ++++++++++++------- .../Virtual_Circuit_Protocol.h | 9 +-- Firmware/Tools/VcServerTest.c | 48 ++++++++--- 3 files changed, 91 insertions(+), 46 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 774d363a..b2680189 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2460,38 +2460,55 @@ void discardPreviousData() } //Output VC link status -void vcSendLinkStatus(bool linkUp, int8_t srcVc) +void vcChangeState(int8_t vcIndex, uint8_t state) { - //Build the message - VC_LINK_STATUS_MESSAGE message; - message.linkStatus = linkUp ? LINK_UP : LINK_DOWN; - - //Build the message header - VC_SERIAL_MESSAGE_HEADER header; - header.start = START_OF_VC_SERIAL; - header.radio.length = VC_RADIO_HEADER_BYTES + sizeof(message); - header.radio.destVc = PC_LINK_STATUS; - header.radio.srcVc = srcVc; - - //Send the message - systemWrite((uint8_t *)&header, sizeof(header)); - systemWrite((uint8_t *)&message, sizeof(message)); + VIRTUAL_CIRCUIT * vc; + uint32_t vcBit; - if (settings.printLinkUpDown) + vc = &virtualCircuitList[vcIndex]; + if (state != vc->vcState) { - if (linkUp) - { - systemPrint("========== Link "); - systemPrint(srcVc); - systemPrintln(" UP =========="); - } - else + //Set the new state + vc->vcState = state; + + //Display the state change + if (settings.printLinkUpDown) { - systemPrint("--------- Link "); - systemPrint(srcVc); - systemPrintln(" Down ---------"); + if (state == VC_STATE_CONNECTED) + { + systemPrint("======= VC "); + systemPrint(vcIndex); + systemPrintln(" CONNECTED ======"); + } + if (state == VC_STATE_LINK_DOWN) + { + systemPrint("--------- VC "); + systemPrint(vcIndex); + systemPrintln(" Down ---------"); + } + else if (state == VC_STATE_LINK_ALIVE) + { + systemPrint("-=--=--=- VC "); + systemPrint(vcIndex); + systemPrintln(" ALIVE =--=--=-"); + } + outputSerialData(true); } - outputSerialData(true); + + //Build the VC state message + VC_STATE_MESSAGE message; + message.vcState = state; + + //Build the message header + VC_SERIAL_MESSAGE_HEADER header; + header.start = START_OF_VC_SERIAL; + header.radio.length = VC_RADIO_HEADER_BYTES + sizeof(message); + header.radio.destVc = PC_LINK_STATUS; + header.radio.srcVc = vcIndex; + + //Send the VC state message + systemWrite((uint8_t *)&header, sizeof(header)); + systemWrite((uint8_t *)&message, sizeof(message)); } } @@ -2506,12 +2523,12 @@ void vcBreakLink(int8_t vcIndex) vcIndex &= VCAB_NUMBER_MASK; //Get the virtual circuit data structure - if ((vcIndex >= 0) && (vcIndex != myVc) && ( vcIndex < MAX_VC)) + if ((vcIndex >= 0) && (vcIndex != myVc) && (vcIndex < MAX_VC)) { //Account for the link failure vc = &virtualCircuitList[vcIndex]; vc->linkFailures++; - vc->vcState = VC_STATE_LINK_DOWN; + vcChangeState(vcIndex, VC_STATE_LINK_DOWN); } linkFailures++; @@ -2533,7 +2550,7 @@ int8_t vcLinkAlive(int8_t vcIndex) if (vc->vcState == VC_STATE_LINK_DOWN) { vc->firstHeartbeatMillis = millis(); - vcSendLinkStatus(true, vcIndex); + vcChangeState(vcIndex, VC_STATE_LINK_ALIVE); //Reset the ACK counters vc->txAckNumber = 0; @@ -2542,7 +2559,8 @@ int8_t vcLinkAlive(int8_t vcIndex) } //Update the link status - vc->vcState = VC_STATE_LINK_ALIVE; + if (vc->vcState == VC_STATE_LINK_DOWN) + vcChangeState(vcIndex, VC_STATE_LINK_ALIVE); vc->lastHeartbeatMillis = millis(); return vcIndex; } diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 3195f3c7..50dadd84 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -130,10 +130,10 @@ typedef struct _VC_SERIAL_MESSAGE_HEADER #define VC_SERIAL_HEADER_BYTES (sizeof(VC_SERIAL_MESSAGE_HEADER)) //Length of the serial VC header in bytes -typedef struct _VC_LINK_STATUS_MESSAGE +typedef struct _VC_STATE_MESSAGE { - uint8_t linkStatus; //Link status -} VC_LINK_STATUS_MESSAGE; + uint8_t vcState; //VC state +} VC_STATE_MESSAGE; typedef enum { @@ -153,9 +153,6 @@ typedef struct _VC_DATA_ACK_MESSAGE uint8_t msgDestVc; //message destination VC } VC_DATA_ACK_MESSAGE; -#define LINK_DOWN 0 -#define LINK_UP 1 - //------------------------------------------------------------------------------ // Macros //------------------------------------------------------------------------------ diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index b6afddba..09de0de1 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -13,6 +13,7 @@ #define DEBUG_PC_TO_RADIO 0 #define DEBUG_RADIO_TO_PC 0 #define DISPLAY_DATA_ACK 1 +#define DISPLAY_VC_STATE 1 bool findMyVc; int myVc = VC_UNASSIGNED; @@ -235,22 +236,51 @@ int hostToStdout(uint8_t * data, uint8_t bytesToSend) void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) { - VC_LINK_STATUS_MESSAGE * linkStatus; + VC_STATE_MESSAGE * vcMsg; - linkStatus = (VC_LINK_STATUS_MESSAGE *)&header[1]; - if (linkStatus->linkStatus == LINK_UP) - printf("========== Link %d UP ==========\n", header->radio.srcVc); - else - printf("--------- Link %d DOWN ---------\n", header->radio.srcVc); + vcMsg = (VC_STATE_MESSAGE *)&header[1]; + if (DISPLAY_VC_STATE) + { + switch (vcMsg->vcState) + { + default: + printf("------- VC %d State %3d ------\n", header->radio.srcVc, vcMsg->vcState); + break; + + case VC_STATE_LINK_DOWN: + printf("--------- VC %d DOWN ---------\n", header->radio.srcVc); + break; + + case VC_STATE_LINK_ALIVE: + printf("-=--=--=- VC %d ALIVE =--=--=-\n", header->radio.srcVc); + break; + + case VC_STATE_SEND_PING: + printf("-=--=-- VC %d ALIVE P1 --=--=-\n", header->radio.srcVc); + break; + + case VC_STATE_WAIT_FOR_ACK1: + printf("-=--=-- VC %d ALIVE WA1 -=--=-\n", header->radio.srcVc); + break; + + case VC_STATE_WAIT_FOR_ACK2: + printf("-=--=-- VC %d ALIVE WA2 -=--=-\n", header->radio.srcVc); + break; + + case VC_STATE_CONNECTED: + printf("======= VC %d CONNECTED ======\n", header->radio.srcVc); + break; + } + } } void radioDataAck(uint8_t * data, uint8_t length) { - VC_DATA_ACK_MESSAGE * ack; + VC_DATA_ACK_MESSAGE * vcMsg; - ack = (VC_DATA_ACK_MESSAGE *)data; + vcMsg = (VC_DATA_ACK_MESSAGE *)data; if (DISPLAY_DATA_ACK) - printf("ACK from VC %d\n", ack->msgDestVc); + printf("ACK from VC %d\n", vcMsg->msgDestVc); } int radioToHost() From 6872ca1c1bcd6b93f55a57b97e8aa324e5545886 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 6 Dec 2022 06:33:13 -1000 Subject: [PATCH 217/594] VC: Properly restrict radio transmissions --- Firmware/LoRaSerial_Firmware/Serial.ino | 40 ++++++++++--------- .../Virtual_Circuit_Protocol.h | 7 ++-- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 22249ded..12e2cefb 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -513,46 +513,48 @@ bool vcSerialMessageReceived() break; } - //Determine if the message is too large + //vcProcessSerialInput validates the vcDest value, this check validates + //that internally generated traffic uses valid vcDest values. Only messages + //enabled for receive on a remote radio may be transmitted. vcDest = vcSerialMsgGetVcDest(); - if (msgLength > maxDatagramSize) + if (((uint8_t)vcDest >= (uint8_t)MIN_RX_NOT_ALLOWED) && (vcDest != VC_BROADCAST)) { if (settings.debugSerial || settings.debugTransmit) { - systemPrintln("VC serial RX: Message too long, discarded"); + systemPrint("ERROR: Invalid internally generated vcDest "); + systemPrint(vcDest); + systemPrintln(", discarding message!"); outputSerialData(true); } - //Discard this message, it is too long to transmit over the radio link + //Discard this message radioTxTail += msgLength; if (radioTxTail >= sizeof(radioTxBuffer)) radioTxTail -= sizeof(radioTxBuffer); - - //Nothing to do for invalid addresses or the broadcast address - if ((vcDest >= MAX_VC) || (vcDest == VC_BROADCAST)) - break; - - //Break the link to this host - vcBreakLink(vcDest); break; } - //Validate the destination VC - //None of the local host targets belong in the radioTxBuffer - if ((vcDest >= PC_COMMAND) && (vcDest < VC_BROADCAST)) + //Determine if the message is too large + if (msgLength > maxDatagramSize) { if (settings.debugSerial || settings.debugTransmit) { - systemPrint("ERROR: Invalid vcDest "); - systemPrint(vcDest); - systemPrintln(", discarding message!"); + systemPrintln("VC serial RX: Message too long, discarded"); outputSerialData(true); } - //Discard this message + //Discard this message, it is too long to transmit over the radio link radioTxTail += msgLength; if (radioTxTail >= sizeof(radioTxBuffer)) radioTxTail -= sizeof(radioTxBuffer); + + //Nothing to do for invalid addresses or the broadcast address + if (((uint8_t)vcDest >= (uint8_t)MIN_TX_NOT_ALLOWED) && (vcDest != VC_BROADCAST)) + break; + + //Break the link to this host since message delivery is guarranteed and + //this message is being discarded + vcBreakLink(vcDest & VCAB_NUMBER_MASK); break; } @@ -709,7 +711,7 @@ void vcProcessSerialInput() } //Verify the destination virtual circuit - if ((vcDest > PC_LINK_STATUS) && (vcDest < VC_BROADCAST)) + if (((uint8_t)vcDest >= (uint8_t)MIN_TX_NOT_ALLOWED) && (vcDest < VC_BROADCAST)) { if (settings.debugSerial) { diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 50dadd84..fa0c5bb1 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -39,10 +39,11 @@ //appropriate datagram type. Upon reception of one of these messages the bit is //added back into the VC header and the message is delivered to the host PC. //As a result, any host may send commands to any other host! +#define MIN_RX_NOT_ALLOWED VC_RSVD_SPECIAL_VCS +#define PC_REMOTE_RESPONSE ((int8_t)(6 << VCAB_NUMBER_BITS)) +//Address spaces 2 - 5 are not currently defined +#define MIN_TX_NOT_ALLOWED ((int8_t)(2 << VCAB_NUMBER_BITS)) //Marker only #define PC_REMOTE_COMMAND ((int8_t)(1 << VCAB_NUMBER_BITS)) -#define PC_REMOTE_RESPONSE ((int8_t)(2 << VCAB_NUMBER_BITS)) - -//Address spaces 3 - 6 are not currently defined //Field offsets in the VC HEARTBEAT frame #define VC_HB_UNIQUE_ID 0 From dd1aad8cd9105277eb142d90541258c51e92b767 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 6 Dec 2022 06:39:13 -1000 Subject: [PATCH 218/594] VC: Fix link timeouts, use data and HEARTBEATS to keep link alive --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 +- Firmware/LoRaSerial_Firmware/States.ino | 55 ++++++++++++----------- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 0796fc81..ef9aeb9d 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -308,13 +308,13 @@ bool commandAT(const char * commandString) systemPrintln(); } systemPrint(" Last: "); - systemPrintTimestamp(vc->lastHeartbeatMillis); + systemPrintTimestamp(vc->lastTrafficMillis); systemPrintln(); systemPrint(" First: "); systemPrintTimestamp(vc->firstHeartbeatMillis); systemPrintln(); systemPrint(" Up Time: "); - systemPrintTimestamp(vc->lastHeartbeatMillis - vc->firstHeartbeatMillis); + systemPrintTimestamp(vc->lastTrafficMillis - vc->firstHeartbeatMillis); systemPrintln(); systemPrintln(" Sent"); systemPrint(" Frames: "); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b2680189..fa2d818a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1707,6 +1707,10 @@ void updateRadioState() PacketType packetType = rcvDatagram(); vcHeader = (VC_RADIO_MESSAGE_HEADER *)rxData; + //Indicate receive traffic from this VC including HEARTBEATs + if ((rxSrcVc == VC_BROADCAST) || ((uint8_t)rxSrcVc < (uint8_t)MIN_RX_NOT_ALLOWED)) + virtualCircuitList[rxSrcVc].lastTrafficMillis = currentMillis; + //Process the received datagram switch (packetType) { @@ -1844,7 +1848,7 @@ void updateRadioState() if (xmitVcHeartbeat(myVc, myUniqueId)) { if (((uint8_t)myVc) < MAX_VC) - virtualCircuitList[myVc].lastHeartbeatMillis = millis(); + virtualCircuitList[myVc].lastTrafficMillis = currentMillis; changeState(RADIO_VC_WAIT_TX_DONE); } } @@ -2065,36 +2069,36 @@ void updateRadioState() //---------- //Check for link timeout //---------- - else + serverLinkBroken = false; + for (index = 0; index < MAX_VC; index++) { - serverLinkBroken = false; - for (index = 0; index < MAX_VC; index++) + //Don't timeout the connection to myself + if (index == myVc) + continue; + + //Determine if the link has timed out + vc = &virtualCircuitList[index]; + if ((vc->vcState != VC_STATE_LINK_DOWN) && (serverLinkBroken + || ((currentMillis - vc->lastTrafficMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) { - //Don't timeout the connection to myself - if (index == myVc) - continue; - - //Determine if the link has timed out - vc = &virtualCircuitList[index]; - if ((vc->vcState != VC_STATE_LINK_DOWN) && (serverLinkBroken - || ((currentMillis - vc->lastHeartbeatMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) + if (index == VC_SERVER) { //When the server connection breaks, break all other connections and - //wait for the server - if (index == VC_SERVER) - { - serverLinkBroken = true; - changeState(RADIO_VC_WAIT_SERVER); - } + //wait for the server. The server provides the time (hop) synchronization + //between all of the nodes. When the server fails, the clocks drift + //and the radios loose communications because they are hopping at different + //times. + serverLinkBroken = true; + changeState(RADIO_VC_WAIT_SERVER); + } - //If waiting for an ACK and the link breaks, stop the retransmissions - //by stopping the ACK timer. - if (vcAckTimer && (index == rexmtTxDestVc)) - vcAckTimer = 0; + //If waiting for an ACK and the link breaks, stop the retransmissions + //by stopping the ACK timer. + if (vcAckTimer && (index == rexmtTxDestVc)) + vcAckTimer = 0; - //Break the link - vcBreakLink(index); - } + //Break the link + vcBreakLink(index); } } break; @@ -2561,7 +2565,6 @@ int8_t vcLinkAlive(int8_t vcIndex) //Update the link status if (vc->vcState == VC_STATE_LINK_DOWN) vcChangeState(vcIndex, VC_STATE_LINK_ALIVE); - vc->lastHeartbeatMillis = millis(); return vcIndex; } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index b5b2c9ef..d8e66e73 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -172,7 +172,7 @@ typedef struct _VIRTUAL_CIRCUIT { uint8_t uniqueId[UNIQUE_ID_BYTES]; unsigned long firstHeartbeatMillis; //Time VC link came up - unsigned long lastHeartbeatMillis; //Last time a HEARTBEAT was received, last time link was up + unsigned long lastTrafficMillis; //Last time a frame was received //Link quality metrics uint32_t framesSent; //myVc --> VC, Total number of frames sent From f2b59078ac5be1ff57438da9cfb9e9c82e8f0f10 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 18:00:11 -1000 Subject: [PATCH 219/594] VC: Add ATC command to send the PING datagram --- Firmware/LoRaSerial_Firmware/Commands.ino | 14 ++++++++++++++ .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 11 +++++++++++ 3 files changed, 26 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index ef9aeb9d..4f0e2b5a 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -52,6 +52,7 @@ bool commandAT(const char * commandString) systemPrintln("Command summary:"); systemPrintln(" AT? - Print the command summary"); systemPrintln(" ATB - Break the link"); + systemPrintln(" ATC - Establish VC connection for data"); systemPrintln(" ATD - Display the debug settings"); systemPrintln(" ATG - Generate new netID and encryption key"); systemPrintln(" ATI - Display the radio version"); @@ -111,6 +112,19 @@ bool commandAT(const char * commandString) outputSerialData(true); } break; + case ('C'): //ATC - Establish VC connection for data + if ((settings.operatingMode != MODE_VIRTUAL_CIRCUIT) + || (vc->vcState != VC_STATE_LINK_ALIVE)) + reportERROR(); + else + { + if (cmdVc == myVc) + vcChangeState(cmdVc, VC_STATE_CONNECTED); + else + vcChangeState(cmdVc, VC_STATE_SEND_PING); + reportOK; + } + break; case ('G'): //Generate a new netID and encryption key generateTrainingSettings(); reportOK(); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index feca41b0..790bbfc4 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -432,6 +432,7 @@ int8_t txDestVc; unsigned long vcAckTimer; VIRTUAL_CIRCUIT virtualCircuitList[MAX_VC]; uint8_t serialOperatingMode; +uint32_t vcConnecting; unsigned int multipointChannelLoops = 0; //Count the number of times Multipoint scanning has traversed the table unsigned int multipointAttempts = 0; //Throttle back scanning when a server is not detected diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index fa2d818a..fa9d8148 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1629,6 +1629,10 @@ void updateRadioState() | | HEARTBEAT received | HB Timeout v +<----------- VC_STATE_LINK_ALIVE + ^ | + | | ATC command + | HB Timeout V + +<------------- VC_STATE_SEND_PING */ @@ -2513,6 +2517,13 @@ void vcChangeState(int8_t vcIndex, uint8_t state) //Send the VC state message systemWrite((uint8_t *)&header, sizeof(header)); systemWrite((uint8_t *)&message, sizeof(message)); + + //Determine if the VC is connecting + vcBit = 1 << vcIndex; + if ((state >= VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) + vcConnecting |= vcBit; + else + vcConnecting &= vcBit; } } From 738a15ee8291c1768034a69413d7511e23fa415f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 18:37:15 -1000 Subject: [PATCH 220/594] VC: Send the PING to the remote system --- Firmware/LoRaSerial_Firmware/Radio.ino | 25 +++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 37 +++++++++++++++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 1 + 3 files changed, 63 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 49e844bb..8312fb64 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1282,6 +1282,31 @@ bool xmitVcAckFrame(int8_t destVc) return xmitDatagramP2PAck(); } +bool xmitVcPing(int8_t destVc) +{ + VC_RADIO_MESSAGE_HEADER * vcHeader; + + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; + vcHeader->length = VC_RADIO_HEADER_BYTES; + vcHeader->destVc = destVc; + vcHeader->srcVc = myVc; + endOfTxData += VC_RADIO_HEADER_BYTES; + + /* + endOfTxData ---. + | + V + +--------+---------+--------+----------+---------+----------+ + | | | | | | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | n Bytes | + +--------+---------+--------+----------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_PING; + return (transmitDatagram()); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Datagram reception //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index fa9d8148..23ae6f2f 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1633,6 +1633,11 @@ void updateRadioState() | | ATC command | HB Timeout V +<------------- VC_STATE_SEND_PING + ^ | + | | TX PING + | | TX Complete + | HB Timeout V + +<------------- VC_STATE_WAIT_ACK1 */ @@ -1954,6 +1959,38 @@ void updateRadioState() rexmtTxDestVc = txDestVc; } + //---------- + //Priority 4: Walk through the 3-way handshake + //---------- + else if (vcConnecting) + { + if (receiveInProcess() == false) + { + for (index = 0; index < MAX_VC; index++) + { + //Determine the first VC that is walking through connections + if (vcConnecting & (1 << index)) + { + //Determine if PING needs to be sent + if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) + { + //Send the PING datagram, first part of the 3-way handshake + if (xmitVcPing(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + break; + } + } + +//Retransmit after lastPingMillis + (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + + } + } + } + } + //---------- //Lowest Priority: Check for data to send //---------- diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index d8e66e73..22d64870 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -173,6 +173,7 @@ typedef struct _VIRTUAL_CIRCUIT uint8_t uniqueId[UNIQUE_ID_BYTES]; unsigned long firstHeartbeatMillis; //Time VC link came up unsigned long lastTrafficMillis; //Last time a frame was received + unsigned long lastPingMillis; //Last time a ping was sent or ACK received //Link quality metrics uint32_t framesSent; //myVc --> VC, Total number of frames sent From c50b45509e411c7982075b7b1b69f99d4356719b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 18:22:41 -1000 Subject: [PATCH 221/594] VC: Received the PING, send ACK1 to the remote system --- Firmware/LoRaSerial_Firmware/Radio.ino | 25 ++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 43 +++++++++++++++++-------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 8312fb64..99ed3949 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1307,6 +1307,31 @@ bool xmitVcPing(int8_t destVc) return (transmitDatagram()); } +bool xmitVcAck1(int8_t destVc) +{ + VC_RADIO_MESSAGE_HEADER * vcHeader; + + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; + vcHeader->length = VC_RADIO_HEADER_BYTES; + vcHeader->destVc = destVc; + vcHeader->srcVc = myVc; + endOfTxData += VC_RADIO_HEADER_BYTES; + + /* + endOfTxData ---. + | + V + +--------+---------+--------+----------+---------+----------+ + | | | | | | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | n Bytes | + +--------+---------+--------+----------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_ACK_1; + return (transmitDatagram()); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Datagram reception //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 23ae6f2f..9ede93dc 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1624,20 +1624,25 @@ void updateRadioState() VC States: - .-------------> VC_STATE_LINK_DOWN - | | - | | HEARTBEAT received - | HB Timeout v - +<----------- VC_STATE_LINK_ALIVE - ^ | - | | ATC command - | HB Timeout V - +<------------- VC_STATE_SEND_PING - ^ | - | | TX PING - | | TX Complete - | HB Timeout V - +<------------- VC_STATE_WAIT_ACK1 + .-------------> VC_STATE_LINK_DOWN <------------------------------------. + | | | + | | HEARTBEAT received | + | HB Timeout v | + +<----------- VC_STATE_LINK_ALIVE | + ^ | | + | | ATC command | + | HB Timeout V | + +<------------- VC_STATE_SEND_PING--------------------. | + ^ | | | + | | TX PING | | + | | TX Complete | HB Timeout | + | HB Timeout V | | + +<------------- VC_STATE_WAIT_ACK1 | | + | | + | TX ACK1 | + | TX Complete | + V | + VC_STATE_WAIT_ACK2 -------' */ @@ -1778,6 +1783,16 @@ void updateRadioState() changeState(RADIO_VC_WAIT_TX_DONE); break; + //Second step in 3-way handshake, received PING, respond with ACK1 + case DATAGRAM_PING: + if (xmitVcAck1(rxSrcVc)) + { + changeState(RADIO_VC_WAIT_TX_DONE); + vcChangeState(rxSrcVc, VC_STATE_CONNECTED); + virtualCircuitList[rxSrcVc].lastPingMillis = datagramTimer; + } + break; + case DATAGRAM_REMOTE_COMMAND: rmtCmdVc = rxSrcVc; From 49155ce15051bc7db031e08e33a74830b09bfc9d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 19:35:14 -1000 Subject: [PATCH 222/594] VC: Received the ACK1, send ACK2 to the remote system --- Firmware/LoRaSerial_Firmware/Radio.ino | 25 +++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 49 +++++++++++++++++-------- 2 files changed, 58 insertions(+), 16 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 99ed3949..499b5c8e 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1332,6 +1332,31 @@ bool xmitVcAck1(int8_t destVc) return (transmitDatagram()); } +bool xmitVcAck2(int8_t destVc) +{ + VC_RADIO_MESSAGE_HEADER * vcHeader; + + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; + vcHeader->length = VC_RADIO_HEADER_BYTES; + vcHeader->destVc = destVc; + vcHeader->srcVc = myVc; + endOfTxData += VC_RADIO_HEADER_BYTES; + + /* + endOfTxData ---. + | + V + +--------+---------+--------+----------+---------+----------+ + | | | | | | Optional | + | NET ID | Control | Length | DestAddr | SrcAddr | Trailer | + | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | n Bytes | + +--------+---------+--------+----------+---------+----------+ + */ + + txControl.datagramType = DATAGRAM_ACK_2; + return (transmitDatagram()); +} + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Datagram reception //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9ede93dc..93bdd417 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1638,11 +1638,15 @@ void updateRadioState() | | TX Complete | HB Timeout | | HB Timeout V | | +<------------- VC_STATE_WAIT_ACK1 | | - | | - | TX ACK1 | - | TX Complete | - V | - VC_STATE_WAIT_ACK2 -------' + ^ | | | + | | RX ACK1 | TX ACK1 | + | | TX ACK2 | TX Complete | + | | TX Complete V | + | | VC_STATE_WAIT_ACK2 -------' + | | + | | Zero ACK values + | HB Timeout V + '-------------- VC_STATE_CONNECTED */ @@ -1783,16 +1787,26 @@ void updateRadioState() changeState(RADIO_VC_WAIT_TX_DONE); break; - //Second step in 3-way handshake, received PING, respond with ACK1 + //Second step in the 3-way handshake, received PING, respond with ACK1 case DATAGRAM_PING: if (xmitVcAck1(rxSrcVc)) { changeState(RADIO_VC_WAIT_TX_DONE); - vcChangeState(rxSrcVc, VC_STATE_CONNECTED); + vcChangeState(rxSrcVc, VC_STATE_WAIT_FOR_ACK2); virtualCircuitList[rxSrcVc].lastPingMillis = datagramTimer; } break; + //Third step in the 3-way handshake, received ACK1, respond with ACK2 + case DATAGRAM_ACK_1: + if (xmitVcAck2(rxSrcVc)) + { + changeState(RADIO_VC_WAIT_TX_DONE); + vcZeroAcks(rxSrcVc); + vcChangeState(rxSrcVc, VC_STATE_CONNECTED); + } + break; + case DATAGRAM_REMOTE_COMMAND: rmtCmdVc = rxSrcVc; @@ -2617,20 +2631,23 @@ int8_t vcLinkAlive(int8_t vcIndex) if (vc->vcState == VC_STATE_LINK_DOWN) { vc->firstHeartbeatMillis = millis(); - vcChangeState(vcIndex, VC_STATE_LINK_ALIVE); - - //Reset the ACK counters - vc->txAckNumber = 0; - vc->rmtTxAckNumber = 0; - vc->rxAckNumber = 0; - } - //Update the link status - if (vc->vcState == VC_STATE_LINK_DOWN) + //Update the link status vcChangeState(vcIndex, VC_STATE_LINK_ALIVE); + } return vcIndex; } +void vcZeroAcks(int8_t vcIndex) +{ + VIRTUAL_CIRCUIT * vc = &virtualCircuitList[vcIndex]; + + //Reset the ACK counters + vc->txAckNumber = 0; + vc->rmtTxAckNumber = 0; + vc->rxAckNumber = 0; +} + //Translate the UNIQUE ID value into a VC number to reduce the communications overhead int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) { From ac83dc4fe323fc19ece551eb245be485c18b2356 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 17:31:16 -1000 Subject: [PATCH 223/594] VC: Received ACK2, zero the ACK values --- Firmware/LoRaSerial_Firmware/States.ino | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 93bdd417..e6691f02 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1643,10 +1643,10 @@ void updateRadioState() | | TX ACK2 | TX Complete | | | TX Complete V | | | VC_STATE_WAIT_ACK2 -------' - | | - | | Zero ACK values - | HB Timeout V - '-------------- VC_STATE_CONNECTED + | | | + | | Zero ACK values | RX ACK2 + | HB Timeout V | Zero ACK values + '-------------- VC_STATE_CONNECTED <------------------' */ @@ -1807,6 +1807,12 @@ void updateRadioState() } break; + //Last step in the 3-way handshake, received ACK2, done + case DATAGRAM_ACK_2: + vcZeroAcks(rxSrcVc); + vcChangeState(rxSrcVc, VC_STATE_CONNECTED); + break; + case DATAGRAM_REMOTE_COMMAND: rmtCmdVc = rxSrcVc; From 05af622d73a0436ec718c053026b5f9fdc3c82a4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 12:26:08 -1000 Subject: [PATCH 224/594] VC: Don't set vcConnecting when in VC_STATE_LINK_ALIVE --- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index e6691f02..ea18288c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2592,7 +2592,7 @@ void vcChangeState(int8_t vcIndex, uint8_t state) //Determine if the VC is connecting vcBit = 1 << vcIndex; - if ((state >= VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) + if ((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) vcConnecting |= vcBit; else vcConnecting &= vcBit; From 34ed47d565ba77a0e2efcffc244155868dda9dae Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 18:22:41 -1000 Subject: [PATCH 225/594] VC: Update the VC state diagram --- Firmware/LoRaSerial_Firmware/States.ino | 46 ++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ea18288c..e5db049c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1624,29 +1624,29 @@ void updateRadioState() VC States: - .-------------> VC_STATE_LINK_DOWN <------------------------------------. - | | | - | | HEARTBEAT received | - | HB Timeout v | - +<----------- VC_STATE_LINK_ALIVE | - ^ | | - | | ATC command | - | HB Timeout V | - +<------------- VC_STATE_SEND_PING--------------------. | - ^ | | | - | | TX PING | | - | | TX Complete | HB Timeout | - | HB Timeout V | | - +<------------- VC_STATE_WAIT_ACK1 | | - ^ | | | - | | RX ACK1 | TX ACK1 | - | | TX ACK2 | TX Complete | - | | TX Complete V | - | | VC_STATE_WAIT_ACK2 -------' - | | | - | | Zero ACK values | RX ACK2 - | HB Timeout V | Zero ACK values - '-------------- VC_STATE_CONNECTED <------------------' + .-------------> VC_STATE_LINK_DOWN <-------------------------------------. + | | HB Timeout | + | | HEARTBEAT received | + | HB Timeout v | + +<----------- VC_STATE_LINK_ALIVE ------------------------. | + ^ | RX PING | | + | | ATC command | | + | HB Timeout V | | + +<------------- VC_STATE_SEND_PING | | + ^ | | | + | | TX PING | | + | | TX Complete | | + | HB Timeout V | | + +<------------- VC_STATE_WAIT_ACK1 | | + ^ | | | + | | RX ACK1 | TX ACK1 | + | | TX ACK2 | TX Complete | + | | TX Complete V | + | Zero ACK values | VC_STATE_WAIT_ACK2 ----' + | | | + | | | RX ACK2 + | HB Timeout V | Zero ACK values + '-------------- VC_STATE_CONNECTED <----------------------' */ From 1e089ac8d55d912709bbdcd131d66c4dd932244c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 6 Dec 2022 11:49:29 -1000 Subject: [PATCH 226/594] VC: Handle other states receiving a ping --- Firmware/LoRaSerial_Firmware/States.ino | 28 ++++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index e5db049c..3039583b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1635,17 +1635,17 @@ void updateRadioState() +<------------- VC_STATE_SEND_PING | | ^ | | | | | TX PING | | - | | TX Complete | | - | HB Timeout V | | - +<------------- VC_STATE_WAIT_ACK1 | | - ^ | | | - | | RX ACK1 | TX ACK1 | - | | TX ACK2 | TX Complete | - | | TX Complete V | - | Zero ACK values | VC_STATE_WAIT_ACK2 ----' - | | | - | | | RX ACK2 - | HB Timeout V | Zero ACK values + | | TX Complete RX | | + | HB Timeout V PING V | + +<------------- VC_STATE_WAIT_ACK1 --------->+----------->+ | + | | ^ | | + | | RX ACK1 | | TX ACK1 | + | | TX ACK2 | | TX Complete | + | | TX Complete | V | + | Zero ACK values | | VC_STATE_WAIT_ACK2 ----' + | | .-------------' | + | | | RX PING | RX ACK2 + | HB Timeout V | | Zero ACK values '-------------- VC_STATE_CONNECTED <----------------------' */ @@ -1790,11 +1790,9 @@ void updateRadioState() //Second step in the 3-way handshake, received PING, respond with ACK1 case DATAGRAM_PING: if (xmitVcAck1(rxSrcVc)) - { changeState(RADIO_VC_WAIT_TX_DONE); - vcChangeState(rxSrcVc, VC_STATE_WAIT_FOR_ACK2); - virtualCircuitList[rxSrcVc].lastPingMillis = datagramTimer; - } + vcChangeState(rxSrcVc, VC_STATE_WAIT_FOR_ACK2); + virtualCircuitList[rxSrcVc].lastPingMillis = datagramTimer; break; //Third step in the 3-way handshake, received ACK1, respond with ACK2 From a63150a1a932f7bcc0824f735c1d138ddd0920f2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 7 Dec 2022 05:20:05 -1000 Subject: [PATCH 227/594] Display the current radio state in ATI10 --- Firmware/LoRaSerial_Firmware/Commands.ino | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 4f0e2b5a..0f964c8c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -262,10 +262,10 @@ bool commandAT(const char * commandString) systemPrint(" Lost Frames: "); systemPrintln(lostFrames); systemPrintln(" Received"); - systemPrint(" Datagrams: "); - systemPrintln(datagramsReceived); systemPrint(" Frames: "); systemPrintln(framesReceived); + systemPrint(" Datagrams: "); + systemPrintln(datagramsReceived); systemPrint(" Bad CRC: "); systemPrintln(badCrc); systemPrint(" Bad Frames: "); @@ -287,6 +287,11 @@ bool commandAT(const char * commandString) systemPrint(" Last TX ACK number: "); systemPrintln(vc->txAckNumber); } + systemPrintln(" Radio"); + systemPrint(" transactionComplete: "); + systemPrintln(transactionComplete ? "True" : "False"); + systemPrint(" receiveInProcess: "); + systemPrintln(receiveInProcess() ? "True" : "False"); reportOK(); break; case ('1'): //ATI11 - Return myVc value From 9b804fdf137105707380f108051267fe4b387c61 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 7 Dec 2022 05:48:02 -1000 Subject: [PATCH 228/594] Remember last successful and failure TX and RX --- Firmware/LoRaSerial_Firmware/Commands.ino | 35 +++++++++++ .../LoRaSerial_Firmware.ino | 9 +++ Firmware/LoRaSerial_Firmware/Radio.ino | 59 +++++++++++-------- 3 files changed, 79 insertions(+), 24 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 0f964c8c..0b287e0d 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -288,10 +288,45 @@ bool commandAT(const char * commandString) systemPrintln(vc->txAckNumber); } systemPrintln(" Radio"); + systemPrint(" Last Successful Transmit: "); + if (txSuccessMillis) + { + systemPrintTimestamp(txSuccessMillis); + systemPrintln(); + } + else + systemPrintln("None"); + systemPrint(" Last Transmit Failure: "); + if (txFailureMillis) + { + systemPrintTimestamp(txFailureMillis); + systemPrint(", State: "); + systemPrintln(txFailureState); + } + else + systemPrintln("None"); + systemPrint(" Last Successful Receive: "); + if (rxSuccessMillis) + { + systemPrintTimestamp(rxSuccessMillis); + systemPrintln(); + } + else + systemPrintln("None"); + systemPrint(" Last Receive Failure: "); + if (rxFailureMillis) + { + systemPrintTimestamp(rxFailureMillis); + systemPrint(", State: "); + systemPrintln(rxFailureState); + } + else + systemPrintln("None"); systemPrint(" transactionComplete: "); systemPrintln(transactionComplete ? "True" : "False"); systemPrint(" receiveInProcess: "); systemPrintln(receiveInProcess() ? "True" : "False"); + reportOK(); break; case ('1'): //ATI11 - Return myVc value diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 790bbfc4..614a5d30 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -369,6 +369,15 @@ uint32_t netIdMismatch; //Total number of mismatched Net ID frames unsigned long lastLinkUpTime = 0; //Mark when link was first established unsigned long lastRxDatagram; //Remember last valid receive +//Receiver and Transmitter status +unsigned long rxSuccessMillis; +unsigned long rxFailureMillis; +int rxFailureState; + +unsigned long txSuccessMillis; +unsigned long txFailureMillis; +int txFailureState; + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - Radio Protocol diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 499b5c8e..369d28fc 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1383,30 +1383,35 @@ PacketType rcvDatagram() if (state == RADIOLIB_ERR_NONE) { - //Do nothing + rxSuccessMillis = rcvTimeMillis; } - else if (state == RADIOLIB_ERR_CRC_MISMATCH) + else { - if (settings.debug || settings.debugDatagrams || settings.debugReceive) + rxFailureMillis = rcvTimeMillis; + rxFailureState = state; + if (state == RADIOLIB_ERR_CRC_MISMATCH) { - systemPrintln("Receive CRC error!"); - outputSerialData(true); + if (settings.debug || settings.debugDatagrams || settings.debugReceive) + { + systemPrintln("Receive CRC error!"); + outputSerialData(true); + } + badCrc++; + returnToReceiving(); //Return to listening + return (DATAGRAM_CRC_ERROR); } - badCrc++; - returnToReceiving(); //Return to listening - return (DATAGRAM_CRC_ERROR); - } - else - { - if (settings.debug || settings.debugDatagrams || settings.debugReceive) + else { - systemPrint("Receive error: "); - systemPrintln(state); - outputSerialData(true); + if (settings.debug || settings.debugDatagrams || settings.debugReceive) + { + systemPrint("Receive error: "); + systemPrintln(state); + outputSerialData(true); + } + returnToReceiving(); //Return to listening + badFrames++; + return (DATAGRAM_BAD); } - returnToReceiving(); //Return to listening - badFrames++; - return (DATAGRAM_BAD); } rxDataBytes = radio.getPacketLength(); @@ -2521,11 +2526,12 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) if (state == RADIOLIB_ERR_NONE) { + xmitTimeMillis = millis(); + txSuccessMillis = xmitTimeMillis; frameSentCount++; if (vc) vc->framesSent++; framesSent++; - xmitTimeMillis = millis(); if (settings.debugTransmit) { systemPrintTimestamp(); @@ -2545,12 +2551,17 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) hopChannel(); } } - else if (settings.debugTransmit) + else { - systemPrintTimestamp(); - systemPrint("TX: Transmit error, state "); - systemPrintln(state); - outputSerialData(true); + txFailureMillis = millis(); + txFailureState = state; + if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint("TX: Transmit error, state "); + systemPrintln(state); + outputSerialData(true); + } } } frameAirTime += responseDelay; From d735d49e69a3bca8a897fc5e2f13dfb2243964b4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 7 Dec 2022 06:19:20 -1000 Subject: [PATCH 229/594] Display the names for the various radio status values --- Firmware/LoRaSerial_Firmware/Commands.ino | 23 ++++++++++--- Firmware/LoRaSerial_Firmware/Radio.ino | 41 +++++++++++++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 6 ++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 0b287e0d..b9ccc3b9 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -34,6 +34,7 @@ typedef struct bool commandAT(const char * commandString) { uint32_t delayMillis; + const char * string; unsigned long timer; VIRTUAL_CIRCUIT * vc = &virtualCircuitList[cmdVc]; @@ -300,8 +301,15 @@ bool commandAT(const char * commandString) if (txFailureMillis) { systemPrintTimestamp(txFailureMillis); - systemPrint(", State: "); - systemPrintln(txFailureState); + systemPrint(", Status: "); + systemPrint(txFailureState); + string = getRadioStatusCode(txFailureState); + if (string) + { + systemPrint(", "); + systemPrint(string); + } + systemPrintln(); } else systemPrintln("None"); @@ -317,8 +325,15 @@ bool commandAT(const char * commandString) if (rxFailureMillis) { systemPrintTimestamp(rxFailureMillis); - systemPrint(", State: "); - systemPrintln(rxFailureState); + systemPrint(", Status: "); + systemPrint(rxFailureState); + string = getRadioStatusCode(rxFailureState); + if (string) + { + systemPrint(", "); + systemPrint(string); + } + systemPrintln(); } else systemPrintln("None"); diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 369d28fc..8910b860 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2843,3 +2843,44 @@ void setVcHeartbeatTimer() petWDT(); } } + +//Conversion table from radio status value into a status string +const U16_TO_STRING radioStatusCodes[] = +{ + {RADIOLIB_ERR_NONE, "RADIOLIB_ERR_NONE"}, + {RADIOLIB_ERR_UNKNOWN, "RADIOLIB_ERR_UNKNOWN"}, + {RADIOLIB_ERR_CHIP_NOT_FOUND, "RADIOLIB_ERR_CHIP_NOT_FOUND"}, + {RADIOLIB_ERR_MEMORY_ALLOCATION_FAILED, "RADIOLIB_ERR_MEMORY_ALLOCATION_FAILED"}, + {RADIOLIB_ERR_PACKET_TOO_LONG, "RADIOLIB_ERR_PACKET_TOO_LONG"}, + {RADIOLIB_ERR_TX_TIMEOUT, "RADIOLIB_ERR_TX_TIMEOUT"}, + {RADIOLIB_ERR_RX_TIMEOUT, "RADIOLIB_ERR_RX_TIMEOUT"}, + {RADIOLIB_ERR_CRC_MISMATCH, "RADIOLIB_ERR_CRC_MISMATCH"}, + {RADIOLIB_ERR_INVALID_BANDWIDTH, "RADIOLIB_ERR_INVALID_BANDWIDTH"}, + {RADIOLIB_ERR_INVALID_SPREADING_FACTOR, "RADIOLIB_ERR_INVALID_SPREADING_FACTOR"}, + {RADIOLIB_ERR_INVALID_CODING_RATE, "RADIOLIB_ERR_INVALID_CODING_RATE"}, + {RADIOLIB_ERR_INVALID_BIT_RANGE, "RADIOLIB_ERR_INVALID_BIT_RANGE"}, + {RADIOLIB_ERR_INVALID_FREQUENCY, "RADIOLIB_ERR_INVALID_FREQUENCY"}, + {RADIOLIB_ERR_INVALID_OUTPUT_POWER, "RADIOLIB_ERR_INVALID_OUTPUT_POWER"}, + {RADIOLIB_PREAMBLE_DETECTED, "RADIOLIB_PREAMBLE_DETECTED"}, + {RADIOLIB_CHANNEL_FREE, "RADIOLIB_CHANNEL_FREE"}, + {RADIOLIB_ERR_SPI_WRITE_FAILED, "RADIOLIB_ERR_SPI_WRITE_FAILED"}, + {RADIOLIB_ERR_INVALID_CURRENT_LIMIT, "RADIOLIB_ERR_INVALID_CURRENT_LIMIT"}, + {RADIOLIB_ERR_INVALID_PREAMBLE_LENGTH, "RADIOLIB_ERR_INVALID_PREAMBLE_LENGTH"}, + {RADIOLIB_ERR_INVALID_GAIN, "RADIOLIB_ERR_INVALID_GAIN"}, + {RADIOLIB_ERR_WRONG_MODEM, "RADIOLIB_ERR_WRONG_MODEM"}, + {RADIOLIB_ERR_INVALID_NUM_SAMPLES, "RADIOLIB_ERR_INVALID_NUM_SAMPLES"}, + {RADIOLIB_ERR_INVALID_RSSI_OFFSET, "RADIOLIB_ERR_INVALID_RSSI_OFFSET"}, + {RADIOLIB_ERR_INVALID_ENCODING, "RADIOLIB_ERR_INVALID_ENCODING"}, + {RADIOLIB_ERR_LORA_HEADER_DAMAGED, "RADIOLIB_ERR_LORA_HEADER_DAMAGED"}, +}; + +//Return the status string matching the status value, return NULL if not found +const char * getRadioStatusCode(int status) +{ + for (int index = 0; index < sizeof(radioStatusCodes) / sizeof(radioStatusCodes[0]); index++) + { + if (radioStatusCodes[index].value == status) + return radioStatusCodes[index].string; + } + return NULL; +} diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 22d64870..c6070280 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -463,3 +463,9 @@ typedef struct _ARCH_TABLE ARCH_SYSTEM_RESET systemReset; //Reset the system ARCH_UNIQUE_ID uniqueID; //Get the 128 bit unique ID value } ARCH_TABLE; + +typedef struct _U16_TO_STRING +{ + uint16_t value; + const char * string; +} U16_TO_STRING; From 7dbd4adb5f74d1080d2b97edcc22707d027df443 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 7 Dec 2022 08:19:28 -1000 Subject: [PATCH 230/594] Display the radio call history --- Firmware/LoRaSerial_Firmware/Commands.ino | 3 +- .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/Radio.ino | 196 +++++++++++++++++- Firmware/LoRaSerial_Firmware/System.ino | 3 + Firmware/LoRaSerial_Firmware/settings.h | 56 +++++ 5 files changed, 258 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index b9ccc3b9..06e7b220 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -341,7 +341,8 @@ bool commandAT(const char * commandString) systemPrintln(transactionComplete ? "True" : "False"); systemPrint(" receiveInProcess: "); systemPrintln(receiveInProcess() ? "True" : "False"); - + systemPrintln(" Call History"); + displayRadioCallHistory(); reportOK(); break; case ('1'): //ATI11 - Return myVc value diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 614a5d30..7ed4c3cd 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -378,6 +378,8 @@ unsigned long txSuccessMillis; unsigned long txFailureMillis; int txFailureState; +//History +unsigned long radioCallHistory[RADIO_CALL_MAX]; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - Radio Protocol diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 8910b860..e0478d76 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -4,6 +4,8 @@ void configureRadio() { bool success = true; + radioCallHistory[RADIO_CALL_configureRadio] = millis(); + channelNumber = 0; if (!setRadioFrequency(false)) success = false; @@ -176,6 +178,8 @@ bool setRadioFrequency(bool rxAdjust) { float frequency; + radioCallHistory[RADIO_CALL_setRadioFrequency] = millis(); + //Determine the frequency to use frequency = channels[channelNumber]; if (rxAdjust && settings.autoTuneFrequency) @@ -207,6 +211,8 @@ bool setRadioFrequency(bool rxAdjust) //Place the radio in receive mode void returnToReceiving() { + radioCallHistory[RADIO_CALL_returnToReceiving] = millis(); + if (receiveInProcess() == true) return; //Do not touch the radio if it is already receiving int state; @@ -243,6 +249,8 @@ void returnToReceiving() //Given spread factor, bandwidth, coding rate and number of bytes, return total Air Time in ms for packet uint16_t calcAirTime(uint8_t bytesToSend) { + radioCallHistory[RADIO_CALL_calcAirTime] = millis(); + float tSym = calcSymbolTime(); float tPreamble = (settings.radioPreambleLength + 4.25) * tSym; float p1 = (8 * bytesToSend - 4 * settings.radioSpreadFactor + 28 + 16 * 1 - 20 * 0) / (4.0 * (settings.radioSpreadFactor - 2 * 0)); @@ -482,6 +490,8 @@ uint8_t covertdBmToSetting(uint8_t userSetting) //Read a register from the SX1276 chip uint8_t readSX1276Register(uint8_t reg) { + radioCallHistory[RADIO_CALL_readSX1276Register] = millis(); + return radio._mod->SPIreadRegister(reg); } @@ -495,6 +505,8 @@ void printSX1276Registers () 0x07, 0x28, 0x00, 0x08, 0x1e, 0x00, 0x01, 0x00 }; + radioCallHistory[RADIO_CALL_printSX1276Registers] = millis(); + systemPrint("Registers:"); for (uint8_t i = 0; i < (sizeof(valid_regs) * 8); i++) { @@ -515,6 +527,8 @@ void printSX1276Registers () //Called when transmission is complete or when RX is received void transactionCompleteISR(void) { + radioCallHistory[RADIO_CALL_transactionCompleteISR] = millis(); + transactionComplete = true; } @@ -524,6 +538,8 @@ void transactionCompleteISR(void) //We use the hop ISR to measure RSSI during reception void hopISR(void) { + radioCallHistory[RADIO_CALL_hopISR] = millis(); + hop = true; } @@ -621,6 +637,8 @@ const uint16_t crc16Table[256] = //Ping the other radio in the point-to-point configuration bool xmitDatagramP2PTrainingPing() { + radioCallHistory[RADIO_CALL_xmitDatagramP2PTrainingPing] = millis(); + /* endOfTxData ---. | @@ -641,6 +659,8 @@ bool xmitDatagramP2pTrainingParams() { Settings params; + radioCallHistory[RADIO_CALL_xmitDatagramP2pTrainingParams] = millis(); + //Initialize the radio parameters memcpy(¶ms, &originalSettings, sizeof(settings)); params.operatingMode = MODE_POINT_TO_POINT; @@ -723,6 +743,8 @@ bool xmitDatagramP2pTrainingParams() //First packet in the three way handshake to bring up the link bool xmitDatagramP2PPing() { + radioCallHistory[RADIO_CALL_xmitDatagramP2PPing] = millis(); + unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(unsigned long); @@ -745,6 +767,8 @@ bool xmitDatagramP2PPing() //Second packet in the three way handshake to bring up the link bool xmitDatagramP2PAck1() { + radioCallHistory[RADIO_CALL_xmitDatagramP2PAck1] = millis(); + unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(unsigned long); @@ -767,6 +791,8 @@ bool xmitDatagramP2PAck1() //Last packet in the three way handshake to bring up the link bool xmitDatagramP2PAck2() { + radioCallHistory[RADIO_CALL_xmitDatagramP2PAck2] = millis(); + unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(unsigned long); @@ -793,6 +819,8 @@ bool xmitDatagramP2PAck2() //Send a command datagram to the remote system bool xmitDatagramP2PCommand() { + radioCallHistory[RADIO_CALL_xmitDatagramP2PCommand] = millis(); + /* endOfTxData ---. | @@ -811,6 +839,8 @@ bool xmitDatagramP2PCommand() //Send a command response datagram to the remote system bool xmitDatagramP2PCommandResponse() { + radioCallHistory[RADIO_CALL_xmitDatagramP2PCommandResponse] = millis(); + /* endOfTxData ---. | @@ -829,6 +859,8 @@ bool xmitDatagramP2PCommandResponse() //Send a data datagram to the remote system bool xmitDatagramP2PData() { + radioCallHistory[RADIO_CALL_xmitDatagramP2PData] = millis(); + /* endOfTxData ---. | @@ -847,6 +879,8 @@ bool xmitDatagramP2PData() //Heartbeat packet to keep the link up bool xmitDatagramP2PHeartbeat() { + radioCallHistory[RADIO_CALL_xmitDatagramP2PHeartbeat] = millis(); + unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(currentMillis); @@ -871,6 +905,8 @@ bool xmitDatagramP2PAck() { int ackLength; + radioCallHistory[RADIO_CALL_xmitDatagramP2PAck] = millis(); + uint8_t * ackStart = endOfTxData; uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); @@ -907,6 +943,8 @@ bool xmitDatagramP2PAck() //Send a data datagram to the remote system, including sync data bool xmitDatagramMpData() { + radioCallHistory[RADIO_CALL_xmitDatagramMpData] = millis(); + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); endOfTxData += sizeof(msToNextHop); @@ -931,6 +969,8 @@ bool xmitDatagramMpData() //Heartbeat packet to sync other units in multipoint mode bool xmitDatagramMpHeartbeat() { + radioCallHistory[RADIO_CALL_xmitDatagramMpHeartbeat] = millis(); + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); endOfTxData += sizeof(msToNextHop); @@ -955,6 +995,8 @@ bool xmitDatagramMpHeartbeat() //The channel Number ensures that the client gets the next hop correct bool xmitDatagramMpAck() { + radioCallHistory[RADIO_CALL_xmitDatagramMpAck] = millis(); + memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); endOfTxData += sizeof(channelNumber); @@ -981,6 +1023,8 @@ bool xmitDatagramMpAck() //Ping packet sent during scanning bool xmitDatagramMpPing() { + radioCallHistory[RADIO_CALL_xmitDatagramMpPing] = millis(); + /* endOfTxData ---. | @@ -1003,6 +1047,8 @@ bool xmitDatagramMpPing() //Build the client ping packet used for training bool xmitDatagramMpTrainingPing() { + radioCallHistory[RADIO_CALL_xmitDatagramMpTrainingPing] = millis(); + //Add the source (server) ID memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); endOfTxData += UNIQUE_ID_BYTES; @@ -1025,6 +1071,8 @@ bool xmitDatagramMpTrainingPing() //Build the client ACK packet used for training bool xmitDatagramMpTrainingAck(uint8_t * serverID) { + radioCallHistory[RADIO_CALL_xmitDatagramMpTrainingAck] = millis(); + //Add the destination (server) ID memcpy(endOfTxData, serverID, UNIQUE_ID_BYTES); endOfTxData += UNIQUE_ID_BYTES; @@ -1151,6 +1199,8 @@ bool xmitDatagramMpRadioParameters(const uint8_t * clientID) { Settings params; + radioCallHistory[RADIO_CALL_xmitDatagramMpRadioParameters] = millis(); + //Initialize the radio parameters memcpy(¶ms, &originalSettings, sizeof(settings)); params.server = false; @@ -1189,6 +1239,8 @@ bool xmitDatagramMpRadioParameters(const uint8_t * clientID) //Broadcast a datagram to all of the VCs bool xmitVcDatagram() { + radioCallHistory[RADIO_CALL_xmitVcDatagram] = millis(); + /* endOfTxData ---. | @@ -1211,6 +1263,8 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) uint32_t currentMillis = millis(); uint8_t * txData; + radioCallHistory[RADIO_CALL_xmitVcHeartbeat] = currentMillis; + //Build the VC header txData = endOfTxData; *endOfTxData++ = 0; //Reserve for length @@ -1261,6 +1315,8 @@ bool xmitVcAckFrame(int8_t destVc) { VC_RADIO_MESSAGE_HEADER * vcHeader; + radioCallHistory[RADIO_CALL_xmitVcAckFrame] = millis(); + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; vcHeader->destVc = destVc; @@ -1286,6 +1342,8 @@ bool xmitVcPing(int8_t destVc) { VC_RADIO_MESSAGE_HEADER * vcHeader; + radioCallHistory[RADIO_CALL_xmitVcPing] = millis(); + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; vcHeader->length = VC_RADIO_HEADER_BYTES; vcHeader->destVc = destVc; @@ -1311,6 +1369,8 @@ bool xmitVcAck1(int8_t destVc) { VC_RADIO_MESSAGE_HEADER * vcHeader; + radioCallHistory[RADIO_CALL_xmitVcAck1] = millis(); + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; vcHeader->length = VC_RADIO_HEADER_BYTES; vcHeader->destVc = destVc; @@ -1336,6 +1396,8 @@ bool xmitVcAck2(int8_t destVc) { VC_RADIO_MESSAGE_HEADER * vcHeader; + radioCallHistory[RADIO_CALL_xmitVcAck2] = millis(); + vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; vcHeader->length = VC_RADIO_HEADER_BYTES; vcHeader->destVc = destVc; @@ -1371,6 +1433,8 @@ PacketType rcvDatagram() VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; + radioCallHistory[RADIO_CALL_rcvDatagram] = millis(); + //Acknowledge the receive interrupt transactionComplete = false; @@ -2077,6 +2141,8 @@ bool transmitDatagram() VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; + radioCallHistory[RADIO_CALL_transmitDatagram] = millis(); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -2461,6 +2527,8 @@ void printControl(uint8_t value) //Returns false if we could not start tranmission due to packet received or RX in process bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) { + radioCallHistory[RADIO_CALL_retransmitDatagram] = millis(); + /* +----------+----------+------------+--- ... ---+----------+ | Optional | | Optional | | Optional | @@ -2584,6 +2652,8 @@ void startChannelTimer() //Use the specified value to start the timer that indicates when to hop channels void startChannelTimer(int16_t startAmount) { + radioCallHistory[RADIO_CALL_startChannelTimer] = millis(); + channelTimer.disableTimer(); channelTimer.setInterval_MS(startAmount, channelTimerHandler); channelTimer.enableTimer(); @@ -2594,6 +2664,8 @@ void startChannelTimer(int16_t startAmount) //Stop the channel (hop) timer void stopChannelTimer() { + radioCallHistory[RADIO_CALL_stopChannelTimer] = millis(); + channelTimer.disableTimer(); triggerEvent(TRIGGER_HOP_TIMER_STOP); } @@ -2603,6 +2675,9 @@ void stopChannelTimer() void syncChannelTimer() { int16_t msToNextHopRemote; //Can be negative + + radioCallHistory[RADIO_CALL_syncChannelTimer] = millis(); + memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); msToNextHopRemote -= ackAirTime; msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; @@ -2755,6 +2830,8 @@ void syncChannelTimer() void setHeartbeatShort() { heartbeatTimer = millis(); + radioCallHistory[RADIO_CALL_setHeartbeatShort] = heartbeatTimer; + heartbeatRandomTime = random(settings.heartbeatTimeout * 2 / 10, settings.heartbeatTimeout / 2); //20-50% //Slow datarates can have significant ack transmission times @@ -2765,6 +2842,8 @@ void setHeartbeatShort() void setHeartbeatLong() { heartbeatTimer = millis(); + radioCallHistory[RADIO_CALL_setHeartbeatLong] = heartbeatTimer; + heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% //Slow datarates can have significant ack transmission times @@ -2777,6 +2856,8 @@ void setHeartbeatLong() void setHeartbeatMultipoint() { heartbeatTimer = millis(); + radioCallHistory[RADIO_CALL_setHeartbeatMultipoint] = heartbeatTimer; + heartbeatRandomTime = settings.heartbeatTimeout; //Slow datarates can have significant ack transmission times @@ -2789,6 +2870,9 @@ void setVcHeartbeatTimer() { long deltaMillis; + heartbeatTimer = millis(); + radioCallHistory[RADIO_CALL_setVcHeartbeatTimer] = heartbeatTimer; + /* * The goal of this routine is to randomize the placement of the HEARTBEAT * messages, allowing traffic to flow normally. However since clients are @@ -2813,7 +2897,6 @@ void setVcHeartbeatTimer() petWDT(); //Determine the delay before channel zero is reached - heartbeatTimer = millis(); deltaMillis = nextChannelZeroTimeInMillis - heartbeatTimer; if (deltaMillis <= 0) { @@ -2884,3 +2967,114 @@ const char * getRadioStatusCode(int status) } return NULL; } + +//Conversion table from radio call value into a name string +const U16_TO_STRING radioCallName[] = +{ + {RADIO_CALL_configureRadio, "configureRadio"}, + {RADIO_CALL_setRadioFrequency, "setRadioFrequency"}, + {RADIO_CALL_returnToReceiving, "returnToReceiving"}, + {RADIO_CALL_calcAirTime, "calcAirTime"}, + {RADIO_CALL_xmitDatagramP2PTrainingPing, "xmitDatagramP2PTrainingPing"}, + {RADIO_CALL_xmitDatagramP2pTrainingParams, "xmitDatagramP2pTrainingParams"}, + {RADIO_CALL_xmitDatagramP2PPing, "xmitDatagramP2PPing"}, + {RADIO_CALL_xmitDatagramP2PAck1, "xmitDatagramP2PAck1"}, + {RADIO_CALL_xmitDatagramP2PAck2, "xmitDatagramP2PAck2"}, + {RADIO_CALL_xmitDatagramP2PCommand, "xmitDatagramP2PCommand"}, + {RADIO_CALL_xmitDatagramP2PCommandResponse, "xmitDatagramP2PCommandResponse"}, + {RADIO_CALL_xmitDatagramP2PData, "xmitDatagramP2PData"}, + {RADIO_CALL_xmitDatagramP2PHeartbeat, "xmitDatagramP2PHeartbeat"}, + {RADIO_CALL_xmitDatagramP2PAck, "xmitDatagramP2PAck"}, + {RADIO_CALL_xmitDatagramMpData, "xmitDatagramMpData"}, + {RADIO_CALL_xmitDatagramMpHeartbeat, "xmitDatagramMpHeartbeat"}, + {RADIO_CALL_xmitDatagramMpAck, "xmitDatagramMpAck"}, + {RADIO_CALL_xmitDatagramMpPing, "xmitDatagramMpPing"}, + {RADIO_CALL_xmitDatagramMpTrainingPing, "xmitDatagramMpTrainingPing"}, + {RADIO_CALL_xmitDatagramMpTrainingAck, "xmitDatagramMpTrainingAck"}, + {RADIO_CALL_xmitDatagramMpRadioParameters, "xmitDatagramMpRadioParameters"}, + {RADIO_CALL_xmitVcDatagram, "xmitVcDatagram"}, + {RADIO_CALL_xmitVcHeartbeat, "xmitVcHeartbeat"}, + {RADIO_CALL_xmitVcAckFrame, "xmitVcAckFrame"}, + {RADIO_CALL_xmitVcPing, "xmitVcPing"}, + {RADIO_CALL_xmitVcAck1, "xmitVcAck1"}, + {RADIO_CALL_xmitVcAck2, "xmitVcAck2"}, + {RADIO_CALL_rcvDatagram, "rcvDatagram"}, + {RADIO_CALL_transmitDatagram, "transmitDatagram"}, + {RADIO_CALL_retransmitDatagram, "retransmitDatagram"}, + {RADIO_CALL_startChannelTimer, "startChannelTimer"}, + {RADIO_CALL_stopChannelTimer, "stopChannelTimer"}, + {RADIO_CALL_syncChannelTimer, "syncChannelTimer"}, + {RADIO_CALL_setHeartbeatShort, "setHeartbeatShort"}, + {RADIO_CALL_setHeartbeatLong, "setHeartbeatLong"}, + {RADIO_CALL_setHeartbeatMultipoint, "setHeartbeatMultipoint"}, + {RADIO_CALL_setVcHeartbeatTimer, "setVcHeartbeatTimer"}, + //Insert new values before this line + {RADIO_CALL_hopISR, "hopISR"}, + {RADIO_CALL_transactionCompleteISR, "transactionCompleteISR"}, +#ifdef RADIOLIB_LOW_LEVEL + {RADIO_CALL_readSX1276Register, "readSX1276Register"}, + {RADIO_CALL_printSX1276Registers, "printSX1276Registers"}, +#endif //RADIOLIB_LOW_LEVEL +}; + +//Verify the RADIO_CALLS enum against the radioCallName +bool verifyRadioCallNames() +{ + bool valid; + + valid = ((sizeof(radioCallName) / sizeof(radioCallName[0])) == RADIO_CALL_MAX); + if (!valid) + systemPrintln("ERROR - Please update the radioCallName"); + return valid; +} + +//Convert a radio call value into a string, return NULL if not found +const char * getRadioCall(uint8_t radioCall) +{ + for (int index = 0; index < sizeof(radioCallName) / sizeof(radioCallName[0]); index++) + { + if (radioCallName[index].value == radioCall) + return radioCallName[index].string; + } + return NULL; +} + +//Display the radio call history +void displayRadioCallHistory() +{ + uint8_t index; + uint8_t sortOrder[RADIO_CALL_MAX]; + const char * string; + uint8_t temp; + + //Set the default sort order + petWDT(); + for (index = 0; index < RADIO_CALL_MAX; index++) + sortOrder[index] = index; + + //Perform a bubble sort + for (index = 0; index < RADIO_CALL_MAX; index++) + for (int x = index + 1; x < RADIO_CALL_MAX; x++) + if (radioCallHistory[sortOrder[index]] > radioCallHistory[sortOrder[x]]) + { + temp = sortOrder[index]; + sortOrder[index] = sortOrder[x]; + sortOrder[x] = temp; + } + + //Display the radio call history + for (index = 0; index < RADIO_CALL_MAX; index++) + if (radioCallHistory[sortOrder[index]]) + { + systemPrint(" "); + systemPrintTimestamp(radioCallHistory[sortOrder[index]]); + string = getRadioCall(sortOrder[index]); + if (string) + { + systemPrint(": "); + systemPrint(string); + } + systemPrintln(); + } + petWDT(); +} diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 92d521ec..4fc1de55 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -867,6 +867,9 @@ void verifyTables() //Verify the VC state name table valid &= verifyVcStateNames(); + //Verify the RADIO_CALLS enum against the radioCallName + valid &= verifyRadioCallNames(); + if (!valid) { outputSerialData(true); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index c6070280..718dbd10 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -464,8 +464,64 @@ typedef struct _ARCH_TABLE ARCH_UNIQUE_ID uniqueID; //Get the 128 bit unique ID value } ARCH_TABLE; +typedef struct _U8_TO_STRING +{ + uint8_t value; + const char * string; +} U8_TO_STRING; + typedef struct _U16_TO_STRING { uint16_t value; const char * string; } U16_TO_STRING; + +//Declare the radio call types +typedef enum +{ + RADIO_CALL_configureRadio = 0, + RADIO_CALL_setRadioFrequency, + RADIO_CALL_returnToReceiving, + RADIO_CALL_calcAirTime, + RADIO_CALL_xmitDatagramP2PTrainingPing, + RADIO_CALL_xmitDatagramP2pTrainingParams, + RADIO_CALL_xmitDatagramP2PPing, + RADIO_CALL_xmitDatagramP2PAck1, + RADIO_CALL_xmitDatagramP2PAck2, + RADIO_CALL_xmitDatagramP2PCommand, + RADIO_CALL_xmitDatagramP2PCommandResponse, + RADIO_CALL_xmitDatagramP2PData, + RADIO_CALL_xmitDatagramP2PHeartbeat, + RADIO_CALL_xmitDatagramP2PAck, + RADIO_CALL_xmitDatagramMpData, + RADIO_CALL_xmitDatagramMpHeartbeat, + RADIO_CALL_xmitDatagramMpAck, + RADIO_CALL_xmitDatagramMpPing, + RADIO_CALL_xmitDatagramMpTrainingPing, + RADIO_CALL_xmitDatagramMpTrainingAck, + RADIO_CALL_xmitDatagramMpRadioParameters, + RADIO_CALL_xmitVcDatagram, + RADIO_CALL_xmitVcHeartbeat, + RADIO_CALL_xmitVcAckFrame, + RADIO_CALL_xmitVcPing, + RADIO_CALL_xmitVcAck1, + RADIO_CALL_xmitVcAck2, + RADIO_CALL_rcvDatagram, + RADIO_CALL_transmitDatagram, + RADIO_CALL_retransmitDatagram, + RADIO_CALL_startChannelTimer, + RADIO_CALL_stopChannelTimer, + RADIO_CALL_syncChannelTimer, + RADIO_CALL_setHeartbeatShort, + RADIO_CALL_setHeartbeatLong, + RADIO_CALL_setHeartbeatMultipoint, + RADIO_CALL_setVcHeartbeatTimer, + //Insert new values before this line + RADIO_CALL_transactionCompleteISR, + RADIO_CALL_hopISR, +#ifdef RADIOLIB_LOW_LEVEL + RADIO_CALL_readSX1276Register, + RADIO_CALL_printSX1276Registers, +#endif //RADIOLIB_LOW_LEVEL + RADIO_CALL_MAX +} RADIO_CALLS; From a10d956f7b4bdc0e0f1c5812637806f3e8bee1e0 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 7 Dec 2022 08:55:31 -1000 Subject: [PATCH 231/594] Display the radio frequency, channel number, hops and dwell time --- Firmware/LoRaSerial_Firmware/Commands.ino | 26 +++++++++++++++++++ .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 10 +++---- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 06e7b220..d39f44b5 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -289,6 +289,32 @@ bool commandAT(const char * commandString) systemPrintln(vc->txAckNumber); } systemPrintln(" Radio"); + systemPrint(" Channel: "); + systemPrintln(channelNumber); + systemPrint(" Frequency: "); + systemPrint(radioFrequency, 3); + systemPrint(" MHz"); + if (radioFrequency != channels[channelNumber]) + { + systemPrint(" = "); + systemPrint(channels[channelNumber], 3); + systemPrint(" MHz + "); + systemPrint((radioFrequency - channels[channelNumber]) * 1000, 0); + systemPrint(" KHz"); + } + systemPrintln(); + systemPrint(" maxDwellTime: "); + systemPrint(settings.maxDwellTime); + systemPrintln(" mSec"); + if (channelNumber) + { + systemPrint(" Next Ch 0 time: "); + systemPrintTimestamp(nextChannelZeroTimeInMillis); + int hopCount = settings.numberOfChannels - channelNumber; + systemPrint(", in "); + systemPrint(hopCount); + systemPrintln(" hops"); + } systemPrint(" Last Successful Transmit: "); if (txSuccessMillis) { diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 7ed4c3cd..cf85bdd2 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -340,6 +340,7 @@ volatile bool transactionComplete = false; //Used in dio0ISR volatile bool timeToHop = false; //Used in dio1ISR uint8_t sf6ExpectedSize = MAX_PACKET_SIZE; //Used during SF6 operation to reduce packet size when needed +float radioFrequency; //Current radio frequency float frequencyCorrection = 0; //Adjust receive freq based on the last packet received freqError volatile bool hop = true; //Clear the DIO1 hop ISR when possible diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e0478d76..050f96ab 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -176,17 +176,15 @@ void convertAirSpeedToSettings() //Set radio frequency bool setRadioFrequency(bool rxAdjust) { - float frequency; - radioCallHistory[RADIO_CALL_setRadioFrequency] = millis(); //Determine the frequency to use - frequency = channels[channelNumber]; + radioFrequency = channels[channelNumber]; if (rxAdjust && settings.autoTuneFrequency) - frequency -= frequencyCorrection; + radioFrequency -= frequencyCorrection; //Set the new frequency - if (radio.setFrequency(frequency) == RADIOLIB_ERR_INVALID_FREQUENCY) + if (radio.setFrequency(radioFrequency) == RADIOLIB_ERR_INVALID_FREQUENCY) return false; //triggerFrequency(frequency); @@ -199,7 +197,7 @@ bool setRadioFrequency(bool rxAdjust) systemPrintTimestamp(); systemPrint(channelNumber); systemPrint(": "); - systemPrint(frequency, 3); + systemPrint(radioFrequency, 3); systemPrint(" MHz, Ch 0 in "); systemPrint(nextChannelZeroTimeInMillis - millis()); systemPrintln(" mSec"); From 75ab795870cb50a8c1520207cbf25fd166ff49c1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 7 Dec 2022 09:13:42 -1000 Subject: [PATCH 232/594] Display the radio state history --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 ++ .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 36 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index d39f44b5..7dfddd3e 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -369,6 +369,8 @@ bool commandAT(const char * commandString) systemPrintln(receiveInProcess() ? "True" : "False"); systemPrintln(" Call History"); displayRadioCallHistory(); + systemPrintln(" State History"); + displayRadioStateHistory(); reportOK(); break; case ('1'): //ATI11 - Return myVc value diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index cf85bdd2..4445d843 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -381,6 +381,7 @@ int txFailureState; //History unsigned long radioCallHistory[RADIO_CALL_MAX]; +unsigned long radioStateHistory[RADIO_MAX_STATE]; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - Radio Protocol diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3039583b..c7a00c2a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2409,6 +2409,7 @@ bool verifyRadioDatagramType() void changeState(RadioStates newState) { radioState = newState; + radioStateHistory[radioState] = millis(); if ((settings.debug == false) && (settings.debugStates == false)) return; @@ -2469,6 +2470,41 @@ void displayState(RadioStates newState) systemPrintln(); } +//Display the radio state history +void displayRadioStateHistory() +{ + uint8_t index; + uint8_t sortOrder[RADIO_MAX_STATE]; + uint8_t temp; + + //Set the default sort order + petWDT(); + for (index = 0; index < RADIO_MAX_STATE; index++) + sortOrder[index] = index; + + //Perform a bubble sort + for (index = 0; index < RADIO_MAX_STATE; index++) + for (int x = index + 1; x < RADIO_MAX_STATE; x++) + if (radioStateHistory[sortOrder[index]] > radioStateHistory[sortOrder[x]]) + { + temp = sortOrder[index]; + sortOrder[index] = sortOrder[x]; + sortOrder[x] = temp; + } + + //Display the radio state history + for (index = 0; index < RADIO_MAX_STATE; index++) + if (radioStateHistory[sortOrder[index]]) + { + systemPrint(" "); + systemPrintTimestamp(radioStateHistory[sortOrder[index]]); + systemPrint(": "); + systemPrint(radioStateTable[sortOrder[index]].name); + systemPrintln(); + } + petWDT(); +} + //Break a point-to-point link void breakLink() { From b03edd7381939ae2303b0405ac14bcbc5bbb201b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 07:18:35 -1000 Subject: [PATCH 233/594] Display the ACK timer --- Firmware/LoRaSerial_Firmware/Commands.ino | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 7dfddd3e..5f692856 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -434,6 +434,12 @@ bool commandAT(const char * commandString) systemPrint(" Next RX ACK number: "); systemPrintln(vc->rmtTxAckNumber); systemPrint(" Last TX ACK number: "); + if (vcAckTimer && (txDestVc == cmdVc)) + { + systemPrint(" vcAackTimer: "); + systemPrintTimestamp(vcAckTimer); + systemPrintln(); + } systemPrintln(vc->txAckNumber); } reportOK(); From c65e1f611e480fca80733bcd78d3d95137127893 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 07:40:41 -1000 Subject: [PATCH 234/594] Display the transmit and ACK timers --- Firmware/LoRaSerial_Firmware/Commands.ino | 30 ++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 5f692856..161b165e 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -287,6 +287,27 @@ bool commandAT(const char * commandString) systemPrintln(vc->rmtTxAckNumber); systemPrint(" Last TX ACK number: "); systemPrintln(vc->txAckNumber); + systemPrint(" transmitTimer: "); + if (transmitTimer) + systemPrintTimestamp(transmitTimer); + else + systemPrint("Not Running"); + systemPrintln(); + } + else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + systemPrintln(" ACK Management"); + systemPrint(" vcAackTimer: "); + if (vcAckTimer) + { + systemPrint("VC "); + systemPrint(txDestVc); + systemPrint(", "); + systemPrintTimestamp(vcAckTimer); + systemPrintln(); + } + else + systemPrintln("Not Running"); } systemPrintln(" Radio"); systemPrint(" Channel: "); @@ -434,13 +455,16 @@ bool commandAT(const char * commandString) systemPrint(" Next RX ACK number: "); systemPrintln(vc->rmtTxAckNumber); systemPrint(" Last TX ACK number: "); - if (vcAckTimer && (txDestVc == cmdVc)) + systemPrintln(vc->txAckNumber); + if (txDestVc == cmdVc) { systemPrint(" vcAackTimer: "); - systemPrintTimestamp(vcAckTimer); + if (vcAckTimer) + systemPrintTimestamp(vcAckTimer); + else + systemPrint("Not Running"); systemPrintln(); } - systemPrintln(vc->txAckNumber); } reportOK(); break; From f2c29a49f84f65d16274dd98e3ec1542d09f294c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 07:41:58 -1000 Subject: [PATCH 235/594] Update the buffer diagram --- .../LoRaSerial_Firmware.ino | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 4445d843..05a9de05 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -198,7 +198,7 @@ bool rtsAsserted; //When RTS is asserted, host says it's ok to send data | | processSerialInput | - | inCommandMode? + | inCommandMode or local VC command? | true V false .-------------+--------------------------. @@ -215,19 +215,22 @@ bool rtsAsserted; //When RTS is asserted, host says it's ok to send data | outgoingPacket V | | incomingBuffer | V | - | Send to remote system V - | | serialTransmitBuffer + | Send to remote system | + | | | | V | | incomingBuffer | | | | | V | | commandRXBuffer | | | | - | V | - | Command processing | - | checkCommand | + | V VC Remote Cmd V + | commandBuffer <------------------+ | | | | V | + | Command processing | + | checkCommand | Data or + | | | VC Data or + | V | VC Rmt Cmd Resp | commandTXBuffer | | | | | V | @@ -244,6 +247,10 @@ bool rtsAsserted; //When RTS is asserted, host says it's ok to send data | | V + serialTransmitBuffer + | + | + V USB or UART */ From ed4487786bce224f2fb45d66164de615b7fb7854 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 07:51:15 -1000 Subject: [PATCH 236/594] Bug Fix: Data corruption would occur when rxSrcVc = VC_BROADCAST --- Firmware/LoRaSerial_Firmware/States.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c7a00c2a..9dfde647 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1726,8 +1726,8 @@ void updateRadioState() vcHeader = (VC_RADIO_MESSAGE_HEADER *)rxData; //Indicate receive traffic from this VC including HEARTBEATs - if ((rxSrcVc == VC_BROADCAST) || ((uint8_t)rxSrcVc < (uint8_t)MIN_RX_NOT_ALLOWED)) - virtualCircuitList[rxSrcVc].lastTrafficMillis = currentMillis; + if ((uint8_t)rxSrcVc < (uint8_t)MIN_RX_NOT_ALLOWED) + virtualCircuitList[rxSrcVc & VCAB_NUMBER_MASK].lastTrafficMillis = currentMillis; //Process the received datagram switch (packetType) From f0c83a926698aec7b72bbb9fb0da7f34d64b6c16 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 07:58:30 -1000 Subject: [PATCH 237/594] VC: Properly clear the vcAckTimer --- Firmware/LoRaSerial_Firmware/States.ino | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9dfde647..5ce3bfcf 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1960,7 +1960,6 @@ void updateRadioState() else { //Failed to reach the other system, break the link - vcAckTimer = 0; vcBreakLink(txDestVc); } } @@ -2166,11 +2165,6 @@ void updateRadioState() changeState(RADIO_VC_WAIT_SERVER); } - //If waiting for an ACK and the link breaks, stop the retransmissions - //by stopping the ACK timer. - if (vcAckTimer && (index == rexmtTxDestVc)) - vcAckTimer = 0; - //Break the link vcBreakLink(index); } @@ -2643,6 +2637,13 @@ void vcBreakLink(int8_t vcIndex) return; vcIndex &= VCAB_NUMBER_MASK; + //If waiting for an ACK and the link breaks, stop the retransmissions + //by stopping the ACK timer. + if (vcAckTimer && (txDestVc == vcIndex)) + { + vcAckTimer = 0; + } + //Get the virtual circuit data structure if ((vcIndex >= 0) && (vcIndex != myVc) && (vcIndex < MAX_VC)) { @@ -2653,9 +2654,6 @@ void vcBreakLink(int8_t vcIndex) } linkFailures++; - //Stop the transmit timer - transmitTimer = 0; - //Flush the buffers outputSerialData(true); if (vcIndex == myVc) From d81a8bfe53740ce25e6876887234a9f4afb6a0d1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 08:19:48 -1000 Subject: [PATCH 238/594] Properly offset the data when dumping the circular buffer --- Firmware/LoRaSerial_Firmware/System.ino | 31 +++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 4fc1de55..a77af7e0 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -509,6 +509,7 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, { int bytes; uint8_t data; + uint16_t delta; const int displayWidth = 16; uint16_t i; int index; @@ -517,17 +518,31 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, offset = tail; while (length > 0) { - // Display the offset + //Display the offset systemPrint(" 0x"); - systemPrint((uint16_t)(offset), HEX); + systemPrint((uint16_t)(offset % bufferLength), HEX); systemPrint(": "); - // Determine the number of bytes to display + //Determine the number of bytes to display bytes = length; if (bytes > displayWidth) bytes = displayWidth; - // Display the data bytes in hex + //Adjust for the offset + delta = offset % displayWidth; + if (delta) + { + bytes -= delta; + for (index = 0; index < delta; index++) + { + systemPrint(" "); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + petWDT(); + } + } + + //Display the data bytes in hex for (index = 0; index < bytes; index++) { systemWrite(' '); @@ -538,8 +553,8 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, petWDT(); } - // Space over to the ASCII display - for (; index < displayWidth; index++) + //Space over to the ASCII display + for (; (delta + index) < displayWidth; index++) { systemPrint(" "); if (timeToHop == true) //If the channelTimer has expired, move to next frequency @@ -548,7 +563,9 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, } systemPrint(" "); - // Display the ASCII bytes + //Display the ASCII bytes + for (index = 0; index < delta; index++) + systemWrite(' '); for (index = 0; index < bytes; index++) { data = buffer[(offset + index) % bufferLength]; systemWrite(((data < ' ') || (data >= 0x7f)) ? '.' : data); From f289a9e789dd1cc97ae63ca7cab6b3d1c169bb5e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 08:22:28 -1000 Subject: [PATCH 239/594] VC: Use routine to send state message to the PC --- Firmware/LoRaSerial_Firmware/States.ino | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 5ce3bfcf..7e26827d 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2603,6 +2603,19 @@ void vcChangeState(int8_t vcIndex, uint8_t state) outputSerialData(true); } + //Determine if the VC is connecting + vcBit = 1 << vcIndex; + if ((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) + vcConnecting |= vcBit; + else + vcConnecting &= vcBit; + } + vcSendPcStateMessage(vcIndex, state); +} + +//Send the PC the state message +void vcSendPcStateMessage(int8_t vcIndex, uint8_t state) +{ //Build the VC state message VC_STATE_MESSAGE message; message.vcState = state; @@ -2617,14 +2630,6 @@ void vcChangeState(int8_t vcIndex, uint8_t state) //Send the VC state message systemWrite((uint8_t *)&header, sizeof(header)); systemWrite((uint8_t *)&message, sizeof(message)); - - //Determine if the VC is connecting - vcBit = 1 << vcIndex; - if ((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) - vcConnecting |= vcBit; - else - vcConnecting &= vcBit; - } } //Break the virtual-circuit link From f7192423388cdbf11ebced04f65f7bea9e8c6d14 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 08:44:37 -1000 Subject: [PATCH 240/594] VC: Move vcStateNames into Virtual_Circuit_Protocol.h --- .../LoRaSerial_Firmware/Virtual_Circuit_Protocol.h | 14 ++++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 8 +------- Firmware/Tools/VcServerTest.c | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index fa0c5bb1..253d8a09 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -162,4 +162,18 @@ typedef struct _VC_DATA_ACK_MESSAGE #define DATA_BUFFER(data) (data + VC_RADIO_HEADER_BYTES) #define GET_CHANNEL_NUMBER(vc) (vc & (VCAB_CHANNEL_MASK << VCAB_NUMBER_BITS)) +//------------------------------------------------------------------------------ +// Constants +//------------------------------------------------------------------------------ + +//Only one instance of this table is necessary +#ifdef ADD_VC_STATE_NAMES_TABLE +const char * const vcStateNames[] = +{ // 0 1 + "LINK-DOWN", "LINK-ALIVE", + // 2 3 4 5 + "SEND-PING", "WAIT-ACK1", "WAIT-ACK2", "CONNECTED", +}; +#endif //ADD_VC_STATE_NAMES_TABLE + #endif //__VIRTUAL_CIRCUIT_PROTOCOL_H__ diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 718dbd10..1025c801 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -161,13 +161,6 @@ const char * const radioDatagramType[] = "VC_HEARTBEAT", }; -const char * const vcStateNames[] = -{ // 0 1 - "LINK-DOWN", "LINK-ALIVE", - // 2 3 4 5 - "SEND-PING", "WAIT-ACK1", "WAIT-ACK2", "CONNECTED", -}; - typedef struct _VIRTUAL_CIRCUIT { uint8_t uniqueId[UNIQUE_ID_BYTES]; @@ -225,6 +218,7 @@ typedef struct _VIRTUAL_CIRCUIT //incremented when successfully acknowledged via DATA_ACK frame } VIRTUAL_CIRCUIT; +#define ADD_VC_STATE_NAMES_TABLE #include "Virtual_Circuit_Protocol.h" //Train button states diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 09de0de1..aa2bbb94 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -1,3 +1,4 @@ +#define ADD_VC_STATE_NAMES_TABLE #include "settings.h" #define BUFFER_SIZE 2048 From a8d32188d873a3405b5024922de27963eedb7ec9 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 10:32:28 -1000 Subject: [PATCH 241/594] VcServerTest: Display VC state transitions --- Firmware/Tools/VcServerTest.c | 93 ++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index aa2bbb94..c5c7e070 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -11,10 +11,16 @@ #define LINK_RESET_COMMAND "atz" #define MY_VC_ADDRESS "myVc: " -#define DEBUG_PC_TO_RADIO 0 -#define DEBUG_RADIO_TO_PC 0 -#define DISPLAY_DATA_ACK 1 -#define DISPLAY_VC_STATE 1 +#define DEBUG_PC_TO_RADIO 0 +#define DEBUG_RADIO_TO_PC 0 +#define DISPLAY_DATA_ACK 1 +#define DISPLAY_VC_STATE 0 +#define DISPLAY_STATE_TRANSITION 1 + +typedef struct _VIRTUAL_CIRCUIT +{ + int vcState; +} VIRTUAL_CIRCUIT; bool findMyVc; int myVc = VC_UNASSIGNED; @@ -22,6 +28,7 @@ int remoteVc; uint8_t inputBuffer[BUFFER_SIZE]; uint8_t outputBuffer[VC_SERIAL_HEADER_BYTES + BUFFER_SIZE]; int timeoutCount; +VIRTUAL_CIRCUIT virtualCircuitList[MAX_VC]; int cmdToRadio(uint8_t * buffer, int length) { @@ -237,41 +244,59 @@ int hostToStdout(uint8_t * data, uint8_t bytesToSend) void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) { + int newState; + int previousState; + int srcVc; VC_STATE_MESSAGE * vcMsg; + //Remember the previous state + srcVc = header->radio.srcVc; + previousState = virtualCircuitList[srcVc].vcState; vcMsg = (VC_STATE_MESSAGE *)&header[1]; - if (DISPLAY_VC_STATE) - { - switch (vcMsg->vcState) - { - default: - printf("------- VC %d State %3d ------\n", header->radio.srcVc, vcMsg->vcState); - break; - case VC_STATE_LINK_DOWN: - printf("--------- VC %d DOWN ---------\n", header->radio.srcVc); - break; - - case VC_STATE_LINK_ALIVE: - printf("-=--=--=- VC %d ALIVE =--=--=-\n", header->radio.srcVc); - break; + //Set the new state + newState = vcMsg->vcState; + virtualCircuitList[srcVc].vcState = newState; - case VC_STATE_SEND_PING: - printf("-=--=-- VC %d ALIVE P1 --=--=-\n", header->radio.srcVc); - break; - - case VC_STATE_WAIT_FOR_ACK1: - printf("-=--=-- VC %d ALIVE WA1 -=--=-\n", header->radio.srcVc); - break; - - case VC_STATE_WAIT_FOR_ACK2: - printf("-=--=-- VC %d ALIVE WA2 -=--=-\n", header->radio.srcVc); - break; - - case VC_STATE_CONNECTED: - printf("======= VC %d CONNECTED ======\n", header->radio.srcVc); - break; - } + //Display the state if requested + if (DISPLAY_STATE_TRANSITION) + printf("VC%d: %s --> %s\n", srcVc, vcStateNames[previousState], vcStateNames[newState]); + switch (newState) + { + default: + if (DISPLAY_VC_STATE) + printf("------- VC %d State %3d ------\n", srcVc, vcMsg->vcState); + break; + + case VC_STATE_LINK_DOWN: + if (DISPLAY_VC_STATE) + printf("--------- VC %d DOWN ---------\n", srcVc); + break; + + case VC_STATE_LINK_ALIVE: + if (DISPLAY_VC_STATE) + printf("-=--=--=- VC %d ALIVE =--=--=-\n", srcVc); + break; + + case VC_STATE_SEND_PING: + if (DISPLAY_VC_STATE) + printf("-=--=-- VC %d ALIVE P1 --=--=-\n", srcVc); + break; + + case VC_STATE_WAIT_FOR_ACK1: + if (DISPLAY_VC_STATE) + printf("-=--=-- VC %d ALIVE WA1 -=--=-\n", srcVc); + break; + + case VC_STATE_WAIT_FOR_ACK2: + if (DISPLAY_VC_STATE) + printf("-=--=-- VC %d ALIVE WA2 -=--=-\n", srcVc); + break; + + case VC_STATE_CONNECTED: + if (DISPLAY_VC_STATE) + printf("======= VC %d CONNECTED ======\n", srcVc); + break; } } From f5d6b01089fad1ddbf3c90bb179073b82f16b2d5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 10:44:15 -1000 Subject: [PATCH 242/594] VC: Send data NACKs to the PC --- Firmware/LoRaSerial_Firmware/Radio.ino | 14 +------- Firmware/LoRaSerial_Firmware/Serial.ino | 35 ++++++++++++++++--- Firmware/LoRaSerial_Firmware/States.ino | 2 ++ .../Virtual_Circuit_Protocol.h | 7 ++-- Firmware/Tools/VcServerTest.c | 18 ++++++++-- 5 files changed, 54 insertions(+), 22 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 050f96ab..2c0e3f47 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1906,19 +1906,7 @@ PacketType rcvDatagram() outputSerialData(true); } - //Notify the PC when an ACK is received for a DATA frame - if ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - && (rexmtControl.datagramType == DATAGRAM_DATA)) - { - //Build the VC header - serialOutputByte(START_OF_VC_SERIAL); - serialOutputByte(sizeof(VC_DATA_ACK_MESSAGE) + VC_RADIO_HEADER_BYTES); //Length - serialOutputByte(PC_DATA_ACK); //Destination - serialOutputByte(myVc); //Source - - //Build the VC_DATA_ACK_MESSAGE - serialOutputByte(rexmtTxDestVc);//Message destination VC - } + //Remember when the last datagram was received lastRxDatagram = rcvTimeMillis; break; diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 12e2cefb..b5147c5a 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -474,7 +474,8 @@ bool vcSerialMessageReceived() int8_t channel; uint16_t dataBytes; uint8_t msgLength; - int8_t vcDest; + int8_t vcDest; //Byte from VC_RADIO_MESSAGE_HEADER + int8_t vcIndex; //Index into virtualCircuitList do { @@ -517,7 +518,10 @@ bool vcSerialMessageReceived() //that internally generated traffic uses valid vcDest values. Only messages //enabled for receive on a remote radio may be transmitted. vcDest = vcSerialMsgGetVcDest(); - if (((uint8_t)vcDest >= (uint8_t)MIN_RX_NOT_ALLOWED) && (vcDest != VC_BROADCAST)) + vcIndex = -1; + if ((uint8_t)vcDest < (uint8_t)MIN_RX_NOT_ALLOWED) + vcIndex = vcDest & VCAB_NUMBER_MASK; + if ((vcIndex < 0) && (vcDest != VC_BROADCAST)) { if (settings.debugSerial || settings.debugTransmit) { @@ -560,12 +564,12 @@ bool vcSerialMessageReceived() //Verify that the destination link is up if ((vcDest != VC_BROADCAST) - && (virtualCircuitList[vcDest & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) + && (virtualCircuitList[vcIndex].vcState == VC_STATE_LINK_DOWN)) { if (settings.debugSerial || settings.debugTransmit) { systemPrint("Link down "); - systemPrint((vcDest == VC_BROADCAST) ? vcDest : vcDest & VCAB_NUMBER_MASK); + systemPrint((vcDest == VC_BROADCAST) ? vcDest : vcIndex); systemPrintln(", discarding message!"); outputSerialData(true); } @@ -574,6 +578,10 @@ bool vcSerialMessageReceived() radioTxTail += msgLength; if (radioTxTail >= sizeof(radioTxBuffer)) radioTxTail -= sizeof(radioTxBuffer); + + //If the PC is trying to send this message then notify the PC of the delivery failure + if ((uint8_t)vcDest < (uint8_t)MIN_TX_NOT_ALLOWED) + vcSendPcAckNack(vcIndex, false); break; } @@ -608,6 +616,25 @@ bool vcSerialMessageReceived() return false; } +//Notify the PC of the message delivery failure +void vcSendPcAckNack(int8_t vcIndex, bool ackMsg) +{ + //Build the VC state message + VC_DATA_ACK_NACK_MESSAGE message; + message.msgDestVc = vcIndex; + + //Build the message header + VC_SERIAL_MESSAGE_HEADER header; + header.start = START_OF_VC_SERIAL; + header.radio.length = VC_RADIO_HEADER_BYTES + sizeof(message); + header.radio.destVc = ackMsg ? PC_DATA_ACK : PC_DATA_NACK; + header.radio.srcVc = myVc; + + //Send the VC state message + systemWrite((uint8_t *)&header, sizeof(header)); + systemWrite((uint8_t *)&message, sizeof(message)); +} + //Process serial input when running in MODE_VIRTUAL_CIRCUIT void vcProcessSerialInput() { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 7e26827d..7cc76f74 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1773,6 +1773,7 @@ void updateRadioState() break; case DATAGRAM_DATA_ACK: + vcSendPcAckNack(rexmtTxDestVc, true); vcAckTimer = 0; break; @@ -2647,6 +2648,7 @@ void vcBreakLink(int8_t vcIndex) if (vcAckTimer && (txDestVc == vcIndex)) { vcAckTimer = 0; + vcSendPcAckNack(vcIndex, false); } //Get the virtual circuit data structure diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 253d8a09..2788a7af 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -32,7 +32,8 @@ //Source and destinations reserved for the local host #define PC_COMMAND VC_RSVD_SPECIAL_VCS //Command input and command response #define PC_LINK_STATUS (PC_COMMAND + 1) //Asynchronous link status output -#define PC_DATA_ACK (PC_LINK_STATUS + 1)//Indicate successful delivery of the data +#define PC_DATA_ACK (PC_LINK_STATUS + 1)//Indicate data delivery success +#define PC_DATA_NACK (PC_DATA_ACK + 1) //Indicate data delivery failure //Address space 1 and 2 are reserved for the host PC interface to support remote //command processing. The radio removes these bits and converts them to the @@ -149,10 +150,10 @@ typedef enum VC_STATE_MAX } VC_STATE_TYPE; -typedef struct _VC_DATA_ACK_MESSAGE +typedef struct _VC_DATA_ACK_NACK_MESSAGE { uint8_t msgDestVc; //message destination VC -} VC_DATA_ACK_MESSAGE; +} VC_DATA_ACK_NACK_MESSAGE; //------------------------------------------------------------------------------ // Macros diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index c5c7e070..f8b17cf7 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -14,6 +14,7 @@ #define DEBUG_PC_TO_RADIO 0 #define DEBUG_RADIO_TO_PC 0 #define DISPLAY_DATA_ACK 1 +#define DISPLAY_DATA_NACK 1 #define DISPLAY_VC_STATE 0 #define DISPLAY_STATE_TRANSITION 1 @@ -302,13 +303,22 @@ void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) void radioDataAck(uint8_t * data, uint8_t length) { - VC_DATA_ACK_MESSAGE * vcMsg; + VC_DATA_ACK_NACK_MESSAGE * vcMsg; - vcMsg = (VC_DATA_ACK_MESSAGE *)data; + vcMsg = (VC_DATA_ACK_NACK_MESSAGE *)data; if (DISPLAY_DATA_ACK) printf("ACK from VC %d\n", vcMsg->msgDestVc); } +void radioDataNack(uint8_t * data, uint8_t length) +{ + VC_DATA_ACK_NACK_MESSAGE * vcMsg; + + vcMsg = (VC_DATA_ACK_NACK_MESSAGE *)data; + if (DISPLAY_DATA_NACK) + printf("ACK from VC %d\n", vcMsg->msgDestVc); +} + int radioToHost() { int bytesRead; @@ -417,6 +427,10 @@ int radioToHost() else if (header->radio.destVc == PC_DATA_ACK) radioDataAck(data, length); + //Display NACKs for transmitted messages + else if (header->radio.destVc == PC_DATA_NACK) + radioDataAck(data, length); + //Display received messages else { From 4e0924ea145ab137214b211518eef8d530bcf487 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 11:01:43 -1000 Subject: [PATCH 243/594] Add command to dump the radioTxBuffer --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 161b165e..66d14a98 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -192,6 +192,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI10 - Display radio metrics"); systemPrintln(" ATI11 - Return myVc value"); systemPrintln(" ATI12 - Display the VC details"); + systemPrintln(" ATI13 - Dump the radioTxBuffer"); break; case ('0'): //ATI0 - Show user settable parameters displayParameters(0, true); @@ -468,6 +469,10 @@ bool commandAT(const char * commandString) } reportOK(); break; + case ('3'): //ATI13 - Dump the radioTxBuffer + systemPrintln("radioTxBuffer:"); + dumpCircularBuffer(radioTxBuffer, radioTxTail, sizeof(radioTxBuffer), availableRadioTXBytes()); + break; } } From dc81a762c6b0d1a2c5befdf998a6f8986525f3ff Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 11:35:27 -1000 Subject: [PATCH 244/594] Display the circular buffer pointers --- Firmware/LoRaSerial_Firmware/Commands.ino | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 66d14a98..43ef1b5d 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -310,6 +310,42 @@ bool commandAT(const char * commandString) else systemPrintln("Not Running"); } + systemPrintln(" Circular Buffers"); + systemPrint(" serialReceiveBuffer: rxHead: "); + systemPrint(rxHead); + systemPrint(", rxTail: "); + systemPrint(rxTail); + systemPrint(", "); + systemPrint(availableRXBytes()); + systemPrintln(" bytes"); + systemPrint(" serialTransmitBuffer: txHead: "); + systemPrint(txHead); + systemPrint(", txTail: "); + systemPrint(txTail); + systemPrint(", "); + systemPrint(availableTXBytes()); + systemPrintln(" bytes"); + systemPrint(" radioTxBuffer: radioTxHead: "); + systemPrint(radioTxHead); + systemPrint(", radioTxTail: "); + systemPrint(radioTxTail); + systemPrint(", "); + systemPrint(availableRadioTXBytes()); + systemPrintln(" bytes"); + systemPrint(" commandRXBuffer: commandRXHead: "); + systemPrint(commandRXHead); + systemPrint(", commandRXTail: "); + systemPrint(commandRXTail); + systemPrint(", "); + systemPrint(availableRXCommandBytes()); + systemPrintln(" bytes"); + systemPrint(" commandTXBuffer: commandTXHead: "); + systemPrint(commandTXHead); + systemPrint(", commandTXTail: "); + systemPrint(commandTXTail); + systemPrint(", "); + systemPrint(availableTXCommandBytes()); + systemPrintln(" bytes"); systemPrintln(" Radio"); systemPrint(" Channel: "); systemPrintln(channelNumber); From 206a8c472026d204e651d631250628708381a55c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 11:54:38 -1000 Subject: [PATCH 245/594] VC: Display the vcConnecting value --- Firmware/LoRaSerial_Firmware/Commands.ino | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 43ef1b5d..9facacea 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -309,6 +309,26 @@ bool commandAT(const char * commandString) } else systemPrintln("Not Running"); + systemPrint(" vcConnecting: "); + systemPrint((int)vcConnecting, HEX); + if (vcConnecting) + { + systemPrintln(); + for (int index = 0; index < MAX_VC; index++) + { + if (vcConnecting & (1 << index)) + { + systemPrint(" VC "); + if (index < 10) + systemWrite(' '); + systemPrint(index); + systemPrint(" State: "); + systemPrintln(vcStateNames[virtualCircuitList[index].vcState]); + } + } + } + else + systemPrintln("None"); } systemPrintln(" Circular Buffers"); systemPrint(" serialReceiveBuffer: rxHead: "); From 74c5f6b8571d1a2ea356e7ab4cbf61df48459216 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 5 Dec 2022 20:12:26 -1000 Subject: [PATCH 246/594] VC: Handle retransmission during the 3-way handshake --- Firmware/LoRaSerial_Firmware/States.ino | 75 +++++++++++++++++++------ 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 7cc76f74..d98dc35c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1632,16 +1632,16 @@ void updateRadioState() ^ | RX PING | | | | ATC command | | | HB Timeout V | | - +<------------- VC_STATE_SEND_PING | | - ^ | | | - | | TX PING | | - | | TX Complete RX | | - | HB Timeout V PING V | - +<------------- VC_STATE_WAIT_ACK1 --------->+----------->+ | - | | ^ | | - | | RX ACK1 | | TX ACK1 | - | | TX ACK2 | | TX Complete | - | | TX Complete | V | + +<------------- VC_STATE_SEND_PING <---------. | | + ^ | ACK1 RX | | | + | | TX PING Timeout | | | + | | TX Complete | RX | | + | HB Timeout V | PING V | + +<------------- VC_STATE_WAIT_ACK1 --------->+------->+-->+ | + ^ | ^ ^ | | + | | RX ACK1 | ACK2 | | TX ACK1 | + | | TX ACK2 | RX | | TX Complete | + | | TX Complete | TMO | V | | Zero ACK values | | VC_STATE_WAIT_ACK2 ----' | | .-------------' | | | | RX PING | RX ACK2 @@ -1997,28 +1997,67 @@ void updateRadioState() //---------- else if (vcConnecting) { - if (receiveInProcess() == false) + for (index = 0; index < MAX_VC; index++) { - for (index = 0; index < MAX_VC; index++) + if (receiveInProcess()) + break; + + //Determine the first VC that is walking through connections + if (vcConnecting & (1 << index)) { - //Determine the first VC that is walking through connections - if (vcConnecting & (1 << index)) + //Determine if PING needs to be sent + if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) + { + //Send the PING datagram, first part of the 3-way handshake + if (xmitVcPing(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + + //Only a single transmit is possible at a time + break; + } + } + + //The ACK1 is handled with the receive code + //Check for a timeout waiting for the ACK1 + if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) { - //Determine if PING needs to be sent - if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) + if ((currentMillis - virtualCircuitList[index].lastPingMillis) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { - //Send the PING datagram, first part of the 3-way handshake + //Retransmit the PING if (xmitVcPing(index)) { vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); virtualCircuitList[index].lastPingMillis = datagramTimer; changeState(RADIO_VC_WAIT_TX_DONE); + + //Only a single transmit is possible at a time break; } } + } -//Retransmit after lastPingMillis + (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + //The ACK2 is handled with the receive code + //Check for a timeout waiting for the ACK2 + if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) + { + if ((currentMillis - virtualCircuitList[index].lastPingMillis) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + { + //Retransmit the ACK1 + if (xmitVcAck1(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + //Only a single transmit is possible at a time + break; + } + } } } } From d4a812e44532c8768cd525d6865b9a2264b9d022 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 14:36:00 -1000 Subject: [PATCH 247/594] VC: Bug fix, clear the vcConnecting bit during VC_STATE_CONNECTED --- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d98dc35c..c5c5a063 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2648,7 +2648,7 @@ void vcChangeState(int8_t vcIndex, uint8_t state) if ((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) vcConnecting |= vcBit; else - vcConnecting &= vcBit; + vcConnecting &= ~vcBit; } vcSendPcStateMessage(vcIndex, state); } From 602cb6b6b8882793d2e1729cc4deba8f8ba9a9e3 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 14:37:39 -1000 Subject: [PATCH 248/594] VcServerTest: Bug fix: properly display the NACK --- Firmware/Tools/VcServerTest.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index f8b17cf7..9e8613a9 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -316,7 +316,7 @@ void radioDataNack(uint8_t * data, uint8_t length) vcMsg = (VC_DATA_ACK_NACK_MESSAGE *)data; if (DISPLAY_DATA_NACK) - printf("ACK from VC %d\n", vcMsg->msgDestVc); + printf("NACK from VC %d\n", vcMsg->msgDestVc); } int radioToHost() @@ -429,7 +429,7 @@ int radioToHost() //Display NACKs for transmitted messages else if (header->radio.destVc == PC_DATA_NACK) - radioDataAck(data, length); + radioDataNack(data, length); //Display received messages else From a2c058f73c63ea49d9d8e9772255dfbe35b0cca4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 14:44:20 -1000 Subject: [PATCH 249/594] VcServerTest: Rename *tty to *LoRaSerial --- Firmware/Tools/RadioV2.c | 2 +- Firmware/Tools/Terminal.c | 4 ++-- Firmware/Tools/VcServerTest.c | 4 ++-- Firmware/Tools/settings.h | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Firmware/Tools/RadioV2.c b/Firmware/Tools/RadioV2.c index 9cda0e7f..2a2ffd91 100644 --- a/Firmware/Tools/RadioV2.c +++ b/Firmware/Tools/RadioV2.c @@ -37,7 +37,7 @@ FRAME_TYPE rcvDatagram() //Receive a datagram from the client if (usingTerminal) - rxBytes = readTty(rxBuffer, sizeof(rxBuffer)); + rxBytes = readLoRaSerial(rxBuffer, sizeof(rxBuffer)); else { remoteAddrLength = sizeof(remoteAddr); diff --git a/Firmware/Tools/Terminal.c b/Firmware/Tools/Terminal.c index 1db8b382..3a54c629 100644 --- a/Firmware/Tools/Terminal.c +++ b/Firmware/Tools/Terminal.c @@ -1,6 +1,6 @@ #include "settings.h" -int openTty(const char *ttyName) +int openLoRaSerial(const char *ttyName) { struct termios params; int status; @@ -39,7 +39,7 @@ int openTty(const char *ttyName) return 0; } -int readTty(uint8_t * buffer, int bufferLength) +int readLoRaSerial(uint8_t * buffer, int bufferLength) { int bytes; int length; diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 9e8613a9..c6028c1a 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -503,7 +503,7 @@ main ( } //Open the terminal - status = openTty(terminal); + status = openLoRaSerial(terminal); if (status) break; if (maxfds < tty) @@ -534,7 +534,7 @@ main ( sleep(1); //Open the terminal - status = openTty(terminal); + status = openLoRaSerial(terminal); } while (status); //Delay a while to let the radio complete its reset operation diff --git a/Firmware/Tools/settings.h b/Firmware/Tools/settings.h index 847ff8f4..c8c70ce2 100644 --- a/Firmware/Tools/settings.h +++ b/Firmware/Tools/settings.h @@ -136,8 +136,8 @@ void petWDT(); int processData(); //Terminal -int openTty(const char *ttyName); -int readTty(uint8_t * buffer, int bufferLength); +int openLoRaSerial(const char *ttyName); +int readLoRaSerial(uint8_t * buffer, int bufferLength); int updateTerm(int fd); #endif // __settings_h__ From c09bfaca8ade6ec0b5b11f30584fd8ae9c699540 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 15:04:05 -1000 Subject: [PATCH 250/594] VcServerTest: Issue ATC command to connect to the remote system --- Firmware/Tools/VcServerTest.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index c6028c1a..5179ccfd 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -16,6 +16,7 @@ #define DISPLAY_DATA_ACK 1 #define DISPLAY_DATA_NACK 1 #define DISPLAY_VC_STATE 0 +#define SEND_ATC_COMMAND 1 #define DISPLAY_STATE_TRANSITION 1 typedef struct _VIRTUAL_CIRCUIT @@ -245,6 +246,7 @@ int hostToStdout(uint8_t * data, uint8_t bytesToSend) void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) { + char cmdBuffer[128]; int newState; int previousState; int srcVc; @@ -275,6 +277,20 @@ void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) break; case VC_STATE_LINK_ALIVE: + //Upon transition to ALIVE, if this matches the target VC, bring up the connection + if (SEND_ATC_COMMAND && (srcVc == remoteVc) && (previousState != newState)) + { + //Select the VC to use + sprintf(cmdBuffer, "at-CmdVc=%d", remoteVc); + printf("Sending %s to VC%d\r\n", cmdBuffer, remoteVc); + cmdToRadio((uint8_t *)cmdBuffer, strlen(cmdBuffer)); + + //Bring up the VC connection to this remote system + strcpy(cmdBuffer, "atC"); + printf("Sending %s to VC%d\r\n", cmdBuffer, remoteVc); + cmdToRadio((uint8_t *)cmdBuffer, strlen(cmdBuffer)); + } + if (DISPLAY_VC_STATE) printf("-=--=--=- VC %d ALIVE =--=--=-\n", srcVc); break; From 8f00514a8db181466569662c31f2cd1b99efa424 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 8 Dec 2022 16:16:23 -1000 Subject: [PATCH 251/594] Light each LED for testing --- .../LoRaSerial_Firmware.ino | 7 +++ Firmware/LoRaSerial_Firmware/System.ino | 49 ++++++++++++++++++- Firmware/LoRaSerial_Firmware/settings.h | 8 ++- 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 05a9de05..c9876a34 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -96,6 +96,13 @@ uint8_t pin_trigger = PIN_UNDEFINED; #define ALT_LED_BAD_FRAMES pin_txLED //Blue #define ALT_LED_BAD_CRC pin_rxLED //Yellow +#define GREEN_LED_1 pin_rssi1LED +#define GREEN_LED_2 pin_rssi2LED +#define GREEN_LED_3 pin_rssi3LED +#define GREEN_LED_4 pin_rssi4LED +#define BLUE_LED pin_txLED +#define YELLOW_LED pin_rxLED + #define LED_ON HIGH #define LED_OFF LOW diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index a77af7e0..881fc580 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -749,11 +749,26 @@ void radioLeds() //Update the LED values depending upon the selected display void updateLeds() { + static uint8_t previousLedUse; + + //When changing patterns, start with the LEDs off + if (previousLedUse != settings.selectLedUse) + { + previousLedUse = settings.selectLedUse; + digitalWrite(GREEN_LED_1, LED_OFF); + digitalWrite(GREEN_LED_2, LED_OFF); + digitalWrite(GREEN_LED_3, LED_OFF); + digitalWrite(GREEN_LED_4, LED_OFF); + digitalWrite(BLUE_LED, LED_OFF); + digitalWrite(YELLOW_LED, LED_OFF); + } + + //Display the LEDs switch (settings.selectLedUse) { //Set LEDs according to RSSI level default: - case 0: + case LEDS_RSSI: if (rssi > rssiLevelLow) setRSSI(0b0001); if (rssi > rssiLevelMed) @@ -764,9 +779,39 @@ void updateLeds() setRSSI(0b1111); break; - case 1: + case LEDS_RADIO_USE: radioLeds(); break; + + //Turn on the blue LED for testing and to identify this radio + case LEDS_BLUE_ON: + digitalWrite(BLUE_LED, LED_ON); + break; + + //Turn on the yellow LED for testing + case LEDS_YELLOW_ON: + digitalWrite(YELLOW_LED, LED_ON); + break; + + //Turn on the green 1 LED for testing + case LEDS_GREEN_1_ON: + digitalWrite(GREEN_LED_1, LED_ON); + break; + + //Turn on the green 2 LED for testing + case LEDS_GREEN_2_ON: + digitalWrite(GREEN_LED_2, LED_ON); + break; + + //Turn on the green 3 LED for testing + case LEDS_GREEN_3_ON: + digitalWrite(GREEN_LED_3, LED_ON); + break; + + //Turn on the green 4 LED for testing + case LEDS_GREEN_4_ON: + digitalWrite(GREEN_LED_4, LED_ON); + break; } } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 1025c801..25b22514 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -331,6 +331,12 @@ typedef enum LEDS_RSSI = 0, //Green: RSSI, Blue: Serial TX, Yellow: Serial RX LEDS_RADIO_USE, //Green1: RX, Green2: Link, Green3: RSSI, Green4: TX //Blue: Bad frames, Yellow: Bad CRC + LEDS_BLUE_ON, //Blue: ON, other: OFF + LEDS_YELLOW_ON, //Yellow: ON, other: OFF + LEDS_GREEN_1_ON, //Green 1: ON, other: OFF + LEDS_GREEN_2_ON, //Green 2: ON, other: OFF + LEDS_GREEN_3_ON, //Green 3: ON, other: OFF + LEDS_GREEN_4_ON, //Green 4: ON, other: OFF //Add user LED types from 255 working down } LEDS_USE_TYPE; @@ -405,7 +411,7 @@ typedef struct struct_settings { bool printLinkUpDown = false; //Print the link up and link down messages bool invertCts = false; //Invert the input of CTS bool invertRts = false; //Invert the output of RTS - bool selectLedUse = 0; //Select LED use + uint8_t selectLedUse = 0; //Select LED use uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training bool debugSerial = false; //Debug the serial input bool debugSync = false; //Print clock sync processing From d19575cf0984702be880906ae1c9b830bcbe4898 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 9 Dec 2022 07:36:42 -1000 Subject: [PATCH 252/594] VC: Add command to get current state for each of the VCs --- Firmware/LoRaSerial_Firmware/Commands.ino | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9facacea..99cb1611 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -52,6 +52,7 @@ bool commandAT(const char * commandString) case ('?'): //Display the command help systemPrintln("Command summary:"); systemPrintln(" AT? - Print the command summary"); + systemPrintln(" ATA - Get the current VC status"); systemPrintln(" ATB - Break the link"); systemPrintln(" ATC - Establish VC connection for data"); systemPrintln(" ATD - Display the debug settings"); @@ -72,7 +73,12 @@ bool commandAT(const char * commandString) systemPrintln(" AT&F - Restore factory settings"); systemPrintln(" AT&W - Save current settings to NVM"); break; - case ('B'): //Break the link + case ('A'): //ATA - Get the current VC status + for (int index = 0; index < MAX_VC; index++) + vcSendPcStateMessage(index, virtualCircuitList[index].vcState); + reportOK(); + break; + case ('B'): //ATB - Break the link reportOK(); //Compute the time delay From b52414b9e5561abf85091ca6b673c4c6ceb1d31e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 9 Dec 2022 07:39:46 -1000 Subject: [PATCH 253/594] VcServerTest: Connect the server to all of the remote radios --- Firmware/Tools/VcServerTest.c | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 5179ccfd..6fb6c38f 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -8,9 +8,11 @@ #define BREAK_LINKS_COMMAND "atb" #define GET_MY_VC_ADDRESS "atI11" +#define GET_VC_STATUS "ata" #define LINK_RESET_COMMAND "atz" #define MY_VC_ADDRESS "myVc: " +#define DEBUG_LOCAL_COMMANDS 0 #define DEBUG_PC_TO_RADIO 0 #define DEBUG_RADIO_TO_PC 0 #define DISPLAY_DATA_ACK 1 @@ -50,6 +52,8 @@ int cmdToRadio(uint8_t * buffer, int length) dumpBuffer((uint8_t *)&header, VC_SERIAL_HEADER_BYTES); dumpBuffer(buffer, length); } + if (DEBUG_LOCAL_COMMANDS) + printf("Sending LoRaSerial command: %s\n", buffer); //Send the header bytesWritten = write(tty, (uint8_t *)&header, VC_SERIAL_HEADER_BYTES); @@ -201,7 +205,13 @@ int hostToStdout(uint8_t * data, uint8_t bytesToSend) && ((uint8_t)vcNumber < MAX_VC)) { findMyVc = false; + + //Set the local radio's VC number myVc = (int8_t)vcNumber; + printf("myVc: %d\n", myVc); + + //Request the status for all of the VCs + cmdToRadio((uint8_t *)GET_VC_STATUS, strlen(GET_VC_STATUS)); break; } } @@ -277,17 +287,17 @@ void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) break; case VC_STATE_LINK_ALIVE: - //Upon transition to ALIVE, if this matches the target VC, bring up the connection - if (SEND_ATC_COMMAND && (srcVc == remoteVc) && (previousState != newState)) + //Upon transition to ALIVE, if is the server or the source VC matches the + //target VC or myVc, bring up the connection + if (SEND_ATC_COMMAND && (previousState != newState) + && ((myVc == VC_SERVER) || (srcVc == remoteVc) || (srcVc == myVc))) { //Select the VC to use - sprintf(cmdBuffer, "at-CmdVc=%d", remoteVc); - printf("Sending %s to VC%d\r\n", cmdBuffer, remoteVc); + sprintf(cmdBuffer, "at-CmdVc=%d", srcVc); cmdToRadio((uint8_t *)cmdBuffer, strlen(cmdBuffer)); //Bring up the VC connection to this remote system strcpy(cmdBuffer, "atC"); - printf("Sending %s to VC%d\r\n", cmdBuffer, remoteVc); cmdToRadio((uint8_t *)cmdBuffer, strlen(cmdBuffer)); } @@ -557,11 +567,8 @@ main ( sleep(2); } - //Get myVc address - findMyVc = true; - cmdToRadio((uint8_t *)GET_MY_VC_ADDRESS, strlen(GET_MY_VC_ADDRESS)); - //Break the links if requested + findMyVc = true; if (breakLinks) cmdToRadio((uint8_t *)BREAK_LINKS_COMMAND, strlen(BREAK_LINKS_COMMAND)); From 8d718944eae4b2fc1005166d04af62d687f9e8d4 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 14:31:26 -0700 Subject: [PATCH 254/594] Fix uint to int compilation error Radiolib returns negative errors. U16_TO_STRING changed to handle ints. --- Firmware/LoRaSerial_Firmware/Radio.ino | 4 ++-- Firmware/LoRaSerial_Firmware/settings.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 2c0e3f47..ac23d6b3 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2914,7 +2914,7 @@ void setVcHeartbeatTimer() } //Conversion table from radio status value into a status string -const U16_TO_STRING radioStatusCodes[] = +const I16_TO_STRING radioStatusCodes[] = { {RADIOLIB_ERR_NONE, "RADIOLIB_ERR_NONE"}, {RADIOLIB_ERR_UNKNOWN, "RADIOLIB_ERR_UNKNOWN"}, @@ -2955,7 +2955,7 @@ const char * getRadioStatusCode(int status) } //Conversion table from radio call value into a name string -const U16_TO_STRING radioCallName[] = +const I16_TO_STRING radioCallName[] = { {RADIO_CALL_configureRadio, "configureRadio"}, {RADIO_CALL_setRadioFrequency, "setRadioFrequency"}, diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 25b22514..07579178 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -470,11 +470,11 @@ typedef struct _U8_TO_STRING const char * string; } U8_TO_STRING; -typedef struct _U16_TO_STRING +typedef struct _I16_TO_STRING { - uint16_t value; + int16_t value; const char * string; -} U16_TO_STRING; +} I16_TO_STRING; //Declare the radio call types typedef enum From cc1919aef97a3fd4757d98336ba14ebee93f3cb2 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 15:23:47 -0700 Subject: [PATCH 255/594] Make packetlength global Used for clock sync adjustments --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 3 +++ Firmware/LoRaSerial_Firmware/Radio.ino | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index c9876a34..98227093 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -396,6 +396,9 @@ int txFailureState; //History unsigned long radioCallHistory[RADIO_CALL_MAX]; unsigned long radioStateHistory[RADIO_MAX_STATE]; + +uint8_t packetLength = 0; //Total bytes received, used for calculating clock sync times in multi-point mode + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - Radio Protocol diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index ac23d6b3..a5b889f5 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1477,6 +1477,7 @@ PacketType rcvDatagram() } rxDataBytes = radio.getPacketLength(); + packetLength = rxDataBytes; //Total bytes received, used for calculating clock sync times in multi-point mode returnToReceiving(); //Immediately begin listening while we process new data @@ -2663,6 +2664,9 @@ void syncChannelTimer() int16_t msToNextHopRemote; //Can be negative radioCallHistory[RADIO_CALL_syncChannelTimer] = millis(); + //If the sync arrived in an ACK, we know how long that packet took to transmit + //Calculate the packet airTime based on the size of data received + msToNextHopRemote -= calcAirTime(packetLength); memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); msToNextHopRemote -= ackAirTime; From b517e13af0196a536ad3fcc735e14ac889331ef6 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 15:24:28 -0700 Subject: [PATCH 256/594] Fix comments --- Firmware/LoRaSerial_Firmware/Radio.ino | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a5b889f5..e28ef3d5 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2661,15 +2661,14 @@ void stopChannelTimer() //adjust our own channelTimer interrupt to be synchronized with the remote unit void syncChannelTimer() { - int16_t msToNextHopRemote; //Can be negative + int16_t msToNextHopRemote; //Can become negative + memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); radioCallHistory[RADIO_CALL_syncChannelTimer] = millis(); //If the sync arrived in an ACK, we know how long that packet took to transmit //Calculate the packet airTime based on the size of data received msToNextHopRemote -= calcAirTime(packetLength); - memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); - msToNextHopRemote -= ackAirTime; msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; //Different airspeeds complete the transmitComplete ISR at different rates @@ -2694,10 +2693,12 @@ void syncChannelTimer() msToNextHopRemote -= getReceiveCompletionOffset(); break; case (4800): + msToNextHopRemote -= 2; break; case (9600): break; case (19200): + msToNextHopRemote -= 2; break; case (28800): msToNextHopRemote -= 2; @@ -2707,9 +2708,6 @@ void syncChannelTimer() break; } - //Calculate the remote's absolute distance to its next hop - //A remote hop may be very negative for - int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - timerStart); //Precalculate large/small time amounts From d29ad813d538e829f8af2ceb48826973da3db13a Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 15:24:58 -0700 Subject: [PATCH 257/594] Rename MODE_MULTIPOINT --- Firmware/LoRaSerial_Firmware/Radio.ino | 6 +++--- Firmware/LoRaSerial_Firmware/Train.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e28ef3d5..73026db8 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1868,7 +1868,7 @@ PacketType rcvDatagram() } //Verify the packet number last so that the expected datagram or ACK number can be updated - if (vc && (settings.operatingMode != MODE_DATAGRAM)) + if (vc && (settings.operatingMode != MODE_MULTIPOINT)) { switch (datagramType) { @@ -2004,7 +2004,7 @@ PacketType rcvDatagram() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (settings.operatingMode != MODE_DATAGRAM) + if (settings.operatingMode != MODE_MULTIPOINT) { systemPrint(" ("); if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) @@ -2185,7 +2185,7 @@ bool transmitDatagram() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: case DATAGRAM_HEARTBEAT: - if (settings.operatingMode != MODE_DATAGRAM) + if (settings.operatingMode != MODE_MULTIPOINT) { systemPrint(" ("); if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 36508ef2..96838243 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -138,7 +138,7 @@ void commonTrainingInitialization() //Use common radio settings between the client and server for training settings = defaultSettings; - settings.operatingMode = MODE_DATAGRAM; // 3: Use datagrams + settings.operatingMode = MODE_MULTIPOINT; // 3: Use datagrams settings.encryptData = true; // 4: Enable packet encryption settings.dataScrambling = true; // 6: Scramble the data settings.radioBroadcastPower_dbm = 14; // 7: Minimum, assume radios are near each other diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 07579178..d825c48a 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -299,7 +299,7 @@ PrinterEndpoints printerEndpoint = PRINT_TO_SERIAL; //Select the operating mode typedef enum { - MODE_DATAGRAM = 0, + MODE_MULTIPOINT = 0, MODE_POINT_TO_POINT, MODE_VIRTUAL_CIRCUIT, } OPERATING_MODE; From bba25ab0701593fad1182425fded4b5f5895e718 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 15:25:22 -0700 Subject: [PATCH 258/594] Add MP PING trigger --- Firmware/LoRaSerial_Firmware/States.ino | 4 ++++ Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 5 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c5c5a063..d523a461 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1223,12 +1223,16 @@ void updateRadioState() break; case DATAGRAM_PING: + triggerEvent(TRIGGER_MP_SEND_ACK_FOR_PING); //A new radio is saying hello if (settings.server == true) { //Ack their ping with sync data if (xmitDatagramMpAck() == true) + { + triggerEvent(TRIGGER_MP_SEND_ACK_FOR_PING); changeState(RADIO_MP_WAIT_TX_ACK_DONE); + } } else { diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index d825c48a..d5139360 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -252,6 +252,7 @@ enum TRIGGER_MP_SCAN, TRIGGER_MP_DATA_PACKET, TRIGGER_MP_PACKET_RECEIVED, + TRIGGER_MP_SEND_ACK_FOR_PING, TRIGGER_TRANSMIT_CANCELED, TRIGGER_HANDSHAKE_ACK1_TIMEOUT, TRIGGER_HANDSHAKE_SEND_PING, From 9ad92563ae14c356455179fc76e9a148c3dc4f9a Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 16:39:36 -0700 Subject: [PATCH 259/594] Move all LED control to updateLEDs() This prevents multiple functions and states from trying to control the LEDs during default RSSI mode. --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 - Firmware/LoRaSerial_Firmware/States.ino | 23 ---- Firmware/LoRaSerial_Firmware/System.ino | 142 +++++++++++----------- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 4 files changed, 69 insertions(+), 99 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 99cb1611..77f0bb1b 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -158,7 +158,6 @@ bool commandAT(const char * commandString) generateHopTable(); //Generate freq with new settings configureRadio(); //Apply any new settings - setRSSI(0); //Turn off LEDs inCommandMode = false; //Return to printing normal RF serial data reportOK(); changeState(RADIO_RESET); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d523a461..87c8d846 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -56,7 +56,6 @@ void updateRadioState() //Initialize the radio rssi = -200; - setRSSI(0b0000); //Turn off LEDs radioSeed = radio.randomByte(); //Puts radio into standy-by state randomSeed(radioSeed); if ((settings.debug == true) || (settings.debugRadio == true)) @@ -1315,22 +1314,6 @@ void updateRadioState() } } } - - //Toggle 2 LEDs if we have recently transmitted - if (millis() - datagramTimer < 5000) - { - if (millis() - lastLinkBlink > 250) //Blink at 4Hz - { - lastLinkBlink = millis(); - if (digitalRead(pin_rssi2LED) == HIGH) - setRSSI(0b0001); - else - setRSSI(0b0010); - } - } - else if (millis() - lastPacketReceived > 5000) - setRSSI(0); //Turn off RSSI after 5 seconds of no new packets received - break; //==================== @@ -1345,7 +1328,6 @@ void updateRadioState() if (transactionComplete == true) { transactionComplete = false; //Reset ISR flag - setRSSI(0b0001); returnToReceiving(); changeState(RADIO_MP_STANDBY); } @@ -1389,7 +1371,6 @@ void updateRadioState() //Wait for the PING to complete transmission //==================== case RADIO_MP_WAIT_TX_TRAINING_PING_DONE: - updateCylonLEDs(); //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) @@ -1411,7 +1392,6 @@ void updateRadioState() //Wait to receive the radio parameters //==================== case RADIO_MP_WAIT_RX_RADIO_PARAMETERS: - updateCylonLEDs(); //If dio0ISR has fired, a packet has arrived if (transactionComplete == true) @@ -1469,7 +1449,6 @@ void updateRadioState() //Wait for the ACK frame to complete transmission //==================== case RADIO_MP_WAIT_TX_PARAM_ACK_DONE: - updateCylonLEDs(); //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) @@ -1515,7 +1494,6 @@ void updateRadioState() //Wait for a PING frame from a client //==================== case RADIO_MP_WAIT_FOR_TRAINING_PING: - updateCylonLEDs(); //If dio0ISR has fired, a packet has arrived if (transactionComplete == true) @@ -1570,7 +1548,6 @@ void updateRadioState() //Wait for the radio parameters to complete transmission //==================== case RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE: - updateCylonLEDs(); //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 881fc580..25eda2b5 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -257,42 +257,21 @@ void updateButton() if (trainState == TRAIN_NO_PRESS && trainBtn->pressedFor(trainButtonTime)) { - trainState = TRAIN_PRESSED_2S; - lastTrainBlink = millis(); - } - else if (trainState == TRAIN_PRESSED_2S && trainBtn->wasReleased()) - { - setRSSI(0b1111); + trainState = TRAIN_PRESSED; selectTraining(); - + } + else if (trainState == TRAIN_PRESSED && trainBtn->wasReleased()) + { trainState = TRAIN_IN_PROCESS; } else if (trainState == TRAIN_IN_PROCESS && trainBtn->wasReleased()) { - //Exiting training - setRSSI(0b0000); //Turn off LEDs - //Reboot the radio petWDT(); systemFlush(); systemReset(); } - - //Blink LEDs according to our state while we wait for user to release button - if (trainState == TRAIN_PRESSED_2S) - { - if (millis() - lastTrainBlink > 500) //Slow blink - { - lastTrainBlink = millis(); - - //Toggle RSSI LEDs - if (digitalRead(pin_rssi1LED) == HIGH) - setRSSI(0); - else - setRSSI(0b1111); - } - } } } @@ -579,7 +558,7 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, } } -//Compute the RSSI value +//Compute the RSSI value after a packet has been received void updateRSSI() { //Calculate the average RSSI if possible @@ -747,6 +726,7 @@ void radioLeds() } //Update the LED values depending upon the selected display +//This is the only function that touches the LEDs void updateLeds() { static uint8_t previousLedUse; @@ -763,56 +743,70 @@ void updateLeds() digitalWrite(YELLOW_LED, LED_OFF); } - //Display the LEDs - switch (settings.selectLedUse) - { - //Set LEDs according to RSSI level - default: - case LEDS_RSSI: - if (rssi > rssiLevelLow) - setRSSI(0b0001); - if (rssi > rssiLevelMed) - setRSSI(0b0011); - if (rssi > rssiLevelHigh) - setRSSI(0b0111); - if (rssi > rssiLevelMax) - setRSSI(0b1111); - break; - - case LEDS_RADIO_USE: - radioLeds(); - break; - - //Turn on the blue LED for testing and to identify this radio - case LEDS_BLUE_ON: - digitalWrite(BLUE_LED, LED_ON); - break; - - //Turn on the yellow LED for testing - case LEDS_YELLOW_ON: - digitalWrite(YELLOW_LED, LED_ON); - break; - - //Turn on the green 1 LED for testing - case LEDS_GREEN_1_ON: - digitalWrite(GREEN_LED_1, LED_ON); - break; - - //Turn on the green 2 LED for testing - case LEDS_GREEN_2_ON: - digitalWrite(GREEN_LED_2, LED_ON); - break; - - //Turn on the green 3 LED for testing - case LEDS_GREEN_3_ON: - digitalWrite(GREEN_LED_3, LED_ON); - break; + //Update LEDs according to state + //If we are in training, cylon the LEDs + //If we are in data radio mode, control according to selectLedUse setting - //Turn on the green 4 LED for testing - case LEDS_GREEN_4_ON: - digitalWrite(GREEN_LED_4, LED_ON); - break; + if (radioState == RADIO_MP_WAIT_TX_TRAINING_PING_DONE + || radioState == RADIO_MP_WAIT_RX_RADIO_PARAMETERS + || radioState == RADIO_MP_WAIT_TX_PARAM_ACK_DONE + || radioState == RADIO_MP_WAIT_FOR_TRAINING_PING + || radioState == RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE) + { + updateCylonLEDs(); + } + else + { + switch (settings.selectLedUse) + { + //Set LEDs according to RSSI level + default: + case LEDS_RSSI: + if (rssi > rssiLevelLow) + setRSSI(0b0001); + if (rssi > rssiLevelMed) + setRSSI(0b0011); + if (rssi > rssiLevelHigh) + setRSSI(0b0111); + if (rssi > rssiLevelMax) + setRSSI(0b1111); + break; + + case LEDS_RADIO_USE: + radioLeds(); + break; + + //Turn on the blue LED for testing and to identify this radio + case LEDS_BLUE_ON: + digitalWrite(BLUE_LED, LED_ON); + break; + + //Turn on the yellow LED for testing + case LEDS_YELLOW_ON: + digitalWrite(YELLOW_LED, LED_ON); + break; + + //Turn on the green 1 LED for testing + case LEDS_GREEN_1_ON: + digitalWrite(GREEN_LED_1, LED_ON); + break; + + //Turn on the green 2 LED for testing + case LEDS_GREEN_2_ON: + digitalWrite(GREEN_LED_2, LED_ON); + break; + + //Turn on the green 3 LED for testing + case LEDS_GREEN_3_ON: + digitalWrite(GREEN_LED_3, LED_ON); + break; + + //Turn on the green 4 LED for testing + case LEDS_GREEN_4_ON: + digitalWrite(GREEN_LED_4, LED_ON); + break; } + } } //Case independent string comparison diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index d5139360..99d6791c 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -225,7 +225,7 @@ typedef struct _VIRTUAL_CIRCUIT typedef enum { TRAIN_NO_PRESS = 0, - TRAIN_PRESSED_2S, + TRAIN_PRESSED, TRAIN_IN_PROCESS, } TrainStates; From e06fdb40ed595afeacf680d0269b4e8fe0f85062 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 17:24:22 -0700 Subject: [PATCH 260/594] Calculate RSSI and move move printPacketQuality() during datagram ID. --- .../LoRaSerial_Firmware.ino | 11 --- Firmware/LoRaSerial_Firmware/Radio.ino | 77 ++++++++++--------- Firmware/LoRaSerial_Firmware/States.ino | 40 ++-------- Firmware/LoRaSerial_Firmware/System.ino | 31 ++------ 4 files changed, 52 insertions(+), 107 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 98227093..431fae38 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -357,9 +357,6 @@ uint8_t sf6ExpectedSize = MAX_PACKET_SIZE; //Used during SF6 operation to reduce float radioFrequency; //Current radio frequency float frequencyCorrection = 0; //Adjust receive freq based on the last packet received freqError -volatile bool hop = true; //Clear the DIO1 hop ISR when possible -int hopCount = 0; //Used to average the RSSI measured during hops - //RSSI must be above these negative numbers for LED to illuminate const int rssiLevelLow = -150; const int rssiLevelMed = -70; @@ -546,12 +543,4 @@ void loop() updateRadioState(); //Ping/ack/send packets as needed updateLeds(); //Update the LEDs on the board - - if (hop) //If the hop ISR has triggered, measure RSSI during reception - { - hop = false; - radio.clearFHSSInt(); //Clear the interrupt - rssi = radio.getRSSI(); - hopCount++; - } } diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 73026db8..785a171c 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -537,8 +537,6 @@ void transactionCompleteISR(void) void hopISR(void) { radioCallHistory[RADIO_CALL_hopISR] = millis(); - - hop = true; } //As we complete linkup, different airspeeds exit at different rates @@ -1442,7 +1440,10 @@ PacketType rcvDatagram() //Get the received datagram framesReceived++; int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); - + + rssi = radio.getRSSI(); + printPacketQuality(); //Display the RSSI, SNR and frequency error values + if (state == RADIOLIB_ERR_NONE) { rxSuccessMillis = rcvTimeMillis; @@ -1851,18 +1852,18 @@ PacketType rcvDatagram() //Validate the destination VC if ((rxDestVc != VC_BROADCAST) && (rxDestVc != myVc)) { - if (settings.debugReceive || settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("Not my VC: "); - systemPrintln(rxDestVc); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - if (settings.printPktData && rxDataBytes) - dumpBuffer(incomingBuffer, rxDataBytes); - outputSerialData(true); - } + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("Not my VC: "); + systemPrintln(rxDestVc); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + if (settings.printPktData && rxDataBytes) + dumpBuffer(incomingBuffer, rxDataBytes); + outputSerialData(true); + } return DATAGRAM_NOT_MINE; } } @@ -2557,8 +2558,8 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond if ((receiveInProcess() == true) || (transactionComplete == true) - || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) - && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN))) + || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN))) { triggerEvent(TRIGGER_TRANSMIT_CANCELED); if (settings.debugReceive || settings.debugDatagrams) @@ -2862,26 +2863,26 @@ void setVcHeartbeatTimer() radioCallHistory[RADIO_CALL_setVcHeartbeatTimer] = heartbeatTimer; /* - * The goal of this routine is to randomize the placement of the HEARTBEAT - * messages, allowing traffic to flow normally. However since clients are - * waiting in channel zero (0) for a HEARTBEAT, the last couple of invervals - * are adjusted for the server to ensure that a HEARTBEAT is sent in channel - * zero. - * - * dwellTime: 400 mSec - * heartbeatTimeout: 3000 mSec - * 50% heartbeatTimeout: 1500 mSec - * - * channel 38 39 40 41 42 43 44 45 46 47 48 49 0 1 2 - * dwellTime | | | | | | | | | | | | | | | - * seconds | . | . | . | . | . | . | - * case 1: > 4.5, Use random, then remaining - * ^--------------^^^^^^^^^^^^^^^^---------------^ - * case 2: 4.5 >= X >= 3, Use half, then second half - * ^^^^^^^^^^^^^^^^-------^^^^^^^^--------------^ - * case 3: X < 3, Use remaining - * ^----------------------------^ - */ + The goal of this routine is to randomize the placement of the HEARTBEAT + messages, allowing traffic to flow normally. However since clients are + waiting in channel zero (0) for a HEARTBEAT, the last couple of invervals + are adjusted for the server to ensure that a HEARTBEAT is sent in channel + zero. + + dwellTime: 400 mSec + heartbeatTimeout: 3000 mSec + 50% heartbeatTimeout: 1500 mSec + + channel 38 39 40 41 42 43 44 45 46 47 48 49 0 1 2 + dwellTime | | | | | | | | | | | | | | | + seconds | . | . | . | . | . | . | + case 1: > 4.5, Use random, then remaining + ^--------------^^^^^^^^^^^^^^^^---------------^ + case 2: 4.5 >= X >= 3, Use half, then second half + ^^^^^^^^^^^^^^^^-------^^^^^^^^--------------^ + case 3: X < 3, Use remaining + ^----------------------------^ + */ petWDT(); //Determine the delay before channel zero is reached @@ -2894,7 +2895,7 @@ void setVcHeartbeatTimer() //Determine the delay before the next HEARTBEAT frame if ((!settings.server) || (deltaMillis > ((3 * settings.heartbeatTimeout) / 2)) - || (deltaMillis <= 0)) + || (deltaMillis <= 0)) //Use the random interval: 50% - 100% heartbeatRandomTime = random(settings.heartbeatTimeout / 2, settings.heartbeatTimeout); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 87c8d846..ac5302e9 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -183,6 +183,9 @@ void updateRadioState() radio.setFrequency(channels[channelNumber]); } + //Clear residual RSSI to make sure RSSI LEDs are off + if(rssi > -200) rssi = -200; + //Determine if a PING was received if (transactionComplete) { @@ -328,6 +331,7 @@ void updateRadioState() } else { + //If we timeout during handshake, return to link down if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { if (settings.debugDatagrams) @@ -416,6 +420,7 @@ void updateRadioState() } else { + //If we timeout during handshake, return to link down if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { if (settings.debugDatagrams) @@ -572,9 +577,6 @@ void updateRadioState() break; case DATAGRAM_DUPLICATE: - printPacketQuality(); - - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); @@ -595,9 +597,6 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - printPacketQuality(); - - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); @@ -609,12 +608,9 @@ void updateRadioState() break; case DATAGRAM_DATA: - printPacketQuality(); - //Place the data in the serial output buffer serialBufferOutput(rxData, rxDataBytes); - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); @@ -641,7 +637,6 @@ void updateRadioState() commandRXHead += rxDataBytes - length; commandRXHead %= sizeof(commandRXBuffer); - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); @@ -657,7 +652,6 @@ void updateRadioState() for (int x = 0 ; x < rxDataBytes ; x++) Serial.write(rxData[x]); - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE); @@ -826,9 +820,6 @@ void updateRadioState() setHeartbeatLong(); //Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. - printPacketQuality(); - - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_ACK_RECEIVED); @@ -845,9 +836,6 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - printPacketQuality(); - - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); @@ -861,12 +849,10 @@ void updateRadioState() case DATAGRAM_DATA: //Received data while waiting for ack. - printPacketQuality(); //Place the data in the serial output buffer serialBufferOutput(rxData, rxDataBytes); - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); @@ -1086,9 +1072,6 @@ void updateRadioState() channelNumber = rxData[0]; //Change to the server's channel number - printPacketQuality(); - - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; lastPacketReceived = millis(); //Reset @@ -1243,9 +1226,6 @@ void updateRadioState() //Received data or heartbeat. Sync clock, do not ack. syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock - printPacketQuality(); - - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; lastPacketReceived = millis(); //Update timestamp for Link LED @@ -1259,12 +1239,9 @@ void updateRadioState() setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer - printPacketQuality(); - //Place any available data in the serial output buffer serialBufferOutput(rxData, rxDataBytes - sizeof(uint16_t)); //Remove the two bytes of sync data - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_MP_DATA_PACKET); @@ -1759,9 +1736,6 @@ void updateRadioState() break; case DATAGRAM_DUPLICATE: - printPacketQuality(); - - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); @@ -1809,7 +1783,6 @@ void updateRadioState() } petWDT(); - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); @@ -1851,7 +1824,6 @@ void updateRadioState() vcHeader->destVc |= PC_REMOTE_RESPONSE; serialBufferOutput(rxData, rxDataBytes); - updateRSSI(); //Adjust LEDs to RSSI level frequencyCorrection += radio.getFrequencyError() / 1000000.0; //ACK the command response @@ -2549,8 +2521,6 @@ void enterLinkUp() triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); hopChannel(); //Leave home - updateRSSI(); - //Synchronize the ACK numbers vc = &virtualCircuitList[0]; vc->rmtTxAckNumber = 0; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 25eda2b5..9cbdb2ca 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -558,23 +558,6 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, } } -//Compute the RSSI value after a packet has been received -void updateRSSI() -{ - //Calculate the average RSSI if possible - if (hopCount > 0) - rssi /= hopCount; - else - rssi = radio.getRSSI(); - - if (hopCount > 0) - { - //Reset RSSI measurements - hopCount = 0; - rssi = 0; - } -} - //Update the state of the 4 green LEDs void setRSSI(uint8_t ledBits) { @@ -762,14 +745,16 @@ void updateLeds() //Set LEDs according to RSSI level default: case LEDS_RSSI: - if (rssi > rssiLevelLow) - setRSSI(0b0001); - if (rssi > rssiLevelMed) - setRSSI(0b0011); - if (rssi > rssiLevelHigh) - setRSSI(0b0111); if (rssi > rssiLevelMax) setRSSI(0b1111); + else if (rssi > rssiLevelHigh) + setRSSI(0b0111); + else if (rssi > rssiLevelMed) + setRSSI(0b0011); + else if (rssi > rssiLevelLow) + setRSSI(0b0001); + else + setRSSI(0b0000); break; case LEDS_RADIO_USE: From 4cb6aa9e8171027c921faa77db648c4e77d0afd8 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 17:45:38 -0700 Subject: [PATCH 261/594] Rename MpTraining to Training --- Firmware/LoRaSerial_Firmware/Radio.ino | 18 ++++++------ Firmware/LoRaSerial_Firmware/States.ino | 38 ++++++++++++------------- Firmware/LoRaSerial_Firmware/System.ino | 10 +++---- Firmware/LoRaSerial_Firmware/Train.ino | 10 +++---- Firmware/LoRaSerial_Firmware/settings.h | 34 +++++++++++----------- 5 files changed, 54 insertions(+), 56 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 785a171c..1c2787cf 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1041,9 +1041,9 @@ bool xmitDatagramMpPing() //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Build the client ping packet used for training -bool xmitDatagramMpTrainingPing() +bool xmitDatagramTrainingPing() { - radioCallHistory[RADIO_CALL_xmitDatagramMpTrainingPing] = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramTrainingPing] = millis(); //Add the source (server) ID memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); @@ -1065,9 +1065,9 @@ bool xmitDatagramMpTrainingPing() } //Build the client ACK packet used for training -bool xmitDatagramMpTrainingAck(uint8_t * serverID) +bool xmitDatagramTrainingAck(uint8_t * serverID) { - radioCallHistory[RADIO_CALL_xmitDatagramMpTrainingAck] = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramTrainingAck] = millis(); //Add the destination (server) ID memcpy(endOfTxData, serverID, UNIQUE_ID_BYTES); @@ -1191,11 +1191,11 @@ void updateRadioParameters(uint8_t * rxData) //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Build the server parameters packet used for training -bool xmitDatagramMpRadioParameters(const uint8_t * clientID) +bool xmitDatagramTrainRadioParameters(const uint8_t * clientID) { Settings params; - radioCallHistory[RADIO_CALL_xmitDatagramMpRadioParameters] = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramTrainRadioParameters] = millis(); //Initialize the radio parameters memcpy(¶ms, &originalSettings, sizeof(settings)); @@ -2978,9 +2978,9 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_xmitDatagramMpHeartbeat, "xmitDatagramMpHeartbeat"}, {RADIO_CALL_xmitDatagramMpAck, "xmitDatagramMpAck"}, {RADIO_CALL_xmitDatagramMpPing, "xmitDatagramMpPing"}, - {RADIO_CALL_xmitDatagramMpTrainingPing, "xmitDatagramMpTrainingPing"}, - {RADIO_CALL_xmitDatagramMpTrainingAck, "xmitDatagramMpTrainingAck"}, - {RADIO_CALL_xmitDatagramMpRadioParameters, "xmitDatagramMpRadioParameters"}, + {RADIO_CALL_xmitDatagramTrainingPing, "xmitDatagramTrainingPing"}, + {RADIO_CALL_xmitDatagramTrainingAck, "xmitDatagramTrainingAck"}, + {RADIO_CALL_xmitDatagramTrainRadioParameters, "xmitDatagramTrainRadioParameters"}, {RADIO_CALL_xmitVcDatagram, "xmitVcDatagram"}, {RADIO_CALL_xmitVcHeartbeat, "xmitVcHeartbeat"}, {RADIO_CALL_xmitVcAckFrame, "xmitVcAckFrame"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ac5302e9..9fc2ce19 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1311,7 +1311,7 @@ void updateRadioState() break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //Multi-Point Client Training + //Client Training //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= /* @@ -1325,16 +1325,16 @@ void updateRadioState() | Send client ping | | | V | - RADIO_MP_WAIT_TX_TRAINING_PING_DONE | + RADIO_TRAIN_WAIT_TX_PING_DONE | | | V | Timeout - RADIO_MP_WAIT_RX_RADIO_PARAMETERS --------' + RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS -----' | | Update settings | Send client ACK | V - RADIO_MP_WAIT_TX_PARAM_ACK_DONE + RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE | V endTrainingClientServer @@ -1347,7 +1347,7 @@ void updateRadioState() //==================== //Wait for the PING to complete transmission //==================== - case RADIO_MP_WAIT_TX_TRAINING_PING_DONE: + case RADIO_TRAIN_WAIT_TX_PING_DONE: //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) @@ -1361,14 +1361,14 @@ void updateRadioState() returnToReceiving(); //Set the next state - changeState(RADIO_MP_WAIT_RX_RADIO_PARAMETERS); + changeState(RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS); } break; //==================== //Wait to receive the radio parameters //==================== - case RADIO_MP_WAIT_RX_RADIO_PARAMETERS: + case RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS: //If dio0ISR has fired, a packet has arrived if (transactionComplete == true) @@ -1401,8 +1401,8 @@ void updateRadioState() updateRadioParameters(&rxData[UNIQUE_ID_BYTES * 2]); //Acknowledge the radio parameters - if (xmitDatagramMpTrainingAck(&rxData[UNIQUE_ID_BYTES]) == true) - changeState(RADIO_MP_WAIT_TX_PARAM_ACK_DONE); + if (xmitDatagramTrainingAck(&rxData[UNIQUE_ID_BYTES]) == true) + changeState(RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE); break; } } @@ -1419,13 +1419,13 @@ void updateRadioState() //Check for a receive timeout else if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) - xmitDatagramMpTrainingPing(); + xmitDatagramTrainingPing(); break; //==================== //Wait for the ACK frame to complete transmission //==================== - case RADIO_MP_WAIT_TX_PARAM_ACK_DONE: + case RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE: //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) @@ -1436,7 +1436,7 @@ void updateRadioState() break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - //Multi-Point Server Training + //Server Training //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= /* @@ -1448,12 +1448,12 @@ void updateRadioState() +<--------------------------------. | | V | - .------ RADIO_MP_WAIT_FOR_TRAINING_PING | + .------ RADIO_TRAIN_WAIT_FOR_PING | | | | | | Send client ping | | | | | V | - | RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE --------' + | RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE -----' | | `---------------. @@ -1470,7 +1470,7 @@ void updateRadioState() //==================== //Wait for a PING frame from a client //==================== - case RADIO_MP_WAIT_FOR_TRAINING_PING: + case RADIO_TRAIN_WAIT_FOR_PING: //If dio0ISR has fired, a packet has arrived if (transactionComplete == true) @@ -1492,8 +1492,8 @@ void updateRadioState() memcpy(trainingPartnerID, rxData, UNIQUE_ID_BYTES); //Wait for the transmit to complete - if (xmitDatagramMpRadioParameters(trainingPartnerID) == true) - changeState(RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE); + if (xmitDatagramTrainRadioParameters(trainingPartnerID) == true) + changeState(RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE); break; case DATAGRAM_TRAINING_ACK: @@ -1524,7 +1524,7 @@ void updateRadioState() //==================== //Wait for the radio parameters to complete transmission //==================== - case RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE: + case RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE: //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) @@ -1538,7 +1538,7 @@ void updateRadioState() returnToReceiving(); //Set the next state - changeState(RADIO_MP_WAIT_FOR_TRAINING_PING); + changeState(RADIO_TRAIN_WAIT_FOR_PING); } break; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 9cbdb2ca..8e883daa 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -730,11 +730,11 @@ void updateLeds() //If we are in training, cylon the LEDs //If we are in data radio mode, control according to selectLedUse setting - if (radioState == RADIO_MP_WAIT_TX_TRAINING_PING_DONE - || radioState == RADIO_MP_WAIT_RX_RADIO_PARAMETERS - || radioState == RADIO_MP_WAIT_TX_PARAM_ACK_DONE - || radioState == RADIO_MP_WAIT_FOR_TRAINING_PING - || radioState == RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE) + if (radioState == RADIO_TRAIN_WAIT_TX_PING_DONE + || radioState == RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS + || radioState == RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE + || radioState == RADIO_TRAIN_WAIT_FOR_PING + || radioState == RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE) { updateCylonLEDs(); } diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 96838243..e1836d8b 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -80,17 +80,15 @@ void updateCylonLEDs() //Start the multi-point training in client mode void beginTrainingClient() { - systemPrintln("Begin client training"); - //Common initialization commonTrainingInitialization(); //Transmit client ping to the training server - if (xmitDatagramMpTrainingPing() == true) + if (xmitDatagramTrainingPing() == true) //Set the next state - changeState(RADIO_MP_WAIT_TX_TRAINING_PING_DONE); + changeState(RADIO_TRAIN_WAIT_TX_PING_DONE); else - changeState(RADIO_MP_WAIT_RX_RADIO_PARAMETERS); + changeState(RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS); trainingTimer = millis(); } @@ -127,7 +125,7 @@ void beginTrainingServer() returnToReceiving(); //Set the next state - changeState(RADIO_MP_WAIT_FOR_TRAINING_PING); + changeState(RADIO_TRAIN_WAIT_FOR_PING); } //Perform the common training initialization diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 99d6791c..6e862f15 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -25,14 +25,14 @@ typedef enum RADIO_MP_STANDBY, RADIO_MP_WAIT_TX_DONE, - //Multi-Point Training client states - RADIO_MP_WAIT_TX_TRAINING_PING_DONE, - RADIO_MP_WAIT_RX_RADIO_PARAMETERS, - RADIO_MP_WAIT_TX_PARAM_ACK_DONE, + //Training client states + RADIO_TRAIN_WAIT_TX_PING_DONE, + RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, + RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, - //Multi-Point Training server states - RADIO_MP_WAIT_FOR_TRAINING_PING, - RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, + //Training server states + RADIO_TRAIN_WAIT_FOR_PING, + RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE, //Virtual-Circuit states RADIO_VC_WAIT_SERVER, @@ -84,16 +84,16 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //19 {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //20 - //Multi-Point training client states + //Training client states // State RX Name Description - {RADIO_MP_WAIT_TX_TRAINING_PING_DONE, 0, "MP_WAIT_TX_TRAINING_PING_DONE", "MP: Wait TX training PING done"}, //21 - {RADIO_MP_WAIT_RX_RADIO_PARAMETERS, 1, "MP_WAIT_RX_RADIO_PARAMETERS", "MP: Wait for radio parameters"}, //22 - {RADIO_MP_WAIT_TX_PARAM_ACK_DONE, 0, "MP_WAIT_TX_PARAM_ACK_DONE", "MP: Wait for TX param ACK done"}, //23 + {RADIO_TRAIN_WAIT_TX_PING_DONE, 0, "TRAIN_WAIT_TX_PING_DONE", "Train: Wait TX training PING done"}, //21 + {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //22 + {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //23 - //Multi-Point training server states + //Training server states // State RX Name Description - {RADIO_MP_WAIT_FOR_TRAINING_PING, 1, "MP_WAIT_FOR_TRAINING_PING", "MP: Wait for training PING"}, //24 - {RADIO_MP_WAIT_TX_RADIO_PARAMS_DONE, 0, "MP_WAIT_TX_RADIO_PARAMS_DONE", "MP: Wait for TX params done"}, //25 + {RADIO_TRAIN_WAIT_FOR_PING, 1, "TRAIN_WAIT_FOR_PING", "Train: Wait for training PING"}, //24 + {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //25 //Virtual circuit states // State RX Name Description @@ -498,9 +498,9 @@ typedef enum RADIO_CALL_xmitDatagramMpHeartbeat, RADIO_CALL_xmitDatagramMpAck, RADIO_CALL_xmitDatagramMpPing, - RADIO_CALL_xmitDatagramMpTrainingPing, - RADIO_CALL_xmitDatagramMpTrainingAck, - RADIO_CALL_xmitDatagramMpRadioParameters, + RADIO_CALL_xmitDatagramTrainingPing, + RADIO_CALL_xmitDatagramTrainingAck, + RADIO_CALL_xmitDatagramTrainRadioParameters, RADIO_CALL_xmitVcDatagram, RADIO_CALL_xmitVcHeartbeat, RADIO_CALL_xmitVcAckFrame, From a275d594f93280b8e13340b3d816c1b9fa5bc408 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 18:17:27 -0700 Subject: [PATCH 262/594] Update comments on commands --- Firmware/LoRaSerial_Firmware/Commands.ino | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 77f0bb1b..f102d8af 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -42,7 +42,7 @@ bool commandAT(const char * commandString) if (commandLength == 2) reportOK(); - //ATI, ATO, ATZ commands + //AT?, ATA, ATB, ATC, ATG, ATI, ATO, ATZ commands else if (commandLength == 3) { switch (commandString[2]) @@ -132,11 +132,11 @@ bool commandAT(const char * commandString) reportOK; } break; - case ('G'): //Generate a new netID and encryption key + case ('G'): //ATG - Generate a new netID and encryption key generateTrainingSettings(); reportOK(); break; - case ('I'): + case ('I'): //ATI //Shows the radio version reportOK(); systemPrint("SparkFun LoRaSerial "); @@ -146,7 +146,7 @@ bool commandAT(const char * commandString) systemPrint("."); systemPrintln(FIRMWARE_VERSION_MINOR); break; - case ('O'): //Exit command mode + case ('O'): //ATO - Exit command mode if (printerEndpoint == PRINT_TO_RF) { //If we are pointed at the RF link, send ok and wait for response ACK before applying settings @@ -163,11 +163,11 @@ bool commandAT(const char * commandString) changeState(RADIO_RESET); } break; - case ('T'): //Enter training mode + case ('T'): //ATT - Enter training mode reportOK(); selectTraining(); break; - case ('Z'): //Reboots the radio + case ('Z'): //ATZ - Reboots the radio reportOK(); outputSerialData(true); systemFlush(); From dbbfa681f77bd2294cf89b5aeeec84c03952928b Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 9 Dec 2022 20:35:44 -0700 Subject: [PATCH 263/594] Print key/ID at hop table and rename generateRandomKeys --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 25 +++++++++++++++-------- Firmware/LoRaSerial_Firmware/Train.ino | 8 ++++---- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index f102d8af..f1e2d8c9 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -133,7 +133,7 @@ bool commandAT(const char * commandString) } break; case ('G'): //ATG - Generate a new netID and encryption key - generateTrainingSettings(); + generateRandomKeysID(); reportOK(); break; case ('I'): //ATI diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 1c2787cf..f24612b2 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -360,14 +360,21 @@ void generateHopTable() systemPrintln(); } - systemPrint("AES IV:"); - for (uint8_t i = 0 ; i < sizeof(AESiv) ; i++) - { - petWDT(); - systemPrint(" 0x"); - systemPrint(AESiv[i], HEX); - } + petWDT(); + + systemPrint("NetID: "); + systemPrintln(settings.netID); + + systemPrint("AES Key: "); + for (uint8_t i = 0 ; i < AES_KEY_BYTES ; i++) + systemPrint(settings.encryptionKey[i], HEX); systemPrintln(); + + systemPrint("AES IV: "); + for (uint8_t i = 0 ; i < AES_IV_BYTES ; i++) + systemPrint(AESiv[i], HEX); + systemPrintln(); + outputSerialData(true); } } @@ -1440,10 +1447,10 @@ PacketType rcvDatagram() //Get the received datagram framesReceived++; int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); - + rssi = radio.getRSSI(); printPacketQuality(); //Display the RSSI, SNR and frequency error values - + if (state == RADIOLIB_ERR_NONE) { rxSuccessMillis = rcvTimeMillis; diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index e1836d8b..e66778da 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -10,7 +10,7 @@ void selectTraining() //Generate new netID/AES key to share //We assume the user needs to maintain their settings (airSpeed, numberOfChannels, freq min/max, bandwidth/spread/hop) //but need to be on a different netID/AES key. -void generateTrainingSettings() +void generateRandomKeysID() { if ((settings.debug == true) || (settings.debugTraining == true)) { @@ -74,10 +74,10 @@ void updateCylonLEDs() } //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//Multi-Point Client/Server Training +//Client/Server Training //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//Start the multi-point training in client mode +//Start the training in client mode void beginTrainingClient() { //Common initialization @@ -92,7 +92,7 @@ void beginTrainingClient() trainingTimer = millis(); } -//Start the multi-point training in server mode +//Start the training in server mode void beginTrainingServer() { trainingServerRunning = true; From d92ab459c61581d59fc7ab5f0bc6d2f9704384ff Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 09:34:10 -0700 Subject: [PATCH 264/594] Enable button training off-the-shelf If radios are in default P2P state, and training is pressed, units now complete training. If a different mode is used, or if server is set, server will stay in training until button press or command is issued. If a user wants to train specific custom settings in P2P mode using the training button, the user should put the unit with the desired settings into training first (ie have its train button pressed first). --- .../LoRaSerial_Firmware.ino | 4 + Firmware/LoRaSerial_Firmware/States.ino | 237 ++++++++++-------- Firmware/LoRaSerial_Firmware/System.ino | 10 + Firmware/LoRaSerial_Firmware/Train.ino | 8 +- 4 files changed, 153 insertions(+), 106 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 431fae38..a05834ae 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -134,6 +134,7 @@ uint8_t AESiv[AES_IV_BYTES] = {0}; //Set during hop table generation Button *trainBtn = NULL; //We can't instantiate the button here because we don't yet know what pin number to use const int trainButtonTime = 2000; //ms press and hold before entering training +bool trainViaButton = false; //Allows auto-creation of server if client times out //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Hardware Timers @@ -472,6 +473,9 @@ unsigned long retransmitTimeout = 0; //Throttle back re-transmits //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- const Settings defaultSettings; Settings originalSettings; //Create a duplicate of settings during training so that we can resort as needed +uint8_t originalEncryptionKey[AES_KEY_BYTES] = {0}; //Temp store key if we need to exit button training +uint8_t originalNetID = 0; //Temp store ID if we need to exit button training +bool originalServer = false; //Temp store server setting if we need to exit button training char platformPrefix[25]; //Used for printing platform specific device name, ie "SAMD21 1W 915MHz" //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9fc2ce19..17b682cd 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -184,7 +184,7 @@ void updateRadioState() } //Clear residual RSSI to make sure RSSI LEDs are off - if(rssi > -200) rssi = -200; + if (rssi > -200) rssi = -200; //Determine if a PING was received if (transactionComplete) @@ -1419,7 +1419,22 @@ void updateRadioState() //Check for a receive timeout else if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) - xmitDatagramTrainingPing(); + { + if (trainViaButton) + { + //Give up and change to Server automatically + + settings = originalSettings; //Return to original radio settings + + generateRandomKeysID(); //Generate random netID and AES key + + beginTrainingServer(); //Change to server + } + else + { + xmitDatagramTrainingPing(); //Continue retrying as client + } + } break; //==================== @@ -1507,6 +1522,18 @@ void updateRadioState() systemPrint("Client "); systemPrintUniqueID(trainingPartnerID); systemPrintln(" Trained"); + + //If we are training via button, and in point to point mode, and the user has not manually set the server + //then reboot with current settings after a single client acks + if (trainViaButton + && originalSettings.operatingMode == MODE_POINT_TO_POINT + && originalServer == false) + { + //Reboot the radio with the newly generated random netID/Key parameters + petWDT(); + systemFlush(); + systemReset(); + } } break; } @@ -1548,7 +1575,7 @@ void updateRadioState() /* - Radio States: + Radio States: RADIO_RESET | @@ -1574,13 +1601,13 @@ void updateRadioState() | | '-----------------' - 3-way Handshake to zero ACKs: + 3-way Handshake to zero ACKs: TX PING --> RX ACK1 --> TX ACK2 or RX PING --> TX ACK1 --> RX ACK2 - VC States: + VC States: .-------------> VC_STATE_LINK_DOWN <-------------------------------------. | | HB Timeout | @@ -1858,7 +1885,7 @@ void updateRadioState() //Verify that the link is still up txDestVc = rexmtTxDestVc; if ((txDestVc != VC_BROADCAST) - && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { //Stop the retransmits vcAckTimer = 0; @@ -1931,7 +1958,7 @@ void updateRadioState() vcHeader->destVc = rmtCmdVc; vcHeader->srcVc = myVc; vcHeader->length = readyOutgoingCommandPacket(VC_RADIO_HEADER_BYTES) - + VC_RADIO_HEADER_BYTES; + + VC_RADIO_HEADER_BYTES; if (xmitDatagramP2PCommandResponse()) changeState(RADIO_VC_WAIT_TX_DONE); @@ -1978,7 +2005,7 @@ void updateRadioState() if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) { if ((currentMillis - virtualCircuitList[index].lastPingMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { //Retransmit the PING if (xmitVcPing(index)) @@ -1998,7 +2025,7 @@ void updateRadioState() if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) { if ((currentMillis - virtualCircuitList[index].lastPingMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { //Retransmit the ACK1 if (xmitVcAck1(index)) @@ -2032,102 +2059,102 @@ void updateRadioState() channel = (vcHeader->destVc >> VCAB_NUMBER_BITS) & VCAB_CHANNEL_MASK; switch (channel) { - case 0: //Data packets - //Check for datagram transmission - if (vcHeader->destVc == VC_BROADCAST) - { - //Broadcast this data to all VCs, no ACKs will be received - triggerEvent(TRIGGER_VC_TX_DATA); - xmitVcDatagram(); - break; - } + case 0: //Data packets + //Check for datagram transmission + if (vcHeader->destVc == VC_BROADCAST) + { + //Broadcast this data to all VCs, no ACKs will be received + triggerEvent(TRIGGER_VC_TX_DATA); + xmitVcDatagram(); + break; + } - //Transmit the packet - triggerEvent(TRIGGER_VC_TX_DATA); - if (xmitDatagramP2PData() == true) - changeState(RADIO_VC_WAIT_TX_DONE); + //Transmit the packet + triggerEvent(TRIGGER_VC_TX_DATA); + if (xmitDatagramP2PData() == true) + changeState(RADIO_VC_WAIT_TX_DONE); - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; + //Since vcAckTimer is off when equal to zero, force it to a non-zero value + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; - //Save the message for retransmission - SAVE_TX_BUFFER(); - rexmtTxDestVc = txDestVc; - break; + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + break; - case 1: //Remote command packets - //Remote commands must not be broadcast - if (vcHeader->destVc == VC_BROADCAST) - { - if (settings.debugSerial || settings.debugTransmit) + case 1: //Remote command packets + //Remote commands must not be broadcast + if (vcHeader->destVc == VC_BROADCAST) { - systemPrintln("ERROR: Remote commands may not be broadcast!"); - outputSerialData(true); - } + if (settings.debugSerial || settings.debugTransmit) + { + systemPrintln("ERROR: Remote commands may not be broadcast!"); + outputSerialData(true); + } - //Discard this message - endOfTxData = &outgoingPacket[headerBytes]; - break; - } + //Discard this message + endOfTxData = &outgoingPacket[headerBytes]; + break; + } - //Determine if this remote command gets processed on the local node - if ((vcHeader->destVc & VCAB_NUMBER_MASK) == myVc) - { - //Copy the command into the command buffer - commandLength = endOfTxData - &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES]; - memcpy(commandBuffer, &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES], commandLength); - if (settings.debugSerial) + //Determine if this remote command gets processed on the local node + if ((vcHeader->destVc & VCAB_NUMBER_MASK) == myVc) { - systemPrint("RX: Moving "); - systemPrint(commandLength); - systemPrintln(" bytes into commandBuffer"); - outputSerialData(true); - dumpBuffer((uint8_t *)commandBuffer, commandLength); - } - petWDT(); + //Copy the command into the command buffer + commandLength = endOfTxData - &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES]; + memcpy(commandBuffer, &outgoingPacket[headerBytes + VC_RADIO_HEADER_BYTES], commandLength); + if (settings.debugSerial) + { + systemPrint("RX: Moving "); + systemPrint(commandLength); + systemPrintln(" bytes into commandBuffer"); + outputSerialData(true); + dumpBuffer((uint8_t *)commandBuffer, commandLength); + } + petWDT(); - //Reset the buffer data pointer for the next transmit operation - endOfTxData = &outgoingPacket[headerBytes]; + //Reset the buffer data pointer for the next transmit operation + endOfTxData = &outgoingPacket[headerBytes]; - //Process the command - petWDT(); - printerEndpoint = PRINT_TO_RF; //Send prints to RF link - checkCommand(); //Parse the command buffer - petWDT(); - printerEndpoint = PRINT_TO_SERIAL; - length = availableTXCommandBytes(); - if (settings.debugSerial) - { - systemPrint("RX: checkCommand placed "); - systemPrint(length); - systemPrintln(" bytes into commandTXBuffer"); - dumpCircularBuffer(commandTXBuffer, commandTXTail, sizeof(commandTXBuffer), length); - outputSerialData(true); + //Process the command petWDT(); - } + printerEndpoint = PRINT_TO_RF; //Send prints to RF link + checkCommand(); //Parse the command buffer + petWDT(); + printerEndpoint = PRINT_TO_SERIAL; + length = availableTXCommandBytes(); + if (settings.debugSerial) + { + systemPrint("RX: checkCommand placed "); + systemPrint(length); + systemPrintln(" bytes into commandTXBuffer"); + dumpCircularBuffer(commandTXBuffer, commandTXTail, sizeof(commandTXBuffer), length); + outputSerialData(true); + petWDT(); + } - //Break up the command response - while (availableTXCommandBytes()) - readyLocalCommandPacket(); - break; - } + //Break up the command response + while (availableTXCommandBytes()) + readyLocalCommandPacket(); + break; + } - //Send the remote command - vcHeader->destVc &= VCAB_NUMBER_MASK; - if (xmitDatagramP2PCommand() == true) - changeState(RADIO_VC_WAIT_TX_DONE); + //Send the remote command + vcHeader->destVc &= VCAB_NUMBER_MASK; + if (xmitDatagramP2PCommand() == true) + changeState(RADIO_VC_WAIT_TX_DONE); - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; + //Since vcAckTimer is off when equal to zero, force it to a non-zero value + vcAckTimer = datagramTimer; + if (!vcAckTimer) + vcAckTimer = 1; - //Save the message for retransmission - SAVE_TX_BUFFER(); - rexmtTxDestVc = txDestVc; - break; + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + break; } } } @@ -2145,7 +2172,7 @@ void updateRadioState() //Determine if the link has timed out vc = &virtualCircuitList[index]; if ((vc->vcState != VC_STATE_LINK_DOWN) && (serverLinkBroken - || ((currentMillis - vc->lastTrafficMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) + || ((currentMillis - vc->lastTrafficMillis) > (VC_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)))) { if (index == VC_SERVER) { @@ -2607,20 +2634,20 @@ void vcChangeState(int8_t vcIndex, uint8_t state) //Send the PC the state message void vcSendPcStateMessage(int8_t vcIndex, uint8_t state) { - //Build the VC state message - VC_STATE_MESSAGE message; - message.vcState = state; - - //Build the message header - VC_SERIAL_MESSAGE_HEADER header; - header.start = START_OF_VC_SERIAL; - header.radio.length = VC_RADIO_HEADER_BYTES + sizeof(message); - header.radio.destVc = PC_LINK_STATUS; - header.radio.srcVc = vcIndex; - - //Send the VC state message - systemWrite((uint8_t *)&header, sizeof(header)); - systemWrite((uint8_t *)&message, sizeof(message)); + //Build the VC state message + VC_STATE_MESSAGE message; + message.vcState = state; + + //Build the message header + VC_SERIAL_MESSAGE_HEADER header; + header.start = START_OF_VC_SERIAL; + header.radio.length = VC_RADIO_HEADER_BYTES + sizeof(message); + header.radio.destVc = PC_LINK_STATUS; + header.radio.srcVc = vcIndex; + + //Send the VC state message + systemWrite((uint8_t *)&header, sizeof(header)); + systemWrite((uint8_t *)&message, sizeof(message)); } //Break the virtual-circuit link diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 8e883daa..faf4589c 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -259,6 +259,7 @@ void updateButton() { trainState = TRAIN_PRESSED; + trainViaButton = true; selectTraining(); } else if (trainState == TRAIN_PRESSED && trainBtn->wasReleased()) @@ -267,6 +268,15 @@ void updateButton() } else if (trainState == TRAIN_IN_PROCESS && trainBtn->wasReleased()) { + settings = originalSettings; //Return to original radio settings + + //Return to original keys, ID, and server state + memcpy(&settings.encryptionKey, &originalEncryptionKey, AES_KEY_BYTES); + settings.netID = originalNetID; + settings.server = originalServer; + + recordSystemSettings(); //Record original settings + //Reboot the radio petWDT(); systemFlush(); diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index e66778da..08fad683 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -1,6 +1,12 @@ //Select the training protocol void selectTraining() { + //If we are training via button, and in P2P mode, and Server is not set + //we will need these settings if we exit training + memcpy(&originalEncryptionKey, &settings.encryptionKey, AES_KEY_BYTES); + originalNetID = settings.netID; + originalServer = settings.server; + if (settings.server) beginTrainingServer(); else @@ -211,7 +217,7 @@ void endClientServerTraining(uint8_t event) outputSerialData(true); } - if (!settings.server) + if (settings.server == false) { //Record the new client settings petWDT(); From 1d1584fbd0ffa5ad7b8786279f1258fbc56d7906 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 16:05:25 -0700 Subject: [PATCH 265/594] Bring back hop flag clearing --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 6 +++++- Firmware/LoRaSerial_Firmware/Radio.ino | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index a05834ae..58f38163 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -358,6 +358,8 @@ uint8_t sf6ExpectedSize = MAX_PACKET_SIZE; //Used during SF6 operation to reduce float radioFrequency; //Current radio frequency float frequencyCorrection = 0; //Adjust receive freq based on the last packet received freqError +volatile bool hop = true; //Clear the DIO1 hop ISR when possible + //RSSI must be above these negative numbers for LED to illuminate const int rssiLevelLow = -150; const int rssiLevelMed = -70; @@ -540,11 +542,13 @@ void loop() { petWDT(); - updateButton(); + updateButton(); //Check if train button is pressed updateSerial(); //Store incoming and print outgoing updateRadioState(); //Ping/ack/send packets as needed updateLeds(); //Update the LEDs on the board + + updateHopISR(); //Clear hop ISR as needed } diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index f24612b2..caceec4e 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -544,6 +544,18 @@ void transactionCompleteISR(void) void hopISR(void) { radioCallHistory[RADIO_CALL_hopISR] = millis(); + + hop = true; +} + +//We clear the hop ISR just to make logic analyzer data cleaner +void updateHopISR() +{ + if (hop) //Clear hop ISR as needed + { + hop = false; + radio.clearFHSSInt(); //Clear the interrupt + } } //As we complete linkup, different airspeeds exit at different rates From 11ace2260e223c5600ec6263503c851f63172dd7 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 16:07:05 -0700 Subject: [PATCH 266/594] Rename CHANNEL_TIMER_BYTES --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/States.ino | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 58f38163..6c5c28db 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -56,7 +56,7 @@ const int FIRMWARE_VERSION_MINOR = 0; // the minor firmware version #define LRS_IDENTIFIER (FIRMWARE_VERSION_MAJOR * 0x10 + FIRMWARE_VERSION_MINOR) -#define CLOCK_SYNC_BYTES sizeof(uint16_t) //Number of bytes used within in ACK packet for clock sync (uint16_t msToNextHop) +#define CHANNEL_TIMER_BYTES sizeof(uint16_t) //Number of bytes used within in control header for clock sync (uint16_t msToNextHop) #define CLOCK_MILLIS_BYTES sizeof(unsigned long) //Number of bytes used within in various packets for system timestamps sync (unsigned long currentMillis) #define MAX_PACKET_SIZE 255 //Limited by SX127x #define AES_IV_BYTES 12 //Number of bytes for AESiv diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index caceec4e..be5c1693 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -63,7 +63,7 @@ void configureRadio() success = false; //Precalculate the ACK packet time - ackAirTime = calcAirTime(headerBytes + CLOCK_SYNC_BYTES + trailerBytes); //Used for response timeout during ACK + ackAirTime = calcAirTime(headerBytes + CHANNEL_TIMER_BYTES + trailerBytes); //Used for response timeout during ACK if ((settings.debug == true) || (settings.debugRadio == true)) { @@ -372,7 +372,7 @@ void generateHopTable() systemPrint("AES IV: "); for (uint8_t i = 0 ; i < AES_IV_BYTES ; i++) - systemPrint(AESiv[i], HEX); + systemPrint(AESiv[i], HEX); systemPrintln(); outputSerialData(true); @@ -1333,7 +1333,7 @@ bool xmitVcAckFrame(int8_t destVc) radioCallHistory[RADIO_CALL_xmitVcAckFrame] = millis(); vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; - vcHeader->length = VC_RADIO_HEADER_BYTES + CLOCK_SYNC_BYTES; + vcHeader->length = VC_RADIO_HEADER_BYTES + CHANNEL_TIMER_BYTES; vcHeader->destVc = destVc; vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; @@ -1574,7 +1574,7 @@ PacketType rcvDatagram() outputSerialData(true); } - //All packets must include the 2-byte control header + //All packets must include the control header if (rxDataBytes < minDatagramSize) { //Display the packet contents diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 17b682cd..9ff0b61c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -758,7 +758,7 @@ void updateRadioState() if (transactionComplete) { - sf6ExpectedSize = headerBytes + CLOCK_SYNC_BYTES + trailerBytes; //Tell SF6 to receive ACK packet + sf6ExpectedSize = headerBytes + CHANNEL_TIMER_BYTES + trailerBytes; //Tell SF6 to receive ACK packet triggerEvent(TRIGGER_LINK_WAIT_FOR_ACK); transactionComplete = false; //Reset ISR flag @@ -1205,7 +1205,6 @@ void updateRadioState() break; case DATAGRAM_PING: - triggerEvent(TRIGGER_MP_SEND_ACK_FOR_PING); //A new radio is saying hello if (settings.server == true) { From 3f74ed433985b3236714370003f104eaecf8f30d Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 16:07:32 -0700 Subject: [PATCH 267/594] Add trigger during debugSync --- Firmware/LoRaSerial_Firmware/Radio.ino | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index be5c1693..a8e90f11 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -186,7 +186,9 @@ bool setRadioFrequency(bool rxAdjust) //Set the new frequency if (radio.setFrequency(radioFrequency) == RADIOLIB_ERR_INVALID_FREQUENCY) return false; - //triggerFrequency(frequency); + + if (settings.debugSync) + triggerFrequency(radioFrequency); //Determine the time in milliseconds when channel zero is reached again nextChannelZeroTimeInMillis = millis() + ((settings.numberOfChannels - channelNumber) * settings.maxDwellTime); From a42b4680feba7bc402b61f5e9161d9eeee073b41 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 16:08:18 -0700 Subject: [PATCH 268/594] Remove SYNC_PROCESSING_OVERHEAD --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/Radio.ino | 1 - 2 files changed, 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 6c5c28db..6eba73dd 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -143,7 +143,6 @@ bool trainViaButton = false; //Allows auto-creation of server if client times ou SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 unsigned long timerStart = 0; //Tracks how long our timer has been running since last hop bool partialTimer = false; //After an ACK we reset and run a partial timer to sync units -const int SYNC_PROCESSING_OVERHEAD = 3; //Number of milliseconds it takes to compute clock deltas before sync'ing clocks uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps reduce amount of time spent petting. unsigned long lastPet = 0; //Remebers time of last WDT pet. diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a8e90f11..4f307c39 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2691,7 +2691,6 @@ void syncChannelTimer() //Calculate the packet airTime based on the size of data received msToNextHopRemote -= calcAirTime(packetLength); - msToNextHopRemote -= SYNC_PROCESSING_OVERHEAD; //Different airspeeds complete the transmitComplete ISR at different rates //We adjust the clock setup as needed From ad6ba486a5effe8b1833e814987e0984b4e137bd Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 17:28:29 -0700 Subject: [PATCH 269/594] Move channel timer into packet header All transmissions now contain 2 bytes of channel timer data, if frequency hopping is enabled. Most packet's timer data is ignored. Depending on the mode, we syncChannelTimer() on different packets: ACK for P2P and Data for MP. --- .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 561 +++++++++--------- Firmware/LoRaSerial_Firmware/States.ino | 16 +- 3 files changed, 302 insertions(+), 276 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 6eba73dd..7e17b6a4 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -397,6 +397,7 @@ unsigned long radioCallHistory[RADIO_CALL_MAX]; unsigned long radioStateHistory[RADIO_MAX_STATE]; uint8_t packetLength = 0; //Total bytes received, used for calculating clock sync times in multi-point mode +int16_t msToNextHopRemote; //Can become negative //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4f307c39..6ccdb919 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -657,16 +657,16 @@ bool xmitDatagramP2PTrainingPing() radioCallHistory[RADIO_CALL_xmitDatagramP2PTrainingPing] = millis(); /* - endOfTxData ---. - | - V - +---------+----------+ - | | | - | Control | Trailer | - | 8 bits | n Bytes | - +---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+ + | Optional | | Optional | Optional | Optional | + | NET ID | Control | C-Timer | SF6 Length | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+ */ - + txControl.datagramType = DATAGRAM_P2P_TRAINING_PING; return (transmitDatagram()); } @@ -687,15 +687,15 @@ bool xmitDatagramP2pTrainingParams() endOfTxData += sizeof(params); /* - endOfTxData ---. - | - V - +----------+---------+--- ... ---+----------+ - | Optional | | Radio | Optional | - | NET ID | Control | Parameters | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +----------+---------+-------------+----------+ - */ + endOfTxData ---. + | + V + +----------+---------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | Radio | Optional | + | NET ID | Control | C-Timer | SF6 Length | Parameters | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+---------+----------+------------+-------------+----------+ + */ txControl.datagramType = DATAGRAM_P2P_TRAINING_PARAMS; return (transmitDatagram()); @@ -767,15 +767,15 @@ bool xmitDatagramP2PPing() endOfTxData += sizeof(unsigned long); /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+ - | | | | | - | NET ID | Control | Millis | Trailer | - | 8 bits | 8 bits | 4 bytes | n Bytes | - +--------+---------+---------+----------+ - */ + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+----------+ + | Optional | | Optional | Optional | | | + | NET ID | Control | C-Timer | SF6 Length | Millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 4 bytes | n Bytes | + +----------+---------+----------+------------+---------+----------+ +*/ txControl.datagramType = DATAGRAM_PING; return (transmitDatagram()); @@ -791,14 +791,14 @@ bool xmitDatagramP2PAck1() endOfTxData += sizeof(unsigned long); /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+ - | | | | Optional | - | NET ID | Control | Millis | Trailer | - | 8 bits | 8 bits | 4 bytes | n Bytes | - +--------+---------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+----------+ + | Optional | | Optional | Optional | | | + | NET ID | Control | C-Timer | SF6 Length | Millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 4 bytes | n Bytes | + +----------+---------+----------+------------+---------+----------+ */ txControl.datagramType = DATAGRAM_ACK_1; @@ -815,14 +815,14 @@ bool xmitDatagramP2PAck2() endOfTxData += sizeof(unsigned long); /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+ - | | | | | - | NET ID | Control | Millis | Trailer | - | 8 bits | 8 bits | 4 bytes | n Bytes | - +--------+---------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+----------+ + | Optional | | Optional | Optional | | | + | NET ID | Control | C-Timer | SF6 Length | Millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 4 bytes | n Bytes | + +----------+---------+----------+------------+---------+----------+ */ txControl.datagramType = DATAGRAM_ACK_2; @@ -839,14 +839,14 @@ bool xmitDatagramP2PCommand() radioCallHistory[RADIO_CALL_xmitDatagramP2PCommand] = millis(); /* - endOfTxData ---. - | - V - +--------+---------+--- ... ---+----------+ - | | | | Optional | - | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +--------+---------+-------------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+---------+----------+------------+-------------+----------+ */ txControl.datagramType = DATAGRAM_REMOTE_COMMAND; @@ -859,14 +859,14 @@ bool xmitDatagramP2PCommandResponse() radioCallHistory[RADIO_CALL_xmitDatagramP2PCommandResponse] = millis(); /* - endOfTxData ---. - | - V - +--------+---------+--- ... ---+----------+ - | | | | Optional | - | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +--------+---------+-------------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+---------+----------+------------+-------------+----------+ */ txControl.datagramType = DATAGRAM_REMOTE_COMMAND_RESPONSE; @@ -879,16 +879,16 @@ bool xmitDatagramP2PData() radioCallHistory[RADIO_CALL_xmitDatagramP2PData] = millis(); /* - endOfTxData ---. - | - V - +--------+---------+--- ... ---+----------+ - | | | | Optional | - | NET ID | Control | Data | Trailer | - | 8 bits | 8 bits | n bytes | n Bytes | - +--------+---------+-------------+----------+ - */ - + endOfTxData ---. + | + V + +----------+---------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+---------+----------+------------+-------------+----------+ + */ + txControl.datagramType = DATAGRAM_DATA; return (transmitDatagram()); } @@ -903,15 +903,15 @@ bool xmitDatagramP2PHeartbeat() endOfTxData += sizeof(currentMillis); /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+ - | | | | Optional | - | NET ID | Control | Millis | Trailer | - | 8 bits | 8 bits | 4 Bytes | n Bytes | - +--------+---------+---------+----------+ - */ + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 4 bytes | n Bytes | + +----------+---------+----------+------------+---------+----------+ + */ txControl.datagramType = DATAGRAM_HEARTBEAT; return (transmitDatagram()); @@ -924,29 +924,15 @@ bool xmitDatagramP2PAck() radioCallHistory[RADIO_CALL_xmitDatagramP2PAck] = millis(); - uint8_t * ackStart = endOfTxData; - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - - //Verify the ACK length - ackLength = endOfTxData - ackStart; - if (ackLength != CLOCK_SYNC_BYTES) - { - systemPrint("ERROR - Please define CLOCK_SYNC_BYTES = "); - systemPrintln(ackLength); - waitForever(); - } - /* - endOfTxData ---. - | - V - +--------+---------+----------+----------+ - | | | Channel | Optional | - | NET ID | Control | Timer | Trailer | - | 8 bits | 8 bits | 2 bytes | n Bytes | - +--------+---------+----------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+ + | Optional | | Optional | Optional | Optional | + | NET ID | Control | C-Timer | SF6 Length | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+ */ txControl.datagramType = DATAGRAM_DATA_ACK; @@ -962,22 +948,16 @@ bool xmitDatagramMpData() { radioCallHistory[RADIO_CALL_xmitDatagramMpData] = millis(); - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - /* - endOfTxData ---. - | - V - +--------+---------+--- ... ---+----------+----------+ - | | | | Channel | Optional | - | NET ID | Control | Data | Timer | Trailer | - | 8 bits | 8 bits | n bytes | 2 bytes | n Bytes | - +--------+---------+-------------+----------+----------+ - */ - - + endOfTxData ---. + | + V + +----------+---------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+---------+----------+------------+-------------+----------+ + */ txControl.datagramType = DATAGRAM_DATA; return (transmitDatagram()); @@ -988,26 +968,22 @@ bool xmitDatagramMpHeartbeat() { radioCallHistory[RADIO_CALL_xmitDatagramMpHeartbeat] = millis(); - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - /* - endOfTxData ---. - | - V - +--------+---------+----------+----------+ - | | | Channel | Optional | - | NET ID | Control | Timer | Trailer | - | 8 bits | 8 bits | 2 bytes | n Bytes | - +--------+---------+----------+----------+ - */ - + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+ + | Optional | | Optional | Optional | Optional | + | NET ID | Control | C-Timer | SF6 Length | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+ + */ + txControl.datagramType = DATAGRAM_HEARTBEAT; return (transmitDatagram()); } -//Ack packet sent by server in response the client ping, includes sync data and channel number +//Ack packet sent by server in response the client ping, includes channel number //During Multipoint scanning, it's possible for the client to get an ack but be 500kHz off //The channel Number ensures that the client gets the next hop correct bool xmitDatagramMpAck() @@ -1017,22 +993,26 @@ bool xmitDatagramMpAck() memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); endOfTxData += sizeof(channelNumber); - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - + if (settings.debugTransmit) + { + systemPrint(" Channel Number: "); + systemPrintln(channelNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } /* - endOfTxData ---. - | - V - +--------+---------+---------+----------+----------+ - | | | Channel | Channel | Optional | - | NET ID | Control | Number | Timer | Trailer | - | 8 bits | 8 bits | 1 byte | 2 bytes | n Bytes | - +--------+---------+---------+----------+----------+ - */ - + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+----------+ + | Optional | | Optional | Optional | Channel | Optional | + | NET ID | Control | C-Timer | SF6 Length | Number | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 1 byte | n Bytes | + +----------+---------+----------+------------+---------+----------+ + */ + txControl.datagramType = DATAGRAM_ACK_1; return (transmitDatagram()); } @@ -1043,14 +1023,14 @@ bool xmitDatagramMpPing() radioCallHistory[RADIO_CALL_xmitDatagramMpPing] = millis(); /* - endOfTxData ---. - | - V - +--------+---------+----------+ - | | | Optional | - | NET ID | Control | Trailer | - | 8 bits | 8 bits | n Bytes | - +--------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+ + | Optional | | Optional | Optional | Optional | + | NET ID | Control | C-Timer | SF6 Length | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+ */ txControl.datagramType = DATAGRAM_PING; @@ -1071,14 +1051,14 @@ bool xmitDatagramTrainingPing() endOfTxData += UNIQUE_ID_BYTES; /* - endOfTxData ---. - | - V - +----------+---------+-----------+----------+ - | Optional | | | Optional | - | NET ID | Control | Client ID | Trailer | - | 8 bits | 8 bits | 16 Bytes | n Bytes | - +----------+---------+-----------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+-----------+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Client ID | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 16 Bytes | n Bytes | + +----------+---------+----------+------------+-----------+----------+ */ txControl.datagramType = DATAGRAM_TRAINING_PING; @@ -1099,14 +1079,14 @@ bool xmitDatagramTrainingAck(uint8_t * serverID) endOfTxData += UNIQUE_ID_BYTES; /* - endOfTxData ---. - | - V - +----------+---------+-----------+-----------+----------+ - | Optional | | | | Optional | - | NET ID | Control | Server ID | Client ID | Trailer | - | 8 bits | 8 bits | 16 Bytes | 16 Bytes | n Bytes | - +----------+---------+-----------+-----------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+-----------+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Client ID | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 16 Bytes | n Bytes | + +----------+---------+----------+------------+-----------+----------+ */ txControl.datagramType = DATAGRAM_TRAINING_ACK; @@ -1208,7 +1188,7 @@ void updateRadioParameters(uint8_t * rxData) } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Multi-Point Server Training +//Server Training //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Build the server parameters packet used for training @@ -1235,14 +1215,14 @@ bool xmitDatagramTrainRadioParameters(const uint8_t * clientID) endOfTxData += sizeof(params); /* - endOfTxData ---. - | - V - +----------+---------+-----------+-----------+--- ... ---+----------+ - | Optional | | | | Radio | Optional | - | NET ID | Control | Client ID | Server ID | Parameters | Trailer | - | 8 bits | 8 bits | 16 Bytes | 16 Bytes | n bytes | n Bytes | - +----------+---------+-----------+-----------+-------------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+-----------+-----------+--- ... ---+----------+ + | Optional | | Optional | Optional | | | Radio | Optional | + | NET ID | Control | C-Timer | SF6 Length | Client ID | Server ID | Parameters | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 16 Bytes | 16 Bytes | n bytes | n Bytes | + +----------+---------+----------+------------+-----------+-----------+-------------+----------+ */ txControl.datagramType = DATAGRAM_TRAINING_PARAMS; @@ -1259,14 +1239,14 @@ bool xmitVcDatagram() radioCallHistory[RADIO_CALL_xmitVcDatagram] = millis(); /* - endOfTxData ---. - | - V - +----------+---------+--------+----------+---------+---------+----------+ - | Optional | | | | | | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Data | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | n Bytes | n Bytes | - +----------+---------+--------+----------+---------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+---------+---------+----------+ + | Optional | | Optional | Optional | | | | Optional | + | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | n Bytes | n Bytes | + +----------+---------+----------+------------+----------+---------+---------+----------+ */ txControl.datagramType = DATAGRAM_DATAGRAM; @@ -1296,25 +1276,20 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(currentMillis); - //Add the channel timer for HOP synchronization - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); - memcpy(endOfTxData, &msToNextHop, sizeof(msToNextHop)); - endOfTxData += sizeof(msToNextHop); - //Set the length field *txData = (uint8_t)(endOfTxData - txData); /* - endOfTxData ---. - | - V - +----------+---------+--------+----------+---------+----------+---------+----------+----------+ - | Optional | | | | | | | Channel | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Src ID | millis | Timer | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes | 2 bytes || n Bytes | - +----------+---------+--------+----------+---------+----------+---------+----------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+---------+----------+---------+----------+ + | Optional | | Optional | Optional | | | | | Optional | + | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Src ID | millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes | n Bytes | + +----------+---------+----------+------------+----------+---------+----------+---------+----------+ */ - + txControl.datagramType = DATAGRAM_VC_HEARTBEAT; txControl.ackNumber = 0; @@ -1341,16 +1316,16 @@ bool xmitVcAckFrame(int8_t destVc) endOfTxData += VC_RADIO_HEADER_BYTES; /* - endOfTxData ---. - | - V - +--------+---------+--------+----------+---------+----------+----------+ - | | | | | | Channel | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Timer | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | 2 bytes | n Bytes | - +--------+---------+--------+----------+---------+----------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+---------+----------+ + | Optional | | Optional | Optional | | | Optional | + | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+---------+----------+ */ - + //Finish building the ACK frame return xmitDatagramP2PAck(); } @@ -1368,14 +1343,14 @@ bool xmitVcPing(int8_t destVc) endOfTxData += VC_RADIO_HEADER_BYTES; /* - endOfTxData ---. - | - V - +--------+---------+--------+----------+---------+----------+ - | | | | | | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | n Bytes | - +--------+---------+--------+----------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+---------+----------+ + | Optional | | Optional | Optional | | | Optional | + | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+---------+----------+ */ txControl.datagramType = DATAGRAM_PING; @@ -1395,14 +1370,14 @@ bool xmitVcAck1(int8_t destVc) endOfTxData += VC_RADIO_HEADER_BYTES; /* - endOfTxData ---. - | - V - +--------+---------+--------+----------+---------+----------+ - | | | | | | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | n Bytes | - +--------+---------+--------+----------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+---------+----------+ + | Optional | | Optional | Optional | | | Optional | + | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+---------+----------+ */ txControl.datagramType = DATAGRAM_ACK_1; @@ -1422,14 +1397,14 @@ bool xmitVcAck2(int8_t destVc) endOfTxData += VC_RADIO_HEADER_BYTES; /* - endOfTxData ---. - | - V - +--------+---------+--------+----------+---------+----------+ - | | | | | | Optional | - | NET ID | Control | Length | DestAddr | SrcAddr | Trailer | - | 8 bits | 8 bits | 8 bits | 8 bits | 8 bits | n Bytes | - +--------+---------+--------+----------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+---------+----------+ + | Optional | | Optional | Optional | | | Optional | + | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+---------+----------+ */ txControl.datagramType = DATAGRAM_ACK_2; @@ -1508,13 +1483,13 @@ PacketType rcvDatagram() vcHeader = NULL; /* - |<---------------------- rxDataBytes ---------------------->| - | | - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ + |<--------------------------- rxDataBytes --------------------------->| + | | + +----------+---------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+---------+----------+------------+-------------+----------+ ^ | '---- rxData @@ -1603,11 +1578,11 @@ PacketType rcvDatagram() /* |<---------------------- rxDataBytes ---------------------->| | | - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ ^ | '---- rxData @@ -1687,13 +1662,13 @@ PacketType rcvDatagram() } /* - |<---------------------- rxDataBytes ---------------------->| - | | - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ + |<--------------------------- rxDataBytes ---------------------------->| + | | + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ ^ | '---- rxData @@ -1718,6 +1693,23 @@ PacketType rcvDatagram() return (DATAGRAM_BAD); } + //If hopping is enabled, sync data is located next within the header + if (settings.frequencyHop == true) + { + memcpy(&msToNextHopRemote, rxData, sizeof(msToNextHopRemote)); + rxData += sizeof(msToNextHopRemote); + + //Display the channel timer + if (settings.debugReceive) + { + systemPrint(" Channel Timer(ms): "); + systemPrintln(msToNextHopRemote); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + } + //Display the CRC if (settings.enableCRC16 && settings.debugReceive) { @@ -1731,13 +1723,13 @@ PacketType rcvDatagram() } /* - |<---------------------- rxDataBytes ---------------------->| - | | - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ + |<--------------------------- rxDataBytes ---------------------------->| + | | + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ ^ | '---- rxData @@ -1973,16 +1965,16 @@ PacketType rcvDatagram() } /* - |<-- rxDataBytes -->| - | | - +----------+----------+------------+------ ... ------+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------------+----------+ - ^ - | - '---- rxData + |<-- rxDataBytes -->| + | | + +----------+----------+----------+------------+------ ... ------+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------------+----------+ + ^ + | + '---- rxData */ //Display the packet contents @@ -2299,6 +2291,24 @@ bool transmitDatagram() if (settings.debugTransmit) printControl(control); + //Add the clock sync information + if (settings.frequencyHop == true) + { + uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + memcpy(header, &msToNextHop, sizeof(msToNextHop)); + header += sizeof(msToNextHop); //aka CHANNEL_TIMER_BYTES + + if (settings.debugTransmit) + { + systemPrintTimestamp(); + systemPrint(" Channel Timer(ms): "); + systemPrintln(msToNextHop); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + } + //Add the spread factor 6 length if required if (settings.radioSpreadFactor == 6) { @@ -2314,15 +2324,14 @@ bool transmitDatagram() case DATAGRAM_PING: case DATAGRAM_ACK_1: case DATAGRAM_ACK_2: - txDatagramSize = headerBytes + CLOCK_MILLIS_BYTES; //Short packet is 3 + 4 + txDatagramSize = headerBytes + CLOCK_MILLIS_BYTES; //Short packet is 5 + 4 break; case DATAGRAM_DATA_ACK: - txDatagramSize = headerBytes + CLOCK_SYNC_BYTES; //Short ACK packet is 3 + 2 + txDatagramSize = headerBytes; //Short ACK packet is 5 break; } - radio.implicitHeader(txDatagramSize); //Set header size so that hardware CRC is calculated correctly endOfTxData = &outgoingPacket[txDatagramSize]; @@ -2683,14 +2692,18 @@ void stopChannelTimer() //adjust our own channelTimer interrupt to be synchronized with the remote unit void syncChannelTimer() { - int16_t msToNextHopRemote; //Can become negative - memcpy(&msToNextHopRemote, &rxVcData[rxDataBytes - 2], sizeof(msToNextHopRemote)); - radioCallHistory[RADIO_CALL_syncChannelTimer] = millis(); + + if (settings.frequencyHop == false) return; + + //msToNextHopRemote is obtained during rcvDatagram() + //If the sync arrived in an ACK, we know how long that packet took to transmit //Calculate the packet airTime based on the size of data received msToNextHopRemote -= calcAirTime(packetLength); + // if (settings.debugReceive == true) + // msToNextHopRemote -= 91; //Must adjust for the blob of text being printed //Different airspeeds complete the transmitComplete ISR at different rates //We adjust the clock setup as needed diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9ff0b61c..5ccad975 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1071,7 +1071,15 @@ void updateRadioState() syncChannelTimer(); //Start and adjust freq hop ISR based on remote's remaining clock channelNumber = rxData[0]; //Change to the server's channel number - + if (settings.debugReceive) + { + systemPrint(" Channel Number: "); + systemPrintln(channelNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + frequencyCorrection += radio.getFrequencyError() / 1000000.0; lastPacketReceived = millis(); //Reset @@ -1239,7 +1247,7 @@ void updateRadioState() setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer //Place any available data in the serial output buffer - serialBufferOutput(rxData, rxDataBytes - sizeof(uint16_t)); //Remove the two bytes of sync data + serialBufferOutput(rxData, rxDataBytes); frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -2207,6 +2215,10 @@ void selectHeaderAndTrailerBytes() //Add the control byte to the header headerBytes += 1; + //Add channel timer bytes to header + if (settings.frequencyHop == true) + headerBytes += CHANNEL_TIMER_BYTES; + //Add the byte containing the frame size (only needed in SF6) if (settings.radioSpreadFactor == 6) headerBytes += 1; From 3e8ea3de3d99d40c8eb2dfb680d5280e1e431315 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 18:56:43 -0700 Subject: [PATCH 270/594] Prevent auto-server training when mode is not P2P. --- Firmware/LoRaSerial_Firmware/States.ino | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 5ccad975..7626e538 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1079,7 +1079,7 @@ void updateRadioState() if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } - + frequencyCorrection += radio.getFrequencyError() / 1000000.0; lastPacketReceived = millis(); //Reset @@ -1427,7 +1427,11 @@ void updateRadioState() //Check for a receive timeout else if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) { - if (trainViaButton) + //If we are training with button, in P2P mode, and user has not set server mode + //Automatically switch to server + if (trainViaButton + && originalSettings.operatingMode == MODE_POINT_TO_POINT + && originalServer == false) { //Give up and change to Server automatically From 42d3d0b0c8a4db85428a42580e34d3c47ff9d7a0 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 19:32:38 -0700 Subject: [PATCH 271/594] Clear RSSI LEDs when MP link is down Only clears Clients. Server is never down and displays the last incoming packet RSSI. --- Firmware/LoRaSerial_Firmware/States.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 7626e538..411d4868 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1014,6 +1014,9 @@ void updateRadioState() multipointChannelLoops = 0; multipointAttempts = 0; + //Clear residual RSSI to make sure RSSI LEDs are off + if (rssi > -200) rssi = -200; + triggerEvent(TRIGGER_MP_SCAN); changeState(RADIO_MP_SCANNING); break; From 33ac727467c215f53f43f91656b8b4070ec42e5f Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 19:37:29 -0700 Subject: [PATCH 272/594] Trim clock sync. Update comments. --- Firmware/LoRaSerial_Firmware/Radio.ino | 48 +++++++++++++++++--------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6ccdb919..b7fed4c0 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1693,6 +1693,30 @@ PacketType rcvDatagram() return (DATAGRAM_BAD); } + //Display the CRC + if (settings.enableCRC16 && settings.debugReceive) + { + systemPrintTimestamp(); + systemPrint(" CRC-16: 0x"); + systemPrint(incomingBuffer[rxDataBytes - 2], HEX); + systemPrintln(incomingBuffer[rxDataBytes - 1], HEX); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + /* + |<--------------------------- rxDataBytes ---------------------------->| + | | + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ + ^ + | + '---- rxData + */ //If hopping is enabled, sync data is located next within the header if (settings.frequencyHop == true) { @@ -1710,18 +1734,6 @@ PacketType rcvDatagram() } } - //Display the CRC - if (settings.enableCRC16 && settings.debugReceive) - { - systemPrintTimestamp(); - systemPrint(" CRC-16: 0x"); - systemPrint(incomingBuffer[rxDataBytes - 2], HEX); - systemPrintln(incomingBuffer[rxDataBytes - 1], HEX); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - /* |<--------------------------- rxDataBytes ---------------------------->| | | @@ -1730,9 +1742,9 @@ PacketType rcvDatagram() | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | +----------+----------+----------+------------+-------------+----------+ - ^ - | - '---- rxData + ^ + | + '---- rxData */ //Get the spread factor 6 length @@ -1884,6 +1896,8 @@ PacketType rcvDatagram() //Verify the packet number last so that the expected datagram or ACK number can be updated if (vc && (settings.operatingMode != MODE_MULTIPOINT)) { + Serial.println("Not here for multipoint!"); + switch (datagramType) { default: @@ -2727,12 +2741,12 @@ void syncChannelTimer() msToNextHopRemote -= getReceiveCompletionOffset(); break; case (4800): - msToNextHopRemote -= 2; + msToNextHopRemote -= 5; break; case (9600): break; case (19200): - msToNextHopRemote -= 2; + msToNextHopRemote -= 4; break; case (28800): msToNextHopRemote -= 2; From d78e9ee71838f9d8aa57f7c4b4496608b0822511 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 19:44:03 -0700 Subject: [PATCH 273/594] Change ATI8 menu text --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index f1e2d8c9..526def47 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -192,7 +192,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI5 - Show max possible bytes per second"); systemPrintln(" ATI6 - Display AES key"); systemPrintln(" ATI7 - Show current FHSS channel"); - systemPrintln(" ATI8 - Display unique ID"); + systemPrintln(" ATI8 - Display system unique ID"); systemPrintln(" ATI9 - Display the maximum datagram size"); systemPrintln(" ATI10 - Display radio metrics"); systemPrintln(" ATI11 - Return myVc value"); @@ -227,7 +227,7 @@ bool commandAT(const char * commandString) case ('7'): //ATI7 - Show current FHSS channel systemPrintln(channelNumber); break; - case ('8'): //ATI8 - Display the unique ID + case ('8'): //ATI8 - Display the system's unique ID systemPrintUniqueID(myUniqueId); systemPrintln(); break; From 70979f2b058ec474f88f987312678fef5c57d481 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 19:48:32 -0700 Subject: [PATCH 274/594] Fold &W and &F into normal commands --- Firmware/LoRaSerial_Firmware/Commands.ino | 64 ++++++++--------------- 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 526def47..a3bc1c4b 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -56,6 +56,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATB - Break the link"); systemPrintln(" ATC - Establish VC connection for data"); systemPrintln(" ATD - Display the debug settings"); + systemPrintln(" ATF - Restore factory settings"); systemPrintln(" ATG - Generate new netID and encryption key"); systemPrintln(" ATI - Display the radio version"); systemPrintln(" ATI? - Display the information commands"); @@ -66,12 +67,11 @@ bool commandAT(const char * commandString) systemPrintln(" ATS - Display the serial settings"); systemPrintln(" ATT - Enter training mode"); systemPrintln(" ATV - Display virtual circuit settings"); + systemPrintln(" ATW - Save current settings to NVM"); systemPrintln(" ATZ - Reboot the radio"); systemPrintln(" AT-Param=xxx - Set parameter's value to xxx by name (Param)"); systemPrintln(" AT-Param? - Print parameter's current value by name (Param)"); systemPrintln(" AT-? - Display the setting values"); - systemPrintln(" AT&F - Restore factory settings"); - systemPrintln(" AT&W - Save current settings to NVM"); break; case ('A'): //ATA - Get the current VC status for (int index = 0; index < MAX_VC; index++) @@ -96,7 +96,7 @@ bool commandAT(const char * commandString) //Flag the links as broken if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { - for (int i=0; i < MAX_VC; i++) + for (int i = 0; i < MAX_VC; i++) if ((virtualCircuitList[i].vcState != VC_STATE_LINK_DOWN) && (i != myVc)) vcBreakLink(i); } @@ -121,7 +121,7 @@ bool commandAT(const char * commandString) break; case ('C'): //ATC - Establish VC connection for data if ((settings.operatingMode != MODE_VIRTUAL_CIRCUIT) - || (vc->vcState != VC_STATE_LINK_ALIVE)) + || (vc->vcState != VC_STATE_LINK_ALIVE)) reportERROR(); else { @@ -132,6 +132,11 @@ bool commandAT(const char * commandString) reportOK; } break; + case ('F'): //ATF - Restore default parameters + settings = defaultSettings; //Overwrite all current settings with defaults + recordSystemSettings(); + reportOK(); + break; case ('G'): //ATG - Generate a new netID and encryption key generateRandomKeysID(); reportOK(); @@ -167,6 +172,10 @@ bool commandAT(const char * commandString) reportOK(); selectTraining(); break; + case ('W'): //ATW - Write parameters to the flash memory + recordSystemSettings(); + reportOK(); + break; case ('Z'): //ATZ - Reboots the radio reportOK(); outputSerialData(true); @@ -536,33 +545,6 @@ bool commandAT(const char * commandString) break; } } - - //AT&x commands - else if (commandString[2] == '&') - { - //&W and &F - if (commandLength == 4) - { - switch (commandString[3]) - { - case ('W'): //AT&W - Write parameters to the flash memory - { - recordSystemSettings(); - reportOK(); - } - break; - case ('F'): //AT&F - Restore default parameters - { - settings = defaultSettings; //Overwrite all current settings with defaults - recordSystemSettings(); - reportOK(); - } - break; - default: - return false; - } - } - } else return false; return true; @@ -620,7 +602,7 @@ void checkCommand() //Remove trailing CR and LF while ((commandLength > 0) && ((commandString[commandLength - 1] == '\r') - || (commandString[commandLength -1] == '\n'))) + || (commandString[commandLength - 1] == '\n'))) commandLength -= 1; //Upper case the command @@ -874,7 +856,7 @@ bool valSpeedSerial (void * value, uint32_t valMin, uint32_t valMax) const COMMAND_ENTRY commands[] = { /*Debug parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ + Ltr, All, min, max, digits, type, validation, name, setting addr */ {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &settings.copyDebug}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, @@ -899,7 +881,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 255, 0, TYPE_U8, valInt, "SelectLedUse", &settings.selectLedUse}, /*Radio parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ + Ltr, All, min, max, digits, type, validation, name, setting addr */ {'R', 0, 0, 0, 0, TYPE_SPEED_AIR, valSpeedAir, "AirSpeed", &settings.airSpeed}, {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "AutoTune", &settings.autoTuneFrequency}, {'R', 0, 0, 0, 2, TYPE_FLOAT, valBandwidth, "Bandwidth", &settings.radioBandwidth}, @@ -915,7 +897,7 @@ const COMMAND_ENTRY commands[] = {'R', 0, 14, 30, 0, TYPE_U8, valInt, "TxPower", &settings.radioBroadcastPower_dbm}, /*Radio protocol parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ + Ltr, All, min, max, digits, type, validation, name, setting addr */ {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "DataScrambling", &settings.dataScrambling}, {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &settings.enableCRC16}, {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "EncryptData", &settings.encryptData}, @@ -930,7 +912,7 @@ const COMMAND_ENTRY commands[] = {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &settings.verifyRxNetID}, /*Serial parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ + Ltr, All, min, max, digits, type, validation, name, setting addr */ {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "CopySerial", &settings.copySerial}, {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "Echo", &settings.echo}, {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "FlowControl", &settings.flowControl}, @@ -940,13 +922,13 @@ const COMMAND_ENTRY commands[] = {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "UsbSerialWait", &settings.usbSerialWait}, /*Training parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ + Ltr, All, min, max, digits, type, validation, name, setting addr */ {'R', 0, 1, 255, 0, TYPE_U8, valInt, "ClientPingRetryInterval", &settings.clientPingRetryInterval}, {'R', 0, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &settings.trainingKey}, {'R', 0, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &settings.trainingTimeout}, /*Trigger parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ + Ltr, All, min, max, digits, type, validation, name, setting addr */ {'P', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &settings.copyTriggers}, {'P', 1, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_31-0", &settings.triggerEnable}, {'P', 1, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_63-32", &settings.triggerEnable2}, @@ -954,8 +936,8 @@ const COMMAND_ENTRY commands[] = {'P', 1, 0, 1, 0, TYPE_BOOL, valInt, "TriggerWidthIsMultiplier", &settings.triggerWidthIsMultiplier}, /*Virtual circuit parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'V', 0, 0, MAX_VC-1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, + Ltr, All, min, max, digits, type, validation, name, setting addr */ + {'V', 0, 0, MAX_VC - 1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, }; const int commandCount = sizeof(commands) / sizeof(commands[0]); @@ -1146,7 +1128,7 @@ void displayParameters(char letter, bool displayAll) for (index = 0; index < commandCount; index++) { if (displayAll || (letter == commands[sortOrder[index]].letter) - || ((letter == 0) && (!commands[sortOrder[index]].requireAll))) + || ((letter == 0) && (!commands[sortOrder[index]].requireAll))) { petWDT(); //Printing may take longer than WDT at 9600, so pet the WDT. From 9d8986977dc92293924fce46aa0ed33861774440 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 10 Dec 2022 19:57:59 -0700 Subject: [PATCH 275/594] Remove print --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 -- 1 file changed, 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index b7fed4c0..49369f3b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1896,8 +1896,6 @@ PacketType rcvDatagram() //Verify the packet number last so that the expected datagram or ACK number can be updated if (vc && (settings.operatingMode != MODE_MULTIPOINT)) { - Serial.println("Not here for multipoint!"); - switch (datagramType) { default: From 8d57e797ab1b82959474fbe58a980a072aac7c11 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sun, 11 Dec 2022 13:43:59 -0700 Subject: [PATCH 276/594] Add yield to enable simultaneous 2 way communication This adds a requestYield bit to the control header. It is only set during ACK datagrams (so P2P and VC modes). The requestYield bit is set and sent with an ACK if data is available and waiting in the radioTXBuffer. If a yield request is received, the system will yield for framesToYield * maxFrameAirtime before continuing to send data. By default this is 3 * max air frames. The system's yield state can be set or cleared by the system sending the ACK. If two systems are transmitting at the same time, this turns into a ping/pong hand off. --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + .../LoRaSerial_Firmware.ino | 4 + Firmware/LoRaSerial_Firmware/Radio.ino | 107 ++++++++++++------ Firmware/LoRaSerial_Firmware/States.ino | 41 ++++--- Firmware/LoRaSerial_Firmware/settings.h | 8 +- 5 files changed, 113 insertions(+), 48 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index a3bc1c4b..3b854fa2 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -902,6 +902,7 @@ const COMMAND_ENTRY commands[] = {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &settings.enableCRC16}, {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "EncryptData", &settings.encryptData}, {'R', 0, 0, 0, 0, TYPE_KEY, valKey, "EncryptionKey", &settings.encryptionKey}, + {'R', 0, 0, 255, 0, TYPE_U8, valInt, "FramesToYield", &settings.framesToYield}, {'R', 0, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &settings.serialTimeoutBeforeSendingFrame_ms}, {'R', 0, 250, 65535, 0, TYPE_U16, valInt, "HeartBeatTimeout", &settings.heartbeatTimeout}, {'R', 0, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &settings.maxResends}, diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 7e17b6a4..af308e0c 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -345,6 +345,7 @@ unsigned long lastTrainBlink = 0; //Controls LED during training uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission uint16_t ackAirTime = 0; //Recalc'd with each change of settings +uint16_t maxPacketAirTime = 0; //Recalc'd with each change of settings uint8_t frameSentCount = 0; //Increases each time a frame is sent unsigned long lastPacketReceived = 0; //Controls link LED in broadcast mode @@ -399,6 +400,9 @@ unsigned long radioStateHistory[RADIO_MAX_STATE]; uint8_t packetLength = 0; //Total bytes received, used for calculating clock sync times in multi-point mode int16_t msToNextHopRemote; //Can become negative +bool requestYield = false; //Datagram sender can tell this radio to stop transmitting to enable two-way comm +unsigned long yieldTimerStart = 0; + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - Radio Protocol diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 49369f3b..2c554217 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -62,8 +62,9 @@ void configureRadio() if (radio.setFHSSHoppingPeriod(hoppingPeriod) != RADIOLIB_ERR_NONE) success = false; - //Precalculate the ACK packet time + //Precalculate the packet times ackAirTime = calcAirTime(headerBytes + CHANNEL_TIMER_BYTES + trailerBytes); //Used for response timeout during ACK + maxPacketAirTime = calcAirTime(MAX_PACKET_SIZE); if ((settings.debug == true) || (settings.debugRadio == true)) { @@ -273,7 +274,7 @@ float calcSymbolTime() //Given spread factor, bandwidth, coding rate and frame size, return most bytes we can push per second uint16_t calcMaxThroughput() { - uint8_t mostFramesPerSecond = 1000 / calcAirTime(MAX_PACKET_SIZE); + uint8_t mostFramesPerSecond = 1000 / maxPacketAirTime; uint16_t mostBytesPerSecond = maxDatagramSize * mostFramesPerSecond; return (mostBytesPerSecond); @@ -557,7 +558,7 @@ void updateHopISR() { hop = false; radio.clearFHSSInt(); //Clear the interrupt - } + } } //As we complete linkup, different airspeeds exit at different rates @@ -666,7 +667,7 @@ bool xmitDatagramP2PTrainingPing() | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | +----------+---------+----------+------------+----------+ */ - + txControl.datagramType = DATAGRAM_P2P_TRAINING_PING; return (transmitDatagram()); } @@ -695,7 +696,7 @@ bool xmitDatagramP2pTrainingParams() | NET ID | Control | C-Timer | SF6 Length | Parameters | Trailer | | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | +----------+---------+----------+------------+-------------+----------+ - */ + */ txControl.datagramType = DATAGRAM_P2P_TRAINING_PARAMS; return (transmitDatagram()); @@ -775,7 +776,7 @@ bool xmitDatagramP2PPing() | NET ID | Control | C-Timer | SF6 Length | Millis | Trailer | | 8 bits | 8 bits | 2 bytes | 8 bits | 4 bytes | n Bytes | +----------+---------+----------+------------+---------+----------+ -*/ + */ txControl.datagramType = DATAGRAM_PING; return (transmitDatagram()); @@ -887,8 +888,8 @@ bool xmitDatagramP2PData() | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | +----------+---------+----------+------------+-------------+----------+ - */ - + */ + txControl.datagramType = DATAGRAM_DATA; return (transmitDatagram()); } @@ -911,17 +912,15 @@ bool xmitDatagramP2PHeartbeat() | NET ID | Control | C-Timer | SF6 Length | Millis | Trailer | | 8 bits | 8 bits | 2 bytes | 8 bits | 4 bytes | n Bytes | +----------+---------+----------+------------+---------+----------+ - */ + */ txControl.datagramType = DATAGRAM_HEARTBEAT; return (transmitDatagram()); } -//Create short packet of 2 control bytes - do not expect ack +//Create short packet - do not expect ack bool xmitDatagramP2PAck() { - int ackLength; - radioCallHistory[RADIO_CALL_xmitDatagramP2PAck] = millis(); /* @@ -957,7 +956,7 @@ bool xmitDatagramMpData() | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | +----------+---------+----------+------------+-------------+----------+ - */ + */ txControl.datagramType = DATAGRAM_DATA; return (transmitDatagram()); @@ -977,8 +976,8 @@ bool xmitDatagramMpHeartbeat() | NET ID | Control | C-Timer | SF6 Length | Trailer | | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | +----------+---------+----------+------------+----------+ - */ - + */ + txControl.datagramType = DATAGRAM_HEARTBEAT; return (transmitDatagram()); } @@ -1011,8 +1010,8 @@ bool xmitDatagramMpAck() | NET ID | Control | C-Timer | SF6 Length | Number | Trailer | | 8 bits | 8 bits | 2 bytes | 8 bits | 1 byte | n Bytes | +----------+---------+----------+------------+---------+----------+ - */ - + */ + txControl.datagramType = DATAGRAM_ACK_1; return (transmitDatagram()); } @@ -1130,6 +1129,7 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.overheadTime = params.overheadTime; originalSettings.server = params.server; originalSettings.verifyRxNetID = params.verifyRxNetID; + originalSettings.framesToYield = params.framesToYield; //Update the debug parameters if (params.copyDebug) @@ -1289,7 +1289,7 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes | n Bytes | +----------+---------+----------+------------+----------+---------+----------+---------+----------+ */ - + txControl.datagramType = DATAGRAM_VC_HEARTBEAT; txControl.ackNumber = 0; @@ -1325,7 +1325,7 @@ bool xmitVcAckFrame(int8_t destVc) | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | n Bytes | +----------+---------+----------+------------+----------+---------+----------+ */ - + //Finish building the ACK frame return xmitDatagramP2PAck(); } @@ -1678,8 +1678,10 @@ PacketType rcvDatagram() rxControl = *((CONTROL_U8 *)rxData++); datagramType = rxControl.datagramType; ackNumber = rxControl.ackNumber; + if (settings.debugReceive) printControl(*((uint8_t *)&rxControl)); + if (datagramType >= MAX_DATAGRAM_TYPE) { if (settings.debugReceive || settings.debugDatagrams) @@ -1704,7 +1706,7 @@ PacketType rcvDatagram() if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); } - + /* |<--------------------------- rxDataBytes ---------------------------->| | | @@ -1976,6 +1978,16 @@ PacketType rcvDatagram() vc->framesReceived++; } + //If packet is good, check requestYield bit + //If bit is set, this radio supresses transmissions for X number of frames + //Datagram sender can both set and clear yield requests + requestYield = rxControl.requestYield; + if (requestYield) + { + triggerEvent(TRIGGER_LINK_REQUEST_YIELD_RECEIVED); + yieldTimerStart = millis(); + } + /* |<-- rxDataBytes -->| | | @@ -2038,6 +2050,9 @@ PacketType rcvDatagram() systemPrint("ACK #"); systemPrint(ackNumber); systemPrint(")"); + + if (rxControl.requestYield) + systemPrint(" (Y)"); } systemPrintln(); break; @@ -2187,7 +2202,17 @@ bool transmitDatagram() else txControl.ackNumber = vc->txAckNumber; - //Process the packet + //If we are ACK'ing data, and we have data to send ourselves, request that + //the sender yield to give us an opportunity to send our data + if ((txControl.datagramType == DATAGRAM_DATA_ACK) && availableRadioTXBytes()) + { + triggerEvent(TRIGGER_LINK_REQUEST_YIELD_SENT); + txControl.requestYield = 1; + } + else + txControl.requestYield = 0; + + //Print debug info as needed if (settings.debugDatagrams) { systemPrintTimestamp(); @@ -2219,6 +2244,9 @@ bool transmitDatagram() systemPrint("ACK #"); systemPrint(txControl.ackNumber); systemPrint(")"); + + if (txControl.requestYield) + systemPrint(" (requestYield)"); } systemPrintln(); break; @@ -2470,7 +2498,7 @@ bool transmitDatagram() outputSerialData(true); } - //Print before encryption + //Print raw packet bytes before encryption if (settings.debugTransmit) { systemPrint("out: "); @@ -2531,13 +2559,17 @@ void printControl(uint8_t value) systemPrintTimestamp(); systemPrint(" Control: 0x"); systemPrintln(value, HEX); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); + systemPrintTimestamp(); systemPrint(" ACK # "); systemPrintln(value & 3); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); + systemPrintTimestamp(); systemPrint(" datagramType "); if (control->datagramType < MAX_DATAGRAM_TYPE) @@ -2547,9 +2579,19 @@ void printControl(uint8_t value) systemPrint("Unknown "); systemPrintln(control->datagramType); } + + systemPrintTimestamp(); + systemPrint(" requestYield "); + if (control->requestYield) + systemPrintln("1"); + else + systemPrintln("0"); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); + petWDT(); } @@ -2560,13 +2602,13 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) radioCallHistory[RADIO_CALL_retransmitDatagram] = millis(); /* - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - | | - |<-------------------- txDatagramSize --------------------->| + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ + | | + |<------------------------- txDatagramSize --------------------------->| */ if (timeToHop == true) //If the channelTimer has expired, move to next frequency @@ -2596,9 +2638,10 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); - //Drop this datagram if the receiver is active frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond + + //Drop this datagram if the receiver is active if ((receiveInProcess() == true) || (transactionComplete == true) || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN))) @@ -2619,7 +2662,6 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) } else { - int state = radio.startTransmit(outgoingPacket, txDatagramSize); if (state == RADIOLIB_ERR_NONE) @@ -2662,6 +2704,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) } } } + frameAirTime += responseDelay; datagramTimer = millis(); //Move timestamp even if error retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime); //Wait this number of ms between retransmits. Increases with each re-transmit. @@ -2670,7 +2713,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) if (settings.selectLedUse == LEDS_RADIO_USE) digitalWrite(ALT_LED_TX_DATA, LED_ON); - return (true); //Tranmission has started + return (true); //Transmission has started } //Use the maximum dwell setting to start the timer that indicates when to hop channels diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 411d4868..aa0a0a89 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -669,22 +669,35 @@ void updateRadioState() { heartbeatTimeout = ((millis() - heartbeatTimer) > heartbeatRandomTime); - //Check for time to send serial data - if (availableRadioTXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) + //If we have data, try to send it out + if (availableRadioTXBytes()) { - triggerEvent(TRIGGER_LINK_DATA_XMIT); - - if (xmitDatagramP2PData() == true) + //Check if we are yielding for 2-way comm + if (requestYield == false || + (requestYield == true && (millis() - yieldTimerStart > (settings.framesToYield * maxPacketAirTime))) + ) { - setHeartbeatLong(); //We're sending data, so don't be the first to send next heartbeat - transmitTimer = datagramTimer; + //Yield has expired, allow transmit. + requestYield = false; - //Save the previous transmit in case the previous ACK was lost or a - //HEARTBEAT must be transmitted. Restore the buffer when a retransmission - //is necessary. - petWDT(); - SAVE_TX_BUFFER(); - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + //Check for time to send serial data + if (processWaitingSerial(heartbeatTimeout) == true) + { + triggerEvent(TRIGGER_LINK_DATA_XMIT); + + if (xmitDatagramP2PData() == true) + { + setHeartbeatLong(); //We're sending data, so don't be the first to send next heartbeat + transmitTimer = datagramTimer; + + //Save the previous transmit in case the previous ACK was lost or a + //HEARTBEAT must be transmitted. Restore the buffer when a retransmission + //is necessary. + petWDT(); + SAVE_TX_BUFFER(); + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } + } } } else if (availableTXCommandBytes()) //If we have command bytes to send out @@ -1016,7 +1029,7 @@ void updateRadioState() //Clear residual RSSI to make sure RSSI LEDs are off if (rssi > -200) rssi = -200; - + triggerEvent(TRIGGER_MP_SCAN); changeState(RADIO_MP_SCANNING); break; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 6e862f15..2304751f 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -249,6 +249,8 @@ enum TRIGGER_LINK_WAIT_FOR_ACK, TRIGGER_LINK_DATA_XMIT, TRIGGER_LINK_RETRANSMIT_FAIL, + TRIGGER_LINK_REQUEST_YIELD_SENT, + TRIGGER_LINK_REQUEST_YIELD_RECEIVED, TRIGGER_MP_SCAN, TRIGGER_MP_DATA_PACKET, TRIGGER_MP_PACKET_RECEIVED, @@ -309,7 +311,8 @@ typedef struct _CONTROL_U8 { PacketType datagramType: 4; uint8_t ackNumber : 2; - uint8_t filler : 2; + uint8_t requestYield : 1; + uint8_t filler : 1; } CONTROL_U8; typedef bool (* VALIDATION_ROUTINE)(void * value, uint32_t valMin, uint32_t valMax); @@ -419,6 +422,7 @@ typedef struct struct_settings { bool debugNvm = false; //Debug NVM operation bool printAckNumbers = false; //Print the ACK numbers bool debugHeartbeat = false; //Print the HEARTBEAT timing values + uint8_t framesToYield = 3; //If remote requests it, supress transmission for this number of max packet frames //Add new parameters immediately before this line //-- Add commands to set the parameters @@ -432,7 +436,7 @@ struct struct_online { bool eeprom = false; } online; -#include //Click here to get the library: http://librarymanager/All#RadioLib v5.1.0 +#include //Click here to get the library: http://librarymanager/All#RadioLib v5.5.0 typedef void (* ARCH_BEGIN_BOARD)(); typedef void (* ARCH_BEGIN_SERIAL)(uint16_t serialSpeed); From 03e43441de7475050c852a3bfd8725b384823432 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sun, 11 Dec 2022 21:31:00 -0700 Subject: [PATCH 277/594] Move beginBoard() to allow for default system settings modifications --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index af308e0c..443afc9b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -501,6 +501,8 @@ void setup() beginSerial(57600); //Default for debug messages before board begins + beginBoard(); //Determine what hardware platform we are running on. + loadSettings(); //Load settings from EEPROM serialOperatingMode = settings.operatingMode; @@ -522,7 +524,6 @@ void setup() arch.uniqueID(myUniqueId); //Get the unique ID - beginBoard(); //Determine what hardware platform we are running on. beginLoRa(); //Start radio From 01e4366510707d781d8acd82b934bc9f45a9436e Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sun, 11 Dec 2022 21:32:29 -0700 Subject: [PATCH 278/594] Adding board IDs for other radio frequencies --- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 13 ++++++++++++- .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 960008be..f86f4e2b 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -126,10 +126,21 @@ void samdBeginBoard() float boardID = 3.3 * val / 1024; //Use ADC to check board ID resistor divider - if (boardID > 1.64 * 0.9 && boardID < 1.64 * 1.1) + if (boardID > 1.64 * 0.95 && boardID < 1.64 * 1.05) { + radioBand = 915; strcpy(platformPrefix, "SAMD21 1W 915MHz"); } + else if (boardID > 2.20 * 0.95 && boardID < 2.20 * 1.05) + { + radioBand = 433; + strcpy(platformPrefix, "SAMD21 1W 433MHz"); + } + else if (boardID > 2.48 * 0.95 && boardID < 2.48 * 1.05) + { + radioBand = 868; + strcpy(platformPrefix, "SAMD21 1W 868MHz"); + } else { strcpy(platformPrefix, "SAMD21 1W"); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 443afc9b..5542a448 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -342,6 +342,7 @@ unsigned long lastTrainBlink = 0; //Controls LED during training //Global variables - Radio (General) //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +uint16_t radioBand = 0; //In MHz. Detected radio module type. Sets the upper/lower frequency bounds. uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission uint16_t ackAirTime = 0; //Recalc'd with each change of settings From f7ae5ea31970784c14cb50905c778cfb3a4d5308 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sun, 11 Dec 2022 21:33:13 -0700 Subject: [PATCH 279/594] Simplify startup reports --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 5542a448..04e68893 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -509,10 +509,6 @@ void setup() beginSerial(settings.serialSpeed); - systemPrintTimestamp(); - systemPrintln("LRS"); - outputSerialData(true); - verifyTables(); //Verify that the enum counts match the name table lengths //Load the unique IDs for the virtual circuits @@ -537,7 +533,7 @@ void setup() updateRTS(true); //Enable serial input systemPrintTimestamp(); - systemPrintln("LRS Setup Complete"); + systemPrintln("LRS"); outputSerialData(true); triggerEvent(TRIGGER_RADIO_RESET); From 03a0228ebea566dd9e7f3e6fe5ca5ea114e864e3 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sun, 11 Dec 2022 21:34:37 -0700 Subject: [PATCH 280/594] Expand min/max frequencies, add validation of defaults. --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 ++-- Firmware/LoRaSerial_Firmware/NVM.ino | 33 +++++++++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 3b854fa2..64848a6c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -134,6 +134,7 @@ bool commandAT(const char * commandString) break; case ('F'): //ATF - Restore default parameters settings = defaultSettings; //Overwrite all current settings with defaults + validateSettings(); //Modify defaults for each radio type (915, 868, 433, etc) recordSystemSettings(); reportOK(); break; @@ -887,8 +888,8 @@ const COMMAND_ENTRY commands[] = {'R', 0, 0, 0, 2, TYPE_FLOAT, valBandwidth, "Bandwidth", &settings.radioBandwidth}, {'R', 0, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &settings.radioCodingRate}, {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "FrequencyHop", &settings.frequencyHop}, - {'R', 0, 0, 928, 3, TYPE_FLOAT, valFreqMax, "FrequencyMax", &settings.frequencyMax}, - {'R', 0, 902, 0, 3, TYPE_FLOAT, valFreqMin, "FrequencyMin", &settings.frequencyMin}, + {'R', 0, 0, 931, 3, TYPE_FLOAT, valFreqMax, "FrequencyMax", &settings.frequencyMax}, + {'R', 0, 900, 0, 3, TYPE_FLOAT, valFreqMin, "FrequencyMin", &settings.frequencyMin}, {'R', 0, 10, 65535, 0, TYPE_U16, valInt, "MaxDwellTime", &settings.maxDwellTime}, {'R', 0, 1, 255, 0, TYPE_U8, valInt, "NumberOfChannels", &settings.numberOfChannels}, {'R', 0, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &settings.radioPreambleLength}, diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index 4ed28f1e..af045f81 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -50,9 +50,42 @@ void loadSettings() } EEPROM.get(0, settings); + validateSettings(); //Confirm these settings are within regulatory bounds + recordSystemSettings(); } +//Modify defaults for each radio type (915, 868, 433, etc) +//Confirm various settings are within regulatory bounds +void validateSettings() +{ + if (radioBand == 915) + { + //Radio limits are 900-931MHz + //USA ISM bounds are 902-928MHz + if (settings.frequencyMin < 900.0 || settings.frequencyMin > 931.0) settings.frequencyMin = 902.0; + if (settings.frequencyMax > 931.0 || settings.frequencyMax < 900.0) settings.frequencyMax = 928.0; + } + else if (radioBand == 868) + { + //Radio limits are 862-893MHz + //EU ESTI limits are 862-893MHz + if (settings.frequencyMin < 862.0 || settings.frequencyMin > 893.0) settings.frequencyMin = 862.0; + if (settings.frequencyMax > 893.0 || settings.frequencyMax < 862.0) settings.frequencyMax = 893.0; + } + else if (radioBand == 433) + { + //Radio limits are 410-510MHz + //USA amateur radio limits are 420-450MHz + if (settings.frequencyMin < 410.0 || settings.frequencyMin > 510.0) settings.frequencyMin = 420.0; + if (settings.frequencyMax > 510.0 || settings.frequencyMax < 410.0) settings.frequencyMax = 450.0; + } + else + { + systemPrintln("Error: Unknown radioBand"); + } +} + //Record the current settings struct to EEPROM void recordSystemSettings() { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index aa0a0a89..7118c30f 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -180,7 +180,7 @@ void updateRadioState() { stopChannelTimer(); channelNumber = 0; - radio.setFrequency(channels[channelNumber]); + setRadioFrequency(false); } //Clear residual RSSI to make sure RSSI LEDs are off From c3a7bf880290d49854cc7e9169ba123121a33467 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 12 Dec 2022 11:25:32 -0700 Subject: [PATCH 281/594] Change 868MHz connector to SMA --- .../SparkFun LoRaSerial 868MHz - 1W.brd | 182 ++++++++++++++---- .../SparkFun LoRaSerial 868MHz - 1W.sch | 2 +- 2 files changed, 150 insertions(+), 34 deletions(-) diff --git a/Hardware/868MHz/SparkFun LoRaSerial 868MHz - 1W.brd b/Hardware/868MHz/SparkFun LoRaSerial 868MHz - 1W.brd index 0f647ace..c8062aa5 100644 --- a/Hardware/868MHz/SparkFun LoRaSerial 868MHz - 1W.brd +++ b/Hardware/868MHz/SparkFun LoRaSerial 868MHz - 1W.brd @@ -10,8 +10,8 @@ - - + + @@ -21,34 +21,34 @@ - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - + @@ -4997,7 +4997,7 @@ You are welcome to use this library for commercial purposes. For attribution, we - + <b>SMA Antenna Connector</b><p> This is a footprint for an edge mount RF antenna. Works pretty well with SMA type connectors but may also work with other edge mount RF connectors. Keep in mind, these edge mount connectors assume you are using a 0.062" (1.6mm) PCB thickness. @@ -5061,8 +5061,6 @@ This is a footprint for an edge mount RF antenna. Works pretty well with SMA typ - - @@ -6146,8 +6144,8 @@ These are the free and generally easy to produce 4-layer PCB design rules. You c - - + + @@ -6486,6 +6484,124 @@ These are the free and generally easy to produce 4-layer PCB design rules. You c + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Hardware/868MHz/SparkFun LoRaSerial 868MHz - 1W.sch b/Hardware/868MHz/SparkFun LoRaSerial 868MHz - 1W.sch index a05c8420..e2fb8635 100644 --- a/Hardware/868MHz/SparkFun LoRaSerial 868MHz - 1W.sch +++ b/Hardware/868MHz/SparkFun LoRaSerial 868MHz - 1W.sch @@ -43484,7 +43484,7 @@ touch button, slider and wheel user interfaces.</p> - + From 91e986614f77093a11dff953c6fc763d330783b8 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 12 Dec 2022 12:01:09 -0700 Subject: [PATCH 282/594] Move LED blink to end of startup --- Firmware/LoRaSerial_Firmware/Begin.ino | 9 ++------- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 4 +++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 46d1b6a9..1e6d030a 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -1,10 +1,6 @@ -//Based on hardware features, determine which hardware this is -void beginBoard() +//Blink LEDs to indicate the completion of system setup +void blinkStartup() { - //Initialize the board specific hardware - arch.beginBoard(); - - //Dashboard Blink LEDs for (int x = 0 ; x < 3 ; x++) { digitalWrite(pin_rssi1LED, HIGH); @@ -23,7 +19,6 @@ void beginBoard() digitalWrite(pin_rxLED, LOW); delay(50); } - } //Initialize the radio layer diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 04e68893..19281821 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -502,7 +502,7 @@ void setup() beginSerial(57600); //Default for debug messages before board begins - beginBoard(); //Determine what hardware platform we are running on. + arch.beginBoard(); //Initialize the board specific hardware, and ID platform type loadSettings(); //Load settings from EEPROM serialOperatingMode = settings.operatingMode; @@ -537,6 +537,8 @@ void setup() outputSerialData(true); triggerEvent(TRIGGER_RADIO_RESET); + + blinkStartup(); //Blink LEDs to indicate the completion of system setup } //Idle loop for the CPU From be12ae7cec0112712c9e6f4c01f2fc13b6161781 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 12 Dec 2022 13:23:40 -0700 Subject: [PATCH 283/594] Only check RSSI on successful packets. RSSI off when link is down. --- Firmware/LoRaSerial_Firmware/Radio.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/States.ino | 7 ------- Firmware/LoRaSerial_Firmware/System.ino | 5 +++++ 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 2c554217..9eef968d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1437,12 +1437,12 @@ PacketType rcvDatagram() framesReceived++; int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); - rssi = radio.getRSSI(); printPacketQuality(); //Display the RSSI, SNR and frequency error values if (state == RADIOLIB_ERR_NONE) { rxSuccessMillis = rcvTimeMillis; + rssi = radio.getRSSI(); } else { @@ -2204,7 +2204,7 @@ bool transmitDatagram() //If we are ACK'ing data, and we have data to send ourselves, request that //the sender yield to give us an opportunity to send our data - if ((txControl.datagramType == DATAGRAM_DATA_ACK) && availableRadioTXBytes()) + if ((txControl.datagramType == DATAGRAM_DATA_ACK) && availableRadioTXBytes()) { triggerEvent(TRIGGER_LINK_REQUEST_YIELD_SENT); txControl.requestYield = 1; @@ -2640,7 +2640,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond - + //Drop this datagram if the receiver is active if ((receiveInProcess() == true) || (transactionComplete == true) || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) @@ -2704,7 +2704,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) } } } - + frameAirTime += responseDelay; datagramTimer = millis(); //Move timestamp even if error retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime); //Wait this number of ms between retransmits. Increases with each re-transmit. diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 7118c30f..27b8bf24 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -183,9 +183,6 @@ void updateRadioState() setRadioFrequency(false); } - //Clear residual RSSI to make sure RSSI LEDs are off - if (rssi > -200) rssi = -200; - //Determine if a PING was received if (transactionComplete) { @@ -1027,9 +1024,6 @@ void updateRadioState() multipointChannelLoops = 0; multipointAttempts = 0; - //Clear residual RSSI to make sure RSSI LEDs are off - if (rssi > -200) rssi = -200; - triggerEvent(TRIGGER_MP_SCAN); changeState(RADIO_MP_SCANNING); break; @@ -1038,7 +1032,6 @@ void updateRadioState() //Walk through channel table transmitting a Ping and looking for an Ack //==================== case RADIO_MP_SCANNING: - if (transactionComplete) { //Decode the received datagram diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index faf4589c..31e4c5e7 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -755,6 +755,11 @@ void updateLeds() //Set LEDs according to RSSI level default: case LEDS_RSSI: + //Force RSSI LEDs off when link is down + if (radioState == RADIO_MP_SCANNING + || radioState == RADIO_P2P_LINK_DOWN) + rssi = -200; + if (rssi > rssiLevelMax) setRSSI(0b1111); else if (rssi > rssiLevelHigh) From e103da3f96d7a3eb4e8e210babb7cfac72042c01 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 12 Dec 2022 16:32:26 -0700 Subject: [PATCH 284/594] Remove unused var. Change RSSI values. --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 19281821..2093b11a 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -363,11 +363,10 @@ volatile bool hop = true; //Clear the DIO1 hop ISR when possible //RSSI must be above these negative numbers for LED to illuminate const int rssiLevelLow = -150; -const int rssiLevelMed = -70; -const int rssiLevelHigh = -50; -const int rssiLevelMax = -20; +const int rssiLevelMed = -120; +const int rssiLevelHigh = -100; +const int rssiLevelMax = -70; int rssi; //Average signal level, measured during reception of a packet -int avgRssi; //Average signal level, measured during reception of a packet //Link quality metrics uint32_t datagramsSent; //Total number of datagrams sent From 2ae52feb527456a4881823038a541e41f2d71caa Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 12 Dec 2022 16:32:57 -0700 Subject: [PATCH 285/594] Rename PrintPacketQuality. --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/System.ino | 2 +- Firmware/LoRaSerial_Firmware/Train.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 64848a6c..1f170c37 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -870,11 +870,11 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &settings.debugTraining}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &settings.debugSerial}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayPacketQuality", &settings.displayPacketQuality}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &settings.displayRealMillis}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintAckNumbers", &settings.printAckNumbers}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &settings.printFrequency}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &settings.printLinkUpDown}, + {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintPacketQuality", &settings.printPacketQuality}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintPktData", &settings.printPktData}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &settings.printRfData}, {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 9eef968d..35a0a87d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1147,7 +1147,7 @@ void updateRadioParameters(uint8_t * rxData) originalSettings.debugTraining = params.debugTraining; originalSettings.debugTransmit = params.debugTransmit; originalSettings.debugSerial = params.debugSerial; - originalSettings.displayPacketQuality = params.displayPacketQuality; + originalSettings.printPacketQuality = params.printPacketQuality; originalSettings.displayRealMillis = params.displayRealMillis; originalSettings.printAckNumbers = params.printAckNumbers; originalSettings.printFrequency = params.printFrequency; diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 31e4c5e7..a3c1adb0 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -844,7 +844,7 @@ int strnicmp(const char * str1, const char * str2, int length) //Display the RSSI, SNR and frequency error values void printPacketQuality() { - if (settings.displayPacketQuality == true) + if (settings.printPacketQuality == true) { systemPrintln(); systemPrint("R:"); diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 08fad683..7aeb4b6f 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -168,7 +168,7 @@ void commonTrainingInitialization() settings.debugStates = originalSettings.debugStates; settings.debugTraining = originalSettings.debugTraining; settings.debugTransmit = originalSettings.debugTransmit; - settings.displayPacketQuality = originalSettings.displayPacketQuality; + settings.printPacketQuality = originalSettings.printPacketQuality; settings.displayRealMillis = originalSettings.displayRealMillis; settings.printAckNumbers = originalSettings.printAckNumbers; settings.printFrequency = originalSettings.printFrequency; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 2304751f..aa459c02 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -379,7 +379,7 @@ typedef struct struct_settings { uint16_t heartbeatTimeout = 5000; //ms before sending ping to see if link is active bool flowControl = false; //Enable the use of CTS/RTS flow control signals bool autoTuneFrequency = false; //Based on the last packets frequency error, adjust our next transaction frequency - bool displayPacketQuality = false; //Print RSSI, SNR, and freqError for received packets + bool printPacketQuality = false; //Print RSSI, SNR, and freqError for received packets uint8_t maxResends = 0; //Attempt resends up to this number, 0 = infinite retries bool printFrequency = false; //Print the updated frequency bool debugRadio = false; //Print radio info From aac02f94f478ccc2d2698763ce431eb5e8c77e9f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 11 Dec 2022 09:58:49 -1000 Subject: [PATCH 286/594] Move report* to end of commands --- Firmware/LoRaSerial_Firmware/Commands.ino | 190 +++++++++++----------- Firmware/LoRaSerial_Firmware/Radio.ino | 12 +- Firmware/LoRaSerial_Firmware/Serial.ino | 1 - 3 files changed, 97 insertions(+), 106 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 1f170c37..58092d1d 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -40,15 +40,16 @@ bool commandAT(const char * commandString) //'AT' if (commandLength == 2) - reportOK(); + return true; //AT?, ATA, ATB, ATC, ATG, ATI, ATO, ATZ commands - else if (commandLength == 3) + if (commandLength == 3) { switch (commandString[2]) { default: return false; + case ('?'): //Display the command help systemPrintln("Command summary:"); systemPrintln(" AT? - Print the command summary"); @@ -72,26 +73,23 @@ bool commandAT(const char * commandString) systemPrintln(" AT-Param=xxx - Set parameter's value to xxx by name (Param)"); systemPrintln(" AT-Param? - Print parameter's current value by name (Param)"); systemPrintln(" AT-? - Display the setting values"); - break; + return true; + case ('A'): //ATA - Get the current VC status for (int index = 0; index < MAX_VC; index++) vcSendPcStateMessage(index, virtualCircuitList[index].vcState); - reportOK(); - break; + return true; + case ('B'): //ATB - Break the link - reportOK(); //Compute the time delay delayMillis = (VC_LINK_BREAK_MULTIPLIER + 2) * settings.heartbeatTimeout; //Warn the user of the delay - if (settings.printLinkUpDown) - { - systemPrint("Delaying "); - systemPrint(delayMillis / 1000); - systemPrintln(" seconds to break the link"); - outputSerialData(true); - } + systemPrint("Delaying "); + systemPrint(delayMillis / 1000); + systemPrintln(" seconds to break the link"); + outputSerialData(true); //Flag the links as broken if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) @@ -113,86 +111,74 @@ bool commandAT(const char * commandString) petWDT(); //Display the end of the delay - if (settings.printLinkUpDown) - { - systemPrintln("Delay done"); - outputSerialData(true); - } - break; + systemPrintln("Delay done"); + return true; + case ('C'): //ATC - Establish VC connection for data if ((settings.operatingMode != MODE_VIRTUAL_CIRCUIT) || (vc->vcState != VC_STATE_LINK_ALIVE)) - reportERROR(); + return false; + if (cmdVc == myVc) + vcChangeState(cmdVc, VC_STATE_CONNECTED); else - { - if (cmdVc == myVc) - vcChangeState(cmdVc, VC_STATE_CONNECTED); - else - vcChangeState(cmdVc, VC_STATE_SEND_PING); - reportOK; - } - break; + vcChangeState(cmdVc, VC_STATE_SEND_PING); + return true; + case ('F'): //ATF - Restore default parameters settings = defaultSettings; //Overwrite all current settings with defaults validateSettings(); //Modify defaults for each radio type (915, 868, 433, etc) recordSystemSettings(); - reportOK(); - break; + return true; + case ('G'): //ATG - Generate a new netID and encryption key generateRandomKeysID(); - reportOK(); - break; + return true; + case ('I'): //ATI //Shows the radio version - reportOK(); systemPrint("SparkFun LoRaSerial "); systemPrint(platformPrefix); systemPrint(" v"); systemPrint(FIRMWARE_VERSION_MAJOR); systemPrint("."); systemPrintln(FIRMWARE_VERSION_MINOR); - break; + return true; + case ('O'): //ATO - Exit command mode if (printerEndpoint == PRINT_TO_RF) - { //If we are pointed at the RF link, send ok and wait for response ACK before applying settings - reportOK(); - } - else - { - //Apply settings and return - generateHopTable(); //Generate freq with new settings - configureRadio(); //Apply any new settings + return true; + + //Apply raio settings by entering the reset state + inCommandMode = false; //Return to printing normal RF serial data + changeState(RADIO_RESET); + return true; - inCommandMode = false; //Return to printing normal RF serial data - reportOK(); - changeState(RADIO_RESET); - } - break; case ('T'): //ATT - Enter training mode - reportOK(); selectTraining(); - break; + return true; + case ('W'): //ATW - Write parameters to the flash memory recordSystemSettings(); - reportOK(); - break; + return true; + case ('Z'): //ATZ - Reboots the radio reportOK(); outputSerialData(true); systemFlush(); systemReset(); - break; + return true; } } //ATIx commands - else if (commandString[2] == 'I' && commandLength == 4) + if (commandString[2] == 'I' && commandLength == 4) { switch (commandString[3]) { default: return false; + case ('?'): //ATI? - Display the information commands systemPrintln(" ATI0 - Show user settable parameters"); systemPrintln(" ATI1 - Show board variant"); @@ -208,51 +194,62 @@ bool commandAT(const char * commandString) systemPrintln(" ATI11 - Return myVc value"); systemPrintln(" ATI12 - Display the VC details"); systemPrintln(" ATI13 - Dump the radioTxBuffer"); - break; + return true; + case ('0'): //ATI0 - Show user settable parameters displayParameters(0, true); - break; + return true; + case ('1'): //ATI1 - Show board variant systemPrint("SparkFun LoRaSerial "); systemPrint(platformPrefix); systemPrint("\r\n"); - break; + return true; + case ('2'): //ATI2 - Show firmware version systemPrint(FIRMWARE_VERSION_MAJOR); systemPrint("."); systemPrintln(FIRMWARE_VERSION_MINOR); - break; + return true; + case ('3'): //ATI3 - Display latest RSSI systemPrintln(radio.getRSSI()); - break; + return true; + case ('4'): //ATI4 - Get random byte from RSSI systemPrintln(radio.randomByte()); - break; + return true; + case ('5'): //ATI5 - Show max possible bytes per second systemPrintln(calcMaxThroughput()); - break; + return true; + case ('6'): //ATI6 - Display currentState displayState(radioState); - break; + return true; + case ('7'): //ATI7 - Show current FHSS channel systemPrintln(channelNumber); - break; + return true; + case ('8'): //ATI8 - Display the system's unique ID systemPrintUniqueID(myUniqueId); systemPrintln(); - break; + return true; + case ('9'): //ATI9 - Display the maximum datagram size systemPrint("Maximum datagram size: "); systemPrintln(maxDatagramSize); - break; + return true; } } - else if ((commandString[2] == 'I') && (commandString[3] == '1') && (commandLength == 5)) + if ((commandString[2] == 'I') && (commandString[3] == '1') && (commandLength == 5)) { switch (commandString[4]) { default: return false; + case ('0'): //ATI10 - Display radio metrics systemPrint("Radio Metrics @ "); systemPrintTimestamp(millis()); @@ -464,14 +461,14 @@ bool commandAT(const char * commandString) displayRadioCallHistory(); systemPrintln(" State History"); displayRadioStateHistory(); - reportOK(); - break; + return true; + case ('1'): //ATI11 - Return myVc value systemPrintln(); systemPrint("myVc: "); systemPrintln(myVc); - reportOK(); - break; + return true; + case ('2'): //ATI12 - Display the VC details systemPrint("VC "); systemPrint(cmdVc); @@ -538,17 +535,17 @@ bool commandAT(const char * commandString) systemPrintln(); } } - reportOK(); - break; + return true; + case ('3'): //ATI13 - Dump the radioTxBuffer systemPrintln("radioTxBuffer:"); dumpCircularBuffer(radioTxBuffer, radioTxTail, sizeof(radioTxBuffer), availableRadioTXBytes()); - break; + return true; } } - else - return false; - return true; + + //Invalid command + return false; } //Send the AT command over RF link @@ -599,6 +596,7 @@ void checkCommand() bool success; //Verify the command length + success = false; commandString = trimCommand(); //Remove any leading whitespace //Remove trailing CR and LF @@ -611,27 +609,32 @@ void checkCommand() commandBuffer[index] = toupper(commandBuffer[index]); commandString[commandLength] = '\0'; //Terminate buffer - if (commandLength < 2) //Too short - reportERROR(); - //Locate the correct processing routine for the command prefix - success = false; - for (index = 0; index < prefixCount; index++) + //Verify the command length + if (commandLength >= 2) { - //Locate the prefix - prefixLength = strlen(prefixTable[index].prefix); - if (strncmp(commandString, prefixTable[index].prefix, prefixLength) != 0) - continue; - - //Process the command - success = prefixTable[index].processCommand(commandString); - break; + //Locate the correct processing routine for the command prefix + for (index = 0; index < prefixCount; index++) + { + //Locate the prefix + prefixLength = strlen(prefixTable[index].prefix); + if (strncmp(commandString, prefixTable[index].prefix, prefixLength) != 0) + continue; + + //Process the command + success = prefixTable[index].processCommand(commandString); + break; + } } //Print the command failure - if (!success) - reportERROR(); + petWDT(); + if (success) + reportOK(); + else + systemPrintln("ERROR"); outputSerialData(true); + petWDT(); commandLength = 0; //Get ready for next command } @@ -642,12 +645,6 @@ void reportOK() systemPrintln("OK"); } -//Indicate command failure -void reportERROR() -{ - systemPrintln("ERROR"); -} - //Remove any preceeding or following whitespace chars char * trimCommand() { @@ -1089,7 +1086,6 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer break; //The parameter was successfully set - reportOK(); return true; } while (0); diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 35a0a87d..85025e73 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1,6 +1,6 @@ //Apply settings to radio //Called after begin() and once user exits from command interface -void configureRadio() +bool configureRadio() { bool success = true; @@ -90,15 +90,11 @@ void configureRadio() } if (success == false) - { - reportERROR(); systemPrintln("Radio init failed. Check settings."); - } - if ((settings.debug == true) || (settings.debugRadio == true)) - { + else if ((settings.debug == true) || (settings.debugRadio == true)) systemPrintln("Radio configured"); - outputSerialData(true); - } + outputSerialData(true); + return success; } //Update the settings based upon the airSpeed value diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index b5147c5a..5527ac6e 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -340,7 +340,6 @@ void processSerialInput() printerEndpoint = PRINT_TO_SERIAL; systemPrintln(); checkCommand(); //Process command buffer - outputSerialData(true); } else if (incoming == '\n') ; //Do nothing From e6da3ea1f735a4c7797ee73b9b1005d126b7d114 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 9 Dec 2022 16:47:09 -1000 Subject: [PATCH 287/594] Add the ATI13 command to display the SX1276 registers --- Firmware/LoRaSerial_Firmware/Commands.ino | 9 +++- Firmware/LoRaSerial_Firmware/Radio.ino | 64 ++++++++++++++++++----- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 58092d1d..e6747516 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -193,7 +193,8 @@ bool commandAT(const char * commandString) systemPrintln(" ATI10 - Display radio metrics"); systemPrintln(" ATI11 - Return myVc value"); systemPrintln(" ATI12 - Display the VC details"); - systemPrintln(" ATI13 - Dump the radioTxBuffer"); + systemPrintln(" ATI13 - Display the SX1276 registers"); + systemPrintln(" ATI14 - Dump the radioTxBuffer"); return true; case ('0'): //ATI0 - Show user settable parameters @@ -537,7 +538,11 @@ bool commandAT(const char * commandString) } return true; - case ('3'): //ATI13 - Dump the radioTxBuffer + case ('3'): //ATI13 - Display the SX1276 registers + printSX1276Registers(); + return true; + + case ('4'): //ATI14 - Dump the radioTxBuffer systemPrintln("radioTxBuffer:"); dumpCircularBuffer(radioTxBuffer, radioTxTail, sizeof(radioTxBuffer), availableRadioTXBytes()); return true; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 85025e73..bbcb48c6 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -499,28 +499,68 @@ uint8_t readSX1276Register(uint8_t reg) return radio._mod->SPIreadRegister(reg); } +//SX1276 LoRa Register Names +const char * const sx1276RegisterNames[] = +{ + NULL, "RegOpMode", NULL, NULL, // 0 - 3 + NULL, NULL, "RegFrfMsb", "RegFrfMid", // 4 - 7 + "RegFrfLsb", "RegPaConfig", "RegPaRamp", "RegOcp", // 8 - b + "RegLna", "RegFifoAddrPtr", "RegFifoTxBaseAddr", "regFifoRxBaseAddr", // c - f + + "FifoRxCurrentAddr", "RegIrqFlagsMask", "RegIrqFlags", "RegRxNbBytes", //10 - 13 + "RegRxHeaderCntValueMsb", "RegRxHeaderCntValueLsb", "RegRxPacketCntValueMsb", "RegRxPacketCntValueLsb", //14 - 17 + "RegModemStat", "RegPktSnrValue", "RegPktRssiValue", "RegRssiValue", //18 - 1b + "RegHopChannel","RegModemConfig1", "RegModemConfig2", "RegSymbTimeoutLsb", //1c - 1f + + "RegPreambleMsb", "RegPreambleLsb", "RegPayloadLength", "RegMaxPayloadLength",//20 - 23 + "RegHopPeriod", "RegFifoRxByteAddr", "RegModemConfig3", NULL, //24 - 27 + "RegFeiMsb", "RegFeiMid", "RegFeiLsb", NULL, //28 - 2b + "RegRssiWideband", NULL, NULL, "RegIfFreq1", //2c - 2f + + "RegIfFreq2", "ReqDetectOptimize", NULL, "ReqInvertIQ", //30 - 33 + NULL, NULL, "RegHighBwOpimize1", "RegDetectionThreshold", //34 - 37 + NULL, "RegSyncWord", "RegHighBwOptimize2", "RegInvertIQ2", //38 - 3b + NULL, NULL, NULL, NULL, //3c - 3f + + "RegDioMapping1", "RegDioMapping2", "RegVersion", NULL, //40 - 43 + NULL, NULL, NULL, NULL, //44 - 47 + NULL, NULL, NULL, "RegTcxo", //48 - 4b + NULL, "RegPaDac", NULL, NULL, //4C - 4F + + NULL, NULL, NULL, NULL, //50 - 53 + NULL, NULL, NULL, NULL, //54 - 57 + NULL, NULL, NULL, "RegFormerTemp", //58 - 5b + NULL, NULL, NULL, NULL, //5c - 5f + + NULL, "RegAgcRef", "RegAgcThresh1", "RegAgcThresh2", //60 - 63 + "RegAgcThresh3", NULL, NULL, NULL, //64 - 67 + NULL, NULL, NULL, NULL, //68 - 6b + NULL, NULL, NULL, NULL, //6c - 6f + + "RegPII", //70 +}; + //Print the SX1276 LoRa registers -void printSX1276Registers () +void printSX1276Registers() { - //Define the valid LoRa registers - const uint8_t valid_regs [16] = - { - 0xc2, 0xff, 0xff, 0xff, 0x7f, 0x97, 0xcb, 0x0e, - 0x07, 0x28, 0x00, 0x08, 0x1e, 0x00, 0x01, 0x00 - }; - radioCallHistory[RADIO_CALL_printSX1276Registers] = millis(); - systemPrint("Registers:"); - for (uint8_t i = 0; i < (sizeof(valid_regs) * 8); i++) + petWDT(); + systemPrintln("SX1276 Registers:"); + systemPrintln(" Reg Value Name"); + for (uint8_t i = 0; i < (sizeof(sx1276RegisterNames) / sizeof(sx1276RegisterNames[0])); i++) { //Only read and print the valid registers - if (valid_regs[i >> 3] & (1 << (i & 7))) + if (sx1276RegisterNames[i]) { systemPrint(" 0x"); systemPrint(i, HEX); systemPrint(": 0x"); - systemPrintln(readSX1276Register(i), HEX); + systemPrint(readSX1276Register(i), HEX); + systemPrint(", "); + systemPrintln(sx1276RegisterNames[i]); + outputSerialData(true); + petWDT(); } } } From 4aa4a1ec5c58857c2c48eca98cbab5d1faf0e572 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 11 Dec 2022 13:16:34 -1000 Subject: [PATCH 288/594] Display inCommandMode with ATI10 --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index e6747516..cb4928ea 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -365,6 +365,8 @@ bool commandAT(const char * commandString) systemPrint(", "); systemPrint(availableRadioTXBytes()); systemPrintln(" bytes"); + systemPrint(" inCommandMode: "); + systemPrintln(inCommandMode ? "True" : "False"); systemPrint(" commandRXBuffer: commandRXHead: "); systemPrint(commandRXHead); systemPrint(", commandRXTail: "); From b8abd7448306f0a258933aa2727d7a112ee6430f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 10 Dec 2022 13:40:29 -1000 Subject: [PATCH 289/594] Use T for the training netID --- Firmware/LoRaSerial_Firmware/Train.ino | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 7aeb4b6f..ae8a3d94 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -142,13 +142,14 @@ void commonTrainingInitialization() //Use common radio settings between the client and server for training settings = defaultSettings; - settings.operatingMode = MODE_MULTIPOINT; // 3: Use datagrams - settings.encryptData = true; // 4: Enable packet encryption - settings.dataScrambling = true; // 6: Scramble the data - settings.radioBroadcastPower_dbm = 14; // 7: Minimum, assume radios are near each other - settings.frequencyHop = false; //11: Stay on the training frequency - settings.verifyRxNetID = false; //37: Disable netID checking - settings.enableCRC16 = true; //49: Use CRC-16 + settings.dataScrambling = true; //Scramble the data + settings.enableCRC16 = true; //Use CRC-16 + settings.encryptData = true; //Enable packet encryption + settings.frequencyHop = false; //Stay on the training frequency + settings.netID = 'T'; //NetID for training + settings.operatingMode = MODE_MULTIPOINT; //Use datagrams + settings.radioBroadcastPower_dbm = 14; //Minimum, assume radios are near each other + settings.verifyRxNetID = true; //Disable netID checking memcpy(&settings.trainingKey, &originalSettings.trainingKey, AES_KEY_BYTES); //56: Common training key //Determine the components of the frame header and trailer From 0e9c1da6ee0a10bbab7bfd32df4d188d8fa65987 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 13 Dec 2022 07:22:02 -1000 Subject: [PATCH 290/594] Update the LED handling * Add LEDS_ALL_OFF value to turn off the LEDs * Rename ALT_LED_* to RADIO_USE_* to match routine name * Move the cylon routines into System.ino * Add LEDS_CYLON to display the cylon pattern during training * Use the blue and yellow LEDs during training to display radio TX/RX --- .../LoRaSerial_Firmware.ino | 21 +- Firmware/LoRaSerial_Firmware/Radio.ino | 8 +- Firmware/LoRaSerial_Firmware/System.ino | 185 ++++++++++-------- Firmware/LoRaSerial_Firmware/Train.ino | 28 +-- Firmware/LoRaSerial_Firmware/settings.h | 2 + 5 files changed, 122 insertions(+), 122 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 2093b11a..d183d893 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -89,13 +89,6 @@ uint8_t pin_boardID = PIN_UNDEFINED; uint8_t pin_trigger = PIN_UNDEFINED; -#define ALT_LED_RX_DATA pin_rssi1LED //Green -#define ALT_LED_RADIO_LINK pin_rssi2LED //Green -#define ALT_LED_RSSI pin_rssi3LED //Green -#define ALT_LED_TX_DATA pin_rssi4LED //Green -#define ALT_LED_BAD_FRAMES pin_txLED //Blue -#define ALT_LED_BAD_CRC pin_rxLED //Yellow - #define GREEN_LED_1 pin_rssi1LED #define GREEN_LED_2 pin_rssi2LED #define GREEN_LED_3 pin_rssi3LED @@ -103,10 +96,20 @@ uint8_t pin_trigger = PIN_UNDEFINED; #define BLUE_LED pin_txLED #define YELLOW_LED pin_rxLED +#define RADIO_USE_BLINK_MILLIS 15 + +#define RADIO_USE_RX_DATA_LED GREEN_LED_1 //Green +#define RADIO_USE_LINK_LED GREEN_LED_2 //Green +#define RADIO_USE_RSSI_LED GREEN_LED_3 //Green +#define RADIO_USE_TX_DATA_LED GREEN_LED_4 //Green +#define RADIO_USE_BAD_FRAMES_LED BLUE_LED //Blue +#define RADIO_USE_BAD_CRC_LED YELLOW_LED //Yellow + +#define CYLON_TX_DATA_LED BLUE_LED +#define CYLON_RX_DATA_LED YELLOW_LED + #define LED_ON HIGH #define LED_OFF LOW - -#define ALT_LED_BLINK_MILLIS 15 //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Radio Library diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index bbcb48c6..467c02b9 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2104,7 +2104,9 @@ PacketType rcvDatagram() //Blink the RX LED if (settings.selectLedUse == LEDS_RADIO_USE) - digitalWrite(ALT_LED_RX_DATA, LED_ON); + digitalWrite(RADIO_USE_RX_DATA_LED, LED_ON); + else if (settings.selectLedUse == LEDS_CYLON) + digitalWrite(CYLON_RX_DATA_LED, LED_ON); return datagramType; } @@ -2747,7 +2749,9 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) //BLink the RX LED if (settings.selectLedUse == LEDS_RADIO_USE) - digitalWrite(ALT_LED_TX_DATA, LED_ON); + digitalWrite(RADIO_USE_TX_DATA_LED, LED_ON); + else if (settings.selectLedUse == LEDS_CYLON) + digitalWrite(CYLON_TX_DATA_LED, LED_ON); return (true); //Transmission has started } diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index a3c1adb0..7e80161d 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -640,25 +640,25 @@ void radioLeds() //Turn off the RX LED to end the blink currentMillis = millis(); - if ((currentMillis - linkDownTimer) >= ALT_LED_BLINK_MILLIS) - digitalWrite(ALT_LED_RX_DATA, LED_OFF); + if ((currentMillis - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(RADIO_USE_RX_DATA_LED, LED_OFF); //Turn off the TX LED to end the blink currentMillis = millis(); - if ((currentMillis - datagramTimer) >= ALT_LED_BLINK_MILLIS) - digitalWrite(ALT_LED_TX_DATA, LED_OFF); + if ((currentMillis - datagramTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(RADIO_USE_TX_DATA_LED, LED_OFF); //Blink the bad frames LED if (badFrames != previousBadFrames) { previousBadFrames = badFrames; badFramesMillis = currentMillis; - digitalWrite(ALT_LED_BAD_FRAMES, LED_ON); + digitalWrite(RADIO_USE_BAD_FRAMES_LED, LED_ON); } - else if (badFramesMillis && ((currentMillis - badFramesMillis) >= ALT_LED_BLINK_MILLIS)) + else if (badFramesMillis && ((currentMillis - badFramesMillis) >= RADIO_USE_BLINK_MILLIS)) { badFramesMillis = 0; - digitalWrite(ALT_LED_BAD_FRAMES, LED_OFF); + digitalWrite(RADIO_USE_BAD_FRAMES_LED, LED_OFF); } //Blink the bad CRC or duplicate frames LED @@ -666,18 +666,18 @@ void radioLeds() { previousBadCrc = badCrc; badCrcMillis = currentMillis; - digitalWrite(ALT_LED_BAD_CRC, LED_ON); + digitalWrite(RADIO_USE_BAD_CRC_LED, LED_ON); } if ((!settings.enableCRC16) && (duplicateFrames != previousBadCrc)) { previousBadCrc = duplicateFrames; badCrcMillis = currentMillis; - digitalWrite(ALT_LED_BAD_CRC, LED_ON); + digitalWrite(RADIO_USE_BAD_CRC_LED, LED_ON); } - else if (badCrcMillis && ((currentMillis - badCrcMillis) >= ALT_LED_BLINK_MILLIS)) + else if (badCrcMillis && ((currentMillis - badCrcMillis) >= RADIO_USE_BLINK_MILLIS)) { badCrcMillis = 0; - digitalWrite(ALT_LED_BAD_CRC, LED_OFF); + digitalWrite(RADIO_USE_BAD_CRC_LED, LED_OFF); } //Update the link LED @@ -685,7 +685,7 @@ void radioLeds() { //Turn off the LED default: - digitalWrite(ALT_LED_RADIO_LINK, LED_OFF); + digitalWrite(RADIO_USE_LINK_LED, LED_OFF); break; //Turn on the LED @@ -694,7 +694,7 @@ void radioLeds() case RADIO_P2P_LINK_UP_WAIT_TX_DONE: case RADIO_P2P_LINK_UP_WAIT_ACK: case RADIO_P2P_LINK_UP_HB_ACK_REXMT: - digitalWrite(ALT_LED_RADIO_LINK, LED_ON); + digitalWrite(RADIO_USE_LINK_LED, LED_ON); break; } @@ -706,18 +706,57 @@ void radioLeds() rssiValue = (150 + rssi) / 10; rssiCount = 0; if (rssiValue >= rssiCount) - digitalWrite(ALT_LED_RSSI, LED_ON); + digitalWrite(RADIO_USE_RSSI_LED, LED_ON); } //Turn off the RSSI LED else if (rssiValue < rssiCount++) - digitalWrite(ALT_LED_RSSI, LED_OFF); + digitalWrite(RADIO_USE_RSSI_LED, LED_OFF); } //Save the last millis value previousMillis = currentMillis; } +//Start the cylon LEDs +void startCylonLEDs() +{ + trainCylonNumber = 0b0001; + trainCylonDirection = -1; +} + +//Update the cylon LEDs +void updateCylonLEDs() +{ + unsigned long currentMillis; + + currentMillis = millis(); + if ((currentMillis - lastTrainBlink) > 75) //Blink while unit waits in training state + { + lastTrainBlink = millis(); + + //Cylon the RSSI LEDs + setRSSI(trainCylonNumber); + + if (trainCylonNumber == 0b1000 || trainCylonNumber == 0b0001) + trainCylonDirection *= -1; //Change direction + + if (trainCylonDirection > 0) + trainCylonNumber <<= 1; + else + trainCylonNumber >>= 1; + } + + //Use the blue and yellow LEDS to indicate radio activity during training + //Turn off the RX LED to end the blink + if ((currentMillis - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(CYLON_RX_DATA_LED, LED_OFF); + + //Turn off the TX LED to end the blink + if ((currentMillis - datagramTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(CYLON_TX_DATA_LED, LED_OFF); +} + //Update the LED values depending upon the selected display //This is the only function that touches the LEDs void updateLeds() @@ -736,76 +775,54 @@ void updateLeds() digitalWrite(YELLOW_LED, LED_OFF); } - //Update LEDs according to state - //If we are in training, cylon the LEDs - //If we are in data radio mode, control according to selectLedUse setting - - if (radioState == RADIO_TRAIN_WAIT_TX_PING_DONE - || radioState == RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS - || radioState == RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE - || radioState == RADIO_TRAIN_WAIT_FOR_PING - || radioState == RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE) - { - updateCylonLEDs(); - } - else + //Display the LEDs + switch (settings.selectLedUse) { - switch (settings.selectLedUse) - { - //Set LEDs according to RSSI level - default: - case LEDS_RSSI: - //Force RSSI LEDs off when link is down - if (radioState == RADIO_MP_SCANNING - || radioState == RADIO_P2P_LINK_DOWN) - rssi = -200; - - if (rssi > rssiLevelMax) - setRSSI(0b1111); - else if (rssi > rssiLevelHigh) - setRSSI(0b0111); - else if (rssi > rssiLevelMed) - setRSSI(0b0011); - else if (rssi > rssiLevelLow) - setRSSI(0b0001); - else - setRSSI(0b0000); - break; - - case LEDS_RADIO_USE: - radioLeds(); - break; - - //Turn on the blue LED for testing and to identify this radio - case LEDS_BLUE_ON: - digitalWrite(BLUE_LED, LED_ON); - break; - - //Turn on the yellow LED for testing - case LEDS_YELLOW_ON: - digitalWrite(YELLOW_LED, LED_ON); - break; - - //Turn on the green 1 LED for testing - case LEDS_GREEN_1_ON: - digitalWrite(GREEN_LED_1, LED_ON); - break; - - //Turn on the green 2 LED for testing - case LEDS_GREEN_2_ON: - digitalWrite(GREEN_LED_2, LED_ON); - break; - - //Turn on the green 3 LED for testing - case LEDS_GREEN_3_ON: - digitalWrite(GREEN_LED_3, LED_ON); - break; - - //Turn on the green 4 LED for testing - case LEDS_GREEN_4_ON: - digitalWrite(GREEN_LED_4, LED_ON); - break; - } + //Set LEDs according to RSSI level + default: + case LEDS_RSSI: + if (rssi > rssiLevelLow) + setRSSI(0b0001); + if (rssi > rssiLevelMed) + setRSSI(0b0011); + if (rssi > rssiLevelHigh) + setRSSI(0b0111); + if (rssi > rssiLevelMax) + setRSSI(0b1111); + break; + + case LEDS_RADIO_USE: + radioLeds(); + break; + + //Display the cylon pattern during training + case LEDS_CYLON: + updateCylonLEDs(); + break; + + //Turn off all the LEDs + case LEDS_ALL_OFF: + break; + + //Turn on the blue LED for testing and to identify this radio + case LEDS_BLUE_ON: + digitalWrite(BLUE_LED, LED_ON); + break; + + //Turn on the yellow LED for testing + case LEDS_YELLOW_ON: + digitalWrite(YELLOW_LED, LED_ON); + break; + + //Turn on the green 1 LED for testing + case LEDS_GREEN_1_ON: + digitalWrite(GREEN_LED_1, LED_ON); + break; + + //Turn on the green 2 LED for testing + case LEDS_GREEN_2_ON: + digitalWrite(GREEN_LED_2, LED_ON); + break; } } diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index ae8a3d94..0ff20674 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -52,33 +52,6 @@ void generateRandomKeysID() } } -//Start the cylon LEDs -void startCylonLEDs() -{ - trainCylonNumber = 0b0001; - trainCylonDirection = -1; -} - -//Update the cylon LEDs -void updateCylonLEDs() -{ - if ( (millis() - lastTrainBlink) > 75) //Blink while unit waits in training state - { - lastTrainBlink = millis(); - - //Cylon the RSSI LEDs - setRSSI(trainCylonNumber); - - if (trainCylonNumber == 0b1000 || trainCylonNumber == 0b0001) - trainCylonDirection *= -1; //Change direction - - if (trainCylonDirection > 0) - trainCylonNumber <<= 1; - else - trainCylonNumber >>= 1; - } -} - //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Client/Server Training //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- @@ -149,6 +122,7 @@ void commonTrainingInitialization() settings.netID = 'T'; //NetID for training settings.operatingMode = MODE_MULTIPOINT; //Use datagrams settings.radioBroadcastPower_dbm = 14; //Minimum, assume radios are near each other + settings.selectLedUse = LEDS_CYLON; //Display the CYLON pattern on the LEDs settings.verifyRxNetID = true; //Disable netID checking memcpy(&settings.trainingKey, &originalSettings.trainingKey, AES_KEY_BYTES); //56: Common training key diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index aa459c02..8859b455 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -335,6 +335,8 @@ typedef enum LEDS_RSSI = 0, //Green: RSSI, Blue: Serial TX, Yellow: Serial RX LEDS_RADIO_USE, //Green1: RX, Green2: Link, Green3: RSSI, Green4: TX //Blue: Bad frames, Yellow: Bad CRC + LEDS_CYLON, //Display the cylon pattern on the green LEDs, others off + LEDS_ALL_OFF, //All LEDs off LEDS_BLUE_ON, //Blue: ON, other: OFF LEDS_YELLOW_ON, //Yellow: ON, other: OFF LEDS_GREEN_1_ON, //Green 1: ON, other: OFF From 9a1a885d4cc84a85b68544e3f0cf3c8ad8b40164 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 10 Dec 2022 08:52:25 -1000 Subject: [PATCH 291/594] VC: Work on connecting one VC at a time --- Firmware/LoRaSerial_Firmware/States.ino | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 27b8bf24..32c56118 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2014,15 +2014,12 @@ void updateRadioState() vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); virtualCircuitList[index].lastPingMillis = datagramTimer; changeState(RADIO_VC_WAIT_TX_DONE); - - //Only a single transmit is possible at a time - break; } } //The ACK1 is handled with the receive code //Check for a timeout waiting for the ACK1 - if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) + else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) { if ((currentMillis - virtualCircuitList[index].lastPingMillis) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) @@ -2033,16 +2030,13 @@ void updateRadioState() vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); virtualCircuitList[index].lastPingMillis = datagramTimer; changeState(RADIO_VC_WAIT_TX_DONE); - - //Only a single transmit is possible at a time - break; } } } //The ACK2 is handled with the receive code //Check for a timeout waiting for the ACK2 - if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) + else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) { if ((currentMillis - virtualCircuitList[index].lastPingMillis) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) @@ -2053,12 +2047,12 @@ void updateRadioState() vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); virtualCircuitList[index].lastPingMillis = datagramTimer; changeState(RADIO_VC_WAIT_TX_DONE); - - //Only a single transmit is possible at a time - break; } } } + + //Work on only one connection at a time + break; } } } From ffb4b9508b989095d6a26a9d32534f4760cc5529 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 9 Dec 2022 16:47:09 -1000 Subject: [PATCH 292/594] Break up the command output to ensure display --- Firmware/LoRaSerial_Firmware/Commands.ino | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index cb4928ea..3e13544f 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -268,13 +268,21 @@ bool commandAT(const char * commandString) systemPrint(" Up Time: "); systemPrintTimestamp(lastRxDatagram - lastLinkUpTime); systemPrintln(); + outputSerialData(true); + petWDT(); } + + //Transmitter metrics systemPrintln(" Sent"); systemPrint(" Datagrams: "); systemPrintln(datagramsSent); systemPrint(" Frames: "); systemPrintln(framesSent); systemPrint(" Lost Frames: "); + outputSerialData(true); + petWDT(); + + //Receiver metrics systemPrintln(lostFrames); systemPrintln(" Received"); systemPrint(" Frames: "); @@ -291,6 +299,10 @@ bool commandAT(const char * commandString) systemPrintln(insufficientSpace); systemPrint(" Net ID Mismatch: "); systemPrintln(netIdMismatch); + outputSerialData(true); + petWDT(); + + //ACK management metrics if (settings.operatingMode == MODE_POINT_TO_POINT) { vc = &virtualCircuitList[0]; @@ -343,6 +355,10 @@ bool commandAT(const char * commandString) else systemPrintln("None"); } + outputSerialData(true); + petWDT(); + + //Circular buffer metrics systemPrintln(" Circular Buffers"); systemPrint(" serialReceiveBuffer: rxHead: "); systemPrint(rxHead); @@ -381,6 +397,10 @@ bool commandAT(const char * commandString) systemPrint(", "); systemPrint(availableTXCommandBytes()); systemPrintln(" bytes"); + outputSerialData(true); + petWDT(); + + //Radio metrics systemPrintln(" Radio"); systemPrint(" Channel: "); systemPrintln(channelNumber); @@ -460,8 +480,16 @@ bool commandAT(const char * commandString) systemPrintln(transactionComplete ? "True" : "False"); systemPrint(" receiveInProcess: "); systemPrintln(receiveInProcess() ? "True" : "False"); + outputSerialData(true); + petWDT(); + + //Call history systemPrintln(" Call History"); displayRadioCallHistory(); + outputSerialData(true); + petWDT(); + + //State history systemPrintln(" State History"); displayRadioStateHistory(); return true; @@ -491,6 +519,8 @@ bool commandAT(const char * commandString) systemPrint(" ID: "); systemPrintUniqueID(vc->uniqueId); systemPrintln(vc->valid ? " (Valid)" : " (Invalid)"); + + //Heartbeat metrics systemPrintln(" Heartbeats"); if (cmdVc == myVc) { @@ -507,11 +537,19 @@ bool commandAT(const char * commandString) systemPrint(" Up Time: "); systemPrintTimestamp(vc->lastTrafficMillis - vc->firstHeartbeatMillis); systemPrintln(); + outputSerialData(true); + petWDT(); + + //Transmitter metrics systemPrintln(" Sent"); systemPrint(" Frames: "); systemPrintln(vc->framesSent); systemPrint(" Messages: "); systemPrintln(vc->messagesSent); + outputSerialData(true); + petWDT(); + + //Receiver metrics systemPrintln(" Received"); systemPrint(" Frames: "); systemPrintln(vc->framesReceived); @@ -521,6 +559,10 @@ bool commandAT(const char * commandString) systemPrintln(vc->badLength); systemPrint(" Link Failures: "); systemPrintln(linkFailures); + outputSerialData(true); + petWDT(); + + //ACK Management metrics systemPrintln(" ACK Management"); systemPrint(" Last RX ACK number: "); systemPrintln(vc->rxAckNumber); From 3aba48a77c5305ab07abe85eb122138d7e3268bb Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 10 Dec 2022 06:35:46 -1000 Subject: [PATCH 293/594] Display start receive failures in ATI10 --- Firmware/LoRaSerial_Firmware/Commands.ino | 10 ++++++++++ Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 4 ++++ Firmware/LoRaSerial_Firmware/Radio.ino | 2 ++ 3 files changed, 16 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 3e13544f..ba776241 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -474,6 +474,16 @@ bool commandAT(const char * commandString) } systemPrintln(); } + else + systemPrintln("None"); + systemPrint(" radio.startReceive Failure: "); + if (startReceiveFailureMillis) + { + systemPrintTimestamp(startReceiveFailureMillis); + systemPrintln(); + systemPrint(" radio.startReceive Status: "); + systemPrintln(startReceiveFailureState); + } else systemPrintln("None"); systemPrint(" transactionComplete: "); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index d183d893..25e3e720 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -392,6 +392,10 @@ unsigned long rxSuccessMillis; unsigned long rxFailureMillis; int rxFailureState; +//Receive failures +unsigned long startReceiveFailureMillis; +int startReceiveFailureState; + unsigned long txSuccessMillis; unsigned long txFailureMillis; int txFailureState; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 467c02b9..de3bc2d8 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -234,6 +234,8 @@ void returnToReceiving() } if (state != RADIOLIB_ERR_NONE) { + startReceiveFailureMillis = rcvTimeMillis; + startReceiveFailureState = state; if ((settings.debug == true) || (settings.debugRadio == true)) { systemPrint("Receive failed: "); From e54a0bbb6c3c13fb67818bf2648828b69e095c64 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 10 Dec 2022 07:02:02 -1000 Subject: [PATCH 294/594] Display the last time receiveInProcess returned true --- Firmware/LoRaSerial_Firmware/Commands.ino | 6 +++++ .../LoRaSerial_Firmware.ino | 7 ++++++ Firmware/LoRaSerial_Firmware/Radio.ino | 24 +++++-------------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index ba776241..60a77b57 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -488,6 +488,12 @@ bool commandAT(const char * commandString) systemPrintln("None"); systemPrint(" transactionComplete: "); systemPrintln(transactionComplete ? "True" : "False"); + systemPrint(" lastReceiveInProcessTrue @ "); + systemPrintTimestamp(lastReceiveInProcessTrue); + systemPrintln(); + systemPrint(" lastModemStatus: "); + systemPrint(lastModemStatus, HEX); + systemPrintln(); systemPrint(" receiveInProcess: "); systemPrintln(receiveInProcess() ? "True" : "False"); outputSerialData(true); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 25e3e720..a10e64f5 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -63,6 +63,11 @@ const int FIRMWARE_VERSION_MINOR = 0; #define AES_KEY_BYTES 16 //Number of bytes in the encryption key #define UNIQUE_ID_BYTES 16 //Number of bytes in the unique ID +//Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected +//Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock +//Bit 3: Header Info Valid toggles high when a valid Header (with correct CRC) is detected +#define RECEIVE_IN_PROCESS_MASK 0b1011 + #include "settings.h" //Hardware connections @@ -395,6 +400,8 @@ int rxFailureState; //Receive failures unsigned long startReceiveFailureMillis; int startReceiveFailureState; +unsigned long lastReceiveInProcessTrue; +uint8_t lastModemStatus; unsigned long txSuccessMillis; unsigned long txFailureMillis; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index de3bc2d8..dd2da37b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -437,31 +437,19 @@ void hopChannel(bool moveForwardThroughTable) } //Returns true if the radio indicates we have an ongoing reception -//Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected -//Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock -//Bit 3: Header Info Valid toggles high when a valid Header (with correct CRC) is detected bool receiveInProcess() { //triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_START); uint8_t radioStatus = radio.getModemStatus(); - if (radioStatus & 0b1011) return (true); //If any bits are set there is a receive in progress - return false; - - //A remote unit may have started transmitting but this unit has not received enough preamble to detect it. - //Wait X * symbol time for clear air. - //This was found by sending two nearly simultaneous packets and using a logic analyzer to establish the point at which - //the 'Signal Detected' bit goes high. - uint8_t clearAirDelay = calcSymbolTime() * 18; - while (clearAirDelay-- > 0) + if (radioStatus & RECEIVE_IN_PROCESS_MASK) { - radioStatus = radio.getModemStatus(); - if (radioStatus & 0b1011) return (true); //If any bits are set there is a receive in progress - if (timeToHop) hopChannel(); //If the channelTimer has expired, move to next frequency - delay(1); + //If any bits are set there is a receive in progress + if ((lastModemStatus & RECEIVE_IN_PROCESS_MASK) == 0) + lastReceiveInProcessTrue = millis(); + return (true); } - - //triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_END); + lastModemStatus = radioStatus; return (false); //No receive in process } From 026d2490382e0c3a4b786d787f6e48dfac0c6956 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 10 Dec 2022 09:50:34 -1000 Subject: [PATCH 295/594] Fix last up time display --- Firmware/LoRaSerial_Firmware/Commands.ino | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 60a77b57..44379d92 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -34,6 +34,7 @@ typedef struct bool commandAT(const char * commandString) { uint32_t delayMillis; + long deltaMillis; const char * string; unsigned long timer; VIRTUAL_CIRCUIT * vc = &virtualCircuitList[cmdVc]; @@ -551,7 +552,10 @@ bool commandAT(const char * commandString) systemPrintTimestamp(vc->firstHeartbeatMillis); systemPrintln(); systemPrint(" Up Time: "); - systemPrintTimestamp(vc->lastTrafficMillis - vc->firstHeartbeatMillis); + deltaMillis = vc->lastTrafficMillis - vc->firstHeartbeatMillis; + if (deltaMillis < 0) + deltaMillis = -deltaMillis; + systemPrintTimestamp(deltaMillis); systemPrintln(); outputSerialData(true); petWDT(); From 840c80abb08ed1a0e5077c7ce8269ae6acaffc25 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 10 Dec 2022 16:10:21 -1000 Subject: [PATCH 296/594] VcServerTest: Disable state transition display --- Firmware/Tools/VcServerTest.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 6fb6c38f..426cc5fc 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -19,7 +19,7 @@ #define DISPLAY_DATA_NACK 1 #define DISPLAY_VC_STATE 0 #define SEND_ATC_COMMAND 1 -#define DISPLAY_STATE_TRANSITION 1 +#define DISPLAY_STATE_TRANSITION 0 typedef struct _VIRTUAL_CIRCUIT { From 34afa10eef8d18a0674db17f680a54493393ac32 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Tue, 13 Dec 2022 13:48:54 -0700 Subject: [PATCH 297/594] Typo fixes --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 44379d92..8f7ad0d4 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -150,7 +150,7 @@ bool commandAT(const char * commandString) //If we are pointed at the RF link, send ok and wait for response ACK before applying settings return true; - //Apply raio settings by entering the reset state + //Apply radio settings by entering the reset state inCommandMode = false; //Return to printing normal RF serial data changeState(RADIO_RESET); return true; @@ -163,7 +163,7 @@ bool commandAT(const char * commandString) recordSystemSettings(); return true; - case ('Z'): //ATZ - Reboots the radio + case ('Z'): //ATZ - Reboots the system reportOK(); outputSerialData(true); systemFlush(); From 156806443c863027f44c1669bfeeb492b990f928 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 09:09:02 -0700 Subject: [PATCH 298/594] Add reset column to command table. Rename originalSettings to tempSettings. --- Firmware/LoRaSerial_Firmware/Commands.ino | 142 +++++++++--------- .../LoRaSerial_Firmware.ino | 3 +- Firmware/LoRaSerial_Firmware/Radio.ino | 130 ++++++++-------- Firmware/LoRaSerial_Firmware/States.ino | 6 +- Firmware/LoRaSerial_Firmware/System.ino | 2 +- Firmware/LoRaSerial_Firmware/Train.ino | 58 +++---- Firmware/LoRaSerial_Firmware/settings.h | 1 + 7 files changed, 171 insertions(+), 171 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 8f7ad0d4..7eccaa01 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -923,89 +923,89 @@ bool valSpeedSerial (void * value, uint32_t valMin, uint32_t valMax) const COMMAND_ENTRY commands[] = { /*Debug parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &settings.copyDebug}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &settings.debug}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &settings.debugDatagrams}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugHeartbeat", &settings.debugHeartbeat}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugNvm", &settings.debugNvm}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &settings.debugRadio}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &settings.debugReceive}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &settings.debugStates}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugSync", &settings.debugSync}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &settings.debugTraining}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &settings.debugTransmit}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &settings.debugSerial}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &settings.displayRealMillis}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintAckNumbers", &settings.printAckNumbers}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &settings.printFrequency}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &settings.printLinkUpDown}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintPacketQuality", &settings.printPacketQuality}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintPktData", &settings.printPktData}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &settings.printRfData}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &settings.printTimestamp}, - {'D', 1, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &settings.printTxErrors}, - {'D', 1, 0, 255, 0, TYPE_U8, valInt, "SelectLedUse", &settings.selectLedUse}, + Ltr, All, reset, min, max, digits, type, validation, name, setting addr, */ + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "CopyDebug", &tempSettings.copyDebug}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "Debug", &tempSettings.debug}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugDatagrams", &tempSettings.debugDatagrams}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugHeartbeat", &tempSettings.debugHeartbeat}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugNvm", &tempSettings.debugNvm}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &tempSettings.debugRadio}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &tempSettings.debugReceive}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &tempSettings.debugStates}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSync", &tempSettings.debugSync}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &tempSettings.debugTraining}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &tempSettings.debugTransmit}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &tempSettings.debugSerial}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &tempSettings.displayRealMillis}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintAckNumbers", &tempSettings.printAckNumbers}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &tempSettings.printFrequency}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &tempSettings.printLinkUpDown}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintPacketQuality", &tempSettings.printPacketQuality}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintPktData", &tempSettings.printPktData}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &tempSettings.printRfData}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &tempSettings.printTimestamp}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &tempSettings.printTxErrors}, + {'D', 1, 0, 0, 255, 0, TYPE_U8, valInt, "SelectLedUse", &tempSettings.selectLedUse}, /*Radio parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'R', 0, 0, 0, 0, TYPE_SPEED_AIR, valSpeedAir, "AirSpeed", &settings.airSpeed}, - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "AutoTune", &settings.autoTuneFrequency}, - {'R', 0, 0, 0, 2, TYPE_FLOAT, valBandwidth, "Bandwidth", &settings.radioBandwidth}, - {'R', 0, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &settings.radioCodingRate}, - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "FrequencyHop", &settings.frequencyHop}, - {'R', 0, 0, 931, 3, TYPE_FLOAT, valFreqMax, "FrequencyMax", &settings.frequencyMax}, - {'R', 0, 900, 0, 3, TYPE_FLOAT, valFreqMin, "FrequencyMin", &settings.frequencyMin}, - {'R', 0, 10, 65535, 0, TYPE_U16, valInt, "MaxDwellTime", &settings.maxDwellTime}, - {'R', 0, 1, 255, 0, TYPE_U8, valInt, "NumberOfChannels", &settings.numberOfChannels}, - {'R', 0, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &settings.radioPreambleLength}, - {'R', 0, 6, 12, 0, TYPE_U8, valOverride, "SpreadFactor", &settings.radioSpreadFactor}, - {'R', 0, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &settings.radioSyncWord}, - {'R', 0, 14, 30, 0, TYPE_U8, valInt, "TxPower", &settings.radioBroadcastPower_dbm}, + Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ + {'R', 0, 1, 0, 0, 0, TYPE_SPEED_AIR, valSpeedAir, "AirSpeed", &tempSettings.airSpeed}, + {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "AutoTune", &tempSettings.autoTuneFrequency}, + {'R', 0, 1, 0, 0, 2, TYPE_FLOAT, valBandwidth, "Bandwidth", &tempSettings.radioBandwidth}, + {'R', 0, 1, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &tempSettings.radioCodingRate}, + {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "FrequencyHop", &tempSettings.frequencyHop}, + {'R', 0, 1, 0, 931, 3, TYPE_FLOAT, valFreqMax, "FrequencyMax", &tempSettings.frequencyMax}, + {'R', 0, 1, 900, 0, 3, TYPE_FLOAT, valFreqMin, "FrequencyMin", &tempSettings.frequencyMin}, + {'R', 0, 1, 10, 65535, 0, TYPE_U16, valInt, "MaxDwellTime", &tempSettings.maxDwellTime}, + {'R', 0, 1, 1, 255, 0, TYPE_U8, valInt, "NumberOfChannels", &tempSettings.numberOfChannels}, + {'R', 0, 1, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &tempSettings.radioPreambleLength}, + {'R', 0, 1, 6, 12, 0, TYPE_U8, valOverride, "SpreadFactor", &tempSettings.radioSpreadFactor}, + {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &tempSettings.radioSyncWord}, + {'R', 0, 1, 14, 30, 0, TYPE_U8, valInt, "TxPower", &tempSettings.radioBroadcastPower_dbm}, /*Radio protocol parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "DataScrambling", &settings.dataScrambling}, - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &settings.enableCRC16}, - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "EncryptData", &settings.encryptData}, - {'R', 0, 0, 0, 0, TYPE_KEY, valKey, "EncryptionKey", &settings.encryptionKey}, - {'R', 0, 0, 255, 0, TYPE_U8, valInt, "FramesToYield", &settings.framesToYield}, - {'R', 0, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &settings.serialTimeoutBeforeSendingFrame_ms}, - {'R', 0, 250, 65535, 0, TYPE_U16, valInt, "HeartBeatTimeout", &settings.heartbeatTimeout}, - {'R', 0, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &settings.maxResends}, - {'R', 0, 0, 255, 0, TYPE_U8, valInt, "NetID", &settings.netID}, - {'R', 0, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &settings.operatingMode}, - {'R', 0, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &settings.overheadTime}, - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "Server", &settings.server}, - {'R', 0, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &settings.verifyRxNetID}, + Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ + {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "DataScrambling", &tempSettings.dataScrambling}, + {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "EnableCRC16", &tempSettings.enableCRC16}, + {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "EncryptData", &tempSettings.encryptData}, + {'R', 0, 1, 0, 0, 0, TYPE_KEY, valKey, "EncryptionKey", &tempSettings.encryptionKey}, + {'R', 0, 0, 0, 255, 0, TYPE_U8, valInt, "FramesToYield", &tempSettings.framesToYield}, + {'R', 0, 0, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &tempSettings.serialTimeoutBeforeSendingFrame_ms}, + {'R', 0, 0, 250, 65535, 0, TYPE_U16, valInt, "HeartBeatTimeout", &tempSettings.heartbeatTimeout}, + {'R', 0, 0, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &tempSettings.maxResends}, + {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "NetID", &tempSettings.netID}, + {'R', 0, 1, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &tempSettings.operatingMode}, + {'R', 0, 1, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &tempSettings.overheadTime}, + {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "Server", &tempSettings.server}, + {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &tempSettings.verifyRxNetID}, /*Serial parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "CopySerial", &settings.copySerial}, - {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "Echo", &settings.echo}, - {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "FlowControl", &settings.flowControl}, - {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "InvertCts", &settings.invertCts}, - {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "InvertRts", &settings.invertRts}, - {'S', 0, 0, 0, 0, TYPE_SPEED_SERIAL, valSpeedSerial, "SerialSpeed", &settings.serialSpeed}, - {'S', 0, 0, 1, 0, TYPE_BOOL, valInt, "UsbSerialWait", &settings.usbSerialWait}, + Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ + {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "CopySerial", &tempSettings.copySerial}, + {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "Echo", &tempSettings.echo}, + {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "FlowControl", &tempSettings.flowControl}, + {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "InvertCts", &tempSettings.invertCts}, + {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "InvertRts", &tempSettings.invertRts}, + {'S', 0, 0, 0, 0, 0, TYPE_SPEED_SERIAL, valSpeedSerial, "SerialSpeed", &tempSettings.serialSpeed}, + {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "UsbSerialWait", &tempSettings.usbSerialWait}, /*Training parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'R', 0, 1, 255, 0, TYPE_U8, valInt, "ClientPingRetryInterval", &settings.clientPingRetryInterval}, - {'R', 0, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &settings.trainingKey}, - {'R', 0, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &settings.trainingTimeout}, + Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ + {'R', 0, 1, 1, 255, 0, TYPE_U8, valInt, "ClientPingRetryInterval", &tempSettings.clientPingRetryInterval}, + {'R', 0, 0, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &tempSettings.trainingKey}, + {'R', 0, 0, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &tempSettings.trainingTimeout}, /*Trigger parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'P', 1, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &settings.copyTriggers}, - {'P', 1, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_31-0", &settings.triggerEnable}, - {'P', 1, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_63-32", &settings.triggerEnable2}, - {'P', 1, 1, 255, 0, TYPE_U8, valInt, "TriggerWidth", &settings.triggerWidth}, - {'P', 1, 0, 1, 0, TYPE_BOOL, valInt, "TriggerWidthIsMultiplier", &settings.triggerWidthIsMultiplier}, + Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ + {'P', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "CopyTriggers", &tempSettings.copyTriggers}, + {'P', 1, 0, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_31-0", &tempSettings.triggerEnable}, + {'P', 1, 0, 0, 0xffffffff, 0, TYPE_U32, valInt, "TriggerEnable_63-32", &tempSettings.triggerEnable2}, + {'P', 1, 0, 1, 255, 0, TYPE_U8, valInt, "TriggerWidth", &tempSettings.triggerWidth}, + {'P', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "TriggerWidthIsMultiplier", &tempSettings.triggerWidthIsMultiplier}, /*Virtual circuit parameters - Ltr, All, min, max, digits, type, validation, name, setting addr */ - {'V', 0, 0, MAX_VC - 1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, + Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ + {'V', 0, 0, 0, MAX_VC - 1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, }; const int commandCount = sizeof(commands) / sizeof(commands[0]); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index a10e64f5..4b8514ff 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -492,7 +492,7 @@ unsigned long retransmitTimeout = 0; //Throttle back re-transmits //Global variables //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- const Settings defaultSettings; -Settings originalSettings; //Create a duplicate of settings during training so that we can resort as needed +Settings tempSettings; //Create a duplicate of settings during training so that we can resort as needed uint8_t originalEncryptionKey[AES_KEY_BYTES] = {0}; //Temp store key if we need to exit button training uint8_t originalNetID = 0; //Temp store ID if we need to exit button training bool originalServer = false; //Temp store server setting if we need to exit button training @@ -534,7 +534,6 @@ void setup() arch.uniqueID(myUniqueId); //Get the unique ID - beginLoRa(); //Start radio beginButton(); //Start watching the train button diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index dd2da37b..a7b8a9e5 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -706,7 +706,7 @@ bool xmitDatagramP2pTrainingParams() radioCallHistory[RADIO_CALL_xmitDatagramP2pTrainingParams] = millis(); //Initialize the radio parameters - memcpy(¶ms, &originalSettings, sizeof(settings)); + memcpy(¶ms, &tempSettings, sizeof(settings)); params.operatingMode = MODE_POINT_TO_POINT; //Add the radio parameters @@ -1128,88 +1128,88 @@ void updateRadioParameters(uint8_t * rxData) memcpy(¶ms, rxData, sizeof(params)); //Update the radio parameters - originalSettings.airSpeed = params.airSpeed; - originalSettings.autoTuneFrequency = params.autoTuneFrequency; - originalSettings.radioBandwidth = params.radioBandwidth; - originalSettings.radioCodingRate = params.radioCodingRate; - originalSettings.frequencyHop = params.frequencyHop; - originalSettings.frequencyMax = params.frequencyMax; - originalSettings.frequencyMin = params.frequencyMin; - originalSettings.maxDwellTime = params.maxDwellTime; - originalSettings.numberOfChannels = params.numberOfChannels; - originalSettings.radioPreambleLength = params.radioPreambleLength; - originalSettings.radioSpreadFactor = params.radioSpreadFactor; - originalSettings.radioSyncWord = params.radioSyncWord; - originalSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; + tempSettings.airSpeed = params.airSpeed; + tempSettings.autoTuneFrequency = params.autoTuneFrequency; + tempSettings.radioBandwidth = params.radioBandwidth; + tempSettings.radioCodingRate = params.radioCodingRate; + tempSettings.frequencyHop = params.frequencyHop; + tempSettings.frequencyMax = params.frequencyMax; + tempSettings.frequencyMin = params.frequencyMin; + tempSettings.maxDwellTime = params.maxDwellTime; + tempSettings.numberOfChannels = params.numberOfChannels; + tempSettings.radioPreambleLength = params.radioPreambleLength; + tempSettings.radioSpreadFactor = params.radioSpreadFactor; + tempSettings.radioSyncWord = params.radioSyncWord; + tempSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; //Update the radio protocol parameters - originalSettings.dataScrambling = params.dataScrambling; - originalSettings.enableCRC16 = params.enableCRC16; - originalSettings.encryptData = params.encryptData; - memcpy(originalSettings.encryptionKey, params.encryptionKey, sizeof(originalSettings.encryptionKey)); - originalSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; - originalSettings.heartbeatTimeout = params.heartbeatTimeout; - originalSettings.maxResends = params.maxResends; - originalSettings.netID = params.netID; - originalSettings.operatingMode = params.operatingMode; - originalSettings.overheadTime = params.overheadTime; - originalSettings.server = params.server; - originalSettings.verifyRxNetID = params.verifyRxNetID; - originalSettings.framesToYield = params.framesToYield; + tempSettings.dataScrambling = params.dataScrambling; + tempSettings.enableCRC16 = params.enableCRC16; + tempSettings.encryptData = params.encryptData; + memcpy(tempSettings.encryptionKey, params.encryptionKey, sizeof(tempSettings.encryptionKey)); + tempSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; + tempSettings.heartbeatTimeout = params.heartbeatTimeout; + tempSettings.maxResends = params.maxResends; + tempSettings.netID = params.netID; + tempSettings.operatingMode = params.operatingMode; + tempSettings.overheadTime = params.overheadTime; + tempSettings.server = params.server; + tempSettings.verifyRxNetID = params.verifyRxNetID; + tempSettings.framesToYield = params.framesToYield; //Update the debug parameters if (params.copyDebug) { - originalSettings.debug = params.debug; - originalSettings.copyDebug = params.copyDebug; - originalSettings.debug = params.debug; - originalSettings.debugDatagrams = params.debugDatagrams; - originalSettings.debugHeartbeat = params.debugHeartbeat; - originalSettings.debugNvm = params.debugNvm; - originalSettings.debugRadio = params.debugRadio; - originalSettings.debugReceive = params.debugReceive; - originalSettings.debugStates = params.debugStates; - originalSettings.debugSync = params.debugSync; - originalSettings.debugTraining = params.debugTraining; - originalSettings.debugTransmit = params.debugTransmit; - originalSettings.debugSerial = params.debugSerial; - originalSettings.printPacketQuality = params.printPacketQuality; - originalSettings.displayRealMillis = params.displayRealMillis; - originalSettings.printAckNumbers = params.printAckNumbers; - originalSettings.printFrequency = params.printFrequency; - originalSettings.printLinkUpDown = params.printLinkUpDown; - originalSettings.printPktData = params.printPktData; - originalSettings.printRfData = params.printRfData; - originalSettings.printTimestamp = params.printTimestamp; - originalSettings.printTxErrors = params.printTxErrors; - originalSettings.selectLedUse = params.selectLedUse; + tempSettings.debug = params.debug; + tempSettings.copyDebug = params.copyDebug; + tempSettings.debug = params.debug; + tempSettings.debugDatagrams = params.debugDatagrams; + tempSettings.debugHeartbeat = params.debugHeartbeat; + tempSettings.debugNvm = params.debugNvm; + tempSettings.debugRadio = params.debugRadio; + tempSettings.debugReceive = params.debugReceive; + tempSettings.debugStates = params.debugStates; + tempSettings.debugSync = params.debugSync; + tempSettings.debugTraining = params.debugTraining; + tempSettings.debugTransmit = params.debugTransmit; + tempSettings.debugSerial = params.debugSerial; + tempSettings.printPacketQuality = params.printPacketQuality; + tempSettings.displayRealMillis = params.displayRealMillis; + tempSettings.printAckNumbers = params.printAckNumbers; + tempSettings.printFrequency = params.printFrequency; + tempSettings.printLinkUpDown = params.printLinkUpDown; + tempSettings.printPktData = params.printPktData; + tempSettings.printRfData = params.printRfData; + tempSettings.printTimestamp = params.printTimestamp; + tempSettings.printTxErrors = params.printTxErrors; + tempSettings.selectLedUse = params.selectLedUse; } //Update the serial parameters if (params.copySerial) { - originalSettings.copySerial = params.copySerial; - originalSettings.echo = params.echo; - originalSettings.flowControl = params.flowControl; - originalSettings.invertCts = params.invertCts; - originalSettings.invertRts = params.invertRts; - originalSettings.serialSpeed = params.serialSpeed; - originalSettings.usbSerialWait = params.usbSerialWait; + tempSettings.copySerial = params.copySerial; + tempSettings.echo = params.echo; + tempSettings.flowControl = params.flowControl; + tempSettings.invertCts = params.invertCts; + tempSettings.invertRts = params.invertRts; + tempSettings.serialSpeed = params.serialSpeed; + tempSettings.usbSerialWait = params.usbSerialWait; } //Update the training values - originalSettings.clientPingRetryInterval = params.clientPingRetryInterval; + tempSettings.clientPingRetryInterval = params.clientPingRetryInterval; //The trainingKey is already the same - originalSettings.trainingTimeout = params.trainingTimeout; + tempSettings.trainingTimeout = params.trainingTimeout; //Update the trigger parameters if (params.copyTriggers) { - originalSettings.copyTriggers = params.copyTriggers; - originalSettings.triggerEnable = params.triggerEnable; - originalSettings.triggerEnable2 = params.triggerEnable2; - originalSettings.triggerWidth = params.triggerWidth; - originalSettings.triggerWidthIsMultiplier = params.triggerWidthIsMultiplier; + tempSettings.copyTriggers = params.copyTriggers; + tempSettings.triggerEnable = params.triggerEnable; + tempSettings.triggerEnable2 = params.triggerEnable2; + tempSettings.triggerWidth = params.triggerWidth; + tempSettings.triggerWidthIsMultiplier = params.triggerWidthIsMultiplier; } } @@ -1225,7 +1225,7 @@ bool xmitDatagramTrainRadioParameters(const uint8_t * clientID) radioCallHistory[RADIO_CALL_xmitDatagramTrainRadioParameters] = millis(); //Initialize the radio parameters - memcpy(¶ms, &originalSettings, sizeof(settings)); + memcpy(¶ms, &tempSettings, sizeof(settings)); params.server = false; //Add the destination (client) ID diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 32c56118..3ceac1d1 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1439,12 +1439,12 @@ void updateRadioState() //If we are training with button, in P2P mode, and user has not set server mode //Automatically switch to server if (trainViaButton - && originalSettings.operatingMode == MODE_POINT_TO_POINT + && tempSettings.operatingMode == MODE_POINT_TO_POINT && originalServer == false) { //Give up and change to Server automatically - settings = originalSettings; //Return to original radio settings + settings = tempSettings; //Return to original radio settings generateRandomKeysID(); //Generate random netID and AES key @@ -1546,7 +1546,7 @@ void updateRadioState() //If we are training via button, and in point to point mode, and the user has not manually set the server //then reboot with current settings after a single client acks if (trainViaButton - && originalSettings.operatingMode == MODE_POINT_TO_POINT + && tempSettings.operatingMode == MODE_POINT_TO_POINT && originalServer == false) { //Reboot the radio with the newly generated random netID/Key parameters diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 7e80161d..64ecad84 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -268,7 +268,7 @@ void updateButton() } else if (trainState == TRAIN_IN_PROCESS && trainBtn->wasReleased()) { - settings = originalSettings; //Return to original radio settings + settings = tempSettings; //Return to original radio settings //Return to original keys, ID, and server state memcpy(&settings.encryptionKey, &originalEncryptionKey, AES_KEY_BYTES); diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 0ff20674..634c0ebb 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -94,9 +94,9 @@ void beginTrainingServer() systemPrintln("Server radio protocol parameters"); systemPrint(" netID: "); - systemPrintln(originalSettings.netID); + systemPrintln(tempSettings.netID); systemPrint(" Encryption key: "); - displayEncryptionKey(originalSettings.encryptionKey); + displayEncryptionKey(tempSettings.encryptionKey); systemPrintln(); outputSerialData(true); @@ -111,7 +111,7 @@ void beginTrainingServer() void commonTrainingInitialization() { //Save the current settings - originalSettings = settings; + tempSettings = settings; //Use common radio settings between the client and server for training settings = defaultSettings; @@ -124,40 +124,40 @@ void commonTrainingInitialization() settings.radioBroadcastPower_dbm = 14; //Minimum, assume radios are near each other settings.selectLedUse = LEDS_CYLON; //Display the CYLON pattern on the LEDs settings.verifyRxNetID = true; //Disable netID checking - memcpy(&settings.trainingKey, &originalSettings.trainingKey, AES_KEY_BYTES); //56: Common training key + memcpy(&settings.trainingKey, &tempSettings.trainingKey, AES_KEY_BYTES); //56: Common training key //Determine the components of the frame header and trailer selectHeaderAndTrailerBytes(); //Debug training if requested - if (originalSettings.debugTraining) + if (tempSettings.debugTraining) { - settings.selectLedUse = originalSettings.selectLedUse; + settings.selectLedUse = tempSettings.selectLedUse; //Ignore copyDebug - settings.debug = originalSettings.debug; - settings.debugDatagrams = originalSettings.debugDatagrams; - settings.debugNvm = originalSettings.debugNvm; - settings.debugRadio = originalSettings.debugRadio; - settings.debugReceive = originalSettings.debugReceive; - settings.debugSerial = originalSettings.debugSerial; - settings.debugStates = originalSettings.debugStates; - settings.debugTraining = originalSettings.debugTraining; - settings.debugTransmit = originalSettings.debugTransmit; - settings.printPacketQuality = originalSettings.printPacketQuality; - settings.displayRealMillis = originalSettings.displayRealMillis; - settings.printAckNumbers = originalSettings.printAckNumbers; - settings.printFrequency = originalSettings.printFrequency; - settings.printLinkUpDown = originalSettings.printLinkUpDown; - settings.printPktData = originalSettings.printPktData; - settings.printRfData = originalSettings.printRfData; - settings.printTimestamp = originalSettings.printTimestamp; - settings.printTxErrors = originalSettings.printTxErrors; + settings.debug = tempSettings.debug; + settings.debugDatagrams = tempSettings.debugDatagrams; + settings.debugNvm = tempSettings.debugNvm; + settings.debugRadio = tempSettings.debugRadio; + settings.debugReceive = tempSettings.debugReceive; + settings.debugSerial = tempSettings.debugSerial; + settings.debugStates = tempSettings.debugStates; + settings.debugTraining = tempSettings.debugTraining; + settings.debugTransmit = tempSettings.debugTransmit; + settings.printPacketQuality = tempSettings.printPacketQuality; + settings.displayRealMillis = tempSettings.displayRealMillis; + settings.printAckNumbers = tempSettings.printAckNumbers; + settings.printFrequency = tempSettings.printFrequency; + settings.printLinkUpDown = tempSettings.printLinkUpDown; + settings.printPktData = tempSettings.printPktData; + settings.printRfData = tempSettings.printRfData; + settings.printTimestamp = tempSettings.printTimestamp; + settings.printTxErrors = tempSettings.printTxErrors; //Ignore copyTriggers - settings.triggerEnable = originalSettings.triggerEnable; - settings.triggerEnable2 = originalSettings.triggerEnable2; - settings.triggerWidth = originalSettings.triggerWidth; - settings.triggerWidthIsMultiplier = originalSettings.triggerWidthIsMultiplier; + settings.triggerEnable = tempSettings.triggerEnable; + settings.triggerEnable2 = tempSettings.triggerEnable2; + settings.triggerWidth = tempSettings.triggerWidth; + settings.triggerWidthIsMultiplier = tempSettings.triggerWidthIsMultiplier; } //Reset cylon variables @@ -184,7 +184,7 @@ void commonTrainingInitialization() void endClientServerTraining(uint8_t event) { triggerEvent(event); - settings = originalSettings; //Return to original radio settings + settings = tempSettings; //Return to original radio settings if (settings.debugTraining) { diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 8859b455..8d8ba847 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -321,6 +321,7 @@ typedef struct _COMMAND_ENTRY { char letter; char requireAll; + bool forceRadioReset; uint32_t minValue; uint32_t maxValue; uint8_t digits; From 59ac2685d365b028bc4d75aba5f900da8947931b Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 09:09:51 -0700 Subject: [PATCH 299/594] Force radio link reset if certain settings are modified. --- Firmware/LoRaSerial_Firmware/Commands.ino | 13 +++++++++++-- .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Serial.ino | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 7eccaa01..c874a184 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -150,9 +150,14 @@ bool commandAT(const char * commandString) //If we are pointed at the RF link, send ok and wait for response ACK before applying settings return true; - //Apply radio settings by entering the reset state inCommandMode = false; //Return to printing normal RF serial data - changeState(RADIO_RESET); + + settings = tempSettings; //Apply user's modifications + + //If a setting was applied that requires radio reset, do so + if(forceRadioReset) + changeState(RADIO_RESET); + return true; case ('T'): //ATT - Enter training mode @@ -1153,6 +1158,10 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer } if (valid == false) break; + + //See if this command requires a radio reset to be apply + if(valid && command->forceRadioReset) + forceRadioReset = true; //The parameter was successfully set return true; diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 4b8514ff..3f831ec3 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -199,6 +199,7 @@ uint8_t commandLength = 0; bool remoteCommandResponse; bool rtsAsserted; //When RTS is asserted, host says it's ok to send data +bool forceRadioReset = false; //Goes true when a setting requires a link/radio reset to work /* Data Flow - Point-to-Point and Multi-Point diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 5527ac6e..ba9231f7 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -385,6 +385,9 @@ void processSerialInput() outputSerialData(true); inCommandMode = true; //Allow AT parsing. Prevent received RF data from being printed. + forceRadioReset = false; //Don't reset the radio link unless a setting requires it + + tempSettings = settings; escapeCharsReceived = 0; lastByteReceived_ms = millis(); From e07437ae34dcdf238bbba47f9a38b0c7c339424a Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 13:53:57 -0700 Subject: [PATCH 300/594] Print output during hard error --- Firmware/LoRaSerial_Firmware/Begin.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 1e6d030a..22ec84f5 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -34,6 +34,7 @@ void beginLoRa() { systemPrint("Radio init failed with code: "); systemPrintln(state); + outputSerialData(true); while (1) { petWDT(); From e041fc970b888dc6bda8a406f2743a4e98453af2 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 13:55:45 -0700 Subject: [PATCH 301/594] Mark server setting as requiring radio reset --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index c874a184..bd199cb1 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -981,7 +981,7 @@ const COMMAND_ENTRY commands[] = {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "NetID", &tempSettings.netID}, {'R', 0, 1, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &tempSettings.operatingMode}, {'R', 0, 1, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &tempSettings.overheadTime}, - {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "Server", &tempSettings.server}, + {'R', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "Server", &tempSettings.server}, {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &tempSettings.verifyRxNetID}, /*Serial parameters From d143e74d07d1b532a10cc8499ae5426b3facfac3 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 13:59:52 -0700 Subject: [PATCH 302/594] Gracefully handle ATW without causing link break. --- Firmware/LoRaSerial_Firmware/Commands.ino | 27 ++++++++++++++----- .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/NVM.ino | 4 +-- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/Serial.ino | 1 + Firmware/LoRaSerial_Firmware/settings.h | 2 +- 6 files changed, 26 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index bd199cb1..1290f598 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -126,8 +126,13 @@ bool commandAT(const char * commandString) return true; case ('F'): //ATF - Restore default parameters - settings = defaultSettings; //Overwrite all current settings with defaults + settings = defaultSettings; //Overwrite all system settings with defaults + validateSettings(); //Modify defaults for each radio type (915, 868, 433, etc) + + tempSettings = settings; //Overwrite all temp settings with defaults + forceRadioReset = true; + recordSystemSettings(); return true; @@ -153,11 +158,13 @@ bool commandAT(const char * commandString) inCommandMode = false; //Return to printing normal RF serial data settings = tempSettings; //Apply user's modifications - + + if (writeOnCommandExit) recordSystemSettings(); + //If a setting was applied that requires radio reset, do so - if(forceRadioReset) + if (forceRadioReset) changeState(RADIO_RESET); - + return true; case ('T'): //ATT - Enter training mode @@ -165,10 +172,16 @@ bool commandAT(const char * commandString) return true; case ('W'): //ATW - Write parameters to the flash memory - recordSystemSettings(); + writeOnCommandExit = true; return true; case ('Z'): //ATZ - Reboots the system + if (writeOnCommandExit) + { + settings = tempSettings; //Apply user's modifications + recordSystemSettings(); + } + reportOK(); outputSerialData(true); systemFlush(); @@ -1158,9 +1171,9 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer } if (valid == false) break; - + //See if this command requires a radio reset to be apply - if(valid && command->forceRadioReset) + if (valid && command->forceRadioReset) forceRadioReset = true; //The parameter was successfully set diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 3f831ec3..66a00e34 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -200,6 +200,7 @@ bool remoteCommandResponse; bool rtsAsserted; //When RTS is asserted, host says it's ok to send data bool forceRadioReset = false; //Goes true when a setting requires a link/radio reset to work +bool writeOnCommandExit = false; //Goes true if user specifies ATW command /* Data Flow - Point-to-Point and Multi-Point diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index af045f81..188076c8 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -19,7 +19,7 @@ void loadSettings() //Misalignment happens when we add a new feature or setting uint16_t tempSize = 0; EEPROM.get(0, tempSize); //Load the sizeOfSettings - if (tempSize != sizeof(settings)) + if (tempSize != sizeof(Settings)) { if (settings.debugNvm) { @@ -94,7 +94,7 @@ void recordSystemSettings() systemPrintln("Writing settings to EEPROM"); outputSerialData(true); } - settings.sizeOfSettings = sizeof(settings); + settings.sizeOfSettings = sizeof(Settings); EEPROM.put(0, settings); arch.eepromCommit(); diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a7b8a9e5..f5063c85 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -500,7 +500,7 @@ const char * const sx1276RegisterNames[] = "FifoRxCurrentAddr", "RegIrqFlagsMask", "RegIrqFlags", "RegRxNbBytes", //10 - 13 "RegRxHeaderCntValueMsb", "RegRxHeaderCntValueLsb", "RegRxPacketCntValueMsb", "RegRxPacketCntValueLsb", //14 - 17 "RegModemStat", "RegPktSnrValue", "RegPktRssiValue", "RegRssiValue", //18 - 1b - "RegHopChannel","RegModemConfig1", "RegModemConfig2", "RegSymbTimeoutLsb", //1c - 1f + "RegHopChannel", "RegModemConfig1", "RegModemConfig2", "RegSymbTimeoutLsb", //1c - 1f "RegPreambleMsb", "RegPreambleLsb", "RegPayloadLength", "RegMaxPayloadLength",//20 - 23 "RegHopPeriod", "RegFifoRxByteAddr", "RegModemConfig3", NULL, //24 - 27 diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index ba9231f7..198e158b 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -386,6 +386,7 @@ void processSerialInput() inCommandMode = true; //Allow AT parsing. Prevent received RF data from being printed. forceRadioReset = false; //Don't reset the radio link unless a setting requires it + writeOnCommandExit = false; //Don't record settings changes unless user commands it tempSettings = settings; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 8d8ba847..cc533543 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -77,7 +77,7 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Multi-Point data exchange // State RX Name Description - {RADIO_MP_BEGIN_SCAN, 0, "MP_BEGIN_SCAN", "MP: Setup for CAD Scanning"}, //15 + {RADIO_MP_BEGIN_SCAN, 0, "MP_BEGIN_SCAN", "MP: Setup for Scanning"}, //15 {RADIO_MP_SCANNING, 0, "MP_SCANNING", "MP: Scanning for activity"}, //16 {RADIO_MP_WAIT_TX_PING_DONE, 0, "MP_WAIT_TX_PING_DONE", "MP: Wait for ping to xmit"}, //17 {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //18 From 5ccaea535622f45c1a4de1b1e10bf7de8c40dccd Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:23:03 -0700 Subject: [PATCH 303/594] Add channel number to VC ACK1 datagram --- Firmware/LoRaSerial_Firmware/Radio.ino | 36 ++++++++++++++++++-------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index f5063c85..939e1700 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1008,9 +1008,9 @@ bool xmitDatagramMpHeartbeat() return (transmitDatagram()); } -//Ack packet sent by server in response the client ping, includes channel number -//During Multipoint scanning, it's possible for the client to get an ack but be 500kHz off -//The channel Number ensures that the client gets the next hop correct +//ACK packet sent by server in response the client ping, includes channel number +//During discovery scanning, it's possible for the client to get an ACK but be on an adjacent channel +//The channel number ensures that the client gets the next hop correct bool xmitDatagramMpAck() { radioCallHistory[RADIO_CALL_xmitDatagramMpAck] = millis(); @@ -1383,6 +1383,9 @@ bool xmitVcPing(int8_t destVc) return (transmitDatagram()); } +//ACK packet sent by server in response the client ping, includes channel number +//During discovery scanning, it's possible for the client to get an ACK but be on an adjacent channel +//The channel number ensures that the client gets the next hop correct bool xmitVcAck1(int8_t destVc) { VC_RADIO_MESSAGE_HEADER * vcHeader; @@ -1395,15 +1398,26 @@ bool xmitVcAck1(int8_t destVc) vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; + memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); + endOfTxData += sizeof(channelNumber); + vcHeader->length++; + + if(settings.debugSync) + { + systemPrint(" channelNumber: "); + systemPrintln(channelNumber); + outputSerialData(true); + } + /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+----------+---------+----------+ - | Optional | | Optional | Optional | | | Optional | - | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | n Bytes | - +----------+---------+----------+------------+----------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+---------+---------+----------+ + | Optional | | Optional | Optional | | | Channel | Optional | + | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Number | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | 1 byte | n Bytes | + +----------+---------+----------+------------+----------+---------+---------+----------+ */ txControl.datagramType = DATAGRAM_ACK_1; From 8ab9cfe856981974bc66f1423ebf63771c54ec4e Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:25:14 -0700 Subject: [PATCH 304/594] Allow packet transmission in VC mode if scanning for server --- Firmware/LoRaSerial_Firmware/Radio.ino | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 939e1700..7bf8e4bd 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2684,9 +2684,19 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond //Drop this datagram if the receiver is active - if ((receiveInProcess() == true) || (transactionComplete == true) - || ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) - && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN))) + if ( + (receiveInProcess() == true) + || (transactionComplete == true) + || ( + //If we are in VC mode, and destination is not broadcast, + //and the destination circuit is offline + //and we are not scanning for servers + //then don't transmit + (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + && (txDestVc != VC_BROADCAST) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN) + && (radioState != RADIO_MP_SCANNING) + ) { triggerEvent(TRIGGER_TRANSMIT_CANCELED); if (settings.debugReceive || settings.debugDatagrams) From c29fa930ecfa80f084979aab9f8ead31ffb64d12 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:25:19 -0700 Subject: [PATCH 305/594] Update Radio.ino --- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 7bf8e4bd..408bbd1d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2697,6 +2697,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN) && (radioState != RADIO_MP_SCANNING) ) + ) { triggerEvent(TRIGGER_TRANSMIT_CANCELED); if (settings.debugReceive || settings.debugDatagrams) From bda54133a5cbfb6640ad14926804ea200a9d3e83 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:26:01 -0700 Subject: [PATCH 306/594] Begin in scanning mode if VC is client --- Firmware/LoRaSerial_Firmware/States.ino | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3ceac1d1..fa2eec32 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -94,15 +94,22 @@ void updateRadioState() else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { if (settings.server) + { //Reserve the server's address (0) myVc = vcIdToAddressByte(VC_SERVER, myUniqueId); + + startChannelTimer(); //Start hopping + + //Start sending heartbeats + xmitVcHeartbeat(myVc, myUniqueId); + changeState(RADIO_VC_WAIT_TX_DONE); + } else + { //Unknown client address myVc = VC_UNASSIGNED; - - //Start sending heartbeats - xmitVcHeartbeat(myVc, myUniqueId); - changeState(RADIO_VC_WAIT_TX_DONE); + changeState(RADIO_MP_BEGIN_SCAN); + } } else { @@ -112,7 +119,9 @@ void updateRadioState() changeState(RADIO_MP_STANDBY); } else + { changeState(RADIO_MP_BEGIN_SCAN); + } } break; From 4f0f4e83a20661b624d1738878ce30b15056da26 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:27:43 -0700 Subject: [PATCH 307/594] Modify MP Scanning for use with VC --- Firmware/LoRaSerial_Firmware/States.ino | 47 ++++++++++++++++++++----- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index fa2eec32..bf966e75 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1038,7 +1038,7 @@ void updateRadioState() break; //==================== - //Walk through channel table transmitting a Ping and looking for an Ack + //Walk through channel table backwards, transmitting a Ping and looking for an ACK1 //==================== case RADIO_MP_SCANNING: if (transactionComplete) @@ -1080,16 +1080,21 @@ void updateRadioState() case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: //We should not be receiving these datagrams, but if we do, just ignore - frequencyCorrection += radio.getFrequencyError() / 1000000.0; triggerEvent(TRIGGER_BAD_PACKET); break; case DATAGRAM_ACK_1: - //Server has responded with ack + triggerEvent(TRIGGER_LINK_ACK_RECEIVED); + + //Server has responded with ACK syncChannelTimer(); //Start and adjust freq hop ISR based on remote's remaining clock - channelNumber = rxData[0]; //Change to the server's channel number - if (settings.debugReceive) + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + channelNumber = rxVcData[0]; + else + channelNumber = rxData[0]; //Change to the server's channel number + + if (settings.debugSync) { systemPrint(" Channel Number: "); systemPrintln(channelNumber); @@ -1102,8 +1107,21 @@ void updateRadioState() lastPacketReceived = millis(); //Reset - triggerEvent(TRIGGER_LINK_ACK_RECEIVED); - changeState(RADIO_MP_STANDBY); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Acknowledge the ACK1 + triggerEvent(TRIGGER_SEND_ACK2); + if (xmitVcAck2(VC_SERVER)) + { + sf6ExpectedSize = MAX_PACKET_SIZE; //Tell SF6 to return to max packet length + changeState(RADIO_VC_WAIT_TX_DONE); + vcZeroAcks(VC_SERVER); + vcChangeState(VC_SERVER, VC_STATE_CONNECTED); + } + } + else + changeState(RADIO_MP_STANDBY); + break; } } @@ -1141,8 +1159,19 @@ void updateRadioState() } //Send ping - if (xmitDatagramMpPing() == true) - changeState(RADIO_MP_WAIT_TX_PING_DONE); + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Send the PING datagram, first part of the 3-way handshake + if (xmitVcPing(VC_SERVER)) + { + changeState(RADIO_MP_WAIT_TX_PING_DONE); + } + } + else + { + if (xmitDatagramMpPing() == true) + changeState(RADIO_MP_WAIT_TX_PING_DONE); + } } } From b48a385c77ec233864e23005bc718c28f37f96cf Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:28:07 -0700 Subject: [PATCH 308/594] VC: Always hop while waiting --- Firmware/LoRaSerial_Firmware/States.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bf966e75..9ff9dc5b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1697,6 +1697,9 @@ void updateRadioState() //Wait for a HEARTBEAT from the server //==================== case RADIO_VC_WAIT_SERVER: + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + if (myVc == VC_SERVER) { changeState(RADIO_VC_WAIT_RECEIVE); From 8f27771ab65e04da1772a02bea35e6788011ad1a Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:42:25 -0700 Subject: [PATCH 309/594] Adjust client clocks only on server heartbeats. --- Firmware/LoRaSerial_Firmware/States.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9ff9dc5b..6a96b078 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2851,6 +2851,9 @@ void vcReceiveHeartbeat(uint32_t rxMillis) uint32_t deltaMillis; int vcSrc; + if (rxSrcVc == VC_SERVER) + syncChannelTimer(); //Adjust freq hop ISR based on server's remaining clock + //Update the timestamp offset if (rxSrcVc == VC_SERVER) { From 0959d728e23ff6f54ca33c20c3246bc99d85ca30 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:45:11 -0700 Subject: [PATCH 310/594] Add new VC state: New Client --- Firmware/LoRaSerial_Firmware/States.ino | 8 +++++++- Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h | 5 +++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 6a96b078..ce62a567 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2677,12 +2677,18 @@ void vcChangeState(int8_t vcIndex, uint8_t state) systemPrint(vcIndex); systemPrintln(" ALIVE =--=--=-"); } + else if (state == VC_STATE_NEW_CLIENT) + { + systemPrint("-=--=--=- VC "); + systemPrint(vcIndex); + systemPrintln(" NEW CLIENT =--=--=-"); + } outputSerialData(true); } //Determine if the VC is connecting vcBit = 1 << vcIndex; - if ((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) + if (state == VC_STATE_NEW_CLIENT) vcConnecting |= vcBit; else vcConnecting &= ~vcBit; diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 2788a7af..21d73a69 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -145,6 +145,7 @@ typedef enum VC_STATE_WAIT_FOR_ACK1 ,//3: PING sent, waiting for ACK1 VC_STATE_WAIT_FOR_ACK2 ,//4: ACK1 sent, waiting for ACK2 VC_STATE_CONNECTED, //5: ACK2 received, ACKs cleared, ready to send data + VC_STATE_NEW_CLIENT, //6: A server received a PING from a previously unknown client //Insert new states before this line VC_STATE_MAX @@ -172,8 +173,8 @@ typedef struct _VC_DATA_ACK_NACK_MESSAGE const char * const vcStateNames[] = { // 0 1 "LINK-DOWN", "LINK-ALIVE", - // 2 3 4 5 - "SEND-PING", "WAIT-ACK1", "WAIT-ACK2", "CONNECTED", + // 2 3 4 5 6 + "SEND-PING", "WAIT-ACK1", "WAIT-ACK2", "CONNECTED", "NEW-CLIENT", }; #endif //ADD_VC_STATE_NAMES_TABLE From eaeb1e10dd86a334aed51f4857c44f1fdf9ea621 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:45:33 -0700 Subject: [PATCH 311/594] Remove priority 4 --- Firmware/LoRaSerial_Firmware/States.ino | 122 ++++++++++++------------ 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ce62a567..0da0ae78 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2036,67 +2036,67 @@ void updateRadioState() //---------- //Priority 4: Walk through the 3-way handshake //---------- - else if (vcConnecting) - { - for (index = 0; index < MAX_VC; index++) - { - if (receiveInProcess()) - break; - - //Determine the first VC that is walking through connections - if (vcConnecting & (1 << index)) - { - //Determine if PING needs to be sent - if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) - { - //Send the PING datagram, first part of the 3-way handshake - if (xmitVcPing(index)) - { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - - //The ACK1 is handled with the receive code - //Check for a timeout waiting for the ACK1 - else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) - { - if ((currentMillis - virtualCircuitList[index].lastPingMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) - { - //Retransmit the PING - if (xmitVcPing(index)) - { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - } - - //The ACK2 is handled with the receive code - //Check for a timeout waiting for the ACK2 - else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) - { - if ((currentMillis - virtualCircuitList[index].lastPingMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) - { - //Retransmit the ACK1 - if (xmitVcAck1(index)) - { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - } - - //Work on only one connection at a time - break; - } - } - } + // else if (vcConnecting) + // { + // for (index = 0; index < MAX_VC; index++) + // { + // if (receiveInProcess()) + // break; + // + // //Determine the first VC that is walking through connections + // if (vcConnecting & (1 << index)) + // { + // //Determine if PING needs to be sent + // if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) + // { + // //Send the PING datagram, first part of the 3-way handshake + // if (xmitVcPing(index)) + // { + // vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); + // virtualCircuitList[index].lastPingMillis = datagramTimer; + // changeState(RADIO_VC_WAIT_TX_DONE); + // } + // } + // + // //The ACK1 is handled with the receive code + // //Check for a timeout waiting for the ACK1 + // else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) + // { + // if ((currentMillis - virtualCircuitList[index].lastPingMillis) + // >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + // { + // //Retransmit the PING + // if (xmitVcPing(index)) + // { + // vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); + // virtualCircuitList[index].lastPingMillis = datagramTimer; + // changeState(RADIO_VC_WAIT_TX_DONE); + // } + // } + // } + // + // //The ACK2 is handled with the receive code + // //Check for a timeout waiting for the ACK2 + // else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) + // { + // if ((currentMillis - virtualCircuitList[index].lastPingMillis) + // >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + // { + // //Retransmit the ACK1 + // if (xmitVcAck1(index)) + // { + // vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); + // virtualCircuitList[index].lastPingMillis = datagramTimer; + // changeState(RADIO_VC_WAIT_TX_DONE); + // } + // } + // } + // + // //Work on only one connection at a time + // break; + // } + // } + // } //---------- //Lowest Priority: Check for data to send From 139a87555a77fd93b5c5d0de671d29abe9992b75 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:46:27 -0700 Subject: [PATCH 312/594] Remove RADIO_VC_WAIT_SERVER --- Firmware/LoRaSerial_Firmware/States.ino | 34 +------------------------ Firmware/LoRaSerial_Firmware/settings.h | 2 -- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 0da0ae78..0ee1a3ed 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1693,38 +1693,6 @@ void updateRadioState() */ - //==================== - //Wait for a HEARTBEAT from the server - //==================== - case RADIO_VC_WAIT_SERVER: - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - if (myVc == VC_SERVER) - { - changeState(RADIO_VC_WAIT_RECEIVE); - break; - } - - //If dio0ISR has fired, a packet has arrived - currentMillis = millis(); - if (transactionComplete == true) - { - //Decode the received datagram - PacketType packetType = rcvDatagram(); - - //Process the received datagram - if ((packetType == DATAGRAM_VC_HEARTBEAT) && (rxSrcVc == VC_SERVER)) - { - vcReceiveHeartbeat(millis() - currentMillis); - changeState(RADIO_VC_WAIT_RECEIVE); - } - else - //Ignore this datagram - triggerEvent(TRIGGER_BAD_PACKET); - } - break; - //==================== //Wait for the transmission to complete //==================== @@ -2237,7 +2205,7 @@ void updateRadioState() //and the radios loose communications because they are hopping at different //times. serverLinkBroken = true; - changeState(RADIO_VC_WAIT_SERVER); + changeState(RADIO_MP_BEGIN_SCAN); } //Break the link diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index cc533543..02f73f2a 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -35,7 +35,6 @@ typedef enum RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE, //Virtual-Circuit states - RADIO_VC_WAIT_SERVER, RADIO_VC_WAIT_TX_DONE, RADIO_VC_WAIT_RECEIVE, @@ -97,7 +96,6 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Virtual circuit states // State RX Name Description - {RADIO_VC_WAIT_SERVER, 1, "VC_WAIT_SERVER", "VC: Wait for the server"}, //26 {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "VC: Wait for TX done"}, //27 {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "VC: Wait for receive"}, //28 }; From 7b47d711bb7e493a43e93757cbf80fa6e35f56a8 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 14:50:33 -0700 Subject: [PATCH 313/594] Rename MP_SCAN to SERVER_SCAN --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 18 +++++++++--------- Firmware/LoRaSerial_Firmware/settings.h | 13 +++++++++---- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 408bbd1d..55392dc3 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2695,7 +2695,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN) - && (radioState != RADIO_MP_SCANNING) + && (radioState != RADIO_SERVER_SCANNING) ) ) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 0ee1a3ed..a7aaa02e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -108,7 +108,7 @@ void updateRadioState() { //Unknown client address myVc = VC_UNASSIGNED; - changeState(RADIO_MP_BEGIN_SCAN); + changeState(RADIO_BEGIN_SERVER_SCAN); } } else @@ -120,7 +120,7 @@ void updateRadioState() } else { - changeState(RADIO_MP_BEGIN_SCAN); + changeState(RADIO_BEGIN_SERVER_SCAN); } } break; @@ -1027,20 +1027,20 @@ void updateRadioState() //==================== //Start searching for other radios //==================== - case RADIO_MP_BEGIN_SCAN: + case RADIO_BEGIN_SERVER_SCAN: stopChannelTimer(); //Stop hopping multipointChannelLoops = 0; multipointAttempts = 0; triggerEvent(TRIGGER_MP_SCAN); - changeState(RADIO_MP_SCANNING); + changeState(RADIO_SERVER_SCANNING); break; //==================== //Walk through channel table backwards, transmitting a Ping and looking for an ACK1 //==================== - case RADIO_MP_SCANNING: + case RADIO_SERVER_SCANNING: if (transactionComplete) { //Decode the received datagram @@ -1185,7 +1185,7 @@ void updateRadioState() { transactionComplete = false; //Reset ISR flag returnToReceiving(); - changeState(RADIO_MP_SCANNING); + changeState(RADIO_SERVER_SCANNING); } break; @@ -1341,7 +1341,7 @@ void updateRadioState() { if ((millis() - lastPacketReceived) > (settings.heartbeatTimeout * 3)) { - changeState(RADIO_MP_BEGIN_SCAN); + changeState(RADIO_BEGIN_SERVER_SCAN); } } } @@ -1638,7 +1638,7 @@ void updateRadioState() RADIO_RESET | V - RADIO_VC_WAIT_SERVER + RADIO_SERVER_SCANNING | V +<-------------------------. @@ -2205,7 +2205,7 @@ void updateRadioState() //and the radios loose communications because they are hopping at different //times. serverLinkBroken = true; - changeState(RADIO_MP_BEGIN_SCAN); + changeState(RADIO_BEGIN_SERVER_SCAN); } //Break the link diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 02f73f2a..00475d85 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -17,9 +17,11 @@ typedef enum RADIO_P2P_LINK_UP_WAIT_ACK, RADIO_P2P_LINK_UP_HB_ACK_REXMT, + //Server-client discovery + RADIO_BEGIN_SERVER_SCAN, + RADIO_SERVER_SCANNING, + //Multi-Point: Datagrams - RADIO_MP_BEGIN_SCAN, - RADIO_MP_SCANNING, RADIO_MP_WAIT_TX_PING_DONE, RADIO_MP_WAIT_TX_ACK_DONE, RADIO_MP_STANDBY, @@ -74,10 +76,13 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_P2P_LINK_UP_WAIT_ACK, 1, "P2P_LINK_UP_WAIT_ACK", "P2P: Waiting for ACK"}, //13 {RADIO_P2P_LINK_UP_HB_ACK_REXMT, 0, "P2P_LINK_UP_HB_ACK_REXMT", "P2P: Heartbeat ACK ReXmt"}, //14 + //Server-client discovery + // State RX Name Description + {RADIO_BEGIN_SERVER_SCAN, 0, "MP_BEGIN_SERVER_SCAN", "Scan: Setup for Scanning"}, //15 + {RADIO_SERVER_SCANNING, 0, "MP_SERVER_SCANNING", "Scan: Scanning for server"}, //16 + //Multi-Point data exchange // State RX Name Description - {RADIO_MP_BEGIN_SCAN, 0, "MP_BEGIN_SCAN", "MP: Setup for Scanning"}, //15 - {RADIO_MP_SCANNING, 0, "MP_SCANNING", "MP: Scanning for activity"}, //16 {RADIO_MP_WAIT_TX_PING_DONE, 0, "MP_WAIT_TX_PING_DONE", "MP: Wait for ping to xmit"}, //17 {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //18 {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //19 From d0dad3d7070bf5fe9669544e6291dcc937f36b64 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 15:03:48 -0700 Subject: [PATCH 314/594] Rename to DISCOVER --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 24 ++++++++++++------------ Firmware/LoRaSerial_Firmware/settings.h | 12 ++++++------ 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 55392dc3..5a31cfed 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2695,7 +2695,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN) - && (radioState != RADIO_SERVER_SCANNING) + && (radioState != RADIO_DISCOVER_SCANNING) ) ) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index a7aaa02e..ef91e071 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -108,7 +108,7 @@ void updateRadioState() { //Unknown client address myVc = VC_UNASSIGNED; - changeState(RADIO_BEGIN_SERVER_SCAN); + changeState(RADIO_DISCOVER_BEGIN); } } else @@ -120,7 +120,7 @@ void updateRadioState() } else { - changeState(RADIO_BEGIN_SERVER_SCAN); + changeState(RADIO_DISCOVER_BEGIN); } } break; @@ -1027,20 +1027,20 @@ void updateRadioState() //==================== //Start searching for other radios //==================== - case RADIO_BEGIN_SERVER_SCAN: + case RADIO_DISCOVER_BEGIN: stopChannelTimer(); //Stop hopping multipointChannelLoops = 0; multipointAttempts = 0; triggerEvent(TRIGGER_MP_SCAN); - changeState(RADIO_SERVER_SCANNING); + changeState(RADIO_DISCOVER_SCANNING); break; //==================== //Walk through channel table backwards, transmitting a Ping and looking for an ACK1 //==================== - case RADIO_SERVER_SCANNING: + case RADIO_DISCOVER_SCANNING: if (transactionComplete) { //Decode the received datagram @@ -1164,13 +1164,13 @@ void updateRadioState() //Send the PING datagram, first part of the 3-way handshake if (xmitVcPing(VC_SERVER)) { - changeState(RADIO_MP_WAIT_TX_PING_DONE); + changeState(RADIO_DISCOVER_WAIT_TX_PING_DONE); } } else { if (xmitDatagramMpPing() == true) - changeState(RADIO_MP_WAIT_TX_PING_DONE); + changeState(RADIO_DISCOVER_WAIT_TX_PING_DONE); } } } @@ -1180,12 +1180,12 @@ void updateRadioState() //==================== //Wait for the PING to complete transmission //==================== - case RADIO_MP_WAIT_TX_PING_DONE: + case RADIO_DISCOVER_WAIT_TX_PING_DONE: if (transactionComplete) { transactionComplete = false; //Reset ISR flag returnToReceiving(); - changeState(RADIO_SERVER_SCANNING); + changeState(RADIO_DISCOVER_SCANNING); } break; @@ -1341,7 +1341,7 @@ void updateRadioState() { if ((millis() - lastPacketReceived) > (settings.heartbeatTimeout * 3)) { - changeState(RADIO_BEGIN_SERVER_SCAN); + changeState(RADIO_DISCOVER_BEGIN); } } } @@ -1638,7 +1638,7 @@ void updateRadioState() RADIO_RESET | V - RADIO_SERVER_SCANNING + RADIO_DISCOVER_SCANNING | V +<-------------------------. @@ -2205,7 +2205,7 @@ void updateRadioState() //and the radios loose communications because they are hopping at different //times. serverLinkBroken = true; - changeState(RADIO_BEGIN_SERVER_SCAN); + changeState(RADIO_DISCOVER_BEGIN); } //Break the link diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 00475d85..aa0eadf0 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -18,11 +18,11 @@ typedef enum RADIO_P2P_LINK_UP_HB_ACK_REXMT, //Server-client discovery - RADIO_BEGIN_SERVER_SCAN, - RADIO_SERVER_SCANNING, + RADIO_DISCOVER_BEGIN, + RADIO_DISCOVER_SCANNING, + RADIO_DISCOVER_WAIT_TX_PING_DONE, //Multi-Point: Datagrams - RADIO_MP_WAIT_TX_PING_DONE, RADIO_MP_WAIT_TX_ACK_DONE, RADIO_MP_STANDBY, RADIO_MP_WAIT_TX_DONE, @@ -78,12 +78,12 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Server-client discovery // State RX Name Description - {RADIO_BEGIN_SERVER_SCAN, 0, "MP_BEGIN_SERVER_SCAN", "Scan: Setup for Scanning"}, //15 - {RADIO_SERVER_SCANNING, 0, "MP_SERVER_SCANNING", "Scan: Scanning for server"}, //16 + {RADIO_DISCOVER_BEGIN, 0, "DISCOVER_BEGIN", "Disc: Setup for scanning"}, //15 + {RADIO_DISCOVER_SCANNING, 0, "DISCOVER_SCANNING", "Disc: Scanning for servers"}, //16 + {RADIO_DISCOVER_WAIT_TX_PING_DONE, 0, "DISCOVER_WAIT_TX_PING_DONE", "Disc: Wait for ping to xmit"}, //17 //Multi-Point data exchange // State RX Name Description - {RADIO_MP_WAIT_TX_PING_DONE, 0, "MP_WAIT_TX_PING_DONE", "MP: Wait for ping to xmit"}, //17 {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //18 {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //19 {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //20 From a8eb9433d0c33a53c34296226f9f7d816ccee3a9 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 15 Dec 2022 16:25:57 -0700 Subject: [PATCH 315/594] VC: Don't store ping times from unassigned clients --- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 31 +++++++++++++++++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 5a31cfed..ddd03fcc 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2695,6 +2695,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) && (txDestVc != VC_BROADCAST) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN) + && (txDestVc != VC_UNASSIGNED) && (radioState != RADIO_DISCOVER_SCANNING) ) ) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ef91e071..9cca65c7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1115,8 +1115,6 @@ void updateRadioState() { sf6ExpectedSize = MAX_PACKET_SIZE; //Tell SF6 to return to max packet length changeState(RADIO_VC_WAIT_TX_DONE); - vcZeroAcks(VC_SERVER); - vcChangeState(VC_SERVER, VC_STATE_CONNECTED); } } else @@ -1706,7 +1704,7 @@ void updateRadioState() { transactionComplete = false; - //Indicate that the receive is complete + //Indicate that the transmission is complete triggerEvent(TRIGGER_VC_TX_DONE); //Start the receive operation @@ -1801,10 +1799,29 @@ void updateRadioState() //Second step in the 3-way handshake, received PING, respond with ACK1 case DATAGRAM_PING: - if (xmitVcAck1(rxSrcVc)) - changeState(RADIO_VC_WAIT_TX_DONE); - vcChangeState(rxSrcVc, VC_STATE_WAIT_FOR_ACK2); - virtualCircuitList[rxSrcVc].lastPingMillis = datagramTimer; + //Only respond to pings if we are server + if (settings.server == true) + { + if (rxSrcVc == VC_UNASSIGNED) + { + //We received a ping from a previously unknown client + if (xmitVcAck1(rxSrcVc)) + { + triggerEvent(TRIGGER_MP_SEND_ACK_FOR_PING); + changeState(RADIO_VC_WAIT_TX_DONE); + } + } + else + { + //Known client + if (xmitVcAck1(rxSrcVc)) + changeState(RADIO_VC_WAIT_TX_DONE); + + vcChangeState(rxSrcVc, VC_STATE_WAIT_FOR_ACK2); + virtualCircuitList[rxSrcVc].lastPingMillis = datagramTimer; + } + } + break; //Third step in the 3-way handshake, received ACK1, respond with ACK2 From b16c6660edcdf56ba1d13701d4f0bbac25399b7f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 15 Dec 2022 15:31:13 -1000 Subject: [PATCH 316/594] Fix at-server=r, properly parse an integer and floating point value The strtod function does not set errno upon error! --- Firmware/LoRaSerial_Firmware/Commands.ino | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 1290f598..41c03abd 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1111,6 +1111,7 @@ bool commandSetByName(const char * commandString) //Set or display the command bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer) { + const char * digit; double doubleSettingValue; uint32_t settingValue; bool valid; @@ -1127,6 +1128,23 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer if (*buffer++ != '=') break; + //Verify the input value + for (digit = buffer; *digit != 0; digit++) + if ((*digit < '0') || (*digit > '9')) + { + //Floating point values may contain a decimal point + if ((command->type == TYPE_FLOAT) && (*digit == '.')) + continue; + + //Hexadecimal values may contain A through F + if ((toupper(*digit) >= 'A') && (toupper(*digit) <= 'F') + && (command->type == TYPE_KEY)) + continue; + break; + } + if (*digit) + break; + //Get the value doubleSettingValue = strtod(buffer, NULL); settingValue = doubleSettingValue; From adf1e15cb2ee26c6670a2658e56faadbe13cdff0 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 15 Dec 2022 16:10:07 -1000 Subject: [PATCH 317/594] Display the setting value if just the setting name is entered No need for the ? --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 41c03abd..7a8fc1d7 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1118,7 +1118,7 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer do { //Is this a display request - if (strcmp(buffer, "?") == 0) + if ((*buffer == 0) || (strcmp(buffer, "?") == 0)) { commandDisplay(command); return true; From 181aab32b53cf245322b57772d5fedf4ed99fa4c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 14 Dec 2022 10:03:30 -1000 Subject: [PATCH 318/594] Remove the remaining part of point-to-point training --- Firmware/LoRaSerial_Firmware/Radio.ino | 54 ------------------------- Firmware/LoRaSerial_Firmware/settings.h | 43 +++++++++----------- 2 files changed, 18 insertions(+), 79 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index ddd03fcc..0c3fa1b0 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -674,60 +674,6 @@ const uint16_t crc16Table[256] = 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0 }; -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Point-To-Point Training -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Ping the other radio in the point-to-point configuration -bool xmitDatagramP2PTrainingPing() -{ - radioCallHistory[RADIO_CALL_xmitDatagramP2PTrainingPing] = millis(); - - /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+----------+ - | Optional | | Optional | Optional | Optional | - | NET ID | Control | C-Timer | SF6 Length | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | - +----------+---------+----------+------------+----------+ - */ - - txControl.datagramType = DATAGRAM_P2P_TRAINING_PING; - return (transmitDatagram()); -} - -//Build the parameters packet used for training -bool xmitDatagramP2pTrainingParams() -{ - Settings params; - - radioCallHistory[RADIO_CALL_xmitDatagramP2pTrainingParams] = millis(); - - //Initialize the radio parameters - memcpy(¶ms, &tempSettings, sizeof(settings)); - params.operatingMode = MODE_POINT_TO_POINT; - - //Add the radio parameters - memcpy(endOfTxData, ¶ms, sizeof(params)); - endOfTxData += sizeof(params); - - /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | Optional | Radio | Optional | - | NET ID | Control | C-Timer | SF6 Length | Parameters | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | - +----------+---------+----------+------------+-------------+----------+ - */ - - txControl.datagramType = DATAGRAM_P2P_TRAINING_PARAMS; - return (transmitDatagram()); -} - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Point-To-Point: Bring up the link // diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index aa0eadf0..66907539 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -108,33 +108,28 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Possible types of packets received typedef enum { - //Packet types must start at zero - //Point-to-Point training - DATAGRAM_P2P_TRAINING_PING = 0, // 0 - DATAGRAM_P2P_TRAINING_PARAMS, // 1 - //Link establishment handshake - DATAGRAM_PING, // 2 - DATAGRAM_ACK_1, // 3 - DATAGRAM_ACK_2, // 4 + DATAGRAM_PING = 0, // 0 + DATAGRAM_ACK_1, // 1 + DATAGRAM_ACK_2, // 2 //Point-to-Point data exchange - DATAGRAM_DATA, // 5 - DATAGRAM_DATA_ACK, // 6 - DATAGRAM_HEARTBEAT, // 7 - DATAGRAM_REMOTE_COMMAND, // 8 - DATAGRAM_REMOTE_COMMAND_RESPONSE, // 9 + DATAGRAM_DATA, // 3 + DATAGRAM_DATA_ACK, // 4 + DATAGRAM_HEARTBEAT, // 5 + DATAGRAM_REMOTE_COMMAND, // 6 + DATAGRAM_REMOTE_COMMAND_RESPONSE, // 7 //Multi-Point data exchange - DATAGRAM_DATAGRAM, //10 + DATAGRAM_DATAGRAM, // 8 //Multi-Point training exchange - DATAGRAM_TRAINING_PING, //11 - DATAGRAM_TRAINING_PARAMS, //12 - DATAGRAM_TRAINING_ACK, //13 + DATAGRAM_TRAINING_PING, // 9 + DATAGRAM_TRAINING_PARAMS, //10 + DATAGRAM_TRAINING_ACK, //11 //Virtual-Circuit (VC) exchange - DATAGRAM_VC_HEARTBEAT, //14 + DATAGRAM_VC_HEARTBEAT, //12 //Add new datagram types before this line MAX_DATAGRAM_TYPE, @@ -150,17 +145,15 @@ typedef enum } PacketType; const char * const radioDatagramType[] = -{ // 0 1 - "P2P_TRAINING_PING", "P2P_TRAINING_PARAMS", - // 2 3 4 +{ // 0 1 2 "PING", "ACK-1", "ACK-2", - // 5 6 7 8 9 + // 3 4 5 6 7 "DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", - // 10 + // 8 "DATAGRAM", - // 11 12 13 + // 9 10 11 "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK", - // 14 + // 12 "VC_HEARTBEAT", }; From b9a88f3b317044cf944fc0cb95aeeb872ad2fda3 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 14 Dec 2022 07:46:56 -1000 Subject: [PATCH 319/594] Combine transmitTimer and vcAckTimer into ackTimer --- Firmware/LoRaSerial_Firmware/Commands.ino | 18 ++--- .../LoRaSerial_Firmware.ino | 3 +- Firmware/LoRaSerial_Firmware/States.ino | 66 +++++++++---------- 3 files changed, 43 insertions(+), 44 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 7a8fc1d7..78bb0bba 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -332,9 +332,9 @@ bool commandAT(const char * commandString) systemPrintln(vc->rmtTxAckNumber); systemPrint(" Last TX ACK number: "); systemPrintln(vc->txAckNumber); - systemPrint(" transmitTimer: "); - if (transmitTimer) - systemPrintTimestamp(transmitTimer); + systemPrint(" ackTimer: "); + if (ackTimer) + systemPrintTimestamp(ackTimer); else systemPrint("Not Running"); systemPrintln(); @@ -342,13 +342,13 @@ bool commandAT(const char * commandString) else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { systemPrintln(" ACK Management"); - systemPrint(" vcAackTimer: "); - if (vcAckTimer) + systemPrint(" ackTimer: "); + if (ackTimer) { systemPrint("VC "); systemPrint(txDestVc); systemPrint(", "); - systemPrintTimestamp(vcAckTimer); + systemPrintTimestamp(ackTimer); systemPrintln(); } else @@ -610,9 +610,9 @@ bool commandAT(const char * commandString) systemPrintln(vc->txAckNumber); if (txDestVc == cmdVc) { - systemPrint(" vcAackTimer: "); - if (vcAckTimer) - systemPrintTimestamp(vcAckTimer); + systemPrint(" ackTimer: "); + if (ackTimer) + systemPrintTimestamp(ackTimer); else systemPrint("Not Running"); systemPrintln(); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 66a00e34..ee69c33c 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -454,7 +454,7 @@ unsigned long nextChannelZeroTimeInMillis; //Transmit control uint8_t * endOfTxData; CONTROL_U8 txControl; -uint32_t transmitTimer; +unsigned long ackTimer; //Retransmit support uint8_t rmtCmdVc; @@ -479,7 +479,6 @@ int8_t rxDestVc; int8_t rxSrcVc; uint8_t *rxVcData; int8_t txDestVc; -unsigned long vcAckTimer; VIRTUAL_CIRCUIT virtualCircuitList[MAX_VC]; uint8_t serialOperatingMode; uint32_t vcConnecting; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9cca65c7..3216b132 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -85,8 +85,8 @@ void updateRadioState() returnToReceiving(); //Start receiving - //Stop the transmit timer - transmitTimer = 0; + //Stop the ACK timer + ackTimer = 0; //Start the link between the radios if (settings.operatingMode == MODE_POINT_TO_POINT) @@ -694,7 +694,7 @@ void updateRadioState() if (xmitDatagramP2PData() == true) { setHeartbeatLong(); //We're sending data, so don't be the first to send next heartbeat - transmitTimer = datagramTimer; + ackTimer = datagramTimer; //Save the previous transmit in case the previous ACK was lost or a //HEARTBEAT must be transmitted. Restore the buffer when a retransmission @@ -740,7 +740,7 @@ void updateRadioState() { setHeartbeatLong(); //We're sending a heartbeat, so don't be the first to send next heartbeat - transmitTimer = datagramTimer; + ackTimer = datagramTimer; //Wait for heartbeat to transmit changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); @@ -834,8 +834,8 @@ void updateRadioState() //The datagram we are expecting syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock - //Stop the transmit timer - transmitTimer = 0; + //Stop the ACK timer + ackTimer = 0; setHeartbeatLong(); //Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. @@ -953,7 +953,7 @@ void updateRadioState() breakLink(); //Retransmits are not getting through in a rational time - else if (transmitTimer && ((millis() - transmitTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) + else if (ackTimer && ((millis() - ackTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) //Break the link breakLink(); break; @@ -1786,7 +1786,7 @@ void updateRadioState() case DATAGRAM_DATA_ACK: vcSendPcAckNack(rexmtTxDestVc, true); - vcAckTimer = 0; + ackTimer = 0; break; case DATAGRAM_DUPLICATE: @@ -1926,7 +1926,7 @@ void updateRadioState() //Priority 2: Wait for an outstanding ACK until it is received, don't //transmit any other data //---------- - else if (vcAckTimer) + else if (ackTimer) { //Verify that the link is still up txDestVc = rexmtTxDestVc; @@ -1934,11 +1934,11 @@ void updateRadioState() && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { //Stop the retransmits - vcAckTimer = 0; + ackTimer = 0; } //Check for retransmit needed - else if ((currentMillis - vcAckTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + else if ((currentMillis - ackTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { //Determine if another retransmit is allowed if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) @@ -1978,10 +1978,10 @@ void updateRadioState() if (retransmitDatagram(((uint8_t)txDestVc <= MAX_VC) ? &virtualCircuitList[txDestVc] : NULL)) changeState(RADIO_VC_WAIT_TX_DONE); - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; + //Since ackTimer is off when equal to zero, force it to a non-zero value + ackTimer = datagramTimer; + if (!ackTimer) + ackTimer = 1; lostFrames++; } else @@ -2008,10 +2008,10 @@ void updateRadioState() if (xmitDatagramP2PCommandResponse()) changeState(RADIO_VC_WAIT_TX_DONE); - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; + //Since ackTimer is off when equal to zero, force it to a non-zero value + ackTimer = datagramTimer; + if (!ackTimer) + ackTimer = 1; //Save the message for retransmission SAVE_TX_BUFFER(); @@ -2114,10 +2114,10 @@ void updateRadioState() if (xmitDatagramP2PData() == true) changeState(RADIO_VC_WAIT_TX_DONE); - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; + //Since ackTimer is off when equal to zero, force it to a non-zero value + ackTimer = datagramTimer; + if (!ackTimer) + ackTimer = 1; //Save the message for retransmission SAVE_TX_BUFFER(); @@ -2186,10 +2186,10 @@ void updateRadioState() if (xmitDatagramP2PCommand() == true) changeState(RADIO_VC_WAIT_TX_DONE); - //Since vcAckTimer is off when equal to zero, force it to a non-zero value - vcAckTimer = datagramTimer; - if (!vcAckTimer) - vcAckTimer = 1; + //Since ackTimer is off when equal to zero, force it to a non-zero value + ackTimer = datagramTimer; + if (!ackTimer) + ackTimer = 1; //Save the message for retransmission SAVE_TX_BUFFER(); @@ -2575,8 +2575,8 @@ void breakLink() } triggerEvent(TRIGGER_RADIO_RESET); - //Stop the transmit timer - transmitTimer = 0; + //Stop the ACK timer + ackTimer = 0; //Flush the buffers resetSerial(); @@ -2601,8 +2601,8 @@ void enterLinkUp() //Discard any previous data discardPreviousData(); - //Stop the transmit timer - transmitTimer = 0; + //Stop the ACK timer + ackTimer = 0; //Mark start time for uptime calculation lastLinkUpTime = millis(); @@ -2712,9 +2712,9 @@ void vcBreakLink(int8_t vcIndex) //If waiting for an ACK and the link breaks, stop the retransmissions //by stopping the ACK timer. - if (vcAckTimer && (txDestVc == vcIndex)) + if (ackTimer && (txDestVc == vcIndex)) { - vcAckTimer = 0; + ackTimer = 0; vcSendPcAckNack(vcIndex, false); } From d0e27380f97cb14f32270380536ed6f0c5ad71e2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 14 Dec 2022 08:11:09 -1000 Subject: [PATCH 320/594] P2P: Use macro to replace common code to send ACK --- Firmware/LoRaSerial_Firmware/States.ino | 55 +++++++++++-------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3216b132..08063e43 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -14,6 +14,21 @@ frameSentCount = rexmtFrameSentCount; \ } +#define P2P_SEND_ACK(trigger) \ + { \ + /*Compute the frequency correction*/ \ + frequencyCorrection += radio.getFrequencyError() / 1000000.0; \ + \ + /*Send the ACK to the remote system*/ \ + triggerEvent(trigger); \ + if (xmitDatagramP2PAck() == true) \ + { \ + /*We ack'd the packet so be responsible for sending the next heartbeat*/ \ + setHeartbeatShort(); \ + changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); \ + } \ + } + //Process the radio states void updateRadioState() { @@ -603,28 +618,16 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); - if (xmitDatagramP2PAck() == true) //Transmit ACK - { - setHeartbeatShort(); //We ack'd this heartbeat so be responsible for sending the next heartbeat - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); - } + //Transmit ACK + P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); break; case DATAGRAM_DATA: //Place the data in the serial output buffer serialBufferOutput(rxData, rxDataBytes); - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); - if (xmitDatagramP2PAck() == true) //Transmit ACK - { - setHeartbeatShort(); //We ack'd this data, so be responsible for sending the next heartbeat - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); - } + //Transmit ACK + P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_DATA); break; case DATAGRAM_REMOTE_COMMAND: @@ -643,14 +646,8 @@ void updateRadioState() commandRXHead += rxDataBytes - length; commandRXHead %= sizeof(commandRXBuffer); - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); - if (xmitDatagramP2PAck() == true) //Transmit ACK - { - setHeartbeatShort(); //We ack'd the packet so be responsible for sending the next heartbeat - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); - } + //Transmit ACK + P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); break; case DATAGRAM_REMOTE_COMMAND_RESPONSE: @@ -658,14 +655,8 @@ void updateRadioState() for (int x = 0 ; x < rxDataBytes ; x++) Serial.write(rxData[x]); - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE); - if (xmitDatagramP2PAck() == true) //Transmit ACK - { - setHeartbeatShort(); //We ack'd the packet so be responsible for sending the next heartbeat - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); - } + //Transmit ACK + P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE); break; } } From 88dd654e1a01ea4ed607a8b1df5fe31bd7794946 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 14 Dec 2022 08:36:55 -1000 Subject: [PATCH 321/594] Use macros to start and stop the ACK timer --- Firmware/LoRaSerial_Firmware/States.ino | 52 +++++++++++++------------ 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 08063e43..d0d3e639 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -29,6 +29,22 @@ } \ } +#define START_ACK_TIMER() \ + { \ + /*Start the ACK timer*/ \ + ackTimer = datagramTimer; \ + \ + /*Since ackTimer is off when equal to zero, force it to a non-zero value*/ \ + if (!ackTimer) \ + ackTimer = 1; \ + } + +#define STOP_ACK_TIMER() \ + { \ + /*Stop the ACK timer*/ \ + ackTimer = 0; \ + } + //Process the radio states void updateRadioState() { @@ -101,7 +117,7 @@ void updateRadioState() returnToReceiving(); //Start receiving //Stop the ACK timer - ackTimer = 0; + STOP_ACK_TIMER(); //Start the link between the radios if (settings.operatingMode == MODE_POINT_TO_POINT) @@ -685,7 +701,7 @@ void updateRadioState() if (xmitDatagramP2PData() == true) { setHeartbeatLong(); //We're sending data, so don't be the first to send next heartbeat - ackTimer = datagramTimer; + START_ACK_TIMER(); //Save the previous transmit in case the previous ACK was lost or a //HEARTBEAT must be transmitted. Restore the buffer when a retransmission @@ -826,7 +842,7 @@ void updateRadioState() syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock //Stop the ACK timer - ackTimer = 0; + STOP_ACK_TIMER(); setHeartbeatLong(); //Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. @@ -1777,7 +1793,7 @@ void updateRadioState() case DATAGRAM_DATA_ACK: vcSendPcAckNack(rexmtTxDestVc, true); - ackTimer = 0; + STOP_ACK_TIMER(); break; case DATAGRAM_DUPLICATE: @@ -1925,7 +1941,7 @@ void updateRadioState() && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { //Stop the retransmits - ackTimer = 0; + STOP_ACK_TIMER(); } //Check for retransmit needed @@ -1969,10 +1985,7 @@ void updateRadioState() if (retransmitDatagram(((uint8_t)txDestVc <= MAX_VC) ? &virtualCircuitList[txDestVc] : NULL)) changeState(RADIO_VC_WAIT_TX_DONE); - //Since ackTimer is off when equal to zero, force it to a non-zero value - ackTimer = datagramTimer; - if (!ackTimer) - ackTimer = 1; + START_ACK_TIMER(); lostFrames++; } else @@ -1999,10 +2012,7 @@ void updateRadioState() if (xmitDatagramP2PCommandResponse()) changeState(RADIO_VC_WAIT_TX_DONE); - //Since ackTimer is off when equal to zero, force it to a non-zero value - ackTimer = datagramTimer; - if (!ackTimer) - ackTimer = 1; + START_ACK_TIMER(); //Save the message for retransmission SAVE_TX_BUFFER(); @@ -2105,10 +2115,7 @@ void updateRadioState() if (xmitDatagramP2PData() == true) changeState(RADIO_VC_WAIT_TX_DONE); - //Since ackTimer is off when equal to zero, force it to a non-zero value - ackTimer = datagramTimer; - if (!ackTimer) - ackTimer = 1; + START_ACK_TIMER(); //Save the message for retransmission SAVE_TX_BUFFER(); @@ -2177,10 +2184,7 @@ void updateRadioState() if (xmitDatagramP2PCommand() == true) changeState(RADIO_VC_WAIT_TX_DONE); - //Since ackTimer is off when equal to zero, force it to a non-zero value - ackTimer = datagramTimer; - if (!ackTimer) - ackTimer = 1; + START_ACK_TIMER(); //Save the message for retransmission SAVE_TX_BUFFER(); @@ -2567,7 +2571,7 @@ void breakLink() triggerEvent(TRIGGER_RADIO_RESET); //Stop the ACK timer - ackTimer = 0; + STOP_ACK_TIMER(); //Flush the buffers resetSerial(); @@ -2593,7 +2597,7 @@ void enterLinkUp() discardPreviousData(); //Stop the ACK timer - ackTimer = 0; + STOP_ACK_TIMER(); //Mark start time for uptime calculation lastLinkUpTime = millis(); @@ -2705,7 +2709,7 @@ void vcBreakLink(int8_t vcIndex) //by stopping the ACK timer. if (ackTimer && (txDestVc == vcIndex)) { - ackTimer = 0; + STOP_ACK_TIMER(); vcSendPcAckNack(vcIndex, false); } From 6b39ee429b78715a5069e14f3ee0f94b2912c375 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 15 Dec 2022 16:55:00 -1000 Subject: [PATCH 322/594] VC: Simplify receiveInProcess checks --- Firmware/LoRaSerial_Firmware/States.ino | 286 +++++++++++------------- 1 file changed, 132 insertions(+), 154 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d0d3e639..82d307be 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1917,179 +1917,157 @@ void updateRadioState() //---------- //Priority 1: Transmit a HEARTBEAT if necessary //---------- - else if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime) - && (receiveInProcess() == false)) + else if (!receiveInProcess()) { - //Send another heartbeat - if (xmitVcHeartbeat(myVc, myUniqueId)) + if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime)) { - if (((uint8_t)myVc) < MAX_VC) - virtualCircuitList[myVc].lastTrafficMillis = currentMillis; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - - //---------- - //Priority 2: Wait for an outstanding ACK until it is received, don't - //transmit any other data - //---------- - else if (ackTimer) - { - //Verify that the link is still up - txDestVc = rexmtTxDestVc; - if ((txDestVc != VC_BROADCAST) - && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) - { - //Stop the retransmits - STOP_ACK_TIMER(); + //Send another heartbeat + if (xmitVcHeartbeat(myVc, myUniqueId)) + { + if (((uint8_t)myVc) < MAX_VC) + virtualCircuitList[myVc].lastTrafficMillis = currentMillis; + changeState(RADIO_VC_WAIT_TX_DONE); + } } - //Check for retransmit needed - else if ((currentMillis - ackTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + //---------- + //Priority 2: Wait for an outstanding ACK until it is received, don't + //transmit any other data + //---------- + else if (ackTimer) { - //Determine if another retransmit is allowed - if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) + //Verify that the link is still up + txDestVc = rexmtTxDestVc; + if ((txDestVc != VC_BROADCAST) + && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { - rexmtFrameSentCount++; + //Stop the retransmits + STOP_ACK_TIMER(); + } - //Restore the message for retransmission - RESTORE_TX_BUFFER(); - if (settings.debugDatagrams) + //Check for retransmit needed + else if ((currentMillis - ackTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + { + //Determine if another retransmit is allowed + if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) { - systemPrintTimestamp(); - systemPrint("TX: Retransmit "); - systemPrint(frameSentCount); - systemPrint(", "); - systemPrint(radioDatagramType[txControl.datagramType]); - switch (txControl.datagramType) - { - default: - systemPrintln(); - break; + rexmtFrameSentCount++; - case DATAGRAM_DATA: - case DATAGRAM_DATA_ACK: - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - systemPrint(" (ACK #"); - systemPrint(txControl.ackNumber); - systemPrint(")"); - systemPrintln(); - break; + //Restore the message for retransmission + RESTORE_TX_BUFFER(); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("TX: Retransmit "); + systemPrint(frameSentCount); + systemPrint(", "); + systemPrint(radioDatagramType[txControl.datagramType]); + switch (txControl.datagramType) + { + default: + systemPrintln(); + break; + + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + case DATAGRAM_HEARTBEAT: + systemPrint(" (ACK #"); + systemPrint(txControl.ackNumber); + systemPrint(")"); + systemPrintln(); + break; + } + outputSerialData(true); } - outputSerialData(true); - } - //Retransmit the packet - if (retransmitDatagram(((uint8_t)txDestVc <= MAX_VC) ? &virtualCircuitList[txDestVc] : NULL)) - changeState(RADIO_VC_WAIT_TX_DONE); + //Retransmit the packet + if (retransmitDatagram(((uint8_t)txDestVc <= MAX_VC) ? &virtualCircuitList[txDestVc] : NULL)) + changeState(RADIO_VC_WAIT_TX_DONE); - START_ACK_TIMER(); - lostFrames++; - } - else - { - //Failed to reach the other system, break the link - vcBreakLink(txDestVc); + START_ACK_TIMER(); + lostFrames++; + } + else + { + //Failed to reach the other system, break the link + vcBreakLink(txDestVc); + } } } - } - //---------- - //Priority 3: Send the entire command response, toggle between waiting for - //ACK above and transmitting the command response - //---------- - else if (availableTXCommandBytes()) - { - //Send the next portion of the command response - vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; - endOfTxData += VC_RADIO_HEADER_BYTES; - vcHeader->destVc = rmtCmdVc; - vcHeader->srcVc = myVc; - vcHeader->length = readyOutgoingCommandPacket(VC_RADIO_HEADER_BYTES) - + VC_RADIO_HEADER_BYTES; - if (xmitDatagramP2PCommandResponse()) - changeState(RADIO_VC_WAIT_TX_DONE); +/* + //---------- + //Priority 4: Walk through the 3-way handshake + //---------- + else if (vcConnecting) + { + for (index = 0; index < MAX_VC; index++) + { + if (receiveInProcess()) + break; + + //Determine the first VC that is walking through connections + if (vcConnecting & (1 << index)) + { + //Determine if PING needs to be sent + if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) + { + //Send the PING datagram, first part of the 3-way handshake + if (xmitVcPing(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + } + } - START_ACK_TIMER(); + //The ACK1 is handled with the receive code + //Check for a timeout waiting for the ACK1 + else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) + { + if ((currentMillis - virtualCircuitList[index].lastPingMillis) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + { + //Retransmit the PING + if (xmitVcPing(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + } + } + } - //Save the message for retransmission - SAVE_TX_BUFFER(); - rexmtTxDestVc = txDestVc; - } + //The ACK2 is handled with the receive code + //Check for a timeout waiting for the ACK2 + else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) + { + if ((currentMillis - virtualCircuitList[index].lastPingMillis) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + { + //Retransmit the ACK1 + if (xmitVcAck1(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + } + } + } - //---------- - //Priority 4: Walk through the 3-way handshake - //---------- - // else if (vcConnecting) - // { - // for (index = 0; index < MAX_VC; index++) - // { - // if (receiveInProcess()) - // break; - // - // //Determine the first VC that is walking through connections - // if (vcConnecting & (1 << index)) - // { - // //Determine if PING needs to be sent - // if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) - // { - // //Send the PING datagram, first part of the 3-way handshake - // if (xmitVcPing(index)) - // { - // vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); - // virtualCircuitList[index].lastPingMillis = datagramTimer; - // changeState(RADIO_VC_WAIT_TX_DONE); - // } - // } - // - // //The ACK1 is handled with the receive code - // //Check for a timeout waiting for the ACK1 - // else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) - // { - // if ((currentMillis - virtualCircuitList[index].lastPingMillis) - // >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) - // { - // //Retransmit the PING - // if (xmitVcPing(index)) - // { - // vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); - // virtualCircuitList[index].lastPingMillis = datagramTimer; - // changeState(RADIO_VC_WAIT_TX_DONE); - // } - // } - // } - // - // //The ACK2 is handled with the receive code - // //Check for a timeout waiting for the ACK2 - // else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) - // { - // if ((currentMillis - virtualCircuitList[index].lastPingMillis) - // >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) - // { - // //Retransmit the ACK1 - // if (xmitVcAck1(index)) - // { - // vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); - // virtualCircuitList[index].lastPingMillis = datagramTimer; - // changeState(RADIO_VC_WAIT_TX_DONE); - // } - // } - // } - // - // //Work on only one connection at a time - // break; - // } - // } - // } + //Work on only one connection at a time + break; + } + } + } +*/ - //---------- - //Lowest Priority: Check for data to send - //---------- - else if (vcSerialMessageReceived()) - { - if (receiveInProcess() == false) + //---------- + //Lowest Priority: Check for data to send + //---------- + else if (vcSerialMessageReceived()) { //No need to add the VC header since the header is in the radioTxBuffer //Get the VC header From 4a571ea9dbd51df0df1cf8e5b22d9c4ab3093f02 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 14 Dec 2022 11:10:54 -1000 Subject: [PATCH 323/594] Display link failures with the ATI10 command --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 78bb0bba..7d7aaec6 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -318,6 +318,8 @@ bool commandAT(const char * commandString) systemPrintln(insufficientSpace); systemPrint(" Net ID Mismatch: "); systemPrintln(netIdMismatch); + systemPrint(" Link failures: "); + systemPrintln(linkFailures); outputSerialData(true); petWDT(); From 0ee1c910a16a28c17f8f6175ee2a6bad7a692de0 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 16 Dec 2022 10:03:51 -0700 Subject: [PATCH 324/594] Add Airspeed calc readme and links to docs. --- Documents/LoRaSerial Airspeed Spreadsheet.jpg | Bin 0 -> 604577 bytes Documents/Readme.md | 13 +++++++++++++ README.md | 7 ++++--- 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 Documents/LoRaSerial Airspeed Spreadsheet.jpg create mode 100644 Documents/Readme.md diff --git a/Documents/LoRaSerial Airspeed Spreadsheet.jpg b/Documents/LoRaSerial Airspeed Spreadsheet.jpg new file mode 100644 index 0000000000000000000000000000000000000000..94e1c99214cef77bb897000d083d4ba87177dc68 GIT binary patch literal 604577 zcmdSB30zY9x;KuQSz3|vl+t9HV@_#iu$weBCB-RC*=Cw(W`iavLOIWD%b_e~mm-oH znp2uW3hGWXb51K2sLUy$l2!@$U!HUJIcK-;z308}d(ZuR{?tO&Vy*QX*7N&5)AwQd z0`fEX53d=wkGI3@)Wx`UGRi8dYUnOp3!wAyQZ=2Avj!#HTN=`{lyM7}lH!uI@t%7^SCFD{{ z8TJ0dn%cVhhDVJ}ZA?~sN9S)(yZQ$PhlWR9j*hW8+_&%EPkor4;eY!4Wl``I`1XBe zToMq;zfakJI4(JGT&q^EmRv2pGA@Z#QQ#&iw_0ke$r||sF4F!N6tte2DkAD*Nf1OXBAx5Tqx zykA0o4vDt98T?)D`k}jiHw(7MaJI_tIK(<(uaxlWcFY=iU76czB`-hOI4(mDgh7`f z>t>fBV$27~pN{cpRhGOM-L<^VVuYf9Hpic^z_chz=SD@~`~v!rj6+jiLINKUqw zQ)5Gw|F)~t>Vls(5Vx}!UzA@gLwH4uWyt%YANQ|9|Hn_LSv<{uX6-@QlX!`}-_9o; z*?hxpU@=rP;7C7h(XBqac4Takr(i?9C`+t|TL-vsVVRzOJ8yH{mm%$zXoO zSbZ83a%;1IE|TCwx>tCzd8e{y@-vt>r9Af?B4|%k}%(R z)~YT^BT8}8GUO}}A)%ERqp%K{V%{p77cK{P2{NfMdfim6R~y*y;D_1xe;xkkRt^+6 zYW^g0|K?{Au@RkHUeFFZZffCu$kp1;F=)={(5!4ZnDSC7cBXIce^*NWk0f`I$bM<_ z6Ap1#HFCd}l{2wawK)G^OMzR;=uZLm+DJKD?O%*6cOPDFdztp;*M9?QRZsmz=U}eR zMDM-%;rn#ffxCVwd0S)HeR?)_tP@w%GWPjIC(2dnEe#bz{wzQLTj>7bUpF)hI~dPe z({)LiQhaYk2vg*CCdTNi*`-*Rm?fd*cFL8?n?87b?_s%lVpP??cTN7VqP=(rR%Fy- zx5fU=$IN5bKHd6)cFakxg}*6Pek;eIB&{v1rzL)!qLfoc|Nk}uN_>-ChDgOML*mlK z!vx+jM7%d)UX3>)FaUMqSy-Gm;v%d@RdsBedS5BQ_%`G3Yi?Y&1HJmMn3zX(60=2dbvYxBq4&SuBd9>^C#k8)9ex>mz8d`R#gb#*y zmm%!^gk?z2{vY=tKOXpT|G$4iz|48~^TeHlsDmv>ve1CtV!xj|d|UWl zn{2&#kBpCzVJ}^?B&y$H^TlyvFtNhOGDJAg_s88AYY-{F@*zBNQ_$d-D_hm!rGZ(7 zG6FTft$};9x919vY)L!VIM%*-*WR&-%k?%y`DMrppF_)#w&)d|To4WU^PxW<`5!!~ zGY-0m&~|`&jrxJ(U;M$=eq&0ol!T+%P`SVz9t!8e=2VaVs&_W%#t$O%-?Uc# zm5f}O8sQ+wTloezRZwmM&*z=xt3iwnUh;n>oKihzf!{c`kpOv9620z*J=Q;WF zk^jM2`t!~IeB?i9u9L5*wM7dya%`-KxMREB;Ir!XyM9c4mI1|d$3;d;iQG}oeO|?h zI{SP+zW86o%^xix)&B;L{s)HSztbZ84|Kf*vGf2(5_I0QD8=O)f+S30Wfq6+32fMl z6l@MQ6_@krm}6bRp+>a^|G1t1vqsSm`sc@agjwN-33AI2%tAa`qB)YkPjC*{%0`Tl zIQj7rpDU*)B#m&GGNI-W?gIIQ^lh(dUVgg9A-;hne7DbGL;up(UavaKek*+w{%he~ zd%85JJ4}`#t;pt3P?MYnH262T&l`E_6M~a`Xj^aA_ujer2RMr%p-VZ=aS%~!qrir) z#da;A28|yGP1q;CY;XyuZyU#aRX;my`gC=Hmf(&}=K7ytRpNgIvC7MkPo^^y0!iLi zaXwJgpbNQoRc7UFw-C7u**20wn3FCiOiQ(&J1%hqqIg`w>+&D3xN52U>#8)!U*10L zC$K%4>5L3CRBZVgYs~zF^W`l=l7v3I@}oeih$N4_KfvzHP#$O>-_An|?n&$r!yfT1 zuQeOS3a(gISgNCMN#_lmyfWVSlawOF*Mey?I|nSHF-%TN*06{@OhzA1cK9fcLt@gnhq0 zZSl88RHI;@Os5e7Nj?;uf>ZyP0vRT+Q<(0jwG64!y0Hu?QT`&)_}4l3;}!or1^Q6t z1fP9j8RFlound`TEI+>H-Cstc2=UNl2s4@*L|29Tmmy@a&aWG~{yN)|2#@}VN`k}S z7sIGy1B;ak-UAEfg#mU$6_tyc`X2pt8W@Ac4Q8l;h3(kzFH7n6rD?%|wpm{9g5P?0 zY)1}(JBSXv&Ul<=)Ft-BdTh+GtP^U* z19P=b)*WL?K|yk_bb|tVAkl$1uv1cI{P479sb$FOSy5wi_++>OHog^a#v{(%=6_}3 z75PyKi-nF%x(=^1B_cctuFdGTSb&Cp-~Y_g+o&d#uv)>#*T-oy&q?N~V%kilpL47S zd2_mTrELk-ot(gOFipRZ6R2)>A?I{m#=S+SkX!TT-{`pm>09pl<%GIaX0~;l&v7f( z?P{^=GRY&LgP0z_3r>*U!m26E*kH(sT#dQyAA5GX5 z1m12GQ4o`4_zr$@8)?jJaw;At`v7Oe$3F(A!IJ+~s2JWhq0(%|cM!wmv63u5ecnih z&Axj8jLF0JDGYq9T&#q-*4GbK;#|V-pXi->)v>j|^0fP%m}Xzu-ZUOoMMO&O4(7tk zbsu;SPK)f)YGm+Z@Q>RHqc8pzoA!O+`=Ea+#pTlgv!CMG$@_Hf@#L|f;Jm<;XZnGk zI&58BnZr~Owe?nDc;F6lVr6p0Bhsrzf48a!Ue-AQJvK+|*}w##lixwgz*~qO(`A4| z9cX3zPHaP4G)=gvSYQD>zK=z-#`p1^Ti92lwp?s(p<64}OgK^E&6j@kc}yenqtkz+F7f=nB$OsVvdb6Ie$3&{=aaV;Tl zt6GtMEfttxGan%?X{y7XdGY(QmGFvg5E6g@F*6_bJrkR>vaZ%6z-9DX9S61~F*EV~ zzy+V26DdGMAQQePe?oKYi zK=y;ZVKyv7vhzW-tAZsnzA#vezhj}I#E-q#zxY?Imih6T-+^ict=4E&E<<2qRq%%I zI>p6)TYrZ`eJBfrazkvg_(^nw_?MIaK&lnseFaW?cRArDh-cr?oan#f?rtHt9Jh@m zGv=gAmLbzpEq_xCYhWn^Zrg;ISp>E;m~a1kE6hhL!?9l(P72`*gQ@iQ;Xpk82-Cmg z;g44+{tb2i3cYv`j)}6wrHxq9GQ`!_`yb$2HNOmb8#n}ZjJ+T-=I@7``%*N_R-dLfQvE+#^^Xi-# z*BhCQIU%;kZ)6VUTad||q3>oiokJi+A zSxh(YZ!DM@9)A}dz z^*W4=3PoC8m_bJEo%6u6{LNI(-u-t%P~|DVnNGWv<5hA0fb zTd}TIAPO`dwi|)=#?J`Q?wKYe{WC_bs`|k!`-hN&t%a{#39j~^5m3T3A$9h5c+i!E zf(sDr9*CtcFHWrV59;x^_Wl!-?!N#-ybidcLs_7~G{pE4$N3!$|5IxW9F+ge1Rr$s z!Bu;bFk`_{fBh@8qZcqJs^NUFL4bOe&qE}Mci>I}VfS#SLHQ-&dk|~!+HY{GKr8>y zWK^2ao!8qgRsle4TxT3_gp1)3+MBc<2i_aEr%Zox(tJQZHy)Otpl?vsG5cO!x&tkb ze}ZmMnUCSgBT`_ha1;D9x(ayXC?H($nU|b^2u6%4h!uP0--WYTVuFw%!qQG9%(Cp4b zO{%mJR|%s%qZeJ!ERq2UU`;2V>B z(mCC4Tu1D972oqpzHahf?A%zK-hCch6k^t6mUbl!aoJH$o#39qM^O_=h7Q;aqwQlc z#|$wpR7Yv&mb1(yOSyQKKU(toce2CyoSuL1^NU{_se|RNrRYWP$ae4>95T!*g#(cJ%xBrm5Xsf|F%WjMgzM zm(CJeooqn$zM3c}ROcbvHpFlz;^`_l{RV)|UxQ9rhSZ05Ff!4#m5i%GC$0!-KU9|q zWkwU@=%(0@Uzye?`L9!P?)(@ZAb>YMK6~Otv+-SEc;+LfA|eadRLiwfNIoA)_2}Wd zg*f+7y|dJ_P}iQ3_0E+x=gbVV97@PO&}1fiO7CT}EzG#pA;OZ?PfhP|aeE!$t%l!7 zwlaR*PfelT3@`tZMpCP5%ni|B>m zq5$yu%ILH+cus{HNWh+p7(*fB+09_yvL7$YvrC>=m^>3lqpQ@>p6mn1$TrJbLO0xY zAE^|&RGx@^=s3S-ug_Ca0MNd4NQe{_E<;ZBW|Ba3=s+aU*Ti#RPnIF^3}w1eC}}Nz zGtPemcV<%L4NLY$q%_+V2djs-FUaFmdGj13H8|S2P*9#~qor`HdGJg_(tZyOuLWbv zXB5$`UpSu~>ZzUv#m;hM4e`>;cUYT1`@xEl$Gc)f3gMB?&UHwPw+s2>K+L-w-6^#P zE?3FuQKWUAq}X>EvL;JUuYjui`C{W|-A@CQy)m}cW>gfk`=Ax+xeo&{bR$U%EolHl z@I-X;EnxEN45{i6q4gkLhSo;Qc!9e-4BsX+?Kcu4D8K+aD%~)Wl%yqF16<)ZI5hJY zm^NeHGg{kuZwBFp1#Fy)Vsr#OiiC|3%f6cw*$HArkMKJ7Jnp143z@nMsU|!@CM`p1 z!zSVwfs9uuCaIRxI+3Ea7Pt*BFB!$6#p;qr0E-gKII&(ZeJ4*)6UcE==CQOg2`13G^!ozS0=$6(sj`w#~0s@{W6}ty1B)*dqVrrx#T5UUh1Ypm8Ld6M)({ zsOX`h=BjkxR03u!}pEr_lp$(N%wx^E&ke-f*V&jj+vp))4%@G$$q*My?BLYsgX?0DqjB}Yn#_G z=O`a&eqTBAc2Oa&t;I%3CR=bZ(>ZvUh02M&Q@oEBN%dN+QkwR^;KqM>2QkdMbt=!O z4(@J@IkaxwsZeH~vq`>%I_l)Z95=d(cH_5XUVxxe^ef{O4A|KzRB~j~Sg_QhU83S; zNXP`Bt&&1F4))UoZt-vU4&knFp{WEYE|y)6%s|(`SPQAq_2lT0f-f#upU&fBGfDRJ z1?EzBWVY+phDD^{k|>qKQ|fFbW?m2v!|EH-={t;*Y^(Vf(Hi(cEjU2t-R8c(%MFts zaT&>K;puZC#(Lx6iqq>jCs3|^Bkb^GF|519E?nydrB^!nT~oi&{#~a6GCGEOs4iYQ zX-|0KJyzI`J7q7fl&UDDJ=^_|tlO1uXpf>KQaoUE=E2Mpfs4F42L6g(Le-|{J7%pv*R9O8WP(mwn%KV4BhFekYWn|6ujvh)eJ88JT+aWCYBZXgTqy0_IW}q3+2)@gr=Yo=7S$6Y{F|`JKNDvTnuPujOKIj47RZb0ApD* z5`<`WSQPTbnF6LaZz?rH<8e%nf=ANN>Kf@M?iK9O$Q~T&0>eLWOK~r(hA~H3IwT%u ztTRUgx(pUt4JR$Sv4~FB+QXyG7jp@KzGX5_fqTnl@8siNjn^0kvn>FX&8&XmMC@u48y)6KWQ=C(S;`PX7#34#$C*|v4*6qd#k zTRaBreid5s&ahV3#y^3rWt@g8;|}pD9oocp@AOa)xEv6}2~VcW_X)MH(13DIQ8q@q z!tC8Fn&(Z^z}^z>Qkna_r1?r{M^FN7JiUl;K<^HeZz;HTgDpi)zIKMMIew#3s7a^u zrdpwDM^uRUPIYF%UypTDX7Ud@`qcN$``>WYdJyLHE)pBJ;eIXlBH3$*YW$|7&c?`> zrTRMXqz&5qewjPF`Lc%0Tp35(HABy%ja&A9FCN`}J&LvXH=YXRH>#VaUU`;Z*aw1%v_dPC$i?WFoWHfsR}Ek{1vl zU;<%?tMnbOXPV4_7#4a{^M2k^+dSJiqezo)MARmw+E|bIz9}s6et+W%Wwxorvfq&A z(>=>n^D!#dt%#JtQuNNhFB>|$F@U9pH?`@~LwFV&X?8fBI?e4hu-%ydFr(al;4LkM zlGJ*<)9t#OiG16q?pwBcs?Xr|-U%8c19Dp7v9BAPt)eebRgkhbJq5zr;7+;k>_0o7 z8+!g0K8956_Yosj!ZQBdM3HUtp_Pc&8W^aG-+J?*qN|JHqAicWa7^08!wQ>@ZrinX z`L|J%f{0LuqHSboaM`K-IT((=Yc2<0f$y8(ok$NK zofWk8F@mP)t3PO2@$7`g#L=%{Scj4+)Zy2ITrP&9a07=iX5^&Y-AdodC$upVL95Be ziA_lGlMC}?dF&A=6BOw?ahCl2)PNUtD;6Vg$a^4S)~5&64Tpm32c(3B?1xP*2o)u{ z;qx1KGy%pM{e%GM>SQiMP7~wReUs-cRoZ&j|5PN$S4kFbh#_MK{D4FrUi08G#HqSY z(mU-lQyIUPzmOS;mO}@F?rcEt@OL;7p{z{yeq{aVd+il}*TU!s%=pFpVJ))+c6?>G zZQ)6$kjN_G5!h=s?@@-WKG1k%^H|~8dhEN@Oat@N_Xh>=ZiQ=!4!N7Rsz-$W{3u-| zO;FTNwl>#x*bA#oJZrmC5z~S1G|566S9o+ULk=S*@4y;-@DmIvVJv^U3*dAgr_7IP zqY+6UN}Qo-0kGpO{B}`=mI2^%-`nS8NPz)Kk*)y9wqGs?8_&OwL0ROd*~qbdqnw*! zD&flzyCg@nb@&U11y*08n*STUqTwuKXkt4mzmaezo$1n!aur1Cquys)_hqycdgpw* zA&Ato%R^=r+B=l)$zeZ5Nu+o9YkwTfoQi87ZgW0ahv_d@+U1f{tX?|}ibS#NoglK( zpJN-1nFs$8x~=+%cpKOcrl~kEkuk6Rp8_IafboxuU?svR$zA|8;Y}O*cf!*NB^e2$lE@^mV&XF zvEZq=V2`6Ep6(&SPEigWj#~q0E-3)Lf=*swzEF|KY3MMHBUPOMJtrMo(F6KcLaR?L zIsqXc8K%O{%jSkF`T!cfqK12e@Xda+*7>RC$;t%{2@!9+-}fR1cQ*BNW3@1O=fK24 z@u3VeDaTpW44QOYxw8fAqx;_DoOPUgWDis30L|MfS|;NTzHV24dK=Y^LJ4T=)<~Z` z*)S&Zku40kWUwEDiYwYW^u2R^Uu*Aml9661Y$M-`>hUgA*4d(j?@E!$&VDT(0O^om zc8-+Tk{zBtt_F|^&{P`KbK1J; zuO8oriBun-5D&U;!;rz$=Xvg?X^)3`&#nFqnTfBw(df1*)iI@z*q}S1r*zvL;4|Oz zR9I&fOYXhXUYZ%*qoT?cgrJz^&%L}Okx-M)*nq@~8G-6(cUnquJj=l)zj-(?jaO`v zx!2y-G=J#BnU}BEg9#<$T^{X@d3Q{5%SbPBmEIOz`;*A!0j;qyr8sS%n2KqL^2Qfa zgx37J#Y9mit3JQpvc5=taH_7krs>+SV`$k_mSxuAz|*HaJl5;~#*wMheY^Kfh=sO8 zoq^U)mmHsrV%@&m8O7QafhrZJ>yCYSh&}iqJlC~c%C+#A)7mc6lYKdXu6iyxewh}< z$V=ax!6y-l?d6_nhpq;(2nOFOevStbKdu0~4B;ZuB0#?exGk31$_*d8#G2r_Ko#)6 z73l*os!(T$$O;Q#vJmy!!ifzyd+u#wNytuvi*+%vWG(Dda6V4cLSgW20OdF)_$XY95L;Hl4tKW>1D1n#rVjS&t@Klj!X)+5>i*3}2+oi5jF)#E<^Y?vyl&&mQWWn1+-~sdUC> zD62qI%6*_cU(}M6GuPvNuO^1Fwbij{xr`|1i|?rIURNAUSsqOGG&=V?y)o9g)Xx0b8+WLEmEIYlZk2lO zA-4E_y!Np})WRjHbuL*7hMDTW)N<8Moo{w*zZDrW{=8(@plkW&4Z5G*Db5YmWZOco z_Wt8pE7^IN7#vO2I|i8H(yHv@?)#mSp#Ni_{jY)fAHDQ{3YD||kQMtqPW~qi^x>aV zhd)?gJ~{zda4 z&i*LxI$uPaNjFW)n~^UVXFddi~I;@jQo>a>^z@ulK+tlT(~ecA4{E zQr+LTb#L%+D73yv)!o%Wx%m{7GCx3ph9wBf2jIH+7lgW~4x$|XC0d3qgVP15VCp0l zOijadQoocVMqja_CsN@XfEu>o#^l6RZ$vUqn^UMf>o(wOHFEIwk;mcdz0o1+`-f?f zl_B2sFfgUC&90S1ETD84dUhh}!sp57z(7=fB77B?VydkpCgD&^`+}{}og_8!GXZK0 zw;6cM-2jJ z-pS^Aie7c)A}L(AGrypg@5aNX^+$W=Nkg4$LylKw6=%GqfESqi>!_Z>*k%>YQ3ohU zNr(LEoG?8zGSZl$9!YhuU|c!TRYIQgg^j)}{&X*G%QKE{|L%o`n7QMRZ=bsV(P~OY z+D5`_MW+8L)zVnwDS$e@T(9hvUl5Xrvjk1y6+SHuZUqu)*BK&05OLzwfX32cENqgLL01jN%L6nvF|lFX za0sqGGuGajMTcV3y?KI`;u$}qq61p-6&P#hO0C@RK#rlY*TDP1ip7mcYX8NO*Cj&+ zVP~U^vXt~Zx~JdO=?=vc4NuxseJM5$a5Z%;d$((^U7p-*aYmucjY{?Nxt?HTo4acl z;>_a7wiCJPFTPgJ=WRPlD|EO`{3^*}2K1Q)lE$Lp>7o|SGDHPu!(Wrgp5ML{ z!17KcC*=z=L7UFF*3Z{xjQRfCjh@XR&r9+Ay-9YrCv{yj?sLjU1Q-0~XB;JxpLE#t z=j#!7ZX~kAd);!#U0l^!=0#>;qxe7;%ROiJx7cI!LVJszZaZmL;YSmqIi-7-!^!He z6^%dF6BKY8K^1gBxOoTRL}_643Iun8Kavn>C_QQ1cHu5Y zox~y}!PYh(;MFkIZOQxJO=TI>KBd{XKL{PyqFekdR$OiL%AG6f`MvOX^qR;0W>*v?DzxTTT z=((TQE4d6>0k_(M+EvijR=?E-t^!@e83g0LLSv)WU1LA9HO)9bTI%a4t(SARi;5~- zujQHey&}Nb*`(Ncuk%Km!4;=2a)0Tz3KOeDpD!f)fuH+oE6wccFsJMLYnKf7yqP+U z8JE=vp7*~}U^V28+U)q~{{4?OdRcmfe`J_F2pJ0WtMmTsQHXJHe&vU}^>UVZJV^z5 zXoFJ&61sMPXxZMqwyb=tOW^$>vkPDf9*N@d68P8FopU0cc}?;Ju=IpbGsOaFL&GE< z%DbB9nF3!gbm9AgS)~J$nY-|&fLXhE;~;}=+80r7tPs*r5PQdR&!+Tm@}aF))`y0l z93VSaRDCL!@~{dS)GvE_=^%GwSdF{KDiT|OPa0~Y!0Uc8Q3J~oTHFgM+SL-@UO;0)Ww0B2dWrA*g@K$6J3lT)O4T(D z7_lyS)v-b&ti$68RXBz;NL8Fd-4Jw%8sOSOOTZ$u+zYo2SO+SmH2SI<06i&!f9#2f zm3HRuY2bCTjgy4oOFv=8U`g@QW5-~M&Hmi7`iCI}aWh(58tR4(1qpA-M~3e39di8D zVjOZ|by%E-nNnQt3EK-f#no&tR9jv@>9zm|lWO8_XLAZKVrl+@k3@mh)gH5V& z!2EgaW-a}fl%8&b)271xAw#hiESMffxA!{Rs39; zh(MIwd>zZ#tHN`5reU5jrqXVNK9kbn*%@P@qNCc8wOuJKS4BxZ^OT-#?d-)q!R7KF z36&lRTKkt!p!>5<^Z*Q47-zz2R9J5rIDnI2O~tte^} zFEI1J3zDfP#zLOT-dLaS9s5>~Io0qBu67|EB;7Q`(JfX`Q4SG^R-7r?H6Yd!9s|rc zuxm|%8urqgog*EWi{$yn@gH$Yv1QZhS;hB!y;?ci{SQj6tCbEsFwC3O-+w3jy+UV7 zt1F9LlVNCYk9z*0uh=K=bzkOMZh(q2Kg2Xt)93ar`9b$9ukK;6$jQ!jq%GU}&Zfd_vQM)3*abx2+)r=|BvrXvy5*J$!uPT1OXE7%%)cQTY+$ei z;gK_YD3Lo0%SWqfmNdhYCV2M7qa*CZ2g#xLfCX++sW}wTaZeU1?0g<}iG_x~FF;={ zI7hudQ%~1x6h8w3Lx}vdj^#+TK_!%3Fq>!%y+yaTHmSXvMJaNnezZg6`Rl!Y+_pCt zzBMx>;$&kZW`NqRM&FhZIKMg6gVL*)gSl^f+c00?A+6iZ>vPUS+DX`&=GEyF&Yb#W z-Hz<)g5yTOBTvQ1&+!HXsxl5BYz)9CBa4#1cEs* zoE~?oA>Ibf36n54R>ePWXrOOsX&Gwvyue1TosLcux@I>+$;N`x8wBzRrXclgeLwrr zk`1UEj*6eb9!7S~4Msj)M2uY$P(&rhb;u3a?XA}4j9LT{r$z<6Szb7ER`xLz`hmWY z2WvwniJ`onBI>sBXWuC)55D;HDu-rjHqH8NVI)0&u1|1Oy6Mr*slM4%MTDD{>Q&s! zX$(Lq6-=F|Ey=fRnVUT`ny7A4Hsx&Yr&V&-AV@vS$mU~)=4MSp&%t7cP1i5y*HGfS z&pY3#B;U8&^<;=@=W?%5Cun2z;`r~5oTu1#YC3(f@D<^bY{5ebtr~DSoIViB zOP*ZASaQY>lQal{q&O`^A6*Af5%>YsF-xXnLj7EwL9tn_44|QbBTFV&WXHHw z{}*Dar%UMN@6I&NY>)cPXXiSi$H1b#enj1UrW5-@4nJGOC8K2h%-;zS6+eeM%Eea< zUJ2ZFywVsmN8aT>>odv;9=`X^+1=akl|*XyYq#y`O}U3@sL|({XZMGgpx%?8rWE@{ z%-9b^-u>7O^~gN>Sy`;T(rrnh1JJ@HrkUZQME7x9*$XL%x-)IYuRw;9q)>y4NZi9% z8w8@^22yfgFnUw{EJJD$s_5s929rSsVUdPwKC{01X@ZVN&yb+7WV)%2PAtQC;;w+= z6$@38apBQjYphcnoKu_%rI6Ngwa=ths5fQ_H~5R37NNi=o3wc zu*=4q+QPSvL?AVLxJYP^7Ukj?KEL#}{gjbf;NH_;J{4fq`{SOr5FfsON9|c8%t<8* zn)x{)u^u*NHwMU*79`YaSkP7H7skJNE5OfqY#;-}4X_$a2=6uyv$MXNo=e_!$x&_N zmY$XdOLI)KgV#~^UE2(d$Zkm$-;S3Qr8L9%j%|KYKr%mugJY<{_h6ZV1pY#r*nE~Q z$A_j{tfc8TePL`E13r5>PDo>62kPSn+=hkw_f%WZM%V9Y5N~OTckFaNqH6!Rcwc?VdDlhIf%s#f(GU z*~=i5m+yVtf1>T`%eVbs#c@Gd4@SlnDTzwNtAzogBH^Vn zY`g>SOvgeJeh=WzX+gF{c}Egczbr$b*w4(d6(8;;gwFm zvFdONvS@59Hh+z|SoW!REP~B!szS=iRw-nBzC;p|g75l~x#2sevJbTh0Y zH$NWd$Tvgr&%`4F(21}rK|Te0`vk9UA^8k|3eJX$d)$|_PEojAOnFV?NMVH(w4z(rU!g^g9-`nT!JD7y%q4E`ilnk}JY~6Ux z)ft$<%y2!dx;*GNWxuB+6wd`X+dOxrAfe*&CG`tA-^g>*oe3A4F1&xAUqj6}IYK?? zMR@e#fkM@vh|Uv=Ky-KexGEo(XtC?uuLI42{%%Z(YUb})v@u=>sHdNEjfuE=lr?8|gevV#)2m+=0)Cxi`E^op5siSpj0SJ)1aoNG$)2u7j1d z_cfcWB&IaGup8^3iZ5TE@WZCFyhlT%X%EY4vu`B)h%<&pf;lNFMwh5)kT29U2!M6rgtukZJauxiQCQwGx})GL^5c1 zZUCN^&LGxC5;NiJf!l1N22kEYg$%A`0ZFaB7LIPGhVC*yRvz}VyO+>8K&GFP#tPV; zmDw1mZk3qw^n$#=CFgXbp4)F1#lh}^^gN5kD}mmwd)l7{G;&kpsLu5oa`9A+?z~c% z9MXzneeo+z%WGcpR^Ye-oW7 zR75z92m`pVRiRot_;#J5@IynjL^&qc&;gp{Fldja=%Zpu#-&grXDyXn<#+yj^8QCUEg5XHQnBIF^4Sf&1JvaW#8pmsW))(U5-EU zx)njt^yqutR7(3u;?cpJ#@Meu<~=oOxjTwgJBz$3xQZ=9s(L2HN>aMdBX@rxffL9_ zh>{JLA&cs~#dB_&TmQ=-(vJ)9_xO?2e?WHrpCx@{vy?(ye!J8(7~+r{;$?HJShqpV z)}`1|&DPPY%h>B_$gSzqyu_0edSnk7vh~dZIq-3UwL~RgK~al(sBO{Izn3+F;39=e z3tZ1u*foL#UJpwVH~}W%T=dvJ_SaPS9-I|$7;zS<1duu!2y4H<$dpg1Sq*EVlN0?lF>qJ%^B8}6t}A`tem4#^sAD|b#_GCH7c zl&E(htar>?>9;`zYfK z>E*`>&OU%9UWY0sw7N|aQW>@DNox%aY-ABBAHNiaPtGrXO@@Q@h`P*P=B!H!Wu=YYr&O$m^ z&2Bp2`r=Drz!N;2=k)ce&@7xc#a;CVThm4i#Q0ZYiw`x$dtPoF`SPtYD6SZmOv~q( z6DD8TtnnR{_(Gu80&;W?51at)RKsH}t zl2wnc5tI0qPZ%J&#zo7=l6zyF`|%|%}34} z+7G;U$tmlq*P>P3=fcA5Mu*=KYu^#G6FX4t*8GH%I>`}N?&?Upl7@Fb!UbAK=I$NL zv^l&RxNp+#lIf6^w0*44U9Y2niuetw<_i=Hz~n|82^T85-W`H5$md+XLpSB0X(wEzTk$5+EkFiHXd45%b6_q)!#8N+ zNYhX=GKx@fVn)ULa@H83msA5Lzg0MPbKP7bO#F z2~P$AH^2bO*AB#0t@QwH$$EyMR=2lyUn$TM%}~JkP=xR%{vL8{NreeyV7{XYp^n|b);-U(7qYnVjRR>Tv)6zetO;_wxoDom(2q<`#LC<9nZmdREqoj z3eO&~aw7(vZn@rLvdQlwN>84mx>U*Lwmr(=D%m>TvaLI7YYCm#9n1;z&e7g%s1*`L zK~ee1LKVAKyavc)HtW0ry3;0;DxwNRFl;QH&6w-v5XKG(s=;vhnTgS_tclfxzzMk! zy4DaOI6RG^Sx1Wh{woj=#=%6)D)Si05pN{hJ*{tJ%k5uLWv@JVP!Ua%0^>G;7SOCQ zq)?#>{|r$>Ny2M`6=pKhWc<)&fwI_AbT14f*`pKSmZIy1bMwuF;#og9&$A7wMrezf zd_|MDqj^s^36e{Xjdf3dM5CUT56`yz#xpROD`|b8RjqB}+vvi<0=gMdQ)% z_xCVK>wfBdadO*GxgAt*NFf(1e9Vgy%jhf}#y@Ak2;n_w4(-}Uv8lMngTeDeCZiv| zrkji)Fi<65EO*ijcadi~_id6U1)Q1eZH*0QC*7q0>Fl6FE=i>|_WofT6kGBfUAM7$ zAR^OYcHU*=h4&eu3UM-svh*`_^ciMi%~>3nW~g^snA1@b7cZf(djAQY7p*S(#OV6l9J}} zn}J?VXD1`>yKx5%A`BaZ8^6JNc1%EUa-482Ag-J91uqF0Bbcz&p_YDHJbX2FTwwri z0;F^7lIJ30H*!0)qSl(AdO@1#-Ld^4<{%!Oq@prIuD6Zc zbtpZNM!p4Ao=PafB)JV?DvZupTR-Y@+YR8{)o-TIPoOBNZJgY7$PId35!5b1i}*#` za5q=9E#!wfpo*XLf#yE=R*&Iz6?REE%s!tcc@2!3wnjevo= z$$MZ}Gqc%pazYy458qt>qFI4&`Bn&{g2P&35D06*lMVs@-NC>IY{AGgTJTa!|c{axlC% zA-Vf2c7J}htB>UO7)wp*P5>Dy54MlC7t-J%}~% z`LD}n2;u(0+`SZA>}F?`-Q+pGRwvZJofCkPa;UU9e2)*{f}oPt_%ELIm7Dn-9RXSW zyttKyl8rR3JkIi;arU~h_aNe4?vKT*dbWSB&-|}yN*c zAta12igW=5k_;+M5|!`_DRXZ3I=^%FyZ1Whth3hdU*d8-g!{Sft9-wot5gD~qoz;F zc2J~ugt?-Z0*4B*w`{^;KNKn)U?CO$*>nk57`Vt=c0P0QiueJHhF##}s8s74cr8iV za55(AI?ip5q=-Q!aVwC{ot@9xirofsZ(qAE$^}ih?!ZnnL5WO$GZ@Ei=@bn1tnPB1 zRj@+|klfQRCO`vY`iQ@PyrR#i?v%I+F5Rq)CaW`4qTn>bEy@Ux8`t*dY7U=j;0`^_pE%5_aXuzx&e%=?(lYx7ys3g!p(DeR(ls0?nO1cJ_@Qy#*;o z9IyFDOqzv+tbE(Y_?g%dcO}kLPye2hmw)Q9OUHLt`>A;xyV%Y_Z#)(*AmdPo#sG`b zr#=U{3FW`KDSMT4t$J@%{%Y#0SAmLz(G%VUnZ>;Gfk8QhI`R|im8%*nY_SD|65Wi? zD#6I73Y&|BBUhp z7xv}i5R@o~ZBT;vUkj;7md!aKBE!;cp{!@z*0iK!x4Xf&_w@NsQ}!r6L85wuM_nEi z`iXL-4;Wizud4Y>U=0dIy!*yQu2}O2wQ$bBwHR9?`IpeI=e5@euQ|V#OICE_4W{c^ zkIN29;<7xTQzK_xc&Ob#8c;im*gr1ud_3!TX{wKmvY?c3jnA+LN)VYx)K+3`IL)ax zK^^;KBPDGWs2QL7gMG^INIOT(FcWWzse1&>@)N3op`x72_S(p^8SV4yIVUaB@H|t6 zR=Xcrl=z43Q<<*(;Y3zH<BgS2f$gZQxBtw=m3PZ(*NUtGCHp?W}po z8!`pye}DuKGo8y}(&Ta(M`A)$M&lS;KwQ8Z#3r2NBGP4g&@tW)V2D%ia}yX;uv^N^ zJ8Bk63;ou6_`$>GrWfPZs5_)YaCu0$qUlDX43p(ydjx?OCp2gnl)ziy6C*0b#p4&8- z^|5rRL)jp`zWkiff|z8Qf9$bPwfbP)NIu5Tp5laV4Dj}j;u&B5;!qJYOls@)?Ttui zHbS$*OwM5st<~oEyN=d@Ht(Tp7o)rzwVcrP`L%`K_3lFy*qEB@p*Mp3JezN0>@>_F z=?8ctSN5D+%oJSyQdKKCB5jdG@~D&AzdjSnEsuzStxVb=F$fn7h;9mJiFhsCIRSy_ zV1^Q8`Msr~^6=KudlCam>~Qo3uqV$9vf)Z`GN6eP%I9`J!VWK7lnjv0K_DJ(EtWoX z*aBipdvF#J93#R{AI{=gGg}}-;U^wjlqJS2Ue$mNE-wk+ECpq;N(XPzUZX_*wCk(+ z#hzG#cbaqe`e5zn!|i0tD>8@Y$OvnaIyc=vb=XHwo)%%`Q-r+0H?=x_K4D07Bvx4~ zbRpt-di_YVN9H~sN-Z5riHNTERV-kn*BCJ2RF2wr#EXr z?Zo34-$8-p5=tphIm15{`{xBYwfiHPyDE>DK+na|vLC+&a;}OjI1>-U89{_5*$`PC z4>Fx&0CIwwG8WHah|DM?n8gSNGrnu5`H6~!a=m8ZTvTSW-A{Z4T{OR#{s_A6cx$Fl zeB7~hT!V%8ZREYSZ%+Or9!pRqbo<#I7C4KFq%V*c%`ogAl8?Qz{tdW91tZ(hC!Co3 z)DsW6!%qv!XAVdo124pdA-lu%(Npo+bSCg$=q$qK$6M#z2)kpK8`|4p0qM8c_YPP-Kseas&HJ;~MP@7dJ^y0(LV#W{eN$)BT&+db*jSGvhow=aYd)f3)vyo`$AV zyW#f%*^UM9HSHB3SEd{BZymOb)I!SEy@biWUcJpvg?X#XePXvL44wIstPI8SvNApj z;kP>7=P-Gx$eVVE1C7Wj8d+^2pq-q?{cx1;J}NOufLYBq6|Z~Y@BF*`Xzj42o}RZ# z-l^*`HmT!{?!%>`(de=H`AdGpp!cDk4gqgH8{t*oX=xKhpwtUjhXyzpO^06y-Ix^8WIQ{yrKk}* zi>i1mMD(tNp>8;s+?FZ#(la^Q*AO7*p_zrpIjE1DdH0FHs{+2~i%6O(QsDO0VH12i z95S-L$&88D{Mo@o7b{UmwuO|$*C}jrW?e@>{yM>s)o<;zj^1dbGMCZ12kR=Lv_JQDVb;C91L~)d5y!njB+l+3iN#vAuRDKXF`( zqWuzlS^YKY#p*N$6=d0yF<$j+1J^IYkLuc7x9JZVEGB=nmZ&(3NYbj#YdjS6%SAmK z=$nwtxr_^k48e1s7{i;hcub(}5~^ah4_A%zq(6wX z9RCkWHZdIDrYQ|nGWVxGeNHhtn^tc<`akrf6QyULpG}>)8_iSH@Ov9SKW4U^(gkd8 zE}i7x#@DDjmw5bJRSpyzpt^m_g{~nS8H*ESJ$ho z4))vg5B6IGxL6X75uUDQb|MA2qCdb1>4aG9Sq&h8gG(HwH|$Od5N!BX23+C_KD&n> zE6uuTX{Hn;Et-Q$_<7pBt71p|Si4MKcmUKZIf2ueoe=4I!lQ+_eXq?vlEW|R!R;Z? zoZouCQ(6U}07%H_(mft2ow?gV0csP)NsB>CENKLnBnB7otcu<7B;c%&1Wa_gKUxTw zc*&fK#BZ)kG&nfjfVHfvVHz6-UQ zFY2Be6&2KaF^;lKG?&kcn=_UC^OSK;6 zdZ`!D=FoWF&i#A6eCD3Ka}m3M&V9v2d)wjMONSFhF^{ABt35?U)qcg~wmjP+SC(L1 zf(;_PO9E}Xje^CoKa%11FdelLwVXlGPGNi>ay!&F7bH78YNstW$0GTw%d63kB`3OQ z6NDHgIF`iss1RtQAtmnME`OZWXbVVOuETY)6Wl#!A38+eJI+W>-*O!02D;eI7R!DC zX^uCTCSIi8uGEKu#mBLcz@SOR8%Y47<5uM7|GC6{fIVB4!=w}^5QdR z^j-|(4%f^=N$g5_NHMYn%HapoM=!43f72LhP)Q2c51g^+@2a`*#7EytE6wgg=ne0m+VF|bbOT%q89HD=0Q`$aN~QmFzmg- zql-ci*JtFVC4>vpWr`=Ro5djVnY759WNP}X(pntijEmjp6h-52z4w+va1k|`$XzZr ze^-CN+|yH#(m}KG9k@5|f^&Zi7!U_@Lkyr*aR{ON2p$nb-i|X9*7AMn*Z81Kk0od*rpxkM?D&TBZJBxFV&0VtH~qL7^PRZ{oo2coKbH$fc*d?xbu2B9 zN?2YB=y0|2FJqe?Ss9|HjE3kz2vuP1aTq9`OtZ$68WTO94L@uU;Gq^H-F_=ghmcfz*t2)VQ9=P$k5lwxjrIZM{@ctA%-*^&&K~U zBtbMpV^qI`E+uU}UuKQ7l0LN3CAf|B{xHR{P(&&;yq<#S7xk%AGvh{U(SISOubbqGr4w58)KmpET%#6iyl?w?q|!};nl zxLjs}U53)xp%@LpjHixH)ZC%bgp~T>!Uk{Atk0icH}31`9iXSuO~NQSoUkX)?Ha<2 zy?etBVRH&&#_I!u#%z2X^|U!3$6_4AOKXjb^oHDSjm$rltMpIT|}zU(2-t7?wkTR;4jsJcnPMPL2O%lZ4o_Aak+bTMkFbRv@w zJ~&m~Td6&^hEjrZ!Q*ZvM6JaCIze+2cNqo@Syh;@7`BWNt2lk{C&kCUfAE+QZ9mZQ zEc3TwtbO;Wc3-b`=?bIBu$Os9 z?`H@%tmtL+Y-E{Fn~u|E{VMl8K{zN=aH+@l841wc0TbQs2)zx%( zkd7LlDc@_^RLB^-j2R%;hVLMBA1840Q0k)ns^;t54vxZy@E(mBJLm{sS1y=7 z)PhLvvWV8Fk3N)&$UCdj@9HCN-hSLV#0^-ZXmY<1`DQ)+6}C@57QwEy%l(x9vBJUS zgc4k55J8NG6-9;W=M+ZuslhCzJADtd{A?HDR31I3Qt;QY7K@7b5975Solk@MbZ_RS z7d}t$zRaA2rbZWk<>XyF9W_#f9m?NFEm$DKC7z({{~HW7E>Um3;v;h4+Js1ICZScS zhdLd|Ey-j?txlbo@E&F8K%WAIpsxr|XB>jACW$fl34%%h-wC=#4S`TB7|jfr8AGN& zA35-J>U3`Gep@t-D@>9pJP`j2OBNlFGVuFxD$owW1b1TPC-GkFpa>(Vj=}2-`#5A} z$-xP`9b(@Q(ADoDt2yWl2(N>VX%Y{$XXy}|_aHCbmU?Rihq!iW+~MHx8d)Ri-pQK9 z_^knq$Tgcf&tju9tmldNFBU{>L5C87mWWPZ`BF1jb`iFB^CF4t5ss@(s%5lyg+m>1 z&zg>7<&5u9OnppF)Y1w|*%4YEafnDG?+O<#pZJBR?K+RY=#ANyEq6(S4kT> z&vH?@NLnPPYcmhkO$EU|cLI3~b`O&l(bqGW&ZMK3Jum!c7yO!VosHB$Mo}l_L{U=`euE0zP{p$P*`plg=JB$8Df6k?AujHR`f1UdZZ z(SR%#S#c?3--v9V^>V%NIFRb-3)g${Qm0wA@5ilYciANfH~WtyPv;?gg?hIiP0o6Z zD;M`?#_P1V|NOWy^jnZQ*gx=rM6b`Pb~a$6>pjXN4ZIGqJ{3|PI9b`ZV*)on@bVUV zRynm8w^mMUc7q9(i3!J2F51IJ|E9#G7`s_#L>h^aS4tdEo}nh*L?d1}@#UM}yFmsT zz5?$cuVBPzoV>IVgmUH<#?M&9+-!EI7C>kvK%_ZpjvrIGGld5#l?)pBCkY(0lU4-ifJP}0@{8A*FheM3R<(rz40g;$*vdBx6qWTzPdn(BSUDr z@NH^o7;-x9a%nGZ(6lfCn}fA;_kY`Xr_XIC1^a9yrQxqpHPCI*Cutr|i7#~%;e^f{ zlswLN9AFJte7(EXxD$e`OawA=X^Ef}7EU-6dZHX*VP=k zcXrK{>@Pw+7y$!t529fXv5o`kwDO1mVi@D9w2Kfwn;qf+C5eBgU+jdG?W*5hs=nUR`YWRX+HjA#inU^NIT_05t64O4F zQ9d2ErfO{Ez7dJs zTqEHI=4Y!i!gj31pdX_YsQ`u8WBkWj?x9-!&D1}6KdT7c^IHF?&T(J={&$OOfrW?9 zdYEX*C*p>xjfVuY${vTA07qoOL1&!en7=FcuGUNoYdZf8`N5mwq_@NhKggvR)Y3#Y z`4+4<=JitgP@yr&zx9u>vs2Aybbou;VcJ1os_6msu8a0+Ufz@y zOd)r5rp^7Xh6d6E_EL@(kEBf0zeU~|UTu8)=F{`++*`Uhnb>Hvt=RJG* zLd}4}dZ$W#tLi{GkD?gTyT0j29R-g}qq-iSy`DonpuxN2Jr8f_)6#+Uol7!35RM2} z^H5a}ccr5u-452Gb-fZus|IU9Je#}~|7Mo%^MQGDRTBOXie=;Xc63e1epy6r z`YxuD2yN}HT=z&;KxQ>pdvF)z^mXGSPvLsr368rNV{Bmt9LQU2XgUbGJqKA0P9Ap_ ze0m9JvV?9A*#Sbgc@0zoU)i}aL7IcAxJQL{Aqvm~3U2@HIpWb$GOH_Km6aD;Z!nSb z{R8^9X9ei^h?cW`it`gyMQ`&Rjs+X19J@UE;q*~swHT+hYMqhI-JadUMD)DqaAWWN zYu*&&yBo~5KMx-&-q*6PQvGm6Pff5+aB7Z4Z-l0)BQnArK$qc@&syx+eoc@0SF;)4!kD?sbbZIUc`v#>So;PS?oteAF)Rfg%YJC91V7z?Z^o#hot^ zkznv%duL7?4kUpY_IwgCwWB+ibn{z)KMv6<kaS?W^g3QswTsBlpKbMl^gaKe<{LMn9iFW*(>Dvz}HhI`-NYO}F7#9ULt7 z(Ki?FS?gGUJ3f3N*~7U_(fg*)2wp5syr|8L%ybX$ zu??YItqquMJjI+`zspJ?3u|Ta>%zu4(o5zIrkWjEd_bbvA*8P83dFr+Z3i9PK6txf zjs(RIWTvMou?X96|8hHJ_Y-J;|4SIzi-h6A{3Skh?euUiJd1Gv0(@V1j~@^%jD z)C&SwNM;3ZZ$^o-Pwh!*$uD^tCyH#?zV-5JNyOjymVRl)oA;)Rp*w^1FVBIoEI***iP64VA85thbfido|UgLvUq7 z%pz~Gi%<=52tGRcy86Qrb8* zMJYkXn4WlLNQw8ROn8+oGljIGe^5tT=a6vh0r5+`RtJl^SX4jXiEzMD;u)x_`XC zww>Je{qwn*y%F#4=Xe&|Y6VfWY{|c0%h1zmv|$7!eH6i$oZuzgq8MzDql*&}pgK@# z;0b|pt=m_k0`M$gA&4L>5Hb1BCKOaQhsi)HI@lOWjv!XTxuj_&E=~3`bT6sl^B{Ls z8KjcytW|}MbcO}?YVQlXV*Gx=QuNg;SMJ7^x_>0MEdIyrsdi?I8so@zK>*(DGi+L;nQi=a6*^Pa3>Q>l}EslG)oZo$7 z&vyN)*Vw;_^E;WkHZDDvt9>|bPNlUbmK&B=(c_ohgOBO|O#)wl3U-7`;H=&x0M{z8 zOYA1h)e-g(V$Gb8Ii<`yoe9E|Ko=zNlU^113AcX9<72V|e4ca)5qb1>DOrLF*GP|R zid%{MZQ^Ki-K4Wxr&-I11~TZF$mFHMlNf0M&PRMM1V1plO`z!3XJ=dwLZO7zkWw zTkk>DLO6T2jY8^OeaB~Ec6Q1M2SokQiuk9P8503><+ye4sxov^bQjVvWdBWfThZ>m zN&qswP+``~r4=L>ZR+sL^YLWl?vrV0*DlQ{7QA^gh#t49)iHA&L-)GcM344}<;$Ox zQ?l6Huf~9IoM$^`G*TX$a3IhlS+LfcEX}h6pjF4Spf`mhWaair1M>HQ#F~OrJAKi_ z$+PGn;R)}^qE%N}z1rDVBi!YPQ$6i)rGul?+tr@GdiRd?u*Lfb-PeE>c$`ej>ISE4 zV_S*fIto0yVT&Y1F!=Jh1R?gsB6)@@a7xVV=BhC|##&S4j51W|!XLS~l9HRIzV6l0fthJ|V;SrGQxUIKRa&gC?OfWlPfCHMR2w-_Y&Gx8 zR<&G>9Ge_%IC~NcuX$JL5A~j?(XjaQ>*CN~l|R$Cdhu79>NynG!}S4q)U0~j!q~*O zg%q2SJlleKQO@B{7tguXOM>I}J_vs{R9lLxo+R+U^HqK`bCxg@$K%>v0XYSK_tDj=mxetf7ZWk_PV zRQcHt?{JAiGTRK(3q#}*qRZ;=2cjm%F}p0$NmbCp#VMV)3xhG^Cb22kbl$6d+y2+P zoMSZJ;z)O&N&v;-)C7@#`(=3R-5l1~$7<^pBgcLVl39f2aPuJQgKgbh9T zl3jnh+R0T|P*42xt3-!+4YgjmW;;y88@l)V7#$p4C)HUJAY;Eig$f0(!9*T20+B^f zSVK%3iU<(6%|xmN-$Z3Egs;iDYsk!5`FUtC9Ax$npA0j$MYrLUgdtUe8NSDu8V+vF zRy=xiB&AF%CHq3#$j328f>OW(qs0SKvc=?;oR2r(u)58z;`^#g46l1UykHY|Zmd*BN@eV$BM}`N~0F7Z` z)LYJmUpLTMf<%fQxdNykTUr5&syUqJe8WKjKgtxifg0ze=;oMAZWii0a}VTf_kyqk zq6=hJN2Reqb`SRgbw}d%Xd1eQ0jqsn%MhE^1=eLI<7IHw+Ayo(W97V)$f|E2tS!dXVj zy^~r8N0W$^XI<%b{drp6IbH*why96m`&v~~`V)3@+>Wq$m-m!MpF4D{fHfoJ$S+>nbzm&&Cn^C;zj<2(t8VQXtHT&$w04~gZl(&7Vd|m^fMH+n0HJorJ;a?Iu zN><$Yv$+I;E9r*AZoy0$4?a1_Crec1`Jzf8lZcE0tpRY(RKY3eayv6@=Lo!dNlPI| zp%OWuLWztJny{y0q2gESR&>E4wiKnj2&2d-k1EUuPmMAkE&cn(vEZ>s^NwMQi5!NB zR{6+$tx}{T`sJqw#eS~d+}B24AM>{bjRq|l^fx-u!UGqd1wBdXjP}a1tV8$Aw|Uh& zq`s*!G3yC8c6$6g99B+M)(hyQ&6`J`@GfZ+jddJsLDT;K*zNk5F@(v%ZX|tnd0tp8 z3WM%7U>koEX!N1782f-u>I3HG`U{UTp7i($q1C;2m=93mKbu0{u9+NnKf5YWY8mAb2b2DWSCL8zX9c*a*!-!|}*2n$}u#Xgm>HF3q5LTxh4EMj}9b_jC z7gl>!?yKCF;q2)9pRk$&X$4+0;PaIi2tZ?s3IErRB-o$OZE^AA1UP)MXbQBWX0f!T zF_zazULMT!t@~$_dhZLGu;N!A%PaYs-yZ&5_SfBZxZo6W($rt#@V3(j(Yu-*vKz#H zm8YL%J91`r3JG^;Ti$ktifw0RprfL7Utw@xhMhiT(M0I#BycEXPL;aLiXxS!3=5{+ zGWpBj0!3ksgIegrjoGyM(zN(#5F68K*@caX)7*9JfaPV`*)uO^A9(ee`f!&P3RLD&=4~Im}2xQt)a-!xX9S;4aMb6Q0qM9 z$Mo3V)+RMMYJ|t))`uE7Wo+S}C?!U-`@Osu07wA{CV=85&-QZqu|?OEMw=J!a*A5j zOnmFVV){t$$URI=hB**&XA6+*AII$J`icrSeme$dN#^@4ixUJD5;r~&o@Xp?4B*V6 zJ6vQwa|bQ~pv$qov%O#Y8l&%Fu$SD!9Y^&ae2+t;vd)Z|t#-D7=J_pu*`JJk(p=dg zeD4N*ovkpafJ+;E@q_q}W8I$3H`#L$#&`WoHHoGml2oIWGTGsKAA}to zn#aFxcu-_)v*ty&9jlwD-p*YdVtqSPQgsrr2JWBvvwk6@2x#S!NV0oW8D9xgwASc2F6cM?# z7D~|Q#bi0am;wVPD_$MCMjOn5E8prcnw zRdQK~%;`iX)MIOVy$^jCWIz>3iu?}dK7G#(Q&z$~xx74`Wwx%E&V-%_1P(^z>f zs1>aV%ZnZM3V!p0MRJsGZ;*Ss{8$d!0@i!n@w*op+NcCU7RxOyGP&c$<2z|Mv$b5` zf)CB9WI$iF#lvw&q4{Q902C*j5Iuyh&LLGKDMFGN4#?ElTyQRmL%UdwxSsX(&tMOq?c-DrI;`Q^6%*x~^)1KJ(!z`4Q~0%BwU)_6 z!1JKh96io@_06>+KCC89yu=1cX4mnPeaIpjvWS zT5m&5u`0&h??@Op#?Xq8lS?Oy=c!R~n zmoxi?>hz{)Lznj3F5{*bzt9_l17iVrTN&H~&fTax$fvdWl4BrsCXY+3#k+yb&Ad@; zg3}bD@)+7PxC0=UCFIGJeu#pd1>?hmE|5!j3wE%3%B%t1wd}9MK@3j5vH#Sw9MuT(n8F3$-f*>4W@nM>)1J`i(Rx#h z-ZQ=U>i_P3dwfCdo8fYo*a*Px3ce$A8E`162^tA#gNDGWnR8M~CmPiKx=LQpp9p6; zU3kE$Yjk-FKOy*1+RoV1(Us?i>UieX_{XE;mk?jb>xG{?oVrql<5;5E>HXA>w)&iI z9>oCkSL|!H5yRF<`y0&tjGWGt3jK)=+Di6&GdNy-PO~b_&T{w9RcpnIJKQX&MvexE zXqcB57`>x=KmE9?u-gVe1#z`6ev{;yv<|;3YF5?36l=}z;81UXg*W<8WH(06!4elL zaJn7AtvJqM*o?+?{$!X>`;4FkEAQqj;7|3|S!rGqOnR!#mI-L}Z!_3NwI#VNVO(5wdU?xj2LKKUvCE0zOFV~Y4GovEBdYg{S#>mr2 zaGYlQg^}<7Y+6xNd8v0yK+Z}raC^53Jn100(W6$v3NJs)(ag5CKB=q2D)9I|A5fmS z=3$Z3VH%1y=@}yem)SlG(WtazOsH1(XUojK;sf{YJr>#+ox4``rQb3pE!9dVWVp!n zTK<7TuBUg^!54|iX>?dh_&23)@;f(8i&?L-|wmFAszYqQVgzQn19CT9@pDp>d{yzMFQ3A6F!NCo2a>C!KncnHxTT z?XVOsAOqqW0D0&R6T;oKSq>J^U|Otrm*j%93U4DZycaA~y7{FRpp=l&PS7Kn?FeJv zQL^C6SW5h^DR^#(zU)<&d+>peO0E9Vr(eYb&&2MbB~J$|E*_~}UOLCTZm?qP?dtjF z@Pl?((IU>Fr*dEZ9=b7d`TM+6vY^P=GtbV^IUvQ@Z!qe)lfPX=h-)J8T7+@(*Q*6@ zn$i}o-zP=iVRJv6LN_ljhEj5>ojMaVlNOkh`XX<@Q3Yg`+_|hCK%Y)s&)5gu6o&I` z7xnB%E$z|0hCxuD5Wz;O*`Z=4@OpwTC2`Usxoli;{mF3nq}}JHxm(BYe5pNazJGoC z-%V(aHVxjFNA;Ty(V~V(_yEglCv(h9!mUJliPiEW+Kvv}cMYq2_Bb`WB*M-yrl_V` zI%Ztp*)^5-oMO1Dw-wYQ;Ms&De8c5daKv5=62Gsi$^Ibk2J#O$x1G#=eexttAnItr zin+Df47DY7h7E00a{O9SeY7Xo#4+(LpfB`l&7UQAPwsDf{c~3*`94{bFbA6XdjOaV zI*3_m<70H834VNc{3Jd+)x zr$-Z@Z5XqPMqrnckGEjW;6#P-sEL!$!Ex!&Ag$UW*#t!h@A2X}N(#{%5*Hy#6a!g_ z4P)U7RD@+NIU^O>J$UmDzcF4eT2F6?lm4R6`so6Bx7GFIeBmlf1Q2& zOo++^r7trbsf5>pCg#9Oq$Lpzsu$a0v*zS-0Vr|{n4<1xn0;<1?_lhdCM{B`=+-C_*zA+011lxZMl~k}MJko8vK7N?l z?#I@0Fva(WY?J1$k=3vR9OO@P!d}?r3E^OR$3q0~EecJHB2&udcCZD-? z6@94D$mE+xO|)H4jd9|~kHmPiMXzglG)Hw|sxVwBa@X0Uh{C;v4GBFR2R$p*z4>59 zo-beN=B(g!biSmG;=K3e(0X};t2vG0g24~KPv6@YB!_9i<6q0*e;O`h;i70A%5RoK^Umb&EFIc_FNYv`+sEp_}@ z!85UGE`aY^oKn4-`gAd{5-jbwU?XeR`dhfUe`K`g+_l&SyH&$|hBz;loRy@REmA!jY317GT?A;tdK{X)3oN*O+8RGc$5@!JP z9h#+4a~<|#vrrRsCQSBLW+A9-Oro5u{_Lv>&g(J zXDW`qWjB!}cfozxG^60fHpnuy1z5AFc0u1;0B5`^!yzy5y629mel{n`xq$-7|X0QfN8v z5yaFmRG^;$bJ5;cKNQ@6n9*jhrB=rQ;?@jmOGj~ zvmZ(kl(9BQnKVTdPfc{E5$f<7q1z|!hfpt%DAB zJKMIL09S3{9X|Ln2HOHvCsWqkDZx~_o+jgcn57@o2$?z@r z`|@|=5c`bJKFVwJRMpmZe++e2SPJ)4obs;sAl4_2RKxs@H8r0bV^cIaSLf?rX?}fy zAGUi6<^*}LqUv3ki=V*?t}-R#(U`T_wspxKGHCjjQNb3*W+3?Fcst}Q$`N3C z_6srDIHfd=?C;E}Snu1u!uh`5lwk+mckR@=fS~C|3yPF>l5Xeu+&}2s{?1yU1moOK zJ0om!=8N|QU~o<*%EbDy6|}O~;T4a=ULOkzDNQ9u^)>Dg^ek#=C12~W+a^nA%>xoX zFjF_aJO1|-*`KrkiW86j0ZM8W+d&IJZQnkFU_y6WqLBixH0_=#jlK`+s6ElPY57^< zT%`=K#BG?wQpkg|uB=z;l|%ULmuljimi%YyoIe^-HQQTpIDy+W?8rKo1!+9(ye~i)(!jk z#_6_WtoGVBD>K)g)sW87l-xI%2K1-;W5?W42-dvh>HK`2-Oa>c{?#eqDo>NX(`QqOa zZ(s0^PjR&b`{y;VBN?_a|7;p?EwVlQ_p_1Ql67jV8Ca8S|8HC7!=iB4#b`n|QnndC z0p^2*^MiRvbqvQ7+aa=nD28l8X8Vl#SaLAJNqC@DMBTNdJ_CnxcU5r*1}d(1i)5yP ztG84BxY+cC_E9!2$R{W5A2!uo10OTSvjHR~uva;uk7C@3*qC&TFw} z&o90y_en9npEJt$o5|N}IneHMr{c6-_`oY0UZRfPzH+i8v?_)}K zkxeIZ?0j|qm@7iDA~YysP)p*S^k>j{e*bJ+4>hym)YyjM|9-l6h&{MBq5YA|MuJry zV&x~m{3C3YmVECt-8Z0S>TKp(I3FDL!~{`fTXe7= zKp24GA?(l)k-wY(Hi3nw<^)ZRQhljoWn|{9{5_WNzU7G9e=_O%UggF~BYCd$2xHBx zKlo?`BVZ_O_Dpr|CcVo&1G(|Zo3I@r(l%z?Y`z{GfE7PPvfv61Y5*Nhh!4^bY)d(@WLGtpy9mk#A z+s`oeKMeqs&-3?KZ;VEBxn4E;;uHB10Cbh`=m}#=v&kMF>7f5pGb+h6? z-Egt`?)J@d`FiGO(k~Bp^AA}asvIt_&aSY%kHZ^VnfNrLb%=baZ?@xyNa}BsM&D=e|W>^#01zhuF`j2XZW@8b5-f==vi` z5acX8!bRONr@J9`0jC)QxCJMu$u7-i^hS4W4jNm+dQtJ_ZUP}9X$!3UN&s=?(zn3( zzddcB{YQUQ^`)Q*xs%r27tW@g7eE)^6vbG0S3ao)4spbV#p~|jTGv}}VqkW95%DhP z`6d0Ia%vIrfrtv@yG8ETz@=Ci>=EMj&DCKYfd~mff8_v=@TX;WQ@#Bc#$-ncaR8^a zgIWc2Qt>#TjoIOMb2^v~ZT>%Kd(WsQ)Arq4$59lN3?jYcQ3Rw$DN+o{ql_Rh66sBe zh;$KAkRUM`rS}mjO3NsQ7$Ag>A|ya4BZx>BP)He6nj|9e9#dvt?oWIF-*4|)do5R3 z@Bu^a`@XL0JkR6!9go7>2wE78<$0-whBfN=yYO3~MNv0uEz3(??5hVgZTA}K_Kpy_ zOtFHZFcyR#;hoL!Hzc*4GVG0nC9YU!SkFZKv#b%Ew z+GmDpU*FLDj-1-4b7-IA+_{Qh~#V!00WP}0b}1PoGyS$}MgYW!l>qAIWemkAT0j~vn*oRg5xkaOWb zfj~-iHU|xZs#vAS_BrA}Oe)IkO>2ePmCg&)m%6m+Z2gB8=Nj)1`{cRGn$q^l``sn` z^)Fe!LZOPTIvdvZ2l0xfU&z#&>%ys~Ib7$jw<~HcHTn)QHrkic$$>=wz7I+QPhOx} zb_qFB1vrV^3wnb8;j!QnZ09sL^4`ShmBHSlZj?x`EahR6CG ziy<1gTU;T8pv)Vh)&Ruy6G>BOK5te0(+o4*PRBujUfcRJ{NHUFs3X#m;~lq2ATt_; zXdNepzAdvC#CQ6mGGeO zAHM2r_V=F+Z_c*_yG>?wl7~g*DCP1)O9qy%c`%uj-iiFVJT33Kl}oFa8ddY0alR9M z7{zs6gV#m2NXAMkb%^YQDL$41xVl>c)vSpHFC%Nm;#6Zh^%E_gT6%DBg8x4fFX{Jy zAQAJngvD%4&qjw~G79if&>~-vi&<;ujVj}Rg)qFuyKp|azBS4&X@K$blvHDNZ1omm zFy^k*dd!k$i;;`!sB;_CE1+wZpsv6C;HY*!YAn7;+kgC#ty05Q)sgErZUn7P^f{nA zb9jKN<3|0wdENPZQ_AxYt%H_Ifo2r-KR^JggIme*7Qul?H!6R~%>8ewsn2cQu=>FP zVvD|5QHo6`HVujo92fOgYJuX8U4?E%tatItq)OOqOGH*6Lh=B=hnKGyQ5wdgxqx(7 z`ksIbpGrZ{*mk@vNOdY3iSeGU5BO~&ct*ElCjRV6kMonIucxdCvS2UL>y!hfW6x30 zYDK@*=j!L;u^fG^5Vr z&Q4#x7e4}XOA}!x52t;YHw&>fu*8QR(tLjVHxv9_U-dZN$lA)~tz*6`Z$}rCaLoGP znu}51IZGp0mDD~@>M08;&7khr$)&=9Cj{osz4NPq6Yp}Wa#>5Ww`KPW0|%av_sYY8 z%?e!#w5^zV(@v-RyiP% zfhG2LDG1Jmt4p2=ICnac5BlsL6S3Y|mmgN)NPAH@1V_zl@iA zu;u+NVCIJ_v~Hw)!FxCIiSy+v$_#}L=w2dwRipL-O2h7_eO3-{zDkZajHm5y`-vud zEay7a!Pgg+=(zW($MC16ub+n8;}2qttBN@hY!2{jt6h>l-(s%sO_;U;w}=lc>k;yT zqj7Kf@})?^x{HG5ud$7pvToD5A#eiD*6d)#)g|Q6c=j{Rl_=rqU)Pv=Cph6JEA!S?K_UJ98 zr*sBT=NB9$C0bvvpN_c^ObmoG)5IG)%A?ITOazQJ3kQ$-pJ z!0i4_Y<@FUboxA$M%0GGXI&!r3^;rtB3A6#HHo|cOO5GFOk_X9J0%G}bsldFn4J%( z({j4lXc6mKP~@*vc?dcDUN}~KApqt+w$$11pqwt3`AC1m#&$AxKP|b|*r3e>8M24- zQQx(~`2b(K{=85}LUnC+FDU8C?EL_}(!w^omQ`i;4#FOAKJgxXw*qZ-9aC^gFQQ?@ zWn$*)^*_U2Ew%#F8BkL_2NO3Y$d8)FhdquU?v*0Ns**=vHS&LsRM#$hdI5CG!g9e3 zTpQ=&GPN|*d4lSQPW^0o+jjSsYEag~=cv%jt&HZvx?!xaOLD(+hbR>PpLC8&ey0nM zxkK+BNTEsS2fzLl6qLlthGoEE(!*jc!9uz~u6x0l-^sAz7k51#P1x1`$X;{$I~W)G zzMEZm7cdgJaH%6$7B!Sy1(I)^LR&U7JkK-6E05GU_`+?Z3PzkVUi zzRLrP;+5Z)XWL~kROEak7ljQojMrI* z|HYH;QQ&KD0%r~K7?+-dZb{FxMCTD(Dc!Jn7p_lU=UHw{Y7}=p2F$m=pp=|AGi?|c zUTflOu1XJ>Q}^2K;`xr(wUpt4q*IJmz9`)G*7Ec%;#+w0xBvagUKJ46&3Tv|t0?oS zc+4D~_qqyt6Lc)!WzBIoE)_bSD=TF=WC2LScO!_t3OO>E#y14GpeLl3ew$6( z!V1kOgG+F`h>jvHVx6;hPGU{cm4)WcVAXyHkJpWXYr#sx82PHp z8K48(V_1tjfs`N*(g*95(x*8@FH8{vQry>%LTDWs8Zit{?&e(PAkrnJ^?oHEtOLV` zXFpgwE3G8?$MJq$F4pttyVwZ0ueC@`zr%3b!a#Cd>-On*yLy9 zQtEnkih3_7((7B-vUmUWFAj0R_CwUPcxUidkK<*)g-jZR9|#4Y4^POR1ti*$qbb<7nGAvqWHmsPjVAmuBKi%l zfrdSP!#Ef{Ly%k1KGNC3m>_8T&aL?G`3!_iMgv085`$eUG5%^ygca zWqbF2k(C+)7i*6o4DkIS!n71rr9ns9G&(GeRr9QOsJMFvedJP$#HVN$-7AHKp} zP`A#z8o8wtNTTG?jc>gQc2^*4j>0lolb%z|#iP#`RFu4tuQ7AeCBKBKQeG{&Rvz*U zcCJ90D5YKjb(5p$#Ji}Og@Z&jakxqcuxb#U!2AF6?0#^mxKjkwC#h@`U$;HwkUjrX zlNcU-s8{Xq#GJ!)?yxyML)rLLJ*YtLx)Vl~{T6HzUVUaYco_T32w*@o-PVXInG$CACF@jVNCPfWcN5-elxs0 z{$ZxnCK{ziv7fvC<^2%Ss6=!%Ffc4Y!!467XobN8c9OBH-8fgJA zA#ANT$@CEUmc_MxiU)cm)-%L_9Vv44b3T?5;$0t67F_Ge8qY3#%84A* za;OWRCXDA9RwH0%kpN%qTvo)!wxZWQvPp9r!s|dta18G>h4gxiljBiXiwby4!Bsjt z>An700iog%uJq*^;NDseNR8d{`$Md5v<0%WL>QG6EGQ)v{6MHc5yZ!7a8;88( zMb2iiO$E7UhW5vH6ntpunjdhMsLiDIjml)PE-mXCKJfp8s$}$K)1#m;v2LPux-Dul zp&z&p=6kT}%ur$m31IW$F$b1G91B4YuLiPm-Y5C-fgnGMcLK8qSJLwhCCiwM%XM@c zGp)NQ*uU)K?zDFzVn%uRno{NKGn;#$4Nydb_Ah&;KZ$fly%_j|goTiOJ@BnR5T6!l zkQDH`0Jo8g@CO!0Yh1^0k231VIZ=6a0KR9db^K!|=BJ1+#H%j*=i^6e47_s!auNd-t-GhmO7zbHRpchzq3M3%^AK0hC08;kBBW$g zWO?}>S;^aGC-StdETwD2aO!%_uaT2WOCPA|eSbgDJgr&}YWPTxK99b0Pe0Wx?)y=A zHq%*h7vyJ73l<0W{T2@AyVk-Wy|d+WA2S&65ew#ai}xO0Q_*;@8@k%tp=ypALK^-Dm+wRg8GqX@6O5_yK9ma)p^ z%@~^KDrutyYAU)KfJbeph33JB4OKNZ4SiFyI_+*fh&WeaWWXuj=^$|bciXipR!exh zTA z-r}xEfAk%(2LF5SXBO)_BDqtBf1krl7Mq9FoM~@HQMpE4%A5VAJjbgYGZ_d@b92_> zYU%$g-QWLAN?nbZw6)dzedVv%nZ3KMNAc=Bx`TC=Qp0BSsm_o+0ZGot`uSCQ@MDvH zs>#@<<645q5Quj&(P404>u;9ntq2<+{D%+gU5)w0A0qld$xlDsqGxaD2=ucp7gZOv zK;wi}sWpD3UMn8`%fcY+K4m_&Jxe$CaMt+CFP==k6a06edTeg*rO&Ri;0KuD@SjO{ zh^^7m;R#R_@#`p!Z`H#7P1jw@Ks20!;5&mtLSbTX~On7F;dSF0033 zJd;JHTgovf&ShH4E-Jd$>zJoqsX?NS7e&Tb9Xax4fNXpdb~M?Is-8K=kOcyRR~0s% z`Ta+RtlT5pG_+CxV)oi>{&ZIh)~ZW06b1*J2|+<&urVq)qut0Or^v=2smVQ`VTYB{6C=oT5PC_L!ZHS60eTt0FVz+ zP7Xv@LE3Il#i0-w70dk>*UZvB_zV6-{yGsrD*H(aV0JtkASv<6Uh~FAOxKH_OD9y% zO!RabRyms%@3*!dD1TSuSh7PKo~rutwR`zw6$_{21qOT)&{5BCA+O=TzqQOHXHhcf z?ybLFX}n+S_C@66IXH@>W{_U&?sp-f2ZoT1Zu!kD5$zPEZG{E+Iy@A`t#V!JMxeIQsRU8pa z8%OD%^GTiQ`?ko>_l>A)GYR{y=%Y5e@?fu>->^c!PC!%1c>1nUm3rg#^}b6K{-}Z} z!OyweU}7o73oO9>ve*r1v1QQF{bCrMWlwpD{m)jT!`> zOUMD?{ufHp*SF+cM(ibZkn2?IaCY~rfIL9R3_*d$)pcV#Kn3_TKDvJ+1Puj zP4orT21PoYZ!4-WKJ9XS$i$^+VQAFfI_oVV>=a?Bs#rU=WG<)SZ5_(2ylMmonoTfF zS*e+;=CH$L6_>b^$$qf7Te=^2pYytvi_Qioc=Vt|k?qI=*b*H{Dd=8|0#;osb~oWQ zkQCpjvgcqFaWUN+{BIm$+E$Eq;0QZlAN;@h=b_YE+)Q6_hij2A$?npg_3NUM%PjGDME3C4`zuR_4FbpIl z%UqYUlCndsMA;|Wvg4JkzG820w1V+f-GUXEI<43LWt4BJoO1sGc3qM6pp8|p11tTYYIEJ;z;Y01vbFHH8X()Z_=fU_xBhIfs7gzdrx`xLkw^9- zkJ!ttUHiH`c_ZbiM_F{4R@OHI$ z4Je6!qd%c}QT*FDUXERh*y}&6-O8QMG%4v*FQ(nD4Lln~VFd;0ktqL6HzPxK2a{zo z&JV$k-&_LM^4_GmGmf5R7JDr%CM!?-Tx`(^x9f#VYBme|pEPJsanLsmNwb}FE~wC( zmWqev46mM_E;;U#E-WZ9FR=^^3=IF07}K2(fjB_y?;ZJTqczJ!Wo9@Qj+MRyYbm0C zbXJFRVYH8=h_{~7L+>J(L^1YF%{vQ>7c?NRCk%QGF3FXzk`_mjfA26Ybu9^ATDi9} z^OBubQH!su!;2znsw+$&eMG>E1A6h)J-lkC=-hpv?|$?Sv`WR+H{jpU2!dj5UTw1q{B4_| zV>w@DCn+VDLUaLE)iW{_fPwLy^`*7ye-v5&HBPgc9x{J-TwUI;J|rD?zN?ht<+I;C#){1){0ugf%abtN3KQgv`*94kVwP zYE605xl<6gQh0!|W{N7JAtr?W>GUu@0$D9`Q>f-tm#4 zvz3b5Z3*+1Rkm?aUWGv^EYE+pd0tnCoF{{hOu12!q0y<-Ba_*zVFk_5;0$og2u`*J z$5=Gq1EiNO{3HP@!ljJ^(!noyyrkXahg)wRyk5q@meFm*0TOf)#Qg36TD}ulIMlJh z861p(2a1o*v_^-*cdXpfV%&?@`;(zJv-D;u->p?3lXEz$CqA$YB+8v%lm}StWC+V; z_5yJ2Zgga{x$w1swB`c}i=V}TlA5pJ9e_oKco*rz9^Jtb?|Jq)ZX?B? z(i4pVkc{kDeOBvCOe=6D_~W8mG_ILih9}idRf)bA3EsxLagToeG@bvM|E$i=krlV} z;Ko3OwcGc;XJtDwSwZ1J|3psYAqt9REuL~d@LY>EU@7GE_(r87*(pO#H5uuxHBqwP z!8$OqNKar^<&h@vbuKcxq^&dO`N&}OfTZm|0{}b7hTru;|NH|Pt_cM@K{Ahl&x(SN z5&a5ngXg2$V9BUG1hDqOwurMX=NNAYrjawuRxuHVJQ>5?kc%W7#K3^}Xw{itKBH~g zET}GfdVUs1*wq=}x(XT+zU`74ix}V=X8~T-$L=mPHz9{$^t1e!PM}yM%*K!3Bd__zIY?VLBjs6wd_RE2PJAZp(RNc8n4exVM$3F=p@+ z=Bc+HUJdh*acv@dTZW|9s($gz+piWF^3a=>kzF2q_Z5}%W$sJw(v%Ug#j_GFY08Q8 ztbp5ZtJ+Jz8cJVRtC#a+zu*^M`%*(E%{mTT`%+}7=`G(HITqh|Zz+zt>5-?*IAqQm zTwt>Q2t*4O`_a&0{k1TGA~zuy9{><;GT;^qHPP`GgyrEAxpraw(qjUL#v8#kx2wDb zDw&-p6JwKTE7zkhd{Z%ZGK$uCFtajpdsm`sh32X?8L0Z)$yt34$Fi1kg4HM7axS%i ze@@}|HfNQbi<*8brTGof{GXjik`1g3oZYKr44g53&KU0n8c78dYNst?=1Tg|4d2#n z9uxY$jV~{@q9}Yy-*M)ytI?LXby(k;}a}koXL=V`w3*xO6W+jD5UZ8#%1~)HH($#sIv!Mn4178MAoCu~G`ESRF;W2MZgc*`L zzzj+fmP+m;LwM*6;%>Zw9+__>ECA!M)d6kWb}$JusK1Ipl=wZHT#r9%JN@|tY%jDA zd1e?vy8i`p{MEJ3qbeC`UTYE4k|o1Jol*yDYS8kF>zA&5snqtRhY@KZt+gIB_mptf zeA1S_s@q@KhMeW8K`+)6H9CBfj@joSWBbxGnVMeH@b(qLl(hT1)!G!uyS^^I)i8r3ATMxWbIbupE~OVe`>ki%+l5QY!>Tdl zBS}r?Z5`9%N(6gL=3LW*B6#mlMJJqV@eL|{Um4QMC(SBK<%ap@!WIC!hly3*STK|4 zh(I_OVVA(8rxA9Ja)@);JwJlK!zD)Ekd{2H<{tpYU2Cp}OkZybZ;au+I5BcSPRDN@ zg7W*mV6@A^qUr)ZwRaSCb$l$|{9xXDk~KTo=cu{jP?qcG;Om#9ZklWTY>JjnJ4 z1aPz%n6c(Hw?5A*V-x~rbB%itE0;bnld6wAj+Kx`-RYLr17PB^*kj%X*Cr}fS!LC|2#LG~X=;b%T`zB#y!Nd0mBCccxlzGA%6fyJ z?r*1ex-Do=GnD=MYQ~BSQq?@G?oWO1tZ?eBHc~4IigMYf`D-l*!V(^ov@(ncvnYh* zu^w@DvlqS>B`4k?dBi{yUNu5rHh681xEmRjCx9jkJ@7ptUB!P~sx&KBYHAq6@1TY! zv|H^D48%1=7OPiR=#^<_QzO3E)6q-HWgxpG5Dh-_$3L(~8qx{$FK9Mp;oz8fhrmkc z{RXuQpl;j*gJLC)R1t48j>L*=A&jrUtFksq^i$+22fZzOET&czp~IKNXeMR=fv1s?u{Kiwqph zva0->2ED&mjZ)u~M+KSThgI%SJ!bo7Xcz=Fk2NhkU;|P~TvSp!!`bhz{Q8Miwm>l) z%m0)Ma7S3KBH<0G9ou_BM|guz6dM3a9ifC1036e5_%M+Nl++DeTG1t-i9~_MIM0wU z4k-h*t(Cu3|45nq)AFsQZ@G+vIE?+o6%cVf-=|WCXnBFoupmL!@RGuPdMOAHI`NH5r*dpd(h=3o zAV@}R1-9btP{B}m4Bni(?mg{Liqs0R9s@DJ0g74OY;ZF;{1PjC_R7@g==YjJvP1c0 z5J+7)SXSU$?r3RyAW%gvZy?C_;9cixA4gcg94CsWdxz#t82|b%*7rQTh4PAw^f9>- zs&1X>>>2Ff%xt0Qdzd6r{uD=mD?3bh1OLgAIB&`tAT}_6|6Iom+V}~uYc~a#MD`N; z)`{%gFkw9OF}~iOGZNP04>IbE{q4RDx*X|5*PyD{qK^|_Rp&9KCy4O#q^7cAQY%<> z2HBXkDbY#13*=a{jNYxor*!XM6E6GF$Isu7m|b*CFPdb-H|@< zmUw20Sc$t&&R1~00GHfLjnP^wE~#R6r-o!%(?(mhE_ zrKr;Dz4C$_f1z@M4dV@$-c;pXr({qA5WRc{--j2CxV-c(Os&ZOSm%2i&myF}VG;7Q z??lN|#IVf6AYBgyFf+ucfxWam%0cxvhZEld-0$hW;MX-C#Vt*T6j%j%jfu^y=~ z>pDg{hngCK0x`c#)~5f}JXANoV%)7Ta5Rnpe3A;CVf<_$CB1HS>RacHb;y|;}>bTo* zuodwAFxd<}Nft^@ItQX;%J{+Ewklx434`JE*(!JN7etYPPTc%oR?V^aUnj&a5p*b~ z7aSQE$XxmXT-Jxp?wbub{a)rCvA!T`FwpdwAf#pihw0_dt@tPtF%M6728KB9bC_(f z&e4YNbFQ#)QScDiA02Q@(FNaJqWI?=-3pO4OTBXQ5C)d|=v3f136WV?bEDghp4m6# zD?YntvVi${$pepf3Tqw{3_vSo1_+R!$GTl zT5M6G_aoGBFz(D+G#Pr~3BBL|IVvifvnw+i&3f?!O5{BipWvEU^{LDi|M}gHDJ)5i z^Xkg2+63py=80mnx~jdjpxR4CA?_7^wsFK36YbxSwH`&zBrlf8#-q>k5h6OHr0s83 z&{Cc-;-Zl^*|C^xm0e0zn~?mS80AiVU@-onLiHtK!(iYwYOX@Pe9G+Y>s;$gjb2R> zV6p>;f=s{&`v~#)Jhrstu~_vDnnMtq0P;~fJD$!W&eisiv6zffoYdfN~Z=ks#--1eY1I+rXo{`Q+g_Dt;R&9^`PHB(lI^!poCqGc&W|ewr zhZtt65h7>n#EG2q&w~Ss>os7RIk5+V4I}{yyJvDDIZHNQhLB@a_wq+J1c2WQ#Rm)I z9H3lq4D8{flYNAb1b}yDOn3{l2l;)(6lRmpx(rP^f;}KTh6@}NMnNyR*_hq)Vux{t z9p3!z+|YcVy59X-q|bucxXHZe=hT9TS2vr*FGUv8*~;URdq4iBofq4nbQvIf#3isp zHhw6m?xLFj>kTdJ8MBOmi!H}c2%H|PXD+{Q^nmmoR)t}P|1WBlTq#)le0WTtZ*r)5R)qw;5t+el}J;hIpA4xX4BH(B~6`L`e?PkzWn` zgHHUL044@+(QD*qO|S?j_yf%#<@6P(qwszd6UKc(o4NsjP_0l!0P+^ki|Y$P9OVUf2Yk6_mSz6$rKS`PbC*4vq!MLYl5x1CJHe;aQ+p5Ni}98TzcuQ#SO4Vqw*4fOk)pAZ zu|?Ps2eZCl=F|g^yhCIgZ;C@sRIKHZVTIG&ndY8p+Fw=f3V~N*6>=^(3tH9T{#5@s z$)`F~g;-tZsi!rS10J>Nns$baftG8NGt)eH9f5njrTh#Z?7X91R0|O z`+*LJR^5y-ywdSU!Q5e5n0M<~eE8L`thGhZpJaCsw4i?kGM*5Y&72)|*FO3J?7F$j zi;ZYjz*&3l*)4KOAN_9gGJHSM<0)ZQ-EL{7_rWmE3@(dThbR`dCONmBm@nY2YB(XC zqIOO!`mf)`;V=DlWWei)N598QzI43qpn5^>6~rFVZy{#}XlFo!tTI$}s4&?72^mQ` zuoUoiy3$-8+*hgO@@eV?ufCwwFeM!aYy3S+XYL zmXp+D`>&xJ!QGxbFxQ?81`MRxZ){>ODU;L!A3*2A70~QIiNAC@-Vk{ZW+qnVMp^lH z;msxMr@5qbf{tK^_(HR!^0S`c^!iNZ)_WO8J}i0uzvF9h5|njZmrMV2TZc>J4Azp^ zgG9$L=i7*Bi~Ljc)Z-WqI(rVgo*5(x!zJ@Z`w(+Gc<_7#!6K@SxC80Koh~_p0wX?| z?EmHiAZJF)$f4RlWYJ)x6^kn4Fe7;3;B@>((bciZA+n_cD)hn_U}-8LMN)Hw=W19GTt z&f+OMGBO^tAw6?#B$2)u*MY^bQeUCpwka>BGDsIwa6s^2r7-^AZ4Zd+#w@wrLq_i@ zFMVVJ4^Vr(bhdmsD^BB7_i7cG_&MH3@(A@GAYtKqWB306?J03Lpd%{Xh}1=8&=*gJ zfrS^6)G=i67X{?OH4PfhJb)hw1x^T0O8#W*JstHFC*v@)HLAWGXcX<@F(WpAitBZp z(eTMX&#z3a3YQ)xeMV~%doGHj*sxy+la)p@#E`T5h@r5zGs01R!Wsa6@_vT5=uVRM zM<0TIN6Gu)lDLC+rLu@_Ru~r!d%H?hZ7CG&Ox?(7DXZ1cIF|Tim@D`2@&qkRCuKDV z7iLCqux*-Fa52!_CNDa<6Evb=N`}^c&N<;6eph0b z?Su27QQj5kDD=BJMg^M9ZFrV*Jo>@KB@r`XI4kFxml@vC#AcG3(>vgsx?U97YP+EX znh$<2U5QzEBATA6eoFkp(JV`RqX!GfVnQGvvb$ z7avY{eb&L4MB++apUi#CwFs~?Ii2??I!NT(q}MQNHgM5kvNN&97|gxyc;qB@k=pt$ z#h#zM_PPB~&v@mbwnG)JSCG1V8L^6Nj0O-GC3C+?N)TT+<}_H$wvWBAw^rTgpH0Ql z0&EXM5xOt9!7I*X$0*@+0)YPJx}*fJ$RnjM$hGrhvRmx}{7V8Lx0t7Xvk<7NF%w%! ze(P65wnia6{r=H>tRgy+4jBbz3%HvX# zitS8>1w6&}LnC=PO67M;*#3nT|NfSHpHN!Q8Pa#6o3)RYZ`I8dpG&w%75UHR9qh$= z2wFLs?3jrrdN;=1J?C>&qmbPcn0Jc&Ad9i zXiOeP9wtsBmG(HeQ(Uj--umZkBcb!+d$@6^@uR>uw4IA|bk!zujhl`MEvY{iE!!^K z;OhR~sa|mpXgvh9Vz!;+&-W4A9M?9U5Jm&HLH}+OnTUa1fUpfp2B*})!_ZcJBuSp} zGv00-Wh%Dk#|)tNVfasDaNn- z_I0wz7au8kj=qfDkN1NTxiPa40=om^6*w zL4!s|pePOwX%0a6M3?EU2bW_YN6YtCr}O*0$HNInrk^~YHjaCDqehAE_Uch?`6rBS zFOQeud#ltF%qK$yUTSV23o#f``-XP!(lK9rw7&vvL_)Iqo#Gu|l{{!jOBoC@dv{g6 zE(DWd6tdOy&7UM_aC`Oa#;Q_{hx-SLhgA6}~{3UjTGiCbl8T1m>?MJ2lO5xsD%O`rtqnxnO-sw<^%=RitXN5F3O3$pus27SGUNE zeHiT(b)W3|`e4BUlzNlPdf@AY=}qSe8{7p>q7Z8qgk{Y<|Z*z78QGJ;rYX@hfRHuVk} zq9(LbDO>qru3fn6OV;r|BE#JCj!iw@EOwRgX+PVd;LL%+wB?}SBM#^;va#d-pa||U zg#w!Efn!Qk_A1KRUG(oZp@RglM>vhd;Fwvu5a7ptC@^Ql;IT;uwkd)kv3C#nsv_21 zU48j1F5T4qR zkS7iUG|GN-!{B-@cLVgsngkB*QEZF?L}$SjnPD2ReK^tueu~O4Wkq^$V$E_SQbS`t zJJB@y;IwH~dx6EXhuAA2GRa(H=Js_vnyqinP$AOgnE&+gChZDW-G?I`1BOU#BW7=hjulmP+1{t<6(&A1I9>1OtY0KuRKMCP zu#(CqfE~P7QW=lQ!nVlV1r8|;6ehX>mTCPI)Gp~b_~9DRn(V2j^Ebf}XU`V)=L=9i zR#GX|#RYp#5u0N+HvVqS3z*JDE!wVKE{2hAF2Y==IlIYm5U_)3vg_IK+ zfR6=(W>=!+@jC=QK#zwy{+=*CDzcJJ5Dr6HybVq9K1>eagAPEsf-s+nHL)MhMEmt} zOe;)1($@7wb;Os&$P(^?!{6lav6ocO>y;XC_lii6&QuiPtQm1`|0)jOqQ1R;C_`h$RUWzfE^DEbLlgQ}nxB17q4SU!fN26CNy&~5Gc%xJXw zO5irI%BDc@z!@LHcCFFR$o|d{6=z2 zu*t&iCk7#AqlA8v=VBv~yL5ti0A>ANjFsenMa&%q@{2!FhGGl^2a~NRAq6-Z<~cL{ zFN_eg6aAIuq;B-ipfn!$K*wOp2PpARocr0Gopxc>0WZSa)l+j93G&zbMh{i$%uWt^ z_FY@IEg-KkYCX1>ht2jK99~1uf~M-Vbz@SAauw1+yP2Bdk!MzIR913~?)=NhwBnq5 zuR~tE`xHor<(Zu?(ajbb*6CzbxyY3dAOGI3Uhti9{7K2@nu`$}UBstTJgLJ4eQ7u0 zAV`-75G%z-zL}SzOn844Dh}juAr($W{!S>D<8~Db9&4CP{3SWc=YpB}>{VQJ*q%|4v%I@Hq8$0+lYrzxiuH+tl}4 zcK^h%W@VUW)lq74{^$2-x}#d*1Z$G2i4m#1SdTnqHb`uuhx0t%SCHtAE6&BTenq^Z zQL~(l*4rFYGe)$B0Y#LhFw%G+;xz>!Ldn+S_@BxC<&1%f-_S4~qdB9l3L}@%cNq!? z-r}2ws<9uIpx@?H@sBH`&l*CRAwFvg8;Cv)qtO@$GInZVf zOCK?Gr|h}N|Jo>Z0}E$QcS0iwv3X87f+Gpt#;OHjYf*eFHf4Q#bvRL;VTE^@At;Hj zfMEk&WmUR&{0WH3!(=TsVeVX>t_zy%w7G3b%ETi$Oce;$SL|V*#389fwl!IdeTRWz zw{iRWcD{~}q-hNqCX$2XSB8b!eElADK3W;kf(8sNk7v)OiDB5h1pmKLn$vit>PTW! zf7K#Bt|^=Ky=nyoW86lc%x(Fns$H=4Qe@CMtmJO(Us7S)&@JGL_k3sfV`@bgp1*b@ zH!^NXy>hNT{&(L@5BDnPoY-eMT3324t~Cr^BPuel#>`>a@b&j#H~bYUVZ*ntEVf{J z(6gID6A4JU-gkrjF_#%mRKO#~P}&eos*pbi1V#t&;Kb6~C`JozKCHoGCtF5#I2W9? zWb+p)Y8^QrY)i7%Ls}c|t`EB@}pX6=Km2oKpbU7%vU%h`Li>~*Eu{ZpCx-dQYZ$WyN`cOoRM1|FK0TWZ> zM45X#JnRsEFu;K9HBfO&E$xJi&pf6ChNySUI{MF*Rqy9<^oJ(h;GpYpoDPKt% z5W0U+7G<-H+Q*;A&9G+t^=P^k+yz{MW9LNtsC?wy`Qi!3uzg!Ii~oZ|*5&;5f>LU< zGeR+~munMwa)OFk_4T(Vg}Ix&a4l&SUXQ(=LsmKB>=qm98RgJa&#{dUc4#}4Z)uxF zwz(bH<*YeY(B8Ma;8x&4F`9T<$U9890-k{L1XE~+LtpD+Z~O$ne54Gi25ugltH4lu1KYVHveu7VCN>VvAmqu3^7(20AM>6z zeE0Bbc7C88;V;NVZ@K2$I3PR6y8;d34e1e>Z#b)AwU^!VN8=g}gkP#na&phxU#;^k zHmAL+lB@fQY?$y3N3fu_eH`{s{<~_o$Ptx1i01nte!;6sDwr{&S!hjkhzP##2bTOU zUK|1W@o#X`?=m#Smj#%9Vv21FR<$K2m2i+h+SenvIV!w0fj>(Y+m3&QXC42xaHJTW z*8aV+B`(yf~&d(-vxEd1n-)?r_1dgM^WE!|D7U;5JJeeQ+M^|_o{){QnKb(^c8yjKIB6 zbf_~LBL`NttU8$GLzkbIu&=tghZCZRJr@Wz&?XzUCL^HqEy{Lc64)_W1mq-21>m(} zdBeLNBBXGL1q@kqHvLWqwr?NG1^2LO$^rplLZ@iQ*GH!FW&gSNc~gZ;pVN@aZt_NF z70-cVi42D{i~7cMWC4XCBUkjF{vW8W9CQTD=OmOW(C>!B@d}TzE?msd%im{uE<~c$ z#9m{8H%r{Xv#1zBt?iY6ECp#h9eKTHSPrx^p=< zOd49Po?(Ks;wG^2^9TylcnteOh&V>SX9VWSH~CyF)fC7=LrVO%)?Mjgk4LBxR+W0` zm!mgHphkxbFnP=u_Vp~D_S_L{;ip0S{rPwsHo)9LWcM}!juqii* zFk-mN=ssNaclghc{s7_UbWi@@wm@oy4C4R0jf(*)Be|8_y_=`?%AQ7i`{Uh1n!m4M zVzJGaY0|!nU&PaHsVgAQa^9h?!Rw&E1=(Yd!MQ#+YDU~ghn9$;G{a)}D$bW{MsZb& zRYNJ4bpqyeL+2M~)G#b#_!F1vlW$yJX;uM5r<1G!Pf708ThEZ*W|iu+Na7pH9lTE% zkNGq1#K3oaj8b||rx9mE$+u6`J{ZQ;Iy4I6c)mSu+e#B`e^-NBa&%s)Woo4r%W35F zIG-Qy^`bg4K9`dEa;{oWJondohnYumQIeo!so-)(xb|1-gRD83wo!T$f6 zn_};f_`k-e0l4aYktJaWs~o?mO6)l+Jt&=Q$2Li`c^jFP2+1RSELas1rkJ6i2Uoe- zTl@#~!ehL3V@}cQK07AY)b#4s@%t`PR@A5YeNS%ZI5n*e=>IK5-oUN}!8L&5mpiBW9;s}Hgg#&7ROi%u{PuP~x9Hbqa^n2h= zLBcvUQ9lc5UpI1FpokHeR5LkDIqC4NlB`ZQu2I?O_PHK5Nmi`SC-u{{a2>q<;@U(G zT8O{SI~N-mx)Wu&lo8?p1o_)ag7K1z^t_XSpc2dqxZt)N0;FGX7sqQx_$7(ClZV-OQ`R zJhVItwtNfGlekhWjCt!%Vkbc)0Id3NZ?s%m9t<68*J) z0Vv>lz6BvTBck1a=m0!zC~qk5@WH+&^o* za%~J7`<=axT96&fv{Le0N~U7Zzda)TIpW^ShvA0ZLVR zRMffVEAf>NFFaDYDT4RSw$T5;J$oL8UE3upWJkb6odtLkzVgA?7AcBfNF=TETkDq( zwjE22%5v$MY#)1fLffCJ9)9G{hjy1k2Qg)f8@D=mI&3QzH`*8N$tn41gh*-`{Jk3^ zNK#Dp{8!H4g|hR_hBsJK14hGDR;8vjdcn9Cp;C*e|BqXiH@Hu7B?CSz8{XEr_Ls;_ zKw4!+@t6gGrB5zKDIrzxXNAFRa#5`@#1N9s777j)3mN@LP3R8Nd`3PRh6O@JQdmT` z$aP*auy05RfKC7LwG{0NPD;;}w37L!QAIfk74^x1Y~=`C7>=r$`6P@mTdFcGlOM87 zrjRIlhp1~S{;Z%BN_M}8O$l6ZT+XW#-K7?%1fy zsWg?4m$nahqJUO4;n$*`89=h6`T^nQa=M>2OkdQEY4^h={o1wSw)pEzyVUc-p z>@!qGvA0L7d^4yqSs;VSl}?BRgX-w-)`73)9vtE;3&3D&G>I#r4-QK$CW@auQJ2T+!XCqSZGBkO z!oMxJ)z`FA?$7ow_f8E9Rk50C9MwvFc%|5}9Kko@rS@79=E$mBfRRt^`w5lEguORp zuPfujWNn-18IqusMa)IFqpuNT;n$ItLed(X21$4-HH{dcUL&ZisMl&jeUVh@1tEw> zjAuK3xGL_NBF87E#XJD9f*_%$KP;ND9+-g&N)f*O%d-mFGpAWOd%4^(|LpTc`}KKO zuh+H*zPb+rnG9y|2t|GPOhb9~O7#58fCgTYL*ky}bk}p6pMugWl5)%HkRdOr4!(W7 zWq-O+-^-fQbbp$k?IAVN|8~B-(VB%tmKWB z%Hf1zqE5#s5E}rW=zqtD|U>Z*$`M(dsRhGEMK4w!{dGaG0u6pMC|o}`Er_s69Nz0(iquOFt0O++EQy_ zy!fNYZZ%NY$t&rh{X>sG2y_bYYeGC8DirAl0wg_ofM6!0DqP!wYY%aWJ3lj(wu~-q z5qD$I2M8K-P2X~EqF676SmTIiN=WQToZo5 zBz(EL)OCJRnjFG@pUs|Nm@;7z&UdP$ingozWpOLa z4p5UWP_=G;D*K19&e8>p!<{u zP`qt0Q@AeQ(t9|XT*-+N3u1nY(Q?I4G^rtkRcYjr;dotP1pNU3b?e&!K>`p?@8Khb zEhI)LJ`ma^tmG3}D{w_API&OY4N&D7&brNrA^!$(uld}WDRUd_&Nxier7BV>{ugk8 zP~%XoG1tKEjycO50tE~XIHp+0`07*1&r zA%wF~d6n}!g!OfzQJ%uUc8SDIc#$fjKY%!%j`u6BCXGIr`RlFY20fc~(Y^rW)!7!G zLBj8%;I{a3R`vn8;86^`fo^EytV9}R3S0*9+Vv7fp-Hw^tRFZ4ewThla@x5yV`bHoq5V&GqRb&%XVy-E@6NPd`>CAO!Ds? z3g+FK;3os%vhA9o&p*Pc_|n~YU`BQWNI5xtrG>M;tG5Vf1VJ zv@WNC5O0@K(ueU`uT1v0Pk9MzEfi91P3Tl@vTFft?NOJ#R$=YE8|iJkp(c=9MjbYe zyqwgWTdZ|wVD}g;d_M`qeBZ4(othOfe7Krw=xNh`!D#qa^|D9x<-d3MeoWzgC+o-{ zpZ`e7?Qsaog{x{=)APjk8MC8c`dSnwpxc7il0=!VP`uDn5Cfjqi~=78;;9!7K-%I! zS!=h6=qtCgUw5aNEW+N9nsCwkr4F+D7ogyKLrD+jB3AT_JAn6IFzZ`**z&w$)d&MQ zkgC$u+bHmXOg=J&x7p;*tED2WIA(kmiIfk)LFu{3%&t8E2o(#0^664%I1s+TAKQYg zKv~U&hY*<$&(_m^BGl;&`&1O!DYCf&#Rr`Ke!=5O_rtsWsArwG$G$bY+p~!#0L|uw)(Cy zD_Fsc&j2cEpV@$NN1JFJ7dXZ|uAE{#XcIdzV6mz!ssvezfIqd%1)Sjf5KR1Ap}+8o z^eA*5w1P4+zyTC&(8UoyLL)-|=UvPq{C-i}S<%I(!sLLdZU^2+lK0eo&So1GCqQMJP23{`f(jm!86o(0C+D*dX8f4RdI&J~*9eQJF>tA)xDSwKqO|MaZm44bUTIF{ zbEY1+>(J4%nqD}hELWGRPo$s}SGE@(9c^dsmj3IWn?M;0?0A0^ot!dBjH2HiK@X4J zFYq6zn5U`v))6p6Mnge$nt$(j=BBnsko%d_oDX2c=wkaf{TJwxGgygpR{I|xsD6mR zl7e)DOYwthmSx71v6dbakU?D3s@|#uLr;4!B z13$l&j}TSvL8ri@-xAd_gp$Dp)o!fhJWii?lqOOb@Rw>sX(G2i%g+Z<`{jC=i2XwE z93(~1M}xBcL_Q^(@m~9;=BLkc;QiCVX*kEOO$PN)HhaIGaf>;z-Y{EUrm2h3f-E-G78^B*M?nGUL$)4w_-W<4fc|RlhNKm zpJ=%NQ!+gj%NM;(_-@-d6MTz%VDGp$p4hRAG=O^R{~XxZtollh$zL%QP#ERR=j6w> zU=$HlvKBg0r`aKZ)wsZQz1t!m*4hIS3A)|t=b$fQtZFRsklwD2j`LO{Hh?i+nw8S! z<0D#;EAg33q~9I!L|_-_dKj9APb^&<3&%MZGR>h z19ut&G<5jOR{a{Tknod|Ix#&` zPVytQm46=VkKEAe9VeSD=Fwf#w@`_$mb76~AIS7v4kWxT>KNo7fzWF&Ta$6l^)>E* zT^;Zl5Z`LfjU#F>j^p2&bah%N<>?ZSr2c6NX`BA_LS-P=2QnQj7OX?Y7NtxsIgH2} z&D8=THi5uNsw09)4-?zJlnVvon^lUv>I?W|fP?~4RDmC>ka1$1U?x4>)5EE6FUBj2 zvxOS{? z%jNqqaJVy3AoKXd)-w|gtsNx5=b?8v)blD7yYWw~En49-TurWPYK30D_4ZvH^5-W{ zx?5`(PB|ib?ACUI!?pquB}b$&qRu`>p~Rl?-kKEDkqIzMvNDU)P4$e{yE6RvBdVu zwt za_G1Ak>LKWi{$ir4C;VBbnWWw#nvPWPTSv8J7ofOIiCDIE&qiuy?p}Hl$vnLo>SCA zGc{^J^*^Af`KW46*22}Y=^9fk{*f(I@r}mp!3(W587SZht!ymuY2eiPfklIY1$NCq zkyF%8&u#QR(wsBkSMFh7km`VbG~hcB`9O`tC;oM?`vfpgbOEUKX>;SMyXAiY8U3y_ zp;Wdi7St?uVtn9Os#g|4 z0q-H)j6k+^Or$(BuH#6L>Cx*c6jh95se4o(RpSwAj>)I`1vGL$2R3{AmQjZNiX8&# z7fiM(BN|`*D{UQbb|YNRmfe%J=ukZOlRvyYbIN{-EwBtQnkXv&iw=hRK>c~E%Vkev z^uo#(F`Tj(jTiW1hHHR$rLrLV9gJbRQGI&a02VY9`HQKLEwH=PGisC7qX4KmOXQP^ zZAZ}mf&TF?-wO@&szN&hh1)}=1#;^t^A7mlQ|U<*sS9)kug#MkoeKaWa$APd+nG6c zVty8QDsE%Wo}&F+i?wvX+?Qsa2TN<=N#GtV-r~p=-V@$DuGN7oGS#oy5_6FVq9VlM zm6OWI=u$WEznWTQoibwLDY@>lf& zBOV{}h?uQ86Onu~a?!6SQ6tmw_AA6!SL^#d#aeG2Q!qR*gs95WWq`jGYmpDvV_pQa zqkN31Mo@)m9nP}8Y{dl2NSjB0Q^3z! zx(d7n!#+0G&CV^`-D;YKnUbwXgb#`5`lH*PFM_?bR!CajtJV@Ly5dA-8<3Vg>be8c z4VkS)521!qRa0fh$)_f!j1%_5j}ihtnVKev`Y#6bRF%6A!?K@GfX)xZlhhe~v=5s! z8k%(vpsKsU3Y%T?#la`UA!NXwQV zks`dp!CxjIIT9q?`BR}%o?%`GRzd1DW#gkEJ<#1cjz3R%ce?E}!IF8hYF_zxX2{<= zuyMx_UGY@S*fe8=>>2;@e((s&8L^Q1H-Uiwa1`GpM(Y|JA!37W3JD=ONk z_s@~O$F~ycW*4ZUQ$rw>ymz(N=;e5!pF4SEVO3G|*k=YSR~&98ypOOGjSeo9k~J9r zGt--hGai+So4^zQT}lA)u2TIl^x8E6gM?JF`-UD`D3~`LYfANk_x0`>|LK}ko$I59 z_nr>+x$PCz&4#X}Wr9i`+YODMy5(aUlFEAO^=jd!e8;z8WijT&#tCt{E-o?&*hVJ}4BA(t#5s6# zjGNN8K*>(REbszL?9t|<`n^tY)q!kJ3Kzx^3mcr}L`*Q9q{l!#`)L}`DOTW+ zY0TPtK?ojn{k);oPKhsGUsePXDK$z5^lZlg8++$TD2rE`V;R$*=4(XyCz+zO8|~*6{CkBhPldt!w7jSZ zytZI{7latgSb?v--OP>SDuBU%2E8ki7PkjwdVGuSCmoLT0`R(i#mvE1HWRA0L#4tFKvwi8jp8)l(%YROg@APK>Dlm zxumh@QL#_5yqmS4FnmFh`1W%_t5stJ)v;_TfEJV@dK-2n_fWNEKo3}p?WLJo(R7Lp zSnAYFmpuE${3W8ELH(tkru>r2!CJfJ3!yDV_IT zkZALv&RG^pa6K(H#BnPnEzAT2&z5tuKkhaRlhK zbpbPPLB*B{87;6gU9QgkZ@9}0w`e}I+eMWsqimLPo2{`aCCoi9V6gJ_91Kv}^PQWm6Mk%_zDTsWdU@e|S|rBVbBOA6DlIbKr4>%J)&91dRQ>J{?Uqph@+Vy6Obl~;hLm!r^04j48UGP<>C=kdOPE^QblI@tOLWYJ&su^CIlVNZ3oPaIS} z>f~K1z5)J|Z2T!85&sUq1N=J&XLwdP%FahB$QOvaHW_(g3ae|9K%5WoL{SsbxRvUAH{};@Rt189$QElQi_7v5>X1oGXoX}ew*Pb3}p5UsksfBCsdxfOQU{n8V zHU2;qL{I(iJSr*m8ThK&U}kf0_Ts^3olb$J@IM-NJ>-uBj@%I%6)6od+ua5k{{XFE ztaHOiw7Z=cQi?S(j&qYIIZH9>$orbW8G|T52G0qOtA8`xL(lZOs#! zh%4tNRy<*zYObIY{d-5{opd)fzd)lN1HS`Q;`^#vYw)REOVxzWSo8Zp$O6ERX9N+DOV<5)$cYbsj@88_yH2;%aZ(hWt&ukY zyaKe#ZutPP3TJPJ1vLHIDssD#S{UxxUg|qe;cWd7*;U#Jcf7Owq@Z^^#GRiMkmM&u zE`Ov{Ure8FT?9(>$Ys8DLXchuqQIjbHZyknqqrQA{`Q>N9utzOOH(B2TbF-X4Mldd z>_cl+*||8xL-q$K>{JSAUy_QdFw^VD@a2?!`37m!V{TE^`9g3x~8K~0^1;X`Qsku$M8MkCw zuIkxr+o5ssTfwMY5iT_G5)w+fVg}Y{_1NN^F%Tr6A%7dhd|VVZ{g2=SquGm;{%71= zstpJ>2hbsCy!u_<$U2FERC~_=WtXRYo3|vK>?Ez4?W(7t@NPfmCWo?q;28XKt}4v3W~aYo8z86a13?;>}J z;JL(@v}|#)tZ5TnifYr{?gtZW?!x2mnEQaq@J?}QO|RwB^lZ$)d%J;Pv?fv$cek%= zJJ)t4^H1x0jZ%f-~M;;s&Tc3LRrQAs=*t*aFJei z$_8xrilcI8B^IumVCMKX<4MrsZf}bC+@o6+a9FT%5i9CQ3IaWg(9DG&89JdEbiX00 z!E--iLR7ZmPw}ffh1iPa8(p9{P0)NQ?kyIS9BK#e+Y@{s&egK0F*2U_IV-|u(%B|< zJXb3uoyVu^$s`Vh5xwtZ#mrwzLAuBSTQ_{5YaM*ZYYR&7=g9q2&gn$Ta;@crP1fPT z&AE3_;aQurTIq}oc?Z=!|zP)y2*tc82_7zcYr(Q5PSriql?nezZS}6CpUrilO zGaOFGTz%hFZSQR6tO8TKet!`W;+V2@YM7ryQ`7hwdMY{0an=}p&zS;N;o+zf;%;fA z@R4MQd=RNlE7;t8+yRkIzJ-Jl8$Oo0i=H1W;u}VTCNl5enI8$ z9SIXgEXyL7YJo9e>rxzKAKi)H88IUqQc(mnu8DhUEdqLwj>V7 z^}6sdNrZfA{vcRZe#DZri2ckTG=R2V3lBJ}x_I-DDB*S=L)la0B=((l_5}%Q34d0a zJ($5I_s@DQZVh~m+qVp#Jv$o_ewEo)Dcw(b4u7;zB1JXf&7dub@2pfqxE~)(=DDpF zri%30N8xQ2Z0&`j+C$j`dZt|*sSD3y2Zmq(74K?!FgqrckxHHY`D-Su6;M_owE_l>i0FQrDeTf|F`t2_jH7 zpYeMUwWL~qRWCB4ZjT^;PhJFyGUkqmj2+}V+dPOs3^8xr)jR?y1z4@j; zGTv4GGT`!^)yiFtgaKFETG-4Kz3{ofvfVP*%+yssFK{jQRT6HwEJI>}DR}KtVpT*P z@zbF5l8b(gbS&{>HJyymtJV4^ieB%%!~*a?VW?kwVElEGTxSJDr#H!M2mnsfKw+>4 zh6RakCnj1FKz0KIUD_Xk7*A1n)9qB>%(&6K9u>%0`?7C&!>DAgP(qU}!WF>w#eziM zg>(WAukG-hRI*l_p0$kO>?LFmp!cPW?u~QQozAuKIsuT)XQ*6)F6p^AERB?SseR74 zU0b8RCtUL0!`(vj{&i1W(xqp9cPTo~Q4Q?qxA;ePfA9DMZ*$p_=0M~unf8|2l>hOr zVS`g@lU18)>`G0DPIvOrz7u6_49EwmAXlh8>hB%iQO>E+-WF9!MHqTO8O7Lh&^Z_! zoVOs>JB2Ac8;(_9ke;3fmQ%*aVqT(>;7g%lzD*5s=tL{vdFJ)*#klcu21|L*GDl(F zc+_`x{zoaPP1p(juWEj8I_~0!8i9WhXoZ;|HD^7SU??9aKQPLaU17xUim7zlNY*|l0>eK$+3Y3qkqT_ttV$$X`e z6Jgy$%HH+~i8=|yOlPF}uACc&Qv0EuHwJ!rFtwc?z4m@jG;X5OuI*C9n*C6f)hac= zMuQW;Q**>M+-bS^lsXc8C46S(zRo3|=sqsMY)=N3@;C)L$?;Qos>V|nXvHF~?`re!aN-Wxqf$;%<{7YMXMA>2tU`YT^Sf}O5cK-@J zTy=0eeRnd=xCyDZk8Jq3_!6Q$Dp4=H9%DF;*gvY@l!oSV^2kx3y1Qg#&plr?`!y33c6FER zw1+eCVOfKBQ_ZSx_x+so-c;SnCPDOPL!YPX@J4OI)qS<7U4_r5t`zGeBFcUhL>p+` zEc;oIYA}My>?|t(X5FcjUkg*q9T*3!)Q+G05W$NdX$D4LLuv8N3Da}|sx5eRhKrqv z630NNBnT;T5+B-xcY!Q~?EgSsyu_Hn;8|pyzzH<3xU|z=2VZWZCsMqWd|JDvW+!)g z8uZr0?{t3|?#eX7`zuKInQ#d?!kuC(NKLo`CTC+I!(Vn?8py{Ul#ilxK>r2H-UbId zC39UX7U?7$EllWk#|qZ3XDnpnz3LY}llP{^{uwJ;iSwwBaxxWk0_UmwQgLs-wMHLK?^iV&Exb|Kl@#QwBtjg{ z?VYN>`1`PsDOWU**hmo^0NX2@xQpBdb%|$Wg&?st5M(MPM)8}8&!@%n<`c{W4dl<^ zc)xMv;UO^Dw##SVm^I6dpnMx#%ED@X^VIwV`dwVaxrNyAcKS3(!gRK^%_&wtF*E2~ z_TItqi|=A#K>;y@t`~-HElLMzK_{0>OjOlE8yIuJ^oaI`SlXJU>MMN*w{oKv^crHL z^x6G7SLsXZVjoXg8Q668?v?$%b^d$UzX)%L9jHRWKV3yor*MHwT3r|I1SjSbq%A}} z&zR1;H_c(o)rAY!@jL65QK~c!uj3)d=6pE+XtN+I?c_Lcyr(W#&{Of#=u04{@)Tn) zu-SnH0qv+#fng9egd^sNC#T!XJ;vn*8RhW-48P`xh>&Cug`_c9Z9GQvu=zYj^RBTU zxH4Gom}=sA_3-ba~ zSw5}Rxw_47WEL^CdcOk>V381=j{gO0qrmLt#{KZ^OAY^h_;Y~9Clac9Z;FPw>&5LZ z@$SSuxS?>L!-?=KBc{ow4P(vtVg4wjzukw*uTmdUlw0A`Wwc%kDF#~N!)CtrJ9e%) z@O!Y_G*Yi6bkJu8J>V{2{8oi#XTf-B&(SK7!tpU1xVBewT>bpY^KZXRGMwG6&aTAW zq!wMpmpyPT02%64{eVn$Rj-4Bxa0gQhD$ef)vhwXOKyA4p&9Wpd=LkF=9GhfQWO7i zqiq=Sb+i5JoO6zSd}GN=`)WipT6Hz67y@FMa;rpHNY`C5AlTRflyY{=iq~*e@L}Bn>|W> zUDQu(AC(sBy|jsPq}!-jEC&H=)$ z=j*eNu$rHdKMA8d^LiEWI2p*>;bM17jYQLZ-!ma%DyDuk3dWY{eT+x%3xlc#zs1je zwlMq1xpdbj*rR~OuKbrn^jE4DJeck?Uwg1#?N@X_v)gRL&GIv5-nzpp{$f$Hcc2UN zF)c_>{&szyb?eR>W9mTZ#S(d=4I3e0oew0Kwd~Q51WgjMP@z8bXX(l`VH! zNcS~z%gL%6^afyN1lW}r3;&$UHti`bqRv93{Kot)$+c zc!Hm{N|yWtxQj&Pb>~1nTqUmSDxGU$^6XiiU_pIu7KfYyw{D?dam>Jxkq2+zVP;c) zhnQ!5Rn!tuKq<0Yb*D- zUEO-%$k&en@>WmP8jqyLoPo%AgW#2pRuCYWR8Y2j!P-?#tapA?T+0Qk!&Q zS^@4|{D6B{hflNjkw)(tQ`5^^c|QB|hAzZ79B+wq_VTEHR2H?C*HXy_yUftCsw@ao5pz52Ip!z+8 zlbD;#{>{jH51$g$&1maqS()aco_xZP#qh@4uePx(Zsru@Fq7>^jRvtE2bM-oBksKO zMur6qWO_%7iRr~dQPkmS=&|R*sCp+S>@DVtsqDt1t34Y8qxn3Af!qQU6`$Kr? znrOeLFsqjuZ_v|1`o1tV_5O$`m<7a`cKN{3{-tAJye@m- zYR1RgDwTVzD_ldIr(q!dLkR8Jg>m)6GgM1*qgiT4fUI)(mY20yO5Y z1TqRLJ2`eAUVWafY%77Qf+c1(#qC|O7Tj-?KHcnUQB@s{_^bV|*caY8Ff5$Td${>< zBI?AZF_T-p(3VU4gYdo%-r_$D09pcBsuL zX|Uu7K-A=i@y7ruXmDN+ZiEz1nNssCJn zIdQ3%I}C^INFQlleG^H|h?e#POMH`yfe6+{QJnus-0^yAl|@Zgs-u4jPvdpb(uF85 z5Au`)BCz<3i=F(3ufl!s9r#osIgemN;1C=|Xq)X+tj^skCE;DNc8y+7!Q_%sd2=AKfUQ`0`q3LRJaX)_NMe7o--O3HiRkUKfzHos}+{5u7Yjy? z@fYuW8_U(JQ5(%$th7bgXQcEf2-H`<3fTaE{GO?RG+W95Vw|mJyS~|x5*qJ)TA+|z zB-Iqa72jjPF>>pN@4yWmdzvfhlRLkW`cs!ODsIzfMs%h+t5Omrf4;iuz8LX?EYGy5 z1!*t>qI2%b8i)y=r~QXg^&C{Jj~3qkja(xX@J$rNzl$Pn#|Xng0(`q4}$laL(R` z$!#CVQznrvwYn2@$D5Qbyt%%Tmc}a=os$_Dfu()M6CKxmj7iJ%tkbX6>NFf*+pPHa zk5NbctdnSNs-AQHV&rSudTRh!x#J@c#aJUK3CyPrkUB5Cc^3fzpo=X@04|xg{d`nc zJ_6qbBB4(KF2EFn2RMHJ?lp=%irfnoa#0H8T(}b6XyESyy$u-eJ(P*bzsQJ-zGhU$;9%LzY~*Oc+(VM#~vh z^t0cMOF-_+anv}sE6kcT?%U&RhOo8jVg~*}9Wfefbgnc@y}xK0=+np)=SlExkC)^y zVjlv`49x3k=(e?m9N<}c_v~%)C0U*{T(S$Ni9g1Ne|P?HqmVclM!V6G=-*#!qLs#e z=AR`x>_q;4sSulfF(IX+B=g$gQWKj*^-sjL{otW5ixyVb4Od&a%(vTsQh8f+f(3RN z%t%-sxiz;@L>6btXv`=J(!xO?kJtdr45fL<0yvClbD6WjEP_Vcgj9)`g>6J11BUR( z!@|+xZtEmsv-07o*Wtt3{fn=R`0=MWz)Tcz=jp}We$9UOV^2tqpYzS5>zu?41~veT zcYeY}mCjZWQ&E>)uH&jUta-z)KlQ`p5eNZcf@?ni5}E6m^F>h&b=^I)VXy+yY0{$**&luHnSE?0|S6k zVAnpqrY|`u9|5_^%6Mz&rm&P9p%3X|B@S`oF!D7yVx?AD1SeSk`-!%qp7;1OEj4G5UZi1>&Amla? zpxwE04(~52T^$m|hsZin3Ch~y-(=r~SLVoZcZ(_j6@Z%u_>vKn<57y;#{{S%`A<_) ztRw($DQiQTOD|7$pW;?R!y>W(WP#{GPD2E(sqVBajBo=E!^~^Xap8)eDJO&rq!eXC zpEoIan=%!SJ9e|+qjz=Oc%u(&_Z4<IjqQgMfRq%jduh=!DySo|nnU0N%S1^cCm#iR-qe$cUIcv*Wg+ls7f587)WN}8;`-N|U5_={M4ZRkSVo)$`3lqQiCm=GO? zLnM{O$|7uH!N8{PN;cz9PXp0V=!?k#S1J;V@!w8Kve!{Tmr%T|s_BCcxo|U9)`k0Z z2qE3upEbqK^e8~z5B@)h!QK10!K=++ErthS*Jq{P!i%i}7oP}OGtv?O{@QZ=FdX^p zm_^_KGhWVrx4dX4LK=wj|Sf!|ZRN;@mFSr*vz?Y*9RID{&7 z=QZ#W#aT58t}s>Ov`>LY0?N++l^*SD|J44aIc?q8pJJx((QT>3baAw=b`FNpEd#aD z>Nb(#yTl8m69I@p~0brpWrtZd# zuAXDAT(T%d%Z@qaqj?s# z%5h)c_c{{$%uq4wv)^xv$9qMUMc@s%O?XfIHvuLeC9A8m^?novmJxJ?F@5g%K)ak& z6iGi2%FIY9-T%(6aLZy%vE%5JS@hmx;!KnCkJ4+N^tV32@fuoA(~8Qdx&{scN>Rz7 z`-~AijB{#S!B~N~`q8b*Zfc2M)5Ax9EE#R^qI1MaozFD5Q$AWp#>a0hzBp2F<#xG8 zEl1TjAepY{FCZKR_qRSl1)m62iiY`K(?fuRO5?qe)f4vM&44}xg;VEs79sa9$-f8v zup94c4L=cK@vpdwUIb*;6c+*48Xy&H8Ks_&KQU>;+;>dLM(g{Iy1sP!sjN46e^5a^ zm8_A zIS%Fg0d@B8eLr-Vp4#+zTc0fs>I}1)@*-lhZq@UGDh`#q`}MyW3>f6l0l%-=_+p?@ zW{19Ga*2}aT|px+@U(|V;7_jIjXWb?HfJ$sH$tU2j2O!Hgt;jr0u7MC9@SP|fqbgs zaVm=8`{K$ske@~Owkp;PsMB%D=rHkIVa$Bxx@L4E$T%}u+SE_)w5*D}AP-mOMf^+Z|_d>mr3my9btsl_Mg~HO!CSZ@4mO2_k+{{(5T6vh+A0&|utv+~m6xL0Xptr0P^{ z-iMC;(Z^>NqyE+FBp+pVxUt}h;Fb>agN6nd+J***d!(oI6bl1INO}|vlhy_JHO#%3 zqn+y8Q%8@yk3ah&Jv~0I?MyYfgvbSK(l)TZgvrOh`Q$kViWnZ*;tm;Iz6%_beLuB| z)PK9c{Aa)U+asy&Ga-rOtZ#zby`co#HDb|+Rx)qIrpz{fShzGn7-$U3Zw{NfZ27IM zr&}u(b8+9$DbK3$ayNE|L$o&DlFqibHSuyM=&z&?;5{(al zup;uHo;?&LvhWcG=L034gy;5Kx0+DGvP17NPhspfRoByTME|yl>F`n*nEFSp|H2$= z+R+S-#;LB`9v}q+=w5&B4#TceMZw~W(2iaU=q?{UH!U84^hNsOIY=Ooj9#1ne!8Vs z4?{o+(S-ne_!=s#WRZ6OuJZUu5+HPv-yXpb4hoBc!v;G9ZH@77(6*oQY_C5gu0DBH zkM3u4(vN=MM($aCBsI4-EU{`*e`Y*U0W+ca)x+<;ouGTuCO^MvE@<|vXOL(3alZYY zN(GeQBd;DjS@STt9*&=_&0+VflYvByf=WQ!#>x%T}0<8J#* zlkOEE4jBcUb=vUQ$#Lla)ct7~vFy@pRIAUB)+N18d zE*>aA?gl6Mu6%@GEo&lP@F_<9(5qfwX*l>pqEuIOg4^WyZtCi+*{4CD>qr;j&>%e5 z(YrPEfvR%t%?G%lJ}qx@noY45JHBDWutpJW>rCOhN%yUDiEH{i*j$Eye~~U4eqPln zJ@AZ$YTHD$+vT=1)$rDiWVX}jb%N3xsmmX+Ocjenyl&H5%xu-E`ukzs)4>G8wyv&% zP@5V4qq#1`>y$ZFysl^Z7G} zP0WGqfL6!8%J%AJ$rGNX#OTtnO9OkR+Oxt}eV4YsotZToq*52~CX&-&?C5~O8$8=W0z{g5i>Z`_Y}9VK9xe%7 z3`symtQO@E_ox^fsiS>EZR2HGl5c5`MbpG{?t@5SO8lu8{@W1;&1+6eG=bwO-tt=Oz%Slp#&92O_X9`IwT1A@8NA4**S8B856 z))ArP_q#fNNl^xPT~qgYUa2bN?^m&`?9~5Fg~40-t0}^0J@V01yQKK&a=3%x_0xi` z5c6MSX9PC`uU8aTE+VagBhg6h_p%DqMdA(gBL;gdc%W*Cy>K0r2piaq`GZsV=q$>z zo;6blir|g01V*aN?c|$wiFR{h^I4lm;Y(8wZK|p5@f*{&=gb`2+rW0p_q8$a^F#+j zgEwV)jKgt9F@ipR{OD6bMwFU9-3_G zU|4hpE-i`SKT4y88 z9*GQ?rM-$P1ZC0DyTp6=W0Q3^FA6h8U~ij$ZL69+^(3H}G|!BRlc$)`%E^5M z7vQ%1PCgWYkhMs|g&oeR0+b?jbL;NH+=;--5U4I*q!b7ybfQWf|Bflvuu`JJ!Q2?poYM3@jnew)Azfp_;#H%~XF7*{(;+Y9Nx%i+6(&Gs+$Osfr$C8w%oPAj0^i zHRS={blTj1|lt@QVGKfeM5riQ^AU1l*Rw)WXMiC<=1O!A8h!7wnh)9Q6QV{9# zMkT(I;#thz-}#-h_sk#jJKvnWzjK{`;^lQMl6Sr9Sx>no&=}TPxQaft*K6OEXV^M08N_8JVP%nu6rm-& zRj^0+Bb?89B3>hug#ZDpZv7YFp8{4VHc4VCj@TaZ1r9%B)}!PJXI0=>C;H3^(bdy& zrR68E=6$MJCpEV{FY9y!!yXfF9imQ_^_98JJ9=ZQP6qBrzUyYXXZhsJxR<2l8gvfg zCas73Ue?|QAO$pYt0BhV3loLu&(?Ok+C6jhhVu=|p)VnQNsgho6yJq{e`Zfxymjjd zwwMOFf$0qftImFIq*SL(W{s>Os9VYKSo{Q0IY`hras~VQ^E*)%qGFT*9K_Xy4*O8k z7M(QqI5m$T_msj{9O@s_T54*NXMK{Ly1RWn3Qp;Dx!2sXvoI*Q?{YJ6?CQynIpqBD zYIy44>$#WSxK2M;J&Wm+Q#sKsQeMlnA&r4n0RF9_;3WP~GNavZMLL+aqRF@X&<*{((59&lWm4D% zzZxQy%xIvL&`4<>2yM8L@YF@HnG(9AJ`DR~p;(FsaarD?YNQ(&pF`IX=xDXXF{~m= z4P@n0vQf%Hrvbn?MMxH-U>COWYCH?j(0b;f*#sN9C0E%s&|4svrIiSvLp?>=A-;M|L>ep1>U7|x@g}CE{ z(enTjVCnkI&`q`6Z!E!CaW7$G;#L5A40_grsaM4+SY-faa9C6(nTogN)D0pnOIe|a z)hG+R^)jkyrcZMI1uiAbxh_m3e~{O5i;Jlh)toh=2vs7($*)POzDeC&8W-8k8_6@+oi!K@O%oo_VCA4g=4Yz92D^QF9a{w#zn zNJ+)Z?d+@o`~?RmJ6;2*gV>IU2I>CdMj{juF_^M4x;dMj^_8 z(8!_*Ap3Hh>p{RO32>%8@hUAtv=CY*tNu7zPOJ%fan(_Jb-WP+!P z`&$%XguH#Aa(zp3#QbZ6y(cw&tZ*F2ENUq#u`Ns0EaBs^ z^8+R8(@ENgx`3heB-3G=nCY$!Mc!VW`&}j~oXt*G2KXwushJCyx%xwWUC4U0AjNDBG5vcyjt!iC4-5t<$2)$0LDg|Kx&I`1aALGgP-{RM#?q4fYQ zcp($9(y&DmOXs0oMR`c$andHCcHev6dS*~7W>tuIBjnYFPAOJ|nmeD4?~HdHGOAae zUsxz`9r4MX95Z{ZEPT;sD5`_=`!jYG%1HVK|cU~Lx`_Mo50(N8J5gAshys2 z2T)P#oB8Wl*}0IO)2hirxn6q94?rK=;GD!#3j&9?xD>kB624{*@Q<>Kwc&VfgzU^a zGlS*RXKwqeIlnAXLyu0g##9 zZI);S^N5TBG{CMqi33BVnGp6WLplM4mRBqq0Tpgh&NC8ocy-=@@e^x5ne0 zo_}OhKS)A=!9-1<(ZTfZ^(h(4`+A&Wj&61{mG6d3X&W^CR(SKiwq~48Kx4$O`j9KfiBfQ^t9x_y zSNj5u^X)FP8ZWTbm@C4)kUVGd841j7XwX88FCBrp0hb<3yn}axUBCAV!w$ z(A))b9@YSd->rkN!JH@|^y^=24qz1P7JS9S4yLH>+<6M`5GzvXgZ#P)H@SnuxPsgYuL@7U+wW%rG)-f3 zTB;z3+~eYQ_jXP0UW%fMo-0N5vCzZE^lFK^VR;lMz>fH_*ItwbC=V8{u&No)C`x<+ zyfvuImLs#f9c9AHE_pD!3qIZL$wOo|5x4*kr`VXM@$nivLMFHc0WC1=Db8$qDI9Ox z7p-A5Y!xj+I1J3~WY-g?D869e%Vc@riwFk3Y)=e@Ha^Ulm`KufBd`7JcsyrnAWPru zTe)M5_S|&at`B84x!T=5uICGLip|*U`xCAn<-=vJ6vyimAIf~{9Xu(njwYE3P_7J4}5-Z35>h^Y?ze;oGa8 zPQm6?6Izwgr;RuIi2FuZ8i`x*k)qP&ay)CDSQWpO_g$iMAw8C`x(UIHqqh+x&G&#Z zv1v;m-a%lE-vnt?m5x)iI&8d>V|0;6sH0)U+X^BiK{6$-s53ucGs(qycZtsPpJt+lQ-*iF>C4$$S;o~->8uO$|1iG)-h;-`GkYCl^v(mD@VBhP+e7U< zrn>aO-QZx?rIS-S?64ZDxJpgUri8-MV~&b?0&E4yVur=Vg2QFsdba_73Q2N%pP{o^ z^-A+scud($nw(exe~4$_PE12=6sv)ZtpDA&cx4{?GTw_#TPNHDMO;D2PJymtu?o(i z9`nQKqp}TG*oEtwPapAI`t07^IC$^!(z}Rs66Ay(dpf>@A~+M~XEBV@Kk^{)%~w|K z2imwG%eZ}MO)i=)t19kpG`dE{Z{{tu?sxq8*TFtIEts8BwdjY$u^;%$Ue7CAJF9xA@f@LePuT|k?KPMuBKD~cco%?&mV(S z*Gf9iuMq6d4K9z-VEg{fC@Om9)L`Sq|N1gk>fzSXT{vN8BBgb6xl9!Lztb()e9wdrqR=#3L#z2 zW+DA0s?m2+B9?P8AQ$};T9zZ5Lg8+Yu53D*Wr2`4n?RFb6oSmq39#Jol0Y+!BK{kEoa!RG#hD-Q4dav^SMouNC zoe-7UC|4o_#F~c#?X`L5?Fv7*xMsGT{SC6YN^~1#fVU7ei8sSf+tEtzK|m!`FyLZS z#Kw4^=KiJ=cY>K;FE%A`WTk~>(8!fV>XpV~5@(gTRovYKA7Q`sd#4e)H&G?V;8@-_ zqc3BHN2t1m;FMW3x!2v*r;gUf3}k^GV^BMtlB?xO{%yS48k*flMc< z1+7y(Okfu!t|;VR=8+k+AA~qic$4-K`T%3~nmfWETtTcsZO4VRBBaIEU?9Nj{^(Sq z8V{Ro?heT?vdvb+aOP#(m}hCFFs9Q_?UfMY&++xe%7w(;J|F6oV1^h1iQR8rX6H0J zqPj!BJb2YLCGxAM=-8D!uQso+yIs~*$#K11y67{s>r-0)RZezh3-U~8RF{~qS?YLm zOmvbs=5a@zuIPBX+^#aD?e6W9H44+iL`&XEvD77q#APr5Me!Lxsz&B#!M8YsBvzdU z88mZ`@Grnb(2|PSh}tCF1HUCtn=rj`Dd1{m3WK0*0T&c_cScZK!zjM6y*->2z^`F#_ zB+n;ORToR#6n*7VDUWtI$!Av(Bl&J+&Ss+9*15`nK+x(|yiIubSozJ&)7!|k#GR@p zx6i?$(Ytca!EwGh?$_~=f`G&hv4ZGf9qsb2S(;+YF1!-JSp3ZErK9W6Z3_te=6sF= z4@KlDwE^Y;`xLVa5nvuRlO>H0yc1?)I);1dcHPOBvKeCGCrI((?^nQ+FspRoO%4~c)1H%&klJr^Jia-L3_OQ zc{iewmLz88Q}#BGkT!FD_p1SJV=qbJ(m699jva|x+`K_Kn0; zba>w(l%`N&AjUXFyyfw${=GWKBa&i4gI{>eyr%M(x30t7VY7ov0Cr1up85EVc5G^^ z|Ex=vO%!I7yKD5&K&f+XaF|$ts*q9%9 z{7fWhb#^MgFDsW#Q9t6~KjY}JrNV!5K6s$=NpmTfkpy_~?xLbzCb$M^P$9nnF1?3i zaldwAffEh@NvWA=85Ej2*G+}VoZd|F+I_q$9P9cBDfnqClhkgU>@k*{Wpw-LQW+Eim%#gtb2)rdqBife7z&QM3!k|er)v9HtEt5(jm zz`_2tM`Z5tgFZv$&i9(7dxkrjj^7(>&eZKheEPUC>x}1Cfz{J4{TN>Ej7?;I4f#dc zE3#cqz>$AN+qv|_4=vb_SAeMVI*`sdV(#mBk!`!G*A6B;QyHhf1=L62w@nEFin%bO@0KLId z9kufSH|LIyTA5LplT$~#AU_YomD+mvC@_fz6-Kj7`t&0vZODh z@ZTZf&qf)`EwPI{00~mdnf{1B0`cng5?A5_!es@7cAMLf(CE;|M7?ACI3mT8^FyQ9N^XpH;KV$8CrRa@W2?^T=ag54xaVPg^~xM$N@at3^#Atjr#E}O>0|wT z_aJ?Maj*P?P~`wcP|cpy}d8 zg~EdZ9RQ%fXd|Q~n!z&T=&L@|{0h*fIR+LcMckSC~!5AByZ z>M}3m?z$CEe3;K`zFnG9W@oOkBLp1e5sf_@uMT5a8~u!UsYROBj}D-YDjkJC1O0*J z>`*)QxtZmrr>W+=UEkRdj-^6ZG=tlDxOKZ7C!2 z{&bqa#b>zPyShCUtaU#LX63S|&YooFodX}pvZu%O{&`ydO;td^ zr_QfnGFpH}&vgL!x`0qqnkwL2? zW}72ch~EL9bhRp09-i%>$;2S!;97qAAX3HhYSYnohK+sVO{2Dv?1Ym`U%iK^(X(pt zQk2Cbp}}>9fLAfO+rECl;K8Sxud<+v8ekj{!{% zr^v0t~HrBxypteKJVslUxIOVTK^;%H0PT6k zTpXeI7&FA{cz}9SGYqu}c4GtX6Q5&MNJ(fwutvtX?VViutoLx!&oSX~>pnE`)>IQN z1Y~W^wz_-tT6>0iaEWwkx}w5{XS#^o=)MPee$XefI} z==y;_82Ptciv>+g7&M$2U|xc*Y%6K+R#86?#w~YrIB&0!{lX--<{Ipu7#I0$u*tpb z(_==lN5CKpqK*4q%()wc_3L$8I&^+s+d1}SEqslJa>b?N^2pe2kQ^@jFtid;pC;|TU#wqh=z%;QpyZloppxn2hYiW<^pC6$XcpZL<%Dz=(9A-*B6CW0 zRP67 zyQin&DpDlLD8Q!G-&{O~^)5WozhG1s9~{*G`bhljy54d^Q18XBtRJG$n~#OjGzr17 zz%4;p@?u6Z@%EsP`3jGiV*d8ri5_wY^Wgmy4tg!#1`0|>YCzp>7L7mWht`DnDMBr2 zf#&V`s<~gh;z}nbz6jj%YjGJy!CqNUuypC`OO>G~x8+j(N)0s;8zx*{noYI2lt{Z$ zQ*-^S2Qr5@zneI0cn;!Uizp~nH5x0+WGSHx%s^ z?#SwJ@y2_@2~=!ofpL~l3y@#8`xv*+DjRJX(MdU4-;ehi2j?Hv+s3M00FG=RW^DE_ z50Q;W3mm{iw^)9ew5q|rlc3JRt3t{358$v?lu18oH%`#eNWGk>g4@$7)`*`!3nD&Q zI#qIPN-P6qWj!fupRbx3j`}s`tg)!P#NumqDuBuks9wm5B^Zm6j7f9NCAr=5X-CEe=mr zWk^h9@59y!ks@%Yi-49qM(8&K=?Yn)En#fryF^is1Ol37!P@yEEes!wRkd4+iU{A{ z^!?#-{Bzz7zCZXTMd1qG!sSF&P(!;+@DG>lXxU42ZX7t-Ph%IOR`IH?a40&*c+R)j zS@P|mEhLDIy~xbVfH19J#U?{@b!hD^c->+}?-DOR+|T4-Wz{`YI!mPnDGsQ>p5BxPQ^Q{qQQpIu)A7#OAbkLAakW(6 zTx%2gV@0>jUW2wG)$7^98Y&{h!O`h$mV?_w#oWCi?>;~-1`_g9T_At-`>*47-^S1i zA{V$KO2SR=WMyY-ZNc>(5nb9^pn-79(>o~w%zC(qbniJb?tS(FI40+s_YalUt2`M( zn^>__AV9wy5M4y`WR{yP#j$1*1Ui|(&clAmLXWNl%_A&6h{7h%+y8~Dy&*!YE{&Zx z*{*t9M=V7Z{Gc4r_eC!tRm?!wK1tIq_}cDZn5F{9Bwfo}YisGbu_|%q)tWze{@;K6 zA0>bP`Tq^8m6u4uO&MaT2#tjlS2P$(pIe)Oszti#CxYEJR`x9);o6CnoRJ0jm&rSr zZ_cLqmZy*Yf#_Y7g`*dQo4Uo<$N~uhS55qqCj1B@jo+2i|L2Rt5bapV5y)DTCSv4t z%5P_7Yi5HY8@hG~)Wt7(-3m2h&b;U9Y0#|Rw(8a1QEPvZA^yMmh=1YcHD!OhWN+Xr zZlF%nY7P{Y?fN)Wl3^rbbdn!yvc1+1PSd`oh2Bv8(6n{!>6tdg-nTvy-!=+$K^#f2 z?+AoZv}(qjgHc`L_6{f*UXq1(BoiYAHV1j>Ap=J;INFT)0LeKvYu$W{u4P(a%08^D z!}#hUUAM!fsa?hMR6WVG|cb(NB9ewKkhg}O$0Xe+*W z@ag<_iCAVlCa6c)GBD=xp4Krp6UDmjS$r(2EJ{Fi`qEf0o~1Tx$o`qje#j1;LSW}$ zA*TyM^qnJ?LU}BIRY6Eyg6Xlqe7?1)=zZR?Qx7k_zwUtG4Ex!gbTWB=!}oXf@GEc@ ze?o2l+2w0cPl0B?3v8P&J#Z!P#Fw@Fwt!0b=WTtyMtKOz1?>CwYFmnEn74W-NP~Hm1gO+FBBUe+^YFn@aXrS>X(&&^jQCC>lqMG z$jka2o$^KRJ~*u~@v|F8EQt4=3?D8ZQ`7Zs!@Q%&>ke8%o9_J*LXjE1vyFJ0oF;{Xko|fZ?7G4~r)Lt~jn&591GR3_%Bw z#@>z|luR|Z^3U81$FyxX=`3$nbn(C`tN^9KztHIXcbPPNEtpMJ7NyaG2+Rhtl>azl z3^0D|5#=SW!9Dd6HHdX!j5iNtddEICY8CjtDma!7qK%z2cB#DWonWX(c1}tV;G|F!lIH?^SMf_wNb{=Xesn>=9aEWyyK*9zie(_I2ew!Y;!nusFz-8m|4AF zrLj8c{8!KaICJ~|8>Ih>M_);h`+H~%;Rq)R02C~3PHO$DsioUBPD;zVn_=k62;9Ti z9;HLCyqEq=(f`+N|MPA0|2O3L_iyhn>k~k8_is+|b&%Bn`7&U(Vy+y!tm^?9>41{Y zQzinpF*9-{H2XDV@`ypWpQU5ZP}Uz9`?thru+RbnpcyHJfp2Z z2Q6GiuR_Wb1RDvhR>)%sY%pITjhBK(+8SE)2gOJbY`WGZ*ta(%YC@;41O@@D!7MF0 zb9LniaousJh&yAg+p{z(4U{Ekh(aw*weJ#l;z7=<^+*cnb^@8fwG0sAO!sv4BW0&pbcmkbkAI_@nU_EaQPAOvR9~3=nG9=Md^U=u}7)H zB}eEjY8E?o_1Dr1W)(%a$>3TWYu^;X?XH;S^)-KI}3(4g4yYdses zSTVy|+861Y1Gc+bLE@j8rhk#?(v`S-QF{4f*J1<=UL8Y}VuW5)<;k2qy5gXO!qFAp ziQwg23u{O7R}Rs>T;^W1)cxg$bcx?S3<|GXe;97sCj(Lwflu=3afUyC-Lr}mRP<5k zT7#KrkbShD($Mn83uROkI(qzw;a*IDAH}+J(@<2GOQ>n%$lKiFf_z7p<@tvVI6Idp z;%B=h>Mb}yuw4wY~>!UHgNJo#~p zwDCQ@%~;r*AgC;E0jUVVIIr%XG`5r#PPv?{rM}KHiq4Q9ckI=GoT0P9OYz-Ki33sFI96kYve~|EC-_8;Kj#o&N@cW}UV>&?| zrG(439c;3#2=!AO+02*|TfPnYM4S7{L$FC#&?mu+6%c~Ui6)7o2t`5=Ko!4n0Q?_^ z>pcoJWHl)X{6UvdNMuDKt9Ua{GRcg%8Lu(YVhEbX5Bof7;JLL~+z5LlR)TddBoOYs zGw-C4sTUVYpoG%A8=ptxZWdTOEB>Uo%OxX zBn_8YddXwf&j=31+g#5vZMu(bB0tBBb$OVKcIDoAz9DX?WIn*SoIRc_+5$0z8GeA) zv0C#6$RBN9e1g@F=Pav(HHcKgcpFPhFIN68F_`ZFTBm>01oX#1N#uh-;m6vc%cneg%B|6Aih3^C<8vH+hNfDAK9oCXfHnl=R*x*=^KKh&)Eb} zRN$wJ?x|r#m(c8@>2f-RZD&37OESLfywfn4}q;x3Kz5FJciIV8{pQI~>>9ywVlNBo5d2{#j zu}2jal3kTJ3K2d-WEzOC?GX?_!o3s7!ZE^^Ze6g;l0%6oua#%T@ox%vVHw&qd95A;|r*5Ai~+% zl}Bt>J(ac*WEUKjzg;)I>SyusTI|CeA5Q$i+x}~n-bzivYcQ07+A^>mz5J|@>D1E9 zTFkO?rWI~$UPy6ryb7MwVwJ6`!P&qwFQ0lOe=7W=+x#D=!~Q_02Ef0aj}|0OQe95+ zL_8ldOEB3S-16HAwY%w${kWkqm){yUa? ze-;DtxsSKz)>dF|ToPHA^6f{C?oa&fQH5iQ3wRqJcP+8BOZ7J8KHu{Or(>9vmN^o@ z!T2|r%D+~GYrYsS%c_%J8@Nczr*>%=e_&!9&Hy&FL0T09lpFGZ6Y#op-9Fg8aC!W? z(PH#v;j!hk-0y0ElHd1}Kz8}xu)GonW}=x}a97;^_M1qy^^{B<@<%KyH}RR!{FZG7 z1|SYttvl-SBD%Iwpt9B;nNaZuNKT6U7-cCS?i!gl>na58c~^>z}NvfPn&)^C-gJMai7 zB$d&E5=`lQ7EG#S!i;gH4BE-?2ztSAiL6jwO)PCdW^neJj_?Vh8e~jG2Um(>ZqPmT zzNvrS>`oX*&|c`8zBp&Na1niyP!F0Rro2bckQSXFeAOqS0B?VP=Mz94wVYb~ko=Fr z(M;^azt@fAXelu_fnW9$=Cz7yND%ZbU2&NdSJ(8{ts^J8bhQgZYk$I*GnQMnepmVZ z#U9F^^nXnLcj5};|2PjD&*`ATO8a-T858}f43E?@35 zYm?19-*k-?{ahh4A@M&#C;v$U0ZK-HI@Db*i~p5g^PiN-Ke_z>8zTHi`aWQqv@F>T zE!(s};Yg5p=+y8}u?PO{Zpf^Zm7Q`)Ej0zsv4V=lUD~fFetto-5OqEMBdhg4#W}_i z=Nfpv48kTF`yS~LDHR_i*bT@ANHOsdqoB;)T4Kw_W-!0baVWTQK%i9*e+F)eW6PC} zynMn6bTC3aIE^EyD01e%RGu8(I^;P{AIXZm|9b2>`7Kr=@c{rC#(tN0RZ~S^+k;}! zw>u>QACSvQ$~ymD;s>PVcZnVu3A{BlDRwOsoFa@}B(#FA^u@b0cBRKxk%Yq4i&EPq zR%|bpSb5!H?;8C;t2Wcy@9(TN=)xSh{l#$sv0cu^Qg9YZeE)FtCK zs?02Q1MPH#6ke5wn7jMPr>dOUJ1-T$yF%C8f|bNQZ7;~GIBr`4z<&I!tNRQ031kYL zThs=3E{440hAr0B3gr1NU|7AoJ<$|igtYiMO)wx5$wd5Mj!99y`pyP6U}M#UyW#k8 z!Vh>~=sYf?Z7GQgX61P+I~rP4M+{>`@|{b2-$%kF#~PkuHoOjveD1F;Y;-1>g?1{| zD41M(P-9Z!i0U*uVa<`p?JAm3&6yoelD?S^m8rxle{zZB=UU(4mnz;F_5Ww)pEtj( zr~G{4tc<)jiSDWDS;4i6e)=t1X7`DqsxLC~F2g1DxnG`Y1e~buGuQia!YGWhDpu*- z^vGx54y_{^MFV%~I`2Cvf&lRV;QC7g7%~QYD$jndkB4!A{RL} z+&B%gB>`|u87nv+E!(mU7r-E>3u9uqJ;ukIllhnpaQZsddWQ$HKZFfeaK1}ethqk& zvhyTpkIl}~cb{~{A;S;r?8O8jD%qc2^qS@mYoom5RYd!eOlycMF==H-coaE0;wm! z3x6Cy|76T0^V=ImAQ8tdLf79ffo`zpdsC6SE$3Rs^qumIj@M1MTDzAS_yFAE9Ex&9 zt)p3~A4PxH!=ey7lMguoYR^Aag*ZIuh|MX;j6KWvl~ip!Z_IPzKY%@WY7CN6i|RCK zE#8eo09cxqZL{cvk zbJmgHB_52VhDBXmS^PFWh&ZC|^&ywYh+r;oF!M(sElvcOZWDJG>p?;DXf)#nhqwa2 z8IV^j$OsR>ilxFGPJZJFdxP{E<3ndMo zsM`kKcgBpTJec7cLhe&sZQah}^%uq+J+!R4^CJ1t0nRZ7Ztp*o9dXJl)%4O+^f=~U zx?Wq#gJDy5Joa`%I!SOKA;&eR+_})NlU$#v+qH9$>|^>BcywZ^CxURef0{Os)13U$|(Q;Tasjk`arMPnv zT;-(Gjl%`D>qd8w_GU4jBzBPMV>?((40u!ZUz5a!!i`|b3gc(8nJ7sJaTO_5iA&iJ zux-pCYG#Wh-U@CY&yeI7(Q9xS?vRL@X{5ztA`5;_ArOM@9|(9Dj&XgG9+uY_CWqHv ze^B`q)oJB?jiQ#hYo3nW;)6bd>-(<($(e!Lt|ipeddFzd?Q({kad~vFjPB5j>HAM0 z^HLSJ56<3kk$jh&Fn1!)pfR^hU?tGsRc7ZAYU%7ALYM#g_Fpt-?ak$?mM|Y;%{TM3 zxR&!p<)BPSWFSVpT2UKcB^rm3r15(NMncbFyhEvYj8LbPf>NJmCQ&;{Al)x*UPwrT zcBK6>g)$isVgv6_89)Q={KoeL$(XdWO(7gO=HO#U(AuS(IVqFpXi7pU$!F$l6^7=V zt3MYVmf(kc=vtPNynD8vtgSpmXV3QK9@j5hd|{oI<#Wt$H^Mpd{E8RktW{$=oas!1!x?xDr{%Ukm=}k z^j~^-tNrVF7BvwZ9)W8Ik~I4PWvzfZcBKPdi>1$70#uA_+Nlas7L(Pn?rqq(>^!Bk zyGm#=0GMZJZJR9{;_OqR%|!PSH)q*)=D#!MMO@y=VR1dZ&)jP%6reamswn(H@4i+} zxy|+Ltz5079J14Ot2ABCOxGjL{6}XMf}}r{YK9IK0p?@z;#th;C7opIVsBt_$s*JH zwpmRM#~H(Pp;y}N*)X1ZoxFXfk9;Z5lxnzD62P6jE5Lf^Pg&BR7R7yj<|os{v(G?k zjQ4JQhYL8+G>i|27U*A#6(FUI`gIhWck#qcI7_-uN>-BtDut85_}d$AW;a(Rgwyai zOU=ER!Q4I<_Ed2-;JYHLD(>n_ayaN#{;k}tu7Dq-{Tx$pFHr>@JFdT}pB z+T6pNt9Yo>ub%`>_9%S2vw(S<-+@TS0!v83`PYdt8 zDmD|{1WXeoX>kv0Rm&RuF?cV#@W}aAFrYHOf>->S>)9-Dnu^WOAI@t*YbrulnQvLo z-kRg}?z{`L`06>_SZCQ{IQS)ge*f$~XoQjovTSuh9atuo)6m|#S$GN#`9-*eN4iW1 zj>yEWWpUmmaTR&dEo%LVnqjY|eLV8hkZxgn#=_q1+Gs5vBsq0nH@f<_>HZTw9U6;6 zRt~4l&RrM1(Z0qo%?YF&OwZHOEpd}ov4e7vh>;36s$P`9hq%s7#)dA97&&NiVftQGfRh$4_P;7DXGQ;s2>b9r6_Ysd?-khA)Vx|+7~efjxAmn zJGo_TUZRP^fU!EbWA4ex@p*k6o3rlzu4WVSMONJlO#LG-_7K0RM3V$7;FRwYPC}F4 zx}gvS%Vk`E5gfqB4Y$bQf}Z(Vz$+nHdNKM{i+&jBmY+8U-`Eh5-vFC$7 za9_ThjG-`b!0g`=zDeax-z(wMq#bT;oHKkE8Y zRpez4y}rbkRlARe-q~N}VwZI`bhK$_=ujCg&SjzMZn&@_p8DGU5;N#exM2!_V7zYCA3rB>(J?dJcl%wbw_AbSJDrp z`e#gKXyO=41AT@x_K?v|s@stAK-?WEe)(<{58Xw0TI6ldNFw@P*2RTRi5#xCi4>G2s$H0dNgz@=*B? zj3`w&#yGi^x>|Y92&6ag@rNYB0b7!?~INz$$ zdR#B$!Fi^G#s$)t%&VTE>!BJK)0=caYRiK^m;N_`6fFHpI(K z;auX?213I}Qkf9NOD?+`Qj4n!OB4D(@Ex7<87jVYzJy*zkBx4MiF8_*+-u@ZjF-(j zO7<ryJej{JA;eOXg>ub6{XxePb&u6 zk9C!zLO)HEY2uOLsu`ohl)Qm5%-Bd)`AfHU{|R5&!%Hl_3J=gO@of1g;c(t=enmA* zC=q(WSHjBR&YiLx)i@If^=a=C<~ABa#_iV+a1_acytW+h&A4} z%x5~TNAw{#>_Woo^cf|R^Krtj-S?KsV%yP6SWf!l=k>^QfCF)pz60mfba#PE{(_z$ zcYCR9Y@Uaf-oAs~c}6CaWoIv3xarf^el&YqQ7~86E2>EK8rhY!SIW_q-sL{Syr-jY zR9L3+pv&C4c~oc+l|F7G%UDh!poS{Ny1-|!Gd6Bf^Er_S<^gIjb2Y!<@j&L*DbXJB zSd5*vcEbP-!dOOlw6Qw=HTB`N-&(L0$!5SfZZK+qeRLy+YehA#)9|&w2Dl>t=p#A= zz}*O=0e7+s4*LwdNxWNV1h0l7__v{(f^zfkgo$;MNNTQiHrg8lTUtQO* zajNhxX=(i2%hcfy0X$7W(F*1h@=+S_ZhM~54c-lI%w%#R8aL~E0yD>wi9Qj)&Szj! zS?iJJ9PC5C*BuY=`$x>NS5y}V1zBHgfaAjtckEKZJ8)i;8AexYq931ht8gZ~Rz_0g zeC`G6y0~`!v`179L_rpy3f*aEQvVde93S8LRj3fZA5bG3U&2C{@tcMS zr@u>NvlPD|(#1w2b-pYWA>j9FL!^RMGV+zlMhKnMw3F-90g>8n8j8(W`}kBj`v_`S z+Mw1Si(m%B%^O6*{dbrergu7uM5!P+!?xDWq$&dN;qQS7ko*7?P zcG9FZ(8I=ne2m-efnXZGqW@-b+Q(v5^wJpCWH12PX3eza5_$ z=$_1fd3)EFa>v_&>776EJSqSr@J7DTZ4bw&^fFym-*<_#5t)tlbA`O~+>eF4N&YHP zeUrarhwK&7ik9_4J5jc8$5El~K#PqqlGgxIdsp*NKY&-J48r7@FkdVn{^X*QVk%~{ z(2TssVj4$uf?{`t6W<;0JMT+^OTi0Y3->~{{8;#_WCtyk^)R2lz+A@$0UU(pJCC%W z;Lok7U6ABlaWo=ZiLeSu@aK^{;V^D5bA!hJ!PmFPGu{9FcU@hjNUP)=yF194l5?`F zx=@5Lr#UPnr?eDZi`lNLFy~$6RK%*$8kXc#!m_b_Bgwfav+Yuj*{3p}+Tr@Wulv9I zz8{a@pB^5{KDPJ!^?DxtIPF)8#D_$xU&H4$k!nti7XJEBZ~ab<%B!j!&Nc4nDhqed zS#8g_=+RgqbSbdll~V&pi8YvWvpLXKwFB&zk+HSbbA1^#g@t?UYmcDCPy6g0 zB9m@uN1D$1A8cuSd*1pZw1Yh}7g^(*rGMw~$~zGw@*>MmzUGvGW*WN}Y9#M=)BNEh zHHSr^Ku;LRRyd6lQHnuq^8#EI77n@}hrsuaeeI@hAOzlbVZ47EOi`IHBk3y)(w~F` zFMVd>Ebc+k?+=Ax{-ZHHaN`~}KN*#eY}3Ck#l`Y}b_#)Lk`piuG~KY2!3RksA!C0g z{^y@{nq3-rk-(0V9K6}=cl-u}#xZYcOAjj$gw7)mf@ zBv*t~o%j1uYD>c01Lbp@kE@%xIJj2F`!O=R$9i7-o4OX$jvXr1f1qEW^OQ6l*{Bt2 z*0}RdWn9u5$*qoG7G@UwdW9yV9FW~%uvFu%o?nwe@824i7Uv>-_}Z@^9^e~w=-K?U zot31wAO;;&xrE}&`8{Rex<*A>Zi7+1tS!kad7AttJC1omPXM-6X^93PiEQJ0_==e; z70~qSMWHK8sj%fpn&9)(Hpl)@e2n0(SVG6CS5+YL%pYOCjC@n@6|P|+iWfv6h8+e0 zM&o0T@%6vN3X)z|o=VYrtzLJ!%vN)|Z)JRQO^A}k(>N*W@*3!78C+{Ry2Vvf&1CE% zK6`@W9++|dJA>_$KhLhNd1*Xa;^5j~{TL3uG@2i-@6@sCE$-<=K}pjZ2TNUxfiax;C^`c+Hz z;JcC_W*OusE)WgiJgFVN=wGA}fM<7E@`^4baTvR?!%zH`gW5^dCK!QUS4gR+eeh39 zKOvG+!OvA31~JzHxn|HX)5g;hGeS6ipC`Iy6Koy*L>%(A^?AyGx?@ot`MUo>`~UQn zsvo^v@4Tl9ao|;aqqi=?t79I^QtTLk$qOyXM> zUF=5++)ee0W)<9mldg30xbfGn`le&7VY$??e)a@zp(rV^#hfhPuykuN~tTW$*k1oBF@OYTRy#R+Fs2y0YRh~UkbZw zx2O)3eVV>02=-%WpYhF~duHOyFK;<9%G9~xA048S+c&M@18|<2P;@OjiKfLG>l68* zi;jb)uZJnqKX za%;1Ny3tA#o7$0kW~b)4z6Fki+!O)4nV%{X0zBOjW>tIKjr#)jd!@$=% z%3IQmc|Kt(S8|pVR&NXOwiLLh2g2TD8=B){vFGLt7i?s?Vv15Oku;pP7={?JqIeEY#% zS^2r5`uN3YWR8tH(N50lAc;pLS`cnoH%00X24j1oMepyBXlA0DpM`4Sv6;sM$&ypo z!Ep@*3K}>RO_(R-U%SAH?Q}J;Wypk6Jii@8mbx+)L zY}rlm{jTfG=fSUK*CmF!n7q9<9{WnwcM|bZp=^59YJ< zJN%@3=lR;spa6y{_Oi^mQl_((-Qgl6O&vcaM&viIyhFYKS96D+{u4s55QIB^zAg>N z|4H1vIC-T-(Eq{E=jl_JBRFE{8B#ZZ`50w;ZE|Ciz>y`}87RpZM$MG|SX%~d$M2*Z zzHXP%T-SD3`Y3LVh3Ig50DDVPA;W75MVJxi_4vAOKH?i+Ew+7s z5h|D9Vjb={{4MYBfcwA$=q%ycZ1C)D+_u-&2IPS!4@ZjQCRj}z;sSah{nDK%oxzIj z-rKYDBXm;sW`e@dkwViQ#e;4J;6=op)842q^v9ojnlNte4m)@N|%JjqdTr%gm{tS(S*oa-{Q`C@hGs(X*(wNsV_vJr}8Ju-mo73vyMl~ zqv|4hqTw9qt7NlhZy(6QZ{Ox!E6^{%J$OI-AW z0x~)mg35h9mYJLRBV%&u-sSYh0$kKCSKtH7YZxTgLp$XShEM$3KF`+A4VF4jsrt+h z_^DtgBclCED;k!Fjb_eU{iCmQKKlggR_&JlCOZXzkYewQ>HXjeTDe{hjBLo@7iYx4 zSA^{8QWu96aPK)Cda@XP5TfL*9!?{vz)IdHs#bzJTFCxmqQ=%6)?rFvW7SQ=7yemi zP5^SJ4d&2;94RQQKID8P0nAyXv#56=JZ#&!5;4UO`qWFA^z$5j#EQXYv?@&HWgXGb z*WNOXaJ^AhtoP3!;w|lRIvnofNv^H!P_vb*!X)ctb=OU#N#ezCwCoo#`1V`vS1YvektIjlm#wwFLmL^o+EKMcpfBF}_8k%X2MBZd_OIFS5iO~Yg>Op+yc!$5% zSImQr1)5p+m$my=>jLt6yvyFb;XFluB8(lOEZTLel7b;D+mc*mGSOv7FoQ=(LXygS z)n4yB-_xaiS7xc7=cO;DH{d{OX7Q&i&g+_Z z{O#9wLTh~~=8iX$0xRvM?j6mukgM}B-3${xj8y}69N@(9GF5Co48qm^SqA|PVGZ;y4>4`IfH%_fv2|5S~(iI$U2n^_}%DR4|| zz3W-$oksmh>!cK8H>K=(wqV?%meXxBM3BHD?pNDOX_+ z-pV=6mbv9D3VlqWOxOca|CEtv^Pm?tQ!u!ZunWGCl2W^%gUqX6RRv^@>k~!aNMg9? z^o2T5_gy1BjyDv!^?Du`a5KfF-tXuV%mgYLx0R6c1fpbgkm>;caGNAO0pDaJ)ZfcY zpfYktLR<~OF7E|6n?q*IuihJTvJu`BJ}>b7>Y6~^#Jf;FTGfR@YTyV9Hc=f{iV`ZD6tbe$kdZJN{BS~LSwTr&w9PL$wT6pRdV5^nGD-8#MwYuKx9no=p_nF>PwPSnzhd7GHp-^7L|EX!X^oR17 zmHcVYpTub(V*vZrr7tspP&|Y##=HikBkSP3L&T&k%T;-ZZ|61kq^Dbt!f7H|ZpR^N zHz!=bT8BMpy{7RarQlULv7#>i=LwJJv)lhNMaF*l3nwiw7fVjK`X%Mv@%tEl zRH{r!gRsjUQa$1TplAXMo4pbE1TIp$OAYTQL}YiM1a@GU8PBW5XZ7MPBoAmLMqage zWDN()p8j3g@$%=Z`Hi2yOe`vYW_rIJuXCeLN{!u}-gk7ofAs!+$H?qQepz|w^UEGd z{xt!a?!~6ugG^W7{L!{cNtqAMBHFG|wJ7|9d+)7vb-!5I?Mq$^y(RPQK9h9oohfg#YYkSW zxf$rU6Ky%Ct4y~aSM>~kuqTO*guL8Mp_sqvGBuWD5bl@85Oz)wE%BEnV5Ps7LL73o zA9Ys2`*9_xCejPN!#&|U07w|Kl0)70Eu@mfMj}{o$h=9rWfT-3R5>yGWb|9b{5?4B z9{c-~mwE2LC93DBlj{_N=HkgdFl7_-fqKh&uby;Tx}+NHp$xfO&yUf3)zuw`I33O!)m^V0{DQL_+gj;l4@71VK4Lcbbv^0C z!v?cArU-yp13rul_^>v`U!kWrg78o=2tb}}5*c12B~ss|NpM5N4Hc;CC@sQKXgz3= z;tgso{@BA~CVuH!=9-N zi$0(%PKZ)LB~mnGXFzR;9dXdndzmEN0NecrABM`YSw8}oZr9=Md?B}G24u(4*MZH& zYdmLLsP%TkQE5b+iXhEMmLQ~PVAyGhU zKj8VC?VCFiY#&%O@K1&5`U+9;{3$!hUm!O%!qZOjrx?pdfXWQbUgIvk0g?G-9%&09 zJ<)K9DqyL;oqC5QvH8@{iIzIc?QjJf!_6c`T?KfBDx9hpulRa;EOPi9w4o~pcU6Rs z+MN2PBzU;;)FJL^n#r5yC^Dyy$Zf7cPCJf0V~9*fd`mUU?Sp;kUTw`kdTd|q&Sjg~ z1aYkykq5M1xfqR-I(j27ep%alymTl_-+bs^xbLT8(@5jj;eF+l7P(vItG{~pu2bO_ z{`%UgPfKL@!1Ls9CDjuVz7>@wm(yM!B&8kzmec>?k@dB9Xwdc+so*xjf}&7bZ7WHXlZ+;fsC~e{8&ZFVtYU3oB0!uRXNm(Yr1Kg;B=}O45*NvwE zEMNcy)`E3=QM$xEo(~8Z@!9FELn*4hu`=(EHf22@Y$r~O738aN&wVGhJbeAYs`6?Z zDq`7)Ya9Bg@&va9!{1rsB6YXNbGMPrQ@_pnwwW{f>UxhWd2IJ=u^&e-7`u9@>$!Qq z9irJwA2XL|gCA#H*Vc zg<4B=N;jz$oJGz?r0!pd2NbW$Etk`!8-Zk^3n9b4A?n7Fp0M&e_NEEJJ56+s5(V@{ zD_lz?Na#;3p)cov_Ydsy?JdqJOcD+4x?qh=t2%=1luQL7+geWf(o{w{G5t z?RRfA_kwRtF7sH;$;igjdGzyL9gm-~7}G(dIIo?g$y#*%eOAa^z59W0R2WQQC^^|2 zs5y1Ss~1lHVs|LL3g{^ICs6iipZj87^y+CvpDW2^Q$ND$+*C{8Ub7eHSV-#z*cjXU z;n2*4Z0Cx|u$S_VJtgquEm@}hqELZP3Msqh{9u-kcy?uJWd>1zIkLi~f~ZUXiDrcI z!XZ|Hpu#U~YhvWcuOmJpyYk+#`r(tk+01Ngc!Bm1M;@r;w_T^?eTf6w-*|+seMx2= z_tpKu{suc6nX&bXF{I^U<|Z{nX@7OZUO}lwDlsSE!%= zEW&SXxFr4(O*uvJm%L1Ign%6-TH(J+qUAK_eTvtBqBMtppW(Bqlc1W)dueutswp!O zO9@VJO+7rp-aeN)$ieNBor6}t48#a6*M)}$!MUjIZV3%RaJhZi@!{y<8Y-G1oLCNq zin>v)gG-%KyE)d+1JrJ6zVm;u+HW0OE011nZ~EwQ)iKc7Qq@~KvqxZRU}9f;nQjp~^6*)_3pbD+U}NkVEVD6w zeWy#C>(debHuWnf(kK#$@zfCZqlHf>o!Z8hsaE-=b@)Cj?75Q*MDMr2NYiC^^r|%N z9satcA1;6_UE~jl2!f{Aj;(rJ=td^~6gM%g1~qic4SlJgrW$&EUFxJ5z;2Cdc``(B z(a6NrVCFEz2c~1!QgatBwpO>N&$dmmo>1AAh2gs(Dvw1$HrG(t)U5<84Asi)M`p!_ z-6%c$Cn1hAxE@9b46pM!U790_PHE8k7y4>lkVRX1!y?gc+;SgwORPkaBh(E$xOa5l zHu)52r_)`0=5PJ4 zaGf@z^~KN>EJUq`x!zd;UFx0i4P<7=uRuw&BugRH0m``@dNOYp!){d^9Q~$h`$g(4 zZd$z7o3gpS{_`VYOI&qZTY2<}sV_y6M0pu1VZcz(z+-J74wHUn0oHFDV6yHJHq?g7 zV&JbrT;V_K)L98nfp9(-5tVpvK71QoBUa%epJ=kBst5_LO}s2&Mb+S=+#&S1{;rXX zE4J-kHA#&4NhTK^WiM<}9y>1-R}HErk_aE-TOu%49Z@=~4`)XOd343lb=vBz4*pi8 z0lCCKKYCYda-$kOAZw4msoRa@U%EDdF?hX38-xV3^k!j$vTO&KmvR+GyvqYxl#O96s@)y%@VS^Dk)UX?SSG!pXMlyVQ)Z4W1CJ$ zVyQ}?i^Ej%M++%#1Fm1*Xfk(|`DT44=%e9qk=1(79QIX^wLGtx>B!X5!$^g_r#w zXj!01X_t9yUy4bDD3fMleI$R@uZpR><8*_!s*Zh!OZ-8m&IRjvAI{8U`%nvwWWYc9 z`kQ(n46`{6b?iGy>JCX-+3m%P=|e$Rz@%-pz?ZBSuPMsls>infMl*Gh`^^UM@#nD_I}NT{&MKwk6lE=yVQ3#_q; zkie*`;dWkVT`TwO{Lq{BODPA-j}%Zof{Ex%9K^7G&8 z)g0w>%&FGh-NIu!dgs8FBmc9Q6bQ)3_V(-U?E3k{FOpyWd@(?DQ}lzDc@hqRkR?JZ zy`;&a8U9j&kcuP(w?U*#Q0;jXj*>9p3CIuwCy8$wr*!%W$lat2Y!fb*6^T=sPepM| z)4dhj`S6MQK;edhwQSt^-0S+8umK{o`IGB`b(1Mn_2B$or7y1`TFT7eKW9RY=zK*#9dEAG1*>lpSX8c; zx3uLYt|X`7%~Bkkyur8~T=l{1v!yUWr6Txjxh}bMT)(eH5A&_jiW$yyZ$214Zc~?r zi@|E%a?>;Np&HnkB&~I}#JQ>;#J>#Sw%e1K=^uwQ!m&W~`Kx=% z6HHKS|K#z7LABMt8+UZ`l(O%jYsdP*(0q{YSOkg_ZTw1f3h}!*mL*QV1pxlL+ zFLp)0HzM>QZ@ZbN9{(C@J?lc4p`-_drzuH)Bm9^AX4gKC27DPed?S1nBAt*k6^A^9 zfCH6;JRcih;h+%CTNSSzSxd*a=2gQ@0iL=*c}%fUg8yhI=e9KaU48^cyTf!0a0FzPq3>(_Pfz;Ar57x_iOjg>{EtaS5TM*}pUQ7*K`ctfddq?qvDTTi^q5J}O4MKxpGbBtm%& zD=z)GulUt85b{W!iG4t&da(N4bo>*1E2js$-YH7z4U>D@+E+7vSYNr)Zr)qQZBVhO zH0YF%e)WiWXg=0i-g{v!1o@<)8=*l^9dUMihi^J9me4H0J9?5LqH;g{)N%NCMi?(n zfX(SL^h`K9vU-^jXM2v{b6}DY@}G#1yx70LM?8OuEpM)~@v|*F66fl0h+G&pIleK-DZ8rNAG~|7M^rZjbjZ zrkNhY`uPOsbN){n*{>UbmfBLDiHbw05M1HoGl>MnU;uI*DrRsBDIzaZW>yNjv>~^` zm47?8a(>Xc9u4{B)qK^CFfn^gke-z%{wVH;L_Mi7*#vBYh_Ciin5fo0e{NcHn3Da; z8HrCgZp9YHqf2%<>ziCEL0)M&In;XwN*1?R#tvF?oy;D8&$Ku@>vi!CXuFge*B?u{ zBn#KsQP$EUb&t0y`wVCv6}@YW?Aaz#2&W#~%pN@3*{OX=9yK{4zj^Lj+ zEF^f+=P5Pwngw>*hQrBq*-H^6yW4IGhAb?YQRY_d-tRZ1;r;GH<2 z`9bSjih)-?&GRJJ;)029KF%P^>Q1Bfh0E8kw}kR6Uma+kg8fegsB9!ZT}tf_S1JrdRnF|zq^W@R1+YUQBnsOJEPg_t&Z26S>xjV z`0)vx{B}Ub+oc9$_#CR{DwP~=Tp?7t4rVINnF-40^D-A4gK%i1`Gbc`1t$*I5SJ5O*4Q`ItCWxOf418^nW^vNyML&E;|E^kL z>FU9q@#j(wJPmUI?W`#V%O)P$Civ)VuLl-d+3tQBq7~QbXC>M^#Gh<#)ec+v2c4mA zd6fFq@CW;AzWdS#Ebq@%AE-xdA^oRCup<3w9`)lW?pNdn#e3klL7i`)gew6_+!5K{ zp?}t$U5=UoG8u6~AbeEVpe~D=Anu(bZBuLx$M^AjK%ym!lH5T4;SI(!JV|o91*g0? znK(`nbRItqpD%T!XZ%e*4Q*t`Fze==wINNw|F?pNPJc0Nhs#7h8C+JEIuc)lAFsf^ z^JcR&N#{`oq|GU;b@1XzadWvPg%u0p`L-q_-jaD3Y|l#Js}z-f3s<#)%j9i zZ(4WFBIffmhUbIr>{7$cImNYpmm0iMXamcPp&!*r2fKxj8{;WbZ(9H~CVjj=Cw%OX zP`JM%w$CMMUtL5(#KRHFXE9uxJZsbkT2BuYVs~B8c!sJ^NxCxw$v) z*Y4Sq`J2m6{{H9)w2_wnSYFb)OISd?aC{W7W-hv9$+_iH4fg_ zdNTRM`$uj9-J4_-aU$%dG_t1*1EJwooT$K+*IipH0f{Dhn{`udyt`h%$IK2 zWL<;rU+R0c*ewi4XlC}-9_Vp0cTYJ|>0;?wbq!9FX;r_q^5!(#4OGTa|J4Azd99EM>iIAo0av#NAwlb}gtId#? zXfq+EI8}VqQ8=&qqyV=h_cE>PXC{PS@43OpOwH}@U*x~joWU+5@5=mOzyd19w-a^^ z5l!)f0?QZJKO8MHoqr+x3PehlFCN3lnJAeXWTrUW1tEJVS&E%qjZk2A2bghszj)l| zJqQn(o4j!S!Et#fGwgkMsO8hICabf#r6xX?DxKpmIx6UDA9HAO4_mS#cE(rRHF42m>lgdh)<6RHnLt_v=bT1nt zp+PP?lJ5IxPvM?|y5FfypSr2JtaUO=Xn0d%7ZT6vL8<{)IwhB?{q3-}Q-lz;bxwla zN;D<746m9t)g_pUC5l)=;se-WHSgiKx5?k@aV>{+6A!l3>4q>;(olann~}P$y37^< z8&~#%RlwhhN~BT(oHanlE>nn@d@hXPU^V8cPpaGZ-`@CKj1vZOu3A&GbUNuvxD;XH zmD%%7#|2nrP69ZOux?Q^)HVz1!;pk_VrF{6-?KYeO(OPcJi4go?6V7Dr4w{bY3``_ zDS6M!|J6O8G5=Q=A#f^6@wZ{v2u@=zuQ)q}iX7pKl~x zt)=H%$buoVkh=6zMA@>S<3-iTm-xX~R1IfXxqG|H6OT=QJe(&#lDKR^D-|YS8|u1J z1ZE9;y7$Q+1p=)XO=?xT*Z$SlO_x&Qe7M8;&uF^-1t)mrCWp#)jg;EyWn(uQ3!afG z*6g?FYlKd>9Qay%p8r(%p?X%Uge+Kr#ZN(|>8Bh392<<){Y4X|lO#dV>n4aK`3Swk zT}$PYNpUNGfTc+dKE%PQ%XI4r0b;)d)YfKdK{+XVxyxE+YKJk?w}dYfi2LS-UOt#y zg!&o5y(C*MNOFt?mI>y`0&LyRRs7X4OWPsAco&f7L2b&6Wn^Y@1~7XXJn3i0U#2I1oB4bn9VVYX`e-1U0vv%EM+7cf+?KR3L0K zQyCT14%rpj*f$@E_pscUM?t_L#~LY~6+!Kk_%%?Gej5yNauqgJ-PsCA0ZCB}dWun` z1}g!DAdaL0#|;u@3ElD(8)3((7bKMqU9qKLh1{i~7;58hjGE|Bf%bZe$w0Eb6`x6n zO1VBZpz@UKo}icM0rnWYjs4AA=(c)BdFQfX4di%5s8A* z=`a8UrNSPUMOA91?!cRRh^tC!WP3+Dj*5qM8*;1md{OA*HzuYEr6%(o}Q|nEa@;8t>0&t`Ji0uYSOJ!8kSq0EY$jSM_8pggzNiU zqgf_oa%P|E*fV?!P9CE+cc11--JMoxt~K%QRt+lw^i1tBP9TlK8KUnxxA$Pft?>fy zX|uzMF;L3R~;a;jms{3gMP1vBR1zWWbnEJ;qR}=Q-SrHEsOy!xRc+|Bj9eK60#lME6 z^`x09fXpuM6;iKPX~Vmvb_4{hR10Sz-X**MlLGrRArP_@1r|Cb)F}Eu=jJHUV6prW zL(PSLTrBzO*-94JWieY+1|o1ao6TfDicOwHHrj{&_E89UYe(yqEhzWHjDA zQh-s73>B9gfD7YC<&SjQ%pFHf{63a7%Jk*sETC9bQVdkt!`^j70F^!dTqNxG-UTB? z8*^FUcaNp!BU^1tq$g#;BTjJG&)#wlPX5N&vUz4|>iEYx#`uQc_6vo%kUbAi_SCUTlnW zvUvjtvY*J1#X!0k_!+cg1w8Jp3`_Dd(F8W_(cjuQMAVzYA9|s03|+ad8>jd$C;@*n z;khSfpZHF|m$|iF=y}JM#}qvW$K=@Yj=2_{zZe#n=7%GBlzorCJYr^pub$Lsh>&p@ zUwW&lD!EQOz~3|yy6{LJ+r{YjLW`EZKYJGC(0P z&`_D?GkHY^c8328A*89uCCkIp&L1MbsQ7>iR1A9hBqme08O*Y#9?=q=>){wlguI#= z10j6Y_EY;x_`7*NPk_xZus^g zB{^%nnIjwHaQP{r1*HXgqW2&OW0N+#b%XP3+5)?3(i& zrR)EB98`Omx%6>P5V5PUYOiI3a!P{FD+4kY4KAoB^6+NY%*}%bN`5!}c&q*$dX}~C zqW$}Gak(6Bi+(Im+o)r>KFR*12f_=iJ4wABp1(MS9w z8S;CqxTzHiNzM6}c-D5J#T@G=Ys!{z*oh?%puSxzGR;YQ`$2nVtAlAXSUY)kw z2BLcSaiMDD*P!7F4(3#9W<$1h2NNkVgq!(dZ_1RZe z4LMcOLga=!zONh6bMC%HOs}qJ=~L`mp4wpMtE+W^OdYGbLYm+9rFovZ z4j$WV*@c8fAR6etOg-!bXr>%@-kboIK|&Z5*sXsk9Vx{9A@iNqeA8hrSdJ20$YB!f zMwLFF>TY?j1O)4Fr{HLJf*xR@BI9 zTH3%dQ7E*bEn6{>s6_s|aWyH0<}~Q^>ha*D(}E4nM5~rKrJ{XY>TB=dl^=h6(jv6G z1D#%D<7+;&Hl~vj^Rn2VUKXwGwULR6%kB@@;hQ`~U+%0z8TCwXMj3{d@1;%+6$sx- z9ej4A$)&zGZZR4I)xp*;Q75RB-rE-Bo9^aa7xbEZ%3<|I|eX zeVxu!CUsoOZ}hKTKugAsJUXnG`>GDy9Lb%{vEy9yJ=c21NQwJSMjv{<&+881hkeSi zni%b8T%YdYg3o39l`Okwn`d|d+|TQn#;D#UntZ3k=|QvdDu@n$b&N@Cqr1f;P(t%s!uD|>%L)rgRSz3U|d-E<%#RuPyrF-m^Tum%|LOb4` zQuYaT%KsHTeb!p)n{KXQ6@Vm?(@)ic>?-iqUO6VN<`d+HG z!WhpJu4IErc)CBZx3_`QiSUP*<2UYq9|{?;rP4y)55^@peF@>MhWXc-sQ?E4$=KuUoGSOrdcZnkkfo$v&iIaE?NxdpQ;UDcooPq9ZTpYEyjZH zG;OmZ4qoUvu1{Wp5!*cW|JSGx;7b1gS1Mrl5=R4&ty}Bp-atF_j?>Ko;Q-#s_`w&` z=3-oaGIG+8%TjaJ9TYHQLNd=c4{8U+J8y==L}{p8XYDYnMV~BSRaqTD&xY?j$MXsGX#bNFjKjz1~7q}NMwyW=o$fbKdP@9Sf z-(&v@<*K_)+`L%EIm6(LvT&oh3;swV`#pw(2&3jD_Ht5Gi#Q54bMCGExcd$Oi zp-Qb~c%`{uqwq+SPgAbQs{sdpmueH=5beR` zggOc1YGp_=VE7*blZ6FL1A$@yWYHwJ4R;-oIX;(HIvu6G6sFq=NY|PXo2Fw~+nG)Y zGl&ha?(yIj#5TOA%rqtOVS?Imkx@+Bo8?Z~Cq2bu(R^f+nHBB0`t8VsNQU5Vnh9RH zrN*fC5Ywxzwbw&gl*!N;0+0sH4A;W+y3{{tiO8x_+w}Nb3_@~Bb(3J8OKoJ=lg7k& zLR5+JCE}n;cIn0{WB20kG!trIftCp>sNu(Rmy3E?dMsIfdz<_Z*^z(N)hG->hlq&- zy2JOsURdhheVtV5yD@2M|Fg#8=xlc6V{x2Ur{iA*l8*brhOTmn38K`e=-r`N#M|RD zDDVWM|5^8z55U7JLi$4(7YNb2`59F^#F{xRD5VbzspQO+mI5Vj50CyzJxGy3OuWK{cO_u6jlyjvujs^FTTGgd;_FT$y1o% zfumNyXMZP{i&uWF5a2SGzaiJnwUys}Ozw63R3_R`wA2uuCp}EqDX+F9#Zj`cEoI9n zLBk)|ErjStgso!4l3c(dqncPffb@+`lvS)g5Z?)7D8wzYBhSWKPTDl6 zr<0Gp0so!uQwj%AQ8hh|tFo$n&s!S5?e)F05beLnCGw35BQQJs9ptU1wv0*-%`M*6 zQOkReIgHAyESz6IIHXQ4G%eEBc;8|{=i5DK*1>;~J2J`)s{X#RHt4WSNt zK7-368W7UuAipIRLnbFW9Rd8d8_WEDg=|YTUzUHV5T2Y_7NTHjJpz)y87ft5>p%XVKj(v zHVPBuLS_zPRXw(sf$Qn9q@+pN#@OdMrHMR#05m7H1iPXaFw9-fj!cYi?oFnb`jg4r z0w;FR5|U1Q1H9OFc)pPI0z^8zKyFp6!xsTj`d}^bjkB8Aj>F1fsS{Gbu}z*fgt1Ud zGZNt~3{qImVSi5P^blZs5wIHpAy7RQn6HP4{^x5YG$vmM+I?d{>Kx2&k*LklcGGtjWJMr`}1 zyj4BZqnYP1vB~LsxTwcdq9w@e$h=R3`=yR3UuUjUW6|H#<_7Fq)e_^9uy*wC2egn$ zcKEoz(esG0kJ~E=E=EI1j7*wW$6jBb0-bT7h}XBu_YcPd&6j_j+Z5M2!L`z6moMFW zJ7QGpG;fPf>*f_U&5R=W1U?S`FEkr z!3=^YPP~Q|kLQS9e$&HPZd}y2dXwV46Yq8kI{fx^yWSf3>)yvx+&U(bwOaO`U%OJ; z{-`o3>F1NX>`K2_^|5}E>A?=t5EKuWM{VJASwDekZ5!NKs~D_O0ns9hGi%)|1ynUc z%nVlTr~s)Bw+U5wc_8fWGK5+Z2rAt_e0Dl2EE)NhZp(j>Iw9NBYWssZ+f`HNDLhy%|M0`f&Gz9o+DsX&!%P8?$CQ+U|x z;mUA|R40zVfffH8LDoU^dQbigOiFOx40bZ+MVGF4cph`ES+iST7mlk7BEBJQx9)W0 z1Uv`ldIPv!H;9=ZJL%j>jk`{cYgKzlK$sgSxel&D`CdUEe!b*3vE9-#ayd%eCTWC_ zJk+`beP0y#EKRSL3z+b4j5E;f@Q6&ha>r0HL)fUDSO;#(U3w?aK*b9p zljts(Fh7!C<8{5)zeJfI#4rojd0@HT z#9rxE0M}3Me*gBlf7`Z@3PW%r&9W4riU~yADYqY@Yi%pEBObP@O<>{c#7vC9`a}B|B_h_pAmOZ!`E4KFbn`zkI9<-i;GaNp)S}qK65S zIC2LHOz=Ahr^a-9rgfXf56&!GDt7!b{0%kwgQq->G-OB1ZWNpt|IzDp;^7G0a!J-? zUZ9MM4@ynG)NaM$x}yT={%1tmZd)3)D96>@Grpb=w^Q{(i&u?Zi?6=@437 z2%$;>X@I)xN7u#&!>R$@w0Z4xizLXdIE)5zpUzL$*$U+MhHG#^Cjv5XTAlnbB*>v9 z47TV#pmLmbq;F~e(2rNK)$4j9rO9vXup0a{=pDEK7zSvefhAyKWYGa@%s~+}et`br zkKb5h1B0Z0%kVDl27>n&Tc+@eEocFIBAUKk7QBA1&PZSh{^^ri>i)Di(?tmY*FQ0p zeyF77RpUKe-vCyIDjx8g^5K&k&mh(Jjo(D30DhBU{RZyIE-X%uNlnI4JOk~zpw4~H5Py+op5be=i&=@ALC~q{HuQg z*oc42&HkrNhSEB5J3etL>9_XVp*0DE5 zwEao8%&)qWr+vg{0}*fkkAHK2^O^krV;L)n_6V~H!D*mf^8}IOK0UdzB>{FaCK;ZB z<~UrI(;Yc=DBajxAbSR23gu&s&BR~7jsuuNOyq=~=vD_Gbn-JBfb9X8!n#iz2GVQ; zQ>c#yU6UeJ;x|m8gVTS`a531KjXS;Z^x8d<#0x(Db(9 ze27M7XBT?k_di*Sez(Ep+Y!MBFhVan@bCrZ$PE}_elg(ebxs2sp;iVsp93_)%m1B5 z7}EcrG#=B(R}=M%0peNEx0_XnfSrjlgkm;`O&7AqV6X1{$4bX|2X0zvCOVV;9 zrT<+w`=2!#Bghm2hPVM8{N$o7kdOTa9UR=_pRr-uzd;9gXk@+tQ!pBP;C|AX*Ke+U zF;)*2`x_JYH#P`RKEJzD|BJ)Bh(H1Gp?Ipv?|t9yRDZ&%k%i$bh_*+5=%mIohl76u zjZH0W+2?E`y5HPU3_CET_V2mVfAPWo#o=9V>EM;^U8H~v&4xqq+qDWPO~Q6#A}+Yq z;eQ%p$JrGCi{AD6X6}p2_0d^1&SqsB_^t1{6t;h}NdqR_zh%4^XJfwt5IKs{KOYZ3 z+t}&_i>tJ#yi^>JJ;lyMN2={;Sgm0Am|I#nIeF z`Zv=Jc#AT7Plz_2?i+5gC{o7=I2)#8fVWt1;mYTm&vf?>p918a@0#zwvEBe4_5c0w z1=z6-W1sT0xZs=pJhru#wND4OVeA`R`k_SEtl9l&X#WeCk&Vkz@B0C>j$bCqHqf)* zv-W>y-`;Ni_l(EYa)~?~MC0`EK=SXpS6GRu5Nx8MkrSFH=|J~P`_o9vPN;(k8@mS@ z#xovon9NQvFnwMIsy*4baoUHW|1iG-T~~E9Tj{h|Hg#w|FacfXmJS9sJ4p8Ik{%;2 zf|I6o<+VP6I0wWt#zTa2;E%U`fIfo+2Ni#o+?NIb=JLkCMFPhm64Lo4hh=a+1~n$j z>%5Zeg|O1N>j~r{(>rx`Snjs4*;F187!J$J{o?aqZ7TovezqcDHGLa&E@~%uPI^6{ zbFFF{OK*5J-{@R2y6rO?p9b&8f|5HALe|54k zP{zid4d|sWH2-@D1@NVg6Uz7vgSSC1bpq=ZM2^uHGdFuobdMqRbUtiAF#iih@c-h? zbR}*)v2?1bMb|ekWk;jcjSv3+^iqDWif3tg6~Ta)@-h{3-nND;mv#!{Ms_$P4}g!gIj#BYeP3I@9>V?uh7`4B2&IKB8MVXP*x?Mgmz zGcssd>{AYt?^A5VTT#~T|1j*$Za2?}qWYfhqD+kaihm4RJpNZ3PvY+fVOVkkh;B9u z%>UZ-dhP#$Fed+}9gqnlK(STWTHYCT0~%D%HE;FX1I2R1W<=BqoH@sg{4iKu8K8OQ zL8{nFI>cK8XFsBN?Dm-!&eUzm`IQb*NaM4+rJ)5X)u55?`m_g)R!?&}>Ft|KG3|58 zI^pCt&2Y^VeKxgpDxmQ$SK5TS)yT2+O|ec{p*L^$w>6v9RXLH52p^Z_&N{gyPMK;d zA9kg}t`<9a-~9S8rvy}CJwR{8VV>XzOAx|GE&9L;Sb!t0$wVGX2!47FCN=L-K&&HV zx2WYPK=DA!HI|F&BEkqv0F!|jGlZ-Ngaa2Q+68)L%@4F_47I3*nBVppA7il6*!ND| zwq9ADaAk63-CrfLzluBpP!$3r zj)i?9(E4lB_RbnEs-sal%OKoON$~Td@ySnkt&~#(i_BUlENeFZ?9+KAOD^`aN~gzb zjKs|>bYR+R!@DJLalhSF)-JrUR3=x+yC3AW5ukF&*V@-c!Cj~4WbdJNOq7)F?Ux^) zdt)~wQ+XEpbZt^j8^69-7j~Lz)2wq0u)-=r9r6+B8r0Zk+Oen`klst@i)&; zum2q7le;pH)sBT-iWtPGO3Vl61z$U)#|N2< z1j!14CI_6)J&RnrKC1GHGo9UZm{(q`ZO^UDx&DUSdriB2DS%thu2PV*mLcgFz#9Cy zvE|>NVpmuNRKp(v8Y<1nRtx7IiH!eFcsZ%$!h~hlM&f=Mp z^MDHY#jzsO!?V?$BSku=UO#;e6n1k=a;yK$my}O0KhWu%OJJHyX15Kt+nZ|Iu>-Gt zpr}pMg$0C6{}9vnqo@b{?CYXD2P4{kkFO_sL!g1fzOk<_Ga^ex-LPG_Euco{3oi*G zaCf7W1WLvNJBUr-Tj7j{Oo*w4d)kq^J_0;mj`aL*2EI&;3O{OnZ8j!>qLF*?`BfYZ*7tJ&C)em*t@70>9jcQ{7Aoko zd)F*vpP?*~?yeh9b0_mit8=^Stb-&h;y^%+-0&6r-I4#TLZZrIO{)9ArmB2oc;7@~ zEBYyb+wB(k0m<|hb?m;Dawc&z*8B`q!)n%h36jeZEgHCAM)d_+GYx_`)|)VATK!vu zOjt$esp+uT^D(!kZ3iMtubnHjVBvkwy$g=F!061bac`58N#0L0z#(UP`3^I6ceyf` zjGX#%)LnbB1&!;L{x!cY?(lp2x-!oPDOvWz7#pKJPn%3F@A}%zy)Kuiix13|+KTPE z{T4%dJu;vpqHQELus|ickQN^ltT0nRpKs(u`o}i(Gvbm=91E3JXx_doE^r*hxq|PO zkfG`|fQVzM{uS+Js>_cUI* z^!QyQ=Zlj|eTUA`W9c@X8)yG85eQAIXS=|e+g@3Z>{z0BaO5;FHA3)LPe{S+I^3xQI&XpcfiPhK8GiDXy0 zz?8A+eDp9akYhcXRM&H4@^MVWMem6fx8hLj6Wk8f*X$czK#aK|(CIqkd0*jx3s0%S zjT4uE`ZIPZ>JA~Y$L_GTr(4;~h@x^@o|=NDd}3W(9NSgi=~3CKTcxuqm&=n#Y95Au zeghWaR=RGQE)RL82yj3`=DhF`-a$li!kEiTz!3eoEG!_L&zTtIB|}@d+8y2nOLiR1 z_Lfbcq@K`KV#pCS%=9S%y-E6#0m~6SPX#ByG71H8$Bw+(iO~eFC{#gS_B=dbnO+#>7-2-&L?X z9UMJkX5cR)w+nuRVjf`CSLPRZXP%GZFwAnaatMJ_jnFkMUj`_>A-EL`x@~)JNqw&o zb%>}id*_5*n|pQ48D*Q#(Omc1Y*^35HBD6EA*wZ~A{}C|sY3<&E3a)Vt-6$TY&T~# z1u1c$@7PT2B)#T3RpNG=#*m>M`VI1y}bfWFBKH^kzFd6nW*I$RFlOS6L zO3^^?Fm!<4Iu8828B+Dr@4+ z`;i82VyF5~T+en?GG22~1baVUFyZ+t0Hv`R6PY7^dj?0Dy~MJseE^36BGg*G4bDw? zyRKM_2j6JciI7f2?gTmMxQ)Ue_;q?1A+1sI*A}S|`mX()s$`rzr_85PcVPzA-m@%^ zEp*iEAjIR$O3Qpk=hCrMqrIKo&C4Ci5bs5=GHB zP%&9r6qaa|DtNt?I`R0JTdl43E&u|LG%3`WLRI$bZyt-7ba?_;3xzGc^m>?R)A^b*e2r4T9i3MKxoXBg`7Y$ z`_irUzaN`;=+npUlf~*WJ%2^kn{GwbhdzaDCKgWRv$*?9oHFy1w)ubAVWVP9-Vq4Y z5FRCvZ5KI^C3b!c+e`klyhlDY?=VK!Ge#?;-Txw#Xb>c{oeigIaj8SJm5)SW_^3v7yx8b{oYB?M}+9lu&dK5e@kDR#=TJV<{?=^WXgAuED2%+elIaYk%L{O`r+&>#zq$V9d z04Y3V@*48TU=Q`}O=vl&Z6&Tn9#W6bNkK|21w3zgyCCK{KR&Xxt|&y`UH6XCP=Rql z{cA%DeJhyswyt}^^$JpArH<|)GAO#Q)OXWF%|Cx*h%GspXxcWI8JL>eLOalN&-b9T z*j={{RH30CPr)eph(yEeo4aRMpeDov9rhvQz?%I2^Ps<*-8K!VFJMO-DzMwAAX&LA zl8E7a^c{&IjD2MNLR4tk4|--eu}iIdrI6onN`N{+3MT9T@5%rWAUNc@7Go$`zs|4{ zJF(Jbf9Lq4IdjXW(1qUF>>8`AxF_D9Ch~7$ZI~sTB)4UixDO6*P>VynyZH*~ANXP8 z-H)Q=ghkA&Lm{1%I!5D=?#g+pLkjd%p*A0N&l??zXuxYwUhop2j==(xKz`$`s9Eab z{PDHy^u&t)P-EHfk1e3mfB|A67~~$rKOq9Z--4I3&`}P9cfVn<7N5XNc{*g@8Dw+?(Zs`jI@ZoKmsQqrWsI~z0_>H12tlJ_kw~SCmz4LXAKD0Lk7%G+Fp35H>(ibIkPqtMH4FB4sF`7bO6n_L-#J4nW!ut|hom$iu z3VW;gYJwBDS)=Xm;S8&t!skegl#z6zy86&+gfx=~iD- z>D;s&DBj8N(}v=?AQXX)U^r-IN*7EHJId1l6^t|Q{gz#!-VzgTeRd2|b~*&F0ihEr zK+6cHH!-LRTR7jvH@B;J&~S?3TpVjtDvfwO@7ivAGvFxsBt4m}?0lOS&GwN_>Kw`5 z)1R+EdD=FZ%T=FTkT1WCIkal%)g;REYB(;>xQp|EO#ryvKz2ltwR5G*BT%?}by$ag z@|Qo$;;llnT=UhDp6Etc9qi>YNW0brwY|4}iI#0qh8)`<}F@GXWX z7B{(%F+xSZFFw&DDP0E2kh1gxVdWU(vZrm`buCqQ*O3!L)nkZyK=*n)Sj4?^)t&iy zv~8&4TtnUCCR1e_#Bh{+gkL2p;zs3KMXeUarD}Mt;=JSfpTI|K8mz055Osqxl#1xd ztP7IFM7kOwcsXgkFOg>PQ-(F<`WPF_CS)Hpu#y%S<&XLfVVWQlq`l}RUgWQptebJiZS z!d%Kz81~MYjmJ##flj9zI`vBQmw%XYf>8P35q@= zO~w_+gQ(sPL6;BS2A%fkiFuW+kE&lUZCT11rp;Dd3!*(w4j!=nQezSlaB(ctN<+ku zaXLi}vA_36u5V!IZ@Gcu-P6s=HqH=f-X(5WQaM`tu2IW|=H2)c=_e+p%Ia-Wp@V;-X?KAC%Z%8T|2 z4uf8uG>}FIy74bz<9;KPGlbR!P!%u6I{-j4ft)HvV@lFl&T_kS26C4b5+?rtUFbx+x*s(@YUbXVV=JsMaK3||I25yjICC|3Hna1**LK(T!2+X< zHX+9H^~yVaYFEMdV008#O+I|fiB6@sz~>0n2TopU8ssP1<1t-|Cdakd$_Bcp8>_JWGvh!#U@=CYpE*WQ$3Il5TAJ1dvozP@rjWz=Y1aE3T zE&*~Fz7HnWq{sxgk@qK`|m0o8{+vy_ywSk5<`E75}t9>SOU zxXWyXEDnY#pVLWK!<}m3o>{&|KTnW?3c8-P>;fTdI@^}m;VljdxD`Xq(G#cM#68BT zzpc1X_{Fhls8Xv-GCePRS);SNqKtirrl)K1yX&odMZWoVA61~D<5H{~uN+|1q`sAc zm_^B$J9(91A9uTz#hJ_v6-Wo=YNy<8>nY3wyTxH<=4;KogfJ}GuNcvW{e1-8#48%0 z@L!eU*c=o?Md2R*XZ#yFEuplaw1bbr#e-#4Lumb(5CqSh-aW|JKiMTR6B3)m?u*8) z0ClYuB@5j;Fv4~P9Q9^dAcUeEPnxSo8I%y?X2#$KnCy9nxhF_c`aoSW9KkuURX_l$Dp-6_l~8**uPs+(O7dLHWK``0F;tb>uf z!aMVMnumuTGcDsj#1ssWl-+Y@RV;8j#l@2t{TW%hCI?(8PBNLtsyDhl)D>YoH7-*0 zsF8L_JqmgNcJ}?yageew=AVKznOZ4r;B=@itLqvavz&!D84)Q665`L8Gqu}BphR}x z4-mQ)Sa1p>Y%XAU-zYM4Ne(ja9I=uaVE3J_9j{C56zv^= z)0TH&r~T>&R_WVlpL5C+${AO?x=x$Ehq+{ndPi1{>qbW@<5gv}?3mSKi+k*P9|ipJ zr*ZEA1}4bS@{)5`z#**nU9`yq6VB-?CuTD%oyaG5I&sUqd`G;>E>U&wgezziXk0JZ z!wQ|ft8Q%2I!aLis;{jMvahmY4mSjkzOH=|%uFaocmys^*H15w@=c*ymghCV-Iapm znRGcq>n|-oEx^QR^kkyq>ZD#4suNHv;jPj)Ksf)0Be%4VWk!xdOxN1B#_!Q?#cK?% z5+tw}*w)XWELH-_b6{~b%O&D?XCaxGF7SS9l0lph=iL;RYXKD|SCGFj1}`UEc#Uu# ze&erAG9NjC@1_nzk^H0BzcyJ5GXzK2%ZciWTs74T1&e9D?>tW~xl zU7*)?1Q!k<+1}YFbtTRX2@$%UV9$~3YwZNDv0&SQ)T0v^`(r(Agc7q2EK=vLd z#l%`25NrpsW0>;gB)pjLfgpi1%8p$6_z)uTEQAl%9F0XvwO%#Cdx#JgEM@8^juVxu zh0|o46B1z(%Rmy(Y7j3|FVF)bx$|Ftg9yLWf-PyL_}Hi$os*O0?%d(p{>K|@gl{#p z9H??(xP{`EWjWSevZia?5y-7f$(NoqPo*lmC#%X|;~|C{_55q{fC7ax)kSZ|{8=Fx znJrNYQ_jlXbJz_KMdeXa+3V8`msQTZ`uJhjoW*H@toqkqZu5Qud-i+DPIWl94QG_n zMcfioiRrQopFpLCBhVaMR%E6~78-5+&>g7=B^4Y8xeov%uL!<+lF0cj>|um9;~6Mu z2oLoUjhjy@{IAjq&4{ok+&k2OAj{`zL%9^)q9>2YI+yL;j}kxs`Ny)A#m{&toFff7%?w^kP-HB#V5tRGJ4Wtv{IX4_Wu_orm8JR4F6{807T%Miw0oTL2c4w(mX%V9Ulgl6a9|ff$;Z#BI-uVnM@{# z@rs~$X5TK@kJ$Q`1X+6U^iF&)dMEDZ8A7Ic=U|7*$%2B$^OP`9%qHo;kF@1l|Ir`j zSPJ0=DQSJxmnk4{A31ptCY?fB{M5H#)Wd1|`7Wk9=4jmm$5ZVybB}vXXBR_Rdy^=- zmom#nfOgr@8#?Ed6FYmPg*h3O`nE4zPLb5k8~@aHHp-c@t=F`?zoWt4@Q<7t21DL; z@|aVxnr)5f6+yB^4KyXa;)mcn8!3y1ii`L7IUz=w9W53L&jd^&DC?q?QmF>TMkVQLvlc z7Re!a0$uD)Xn`3g=w-9(OG2Gf20IKk{$(bg1}=;Q2wR_4f_%#^te7hY$kYTCW1-^= z1BJ9QzfbD>xQeND^14xNGZ$(8DHQck3-k0&|LQMfm0_#q56_wYDWvOd&RI%lqjE!S5KkWZ zP3loy4^f?HR-U@Umv{Wq{HR7o_=jfbyRkd{(V|@gfJlA{XvT$tr_|y0E#}_D4-ggT zGZa6|w`&CGfCluHx&0n5Q+QW&cp7(}i%znVI{p5WmCja5>kfUSp5^Roz=JF zQ_J>#RUR(Km-B%f7e~e>d~T_P{P{@S54B(mPKMxTglR2O*vOYW^yWXH8N!9CiOUSL z+71Hu1xvcUc-QfV_+H?y9GFH-^evflkM(snR^Uyzdp=?BKl$uGV%kBY z&GdxX&=7Le2LJNFmCmzs{aHg#E1u=s-WsXAk!O$R=}G&TbU8@7Dm0VY1B`V|4L9_p zZBL45SI4vcZRN^r4PN8R(;06%i$XpqQG@P&9hx5`Eop9X6)Rw3dL*WLvAzehKyN1$iDVD@YJl{KW00RN%*u zQgsHdT}Uas^e3$wPUWuh%<68l7y{qFDy-*$x+0< zXqUB(iaWAiw4#5;R&9%#S;Wt76s}*^$+lwOm=ny41E(m$N~dCk8C>3m)Wxb+TD$_2 zNontz417LY*q^+=1e4N+d~*J7uTT1eKJ~DnJOo|xxB;VYrd){`rBs0-q3(^jb@+c` z>GWnr^2*d{T-ldEa_yr)L6K+2eO3+}BJeW`?6^b!sDm3_WL(MOO;z*MYGZ}k)X zp8xDE76!zu--Z`{@ou4Wx4wv@hFLE4)>fPx?H`+%I{k^4e>EJbIEQ=&2)fhUuOf*B zz5$kEFRX6aK94y;JiA9H=hnjq|n=;a9GV9Bw|N!LvzfK{`TIc(D=w$Y3q13 zDTG1rtIks0tZ+XKXR7N0HdXWh03$Yzg13r>2;0}f?p2MK&uA~9xUOv_ySe9+I0ekF zOU`V9JVcnekWz-7=IB#byxU$SjwN z_iT#nNcsc6vC(%A3 z_3SW0o2ANkRODq1z(qSDrwF|cE47h>rUn*J!Kvq*=~VSWkCOQ>8D?*|hpog8<*P)` zrZbwHj3sTzN**`D&OQ&y__B>nPf*F#%FOn6fCDuXlFBOS)*pXu@@x+{Ff+$oX|9Ig zOFKL~J#YgImwXs#er{yIh@4)<`702>dnYDa<(+sWq>LI4u1MQW9Mw4icFpS&v{VffFQeZ7=- zpxbe}%CNyYuImvlhJO(#QXd-@NfPDolYYBFfC#@ny;EREhHfEK@WunC(=Z6tHFbE1 z1w`Yn31F8w8xTbFgkr~fN-6>s6vjkupZe7DQ^`U{fu+GnBDp>-B04^U&{K@!UFO)f z^+8%r590YKd?&qjxt*>S4D#&FRiZB9)9EGSnX05|MVvX`G89dNcNM!H6W%7A=Z6Sf z$zdv}8v$>_TYdta$e&3RD1r>whza{0C6DKSbr{swx>r)F?p3ru#3?hONYBzlXJuw? z6bNP7=V~VbhCOqw;>r)jmeHaroz92Jf!F*7ib7qE?DfmDZ1AKphT2(7*9knhtjpmu zme_7=lXi#FV6uN|r;RgpKXoV{o$9V7>b)h55#|sA`%;iKgvCh?i5CX>P(nsP^+E`< z9}^W@`su29@eJ$)oaeSA$G4W^4Y}OMZ$uG?6(6DKmxCS;w^IHes& zPac$%OesImOVf>V*OCwJCDY?vMJ%@uw_7T^mrqswiVV;B)1nI6x4cn_*d8g2DGiDA(>^mxdkvnCydAv?1Fo zWwrpx0Gc8!BIOpvWp{@^6ZkWp$~P!30A8%;Msl*4w+O9ipP3(|WHP?a-Xy!KraVYx zbSWpd1IF5&tA(!ZT@%L$?v?N$Z|61^Sw6WF?w^`?O!mdi74EaYsvAaKY;YCUtef-NAbnw?H-)UR?ut=QqF#~UiAD^zHr`L^DiLCHF z>Q=zw#6ScdnwvD0LG5Fq5)02$S+Gq|SNj#DB+cP8<~e6o-I5D$?^6l3I+Wpb>j2J< zG~x59WnYf-K&5{zx`H(ioBv>$d-0ApR?Wk?kR25rG9^##2dM+DQJxY@hlVPAL{9!n z?dTEPHnU*O)ET-jiru0@T&K196rUezw^u!uPJUz?IHVFLzPTZ>6r?_?LeW0PYD3Q(j6jkEOmL8y zwl0Irp>r?#Ki3Ie^o|}IO*Hi#zhbs>E1ja_kneJXyzNQIVActXAV@spdfq$~d11}q*h-uUb}Fn~XtN%oO>eq&tWST?Zr{`2m-c1%zV2GR z&>ZfP8D(*&O@^{Lj+SvP($!`{`*K}QTOvo<2 zHMcY4C@wBVvw;;99edPNZV~N2>OEZhA9@cBqoBryb&oC?l2Zwlj4}#m`%+ATKkJA8 zF}(K4q=y=vWq5GSAvGcb;0k@PHwt8}Bb5uH2W*wjt0|XtAgYYKbT2$JWG=)j*?{b& z=XlR}hyA6@fZ2m?WE~sBDVOx2xmNuno&E|3{rV*4U3s^@5)G_^zjXIIIrY=q?k(nk z6bPiP{n9{e=12T?AxI(y6US6}alqLPhG(N|3_7W-s+{SkqaDBn@94{JjQv#$N|>3R zP?=^gcUGkk#9Gv#doPg+qn$`OgiN`0ED6f!cxGNO^%K2u5a(Opc&6^FXpS!>(BO;$ z)=vz61pOnzfP-YlE_Tf1C=1jebFLm!yEStB;mXvPnXQ#GRa>A@R?e@cD4>|ozXSqh zFTrO1Nhpm5m+*paJEkzcoPHOgzU^(x8#YSWH{>fwPa^HoJDAk-Uf!o2k!67h2M0q2 zGq_W|x7hOVneaiGHH!|P+i;40{h^Mtg}h6}CuYvzcB((qXx4{qu?r{*d@i^WkwS5R zIkkw%dprv6Dj|pW#yP1!(;2GHKoc8{m86RSegntOK_Zn zVS7A?r83mhIC|}VlAOr23Y>o&4=K}hRjxh*c3lv&FJnd+X)fBH^xzX=XOlet1Wsmf z)j*rsOXDQ+O1MtNKys?&N7Rv-$VF}pdpd=*024WCFM^A@Wc=|#I8(~?CreS)d z2auCp3|wyh^^_Fn`MClAiih|S<@M6$N5MUNRGxUp4~=fK&kg*%ux7#5MTmW<1lqxw zTtXW`Lp0H6%&^QeI^0z}#E4f}$&8Z==`R;Hr^Fm4ALw>a_AUlxKV8iH)3=9sAR|dW zS^H9e4I-TzK6I8%?!ITDp!Dc`MFW`HKhUVCO z*fWUHe|@2j zY3qOR#)(+#PD<~tzEc)Weq%Wl!af`&w^CtJ%@3GomZM;)mDa3J!}&Xt)B|0zd~Nm* zMS;pJttG8Kgo5tjQcX!e=gd%MaD_=6p6<*-Mm(-a`wtAdQ{swT)mD)rlsF@;f>rHY zng0!I-Z7;(ia5fe)}a)!#Fr{{_&vj`3mFE16^O{0=19lkCY>lD(X?|?c;r-V69}n& zq+S~xh!K>57jO~iU%O+tA9=TcD3!WEjZ?5F1ZcyBm$0pP7;X>eD}x{o;vz|r68&#AWJN##DQ%$6FL)5!|C4Y?!e%sh*({EGB;r`s>%RaToT zuMY23Q~hvjz1fR(=uYPO{f20{z{Iwm%`R8@htJFD^yFKlp+81We0}Ms#6^OH!fdr7YF3?S2-M;{BzE~}LIrFC z@lKh!0c+{B=y-<&#rCLqOZN)1u#a?_?Ghd6(Aa}HDiSfgP5#>SOd3#KLDW#2vNXs- zUn6iH!q^3jO6^Wh;_evqcmXemLvM$rp=A-<%`=d7`m20GDj;%HZGsFUD;D>i33K5p zSq~MoPpJpvL~32S4Nt_iqB8yMX})d65>ZJ9?3+(|$cm@ud=4qQUB0Btj4J7L*C_L# zq*l33xUH{7opIOkImpr}IbV2T!YWW&1qw1};Mc*)Ro{#P0DR#)2nwsj zlqm^Ly+A{>s~S|1i8%{P5t3Z-j*(|q^wfceyM2EHGcs{{N9l;lh*D`|(;%UaCU$gf zbyPMka>HTuTBT-+0l~S|0w^_~O)5wOjB24@ER&XHcU%Sm= z(z&QG*I!tCgL*H)$qGoNjW4)%vy;}#W+{u!#oDi&3ZwqSA&04dG|+Sc$c2G<)ev@s zs=JZt&z#6slyqrgQBV^B2R@vpopzA%Z~<8?a%gu`Gue`EggZ23@@d0IRltw_wP_D% zEh0)lZR=k|5@B3%-dRw@8ty|#7wHVpL(xfn7;c}3@GdeaM;vG?13H%xi-j8h!AEAs z2Pqd)1B$L}YKCYiRlo`bc=iMzheEd3F!IsWFGg!i7C}FCxK=Jbw$D~5Dr$&C{PGk> zRQ8eCqHN{G)?rseO(!ubpf#RWem)PQ@}=DMP*?1SXbyDYAgjn=XXs6GYCGRD+qtQ_ z?sE8~cnZbx;_VzW*nG^P)v);9(laP{A{8}OvGd2O@3ZNC1upc%Uy*y7j`PprUlD2v z0(ouTgL}MG;bVFb`qye`lJ_I{tYbT7Ug1q};ZO+Y8C=nUnKPj&lK4%emBZMLWw(#$ zRlTBAr3w&{`J=sNiGHSK>YIF^$MB^Sw5qCi0ITy9 zC_LrkU&5ffuv)MN5usnE(}|o@(~u^mlP)$-+-bERRI(qZpOu!y8lP?k;1g=_G-r}1 zRu@?}h4pN|ipq#qrq|(+T)&ZdSS^%tY~YDmmt_S#aOM2zpKhI)3feZ5abuYE*Cxk* zaERGxb^6Tp>AKi-B=aEDz>gEP7mhqgC=F3j%#0@PB(&N!9p+4eOqL?f^gjC4<4+~K z!F%IOGC^r935w;Vb0R86EpxA|BePfOG9gjk3x#><6OslFr|KI9fkF~co@f;Z!Ob~N zP8WR-{ynVPshkUM#k{1SeX)_0ypB7{S-ycduH8nL37t9wp`w7t7Mt8+IS%=-);`|A z`9E1u>AG>A>z7gK$c*qgb=&1F@THK!m7hGTlH#JP*XCXFJ=3uhM($d#Yt;IS0$qIH z4O9E6N<$3hg5@2Y=?PbA3fbwA=7Bd}oixN5-QP0E*`&3rH|=Z=7Bv%*`j|i`G79Jd zc~E*lg>cR@s+C)U1CUV+0C#tqFyMeZD=j#gGY*z}i7AZ3gM&78;l5hlCJ@a24w$un zvOTPoL>gf56Y!`8A3?tn=qhr_8QUFF+P`M;FdaUr$cw#oI`CF^bhZU3+-ZnYPL&|t zas+LoV?fLSy?U!}Obld_Gbxn_?scqId;-%e%rV?j4CBU>k0bV$>r_X zuWjI{2A!^7H$9m%laox7?Ar;E>|XSnmixeDuozZH3Hko}SEbXwk75fGW-FJ<8{dRQ z?Rce@v4U({P9t#sLBC<8G%OHj(m}Q5B9j%F49;CbtaiNzbtP{=?lKv$mSi}|qfGYx zf*tCxx1!wwys!#nwCFo%l~1xLlVejMjb|!F#4y>3vG+ccZxENPEo`z*#(0P2$K@%# zt)w?Q6&ap-v{a|t@3rVw4W(WJ%AS?FAhMz=b(drMq@UQlvg za5t`LkmNqY5e+2S*=Jd!^4;DAIy<p?<9Kz2unQZ@ zH}-yBC_=eH+xZyqvNj41i;r~h&=Q21hv*s?wBTp1JUv~Iz(#^yisMY*in|0oU>9HM zIzEUnB>Q1|SRcD!IVBog+jNa;&Qun8Zat!zR}&RcI(}NK@`f4bgYm$^P)v>dG`z0E znUYe_AP)<27+YECu+1;FiCIsQf|?sSMkG9rX@7PnDPK!)pExd7kAGAeHmI$M_Z0ZM1p;{h(iV5oXB5>Ydv(b0)QUSHy|O ztTaJQJo-A&<(mOCp-xJvpFju9>TR6FKwZLqw30&v=Oak2a=g)LfO99>{?0rnxk&^+ zgPzfDU!H_}aXgx();^}udR}R^ygjIS=9pmKSwEKBb@4aq0FS2Io2l1W7wSK&@-{2f zh3tLMRO8dID=E%e;9JDKl}S3#Efdx6UifIPgKxgH*TqyLH|$vSP;MYfM%{a;1r!R$ zXe!&;xB$hp3rDA0;S-9YZ9nr7&<9XSjbunqe&OlY0~xubK+evz$IwTSgwK5ex)u-x z=M%gKeV#)>9M_*AI^R01k?=Ay*(lV~@A?2Xvh_;i&Xj$4`Tn!FQDF&+cG-R70^nq9 z+1PB&A+b(^0)8*nK_sEb-xOj6hh|xk(m+5qA8eCS%^VxhzP39@26BA~MCXI+n4~m5IVkge0JG^>wQ0o)bn}#mudq1tzH&yP>Bx~!$Thc_d z)7>sjW$u;fm)co+LzgPiGA@;&!`d!1I|oZMM#agCoUCmD!xp`VGBG>7ok>o4meH(+ z1A}Cgv<*n-VkDyH2D2<|48Nl6zW>5&1fX#KpC4==Q1JosOJrE}zClOQe3Z2u`Wbw2 zdEe6>t=u`as7|8Hr4{2{^0xa5e+OOo@rr`oJ)&Eu`I`u=bNF7mfcyCSzX7!1|HrZA zv&9a)959W$=BL#(gb$Vpk+3mOk%A>Q!;ahhzL+AAvu#z0Y^#iASzUX0oLMQx>Pr`G znwy60(j2SbGW-m{75PqasO1 zt|fm)RL&J+9ehy^kvi9TRUBy#;nU9u>2fKDc5hyJLR3f;b3EL8excVV@Rr5+5$OzA z9a^xfhwWF)3`43DT5SQsbt!U77h4s+6Zkf*Fpm2RViHWL61Y&}ZaLmu%C*lTbAhh# zaWq;f3@K%5;4x}j^0Dn}<7`)oH%j8uxsWhnB1xE{G;re(_T-PQUO0Iq1~pYs=Z1e$ zeT$?!${Rk=BT=G2R>{)>5})Ky<4o#krOLg=Jgw~Z4^EFt;EFnKkN+gr=Gl3!50%Hm zX3D0$gn}@e!b+9?q!YbHH`MUgH<*nDqHRJ>eXIIzD1yTlNo)t+;w%UCD^8sG9@gf! zgOid>56c;^w*&czWSsGRuh!Dw#{D^^6S(7Ei**ZOlEE73roOtJdLZ}mJzLaRW04NM zw#NDGCsn3)IJxWEE$a;3&V3}9-9O%UEDK?(VsX{*57IHi8<_IhOiYh_?3Kj=&x_x` zSPeKW`ln0Om1=qzygd@{$6c>TpPX%Vgf)bqDA^vV<De!e-ztWm=+BMR4qsX`T@L);Lrp?y92%JfpG%3{MLa8 z(>-FRA!-!pfvvj1@;O|&6cNE}%&a|QYf>>_zL{@S+FJoKEMaE~ z$@X%Luz)f_n;mpiy6RS4C9f+-QpSYzlU)mSefxpnxl4wx;2llJ{+Wi!!twly5SF0{ z28l_nbb3ft^6-lBC<=Ah?h>P8*Jd?PROZTyakngL8IW7BK0fJ^nP)jbN}+lfm8eSV z5>5b7qVM^Vwt}M>-z4bl^o747xkkt!vMD#Y+Y7TeVX71 zvl#9-L|`5oE&Ouo?wT;9s_e#Wa)+&|?Ys8QEN@-3;^zg+OH}pKeu#Ig!9x@)bcju* zWlV6lyr#0YbK~=nnRzqLut-vdYyT0FfbY(1g4Yy5(W&ygJM?icmXlM2nEF34BY*koUMoYnqZ zdj2y78p3+}WJtW_&|F`>y>kIvpM9Xgy2Hzcw!=f^;fDuVBp+!PZz#j?Be?%>aZ+EZ$*YuaM&VEwX`#mMz=lz{yxYMP zxKszGe&#AlYU<0%`&H}>PZ+8Eu1Np^*!PUscgKo-=mRB#<=~gYM1!{>z$gSR!G?c#&Mx&-Ot{j1BVGOeX^S4PY@j2(v_cBw__mJPcUJ@#K< zhQ#L@nreDKA{0isAC7?<0v0@}Dj|G};r4(q8{H=wb@P^$BPHr&kB^9b+D^o;7#FwM zqh^;1o|4r={Cw-?824_;q|Xz+yShk!vlwrRf7fc{FsFfrpJ4ZtwDhM_8?ho-@p}vR zz)q%nkvHBQb~fE>@xGnrEpsR%_ommb?zkUMiEDGUfwKm-smss{3O2|N0`t2?O86_{ zI0ZTa`Vj!NIfV)r5W}1dOiokK{akXYwpo9hDaw z86{~F@drVU9zuZj+IB{!PQW|Rsp$IpMN=xa-Y#-SvCSSuAUz9Cd9ZMnn&0*jB=aYZ zHL2lkd=iYo6Lc^@B-Xy(y*^Lp=OoAxdRcCm`WPIcPdf&fe{k0TN_cmct9J zE>uP3eyYmzvXv~5-aiv>3k&HcDXkLZ3E?@AMpb9C7*I%svcMFm3$tlqFj;I51HH?I z56t)Vd?>QDMVo`nLhL1nSRPDd!4vT&9MqB|B@DKCZQJV7-rbm)d&|<5?9lb5(Tpgw zhDtla2XnL%h>`1&TkE8+>G7|K+Sdt*O$JZ#9(d2@3C~H3#6Q#nTPrg(pqX~^sIu*|()~2_If`CemgSIJT)+d} zn8!m8EQYSzo5lsR4Bb98))ZB9yUwe(4#p*$FR1f5vETM;6Wk9F;h_KyIe=-|aPt$e zsMbQYM)DsDXwD>3EN02aZG7!@m5m>77l^O^K|73N~K* zb(_78jqxkb$S(}Fk5K)p;B{r6|HLR+6Rwyr{~Kp-AJ6pu|Bu({l;UWqT*po!*BnJk zY^OR1napJqJCfL>MPxGDIfc2{IEj?2rCDn(l3WXmxp>Esn2V^eEtRWmU98u!+4(+u zfByM>zqfO4x6keOU!|MzdOe?yi~HmLxZkz-?9Fl;_U7JgnciSLdwl?<#?}vf{o!RE z%hVlB8p#7~11k1LZ1J3_d+dMz?fkza$`&us$#i;9W^>`&+tfsGYRFIE*S${eYy#0| zCAICyEO+NA9JdMH30K8ymg-4dZY11|@^3`!5%=|EIe8p;e8aP&<@k!NY#``mKs+$S z)XqJmnWdS^6FqD$*09R0N$vG1G5aH-ejea^!sQ5@3(DbeN#JSE^BU%=G@jcrwG@tW z-W!p&$+P0TyRRa&kGi>=zXoe9(I)kzKC!uoBHzZ8#?}`choZsrGg~Vqv>6-oCK2k+ zfMy4HV+Ot0dSq7+jQa7|y~&!i&x90Yz2C2OKURKQ21AH9zRMt1(r%S zvwy2+tOM4nB1+$i8h(6ctL=C$N)Ol8ZuuZC~FieALqxC(+;p z?PKa_Wfs@c@wKVvUG|i?`Dgltn3bIHH?x%h+kqgqt6C6D8Pr@Qhc1Qg_F8YzL3BS^ z=6JV0D%7w{R}i6dkvTxUVEtma*mPJ5#1NXV@V`}8c?&0*CUF!{XXC*IR_cU(2dw-M zI^Z?BP|iAOx=F0*pRLn7%q043@W7CK5+JnoC@jUJ9U3~8Uuk4}h%fzP)G=JRs1?W9 zNDOjyAhT+MaeQLJ@f_ZMjGI7OaN@?;X98qGm8*$R8+e2*med?OQIT!rubkJCFN%&xb;Vwxfd z8M0TkQzZyO1PZZyuop2e0#ctW?raLs>b?^{GnI67tsNPfgw0a?$Osv=sIH0U;|fwzQ6~tuIaS1@u0qS~8ez01uTlTUF-x>i?-;r{d-bmi zG>gi1g?aX->evdH zG^fVBZMfc&)8Vd|8pE)S=z6BsXthbFi&b?592dfBzqhBY;*7aF>q>(eh_e4wFVudv z{-3<2YdV7c7Adyu(2M9sr{0kEtd$JYe1Ljfq4qjLdK;5xc)of#f2`oziHIRMSR%%8 zq21JF`${OR8w>clceu6ZJNAmRGlX8sID|PA1CE5Lc;?BF#w*C6*9WH=VhzO$z0|?Z zZkhbhec%L}U6}~V?_>|2L(ib}_v9ER=2bB6(ct&7)@7#@ui;|n;QhTw6;c9IG0hO8 zUaNG&zgO&nY6S4ETVMblNm6?5Xg^D#>K`b0vJcAYx?+5_o{;%RWbOP2rK?74czJm* z!*F~XL_VC1@Ce-7k{ywj_po5Kt>kcjIkyPPtMz4V{&XpJ7#q^($96fu^ia?43p4de z+pnEj?bqTqk!|`|XyD@O+gfo+if6C2qjz5XjQ3R-oj|}^=DRj>dqg6Fqe8gW|_1_!jOg5Lw@e*c_>ERq#Z|l1pv-v2}(A0s*!$ z_XrbPt_gUyzrb%z3-e_J&b$Y6!KYUVX z+FNz5DlA~Jcp&#ss=5o<2>q~~JvexZ5JF0%P^Jl5y25N1O=U0gqKay zz2btD4%J16i427HB3BhT%bK$8qGV0)1eCwtdeS9fl-HD!pkP_A9QydFyTgdlUAmjm zb0u{m!?;ZnJl>L+AUxFIQJHZM7dQG4IaycYehYQpV%aR;2yuQ_n9hMpQ-}V2Z)lR8 zW2h710cJ1$Gf|1Yl>aPbq@o&aQ+&6gFTcx*vBJW{uFY82y}c(T+`d?%->xQARxU1b&b4vd+MC&gVQL*(r&FT zHAP^r4K3iYR{15td!H`B`_M>^*}+Jc;zX8N?p%s**dV$b9E(TeJ|NAnxT7$)I}U+g zAP@WkKO*)*_C3h0iaq~j5YHBe*o)!x&w4&xil4-kOk|YTBs)m_E~D*G_2dA1VRdg) zIUc_4EY+KT!Gc|QsO-9S@Yx?T|H^s7)dBFXVjalwT;)^b8F(#&wybb~B2eOh9xpXH z9(-QO3&5P%PV$+p)|l{X5`Vo0>*40W>)9EOK6i<(9t9y2A3oEqQR5a%aU~wuj49mdh zshEq^Ua&)`pE#SJ6&JS`vvlg`w2wB}o7u9KqFh*T97^4Z;TX0jddIgy0eJ*+lfY)n zDNQ*`p4E`6urI@c6eZcwjj#tlasGRx8e%`Ovd>}9`z-=5{RMb^Kn^9c-9z{tQN|N> zm4eYr3idR$dw5}5gP@Fy7Q7s^sbAa=3l$egAA)bN2N?DAfYqgMxfWdau))2S5kkwH zo*WG#`xN+k;d_u=%OtSA*7H|9SvMObJ|cKF7koeuL*qwJ-JhSw)jWi5V8(2v&p(@6 zZd?v>t#7K$!TVKs7v*a%-AGj?U8jY4ObXsz{x_l8z6lp&E2@=^=zDZI{wLk9f_4jusV%1j)3~vk%+MWuQ0j zRBF?dJo|f{Gz&m*%ZsvY$0P?;f%)ke4CP+sFjIHwyf|3uKZ3DKZ|q`1h*8U92rki;J{xP(t#}&GfwE3@ z{JqZOIeXhe0%Pi?!oO^ny?2jx(iTC*QMEkrt;EnX?>3LNo0__QEgjh>9m_i%w#$=% zOHaW)r(-lLXwX6@^Yz6T$?F z=8$|L8v!FnO5O@Rtso|J7of54HA5_($5+xdBzv0qXB%#A#yIBe_%BkWMC6K2VBwBO zWS7aj39^I!0dY52 zO-Lb~^h=aK{Yt)p|J{PtMZgX4f($4b2vd?1#IL4wf`QquDl-`jWncsd>zw{hVl6-L zwNsa}WWAN-$LLHzIPtl))d-6|_5y>z09WAUv$lrj<_1MyPjTa7Ox)h0ZZNGI0(^r^ z@@|qNRQUiLY``~AS<#o9K9W2rnbjkbUvf7rQy!jI?0zwiNuY%){(J=0>XmY&1C}rk z&xxB~RSDAI@#+nZv{xVK_Nbl3$^Ef;%&PjlcMiD;?O#gV29gg|Rpmw3FZ$l+xWvBo z4STAmas6#x^}7{!uVAt(l@z=Nip1Ge#;$tC_c+Ej@Mk9cQPl*P$=Slxh^{=uA#s9S z{lYr|P>F$#kwW5{Bnej?T_rUF%JMNL#O+lmjXj97hP}cUS~Wbt>x7gg2-**P{5TR- z?a&)V8p>pV)_Dd?o7RH@1TbC2BvR~k5V%^0br!&XAgvb=z9Uc1sFHUOFH8v3GL}}8-pOZqsAN=9 z>B^&vyqm1!{rlNpvI9AvF0F-NPxmFAx3(VxXjJ66kSV&?@~(vO{TyR|eEF9`wnIY5 zhZ|DF*T;mJV++mLuylnJtU{1Xcc%m}mlQo-kQoqo+TLLsC<5p5TOvZ{+WP(S%UUe} zuAF~yUhn4@Ahho<`}v8qsze2dWrCp5l-3_tpiuUfefLY>Dap>Vv)|yFxUijyA4lJ& z{j4jrHW5G7HIECA51$>6ZhU!R`MOrG8f~}w<|7yz_oqgWJnp*m{SzcK`p?kcapv2Z zDdc~(igzoV^yb3a`tI(;(#icVl#s!KPYB*Rn9=B$R(zTuo*HJ z2BadaAV7RUihUmw==wHIlCQtF1+$4OT@DY*B-X;2W%Fv{jSxPltJHB0(PJG2_wf@L z_blfo*_MVs4M7`PI~M;?G3T0DMmr721XkuSM@x6RGVM<;#Y~H>nnl-L>~?!ij`^_B z)xqk@D>?!D0Hr;X{rspZtW; zXzT5)!43yQOXH;#iMm1d!I9)41Do}}v2Mb~f5x@{mFe_XlJxhrdz9HUWc?RKekBQP z?q5wM_txQ81E>u(n>uR0O})a)UeWOndXY($n(9+f?p|dx7%&fFX6Z}C^tNWGVKh^ z4i?+tUz~S#$TRn4o1t^*$4w7b6hFopnV{@UM`;D^AoY*V3xE1@M-%`{XSu4m#4V!P zXj`%&`8ig61fHe82MQAsv=lGF27}gP2!x;{9s)Kj-wl%WIw(Or)2SH(6Q{K^A!euI zdkHuf3@ThNjsXK!nu$y(=VxG}$$$aH?(zCys|D@7wY5!2S!p-lbyKE16q}Kf2oW+x zfrMnz#kH$n7#Y(rgi?~nB7hF;c`Go;tNRAYN}^vQvQD+9>_BJ{38Dr@s^TXs(W4h5 zt*s)WC>oJdqtlvW4O{G-HhCic>KG38#gCU^l`@#XOWs9E+vg{?H|q2ZrV98Mj;FMtAo7c}|y5t{BcKsI)#XlZ&|8w^o!SHnkxWfPqew%Lrj{M}Rwr#Sq$s7`l zr9SLQ3~iU}7|=OR-XkdYE8RXY_R9dHtpUWoeoTyyyp?;ZhU6e|`3OsIKBfKZt%@(9Dq$ZQ z+;0EdgZszl_OBeEa8QEy5+K9AjL*SX9@!AQw@eD?@Qw&(w&4m^_qN|zq%;gIO;o&} znx?wJpO;+I=whNF|`~7P-o| z!3Et&wdKz%FWFLkNjXwozQMYSpxqLsK?}kn3SQEJjS_8GuZ>tR^HtKO?sP zXSHO&O_01nw*ScW+)c?&^y-=$<)@hXq1A+zT)hY<`?>vb&ys?_xJ5f3BJq zfT1r<==m=Cv$S2*!w1apH{RaJpQx|I#5;W;NHf1w3eC^dVpk5fHq)F>_yLlbUtY^= z9h4cP&ih`ady_@-i%cR-08>Ny7r(AE^BvT4#`q5-Xsj?rl~K zSsY0hY_B*Xg*lrSFC&b^IW&^GsAhv4%a5N~2hF%iG;XtJaG3E74sNxvC^1~GC;!b3 z6Z|`b26+-7Y4EGl>LVby%z*KbOfwR5;kKw~CF4pOCjo?KxHuuhF-kO~kH4INxy%A< z(O^NwN3tsGhVQb|%~DAZEmG{z^EvHiF#NeYE_@`8TibKc3|;AP;bc4u6t*dC=W%cPnv<+ z7d)f8%VS3-CpBN^R^~^}*n4=>j9jgzYl_?JMEMz9B&wqG%H=wVtlJ-m0M*{_lUNhz00Oe?lq0sb*mk4m6fDn=|Rl?GHQ-tOWBZniJOaEOLEhG zY3}_6mwgza*;tQHs@a&KK)G}{R+jrVWC#@*JCHld>6`qNgm5;A{rD`VU<&(Blv?k9 ziRcFV^3kr91N1BR>=(Qg_BOzV(l4jt{4yj5)d?Qtjf1+0d5og}dQH z)0lsm$o%FwAp%tV9g58&35eQW-CkFb8UWYEJ`4bW8Yw`W_IMUp9$p!e&O>$r4|+mZ z$lixV?i2O#63uHJ)&RWAEpUkcQ~cD9zTEI6`SDY>mmC&A@JoLux>mzLAMw zDf=T8`^7!nYhQE;`rwC-A{)zt3XVNr);PW#AXr}M0gOLDiSwy+-C48)E$(hbl-Ks` zIdkt{-0Yu{=`kF(z zz#Dt!ZAf%tBuUPoI6k1Gxk&F<8mdqp|{ zg;M^@*K-9BZ%Rb(SMnAKg;zu;EVX*|6cOXAr&T5IvMdyX_gxdtAsfig8-)uhIKc=V zKWc*Jb=A+gr0uz9Wfq%{I_-=N8${r<(M>?Z1XXfa=HMGST;{%wiLw=;Jjc5OC_O!) zWEL7bm?l-fsCf1=7=X06rb%uTX|3My!E2LjOQofzMUrv3w zWt7VJHYgZ7mrkS{O?j$@RHG0}A}dg$4uWIbZg zG5+5A+NOqvmh9ns&1q4b55@%ywN##P{*1{PoY`oMFFtg^1^dJO@i+0NT+^h3Gt0QM zOLJt}0qY&t81+2AIDAq~;bAta9Pi8??kzVkfABg70UeQff>W!i*Z?W@Vz-htDV@%g zvz{t0(C?Q*FA0aX;VEKYbGSAMJ<6^99K4@LhSyO=5p*uj$+Z`Qy3T@&At`3ERmXuTb|+#^O2 zSK44=S+Jxjqs^eK=gunF z?8lbaor0Be=_Jtc+1@#;&e7>2xAKu+B+_b~pRzziH@lJ*esM5XZ?lFYPkNlov zx~QG=a$6j~+lOry_rAAyH&EYj4BO>&a3tsYE5!)Fn2u)E(4{l=wp_qDutgXIMGZsU zr;dR0t}gbg>B?fHpL~yo&cHIK3c%V;wB(E9hDuUc4lmb|M&8#d{&U(SdwgVS^Ox}K z@yXEDYyX0fy}Orh;bdWH_w*>0r>@ZQ$=MIFmfnvrQ<;CxEnOs%thN^H)l{J}Pe?CH zzeoein(~elkksB&>|6W%{JqAus1L!RcL0I4N%~0rl z=yUKeqeK5*_jKoa@w>5KNtpwCc!IT2L+3d^ zB_#@Pli&itBhvNhfp3p1@rBMU2Jjtymw{{#R~>)gGGf!XoyVd|@PR*r-6Dj2%PZmm zfpMQnhcX@voA(hPO*k`V+ZjDGv=HUzj(QxD0kn}KbA}%Kimf|UtTPh|n{v*Rj>PP3 z7baWu8J21RHCapf;Rw#DD1ECU-m2v*hb%njsqYg$=ls52QRS~Ki|)2y% zK)p4r5`9OZ?a#h8AR%Uaa^ES?0S3zDKuSk%my6`i+lO?NeUI=Ve1+b7KdQ_f8#?B6oTUs=tO9JW4wRkCp|> zR-O!HAr3(;Wecl)*_3lR^qD%lamM|iDcrO^BzQo6c4fK&5L%Z+tq$XKQfe89j5d{_r_Ml=|HBj`x1dy0EXBjA@RPwM|a|49*(w zZo_k*S%NaBmm$s>lZmcbo4CCdeLv-+0xivs(oDTMyF3Fu^7+hD*}NY+sE~%^ z=O24zzI8_75*(y>LIy<+I5RsY)M{It*X>OCZHwW$^@gSEHsrcFZ88f#*tM_tP1$Di zey?A0UpEc^7;wet@=zggW|`^}z?ZL!W!+eHz;{@@()3%sb;dLj3dRX7u_v-l)JZ_G z${RfUzDZ#&zBw>iX$h0+{_!}iomA>Rmf(bR~^-D*YflFgYWpycdTx0kN|tM3##Tl zNNoThC-q|M7|V9zeelc#%C;)RM!5$xEFh$n!o>l-14C&yo31>294-KxTT#MiH6kx6 zhNgh=8$2(0H9)BX_{P^ExWeGh?Da|T7k~D~5YvZ?q$lGRe5H?Q?Zv=YV3=*JZkpsi z)ysIiBr=jMzCeqp=wkoqaN117AD2!2nxEBk;f$4o=00bnwF6c zudy7RMZpfL=rLs z3m50FfXe0IQ_4X1-SPz&WH()_vCJ%=GXjXc`3}W(-Jzm@g$14jDbET4c-%w%YOrIe zsb0HjZs)pyeq;k-nIl7zd%+vZu94LA)I?O1z~C-lU`>U?91jsR+9k%qP1X0K6#C*X zpA{NnT7vgr;vL+6i0RTi(SXC~Sv%LYXD7RVeC2teE&^S_USbZIj@F=e3bX)&zQ7`( zK2%HY7v-C0@|>xYFHtug1~{?{!w7$~?c(PJzXGzm?Hu}YQ2*)L0ta= zHxwNA^6)^%{6e;toMXca9!Vn?tZ<`+<~Thb|N#f@L@+31*Z|Lxo>OIm__b*Fl&z8~FhVsw_TQ zFbEGR)I+EwOd5@iwO5rK#M{N}jGTy(s+I0x>14*{9MCVeUXu8=7gvO&8RWOxFqdmF z$mq0kkzvInx{lwXdsObumiH@~v}&DOnD~dye%`J{YR4AyA_At89FcX>eA5Yza%`>) zk@=rLst!~B(vBy{); z$;o7B|Jt-&ZqmlLY*UXHy}s0~zfO`|KGG+eQNgw9(LcUkm?Fi_KMFqOMF@?*^4xQ7 z<(nUn5|x5il8|%BYNSX=NyOdso(ppfPZsxy#!JK^l%xR!3S`z51hbN412I@NUEGXP zi;8@4gH@xZf40uLMi&cJpN)?0+%u zzuJxn>_xLq@p)&=#@Xp~c!f;!nEFyozKNh>EYbt?JnnxKtbh#V(BP1pd2?$V?o5Sy z{jXv%3)Nw+M5VDX`?`I51&T_Y&ui80*kEseAljYtmqwct7!f*wbbZ`%G4?lQXh#=P z>TMDnVSI_UHq!OiEeRF}1nI6INLZ0~w zbV&Ulr7toYIKyR|dryzt$b0SJyWpGlEYi5I10TR2J+tqF#G;XnbwC%`! zr}mVDfSawJ#RY{QSgiSgG3O-GWxs->9*d`*nPqQHp%hl*KF$;f^CJ1uTbZw}=Wzt@ z$EycS_}5GKR&>o?vgj&5E1vY%X*6rHc8@ddk9C*k*tsy>8MaJd_<&;ACgxP#=h*)A z0r@6;gtDY0gs)kPdoG+@@4SwbFLP5&=ztZB8sd&8tpS}gG;Y0h4dGvwE0~qL|nh`O1SS9 zz6>-N6c$8XQC}AWRNy)at%Z?AaK|km10Gz>5mq(C#ydFmvIGRp_tL@-842;_Q5p|} z=k24}Ge4heXid}2O0+-b7}HOyWGTmXhuR!`VTB3A)X#SiF8k1NXE^+7ukv}>`Avsm z<{nc%m8k{wY@PUvW9HH6Wj?B3G>w)Ga@gp*dGXQ<#R7OlP=xXgx_dll-ei zZUA$+TW6!p6Ze4_4eE!7`Zqv~*7&@>mxPBpX9<%sfpIs1&fQ_X5;wGtWFby#5m7Wk zaIOAj4;HS!pV~-kn()S9_TKou3Da9?lI|Q8MbI6LaUAC9+zbuzlR1z_r?AWGt4Qu* z|4MO=5J(pOy-pG#*fqVasE?wgIKUHk5wvin`Vf^BvY_}*lA7Hv_7YO*j^;H)f3UkvWT8Szut%dfcc%HA}lY1DG~pG>ohv_mZIF6$|GKc+ja zjL7Gp%fmgxLfRd+cOK#5e6-Q!hr-%T`0Sx&vrn=2`9RFgXAUSY6voE(`q1Ipif^a) zDi@DT;np_vW!{r0EpL#-I37ugp5<UluvYJdJ2{vA}*CtU?5Mb$^mSy@jsFpc>F zrw};oW$+G86dx1p$$D{mzBQzQa!))1;}W=3?skL)rE5c(+yoLtizmDxP_oPd^BcUS zhck4UyAJWAJQnh?&p7XxBhmXJ!4u&BK*O-A_KIrcJ6*unHtNmxma(xijk>Q2GvY037rH_=pDrK|r) zcDSjX_8cmw{#wl2+{&w*+n0O->dQQ=Spn+)p1n3F1Bsu`EIj9p$sRoXzi-C5{jq=i zvEJ~vb%r;dtc!IszSDDpt>xlVoVR;pqTBo2pvF~0<&|F^OiGYlIgklVvkV2gGYn^sVa)1LaAAX!~Qe9YO6uUfID1 zJSt6>m#k_qTiQB&1YhMlfp}m>hr0X{(4sdNF#2GV&Ez45YTJGZyAKId9DpcK&?M)_ zke!J8LzKQTlqj+0Ik39zW27Ni6ZJR{O?vNt zb5CiQ1Mx~l;$=;h%(yw!H$}>9?$~=5YbY>--z7BjWoNFocFC+mX*c1T=xlxVQQlK8 z73I3s)*fN478ZxLGjJ(>*kCf;<6oWehIr-R6+20w17d^~*gDdC@}`E}ybPb$&I5G= ztA1!|Pha1G38b>ZlC%Y)iU+ze5mfNj^`X~cv$_C$KZD&yj5;pKNsUivhXN!jJ!IVp zxl;uc6;#(;*ET{ra^H2=^AKFqOxRfc^3kq|p*AmBbRtL-~&Y21p6PPlqs~ zg}8Htma?b(Y~=ca-Zry=pQJXa#|OUJRHv8fr~Y8EfIIJZ;+#3`_dp`YH+zpC-k0vU zcZYTc@2`*DCbKr$DFTz>u3na#bMvJn9oxk-=0bS;Qytrj#4AvXdwH3uOYy~ekz=1) zYUdf-Hkd33KeHWlQ!fSZYm{)tly!jEn#WBqoama-Q1BU3lsa|llWJs-LQj0JKp2f5 zd3k)4i#tf(vmm|swYBGFmQfGmH>*85PYNw(!Wx@9vX&fti_L$H{WIC&dcNa?Dz%57 z{gJ-QPlrf5TtuJ$&5T{8<6k6tyU-Dojun9^OSMoka z6*e%^K11;o^{B70L#kt0(tLfLsaGA!w1i`G&X&DFz!)V85H=oIqN@^ zNbE}<4_TKN(TyD$Jj))e*#4LHX}BW93?KS__H0RlEwzG zZ+rRLebUdt`@Q&f)jn80S$Cf7Fy4k_%||6x;ciB8-e*-rr!pN+w+~%TvDqp95@lz4 zlvd#rPcZ4)UmY~bo9vIfSTE743}BiF=ClvFRJu;0ndUMKbjzRPoR zXJBz#XXt`|zSd}}1FaY>l}Zv4p7M#n2L-r4qz3@ z_w|)wCZMn8pRm%mGZ*gp>x>BHd+}rUnU;$mzj{rnNU6|anWY@8|MYrglMnDosUzgu#wST+mjgg{cG^x)b9xe7tDf3^=S>^CyD*aEe6d&LH7Cq^k7I5OpI zpCS1{j|B`RqL&MxU#dg?GrrC3lJhZ*EMYX#bL1tut*t5hM0*Va4$lB49QWs%Y5E%F zxR3oIFSV7wv;?#j9MDxgB9!>P%Eu7#d3ZkCXUbXE?f#jE2i*%&Pw-&$9-prf)~|mJ z(YG@cZSKF!@ln%>3)kvrI5|v@#4%B51Pk&8Nnl2stPe#91FpeOIw)le;wdCTCG)wnZX5Y+izdc{8{>cOU1Vut8aOx z#;B*Aov^nM4H6m#&_8i>R|e2(g;U zQCqmlQJU2)aKN@^Z-9N*vrg$=lMbG)Oz^#5C?*}5U(sI*&U!2T+Td5h#A>M2V=ImSP=~5NR&kwNp<$ma2 zI~pQPMrl_BEby_B}X^gcR`Iso?{05h|Hfm=ETM#RJDZhi$wKZ2KXkI)=C|H1YX;$ zsb6NOGkW!~jZ=&GuR1QbYjCZ7)V5GDv#-NKOvw4H0{l|z2PdeG-jyu+n*a=|dW}&P z2waj@4Ou}u+_00O^Ih&uM#qOM{rc4^Q`x4@_UXJ{mP5WTQyI-X=*JtYL3!VJ%QDNq zI2wZt;JEp5{H@e?h8{f1u(rfq0@3_Y$sr#W%Fo7|etbZUTBdFc-vlMgl7aKwbxD|F zn;uNy+NVNDYt8AYNvEtMY0JFv7;9bwv|gelD(Jf1<`F8INh{TMAauWE=*)lh$E^I0 z*n{(qYJGS0GiCDvrs;QU+tZn%*aFEZN$^gc^H!-3W6rfl4xd5JEgE zR8Z?@n`3re`l8C|yt4^=(Bc4G?OM;tYRqs-1sJd( zrb^9~eAJ}z|BtZypEut9y>2Q?+6Xzz_7D>U>~w_1>BUFL9uR|oF)Afug*pXGt$!IE z)D6ht;5>C$^5v#?|GHdIm|Eq(s!>`{;80qfXt>bWdg1!T=d3XiW->U2{Nq|foTa_! zSrZU^o>@=9ff)_?Dk{ABeW0~@IaQWP5>STV30GGRe`Z~!0+7fp(*QLgf574D(+Aykk_O0<6)CU z?hqI;Xf9xXkZVfNNv3h%1lmS)2svM5U}H`Gv$(ze)hhD$c&&+(ZMoB%nuDS;?h4MW zJ^r!=m;M3y$PRpSLR)*_s8UJ7ORza|q*x|AP+d*#nKlw{W!{n74nO#^vXkUB7MGL8 zXw+{P!Bt6CR4-38UBA&bWBjk9j?scOosZKKnDP2GMEKF-iUBh^>a^J)$E7XU&1Llc z7;m@-6u6W9thGj+x@s+N>6n3z9TT&&BFQ0sPknMxU*A^Xmt$1Wh0ZNoIpoXI8B(WG z?TuF+#+wYl(rJGgEEm$X$1^!5R|=M|$?)GeIS(hg=EDlX^6dmCMG1Q*7+270IXIWk8tcvBJY{>_pWhGmsQ9?TlvYGpLWI$B zTlVmY@umR(Wy2G{HWxZQvV*}WeW`tGR7bDB*Kw?>{m^FW#+yg!kzJmqu0=Q9keQCd zz~8qOpx%T`n=5K4bcw^I{6pS(!XjEr;@v4Ie8AMT@G#<(yFC&bFvp>S($4Qk_397hJdO{%QJj->!=Oy5pNf zOJnn*hxxJc3;Wk&yz|eiV)D+;{&jZ_6`N8V&bB4M<0pgFwp9F)dYof2Xley?7bdV* z_Ic-pPw&gjyPSK%d=~c7K!Gw>Y&i_bKLFMMQ`g{dFru`0fX?B*zXSUXIa`JOm}L!_ zMy%aZTE71Gx~fHqPRHNt?tItb@l8D#|8=b}no_^xTd&x07>Dt?=IAXk?wgu;GP4;{ z{(2M$9LhT9ceTDtm%Bb7`9MVi53lVDl@~7DYZ7x)_e$V&>rno^!Xe%z-kf7`bJaQc zhRx$WLoY%DY%9id$pbK#e6##Tq4`siZR9W*yjHRN>zTU{8YQG)P&PcsyFdP5tyI6D zsoS&HcHX%M)qroWuX|}I|1l8GlGIHuWWD9&QY>H>8vFDK^C$z37N&! zYE!>MJdB21#me87d?c4Sv)HQ|*qtkO1pOD#RiS56_v)!^H%A-6%Jv}W@(}vbxMQ$A z^>a{jyv9nt^5@D~F;qc0$;s&Re=@YVo$Yck(YxEXYd|d|-bgFiKQQwWEdp>9T=K{* z{`rnDnRj>Xty2I)N+k->or0Kpk;*6F+FRzO{%xr@FTzL7!U%t`D7GTx8QU<*y&dQU zC-*s@jk|UE|E^%0fL-(f=&^(iU!huyzv8SPpL-gY3Gm#Wp^np z{Ls^7Eg!Zz7^5G;0C|;tz*_yMlyFnS;DZh~Xgg?$9iuFriy7WHoy2r*mTfsOX3_t>CMc`@uq?{O zqXiTpUp6(J#aT}I6SL0WIG@?y!4KY_GLMQ|aLr{M5CMBb|Gc=|w<#>`Q)~D8y_0%9 z@h?NsiXD?O33{6t4N9-wd-c^cCel1LXiihIJ!Xv4YG$>`m@`$1IFo z7Tgp@1eRSrwD+d#ubf@pripr!+3J42xn8XBka{2@8c^{U?j((Ey!N865PQ|kCT~v5 z`I7sEr-@)Z^BbkzTlBE#j*k_&XPaqmacIdS~l)hNu5 z5LR3xy#oCu%F&;JsA#eLAr`k9!Vx&_T@@a_wfu`Y=Xj&>!?hA*RLOqP46Va}WC=<; zYfuW+CG>iw>BGe^dYh~GhG?*+E0N@lv&P3;M9cw$obF~{<)6NWM+Db#MZ_p(Bj-yhMl{*VS9O$OSOCaMaE<4A2;`; z;QHHfKE)3!=q)ijs%3X-Q1|qNhVwFv7XBmCEN^bhvD|%K{A_c#q~n6cGd6h_>w}N` zRHN(h{kF-ehx`%EcblIo|G$H`o$J9^_}HaOgb0Gu>sOK>%oNXZaB#3Y;|C3?*ucZ~ zlgDnAogO-M`)!mf$_xMENj=soJJf-c)23yzBT1d0HY9%MBjI+hd>?tGAfx_PT+rYZ zuT<`~HaGD*v}A8lsNNdTmX6o+;MoTh`WC<=9v7k78w(enJrlZFo~iJTFEu@aU5FE) z?`03u-AkVp-e_Q&<0$94L}l_fWRW*8>g$wui8psM*C(>98Zp z1XcyriwbLT!PU(AGakbrR`y*4}P=*Jq^%LYDVMTe6(Wjl?BuBFWP7j{4A_?NeYn-MYB z-^Ga=7Wink8I;byFaE?EP)jN1jIu3!@q7--%sZFZ++|hSHBPq+AJ7?nv*0W=MX~Fs zwOLI6=@GzTegoE%f?jhc^L*3)!N@_KP*YVuU_R)9e%!$Syo?wZt|}C(LURvuUGw9m zNtFWwoY!HE4cVLDBpP1!b9OoR>)no1YkA0M#Gn0F$Fj8UAWzb6~6^K3ODWm^m#FB=K+VVp1ozvjxYhSkz1T#wHM0rzv&=@`rr0`0Mg9AyQdxH$vVNMNyY~ z?Yi8dE1pZE#!EuUhI%_rN_;EJ0--`LsswWR>O-8K2r2StZXCVG+j3A?G5bEm)?5^0 z_fsv6y~W+Gy#uD2N?W5(s`&8i)(bZvV2*^RupTk+|| zoKJ2j8#boVZ0G0kdz;eWYCh`PvAtHs7e9vwX=Zc^ByfVa?r+q-x7z|jxNmJdQ zpjLtr<6lZ5#&S4EZU}J%$ZPr=)@}jhaVkNml;ODHf!uSnp8I=Y!4m&2hG@{)YMuTW zt9*n5J@#K?0>;k}b-XM3SkU;{9Iqz6;pm6G!8=ao9xuzDiMz4Ih4_m5-J7xi8;<|B zyb>lnEVVl<${maA)goEqi|a+`@|MI<6#DX(<`Lc*;7>E)p|X_NKIsO0ssl-SCM>N1 zVNE(q(=@hA4M@0X_0-KE4CP0_tpWmj-IQ(jKpSAKdpc6bUP|ZkhSH`%%bC(AL}oOc z`t!glA(eFKZs2rv)#~6g_NC}(X;I~(r%vCWNy`C#{Le*6?MG)s=|EZ*keQFUrNC&47w0->a;Dd-My zhF2X?4b$rrtXYP)5q99GfFEF`)JR))BJ2q|gs<-JixY@G@U!=;rT2Ju4)acO{C($p z>nkSiu7JjzhI{(V56upm9AoY>v}wTirkYfY*(iHWcRnwp-rRF<57&BMiAk58!Fk}I z+QRAo3^Ju3A{|(gM%HX4kLasO4uHdWxR0v}AW!ga6$W5Fa7=*h(Y02Yf&g#2rW^T# z^Sv3ozP3cY?>C^)1A{FB8>G`>DGNurvpDtaMq*ga)Bu=58kg)u{yfg7`9}J((RN~} z)z)Sw-Wg?62^PWG>AW!--lgPtMLpiA{WfpQurGV7=K((Igol99 z@4z4msFs~T>?vk#K^3=`;Qe+Fq2f(P=iai>adX=qBx zxskOU4PE86KUJod=m#9NYHFg-Y4;b4Z)L`<{U7e$G_1+2+xxW20TD4GG73aRMnM!o zrkIpcfPfI0ktre~lNb@u2qAF*!kkhWET)tqLl}(Aib#Y6C_xLEAt)h9p)yIL5biQV z_2xNme>kU~)7N$SdQYFH`y1-5m&v{N+H0@%U%!8GS#=4*lfPPwc>yk6x#i`yQ=IQB z{J%<-YiO$pf*O+WYVOMtHDWVIT8~!pC|bisPMFlWQISsXI|gHc5$Ug5N+NeK|GRpl z95|W5@MlFR=-wLFM7-NpD$5YrNc-uq$B-4D&8_TZrv`0?IGjKS;T=ASjIa^h;Q?a# z%Ux3(u+ZSHkc6XoZNN`ZQNoei^WwM(6CPq3d5tpJkU(BKJ~NQk0#^bT1|pC^o`y^W z{yhn_rH4H71eQtPMmPX@%#3HY2l6wWhLD<()0_Cjp0fl4eBe-Uk__iK`%b$Jziojz zI0Zct*%f({x|NT0z%YtY_X6gQb(AK$mtObTYzy=DJ;jUNZm;0UbaKl+=sdry=rPf& zuz0w-SUamMohmBK4c!(O>Y)jv_Rn59Q>tiEW*-McO0*x)N^dV;@(RoBoQ<0grq=DQ zIVVsx1cuxZPUbwjL`RRxlW~!wlJ}3E{qYUP1SzsHjwF4R&?@~UztUqz=QSq16Q<}k&`p-A^b&yzS)dydOF-e+{Eq;eNZ zi<3Hzs(*Uxq;`47%Ttb49(j)EpF(et``*>Sxt?VO47HQKzF)aD-fym$b~=?uUe0ZM zh*7(B{AP039^#L$X>)CXS%ufi&6`??vMVou?tmEOJDmi6*tA0G-q^!#svEzfBYC1N z`Q<=6??3Rqw%hFx6Eu z4k1u`!V8Wyy37IhA=(4ao;D&&vFn?+Fkaiph}y5YbQt9$EFD4xd@u1k@&@^Ymm?gB zaaJ=qS0zGg`5Jl}a11#$j=_X_S1?q2$UL`d79!G&z!ZUy;srvZ#XQQl#Ha&|?aW*E znAH$|^VXz`v)WoEH`m*8?m~n<0yIHNN zW@x2w`fflAVazO(nwOK;{Fd&5t7;-DZF)0=<^?);6@ICoNgiFv^j{#t-uH(|+SYz? z#>Qhe-JvKs`jo35?Hcak)OeO@*r^ZXms~QcIceFKFkGh<`2Z8G9vSz^)k`D9?W)yL zKQBEe_Zs^~#`y^Q$5kV4pM~ z(nZ(~u(ks%xtZ&?q+48wq7eKwfyM1O^d|gI0(MT*TYp=DOqz7?_?K+G9OMs>JmpnP z)3Lh{(Yv|%S!O`P#kQ!x13fJbgh$>he$uy`&(+Nbo$@{I59fl09vB_{+OBDRw+_wn z6vmgin`TE|emv{?I8@fL&xltpYpYO1bxYZ7)Nh`v?dl4m;5Q@Bs=ng)2H%GBtn58M zy;kkpl~wkosVn2+$92gbT&fDHezU#OvY>r+eH%_*k+piYi7$&t*kEN2-QpkR)zOz? zMc%$*KZwpB7LGtsG(4gKdJEJedxS&*YVoKj1k`z3Z%41nYkLi~2MRs`x%CeM)J#wT z|I4~16rW7`*uXEZ>rJm)L2VD8*1hkLy5=R2NjKlp`e%)Oh3U%x!>m~+OQU#K-@X!5 zw7c((GoXrlUdR6Sy-I(D42JYC^uk1driD|v^*8$tiS=Qoy;33oyHd$p(b|<^GTPR^q*Jf zl)P*&yOqD+EGY~4&cDm8k$6K9=R?x+_Y}#iyQ5AV{0{-z$ExqL8&G(`L0LiuDk6AA9I=R-DGmU3I z!%8WE_nhW@z&|bezUF2=PxJyX)j&fvtIPLXRi6HFHBN&fof1{Ilx=a zs1)i7*oQdCt!<}e0n#g5%jK*eCtTN&9S=Cn?m#I3!@>{H42EXpkYd3w))jdzY67S^^tmwrl=VnsS=|h zPzA119m8sdgkgn6FnU-_&@%TvR+8}kfzgYNOWLmwMK*kAUisQR+S!+fu!~e3U*bbP z&N3?5d59LT5Z5!f!Xpwf`OR0}Hz&1gCWRs1zHf@`zo|FwqQ0E3Zc_UF&+f^!J88Lr3Y0GMr_JiQxGD?7ovfY}&bX@9nL>@8zG6 zsp>vmrq*+&*pp)(VOLX8I`@QnJ36u{9H`tLijIk0p&C%J1VePn5W2JVy9Q_*=yfh{ z0iY6ts;I)_Y?4@o`{De85M=SmT0_H(@1wKr$F8>>r)3;y9j56v-#c~bfOk%G_8y0z zY>{%5`ZDNlv+nRnOLka(5%*J<35i8o>=x+~2K+AO2^cdZCg!G=)hJS1Y(9=WEW*)JVdv!CT# zdp|r&ZvD=TdEl4s1b86jr_DauKc?E7*1gUUHgSL#VBVFN6Sb$kU2HeHX4V+lopv#B zOho$a@SWCtqu=KiukC-Y*@&_S&2ldT%>DX@uI-=u6>V9BO1HDcx*$aS>BnD_++3kZ z_O^o9cJfa!`r&x)rZx&pb?D>dWne&~}QkOqi_x2>N&uqRrw&zUevp;MKE1b!UmP*%n zZA*_#>QcvZv~13eBs*164NT)g)o_oecU9F`oSw;|*gnLya^-~GW>vzzzc-NOh@u_1 zkiR#$2kNfZF>lkc0~>MV9xZtZW#A4T4t!~ShEdVrsOBRGN=nY`@12heD2pYp;xWNAe0BpZh>_I9G$6E$HJ__hv3uRyDECfgY>QZ%;g*&W+d%|kZ6zw~qGLVcgH z4pT7-Z__^I<~e%jXdjJ3)~UX5mfhabvcv1;<3K~){OauO+3xd4E8Sy?M~`mTbZmCd z(?C!;Hv%0K4G)#>z@>3_(_C}*x11BWQtD@^XzeK|0toXHGSR6mN7)Cvi?f2+-y&(z(=@Byk?oopEcqcw6pz$hR2xORkaMJQ}98G>IlHNe< zHre|5`#@H6qMSX+;Wp2MVmq{S+2nEd#bhGzLa`09WgqOUH2g z4!WW^bO%`l1QCTGHju3(6p${|zguDSl)d?y@k?%`!{+oZo}pVUPvf|g6(O|XqLR*A zi)~SwuBFQ8A79p4=N*`TSE)YD@{Ek(R*CSHY5c}I1ZsS40OsTwtad)cUn%e71NC_A zC2f6)n={b7Hw298_cR;X=~w55_>otPegFL~d%R#_uyxbUpTHraz4uP%Cap3h(=33v zjqykxEmwY0m{`$PA=_L%SSD!Xk`|`~HryvgAeE?t{5hyC?9Z{%cNCd+HU77miic0a z*9dSvCNn;hHYp?Bndpp$E%@Z~C#OFPJM{0dUE2%C9QQrF>u1$s7OjbcWrB7eO2`D7 z$4S8p4($~xld!vKj2;4;)SjW=lI{^-ffRX^Tgc;}IT||y%OM;QCqbl{Xg#n?d-fa$ zoJYR+4E|?6IrUO}-W2DhhgYF=+-VG3yD~@fJ3RXS>l<#4u2KZBv-%L>IqO~$&W+$* zX-+DP1={n82+uN)`4yhg&t>U=(=t-25qR2!U?7RoeR%&}3M%SXPbR0tZ>Zua6yeo- z<%@N)K(1&3?o)#{=ozN^FnxsVY#xH_lG?vJ#0~o$IOaCa80gd51@a88J(HZ;}NmYk32Es6b68*a1 z33h@tH51)awoQzKx~aG}lO_1=w&_;XD}X{gGl;h_ADyLjf3OG`2voMINiFiT z3`JXWo^b<0PUhRlW>D8h-w&57i0;Y{MSF-{>4NDRfns;5Wr1D( z8VG2qgpsCb_hnzcM(Gc5`Hvk?NyYm}>wus`In#j_U`~&=TyDROJlasKtT2+|=Zg+= z*upJWzuO;v?y4B6W%&TO&E927F6Rfky8$Du-G;$)J!YRzW~(Pn%r`Q_eyuP(oNal2 z*1b7?>uZmEWDDvrkj=uU^HA0OavmG1stpgfM>^G%n&POx*P{Nt3c%YD0_EDxzd#n? z?&lAo1ldwKydrd5FwUtY(J!D_a;|)K@+u_@wHu)$D06~l1&W+cIUVqMf+++trcQXn zK=yCY3vNF)#rjL-BPh%Ty9>JPTtiDMTrB}Nd|$TTkmTj+-y3#*!g<|FMO~} z&B+zXI-7b>W--#f*_o^7lx@8dYJdytrQF8A+cyd-h2;X>`&H8McZ5-TlUAz;AwDI^ zY4;LY&Y|Gik~|JHz#-nE$RM{v-+{BVRLO|Zc@(DwQ^6l;X&-9>=d*>}!`5VT;@yVzm3i$Fo5gQjOuUtfx>ld-7~*Ac@gqiAz+^X*qbfVB9LyU%@FkykFz#M1V;P zrXH^FrB8+v24i|J|NRgy3@y+~kBX^f z1-yJaa!ftHL_=s@-n{l}#fL!Fd!u^rs_^g}Iz^duCqafR3x;7NUEj@9ttT(9ndWb-B4E_E{t_aO1{4w9Wf zNapgm#8Ar=+6!abBrdbrD4_3p?h)`?G&wZXz z=jOQTHAdz^XB}Qlj+92<^&mCIFy3*U%qy+$RXS0c&KhZ>qFf(aJ@25UQIpT)o#KVs z=4T^3br!3Wz!uvy4B7D5x9IhB24;)c6%5mpd39JOMU7yzFs&{aNP8r}W{LOc^GSBX zNGA(M-__s#3hjSu?`7=nSspc<_j9E_%YE(1l^Kms$z|S;H4PtRj^%2EpeOs9N*q0e zFRrRmRL`uN0`7g{0XikNM-!75xfI;Aes0hDg>)U7)Hs6H-gu3 zw3o0;kWc3xCt!E_y+^*}(v_!Q*B(oM9NvJt-erx>JLE)}+EzOs{3*G!)&5GE+G}S8 z4Tn}{aJ5s3GxzDUO9(!{t0yml3u5(dFDs{FeeQ7!?+|%-^Y?oh3_p|LvL74lY&7D2D@R2 zv=qoevnbtkc-&kwcI)Q^ALfF@U#fl--2v4-#H(P_$#HQacKS}1yg{9LrYP}uypjcJ z7VWK?I@jKitEoO}=%D1~ORAD%fLp_b}uel3<=7%ORG_j>6C;hSpE+E z3RFnX<1wsd4yBWRYu!W%q?wS6kT1Bt6a`Fvu-z1rOeejb(iJ6d{izpeDIkK2C%Z|B z+@CVzeOF`e%%y*e;RCL2aH7(!AGe8@6c0&*Z^7X_@p!SQlNM zGUe|Ck>sEJb?Rcxu z?vl5SEJh{h7}JB%SFt0gSH`tMLz=n(W3UCBn7=m!4}c5JymO7mMt-Jr8<`&kzNUh# z%Y?o7qkwp^NgT@$N*7>-``>kJn}(Aqs&Vj4!Y2MSN#u?H0ip^L0{uA_$shvmo}9!H z{RHe=?G}0&?T+xwCuM`ceY+n$22v2->5k!MEsq}JHiEhMxTKt5-pO&sPN7dq%B6;c zDGHc>lMW%2VpvD`?(C%_=E1Jb+b;&AVXhw3Rg|zjY{y5X{B!SLKK{xMxzpq1 zKAT%}UNZo>EnCAIzR{KD9S^FdTkW%P19Amjs?|BMSGpZ0XQ4%z`u;2YSd8EIF>CXP zs*&W#ljijg(t4-PYisqMfi6$WNob+>g^_?UFjIk4lT>!>m45KUFv(ft@ZEIIBN{!J zB%xpr3>y;;M;5KTbR&kn^zSTe?S#qOVP{5%!$y#YBRX`)#Hbz3*pQ_1g?wwD&+snT z7To=AR4ZmFM$p?6q#+;_RN!Ep^i4oZU~)SaqBx8ine73~#SeE4n}bnqkL3>~IXI>s zcW@p@L;=NL+m8CgkeGO-w`;F#qO5+sFf?RdOg)+Ee6O3DgwAW%!cEBnEmDZmPS~P(bN~ z?V6Ly^V=~y5Mh;K=MjHzic8*#GiF0nkXEZ;bAB13ExyLFvcHmXbtzO40e_K2E?lov z5Mdp=;hAkf!=j19O1IsO6&!R0;~+l#wV7*`B3VGgsfYAD1gYiRSgs?&rmkP4jp@Dd zo7j$w85Aq+cNFAAn{do$E6T$0qPaM>!m+!Z8NO7+7Hy;4xsOdc={T#zyP%_iZ`4dF zU#%LG3_Qgu^!I0AJcL7Kt5Cbj*#0P6`D7;dNZe7Yo>Ct}%Nty*`n}>RPOOoZY10*H zkCI{1c{Bc3suBsHQS&T4b9~+wI&W4ZJQ8ngg<2!{M`uQ9y;BlBKvIh4(>P+dTKw@J z$AP;UVF$KOglOE55Ll(Lzb^o3ZRiaPb@1s0redxGKKlHiYdIl zAbV}G(|oLsv*6+hm%9!AIkb zalEu_s}Y9L&rukd5g5O)woIt+_MqOpS9j@;BmKzd-js_!wsmnMz?ISSVQOxufk)n{ zv20X?>KOH;XSpk|#rfPAnnR;vm9K5WHaIbQYuOgma7oC;g&$-?+kOEgn!f*vW zbU1)D!uihdNoS!)g8)?GZ)xd(>psIoo-jPUvjtk3SfmpcoPC9%SIFX=$ni{$VXIM$Qh2Z=OFOGF zjhWbN&<8~PPl9F7&2rCiJmhYcIhD&^4Sk@FYnffF4l#6AyVEx}($zchl{0_-FfL2! zlG&9NAmwmE4w*%80BOw&NDbNZZ$b=aj?`DGSeN!y8fLUVAMq!f1ce^7oC=Eacb}20P#DHKQniQ! z;^NEYAK+U-Q^W|b3DR2$ z!D!T#lE^2feibE-XDde!*V>T(Qj4PiCTAl=0?%h8*PJO+ zTsng|B~On)0Sb1oVBu9;Yw{)OR-COcYM6c*rSLf+sEGTKA5e>W_1mEEL%b2yUT~e8Y>|K^)5FP`^quqev3w+*e9!Tf%bxXD>RHo(m9s5ep8k%4CJ$o)fM#5zY@`MRMf@bpvdQmC_ z4Lo+N!wk2PXG~hG;h-n8JJiL#@66#UkRC>06G@c8JMbu~xK>ZdS(j9UKQvmzO^iqw znwvgc$gmp}2Ld425panN7aKt@`9u&j8^XGx4TzUsDtbc)MMjilAOL6~3;KF_*{zE- z3o_z|89J#{7IKLYnM~nN?Gz(bVj$8_f&;9iwO7z%zF4Y~Drm%vs^& z(X4v45i0lax7pk)>`5p98M;(fXy24n8;h;GsO0UVKElbUbea+e%+*HA zy|Z2I3iN>Y_gTWXJ^pI;TElXNO1tNSr#BvYC33n;9^F&p1tL$ z=Z1-u(7mMyKALH)*ae6eOU=;&xh@N3_6|h+lX;{K-@Zh&OWG?{er3;B?k!@4`DSR2*RqopPbQxp4P#U#RN&qia_7h0F`hRh_S^sG~R7u+ePG z=VzTN8_N71k-Hb?yr`G`Vq>!WI~2Fn{`r|VH`3thdiZ;4sG4(L;MZ?XxfcLz(UT`| z(*FxBtmaoTe}=k6zT#~|gl%G9kP?fc@jpq@+Wq+yu0;yi74&YltXUHLl{Nd8*g$Z% z!w?@ZbPhO49ACbJx8ip#FzctFinae!4!0A%Yhhgu_Y#$bMEFVv=`C!MR8xEriWLRp zZNMrxA|?#h5fq?f456Z=C4OKFVGu}T%#OL}F)&&LZRxup6-egB3}%P)tyE30r8yRr z!nw~pGQp1gegIsxm8rqybAEP@J9fC-sWIw5SbVT&n`-s_%0K4MfVOb|5!HxupR9o< zaV~hIexl7cBff?$m(ew=Iv+z-H|;tP#+5(izz!!z^wb_PGvF-vPmW}CeN+wb8M6vU zO^;Jo{mh@{<*8OrXk4_A@t@8Ob&-w@2F?V}p+Hi=1+83vaN9NK%)kHbNBR!>%p>9s zyb7#`tts|u0md-Za-vfUp#CXClByHlwDm5=5Je{{UoClX;UxT7fe91bMaR*L972cY zJMq~t9$gK0Eie+pGN>h}PDiNcrPo`GSrfC>do%ags`yNd^&j+jSi(Wp8+G`mwUT`M zCXBq5ilSC#`&z$fF9);7LL6BA^7`YRpWwsK2}V!>r?<=btwXO=4!SQuaYD zI<^0>*HbS;7uU9XeZ*GN_pbYbrL?zM)SPlsS7HKjl6r9@)Rq-I>sm{-r`^3N9h7#z zstt_9TF{)D_9>SwMqbSQ`o_+(SIw@c9KQHH?}({)Z(pgGU*&$PdwW%)%J&j*8|4j( z(PfY}?`~!%eJ4VbZ{iJ!>K0Iq`^8awayDX*=qFqOCu#hq6yC{2tKg&RAzCF1TJ~Kh z{q-Egq+j)q1X2DW79RQCZxSeHp0_7vMp5Z@hI+72Tcd|Ms^g4vSHGJB;deh;!C@n* zGQAR34Z@~<3F7BSk4g==XnTliugoQ*@B(|!3W0gJ_0U6%LIzd#{O-K7_7%Eyr~U{U zG#vVr^}Ni;M|6p&->)&7l^0>!u>p$xdxL?Cqye?~3N=$m{62``dmFx)SeFosuL)r3 z^e7kLuX0~>hBC>XDOg+(I059pqEvPo5eDxevog7uSH4YKfod(B3>lu_4@2iM8>PR) zcfm3rzladq3TN9~c1G}=8;>njCw0UR6F?O)g$zo9E`M(*vycF{?oiO__PfU6!t7YWf9Gy>b)% z>|*%yrOI817+@2Uam46WY_s!k=Z`!mtX?8^fg1R2pxhZOlD>jN4Fjx$KtgHrlZ@Di z2pM7}YV^e)08zq7@`(Q3qv^l#>1MJF6AKxl#Rt@Xh1U-(ZoO!YOg;c|H-K~wzqN~L zg8%t^TrROwzv1gqwbn!3+qB)))*5<#s0WD}T)M63?+x}?(FP*79~DlM<}Shc;16b~ z^p%T1n+OnFjY9Ay=Um@`5A(0{_!ea4KVBIYZ+$_MLp)~}T>D3`ELd&&l1G{s3Zx2> zxZQbV@vd}y?$^_G&%JG*D_!GcR`mYUceCL?c{vXMjZXs`0tht5HajBW<3F>(Ta(q$ za~NSZ*!|mQ(9PR_%p+aPk|LG0Q_cVBIjj8L5!Ltfgqz1uoDlG4J&2#*0^}VGoEb|% zy*^Y@CRGNG85Ytkwr~UBXyp?IF%kn7@L~TKf4UZj)FyVi6FGZFeqVo)wnqk6_8kcO z$JX1Lzb}!it@yYMd& zBH8-R(FxHPYau5Z}@{L(G-fcMU&z%12M%!W%|_e1`_x*do+ z#P;ChJSM|FpM+0*5^lPuP&RklK40mt@L2HefiU# z|0HaKRQjJqZQvyIPow;Q@$oSPNM`6WR22W0h-7Im>gnDG;om>!9Qn89wB1GFC4Vk& z8puU9J?cAEyN~rk_3+*F3llYaxBT-+{GSBk|M$<-|Jr%oOn!*}$6L!ZU}Dx!-~~w| zcmHV$-<{0bue*z~GxFkvZFIhqUY}8qo%e?VFYnN-mhKG`|IM4{f7|A)pZT}53J8$z zcxVf)B)7KrI=oB13(Qv>mUiy5*dk2`B|72z)%vEzykm}E*Q+seB#=Gg1d=W5hVBr zl`68Skbe*TX)ohNpdRCEcw^M~u1L2il~#qoh?Z+nkKf9#jI(eui)a=C;GuphfdN3% zp1(IpFY1dN(utoHAA;#nTjJjv-gIxkuVHhT;j!~a}L9V1aaHB$Y|60wz{K?dNXMIQLNq5PgCa)hrM>XLmciFEy+cy1_ zeqN8{7OQygn#h0Z_PN?&41R%!bW>j2^m`)-x*_ z`TzFK5CQ*R8A;>?0?0`AJ^1{6=e?u(_+NZ{JWbl4u(kvTHqef!Xd5~iD1Nqz%>@kv;OXot z7X|#P_ep-kUl-83T9Ha*-hf!iFwo}g3jrAvKrRwV2EW~Lw8){r0kdGL(`mRIkl$5E!ZshC&5pT)vlW8iabWn7vR5^03( zbl8|u9{EciLZf~%nGYfQil2wAlIwz#a`xkHd~?fS_IQq>!VIrYy5_^px@Ok_^QvCtWv)tW`uiAza8v*-UTLpK|IgkAPrj>q=JBB0~;u`|jH6KYe zVKY7u%H*14b?lcEfa0eOG4(sV$3!h0D3Hu~#5BSd!9XWAnW%*@xz3A*b5xS?D9*EX z=km(#%nz~^gDdh{(_j0$gckRG>%?pK9M_J=pSKFQqLgPjSZX12lR1)N?gn5MO1FS7 zo(C|~IOv(J>Rsb*7UJFyZGoRhc>~E?20H;R$pPz!bY#}1AAJ%>VP zvJF4Ym~^4G&K-ORF&GIx8LLsm9Z&{rXVmv3`t3!U?ii{uKHN|Aw#RvoQg&r(ohn-8 zPOp73FrxhaL>rcW(YMX~?+w=n3>)fl%JMlOuAcKJJg8~FHKE?J)*MWS>_`Em2{WI@ zN*!{)*I7?$|I%S7YuMx!*T62;eq?S&;QPia%*9#Ztu4&YT%YswS)cD8*2BWS4k%i* znZSbYUcpS20Wt4e(a-A=Mf_2k!y_PqZ$RHB{D51Vp^@7YA1(xGLGfTtVEolqR4^^2 zbiZKvra3*Wph5T7`08+}>ELQ6jr29b+J!lOt!=cf_cL)(PoT{pAo%0VK$NSbh8{s@ zg>)ko1;@ytJb4r0b4Zp0o4n66>3ZIbhD}BKA%BhK$DWeh?T}wkN2i!asU%7)+{W51 zaBT$zF_NNc7x|`lj1}Bya!yELFn=V_{A9M;Npl_jF0&+BV|HY0F7P55^=q+!splyx zS(s2c+Wk7;o?S|;x$~M6XrF(m&e5r9?zQ6x)kDs;9AWe5`7Ei+J=<`i7HNpDQCrQ3 zDa)vHEvz(ovG9L!Jt|K4hX~>a0h)^@A$Q`D0v{$>Nz&%Br>iYwx`Xm~N=}j(G!mA7 zP!rZQtvV!TMu&A5wmgrojA(Pox}?)F((LblD)vC%K@AtDCg9U_hS9GQq;S8|-ZVMX zyZj~Kf;-w5hq`+whg#Viy01OXuV{*>9;Ld+Ydd=7hP*v=+4q|b0MR-H)PMNeIH?g7@quHgi2eaNXP`~SG+XqtNj`oSx9f~KeA?|5*jA{{7>}AUR&gi0 zHWzg(x0z>gUoBWXE&o`jQ-pb|NiS`AO1L%F6rgkS_l4oQRmzjl=Ry0uZrn&}K5ZYE z!t|_r-E_R!=^k~FTc#R&wdY8dr}qw@?vH%8JZl`DVOr*D>SUT?sFtb=hLYX#&sPpuYKWk|mWo=midn}8rA1x6xv}&^;)Sr40rS6r(KZMws9Ttp z)>rftsgZ<+a1ovA7Sw>YTayZtl1&8opv2`h{#Fup>-e8=TQlep`z3oeja?sST_HLQXljPB5_n>YE zHTPx&>ZPoAe5k!@#4z=cU#SB~HCiN36ovUMe0q-9M|7Sr0_s5kVHyM()Zj0^18~SAe*JueggB>0%Q9eJd& zW3=*lk@p~WYK}gE=3QSpA#es{gYg*}3mBQs0ccStDhIe07|hT&;&s|a5GOb=l60q_ za*@X`;I<>xG4O?HwZT`9>}cZKAJ z7#-TuJa2zAOE8h4Mj8<))!5TaMx``&^QT`Ky|C2WYI};gwfW&yUd9KHGQS%&jPYzt zwWmjc-b;J4at%~Ns+RvqWm=A5sV)%K*L1sQm5BywJ*Xtf-v6jPagQy^J@W&mc-^Nv zi`{JTYWa2>CYyK>LaK!qhElS`Kge{W)1+1}>7(r~9ISj4MH#6M9fQNbd_rW08U-!D zq?^}Zl%me`S4Vzt&Zdx}R~YeHMYMHN3w9u_m|MjP`r|$o9&zA#xw`XoaRmE=S7eoF zo4Jm9?&pyj4Lc|ExuFvEobi-I8$hr)PWdXj?3k!xK2{ugweRtHo`J_Y*VR3%KoR82 zhCqEAPP|RuM$nNC!{t$z;5)_oMI3f=`|)7Dp71J22)Z!0gZA@jy~||Qljp{yJHS}N zbF@Pn2WKLU1iuSfvrNQrPKllqM64F0Mr(N^%LfA8zklVjzMbY{vnb6=OjK(@HbE6{ zE2&3CEKmAyNS#3Lg=z4cx0dN7PZo6&6$mn5=m1DD(mJ%~r;SL$TuHh3x=2<0^PqG) zj_}Q%m}2d6KL{FrRbP8urNDa)iSJL83EF}4(ootqM8)hBe5+KyzZO1T;zXF5_icNZ zfmDElo`oeD;@3_&?Hsr zxmVaIWMw1zlAj84_7{iQdp%64bWJ%$f1hC}|Koq0|K9+>4)E-C;WePhr=proNF6(I zWFZ{{0W}EdCbxe`5@2W8>2%I+$cly*1xVj{OOYdZ8>)05pTciz|A9vA@+|~2L^B7| z#d~Hv9s>aS5vUg{$*l7Qdx?B>1=wLhgV7=f>04ss@{&wgs8&&wpnrx2qDJN~bMGW& zuZPFECIEY)EJ;Pj)t}XSg(W4w~m3tqpO2P&-bPY?iuoro%BXv0g z`~}!s0_MZa*Ww*I9{2LKi!?oy+{-$7rr#|S-fAPXqHlH$l96(1sd=YH?!|l*;t=^w z)hA8Ys%f-~YWZ8r6R!Fu&OT-I6vucT89kU-xBK;B@sYnb>>noqG{R%hMBMxmcmiRP z!B7kUE|FOt-INx3@`^l~_wr*62b5`~qac2FV)lq-N7_~R_XdQ1?~vQ^#zRt5i>~8g zfaUra%zXPtQk22`a4Io{u&Z6rdh0h}li=dR;4cJOuGc4DX>BE)%y5x8En^%oov;2_ zC#d)-uCMfuBWpLP{V;g6_1(xb-(RZdRl8-CSepwlK8z8nVO~{mjQ@E}bv5PS?3jYF z2=3vPhx*<9`R0eA`Sh=IYIfZ4;E;^#B~EH!m0Sp~3XbIRi7$40U(|)C5qVu~DWeqJ z$wmqs*GUBelJL)U&RYa#qu4=G4H(=MZqQ5M*0d&8Toc+uVIr?6+-qO zc<-Lsvt?(0EWXb0T+QWjtWM0&utYfl$3xQ?@0j+8dfL{vhu#mmOHTg>?WBL6Zl~P_ zf(LNdB^(o<<-?fpTc{lf*!sZoE3yoZ_lmBCz;H3j(zkRuq=xhzs;RV$NB*I;YsrK| zCdCch?yv+yZ$I83Y<_D3MiFCqCc9aXkWa=8wR`bFX|TIghkuP+8#T^`A)FsZvFdVO%uF^P z>`ymasL}NBY%Cl*H9hW5zg5;&bedkEkoWOf#xHqJ&n|gufe6VmlRR$JA1V9t+&(VQ z=+L%0^`UI!r3;OhjIAG}pNrXjRnjP9pLgkAwrU@cAO_euO)tJIv9EFStTb?a-~6cD z-L#C9cd8e*8ep%h3WVYfJ&6*fU(>w*J?!GQt#}NVEf2|cb!ZFdS@D@BFvz3JT9?X2 z?}{qSdL*Z*_w!)wM}V+BIq$I+oG~ODSf6im<+qm2gzMlV+W-><793Ds=%6eKg&)*M z95G?ASr~>j7%fUgO?>w*s>SO7P!oKzat75+k%M48*9-Al7wZM;8wFdw@R;{p;;U?< zXpz>bl=5kpvdOt6{}}@#yGx%~P9tB%FQ)8GvmVX*RfOCu+m`!@Ka#x-b+$6CQW#_+ z=eE#yd2UKhy)~nF;Vk}YL1T(l;;E`0#4l^}!GBhJQdRVO9_tU*iC7P!Q8{om5CkY& z3DcN*8pGHWl^Ucd$wwOEUT{e%;uB^n*E%)w=Gdj$N#0t^8Xj`s*InQJg&1_uo27VLG(VHg3 z-p>FkI8oND1Uc{g>RpK+bavEDXqdA<`;=%XtNuY&yt?nU(nab>wzZ|&7}d>SYGGt< zBsVJ9^J#NKj@HXd$8Pw;ybUd_b3kWY|9u1uy^Apa0S55g(L(<>SPv1>hb=iL@z;b^5Q^`|6P8P;#L#7uGNlpQW0Hf} z4RS|)Lf?0XC1dWdUyKNQ#m0Qf%~f>Tc-z|IbwN%)2i5h~!hy`~?iSJ#D?Tf`9BGZJ zyWkM@XRC?GPdWlrprlnAOAe%~8wu7?$0)MIVE9DA%)rt$KJNNYkUZ}=O!$s25YhyR zO!eMWJn_jw>6T~D-T~taaPr(TC5EvfWl`r>Vz}5X+R?Xj5=A^ zb#av%eE8WObfIcBG%;&gJyCtH01?hTY>5XPFb60OTyy*)mlMmD90G%vKfm$xs zKC6Dsp(oDQ{~DxhUd8gF4l+YJYH~`{-OJOw1R2?prv2d#u(Z5Oo=KSuu8v=c+7)LN zZ(lspLe^E0G7@X#)4+7}D6<~9;>J7|?ALl66+#j|t<#8)@GevL)QC)sJOaj2MAxOV z3jz}^r5kpOuy+)B5H}5ki!RHg4l}$zKkPO>p1Uj$IlscFB3$@%#ftUGfMCfKe-wfZ=-r)9> zr(ztLdl;%7xw3j>r%)9WmO-^Iu{vKR@!9s7tK)s2@orY=cJMz=7C+&f@L8VvIEc-SXWYl$rPJwW`R*$rKQnf zhcMjhjJF|7xZ+Bk7Mrtt+br?_pzTe=l3e@u-&zf3R%B)lsjM<{mIh5Dg=ghbQ&X2X zh9%2f=9H18g^I8$6?5V#r!tjgN`y*eLY5h%I6UbxbDk;%GUtR!ypv)5ul9cRd-iYd zcl%9mJdTdKz3=P#4(Itf*_#z615Jm;p1LHHp6bw8xb%QTQ9>^Hsyk`Xp_fe=4%L6O zmpu@`?9lds-DnMDT$jD;En$WFn`9MY6CXbBD>T@u+uQ2;U@uR4zowc~iLUXj@!9JA zz#{a^t4^SGM~=MqgJOCa6ej52EiDpFK+T4!gq^E_>bN~^EtuVefDf1i0%Xytf0GN-F9v?pKF%I>K5 zgRtX%48L|pN&nv$wc{O@jP2|ON6=y zUfoS|nb6lPGB)@X^+m~<#&0EVls2@LIm;N(uzWw^@FY3IF+>_G(xH;nAiOMD>Xykx zfc&tQ)ko~wnU&SZMJ!5vmAB1?VmIUQt}ko>4GfW=#w+ts z+60CS=q+Ynz0!oL}aZqtm!p`6ZPf79DdHVAoj!7e5ol@?hDM0T!<1$BK&U%T79-N5A#GKR5h33ZQwkZ~h&bRq363)O0bz zy9^mG>y?#{X*)d)Ph+OZe)F$!G>l0kPkoF{b(e1d9GyxjTXqa2r&=h^{YuW{IY>4X zHPptu7N*eFlUvR(UMHohrIUWnl9$Q|qm2>-4^p`or`fNsrU1!JclfD1hyCW&%$T&w80agi|j*i^q9@S>;8r68H z2^1id^GAzi4+fgdp3=5rWw;F7X4I~6QYWpR(w-sV(ZCTd7z<@dwCud7 zyh6KLd#1~&`w_Jw`b%&W^894MKIk?f3sd7`^?EMcti3b>na|!bZZ7cd&8?c!*Jbw8 zy;5AT!8h&oFA4e+o%CE%wyZa6^U=!d@Dq4oiuXnL7wPC=kIA1DdK3iNPMNjmn0w5T zMm?yVDt#rFeEtl65~kQ$-%)NmNVEijQ0Oh>=$;#@pP(Oc4ZlcM1Y_(4xdbov5x_gYK7la*+OkX9C)2~FDT3@ zQ}o2Kn9j%$Ez0d2(2#sE*0UIhkmnd$+)XLO+Qr{NI=nx4i@r>>nrZn>cIR);oY_8> zS+u|u+3t1mrBV@L6Z;-0Yvk%|EZ8am3ms7;C^1Ge{I zqfh38lz)*7iQxk_wz_yb{7tl;qmBfFxEY9oOUn7FF->MV>; zlGPRafk!HVc5Q`U$5>n|1e=}-6{szC{`E#t=D7j2ErJ&eXQ<_1u8X@zLgjRJU6 zst{3iqsMFk1bEM!%38ARg?LGiaIejNjQ+k>3$kqGt#6RX@*g~+C*lu9H}dK(Pti2d z(d4n96VNiKp%y8vsbm*ukHk{;86fh*dZ2UjXblB?fT$xy3;3Lkcjf3A)nTYZCC&I^hlv~JEt}i<`SgZ z5#DhFd6tL%b7}RC+rpfy?eq=7^g#Rw_7>IN)~rK&x${z?{@w6tjOEVR7Tb8ej9s=D zZNp7Ii_L&LJHYYRmsQpN{&mZT8b^q~&Y6dOYjsvLu#S?SMCUdrcDjX1D2oX3CZ36E z$ae;(`3*n51Zv?YzkuUD4xNtLq1MZf1o(KIHc?9wE3{uUzG{Gjk%dtvZ(hWSmwtlY zOtgdi8I|?`QyfB8bE|w*WB2k4AQq@LuN9^AjJY)<$*n$PbE#xeaVvWqEru;{r3jV! zh-xk57pAk&HhJdIyhzD!Mr3~KcZM#<0ptE^5$gfb8cJm%vYKK=xqi(kE8rnd0VT^& zex%__#<=IF{|v@F+osbls$5~5=kqgQ1e7I^wASek+C)9CrR}DjeVB^P&(3wU zMmn_JZ2SGPy2vQiJmT4#y?^mXpD5~Snm&4#V}N|#BTg46LqnAg(+;QEh&D`{e?cWC zxU=v;Jm~SR(cQkikPY-TrT$nThC+-uIi64{Gl$$I6yT>M#)>DjAsyk;Dk1 zGUff@0BK}4KZ;KYwd=D5)4)a&ExKA+>E5TX4`;>L7Z3HoZd#wc#*z2Dnb21Cq8obJ zd@|5m5F9@IHHeEm1%b!CzO5hF=n=W0yWjG3iH^f@q@MvW^4$TdI>-|%&LAC##ttr} zku*{nugF@(t@>B8AI_i-Y-LA3m62?Fr}{7GuXMMa8#!O8cIWKj&35s$-%9JsKZ?Qh z@!=l|FJR2~#t4QekC|2!gg`{^^3!t(KR8&C`B&YkI4d=}r`mGkofG{JA3FNa3dKgJ zJm0hK{&6(%ynl5{?`pe_JV+A<^J*{CpIG3;__CHypE>Wd*dUg-3)C5ql%Gv^K)MiI~-$^w?CGTAI7SH5uK_*1tdw@50WI7cjG{d(hZ*1 z3_c9z5YzMC1$^XDw|moFms`8yF&z;jTh3R$wu;aXz@BlUJG^;+<&(YHqr)7KSL2h+-Rf-eglQ{c@Q#d-=MODYNXDsytNYhKfxvrI^lR~KV5k5 z7&@J_yKkATHFx9)GPF3c_Cu_7)YyXxX0%M@o%bP8MAIVmk=Fy340RO$YK6?@XQG_k zfcjawOyo=6~4=H##vCibSLlx0jz+3wlG7<-?fJ z1JdbiTh^toqPUM&8bfJGsa1meUn2%F?2MY6(z-*X@xZz#hQDWeY~xh>hrzey<700=>*gBZ1cL3Pp#|5tM8R`LbBI;p?hrWdv49l=<5AzR2E z9Ut6TYlRA5^ zxAwRG{+J_HcXZum^oENqG(LKE|CGhRyoc?~TZT6t7MPB_MfbLy!j#sy?hNp$>BacT zAm(d}-)j6yY{Qq_U`D|VS38rXz?^e*vw?>NCB>G|g-&SQk1T+E;M%zE4FZXPu)+18 zHN&UK&7PK3u>bG$tj!ItQ%nj!Cdak09^98TsD~w{M%q+F#3lDSaGy0jzP>u-IqzTE z-gmtC$l~6by*(QCclZTBBL_0;dcc(6Dd{JLyYg{j<5?Q}vp67+RLQg({9iWQmsW zO}-pKXz-GD+(_W37nkpR2{}pRf_{R%kd|W`OUr}BRT8HCr4ODJk=2AvH(FVSd3A|3 zkv^K~E#U#gKKA}o%jZ+>LD6qld8h7#^Xdcd=afe1k2u;PiG2*`(SbJKG9*+QxcQNq zJErWs(}~_DixvVo`9L8yzwG=Kn1*XEGG1<%`Vz2zu3};W4lGyt!8|oG^GsjW^6@@n z)n5fPi62-qypFyfE#q$knacKre3=E5Btocd1z%e;HV7o+%&!Jr<1S@CZM;1igQecI z^V!a)C|qiy?T2?wjK~@e&S)5N-?Nsxd%XVt6+@gWbo4se{PFaYQ;(3r22U}GW^%`E z$24<;kt3Zp2MXeMs_OJPv%)##t>7eIkOo&U=iRUkmz$mpIe2K+c!hseKh8c> zlz%O%cF&M?n<6TR4wB(q)gEYf2YLffQ$ditoXF*n(?T`DiJu{o+7!`tArkiAb9-x> zlG26H82R9Jw&Cjschh&v?=z?3Uj_P$LF$RoIxWo3K^Kp*^MPuxOqO>}j6Vxw8-`__ z0Fg$6)fVwtOl87~5 z1~`@3;3Ex={m-5wr0~s&plOC;XTEqio3w_w2MS}&fI`d}}4v94F=n@Kr6GFWi4>-bQe+QO( zUJ`vA9*A!88DO5RyR*e9=EKyCQ0lONf@@+A)#&*$ED|jInfikjm~=gtK$bV%%zMXN zOCLyY@}n>3cv;yTdSpbcuL+sgt#d54)$`99!)3$`;KN_5%ooP(_sK=+Jp;O(2C>==)gB-4 znaTx?J{gJ33r*J2NcXf@Bc1;9M0>W|gN{d0Pp>qR=v$j`e`(L1y$IzvUYgDpDE)MD zUQLZO4y1zJS^J}7YctyY?=y!A=KXTbhYKD0Sx1K$x(|oIf)qnKr+=w<*!;OXIP4(d zofES3+Z>W}@mVL7Y)8lBgn9U}TuUvw`_jz6dWXI_NVW3)?*XTG{-vxnmS2JKr$gEx zd#EfYhfR#F1I136jsQe4(zdF05WS!qkBF{N;nfrp$#5ogljI6rq&?8IRq`3W1&1Y` zkS`!)g<+sm2Ij3#d-s!NImfgkol*Q97a zkRP*ts4>0I?o)2X%z^h~?}J~9@Vh}!_sECnBaKzg?O??^zIj4`=1L%I5r5nmJcnBV zq;ptV#M4uplP=|uBVnW2&1ik1iBY{YQsmZ(+u96BY%ozVm3($78wm&P9$1d7v~ZVR{+Gk3e(=+pxw2%@noG;oE%iUty*I~%ORy$^O* zSP-h;)_`H2ZKT_Lf;6d)iNtIj0$_xSb|M0gZb1dX@*X8Wz+kPU5wG1N-iXF7c9 z?SDh`{Y!qZ9H!MeU_kB1^+`kH5hKIGqPuVbLS5Ab*LFN3b#77pT%z$}VkZzKutkRJ zWC>8V)M)J3yqB}!$SS#oyf&(Fg;MLte>^caHk`Til+#a0@#v@5OYKXEKjL&3P1P2K z5;q|ZRQ2O8WM$8Ko7^5kY}swVNO%z#$n?yZ{D1mOA{Mhg|)}$#tg;=$wrc1gh~A+eXZxT z&6%$LHGNDgh8iX((!1eBR+&duS;Rtghx%ag6VWDyenM1-#=^lJezH5sg|q$5%^n9p zy<$Gleb8Sy>YJYs4iN0=*{y>|fMlD@am!R`LN{dp0#zM57tVpbYK6w7H#&MDaofa` z;RO*o2A5Zi-o`UfY=>@$VqY3J1F*H3+QWjt0j`PU%=E}Nc}jY0*M_?c z_)*1WW&K`rxd{;gBvkRFwSg`jyc>iH;yu zL!gg;MP9G^kID>ir{h2d$DX;>V(RBwp601;VY_6*1L`H-?xL@l5*t0y<2Q#ZmPf-3 zq}s-gE*_^`+=VsUBAA`&8YZaNaR& zAmXE}d@1r=#ou zv+=sh&PCKi`LIK}9;d@IjOr3j{6zGk2KHJl`E{Unywz!4ecPL0=ygt~NLCp*u%YRy zr_R`e@y3^-FLPS*1)p^1cC<8URA8%|a_Bu4j`anp1q@l)u}jZWABB{TU>^enV<0_1 zWIo7HcdaV6s&@7DK3w2`HvjfYiEl1j(h>gOva6w~_ z?h==f9SAZqS)pGde_&f&L z>3>RoS`n{g@^nc9@W<>lM~zrXmw*f=ZSb*Uo{*o^zO}ev<%2QA6|>v|A#g5{LR{T^Keetuway%4IfL!8$>6YDvTS6 z6FUZ*&M;%2uDTXVx@}h)2RReQh_X{>FT%9^#@%$vJ*^9kLQk~k#+*7#u_kkWY_#%P z!=gHEYr+rLx~d@^<6Go8X zjTT{@-Sb#XWj+{=nJ8)4TIp!yyp#6G+5ZJNvFSo+)Z+B*4=~OO`n(u zVB_<`=?oNHu~EuSTLybMd!d_P=}$B#k0K@;c}hLqA!?;3;5VZ;k&L=i1oka0L zU7EOZqw=YA!$;nJ0LsPZ*edP2`-qXj$|u~V9jY<1mZMX(YBcnavP`i@k}WHRqU8r6 zgm43Op*dU{B)cg6Ous7h$tX9KI+F%yoE-|w#^OnS8%%$mtfNLj8ANwWj|t~e>jf%9 z9@SY|HO3vHJWMr74!+v@JoJ=DE_O55!fsYLyGf)KIOYlAncWD`?#K<4=uP5(O^F|Cw$nWmYK1QQ%TLR zv}RIT$JkGU$4D0^B)*0zEkN9zvll9spK_}py7lt$d=1rW;7#JFc0woMu1cE9gcu<4 zX=bNQaB|MseiOFo{3nfr5$Ju0aMp{@nx&P4X)-Xn+YC-)6O+if%u^Ii^H3GG+K{7 z4cRoYfr2U1vlW~7sowtm_*rsRDL+Y{P`eSf;f`z76o{tGD>v3ykk}pIL~Bdb1hnrS z_{CdbRkIe1Z@~0s=tl!v=cl=%+`0Du^~XUI-_``Rs%(LBVDCFfM?_tS2f1`=uMDpW5qZQ7 zH4ynlDDy6uLTwh)KrA4$uXJTQDW*f49v{V{af8XaxsT~m2O z(AUye7~RisEd?uU-A>MIZzYHuqkHF?J?qb9gkso!nnB|M>_D#9-b&qe`l*3*vku&) z920#|`h!GWht8@w|4XHh6`-5iJDf>gP^%%&ZfLs@Xkx&~`^MiDOHz~OWltisPeEE8 zNPc?LFAoUz?W@!bv9yRbxulJowZFnMl6tMA?H@ev*LI|Y#ImNpE4hq2#riU4SAXVT zPm*fB&&th79dPc|g|CtcpwDsq1(0z!4|4ulvjZZxCYN=zfjYNtA{%8$ddu4bt%#0( zV`8=v!&IXx(uyJTB}ieKz^bqkE@%)0k&3Z~2w} zMjDK(>FqD_|D!DCc2gzmyc5w)mU=!=|1_^(e&l*(2n>U%EsOe%<%96HGj=qIDYQtD z79wvIMD;Vw`U~wBK#Dv%lTE1d$lEs`4|d^m;S+8bj86;okgy^0|pofe0o*zyUmJS~?+WxIsDu_7CIH5^>Ae(oNoOqG2PX zBbgT9w(z!(5(-I1KtvV#(-6;$FdnQ9%XCbU?hfV+*pYY5>)2ma*_9uQ=>IkzVu+e} zkoHh$90E&cSOn|1`qF*IE_;*_T`lLIjXilNPA-T&8*?_sSMa4-_+h_qVa&j6Z~9So zvLt63Oh01Y7VT&7yRaE>jr5ku*%t65Z0CJf%k&Fl{Rvr#^zJT71tJnd8_}lV|5;;f z!vfe2uyC0r-<;p`Lk%2?kWS0UKosaf*oB~=wHFn!=nPhquaK-i>Iss7d)jE%NtXRY zze!f;R&vYOP`*@cl98)$VuN=HJIG=U;LZ(*e2_!thNmjMo)0{zMp}1XEH~one*;&J z$K~AfPBDb6!#T5^hrG3!ErFk=iV;=?mF6xb4uN!Je2u4;H%tHD%uNGnvw;i?yXa1> zoJzAdrBgbIGR}Y$HUh%JB(w$J3Pr?^m=-m#gwRDHh!>BgQ?Mf(mLC5qkd<2u z{PCjS(SE~9J3iaU@>%1cb=;?f0q%bu)W4MO6e75ppBnS*aoqV6M{8|DjuRgdwiv0Y zn;$PVYr(7Cs7#mLssvfQu69mTPOh#E)+6+djfZ28111;wY_|EeKrg%f?CsKNjPL3v zo2QneKw9sSXo!wmP*#;qcXfB|{&GKS>lUXF75azZG^~rNbZv4DDOf{saCqh5uy4oO zg%fJGN)*k?LUK5JeBC6R>X;~^X5zp{CYD}8XRA!5_F1UIgcWyX7Vl>WCM%K-c6S%O zjFU{C80Pu0(gh~X-zPLxS9R`@*wO~Cagbhu$hMgqXE z={2bOhfS$1a*~_`;1q zc?X&%0s1;^1e9MB<3VUierkxyy#%)xK?IAERc{)nDf5G_R-p$F#G(H z#jRX)*ph#t!*RP9|1!M*rdfZb&sbnVG=}cFbEad~;{OrCt_NlTYjI9LJP4qK5Yhqj zk~TT4Fu_UE$t~_IqSFvsXsi-|{{?jcC%L8o&b(GwJ+M*&#$<46BKLC;yeyx9h_J=u{19&}dsfI)a^30uo!%Qi1RYQ*JxU1K^-U zVP;r=!qQFB2G9;=42#i8Bxceb9PP)iJ zFzP(lq3JM?u}%|t+rM(5412gOu=QEzrNrvg*dqUn^V;VAX&1k}*C)hW%-i8`tj2aO zyyh*%X75w|LPy7Hk4t%QO;-m(`~6(Kuqi9Y(t`m2Cx^*(%WUgxiuYyeHBS5$-~R6o z17EWfB9}Z{-}GNyHfO2*z7y6-!;I(0y?7eF={QhrlqQ+?dmKDdX}Dwae^V8|W&O}O z{+k?wyN>2i+wW-!sn@|^XWI>l&MOc$A9)i6CkBI^)1M{ewjQn1AY(oyHX48lL)vJP zCyq!^^IDA0T%MObT~Yn~SUOGF<@sVaLy~iQ9=Pe6=7BG-v4c4Efg7wxBP`WbYk?^({$J$jsDj=Meb_Yq@Q z&MT4A{TEXL(ScrBL4=-PD~5?FeQxp^qZO)Ilhz8hnjRx}2JX+v-;A7h-L09{=KEV? z?mVXT@^MC+D_8&0>4Epy0Q7G&e^tU-(NZC}`oem)kmo$BhNjkNe}5tOBfTnNFKS44 zcaZi;hTD3!fe0L(MLkK%E6`VTEZ&$*#S7O$*(?PPLP);08y%F|Q@$Z6Mu$OBeCxhI zlG1iK8n+W=8P%Ab;)r9vv2d{LmLNu`|4ctqz8XJ;VY#&U z%Ho-3JtLiVhRAPou7{>-TuYeVEInWYrSF&AQ8>cuYqA)+{SSE-cFeP8qK5+^?ri~r z7Q1kDKPK`Jb93TDpz3x0Msn!W6=#{K8=RdZ3qM`^?l-slQe4B8zlEDH zY3BV@6J)fjB`5s8t6>L7*!2MkyUzcBgk4=$r^|+&Yo+K#9r-IfNQ9u z8ND7P5E+9b7k;CJHQa`~j$|yZ12rH{LuDKT22g$5KRfwz>r0VyLeAvN8KXcs5)(MH`h zz3J$FS-ogFK)UO>^5%olk+y1F%&`6NWtZ^C=!h-Fx!QEr%SN}&JB_{bwuaGRv6!5> z6#e3cy2=o(?2bTs7hL4==&&KSs=Awp^_L7pAE%il-?>|(+p|;ZFJ6cYwVa3Ve96To z9C(J+cACYOMhPz_fU7ND`4K#AcMtD_RPO(lmFGas#cgz4EB{4d3-IWOtX<{%coKBh zxJ!QUjTZQJ)Xxel>Cg8BAAas@@~u1_ZG?h?M7cUR$eDZq&T~9N#tRXeUa%~B)8j$wu;GMUqd6x(X9@2f!`%VQVQ+SbW?dXz zk7-z0UTP}91D*GwDg1}DaM!8;_!XnE+Jm?bLI16$nv|3KE+!hw9|i>EQ$J64fBx#~ zm*vOQdA%)opn%zya-#hRD}*+~&W5i=3!{jCD2sX4;2I86Qh-}%TujR(0U<;hS*{`X zQE8ze9}*1e7rM#tGPbOZuQ29EfS>XgRaZyZO@J^D)F$Dl#3Gz zf9O3kYHTNJYwuDm9<^uOdN|Wg!)aUlpiB@@4&PPt{B^HPo7ouu(Ps=e)kl)0OpC;$ z5yu}HS@=#B0CfZQpv-|pgc_L%b<>C&Ga-dtXl?UXCUWlD0#;g%dj= zxSzx{mjla$*I&6S9f{8PzPvtL1aE%t=L1zvj#D=hy>}to1`eaMs8ph*sce7lK9nYx zU`Jd0XH5YQK5fCmASoAzB5|0@^A9GsBcg{z8b{RdE!&jsehJ5ju-pfAy7@ zb1Pc%Clve22$?O~m5!I>wb-~m#1C8s{GPH zpzW8hlUeDfTe%=!&OzzOo#!~=$(LooX!v*a>$Ao^`m~^Crf|aM@O(pVLHuv=m0rKU zceG-Hg$M@2$SOO}YpT;JSf*Rt_1+fRj(XD+fjjJ9S1=>Gm~d$VxDR?-uZ(?oveNkI z8al1K%lSr2qX~s#w-tS+_fcFp--~qSa%(>H;L}8FV6n+J_G1CQYBr9+Ds1UFLYLhc z8~*yn2QqzALsz}|XN@{)7j#(cRrDjNB~|&rF-h*H(1W-v#ew41a?L(Pl9-xF)>U{W zRuJZe*8J8pD7+qYv`JkmXfNOWGGn-P)%si4r4kj9Q7IEnqIOuV@krCj3-ecI!f_BI~c2cW+VKqRT> z+{l9UdBYR^0PLS;(E-DHfRrcZ24C08N;B2Ms*Ws3S(*rf%00lplF-xfl&qyXNP0`w z;UeHobXrM*d^FoS~-SrbdxHNk{rmhm5=hv3q`W z4=rZ5Ssz0Wtg5%OHVu~VuZ=w$A5f>8^(4os3>|xu<)?|UWj^;WL>^>fd@DEqu=<;| z*jWZXqzAf$K`AOn8{iQZk(KU=F|Xc9@nf_}w8EC(4AW8o!)scNYC9Wpw_{TpI~4oK zBV@@fWwvTNI#aa+x+WtpkQcLI98isG;_H^={v@|?Uhh-9C=>i_iDb>-x>!b!7+X{c3Elh4hBEc-1onnD@Zbk^e&ySHSbJL zSvDKGP6cV$bhFN5y=`YN#yl`a)GVJI%1V>m$qOY{i=u}Z*fMei;>r{1z6y&=(H@86 zJPzgj@tiz{mN`N1rSm@0pym|BSa?hME})?{n+eqh2yyszuyCEF136$^gFty`+_RS$ zCmEldXqUrpj#*Q$DRzsf8nf2X58XxV^^iQY?q2KaV0-qX=(^YUu`{|PD^N_4Z~#mp zdkJDlvQ)eK_>t&L99*TL8p3I-)}1E8kqx;DKe-F>C&~1+Z!k*v9?4R>47#8Ty2ifw*)*$JT4iYk51;A}k6` zDNH|@m65%seqPmuIpJJjaBx2u!zJHNtJAI}EWBq2(e*9I z61P)vLgLD0#x&~undtwN4|xjF+2uQ>A6OE0P~QcfaZ~YX#`8&^Z4*$W|Iq!H+=d9L zb~}9|WwXUB3zUux=!IfwrR>qY$)~hQcrK6(5;Y`Wh5YO#MIsnHOmv_}yFgqxx`m%d z+Vi-f9=#PUM1$t?209v_F|40)O;~p2Sa-~@8?R}Ald^betFb???zY&uf0VFC|1smw zVh-+1%h@PVB&P3U!NdNR&I1gWfq2FyvDrYIi+8|t=1?$J^#RDUysIP=i|B>V2+b`n zHiS;2;n4uuExMk!|EK2<`yPrHqg`_&uzsy!b73(l@NgMgkOI4j#9dF=4a@#uB|+zr zj6=rX@Jyva0y3p>rG>qIZpAC=lcJ!*Y&HrWJysdBN zQ^{u_uJ}(^4S58ZO;&ND7<%N$jWsSJQl7Po^i4C>pbR~NN} zeCk*`MF`tY`~@-*`TW!^^hx8_PYB9(3rgHtd(z3k*Raw^Jq@jWFz@W&kLR4yjg35c zd+3DqRLOUY%r`IAn~RR~wCkGt-NY!`xybJDmU@FZd&3AgirsEabTQHT3nM7}yfpgQT z*Huo$xCSY_V}i;D7Gt=iqm{3vGE<}|@57pb*`4@^?4j^CS3vQS1`qre5F$6-(vzOAzz^H`Y@7h!&auSWzgIsrh+(_D$0jl_ zu&e6x-KjnjWqS43!Yvy&^Op|j!fM{wiYnDp!@|D2+lAzF+(Fs#`(%x?Pu>W+n^U@UlBI-Q6$e@v?O$ zHS;vN*&MY#n$Kap!jDP* zDAION=ELnNs@>5Dv0;k;pk183=i3ku>3aM6Xk_7y=r5cEq5(LMk_cVG;at+jSrtP0 z*EgCG=?(1(zLk6vz4M+YiEITnt^AHvbSrLibjj6rL~gjKoE|O>7Z~l9 zM7GfkW*hhE<3NCT>Y=Cil0=4{p-oSo-TUoh%kg0BH~67)IIP=D&R+~2I|$Uen6`mL z6Ktu8a|zec1vzNZPJcMm(lzGK%&7?c4%Ahtd59y$*6wEPh-K_RY?!nF#WupmGcy^P z%&wb^g*i+?6k{>Em*GXK2!vDk;ubNmK%4%sKnJqCsBg&G@FzNp@gnxv=Ox)+Pzoqp zyLXj@-VhpEw!qh`3>2x#Y~Fqa{@&zH9c0KD@#HYi2m+hC277_rOc5dKdj84F4|`9r zSlv-MHVTnGNE1euj0bF>7b^9C)>K3H#45fxlVlCW`Y6j)#-z9Kjo_Ngp7?1RLXOdL zo6g8j5Z6nluiGNl5Q+t~-TRk761|;h$Y`^_mH;zU zq|`%&CX{TH)(px%g|>aRF@ko@7Ps9i^+A+uwIkaucDLd2iQqik=rOTrE9AK;a3p~~I^ETI^l&3EkZ~U->cPQ5pZcF3%cF~o8NuL?v;yoOw zizh)4Y3@PYWDSkYg$D!723eUxF!~vh%PN%>O%ZYjsNPda=SQ+E-JxBHh+SZZua2@L z{Rh2WWv$Qx3*jfnls5CP;JQnWI>MfD=jP$3W3)8mx|jC^$iTW+q8iceg^kzjIGdO0f zogqnS0bb9k#3acv5$f(0m5q@;Z^*7EQ{Os=bo8bE5{2kx!Yi|**$)C}0Kw5F`gopG zSWP$sa}W&P4O|Smw&rxjgJrlw=D_NN%+;cNV}?yxR6Af2oOU^nG9K;BX++gy$}NIR zEoJ7IX5bNu$tyd#(Gqqi><@a^wix)BZOqwNaCl*=4r(9xCfymXe~B`~rzC(ci3fk% zgs8x>@+iWW@RM(CWA|BMdOUg;{_5w<`&(TKJmyLGEmB_iU*9L&Su^yJr6x`*^z?Gw z;X-%bm(tQIzyRETG3??u2^ezi$l@AUJNSXSzo^td95TL?)%x!He8>MiRXwV#AV<=u za7iH!gQLTAC_OG5R zL>E|=-Sf&J)<0|9n8MlqiW|)!3emttKQ4%=7E<&w+MkJz)=#VnEYxi;W%e?B#s)}l zasV$W=1UFse)+SUGbebbov@_?(KGM$iFH{I=1w8mcRjqTD)mqoLd@FDEGI!&o66f# zrG5--9{yP)-Tlv+GvJrD!HTu!N9qH#PPMu1pEYKn1Grv99{m$!z^%UFYt}5SuIU_( z{%zdIs+g~1Uc2s9pP6Y{9hPIqK1eG{JaL?HWyRTp zNs`ZBb-uii0oU|(f+7-1k~D}SU-^thiYH#t#t=f&4kxwtaM_7M%_8CHRT9v8IN7^pMPF`D z%8BWOh$JK3#D(uOqUeqix<_x`{xX0xcL})0!qDn-#D0iMOktGA>E7o9w=*G+k7j0xm&DcSJdk%;>(lq^sMLlVK`>qd;(tV-(Xf6W@cezeSalcYe z-1Dfv#d+_ss(wrzXaIP2fKP}YGxdQcq|*W;z>y%K2`_}ORuHr_iMZw|0nZ2K7$&zn zO>jG-EH#xZHOIw@7&jkrQ`LTv_S~?x>&ABZOVbBRmU}W2vmMyBsfRwj>GHl)U6;>F z#)UMuYag(ybUjd{qvxXsEK33SNHb;1=vQPsBXh~z;}X}QZ>l-$z#Sx*&(IO)rP>x} zEnGvFPdZk<_RcfXEu4&keb|B^;br#sqJDQuynNtRRi zOX-n@t4S#lsnuQt7FiZcX3kf0Pq>FMF&O^?!9<%~G){qOKyR~fHY^%Y%K9t46Y1VL z5!i&8UWP>;hT&RpI=aJuzB6F%tc5&=J2%9v@(wPH?7ijM*$t<%h2;L z^T_inGW?$@(yY$bzu2ZwuaKI}vy}Y$6uu6Ojp1sfePglu`)eu*>~{2)vryY)?28Z$ zh!!lF$R)v@#qg~$`V*9;Zx3FiJ*A4s+vbXfj}Ofm`n}E&b$+#|t196HHD)B_h(q`#vU{YIn3# z8+1c_r=$LKYzb3W?qSFL>Q}s0Zl&tE%CGOa*5nr^q8iz^`DD?wQ6_{2KvAULj27M@ z!X6L=B3$m)bnXIAM`El0rpi%kGS^$~m0c{R#Lv139vo+6LG5P8CSguc`Pf6$-gU6qWty>;{Mt_Xx0}==kT#1mh{Za>J zbJ|}ii|KL{AVmg~I{6&*LKnEQuPS~9riN%R0%7!;`ge;v_HkCwIX*ZGD)};K7wNmhL3KaC$4sWqBDu->QwD<;ArZYCj z^S|_G<&f8FQ$*zNyMCli|Lz1@<`XuST9<)>|NDffo&r|?A&e0O+yf0gP}>@g_EK>rJsmTs8$8I(frZZcr~*)W7=pS=21mv zf!W#jh&V563)dVmwshZ;q(jp?Jry;m+xEfw4sEAmtsFZq(>JOBDwaaXPcqz|jJ`>{ zEbBspWUrE9pvo~am#AeX9-%d_UcKq4#UPR;Jz3$P+*NFy7Xdf}PY6cJj#@-KA zOupW=;`UTB-39ceTKygTO^$~pKEc3H5QW=9FqT~ha&1^HZyQiq;IaWomIazhxOg+E zZ(>KnL>>TS1+f(SBx-GF%2z^2dl1gRwvHWzi7q#xy7x{FEqmFlxDM}m#-aJVp>fh8 z<=@+Dn4jOEx-b34N^6^xKAtK(+8$8j+Ytz$sFknNeJcSmliAMoP~|2?EYI~Z{1CFy zKLXqbDlyy($#=3G*tJvKGVG;F-HuJdU(TAhQ;!_-|1>+;+{eaTThUoqk%S2N^|WRn zQCR0_&WsuRK#}m98|~9rTxDtkXISAL&j?w|glM5&Fz@<0YRCXL=q0Zy#PV2(-Lrvx zAt51zUoW!jh6d@biPb$YN}d+X+`Ye2&%ZSK7ycChhV)~awH(PRbO#sNl(`20G3r02 zduUh7lY4Xgt1q=Xe)0TJY?V6l6`fLqTDXGMz4FhRFNfwPWa}g=s$!_T!P59&?AttT zQa5iubeK&V-=Q2I!3li_X0&g>Th{d5~z9ZCrhiSBshk9x#8;A#cJ#|OcGd=y)o4|Rs?cb*=YpOoHdLNeU6mQP>VeZHLw{)Bnw zy8j*@Onf0n*;Pb)0Dr?^On~q072N(f^WS`L7aE*{X=E09jF4-8kB9%YGeGZ7vX6oc z2)2SEkX4;aNNzA&!@fogkZ*wS9VKMNPJ}>(fYMoL3L4+-DJX6%1*e)72e+MQ?gIl3 zgsrL#&HJ-!=B|zvva{@5010g4^DPb7xRmsVT|fy*M35qhXgr!ScRTNU*ShQ8@xyz+0*jNJ zz4x=9@+)c|h6+3af^RKyjIMtG0yb`GJu_f*Y(MBaS)w?(YO8PM5W0=bXSgJvat;{bQ5xDd~(TYoKqG6+q!zK*7vk zprh=!P?r6anFO=u3i@a->=o|tOCR?M`k4;|KvsK9khsV73OMdg@e$ilgNND4Ja8E? zh;WP1aiSy*YxR>^8s5d?&fViRitP^ualV=@E(0;wkluZh8Oj*%_I9F2pZ|3oosyKI z<3m~d7=XkP5wN~8B=h!Cw2XGsmOz7s&!abL1`mQO85dPZeb7vgPDo3 zHS8g|UGz#R7TBidrM+ZmAMJo+7>)P9Di5As~l3f*ZVD8IxRNTvnV>^4*7 z{fzHA`F5Ppg`WfEM5CZ!sSUkPI0j?8#?zEF3V@>aEF+Nf#LDOZ>~U@5+`ac#qWdS@ zU#@y5&UJ!nIx$!{Li^-I16L#8M-Di90T(?LV5V@3xW4xHHp>IxGy^redOUCf`(~)q zW3gX2f@IqRPI&8bI^O`ua}Eng7RHz?M$Wn^x|bk0~vP<4$cK|PQDEYmHt|s$;~;@QFx;1UB!_8Ill8!p5FQ$Q(QN! z{&j0znvtf^95;6(6fbc#V%TM1&J^F}k6$dStAv2Y_d43x{(uuFp_tZ0DPmL|7(VJS zsjqjX;~eVgz5d`wS2DB|Fz_A17l0Wsnv54UMiLO>_^a?aAJCWB9&rG^Kg4^v|2x~f zD-5yts8btqA_Xsq--{;7(N}4b5%Bq3mi0t+V^ye6g|XI^_H*l~rp(%WH7ivF+Hye7 z*xl@bC~mI3C8vCBIR_C69HNI>apQ)RQ47zlekYn?Q0s-FD2CRBxGL9yY0MqTly^B- z&*HSDQA#211n=wb@`WxH&_$jst`~6-yY~^_5%`+g;DCUtoKO+U;++vn(v$FFw8baD z6Nr|i{0sQalgN|9Qf|UN4mu8SC|)k72o5GBMkQN)(+R(MFQhQVOq*+ja8E()z?A=G^kge_eU%%SY6MK07xBgr)<9rLl3J96{4BT z6k})cZ+&l~>vC4xj3%~#(ILRr&j^YbY+;)=)>1$d1335_n!u*4?fd>MR(T$9@~6W` zaj#xvssHD7b#=9cyR#1J-#@`$B@%oo7jDG1EDnq--1f5#g6$56w1c*Z+Z-0o9^B(` zm#+$a$MTO0{xsTD06l@9Xko-TG1j##_7aQ0l&xSBgks6yxuQ@O1+(Ag&(2n$))u`; zpReU2#M*p<^RrXn`FqFSZ$19%|3|gyA8Fx7vsv-=@9Lut+lKt>4#kjdC3+~+%)6{Zm={#2z^j+aYW$5*Q&8!Nrj?p;tIyH2>< zjZ@V_l*FRgH@O1nIfyg}kxyt!h?{ z+HNFM!}JiN;7b~E3R+h2(b0*X$*$Jx+%Q!!qVoC2@jCa2L+}Ow9tBC9 zLGE~!LpH-Xg0*Q?NIGF@Wm7y4IH?nYBK!&o2gWE+6Idn?SF9{iUH`QmMw5(>6MB*J zTOmQ;q<<$s+MB=<$=2J4E~z8(0QYMC?fKIMZVxK=zo;y;e)GpL;oqZ>=KnBi{qXNk zY>Eq?A)a`ovZj{U#Qx3s=srnIbgwBbj{4Q?B-k)muslU;;VU?8w%}RgO#9KQ&H`T0 zUq^RuuFx5Q-wo#d*E&>r)!4imG4)Gl&*|KEGvUQai%yI=FJuX2)u^e zrj=9n zZMyCrI{46QzqMtCEzKX@LD{i7?Rh73?vk1V+0wz{(;HKRcQr0K(F-Hl-Z?ftxrG*Ik4_zwD%@SiJK4h!+;aWC4P;ZTC*L~3`7*bG|E`(k{lQy5pSvQcf2e-` zZi|SSUqvyZp1G1o<``vGMU*_01BGaSh7EZf3L z8!P9=OG_X6^__ExR}9j~8jY6P=)wwp(?@m;BJTPmu;aeVKqtd>O+O@X(iPCRYa2p_a-JsQ>QWGwM_)-i z12ma-5q4ldk&51MVo0RHq#2XfEadr`9BaO8fl$=^PpmCOK$lvYeQOkMXY1GSuAiyI zbRO^wyXMDeP4QT;*URZipLEdU@kRUR=GM4qiG%$nuA7b^w|-oY?S(hIr87v zlm5mmhwS zGIM7%!J+wbq>?-)_I$ITQ85g-maopp(t7cV*qrEafjO3xy?*`ERpR^gfy3XH^ovZ z{e|ik(Sfq%ymRC4n%g$i6mwJ^3(&#Z5upwS5LwafaGvhr88CqP_eg5}?}2we3IdX< z-~bOZ1fm|_5AZK}))R;QL6-jg0Pi?eJot;saeqO5&Y|;JR5v$QY~`Qs|I;9DQi>a* zvmN#DEV(3FV3-KET~PL!HYd`pXow;7(SH6!fb%|>z8dnm;!$2_ex*1UE%!lQ0kBS z!dEYL6!>nSI}YrO2#9n)0Q6>(WI&KUX8ryA}}{j;Gf}LpI^Yx-7{i*Y%}a)3=dRfem)s_ zd9RUwp-Yjkg|b+2r+#Vh_`PX2Z94|V0U^C=x8AQccyS-%R?(gDFF}loks6zr{D_La zL9*@rIivS|Cme1xqNJD9GuM9fMSk?cel=JIpD|Q8!s=DLe1-|%_)7g0bL+I@KYQzb zz+RcnM7PuD@mIq*W$H15)iQs%2cQ(g03YXnR51SV(3{1wtk=PT*zr|Rz&G}1InKej zO@eZZpP`d((;IwaGSt8VakN1wYRqoy@y<*7umMMJ7>Vt@JpgN#Nw0a=td~(#Zm(2* zqj7U@+_-LlN@m{lpWqvX;f7}}oQVwOm~g+fG_|Ch*!b`(_;Ca9elEL*Q9pI~=ouq? zlatq_pjj_38+Z4=nBd)?n7X%2wxvAD&aF>(I5)|?kvmqgzUor(m*9gPWLLS*&#RWXk1?~k`7l1wDgj0C@b>vh*#Ut zh^Hv#xW+6y6fUnboF5@~o^Tbs4$!f3`Eu!Nqte-xi5|DgjvUXf;gFqU(`}L+Og*8dbjDebh5t$X91;6h9$L|2H@@qL4=Wn? zqKX%RjYQ8`UhC|pH!Om!vyAWh&uIVW4rOI!y<=hQKXGs9^WAGg$zAqq};0kHrz;2%c@fIlFv; zeJzv2+2*p4A_%)hn)5S5EXFfU|Bt12v(##`-XxF|5P=L(tM`h8u0Aq=jwP@>R?Dppkg* z9lq<7g)|Z>n>n99J@4GYULq0@*Vc9$4@F0(u4yhuyXE*t|CM{XMkOHvrF-`A;}RHJ zH!{jUs62Y4NnI=-bH}%to5PYVEbqxLb!mTOqF@!N^>mJ`eWceG5pw)aj+$*zupY~b z41{dDNng~Q_orQ6Qc#`6y6}F&Jy-sPUE9H`Q{#Z}tK)OBf;yodBU8FQVFAk0#%A1 zgu9wbmkO)W_ihwzw_uxor~BdA0dm1sPIvm9?ihVOKT>2HSus&%8(w8w?Ml+@3>q}_ zNF%A0lGG|gtz!Q@QVzVNE#?EdR(q36!}n&=Pl}^Sq@NN0v=7u}<|RN^;g9T8ZvTbL zbFnvL-pe<%9c?hUH+&OZFO!0zZHIw73hno8_rcu;YS;X2+wa>Tnyk0WFz90VKscl! zG5{0Bv<(n^B=tnYtbxl79Ak{+K`4@K=@8FV&SyO{WC)nvgNF-A-G#HCyL2lvNy3x5_nZyh!u; z$HCH;---+SAD8$&DtkYcW9eS4X`O3`1)ZbP_hadu+Ei8B(h~dV@pRMsY2I-)B60XY z8py$WCF1;4fc>NhFGToq;TR;w*cHhm%JQ$V34A#bp{N4WG0sn}nN5C#gdVYak^3Xi z^Oa!-&L-N|M-*HLt$H;*BIl*QC+N0ED7%VlAV{<@g;Z^z0`?hhuAo+Ez&DvGoje54 zdVFoL1Hdr7%huu$M%AmpW>P<*$X!pPuUJyhw5e;Xpcm<*GvpJWz<6OId;g63tZIOg zbBUjRb%2#q&aSZvXHH&=qojhJ#0k7^sFh)kg58@6`R6_P1G+k6fY=s1mG*wB!aXfO zK|{gq(d$r`wA}j@pvMu?6+QXd`+j;xs*B+FzqegZm-1r|#77d-!My%MCKG?auBvBo zR6GRF_Mk&rZxT9~@7ocSWA$~GjCAEq?zm6-9UrAb%B!3()Ps4SlH#z;^vQYJD><&^ zV~$_);>pbnMAMtR46NOwHJ}>491FO|i*EI9fqd@;Yqrvr}vMwHhUSya&u&~aRQIw4+CD&*-PHI)tNUW1-HD96KR!* zTYC9@TRaCxJTUT@$zwpYJZ8q<+lFE9tU((3ues#^Ph0HuchlT|{cl(fr)?3>my+UM zp|RiP@Xq00)0Gi{Z(RD1eVM^YPT)-NT{jVne-cIEe3#HN`_QloU+4x+E%U|l~ z==+>~v!inGamQP`L++Vpj%zq~9uTvSKJRes5!tFRW;Lhsxvdk$L2Ln#O49PfsqkF?d|+5Xf@l;O4nDq>CiZBWD>Ybe(${C9 z$LJ(0Q=v13LtlsU6#4pWKN?4ai3W7CK&&Yg$*?#0Oq0g&2Jmc80;ENZ!^$x_6bOe0_Jub7Nn@pn66=Adn~>OvV5fUASF+qvjC)+PPiBt`p1N_b(&X{Xj~ z^t0E(VH(@P4t_6~XXl5TMKs1!)|b?Yp;ZGM;j&N+grl93v5STI_S^=!L|v*8R0ZD{ zX%!3J!X#jHRURO}-tar$R5^T7H*#fRm*5^a1`^4|lI-PPiA0zZEexHc{4*ybGk^)K z2I*ZewkL2`rls&znMESb@z;zt&^^V6U*f|~rkdY8_p~|U{(%B7a)Rq`V{KZ)q1us$ zT%OSq-HeQ3VmD7A(E9 z|Ha3^=RSG&Ioj2LU+l|GOcpXzPd1!9Dz90`9EAV|WIb;Yz6X=oFFXOw^3V{xt5|Z3 z3YUcH`WC*kX*$3^aWp&0And_JTQ!2Ytp~V)>(*dRX~$ai(?k7tZ27+N9X`14jF^pX zO3JXGBLz2#VA~g=7RfUkI;B8@snMc>cjDDN{o(>2H$hu`3S(awqTkB}O23bV%99_i zb!z+I-sqM1%+IfJo}*4uzg3$gO+S%&Ait{NUS#T7`B~el=-?idbNk!zba~fh*JXFd zHxz}|Na+9z&!O4TJVs7qy5cy-P0=~4e=B||`{s549;wz1y4XUfWoAX6`1nG=edHAX;)dxDikT_AFCj*#9aU)sFWr_uhX4!^TOBZwZV5(Jwsa$4@H`%-P#au2>mrHpjY=TtkE-2A_nE} zOjf=aLpAjstg*#!eUyGj*G=}ETPDR7&{O0-$)mH*PH^lPm zPytF}-M7Wf5pA7SFgNjTvlj6iVOD&!U(vB=h?aqG8m=RJ-V|T!guiT;2nSmU zyI6ZeyK!=p5qnj87c|GQN8uY~@JN{W`?=;6K0GugW)D0pFq~;`aUfas{>L|$-_w4O z%~oE|r&u+_y4H(2Q*@%Kstz@_N3Z9OU#PLFQ8-S|tuf7B+C1%a;SJ@uxrSq2J|e{I zW%{`-{ldv*(M($3ZY#St*`7ZbpdfYmfr1R-e*PXo6@D=npt?L#X(3@~mnm8p5b_r) zv{Ioy-c8{=ve7zGWulh0_y9<-UG%dnI@(2V6xk4AhN)U|wzZAvj+=Xr^wZrErbCD1 z(ffxk(3)HRKwQEzlpp`SjhGHD9#K5N8h@N4K-SfWWtK01MG|{y{H8x8?jjbeT4k~k zY&$fhb+*Fdi68ux6t_R~Y^Mrm2S?U%md@1Cystn#W|Q^nHFtM+n2wgrk9Ov^A8k#K z1gx5%khTa$Be2NP0}yJ_aif4|g$?0o^ys{bpeHLULv~>0+QG^t@~vkqU?C=Q`Nc%E zKPyI~i{5OxG!VCWnKl-gv|k6>cpKbUP(EchKUn$aK&J5zUFh z!2=!K%KOQ7H~QUz`|V9LTi`dcY7na6uv?>HWP%utQ_+@JV_l*`_3x5 zHwTOgfkqz5YNB0f#}^AYZ3?Hdf^637Cs4eC-KU$KL!5^(nXd-IR4*FYrdi<@QZw{J zs-uob3#d^yb(~0}+PLY4zqjQ@2ucGSi?Q{~X(Zg`v4|4Wt#0R%-R>2HYW62pnPKf$ zNqkq$e$&Ys+}OnIkY zH4b@-wN&t58_P<2KVk^Z_Iu4i!$y*3$HB|BX(i4ws-q4erAnVuTfZQ6rfvmO-u?gN z!*71H8!un@!Ljp2_99-PxdzagMcO{>IjEj@N;r$(W1$2&3`fGGfzTO@FRLRIMJ?9t z1iY{4q%c`tud)^gzEKy7B6$^5rDA&Xr2305uM{*J#ukN4KK@kcQRi-?>71HPIicBa z;4|o)Lz1tpS-EH$spASf0G8Zqyui;>v&mY)^8GqqwvK0WtBKF>kMq_eIgVWmNX3=&?xhV|*kx~}|SD)EEI{O1RoDwauvG&a`@7J*~2@F=IbzivLUODMrE z%Phwv_9^RdK!v|16paQiaW4k_pK-l_&Z)>@{6!E?<-;OkRHg@ampyHHntR+oMfSdp zj9Y%bgT0kbzVob{Ly_&2)^~}%kg5A=DESs!=d4DQd`*Cs`F`&v56KIbPsgiFpN8zCXn6ps4C&t< zy{>X!y1_MKaa^kg3p)y<$b$t-_4{z+#HK`=UC!jYjCyW(1WRixY>@1o?b|86>Vmvx z^6d~}F`wR$o%F;SL<0fljoa7+P)G+c`TXUG-xR#O@K*~Bb5unZihfXhp0Pic-*C1+ zybyKV#zD#>Ew_Tw;8^MZgTF@o&nMH)n*ZY-W)5dy{QwV!HDo6yfzE~vz~zVV&DnCX zp9(tgeVrzp0lut+P!!ET?85k*o?snCuKB5Q^gDPVKC>(N=a1Z*@V#g+ZBYLD!=rM( zi69AoX*8{P_e-1_|B&Dj;wnCcCXRJMo6{4_@IdrfObNU#IA`=8);csaDxBi`dc{JU zJ+M(9iK?0(DEE5Pr8@umT$OQkX1&A#O;yk}wjBP958TkJYv3+S%z{fA-pDoXAR4y_ zavXekC%kN(V#x}fHkZsP4V{I;(OI4Y@*Nj9O>A6(V#pSuH?8=>M4r0vnV$w!!`%EG zICWLgLh$?e;Sj!bHdJU18JB?58Voueq`Brk^o}6!>^oQIZc&+=iML?Qd-4YoJ{2TI z7AciHZEr|W@NjDidK1UFwn11I;T?vjqS#_s&<6DwxB=Zl%3Cgyz_*J5msb%%_Blf* z#z9FIFKx}};RVOk^G);nhC4{eUAVz!kKNbClf7KascQGau|tOGHvV9MXCO%tozk`? z@CE6nn5&k6759R)$2{0}jO^6sRv6T7Tcc*PxMU^M(m@ZQgdy;>)B}K_r~@TpGL;wxQ;b4&0~uG+inVxA?2K zz*3Mq@QV+S+p$p$d^}MRco*R_=psT543mDojg9OfrXbnAbP#-c{@$jH>74PI);BXJ z`9jEUWnDpA_2<2~g)L-5TRk<}E-IJ#aoWx^)&78$u?rXM-+O2qPAhV@7%_j6n~I;a z5Hl!up(!pc=2-80_Ay?x2GzqZGHW41qo~2 z5Fyv>MovnLGXD_lj~Y%%i$V|Un#HW2Ms{UTfpC~cV&t@ncx?AX&veB|OH9#0*OEPY z-i6akl%yit@Se#WlDFF-#cR&l=nupnlw?jn2unL2d<*<%xDrj5)cQVe|4F=IN07^Z z@0j!yA{fYJ=?w5t+sz8d=!aj9y{u@vq+R=i%k77_DHGrz?FU|06n5`4_9q5C9Zy}} zFabz;n5=OVd$fC4j0xsf142-be~>4E)`0# z4!^E89j5y6%j?s%wS_C=4xhuNzPr!-Ge6$7sWr6TZ<5`rLfSp-8XkL}wXnvV`Uh$eVBoMB(P1shS-fc(|t!DSVBuf zRlRh!2k5PFV;Qu)nA>OSi7wMrfR73V`f4S6YttXizHe$ak@N7_Kd`f1_M^Q~vSNe8 zF1AM3CZtx^2T$~~fV$X2+%0Gpq&e{J2XOsPO}-ObgD>a76>EA zVEZ+v_ZKX#{5s}VrygUq`UXWsHlFarg%E>x)Z337>vD0c3gs&rX2pd7EF>{v=`gd> zE?3{C|BKdmB`H3OtadQc%jwbAi&gG%DCdY^{ODjkMRUZJHLTh1lts|ZTm>-MOQWw6 zS%kZd$Yc-_@WJpo0Nn#14di|Rcke#QkzjzE9W1{GXZthoE3N}2>QM;oRVOez-k{#5 ztkMFeBQ{Qtu>N|Q^}Mw{f5HAqpGw`IKL{WfWy+^}+^Z6MopO~s6poJ+vNT8bXWUOC z>DU^kj`iEF^vfJ_Hv9Ch@)X^(tuFFohsTwC+>b6$3}L;QhrmcJ0%%4ScY=L8)_600 zZaOoXw-4WS+|1s1xo>k0#U%*-Y zF5FG|ds~eRc!8JdXe-+h?x}W9$EnASUp0RHWb=vlS7cJ;<>oBg0svSzJE=0sYX3#S zh}#&P8<6+qiea~7o-qKHILdiLqs(7G*xxA)>9NmvWO3aD_rahl6@acmC4v{_YqG7# zQNUtx)1G5RW3&29%Np2ujKtshCQ&hD;pnX=d`V!q&fZ#|yPD^xC44p6Zd3YRwL6tj zWY?Ri_~6Ci`iEsdS=#CLTp~N!xP{kvNantKcuPbAUe#X{G~zsfGLWWEt!epR|C*zy zvjzlg%f^5yR?k=^lagLzCt>RbOd=zeQo5M+Q`sRvTJn&ch^4JkYt_?4;s*aQG%Zr4GI7d?~0bd093Ez~=ifLGy%rb-eT7 zTK|;_4jowT3ej{Yt2=)~DbIL13t!*L7DxYgq=|YkpNyDGTUy7lz zI{@0A_G$smKZIw`-?8Ru$}w$mM<$&>sS7<|cQPo5ybyPV?|{ zw296sWXV(m7wN>`i*gWz7a0hQZF*q?Q-^0)$?t;qb18nFP>sc0b z+b6aEF{WGZ7<#uoL(6DBn{yCw>b+b7;fX%hCigyHyW13s`jz;_rwYhO0*#}<4Fd*j zu9@2kUAzfu>PwI@!2Is`0%h`k#SP$0p@DK@JWh#I7R#zQJfkWg3Dta0ksZEI8|Td( z2D&d`L`PVTVea;ZVWGWpieVkO4Wv1fA|H`6Fyw(#T|~g zR*Exmp1n*!1a96FicKJbhzd~RqIxD9&ob#D%J73Z+_HS(qfW`Te_T419&+M}RWHiw z!A?DGNWCyg`wt=jmX0_G?u9gDBXNC(v+MyK<;e9B8#?N`(k~&>VB4?~OviY?9g`b4 z129@{?S<{Z+Ow;`ePb3=fq^j-?glVIm7F=q-Y>b|2#G>xlGq z%yA8mz1adkqLJ_FSk6HQ5s{?up8g|oz`7f;}2B% z!N?=v;;-6+m`2HuSAV@OoR}Z+>n{az@>+xY)4YvI`Zle#_xf~=LfpCfPKiz2BRNQa z1qU+VcfT$kBpQ2%xM|4q&LJOSK~1v5X}VLk4lK6gGsLb>h%`lbyxDeMuyAbOD}47` z=oE0fxSdQD)5G_rm(Q)5b}SFjwg|=ANI)eQ#l$#@@SZXzXfdCyLypgHTQ8Z zJT96zExk5f?Jv}}(|GPnvbs;7WFIp=gJ$+ZK}L4%~QN<8aQE`=I?}h;!bt zu)^qx(v?9ZyDDwk)mMvm?8{s3btj&I(_^P|uX?6vEO70@xCfsCWuMSq>`sismJ@!Zq zd#Ddk!0iJbb;$s*z8U0d!+g$nVyf=Ab!R!HQD8woC3mY;wkAe{m{wF|sW8zqSS+XD z=T>QPZaiI4zer2_zHgnNK{Afp__d?-I$AD?0@M-+a@3`I`8!7M$EHgu z+Qs9}P@pEyR65i(uF$;b{Q2vUjVsAT#uLRpbwzya8kk-IGP?zM&8_{2Qq-g!?>Erkirm?uZ34(PG2^(hkiUfJiRM|6lV zzFAlK%sqjwG56lSlDGn6+mFWi?Sd3|*5EF15gy>1H_iC7KjiS7uu_xxn|(m+9o-9) z3(a3n2370qd@t@WM1jn$Te%Rq%^I;GBKGARmJc=*Sp_Xo;Z!aw`GR* zBDmY9XrJyeOP7Ec^UpMG;7TSUeE9`I4ZPQBg8fNQ0@PXJ<$z4V`fc8S?CXFtdKQ~N zu+RzJr10%HAWy!7mw66E7WruQPf#@bZ6DkC>WB8=sLIu`x#vn#g*aG?V4clj9@WU2 z>AsR7y*Bo2;zcE(x)cw(y~kgA7eY1#3cZ;WCyfm?KiAa(k<&$2Lrapr(WcfSfvIP8 z=nhW*UMD7^mI2VyOEM8U#Xlg%op@V ziSD|29*dr2c8G~ir0>IlliVdoyF^RIZ#NUGT~$8VG0E3rIl?dm?jw#>{Ga?|1zrii z&^S|d{k+Y%m(*v}dsb6Y-8Bb~eT&O|Sw|I4zQlxKLqTSw*upvKjqhG5U?TbZZ6y-K z%6&2Z?dSEvIyErbj=$?MbnM-c7CR7)0W+e-94OiDH_47xg_jV6#20~g(IV%C} zCNA@ruFJ1}3L4r`8Etc^f<;lh0s48_rB!C{-CEk+zZH zIl$c>6_8l5VL0ng&bdsR{m3Qc7I`~&4c@5Gu5YB>b}q#y;7=QQr(#~uNy;8C0iWiFQc@hjV(Q+fd}`^L1JSXX;^MZb4%x@)y2cjsVn_O`L{X<%Z}a< z+@Gz=fbkCkTJ@p~EyKebd9UftJ7TpTW~d*(uTux24EP-qq-;hidGwhM^uiR@?f^sx zU1=di>Zsb%)aSmr##`L!FzI95KhALJ>jkjb5&$+kY03V578Wmwdj{~%@+^x3d1r~; zfu8ngZY5HrAgWO==*kq0;UuwrI-=YC>!MU&U)>s zhHH`)4;(=I?9Q9>X7y4v>wj-sdR^5Lol)|QY^j>o!ye$L`Y8kct#a1i+e&3WxBO=( z{upw5KT)nvVpfOQNr5M#JEb6kP_%~^8B7!Dl<-~+!Uqu(VK9Dp6fYzQCT(^T*d`!m zbLjx+ZZRy8e(f+669_;~tA65Rk@L}l(1Btzq<9R$y;^$7@kH0tEt^Zh0(U3MPUmVI zHDf4fyhiWR`$aZX;lFl8hP)A6ts7wF8Rb@EEg9b)aAzoS%go$pv1KV~ImaR-%>PL> zYu^7WSsz(_iEV7~0IRDiKN%madsRnsF~mQdI36Z*SRQxiyLm7km!$8M@>B^X5o(b#sx*;e@PmzYTLL<@Kp^Y+HIZ$CjkzcliQAab(XSde|o zn4Nw#R=;T-p8wLsedlkuwuIcHuXvt3N7P31yND2L$@Sci#L0j(ZeeRRqB-jeO1&wu zfot5^0vuHE!M}g|0_Wgr1QmCexQNg>LEtJN(SL!Pz&lXieyQIQ_62fl=)pWa+MEgM zHt{W`pbggd?pl>wyaO`WC2R5Btxuha;u7;QE@GckdH6ubIV29}HjK9jeQ+E0(eOP8 zWgH100dT2VTl{yx);)~B`bML-*hJ8RxI)+oG^S1N3KjXT4T3Dh)vzlQOo@JTWn>a; z58OgNwAeuK@DmIG1t)O(XV(<>j&yLugKGFDk!a>2W@;FsyGgcre~3_qlhautQ(N!} zKz!`n*nVK{u>&;JY5@v8+Jjy9dksDi8K18aXQUU}cS~sA{sg#4@O3Cqv|W(d?Lfwc zGVH+0mUnRYC}nM5&^I&1GUCn981YPU-}NThO`Sqg7AvnqEu+R3_f{%h8{^xPR1Tzo z3w;9)TAn}40Y;r=mAY1Zs-)fjV`9jrDQbG&b|rikPRZQ^k_4`)-PDcnt9ce+E zuNHU=+B%3+JKjT5iv(HzQ9vgq9KbZN;Hm3Esa2S~8LHNiiI9e@nav3RCUym57FHU3 zXK!&1Gwh5F6t5&Cmkb+X0#IX@6TKKcm7dy|j9*=~S>@fOOV(nE6zv?cBfN+6i-eT! z9@Oy(F0_6n>fvw8ikTIf%p=Cy0F&iGb~c|P9Ryek^_rCLTy2LU8F%tEfLn&vZ;Q|L z49Hl{H8*wtX1GVmJAAz8D(y&DcX|kJ#BX2?4r%6AE@%nvTS%|n`H;ngNnlXDNGf1X zuen1HONR7|(2*sOoV%b#xMKlow&7II+z{l#_WJlZ_;a=&{d&eRVe8%eFxIDnug7Wb z#Y^JGztOwv_G48VI-wIF`*_4g^ljcHN?4r4JO@ezCak=aM)+*#VsoPKFjhCUuTzZO z-*LMnbbvXU0`xLt9ow4O^d#y5unIr>RMjX){n?sNpX+(~%-_HhYJKYKa5E28>Dsep zm)oS0A!p8hSF2p4aAU;Dv)awf^5C9qZFr4q#Sq|t%G(W5QWB2pc25nAW3a?yUC|~( z4#vW&l>ViIUey1Vk0{$09|cm40I&4vr)WN18$D$XMkZ50WJ6a{D3l`i>nUB zhO*}d94Bel>Qb&l76XgIhH8k!D9--|@4?3|C_@b-j9uTi>DbcetROJ^pF3a!^`Oc_ zLra4OJZFsFcsR9=MZ}zDZk?wJPE}W)5wjEN`SjQ}RKi;0FGfhVyYi##8`wP|R%J>K zl&Mv#NB4{5oJ-3GKP{cG)jxaTX0EDfj-{HjSMBws(G#v;ibK=Txuql#XOI%a0`kA} z+xuxQkN((}?H zH$>?SLUMsSut#D}kXB*rXU>2vUAMx|1SzrwC78Zx{ zIC)$=It3{MCV+^BUrI>)tMwBoV8v9$fZv1d>O6`){}l3K)40F0mFUUWK76i{*calb zIlUg~DBqlXXq=B`&^D5cU_V3Zdg802y`KII?+g@O*7)rtg%?toIu)786OT(4L^2E+~Ke z`6y0`~y`#s{Qftis^T8M_%1Ae!LZr-Z(e?OaKBvU#BYP_C`7{Oky4e;BZfX zB!B{gz8&9Xfi3E(+nP7WZX zg}PXcmsE+L`1%}VDnEvI0t-{T9sh|C*GfxiKNH1n>+6I;F^szHr)s?3U`V}NA?a1K zm=t?I+35qP#>_?cb7ev&pV4JvUl@MlRICRN8Zq2EP1O3`d)>Z}6}DVRIb#(*`4;2k zt9sTBt{nM*#HHR7{W)ev^E++pixaHG#7m84~_9T_|z zD0U;m?;4L1Iu$s7Z{y3*xF?`^*5)=KL#o7iSHubAHbWxp`BOj+2+6wy`E+;gg{;f? zI+JF4lvSV*ZI54Uw~*sJ!SoEwT^voN;`gnz#?4*JV-(cwnO~E0=XSyhT%VHJ9<0p- zA)tG8fe0E%PZ8>}tvB2l$P|PuA`m`*lJPk)-a<<_fB>DL?l)2le2=3T2DH@uiNK<* zy8RUAz@lIJqlMu6XG$?!y`6_SMkXHK7b(dWTBjKEw0-s=SUEI8Lt?t<{HO}IG3vdY z$V02b0KFp*Ub{OwH)&spG|y1UQMeS|l8xG9rC9K-R=Y`6N?S+FH9zA0={g11>)edv zh498a6DoHy=w-ErZC!-1 zDS&g#ovfB{aNMgq%NsguM{c|TG`z3U(C)0y?;3a3b1Sr2SK9162 z-bl8Ut_pPSVysM6=(v#F6>>#n6elu1tHP=VKe04h=sB(w{y83KC{(@?juF9UCd}5k zj8luGo`E^W-`i4XSL+CV;ALX|>Q|7yM<* zS*lLcKN}ORaL{enRL+UhQqMWFU<_K#a?de~#41}9nPMs<0v)7<@ z*BU1V8+vu+6HA-Qk^5 zp(}unzQDRWAB?%>APMA{@Ca_0CW%%5d)onOMY}mWdZ73?5Ue?aHTNjdRtv{_~xc2eUms5f=ASD4@b2~*rInc&EA|Ubu zs{j!Xu}D1H-AYWd_!St&sM8fXcVSQrxjuTb#m@1_(RF&_9m<9^$7ltyw^O=Lzi4Ok z?;kW`3|T$RSzdd0QgzCEnBnqRD<3;C%^u0bVfIsK(Bxf>CF=N12{77GA;W5b`bLH? zyX8){1@*zZ2VM?MQm@8xb-X_tGy>bQ#H^Cj$FBxxoN!N<2ToPptoq}3KAl*Zrs$px zfQP00FVfC4s>!VF_jXalNN++$5$PgI5kxW;KtVu42O)!qG%+e50YWk&MM^-WDJ`Rv z5NXmCfd~OIf`D`gLP#>GG)YwAttoRh^SmF-Q&X4!B?4 zhSHv;O54k-S$`t#m<_=<(U!mSeX!evm9zwGDB=|?4eU-Dv1=4?gOMOO!R->L3jYXg zHR62hL`a8L`g0PRy9r^Q!{#PvSeH0_7lz%@l96hDvLraKCT@+O@0!E6Zf|?*`xUfR z5mOZAIG*DGZ;FBgyPK#e%oUCCi2BQnD}c(iZ*CB=qKfYOE6S_++V@)@B4rnS*>}L$ zJf>K(PvN()Ku)@^=4i#H)*mOnQ^^_HemHe>x`Szb%R&D}o+ zQn?gnQ$1S+&Kw{z^H;V_bkI==hxETrD||&swbvhNEcph1b^rzY1DfIMo#yoBDa9<2s9lg^`*7K-Oia&>>~B^c5(K(mP%B_xzwY0wi^@s3hfBHqtO98G#e zlgK+Qy>?ak23TkzD@D}G9u{d{3K<}ov@HXfH7)RGxqrPW2SxX>Dj*9u*mbnWJ|Swp zPg22-DQwl-Z4m{zeXmlds{Fy-7Bp7k9?yxZJuwND}32bxm7ZcYmFzmfWG+%$lh%mPWkAdIj<<{+j;Ai_j7D#$Y;eC14IlSEA7vjoqJK@PAz(%ImlK zAIh%|;h`i18_6Hdi1Eu<>C#|2{7wWir^s+LTxewIZB5j9%9*^_a?I1X^Sx|)Lwk)i zokgCX%!}IXcAIBFx^}+0(43+>Kmt;{qrSJ(#bImFm?Zlq7*P@UWyM6}g3IjRA3MQZ zU1VYjS8=i_!^NfY!T*}R?V3*Q3npNpp zlEQeEu&toC5i^ptXcpWCdMz2|#YtrChd$S^C(hEq0Mp1oeYf}Q6Y`xpWQ3o6!Ob@k zO5uK9Iob_@oexM#1okK{>qJr@CGnJGrSe0UdIo~*6eIfT8`OdX3%dRK>%kYEHpwIs`tJizlI+9+pOuB+$}XbX39RQ zJyz^9{EC0|Vy!}@TRjkW0B!Si}lJCvL#nuMB!(JjSe+o zpefKdSRI28>E{my^{-H32L5+t|6OI;sdjN81qgUiS%?ID00GUMNVeELC9r}_`K|&@ zs0~m*iEym{Q_Nid$9QC;5K`Rt2_{Ie%WJNh!GU8wKfJx8y9o$u37noP-JTf;OvOR}T0 zz6XtFKh|3?v*b%4bQ`P(xG@NMTF67Ilsfzw=|Do|Tap9rBSPA%bCISOWy6 zzWkEs?o18BPRo^dG@w+$@;cc~KIkYC8+J9tXZQIMm|EfMW3u>{+CYOGqZo1(ti#@hfp2F zO#%;KWqd`G^qTZJ?zRf~PRi6E8`Xst50t|haIf599_2&US8!0gD-)nH6+bocu zU{yaNi~d9apBTVUgf!TEtVz9)(7qq**UGiN4a#YFx-ARvOqH;%BVR)=jm%NvupZnP zCaSX#1#)_c280AuaT2evlnG^Ce9LNT84{=;sIvomq*1sbE}x~bvVD$tmjOl^(Tv+D zW&CBJO{xVQ2g$n#I$i^%f}ImRAV>&EyR%DnLxPjsV%BVZ35M!0ijAC$Jb_(y*b1B&jRydJH*ZA`Z9UAToytR2g0|ygP9;}>`_&3Z*GEO=HSVBCOs$^#Abq= zUwt7`>1sb4EvBC;P%H7s*hQcDXqJ&;bRxj+tszkkbJ}_Is=%%Upfw{?&*y`&HLDWc z#*Din31OY!H;bd#Gn~Tmp>|opd58?WRXk(6c^C^W33h#*ynXCAeW#l1xpLX1K2Y!% zckaQ`(nb1K?wGj?OYaox12V5JYM>;n8$Q!{r~gS-O_cZbUA0J3RjFdwd~L>l(wc`a z0G?0a8=e6l=$PkNYWi3D#rEOc(WNk(;xzxiH>kVpI$?HrKFj}Z8R?;Om3*}t%#(1| zc6S~9-WvqMV%yB23>IZF(z!OND;F=LhnOn|yXbljY>CRlfi2BlrUHo|G#`DAbv!iW z{4=TGNLA_BQw-i!FY%CYhBhUxi(7w=^l3ntY+-_lWTAmzeY-k_)PZQoO%Ry~u)-vP zZx~Cc!Kzt~;d={YM?@+MO7gT=c`pdfN#zHyH9Gp?cBxd6JV)uZUj$3++Wxl@e{WC> zj_&RunlVDgTfBqLG2weSrRL-z5PIEtqb6(rgmkJ*jh<6{gL=I*5ZlFq1Z1nSsva7wXsoofZxPW|4bW2v zUL0ySzSDT5<}SX`p_3w$idr^qw79v*r?B6FNcDI?CcnS&%l~haws`a2olzwd^*`Q> z%U&`qhKhzwrlY%UWtD*S0neUfHcVPHgwwGQLrXz9eN0(OSD`*z?oKgw14LrkGU!`~n-^M0@q%l!`&+p= zDc<2wgxH-JSeH=zS0Sn09E*Tp{1X^hXUldfWs1V9;smUi@I3_R5yKN2*29K)LXo%{ zt!3T9tL#4pl9{8z$3`mwE*v2(y%Eju{HUtX-c~!Ne~`Pa*)IUymyl0R`SV7Ezk}q% z-)@9+ACMiG9=E*H`i|NdmOA8=o*e=5S~UyMO@o1{Ub`dVvyw5p(e!80jE_wnTdt$Z|!dB8KXY5hI2e_QViY3mD}R&KJ>; zXHs_zbfq4u=UYG@x)F_j42nfFt!ht|+32j) zpvo~;kZ^X&wege0t2#kv%k z%Q(Wn<}U6RNS_~y??719bBR=+ja+LgdaCyfG;Km?q$G&MzyP&vH$tg3v~mjjD<`)H zE{273H$k`bmBFFz4BeiAg^}osMbeo*p{FLM-)O#Mn*q%=f=XY~fGV9?@^%#9D_c#+wFFxC3>Bp~K#Zos(&PiBby zy&-5-3VQ+wX!*@;2bVn&zDtC_6t*j4^|{%Uu0X786*SUCFH2(UfO_dms)a0fi&<`R ztEFLdGxg__ntKl)I!AbLXF@7z!&xdI-DhQHfoS)Xs*WA%Z6+ z*k~Wnh&K$h3Sa1Pd~BtMOv3J87^973EsPcFUNJtJt>1_|);k0SR$5=&K$e&4UfxU6 z#*jRvK=L1UAGHB%3e67LD9Y03la9{)lg zJ7X6(yNh7MV+#nlB59?tI8fC1d&5c)|Ez5wfaN?v%#kHx151guUd3#0&a*J-(cCl1I+WX3>n^E{x&lo`7US?qyl9*}-NG zk;fS~487F(#b)d)!A-P79avh+QNw<>^P_ryHBd$xmBlHO{mk}fMZ-aUd5SK@4@^B& zJls>h=g%>^5YU0a_LR1YtK{2_RF!nCJtQagh0BUN$MoH&4CeBP!GuAY=1Qq-Pe_O?FGlL z1Hm~%fi!Cpa55ogk!imbO?UH@YN%D>Jf-HLHDx_l`r_gV8?DOO;~zg=_A1TxzMbJ` zX)V9i%KYIETu*BhkgxcV?cnc$@9V`4BgwBV0>AVkn~z?7>Y{N9W7D9aZFE?+Xn*{Z z@BY@3`YCOS)}9fi<0Yn`{`PPYCy%4!YhkZeHwRL7S?mOOkoE2dEU{f#^cKW7kwA8G z6;eY9WNOyU2pDk;wg2KZhOfn{GZB+HBM!suW0i|$3G=l3Xn0{ntWj5fX^s{8SFjwF zg9FfZdpv+E3!GQ~fwuoK+fw0Hl?>+E?^ftqMi1XCJlBgXnAh{yvT@5Q)u_lSoXXm_ z^TuDKt@b;ASbD?^KKl5wY5G5;?yZ`_!uNpGErm4`Qa0NbFWw;Migs9nQ+6)ymqCBC zI|oK^H0^t3XWzZ)eQI&#H$duE_4tPsZYrbzR=5-3(lJDLqW>(0fgOi*)nWTy8% zKOzhr5pqN3-o=wQ4ji@O)@ywSX?zzPIzW3Rf%fE@yuw2oK$9YFeWWol0=^YW?ScbS zGj|8xoI#C6`*3d6-0+XAz&Lo0wJP(sc)SZPQfzrSVmNh8SL1=LN682?YX6nQBlQYB zQeF0vjzx#S2*Txth{J39kcBoz@abZ+hzh4{Qy-~>;QLuo!RJnR2M-kPJXBeztA`4; zHy@rSd48Q=A=3g~-WOZ>3>#-JIAIwt2`$7V6o`iA9)WeLy_mka)lSr81{=KZ)p(So zRh3hC#lIws{xFG%w8aWCHZ& ztul0D!Otx7-S4%+l!6TqaWdL*(k6X(<3&paqff(KZW;Aeo&`7{6L zLb+TcW?j+EM8qMQz9q1@u)7g>xh3v8YIQq!j>m`8CO}Rhemk-Tlka9{!aI}}?6a@E zWs_~+RBK=Uqix?yZoO}TLs0?COMh=T^MSYiWxGLXZ(SfkV!=vV=HqaDl$P(pNU6Qf z*%6YBB*~^gtpUt#D~yoD=!fI|6iO9JU>?r-;}ImY%v=v=vX(R0!&0cl0S3I>30YKauh;XJ|Ij#S!6d{)0>|k7%*L{nER7kyx}x>3_8L>v0ntogp|9##K8no z;J?Z{xjjw{QA<&i{j5(#$$%gD7u+t;!nJ{F!5Q4~k}J)$?$+Dsjf|CMxlvKjFQ}EF zITZZk$Wn9!?|zkCDWdA^$;osRoF{V98KsfNQLZ)Sx^k#i9H@dj@uMhDxn!Vvg4IebM0A&o3 zh)fH1PV-mCkYDSkfcCYnuoN*@z}ReV$z~{ErG)8-Ao!hjJ*a|wsGfVe8!nERy91_KE;J)G@dx_$sKPi#&`Sz5P_ao8rgOMXWxboiU*lBr$*T7Pd1qf ztjTo9&vK|?Iwtk?!5n3YgX06nUNM+r4^OLjxqDP3VP{CG*`E3NV!PNpM_%~%pq{rG z2SVYu_6=HKE$zX+$%FS*`i-tWOYh6Sk#%}3zE%rCS3}#(;V0*C3EqX`AT!n(qD!?J z)S7=7q9mn9_uS^Kl!weKk+y~Cgk6*!BV%y z5!?vC9}?+@5aOo+V1b5jAkhM`u%~_1khXprk#h!cn2BAG0A4lFv|v%oyv%K0K8iI* zJZ+(SCO&WYe8o3Z^}~1hoqa_FTih2IvM~52-+6I3D;P=M-Wu#gRsaihL8lzko{T=& zUKxCHY3JKQQ(6gmG>tq;Qs+HJ7L@Mj=GUc_Dr(5}>#K;y;m=^*`f}J~+;3g0Q>S@l z5P@BDr=^m<+6lyzS4Cnq;LxjZ@u?PaA-P8uzPt-T>^8S%!6u_wUpka3GZ)O9+Ik)V zg(ABD%cD7oC#t#DD*^nC5S!)pyXYwB%piqN%~9Mt%*$p33J~R4ABBk11((Dcxcp?) zZoM@O>5l82S2(jVitqPE2eH!;5@Wp=4-q4-)$4Vo{^Eb@wL*{nPPVGs?HVUpE5_#! zvH&+*wm(`_eLx!e^Pdj@ z&G7z%gKj^0Uq5@l3%~!nVQ~i=)HaB9*n_wGs8UONC_ z4+qEncm8#x!hpd09c)nlo0YQucayepV1d%Sx}5oB38%q5l)M%E*j^^DJ3PZ*~xbx zo~;p@9(cgSvtcfX?_1qY3(N3xg*XL>#=_GzsHj>n>cU*5Z||W8@HEmyW`TIlnHos5 zER&njO+S3cs!_>0S)}2PqdG1{iyraNyD`y-`WciREcdZT*-XN)y7w{3ZmHbYkaJeu+Hz}s_SBjHzGESR3c*0_He!H&-NH5-}6yv^Z8 z_s|;ut5ftlZ2~`63I~MH3)%wU0RbU2>GDB`|2(C2La5>Ei`#Q~rhpKd6f9g&c)Gv7 zjtng5zdnwio3+=17`_MY6-w6Ji}Qg7>Rw>@0Zu2f5a}Y=;s}Tbc9@5<8PI;3KqjU~ zRS)v2p0OjnySRkfjXlSL|Hqaz09)ex;9^jw=bP0suX@u!KW)}KAji1pn+{Xks#L2= z-M$2;lr_&iRf2o-WVCqb&NuQzv3_*=!GZ)H@7UOpsbdT;m2=%T|DaYo?VNK@AHU|B#Q zz@#hd%p8I53G&oWr}(~H3O(-SZGs#P9bZ%!X8Ka{BsFq%Q)Ve3@XX|0Ym8I&LM5V_ z#~K?O96QkH?EmZFR1ZLBZ+cq2U52f7s$jwL^Dx%2!&D!S3N>=3yCtWnU;v9Ejh`bhI7o&n2Hxcu zL?;y0lDexQhi*iz(O6>5UZp0YZ^X`bUVrX?UYi)md#!W?0*iRC6gf)EzrTUW0jdnf<39c@0{J)Y~|HW!4H;-#M-#7g-nOZBg61LGau>izW4+o zeGUwgX3kH@h3Whl-p&btJ7=sY!IePy2e7pT*@;u|7=Ki%~K% zTS-n(K@keThCkB|@92j5Hm=7;ndD4bV5$3GVGsb@Bs45lNV$<=ZrPYVBtz)xB8 zNMpDY7&q#Ksw9Wfvtla}16N_IYCH6jN!jU5IO~n^I|*e*@L|S{#ZdIg$|Y6*?6&1w zgTkt?mZKl~UTRng!KTk#LHk~)pQTSfPG!_mR@5Hj-nQ$HFPH8m=wOehJQyq{mw`M)<15a1SO5gT%oms`>NGvHPtaQcE6Zc%=*7ZhgC zjqD*p`?jyFa&5bQ;FGEjt)f@LmG$s6#53=;v1$UCiZAb1sSMNIVI&yV^_{<;AJUE3 z3s82mrlUBi5LIJgBABJ6xj~z^WruLofIgvM4?quzvsA3-N}CcavcsNu2@bS_s7b2? zy0Nuz;LhXM!7ax-J8=nt+8z( zu7s!hp>DInzrKSJj1^=S7qKE;2(PelIq^Xk@d-aQoxN%;EK2%BN#f^OtbStlEr8z0 zN)=xUs-C8e<1~c@h^zWio>*7TJmpTiCLI`gjup75S87AK8lA>Dp4E>+m}vOPa#7c? zej3(PHNSf47ML5}&l=k|e(14#e5AAvR|+?T=h!W@i)Bm|p(km))Lh_y3?HZFaL3z0=Hw*{ndW7EVVv$r*AT6|693y|^H3G5hQ8s!lMC@opMp7(gY>en z#IrRx1|Im4Z`sh6TH!MCo>C7K@uH&Uql-a$nE;n-3Njid?G*R>9SM2dKoDYd#;gfZEjBIlVx-RpBi{O{JUu@`CwLG zXp1AAt1ad>t z#Od|)TJ|`2L?9IWXtEBNUC(s8{mq)!vzkl*nEe$rp5h#VzQYyoddwZC?2Q zJgqf%$444aO5(w1h6rRhs5G3iKuMTF2}H*ecZ>Erl+S?5yF67N+V@Q$bM%#uICpHI zw)9{^#XAhGb5>TE5PB7BcVaww@2yB!%$AiCl_BVTfxp`HmhLq2O(D6a1v~W&^JI$w zgrv$kKYwz}!6xfP+iJj}z%L=aAv?#&e0lrIKR9b@oM`{BdFG`H#Kl1l(N%b#8fq%%{IM_;Au@e*xBeqLkB98_Abz?75@IySEyG8LvBa zE%aT7F*HCAWvqPR?`J?YXSnmC%~Y zARh4qlY*XXtTC*TN49YW6W>!B;{xYebE&)|7p>JiryFj-W=36ts%3p_z zngv{N8Zbro;)W4&fLcbpkl_M(u!+eayvIYDx#(>Ltsv>z0)EBa!({vlk)ui%%v5iec?-@C zHD*l4h2ZyIoc`#ez!}Su3cf#MXk4UP+f2w&whhnFuu6M2($SmcBUPf~lzqr$dEPPI8dUPJ}cfm>c;G;W}6DL%IpQ)Zecs+GCBz-!};*L2rk$y zY!Zmi;kSRJM<$u8&kb(>Pf^N8kwZ#oJyGIG^Dp^mv6u5HnJ?E7@FZ}&=>k=vt*X-C zrb29+u1sM5fG>td0^i2Mm8}quV0or^IqMefT0VwTXlM}y>|0z-F zXpOal6ePYdpbNPf{!a*#d1NKOOR1n-930hWQlU3q;J#i}RB;*0y5eEogKwrRCv)MQ zJ_i7U196+!G?-z&gRLj;$YG=kWZA$DfGprcx2hD7gNcao&}|Ie+*P!90&(f}@>;*# zVsDV|^Tw7$_k$^}>1U_htD*=QEArro%#K*>u;m*DQEHP`*&ff3eD^L`x@TPZV0+=G64&Qf?!95|Y%p;5yytTG z`MlUTNxk2`jl`Vefwk73!2TJM8v+l)Qtvc}rcX|KNE%PMlGDVEg)V#FhK0hf?_=xUP2Pk#?0J zRh9L~MsM>u2vJ5W#vT~{ih|V80-1W-k#@O{Tug*upIkO)v6sd>n2y*IRKYZb+qxTe zHztRF2>FF_#lkz>LGmtH*Ll&cV4jer?RqmSzY0lg;)PYztg1UK$$nP{I)>A(^UV!J z6T|BeW|;bxAZC>)4P9`|&^z986E|QypVVfR`Yp6d8!htgc^63qh!W9`K8{m2We>1T zAv_ad2~G;L${Hv>Sj%@eg$mWOb<^%GAjbqD8r`M_!iB}( z+6qc{U-K}Gru=)Pl=1kVk{hOyf_O2%q*RW^u*+JYotSv&J6#RZGZbl0Hog{l) zzRN)HD`F3dx7nptdR7z zhQ7T!TE;?->p-(J5s86M+^Sd$DodzcUMEXDr=c(u1k^BhA8M`)bfiQ0lLXyQ`X$Zo zUu_NDJEu=ceH<-P%8^S{z4`d`R;8h0F$?#h^!-_Zm(*+w8iINncZPo)cbs?8I%W8w zU##a~c8E)jo1#zKIoGO(pM{rhW@c4LegL0=H@$$KdK|AEJOb^sI2mV0+B>P_g^}u4w*DdZ5nj-`u{RY{!G^pG< zkUU=H+K?66?+OImzLB)dXX^{-t&2Cm*sj>-NRm_oCUOe)*_%MV3f(caDX^b>_R>5t z4G5!XbfVJnjc9=swR3hSCu7C#NpQ5o3Wba4YIZ{@P<%6{LwJ##zgi+{1k%c)ojN7w zLd!5T-tR&pApM8<{9+P4lW$!z#kd$;?@5h{mB^n8`u0t3c>A)0;D9mw%W5^AX~05> zoIwL_heVQqzeI%2V{-rAkZrgTQ2S&fz3cKtJ3_(R)lB|n{B$3G6K-;doJ(#01vHKM z#|4%yN9>Fr{$CA-rk_Rif9zDM0TP3}OB3P!)@Q~=(xO?V9zvzNPmpK({ih&~JwTZ& zZ%KUgbRjRHH7EDStzzFuzGAD%xHR2PH|(@_!%b@)wF(_c)KRQ5lmu+~BVnOAfSm6^ z1OTB{VmJP_&xzc#Z5%<7$+~LU7DZuey8_f=w;h=`%yt}tcNOgb2^SiE_WAQ?wGTr4 zOfq$*P7SLW2D%1y%7lz|BjZX_s_c-3XRAFQkl;am_D()u$+T(L2IYC5@iJm-_Kely zaKcVX>tlPL;lsIY3Zt!X^Gm`poslw7@r=GMa-U{M^F9gFSz}n=Y8pqLHvQ70}7qU>g zv;%J+q%s#O&rIq-O*Qi<&`Hh&6;FqA(go&_H+TUd;Ol3D+{E~{u!=kz5?#a7k8joB zOVNXVxX~L6tuM4b|E$OUQe9K_JTtuR&wcgTMIwVRnQkHqh@H_w_Qcc9Kz=tq4yA%ANccf}mO-Wz)dlEtj`VzF_1#$zjx zX8w~zo`3wLjVo<{v0e^qH|%=d_~_J&T~FWm#lHO6R~WbaNj|0*ICCDH1rQebfqis1s=I0N329{0m-fu&qwIe>p)Jy+uePJC%^_h4>EMG#N^ zLREKboKC93m5i(Ri*=nz_LVAT4Q$tf_BT&TRaC|^>{Q#!vO-*`$+NmQbo%pMa!8J1jH8HEWU0#6(I(Nf%Kl5aU0RpOa(&enB!;bK;j>dm7L3f z)Ih6&(Fqi1n(mZ!HvPDj%p8>Xh^X~%D@rQyntoy}(O7A4T1wM>$oWzW@n@#VV;~~H zEc@W1bNRbcEsG!<IZq;FBz!sZ^Jr?ue zcUwJ)|FZ03>)Edqi519uxy+P*NGSH7jtg z6Adr6dTCT715L0%T}K{(c-R1RVK5rHrh;(4a{-EBIY^7B@@_@qZFXK~hpTf!jYERu z*yo!cxu4s0wNa_j*G!Z-{aQ=h>+3JI}p9MwTpm?nPeBZGTesB1_G) z09~VNaF}3zMm~_G)hV-N&8!gXF)}TdNesj{<}Pn#W~9s>g1BIL5ER!r-)Qi=N_IK7 z*^IOnxIoemXlj-ST)FKDWwbIXDX{$zlE$)k zcsGB!D&ktMgk-i$f~97qRLZ=^)nIEF`g;=LtScDu#D5qKcIskjjS?(eG%x8`xe+{5 zoLmhmxrZ8D+|sfY$KPL^9e0nke4A|xW{;hHly`sZbqk+W&Ct-VBlQjayT!5oS6lG4 z*8r5()=TX`C%1s~PpSQD?9RiWrLszmPSDbX`O>fMTa0u4vnWH zd@R~4(132^D}vk2*LU?GP~QUu$WA!SJoIVEkJy~d>P4SJ95)dD_=d!qbme|)am^=r z&h$rx8s-}F_E3+S->|B1CYec9nH>!exLZbc&O+&s?Tyqt4d1i5PGF9AvD}6W(Ahcf z)7{F@zUzqRMPE&2ZD{4El*iO034+OI@a*yl#3l&JM&jaTsxk)Q(;On!Y1fXJp z+6&MZN}?grF=&c2KHEFO9PEKB;5#J2EUSe&M4A)#KJtU_JQCU}%iZoNOguQ&s)I(8 z7xN0cwEUv3JsT_^H1xfkXN5M*?S^yCif5z*unR!3-EQ!1CN|W<*daN9Ekoh_S~LB` zAtiui9^DBgBJOTyiHlwZxLO#a_UuO;s^Seci+9VPVh^bLD?|PK|(YT**b-IYaj;@Ttga7g8-fY+7$fRtp<$ICH)RlVf^Ib@j}$ zt~V@(VZ%A26|SVD_!=d{CFcYqh0zCi5XAf!Lk?^E4ZaTk({Igf-|x5X&Gv_0*|A6C zKeZ0Et*Y?1Pxd=!@JtWxFge4}SqUvgU8A{%{sJ8|vw|iI(d-s&8D?PJ+TeHK7 zteQzenm1~_jxD#>>v7C|skxx;S2KYK9h|J_qSDm5i1jrqsjQ7$MCUEhMkvQ<5K>DL z$h>rg?-7Uxp=;=X`$yXkkRTeFA}PiWS`dpOW2u^79{Z$UjeGEqR57o<+Zr1IcUKxn z%&j*o*7eF)H;TG}GF2RI69j5V)7_LU>*8r!z@T$nA_mQrPtg;&1?H$~Hgn~vp~^hj zB&%`eXr<%6QBVi6Y)z0metW+}!!fch@+ zn*6QKLl1Y3biMt11L;^w)|lDD6vUTLjW$6}h3;Sm+OSyb*vDuX6+a!r(}hgt3b%t{ zWcU<=M}u5AA3Dds&@q0m;1VFaOh<5#0BDWTpyo8YSo9-}CyKr5+CNs;{yoC98 zpVix1gGap2scrgvRF(bj&TzcSvvF~>%BQ5lwL$|73*O8sT_5uG>_tX5tz7}5gpciI z0+9qI!(3fIg>s-qV`Y$j-?r|jxnvjKAIQDGA}UknN{CIwG_2wuf@?$Jsrr+5L_0c| z55B?Ovxi^U)T*vUm(~|MuodB8o|4uI7Zl@A=llXu_Y39I7HVjlXk0bm~;CU%fDL0@Kw?D}#vm4NO z&*ZLhrRQjIt@{q$mZ*AP_2hJ@MB)4L*Gm(KaXD>vmOIZuj3C5Zb5Gm1o{(S=6q)@K?H-JYp;E+Hk!tWj;~gp!dk+Yw}G# zcKQf)E@-Cb2pH)^w(I2Pe!EY#d64CgJYovlfbR$>qA`gpCxM{36Yu6TD3pboqVXIc zasDMR0XU~Y69Y>TRhwCGkPlg(oA?Wu@(P&BtU#M{#sUt}ZmCf-?e6CEC2X{Xp4}9xFUh#VK|?$+o4Eq34rP2yY)hN z6rvSpb@??c!Q$`|ac8Su1j#)FF1ZGLA#5pEzDV-)b=zM59n2fg z1zSAHK3TVU24cFgH09!HhlfAjc z@il#SyI*@EJLlI9yPHSswHcRL9LqM3-FdTEJ4;i0sLQJ8NM%rui$#kNbkT!XVtA=9 zg@52w!HS=68A#UG_<91LR3?}(*n|mSZJp@GZ!&uSVh*(f>&P)l6{%p)jka!P+A`!I zgewQ0ZfI)Wf0}BhGG9jWCoc?poKq<aXbe>2Xxa$Gjdg>0>l8dG+heA(5z$8m&t= zYN}_6zdY`7O|QZ3AiFENiAd@od)q0U+{{3EcvRL`czb(w9(aT}7S5QiVKJzM0j%5v z3dn+|7$6<%fJ*_84ie-UD*)O6xu6~H^+##f_>X16mAC!WyWL}d)b=W9Kg=6__B;;?bZP))i-cG26$w|( zQofO=VYvg61oD|I8Wo=e z7efTss9=8^Z^b{QF+<(FQ5!KM?p)F`VeXiPQt!W+wKU#_I8Plz>4 zB{oql&qXI_HgwOwsK1=_w~2QdbXyn-mmCdUtsI6mD?ERaFE<@6IKM=i3(*m+ zxp|4j?;UG-Ghs4095lnolNP7XX8;Hu$ z&b|eUv=<<+;y|3q5ih95ZK!o)^`S>47@F7HLx$5H0_Dhxh2d!w{?beZBPi%rQ}dhV zWb-}3+g|0)?%4LKz!a51hzmw zE@TCt-kRFu>j$L82$l)mu2k*X8#gobQ2r}YX(WX&H+#Zt^DJc&eHR(vWHSK<I_^ z(33_*?Osx~Xd1yC2gn7;vg9LC;#wmr$wy^E|1n-1VzVTws)Tt9xh6LC>T3ZYVr5Y5 zi*_3>_PY4>X<3!{MLY6tX5k}Ey`9N}{Z__VLF3~FX_jY)I_(>z$;mE(-A<72a)5>B znuU7o8~sKOw}iUqGn2icOUSm0|#M2e{WC#doB8Ncq(eDzDFcA)*8eq z?h#1~%gkfA4y=jf2Kn&|7=1@Bx)XFAr);Tai1@3GsBKj}RcZ^OW87UiH_wIm$(Y5x zvyBOP%-@fr=gF_cvQTteoAuUvGEt%d|2tf|)qu0uv&x!CT&vjtQE1~nnvlmFCB-w6 z;002&@EE-irYJR{3^Uxvw-~$_$8de#=)S6NC^W5>8{2u4Y#K@gO&4T3X`Uvi2&zd0f9wo3C&1vahY z*wbWS4&}aLmMei_xyt+;9`e1U-8JRdWygf*$utXjaWVULwQ1XLnc?hZ4ipT%IrAb* z+vf(ASz&g4-lo*v=4Ros*Q4nTW2&BG21es@2?{dvukg?LnLP0)C8HvRJ^Z$|sPce67 z=6ems-!frr3qWO<^_L(dEc6z?cm_9HZ1`e`-*Q^LbvQV>z|7eU0U~bn3Z9AX`Fn#5 zMxRRAV)*xly;w;|kBl2o(qGZzmVIHpvDh;qQVpgw%BNli^u~Qq7^erl0p%rdX@0wM zNT6Sh>8@$DV|oBkTz6AF2FK_&d(vM*tDGa7H&v_mZN5YE3wA8D8Ok-%l?k!Q-WyO) z6LYBJSdBTH?LbooAYN~`_)>|yl11XCO55I=3-kHGW? zvDC!?p3$*uD7D*_U}|oCLi?eo0nbGTm*wb@zx99PJ~q62^1H=u;IO^07dQ^I9imOx zO%RMpBorOyIDl}BZo<}WKL(B5;5ayND4kUS-M9msq=axrQsy*PlZzBsLp7DbKAWMq ztKo$wTlN2VRixlliK(~cM?6N?;FQy^_?v?M~A+jaRLdxA{>fk7_IW`v*fQ7IcK}AumGqi>TsdQBA>bQU1VC zR0wb87b{_+UAW$*vVSPxfJeL^LS^ta597ba&8`Ei`xqd{<~YJ-G1I&_4*K`^(AF|w z6*gID;f5#;Zp%c@G^?zT8~wo(qWrxVd&m6tPb=7JINK zn}-|wV0$r1P8Rqi|8eu<#k8u6_{+0$I+E$`=lUH>*P_Qtoz9)JDLjAT|B&^iaY?3a z-_0~-Wky=AsoZ6z;+Ex}!96)?YHH$|mW(>)mYG|U63k?ZYur;Vji!tt5-O1kS!R%; zaHnJDo?0p}wz%X)C0--Tyyv{{r}ue2_N5+}Q;s1Vvl z-pTI^$zmi7bpSKY`+Urr=}1!A&pQ-TN+x(Ye>b2mf05b>(b~{tkF^@m5|QFkawB&r zfeiN|!f=VCa{thez9_HGoJ?q9nD81Jx5 zKdQs2G4TWfS$JJmez|P^<0wu$alB>k<@N;DpZ6BU`%$eo62}-oQy)kRPc5}j{VF`* zW0=<4Eh*PI_4aD0=b8ULU*B>{@o~-CxkIOU_^#QGhk4#CWC)Z0jr}>Y;Ev@td ze?GE+^p(fgesbz}@N63@o4|y+T0q+3mI?9Z*Sm2yjJLs0r0L?6pH&WuvyWQg7AXU( zq!RT%A~jUsxX&My{`p1m!!UO@esS@=a8Dv;PnVH#C7?Th^o(+7T&(P@eNr!sxl~1d zMQvLb9N#A+!`e`~WRjeL)WYMwhle2HbMTqSNYQ|MUQYA-TzCB@VQYMxHEkVn`#vc) zD>X%j1rQQX;I)c-{J10Gz=e+5__GI-ditUthArbMr?>Y6+423Uzw>Ku>^V8A0 zsrr#NN9b78*VsIkA02ee)PnpSWTRWdxCv&()w!q?bLC8hFSj=);D{`Zx%4$iZ%Y&$qdLh(kX|FeB^puB-jpjaMhz>}MVLMCOfP1d_HT$xUZdZ)k8}PxyIRlQ=naxuR7`^&h>+c2Iv&6frgZ=A4(I?-$Z? zx;&^4gyFeL8NKmn9sK)wg|Rj~D76*Yqj$rVhOQ)vu`Ri(EBsvcYLT0h;r9ydM%=-B zSB5YBR3Y~bjkxxG9XFylf)GER*=Fdm#l1Z0p>}F(^%Y*5;K#$MbiMxB(dwn!Of22S zXHg}%Yw3ZBpzRm$;fo$RSFSc+IlB03(PYKz(|)L6RN9vu%zRIc`Pi8a;qiokG8MC+ zgkT0%KKUY&06)ED`qjf2W97 zmMe}y@8>Bio25cfkF(nS_ojmEtQCzY3A=Ti51<^6jyPks2wof7_0g%as*g(UgvL_10 z3UTXHK+qV?3T>%=_eZmEIBIZFJOA_U;x)s%e-U-;gJ z)imB zshRl{{#BwS;R2iojj`e}NZXvtv9Wjb&iZ6a=U4y)31yx+K1|PZ(~B zK%nlHH>G=2jxQ80(|_rWPJGoPs#{6LPX*FX=CssHwN3piN)vBYIyg55CrmIyYEZ)^ zAduHMyGe5=<`Hp(iu@DSmLG?UB{;I1b(G|7tQrWw-vrx+OMg&p9_@;e;EHOGLLK~7 zGSP5M5uujv!0fV3xF#Obk9YiXUPxO^kD&AIujpb?J?n(+QWNncS)~Kw>~vSM=7+B1 zzWqelr+o4t(E_TLbd;!$;SY^IH#k(B#C(IVJ}YJ17l4B}hMHT^4T1 z>yj=84rH3LSmR|6M+UiVC))DQs|S`FTV?7twAWe z^R=3F;=F@{hF7vjUHaNe)&)JMN=XY|H^gC9tDgBq7&cHoQ)TUV7dMqK#quf7@o)%b9AA>~?yHjtag1(Uf(?701R2&E0mb##ANQzO_cXCq8E`=_P z4=J>DMQe}#fkySDBn1kL`ELn8cYK`y$GJ<5gvHhl=e2Q?X5HG|(hIx?v-llDfpY5y zrJ^LvZoFsEuxP?TkZxl}-ziH*@ z=~u*q;Y+hRg_yK7hLqE&N%TFo7@w`df~rb_8~o>H<)#!I|gJG-89o&j@9@5;&Fsg!UASsiGSx7=m;IAtTO>xh>X2OZ>53t~LeRCLMoY^uR&=R7xFmEI!SvG2$;C zIGthh&?2-gAsI5uWplraR~f>#(l(HsiR2@%W3)0TCB70wkrUBM`KuyAjM+*!{S+RB z{!OqOT|sNXC%&A8aD}6uWA<}EZjyN`S!bX(oJ7v_jahcQ`idSg6}2e&sPw=xd=%DYeeSqd5PxE07OA$88FmHiYDX5q>shX>x9uK_8Jy7! zIMUZM?Ad6>(};m3V))~Nk^Uvgd6$sBC=epsO7ow3?A;zOvIKGL9PhH3@>3FXP_~Y8 z&L;HZ!YZv(V(QD#ZFvoJl=DSU=67@&^ge(+HI3z+Cd-gSRP6~3+L3cYMGbv9#TeyZDXZJ01|l= z7^MAvlTeCyJBa)7+FjqP6)(FTl^&h1>>bipn80KPoF#DFh@~hF86(g(&?S~*Y2!e{ z$I$Tn%JjI*ocaVOU~PQ#>|tF1DZyRS+=IKL{#sST0#-ZTR40k?=sv^q~wuk+q-wkGuLkzjO(UtI38f+nMI&n9~j~+HgZs`e;l?R{tJifpH zZS?~)NS_HLnJ4LysQ9&JpfIj7r1_K^Nmd6><^b8*ckr)LvKLg|LFsZASsxg5Gnw}p z((;;L!tDbnYWU0FM7Zzp9+JLPJSl{<>w6}?En>KN`Y-!uCs#tCKOQ-qzOMU)4TsHj z=ujcrnQ;zjDsX-mLprf4m=5_xWJ~6Lqii9BJ|O&y+!Oli)WCgMF3u9L4OK7Dx)X~1 z2^aR9Ep1;#5ROZ0>+A;_n(f-1Dklxl=;<}f;i7j|I7t z$u%0I8VZy>Ckrs!z?=I;l5+hbS_9^RI?6%`B@e(3Zq_2U{y9Y`RKqCj+E<6q>YG@{ z?d(di%>^SV;rrptlpP?GAw8ifQ}hfGQt|hu;A_@jZ#*t?q_UMIq=d7WB;HgdAT#t+ zA+XDOIraLGw1+T!((c8H?DYbIsad{TsyJoq3;I&k&6)&_uBwUEUG|k{W}5#qGvrn= zeC~X`Wv0V8>x6w>A;aZbI2W;nLO1wzfgOmT^e8V?eq#1>Fi9R|Orh?!34q73JoY9= zn^**qt9|rcqM7rJ!zHQ4llGvHG4nXxH8ZU_)==9&JAh#uTfoF(dxC0Q)0lpxp{^P4 z^-o|9D!v70^_@k`-mV1EHlhhZYZz#*4#MM*x`eWF98woaP0Ib_UP#<@@3fVsAam=2}laGqukoL0OlAt}}U5qSx!aCZmg+!h;x-DY&)u7Tv zLo3RylPWE1ok`Umb?p(wd6p~Ay5`3>sR7*{!6rU?KwCqO!yrahFyuyjS#uH@7lBwK z?i*aXmh5dOD|-m1{IodETElrBmFXv+#?~IHru(%A8z*>9hw%F!gyv@&T6DgmMLIVx zS(C>XgBO?{tkc0ePbQ|;tx;ZO^PzPG{v7FbI>7EWZY_?z0Cjta0+kzaCQ=u+l%FBK zptf?@xoa3h{G_Z}+R#y#l>MN(u`jBgpa-!id`!{oF=(zx5e38)OBGMJQXMu_-MfJ! ze-$|TGR^Y4Jny>$4`xTm=+(Ztm0)=koG$BvkI5ZCo6}k4-Aja{(kn}1QlWpTY9Db5 zY{{{bA@ba2uKr9wFZ5RtqJFfKIQHzO_KNwyl`L}pFjtM|nwAqJ9qF{sofM9c zO$%h{0otfirqxsiQJA=Y_A)2Ic14U7rNT{x3S^O6;X*SZ=6WSgJp-@W^|2$nnsmA6Ja@e_mAI$MVZbiYP9~n9*&$xqhZXulCgP#XrZ`)+-~6aRG+Osj1)Xug5vY zOWrI&xE>?`>bW%e4{)F?OWAqYSDW# z;p+Tc$)iBf<5k1v^9mhv#Y4Zu<|B3`qFtdiiDz@4ZsGOUVZP0gT3WplN$%|+i!oZk zUDUA`{hXaPmio(MCPo%=L!%-jMDSH3nmeF?M>o&O@(3n6K-2)PRNO+maan znKMA_i7i*so8;aZlIY>G6lpJ_n&KD3>nytLlm)2P&mr>lc{&IUy-fohG;D? zLH0#0V1pjQc||gZ#wd|1jL+GT#=>%(KYzJqEw=*3W>ffM10f7r?`4w#+8Qctn@9GQ zdd`DPSHw4XC$Z`KKK6GBonK&|9;8;6Su<=V_Fw|JMNd|(B{ac;M#aeMoGrK3`L3pK zYgSfdl@8SoCd#|ZG_v+UWlPfuR${i7_mNUpV7hOqs9lKnaH!Mwa4<3Wwe(crefl=h z%>Umi`KH->$6qP3Cgr^~+|(C7NyEBJe}12B5mHCxqQF6u&w07%?3g1P)m%rLhB_z!G$7<2K>cqm~dQNj@yoxNqVODst!Z+^&147$T!fR`}9@2 z&$~yHT1M`tMpNpZw2?m;EZ&M7+8(~c`{pr3USFW>*OJ5>!wbf>C`x;Y8CJ7txygOn zLmPDhX#EVz50otkPjqlLdU3X#o@hJg@Vi|j9^0{%zv^zW|$9k2r?t9ERWCx5^^^k0@phA0M=J-kAmK_=ki0{dnU4x^0;4$wOe z-a${XPbBAQB~`I+5^}Z{XVxnYKpHo1$JuY>KYQS1=&t#3`!SYEj#p%a<6W(hCbHe9!~ESl3HB_OAb z5jQtdo@1se=RLcT#pna0s-5vj1J%LAxqX-fwxMFvC}DpcepF!mFDUDmug`WNz9sha zwBAcY==US>wt~-X=Nc$G?cl6LugjlX`StNmp*Gmkllgry(lFO??PHzx(36X826yiF z^`q)h;m+PpHK~zrC;W2)gO4KXsi&5Xup%aj9w=^R1fD zoTv|1i}oH>VBxpWB4RUs#-|lxbjkJ@FtxphWKK1jntO_>V`>gWQxF0UXr#gI$ z?yh!2;CJLtftcIm|CVs;GJm``q_Q23K@r+bmKWjzLeool>E=9Js!RY6&r_#uMe9AQ zH!*F-6o@af70hW7lAy|ND)J-JD4A=jIRBUnTZ;U5<2NLI7+vCaJWh1|1IQ=m8Z2?u zptwSghDp$f>RlkMR{kf;-_>c4*ex=@PO@bQ3y@$c}WFqg+kiMRK;I_;7ZnUZeB)2;oyxJH) zhc>1ge5S1&Zs`fCI8qx}ZfbSXc|3A*#@ji5AQ8?!0*#s7Bvi|ul`Je1%GzZFxFARPnt%3bPBn0 zH2z9QIPpo-bYj4Oc2tbj$|c0a(xQg*^*87Ky45?os0Vrfc{@}5XyKQ zvjE^w%-u!UGR)n(RAKA@3CR(CI`{q}1V|{~E;&sa8&AZHWF#%&+j9o9iYG|(b&=-( zlnvCxh-RsfiRUWAhTtE@m%4OwD5O$I7#|mtP9K?3jYxY>WpzqfZRUIWN0_y#TU(Ll!QHwE&p6c%UFMbQn-+B|Hc>)(nzs?u5$LNifCIm5l z5nj;iOrx}wJD;dUTK5I^Ovu_c1i<$kUc0Lt*X;Rqu}GujxS0y`kESYWfTZ;3Iv!E> zk8GfWyix;Z$Jgoe$-=&8p4|qMeYU&b!o~dAeGr46vI{`Oe#{4hO-VXBRiB<-YkD*F z=0UE=8u9PP-0{pL*`vFj8rQ?ScdoBLavbe_SGW8ZUC$%aqPssd=%d{GSH=p;+pdh} zQR&pqG8d$kI`;=KuwEYL;Lg7{1$DU6{2=a~UNQaV}c=Fj?AyBj8f8Hqnx zdtmJ1vbDmUjej2r$tcn5l_9dGOxB6GD(CgXQq~?ZX=RD2aFZUKu3XLqWI&Q6vXl*9 zwWnw-&OKrmUE*$rJG!S|R-4W;nBb$L-;SL6_Df%g@zIf#I>(L;4?4sZ`j$V~4-5u6 zwC%sKzGTL##s~BUUdy$S+1@r-Cc(2EbbK(DFV8)@UeY-Be`U@Vq7P&E``mt#N}r(5_;4Kp#zPV!TV5M?76T#Loz-ql?2 zyF8OpKv5Y{4cY=UUL~~_AaV$#bfPsOokoIfrqqWYX(FXQr;w{kJMuUP-H%-JjuBtM zb!KD2hBfy|Olm59v5GLFtUjM<;)~ikDmm!Q}M?<*YQP{OA`shCC_S3F-B{!VFY}Ls%j5aXcx8F12Hh5cQ?W5e{Ztz1@I@>hvutD8^bs#1SMDf z?BZ{nK(v~4LjVi;^GM4!$n0h&X$PF^1ErL%AvG8!7u}4+P0B-`M)@j>XHZEe=Q`Xl z>OW3ze|p<5Y02SPgvX<#rp()exP>7RPoWi|+7SzxNOXl11i>6$hoX<`0{i^+(|Cp~ z5>Ai@!&xFs5%B1>_2Sy&3RNb+PVp!D(By*u^rT?+AjA;-#x;Y*DTV-s{4U)3-EYi6 z@v-7-NRN@sc;f4>TQ?6l*nPf1$2vC~AE_}u+nL6XHuj-LS@a(XeRuQ7qnJEeK-!l8 zraPjbG(3YTHVNcR{Qh|{EYvsM9a#>%5*Z)GCIz&-p|sPnG>>~FCT_+^^_wi4>|2Zy z8upO}Nc*N?Z8_ivpHq$E4A5%C>Dg^i-0}=v)s6lZ?vNu@LqrXTviuZOCi8YW3~3Pj zmCv}_;dhAQ@b@b&Vjs%Xq|n%Oc*TA&ah=?1MY4tvN{T_(w|(ADc3l zc)Fw?Nz)7&t4#^`_{3*P^XNLrAGDR=KzK>_F+wP$E=z$6fy)NXmS_+Wuvukkv+8JK zc3hDn0%{Vg6%ddjK#G8Z-m}u1xpi&$TjG5YIYv$a6ZOOB;fxAy{vomfS-ot3_*bU$?BO1zzv)f2%{$U6$1eZ9DJ1A{xv58u3vjQv?}okra*La& zzf7bR{S(e=XisWut-{~!52ouD_Iess{n+w)y4=Xh!<8|)ANxqFCLwe|`7dzEND2+O zO~fw7bx>9!q%=`J19At@#v`1Nv>8w9;i+}`ib>zXZQw0}BcK$}aKhaUpHiG0Q)$CP zy^*?Pfk8vZJ-jefZj;AO(wUgPz-c&{dQG<0+193Xx|bSsf8lXvxO3q=I=ytH+}?Yw{Na!kaYBBtK6*yY7&Ae^uW z$DaGlFz-)1vVbis3kvOg_`jsC9%Kv2v!q_)J_u7na0l=C)>*|#>7_hT%I+kzE=*!1 zxgyD#2t&B|^vI>v41cxPaBlB<#m1$)EN0@Fh_k}0_s_YE4iJ!>zUh(wqekHK}nKlX5b$3+01h~4gqCSyYl$Hu~KhR zNK+mwx5hs$C^3rIViAKfPmjJYKGJ2y^hl&J0?Qz^Wej5u4wdmcAfZC*)2J+4S;;k5 z?1!x(6%ICQphy4%{V%HyBz1hg&`;GrP9yh*aIaJ{BhUW#_Q{N;vqLjDgzKIX zQ;$c+_2sAfFN1KQ#7xV8TczGu?`P&ODQ<}#K}tqDmyc69Na5tep)6~}+G}PR}rj`B`J8)23geu*6JOT_^ zcK-}n6fk7DgCXn2f3|PB`|V}B+sj9?Cnd<`CB%x8)JIk}!F5ng0N53`Vgb4;);DbX z{PS^JIXq28u(D{C_Yc`tJpZoolC5|+!;+eZ;jjzbt~M&%rLQ1%ZfIZaVe<3Is_Spj z)cS;JB1QDf=3Uauv!YqcJ+7H1+9&kfvP_uZTQr~~2U*KOawxlk#%A zJY)V`JUsQrM_D7g6MgGtQcmHu9c|^_uM>wfSD0Nkb{^F6k0KRot)$<%6CY}Dq};{Z z{;x(~Y#Ki*(L2o0+WYl6M*~jG76(6!t$(Ldj#gY5L{WNC%y{TSTyRh)d$xYgSIRWfbLwQHFkMNVU zB3I%M0_wYM>@l!;eoHcxC79bu_=Yu$pPP>uglJ*X5N2{?B%oaJ`()+P$^5Ul&GA`I zu9to}f?w+X3>OPSeB)9RQtJAWc3NM1FZm#DbhCnv;0f3CLxSw=SlE(>nN9Q3uyFi; z`^y(?W2{c3F;Dtv=jB!$+wAw~n0g*9ut_6i@3W@HKR>A61v2!QIrvw7QPLx{Rd2gz zCq3UVumOBA%3PG1X+AI2D;iUsgKOmbh|}Rte5xT~=QXoFO#96ic?@v>@Gi`~ri$OM zs5QV#qR2`*;BuKzt8Eci3uZ_M#6cu&x04O{+dw@!0s3|a0>-|MijDjW3y9Xl( zcx1GoxARJD5hK9Ej%8)DHPh73nK@eOt(nTqT_|_eyXx1TimlKI`WPiK?7o^|;jfk( z7QVOK?GP@P5rN1Ljy*f1TfkrV+Va|;HBpl2;;H#dX2N6>$ofRMrpa!xunorKVV}{YSKmIRtc__x|3rX^mPqYX%7m6x-$6ij&IU(PnQ6X3}-OzZ&5@Xy^@PFHhCs zkF57jKO(0l%#)@i|L%Z=^E1>EOe;q~H>zM=s~0vX0z(XJTH=Yu{nOXMlXC67+)km> zqO9g!00DHEwu$Qd8sYr;xNb^5W*f^Pzz}-CP~)?=eBf&Oo{W0(xaw3$YW-hz2P-@S zY$kqveaY5}8K_?N4)5Y&A9W}C{#fZ2z5XB~v*2Z8PH+Lt@Z6_tZn5i`zI*wO@D3)< zsQ`cXdrBa?v_I)YbDYn1a<8DCW_&iN;($xCYe4gpn8AV`p>31rOnQjyv3_j3vWv)G zv(kM$d}=3VC+WW&(W?BLkC=_`Q~p^#MNZvAQ^m3>XnnIS9j6XbLE)aa;bx-%dySJwY|Aw;M8k5RG>QC(J}%$Bp+7uR*eZSx|6@nJKL&nL?ydHDuM%9^}UtdEVaQ&>O7_|KN3eyiOQbJ$K0 zNNERqH1o+Qt$^j={8)aj$%yPfJ)rqXK(QhfKrlV8ud`L|LUfszQbXayAcnuT-oT#g zd=K1Hd#pKMicsKz%iwi7AGvYsqr;PbZy_E5JymU#mHiS*0frL?YEKAuQr%wMx8TkH z_urd-V&@}UAF-sHe~@MYR8%9L+A4c8zq&_}IIIYk9u=d@rRsf-29w2Z)>ok~l0Q4D zuJQ8h=k_LQFbR&g{U47k9~cvjRnpW7E0Ftc_}xf6(;M_BpkvEo$iPeM(N5depJ}qf z1lBnFQ6_NMhA<4;7zPgZd7k9Fb61e(hOA`H%?6~MEqVlBgVu)8^;py$yx3x-)GM%l z$&cF3@}N2-l3K4kRF6nX3XUE~{_e1)qtg+-fk02f)nPQpnYr{QpB?O2zxz1b5<#vb z7rz6sQrp)TiYOnV-jMz~P!FyKhy~lgiHhDZ9e0>-{w$P9;bS1&iSQF>9aM(O0w+lz z^zuEQgaC_HOfRm@xKPZ#?s^qk6}*kWfeZvv9;V$$c%7GETR5u;r4%0=ZlBu*^GALC#QRa0|DfG_goa}6imzR7AzA|Z&T3ir`J z0oG9*^LX4XE{^d!+>>_hO=4(BR^`M-{guv(!=6!&zfZ`V4994$XE_%m8Wv72IR9~^ z!}@LZRIO(L!|$b!n3mf+^xL%h)aQQhK!$$Ef$_^>;hLl_#&1zltjMA>d9d?n4{_|HJ19`OK40ji#DouKiALu38tQ~fqQsNE`NbS)~|IIs! zv`w(s8*x4sA{CNm_0v@sy1`hQV&EvlN8R9Msn^2i@{|O>N4s1kKhky%{9&u*vkn_o00V&yy81UcW&YCr;mWbQRPk zi_Rm1UMXTLj00!|_oRAfqaTh4gIMOmsmGs3pAT0UO$3__xLLKl&dz8FI$o1i!0)_s z6`O||=?(bQ;|HSp`kxO8a2Yc^G*6O3>mp ze|eO`4RjX9<9?Yv3`a@TWe(#2=p%ibu)*tQ8!gUlA^3n*Wc8P?tHl9uj37IOV$)?S z0`iy}a01st>X+My5B0t!;S(J@p`l+C0dBI5lj{ZaVVg|LYEm8%q2g1GS3!&AnPJsV z*he&^*h2xB04d4;J3o8l45@|4sjVJZ_}-Dg0^9;Ktw{{Ris9wkPnf~by0(H8FlHUz zG=9?UT%&_o=$@kcbnSk+!JVq{{Wg&a&R_g}EE8*Ks&-9VdriJtOw0W|*2gQf6T0X0Va-!6wJ zF>6eI^fnPoipPwxsKQ~2mC%;68IF9y-@f#aS%X^F{dGdfEG$81$G zDMsg42NqcSy{r4i%6Hs(<9NV+!Rok6;5#r5c^)oBW@!g5wqYwAL7ue@a@wNbK^i$2 zSYh;Hj2U543be_Ac(*uCO@eKLxBYuPk6=`kWB@h7*fv(Wub}d39dS-i@z1Zu4&bobS5Y@&fd7NfxcV15PK* z_0G_|u9M;=HhFdnd>)eYBI?h>XC3q$%)dT!@&}F&jOH7Br9ekI7yjVU#)GD&eU^iP zL64JutM=!N+nmMvd(=p9Nbim|mS5)!k-KL0t#aqa-&zH@X`q=lwBG3vVK5fokU*^s-2Abyy5Wq zzv7Sm4Jh^`n5T$GY7f{4rQQ;bv8uXkP#k=X{*45WtMw04%L#|fD()VZ-lBm+qTotTqs7G+^*Veouu}HZW!O9O;bS+$CgReB5l>Ol*`fUTAE7*5Wyad{hTV^Y zEmD5w+7PZlo`u9Mc${oktMZ<$!`xN?-Vz6K?bm^&-}d(@;sCDw*;+@T!j=wJ&LYLX zr78J}Bq))ztrW+vdRHhOx*-wN9)41q*%Wcj>HBAGqoj_8pv91KlznPze~W_|GN!cD zd2FLJUiJB=vpJ>c@GI}`x<1UjJZ9;sS;AN5sDCdAzTB};VN=Z9 zCO+I!cAKFKd>X^bb!&anu6mmOxpm*b`HnusZq+2UwWGKzs11md;+Ee7&*_g>yHfA_ z{$AMSCHEryy9*AUKW=vZwpa0ap8@NTInkBd(oF8K@vVtt=mT&5-SMjb-Yo86yAl^T zZmWoBIFI+~HPCpJcYh|K)8k3g8#pA{QmE5GM2>J0ICJWKb@(coY{-?E>n~i)%jHt$fdZ#P{p<-|4ihvL9oLBv@4B z$WbQT@;i)?DU{Wi`8#tDSC?lX(mbN~ZK!w|q`BJyo3&2_>>x>*y09b`F0ag=NA`5I z!UMgS9Z~SAw2e>PA!YYjvQEqjPrh0`IKi!a!pl#WUB#_vj*Zc>?O_Zo!hBMked1XTnm>a@w8yR0J&Xt82;iTjpRL8 zJUW}?R8`M4ALAsSSc<4x56c*1bOyX?3}~_v``uEq9;7=OnDOlXl~;>q5(+RVieG7c-u#Ep?DlQ+|i3=Z7&*SPN0*XuoKOC_ZetE@yD&A=x9 z(0f73G{y&xry93YZghs0aFXM^c-&E7y!(3-GBQDT`d3g|&hpZhvBU%0TueRgF>c;8 zcy_Myf#VrQI9+e_Zd26aEaPz5TT%?LO~l?X;TT@G9}p z8@NeA^A+3X*XlsRe1#oEJ2|OHmhklPp=YEu`E4LDp!W;DRjm4Z6LwGdi{A(Euh@lF z_HX@9w;2%YntdN&5H_cU>Ni+=qrCx4H{|$Yw2AxXkVgn>zJOw5IXzK!`0M2$Xt-3@ zqtwacNJ*1?yd^Rt4!1Prf$B<%3DKX}kg$Nry14veP*Z5H&s68(d=q`_JvL4=vE0<} zF0;R??0z6$;V!o%3NY=>80Z(gnt-%96M&{Oy`8qp3!t|iu`(m~U!2v|aaGUI- z=n~MQTm#rsyw+v>9sCOmt`;Yc8K|n8@U5W6MZ2B718(b)x%LwGzFIbGndtgngEGNu zu?4xv;qS%N>#F^#SG=tp%5_^njk$pP@+-r=8U7-|l+MF_k|xrg1Xu9i6r7t~)4 zb3-GFj=VvKqK31|z3t&*L;=lNe(|`W&DMr*YZ5a)Mp;apZP+4))()4a>y|dTYk?X} z5%WyG4ePX!obLVlqp5j+jmy}7rJA?OG0o$>7xpfo0vLYucw*E4eE*oPgZ<{4A-N=U z*;k%DOPs{D4GAz?6$#2(w;bq`PzH^0kK_MHXAqk|_(r0V3c1HedbPdd=!p&qRlI3bw);Jk1J(R+ZNc4|MP_K{-DG*Vyo~wHT`inNTZtlOc!>y6kj$H>8+a=A8y^Gu z+@@fVLATq%-w@v^2FCQ_?%IDuBchmi^giUSjMXh~@R;t;=MtPscg{V{5IGNhQ$Bs; z;jNp-b}|qLH_{`p31H-v(X&a98F~UsglpN=t5@R&2lb$_N_0$>-`?Y;y5Z+q-^+@% z-Y&XfndtDP4IRaR0fE9xUG=Mgg|DnCor_G4fc1f8(^{@!#RkO*6>KyRI@ zXcN2{WIFx5$-EU@_6pL}&p^Js@sg+6w7RjWe=;Rg!|8_!2O~3&CsN}k#>eoyPt~HO zW6#h%Un0k>lMmGnd9-uKKSgm`pL9OC>15Z91YdCBCfPq-;eWMN*6% z;dMENI7|YW>uu66^OZ$}E%mBV<6{$;pD;zdHuf?@^yE@)$~MJrWi3GsGLg-UxkWW6 zOoVP!B~Disu-jJ!9qt~m)jpx0s5mP3%ggpF_XlZld2CC$`H;S+ zHo4FH2*_P6p%*anK+alm33IrKJZOh}Mh`D_9pj|up~9S&Cs0~_0n7s0r!U?WI-H#+ z^`_fRJ%SdBO5IM=fwRwwYp*O%#1{q<)X2PfDyid8Wem7zJ0LViOqsg31PBdH^tVKi z7@_=aRD^s!m^H6o49CFoG3V->k?u+Rqaddl^sAR6j2z})1;2x~6k8h{b;DheQ zgf{nN#<_&`T9yR&GqN2>w7;46pXN(G(V(M%mR3Vs;e}%AasaUR2$yFha|M|1)<{N6 z*|=q}$Ey)8NTCMy*)DIlF*~htZ_4t}YxZ;M z-sMc78<4R3=E#u;U!fg0Ie}Y$oWyo)EG}R_i!{*2F*U zHQra*+j);*_G~iT2#K4pUS94Bh>3Ifg6qC&NJ^jd+u!H;&G$j23G)x}I~}}6Gflne z7j4uKWWZ}Q2GX!WapC1&0pPQk=$cl{^eB766m~WodVpdjj+@K3te`yZo&z3^9qHw6 zZuEgqf^@#%?)xnkauB$oq(|Zstd5cX($7o*Zt0wV+|vFLZ~EEe1HmhjxD?0WHmA@K^JAdMwp9giFI5;9jeExxp9fJ|8L$#q91fa5I%2bAK{* z_0p1rwDmNB{*Q=Jq?Q!gRHe7HQ+Q!|e#@2q`E$HW%W0+;s24kBbFwzd<8 zrLwXoOIQXX*|U0@Puns1Xd%-gFxUi(K^u#70*1)iC_Oz6))MzXQ*NKw%qaNAgx_9E&Bc2 zV-C*FdU|NpCqzS>M08jnb33pbTsh)P{YUWQY{O(92I6+L+3O|cK>UrKDJm=Ca4}R6n zSA3XTuF7X7rFW}lkS!OO(vbJT6K+6&_uV@P0}4~l;a6n|5VD&>{(Do0?hUs%Xhy^! zlj7zI(R$T=qaR7TlNFZIr;_8s63kD7wtVg}=^xxfBWpu9ju1{sS&HnO|HIvTMm3r5 z`{JmhD3(Zfh2=UlLR3?14;bhV-`1#W1Umx3Ie>aE=Z?abrSsa2Ny zwq|WxoE`-HCBG+2C7CZBs>+s0f#|&%r;i%M*16l9J>9sKh0GvEnuj*B{xA90x0H@8_w?ZmGPP8H3;`cXyA1 zL>Z4|V|O3-8n65TeTjdDPgb@8jmmMkWQyUEUM!lmy3hbbxHzS9UZ@lb)^ZQOzA;M= zG6Lk)Ka`VsL@+D2U7MAI_Wi_uqYb5NIyin3nsh7P8-P{EjtM;5P^n}XQhDY&N*=Gp zhbeRkT_+dr05KfJ4&*LDPWKzo{mX%pwP=eVxdWEm*dOw(NH?B!!}D~}Zr|9-@Ezg% zS2!ZpnWj_*$egz?iFGBFs0$0X8K#2ZdBkH}Ag9Pg$ebswx^SG+<*HALk1XY1* zJ1S#BQL5vGw-Y)t?||TeT9aBxj!G+ZIO@;=V73(DruZQKS;Wr+buA8GPYu?;+ooBT z?454(il<^-8khY%P%VJ(5q(yzKbv8AKgMV)Mx&D%t?DVSZc1};n?_TV4c#yA3JLfw zbJZgeYXNL<{tzm#xL-Te>&ikn>1E5Wk=y&rXY5K_G9p5cWgB%OiF{NdQC(_57$dJo z>R8U%hp!?b=42zAsctL8h(_umVDXCSLs}C$V}cW;dehpQCFEuV6?d?GC%BC_4|oVA z-0(~5kM&#>90#`(kP@R1+rm8ak!7?nbxcxJw{IuFQIF)g^)kaqiEyQGkb0SLX$KK3 z=7A{^{z}7>UVPlDGimh5%OZU!NBDHsNig;qQ$tDuBC^mu)GgH0FAumhopzsureBLx zA)hD%ygN6$R@0n&Hw+ zXWl^V#ToLjjtj=+b#(3K0HO9^xm$(cX={JFj(dOR{(8*|U>P6c@MwyvZvI4})iqb4 zPS0@psraZWumQJMw*Pi4_4T%Y%n8}+zV^&V2*3&+i0b7MLnhvRmpM6Pfvr)gZd#FpS*Dvso8IF1S-xb^4*MB)`;HcH3+oy&c$CJ{ZmmC*vAajI>qbg zppP!_?dN)g8eBw5#M+n+XlAG;pn?H(PFwm$dI0z!AH_`47C5LB|HH!Tw|J;^V$bJ* zGMa1=p(4ggUkN@BHQSe99ExB~0~Up-pOY5*KEyonx}%p_;%%2>ZAx*49d^Jp7l!am zM=J17C$~m;eD24*A4#>?ZS1cN{(ox6&d+})&mU2pM+k#O^}WqzLLJeM5(;6zlmq|4 zVj)qAToaJo38k$ngi5!7$pzgJCw3ys6GJpm>BRblcrhArvssq+bMY%KQ+6yQJc!7H z!~Aeymf`0cv2A)K;vp_xN_1nV#6SB^&NSU#W-IMQt7));*CJ1bC7H|aKRI`Rc_W%{Mqoa>i*B&3)Qd zes)KcbEc&I-!7yU?}|Z-i4a-J0^5p}3|!2bTvhu5WGFl5y?TN0wg~}Ba23KEvXXQY zHUe}ONEPTL7ZHzAKm`{P+lwswoRRvW$iq-_EZF@9NUFuEk03p%!r>+K`fi$iIWG9j zsvBxcUF@&Vzv?8GOtJfEh+vO|=UC^=-QEt)S2A^EQ(6aZVe>O6wHU@YU)~Un3;W^yXDJfE>X8bx zH&g**bm&yqR8Aev<_4v!Y&K?aFt_+9xr4ZE4Wp1jh7q=abZd*ZT}4Pc0Y0V%57$I0 zhWe{Y9um)shETx~uu&+2f9dWdS~Eo31o|eqBT(f%rcT0>V<@Q>Fl9)4oVVSweLQFh zB^zF!;W=mhxOi_=wcb|1cH{X~TNs{uD*gm>LH1eftz6_W=yq-aE(PZ+Jy;#v?d4Zj z%rDQB#+4a3QB17nlRJ2VUhb>&rd?*yPKVEz?RwQms;4;F47L`=D#)TBu9&!)!pL+U4((U1or~;W4#u<<%Eufh>#sr$fa2T@Y*6fl31Om_w-` zWz~4fkH&qMIW9vuf=gzh>L0%VP0UG2IdHQT){3GaUkl0{chlsV&xwJBFjwm761o+mr456cEHD6d-v+qv&aUEbrs%eQGL?ZBUsFFV%u}@?>zo4 z)B6YEB^;umMvcD9JZ=XL4n}0iFhZhjKL>tLYQN+WbLFDj|Bb76N%uft0JQO>HhlQ> zM1$qrmc2Um1Fvhqk^5m-@@RJsPsR0t!eNY5?!}L($DWV2YZ@FLowyE6as0sd=t}&& zh&l%odywaqzsvNW|80X*8A#GtN=Z5*8Ne(FmG)vJhYbJgF7Alu4vUq+Vc&ilHl`u^ zL2*0$*ntzo$$YTu`}xizOU54gJ-?tPE1#cKP8fR_yGcP`?x9+#3>a1<0$}}`J7>x7 zlA9=@53vrc2OU#MkSEZIo>;LVjcfo8o|M(&#HEpPa4Y{lmv4Z({{4-qGPD#3BNI<_ z%!Yz@f)?j?n6!6u4akt2rA?_M1PSsZ=l@@!}^9 zxS*OiNo6iY!=K1XjnE=+hNo+sLALuOm3#PI#{TZA43P)g$q>5qp|Z@s`PCja@EyRl z9O#T=&#k@1AMb~($ln7s``WSF<|yAH+<8X@cAlu(_kL`|^zKh?Bi%o4>i9Q` z9a?|?5|Qw?>8Q2Xp{Zxzzs(tZf@?RiFK5r8GrRgux`NoTEBB{o?^}18sf{xKFT{>* zz-1hSiM`%G1xad{=j<`_t zEFRo-y5y>bj^VzzxGq7{s$lPZ~nVyBoAbwU>5lOT%?$-rIC|;6F*az8R#)!PQ1`^m5 zNHV1+_-#V1r?(a)e+rqnB;VDk*8Q`0^Owc( z8Sw7^vW)omxjGO^`;STvM?8CQEu`)J8`$R^=urKSKn*@oZ>L{ROc5c&x!VSEpTd+b zmwpP18~vZ%?rFpR|Ej~n6tz}$oHmc!{0cM%Ywg~qeJSHm$}e}oN(fXPdp#5{h0pCe z@tfhr3t4|%bo{q>d;g@5?C3}SMhgE4rc1a4+LEt zn3taiO!1A~q!hwVX%B$!@8^xrCO7A&Tnm^yt z($1g+hiPYp>820o-;|vp-Q{6cwuTTPH0Ha^SG4{A`q!V}JMf2rw%TLqJGkf^T;Pb3 zgwP=)WibExX_vo#c%02ao}Qre`ctU*n#tK_OXWY;a~$KYt;Uy>af8p8_&TfbPCpeV zG6IH}IWJISwmkY=njjU3JoFJhcz|I`Q9Z~Te@beQ#)N_E<5k2qeCR1Aj5T=_#bF#0wl|S(=Hn-}((4Lc?Q8Ph+(XG#;rg%S-~mCbUjLL@ za)zNoBH|*~$?YnvdJxEydgPprQt#~yI|n?h3P?=DpqYrPb7{>T9 z$r00vAaZ@-+g_D~gkJ@&#fwnhg%T z6{bnnNwV_EqFpf$5*-e_(b@hl^>K<`eN|E2o-E6zqC6ty9h7w}%?`Kgcq7M;O5f?|jl+OD2*)mkc8b#k_I#3&IoL6H)$u-Hqm?f+S ztnQfWhye>J`IyYz!bT36gsIQ1tR_q};Z;|FEX+Ew!Z4`kYS~Tg^!67Rx&5W%2L+#Q zOIgL!jlcu5YI{jX)u#H|uaRyw$`leSpvGdi4FPlC4q)gDPuqk}K{L3Ej++5e68K9t zL`K_SJw@50*_Q{7)@vTpQPZ?1D0BZ<{LHI#ZF5cv%3(y?L+{_^Zs(PE`aN|?zY-JO zj;ZWCQ{s_N^NFlXZJX3f`gpN9kcK}j$smSf5{O#}ilDUZ>o1B z>`u(twQ5~AfDQQMkYwoP9S{PP62=#XU{p=H3|vS%@+(G6~GFMG0K|zy%RWX zF7$Y++WEc3)=@bZFTh}iQ8hqi20arMy1M1ulP6blfJ}x9Es-@ybvm`=@sIvl(Uix) zHDF`3C1)XZ2(t;P5mt2gp#zy6)9f0mttCLmU;Ogu=>V_}(%`7KkP^X7Z9UywbSRVa z%}ba`FYz)N=n};zrKY#+T_YJAFl}yJE48^DJ?Ee4W98- z@cTrSZrNonbswgz?`4<1FMU@~AbnBDIV1A#I@le`(R|-o5c<)p;&Pdqem_Z9-_D%d ztMydMM1|KE5@{+yxP4T_C*@OP0wEOjavuz0?E%S@-r= z<8` zr!9JR0O1^u2ZF9iKcEVqVUD_GJ)HCf*ePwEU&8FfsdAZVCf78m^GmedCiD1{p@B;s zV#S5ClHxLG3iN@h0>r$Uuu-ZSaYw4Lrckn_u3Pa;uh;~|0(V}tv$y%|ssUh|r=jA3 zgoVvB;r?1+UB5f55_)k)p7xPmG2_|y0I_`3VP=tM=TEO}K6-Kg4%`SoBnVO;C|~T% zs}tSFcUr~NNA{N(W-|8pG#@>A-b`sKi9WC6Dt~+u>pYXL&(ke%doTw zc4wOJ%#3@PUD3V_S!;8?%VVR{T86vwM^bUv4_-H{I^@9#8)iT2D|0Gzq!@ebOWNVb zn47nD6Dk{VT%0M6eWLoxfY1F@e=;DGeCkLe!9ynM8s}vNdZKtJF9_HnuZ;>r32)#H zs&mD%01F`gRgm<6KcN7vEbu2Xe-+x#D;J~bmG~zWA<3fjCRv-@C&E7kT;ndA8FoWq1K&i7gnbK~%KXU|Wf zOQlBq8tB!K8{Kzn{<+J*QMPLF&vQ?io{#FxP1od6_6#R8=VSp@e0pQcr8Da=nfX8l zq6AA8xW?%?08S`M#1eo82AmVjI{Lh_aAg=HX+(u%;9>+s7H$yT8USoU`^lcxN@5~e zfpK(Jds~e_B`c5%zvFK-bPb?nYFD*hYp<{G|D&N7uKDx?KRUgeID$JGj z>Mc$EYJp+E<>lGH=bpw``^|iVKLh%FVdJW8M+FuO0`oWGkc&%tjpAv#}3}fM92OQNasa zAi~5cp?lKv$E>i5ux`qz*Ni3nl7BOE%W?rhu9E6v1XKeT?aT_WCpm5b>Te5z)U&Ar zG)?>3?4UJyx9P!jszYfI&6?<)==U5I;Axo)-=g^oLlZG%c0VR6%)!S2&{&69ZM4I# zz_$t;r4Qhr<}>!+2%VE3yhqO-NU%Kg*6EDGu?LS%%H58+>AO^E(c@y1qqqIfy~FP# z1B`6!RJW%@PDM?(U7zn(HX`iP>HrirG@!8Au2I-_(K_RQ1ES}@10u%YLI&VD){5dF zOK#Z0D1tMpT`9;C-4OzMo^%7@_X!P~8>@%Jmj{>U6 zh9bG0VVdkAnE#ZqyJ;BF9QgQsC{j-NVJLHG#d3^XgG!GAQ0k|!xo$p68SmOBwZt{D z;Txpt;}DbgG2=IEkrL2n<_WoH4*)PpG2Wya3OmjabPx8V1PtW!ofy5>d}lRC)N09p&OfWHjb?EEN`ZyC2Q|KJ$m@=s^GP?FF>+6a`qJc-!Fe4!`cQyR-DOc5Pp}ly zr8YNP1shOuQ~S^EUw?w7<5r;}2jFc`fqs4UCO(zW*{D>wyi^;% z_u(WwlL5zwFF>vQh0FMC6hu;f$OpY; zUg`lMUjaZXBjU*dm$R~yXci`Oq88J}jGurb;nKGgDg*^7M_c_YrWL+*!MGl1|H=w1 z=g)E<_M{V32N*VDyJv5g=c5U#&{&&uN0=7*jzwA=!g08`kB}T5CL)0vW0F3xz1e_I z%nbH};GlvVsTDmDM%*F*(M6V}zq}wgAsC7lmWU3$t&32Bc%o12ri2M4snL5|evdLm zR0mLtm+qF0x0i)9nb6qzq8c8;GugoUenDpDzEY!r%C|k0ZmfX}1kJVGjay(Bd}{1e ze$`YW%fUHQCH>QCfxB#%rnb)3^tLXh8Py@t<{e0GME~q7&nn(G6wggPaBk)4*lEuZ zYP!3f+&`cu0%nYlHJo$q9Bd8C!B=g`BgrGfAZB26E6|s@d=}`oz=^D%V zK;hTcE7KyW8H5519&e!Uf2!MOH1-1mEd&k{%2n7HZ3@+EEcBZTRz;<$KxJ6bw zd;5!Tt1kA1)4^ckoQW?>TM?9}s+02{DWFP|5qx4_6|i0scTxnN^RI-Ly7;=}_K59T z5p1N^3kij>1G|VnBdi~0b%kH`-RV7Bw3*~CNCaZS8A`8!JLL`aJTlehsFkrRFpI;b z{~82~WeIK1T}5tum(q%% zPw7#)dPZ&BAgw^e^#(&P>+GO;9n7jvA&&dW6_IIa!UF;_45h3#Z397YnsgUFPD1@v z(ttDvExso=;+-N;|0CFlkM|N(rW>T2g?jPzu+%)C?=o{^lW`a)K}05F(+G69*|Q4& zi_o6!2y0i(52vRNwRQMCeU{bgV*#2yb>N7u!yy7v`c3H$C|qbHVhEC`7gl=a6M;~M zI%=M^ilw*xX|tWXJ*K0Lxt*~k6#m162-JHCimcJ#4CKCv_hSFy|4D(>M3S@CeFF6{()zJm?X(-CUVKQfsX2Tnegv2JHPAHK))n!74NgL7ZUlN9MWjgSzpobg5gx8VJJr1Mopl@^X$NA;2yH22UVJm zh*bpR)RO5K_PA!faNtJFj;t_Ps)^V$LdA(WIF(A3GoQ7_opy}rrMft0m%pXFvLERz z%~el4e*gSs2U_sll(EfBMUI|Re)^5eCBAaS9#u~UB@FE%mB_vWY)^K;l=u1QKm#B) zOou&iDu_xC^Knk28SX{Z6p>bc5fkI3a^3=_C>I(Q#Vu4J zIHPvV({FR#!c*>&Lc$K5$jW9x7I%FJp%|vE)g-h`?nz!=UCpp{6<)tvj*DRXq+2JE zXF(-<8jy3__ed&1ZsUFBQ<5(+R0wY)EpJLU5dgAJJ_?pWtb?_}5|Ea_y52FitqGHf zv>pd?jQ7N6dzU9vSwaoAx|UemO898nn(&OF`q1z8*PovWq{%au39Umb`T3lH8nqq1 z)>ajFr*d^v=dPTzD?MLF@p%kiJ*Al9kzU!Q=Hjlh#|n=U#dJKgz80iJ9hAP!5=G>} zT+Fcn!g^gb*L2_xVC|ZzoqkzJ^C;WzH6;29EM$~FVw7!;@DcB3MJtj#8`nUgqNp{k z6}a9YO>twk5K9=wV|HjPzzLlMvt*v^P2}F=0a7W=mXv25mj)NXZoY2AH5Z0R*w){?HTJnV+2o#VHn7BLhD)t$;UoZ zsv~`i(OMuFieZvshVe{{ZXTPNG_hH_Rjk%84vj07zF}Aim`Q*`p>amFDI%Vr_PKEs zp7^nM9yD6!1N5R#C%@{uM+IdKuw0+MM5|>FG>K}`i&S={NG_2V^inwVtLQQ6Uyi$^UC5Lb94>pw1LB|?kTjCS9*}>(Ls6OUc z=T4zles-;{PeLT7Cg&eJgV~=fjTTp>az<1_FF^wgiGJ--6k9_)q#FK5%-9BzqkzOl zug9Hg`(_G(1M?mj3@vc!J0@^9*eJy^;l3Wgszk#+yugGsUC(UFY{3awan)m=O=ozVMHt18@`@#rm98+yJT)^@fz^>_WV&XB3Qpuf#UmasJVq)OZ`M{B+V`V22^K% zBL?>*`^!QPR(&B^K5Sl>PhDoJL3!MW`H9tVv990-i~jDbi1>C%3j?ZKJcz?wO2_=c z*6YYUgO?(Z8bCi212ZN@K=pbl5h`C+1`~&yK+}A>nqfen`@|*6;dg^feo(m8Ti64v zC#`X=?dtHk2fP;aON_>^(Afx~Cks#v$SN5^pJ5(4zN1aD)^uy{8oHKY%T@)($lWbv z%sW~^Lo}bD3m+-YeXJm>7zlg+z3%Bxva0G4_sUK;Jw9mC#(Q1^)Mp#Nd_tRv26fZC zE*{-QwR6MTD;mUDr9Mk_ulVQ|=%oB4`Yd)JFa^vZY6m(3LIZW}5U`)!GWTa3w_PU- zujo=vVV_b(=QdY#kh=jom?+52S+&9vx1la{Tk;#@nIT8W83cVv{ho>YL$ULAORSNJ^6I!yUeogAhl3-tB}qGqhnIC`lnm<2#^mI z7|NgXOGh1hQh@kX7AHe6!I|e4>UcgK6x(vg;3?NVXjGhr<6vEyMvLW{!8#3^4jU{H z%Gx=%=piw6*PF`dU&UlK#_mvfEt!4PlB>r?06k|Qr5ZdB`ml}LfFO#V$X58}0kDWi zu+gJH|G)N4t$@|G(%4=GZ5Mv!HKx*SyrU-;?vi%nLXZ<>{}JTx0^qTTy+=g@2M|#4y{-tD zbp$iCD}c75tUoD;Y+Kgv!(nr<=Xb}`;!85=iP>C(UJ>N?LMt`rVw9DgdyQHg($6N+ zZTjn?`$~F9f4We)3^QCT6p{H6>!ojqPjB36j$IAI@9ih-2A{g^a?ed>UGPm*2x?TX zT@_V7+^QuDf^cn5J7Sc|kP86{zm_am+9>F)7Mf8!PXUVc!S%FInZ#aDnz@I=@Pasr zLA=;gyLV(1OXG>HHsDh_8!HMNn8Qc$kD@u2sEt6jYLK|@Vl$Dqpx_Txvh%;o++qeZ zNA-B5HuCzgB1>WJlaI6Q{)XsvknK+-k0C^9?l-sQr_ToHpYj)HqBZ*4DYnM1h4p3w zl~r-p(K$A!Lk!)32B&6&)s5xofX@SMs}WV6uV(|Oy{)5OBfT`Bv4K=b+D6NJQ)t{! zhKShG)XVDH>iX_g)5vS0Vr*!CL1@z;wLj_9t+vUtr|tHD%v6ZA88pkObQ9l_6jfvu7;$fKH|=V`tpY(yokBOt6uo1fn^eyCMz=xpsq>J`3a$Y>k4SWP4Q+>$*MZu zNXU6nWxuK~KHMu-j1{gwMcQ%pGib3LPuUAi(n1Lvh1b)5L-QPE=OO@5ETirrS{sUB zG0Z{up8Rbzf&I)+WO6s`hI@nXv$MQ!UCqle+xc5^;pZH{kD1Zsim@L=)%F{)2=Q`}LY|Wg#M!s7cVr zZm zgRH4usSdCJlgIks;G7VLZyy+%mQA`yBXspiU!k;QGl0T?9-r7Q2^5KeE+*p$Af41P z+lU**b|}SPK)^tx=m}KQ83<4}5?A#`32&N)*S z`JxeVXSj>E2{_6Wp=Dk7am9`ygVJ7(lQ+JlBt681GtdcLsXlN})Kk>3=W#Tt^Ig^blU<~D*JR4J9)5-m*T z53?iEiQAUE#c{#hZqLb$WfSq@<5edo$v2srQ7FX>SC2j$gkX#%-BFj^8cscn3CSJR z?MNJo80lycgI@v>`+_W)Im<6?B^ zt7czmiIXDTr_kGuR3|bMjrbkCMehLOxxvuixBueW9vSQ^lsdEr`=f>ay!1G5f7ZP+lCzjTEtc?4< zFzWI`RM)-w5`M{qJ5c4)dFlhw`w@=Ms0D_u&l+RP&nDcZwKofLGw>t7lCD_tVQsz> zJET1m4J7tMcrvCAF|P``rdVjhswwW9yM)mkEL`9Zw@-}9zLdfxzd6+cQApI5V!?a| z`)I6NKT=)npUizE#thjwPlA2GA%a%##cM=$Tu?o1`~uIHxYs&2J&y9~XLv+)NW98ca9zSpb_ zp!Ne%*elc&T6!C&AdcqbYnQW`R{?}Hv$@9b;V}2L@_DN<>XyIsEvkMfo(sSS^b9cD zGh&ag7Y>kNs9!(CPBz|dwk1B+(h>di6q@G}<#Az7z1Lox@L4^DQ447p9=%Y*JNmQ7 z;&xkhgXi&}XMN#IP{1_70Hklrx!FnPn61bS@Qa!J?LEmRgf(R25u_Fl#`9z|Gnonm zBXKZ<=0*UlT;46|7V){dUa7hGm*FV5ir36g!x@NvRxm#$dQObvN^Ch@$gIfVfWWe8 zQFOp`)AOnWurTyPb6Vb%ve3EG$C=P?x>9)Y`liL?ns&Fs!@R@OsnmjIx8+a_y#$Uymdf1Jc!g_9J&i_WA z+Z|2A1-26#D05#yIdhdiu~FL1tov9vxsaGY48#xC=(*iORY!q68}n{Qg#BY+Y8mPe%U|^br%Pj@h_~|um7*MyZ5Gtfc4t)PMOe`D_ zW+yaNCslwQ?__X$PYO~`n48E=E5z_Jq=j76`k7Hi4ws%NH39>cdv7MV7iyxb#>Qyh zQt~^z229LHzMOmeTiQLdhMXOkLNOeO5XIFiTN_fE zr=PDLSF>7V%r$@yfAc2?i#TYJ6Zw(yX6fW}#(pR~8rh!eKs*WBZc% zq+Pg1v^KnNe{ufUnY#W-;UQ+}d%*ULQK{Y_8U>;nu=(Q^*a=m_W?(0Y4_e&^THd?( zbwXJ-Q3(}D1e?STpanMED%~rzBoTD|#cskIq<(JR{8>SlAM}-qi{(z-v_VAgOXD6z z@l;dHS55r+5y_L!29|zb#^&u)FLHl)-jKERcI(FnIiW)p<@T^;2et5v#yY9#6_&<_ zR1Z>ImGuY94txGV*%(hvyiE7)Zgub~pu=tzZ#iX&vo=eQ)-!|(+%IBw@RY5ulD{yc zbI+H}&2&HqX{n--3c?cD0X=I{p_hfN?8~VBY$=#&(ylKHU1`Xmj=lb-vXYra^|6hn zU|J)0o7f!m?%tL<|MkG{yOwD_oEF16qoHDd?Kkym4hKn#od>3}ul1h{u~VU`k6b)@ z(MD3OO|$AOpc`C0=InH!x#d&0#*waZY51{!bk+Fv5?IS>Ypwo*KO$hVdy=fH)A$pq z6AHlE>uBHpaC`t5{3_x1L$y`-%|l;obh*0^J+BD0?%*J{_)klLr51P7>RwChvTw;z zJ+PSz!>7YSEpSU+8OMeeJ_5U&Bq~pGV=^MsVz%W5uj39wE)1hZ(8a=^B3p$0YzDA) zZwJ%nfa|@*0y`!TwoB|VKyP;CF=5yu-DFM^B{2%Fgqa8%27rb-HTwSb&893X*{(Zp zLY*!#w77A#ADy#G(x@pf`(E~^XGfR!Q4`xf7ivDwsLGYe0= zrEUAQWUenVS4Zy2{a1m8mR;EK4rN!}glvlRb(#Ilz_jG5X|7QZtsXsk^*BiOkH(?c zcCtozv%7%b4nRO1=v~0NSB=Zub{1#J{dDU~vx$&AnVHCKiREOyhl-@Lyc_LDey^YZ z8Bzay%&~t-g2XMg$lCUM*VA0t@LhzLm|X;22*nScyCwL>J1-sY1EErzpehdULtW}N z5|J^aE%Xp}p`4ODwo^3I~w|*3!glC3a> z7CgxD(bxtL(^pptj=QTq7+`r&x;;>H_1Raee{8!DrRf?~k|N4}N}kJ)EW|=Sks@~^ zkFN3y?z%}jigRYqrSPn=fMC+ga*i3*aOvn{^u3`ry8 zsO`PFz=T5J6k*q3m_eBHP(h76!(TLV$QBHmx$D6{Eb??UK_{{lXkdNmX@*=h$C^@0 z@hP;fFH5HSE9(L$dDMJOwX}C!Va+0bvM`^NL^#Ca!fqlhBvsc)iQoyLxM;Z#q0)U& z26cv5yD)FU_XQ)*lnDij&^)aix1*1^0T;M>gmqp0Xll!fAJ)2oN_{jvWKArP_JJv) z)C@-wEl3jT;7LW+LwMa)Yba3kOo(VjrK%89)bbK zDdTmyT7rn?r-RL&yocqNbe;~_$dR#Azvmd%k88Jtda!K|u?MIcoUd&x*xy_za*OGlaN=y8;X`M1giyBfn3fC6_ z16ItQ?S(L)cj{YA=-1vOxQ*W{Ng&AKVbE8hEcZMsX7RM=1NS*xLX_f9)eu|ff zsqCfh>Lf2j2kG}?uiRO&=Pj%Yn^0iD{P8wIcxH3(q!zIqsnEZKQijI3JDx{y(Q4D* zEDtr@;pGa@sX={@v)URx9hx$#taH+@k5-elzRQf6$95y!lHN#%7vn_%0?Hl2Zu`|p zX&2HAxOuk-Uf4mQ!c$x#8@5ioL5QBWr9qV<##2a@hgs+S-rMK+sy~3Y3is8(S)>%Q zGSFmmlXhgCgsP=K3dGh&$MaCBHhZ)qV4pu8$_`F{^C_Nkr!r6C!*7tw+U;(o>2b52Vj~8~p>T-Q$Rij~9Ms{dmpo>{fkb}ZX{Y5*K zvctTZ?Z@0^xu1tJcJ;D6OVUf0!Hn0`eYdN8_tFiqqAW+-Y={BF`wJ}DCV{*J|NUU2 ze;R*IZ~$bG1^y{e$!a5ifI#7LroTB#kj0H?pGa%e-ZQMq`*f!S3gUqg?aa{1GV@Ne zZ^XVlUZs|$RG~-SFEmfRtYi9Ul-t!p&4zmKei}ykR@dx6r9(k^FWa4di#1evrzAPY z`T(o9-HmE|+}Ql{TWT^8`^N@+2G_Ldv)=V+I4D(c1xt~*(Q5`nA9Ju)A?Ii9x7yK8 zdbT^%J6PfRC^qP!8s~NgguJH59t$0xVvNjRBC3DA2r$*hW$^0+pQJ7iSAPYval}@0 zFvEMqoUC#OX4VJ1hC=?h)0LQ`eaP&TvBJhE!_!xf%?KDRG@|uhQv2!j#jsGM_}eaV3z70e|R& zkzN%>xrCl?y=;eq8ZR5Be8Omvp1kcyvqfkCkUcxrL`CPgMZi3ixsI>enL&mFaNM`%Rv650c;gHxQZSoR@2Ug3v0zh8E!vyDl zpH;QC2SefH8*BJU+_7Q1u(QGb0~QGA1l@QUriN8d@bFacxMP8sYcaW)=ODRhBSY8* zv;|#-vV7SNayr2X`c?(qXxBb0DJJZmW$p+^T8-iwxy$6lc_|DkY5fM@v1aLv^P-;%x3(-|c;S%QEFiYD6to=T*5otx{5Q#4d<0ZGF z(%~w)f+J!(D32e9LH9Nv-9+bEA=PjiETmTd?tZZPPUr-$}YfY#5h{`B+zSPEY9I9@=> z6eCa>z&QlIaST`4?!WPw@J3?u&)gK7@JZhfM;w8h(8uArhb{Y2O1LA=gG-gOzA&Zf zg=_ok{;aNwFyS@TC7q;Q(($;%_)$Veo!d%(x5_P@*l0q$I5GN_r`kG#)WE{Giw{m} z#W913NwCSQ#OP3Pp0<2TWb7q$O*|cDEvW0UOP{Bmx##Gz$_CQ0e$Z{{sz>91#g?Hj zR%IIn~BiR1s@N?P3i45)c55wck+u!xo8+}iTsiQ1 z#5}+b@>SaxU^tz&E7D%=eL@Ng4kU4jRjGmx$Wp zK!_V9n1p&?`e>P90!5WdYwEWOn=tQf)D)`U% zR90`>+hP}&^yi@gp>NCPbnVO|wdX56X=(%MjQpy9P=fN_ zwq~#W6+payU~D72_22dk3}a@wsw`N>1nknnoP{rQo%pN(mPptO9r)zhS9WkZCZi)! z99%j+(oGCMcyR3XTeaPRO@}MKNRVbT6h@voQ-;ZqXGpc*SvN$ z&Vh_FrLIJW?6I7~odpF!8tuA#9Af7SlEXP4w2@Dy8kM-lDrmFk6{ zY9rbJ)(>yq$1nznLDhD+k~a?Bid-iMe~ML~dR-eTRqdY_Z{(Wnvk-Wsg&%FNjt{z= zcObMOPvK`RR;5K;xUa$Bwaac5Lq+?%K!dk+-tgob+6nIEr6*6QJ`q5CS1Hi&={L6u z2bc8oWW%;Xb@QOqYX)`+W?tzYx&SJye!0j<-c#AdoDWYbz~nUKm$4t2~cgnC3yf86bm&vs|KrBzf0gazno$7Fw(x#92j$;ge>nYrgc2Jjv3 zt-pj0PcAVy=GPFVr{9C)sz9dwta6tCCRk%|8Q?o+{KC!y;i}sWT8P*w|%5|Zr z*%dnUt{KA0G6RpmliChnA_4#LZ`|{ zSi-)(W_>X6RK|s!5sov7s);sQp;0^%9IJgmn2=kf<^?81KYq0^xgPF2EL9h86cF1c z?l9{t+aq8z{<_UV*t`*+ya~v-4;Ix%m%R^Za;lvqCX8=_4wsJM9LBrs8;d$lXR+Y0 zrO+=5i#(|Z^LbP5;o?RdEnvvSB}Z-5`f??3rgq6T=zl!x(6GmF3s7<^7LKfI!kyii3IJX4jmP4yDX^HK(%B!Ac*1 zya8DRd#_y5@Q+dTb#>LKBuwA73&2HX(gH505HRee^pvO%mvWI?g|z>I65`T+Kd!vRrL$N+Umh=X$Zs}A`@T5@}DaY`A@dQkssQ@CBp zsa(GrfP)0as)Z-68rbI-o;c?BG`)N%J;+wksX>K?RkjK4m=HKd-10ZgdKnX(gm)Es zPA;(M+kgyZB%od_aA9p90$V*MVA7-b^^k4Vp<2Q4;6nI zUe9g0@Xb-ksee3^Pg~CT>Pz2@Ln)A@als|z0fA+Ekyn75t=I7QzhdvLscl7R4g>fOs0VKV;b99E2H5d zyx3C_qnO&pH>*e08NKZo>KIsIhYn@#aj!fjzZ~RFB8& zj14+Jw|6LpI=ZvZ2_V$P8K)@jvX7f!NoYRnmOUfEdc~N7M`_?t&_Zqyol5vbNp+BH1y zA#$fbMrz`1fSNKQnXwUa6-=;({1u^SK-JOK5WsHX5fls72<}Fk_Rik9CD@LT+$(3h z_x6pAPOHZi81|psTlM72J^R1aDkq>f#Hq=*Na#f}hzJQ{Mv4XqWRwmG8Ko&w z#1KL#A_5`^L%$HTnT&g$m1biMJG8_7uV9_Zq~thZKISSR`}pEUq1)UH&)jfs zyfd)!(hGZ?>;NcGVqF6R7sT%0TZUv}K0N%PC6qG`;9?mf@azUr;PH1V2vm0ga!Hnl z*$ut#;`u)x{BpJ4t_9>3wol7MBKJZ+VJ9@IyQoRLLGdAlwRg+Mk$=G%sYVXvU(_={ z`*w2rq({m*@XScMU4-7(2tbmYIQKWMCU9|t^=TA0IJ-7NyI#)Q{kuzsFn6@_l`rPz z-}up*n5m_|gEWvqkM8V5q>^|Y(-L-(G7zHV*=_cqTremUDne0U0Cp~;XW~<*b4J^? zMuAkKg%STf$hnss#1;MUgm1fqU;aW49H ztkcH|d6S{`Z6lBC-w!a zLYynZC&&Cw#I-Vc#O)aLpE|K-s0KBk`h{U8qORY*749Agzr(b-1}1&x^+D)@w(%|6 zyB}`ZvSsUUTV}RMok{mOZf|$=?o*rpq89Hw02q&$dXvQ*h8URQ--_2J1oBdP5br8M zoAn#fjEDGPZdxIJ>m*T=v>yyZWLx5lQ_2Z-oaxkW$V`;91)kIIdgl3fiyr5q{nWB( zuVsWo8($n&IxpNeIsfZ)ET$bq1#O3<`E@TzJL8q-Jk626#j8T0j(kQ^{lXH)L$H&k zcE7Ac-EI{G^sY6UOi1c1%6v2W78Wux`uZyGKfZS5F81QP!RsYSMT#fLkLJy6&}$6) zlg!i_T|)q_9sfMi$p!wX>3aFqxapPx=3vM0!wR}btA#bVWEteY1C>SC@C$T!PGM7p z8~vQDEzG*tul_ST*Pw-=6XSj$`8%m%IS`9q+0HuHrU;JXndMHs(|w?+5!>^N8fK>V zzn`0+e56#!BK>M}dt(dLy@zKVD5~BG{oYl}`jc7>O^xYXMQ?SF8U;v4q>F5K-b;|k zkf^Lf5_RGQh|XQ!12uA`mbMB`k`nS6U7IHNhZ%$j_JmY2sSZG*5q5l#S9~6XL_e>; z6H15dyZ(&nrdRwu&A&6R1RvIZ?3spNEB0c*uYV+KtVY{BV(Pw)51CS^u#Q!E?br9V z$(0-k;^Uo&PrV|U-38@N;n_50k^NwHpn^^PQopX-ECOp|a6QNTF0=2rzGV6Nw) zOBvAv$_wR!Fp0t9`o-zpFsfKV;ELJEb?t83-U5MKCj`-QB(m^kM{_QN8Ctxj#_GqA zBE^)>HdTHZ!z3{hp%x;MZfoIlH(s!BDz4|Y?yO$SA9x9q3N5Zr0g)2P%V>dNJPHcq z1Ag1fC7JV$EkncYYw_|Bn}@L6Q8(i4<#=ALL*b_3!*DVGf(MPW)m<^ znbqm(gTg+AowK{WSb_b!UVXK3yI$MVG)!)40(I{J2I6+%jy=X|h9NHOVfq*M|Mpk@ zfdZ`lDNqJq)Xq|D*~-HwvE(9EF$V}zT$3b=Gc!~;;hNClUYOCe41wm*JQy{p8C}Il z4^e+UjJy`Oa`>(pZN;SPXt?fE@gToOcp9+>-*$vh@kw}hgBGs|VhdTf4QrCE5Kxc6 zcx5KDl7~-Z$+lQVRm_6`>znLC1iN_Kw;GuNg~v_RIFpt6qNDuf)Rr@kFRJ1pJ0l8C z7Ug(*2y6T9QdZ2YC}VUS1vY6!_Z;ugq;-m?u`XmGQ$EITVhx zBmO zI;x%rJm*8sT;n>Zsc4Ch=SI$FRh4}D~vKoxa2B-W7^YHyO2fIIjm+bz2xYC$8f59Z-z;o<2)#~FPVH9T5ykk-BP zX?!wo{90!qa;{AOfG#C~na_AAAf^f@1w{aA1-hS!i$SHk2u zjCRs~;_;DqHBe=kqY~;#NtBuClp8`9Xz#c$KWky!N?D+hmu6v7g_|-E9Ekat#r(Yf zv81$r3A3GaS>$|MvFOyuF^L%w4_wU8ajBgHdx<|oEI}jJqF8FV?XdVlUPrT228W{5 zZvOfX2bHjnN)#WS*+o>~Y#?nN5YkW}JIi)U(@4MzDrUq;_R>O`0z>JHqph4X%odhH z##uW|(vBUhv~z&&Fe&Q=ft%f=!-DesJxs+|4+xjfgtyvVZ+t!_QkpJq#hKTBr;1x=9EU7DjnCy*rAPs-^|O_%%Fax&%R>y+r*0 zT39{DF$+sBN{(<2Gt{Pxl^6>i*C7&G9JtjmhvFW8q#!nA9hHu+s%Jf|x>N7P`S4_= zqj!JU1%|g<`o@RrAgnkfK=LA0X!Y8)%axWwbJ|ZXv2kH2_)yRW*<;3!b?aw!-LIca zcX}{OIffe3yQB-O7sm2N_1ro=(Mk4Ex@QY<*&*&U>#xXX3wx&}HWz&*FXPoffd_if zHzC(N$>K0bm4l-CnB1yIDb1@L84j~Thyx%OL7hz&`({j7Vb?%<8;mgUvtS@Oa>H@e z@rd9GU%dV-2Y=|B!BLa_zAxylqg6>)Jf#)|X7M_quE)E65rFPA*Zl;c!nDFA&(p~r z%xMnzU7q=irrkT_Jal~5?;&{-C~@NwB>)i&%qxe473-S3Ys76Htmr0rKI8|dC|X!dSk(YvJ^l(B)s4NCeHNP zF)M|OArjRFe*bhzy}s&2D4-$)wO_2nJUg*53ud|MHiE8uBP}latK=K5bN(t}yOrv_ zEcQx|7nJ11;sdpMXO(;ns~?#K3 zJXC#aJ{aVl2IISzoTqDWzq<@{UVTY2I!b4Rg7ZqxF;K>3>sFkkahp*f-%;o|0)kdj zNC;r4d`{T^oCV$4oYCqC)|OKED;$&V0Tn_cmqj`JY`B4?=0AZETQmTHVRDoaf2y$L z^E2Zj)i?E)S`Kf-W{f%bwc^k~>;d&r(q-y9`m@b%b-|tn%f!%&VA;7*6#83whHT=Jcw&lha@Jl zD2e}qyPS561d7_NVrNL6=e(sqEN-Shf^%EEedE6P5a&f_?P!<>7T^d6poq>1|4MvB z)T4!5b4CijCNegyl#00`*1aX4I4_HkFpnKo=b;v?J{|sCIHmq@Rt%g%8q}MKuq8AVFH= zx3nN%lLRnyi6jy-xEnn^mL|gc4}{Ot*8rbPZ0JBF41IGzZa*WC4l9L>r(v8ckYk|I z?b5+OJ{-b|^$aKQLganJJjX8>DN-;@Rty8B;0nhxK^(-Z1oe|SMrGdL059Ko+o<1{ zx9Oj95lZP)vlNiJmh3Ux^_;o-m>!Alc#A{K)FI5?_jZ*y+1(Rm;O6Yl2U#IyG77n` z9~4Q+Ee7lkuS_j3zff|kB(F!-$sJSxpIeSA$*9C#@=Bi?B3E`)gz)i6&XF8>}Yx#7YM|fudb#nbj%ewi;&{XW5mSc0gZkY+p{8 zpe7w2u{tWg2KDimIhz7|We|N(2vo=c<5p@o)*eW8M5~J;H*$%V(epV^WK7q8xJadY|BGsnK6h(k>~)yVMd8^OniOQ@H~?QZ*bm4 zG%KEAwG*t27pAt2KZ0cW9t-YoqGG3$t+tMU#+Y|zMC=|EWs=Y$b!aUWgy0g`-TB$^ z+lEo~Esi&JC+(u>@Kx-kg^wp6bY3C6?ORZ;$RF?W$d1^J@$Ph^BI}u}#P=uJ-G#Yw zu3ZsTCXF34p#dJ61qQO++3LSNtTfM_3!uSI(43s5bpo(oYp|=)eOnWv%z*vQ;S{fE zk6geWOTyPxf+>f9a~%v#1^~l*-l5XN`T@|>bx&z-kRuVQ#Os6c#|iIwc}p@p*mfwG zyEjWfVx#I1fVMtU?I4VM6My_O{Gd59rG*r&Mg(>>cdxHRQa;l1-?ofg(tZOqGb>5Cr)cZ<{t z=ow|@E8&u_g-RA~3juQ@j&AF1TB36&D1H{7&=hmRKyWf6B~wPv>!9;HlyEnA{4dA8 z4HFxPa3FA1zGa(Ms+h#HjOCEJs$s$tEpLdSJbaqZv+DmOOZ2FnhN49XLPSrzw&3#A z(Xr!EJ6X+wSU*3<0|x%BqXn17SZDNtwM7T@0dk|bKU=%&F+S$NxV}-}pBx3J{3Q6V zbO2~M6`FP{p|eYv4szs9`_eK&4#USKyQwJ{?YD}k&ocrXd^EZmU3LV-1L?XIFwlPO zm@Odl>g#n3-)>rj+GHZEFCZbEpnnapK0k#0hMK9ue9Zvg2;R8&A5v`9f&kw3?=27a zRNr*W^BFXvsLA@eztJ46uRiy4%EdRObt%4sL9F`xF-u?DybDj88Z+D*qkkG(vTW_t zGt|DE>^7+y9~0jX2H}nadjR8Ac6!D+m4!u6QT1z!@1wwOa{xklZ4|JuHCrT|ZTe6k zcaZ1k?-<+VuLUK=fzwHd;sYL$CZWe|Y&q)v!3pB^dI6rj zfRcGH27wz1U%*5V39dyF@lXZfxTxJRerR(j;vi_NVSJuaOYjsNMC@si`)kYj4c=pSGl3ge?={1@4g-dLL7)@(&mf)M z2%MtwJOBv=k;3}M>RfH|R#f;4C|0eM=21#SIb@Qda|TAyYUcuf0j}9vLUe?Xm5{HK zfRz%Zzh9#Fq!v59&jP_k$`UGVn;VooZcboQ#g^4rPsO!gywVogOTg01ix6o&*O-)fLv>XX^Pt&ta>ff9h% zm@$(PN|I~S5!(x%m995tNPZ-Iavh9}GjR^PCR}=ydHN#Kap>Dc-Hwhv+c)1@kv7AN zh9Z2_<1~AH0E@e@->`xER*r6HF2J;~-4wVXuSVb85ZjSsI{CH3z%$rV7wjE1miDW< zV@%CQbo8?$r3;?b$Gy5ORyWPNh_&QsAcE`)`R^PnU0K;)koO_KQQEc*RG~Czs~`_V zn@VV0M?^c~AQ%|F;M~kG!btgJ^GWITn?j;t>lbet90vAIhx%BR1Y@ggvC(?IuOb3( z?*5}<$hWuYTs8b@iBAqe?=_jG5m%%1=VPo*Rrx60NB1jT`xPd%YufeG^m{3ZG*hG) z|3M-xASIJRK-S!DqBjpFFGh~E?VrOOdMr^OYK^y{#Af`-)n{~t!f7@lO=7~=tcl{t z20OKmuF5PKpXc43dc8jTCHLL^zb0W_q_^AJQF6r745A4?rF(foUPypoTI^OB2S#B9 zBj2rIpdhe^-IiEF?hnss-p9^p8#>F0wa226wuERyytqoX*yKV~+ZcNdXPma0; zQ)KVF&7%;Pp_(%Yi29kKV~7V zr<@vY_c;$&>sK8+R!#G&u0B@9aftQxCHx+izH5e};Pz|9Z@U{^(RBM!UH3{YG~L_G z-Z|Rd>1)=?)pSGV8@hdx@>euXx!XYE)fL`F{M`cq(8s@g1h9?YR`DA@J})-}d1bI1 z(pBU;=kpu2Z3_!aDW?GB?Rp)S9SZUeXbCJ&ib6V6E!VR{y0JG#PSmVXhgL%n< zpq>ngHsPkwZ-{7J@mc?o>`{@!|y9Jk!Y-% z5~?h&-;tc`8fT<#e0*F$TQwbF*t^VThm6q@gho1cy@q3YD!#BqgQ&pNG7q=;*Ay>w zrdorA#gnf&R`{voSN-NjnJOSauR(?Tp~Tc?cPK1{;cvur%`wk!oZe=kQ4XN-LmQ=6 zMu;|TW&qn*RwnL$GW4irIIH6>A9I=l@j12XnTtnGN7GoDllotd zD>`@wg_Naq0erp7N2-IAq0ZAvw>lmF26Pj)=`Ch6{Wm!^?VTGXKiCKeWXDtgx-wEaV#o$YPhPtI zuZ?JVY!$eLcp#VXrxQvJ-H=UFaZCTRHe09LB?l3V&cO~bDILEEKooLQomodX&DEHh zz(ikI!)YXRaDJ_&JJFjsHVqbs5Y^TWYrXE+UI&;*Bxd4(8~MLC+J7Ca$!wA1VA6`D zL3kteP{=bd@3f_g^?^>LTTP9%KQ0B_=k&j`kG~Gf62j56!Q-{_XQ{wuX7Z z@40Gp@3VB%4wnWyn^v5WTNBfRVQNn|^I-q75HK0-AeX}C%$l7w^&ZUz8npD;$K|oO z2zvjbo=w!R_YR(o0<C??t;)jZYKZ0&urAs7E6M$f*W%w{gO5yB8T8|?uSj*W zAPrkFsKdD_-8IRWdgE%5u6X+!$qAwf^d4-i%RTXmkSgEvXMuAUEcMi9wc|t916|Yd zAH=7hk6MIv1V^upzri5WTJ&iPh^s4AH(n{bA3PMo)$WQVTmrE2beBqR?oKkZ z+kS|mVvA(=y08rCe|}&kmkeW+eC3n9%!SOPb@J1{U1Nr`53ZR1-f556*5#5LmJa&L zufrSD`Y3L!^P356v!5-sBq*^nXdU`U8#XPLzS&zmUy@W<0Wv`L2aa*pk~7Eoy^^pT zlG)5~`w6kLo`U4W-x2)?u{zWa@)L^==XI$aA?=!D)#okV=ZOY(E|?2z)PQ8-`4GN} z^^C{&xV5+B;Na)x@b_Jt!yyIx`f~RU`#P$A>*w(f?un>?)7_rraxPBaEU3Tt@A|qy z$Jjj{YLPs2(Q!lG@t~@>%p=LV1rLY)o>KfQQ3BiilYUQwhl)-?XDT3Ght(;5GF?!| zv!NZpqfrr15u&_p&k;i z)@N}pl4F9|H8NO#Wj?Z9U!7A)G#fsqF*e$ADvmE)``74W3(ra%Tv!ZbKu`T$t9PKE z9dFP&cC1#8zQ}VzXP+3Rr7}_}_9rUlZD?8a-9}iRq8`i_wxm5m)Ap!YIzd(%eexDJ zy`)=E5sU51H8L=qcM)23m(Eq-VT;p7I8!ofp3oab?>~qpV>_&-MW&M1YKj{Vh^M$4 zdglefneiGRr4`?KD3sgSHITtNaGa#R>Nm&_2-(xE!s%eo%|lFN{67K*sjuSKGaE zW$cb*mNADIpyX4|`NwD|51XFpZx^I^jj5!|uy;t|@8Hxfm5iK_3?iN`FD?k?5!)p% zi}_A}6IGPDskbq*Ryx)k6{54~Ct#u}-}#hq33ZTg3ne`_v=7?VO) ztoo$#zIlqxZ%U-WwCDWr21p+Q9Gzf{5W z?ik~K;VNHV8OSWILyGsYgyEwt2+ndkhVMN6whZ`?eEMm;#k+(M$XOQ4hq4jy78m>s zes@33mr%*IO$QLSuP{ciW9=4jd$}2Zr{3m>7b=)x4VH?`V%&X=F`%8znAC_odyW(50FZY_h-yL zaD8gth8vP(LBJ%K3uF)#zz;w0KXqZiHIi+ShU zmNp;9eFSB759h+?)*U~VHe%ihZ+c$%#*#w?!=N9RJ8H;9a*#By<}F4jJ=CTFy&)(q zD&~^-&a9NSBm9(JlrCX{WIg+6Y63oh>JOIY^( z{e2=Z5p0(}yiuaQkq0>Oq+d9KyAIH|!wg_3&0-7rM+m+69en~#L~;Z%#QBy~JCNxq zBRHM=327FpTB%B1jt`f{JZ;TG^@rom0G?POSm)lPA@Hidz{KZB?|@3Xu>T58%O+Vp zr^4&riffqZitS?Wz|<^L&1a-^v@&4cr4#+J#D`+HgqEjyxaoGd&37B*&=s3#sALz? z9$r6)>3lS+JO)&cTJVuj*JM;GYbG1RcOY1x9{2wzVq<%9NMxLo7d=C0wo%B-)d_rB z$h&JM&ly8o|FZkZUvj<({@gUAk+-4lycsLjX|hpB`uoGk-Jnnql~4m6?MXIt4D4(> z3O;)jsr@t&#hHd-Z>~?Z)`Vi7A{WQ$pC>dabf3Ps3f#mqMl65bG;+y!s#p4BQFNht zvbK+%io0D+j@5o5NaMUML&Ra=zV`Ar&@bnnl5>`E`kMZ++J|m_ z!sFV!+C&=yraROCCy+g0Fm!2X;E5-dQn%Z7ZQgrg6hDv4=Ovlb-t!RR{Zd_+t%mHGU6HcEUGh*?`w8){8+XN)*9GV?OQ|!g=XX&u=c+!~JbhQ$ z0cemh7h`%}F27BX?3xpu3(4ctGv^o(JsTjdI+qE?IomlH23DBkAuid z&)?L;CQ!o5d^!-Q*4G@do{TGdf6=@2e$p4kppYJhuT&~~tn)&(^SjZGjzasq>}b#W z8*U>teaC)6iWZLr9kW4Ts?dzyKM(MZ8N*bhV}CXdKv$ceqPA4nQ*@0g?7}BS3eIx2 zdHa^mhBM)j^Y(OZEa|&Ds1a34ygx%CeT{gA+3}wgxF?5`ErF!jl)4xpI(_f1cMb8U zyMNp%PVw?q`6cmGRKV^}U#cSxTqUs?q?ugIHwxruxlZpBRxHn@|}{SkeO%)xTxm9<_Y`=7-jWzQ1>nPAQrLwJ#)FFeZWsRf)LRy zQEO~{0bM*0Lz`P)YUrwJRHRrw=*J&nU)SvJsrvqBaCNe$@6kL0AMAcpl3KPnZgkKQ zPDnpFdm}sg^GIUTWt!Rc&Fd)BD>$YcHyh!GqHk1%rGrg$?5F%>T|2j2Bf55Ys;uEm@9{i{%XRmG>FZKQ$zLHG#eug#U%F%Q|zUA!}9KGxVaUs07^4;ou#Xe=2!*cass97 zsfHJ^!|06M#&+1d2a;V$B5SD9hhN-5f;4X~zOS@OkxIRGS&#?1;+Z=W(`*1^fACil z9E?8zX2Be3U2 zih%46iL^Dip^XV(Er5=7Gvn)*^H)-AM`qIjNvHVTl-Z@+(>haT>UXRT1s(O+Qn~kx z!b1ubid+UH1j!x7X1<@Q6m$&9 z&S|K+9`U{uj#R2dh2Q|}45UQ|pkNGCTS_2xn#&e!@@-+5E{-#dP~$%ci2wa0-jxzy z*EOyt}7<=_CQL|OyvB~nNH!3SJw>MCicSf?JkFF>(LKj4A0M!cx24S-h^M8~g$ zI`6?N0-I`vU0I01yi%VqW`bhI4F$AV_toa-;-H^*AOc07`xKn&pA1ngtwL~ifuY^{uEpe>qdn66E_3XG$xvn&@tEz%m_=&2{8iloX9?$jI*g@m`4RAWqarr z!m}&gXz(a(FUAEJga3cunT>Yd`p3lwZt?%wpNjY4ek}%0G>f|esJ3tDnd+j0lGh)P z=7vto9esKe+_;o;b^v8#fhK?7k@hs3m`ze;%B$Q#<(hPAWK2+KpKOr)#7MpjMfXB8v?oszI-? z)8cUD2Pn34-TiR`%?}r^AV}#rE$hp^`4v0l`|@Ve_nf&MJX=4FmR@7; za=qP+?o<_|-CcDtIGv&rt71A9Id7q+^=^z}V&gwB$;6(^uazU<-o_Y(4Wu)4vp-y} zIO~%5>(}xe`)h6+bfa`&+34gylwA?-mKdyJV~PLY93E#hQ7NSVRD{uDREeX>W|q(P zd~AK(N$;S%&5H1^T<^TwI1LwC;{^*IJQl0 zT%X2oOlWbb&{%D2(k#AS7LqluxaR6zes5Iyb(?PT-t2>wT(hRg>x?hYnGvHoOjjtI z6>Tc2Es(&YG!$GaK;*qHslB5cU5so!?$r^cp*>8Qw>O}enVH)7wav%$T1;R2-z|7` zgH=!%D~b0{scNesH&?Z^{GniWts-^v!VQvLLAGUb$SX{WAB}x9y>Bq*oo~Yb`r5q` zWC;Vn6;lvz6)fM5hZEg5Zj%DSq`KNHsZPzqmxfp~MONDdj}ti6THXSu`!f{zPL={O zcn0&^L~aJ(xqVGyFuGXZit-WUmOPv@8Se=7b$C_Q(CiiD1d~hXXzErFtXk%1=xBGl zy3Kp!8PY>s$=ZWV54t_@MT?w!O}Dc#2$zn{Ht@QSJ-5So!Ywak#Ren@2mrKDIyXeK z&(HB0t#AeMJSN&8ZW{-F3d!FYf6@nt0Omu~@F~0$F&2$>^4C1z-Z=8dzMBncV=0M5Gp*s zk-tiX5sKM>p|!LcCWCpi8j47%*~=)M!sgu=S6t~>I{4)EQ+0&^cbk(n=j78KwbBo2 zc)2i0W=5F{a@W_~Z0wNT-*Qi}k*eLDu+ z3Ujgg41OEorndl3ws3tc`2$r)$)s?mWeKs)&qt%GL)}{PIIrJ_s)m`5ak0u9qp9Qf z#9KDVteJFR{GV(D=t4h&m%1tGyeTYF637#ryJBFOteL{1ZIF>bszdFFx$zyYL)a$E zLni(daTl2QrM2i-jwch(JjCVdmt&1zm1AROi|Cie6+JIFYq*dX`jmEG2q(Y|YFurV zy9*X<5--#BZ15BI2HC&Nnnu_F*0=Vn(eq9@$Bd#o{QH#9blq^5aHq(C-8p7W_*cr^ zbW}X?$w%fc9kX78csv(%Ynp!sCM~|4Fyqe>?UZy1txo3kSRI0VuX91`e;fPRb9#Fu zKD`au@T3_88xS=kT58@#|xqv35;7k zZe2DM3pHK0c#r__-DOw%3H${vq;~zDRKq{OBpsYa`(Du&p1#_*kx2@^F5f4+oPyR- zGvF`C9;sT{ajio0YTU;P5Sn>tb58a2-&;zt5)k&D44zNG>OGzR4?WJ_&~6O>XJ8(7 z)P&921wH7QvHS!x%fZ83th>22t}~-70Bzkb=3>zg`e1? z>1@Y_C(nwoWp{D{Gc~W6DsVwa#v&7Z1gi}OT@XhFjfWO#5m$7CzZ(kYb*t@!Hh(w> zdW63Pr_K%$gRrr&Gt=^u;^RP<6inJfNNZ83vZjZImM+|x!_($np_{G^wQWV899hZJ z_J$Rn>+k6@RSKqV< zxc2zS-H-1ygF6v#eI>gp`87R=o$+u;Bm^+R?Hh!fJ*;}IjD~@2L{&bx?X@W{L;Ep3 z{u=!8gs5q7tA?E#hvOCcg{Sq9(olq3b8}mZv2b7GyXwo$zI_X-v1DzxWQ|uYd8p%h z^Q1n>@FIV;HUDFy$rpiFbc79m?L&0arZCm*laz)=FDHpfiK$uhA^Ch_eqAq8Un6zq z)vf)qLAS#D{lk*`?oRi|_jkIp|Fb$jJ}sOD{|uG9y!&K~XuS`M0v*)kTWhIQBhvsf zkSui^HR!HrRd8`99N1d>kA=}7aJMPupSg(Xz|_O2hSay8B+~Rs!YQGTV2s7@;q|1| zv)-=EXwqm9jH{K@V#z7q52dujVuShBjwDV;a+6&VkG7FbUk#V3Qs$NEk?^vieV8`Hz0 z|Gj0`ow^5h&0#*LlZOL}=B;XkksCRnJ5Mcc__=t0bsG#E1}3b_D@>xAz&LAWI?V^s zg@H5*5Oo7-7CKxCcp$;-w}1Z{8&$8hILL`R`%&mD6AC~6un!-0B7NAeHvdZI>GvKF z2rf()u7Z_!ixHfV=HpUV;yK@vqth1ZK^0w5J1O*&ypFOgbn+f%Dn~RI)RyR=D-}|i zy4PFbZHu&}2IiOej|Ce^b}mfMyM-amXqxVw&I>utW)C>d;(akaJVavrwgs@2ux=MA zK}kGtn8Z*!VB1g>)J!@_!9t$D)@_9vS`?qD@W)yyLw(%ox4nJs&0mT)(l{8od6)Uz ziEsHouX|9gSG31zK2T|XJkG%{g0;2%Ff=GY`a;h9X`#$UiqZ0ckY>{o#A&$LRfKv+FyT4XaW>+ogadwc{LI$s4KETTZLXBM5pgt+w&M)NrR&2roKGU;sT?7UHkU zA4>={rdNgfG;H*+8ytL9V^yO|@7&vX3%z>D%m$B0xIIIcrEh9qI=k`Km7(Is)Di~l z>P6?hP;NR6N7Kz$u`Xpb=z5y+WVvXFSUHvDlG%EB#M2`>_$BJCNV4m`$l)FIl#~Qc z*@3su=i|4HMouESC3~Rz;FC@AsV_-sQ*zB?-^zg#*9Srt$D{}U4s;vwnt%u7eEz`_ z+`r^{X-`0f_4XykhYxW@yuPofbdU&3nn_@76UQOgHR%Zc&(pW9R7S5^#LZxmg#OwX z=m@JzHiEO4!DFR0e@Wnwwh~CTkbpDrc4(y?-r3AZ6FZj7Jsb9cZil*-V=^@FUqheI zA`CKJ5x4rVHQ0<;AOqUF{i}#XwVfn5sr`UNm!QR2CKE~YcTj>`%Me(|d0nj9oVnV&_b`5fSRa`NhO%)nuIis3WWTNPKPR*$JGX!5gM(6PPu z4`)Q}H&EYhc6y;YQ<(pBdU~?Koi01GHaoq^#0!K3*JR~(e2U+Za|dyigYF8Rx6r3N zzM*{{SDz=kdjI-?ZjzE(R|rn|NN^q0>g+4O5(6S|J}ks~elhyd=my={PRz96Iy zu`bmBUV54Y)+12IKxgPsMmy#Sij6tApmqu>;jmJHmOA6#TYey(;v*8;c1{x#fZ4aE zK$ubCH8(Z|A6g9_?RwykMTOPsg-`YI@xmS8#f^w@z@Mg*ngmck#9J+YeuI$qI3!Ef z6+{o2&9LM{)Q1?Yv?f0Elk&aq?StFAoc`~?H^LHaGZ%<-WMWAxoMZPs*&O~RN68| z)<}({*>$_?1&mbj1`cHO&8May)HSyL`-L?1c z5E`To&^O$c!-(biHbCPqCE6I*MLV|EDNdmoF^OI?+<{8Gk8y>y0tOrf%Zi`C*s zZy=tGFCsX|--=~c5vg$7PShds=9Gs(P@mBwpj;vE3ct^&eaC=LoEQ)7;4I;Kc|Qwe zbA%60K6|i&%RM#ss2OT#Br#6Os&g1XNhals^AvMvIZ?XncQ6PfCk3hbo5^u0Knv`5e^UW8^ub8cjM1$)b-KY2WH&%#lIoIU3qwAxnV>(%DdGFoz|xK-M%gUdWyA6@?X znUVqklRRZoDHa_6LVK^>Z^DN9wOg#E!(ZuU(-t4(R!-if;7&$<>T|tYZT9u+>yvTF z?~a;`z7BMg-UbRaHvndj15>~hGAS6OFf9wcArT7SzKjyWnHh*VcSA3fVSSo+S-uk8A*}Dlw2v z`i2Ct(mGE7oihf-QRYu?$V?z4yRV}DR{PCrJGeoQp_^1{P^Ln-WKX=rrN_Xl#FgH@E*T)gw zrJa-aFI2;adBPIA#~DtMzY>^(?m=+FoS3H<7&{jpDw-?IyT#XH_PG9B@ z7VZ69EE~VaiU#NakQajQgxC$D4pU}MPomBYS-2=>aZ$IY0V#hMOlJ@fBw4hHP88N24^YTK|J^B`5cCqHgUmAjxhpU|@H^d+~Df~8& z&FDtyxRK)ueZq5G{l`qByIqowdFaU{PylmwjxCpXHyL31Zr6brMlI}YgWpfgL53zx zH{C~#!8Z|ZwsQvYgKg4zpkCXJnkmFQ5tcmihNu{Rd}A2_bapHJ_Z-_yu@-k)X6nJA zn;jtI<)R>PCOL(R-%3c|X}l7b;xlyhT2S4{)zw{rce{l{T>PbfD5)Bmj>Fge_?5fw zB%OC-pbK&CIwOfVbhS-`j#CcK2-t9FkI~Is0au3!qIhXqIp$9)D#A}U8|e<_<6N?F zZ8NO{L#1l@Po=-o~WqMe|#YReDnSJQO!~4od{SPgDVzH{Q;PQa|guo147w1 zEyhNLGHmmxwlvXBvASR_qs5t9a{zZCL;~lh+=@RWwuDS7paiZI8L-5-rd9P6n8S#X zUfJF^6U{y9$K5N3OLo0xocdG<^yEPLp$q))49kJ00$`Rv78JbO4eLs-aNj(SbBD)Szbq#LB*8zaI;3%UF8@<901;pa;-R6{<*iUN8~vs~7zk^1?JWA)aL7{o}tjbxs`5lUV1i8et+a>$l zKfPN|@&AC^YCjGiM(`a;Z}*+kq(1IDdTz5m?{q$9(st;4DWQj5Wtd*~r$Tf2{o1ws zJ2u~MFAcv>v-p?P_fLZVe`tf{HtxDEv>pAPa_9pbwWlB?jNv4=tn=E19N0qFP^bjf zwF{RP2H6)K|q)V`NXF zH1=pzieSt9J-Gx?}GOlnIoz~?_e@9lu2Hvl}6>qHLoV^jMZ zMwaS51h6!HRxsk7XP0ezzF&Uz$PMAw1w?=^^T_?MmgTyE*%7S0JKr>tTk+8}|DL%? zcA&~7oq*YlN@*wbg)$~5;Fz_2PSEbi&pi|N*E7ou)}BPJ>?qq?<;O$&F9s>N%GgeR zYyhc0Z+&pZ0u9|aE*Wdpt0zhA+Nhb_-suxInCuc}2lS&H_D&;?}shnp$Ra$({n9KLL&v^gpCaHYhWQ_zvtE%qOKb&h< zb|=@Amsa@B-R`jx7$1q{*Ce|J1tMTyDlrfr+5i}LAyCEy@-~nne0%&!0J8_8e$y$1 zvTRH|74P7hy;%@uPuAQ}U#LMc{+R7`wRa_0;IpD?I+|E9N>hm#ErExZ({3*E+(#%% z=m}ThYYINFzi%myf)DOr#D(8oi_0^40jz#CB;!wo@xyt41KTdzl0IMLLmc}=5o@)R zwy|bztDzuvFyDlYdV*hcMp9O*L%Z8l`0>@%z{m9)ayKyf&g0gkNSD9j4eSzwybTLa zk=a`a#?#99b`QvT`r=9<&(8er5$9vv!KdFpm$Xe@JbIoVY|NQ+I1|o!; z)*4M0o2|p}vWvDB;Q(LC2uA*TRsBsiuk$qDrfAy$*0&4^YaOl~Fg9sCxp9bIV*aN? zT5bO*hPa*2l9&i zhmUl7OJLYXPD(bQgr`~XvdJ!P0bkGZRZD%;&8Yfc>}##>mcD&h+84L?a)L|P<373p z?)l%<(f)FS)Ni>T+|PeBO?pB38TpT<$-YkVj9aJt+RD4U!^v_f<&N(TbKbnHj?wny zi{(50#Y6PJTx9DJU;^)^FStN@HCm=_BbL38o}Ngd+o3Nu()Nqz(DHS6!BO%3a*Ow- zP6PB0jf0k2_N()3PG?B)#uBqv7X4APlW4bjwNIy`c%_s0-xXa=!jinj~Nss{Ll#ENTu+JUqBXnb6V`rQXM z;papS;`^ZPwx;?vhme!fL`EQT)D%z%xa%ilYbxu&1;UDKM?`w;SIznm(VG|5dLNm( zhEe40`2zMo(o2ngTw`gUK9Qbux@Dnqlw$Z;Y{9u(+QnE#XlXoY;Tt${)jo-Ji||l5 z{3&T+uS(l1tTzQ~TQH4)^g?fbYF3)XE7UG+Wpa;5vy~Zq$-v~+qIJMwEMjTP``Ln| zeBk`4;D;&t)ayTRuD<`QvlG(Fnmy~)ek{q+!ytxZ=*8>=vx<_{jHkRD6ixM6Y3}~J z>OQ{Rc|~S*2vc%mZus4W-4QMZbekRRPD++82MvHLC%TPnUt{2z>C}18Qr#lLyPSM2 zkOyJyb+*&`n8Sq#5PtqrF-ZQq5c;|r4ti|=SeYN1_&ZMiGb(ERGW<)L5`Off;A%*+ z4qGe}tof73T0C}bz;WC=v%fkn_}>*tU{*R1qgU68um~TFiYwGDlm(SDoAuIdOKG-J z&|cmr<4}Q(n1nvfuWv`EY5!Qu>%D0s&(7S#WM_<^-c(icv7pDlAK*UgO@d_h3&v3e>q*$-|O68Gw#=f^cXZXx;&yu60WiRZKRg2JzH%0lz7qZJJ{3(2C!VMGOK_Md%6fLnE9XCb`#nsPV|; zMsdz^)Vnlz>PWNcpv~d3(FkSr>x{&Z=Z$YeB5`AfVtpPp6YtL}UZnV5FeX4jv2bT)Qu-i1UmM^=+_31`laE>VSG1Su4KW8b3r# z!T&-`Ldanw*l-ohIo>FAt3_f9{7CdBn{RUvgd&sbyR8GW@zc4~IeF&Qs7sNO-pf%& zMU1}-ma%W-cFP63B8w{6IVzB z#pwrd04RxH0u#ctA&{qX!H{o1Z420llmvq`w(9{`&EEAdyMt}`5XL)_;F6expDZEO zk#69A!1b~=;5K9SfYJCL!IN+aQzdL+ZN@1=3c&Ow3hPM|T?0R+oL?oTUKAaf?Ag@y z=3|?Rx&xs`*0(}i`B@(8c=gSb>W6*24q75S@c(uHPlW7ktiDSf6A-{>+=odVeuoik zHNQ)Bx#KSoh5LZR2T4-nEmBHbg95<1q9Eg|Oc+7?5IFjrWDIbSs5fkMIjkKbBXJ)7 ze1!tmVp!z#dsx9Vc@m~Dhu(s9{yhL9wT!6e)?~7KcQoI+{um@I=TXrV3AwrPK^CEDr ztF(_rCE`PUjj##qYuSRTx{=yN`w?ctk;2Z*Jww5K(mg9CX}fNQ+41he(>Ya|h@orE zb$s9aO0w#FN?UM@^}92Uv69{qK+H|h+)z@Qa4OEG`BrhJ|1(X85Ve6k9iwF9_P}$c z_LP(8JTTx9a)(Rvcs$_d17X6a=fexoV9ete+Ddy!@;4t;LeS82~^KB2j6w9iiEl6q?^|_UC zI+{Pv4!m^CAKuHBl->0pWINJ*CV6h1uC!apPWeoAE^2?~DI0#yrMUfZ4`~Cp3J><> z`6cC7Kg!wf+Gd|~z0eW6>WwqtJwVjhHwpEbwAnmHtw}W~Y25_%Qgb*ncX;_MUqBby z%uY!2@WhJ}TYMmeRA}0&FVVszxbkjA0I7-M+~H#143uezC+26t4E{# zatE(y0+xC-80ebN~Hxf)T%Fz6Z9`~L^CSj3$Y0$8!lspUbLjqCOC%29lMzZh{-|#+6PP7}< zH1e4I9S6Rw1H|UZ3MR!xHb6zQasRV5NObTKnBXd8S~5qfznIG-O%glwo7Z7j9oh!N zqCEJ;Z8F!6`_F`^Q8odGl@P?KVjEBHk5nK;u~*@aiZg zVhgmMb1t3j=Z7Y!yq%N4`@JF#SEO1^Xbqc~SQ_t$GJMypr3FEY(%&nC@ zAFyxr18oY5AAK$38SW&p8>&>IBVhpJ{mvsguI%`j2YK~|4=9L#Hk)G!*7H^*`B2d_ z;)ylr`nO=g>tUSLEJ8};6e&btEg=FQ(K(?zgf(Z&Hkq;!serZV&hbtb$#R@ydYz$6 zE@|ByLmhzYoS3h;*9}|D=C7*nnuxeE*U&k7?);~nY3Kd&)m)TjuA^#Y?q-;n#6BJY z%3$l;Os_H-V9`9ac-}Vt;I%=z65vXo9GIf&y0#5xe0026i#RtQ$OV=O!;C5#>Vd9X zrg|RLFHZ?f3s6jh-S%aCqkyteI+QKhJdPEB1Q#F1*ytnaks!ncje3*F@#9=UI>6dv z`oBzK1q}grns=s6AU4emvo=gZILofvm$9E$uoUtZGU?YXz> zkuNh>FEz_7&J3I9M&GF4T0+U0yiZddOghwIhWqM48@Pk?=?uQJ<3Z)Md74blM=||D zfF-OK)?fzA2 zclJxEn3HKk7~ z4`=#&hL5VtVc)g{S1NeW-qePwW3IVV7iRmW>AjAW^W3kGW(S#5P^lcnIydZ(=k9gd zmUC>|%H%vf*s&6-X$XO=ey1?(O;Z>ko<-+s9tl^h@O1?2zvvwlzioteyUQ$|GgAHd zBq@afW}nW2Ixz2a0yraLYm_XmTVh-*e)u&L&0c9)lAXYH2bl9bS8iCq2&>mefji!J zsgolTSq!zUAg`9Mfn-3}_)$KA)S{K+WmkgjD(Fc7OaE^CY&P4GuFE02O6+$0sepX=;f<_ z%l3a|alhpDyCwsggK}wqOXn5`_^oyqv!rm#;8e1cWyYmA@-n71BdpX?%i4_WQUq6W zd6%p#-@@HD1CXG3x}mk3#4G^A1I>H0=aN9T3rM(l;nGW_f6hGOXWmLal z&;VX$N#NkWDK~;b$}dO!z^JZ?p+d&mvnDzNvX{&Nks0s--N;+v60^W5JyIVLDX`j_ zWyJSz=74!fX{u*d+;H#YyQBC^88+)VnNFwP+C&IXiH!K?Qu%QItS@YI(vB#CWZTnv z{t)L?Xq=L`i4F{y{C&zV8k)5IvLAv^a4&tA^3T+?c|1th3<>`h5ST>0b%(6)0a73u zYG0Hp$ zA|?HBg!28iG4u_Q_ar(cekIM%*-9QlJ2puEg&}f`9sU}wsUVH(!|^T1>GI{Jdi-&;*}Y{yx-GcwkO^qB)MgP zO2}myMwdCrr@EyDeA;U#+U-J7zgrLiGX#k>)uk$ytL-1WW?M5wiwbkw(b8+$*Zs~u zI=?z2+`9ix--&7%6aLv4lOTB+kCgo8H#{Y!^lgM!H({DV;<+Nn(3}9;HRUq$n(9=p zSsmBS*jn>Ea!~*|abWF*(vITa!P>)bfA_ln)R*@W0o#VbWT@7m5)2twjVVu0fU?%f zW|+q=N_vu~i)Z*R`W(a06Sh6aD2Rnwmy)w2n_s?cwvJAsYC2TXai|!J+N(+Jtr;Gk z=wLNZ0iac81g4Tx@~ftDu9sBGla0VcBDSiPvtP3>Cx`=FmGo)m`$XV@)K&(&8BL&k zC~0uMczPZBO|MAtz8~*nqOBg-k2=_DLgdqZv9L5m-CC8AIvM&hYLw+Tg!R2n#t^x^ z?LFqe#08xghfX5NSd3!|BAwa`5NXZkBBMb|)tXAn56ZUY^+TpX1n0`U&)R?1%iCuK z-XdG+rgS1l*<5wEGK1V}A+7_N55I*1wp(BF#8^z0w&sxGu?K?0V*6x6l~iJVx7l~8 zQqKOn_|?s=uAPB;s0T%TRQta6A({y)c7|Q*LDL_|pulHhI`;ljRh@jf;e2Ey=U%eU+Z7Ho8ngTkgj%eeCgn&*g3b>z%b9NWsZdhyDOShDaP^hgUn}Z z&?->acna|KUl~lL=ci*gDz1ZoIBlaQu%#ibmkhzH?Q8jEg2aA&AdBF{QxM$iizST% zA*B}&pKS6(6j%Y}Po9}tT3|+SmNE`B=sQs=1W1+yK1+(xFa|5hYY&mH>qxJ)MLrouAtInmbJ%>dV`kI z?zqo2C)7dS&G)MC=Tf_Ymi9?5Axg{0do;%*#qhtb{kOn^f#=pMH#ea#PC|s8F?HYx z$eee9JZZrv3j541Ew6bGzFsoc_{q$K9v^p$`tJOde3gRC_pBs~>*wAZM@CLRYY3C{ zZZ&(gt7BrnP)8IC?dK!7^@|LltH>In^Mq}rYOC}((|1wq`b3;Xn6_1A6NKafms2HP zK=C6?Ay$v`?c*7^CuSL$!h(scb{Cu(dX0QCn|;3OL_>SwP>yEWgHjIPQXw<^+I4ly zQim~ngjZsz7qk3l@}B3p;km_xjNqQV3H?)3l)Vmjnl-jp9>N*sx{-l?^iqC}cZET0 zco${!!1VMqb@B1=Fc93Dr{@oSeD#I8Xf6!Lb)#!~7vsp1R@-I!MP-k}-a?#l_9rrDNm(|u zKF_X>he-Z$s5ZS|zoe1?*Os_rsW%3)K_{C47w??pFafOHubwVnUc?JdFK0C4PDX7@c z?cb~-j;~9lE}zQFf-7lW(MaLP@+X$`s`FrKVj`j4G1>IJr#3pve~G zwY!03t`cII92WqN^#I^_=RW|)TfKf>gIrp2O_gUPKk$gDjZqxkVZWbv$+yZd`6z8I zz#+tjRcIO7Rgzp8k`5l>mPO`qFa80F!0#INzSe<`&#QAkJslhGF)xXPW`(-|>P3eW zHSQqz16t$_2ugXTi%6l(9777X10r)&7X9!Xd{TbVW@uX(gztnaTf(rrxdy`OJ z#L02u4>JK<_y=k^8iiIKH_;kUT%*Irm5fHaC&xL)iecxJyvy#LPTIJ*k4H^e9inJo zIAVR*#^Y%%SEDp@xHE7D2!6v!)g4~bkLD88bBYc`z_LeZy4~(de%sU$jX9oCfmtub z;h86%0G|p?lU=?!!TPOk^j|4%h3F7MyLt%e_NfCW^d8F|ecc^P=6<%)4? zi$rR$dC#6(kF2tyup3JxUEihZTwXlF7>hh2GnYiWPS0Om$?uz;-&ZXR8F+uqHsM+> zZ)Nc~2c5~4$k+*$L_b5>Lf}mRO9&Gtgd=2ZWkf&M^6M9F(69$cllHfT`*4FO{1{d` z&~y1sz!gX8QWnc8YJS~!#=?~9e!C=67vaQ7 zPnzX$U1KDXQr&bnv%s_!l!!JVBE|4myd@@)uu{e*NkJMviEl6hq1Pzbn!yGx6o6By zqkHe1SOBv;)uZpmouUnx5nKBmhVn*=Z|19T<~_2s6EX2M^Mm;>;Gu-xXE@hf2T@Hh zs)pRdOK{Aqw|G+NNV#C*?V>mp5c|a-{J~B&50d2thM9ZjK|4pcTx77LO=Si}Z*#}q z>NIh-tu27$k$?bt@-d^v`v&gNC~kyQU6G8H<*~R~Wx}&Z=^`t>U0yvGy-5UY?@WOP zDHO34GcY+(SOV#_O>Bk|c-$7eT!nGI!@z}$vGcv@-mf^$$9+ehEN?rB0-3!@U?&m)GFow>v@v> zeq66$bj2D+Dw%Z^XAKW;_|Velb1mqSyPI#aitKIn?5jRO+?6y+=K1DIH$j zF$;F6SWk7;j5C-)vc2nSV_0x%g|l06fM#t>_-Mw6!ZPuFADKYQb!`y`nNi`@iIU;h z`#!e;YU;wJs%prbfM?9r#E&wjh!oftgkjCUTqd$x5-`X1D z-YPrs30z#0aiaQt>0w%_N>#Lr_pzmn@dGvnY(W%`^IiyULs!9!17Yz7=LJbCppzDX z;8g(-uij6*Bjpk~=rS#$z&U9tbsHl?xivCq*lh0Zwcr?(flF8*e@OFsu4P)g`|6NC zbKKGC{VH9?vbl8LgKSNdHqzY7(I3v-X!79HM<-aJXOW!GNe*p*V)w>1SKX{E58PXK zq6Jw!nwHhprIxAM5#44_tr0|*bA2S)5&w#bA5OfV%c~bDzMX&rkV8rG$jdyI{A3eQ zAI>&>Og8z-NU@%3fSlZ4R1SzjIldOAi4(i~M6ORVVXarK-#jzI9xe-Oc+bUP{ARBX z*WWpA7MxO{X)N=sAUBvmJ!TR^+Y17EG_lFxP9df%BiP020R?6DCXXUB;PD;gtt>5gxkHRh@zd**2f8!JR^nTa=Ur+0R2 zUJyBG(>W2}YMqvTZ@eA_$WhQEcb(V^EUBsl64WJnr1Tl8h!CY{e5m!Ct7}< zu|cvK(=r`VvHw#IlAtXkrv8`(*Cl76;eHe5#%%{KH2p>eTB&QEns$5jTY?Nd(^kS= zbGOPJR>}`TTnD+qsgSLk_E_q7?7u@_+&7o0;YcDS*Tgg~DdY_%A4t|d7?14}jK;@M zvpPb);Q|R$ak>F$x2(XlmtBS7wYn+5rk*yKxX<4vlIamRxQr!mLvnU3A^X>CE|y$9`W`}ZgBvbeOY*Mlf^W43qorJYgI z@t*f7t<`suRi9wyW#?NNg|YX2J;XhG$A{*YXP&>%EwBHo%fCM)5K55Pn1G562QNMu z7hbFeXe!#k7ngC%#*X)q6P0Ll2<%=7l6?auYs0T3)$;<|ojhtACQ=QM8f{%Ih)3rDP^BTD2i(?@-3?AHhJx zRGFnoibtX44f@tomqs%-8Bi`*lpY^*&)Re7N}yYY|Mf!jRr!=+;BWhNN>;z?;fG|> zcC6BK{#HaaY@wU;JQe{aAP}}2CB0~#Hxkm{J%NDeoB^Trt75>`fZ2FP3NJ$n%%Xh* zkzZk&nDLi5SRIc+6kTV3ms!j||m&9^v5tbRf&V{jdvc0-Giiy!y34|RZD=P{q&i!&j7x?5w|t3f80;$(=y zBxo6OD49%-x#rK6{#8_zZ-|Yy+5r7l=k;Kl4&u;d{e#Cx13%sidK$}HHgu<|tD+ZO zcI7(C1c+@f?AfD+jdZEkg)I`?6|B2e@;ithKv|80Rz_jA<2qPXc#)zh3e{`kr ziVjBHmlghT@xl^Thhx#ON|tOP^GDhzbfEh{#|XcH5BI~5RkNK53%4lYU|)+>E|LZV zMKAE9dSIR2tTiPRDeL$VwsjqPx9svpYlC&is4Lo7vP6w)J#%!IXD*ub8{tDbvPJzNuGa~+V!Wi zO&mPbZn&rFft{wgzl&VCzNbg4yuVkMzp7{OEo>c zMGQD?wV1w_+{dKnx&NBu(3CHFEAbWBoxj?eK&An}0TkJSQ-xA_!gjB>qY(9_#G>zl z%}F*wA?$5{A3D>pEkJwd_q-AR{@u@OZoR2#Nb`$Gbl=wI_VepZ^F)JV9&<-s9t;4G zFQ&`Xz98OWc;>N7ELA3FdUo&;fE70T+Pe-p8iyCeKf;+|)gclG4&Xoz$W+kh3X2N_ zvkLOqV`93k7FO%od5f8vMMD52G#w8G)LC?*YF!hk()gWd{t?sJI%)seL3?n#R|Z!s ze#|%vYFq)}nZFqhjZ6-WFnhORdV#K@8nMvBm-d8CPaNuxYz(~4?$v@+CW)D`#&Av? zeI-?S?i_SG)7!5hGUg=Q-D>Rp&}@Uyqt_f`<9o(dog3cMRcC-XIOrLoT7Rq#2Ft8a zJ5BFiB%?03yVCvg!Y&8$ED@=jyN+OWT%7i^tu1&g?-mDnKbUKW(>o3=Xl`<*Th`#D z-@CqI0DrdkADnaR9He%4zo8`YkzCeGcoL!#@iJ;1_OiJ0A|lWZss&VNLnM#1xK5YF zByJv?9Vyb{=_Q&jS8P3xkZ(SMwdPCzy3m;L_Oa3qXKypqutG%*vm^qA(CUySXz)a^&k>~2f;){ z!1h5?tWIb#>wTDY?bIjAqch=`eI^3V{OR{9T5tSZ(YsLpFl%QoP}uqE8Ft8_APJ>Z zeZG&pu+$^`W3UYXAUwFv?0FY=Cg~=#pXUtAj=FGYK z_vKgS0v)QG8*w)^g{s;Wz{*~Xht>-~?J7GBd~X*JoInA{{=3wOFTTae@YRvvrB(g) zlQ76oMf9*+BI65!`a0oR{JgcuzJf8c-tf$b1!?O{0q~s`9~(ajoZ>0r>J#;)6Nuc< zM`7yDB_peO#aH)!m-3F>R@m5og4J4?js6w!10#|_K-a6Z0|7xDeu2$L3u2)Wem=W* zfx*vjVLZhz7)~OItc{lNF~hcIW&XJrlRXLTfW(JCAGIw&bZVfNnd#83y@#jL`Z$&) z8}4m=8G~T*b{828mSep+vP&??d!xaSVK6nHB1!m(!~1I{=Z^ z(Pk9mzA7;AB&SeRgZ5#tN5YO}8F=pfswZP{X-VFSz?}9eP}BvZ*!q2Kf%8F4D=y{N zDRv9{q94t6m5{S?u{I&@G8`<4-6YG|I-v+IU#rOUg*f}WR2X{m91pumoNaTRzl+QH zR3l`u=8fkj+ z*(#jtO>A5zJccAr+7a6tJ=v!35L`GD zpU$XZrQ0Cr=j*tS+aNQ9iKSe z;qLQI)`Kk?SmsSZ*a^xZp&e>}?)gqRG9fW6A=DZVv#NSbgWg$gz1%s;H~W3oqa(O7 zXLhjUqnpobk70w_^MOzKh1j#D?rlM{gSdXPwjcv{nl;`>U{J#N88KNjVaQ{&0 zdx|HWVG&gy_Qg5W@?)Z@PIB0e?VVq%nv6Vv+ha~Y-6b~SZFfuoFzrd49U{9G`6g1` zVW`3r9TbDnYaJdDZlI$DR3TvEJVfp1aSD;HTwQJIq z$qjpRI_D4#2%H~x#ZtC7{V&P)RE*k2TmndCPgt1#087NnN(`ZHppYOe6qn(8(KUMD z2C(FCM?`Ks&rTw4Gf)uOkF=4tB~^4D*=FO*{^J)Yn2tHlF-?z^O`oq&!gw9tcD!9Z zZqRy}{`Jale}3ka+5&S2i?Yh%Y)xQwJC#fFjEh;;O=vCb@xpq%6dz^0NMnvCu9e&YeZ3)lmN(V_gG1}u9VUTW(Bcs+FgrzSq) z))Py^Fb!sr?)6PV*GkFTn$H!=-JXrLC-WvgPalf^{FPA?^UiOAoXQ+bp9jP(cbZEH zJ`Z?~TJC+7(Dxu%zgC}IZ5lQI%tu_1ow1uypme0`c0T6K4q!u2Le^6u+wKQ3;u9Jh z1SW{i_Kr#aL=pr*Og>U@g?Dw~D4)=g&WFD&(iayjilmE07l#|wI7GRyqMZca}W(xlhO;JLa0ztFuDHRXBYnkIW&aF~VlPOGTlz zB<+Zr&J;vY#Vwo`1Q$v}oowX73g>zP_v#wrCLOp{)c~hyKB5mB87({=zS^6Gy$%4k zC?4x2Vnf_(+R*V!LR|CMxmVOEW zs5NsDQNL1>si2tJ*KrcethgP-3^Li+g&(K}YUgB?QZr((SEON%K1-pd0!<6UOP{P8 zORb4{=B?B6?J=IY2iYD5?$e&)?^2QC4H(oH|3RDm>~27vpUqn}O^vG2+V!Q)+LF>8 zfSR?QdP9mjK!4;HU`A`GuV*Br+&NY@ZFKahx!+Kpf#P0Hr3G*3NS%7&$&LqO)wz~? z$a@+t+rRGJ=+<;Wd&d)l{K~c=s*C5&iWhFK=?|_iCA`laZdj3F@)4r+08p@estr18 zB`Qic`Y*cvq^Z)z0xJr)A2RiT68NDnn-!qk$s++=`{p5$3b$~*Gv6U8E}>%EeEika zuX;l@+s=(eoN+eB9wpCSy}g@%QoT1&@qWz1W)-jkeu3^Ywbj61@VGyQ3joB?0uJi| zOc-|_BZ*#{>5c3U;lols@{w7VhG@B&?Gu06kJZ(!p35%vcL=?>yO931JJkzeqNau! zM44c-0=y5A2+h+pN+En~C^G-xP!Dui1*znM zxex%ipv#mlpAXW@WIvd0I%T3vQFDA?IC-fCDDdeXv_+a1ZTZzNm4h=Lz+C-R+W`KZ z+8a4)lb=b)hgco0uo1F@beoS-`0W)PciF7h`+bufHT!ar?x zdz6_2KOhR6#>ZdWr3WFuh3WPev&%S)Q6WI@Zv=Ck1#6A}@ZLncLbE0q5@3Ol9JDmr z&5laN?F++MJ&j6|XhTVp#tDMEP&ROhifGnpF3^C%fZsPx3M!tI9$CCtl00u`YgAW1 zw5z%?Y>~6r9qJg<6Xr_QOJNQayUTaGloUEz24^wZAtpYb8!%Xp2O)YUj+W6L!L9?e zl-7>=;*54TDsvQj#6i#IXMWCXcOJEx>MlH<#BZ!M=rGCL%h`ABqBTWzh_=NmOIChi z@V%PuB)s-x23Ca~mC35>eW{h;F>nEK9x?Wjg8<`JzT&%B4O>bX3e(F~Nv?q@U=*0@ zpt#9Q(C|~vZfo$q(dasfPeHAuX*fmP6`4{10Olg*zEJNZqqh0^XP`vGu$TwXwbXW` z93#Wxe(#v_l0@bf58sAT5h)y+QU<>!(WnH{VW;EA-_F#pC}74>gI`Iv3hcp371=$U z8^&*lt%L9atyeXN!-p>oCN{B`Ozj8AL zm!bs>|H2bfk}fmB5V^qmU{vs3uWr5t{*kp|pFXo654NjqYfV|ub{vdg4n6;ABhC@z z6+RMG?7=~-tk7C?W)QoDZ4AtRlJOV8;1~QRSU5mF{L_Fb7TAn#hQQ;r9;nd+cJWvQ z8;8pmMo@B@H1uqN$5>`l$6zj|Ai3}vrh^DZ#B=T7O zx4@J)y))o&-`7F}TQ4~luYomg(td^`hON9E+FqvZ{<&^}UM!ELMOD8S(x}zs`dBtOvAb9TdlSN2L%#v6nn;K`DGlcOh(m<|n^ZZb!3@RuZ$Lgu7T4$g9G9 z3j}JOMYlZl41d~NniujbnAR(c=5+@^R$#hN?(X=7wcD~ z$@w)tNstWLFFA&EJCASX?@nDnzyygIH{cE@W zf6w9jMRvkqT;C21{tO6OU~0AggUqV!qf85iJbp57THmvry2*C~g6BqvN!xO6tb7!1 zP{{$z<|@Sbiq)T&7Qcf_O3?U+k->n`@9QNRIs$OwB`y+B5{06H_#Z7c>-x_g-g!&V zA$|y$SBKWf-vW`*m~CRRJYPk!C2G2ZmcZFn^&EWv%p}b}=3e;T-*;fj_35)souUH{ zi%LM!RVZcvu{akGIKrzTIe6w-Tp#*tDrAl5?3GQQ9ACqvQ4LbA4ZT?<1~V3;Y2~CziZY?kjMwb1vvlJ9v)0es`nZ zAT#OZfxmgM$bbD9O8sXK@8m75ElUS5O9h(FYwI$*^!#d&Q6JV0{L?Sxk!RJfTY?qv zEspZ7`_|>hl>H*=qNagaj{lA@381;-8iJGr{qbW zo!viyuf0(}c=|+_JZ8MXZsL#o_pbcS75onezOC23OI?T>`w@_Qe*@1r_`WZ@$@7nQ zBGzKGZAC=#Fp8h(H7@WdGv^5t*xs z;ElcEp_rlP(f75@_u)V9k^HH$5UCvKwF!Rd8> z9XbBpd%`_H?!N7|RKkPqh4MLYh~4_gaxe0fumVlncb#(sP5 z#hLM7>&SCbe-{Oaf1N9V*#DP7<3D?N<>cBc>jr>&9R26H$sQmU?@at>c>7ja8UUq~ zKS7EV$$<`L)=!4dz25fRJUJ|zfdzRx40#F+h%c_5r zY`}not3X`}Qjo~JpiO`*Wdl%L-^{U>Xbu13Exv)ca5juWVuGCnBNU(Vd-sooGgtM0 z&s&>b`&_0CC{TYH0sq6P%HM>*zssBcH+C4!o&VAF`QLp2zDPz{HAnfI?(O{N!+j&) zVdV5N+&^{+sgnw2g(<1P%52HCN4&Nms(Y}>=kY1fr^fW#X8CO7O?(@o`CZo27Muxwu22e~`D;_o*4@^;y@})U?i%Pr|PH4yRZNs_Bcd>`qg)lbb z*JdN^5rT8yMdL^Ojh|?o^d+?uV~!ARxQo>~Ryvm*jxwEAAtENJcr8ofuZYUf_NC1Z zLR;MHXbJemRUH)JQ3Tktod?BPtUXi)*d%b3HzeL1l7O!ywr&CNGwGL?!x5@Bu|S6( zZmZHD45)k(ZjHVfY7GOrHH8x^{pY_KWLfO5jx7x;V;?ezai?}m_awckRK+sJzIfzR z-m5)T2D{lkXHT>mG2h}!`L(lqy~`n@|D~*|B*!@Nv3%atqPn1H*-f_8PBubHv=chS@dN6VbT+ZmS$Yz= zxQ-3f>YZQ$;s(FBV@0A5yo+0bUN6lC&Q}2{QaqQD{FNm=-uW2qzy`JJtLb`eiz=s| z^Uh@53*B8%Q3_yNHF$6(Hek;}#g9Q70xn~W4@JI<@D=9(?KPO4;@2lNUZ#uAKsU-H z{kFRXCi%ZXXSjG-FumabVQkh_6vSfEa7k+twIAScswK|1w1= zJjI;HoU#qeU~)>?vmK6)WVfXh+M=}kYY+ByxVH*5TVv=3ppV=h7&bsfZu2^nI^EC@ z=1mXw34HSm4O=*chr0Mc^W$^MM294=tNMIWQthpk`?znq0;!VA(e=x7%hP~`M~xpA z+(eG)oE~IU;uKqiyJpdKUa3ME=ZF3nE6#ds++bq@9fF^DLF8U3y$-7BV&=HZL3QyE2u23=oDac^w; z^NlqRL=##Ea|-8zEJXFFV&!EIVf zSF~!Hd9BrQ{MWFq`?*%Ax_>b`r+2*_>>3Ffp6s(s6n%{d?5XPkokJ z;fH=I|-q?Ol4@88elR-a-Ru3Uc?Gx+hf zZRKLAr3+~&m>{q$Iy|qc?cPeN?`_*=F8D!Qiof7gF%Ez;142|dVvDWZpU^F)IqtRg z)}h9|JTg19Lj%9be_nPHpE+TS9{1xMgNVqe?GZ&=eRpV7NC)_Z@&5etM31;QY7Yri z{uW&MSJBXW-nbusm?As_JbLO}ev|CUw(o1+g=@!cYUB}^nKE3Lf$~SgSiL9jdxTyh zUjmx{vz1ESfW>cfW4_4~0>x~XpzgzBIg_j%MF@*lcBs*@m#$=6=OYyBID>?P+M&cP2oPdm$Uzr#oSm1>!KYM?LG?%PAUDo75Yi_HEq`j5 zxU(_u{$X26uzKxl>}W&oUiKUq2S&=f=N0bt47bP)2i8fp{ceQ^J4iJ-*QN||xT^1; zwARMZ+=3zim#Q|P@i8X(Wy_x z&XnJGsxNvIkzP@1__nQMJ55POX&*m!))sYVIy;Bf>h_paO%*oWxk%Cf+>DAzJmdJp zb{K0B+Pg_y9zN!&(*q;Ce?ac$J53}#H;CYO1T5fGbG$Y^T9YxrC8|KsFx4p`2kfMG zNy#O|EfyjCqtOLk2rWhgBMDJ!^^%yxsIh+`G{I0?NHrp$CO{_JY~=a1z!e&;_zII= z`Mmqg9gq;o*GrY?L0nE9C4xWSGEW9lmh-4#TGS+x;QDU9d4Jz@7OSf<9ht2*lBG>9 zOQ~*gC#U3))nbM|W&+G9*TlE;U0)`EQX{hqD3&IuoNN9io}MAeIs0;Sy*20hq9>vS z2KYsJfvck%%{nqcy=ZtJS-;+`@O<~5JklshB4b>NdyAH_HNXwTuPj~_ZpDT{36rL` zwD$q02Ge)hf^$4=n)ISP@9>FaXLMVU>;TRjGyM_l+7j{OcwRE*vUhcJ<#Q;3?fb-% zdb>D6ap04YiS13`M+7FV6FO~evf0GNGbLD!THt4dKlfeg)|$2W4frG@$p+6p43nNg z@2EAEKX1Dq(&c*adovq;9oz62rr^-U z{f^0@ng_HHp$l_tzEA1k<%KxMuMq}iqmw)+K)XFq{2(|hPfsa@j5^q0@pzDG{W{PF zR-c`fQ>j#}|7EXZ;(1Qh1=!HsK@lb`x4PkA%Ft^# z|NaAGFCz*+p6)6x#90Hf(fS(JShYY$oVAGCi#-4h@y**1KvEdlqb71<`;mFvHoSDB zF&iy~fiq`?9#}mgGIcqEm?F_>x^rc?qVYa@gCmVvdGApo#qo(LJa6h*d?h*Mb*V>^ z?RiIP4H$IOQ37u;`%5`xLCqD*7mhrjqiPQ&ST{DmN$B_RwDHoBSI4y5P#2UaD-G4bt8cgzqOZr$FvA+Bu#QNjB1Ye%jg0s*VYa*uM zBbvl#2j2b)>I3qm~T_hnvLM}Rh;{wS9>Vfur}mLK(N?KHuIHIr$_$cV`mCCd9^oIgks`x$ubP(msH}L-@>;zxnlFL*-9E z`zAdVwMY?0Lexp%4h7oCV4TdfvtpDX2+l`BVPx!SK9FIK$2(&n!1k+XLnvbETNoRf zrLjk<1|%ks+(terH4dIkQmQanCN91Ym_J_0Gs1`R&NauGA1GW+C`Qu#XW}h8Qx;BHFd#F2d+*(XDQg`)k_ z#-Xa^j50@$DX-f2=z$Yrw0sJ4knEn|A@I0#FG)Sko1WaWcc2IkTlNVuSIMHJw45t5 zeGt64QJ9bhRV?`iF@l&xUHg#b&y?Us|1#iJP}ClzB^6 zZNWEsRj8KU&QyJ2*P5qZWU7Sr^_XJXKL}QjGYooK^3h&3{>e)FA&1;>vJ0t6I9Ifg zL+e&pKE=o%bIY~9mt^B%`xGb&T-$NgeW$h}wgVwl5<^U|+5cUNW|HlfBjx>JX)#@R z0eclpP3v)ggODXy&^t$KtErQ5EdW#!asc56ZNDEE>;^|WK3ljO;e_6 z3m+Fetshj9O?=xuXek1VYZSGPT$oGN>fF!3JK00uH0+*7rZvVI{Y9vh(&ox+ zwQ0Cq>PojcMf>6&SA5K)sa6gjiCn(2MUy$a&{~x~IG^(ldU;D4Mii#MKc}x`% z86yfZMbc^k0U;>l*}|VE#rLDi6IMEgvde6%ol!LdpR0n|nlK_^TJx zPO~Ea^`P%{(qW8(WneIh6LP5QX~NnF*p5bSAn~(&+Kc8)ijw_diRA=%E>6SOg@Z3- z$3gCNTul0d%0OO6uEd$wZRa;7QLG7 zvi1r$->+9TjbHE&!PC6POg|kd)c7S^>(=43@4k=K19i zj}dlF*lWiq;a3-$uM-l4BfO0Q$!;j2=QSTCzoN?e7W&$UP@RKggIcD(V6{Gey*$H&OM-K+1M0f^Gm>(#9Mr^jVSUF8;&82Jq z6#DwLdyVL-U-N0i4#xw<=9Wg@qKdcOHeRA2Zm6S+M@)yM?WWN)Lr1{Q&MmTxkGeds z%8nVyui7nbCq-<$V>`gA7iD`Tp^#jkg4_TGi4Jf6n5WBpgil(CEVMWJDBBNGN?vd8 z^IR!Y3iuNao0lEXN$opXeuWaRPj-E~Zn@gXa5^uXFQ1ojacz`qK#R6Sb(pWpx)Hc1i}P6hVO?Rosx z($p4{EmLY8OBu;MndV-`A5nJueb_3;_VF;n=&8!Bit5dKVKqh`i%UNAHysm8npIyd z)kC8*(v^0Nm0%-B9FNo1sQ&sk)=TIjnZvvvhz2`Mbx&Ri<yQ|3ohi zJ5|vp4Gf*$jnTJ?{8N#CA793ZwnGR2zzS=z1ZBZqX@In?+t`ac#yLNMb06P80q{g< zZib&{tX;I3D(BpdtNg;a)8XocDp&PPA0_WOX`P3*-m{RS{IM6sxo36Bb6v9JPz zuw7b@=xJ6USnKMIb>a9@h)QSxWIEFO+DqgDcf^5)b8w9*96EL6D}TNZr6`GHN}unf)UTfG2stS9%Wiw{ zoJ4MnyUI_vRc`cW^p&lPX<4sX)<;boF4omXN~^Cm-|F+~bIFfXGr|WHuY?=&XuJ3i zTQ0=!t2*v~kDE{DHqnP#hwH}~h7tJ;H-a+b=BRn7aSHDw^OjrN%N%EPrEwvL9}k;&rUe3Ne18L?bw^3&B76 z)?PQZb%k}v&48~z(ZsTOJB1aVY?|zN>t;yB?F7Xl8VxQ@_g^Pm3Tc*TdnAH}nl$%+ z7Ah2efPJFDj~)ES0m&I{VK`NK=yHB>Mn4jAn)OY)JY;QxnmnD zmm>8jl44t6#txPye2zoM17ZFSm_IpW_s1qeh19DC9uv3%$-u-n&&2vj@7t^5T|I>D zOkry;3xG->J4e`uhtQTJKb>F@_Yf>Epih8c9{|E`ggk?hzT>|<6{cnZxui7=T=2il zOp$pLM}(WKyQrMD!z^3sqoB`)N(x4W-jk?fTTcdT6Sun2rspEGQIq-(e5Eqc#TZ|D zVY`{dYPyEwiQb8V81Himdw)Hln0ND-XJwGtzDIg19>ozkbQ7}!EVG9+KmOvtz#V`4 zi+itn-_3_gbX>eI%XDPpGjo;V!@&{~G*v{39GMB@HCyV~@lp%f;c+2X?KO#GelzSG zo{-%8Uh9QzWKLH!UkmpgJRA&7NXxQk-4ZnMHLfjOrS&r1Bd*bAj4yA-0Vi=;Fb~iH zPOug<#V^d{;pD;nSj5w?Y*xdkzW5usxLqA4t0$SkW~=URkiQV|}*CdWNBgKMtU##~C4&QGDaj zOK9##?q8fYUYwsBGr|hZ!$J%@ixbqvnkh#I<#Nu)KWsl)(dgV;88gc9=6KhvHx78O zx~{r#fQ;~VnJ;+hk!1E4cHbABor~UV_LH%-1j|z{pJuV+ZL6+%($g{#e!Xd}iE%JJ zukrcw`}aNPW~}vY|8@JmkB`Q^YsN;K%c1qYeN&Vt036fcj*S{H|4>YU!yW~Lxd%7P z=zNNi&z<~mm8&kESMohqW=D|}_G9dNk)UXHyPuSS>T+AqLXhY7!_JWSj1bk6=hQy2 zjt=GHGG#|$1z|c6%H9ArustKb&rNIbl)xwOZ!@)Nxalvem0BRQA-U4`bxNM;X=k+B zAhM%l0HHwe2mJH|gIv#CI=% z;^+CVOEZOIlR8dGtezKULya73B-Hbia$ z{bQ}fBv0fDzZVC}#&9i8z3hF)j_u6+|syMoTU_xTF}L{m*mrLdAzgVxVlCqy&sNC^DuE_ku#k z?PML3{ZaKjs7k@Yxil>@8zgRWMCah1bO_Ol{I*Lzv<^=eWda-SNI|E;`d~z8(!N`~ zCDoaT2{z}C^-omob?D=%^_hfBS+wnibKx?{tvl0B4&b!Wn&(=#cQ9tCl zc$X9Ed+2%1+M>~)w>^q&0xDe&RH^B#SquOQ2US&I`&)^Nb=rjCScz@ol62F5oVISW zJYGvR^eK-1>`e>jBiAv2CUjKFWXVZXA*%w+FU^2~DawJ%n&H46N~^(*HVV>N><7Yf z=CXvJ`(=91r1fYgNXrYp(;3fPn8h8y9|{Z(cfyf1-5=C-7vc$VWgMJ-BHkjKj~j+@U2uIqH*c#K0G>zLO1UfoU|t~zeSO{m!rZIJIf{k2o?BsLg_NAXj@9}$V`Hujv)kJ ztvBp};+!)3`T`>dScWXjcHrx;0sO1FV2LZ_GjLs4IQSx9=W~96a}DInOx{9dqjr{M zWYqt;ESf_8A~^)bQ^C85+>1_a`Y|G|5uV~L-RKHzm|eb&-B+1V4;OLL;#)FyQWPd$ zjXC_%VRfg;`0&^tEe+&5j>EdS2{Yl=RX0C!d%6nSagG+Tjvfq+u$uec4J$6^X_}97 zIUY58ikHXp6P2Oj56`NHU2na1LU2k#b7*<8cQu7z1;L~p>(l z+~`AT1^Cj(#W8+gY&pWO5_}M8I;jt=x9+TPH6N5LVl>wsHr zPaT{mz)AGgF#FJYwb;gY4t$EnYPnn{jmd^;XRz6UzFboa_05ER{f|QY&(sA0Lu=_* z&sf!~6UCu&hEs*AcM?Sp zLI?AVmzz=?Tb_4h&%cyu5rN*B*)fQHHz+!Iw}+(xs|eX5s`Qa08tQO@?1BGoH{l2P znGkH^$i(klA^VN~ve!He0vK5zF`zx4qPui>NW=`-w(fS!lAp(Sx+M)qnR@;Bw5ew( z|JY{u?uceJCsPLSq4>~^d|JGZ!TDEk0kqa$7xkv+tNsL&e}rvIo^^*T8-Veawr2EC z8&q+xzPCv~uh;he1Txz3K#}D+wu+PdwTi3%R2x~p^ezebn`V2v+L6+hzv9*Cm^6A< zG$*AIPr@jX6EGk0Y6z-us`w1y3hX2PiQos{ZIIxE27S$WnB84_*-zT(JW3XDFA$Wf z@sYh0MYO^rT<|JJhmgv9pkUzpjH@QLa~R*)h;Iva{Ac~*gD+T7IQ%{wFuC85z6D3$ zDJzF(!U@Vi^Ub+!Fa?>tVIcOIBI7_x$asplU?+d$kG%Ce@BYNOUL4;gxWIb-!6Zg2 zYPm+8|M*#UsC(AAlk&43rHEM<`3Kb<0b0umMh|zwoAk% z+&9E`$iEYDLg#rg=3ke~)+!1%jQCma|LJ`HmhpLRFyiWw2MMvM^;8CDI$%C{nD#Dc zuE{+vZUeQ@(s^fKVo9;-u0E6F*!1S{x#7i*C4*jEA%+REs zDmyaa7E<@igXVWjyp6sQatHg{4SNmz60hv$20>9!a0aj>=Rs9EFZ|R3pykejQOO0} z`$B2$GdQ6>Uih38ok2Ns4BTi#g-DGMi(}K})+3RzNuARd**f>0xp)?b(Z^}mMVG1S zLq2-9=((b4{GyC=EXB(|fEoR?H#x$2$VY$J-JG7=%M-XcmCey{t4aknHk>=%=u;|K z#m$#kd0shA0!;3Mwo_zaeW<}He!*phXic(oh_B`jz?OW>JBN$RU>_!B=D?spE*t#= zYaf()%TmktDVV*Ga=Ma2sV6f*u3q;+5a0mCOZIqt*!*FWn1J4%Tp5c~)njsEwuz zY#JPf<=s2lI7#Cop9tUL7;8tB?n>=T_i|GbtqGPfmkR>$lHL*rvzTLyOpXsj+m7ln z-spwMVK^cD6a!~MA*5fCKBkdNH+EXZzLOU?_2Y70i+>=P!5dcBm)%S3;a7lo@jt=Dk8rjGo(U42fe7!)*E!VBA_aEU0Cz6;1n~apW z43S-9I-WTAXa&0AZ_EAtBS$Bzhu>Y%kRI)^_2EFF;oFjgh%>Xc`#M@uy0eh6Yf@SZ z_4S&dk(yKO$<)T0fP@PNjWhQhlQU{r&0eLry$$C@DMk)1iTR61$LXGXEcO4VoAL2C z%|Im(N1BlvWPd9$7h->e{R!KtlC#npQWWJI;v1p@K(Z7VUjQY{e0FDZej7;@uEMF~ zXM1Hj(684?0_8ZX9;+GOqK7l9Guq|s90|52ddat?koO_TxbqEuuf47Z4R?It=Szvj z0>godmgzt35i{w)K-XwXoS$*brMu3$7!|eGjI8T1tj9%oiGv?9qU>jA`lX}KsiL+* zIXRq$k^MyPL#ojb`ALD3`$L)`J>9mV>&dM2Hu2Od4y*<8WU5d*Te#GZQ^9~GPQjeM zlwxm!FXNe(QXhgDJ|bUY$1BBDTtmif}*ylOi@)z zoQoZPBXVo9v)ny9I5+=P#ffveaysgt#n?zJ5(F5~urp#?myXu#L&rozCDs#*qT4h# z*HUyb)$4(VV_QJuwf^v9Pm1YcOtguvQM;;7iG5mOMXas;q{Vxe!iq@s&bx^pB0~Qp z{V@#YchiVI%odK9n;8yxxyGNr%JxKv{Uu@2e9|v~ETBV_lgK9&5G{pjoDn4c&zFx@ zw~%CeH3E7Jt-u3C)PWUHfmA1@@a;~82ab+u9ZU#+h>w~3>NlBU+!$-4|Mp8Ax68R^ zJ)5=;-P?<9qviI6e&m)(#MFRp+myz@_4GZLOCop8`KZ{RH86D0!E1WQ@;`d^*%%!! z>#C0M49=*uG9IurBJHc7iJuO6lwecNLd~-{5g(ERZe2N1I;`-JIEj1AUL&xTWcwtp za82{SmHfDHaVc+&;L!9D?9JT3g5n#(DL8^xkBLRzlwE19f63p_dH?4hDZv4u$gH2U zbxsjp#_uEn=H^9s81&-nu|l!v>?kGmhqGMU!x>g_7mcb>{M~t*#>~^Cnf_kFFDa%WaxZ^!`0T*~iV>`lMtv z_`y1D?u`e{i1cpkHQjXw_Q8xe=*ppX`RyByX5NMZ?Zr%n`amSbz03IL&wECxM%EQ& zsA0%IhZ|!~msSTF7bg}IxtFX7_Mx6C%Ej-`xvO@F9jv`7{8k$hc0F|T{mqE{%jukv z>(5FwX2U+c+4*7cYf(UoLF{wmLxNW|DkrHH-5<(~5*RgCFI-J>c|Yy@AA7u7k7o|w z5URytl4v+nh~t%I74_k{|c-lZ$nI7PwOUhjpqaE(W!s7 zi&CT=EF?MshHxQLfIEPuiT}RERyLYWOte}$k~$U&F+XHqQPx+w{G%r{y8QzWGBtd!KHPeOW|y9p)Lr~^u3#Zs;Hf+d7Sk8 zW^;LHjMu==|I|3VZOrXm{G|TJu=q4>IYZ^huR3f=erJdDn&i}feM6NQfW1GIFmp!W zSR%HTjZm~PQ94OyyP=)8g|0kQA1Zey`+D85wr2M8xCP{%7?#1^pIV-lXqZ0wG;m*p z|LgX-#O~hbl7@-TYHOL$tmrnR26_PyWWBWh3lXtGQ6(USc?-z(?KOaloB+14x)MvF zhY&mH1!-|oF^EmnKFN>peg5imFlNOk46=6-aC>LCwoK+DCT+Fz;&AoHDc`T98;=f@ z1dP9`jPb2@DM2*4J}BWbPz;}boAkRM3XuphRR#Msz81=*bremonsYz)g*J>2Kc3z? zU*K|?9z1!sqL}Vk6KLGiYhFutu{6l(kF>s;?i^+~kfm*k7aM)<^D03aNPj0?WX~A9 zKx)ZMg9zxi!~>YMS+OCUCkhZY4?aMu)EAb9HzRk6116|BDS9}0RO+2tfs^9!=?ci=(xqjW^KZ;8 zmS?hq%O?9y7t?m+R2?eNEUdlrHPj0+vIgnCd9c-YDH%FYLUTJSRexCj$N%m|#}a4Q z?WA>iVJaUraLQ`Iy-3(V`c5{8dGaVv94;HXGJl{8At+J>>-Vx7U~I0AmyNi?PQus_ zVObwz-z*>i%!2v&b|PxtD#x+_)juhpp8h*%okF!zxR3Obix&%CIC}r{cu$H!UYXV5 zYBDve&)4O^U-V&HI5+uH3w>=h9odvl3e6D_2WVFi+j3qV%%x@K8-@E#(BEdc+@bCE z9r&P*ZZD=;k2a$sPuCr{ZYUPGxpdne;0#cA`xL~GLWvWYHdml~X!XmJS;MJ6J%)Tc z+vj!x+$26Jn8j^7rPK#Pjm=o54%?0>TcelIxq`^WixXwXHqg7P;3vK{Smt6NT-e($_+LEr84Ib(PKwCPR| zu#uRpxBa?kY^-o!yRiBx+vc zri57jDv5~?-6->Izat7poZZUp-W!1my+wE6H7wAuCGG_YX;jUM#+lLL?n-3zT1=8x z!37V_P*YfgD}r2HsUCXzGJW?;ZkBh0Z+--p2dbuu=pmQC7webn#W68n1-4yZK)6U4 z^$_bD80f|f-K0-2YcDVh<)p1qoY2>3@#KHI(e3~DpNeJIFegBhQjOfsj>Q2)1P{lV z&(hroZ-eYa!FOc`f?Y-B@ChgZ0AZll*Or?@wF*a(^SYKR$}BsM2F$3-r<4$Z93 z=XKfr6^-K8MAt>7Q5UCfet?c#=kE*W53a3>16Gj-SV(XWA?%fjgK6v{5Y%>J0QZE_ zkGKxhvP6``wTPG6+il!WG#*9!0-$C%{}EYy?0pC*h&Xo?48ihV5sy0$sEz{YRC^vz zB%2u}wV)r&?U}X-yHr@|?3jIsc`n2D&#no>3C^z?@ey~ExF3omJgrTRgl^VH%q?pB z9uyqSNZPI)an3tUKiG?*;c=7ZUa5`J6IH$O=nGBSs~bIxEmcv$Jo4>rGQH?|nT|zZ z<-JzmTOI{UOGSzxp%W90*@idhW6Q}7#FAQ<28(t{lBCyUdx$+aHR2JlM3^rkoDxv7 z18_a-2tMLwQ9t54pqIq}@MSE-K0RSEl3V_v(cpRtMhUNK7Z=!DN(Y0DtXFAmQvps- z2wu``b`&FUFvL{obd3-w91Qw5e?7{X^h?|s5c4q7N6{pleFA^xWne`=a|xI@{yPc4 zUdZ>RcB1-bi;#Gw7gf8)etpL6a^TsZQH0J?kg0S$n?L_k`zAEb`EYOMUeiTmBaONU z&qoFb_qL2GwQr%T*+(wF7@%t2F112pk!vloG3DqXmTsXrm$7Sj2g=qW!WH3Ise6_? zEF=`Gp-gS><=zXTA&e|THIlrBg_L(s3U?|mdQwd+1Mq=P)8d#)m8rE@@egj|)4|cP z4TQs_cSsLu%a?@Nx>a1+07;QNn3sTzV#$xK^I_v?;XAYrAPKKk?Y_&GW!1s;aAlYO z6Vr;Xx=3&km_9m*FJ$`~7j1W}+HUY7!rQy}SPQ3)Y8KVcyWG{RPg`3Y*0?;$`2{BU zoP8NmWuTbzkQSgU2&gNG^0-=Ob-a!`l9$h|SbA;f(jIaD!-R!Lesph@-pRhu`rggM zLO9LRz^lBre)NC9jGhH(R?rIP2NhxDz;VKHrjW7G2T0L;IhYhFXq4hygbq^$WzNs- zRXU(T+V9aFWizBKN|35?y<0?NN~%hun|^oKxRK+0F{L$O?U^_v8lylw1sjX*!YX0} zOy*my%P*LEE(tJ$6eS>2VTgl!#6L*Rr)$L_T!iF))U}Xf(%)EW(@~_>3^qA|Db_LO zX!_;WJr;hb&}(6ir5G2u`cA|juhAG(x#!y$sRk5Nv9aP0t3Hi;MoR?hQw%3F+TLJ{ zC|?KJe002zqiBL|XbL~N*h#@Y21c^+87t_a4#K0Ud+)^hTlpG z(5e=P0|9ZU*cgoI_Y+*fv^yE3R^1h;2{%AFgOq5(`oe$Z;xE*hYyhrki+q7q!TT+g z6-oRcM>`5I(CqJ3!V$vI#loA$g$aY9f#Mb>v<60(pRq zAcRY6Nzp|!N1Ao0iB2n57QQTU2;LNk$riY zwNr?BLAj-C_O{Dsfue&??ZLGDT13Z5PE0Y`-@uJ75V2ry=cl6Dl``Buw9h_9&Geri zt4uO~gbvSXOk8B7{GD}Pxc22s8h<2Uz`cSh@PZIyT zV_3T`wBvVh7)b!`&4GqpU{{dk$8Ou`@7Cj^azMEIUdSL<5(i-FwyBvc4ZH!*D*Jb! z+X!DM+A{~H_x+-Rh4gf3c<|lcD(&~8)l;f=@;MgPDQ;)hV`gHC^~g1j%peC7)4iuu zZ2xSzE3z;gC4iZRk;Nh2a5hKmLh<7?cS-Mk5*#k$lp(hY@Yx+lx9z)hX`8k6cfajk+r9mI zxui&1Cfg~S(?!Cwg85u9i#F(CPfzvR+rzE@l6UsNcSV`P!Sz6>neWaN3V4S-SJP-5 z#0_vH0Y#7sv2RXo>m%jqI<-sp-e_5LpC~WqHCw7;Xy}5#+Dg#dMAvG+FGpB-c-c7 zThd2Uj2bRK0^=wVzMhxpU2%zdq(wc=QqQv@`c3H}#6DslM$`T%ewgEnL)vT0ULv=# z4oEblcV)If<}KewdWQ%Ga`meMl!D}Hi_j_CvfC588|;~_Zhiqaka-@fjMq#~3mzyF zim_~Hvn#E%HRd&+!P01dI@z5nvXxemqS-qMKRt)(Vjmnq1*Z&FSt+oy*cASDaDkL( zuLrZwLRMrz*J2%p{P$)*gN7EUKe0TtvrTC{A~ZZDMJ-<<^7*S zLLEZcse4icIW?ZO%VKA*n!MhyV?GTYMHb#PP5=ApF2_r(1;;+IcKSRX?MM6Q=AVjs zd$*ovbcYIVP2G6^H+4uwClz-~q{%aH@V8nS>TmpA=7u#it#5@+YIgJzK}B{WoDdHz zQA_YPVfp}T8&uYd1!wBxskKeU*JD|9sBL%OAJux_N;aW?!A&#`^_uoA}ShN7@O5e>L0~ZNkqmrB-4!K}2wX z_LFO`CoLm*K=b|(DUYSMj8iA-1k2ulSx=L?yRw!tc{upi~O(Qt-TR48&Hy}w31G=o}jdziJ zK%Yl$j%RrEtcDb5%rofz47SZDZ|}Xu(KY>Zbhv@V1AP%eL~m`g>%X`3Y^w;1Slm*+ZHw%`T)lFkk?j!OiJ{4VPQdw zb&&f`hO?8i69~jI@)0s?z>2j(_@u5TZ);Lbq?&vBe@*8q#e4r_T6-bJ^C8T)5 zKND4CvmjiQGx9D$bgv7}1Px^lQ5n2IMLY0v{UoJF2GCc~Z12T8y$E2E`*7+pVAJ*< z+J5c@Vp7-j-Ls97k*%+y0XR(u_s?g*lC8<1nZ;dIIXhOR>!Tc# zu0C0U3&-7^=1MQzP;S4Wg}<)&qw=MJ-G^`9`u6+7n|9vb%!=AYL+1^Hq&*hZrwfdf zC+MU3;YeZs<>nWhr~etqYSuUZ4_Gz5q#{X<`xiL48 z+vh>Q&+X+UFLHRx+_E;h$uD$X=|S6jPs0uxx+@iRYu@?b9jR?EC$EV=JNq>`)=6$N z-)AxHUU}J~3&nz!_wcvg|3%yjG6IF}Vo`y#f`uZejG-OWZW4YJjO+@Y#{2dAo&4Hy zg{Tb`QWE1H;q8LJzI}EzFz@xrsea9n+m^3Rem(iwEn)BNehbgzopAHox{xL+6IOMD zofQ!b+S%716Vl-g(B+j!N{lhtU|MH?7!PjU8!SzU(c5(bDm)H6ZmJUXu}$oLe0g*V z5WRi>OHq%lTQj9`d29RG;e)RtCc~gc8|yfC%x%1a-B(X{hGWyP*LMTRj>c-(nj@U+K$qX_Ikeu z0p2Bsj_L!9>x~} zKtD^pi%IS+H>Kp6Spfeya^p)#tij)yT^*dfEkQ$9n#E4Cv1MqMha|_NaeS=8 zawGME>=mxHqvv_B93PWY1E<#y^Gr`;)d+_mm|onhN&d6DmhMzm3hZ-zhTfdFG`I9GiQbT| z=yqbZn$DS0QCas$ZkwixHR83R`X4Up*xMs^|ajcp8}h0&v18<%)vD`sD_W~OPIm+;l4v# ziY@RoC+So6ub5r3A+~l(LQy#_KRGUyCr6ebt@;Dyf%JIyTePWgwr{_L)bijoJlHU7 zaDDTX7f%Y9ygOR&69W7Kp2i_HpQhoT0Z|y^+)33ACN+Si8+7m0DQ!nxP_tfCaQegn zWnA|h=`z*IGoOl`Nk5wsYV66G@L6&6F~}}4;?kGbVkS9L)LS$s6}hIzn-LDDJ@cOl zH}$b<>S6ZHn*p*EXjry;Rs0j4>>;JIG$cnSW&2lfyD)L2orGONa-qyhavu6x^krkl zP85yVN!VU3Ig#lP|Hung<6U8dLN~VLk^2JR#@?m#5c&FF!ui6k9~)QQPS%^jb)##0BYb6Lkl+8| zfpF|9_-8x`hQAJ;oC%>*r{N{>p(UqLf}cohwb_ny<^2IhKm$jh?^v>TQ0~6xD?Lr~ zzkFDm%UgQ6TaOL}aq0XLXBWyo)w`Ur=CX#{wChz~fu;p# zN4pl$S8yiJ)c}{hB;MtAjr+t=*Hy*RLq5g>4>{=$)-duWX`u?I*yUUuL$riC2H9G& z;dxC-px77mtfKCTm3w0Ug^abmu8Uw5P+@T^3M8WdAD zGblMoRPSU#e)%Z{(I#>F<>(9*FjM#}Ehp*`|6o6f>qv9n4dCE|hlp>Hcy#Ml-1V_EXl=3(fK02S()CUh+?MzZsP1 zf6+{C+l?ASuR@)kpO5)#?_szX?7sXX-*Calc~H?$!#2L##aPi(`SIVhn}$AgtK0@z z>IuZ8<{k`D_kWhWHBEmu zD@z};_5=_|DLW)7LO%~^qsU$(dd~jw<8e!r2Eh`yZI~}su%Egxo)Wum|7M&|x=wK8 zNuADdCjkU=t_gi$H|hTQ{E98={BG1M@sz`%UVlpE1t{*-OXnRhfrQ1TO1(*f$;1ITn@XP z(uY(e0wAuGwkWNZSr9BB_H}UZ=XDl=xz2Vs5Yh|AeU#ny8gOGj(S~ckgZR>3n|v0| zH=Oseq*r z`xUhM6Q$(F8QJ@Q)LzofIX&S>A6WXoYZx2Y;Q4H_nt89&`N%B&j&A4>;mp{@YGFqK zQ$ZX4c11M7jPs*=)4Y^^8|wZ}_>a$CR;7~$2$J#-P>$vw9h6l=fSG>$-f&f=PiDW9 zS}@;ev^n#Y-hTe_ub#D~M%`DaWLi#v6}jN7d&Nf=rnS@8-#C*rtd3?nm*Hp;?t3%l zjA9mrH?`&rEpc#dh-+<()blW~6zsh(v;y+{T2dS)Yff*Ba8)4QC}bJKSz?Ee6ctZw z7SRe0&k%l^=g8EAII_eKyvGr&j;SUBt}%F}bOuR|`$Z^wU@^lK5Sd;n4cP_ zUuhv@4r@3b=y$BNwQTSzZgBUaUGT_?`s`Ry-+6~KJUf&(5N0{Zj}31%i9gvSFiMH> zOZE;mBEBiq)5?EUsp04oo8*#haIB9f2si=MkgJ~|H6Ht2ksE9%d^ooI(<1cf79XoF!E(D2p*Ldh4hrYx4jOsW&8~;E z?ckHx4A1Ca9`vTp$FyGeopEkP7@X0`92lsj5AW6vYm3{+h(!%V{GGqibv5x$+4G~U zsDAONC(XduA~xcm!UGTQG~U{(0HG9%9#vW=UI-%IOmz<%SqHTJD+7uV$>)Mum+aM?FI9^bw=pWgsBsmV&9Ko2*2bE5c~uF}!@_ z&IFbgE03phSupT;Cfar*-;{Xx{h3C5pxAQ)treECd=XUMq44PM#PO-EW=``hio9$m z5x8acvkpq^AcOv312^HfE;^k1^V+8sUUu+?(w8OYzx*PqPj%_n;XLDaZVZUr8}?31YqRPQ15b zjfq4?YqmvAzW!dwY$QdD!%lu+`rs~cekX$B9DF6@lgy-HS-$!H8Ygbiu72le! z#}OW6M>KQi-wpFt}Atx|8D=V{NH; zaH07-IMwasv3vc@2WK8mJL^|g90(rN7X-PX9Q3h>K>IArJP(>U_JlK8p@&smtLL}A z`l+6;=URFr3$^Z&*!%YYHDVW+8ql>o%BdpI-JAZ-Kf}`WkHOE!Wfx$h$>3GT8qn0T zFH_w;iX;Z!q81ufp8TOW*0a}(9?*emn*_!PL9al@sWJcv@-kn#ZNbpr|Mg9Nq~Isw zHxu!s$iHA*3gj~M`6u?M1u6eB{t@Se?vckWlTBt_PP|ioAiF(x$wO%QfzF#|x@sZ! zb86vR7RIx)8J+@;{h=F^Zn}_-3;Iq0=3@2HA@6^rl(w4@YMvFb>9%tNJ_ld#niN@1 zXt)3c+M}Aa$n)pp$M4?!rS@5kZoDu5pNN;Q5m7b9E}%EXL@V>wqd%vgm0iSRWV<0y z6YT^B9D%_AvM_CMtr>dGC#^fw635QtQ$>ao_R11hA?ijvT0?CAk=>e_l^hq?k7P!! zQR**9euj38WVdxY1#vA`o4>(d*-~)z8~-ByK_t@}-}rL^1?@1*6_5%9KIkxl1WGs5AfqD+h++s!3V{bgI9U))*kffszVm3 zR&zU`y?0{+;;A)Fr85ze_cLmerT~0g#Yt{QQ5}6j@H+Pc^{6jrbV#^qt{lyqH}RsC z>7PO5R2fgdEkthDlnrEQ7GG_ATZqvi_w$urn4l9~;H%HP>90fD4HWkL-K=4Fy6#X% z`=G5kXNZA~`Y5&xsEwFHPM3)uzy}wK-dXj}x0cd`PJAaoLa^dFtt$v0@J~{pyg|0I z{Q-#~lq02YlH?^`aDjlb4`0>`T*EA8WO|JxNr=pDwx~Sv)$M+Y0>X=H^2xHm>U`lF z8RqTAyP}1XqO}RJjYM8bv3MV|)YnehK~%!a@oo|icV(`1gd8I6z(=~hHM3mJ!`kCo zhOZ`un+~5od^=Q(Eq!tW@v zlfa)R=tggEMtO=Xjl)m90UW+3n7{b!g}ZDKpfoA6Hz#pI&!G>BU!}_1gmMygb3%? zw}~$j-yoUIg|fqvs}oGS#@Kb^-Z^>LLr4=K*3^E7Q^9%EgF!?Q4?+4JSzlBpnGb^1 zUOza`WuQIyap`?kN4PB_k3Of9y zHH%k0Ds{sO$dUuIf@C#HM|W8c<{~?1{=gU9Oyt7X9@2Kg_gpXxNL^5^%-~@(CCIT9 zL&*vLy!J;->&V)K@Pl2Tsal^$l<$_$qb&Lvg%3rk^^a)sF|M?^z|??^HJsR$5Dv$R z-SHdzi9yh$Z6k8oYO?QTuYl896Yt8K@59`h-%s3yH};n15uA949Ke4#0gRC$2jK)> zWs>h0m^Kh~A(g?LW>97ATK*gzfGZ#lhekHeN0Vj(O2$k4!>WFvJ8D$njI6s%&rUEd z`r2lPq9ZWti0F0gODo5}vKYfQ-iw45(dCi^JCkAIwB|iSv2(wPLp&ZFsC&&+y%*gp z9=9C=>4&~|8gsV#jiwtC%i4-s`py@q|Fp#hayb;_#~UWTMJf~RI18r60)ur+s1ut< zJSI%-Wov+xFDp(nc_xx9($ zz2R@Ma|*CHZ5sRZ)ziJ{9s|cdBxWI_QH|pt%KD9xPgUC9@wgdbJ>^ec`;TGr zpH1Hwe;ApW0=Ys&Gs%8P?Ha*Km?^p_{oS#(0Us*)Q3#NgOm@Nzq!QY)4$j1KnWyCX z0=mi9Bp1L3&!WrE1jYn}P_1sFy%P3SSI-U}xh-Dgnp}k}7JVxYYI?5@&Uh5_ym*yI zCuv5b`OhL=dU_WN9BZpREgEf8^j(_5mWOG({igZRKK|wRY{m(r42LA|8V}OGw(#|% zVkZ}xDfZXK^Pqu_)cg{m@2eXQ76e0Oq$f1+5igg^GKCL%L#*NF5HDQ5r2GzG>4>lRJQ z2`MyI^iniA-k8fMELBpt*()ae^sGfikl8!hdzyJQrZVPj4X_R1P;JG}GG=wJXcn~; z9{jt36aab~-8W{!n$0G+i`d6?|AeM_pXz9-{pZko+r+N0FaI;2>Q(Z8=TjXa&XKlB zaAFTxKZ)tICRk8#;-p~;WY&_MAms9SZ(4JHf~6a#x095Ej^??!o*vskJ7hfkxRte7 zSETd)FSkT}X2GAsnypR=#-RvD)uyPh#}gkOWNH3(w_TK9T-o(n$P9NyV{{F> zgs7LN5aytOQj(3QO;Q`)%SPC8prqT#nL_Tj-xiD6Cc*MYW(>~5*B6kw_LMwlNdpHU@GX4yv1O=nnygs@Nl)MRa#ZQLJ{*KP$~4AN7m^J#=?JUd)P+pzv`Fp%M6I3vj z-US!6DgC(&la;pB&_yE)B6H8ZktXj_Bk!oBP##w9)*p$*QI6dgOK(P6`z}@_;%VCD z74;MK^Z^gfSfLZSaxd2Og4p&RLHlv6*sSz~#@0A5ZYN?7KAcmf*8ZH$HSMzceOYry zw~U^o|B05qvxqjkJQ}=Wb=N%o`9FFce&vq(wYT5hTWn10H21$1*5Y5`U9EY!k6Y=a z-sp(_`|@J$vPwEiZ?CGx&qDk;%?I`1z8e3KKKu??NYhu=GyaFrw`t#9p!xpCPsD~T z!F)fTq(Cs4-;E*|#zL_?g8@VqNylF60{qn;~^Kpt2bI9#cbIJlm1E1Gh?ntf2 zCvdhlvLm3KK;#=sT|E#T$8f6GQS*289ghc?!gonLwSk08qWsh*Qg@nge1c_6NS6LC z$$&ZpSw&fdbb)Kpb_u2(>eV`B^&=eF43l{)^AeW_XM}^jRxg*aZ$E<~tw!1x@f%sg3Sm~KG6ZA4O6nWM`c$TlW zo+vC6`AylI62e-<*a3j@;x(`1AQGQkxJkE=f4*8oChaC_OVl9tn$LpNju*N1_4mM4 z+a^u3%t(QJ_LDmwJ_k)a=#!aD5q{*Gx)Q!E)GaYei7)@XyfsBHtIt*5X?q~zI_>{( z_nuKrrfc7~GmfGlMnnWc$tWV7NE5-BY;^zu0qIDKfJhS~DiR?iDnjUtN>KzOLW}_c zk!Aq`gffDPbO=gFA}UQ1OSmM(=Vb2p)7~@dUH7~8UibaHUtGw#NUm~T$9dHM4|(GL zR4BRi*s$F5k@jx2LXadNInXSkYu6p2qfa90=!ggxXu!4D*R?ZP&@@)~duCodzvF~O z5p{m1TgmUIOI?0)Ze{m_NM7M(TK`HYT!rsm(BOwSfN71e#Vp|8NS~EYhWlc=`mTVe zji?l9{fV-7rt(1*KWpM~pS|#4m!e45oLRY!rgaB?%8qpNepv{7r z3WAdW#$Cz?oO7w?S3T3bME&R+*-gi%e@=LA3psqD8WId%>MPQ0INy&=>kfmau+sF* z@8t(fl27gfcx^X*x8%cnY1W=ww-hZVXh9D1{Yf{tN~rP;IucP%?KVfP(ry_gDgBNDMz`8cHtSk zPc>Y&GQn;Dlu@jQFkuvh8D&j_K~N?&_y<3EGD86G>uOoe$6fkraqEac<_e{~(TkN* zV~#dSNl4r{dJO{Q)?a#{TA(?D{aI>B*du8m?(>2C+8GUvshG&cG>SaI?;f$|+D8EX zT5MyaVddWq5LF_Dl$z$zwEBs>hXPnK!9cz`y~#6CBj+;l%~N7Ybk)qV(zoiZo+->} z=*8|6k_RTg(|xlX93Dgl%>swjw>9{FS5Fq4;v*ch8X+zjsSE?On}KhO!RA@-4**%J zt?BFYz;M`_{Y7zu=i$a(Z9_DJ9*;m-w=P@LW-Bk@Y2ojnt;Shr-bktDRnQCu_^Moz zXrtecRkIeH**8EiOqUVuGBzl@40Wy;p#sLw^@uAE@n*t5B>8p7u`j}FLqu)pL-*un zfn9s`YJ5T`dsEc|v;jfoHA;)W=>G_!@{rI0v^621_BwNUq=aLzyJfIWE?MyN#BO@D z(d(j;jJ7Y&4L3}?-3HZAccGH57wx69K2`S>6+Td!Dm+VfwkUP*@H~l{wNtWtCX>?< zD(LiR>jBBN`C(mWsuk!*7()?ZCZ+J-vm!SOV`q=?Qs{0LRzL$oUqB1=;2oe1km_;s zgZGI;h@a3qh%Z?w#Z%G9HcMY@yL=R9@(I`AZAjPoWkxo7D`EctaYqCT0RG6Bv3PqQqzB8obm%bs>o4iG9#EB5IN_;>K4_99>Eb)M<+jJbg#$|<{DyFb%6d!)i;wJl|& zMBRfYJY7?p+mza0bY@o7V@FBKxtJSP9(4hcdkD^JeLPdC#(pD}FSs=F`}WSk4?k8# z=0-Yawv0=1h-*L(zCDob>n3fD7O%M@b^=$iHPF zTAg2z0p{)5@{{;ivvUp*F0?Yrdbm5KZoP#(uG9LG^kuegP(HS%e0sjb4%z;CB>QMm z>r|<>%UJo<)~S!jc&@IV2Kut)B@0SxeCxu$Bxw8ICD-NGP5a^$unG>v@Uf?3>7TPS z!35N|S+6CUZkJ~{@ypT}*_kq8kN}#!a_WRJ22(IH=5sE4LQDE$wRAlVfc!vydrk0z zmoUc1JFsrtCh=ydK;S5qy)HTe8hcnEf1WP6HSrH1rao1?SpJfaWnvx$K0{^|cuDSJ zrs#4(%4yvT3L<0Zn~|cUb-))N;HXsnK7de|Bt`7Lqczw`1}(;2Yyi&D&Jg|y&N&NS z^hZEiDdP1{AfFj60^}SY2JXiyu_b1OoReRS_@6p{Ehd!_LSb#kz~&xd#7c2Oak0DI z(A$y%&L|*A*qB!4&IkX_1gAf0x2{zw`axyV=SR1rLZm7ae}boKh(M+oIt92WSc#D} zkde<~08o{q^1{kxr+oMmR5dVI`X4>~0UiUc0MJ_8Gf8dUq!n1pN9PlWjev&rC#+?= zXBtSRUyhnzmtq;pDjzRyG?Txy3P_Cq51i}2=}vSH0cskBwGBtGrU4FshC=U=dmB@dqck1eymS@t1dX2wc#SBo!wrUlLx zJiO<_=7~-$RiXx|&8Wj&f84oCZv%$!bnbNKPhPB^F^qxU`xSNg|9cpd2=o29? z0n$W`2q19zNRVRyGc@z;3q|OfkiZ-C{c5dD;Q3EkaTWKbqjR7T9Lqp@ty2V& zf*cSMtlE}+%@dyXGL z{a97gw}O+qXG>Z+TY!`MHmZmqWB(tV+{TLEE`pUL+;Voi*(tA+thGPaF=wG+?)arCRLi{`1Nl-u}%MXT-fUsCve+54FrgP+U-jKMkQ;va%HDH>WIG%@h;c{Hf>e+i>tZM&NiOQ zs&U<2e(~=tx82_uZosnqOH2QsJ|6I>R`#WRx$}%a4gDqyio&)-(iKDBlb_Q8FT&}W zuJ}r0->;b#oI3yMYJ>jH;Un^YV?h7L(gV)yU+xV5(ZlDaDVZS6@}aXj1zV33Z)W-n zsu%iz_F_LAJjn%y0ApLvkLyCNlXkFl$mRjIz-C&Cw|F1P=`0K#od*h98j!MgLB}Rm zL}R{5_l5F+8Qakg<*ePejAnfTeJ&EgP}6bX;lJ*yB2BF{bAG>FD%uG)xRgW?()=X0 z{ITlAw^fAoAf5^sM24*F@_;wFgQO5;R!(fCG?>+yws8bodKk5g4lk2KL9eg2?^@#* zWpGUVfL1v6AH6jn|4pB6G9}3-g(m?Doyp2oN*Jq+b4sjk{Fh7f{gJ^Wl;)))BVAt& zSWa&XA}daufAeDZFaN8qZSMbr`*8aIhUNd?e8zK#4lLNG6%gca_ZhXxv+~LsBQ+$HTBfcySABD&9yt5PWC(69^4L;A>KtymF@(JeJ)}f%H8yH5KdX zp!FgQ9`PFq#(FbT(D?gvbk6SbQO#{lepwFOSNqT-9jm|D{fm7^i~}?O)ohLe0fLk? z>^=az|7Ak0n(=WphcQtrbmC(c_VHV$zfs0(r;rQY0$5wTWoi)Q!e!#Y9d8TmOqf)aR6=)UfFVjAv(B%_+tu63a*V(vmBH6s31Qdo85Xf$+{GwmugHM`AXTrV0bip9&}`q-gtd6GjGY(w&q}%~jBToX9J@@C}@uKH7z8 zFuuigNv3RGKMLXt4Tq0Hde zXU&}*UR~|DeAgGdYV(!BP?87p>5T`~dUtBoH4OKud$~_GXT@Hs@H|j6#d?rPWJ^mU{wlV1A^Ym2qI9aEJG1cjg-DTMI(j4m5HTS~Y>ScX=eG2=FE%8&4$dGxyfF^t6jnPRn zEK8I9fp0F!$6kCM6C34T?XJhK%O-I~)AiPq!l&?vtn9 zVtV;9ZC+Z_-h?dl&bsD5P<`Ws&wT9Bp3nJOs=n^I)E80SieA;5Pkc=r=iIkbPNqKpify}4DIa`(v-B@6X`nzQuF8#=G*`za@ktD)%g+CnvU zkGTZrIAoqN^ECkH%YRiPf14U%RQ%$pORCt46m>rw3@Mw{DK}VpjpjdEb9;oVT57NS z{VSi=8nD?4a|)>j%(EUN47nC`Az0x=Fv1Z@Fw3hAjV&n+eV%pvieZ9S=qiAJ&Z+8=0$B%rw35k`JCeq2gm@Yn6}9DNUcQ z&<`BQ{472%C+Fl5Lw7c%-FWZgAAFDA6RWv#4?D#7Xn@YaM)@?R`)$7s(Gl%=8+r{p z+_Uz4v3G>sJXYgV{wOn4)lRB-`tiIG>bFui^V_?9G$UhPR@gS|EavO@^IhiaML1#` zb}eS=CgTxfepKX1*e}T;tZP`t&6faX9F|{=3CEH_o7E0MPFoVDgOUwwPVEQgzJvBq zTFIM(f{lp>*gkwI{pWG$60QkT5w%nhe~%KjtN=2e;z!U0esw39=sDqp5NZO^r66`1 zL%sxPb!v4Gv<_U9c<;z{Jxe+6@Pp@c0g5lG#rjzKgaauC3Qpc(GsXYhxc(*Q2$C|v$R9g3^Ee`h1S=j(3@U=%TQy>KMx*R^7Z$1rr#%~dPkph=#*eL)tu=PIjE50X_gjf|9yKT zf&K#pB5F&;ryh?=yCu^+W80X4q_Xn;{aG7*^D2A!uK?$&zTla%2x8S!m9!U(FbOww zA2@B;=O@Lipf7@Yb0oG+2f;L`CjQ&kKO}Yz;=@oPA2t5kEBi_0P}5r5LBvLD$NV47Yqc>>~7yOw5dQz$f$_yoyw?| zi^z{_JTl9gs;_@Xw;mGsLn@XxyVqyuhN_AI(1Z0}K`?bKub!CTID&x7Em=Cadc1dsc+RGaxo z!Y#15M0JyD5baqf*xW_2A_I&k7Gk0o(@78e7AytcP7*rMyvb>0Y?Yefzi>&+1)!vy zCBQNfb>!4K>?Jh3g}<0qjSAyZ@4zura2-a>SLqC@jnoZQ&+DlAq5-8=u~a;IDAZRU=);L?oV)48W1l`hvT`(yke zJ}NHD$A4e^klsiNLM^?=w5X>n{GdGib7}H&r)(^Js1*LPVE0Bm%VX9}_^rY_2UV({ zSN6pi#_U#1oqi7rn9QIr8W}-bfdST5(pXJ~c`?$=@7Qy7+G2WcHC0{IKjud2r+p%4*4QKDU7 zAJJkQuVw*%c&;ppvcau4P*NcF<2`3Y;8K?@Go{MD;aQFCXw#=nkOlviO%s{9r0~5E zcabx2yl~O>>86*T*!84KVk<7d!ZgG!Ai0&h4SuZ3p*-r&VN4Yh5#ZB;;_l9#g|1J% zzgdnax1vo18aeF?ZX-UQQ2S)En;ZpZU4^jB?Sp0Mh|S@`{AFaC*3h{MpOd3w+>`r= z@XDksJIhZTMTe$$+EyO5+cbT{s#nol0a0m7s5emxwKmJ;y)=aTIQ01V#_IO|k&KB_ zI?$jd(y!Vp<0zBb!!7BxTTO1AKGPP&v-UlZbs^Y9<&Qz;D|23|@H#cZqf(#pdRnRd z{F;BZxPMN^uO;DJ*5Q+Qw1sEYguMcp`3roCa1d-RTEd-tMpurk*aBJ-9QXp%pfE6e z1sBuwm8%IbHK4MFLMQc8KeN(iY9S$}TDcydvDS?As2ldlHigA;vmBP(ngjs+ z*N}1F>3(2fpwszU`?IkzW~c$x!-)+xf%W>6{YN@nCalg#KYP|X|5{2eOpg!N?KZBy z)Zx6V`S3HUb?$1HRmpB{MoDBL81JV`zLG(kQa2Q*=aT7JUA zjWkY~Bhod09X8Y&G31OiA`6u^^EAshBs^jQVDLZzQN7Khp($SUh|CPRM=^_-M;+_kJ7{zx~RenplKC= z!ZZIKx?o_dP*96)p@~E`t~beC1jGlWnui}!FyFKXqRw2Dm7XU}l})O>7KaEhQduj3 zV}}raiXqSvssnp>PP_r)i8N*lX-QSzD#NBMyPU$h5cSy1MX5f)aCoY-tAOVe(!u_P zs5-OMM|H%-ouxsh`*we?GM{^HbbO)rbX~;>71UuKGCIF+0*EkL2fivRHN@1tcS;!I z1fDHhqWPN7bq!KgjY?F!O48f7+ViGwx@R8~PH0IlJB18-;lxO+${oF{8%-GE13rj_N ztn>6ayXqq=JS&`wsMO{e8>@)Hz!Z&cAoS=0gdPvB2tBG)_9WfWy#wn)K)RqsvjW4i z6W#!$;xfp##S}#SnpeeqMIgAGMVG!})D?`KTO5Bl9)Qo8!d|ND%PzeA1Ve6e+)Fse zZ+iNwHe_l-jgz&jEHjevc~bFf7a) z4z0gnN;tWb2U|cLj&jXun)Xuh4~$K!P8Ye-{3dMoz(KkE?8~Iy&xJMlW-O1=yqO!p z#G1_B(+@MlB;*vj?StmU2qHNMh(`QSjcdVf?Ouh{PWS27as=pSIOghh zLW#`D`M$LY2x1=Fr}JagA)}A0c{oV#M155__91P-;4Eix(;Ql%g#iWxw7k&<{JBWi zxk>nIR8ZB&NkxGdqx4Pq=kB;L`cWvvclgt`#|_u4D&I98<;CKM$%VuY$_63=XlvGm zE!2xpgpFXDji@62h3^4;VN#Y@ZG|DGfmVV@QzTWf4TP&=i4WBAvoD3#9_|ks0x+N} z78#;M{mGF@C-)A^!n`5VNZ-6~!`@A;g-pBq@s{{+ZU@g^=j>LfHv>pJ8 z-0)kd=25%J9805!E`NNcXXK5{wXnC9c5-f>9{UQLRYC7%W`)S{Y#M@R{RhCwcy>jMryMn>ds=fw(OjXx2+Z+c?HQR3;y{;7u!MfCP-5bJFdu;fM&TU z#K!P=jr!5_rUZKA?wR+IlfMpGgbmEQ)`3`#d3S<6$Cy9|2>OJveC7EC5sBE%sMERJ zvJOJD!tFZxSTdbp5VA((gxBhdUrb{}va?8=TP($UAUc>opDO5Dz;tSAWPj;kKcZIA zqB#L>uMYB%o2yfhO+RrGErFLzZZf-5)m$ok-PyY)?GK(@!eQHOp*5AE3sz?dku><3 zyeCxKdpbb(-q-(io?(l)idK^a&(m4&%6&34?Lp@do987(G$liZ5s+J<&a>;X;_-N{ z$&GNTTk+x1u~%ggouZSN1?oR*F~6AoFJu-=xa2T2|MjhdlbY+Z!9}A77OBzExX=My zSi?1g00#qucj2erU5?B4Ob-n&Gl{->^k_2i2q;saVE4rqf+ew%Oe?FgBPR``9J_++ zYukU!aYb-`>$jH}EMWG1llKi*&Vv`6WDGsOZIF7OOf z-}rMe<}Lk%`LJyxT{ZN3W{K*)AwXg-FuV}r=9XEm=a8lB@&Pk};bhZ5{}fMnN%9O6 z?gHt!%)b=5Nnc`Cx4?)m`S>_)UuT~RPzq#gqSe5-hSw8;9T2_6suEvd*Tj-8P8tTd zcd!C01sehb4kmIJ$xl-%Et3qn0~_h)=TEg13(g9SCBLKhfD3m*Z(z+(9#zlx-h)b~ zMv@Q2q&3CLB#m}ln=ylE`=Mg)g2QyZ%Fd=C zG%VaVaWF8VF+Q)X2rpxeZ_(&zvJ?_^ z+j=wY+Xe4Dcr5j%Ta)(7oQm|WBV1VJlaPu1Kn7th zoP{soV#L1!b+1#d1RFYG&j4;`V=YKPDR%EJ*_qMnr$Fwlxo|2mqr*`qh4Y+ia=lX7 zIMO5=n^m$<;#{PyUmq?aMcXRmsoDZapq8(3F?AxP6m>#vSMRP_mt8MIu9T*v7wic2 z6S@E7cdbs^soGEVQaYZ?-cjf=3#J&c!q<}Pq+r~H8fc`uids`4?ZMVsrV+pubzTtJ zijPAK9&%j7IbR}JX|@ZK+%k6|8{s(Sf|goNiePPW>;wE#H^_sb%+BgPJ6cIF+1W^C zecxX-0AtaJ!}D*(D6LK3#VCaU|0j{?ozAFEi4tt46L4uakY$_D69uOS@Zdr|n$ zc*>pf7=R!~9i_3}0;?@nA3`PYM_Yw0yKDyt|0pa-em{H;Xy|X=eYa3!DNsi)t#z>H zbCUy*t}>g)JfgYh`@n}LBQEt+OntB&zTYdC+I1}E1|bwz@v1&@PkQdOZBzJbn&D}s z)88*w-W;m-Tkis6K7N`zz25z{aq?lbcXn4$l&f2wYS3&qOmfQwb|ruNuAJf1-{>Bt za6vACH^pehUL+wHCtQFG+p2oxn9Ed4+5>4XqE0#IT);u$4nbUJwK_x=Dn5qZF<|Gg zaJ=szGtP}*aNTB<6qc~K0fZm-p3%l$@{8;GWT66Az!O8sL~-A4-P^MSgLZuByGQII zszEXLB@Lq;m|xKP($|<@np`=3jeb#@o1w%ifEatrYlgcdW7fyc`K4hRTVx;?7dRPXKR%T|np`)L7-Ijr9*?VY!c?;wRfBkGMfpJm)8Fe7?-n$49=VN>Sr=?3`+Fr| zi}fU{GgsSE58E6j>etQj;9Y0>E8Ugf4V38iGr8z$IEZwJmIk>I4qz0ovEfBjg1W&{ z&<6R)8;27Q4GQ-}xMr!#tMr#Tn=(0Ihn@T9_ViKJ$y46R#6e7*4igL{cu%=>kaE~t zEV8tIrq4z?9aB0gEE&ZZL1^V@SdQJ(igc|%d)5e+3 z=;WNP$^2mGbjaP628(e0VsAS%Q*pKrd*0H2V9Dh<7SfXGWTZ@Dpz9!YVE7P>348K0 zg!cqOI$$AVpb{QNm7oVvIWDP0#IUxYalTZ9_M3y#F5clw8HDAqwgd#?qtu+=m)uV+ zJ~=O5BgxtK?LGA#`^6YWm8cfQmIo3T*%T0afyBixlR(GMQmipR3P+4%+Zbyx5fnHC zyzWd=6fBu2>#~usoo`1boaGVE$6ZegK1=rD+DLPyHcw&xY;F zER*VA%Tf67JAnM$j?YGY&J2Oaf*4C=5Uo)pzjJvHBNEm;cX}2H#r*XCvYe^|XckA}>O;BnM?6cyir#vQ!%>=$<^5R56B&9S;W40Hk9Gl8P$1AXb( z>U0O1bIE7hG!>~2oqeO>FvL^ey?Ud7a$!@sH$H7eO$i6&OXk_feMd)$l za3%avX82H1K>Q8DQ`b8Zs(80TZBk~%Al=q1^EeNl zUL1+F0fW0gi7-)Y5CgUKBFEjQUS)yiJHIJp4v@(4UbrO+oN*1D)ii-Z`4t{~O6u`~ zDMTkC+q!eOtCrh*u)7eGRwr-9B&{EHzvamPkTms3pe!^_=18>KO=c0c;8O!pKp6JgKI^ck~O+AabuzR`{9tmQ}uF=eK5;1PMd1JW?gfG(m`c!IVbkgg;12HqMXYb z_sFZ%w2osjW23q~rX5FKqMxmZ>N(VWeWIin+;p%<=jdk_1garhD?b+qE;a<4_+;9R zMtIOfg#*+}-Jy@;QT@e+J08tm2yj4nw>~f=nuG1PggA^{N7R&d#>3|8L7~l$(>KrN zx@2Q(fU0pA9Z&8;Y!xH;luqA1X7wtR4{BV=+2l0K((TXOKf02CpuT zG8+SRp8;BYebheFNJ)q!yD4gFF_rX)JkR67R^c0&Xe)kxHQ$@_c%aFBY9YZ9=u29d z(eU1f2kL*K&BYrHjWph4d(<>H-0ID1s>rYkx_J@HP2U`S^`C)t{@vxLyn7K1VGH}Gs>c<9 zKrIo8AM9kqP@A!y{Jx7#YE@z%L15k#DW!p}fZiUJPu$G+m!bqG>hH5AK5DM3AF?&==DVveWLn$8oUO2?U6O5sty&Uwwo*c#=ADrtvrNBZe<2JmrWzjn}0B{ z`LPOxt=(PCSLQ7;F;_6P^3~matEA)XwJqDg8Vx7zi@?+c0K|w`PSAGxk4{L^5Oh zML}X6{)z{41zTINfD;`j$ODbIU+~qfOKhdDjIFfGqzHBvLWcOWSR0BEq2>lABS>4C z67E5Y|ES~lC51d4#GR_suylSBrBXL-JKRLbaZ$e>w2&L+M%{IPTRmEL=k_A|V*x*i?wm8r`` zyKX#SXKNP*Kk;;Uon@VSrbz=88V)8Wj`7XuUUau&OXV4Om{{?HyMQ8AWKOY}IY7Cx zR=E|+a_23!Ggd=9sZ#Yy?~pseM_^km!icqRak1&hSPfX)=+gz-MO4(iIZY%K$A9%> zl@q_3Oz^~U3|>00*vx`>962@z_HkKQa*lnH)Mia^Rn7n|Fm~L<6T4Y_Kv4X$pbhg2 z-rYwK)_K0MfNPoBWIq*P41PuP5yUP~QZ4YY(SaldD8b`u^_}5w;TCIRo5`L-cOUzW zFHdLWSvMWLNKadykHHriZwDIk&UN&r=fMI`Z8!9|SBIlga%MAaQJc)O8TO-03Ea)h&U9`69upW5Dp>P!elxLO(;C7l12-uReySb-H^cJUdb|M=0zI*- zV=8i}RKG3+S>gVoT;wtz@3~7f= z&W&=-rN6SbZP=Jg2a|c8S-4b!yvPAE;KEWEQ?;zc%b1g-WYQKeJQo02iFBaeF8Cj5<_Et9sCBUAE9WQt8V921r*8AQpB8`X2=g39B6akFjBXt>jb}gWF%#Pc zUtjYE$Y3>Nwj8J5&vik zMip-243zt@YKC6=6eFNm;s-PK2?`Gia@q9ZAWXO1B6OkD4SN^CcfrZxu^hW>2CnZS zR<3F99e^LkwP9JjLj%N3;@x}_`3}GO5>T*WY$ApBjrVZ@mr4u{**GJx3%P@jmF@(k zUO*2YI|m)BP5IKfbnW1Vh+X@aj|Qgbd+NLQM2-~Ac~|F80}?vjYN*1OdboBy-~iS; zT+4H)uTjjc@Tzcgqq|Yc`@Z@Xo7 zs3=I~%s^&{N`I!FtwQ#{@M7MX%8I|Pe)Ysn@I;qtH0hr+0f?L@*x-{)WK57yTfE0s`cSx)9<}*a(Xc1i%2z+syp6g zzx^K9K6%PNKzC$0y@I^_ervwjPu>c4Z?9D*mu53ZI$iUuuVq>HSCEr^!ANIrI9E%@ zwpJLsBR1&x_0EGViEo3JTxO|lwnp$zg86s(7J1?*S{I7pW2f#$N;eHh6SqJ=yCh*(i{nWA z*;g>~Q|gaZWby7oOb12{ntc_m%v{c=auznvs%7agm$N>R!)R~pm(v1^LtSRaHGJ{I zHxr}uo>%lKs(R7CrH^IO)_{{_1#4}vIqFpj9*9@vx{(S!>pU&hoW zN0`10Z>QAKoLp&krkw+{lq_pw4|=!UIy;!kU@pDOd>|>N_Ai74&i^SSa1F!vcL8pA zHmJ)Z8Z=AqiGdf+pE$&a&CdvGyO!BNf?_rvNhWu$2NC+ymm_2jLLJIZl>Okvz|vs&n{zdr+K%zRPnwd(<}SzFcyq$~9JMJXZ_gvE zQ+xAs0}gwc0TJVJTgoSC1?XCl_Gv66HmJ$Hn_g7R%2K~)ns0nz1beal-RCpOm=Hq? zXXTC)D)wkx#0DjTmzIKl-h*kk(oS2Sqi)$7@rxgE?W|9?xH%i}XFFK>{x+ph#{L0| zw>$SfvpOfb|Mex-rE$n`^ZWKt@0!PceyK_AJ(X%dXa3|nSykjI``F)0NB-z!gJ#)G zPv6b6w$7$?Eu&41v(B>h>b9`;sZ8cLXKs*Y9Wy;WeWg^@7FEHi^Yw4uE&DHwh)V;f zT>pW%P2sy~D7y%%+ZmTJvMA>@pj9E0T@OTf2l>6+_sKD|zVdLXIfQ$FUzP`Q;&I$t zz+%TGPO*XF=FY2k=C(FEYG(;=a@8v2pcA!E3sse+>xb{o5ZCgt3)=v393I<{%&(ZD zEszC`oW3@OTts2w`vQ}X<&Z-itmZykb4L3 z&72oDY&|c0`1xJltXD^jlI+5PEz>u9_+V9Dy781x@2-v> zllOVH&=-Oen2sjP+2x;`Hf(X1u-=!|ctE`q&pjK>L(=oz^TFbMqW;k5f`f73D_2Nu zDz06Eo5C)fAes<|DST(x_;njW-@-mo6mcMMJ>)KmfV{ha))Q#fzRsy|QLz`;6J>gN z8hf1}ZCosWk<&osuWnNa7fgce>FwqR5%dQ>-4#%jZ9~(@_5EMj<91UztpNz35C;z(=!*@ZCw^{%iunxDl}~tXo_R^JWuF zVAB);uy@|9x6|Zw^v+14TEJOXff;^w!fx(4i+XSylT-1f?Q0wkRMvWT zPH(8vxxhSTEOX)~cO^qqxFkf$*Jn02%Echr0i7R)GVT6wy@H&tC0EG(klxmHX1X{M z|6o)VrC#BnJWaopg3YY9RXme<`YW+Q)%8N&$-R<>yI0fxl_Yr)jR70T75s-T(gwz{ zAFC#SGAFY#5M(suRQyVv5ZNz<=-0<+I zk>|~$9_in;gj*pDpoAWiRHHG(K16L_GHNe>X^OPc0y6I+$ewG{l;^XcFs2&;D{x5# zO6eprF!8tILL$>}m>iVTR5g&0kiHpGMZYcY<)}qf9`MWPD7>)ObRPBVmy?HeEK!O2 zTcUj3G9&t#2Q^Yl5wT^Q)Q+Bw^;TyAOhYayw{#(#GYzv-Cew@&%wUSSK1V-9f6%qq z)0v80*r?oQ>#1iO3A|3i&d?j~CDysq`X*i6eMNU?pHobUqb)!4yVInW(g|!`PU_c> z^@wctqk@hcNQaWb2&Je7d|b{OO+#<|xJ*uKTF;db`#9&TBMjrM9@P3-)A#n{va*-5aPEj_C<$(C@ zrXQ=+TQnljrcHqBD}BMJBPC0J6+1vTr|g@KN=m|L6XF9yEgE>4wy4CgpzU+M1*z@E zWi31TUk>v4@2xyvp0+Z`43YILf4}9Ud#2t2&)MLIH&7m=vrZnx*a&aAOra@V$*mq` z6MuVvHd>nALp2C}V^ipzXSZNFZ!0rb)T5tUS`ac9^wA?Dr#8>(HIsV=B5f=yjz1&3 zPKU=7e5Ko*S3jW%a&8DIsLMT^W=Kl|HCHOt-@$^vT!=SoXKY3r5ZRMvP#P`bX*GDd z!6Yy}cNr6o9EbUhpsXzd?54cC18|_ZrCuka-X)7I`^`J!6~DTpjf))TO}%_No4@%W zKEDO-=wdd17Rx+awe>n)ym5E#y_%cu)v3&`%|-qd5*J0=q9^pnJ!T~f)bx@)Jr;W0 zaMubz)RsLH_{r`v^Mq;VR7|9!TxR}Ja)o}Zf!no`^u9Ai#`>BAnKMHk0u|rkhk3l0 zz1{#3Ya4llzJpjyd>gnCY7>AWJBYdd0VJUur|?aQg0nnWYnC@hnm2VTM==5eTZm1d zq*?D{B#WJ*FK}}<9cqxmYsm*kzR~a99Em8+9?qBZdzNE7g6m-`i? zAYGp*QYUPNp4}xxN>Zdcgwv2R->#dufe!@E10!*g}s}zXO=*Fn@%~x{vzCFd_ z*+4LXB9my?e+sP{1%KyzA^DudvI9=6kXO>veeY8)XIz@z8OtiC#@hN-H@BW$*IoSc%LbL;hDlby~l*pO%!RgW)!Mk=- z!D|%Z?2~D8(otlg&H$}!sWAZ#NYtf#tk6_| z0f7X=ABqu#OjXZMZ@qMZZ;DWlly2Z#cD&T-!fb5v=CG5|GA=4UQCN9Y!j#-Z#jVBl zALFT&Uq|85)7Oo1l?zCuk;gQ>CL>amS0(C%vP|mTxlY@<(@>^19Ey((f#=cNsi=Jp z=cb*DBSAULcuUlZjB|mF;of#h$VUFdc56?;`YsA`$Mm=KqG4XI%8yk|3}H^ZNri~b zwzasxO@}}$wU}xt`~$?-tH3&q__1ovhScU$kvk-krmqs87r+)SiL|9J!T75Og_VM1 zX$kL2iitlndAKn}>3X(uk~Y*YsO8Yw$C?6=DVXpaNdF*|kJ(_)%Te^HV(h%#q6P97 ze%L)M`S`vCO)Trq7c?j|kZVX_mUc#)@=!*)({6t5$ErT~MKlfq3(SYqrhtjK$8R3( zbb)jm=i_iycvB-{FEh{#asr#HIgmpM%SmOOPvib!ct?P}($~Ql{~}E1Gg6TR`SN0< z%`NxWr!4f&1ss@TJ`I?j&K!+DL9t-S2;@MpB3An@SjpSVicRR!3C6>MBoN-U{M-zB z!&%77B)%(Uz4X=SQ{{FeXZHSv;TG6r6k-)4+(kYRe~Yu`9Z{J-5eRvEJ#OJk-aS=% zu;hEPcE5Ut`cmhhZ>%@&ezY1 z#J})}d^0;-M0XmxRxBHjEt;s4<={UwFAlK%m(ql>3Y!g&8dhO zLC(T3SZN-D5A6x0OSiX4nn1IY7DpqiI?2$S{S%%f?1f~e-pU*rBx**%HJ?pYB5lZR z0F-wUVFRkIj!a)QKNjn`{8J{)sKR#S0l)E<*iKT$m`eJwYAewUa^ZmMTJ?ZDdZj}Fynw;$-PG*EN{)ym8Q_@D0ugk4}ud} zl}?;Qza!V(UmqV_ytSb8s3PEm`PtC4@>;k`YVa^N!oAQZ{mgrs+uirm4><1mbs@eM z+bi0OiP&g|;zM8Bv*13mcbMh6dhcy5HkzBjRr5yE%Pc z$!PNr*fYWpfu=fp%&En(J>uip(tYP5S4(9T_!7c8sVTYbjCePhdz`&ykoTA$(B1V5 z6ob00;i78K5enQs?mZVevbL}yl5vE@90Sw5*ss_6##RH`ot7~6$Epum_6`nN8!%?) z9`;Y)yLDzdXFQAD-0O_0i?Dg-${Z{uQ+p3&!B0a3P?dfdUo^c%?HDmi(GA#7Xazwx#6Sd zq1JZlF|-c;OsTAmXe}G>*xj-fuye((q4%V3NWTy)?g!%II^j-}X_dZXPzDpR)$p6q+D}fq zm#4LwK-nJ`9WWB*mwMvRJM2}dGQkmi<{I%fepmGrMjqrML8>wgrbw_Hk*eAAQx-Wc zT{y_lG7G%jS9UXCs^*W6@ug&b0dHyH0v9I_g;qwNi$+g&eQ_FCzI9dV^@#fQ3FpY*YqGR|e& zEG~lJe3wSx3@woEj(-Zv?QVd{#W88 zwuYQ#DBa!3@j+|jYk4s7mQUTh#?I#Rh*i!@psEo32>KwP3G+t95u9`Us%DEnI}gnMvd$$Q|79&64zMAj<}}L$Nn+QB`aQ zZdMj##w+NAsn0c7Yu@B!>3`|MDx>#^2?FJGl&cas(c&5t&sPUmEsg9f86mLMQvFu%$ zUUXkQ2iG&j4yQ|XhjWipeKSGn2)-wNfKFzLtj~a2InaVz>Vp!Lfo6g9+ELLe68AXq z6+bDvHcqg(aBrRP$raxaSEu1U~X_aYX?T*^niZf`q0_C zp?#CS9boA0Uq9>q-`H!|UmRQULTXHE`v+qx3$zHJ6z&sHP%*fPFv(>LhCDkUf>ng1 zN@NaTR`b7wZ%qhl{1o6>3sxv&*5wO>thz4NL1D z89K|}wSTg|-L)umJ!n6R)dRlG5Y26KmP2fWM`om3g|jQ&xh%x3sB}-4J?#{zGlBlJ z1Fl6OZU8uukqrLer2}Z1ze1JGowfCtJ>YhQr~ACL7=npFB3OxWqFQ!NQvm{Ne~r8( zrLGsFh_7%L^krHAE-bi#`&-n+M?t4$A3RdXB2p5#j8w{2z;-%4igFdmbzsMqZPRE6 zFH}Y;FKvAqc!?I__2?iiLU{gRLY#PC#-BPg_J)#+7=bA!3@gPC_xms7h9W0fPYSd;z3c!x=vd?g>|}T$9tR~%zETfL0XLNRaOpu z(pmXY&%vqr9fB`J0>KB#j#?qu#ykFy6qCO5T!Bv#3>NEA(tso87)*RPXMFU>s@oRF zU2{M_^iQkwIS`5krvGv-u{HZ)lv%Ht&Wcfb#8d!!DC@4dp|?vHP&0P}UUtoR#y_n5 z%eCTMa@ABM?vplUWbFrPz`&2>kEwe;fL+NO_ za}lER(kzSEh|nc zs|2c6-cRF_nq<~iL<12k+7+Z!SBxLI$AuXHo;;WppqiPeS zNbHE9s3}MJXj{KnS6VRW>@848U4D7eiBvfPXb)9p8kT;yG%|&6fD)C# zmGG`D-+6nwOfo}YVGemJ?lcva`F-2l2$%|I=01n_&EK$@vKt5&lAJ+!`&V0|k90d& zELC9f7<~XM1nAow|JWU^DA(psxoX3RJQNTWTEIh(MJznEG!Y;LlX8oPZ==IP^zn939;S3^0q6h3?9 zNvUe51F}$&>ZX`k4bQD`1^h{Xk~1w$WsX#mtDULvVZ9!X7j@527T}&Og!H`e%WNLE zEz?%Dn@o1F2y(8MeH}w5cesO)!8(@iPKP5lgXj+WHK#L2fnXxA%p^sF%-)6o@wR^P zJ`P3ZqZGuomoF0bLO3v@Ap;!@>=(OEfwm(&P3EilSgSd^ixZl-=~6mL=EERn@J zT>BPfe2zDuZQWv|Q8u6~1+2byVAzmN+B~BKpAnP}Cc`{B5+^syx>9&MX(xri4zDV= zh&oc8)X`t*c3W$8CjEx{eVQ?@(zDXgW7eaPE#KHYD#5{Aa1CF6j+ZLoyt;n` z49RWX65=AC-tOA^swlWbIhomI{&CPXM?aH3kZi4AcOeQ0WAC}(aV~s-1_N8mUwS*G zQq4|qU5|dOD$6IfBA}ay+Gic zC$Zyie*4T!;OgX?|1P#+9;!fT7nG+JU@n9MX^g-An!o;%JFr`5G#F>#AEdBO1ZunrenDFn$%h}8>ibq~&;QVc{uMG2{R1K{bu(mRtscxD zA4cg^`A2H14c!}Dc;7Pc2`FXrmeme}r9cC3mhQQLy)OMJ=@lYC7Fl+gY(AG19S+p-;A$?AeuLiDUvuTE1qM z!<&{7pd!67*}f+mCg+^R{)6_>l zZYOuccUEv0H{tgl8pceOwWtXY?T8f6b0w)?pkx!Upw5;c&{WO_ExFhLfU zwsZ?4$+BWM5M6viS{i@CnGky2dx#S`V_T@!VH77RJ(@|zWGFugE{GPMfa5f;#?h)@ z&<`s2$P} zDc#`!4JiGyvky1~H;TXh%p!4ptp;%867UnEK#AxBXK$a595}}3V6K;0XD8HXyKBSi zkY3XP5p4G~OJ~kl0RA+Qx7okCTWDX()?7>Nn12s?N=Pl+BB=J)nT($yB|J z0T}(rFIv+6i_o)gzl3U0wp{b7b0Q?vA0zQdSGFB6EFplLf>1ES-g%107_@Xir|(&r zIAIiR>-y*PGBIz|e1$T*V@spUH4{q%3Zsgxo^|cq*;~kd*Oz+I)EE%>7VBr)q$_A% zoBj}fsSf|o%PH^lpPXq|25^I(18ZL!Mp_d%4)|MOznu4o((gTzCbMq6$4*{K(~{;p z{~>{MWJ&)Gg2w@-W&ECb9X?uA1YsN) zP2a|DFn7IT@9e6GHR0i0+I5!hh@Y!pp_Aj;Bl!BNOprms6W}af?g`IdS3snL#b$Pk`c zrQrLT-4#M@@KGW{sNKA<{bgv7A36Fz4sMxRc`5TQBQ(HuK(5P>*~SjE#d$#d>EQ4-AX+exf%>f#W9%>!M&<5D zm2})rMwBgKd1c4doVdkov~ru@0So4JU~ zG06cZN`gA}j2iL+#)}SeE%F*W!APf0(gOxzmqTz-L>|u3`F+EKF+_s40nl;o*}CZ* zsrZ%IM-@|9&I4|ZB^LKZx|_}K(< zt^-8jGDXTH|NJPkr*K!zWJoC9-I*ZMMlLoiHw+M$HZ;So*IhPrOh1F2SWmOe0M6u=*~(YI}AruzVS z)JA{otdNHp8K>%Y?Wt%xO9lrseL;&HM;z8~HD{e+kjhZ5>xrpJ5*UdANpJ^UBKQc; z=3u25f73+uwWwd!1gTP_ifu#Z#>AfWSnunO_#h|7TD7r2oFQCLr3p%Rq}i76ku1B7 zGUF?S94(874ayM{)RmK|`OHS=s|J1X1$kEXzWJwIcHJozbM!G+oi~ermh0tZ%4@AP zGB(Y~71nR(NTRO;+rI6zkn~0I={y<%g6o1@T?@AuMzBV@Aq^b7MF*IGj_b_2~RFt#p zFi;`XTOA413}t#9dd$bZsnB=`ctGE{Zr009o(lcObro_;15t{?l557~pHiK(_TYkO z9!6qEv`(^oDTSl12p`D~?}b6!wQkbXjGjrPu!~GcinwN~LjAK6y?buY#v5l_L13J! zq2#G||3q)D$Ti3OOUU>N9=#*ii#7c}TEtE#6@pj>WJVIC2_|Zes^xZPM=qUo1+b~A zc*oGq|0!=TZ_}UItB-I%^TLpE^c}9(h~KU6v;(oEe)g zHnH>-a4?t~AxMtkJ47iH$>McD zm~NWOKHS5yD!P1)5rC2Z#^s*d@#tgbh0Y+lQmxP9#7)-~+L9eBnvo-ZoE;4`1S$Ij zwXhj9`L!*WQN%>shB;xB&eMPwNoL}dhB>t`wqBv|a&TZI+L#*U+|AJ%8E5UF?YS=% z<^OzMbZN_b24-&1-XS4)r|ZCyk{Wk>U(mHL*Sb?uwPA!*sV*fiy@1Nz*ZI~7SCI(A zdf$Gj^@+bH21w6B)@vp`H&wlQaxBEm1!QV7^zI>AEyh;;X7ru`qQu*w zkdvS8(3G0@&fmZsWh6irb;(P^)`Jww##8+@I6g+4@jyTZ+U{t6nV06`G_Lsls8=Ar zUF~zUD4U3#NPt!Y>EiaFkABZZZ2?~y3T7f>_8Fg>%{1qXz%~bTFtgZ5t47-P==!r1 zC4SIt(=c5;2>}z6#rxHKEbImdh>=AH+kUl>PE*T<=Z$qwpC;#Ak}XlYM)N)qT$R@k zk^qHzsxnLw)oV3i+wR)?FA9Oag4{^Q%JjrakVTiEZ{`oTke)8A7Cd(R%l<>($LT=#36 zP31Kjj|90IK$;y@W2>9&f{QZzR?{lg{^Y3qssBYQKTcoIVJUqK#5A?OR~q}$jR255}} zH2(lc24$d-zCO7D&Qza;*PMgyVIgU*#>pmcrz>fCm1M+XaL;_xu-a&2j@Uq4UZl8 z)!&Aj8qDgi&4=!4516a_N5y)Fs~A+q~8Xi z-cPGD>EldDd(G3@$hl=L9EedjD%@q2o7WFb4?+p#_0+os*?q*`kwlHnd5(^M{kLL2`aA? z8ek{#p^qgl1mJlM_nEFIob;4ULCAmEZByfsWgO3)s#-l8Yvp@p&&(pdtgFYNa>EeSDZ$Zu<)dpJkXCJf#O=uiSx zqXi#>5#w}FX>YX+DA+x7!_%>2khcyO`V<^acN-^|-FwWaKd2K&N7ZjV-HQs z^MV-@fCZM-bF>L-lmW(f8wZNE9ONjYfPHkKo^8rjput;wkduM2{-N9)ZZn#`3Bw8u zn#z{wbL|Hy>%FL`2`l2_-NqdQixrA4wGIjD^Q4|xbBAG)#{kSN@`TMtoB(HxNYxuZ zfR+cKJDXNK)vW7XSPOnP=h!TauO3F@K0}jY3G62q5Q54WSjxMBNm^Uf0@}EUbv4FoH>(lp(NW#(>o|Y9@g)=6CoLuAaVlL zO&@RlP^g!n>7QQ1etj}k2UG>KuBad94SPLx&&B2#9Dng*SKZ?Rn}XLa%gxiHV!82CT0Z@%yGli?V3kR=Fs`HzFl z+n&n@`KU`QfiOB(Z#|40%5O;h6alg_q|2DDRjf;QjggRQr%Kmi;5ffq=1fP`lUpzq zFNsw8)g-;^uY;vdnhM^XbX$t)yf1tSq~%}-584)+tzv|hjl^Ox6r4uQ>a!G?Woi@H zfn!1_y}s*D2O@UNGoKD7il|v}Wc=h0LMN~L(lOvVGx6Cd1TKZH-!R`3u;i-y;1%nc z`#Z`5XkBT$^;maQs09|Sb~5MoHbz{#wu)Z+sIZeAO6_Q5+eVp(zKuQFpP@>O3VJ&D za(+e^Xys}YLO^S47mj<8kz#20a|XNYJ;P@Gmag~-HE6=<3ZaC+g^X!X(B)XqOItOi zX^>3$E28BmlZ5L}mU_WGBkdL@Y!^OVRhar*A&l1j#ubLG_aVc6J;)7?-mPCT?al%{ zF!y;lzXKZ5$NasA7k)BoIWkB0!!OI(Y0vzV17)Ure5w=iyaPrLM-;q z%x4V`6bA$Zs=X;M?hNb#1W2Tb*a z*Q#4Wyzkoc6`Gi($rh;lOZT_*ug$0#fVOnQ6LbB_LecG@a1SZ7E{8)PQQPea-cL`w zEw<0g2MMNlJoOAz3?)upx>*mZD>RyN~O3tA?ugf()R0p@Xf% zX#ukmvbD0)aJ19pJr^pGK>K+!`oW$^v+uxv zj+-!L36Lg^hCM);zMwI-H)V5y-j;nf;1bf|N7*!~)83tR0S$%|ORcz(&CLe|scSqD;?ng~96j&*C z`Q4ChVN|_-x&6y0*7S1!+LlL|UVIv+CxE*#&Nx&X21eBzg9)1eF-fG04RAzQo}qf! zSR(Z?Y$A6|kWom3w58WYhUKB06&#weBABC?x0$sGN`z4uY3v|%Dmw;)K27g}j4%)v zaA)zR;F@{)M!>}+%~fyD#lp;PRPd|`W<|Q0keG{qE79i6`ygeWRKV3EEu9pKOk};KySCD zV*(NZrJBLtg_w3LY3o-oNDh50pmbZeF?>M22P02I#wr=ppHpxLSZ0h$fbgPe;Ly5S z+$hXwx%c3pvfb=JFj__5)!hpTsodDSI;qKOCMqQfr7 z&(%vf(F(?a=7{(M?EGx&vG_}sg@yvDx?4T%^X+?m+Ran#S0wET28PD%7N86~XJUTU z98`jz;lM!9KtPtQeY*JdcPHskq%rhN>`Bx0f^KxSEfRfhxitwI{nSg)GrVfWOl4+C z;ZM>ZcA48Zx-I%YfX;px5;Pbi7dK7BV4!GYaNZI)q}Aa4^wt=P0_-9+aNS)fYy$n_ zh-Qo!G0qI6-XU3J&>T>U+;*wZS zd%o;jUjT-($}tx#>I;83DczTEe_u0YMp#DAH~!(L1R1MT;o=rOdqb6k^Jnr;wwn2Z z|GQqx?=4kjbY$Wu_KSOfia+1^dE zt3Yeg^p+vfPwj+%xm&X0!tMHr;9*z!C+Um-T)`!XxK6{P!Qv%N&NNj-W4AOa0E!4e zWB}QRR%Vn>u(7}C=uo>@>Z>nyb2CGloTYmUdjdM8n4To~zAG!cmAZOAdEyrL+1yxF zUK-`;ekmByTdZj%(+|2XERs^-Ub>^m9RE3!)bmyaUp-Agj&Cfnh-Qkz*Fac%L+E$= zdQ@BWZcLqYdddKUFSaUoV|GEKvAd6EFMj~zj9Z{DOouf{uK*H1*0==G z35n8nt{aO3@XVbiRfj6$5%xv(oeUx^m$PFhZP2E71sRR6g)S&b?BhVGt6?i$yHHYq za)}I&fbMRTL`gN-Y{PP+Ad^u|xCmIy(Ks?5H^$YQn?xV$I8A$!Oxb;8xXq?dm2qXD z&AhSp>9O84K7Xkw_DD`NDz7YG*y?(@d4H~4Yl9G&1}sbBwpTmWH-AZ4u(&(HaK&Aw zXv$huU`8g?2Eubn*v}|4K@-!ioGLt?nI=9&rq)Qt6-ra`BVSBVtIFkdKoJi24gfQ^ z!ohK~X^lI8U9Sa5E$hV=MdP2q7Csh5KaedcVd;IflomSJx3ob_)t<#=(S zGqQVc)m%zVGIC2l7xpJw+(z5~UEz?hsFioX`mH;e$;Rrq;uZn3^?Yi5Qipt+#rkcE z*6n!t`e&;HNnQefVodZm(&)ZT(=Z_-a)}}C*)|@PxzyAZJ5Ieg{%(IWhMUHdY1}`W zpT5{hb!`eByI+b5v3*Q~M%EjUkwOd&$|rm|O0HGJCi+XcoOw?`IrL&>e@*$>3$={^ z*Y@Z^tosZ{m@@*U>Q;m`ZUK|Ckdfo-20e=eJbS5_6}X7k4J!74T&yF8wrR#=^Xz*6B=e7;`90Y`*cj^5s9efJG7=ANw8+f^Zt`~-4H#fJIB znMF;%qQ@)5xQKvKqurVSC1|ni!#@dQ?Uf=C1+pghq=L+B3ooN;Y_d%=DzXMaWXMD& zl%Qo9J{9N-y(_!rDBI&2d04ypVV7s?d8udHf?M(BjHFX%ok3q-%_;8*6w$#;DGm`_ z?QF@;!hqp1I{s1rbsGzHt%Mvkdfr-=z4t#s<}l=#U&LsP|HP;&e0VwcG%M*t+AE;Ob{Znqfr z3$gD;l?*iZO*e132?7 zK};8B3k?+J!im(2OGDhGOr_>HA4O7&{d5jEy1E2fRC8^QHjLbX+`+H_9uA{z5;^Q( zP7Az+R?cDEc;|q^8g&PB!c3`a?;Q;u4!`RN2#{zI6cO|dDDK78v{~euQi7p*xwGnATUwqMMOoF7op_`{GZ{>mZ3>sOh#%YMS8V85Nl(c)$5)pW57gEa z?&`is@0*kc^94@?r;5woy`o(_%!p&22YRucBail=rnfB}uF&5F+z;M$%euR&arWZQL84svJ7Q1*^;S4(1=*^(KEI< zoow2Otg$I)%yV{egKb5w=a(X!WoZD6qD1E!AS~T-)_Tgwpf;_~DGHfL1o*%mT?wC9 z{1gynXo!pt`Qzi%sq+r?qLW?w?ATcs6F{6GRS)$a|xDWC3Q!8SICT8)I!oYh{6Zp>R7jW6S{0u?lPO5C5Y+tTD zf1$Zq(mfaJNGWdn6a=|4WfXW^XVk={L3bp&Dn7*oT8~DHtMBCup*d_N}H| zE`IfLRp}PA)|3T^-TkRXfPrT{zpJjyWBn*G-U%88d=Mfl64RD(8g(idVRVBy8z}18Rq8fG5{Tg^Al2wo2mMf z*jra%mmmzO?cq!HuGA<>?M(pBw(e-L_gtR{U|m_n@Lp=x;Tf#BvRHo_nTLw{xh}JF z+9A4prQ_s>1lPKVyfBLueW2aD!2}o*Fr*X1q;FhF(m=EEBcvgETmtJKE(|mQqMAFlk^SpBoA3I&cwju(mjv<>bJHwr?KBe5Jpbh_e`Ek%f61{=un+D%*oEyBC|ilz z^7o}|!2Jk@ZLcroAD{R)E8((+MiD2QCOM>R5c>3`TR+&{{(T|RTxD9=ufhRH<|anS zH!dTV>Cfv8+20pqi#Z4qsr9u)o?m${Su-xl)IW7er}TnG0<=?4HqXay*A9%?}5B{;#5!-V6aX zzGVee6z8|#TFz}p>#4tJnBb3(eBAsOv3wW(A1S`StC=^q3Tx-tjsFMNp3aW&JN^ zv5|6rTMk#*KWgRg@)wNy@e%2Nvz&iV0YI|vWk)IaX{7NrI3;*p0e(L;)KwPB5&O;$ z?B80_T;JbT0~C6RL4CT;feUtHMY6x>Z?BSDKdm@0_Xhj!Iw%<)ayI`c&4Is(=Ra7`KkDe;HTvJz1+f3%=YjO(yX)iYzYp?Pb@&rQ0O=$q z!U(HvfKY@W0jsWkfg92sq=x}UGAj33MlY5Q!!-&s;QNxvA;{=#IjY*g42^iJrQyq+ zxt>|HzW-5|lHE@h4;(*VgC!RcCvvf0;$W=NrJHb8*Wedt2eN*+>aya2*C)E#Dpp`x zU5{0Ldpf>x^eQnB8#m1=Fs_3}jM0o6YDci=94G2YWyWxEo~cw~7KZuH84T=_T|o@QJ}Oypg`91`67lN(gc*s$-&rj^(Av@wD{W#lb z;+!Ko7I;dvyqMwmJ4P9P?rHp!G#h(=cD4$^Fts6HyC?qHo1 zIyS>ILtpTk&05A?cY$jsqAfT*q-p1|MD+sAd!C(9AcOeT`xkHhG;lkIJZ;r}05{+wIE0^-_5ZN=%}@(`^R`t|f$?#DYmwK}dovg3%RoC<6YSBgq)O`C zSD6nP4%f?~uFEcx_8a5=@r@TZ{qdDQyz%Gle|Y7OOXvDw?PXYAQ_5yC+uyk0GOfu! z^n#Iu{o+DTo?g4JcpE?ChO+!kKZ4PNOUlnMD_mE8+?D^4pZI^RKD1F6IDmHcZMdcf zPC{J2cZ<2vqcM_Pv-|_z`a4olZoKDhL{qdPq(rlptlRz|Ij$c?_k-mADXmpr8Djl> z{5;9qbsgJx*kSrQ^H znjVQqMRe-K;}Nw5N2A9W{_RUL)KBKRE_?dly$FvtI@)5#rT)YF{%CssCEE0#w}G~6 z0-QtpETU^V#Ri$4&F3=IM&8{u1S6O-Zp$(PmgAJ~Sk=1sTV=Q^RQ~+Gvj}T#enuFF zZ})|S0!F&|nw8M!F8QaR0+qy-v#u{(e5iI9A0q#Y=nrS`pHZcMg^iSh zp20W&)b6`4BexEuL>bY^N1uoA?=+28JFc=7zNgX5Lq+zWXj=YNld{SQwz4@&C0CCL!+Nc7Ou373eUuYD*P?aYX7o|UG4a@KQIpL&;b&ikwm z-Io4?A^9(O6#tYAX1=>`Ug5KuC+=Qe`MaKtwB)$%~%kyGoq|bH=1Vh*}n~u1U z2RjPr3WcVF^#C2449ytIc{+2KrIg zmyZkADz@wrHy2*5YW-&nAoM=$a|VRjisZ1>Fc~(p&JrLZ-3*Fo10cM8))g~R@wF7Y z5C}s5_lI>c-l31bXY#&gf#^8;8&}G8OgDTL)Kpf2S;`f?vTQkke@Q;|8B; zc9G6pJ##~lkXGgT6j{ifJB^1C3G!% zF^@aEpR+LVDRk(fO_JS?kH0_bqqgwYPFE^6%<00D8-x5Rt-@(qfwvP%n$_Fr@7M`y zC)}R)ViDM(WV2gxr5(bam5deYqfbH={>+Rk6$>Ad##^ow2W%XegL7h{A2V^C1wAX?Du{NaN(x%6uQ zlS-z73{Ph8&*GRNXq8B@2fGC8v0}9~BWn^z+FSC#Ey*fgTHjLJ$uxh165 zsnk_bFaNYMvslbL;5E?W>*NAwmy*W@FeZQGsiB!BGO zaFbxFxSi7vs}A9*`hfOjT+*AmRHmYB z)_3d>GnNa!kq|z2?Zu4aAE)0^oBf3LVtR94>FllT_fI&wTM;XO=?3j0{yPF>%9H|z zl*o_Mt!XK8LGm)8ZoTsEnQHaXjpNFJQWE8Xh*Y6=er&80HdF9b2B zBNp@%k#obX@Zm1HN9-D1E9T0=2L_E=srZOez1%n`NqCMbMbld7(b}z6F3@|=E1zAT z`ZQ!TdUfRM;_4Sk9UtprOjdji`w7-n>nn0`VO^~gmqTT#UTb;k_=Amt?~Mw^vm6j) zSL^{DIgjdk=I+INV~#iB?avd9pG#OHh}3t0_d<#-roTW2l_L^4{Tr9+mtjOI!Cku5 z=#5HjiW~8{G(ozt((A#ey3&fw?uz8@=#JC0l<%pYfcV05Rgq?MV*KTxtGMyxNPIqfYMpg?{_SZe8gzICX>?fdKM_B&K z0Bm@M?s8*b32jvJu^kW95mfH~Qah-m#BbiqMT4(}hS`nVT{s%&@1gZFZ}f?-e3pSh zt-C0^BP%Nn5CKi4K40#r=AIWEa2)tNH8ow!2+Hjo$voBlynr(TuS~0UL6lqu9DZ#J zEhV$*l{*&*Vj7d+n$~%b)PZ9yt}%I6WRq<+)cT#O zfE0*$1vj+YvKmkvR0o&bdfiEDBS%|ZR56fhIlyBMERHMZCH_kQv^t6dg+#(xy>E|g zU|q_5AoM17l1t@%4|Xxk^TgfxgGW;2@uH2wxk3e?H%?oHYwYN>dgS@h_1-v0%G=Ro z>eallL@rvc4Oh?XZYkSLmlute;OY0l%qa$jb6Y;vX8ddI^IF67R>#W^g zF6LI(I=DRDP%+;jhmD{gNXhY(Y{#n%nntCAOMKHbPGk8qNKW(Z19O3ZJy$J7KAFQ3 zhzo56Lth8d#sMS;*z-3di~CnEzI*p#<~^$1=1nGjdQ8aX#;oLh@tM`KfhAKJ?!bjOwZCDpca43~Zpd(K94tC)XMFr|RLlgSjykwl!IVHn4y=8e zYigZ2%g)o;bi?S7j}QSZc`K5TWsA>xkbO6UKQBbQvcc(wtkZJ zK}QL0U43@Ab$^l0O<@6-;7E{51C75!QF#xWz`j)_PnFv)|Om&qo^}zklE77D}4k z1Jr#xI|FNLobmvUjKey5aQ{Mt{k^{puGO zxPIzWwQM1YfXmW)Sd?db>TStaUpYkuc0-znm!6QU!!OliJs3!3Q8x6j`QG(%fnYc4$kTZvVG0v~~>o2}WdH?f16n)=4yk(wS2=*ygX~%S%i8UvI;$ z>sqzDSURf>9Ow;a`q%<7HI2Bz#r<)E+hb$j^fm{$xI(g!MGL0Jtv2tl^@~Y<#K`3| zO<&5pa!r9tX`|hAWyGyZ#q&E=6+voY_OHx~t>ckNM)Fm5CU@Encu}tWPQ3W5{Cms@ zwlX2F%O`;8gn6w#$ZPB9UG>73_L*#XTt9zUQ$L_s+Z^>+#a3@wuj+z+TEdY?mpj&E zb~V;@{_drxwd_)?$E(Ge#FG!Qp^13zsmROj7imTU#gwEfy_fCn?bm+$U~X*qNpTia zbv4B;i45&LsK(K zA5043v2(KhsjN)f?6b@kjQ?Q6LDamM)tZ-#F1siG;Ww@ZVa2du9h*EN(-uS&1}W|w zVO@5f)xP003&qgnQbW#P9T71hYlruqi_24WGlM{ z>-rqZcL9k-K!*xmdk<*t%gE2FNF3)bZ_O`0D-~NY-TMX9mRscL9aELDc9}w}=O4HB z2)@fNz0^S;U?&iy?I%sxY#;2ZI*mpgyI)waT#*a-d5YPz4jQ~;r&_F zpwR=5wXX{Hr)X*UIgC2#O-dyDR;7&vPS=jVg0Z&2+g}f1uYzJehWi{bi{8#oAy=k# znD7qx?*{mHxhGJLQgBgTbKvi`_F3}-?luleYK1bMIHwnwP03F%i7Hzv?O5P zYUN1xTaOu47oY;^_Zd>$mOL>pP_jVs|)1kM&ch*lLNvHWcb}xaBpPsXnEB~gk z{#TvUZo+->W_!I<3IsA&clG%LM4eA!^+QCk+RCHCJcOoRAwuv|R>`b=YD(4FqS@Mg z79>^vrX_*y3srir%gXI7H?8NnPTmIKZ;x+WOT9L;shCuXDeEnaZtRX_-eY6~SV(3q zrw#Rui`^t(GhI0H9Z&~N0U!cfNHl(@Jkf25kF}F=lLqFO??OA#CYhof!Rg#d&?|2` zP=69|gzO?xOi3^yMlDlsiHrrYIS(`MAu~OGXohGX$-L^COwX&eB zVcBC^Q~SNy8C$4F@g-Ty=~P|KYxF=&hAu`0lkz86p}znWW4TEE#7Q7}fVKk|sXj!r z+Oyvgihlq#{vB-f?*RL@{b7vycYGp=24SHbfbH`mLD!OI|N0W4KE5)HYU}Gr$NwUgx{d+xJ&^n(aS6@vHXs2{m@YObcwpdaQ@uGdF3-E~uAt zDLL)q%k$Z*wg=NXUS(Y@mdY|aiO_n|tEQkycD*fkSBZRADP8``__lTQ;H^t+%3nYCXTO$|ozuvfqM6L`f=h3wn$}sl3Sh$m^ zbN;h)p_(DO#5~1|l%CO^N9J}u&$OT`v#UA#){|GJ++O>)WrLg z``baEdMx^+%OqUT%lYtNz48Qe6nTLF=01FZ?KI9AK~>njMZ-?@jLwT#XZTXEyY-74 z9Y_-DrW8@ZXQ<&%?+x;xTH%)}H1EByWSS=N6>CWn6*T>d<M0^=mL7@sF= z!wP6yYvUSs0h(jbK_h^+r(HwJO47E-(kd@$=5Z}?CgD*Qfg_&cqXG~mTKUTmYHLgvGXmO99qM6GnhnGwQ zrmW5EpY|pIu6(mcDU;>|k?ZMACDw)ezLw~!DP89+R=UAoEMk_-a5o3smIdRi6?;+F;k+pR<~6Cf8fn&9@+ps1 zQnt)nw|*8Z)c0!UKo&(z!>qDT;X-!k{i*PQTHZEobG^DTDH<%Yo}WzIi|S~_9Dd6Z zCqs6AtSgMswxSUE=LST!N3zt&qkGyzP3;F+ryqIO?j8O(tdO1MUvSkamXlhG9nxyj9b_=z;mmArv3+g-tVOD$eoZRWQ|OuL zh~CwE#n&rBhV>r$PjUOn6z+RgqvrFdyuzn^N&eHJ=he?|$z?f|dbwr~PYgD#wMx%~;|?)2 zXr8~p`KPHSg_u>^yV)_txEGr2�>$&mMwqr05HtB=xLoybiCDuaH)giwzI54K3L< zxnk}jhSv(_47FM_!O%dRz)zq1!jYZQh32a=CDp4khIwAar^FYfgxgiWKB0qYTuJd5 z8NGWSQgh{k1D4{^;)STf(^Q=t3Vx3*YOuxLCOTk=2c@&>(5ry9pqzp?DX2DhdbIHK?*v$_5IsIPV>Ga_5AI;%gg<@5KKdwS-fO@zr+yU>j5Z!D+U*OvPV zHLur_P8ceU#or$G9<+Y;#+lL9qzR}xpqu9o%221G63|xps3>P@Ti8-SgUVowj`&-a zqS<(2Fi^fhULL4u4f{4 zwENxrkSnAo2f9}CL04<+bV~k1`>JA~V_>FuaeKLam0A2H(8y{qtQ>D$m69E!;Gi8f z5w!@i5}025N6&IFo#P-pATv+TV}yDaSCD)k$G7MJ2bED#!U!>}f2MvTGy7Q~1JWWA zK5T+lc&Dtd)}((R38G+IEBGZ8sTm;Cno?+TG2!y()9nf}`k5D}ai8_GRd6c3nJS4i z59Lym>%~Hr@l$$^)dmHVind*8@D4>gJ?r4*c9r4tGpbl2hJLd>F_r}bxo81~pVMId z_EOuAIwt{<0`fxN3U(VJv*2bPqr$km-lky4$v`720DH7~JOT3B-hgRb0?+}Qr6BiR?ltMOix3D0_xs=51O7Gt za_n49K#LZjYDtY;ef19H`>-~QQg^*61-6qR%XIE3*LI-bAgH4=Bx}s^!Fnz7t4{-; z9lC4cx`Y`z3g&ae!iRc4(44cx`fd|eic+`+Scr-7-3aGB)R}0O0~rqi{U%2VMJPWq zC#-ya*gCZwHQ%hM9~m$%GFJx)t9j0RIz!VTfQPsl>DEsPm5D**UTv*VtONq#>8l#5 z#)TediatgTLFiWQhf|5bQ@r`QEKkrK#)DQoy(K`XB2RiV1(E=~2N)P3d4UCh)ZxU* z^d=?|io%_t!XjB3`QGVx-bPt3zTwMv=Eqd~dS19tml75zsarX@AxCG$y^degTg!k;t_5Ke}6@luA7U;w~3HvyT{j7)vZVsm zIhU@(I5)Plj4HygU-YrlXF*YU8j#g&^vDJq0;3=s@4&u*is5JeJK*CnjN*hA{l@hW zyirBC5X9F|7|8g8ZmnBH-bOJ%P67Q1Z2_S7Gh}F_NLvdY>a7W-L*sN50c_TR0*ls! zk`xq|SDAnlOW;A>TlD?oWD^`D{XqJ@K3*Bjy&U6jV(WE>tqL!3=GPQo;)S zpuvPG!$<@5Xh^MV3#kNrndu~GUVC|k+JcW$%I(#@N#VV6W7KFyE#oOtp+LD%BN~1_ ze!dyMlYu2Q9%(<-7??7>Qhmk5Zza}jZOX8>Q(_jt)Gz)9=6NqvOCIzNnA*P*!c<$t z1`@Y3PNrS6i54abyA9#0+CTHuWyAFq=xUNHmn{;1(*DNfSQ=YyPT=HrcF3BgFO@Gs zt5+KVEI2%v#KEX_v1C|A-?%!wVdR64zj5Ic{6JiS1%SilVd8zr&{+=eJmf255JoMe zDShKo0Mh&6IY1%%9^E^8e@W*rrpMV<)*3W#u_=KFcm!Ly4la_xua}sFk79 zF(3L~-7=;rz0Pv*avQB2a&Y}|Qvg8whh6#Oru+(F=$FA}CLMt?;G2o8C%OOex3tmr zMYu!2Z!iyatGCxFJQ9P}rR=1DhV-hK!&w$?87kYR+}&N4tM2?P`aiKr|CNolrCzqy z2PJjAP|r(%q&!wXy8dTL1%;*P)UelTrwbR4OLi`gKD53th?BfQ~P-bc6 z*cp@AyKL}w5JgVlgAP`3-uJ!so9_-`l8UD=W)_M~tC zI}-BIyngLv5a3$Z<3+_a?>#5H@W6wYw5OnWm)rf4l9JphtDn|cNPo0Tf7pWm(neO! zye*50R(%=1_NJ|SWk?7QtXsQ?-1wf7@Vj#%lNT;9Q*GsLm$kR54sV(yZYrOwVc?i9 zBh9__zMO83$ncqSFhJsvlpovoOi?3)( zbh`$2A{aL~9M5p2KW8jXC`H-Z#Ogp!7oa{3Hr0){utTh9KkLX+J@N}lS1QX#Kjjx8 zYzF*}cW%ins8E|)7__lD@GfpxeVilgUqhts%U~?UJ$oZApG!KS`G44Z6R4)Lbz4}= z5(EWAdJs~Ih=7PFh|(mbC@2V#z7r5pAs`|u5+o#+2%(P+ff9v?A;i##^nyr)(3Bt| zeWMZ*1f`Q5m9QmEy+xhoo~nBHzxTd-&-=&!k28jY;cnR3S*+$;^PAtC`;_I9^gh5{ zjK0S4ZH0X^7h}vbsEBWiE^m4Z#&tU#?0PbEwlz74ZxY5RVhK@}SPj0&zXjZCb0lhm z01DAW0ls}-35rvLOXP+!&NFRpEGT*gl0tWanX(J=5vFfP*Og!Hox41}!StnhNdaTs z_K{1Mx7KU(+hGA@z`p`+>Nt=qM87+A9(s4 z;|0y4qXP2wb5R;Id-hzIQShD`xqf4$xD}&acBf~L=X--{^(ys!2`=VUfsPxNzPi$N zG};TC?zqr3p2qa++Zm@uWzbYhqlZLh*LOx=M(D7{u=rGF`QwZ-c~&bDENkkf`u^m$ z?5_c;?ZOx(uXd-(99)%?}X8D&d#sd*Zf5@eSd9`y4*kb$uEw=ct6b~S^ z;a(6$?uKlyiw&`S;5}W}PidA*7~KVxrs0i;f}tsM)U`8ltq3RqzQPaRpn1~d_Suco zW1A9MPCi~RHkht@M&BMC;rz1DUi*AIPdH2 zHv&3>coRAqDw zA1)jyyZUOxS_I8fX;X#l5%XF6#av(Y^5+vjXLpxa@979V!ShVs{mlGUNeXM?t8t&P z{Gy+7wQ)cM+4v?^{bHkIwSI!!S}AANF_#t>*4UTiK1kf7W)qnesMlMncF~1uyY=Pn zvu9M~tuuF}c?RHXvqbLY3?Dh9wKSPW0bkbC=TT(X2rZvmr#Xbv6ZoAtj{xenL)A6OlN3heoYb% z#NX2DhAK|gKE46x3h{5*)ri(oWe}5Y5NZhQ#eK)bFZ{OhvVEeq`-M)YMiAHQEuhgQv;-F6490n~aFbC>R==fJOmsdg zG$=gRNZw@^D zE0rX?bf~hiXDHuaT^B?y{g!U0xwMA5ZuM}9wlRyChqJ|zZSDEcx6BjMU$~6elle4N zMnHe~?7a`W7{na={T=dgZDZ8<7N=BLDKws(xvJ<2y+k$X#!0%?Pkj!=tyg zI%kBA^pJ4}F+dC)y|bwm;m&Y&dr&B_gQ)5eON_7VG~_0stEdQQ2BfmTL+*sTSlP^G zt59w@i!{87OPoWEl_un>_C;!<@h!Edzo5v}8Op2$4|jO`f1lKGRLM+*I5_`-oTXH!88z zEA+$I2iM$n&a>Ug#W8ne@h|VnJnd{;s$WJ&OJ;HPVy##DC?pq2zXlRf1DQb2xUetu zKpOBFa5kl-Qxi)152Su{Y1jVQ&MRJ8p1cn~>71#nbkyuq_=1-k^;-=@2jsbj!Z5__J05J$A&sQ)$#8&R8`zd!xWrCyfe(D{Xw8;v z6?u%Y+YrNd55xO4)1LyvDqGf5bkgIHY`=KU&kAnP;x(aWI5&9IVovu^FSBx!5?_KM z2*+A;!0N~xCXPoXpvD-viXsMHat2Ygc&*+*a6)*ImtG}M$Dm{z)?nAR5-KPy9WzEm zza4#ed7%OHj`=+2yYMt77^FW7y&}z0io+vQ3^xn>vo&hxKAB85^@`j1HF*d8o%lar zxj{x0_IB>g+DZ4OIfjr!Y%D*iqGIHobN$tCExohrFWYN*Xv4Aal{$9xfoUgtbKki4 zJ#v(DwMSfuC`+}E=15k*-D0;l)t5@7FT6Mr{kZQTCwP440`g1`Gc3)eg|NYLl>k72 zEe(2b4){_D{!*ZijfIj{VQ+I##ClnrM=TUGRs*1P%V30b7i-1z!1{N5U&lV_(AD+qiL|p!CrTC zipQu{{q5%5t&d+vcJ3-K+(E``Bp4&lA`#me3(+x;_RgY}`shYsh%*1x-!V=SMbPD7ed9rI6RoQ`XpfS0gXAZqM6n7YaN-h-*u zdz_9rW6$i|ku-Jo1Bw*tEGjM6P*nVtE#-?J(Di{kqXmKDZiM7A_r1~dO(SBpJCH>w zC$e5N7xai?LLKnCAM(7;>af_S>ErAhu9#p`$m0M(*vw;D|5_929A%MeAN7qBYtfL=~_i_glmK+ zhO2)jA`R);%WzS%Rs*Y+M}m5;`lab^>@OYbKN!=c_Z6IuzhAqJQ>JA8#g0~{KQ_4_1X7LDeYsn6t|;k^MJPfa-lG$k!Lgwnod(L7k&1W-nC9^W=E5IAE_$pS zugxo+bKQ<#>7zkOY4}c5Wtxao5K3y9iXH+@GCSuj%f0=WuAzQvznjaMkk)T(GEkizpGl8DtJu3XR}7Nagu!p$6*}#)cq&@nbTn^v#QRS`d)Y^ z4=HPbk8z2ujB3DfveNd`8%uX#EpuEK!$nlvwONxJ78|P2*(R6)`o`W>>iBd6nd`X` z%?3&y(~1quJN6?NTP^$ad8X@-yNxFv(mF(UtmRWlm4w*{KEkuviGh|&oD^*p0XV$V zo=*Z%@ZAukZqInP*V43Y@gMULygThTrs0;Om0IVheEOiphogi3~lH-36IuW3F zaCijdB2~oO5bzPblGg|m@pBXeU?a|bOJ6YCfSuI!aIqFB_g^E^CDcYhX|Y9OH6V;f zoEt@mUxP_YQ9L99!cYVVWN@3zz_UW<*gS7SQ-BEIO;VD=V+5!$lz)Nv#HjgOARqa< zxIlD^aJtqn_LL>Fg5Id6dI)UJ_pc9vF`UeRjXS49ZWJ6D0ozQZ+cATW%s*_-^H+xd zOH}3eIxO2(v5=BL%-BIx#+)4&8vnYYI7whIm<8nB!Eyo_a55Q!BUlz^qAd-F;05ui zJdbpt%Q!o&&Yi~j)}W8Y!XQ$t9au1wJ}NQBl#y zuul(?8XX72d}eO>BW_RBxCqq5t^RA70n7yCX4nbJ@`Zom8p&bra|tgy0O>YULuaA?6T5i_wndOk9CW(B^x7vS%p&jtx@)%gY+*Kj_b<4AHHn+ zQ;yxwmgV;_E@a?Hl5YHF1f6NwGjHI#FnH|2otgN*C^7vel=RR3;jue*tW;teZwSqk z7L25@LC1LP>n7m}@Wm#)X~XMFMNDPbpEbIt7V7_y`V}Dyr^ndTcbkOyeQC^<%W|!% z&faS(WvN&2B64GAV93c8rp(n?v*x~eY-|o}ZjK0zOMJ0rEm}G~=XMmIx)xp8CGYQfMBDq_TWej19}#=;ZK%(| zlyau%F7Y{F*SPzsmQp{iN{RC=2j&_RJ3}oHwy&wjEG@XJwgs2s4!-o0F9=UF-W7J{ z*duHG3RU*GHsO{%h%fU1A2tT~u8v3NEXaUktS;0d}e?*X7Q z8)I$mG?+BMsJe5Yc3t7iz}Wi$LH_SI!T-cF0;jLc-5!M_Gi6S#iM;dvycjg%Sy$jG zp19_g!%nsLY<(f4wmKp9sr;SGe{K_iv;V$nsef$QWwieHP5YmC3Q#f$t#-i@e3O0{ zu*u7J6Ij8Y|rN8}`8{3p5@$?j+(Y2o# zxQry_+1$rQHA@`p_JfmtuGc=wRzG=aD8A!zI z2Uycvyx^r%XHSAos+(Bu^bvd69`Piw~XsCk@*f@B-Dx^c#z>nNnnb zIhqUln?W^VJ?7RExg9^>e^I6pBAQYy++0!4SWZyUZJr# zE5)`~J5Ys)+i!X3R{QF}jjyLtZ*Sc{(*Cj-dHUqGRe^sTga0G#7^)R}`4>3uXvY5H zSqFq~+E1Y4d{3i+5(K@&j&~XkHXGHF?eg@C+fO+>N9f~r_eaK%QYlrZa}#=Px0VTP z;gHc$gP9x`>of=EY?Q!^?O@esNH=-i@NUl`r}sHyt#7Kcmv#%Zxkkdp9u!nONceW= z{;J;}Q-6Q>-@GgvdTh{gf8O*3jQ88cr1RU)F07#(svBJR#arWP+JT|%oys>AeC$f; z%ymz_PJQ#4I05$CO#vvO2)+w|Q2bV)8mJC>n!XVg(SIeVA`%!v;JMGXKyG zrZj+ne_7;)3ze2X>_+hNG}=V?Cg7QgrI$bK7gN)klG z#?t4fk(tls!j8sLD&bGj{nfRmzABQA5aw+?7lC2h`Z)%q)Kz^6FEWc~~Z(n5fA4OjOO71WI!*BeE*n$Jk zy`3b=XUdfT_vl`73n5cy&()2jg8*st!-tlt+x^!!1uVL)*`jBPdPP7JB0G% z3y@Y8WIdkMx|S`AMYq+t-ke5$-xmpNveCp!FVirs$qw(T3f}52zLh9FpFenP;l$(0 zh@b1N)*W)RlXc3Vc5aKo_KH6AD;);H;TKvjh@4Hgy;4m+SF=aUsYLy(OS4;(Qw#T@ zi<<%G;Yod$3)l5+$cLR6+fD)>aDAsD^x7?DN_y^eeWguyt~0j7c0pZCtp@#fU@YPOQY6HZ#<^D)x0mxoull z3Szxbhdy&V*dv?PmOQs0gE~|P$5s0)V$5+PHt9&L2w>)8_nGPqMp!!6iu7FWdbcnL z6ry}+dn~*FM~xYgD2PaQ97PVlPAJ)n3M?J+Z&{ioqS7i!@qSkIy$d$r)@NHSWaMn2 z`phWrtVGi=yQLX>(yJ#=+IBEpjGI5mDr!4duU!pHvs!G7OV$iL?`sake2LjB3Q60@ zG@%1yl!cJ~w-B$@iQ%GTy|!9gj@an@;{G&RE}^ZR=H;4IT{Js^DDI})=Jb^~1D*S# zfJf7=@gXS1^HtlN>D~l)ws=&mi&)dJ9ScH~6_LyY@lH5eU?=VZ%!yaEV^)FOpo9gh z7;Bzn+dDb_rP>CbCOZ+QKo9Zue}l7#73@EvGHmi@?!X&Y#ws*2JqR~2Aa*@*HYDXQWwFjy0iTX3#VVWQwzC~^;kYAYoXKaZY2jiy3ki&E*yCzr_i-M>}YzL zdOh>gjAjPcxVvRrU!}IAXKKf)_!9LVs(Nl~isPfL1sbcYFiw60YZW8_Hve%_X}UGb z^l<-D14<^2Y$5(sLDJFh*K^EKCo2|Ss56-ooIQl{f;~X6rzd*wl{9>DY`z(Rz)3;q z79#~AN|b_JjUS*?XQfuor`tA>@`M&JJ(NqCEg_5nBy>!aI~RuECiG~40Afy2!^Sy7 zEzS7V(5Kt_kJ9Kc_1Kx3Z1n5-VKYz(cw2B}YB^2MMZ^K>M6~;zD1n#dZnkm;C40yVU^&2VcE)}6x1V^c8N%qW}{?1FwPAsGuTI-h4|T>l@kXnd)||AhrBbUdefv)o{VWRsG_*p zJ}+*e$N|~i!4@~z8ma*{lB<7Rk=CFcN>K-{;%vwPxIz1K(9K;V>8v(Hc>)QlX&xcX zMLfV6i*kUKsr{i$ewa-c|2?yBhvi{V+_MJf5Npb8xc{OLCrMx~kD`2y zIH_#tQN7be-YP%~5E_N&g|df7-j5pga-e%xVzBAIu0Z#EO8)GLJvr^}Xt26tPQ@Uj zmKL9B+Sx)-iSx!q!S;qw%!JL%4FbfS-Qi}y}wkQ-BMl73g29j@ddcE zb?_?(0F9O+gYUcopx(XWPXCG^B7QH<_iiHqe%}fFVQZg$us-XuQov4#&^))39jt)6 z3!Tm791#_*|LquXwTtXU(+=a)o^%)OZ68 z>TN;D+~#JrgeQO`NW

xd$I}^@)v03HY{L({Q+@8a)KB-DhNK!B)Z`wR+}FOMKkB z2VKYTU`cIs6Y7NVJUE~^)`l3V#2F%G#XW5G2D1=?8sT*58>1fyvbxVb>vh5;n<&a5 zOHr(CMyNArr6anDr3(%|$9NtIksTrwn#j7GDVBRy$6Jiwv12JCc(5ze&4z3zWqBwh zBgB5O?>y{{Z6{;;?lHPaRcS{xo)47F25vOP*=SXrMd{$I)W02i7%<^pg5_>9aG@qr zwL&S`l@`M9xI<9N^T;nQIV@6)G$DAKEkzI`v>ck*%ahX-Zr(L;4) zy_k+n$4k+H&acV&Q(tNinBKY+;;6LAF@M6QFqsdEA2~FdXAFPwUhKUIT{L^biJzWT zQDlyv^K>aWP-1s!io30L*2y{epHMXMkEIu<{`XM&zb7BBcue9CQvD_E%bpAWj(@=4 zauupqYJ>zJ*3Zg9ETv%+{vM2?0H*Lu6&E2`In7?$AMtOd3;&N2aDS}0T=;7ts49Nb zvQq;AI5h+=J2h*uVJ9T(g9>WZH)Uk3^9b^j<%k5^relUcx~qKEMH+UQ@+9Sr{X zE5{^XAl4Gza|swD!M@nFzygmnrW3Q+8h*`x9V*!9JAd3BxRw*ivyC?j|5|cVb#5V+ zz)}Ym?wS%yhG^EP0TiBQsa-e}3X&bUcNzO-TJM$w*Tp9==x+~Jc#T^B@a{;#yM1~f ze%wuWzQ@sXVK|oPwbJza3imJn46!lY)=vipfX>DYN2hy%IyCcJG~{X3J_lg_XLr8H zF<(bTJ!86u0V;8A@^$nhgZok${dDjcwFAy6-yCPeQUrmzUoc<&)aW|6+%(kTV#^iL zV)H{($oxo8?|5m-Im?Sw*oW0%)FajwzaC(2bZPTftk->rje6c73B15jps3ib`&6$` zCEpWFkfN-kWeIEPVyQa<)yA>TRnO~GSpI5Q#8If^Q%)==Cx%06`*p>L5yzu>d6_*U z)Pv+29ZH#7O`=H-;V_JT1fP^q zYEXFV1#34F)R7jj_Ly*_=J`;)VJJjxUSd9HgO$AuI>#J$P;r3J*NejzI_4xkcxE|5 zqUyVmu~w;spu)K2_El(mgf;N3h6Bbmtt?lWHKxo-Hr08|trFN`5Hd!Q%;A}%P;L?w z-r~0lwlBbW6PrLicYRpiN91ZKfg(}M)kL@s>;UB~a^6FJY+tXo_x+-9g%HNTZTzaF=55>~tB7gZyM0S=Gfl6j$mYU@w6ohEJBKkb3(tRu&PwbR^B7GrjlV)?7`306-$)}Htr!py(|~pUS8TDQLON>8aV5|BE|B) z@DrK63ryk?A^A~#&;#5ym>w3*^F9Pba{TQF=g;{Zg8b76oFAZqlu#o+{x)@ZYPy;V z_(}dUmI}~a)@?wS5~qlM;s_{;@Jwq3h#yAGtBaV;><#syrNV=}M|A$P5b@+`L3k?8 z>CtGk!4EYxf~euSO*K{Ns@ko)GcbMh&X>SL?c|n;`6K-;uKb~-i|t<~7(UrG6_fl3 zP^seCYHHw4KKfyeYpLbY2W0Quj=SUwZHqe@j%kdxOnLsL5Ob4nCu`3`)B~tm7Kek* z;spuQJqY~m%WjE|4f>_Ogaz`IMJ4kM^0>32yO+Q_cJVKki6{-bW>A&!eIt?cvtA&t z3%7AZD*O~)7YLz4%r6l(jEU;PF8QK?6PiEbt+fmXI<@}oo2iH8o-JF_HW!Aq$=kVE z33aj=YXns~^Dejek^UwaLD{|7X;ULT8}Ds)rhDA>VRjsTqhMEJUip53t|A>t?J&pB z7UWOc_|4f}p2&!toJ?<<12s?Iw?L#h|8bEmHz=`cK0&>q>Lg{QQN8>aB^|Zm5`_fT zw#_`rMtj7_?jN5Kk_l@~OEodZIA^G}j8i&dc0AoeHM~+|PQw}MPbdj2wrVPli0%hb zo{kKt*ft3VF3E$M5_Oqxp5#U1qV9Q$VV{A})lqQs`|m-+!Zvplk~NkoRu znwAR#ESq}_n<`|;`Id)(YBKG_2jp?VG;mR(JCpi@hI?CFJAzNr5R7B2o+h`+NoGa- z;V+K&N?fKtGwcfVb1*Jc?|iDI1-)b8N(RqQeynTg3%>;B6k_!S{5x<^5f)4_3e2-$ zECEWDYJI?iDfVid3ddKDPv{nZhuaN?IKB(9zCksZStZ#lNV!W&tYfk$vaCIK@~|nb z-iI1Uv_149>|J!f;haefWtftT09!52Dd7Q8PJJ|hwGn(H;WneB(Je4ZL>Ao?QNg(& z;TDucgRW3p+$xCT=Oc#v(8BG6Il0Pcv)mxQ*|24zxSRj8d2R4(*4V>dN;NhFYZno< z$mnUDsKu`uYWHhT4f%3%r)TkLOWTSxyA0zDOnN{l%@mPlvZU`{&ugb>B~) zxG4Q|3kq|nyQMcxW81?n&G>W`C2yAj=L~9m2e7#xxin5}v2kKpp3)DnQt#zIO1B8x(l zwRXIY9QvBJYu*`3F}-(roS$If6Qk*Ju)H$CAjZdPm0MMH%{A4Mv1_BFjE>*4@Tj-f$sy_a(TqbE>RyxWZe(uq8lcU(=FR1K zS8Gd`k&lPAbheD7nMXM8a88LgzeMYBcE(knc1+N+OAXX;cPq4u)OIFYB1K9PKmB9q z_CFsPO4!Z<=D=1!y#Z6g6C7B#`%iO{mylQYKKYH>qW-gb?y?ua^;0)Kdi>@Eka#d@ zJx>{U0qmI^ZM>fRT|p~mkL5D+;I)r6$U9wG2B@>ItoZY(KcD#@ys3?+s0Dm429J*9 zF8f~ey)0|V^IP`4pzqppfti>Od@n3KsqwlNKA#5KZUWDgwZQ}!?GKVy=pQA2e{B2v z!~f!CPiQ|EkPS?Cnp$lqQpWGDI`7BOKakWEg&x*6r zzuC@I0|fN?!g9lzeEsv86@NbU=QICvH?1Tw8o4WDINe+6)C$cr5mFA)3(ETl9K_&} zXjeF?{{82JMOEIrYx`>g?2!N5rW9|EYhJ}#n!4-K{r-CJ(w)q`l?FqnvGEy`2h%-x zir4g{KZGit&;Jk_JF;Tv^D-f3e;DY&QZvAN2=nnzCszFF%%4vDlWv*OT4T8%yqH9R z1bx|&Bu{ONxzcZrB%9?oAGACK97!B%YaRz|zRD=e{!xac4V;IR0ZS3eW&Wmm{D6iqZk=57Q{tp%U_*3m1R+)!1hGW#>y^Jh?*9GZe{@*_`BXA-b%um5+BQ-=41N60N}9iQ17$tt zn#%Uk&A10I1JC%@M!LHNcZB=?*T2_)*fPc+;+9>UFjuFqEW0>OrSH%4TXu2cbn9E% zHhTdVCkvk|@$!%L1^v`N_o;tU|M*Yz)foiqG4?93FWxzQ#qxX(;;6Chj`$c8hJ5+e z)?0SDn^Wz7IDYO#qH5}#|I;5ew3aQe4D&!pV+Eq$POCp2ulVDMKOX<-hQKb#Wu5un zm3?1<&iq2}#<&B&>CCepxME3(<)3$}Uanq{);{63SY)&Eb6b;1Er}l|%3uBVb=L&~ zCQY7}IVx3hA8gXs9f8*ClEX~jcCL}X{(W?+TAW$OQzibpzjz4(v;mpNnOo(4=Uk+( zoNeEy4LU#R_L+1Tytt(lbu+;0+=(-uaWAx2zk1hW{3(Pa+~gxw*k77E@3w)x*S z02af6dcgiYkd;7ytZ9G`r-Ojvu@>TZqjezvK?HNedpkgekcOf}(tc_nc#s9$zh5#4 zO-f_)ON#Zo0Y5_12;c+Tz-)ZopppGm4;usmBXh7+Y<2=jnnxV-qchYcUUP{lJ_slg z>q{jGuW1~LaQzfPm}L8}7y4HV{i_fCYn1(K9{RsGF|NwxMP0|~g>lddZ{J`8Sp$p8 zVyyxCHLQv69=-e7hI=Lt7bJMj#o6lD6&dKQE3Q>)J0__q(DIoX=i7=h52_d3xFI*( zF?Kaxx7+B`*27GW7aZ8(^$5k$_D3H3`K`d?qRbnQy`@y#pSL+Z_IrDlxI&ZM$j*|S zd&zd+4j8DFPA1(v@M(i`-`01T+R{(fCcF!|y1TSgr@FSAlXmP;9>YG!_>{Wl-yuk-8>@uu2yTdu`g9R9I>Q*~u;UO)A+Wbx#ZLo3{#RHRRaO66l#VHU%q*Br?6_9&}=Y_RRL#qAdj8N#QI6E5I0s(xG@6smG- zYRRUEAWd}$r2G3FEKdl|_6fzDNjtTs-R|7}r|amNEh~b%e_ZXxtRNi&54n@?8D4U3 z`$TYZagowXqjhN-=H*FyYTu9pa__%hUHJDxqTRH5vT2OQ3dZ}lQzIqsQ>~*P_-N#4 zsgl>6{odfA!_x^}{U-(q@65egvj$Z-`ClG|0rLiDk9K*_PJ+qNWR>$bs`K{DmZ`cu zKfY&dt?OTOrdIo`-TPIbVX?;BCZTm;^wrjkBD-#%12FZ8ul}=p;j!U(Z?yTyz~@({t8rANuLBBp zuV_(ft1!?UYh}XOg!Vbms){SzFBe4{rd7HeeE4!HlXT|bPs!iB6G{UEcCRRA)~|p2 zi^t;=Pvhh=uKw1tk1I&%vWmy?`N3zy!e+S`yUG%!O1*Vq+M5gS?79Cg-YNI$vA?z~ z>j?;5vW@_4DMTXSE0d+T^0RFd``cd1h`sog|AAju3~0kr{+Q)nh7F%v$@{`WRCL9e zuoXiaGaCMv6iRwa+L2ui*dhNjCkfn2X!>=si$wi~b z*`ewMw;xPpD~(689xSZ_krY7#xe`lh8d-v4nzdHIe62ks=a;N_~Bdk zp4(w%;M|a6-&?w15r1>6xA;*h{ji(&nS>sx9a}uf@7(hBG{WAt?}`o>s4c@Lp7;y{ zWgD{8*u(MKKwiv78|BGPN=g1T~H7_k@{tu5uN>94IcC2ib~BOw>U6H#2wW_@T*f7p*rY|I(1I zE9J5_rE}-Sh@9gOink3!FoB8h$v|}0okQ)WT5|HCw(nd;3tGV!FZ7hCSDq*LM5OQ8 zm18j<+zNbq{aztQD5YjkJ39<6)seq?`{fWAZ?}^qa!yCanja|aHQ96HNW|vmN7E&n z4!=1!>2$k8w(7Z16{d3lGnlXjuPnqsrGV9;NVumqzw2e$mEpdLOc=3(q{h^r2z`BxL$R z1oofbWK)dHOll+VzUNB;H8b04aODZIEnD0$y7B3p!5hbL`$x))T>onzosi_G3Ga_C ziAKp=dX1H;;0^*h4Ut(9i9xc*=D!Q|lY!=w7f5r##pJA`P5aZQetq&=-hF{@yD?h1s*U!N-bR+-qy21EbrGZ-|B*sXdC{qA4OA zL)u9&?Rp(o`$*I`^66pze@EjGicR# zTeI)l=i)t(2Md+N*1#jM7ER^=*PBo9m&e5xU_E@hae7<_E&n9Gc<(?Dn>A_)*-(o^ z{WPI-wVa&q+L|fBgQm}{Ox|4-7)s0J&SiOvG zt>-A!Zb7QA>E0z~Tl%FJYA$&^)u8E@vxj>pCsG)*-DDj3og4ExcKuIP1T|=JejFGe&|D6&UY-X8Bx69 z-Si%L+2Z0#Q9>#Nzb$uPbgnOuj(1me&mCTG6*K?+y)?BrpnQGOMH?FW#D* zm`oer)^5ERWF9_F2cBMCj!J=xy$m(Nn*sdi;BQHUa|B{HZZ(fIr^N7ti0vYmHuntz^0K$d3%_Q8UOWP|(LB3#F&%?qdHX-YdVc9!M0NftEC@HuTX z_+C!&v^A|4ikx%y)iHVtCYe{J1{gN^fjpNq^JG$WWo;~NpL>ShLy%`lY;|sSrLHqv zXS12K43*spp?qm#iABc=pWSdDARm5PuXt~$-|k*ZD`=j(2MAjRwjq=(W%97>X0iNL zSdI%Ovem+~NDbWnfqGr8|5_~7m2|MTL>Q~gC>R=s^Kko0Wc?IGZQ3`EVrZP`vD6;2S6piKe4RG z_xm&dx@Ebc{(smmtM3{qhnSgVX3oppIP&R4e7o)56XorREk|nio;9BHi~n(BR~q5H z120^ZV!kNOepJ*Eyh@wD4=}k8ipYQ`{&u-Ae7YQJiA8`|dFA)jue)@L%L}Hq(?jo` zc{?-la$r#TEbRUbNQ{2^q#{EWP0pTk+kE9Oq@>@NN#Mu*o*JxPP9Ft@DOHkSa zvRP~6ZR3h{jCH#k?@N9B>%J@bFCX|*NnYsBrE0q)zE@H1K2qB{bnbmsCTEYt$eaA~ z>V4W5z7g~^?(Y7}zV2^Jw9@Klm3wYhx=&5C_P#q_{weC^V9l=4;|;#)X2;P6fuA1D z6@GVh+jm#~zFPu%3=el0<~>b2`2@YoGCq`Tt4aN z0HSs(uTNdzd{z4W05??&$)JlV*fP0U|4hF zwcDA~S{Ea-SA6r}m%%~2&7R_wI6v5MtfMPgY$D1Q@9E_EHYmJsDG_2@tjNx~DinI|rzi3e&%O<^;L z`8>0S-q3RMJHpQS;kr-R~Pmp&%^!#NqjecBSd70T=-8##09xG zG0!hMVLr4m%U_j@&ks}D)C-Du-e35Jq6D$ZN4A!!ruKMv%QM7t%KC5|YTQ2#rHM1! zGE(qNwzPnme79LkqoPl{){8y7S#%c_ijsxVt^O7Kn*vSvRR@bHj|xE#Me#FwkUwBh zmGq|h6qF1gY@;|kQUqIqQSx|YETzp~wP70uvB5HS{GCMjPYt>|ryV#JZ!|ADhWb2Q z2%nRZ&9uV3x)fm=ks^Z%+IYxPM*Y@A){d>!2o>@f#BqjNOkaStWLDc&;`R`a=978> z6W;gEyN;(4Z)f6XR%Tf%&-ttcRNi}5Is5Eo3vK0znX;zT_V+XWE%wG)+XIR8HSO6U zH5CuUXr8T5A%$Pf8+azrp28zVw-Tt2gvyZrtQypYhDjVF835Gp`Bbk$$H_raabjf* zqRQwhkf1V3VJmpJ!s8{_Y|S$t#{DQ-MCi@2$Iy;ij)fpbqk7FZrNSeM!Q zH2f;!JG@4lK%xbaTE82pt!cxOkctPXg;Iea7Bk0k*;U~zwmQ6oP6AR`RarvqUsovW zh*`ZtU!Yka!^0u<;d2c?z)1u|e3rwY5JmTWcig+JDrCqksU^l#pXu9`X`X{I3fhR+ zqoO$%ag(ZMw~M;5P1Czt*HIq6Fm&dP3}so9IT zcAj1*9|N+Je!ljC<%Y%q@)40A&8`nJA#jY|oYWK7;adphvGnKYc!Y*g6*?ZT24%IO zQ<*Bit_YFa0DowyE`x!GPb(e;0l9);Lb{eQyfb$*FY``pq|PW&?pl2`CX$A6@rzbR z2)7L0FwBYX8{~ORm^CtCoq1CIS^;VZUTg(52gK{Mzpe-k{|(zt(wE=0kD1)*2`$T6Y1$%dDvRA#T#t7L~B z$0~(x-_TXoudA;ZJhtn~C4F7pZ7$Czk1HU3hsBrwOfAg#bg9V6Z=%B_@%5=$%bc5|3*XduAVKptny`ojb+ZeXb< z=_}aT$RZ6!ra-Yx6)LW(bWScgbHqUB(T$}9THrj0%FEEL>?C%TZ5w-p&;p>9(xghO za1fxg{NEUH7OzF7S#zUi%Q)fbC=g2!6e7EZIijw)Cp~#THTI_P zhaz1JS)9P@EiM^4Ke2lGsM_0g+5B#V7c7Rcs$Fuf>1I9Yf&9sbe9GqlB5Z1?T&~U;b+#SNF zaShSlW=vovmx7^uS>U3C+m|JHI_f%%n`1+ssXr>M&IWrxJ^U9q1FFRIi4Dl;%Mlrb zXb*W8bc|9mnWuz>Tu&48W>4izy(+Ayso6R4D<zCKS~NWj|2%t zSisI(E2l#E{D}k~s_go<#6ySyrwHhXyaPHSe`uS3z0ZHYeIUkdfll{i?GpjAiW_tI z{nOef^-ly=l%%Q{d(ZcEPs#7^o~|_BTv4%SsLf<0$J;8^d1}e+eZ{o%@gqTj7qyY4 zg>{x)-#b6Gl!=)r&(hWRz`T0*6kFQfm5y42H-n4#1-+IY&_(_^xCXk&J&sToD}|^N z=VHfP5hV{UqfJvbG;Di|+x3{|pKvME%7sHr3h18K zux9oMBk8Pjy_lhxeSK{cuXcF`_b@uQTQ80~T7Dh(&at!Ti^pv&JC;^k8?jdIQ}5)` zq+*ft`NqHiN3CJ5Ufl&cC=Sc#Y-@KStBZlYY6)6?qh)PA05$mw`-&Xz?ey$=MY&3V zz*rc${X8pItFMZ%PIw8S^p&KA?^5yC#BYX|1s)-Em>dV)R;mCa_(iY+PotTwfitEH zSHVZ9LTmUnAFlu*QmayY1IKj}ON5wwYr>wm06I2%#;-3tFQ}0MZ;hHeGS(%5JLnMJ zgwQl)Cq?0}Zr^$nX)DPm2m z5m(|)C>NcM+}a!2DT~l_;0XHri*ft8G9eu}-M)_j=bdsr)HplY$0r)?oU(TKc*w=4 z9WBVG&QA50q+j30(QCT*_?x5Tv^=GvXq}d?{*uReMoODw&JpJ#f6X|?#-o@v5tkg& zlYUzILdJwT)@z@~?ZTono~6Zh*<>@1^?B2agvY3qNOYokR#B#RJg5HM~M8D?Fu+c&$N0Xx(pux{B27GbPF5Tzfb5iq}sg zcjTO_4>e0N?T|r!X%=q@^+R*~g$ljgDAxSS7f~?+ec=`OCKTJCFWfdLHp8=USuyGc$dnH}4&j^^rPg z@@(rDR-K}Hal6SoC3_W{efi@=mJ_23a!yFG1q?wa*R8Q_4Vg{vV^;hVfn2tS{ zv;AOj+Xmh_GT{_dG>uIr8{bL`?v39E$d)y7ajb#cbp7P?C&%4T;qi1IH6rzn%hJ=h zqMvQ$7SVvPK5WnBrvVPcd^cVpm?;hL6gs!&Dj}G?LEMd!MM_SKji528Dno0%sHO^Z zKTtQs(zI>W`rf=D=fTucR$R#?U(<8j@!hgnTZ<5PwBC4aVjxkrf=Q$B{0Id!Bql1nAAq8(uzaU4*o5bpYcSN^>=MaveK@hLn~^KKK987l6ktLXMXwSJ))EV9nZx6{Nx52)`Yw}~ zPEwN@t9_(-s>yD7#P@ls?bt$;h8KuRcOrkl-sRZNDG_)Y{7X>gGoB5b?+CS~2vjk6 zZJyCpp?@K6C7ePN`imYTH-nmyC}_sN-_lx#0z8Sal8ycw@KP|I-JFdZs8$XPM@`(cG&tJ^dC#}F`%B_wadPmHG2oHAyA`2j5<}1sN&|sxO5&&luT-dr z;NxH_2TVs|ecruX{hrD&9btq(PiR;Rb(Tz4MTb&{hFEl<#+99!a;jsUSxSzyQeTgF1K%y09IktCm^UHSA)m&R$$$|XMy$eU`3FCwj3g1GM zY!oEQ7GlOpBR*q`*O7|2wLHSCKc`dsd3HDnuZVAiJ`&_BHRdI8F(gh~3SOC{B9w*W zPZuA@sIP^~jpG0)m!Mp))AsX(^X}T!S3p&hRiX50 z^$fgby;a6X%RS0z8+)D|8Grb`cF%?j$0kdhD4o6CA*qhN>+fh8@we*m=J`=BaP9_#JFNaoY@+v;tl`CMi@TaS_cT z+voUQwJ`8y+B`bCKnSvO8JlDTP+_C}AOf+ev&Is(wrNaVS-MxcY zr7Lj(^>>f)zM9LWz9(^uuu=Bir6stD;}1LJJgho+*d?dt6Ts51*v(egIU+$Ke66iK zdZ;h*bYeOtHErYg@W8zf>e1oV%}*w8QxgCae7EQJOXs^YLha?^(p1Ht6!>2aw_Evz zgeXFvJ0OU0EhSI26}|p(U!aIGr{61z7Lf=+_Wz5$cMpfMZQI7RREbGKmQzteXhF(i z=5B#-qsTfYNepRjAtPg^lJ#P4ggas?gpg!VRuf}UNwUs}F*B7cCfA5@WfpyB-}XN5 z{XECIS1iXz|1?Ld9WHK$#E& zX}qK$*wAvEnQS2eJ%rjU++H_a1OWzsT&NoD#^M+`X$8|B`DI{u%pt?GJGqfx8!$i2 z=FFkzk{y1`J|<0m_W(gztcAx=P+#}%gmNADF@AGHKKAULlA{;)PVQd}QT?7ZZmsja zwAd-vyWe}^&Ye32Gh1TM`PY;vhdpM8IBRV0JV4evXW@|^lH!y?2 zMTu|;ILSm-k`~&?|9ZX)r%4w*NWET8pt6uV@>62pUhyj`K>Hg?#0aA*u^SM$Ne7RX zdANg6JqROwS}52xAih3Bfz5_#KYV`sZ;vg3995uvA2|!&M_<4jxO)@&nI3xT2>@hc zdV+v`$~KArckcg#dycr(scR}y#%m2`TksI^a#KS~?(d6MzU8kinU+Nd@iMYa~a=kr2qe#zN3<>i9%xJp^p;* z4CnFoJVJMJzDa*%(T8MZT(+C-VOeKOhfVj-BB2yKP8`bxeCc(EkwP*^V>HxN0GBfA z1Xx9O@j$P%4`e@Gpho`H`&WI%$p`Qx7eU^1fJIwTq@sn}K9Cm7eS29<1bGYg&r~U^ zEw2k_pS|8Pu#nbhJF=5ZnzMxztVXxl$cJhiQk8K}tg0)Ocye`CY{``qcl2%po%_A5gRA=f zMmW#>C)WAjk6e$ka6i_2kv?>BkRsdK1_@p;Li}3;T6Aj>NJ(nO)&4Wi3j96)$G@)D z0^O(ipdSb=1rTeC;*pm3lE5(XhaABZXx6@K{weXO{P6-q;uG3;UIr$TMz#2!xxrFa zxmcRN!>xne`x^Z2WOVPNNShs8u+}86pGyBL<87qocZ>x{H~-~6|FZzge?Qs(qyPGR zB)#e`0v4!)gM|~`bKvdpWEY#Oaok3Hvn{Yc;zeimJiK7P#ee3NllgV z*Or4dMyNfOy>0_xB8+_=SD9Gmhb{eZF7hgUYLt0Qt*2t*?AK=&4I9tYHI(bm9V`#Q z>=pOZ>K*2W1e$QyePO zX>{!0U*hPPL*L%<`c3C{E7kCIiFBKTbiqb;Wbtl$RA;`nOTKlKy|=USnL8Dt`I_#Y z2mQnzOr7N1vcL=5Vm^pk1CKfJq^H`dl+O`N;Rh_FqRL)oq=;>@=w|Ex1_L^|I}Q@;ICS~2&Bz5FrFNl>kQR_>9d zx-X=rT**hrXPRB;xYc>5nR(!WcBjgXPG6j2Z4RZLI`Tc#(x&ZpbJ64DIyPZ}Lxo4) z984*6Nwq#xZhs~trBEjv`(|syggLoegcM7egAl$KY-$&238ebNSpxg%W#qG|HcH}Y zAl^IQ{+hU*aJlArA4>CM535v9sExdVYf-Uc2VeRDlqAe|z4k5i^85>z%Xud-Z+`oy zI34fMVWOBu&}C6n@E!tz6J32jBu)iJm4PI`rf1^5_k?35o`DRD6*-uIi2tP=hl|>jigIOg-#hI4bCL~ z<&*ofeo90nmhHRft6upTt}0{K2@H9z#YzODbACDp%Qn2ReJwU14q(=z*5JD)U2mXt z;W4qSg92DavSTc*R&y<}EKMT{&#;-n(7dr|%^Fi05aDo}v9oqnd;xF`S&#@KhCVn9feF zS|{8K8Qc^r3!UHro)z06F-&yOE9mS1K^n@Nq}*z4_2B%rrPmHV zO?=3VCD^GNV&70-I0xP-?d-dA@3Bjnau~Z%UOQ^wjx#r){Ea;@6Q`EghewyEW<0R3 z#J+J%@l_7PzNvM8;~0{ZqCIe@fOhH4w^uW0e+0vS62}dJYHZ;?ff>Bg%{HJ{x;l** zY7Vjb2^OqoCtIFkT(~xsEDQT~sBht=rI*a2qJlq_3!E}!t{{I zq(I7O%*(9=x8yDomGly`&K6QOSli2cgH{lphoe}pQJatP_PwOZT!NqGb|pMO%RJ<* zIX5OPTT8BfQqE5h)A_001y8J_Y{KqyJe+}^2-<(|V5&!WzPx3nlGB|+mn{E;2Xlb~ zcglwNgVFbjx!T_?HVn3Z7bqKP>4cl76wXa4wN=%v)ITfRXkjmpdOD08eRCkEE-=Ty z^s^~NhTs7^a}X-{SysvImOTU%&jEzk5Z+^ojiCe85j?QoV5YXHjp3($&+&lly3(4o z9|bxx@ix-Fmbaps^=hS}!Uy$yf|5@d+VptGIC3-}XsK9M_#XKwaeXV<&=2_~<>6rA z?xMoSyKN4RGEdox$II-Eu-*N|I(KG{i5`chaPV*T?Je5>Uv6GcLfe=Nn;QU_P3}am zd6IaBe~&^**N%~PEc7nt{FDIh+U$UT8h#Pa-4Y-BOqn+jgO^Qv-`}Gi z(das1OgDN0Ba8wptir!Xd4gwBKP0pNF%PT1N5;z+6&L>Bg#P6l3ex|0RVMn7$YPW0 zwa)=^G+XJ1r57DQJFSV8D1`{N@WEu;s_6SN{++aiJ#f9aBOlyuJ6Gq=)K%#MAn5=4 zA@R*m3H}x2nx7I&?)VEW;RWwOl;ioqjL;2>0HS zL^lMAD;UME?`I2;!(lxn+X6O!Le16kMcQ|}7a5%kLAvB;AKTt$?)rFa*Eg5&H-smg zFXr-~b!gpAv*J7Hw|D=^(j5Pyx^0zN-9#fP6%*_;4w}3NK37u*m&{FnZyv0Y&dv`m z^jn=#&bF)R3TWl@&LH`e_mmjwX3V`#~O;${x1s6=t|IypH#C*>0S)1I_gpt1mIC%L2+zEGq8>RC%x=U^WC?CnpH4 zDzWNn@2Sa0=fm}I?aHZ7QXfo1mhvYV1yzJ@nf%}u3(=?B21rQGsvV`@Cp38A1!sB> zFQ!TzAJ7Ajr_*q4V`H$^?={qOR~lY44eD#8^^e;6HMOY(bVevkv_t+K4ids+eU%_1 zT?5azsT=m`&pDlmafhlB7SUVW>@Lhl1{z64H7kE(E6t%sO9HjORHQ1uF0oVplA*0( zk1}BRPVRI$MECw4Sr_?s%#+=tJ@uRgTEa{@;1dS=~6T&y0vKzmj< zsHb09Re{@;7G*nY{jgw|-G7#ei&Cu?)>!QrtLgq&p&IPFTzx#-aenY+OeRbId4C}(ac zlG?T>g7Y?D$mUx;__8Ntpgoy;nmIT{yk~2}wg@SlsGeOJW&7SRb)m6qQ%hOdIjhx; zzvr#~HuIZaW0J(j{@T*;a#M3F`{rlO1GSpAmBy#5GfeYzXSH0iwJV-n$l^37ojQIh z!}9aR*t;A-ZE3ws|K{@+FYaWoEnjDPFk?p>#aLr==)(JTckgCstNpewy>ai6R-K}h zZ{gcti5El`q1AYbO`dbk@#M1uej_`mo`O^b(+`xhG4=%!aU)K-txoUaIxQW`buBe6 zIm+kFpI)a%>MBrx%BMH7fgvr4P8wTDF+tsC@T(t9+dc_WwB9$|;Qi-v&fbRDx%6WP z+A=Jlm|J5UleB)x<(RT-GlHcn@;nnY-!C1# zljpW1waU1s0m;?9mg)aj-dKq+7zP$oiA_pHLVGy63~tt+s?MXkc<+bCrxG-pZr}<= z4Q;&~xat>ucV=6AI7OWfR>iwSfybXV*oeyxdj@Z@0+;!qb z_ECxNUXq;UR*L-W^_;jSJzglQD$&fM$eUg=ldBNJagd%_smqTH(Xf&(UdqZY?eV{6 z!`nt_G9%FTdq^yeVLdMD(yVdTp!3 zF>+A$)ilpu z+hn)e3%(k}RKj2FN6Io@O`LUAmLKXYN}u_IS|YlLx=?qx>qU#B1Q*A#q8muhZoH3A zW@B!cp(|~rshm^81W%U8z{xY}uZu70^2269G(eTlUW41Soanm+omWshF*>^?@bEAr zHmj%kU0zgR{ox#!jNO9~`(GK4c`@uZTzp=Wu|Bmu=^7x1L@C{uYWrH9Q08L2UFWaP z@A^M94m4en8BVbHf?Mn))_;}w&&X(vq{nb@n5pf!gJW|N-FBdI;v0VkKkBkU?_pH( z3){J1s4GY{v91a^jB6z&%;S5g zgL+k!QyD6aibSVg*GTXNqa5F_cYaqGv9@=cheA0% zxjwju%GgEU^U1|j;k$d>F=|FhSkQOvu`K#_bZr$tT-<==g9e=>eE)uxs1kJW9{=TY zeDfDR^CgbV1(4o~0x)Ub05O4NfT08_2y~~uuYN+Bv;uI=j|UZzOQr+B#PtMg{Nll{ zD>EB@7bCx4bZv&Fv8Pib&YwK+uZUx<@ZjezofE&SeA{lNi1yJ8HW| z-$IYY?UYqpHhTBZ1bnP^q+h07vv64z<2n{5;d(S2phMTd_H$t>iL^L1IV15Ekm6`N zX_L)JX&}i5uIAP$A_FOd+A2izXdMO#1$!Z_+g!q-No2gb+(=Uh2aw7<0OLTvFf`~s z-ifXpJtVFPRg2C4IIq^+l=hVIT0_76k~$Il7LxzrqmyR#>~o6W(Wsgf%DxN5+NNPi zmzNHj{8^muCm;CEu*|9B^qt~8h1)xgc74{i@vzhm9P2|;eoEBUbCEnJ4lWsV{^p~A z0~8Al>vYBbl<=jkf=k-plUjdciwZ#2ii7u#%=PhFGf1~z`#{RflS5$uI#rkFjijQG zRy;*Jo&?X3xF+v(kel?q+=p%T%AU5osy6Z{N4&|2M`K$FxA=R(oG1Z~aCC&IP5@EU ztp~QoT$M_pIYdbzVI6KVYnBtm>eIuRnsdxrR4tFh(n>YcWjQ3P?|_>IyGxVXZOY+4oC4k*kxz8LAOb@vl2L3tD-4naAO%A1C z_*ph2X?NBq{BwT?>5R`GPW2^svaXuI*9U(D0bR*`jb>?kvaFp#3O%Six!2hDKzdJh z@ArYW?u;X^x)Pco1h-dUB*coUP*Qjrpn9kCtX$#O02kbZQ5K#CQ+b3(u$G0}^vA?o zaX0DwYtIH-*3Br=*3u^bLdv6*W@#!o>Zn|Mi_D-N0ZDoQs~jKNRg>rXjM@;ju4)_y z@7z-<@C;-^4y<6oMI=L=l(=Hhg(T-{#a^KWBG(hnKvtc0RxC|#!^TE9IPwV=sl23i zq9fDIx{zu7L35-wHS~1Spq1~Q8e(?HYi~7UCpmiL6Xi!+4dqel-P3&!dK`;wj#M0W z|C8l&JpXqql!mP1>4FQz4hW~wfwPqB>OfU>oewmXOto9L*b8faAqy^irPW)_y?#XOT`@|n?@Bg+ z_k7C);L6l9IG80BO|pe)zJcXXj)Ny@V;8%WV23MzOgLW8J8AO!;WG96xk*(u@nuT{ z=D{|{GkKL5-Es%URgryR+ZX0&NG!H2O8L$-l(ug0-o1NSmmNbszx{kW1(&F{&(h#X zJF9u+tY7=!R~xg1vbkBGYNgvceOaa>DdjwcgSXnQnhvF@JCYB(Dhpd=Iln|DAtz*_ z$^4u%+s>9{CEV5s3}(-g;?Z?TZVnIlOC}JtVKhIFhnUlV>K$|1X!6vm(Q+5qE}C=d z3Q7`C5qlX`gf%jEqe0D5Uv}+Ek@}7%f!yfHkFLpPpoMx}j>9OTbES%oOhrQOJtz3d z=x5AZdg6mZqgqim2}jw`5zM=VA)}?(yfrYIQ{Oc|7TTfc(bZw=hH0;qo|*%f2qa0N zC*>@upf5v<;|RN4iAyT=paWK^x=L+UvNEj< zw{J@)?F9+k?=?bI9-zlC}+~& zU1Q_?^jT_PSYc0mCQFzpd&UKL-e!3i)%8_l#!`OuF)!P%z(=R^PPx6IwoA5?s-D|GiH=TDbjs7knF|NDVMoM~P0?&J ztWWBg7O%p`@SpGuxLr^cpVS0v2!0>MqdP%ao;LSD;5I{16MFGv2G|9~!BBh$h#7k4 z2Eg<2N%WAo4bEdBJ1U(f7roV9^i>Ftm0C}#9Xeef6zxE#1-1LWXN&Qa~yD-x4f!WrXqfXh2iS5>|FV`aI4{D)+d_MTvPZYI)=GT z7y^CopeP89AB}%aueyx`=7GUM7GB0K&!O?Sg1QxMbH3lU2QlTUmcCq#)Y}1~pzNZ@ zXC5S#q@O#n|o|PUzz2Rq^oJVDhA~~&~t?UioC!AHV|(*#$hr_ zx+sX24YL@?1-Fs-%6q@48a)ZJCJ2#0IK*}st9iM?j7R%5hw_8r!Xn~OLuGt&lkYz7 zT~Jo-liE-gmbg1mnVLSMfZ7w+$?5F3deWt0V9UmU8d2*hl#=*^@^UL$eFg(LS8V}* z(iW?cT8}sV>f^v9i&cVXb>wz3!|F7Qek^{=KvhC5BW)NJl=3+yFspnZFsbV7rpcaQ zZ)`Fy5A41$eA?5fqiJ^2>pr97N0OXtu0P`&l_*^h-7f4_{tFMCD+N;?-X8c@!)Q|| z;xUbRUL(&YH+=s>K3ct4<^7wlllKB_;v8UKRi&U*EPNs!8*#^5DrL%4 z(O+$$w4V|k4In6pGbtf2dSU29)4>*X7!UwEfe7vT@&`c-{!R8+uk>`ix<+4kb7oYN0Weg5N+&Mhx8 z-o07)rfXVh>FMd2oUka6y@JCP-O|e1H`g)VzCRCWm0Er2QH{!vc$J*3eyosyhcF3- zxSQ99$+*giF2Qe9;9CBn3-?Ic%H{^F=)vQcM6h2WU8IUyORx-8{`Fh*-Gg;2#h;rK z(yVuXysw(mN@7INY05Y=J*+; zv5n^A%%Xjw!29F9(2VCwcc%vH%(9Ir?RF=epouK74X$+?#a>_P%&MGKt`HxDqOdh? zGb763gWg)yKQVi;gvUq)K=pQ5PuMFeSVS@oA~xY?*_e2OZ<%o2dxGR-Z~RmIQ9>wa z93%GAwu;q+yJ71G0bOj#ltpQT15m@KbRaicn0J;x!8SzSQtrkSX4zDDV)P45RkqOZYe*Ef$1pQSPD_22BxIR=3(O zoL`<}+oRG!RD(!d9AxUy^mMS|vf<)gTxFQ8$iCN3aVxRnU;AJFUS?o)=Jgm)d}o!~ zwpDTfDt?y^!gC~u(U*4h&oTqn+y&I8#1=gOT>E?flo)UxiVT(rVOD=BAtC3;f_-3Z zZs1pidI3zFv{xaMU+NLk`8p_V9T(fs7}ibMO!;U+&`oSX8gd3}^D-pvM2LVfQemPYN4A4_PR@6kaT_=#b>QcolADz z*IjW^LHOU?eB6edUE$w40)O2X;%~qF3u=%ziDm>_QJHwB(3L_C=38?Q$FdTrd-}q>(5w+UEwup?&!r}$ettLWH$b>6WG*IrdfC}EM+PFI z93~fJjq6tN)SEwf1%NX|qzE?(%!FFiFD@0RZ5kI!mf*X(R#v+9<7xs|3aZPoPwm3w zA7x0(YJ4|)bn!gmwLv^nmS8+VVX4p(0wCuC(PBbTUbS zir5Zkwm&8IqV^09cXvbcX|)dWaC}FL!jRc9qdw{gwNcOZc)j&hbL39Z29Q!bdo1*W zYXs|vuj`1LGuck~dJea~iI9P+(R!+IieA-3FDd4M4J%yG* zS=iW>7tgie>6_w%gC}tELK08Ekhe<;OL(4@k}XCKFM9f=#=rh_@Nt#ls^yY)3>hQU zBHLTs%G2l=_ErqOWLh_X8uc&h=GT-oZM@bOoz#E1Q+jZ_UVRlIsRuIPYCt%4TEz;I zKCh0Q*fn(i#&|C_*r(#SLwM8Qqs+KCAD-jkqd}lVyCbo)^)FbAM#=W#BPhR2OY?6WWJ^ zjKORT^HdT#@p7$EyWpC3@#c|AKY?TiP1VA`_;qjz{^9r2m*Pfu)sU}qovWQO=`J^n zXu-760silBIv{MKWocN4TjI@dZ<#SYvPB(#;&)zsydha}Ryq7zMfih~X^dn=xGi=M zR8L+?^*{R*G|8v|Jq%9K^qKB!ebWls{y!8sbSDXi};*K-(a;A&)ie2 z`-mYNyqj1IOjP)KFqUl)H*u(mHxbJY_GBZHo)mC?Z|FnP)pzia?FMo|-aBDmEUU*s z4u648N9|}Hq#iN+Q74L1li#_i^1F;HNR4HA{d7>Irp)a_UqYmuo+bzL;(Oj#G~u8( zQfP%_3K85j`s(9PvPelPiTjD)LNIqgDrCgMNRe7W^U0OCzA$O66Lmv0 zzxmPfjs|uB!>{?qpzuUwh45t3PYLhFv^Gs+kHM!Nzg7Mb@%x0MaCFuQ@d$G( zL2EJ!0c&@0<)CZyt_e9&DGggrz8NY!RmaQ2z`cAoSgu0o_5yZ?PS7Xhppy*cF)V2_ zxc$1B^a1=WTWLo%4}k|yYYdci@A}jP5If)3+E$I>#GCSYV=Q!gM4qGQ9*ClTN^BAu zHube^o0wNugkyM9Z0CCgTJfC|=)QUprQ?sUC}jOpaYxcOnn1-&Y{wUNpvL$#OUTu?cORpdcaE7a^@HBQTG|K zOik|aQO$*5abJjLpwd{x!h^}564LeDm}me-)dM~7Y1CG@8f+K_qS7X;z*>kDl^QW& zE4?txdf^t)-6p`pTm?sU5hW~(Y-NlvH@#|rF0($Sd}9r+NS$w9PHVtXFqk#zJrjgbmm;qw}Z zme8_ecF*??+1B079sK-~69EI}`NPF(%<1Js+9kwI)XqWTrkN28!fa>Jgt&J?huneL z_>RnKR#Qv2y2npX7s`LcOR}qOi1nvOu6NO^ zwtB)b*0l|uD&sbt{rpw({hgTCqa>`c04#d*ZLR9>pWyzK3Y+mz<6l%3I$T-3M_1f=I7- zQDCH13vk2%v}6t&8UMv$0&#;Jcf(@_9k=e~o9JOru-Wv3g+S|dQ6i{3BFWMR6Rkr+6Bl7vX@ z9H3*iEn>kkZVxv7tJ2b4X;q}ytm?O7AI6IpE67|iY;d$?|!Hnw>noA|W z=p7)+b&EhQ9GnIgaXf@L2flCvbqeLZNVpu(1C)#6RbY@@$F<=@M z$bORgFtDbT@4jTNVauC{A+66VHLw512=KL5MyKcDrwahs#m>DhTErEp%;%cM5o^}8%<$IO&MkQ8?xc_Wubx%9`K!o6J35_SDh3xvF3 zwh^jn6(iwDsH+vc%0ed*8?k-U>hh5B4WV%%e4&fV@1cLMN;B36 zkWU92w=|7-dKr}uw-1*mv^!^O$R^w=aV+|*Z669OjDV}eonmccZ5z86*5h}N%a&{y zD3RZAY;WfQmy`=s>)p@1ww!U__ID2PFQao)v9wQ0pkm^SOd&1l4}V?LK}G*q4H_5u zQeZ_bW+)H-;z?wqQkO8F23P)?M`J{_8?Uq_J@|(Oyz49EdH{>oL!UO6X`sIQ}fuQIKCO?rm_OV^Tfh%Gl;OSMn*Ie|)V0;v?S45sr#{;%>um zjlVK)gxChycG2b7YY|RXh2GJ*w#n$czLtX)ECeH_P1%(W2>RyzxLUPC?1)wxGARBy z&%TPqz0%v}y#}rX+ssxtWBKIpqP%Cbv4_{eP9Oh$2amT+voI*Nv6t)S6^QRv&&;yO zv0`LkL;Q^P56RxBGJ<`7({C+%@779t%&g1^ zT#Q{6xRYFc6|Y90D2yqEe{_(56&#+W)hMQ+cEba04O($6M_?_se=7~_{nA&OHfptl zaMnYp2~SNT7&w+fd590E3g9-e_lK#JM6qnm(>%L8dc?}^PV$3}&DTOYLOzY%Ab1H3 zi0v&KMbCDT`L{$ZX5mkWOcPq`Pl;q2bOtjT&+?~J7?ercoIe|V+j~7!f0K5_VXII% zzKh`yLqrn0i7HW5$;RT%qB~+0tH^SUw^!@t+`6U)ySN@^ndwryen58U<`GlzZT~u z@T3k(b=BN4!8U@isIWze5H89g9)cy?C)9v&Lj9z7tx--ZlHm^!RVET|LudEiXpx@? zEZCgcj*)JX=O7TZ?F#i<-W2Q&^8D)E83kAv)hHQazj98RS#T&6#b&ml<;A^B6|T^L z#K|{)Dc&>Eq`?8gVjux;K$SoI3NYzZu>$zD)ugtkDsfTG*qf&ne>vRN>p0V5g!;Pa zSjp??hn^p1a=kt;51##pvCu!{EdbBiN(9wHX_WyyVwCS2-Q=C~6ND134jLXnqHQ#@=BadeuODb~dIF%wx4QiAf+;E<6AMKBN!ORA}14dh6tvW1lOUe zMTga`!ILzVAkfxTRy^$#q}XtM1|-Cv$197cK1`y{8OvFiza6j$3aA;k+k8+0UjlMk zSerbT$~!na8J%+*0N#vPCRdijR3aRKeR1K?7~3V~iqL;D1fVCR&G6G1u|l#~o?q(8 zDoL}bxZYPCY%AU(I&LeUZ5rtwjo=`-F9ha7DbR%Vm+%)kv*<(x%3kD&>T+1~>+v(VSg}TqYfVec87iE)2Fs(})C&s;W<*DIFaw6dV3#gC z+t+>tvvYj5bz#Plmu9|fG?xb`RT1cx;lA^awt|A#VvHZET7z4V+5|>FSXYTs#F1k}(6+8HPr&#O4!HF*~a50#51~h*%18n(s zdG9Fx;g7)s@w65#34fbC-M=&Ew7Lw~C20tB%u=JC_i*Rop{e=dfGW)~RpZ>t z+20TTDBCH~`yM?>!5|DVYrmo;4O({1Dv!K0SR)MUYeC}=v4|yptS`}$?c(k517s&s z>T5E)_9^-7b7+{t(;x1i*&P+v@S^_q^mowyuF*EWKzwyASP)djEw2#z@h;UqnWjnM z0Js^%s+_nx`5D5+3XRHU4+LHBn8=A#C#x4LJcuOWa8q`>Il5Ls%HPI1fFn^|?_8h1 z;1cm25)_2|b$e6P_rVq{a^i{&9NLN60qrKo8W2vD!GSXiZL??Jb3gGEvLZJtjAUQi z>hV!@cXP(rge`Ky#~qCTQKTA3E}7DRpgzwC?F6!d{%xYwUIuiE)FY^bZm)HhNQtC}Q z;^X&}U#k8XzL<9G`;K)ErAOE|P==s1c7b*iS<8IMOh&Ia^L>cN(UDNSp>cmLGoeKd z>b=#QO6`UJ`hc-5a}NSOnukbO=gh(YdqTixpHDWaN;T?DrpOwqkTwN+ALV^3WnFdi4&9blIaIr!@MJx zgB47rQQKM1QiJa&p8TUT{mEOMB#G$)c8Bakcka;UWW{d5%e<_j%xj1@sVedxo3sha zpM~otnHz+r&`^CWL0eUsn204knfcSR_Uns|kwhb|ZF_FfL|BFGkK8AfQCb4jwTT<2 zCF#ss$+>8r0cW`b2nC#mO1Q;5|4Bc}4QgXEnvU8CA!A$Q@HzCu#2tezeb@+&(nzN| zVo+amM6)`dn}O9lTa(O{lIb2h>v7tstRvxDs+Mprl+p+d0Yp-MPNrP)hN2aDQjMGyXq$Q^M*F8w{fAAU^R z6Ev`d+lY5+2hAwB)TIq)gcg%$N4P>${K$IQO9h(M_0NXRhfCKvM;WG`2pdZWdX?#mqoH(Q!DMqEb9j^6&$TC|E`&7x2yL{?D=b(Bo^#6#2cre#$!+ga)Vw| zXjPBE*n@zN;e4)o*r-!nUen=24D68 zekpET%p)Fc&O3&mI!p>=-Xtj*fgkc_k*r`v5ZNkl4O0#1>cGmdeO!fJ1=NqeW1y+T z#p&z;Om+Q>f+Gf%!m>9{>+`N(xM5D}M6X?^hYYoK`T|)rpEoq1aaK0U|8cA>?M%HuK}R5 zQj-rWn8e@SJirDDIx4mUX0oD2w%)D*`m*}=p(ayy^PXTS0ZQ=uAT?*tPl-3RsYTC4 zD^GzY*^&Xnm2UzuK~aIWw#5Vp<2od?Y!>$+SBRr+jj>ZGNyLhz+6%8@HhDY{T*qZ?s|SZXzwY~!ZmCM>2rsxd2;NDG9J1;^|IbB zTFbLOGlNIix+ksqmkHh~W%3R$^{n@_|cxP9ZcurA1xY zQ&X$$yZ>BkP&Cd*ucPTHJzC1W(fu9oSr^{%Q>4Adg$nthp}&It#&RWuRWL~)LGXp* z+ofpR#!0P~qyXC1fQG5|AY6jF2^8ELCzhHX79YGB9!fHZUBbx*h}XwB#?vVm>y>et zL6Hp;uk!GtYZS~|9)QT72m3w_MCm{RxZAT9mg6|2F(SAsY|2f39b*Udu7qdvj$=3U zZApF@bWg9CZ<=p!xRhV^5nFBJle^OQ=#pr$`l)0j8#lUo0ALSw^=-_tL#}Wi`*khp z7E_A2lb{13+MX|B)}n73>Y1{gfw^?8fx_$=FT02N#}RTLJ@7%+R8LCXdH0Qa-|H;9 zTV#l@t)Y7UZBZc&yODSV&g-1e7ZnDZ85E!Mm|d~DG2j3lPGzYb<`2*ttZwh_rzTud zCSVv&AGcq6{vwU-^yQUOqUP$}xm1DvS6X%5P2QVpa19VDRVH_0Qp86^4+z*N_!3KK zoR)BrXO$9MVP9d`jgb7By)ff)#uIz(aNRIh>Y(Q8BP;rI4x`9P%W^=-qtqI6Z8>fj zTONgL%pq@fD8^r#W=EfBWo||x11ed)tEaCQRqKBU_nf{sY@}OGlrA0QaxWnR;{qkv zhRkI3?XWCwR=g>TSKc{dfMp?WgJe@I33OqN`t_&CRuH8Xy(MTRK3(1FuGWXSi;U*c zTg#>2K=d$tx7!xrQqaxM?@(b>Q4MLqv zeyRhQ#P$|l!dVzMNlIWU&M!G2z7o}%q2*XJ<(2s^gDpYE1)8TWhY37Osk8A^iq6i= zGfo>AkI$|dLQmc!AyJ2f{=5=yVlW#aA4pOlT_UXl{0mAv%@0Gq{^&-ob8iP)<|C|Y z*9<3$$kBd&Yf(p_Yw4W$=7B|kA9(y2zD`O1Bi+l6Z>`)sz##*EkSWVQ0i_OKF-XI# zadjRR8(_U9c#rOu5u6~qx?)!9u6c)H{aTgsS?ll*6q~`F*`1>yR~2!8wLbfBA5mte z!c%OeZ4nw0JSP#VutS%(Qm!qvn&)uah-=V3p>l8RTxIiMNRWr#+qxW*e}Q)4^8;@^ z)~9B>xPA;;i1!pd0X})aL>wc~6PiG2HKIg;9z52Drdm??&b632p#v|6Zl`6d*s|BQ zy!1N=SyIzVmk&2Y=`-u?)(yRM*u5j;Jff695}XVotwskUN54;YB^yPCSH;ol>~nZ0 zETDDcZ_|4sCmm8r8wsX3P8if4;pxg%SKuk$kn70Q=QPlrAWem@O5t$};;F%K(`YUG zmLc2~WnJmR{a^TBt~pLP7Z*8b4(vw~bS&RS)k=`eJTTm49_U_^WJL(K(VTgg9-66f zkRUQW0U4yU$i&s=wB=#|6;_fHMpg5_`!Z;d)pDCD!P*(S|Xut80iC==XZV zd4uU^G73AcqvJ^d^B<%Ip@hv^R>R-$b zOLRf+CBGh5eVG1AXT!@KVaxyj;F9O&lbbLsbi5ecnlwTrn=AuswW7B4)goo#nof27 z;YFr2?Aaz(@8j;K4K2;f;$z%j#0{M6tGs+ZHz>LMZsDyBK3L84GwzRdhDt2+ZH69O zrYau*pq~+tdN|jAr&v#8d!?V&LyP`mms*>!fPt*Qp8TB@itRcs8m9*f_L*yu$Y|&@wrVo`=?Q6usa7MO*jT!Dl@wCNa_`JejDD_sj0d- zL#fqT;pm4>dQfoSq79>1hwAs*>qyedmB-p<908tR-uS`$#7<%a>G!e zBWVA_T9jF=SO>_*K86eFCJKeDX5=}ZLTq86_~6hLDtZIf0|?PiGssD`je3gCJiRYu zR-;Pe?aXsSKG2u=%9*WWjEWj7zrt~^gZmV|pGt4ZcaPFGraEOVq_`U9Eu=V)eXTVP zxR1yq$O9(fEA-ZueL?6|Evn)nQVotQK8n9O+wKA?fd?k5(lDFNoO!;-nxWXY*~ER+ zVWL`q_ul8jhK-f_-?T3b`WC!9{4C>e+Gk(CT`d_8Q6g2;9(6HN$?k07nV?P7u{+K- zSK&aXt0@nQ6*&0U7|X58x)=6;Uk=%B-4I^ZtY!1?OlFBAdfT4Dy{|j1ZTgezO8?PM zMl&%&CtmlzO;iEEJcWx8M9olY&~%t+A`F2evE^{DyC|KgEZqM-Sf~MtYz}nF8vK=k z4zz+;s#jyiDZqQ{Q)kZ}kC|@^?4w047sk%EJS8*A_O_uX?tCiT(A89|ZFwLKsDQXs zJUX|#uL!@i6SMN{-2U7gB%?r1`Fqd64k8i`N&eSP*JjxOH60Q$Ydu;l_ zlVv}6*$_RdztARbTiEuT+Wf4sUUWCtwA?!QQ`)IdsdqK4<+GCh3}gzmaf!&q2sG6| zEQfgpXjUiGyllev4gx3DqmJb^_6JiQ0?BXv{*;GHDGwL1Gj~%>?#w#HdN`$eIA17M zviNR!CZowuNpH83o|A_m5-M)dB=vn1zo)@=#5ZWs<}gxQHVoJ`R-uC9px9z0*bC6- z1A_|x(@*}wAvbxltwd_Q++TRd$Xp@lI_R?cH`fgW@BU|J|G#p~_2jv!Dtuu(a`U%6 zJaS?!QYk1`s{>7Lkp$Mte04Q|c3`aGaIh3yZ^CkzO5~1ws$ofzX3j<$KBG0Mf#`K0vEgECIq4nnq>6q>A@N ztIW_|mn|KbAkqYZTgPMNPy8(=@xKc&lC4b0-rb;7fb+XZQdoV%fNeffs$%O2+x;Zp!PiZZ1r;*#R-lM;*h0Zvkmi3 zC7-TMGa9ZS+^zC$&^!1t_=|*)C;Cfx2xJvxZd57$5T6dG`UotEeFghrBoEQST!%Ue zd!BsyOBUV~Lbfrt5aqDQZA7JM(5n~Ld^B^iR@9pY$jK$AgSxA>;CoN=ZcSV0n;pYR z-kzhrB02v%iHtORCVBDxhOhTGP{gP(GANrF{FEqD$nh5ZJAIV@O7R&<0ZV_~TKJeR zN94*9eaYc`IRBo6?Uf?FTA?K6wAF{e9*E?L1MkD~J?XiAEf3^K#cn-OPuxa7I;ek` z`-Z&yrndEq zJyU^armc5>o;=<6Vf06EIJM#8YGB>~5~9^`iT5Bus~B5graY0uIf_MEl`udG@+Dc}vm?56}S zET@yr1~`SPa6N+z_$RWXo<+>xtqKowpN!x0<$(EZ3(vpPH)*3 zfL5A>k3xB^CZ9{%F?ju17o;`uP zzH2%y_O@egO(5XMVFMf~GV*v>jrKG3O)w2~-~F-h*{g&*3ypML_XtA#FYewvs)@AS z7llRzQHY3$jA=y$nbInV0wzun5CIXHg*4L05HXE_LR!RvUfN zl)SK~QIl_<&-cgkmZ?jWHTVo3T{C=q)6f!7-BK%Bd#BL9vir;E6)<7zJ`aPV))Ly?Dk@bPJBG zkXQ;`vJuxw$6wkPn+>(p&(bsVa zMiZdC-kB}bZzYYBBiDtm*Anjx{EfV;Z}Z1V#oOdAuF{-rJ!u<^pK!`g*gV-1pbB|r z@CvYJdgr!QM;v!HA0Q1b@~GZpj@#90IX7x9Ww(-!irr-IxH>XZ*+9SsxQ_=|$8mK@ zlylq>0l%;UGH~p-oHy=8Y#{d+?yrkLaw;yDd^jU;sH^IUcw_dhCE&zG|D0QI$EvQN zec;ugbA;g`=qTSxqMmDatJ&AQW&$;Gi$|YdE%f8pWrYYY-CaCG9zkkFW0Yb2$(CFZ zYEy2Zp%XI2db!Oe!8|nLkK|ppM2{m|)!vJB&;zs;jH= z=6yeeej{bERAhY^=6K3P1}%lF#1!krijGOk(Q@Qoy0QgVotXb>+_l||!dswP7z#H| zE!SB^p%RxuWtzl%{)cpFm9xp|7lY;&gFeMoeviI1E^=HGkn@5EvjIDL&NHbY?Z_(A zjBS3MdQ%xEd(b1CdZRVv@sh^k`e~)-yiNrCNn`v3g$U4Kh8)8Qy zmiL~mB~vFM;aH)dX_>6lXY0e`uAe2Zf=h(XKN*{I3kS+A#c786n6Xo8|khNe(p=LYT1bzV$P*P|l-L{Z3i8N&{CGm4BM3aqYbZ@fgF8EEA`SRtl^LJ=6h$I#ICC^mdV`tB zpJ(|UnelNL*{W0Eo&KB-9j3wCZR6s?=nw~~IND=&k9nd)fIKi!cjMgp3+gRmN&8=c zW0xk~Ycf!q0swRP;${J?F>#1$@t zh2C|57jN%a=#bX8HRX5KYaT;MJbYIBoWM(myK(fyX3dArdYu>dR=)%n9!$xbensi&Q-Dzm;L%4=}KmT_q`S!1Z|$^85po5WouKwhckw+ zicLTr)a*J;oHy@minauoC@?_1G7OY!`TJ$;LjJl2G89oNa2T8*vks0I z6ol~1i-d&Zpt^q8*XE_K*$C*W4L&^Qo$t2tr6~T6&Qzfde%!Qg)Oo+ZS?>sE54Y`E zX^t(X8Ch8>Q3L+$oURPqX4{`I|Lvg$#|d^K%N$UiCR3I0y+G+!tI^wR?U8f&%UG|uLGjfsz=_AZp5Z}v~D|j*^gxRvSl&3J~ zl}xU#n}Gfe44k3>#;0!xM*-1eV^-g3=1XB|s%z#*%E;V;DWhg}HP>`AfRJIl&%U}L zv^98ZEV$1$NfgA^aH)VVmBDR<)I1-G83Pe}n( zgMBG?<_&zZi}#pbs{dnmncR0x4yBSH*$}tbL|;WVATG+*vcn*N%*?Nins9@<+GUt- z+nA6E&J*IIowe&V3&Q3ZQGjJ3N)8u}i^zKSA;zxSufpYvy5XJ67U#!{es)|+L0e#Z7J@DAkXTozSVhxhU z5EG+0UAywRfDyWql8JL3vXBy7GRMO6@WVuQhu;CS9gf?)FEz6+Ouf3N0+!QNfcSb1 zo#KTXcH)`d#Kx@dM<-0u?il2~dn0hA79cYI`AWL;^iJ-VhFhkY+Rjflb7e|DZ%@+I z2_hvBb6r?d?xS;Zh-Gt&Fs${t&e3zSmG&9A2fJt$LOzY2QRe~E=OhqaLyHNm}FqjEOl;_?wllGpiB)3>$vL9Aw#Ki;>owdGyUedCFBTwE!bmgE-X?jfZ_93)9vF>>H{rW!+ z?CZt7_j>}g25)<$bRw$ z{Gex^UR{=D*G6Jr6Kfmvus~XPqafJU)YgFUK7g3%sL?Qgwu=y^xMDw6R`Kzz^VjxP z#z-!oKCd7qPqU2SV(5vp*p#Rs3IjjZHIugjBNz0y8el;sIj0sHu)jY9%JgOCW&K?V zTL!!vpK-eQY4WcEdcu{1z&N}F5(pNWx<=xgk-xA*yHv(}iMJ*y8)0A|vS|d)KP!}wG$-Ep4txzH`I1}iu4?Iw8XmoQ%NnNCMW_O`6Zgr&=dA}ay00k z?*}4+UzjHN;DRsMIf^gva_wE359s~edd@pmCpp1>rzd#yOs5pO8*md5R={doXm#Z* zYmkHt@ZjuQ1Hq`9MD58ePG5hfvdQFux6_x8#3Z-1KKh@zg$jWeAtF6bvOa zv^RlGF0`h<;=Yf8*s|eI=3i}><(AoFPEBY-m?s7gW79qflIOHG#tAbbXv1k^!rAF@ zRSti(^BN5~T%{OpGIzbj!Q%wf8)m&wlO~~9{PrOEfNw{$H4{1_9P6YrDZ%W_01Ig` zS@{g{7QemOk9J+(94znY4O)EUYO6F0oX)hD;qmAD${tVY@9J6hfiti#YHu5NYMbIC zSM|(|Z84Sq?(qPei7m@&23fV-Oo0t^Q=|QMz$i#8s_Bs#!ubL@q3X>QN-98waVf|x z58?R#4J?g&aa)@*K;?hx3CbN1F6XrS`&uJbt zk=hTnan(sUz1L|c@-E!rapj$K>-_GF+z+!a*`)NQV{x~#=9eo7mce>8g@lQFx*>h< z`=%6(Ctd3cLvqYwJ*w=-YDY)))QV`~#s$>N#m-yb9}aqH|NZazX5KO6h!vR9?VY8= z2p(G&@<>kh`lpQTr?BZ21uR^P2eBe&U}Cy{Wm2X96Mo3unUTX(fxo9hfQ1MWScr6a z+O7mo3D?}w5^k>YVJ5`1v>_1RD<22IF}~XsjtslKP3?T)Q~k?)uny-6sS?UsQzyL# z`YWZ6&-1>x!eLak2QQyHoi03wsWg1${Ln2VV*MXa0yjiZ$=*Yk{7lJX_kMZN!T7Gj zbQNpbg5|Unqj*vg3#M5@%Dj{SktaoAL$r2edKzT&Y9)k*Mi8sU1ga&xHX$lerb86) zfNYim&@te2&rRD`7e^ZPr*WKc`mh(L^JxJ7>-36VV}`@fXl?f23->;^p3dHKs=`%6 zS9M#``GdzTOErw6BR90T=jc~>7;cHzjuE(cG(IV^bI&XYzdarG#wKu-8W0vZbGAt7 z-Ki3%{IS!eTaJ6|>Nzd2u40_h?J3d*0XZHeN<5zRySq4E1EwykI!gFUJaUtai>dCK ztAqqCIg}d3v;}qqkPqW|7+@*xWu5(Ky-8y+)U3 z84=^#sUW%VBD`C?EmYqYBtfksccXX1DT1y{%tp!|sEpC}D-B1H?VT50m4^d}jKf2= zHV-&L^9aL$-tq5D`UWDU#{TZJ-&#(Wn{L__3~Ogumb<4n4Yq_>d>#FsaJ-O{i;!mI z^uLO?j+yGqQR5r&qa38JYAK~y?Fg?{J7cQrWI8?{;6g6%9UXE7v|Bk?Pujj6*@D3M{0PWaEtH2G(WJzrQiT1Eb3%)e><2_1g`Ys1EKxrEnNeB8N%x{i)JXCI57`v&np}GcSN$$Se3qC$kqstZ zujnf08vs2e)pMCG*I@AQg9))J0^|FvF>m~!s-Ew${;^pUY-lxah-mA|WHYU!5F=yj0q(_2r$ zV?S%{I+CfGTjk%__qsEMw$A`*N2%#THSwemF7GU`@@%}^@mrY@%jfum0b7;*?YW4Q zV(mzVORbd#R>L%MPv&nU3BEDtVpp|*I^)l!9d3Os?#(@~IC<8TDlsY8fg#GY3%4u} zoWv?5F_LKSFe+$6au23Dcg|gWQu3!zu?-=A6f)-sy5?_yl_7(HfWdEUH5`tljrZ3)RGWMHdCh>P-Sg@}^f`D3 zxWGzqs{p6W;ju?GWy;tIzgoZ7tRg<$x^GyRE!c?Z7MbogmE}To2h=8Ss7nEGq9(rb%0osR-x^r za;vP|Ie04|f>dA;-7OT30EoO1S)>~xLG z*I$UdUJK6omKeKHkNIP4W~ z_zI%}FY>JtqcN+!!|57k66d1nZa98U*fzQp_pmqy7?vmq%SHF356G7O{*q(T`?hXT zP#rI%36X*}>$W{mL_)zwc@-$p?Po$p8)eLjmwQcS1#^Aaa16SvbSl6Yc83@|!_4#Z z4F)m1#(jd5FA`7o`Jz;~PWSyIQcgx*J0)8B`rh0vr3}mWcEqMYH%gPTeMzidcR)sT z+OVM9+uX74h{dOrwy5vKao-rBa^W^desOAry<7Hycw9w@O2WmxqKlV+au`6^X!V>CqFFYw2<(LMPFcK_C;hft{gv0fUAKqh04#| zi#;P0PjS@uH)l_3oleZ&|NMw~$?>~=nF+v&!aRK7MPtE^4)^7o)aia)eRi{Y+5A?Q zJu!OLcb{4<@0j~2IOS*<7mZsm!G6drvkwP3=KZu`9CV@p)0#(^QxTgs37kPoc4S}@ z_9>aUWWA>J9;Mlk0v-h=yw}U*N}z$|ETh%LdThzog`f$W1(i`1-LY4rr$(p;i>MWE zP#-@tdh=493gi{Xd%P) zFt6aknBS00Z?;of5FkqkJ2RsPAH2TGR#cF_Z%Y7!O zK}r$z@2FQcPX6}=_}5P)Fd19*5oDDnz)7HL1n;!k<%ir*fo!N|nfg;<6%Z)Q=M|K; zp_0)k7*7$<(1PpaY@$Z0rh*7Pf=!4|Da?P3n1 z_HejXzrr|cE$J6+L<^hBMq2h%gpTikh5QO+yBiVV)zJHt&GS7o44|1X2;7Kbqu1f1 z-#*iz4$j2@rhCAShFo{Qrr5|;z0OvvdE8+u4v@v6IH>i8EjY~?>xE_pHDEtH&%blm?pR{XGq9-(I zXKfH-ISkOdXp@bI87+Te@6F`3w@|i7;<_y$Bm!CzpQx0O15lB=g&VazH#nszx)zNJ``I|QGnUIo*@sB(%{BNZ1I_n1B9Hz zXf~f#x%0*28P*=5-cTCjOHD{6GubA;wsGLox$NRF|NQuB$D3T|Z!>!jDY6dSzoH&K zd8y}a#Je?@zKzjObZcPk6tooNA`6*v5&rd;sL$Kpt2|$7FOfeqbuVcDD+R3s2MsT{w$hZ$sBb>TZ;Pc7l3c(Di(N5)QS#xi$=LX`@4M{BhxJ;s zN(nKWo^|QoDBF01Yrfk*Yd|*ht?Pqc0+}gy{zPKs#WnJyd*rrWQPf?3_4}#65GN7+ zA>X?dn<&o~B-h#JabjC;CU?b&BmyzdE)5 zV`MM2SX}&7|Aj1Cj-68XsrQybcVyRbx!?BQUB4nIefnSAxbhb7NXU!M9{dC`ZSm7%G*IEc^5BM(URyJZKEKzF8~IvU$|`0*RU6u*+E??7q-^*uZjgG zfqFGb#iPQ>6$L4y0R^oSr9qRD-N-h*W9^s4M~Oi#LqK%0G=EgQx8*PPwNqVwuA7OM z4i)8yod(EFQM#4%_nn*0b4{)uLgsSi*yl0nba^1bq`^jak<5qH4@vgIWr@0%8=MO{ zHGxr2gsgKDTPJ!Jm7=dLq+EGIFn_Qmg|8{fO|#u~e`aus+$Aj&x$j*&T#=vyHh?*4 zhHO88@pp+$rUQ1vyoyfj7?QtM|Ehx+TjKzf}tzf3t#0e=*U{j!AMG`HbnjnMlWLF_VU>{P-#ay$aE#z z!kD|Ggw&ey(!mP`ehi)PQN4#1!Mu8`){Vj0Q-4WsIs9$)4hfwCav35RfBJ#$ZmtP3 z@$N%OFya8>zk}@40N7?4O_Brq_ZcYm>)XC`d{uN{zH03b{tY+|P(lV`)gtVZ0FAG;S#* zw9`}f4btkH=_(DFB6z2h=fL)nNxbi8deFI>{_pLnH$a9Q*fbu2HvQpfU9xPm#H(MX zk0Uo>su~8htulY_Y1qrJ2f9%4p`#X_n^#14_z~zC9seU^%`!cok2m+*HFL_!FVJG- z$Q2~bL6U(0arMu`Jht{2=@cYOD#jPC86+FnT_#Y%_*^yO9z1s~{PfGg<6G;z9Acza zp6QN5%jP27^UFnlIgo$C5j$r_$4kLIBZDIYA2-x#qpnLLg{4gx1ZpQ?HvzELu>57J z1xBeOd@k1VZd}a$QO@wuK<%fS~tuGy9-Dal*gv$SRQ1R2$MU1bU!aaql zgD$SKht_mk8zHYODSVgz?go~E1G=%_R=VY{Sj%A$q#ttDzghoI#?cnw+Ui?N z6T&Ena5IEXV7VcuMiZlr@8))hp+S1{7Xg(6|a``VxzyrOT+dV=^S~a^=@Bg zh)s27%!fnD8eTT1*WYxJ)7qZ1T~PMkb<*k3+NGE z+26<#xxgveCg?Mj!kEcBcOy9B5uEc`hkEI|+3T160fceX%tmryYi(pSpi(*Zybhc3|xHFvHgvrH0*nb{b|&4R;jNrmr{9 zum^Q0|C-pVKi^C!ebDf5mI%D{GbvN&l zOUYl~@W;Q59;){1-1N^)RH{T-`nOC?_Rf)ChSu>T9bpaHMxtq`3{sGgAc$>S7}epM zvk@tQ>_iUGyMk`*DxAWnY;fX94_Nx2nO-4?{_2~<`83=d9MnRYYde)5IJMGlC~0-4 z9*Ek3x@2kKb}=d{{y>yn)alZWQ*P4tCB_jg?`n*^R!W)F@ih<1-6NNA%z|BRxi&PD zGGxfEi+yEXE*r_cYE)O^@`|wv5as`iVrs&tT*qiOU_f05f-Yl$w~ny4?+g>37CX(% zy~NIS0sqBn$Cte%AF&;<1R^AVb4IlkZiIaj9s)a`$rQ`ZonN#n%s*3$8*%xUKAl8> zA!%{}3}&=PhJF$$Q>kJv%3@BjOgomxmafAA{^vHUdH)}B?|iQy;4m%#`-b3y!Gy%LI z4VZv~lG)O$d=Peo;j`08mJ|wrT&8Q|l4+tY1YXQ|z^5PF?wZZyjc`KAtp3{E!vmUrIRd_C-30uksWN|Vxp(x# zIiz7%rhwiE6k`CKlUeo43<)6t3N>o_h`L7 zwFkds5u$J~UK;vugB7_a34SMP1}eTRNsqi4JJq-FB&4eT8LKvV+4W`3OX|e)+J>rn zGmZX@N4IBet0D>O?a7|4a-=RHUgQI=oXy0AJJ8cosiNi<#E3 z*S{f05j;#YH!Z*h8Z6jR^04s{?9z2g*7W@5#bqzDTH|1G-Xg~_()7=NW(iiKa(9Yu zhIu&sw#qjbu*p!f^z{{Fm050e>??*EX*;u@>stp0G9(7j=E{lP6|ZANBf!V**tbSy zJZ3%(RUJJKaEPu^hVJgi4?@$^A3t6#cOmnN{A3n0)5=8E{&eQG!&+PZF+Qv~6{;X$ zq+*^S=gfqZHeYTFDxF*DHCG_CX&?RA9Y9P3hQvQnwM3bvkX#?5_q-;{Ii!WW1B&@1 zDmM0Q*Uag4Y1uLm>xCC!Mr=d@FfC%j36qMfc727@8freBiN(WlopIQD~*@ zP3L*Dhz2eFx_j@jJiLRco0%Z1Gh)CXqXUM+e4gpLMxM&X1X zuMu=-N7-}Vpdx(KfrNvt)1L(9e9ItnRr2r6N~SFAPZZAI)O%ba}uzWB%Csv0#1efgL2_+P#_ z_XYR|ILBB}EpG0WiyQc_AW8ZCt5&>jN<5%anr#rziE7B3Z0W@Eu&JNxnRy7!utJ?~ zl1HRU!#T$tOn;LW=yk}xZ*LZY-+bwmBTmk7#g;H_qApYR8|aO=Z4vPTuPoNy09fx^ z0l^1KNh0a3$q$>*TeEzm#}wQ#&6g^h+vlt^o5TD*pRM^e|KA>SJ)*(3pL^%}@`Bi7 z)pzC@mIcPW{DZ9E#&<;t?{3dkV6R zKcUTTP7$c#M?vXw(+D()bPi?dw}}rORP>h7eb}-Wn-Wd4l=AtrE<3Qo2$t zUMJTV{~grxZTP|M@JsgFbMRc%2DgT53FQW+>Kg7HN`6RSr1*(;J@(Qvn~9v?8X0C` zwXaKjBJ`R`{^k3HYp;<(>Vd}SuE0y4>wb+n$gyu`{oetc|L4gW06iC7=%71IY}tsf z1`6Nv@U*}TaZH$-B~fMV?6o9}^f~v&B>3@$s9^b&15LLRQ%IPXe>me<3MwMYa_l<) zVc*xLk*4FvpJW`;#y~I0K*Uk}bN4?b10oLf+mz<%pCS%>>d(!e=kzZThs#OO+pa4G z(hO~@pA|(CYOeT)T+acl#GDQ!C81nUQfy-Xl$0F%1f&`Nn;lcJNMZ(O1Pk4px?xtw zAt7)#;R8>SBuk_!@kuChY-JxCT5Q)hfaCdo>z1Kf_T#X{@8qoz>(REHu9Gj5LrE_@ zUFqQ~KVzhRsxba1jgNEYj2>_KukcNI0gkEIng$;4okCPg2L*AMUtXcS*qoQ}G%@C> zRq{OQLW7S0iGZy~p4PPVd|T635lF{nUEc0zll*o5j_01YN|3*os7|=^7SBIm2A5p8 zpo*vc*TGf5Io$(2eCOuI8Ip%-ts{Y z6RZ4Ie(IJa6wZy}gQjpl#N}Xq0q#l(UDwQITZzp^GBbQFSGi#a?==bg2*z{N#w7N% z3CT)Is%3@eqEmn7z7AggA4$>9N8ddkBU~7|Tbe;RPg8-xi433*;Bl(9v3$LdBXAQB zF*558AB02;P1qim7e;{x)9 z1}!R&e8QJ~&2!D#upFAcZ19JiE<2PlY9iV}7o`ia%_AUqB?FNhx134|qpc<)Cdj40 z^e(j){(cWfET2eXrvjrT*JNKtlC1&Z!8BQ0SV7M!29B8Kt{cLA$+;jfQL^jd2mk44 z`A*Vacv5@;sN&b650ZQ1FtmB}7Gk~#D`d3N6Z0r@Jf16pgdRX|l=ad#QG%<{SA}0@ z`Iz*EpotH-Rz1i&m^ey}~b7gPP@o-e`<}YAVxWR>-xtT+_4Yp30R@ZbS^l<_94zRs-@{$ml0hUD) zxer;1OZo}+L~w&*CMDa+x9-TWVY)?s+e`Z&az41qCAk>UJpIJb;s+LVO#tn%4RukP?if0iJLQKwI-J-4ME-=>tdhme@5yT%aja^?eU4#j2a|!C{Zb zYcf?|y1Y=$j=o^IsXoz`g?e$8=8oxf1H|NTF}i|YLxr zjGM~F(0cv6fgorh)e^BTd~wX~PqT<`(j8`F9(_giIT~5g`H~mj7xX`rWa&H#slG8d zGs@rc()hnxg5q0s0;$n2)<*wtGmi!Apf0s3HS#|{t+aPZ|!KG>E zwbMUcx^HUTnn!MbJW;Otwq}VS*)9WZZBfcm@Lkk^YGC`xHJ|>gxcP6kU;nFq=c(5& z$$aT5y&?hcSR&hU4{;L>?t|AYvR<6%;))cYg|dfIr|atSia%DqesOsdWag^lS+qZ%uwzo{c^Jpx;0{X2;th(_j<5{SLbZZbl0{+6uxz-p}0h ziz8IW_CnIq_n^)#i%z45jmE#G_kUM>g@N3qC=nT!7pCw-+FjKoUhw1uCIwToiK_rC z-MG|TWR>RJQSs6DaGYSGi$h6UcUiayB!vFJO?VOuVbh0N(S}01hqRJ0IR4mB4MP=y zm>ra9KdzLOAyf+>YmiA(cB5?wjyi`z0EsJiB3PCFx^EMQ#2cm0ag3v9)*J*W;)E z+`#F16P%h?W<2ccvDeldXM8a!{8Y=LTUmO${q2nf$C=a5`Nnp?e{#}rF_HSbP} z`}_M~R^X}3aO^NY9IKGpawJ+>aNOIWpI%+O%e`DXdTKB|^Ku!##MFY6MSzVp2DtNz z(5*AL>o`E90oy4?homgTW-t*OiMKdNg}`3co>l>36f3faEg`mGC^4sxU&2izSwUQ< zKAFXA>`D?nwL-GD!s$LP|9R4lcwcM!b9BD1_a~i&XVnN%-cmLj0-op#u!Vu|pSmx~JhYmGbGk5W#!k3Jn98E8<-cyXdrO@d*Hm07;2(XAe z9rmUoyduWcDwi#F|+1uXn*OKveU)BOfywN$ga~ZZbjnKc{JlI zwjBqXVA|$Q1g>+uGKnIX+1bu=SFupUD93!xGq~fJ^lo$`O3*%2H}|rem^~%(jsu27 z2O@9e7>LnsC0%y?rP{r@0|K0$X=T)Dq4#hgw~iA?E;V{*c5xu-%+8%P;kVCxKl4az ziy1mlvZW(u52s!FxI|a?hn%O;)2KuLnJfn&`EPvsXzU?iou?2`sPxg9d7m_?e{*_tPcLBH%tr7DU5|?JX(pJ1Uspgi>VHLPA?Fee~ zhw$CyLAU~U7yX{+Z|+t&jV!*|U8ZToh!EEwx5~_C-VQc*v@bQybbeBB4iISdtptf6PJe_?HfCqdvEDGES`A`Zw>NGTd4sSny7fJ$8|pO|mw9CQT`&QjOLgDrH`h8^XjaK3CKMva(FsEG)^VCwm9OOUsC_Ux`xErX@& z59-<(IcaQpqgq#e8-F_Bb>GEY=BK7Nn7JxC+c#5MxchXThvsTsLT&TOjArrLf#Q@i zy`N%=0aY_?*Q4>=TEH3=?nxI#7bAm{s5UQYqRr`__x)R$Z7blrw$c^bhE)g6IYQ-r1kUMt}H-UVGQS^p)v3|vm8we?B?65ZQO*?6QtCyleu1B+1lPtR} zRnCgN2Y7v^W)h3q(a;2fM@?UiZ%U(Ql1YGfTH9UHi@GxN{%LGpO;|#@#mwpOp0kgo zh3k-MwSVt-FI(mtxqqTAWglnt4fm7^Pgk5OunzYKc19Y?dT`Jwwi-zrVoNkMoA+>K z-2|aKs90?6fV-@|F@%|O6M{i?T{AnSHj4a)yGjzn8z#96OJ5n4RcFX~-u`UST<@d>&<^U}?b;yj?;uBFer3MeD{K;?&MM=HS?uPEzHWu|9HK0{w* z^5Fp6kR0Wid9{@BFze2YSaH}{FDKJUJ08uBmXxM{?4FYVXEoLL;ht>7x9>%?3;Ek> zc-tzy&nJhYJH$@roWbny!Z~uK>#kyIi+u@3;J)$+>opUjw`F4Xbd9lPahLrb;(`d9 znf+oAq2{@VYB59pTpO6u#0%EG(WsRW5HrnDclKCmMTTj4mV9~LKBsK*tMa6+*$=s2 zI~YI(9|TnJm+B7bn}K=8KbqJda?b+jKja?#HCO^V&Q+C51OOQCEbVWmdH^rKV0;%@iEbOCBlC8glxFZiJ}Zoa>%IXHM!2>Gng>H zR0}P$7dJe}Lyd`#Zxhsn*tyHa_}aLXL|_oEGA!Bt92P?t!sq(<{&x!5?ytBRmzmQ@ca22wl)AvjTC z6Iv|o*?=_z4Ip7`0#t4YKVr%$VQlgO(;wNazoQ4k(;2b=d`u{8T@+5>>*S*6h=QH9O6($*b|diB|-Na-){6fgTBQltS9ap6uVqD zE6F*qes59e$KdnjYKM@nUA*~UW?QsFGR&h7>I+(}&8bg`u~?z$O7Lygue~G;Kch=^ zCSYebv-6yTb89m7Q8;=7RM7hE$thE-TSS~o4*$URs@l-|l@L3b*%|9Ew9n^#Sm!FV zN)POrm$>px^bA~?LnN$XO3}-@?yW;L@$oUw2e(z$u^O{FFKoXNar3K3%GrOKuI$-a z1II#~=0ae9QwQNVT?)finII`v;GSo;t~p>+RfNl=yO7d?)PbLh?CPp(tu%A3-<3U? z@ycXH=W2S@_3gE&8d^Lmj&S}Vw`Rc=%4l|%?8=oXHCknsN}p1GPm6iYLSp7@4AUOM zc+@;se88$*%hJboA9=v=>exgdQ36*!1j}_i@sC;L7*c2=j*v^3hN^i8iB7Aqt1T{; zLjk^=U1;4OZ#j53T&EX8~X?-M#OydEIvOY zC2Ng$VBJk0<+dlB87>?{lc=l9ZoC#NQutfRU2MsMk-{RM+Z2b$qD)-JKqnX}GAk(O zh5(@qmmUT++gjMGjkQ)7kQl;8AYd9tn=$VsUOioC41pEK@`-O}#FJ1fYm3n&R#8ao z`kY1I4ht(ars*v19+0m7hnX`75BYyKb6)&u=1ks7?!bYDaSjDMXmRN*b6LOjcqJ&I z^MdvufJ6mK|R{Rj5Z%fpKoKe&NNi+NzPUw(b2(QvJ~1L(g^l zZuah1Sjm7=5+%DJaWn9_H^dYsLLuwcaX&MP)%5Wg2%*?z|bmahD8!WW>e6DzN%!1U?-z%qk z4R$UHEV5~06CmPvPD1>U8wlp5ub=_v^S|Lm|5?fXuln5+8`^iRXu4P)2HmWaWcMND zb`sbXZly^du;)H4p`>RqSF%L~VC3je`RBuZ%J2(5U3vhvzG7`{B*!QKQD2ym_9N2A zXmzXRg7|hiNO;VkAW24W0T1|HlT0bUpr{fP0pS|pa@m>)LtedXyP?^_{e_r+xY3)0 zoA{vh)a;ux;$+S9I-lQf>N>s%23@En*tL(aR9Q$hm&i{SEdQ*T=I=T_V6gvn{BgI> znfLpPqw&Y@mslCPSOyur+nD*{MX>f?*$3>tzS%RlC$7zn9P&>r1#-fz{eaJ*Z`2f3 zN-HWTp_gOmX`16^(uWmaDCZ2}3PGm=pOMJgBtZ*9uGUZx zG&okkV06=>S-4G_OWF%*HnUdsEnKLuRUzQfiI2j#)$NHoXENr_@@FFaJn=RaXqyc0 zxwz?}(&KwdTovv26GC!(sA!-4ZCUbmR0z4C}kf*>usuZvE4^9H+K@95BSD?MfLXHSWC^mOyxKc?S2neDG=o&r|{ zk>q{+Y@Yr)xHiM^B0^i|XdiVVeVGDz#Cwac79YtIpZR;sr==Z<4R=B3n4VqY>z)2Iz!R)?j;LgY^g12t z$JD&81!=YQAhqC~XzwgQKc&*#xh~#81p%rVk%d<7CTsf5;HsIN)_p68473t)^N=gO zoeSlSI~9+$o7O?MK-|p1eUEYNlCShCvp{l2sKg)=KbcyWyHvj1GWSq zOggW>w@>0=Dlt+1Ks@(eLKI%@`<>}$@`zHs#AS{LC|$7o#CtqcbfqZwU4t;P*iS7r z29FLO*uCX(Wkp(rXK8(>^?-qv(+4+dh0AYQi3}yL@__r-?v*cMa|-{W?nutrOx2Dg zy|IA$L)=IFmTzS{o^I)HamsO9X-mI$?LSE(0N0mn9AH_J^m4%9M4x3Nd&k-dwa?p# zaWkW?G_(RSv1wmi{|y|ukD_Nmb+;2LUYD5wt!v12Qb=)E5b?9+V1wl&=TiTpszt?J zrQsXeqbv6>K6)Bvx69r&*Cn*g?RHt6(B5)e!GYtdmIhjmePu4CW)A&l&W>eUy7~H| zgUd7Aa_f!i+dOZ(U)Xs2bhv%=`yz9&A5Z*^Q+k4GkPn*x>Pfmzg9bFqk!irYdB}8( zGV_BwZz$E)MvV0#$qPSM3o*RfQej4GfHfRZPAnriuxy0LR8&nl@V$W(Ig5ar9nRPD ze_FizJNXVo5$Qp>U~G|DhtU85wj^I+a$>Q+Iz#8E90BsBKKw< zD0hrE_il4@OIF_1uIL94zgT5mRjDz_T?0U!Js_1#6LM|lsLV9LpDVT=A`*R;!OV%yRe=^234(TvH&Z6JjS@t%VXu{=t8fKe}yM}V0JB9YMW zKiGToxTdmwPZY~i6x4`JfU)+)LUy6@IYoaoE~EO6h`WNL$LSg9V9I z(_xCv&P5jv?osvV_ACu3kob=2*-})dS$SFxQF#VArA7fhwl0@5DXwAJ#xVRF&U`;1 zzeD#=$CyeTJ&wU6Z=x5GT<0%HnPu|^U%a;Ng0|Nepx0)`p79!?55l$91 zJW{e5*Z4R9pGG_k^}Ld&yk)`^2!!V9I!?KFqjEjSNdv6AeT5XVUd%(5&Ty?ESjIRJ-CoeRS80X1LF74p)uU z)R`!EF2nUaC_D4N@8|;(bfqh^C@8b#cjpIOiwO;T7>fmSg-Su*LJ+v=(ThT013D+gr&@gWJZp@Rh zD}66ttH76LkS9lr+Z!ICJ>DdRFV6Y5!0!L*s{hwnDCcl=HcV`eGCU5;F^-aoDK`ODdhx%pn zD{!WkP`^pNJKOM{)C1@J7J73&|IodVwGY`Qbu;(!H4|8)`t4*|tG(jQBVK_UcbFIy zD4vu08pu*64NCgyhChX_JMMX;bc4^7?VL9 z3l)GCw1&RpAcrad4)VNl3cx`g_%3Iehy0F%MC2n`_d37hAP4@2gFGz(ILNgSFoP-o zpBvN~^$f9&N2M9dGN2<}6K#Zw)X;}es+Y&O$cBwn3Y+w80oAr@OiNFGMI{@hv@prI z+wgD(Lp(iR_b&hA$52(riLcGc9!n4Z{yNk}cQZe2X#+JFVjprXFFa-)n*kSHkHLe~ z_M6n2{noJW@1rB!cCG$XSmMbzQoV$6v*pb3Q{Ddvcm7eI`oDufKA&9Ce+xf0K}kG9 zTcc*0Z`HRSqv*qAF}Mhs=>Wdr9HDU1vu$H~6uT(WX=HYMSv%d3OL84+5^l(!Q~k6y z@y6Z?e+|xa!9%(mNe>{?XUI)BeaQ<@0ip(@H7w}Muc?Bh(Jv^5so*Kd)Q^d@+9wfe zl9xa)4c~>z);QSls%c0b-{t7}*|oI|blj0jp=&1_7~+(O@FhWu*TnwJ%z;y|Vf1x) z3o3&el7N?j3Z5Yw;9SEv-W|5OXn!bPBZirU<~b45NSh44khu0uiH^NH7O5MNk;@8b zOw!K{olTwDi$ZCq31d&k%_S)>6VhIoudVwB)cb#IRJvcjBEpwT0bzt+0CDeg8U8h$ zg%_V_YFlMUy~BooliCxRiC^-Wq<$HX4e&dv7Pk~{k>)0_6W?_;v^bHkB`)mTWZ;IX z3$QV3s0Wm~AMb{o<#7yLA7AtFxbKmzcrO}lEB?#AaZNkr-Ia+I*@M{WuJVvc6FbVr zsk)CVuj}9#;A?8YP1($_qULNJEb^5K?5i6#Rauu}v**XV-eV0JC(?2?FKIj7Zs`~1 zUGkdlP|W@EHv^`1xpi-bf**ui+#Q;G;~VOmJlI~~B5M7;Dc_rPF2m!|0(px};bnc# zee&gwHivpjtl6)vd4`xs~gl$EkLP}4uh1)N-M2*J?s^(<93!DK{;``HT~V zf9!C_yzVu4 z7>R0ZJ!x5Zv8C8!XvTj!Cq-(a_V2K!Ag5@{#jA<=ordp6CE_T&<%4+OBR_jpl@pbrFeq=^-iklKUUnG$cyTWy{hgC_*@1qc=Betu^M_6;1YiHhoGPY9eUs8Ox&sW% zklTS`EZd56IRh3q0Ql6{@pg2M{?Ft@SWLLY8VVL%<$*Mt5KY4!;=gNU z6BR=w>!4U32=p?8>P5dm1^o5sjJ3rRyttbefZ(e0Xv8uaH9|=kDBuRVkHq+!qK50u z4gK67J)%5mC0J(zNp=_^=$XBDJyq&l9;)+cgzQ?HrDY7F zkgThDeb%sm&PmB;(FJG&8!sEa$a;%^23NDPX8CQsEL#~9p&vjQGlMH(&G_}{K%rXt z=pEFAq5Fb{3jZJ_I5K|2#Qk{ly2`-#;ElvSV%Cf@q|EY{@0A!m;%_Uh2xsn)C}0hm zl4wdwZN=sAG{k<9EpLbh-!kJA9Q)93dF0p0b`SC(_EpoQW9XK_@bLIc4bzUX?xEvd zu5JQqKxI<-X1A=l(7-SkGAiGM*z}I9H|$Z_S6FT9QmEO_l3f`-g6d zy{&7Rm|;>lw>(DzEQ!(M&N>+WIwu6EgLC^ ziR(dZpPX$REbPW9uFGtCCn-DsANX$K5@Q_6$G6NC4MCp^UjXP=lnUv$DD*CD5j zJ?HFfeAlHgQaSR9%jK+6hD%PVR*m1WtQ?NY-jUm#mAxX4zJu}evPrkkaqK*i!d0-J zrsG34p#{gpHC9nPlE9Cz0Gi5+!e20Y_^F>el`yJ02z87$yCuWwm_(Hy$8{^ie3J^` z36f_~8Xr~}Wo_sik-<|Dn6|{u-zS8AAMt5ac`G>{iAB#s7=j@Ev?`3oT!-=Uz1ux1{3r>xq$5?TMBi zrM6yzrVs4>U!o3O%~f-M$e>uxES>R<)zphs3~>`Pnudbpvu9WgIXPXdw7g!N{mv=Gzc{azI-Bc&%sUohqjP^jOtQYyPF?j zJ)EPh`djjeBi*6)SFKOEocR53bh4Q-THUqFW^e3kp~E!z6eD_7B}B_R?Z ztMwE9LuBbY4U9-*oovzSXMI8Hbin5@(|i$k3#S$csFTkpj#Dx zbJ?(cE2xyNpK*M6tk~%Bn6Z)e9dG`|8m03Iu?ZoypF_7)@5aAEL1*EwpGpWY0jMC; z*7Hh%h>|6nuiOKZI}}KW7G>_?yJb|LgJc@KtMD(pLQUG#dMIDt(Sv0 z(OaU&W?|bz2)4%d=h1+8*WIa5UUas^_0YrFPgYwlbutKHIl(KjzR3Wh)AH2m4+HJ}&|(uFOsZldJeBAd!K#xw$fJDW5rZ{A zift^iK93fS!T%4=8PIk-n+QA38+yj|qTzN8f1$!KaWrOAFt@1WgX+dqG9%Q|DD}ZS zve|;3xmDk8_)GPsz$B*;ZcW5&%FN8pk)3CfH3^D}?lAxZ1pH?}8hsRUXDTBMhiVb2 z$V@BvY=rzV9woW1p1rJqA@{X$>oO|Nh#fO$+B4J^^MKr_O ztQVcqyS!@CIL_LY0c8z4Yj9B+M6>ci%Uu4>`B9?TpAx;?D!cr0d!4#q%iCG;WgQ=7 zN*%X$oi=e!)_K)EdjMK*o*(}PHo7Q=;@h-{D|5PZqi%qxVdXnWp^$z zRBz?snGeD-4!2=v)-U_{rJEh1vVv5c9LlOJW7r+zhdqldJrK2{6myTU46;VNCvNp) z2$-O12dnn+z!&rMDOO-wU9O$4dtfq+y~q-$3KGSYc75NZc4F-TO{xCccWv#<+nOoQ<`vlMGQ++v9;19BuXLBsh+l}T{i zkhX=aUQMoA*wGbfD5-pCic-5`X^~1XbY|V1=sWKpK1xu%7j~*z&9Qd?CUa4ZpH;=y zaxM&!2+Y0)$&6bk+xL$J9_o*()>EUD--t&03Yvma@5(6Zjb(j}RNY^)Qh(uVkk-;% za#XKfKxE*d&$g!TZsG`kO&w!z+5Ok85dKb~1N>wp^7EH}Bhr!nN~GI%b)a`fVXUbL z6)@V4Vl9egkCVKbC(Nr2!=2hjVhW0WuzxTav0`m2dpD&!K{w)~5tW$BUjtnUrmP^hSm;TF}!$uytlXgicl-^ z@cZM$=mR&_l{^Gi=4;l1%<_e`kA(6C+}?H)iubYT;ldS9FUE?PQqwYd(ATj1#p1ro zIQ1M;r@fa9vKxLGIgso+-i<{=)+}VUh61sx*mPW?`yu}q-;UzuJnAOIVbKL~vB*2p z;Mb=uH*~Gyc`)EyM|s|q;y9JXNLVlojM{tYc~r#Sy8`Y`>Gw~ni_{+LMe0S*V@)DIh1UXuniqiq0##pqQG!36-paou{eMsIte0MwO%sC2exB)29 zUMBAml6_nN%28nw05(rY=L(kJOgbKw{<{3XtrhdHt90wPF(tPCev)T6&^#Lf?aYa} zzo#tCld=&t`>cvMUUl_*W4+V2Sp3_p)g|FHh@KdK!x9$l0f0PgIO z2FPE^lvY~*p3v>Y?fxc3l*J3TVF1R*)qmeVzGwY54tN@E@HF86;Aysa{@c^6^TzE2 z#K~|PQ?Pyn|K-NaHH*L1QuP0N$tmeS0HGXp1w8b-ziCl9C>-F9p#a#1sHS^d7NKreETm?Sa2z9D4!`S}7w2*w~G!`~w`fVa~+&noWEAD^J>~ z@tD^a3F61^1+n`-U&?hjRcq@JxXsJul}E`6F;Jzy=U5%0p;Ttu?Cq4+J8exV68^q;C=0j2;Oa)}n zL$JV!#H-pw&?6(^jCjjjeiVQGEaeM)bEi6X%VROAiAhAtd=a~`TZl13X}&=#bb>@x zMy&jmdpyQs6OSRcm*xNZX=cNj_dc|(YMy(%m)e^2`^rj}_zL0f^;v_v zRUUq#3ytMaDV$Axhq7WWJ+96uwcEPIU_tS%!7q2B@*Zc)s;A}zf9x+;9fCP$|KMC$ z6@3iTZ*H3Ip3{2C{rv@trasU5)NIny3Xqa$g)(P{ZfYp-sTqJ{Nsj%;ni5%9r3998C0r;l(ph575xEvubaspQ22YqoM ztkj5wW#JnsIcPbnc)tFuMQ@}W^dNFa_#_~}bk+t2@*M_f2!oW(k+ojOv(*nS4y-X^ zLnntmi2Xn7nSEBxsfu>bx&+yEKfxj_Z-(|6>cwYYsvN5vTKdrLxzSu(8?BPOM!!$| zGsUxECj&RSI(0?kP0pO&z0^Xl;%#N++KPioU2Vf;o&JhFW>E(SmN*(L?Jj%)P5>@D zaAmAB8q%rn)ek`&pz)*WB;^Q+7VhQD4~^ogPJ@CMML5 zg0N-#197#LF&n86I-?BMZU~TOLG_?-=C?eH%vh6NqLeLY92|8;5s~r;UG2@C`+m@Uz0!dJ=d#7K`ay@R4(k8F zm#=nidF<9bQDU1_RnzHV;zIDj2lu(8QeA7#H7Ek3!n^v)_jE7!j**?S;joNKKUgL~ zr>{c8JAJJBSh+(6!OFwZ+cGz2ArKtxYYr$#d$NxzCz&pmfmqQXYrYO8vi#6HBmIwLoC-#c4* z)@1BM-|bp>jiX$4rPUQ}IaQpYr8_6XL-!1>cEgQ<5A&)~Kw?kD_7LV%e|gyLZnvEM zBbBZJ{Q)occXuDU8uKPY|K7si(L9nsWCV!--_p4SvYXxTz6bpS)Y22q9%6V&WHMRR zKq$zVtx+&*82!vwPTav7YY&Qw6qoa!_OADC88=H%7>xwRB8_c%_D{!}ppV!NhY0F0 z($1iI|4WE&)c@&4gaLI|CcW3T9Bb9!9<=jMD<^rH3XQV;YOmft-O_pMKjzUa2r zapB^ga*a^S7{9#6ocMxLdywB5&5Lv%P)(7){C6akRT9UGrtZg1O}@aj(&sG8VNLkU z$+1r(cVZFz&Lr!>-Z~=tt>sHe-?)_@3)upI=IgsGuHwiKUoS8W`EqZ47F;`u`iiP- z&Qy7SUF`x46;*i8T52DXVcKhHMcrZCWG{lVmQ#Sh5C|2VcwVgfoB4w7v{Cj`^}#m3QM;b+DEGB z_SNdR)5bFAR`nfRa)Ndi5&Pg%WDdR0cq`TuDy<3aRO*0MSR}Y;0~L~HS-ZIryJxnI zx9=9}iUN4SeaMD4{@!5-EhNl~9oK`Gg>DOcls=zo8~vhB{E@Mkxv!ix{YUB%{%1RY zO}iMG@!CHDEWmvFwsN%@^Cg<8%S5lfyx#S!vbb`l+fb zH^Iiig*_|Q@T=AKYv2n=9K&Nm?r*Q5IH&v}RNJ^!joJRkJ{M(ND{l<&T8pL4^55Ob zJ?z;N^!UXk=<1ktu5+0AeoB{&; z4yx341mg$%;@WuL+CY!*P&X+JDUHw+!Cr}sL&*SsY5HN!dNm!ZTOU49vZy*&vql?a z%a#X(IX8s|Hkbl5Lc=qg*Zuif@t3?$PyhI)BkN(e`C-0$w3@Y}ms`(S6QUw2f9DC- zj>GBS1DX6 zqgT<&Lw{Rr$;Xw}_L8UUY8F(T??*qF_gbs+vu#WMc)v6tjB%6UOfr0(e8NOLLd^yh`NK4n&Vl_}#r^~?}>v}5V z&g9)AGWicHM?K0_#$8U2WStJw^_z*x%z1NmW;psz#%R7@@jqkYe?Idcbsjs-#kBwt z`&QFkOtM0R-PnIjcM!Tez;lUL87XNiuH*2SVXK;eCD z{U2sDA%~eCNWFAF($Ax_6JPLKF%=10ugtuS5Osuw_hgUXwSGQ|y(s=&Vg?YmEnJg$ z-m)+6O{d8taXt0d<*8{_Ij|jog=<*xQm-3Q!_W$nKIVp%w+TMH`kUTp3CC=~nxLO18mu=D)teY7Xo66Dd~e&qMwW5XHz_5AX+RiWrjA0IHl^=>&2%aW z84kGxS=fwHwnn>;QBM5Xwi3xe3VMenRwDC8NSOzFMs3-4pHokODY=!^WW29shz5T@*Kca>fDEW3CwY4Wou1m5heURpC;X(j@L*A;zECX^z>vy zsZ=yTL?X}ZIz$bpPHEH78*tk&!QJ=4s#?gFY1Rwgwf7>^0e${v62#cdV;j_zh|S&le2`lS}-%%gY1WWr9h zyberx$q^LtVyxu)Y~;1bWT@6vlJ@ zfK0VzgvfFOJ$=$Wf6%x|xT~op-~35q%RMWdb!1+>L`IQs%t6y&sbG<);JZwbk#KqJ zL8xUGxe4n8C9;vwN#tz7RSVHx4Qc*zDiBe^p!_6szO+c;z{EEx2cc(D)$7xV`D+u5 zPcUSEYfDNHW)hSIQaZl-0n95QTZk>9)5g#~kkrl+UP-?joXm(Fz(ZQKLxd72g5$Tm zqYE6PwFSK<*q&YKrIf@I&))k@YPy<}CsG6{$WqZPb!t~1Q>s(VTNKsABljSu7(z$jBSoS2 z>O>e&HZ!|5ewx5pVv@Q$RiJRg=QsM6mrQ0tcO0wYJ!cmPeLT)f03(N04^#-(juW{c z+D(Ol7;0ba znp-MEqZb6x{eo5FiUr@~Os9+7;KbOAfyJl+^pqQ$*oRjHmL+^NriDJaT5Qata<$kI z4wLL=GV4A#us)(TU_D!b2x1B3W|0b1RGhZG|E1V(Q|w}BX@hQHrA@4Q?%MNkvtQvM z65yAy9yc7I!B@_#!J0<0YGFT*FInxGSZ3=xCAdebdWYOqC1qbIm%MEDiz@L!FDe~u zi(voZ=b6E9az3=Xp|#;QXI|!io$O5)6JA>^r3&>P|JKiJ#6NrI=_>P>l!8$H>4s&N z*NDQp_jwb*ec2f6SRH<~cY;vz0k?CMQsm`5ZslJS+N>H-5Ne!W7i(zaK3+$O@$)}8 z^Ky9Zwtbm*#PHrbe{R*Z=*RsCmcn(g_gElwF*F(#h5&2)DDoUJtrN+|RYpF*I6j;C zQQU}A!94BL(3!aS+|OT=)~Uh+_7bw0ZzJ7*OPZUWw%I#G%yrT`;9ZCw+rK=`AvNOq zG*-erb69q(QDChDTLewiTk)TjFHN3i)l~N%I&H>(az7C*jba@3to%5dLEhP4nrrW? zKh@`w>p1W>t5ny=l#7JyYLM?ar$OHbY!9O5y7)-!CW(IDvrslvK;MqR!IXw(d!eMt zmJAlJcA_+@lmX?Vw-NB%TM*7EVC3V(k%`o$AyD~~-F-{t=RtU=LoT=%^nEuXqk!yO z9+qJ;b2=x~z19ewQe>H{^|9O4X)2XH zcrZ2J++v~Z<+X#ppfY1fw0Xg#wjta?At136X#RTVahYmJ22TN z$}^7Z?!9cgRyS$uYNI=wH25pV8ITNcR}-%c@>3$Yjt z^(D*@tBwcPgsh1sR$ZsV))K|F-E_^J7Y!YwjEr4Cc#~3t^=#3xh1MKf)WIFz9#A#;63S0^!@0fa;PtW3tc0G6# z9-zT{+u6XTrsWe;n_2;&iyMdHQIpMf#%@LTY^NDfsxSp!BTZby`01g9#LDTZX*;c+ z;l6Q&_n>>xTIE@&+pW*eb+}Eiyp%EHR(aTVd&cT_%WCJ76cu}h$vga9weT+49w75p zQbnS0666Nu0aL*-e7E(PZ&FCr_Ywt();Fm+%5G_q<^=pRzYfnrvC$GgPurGo7JN&n z?_@^1UF`q}?rlblzMH<@@#9xCl+F7hk{(RI4YOd}rPQFqqi6uze3uka97?{myM;J- zYsxFyGq&25He8+7<3W&7(9|8PJgn2bJ>wlPP4_j)I$Kz-d*pc7!1o27^I@2*M+tjw zYm#-kTo>GRmgnP~;&em5{SpmK0V@(17DITpT{k{0-wu>oQgMcj5q903)=)ej@xHH9 zmVdA~Po7~7{v&c@QFr)zFCIMmk?~H`u>F(m-j-Vy7d_X5{^?&9%ZF-*S{aJY1X<7g zVLwQ!lvQk9u#dA!4Fh#Lw3MsPD}=lB440?T#tB$!Okd2xF^5xyds|C&`YnJNJLS6- zw=Xyt|1v0BvirM+T{nCK(Acr-C8o6@!I6xUZ}hk8MOXt6{IU040kXV_;??a8!__n^ zH#T~Q@9Pf}=NH&BCR#mw@_oyF+!wzCc7`L&q5SGf;IU+#R)^@@S%Hech}_pCV__!c z%lufas{$~g_obXG-Pvd4np0jD2>j`Ft#|k~4q?rOKTG;M&7epYz@Kz(6PHPLK#eTv zOsr1@Mz))(2tIHuB!8KPm&d&KWJYOND9_%z9MX0`k-t{rr2Pv$gz`lHoYueDQL>M79QW_KIHr#~Xj%hr} zt0~^2JSkCWn84bx799tmTE@i0VnAa{zV}?)^>gzC)sdVtst0o3>)pz5$(_?Xa)!M&nf0)K=6DYa zo4_XvE<#=`<6Quq!A_-r*UH=~+RPj3@r}EQ*o&E@Q`KXZr6ETa5HrWb$P%=>B^G?` zG;C00FbgH;|1+B^+tEP)FSx08x|tyRpx;2Wc0MLjFf6_y3gj{ABo!-7VVGpFSPy8+ zJ?7Xp0WEni8lIAR9jG<{o7Ib={{TJDKxcVs+THaV4= zLH8`yARJnBZQfH@HClNr2cef+`7$}gxln@0+@Y{FaUIS=GBAnWFe=DJF=Sam zLrcpMTu;y;@8FPi>hsr4J3%1Eb`McB^1pJ9kn_v}>lU}@4zp8v~ zWjTeaAW~;B(@0ye31nbKgJ+8m0^blbvzx%YHBd|0ILIvzy4-vT zylMv4VIG>U*(T2JK`OeoHKBl`p?BHW-Ca!4h}-o^=!=~Y-0B!Sw)!j|m4-dSQupfR zhZgJev6kaQH$^57u|JP3fz2@izqKngnypXV_Mydc?akt;dBLRMKmeubvSCf!qo#M} zw`60Gf7x|Um8+n0Kl`Cbfk*%8eMPb~9(|J{5kP(-gWvH@>J#Ih+R#l3Ds=`qvX?J? z-6}MkMsF%c)@A0$W0LK=pEN#k&T&dZ74#2*u2Qtm16w< z^@8g0@!YYf)6)a0UoJzzj>WpW^PSFzoVUyHE(fdc&kXM}*V}e`BdfSEL3-}{`{=-qKHMYw90VTPm4b?0;!H?ZK!B$E2`p8HBa$39m z(-`ZBh~#rtKUe&sJ#hs=tox$}%Iw*AY8V*5x$mXBF;&k7zp3gvoqXQYq~h&=_!Iw& z2mD`HDF3NZ8bn7*e4JQf884C#%!pWVqT5&wb7NDIbRVMEhNm#a5iR=n@s zS8x>AquWQSleyADJHMXM_9)57+P;a_w$_e&%i?t053S7Tc(dE~18NroDMvlJujzBE z{!3g7IgvGJN=EWBo%wM$fy7X?Aev83jqE&t#Y3sTV=_;Jvwa^%-}mV-4oKxe>HKI1 z%7m5i8KKtVZ=tH?MMT1E3=CxM2~&X4n& zqA*vRqQ0hR8r?|QSFx=u%&j(cz|`K^<*st>Xs&8#RpHF&+qt}WfHer_mIC2P>F?;e zo_44n5H@V=L|UX~qIf&}JIHOvXEybl)X!V5Qm2aL@x$RgXee`zDpoG`5|98;?H2$` z(^3Hu>oKq~{HF(l|NF60|No)>=M(?`-dst$2rV$OeWYy&<>w_>o78SW=l%(t4mJV0 zQ{#;>4H$2CVb$tOlwovYI#TJ2c$X7jsg)J6W6@Wrl*b9z_o>qwxquzWJnVZ&$vU}% z-RYIae{ymlEqaMAwcxmp*vGDRiLLA_3bw7inyV}Hy;SL|uyEM{F(Oy%X_CuP&E4@1 z??t<35a36f+U8Gm=MbO$%4-VOGtG5CFmh#L783OCkZogiKlcT^yj<$wN+{QhnVvhY z$S(c^(DweHagkCSTtvw6-lv%pH$h3>qnaHPR-3CARF`iq2ZTRl+Kp4t+ZGW!sO=et zw7oxSpkSCJ?`Hi2e`K`OEL7{!_gstig>}PIHFl22_(%TC>u4^h#9TqA13PP*_{yfM zF3?kU0$c@q-0_Hgr0>o%W9?2AKK|`zkw!Bot}qPSFMOB^Fa<1h8@6o{U%MN>qf;JQ z&qgOn_Aj(;r0{LJNIS5`WgwdFDn2|W(hp=2khHjf-8_6Ix87`;69~H@(h>YI(RtcS zRk4Fn&nW$zCjGuev?Emj)XoudfEb&{*Jq)Y-t#nB#2!B?V_G8P!)sdvHBA_9y9mic z6E1?T2@;s_2^}yA-RNy~Rvs)W%n$7}>u-FNXK~{~40m_kHd0{ltk?)Jf)B+APq4(n zE2}_r{k!lw^8L#1tK3O-{`}Dm*kFz^BZ-4gvGV#(>m~7=(bJ~vgSQ?mthQY&;0$eq zIFikafh?r5591_n8qk-U@!P2v#nPvnM=l1s?A=@)(&oQL|4I4a!A}8|@ufw|U|>BslaO{z z;iWL#zA-DcmNmn-WB4@7VV|- zK1D(ohPX(|BK0+%2mGXXa2~H7vK1{a!J7bV3k0Qk5-knXlmv7$HY?^S zTKn7wAhyP}QrM^bXn`@9{KEiwW-9y{YX{k}649JIpnk>ngDU{Jh{n(hYe=G%MRcH` zT-8dld!#HS0^fbx4v*bC606iUI^o#dnliJiH=Bhh9im8Se+AP_zb5Xzbz* z*5BOIG%SK6vyH4w)GO-QvVL5p$n=A_y^r`$r^5G#4Pa7FCH1!P?^yh3XZXTPiLJto z*n4e#B`=Q_xgN7^@l-oNg)UhgHmWRzP)9p69W9gurlGivBXh4rx$SY9%**lCOGW3)qf)dZLrghq_3U&G3L~dC?I`dX$PF#l!235rel5L@7sd= zsGs&hEt1|M(9d=9@|bH{_Xm#7P|RUIMlH4I57ngR31T2k%}wpc*_-xk0Xd@8fft%7 z>Q1f-&yRR`c-Zwq{uR@xFE8nDYQt(|TkW0Sbada!E zu_x%ROi*%u|+j7l9 zxdmq}y3F4|1>(5K*MxQ_7;QNT&8z+5OLqjCoN}2At*P z898&us{1aVSY&nr zWMs(vaOq0M<#)L`4F94jkxAt(_qExH zcHjkfc@dLff5?#h#P>?U$)N)ls5@ipVWRb*YIdE|Gish0VY@)K>j%Og&8^CN_GQp0%Ql> znpHO9lCqhr3c)h>rpOLP@;#?|-Dt+4gK6l~U9-i~`HV`m(4p*bq;*i>qF zJ~&J68ROvKPLFf&yM|Z*u_-Yd$&YV$3O<|fG*>_MMVN@2l_cZGH{-*@>!)^O!$4ig9G%Miq3N)EZ=00( z_jcP(#ZKN{+>a@nzJ!~wk09j%eT_2v#vuBF#X84b+WzmpK=?)1+)}-f7sxSUt-TN# ztcqnW?n6%X<>-PxZc>qrwJX_g&IO^Rdk?jnu4N3EsC?12)i3Hh9CVT?Yip8gw|9)3 z$Kt~nwq>p446?36(Mij0A@T8G4}Iy8bxzyvY?++=Tvviw#mj^;o8I>l zlSvK6U8o&X^YCWOUg4cmP^B622|3z`zv;>bIk^kbTfS9#eR$)HYoL(E5*uC50hd3 zQ%AYOty;eKO1>*<3Un-wn&hYB|iATL*##Cv(#i8yJAz#(kRPZLRi5xlB%Bb(mjhj!=lcgmDhu!ZZehVK;N`JbdtvDU)ybt*;aPl_cI0=Y-1HvU z>k1)zsN33xjpl0eqI-OcsJ$%V2Kx$;C8@hPI^?P?^-QojCv34CN-IKp5-u8YW0;To zSezT2leS(rDz#RuruE6k-B&xZb{D$=l?D~Jimzd|F6DOeZtC3q?UYM=8f#`x*?agM z)*E)Sw$AL*ki4F2^*z?x3+Pr?6Uxbo3eEhca@V3mtU1>mf~f9+MU$LhZn?|>vgSv_ z1VNgZj8KQD?6{>2Qq%i%iKb()FaXyNm#U>+;08ZKuZ!q3!m5L&1{e5Z`9s&)=q;GW zpVc+fV( z=yAN-Adg56P!KpmljuBY;geE;25zHw9+wd{7E>_r?&+%)o2Uniul%i>E8#77e_cGg zf3!X==Mg5~CnwY`cT>Mhu4Q~*LS8>bHCNMhdly^x&)4z4a?s+ciakCZ4zY`8N|Ljs zPa2f^9ULfeXeZz79Vz>A^P%u{mWCH0t3=nG5QfPzXw=J@+nIPiD}-(=7}sT&q+>oh z((xMjFzO9UFOdR~eVojNaYz}K0B^J^xWuby;El4#nE*O0)684#VX7=5A6FD`xM((< z-r>cfcM*(Fj~iaVx}A+J9~H?(v36l5K6|%>TOWJrujxEuHN-)34TZl-h8WB~)U|*e z;ztx>w(4Lq2(L$h^~^V^5Lg;s1|R%%Ss9DW6VSws=J0FC9f(~E;6V79H`(2}lP`UZ z0$T^!3H@k`v84=5{)F+4g$NKjszZ{2_K{YeT5C#Z9;+OcUmQv7X6ac*io1(7RWf=K z&&K7<8P|-G)k-&Wb9DO(wcHAn?;Z9e1RaX`q<84K-R*Kqxq3&2-RhxK2cKNwF?=A&F9A2KFxlMdqxv!p2^MDe0`Zl+$fIBZkwkMqBxBP?M?1^8Awv8H^ zjeN5dAHOg40tqh<^c*X21D)_TXr5(!4ZhRxn^ZVFwOB`dPvpYaWPzdlSaIo+BN(^t zV#Ih1Gqv~#4^hN1-a=0MbsW2~(^}QP5mQZTDLwsoG}6p(b)se|;-c|_bDcfyaIWt0 z^TdM#;co5cIIa`}YO-lNLfuVp>vF8g*W?SX5tXy}D=&6GuCGJy^=Yac z&`y(c-5KWb=oZG$>UZ={^pkVDp@Aun!fEim8>vUVzCnT$-Gx?&=Th! zSQevQzXDMn-aQ7U#z6n_CvhZzz}`rJ@T|xs(kuzmjm=A0I?o&G9!dgh3LHp^Z}DN_ zGg|zBDl@LWAPU;dK`DU>XR#qapu931VazudlW)RjDZ8>X5({&uBj z)V`hvTIt|Sd%xs2i)RFb9Iv(|17{(+#HXiF!(fW@(a=G9A6|ZCh*7{vq_o3jB?_Xw zuOiK!)Ei^tS<73(5h_shEL7^3FrW_WR+^8G-_Y)xyP};fcye09+>LNydh6{=7j%co zJ4cJIPF28QY2BmUwES$PB0Se1Pg>2`(A>3nk9(kPg~oxL@Jk~xylRuKiE204wcdAc zJX~u-_3o5!G4jvub?K$6xFSX<4m)V)M=E{qB%>l^7rp2$0sqQ?U;*`J#hIlp);ZG^)&wcjK%!&k~v@5kB{y`WqA2cUPJ5vGD1*-@Mqj` z@kRh`6k20qvAcO>PF#=iM(oiE4xiXNBn@4B?>$!c1@x{1^b#F~SLyEKl-grJ>aA@& zccQh{{K>e{yR!M&18euJw(4PWf13*!^z7~nE8f%Wmdtw7TyABPqp5SYQBT&e*R{KU zf3BK_Uuvb>;*MIUU!Q6@H}MahbP93YoH7)Z(OB*_<>2l8&L>srJb1k-ZLt)+Sv`E{ z8UI+7cDu>4hvI;2jnY1xvuGRC#a9MPMu|F`Pi=;D1^~@hc z#wj$xmDVPnGK~=4r?DA2OO?UO{T`k_nIwW=`V2*$ZsfWP* zHJVN0S_mDnk>Y#!NMt@4Y;8IQ(iH-YCfDSIzC|q6t5B@^jH_ni!3}E?)Z1~7i*9NAJ5!+)hmwP znrEkMTXCQ;!e%GuI(v)lE|pXVMJ-h*qaCRi?V7XStQ2KNY!pB*DvG74hQ03H?+z;k zIqMbjzc#zMrf9h2+Pl!kUqaE+X=Lshx7T9No2J<(9NcQNY7LwN=Eo~hn~6PVab@C0 z`~^5eS%7Iw)MUZvct+f5{FKcsbqOEG*FK&43}Dlo#j>+kDC}=inVtG~oFBD_D}mRv zt3Fx-8uj9*q*gGz=dYr{2RA~VOjP?A_D)q|)mJ|n)fK}O5X5Dj8j|VNHBy!+k?*@I z@B(1Q?#OEh)jP8n^Mt#wXT|rihhzCR-7^Zj;vzsfDd4RPPUM2RhK{(r0(#DViwIdU z{DIRG@Z)FUcRA*po2WD^uS3By9=3*# zK6{mYK(5=fglpn`+S6FO=lqJvldJb?3{k{xb~#G7QfD$*^^jHL24C~TL7Q-k>+YR3 zkty0vmG)Re8zk42!8mn{DDZ;g!yTjO*l4x1wmH&>c-q+4A$wayPZ{^ z(cxGrH$>A^T{b(u_0z+1=NgjGS931JttKPnyR7vi3pBExRXqhXQhlGnC!F3wvjfdd zl{C83oceiBy94B?%Hkg)fowtXvPpSq)RS*-80%a|hw-i0i0<5_l3$8Uouh3HXvG@A z%UGvxHSu&`#0Ob6YSxP9j16In#?ua;8mA5ezL6BqT-bY^kSNrc0Wz8r8q0&t#)zB1 z4_c0Ji5g3IUf4pE$EG)RX zEU%IijMRsX#dWaLJ@tmw`a}at* zUd>U-QOHug!}#7_b;TOZkc&@6D4n{(D)C)dXw9IebCP%46uuEuKCK%_dDbN*K1oB!%=S5L`nz=GGq5WDvWO4!l`?&L3BjeTpnfu7B;^lkXPSWTt} zG2dT!bb$UWG&`u3whoHO9|5HB>EXiRdQYxls{-OC4o3d2U=ZE>o8V;&ZCCdkvU|67 zY0U>@cZ5Mh$<kh5TxCDjj18j~)9EJZD!spzXO>fX+AISkoa*xTw;1jEqVRwq zA!koP?65nWZqvPzZnU14M_=72rJS;?glH#z2K0urLI|fEmF?q&VXXT{Vo*ta~A!=a@ z%iWGagEw?nj@-cXR;ty?I=#?NCIj0sw^4U9spA2MUV2OS>`6acl3A&JK`g;FETYb9 z+3U4!Xkq(uy0&YWdi$7!MaZ3o&#zT|EfD4>(`Je$6TQK}8GkU5JbEy(+UK9b_%yu% zfD&8AC23Crq(K)R=E8{ZGgWx}F_o9hQF$6_(nI(4P5$%aYwAtVN8>QShDojSkuQ%5s->+ZQG)b=AHYE?>zBDrc@at=w-k&UT8Qii((JsPLS7$fN{qw%y!iFK#?_yMKw8AUTe z0qc9TOSYRIF%v)7R^{AtHSwHue*5%WX|`ZGRZSt337^ac$j8H{ZuDf4!6p=UPj&)-JEI(?Q%$>SbZ?>UyQMdkuPWd<(zHT`720Gv}M-1Jn z>T7(pzpCD&b^Xnzn2nalcYxJGJ>bJ_gA#rL5myp^h_p2{(%-3}uM*TM^nR}R&~Ec$ zSkt#(!_y?zF={{!?SjOOb9Frdo)?r5#s04O-*&6l(rO=sCa(GRlY8}wR7q_`4`nWE z3n(;oLgNX*vmTT8C&?1LL2hZDO>c(B$O4l6`2ZO6^2<`dQ}$(|l%dQf4HGrqc3+`d zPrdBE{8A6M=4!lJO+T|@Q!$v}@qJ?@_J0$h`{T=hwKQ~(rvyyCp5WWTyb%ZoMTR98 z>_r;!@m3v)YrkWo>{1#_W+UMzl8ynZv@N+$8n{xrSkp%|{pQzIk51HWDSG1>Srs}1 zTk~1Co2!|G-vcz->fnIgW!^Z78o3tyV)px`L9#!EfkQ*5 zcqaS+I_FR^mo)Bu6RG1z66c?SCkPe%FjB%zF4$E-RTxXymDb~fvl;jAl7UWqMQ_I5 zz5J})i`F|lI^o*BWBXgtmpkSCEO|g^EASp85+MjF4%9k>K8@BUH-vf$O>$Zus@=t|sGtzADLTwYX zd8RViZKb>lja@|SaKXy9(G&-8ifq!RQ*87mY<#=BUbBh}*BdyfHG3!yhJGoZ@?Cu! zTjC?-I(^NmD*U9rt?7pd*Kc%5zO*3Imb=d5Fb2;LZH)N7r@Sz(*S3DoX^yzBK0qH3y&@pUX{E;d~Kt>@~>zK@{J;k z!lb=qY_n?aeD~V_&9rACC3S9}Cf)mPK4HXDou|;vGkE&VwD37axyWugg^4uxFnO0) z+9{Vgh=I{IkNLiMOT58((3F1NERG*h0PNtmriO8D1?rF7U|mZBmJK_5kg79>4nhu3 z`b{GIsX){?d| zB+M*AEl#M0I~ug5<|ba%zbk}kYD`aFJj8K|vL=?Ci^zz{!0j!FjAy~0xqdf{q1bJF zR^DXo>^$B;EVutqIE$w)p^JGsZZ}Y#OIu*&6BEk>yQZsG1s=p*;jcYN{Ff~jl|^Q= zeQCQL)Z$W4RZe(>674GMH&hkuepmEUnWz*)Vx^{Bgir_DyeB2acgnCKclk0PpDK)9ydoZH}!>SyE{$` z1N3H}dz^ck!^}&cEc0P>(sfT;Of~eqI>c~&Y=67-ib88yBI2dBB;+Yw z&@m%d;!Ynff0k<8kD8e93q#$tE)i<{MjB4cA|on^Rxdf}P5vOP(8HN1^Y^YYumX?0P(GEsO~^9b*>owK&qz zY+jb;FDtu!=ly_LLno^|u&P?~7^nUme2BlDqjQ&ovg&MK59g8-`KiEMQ zl&hqBaTdCo)bm`wfMR;SmhMoAe=u*H=G|#`XxgH_2*2R`w7bxz!^t*B!9X=O+L9m4 zXsz<;PNXW>l-la|IhcoB=0M?cr`|;+p@Q1$JbeUy_7is}deJ2osp?KO-29(n8HY`W z=!!_upZc(s)pvnfI7l^+kn(&2XDl|10G$Rz(H|m7q&1cF_!s-4sgd_1QM@42p8Bxy z4ql|56K_NR8Ofp9(jPrRiGYeT`H6YOdG%)b^$i7s?`h*qejoUgYo+ zuhccn$7Nde&{RGTQ|~T~s5UxgIM%Q|^GWhE3PFt06KxLM;BNh=i`vo<>1-8ep_7_< zbo>62Ieg!@$eaO#n%FBE+v{Ur5h_r#(Hv@HMl6FE1Aji&7!(VK83ZZ%(6=vP)XjqQGKI5!`LJuY_Wov)=0>KSJd;z#*m%NgSIJ^U`@~C$M0#hw^w5s>Z&jMkV<@5f8Jn0KXMz^%r>(Hd4xk~K}dBx#i=AO z05KD5==!L9kO<#&FlVYMF^r)6IbfW+F&pk&`^`W?AF@2dlW9hsuUZJB?Sed+-YxTh zfWf4<5My0Yptc{2*Q#(o(t=zI#d2tWV~ptJq;DH3CB}hx2_rG`lQ1$pI(l5WdAr)9 zwaAh7JS(3PQ>keg_8bQxJl8c!<|`Z~Fxx?zJe+X>yuvuYF8UI-rXFr(yOsoRYcVY$ z#}{^V*qHN9FT8b7?LghV+DEa?yCN8RYwNL?*Ja2d%zokR1Axxh+rSup-Legk>7Ap8P!vQ_Lyu}yOzDu z6Sz22x{jH@lB`d-J4jp88-&~g-C%%JWhE@m2_u2fF$vCv%$n0zO^aCwAdaY&&PXB@ zU_;LK>q~5$Ty9kama}F0mPUo~=GwWJ20u7XMWg+7u3ya}>v(s{+c&kXv$cubYbPo< zKDm;%LzmIMc=5@F*)Ky>Z^gr=IUUfi&3&z=BuF)})J;~7uiM;DaYy*u+D5FXh(W0E zC%BWG&M`2!Y#qS{v1W(++ggaQkGTC2mFT-GDtq4nuMU=xaMu@G?D-GSK)}hS)ySC4 z>%%GGR$+zj8QwZ(M>%t&vi;yco!Gua!l@8Fd zoAt_hE?7bnaudglkpQYgShJ?#t(jsUz6`6ynixI?4?@?;9__hq)tKP{x!k+bT0TX( zZg2A~sx%xaM)swy`s#0O18q}Hw2kZ-Cv2~`AHIGy)mRcgr*JjJccIlvZhxz8;ii`T zwy7?rM^!a?5{oS~XWJ_&y{V2>l>MhV9YAACY1P6Fc z{;~crtME3G6hOQk`HWPAqS*mKZo(dgfpXTog3qWZuz=fFj5Wx{%sI)X>sbX0ibJN3 zL@a#G1rNR&W1@sN=pn^qW6rzZ|<~d8Laxf0|tCl}M^_6-_Rvs ze*r`o{h~aoC$9|S{WYVXV%y;2SZd^0YGNPUlaiT|pH!&hhiy~$^wbrA0pet3X5JM}c zXi#^qZ@F*!u$C)6iEA;_s9olprb(X8$bQ0*SPU*LBOk5BX!y!hhiMo;^m}_X>{*zm zJz102S9I|xy@q1#)=P1laV7=qA9g^NW3oFw*iSNsY%N~cKS|R_HI|9&=e6r!u+CO6 zKmkXTP@mpHYp0~cHidF6rdjsaOHs_OSAu=;EgyU_U~O6825Gjbs6(0K@AZF(M0F3H zWFqb&4`;T7s~AAh%!j4*`->k#Z0B0;RPvsF1AiL{;bWm*f5unV<-VXTAL+M*8-yL0 zO~ReH<4`AOa5)09m+Ga7kms%a&W`C}+;Y^w~rlY4dG&Mt-Wh9bZW{GMV{ zdYLQjp}0*zQvph{p;Pn6`^c`Fs$}aBu z``IpKZ-`|cdV&6w#GKsmqoPb|Qx_n)F~=D2HNIn8@o?y*RqQDa=~lfcr{`Of%2p1I z81Qm-6&;JyW6p1N8}iAxawF5Lp;EW`Wpckv*7n;rw?D-f#*%%$@Uk>0O4{(%6@cRC zX7oZnMg1~|+lDP4p;uCs{c;4p7WroCU_c)2_<+vD`_Wp7mFX>Z?|UZc6+{F;7;ct` z<@07p&>3v{50O-TLeoib-y>J2giGCR8L~LTx7gWJ3M4UHIc!O0WhWcCjR^ z?KW1FFGh+PtYgr29*l`!X$Iox3YVc?seJ=pVxJBc>n1JVyz-YH1uQJy6S`=1;G5e3 z8P*OkSoR!(f13w}! z6m=%`RYYjZMyt3FPV=W*Y-F-it=s$zn)wz2Bx3xdY z<$VV7rQ$l~*tX4~2$xW<2J-~ZE{@}D?M+fTW0 zSnT5d9EnTyuER#R>=_ROV62e%G5dR_RYDR~TV#{T_2_%3vMGl)CF5B%6{AsG9+&&_FN2t8R z&3mmV>{U=p)6xD~sy0k7S9|q~r*`Ql$=92|$#CoNUSiYNo>8m+t-=Wo;50YLMp|?% z6F4<-06(*te}bD&zm4*Py`N{n;zBXeBnkXZ9ESS>d-4^b3KSq@Ack`fSRQL}z#I#c zDkCu))a)8+V=a4wbasrky6o5?)|cye7#C=M_i6s@uaDB4T*--Eh6?r}TH3A^6L-AS zHA30y9ckJNE$IqZOVP3?wrUipN&u-_UsK5`N=)&lQ|(t1Jv)JVPYPK%%zG+!4$Vzq z5!(pz*o0R4#!z4)^feY#&q+uwfgB_o0{LW6*vu<>L=Ph*x-3LBSpflMB)$ zZL>fYKjicv|5p@qH-N@VA6%Yd8BEP>W)fvE9$k3-v6~({8z67y2+XD8g(D~VKJ8#t zdCc3M;eMG3=~N}h4Hv!DqDr;eGn0HzspqqDrcN)vE<#u*8Qg8kAp7f#1K;fo? zC@+AVe6{LWR|Lt9qi)ulvo`BfOdMENYuhdRdoR_*M0&t0Efr|f{O5xF-sRg|?eu1X z5_E()1@xbTn54O2uykl+W1?`uU?CDc#<4wW$?CfGfJvMk;T`60hr%A?ECreL^Fhf# z>~0ri@r`9#$Hc_FsHz_|EbTX5vvC+qwpH;M#{v8h5qd8q+y3&7t&;hn8mgZ z_U@yaGx8os#G|7XEy8?Q6H%%1ELGELOf*FfR5;CoUa$ zD{r&W-eRHOPWLqrP;3w0@wb~S+|)Bw_{eFs&(DjSdb&LywHHE@h20+O1rL!%Eo@Zm zSdB_1m%98L)W+KhePGWw&!HJ(9DB5sA7+b*9;@p;nTao@0--V|Mi5X&K}GZ3*(2A+ zmTz&?;_G*@=o@heu4z<>k8jzRU!l$`e9hd?t2cu9(%q;4if|*}9k@PtqXiB4-Pl+l zcZpboRb`q*g~6l%?&277{jlF54=n*edZO;+7(orFCX^`u+yj<(3+U{iWlSH?TFJ$s zZm_zx^N-Og-kBI`(;Czgbt*B^)OVBi&veo}EhQ2PEjQ)3L?)lp9s$^Ng*JT@ z4QW5KEl(}3zEO46_l&2SN|Be&h3=h%uR|*1hY!)WnRs%g7`^x*mBUaSlX{!LIU5uO zlxG2Xt}7lR0Wbu4m@PDwk5I2WsCla6PD3Z*%_tCeR86&#RA%rpoM^!MA7jTZdyu*5 z&4U?{Kx9DEtKfLJSHu&@?szxd8Jr_U0oFFM50U6ZkP_CfDY zWLX6V^|yglSGr_74!1Vs$CAMe zea?Yb3mzc%;%wswFhe9Je9nzYjKfrBt%6chj+|7t|laHb(^kn(=GWrVR z$;Q~Cs{H*;NlMzM+az6qYeu$0=!9clit4@~(`?tp3MHhkl?l!Cw{uoz8N4m{p0Vw;^?VH| zmv;fXy=&YH!1qw{d?zS~XN(`l>q4n)`da=OXoS;+;pjXr&hvpkkBv2espuO|67@$n z6B*h!_6{V4d3v%uE+aECj@gaXFrmH0;l*{pX$J-z}5 zJaaXysiq(pz7H?P*+}Kw3v&r9s~1(fX=0_)Kz83p%$pvo)W2T#U~8sC+b!lhvcmLv zuXK$t_#me?t2+M41UeJOdpOwCog`;^IMKk>2iT1s3&a2DOdHC zB2@rlX>U%|21lN~U%0V%RK4%+ezZXX9E8?AAofo9mP; zb?)0Ckb>x3?{UIx2af;%7*lfz>@E?$E)>Fs4XH9$fGavM8);SSls(R|N{_E3-Gxh6 zQGuW&KAP?BZ&uUGKf;x29=@E*SqptuwRN{_y@nSh^3*J8#HN}tPhEiS#+ix{)#x5F zxz>4E=hT!;ZERyTQv58^b{pj@q>@kAv*jjbzG)oBsVL~rT=1c7kvM0ny6z!QqthXJ z%TbpW3(5NwvR83P3xn zaGt+`JI|7xodf9#3qf`je7Isc4BFt!@gHwie~QzCZt*OIA89&}I;V@3N@@Y=f|Z<| zoFK+!K{jp!C&U~n+mqfz+sT=$y0gRaML|Naf7oR}TMH;DHK?k8BjS5L-cfAR*&fr9 z6SuMQ2F~yM3#NNoguDaMemzuI+bN3i{d6tLH&a{SU^!u7d#BIK7G!yf>`RTW=9_kt(bchR$`& z@Ifqj-^vKU_wUOR4t%81yZHP6_Nty}h@b*ei`YN@i2ToNe)bU=rO|W$!}IbPP%j6u zOo1YpUrd0U(+5kTSOj;__Qhas1v-YlpA3OhYDxH5rPFNY_{cu1 zjc*z^yeytsQ6MY6cRk{tFDrFo%_mZM&1@;}xDR}b$u3So+j0`Ps-^gLf&^_X){@!9 zK*S(LpqWCjwvev(6;yz;KAzol;a{$l&VPb^9N0A_FB@6{|KYUe3^gzxfULBMwX#!uUI^uMV5Z-mF7KDlyM1UDB+f}J)0 zbP`~2VSFq|s=NizoY;GXsyfpAz3zD1)5>GcTUZ%VPbaR&J0wNB%wK> z4E{a5YOoOl!xEc~;kWD6IWyLSRA5;}U?;~uF)Kyf#z`=hjV+&sLQ}O-jAam-+;@?R z5NSzR788&NRVD^Sei@`WJ=PfXOFf9|;*1X^aYdKef*MdyI4J~4pqNoaw@Ku90LF=9 zNTmC?Tb|;oEe9p?4IcyH{?*jz5$bT|!%O#N-*1R~bW8~mPg8(2TY*?=5Nk~8GkqAp z6FSApMgtj71K6ChYll!0u$TQ+fQ~hz!P1tuk3`sFT=Qrg;G;6SUel5%M^Kf*ZY9ew ziTdtv`>%6>q`~j&1}xAl8vt~v@l-cQ@Wt{-k+|P%POtr=6D8Qx%;MDWqjc!Ao?N0Z ze|SU$TMdHNo$hcUutOK#Kk1tCN8HH2l8gU?|JHywqV4THkRyhJwe`~Oo|!S9b&t4I z{#}|ju+a4O0L}{z{Sg5o|G&2d2pj)X-WIRn;)G8uf-iz+6e<2g#F;q0H?j~YETm%= z41Gw;I#@8gTI07q-kSWE%luCS;J^7sU6(lF_k%OSRuuR()9$73ME}1GYD6a7hoGP|E zrOzS@f*&GiVS@^W{^f_r;NyS)!}=A2_G|uro`0MMRBE1%9Qfl`Xrm_nblUvL07O{I z{TWTszklmLzSM4^58ZM0kG~*q=O4HKgJ1OTHz)UH;GceO`1^mfaKOEG%4qos@4LwG z{O!lxIXjTT>G;w&tp64e^x}^xt#g3&fW_J(n(SB<6a#B^SRy^k88voyc5BY{l z6km-Be8>yQf82b$?vLC5#ka+(6};p0d5T9ig(WHHmRHqO+|B0*W-PkhPW;x|a}ay) z_ya$Q_YU&28iW6%PpRDMtjf+EXXV3=Dg%83#^X(kn(rT4;Eoq;I#+?cyO5xp^yu0| zzl#G|yW!gZ>a}Uhfo&r+9WPi%`8?@Ko+&We7=3-WbM$=dFHgB}Nv}_r56lHjOPjtb zo_8dA{;{k7n_d53yKp6ht&Oa8Ou;4wRUv-kiYF{ijZcew{``0? z%~DWDze0+^a6o`E z^L>}W~1ka~U~9!0w1cD$(LqMGP&uF3gipHV}OQ#?TTUg1tg zmHM6q^!oSn*eTlj7d;0{Hc-iB;upvSt5`LV^g}Nen+8>1jTX9uNdD*C*}B#>5$Ns z;`7rMJ5s3D_7gfTMkkJ@KK^{MT+V35NkYjaZ;&DEk79UI7M50gSd`Xqi+9_cs1y7M1@jc)8m#d+@Y$9ZJSwO-YVh5K)pn4J~5n?b%oH)K06t0qv0JIN4QG!2n zC(H)VxLy#x>*@TO8oW$eboGKImt~2P z$GS1_I=!ZY-7!yV<40KQIPt4#e}hqSb1 z7#F+4jfKCDRj=Hl`~MJG!{5&xjG;;5yT`=NL*1MxG_*2)1eP$iX@0y1ft2xoac2j9 z$3PW4VQdTV`M@3NF|jFj85x>BgqI|>TCz{|lntJ3;LK$|jgXxaThy)97{c!^*zuBB zP1Z=6l56noBirUH^i9b6n%d9UQ4|*z9OIK!VH)pU`~2-$jEfiI^Tif>?WbF%5_*9B zcb30hmY2MRwsrPt^R~=kn+K^|eo?=u?npjUCd-O#rmyGA0TB>!wrF#yzfh7MixJ~X zu}E>FN4c`gfu$vc^N?YqtMbZS6RQ%!rO!KLorVjCLfsE72UQFWe{{F{^!*ygN>3|b z>SD0;uOKgqkVJXMHZ?hDSoEZ8skgM-PQ+im79ZGcMT?tnVivQ54_{GHQ0bLQpooRC zv28z1Ut3Ljf)gY1_fYpedGS&ugp;|P10CnKqhf04;s{(Mi?o%GfmE5)VK{Rtd2pQC z+5l%JH*j9IZfu6GPNP9p7rNogY>;YQaU4#{?t#qC_G{zZl@sZKnJ1GwJ;GMkzASe> zV=7j44!KXIy+Hve7%O#yM8C}Rg=td@?FHdN;&(*8EBG1AtFsX&LKH^n# zFxv6;aQ(HfYo^xRGpf2qEJUdG+V8`zzxRE0&HaPOulTQX zC1#zKmu!Bvi<)u2mvp|~Gk@;<|5&5`+&RH!Z;r_s9iA3dd%Pie&0EnuTO)Q|Z9HiF z>HMNm7xUi2gGDuJPpt7d`i|UA73RP6b_&If{Vc?+w)mUURxB!Xhw8i3W6ov2BF}dI z-c)7!DL5*}defq4AV^dDy1{2tKYvK|&&Z<5yXO|;bV7D0%6u=kiOU-=HYfsDSS2}h)NuSFS~5>%Q$fi<&C(#R+{t+z4p;=P>J_u z616XbjO3jp-1ifnA#?6;3S<3uPfv{y zmrK8LLp2e;)-PJ^OVz*G)d!Nz);URN#9QRcu^XxtusAOhg_26U&8 zt(Sa-U#*dEE7Ex;4SBh)I?{qt^9zE^VkN`?=RN%#1S;Gcq`2stwEeh~!r!3{1rTZZ z0??x{#YqcNghu>;TBs(R3AD(r%eHhSqd6H}rhNd9zl9KO=t^jwk5SO6;7&2ENO4te zM5WKhRrS5tTcOmgGZIfuWYoxm?6H^~_eLvRlBVM7Pk2tf!$)vXtim}WaGqw2Zh{_w zd}0WdZ(0vka1MQAf4Zik1(4%yoPEP>7LZkmD}Td)XBtl^wK;4KahC2M#u?2oYz!*) z+j_?xcL?LijlRmJU%(>PMX??!vRa!Lxfjcwvv*bMDe({ zk`6-g4r%Z}twImk(e>p00#VTnc1T9x!F0XiTZJhD>Be#cyWg z;d2kTnd9knQ>_`X9p8tM zb{h9?LW@8owqdML+6T;jxS$~J<$yyUT6V0cNcPPxHpn&`U#rL+gK|IB_m)J)Mo~JL)p~R_pnz^@-%7eHHIT3`(*k> z44Ff0aRq>0AUcMKsnWJJpR6}jk>-r_P7e$=1t1-g6?vWCQ9%J2+STQt!1lOi7&V^j zy)3~y!0l2eDs};smg{W{bdnarBUKVE%)xKG7QP3jAyq8fmR@0Z`$D^I+CKT%enQ0F z4#3s$exzch5vJ~8`QgF@FsM;g=qc2AXmqyFHFU;IYKv~z^!RhFw!>6Yftvjxh|~-m&4K=X2+*Y4OErR0RLe5p482_Dkc$9uLMSWw*eG|w0RVZW{%wq z1ZKZL*`m@r*(?UR9Y1vaFwk9-p)yVuL6@yy%h5o`wkztoCgY)i$=C?lra^|9eJy>wE(8sDJ`T zL)2pMjNalPZZ6FvjK$J6RYKxVPjc;mYVHM=W@ggGqc_|*rkTaM6`AM(iO!{^v?P~n z`%+E0*tQ_0X(D#MCYsoK!5?y*b+c?{$ z>dYbWjQ(sOn44{SAW_-4U*iJvo`rsJU)~Rq5L-=|F1b1YIKr3Pk_ILc3$2kPZm&@6 zBp1a@PY~*k;ZTBy^+sI1al|Xwu-YHc9XZt+dM>v(q&9KYp-o8@F=w@j()ShUCJiu%25j& zvaNw%dnxV^{1f55=<*;mcVT>f$g>fyA_Np)fBsQOz#-_6(QN5UgX$Dt4eqienY+0WNYsYHWPTZ8V<8O8Z6Uv%G&UHFQ+NCm@o zBamZ(v%RD6GFXDW;88(2)H_a9;Qs{;s#$? zQU!rHUoT%gG@bUsD~H0iJmOoKn=XtW8%q$zd4wE^|JA=?URQSNOOeNX5xUW_HhVerSX}>mZpZovz(li*mUP+Ev5DDY`Bzv z#%Rrry>IJEfV?juSbxPyLTllEk7ZqglLIbNNyVzUcJ|a(zX+3q)1;R~18cN*j(tgP z)r5^rK~`X$Alm?O`_Rg-4;3EWd5+tmVA#Uev=j%!U`KnZu1Zf1nseZ2s)i zUMT+t--hQV9Qh%lY=9Y(U9JGG9l#&9W|qo@y9r{#x`vC#)e?lFd;*uyjFLNt(>ETt z$E0r?nz=q04Ya8Z-60egWXwq|9-IA3^%!$3#f?Eb_H$Mesw@)}t76(_Ti8L-!-aDr zob*?y^4KIWbPg_i$Ut7)buIAf!ds|-AZbFQ`BxLQ$2e_nT8b8OEjHbPL*z@B@u#oB zB@GY>qf&k29Kp)%(zMZ;QvgAb^knd|S52?Ojs53m&bHe1=(SiUA2gswFfVfKIu9If zF?Eh>&9*-0Aui1Unz7`hQknEY>PZRpHkjpv*F!BUg``@>W>aSu29z^!u+6R`_#qFS#e$LnBo#gvK;U)Yn+#UDwEOUi@sEzfTIn~-y<69|ZzJsnK1L@vyBHx^%H43kN z_M(?rTh$NFWP}=i$s#t-17SN`(h#Xt!Mm9x^98lR#E3K1h?j<1INkM_k|8iU?MMeJ zMV~$(8ROkjBix?u7Hb9$cWUX{hEvHHbF8h(w5jQttFl72Ua~rYz09RphlNljp4Pr5 zl*qNG1nYnQ?gId1jS)HY%AS& z3{qd2vt!T4p%iG^xNUvNwYW7<8wWlv$qjf*W=3vpL>dTt=@sakp+UEDwhtn-LkZ#@ zI8)O2%B>{|f*zuh8^Qh@gg{)XPBxOAJL)eKJHmxCX(Vn9bE*|H#Avz>oActdxe3zr z;T!mH$eYV{g&qNkRWPqt7dC4J>vue3xmeG5)8$m!2&aUE~ZMlG;KUZ0izDzu>vQBkgcvQ?-sfp|aWjDOo{3Sf)@R zzS1kMsIJrwW}T5_?%{2l20zrPQD>2CcOo=crpP{jmVB#yB6U1i+K*=-qtQ>l(cyZ> zQ=#3X7l`_4RG|Qk3qUS6Y=$ysu2H0a2@bBCgM z8jztI7YMU(2+h46dH5c>Bu&|uGLE{vKucwhZTGu=2fx0ie-E8#dmAP;tuWDaJbYGV z3s@G`T5)eQ!Pi~{GW9j%;cb`%xGddo49x50Un{(UB3MI@i386Fs9m5KT|;!1=$xiM z3(#?TJftB*ia8Kjja<2d(`J&4zm7E>GM*&C;{(&sTR-}(T7C}Cs?*o-w&$iSytmx; zJWEHpk7746k>%q>a85H!?ORr=(z}9aFm)bI&5wPnDra1)rKP9QS?IJz>qVKN=#r1; zfvcsww4xJN$uS_zaJky+K}c?Vw-I#&`Jl9YLQ7m0{F4{89YSPY(K}085r(3nqrNfhEWh zstLOZ+qLmqaF$#;myzNf&Fk#9YUO*1k5QrxrbhOIj5NI-hYV#&*FgG;%CCsZicJ zo?<5%O$Po?_T{Nik>`LEEPG&Cqx4)g7Pq%DC9**8K+~nAqTZ>fm0y?29m?e*EW{kt zyUC9E#xffAxu@vcaKMxuQj?vXU`0ALW8`Tnd{@p3{>92}Aq>h~9sx=HuAvSFd=nr> z(iG)G0gMY5Dku}Gb+RQwScGNkaxYd~sC#fdii+)WjCFk%8sOS%Ut8YK_X2*AV0yS6 zLvNkKEN>7z(Fv2s%i#{Ex1u;UrHbsJ7^E`JW&pVxx1Z}kpRE~(-!q%;aIv8V(E*AAu>|p5&zGffx^Gr+*^tAXUG&v-l%M_0yRk%zVXKNftnNhb% zR#mn=eD0`m>e2jj?h4ZT?aA>i_LMoJ{OFf01I?bu!g)Gpj9zI5EO8+xHoc|meTOLr zbrV<`r!U)8LRKM<@jV%Y#40b&%A7g(C9|z55@iYX=8oZZc7LS-CB1Ha)ZGlny0F~O zy3mKF6-1t&f(3R#{{F!9`~yKA${)yO7P3kRo9p+&;%JKWR|rUbe%1&G*Lm#WOeI$x z#<1vjXgUD-CEI%R26~{fLC?Be_?9#Cfq%kk-#7D|8T+a}sb6I64r&2tOjJ+n1r4U& zmM96w5{K*OEYl{SqF)2;Czh_P#Qq9BHO0?YH-XzK>}a~ar^b7@_fiQGE*aN+{?}S- zf(KJa^X;$Cvla9gwhSuYdCKmvwMlbvEl_xVuDt**X=+ETOgPsU9IByVplOq&%7m$DX1h;US`(`+mAameSEOqISnnU}Si6UKNn+KPOH8w@= z*)~zc!_PYyaw$CH==64WX|IlXKv--8!Z= zbvvw$jVo=YCsy{(uFF!3`(~1r45#r4cBlP0&2brj5_FP7cMm#^)H zxKE@*_V(Vo{GBGFzH&1SLDlUQ)*C6)Z=;i3Y+1+yUCg^__!)LM1@P%ulexTz z0ipuI2R4Dar%9dk9r%5~`ci>t2wQOIpxKF&M8K5k{tl3Z$=cK63{hTXE6c*^MqO3n zQ~st6)F}2({dwcnSfh)>;S~WI$z;%G+PUt~32&#S4R=q*4~PfOum? zWNHvmj1b91c1msl@J9Ed68|#c@K8lD^tIE3xF0X>Jo3Zb@9ePX2<*q4+*ZYo)w?xo z6!E>eGP%TU$|K_M5$Ju|F?REbiUYpR&IWJ%OOy8g&2)C3G-K&LNvf*l z4CHqPF5|xBm4T;`EWisNw#k2+Iw+)zPI?OVK%Nvxl1=Ku+@JI(x3?V;=4tCA?+|5& z1*%X@-9#lpZkqoou+urZWX$02oaOE8hi`hb4i`mKOaHSWR{rl7B{72MosQ(!lIu_LF~3w59@aGoHA_b9=}uxyMr-Z8M!^Dc;aVsm!wK z#OE;6$z?Awck~Fy68fECWH2|k7rH*VKs2N2u z@~*H0PY;e3Az(h=k~qeCj(sHzW@78}pB7Bbu@vb(7-`XfVPkV6vtr*l;p0^V{2OZL zyz(|-o|zA^d5Rj&!R&Pj+}rsVV5i8%ULC|I#X?~xqVg|V>GwWUFKw+*e89VyQg)@< zHBHvwXs>BH$-Ym=pr)lRAlB*9ij6U;>QcwFO0MVOf>#{w3$rluOhTJIuO|NUafYOW z;nNhN0B1F6C6)d~s>8uPgn6+fdxUxERj*W1!EOEu?<%~;K~EO&ya!Q8SNQxe8%=3B zBgUle3{aMS!QBL}SwMbFIvJ(MjbupfIpxiX8>s8zQsTY1RdMw@G?xm8>OLednlbd( zgHnd9adLmN7VdK}3WWVYl3)T5j^cU@d|YzjGv5(zHb+_m7k~7mHz$&b0Pn53ut^Za zk=!7-^b&jW%|euH}cPgJaS9_T48Cta9!id6xPI{LNUBL^BKG568BNcF@z3T zVdfUSoOXP;-GS-Uc{+vKV4qUWa%2uu)$`8DV(sNiCus6~6Q@`295pPBqvey{W84AL zcDkOr1%(m>3=kFlL3l!Y={KrOkO)b=(}37Tzs}kya1$0(E`fj#5(pE@M^EnK;RXbf zfg#W)h8u*v*J4+KGm-`QD1jfeDk{i{K|i$ImQQYNhhsrC?kX$+q7E!W!FLs24zNVK^*}mVhpDX#0k$%aC&O?( zEx19(2(=*-r)hR;5s1-B(0g0-KvqIB{FS?z;VC;zzlz*SYBwNVLr(Hv5fxv+57`we z93N^5%6y(Ab7;8k(-r(Zc7Arjx5o9U3Yx{Ab8?D}%xz%LAukFf3j;%5TP23S4pUxX zQv|XYc7K{ki4gfRL2{y&sDKEdE@M{0RxPjr5+pY@ebp=_1^V)8|H zBsTS!P~dp^!@g{`u)L^1(b#S3T?&qKxbzZlDAV+j%P?guz-#A^5UJi;gYzzC+54@u zn9fgHseG+nmO%Mac0RkV1T&v0s!w}DBd6K~WXh?Jst-Pu??M-oHVM4B{hd=vW8I5e zAq<1G)hy~yI4Snfb+fHpBGCFVuyWt}i)YvK^Eg!-;klJpZBpZ^!!^&Wy<&nnh;4Ad zI?Hh|xS+maB#gQs47BG>PCqND@}2GLJD;a!!~LA$_sc8Me$2Wj)!HV5reL%0=9f5N zZ4S1FvH4hOpG?vFoZTvKJ4Ml?DPtq-x|SRDj+b{yU)Y97B+YcNM2vhuw;jp0 z{(o8Zph1?NZ4IQ0;1_<-88ZJ5cVBvg)4%88|0&V)OKh@}@h^WNy>Z#9gZ0yxI%z9< zhduqzhA|a*2NpN5@$%HW_b9_WSLz1k0)ROp>6LuTUh2pW2Mz z!E(S}F(MumTmjL>C&~!dAw){smTyyAAuDDwHIB4_;LWDrHgp4q$%p+@%RJ!gjS1y{ zyXWE;4vyrUO#Z9-R^|bsk16bOsmEmIe7@`x9KcmFLLO^tNP0dwzT8po=JNVWRD!PY zs90jYoaA}k4DO4POUrpD|28>>m?U)+>cAA7s#=p5H~iN1d2P`V+Hn9JPx?qhA-yyF zw(IB0tR~nuLyGIU1)}X0{20L%cnQr!PxSNLD7+o8%2OhPs0DRB$ z6vcNB`JSH$*T}qsa6t@?t31S?1_oCM*lzEP{eVC$Di&ff0yU%U=!x1DOZ9;U-_d~- z_{iEKiEjnyR8k@&{E{aRkB36N95T?YA@N9_AG`xX%!~{6iN7NA_HdC55A_e{K;V4@ z={~(qj)6WfC6Is?JdN%yF>^4RoVncuXO=x% zE~_d@F0f1~DdD5N9mvH=B?nICbnveOF{z`%m5&8?3l31l?}e9MDfRmD&jwJdPsd}c zv7MOr$7V!3CD_PhVCwc3yE{BM3DKaMggPcLfIrCHZ`%$}3$DTYOZ_LqDYt)UP-F>a ztun#GG-5K(cyIBBjN0WHOt|d+zX=%3s;2Hw*8_{2*{3gp9)>X?h@z; z$t|{gKUj=M6_yiK002?@m5C*j5OhCG{A76XeUag8&L(^<+h}nYeSZs(9N%ltGS#ND zzoj-Nwwx>woXT1`H=^5o*+{nLeec{zr^Wmu^*S{mj}@GZ2i|Uy-nQaNak`2_yo?$3 zsebYEl@5td(WS!Zkjwzx0*n+UzYx#1K}tUI`f8$9{@pb$z{YdPB?YaGBU9+T*6-?c zex_<;gKScKR$U#0Be5yeFRIDt=`0Ym@y`Z%(GYeoA+}1?V`^RuJPtHOJASn|GX?hy+?-Mmt+s|*!0DVmw8bzm{Jh73g!DT>1SIEouKY-_ z;@CK*&S3d`6R^AIiH0XZVxTh*4N>mUy})V6h4=wCL;Y}tIBptGrHD4V<6km8HvwNV zN?HVIa~MSdUSHz4&4RRT?ihBuWs;bSE5TY6Ou4(WFhO1+L9ro-{*hM>WQmb>uNgIO z6^Ek*Ho9v*tLMxQR3?PtZ#4ggcAhC7?bfcfKG=b#!F+X8vkgr&AsEuH(Oi4J(W`eu$DFC(#I!&>u-ehZ_ z|4fkM=$leg9+TMn+eTV&AtA9GjOq8ldtCBuw6D;D_eR*N3V#4&!xYB1s7-?&CAtQf zyTeCGornW~TBgO-;2E_+#c8Wcca@KF#}nJsff@d8lrcdL9xWwk!13IxK`Nb!u_LKv z__=P^mc}8)8Ky+>+%1Ne>X<{f7p?1sX73K0aN2_}?^DKQ+>*wMFv6xz`SN43#d^gD zG%wYRgcwIThs}DQke@R)!OTBW)IT)C(doHt;F%iy37kStyy^n$H-2scy?uJ#IpRw= zja(J~7FDY*Rh3w5=~!a}Z1HJs*r&?BASO#9IMdXz1hV^23Cqy|R9N+gETX0#yo7*N zGO0R)Ocf!B14J|7L*lVc@9EZ1o;6H@*4Q57OeC=2Ycs$wF)>CaqJj_gEhwHfhhPTQty9rXN9jlwH zBz^F%mPCeT0lE9uC;JLFX=}@tQHqAawIo??4R0|xZT54J~otrRn_sXzx zX-w6=)s)}A4L^yc*@RT(gcza>)U0w`{J~ND{6%}3Yv`ro^VkLYqI47}$}GT0K)_z$ zc>jHrG~S{EsE$FUUMI{nvnnLDZvxE*nLuzpltyiY&jq0CofGJR{ksSbaQC!VBxK7W z)+vDP@1zBH?9aAis(jO*ZATNhPETobWYNisSBGl{kz1c=b(5i`BQ_m}Z8%ylfe zsAh20mMV+yIO#N2FQF8yG)iosUEij9UkyAj*d{Ab(^A|1OGZeOho?s0iIH1tmgBV5 zo%kGuzl#c@OR)}Q!(-Vso*E_g3Wgfu);$Ujd%4p7BNU{5$Pjx9(QYfF*wRBYxfd0N?fLsBZ8gUvLN-k!$Bl0tdsRldG}Ol6XjGSdN=E(#+D?O&GnNM) z1w0aT(WD#JTdxxjHz?6~m!7?QlHr516g*G4_T=Ijl?$NY`nRI(Up~H?0ZH7!-YOtA z^#)?oh{oTPe`z>g%UOX!(3ASlhQdF_Xd9$2Y)M}epeqP$z6Cy#A<`bB`(Y*URi_3) z0C1oC4Dne`6_B3SmiD`)Y(~_9bsopL1#)_T`yaGvFtLbPEzuu=@9i5{D=2Fh2Y(3wm&55rf(Skw6O0Sk1=myJWu}ES{e;)sh^6PtiMk?!EfVs-nhpsg>!}Yd(htUaGN!rMweyeA! z&A81-<#q4DU4P)#oZXJSRBB}$zZ<1*OG~r#Ke{7~*oUZ%Qb5$MwQtfXro!+c4>JZQd z1T>>liJLy)+%h|p{ku%U#og`vT+_zb58o!3U!8e#rPli8Ku_%fUsaGITUNdVAGzdr zlJ+(hY-GFo`NmiF@zDe+)dN)65w+#(7w>mr+UKo>s5&b278I={;r_LDgup-my5|7`fiYTFLJC=<1`ScR^ z{?i@M2xsUM`(QDV0)P6>Yt0w1dvKBR-SW+T743He24aQDy)G^8!3-}W+5^ytag z!OI0mkhrOR>e(c2A7Kx?d3V_}SJs@Y;x(28QQ7L5rwi^D4ho2DXi)uqrLugncr>i@ zMb?q@L#A;jbJvoc!Js{+`|So}ywra1l+QuIaR}jPe8l_o?5vT0_yC#> zj?lh9jR_V{G#p?Cz_T93|MZ8X%N2t5KJBj*!OeerAw|0ewSQ3VfCy#%mnCgTQNP=z zM$TTTMqgY0;9ok7Yi!oyJ@t#a!R!pjkqlaT2G7pL-u~s=TuVlC zDfBIAtZYi0Xp8S5cO_@Wgz8qx+nc42M9MhM{HC_89NKkJxFp zy|>Y7f$5$m|UU9+;jv(>mz)E{xxA{jJJT>J+5?Cp`Q%@9s2E*Qi-NxJ%b;5WfXLgvuK&15}c)EO=ykyeg1K*MkIsRXm8s9uW?DIAl0}Q zz3GbG!ExA)22o+bJN(`J$oh}NZCQ$O>k-LngaEU(sB z%F#8d?2FXY%r$`k$;PgUiW$V(giE0Xgjodd1dRP>gT0n^EAO_+FZZz5xf~W>Hhf8F z;NhG#u+3^A^q&pK?(;Q3>*fVP@}CW(KJ27*)NLIAo&A4iXw>j}^31Z#o zNmKMh z5u$JzsM|Iu)M7-icsU?XHlDv#a1avZLDX~4xd`Ut zEAUCa7dWXjnCY&$btfnXE+tVFDn%w!Z~=2>?p5TmWzm4LuB>dsd|=7%&AyL$ zsczpGVdKsACBRQ+n-Y=hvs%6S)Ffu~=&ET`SaqEba$Sru*i-FdddcgJJbU&XcFD2q zs(Q4H-p*Au@8ie2@)7%OKx-u_Bq@_X*X^y5kE^QG*oQG-ax})MW~EnE$iZv+t#K{C z#50f$bz;7gB<=@WJ%f8^^ilg?JQu!PQ0DbAQQIl*d%!8+w7`o)RTTL9G*Q_fvp60| zM{We|mmR@i;`7G4TSv3q(y!%U3IxvlBLrhEIdzqU2#98-PLCM?Z+-ADo}U@5Pow?W zS=0^9bVj!p)jyk$fIi}HxH@;uhWqSg+!{8x+x7Oy@U`o@KPM_Tr3LT0c0xBp_T+ra z)Vm&*XU~e}UyVq8ABbIh?uq#)mji%keeydP#ztJ~_?LO-T#L_|)1 zJ4FKY+B6kYyPmT1_G3o$mlF4NmtHP!E}};H9nh@l^YZfxQG9FSik_yrq3?sT<{}&p zY~_lCl_Y;0N(G17Jgg5)urSVZnqnk@gPn|#i>}XMet(bkG`c+^|WQWpxDM9YN`9f34^hAZN*e)dn8|V0d_XK4z zWbX{QEb-58VpHofcN*26#;up4goi>{qp5J}xM3nEga*bH;dA&@z#d)XjvS!LY+xaLbpUkW3 zHeoBqS5?%r?gEh8<+AU#!jev__bag&oz4rzi^oS&(KyALB&L0V+ooSQB|)w%U{bio z?4LyPsPLy^L>yv2ahpJM2&E0*n`Yfdwrzqp&j=GT0ie_fi81rg)2WnHLP(|UpWjR+ zQrxnc!TaF3pMMQz9L$zFL+5N$=Q#ZdgJUln+pH^9^D4Y+QwtJMXQ%zUjA>D( zkLHY{4QQlP_MESyisLDe&t>zV4^R^Ial?Q9#Pvvfp@Y;XeJ@WG7MArti1SI~>6bj~ z0=aQs2c!cA3%>VwzI#%wQ(`sQLp$43@1y>(c)`nRWrbHMOe2~JoP~ve9%RKPcVH)t z+Nt7ghQwj)bGdHG`^qK*L?cm;6tRn_@o*Y|d>ipEJFyC=gRq73@L%xfKLtdTB#aQd zyC6_ye~7hZPt_kIgUql%xeFd1^UgV#c}h1OP;2OjVA1x#oa_`q3^=EEg7T;KfChE8Px+U)wWU--!F1*b+d={~`Vphq%(d!O5=nO;~pAnqk(ay6{D;n`_htKz&o9IO+D zIY33*u9P%8WI2{a!sRS_KfZAo-prStrPq!p{m`-kw_WGAXu@G7AS4BP9K=?FtnkS% z_9~f&-6Rr+Ej-(XXm!vUxh|ND6&Ii@3D$LkVX<`6z!+`xH#7d}ySE2N!|yxbO~s3Ca| zZ=;uiFhpb(zf@0;m+3VPNtsbMd+}(wt4>I1NJ{ZkY*2Q7$k$yTGC$q3+~FIZz#HQX zIVG09uKCD>URrv%h)sUI;g}GkC|DQfgGCa_N8=?K)U7wrAks5;CU}9(CdFq{w}mZD z^v`JD;Vbq+b0l&t?2FkBysD}9iL%7kL>X=+jc2|Wqpje}^|m(IU%##GV|!89@v(x( z>=Tznjkn(&?*hWdlck{-ij#}a6Gnb&Scc7{RNQdOe}~DQbjV3fd(Wwnewwgefi1nN z^C$_Q6#i#{dU~eGa7M{wVSs(u9N)N$Hrt$J#B)^Zoia%dhL09^B(t^>>>(@@nMSIs z>V!py2s@q&9|4#ZnV5eRwx2o-+~PN5Mzh_M0nsTE3U2RH?g>75v2Gyu0vE(B6E1T4 zya*bFm9*RtNz$HQDG`D+9VfA8D^w;DATzVa;R`} zRA(Qxw=WTxIAsBm{NuRN7=&*L*t~4_r>%d~nTt0b?iw$MOOPek6x;R`XM24sx2N@; zH_izwnKZ07#k=97jpZxQcu2n*Z+tOJ<-_^9>biMvS~An7HGpL#5C_2GbwH$)^V+;0Q#s}ipb`;BN;nI{9KO-h@LXvmvG7pOM(Q|iXyRO&z*?bg#@6Empe zYMc}DZlZU!H>A=e74CIi0F-~eNsNr9jH1E*j2fAeo_d@5ugPU*4l64F^_&vsTpL6k z!rKI)I~S)(iJ+jtwgVBLJ>=7v_;g4=RSb}2-6@HYBuNA0_Bl7@?7%XAWprZ`oL+Kv zvOM}>l!JecPiJOpUSG@bWe|0@qXJ@hLYOW)1gEMC1AtXUk-8vogt!~DN*9qceeP`S zc#zF%n!Ui{7yx(T&*9J_Q3_DL==TK2+r;3+b#h`|qH~9yJ1b!F%TuHKhsGcBKunB@>_$Z_5ZmUm0kGmkz| z8+N?u7+^Jzc1lyu^B>Ka(+uCGy;&lAm@)H_)|W_A$GbRFTNNq$Oh;AhPduAV8zfEV zPHh`DLvyiwTizXEAuv!TamG7A|C^3jMNHaW9z*S#6kLI_yMp5ZNWcP+>o$1i)ueO0 zX!~rN*kBxLGt@w(ee$n`4VM_vnR%a9D`|gya-WkM#7qI<%R%TDu! z`X00&HI2H5?r*n`BiBilhcz6=&AexV6X`pEo$B7UAv5bgfk@{ag$)2K^sqpTd(weo zJ3!b6Qb^7>v<#N7?(lli{9^3LXw$8q-I^uI`LZeA7%BNK{m*^sVWbdp$Gc0y?>NVj zgjb(`9CepcUo9DK7k#0rdDmL|?rc#_Dn4)gh95>jekjAc*HZsk zuTG~U%`#Oz3?v-e$IVVdVbDA@&)W@KKs}_pk&2Ruq9Y#kS0oCyUTjf{y|HXgB>w`~ zOVaDJnz&>voJxcLaD`E<#eBKvE${~pZP2UUFfxKWyB^KL2G~jTep(~dAr>0hOBB;? z#*)%3eJ@I#AOB#qGwz-Y@X8aCZa6(n2*FU(_)+|B4y+|;j%~rd2|2eC8E^EM6xr)&bA6t7xC1Ik6aC&kJs9f9EH;uJPgyolk+2o8HV+koa1}L5aULGy`K} zo|XRbQT~a{E?-GR7UyS;iLYU*nc2%AGEl6m*1H1W0)gHocu&Y1GSK{RK*EBOd zt|Z&D1j-|*LGI_s-9-HXf%P!?Fqa%b@E{GHG^V`-a&#aT$M!%p(l zK#Q7Dyx`EE7NnwY(S{HA)#uvzj_LO4S+nkgK%JD18Lmte*Z8{cKAnuFtE~-sl9Y}w zj%6cB6r>?q=1zOdIb7+@0t-ouSXMW(edR*-E+~Uu8`5OH!&WSF=H{bv$?5`a7YD}G zYMtJK^VvrF1-rDB_40P!$|$+i=~R-^<eT1j`tv3jJ*Ttl?O^_SpM5+x6dvFVEjd}o1H3ygv0yGff`47if*imX@0 zgxGMt0lelqRR0Dvh4KQvGSRhDPia$lv~BA^7-j)2)lIYD?ziPj51pE!pzTMCM$F(V zI_Y@F1#fjcL9_aK@>E>_P=zs_$}YOqg~+GKYUs50&=fOb(-1lps|E>W3B3VVOiN6> zX1$+z(H4$-hg=E?$cZ~N2^Obnp_XgK zPRNX5yPyz&_+Z5KwY6<^u;4yvCdX;FoO&7%eWM~ESU=CAqj@c1N#(^0uUNyUH>%qu zX&|LsAs>~7OR6$@G?3+GZ}3#j;aQol%VmamVmZyOJF5`)#pV9Rj3`Ibq$fXp-r($x zccmKJ)0Ex9e#vz%(PM)bTzww2ufzo70QEeB?Wyu1=8SvJ^N7^n3%+_PgF@t{KnIjc zkuwQ-zyM^ayMNNT3$0ApI*o}Zs=)PJiU*7<0)bCe3`)FPLKD!av`NEw)OLb8bc4(e zUXVP1r!rB8>qobd4O?R><^m%t0-FDForNYCLBDo(IQW@AQH8?s>#CX*o53)H6#y0V zaT^La-$yddo!K&A&h43FJvX8V5L~%wDyY&Is0pNP0&tj@AZXW&wH&nnYBy+xqQ=yGJ~_ZDo~%o!>fg>D3BQ{)&?UNnYKq+j>gHW`FvNECSp?BGS<4~*@o zlJj>1J@3`5nO9jNWw?Z+Q1`*bw{hf9OKt6g`AaQ#O)9>dR6oRC-C_VpB~CtU<2Eep5#d3lp7#Wb~%aP2m2zkA(R2;<0_?y+o! z;OubbPw(UXvTLFpKcON1j3XToi&Y5$lvg@}z(yhMi!gg}l|rs2_UDin<@{*Egz=f( z1$znl;7Je7J5d~L?QmgwGpE357(V+0Hv4UAe~=rxzP^8}=oMPb4AiGPH}ipw-9BwJ z`w0oRG-=DLhS0pUS~iYePdqFdAwdIRfvncY%EacjgXr(}2of|jbyC#bSj(4KrSr}| zEf%w`-#K!dQ5l!27=faOVji5-wL%QUvyXJ;c?tLf^>GYdj|zS(I{JD+Dcv#25uDkudT`>bw8@O!Kk z?8Rx1`b#tj=u-e^>yX%Q2j4Fym)lIocQV@K#tbP&;)KQC^z2+HPL1EB0Aht4%Hd+B3l2ZXfi?eFGkr_Cyl=hj_$J4 zJg2|j8XR;UM0+Wc7Nc(HO%$uy6qfL>SEkjLs6WZnI5QIY*SBRJ3pz)+e+mB{T!g_H4Rm2YlbyxMu!RSg#jIt0bB*}lNskl6aFR9V02Zdf?#tjj=P`Jos5#f zwH<~r<%FZ7(_Fs}>R!SDNVT1HFMl5-dC$!CDPA-=&J-_0-$NQ`g=26MlYCc zzLD)WoZQ6;d6CjB=N97fHmvkgv77#ESEX#-%Fc;64X;v50J=%qH`Xxtk)C;`m)K}O z%}b@60I$2nn9aKyo*Y`leI9`_F8ioi%}eXx5A5ESr?^a05B;2}xmE2_=8lVHY9mab zx+hnttI`EKVZaGg93-5>*yWXI5L@?1;U@`~y8S@eMk;(2aocYB0m>`kK$>3ng}_Cd{~5dHe{+HU z&p%||;oU{2vaz7u(+%1^i#KkY{c@B1LmQkuue757*gJ zz9r3$~#9q-5$`~G}`JO5olL$sNy0-hny9$H9bV&+M+M5B;3Rd#n8u zAI{z5d|XF&0kW!KU-xCW7pRGKiG6#CefcLw=>dq8EYDrsANf1R%>2g@dr(@qwO{&* zxVN*B*xjoe3%^CI8>E#)#g-Ub1B z&Q4qXsnCIf#?z`$_-9?Xh)KQ{gvgmhNBI zHhkIp{NB$Cr~htv`3RVIhjYhIgOpmnO@N{_aU1-SOW^^=(_J+;g4KB4`<6pA9O%lb zz(T&YXwp=YLOimasQStJDSUOTYq--bV~hknunjH#7MTLxc?MPK6a05#T4y_tWvL=IYY^vQ<~!faU2a~iq<^j(AHkr=S$YX>48;q zM3v$1PX`u$yjJW!JAC8IaFBLou~W`iZH@N$u=hfeMk2`O9!4*N;;&9pn2tzdt>2j+ z{rM2|`TQTuAz@YYqO(@=Vb4xzz020*h;N$yORPNXy|AcRQ=NWzlKpWy4E+Jap>$#Z9Jwh- z5W_!XX@ek?D@PG%l+F?l4T}!Wk>q;j*U5J(ydclyf}g03FNeZ|&=*EOx3si9 z^28Zx6_0+qW%->oq~$hwomeXB^*jQ%ayN72fR#K&nQ*Cj+)QD*pMu_tk|dbH9^P#9 zR{wQLvH;DtZb8QQRR!b-v&h2QD)>=vOqpwe?K7*Z`ahqpXdW3U2;{9Z7}uzi7OxbJ zP+@ztlFRa0%_)#TBd)VWP&~T~8L#$gq0Wh~+3vy>mE{fx-fUVYErxiA$JJpS42|I9H$^qSO5^HbElAiJ; z=yBr?g*62^T&j11gewML(zlb}PQ6x#{c1@e+XMT77a=BuE5?zFuZvZ+zV~WUnL6$NBahg9 z8X{7kVLe?x2ADeKutMC3gIHxBVI!5NhEmPWWQFZttxmXR(1(HZg!w~ zjZq~ATaH^pTHlj7$HRja4VXFcZ*#K9-iV2u{>9pv{nQV`Y;f}3L9pR209=W0?MB!H z7eIbpS)|%5DqCwi_5G3u{H7GtnOh4jio3bqP9vIxD{WFQay#W7o?!@Kl+qShGmzFxl7-mI4Q8~lkw0b61{Wx;`K(M_t`M&%+ns;OXg{9wy$Gl%88X#|s@ zL&8jgT_kY33K|ZVjK6>*N)OL)cc=P!M*)$u{vbTWcNs+*pvld&x2AVVMXqk5^#f3p z`futVpQQW|X*i&?mhkzO;d4RX<cTt+Pybye$B)T_}dTEzX^&&-yr!a zo#Z(UnTd0hjCg3o_JgoHZl(ffV6Hc92axAVl;Ss$Nf7#UHq(?yDZ-n6u|qH->3$Dw z^LgENX;TI-Te3KF;7v?^Q#5ae1i2W8xPhYAOyx#qvAC~lyqM+AN0j=j%59w z(q4bf=kA`jCSE(gey`X7QeXc&M&iG1Kuh3A26qY$R~e`ab6tJRdF*iRxns^DwlKo$ zz|$*2PSgPNgL~?rM(k&qn*V(d_Fr$u8%}1lA?`8O7Y4q$ndw;+%-!nLbo0popAil% zx2z7IYC7Vyp=|f50~fY+|DT+J|9YYSg^B$C%jp49fuSYxR}c%VKPgMmANF6r_Pkg7 zvCom`y+<=7k32XF-H1}Sui5Taf+x$YbJPIRLF zYu}X2dl~p^>8{bbFEt2-WB>6n4{ZETCgi`oULYL!-!E8J|6#TaY%>Fc0fT_x?E`fo zyFfy)X1av#>))3dG3=2ftCUmqg^u}G%A+poc`q6K>+Abp`TqZ=%luEiW)-v(!FouN z{N`Zy)PDuAk&A3KpU-C5gHPArmaUIawtY>1^x$pOk(FN&!T-aN_)q@DkN?qtx~zal zI0IsWRZb5wbwN(>^WXJ*5AExfSR&i2DWBLjmhKdFx90QjeUz4wjm!NVw(fzyZ0XY) zY8&qkO`#{Tzb`K?K26*1&EoGBY)$8#*&BcP_1o^vSyuxWOaEuE1yA>It~Q?^!<>|j zhx)sx0F^Wj<5)Ww?%{mqB^>NmM8ZwsLr-&0e{e@8p$^S9I$R$TSQQ16o|hA^r6l%O z=RB(nobUOAa8D#FmW=+EAg~8p8oeDOB1n@uUUB_|dssi=w@rUTwP z{ihW6HJW_|Un@!s?nDaq3qTe8wcSdfDJe@Dlz?eK!9}`D6~Mle98jkV)!) z4Y;x_LE`_yotU(9n?HXK7e6kN64>O-Tep}-6bElBgM6dH_}rkN7~aWFGG`_>|LcNt zL+F<4dKQt=_ttKy=pj)ieZ`X=SeD=@oayc59c=!2aq&wDbYpS|gccvm`7$^6B{(@C zi^uIq8#?>8v^4(F?SD6f=EzqfWF2>$tR=AWDVl5k-5~&VaHwe5fHs`%@SF~fCx&Bo z&1J2jCloV3mo1L`NN!D)c>9{Wc!gy+I5yI{-~By~b>Xi+{>xc^6gpcN*n!ernF*fA z2>oZn(QeP#yEoCk{gcu);!mC&S1rCC%PIW`m$jJtS2ZeM#GGX{&6CZ$J-Sn79v|>N z68jx}%wg(5o5qCWta#x~F;6tQNYX-}_2ibxhbH2@frUhi?CZe~?Gh~*E#{K+iMacf zNbRF7EibOU+qHtV(;qf+e&8J3^0u=yd~{I)gt@-D*fQ|L&ay|#=;B~qp2c}`kCY1* zH`W+&;-|u%x`=G4I^5x>J`RlmWeuGkT}8RJ!`j-@^rqp3N@PWLZ$s;g`o^5ODLsAX zBd5%2YAsm-2@1OCH2H~py59?*-gj5MqO7S&x@nhJ8B|%^b~_m#DzW;y***7t*zf)b5d!=Gte3V(AbIrxnypSWtX6CiipL>}8=*I;kKU}|fW+{4d5bB`uay=ZP{if1DXql4nMVQSyMcR%Ha6U$YxGs+AMQtXzBxqmF3 zSNi>dZ$xjcp3%;wXwvw{tvzZn>a3LoNhx6wz(x{+g@4}qt-l?aBs_~)IfTE>eX)$M z^u%9jj$izKH35J0w_}Yj;xN)lbLZb2%G7a8&-n2(6E^ntJQi;G(#6&CN!6J*i=Vm$ zi8(jMESMp|d!?bg!bmQOs=DPFd#-G{z^2pc)DGDM%V6>YWQfTp~e>%?S*8T54 z>V|?;hAS&&Dn4RDyd|1DHNMx?1;1RY@Yc%vtL6A`=P66wFXz#puDy8kL%X*K7g{ck z%JaI}kaKq~E7?3n_h{Q+4{X)vq`Iz5A_e^9`-)edPJH!M{c^>N`uo6lciX@`7276L z)r_!&7sH&#bq(=W`R<|3N_Pm=GLEN9(~PrymPpf@I6ME;qnGU3ZyGhF zCtBJs>v(+cNt8aJ0z1YoM^%P0u8|mL@jz;@S!BYyg8n;3_(ZfKwOpxd{cTgukLAm# zL*8B&JM2HxIS&pv(DhIa<{|wD{19o91y_Eo9ayo(+AaiYndkpsyuEo;Q`x%ijb$k+ z3IftMDMdwk8#)n^DiIJ6B7G;MkU}~UMUW^VQIWn$DP2hyF@z9W5h98}gajx-Mfy$& zNd%>nL?tXqyXkJvx3We;~)L`jOyIdM)|nuSeur^@ZLuS zE<@*y=9(NI*H%1jq?Y=H#wt5KB573HZxtMV<9CD(e{DJL*P9ca&N>gGg{FNR$u#&; z66pnVrx3|ycF?vNuBsK~7Y_DxV7KrP*+LZAJ9Kn=dq9Cn_fwvvSHbB!h{Df;V@~$b zEeS2vvkaW}>e`0M=l4EDm*_tyh8@<}aXd8X%c8A??tRAXy4!-9uwKfh$0&V6=5Eu- z$)fYP;CWnBj)(Z=1M$#pt*lkq;PB>t_1*ch`zDX?JxkH=rI{)OA9Ih@Hq`oxsDJz; zb+D;VrLOLLj6hGXXugomk9YJo2yT5G%P3HCo$9VNUZP(9d+iZyq z$Fp9bpRMFSKMKDyKXzr!RN_89*iW2rgNPTRQq>tDVmMJ(w)wtKVDP($9P*(`GNZBl2{3VS5#g{LV` z)a;SG92GSD@ybu1S4Ni=L+26(_fGt7`Z1r>@bGc^N2!csJ~On8KihBmq0c3MVlH*Q z0JgT4k^E}h!+d;S(Tn5c2tC7@fu+SR>hp_IQffO42c>qv2Zx*+ZeSkkFGZF$H8#6; zO++r=v+?eZp0ZQXf8&*l_Cs(7zDX$BYhbrv`~9Wugm_#N(;ELr&~}-Noo$x)OHQ(L zNm6=_pG=6@geI%G^!3D=G>}Dl52kSg9_UNfZ->RMG;MeRjfqb%6#>zqu^u;Hwb1(L z`cav25F&;V`1YdwxuW9(bA?e8Lo=EGvBU*>jqR;>?6o?cygRqIQPbXXYykI{)zQzJ zXu`~vX3=#ah(3_S+W$fAIB;(}LLPjMsce{F!vb^tmrH*CvQ6~V_}|a?4{FEv0?>o2 zQ4rV+_lQyq5v!`a9slX=iPZ)>_6P4$nPOXiKk+jx5E%>t#qsOS zv;VYp*pLG1Hx*mKjs0o`&Od3e82!&;(}O~=3)cia!B^+@h|61?{`Ge@YxID|AVFim z?@40i|Jj%y9(<4f?iov=ElHMtlbGMsCUM~(9=M)=IpvJ@T1q2#0{`# z4g%-v+uj?}-dz|#ES=#Y#l=F6pEWdgNqD{Y5-Mo>@=P-XIvrq7F>WahM|{lG0FKO$y_k~g`@W8 zbmDC6`(>ShJYD6xwz0dHb%t^euI^LKNol0KREP4U~3<5Y%Da$Y}o&7!w zE3*(_%-!4K#h7UNCZPZwnSo^i^QE`e{9NwXGN{UDfy#b*ENdzrq0kjOC){%baJIXcQ85lr0ZngxLfB) z?JaT=I^Dd+CfwU{zEe5;$>FSR4f&C)!V{rvm~lZvZ7Sb5$hEk^eOBGZ{?`xPCgDT* zirITe_O=zUEmdrI*C1l%{N+iWMIPM;m_%+fMw4m`rh5L^nSvgm znHjyoJLV4+)Czr}kT`C)no$x#GPnsCICy|I%F@{{<6Woj`8idgqa1I3{VjzKm&0}V zV0AFwqg33jYf(!Cq7Qapl8_}?mQE(2b4T z1)(^eH$Zx6lYG8Llr5CQHj-PM-Z&yu5d4h64YZn6pb>1hsrmI=Emsl)iD6eP_6>Wk zM@)@>;D<$poV{%qCG(p0=>4*w68qfZeMeL>(ADa(X?U1o`$2S657u<3^~in5k6*+y zEg_+$SdsnLV`wT7E!Egw!b3v0k=)dRQG|x+ zN_Vp3CI|FdKuwTS^rztx)tIWYdc(8$rp$0VDO+l2C+Xxstd?qQgZqIeXTuhg$zHO` zy)D_>$^(LRZ^vrsUM$vSacpxaA3wWg`Q9d~9G9ag_qq~~m)F_`9A$ykH0oq_O%;)@@*nP{P;GG81tKkwlXojG4$8+h^RBh3;O9b1n|PV_{O zjyuC{!ci)fsNifu86~IUt*$kaZ+4&6LasgNbgy+j@}7L4x8#(qG|e&?`IeI0Ww%53 zV(uBh4eoX0n;huP6xH+1MK!cgN7POX9E#^fu#yS27z2QG^TWQyNw#YUFO48VcZ75? z71g?6+p&klLtp~LL`Jcs%eeHVYet7 zNx?OSFjgkD*hXoX9o+ZqW=50-2r$nU+=u~hFYF80c1t8rs=HlvwAHJ6V(N|XcoTQ& z;JjF#npU^JsB7AYE3gkU1cJZczp98Fh9909c%5738Pf)eB0(XTz}~hfPukDH`xXgf zxrd_*6zb-^hx7Wq_Gamv8&?V_XeK(4oAS&QK#A1e1#|bgwIGKxt<);yT1?mz56Z{m zb{|knaXc9Zs9vBgeo2#U!l{E9DOYRh&qc7ZYt6F1^vg{_y-db>O{vV_p0U`Y7?+6n zi!Gz~Cg1qpeETBgj&-bh-;t|*`x17e{29CE2l^XZjKjL~RJ~uE3R3Q}J(L;YWP2!m z@cWPNUQC6xC+U2~zFOb%s*&nj^j_DT=vy0|24V%u*1It=c5aDb3i z6{GiB$FxQ;ztmjEz7}o+4)-`CQ7$5rx&Byq6_o>~q^p5jPhr7wnLrO?`Py=;-~uL? z+f5Yci^l^j+aNoE0(Ml1_OL$nf@Q=EGZpbLxnO1`b=qWv$*+Rk$WYJmfn`g7_A%uz zf?CIbf|tAs2xEqmnf$GJu3o1{<;J@?Pw{JlkL_V%&YX9;NBB_I+i>%1!xVP7%ez|L zhWjja_@3x275XK`)%L_^(@F!aA%c)l+(t{IS};U*8VUWhQ-o%135+LB2cH>cqyIJR*+f< zvNCLGZQ^;qF%Irub$CdA8KNi}e`Wc)plxgPE6ag6yveL>1#p@(Pw)CSce9!ke`j(o z?KxFyF=oUDPae`@OW&1FNQTT5901 z-1Nr<^Eq!JBYVN!Q%Wn@)5&CCOjpEPD?EtlCI2QWeENOjZans@%q{W84GA7Nw)LGS zugnxLT=eDvD2n#d&^HO&6)yPCJM3;CzFPa}?z+=_;EC2aAP6AN5!TIcH)sGVu=QV1 zfxjNYb`w6O?2VWFn=Ad@)$JO{!m{U@VEMiMxxo0F#K2P_h#ApcW{~33VB7UK2baL0 zKM-yA07nPXA;gyppe;D=FdFQ3a*hA)SiCB(SN--90KyO&ks`pyW&$+DkKfT0e{)Fl zKkwdXX=I8Y2dP`cfTg2LvHA?>_&Z$EKTh!id;{3VYj-4tx=|9>4?C)FV2P#KjP*u& z2X~$42OjI${dx2s-Gw_>zix&H-4)il@ted85jdOH=p|C?0ry}$4Z_%S*g`sMeIVnj z(I@GXd>QB*YNA@4#hHl`t42uxGi(ItGH@f{f$;wEK+LY=z7mKF5bo<|7+sin$0|{* zQos-YE@g#4dNaA`8A90qz1#W(CSc`;*|8vvW4WQX$Y#z^G+|%;%zs+lWOM4`WAsR@ z!rmgKJ+$A+{9sWbep*x_-YoyTU#N@wL7Hq16UU;nG4!72+e z`_GmkC&=O~=J%H2f53f826ULe9tlEzwf;woa`@m86XX8TBH}ZGu#6~H`S)YvL*mo# z@W1|ZOyfSh6@&ORV2Xg9=0BJsV4(lyARi2`+{rj1QLhwIkN`DkqKRBu;To5|7b_sHcW6J+7Hri1yo{W1dw8Zua&J7CC4V!7 z|L)-6|8je{b7ez(G^A<)2DKvLS%~>jiT6LxUS7X3@86Tyt^!Da(I&Fo&#d=1gg=@j zhQ{I`YhY4>{&j)&s{lEUpsHC9&4^!zIxX{3Uq0 zX&_l2bmp7Hv+pp0&(?N)>Ng3IlM$MYV& z(hyq}u4-EyD3eUhD-<}l18edpTsGt3n<>Zbky`uP8T;R>J*c@f{(3Y1zp0=8^0-6) z3s3A-`l7p2z`79h?57Jc%et>jl;G;0PdqZSU<@`=#<)A{%A}cp6j%RdsC4natsOy7 zoLd{7zP>Ar_4Zt>6YP{tp^gx~=dNe3#J)_YI^wNgHyX}*c6`l4Dm47tM7b{W*O~Iy ziSl1~0c?c%53dCDN&r-H;6@+rV#*zyOr8NO)%dyGPkY9^zuq}~E5P*Q?YM{g=luU~ z%*?-eRLY+h;>aLhoI-E;Gk9Qic;{UqV(!oSc-G)xnd--9sX;c8FKX|Og+G{f3;O?@ z`G0f$|0k0d*uXEuH9)xIS(e9YUQvkisf{*>6o#Ab#LquAq)~@2W=aIVsTxNdrm(M@ z{^f1)H*e7YKaVn~pE6C@8FmLIy|}x;ZtV2&5KvN4w5M$)#nItb#M2*kXDKuveiY(g zMZfy9Og>z;4}6uIV9UGpM~8f#iicG98`QyHg(P|L_d;8)JMatKY-iK+)qWE^)P1mB zwSDT|4^R5)zp6DM1xl7~-z3Ng$}+nZBD<$1{1>4~nQL(?%T(`szh zw84>HEJFO8u#pz_q*V(qxZnfdQ=V*81ZnW!BU$;hT;+3*2_a(6V zZ!mXS9-~A|ZyJ@SPs7BcuVbaEYn5Chx4m3B9}?lMiYOI=CC!gBkQu1;2m2SM1K1hE zRl;!(X+Y!{NV|oQ{vsoTp6axq(UB9!=%@YPM4+TGNZ#=^_9L+EvF9n)*1h$!H+-_I z<;VO-ni_-4fsGcouH`pWTgX$6U%XV@mZ#)OGGAF*bf}gSxK8*>#U_I8HWjv@lXNW{tknn z?i|SMS?N(8{Ki#^50t{VrkGfKUCm&w`Nh-ontL&XH-7BeZOX@YKi%0$G6!svg)3SE;$7jl$2WKDw8K|REvYj ziYD;tEdv>IG=TFUx>BzVx@HEexOcgbtrcD>Y5KTzxe=Y%mCr5A(+AeO!Y|j&x(JeQ z3-*A@Jf$TTJ8DADforl1D1pi!XtF{C8?_mPl58FiuR58rryJQuJB8MVF+aEPf}lG) z99>6+K5VIq_7v~z_322P!a+8(FQtz+3Zk~*;=UvEqly-pOzP&g1Y-DLxLAXil8%eQ zPxW;XcR#EaByyEkt^Elpu>sPNH{`ejED$goL?6)}pVi1VNvO&KyF zEf7>JrFh+7m&+2p&%j;4!QKLcsLcf&RC2)9B3qc0AFDL2qDU{f`StOa5gK?W-B(BR z<&V@}8{z6URf_J~$y!P)84*+8H<4bZ&{+rK%`_d_2MY@xW$FcmaSl_QQLgKyszmzq zBSqH&o`Qop#^a3tDXWuxP!mrf{JBR`m^-~!uAqrNQhadaAYi4o$Sn199qF<{3;YRg z(4O*ahs8&NkCocy_ zoR2(@efe>GkhiVdF6WJ(@{WogIzpi=GA_q+f*Fl&kX4zqA+0)I=r1@^h<-=Eh z2jWy(%!L|-WbaH9^$SW$i46K=ATMUtxjgE?=i}t32R_?SYhjq<&zB zI^Jp5yhPdR1NS-Z;BqC8y}Yl{Db7l-6RY^Tbq5Qz9n$U`g-u|U0qN>Xloa~%DS^lA z3jazXOJp%+Kd6tLs(j;gDem1oKcDxt@umLY)p*kFcjtm$d!5sk^;1b@oaM>$|H$;P zccOfvK)DO@#O-gW0gwxy|Nw!DnBR7y%ComHk9!@261zcW2E=&G4^XnFm9 z=9OKZn>+rqluL3Bu`D^$hG1Vv6F6g3c$!ZHCo!gtP#Jrdsuu&0`MTHGE7#%sA*Z2d zOqqLwLKJtAO`tm+B7h=3j=a0;sKfLlOiVZ3ua-2`dj zQ4T^9?bbz*#DyUzk^Boh1@f)--P4epE03JCY8Aqdfv7A$yc`sNdf0LYOQoW5_+4xB z=NPrCT3IUIP3|P;*f96vt0i0AFVQ+hB-KBz-KM~jNfx;g9wgw{+FRlfyHBOtxr z`~C9yb}#cv+7+WPTbE07$b6UFpaZW=bAy$84`=4}YjM|v@_nprkbs^4gj2GS)&3Ol z*l~?A$%wO2x0u^4mAC>QC`zfbf5Oo|!=;rcL-%*8H-h6Jm{R2 z8In7f6BbCeOW0gi6JDAmxuhmI^b3c!4P&3v2|qfF@nIcfMwp#@k*rjr>&_XmN)Pa$MN=qFh^cg_IXPicE+)+J+Brt{4YdC&fy)OxR<5Y z3WPSgcv)>1+n8zH3Ox-x!!t=m$Bpb<7kHO*$c5Nv=#$_$=}bM-X@%;e6puni?;#RWB_OkYCfG`+e?{!W*M0zD*0BwYqs# z?#0cNqn`C?jN!Fv_JRY+KJu!=^8;P+56I54S-Qsehz?1rF&|u#ER5%e($2h3qP+ACM^*?W$njMJs+{Qe(17-W~`^_KzB1jl}5Aw28sIGQXQIpV% zi*)7I-HzgJWE=sh-*_&J9Z*7ICOl=})NtpvViY8oH?ctGElcQSQrrnbuRWT38o6^t zQRP02%)8 z;!@Gg7!lR^H_FLF>cbc1ySmk1c^4Zhmdq*-@$+U~o7&3B2T0_~;zt+cb*qD$RgE2q7JmM1oRg@C~JH0@w-h0qn^1AxMd>wz30r zCgm4siikPOol6uPoR2HD9JI7!!DObx19m_eyW-%gQM(qlO-6pPuxi7nBDkkthOj`m zCmYNpJMJfg_$~{hTZmu{1phpw#R3WcLl}R+8ay!%cuG;Luw;wmhA8$9Yda@x2P*Vy zOLA@Pn*jRGT#tkAQ-EW*OP{@mxS){=jCmUAr3Eorh@^t0dBp+r%E8-R}) z&#DZOeM^r_up?$Z^sMDl2HdNMOB{wL_ys9n>LpAWAp7uI8G(Z%r`fmeEL;xGi>6~H;GbQ;cPw5-yvdy{7ddpmY>AqAzM?VsKqNK{7D zI*_*0K`9UUz!1N;rn=9RKAfd$EF!~ID>RF%9F?3al=5pD?sU8h&)YKa^2nJ!nVZG_ z$Vl7bh+lc*^R{xc^d}C7`W9Fm-+)(g`Fl$DxCg0QK3El}MNJj4-ZLV#S@1UQvF^$J zC1G{gQB74*QM)!eio@(!nLNfqG1_E&hcU8M&@@;ZeSt|~{JEd)crZFGz1erWSZ<{0 zZfjuqe9MmRjA{eklRIjn!#*M=A0w)4ml6-RC{o$=Hzt|sLLZiu9DfQKrL;iXhSi58 zJ1v&q%wIH=eecJ=-nmne3`g?uGd=c>_b#qz#d_o|$Nw7l|$ zi5||yM4?$CrQqG;7gq0$u2k%~|57V8Gql&GRJvBh%^8h}YZ#=4$1Jiab)i+m93>Uo zfT2a%u5-h>?H;hVEcU|*R}b9-pV(o;nWVn7XM(7guuK%!r9RaHpKfxDkc?7j--LZZ z*xZDa6aK<|mBWJpbY88%K|B$l1>q}zr*i_J`J}_tTb*Wb6CfjeeF^s)VZw;sZZWW$ zV2DU7wdCy^O{{yqV`R1$@;cSf(9V+_2mE{Ta%`1&SiEJd0N`P{{6wBohbiDSRlh}c z7_nC%FN8w7sxW}N`8M#dh4rA4M`1fLRwM{yrsR3rDJ!d6=D2IA`flFbS5H_85x+cs zI{NF-GUk_Rm9wR08&bPp+PDF^hldt|#nyFqAs4h9%0foD9?Z5V)jmk_J&J`LeBP-4 zu;i5U9@Ugbd7YoaaHnB-dr%lFV3&es|r;!_X(aV>Wtx;s7d0pJsUjX0|}`?bRV zZm)N^3CoteTW~@=*{%x(34+8uh==d#w9TR7eS_G;80S_XNng>3h@gmySalgSK>Re% zQ)7YMs~+ER3Qd(s^_{Ec1_iVbF14rFy%xe>Qhcn9(kzd~hzk7BnbgSWS58MfYAoJ&%^YCwQ+031{k#?KW9x?8>b9{@Ax<@# zf|Nw56nhe#JsRZtxF$i)nGtq7xuS_ARd02J@VfGZ@`H~pUSo#}i>uD!sJeD}gGtCM zq%+b=T3Hr%h`K)Afyh$QNs#7y#9VqSxPW~Pw}cY8{S2CN4D1#k*0{3l2;RSwem$s0 z@$DI4fGMyCoQQrKXah%Y3&tRdhw3VPpGCETu@m~UuyV}jY?~aXfmU_Qq4)}1paCuY z>=TQT3i6VR;2dyf@8jST3*&kcv{+dwDL7D|j`9{gq=iJPvDU7)CV)SrfYF73se3)E zyMzLKQbuZ^Mz(ZYFi)yS76^3+oqpw`O;3?qyY{_PovlWnQ~s6uP?Wx$Zxn#yvF6>(CmQ0e(8|= z#mAWK;XMlBrLUsry9Q$#4%1yz_Hqvq_3ePETYmW=mGhUQY6mDUm4|?LvS+qjohBK+ zr$X1Q+NnGD>b7QE#49F8AKzH8D*k-LIw6pDr$36)GfkBECO?pSC6+e z;I0ek7IFBGG4(05&FxByldJ93_ufLy_Wlu$Eklv3PWvKPbxfp=p}#~1)?n2|Rk)cd z-|Q?7VdDqd!!ZUT$rnd|8_(YbF$EWJo%2wKKnFX4P!{h%SJScbTyYLXXmK5oGL<$L z1#%lZO}Q_t!!-{K*AaQ96!o+UwDT05kKI3J{71c6*>Tc&th_WCIqvZ2ut|B9TU@rx zrlPZ?rQnxg-PBVA*`#dU;)p<&VR*)r@2&-BX?MLE1H0po43$63%sLxC3F{L*RV$dG zeC}=Xu<%ZgI802>My2M%6f$omB*B7p+oubr3cpDRcg6ATxmvGDfegLo4TL)lrSQy-j~dsfwr__}?59TZ*gga`C5gc5PZjSAZ&!US zR$7E9zT?3Q1G`Okss#Cu_KIq6>yl;P?Tk0ov0HeM7y6k+45-jNQc2YP9T;l6 zuPn_P1*#6Zt5a()T#LD?^VvOHw_R!DftoA(lQqdrDDfnqRn)ELhto&X{|8JS6RJsoV(g-(K>C(x=$3CNG5SjK8|v~6qPVoB@>lzf06 zl;L=X>2N#N39*biCJX}u5RWnE(t40L35`I^QR0l0MKb|Wfl9sw7SpKI0#F)k&8r$> zHQ8UCSy4t4fZ_=X<8|X?!rHg+$X&}YfsC{G^*~Jh&WOTfyt?Y0g%;z(KK>>IgQL#v zMtkUioMp|(%x6OdcIx}iU-7e(_hII_7K|LRG}PX<=yO(w^Ki*V`RqaHjUGea#@GA2 zX}o96SH0gLOPdP;59?RY1fMc6?}{HG>f6f5B6n7Pwm`VO(DHQ;zS-rFGK$iqg)wg- z8iNax>j2(D`16xCUeYurf<@RS)P}Tqjr=4ivAXNtcpZ_w4L3i1QmJ3!5IVj`Pd9Cb+;~!a5~Lxpz*r7o{5c>(iWB9oMol(9 z!>KhPe#Ce_hT^WW(6P*IuX8XJ1o5R zX}>YIBf4capx^U<=F;ddB~J}5wd!%n9RCzNm6=6&q}2V^%VpRQ7_3*%F}NoG=|+ zpW;YuC1>$4{#?yB!TKUwE1yxZO~%VC|wiPvHTtg6Nh{8<|Zk^EJnZeIj-iVty{)3&n~bHQZ?E2V3>8(aV^b-(WBv88hEqsgIl#T zkr5cQtao{2y%h^sW=}XMCBW6a#0D{ZJyu9MFh=Y@m55CI848(6Kz2;4kG6VLFBFz! z-~OTQ2~U#IaxRA@lAd(rBv1nUQAZ}j3S);yAFXG-yTUg^DGGN2J>zfU5nMy_(mXGb zg&{N|I+z(a1%nH#_35fT44G{h7pMy)-1&C{Szt46^FQEm* zHNsWF2D(0@)xPqB>Z2r9rO+$tzV-g638W8e1x%jt=u(W8^_eJ;`vddENR$TG4Gn7_a<=`FrqCqa%)Lb=|GL%{64lESIZied3Mc|_;bN~-BPK5@z#CkuJ8`#WT^z+1Nm|K;s5 z2EW;>REehoT@w6)vEa71#m+3dS{?IpGV@Du$~g(AD&kxf~&|s;g%0 zkd8{dgZc27|kT(;%r8ol9B){mFasM8Tnu7agW z(@LWz+EUx`&w?mhK>}ETWt*1zyL6i`43Nx+__=RLsv{2`U%RG*L3#HCUi^gba&Eg8 z6K-o_q|jqqs|%?<$>&j77+@mvNNy%Lt?LHL~9h<-h=2ZhA=+snvT7EzhjP}8y&2l1?bLeQhR71mUcRn%!V344Z8h(PUv8--75rogiH z1C{5k$zKN}mp+>Ko4JM0Jtv7sIs+7o@cPCsTe+;0Lu5Avpf?d1$vA6HM}2Qw){b(G#rJ>|kL7oitZ(#)pp&H0K{#948}(N46gcjoa&8tz%)cXCOo&xYu| zv4?yxMBhA%(jHk0oR@fnrY`j38usxM9&__cSZ{5_dg2knM)+oQDmr_}?Wv>DR*chF z-Ium_mjT)E7 z*a)!3JHvfXC)|Q5Na8Uz;Dx_Mcva~RMix5;&R53y92>(63K-wa|;0Ew_Hr%x{ zV2f&;wdtojFu8#67N@NGPB?A8^?9bbZP^Jp06 z*a!P5$Skd!SD6Gkcz>7)XTA3;EP(~5y?mgOfQ)?Er)zTbQn)PH?M{W}BdtxT-M0Cg ziB;lUNM3MKxKBjPW(zR{Kz@$wdUBp84 zAU8r$jP*@fe0)M=Y{j(y{K3g^&yVLGl(LPlpz=&W&eyHm0=e8z6v|M!uWnowm|53` z;u@Km@I6!Wg>5V8UyPfbe2#s==U zJ8G2j4^2LoCJps+(mP$|vON4I1P^k7jFgO&N(z|P94t}Mzj9=gif#Gch2a8zlmM+KJ>aUC|a-;TB$`vYS+wplui4Zu7R;$4Of82A?9HLxP^VllJsgz>@)ah4lVhv#4VM@3c3g3Na?dKt(+vOVH zm#j92R}sdoL-mm}{hM4%gOG;RiG{V>$E11gv9me%D}Uwbdh9mzaVc?7$ws+msSc0< zD|j3c?2Pj*TQ+8BCzV>1{5EpxkxN!EY&!ZgSW#Sn_ZLmt7IV$#y(q{+K z?W6)o2xU`pU8ygVzFQQz@#KVuMv<9+;o+ZRI!+nyOfik{M+Ik+x?ZQ2LCp zM>4M&NsFhLjfnJi(|!RwwA6Bx45hYf@j`CKEH2WLn2KKrNt(J)(u@Xt&4c%zE+Zkt zjR>VAueWPqJYRoV)?!32!WCX)94Y7&N0ej)^TJT%)Gwnpcs~IgJB-={SeyP*zC5pI z%TZleeQs%5d?dL!V-=NzP{eFxXebaN#**p-px-|J8brN6ycWhl#+* zQz}c_Bf`pgYpwkK!dRU^y)NvcMpCwECgEv+O5VxATWyQcugb|PLqtR0k%MbpZe)GS zV2`kLGKJJ98pFI`iXL6!fYtFwj9 z?Ga`K015m9%^Z-lRb2g-)44KsHy}Gn;RMoXke6`v-{1UCv&@as98meF4JseO??L6` zcF$Shh`He($wqIi^7o2KI+MR~5clQg^uH=5_5OOwY02j>Dcn0YF3d=9G(qZ{gg1U> zUjq5(O_vK$pUgrDd_y$(n}jn~`kr+5-=|*mZjQDqV2pVC$(L)<8NmCSDmu6JTyULYmzmqBMTXCC#0DTQ?TEDv32@f!D6292AgUU`8fIEJZ&^^GK38P^?LJ+A zu-|NkERtPGqCruuVEfmbZbd@AFPK*Re|zgIP5v?`xYKtd??Z8RgI{*2_Pr8l=G>p> z7kb_e;}@k}_y~U)7_|M;FLVA#OFY#pCg;c_pFc)P){zUV=Rw^P7(0JW!+qG;)r9!^ z!v~P_l^OyP**mJk|Hs+4f93185XA5pJn(Zh0-P?zf&jtK>it=`cTT)0QHI5uxUjCe zFLa|8|0rN`oDk)KI>a|{$!`)154l|lu%9(!Is18q^Gw+y*30IUDC}+{Iu*XDk%y&J ze!$AN@KRH{wdtl}?a`(z8TXb!OZfWOc>bq_F{fd}pPIw=CHF4ac>g9OW{9^QiQo>g z`j^(&%tV;H*eHTPZ3cUI5Myrc5=8A128kYglQ?fAE0p5ZGo)&R?~BN*DsZU?oSfx% zh;_;*^jb%|2{aP&{zLm#yx=g0y*Dc?V!MHHBwF`QCnw+3C00|j-Osf+p_l)-~mG%+iazU z@yaI-#RMh~eR zU|&|ZaUd*DI7a{(#*yt--ra@Wb{bj4KfF2|jEP1;$(XX)s7^6g;?njq?pOfpf3|1C|8&?HJzwngy zUO41k7+r1KW}*J3m!L=VbU$1u&-m=w*SLipu*W^Dmq*TfudIs3S3MV~=H;Eo!W8s1 z>_q`CN#Sz!2R`+ZGcg-LXzo04(2UM`3laUG;4sr_@The_66V60Fdg1g&PtDy+X=2@ z3L+S`LAY~FC|QX)3$a*NEnRpaY=!2GR=q2)7boB3)40ETw=9eug0NS?r%E3XhY@FK zH#PM{#k63;EyRzOmnzZm?mX0WOGM?o#VaDqvDQIAA2U-b-7`uJWu9so`BJ2Im!&wF zyRF;L+wX<#28X0`Rl~zX&)jn)C3Ow+USpz-gL!}kOIYg0-^+jQL*{*sPH#EUwIZ@;P7dEx(B^rN{D&)fxtwUxNOpB4%fuCZ zeKrb~%PQF3b(v)a!4a^1?pcZz>;bNSw*|Ug0`0fZXZ`{-z{XEh_4-S)OdABpf8oY) zWR{B9Sy3#0JqtGJ^-R-{8`W)K7`1bJzVxSuqhc8-(W{Wtr3!CLxD?6q0m22^%w4bi zX)}cCch~3NdaC<5i?~|>O0UHSgz%wyJRNtYu{kpaGO4W)I&*6*KE}0H`DXiBEr@+f{nv!O!XAWst>z^$Iabm0iHbe;NvQq zCrNjJ(3ou`Wv7x(AL|o=?#L&imQj7Cpoj@ zSH#tn%zenoU%wu74`=a|(_=UVEG;1Ii`iGi<}OP8Df9-zBpBlZ>LJ36Zs63~)v;3u z%^HjW3pM4(fKlTZ{mJbHLx^De7^lNK;%1nP`s7bxi0vlv$noh##^iGEI69@XT_Jg_ zD?qnByQc$j!5?w@(O8UV+xnQ^GuQ*vvIdKm3f~UJSNRLKBm%@ipa3ll6t!Zkc!HC> zxa<`~I8F^z6e(JU@DS`Dzez|6?YO-hR%}HYOQIQ}Y{HaUXg}P{<2M^N&(NfLh|RZR z_+$HXY|ysc@$&^6bU=c;!W$m`$`xHT9KG)nbT+P-8HeOt*j z_e;BY%Ge=(tSB9hKx?zL=|<8R{}JrYK7p0EPjjn?h}pp1=*Csb5Nk!ic8RsaV44^Y z5`M=5LeWx(joRW3`pO8DHCCS@{2cnFEvkDfJJQZ4A$luDDoUyXhjJ3$C9wzGMH7ElsoD}*X!)^hC9$B7eQwA_p9k!ObK~!?nMGw81 zL*D)8SIm1K?eU3JH?ACVAumNZkVkh_wwTxd3XO4*{F~h7Wmu5FN8Ib{)j?2)h8WDP z#`V`}D(yQlF~He|kXWk7D@bhJ&BpISFS2miz|AG<8EP9Q{xS>f@#9cDa(2z!&^d0T zsE$TP^D+x7Zkwi^Yn~1Q8<&aq{K;Vy|B9&0(9(yi&F){e=AziBZiI?INceiTv5`rz z##I8HmR-87jT_w45%nXGr!;r;%c7H|>Nh`YS9#pP>#m(iuy*Ig6vC-HrZa$;OWrz! zRZMYxw8@p>fG)O`_SkJ}SRUjIGH+R@I=!n#tcuC1IHT3uk7CuNoJ$HRDK%Wk@`{$Mj!V~2y)#MI(0%sB z*Qkla?=}!TyBQRNIe}x#q_zgmXvDT_eD}||dHp3XSwj&g>!&9?D{4W6MI|$vsNy2- zV6Wi#H;J?Yx|59GU~3eK4rY?~LXPiLn3oK>Y0T^PxNufUDEE|q3(t4w;+_q0V2LPh zK7A!m=uQJZhHOI z!sJ=l_Jq&1N`K5;a&RW;@3R~%!nEEg7}Md|SR8i$x^Ms>UKY3NHt_GfKKo+pu4TQHA%pCsGkYbb(Yj=pr{%@++1$U4!zE_cd&l>xwVpD@w|ei z4ZIu%Vl!@r2$NoFb>zM>h~l?GCES#5!j8H+Dq&Ye!Tm6-1f(V~#vB=q6kf6n*nTI{ z>~u~5WWv5$>)E-(!K{(?;EqpVpW)N)6mao>luSYe;-C)xhRV&13#|#c}ef{7@>G5KPbJ^>)zKu zYpDX40#kgEWa4_8?USo*6G-2LGqJfrMloMgXR2yE6`$r_J#aiTxD9BQ9Tl8C*KmbR zk?%;eFK51EYi^zudbEnFgw|}qNszNJx*;|d)e7YW-qTP~Hy{Fg33u7ikD#}Gre9l2 zw8T}SwVXvDTm=E=EhO(4OSg$`tB2*C=125bF&7~;8<^K3jH91;y|BV0*q@!hw0E~Dsi;TIvlwL@ zCJKV3^dJ08(v6u_HP#srPb490ZL_sx)6o{UtDOmBPq^4+LELw=bW_3dMh@!w8g5H# z$d5QFOQT4hmL1g0eHArXz%Ld35BAH2#NPdcffPl;x zh7=+)iKu`?3B-wcNQn%JLc{` zhN3DbW+xOm`cQYvXhm>?Y|e($x9CMYPg2;6t8sq~DBPS!F1b@x$+d*hNv^~&(w~5r z3RW@Z)Tc+3gi88g)j9A)Y#2Mj`W574i2KPfCOw*Qf8oxKeG(al zOC;1=1Nd|&))G=@yHE%(JyH-7{3ytRiHuh{G$cHVNoArF&Ar%oXHIN=HG}*UG}F;p6>{9{-Xef5?4nOdwKPHDpC>U+Q;i&l!qBSg147(@x+7}%$u7N;|y=S&rhjdV;3wUM%G3b0@vKpSe@5uBHVG++{RB z)D|jUO>2uPquqXMld?3=x$Dtp#Ekw~VIZ8Lwb$HFjP~rgV%plfbT_j5SwE@CbRHeESCJ?UM7tF}j{%_*h{$ zIS7_Yu9;rWXxZHr&8)bv%2;llOc!nwj=mByIX7%0_r&WDFt>EmCPqjazK zh&sphrv4ZZN9NP@l%xhVX!A4;4^P>_0AYsC5 zJlbKRCrZNfyS#;HPblsI`#MH?=y7A;wxx(h}I?Fs>es_pOsyxB4A~}lQ9Q%Ge9dt-0!IE6=lL6-=D*8 z5?)#_QP^8MZa8MFA!YauU{aCy2nSF=o^)9h#1X`+fk*@3EqZkXf_XP^5QOWcvnF!J zn|H6}lmxuWi^10HQoDHRs7zYbkr|l6dZ`=tgHu%5xfdg#a_rPG7W1;)w?A(q9})7n zY4VLR-UQ^47=$ga?To`f;$6aKb+^!>RwP8sz;c$!#GjJ)8 zy+LW*Xj{^A?p^QMFQ=+kkuP8!a4BqvphT$2KgdB+ z!VY#(H`Nnc4#sugKqr|8b4*UCs0gZlrsD#;U8pyDs6GbA*n;VtZ1zN;AXE6$OW)wq zGPE~L1)_~DobbFGv(SlXQFFVg9cxKA;9V_7KEIrp&iWKV z-3V*8zsdOGo+|!LF)DC=h4c@bex|a z9w>hB#d-c?^NoqE%eygTE+1G91Z;Bm^LKMPKkoQDoaC*gh=Y!v;5fj)KBy|_vu0t5J zY*mNHe%6AgO^4k~{S^-tI~ZO+C#aocv^*CyB6eW|p?6bycUJh@2IAyH$v?Hg;s)T` z5awf;Q06iokh96H1?mF``OpSbWhsM)SH_qy(CKZ9*ilh#^ zs*xINIzoznVqkq#$4LA@9Td$>jY(DhVZl0c1sEq;%-}oj7HWBVe}N_84X88tLzERq zrrRM?mF=zz5iWwsm%?tU2VSdU=@HQiiZ(r`5pfQf{<3EM-Q39xM8Y1YNai;!zbg;x zjnhx9+WPhzY56~MDTROh@c0+^VzUfVjdg2_3EM4~FC{Oe5oBBT@#6&do3uGs$A-D< zFF0$s+R64FrMM618s949(ml>V+hm<1O&rw96+Q#BDm0q&B_10ST67k&rkHhC-mkjdAsU~LFJl|rj_wpz(H=E>?v!WLXJOt(r3M{GS5@jK9wj&5HklyJAfbd~s7`j&n zSVoC}+gLS5VXP0Qfl)X9Vbu}}ixlqZ4>MXiu;Fswz2T0_tAT^M2mcU>(GE`YOpEis z-F>-_lty6}V>_jrXZ^;7{ckzyLvu`)a0nUh1%DXfN_yukf4VYVt3StNVZ_WI*weVR z^$}C@bq?w!$%-mD4^u72rXm)rffS4iE67i`cHy9DFgW%w`*faw zTUc)Q;NnHTL`etHYlLZU?UT6>MVLXRkf-k7Xh;P+IPzXh$}*GCu4}?w=QySqEsr^# z;#iP@C{XZ6VW;n2K?Q^J2I~d|N5*aQ;-7fQ&ZuIOC@nez=E_go`mq)n^FE?&?2q+1 zMn9LC>99wBpE+Xsnsw+hX>tGX%S&#yzV8J|dUC=%QyCbiLkBQCIo>u)_*Em>-1q}i8MW}9y_MC#Jm zOvAlcrY%{vwrCBkr~5N}@JUUe;IRz>p?O!ly@CNwbBXwL?hg?d*y1kV^*z9mi!U7f z+O#dy;!7GABj=OSX%l5ioDV;!`-+@uyU&{`V|+K%4Fi9tAq-l!gRVlQ0$yG?^E#=stWIMoaX zp1YaCaQWad`>%!1Lh_VZ-e(t_GE1kWF%diAlTMwQeshDR>U!TI?z1yIG&{t**KgE3 zzIwWAbg+!&0$XC&X$+>7oeQSq<2frjuQ~q za|TPhK9F<{?8~-|(p)i{Fkw}VYSUD<3<4`<T&_6|i;K-r zT6U-}+EoMzHKC2F5IQP*f{2+g8(6wz*sOkxT~W-v7aI0FWOMNI$PL#Q4|NPgmtM%D z9^SpY{;X##M$x(4D&4_bC+pCw$$*TCc9njkGN&rXy=?tWYCvvZg33H2*qLG}am*#- z!i(1ztFNX5_JngYEbQI!dhJgjb-6?N9HlWxvJEMLNv2TfQ5r+s!{555hWS#!9=i>2 zm;)&&!6p;~N*CLjcJgfb7X(DU5l5;W-Vn~0HffLBm*bcM7W7HEIhEag$X`pbek8s! zpjkDm15=Y{!8MrHDEv>O^Ldw2qp%a(YKEFl8s;JAtQTF_*6p~O#Ki_SZVI1a1cQaN zXhXOsA&Txb&oIu*&MoZq?)J%~^$K5;frIO@2WT@vC+mqnROj@|u1el?E<9U9amwC{ zP~79>=6!tuctAe-X_j$$egA@M1ofOZF+PpxCFwF|^{6=U%HtO+?dKXfl217!DI)=D z3DexjW+Gm{c!?vQ)G^u@Z*Cz!fGKsECORChaF#iYd!2FMhz@)(%i~lUz_Nd@V=VKw z%po_Ah5w@<>Xt+%9QqD6z4(%VTvzK4{U?#qugK2<{kTO@CS0J$ZvQ7KRM2YvXIazP ztylj6ntSoTenJF$kKG`2Q0IcIx+b@myq+Zd!DB2^#KRO!K-Fu>Ktc0M3U2Mo)V#>- z?2{jw9Pv zE*p^(5vJ8E7wEd{M`Q33r!Pf9VKcP_tof9As*|g_yj5*zsnwpDM5OMt^t9u~Z={1E zE;GeW&l)cko0>&@CtSY{9*mNH4VXWy&hHzxF6YOgPJ}m_F?G*ei+%G2%Yn}3oV(wM zds5!^YCM7IKnzYa1t-!1+TWp1EG6X#XEvz~w-uAi6iuoHaH5x|FD7Bbid~r<@jOI! zOt+-8^z69ze0W&szP9_H#!vL?Cm%?^N_kHPDkfuBaDiR8yY6Eit)L|i&JW;_JBHD_ z`J(LQspg(mpQF?bR6T*cN+U}fSy(y`AuNED95Qk3lgeaU!4Jbl_Xu9R6Zd!cHPk&U z`gY;cm6R+^+nNZHis#DYWUih`vqndb*lKQbcbD8dg{~a^I{Ddow{#64cKEn|rtZZc z%h_qeUo*n4;4tay$}0Bi-B{KeXd9=2GFhufQPyLICq}}gp_2!VCY2y06(Po<`l_VT ze7AEZDM%$KkwK;+f5Z@lk}$~y#S>#Q_gHwb(9Kxtm_tQba7|0Wg%^W*y5|#^jscZv zCpCJj+_SW;rNpj=82wyQ=#0EjBy-B;8S%)N6E|yaa5^+kGMia_w`4HeR-ZyI)^sU# zz&5Rw4P=!L6(i2tny<}Uxqj)=w9eDZaYn^sCU3`?KtN`X)Df$GYJhJU7QwT?9>341 zN{2hw4*1=18TNO#q$q|LpEfU!|~?t<4dVb5o4iT8ZB>Yobj z4pkU_y!ufF8)X-+b>zkfN-8aiPR!ANlorsX;yF0;7?l>#=a5xq2ix(+-2Mgz%+-t{ zt-;jXjq5>OkD{aDQ43*JcaYM2B&Ul3YgL5oouvdp6~uynyCMHx_w;T9x!YwdtygB* znl?0krR!(bL9b^mULAJrMaH{9_}^va(i<~k3Aj)X+?7x25k2nH%__N z{Q7er-%v&RBW}X2im;-l!GjkUXWDcti0?nMlySGUUQ;Zm5G&OXXop?41ycKrpB-s#`h{wAFtDgb6#TU8}6CZv-MvQkH9($ zs6uP95U`L)>|z1&A9Jy>hOeM)Sh1EPS*&S2<}#ga_PFxn+H}|QakO**_vwHsD_VM5 z;kYBF;W?I|Q>;|=wbN;F1lLAj69n5@G;xd-2p}LmKj0x3Az%zVX#kED2(%&~15CO5 z_HpsTt?(6Pz(WA*HxPl+{q9Zy%cYTp7{EmMyYC3%S^puj1^;c^f58s`yJ15x7qtOf zNSk{XY6WQ)^Gj-+ELIVcD6fGMX^oS#$@Jvll3CD5ziqz$MRSvi>8nN#EV&9aG8Yd- z=9}0zJaQgduBsKN+gAf_U10)?hP(hDhjXt88j$DxflzA~P>EK}7B00Pw;R)CU%+zY`P4=`kNb|y8{eR%J_|8ZDJ>2e zPOOb3g3#Z=zuhs{@OJd;7wGz)iFw7!QZ+(ai{{*!iBME!ab-vtX?9Y&DWE9$+J;$W zRg}@t@_k_3c0Ap%ADMfp%yX&gDD;izIF6O#I8wlj0qkU54uP8p@=hgN4nAcKmw@~6 z7syRC$#6Mw5_ZX4mjq2RWt%IpVNk^s8Ri+J7+1rp+U8l2x8M>wmfe#~R7BvPQ9?`Y z{o_L_rPBOu-OAjeO#K|p`=^U-OZL?gl}(#7RMJEG?_Mu+Fr0QsH>)dl$XY9g1r>c` zZQJqM*LgZMXK-#6c}>5<`3%wigA-{<%X-X}v^%(C0t;hyAyqJd17tz#>6}$srd5|W zsRNeUq6BSYM=)`;W%-eXyxe$%6r@BY|73oW1ci*Xtx`=IP2wZV^Zn1%_k|hR+kP1P z;t~^jR#v*;>)3&NSvH2DIm(_XNDrz9g?GxGE0twB*OmA9^?6pe13F|+9ZRoHN&oV^ zXYTRCsU$6a%VO=7tM!=;MtS7s&;r7)azm%nPvUb7vn@sBbyDQFi?A<;b{Qhm5DLRW zL#!1f7wBKv&YVm@`~dlIe+iwP zscA?!vaFLg5tEY{)zFi@YHQ(-<7a)4kvWZ|JJ%slSA`qPS}r6wKd3ofv$Ys~x*i%`H^+kfh5BWKqYTG>5Xp73c>(oL;>`?})KJP{E+?Luat zX5p(mCX%lar6;riPHx&hTZK3*c&6ibgOJnl4Ez)N1lO~i1bRUV2g%Sna677qgnbim z;6iD$Tp%@16v|ZI0){g!s-RSz=nUG?2Z2LYT`%&>1{DXLnMd<);34-$QN9oY^%XH? z5Ki!C0AMdR40Z^FNlB0ZS(f5B^T_|cmj51?|L^C^79%pHdHdK5ebzhlzD540ibbc7 z3(*@tDJ*_fZEVoD)QLaumZvc;%B%a-D{8<(f0M!jqTRU<03a(VFIyQC`CX8K``m{N z!0}9A;JMg*-gFpk-76}${oxH@ut+mM`8>e~emvsP9g6T`%0|mRx8&B|`X4QoUM{&^ z{4oA7;q4>%s0pB>87vSW7*K2sd?q4%M+SqE-0Ypn0)TPXMJ zN?7fZ){3$0#NO4@4Z~(s{qpMGLuU>Wc28#QtvO%s5dW=l|Csww z7^XKC*L}e6ic_Q8&MRisN66K)83DF#{P6G2-q7DT04&U`ek*%i2}5&5TK`H%Uz zhw`7J=JVq2l}%EZJ^>}w165Uk+t75R&#<1}->vw2=@U)O*V`BMA4f}XA^vBL)8GBx z|L0$<=4ss!+@s|MLXD1n6n50MDLuX+(R@I6{X_Mxvc(;#!k&Dd69)IWa`=0U3(0L!c25@Rt(T72XK=~=;@dZ8t=POj$AbTuN&ojT zZtLHLCD4?2y7@j3OyaRr^YWH5eC0v-*@DR{QbcF zzY&Rt{`KQF1=qo<-l_mv{>ZW8rUwtaxi3`oKM{Pea?gw~Ks6Cx} zgywWmdu~oj0~ssbJ>zFzvZrYkOqdkL+O7$T|ad?tF*JSq0vH!KW3LSH(LAdVaxI$Xm<+G| zTKyd3Xd}oa2cbpy$QPWJYTg;3#(1EbBlp$}M};TdIB_jbn&(hR9PeUkmy;JCEPWn4 zX#M3v4R1f_*Z+m>(lKGkLBDm}5!daj2>GzLzTFgEBC~LcSPZli+@Z9Re`G5i-@*!FW*^yBB?(@AFOb$SItb9%NnG)RjkiBWvJeV#g}`D z=P)#EIu1Fv($dJ+or`yQy>F@8dp_i(D_CS9T9M`eg=d>x$~^QfN-=cnPHWij!r##~ zjtS&oN15*sE-qklggkZN4&YB55)S7P7O#V{85dMV15=QJFqp7Uf=v?cHK31@@S|FXX6$&irI6QpwtAG^dGy~fp_ZQvR-lT(G4&PStTTsFDkgz-_>e19KRoM z%)@U1FYHh{Aaf_LA3Ym6SJ!=fe%x|pE!;)HQpz$>#Vd(ASa1aaZ*wVCh&e^8*Y`|5 zSK;i;bt5HZ`R5L18HmNTJr0U7dy$}@L8`b!p2X_J*yeb=;80l?NFs6YvA!h)wuTsFggNumai&! zP=_9~YZI!{fu0$Wf-r;x4BW^ncN{R2VyIw~Z4tpNuY$ZLbner95zx-!dBwUlv$lD5 zYw@WDkH%}aSA(16Qjk+Q zsbkF{H=aF4ihHw;+>Z+lMeYGUALSgau6AEtXiMolDv@dzTuay!45}@QOKZzN;evu1 z?P-MiXtYF1JLkk)o9AV$v_WrKxKdKF?ZMiB#OYE8VuhE2*+>d(+*Y5U^ZC<} z&hQklzXRD*h8`Q{4*5M70)*!$p#C+=cj9RSGX3x(sF=wKHH6eLLJHDPFL^VOI+)FwAI`D%f!sJ4t(XSG7w^Y>k@iQxmF-P*P_62 zb+Ttw$v`L?bKG(;&o0TxMs{vQi_?LD#JP(=JTz;gqA1{w#kPS3EoTrLiBA_eIdVt*3cGdJNOp)#L9 z#$;;T&%P#i<1DaP&g2rPfNl!~q_17R8g=~$Er`Vkr0Uw81RQ;qz40cc-5KlD^n&9y zQ$h5FQWU>+w3FEm=nOc!J8m}wCkZ0m@mkPCp^KJV`f_v+z%Z?@EvH$SsN@!@{bL$V-9|gC_!Rb<7>=n!^SBJhv z7(0$kLB51%L@tk0yr2r;F_ngaF>>1`N5i@2*d9wTcBMtAJ=77)T0%N;+^d1;WYyh+ zwfX9lVHG9LiGrU2xlFb~Hm81LXyL+8(7D48KVR;jX>)%&eW-2B7+*)BQ--^Ptn{&i zbp3cB=V6^Y50#BndY>jH0`bf<>=&G#pS8xJLnyVT^KaW`YkA0^KGGf?1*->QtkOhPRFg5eV@F8$YSd@O){)5 zTg#HU->}BD!abYjhoYe^~kY%tB{kZ9BqqfM_efc+%WTzd$7) z84P<4lxd1jUs9D+yg|$JB!@g(L6%Nwbowr9M+S8w-r40v&D)79Si`jHib}giX-803 zC2+Mv^pWSE1KLIThPn?$xhE&35|daz*`8t0ujJfeKn#AuidV&?wx0 zey%d8bLYDl$4{$pKVe6U7?jMO1YJH-Q}C3r3yFo$MjKp$Z^{rvP=v~g3IpaQDi4En zYjL|2_z_{QlWLPLaPqQ zLO%A^eAj8edVKfcD`^dB-sf1#rvBCM67L54%{r&4IT$kJpLKfor=&SgXT2_qunE(Q zvH7&nt(T-x>HjN`k^RSR#}#XC9=%1Kf20mLKp8+gffB_0B;!j`*m`Q!ZR8JevV03D zn-fi&BVo2O1~-R#Zr0+GRLt3HDyqYUyWCzM2cS;ug|4&R#{Zs;GK43WLoRp~j>T{t=0SW>cuXTjDM+VIKTGeQI0Jc~1py~T^= z9~~0zGr1Y2D##EX5%!%4ZbYX##tSv$Itjo~3{=g**>cmP)cf4w)zBLs=o6d9bAUq2 z#yq{=m3s#j92~lI;1zo*zp`+u8PyX;HOFjXQmRRN^}ce%NJeJEfPh@FqZ4yu1+UXw zeB_+!$#a@3AE4{*aUC*U>nNUIxgD&>eecW1ewS?Yt`(c2WadbLUCy4@-8ab;g>x2Z z>gb(2?GzIEae$MZ%Nb@3qH9Tub@0?CInF@)gp44&$%3=M#n%8kCQ++eID1_$iTv_f zGf(>?M$RiQdH@6~iL;@;+v?#z$&LjZYSw-V@sXqE8eel-P{giqk+lT=o+_qtZv6Kp z?z;g?VC$yU8x|}DT??R0?s%(;Do8lzL25ST(T;`=w0GQ6Y8^XJjZArtV}K+r8F{hl z0MC5@+l8#h6z9?aM=gy!erMF}MM#TZN^9?CiPwW^sO(6HA@eC+@yOzCQJr+Nd($%+ z1__s40~9|_aCaB$=-ybLmLDwdzU94swNc@I8b`rcv#hSnO6Fro4{UvYwVan1(xNJ! zf3Es+h32ur%_Z}br)YNr7lUu?iKDh0Ezpy~+y`aZ+tJ>TI`_;YW>u>Nj%m%8I?ijy zy&TL-T!P0=5faEMAVL{1n>P{+acRRqKFB(%9J9dGd)th%bCknS9Gf|J_&dVv+V72q zxNn}0E-+vrp#vLB%4K}r!L@Kj;ro^Y5T0RGBOpY<#rcx#hIG(Xw47we&=bcvGEMl> zaB7?kq{)QEPu6n5ZfA#R2Yfe>Ub61!xrI{eNvz^qGJFnB*zeQzXG~_mo!N02!w6q3 zLTy5*js?=sy5I2y*(FKC#;Q;NArP*^%F1q5*_?*>tJ z+FtS0)OjNpKQl9vEml`;RGbEwstYlrb+%bGJ7)F(0b9i^;;u`wICs!TA`k;CCK!iI z=^4dyv1}N%LffjhqaBw;NW%IY>lh5`esiV?@*O?e3Y zw@)Y=#qMVdd7s8F3b@r}#^IF~5p9zzyFg2SHJ*mt%nglNxKPJP?l#b|*W}q^(oTll z%U!i{Q`6{zBBLEZ3IeL~zH@x7LsuRjdf@3mc==d!z!|TF*LL#2>x8(cUB5OBj2L~i z9GO=wnPl;g_A<@nWW$|b3Qi>K_=Rrx?LuC#=l&}v6}@FMt**JbIpwJT(6Sv&`9Llm zT4MG{^3OolQ9RvQat%Hq&J;so$UbCuG3D;Y>3rn>3MDhhcL~z#dhu`>p_*O?9ESO3 zNJubCqGJtM!F*u!0|2M&s2yv%7Dn(r!K$8lVQ~CzqK>QVFP#Nqk7J9iI9+RZFLy{c;aKGw>6{D((gL5jeQ1vi z_z10D#e3u#Q^v*`rehI=ID~7pY})oDgie476;Tkqi`h-;K;J~e>If42NYJwv48L>3 zfjk9ELf4Whuv_pw=6(PuhRa|@89pY=>4IDsRViHCgWXbGIcFS=qNhh)=cXzj??3us zfI?P-Rn<>3NVD4RjJE323YR_7#@ z15jw-6Mi(i7Ae^j%uqIQqVttXL-UM(n^3EMJyf;0`1RbYz?&tjyAVIP#`3MgQ!A{+ zm7bMlF|2i_(;Y%%_Il8(AEdY)TF*D@%}^m3$?F`{Dhijfs#L5*9Nc}mGk5@#>>`*? zBk8!zkhHw}DiXPdel82rvBj{MhxFQ~N!XLTiYR(muk@O?ooJaXW-=%`=+b!27>~Vt6N8V|5GeJYC4m&Gj%gt~V$i zs$0)`td~fhDkapAQW54DuPJUS=w3VByAHn#S8f4ZET$;6>K3jR*9uzSy!m1TM%~vM zMpdd%1vEEUTZhNDv0CF0VOOe}9IqEvH0N@~t;3_?Y34_b=!RO(rFT0yZXf`$4$1R@ z^jokZX0Jha*_kQ_{n|3C;$6EHk0jBYOA|^{NoKvIC5^uO<;QE)l3f`~`R49$QHRI6 zW|>r1Px(QThGpKBy3=ls{+h>JoWadIG2t15|2 zWF{tKuQ2clAm0#YZ?wXt&A0bdOqx*ONpfQ!USDTkK*J~{bH;s8IO_gc@VwYKQGK(C z^2cmLNXR!DodvLkdqgc9mE>HL0~dF8zFT~&V4qu=A4ld;-Jz(%#aDi}GTn~_E9fCp#9u^!F~N4!vgfundGUX@X;7< zBNWg4KI&Lr5g&nsyjqHJa!lW!%wG0-lS`Ot+ zaNr&p`W1@$SwTmJDjeL-ec!F=mjYo)+;#%6=`B%}ypcmZo*uGp8UzS?2Mw=8G8U_) zzj4+M!$eWQbHe6a@WA~Fu}GFyCSloM%#dY2$jt`k3=UCRwvvQ_mVE%pztAks08%ms zQCLgBdLXreK0}`=bYf#AX^OlP+MuyqIm7LGFkq;mM?u0c4g;-mQiGLEmyEi#nUj=c zop8gWVYZ!)knK}6@_oYm5iD|QnJ)w9UV!jZ$k7O@%E33B;kIQ(;UIjsP!eLTo=+?FNXIrvI2d>r^tS}W1L;whEK--EzN;qVwVT!Jd6p(a@cu+>)~Z`@ zkGQpxt?#(FGw4gw?C?byYBSgq1KGX~M%qk^$B}O?H@UL3JJwW|sV&L_U-#FNhu6Hc zU)5i)Z@5vMUPtvkr&Bs(X*mC`(ZtF-!;KPYg(M0AIy!*-riF)2V*zSx>6nOQN0;DM+yJ=z0J0V) z5O(9HmQVj7^0^GjILyWiBoKSS(l_xC_@%i5aM_;$-&DkcjpChV(2kr5K0`@$!{w*MP{&|sW!o=_-MtT+ioLbIUI;5+HBbqio%}oC51_ceca`HtV3f&CH1o#C zf7Sw_aF9T{=9ll%H6Rr&l9TyI>6&eRMZ|(!BX6 znH(4u$zcHK*RN+6Zr-lwpU&d<5%+sn(H|ls;Pp*3BjsQ2MjJ+fujpc8gl}&+3V-wJ z-9G&fKE!|ifXFvhMmYbmGX57+<87GtX6Saf1Vm!W zfYLJ7bV?Uz1t>_7%X>yYhWa8ou!N|ijx`wZR$s|iFJBK_jFX?QhObqL_2{2!2x~lA zC$aZpg_~^iw5}8&m;4smJ189f&RaXd13Ee?Fj?GcI+2HFp#@UkrD!nbXa5lSV4PnH z+ylvMxNyWH_RinEieJK| z-f1WBSwVky&ZnS)nTLj8KuAUIzvd~vt)d_k&KqZrUNYaVWMVc>AzWs@FBFxLOXR!M zzZ%HgREFF3(mj=Y1P_)EO4k#Jp4+#dEAD&fW)k*X*JBFSVIO{aFfz=MN^d}2s_?sB z5+d8dj7$9KRW-RGlm2z94*X^2JNcOF=Y4+f?0Tcs`LQnq*R97F{X?W)yrpA^EY@UD z0a>^H79`xKh+5ExQajAnKcjTlCyGnJz_y)b5hS6?iT*Jm&+cW;^1C|vljP5#p3yc^ zVfJrq)=;@`>fhGdTocpW1;lk;_lVF>Hp=Vk!OyQB*R zC0Yrx03NoS7-unT2)3@1b%ewg<+yH@gHUTZ_5ugi1|HMVE?W+XX0g?clbc2YdY(sH z;bOugAxLT6HTVv`83&%G7ge76%!rU6)S%bSVtvX_K3y1#tSI_8+j$sYcfY*b+#Xfj7XR|PjQy|@E_4O?Z6@$%`yf8KhY0J{!MsCHGX9A+z{nLYn1@961o+x?6BUmHF zGjEv%2t$9xRPpsrBUVzEpBQ3z+^LH#qvm_K)rCqc4$&{PUWlZtIMhGL%Q}3vAnbuF zWmV@x(K(`K+=@IZi)E*@)0LzcQm?Jxa0rxqvyFf_EoqlQ8qvWVmWrFoLQ4r%L(mH5 zz+YjELUG|cLVaP(cR*nkW5A6W1~L=$SWD^#*f(2D_av-`i+BtB@^Xr+_$nX~$33oJ+cNcQF_x0MVZWSo8(KH`XZXp}l4luQv zhDBUB??%-45txl=(j4tj0-7eiN~VglJlt8 znY@OVm9O-PPQPA$fHg)H$d-s}Q>U{+-<4&5J1m)fhDqQ=x2ou~n?DX|26L&5p*sd?9<7yn+KLxU+aka2T3a6_T zw?(M6@Q29m+Mc^`=^iTn)>YsI0;i%yFoE72(d4GB7~$SN(1-$e1F$L8ao2+HAx>+b z(GmW!D@8_C8S8ej;gq~SAFVK5x3Vu@5~xhBFVG6KJnNEMPw9dsefF5a>y)`te-#v! z>sVkI1*!{!vqPk$X$oc3k8|Wk|K^}}AJIC`cac1L*s1;3X_wIzns+8Ko|Np$vR;s$ zqjPa?*+HH7juvh35V=4DjzA;|ok-Y<3+{%!oq!3R-~yZNrgE3`qJbha4$o`=+ds@B zH_mVeJbA2#M&5}@ZAHuR#f$l-oDVf%k|{CmXPSdHx7R)WXc-+E{&Y?V=jr7ifbkEZ zm+no$GJJmuzm0~sNJF@{^>KRy&6%)dgn{7k*QPMo-CFe68-N*Wata0*xFmDAP@dCl zG$`aIZ?6mG7%$s!5(o!!pP)vAa?IlD7)QI(Yvv=8xt@!@vNAy(5;x0tjiuu*e8Ke5 ze&xKvng=yzmV#EJc&h7LS`o|lQM+r~jEsf=EuQ?AWx}nn93LO|*Bop+XR)EKIFfgz z%~H~x)KqqaPW7WBp8JFc@8`&4=wB+V>QovW!Qh;QI$G&x_@Fkyvkl6FFE;2=k)sC8 zN)mgaee#FOCVx&rI8YGEgS|zIj+8v)bex8Ip3a%!66!z#6W3E99W!p)Bziq6@Og+w zZvBl!Zm9LslDPW-0Iml9O0m4XpXSlERg~;Ef^2h1b|Rywz<`Zjq&x$VXICXgZVHXB zGPj=+z~AB&nvgt8To}ffLRFDe)Jqip5`@siB_XzPbgs#v67W-#V&3x}c0MoOtF4!L zHrD<;uM;P(?Bp^7;K{purLJer#3kjkF5CwPQJ4m&VN%NDYRP<=+hCj?e@)k0+TvjOfF?_`txm>qq-&>AmxObHeu11)f*|*4)9T;;Ibjp| zfBbLjV~!*LjxKJ|pB*hM9#e~q-0~h87M_@t1u0fd6>hFIcypx@iP-0Y*(gndHYYBYMzG-sY zV{8|J!4irM@j`@yKmss)IelUq8@64I-i6CP5be zt@_E~(vi1wDsJ6NemjK^gIbBzM8>zHd*AQkkBfNI1t=#^)VbS%>qecL$ zx(!r|k9~n~D$kIYddAfb61bIk3LywR7Q+f62qhUjt%EI*dPgvm(OeV${+}lWxd_K` z*jN)8xskP{rU=)1969yW>}y5wY+m%z5bxJ|U?ymca=ZNrsDR5Gc3FXDs&ln_N`l1+HIhGZYuu- z*pV6whv00t@X>*eT->g>!*QeNdeM0E2tU%?X1?%FIPVYp=;w!*WKD_pSm5PLNI5w3cf6fDAOC*k^X^%aF%i z!jCJ;`<+P9cnT6ZV{2(At3uU`BLlQIPBMQJBi8=wAnga%NXh>tC8MiyPP&f{+r`MH!)LGu1UnJ~DDz7Vj>!(k6y~$1Z02*HRrz$^} zviZgBsA_!vi!xdbnZY81Pj4c6#?`?>5W|WF3&G%6NO!qr*tad?p29J}E^&$Bg`HcB z8^hJR6Lu#u;&lSq7k*8;t&ytM)mm2P;_ zK_daDN`eYJ`z={MooT(t&sy*D^A-$|K-2rb_lzJ49|gg28(1b6fkFvKy~PB#8U_}K zXrY++A0pPHyz`*f%@KPEF#pGHBKudlFenY{!P_mQ#c2THIeZe|pXoSi$BiBaX2@`9 z41woQZQMwX!rw)}2BSU&Qc?94F>3VE%1&-XTj zUP>Xrvd1W(0;>TA>@IgsZ2X?+7rzZTdt457~Tg5wP41(vv%(FySfUzvc``hB|vvu*(cClz5K( zw(lnF&)@R=I{zeNl_{a`S2;T)zZQE{uwzi#^N zO_A2~@t_~beDwzSni>XV+M;L)0e3ES2zGvt{H?p#V)Qa^lcm6Y;?Lf$V@x4AiS zw$7Y8#h=|;(P!ip8)MZU)n%A>)IRX^i=i<8*RFO+OY2IWRX^MGYbv|@Nq5R?zf2Gf zJv0LfmH&{>SN;iHj}*Zi6`q8P<3?UT#m&8>H=6w$99REA^ZAQ82s1x-p@HTz2nnUv zl6{m#A`qZ_c+aGr%+*<#+ze@~(tWb~DA$~$a!U@#wCS`X8j8GnTTM@HXrDsy(8+z*mXF*2h^S-Z%-7psoHk$qlJ z&HPbiN6+G2;$uP355Gib9BbWsTa1GqNL-8nfjW!baTAm*fk7%F|;4 zp3{Q|Jtz2{166&s%c99@pBm4uB|E7O6?KxiQAH+V=U-*oiU=))KaTtZr9i?8{I~mvM9TSn3D%J0}1WJiceq zPPE-o6xkk%91TP0ce76gk@r~RJC-!J4H9Zk4wBr$w^zjnyuwcfjItN3Rcbdg>uS7e zm9klG$pbpHA=2_NW!0UFTV2V4?@Xg!6pXvHJGBlCmBuY^L_Zru6)s;mqGjOy;6^5? z+vSqOMc>_Nf+Ydp=;Gjd0sn1d3o2gkmmi9%DP93}e}*DB2cFkidSY=SFW<-NpJd z2|LHex}RaLvCzOX9Z$e4Zl3p$D$e2>Kk`p}S%qvZBMq&#{RcX z#7pOM;PdNcF4=mFcY~$Qc4MxkrdJtd;BgH=-H%(}dr-yv4-x5He>;;kS{s4e3}no(r^(#hAIpzQe%XU5X;d z|Hd~9M#wKjU{oyB7HdDbvpVpV!HN#m62+ey2F|wvbo_@7ydOeh`Q>BnjGWAG+i)!?hYt$jb_8M!;B}Fw4DdIfd zt15bfUwNmU_-(-lIT_R*uIs+li`!MQ{A6_86x>okU3|n5zV5BJKj8g*Y~U8&5{l&f z_=@M8Ci61eu*tGG7%q)H9Kftfo!H4XoY;~m+?mZzMYB877ZI{pi(cK@{E%^GZd(-$ zYei<)lNp4amfWyF>*|(Qb+T*&<3yEmqvqn~B9B$;E8`t(kGG<%z?*A&$%GoPBoekk z2ld8t86LOD0e*!T(bnD(Km*FJ6W8059>?+-)b0BWF&Nv~f?Ue@H}hl94-n1sGRXTS zVjI7P1_>1BCA)*4S?8sD`vg12opsBV1-7U9IV({woIBK2!nrA~YR;eq)0L!!*dKyE z&~D@EC}}6^IKta3JIr{li|S7=CJk}WE?08aLOr-4{Z!X9zIbb1_|2AmK5<Ve)v4%16?1oDLwKR|(ua-h&H;T*Gcr+}Ht8 zkgjAaDpDf|q6mo%P!N#bVIv|XLPS6Ugv3UXmMwJ)Lez+%BVCF_AcQT5bdXS!2uhPg zAbgnOef`h5cjo^8%$aj%&b>2d=FTvgp-l4e_IIuItffxWBaZl(+KxVLtSw}Q$P|Fx zVRjt%>16x2H%9-!yg!&7w9N&9jnK0mT6WWw>!fi0X}h*L>s_YW`D+V}s0nIE*Xe0) zV|aF5D-F%Et|k0%B>aA9{B(}z40w3||Mn{KKlhEOz#lpD%(6+iYnHdYeuLrz8w(TS zw;G9~mKH?*Um-5B;PSUkJDnv7Zo)pjglR>@PT>GdhT?}mfCZksoNCsdlu-6Km`2s3 ziEAqdP@dbNpbAXxIj}+z^i~KIQKP^zM)+_TM$x42Q+(m&dMQ%iSu6z(G&l@@hfLW5 zg;2AsWmB7s7b`TI*&_MI(7~ssw%j{pyR4A4AGeGT+ziT^;q3iNN-48St_|pLOe6Qo zwoD>|qp%ZwUz1~Asb9>InhA%|%e%QJ2fY|89q|3aQCMAY0Ry??8&OGkmLD=?q{P#p zF=_;U2I7f1Zzb5FFnJ=2NH`)9wxdBHN1=ADM>&1+MY(}Ad*!5Qh zJqFft7e$vH5K);*RC9+)`x%nLDA|-e?7!;x-kXCsW+!f{p0zt;f#>dcEKhNukj2Yf zPo~H`?MgXg7te3EBgc}?yzR<^m0kPU6KS4rSSR<*`tt_&1vpX%c!>d&KzhwfdSa0x z88-bGr_>Bd>d!rV^=%^u-VT$;{fn>8rUh;9TA+_xr&^3>jS5v3M+Z(lAI(;$zo=cM zk4#O;Q*^*`DmlKw!pHKr0GE6Ub>#qV8d+Nb`ac1L{O!HSxjD!rE`_jpGbh-TWhYKA z6Zsx5!iTZrmnvcaTK<836PAn<8Lki>=9{NG4$C=mvIyPp$pjGRS0LY2fb?E>aRka9_CW2Kn81^nr$>0!3^~3mntK^) z;$3vbb&vSju?aM#9y1z3(s~zgZ2G!{FM;w$nJevhNhsb6loX&{p=kCfm&n(TUX;h#cqD^yq?PXYTABbx?GtaFm{+AEa6eHQ)+Z0;~_=OJm1)D+4%l;-MK zL7s8}?0vv|mSa?A>})6TcQwvf${F6RgGL8*`k(DDP|OIt-cq6LBol~l%FwYba_Mhx zOCDAjNk!W?-cSZ^G_S=LQH|a+eM#kph2YimIhXvYakXBsDEB+&GgJjF@eXeB+ zjwI99f7n-M0^yJp*ex8ju^>6JjI>fV_Y6l_EQxcqQ8*K(`zxU&4n6M8fD%8hy zfg+v?YQ@7A6}DyXW1*&YHk)Tg?1b$sKh}tsS)AHFtjA?*0#BtC#HHMA@4e$nKWVsI%Rd}@e08N8K2~goRrH0$sxMw zb6njrHGArek8Q2ueji72H5sIsn3fiqk_T}{g17vW9D;Bcpn|vqf^X z@cq=$2l#m5DRABQVoJ8}G_rXSn3e9~uUL`L3}=1_>y?M@{DjraZ_vNSub*DFUA1Z% zT5HOUF8WCrcM|S4;TZ~BPa(ya&~z!?05oQ2#RPP*ZH0s6-hoU)i~NNW1Tb>gPciXA zDGL1|D*}8cuHQmbO$-wWgTlwcyIFX4QxRbO4 z%u54?Qw#@^{b|m`8%2V^g45>vZ?Ee}#qOFCBc33|mRv|DpRuEv7o0JFR-nKbfo$4J>& zHuHYLKVAtbF;@2D`aE|+PAtMBR3Q-h#SM1QKjjvm=cKn9VmB03AFbc$57%h$8KqP$ z+rW=-rjxzcPBC64)sQJ=JJ@zA=w{ST;DLycTA=L0f^=o{ShYC_nONJ}6bz&r#!BZko7Ye6%O z1fX>CpB|TEz}ks)B?tC&s{ovF6W1#qP~?S%y)Uqu18XaL_~!{7k4xq$I}nOpMP(k4 z`gHrxjQsr#*3cFBx*xg*SL(P&<1+)LhDsnSbP?b6UTLDs=*@o3IHPfAFw&2#;^JR871$5Ki2Oxx3ntqyLEY9wy;>r2=bAhSJS)ZDD@W>go%p#&3i_ zNlr`=e}-xJlrJZ3`>a9L>e;KjieTsZQR|k)#%RTXS#E~lRV3qM0}1C=vQRNN1XXQx z!lc&WV-XBhnpkg&Veca&lL$7xJ!Bi~YSOo~wtQj1(gD|7;@pXTxVD3O2747?(lvQ_52!%(eOQ0ZxbAHU*kbtT&4*!w0freHd+CC#p^L_C6Y!h*rpeX5`x)W~qwO;6 zax;9?2TIJcr$#d68OZsif`lSlel^J|d64Ysa2ssKwy)Kt2^v;9wt|mp_RuIt!$Jlf zJx0@08w|Nac73h#CD%HUa`5>mR*VTx%tk8ncg-lFgsVRu0-Kj{xcq|_K+}1Ka!Sc) zAj0(`?R^!@bxHd-{kx&G+4Lf{=qjD$jAGSY=3%~OlE=G7EW6UUsZPnh%N|w%_5{s< zHg?MClFP?SE&urM@onnD%wHmhEDeN|CE%d)(okf%dA9;1 zC}MoG6090@=;b%Vi1xEdF&8?WA+Oi9l*dNrj}>ub>6e#GQ*D~7#f#IE@(e$xx|q=Q ze0e#ap6Ter6+rS+X^~xC;GNeqv#|0|MR3lTQc{!!Javjd-|n3sj& zP!m|>d<-k@@zp}uy))!x12%<5Cv<@IoqMVgnZ%c7n^6@YW!2N2bldzx<_J1rc)o*Yo<{ZdPWTy&hSba+xm~r?FcUnb1bRtf?+(^Z zKUPE%kiKPNS0G6^ts+hqzblL&0=8P5Dkrr%hVyg!y}^`XJ$t%?5Sv{W>W?rOc|GF9 z+!mYv7+ttvGyQpt|64oR>LG+daB7Qj14ov~v19ed3DvM68Mu#h)i%5YUvseeN)X$? zLXZv)jfCW=p4oVyXTXfMDUzvqk>S><1A($F`}%niQE$ttv z?jO3D@$Orf?&v1zNP*mxuDQj{a^*b$+B9pM=4$&Bf8MIC_=KmY12%2P5}C8g*s-+9 z^!iNZwn8aP7+r-{4~PS2t;AGJF|uL$XFwf8GH&%yxEeyNS!*!2_|}zr%;Bb#KJ(wDu1v2 zdKNCVGIfLw&mAMpHZysS%mEf}2bEs4w`RQ$;xVn`aY|+kHQ+FZ;n{d8Leu9UJc%H} zSA$3#R}154YT2E}@LwWH*CCIYW$$LunS`EF3SIfwXg1JYr74~o)<@~*NUJ)S2kI9N zkX`jzk2B>5iKl&Wi?7?F%?nLMF3}yt+oMeLZRRpiX9l@;?X>D5 zx#J%y{(8M&Dn(WsEQ3#eu=&|sVzz%1QKCLIkd2Hnxp8v!#-y*SwO<+j0wBx#O3>?&irkag1l_A{+8X}@=p!%x0D7-O<~ajl94o`lIe!u8_+Mg zl&3v5s#mfrBt9oa^4(t?T|UP@c%+mfF* z_$q{&%jiVbZI@9w4y=YT$=L_x6zo&{X?kRJn-u@cm?8G*CBz>jZl;djq{(r<&TQN4 z+Yffx23l5_{+>5?n!VMr14Z&fcV74E3hME}usHl1Li?nlCyu_8f;7J_KrT59UY1~I-%WjAO#%~66|^E zM2}|l4Zrh-&@E3jxSxesVwU98RDrYi?az&=M7TkNdHzWW1;hMLws{tIDVJ%lUOFWg zI=0!Q&1IdGSbxQ;QS089(AH|j#jXSA`#tKrLu|-VX%90`cTMhp^nXU8{zt&&|L|qU z876UayDQKubVdcR{dSdu{FvGm^t?Yu)Uf=5$qEFt^rX78~nd(fb}Zpdw}8yceGUNU#58Z?$t;tVQv;=G0#HjA)UteE`reAELe-9mM;6 zK7x4-oMA!^R90S531ib64WH>YKFR#c?BY3Tmv59U`eF%Xn%+=kWcbOgadbLtPfv(O z{vRFUfv%?9KN_E~s~MFdgq@h%4|I`Cvw|>VK z7xuL95zu1-4{&q9uSNl7iX#?~rZ147nnq=SvZ+jf9Ejk~^357l;Otzh5Sjr6&j0^T z|1<5h6&*V*aZMHtI${d|lf37#_%HA7NX=8Km!ypB@NI8Xoj<>N(ufK2IB2Edak=Y_ z%8Tc_O4q#Z<-}4Qvrh1hnj4=!xEK^98GrL_X785kar#;+(OWz&LoV#k_skv{oj$Js zHQMf z_bscW0OR+5nQ75tT>f1$qAJb1f*D5CKUlyyI8x&+qO&<^C!P+JH4x7CgJk6ostB1sZT zk{)OCPHTDqArLJq3o9wiJ!bGo~T7)k(9>;Wg>?_Sj_*jwcPY{;Skg%tL!% zC6WDJ5uM220_wQ|(k;iHc4H&*7+!P(hUBC5lu);Rgc9EoTms09?-fM>u2(~$k`8uV z!OWyZ8%`w}F6-r7SI>2dlgHKrNwjefxl06 zi4j_YfwdBAJ0I78K!-aJoH5Ai9cy>5M@ojnm7u6;huR_ZU(Fm^rv7Y4c`i%LL$lp` z52B+h&($HYt2t1BLpIOme))LhT`e-oke0cNb>g4k-~gNUIhjr0y@`wc0`-k9uE(_ ztbxd!g7r1d=BcXlenBMIQZ{=IIkqu_UYckeO(EQG5%Cm$ikR9nY(;v3Z*wZLdHnof`atc@;lYMkw=w2}s9H_21Q5x@D>T z2)%KqNb^Wg56(69Rm+=(iX)_@N#$ny6a6n@$&Vb4+U}ocsjXVB+NX7z?kiI>r+>Jk zy337uXU^6LeGT+ZC|_r2kY(_wyag`o>*xOgO~X2sh|;+39Iq6kLk#d2QC^Sn0TQYm zbTrLt|F=%w~881)qaI0+k~UN8VIX2LV~m&j12ke8prmguJX`y?0?e z$2p;|kp10u4unIkl-Ly|7r9B>^;JAB^DHp0CTiqsNGHw`n&i6`mX0QpyWWl2l_j*d z%g*YnvrA{K@&Ykd?s|9ht)Zh4ZR?GYRyiLbc#aI9Jj^$ML}$G2=Ziq$td)3r=X>5V zZuANy%5GwTy3$>fi-t~A%6;X-ee1l%mEFQV;pw33XUmi=lyaLSbWv_+k1u~e)Ko9Y zm#Uu$PFKz?#av`YbXIJ|+Om0&m^+)?DIrPq+7k|c+#(BQ&n$;><3{=CIT6-g3Vh9B zEV3Bt?WiZ#8qogbJNT6N8xmrJf%_fT*q}YfvbMH4gv+XY`z+>`=izYUI2Gx{RP~du zNeVWF#fAg12w#BAIg(Y1%w!H+I;x2K)RlMj5-POqM0^$DaHc%KFl?VyEG_n1WLg!! z6I2zb*Tj^mntOh#VR1~8iv-5Zim{|~oZh7-*7TSU4JnN{fgxQ2&nT&kk)#2ig!9ZG zNmr<~5Z~5G05km&fy(_^M`pN$8J~1^E4$0JJVhHnm((70=>&bg(eojSiFM+npa|BDzFa1}tvg7yb-L2@ zDTA;V(x58FKf+4KU;u_3?r|M(FRf8A)A8#;iVh8^eAh1h5~hZUk&*fjo?#f-HmAKa zC!|bKUj<$YFJ%OdydTLlA68v=tT!J)(B&{0c$=Uf zi0fVWCDK@Cm9LxZ-$&ZXRHJF@wpEnmKSA+;u~t2skk~9|kciMg8G0qRAh6crzVKh19B_yqWQ=xBgJ9FjejJjo0L`+$ z^V)JYsFIK>`5qJK{`d5M~2Xl9S7 zwI$9bbufN3Ydd9s7_`#cygl^h`^&i$^AIl76#@|38k?f~wC2EO@j|<_ii9%TN9yMF z@0L%Xm7Ek($!ca($(2?iWcc;M5p0d$0S6h=0wbsS4X`@;P~uu72{T>Jn1`M9DQqOG zvlICFv<)m6X2&aA+R=WAz~X7GEMp|~L2uieveCGp-cEx2!Bb);RgLjHq$SUCb|&tN zXN(VsL7Dc;#J^9Dn%=ZXcFi^-t7j?(+GfMw^|;msT=_xMY03-0s+nbH9^cUDC@=kv z)+Zh~`mVO)y|NAv^fJ)!E&viQG<9cmz85O?yn_0f7M(sE*yBOv%`Edr)Zg@N#bJl-6J~>SZtK8)X+c8I@xl zU$$n6@VICWh?|3E*2CB1uURZ#pI(-|b0wg3Atl1^X!G4D%NAnug4L@6TU)_m*)q%c z%nM{`jQ5?5+3fEOwNuJ@1( zfB)d@HzVCCSk16^ZR$bWDNe-qpOQQcdl32%1US4c6L?PJfx-C0WO?4YYE0xiZkg3G z<<7I~BulJgGg#ct7@%(ZU=VLGY7#&p!8hyfAQ~|Ue7lsN2NS$Ax0%+ z*Oh-MqHLj?EHY`3fXhOIO(x;Xg~LS-{t+aJ8xgq;P`Nv3EQ( z;UI21|!?NI9nrl*3dw#&NF=0pOj)! zMw@$KH|CJ9w0D3TUi8U+(R%^N15h>@Uhop=`mumo@xK#nbJW zzeH$fwKQ5i)uB81^=K)+`mkp3Hev@M4hO4#6UU_a^>56xWQBEx&sKy_wz=pNj2DJR?@6<`( z^CG5X3KLB;v~8GpjMemC9m+I3ZKTOwKLHsU5Nl!$9n_fWX8V?p zrv9Xzl}^mkb}q*3{;U;XUt(S>ZeIK~vAtW)#Xc?RPKS?*qrQ$E0bZ479vGInTbI19 zuE(zN?-W~)WENdV+Gv)}3@$BeYB`$JRn|TDG6H0pDQmfY!U%|74-;Z3OYZ&1wK0@e)-?MXd@P+6Pg@y3E8KO6 z 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 Date: Fri, 16 Dec 2022 10:41:58 -0700 Subject: [PATCH 325/594] MP: Sync clock only on server heartbeat. Prioritize HB sending. --- Firmware/LoRaSerial_Firmware/States.ino | 167 ++++++++++++------------ 1 file changed, 87 insertions(+), 80 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 82d307be..efbf6668 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -18,7 +18,7 @@ { \ /*Compute the frequency correction*/ \ frequencyCorrection += radio.getFrequencyError() / 1000000.0; \ - \ + \ /*Send the ACK to the remote system*/ \ triggerEvent(trigger); \ if (xmitDatagramP2PAck() == true) \ @@ -33,7 +33,7 @@ { \ /*Start the ACK timer*/ \ ackTimer = datagramTimer; \ - \ + \ /*Since ackTimer is off when equal to zero, force it to a non-zero value*/ \ if (!ackTimer) \ ackTimer = 1; \ @@ -1282,8 +1282,11 @@ void updateRadioState() break; case DATAGRAM_HEARTBEAT: - //Received data or heartbeat. Sync clock, do not ack. - syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + //Received heartbeat - do not ack. + + //Sync clock if server sent the heartbeat + if (settings.server == false) + syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -1293,8 +1296,11 @@ void updateRadioState() break; case DATAGRAM_DATA: - //Received data or heartbeat. Sync clock, do not ack. - syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + //Received data - do not ack. + + //Sync clock if server sent the datagram + if (settings.server == false) + syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer @@ -1316,20 +1322,10 @@ void updateRadioState() { heartbeatTimeout = ((millis() - heartbeatTimer) > heartbeatRandomTime); - //Check for time to send serial data - if (availableRadioTXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) - { - triggerEvent(TRIGGER_MP_DATA_PACKET); - if (xmitDatagramMpData() == true) - { - setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer - changeState(RADIO_MP_WAIT_TX_DONE); - } - } - //Only the server transmits heartbeats - else if (settings.server == true) + if (settings.server == true) { + if (heartbeatTimeout) { triggerEvent(TRIGGER_HEARTBEAT); @@ -1341,6 +1337,17 @@ void updateRadioState() } } + //Check for time to send serial data + else if (availableRadioTXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) + { + triggerEvent(TRIGGER_MP_DATA_PACKET); + if (xmitDatagramMpData() == true) + { + setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer + changeState(RADIO_MP_WAIT_TX_DONE); + } + } + //If the client hasn't received a packet in too long, return to scanning else if (settings.server == false) { @@ -1997,72 +2004,72 @@ void updateRadioState() } } -/* - //---------- - //Priority 4: Walk through the 3-way handshake - //---------- - else if (vcConnecting) - { - for (index = 0; index < MAX_VC; index++) - { - if (receiveInProcess()) - break; - - //Determine the first VC that is walking through connections - if (vcConnecting & (1 << index)) - { - //Determine if PING needs to be sent - if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) - { - //Send the PING datagram, first part of the 3-way handshake - if (xmitVcPing(index)) - { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - - //The ACK1 is handled with the receive code - //Check for a timeout waiting for the ACK1 - else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) - { - if ((currentMillis - virtualCircuitList[index].lastPingMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + /* + //---------- + //Priority 4: Walk through the 3-way handshake + //---------- + else if (vcConnecting) { - //Retransmit the PING - if (xmitVcPing(index)) + for (index = 0; index < MAX_VC; index++) { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); + if (receiveInProcess()) + break; + + //Determine the first VC that is walking through connections + if (vcConnecting & (1 << index)) + { + //Determine if PING needs to be sent + if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) + { + //Send the PING datagram, first part of the 3-way handshake + if (xmitVcPing(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + } + } + + //The ACK1 is handled with the receive code + //Check for a timeout waiting for the ACK1 + else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) + { + if ((currentMillis - virtualCircuitList[index].lastPingMillis) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + { + //Retransmit the PING + if (xmitVcPing(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + } + } + } + + //The ACK2 is handled with the receive code + //Check for a timeout waiting for the ACK2 + else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) + { + if ((currentMillis - virtualCircuitList[index].lastPingMillis) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + { + //Retransmit the ACK1 + if (xmitVcAck1(index)) + { + vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); + virtualCircuitList[index].lastPingMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + } + } + } + + //Work on only one connection at a time + break; + } } } - } - - //The ACK2 is handled with the receive code - //Check for a timeout waiting for the ACK2 - else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) - { - if ((currentMillis - virtualCircuitList[index].lastPingMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) - { - //Retransmit the ACK1 - if (xmitVcAck1(index)) - { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - } - - //Work on only one connection at a time - break; - } - } - } -*/ + */ //---------- //Lowest Priority: Check for data to send From f9d47087b53468b23b45b255c12b77aea8448ed4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 11:05:56 -1000 Subject: [PATCH 326/594] Fix the upper-casing of the command --- Firmware/LoRaSerial_Firmware/Commands.ino | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 7d7aaec6..10cd1244 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -686,18 +686,11 @@ void checkCommand() //Verify the command length success = false; - commandString = trimCommand(); //Remove any leading whitespace - - //Remove trailing CR and LF - while ((commandLength > 0) && ((commandString[commandLength - 1] == '\r') - || (commandString[commandLength - 1] == '\n'))) - commandLength -= 1; + commandString = trimCommand(); //Remove any leading or trailing whitespace //Upper case the command for (index = 0; index < commandLength; index++) - commandBuffer[index] = toupper(commandBuffer[index]); - - commandString[commandLength] = '\0'; //Terminate buffer + commandString[index] = toupper(commandString[index]); //Verify the command length if (commandLength >= 2) @@ -739,11 +732,21 @@ char * trimCommand() { char * commandString = commandBuffer; + //Remove the leading white space while (isspace(*commandString)) { commandString++; - commandLength--; + --commandLength; } + + //Zero terminate the string + commandString[commandLength] = 0; + + //Remove the trailing white space + while (commandLength && isspace(commandString[commandLength -1])) + commandString[--commandLength] = 0; + + //Return the remainder as the command return commandString; } From 0e605b69c3271b01341af478bf601731933c82d8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 11:11:20 -1000 Subject: [PATCH 327/594] ATT: Copy tempSettings to settings before starting training --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 10cd1244..cf469cc3 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -168,6 +168,7 @@ bool commandAT(const char * commandString) return true; case ('T'): //ATT - Enter training mode + settings = tempSettings; //Apply user's modifications selectTraining(); return true; From 8b4c0c3705ede0bff58ba439b9dd67912852194b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 14 Dec 2022 10:17:22 -1000 Subject: [PATCH 328/594] P2P: Simplify the state machine This commit makes the following changes: * Reduces the number of states in the P2P link-up state machine * Prioritizes transmit operations * Heartbeats * ACKs * Send remote commands and responses * Send data * Link kept alive even when maxResends = 0 --- Firmware/LoRaSerial_Firmware/States.ino | 488 +++++++++--------------- Firmware/LoRaSerial_Firmware/System.ino | 17 +- Firmware/LoRaSerial_Firmware/settings.h | 34 +- 3 files changed, 189 insertions(+), 350 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index efbf6668..d2b838b0 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1,5 +1,6 @@ #define SAVE_TX_BUFFER() \ { \ + petWDT(); \ memcpy(rexmtBuffer, outgoingPacket, MAX_PACKET_SIZE); \ rexmtControl = txControl; \ rexmtLength = txDatagramSize; \ @@ -8,6 +9,7 @@ #define RESTORE_TX_BUFFER() \ { \ + petWDT(); \ memcpy(outgoingPacket, rexmtBuffer, MAX_PACKET_SIZE); \ txControl = rexmtControl; \ txDatagramSize = rexmtLength; \ @@ -25,7 +27,7 @@ { \ /*We ack'd the packet so be responsible for sending the next heartbeat*/ \ setHeartbeatShort(); \ - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); \ + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); \ } \ } @@ -563,6 +565,33 @@ void updateRadioState() HEARTBEAT 1 ----> Update timestampOffset */ + //==================== + //Wait for the data transmission to complete + //==================== + case RADIO_P2P_LINK_UP_WAIT_TX_DONE: + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + + if (transactionComplete) + { + transactionComplete = false; //Reset ISR flag + + //Determine the next packet size for SF6 + if (ackTimer) + { + //Waiting for an ACK + triggerEvent(TRIGGER_LINK_WAIT_FOR_ACK); + sf6ExpectedSize = headerBytes + CHANNEL_TIMER_BYTES + trailerBytes; + } + else + sf6ExpectedSize = MAX_PACKET_SIZE; + + //Receive the next packet + returnToReceiving(); + changeState(RADIO_P2P_LINK_UP); + } + break; + //==================== //Wait for the next operation (listed in priority order): // * Frame received @@ -613,17 +642,6 @@ void updateRadioState() breakLink(); break; - case DATAGRAM_DUPLICATE: - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); - if (xmitDatagramP2PAck() == true) //Transmit ACK - { - setHeartbeatShort(); //We ack'd the packet (again) so be responsible for sending the next heartbeat - changeState(RADIO_P2P_LINK_UP_WAIT_ACK_DONE); - } - break; - case DATAGRAM_HEARTBEAT: //Received heartbeat while link was idle. Send ack to sync clocks. //Adjust the timestamp offset @@ -646,6 +664,28 @@ void updateRadioState() P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_DATA); break; + case DATAGRAM_DUPLICATE: + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); + if (xmitDatagramP2PAck() == true) //Transmit ACK + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + setHeartbeatShort(); //We ack'd the packet (again) so be responsible for sending the next heartbeat + break; + + case DATAGRAM_DATA_ACK: + //The datagram we are expecting + syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + + triggerEvent(TRIGGER_LINK_ACK_RECEIVED); + + //Stop the ACK timer + STOP_ACK_TIMER(); + + setHeartbeatLong(); //Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + break; + case DATAGRAM_REMOTE_COMMAND: //Determine the number of bytes received length = 0; @@ -677,356 +717,176 @@ void updateRadioState() } } - //If the radio is available, send any data in the serial buffer over the radio else if (receiveInProcess() == false) { + //---------- + //Priority 1: Transmit a HEARTBEAT if necessary + //---------- heartbeatTimeout = ((millis() - heartbeatTimer) > heartbeatRandomTime); + if (heartbeatTimeout) + { + triggerEvent(TRIGGER_HEARTBEAT); - //If we have data, try to send it out - if (availableRadioTXBytes()) + if (xmitDatagramP2PHeartbeat() == true) + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + + //Save the message for retransmission + SAVE_TX_BUFFER(); + setHeartbeatLong(); //We're sending a heartbeat, so don't be the first to send next heartbeat + + START_ACK_TIMER(); + } + + //---------- + //Priority 2: Wait for an outstanding ACK until it is received, don't + //transmit any other data + //---------- + + //An ACK is expected when the ACK timer running + else if (ackTimer) { - //Check if we are yielding for 2-way comm - if (requestYield == false || - (requestYield == true && (millis() - yieldTimerStart > (settings.framesToYield * maxPacketAirTime))) - ) + //Check for ACK timeout + if ((millis() - ackTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { - //Yield has expired, allow transmit. - requestYield = false; - - //Check for time to send serial data - if (processWaitingSerial(heartbeatTimeout) == true) + //Determine if another retransmission is allowed + if ((!settings.maxResends) || (frameSentCount < settings.maxResends)) { - triggerEvent(TRIGGER_LINK_DATA_XMIT); - - if (xmitDatagramP2PData() == true) + //Throttle back retransmits based on the number of retransmits we've attempted + //retransmitTimeout is a random number, set when the first datagram is sent + if (millis() - datagramTimer > (frameSentCount * retransmitTimeout)) { - setHeartbeatLong(); //We're sending data, so don't be the first to send next heartbeat - START_ACK_TIMER(); + lostFrames++; - //Save the previous transmit in case the previous ACK was lost or a - //HEARTBEAT must be transmitted. Restore the buffer when a retransmission - //is necessary. - petWDT(); - SAVE_TX_BUFFER(); - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + //Display the retransmit + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrintln("RX: ACK Timeout"); + outputSerialData(true); + } + + triggerEvent(TRIGGER_LINK_RETRANSMIT); + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("TX: Retransmit "); + systemPrint(frameSentCount); + systemPrint(", "); + systemPrint(radioDatagramType[txControl.datagramType]); + switch (txControl.datagramType) + { + default: + systemPrintln(); + break; + + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + case DATAGRAM_HEARTBEAT: + systemPrint(" (ACK #"); + systemPrint(txControl.ackNumber); + systemPrint(")"); + systemPrintln(); + break; + } + outputSerialData(true); + } + + //Attempt the retransmission + RESTORE_TX_BUFFER(); + if (retransmitDatagram(NULL) == true) + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat + + START_ACK_TIMER(); } } + else + { + lostFrames++; + + //Failed to reach the other system, break the link + triggerEvent(TRIGGER_LINK_RETRANSMIT_FAIL); + + //Break the link + breakLink(); + } } } + + //---------- + //Priority 3: Send the entire command or response, toggle between waiting + //for ACK above and transmitting the command or response + //---------- + else if (availableTXCommandBytes()) //If we have command bytes to send out { //Load command bytes into outgoing packet readyOutgoingCommandPacket(0); triggerEvent(TRIGGER_LINK_DATA_XMIT); - - //We now have the commandTXBuffer loaded if (remoteCommandResponse) { + //Send the command response if (xmitDatagramP2PCommandResponse() == true) - { - setHeartbeatLong(); //We're sending command, so don't be the first to send next heartbeat changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - } } else { + //Send the command if (xmitDatagramP2PCommand() == true) - { - setHeartbeatLong(); //We're sending command, so don't be the first to send next heartbeat changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - } } - } - else if (heartbeatTimeout) - { - triggerEvent(TRIGGER_HEARTBEAT); - - if (xmitDatagramP2PHeartbeat() == true) - { - setHeartbeatLong(); //We're sending a heartbeat, so don't be the first to send next heartbeat + //Save the message for retransmission + SAVE_TX_BUFFER(); + setHeartbeatLong(); //We're sending command, so don't be the first to send next heartbeat - ackTimer = datagramTimer; - - //Wait for heartbeat to transmit - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - } + START_ACK_TIMER(); } - else if ((millis() - linkDownTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)) - //Break the link - breakLink(); - } - break; - - //==================== - //Wait for the ACK or HEARTBEAT to finish transmission - //==================== - case RADIO_P2P_LINK_UP_WAIT_ACK_DONE: - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - if (transactionComplete) - { - triggerEvent(TRIGGER_LINK_ACK_SENT); - transactionComplete = false; //Reset ISR flag - returnToReceiving(); - changeState(RADIO_P2P_LINK_UP); - } - break; - - //==================== - //Wait for the data transmission to complete - //==================== - case RADIO_P2P_LINK_UP_WAIT_TX_DONE: - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - if (transactionComplete) - { - sf6ExpectedSize = headerBytes + CHANNEL_TIMER_BYTES + trailerBytes; //Tell SF6 to receive ACK packet - - triggerEvent(TRIGGER_LINK_WAIT_FOR_ACK); - transactionComplete = false; //Reset ISR flag - returnToReceiving(); - changeState(RADIO_P2P_LINK_UP_WAIT_ACK); - } - break; - - //==================== - //Wait for the ACK to be received - //==================== - case RADIO_P2P_LINK_UP_WAIT_ACK: - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - if (transactionComplete) - { - //Decode the received datagram - PacketType packetType = rcvDatagram(); + //---------- + //Lowest Priority: Check for data to send + //---------- - //Process the received datagram - switch (packetType) + //If we have data, try to send it out + else if (availableRadioTXBytes()) { - default: - triggerEvent(TRIGGER_UNKNOWN_PACKET); - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("RX: Unhandled packet type "); - systemPrint(radioDatagramType[packetType]); - systemPrintln(); - outputSerialData(true); - } - break; - - case DATAGRAM_BAD: - triggerEvent(TRIGGER_BAD_PACKET); - break; - - case DATAGRAM_CRC_ERROR: - triggerEvent(TRIGGER_CRC_ERROR); - break; - - case DATAGRAM_NETID_MISMATCH: - triggerEvent(TRIGGER_NETID_MISMATCH); - break; - - case DATAGRAM_PING: - //Break the link - breakLink(); - break; - - case DATAGRAM_DATA_ACK: - //The datagram we are expecting - syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock - - //Stop the ACK timer - STOP_ACK_TIMER(); - - setHeartbeatLong(); //Those who send an ACK have short time to next heartbeat. Those who send a heartbeat or data have long time to next heartbeat. - - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - triggerEvent(TRIGGER_LINK_ACK_RECEIVED); - changeState(RADIO_P2P_LINK_UP); - break; - - case DATAGRAM_HEARTBEAT: - //Received heartbeat while waiting for ack. - //Adjust the timestamp offset - currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; - - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); - if (xmitDatagramP2PAck() == true) //Transmit ACK - { - setHeartbeatShort(); //We ack'd the packet so be responsible for sending the next heartbeat - changeState(RADIO_P2P_LINK_UP_HB_ACK_REXMT); - } - - break; - - case DATAGRAM_DATA: - //Received data while waiting for ack. - - //Place the data in the serial output buffer - serialBufferOutput(rxData, rxDataBytes); - - frequencyCorrection += radio.getFrequencyError() / 1000000.0; + //Check if we are yielding for 2-way comm + if (requestYield == false || + (requestYield == true && (millis() - yieldTimerStart > (settings.framesToYield * maxPacketAirTime))) + ) + { + //Yield has expired, allow transmit. + requestYield = false; - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DATA); - if (xmitDatagramP2PAck() == true) //Transmit ACK + //Check for time to send serial data + if (processWaitingSerial(heartbeatTimeout) == true) { - setHeartbeatShort(); //We ack'd this data, so be responsible for sending the next heartbeat - changeState(RADIO_P2P_LINK_UP_HB_ACK_REXMT); - } - break; - } - } + triggerEvent(TRIGGER_LINK_DATA_XMIT); - //Check for ACK timeout, set at end of transmit, measures ACK timeout - else if ((receiveInProcess() == false) - && ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset()))) - { - //Retransmit the packet - if ((!settings.maxResends) || (frameSentCount < settings.maxResends)) - { - //Throttle back retransmits based on the number of retransmits we've attempted - //retransmitTimeout is a random number, set when the first datagram is sent - if (millis() - datagramTimer > (frameSentCount * retransmitTimeout)) - { - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrintln("RX: ACK Timeout"); - outputSerialData(true); - } + if (xmitDatagramP2PData() == true) + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - triggerEvent(TRIGGER_LINK_RETRANSMIT); - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("TX: Retransmit "); - systemPrint(frameSentCount); - systemPrint(", "); - systemPrint(radioDatagramType[txControl.datagramType]); - switch (txControl.datagramType) - { - default: - systemPrintln(); - break; - - case DATAGRAM_DATA: - case DATAGRAM_DATA_ACK: - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - systemPrint(" (ACK #"); - systemPrint(txControl.ackNumber); - systemPrint(")"); - systemPrintln(); - break; - } - outputSerialData(true); - } + //Save the message for retransmission + SAVE_TX_BUFFER(); + setHeartbeatLong(); //We're sending data, so don't be the first to send next heartbeat - if (retransmitDatagram(NULL) == true) - { - setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat - lostFrames++; - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + START_ACK_TIMER(); } } } - else - { - //Failed to reach the other system, break the link - triggerEvent(TRIGGER_LINK_RETRANSMIT_FAIL); - - //Break the link - breakLink(); - } } - else if ((millis() - linkDownTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)) - //Break the link - breakLink(); - - //Retransmits are not getting through in a rational time - else if (ackTimer && ((millis() - ackTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout))) + //---------- + //Always check for link timeout + //---------- + if ((millis() - linkDownTimer) >= (P2P_LINK_BREAK_MULTIPLIER * settings.heartbeatTimeout)) //Break the link breakLink(); break; - //==================== - //Wait for the HEARTBEAT frame to complete transmission then wait for ACK - //and retransmit previous data frame if necessary - //==================== - case RADIO_P2P_LINK_UP_HB_ACK_REXMT: - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - - //An ACK was expected for a previous transmission that must have been - //lost. A heartbeat was received instead which was ACKed. Once the ACK - //completes transmission, retransmit the previously lost datagram. - if (transactionComplete) - { - transactionComplete = false; //Reset ISR flag - - //Retransmit the packet - if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) - { - RESTORE_TX_BUFFER(); - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("TX: Retransmit "); - systemPrint(frameSentCount); - systemPrint(", "); - systemPrint(radioDatagramType[txControl.datagramType]); - outputSerialData(true); - switch (txControl.datagramType) - { - default: - systemPrintln(); - break; - - case DATAGRAM_DATA: - case DATAGRAM_DATA_ACK: - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - systemPrint(" (ACK #"); - systemPrint(txControl.ackNumber); - systemPrint(")"); - systemPrintln(); - outputSerialData(true); - break; - } - } - - triggerEvent(TRIGGER_LINK_HB_ACK_REXMIT); - - if (retransmitDatagram(NULL) == true) - { - setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat - lostFrames++; - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - } - } - else - //Failed to reach the other system, break the link - breakLink(); - } - break; - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Multi-Point Data Exchange //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -2252,7 +2112,7 @@ void selectHeaderAndTrailerBytes() bool isLinked() { if ((radioState >= RADIO_P2P_LINK_UP) - && (radioState <= RADIO_P2P_LINK_UP_WAIT_ACK)) + && (radioState <= RADIO_P2P_LINK_UP_WAIT_TX_DONE)) return (true); return (false); diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 64ecad84..fc6d6f30 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -681,22 +681,7 @@ void radioLeds() } //Update the link LED - switch (radioState) - { - //Turn off the LED - default: - digitalWrite(RADIO_USE_LINK_LED, LED_OFF); - break; - - //Turn on the LED - case RADIO_P2P_LINK_UP: - case RADIO_P2P_LINK_UP_WAIT_ACK_DONE: - case RADIO_P2P_LINK_UP_WAIT_TX_DONE: - case RADIO_P2P_LINK_UP_WAIT_ACK: - case RADIO_P2P_LINK_UP_HB_ACK_REXMT: - digitalWrite(RADIO_USE_LINK_LED, LED_ON); - break; - } + digitalWrite(RADIO_USE_LINK_LED, isLinked() ? LED_ON : LED_OFF); //Update the RSSI LED if (currentMillis != previousMillis) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 66907539..b6c8f5e5 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -12,10 +12,7 @@ typedef enum //Point-to-Point: Link up, data exchange RADIO_P2P_LINK_UP, - RADIO_P2P_LINK_UP_WAIT_ACK_DONE, RADIO_P2P_LINK_UP_WAIT_TX_DONE, - RADIO_P2P_LINK_UP_WAIT_ACK, - RADIO_P2P_LINK_UP_HB_ACK_REXMT, //Server-client discovery RADIO_DISCOVER_BEGIN, @@ -71,38 +68,35 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Point-to-Point, link up, data exchange // State RX Name Description {RADIO_P2P_LINK_UP, 1, "P2P_LINK_UP", "P2P: Receiving Standby"}, //10 - {RADIO_P2P_LINK_UP_WAIT_ACK_DONE, 0, "P2P_LINK_UP_WAIT_ACK_DONE", "P2P: Waiting ACK TX Done"}, //11 - {RADIO_P2P_LINK_UP_WAIT_TX_DONE, 0, "P2P_LINK_UP_WAIT_TX_DONE", "P2P: Waiting TX done"}, //12 - {RADIO_P2P_LINK_UP_WAIT_ACK, 1, "P2P_LINK_UP_WAIT_ACK", "P2P: Waiting for ACK"}, //13 - {RADIO_P2P_LINK_UP_HB_ACK_REXMT, 0, "P2P_LINK_UP_HB_ACK_REXMT", "P2P: Heartbeat ACK ReXmt"}, //14 + {RADIO_P2P_LINK_UP_WAIT_TX_DONE, 0, "P2P_LINK_UP_WAIT_TX_DONE", "P2P: Waiting TX done"}, //11 //Server-client discovery // State RX Name Description - {RADIO_DISCOVER_BEGIN, 0, "DISCOVER_BEGIN", "Disc: Setup for scanning"}, //15 - {RADIO_DISCOVER_SCANNING, 0, "DISCOVER_SCANNING", "Disc: Scanning for servers"}, //16 - {RADIO_DISCOVER_WAIT_TX_PING_DONE, 0, "DISCOVER_WAIT_TX_PING_DONE", "Disc: Wait for ping to xmit"}, //17 + {RADIO_DISCOVER_BEGIN, 0, "DISCOVER_BEGIN", "Disc: Setup for scanning"}, //12 + {RADIO_DISCOVER_SCANNING, 0, "DISCOVER_SCANNING", "Disc: Scanning for servers"}, //13 + {RADIO_DISCOVER_WAIT_TX_PING_DONE, 0, "DISCOVER_WAIT_TX_PING_DONE", "Disc: Wait for ping to xmit"}, //14 //Multi-Point data exchange // State RX Name Description - {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //18 - {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //19 - {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //20 + {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //15 + {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //16 + {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //17 //Training client states // State RX Name Description - {RADIO_TRAIN_WAIT_TX_PING_DONE, 0, "TRAIN_WAIT_TX_PING_DONE", "Train: Wait TX training PING done"}, //21 - {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //22 - {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //23 + {RADIO_TRAIN_WAIT_TX_PING_DONE, 0, "TRAIN_WAIT_TX_PING_DONE", "Train: Wait TX training PING done"}, //18 + {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //19 + {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //20 //Training server states // State RX Name Description - {RADIO_TRAIN_WAIT_FOR_PING, 1, "TRAIN_WAIT_FOR_PING", "Train: Wait for training PING"}, //24 - {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //25 + {RADIO_TRAIN_WAIT_FOR_PING, 1, "TRAIN_WAIT_FOR_PING", "Train: Wait for training PING"}, //21 + {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //22 //Virtual circuit states // State RX Name Description - {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "VC: Wait for TX done"}, //27 - {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "VC: Wait for receive"}, //28 + {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "VC: Wait for TX done"}, //23 + {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "VC: Wait for receive"}, //24 }; //Possible types of packets received From b0e1da8cd58c3d4ec4c20e3b5d4bc4267d44d79f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 17 Dec 2022 11:51:55 -1000 Subject: [PATCH 329/594] VC: Remove VC use of multipoint frequency hopping synchronization Restore 3-way ACK handshake --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 57 ++-- Firmware/LoRaSerial_Firmware/Serial.ino | 10 +- Firmware/LoRaSerial_Firmware/States.ino | 320 +++++++++--------- .../Virtual_Circuit_Protocol.h | 17 +- Firmware/LoRaSerial_Firmware/settings.h | 61 ++-- Firmware/Tools/VcServerTest.c | 18 +- 7 files changed, 244 insertions(+), 241 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index cf469cc3..ea77c2b5 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -122,7 +122,7 @@ bool commandAT(const char * commandString) if (cmdVc == myVc) vcChangeState(cmdVc, VC_STATE_CONNECTED); else - vcChangeState(cmdVc, VC_STATE_SEND_PING); + vcChangeState(cmdVc, VC_STATE_SEND_UNKNOWN_ACKS); return true; case ('F'): //ATF - Restore default parameters diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 0c3fa1b0..bf88c906 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1282,7 +1282,7 @@ bool xmitVcAckFrame(int8_t destVc) radioCallHistory[RADIO_CALL_xmitVcAckFrame] = millis(); vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; - vcHeader->length = VC_RADIO_HEADER_BYTES + CHANNEL_TIMER_BYTES; + vcHeader->length = VC_RADIO_HEADER_BYTES; vcHeader->destVc = destVc; vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; @@ -1299,14 +1299,16 @@ bool xmitVcAckFrame(int8_t destVc) */ //Finish building the ACK frame - return xmitDatagramP2PAck(); + txControl.datagramType = DATAGRAM_DATA_ACK; + return (transmitDatagram()); } -bool xmitVcPing(int8_t destVc) +//Build and transmit the UNKNOWN_ACKS frame, first frame in 3-way ACKs handshake +bool xmitVcUnknownAcks(int8_t destVc) { VC_RADIO_MESSAGE_HEADER * vcHeader; - radioCallHistory[RADIO_CALL_xmitVcPing] = millis(); + radioCallHistory[RADIO_CALL_xmitVcUnknownAcks] = millis(); vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; vcHeader->length = VC_RADIO_HEADER_BYTES; @@ -1325,18 +1327,16 @@ bool xmitVcPing(int8_t destVc) +----------+---------+----------+------------+----------+---------+----------+ */ - txControl.datagramType = DATAGRAM_PING; + txControl.datagramType = DATAGRAM_VC_UNKNOWN_ACKS; return (transmitDatagram()); } -//ACK packet sent by server in response the client ping, includes channel number -//During discovery scanning, it's possible for the client to get an ACK but be on an adjacent channel -//The channel number ensures that the client gets the next hop correct -bool xmitVcAck1(int8_t destVc) +//Build and transmit the SYNC_ACKS frame, second frame in 3-way ACKs handshake +bool xmitVcSyncAcks(int8_t destVc) { VC_RADIO_MESSAGE_HEADER * vcHeader; - radioCallHistory[RADIO_CALL_xmitVcAck1] = millis(); + radioCallHistory[RADIO_CALL_xmitVcSyncAcks] = millis(); vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; vcHeader->length = VC_RADIO_HEADER_BYTES; @@ -1344,10 +1344,6 @@ bool xmitVcAck1(int8_t destVc) vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; - memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); - endOfTxData += sizeof(channelNumber); - vcHeader->length++; - if(settings.debugSync) { systemPrint(" channelNumber: "); @@ -1356,25 +1352,26 @@ bool xmitVcAck1(int8_t destVc) } /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+----------+---------+---------+----------+ - | Optional | | Optional | Optional | | | Channel | Optional | - | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Number | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | 1 byte | n Bytes | - +----------+---------+----------+------------+----------+---------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+---------+----------+ + | Optional | | Optional | Optional | | | Optional | + | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | n Bytes | + +----------+---------+----------+------------+----------+---------+----------+ */ - txControl.datagramType = DATAGRAM_ACK_1; + txControl.datagramType = DATAGRAM_VC_SYNC_ACKS; return (transmitDatagram()); } -bool xmitVcAck2(int8_t destVc) +//Build and transmit the ZERO_ACKS frame, last frame in 3-way ACKs handshake +bool xmitVcZeroAcks(int8_t destVc) { VC_RADIO_MESSAGE_HEADER * vcHeader; - radioCallHistory[RADIO_CALL_xmitVcAck2] = millis(); + radioCallHistory[RADIO_CALL_xmitVcZeroAcks] = millis(); vcHeader = (VC_RADIO_MESSAGE_HEADER *)endOfTxData; vcHeader->length = VC_RADIO_HEADER_BYTES; @@ -1393,7 +1390,7 @@ bool xmitVcAck2(int8_t destVc) +----------+---------+----------+------------+----------+---------+----------+ */ - txControl.datagramType = DATAGRAM_ACK_2; + txControl.datagramType = DATAGRAM_VC_ZERO_ACKS; return (transmitDatagram()); } @@ -2634,7 +2631,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) (receiveInProcess() == true) || (transactionComplete == true) || ( - //If we are in VC mode, and destination is not broadcast, + //If we are in VC mode, and destination is not broadcast, //and the destination circuit is offline //and we are not scanning for servers //then don't transmit @@ -3074,9 +3071,9 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_xmitVcDatagram, "xmitVcDatagram"}, {RADIO_CALL_xmitVcHeartbeat, "xmitVcHeartbeat"}, {RADIO_CALL_xmitVcAckFrame, "xmitVcAckFrame"}, - {RADIO_CALL_xmitVcPing, "xmitVcPing"}, - {RADIO_CALL_xmitVcAck1, "xmitVcAck1"}, - {RADIO_CALL_xmitVcAck2, "xmitVcAck2"}, + {RADIO_CALL_xmitVcUnknownAcks, "xmitVcUpdateAcks"}, + {RADIO_CALL_xmitVcSyncAcks, "xmitVcSyncAcks"}, + {RADIO_CALL_xmitVcZeroAcks, "xmitVcZeroAcks"}, {RADIO_CALL_rcvDatagram, "rcvDatagram"}, {RADIO_CALL_transmitDatagram, "transmitDatagram"}, {RADIO_CALL_retransmitDatagram, "retransmitDatagram"}, diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 198e158b..5ac4b529 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -386,7 +386,7 @@ void processSerialInput() inCommandMode = true; //Allow AT parsing. Prevent received RF data from being printed. forceRadioReset = false; //Don't reset the radio link unless a setting requires it - writeOnCommandExit = false; //Don't record settings changes unless user commands it + writeOnCommandExit = false; //Don't record settings changes unless user commands it tempSettings = settings; @@ -565,14 +565,14 @@ bool vcSerialMessageReceived() break; } - //Verify that the destination link is up + //Verify that the destination link is connected if ((vcDest != VC_BROADCAST) - && (virtualCircuitList[vcIndex].vcState == VC_STATE_LINK_DOWN)) + && (virtualCircuitList[vcIndex].vcState < VC_STATE_CONNECTED)) { if (settings.debugSerial || settings.debugTransmit) { - systemPrint("Link down "); - systemPrint((vcDest == VC_BROADCAST) ? vcDest : vcIndex); + systemPrint("Link not connected "); + systemPrint(vcIndex); systemPrintln(", discarding message!"); outputSerialData(true); } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d2b838b0..487ac504 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -127,22 +127,15 @@ void updateRadioState() else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { if (settings.server) - { //Reserve the server's address (0) myVc = vcIdToAddressByte(VC_SERVER, myUniqueId); - - startChannelTimer(); //Start hopping - - //Start sending heartbeats - xmitVcHeartbeat(myVc, myUniqueId); - changeState(RADIO_VC_WAIT_TX_DONE); - } else - { //Unknown client address myVc = VC_UNASSIGNED; - changeState(RADIO_DISCOVER_BEGIN); - } + + //Start sending heartbeats + xmitVcHeartbeat(myVc, myUniqueId); + changeState(RADIO_VC_WAIT_TX_DONE); } else { @@ -956,10 +949,7 @@ void updateRadioState() //Server has responded with ACK syncChannelTimer(); //Start and adjust freq hop ISR based on remote's remaining clock - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - channelNumber = rxVcData[0]; - else - channelNumber = rxData[0]; //Change to the server's channel number + channelNumber = rxData[0]; //Change to the server's channel number if (settings.debugSync) { @@ -974,19 +964,7 @@ void updateRadioState() lastPacketReceived = millis(); //Reset - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - { - //Acknowledge the ACK1 - triggerEvent(TRIGGER_SEND_ACK2); - if (xmitVcAck2(VC_SERVER)) - { - sf6ExpectedSize = MAX_PACKET_SIZE; //Tell SF6 to return to max packet length - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - else - changeState(RADIO_MP_STANDBY); - + changeState(RADIO_MP_STANDBY); break; } } @@ -1024,19 +1002,8 @@ void updateRadioState() } //Send ping - if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - { - //Send the PING datagram, first part of the 3-way handshake - if (xmitVcPing(VC_SERVER)) - { - changeState(RADIO_DISCOVER_WAIT_TX_PING_DONE); - } - } - else - { - if (xmitDatagramMpPing() == true) - changeState(RADIO_DISCOVER_WAIT_TX_PING_DONE); - } + if (xmitDatagramMpPing() == true) + changeState(RADIO_DISCOVER_WAIT_TX_PING_DONE); } } @@ -1185,7 +1152,6 @@ void updateRadioState() //Only the server transmits heartbeats if (settings.server == true) { - if (heartbeatTimeout) { triggerEvent(TRIGGER_HEARTBEAT); @@ -1531,11 +1497,18 @@ void updateRadioState() | | '-----------------' + The ACK synchronization handshake occurs between VCs that want to + communicate. Normally this occurs between the client and the server, but + may also occur between two clients that want to communicate with each + other. One of the systems initiates the three way handshake by sending + UNKNOWN_ACKS. The second system responses with SYNC_ACKSZERO_ACKS causing the + first system to respond with ZERO_ACKS. + 3-way Handshake to zero ACKs: - TX PING --> RX ACK1 --> TX ACK2 - or - RX PING --> TX ACK1 --> RX ACK2 + TX UNKNOWN_ACKS --> RX SYNC_ACKS --> TX ZERO_ACKS + or + RX UNKNOWN_ACKS --> TX SYNC_ACKS --> RX ZERO_ACKS VC States: @@ -1544,27 +1517,56 @@ void updateRadioState() | | HEARTBEAT received | | HB Timeout v | +<----------- VC_STATE_LINK_ALIVE ------------------------. | - ^ | RX PING | | + ^ | RX UNKNOWN_ACKS | | | | ATC command | | | HB Timeout V | | - +<------------- VC_STATE_SEND_PING <---------. | | - ^ | ACK1 RX | | | - | | TX PING Timeout | | | - | | TX Complete | RX | | - | HB Timeout V | PING V | - +<------------- VC_STATE_WAIT_ACK1 --------->+------->+-->+ | - ^ | ^ ^ | | - | | RX ACK1 | ACK2 | | TX ACK1 | - | | TX ACK2 | RX | | TX Complete | - | | TX Complete | TMO | V | - | Zero ACK values | | VC_STATE_WAIT_ACK2 ----' - | | .-------------' | - | | | RX PING | RX ACK2 + +<--------- VC_STATE_SEND_UNKNOWN_ACKS <-. | | + ^ | | | | + | TX UNKNOWN_ACKS | ZERO_ACKS RX | | | + | TX Complete | Timeout | RX | | + | HB Timeout V | UNKNOWN_ACKS V | + +<---------- VC_STATE_WAIT_SYNC_ACKS --->+----------->+-->+ | + ^ | ^ ^ | TX | + | | RX SYNC_ACKS | ZERO_ACKS | | SYNC_ACKS | + | | TX ZERO_ACKS | RX | | TX Complete | + | | TX Complete | TMO | V | + | Zero ACK values | | VC_STATE_WAIT_ZERO_ACKS --' + | | .---------' | + | | | RX UNKNOWN_ACKS | RX ZERO_ACKS | HB Timeout V | | Zero ACK values '-------------- VC_STATE_CONNECTED <----------------------' */ + //==================== + //Wait for a HEARTBEAT from the server + //==================== + case RADIO_VC_WAIT_SERVER: + if (myVc == VC_SERVER) + { + changeState(RADIO_VC_WAIT_RECEIVE); + break; + } + + //If dio0ISR has fired, a packet has arrived + currentMillis = millis(); + if (transactionComplete == true) + { + //Decode the received datagram + PacketType packetType = rcvDatagram(); + + //Process the received datagram + if ((packetType == DATAGRAM_VC_HEARTBEAT) && (rxSrcVc == VC_SERVER)) + { + vcReceiveHeartbeat(millis() - currentMillis); + changeState(RADIO_VC_WAIT_RECEIVE); + } + else + //Ignore this datagram + triggerEvent(TRIGGER_BAD_PACKET); + } + break; + //==================== //Wait for the transmission to complete //==================== @@ -1671,36 +1673,18 @@ void updateRadioState() changeState(RADIO_VC_WAIT_TX_DONE); break; - //Second step in the 3-way handshake, received PING, respond with ACK1 - case DATAGRAM_PING: - //Only respond to pings if we are server - if (settings.server == true) - { - if (rxSrcVc == VC_UNASSIGNED) - { - //We received a ping from a previously unknown client - if (xmitVcAck1(rxSrcVc)) - { - triggerEvent(TRIGGER_MP_SEND_ACK_FOR_PING); - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - else - { - //Known client - if (xmitVcAck1(rxSrcVc)) - changeState(RADIO_VC_WAIT_TX_DONE); - - vcChangeState(rxSrcVc, VC_STATE_WAIT_FOR_ACK2); - virtualCircuitList[rxSrcVc].lastPingMillis = datagramTimer; - } - } - + //Second step in the 3-way handshake, received UNKNOWN_ACKS, respond + //with SYNC_ACKS + case DATAGRAM_VC_UNKNOWN_ACKS: + if (xmitVcSyncAcks(rxSrcVc)) + changeState(RADIO_VC_WAIT_TX_DONE); + vcChangeState(rxSrcVc, VC_STATE_WAIT_ZERO_ACKS); + virtualCircuitList[rxSrcVc].timerMillis = datagramTimer; break; - //Third step in the 3-way handshake, received ACK1, respond with ACK2 - case DATAGRAM_ACK_1: - if (xmitVcAck2(rxSrcVc)) + //Third step in the 3-way handshake, received SYNC_ACKS, respond with ZERO_ACKS + case DATAGRAM_VC_SYNC_ACKS: + if (xmitVcZeroAcks(rxSrcVc)) { changeState(RADIO_VC_WAIT_TX_DONE); vcZeroAcks(rxSrcVc); @@ -1708,8 +1692,8 @@ void updateRadioState() } break; - //Last step in the 3-way handshake, received ACK2, done - case DATAGRAM_ACK_2: + //Last step in the 3-way handshake, received ZERO_ACKS, done + case DATAGRAM_VC_ZERO_ACKS: vcZeroAcks(rxSrcVc); vcChangeState(rxSrcVc, VC_STATE_CONNECTED); break; @@ -1781,11 +1765,12 @@ void updateRadioState() } } - //---------- - //Priority 1: Transmit a HEARTBEAT if necessary - //---------- + //when not receiving process the pending transmit requests else if (!receiveInProcess()) { + //---------- + //Priority 1: Transmit a HEARTBEAT if necessary + //---------- if (((currentMillis - heartbeatTimer) >= heartbeatRandomTime)) { //Send another heartbeat @@ -1864,72 +1849,91 @@ void updateRadioState() } } - /* - //---------- - //Priority 4: Walk through the 3-way handshake - //---------- - else if (vcConnecting) + //---------- + //Priority 3: Send the entire command response, toggle between waiting for + //ACK above and transmitting the command response + //---------- + else if (availableTXCommandBytes()) + { + //Send the next portion of the command response + vcHeader = (VC_RADIO_MESSAGE_HEADER *)&outgoingPacket[headerBytes]; + endOfTxData += VC_RADIO_HEADER_BYTES; + vcHeader->destVc = rmtCmdVc; + vcHeader->srcVc = myVc; + vcHeader->length = readyOutgoingCommandPacket(VC_RADIO_HEADER_BYTES) + + VC_RADIO_HEADER_BYTES; + if (xmitDatagramP2PCommandResponse()) + changeState(RADIO_VC_WAIT_TX_DONE); + + START_ACK_TIMER(); + + //Save the message for retransmission + SAVE_TX_BUFFER(); + rexmtTxDestVc = txDestVc; + } + + //---------- + //Priority 4: Walk through the 3-way handshake + //---------- + else if (vcConnecting) + { + for (index = 0; index < MAX_VC; index++) + { + if (receiveInProcess()) + break; + + //Determine the first VC that is walking through connections + if (vcConnecting & (1 << index)) + { + //Determine if UNKNOWN_ACKS needs to be sent + if (virtualCircuitList[index].vcState == VC_STATE_SEND_UNKNOWN_ACKS) + { + //Send the UNKNOWN_ACKS datagram, first part of the 3-way handshake + if (xmitVcUnknownAcks(index)) { - for (index = 0; index < MAX_VC; index++) + vcChangeState(index, VC_STATE_WAIT_SYNC_ACKS); + virtualCircuitList[index].timerMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + } + } + + //The SYNC_ACKS is handled with the receive code + //Check for a timeout waiting for the SYNC_ACKS + else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_SYNC_ACKS) + { + if ((currentMillis - virtualCircuitList[index].timerMillis) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + { + //Retransmit the UNKNOWN_ACKS + if (xmitVcUnknownAcks(index)) { - if (receiveInProcess()) - break; + virtualCircuitList[index].timerMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); + } + } + } - //Determine the first VC that is walking through connections - if (vcConnecting & (1 << index)) - { - //Determine if PING needs to be sent - if (virtualCircuitList[index].vcState == VC_STATE_SEND_PING) - { - //Send the PING datagram, first part of the 3-way handshake - if (xmitVcPing(index)) - { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - - //The ACK1 is handled with the receive code - //Check for a timeout waiting for the ACK1 - else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK1) - { - if ((currentMillis - virtualCircuitList[index].lastPingMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) - { - //Retransmit the PING - if (xmitVcPing(index)) - { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK1); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - } - - //The ACK2 is handled with the receive code - //Check for a timeout waiting for the ACK2 - else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_FOR_ACK2) - { - if ((currentMillis - virtualCircuitList[index].lastPingMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) - { - //Retransmit the ACK1 - if (xmitVcAck1(index)) - { - vcChangeState(index, VC_STATE_WAIT_FOR_ACK2); - virtualCircuitList[index].lastPingMillis = datagramTimer; - changeState(RADIO_VC_WAIT_TX_DONE); - } - } - } - - //Work on only one connection at a time - break; - } + //The ZERO_ACKS is handled with the receive code + //Check for a timeout waiting for the ZERO_ACKS + else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_ZERO_ACKS) + { + if ((currentMillis - virtualCircuitList[index].timerMillis) + >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + { + //Retransmit the ACK1 + if (xmitVcSyncAcks(index)) + { + virtualCircuitList[index].timerMillis = datagramTimer; + changeState(RADIO_VC_WAIT_TX_DONE); } } - */ + } + + //Work on only one connection at a time + break; + } + } + } //---------- //Lowest Priority: Check for data to send @@ -2062,7 +2066,7 @@ void updateRadioState() //and the radios loose communications because they are hopping at different //times. serverLinkBroken = true; - changeState(RADIO_DISCOVER_BEGIN); + changeState(RADIO_VC_WAIT_SERVER); } //Break the link @@ -2502,18 +2506,12 @@ void vcChangeState(int8_t vcIndex, uint8_t state) systemPrint(vcIndex); systemPrintln(" ALIVE =--=--=-"); } - else if (state == VC_STATE_NEW_CLIENT) - { - systemPrint("-=--=--=- VC "); - systemPrint(vcIndex); - systemPrintln(" NEW CLIENT =--=--=-"); - } outputSerialData(true); } //Determine if the VC is connecting vcBit = 1 << vcIndex; - if (state == VC_STATE_NEW_CLIENT) + if ((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) vcConnecting |= vcBit; else vcConnecting &= ~vcBit; diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 21d73a69..92bafe19 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -139,13 +139,12 @@ typedef struct _VC_STATE_MESSAGE typedef enum { - VC_STATE_LINK_DOWN = 0, //0: HEARTBEATs not received - VC_STATE_LINK_ALIVE, //1: Receiving HEARTBEATs, waiting for PING - VC_STATE_SEND_PING, //2: ATC command received, sending PING - VC_STATE_WAIT_FOR_ACK1 ,//3: PING sent, waiting for ACK1 - VC_STATE_WAIT_FOR_ACK2 ,//4: ACK1 sent, waiting for ACK2 - VC_STATE_CONNECTED, //5: ACK2 received, ACKs cleared, ready to send data - VC_STATE_NEW_CLIENT, //6: A server received a PING from a previously unknown client + VC_STATE_LINK_DOWN = 0, //0: HEARTBEATs not received + VC_STATE_LINK_ALIVE, //1: Receiving HEARTBEATs, waiting for UNKNOWN_ACKS + VC_STATE_SEND_UNKNOWN_ACKS, //2: ATC command received, sending UNKNOWN_ACKS + VC_STATE_WAIT_SYNC_ACKS, //3: UNKNOWN_ACKS sent, waiting for SYNC_ACKS + VC_STATE_WAIT_ZERO_ACKS, //4: SYNC_ACKS sent, waiting for ZERO_ACKS + VC_STATE_CONNECTED, //5: ZERO_ACKS received, ACKs cleared, ready to send data //Insert new states before this line VC_STATE_MAX @@ -173,8 +172,8 @@ typedef struct _VC_DATA_ACK_NACK_MESSAGE const char * const vcStateNames[] = { // 0 1 "LINK-DOWN", "LINK-ALIVE", - // 2 3 4 5 6 - "SEND-PING", "WAIT-ACK1", "WAIT-ACK2", "CONNECTED", "NEW-CLIENT", + // 2 3 4 5 + "UNKNOWN-ACKS", "SYNC-ACKS", "ZERO-ACKS", "CONNECTED", }; #endif //ADD_VC_STATE_NAMES_TABLE diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index b6c8f5e5..248156ad 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -34,6 +34,7 @@ typedef enum RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE, //Virtual-Circuit states + RADIO_VC_WAIT_SERVER, RADIO_VC_WAIT_TX_DONE, RADIO_VC_WAIT_RECEIVE, @@ -58,51 +59,54 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Point-to-Point link handshake // State RX Name Description - {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "P2P: [No Link] Waiting for Ping"}, // 4 - {RADIO_P2P_WAIT_TX_PING_DONE, 0, "P2P_WAIT_TX_PING_DONE", "P2P: [No Link] Wait Ping TX Done"},// 5 - {RADIO_P2P_WAIT_ACK_1, 1, "P2P_WAIT_ACK_1", "P2P: [No Link] Waiting for ACK1"}, // 6 - {RADIO_P2P_WAIT_TX_ACK_1_DONE, 0, "P2P_WAIT_TX_ACK_1_DONE", "P2P: [No Link] Wait ACK1 TX Done"},// 7 - {RADIO_P2P_WAIT_ACK_2, 1, "P2P_WAIT_ACK_2", "P2P: [No Link] Waiting for ACK2"}, // 8 - {RADIO_P2P_WAIT_TX_ACK_2_DONE, 0, "P2P_WAIT_TX_ACK_2_DONE", "P2P: [No Link] Wait ACK2 TX Done"},// 9 + {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "P2P: [No Link] Waiting for Ping"}, // 1 + {RADIO_P2P_WAIT_TX_PING_DONE, 0, "P2P_WAIT_TX_PING_DONE", "P2P: [No Link] Wait Ping TX Done"},// 2 + {RADIO_P2P_WAIT_ACK_1, 1, "P2P_WAIT_ACK_1", "P2P: [No Link] Waiting for ACK1"}, // 3 + {RADIO_P2P_WAIT_TX_ACK_1_DONE, 0, "P2P_WAIT_TX_ACK_1_DONE", "P2P: [No Link] Wait ACK1 TX Done"},// 4 + {RADIO_P2P_WAIT_ACK_2, 1, "P2P_WAIT_ACK_2", "P2P: [No Link] Waiting for ACK2"}, // 5 + {RADIO_P2P_WAIT_TX_ACK_2_DONE, 0, "P2P_WAIT_TX_ACK_2_DONE", "P2P: [No Link] Wait ACK2 TX Done"},// 6 //Point-to-Point, link up, data exchange // State RX Name Description - {RADIO_P2P_LINK_UP, 1, "P2P_LINK_UP", "P2P: Receiving Standby"}, //10 - {RADIO_P2P_LINK_UP_WAIT_TX_DONE, 0, "P2P_LINK_UP_WAIT_TX_DONE", "P2P: Waiting TX done"}, //11 + {RADIO_P2P_LINK_UP, 1, "P2P_LINK_UP", "P2P: Receiving Standby"}, // 7 + {RADIO_P2P_LINK_UP_WAIT_TX_DONE, 0, "P2P_LINK_UP_WAIT_TX_DONE", "P2P: Waiting TX done"}, // 8 //Server-client discovery // State RX Name Description - {RADIO_DISCOVER_BEGIN, 0, "DISCOVER_BEGIN", "Disc: Setup for scanning"}, //12 - {RADIO_DISCOVER_SCANNING, 0, "DISCOVER_SCANNING", "Disc: Scanning for servers"}, //13 - {RADIO_DISCOVER_WAIT_TX_PING_DONE, 0, "DISCOVER_WAIT_TX_PING_DONE", "Disc: Wait for ping to xmit"}, //14 + {RADIO_DISCOVER_BEGIN, 0, "DISCOVER_BEGIN", "Disc: Setup for scanning"}, // 9 + {RADIO_DISCOVER_SCANNING, 0, "DISCOVER_SCANNING", "Disc: Scanning for servers"}, //10 + {RADIO_DISCOVER_WAIT_TX_PING_DONE, 0, "DISCOVER_WAIT_TX_PING_DONE", "Disc: Wait for ping to xmit"}, //11 //Multi-Point data exchange // State RX Name Description - {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //15 - {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //16 - {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //17 + {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //12 + {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //13 + {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //14 //Training client states // State RX Name Description - {RADIO_TRAIN_WAIT_TX_PING_DONE, 0, "TRAIN_WAIT_TX_PING_DONE", "Train: Wait TX training PING done"}, //18 - {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //19 - {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //20 + {RADIO_TRAIN_WAIT_TX_PING_DONE, 0, "TRAIN_WAIT_TX_PING_DONE", "Train: Wait TX training PING done"}, //15 + {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //16 + {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //17 //Training server states // State RX Name Description - {RADIO_TRAIN_WAIT_FOR_PING, 1, "TRAIN_WAIT_FOR_PING", "Train: Wait for training PING"}, //21 - {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //22 + {RADIO_TRAIN_WAIT_FOR_PING, 1, "TRAIN_WAIT_FOR_PING", "Train: Wait for training PING"}, //18 + {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //19 //Virtual circuit states // State RX Name Description - {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "VC: Wait for TX done"}, //23 - {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "VC: Wait for receive"}, //24 + {RADIO_VC_WAIT_SERVER, 1, "VC_WAIT_SERVER", "VC: Wait for the server"}, //20 + {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "VC: Wait for TX done"}, //21 + {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "VC: Wait for receive"}, //22 }; //Possible types of packets received typedef enum { - //Link establishment handshake + //Sync frequencies, HEARTBEAT timing and zero ACKs + //P2P: Between the two LoRaSerial radios + //VC: Between the server radio and a client radio DATAGRAM_PING = 0, // 0 DATAGRAM_ACK_1, // 1 DATAGRAM_ACK_2, // 2 @@ -124,6 +128,9 @@ typedef enum //Virtual-Circuit (VC) exchange DATAGRAM_VC_HEARTBEAT, //12 + DATAGRAM_VC_UNKNOWN_ACKS, //13 Synchronize ACKs client VC to client VC + DATAGRAM_VC_SYNC_ACKS, //14 + DATAGRAM_VC_ZERO_ACKS, //15 //Add new datagram types before this line MAX_DATAGRAM_TYPE, @@ -149,6 +156,8 @@ const char * const radioDatagramType[] = "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK", // 12 "VC_HEARTBEAT", + // 13 14 15 + "VC_UNKNOWN_ACKS", "VC_SYNC_ACKS", "VC_ZERO_ACKS", }; typedef struct _VIRTUAL_CIRCUIT @@ -156,7 +165,7 @@ typedef struct _VIRTUAL_CIRCUIT uint8_t uniqueId[UNIQUE_ID_BYTES]; unsigned long firstHeartbeatMillis; //Time VC link came up unsigned long lastTrafficMillis; //Last time a frame was received - unsigned long lastPingMillis; //Last time a ping was sent or ACK received + unsigned long timerMillis; //Last time the timer was started, after handshake or ACK //Link quality metrics uint32_t framesSent; //myVc --> VC, Total number of frames sent @@ -501,9 +510,9 @@ typedef enum RADIO_CALL_xmitVcDatagram, RADIO_CALL_xmitVcHeartbeat, RADIO_CALL_xmitVcAckFrame, - RADIO_CALL_xmitVcPing, - RADIO_CALL_xmitVcAck1, - RADIO_CALL_xmitVcAck2, + RADIO_CALL_xmitVcUnknownAcks, + RADIO_CALL_xmitVcSyncAcks, + RADIO_CALL_xmitVcZeroAcks, RADIO_CALL_rcvDatagram, RADIO_CALL_transmitDatagram, RADIO_CALL_retransmitDatagram, diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 426cc5fc..8c992a73 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -14,7 +14,7 @@ #define DEBUG_LOCAL_COMMANDS 0 #define DEBUG_PC_TO_RADIO 0 -#define DEBUG_RADIO_TO_PC 0 +#define DEBUG_RADIO_TO_PC 1 #define DISPLAY_DATA_ACK 1 #define DISPLAY_DATA_NACK 1 #define DISPLAY_VC_STATE 0 @@ -305,19 +305,19 @@ void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) printf("-=--=--=- VC %d ALIVE =--=--=-\n", srcVc); break; - case VC_STATE_SEND_PING: + case VC_STATE_SEND_UNKNOWN_ACKS: if (DISPLAY_VC_STATE) - printf("-=--=-- VC %d ALIVE P1 --=--=-\n", srcVc); + printf("-=--=-- VC %d ALIVE UA --=--=-\n", srcVc); break; - case VC_STATE_WAIT_FOR_ACK1: + case VC_STATE_WAIT_SYNC_ACKS: if (DISPLAY_VC_STATE) - printf("-=--=-- VC %d ALIVE WA1 -=--=-\n", srcVc); + printf("-=--=-- VC %d ALIVE SA --=--=-\n", srcVc); break; - case VC_STATE_WAIT_FOR_ACK2: + case VC_STATE_WAIT_ZERO_ACKS: if (DISPLAY_VC_STATE) - printf("-=--=-- VC %d ALIVE WA2 -=--=-\n", srcVc); + printf("-=--=-- VC %d ALIVE ZA --=--=-\n", srcVc); break; case VC_STATE_CONNECTED: @@ -356,7 +356,7 @@ int radioToHost() static uint8_t * dataEnd = outputBuffer; int8_t destAddr; static VC_SERIAL_MESSAGE_HEADER * header = (VC_SERIAL_MESSAGE_HEADER *)outputBuffer; - uint8_t length; + int length; int maxfds; int status; int8_t srcAddr; @@ -413,7 +413,7 @@ int radioToHost() data = dataStart; //Determine if the VC header is in the buffer - if (length < VC_SERIAL_HEADER_BYTES) + if (length < (int)VC_SERIAL_HEADER_BYTES) //Need more data break; From 7c745fe02643526324504edf858e7a172491b71c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 13:26:47 -1000 Subject: [PATCH 330/594] VC: Move valid boolean in flags --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 ++-- Firmware/LoRaSerial_Firmware/NVM.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/settings.h | 8 +++++++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index ea77c2b5..991161c7 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -542,7 +542,7 @@ bool commandAT(const char * commandString) systemPrint("VC "); systemPrint(cmdVc); systemPrint(": "); - if (!vc->valid) + if (!vc->flags.valid) { systemPrint("Down, Not valid @ "); systemPrintTimestamp(millis()); @@ -556,7 +556,7 @@ bool commandAT(const char * commandString) systemPrintln(); systemPrint(" ID: "); systemPrintUniqueID(vc->uniqueId); - systemPrintln(vc->valid ? " (Valid)" : " (Invalid)"); + systemPrintln(vc->flags.valid ? " (Valid)" : " (Invalid)"); //Heartbeat metrics systemPrintln(" Heartbeats"); diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index 188076c8..b53ba4b8 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -137,7 +137,7 @@ void nvmLoadVcUniqueId(int8_t vc) { //The unique ID was set, copy it into the VC structure memcpy(virtualCircuitList[vc].uniqueId, id, sizeof(id)); - virtualCircuitList[vc].valid = true; + virtualCircuitList[vc].flags.valid = true; break; } } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 487ac504..0e34c6fe 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2609,7 +2609,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) { //Verify that an address is present vc = &virtualCircuitList[vcIndex]; - if (!vc->valid) + if (!vc->flags.valid) continue; //Compare the unique ID values @@ -2635,7 +2635,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) for (vcIndex = 0; vcIndex < MAX_VC; vcIndex++) { vc = &virtualCircuitList[vcIndex]; - if (!virtualCircuitList[vcIndex].valid) + if (!virtualCircuitList[vcIndex].flags.valid) break; } if (vcIndex >= MAX_VC) @@ -2648,7 +2648,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) } //Check for an address conflict - if (vc->valid) + if (vc->flags.valid) { systemPrint("ERROR: Unknown ID with pre-assigned conflicting address: "); systemPrintln(srcAddr); @@ -2670,7 +2670,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) nvmSaveVcUniqueId(vcIndex); //Mark this link as up - vc->valid = true; + vc->flags.valid = true; return vcLinkAlive(vcIndex); } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 248156ad..a8937a88 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -160,6 +160,11 @@ const char * const radioDatagramType[] = "VC_UNKNOWN_ACKS", "VC_SYNC_ACKS", "VC_ZERO_ACKS", }; +typedef struct _VC_FLAGS +{ + bool valid : 1; //Unique ID is valid +} VC_FLAGS; + typedef struct _VIRTUAL_CIRCUIT { uint8_t uniqueId[UNIQUE_ID_BYTES]; @@ -176,7 +181,8 @@ typedef struct _VIRTUAL_CIRCUIT uint32_t linkFailures; //myVc <-> VC, Total number of link failures //Link management - bool valid; //Unique ID is valid + + VC_FLAGS flags; uint8_t vcState; //State of VC /* ACK number management From 66629ecf183781ba3cd6c27245c7580785d5b529 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 19 Dec 2022 06:35:25 -1000 Subject: [PATCH 331/594] VC: Display the disconnection status --- Firmware/LoRaSerial_Firmware/States.ino | 12 +++++++++--- Firmware/Tools/VcServerTest.c | 4 ++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 0e34c6fe..82c075f1 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2482,12 +2482,15 @@ void vcChangeState(int8_t vcIndex, uint8_t state) vc = &virtualCircuitList[vcIndex]; if (state != vc->vcState) { - //Set the new state - vc->vcState = state; - //Display the state change if (settings.printLinkUpDown) { + if ((state == VC_STATE_WAIT_ZERO_ACKS) && (vc->vcState == VC_STATE_CONNECTED)) + { + systemPrint("-=-=- VC "); + systemPrint(vcIndex); + systemPrintln(" DISCONNECTED -=-=-"); + } if (state == VC_STATE_CONNECTED) { systemPrint("======= VC "); @@ -2509,6 +2512,9 @@ void vcChangeState(int8_t vcIndex, uint8_t state) outputSerialData(true); } + //Set the new state + vc->vcState = state; + //Determine if the VC is connecting vcBit = 1 << vcIndex; if ((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 8c992a73..820d7dd1 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -317,7 +317,11 @@ void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) case VC_STATE_WAIT_ZERO_ACKS: if (DISPLAY_VC_STATE) + { + if (previousState == VC_STATE_CONNECTED) + printf("-=-=- VC %d DISCONNECTED -=-=-", srcVc); printf("-=--=-- VC %d ALIVE ZA --=--=-\n", srcVc); + } break; case VC_STATE_CONNECTED: From 656fa9cdeb8e1948c762c82a0974e60fea2b2e3e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 19 Dec 2022 06:31:47 -1000 Subject: [PATCH 332/594] VC: Attempt connection if we break the link --- Firmware/LoRaSerial_Firmware/States.ino | 12 ++++++++++-- Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 82c075f1..ca4a7fcc 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1843,6 +1843,9 @@ void updateRadioState() } else { + //HEARTBEATs have not timed out, request to reestablish the connection + vc->flags.wasConnected = true; + //Failed to reach the other system, break the link vcBreakLink(txDestVc); } @@ -1886,7 +1889,7 @@ void updateRadioState() if (vcConnecting & (1 << index)) { //Determine if UNKNOWN_ACKS needs to be sent - if (virtualCircuitList[index].vcState == VC_STATE_SEND_UNKNOWN_ACKS) + if (virtualCircuitList[index].vcState <= VC_STATE_SEND_UNKNOWN_ACKS) { //Send the UNKNOWN_ACKS datagram, first part of the 3-way handshake if (xmitVcUnknownAcks(index)) @@ -2517,10 +2520,15 @@ void vcChangeState(int8_t vcIndex, uint8_t state) //Determine if the VC is connecting vcBit = 1 << vcIndex; - if ((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) + if (((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) + || (vc->flags.wasConnected && (vc->vcState == VC_STATE_LINK_ALIVE))) vcConnecting |= vcBit; else vcConnecting &= ~vcBit; + + //Clear the connection request when connected + if (state == VC_STATE_CONNECTED) + vc->flags.wasConnected = false; } vcSendPcStateMessage(vcIndex, state); } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index a8937a88..04d14d9b 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -163,6 +163,7 @@ const char * const radioDatagramType[] = typedef struct _VC_FLAGS { bool valid : 1; //Unique ID is valid + bool wasConnected : 1; //The VC was previously connected } VC_FLAGS; typedef struct _VIRTUAL_CIRCUIT From 02b497f6d0a3e3c0a6572c30031d9529c0ec8caa Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 19 Dec 2022 16:39:12 -1000 Subject: [PATCH 333/594] Display bytes moved from commandTXBuffer into outgoingPacket --- Firmware/LoRaSerial_Firmware/Serial.ino | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 5ac4b529..30c0c727 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -197,6 +197,14 @@ uint8_t readyOutgoingCommandPacket(uint16_t offset) if (bytesToSend > maxLength) bytesToSend = maxLength; + if (settings.debugSerial) + { + systemPrint("Moving "); + systemPrint(bytesToSend); + systemPrintln(" bytes from commandTXBuffer into outgoingPacket"); + outputSerialData(true); + } + //Determine the number of bytes to send length = 0; if ((commandTXTail + bytesToSend) > sizeof(commandTXBuffer)) From 895c583abc4dac792568027bf47eac0e6361ef95 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 19 Dec 2022 16:42:05 -1000 Subject: [PATCH 334/594] VC: Fix local command processing --- Firmware/LoRaSerial_Firmware/Commands.ino | 15 ++++++++++++--- Firmware/LoRaSerial_Firmware/Serial.ino | 2 ++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 991161c7..9cb26f58 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -159,16 +159,24 @@ bool commandAT(const char * commandString) settings = tempSettings; //Apply user's modifications - if (writeOnCommandExit) recordSystemSettings(); + if (writeOnCommandExit) + { + writeOnCommandExit = false; + recordSystemSettings(); + } //If a setting was applied that requires radio reset, do so if (forceRadioReset) + { + forceRadioReset = false; changeState(RADIO_RESET); + } return true; case ('T'): //ATT - Enter training mode - settings = tempSettings; //Apply user's modifications + if (inCommandMode) + settings = tempSettings; //Apply user's modifications selectTraining(); return true; @@ -179,7 +187,8 @@ bool commandAT(const char * commandString) case ('Z'): //ATZ - Reboots the system if (writeOnCommandExit) { - settings = tempSettings; //Apply user's modifications + if (inCommandMode) + settings = tempSettings; //Apply user's modifications recordSystemSettings(); } diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 30c0c727..dc31408e 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -822,7 +822,9 @@ void vcProcessSerialInput() } //Process this command + tempSettings = settings; checkCommand(); + settings = tempSettings; break; } From 224614f45626ceda0163f05af2a1a390dfac6cb6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 19 Dec 2022 16:43:51 -1000 Subject: [PATCH 335/594] VC: Fix remote command processing --- Firmware/LoRaSerial_Firmware/States.ino | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ca4a7fcc..ee02ae38 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1724,7 +1724,9 @@ void updateRadioState() //Process the command petWDT(); printerEndpoint = PRINT_TO_RF; //Send prints to RF link + tempSettings = settings; checkCommand(); //Parse the command buffer + settings = tempSettings; petWDT(); printerEndpoint = PRINT_TO_SERIAL; length = availableTXCommandBytes(); @@ -2011,7 +2013,9 @@ void updateRadioState() //Process the command petWDT(); printerEndpoint = PRINT_TO_RF; //Send prints to RF link + tempSettings = settings; checkCommand(); //Parse the command buffer + settings = tempSettings; petWDT(); printerEndpoint = PRINT_TO_SERIAL; length = availableTXCommandBytes(); From 3969ea0931d51b5b6bd76635889ef10150c875a8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 19 Dec 2022 16:46:07 -1000 Subject: [PATCH 336/594] VC: Notify the PC of the LoRaSerial reset --- Firmware/LoRaSerial_Firmware/Commands.ino | 8 ++++++++ Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h | 1 + 2 files changed, 9 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9cb26f58..79c6696e 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -193,6 +193,14 @@ bool commandAT(const char * commandString) } reportOK(); + if (serialOperatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Notify the PC of the serial port failure + serialOutputByte(START_OF_VC_SERIAL); + serialOutputByte(3); + serialOutputByte(PC_SERIAL_RECONNECT); + serialOutputByte(myVc); + } outputSerialData(true); systemFlush(); systemReset(); diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 92bafe19..89594c4d 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -34,6 +34,7 @@ #define PC_LINK_STATUS (PC_COMMAND + 1) //Asynchronous link status output #define PC_DATA_ACK (PC_LINK_STATUS + 1)//Indicate data delivery success #define PC_DATA_NACK (PC_DATA_ACK + 1) //Indicate data delivery failure +#define PC_SERIAL_RECONNECT (PC_DATA_NACK + 1) //Disconnect/reconnect the serial port over LoRaSerial CPU reset //Address space 1 and 2 are reserved for the host PC interface to support remote //command processing. The radio removes these bits and converts them to the From 58c4c5b7f1c4fbfc68996b86f3381911245e3cc6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 19 Dec 2022 16:47:43 -1000 Subject: [PATCH 337/594] VC: Disable remote commands in virtual circuit mode --- Firmware/LoRaSerial_Firmware/Commands.ino | 25 ++++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 79c6696e..49224a57 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -23,6 +23,7 @@ typedef bool (* COMMAND_ROUTINE)(const char * commandString); typedef struct { const char * prefix; + bool supportedInVcMode; COMMAND_ROUTINE processCommand; } COMMAND_PREFIX; @@ -677,15 +678,15 @@ bool sendRemoteCommand(const char * commandString) //---------------------------------------- const COMMAND_PREFIX prefixTable[] = { - {"ATD", commandDisplayDebug}, - {"ATP", commandDisplayProbe}, - {"ATR", commandDisplayRadio}, - {"ATS", commandDisplaySerial}, - {"ATV", commandDisplayVirtualCircuit}, - {"AT-?", commandDisplayAll}, - {"AT-", commandSetByName}, - {"AT", commandAT}, - {"RT", sendRemoteCommand}, + {"ATD", 1, commandDisplayDebug}, + {"ATP", 1, commandDisplayProbe}, + {"ATR", 1, commandDisplayRadio}, + {"ATS", 1, commandDisplaySerial}, + {"ATV", 1, commandDisplayVirtualCircuit}, + {"AT-?", 1, commandDisplayAll}, + {"AT-", 1, commandSetByName}, + {"AT", 1, commandAT}, + {"RT", 0, sendRemoteCommand}, }; const int prefixCount = sizeof(prefixTable) / sizeof(prefixTable[0]); @@ -716,6 +717,10 @@ void checkCommand() //Locate the correct processing routine for the command prefix for (index = 0; index < prefixCount; index++) { + //Skip command types not supported in virtual circuit mode + if ((!prefixTable[index].supportedInVcMode) && (settings.operatingMode == MODE_VIRTUAL_CIRCUIT)) + continue; + //Locate the prefix prefixLength = strlen(prefixTable[index].prefix); if (strncmp(commandString, prefixTable[index].prefix, prefixLength) != 0) @@ -1262,7 +1267,7 @@ void displayParameters(char letter, bool displayAll) { petWDT(); //Printing may take longer than WDT at 9600, so pet the WDT. - if (printerEndpoint == PRINT_TO_RF) + if (inCommandMode && (printerEndpoint == PRINT_TO_RF)) systemPrint("R"); //If someone is asking for our settings over RF, respond with 'R' style settings else systemPrint("A"); From 52b63cfa8e81cd05070a56e1642a4b78dc9012d6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 07:48:08 -1000 Subject: [PATCH 338/594] P2P: Update state diagrams --- Firmware/LoRaSerial_Firmware/Radio.ino | 91 +++++++++++++------------ Firmware/LoRaSerial_Firmware/States.ino | 59 ++++++++-------- 2 files changed, 76 insertions(+), 74 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index bf88c906..0cea283e 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -678,55 +678,56 @@ const uint16_t crc16Table[256] = //Point-To-Point: Bring up the link // //A three way handshake is used to get both systems to agree that data can flow in both -//directions. This handshake is also used to synchronize the HOP timer. +//directions. This handshake is also used to synchronize the HOP timer and zero the ACKs. /* - System A System B - - RESET RESET - | | - Channel 0 | | Channel 0 - V V - .----> P2P_NO_LINK P2P_NO_LINK - | | Tx PING | - | Timeout | | - | V | - | P2P_WAIT_TX_PING_DONE | - | | | - | | Tx Complete - - - - - > | Rx PING - | | Start Rx | - | | MAX_PACKET_SIZE | - | V V - `---- P2P_WAIT_ACK_1 +<----------------------. - | | Tx PING ACK1 | - | V | - | P2P_WAIT_TX_ACK_1_DONE | - | | | - Rx PING ACK1 | < - - - - - - - - - - - | Tx Complete | - | | Start Rx | - | | MAX_PACKET_SIZE | - | | | - V V Timeout | - .---------->+ P2P_WAIT_ACK_2 -------------->+ - | TX PING | | ^ - | ACK2 | | | - | V | | - | P2P_WAIT_TX_ACK_2_DONE | | - | | Tx Complete - - - - - > | Rx PING ACK2 | - | Stop | Start HOP timer | Start HOP Timer | Stop - | HOP | Start Rx | Start Rx | HOP - | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | Timer - | | | | - | Rx | | | - | PING ACK V V Rx PING | - `----- P2P_LINK_UP P2P_LINK_UP -----------------’ - | | - | Rx Data | Rx Data - | | - V V + System A System B + + RESET RESET + | | + Channel 0 | | Channel 0 + V V + .----> P2P_NO_LINK P2P_NO_LINK + | | Tx FIND_PARTNER | + | Timeout | | + | V | + | P2P_WAIT_TX_FIND_PARTNER_DONE | + | | | + | | Tx Complete - - - - - > | Rx FIND_PARTNER + | | Start Rx | + | | MAX_PACKET_SIZE | + | V V + `---- P2P_WAIT_SYNC_CLOCKS +<----------------------------. + | | Tx SYNC_CLOCKS | + | V | + | P2P_WAIT_TX_SYNC_CLOCKS_DONE | + | | | + Rx SYNC_CLOCKS | < - - - - - - - - - - - | Tx Complete | + | | Start Rx | + | | MAX_PACKET_SIZE | + | | | + V V Timeout | + .--------------->+ P2P_WAIT_ZERO_ACKS ------------------->+ + | | | ^ + | | TX ZERO_ACKS | | + | | | | + | V | | + | P2P_WAIT_TX_ZERO_ACKS_DONE | | + | | Tx Complete - - - - - > | Rx ZERO_ACKS | + | Stop | Start HOP timer | Start HOP Timer | Stop + | HOP | Start Rx | Start Rx | HOP + | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | Timer + | | Zero ACKs | Zero ACKs | + | Rx | | | + | SYNC_CLOCKS V V Rx FIND_PARTNER | + `---------- P2P_LINK_UP P2P_LINK_UP -----------------------’ + | | + | Rx Data | Rx Data + | | + V V Two timers are in use: datagramTimer: Set at end of transmit, measures ACK timeout - heartbeatTimer: Set upon entry to P2P_NO_LINK, measures time to send next PING + heartbeatTimer: Set upon entry to P2P_NO_LINK, measures time to send next FIND_PARTNER */ //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index ee02ae38..6af03abe 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -157,7 +157,7 @@ void updateRadioState() //Point-To-Point: Bring up the link // //A three way handshake is used to get both systems to agree that data can flow in both - //directions. This handshake is also used to synchronize the HOP timer. + //directions. This handshake is also used to synchronize the HOP timer and zero the ACKs. /* System A System B @@ -165,39 +165,40 @@ void updateRadioState() | | Channel 0 | | Channel 0 V V - .---> P2P_LINK_DOWN P2P_LINK_DOWN - | | Tx PING | + .----> P2P_NO_LINK P2P_NO_LINK + | | Tx FIND_PARTNER | | Timeout | | | V | - | P2P_WAIT_TX_PING_DONE | + | P2P_WAIT_TX_FIND_PARTNER_DONE | | | | - | | Tx Complete - - - - - > | Rx PING + | | Tx Complete - - - - - > | Rx FIND_PARTNER | | Start Rx | | | MAX_PACKET_SIZE | | V V - `---- P2P_WAIT_ACK_1 +<----------------------. - | | Tx PING ACK1 | - | V | - | P2P_WAIT_TX_ACK_1_DONE | - | | | - Rx PING ACK1 | < - - - - - - - - - - - | Tx Complete | - | | Start Rx | - | | MAX_PACKET_SIZE | - | | | - V V Timeout | - .---------->+ P2P_WAIT_ACK_2 -------------->+ - | TX PING | | ^ - | ACK2 | | | - | V | | - | P2P_WAIT_TX_ACK_2_DONE | | - | | Tx Complete - - - - - > | Rx PING ACK2 | - | Stop | Start HOP timer | Start HOP Timer | Stop - | HOP | Start Rx | Start Rx | HOP - | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | Timer - | | | | - | Rx | | | - | PING ACK V V Rx PING | - `----- P2P_LINK_UP P2P_LINK_UP -----------------’ + `---- P2P_WAIT_SYNC_CLOCKS +<----------------------------. + | | Tx SYNC_CLOCKS | + | V | + | P2P_WAIT_TX_SYNC_CLOCKS_DONE | + | | | + Rx SYNC_CLOCKS | < - - - - - - - - - - - | Tx Complete | + | | Start Rx | + | | MAX_PACKET_SIZE | + | | | + V V Timeout | + .--------------->+ P2P_WAIT_ZERO_ACKS ------------------->+ + | | | ^ + | | TX ZERO_ACKS | | + | | | | + | V | | + | P2P_WAIT_TX_ZERO_ACKS_DONE | | + | | Tx Complete - - - - - > | Rx ZERO_ACKS | + | Stop | Start HOP timer | Start HOP Timer | Stop + | HOP | Start Rx | Start Rx | HOP + | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | Timer + | | Zero ACKs | Zero ACKs | + | Rx | | | + | SYNC_CLOCKS V V Rx FIND_PARTNER | + `---------- P2P_LINK_UP P2P_LINK_UP -----------------------’ | | | Rx Data | Rx Data | | @@ -205,7 +206,7 @@ void updateRadioState() Two timers are in use: datagramTimer: Set at end of transmit, measures ACK timeout - heartbeatTimer: Set upon entry to P2P_NO_LINK, measures time to send next PING + heartbeatTimer: Set upon entry to P2P_NO_LINK, measures time to send next FIND_PARTNER */ //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= From d68eaf3643ac9647405005c4240e557f7e4a3245 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 07:57:36 -1000 Subject: [PATCH 339/594] Training: Update state diagrams --- Firmware/LoRaSerial_Firmware/States.ino | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 6af03abe..4fbf6f76 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1215,24 +1215,24 @@ void updateRadioState() V +<--------------------------------. | | - | Send client ping | + | Send FIND_PARTNER | | | V | - RADIO_TRAIN_WAIT_TX_PING_DONE | + RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE | | | V | Timeout RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS -----' | - | Update settings - | Send client ACK + | Save settings + | Send ACK | V - RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE + RADIO_TRAIN_WAIT_TX_ACK_DONE | V endTrainingClientServer | - | Restore settings + | Reboot | V */ @@ -1360,21 +1360,18 @@ void updateRadioState() +<--------------------------------. | | V | - .------ RADIO_TRAIN_WAIT_FOR_PING | + .------ RADIO_TRAIN_WAIT_FOR_FIND_PARTNER | | | | - | | Send client ping | + | | Send RADIO_PARAMS | | | | | V | | RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE -----' | | `---------------. - | Stop training command + | ATZ command | - V - endTrainingClientServer - | - | Restore settings + | Reboot | V */ From 259e4aace4b266a830bb97e1011a21d4c4f67a10 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 08:22:47 -1000 Subject: [PATCH 340/594] P2P: Rename PING to FIND_PARTNER --- .../LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 12 ++-- Firmware/LoRaSerial_Firmware/States.ino | 58 +++++++++---------- Firmware/LoRaSerial_Firmware/settings.h | 18 +++--- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index ee69c33c..caefd96a 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -563,7 +563,7 @@ void loop() updateSerial(); //Store incoming and print outgoing - updateRadioState(); //Ping/ack/send packets as needed + updateRadioState(); //Send packets as needed for handshake, data, remote commands updateLeds(); //Update the LEDs on the board diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 0cea283e..4fa5f74a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -732,9 +732,9 @@ const uint16_t crc16Table[256] = //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //First packet in the three way handshake to bring up the link -bool xmitDatagramP2PPing() +bool xmitDatagramP2PFindPartner() { - radioCallHistory[RADIO_CALL_xmitDatagramP2PPing] = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramP2PFindPartner] = millis(); unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); @@ -751,7 +751,7 @@ bool xmitDatagramP2PPing() +----------+---------+----------+------------+---------+----------+ */ - txControl.datagramType = DATAGRAM_PING; + txControl.datagramType = DATAGRAM_FIND_PARTNER; return (transmitDatagram()); } @@ -1005,7 +1005,7 @@ bool xmitDatagramMpPing() +----------+---------+----------+------------+----------+ */ - txControl.datagramType = DATAGRAM_PING; + txControl.datagramType = DATAGRAM_FIND_PARTNER; return (transmitDatagram()); } @@ -2347,7 +2347,7 @@ bool transmitDatagram() txDatagramSize = MAX_PACKET_SIZE - trailerBytes; //We're now going to transmit a full size datagram break; - case DATAGRAM_PING: + case DATAGRAM_FIND_PARTNER: case DATAGRAM_ACK_1: case DATAGRAM_ACK_2: txDatagramSize = headerBytes + CLOCK_MILLIS_BYTES; //Short packet is 5 + 4 @@ -3054,7 +3054,7 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_calcAirTime, "calcAirTime"}, {RADIO_CALL_xmitDatagramP2PTrainingPing, "xmitDatagramP2PTrainingPing"}, {RADIO_CALL_xmitDatagramP2pTrainingParams, "xmitDatagramP2pTrainingParams"}, - {RADIO_CALL_xmitDatagramP2PPing, "xmitDatagramP2PPing"}, + {RADIO_CALL_xmitDatagramP2PFindPartner, "xmitDatagramP2PFindPartner"}, {RADIO_CALL_xmitDatagramP2PAck1, "xmitDatagramP2PAck1"}, {RADIO_CALL_xmitDatagramP2PAck2, "xmitDatagramP2PAck2"}, {RADIO_CALL_xmitDatagramP2PCommand, "xmitDatagramP2PCommand"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 4fbf6f76..beef88e0 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -108,11 +108,11 @@ void updateRadioState() configureRadio(); //Setup radio, set freq to channel 0, calculate air times - //Start the TX timer: time to delay before transmitting the PING + //Start the TX timer: time to delay before transmitting the FIND_PARTNER setHeartbeatShort(); //Both radios start with short heartbeat period - pingRandomTime = random(ackAirTime, ackAirTime * 2); //Fast ping + pingRandomTime = random(ackAirTime, ackAirTime * 2); //Fast FIND_PARTNER - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive PING packet + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet petWDT(); @@ -219,7 +219,7 @@ void updateRadioState() setRadioFrequency(false); } - //Determine if a PING was received + //Determine if a FIND_PARTNER was received if (transactionComplete) { //Decode the received packet @@ -247,8 +247,8 @@ void updateRadioState() triggerEvent(TRIGGER_CRC_ERROR); break; - case DATAGRAM_PING: - //Received PING + case DATAGRAM_FIND_PARTNER: + //Received FIND_PARTNER //Compute the common clock currentMillis = millis(); memcpy(&clockOffset, rxData, sizeof(currentMillis)); @@ -258,7 +258,7 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - //Acknowledge the PING + //Acknowledge the FIND_PARTNER triggerEvent(TRIGGER_SEND_ACK1); if (xmitDatagramP2PAck1() == true) { @@ -269,24 +269,24 @@ void updateRadioState() } } - //Is it time to send the PING to the remote system + //Is it time to send the FIND_PARTNER to the remote system else if ((receiveInProcess() == false) && ((millis() - heartbeatTimer) >= pingRandomTime)) { - //Transmit the PING - triggerEvent(TRIGGER_HANDSHAKE_SEND_PING); - if (xmitDatagramP2PPing() == true) + //Transmit the FIND_PARTNER + triggerEvent(TRIGGER_HANDSHAKE_SEND_FIND_PARTNER); + if (xmitDatagramP2PFindPartner() == true) { sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK1 to contain millis info - changeState(RADIO_P2P_WAIT_TX_PING_DONE); + changeState(RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE); } } break; - case RADIO_P2P_WAIT_TX_PING_DONE: - //Determine if a PING has completed transmission + case RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE: + //Determine if a FIND_PARTNER has completed transmission if (transactionComplete) { - triggerEvent(TRIGGER_HANDSHAKE_SEND_PING_COMPLETE); + triggerEvent(TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE); transactionComplete = false; //Reset ISR flag returnToReceiving(); changeState(RADIO_P2P_WAIT_ACK_1); @@ -321,8 +321,8 @@ void updateRadioState() triggerEvent(TRIGGER_CRC_ERROR); break; - case DATAGRAM_PING: - //Received PING + case DATAGRAM_FIND_PARTNER: + //Received FIND_PARTNER //Compute the common clock currentMillis = millis(); memcpy(&clockOffset, rxData, sizeof(currentMillis)); @@ -332,7 +332,7 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - //Acknowledge the PING + //Acknowledge the FIND_PARTNER triggerEvent(TRIGGER_SEND_ACK1); if (xmitDatagramP2PAck1() == true) { @@ -374,17 +374,17 @@ void updateRadioState() outputSerialData(true); } - //Start the TX timer: time to delay before transmitting the PING + //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_ACK1_TIMEOUT); setHeartbeatShort(); - //Slow down pings + //Slow down FIND_PARTNERs if (ackAirTime < settings.maxDwellTime) pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); else pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive PING packet + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet returnToReceiving(); changeState(RADIO_P2P_LINK_DOWN); @@ -463,17 +463,17 @@ void updateRadioState() outputSerialData(true); } - //Start the TX timer: time to delay before transmitting the PING + //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_ACK2_TIMEOUT); setHeartbeatShort(); - //Slow down pings + //Slow down FIND_PARTNERs if (ackAirTime < settings.maxDwellTime) pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); else pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive PING packet + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet returnToReceiving(); changeState(RADIO_P2P_LINK_DOWN); @@ -542,7 +542,7 @@ void updateRadioState() System A System B - PING ----> Update timestampOffset + FIND_PARTNER ----> Update timestampOffset Update timestampOffset <---- ACK 1 @@ -632,7 +632,7 @@ void updateRadioState() triggerEvent(TRIGGER_NETID_MISMATCH); break; - case DATAGRAM_PING: + case DATAGRAM_FIND_PARTNER: breakLink(); break; @@ -937,7 +937,7 @@ void updateRadioState() case DATAGRAM_ACK_2: case DATAGRAM_DATA: case DATAGRAM_DATA_ACK: - case DATAGRAM_PING: //Clients do not respond to pings, only the server + case DATAGRAM_FIND_PARTNER: //Clients do not respond to FIND_PARTNER, only the server case DATAGRAM_REMOTE_COMMAND: case DATAGRAM_REMOTE_COMMAND_RESPONSE: //We should not be receiving these datagrams, but if we do, just ignore @@ -1092,11 +1092,11 @@ void updateRadioState() triggerEvent(TRIGGER_BAD_PACKET); break; - case DATAGRAM_PING: + case DATAGRAM_FIND_PARTNER: //A new radio is saying hello if (settings.server == true) { - //Ack their ping with sync data + //Ack their FIND_PARTNER with sync data if (xmitDatagramMpAck() == true) { triggerEvent(TRIGGER_MP_SEND_ACK_FOR_PING); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 04d14d9b..3b9d51e0 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -4,7 +4,7 @@ typedef enum //Point-To-Point: Bring up the link RADIO_P2P_LINK_DOWN, - RADIO_P2P_WAIT_TX_PING_DONE, + RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE, RADIO_P2P_WAIT_ACK_1, RADIO_P2P_WAIT_TX_ACK_1_DONE, RADIO_P2P_WAIT_ACK_2, @@ -59,8 +59,8 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Point-to-Point link handshake // State RX Name Description - {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "P2P: [No Link] Waiting for Ping"}, // 1 - {RADIO_P2P_WAIT_TX_PING_DONE, 0, "P2P_WAIT_TX_PING_DONE", "P2P: [No Link] Wait Ping TX Done"},// 2 + {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "P2P: [No Link] Waiting for FIND_PARTNER"}, // 1 + {RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE, 0, "P2P_WAIT_TX_FIND_PARTNER_DONE", "P2P: [No Link] Wait FIND_PARTNER TX Done"},// 2 {RADIO_P2P_WAIT_ACK_1, 1, "P2P_WAIT_ACK_1", "P2P: [No Link] Waiting for ACK1"}, // 3 {RADIO_P2P_WAIT_TX_ACK_1_DONE, 0, "P2P_WAIT_TX_ACK_1_DONE", "P2P: [No Link] Wait ACK1 TX Done"},// 4 {RADIO_P2P_WAIT_ACK_2, 1, "P2P_WAIT_ACK_2", "P2P: [No Link] Waiting for ACK2"}, // 5 @@ -107,7 +107,7 @@ typedef enum //Sync frequencies, HEARTBEAT timing and zero ACKs //P2P: Between the two LoRaSerial radios //VC: Between the server radio and a client radio - DATAGRAM_PING = 0, // 0 + DATAGRAM_FIND_PARTNER = 0, // 0 DATAGRAM_ACK_1, // 1 DATAGRAM_ACK_2, // 2 @@ -146,8 +146,8 @@ typedef enum } PacketType; const char * const radioDatagramType[] = -{ // 0 1 2 - "PING", "ACK-1", "ACK-2", +{ // 0 1 2 + "FIND_PARTNER", "ACK-1", "ACK-2", // 3 4 5 6 7 "DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", // 8 @@ -263,8 +263,8 @@ enum TRIGGER_MP_SEND_ACK_FOR_PING, TRIGGER_TRANSMIT_CANCELED, TRIGGER_HANDSHAKE_ACK1_TIMEOUT, - TRIGGER_HANDSHAKE_SEND_PING, - TRIGGER_HANDSHAKE_SEND_PING_COMPLETE, + TRIGGER_HANDSHAKE_SEND_FIND_PARTNER, + TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE, TRIGGER_HANDSHAKE_SEND_ACK1_COMPLETE, TRIGGER_SEND_ACK1, TRIGGER_SEND_ACK2, @@ -499,7 +499,7 @@ typedef enum RADIO_CALL_calcAirTime, RADIO_CALL_xmitDatagramP2PTrainingPing, RADIO_CALL_xmitDatagramP2pTrainingParams, - RADIO_CALL_xmitDatagramP2PPing, + RADIO_CALL_xmitDatagramP2PFindPartner, RADIO_CALL_xmitDatagramP2PAck1, RADIO_CALL_xmitDatagramP2PAck2, RADIO_CALL_xmitDatagramP2PCommand, From fd2ed85d27a39a6ffe560afb40dfa09696d62749 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 08:26:37 -1000 Subject: [PATCH 341/594] Rename pingRandomTime to randomTime --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index caefd96a..636d9b7d 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -431,7 +431,7 @@ uint8_t txDatagramSize; //Point-to-Point unsigned long datagramTimer; -uint16_t pingRandomTime; +uint16_t randomTime; uint16_t heartbeatRandomTime; //Receive control diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index beef88e0..58cb2925 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -110,7 +110,7 @@ void updateRadioState() //Start the TX timer: time to delay before transmitting the FIND_PARTNER setHeartbeatShort(); //Both radios start with short heartbeat period - pingRandomTime = random(ackAirTime, ackAirTime * 2); //Fast FIND_PARTNER + randomTime = random(ackAirTime, ackAirTime * 2); //Fast FIND_PARTNER sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet @@ -270,7 +270,7 @@ void updateRadioState() } //Is it time to send the FIND_PARTNER to the remote system - else if ((receiveInProcess() == false) && ((millis() - heartbeatTimer) >= pingRandomTime)) + else if ((receiveInProcess() == false) && ((millis() - heartbeatTimer) >= randomTime)) { //Transmit the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_SEND_FIND_PARTNER); @@ -380,9 +380,9 @@ void updateRadioState() //Slow down FIND_PARTNERs if (ackAirTime < settings.maxDwellTime) - pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); + randomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); else - pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); + randomTime = random(ackAirTime * 4, ackAirTime * 8); sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet returnToReceiving(); @@ -469,9 +469,9 @@ void updateRadioState() //Slow down FIND_PARTNERs if (ackAirTime < settings.maxDwellTime) - pingRandomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); + randomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); else - pingRandomTime = random(ackAirTime * 4, ackAirTime * 8); + randomTime = random(ackAirTime * 4, ackAirTime * 8); sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet returnToReceiving(); From db804c40eec317225ad4b89852c5ae2d68c06fae Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 08:33:56 -1000 Subject: [PATCH 342/594] Rename clientPingRetryInterval to clientFindPartnerRetryInterval --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 4 ++-- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 49224a57..1097e92a 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1037,7 +1037,7 @@ const COMMAND_ENTRY commands[] = /*Training parameters Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ - {'R', 0, 1, 1, 255, 0, TYPE_U8, valInt, "ClientPingRetryInterval", &tempSettings.clientPingRetryInterval}, + {'R', 0, 1, 1, 255, 0, TYPE_U8, valInt,"ClientFindPartnerRetryInterval",&tempSettings.clientFindPartnerRetryInterval}, {'R', 0, 0, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &tempSettings.trainingKey}, {'R', 0, 0, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &tempSettings.trainingTimeout}, diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4fa5f74a..96dde50d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -314,7 +314,7 @@ void generateHopTable() + settings.verifyRxNetID + settings.overheadTime + settings.enableCRC16 - + settings.clientPingRetryInterval; + + settings.clientFindPartnerRetryInterval; if (settings.encryptData == true) { @@ -1145,7 +1145,7 @@ void updateRadioParameters(uint8_t * rxData) } //Update the training values - tempSettings.clientPingRetryInterval = params.clientPingRetryInterval; + tempSettings.clientFindPartnerRetryInterval = params.clientFindPartnerRetryInterval; //The trainingKey is already the same tempSettings.trainingTimeout = params.trainingTimeout; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 58cb2925..8044f56b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1311,7 +1311,7 @@ void updateRadioState() } //Check for a receive timeout - else if ((millis() - datagramTimer) > (settings.clientPingRetryInterval * 1000)) + else if ((millis() - datagramTimer) > (settings.clientFindPartnerRetryInterval * 1000)) { //If we are training with button, in P2P mode, and user has not set server mode //Automatically switch to server diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 3b9d51e0..4a19cdcd 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -416,7 +416,7 @@ typedef struct struct_settings { bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive bool displayRealMillis = false; //true = Display the millis value without offset, false = use offset bool server = false; //Default to being a client, enable server for multipoint, VC and training - uint8_t clientPingRetryInterval = 3; //Number of seconds before retransmiting the client PING + uint8_t clientFindPartnerRetryInterval = 3; //Number of seconds before retransmiting the client FIND_PARTNER bool copyDebug = false; //Copy the debug parameters to the training client bool copySerial = false; //Copy the serial parameters to the training client bool copyTriggers = false; //Copy the trigger parameters to the training client From 943413ce4c6fb29edf748e00f0789e29cd18d18e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 08:52:45 -1000 Subject: [PATCH 343/594] Training: Rename PING to FIND_PARTNER --- Firmware/LoRaSerial_Firmware/Radio.ino | 12 +++++------- Firmware/LoRaSerial_Firmware/States.ino | 16 +++++++-------- Firmware/LoRaSerial_Firmware/Train.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/settings.h | 26 ++++++++++++------------- 4 files changed, 29 insertions(+), 33 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 96dde50d..3ced5d9d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1013,10 +1013,10 @@ bool xmitDatagramMpPing() //Multi-Point Client Training //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//Build the client ping packet used for training -bool xmitDatagramTrainingPing() +//Build the FIND_PARTNER packet used for training +bool xmitDatagramTrainingFindPartner() { - radioCallHistory[RADIO_CALL_xmitDatagramTrainingPing] = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramTrainingFindPartner] = millis(); //Add the source (server) ID memcpy(endOfTxData, myUniqueId, UNIQUE_ID_BYTES); @@ -1033,7 +1033,7 @@ bool xmitDatagramTrainingPing() +----------+---------+----------+------------+-----------+----------+ */ - txControl.datagramType = DATAGRAM_TRAINING_PING; + txControl.datagramType = DATAGRAM_TRAINING_FIND_PARTNER; return (transmitDatagram()); } @@ -3052,8 +3052,6 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_setRadioFrequency, "setRadioFrequency"}, {RADIO_CALL_returnToReceiving, "returnToReceiving"}, {RADIO_CALL_calcAirTime, "calcAirTime"}, - {RADIO_CALL_xmitDatagramP2PTrainingPing, "xmitDatagramP2PTrainingPing"}, - {RADIO_CALL_xmitDatagramP2pTrainingParams, "xmitDatagramP2pTrainingParams"}, {RADIO_CALL_xmitDatagramP2PFindPartner, "xmitDatagramP2PFindPartner"}, {RADIO_CALL_xmitDatagramP2PAck1, "xmitDatagramP2PAck1"}, {RADIO_CALL_xmitDatagramP2PAck2, "xmitDatagramP2PAck2"}, @@ -3066,7 +3064,7 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_xmitDatagramMpHeartbeat, "xmitDatagramMpHeartbeat"}, {RADIO_CALL_xmitDatagramMpAck, "xmitDatagramMpAck"}, {RADIO_CALL_xmitDatagramMpPing, "xmitDatagramMpPing"}, - {RADIO_CALL_xmitDatagramTrainingPing, "xmitDatagramTrainingPing"}, + {RADIO_CALL_xmitDatagramTrainingFindPartner, "xmitDatagramTrainingFindPartner"}, {RADIO_CALL_xmitDatagramTrainingAck, "xmitDatagramTrainingAck"}, {RADIO_CALL_xmitDatagramTrainRadioParameters, "xmitDatagramTrainRadioParameters"}, {RADIO_CALL_xmitVcDatagram, "xmitVcDatagram"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 8044f56b..c97fcda2 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1238,9 +1238,9 @@ void updateRadioState() */ //==================== - //Wait for the PING to complete transmission + //Wait for the FIND_PARTNER to complete transmission //==================== - case RADIO_TRAIN_WAIT_TX_PING_DONE: + case RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE: //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) @@ -1248,7 +1248,7 @@ void updateRadioState() transactionComplete = false; //Indicate that the receive is complete - triggerEvent(TRIGGER_TRAINING_CLIENT_TX_PING_DONE); + triggerEvent(TRIGGER_TRAINING_CLIENT_TX_FIND_PARTNER_DONE); //Start the receive operation returnToReceiving(); @@ -1329,7 +1329,7 @@ void updateRadioState() } else { - xmitDatagramTrainingPing(); //Continue retrying as client + xmitDatagramTrainingFindPartner(); //Continue retrying as client } } break; @@ -1377,9 +1377,9 @@ void updateRadioState() */ //==================== - //Wait for a PING frame from a client + //Wait for a FIND_PARTNER frame from a client //==================== - case RADIO_TRAIN_WAIT_FOR_PING: + case RADIO_TRAIN_WAIT_FOR_FIND_PARTNER: //If dio0ISR has fired, a packet has arrived if (transactionComplete == true) @@ -1396,7 +1396,7 @@ void updateRadioState() triggerEvent(TRIGGER_BAD_PACKET); break; - case DATAGRAM_TRAINING_PING: + case DATAGRAM_TRAINING_FIND_PARTNER: //Save the client ID memcpy(trainingPartnerID, rxData, UNIQUE_ID_BYTES); @@ -1459,7 +1459,7 @@ void updateRadioState() returnToReceiving(); //Set the next state - changeState(RADIO_TRAIN_WAIT_FOR_PING); + changeState(RADIO_TRAIN_WAIT_FOR_FIND_PARTNER); } break; diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 634c0ebb..2b0f046b 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -62,10 +62,10 @@ void beginTrainingClient() //Common initialization commonTrainingInitialization(); - //Transmit client ping to the training server - if (xmitDatagramTrainingPing() == true) + //Transmit FIND_PARTNER to the training server + if (xmitDatagramTrainingFindPartner() == true) //Set the next state - changeState(RADIO_TRAIN_WAIT_TX_PING_DONE); + changeState(RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE); else changeState(RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS); trainingTimer = millis(); @@ -104,7 +104,7 @@ void beginTrainingServer() returnToReceiving(); //Set the next state - changeState(RADIO_TRAIN_WAIT_FOR_PING); + changeState(RADIO_TRAIN_WAIT_FOR_FIND_PARTNER); } //Perform the common training initialization diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 4a19cdcd..24a11655 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -25,12 +25,12 @@ typedef enum RADIO_MP_WAIT_TX_DONE, //Training client states - RADIO_TRAIN_WAIT_TX_PING_DONE, + RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE, RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, //Training server states - RADIO_TRAIN_WAIT_FOR_PING, + RADIO_TRAIN_WAIT_FOR_FIND_PARTNER, RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE, //Virtual-Circuit states @@ -85,14 +85,14 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Training client states // State RX Name Description - {RADIO_TRAIN_WAIT_TX_PING_DONE, 0, "TRAIN_WAIT_TX_PING_DONE", "Train: Wait TX training PING done"}, //15 - {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //16 - {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //17 + {RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE,0, "TRAIN_WAIT_TX_FIND_PARTNER_DONE","Train: Wait TX training FIND_PARTNER done"}, //15 + {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //16 + {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //17 //Training server states // State RX Name Description - {RADIO_TRAIN_WAIT_FOR_PING, 1, "TRAIN_WAIT_FOR_PING", "Train: Wait for training PING"}, //18 - {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //19 + {RADIO_TRAIN_WAIT_FOR_FIND_PARTNER, 1, "TRAIN_WAIT_FOR_FIND_PARTNER", "Train: Wait for training FIND_PARTNER"}, //18 + {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //19 //Virtual circuit states // State RX Name Description @@ -122,7 +122,7 @@ typedef enum DATAGRAM_DATAGRAM, // 8 //Multi-Point training exchange - DATAGRAM_TRAINING_PING, // 9 + DATAGRAM_TRAINING_FIND_PARTNER, // 9 DATAGRAM_TRAINING_PARAMS, //10 DATAGRAM_TRAINING_ACK, //11 @@ -152,8 +152,8 @@ const char * const radioDatagramType[] = "DATA", "DATA-ACK", "HEARTBEAT", "RMT-CMD", "RMT_RESP", // 8 "DATAGRAM", - // 9 10 11 - "TRAINING_PING", "TRAINING_PARAMS", "TRAINING_ACK", + // 9 10 11 + "TRAINING_FIND_PARTNER", "TRAINING_PARAMS", "TRAINING_ACK", // 12 "VC_HEARTBEAT", // 13 14 15 @@ -286,7 +286,7 @@ enum TRIGGER_TRAINING_CONTROL_PACKET, TRIGGER_TRAINING_DATA_PACKET, TRIGGER_TRAINING_NO_ACK, - TRIGGER_TRAINING_CLIENT_TX_PING_DONE, + TRIGGER_TRAINING_CLIENT_TX_FIND_PARTNER_DONE, TRIGGER_TRAINING_CLIENT_RX_PARAMS, TRIGGER_TRAINING_CLIENT_TX_ACK_DONE, TRIGGER_TRAINING_SERVER_RX, @@ -497,8 +497,6 @@ typedef enum RADIO_CALL_setRadioFrequency, RADIO_CALL_returnToReceiving, RADIO_CALL_calcAirTime, - RADIO_CALL_xmitDatagramP2PTrainingPing, - RADIO_CALL_xmitDatagramP2pTrainingParams, RADIO_CALL_xmitDatagramP2PFindPartner, RADIO_CALL_xmitDatagramP2PAck1, RADIO_CALL_xmitDatagramP2PAck2, @@ -511,7 +509,7 @@ typedef enum RADIO_CALL_xmitDatagramMpHeartbeat, RADIO_CALL_xmitDatagramMpAck, RADIO_CALL_xmitDatagramMpPing, - RADIO_CALL_xmitDatagramTrainingPing, + RADIO_CALL_xmitDatagramTrainingFindPartner, RADIO_CALL_xmitDatagramTrainingAck, RADIO_CALL_xmitDatagramTrainRadioParameters, RADIO_CALL_xmitVcDatagram, From 023668b7371df8d26754d3cdb7164c44fe21ad66 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 18 Dec 2022 09:03:17 -1000 Subject: [PATCH 344/594] Multi-Point: Rename PING to FIND_PARTNER --- Firmware/LoRaSerial_Firmware/Radio.ino | 10 +++++----- Firmware/LoRaSerial_Firmware/States.ino | 14 +++++++------- Firmware/LoRaSerial_Firmware/settings.h | 14 +++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 3ced5d9d..b837529c 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -955,7 +955,7 @@ bool xmitDatagramMpHeartbeat() return (transmitDatagram()); } -//ACK packet sent by server in response the client ping, includes channel number +//ACK packet sent by server in response the FIND_PARTNER, includes channel number //During discovery scanning, it's possible for the client to get an ACK but be on an adjacent channel //The channel number ensures that the client gets the next hop correct bool xmitDatagramMpAck() @@ -989,10 +989,10 @@ bool xmitDatagramMpAck() return (transmitDatagram()); } -//Ping packet sent during scanning -bool xmitDatagramMpPing() +//FIND_PARTNER packet sent during scanning +bool xmitDatagramMpFindPartner() { - radioCallHistory[RADIO_CALL_xmitDatagramMpPing] = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramMpFindPartner] = millis(); /* endOfTxData ---. @@ -3063,7 +3063,7 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_xmitDatagramMpData, "xmitDatagramMpData"}, {RADIO_CALL_xmitDatagramMpHeartbeat, "xmitDatagramMpHeartbeat"}, {RADIO_CALL_xmitDatagramMpAck, "xmitDatagramMpAck"}, - {RADIO_CALL_xmitDatagramMpPing, "xmitDatagramMpPing"}, + {RADIO_CALL_xmitDatagramMpFindPartner, "xmitDatagramMpFindPartner"}, {RADIO_CALL_xmitDatagramTrainingFindPartner, "xmitDatagramTrainingFindPartner"}, {RADIO_CALL_xmitDatagramTrainingAck, "xmitDatagramTrainingAck"}, {RADIO_CALL_xmitDatagramTrainRadioParameters, "xmitDatagramTrainRadioParameters"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c97fcda2..cacc8ea4 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -899,7 +899,7 @@ void updateRadioState() break; //==================== - //Walk through channel table backwards, transmitting a Ping and looking for an ACK1 + //Walk through channel table backwards, transmitting a FIND_PARTNER and looking for an ACK1 //==================== case RADIO_DISCOVER_SCANNING: if (transactionComplete) @@ -1002,18 +1002,18 @@ void updateRadioState() } } - //Send ping - if (xmitDatagramMpPing() == true) - changeState(RADIO_DISCOVER_WAIT_TX_PING_DONE); + //Send FIND_PARTNER + if (xmitDatagramMpFindPartner() == true) + changeState(RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE); } } break; //==================== - //Wait for the PING to complete transmission + //Wait for the FIND_PARTNER to complete transmission //==================== - case RADIO_DISCOVER_WAIT_TX_PING_DONE: + case RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE: if (transactionComplete) { transactionComplete = false; //Reset ISR flag @@ -1099,7 +1099,7 @@ void updateRadioState() //Ack their FIND_PARTNER with sync data if (xmitDatagramMpAck() == true) { - triggerEvent(TRIGGER_MP_SEND_ACK_FOR_PING); + triggerEvent(TRIGGER_MP_SEND_ACK_FOR_FIND_PARTNER); changeState(RADIO_MP_WAIT_TX_ACK_DONE); } } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 24a11655..2036352d 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -17,7 +17,7 @@ typedef enum //Server-client discovery RADIO_DISCOVER_BEGIN, RADIO_DISCOVER_SCANNING, - RADIO_DISCOVER_WAIT_TX_PING_DONE, + RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE, //Multi-Point: Datagrams RADIO_MP_WAIT_TX_ACK_DONE, @@ -73,9 +73,9 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Server-client discovery // State RX Name Description - {RADIO_DISCOVER_BEGIN, 0, "DISCOVER_BEGIN", "Disc: Setup for scanning"}, // 9 - {RADIO_DISCOVER_SCANNING, 0, "DISCOVER_SCANNING", "Disc: Scanning for servers"}, //10 - {RADIO_DISCOVER_WAIT_TX_PING_DONE, 0, "DISCOVER_WAIT_TX_PING_DONE", "Disc: Wait for ping to xmit"}, //11 + {RADIO_DISCOVER_BEGIN, 0, "DISCOVER_BEGIN", "Disc: Setup for scanning"}, // 9 + {RADIO_DISCOVER_SCANNING, 0, "DISCOVER_SCANNING", "Disc: Scanning for servers"}, //10 + {RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE,0,"DISCOVER_WAIT_TX_FIND_PARTNER_DONE","Disc: Wait for FIND_PARTNER to xmit"}, //11 //Multi-Point data exchange // State RX Name Description @@ -260,7 +260,7 @@ enum TRIGGER_MP_SCAN, TRIGGER_MP_DATA_PACKET, TRIGGER_MP_PACKET_RECEIVED, - TRIGGER_MP_SEND_ACK_FOR_PING, + TRIGGER_MP_SEND_ACK_FOR_FIND_PARTNER, TRIGGER_TRANSMIT_CANCELED, TRIGGER_HANDSHAKE_ACK1_TIMEOUT, TRIGGER_HANDSHAKE_SEND_FIND_PARTNER, @@ -385,7 +385,7 @@ typedef struct struct_settings { uint16_t serialTimeoutBeforeSendingFrame_ms = 50; //Send partial buffer if time expires bool debug = false; //Print basic events: ie, radio state changes bool echo = false; //Print locally inputted serial - uint16_t heartbeatTimeout = 5000; //ms before sending ping to see if link is active + uint16_t heartbeatTimeout = 5000; //ms before sending HEARTBEAT to see if link is active bool flowControl = false; //Enable the use of CTS/RTS flow control signals bool autoTuneFrequency = false; //Based on the last packets frequency error, adjust our next transaction frequency bool printPacketQuality = false; //Print RSSI, SNR, and freqError for received packets @@ -508,7 +508,7 @@ typedef enum RADIO_CALL_xmitDatagramMpData, RADIO_CALL_xmitDatagramMpHeartbeat, RADIO_CALL_xmitDatagramMpAck, - RADIO_CALL_xmitDatagramMpPing, + RADIO_CALL_xmitDatagramMpFindPartner, RADIO_CALL_xmitDatagramTrainingFindPartner, RADIO_CALL_xmitDatagramTrainingAck, RADIO_CALL_xmitDatagramTrainRadioParameters, From 71cc192d324bb187fa825149db682489179c9346 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 19 Dec 2022 18:07:29 -1000 Subject: [PATCH 345/594] Fix the macro that sets the ackTimer Fix bug when ackTimer is zero and a comparison is done immediately. millis() = 0 ackTimer = 1 - ___ -1 = 0xFFFFFFFF If the resulting value is treated as an unsigned number it is larger than all of the timeout values. Fix: millis() = 0 ackTimer = -1 (0xFFFFFFFF) - ____ 1 This result value reduces the expected timeout value by one millisecond which is significantly less than all of the timeouts. --- Firmware/LoRaSerial_Firmware/States.ino | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index cacc8ea4..76ff22e7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -37,8 +37,9 @@ ackTimer = datagramTimer; \ \ /*Since ackTimer is off when equal to zero, force it to a non-zero value*/ \ + /*Subtract one so that the comparisons result in a small number*/ \ if (!ackTimer) \ - ackTimer = 1; \ + ackTimer -= 1; \ } #define STOP_ACK_TIMER() \ From c3d35991b1499b244432f99c58e2b3abcf53c4fe Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 10:23:21 -1000 Subject: [PATCH 346/594] ATF command now clears the unique ID table in NVM --- Firmware/LoRaSerial_Firmware/Commands.ino | 5 +++ Firmware/LoRaSerial_Firmware/NVM.ino | 49 +++++++++++++++++------ 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 1097e92a..198e7e5c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -39,6 +39,7 @@ bool commandAT(const char * commandString) const char * string; unsigned long timer; VIRTUAL_CIRCUIT * vc = &virtualCircuitList[cmdVc]; + uint8_t vcIndex; //'AT' if (commandLength == 2) @@ -135,6 +136,10 @@ bool commandAT(const char * commandString) forceRadioReset = true; recordSystemSettings(); + + //Clear the unique ID table + for (vcIndex = 0; vcIndex < MAX_VC; vcIndex++) + nvmEraseUniqueId(vcIndex); return true; case ('G'): //ATG - Generate a new netID and encryption key diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index b53ba4b8..cee522ad 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -100,22 +100,27 @@ void recordSystemSettings() arch.eepromCommit(); } -//Erase the EEPROM -void eepromErase() +int nvmVcOffset(int8_t vc) { - if (settings.debugNvm) - { - systemPrintln("Erasing the EEPROM"); - outputSerialData(true); - } - for (uint16_t i = 0 ; i < EEPROM.length() ; i++) - { - petWDT(); + return NVM_UNIQUE_ID_OFFSET + (vc * UNIQUE_ID_BYTES); +} - EEPROM.write(i, 0); +//Erase the specified unique ID +void nvmEraseUniqueId(int8_t vc) +{ + uint8_t id[UNIQUE_ID_BYTES]; + int index; - arch.eepromCommit(); - } + //Set the erase value + for (index = 0; index < sizeof(id); index++) + id[index] = NVM_ERASE_VALUE; + + //Erase this portion of the NVM + nvmSaveUniqueId(vc, id); + + //Invalidate the VC structure + memcpy(virtualCircuitList[vc].uniqueId, id, sizeof(id)); + virtualCircuitList[vc].flags.valid = false; } //Copy the unique ID for the VC from NVM into the virtualCircuitList entry @@ -143,6 +148,24 @@ void nvmLoadVcUniqueId(int8_t vc) } } +//Save the unique ID into the NVM +void nvmSaveUniqueId(int8_t vc, uint8_t * uniqueId) +{ + uint8_t id[UNIQUE_ID_BYTES]; + int index; + + //Get the unique ID value + memcpy(id, uniqueId, sizeof(id)); + + //Write the ID into the flash + EEPROM.put(nvmVcOffset(vc), id); + arch.eepromCommit(); + + //Place the address in the VC structure + memcpy(virtualCircuitList[vc].uniqueId, id, sizeof(id)); + virtualCircuitList[vc].flags.valid = true; +} + //Save the unique ID from the virtualCircuitList entry into the NVM void nvmSaveVcUniqueId(int8_t vc) { From e87527542abf0f004277abbafde931431df7f091 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 10:32:44 -1000 Subject: [PATCH 347/594] ATI15 - Add command to display the NVM unique ID table --- Firmware/LoRaSerial_Firmware/Commands.ino | 22 +++++++++++++++++++ Firmware/LoRaSerial_Firmware/NVM.ino | 26 +++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 198e7e5c..43c535ae 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -36,6 +36,7 @@ bool commandAT(const char * commandString) { uint32_t delayMillis; long deltaMillis; + uint8_t id[UNIQUE_ID_BYTES]; const char * string; unsigned long timer; VIRTUAL_CIRCUIT * vc = &virtualCircuitList[cmdVc]; @@ -238,6 +239,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATI12 - Display the VC details"); systemPrintln(" ATI13 - Display the SX1276 registers"); systemPrintln(" ATI14 - Dump the radioTxBuffer"); + systemPrintln(" ATI15 - Dump the NVM unique ID table"); return true; case ('0'): //ATI0 - Show user settable parameters @@ -654,6 +656,26 @@ bool commandAT(const char * commandString) systemPrintln("radioTxBuffer:"); dumpCircularBuffer(radioTxBuffer, radioTxTail, sizeof(radioTxBuffer), availableRadioTXBytes()); return true; + + case ('5'): //ATI15 - Dump the NVM unique ID table + systemPrintln("NVM Unique ID Table"); + for (vcIndex = 0; vcIndex < MAX_VC; vcIndex++) + { + systemPrint(" "); + if (vcIndex < 10) + systemWrite(' '); + systemPrint(vcIndex); + systemPrint(": "); + if (nvmIsVcUniqueIdSet(vcIndex)) + { + nvmGetUniqueId(vcIndex, id); + systemPrintUniqueID(id); + systemPrintln(); + } + else + systemPrintln("Empty"); + } + return true; } } diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index cee522ad..30b62504 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -123,6 +123,32 @@ void nvmEraseUniqueId(int8_t vc) virtualCircuitList[vc].flags.valid = false; } +//Get the unique ID for the VC from NVM +void nvmGetUniqueId(int8_t vc, uint8_t * uniqueId) +{ + uint8_t id[UNIQUE_ID_BYTES]; + + //Read the unique ID from the flash + EEPROM.get(nvmVcOffset(vc), id); + memcpy(uniqueId, id, UNIQUE_ID_BYTES); +} + +//Determine if the unique ID is set +bool nvmIsVcUniqueIdSet(int8_t vc) +{ + uint8_t id[UNIQUE_ID_BYTES]; + int index; + + //Read the ID from the flash + nvmGetUniqueId(vc, id); + + //Determine if a unique ID is set in the flash + for (index = 0; index < sizeof(id); index++) + if (id[index] != NVM_ERASE_VALUE) + return true; + return false; +} + //Copy the unique ID for the VC from NVM into the virtualCircuitList entry void nvmLoadVcUniqueId(int8_t vc) { From a0434249b501dc201494f29d5d9a75f7b55faa48 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 10:34:13 -1000 Subject: [PATCH 348/594] Layer the NVM routines --- Firmware/LoRaSerial_Firmware/NVM.ino | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial_Firmware/NVM.ino index 30b62504..46460446 100644 --- a/Firmware/LoRaSerial_Firmware/NVM.ino +++ b/Firmware/LoRaSerial_Firmware/NVM.ino @@ -154,23 +154,16 @@ void nvmLoadVcUniqueId(int8_t vc) { uint8_t id[UNIQUE_ID_BYTES]; int index; - int offset; //Read the ID from the flash - offset = NVM_UNIQUE_ID_OFFSET + (vc * UNIQUE_ID_BYTES); - EEPROM.get(offset, id); + nvmGetUniqueId(vc, id); - //Verify that the value was saved - for (index = 0; index < sizeof(id); index++) + //Update the VC when a unique ID is in the NVM + if (nvmIsVcUniqueIdSet(vc)) { - //Determine if this entry was set to a value - if (id[index] != NVM_ERASE_VALUE) - { - //The unique ID was set, copy it into the VC structure - memcpy(virtualCircuitList[vc].uniqueId, id, sizeof(id)); - virtualCircuitList[vc].flags.valid = true; - break; - } + //The unique ID was set, copy it into the VC structure + memcpy(virtualCircuitList[vc].uniqueId, id, sizeof(id)); + virtualCircuitList[vc].flags.valid = true; } } @@ -197,16 +190,11 @@ void nvmSaveVcUniqueId(int8_t vc) { uint8_t id[UNIQUE_ID_BYTES]; int index; - int offset; //Read the ID from the flash - offset = NVM_UNIQUE_ID_OFFSET + (vc * UNIQUE_ID_BYTES); - EEPROM.get(offset, id); + nvmGetUniqueId(vc, id); //Write the ID into the flash if (memcmp(id, virtualCircuitList[vc].uniqueId, sizeof(id)) != 0) - { - EEPROM.put(offset, virtualCircuitList[vc].uniqueId); - arch.eepromCommit(); - } + nvmSaveUniqueId(vc, virtualCircuitList[vc].uniqueId); } From 186425f5377a7fc797c3ba40408ca430a5e195b1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 10:36:44 -1000 Subject: [PATCH 349/594] Enter the server's unique ID into the NVM table when server set 0 --> 1 --- Firmware/LoRaSerial_Firmware/Commands.ino | 26 ++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 43c535ae..a2e07547 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -946,6 +946,30 @@ bool valOverride (void * value, uint32_t valMin, uint32_t valMax) return ((settingValue >= valMin) && (settingValue <= valMax)); } +//Validate the server value +bool valServer (void * value, uint32_t valMin, uint32_t valMax) +{ + uint32_t settingValue = *(uint32_t *)value; + bool valid; + int8_t vcIndex; + + //Validate the value + valid = valInt(value, valMin, valMax); + + //Check for a change from client to server + if (valid && (!settings.server) && settingValue) + { + //Clear the unique ID table in NVM + for (vcIndex = 1; vcIndex < MAX_VC; vcIndex++) + if (nvmIsVcUniqueIdSet(vcIndex)) + nvmEraseUniqueId(vcIndex); + + //Set our address as the server address + nvmSaveUniqueId(VC_SERVER, myUniqueId); + } + return valid; +} + //Validate the AirSpeed value bool valSpeedAir (void * value, uint32_t valMin, uint32_t valMax) { @@ -1049,7 +1073,7 @@ const COMMAND_ENTRY commands[] = {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "NetID", &tempSettings.netID}, {'R', 0, 1, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &tempSettings.operatingMode}, {'R', 0, 1, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &tempSettings.overheadTime}, - {'R', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "Server", &tempSettings.server}, + {'R', 0, 0, 0, 1, 0, TYPE_BOOL, valServer, "Server", &tempSettings.server}, {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &tempSettings.verifyRxNetID}, /*Serial parameters From cb94655db5beab3e4ca2697379e44fc664ab4285 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 10:38:25 -1000 Subject: [PATCH 350/594] Enter client ID into NVM unique ID table during training --- Firmware/LoRaSerial_Firmware/States.ino | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 76ff22e7..4228d7c4 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1401,6 +1401,15 @@ void updateRadioState() //Save the client ID memcpy(trainingPartnerID, rxData, UNIQUE_ID_BYTES); + //Find a slot in the NVM unique ID table + for (index = 0; index < MAX_VC; index++) + if (!nvmIsVcUniqueIdSet(index)) + { + //Save the client unique ID + nvmSaveUniqueId(index, trainingPartnerID); + break; + } + //Wait for the transmit to complete if (xmitDatagramTrainRadioParameters(trainingPartnerID) == true) changeState(RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE); @@ -2682,9 +2691,7 @@ int8_t vcIdToAddressByte(int8_t srcAddr, uint8_t * id) } //Save the unique ID to VC assignment in NVM - memcpy(&vc->uniqueId, id, UNIQUE_ID_BYTES); - if (settings.server) - nvmSaveVcUniqueId(vcIndex); + nvmSaveUniqueId(vcIndex, id); //Mark this link as up vc->flags.valid = true; From c429858a86f24d8ddb14a000cb24d3786a255382 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 12:17:18 -1000 Subject: [PATCH 351/594] Rename ACK_1 to SYNC_CLOCKS --- Firmware/LoRaSerial_Firmware/Radio.ino | 12 +++---- Firmware/LoRaSerial_Firmware/States.ino | 42 ++++++++++++------------- Firmware/LoRaSerial_Firmware/settings.h | 18 +++++------ 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index b837529c..4d516348 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -756,9 +756,9 @@ bool xmitDatagramP2PFindPartner() } //Second packet in the three way handshake to bring up the link -bool xmitDatagramP2PAck1() +bool xmitDatagramP2PSyncClocks() { - radioCallHistory[RADIO_CALL_xmitDatagramP2PAck1] = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramP2PSyncClocks] = millis(); unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); @@ -775,7 +775,7 @@ bool xmitDatagramP2PAck1() +----------+---------+----------+------------+---------+----------+ */ - txControl.datagramType = DATAGRAM_ACK_1; + txControl.datagramType = DATAGRAM_SYNC_CLOCKS; return (transmitDatagram()); } @@ -985,7 +985,7 @@ bool xmitDatagramMpAck() +----------+---------+----------+------------+---------+----------+ */ - txControl.datagramType = DATAGRAM_ACK_1; + txControl.datagramType = DATAGRAM_SYNC_CLOCKS; return (transmitDatagram()); } @@ -2348,7 +2348,7 @@ bool transmitDatagram() break; case DATAGRAM_FIND_PARTNER: - case DATAGRAM_ACK_1: + case DATAGRAM_SYNC_CLOCKS: case DATAGRAM_ACK_2: txDatagramSize = headerBytes + CLOCK_MILLIS_BYTES; //Short packet is 5 + 4 break; @@ -3053,7 +3053,7 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_returnToReceiving, "returnToReceiving"}, {RADIO_CALL_calcAirTime, "calcAirTime"}, {RADIO_CALL_xmitDatagramP2PFindPartner, "xmitDatagramP2PFindPartner"}, - {RADIO_CALL_xmitDatagramP2PAck1, "xmitDatagramP2PAck1"}, + {RADIO_CALL_xmitDatagramP2PSyncClocks, "xmitDatagramP2PSyncClocks"}, {RADIO_CALL_xmitDatagramP2PAck2, "xmitDatagramP2PAck2"}, {RADIO_CALL_xmitDatagramP2PCommand, "xmitDatagramP2PCommand"}, {RADIO_CALL_xmitDatagramP2PCommandResponse, "xmitDatagramP2PCommandResponse"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 4228d7c4..825650d6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -260,11 +260,11 @@ void updateRadioState() timestampOffset = clockOffset; //Acknowledge the FIND_PARTNER - triggerEvent(TRIGGER_SEND_ACK1); - if (xmitDatagramP2PAck1() == true) + triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); + if (xmitDatagramP2PSyncClocks() == true) { sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK2 to contain millis info - changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); + changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } break; } @@ -277,7 +277,7 @@ void updateRadioState() triggerEvent(TRIGGER_HANDSHAKE_SEND_FIND_PARTNER); if (xmitDatagramP2PFindPartner() == true) { - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK1 to contain millis info + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect SYNC_CLOCKS to contain millis info changeState(RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE); } } @@ -290,11 +290,11 @@ void updateRadioState() triggerEvent(TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE); transactionComplete = false; //Reset ISR flag returnToReceiving(); - changeState(RADIO_P2P_WAIT_ACK_1); + changeState(RADIO_P2P_WAIT_SYNC_CLOCKS); } break; - case RADIO_P2P_WAIT_ACK_1: + case RADIO_P2P_WAIT_SYNC_CLOCKS: if (transactionComplete) { //Decode the received packet @@ -334,15 +334,15 @@ void updateRadioState() timestampOffset = clockOffset; //Acknowledge the FIND_PARTNER - triggerEvent(TRIGGER_SEND_ACK1); - if (xmitDatagramP2PAck1() == true) + triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); + if (xmitDatagramP2PSyncClocks() == true) { sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK2 to contain millis info - changeState(RADIO_P2P_WAIT_TX_ACK_1_DONE); + changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } break; - case DATAGRAM_ACK_1: + case DATAGRAM_SYNC_CLOCKS: //Received ACK 1 //Compute the common clock currentMillis = millis(); @@ -353,7 +353,7 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - //Acknowledge the ACK1 + //Acknowledge the SYNC_CLOCKS triggerEvent(TRIGGER_SEND_ACK2); if (xmitDatagramP2PAck2() == true) { @@ -371,12 +371,12 @@ void updateRadioState() if (settings.debugDatagrams) { systemPrintTimestamp(); - systemPrintln("RX: ACK1 Timeout"); + systemPrintln("RX: SYNC_CLOCKS Timeout"); outputSerialData(true); } //Start the TX timer: time to delay before transmitting the FIND_PARTNER - triggerEvent(TRIGGER_HANDSHAKE_ACK1_TIMEOUT); + triggerEvent(TRIGGER_HANDSHAKE_SYNC_CLOCKS_TIMEOUT); setHeartbeatShort(); //Slow down FIND_PARTNERs @@ -393,11 +393,11 @@ void updateRadioState() } break; - case RADIO_P2P_WAIT_TX_ACK_1_DONE: + case RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE: //Determine if a ACK 1 has completed transmission if (transactionComplete) { - triggerEvent(TRIGGER_HANDSHAKE_SEND_ACK1_COMPLETE); + triggerEvent(TRIGGER_HANDSHAKE_SEND_SYNC_CLOCKS_COMPLETE); transactionComplete = false; //Reset ISR flag returnToReceiving(); changeState(RADIO_P2P_WAIT_ACK_2); @@ -445,7 +445,7 @@ void updateRadioState() startChannelTimer(getLinkupOffset()); //We are exiting the link last so adjust our starting Timer - setHeartbeatLong(); //We sent ACK1 and they sent ACK2, so don't be the first to send heartbeat + setHeartbeatLong(); //We sent SYNC_CLOCKS and they sent ACK2, so don't be the first to send heartbeat //Bring up the link enterLinkUp(); @@ -900,7 +900,7 @@ void updateRadioState() break; //==================== - //Walk through channel table backwards, transmitting a FIND_PARTNER and looking for an ACK1 + //Walk through channel table backwards, transmitting a FIND_PARTNER and looking for an SYNC_CLOCKS //==================== case RADIO_DISCOVER_SCANNING: if (transactionComplete) @@ -945,7 +945,7 @@ void updateRadioState() triggerEvent(TRIGGER_BAD_PACKET); break; - case DATAGRAM_ACK_1: + case DATAGRAM_SYNC_CLOCKS: triggerEvent(TRIGGER_LINK_ACK_RECEIVED); //Server has responded with ACK @@ -980,7 +980,7 @@ void updateRadioState() if (settings.debugDatagrams) { systemPrintTimestamp(); - systemPrintln("MP: ACK1 Timeout"); + systemPrintln("MP: SYNC_CLOCKS Timeout"); outputSerialData(true); } @@ -1082,7 +1082,7 @@ void updateRadioState() triggerEvent(TRIGGER_NETID_MISMATCH); break; - case DATAGRAM_ACK_1: + case DATAGRAM_SYNC_CLOCKS: case DATAGRAM_ACK_2: case DATAGRAM_DATAGRAM: case DATAGRAM_DATA_ACK: @@ -1933,7 +1933,7 @@ void updateRadioState() if ((currentMillis - virtualCircuitList[index].timerMillis) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) { - //Retransmit the ACK1 + //Retransmit the SYNC_CLOCKS if (xmitVcSyncAcks(index)) { virtualCircuitList[index].timerMillis = datagramTimer; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 2036352d..7b7ff034 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -5,8 +5,8 @@ typedef enum //Point-To-Point: Bring up the link RADIO_P2P_LINK_DOWN, RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE, - RADIO_P2P_WAIT_ACK_1, - RADIO_P2P_WAIT_TX_ACK_1_DONE, + RADIO_P2P_WAIT_SYNC_CLOCKS, + RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE, RADIO_P2P_WAIT_ACK_2, RADIO_P2P_WAIT_TX_ACK_2_DONE, @@ -61,8 +61,8 @@ const RADIO_STATE_ENTRY radioStateTable[] = // State RX Name Description {RADIO_P2P_LINK_DOWN, 1, "P2P_LINK_DOWN", "P2P: [No Link] Waiting for FIND_PARTNER"}, // 1 {RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE, 0, "P2P_WAIT_TX_FIND_PARTNER_DONE", "P2P: [No Link] Wait FIND_PARTNER TX Done"},// 2 - {RADIO_P2P_WAIT_ACK_1, 1, "P2P_WAIT_ACK_1", "P2P: [No Link] Waiting for ACK1"}, // 3 - {RADIO_P2P_WAIT_TX_ACK_1_DONE, 0, "P2P_WAIT_TX_ACK_1_DONE", "P2P: [No Link] Wait ACK1 TX Done"},// 4 + {RADIO_P2P_WAIT_SYNC_CLOCKS, 1, "P2P_WAIT_SYNC_CLOCKS", "P2P: [No Link] Waiting for SYNC_CLOCKS"}, // 3 + {RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE, 0, "P2P_WAIT_TX_SYNC_CLOCKS_DONE", "P2P: [No Link] Wait SYNC_CLOCKS TX Done"}, // 4 {RADIO_P2P_WAIT_ACK_2, 1, "P2P_WAIT_ACK_2", "P2P: [No Link] Waiting for ACK2"}, // 5 {RADIO_P2P_WAIT_TX_ACK_2_DONE, 0, "P2P_WAIT_TX_ACK_2_DONE", "P2P: [No Link] Wait ACK2 TX Done"},// 6 @@ -108,7 +108,7 @@ typedef enum //P2P: Between the two LoRaSerial radios //VC: Between the server radio and a client radio DATAGRAM_FIND_PARTNER = 0, // 0 - DATAGRAM_ACK_1, // 1 + DATAGRAM_SYNC_CLOCKS, // 1 DATAGRAM_ACK_2, // 2 //Point-to-Point data exchange @@ -262,11 +262,11 @@ enum TRIGGER_MP_PACKET_RECEIVED, TRIGGER_MP_SEND_ACK_FOR_FIND_PARTNER, TRIGGER_TRANSMIT_CANCELED, - TRIGGER_HANDSHAKE_ACK1_TIMEOUT, + TRIGGER_HANDSHAKE_SYNC_CLOCKS_TIMEOUT, TRIGGER_HANDSHAKE_SEND_FIND_PARTNER, TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE, - TRIGGER_HANDSHAKE_SEND_ACK1_COMPLETE, - TRIGGER_SEND_ACK1, + TRIGGER_HANDSHAKE_SEND_SYNC_CLOCKS_COMPLETE, + TRIGGER_SEND_SYNC_CLOCKS, TRIGGER_SEND_ACK2, TRIGGER_HANDSHAKE_COMPLETE, TRIGGER_LINK_ACK_SENT, @@ -498,7 +498,7 @@ typedef enum RADIO_CALL_returnToReceiving, RADIO_CALL_calcAirTime, RADIO_CALL_xmitDatagramP2PFindPartner, - RADIO_CALL_xmitDatagramP2PAck1, + RADIO_CALL_xmitDatagramP2PSyncClocks, RADIO_CALL_xmitDatagramP2PAck2, RADIO_CALL_xmitDatagramP2PCommand, RADIO_CALL_xmitDatagramP2PCommandResponse, From 0bfd0157e5d09e56b824dc7df945ac5278a6d37d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 12:24:11 -1000 Subject: [PATCH 352/594] Rename ACK_2 to ZERO_ACKS --- Firmware/LoRaSerial_Firmware/Radio.ino | 10 ++++----- Firmware/LoRaSerial_Firmware/States.ino | 28 ++++++++++++------------- Firmware/LoRaSerial_Firmware/settings.h | 16 +++++++------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4d516348..a1911dff 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -780,9 +780,9 @@ bool xmitDatagramP2PSyncClocks() } //Last packet in the three way handshake to bring up the link -bool xmitDatagramP2PAck2() +bool xmitDatagramP2PZeroAcks() { - radioCallHistory[RADIO_CALL_xmitDatagramP2PAck2] = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramP2PZeroAcks] = millis(); unsigned long currentMillis = millis(); memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); @@ -799,7 +799,7 @@ bool xmitDatagramP2PAck2() +----------+---------+----------+------------+---------+----------+ */ - txControl.datagramType = DATAGRAM_ACK_2; + txControl.datagramType = DATAGRAM_ZERO_ACKS; return (transmitDatagram()); } @@ -2349,7 +2349,7 @@ bool transmitDatagram() case DATAGRAM_FIND_PARTNER: case DATAGRAM_SYNC_CLOCKS: - case DATAGRAM_ACK_2: + case DATAGRAM_ZERO_ACKS: txDatagramSize = headerBytes + CLOCK_MILLIS_BYTES; //Short packet is 5 + 4 break; @@ -3054,7 +3054,7 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_calcAirTime, "calcAirTime"}, {RADIO_CALL_xmitDatagramP2PFindPartner, "xmitDatagramP2PFindPartner"}, {RADIO_CALL_xmitDatagramP2PSyncClocks, "xmitDatagramP2PSyncClocks"}, - {RADIO_CALL_xmitDatagramP2PAck2, "xmitDatagramP2PAck2"}, + {RADIO_CALL_xmitDatagramP2PZeroAcks, "xmitDatagramP2PZeroAcks"}, {RADIO_CALL_xmitDatagramP2PCommand, "xmitDatagramP2PCommand"}, {RADIO_CALL_xmitDatagramP2PCommandResponse, "xmitDatagramP2PCommandResponse"}, {RADIO_CALL_xmitDatagramP2PData, "xmitDatagramP2PData"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 825650d6..9c803fe9 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -263,7 +263,7 @@ void updateRadioState() triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK2 to contain millis info + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } break; @@ -337,7 +337,7 @@ void updateRadioState() triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ACK2 to contain millis info + sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } break; @@ -354,11 +354,11 @@ void updateRadioState() timestampOffset = clockOffset; //Acknowledge the SYNC_CLOCKS - triggerEvent(TRIGGER_SEND_ACK2); - if (xmitDatagramP2PAck2() == true) + triggerEvent(TRIGGER_SEND_ZERO_ACKS); + if (xmitDatagramP2PZeroAcks() == true) { sf6ExpectedSize = MAX_PACKET_SIZE; //Tell SF6 to return to max packet length - changeState(RADIO_P2P_WAIT_TX_ACK_2_DONE); + changeState(RADIO_P2P_WAIT_TX_ZERO_ACKS_DONE); } break; } @@ -400,11 +400,11 @@ void updateRadioState() triggerEvent(TRIGGER_HANDSHAKE_SEND_SYNC_CLOCKS_COMPLETE); transactionComplete = false; //Reset ISR flag returnToReceiving(); - changeState(RADIO_P2P_WAIT_ACK_2); + changeState(RADIO_P2P_WAIT_ZERO_ACKS); } break; - case RADIO_P2P_WAIT_ACK_2: + case RADIO_P2P_WAIT_ZERO_ACKS: if (transactionComplete == true) { //Decode the received packet @@ -432,7 +432,7 @@ void updateRadioState() triggerEvent(TRIGGER_CRC_ERROR); break; - case DATAGRAM_ACK_2: + case DATAGRAM_ZERO_ACKS: //Received ACK 2 //Compute the common clock currentMillis = millis(); @@ -445,7 +445,7 @@ void updateRadioState() startChannelTimer(getLinkupOffset()); //We are exiting the link last so adjust our starting Timer - setHeartbeatLong(); //We sent SYNC_CLOCKS and they sent ACK2, so don't be the first to send heartbeat + setHeartbeatLong(); //We sent SYNC_CLOCKS and they sent ZERO_ACKS, so don't be the first to send heartbeat //Bring up the link enterLinkUp(); @@ -460,12 +460,12 @@ void updateRadioState() if (settings.debugDatagrams) { systemPrintTimestamp(); - systemPrintln("RX: ACK2 Timeout"); + systemPrintln("RX: ZERO_ACKS Timeout"); outputSerialData(true); } //Start the TX timer: time to delay before transmitting the FIND_PARTNER - triggerEvent(TRIGGER_HANDSHAKE_ACK2_TIMEOUT); + triggerEvent(TRIGGER_HANDSHAKE_ZERO_ACKS_TIMEOUT); setHeartbeatShort(); //Slow down FIND_PARTNERs @@ -482,7 +482,7 @@ void updateRadioState() } break; - case RADIO_P2P_WAIT_TX_ACK_2_DONE: + case RADIO_P2P_WAIT_TX_ZERO_ACKS_DONE: //Determine if a ACK 2 has completed transmission if (transactionComplete) { @@ -935,7 +935,7 @@ void updateRadioState() triggerEvent(TRIGGER_NETID_MISMATCH); break; - case DATAGRAM_ACK_2: + case DATAGRAM_ZERO_ACKS: case DATAGRAM_DATA: case DATAGRAM_DATA_ACK: case DATAGRAM_FIND_PARTNER: //Clients do not respond to FIND_PARTNER, only the server @@ -1083,7 +1083,7 @@ void updateRadioState() break; case DATAGRAM_SYNC_CLOCKS: - case DATAGRAM_ACK_2: + case DATAGRAM_ZERO_ACKS: case DATAGRAM_DATAGRAM: case DATAGRAM_DATA_ACK: case DATAGRAM_REMOTE_COMMAND: diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 7b7ff034..a94f9875 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -7,8 +7,8 @@ typedef enum RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE, RADIO_P2P_WAIT_SYNC_CLOCKS, RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE, - RADIO_P2P_WAIT_ACK_2, - RADIO_P2P_WAIT_TX_ACK_2_DONE, + RADIO_P2P_WAIT_ZERO_ACKS, + RADIO_P2P_WAIT_TX_ZERO_ACKS_DONE, //Point-to-Point: Link up, data exchange RADIO_P2P_LINK_UP, @@ -63,8 +63,8 @@ const RADIO_STATE_ENTRY radioStateTable[] = {RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE, 0, "P2P_WAIT_TX_FIND_PARTNER_DONE", "P2P: [No Link] Wait FIND_PARTNER TX Done"},// 2 {RADIO_P2P_WAIT_SYNC_CLOCKS, 1, "P2P_WAIT_SYNC_CLOCKS", "P2P: [No Link] Waiting for SYNC_CLOCKS"}, // 3 {RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE, 0, "P2P_WAIT_TX_SYNC_CLOCKS_DONE", "P2P: [No Link] Wait SYNC_CLOCKS TX Done"}, // 4 - {RADIO_P2P_WAIT_ACK_2, 1, "P2P_WAIT_ACK_2", "P2P: [No Link] Waiting for ACK2"}, // 5 - {RADIO_P2P_WAIT_TX_ACK_2_DONE, 0, "P2P_WAIT_TX_ACK_2_DONE", "P2P: [No Link] Wait ACK2 TX Done"},// 6 + {RADIO_P2P_WAIT_ZERO_ACKS, 1, "P2P_WAIT_ZERO_ACKS", "P2P: [No Link] Waiting for ZERO_ACKS"}, // 5 + {RADIO_P2P_WAIT_TX_ZERO_ACKS_DONE, 0, "P2P_WAIT_TX_ZERO_ACKS_DONE", "P2P: [No Link] Wait ZERO_ACKS TX Done"}, // 6 //Point-to-Point, link up, data exchange // State RX Name Description @@ -109,7 +109,7 @@ typedef enum //VC: Between the server radio and a client radio DATAGRAM_FIND_PARTNER = 0, // 0 DATAGRAM_SYNC_CLOCKS, // 1 - DATAGRAM_ACK_2, // 2 + DATAGRAM_ZERO_ACKS, // 2 //Point-to-Point data exchange DATAGRAM_DATA, // 3 @@ -267,11 +267,11 @@ enum TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE, TRIGGER_HANDSHAKE_SEND_SYNC_CLOCKS_COMPLETE, TRIGGER_SEND_SYNC_CLOCKS, - TRIGGER_SEND_ACK2, + TRIGGER_SEND_ZERO_ACKS, TRIGGER_HANDSHAKE_COMPLETE, TRIGGER_LINK_ACK_SENT, TRIGGER_LINK_ACK_RECEIVED, - TRIGGER_HANDSHAKE_ACK2_TIMEOUT, + TRIGGER_HANDSHAKE_ZERO_ACKS_TIMEOUT, TRIGGER_RECEIVE_IN_PROCESS_START, TRIGGER_RECEIVE_IN_PROCESS_END, TRIGGER_LINK_HB_ACK_REXMIT, @@ -499,7 +499,7 @@ typedef enum RADIO_CALL_calcAirTime, RADIO_CALL_xmitDatagramP2PFindPartner, RADIO_CALL_xmitDatagramP2PSyncClocks, - RADIO_CALL_xmitDatagramP2PAck2, + RADIO_CALL_xmitDatagramP2PZeroAcks, RADIO_CALL_xmitDatagramP2PCommand, RADIO_CALL_xmitDatagramP2PCommandResponse, RADIO_CALL_xmitDatagramP2PData, From 37a5f693dab2a063f5acd23c5ba23f9abf6cc8e9 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 12:34:23 -1000 Subject: [PATCH 353/594] Replace xmitDatagramMpFindPartner with xmitDatagramP2PFindPartner --- Firmware/LoRaSerial_Firmware/Radio.ino | 21 --------------------- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 1 - 3 files changed, 1 insertion(+), 23 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a1911dff..0e7284d0 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -989,26 +989,6 @@ bool xmitDatagramMpAck() return (transmitDatagram()); } -//FIND_PARTNER packet sent during scanning -bool xmitDatagramMpFindPartner() -{ - radioCallHistory[RADIO_CALL_xmitDatagramMpFindPartner] = millis(); - - /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+----------+ - | Optional | | Optional | Optional | Optional | - | NET ID | Control | C-Timer | SF6 Length | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | - +----------+---------+----------+------------+----------+ - */ - - txControl.datagramType = DATAGRAM_FIND_PARTNER; - return (transmitDatagram()); -} - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Multi-Point Client Training //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -3063,7 +3043,6 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_xmitDatagramMpData, "xmitDatagramMpData"}, {RADIO_CALL_xmitDatagramMpHeartbeat, "xmitDatagramMpHeartbeat"}, {RADIO_CALL_xmitDatagramMpAck, "xmitDatagramMpAck"}, - {RADIO_CALL_xmitDatagramMpFindPartner, "xmitDatagramMpFindPartner"}, {RADIO_CALL_xmitDatagramTrainingFindPartner, "xmitDatagramTrainingFindPartner"}, {RADIO_CALL_xmitDatagramTrainingAck, "xmitDatagramTrainingAck"}, {RADIO_CALL_xmitDatagramTrainRadioParameters, "xmitDatagramTrainRadioParameters"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9c803fe9..3431a7ef 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1004,7 +1004,7 @@ void updateRadioState() } //Send FIND_PARTNER - if (xmitDatagramMpFindPartner() == true) + if (xmitDatagramP2PFindPartner() == true) changeState(RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE); } } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index a94f9875..48743560 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -508,7 +508,6 @@ typedef enum RADIO_CALL_xmitDatagramMpData, RADIO_CALL_xmitDatagramMpHeartbeat, RADIO_CALL_xmitDatagramMpAck, - RADIO_CALL_xmitDatagramMpFindPartner, RADIO_CALL_xmitDatagramTrainingFindPartner, RADIO_CALL_xmitDatagramTrainingAck, RADIO_CALL_xmitDatagramTrainRadioParameters, From 482c569bd2808fa6d40a9a1195d0a924ee45bcc8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 20 Dec 2022 12:43:40 -1000 Subject: [PATCH 354/594] Replace MP_WAIT_TX_ACK_DONE with RADIO_MP_WAIT_TX_DONE This commit makes the following changes: * Establisheds defines for SYNC_CLOCKS_BYTES and ZERO_ACKS_BYTES * Verifies the values of the defines above * Replaces xmitDatagramMpAck with xmitDatagramP2PSyncClocks * Adds channel number to xmitDatagramP2PSyncClocks * Adds discovery and multipoint state diagram * Replaces RADIO_MP_WAIT_TX_ACK_DONE with RADIO_MP_WAIT_TX_DONE since hop can be done after clock and channel synchronization --- .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/Radio.ino | 84 ++++++++--------- Firmware/LoRaSerial_Firmware/States.ino | 91 ++++++++++++++----- Firmware/LoRaSerial_Firmware/settings.h | 23 ++--- 4 files changed, 119 insertions(+), 81 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 636d9b7d..147e7164 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -58,6 +58,8 @@ const int FIRMWARE_VERSION_MINOR = 0; #define CHANNEL_TIMER_BYTES sizeof(uint16_t) //Number of bytes used within in control header for clock sync (uint16_t msToNextHop) #define CLOCK_MILLIS_BYTES sizeof(unsigned long) //Number of bytes used within in various packets for system timestamps sync (unsigned long currentMillis) +#define SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame +#define ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame #define MAX_PACKET_SIZE 255 //Limited by SX127x #define AES_IV_BYTES 12 //Number of bytes for AESiv #define AES_KEY_BYTES 16 //Number of bytes in the encryption key diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 0e7284d0..82bacd67 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -756,25 +756,43 @@ bool xmitDatagramP2PFindPartner() } //Second packet in the three way handshake to bring up the link +//SYNC_CLOCKS packet sent by server in response the FIND_PARTNER, includes the +//channel number. During discovery scanning, it's possible for the client to +//get the SYNC_CLOCKS but be on an adjacent channel. The channel number +//ensures that the client gets the next hop correct. bool xmitDatagramP2PSyncClocks() { - radioCallHistory[RADIO_CALL_xmitDatagramP2PSyncClocks] = millis(); + uint8_t * startOfData; unsigned long currentMillis = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramP2PSyncClocks] = currentMillis; + + startOfData = endOfTxData; + memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); + endOfTxData += sizeof(channelNumber); + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(unsigned long); /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+---------+----------+ - | Optional | | Optional | Optional | | | - | NET ID | Control | C-Timer | SF6 Length | Millis | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | 4 bytes | n Bytes | - +----------+---------+----------+------------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+---------+----------+ + | Optional | | Optional | Optional | Channel | | | + | NET ID | Control | C-Timer | SF6 Length | Number | Millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 1 byte | 4 bytes | n Bytes | + +----------+---------+----------+------------+---------+---------+----------+ */ + //Verify the data length + if ((endOfTxData - startOfData) != SYNC_CLOCKS_BYTES) + { + systemPrintln("ERROR - Fix the SYNC_CLOCKS_BYTES value!"); + outputSerialData(true); + waitForever(); + } + txControl.datagramType = DATAGRAM_SYNC_CLOCKS; return (transmitDatagram()); } @@ -782,9 +800,12 @@ bool xmitDatagramP2PSyncClocks() //Last packet in the three way handshake to bring up the link bool xmitDatagramP2PZeroAcks() { - radioCallHistory[RADIO_CALL_xmitDatagramP2PZeroAcks] = millis(); + uint8_t * startOfData; unsigned long currentMillis = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramP2PZeroAcks] = currentMillis; + + startOfData = endOfTxData; memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(unsigned long); @@ -799,6 +820,14 @@ bool xmitDatagramP2PZeroAcks() +----------+---------+----------+------------+---------+----------+ */ + //Verify the data length + if ((endOfTxData - startOfData) != ZERO_ACKS_BYTES) + { + systemPrintln("ERROR - Fix the ZERO_ACKS_BYTES value!"); + outputSerialData(true); + waitForever(); + } + txControl.datagramType = DATAGRAM_ZERO_ACKS; return (transmitDatagram()); } @@ -955,40 +984,6 @@ bool xmitDatagramMpHeartbeat() return (transmitDatagram()); } -//ACK packet sent by server in response the FIND_PARTNER, includes channel number -//During discovery scanning, it's possible for the client to get an ACK but be on an adjacent channel -//The channel number ensures that the client gets the next hop correct -bool xmitDatagramMpAck() -{ - radioCallHistory[RADIO_CALL_xmitDatagramMpAck] = millis(); - - memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); - endOfTxData += sizeof(channelNumber); - - if (settings.debugTransmit) - { - systemPrint(" Channel Number: "); - systemPrintln(channelNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } - - /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+---------+----------+ - | Optional | | Optional | Optional | Channel | Optional | - | NET ID | Control | C-Timer | SF6 Length | Number | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | 1 byte | n Bytes | - +----------+---------+----------+------------+---------+----------+ - */ - - txControl.datagramType = DATAGRAM_SYNC_CLOCKS; - return (transmitDatagram()); -} - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Multi-Point Client Training //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -3042,7 +3037,6 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_xmitDatagramP2PAck, "xmitDatagramP2PAck"}, {RADIO_CALL_xmitDatagramMpData, "xmitDatagramMpData"}, {RADIO_CALL_xmitDatagramMpHeartbeat, "xmitDatagramMpHeartbeat"}, - {RADIO_CALL_xmitDatagramMpAck, "xmitDatagramMpAck"}, {RADIO_CALL_xmitDatagramTrainingFindPartner, "xmitDatagramTrainingFindPartner"}, {RADIO_CALL_xmitDatagramTrainingAck, "xmitDatagramTrainingAck"}, {RADIO_CALL_xmitDatagramTrainRadioParameters, "xmitDatagramTrainRadioParameters"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3431a7ef..2dfb9375 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -259,11 +259,11 @@ void updateRadioState() clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp timestampOffset = clockOffset; - //Acknowledge the FIND_PARTNER + //Acknowledge the FIND_PARTNER with SYNC_CLOCKS triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info + sf6ExpectedSize = headerBytes + ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } break; @@ -337,16 +337,16 @@ void updateRadioState() triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info + sf6ExpectedSize = headerBytes + ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } break; case DATAGRAM_SYNC_CLOCKS: - //Received ACK 1 + //Received SYNC_CLOCKS //Compute the common clock currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); + memcpy(&clockOffset, rxData + 1, sizeof(currentMillis)); roundTripMillis = rcvTimeMillis - xmitTimeMillis; clockOffset += currentMillis + roundTripMillis; clockOffset >>= 1; @@ -394,7 +394,7 @@ void updateRadioState() break; case RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE: - //Determine if a ACK 1 has completed transmission + //Determine if a SYNC_CLOCKS has completed transmission if (transactionComplete) { triggerEvent(TRIGGER_HANDSHAKE_SEND_SYNC_CLOCKS_COMPLETE); @@ -545,7 +545,7 @@ void updateRadioState() FIND_PARTNER ----> Update timestampOffset - Update timestampOffset <---- ACK 1 + Update timestampOffset <---- SYNC_CLOCKS ACK 2 ----> Update timestampOffset @@ -886,6 +886,53 @@ void updateRadioState() //Multi-Point Data Exchange //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + /* + Server Client + + Reset Reset + | | + | | + V V + .--------------> RADIO_MP_STANDBY RADIO_DISCOVER_BEGIN + | | | + | No TX | | + | Do ??? RX other V | + +<--------+<------------+ V + ^ | | .--------> RADIO_DISCOVER_SCANNING + | | TX Resp | | TX done | + | | | | | + | | | WAIT_TX_FIND_PARTNER_DONE | + | .---' | ^ | + | | RX | | | + | | FIND_PARTNER | < - - - - - - | TX FIND_PARTNER | + | | | | | + | | | | Delay | + | | | | 10 Sec | + | | | +<----------. | + | | | ^ | | + | | | | Yes | No | + | | | + <---- Loops < 10 | + | | | ^ ^ | + | | | No | Yes | | + | | | Channel 0 ------' | + | | | ^ | + | | | | Hop reverse | + | | | | RX timeout V + | | | '---------------------+ + | | | | + | | | | + | | TX | | + | | SYNC_CLOCKS | - - - - - - - - - - - - - - - - - > | RX SYNC_CLOCKS + | | | | + | | | | Sync clocks + | | v | Update channel # + | '-----> RADIO_MP_WAIT_TX_DONE | + | | | + | v | + `-----------------------+<--------------------------------------' + + */ + //==================== //Start searching for other radios //==================== @@ -951,7 +998,17 @@ void updateRadioState() //Server has responded with ACK syncChannelTimer(); //Start and adjust freq hop ISR based on remote's remaining clock - channelNumber = rxData[0]; //Change to the server's channel number + //Change to the server's channel number + channelNumber = rxVcData[0]; + + //Compute the common clock + currentMillis = millis(); + memcpy(&clockOffset, rxData + 1, sizeof(currentMillis)); + roundTripMillis = rcvTimeMillis - xmitTimeMillis; + clockOffset += currentMillis + roundTripMillis; + clockOffset >>= 1; + clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp + timestampOffset = clockOffset; if (settings.debugSync) { @@ -1023,18 +1080,6 @@ void updateRadioState() } break; - //==================== - //Wait for the ACK to complete transmission - //==================== - case RADIO_MP_WAIT_TX_ACK_DONE: - if (transactionComplete) - { - transactionComplete = false; //Reset ISR flag - returnToReceiving(); - changeState(RADIO_MP_STANDBY); - } - break; - //==================== //Wait for the next operation (listed in priority order): // * Frame received @@ -1097,11 +1142,11 @@ void updateRadioState() //A new radio is saying hello if (settings.server == true) { - //Ack their FIND_PARTNER with sync data - if (xmitDatagramMpAck() == true) + //Ack their FIND_PARTNER with SYNC_CLOCK + if (xmitDatagramP2PSyncClocks() == true) { triggerEvent(TRIGGER_MP_SEND_ACK_FOR_FIND_PARTNER); - changeState(RADIO_MP_WAIT_TX_ACK_DONE); + changeState(RADIO_MP_WAIT_TX_DONE); } } else diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 48743560..00917502 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -20,7 +20,6 @@ typedef enum RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE, //Multi-Point: Datagrams - RADIO_MP_WAIT_TX_ACK_DONE, RADIO_MP_STANDBY, RADIO_MP_WAIT_TX_DONE, @@ -79,26 +78,25 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Multi-Point data exchange // State RX Name Description - {RADIO_MP_WAIT_TX_ACK_DONE, 0, "MP_WAIT_TX_ACK_DONE", "MP: Wait for ACK to xmit"}, //12 - {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //13 - {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //14 + {RADIO_MP_STANDBY, 1, "MP_STANDBY", "MP: Wait for TX or RX"}, //12 + {RADIO_MP_WAIT_TX_DONE, 0, "MP_WAIT_TX_DONE", "MP: Waiting for TX done"}, //13 //Training client states // State RX Name Description - {RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE,0, "TRAIN_WAIT_TX_FIND_PARTNER_DONE","Train: Wait TX training FIND_PARTNER done"}, //15 - {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //16 - {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //17 + {RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE,0, "TRAIN_WAIT_TX_FIND_PARTNER_DONE","Train: Wait TX training FIND_PARTNER done"}, //14 + {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //15 + {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //16 //Training server states // State RX Name Description - {RADIO_TRAIN_WAIT_FOR_FIND_PARTNER, 1, "TRAIN_WAIT_FOR_FIND_PARTNER", "Train: Wait for training FIND_PARTNER"}, //18 - {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //19 + {RADIO_TRAIN_WAIT_FOR_FIND_PARTNER, 1, "TRAIN_WAIT_FOR_FIND_PARTNER", "Train: Wait for training FIND_PARTNER"}, //17 + {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //18 //Virtual circuit states // State RX Name Description - {RADIO_VC_WAIT_SERVER, 1, "VC_WAIT_SERVER", "VC: Wait for the server"}, //20 - {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "VC: Wait for TX done"}, //21 - {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "VC: Wait for receive"}, //22 + {RADIO_VC_WAIT_SERVER, 1, "VC_WAIT_SERVER", "VC: Wait for the server"}, //19 + {RADIO_VC_WAIT_TX_DONE, 0, "VC_WAIT_TX_DONE", "VC: Wait for TX done"}, //20 + {RADIO_VC_WAIT_RECEIVE, 1, "VC_WAIT_RECEIVE", "VC: Wait for receive"}, //21 }; //Possible types of packets received @@ -507,7 +505,6 @@ typedef enum RADIO_CALL_xmitDatagramP2PAck, RADIO_CALL_xmitDatagramMpData, RADIO_CALL_xmitDatagramMpHeartbeat, - RADIO_CALL_xmitDatagramMpAck, RADIO_CALL_xmitDatagramTrainingFindPartner, RADIO_CALL_xmitDatagramTrainingAck, RADIO_CALL_xmitDatagramTrainRadioParameters, From e1c85050b7e651eb1d3922590624431c157c8d69 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 27 Dec 2022 09:59:46 -1000 Subject: [PATCH 355/594] Rename calcSymbolTime to calcSymbolTimeMsec --- Firmware/LoRaSerial_Firmware/Radio.ino | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 82bacd67..31704248 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -56,7 +56,7 @@ bool configureRadio() // HoppingPeriod = Tsym * FreqHoppingPeriod // Given defaults of spreadfactor = 9, bandwidth = 125, it follows Tsym = 4.10ms // HoppingPeriod = 4.10 * x = Yms. Can be as high as 400ms to be within regulatory limits - uint16_t hoppingPeriod = settings.maxDwellTime / calcSymbolTime(); //Limit FHSS dwell time to 400ms max. / automatically floors number + uint16_t hoppingPeriod = settings.maxDwellTime / calcSymbolTimeMsec(); //Limit FHSS dwell time to 400ms max. / automatically floors number if (hoppingPeriod > 255) hoppingPeriod = 255; //Limit to 8 bits. if (settings.frequencyHop == false) hoppingPeriod = 0; //Disable if (radio.setFHSSHoppingPeriod(hoppingPeriod) != RADIOLIB_ERR_NONE) @@ -80,8 +80,9 @@ bool configureRadio() systemPrintln(settings.radioSyncWord); systemPrint("radioPreambleLength: "); systemPrintln(settings.radioPreambleLength); - systemPrint("calcSymbolTime: "); - systemPrintln(calcSymbolTime()); + systemPrint("calcSymbolTimeMsec: "); + systemPrint(calcSymbolTimeMsec(), 3); + systemPrintln(" mSec"); systemPrint("HoppingPeriod: "); systemPrintln(hoppingPeriod); systemPrint("ackAirTime: "); @@ -250,7 +251,7 @@ uint16_t calcAirTime(uint8_t bytesToSend) { radioCallHistory[RADIO_CALL_calcAirTime] = millis(); - float tSym = calcSymbolTime(); + float tSym = calcSymbolTimeMsec(); float tPreamble = (settings.radioPreambleLength + 4.25) * tSym; float p1 = (8 * bytesToSend - 4 * settings.radioSpreadFactor + 28 + 16 * 1 - 20 * 0) / (4.0 * (settings.radioSpreadFactor - 2 * 0)); p1 = ceil(p1) * settings.radioCodingRate; @@ -263,7 +264,7 @@ uint16_t calcAirTime(uint8_t bytesToSend) } //Given spread factor and bandwidth, return symbol time -float calcSymbolTime() +float calcSymbolTimeMsec() { float tSym = pow(2, settings.radioSpreadFactor) / settings.radioBandwidth; return (tSym); From cbd9388fb1b189e1361c04e1b5432896049459be Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 27 Dec 2022 10:04:05 -1000 Subject: [PATCH 356/594] Add routine calcSymbolTimeUsec --- Firmware/LoRaSerial_Firmware/Radio.ino | 110 ++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 31704248..48997870 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -264,10 +264,116 @@ uint16_t calcAirTime(uint8_t bytesToSend) } //Given spread factor and bandwidth, return symbol time +float calcSymbolTimeUsec() +{ + //The following documentation is from https://en.wikipedia.org/wiki/LoRa + // + //Each symbol is represented by a cyclic shifted chirp over the frequency + //interval (f0-B/2,f0+B/2) where f0 is the center frequency and B the + //bandwidth of the signal (in Hertz). + //The spreading factor (SF) is a selectable radio parameter from 5 to 12 and + //represents the number of bits sent per symbol and in addition determines + //how much the information is spread over time. + //There are M=2^SF different initial frequencies of the cyclic shifted chirp + //(the instantaneous frequency is linearly increased and wrapped to f0-B/2 + //when it reaches the maximum frequency f0+B/2. + //The symbol rate is determined by Rs=B/(2^SF. + //LoRa can trade off data rate for sensitivity (assuming a fixed channel + //bandwidth B) by selecting the SF, i.e. the amount of spread used. + //A lower SF corresponds to a higher data rate but a worse sensitivity, a + //higher SF implies a better sensitivity but a lower data rate. + + //The LoRa PHY is described in https://wirelesspi.com/understanding-lora-phy-long-range-physical-layer/ + // + // SF = spread factor = number of bits per symbol + // + // M = 2^SF = number of samples per symbol + // + // B = Bandwidth of the input signal + // + //From the sampling theorem: + // + // Fs >= 2 * B for real frequencies + // Fs >= B for complex frequencies + // + //Since this is a complex frequency and we want to use the lowest possible + //sampling frequency, we have + // + // Fs = B = 1 / Ts + // + // Ts = 1 / B + // + // Tm = M / B = Symbol time + // + // F1 = B / M = frequency step used within the bandwidth + // + // Fm = m * F1, m = 0 - (M-1) Frequency starting and end points + // + // u = B / Tm = Chirp rate + // + // Example: SF = 2 + // M = 2 ^ 2 = 4 + // + //Each symbol time (Tm), the signal changes frequencies from f0 - B/2 to f0 + B/2. + //Each symbol has a different starting (ending) frequency, but scans the entire + //bandwidth. + // + // ___________________________ f0 + B/2 + // /| /| /| /| + // / | / | / | | / + // / |/ | | / | / + // _____/___|___|/__|/__|/_____ + // f0 - B/2 + // Symbol | 00 | 01 | 10 | 11 | + // | | | | | + // 0 Tm 2Tm 3Tm 4Tm + // + //The signal is e^(j* [(u*pi*t^2)+(2*pi*Fm*t)+phi]) + // + //I: The real portion of the signal, cos((u*pi*t^2)+(2*pi*Fm*t)+phi) + //Q: The imaginary portion of the signal, j*sin((u*pi*t^2)+(2*pi*Fm*t)+phi) + // + //Decoding the signal is done by down chirping, multiplying the following + //phase locked signal with the broadcast signal and putting it through a low + //pass filter. + // + // ___________________________ f0 + B/2 + // |\ |\ |\ |\ + // | \ | \ | \ | \ + // | \ | \ | \ | \ + // _____|___\|___\|___\|___\__ + // f0 - B/2 + // | | | | | + // 0 Tm 2Tm 3Tm 4Tm + // + //The resulting signal is effectively added and offset to produce a constant + //frequency associated with the symbol over the symbol time. + // + // 0 Tm 2Tm 3Tm 4Tm + // | | | | | + // _____ B + // _____ + // _____ + // _____ + // 0 + // | | | | | + // 0 Tm 2Tm 3Tm 4Tm + // + // SF 11 10 9 8 7 6 + // M 2048 1024 512 256 128 64 + // B KHz 62.5 62.5 125 125 500 500 + // Tm uSec 65536 32768 8192 4096 512 256 + // Symbols 15.25 30.52 122.1 244.1 1953 3906 + // + //Compute time in microseconds using bandwidth in kHz + float tSymUsec = (pow(2, settings.radioSpreadFactor) * 1000.) / settings.radioBandwidth; + return (tSymUsec); +} + +//Return symbol time in milliseconds float calcSymbolTimeMsec() { - float tSym = pow(2, settings.radioSpreadFactor) / settings.radioBandwidth; - return (tSym); + return calcSymbolTimeUsec() / 1000.; } //Given spread factor, bandwidth, coding rate and frame size, return most bytes we can push per second From 6579798d70bae78b31de1446acbc6e96876e71f6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 27 Dec 2022 10:12:25 -1000 Subject: [PATCH 357/594] Rename calcAirTime to calcAirTimeMsec --- Firmware/LoRaSerial_Firmware/Radio.ino | 16 ++++++++-------- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 48997870..a4ca9f74 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -63,8 +63,8 @@ bool configureRadio() success = false; //Precalculate the packet times - ackAirTime = calcAirTime(headerBytes + CHANNEL_TIMER_BYTES + trailerBytes); //Used for response timeout during ACK - maxPacketAirTime = calcAirTime(MAX_PACKET_SIZE); + ackAirTime = calcAirTimeMsec(headerBytes + CHANNEL_TIMER_BYTES + trailerBytes); //Used for response timeout during ACK + maxPacketAirTime = calcAirTimeMsec(MAX_PACKET_SIZE); if ((settings.debug == true) || (settings.debugRadio == true)) { @@ -247,9 +247,9 @@ void returnToReceiving() } //Given spread factor, bandwidth, coding rate and number of bytes, return total Air Time in ms for packet -uint16_t calcAirTime(uint8_t bytesToSend) +uint16_t calcAirTimeMsec(uint8_t bytesToSend) { - radioCallHistory[RADIO_CALL_calcAirTime] = millis(); + radioCallHistory[RADIO_CALL_calcAirTimeMsec] = millis(); float tSym = calcSymbolTimeMsec(); float tPreamble = (settings.radioPreambleLength + 4.25) * tSym; @@ -2612,7 +2612,7 @@ bool transmitDatagram() endOfTxData = &outgoingPacket[headerBytes]; //Compute the time needed for this frame. Part of ACK timeout. - frameAirTime = calcAirTime(txDatagramSize); + frameAirTime = calcAirTimeMsec(txDatagramSize); //Transmit this datagram frameSentCount = 0; //This is the first time this frame is being sent @@ -2706,7 +2706,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); - frameAirTime = calcAirTime(txDatagramSize); //Calculate frame air size while we're transmitting in the background + frameAirTime = calcAirTimeMsec(txDatagramSize); //Calculate frame air size while we're transmitting in the background uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond //Drop this datagram if the receiver is active @@ -2837,7 +2837,7 @@ void syncChannelTimer() //If the sync arrived in an ACK, we know how long that packet took to transmit //Calculate the packet airTime based on the size of data received - msToNextHopRemote -= calcAirTime(packetLength); + msToNextHopRemote -= calcAirTimeMsec(packetLength); // if (settings.debugReceive == true) // msToNextHopRemote -= 91; //Must adjust for the blob of text being printed @@ -3133,7 +3133,7 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_configureRadio, "configureRadio"}, {RADIO_CALL_setRadioFrequency, "setRadioFrequency"}, {RADIO_CALL_returnToReceiving, "returnToReceiving"}, - {RADIO_CALL_calcAirTime, "calcAirTime"}, + {RADIO_CALL_calcAirTimeMsec, "calcAirTimeMsec"}, {RADIO_CALL_xmitDatagramP2PFindPartner, "xmitDatagramP2PFindPartner"}, {RADIO_CALL_xmitDatagramP2PSyncClocks, "xmitDatagramP2PSyncClocks"}, {RADIO_CALL_xmitDatagramP2PZeroAcks, "xmitDatagramP2PZeroAcks"}, diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 00917502..164ef666 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -494,7 +494,7 @@ typedef enum RADIO_CALL_configureRadio = 0, RADIO_CALL_setRadioFrequency, RADIO_CALL_returnToReceiving, - RADIO_CALL_calcAirTime, + RADIO_CALL_calcAirTimeMsec, RADIO_CALL_xmitDatagramP2PFindPartner, RADIO_CALL_xmitDatagramP2PSyncClocks, RADIO_CALL_xmitDatagramP2PZeroAcks, From b018c5a4ec2788225f68d4a8f39741fc92171958 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 27 Dec 2022 10:18:52 -1000 Subject: [PATCH 358/594] Add routine calcAirTimeUsec --- Firmware/LoRaSerial_Firmware/Radio.ino | 26 +++++++++++++++++-------- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a4ca9f74..89b31dd2 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -247,20 +247,30 @@ void returnToReceiving() } //Given spread factor, bandwidth, coding rate and number of bytes, return total Air Time in ms for packet -uint16_t calcAirTimeMsec(uint8_t bytesToSend) +float calcAirTimeUsec(uint8_t bytesToSend) { - radioCallHistory[RADIO_CALL_calcAirTimeMsec] = millis(); + radioCallHistory[RADIO_CALL_calcAirTimeUsec] = millis(); + + float tSymUsec = calcSymbolTimeUsec(); - float tSym = calcSymbolTimeMsec(); - float tPreamble = (settings.radioPreambleLength + 4.25) * tSym; + //See Synchronization section + float tPreambleUsec = (settings.radioPreambleLength + 2 + 2.25) * tSymUsec; + + // Rb = SF * (1 / ((2^SF) / B) = (SF * B) / 2^SF bit rate + // float p1 = (8 * bytesToSend - 4 * settings.radioSpreadFactor + 28 + 16 * 1 - 20 * 0) / (4.0 * (settings.radioSpreadFactor - 2 * 0)); p1 = ceil(p1) * settings.radioCodingRate; if (p1 < 0) p1 = 0; uint16_t payloadBytes = 8 + p1; - float tPayload = payloadBytes * tSym; - float tPacket = tPreamble + tPayload; + float tPayloadUsec = payloadBytes * tSymUsec; + float tPacketUsec = tPreambleUsec + tPayloadUsec; + return tPacketUsec; +} - return ((uint16_t)ceil(tPacket)); +//Return the frame air time in milliseconds +uint16_t calcAirTimeMsec(uint8_t bytesToSend) +{ + return ((uint16_t)ceil(calcAirTimeUsec(bytesToSend) / 1000.)); } //Given spread factor and bandwidth, return symbol time @@ -3133,7 +3143,7 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_configureRadio, "configureRadio"}, {RADIO_CALL_setRadioFrequency, "setRadioFrequency"}, {RADIO_CALL_returnToReceiving, "returnToReceiving"}, - {RADIO_CALL_calcAirTimeMsec, "calcAirTimeMsec"}, + {RADIO_CALL_calcAirTimeUsec, "calcAirTimeUsec"}, {RADIO_CALL_xmitDatagramP2PFindPartner, "xmitDatagramP2PFindPartner"}, {RADIO_CALL_xmitDatagramP2PSyncClocks, "xmitDatagramP2PSyncClocks"}, {RADIO_CALL_xmitDatagramP2PZeroAcks, "xmitDatagramP2PZeroAcks"}, diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 164ef666..325b8f4c 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -494,7 +494,7 @@ typedef enum RADIO_CALL_configureRadio = 0, RADIO_CALL_setRadioFrequency, RADIO_CALL_returnToReceiving, - RADIO_CALL_calcAirTimeMsec, + RADIO_CALL_calcAirTimeUsec, RADIO_CALL_xmitDatagramP2PFindPartner, RADIO_CALL_xmitDatagramP2PSyncClocks, RADIO_CALL_xmitDatagramP2PZeroAcks, From eed60a6be1866ee451908c2dc76295db7af7e086 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 27 Dec 2022 13:53:14 -1000 Subject: [PATCH 359/594] Compute maxFrameAirTime --- .../LoRaSerial_Firmware.ino | 65 +++++++++++++++++++ Firmware/LoRaSerial_Firmware/States.ino | 3 + 2 files changed, 68 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 147e7164..598e5791 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -503,6 +503,71 @@ bool originalServer = false; //Temp store server setting if we need to exit butt char platformPrefix[25]; //Used for printing platform specific device name, ie "SAMD21 1W 915MHz" //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +/* + Server Client + + HEARTBEAT send needed + call xmitDatagramP2PHeartbeat + .-------------------- Get millis() + | Add millis to HEARTBEAT frame + | * USB Interrupt + | * hopChannel if necessary + | Software CRC + | txTimeUsec Data encryption + | Data whitening + | Move data to radio via SPI + | xmitTimeMillis = millis() + | * Radio arbitrary delay + | TX HEARTBEAT - - > Detect RX in process + | ... ... + | TX done RX HEARTBEAT + | * Radio arbitrary delay Radio arbitrary delay * + '--- Transaction complete detected USB Interrupt * + .------- DIO0 Interrupt + | Delay - finish previous loop + rxTimeUsec | Transaction complete detected + | call receiveDatagram + | USB Interrupt + +------- Get millis() + Move data from radio using SPI + Data whitening + Data decryption + Software CRC + Start processing HEARTBEAT datagram + Get millis(); + + CPU Clock drift + + timeStampOffset = TX millis + txTimeUsec + rxTimeUsec + + RX uSec RX Var TX uSec Hop uSec + None 1379 216 1970 22 + Encryption 1957 155 2540 13 + CRC 1387 178 1986 5 + CRC + Encryption 1960 223 2557 15 + Whitening 1393 231 1993 28 + Whitening + Encryption 1963 263 2562 22 + Whitening + CRC 1415 249 2014 13 + Whitening + CRC + Encryption 1987 220 2584 27 + + Options RX TX + CRC: 8 16 uSec + Encryption: 578 570 uSec + CRC + Encr: 581 587 uSec + Whitening 14 15 uSec + White + Enc: 584 592 uSec + White + CRC: 36 44 uSec + Wh + CRC + En: 608 614 uSec + +*/ + +//Clock synchronization +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +uint16_t maxFrameAirTime; //Air time of the maximum sized message + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + //Architecture variables //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- void updateRTS(bool assertRTS); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 2dfb9375..eda96e15 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -109,6 +109,9 @@ void updateRadioState() configureRadio(); //Setup radio, set freq to channel 0, calculate air times + //Determine the maximum frame air time + maxFrameAirTime = calcAirTimeMsec(MAX_PACKET_SIZE); + //Start the TX timer: time to delay before transmitting the FIND_PARTNER setHeartbeatShort(); //Both radios start with short heartbeat period randomTime = random(ackAirTime, ackAirTime * 2); //Fast FIND_PARTNER From e035e206534f7baa38e973cabcffa0f6ea46e0ef Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 28 Dec 2022 06:02:24 -1000 Subject: [PATCH 360/594] Compute the transmit time in uSec --- .../LoRaSerial_Firmware.ino | 4 ++++ Firmware/LoRaSerial_Firmware/Radio.ino | 7 +++++-- Firmware/LoRaSerial_Firmware/States.ino | 18 ++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 598e5791..13802904 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -564,6 +564,10 @@ char platformPrefix[25]; //Used for printing platform specific device name, ie " //Clock synchronization //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +uint32_t txTimeUsec; + +uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine +uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram routine uint16_t maxFrameAirTime; //Air time of the maximum sized message //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 89b31dd2..e2acd20e 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -678,6 +678,7 @@ void printSX1276Registers() //Called when transmission is complete or when RX is received void transactionCompleteISR(void) { + transactionCompleteMicros = micros(); radioCallHistory[RADIO_CALL_transactionCompleteISR] = millis(); transactionComplete = true; @@ -2247,11 +2248,13 @@ bool transmitDatagram() VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; - radioCallHistory[RADIO_CALL_transmitDatagram] = millis(); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); + //Remove some jitter by getting this time after the hopChannel + txDatagramMicros = micros(); + radioCallHistory[RADIO_CALL_transmitDatagram] = millis(); + //Parse the virtual circuit header vc = &virtualCircuitList[0]; vcHeader = NULL; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index eda96e15..bd11bec7 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -48,6 +48,21 @@ ackTimer = 0; \ } +#define COMPUTE_TX_TIME() \ + { \ + currentMillis = millis(); \ + if (!txTimeUsec) \ + { \ + txTimeUsec = transactionCompleteMicros - txDatagramMicros; \ + \ + /*Display the results*/ \ + systemPrintTimestamp(currentMillis + timestampOffset); \ + systemPrint(" TX Time: "); \ + systemPrint(txTimeUsec); \ + systemPrintln(" uSec"); \ + } \ + } + //Process the radio states void updateRadioState() { @@ -290,6 +305,7 @@ void updateRadioState() //Determine if a FIND_PARTNER has completed transmission if (transactionComplete) { + COMPUTE_TX_TIME(); triggerEvent(TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE); transactionComplete = false; //Reset ISR flag returnToReceiving(); @@ -400,6 +416,7 @@ void updateRadioState() //Determine if a SYNC_CLOCKS has completed transmission if (transactionComplete) { + COMPUTE_TX_TIME(); triggerEvent(TRIGGER_HANDSHAKE_SEND_SYNC_CLOCKS_COMPLETE); transactionComplete = false; //Reset ISR flag returnToReceiving(); @@ -490,6 +507,7 @@ void updateRadioState() if (transactionComplete) { transactionComplete = false; //Reset ISR flag + COMPUTE_TX_TIME(); startChannelTimer(); //We are exiting the link first so do not adjust our starting Timer From a7949c0ceb4f919d54d9f6a809d7b441a7c231f0 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 28 Dec 2022 06:12:17 -1000 Subject: [PATCH 361/594] Compute the receive time --- .../LoRaSerial_Firmware.ino | 2 ++ Firmware/LoRaSerial_Firmware/States.ino | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 13802904..e6239de5 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -564,6 +564,8 @@ char platformPrefix[25]; //Used for printing platform specific device name, ie " //Clock synchronization //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//RX and TX time measurements +uint32_t rxTimeUsec; uint32_t txTimeUsec; uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bd11bec7..b83965e2 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -48,6 +48,21 @@ ackTimer = 0; \ } +#define COMPUTE_RX_TIME(millisBuffer) \ + { \ + currentMillis = millis(); \ + if (!rxTimeUsec) \ + { \ + rxTimeUsec = micros() - transactionCompleteMicros; \ + \ + /*Display the results*/ \ + systemPrintTimestamp(radioCallHistory[RADIO_CALL_transactionCompleteISR] + timestampOffset); \ + systemPrint(" RX Time: "); \ + systemPrint(rxTimeUsec); \ + systemPrintln(" uSec"); \ + } \ + } + #define COMPUTE_TX_TIME() \ { \ currentMillis = millis(); \ @@ -268,6 +283,9 @@ void updateRadioState() case DATAGRAM_FIND_PARTNER: //Received FIND_PARTNER + //Compute the receive time + COMPUTE_RX_TIME(rxData); + //Compute the common clock currentMillis = millis(); memcpy(&clockOffset, rxData, sizeof(currentMillis)); @@ -363,6 +381,9 @@ void updateRadioState() case DATAGRAM_SYNC_CLOCKS: //Received SYNC_CLOCKS + //Compute the receive time + COMPUTE_RX_TIME(rxData + 1); + //Compute the common clock currentMillis = millis(); memcpy(&clockOffset, rxData + 1, sizeof(currentMillis)); @@ -454,6 +475,9 @@ void updateRadioState() case DATAGRAM_ZERO_ACKS: //Received ACK 2 + //Compute the receive time + COMPUTE_RX_TIME(rxData); + //Compute the common clock currentMillis = millis(); memcpy(&clockOffset, rxData, sizeof(currentMillis)); From e796fe3b3a799da11fe5fd16c0701462a382d50e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 28 Dec 2022 06:24:54 -1000 Subject: [PATCH 362/594] Compute the timestamp offset --- .../LoRaSerial_Firmware.ino | 4 +- Firmware/LoRaSerial_Firmware/States.ino | 71 ++++++------------- 2 files changed, 24 insertions(+), 51 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index e6239de5..832ef06d 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -448,8 +448,7 @@ unsigned long linkDownTimer; //Clock synchronization unsigned long rcvTimeMillis; unsigned long xmitTimeMillis; -unsigned long timestampOffset; -unsigned long roundTripMillis; +long timestampOffset; unsigned long vcTxHeartbeatMillis; unsigned long nextChannelZeroTimeInMillis; @@ -571,6 +570,7 @@ uint32_t txTimeUsec; uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram routine uint16_t maxFrameAirTime; //Air time of the maximum sized message +unsigned long remoteSystemMillis; //Millis value contained in the received message //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b83965e2..8ca10817 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -48,6 +48,13 @@ ackTimer = 0; \ } +#define COMPUTE_TIMESTAMP_OFFSET(millisBuffer) \ + { \ + unsigned long deltaUsec = txTimeUsec + rxTimeUsec; \ + memcpy(&remoteSystemMillis, millisBuffer, sizeof(currentMillis)); \ + timestampOffset = (remoteSystemMillis + (deltaUsec / 1000) - currentMillis);\ + } + #define COMPUTE_RX_TIME(millisBuffer) \ { \ currentMillis = millis(); \ @@ -61,6 +68,9 @@ systemPrint(rxTimeUsec); \ systemPrintln(" uSec"); \ } \ + \ + /*Adjust the timestamp offset*/ \ + COMPUTE_TIMESTAMP_OFFSET(millisBuffer); \ } #define COMPUTE_TX_TIME() \ @@ -285,15 +295,8 @@ void updateRadioState() //Received FIND_PARTNER //Compute the receive time COMPUTE_RX_TIME(rxData); + timestampOffset >>= 1; - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; //Acknowledge the FIND_PARTNER with SYNC_CLOCKS triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); @@ -361,14 +364,9 @@ void updateRadioState() case DATAGRAM_FIND_PARTNER: //Received FIND_PARTNER - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; + //Compute the receive time + COMPUTE_RX_TIME(rxData + 1); + timestampOffset >>= 1; //Acknowledge the FIND_PARTNER triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); @@ -383,15 +381,8 @@ void updateRadioState() //Received SYNC_CLOCKS //Compute the receive time COMPUTE_RX_TIME(rxData + 1); + timestampOffset >>= 1; - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData + 1, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; //Acknowledge the SYNC_CLOCKS triggerEvent(TRIGGER_SEND_ZERO_ACKS); @@ -477,17 +468,7 @@ void updateRadioState() //Received ACK 2 //Compute the receive time COMPUTE_RX_TIME(rxData); - - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; - - startChannelTimer(getLinkupOffset()); //We are exiting the link last so adjust our starting Timer + timestampOffset >>= 1; setHeartbeatLong(); //We sent SYNC_CLOCKS and they sent ZERO_ACKS, so don't be the first to send heartbeat @@ -686,11 +667,8 @@ void updateRadioState() //Received heartbeat while link was idle. Send ack to sync clocks. //Adjust the timestamp offset currentMillis = millis(); - memcpy(&clockOffset, rxData, sizeof(currentMillis)); - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; + COMPUTE_TIMESTAMP_OFFSET(rxData); + timestampOffset >>= 1; //Transmit ACK P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); @@ -1040,21 +1018,16 @@ void updateRadioState() case DATAGRAM_SYNC_CLOCKS: triggerEvent(TRIGGER_LINK_ACK_RECEIVED); + //Compute the common clock + currentMillis = millis(); + COMPUTE_TIMESTAMP_OFFSET(rxData + 1); + //Server has responded with ACK syncChannelTimer(); //Start and adjust freq hop ISR based on remote's remaining clock //Change to the server's channel number channelNumber = rxVcData[0]; - //Compute the common clock - currentMillis = millis(); - memcpy(&clockOffset, rxData + 1, sizeof(currentMillis)); - roundTripMillis = rcvTimeMillis - xmitTimeMillis; - clockOffset += currentMillis + roundTripMillis; - clockOffset >>= 1; - clockOffset -= currentMillis; //The currentMillis is added in systemPrintTimestamp - timestampOffset = clockOffset; - if (settings.debugSync) { systemPrint(" Channel Number: "); From 0db81e5c6902afaead4fa286ee85cb9e4451e003 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:57:09 -1000 Subject: [PATCH 363/594] Add millis to DATAGRAM_DATA_ACK --- Firmware/LoRaSerial_Firmware/Radio.ino | 22 +++++++++++++--------- Firmware/LoRaSerial_Firmware/States.ino | 5 +++++ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e2acd20e..7efc905a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1041,17 +1041,21 @@ bool xmitDatagramP2PHeartbeat() //Create short packet - do not expect ack bool xmitDatagramP2PAck() { - radioCallHistory[RADIO_CALL_xmitDatagramP2PAck] = millis(); + unsigned long currentMillis = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramP2PAck] = currentMillis; + + memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); + endOfTxData += sizeof(currentMillis); /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+----------+ - | Optional | | Optional | Optional | Optional | - | NET ID | Control | C-Timer | SF6 Length | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | n Bytes | - +----------+---------+----------+------------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 4 bytes | n Bytes | + +----------+---------+----------+------------+---------+----------+ */ txControl.datagramType = DATAGRAM_DATA_ACK; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 8ca10817..14058b09 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -692,6 +692,11 @@ void updateRadioState() break; case DATAGRAM_DATA_ACK: + //Adjust the timestamp offset + currentMillis = millis(); + COMPUTE_TIMESTAMP_OFFSET(rxData); + timestampOffset >>= 1; + //The datagram we are expecting syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock From 05d0c1f9a0fbceb565faee1276b3141f421fbde8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 28 Dec 2022 06:56:13 -1000 Subject: [PATCH 364/594] debugSync: Output sync calculations after starting timer --- Firmware/LoRaSerial_Firmware/Radio.ino | 24 +++++++++--------- Firmware/LoRaSerial_Firmware/States.ino | 33 +++++++++++++++++++------ 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 7efc905a..78bb4271 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2974,17 +2974,6 @@ void syncChannelTimer() while (msToNextHop < 0) msToNextHop += settings.maxDwellTime; - if (settings.debugSync) - { - systemPrint("msToNextHopRemote: "); - systemPrint(msToNextHopRemote); - systemPrint(" msToNextHopLocal: "); - systemPrint(msToNextHopLocal); - systemPrint(" msToNextHop: "); - systemPrint(msToNextHop); - systemPrintln(); - } - partialTimer = true; channelTimer.disableTimer(); channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's @@ -2993,10 +2982,19 @@ void syncChannelTimer() timeToHop = false; channelTimer.enableTimer(); - triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug + + //Display the channel sync timer calculations if (settings.debugSync) - outputSerialData(true); + { + systemPrint("msToNextHopRemote: "); + systemPrint(msToNextHopRemote); + systemPrint(" msToNextHopLocal: "); + systemPrint(msToNextHopLocal); + systemPrint(" msToNextHop: "); + systemPrint(msToNextHop); + systemPrintln(); + } } //This function resets the heartbeat time and re-rolls the random time diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 14058b09..bb777e07 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -63,10 +63,13 @@ rxTimeUsec = micros() - transactionCompleteMicros; \ \ /*Display the results*/ \ - systemPrintTimestamp(radioCallHistory[RADIO_CALL_transactionCompleteISR] + timestampOffset); \ - systemPrint(" RX Time: "); \ - systemPrint(rxTimeUsec); \ - systemPrintln(" uSec"); \ + if (settings.debugSync) \ + { \ + systemPrintTimestamp(radioCallHistory[RADIO_CALL_transactionCompleteISR] + timestampOffset); \ + systemPrint(" RX Time: "); \ + systemPrint(rxTimeUsec); \ + systemPrintln(" uSec"); \ + } \ } \ \ /*Adjust the timestamp offset*/ \ @@ -81,10 +84,13 @@ txTimeUsec = transactionCompleteMicros - txDatagramMicros; \ \ /*Display the results*/ \ - systemPrintTimestamp(currentMillis + timestampOffset); \ - systemPrint(" TX Time: "); \ - systemPrint(txTimeUsec); \ - systemPrintln(" uSec"); \ + if (settings.debugSync) \ + { \ + systemPrintTimestamp(currentMillis + timestampOffset); \ + systemPrint(" TX Time: "); \ + systemPrint(txTimeUsec); \ + systemPrintln(" uSec"); \ + } \ } \ } @@ -967,6 +973,12 @@ void updateRadioState() case RADIO_DISCOVER_BEGIN: stopChannelTimer(); //Stop hopping + if (settings.debugSync) + { + systemPrintln("Start scanning"); + outputSerialData(true); + } + multipointChannelLoops = 0; multipointAttempts = 0; @@ -1249,6 +1261,11 @@ void updateRadioState() { if ((millis() - lastPacketReceived) > (settings.heartbeatTimeout * 3)) { + if (settings.debugSync) + { + systemPrintln("HEARTBEAT Timeout"); + outputSerialData(true); + } changeState(RADIO_DISCOVER_BEGIN); } } From 02e55d4d8d8ef42eb521e63e8326a32d9b7ece09 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 28 Dec 2022 06:59:43 -1000 Subject: [PATCH 365/594] Make sure the TX channel timer value is in the range of 0 - maxDwellTime --- Firmware/LoRaSerial_Firmware/Radio.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 78bb4271..4e7b0a59 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2419,6 +2419,9 @@ bool transmitDatagram() //Add the clock sync information if (settings.frequencyHop == true) { + //Hake sure that the transmitted msToNextHop is in the range 0 - maxDwellTime + if (timeToHop) + hopChannel(); uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); memcpy(header, &msToNextHop, sizeof(msToNextHop)); header += sizeof(msToNextHop); //aka CHANNEL_TIMER_BYTES From 126c59864381eed542a97b1281a414d500f06ccc Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 13:55:15 -1000 Subject: [PATCH 366/594] Use common timestamp when displaying radio metrics ATI10 and ATI12 --- Firmware/LoRaSerial_Firmware/Commands.ino | 48 ++++++++++++----------- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index a2e07547..9160ea98 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -298,21 +298,25 @@ bool commandAT(const char * commandString) case ('0'): //ATI10 - Display radio metrics systemPrint("Radio Metrics @ "); - systemPrintTimestamp(millis()); + systemPrintTimestamp(millis() + timestampOffset); systemPrintln(); if (settings.operatingMode == MODE_POINT_TO_POINT) { systemPrint(" Link Status: "); systemPrintln((radioState >= RADIO_P2P_LINK_UP) ? "Up" : "Down"); systemPrint(" Last RX: "); - systemPrintTimestamp(lastRxDatagram); + systemPrintTimestamp(lastRxDatagram + timestampOffset); systemPrintln(); systemPrint(" First RX: "); - systemPrintTimestamp(lastLinkUpTime); - systemPrintln(); - systemPrint(" Up Time: "); - systemPrintTimestamp(lastRxDatagram - lastLinkUpTime); + systemPrintTimestamp(lastLinkUpTime + timestampOffset); systemPrintln(); + delayMillis = lastRxDatagram - lastLinkUpTime; + if (((int)delayMillis) >= 0) + { + systemPrint(" Up Time: "); + systemPrintTimestamp(delayMillis); + systemPrintln(); + } outputSerialData(true); petWDT(); } @@ -324,11 +328,11 @@ bool commandAT(const char * commandString) systemPrint(" Frames: "); systemPrintln(framesSent); systemPrint(" Lost Frames: "); + systemPrintln(lostFrames); outputSerialData(true); petWDT(); //Receiver metrics - systemPrintln(lostFrames); systemPrintln(" Received"); systemPrint(" Frames: "); systemPrintln(framesReceived); @@ -362,7 +366,7 @@ bool commandAT(const char * commandString) systemPrintln(vc->txAckNumber); systemPrint(" ackTimer: "); if (ackTimer) - systemPrintTimestamp(ackTimer); + systemPrintTimestamp(ackTimer + timestampOffset); else systemPrint("Not Running"); systemPrintln(); @@ -376,7 +380,7 @@ bool commandAT(const char * commandString) systemPrint("VC "); systemPrint(txDestVc); systemPrint(", "); - systemPrintTimestamp(ackTimer); + systemPrintTimestamp(ackTimer + timestampOffset); systemPrintln(); } else @@ -469,7 +473,7 @@ bool commandAT(const char * commandString) if (channelNumber) { systemPrint(" Next Ch 0 time: "); - systemPrintTimestamp(nextChannelZeroTimeInMillis); + systemPrintTimestamp(nextChannelZeroTimeInMillis + timestampOffset); int hopCount = settings.numberOfChannels - channelNumber; systemPrint(", in "); systemPrint(hopCount); @@ -478,7 +482,7 @@ bool commandAT(const char * commandString) systemPrint(" Last Successful Transmit: "); if (txSuccessMillis) { - systemPrintTimestamp(txSuccessMillis); + systemPrintTimestamp(txSuccessMillis + timestampOffset); systemPrintln(); } else @@ -486,7 +490,7 @@ bool commandAT(const char * commandString) systemPrint(" Last Transmit Failure: "); if (txFailureMillis) { - systemPrintTimestamp(txFailureMillis); + systemPrintTimestamp(txFailureMillis + timestampOffset); systemPrint(", Status: "); systemPrint(txFailureState); string = getRadioStatusCode(txFailureState); @@ -502,7 +506,7 @@ bool commandAT(const char * commandString) systemPrint(" Last Successful Receive: "); if (rxSuccessMillis) { - systemPrintTimestamp(rxSuccessMillis); + systemPrintTimestamp(rxSuccessMillis + timestampOffset); systemPrintln(); } else @@ -510,7 +514,7 @@ bool commandAT(const char * commandString) systemPrint(" Last Receive Failure: "); if (rxFailureMillis) { - systemPrintTimestamp(rxFailureMillis); + systemPrintTimestamp(rxFailureMillis + timestampOffset); systemPrint(", Status: "); systemPrint(rxFailureState); string = getRadioStatusCode(rxFailureState); @@ -526,7 +530,7 @@ bool commandAT(const char * commandString) systemPrint(" radio.startReceive Failure: "); if (startReceiveFailureMillis) { - systemPrintTimestamp(startReceiveFailureMillis); + systemPrintTimestamp(startReceiveFailureMillis + timestampOffset); systemPrintln(); systemPrint(" radio.startReceive Status: "); systemPrintln(startReceiveFailureState); @@ -536,7 +540,7 @@ bool commandAT(const char * commandString) systemPrint(" transactionComplete: "); systemPrintln(transactionComplete ? "True" : "False"); systemPrint(" lastReceiveInProcessTrue @ "); - systemPrintTimestamp(lastReceiveInProcessTrue); + systemPrintTimestamp(lastReceiveInProcessTrue + timestampOffset); systemPrintln(); systemPrint(" lastModemStatus: "); systemPrint(lastModemStatus, HEX); @@ -570,14 +574,14 @@ bool commandAT(const char * commandString) if (!vc->flags.valid) { systemPrint("Down, Not valid @ "); - systemPrintTimestamp(millis()); + systemPrintTimestamp(millis() + timestampOffset); systemPrintln(); } else { systemPrint(vcStateNames[vc->vcState]); systemPrint(" @ "); - systemPrintTimestamp(millis()); + systemPrintTimestamp(millis() + timestampOffset); systemPrintln(); systemPrint(" ID: "); systemPrintUniqueID(vc->uniqueId); @@ -588,14 +592,14 @@ bool commandAT(const char * commandString) if (cmdVc == myVc) { systemPrint(" Next TX: "); - systemPrintTimestamp(heartbeatTimer + heartbeatRandomTime); + systemPrintTimestamp(heartbeatTimer + heartbeatRandomTime + timestampOffset); systemPrintln(); } systemPrint(" Last: "); - systemPrintTimestamp(vc->lastTrafficMillis); + systemPrintTimestamp(vc->lastTrafficMillis + timestampOffset); systemPrintln(); systemPrint(" First: "); - systemPrintTimestamp(vc->firstHeartbeatMillis); + systemPrintTimestamp(vc->firstHeartbeatMillis + timestampOffset); systemPrintln(); systemPrint(" Up Time: "); deltaMillis = vc->lastTrafficMillis - vc->firstHeartbeatMillis; @@ -640,7 +644,7 @@ bool commandAT(const char * commandString) { systemPrint(" ackTimer: "); if (ackTimer) - systemPrintTimestamp(ackTimer); + systemPrintTimestamp(ackTimer + timestampOffset); else systemPrint("Not Running"); systemPrintln(); diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4e7b0a59..47a68fcf 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3240,7 +3240,7 @@ void displayRadioCallHistory() if (radioCallHistory[sortOrder[index]]) { systemPrint(" "); - systemPrintTimestamp(radioCallHistory[sortOrder[index]]); + systemPrintTimestamp(radioCallHistory[sortOrder[index]] + timestampOffset); string = getRadioCall(sortOrder[index]); if (string) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bb777e07..3a033901 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2499,7 +2499,7 @@ void displayRadioStateHistory() if (radioStateHistory[sortOrder[index]]) { systemPrint(" "); - systemPrintTimestamp(radioStateHistory[sortOrder[index]]); + systemPrintTimestamp(radioStateHistory[sortOrder[index]] + timestampOffset); systemPrint(": "); systemPrint(radioStateTable[sortOrder[index]].name); systemPrintln(); From d9dee0262c524390dd7dbc209aaa19a84c7d576b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 14:07:44 -1000 Subject: [PATCH 367/594] Add LEDS_MULTIPOINT to display HEARTBEAT and hopChannel --- .../LoRaSerial_Firmware.ino | 9 +++ Firmware/LoRaSerial_Firmware/Radio.ino | 4 + Firmware/LoRaSerial_Firmware/States.ino | 8 ++ Firmware/LoRaSerial_Firmware/System.ino | 75 ++++++++++++++++--- Firmware/LoRaSerial_Firmware/settings.h | 7 +- 5 files changed, 92 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 832ef06d..e00a270c 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -112,6 +112,9 @@ uint8_t pin_trigger = PIN_UNDEFINED; #define RADIO_USE_BAD_FRAMES_LED BLUE_LED //Blue #define RADIO_USE_BAD_CRC_LED YELLOW_LED //Yellow +#define LED_MP_HOP_CHANNEL BLUE_LED +#define LED_MP_HEARTBEAT YELLOW_LED + #define CYLON_TX_DATA_LED BLUE_LED #define CYLON_RX_DATA_LED YELLOW_LED @@ -380,6 +383,12 @@ const int rssiLevelHigh = -100; const int rssiLevelMax = -70; int rssi; //Average signal level, measured during reception of a packet +//LED control values +uint32_t ledPreviousRssiMillis; +uint32_t ledHeartbeatMillis; +int8_t ledRssiCount; //Pulse width count for LED display +int8_t ledRssiValue; //Target pulse width count for LED display + //Link quality metrics uint32_t datagramsSent; //Total number of datagrams sent uint32_t datagramsReceived; //Total number of datagrams received diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 47a68fcf..ff4a735d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -535,6 +535,8 @@ void hopChannelReverse() //Set the next radio frequency given the hop direction and frequency table void hopChannel(bool moveForwardThroughTable) { + radioCallHistory[RADIO_CALL_hopChannel] = millis(); + timeToHop = false; triggerEvent(TRIGGER_FREQ_CHANGE); @@ -3181,6 +3183,8 @@ const I16_TO_STRING radioCallName[] = {RADIO_CALL_setHeartbeatLong, "setHeartbeatLong"}, {RADIO_CALL_setHeartbeatMultipoint, "setHeartbeatMultipoint"}, {RADIO_CALL_setVcHeartbeatTimer, "setVcHeartbeatTimer"}, + {RADIO_CALL_hopChannel, "hopChannel"}, + //Insert new values before this line {RADIO_CALL_hopISR, "hopISR"}, {RADIO_CALL_transactionCompleteISR, "transactionCompleteISR"}, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3a033901..69bdeea5 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1201,6 +1201,7 @@ void updateRadioState() lastPacketReceived = millis(); //Update timestamp for Link LED + ledMpHeartbeatOn(); changeState(RADIO_MP_STANDBY); break; @@ -1242,6 +1243,7 @@ void updateRadioState() setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer changeState(RADIO_MP_WAIT_TX_DONE); //Wait for heartbeat to transmit } + ledMpHeartbeatOn(); } } @@ -2222,6 +2224,12 @@ bool isLinked() return (false); } +//Determine if multi-point sync is achieved +bool isMultiPointSync() +{ + return ((radioState >= RADIO_MP_STANDBY) && (radioState <= RADIO_MP_WAIT_TX_DONE)); +} + //Verify the radio state definitions against the radioStateTable bool verifyRadioStateTable() { diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index fc6d6f30..0d17a326 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -630,13 +630,10 @@ void rxLED(bool illuminate) void radioLeds() { uint32_t currentMillis; - static uint32_t previousMillis; static uint32_t previousBadFrames; static uint32_t badFramesMillis; static uint32_t previousBadCrc; static uint32_t badCrcMillis; - static int8_t rssiCount = 127; - static int8_t rssiValue; //Turn off the RX LED to end the blink currentMillis = millis(); @@ -684,23 +681,76 @@ void radioLeds() digitalWrite(RADIO_USE_LINK_LED, isLinked() ? LED_ON : LED_OFF); //Update the RSSI LED - if (currentMillis != previousMillis) + if (currentMillis != ledPreviousRssiMillis) { - if (rssiCount >= 16) + if (ledRssiCount >= 16) { - rssiValue = (150 + rssi) / 10; - rssiCount = 0; - if (rssiValue >= rssiCount) + ledRssiValue = (150 + rssi) / 10; + ledRssiCount = 0; + if (ledRssiValue >= ledRssiCount) digitalWrite(RADIO_USE_RSSI_LED, LED_ON); } //Turn off the RSSI LED - else if (rssiValue < rssiCount++) + else if (ledRssiValue < ledRssiCount++) digitalWrite(RADIO_USE_RSSI_LED, LED_OFF); } //Save the last millis value - previousMillis = currentMillis; + ledPreviousRssiMillis = currentMillis; +} + +//Turn on the heartbeat LED +void ledMpHeartbeatOn() +{ + digitalWrite(LED_MP_HEARTBEAT, LED_OFF); + ledHeartbeatMillis = millis(); +} + +//Display the multi-point LED pattern +void multiPointLeds() +{ + uint32_t currentMillis; + + //Turn off the RX LED to end the blink + currentMillis = millis(); + if ((currentMillis - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(RADIO_USE_RX_DATA_LED, LED_OFF); + + //Turn off the TX LED to end the blink + currentMillis = millis(); + if ((currentMillis - datagramTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(RADIO_USE_TX_DATA_LED, LED_OFF); + + //Update the sync LED + digitalWrite(RADIO_USE_LINK_LED, isMultiPointSync() ? LED_ON : LED_OFF); + + //Update the RSSI LED + if (currentMillis != ledPreviousRssiMillis) + { + if (ledRssiCount >= 16) + { + ledRssiValue = (150 + rssi) / 10; + ledRssiCount = 0; + if (ledRssiValue >= ledRssiCount) + digitalWrite(RADIO_USE_RSSI_LED, LED_ON); + } + + //Turn off the RSSI LED + else if (ledRssiValue < ledRssiCount++) + digitalWrite(RADIO_USE_RSSI_LED, LED_OFF); + } + + //Update the hop LED + if ((millis() - radioCallHistory[RADIO_CALL_hopChannel]) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(LED_MP_HOP_CHANNEL, LED_OFF); + + //Update the HEARTBEAT LED + if ((millis() - ledHeartbeatMillis) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(LED_MP_HEARTBEAT, LED_OFF); + + //Save the last millis value + ledPreviousRssiMillis = currentMillis; } //Start the cylon LEDs @@ -780,6 +830,11 @@ void updateLeds() radioLeds(); break; + //Display the multipoint LED pattern + case LEDS_MULTIPOINT: + multiPointLeds(); + break; + //Display the cylon pattern during training case LEDS_CYLON: updateCylonLEDs(); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 325b8f4c..1c1a5897 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -340,7 +340,8 @@ typedef enum LEDS_RSSI = 0, //Green: RSSI, Blue: Serial TX, Yellow: Serial RX LEDS_RADIO_USE, //Green1: RX, Green2: Link, Green3: RSSI, Green4: TX //Blue: Bad frames, Yellow: Bad CRC - LEDS_CYLON, //Display the cylon pattern on the green LEDs, others off + LEDS_MULTIPOINT, //Green1: RX, Green2: Sync, Green3: RSSI, Green4: TX + //Blue: Hop, Yellow: HEARTBEAT RX/TX LEDS_ALL_OFF, //All LEDs off LEDS_BLUE_ON, //Blue: ON, other: OFF LEDS_YELLOW_ON, //Yellow: ON, other: OFF @@ -349,6 +350,8 @@ typedef enum LEDS_GREEN_3_ON, //Green 3: ON, other: OFF LEDS_GREEN_4_ON, //Green 4: ON, other: OFF + LEDS_CYLON, //Display the cylon pattern on the green LEDs, others off + //Add user LED types from 255 working down } LEDS_USE_TYPE; @@ -524,6 +527,8 @@ typedef enum RADIO_CALL_setHeartbeatLong, RADIO_CALL_setHeartbeatMultipoint, RADIO_CALL_setVcHeartbeatTimer, + RADIO_CALL_hopChannel, + //Insert new values before this line RADIO_CALL_transactionCompleteISR, RADIO_CALL_hopISR, From d7e2a2e2c394594f730edc5668b4cba009aa2c4e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 14:11:27 -1000 Subject: [PATCH 368/594] Add printChannel setting --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 17 +++++++++++++++-- Firmware/LoRaSerial_Firmware/settings.h | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9160ea98..4d6e910e 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1039,6 +1039,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &tempSettings.debugSerial}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &tempSettings.displayRealMillis}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintAckNumbers", &tempSettings.printAckNumbers}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintChannel", &tempSettings.printChannel}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &tempSettings.printFrequency}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintLinkUpDown", &tempSettings.printLinkUpDown}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintPacketQuality", &tempSettings.printPacketQuality}, diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index ff4a735d..fcadfa7b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -174,9 +174,13 @@ void convertAirSpeedToSettings() //Set radio frequency bool setRadioFrequency(bool rxAdjust) { + float previousFrequency; + static uint8_t previousChannelNumber; + radioCallHistory[RADIO_CALL_setRadioFrequency] = millis(); //Determine the frequency to use + previousFrequency = radioFrequency; radioFrequency = channels[channelNumber]; if (rxAdjust && settings.autoTuneFrequency) radioFrequency -= frequencyCorrection; @@ -187,12 +191,21 @@ bool setRadioFrequency(bool rxAdjust) if (settings.debugSync) triggerFrequency(radioFrequency); + else + triggerEvent(TRIGGER_FREQ_CHANGE); //Determine the time in milliseconds when channel zero is reached again nextChannelZeroTimeInMillis = millis() + ((settings.numberOfChannels - channelNumber) * settings.maxDwellTime); //Print the frequency if requested - if (settings.printFrequency) + if (settings.printChannel && (previousChannelNumber != channelNumber)) + { + systemPrintTimestamp(); + systemPrint("CH"); + systemPrintln(channelNumber); + outputSerialData(true); + } + if (settings.printFrequency && (previousFrequency != radioFrequency)) { systemPrintTimestamp(); systemPrint(channelNumber); @@ -203,6 +216,7 @@ bool setRadioFrequency(bool rxAdjust) systemPrintln(" mSec"); outputSerialData(true); } + previousChannelNumber = channelNumber; return true; } @@ -538,7 +552,6 @@ void hopChannel(bool moveForwardThroughTable) radioCallHistory[RADIO_CALL_hopChannel] = millis(); timeToHop = false; - triggerEvent(TRIGGER_FREQ_CHANGE); if (moveForwardThroughTable) { diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 1c1a5897..6710f5f2 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -433,6 +433,7 @@ typedef struct struct_settings { bool printAckNumbers = false; //Print the ACK numbers bool debugHeartbeat = false; //Print the HEARTBEAT timing values uint8_t framesToYield = 3; //If remote requests it, supress transmission for this number of max packet frames + bool printChannel = false; //Print the channel number //Add new parameters immediately before this line //-- Add commands to set the parameters From 9913c0baf24a751f2f5f922a4229bfd135bf484a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 14:26:07 -1000 Subject: [PATCH 369/594] Add TRIGGER_TRANSACTION_COMPLETE --- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index fcadfa7b..913f78fa 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -694,6 +694,7 @@ void printSX1276Registers() void transactionCompleteISR(void) { transactionCompleteMicros = micros(); + triggerEvent(TRIGGER_TRANSACTION_COMPLETE); radioCallHistory[RADIO_CALL_transactionCompleteISR] = millis(); transactionComplete = true; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 6710f5f2..c1fc2b6f 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -240,6 +240,7 @@ enum // triggerEnable = 0xffffffff TRIGGER_CHANNEL_TIMER_ISR, //0 + TRIGGER_TRANSACTION_COMPLETE, TRIGGER_RADIO_RESET, TRIGGER_HOP_TIMER_START, TRIGGER_HOP_TIMER_STOP, From 16303ec68e6fbb9899c092f6df86d761d9ff298e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 14:33:00 -1000 Subject: [PATCH 370/594] Add TRIGGER_TX_SPI_DONE --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 ++ Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 913f78fa..a0e92bce 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2778,11 +2778,13 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) } else { + //Move the data to the radio over SPI int state = radio.startTransmit(outgoingPacket, txDatagramSize); if (state == RADIOLIB_ERR_NONE) { xmitTimeMillis = millis(); + triggerEvent(TRIGGER_TX_SPI_DONE); txSuccessMillis = xmitTimeMillis; frameSentCount++; if (vc) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index c1fc2b6f..ee448b8f 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -241,6 +241,7 @@ enum TRIGGER_CHANNEL_TIMER_ISR, //0 TRIGGER_TRANSACTION_COMPLETE, + TRIGGER_TX_SPI_DONE, TRIGGER_RADIO_RESET, TRIGGER_HOP_TIMER_START, TRIGGER_HOP_TIMER_STOP, From 57194bb25557ed2f1ebe15f3aa5262dd17f6bcf3 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 14:33:21 -1000 Subject: [PATCH 371/594] Add TRIGGER_RX_SPI_DONE --- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a0e92bce..0733e200 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1539,6 +1539,7 @@ PacketType rcvDatagram() if (state == RADIOLIB_ERR_NONE) { rxSuccessMillis = rcvTimeMillis; + triggerEvent(TRIGGER_RX_SPI_DONE); rssi = radio.getRSSI(); } else diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index ee448b8f..0ec50b86 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -242,6 +242,7 @@ enum TRIGGER_CHANNEL_TIMER_ISR, //0 TRIGGER_TRANSACTION_COMPLETE, TRIGGER_TX_SPI_DONE, + TRIGGER_RX_SPI_DONE, TRIGGER_RADIO_RESET, TRIGGER_HOP_TIMER_START, TRIGGER_HOP_TIMER_STOP, From 8169126789759ff9ff7f59c59890df66426971ad Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:19:50 -1000 Subject: [PATCH 372/594] Add verification of P2P_FIND_PARTNER_BYTES --- .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 3 +++ Firmware/LoRaSerial_Firmware/Radio.ino | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index e00a270c..d5b1fbe7 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -65,6 +65,9 @@ const int FIRMWARE_VERSION_MINOR = 0; #define AES_KEY_BYTES 16 //Number of bytes in the encryption key #define UNIQUE_ID_BYTES 16 //Number of bytes in the unique ID +//Frame lengths +#define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame + //Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected //Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock //Bit 3: Header Info Valid toggles high when a valid Header (with correct CRC) is detected diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 0733e200..38e603a5 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -868,9 +868,12 @@ const uint16_t crc16Table[256] = //First packet in the three way handshake to bring up the link bool xmitDatagramP2PFindPartner() { - radioCallHistory[RADIO_CALL_xmitDatagramP2PFindPartner] = millis(); + uint8_t * startOfData; unsigned long currentMillis = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramP2PFindPartner] = currentMillis; + + startOfData = endOfTxData; memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(unsigned long); @@ -885,6 +888,14 @@ bool xmitDatagramP2PFindPartner() +----------+---------+----------+------------+---------+----------+ */ + //Verify the data length + if ((endOfTxData - startOfData) != P2P_FIND_PARTNER_BYTES) + { + systemPrintln("ERROR - Fix the P2P_FIND_PARTNER_BYTES value!"); + outputSerialData(true); + waitForever(); + } + txControl.datagramType = DATAGRAM_FIND_PARTNER; return (transmitDatagram()); } From 0ac1cb8ea348c6d27a9d28f876a8f89523eb453a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:22:34 -1000 Subject: [PATCH 373/594] Add verification of P2P_SYNC_CLOCKS_BYTES --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index d5b1fbe7..cec0537a 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -58,7 +58,6 @@ const int FIRMWARE_VERSION_MINOR = 0; #define CHANNEL_TIMER_BYTES sizeof(uint16_t) //Number of bytes used within in control header for clock sync (uint16_t msToNextHop) #define CLOCK_MILLIS_BYTES sizeof(unsigned long) //Number of bytes used within in various packets for system timestamps sync (unsigned long currentMillis) -#define SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame #define ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame #define MAX_PACKET_SIZE 255 //Limited by SX127x #define AES_IV_BYTES 12 //Number of bytes for AESiv @@ -67,6 +66,7 @@ const int FIRMWARE_VERSION_MINOR = 0; //Frame lengths #define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame +#define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame //Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected //Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 38e603a5..ee11349f 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -931,9 +931,9 @@ bool xmitDatagramP2PSyncClocks() */ //Verify the data length - if ((endOfTxData - startOfData) != SYNC_CLOCKS_BYTES) + if ((endOfTxData - startOfData) != P2P_SYNC_CLOCKS_BYTES) { - systemPrintln("ERROR - Fix the SYNC_CLOCKS_BYTES value!"); + systemPrintln("ERROR - Fix the P2P_SYNC_CLOCKS_BYTES value!"); outputSerialData(true); waitForever(); } From 1edc2b750437971fe4b377d5c57eed24b6e9978b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:25:43 -1000 Subject: [PATCH 374/594] Add verification of P2P_ZERO_ACKS_BYTES --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 4 ++-- Firmware/LoRaSerial_Firmware/States.ino | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index cec0537a..18955d47 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -58,7 +58,6 @@ const int FIRMWARE_VERSION_MINOR = 0; #define CHANNEL_TIMER_BYTES sizeof(uint16_t) //Number of bytes used within in control header for clock sync (uint16_t msToNextHop) #define CLOCK_MILLIS_BYTES sizeof(unsigned long) //Number of bytes used within in various packets for system timestamps sync (unsigned long currentMillis) -#define ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame #define MAX_PACKET_SIZE 255 //Limited by SX127x #define AES_IV_BYTES 12 //Number of bytes for AESiv #define AES_KEY_BYTES 16 //Number of bytes in the encryption key @@ -67,6 +66,7 @@ const int FIRMWARE_VERSION_MINOR = 0; //Frame lengths #define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame #define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame +#define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame //Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected //Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index ee11349f..6128a819 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -966,9 +966,9 @@ bool xmitDatagramP2PZeroAcks() */ //Verify the data length - if ((endOfTxData - startOfData) != ZERO_ACKS_BYTES) + if ((endOfTxData - startOfData) != P2P_ZERO_ACKS_BYTES) { - systemPrintln("ERROR - Fix the ZERO_ACKS_BYTES value!"); + systemPrintln("ERROR - Fix the P2P_ZERO_ACKS_BYTES value!"); outputSerialData(true); waitForever(); } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 69bdeea5..f7c5d1c5 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -308,7 +308,7 @@ void updateRadioState() triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { - sf6ExpectedSize = headerBytes + ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info + sf6ExpectedSize = headerBytes + P2P_ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } break; @@ -378,7 +378,7 @@ void updateRadioState() triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { - sf6ExpectedSize = headerBytes + ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info + sf6ExpectedSize = headerBytes + P2P_ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } break; From 2d1bc13eb241d172e256f7ed7a104bcc82f56835 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:29:08 -1000 Subject: [PATCH 375/594] Add verification of P2P_HEARTBEAT_BYTES --- .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 18955d47..1de1d7aa 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -67,6 +67,7 @@ const int FIRMWARE_VERSION_MINOR = 0; #define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame #define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame +#define P2P_HEARTBEAT_BYTES sizeof(unsigned long) //Number of data bytes in the HEARTBEAT frame //Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected //Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6128a819..c45440a9 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1044,9 +1044,12 @@ bool xmitDatagramP2PData() //Heartbeat packet to keep the link up bool xmitDatagramP2PHeartbeat() { - radioCallHistory[RADIO_CALL_xmitDatagramP2PHeartbeat] = millis(); + uint8_t * startOfData; unsigned long currentMillis = millis(); + radioCallHistory[RADIO_CALL_xmitDatagramP2PHeartbeat] = currentMillis; + + startOfData = endOfTxData; memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(currentMillis); @@ -1061,6 +1064,14 @@ bool xmitDatagramP2PHeartbeat() +----------+---------+----------+------------+---------+----------+ */ + //Verify the data length + if ((endOfTxData - startOfData) != P2P_HEARTBEAT_BYTES) + { + systemPrintln("ERROR - Fix the P2P_HEARTBEAT_BYTES value!"); + outputSerialData(true); + waitForever(); + } + txControl.datagramType = DATAGRAM_HEARTBEAT; return (transmitDatagram()); } From 9658fe251c286eb5b9ff76e111b97aa630c5ab42 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:32:26 -1000 Subject: [PATCH 376/594] Add verification of P2P_ACK_BYTES --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 1de1d7aa..8b458d8d 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -68,6 +68,7 @@ const int FIRMWARE_VERSION_MINOR = 0; #define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame #define P2P_HEARTBEAT_BYTES sizeof(unsigned long) //Number of data bytes in the HEARTBEAT frame +#define P2P_ACK_BYTES sizeof(unsigned long) //Number of data bytes in the ACK frame //Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected //Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index c45440a9..60b0c2d1 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1079,9 +1079,12 @@ bool xmitDatagramP2PHeartbeat() //Create short packet - do not expect ack bool xmitDatagramP2PAck() { + uint8_t * startOfData; + unsigned long currentMillis = millis(); radioCallHistory[RADIO_CALL_xmitDatagramP2PAck] = currentMillis; + startOfData = endOfTxData; memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(currentMillis); @@ -1096,6 +1099,14 @@ bool xmitDatagramP2PAck() +----------+---------+----------+------------+---------+----------+ */ + //Verify the data length + if ((endOfTxData - startOfData) != P2P_ACK_BYTES) + { + systemPrintln("ERROR - Fix the P2P_ACK_BYTES value!"); + outputSerialData(true); + waitForever(); + } + txControl.datagramType = DATAGRAM_DATA_ACK; return (transmitDatagram()); } From ec605cebd8ebd000c8eb1828df1e75df865d531d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:34:35 -1000 Subject: [PATCH 377/594] Add verification of MP_HEARTBEAT_BYTES --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 8b458d8d..ff7ec1d4 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -64,6 +64,7 @@ const int FIRMWARE_VERSION_MINOR = 0; #define UNIQUE_ID_BYTES 16 //Number of bytes in the unique ID //Frame lengths +#define MP_HEARTBEAT_BYTES 0 //Number of data bytes in the MP_HEARTBEAT frame #define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame #define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 60b0c2d1..86469b81 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1138,8 +1138,12 @@ bool xmitDatagramMpData() //Heartbeat packet to sync other units in multipoint mode bool xmitDatagramMpHeartbeat() { + uint8_t * startOfData; + radioCallHistory[RADIO_CALL_xmitDatagramMpHeartbeat] = millis(); + startOfData = endOfTxData; + /* endOfTxData ---. | @@ -1151,6 +1155,14 @@ bool xmitDatagramMpHeartbeat() +----------+---------+----------+------------+----------+ */ + //Verify the data length + if ((endOfTxData - startOfData) != MP_HEARTBEAT_BYTES) + { + systemPrintln("ERROR - Fix the MP_HEARTBEAT_BYTES value!"); + outputSerialData(true); + waitForever(); + } + txControl.datagramType = DATAGRAM_HEARTBEAT; return (transmitDatagram()); } From e5741e278883fa18b49df2e266f0da4aa1e54783 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:36:24 -1000 Subject: [PATCH 378/594] Add verification of VC_HEARTBEAT_BYTES --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index ff7ec1d4..b753af3b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -70,6 +70,7 @@ const int FIRMWARE_VERSION_MINOR = 0; #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame #define P2P_HEARTBEAT_BYTES sizeof(unsigned long) //Number of data bytes in the HEARTBEAT frame #define P2P_ACK_BYTES sizeof(unsigned long) //Number of data bytes in the ACK frame +#define VC_HEARTBEAT_BYTES 0 //Number of data bytes in the VC_HEARTBEAT frame //Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected //Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 86469b81..0f274c38 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1389,10 +1389,13 @@ bool xmitVcDatagram() bool xmitVcHeartbeat(int8_t addr, uint8_t * id) { uint32_t currentMillis = millis(); + uint8_t * startOfData; uint8_t * txData; radioCallHistory[RADIO_CALL_xmitVcHeartbeat] = currentMillis; + startOfData = endOfTxData; + //Build the VC header txData = endOfTxData; *endOfTxData++ = 0; //Reserve for length @@ -1421,6 +1424,14 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) +----------+---------+----------+------------+----------+---------+----------+---------+----------+ */ + //Verify the data length + if ((endOfTxData - startOfData) != VC_HEARTBEAT_BYTES) + { + systemPrintln("ERROR - Fix the VC_HEARTBEAT_BYTES value!"); + outputSerialData(true); + waitForever(); + } + txControl.datagramType = DATAGRAM_VC_HEARTBEAT; txControl.ackNumber = 0; From 4d3fadaea5c382b76831bd94c7de6be69a2c516b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 15:38:35 -1000 Subject: [PATCH 379/594] Clear timeToHop and partialTimer in stopChannelTimer --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 0f274c38..9bff7b76 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2918,6 +2918,8 @@ void stopChannelTimer() channelTimer.disableTimer(); triggerEvent(TRIGGER_HOP_TIMER_STOP); + timeToHop = false; + partialTimer = false; } //Given the remote unit's number of ms before its next hop, From 99ef0bdcd54deb0919d75eeeb0634ddd579697c7 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 16:29:10 -1000 Subject: [PATCH 380/594] Always create a new heartbeat message, don't use retransmitDatagram --- Firmware/LoRaSerial_Firmware/States.ino | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index f7c5d1c5..72cd1fa4 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -824,10 +824,18 @@ void updateRadioState() //Attempt the retransmission RESTORE_TX_BUFFER(); - if (retransmitDatagram(NULL) == true) + if (rexmtControl.datagramType == DATAGRAM_HEARTBEAT) + { + //Never retransmit the heartbeat, always send a new version to + //send the updated time value + if (xmitDatagramP2PHeartbeat() == true) + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } + else if (retransmitDatagram(NULL) == true) changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - setHeartbeatLong(); //We're re-sending data, so don't be the first to send next heartbeat + //We're re-sending data, so don't be the first to send next heartbeat + setHeartbeatLong(); START_ACK_TIMER(); } } From 383ccf51d1695c27608baf1a0c89a4393d76da11 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 29 Dec 2022 13:24:52 -1000 Subject: [PATCH 381/594] Rename msToNextHopRemote to msToNextHopRmt, don't change original value --- Firmware/LoRaSerial_Firmware/Radio.ino | 92 +++++++++++++------------- 1 file changed, 47 insertions(+), 45 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 9bff7b76..5eddd8cc 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2926,6 +2926,8 @@ void stopChannelTimer() //adjust our own channelTimer interrupt to be synchronized with the remote unit void syncChannelTimer() { + int16_t msToNextHopRmt; + radioCallHistory[RADIO_CALL_syncChannelTimer] = millis(); if (settings.frequencyHop == false) return; @@ -2934,10 +2936,10 @@ void syncChannelTimer() //If the sync arrived in an ACK, we know how long that packet took to transmit //Calculate the packet airTime based on the size of data received - msToNextHopRemote -= calcAirTimeMsec(packetLength); + msToNextHopRmt = msToNextHopRemote - calcAirTimeMsec(packetLength); // if (settings.debugReceive == true) - // msToNextHopRemote -= 91; //Must adjust for the blob of text being printed + // msToNextHopRmt -= 91; //Must adjust for the blob of text being printed //Different airspeeds complete the transmitComplete ISR at different rates //We adjust the clock setup as needed @@ -2946,33 +2948,33 @@ void syncChannelTimer() default: break; case (40): - msToNextHopRemote -= getReceiveCompletionOffset(); + msToNextHopRmt -= getReceiveCompletionOffset(); break; case (150): - msToNextHopRemote -= 145; + msToNextHopRmt -= 145; break; case (400): - msToNextHopRemote -= getReceiveCompletionOffset(); + msToNextHopRmt -= getReceiveCompletionOffset(); break; case (1200): - msToNextHopRemote -= getReceiveCompletionOffset(); + msToNextHopRmt -= getReceiveCompletionOffset(); break; case (2400): - msToNextHopRemote -= getReceiveCompletionOffset(); + msToNextHopRmt -= getReceiveCompletionOffset(); break; case (4800): - msToNextHopRemote -= 5; + msToNextHopRmt -= 5; break; case (9600): break; case (19200): - msToNextHopRemote -= 4; + msToNextHopRmt -= 4; break; case (28800): - msToNextHopRemote -= 2; + msToNextHopRmt -= 2; break; case (38400): - msToNextHopRemote -= 3; + msToNextHopRmt -= 3; break; } @@ -2982,66 +2984,66 @@ void syncChannelTimer() uint16_t smallAmount = settings.maxDwellTime / 8; uint16_t largeAmount = settings.maxDwellTime - smallAmount; - int16_t msToNextHop = msToNextHopRemote; //By default, we will adjust our clock to match our mate's + int16_t msToNextHop = msToNextHopRmt; //By default, we will adjust our clock to match our mate's bool resetHop = false; //The hop ISR may occur while we are forcing a hop (case A and C). Reset timeToHop as needed. //Below are the edge cases that occur when a hop occurs near ACK reception - //msToNextHopLocal is small and msToNextHopRemote is negative (and small) - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in negative (and small) then the remote has hopped - //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) - if (msToNextHopLocal < smallAmount && (msToNextHopRemote <= 0 && msToNextHopRemote >= (smallAmount * -1))) + //msToNextHopLocal is small and msToNextHopRmt is negative (and small) + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRmt comes in negative (and small) then the remote has hopped + //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRmt + dwellTime) + if (msToNextHopLocal < smallAmount && (msToNextHopRmt <= 0 && msToNextHopRmt >= (smallAmount * -1))) { hopChannel(); - msToNextHop = msToNextHopRemote + settings.maxDwellTime; + msToNextHop = msToNextHopRmt + settings.maxDwellTime; resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } - //msToNextHopLocal is large and msToNextHopRemote is negative - //If we just hopped (msToNextHopLocal is large), and msToNextHopRemote comes in negative then the remote has hopped - //No need to hop. Adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) - else if (msToNextHopLocal > largeAmount && msToNextHopRemote <= 0) + //msToNextHopLocal is large and msToNextHopRmt is negative + //If we just hopped (msToNextHopLocal is large), and msToNextHopRmt comes in negative then the remote has hopped + //No need to hop. Adjust our clock to the remote's next hop (msToNextHopRmt + dwellTime) + else if (msToNextHopLocal > largeAmount && msToNextHopRmt <= 0) { - msToNextHop = msToNextHopRemote + settings.maxDwellTime; + msToNextHop = msToNextHopRmt + settings.maxDwellTime; } - //msToNextHopLocal is small and msToNextHopRemote is large - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in large then the remote has hopped recently - //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRemote) - else if (msToNextHopLocal < smallAmount && msToNextHopRemote > largeAmount) + //msToNextHopLocal is small and msToNextHopRmt is large + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRmt comes in large then the remote has hopped recently + //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRmt) + else if (msToNextHopLocal < smallAmount && msToNextHopRmt > largeAmount) { hopChannel(); - msToNextHop = msToNextHopRemote; + msToNextHop = msToNextHopRmt; resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } - //msToNextHopLocal is large and msToNextHopRemote is large - //If we just hopped (msToNextHopLocal is large), and a msToNextHopRemote comes in large then the remote has hopped - //Then adjust our clock to the remote's next hop (msToNextHopRemote) - else if (msToNextHopLocal > largeAmount && msToNextHopRemote > largeAmount) + //msToNextHopLocal is large and msToNextHopRmt is large + //If we just hopped (msToNextHopLocal is large), and a msToNextHopRmt comes in large then the remote has hopped + //Then adjust our clock to the remote's next hop (msToNextHopRmt) + else if (msToNextHopLocal > largeAmount && msToNextHopRmt > largeAmount) { - msToNextHop = msToNextHopRemote; + msToNextHop = msToNextHopRmt; } - //msToNextHopLocal is large and msToNextHopRemote is small - //If we just hopped (msToNextHopLocal is large), and a msToNextHopRemote comes in small then the remote is about to hop - //Then adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) - else if (msToNextHopLocal > largeAmount && msToNextHopRemote < smallAmount) + //msToNextHopLocal is large and msToNextHopRmt is small + //If we just hopped (msToNextHopLocal is large), and a msToNextHopRmt comes in small then the remote is about to hop + //Then adjust our clock to the remote's next hop (msToNextHopRmt + dwellTime) + else if (msToNextHopLocal > largeAmount && msToNextHopRmt < smallAmount) { - msToNextHop = msToNextHopRemote + settings.maxDwellTime; + msToNextHop = msToNextHopRmt + settings.maxDwellTime; } - //msToNextHopLocal is small and msToNextHopRemote is small - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRemote comes in small then the remote is about to hop - //Then adjust our clock to the remote's next hop (msToNextHopRemote) - else if (msToNextHopLocal < smallAmount && msToNextHopRemote < smallAmount) + //msToNextHopLocal is small and msToNextHopRmt is small + //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRmt comes in small then the remote is about to hop + //Then adjust our clock to the remote's next hop (msToNextHopRmt) + else if (msToNextHopLocal < smallAmount && msToNextHopRmt < smallAmount) { - msToNextHop = msToNextHopRemote; + msToNextHop = msToNextHopRmt; //If we have a negative remote hop time that is larger than a dwell time then the remote has hopped again //This is seen at lower air speeds - //Hop now, and adjust our clock to the remote's next hop (msToNextHopRemote + dwellTime) + //Hop now, and adjust our clock to the remote's next hop (msToNextHopRmt + dwellTime) if (msToNextHop < (settings.maxDwellTime * -1)) //-402 < -400 { hopChannel(); @@ -3067,8 +3069,8 @@ void syncChannelTimer() //Display the channel sync timer calculations if (settings.debugSync) { - systemPrint("msToNextHopRemote: "); - systemPrint(msToNextHopRemote); + systemPrint("msToNextHopRmt: "); + systemPrint(msToNextHopRmt); systemPrint(" msToNextHopLocal: "); systemPrint(msToNextHopLocal); systemPrint(" msToNextHop: "); From 3fa1267105325e666802c75dd12048c183519a36 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 28 Dec 2022 07:11:01 -1000 Subject: [PATCH 382/594] Use the txTimeUsec and rxTimeUsec to compute the channel timer value Compare the result with the previous implementation --- Firmware/LoRaSerial_Firmware/Commands.ino | 34 ++++- .../LoRaSerial_Firmware.ino | 4 + Firmware/LoRaSerial_Firmware/Radio.ino | 117 +++++++++++++++++- Firmware/LoRaSerial_Firmware/States.ino | 72 ++++++----- Firmware/LoRaSerial_Firmware/settings.h | 4 +- 5 files changed, 197 insertions(+), 34 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 4d6e910e..61672439 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -409,6 +409,36 @@ bool commandAT(const char * commandString) outputSerialData(true); petWDT(); + //Clock synchronization + systemPrintln(" Clock Synchronization"); + systemPrint(" TX Time: "); + systemPrint(txTimeUsec / 1000., 4); + systemPrintln(" mSec"); + systemPrint(" RX Time: "); + systemPrint(rxTimeUsec / 1000., 4); + systemPrintln(" mSec"); + systemPrint(" Total Time: "); + systemPrint((txTimeUsec + rxTimeUsec) / 1000., 5); + systemPrintln(" mSec"); + systemPrint(" Uptime: "); + deltaMillis = millis(); + systemPrintTimestamp(deltaMillis); + systemPrintln(); + systemPrint(" TimeStampOffset: "); + if (timestampOffset < 0) + { + systemPrint("-"); + systemPrintTimestamp(-timestampOffset); + } + else + systemPrintTimestamp(timestampOffset); + systemPrintln(); + systemPrint(" Adjusted Time: "); + systemPrintTimestamp(deltaMillis + timestampOffset); + systemPrintln(); + outputSerialData(true); + petWDT(); + //Circular buffer metrics systemPrintln(" Circular Buffers"); systemPrint(" serialReceiveBuffer: rxHead: "); @@ -748,8 +778,8 @@ void checkCommand() //Locate the correct processing routine for the command prefix for (index = 0; index < prefixCount; index++) { - //Skip command types not supported in virtual circuit mode - if ((!prefixTable[index].supportedInVcMode) && (settings.operatingMode == MODE_VIRTUAL_CIRCUIT)) + //Skip command types not supported in current mode + if ((!prefixTable[index].supportedInVcMode) && (settings.operatingMode != MODE_POINT_TO_POINT)) continue; //Locate the prefix diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b753af3b..59880cce 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -582,12 +582,16 @@ char platformPrefix[25]; //Used for printing platform specific device name, ie " //RX and TX time measurements uint32_t rxTimeUsec; uint32_t txTimeUsec; +uint16_t txRxTimeMsec; uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram routine uint16_t maxFrameAirTime; //Air time of the maximum sized message unsigned long remoteSystemMillis; //Millis value contained in the received message +bool rxFirstAck; //Set true when first ACK is received +bool txFirstAck; //Set true when first ACK is transmitted + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Architecture variables diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 5eddd8cc..e41d5cf9 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2926,12 +2926,70 @@ void stopChannelTimer() //adjust our own channelTimer interrupt to be synchronized with the remote unit void syncChannelTimer() { + long adjustment; + unsigned long currentMillis; + int8_t deltaHops; + long deltaMillis; + long millisToNextHop; int16_t msToNextHopRmt; + bool syncError; + long txRxTimeUsec; - radioCallHistory[RADIO_CALL_syncChannelTimer] = millis(); + currentMillis = millis(); + radioCallHistory[RADIO_CALL_syncChannelTimer] = currentMillis; if (settings.frequencyHop == false) return; + //msToNextHopRemote is in the range of 0 - settings.maxDwellTime + //Validate this range + if ((msToNextHopRemote < 0) || (msToNextHopRemote > settings.maxDwellTime)) + { + int16_t channelTimer; + uint8_t * data; + + systemPrintln("RX Frame"); + dumpBuffer(incomingBuffer, headerBytes + rxDataBytes + trailerBytes); + + data = incomingBuffer; + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) + { + systemPrint(" Net Id: "); + systemPrint(*data); + systemPrint(" (0x"); + systemPrint(*data++, HEX); + systemPrintln(")"); + } + printControl(*data++); + memcpy(&channelTimer, data, sizeof(channelTimer)); + data += sizeof(channelTimer); + systemPrint(" Channel Timer(ms): "); + systemPrintln(channelTimer); + + systemPrint("ERROR: Invalid msToNextHopRemote value, "); + systemPrintln(msToNextHopRemote); + waitForever(); + } + + //Determine if the remote system has hopped and the local system has not + deltaHops = 0; + deltaMillis = msToNextHopRemote - txRxTimeMsec; + if ((deltaMillis <= 0) && ((currentMillis - timerStart) > (settings.maxDwellTime - txRxTimeMsec))) + { + //Hop one channel to catch up with the remote system + if (allowAdjustments) + hopChannel(); + deltaHops -= 1; + } + + //Compute the time to next hop + adjustment = 0; + millisToNextHop = msToNextHopRemote - txRxTimeMsec; + if (millisToNextHop <= 0) + { + adjustment = settings.maxDwellTime; + millisToNextHop += adjustment; + } + //msToNextHopRemote is obtained during rcvDatagram() //If the sync arrived in an ACK, we know how long that packet took to transmit @@ -2996,6 +3054,7 @@ void syncChannelTimer() if (msToNextHopLocal < smallAmount && (msToNextHopRmt <= 0 && msToNextHopRmt >= (smallAmount * -1))) { hopChannel(); + deltaHops += 1; msToNextHop = msToNextHopRmt + settings.maxDwellTime; resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } @@ -3014,6 +3073,7 @@ void syncChannelTimer() else if (msToNextHopLocal < smallAmount && msToNextHopRmt > largeAmount) { hopChannel(); + deltaHops += 1; msToNextHop = msToNextHopRmt; resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } @@ -3047,6 +3107,7 @@ void syncChannelTimer() if (msToNextHop < (settings.maxDwellTime * -1)) //-402 < -400 { hopChannel(); + deltaHops += 1; msToNextHop += settings.maxDwellTime; resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } @@ -3066,16 +3127,68 @@ void syncChannelTimer() channelTimer.enableTimer(); triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug + //Check for a sync error + deltaMillis = (millisToNextHop - msToNextHop + settings.maxDwellTime) % settings.maxDwellTime; + syncError = deltaHops || ((deltaMillis > 3) && (deltaMillis < (settings.maxDwellTime - 2))); + //Display the channel sync timer calculations - if (settings.debugSync) + if (settings.debugSync || syncError) { + systemPrint(msToNextHopRemote); + systemPrint(" Nxt Hop - "); + systemPrint((txTimeUsec + rxTimeUsec) / 1000); + systemPrint(" (TX + RX)"); + if (adjustment) + { + systemPrint(" + "); + systemPrint(adjustment); + systemPrint(" Adj"); + } + systemPrint(" = "); + systemPrint(millisToNextHop); + systemPrintln(" mSec"); + systemPrint("msToNextHopRmt: "); systemPrint(msToNextHopRmt); systemPrint(" msToNextHopLocal: "); systemPrint(msToNextHopLocal); systemPrint(" msToNextHop: "); systemPrint(msToNextHop); + systemPrint(" delta: "); + systemPrint(deltaMillis); + systemPrint(" deltaHops: "); + systemPrint(deltaHops); systemPrintln(); + if (syncError) + { + int16_t channelTimer; + uint8_t * data; + + systemPrint("rxTimeUsec: "); + systemPrintln(rxTimeUsec); + systemPrint("txTimeUsec: "); + systemPrintln(txTimeUsec); + systemPrintln("RX Frame"); + dumpBuffer(incomingBuffer, headerBytes + rxDataBytes + trailerBytes); + + data = incomingBuffer; + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) + { + systemPrint(" Net Id: "); + systemPrint(*data); + systemPrint(" (0x"); + systemPrint(*data++, HEX); + systemPrintln(")"); + } + printControl(*data++); + memcpy(&channelTimer, data, sizeof(channelTimer)); + data += sizeof(channelTimer); + systemPrint(" Channel Timer(ms): "); + systemPrintln(channelTimer); + + systemPrintln("ERROR: Must fix channel timer synchronization math"); + waitForever(); + } } } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 72cd1fa4..a2dca484 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -20,7 +20,7 @@ { \ /*Compute the frequency correction*/ \ frequencyCorrection += radio.getFrequencyError() / 1000000.0; \ - \ + \ /*Send the ACK to the remote system*/ \ triggerEvent(trigger); \ if (xmitDatagramP2PAck() == true) \ @@ -35,7 +35,7 @@ { \ /*Start the ACK timer*/ \ ackTimer = datagramTimer; \ - \ + \ /*Since ackTimer is off when equal to zero, force it to a non-zero value*/ \ /*Subtract one so that the comparisons result in a small number*/ \ if (!ackTimer) \ @@ -48,19 +48,21 @@ ackTimer = 0; \ } -#define COMPUTE_TIMESTAMP_OFFSET(millisBuffer) \ +#define COMPUTE_TIMESTAMP_OFFSET(millisBuffer, rShift) \ { \ unsigned long deltaUsec = txTimeUsec + rxTimeUsec; \ memcpy(&remoteSystemMillis, millisBuffer, sizeof(currentMillis)); \ timestampOffset = (remoteSystemMillis + (deltaUsec / 1000) - currentMillis);\ + timestampOffset >>= rShift; \ } -#define COMPUTE_RX_TIME(millisBuffer) \ +#define COMPUTE_RX_TIME(millisBuffer, rShift) \ { \ currentMillis = millis(); \ - if (!rxTimeUsec) \ + if (!rxFirstAck) \ { \ rxTimeUsec = micros() - transactionCompleteMicros; \ + txRxTimeMsec = (txTimeUsec + rxTimeUsec) / 1000; \ \ /*Display the results*/ \ if (settings.debugSync) \ @@ -69,17 +71,20 @@ systemPrint(" RX Time: "); \ systemPrint(rxTimeUsec); \ systemPrintln(" uSec"); \ + systemPrint(" TX + RX Time: "); \ + systemPrint(txRxTimeMsec); \ + systemPrintln(" mSec"); \ } \ } \ \ /*Adjust the timestamp offset*/ \ - COMPUTE_TIMESTAMP_OFFSET(millisBuffer); \ + COMPUTE_TIMESTAMP_OFFSET(millisBuffer, rShift); \ } #define COMPUTE_TX_TIME() \ { \ currentMillis = millis(); \ - if (!txTimeUsec) \ + if (!txFirstAck) \ { \ txTimeUsec = transactionCompleteMicros - txDatagramMicros; \ \ @@ -269,6 +274,10 @@ void updateRadioState() setRadioFrequency(false); } + //Update the transmit and receive times until the first ACK TX and RX + rxFirstAck = false; + txFirstAck = false; + //Determine if a FIND_PARTNER was received if (transactionComplete) { @@ -300,9 +309,11 @@ void updateRadioState() case DATAGRAM_FIND_PARTNER: //Received FIND_PARTNER //Compute the receive time - COMPUTE_RX_TIME(rxData); - timestampOffset >>= 1; + COMPUTE_RX_TIME(rxData, 1); + //In the case of a retransmission, stop the channel timer + stopChannelTimer(); + startChannelTimer(); //Acknowledge the FIND_PARTNER with SYNC_CLOCKS triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); @@ -371,8 +382,7 @@ void updateRadioState() case DATAGRAM_FIND_PARTNER: //Received FIND_PARTNER //Compute the receive time - COMPUTE_RX_TIME(rxData + 1); - timestampOffset >>= 1; + COMPUTE_RX_TIME(rxData + 1, 1); //Acknowledge the FIND_PARTNER triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); @@ -386,8 +396,7 @@ void updateRadioState() case DATAGRAM_SYNC_CLOCKS: //Received SYNC_CLOCKS //Compute the receive time - COMPUTE_RX_TIME(rxData + 1); - timestampOffset >>= 1; + COMPUTE_RX_TIME(rxData + 1, 1); //Acknowledge the SYNC_CLOCKS @@ -412,6 +421,9 @@ void updateRadioState() outputSerialData(true); } + //Stop the channel timer + stopChannelTimer(); + //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_SYNC_CLOCKS_TIMEOUT); setHeartbeatShort(); @@ -473,8 +485,7 @@ void updateRadioState() case DATAGRAM_ZERO_ACKS: //Received ACK 2 //Compute the receive time - COMPUTE_RX_TIME(rxData); - timestampOffset >>= 1; + COMPUTE_RX_TIME(rxData, 1); setHeartbeatLong(); //We sent SYNC_CLOCKS and they sent ZERO_ACKS, so don't be the first to send heartbeat @@ -495,6 +506,9 @@ void updateRadioState() outputSerialData(true); } + //Stop the channel timer + stopChannelTimer(); + //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_ZERO_ACKS_TIMEOUT); setHeartbeatShort(); @@ -603,6 +617,13 @@ void updateRadioState() { transactionComplete = false; //Reset ISR flag + //Determine if an ACK was transmitted + if (txControl.datagramType = DATAGRAM_DATA_ACK) + { + COMPUTE_TX_TIME(); + txFirstAck = true; + } + //Determine the next packet size for SF6 if (ackTimer) { @@ -672,9 +693,7 @@ void updateRadioState() case DATAGRAM_HEARTBEAT: //Received heartbeat while link was idle. Send ack to sync clocks. //Adjust the timestamp offset - currentMillis = millis(); - COMPUTE_TIMESTAMP_OFFSET(rxData); - timestampOffset >>= 1; + COMPUTE_TIMESTAMP_OFFSET(rxData, 1); //Transmit ACK P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); @@ -689,19 +708,16 @@ void updateRadioState() break; case DATAGRAM_DUPLICATE: - frequencyCorrection += radio.getFrequencyError() / 1000000.0; - - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); - if (xmitDatagramP2PAck() == true) //Transmit ACK - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - setHeartbeatShort(); //We ack'd the packet (again) so be responsible for sending the next heartbeat + P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_DUP); break; case DATAGRAM_DATA_ACK: //Adjust the timestamp offset - currentMillis = millis(); - COMPUTE_TIMESTAMP_OFFSET(rxData); - timestampOffset >>= 1; + COMPUTE_RX_TIME(rxData, 1); + + //Stop updating the rxTimeUsec and txTimeUsec after TX and RX of ACKs + if (txFirstAck) + rxFirstAck = true; //The datagram we are expecting syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock @@ -1045,7 +1061,7 @@ void updateRadioState() //Compute the common clock currentMillis = millis(); - COMPUTE_TIMESTAMP_OFFSET(rxData + 1); + COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0); //Server has responded with ACK syncChannelTimer(); //Start and adjust freq hop ISR based on remote's remaining clock diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 0ec50b86..9164af51 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -241,18 +241,18 @@ enum TRIGGER_CHANNEL_TIMER_ISR, //0 TRIGGER_TRANSACTION_COMPLETE, + TRIGGER_FREQ_CHANGE, TRIGGER_TX_SPI_DONE, TRIGGER_RX_SPI_DONE, + TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT, TRIGGER_RADIO_RESET, TRIGGER_HOP_TIMER_START, TRIGGER_HOP_TIMER_STOP, TRIGGER_HEARTBEAT, - TRIGGER_FREQ_CHANGE, TRIGGER_SYNC_CHANNEL, TRIGGER_LINK_SEND_ACK_FOR_DATA, TRIGGER_LINK_SEND_ACK_FOR_DUP, TRIGGER_LINK_RETRANSMIT, - TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT, TRIGGER_LINK_WAIT_FOR_ACK, TRIGGER_LINK_DATA_XMIT, TRIGGER_LINK_RETRANSMIT_FAIL, From 0cea43e52b3a57ed3ac118e007d0d5d0863e3d9e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 1 Jan 2023 09:36:03 -1000 Subject: [PATCH 383/594] Fix build error --- Firmware/LoRaSerial_Firmware/Radio.ino | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e41d5cf9..54c09bae 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2976,8 +2976,7 @@ void syncChannelTimer() if ((deltaMillis <= 0) && ((currentMillis - timerStart) > (settings.maxDwellTime - txRxTimeMsec))) { //Hop one channel to catch up with the remote system - if (allowAdjustments) - hopChannel(); +// hopChannel(); deltaHops -= 1; } From a3532647b8a4bccf3cd19257accc42de16ae9c22 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 1 Jan 2023 09:32:59 -1000 Subject: [PATCH 384/594] Rename partialTimer to reloadChannelTimer --- Firmware/LoRaSerial_Firmware/Begin.ino | 4 ++-- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 7 ++++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 22ec84f5..20d213e5 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -100,9 +100,9 @@ void channelTimerHandler() timerStart = millis(); //Record when this ISR happened. Used for calculating clock sync. //If the last timer was used to sync clocks, restore full timer interval - if (partialTimer == true) + if (reloadChannelTimer == true) { - partialTimer = false; + reloadChannelTimer = false; channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); } diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 59880cce..c60dd95b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -162,7 +162,7 @@ bool trainViaButton = false; //Allows auto-creation of server if client times ou #include "SAMDTimerInterrupt.h" //http://librarymanager/All#SAMD_TimerInterrupt v1.9.0 (currently) by Koi Hang SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 unsigned long timerStart = 0; //Tracks how long our timer has been running since last hop -bool partialTimer = false; //After an ACK we reset and run a partial timer to sync units +bool reloadChannelTimer = false; //When set channel timer interval needs to be reloaded with settings.maxDwellTime uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps reduce amount of time spent petting. unsigned long lastPet = 0; //Remebers time of last WDT pet. diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 54c09bae..de7109ab 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -725,7 +725,7 @@ void updateHopISR() //We adjust the initial clock setup as needed int16_t getLinkupOffset() { - partialTimer = true; //Mark timer so that it runs only once with less than dwell time + reloadChannelTimer = true; //Mark timer so that it runs only once with less than dwell time int linkupOffset = 0; @@ -2919,7 +2919,7 @@ void stopChannelTimer() channelTimer.disableTimer(); triggerEvent(TRIGGER_HOP_TIMER_STOP); timeToHop = false; - partialTimer = false; + reloadChannelTimer = false; } //Given the remote unit's number of ms before its next hop, @@ -3116,7 +3116,8 @@ void syncChannelTimer() while (msToNextHop < 0) msToNextHop += settings.maxDwellTime; - partialTimer = true; + //When the ISR fires, reload the channel timer with settings.maxDwellTime + reloadChannelTimer = true; channelTimer.disableTimer(); channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's From 35b8d4dc6d2ae236607a681def4bd6ba20634223 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 1 Jan 2023 09:45:18 -1000 Subject: [PATCH 385/594] Rename timerStart to channelTimerStart --- Firmware/LoRaSerial_Firmware/Begin.ino | 2 +- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 20d213e5..67f791a8 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -97,7 +97,7 @@ void beginChannelTimer() //ISR that fires when channel timer expires void channelTimerHandler() { - timerStart = millis(); //Record when this ISR happened. Used for calculating clock sync. + channelTimerStart = millis(); //Record when this ISR happened. Used for calculating clock sync. //If the last timer was used to sync clocks, restore full timer interval if (reloadChannelTimer == true) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index c60dd95b..dbc375dc 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -161,7 +161,7 @@ bool trainViaButton = false; //Allows auto-creation of server if client times ou //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= #include "SAMDTimerInterrupt.h" //http://librarymanager/All#SAMD_TimerInterrupt v1.9.0 (currently) by Koi Hang SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 -unsigned long timerStart = 0; //Tracks how long our timer has been running since last hop +unsigned long channelTimerStart = 0; //Tracks how long our timer has been running since last hop bool reloadChannelTimer = false; //When set channel timer interval needs to be reloaded with settings.maxDwellTime uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps reduce amount of time spent petting. diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index de7109ab..c6494d9f 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2495,7 +2495,7 @@ bool transmitDatagram() //Hake sure that the transmitted msToNextHop is in the range 0 - maxDwellTime if (timeToHop) hopChannel(); - uint16_t msToNextHop = settings.maxDwellTime - (millis() - timerStart); + uint16_t msToNextHop = settings.maxDwellTime - (millis() - channelTimerStart); //TX channel timer value memcpy(header, &msToNextHop, sizeof(msToNextHop)); header += sizeof(msToNextHop); //aka CHANNEL_TIMER_BYTES @@ -2907,7 +2907,7 @@ void startChannelTimer(int16_t startAmount) channelTimer.disableTimer(); channelTimer.setInterval_MS(startAmount, channelTimerHandler); channelTimer.enableTimer(); - timerStart = millis(); //ISR normally takes care of this but allow for correct ACK sync before first ISR + channelTimerStart = millis(); //startChannelTimer - ISR updates value triggerEvent(TRIGGER_HOP_TIMER_START); } @@ -2973,7 +2973,7 @@ void syncChannelTimer() //Determine if the remote system has hopped and the local system has not deltaHops = 0; deltaMillis = msToNextHopRemote - txRxTimeMsec; - if ((deltaMillis <= 0) && ((currentMillis - timerStart) > (settings.maxDwellTime - txRxTimeMsec))) + if ((deltaMillis <= 0) && ((currentMillis - channelTimerStart) > (settings.maxDwellTime - txRxTimeMsec))) { //Hop one channel to catch up with the remote system // hopChannel(); @@ -3035,7 +3035,7 @@ void syncChannelTimer() break; } - int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - timerStart); + int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - channelTimerStart); //Precalculate large/small time amounts uint16_t smallAmount = settings.maxDwellTime / 8; From 538b1f728417b5b1ebe038ff2a16f034bae64108 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 1 Jan 2023 10:29:53 -1000 Subject: [PATCH 386/594] Add comments to stopChannelTimer to describe where and why Update No-Link state machine diagram --- Firmware/LoRaSerial_Firmware/Begin.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 68 ++++++++++++------------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 67f791a8..23ae81d5 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -91,7 +91,7 @@ void beginChannelTimer() if (channelTimer.attachInterruptInterval_MS(settings.maxDwellTime, channelTimerHandler) == false) Serial.println("Error starting ChannelTimer!"); - stopChannelTimer(); //Start timer only after link is up + stopChannelTimer(); //Start timer in state machine - beginChannelTimer } //ISR that fires when channel timer expires diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index a2dca484..bf8a1d28 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -156,7 +156,7 @@ void updateRadioState() selectHeaderAndTrailerBytes(); //Determine the components of the frame header and trailer - stopChannelTimer(); //Prevent radio from frequency hopping + stopChannelTimer(); //Stop frequency hopping - reset configureRadio(); //Setup radio, set freq to channel 0, calculate air times @@ -216,40 +216,41 @@ void updateRadioState() /* System A System B - RESET RESET - | | - Channel 0 | | Channel 0 - V V - .----> P2P_NO_LINK P2P_NO_LINK - | | Tx FIND_PARTNER | - | Timeout | | - | V | - | P2P_WAIT_TX_FIND_PARTNER_DONE | - | | | - | | Tx Complete - - - - - > | Rx FIND_PARTNER - | | Start Rx | - | | MAX_PACKET_SIZE | - | V V - `---- P2P_WAIT_SYNC_CLOCKS +<----------------------------. - | | Tx SYNC_CLOCKS | - | V | - | P2P_WAIT_TX_SYNC_CLOCKS_DONE | + RESET RESET <-------------------------. | | | - Rx SYNC_CLOCKS | < - - - - - - - - - - - | Tx Complete | - | | Start Rx | - | | MAX_PACKET_SIZE | - | | | - V V Timeout | - .--------------->+ P2P_WAIT_ZERO_ACKS ------------------->+ - | | | ^ + Channel 0 | | Channel 0 | + Stop HOP Timer | | Stop HOP Timer | + V V | + .----> P2P_NO_LINK P2P_NO_LINK <------------------. | + | | Tx FIND_PARTNER | | | + | Timeout | | | | + | V | Stop HOP Timer | | + | P2P_WAIT_TX_FIND_PARTNER_DONE | | | + | | | | | + | | Tx Complete - - - - - > | Rx FIND_PARTNER | | + | | Start Rx | | | + | | MAX_PACKET_SIZE | | | + | V | | | + `---- P2P_WAIT_SYNC_CLOCKS | | | + | | Tx SYNC_CLOCKS | | + | V | | + | P2P_WAIT_TX_SYNC_CLOCKS_DONE | | + | | | | + Rx SYNC_CLOCKS | < - - - - - - - - - - - | Tx Complete | | + | | Start Rx | | + | | MAX_PACKET_SIZE | | + | | | | + V V Timeout | | + .--------------->+ P2P_WAIT_ZERO_ACKS ----------------' | + | | | | | | TX ZERO_ACKS | | | | | | | V | | | P2P_WAIT_TX_ZERO_ACKS_DONE | | | | Tx Complete - - - - - > | Rx ZERO_ACKS | - | Stop | Start HOP timer | Start HOP Timer | Stop - | HOP | Start Rx | Start Rx | HOP - | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | Timer + | Stop | Start HOP timer | Start HOP Timer | + | HOP | Start Rx | Start Rx | + | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | | | Zero ACKs | Zero ACKs | | Rx | | | | SYNC_CLOCKS V V Rx FIND_PARTNER | @@ -269,7 +270,6 @@ void updateRadioState() //If we are on the wrong channel, go home if (channelNumber != 0) { - stopChannelTimer(); channelNumber = 0; setRadioFrequency(false); } @@ -311,8 +311,6 @@ void updateRadioState() //Compute the receive time COMPUTE_RX_TIME(rxData, 1); - //In the case of a retransmission, stop the channel timer - stopChannelTimer(); startChannelTimer(); //Acknowledge the FIND_PARTNER with SYNC_CLOCKS @@ -422,7 +420,7 @@ void updateRadioState() } //Stop the channel timer - stopChannelTimer(); + stopChannelTimer(); //P2P_WAIT_SYNC_CLOCKS timeout //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_SYNC_CLOCKS_TIMEOUT); @@ -507,7 +505,7 @@ void updateRadioState() } //Stop the channel timer - stopChannelTimer(); + stopChannelTimer(); //P2P_WAIT_ZERO_ACKS timeout //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_ZERO_ACKS_TIMEOUT); @@ -995,7 +993,7 @@ void updateRadioState() //Start searching for other radios //==================== case RADIO_DISCOVER_BEGIN: - stopChannelTimer(); //Stop hopping + stopChannelTimer(); //Stop hopping - multipoint discovery if (settings.debugSync) { From 1b4170ad79d8b24571af4df1dcd95e5377d4eb34 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 1 Jan 2023 10:38:50 -1000 Subject: [PATCH 387/594] Properly handle DATAGRAM_SYNC_CLOCK and DATAGRAM_ZERO_ACKS in link up SM --- Firmware/LoRaSerial_Firmware/States.ino | 32 +++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bf8a1d28..4fe9b2ae 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -240,21 +240,21 @@ void updateRadioState() | | Start Rx | | | | MAX_PACKET_SIZE | | | | | | - V V Timeout | | - .--------------->+ P2P_WAIT_ZERO_ACKS ----------------' | - | | | | - | | TX ZERO_ACKS | | - | | | | - | V | | - | P2P_WAIT_TX_ZERO_ACKS_DONE | | - | | Tx Complete - - - - - > | Rx ZERO_ACKS | - | Stop | Start HOP timer | Start HOP Timer | - | HOP | Start Rx | Start Rx | - | Timer | MAX_PACKET_SIZE | MAX_PACKET_SIZE | - | | Zero ACKs | Zero ACKs | - | Rx | | | - | SYNC_CLOCKS V V Rx FIND_PARTNER | - `---------- P2P_LINK_UP P2P_LINK_UP -----------------------’ + | V Timeout | | + | P2P_WAIT_ZERO_ACKS ----------------' | + | | | + | TX ZERO_ACKS | | + | | | + V | | + P2P_WAIT_TX_ZERO_ACKS_DONE | | + | Tx Complete - - - - - > | Rx ZERO_ACKS | + | Start HOP timer | Start HOP Timer | + | Start Rx | Start Rx | + | MAX_PACKET_SIZE | MAX_PACKET_SIZE | + | Zero ACKs | Zero ACKs | + | | | + V V Rx FIND_PARTNER | + P2P_LINK_UP P2P_LINK_UP -----------------------’ | | | Rx Data | Rx Data | | @@ -672,6 +672,8 @@ void updateRadioState() } break; + case DATAGRAM_SYNC_CLOCKS: + case DATAGRAM_ZERO_ACKS: case DATAGRAM_BAD: triggerEvent(TRIGGER_BAD_PACKET); break; From 4f9d51fd03375c6f63df06f553635b80f43ba4ba Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 1 Jan 2023 10:57:05 -1000 Subject: [PATCH 388/594] Define a single source for channelTimer corrections For point-to-point mode the source for channel timer corrections is the system that sent the DATAGRAM_SYNC_CLOCKS frame. The other system is the only system that updates its timer. For multi-point and virtual-circuit modes the server is the source for channel timer corrections. The client systems all synchronize to the server. --- .../LoRaSerial_Firmware.ino | 4 ++- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + Firmware/LoRaSerial_Firmware/States.ino | 36 +++++++++++++++---- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index dbc375dc..b7978bf1 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -519,7 +519,7 @@ char platformPrefix[25]; //Used for printing platform specific device name, ie " //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- /* - Server Client + Clock Sync Transmitter Clock Sync Receiver HEARTBEAT send needed call xmitDatagramP2PHeartbeat @@ -579,6 +579,8 @@ char platformPrefix[25]; //Used for printing platform specific device name, ie " //Clock synchronization //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +bool clockSyncReceiver; //Receives and processes the clock synchronization + //RX and TX time measurements uint32_t rxTimeUsec; uint32_t txTimeUsec; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index c6494d9f..1542f053 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2938,6 +2938,7 @@ void syncChannelTimer() currentMillis = millis(); radioCallHistory[RADIO_CALL_syncChannelTimer] = currentMillis; + if (!clockSyncReceiver) return; //syncChannelTimer if (settings.frequencyHop == false) return; //msToNextHopRemote is in the range of 0 - settings.maxDwellTime diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 4fe9b2ae..88fa24ae 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -157,6 +157,7 @@ void updateRadioState() selectHeaderAndTrailerBytes(); //Determine the components of the frame header and trailer stopChannelTimer(); //Stop frequency hopping - reset + clockSyncReceiver = true; //Assume receiving clocks - reset configureRadio(); //Setup radio, set freq to channel 0, calculate air times @@ -182,8 +183,11 @@ void updateRadioState() else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { if (settings.server) + { //Reserve the server's address (0) myVc = vcIdToAddressByte(VC_SERVER, myUniqueId); + clockSyncReceiver = false; //VC server is clock source + } else //Unknown client address myVc = VC_UNASSIGNED; @@ -196,6 +200,7 @@ void updateRadioState() { if (settings.server == true) { + clockSyncReceiver = false; //Multipoint server is clock source startChannelTimer(); //Start hopping changeState(RADIO_MP_STANDBY); } @@ -220,17 +225,19 @@ void updateRadioState() | | | Channel 0 | | Channel 0 | Stop HOP Timer | | Stop HOP Timer | + clockSyncReceiver | = true | clockSyncReceiver = true | + | | | V V | .----> P2P_NO_LINK P2P_NO_LINK <------------------. | | | Tx FIND_PARTNER | | | - | Timeout | | | | - | V | Stop HOP Timer | | - | P2P_WAIT_TX_FIND_PARTNER_DONE | | | + | Timeout | | Stop HOP Timer | | + | V | clockSyncReceiver | | + | P2P_WAIT_TX_FIND_PARTNER_DONE | = true | | | | | | | | | Tx Complete - - - - - > | Rx FIND_PARTNER | | | | Start Rx | | | - | | MAX_PACKET_SIZE | | | - | V | | | + | | MAX_PACKET_SIZE | clockSyncReceiver | | + | V | = false | | `---- P2P_WAIT_SYNC_CLOCKS | | | | | Tx SYNC_CLOCKS | | | V | | @@ -313,6 +320,16 @@ void updateRadioState() startChannelTimer(); + //This system is the source of clock synchronization + clockSyncReceiver = false; //P2P clock source + + //Display the channelTimer source + if (settings.debugSync) + { + systemPrint("Sourcing channelTimer, TX + RX mSec: "); + systemPrintln(txRxTimeMsec); + } + //Acknowledge the FIND_PARTNER with SYNC_CLOCKS triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) @@ -393,10 +410,16 @@ void updateRadioState() case DATAGRAM_SYNC_CLOCKS: //Received SYNC_CLOCKS + //Display the channelTimer sink + if (settings.debugSync) + { + systemPrint("Syncing channelTimer, TX + RX mSec: "); + systemPrintln(txRxTimeMsec); + } + //Compute the receive time COMPUTE_RX_TIME(rxData + 1, 1); - //Acknowledge the SYNC_CLOCKS triggerEvent(TRIGGER_SEND_ZERO_ACKS); if (xmitDatagramP2PZeroAcks() == true) @@ -506,6 +529,7 @@ void updateRadioState() //Stop the channel timer stopChannelTimer(); //P2P_WAIT_ZERO_ACKS timeout + clockSyncReceiver = true; //P2P link timeout //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_ZERO_ACKS_TIMEOUT); From 841baacc8f09b23c6b8fd3030d91e2d4b9ca8549 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 1 Jan 2023 11:19:32 -1000 Subject: [PATCH 389/594] Properly start and stop the channel timer --- Firmware/LoRaSerial_Firmware/Radio.ino | 5 +++-- Firmware/LoRaSerial_Firmware/States.ino | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 1542f053..651a9115 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2906,8 +2906,10 @@ void startChannelTimer(int16_t startAmount) channelTimer.disableTimer(); channelTimer.setInterval_MS(startAmount, channelTimerHandler); - channelTimer.enableTimer(); + reloadChannelTimer = (startAmount != settings.maxDwellTime); + timeToHop = false; channelTimerStart = millis(); //startChannelTimer - ISR updates value + channelTimer.enableTimer(); triggerEvent(TRIGGER_HOP_TIMER_START); } @@ -2919,7 +2921,6 @@ void stopChannelTimer() channelTimer.disableTimer(); triggerEvent(TRIGGER_HOP_TIMER_STOP); timeToHop = false; - reloadChannelTimer = false; } //Given the remote unit's number of ms before its next hop, diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 88fa24ae..bdc36da4 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -201,7 +201,7 @@ void updateRadioState() if (settings.server == true) { clockSyncReceiver = false; //Multipoint server is clock source - startChannelTimer(); //Start hopping + startChannelTimer(); //Start hopping - multipoint clock source changeState(RADIO_MP_STANDBY); } else @@ -235,7 +235,7 @@ void updateRadioState() | P2P_WAIT_TX_FIND_PARTNER_DONE | = true | | | | | | | | | Tx Complete - - - - - > | Rx FIND_PARTNER | | - | | Start Rx | | | + | | Start Rx | Start HOP Timer | | | | MAX_PACKET_SIZE | clockSyncReceiver | | | V | = false | | `---- P2P_WAIT_SYNC_CLOCKS | | | @@ -245,8 +245,8 @@ void updateRadioState() | | | | Rx SYNC_CLOCKS | < - - - - - - - - - - - | Tx Complete | | | | Start Rx | | - | | MAX_PACKET_SIZE | | - | | | | + | Start HOP Timer | MAX_PACKET_SIZE | | + | Sync HOP Timer | | | | V Timeout | | | P2P_WAIT_ZERO_ACKS ----------------' | | | | @@ -255,7 +255,6 @@ void updateRadioState() V | | P2P_WAIT_TX_ZERO_ACKS_DONE | | | Tx Complete - - - - - > | Rx ZERO_ACKS | - | Start HOP timer | Start HOP Timer | | Start Rx | Start Rx | | MAX_PACKET_SIZE | MAX_PACKET_SIZE | | Zero ACKs | Zero ACKs | @@ -318,7 +317,8 @@ void updateRadioState() //Compute the receive time COMPUTE_RX_TIME(rxData, 1); - startChannelTimer(); + //Start the channel timer + startChannelTimer(); //Start hopping - P2P clock source //This system is the source of clock synchronization clockSyncReceiver = false; //P2P clock source @@ -410,6 +410,10 @@ void updateRadioState() case DATAGRAM_SYNC_CLOCKS: //Received SYNC_CLOCKS + //Start the channel timer + startChannelTimer(); //Start hopping - P2P clock receiver + syncChannelTimer(); + //Display the channelTimer sink if (settings.debugSync) { @@ -442,9 +446,6 @@ void updateRadioState() outputSerialData(true); } - //Stop the channel timer - stopChannelTimer(); //P2P_WAIT_SYNC_CLOCKS timeout - //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_SYNC_CLOCKS_TIMEOUT); setHeartbeatShort(); @@ -556,8 +557,6 @@ void updateRadioState() transactionComplete = false; //Reset ISR flag COMPUTE_TX_TIME(); - startChannelTimer(); //We are exiting the link first so do not adjust our starting Timer - setHeartbeatShort(); //We sent the last ack so be responsible for sending the next heartbeat //Bring up the link From 83fa69e0761476d6a281094623bf0aafbed8a129 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 05:25:34 -1000 Subject: [PATCH 390/594] Output channel number LSB on A1 each time channel timer is loaded --- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 3 +++ Firmware/LoRaSerial_Firmware/Begin.ino | 1 + Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 4 ++++ 4 files changed, 9 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index f86f4e2b..7d692b39 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -87,6 +87,7 @@ void samdBeginBoard() pin_trainButton = 4; pin_trigger = A0; + pin_hop_timer = A1; //Flow control pinMode(pin_rts, OUTPUT); @@ -115,6 +116,8 @@ void samdBeginBoard() //Debug pinMode(pin_trigger, OUTPUT); digitalWrite(pin_trigger, HIGH); + pinMode(pin_hop_timer, OUTPUT); + digitalWrite(pin_hop_timer, LOW); //Get average of board ID voltage divider int val = 0; diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 23ae81d5..2d12f71d 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -105,6 +105,7 @@ void channelTimerHandler() reloadChannelTimer = false; channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); } + digitalWrite(pin_hop_timer, channelNumber & 1); if (settings.frequencyHop) { diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b7978bf1..47e8ce0b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -102,6 +102,7 @@ uint8_t pin_rssi4LED = PIN_UNDEFINED; uint8_t pin_boardID = PIN_UNDEFINED; uint8_t pin_trigger = PIN_UNDEFINED; +uint8_t pin_hop_timer = PIN_UNDEFINED; #define GREEN_LED_1 pin_rssi1LED #define GREEN_LED_2 pin_rssi2LED diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 651a9115..e04178e1 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2906,6 +2906,7 @@ void startChannelTimer(int16_t startAmount) channelTimer.disableTimer(); channelTimer.setInterval_MS(startAmount, channelTimerHandler); + digitalWrite(pin_hop_timer, channelNumber & 1); reloadChannelTimer = (startAmount != settings.maxDwellTime); timeToHop = false; channelTimerStart = millis(); //startChannelTimer - ISR updates value @@ -2919,6 +2920,8 @@ void stopChannelTimer() radioCallHistory[RADIO_CALL_stopChannelTimer] = millis(); channelTimer.disableTimer(); + digitalWrite(pin_hop_timer, channelNumber & 1); + triggerEvent(TRIGGER_HOP_TIMER_STOP); timeToHop = false; } @@ -3122,6 +3125,7 @@ void syncChannelTimer() reloadChannelTimer = true; channelTimer.disableTimer(); channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's + digitalWrite(pin_hop_timer, channelNumber & 1); if (resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. timeToHop = false; From dd13387d3683a1299d703707bd90b04224b747e5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 05:36:16 -1000 Subject: [PATCH 391/594] Minimize rxTimeUsec and txTimeUsec after initial value following reset --- Firmware/LoRaSerial_Firmware/States.ino | 27 ++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index bdc36da4..07908a32 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -53,15 +53,17 @@ unsigned long deltaUsec = txTimeUsec + rxTimeUsec; \ memcpy(&remoteSystemMillis, millisBuffer, sizeof(currentMillis)); \ timestampOffset = (remoteSystemMillis + (deltaUsec / 1000) - currentMillis);\ - timestampOffset >>= rShift; \ + timestampOffset >>= rShift; \ } #define COMPUTE_RX_TIME(millisBuffer, rShift) \ { \ currentMillis = millis(); \ - if (!rxFirstAck) \ + long deltaUsec = micros() - transactionCompleteMicros; \ + if ((!rxFirstAck) || (deltaUsec < rxTimeUsec)) \ { \ - rxTimeUsec = micros() - transactionCompleteMicros; \ + rxFirstAck = true; \ + rxTimeUsec = deltaUsec; \ txRxTimeMsec = (txTimeUsec + rxTimeUsec) / 1000; \ \ /*Display the results*/ \ @@ -84,9 +86,12 @@ #define COMPUTE_TX_TIME() \ { \ currentMillis = millis(); \ - if (!txFirstAck) \ + long deltaUsec = transactionCompleteMicros - txDatagramMicros; \ + if ((!txFirstAck) || (deltaUsec < txTimeUsec)) \ { \ - txTimeUsec = transactionCompleteMicros - txDatagramMicros; \ + txFirstAck = true; \ + txTimeUsec = deltaUsec; \ + txRxTimeMsec = (txTimeUsec + rxTimeUsec) / 1000; \ \ /*Display the results*/ \ if (settings.debugSync) \ @@ -95,6 +100,9 @@ systemPrint(" TX Time: "); \ systemPrint(txTimeUsec); \ systemPrintln(" uSec"); \ + systemPrint(" TX + RX Time: "); \ + systemPrint(txRxTimeMsec); \ + systemPrintln(" mSec"); \ } \ } \ } @@ -280,10 +288,6 @@ void updateRadioState() setRadioFrequency(false); } - //Update the transmit and receive times until the first ACK TX and RX - rxFirstAck = false; - txFirstAck = false; - //Determine if a FIND_PARTNER was received if (transactionComplete) { @@ -642,7 +646,6 @@ void updateRadioState() if (txControl.datagramType = DATAGRAM_DATA_ACK) { COMPUTE_TX_TIME(); - txFirstAck = true; } //Determine the next packet size for SF6 @@ -738,10 +741,6 @@ void updateRadioState() //Adjust the timestamp offset COMPUTE_RX_TIME(rxData, 1); - //Stop updating the rxTimeUsec and txTimeUsec after TX and RX of ACKs - if (txFirstAck) - rxFirstAck = true; - //The datagram we are expecting syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock From a3161efc123bd91cee7aa74a47b30672ffd03edf Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 05:50:07 -1000 Subject: [PATCH 392/594] Remember the last value programmed into the channel timer --- Firmware/LoRaSerial_Firmware/Begin.ino | 1 + Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 2 ++ 3 files changed, 4 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 2d12f71d..597df512 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -104,6 +104,7 @@ void channelTimerHandler() { reloadChannelTimer = false; channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); + channelTimerMsec = settings.maxDwellTime; //ISR update } digitalWrite(pin_hop_timer, channelNumber & 1); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 47e8ce0b..bad7b43a 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -164,6 +164,7 @@ bool trainViaButton = false; //Allows auto-creation of server if client times ou SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 unsigned long channelTimerStart = 0; //Tracks how long our timer has been running since last hop bool reloadChannelTimer = false; //When set channel timer interval needs to be reloaded with settings.maxDwellTime +uint16_t channelTimerMsec; //Last value programmed into the channel timer uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps reduce amount of time spent petting. unsigned long lastPet = 0; //Remebers time of last WDT pet. diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e04178e1..d68b9de4 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2910,6 +2910,7 @@ void startChannelTimer(int16_t startAmount) reloadChannelTimer = (startAmount != settings.maxDwellTime); timeToHop = false; channelTimerStart = millis(); //startChannelTimer - ISR updates value + channelTimerMsec = startAmount; //startChannelTimer - ISR updates value channelTimer.enableTimer(); triggerEvent(TRIGGER_HOP_TIMER_START); } @@ -3126,6 +3127,7 @@ void syncChannelTimer() channelTimer.disableTimer(); channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's digitalWrite(pin_hop_timer, channelNumber & 1); + channelTimerMsec = msToNextHop; //syncChannelTimer update if (resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. timeToHop = false; From 2fc29a97dfaa9c78e5cda1fc46f3d5e949df606c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 06:01:42 -1000 Subject: [PATCH 393/594] Add mSecToChannelZero to compute the time remaining until reaching CH-0 --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- .../LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/Radio.ino | 28 ++++++++++++------- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 61672439..ad2b3528 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -503,7 +503,7 @@ bool commandAT(const char * commandString) if (channelNumber) { systemPrint(" Next Ch 0 time: "); - systemPrintTimestamp(nextChannelZeroTimeInMillis + timestampOffset); + systemPrintTimestamp(millis() + mSecToChannelZero() + timestampOffset); int hopCount = settings.numberOfChannels - channelNumber; systemPrint(", in "); systemPrint(hopCount); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index bad7b43a..351bf676 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -468,7 +468,6 @@ unsigned long rcvTimeMillis; unsigned long xmitTimeMillis; long timestampOffset; unsigned long vcTxHeartbeatMillis; -unsigned long nextChannelZeroTimeInMillis; //Transmit control uint8_t * endOfTxData; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index d68b9de4..0eee7b5d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -194,9 +194,6 @@ bool setRadioFrequency(bool rxAdjust) else triggerEvent(TRIGGER_FREQ_CHANGE); - //Determine the time in milliseconds when channel zero is reached again - nextChannelZeroTimeInMillis = millis() + ((settings.numberOfChannels - channelNumber) * settings.maxDwellTime); - //Print the frequency if requested if (settings.printChannel && (previousChannelNumber != channelNumber)) { @@ -212,7 +209,7 @@ bool setRadioFrequency(bool rxAdjust) systemPrint(": "); systemPrint(radioFrequency, 3); systemPrint(" MHz, Ch 0 in "); - systemPrint(nextChannelZeroTimeInMillis - millis()); + systemPrint(mSecToChannelZero()); systemPrintln(" mSec"); outputSerialData(true); } @@ -568,6 +565,22 @@ void hopChannel(bool moveForwardThroughTable) setRadioFrequency(radioStateTable[radioState].rxState); } +//Determine the time in milliseconds when channel zero is reached again +unsigned long mSecToChannelZero() +{ + unsigned long nextChannelZeroTimeInMillis; + uint16_t remainingChannels; + + //Compute the time remaining at the current channel + nextChannelZeroTimeInMillis = channelTimerMsec - (millis() - channelTimerStart); + + //Compute the time associated with the additional channels + remainingChannels = settings.numberOfChannels - 1 - channelNumber; + if (remainingChannels > 0) + nextChannelZeroTimeInMillis += remainingChannels * settings.maxDwellTime; + return nextChannelZeroTimeInMillis; +} + //Returns true if the radio indicates we have an ongoing reception bool receiveInProcess() { @@ -3274,12 +3287,7 @@ void setVcHeartbeatTimer() petWDT(); //Determine the delay before channel zero is reached - deltaMillis = nextChannelZeroTimeInMillis - heartbeatTimer; - if (deltaMillis <= 0) - { - nextChannelZeroTimeInMillis = heartbeatTimer + ((settings.numberOfChannels - channelNumber) * settings.maxDwellTime); - deltaMillis = nextChannelZeroTimeInMillis - heartbeatTimer; - } + deltaMillis = mSecToChannelZero() - heartbeatTimer; //Determine the delay before the next HEARTBEAT frame if ((!settings.server) || (deltaMillis > ((3 * settings.heartbeatTimeout) / 2)) From 85f2685eeb27f30706020bdc1492d8e15fcb38ec Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 06:04:19 -1000 Subject: [PATCH 394/594] Pass the number of channels to hop to hopChannel --- Firmware/LoRaSerial_Firmware/Radio.ino | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 0eee7b5d..70db4782 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -534,32 +534,27 @@ uint16_t myRand() //at the beginning and during of a transmission or reception void hopChannel() { - hopChannel(true); //Move forward + hopChannel(true, 1); //Move forward } //Hop to the previous channel in the frequency list void hopChannelReverse() { - hopChannel(false); //Move backward + hopChannel(false, 1); //Move backward } //Set the next radio frequency given the hop direction and frequency table -void hopChannel(bool moveForwardThroughTable) +void hopChannel(bool moveForwardThroughTable, uint8_t channelCount) { radioCallHistory[RADIO_CALL_hopChannel] = millis(); timeToHop = false; if (moveForwardThroughTable) - { - channelNumber++; - channelNumber %= settings.numberOfChannels; - } + channelNumber += channelCount; else - { - if (channelNumber == 0) channelNumber = settings.numberOfChannels; - channelNumber--; - } + channelNumber += settings.numberOfChannels - channelCount; + channelNumber %= settings.numberOfChannels; //Select the new frequency setRadioFrequency(radioStateTable[radioState].rxState); From 4a2d73069b0d69b608f6904e69d8749186acc3f0 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 06:18:12 -1000 Subject: [PATCH 395/594] Update channelTimerStart in syncChannelTimer --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 351bf676..d4ded453 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -162,9 +162,10 @@ bool trainViaButton = false; //Allows auto-creation of server if client times ou //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= #include "SAMDTimerInterrupt.h" //http://librarymanager/All#SAMD_TimerInterrupt v1.9.0 (currently) by Koi Hang SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 -unsigned long channelTimerStart = 0; //Tracks how long our timer has been running since last hop -bool reloadChannelTimer = false; //When set channel timer interval needs to be reloaded with settings.maxDwellTime -uint16_t channelTimerMsec; //Last value programmed into the channel timer +volatile uint16_t channelTimerMsec; //Last value programmed into the channel timer +volatile unsigned long channelTimerStart = 0; //Tracks how long our timer has been running since last hop +volatile bool timeToHop = false; //Set by channelTimerHandler to indicate that hopChannel needs to be called +volatile bool reloadChannelTimer = false; //When set channel timer interval needs to be reloaded with settings.maxDwellTime uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps reduce amount of time spent petting. unsigned long lastPet = 0; //Remebers time of last WDT pet. @@ -377,7 +378,6 @@ unsigned long lastPacketReceived = 0; //Controls link LED in broadcast mode unsigned long lastLinkBlink = 0; //Controls link LED in broadcast mode volatile bool transactionComplete = false; //Used in dio0ISR -volatile bool timeToHop = false; //Used in dio1ISR uint8_t sf6ExpectedSize = MAX_PACKET_SIZE; //Used during SF6 operation to reduce packet size when needed float radioFrequency; //Current radio frequency diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 70db4782..a3d92aae 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3135,6 +3135,7 @@ void syncChannelTimer() channelTimer.disableTimer(); channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's digitalWrite(pin_hop_timer, channelNumber & 1); + channelTimerStart = currentMillis; channelTimerMsec = msToNextHop; //syncChannelTimer update if (resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. From 58708d83ce522762efa6c8754841ba4a0cd8911f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 06:27:37 -1000 Subject: [PATCH 396/594] Add channelHopTimer to the list of RADIO_CALLS --- Firmware/LoRaSerial_Firmware/Begin.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 1 + Firmware/LoRaSerial_Firmware/settings.h | 1 + 3 files changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 597df512..b92af95f 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -98,6 +98,7 @@ void beginChannelTimer() void channelTimerHandler() { channelTimerStart = millis(); //Record when this ISR happened. Used for calculating clock sync. + radioCallHistory[RADIO_CALL_channelTimerHandler] = channelTimerStart; //If the last timer was used to sync clocks, restore full timer interval if (reloadChannelTimer == true) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a3d92aae..634223f0 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3390,6 +3390,7 @@ const I16_TO_STRING radioCallName[] = //Insert new values before this line {RADIO_CALL_hopISR, "hopISR"}, {RADIO_CALL_transactionCompleteISR, "transactionCompleteISR"}, + {RADIO_CALL_channelTimerHandler, "channelTimerHandler"}, #ifdef RADIOLIB_LOW_LEVEL {RADIO_CALL_readSX1276Register, "readSX1276Register"}, {RADIO_CALL_printSX1276Registers, "printSX1276Registers"}, diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 9164af51..91c11ec6 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -536,6 +536,7 @@ typedef enum //Insert new values before this line RADIO_CALL_transactionCompleteISR, RADIO_CALL_hopISR, + RADIO_CALL_channelTimerHandler, #ifdef RADIOLIB_LOW_LEVEL RADIO_CALL_readSX1276Register, RADIO_CALL_printSX1276Registers, From 6efaf587ab1e2af8a44593b1f3ac5acf765d3605 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 09:50:20 -1000 Subject: [PATCH 397/594] Verify the channelTimer value when building the TX header --- Firmware/LoRaSerial_Firmware/Radio.ino | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 634223f0..93623658 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2507,6 +2507,35 @@ bool transmitDatagram() memcpy(header, &msToNextHop, sizeof(msToNextHop)); header += sizeof(msToNextHop); //aka CHANNEL_TIMER_BYTES + if (ENABLE_DEVELOPER && (!clockSyncReceiver)) + { + if ((msToNextHop < 0) || (msToNextHop > settings.maxDwellTime)) + { + int16_t channelTimer; + uint8_t * data; + + systemPrintln("TX Frame"); + data = outgoingPacket; + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) + { + systemPrint(" Net Id: "); + systemPrint(*data); + systemPrint(" (0x"); + systemPrint(*data++, HEX); + systemPrintln(")"); + } + printControl(*data++); + memcpy(&channelTimer, data, sizeof(channelTimer)); + data += sizeof(channelTimer); + systemPrint(" Channel Timer(ms): "); + systemPrintln(channelTimer); + + systemPrint("ERROR: Invalid msToNextHop value, "); + systemPrintln(msToNextHop); + waitForever(); + } + } //ENABLE_DEVELOPER + if (settings.debugTransmit) { systemPrintTimestamp(); From f088f78e49fc5e80c6148df47327fa56c23f767c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 1 Jan 2023 18:04:08 -1000 Subject: [PATCH 398/594] Update the channel timer synchronization math --- .../LoRaSerial_Firmware.ino | 7 +- Firmware/LoRaSerial_Firmware/Radio.ino | 291 ++++++++++++++---- 2 files changed, 238 insertions(+), 60 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index d4ded453..7f39b0b4 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -43,7 +43,7 @@ const int FIRMWARE_VERSION_MAJOR = 2; const int FIRMWARE_VERSION_MINOR = 0; #define RADIOLIB_LOW_LEVEL //Enable access to the module functions -#define ENABLE_DEVELOPER //Uncomment this line to enable special developer modes +#define ENABLE_DEVELOPER true //Uncomment this line to enable special developer modes #define UNUSED(x) (void)(x) @@ -77,6 +77,11 @@ const int FIRMWARE_VERSION_MINOR = 0; //Bit 3: Header Info Valid toggles high when a valid Header (with correct CRC) is detected #define RECEIVE_IN_PROCESS_MASK 0b1011 +//Always define ENABLE_DEVELOPER to enable its use in conditional statements +#ifndef ENABLE_DEVELOPER +#define ENABLE_DEVELOPER false +#endif //ENABLE_DEVELOPER + #include "settings.h" //Hardware connections diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 93623658..bf942dd7 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2957,6 +2957,7 @@ void stopChannelTimer() { radioCallHistory[RADIO_CALL_stopChannelTimer] = millis(); + //Turn off the channel timer channelTimer.disableTimer(); digitalWrite(pin_hop_timer, channelNumber & 1); @@ -2968,14 +2969,21 @@ void stopChannelTimer() //adjust our own channelTimer interrupt to be synchronized with the remote unit void syncChannelTimer() { - long adjustment; + int16_t adjustment; unsigned long currentMillis; - int8_t deltaHops; + int8_t delayedHopCount; + int16_t lclHopTimeMsec; + uint16_t msToNextHop; + int16_t rmtHopTimeMsec; + + int16_t nextChannelTimerMsec; + int8_t deltaHops = 0; + int16_t msToNextHopRmt; long deltaMillis; long millisToNextHop; - int16_t msToNextHopRmt; bool syncError; - long txRxTimeUsec; + +#define COMPUTE_OLD_CHANNEL_TIMER_VALUE 0 currentMillis = millis(); radioCallHistory[RADIO_CALL_syncChannelTimer] = currentMillis; @@ -2983,55 +2991,213 @@ void syncChannelTimer() if (!clockSyncReceiver) return; //syncChannelTimer if (settings.frequencyHop == false) return; - //msToNextHopRemote is in the range of 0 - settings.maxDwellTime + //msToNextHopRemote is obtained during rcvDatagram() and is in the range of + //0 - settings.maxDwellTime //Validate this range - if ((msToNextHopRemote < 0) || (msToNextHopRemote > settings.maxDwellTime)) + if (ENABLE_DEVELOPER) { - int16_t channelTimer; - uint8_t * data; + if ((msToNextHopRemote < 0) || (msToNextHopRemote > settings.maxDwellTime)) + { + int16_t channelTimer; + uint8_t * data; - systemPrintln("RX Frame"); - dumpBuffer(incomingBuffer, headerBytes + rxDataBytes + trailerBytes); + systemPrintln("RX Frame"); + dumpBuffer(incomingBuffer, headerBytes + rxDataBytes + trailerBytes); - data = incomingBuffer; - if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) - { - systemPrint(" Net Id: "); - systemPrint(*data); - systemPrint(" (0x"); - systemPrint(*data++, HEX); - systemPrintln(")"); + data = incomingBuffer; + if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) + { + systemPrint(" Net Id: "); + systemPrint(*data); + systemPrint(" (0x"); + systemPrint(*data++, HEX); + systemPrintln(")"); + } + printControl(*data++); + memcpy(&channelTimer, data, sizeof(channelTimer)); + data += sizeof(channelTimer); + systemPrint(" Channel Timer(ms): "); + systemPrintln(channelTimer); + + systemPrint("ERROR: Invalid msToNextHopRemote value, "); + systemPrintln(msToNextHopRemote); + waitForever(); } - printControl(*data++); - memcpy(&channelTimer, data, sizeof(channelTimer)); - data += sizeof(channelTimer); - systemPrint(" Channel Timer(ms): "); - systemPrintln(channelTimer); - - systemPrint("ERROR: Invalid msToNextHopRemote value, "); - systemPrintln(msToNextHopRemote); - waitForever(); - } + } //ENABLE_DEVELOPER - //Determine if the remote system has hopped and the local system has not - deltaHops = 0; - deltaMillis = msToNextHopRemote - txRxTimeMsec; - if ((deltaMillis <= 0) && ((currentMillis - channelTimerStart) > (settings.maxDwellTime - txRxTimeMsec))) - { - //Hop one channel to catch up with the remote system -// hopChannel(); - deltaHops -= 1; - } + //---------------------------------------------------------------------- + // Enter the critical section + //---------------------------------------------------------------------- - //Compute the time to next hop + //Synchronize with the hardware timer + channelTimer.disableTimer(); + + //When timeToHop is true, a hop is required to match the hops indicated by + //the channelTimerStart value. Delay this hop to avoid adding unaccounted + //delay. After the channel timer is restarted, perform this hop because + //the channelTimerStart value indicated that it was done. The channel + //timer update will add only microseconds to when the hop is done. + delayedHopCount = timeToHop ? 1 : 0; + + //The radios are using the same frequencies since the frame was successfully + //received. The goal is to adjust the channel timer to fire in close proximity + //to the firing of the remote sysstem's channel timer. The following cases + //need to be identified: + // + // 1. Both systems are on the same channel + // Adjust channel timer value + // 2. Remote system is on next channel (remote hopped, local did not) + // Immediate local hop + // Adjust channel timer value + // 3. Remote system on previous channel (local hopped, remote did not) + // Extend channel timer value by maxDwellTime + // + //For low transmission rates, the transmission may have spanned multiple hops + //and all of the frequencies must have matched for the frame to be received + //successfully. Therefore the above cases hold for low frequencies as well. + // + //Compute the remote system's channel timer firing time offset in milliseconds + //using the channel timer value and the adjustments for transmit and receive + //time (time of flight) + rmtHopTimeMsec = msToNextHopRemote - txRxTimeMsec; + + //Compute the when the local system last hopped + lclHopTimeMsec = currentMillis - channelTimerStart; adjustment = 0; - millisToNextHop = msToNextHopRemote - txRxTimeMsec; - if (millisToNextHop <= 0) + +#define REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC 2 +#define CHANNEL_TIMER_SYNC_PLUS_MINUS_MSEC 3 +#define CHANNEL_TIMER_SYNC_MSEC (2 * CHANNEL_TIMER_SYNC_PLUS_MINUS_MSEC) + + //Determine if the remote has hopped or is likely to hop very soon + if (rmtHopTimeMsec <= REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC) { - adjustment = settings.maxDwellTime; - millisToNextHop += adjustment; + //Adjust the next channel timer value + adjustment += settings.maxDwellTime; + + //Determine if the local system has just hopped + if (lclHopTimeMsec <= CHANNEL_TIMER_SYNC_MSEC) + { + //Case 1 above, both systems using the same channel + // + //Remote system + // + // channelTimerStart Channel timer fires + // |------...-------------------------------------------->| + // Channel Timer value | + // |<----------------------->| + // | | rmtHopTimeMsec + // | |<--| + // | | + // | Current Time + // |<------------>|<------------>| + // txTimeUsec rxTimeUsec | + // | + //Local system | + // lclHopTimeMsec | + // |------->| + // | | New timer value + // | |<--|----------------...-->| + // |----------------...-->| + // channelTimerStart Channel timer fires + // + //No hop is necessary + } + else + { + //Case 2 above, the local system did not hop + // + //Remote system + // + // channelTimerStart Channel timer fires Channel timer fires + // |...---------------------->|------...-------------------------------------------->| + // Channel Timer value | + // |<----->| + // | | rmtHopTimeMsec + // | |<--------------------| + // | Current Time + // |<------------>|<------------>| + // txTimeUsec rxTimeUsec | + // | + // | + //Local system | + // Computed value | New timer value + // |<--------------------|------...----------------------->| + // | + // lclHopTimeMsec | + // |------------------------------------>| + // |------...-------------------------------------------->| + // channelTimerStart Channel timer fires + // + //A hop is necessary to get to a common channel +if (COMPUTE_OLD_CHANNEL_TIMER_VALUE) +deltaHops -= 1; +else + delayedHopCount += 1; + } + } + else + { + //The remote system has not hopped + //Determine if the local system has just hopped + if (lclHopTimeMsec <= CHANNEL_TIMER_SYNC_MSEC) + { + //Case 3 above, extend the channel timer value + // + // channelTimerStart Channel timer fires + // |------...-------------------------------------------->| + // Channel Timer value | + // |<--------------------------------->| + // | rmtHopTimeMsec | + // | |---->| + // | | + // | Current Time + // |<------------>|<------------>| + // txTimeUsec rxTimeUsec | + // | + //Local system | + // | New timer value Extend this value + // |---->|------...----------------------->| + // | + // lclHopTimeMsec | + // |-->| + // |------...-------------------------------------->|------...----------------------->| + // channelTimerStart Channel timer fires + // + //No hop is needed + //Extend the timer value + adjustment += settings.maxDwellTime; + } + else + { + //Case 1 above, both systems using the same channel + // + // channelTimerStart Channel timer fires + // |------...-------------------------------------------->| + // Channel Timer value | + // |<--------------------------------->| + // | | + // | rmtHopTimeMsec | + // | |---->| + // | Current Time + // |<------------>|<------------>| + // txTimeUsec rxTimeUsec | + // | + //Local system | + // | New timer value + // |---->| + // |------...-------------------------------------->| + // channelTimerStart | Channel timer fires + // |--------->| + // lclHopTimeMsec + // + //No hop is necessary + } } +#if COMPUTE_OLD_CHANNEL_TIMER_VALUE + nextChannelTimerMsec = msToNextHop; + //msToNextHopRemote is obtained during rcvDatagram() //If the sync arrived in an ACK, we know how long that packet took to transmit @@ -3078,15 +3244,13 @@ void syncChannelTimer() break; } - int16_t msToNextHopLocal = settings.maxDwellTime - (millis() - channelTimerStart); + int16_t msToNextHopLocal = settings.maxDwellTime - (currentMillis - channelTimerStart); //Precalculate large/small time amounts uint16_t smallAmount = settings.maxDwellTime / 8; uint16_t largeAmount = settings.maxDwellTime - smallAmount; - int16_t msToNextHop = msToNextHopRmt; //By default, we will adjust our clock to match our mate's - - bool resetHop = false; //The hop ISR may occur while we are forcing a hop (case A and C). Reset timeToHop as needed. + msToNextHop = msToNextHopRmt; //By default, we will adjust our clock to match our mate's //Below are the edge cases that occur when a hop occurs near ACK reception @@ -3098,7 +3262,6 @@ void syncChannelTimer() hopChannel(); deltaHops += 1; msToNextHop = msToNextHopRmt + settings.maxDwellTime; - resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } //msToNextHopLocal is large and msToNextHopRmt is negative @@ -3117,7 +3280,6 @@ void syncChannelTimer() hopChannel(); deltaHops += 1; msToNextHop = msToNextHopRmt; - resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } //msToNextHopLocal is large and msToNextHopRmt is large @@ -3151,38 +3313,45 @@ void syncChannelTimer() hopChannel(); deltaHops += 1; msToNextHop += settings.maxDwellTime; - resetHop = true; //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. } } //Insure against negative timer values while (msToNextHop < 0) msToNextHop += settings.maxDwellTime; +#endif //COMPUTE_OLD_CHANNEL_TIMER_VALUE //When the ISR fires, reload the channel timer with settings.maxDwellTime reloadChannelTimer = true; - channelTimer.disableTimer(); + + //Restart the channel timer channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's digitalWrite(pin_hop_timer, channelNumber & 1); channelTimerStart = currentMillis; channelTimerMsec = msToNextHop; //syncChannelTimer update - - if (resetHop) //We moved channels. Don't allow the ISR to move us again until after we've updated the timer. - timeToHop = false; - channelTimer.enableTimer(); triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug + //---------------------------------------------------------------------- + // Leave the critical section + //---------------------------------------------------------------------- + + //Hop if the timer fired prior to disabling the timer, resetting the channelTimerStart value + if (delayedHopCount) + hopChannel(true, delayedHopCount); + +#if COMPUTE_OLD_CHANNEL_TIMER_VALUE //Check for a sync error - deltaMillis = (millisToNextHop - msToNextHop + settings.maxDwellTime) % settings.maxDwellTime; - syncError = deltaHops || ((deltaMillis > 3) && (deltaMillis < (settings.maxDwellTime - 2))); + deltaMillis = (nextChannelTimerMsec - msToNextHop + settings.maxDwellTime) % settings.maxDwellTime; + syncError = deltaHops || ((deltaMillis > CHANNEL_TIMER_SYNC_PLUS_MINUS_MSEC) + && (deltaMillis < (settings.maxDwellTime - CHANNEL_TIMER_SYNC_PLUS_MINUS_MSEC))); //Display the channel sync timer calculations if (settings.debugSync || syncError) { systemPrint(msToNextHopRemote); systemPrint(" Nxt Hop - "); - systemPrint((txTimeUsec + rxTimeUsec) / 1000); + systemPrint(txRxTimeMsec); systemPrint(" (TX + RX)"); if (adjustment) { @@ -3191,7 +3360,7 @@ void syncChannelTimer() systemPrint(" Adj"); } systemPrint(" = "); - systemPrint(millisToNextHop); + systemPrint(nextChannelTimerMsec); systemPrintln(" mSec"); systemPrint("msToNextHopRmt: "); @@ -3205,6 +3374,7 @@ void syncChannelTimer() systemPrint(" deltaHops: "); systemPrint(deltaHops); systemPrintln(); + outputSerialData(true); if (syncError) { int16_t channelTimer; @@ -3215,6 +3385,7 @@ void syncChannelTimer() systemPrint("txTimeUsec: "); systemPrintln(txTimeUsec); systemPrintln("RX Frame"); + outputSerialData(true); dumpBuffer(incomingBuffer, headerBytes + rxDataBytes + trailerBytes); data = incomingBuffer; @@ -3225,6 +3396,7 @@ void syncChannelTimer() systemPrint(" (0x"); systemPrint(*data++, HEX); systemPrintln(")"); + outputSerialData(true); } printControl(*data++); memcpy(&channelTimer, data, sizeof(channelTimer)); @@ -3233,9 +3405,10 @@ void syncChannelTimer() systemPrintln(channelTimer); systemPrintln("ERROR: Must fix channel timer synchronization math"); - waitForever(); + outputSerialData(true); } } +#endif //COMPUTE_OLD_CHANNEL_TIMER_VALUE } //This function resets the heartbeat time and re-rolls the random time From a167e3626aa2d46386c1aad3486c9946459fc99b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 07:27:23 -1000 Subject: [PATCH 399/594] Remove the old syncChannelTimer code --- Firmware/LoRaSerial_Firmware/Radio.ino | 199 +------------------------ 1 file changed, 7 insertions(+), 192 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index bf942dd7..25d504b3 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2976,15 +2976,6 @@ void syncChannelTimer() uint16_t msToNextHop; int16_t rmtHopTimeMsec; - int16_t nextChannelTimerMsec; - int8_t deltaHops = 0; - int16_t msToNextHopRmt; - long deltaMillis; - long millisToNextHop; - bool syncError; - -#define COMPUTE_OLD_CHANNEL_TIMER_VALUE 0 - currentMillis = millis(); radioCallHistory[RADIO_CALL_syncChannelTimer] = currentMillis; @@ -3130,9 +3121,6 @@ void syncChannelTimer() // channelTimerStart Channel timer fires // //A hop is necessary to get to a common channel -if (COMPUTE_OLD_CHANNEL_TIMER_VALUE) -deltaHops -= 1; -else delayedHopCount += 1; } } @@ -3195,131 +3183,8 @@ else } } -#if COMPUTE_OLD_CHANNEL_TIMER_VALUE - nextChannelTimerMsec = msToNextHop; - - //msToNextHopRemote is obtained during rcvDatagram() - - //If the sync arrived in an ACK, we know how long that packet took to transmit - //Calculate the packet airTime based on the size of data received - msToNextHopRmt = msToNextHopRemote - calcAirTimeMsec(packetLength); - - // if (settings.debugReceive == true) - // msToNextHopRmt -= 91; //Must adjust for the blob of text being printed - - //Different airspeeds complete the transmitComplete ISR at different rates - //We adjust the clock setup as needed - switch (settings.airSpeed) - { - default: - break; - case (40): - msToNextHopRmt -= getReceiveCompletionOffset(); - break; - case (150): - msToNextHopRmt -= 145; - break; - case (400): - msToNextHopRmt -= getReceiveCompletionOffset(); - break; - case (1200): - msToNextHopRmt -= getReceiveCompletionOffset(); - break; - case (2400): - msToNextHopRmt -= getReceiveCompletionOffset(); - break; - case (4800): - msToNextHopRmt -= 5; - break; - case (9600): - break; - case (19200): - msToNextHopRmt -= 4; - break; - case (28800): - msToNextHopRmt -= 2; - break; - case (38400): - msToNextHopRmt -= 3; - break; - } - - int16_t msToNextHopLocal = settings.maxDwellTime - (currentMillis - channelTimerStart); - - //Precalculate large/small time amounts - uint16_t smallAmount = settings.maxDwellTime / 8; - uint16_t largeAmount = settings.maxDwellTime - smallAmount; - - msToNextHop = msToNextHopRmt; //By default, we will adjust our clock to match our mate's - - //Below are the edge cases that occur when a hop occurs near ACK reception - - //msToNextHopLocal is small and msToNextHopRmt is negative (and small) - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRmt comes in negative (and small) then the remote has hopped - //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRmt + dwellTime) - if (msToNextHopLocal < smallAmount && (msToNextHopRmt <= 0 && msToNextHopRmt >= (smallAmount * -1))) - { - hopChannel(); - deltaHops += 1; - msToNextHop = msToNextHopRmt + settings.maxDwellTime; - } - - //msToNextHopLocal is large and msToNextHopRmt is negative - //If we just hopped (msToNextHopLocal is large), and msToNextHopRmt comes in negative then the remote has hopped - //No need to hop. Adjust our clock to the remote's next hop (msToNextHopRmt + dwellTime) - else if (msToNextHopLocal > largeAmount && msToNextHopRmt <= 0) - { - msToNextHop = msToNextHopRmt + settings.maxDwellTime; - } - - //msToNextHopLocal is small and msToNextHopRmt is large - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRmt comes in large then the remote has hopped recently - //Then hop now, and adjust our clock to the remote's next hop (msToNextHopRmt) - else if (msToNextHopLocal < smallAmount && msToNextHopRmt > largeAmount) - { - hopChannel(); - deltaHops += 1; - msToNextHop = msToNextHopRmt; - } - - //msToNextHopLocal is large and msToNextHopRmt is large - //If we just hopped (msToNextHopLocal is large), and a msToNextHopRmt comes in large then the remote has hopped - //Then adjust our clock to the remote's next hop (msToNextHopRmt) - else if (msToNextHopLocal > largeAmount && msToNextHopRmt > largeAmount) - { - msToNextHop = msToNextHopRmt; - } - - //msToNextHopLocal is large and msToNextHopRmt is small - //If we just hopped (msToNextHopLocal is large), and a msToNextHopRmt comes in small then the remote is about to hop - //Then adjust our clock to the remote's next hop (msToNextHopRmt + dwellTime) - else if (msToNextHopLocal > largeAmount && msToNextHopRmt < smallAmount) - { - msToNextHop = msToNextHopRmt + settings.maxDwellTime; - } - - //msToNextHopLocal is small and msToNextHopRmt is small - //If we are about to hop (msToNextHopLocal is small), and a msToNextHopRmt comes in small then the remote is about to hop - //Then adjust our clock to the remote's next hop (msToNextHopRmt) - else if (msToNextHopLocal < smallAmount && msToNextHopRmt < smallAmount) - { - msToNextHop = msToNextHopRmt; - - //If we have a negative remote hop time that is larger than a dwell time then the remote has hopped again - //This is seen at lower air speeds - //Hop now, and adjust our clock to the remote's next hop (msToNextHopRmt + dwellTime) - if (msToNextHop < (settings.maxDwellTime * -1)) //-402 < -400 - { - hopChannel(); - deltaHops += 1; - msToNextHop += settings.maxDwellTime; - } - } - - //Insure against negative timer values - while (msToNextHop < 0) - msToNextHop += settings.maxDwellTime; -#endif //COMPUTE_OLD_CHANNEL_TIMER_VALUE + //Compute the next hop time + msToNextHop = rmtHopTimeMsec + adjustment; //When the ISR fires, reload the channel timer with settings.maxDwellTime reloadChannelTimer = true; @@ -3330,7 +3195,7 @@ else channelTimerStart = currentMillis; channelTimerMsec = msToNextHop; //syncChannelTimer update channelTimer.enableTimer(); - triggerEvent(TRIGGER_SYNC_CHANNEL); //Trigger after adjustments to timer to avoid skew during debug + triggerFrequency(msToNextHop); //Trigger after adjustments to timer to avoid skew during debug //---------------------------------------------------------------------- // Leave the critical section @@ -3340,15 +3205,11 @@ else if (delayedHopCount) hopChannel(true, delayedHopCount); -#if COMPUTE_OLD_CHANNEL_TIMER_VALUE - //Check for a sync error - deltaMillis = (nextChannelTimerMsec - msToNextHop + settings.maxDwellTime) % settings.maxDwellTime; - syncError = deltaHops || ((deltaMillis > CHANNEL_TIMER_SYNC_PLUS_MINUS_MSEC) - && (deltaMillis < (settings.maxDwellTime - CHANNEL_TIMER_SYNC_PLUS_MINUS_MSEC))); - //Display the channel sync timer calculations - if (settings.debugSync || syncError) + if (settings.debugSync) { + systemPrint(delayedHopCount); + systemPrint(" Hops, "); systemPrint(msToNextHopRemote); systemPrint(" Nxt Hop - "); systemPrint(txRxTimeMsec); @@ -3360,55 +3221,9 @@ else systemPrint(" Adj"); } systemPrint(" = "); - systemPrint(nextChannelTimerMsec); - systemPrintln(" mSec"); - - systemPrint("msToNextHopRmt: "); - systemPrint(msToNextHopRmt); - systemPrint(" msToNextHopLocal: "); - systemPrint(msToNextHopLocal); - systemPrint(" msToNextHop: "); systemPrint(msToNextHop); - systemPrint(" delta: "); - systemPrint(deltaMillis); - systemPrint(" deltaHops: "); - systemPrint(deltaHops); - systemPrintln(); - outputSerialData(true); - if (syncError) - { - int16_t channelTimer; - uint8_t * data; - - systemPrint("rxTimeUsec: "); - systemPrintln(rxTimeUsec); - systemPrint("txTimeUsec: "); - systemPrintln(txTimeUsec); - systemPrintln("RX Frame"); - outputSerialData(true); - dumpBuffer(incomingBuffer, headerBytes + rxDataBytes + trailerBytes); - - data = incomingBuffer; - if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) - { - systemPrint(" Net Id: "); - systemPrint(*data); - systemPrint(" (0x"); - systemPrint(*data++, HEX); - systemPrintln(")"); - outputSerialData(true); - } - printControl(*data++); - memcpy(&channelTimer, data, sizeof(channelTimer)); - data += sizeof(channelTimer); - systemPrint(" Channel Timer(ms): "); - systemPrintln(channelTimer); - - systemPrintln("ERROR: Must fix channel timer synchronization math"); - outputSerialData(true); - } + systemPrintln(" mSec"); } -#endif //COMPUTE_OLD_CHANNEL_TIMER_VALUE } //This function resets the heartbeat time and re-rolls the random time From d9889c17433a27b512b80770839ba65f8fb0b105 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 3 Jan 2023 06:09:58 -1000 Subject: [PATCH 400/594] Always reload the channel timer in the ISR --- Firmware/LoRaSerial_Firmware/Begin.ino | 10 +++------- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/Radio.ino | 6 ------ 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index b92af95f..961d93fc 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -100,13 +100,9 @@ void channelTimerHandler() channelTimerStart = millis(); //Record when this ISR happened. Used for calculating clock sync. radioCallHistory[RADIO_CALL_channelTimerHandler] = channelTimerStart; - //If the last timer was used to sync clocks, restore full timer interval - if (reloadChannelTimer == true) - { - reloadChannelTimer = false; - channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); - channelTimerMsec = settings.maxDwellTime; //ISR update - } + //Restore the full timer interval + channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); + channelTimerMsec = settings.maxDwellTime; //ISR update digitalWrite(pin_hop_timer, channelNumber & 1); if (settings.frequencyHop) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 7f39b0b4..be1d3708 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -170,7 +170,6 @@ SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 volatile uint16_t channelTimerMsec; //Last value programmed into the channel timer volatile unsigned long channelTimerStart = 0; //Tracks how long our timer has been running since last hop volatile bool timeToHop = false; //Set by channelTimerHandler to indicate that hopChannel needs to be called -volatile bool reloadChannelTimer = false; //When set channel timer interval needs to be reloaded with settings.maxDwellTime uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps reduce amount of time spent petting. unsigned long lastPet = 0; //Remebers time of last WDT pet. diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 25d504b3..6b774312 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -733,8 +733,6 @@ void updateHopISR() //We adjust the initial clock setup as needed int16_t getLinkupOffset() { - reloadChannelTimer = true; //Mark timer so that it runs only once with less than dwell time - int linkupOffset = 0; switch (settings.airSpeed) @@ -2944,7 +2942,6 @@ void startChannelTimer(int16_t startAmount) channelTimer.disableTimer(); channelTimer.setInterval_MS(startAmount, channelTimerHandler); digitalWrite(pin_hop_timer, channelNumber & 1); - reloadChannelTimer = (startAmount != settings.maxDwellTime); timeToHop = false; channelTimerStart = millis(); //startChannelTimer - ISR updates value channelTimerMsec = startAmount; //startChannelTimer - ISR updates value @@ -3186,9 +3183,6 @@ void syncChannelTimer() //Compute the next hop time msToNextHop = rmtHopTimeMsec + adjustment; - //When the ISR fires, reload the channel timer with settings.maxDwellTime - reloadChannelTimer = true; - //Restart the channel timer channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's digitalWrite(pin_hop_timer, channelNumber & 1); From df19f6bd5c07c8d57874f45ee4128a8601743892 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 4 Jan 2023 13:55:57 -1000 Subject: [PATCH 401/594] Update the frame diagrams --- Firmware/LoRaSerial_Firmware/Radio.ino | 108 +++++++++++++++++++------ 1 file changed, 82 insertions(+), 26 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 25d504b3..611d680a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2423,17 +2423,17 @@ bool transmitDatagram() } /* - endOfTxData ---. - | - V - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ - | | | - | |<- Length -->| - |<--------- txDatagramSize --------------------->| + endOfTxData ---. + | + V + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ + | | | + | |<- Length -->| + |<-------------------- txDatagramSize --------------------->| */ //Display the packet contents @@ -2468,6 +2468,20 @@ bool transmitDatagram() hopChannel(); } + /* + .------ Header endOfTxData ---. + | | + V V + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ + | | | + | |<- Length -->| + |<-------------------- txDatagramSize --------------------->| + */ + //Add the netID if necessary if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) { @@ -2493,6 +2507,20 @@ bool transmitDatagram() control = *(uint8_t *)&txControl; *header++ = control; + /* + .------ Header endOfTxData ---. + | | + V V + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ + | | | + | |<- Length -->| + |<-------------------- txDatagramSize --------------------->| + */ + //Display the control value if (settings.debugTransmit) printControl(control); @@ -2547,6 +2575,20 @@ bool transmitDatagram() } } + /* + Header ------. endOfTxData ---. + | | + V V + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ + | | | + | |<- Length -->| + |<-------------------- txDatagramSize --------------------->| + */ + //Add the spread factor 6 length if required if (settings.radioSpreadFactor == 6) { @@ -2586,6 +2628,20 @@ bool transmitDatagram() } } + /* endOfTxData ---. + Header ------. | + | | + V V + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ + | | | + | |<- Length -->| + |<-------------------- txDatagramSize --------------------->| + */ + //Verify the Virtual-Circuit length if (settings.debugTransmit && (settings.operatingMode == MODE_VIRTUAL_CIRCUIT)) { @@ -2608,16 +2664,16 @@ bool transmitDatagram() } /* - endOfTxData ---. - | - V - +----------+----------+------------+--- ... ---+ - | Optional | | Optional | | - | NET ID | Control | SF6 Length | Data | - | 8 bits | 8 bits | 8 bits | n bytes | - +----------+----------+------------+-------------+ - | | - |<--------------- txDatagramSize --------------->| + endOfTxData ---. + | + V + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ + | | + |<-------------------- txDatagramSize --------------------->| */ //Add the datagram trailer @@ -2669,11 +2725,11 @@ bool transmitDatagram() endOfTxData ---. | V - +----------+----------+------------+--- ... ---+----------+ - | Optional | | Optional | | Optional | - | NET ID | Control | SF6 Length | Data | Trailer | - | 8 bits | 8 bits | 8 bits | n bytes | n Bytes | - +----------+----------+------------+-------------+----------+ + +----------+----------+----------+------------+--- ... ---+----------+ + | Optional | | Optional | Optional | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Data | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | n bytes | n Bytes | + +----------+----------+----------+------------+-------------+----------+ | | |<-------------------- txDatagramSize --------------------->| */ From 381736853c900356f805fb2744d84c6b6ed0eb6e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 4 Jan 2023 14:22:56 -1000 Subject: [PATCH 402/594] Update the trigger names and code locations Please leave the trigger list alphabetized. Add and remove name only. Reorganize for debugging, but DON'T check in the changes for debugging! --- Firmware/LoRaSerial_Firmware/Radio.ino | 8 +- Firmware/LoRaSerial_Firmware/States.ino | 101 ++++++++++++++++-------- Firmware/LoRaSerial_Firmware/settings.h | 89 ++++++++++----------- 3 files changed, 117 insertions(+), 81 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 611d680a..b39c00d3 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2146,7 +2146,7 @@ PacketType rcvDatagram() requestYield = rxControl.requestYield; if (requestYield) { - triggerEvent(TRIGGER_LINK_REQUEST_YIELD_RECEIVED); + triggerEvent(TRIGGER_RX_YIELD); yieldTimerStart = millis(); } @@ -2372,7 +2372,7 @@ bool transmitDatagram() //the sender yield to give us an opportunity to send our data if ((txControl.datagramType == DATAGRAM_DATA_ACK) && availableRadioTXBytes()) { - triggerEvent(TRIGGER_LINK_REQUEST_YIELD_SENT); + triggerEvent(TRIGGER_TX_YIELD); txControl.requestYield = 1; } else @@ -3251,7 +3251,9 @@ void syncChannelTimer() channelTimerStart = currentMillis; channelTimerMsec = msToNextHop; //syncChannelTimer update channelTimer.enableTimer(); - triggerFrequency(msToNextHop); //Trigger after adjustments to timer to avoid skew during debug + + //Trigger after adjustments to timer to avoid skew during debug + triggerEvent(TRIGGER_SYNC_CHANNEL_TIMER); //---------------------------------------------------------------------- // Leave the critical section diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 07908a32..927a7b79 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -335,7 +335,7 @@ void updateRadioState() } //Acknowledge the FIND_PARTNER with SYNC_CLOCKS - triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); + triggerEvent(TRIGGER_TX_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { sf6ExpectedSize = headerBytes + P2P_ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info @@ -349,9 +349,11 @@ void updateRadioState() else if ((receiveInProcess() == false) && ((millis() - heartbeatTimer) >= randomTime)) { //Transmit the FIND_PARTNER - triggerEvent(TRIGGER_HANDSHAKE_SEND_FIND_PARTNER); + triggerEvent(TRIGGER_TX_FIND_PARTNER); if (xmitDatagramP2PFindPartner() == true) { + if (settings.debugSync) + triggerFrequency(channels[channelNumber]); sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 we expect SYNC_CLOCKS to contain millis info changeState(RADIO_P2P_WAIT_TX_FIND_PARTNER_DONE); } @@ -363,7 +365,7 @@ void updateRadioState() if (transactionComplete) { COMPUTE_TX_TIME(); - triggerEvent(TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE); + triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag returnToReceiving(); changeState(RADIO_P2P_WAIT_SYNC_CLOCKS); @@ -404,7 +406,7 @@ void updateRadioState() COMPUTE_RX_TIME(rxData + 1, 1); //Acknowledge the FIND_PARTNER - triggerEvent(TRIGGER_SEND_SYNC_CLOCKS); + triggerEvent(TRIGGER_TX_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { sf6ExpectedSize = headerBytes + P2P_ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info @@ -429,7 +431,7 @@ void updateRadioState() COMPUTE_RX_TIME(rxData + 1, 1); //Acknowledge the SYNC_CLOCKS - triggerEvent(TRIGGER_SEND_ZERO_ACKS); + triggerEvent(TRIGGER_TX_ZERO_ACKS); if (xmitDatagramP2PZeroAcks() == true) { sf6ExpectedSize = MAX_PACKET_SIZE; //Tell SF6 to return to max packet length @@ -473,7 +475,7 @@ void updateRadioState() if (transactionComplete) { COMPUTE_TX_TIME(); - triggerEvent(TRIGGER_HANDSHAKE_SEND_SYNC_CLOCKS_COMPLETE); + triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag returnToReceiving(); changeState(RADIO_P2P_WAIT_ZERO_ACKS); @@ -647,12 +649,12 @@ void updateRadioState() { COMPUTE_TX_TIME(); } + triggerEvent(TRIGGER_TX_DONE); //Determine the next packet size for SF6 if (ackTimer) { //Waiting for an ACK - triggerEvent(TRIGGER_LINK_WAIT_FOR_ACK); sf6ExpectedSize = headerBytes + CHANNEL_TIMER_BYTES + trailerBytes; } else @@ -713,6 +715,7 @@ void updateRadioState() break; case DATAGRAM_FIND_PARTNER: + triggerEvent(TRIGGER_RX_FIND_PARTNER); breakLink(); break; @@ -720,21 +723,24 @@ void updateRadioState() //Received heartbeat while link was idle. Send ack to sync clocks. //Adjust the timestamp offset COMPUTE_TIMESTAMP_OFFSET(rxData, 1); + triggerEvent(TRIGGER_RX_HEARTBEAT); //Transmit ACK - P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT); + P2P_SEND_ACK(TRIGGER_TX_ACK); break; case DATAGRAM_DATA: + triggerEvent(TRIGGER_RX_DATA); + //Place the data in the serial output buffer serialBufferOutput(rxData, rxDataBytes); //Transmit ACK - P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_DATA); + P2P_SEND_ACK(TRIGGER_TX_ACK); break; case DATAGRAM_DUPLICATE: - P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_DUP); + P2P_SEND_ACK(TRIGGER_TX_DUPLICATE_ACK); break; case DATAGRAM_DATA_ACK: @@ -744,7 +750,7 @@ void updateRadioState() //The datagram we are expecting syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock - triggerEvent(TRIGGER_LINK_ACK_RECEIVED); + triggerEvent(TRIGGER_RX_ACK); //Stop the ACK timer STOP_ACK_TIMER(); @@ -754,6 +760,8 @@ void updateRadioState() break; case DATAGRAM_REMOTE_COMMAND: + triggerEvent(TRIGGER_RX_COMMAND); + //Determine the number of bytes received length = 0; if ((commandRXHead + rxDataBytes) > sizeof(commandRXBuffer)) @@ -770,16 +778,18 @@ void updateRadioState() commandRXHead %= sizeof(commandRXBuffer); //Transmit ACK - P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); + P2P_SEND_ACK(TRIGGER_TX_ACK); break; case DATAGRAM_REMOTE_COMMAND_RESPONSE: + triggerEvent(TRIGGER_RX_COMMAND_RESPONSE); + //Print received data. This is blocking but we do not use the serialTransmitBuffer because we're in command mode (and it's not much data to print). for (int x = 0 ; x < rxDataBytes ; x++) Serial.write(rxData[x]); //Transmit ACK - P2P_SEND_ACK(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE); + P2P_SEND_ACK(TRIGGER_TX_ACK); break; } } @@ -792,10 +802,11 @@ void updateRadioState() heartbeatTimeout = ((millis() - heartbeatTimer) > heartbeatRandomTime); if (heartbeatTimeout) { - triggerEvent(TRIGGER_HEARTBEAT); - if (xmitDatagramP2PHeartbeat() == true) + { + triggerEvent(TRIGGER_TX_HEARTBEAT); changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } //Save the message for retransmission SAVE_TX_BUFFER(); @@ -832,7 +843,7 @@ void updateRadioState() outputSerialData(true); } - triggerEvent(TRIGGER_LINK_RETRANSMIT); + triggerEvent(TRIGGER_RETRANSMIT); if (settings.debugDatagrams) { systemPrintTimestamp(); @@ -867,7 +878,10 @@ void updateRadioState() //Never retransmit the heartbeat, always send a new version to //send the updated time value if (xmitDatagramP2PHeartbeat() == true) + { changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + triggerEvent(TRIGGER_TX_HEARTBEAT); + } } else if (retransmitDatagram(NULL) == true) changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); @@ -882,7 +896,7 @@ void updateRadioState() lostFrames++; //Failed to reach the other system, break the link - triggerEvent(TRIGGER_LINK_RETRANSMIT_FAIL); + triggerEvent(TRIGGER_RETRANSMIT_FAIL); //Break the link breakLink(); @@ -900,18 +914,23 @@ void updateRadioState() //Load command bytes into outgoing packet readyOutgoingCommandPacket(0); - triggerEvent(TRIGGER_LINK_DATA_XMIT); if (remoteCommandResponse) { //Send the command response if (xmitDatagramP2PCommandResponse() == true) + { + triggerEvent(TRIGGER_TX_COMMAND_RESPONSE); changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } } else { //Send the command if (xmitDatagramP2PCommand() == true) + { + triggerEvent(TRIGGER_TX_COMMAND); changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } } //Save the message for retransmission @@ -939,10 +958,11 @@ void updateRadioState() //Check for time to send serial data if (processWaitingSerial(heartbeatTimeout) == true) { - triggerEvent(TRIGGER_LINK_DATA_XMIT); - if (xmitDatagramP2PData() == true) + { + triggerEvent(TRIGGER_TX_DATA); changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } //Save the message for retransmission SAVE_TX_BUFFER(); @@ -1079,7 +1099,7 @@ void updateRadioState() break; case DATAGRAM_SYNC_CLOCKS: - triggerEvent(TRIGGER_LINK_ACK_RECEIVED); + triggerEvent(TRIGGER_RX_SYNC_CLOCKS); //Compute the common clock currentMillis = millis(); @@ -1143,7 +1163,10 @@ void updateRadioState() //Send FIND_PARTNER if (xmitDatagramP2PFindPartner() == true) + { + triggerEvent(TRIGGER_TX_FIND_PARTNER); changeState(RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE); + } } } @@ -1220,13 +1243,15 @@ void updateRadioState() break; case DATAGRAM_FIND_PARTNER: + triggerEvent(TRIGGER_RX_FIND_PARTNER); + //A new radio is saying hello if (settings.server == true) { //Ack their FIND_PARTNER with SYNC_CLOCK if (xmitDatagramP2PSyncClocks() == true) { - triggerEvent(TRIGGER_MP_SEND_ACK_FOR_FIND_PARTNER); + triggerEvent(TRIGGER_TX_SYNC_CLOCKS); changeState(RADIO_MP_WAIT_TX_DONE); } } @@ -1238,6 +1263,7 @@ void updateRadioState() case DATAGRAM_HEARTBEAT: //Received heartbeat - do not ack. + triggerEvent(TRIGGER_RX_HEARTBEAT); //Sync clock if server sent the heartbeat if (settings.server == false) @@ -1253,6 +1279,7 @@ void updateRadioState() case DATAGRAM_DATA: //Received data - do not ack. + triggerEvent(TRIGGER_RX_DATA); //Sync clock if server sent the datagram if (settings.server == false) @@ -1265,7 +1292,6 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; - triggerEvent(TRIGGER_MP_DATA_PACKET); lastPacketReceived = millis(); //Update timestamp for Link LED changeState(RADIO_MP_STANDBY); @@ -1283,9 +1309,9 @@ void updateRadioState() { if (heartbeatTimeout) { - triggerEvent(TRIGGER_HEARTBEAT); if (xmitDatagramMpHeartbeat() == true) { + triggerEvent(TRIGGER_TX_HEARTBEAT); setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer changeState(RADIO_MP_WAIT_TX_DONE); //Wait for heartbeat to transmit } @@ -1296,9 +1322,9 @@ void updateRadioState() //Check for time to send serial data else if (availableRadioTXBytes() && (processWaitingSerial(heartbeatTimeout) == true)) { - triggerEvent(TRIGGER_MP_DATA_PACKET); if (xmitDatagramMpData() == true) { + triggerEvent(TRIGGER_MP_TX_DATA); setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer changeState(RADIO_MP_WAIT_TX_DONE); } @@ -1722,7 +1748,7 @@ void updateRadioState() transactionComplete = false; //Indicate that the transmission is complete - triggerEvent(TRIGGER_VC_TX_DONE); + triggerEvent(TRIGGER_TX_DONE); //Start the receive operation returnToReceiving(); @@ -1809,9 +1835,11 @@ void updateRadioState() case DATAGRAM_DUPLICATE: frequencyCorrection += radio.getFrequencyError() / 1000000.0; - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_DUP); if (xmitVcAckFrame(rxSrcVc)) + { + triggerEvent(TRIGGER_TX_DUPLICATE_ACK); changeState(RADIO_VC_WAIT_TX_DONE); + } break; //Second step in the 3-way handshake, received UNKNOWN_ACKS, respond @@ -1856,11 +1884,13 @@ void updateRadioState() petWDT(); frequencyCorrection += radio.getFrequencyError() / 1000000.0; - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND); //Transmit ACK if (xmitVcAckFrame(rxSrcVc)) + { + triggerEvent(TRIGGER_TX_ACK); changeState(RADIO_VC_WAIT_TX_DONE); + } //Process the command petWDT(); @@ -1901,9 +1931,11 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; //ACK the command response - triggerEvent(TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE); if (xmitVcAckFrame(rxSrcVc)) //Transmit ACK + { + triggerEvent(TRIGGER_TX_ACK); changeState(RADIO_VC_WAIT_TX_DONE); + } break; } } @@ -1919,6 +1951,7 @@ void updateRadioState() //Send another heartbeat if (xmitVcHeartbeat(myVc, myUniqueId)) { + triggerEvent(TRIGGER_TX_HEARTBEAT); if (((uint8_t)myVc) < MAX_VC) virtualCircuitList[myVc].lastTrafficMillis = currentMillis; changeState(RADIO_VC_WAIT_TX_DONE); @@ -1979,7 +2012,10 @@ void updateRadioState() //Retransmit the packet if (retransmitDatagram(((uint8_t)txDestVc <= MAX_VC) ? &virtualCircuitList[txDestVc] : NULL)) + { + triggerEvent(TRIGGER_RETRANSMIT); changeState(RADIO_VC_WAIT_TX_DONE); + } START_ACK_TIMER(); lostFrames++; @@ -2009,7 +2045,10 @@ void updateRadioState() vcHeader->length = readyOutgoingCommandPacket(VC_RADIO_HEADER_BYTES) + VC_RADIO_HEADER_BYTES; if (xmitDatagramP2PCommandResponse()) + { + triggerEvent(TRIGGER_TX_COMMAND_RESPONSE); changeState(RADIO_VC_WAIT_TX_DONE); + } START_ACK_TIMER(); @@ -2100,13 +2139,13 @@ void updateRadioState() if (vcHeader->destVc == VC_BROADCAST) { //Broadcast this data to all VCs, no ACKs will be received - triggerEvent(TRIGGER_VC_TX_DATA); + triggerEvent(TRIGGER_TX_DATA); xmitVcDatagram(); break; } //Transmit the packet - triggerEvent(TRIGGER_VC_TX_DATA); + triggerEvent(TRIGGER_TX_DATA); if (xmitDatagramP2PData() == true) changeState(RADIO_VC_WAIT_TX_DONE); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 91c11ec6..cf1e1a40 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -239,63 +239,58 @@ enum // triggerUseWidthAsMultiplier = true // triggerEnable = 0xffffffff - TRIGGER_CHANNEL_TIMER_ISR, //0 - TRIGGER_TRANSACTION_COMPLETE, + TRIGGER_BAD_PACKET, + TRIGGER_CHANNEL_TIMER_ISR, + TRIGGER_CRC_ERROR, TRIGGER_FREQ_CHANGE, - TRIGGER_TX_SPI_DONE, - TRIGGER_RX_SPI_DONE, - TRIGGER_LINK_SEND_ACK_FOR_HEARTBEAT, - TRIGGER_RADIO_RESET, - TRIGGER_HOP_TIMER_START, - TRIGGER_HOP_TIMER_STOP, - TRIGGER_HEARTBEAT, - TRIGGER_SYNC_CHANNEL, - TRIGGER_LINK_SEND_ACK_FOR_DATA, - TRIGGER_LINK_SEND_ACK_FOR_DUP, - TRIGGER_LINK_RETRANSMIT, - TRIGGER_LINK_WAIT_FOR_ACK, - TRIGGER_LINK_DATA_XMIT, - TRIGGER_LINK_RETRANSMIT_FAIL, - TRIGGER_LINK_REQUEST_YIELD_SENT, - TRIGGER_LINK_REQUEST_YIELD_RECEIVED, - TRIGGER_MP_SCAN, - TRIGGER_MP_DATA_PACKET, - TRIGGER_MP_PACKET_RECEIVED, - TRIGGER_MP_SEND_ACK_FOR_FIND_PARTNER, - TRIGGER_TRANSMIT_CANCELED, - TRIGGER_HANDSHAKE_SYNC_CLOCKS_TIMEOUT, - TRIGGER_HANDSHAKE_SEND_FIND_PARTNER, + TRIGGER_HANDSHAKE_COMPLETE, TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE, TRIGGER_HANDSHAKE_SEND_SYNC_CLOCKS_COMPLETE, - TRIGGER_SEND_SYNC_CLOCKS, - TRIGGER_SEND_ZERO_ACKS, - TRIGGER_HANDSHAKE_COMPLETE, - TRIGGER_LINK_ACK_SENT, - TRIGGER_LINK_ACK_RECEIVED, + TRIGGER_HANDSHAKE_SYNC_CLOCKS_TIMEOUT, TRIGGER_HANDSHAKE_ZERO_ACKS_TIMEOUT, - TRIGGER_RECEIVE_IN_PROCESS_START, - TRIGGER_RECEIVE_IN_PROCESS_END, - TRIGGER_LINK_HB_ACK_REXMIT, - TRIGGER_UNKNOWN_PACKET, - TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND, - TRIGGER_LINK_SEND_ACK_FOR_REMOTE_COMMAND_RESPONSE, - TRIGGER_BAD_PACKET, - TRIGGER_CRC_ERROR, + TRIGGER_HOP_TIMER_START, + TRIGGER_HOP_TIMER_STOP, + TRIGGER_MP_PACKET_RECEIVED, + TRIGGER_MP_SCAN, + TRIGGER_MP_TX_DATA, TRIGGER_NETID_MISMATCH, - TRIGGER_RTR_SHORT_PACKET, + TRIGGER_RADIO_RESET, + TRIGGER_RETRANSMIT, + TRIGGER_RETRANSMIT_FAIL, TRIGGER_RTR_255BYTE, - TRIGGER_TRAINING_CONTROL_PACKET, - TRIGGER_TRAINING_DATA_PACKET, - TRIGGER_TRAINING_NO_ACK, - TRIGGER_TRAINING_CLIENT_TX_FIND_PARTNER_DONE, + TRIGGER_RTR_SHORT_PACKET, + TRIGGER_RX_ACK, + TRIGGER_RX_COMMAND, + TRIGGER_RX_COMMAND_RESPONSE, + TRIGGER_RX_DATA, + TRIGGER_RX_FIND_PARTNER, + TRIGGER_RX_HEARTBEAT, + TRIGGER_RX_SPI_DONE, + TRIGGER_RX_SYNC_CLOCKS, + TRIGGER_RX_YIELD, + TRIGGER_RX_ZERO_ACKS, + TRIGGER_SYNC_CHANNEL_TIMER, TRIGGER_TRAINING_CLIENT_RX_PARAMS, TRIGGER_TRAINING_CLIENT_TX_ACK_DONE, + TRIGGER_TRAINING_CLIENT_TX_FIND_PARTNER_DONE, TRIGGER_TRAINING_SERVER_RX, - TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE, TRIGGER_TRAINING_SERVER_RX_ACK, - TRIGGER_TRAINING_SERVER_STOPPED, - TRIGGER_VC_TX_DONE, - TRIGGER_VC_TX_DATA, + TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE, + TRIGGER_TRANSACTION_COMPLETE, + TRIGGER_TRANSMIT_CANCELED, + TRIGGER_TX_ACK, + TRIGGER_TX_COMMAND, + TRIGGER_TX_COMMAND_RESPONSE, + TRIGGER_TX_DATA, + TRIGGER_TX_DONE, + TRIGGER_TX_DUPLICATE_ACK, + TRIGGER_TX_FIND_PARTNER, + TRIGGER_TX_HEARTBEAT, + TRIGGER_TX_SPI_DONE, + TRIGGER_TX_SYNC_CLOCKS, + TRIGGER_TX_YIELD, + TRIGGER_TX_ZERO_ACKS, + TRIGGER_UNKNOWN_PACKET, }; //Control where to print command output From ad21e4e088bec93e34cb46c180b78d918ee9194b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 4 Jan 2023 14:38:01 -1000 Subject: [PATCH 403/594] Start hopping channels with TX/RX of SYNC_CLOCKS Set the initial clock synchronization at less than a millisecond. The separation of the TX and RX transactionComplete interrupts is around 650 - 700 uSec. By letting the transactionCompleteISR routine call startChannelTimer, the first expire time of the two timers is within 1 millisecond. The transmit completion routine for SYNC_CLOCKS and the receive routine for SYNC_CLOCKS both call hopChannel to change to the next frequency. This ensures that the two radios start hopping as soon as they are synchronized. --- Firmware/LoRaSerial_Firmware/Begin.ino | 12 ++- .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/Radio.ino | 95 +++++++++---------- Firmware/LoRaSerial_Firmware/States.ino | 39 +++++--- 4 files changed, 80 insertions(+), 68 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 961d93fc..0cf06f9a 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -100,13 +100,17 @@ void channelTimerHandler() channelTimerStart = millis(); //Record when this ISR happened. Used for calculating clock sync. radioCallHistory[RADIO_CALL_channelTimerHandler] = channelTimerStart; - //Restore the full timer interval - channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); - channelTimerMsec = settings.maxDwellTime; //ISR update - digitalWrite(pin_hop_timer, channelNumber & 1); + //If the last timer was used to sync clocks, restore full timer interval + if (reloadChannelTimer == true) + { + reloadChannelTimer = false; + channelTimer.setInterval_MS(settings.maxDwellTime, channelTimerHandler); + channelTimerMsec = settings.maxDwellTime; //ISR update + } if (settings.frequencyHop) { + digitalWrite(pin_hop_timer, ((channelNumber + 1) % settings.numberOfChannels) & 1); triggerEvent(TRIGGER_CHANNEL_TIMER_ISR); timeToHop = true; } diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index be1d3708..3ebadf15 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -170,6 +170,7 @@ SAMDTimer channelTimer(TIMER_TCC); //Available: TC3, TC4, TC5, TCC, TCC1 or TCC2 volatile uint16_t channelTimerMsec; //Last value programmed into the channel timer volatile unsigned long channelTimerStart = 0; //Tracks how long our timer has been running since last hop volatile bool timeToHop = false; //Set by channelTimerHandler to indicate that hopChannel needs to be called +volatile bool reloadChannelTimer = false; //When set channel timer interval needs to be reloaded with settings.maxDwellTime uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps reduce amount of time spent petting. unsigned long lastPet = 0; //Remebers time of last WDT pet. @@ -585,6 +586,7 @@ char platformPrefix[25]; //Used for printing platform specific device name, ie " //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- bool clockSyncReceiver; //Receives and processes the clock synchronization +bool startChannelTimerPending; //When true, call startChannelTimer in transactionCompleteISR //RX and TX time measurements uint32_t rxTimeUsec; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 890e8262..86267ae4 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -702,9 +702,17 @@ void printSX1276Registers() void transactionCompleteISR(void) { transactionCompleteMicros = micros(); - triggerEvent(TRIGGER_TRANSACTION_COMPLETE); radioCallHistory[RADIO_CALL_transactionCompleteISR] = millis(); + triggerEvent(TRIGGER_TRANSACTION_COMPLETE); + + //Start the channel timer if requested + if (startChannelTimerPending) + { + startChannelTimer(); //transactionCompleteISR, upon TX or RX of SYNC_CLOCKS + startChannelTimerPending = false; //transactionCompleteISR, upon TX or RX of SYNC_CLOCKS + } + //Signal the state machine that a receive or transmit has completed transactionComplete = true; } @@ -729,51 +737,6 @@ void updateHopISR() } } -//As we complete linkup, different airspeeds exit at different rates -//We adjust the initial clock setup as needed -int16_t getLinkupOffset() -{ - int linkupOffset = 0; - - switch (settings.airSpeed) - { - default: - break; - case (40): - linkupOffset = 0; - break; - case (150): - linkupOffset = 0; - break; - case (400): - linkupOffset = 0; - break; - case (1200): - linkupOffset = 0; - break; - case (2400): - linkupOffset = 0; - break; - case (4800): - linkupOffset = 0; - break; - case (9600): - linkupOffset = 0; - break; - case (19200): - linkupOffset = 0; - break; - case (28800): - linkupOffset = 0; - break; - case (38400): - linkupOffset = 0; - break; - } - - return (settings.maxDwellTime - getReceiveCompletionOffset() - linkupOffset); //Reduce the default window by the offset -} - //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Table for use in calculating the software CRC-16 //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -2529,10 +2492,10 @@ bool transmitDatagram() //Hake sure that the transmitted msToNextHop is in the range 0 - maxDwellTime if (timeToHop) hopChannel(); - uint16_t msToNextHop = settings.maxDwellTime - (millis() - channelTimerStart); //TX channel timer value - memcpy(header, &msToNextHop, sizeof(msToNextHop)); - header += sizeof(msToNextHop); //aka CHANNEL_TIMER_BYTES + unsigned long currentMillis = millis(); + uint16_t msToNextHop = channelTimerMsec - (currentMillis - channelTimerStart); //TX channel timer value + //Validate this value if (ENABLE_DEVELOPER && (!clockSyncReceiver)) { if ((msToNextHop < 0) || (msToNextHop > settings.maxDwellTime)) @@ -2540,7 +2503,8 @@ bool transmitDatagram() int16_t channelTimer; uint8_t * data; - systemPrintln("TX Frame"); + systemPrint("TX Frame "); + systemPrintln(radioDatagramType[txControl.datagramType]); data = outgoingPacket; if ((settings.operatingMode == MODE_POINT_TO_POINT) || settings.verifyRxNetID) { @@ -2558,10 +2522,34 @@ bool transmitDatagram() systemPrint("ERROR: Invalid msToNextHop value, "); systemPrintln(msToNextHop); - waitForever(); + + //Set a valid value + msToNextHop = settings.maxDwellTime; + } + else if (settings.debugSync) + { + switch (txControl.datagramType) + { + case DATAGRAM_DATA_ACK: + case DATAGRAM_SYNC_CLOCKS: + systemPrint("TX: "); + systemPrint(channelTimerMsec); + systemPrint(" channelTimerMsec + ("); + systemPrint(currentMillis); + systemPrint(" millis() - "); + systemPrint(channelTimerStart); + systemPrint(" channelTimerStart) = "); + systemPrint(msToNextHop); + systemPrintln(" mSec"); + break; + } } } //ENABLE_DEVELOPER + //Set the time in the frame + memcpy(header, &msToNextHop, sizeof(msToNextHop)); + header += sizeof(msToNextHop); //aka CHANNEL_TIMER_BYTES + if (settings.debugTransmit) { systemPrintTimestamp(); @@ -2998,6 +2986,7 @@ void startChannelTimer(int16_t startAmount) channelTimer.disableTimer(); channelTimer.setInterval_MS(startAmount, channelTimerHandler); digitalWrite(pin_hop_timer, channelNumber & 1); + reloadChannelTimer = (startAmount != settings.maxDwellTime); timeToHop = false; channelTimerStart = millis(); //startChannelTimer - ISR updates value channelTimerMsec = startAmount; //startChannelTimer - ISR updates value @@ -3013,6 +3002,7 @@ void stopChannelTimer() //Turn off the channel timer channelTimer.disableTimer(); digitalWrite(pin_hop_timer, channelNumber & 1); + channelTimerMsec = 0; //Indicate that the timer is off triggerEvent(TRIGGER_HOP_TIMER_STOP); timeToHop = false; @@ -3239,6 +3229,9 @@ void syncChannelTimer() //Compute the next hop time msToNextHop = rmtHopTimeMsec + adjustment; + //When the ISR fires, reload the channel timer with settings.maxDwellTime + reloadChannelTimer = true; + //Restart the channel timer channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's digitalWrite(pin_hop_timer, channelNumber & 1); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 927a7b79..6251c474 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -243,18 +243,23 @@ void updateRadioState() | P2P_WAIT_TX_FIND_PARTNER_DONE | = true | | | | | | | | | Tx Complete - - - - - > | Rx FIND_PARTNER | | - | | Start Rx | Start HOP Timer | | - | | MAX_PACKET_SIZE | clockSyncReceiver | | - | V | = false | | + | | Start Rx | clockSyncReceiver | | + | | MAX_PACKET_SIZE | = false | | + | V | | | `---- P2P_WAIT_SYNC_CLOCKS | | | | | Tx SYNC_CLOCKS | | | V | | | P2P_WAIT_TX_SYNC_CLOCKS_DONE | | | | | | - Rx SYNC_CLOCKS | < - - - - - - - - - - - | Tx Complete | | + Rx Complete ISR | < - - - - - - - - - - - | Tx Complete ISR | | + | | Start HOP Timer | | + | Start HOP Timer | | | + | V | | + V | In state machine | | + Rx SYNC_CLOCKS | In state machine | Tx Complete | | | | Start Rx | | - | Start HOP Timer | MAX_PACKET_SIZE | | - | Sync HOP Timer | | | + | | MAX_PACKET_SIZE | | + | | | | | V Timeout | | | P2P_WAIT_ZERO_ACKS ----------------' | | | | @@ -321,9 +326,6 @@ void updateRadioState() //Compute the receive time COMPUTE_RX_TIME(rxData, 1); - //Start the channel timer - startChannelTimer(); //Start hopping - P2P clock source - //This system is the source of clock synchronization clockSyncReceiver = false; //P2P clock source @@ -338,6 +340,7 @@ void updateRadioState() triggerEvent(TRIGGER_TX_SYNC_CLOCKS); if (xmitDatagramP2PSyncClocks() == true) { + startChannelTimerPending = true; //Starts at SYNC_CLOCKS TX done sf6ExpectedSize = headerBytes + P2P_ZERO_ACKS_BYTES + trailerBytes; //Tell SF6 we expect ZERO_ACKS to contain millis info changeState(RADIO_P2P_WAIT_TX_SYNC_CLOCKS_DONE); } @@ -367,6 +370,7 @@ void updateRadioState() COMPUTE_TX_TIME(); triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag + startChannelTimerPending = true; //Starts at RX of SYNC_CLOCKS frame returnToReceiving(); changeState(RADIO_P2P_WAIT_SYNC_CLOCKS); } @@ -378,6 +382,13 @@ void updateRadioState() //Decode the received packet PacketType packetType = rcvDatagram(); + //Restart the channel timer when the SYNC_CLOCKS frame is received + if (packetType != DATAGRAM_SYNC_CLOCKS) + { + stopChannelTimer(); //Restart channel timer if SYNC_CLOCKS not RX + startChannelTimerPending = true; //Restart channel timer if SYNC_CLOCKS not RX + } + //Process the received datagram switch (packetType) { @@ -416,10 +427,6 @@ void updateRadioState() case DATAGRAM_SYNC_CLOCKS: //Received SYNC_CLOCKS - //Start the channel timer - startChannelTimer(); //Start hopping - P2P clock receiver - syncChannelTimer(); - //Display the channelTimer sink if (settings.debugSync) { @@ -430,6 +437,9 @@ void updateRadioState() //Compute the receive time COMPUTE_RX_TIME(rxData + 1, 1); + //Hop to the next channel + hopChannel(); + //Acknowledge the SYNC_CLOCKS triggerEvent(TRIGGER_TX_ZERO_ACKS); if (xmitDatagramP2PZeroAcks() == true) @@ -477,6 +487,9 @@ void updateRadioState() COMPUTE_TX_TIME(); triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag + + //Hop to the next channel + hopChannel(); returnToReceiving(); changeState(RADIO_P2P_WAIT_ZERO_ACKS); } From ab5b53774f0fa4474f519c8390d9d0e3323fedf4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 4 Jan 2023 14:42:56 -1000 Subject: [PATCH 404/594] Compute the retransmit timeout (backoff) in retransmitDatagram --- Firmware/LoRaSerial_Firmware/Radio.ino | 6 +- Firmware/LoRaSerial_Firmware/States.ino | 106 ++++++++++++------------ 2 files changed, 57 insertions(+), 55 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 86267ae4..e11f40a2 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2961,7 +2961,11 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) frameAirTime += responseDelay; datagramTimer = millis(); //Move timestamp even if error - retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime); //Wait this number of ms between retransmits. Increases with each re-transmit. + + //Compute the interval before a retransmission occurs in milliseconds, + //this value increases with each transmission + retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime) + * (frameSentCount + 1) * 3 / 2; //BLink the RX LED if (settings.selectLedUse == LEDS_RADIO_USE) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 6251c474..88f6d8c8 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -836,73 +836,71 @@ void updateRadioState() //An ACK is expected when the ACK timer running else if (ackTimer) { - //Check for ACK timeout - if ((millis() - ackTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + //Throttle back retransmits based on the number of retransmits we've attempted + //retransmitTimeout is a random number set in retransmitDatagram + if (millis() - datagramTimer > retransmitTimeout) { //Determine if another retransmission is allowed if ((!settings.maxResends) || (frameSentCount < settings.maxResends)) { - //Throttle back retransmits based on the number of retransmits we've attempted - //retransmitTimeout is a random number, set when the first datagram is sent - if (millis() - datagramTimer > (frameSentCount * retransmitTimeout)) + lostFrames++; + + //Display the retransmit + if (settings.debugDatagrams) { - lostFrames++; + systemPrintTimestamp(); + systemPrintln("RX: ACK Timeout"); + outputSerialData(true); + } - //Display the retransmit - if (settings.debugDatagrams) + if (settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("TX: Retransmit "); + systemPrint(frameSentCount); + systemPrint(", "); + systemPrint(radioDatagramType[txControl.datagramType]); + switch (txControl.datagramType) { - systemPrintTimestamp(); - systemPrintln("RX: ACK Timeout"); - outputSerialData(true); - } + default: + systemPrintln(); + break; - triggerEvent(TRIGGER_RETRANSMIT); - if (settings.debugDatagrams) - { - systemPrintTimestamp(); - systemPrint("TX: Retransmit "); - systemPrint(frameSentCount); - systemPrint(", "); - systemPrint(radioDatagramType[txControl.datagramType]); - switch (txControl.datagramType) - { - default: - systemPrintln(); - break; - - case DATAGRAM_DATA: - case DATAGRAM_DATA_ACK: - case DATAGRAM_REMOTE_COMMAND: - case DATAGRAM_REMOTE_COMMAND_RESPONSE: - case DATAGRAM_HEARTBEAT: - systemPrint(" (ACK #"); - systemPrint(txControl.ackNumber); - systemPrint(")"); - systemPrintln(); - break; - } - outputSerialData(true); + case DATAGRAM_DATA: + case DATAGRAM_DATA_ACK: + case DATAGRAM_REMOTE_COMMAND: + case DATAGRAM_REMOTE_COMMAND_RESPONSE: + case DATAGRAM_HEARTBEAT: + systemPrint(" (ACK #"); + systemPrint(txControl.ackNumber); + systemPrint(")"); + systemPrintln(); + break; } + outputSerialData(true); + } - //Attempt the retransmission - RESTORE_TX_BUFFER(); - if (rexmtControl.datagramType == DATAGRAM_HEARTBEAT) + //Attempt the retransmission + RESTORE_TX_BUFFER(); + if (rexmtControl.datagramType == DATAGRAM_HEARTBEAT) + { + //Never retransmit the heartbeat, always send a new version to + //send the updated time value + if (xmitDatagramP2PHeartbeat() == true) { - //Never retransmit the heartbeat, always send a new version to - //send the updated time value - if (xmitDatagramP2PHeartbeat() == true) - { - changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - triggerEvent(TRIGGER_TX_HEARTBEAT); - } - } - else if (retransmitDatagram(NULL) == true) + triggerEvent(TRIGGER_TX_HEARTBEAT); changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); - - //We're re-sending data, so don't be the first to send next heartbeat - setHeartbeatLong(); - START_ACK_TIMER(); + } } + else if (retransmitDatagram(NULL) == true) + { + triggerEvent(TRIGGER_RETRANSMIT); + changeState(RADIO_P2P_LINK_UP_WAIT_TX_DONE); + } + + //We're re-sending data, so don't be the first to send next heartbeat + setHeartbeatLong(); + START_ACK_TIMER(); } else { From 798d3096cde48e319d37647df03b9366890266c1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 6 Jan 2023 12:57:51 -1000 Subject: [PATCH 405/594] Clear timeToHop when delaying the hop --- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e11f40a2..0cd3df6b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3076,6 +3076,7 @@ void syncChannelTimer() //the channelTimerStart value indicated that it was done. The channel //timer update will add only microseconds to when the hop is done. delayedHopCount = timeToHop ? 1 : 0; + timeToHop = false; //The radios are using the same frequencies since the frame was successfully //received. The goal is to adjust the channel timer to fire in close proximity From ab58f447fd0be3a5c2453a84af002d8936c61a99 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 6 Jan 2023 12:59:12 -1000 Subject: [PATCH 406/594] Don't spin forever when a channel timer value error is detected on RX --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 0cd3df6b..6c1eefc7 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3059,7 +3059,7 @@ void syncChannelTimer() systemPrint("ERROR: Invalid msToNextHopRemote value, "); systemPrintln(msToNextHopRemote); - waitForever(); + return; } } //ENABLE_DEVELOPER From e9519a760767ce60f01c1de24a62823973348fc7 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 6 Jan 2023 13:18:07 -1000 Subject: [PATCH 407/594] Fix the case when both the remote and local values are negative --- Firmware/LoRaSerial_Firmware/Radio.ino | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6c1eefc7..f3a59320 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3105,8 +3105,7 @@ void syncChannelTimer() adjustment = 0; #define REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC 2 -#define CHANNEL_TIMER_SYNC_PLUS_MINUS_MSEC 3 -#define CHANNEL_TIMER_SYNC_MSEC (2 * CHANNEL_TIMER_SYNC_PLUS_MINUS_MSEC) +#define CHANNEL_TIMER_SYNC_MSEC (txRxTimeMsec + REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC) //Determine if the remote has hopped or is likely to hop very soon if (rmtHopTimeMsec <= REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC) @@ -3115,7 +3114,7 @@ void syncChannelTimer() adjustment += settings.maxDwellTime; //Determine if the local system has just hopped - if (lclHopTimeMsec <= CHANNEL_TIMER_SYNC_MSEC) + if (((unsigned long)lclHopTimeMsec) <= CHANNEL_TIMER_SYNC_MSEC) { //Case 1 above, both systems using the same channel // @@ -3176,7 +3175,7 @@ void syncChannelTimer() { //The remote system has not hopped //Determine if the local system has just hopped - if (lclHopTimeMsec <= CHANNEL_TIMER_SYNC_MSEC) + if (((unsigned long)lclHopTimeMsec) <= CHANNEL_TIMER_SYNC_MSEC) { //Case 3 above, extend the channel timer value // From fc83ce5e3503004905a60d3f5d71af08d1ecc74b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 7 Jan 2023 12:34:07 -1000 Subject: [PATCH 408/594] Transmit a valid value of channelTimer when the timer is not running --- Firmware/LoRaSerial_Firmware/Radio.ino | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index f3a59320..a9595f23 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2493,7 +2493,11 @@ bool transmitDatagram() if (timeToHop) hopChannel(); unsigned long currentMillis = millis(); - uint16_t msToNextHop = channelTimerMsec - (currentMillis - channelTimerStart); //TX channel timer value + uint16_t msToNextHop; //TX channel timer value + if (channelTimerMsec) + msToNextHop = channelTimerMsec - (currentMillis - channelTimerStart); + else + msToNextHop = settings.maxDwellTime; //Validate this value if (ENABLE_DEVELOPER && (!clockSyncReceiver)) From 6542a09ea32d356dd992dc677e70dc67a736d241 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 7 Jan 2023 12:34:55 -1000 Subject: [PATCH 409/594] Stop the channel timer when the P2P link is down --- Firmware/LoRaSerial_Firmware/States.ino | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 88f6d8c8..b9ce991b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -293,6 +293,10 @@ void updateRadioState() setRadioFrequency(false); } + //Stop the channel timer if it is running + if (channelTimerMsec) + stopChannelTimer(); + //Determine if a FIND_PARTNER was received if (transactionComplete) { From 2be3dabe2409fbcb812546fdf813c9bcc45c27a2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 7 Jan 2023 12:49:52 -1000 Subject: [PATCH 410/594] Better handle the LEDs Perform the LED operations conditionally depending upon the state of settings.selectLedUse. This commit: * Adds a routine to blink the RSSI LED or LEDs * Renames the routine to blink the serial TX LED * Renames the routine to blink the serial RX LED * Adds a routine to blink the radio RX LED * Adds a routine to blink the radio TX LED * Adds a routine to blink the heartbeat LED * Adds a routine to blink the hop LED --- .../LoRaSerial_Firmware.ino | 23 +- Firmware/LoRaSerial_Firmware/Radio.ino | 13 +- Firmware/LoRaSerial_Firmware/Serial.ino | 8 +- Firmware/LoRaSerial_Firmware/States.ino | 7 +- Firmware/LoRaSerial_Firmware/System.ino | 301 ++++++++++++------ 5 files changed, 214 insertions(+), 138 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 3ebadf15..62a31a3b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -125,14 +125,16 @@ uint8_t pin_hop_timer = PIN_UNDEFINED; #define RADIO_USE_BAD_FRAMES_LED BLUE_LED //Blue #define RADIO_USE_BAD_CRC_LED YELLOW_LED //Yellow -#define LED_MP_HOP_CHANNEL BLUE_LED -#define LED_MP_HEARTBEAT YELLOW_LED +#define LED_MP_HEARTBEAT BLUE_LED +#define LED_MP_HOP_CHANNEL YELLOW_LED #define CYLON_TX_DATA_LED BLUE_LED #define CYLON_RX_DATA_LED YELLOW_LED #define LED_ON HIGH #define LED_OFF LOW + +#define LED_MAX_PULSE_WIDTH 24 //mSec //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Radio Library @@ -364,10 +366,8 @@ bool writeOnCommandExit = false; //Goes true if user specifies ATW command //Global variables - LEDs //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -int trainCylonNumber = 0b0001; -int trainCylonDirection = -1; - -unsigned long lastTrainBlink = 0; //Controls LED during training +uint8_t cylonLedPattern = 0b0001; +bool cylonPatternGoingLeft; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Global variables - Radio (General) @@ -390,19 +390,8 @@ float frequencyCorrection = 0; //Adjust receive freq based on the last packet re volatile bool hop = true; //Clear the DIO1 hop ISR when possible -//RSSI must be above these negative numbers for LED to illuminate -const int rssiLevelLow = -150; -const int rssiLevelMed = -120; -const int rssiLevelHigh = -100; -const int rssiLevelMax = -70; int rssi; //Average signal level, measured during reception of a packet -//LED control values -uint32_t ledPreviousRssiMillis; -uint32_t ledHeartbeatMillis; -int8_t ledRssiCount; //Pulse width count for LED display -int8_t ledRssiValue; //Target pulse width count for LED display - //Link quality metrics uint32_t datagramsSent; //Total number of datagrams sent uint32_t datagramsReceived; //Total number of datagrams received diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a9595f23..fe1929a1 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -558,6 +558,7 @@ void hopChannel(bool moveForwardThroughTable, uint8_t channelCount) //Select the new frequency setRadioFrequency(radioStateTable[radioState].rxState); + blinkChannelHopLed(true); } //Determine the time in milliseconds when channel zero is reached again @@ -2190,10 +2191,7 @@ PacketType rcvDatagram() linkDownTimer = millis(); //Blink the RX LED - if (settings.selectLedUse == LEDS_RADIO_USE) - digitalWrite(RADIO_USE_RX_DATA_LED, LED_ON); - else if (settings.selectLedUse == LEDS_CYLON) - digitalWrite(CYLON_RX_DATA_LED, LED_ON); + blinkRadioRxLed(true); return datagramType; } @@ -2971,11 +2969,8 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime) * (frameSentCount + 1) * 3 / 2; - //BLink the RX LED - if (settings.selectLedUse == LEDS_RADIO_USE) - digitalWrite(RADIO_USE_TX_DATA_LED, LED_ON); - else if (settings.selectLedUse == LEDS_CYLON) - digitalWrite(CYLON_TX_DATA_LED, LED_ON); + //BLink the TX LED + blinkRadioTxLed(true); return (true); //Transmission has started } diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index dc31408e..7a13f2d4 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -247,7 +247,7 @@ void updateSerial() previousHead = rxHead; while (rtsAsserted && arch.serialAvailable() && (transactionComplete == false)) { - rxLED(true); //Turn on LED during serial reception + blinkSerialRxLed(true); //Turn on LED during serial reception //Take a break if there are ISRs to attend to petWDT(); @@ -262,7 +262,7 @@ void updateSerial() serialReceiveBuffer[rxHead++] = incoming; //Push char to holding buffer rxHead %= sizeof(serialReceiveBuffer); } //End Serial.available() - rxLED(false); //Turn off LED + blinkSerialRxLed(false); //Turn off LED //Print the number of bytes received via serial if (settings.debugSerial && (rxHead - previousHead)) @@ -440,7 +440,7 @@ void outputSerialData(bool ignoreISR) dataBytes = availableTXBytes(); while (dataBytes-- && isCTS() && (ignoreISR || (!transactionComplete))) { - txLED(true); //Turn on LED during serial transmissions + blinkSerialTxLed(true); //Turn on LED during serial transmissions //Take a break if there are ISRs to attend to petWDT(); @@ -452,7 +452,7 @@ void outputSerialData(bool ignoreISR) txTail++; txTail %= sizeof(serialTransmitBuffer); } - txLED(false); //Turn off LED + blinkSerialTxLed(false); //Turn off LED } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b9ce991b..057add9b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -741,6 +741,7 @@ void updateRadioState() //Adjust the timestamp offset COMPUTE_TIMESTAMP_OFFSET(rxData, 1); triggerEvent(TRIGGER_RX_HEARTBEAT); + blinkHeartbeatLed(true); //Transmit ACK P2P_SEND_ACK(TRIGGER_TX_ACK); @@ -1288,7 +1289,7 @@ void updateRadioState() lastPacketReceived = millis(); //Update timestamp for Link LED - ledMpHeartbeatOn(); + blinkHeartbeatLed(true); changeState(RADIO_MP_STANDBY); break; @@ -1330,7 +1331,6 @@ void updateRadioState() setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer changeState(RADIO_MP_WAIT_TX_DONE); //Wait for heartbeat to transmit } - ledMpHeartbeatOn(); } } @@ -2903,6 +2903,9 @@ void vcReceiveHeartbeat(uint32_t rxMillis) //Update the timestamp offset if (rxSrcVc == VC_SERVER) { + //Blink the heartbeat LED + blinkHeartbeatLed(true); + //Assume client and server are running the same level of debugging, //then the delay from reading the millisecond value on the server should //get offset by the transmit setup time and the receive overhead time. diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 0d17a326..9e2de995 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -592,31 +592,166 @@ void setRSSI(uint8_t ledBits) digitalWrite(pin_rssi4LED, LOW); } +//Start the cylon LEDs +void startCylonLEDs() +{ + cylonLedPattern = 0b0001; + cylonPatternGoingLeft = true; +} + +//Update the RSSI LED or LEDs +void blinkRadioRssiLed() +{ + unsigned long currentMillis; + static uint32_t ledPreviousRssiMillis; + static unsigned long lastCylonBlink; //Controls LED during training + static int8_t ledRssiPulseWidth; //Target pulse width for LED display + + currentMillis = millis(); + switch (settings.selectLedUse) + { + //Update the pattern on the 4 green LEDs + case LEDS_CYLON: + //Determine if it is time to update the cylon pattern + if ((currentMillis - lastCylonBlink) > 75) + { + lastCylonBlink = currentMillis; + + //The following shows the cylon pattern in four LEDs + // + // LED3 LED2 LED1 LED0 + // X + // X + // X + // X + // X + // X + // X + // X + // ... + // + //Cylon the RSSI LEDs + setRSSI(cylonLedPattern); + + //Determine if a change in direction is necessary + if ((cylonLedPattern == 0b1000) || (cylonLedPattern == 0b0001)) + cylonPatternGoingLeft = cylonPatternGoingLeft ? false : true; + + if (cylonPatternGoingLeft) + cylonLedPattern <<= 1; + else + cylonLedPattern >>= 1; + } + break; + + //Update the single RSSI LED + case LEDS_MULTIPOINT: + case LEDS_RADIO_USE: + //Check for the start of a new pulse + if ((currentMillis - ledPreviousRssiMillis) >= LED_MAX_PULSE_WIDTH) + { + ledPreviousRssiMillis = currentMillis; + + //Determine the RSSI LED pulse width + //The RSSI value ranges from -164 to 30 dB + ledRssiPulseWidth = (150 + rssi) / 10; + if (ledRssiPulseWidth > 0) + digitalWrite(RADIO_USE_RSSI_LED, LED_ON); + } + + //Check for time to turn off the LED + else if ((currentMillis - ledPreviousRssiMillis) >= ledRssiPulseWidth) + digitalWrite(RADIO_USE_RSSI_LED, LED_OFF); + break; + + //Update the 4 green LEDs based upon the RSSI value + case LEDS_RSSI: + if (rssi > -70) + setRSSI(0b1111); + else if (rssi > -100) + setRSSI(0b0111); + else if (rssi > -120) + setRSSI(0b0011); + else if (rssi > -150) + setRSSI(0b0001); + else + setRSSI(0); + break; + } +} + //Set the serial TX (blue) LED value -void txLED(bool illuminate) +void blinkSerialTxLed(bool illuminate) { - if (settings.selectLedUse != LEDS_RSSI) - return; - if (pin_txLED != PIN_UNDEFINED) + switch (settings.selectLedUse) { - if (illuminate == true) - digitalWrite(pin_txLED, HIGH); - else - digitalWrite(pin_txLED, LOW); + case LEDS_RSSI: + if (pin_txLED != PIN_UNDEFINED) + { + if (illuminate == true) + digitalWrite(pin_txLED, HIGH); + else + digitalWrite(pin_txLED, LOW); + } + break; } } //Set the serial RX (yellow) LED value -void rxLED(bool illuminate) +void blinkSerialRxLed(bool illuminate) { - if (settings.selectLedUse != LEDS_RSSI) - return; - if (pin_rxLED != PIN_UNDEFINED) + switch (settings.selectLedUse) + { + case LEDS_RSSI: + if (illuminate == true) + digitalWrite(pin_rxLED, HIGH); + else + digitalWrite(pin_rxLED, LOW); + break; + } +} + +//Blink the RX LED +void blinkRadioRxLed(bool on) +{ + switch (settings.selectLedUse) + { + case LEDS_CYLON: + if (on) + digitalWrite(CYLON_RX_DATA_LED, LED_ON); + else if ((millis() - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(CYLON_RX_DATA_LED, LED_OFF); + break; + + case LEDS_MULTIPOINT: + case LEDS_RADIO_USE: + if (on) + digitalWrite(RADIO_USE_RX_DATA_LED, LED_ON); + else if ((millis() - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(RADIO_USE_RX_DATA_LED, LED_OFF); + break; + } +} + +//BLink the TX LED +void blinkRadioTxLed(bool on) +{ + switch (settings.selectLedUse) { - if (illuminate == true) - digitalWrite(pin_rxLED, HIGH); - else - digitalWrite(pin_rxLED, LOW); + case LEDS_CYLON: + if (on) + digitalWrite(CYLON_TX_DATA_LED, LED_ON); + else if ((millis() - datagramTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(CYLON_TX_DATA_LED, LED_OFF); + break; + + case LEDS_MULTIPOINT: + case LEDS_RADIO_USE: + if (on) + digitalWrite(RADIO_USE_TX_DATA_LED, LED_ON); + else if ((millis() - datagramTimer) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(RADIO_USE_TX_DATA_LED, LED_OFF); + break; } } @@ -636,16 +771,19 @@ void radioLeds() static uint32_t badCrcMillis; //Turn off the RX LED to end the blink - currentMillis = millis(); - if ((currentMillis - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) - digitalWrite(RADIO_USE_RX_DATA_LED, LED_OFF); + blinkRadioRxLed(false); //Turn off the TX LED to end the blink - currentMillis = millis(); - if ((currentMillis - datagramTimer) >= RADIO_USE_BLINK_MILLIS) - digitalWrite(RADIO_USE_TX_DATA_LED, LED_OFF); + blinkRadioTxLed(false); + + //Update the link LED + digitalWrite(RADIO_USE_LINK_LED, isLinked() ? LED_ON : LED_OFF); + + //Update the RSSI LED + blinkRadioRssiLed(); //Blink the bad frames LED + currentMillis = millis(); if (badFrames != previousBadFrames) { previousBadFrames = badFrames; @@ -676,35 +814,39 @@ void radioLeds() badCrcMillis = 0; digitalWrite(RADIO_USE_BAD_CRC_LED, LED_OFF); } +} - //Update the link LED - digitalWrite(RADIO_USE_LINK_LED, isLinked() ? LED_ON : LED_OFF); +//Blink the heartbeat LED +void blinkHeartbeatLed(bool on) +{ + static unsigned long ledHeartbeatMillis; - //Update the RSSI LED - if (currentMillis != ledPreviousRssiMillis) + switch (settings.selectLedUse) { - if (ledRssiCount >= 16) - { - ledRssiValue = (150 + rssi) / 10; - ledRssiCount = 0; - if (ledRssiValue >= ledRssiCount) - digitalWrite(RADIO_USE_RSSI_LED, LED_ON); - } - - //Turn off the RSSI LED - else if (ledRssiValue < ledRssiCount++) - digitalWrite(RADIO_USE_RSSI_LED, LED_OFF); + case LEDS_MULTIPOINT: + if (on) + { + digitalWrite(LED_MP_HEARTBEAT, LED_ON); + ledHeartbeatMillis = millis(); + } + else if ((millis() - ledHeartbeatMillis) > RADIO_USE_BLINK_MILLIS) + digitalWrite(LED_MP_HEARTBEAT, LED_OFF); + break; } - - //Save the last millis value - ledPreviousRssiMillis = currentMillis; } -//Turn on the heartbeat LED -void ledMpHeartbeatOn() +//Blink the channel hop LED +void blinkChannelHopLed(bool on) { - digitalWrite(LED_MP_HEARTBEAT, LED_OFF); - ledHeartbeatMillis = millis(); + switch (settings.selectLedUse) + { + case LEDS_MULTIPOINT: + if (on) + digitalWrite(LED_MP_HOP_CHANNEL, LED_ON); + else if ((millis() - radioCallHistory[RADIO_CALL_hopChannel]) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(LED_MP_HOP_CHANNEL, LED_OFF); + break; + } } //Display the multi-point LED pattern @@ -713,83 +855,37 @@ void multiPointLeds() uint32_t currentMillis; //Turn off the RX LED to end the blink - currentMillis = millis(); - if ((currentMillis - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) - digitalWrite(RADIO_USE_RX_DATA_LED, LED_OFF); + blinkRadioRxLed(false); //Turn off the TX LED to end the blink - currentMillis = millis(); - if ((currentMillis - datagramTimer) >= RADIO_USE_BLINK_MILLIS) - digitalWrite(RADIO_USE_TX_DATA_LED, LED_OFF); + blinkRadioTxLed(false); //Update the sync LED digitalWrite(RADIO_USE_LINK_LED, isMultiPointSync() ? LED_ON : LED_OFF); //Update the RSSI LED - if (currentMillis != ledPreviousRssiMillis) - { - if (ledRssiCount >= 16) - { - ledRssiValue = (150 + rssi) / 10; - ledRssiCount = 0; - if (ledRssiValue >= ledRssiCount) - digitalWrite(RADIO_USE_RSSI_LED, LED_ON); - } - - //Turn off the RSSI LED - else if (ledRssiValue < ledRssiCount++) - digitalWrite(RADIO_USE_RSSI_LED, LED_OFF); - } + blinkRadioRssiLed(); //Update the hop LED if ((millis() - radioCallHistory[RADIO_CALL_hopChannel]) >= RADIO_USE_BLINK_MILLIS) digitalWrite(LED_MP_HOP_CHANNEL, LED_OFF); //Update the HEARTBEAT LED - if ((millis() - ledHeartbeatMillis) >= RADIO_USE_BLINK_MILLIS) - digitalWrite(LED_MP_HEARTBEAT, LED_OFF); - - //Save the last millis value - ledPreviousRssiMillis = currentMillis; -} - -//Start the cylon LEDs -void startCylonLEDs() -{ - trainCylonNumber = 0b0001; - trainCylonDirection = -1; + blinkHeartbeatLed(false); } //Update the cylon LEDs void updateCylonLEDs() { - unsigned long currentMillis; - - currentMillis = millis(); - if ((currentMillis - lastTrainBlink) > 75) //Blink while unit waits in training state - { - lastTrainBlink = millis(); - - //Cylon the RSSI LEDs - setRSSI(trainCylonNumber); - - if (trainCylonNumber == 0b1000 || trainCylonNumber == 0b0001) - trainCylonDirection *= -1; //Change direction - - if (trainCylonDirection > 0) - trainCylonNumber <<= 1; - else - trainCylonNumber >>= 1; - } + //Display the cylon pattern on the RSSI LEDs + blinkRadioRssiLed(); //Use the blue and yellow LEDS to indicate radio activity during training //Turn off the RX LED to end the blink - if ((currentMillis - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) - digitalWrite(CYLON_RX_DATA_LED, LED_OFF); + blinkRadioRxLed(false); //Turn off the TX LED to end the blink - if ((currentMillis - datagramTimer) >= RADIO_USE_BLINK_MILLIS) - digitalWrite(CYLON_TX_DATA_LED, LED_OFF); + blinkRadioTxLed(false); } //Update the LED values depending upon the selected display @@ -816,14 +912,7 @@ void updateLeds() //Set LEDs according to RSSI level default: case LEDS_RSSI: - if (rssi > rssiLevelLow) - setRSSI(0b0001); - if (rssi > rssiLevelMed) - setRSSI(0b0011); - if (rssi > rssiLevelHigh) - setRSSI(0b0111); - if (rssi > rssiLevelMax) - setRSSI(0b1111); + blinkRadioRssiLed(); break; case LEDS_RADIO_USE: From 91aca0aadfb3c38f66fad1929c18544541f6e13e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 8 Jan 2023 14:17:49 -1000 Subject: [PATCH 411/594] Add the timestamp to link up and link down display --- Firmware/LoRaSerial_Firmware/States.ino | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 057add9b..3d3e72f6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2618,10 +2618,25 @@ void displayRadioStateHistory() //Break a point-to-point link void breakLink() { + unsigned long linkDownTime; + //Break the link + if (settings.printTimestamp) + { + linkDownTime = millis(); + + //Offset the value for display + if (!settings.displayRealMillis) + linkDownTime += timestampOffset; + } linkFailures++; if (settings.printLinkUpDown) { + if (settings.printTimestamp) + { + systemPrintTimestamp(linkDownTime); + systemPrint(": "); + } systemPrintln("--------- Link DOWN ---------"); outputSerialData(true); } @@ -2663,6 +2678,7 @@ void enterLinkUp() changeState(RADIO_P2P_LINK_UP); if (settings.printLinkUpDown) { + systemPrintTimestamp(); systemPrintln("========== Link UP =========="); outputSerialData(true); } From 3f2898dc034e0bf92d429635d05ac8c5aba07e28 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 8 Jan 2023 14:28:23 -1000 Subject: [PATCH 412/594] Add frameAirTimeMsec parameter to syncChannelTime --- Firmware/LoRaSerial_Firmware/Radio.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/States.ino | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index fe1929a1..9d7d4dcb 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3013,7 +3013,7 @@ void stopChannelTimer() //Given the remote unit's number of ms before its next hop, //adjust our own channelTimer interrupt to be synchronized with the remote unit -void syncChannelTimer() +void syncChannelTimer(uint16_t frameAirTimeMsec) { int16_t adjustment; unsigned long currentMillis; @@ -3097,14 +3097,14 @@ void syncChannelTimer() //Compute the remote system's channel timer firing time offset in milliseconds //using the channel timer value and the adjustments for transmit and receive //time (time of flight) - rmtHopTimeMsec = msToNextHopRemote - txRxTimeMsec; + rmtHopTimeMsec = msToNextHopRemote - frameAirTimeMsec; //Compute the when the local system last hopped lclHopTimeMsec = currentMillis - channelTimerStart; adjustment = 0; #define REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC 2 -#define CHANNEL_TIMER_SYNC_MSEC (txRxTimeMsec + REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC) +#define CHANNEL_TIMER_SYNC_MSEC (frameAirTimeMsec + REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC) //Determine if the remote has hopped or is likely to hop very soon if (rmtHopTimeMsec <= REMOTE_SYSTEM_LIKELY_TO_HOP_MSEC) @@ -3260,7 +3260,7 @@ void syncChannelTimer() systemPrint(" Hops, "); systemPrint(msToNextHopRemote); systemPrint(" Nxt Hop - "); - systemPrint(txRxTimeMsec); + systemPrint(frameAirTimeMsec); systemPrint(" (TX + RX)"); if (adjustment) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3d3e72f6..a217f9e1 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -766,7 +766,7 @@ void updateRadioState() COMPUTE_RX_TIME(rxData, 1); //The datagram we are expecting - syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on remote's remaining clock triggerEvent(TRIGGER_RX_ACK); @@ -1122,7 +1122,7 @@ void updateRadioState() COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0); //Server has responded with ACK - syncChannelTimer(); //Start and adjust freq hop ISR based on remote's remaining clock + syncChannelTimer(txRxTimeMsec); //Start and adjust freq hop ISR based on remote's remaining clock //Change to the server's channel number channelNumber = rxVcData[0]; @@ -1283,7 +1283,7 @@ void updateRadioState() //Sync clock if server sent the heartbeat if (settings.server == false) - syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on remote's remaining clock frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -1299,7 +1299,7 @@ void updateRadioState() //Sync clock if server sent the datagram if (settings.server == false) - syncChannelTimer(); //Adjust freq hop ISR based on remote's remaining clock + syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on remote's remaining clock setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer @@ -2914,7 +2914,7 @@ void vcReceiveHeartbeat(uint32_t rxMillis) int vcSrc; if (rxSrcVc == VC_SERVER) - syncChannelTimer(); //Adjust freq hop ISR based on server's remaining clock + syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on server's remaining clock //Update the timestamp offset if (rxSrcVc == VC_SERVER) From 72e44927074c2d1e8d9550277983597c7f24ebb1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 8 Jan 2023 14:52:33 -1000 Subject: [PATCH 413/594] Add channelTimer history --- .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/Radio.ino | 18 +++++-- Firmware/LoRaSerial_Firmware/States.ino | 52 +++++++++++++++++-- Firmware/LoRaSerial_Firmware/settings.h | 11 ++++ 4 files changed, 75 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 62a31a3b..90c8e154 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -429,6 +429,8 @@ unsigned long radioStateHistory[RADIO_MAX_STATE]; uint8_t packetLength = 0; //Total bytes received, used for calculating clock sync times in multi-point mode int16_t msToNextHopRemote; //Can become negative +uint8_t clockSyncIndex; +CLOCK_SYNC_DATA clockSyncData[16]; bool requestYield = false; //Datagram sender can tell this radio to stop transmitting to enable two-way comm unsigned long yieldTimerStart = 0; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 9d7d4dcb..d008d3ba 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3075,7 +3075,6 @@ void syncChannelTimer(uint16_t frameAirTimeMsec) //the channelTimerStart value indicated that it was done. The channel //timer update will add only microseconds to when the hop is done. delayedHopCount = timeToHop ? 1 : 0; - timeToHop = false; //The radios are using the same frequencies since the frame was successfully //received. The goal is to adjust the channel timer to fire in close proximity @@ -3235,20 +3234,31 @@ void syncChannelTimer(uint16_t frameAirTimeMsec) //When the ISR fires, reload the channel timer with settings.maxDwellTime reloadChannelTimer = true; + //Log the previous clock sync + clockSyncData[clockSyncIndex].msToNextHop = msToNextHop; + clockSyncData[clockSyncIndex].frameAirTimeMsec = frameAirTimeMsec; + clockSyncData[clockSyncIndex].msToNextHopRemote = msToNextHopRemote; + clockSyncData[clockSyncIndex].adjustment = adjustment; + clockSyncData[clockSyncIndex].delayedHopCount = delayedHopCount; + clockSyncData[clockSyncIndex].lclHopTimeMsec = lclHopTimeMsec; + clockSyncData[clockSyncIndex].timeToHop = timeToHop; + clockSyncIndex += 1; + //Restart the channel timer + timeToHop = false; channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's digitalWrite(pin_hop_timer, channelNumber & 1); channelTimerStart = currentMillis; channelTimerMsec = msToNextHop; //syncChannelTimer update channelTimer.enableTimer(); - //Trigger after adjustments to timer to avoid skew during debug - triggerEvent(TRIGGER_SYNC_CHANNEL_TIMER); - //---------------------------------------------------------------------- // Leave the critical section //---------------------------------------------------------------------- + //Trigger after adjustments to timer to avoid skew during debug + triggerEvent(TRIGGER_SYNC_CHANNEL_TIMER); + //Hop if the timer fired prior to disabling the timer, resetting the channelTimerStart value if (delayedHopCount) hopChannel(true, delayedHopCount); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index a217f9e1..759b640e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1355,6 +1355,7 @@ void updateRadioState() systemPrintln("HEARTBEAT Timeout"); outputSerialData(true); } + dumpClockSynchronization(); changeState(RADIO_DISCOVER_BEGIN); } } @@ -2615,6 +2616,43 @@ void displayRadioStateHistory() petWDT(); } +//Dump the clock synchronization data +void dumpClockSynchronization() +{ + //Dump the clock sync data + petWDT(); + for (uint8_t x = 0; x < (sizeof(clockSyncData) / sizeof(clockSyncData[0])); x++) + { + uint8_t index = (x + clockSyncIndex) % clockSyncIndex; + if (clockSyncData[index].frameAirTimeMsec) + { + systemPrint("Lcl: "); + systemPrint(clockSyncData[index].lclHopTimeMsec); + systemPrint(", Rmt: "); + systemPrint(clockSyncData[index].msToNextHopRemote); + systemPrint(" - "); + systemPrint(clockSyncData[index].frameAirTimeMsec); + systemPrint(" = "); + systemPrint(clockSyncData[index].msToNextHopRemote - clockSyncData[index].frameAirTimeMsec); + systemPrint(" + "); + systemPrint(clockSyncData[index].adjustment); + systemPrint(" = "); + systemPrint(clockSyncData[index].msToNextHop); + systemPrint(" msToNextHop"); + if (clockSyncData[index].delayedHopCount) + { + systemPrint(", timeToHop: "); + systemPrint(clockSyncData[index].timeToHop); + systemPrint(", Hops: "); + systemPrint(clockSyncData[index].delayedHopCount); + } + systemPrintln(); + outputSerialData(true); + petWDT(); + } + } +} + //Break a point-to-point link void breakLink() { @@ -2630,6 +2668,13 @@ void breakLink() linkDownTime += timestampOffset; } linkFailures++; + + //Stop the ACK timer + STOP_ACK_TIMER(); + + //Dump the clock synchronization + dumpClockSynchronization(); + if (settings.printLinkUpDown) { if (settings.printTimestamp) @@ -2640,13 +2685,12 @@ void breakLink() systemPrintln("--------- Link DOWN ---------"); outputSerialData(true); } - triggerEvent(TRIGGER_RADIO_RESET); - - //Stop the ACK timer - STOP_ACK_TIMER(); //Flush the buffers resetSerial(); + + //Reset the radio and the link + triggerEvent(TRIGGER_RADIO_RESET); changeState(RADIO_RESET); } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index cf1e1a40..694776e0 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -353,6 +353,17 @@ typedef enum //Add user LED types from 255 working down } LEDS_USE_TYPE; +typedef struct _CLOCK_SYNC_DATA +{ + int16_t msToNextHopRemote; + uint16_t frameAirTimeMsec; + uint16_t msToNextHop; + int16_t lclHopTimeMsec; + int16_t adjustment; + int8_t delayedHopCount; + bool timeToHop; +} CLOCK_SYNC_DATA; + //These are all the settings that can be set on Serial Terminal Radio. It's recorded to NVM. typedef struct struct_settings { uint16_t sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int From ca7dde8d9b9aff55cca8a2d1117982f7bc28f98b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 8 Jan 2023 15:19:12 -1000 Subject: [PATCH 414/594] Add a trigger to measure the transmit time --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 12 +++++------- Firmware/LoRaSerial_Firmware/settings.h | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 90c8e154..b24d4afe 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -584,6 +584,7 @@ uint32_t rxTimeUsec; uint32_t txTimeUsec; uint16_t txRxTimeMsec; +uint32_t txSetChannelTimerMicros; //Timestamp when millis is read in TX routine to set channel timer value uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram routine uint16_t maxFrameAirTime; //Air time of the maximum sized message diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index d008d3ba..e6df726b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2490,6 +2490,10 @@ bool transmitDatagram() //Hake sure that the transmitted msToNextHop is in the range 0 - maxDwellTime if (timeToHop) hopChannel(); + + //Measure the time to the next hop + triggerEvent(TRIGGER_TX_LOAD_CHANNE_TIMER_VALUE); + txSetChannelTimerMicros = micros(); unsigned long currentMillis = millis(); uint16_t msToNextHop; //TX channel timer value if (channelTimerMsec) @@ -2534,13 +2538,7 @@ bool transmitDatagram() { case DATAGRAM_DATA_ACK: case DATAGRAM_SYNC_CLOCKS: - systemPrint("TX: "); - systemPrint(channelTimerMsec); - systemPrint(" channelTimerMsec + ("); - systemPrint(currentMillis); - systemPrint(" millis() - "); - systemPrint(channelTimerStart); - systemPrint(" channelTimerStart) = "); + systemPrint("TX msToNextHop: "); systemPrint(msToNextHop); systemPrintln(" mSec"); break; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 694776e0..64f80453 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -286,6 +286,7 @@ enum TRIGGER_TX_DUPLICATE_ACK, TRIGGER_TX_FIND_PARTNER, TRIGGER_TX_HEARTBEAT, + TRIGGER_TX_LOAD_CHANNE_TIMER_VALUE, TRIGGER_TX_SPI_DONE, TRIGGER_TX_SYNC_CLOCKS, TRIGGER_TX_YIELD, From 1743f9daf8706f3301d56d9ef36c93667210060b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 8 Jan 2023 15:52:59 -1000 Subject: [PATCH 415/594] Compute the transmit times for SYNC_CLOCKS, HEARTBEAT and DATA_ACK --- .../LoRaSerial_Firmware.ino | 3 + Firmware/LoRaSerial_Firmware/States.ino | 56 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b24d4afe..e93d9d6b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -586,6 +586,9 @@ uint16_t txRxTimeMsec; uint32_t txSetChannelTimerMicros; //Timestamp when millis is read in TX routine to set channel timer value uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine +uint32_t txDataAckUsec; //Time in microseconds to transmit the DATA_ACK frame +uint32_t txHeartbeatUsec; //Time in microseconds to transmit the HEARTBEAT frame +uint32_t txSyncClockUsec; //Time in microseconds to transmit the SYNC_CLOCKS frame uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram routine uint16_t maxFrameAirTime; //Air time of the maximum sized message unsigned long remoteSystemMillis; //Millis value contained in the received message diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 759b640e..626262a3 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -488,6 +488,17 @@ void updateRadioState() //Determine if a SYNC_CLOCKS has completed transmission if (transactionComplete) { + //Compute the SYNC_CLOCKS frame transmit frame + if ((!txSyncClockUsec) && (txControl.datagramType == DATAGRAM_SYNC_CLOCKS)) + { + txSyncClockUsec = transactionCompleteMicros - txSetChannelTimerMicros; + if (settings.debugSync) + { + systemPrint("txSyncClockUsec: "); + systemPrintln(txSyncClockUsec); + } + } + COMPUTE_TX_TIME(); triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag @@ -661,6 +672,28 @@ void updateRadioState() { transactionComplete = false; //Reset ISR flag + //Compute the ACK frame transmit frame + if ((!txDataAckUsec) && (txControl.datagramType == DATAGRAM_HEARTBEAT)) + { + txDataAckUsec = transactionCompleteMicros - txSetChannelTimerMicros; + if (settings.debugSync) + { + systemPrint("txDataAckUsec: "); + systemPrintln(txDataAckUsec); + } + } + + //Compute the HEARTBEAT frame transmit frame + if ((!txHeartbeatUsec) && (txControl.datagramType == DATAGRAM_HEARTBEAT)) + { + txHeartbeatUsec = transactionCompleteMicros - txSetChannelTimerMicros; + if (settings.debugSync) + { + systemPrint("txHeartbeatUsec: "); + systemPrintln(txHeartbeatUsec); + } + } + //Determine if an ACK was transmitted if (txControl.datagramType = DATAGRAM_DATA_ACK) { @@ -1374,6 +1407,29 @@ void updateRadioState() if (transactionComplete == true) { transactionComplete = false; //Reset ISR flag + + //Compute the HEARTBEAT frame transmit frame + if ((!txHeartbeatUsec) && (txControl.datagramType == DATAGRAM_HEARTBEAT)) + { + txHeartbeatUsec = transactionCompleteMicros - txSetChannelTimerMicros; + if (settings.debugSync) + { + systemPrint("txHeartbeatUsec: "); + systemPrintln(txHeartbeatUsec); + } + } + + //Compute the SYNC_CLOCKS frame transmit frame + else if ((!txSyncClockUsec) && (txControl.datagramType == DATAGRAM_SYNC_CLOCKS)) + { + txSyncClockUsec = transactionCompleteMicros - txSetChannelTimerMicros; + if (settings.debugSync) + { + systemPrint("txSyncClockUsec: "); + systemPrintln(txSyncClockUsec); + } + } + returnToReceiving(); changeState(RADIO_MP_STANDBY); } From d962c67dcb555417a68368d155e187ba6c3ebb46 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 8 Jan 2023 17:58:27 -1000 Subject: [PATCH 416/594] Pass in the frame air time when computing the timestamp offset --- .../LoRaSerial_Firmware.ino | 2 ++ Firmware/LoRaSerial_Firmware/States.ino | 31 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index e93d9d6b..f1e2d81f 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -72,6 +72,8 @@ const int FIRMWARE_VERSION_MINOR = 0; #define P2P_ACK_BYTES sizeof(unsigned long) //Number of data bytes in the ACK frame #define VC_HEARTBEAT_BYTES 0 //Number of data bytes in the VC_HEARTBEAT frame +#define TX_TO_RX_USEC 680 //Time from TX to RX transactionComplete in microseconds + //Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected //Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock //Bit 3: Header Info Valid toggles high when a valid Header (with correct CRC) is detected diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 626262a3..d425c548 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -48,15 +48,15 @@ ackTimer = 0; \ } -#define COMPUTE_TIMESTAMP_OFFSET(millisBuffer, rShift) \ - { \ - unsigned long deltaUsec = txTimeUsec + rxTimeUsec; \ - memcpy(&remoteSystemMillis, millisBuffer, sizeof(currentMillis)); \ - timestampOffset = (remoteSystemMillis + (deltaUsec / 1000) - currentMillis);\ - timestampOffset >>= rShift; \ +#define COMPUTE_TIMESTAMP_OFFSET(millisBuffer, rShift, frameAirTimeUsec) \ + { \ + unsigned long deltaUsec = frameAirTimeUsec + micros() - transactionCompleteMicros; \ + memcpy(&remoteSystemMillis, millisBuffer, sizeof(currentMillis)); \ + timestampOffset = (remoteSystemMillis + (deltaUsec / 1000) - currentMillis); \ + timestampOffset >>= rShift; \ } -#define COMPUTE_RX_TIME(millisBuffer, rShift) \ +#define COMPUTE_RX_TIME(millisBuffer, rShift, frameAirTimeUsec) \ { \ currentMillis = millis(); \ long deltaUsec = micros() - transactionCompleteMicros; \ @@ -80,7 +80,7 @@ } \ \ /*Adjust the timestamp offset*/ \ - COMPUTE_TIMESTAMP_OFFSET(millisBuffer, rShift); \ + COMPUTE_TIMESTAMP_OFFSET(millisBuffer, rShift, frameAirTimeUsec); \ } #define COMPUTE_TX_TIME() \ @@ -328,7 +328,7 @@ void updateRadioState() case DATAGRAM_FIND_PARTNER: //Received FIND_PARTNER //Compute the receive time - COMPUTE_RX_TIME(rxData, 1); + COMPUTE_RX_TIME(rxData, 1, calcAirTimeUsec(headerBytes + P2P_FIND_PARTNER_BYTES + trailerBytes)); //This system is the source of clock synchronization clockSyncReceiver = false; //P2P clock source @@ -418,7 +418,7 @@ void updateRadioState() case DATAGRAM_FIND_PARTNER: //Received FIND_PARTNER //Compute the receive time - COMPUTE_RX_TIME(rxData + 1, 1); + COMPUTE_RX_TIME(rxData + 1, 1, calcAirTimeUsec(headerBytes + P2P_FIND_PARTNER_BYTES + trailerBytes)); //Acknowledge the FIND_PARTNER triggerEvent(TRIGGER_TX_SYNC_CLOCKS); @@ -439,7 +439,7 @@ void updateRadioState() } //Compute the receive time - COMPUTE_RX_TIME(rxData + 1, 1); + COMPUTE_RX_TIME(rxData + 1, 1, txSyncClockUsec); //Hop to the next channel hopChannel(); @@ -541,7 +541,7 @@ void updateRadioState() case DATAGRAM_ZERO_ACKS: //Received ACK 2 //Compute the receive time - COMPUTE_RX_TIME(rxData, 1); + COMPUTE_RX_TIME(rxData, 1, calcAirTimeUsec(headerBytes + P2P_ZERO_ACKS_BYTES + trailerBytes)); setHeartbeatLong(); //We sent SYNC_CLOCKS and they sent ZERO_ACKS, so don't be the first to send heartbeat @@ -772,7 +772,7 @@ void updateRadioState() case DATAGRAM_HEARTBEAT: //Received heartbeat while link was idle. Send ack to sync clocks. //Adjust the timestamp offset - COMPUTE_TIMESTAMP_OFFSET(rxData, 1); + COMPUTE_TIMESTAMP_OFFSET(rxData, 1, txHeartbeatUsec); triggerEvent(TRIGGER_RX_HEARTBEAT); blinkHeartbeatLed(true); @@ -796,7 +796,7 @@ void updateRadioState() case DATAGRAM_DATA_ACK: //Adjust the timestamp offset - COMPUTE_RX_TIME(rxData, 1); + COMPUTE_RX_TIME(rxData, 1, txDataAckUsec); //The datagram we are expecting syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on remote's remaining clock @@ -1152,7 +1152,7 @@ void updateRadioState() //Compute the common clock currentMillis = millis(); - COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0); + COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0, txTimeUsec + rxTimeUsec); //Server has responded with ACK syncChannelTimer(txRxTimeMsec); //Start and adjust freq hop ISR based on remote's remaining clock @@ -1172,7 +1172,6 @@ void updateRadioState() frequencyCorrection += radio.getFrequencyError() / 1000000.0; lastPacketReceived = millis(); //Reset - changeState(RADIO_MP_STANDBY); break; } From c3843dc3354acc121dc4a677dfa997ad78ec5250 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 06:05:36 -1000 Subject: [PATCH 417/594] Rename FrameTimeout to SerialDelay and move to serial settings --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index ad2b3528..4ac6950f 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1102,7 +1102,6 @@ const COMMAND_ENTRY commands[] = {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "EncryptData", &tempSettings.encryptData}, {'R', 0, 1, 0, 0, 0, TYPE_KEY, valKey, "EncryptionKey", &tempSettings.encryptionKey}, {'R', 0, 0, 0, 255, 0, TYPE_U8, valInt, "FramesToYield", &tempSettings.framesToYield}, - {'R', 0, 0, 10, 2000, 0, TYPE_U16, valInt, "FrameTimeout", &tempSettings.serialTimeoutBeforeSendingFrame_ms}, {'R', 0, 0, 250, 65535, 0, TYPE_U16, valInt, "HeartBeatTimeout", &tempSettings.heartbeatTimeout}, {'R', 0, 0, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &tempSettings.maxResends}, {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "NetID", &tempSettings.netID}, @@ -1118,6 +1117,7 @@ const COMMAND_ENTRY commands[] = {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "FlowControl", &tempSettings.flowControl}, {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "InvertCts", &tempSettings.invertCts}, {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "InvertRts", &tempSettings.invertRts}, + {'S', 0, 0, 10, 2000, 0, TYPE_U16, valInt, "SerialDelay", &tempSettings.serialTimeoutBeforeSendingFrame_ms}, {'S', 0, 0, 0, 0, 0, TYPE_SPEED_SERIAL, valSpeedSerial, "SerialSpeed", &tempSettings.serialSpeed}, {'S', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "UsbSerialWait", &tempSettings.usbSerialWait}, From cf5f7bf6f590f2e119a5f37f03e73ee1069382b6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 06:10:54 -1000 Subject: [PATCH 418/594] Move overheadTime from being a radio parameter to being a debug param --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 4ac6950f..eeeb144b 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1068,6 +1068,7 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &tempSettings.debugTransmit}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &tempSettings.debugSerial}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &tempSettings.displayRealMillis}, + {'D', 0, 1, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &tempSettings.overheadTime}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintAckNumbers", &tempSettings.printAckNumbers}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintChannel", &tempSettings.printChannel}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &tempSettings.printFrequency}, @@ -1106,7 +1107,6 @@ const COMMAND_ENTRY commands[] = {'R', 0, 0, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &tempSettings.maxResends}, {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "NetID", &tempSettings.netID}, {'R', 0, 1, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &tempSettings.operatingMode}, - {'R', 0, 1, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &tempSettings.overheadTime}, {'R', 0, 0, 0, 1, 0, TYPE_BOOL, valServer, "Server", &tempSettings.server}, {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &tempSettings.verifyRxNetID}, From 390c62bc9463e8c1fedb3ac4a61b51c3c03a046c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 06:03:32 -1000 Subject: [PATCH 419/594] Describe the steps to prevent attacks on the LoRaSerial network --- Firmware/LoRaSerial_Firmware/Radio.ino | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index e6df726b..543528fc 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1536,6 +1536,110 @@ bool xmitVcZeroAcks(int8_t destVc) //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Datagram reception +// +//Security or lack there of: +// +//The comments below outline the radio parameters and how they can be determined by +//listening to the radio traffic. The required parameters are followed by one or +//more step numbers indicating the step which determines the parameter's value. The +//comments further describe the steps that can be taken to reduce the attack surface +//for the LoRaSerial network. +// +//The following settings define the LoRaSerial radio network: +// +// AirSpeed - 3, 4, 5 +// AutoTune - This is receive only +// Bandwidth - 3 +// CodingRate - 5 +// FrequencyHop - 1 +// FrequencyMax - 1 +// FrequencyMin - 1 +// MaxDwellTime - 1 +// NumberOfChannels - 1 +// PreambleLength - 4 +// SpreadFactor - 4 +// SyncWord - 4 +// TxPower +// +// Protocol settings: +// +// DataScrambling - 6 +// EnableCRC16 - 8 +// EncryptData - 7 +// EncryptionKey - 7 +// FramesToYield +// HeartBeatTimeout - 12 +// MaxResends - 14 +// NetID - 9 +// OperatingMode - 10, 11 +// Server - 10, 11 +// VerifyRxNetID - 9 +// +//Attempting to attack a network of LoRaSerial radios would be done with the following +//steps: +// +// 1. Locate the frequencies that the network is using +// a. Listen for traffic on a specific channel to determine if the network is using +// that channel +// b. Repeat across all of the channels in the available frequency band +// 2. Once the frequencies are identified, attempt to determine the channel sequence +// by monitoring when traffic occurs on each of the identified frequencies, this +// allows the attacker to construct the hop table for the network +// 3. Look at the bandwidth utilized by the radio network. The signal for each +// symbol is a ramp that spans the bandwidth selected for use +// 4. Using a receiver that uses the opposite ramp to generate the sub frequencies +// within the bandwidth to decode the symbols. By monitoring the network traffic +// it is possible to determine the spreadfactor since there are 2^SF sub frequencies +// that will be used within the bandwidth +// 5. Now that the spread factor is known, the next step is to determine the coding +// rate used for forward error correction. Here 4 data bits are converted into +// between 5 to 8 bits in the transmitted frame +// 6. Look at the signal for multiple transmissions, does this signal have a DC offset? +// The data scrambling setting is false if a DC offset is present, or is true when +// no DC offset is present +// 7. Next step is breaking the encryption key +// 8. After the encryption key is obtained, it is possible determine if the link is +// using software CRC by computing the CRC-16 value on the first n-2 bytes and +// comparing that with the last 2 bytes +// 9. Determine if the link is using a network ID value, by checking the first byte +// in each of the transmitted frames +// 10. Determine if the virtual circuit header is contained in the transmitted frames +// 11. Determine which set of data frames are being transmitted +// 12. Determine the maximum interval between HEARTBEAT frames to roughly determine +// the HeartbeatTimeout value +// 13. Look for HEARTBEAT frames and FIND_PARTNER frames. A FIND_PARTNER frame +// is sent following a link failure which occurs after three HEARTBEAT timeout +// periods. +// 14. The MaxResends value can be estimated by the maximum number of data +// retransmissions done prior to the link failure. A large number of link +// failures will need to be collected from a very radio near the network. +// +//How do you prevent the attacks on a LoRaSerial radio network: +// +// 1. Don't advertize the network. Reduce the TxPower value to the minimum needed +// to successfully operate the network +// +// 2. Encrypt the data on the network. Don't use the factory default encryption key. +// Select a new encryption key and and train all of the radios in the network with +// this key. This will prevent attacks from any groups that are not able to break +// the encryption. However the radio network is vulnerable to well funded or +// dedicated groups that are able to break the encryption. +// +// 3. Change the encryption key. When another network is using the same encryption key +// and same network ID traffic from the other network is likely to be received. +// Selecting another encryption key will avoid this behavior. Also the encryption +// key may be tested by setting a radio to MODE_MULTIPOINT, disabling VerifyRxNetID +// and disabling EnableCRC16. Both settings of DataScrambling will need to be tested. +// +// 4. Use a network ID. This won't prevent attacks, but it prevents the reception +// of frames from other networks use the same encryption key. +// +// 5. Use software CRC-16. This won't prevent attacks but will eliminate most +// frames from networks using the same encryption key that don't use a network +// ID. Occasionally, when they get lucky, our network ID matches the first byte +// of their transmitted frame. The software CRC-16 will most likely cause this +// frame to be discarded. +// //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Determine the type of datagram received From 267b183304a61ff07bd52047fca525ad1a0f2bbd Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 09:55:40 -1000 Subject: [PATCH 420/594] Output the correct channelNumber LSB on port A1 in syncChannelTimer --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 543528fc..d2b8da17 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3349,7 +3349,7 @@ void syncChannelTimer(uint16_t frameAirTimeMsec) //Restart the channel timer timeToHop = false; channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's - digitalWrite(pin_hop_timer, channelNumber & 1); + digitalWrite(pin_hop_timer, ((channelNumber + delayedHopCount) % settings.numberOfChannels) & 1); channelTimerStart = currentMillis; channelTimerMsec = msToNextHop; //syncChannelTimer update channelTimer.enableTimer(); From 3ba72d3306d0b1bf4f9e6d056641097161477673 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 10:00:25 -1000 Subject: [PATCH 421/594] Display the ACK, HEARTBEAT and SYNC_CLOCKS transmit times with ATI10 --- Firmware/LoRaSerial_Firmware/Commands.ino | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index eeeb144b..2bd08c72 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -411,15 +411,14 @@ bool commandAT(const char * commandString) //Clock synchronization systemPrintln(" Clock Synchronization"); - systemPrint(" TX Time: "); - systemPrint(txTimeUsec / 1000., 4); - systemPrintln(" mSec"); - systemPrint(" RX Time: "); - systemPrint(rxTimeUsec / 1000., 4); - systemPrintln(" mSec"); - systemPrint(" Total Time: "); - systemPrint((txTimeUsec + rxTimeUsec) / 1000., 5); - systemPrintln(" mSec"); + systemPrint(" ACK Time: "); + systemPrint(txDataAckUsec); + systemPrint(" HEARTBEAT Time: "); + systemPrint(txHeartbeatUsec); + systemPrintln(" uSec"); + systemPrint(" SYNC_CLOCKS Time: "); + systemPrint(txSyncClockUsec); + systemPrintln(" uSec"); systemPrint(" Uptime: "); deltaMillis = millis(); systemPrintTimestamp(deltaMillis); From 4969ce6aad4f7d11950f3b97bda192153472b4bc Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 10:04:26 -1000 Subject: [PATCH 422/594] Update the VC HEARTBEAT diagram --- Firmware/LoRaSerial_Firmware/Radio.ino | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index d2b8da17..5819129d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1384,14 +1384,14 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) *txData = (uint8_t)(endOfTxData - txData); /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+----------+---------+----------+---------+----------+ - | Optional | | Optional | Optional | | | | | Optional | - | NET ID | Control | C-Timer | SF6 Length | DestAddr | SrcAddr | Src ID | millis | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes | n Bytes | - +----------+---------+----------+------------+----------+---------+----------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+----------+----------+---------+----------+---------+----------+ + | Optional | | Optional | Optional | | | | | | Optional | + | NET ID | Control | C-Timer | SF6 Length | Length | DestAddr | SrcAddr | Src ID | millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 8 bits | 8 bits | 8 bits | 16 Bytes | 4 Bytes | n Bytes | + +----------+---------+----------+------------+----------+----------+---------+----------+---------+----------+ */ //Verify the data length From 436d504d3cfee251990d2d66259e262db1904f9e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 10:12:22 -1000 Subject: [PATCH 423/594] Properly set the VC_HEARTBEAT_BYTES value --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index f1e2d81f..a7d2cda9 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -70,7 +70,7 @@ const int FIRMWARE_VERSION_MINOR = 0; #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame #define P2P_HEARTBEAT_BYTES sizeof(unsigned long) //Number of data bytes in the HEARTBEAT frame #define P2P_ACK_BYTES sizeof(unsigned long) //Number of data bytes in the ACK frame -#define VC_HEARTBEAT_BYTES 0 //Number of data bytes in the VC_HEARTBEAT frame +#define VC_HEARTBEAT_BYTES 1 + 1 + 1 + 16 + 4 //Number of data bytes in the VC_HEARTBEAT frame #define TX_TO_RX_USEC 680 //Time from TX to RX transactionComplete in microseconds From fb6f8fcd9d4c547eaedd0e6bc811b04db96df34b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 10:18:52 -1000 Subject: [PATCH 424/594] Add virtual-circuit triggers for TX and RX --- Firmware/LoRaSerial_Firmware/States.ino | 31 +++++++++++++++++++++---- Firmware/LoRaSerial_Firmware/settings.h | 10 ++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d425c548..cc663da3 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1867,6 +1867,8 @@ void updateRadioState() break; case DATAGRAM_DATA: + triggerEvent(TRIGGER_RX_DATA); + //Move the data into the serial output buffer if (settings.debugSerial) { @@ -1884,6 +1886,8 @@ void updateRadioState() break; case DATAGRAM_DATAGRAM: + triggerEvent(TRIGGER_RX_DATAGRAM); + //Move the data into the serial output buffer if (settings.debugSerial) { @@ -1899,6 +1903,7 @@ void updateRadioState() break; case DATAGRAM_DATA_ACK: + triggerEvent(TRIGGER_RX_ACK); vcSendPcAckNack(rexmtTxDestVc, true); STOP_ACK_TIMER(); break; @@ -1916,6 +1921,7 @@ void updateRadioState() //Second step in the 3-way handshake, received UNKNOWN_ACKS, respond //with SYNC_ACKS case DATAGRAM_VC_UNKNOWN_ACKS: + triggerEvent(TRIGGER_RX_VC_UNKNOWN_ACKS); if (xmitVcSyncAcks(rxSrcVc)) changeState(RADIO_VC_WAIT_TX_DONE); vcChangeState(rxSrcVc, VC_STATE_WAIT_ZERO_ACKS); @@ -1924,6 +1930,7 @@ void updateRadioState() //Third step in the 3-way handshake, received SYNC_ACKS, respond with ZERO_ACKS case DATAGRAM_VC_SYNC_ACKS: + triggerEvent(TRIGGER_RX_VC_SYNC_ACKS); if (xmitVcZeroAcks(rxSrcVc)) { changeState(RADIO_VC_WAIT_TX_DONE); @@ -1934,11 +1941,13 @@ void updateRadioState() //Last step in the 3-way handshake, received ZERO_ACKS, done case DATAGRAM_VC_ZERO_ACKS: + triggerEvent(TRIGGER_RX_VC_ZERO_ACKS); vcZeroAcks(rxSrcVc); vcChangeState(rxSrcVc, VC_STATE_CONNECTED); break; case DATAGRAM_REMOTE_COMMAND: + triggerEvent(TRIGGER_RX_COMMAND); rmtCmdVc = rxSrcVc; //Copy the command into the command buffer @@ -1984,6 +1993,8 @@ void updateRadioState() break; case DATAGRAM_REMOTE_COMMAND_RESPONSE: + triggerEvent(TRIGGER_RX_COMMAND_RESPONSE); + //Debug the serial path if (settings.debugSerial) { @@ -2022,7 +2033,7 @@ void updateRadioState() //Send another heartbeat if (xmitVcHeartbeat(myVc, myUniqueId)) { - triggerEvent(TRIGGER_TX_HEARTBEAT); + triggerEvent(TRIGGER_TX_VC_HEARTBEAT); if (((uint8_t)myVc) < MAX_VC) virtualCircuitList[myVc].lastTrafficMillis = currentMillis; changeState(RADIO_VC_WAIT_TX_DONE); @@ -2147,6 +2158,7 @@ void updateRadioState() //Send the UNKNOWN_ACKS datagram, first part of the 3-way handshake if (xmitVcUnknownAcks(index)) { + triggerEvent(TRIGGER_RX_VC_UNKNOWN_ACKS); vcChangeState(index, VC_STATE_WAIT_SYNC_ACKS); virtualCircuitList[index].timerMillis = datagramTimer; changeState(RADIO_VC_WAIT_TX_DONE); @@ -2163,6 +2175,7 @@ void updateRadioState() //Retransmit the UNKNOWN_ACKS if (xmitVcUnknownAcks(index)) { + triggerEvent(TRIGGER_RX_VC_UNKNOWN_ACKS); virtualCircuitList[index].timerMillis = datagramTimer; changeState(RADIO_VC_WAIT_TX_DONE); } @@ -2179,6 +2192,7 @@ void updateRadioState() //Retransmit the SYNC_CLOCKS if (xmitVcSyncAcks(index)) { + triggerEvent(TRIGGER_RX_VC_SYNC_ACKS); virtualCircuitList[index].timerMillis = datagramTimer; changeState(RADIO_VC_WAIT_TX_DONE); } @@ -2210,15 +2224,20 @@ void updateRadioState() if (vcHeader->destVc == VC_BROADCAST) { //Broadcast this data to all VCs, no ACKs will be received - triggerEvent(TRIGGER_TX_DATA); - xmitVcDatagram(); + if (xmitVcDatagram()) + { + triggerEvent(TRIGGER_TX_DATA); + changeState(RADIO_VC_WAIT_TX_DONE); + } break; } //Transmit the packet - triggerEvent(TRIGGER_TX_DATA); if (xmitDatagramP2PData() == true) + { + triggerEvent(TRIGGER_TX_DATA); changeState(RADIO_VC_WAIT_TX_DONE); + } START_ACK_TIMER(); @@ -2289,7 +2308,10 @@ void updateRadioState() //Send the remote command vcHeader->destVc &= VCAB_NUMBER_MASK; if (xmitDatagramP2PCommand() == true) + { + triggerEvent(TRIGGER_TX_COMMAND); changeState(RADIO_VC_WAIT_TX_DONE); + } START_ACK_TIMER(); @@ -3027,6 +3049,7 @@ void vcReceiveHeartbeat(uint32_t rxMillis) memcpy(×tampOffset, &rxVcData[UNIQUE_ID_BYTES], sizeof(timestampOffset)); timestampOffset += vcTxHeartbeatMillis + rxMillis - millis(); } + triggerEvent(TRIGGER_RX_VC_HEARTBEAT); //Save our address if ((myVc == VC_UNASSIGNED) && (memcmp(myUniqueId, rxVcData, UNIQUE_ID_BYTES) == 0)) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 64f80453..33e7257a 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -263,10 +263,15 @@ enum TRIGGER_RX_COMMAND, TRIGGER_RX_COMMAND_RESPONSE, TRIGGER_RX_DATA, + TRIGGER_RX_DATAGRAM, TRIGGER_RX_FIND_PARTNER, TRIGGER_RX_HEARTBEAT, TRIGGER_RX_SPI_DONE, TRIGGER_RX_SYNC_CLOCKS, + TRIGGER_RX_VC_HEARTBEAT, + TRIGGER_RX_VC_SYNC_ACKS, + TRIGGER_RX_VC_UNKNOWN_ACKS, + TRIGGER_RX_VC_ZERO_ACKS, TRIGGER_RX_YIELD, TRIGGER_RX_ZERO_ACKS, TRIGGER_SYNC_CHANNEL_TIMER, @@ -282,6 +287,7 @@ enum TRIGGER_TX_COMMAND, TRIGGER_TX_COMMAND_RESPONSE, TRIGGER_TX_DATA, + TRIGGER_TX_DATAGRAM, TRIGGER_TX_DONE, TRIGGER_TX_DUPLICATE_ACK, TRIGGER_TX_FIND_PARTNER, @@ -289,6 +295,10 @@ enum TRIGGER_TX_LOAD_CHANNE_TIMER_VALUE, TRIGGER_TX_SPI_DONE, TRIGGER_TX_SYNC_CLOCKS, + TRIGGER_TX_VC_HEARTBEAT, + TRIGGER_TX_VC_SYNC_ACKS, + TRIGGER_TX_VC_UNKNOWN_ACKS, + TRIGGER_TX_VC_ZERO_ACKS, TRIGGER_TX_YIELD, TRIGGER_TX_ZERO_ACKS, TRIGGER_UNKNOWN_PACKET, From 79da1cc80db3f5d8054ea90fde2c30618924b123 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 10:22:11 -1000 Subject: [PATCH 425/594] Virtual Circuit: Synchronize channel timer using the VC_HEARTBEAT frame --- Firmware/LoRaSerial_Firmware/States.ino | 70 ++++++++++++++++++------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index cc663da3..c3334279 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -187,36 +187,45 @@ void updateRadioState() //Start the link between the radios if (settings.operatingMode == MODE_POINT_TO_POINT) + { changeState(RADIO_P2P_LINK_DOWN); - else if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + break; + } + + //Virtual circuit mode + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { if (settings.server) { //Reserve the server's address (0) myVc = vcIdToAddressByte(VC_SERVER, myUniqueId); clockSyncReceiver = false; //VC server is clock source + if (settings.frequencyHop) + startChannelTimer(); + + //Start sending heartbeats + xmitVcHeartbeat(myVc, myUniqueId); + changeState(RADIO_VC_WAIT_TX_DONE); + break; } - else - //Unknown client address - myVc = VC_UNASSIGNED; + + //Unknown client address + myVc = VC_UNASSIGNED; //Start sending heartbeats - xmitVcHeartbeat(myVc, myUniqueId); - changeState(RADIO_VC_WAIT_TX_DONE); + changeState(RADIO_VC_WAIT_SERVER); + break; } - else + + //Multipoint mode + if (settings.server == true) { - if (settings.server == true) - { - clockSyncReceiver = false; //Multipoint server is clock source - startChannelTimer(); //Start hopping - multipoint clock source - changeState(RADIO_MP_STANDBY); - } - else - { - changeState(RADIO_DISCOVER_BEGIN); - } + clockSyncReceiver = false; //Multipoint server is clock source + startChannelTimer(); //Start hopping - multipoint clock source + changeState(RADIO_MP_STANDBY); } + else + changeState(RADIO_DISCOVER_BEGIN); break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -1780,12 +1789,21 @@ void updateRadioState() //Wait for a HEARTBEAT from the server //==================== case RADIO_VC_WAIT_SERVER: + //The server should never be in this state if (myVc == VC_SERVER) { changeState(RADIO_VC_WAIT_RECEIVE); break; } + //Stop the frequency hopping + stopChannelTimer(); + if (channelNumber != 0) + { + channelNumber = 0; + setRadioFrequency(false); + } + //If dio0ISR has fired, a packet has arrived currentMillis = millis(); if (transactionComplete == true) @@ -1796,6 +1814,11 @@ void updateRadioState() //Process the received datagram if ((packetType == DATAGRAM_VC_HEARTBEAT) && (rxSrcVc == VC_SERVER)) { + //Start the channel timer + startChannelTimer(); + channelTimerStart -= settings.maxDwellTime; + + //Synchronize the channel timer with the server vcReceiveHeartbeat(millis() - currentMillis); changeState(RADIO_VC_WAIT_RECEIVE); } @@ -1818,6 +1841,17 @@ void updateRadioState() { transactionComplete = false; + //Compute the HEARTBEAT frame transmit frame + if ((!txHeartbeatUsec) && (txControl.datagramType == DATAGRAM_HEARTBEAT)) + { + txHeartbeatUsec = transactionCompleteMicros - txSetChannelTimerMicros; + if (settings.debugSync) + { + systemPrint("txHeartbeatUsec: "); + systemPrintln(txHeartbeatUsec); + } + } + //Indicate that the transmission is complete triggerEvent(TRIGGER_TX_DONE); @@ -3034,7 +3068,7 @@ void vcReceiveHeartbeat(uint32_t rxMillis) uint32_t deltaMillis; int vcSrc; - if (rxSrcVc == VC_SERVER) + if ((rxSrcVc == VC_SERVER) || (memcmp(rxVcData, myUniqueId, sizeof(myUniqueId)) == 0)) syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on server's remaining clock //Update the timestamp offset From eb033a5eee89f4258a2b617f05de8726ddbde6c6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 8 Jan 2023 17:17:30 -1000 Subject: [PATCH 426/594] Add DATA_ACK, HEARTBEAT and SYNC_CLOCKS air time in mSec to SYNC_CLOCKS --- .../LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 25 +++++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index a7d2cda9..b3ab6c2e 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -66,7 +66,7 @@ const int FIRMWARE_VERSION_MINOR = 0; //Frame lengths #define MP_HEARTBEAT_BYTES 0 //Number of data bytes in the MP_HEARTBEAT frame #define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame -#define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame +#define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t)) //Number of data bytes in the SYNC_CLOCKS frame #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame #define P2P_HEARTBEAT_BYTES sizeof(unsigned long) //Number of data bytes in the HEARTBEAT frame #define P2P_ACK_BYTES sizeof(unsigned long) //Number of data bytes in the ACK frame diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 5819129d..541e4cce 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -889,15 +889,24 @@ bool xmitDatagramP2PSyncClocks() memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(unsigned long); + memcpy(endOfTxData, &txHeartbeatUsec, sizeof(txHeartbeatUsec)); + endOfTxData += sizeof(txHeartbeatUsec); + + memcpy(endOfTxData, &txSyncClockUsec, sizeof(txSyncClockUsec)); + endOfTxData += sizeof(txSyncClockUsec); + + memcpy(endOfTxData, &txDataAckUsec, sizeof(txDataAckUsec)); + endOfTxData += sizeof(txDataAckUsec); + /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+---------+---------+----------+ - | Optional | | Optional | Optional | Channel | | | - | NET ID | Control | C-Timer | SF6 Length | Number | Millis | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | 1 byte | 4 bytes | n Bytes | - +----------+---------+----------+------------+---------+---------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+---------+-------------+-------------+-----------+----------+ + | Optional | | Optional | Optional | Channel | | HEARTBEAT | SYNC_CLOCKS | DATA_ACK | | + | NET ID | Control | C-Timer | SF6 Length | Number | Millis | Micros | Micros | Micros | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 1 byte | 4 bytes | 4 Bytes | 4 Bytes | 4 Bytes | n Bytes | + +----------+---------+----------+------------+---------+---------+-------------+-------------+-----------+----------+ */ //Verify the data length From 2309b2bdb1a4e26f3873fbb24c1993765631f8ed Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 9 Jan 2023 11:19:37 -1000 Subject: [PATCH 427/594] Multipoint: Start the channelTimer synchronization --- Firmware/LoRaSerial_Firmware/States.ino | 109 ++++++++++++++++-------- 1 file changed, 72 insertions(+), 37 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c3334279..8a160faf 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1095,8 +1095,6 @@ void updateRadioState() //Start searching for other radios //==================== case RADIO_DISCOVER_BEGIN: - stopChannelTimer(); //Stop hopping - multipoint discovery - if (settings.debugSync) { systemPrintln("Start scanning"); @@ -1114,6 +1112,8 @@ void updateRadioState() //Walk through channel table backwards, transmitting a FIND_PARTNER and looking for an SYNC_CLOCKS //==================== case RADIO_DISCOVER_SCANNING: + stopChannelTimer(); //Stop hopping - multipoint discovery + if (transactionComplete) { //Decode the received datagram @@ -1157,31 +1157,65 @@ void updateRadioState() break; case DATAGRAM_SYNC_CLOCKS: - triggerEvent(TRIGGER_RX_SYNC_CLOCKS); + if (!settings.server) + { + //Change to the server's channel number + channelNumber = rxVcData[0]; - //Compute the common clock - currentMillis = millis(); - COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0, txTimeUsec + rxTimeUsec); + //Get the HEARTBEAT TX time + if (!txHeartbeatUsec) + { + memcpy(&txHeartbeatUsec, rxData + 1 + 4, sizeof(txHeartbeatUsec)); + if (settings.debugSync) + { + systemPrint("txHeartbeatUsec: "); + systemPrintln(txHeartbeatUsec); + } + } - //Server has responded with ACK - syncChannelTimer(txRxTimeMsec); //Start and adjust freq hop ISR based on remote's remaining clock + //Get the SYNC_CLOCKS TX time + if (!txSyncClockUsec) + { + memcpy(&txSyncClockUsec, rxData + 1 + 4 + 4, sizeof(txSyncClockUsec)); + if (settings.debugSync) + { + systemPrint("txSyncClockUsec: "); + systemPrintln(txSyncClockUsec); + } + } - //Change to the server's channel number - channelNumber = rxVcData[0]; + //Ignore this frame when the times are not filled in + if ((!txHeartbeatUsec) || (!txSyncClockUsec)) + triggerEvent(TRIGGER_RX_SYNC_CLOCKS); + else + { + COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0, txSyncClockUsec); - if (settings.debugSync) - { - systemPrint(" Channel Number: "); - systemPrintln(channelNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } + //Server has responded to FIND_PARTNER with SYNC_CLOCKS + //Start and adjust freq hop ISR based on remote's remaining clock + startChannelTimer(); + channelTimerStart -= settings.maxDwellTime; + syncChannelTimer((txSyncClockUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000); + triggerEvent(TRIGGER_RX_SYNC_CLOCKS); - frequencyCorrection += radio.getFrequencyError() / 1000000.0; + //Switch to the proper frequency for the channel + setRadioFrequency(false); - lastPacketReceived = millis(); //Reset - changeState(RADIO_MP_STANDBY); + if (settings.debugSync) + { + systemPrint(" Channel Number: "); + systemPrintln(channelNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } + + frequencyCorrection += radio.getFrequencyError() / 1000000.0; + + lastPacketReceived = millis(); //Reset + changeState(RADIO_MP_STANDBY); + } + } break; } } @@ -1319,12 +1353,19 @@ void updateRadioState() break; case DATAGRAM_HEARTBEAT: - //Received heartbeat - do not ack. - triggerEvent(TRIGGER_RX_HEARTBEAT); - //Sync clock if server sent the heartbeat if (settings.server == false) - syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on remote's remaining clock + { + uint16_t frameAirTimeMsec; + + //Adjust freq hop ISR based on server's remaining clock + syncChannelTimer((txHeartbeatUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000); + systemPrint("HEARTBEAT TX mSec: "); + systemPrintln(frameAirTime); + } + + //Received heartbeat - do not ack. + triggerEvent(TRIGGER_RX_HEARTBEAT); frequencyCorrection += radio.getFrequencyError() / 1000000.0; @@ -1338,10 +1379,6 @@ void updateRadioState() //Received data - do not ack. triggerEvent(TRIGGER_RX_DATA); - //Sync clock if server sent the datagram - if (settings.server == false) - syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on remote's remaining clock - setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer //Place any available data in the serial output buffer @@ -1362,16 +1399,14 @@ void updateRadioState() heartbeatTimeout = ((millis() - heartbeatTimer) > heartbeatRandomTime); //Only the server transmits heartbeats - if (settings.server == true) + if ((settings.server == true) && (heartbeatTimeout)) { - if (heartbeatTimeout) + if (xmitDatagramMpHeartbeat() == true) { - if (xmitDatagramMpHeartbeat() == true) - { - triggerEvent(TRIGGER_TX_HEARTBEAT); - setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer - changeState(RADIO_MP_WAIT_TX_DONE); //Wait for heartbeat to transmit - } + triggerEvent(TRIGGER_TX_HEARTBEAT); + blinkHeartbeatLed(true); + setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer + changeState(RADIO_MP_WAIT_TX_DONE); //Wait for heartbeat to transmit } } From 788e6530d31d7cf8487109b0a2f3b06a01398551 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 11 Jan 2023 12:14:23 -0700 Subject: [PATCH 428/594] Typo fix --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 541e4cce..d8253ca3 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2605,7 +2605,7 @@ bool transmitDatagram() hopChannel(); //Measure the time to the next hop - triggerEvent(TRIGGER_TX_LOAD_CHANNE_TIMER_VALUE); + triggerEvent(TRIGGER_TX_LOAD_CHANNEL_TIMER_VALUE); txSetChannelTimerMicros = micros(); unsigned long currentMillis = millis(); uint16_t msToNextHop; //TX channel timer value diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 33e7257a..a7f9eea7 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -292,7 +292,7 @@ enum TRIGGER_TX_DUPLICATE_ACK, TRIGGER_TX_FIND_PARTNER, TRIGGER_TX_HEARTBEAT, - TRIGGER_TX_LOAD_CHANNE_TIMER_VALUE, + TRIGGER_TX_LOAD_CHANNEL_TIMER_VALUE, TRIGGER_TX_SPI_DONE, TRIGGER_TX_SYNC_CLOCKS, TRIGGER_TX_VC_HEARTBEAT, From ad05fd7efff7340486b6f58724878dac55d53359 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 11 Jan 2023 13:22:49 -0700 Subject: [PATCH 429/594] Extend timeout during P2P linkup To facilitate much larger xmitDatagramP2PSyncClocks() response. --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 6 +++++- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b3ab6c2e..00b47422 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -378,6 +378,7 @@ uint16_t radioBand = 0; //In MHz. Detected radio module type. Sets the upper/low uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission uint16_t ackAirTime = 0; //Recalc'd with each change of settings +uint16_t systemDescriptionAirTime = 0; //Pre-calc'd amount of time for xmitDatagramP2PSyncClocks to get to receiver uint16_t maxPacketAirTime = 0; //Recalc'd with each change of settings uint8_t frameSentCount = 0; //Increases each time a frame is sent diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index d8253ca3..65d79fb4 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -64,6 +64,7 @@ bool configureRadio() //Precalculate the packet times ackAirTime = calcAirTimeMsec(headerBytes + CHANNEL_TIMER_BYTES + trailerBytes); //Used for response timeout during ACK + systemDescriptionAirTime = calcAirTimeMsec(headerBytes + P2P_SYNC_CLOCKS_BYTES + trailerBytes); //Used for response timeout during 3-way handshake maxPacketAirTime = calcAirTimeMsec(MAX_PACKET_SIZE); if ((settings.debug == true) || (settings.debugRadio == true)) @@ -3165,7 +3166,10 @@ void syncChannelTimer(uint16_t frameAirTimeMsec) memcpy(&channelTimer, data, sizeof(channelTimer)); data += sizeof(channelTimer); systemPrint(" Channel Timer(ms): "); - systemPrintln(channelTimer); + systemPrint(channelTimer); + systemPrint(" (0x"); + systemPrint(channelTimer, HEX); + systemPrintln(")"); systemPrint("ERROR: Invalid msToNextHopRemote value, "); systemPrintln(msToNextHopRemote); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 8a160faf..0f77ffdd 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -466,7 +466,7 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + if ((millis() - datagramTimer) >= (frameAirTime + systemDescriptionAirTime + settings.overheadTime + getReceiveCompletionOffset())) { if (settings.debugDatagrams) { From 729caa9e2c26b882297ac9e57cb67a5c7a85d725 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 13:40:06 -1000 Subject: [PATCH 430/594] Rename txSyncClockUsec to txSyncClocksUsec --- Firmware/LoRaSerial_Firmware/Commands.ino | 2 +- .../LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 4 +-- Firmware/LoRaSerial_Firmware/States.ino | 32 +++++++++---------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 2bd08c72..e0fd454b 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -417,7 +417,7 @@ bool commandAT(const char * commandString) systemPrint(txHeartbeatUsec); systemPrintln(" uSec"); systemPrint(" SYNC_CLOCKS Time: "); - systemPrint(txSyncClockUsec); + systemPrint(txSyncClocksUsec); systemPrintln(" uSec"); systemPrint(" Uptime: "); deltaMillis = millis(); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index b3ab6c2e..d1edb583 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -590,7 +590,7 @@ uint32_t txSetChannelTimerMicros; //Timestamp when millis is read in TX routine uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine uint32_t txDataAckUsec; //Time in microseconds to transmit the DATA_ACK frame uint32_t txHeartbeatUsec; //Time in microseconds to transmit the HEARTBEAT frame -uint32_t txSyncClockUsec; //Time in microseconds to transmit the SYNC_CLOCKS frame +uint32_t txSyncClocksUsec; //Time in microseconds to transmit the SYNC_CLOCKS frame uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram routine uint16_t maxFrameAirTime; //Air time of the maximum sized message unsigned long remoteSystemMillis; //Millis value contained in the received message diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 541e4cce..9be983c8 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -892,8 +892,8 @@ bool xmitDatagramP2PSyncClocks() memcpy(endOfTxData, &txHeartbeatUsec, sizeof(txHeartbeatUsec)); endOfTxData += sizeof(txHeartbeatUsec); - memcpy(endOfTxData, &txSyncClockUsec, sizeof(txSyncClockUsec)); - endOfTxData += sizeof(txSyncClockUsec); + memcpy(endOfTxData, &txSyncClocksUsec, sizeof(txSyncClocksUsec)); + endOfTxData += sizeof(txSyncClocksUsec); memcpy(endOfTxData, &txDataAckUsec, sizeof(txDataAckUsec)); endOfTxData += sizeof(txDataAckUsec); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 8a160faf..758c0aa6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -448,7 +448,7 @@ void updateRadioState() } //Compute the receive time - COMPUTE_RX_TIME(rxData + 1, 1, txSyncClockUsec); + COMPUTE_RX_TIME(rxData + 1, 1, txSyncClocksUsec); //Hop to the next channel hopChannel(); @@ -498,13 +498,13 @@ void updateRadioState() if (transactionComplete) { //Compute the SYNC_CLOCKS frame transmit frame - if ((!txSyncClockUsec) && (txControl.datagramType == DATAGRAM_SYNC_CLOCKS)) + if ((!txSyncClocksUsec) && (txControl.datagramType == DATAGRAM_SYNC_CLOCKS)) { - txSyncClockUsec = transactionCompleteMicros - txSetChannelTimerMicros; + txSyncClocksUsec = transactionCompleteMicros - txSetChannelTimerMicros; if (settings.debugSync) { - systemPrint("txSyncClockUsec: "); - systemPrintln(txSyncClockUsec); + systemPrint("txSyncClocksUsec: "); + systemPrintln(txSyncClocksUsec); } } @@ -1174,28 +1174,28 @@ void updateRadioState() } //Get the SYNC_CLOCKS TX time - if (!txSyncClockUsec) + if (!txSyncClocksUsec) { - memcpy(&txSyncClockUsec, rxData + 1 + 4 + 4, sizeof(txSyncClockUsec)); + memcpy(&txSyncClocksUsec, rxData + 1 + 4 + 4, sizeof(txSyncClocksUsec)); if (settings.debugSync) { - systemPrint("txSyncClockUsec: "); - systemPrintln(txSyncClockUsec); + systemPrint("txSyncClocksUsec: "); + systemPrintln(txSyncClocksUsec); } } //Ignore this frame when the times are not filled in - if ((!txHeartbeatUsec) || (!txSyncClockUsec)) + if ((!txHeartbeatUsec) || (!txSyncClocksUsec)) triggerEvent(TRIGGER_RX_SYNC_CLOCKS); else { - COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0, txSyncClockUsec); + COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0, txSyncClocksUsec); //Server has responded to FIND_PARTNER with SYNC_CLOCKS //Start and adjust freq hop ISR based on remote's remaining clock startChannelTimer(); channelTimerStart -= settings.maxDwellTime; - syncChannelTimer((txSyncClockUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000); + syncChannelTimer((txSyncClocksUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000); triggerEvent(TRIGGER_RX_SYNC_CLOCKS); //Switch to the proper frequency for the channel @@ -1463,13 +1463,13 @@ void updateRadioState() } //Compute the SYNC_CLOCKS frame transmit frame - else if ((!txSyncClockUsec) && (txControl.datagramType == DATAGRAM_SYNC_CLOCKS)) + else if ((!txSyncClocksUsec) && (txControl.datagramType == DATAGRAM_SYNC_CLOCKS)) { - txSyncClockUsec = transactionCompleteMicros - txSetChannelTimerMicros; + txSyncClocksUsec = transactionCompleteMicros - txSetChannelTimerMicros; if (settings.debugSync) { - systemPrint("txSyncClockUsec: "); - systemPrintln(txSyncClockUsec); + systemPrint("txSyncClocksUsec: "); + systemPrintln(txSyncClocksUsec); } } From 18bf578736d07a2ff06da9504e2569cda66868d2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 09:37:47 -1000 Subject: [PATCH 431/594] RX control: Add the ignoreFrame bit --- Firmware/LoRaSerial_Firmware/Radio.ino | 17 +++++++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 9be983c8..745d6098 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1932,6 +1932,20 @@ PacketType rcvDatagram() return (DATAGRAM_BAD); } + //Ignore this frame is requested + if (rxControl.ignoreFrame) + { + if (settings.debugReceive || settings.debugDatagrams) + { + systemPrintTimestamp(); + systemPrint("RX: Ignore this "); + systemPrintln(datagramType); + outputSerialData(true); + } + badFrames++; + return (DATAGRAM_BAD); + } + //Display the CRC if (settings.enableCRC16 && settings.debugReceive) { @@ -2940,6 +2954,9 @@ void printControl(uint8_t value) else systemPrintln("0"); + if (control->ignoreFrame) + systemPrintln(" Ignore Frame"); + outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 33e7257a..34eb4ef4 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -325,7 +325,7 @@ typedef struct _CONTROL_U8 PacketType datagramType: 4; uint8_t ackNumber : 2; uint8_t requestYield : 1; - uint8_t filler : 1; + uint8_t ignoreFrame : 1; } CONTROL_U8; typedef bool (* VALIDATION_ROUTINE)(void * value, uint32_t valMin, uint32_t valMax); From 72ced00d78453220dd7eb404a23f63dce2c37917 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 10:06:08 -1000 Subject: [PATCH 432/594] Transmit ignore frames to get TX times for SYNC_CLOCKS, HEARTBEAT, ACK --- Firmware/LoRaSerial_Firmware/Commands.ino | 4 + .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 79 +++++++++++++++++-- Firmware/LoRaSerial_Firmware/States.ino | 25 +++++- .../Virtual_Circuit_Protocol.h | 1 + 5 files changed, 103 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index e0fd454b..e50dde1a 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -413,6 +413,10 @@ bool commandAT(const char * commandString) systemPrintln(" Clock Synchronization"); systemPrint(" ACK Time: "); systemPrint(txDataAckUsec); + systemPrintln(" uSec"); + systemPrint(" FIND_PARTNER Time: "); + systemPrint(txFindPartnerUsec); + systemPrintln(" uSec"); systemPrint(" HEARTBEAT Time: "); systemPrint(txHeartbeatUsec); systemPrintln(" uSec"); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index d1edb583..40a52a50 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -589,6 +589,7 @@ uint16_t txRxTimeMsec; uint32_t txSetChannelTimerMicros; //Timestamp when millis is read in TX routine to set channel timer value uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine uint32_t txDataAckUsec; //Time in microseconds to transmit the DATA_ACK frame +uint32_t txFindPartnerUsec; //Time in microseconds to transmit the FIND_PARTNER frame uint32_t txHeartbeatUsec; //Time in microseconds to transmit the HEARTBEAT frame uint32_t txSyncClocksUsec; //Time in microseconds to transmit the SYNC_CLOCKS frame uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram routine diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 745d6098..a92f1105 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1365,6 +1365,11 @@ bool xmitVcDatagram() } //Broadcast a HEARTBEAT to all of the VCs +bool xmitVcHeartbeat() +{ + return xmitVcHeartbeat(VC_IGNORE_TX, myUniqueId); +} + bool xmitVcHeartbeat(int8_t addr, uint8_t * id) { uint32_t currentMillis = millis(); @@ -2947,15 +2952,17 @@ void printControl(uint8_t value) systemPrintln(control->datagramType); } - systemPrintTimestamp(); - systemPrint(" requestYield "); if (control->requestYield) - systemPrintln("1"); - else - systemPrintln("0"); + { + systemPrintTimestamp(); + systemPrintln(" requestYield"); + } if (control->ignoreFrame) + { + systemPrintTimestamp(); systemPrintln(" Ignore Frame"); + } outputSerialData(true); @@ -3103,6 +3110,68 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) return (true); //Transmission has started } +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//Transmit Ignored Frames +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +bool getTxTime(bool (*transmitFrame)(), uint32_t * txFrameUsec, const char * frameName) +{ + bool txStatus; + uint32_t transmitDelay; + +#define IGNORE_TRANSMIT_DELAY_MSEC 100 + + //Transmit the frame + transmitDelay = 0; + do + { + //Delay between retries + if (transmitDelay) + delay(transmitDelay); + transmitDelay += IGNORE_TRANSMIT_DELAY_MSEC; + + //Fail transmission after 5 attempts + if (transmitDelay > (5 * IGNORE_TRANSMIT_DELAY_MSEC)) + { + if (settings.debugSync) + { + systemPrintTimestamp(); + systemPrint("TX ignore "); + systemPrint(frameName); + systemPrintln(" failed!"); + outputSerialData(true); + } + return false; + } + + //Attempt to transmit the requested frame + txControl.ignoreFrame = true; + txStatus = transmitFrame(); + txControl.ignoreFrame = false; + } while (!txStatus); + + //Wait for transmit completion + while (!transactionComplete) + petWDT(); + transactionComplete = false; + + //Compute the transmit time + *txFrameUsec = transactionCompleteMicros - txSetChannelTimerMicros; + if (settings.debugSync) + { + systemPrintTimestamp(); + systemPrint("TX "); + systemPrint(frameName); + systemPrint(": "); + systemPrint(*txFrameUsec); + systemPrintln(" mSec"); + outputSerialData(true); + } + return true; +} + +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //Use the maximum dwell setting to start the timer that indicates when to hop channels void startChannelTimer() { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 758c0aa6..6495ae1c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -180,14 +180,20 @@ void updateRadioState() petWDT(); - returnToReceiving(); //Start receiving - //Stop the ACK timer STOP_ACK_TIMER(); //Start the link between the radios if (settings.operatingMode == MODE_POINT_TO_POINT) { + //Determine transmit frame times for SYNC_CLOCKS, ACK + getTxTime(xmitDatagramP2PFindPartner, &txFindPartnerUsec, "FIND_PARTNER"); + getTxTime(xmitDatagramP2PSyncClocks, &txSyncClocksUsec, "SYNC_CLOCKS"); + getTxTime(xmitDatagramP2PHeartbeat, &txHeartbeatUsec, "HEARTBEAT"); + getTxTime(xmitDatagramP2PAck, &txDataAckUsec, "ACK"); + + //Start receiving + returnToReceiving(); changeState(RADIO_P2P_LINK_DOWN); break; } @@ -195,6 +201,8 @@ void updateRadioState() //Virtual circuit mode if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) { + //Determine transmit frame time for HEARTBEAT + getTxTime(xmitVcHeartbeat, &txHeartbeatUsec, "HEARTBEAT"); if (settings.server) { //Reserve the server's address (0) @@ -218,13 +226,26 @@ void updateRadioState() } //Multipoint mode + //Determine transmit frame times for SYNC_CLOCKS, HEARTBEAT, ACK + getTxTime(xmitDatagramP2PSyncClocks, &txSyncClocksUsec, "SYNC_CLOCKS"); + getTxTime(xmitDatagramMpHeartbeat, &txHeartbeatUsec, "HEARTBEAT"); + getTxTime(xmitDatagramP2PAck, &txDataAckUsec, "ACK"); + + //Start receiving + returnToReceiving(); + if (settings.server == true) { clockSyncReceiver = false; //Multipoint server is clock source startChannelTimer(); //Start hopping - multipoint clock source + + //Start receiving + returnToReceiving(); changeState(RADIO_MP_STANDBY); } else + //Start receiving + returnToReceiving(); changeState(RADIO_DISCOVER_BEGIN); break; diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 89594c4d..aaa4000a 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -28,6 +28,7 @@ #define VC_BROADCAST ((int8_t)(VC_RSVD_SPECIAL_VCS | VCAB_NUMBER_MASK)) #define VC_COMMAND (VC_BROADCAST - 1) //Command input and command response #define VC_UNASSIGNED (VC_COMMAND - 1) +#define VC_IGNORE_TX (VC_UNASSIGNED - 1) //Source and destinations reserved for the local host #define PC_COMMAND VC_RSVD_SPECIAL_VCS //Command input and command response From 831fd2978f5c3a4c4f6319bd5b318aad3c94eb5f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 10 Jan 2023 16:21:35 -1000 Subject: [PATCH 433/594] VC: Fix computation for next HEARTBEAT interval --- .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/Radio.ino | 8 ++-- Firmware/LoRaSerial_Firmware/States.ino | 43 ++++++++++++------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 40a52a50..13782497 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -596,6 +596,8 @@ uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram uint16_t maxFrameAirTime; //Air time of the maximum sized message unsigned long remoteSystemMillis; //Millis value contained in the received message +#define VC_DELAY_HEARTBEAT_MSEC 5 + bool rxFirstAck; //Set true when first ACK is received bool txFirstAck; //Set true when first ACK is transmitted diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a92f1105..4308a979 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3555,7 +3555,7 @@ void setVcHeartbeatTimer() petWDT(); //Determine the delay before channel zero is reached - deltaMillis = mSecToChannelZero() - heartbeatTimer; + deltaMillis = mSecToChannelZero(); //Determine the delay before the next HEARTBEAT frame if ((!settings.server) || (deltaMillis > ((3 * settings.heartbeatTimeout) / 2)) @@ -3566,17 +3566,15 @@ void setVcHeartbeatTimer() else if (deltaMillis >= settings.heartbeatTimeout) heartbeatRandomTime = deltaMillis / 2; else - heartbeatRandomTime = deltaMillis; + heartbeatRandomTime = deltaMillis + VC_DELAY_HEARTBEAT_MSEC; //Display the next HEARTBEAT time interval if (settings.debugHeartbeat) { - systemPrint("deltaMillis: "); + systemPrint("mSecToChannelZero: "); systemPrintln(deltaMillis); systemPrint("heartbeatRandomTime: "); systemPrintln(heartbeatRandomTime); - outputSerialData(true); - petWDT(); } } diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 6495ae1c..0ff02200 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -210,18 +210,15 @@ void updateRadioState() clockSyncReceiver = false; //VC server is clock source if (settings.frequencyHop) startChannelTimer(); - - //Start sending heartbeats - xmitVcHeartbeat(myVc, myUniqueId); - changeState(RADIO_VC_WAIT_TX_DONE); - break; } + else + //Unknown client address + myVc = VC_UNASSIGNED; - //Unknown client address - myVc = VC_UNASSIGNED; - - //Start sending heartbeats - changeState(RADIO_VC_WAIT_SERVER); + //Server: Start sending HEARTBEAT + //Client: Determine HEARTBEAT transmit time + xmitVcHeartbeat(myVc, myUniqueId); + changeState(RADIO_VC_WAIT_TX_DONE); break; } @@ -1853,7 +1850,8 @@ void updateRadioState() } //Stop the frequency hopping - stopChannelTimer(); + if (channelTimerMsec) + stopChannelTimer(); if (channelNumber != 0) { channelNumber = 0; @@ -1870,12 +1868,17 @@ void updateRadioState() //Process the received datagram if ((packetType == DATAGRAM_VC_HEARTBEAT) && (rxSrcVc == VC_SERVER)) { + virtualCircuitList[VC_SERVER].lastTrafficMillis = currentMillis; + //Start the channel timer startChannelTimer(); - channelTimerStart -= settings.maxDwellTime; + channelTimerStart -= settings.maxDwellTime >> 1; //Synchronize the channel timer with the server vcReceiveHeartbeat(millis() - currentMillis); + + //Delay for a while before sending the HEARTBEAT + heartbeatRandomTime = random((settings.heartbeatTimeout * 2) / 10, settings.heartbeatTimeout); changeState(RADIO_VC_WAIT_RECEIVE); } else @@ -1898,7 +1901,7 @@ void updateRadioState() transactionComplete = false; //Compute the HEARTBEAT frame transmit frame - if ((!txHeartbeatUsec) && (txControl.datagramType == DATAGRAM_HEARTBEAT)) + if ((!txHeartbeatUsec) && (txControl.datagramType == DATAGRAM_VC_HEARTBEAT)) { txHeartbeatUsec = transactionCompleteMicros - txSetChannelTimerMicros; if (settings.debugSync) @@ -1915,7 +1918,10 @@ void updateRadioState() returnToReceiving(); //Set the next state - changeState(RADIO_VC_WAIT_RECEIVE); + if (virtualCircuitList[VC_SERVER].vcState == VC_STATE_LINK_DOWN) + changeState(RADIO_VC_WAIT_SERVER); + else + changeState(RADIO_VC_WAIT_RECEIVE); } break; @@ -2123,7 +2129,11 @@ void updateRadioState() //Send another heartbeat if (xmitVcHeartbeat(myVc, myUniqueId)) { + if (settings.server) + blinkHeartbeatLed(true); triggerEvent(TRIGGER_TX_VC_HEARTBEAT); + if (settings.debugHeartbeat && settings.server) + systemPrintln(channelNumber); if (((uint8_t)myVc) < MAX_VC) virtualCircuitList[myVc].lastTrafficMillis = currentMillis; changeState(RADIO_VC_WAIT_TX_DONE); @@ -3124,8 +3134,9 @@ void vcReceiveHeartbeat(uint32_t rxMillis) uint32_t deltaMillis; int vcSrc; + //Adjust freq hop ISR based on server's remaining clock if ((rxSrcVc == VC_SERVER) || (memcmp(rxVcData, myUniqueId, sizeof(myUniqueId)) == 0)) - syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on server's remaining clock + syncChannelTimer((txHeartbeatUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000); //Update the timestamp offset if (rxSrcVc == VC_SERVER) @@ -3137,7 +3148,7 @@ void vcReceiveHeartbeat(uint32_t rxMillis) //then the delay from reading the millisecond value on the server should //get offset by the transmit setup time and the receive overhead time. memcpy(×tampOffset, &rxVcData[UNIQUE_ID_BYTES], sizeof(timestampOffset)); - timestampOffset += vcTxHeartbeatMillis + rxMillis - millis(); + timestampOffset += (txHeartbeatUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000; } triggerEvent(TRIGGER_RX_VC_HEARTBEAT); From 3b9a998de36dd9fd1f3edd6ab6e9ab67ce2c4a8b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 09:01:13 -1000 Subject: [PATCH 434/594] VC: Fix the timestamp offset calculations --- Firmware/LoRaSerial_Firmware/States.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 0ff02200..21f84b1e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -3148,6 +3148,7 @@ void vcReceiveHeartbeat(uint32_t rxMillis) //then the delay from reading the millisecond value on the server should //get offset by the transmit setup time and the receive overhead time. memcpy(×tampOffset, &rxVcData[UNIQUE_ID_BYTES], sizeof(timestampOffset)); + timestampOffset -= millis(); timestampOffset += (txHeartbeatUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000; } triggerEvent(TRIGGER_RX_VC_HEARTBEAT); From b062e95e6ff88d80515fd90c0ea9512e78bff2ea Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 14:25:05 -1000 Subject: [PATCH 435/594] Pass microseconds to syncChannelTimer instead of milliseconds --- Firmware/LoRaSerial_Firmware/Radio.ino | 4 +++- Firmware/LoRaSerial_Firmware/States.ino | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4308a979..16b84c64 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3210,11 +3210,12 @@ void stopChannelTimer() //Given the remote unit's number of ms before its next hop, //adjust our own channelTimer interrupt to be synchronized with the remote unit -void syncChannelTimer(uint16_t frameAirTimeMsec) +void syncChannelTimer(uint32_t frameAirTimeUsec) { int16_t adjustment; unsigned long currentMillis; int8_t delayedHopCount; + uint16_t frameAirTimeMsec; int16_t lclHopTimeMsec; uint16_t msToNextHop; int16_t rmtHopTimeMsec; @@ -3293,6 +3294,7 @@ void syncChannelTimer(uint16_t frameAirTimeMsec) //Compute the remote system's channel timer firing time offset in milliseconds //using the channel timer value and the adjustments for transmit and receive //time (time of flight) + frameAirTimeMsec = (frameAirTimeUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000; rmtHopTimeMsec = msToNextHopRemote - frameAirTimeMsec; //Compute the when the local system last hopped diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 21f84b1e..b28a7b7e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -826,7 +826,7 @@ void updateRadioState() COMPUTE_RX_TIME(rxData, 1, txDataAckUsec); //The datagram we are expecting - syncChannelTimer(txRxTimeMsec); //Adjust freq hop ISR based on remote's remaining clock + syncChannelTimer(txDataAckUsec); //Adjust freq hop ISR based on remote's remaining clock triggerEvent(TRIGGER_RX_ACK); @@ -1213,7 +1213,7 @@ void updateRadioState() //Start and adjust freq hop ISR based on remote's remaining clock startChannelTimer(); channelTimerStart -= settings.maxDwellTime; - syncChannelTimer((txSyncClocksUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000); + syncChannelTimer(txSyncClocksUsec); triggerEvent(TRIGGER_RX_SYNC_CLOCKS); //Switch to the proper frequency for the channel @@ -1377,7 +1377,7 @@ void updateRadioState() uint16_t frameAirTimeMsec; //Adjust freq hop ISR based on server's remaining clock - syncChannelTimer((txHeartbeatUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000); + syncChannelTimer(txHeartbeatUsec); systemPrint("HEARTBEAT TX mSec: "); systemPrintln(frameAirTime); } @@ -3136,7 +3136,7 @@ void vcReceiveHeartbeat(uint32_t rxMillis) //Adjust freq hop ISR based on server's remaining clock if ((rxSrcVc == VC_SERVER) || (memcmp(rxVcData, myUniqueId, sizeof(myUniqueId)) == 0)) - syncChannelTimer((txHeartbeatUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000); + syncChannelTimer(txHeartbeatUsec); //Update the timestamp offset if (rxSrcVc == VC_SERVER) From 873fcf6d47013c16e2a13a89048a0315ef5b121b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 15:17:43 -1000 Subject: [PATCH 436/594] VC: Remove dead code --- .../LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/Radio.ino | 4 ---- Firmware/LoRaSerial_Firmware/States.ino | 14 +------------- 3 files changed, 1 insertion(+), 18 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 13782497..39206ee5 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -465,7 +465,6 @@ unsigned long linkDownTimer; unsigned long rcvTimeMillis; unsigned long xmitTimeMillis; long timestampOffset; -unsigned long vcTxHeartbeatMillis; //Transmit control uint8_t * endOfTxData; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 16b84c64..dcd02d8a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1419,10 +1419,6 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) txControl.datagramType = DATAGRAM_VC_HEARTBEAT; txControl.ackNumber = 0; - //Determine the time that it took to pass this frame to the radio - //This time is used to adjust the time offset - vcTxHeartbeatMillis = millis() - currentMillis; - //Select a random for the next heartbeat setVcHeartbeatTimer(); return (transmitDatagram()); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b28a7b7e..fe0f2b9c 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1898,20 +1898,8 @@ void updateRadioState() //If dio0ISR has fired, we are done transmitting if (transactionComplete == true) { - transactionComplete = false; - - //Compute the HEARTBEAT frame transmit frame - if ((!txHeartbeatUsec) && (txControl.datagramType == DATAGRAM_VC_HEARTBEAT)) - { - txHeartbeatUsec = transactionCompleteMicros - txSetChannelTimerMicros; - if (settings.debugSync) - { - systemPrint("txHeartbeatUsec: "); - systemPrintln(txHeartbeatUsec); - } - } - //Indicate that the transmission is complete + transactionComplete = false; triggerEvent(TRIGGER_TX_DONE); //Start the receive operation From 1a84fca2b0b0f3b88b8db549737bd7a32f10bf6a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 15:27:56 -1000 Subject: [PATCH 437/594] Don't reference currentMillis in COMPUTE_TIMESTAMP_OFFSET --- Firmware/LoRaSerial_Firmware/States.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index fe0f2b9c..26b2f6ac 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -51,8 +51,8 @@ #define COMPUTE_TIMESTAMP_OFFSET(millisBuffer, rShift, frameAirTimeUsec) \ { \ unsigned long deltaUsec = frameAirTimeUsec + micros() - transactionCompleteMicros; \ - memcpy(&remoteSystemMillis, millisBuffer, sizeof(currentMillis)); \ - timestampOffset = (remoteSystemMillis + (deltaUsec / 1000) - currentMillis); \ + memcpy(&remoteSystemMillis, millisBuffer, sizeof(remoteSystemMillis)); \ + timestampOffset = remoteSystemMillis + (deltaUsec / 1000) - millis(); \ timestampOffset >>= rShift; \ } From 72b44d98d7736b6164684785d29859070a606889 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 15:30:01 -1000 Subject: [PATCH 438/594] Multipoint: Use the measured TX times for channel timer synchronization --- Firmware/LoRaSerial_Firmware/States.ino | 98 +++++++------------------ 1 file changed, 27 insertions(+), 71 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 26b2f6ac..87a52128 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1130,6 +1130,12 @@ void updateRadioState() //Walk through channel table backwards, transmitting a FIND_PARTNER and looking for an SYNC_CLOCKS //==================== case RADIO_DISCOVER_SCANNING: + if (settings.server) + { + changeState(RADIO_MP_STANDBY); + break; + } + stopChannelTimer(); //Stop hopping - multipoint discovery if (transactionComplete) @@ -1180,59 +1186,32 @@ void updateRadioState() //Change to the server's channel number channelNumber = rxVcData[0]; - //Get the HEARTBEAT TX time - if (!txHeartbeatUsec) - { - memcpy(&txHeartbeatUsec, rxData + 1 + 4, sizeof(txHeartbeatUsec)); - if (settings.debugSync) - { - systemPrint("txHeartbeatUsec: "); - systemPrintln(txHeartbeatUsec); - } - } + //Update the timestamp + COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0, txSyncClocksUsec); - //Get the SYNC_CLOCKS TX time - if (!txSyncClocksUsec) - { - memcpy(&txSyncClocksUsec, rxData + 1 + 4 + 4, sizeof(txSyncClocksUsec)); - if (settings.debugSync) - { - systemPrint("txSyncClocksUsec: "); - systemPrintln(txSyncClocksUsec); - } - } - - //Ignore this frame when the times are not filled in - if ((!txHeartbeatUsec) || (!txSyncClocksUsec)) - triggerEvent(TRIGGER_RX_SYNC_CLOCKS); - else - { - COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0, txSyncClocksUsec); + //Server has responded to FIND_PARTNER with SYNC_CLOCKS + //Start and adjust freq hop ISR based on remote's remaining clock + startChannelTimer(); + channelTimerStart -= settings.maxDwellTime; + syncChannelTimer(txSyncClocksUsec); + triggerEvent(TRIGGER_RX_SYNC_CLOCKS); - //Server has responded to FIND_PARTNER with SYNC_CLOCKS - //Start and adjust freq hop ISR based on remote's remaining clock - startChannelTimer(); - channelTimerStart -= settings.maxDwellTime; - syncChannelTimer(txSyncClocksUsec); - triggerEvent(TRIGGER_RX_SYNC_CLOCKS); + //Switch to the proper frequency for the channel + setRadioFrequency(false); - //Switch to the proper frequency for the channel - setRadioFrequency(false); - - if (settings.debugSync) - { - systemPrint(" Channel Number: "); - systemPrintln(channelNumber); - outputSerialData(true); - if (timeToHop == true) //If the channelTimer has expired, move to next frequency - hopChannel(); - } + if (settings.debugSync) + { + systemPrint(" Channel Number: "); + systemPrintln(channelNumber); + outputSerialData(true); + if (timeToHop == true) //If the channelTimer has expired, move to next frequency + hopChannel(); + } - frequencyCorrection += radio.getFrequencyError() / 1000000.0; + frequencyCorrection += radio.getFrequencyError() / 1000000.0; - lastPacketReceived = millis(); //Reset - changeState(RADIO_MP_STANDBY); - } + lastPacketReceived = millis(); //Reset + changeState(RADIO_MP_STANDBY); } break; } @@ -1468,29 +1447,6 @@ void updateRadioState() if (transactionComplete == true) { transactionComplete = false; //Reset ISR flag - - //Compute the HEARTBEAT frame transmit frame - if ((!txHeartbeatUsec) && (txControl.datagramType == DATAGRAM_HEARTBEAT)) - { - txHeartbeatUsec = transactionCompleteMicros - txSetChannelTimerMicros; - if (settings.debugSync) - { - systemPrint("txHeartbeatUsec: "); - systemPrintln(txHeartbeatUsec); - } - } - - //Compute the SYNC_CLOCKS frame transmit frame - else if ((!txSyncClocksUsec) && (txControl.datagramType == DATAGRAM_SYNC_CLOCKS)) - { - txSyncClocksUsec = transactionCompleteMicros - txSetChannelTimerMicros; - if (settings.debugSync) - { - systemPrint("txSyncClocksUsec: "); - systemPrintln(txSyncClocksUsec); - } - } - returnToReceiving(); changeState(RADIO_MP_STANDBY); } From bdb3595d5dbdf4779a481c14a5c3804d784d17ac Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 16:10:36 -1000 Subject: [PATCH 439/594] Don't extend the SYNC_CLOCKS frame --- .../LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 25 ++++++------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 39206ee5..7e727b11 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -66,7 +66,7 @@ const int FIRMWARE_VERSION_MINOR = 0; //Frame lengths #define MP_HEARTBEAT_BYTES 0 //Number of data bytes in the MP_HEARTBEAT frame #define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame -#define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t)) //Number of data bytes in the SYNC_CLOCKS frame +#define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame #define P2P_HEARTBEAT_BYTES sizeof(unsigned long) //Number of data bytes in the HEARTBEAT frame #define P2P_ACK_BYTES sizeof(unsigned long) //Number of data bytes in the ACK frame diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index dcd02d8a..299e0043 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -889,24 +889,15 @@ bool xmitDatagramP2PSyncClocks() memcpy(endOfTxData, ¤tMillis, sizeof(currentMillis)); endOfTxData += sizeof(unsigned long); - memcpy(endOfTxData, &txHeartbeatUsec, sizeof(txHeartbeatUsec)); - endOfTxData += sizeof(txHeartbeatUsec); - - memcpy(endOfTxData, &txSyncClocksUsec, sizeof(txSyncClocksUsec)); - endOfTxData += sizeof(txSyncClocksUsec); - - memcpy(endOfTxData, &txDataAckUsec, sizeof(txDataAckUsec)); - endOfTxData += sizeof(txDataAckUsec); - /* - endOfTxData ---. - | - V - +----------+---------+----------+------------+---------+---------+-------------+-------------+-----------+----------+ - | Optional | | Optional | Optional | Channel | | HEARTBEAT | SYNC_CLOCKS | DATA_ACK | | - | NET ID | Control | C-Timer | SF6 Length | Number | Millis | Micros | Micros | Micros | Trailer | - | 8 bits | 8 bits | 2 bytes | 8 bits | 1 byte | 4 bytes | 4 Bytes | 4 Bytes | 4 Bytes | n Bytes | - +----------+---------+----------+------------+---------+---------+-------------+-------------+-----------+----------+ + endOfTxData ---. + | + V + +----------+---------+----------+------------+---------+---------+----------+ + | Optional | | Optional | Optional | Channel | | | + | NET ID | Control | C-Timer | SF6 Length | Number | Millis | Trailer | + | 8 bits | 8 bits | 2 bytes | 8 bits | 1 byte | 4 bytes | n Bytes | + +----------+---------+----------+------------+---------+---------+----------+ */ //Verify the data length From 74762b2b334646da0591263bfeca6d8cabb18b0a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 16:26:19 -1000 Subject: [PATCH 440/594] P2P: Fix error in link down/up synchronization --- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 87a52128..6a0ab983 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -722,7 +722,7 @@ void updateRadioState() } //Determine if an ACK was transmitted - if (txControl.datagramType = DATAGRAM_DATA_ACK) + if (txControl.datagramType == DATAGRAM_DATA_ACK) { COMPUTE_TX_TIME(); } From d5ab01bdf8494c9ae8c5ebe310a7886057c38a3f Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 12 Jan 2023 11:13:17 -0700 Subject: [PATCH 441/594] Fix buffer overrun. --- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 7a629cb3..790fbefa 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3433,6 +3433,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) clockSyncData[clockSyncIndex].lclHopTimeMsec = lclHopTimeMsec; clockSyncData[clockSyncIndex].timeToHop = timeToHop; clockSyncIndex += 1; + if(clockSyncIndex >= sizeof(clockSyncData)) clockSyncIndex = 0; //Restart the channel timer timeToHop = false; From 43c99dd583c1fc771849837c73b0e686af60597c Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 12 Jan 2023 12:09:47 -0700 Subject: [PATCH 442/594] Fix wrap around test --- Firmware/LoRaSerial_Firmware/Radio.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 790fbefa..1b3d6037 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3433,8 +3433,8 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) clockSyncData[clockSyncIndex].lclHopTimeMsec = lclHopTimeMsec; clockSyncData[clockSyncIndex].timeToHop = timeToHop; clockSyncIndex += 1; - if(clockSyncIndex >= sizeof(clockSyncData)) clockSyncIndex = 0; - + if(clockSyncIndex >= (sizeof(clockSyncData) / sizeof(CLOCK_SYNC_DATA)) ) clockSyncIndex = 0; + //Restart the channel timer timeToHop = false; channelTimer.setInterval_MS(msToNextHop, channelTimerHandler); //Adjust our hardware timer to match our mate's From ffe41dbbb907eb04e9d9644cf73769ef170f4bd8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Thu, 12 Jan 2023 09:31:06 -1000 Subject: [PATCH 443/594] Read the IRQ flags from the SX1276 when transactionComplete is set Display the last value with the ATI10 command Work-in-progress: Searching for cause of hang with receiveInProcess set --- Firmware/LoRaSerial_Firmware/Commands.ino | 19 +++++++++++++++++++ .../LoRaSerial_Firmware.ino | 1 + Firmware/LoRaSerial_Firmware/Radio.ino | 3 +++ Firmware/LoRaSerial_Firmware/States.ino | 10 ++++++++++ 4 files changed, 33 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index e50dde1a..7e549c7d 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -578,6 +578,25 @@ bool commandAT(const char * commandString) systemPrint(" lastModemStatus: "); systemPrint(lastModemStatus, HEX); systemPrintln(); + systemPrint(" irqFlags: 0x"); + systemPrint(irqFlags, HEX); + systemPrintln(); + if (irqFlags & 0x80) + systemPrintln(" RX Timeout"); + if (irqFlags & 0x40) + systemPrintln(" RX Done"); + if (irqFlags & 0x20) + systemPrintln(" Payload CRC Error"); + if (irqFlags & 0x10) + systemPrintln(" Valid Header"); + if (irqFlags & 8) + systemPrintln(" TX Done"); + if (irqFlags & 4) + systemPrintln(" CAD Done"); + if (irqFlags & 2) + systemPrintln(" FHSS Change Channel"); + if (irqFlags & 1) + systemPrintln(" CAD Detected"); systemPrint(" receiveInProcess: "); systemPrintln(receiveInProcess() ? "True" : "False"); outputSerialData(true); diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 3cb31840..89a09406 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -385,6 +385,7 @@ uint8_t frameSentCount = 0; //Increases each time a frame is sent unsigned long lastPacketReceived = 0; //Controls link LED in broadcast mode unsigned long lastLinkBlink = 0; //Controls link LED in broadcast mode +volatile uint16_t irqFlags; //IRQ Flags register value volatile bool transactionComplete = false; //Used in dio0ISR uint8_t sf6ExpectedSize = MAX_PACKET_SIZE; //Used during SF6 operation to reduce packet size when needed diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 790fbefa..d0d5abc1 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1662,6 +1662,9 @@ PacketType rcvDatagram() //Save the receive time rcvTimeMillis = millis(); + //Get the IRQ flags + irqFlags = radio.getIRQFlags(); + //Get the received datagram framesReceived++; int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index a90ec492..aed1816f 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -401,6 +401,7 @@ void updateRadioState() COMPUTE_TX_TIME(); triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag + irqFlags = radio.getIRQFlags(); startChannelTimerPending = true; //Starts at RX of SYNC_CLOCKS frame returnToReceiving(); changeState(RADIO_P2P_WAIT_SYNC_CLOCKS); @@ -529,6 +530,7 @@ void updateRadioState() COMPUTE_TX_TIME(); triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag + irqFlags = radio.getIRQFlags(); //Hop to the next channel hopChannel(); @@ -617,6 +619,7 @@ void updateRadioState() { transactionComplete = false; //Reset ISR flag COMPUTE_TX_TIME(); + irqFlags = radio.getIRQFlags(); setHeartbeatShort(); //We sent the last ack so be responsible for sending the next heartbeat @@ -727,6 +730,7 @@ void updateRadioState() COMPUTE_TX_TIME(); } triggerEvent(TRIGGER_TX_DONE); + irqFlags = radio.getIRQFlags(); //Determine the next packet size for SF6 if (ackTimer) @@ -1267,6 +1271,7 @@ void updateRadioState() if (transactionComplete) { transactionComplete = false; //Reset ISR flag + irqFlags = radio.getIRQFlags(); returnToReceiving(); changeState(RADIO_DISCOVER_SCANNING); } @@ -1447,6 +1452,7 @@ void updateRadioState() if (transactionComplete == true) { transactionComplete = false; //Reset ISR flag + irqFlags = radio.getIRQFlags(); returnToReceiving(); changeState(RADIO_MP_STANDBY); } @@ -1498,6 +1504,7 @@ void updateRadioState() //Indicate that the receive is complete triggerEvent(TRIGGER_TRAINING_CLIENT_TX_FIND_PARTNER_DONE); + irqFlags = radio.getIRQFlags(); //Start the receive operation returnToReceiving(); @@ -1592,6 +1599,7 @@ void updateRadioState() if (transactionComplete == true) { transactionComplete = false; + irqFlags = radio.getIRQFlags(); endClientServerTraining(TRIGGER_TRAINING_CLIENT_TX_ACK_DONE); } break; @@ -1712,6 +1720,7 @@ void updateRadioState() //Indicate that the receive is complete triggerEvent(TRIGGER_TRAINING_SERVER_TX_PARAMS_DONE); + irqFlags = radio.getIRQFlags(); //Start the receive operation returnToReceiving(); @@ -1857,6 +1866,7 @@ void updateRadioState() //Indicate that the transmission is complete transactionComplete = false; triggerEvent(TRIGGER_TX_DONE); + irqFlags = radio.getIRQFlags(); //Start the receive operation returnToReceiving(); From 555475166a995cdee894ca112e3fff261c3173ad Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 13 Jan 2023 09:00:24 -1000 Subject: [PATCH 444/594] VcServerTest: Turn off the debugging and ACKs --- Firmware/Tools/VcServerTest.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 820d7dd1..cce170cc 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -14,8 +14,8 @@ #define DEBUG_LOCAL_COMMANDS 0 #define DEBUG_PC_TO_RADIO 0 -#define DEBUG_RADIO_TO_PC 1 -#define DISPLAY_DATA_ACK 1 +#define DEBUG_RADIO_TO_PC 0 +#define DISPLAY_DATA_ACK 0 #define DISPLAY_DATA_NACK 1 #define DISPLAY_VC_STATE 0 #define SEND_ATC_COMMAND 1 From 72d1ae1605feca04fed67e01d51aa46348b93695 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 13 Jan 2023 09:16:54 -1000 Subject: [PATCH 445/594] VC: Add VC LEDs pattern Flash RX data LED (GREEN_LED_1) when receiving radio data Flash GREEN_LED_2 when receiving serial data Flash RSSI LED (GREEN_LED_3) when waiting for server HEARTBEAT Flash TX data LED (GREEN_LED_4) when transmitting radio data Flash yellow LED on hop (channel timer interrupt) Flash blue LED on server when transmitting HEARTBEAT, on client when receiving HEARTBEAT --- Firmware/LoRaSerial_Firmware/System.ino | 105 +++++++++++++++++++++--- Firmware/LoRaSerial_Firmware/settings.h | 37 +++++---- 2 files changed, 117 insertions(+), 25 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 9e2de995..b1f0b256 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -647,6 +647,7 @@ void blinkRadioRssiLed() //Update the single RSSI LED case LEDS_MULTIPOINT: case LEDS_RADIO_USE: + case LEDS_VC: //Check for the start of a new pulse if ((currentMillis - ledPreviousRssiMillis) >= LED_MAX_PULSE_WIDTH) { @@ -708,6 +709,13 @@ void blinkSerialRxLed(bool illuminate) else digitalWrite(pin_rxLED, LOW); break; + + case LEDS_VC: + if (illuminate == true) + digitalWrite(RADIO_USE_LINK_LED, HIGH); + else + digitalWrite(RADIO_USE_LINK_LED, LOW); + break; } } @@ -725,6 +733,7 @@ void blinkRadioRxLed(bool on) case LEDS_MULTIPOINT: case LEDS_RADIO_USE: + case LEDS_VC: if (on) digitalWrite(RADIO_USE_RX_DATA_LED, LED_ON); else if ((millis() - linkDownTimer) >= RADIO_USE_BLINK_MILLIS) @@ -747,6 +756,7 @@ void blinkRadioTxLed(bool on) case LEDS_MULTIPOINT: case LEDS_RADIO_USE: + case LEDS_VC: if (on) digitalWrite(RADIO_USE_TX_DATA_LED, LED_ON); else if ((millis() - datagramTimer) >= RADIO_USE_BLINK_MILLIS) @@ -824,6 +834,7 @@ void blinkHeartbeatLed(bool on) switch (settings.selectLedUse) { case LEDS_MULTIPOINT: + case LEDS_VC: if (on) { digitalWrite(LED_MP_HEARTBEAT, LED_ON); @@ -841,6 +852,7 @@ void blinkChannelHopLed(bool on) switch (settings.selectLedUse) { case LEDS_MULTIPOINT: + case LEDS_VC: if (on) digitalWrite(LED_MP_HOP_CHANNEL, LED_ON); else if ((millis() - radioCallHistory[RADIO_CALL_hopChannel]) >= RADIO_USE_BLINK_MILLIS) @@ -852,8 +864,6 @@ void blinkChannelHopLed(bool on) //Display the multi-point LED pattern void multiPointLeds() { - uint32_t currentMillis; - //Turn off the RX LED to end the blink blinkRadioRxLed(false); @@ -874,6 +884,49 @@ void multiPointLeds() blinkHeartbeatLed(false); } +//Display the VC LED pattern +void vcLeds() +{ + uint32_t currentMillis; + static uint32_t blinkSyncMillis; + + //Turn off the RX LED to end the blink + blinkRadioRxLed(false); + + //Turn off the TX LED to end the blink + blinkRadioTxLed(false); + + //Pulse width modulate the RSSI LED (GREEN_LED_3) + currentMillis = millis(); + if (virtualCircuitList[VC_SERVER].vcState) + blinkRadioRssiLed(); + +#define VC_SYNC_BLINK_RATE (1000 / 4) + + //Turn on the RSSI LED + else if (((currentMillis - blinkSyncMillis) >= (VC_SYNC_BLINK_RATE >> 1)) + && (digitalRead(RADIO_USE_RSSI_LED) == LED_OFF)) + digitalWrite(RADIO_USE_RSSI_LED, LED_ON); + + //Turn off the RSSI LED + else if ((!virtualCircuitList[VC_SERVER].vcState) + && (((currentMillis - blinkSyncMillis) >= VC_SYNC_BLINK_RATE)) + && (digitalRead(RADIO_USE_RSSI_LED) == LED_ON)) + { + digitalWrite(RADIO_USE_RSSI_LED, LED_OFF); + blinkSyncMillis = currentMillis; + } + + //Serial RX displayed on the LINK LED (GREEN_LED_2) by blinkSerialRxLed + + //Update the hop LED + if ((millis() - radioCallHistory[RADIO_CALL_hopChannel]) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(LED_MP_HOP_CHANNEL, LED_OFF); + + //Update the HEARTBEAT LED + blinkHeartbeatLed(false); +} + //Update the cylon LEDs void updateCylonLEDs() { @@ -911,29 +964,41 @@ void updateLeds() { //Set LEDs according to RSSI level default: - case LEDS_RSSI: - blinkRadioRssiLed(); + //Display the multipoint LED pattern + case LEDS_MULTIPOINT: + multiPointLeds(); break; - case LEDS_RADIO_USE: - radioLeds(); + //Display the point-to-point LED pattern + case LEDS_P2P: + blinkRadioRssiLed(); break; //Display the multipoint LED pattern - case LEDS_MULTIPOINT: - multiPointLeds(); + case LEDS_VC: + vcLeds(); break; - //Display the cylon pattern during training + //Display the cylon LED pattern case LEDS_CYLON: updateCylonLEDs(); break; + //Display the RSSI LED pattern + case LEDS_RSSI: + blinkRadioRssiLed(); + break; + + //Display the radio use LED pattern + case LEDS_RADIO_USE: + radioLeds(); + break; + //Turn off all the LEDs case LEDS_ALL_OFF: break; - //Turn on the blue LED for testing and to identify this radio + //Turn on the blue LED for testing case LEDS_BLUE_ON: digitalWrite(BLUE_LED, LED_ON); break; @@ -952,6 +1017,26 @@ void updateLeds() case LEDS_GREEN_2_ON: digitalWrite(GREEN_LED_2, LED_ON); break; + + //Turn on the green 3 LED for testing + case LEDS_GREEN_3_ON: + digitalWrite(GREEN_LED_3, LED_ON); + break; + + //Turn on the green 4 LED for testing + case LEDS_GREEN_4_ON: + digitalWrite(GREEN_LED_4, LED_ON); + break; + + //Turn on all the LEDs for testing + case LEDS_ALL_ON: + digitalWrite(GREEN_LED_1, LED_ON); + digitalWrite(GREEN_LED_2, LED_ON); + digitalWrite(GREEN_LED_3, LED_ON); + digitalWrite(GREEN_LED_4, LED_ON); + digitalWrite(BLUE_LED, LED_ON); + digitalWrite(YELLOW_LED, LED_ON); + break; } } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index e3cb26d9..52225e10 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -346,20 +346,27 @@ typedef struct _COMMAND_ENTRY typedef enum { - LEDS_RSSI = 0, //Green: RSSI, Blue: Serial TX, Yellow: Serial RX - LEDS_RADIO_USE, //Green1: RX, Green2: Link, Green3: RSSI, Green4: TX - //Blue: Bad frames, Yellow: Bad CRC - LEDS_MULTIPOINT, //Green1: RX, Green2: Sync, Green3: RSSI, Green4: TX - //Blue: Hop, Yellow: HEARTBEAT RX/TX - LEDS_ALL_OFF, //All LEDs off - LEDS_BLUE_ON, //Blue: ON, other: OFF - LEDS_YELLOW_ON, //Yellow: ON, other: OFF - LEDS_GREEN_1_ON, //Green 1: ON, other: OFF - LEDS_GREEN_2_ON, //Green 2: ON, other: OFF - LEDS_GREEN_3_ON, //Green 3: ON, other: OFF - LEDS_GREEN_4_ON, //Green 4: ON, other: OFF - - LEDS_CYLON, //Display the cylon pattern on the green LEDs, others off + LEDS_MULTIPOINT = 0, // 0: Green1: RX, Green2: Sync, Green3: RSSI, Green4: TX + // Blue: Hop, Yellow: HEARTBEAT RX/TX + LEDS_P2P, // 1: Green: RSSI, Blue: Serial TX, Yellow: Serial RX + LEDS_VC, // 2; Green1: RX, Green2: Sync, Green3: RSSI, Green4: TX + // Blue: Hop, Yellow: HEARTBEAT RX/TX + LEDS_RADIO_USE, // 3: Green1: RX, Green2: Link, Green3: RSSI, Green4: TX + // Blue: Bad frames, Yellow: Bad CRC + LEDS_RSSI, // 4: Green: RSSI, Blue: Serial TX, Yellow: Serial RX + LEDS_RESERVED_1, // 5 + LEDS_RESERVED_2, // 6 + LEDS_CYLON, // 7: Display the cylon pattern on the green LEDs, others off + + //Testing + LEDS_ALL_OFF, // 8: All LEDs off + LEDS_BLUE_ON, // 9: Blue: ON, other: OFF + LEDS_YELLOW_ON, //10; Yellow: ON, other: OFF + LEDS_GREEN_1_ON, //11; Green 1: ON, other: OFF + LEDS_GREEN_2_ON, //12; Green 2: ON, other: OFF + LEDS_GREEN_3_ON, //13; Green 3: ON, other: OFF + LEDS_GREEN_4_ON, //14: Green 4: ON, other: OFF + LEDS_ALL_ON, //15: All LEDs on //Add user LED types from 255 working down } LEDS_USE_TYPE; @@ -445,7 +452,7 @@ typedef struct struct_settings { bool printLinkUpDown = false; //Print the link up and link down messages bool invertCts = false; //Invert the input of CTS bool invertRts = false; //Invert the output of RTS - uint8_t selectLedUse = 0; //Select LED use + uint8_t selectLedUse = LEDS_RSSI; //Select LED use uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training bool debugSerial = false; //Debug the serial input bool debugSync = false; //Print clock sync processing From 65d6d0b541e78d8a1a9f029e2beab4fea2ffbf56 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 13 Jan 2023 10:57:58 -1000 Subject: [PATCH 446/594] P2P: Add point-to-point LED pattern --- Firmware/LoRaSerial_Firmware/System.ino | 38 +++++++++++++++++++++---- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index b1f0b256..11b02657 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -646,6 +646,7 @@ void blinkRadioRssiLed() //Update the single RSSI LED case LEDS_MULTIPOINT: + case LEDS_P2P: case LEDS_RADIO_USE: case LEDS_VC: //Check for the start of a new pulse @@ -732,6 +733,7 @@ void blinkRadioRxLed(bool on) break; case LEDS_MULTIPOINT: + case LEDS_P2P: case LEDS_RADIO_USE: case LEDS_VC: if (on) @@ -755,6 +757,7 @@ void blinkRadioTxLed(bool on) break; case LEDS_MULTIPOINT: + case LEDS_P2P: case LEDS_RADIO_USE: case LEDS_VC: if (on) @@ -834,6 +837,7 @@ void blinkHeartbeatLed(bool on) switch (settings.selectLedUse) { case LEDS_MULTIPOINT: + case LEDS_P2P: case LEDS_VC: if (on) { @@ -852,6 +856,7 @@ void blinkChannelHopLed(bool on) switch (settings.selectLedUse) { case LEDS_MULTIPOINT: + case LEDS_P2P: case LEDS_VC: if (on) digitalWrite(LED_MP_HOP_CHANNEL, LED_ON); @@ -884,6 +889,27 @@ void multiPointLeds() blinkHeartbeatLed(false); } +void p2pLeds() +{ + //Turn off the RX LED to end the blink + blinkRadioRxLed(false); + + //Turn off the TX LED to end the blink + blinkRadioTxLed(false); + + //Pulse width modulate the RSSI LED (GREEN_LED_3) + blinkRadioRssiLed(); + + //Leave the LINK LED (GREEN_LED_2) off + + //Update the hop LED + if ((millis() - radioCallHistory[RADIO_CALL_hopChannel]) >= RADIO_USE_BLINK_MILLIS) + digitalWrite(LED_MP_HOP_CHANNEL, LED_OFF); + + //Update the HEARTBEAT LED + blinkHeartbeatLed(false); +} + //Display the VC LED pattern void vcLeds() { @@ -964,6 +990,11 @@ void updateLeds() { //Set LEDs according to RSSI level default: + //Display the RSSI LED pattern + case LEDS_RSSI: + blinkRadioRssiLed(); + break; + //Display the multipoint LED pattern case LEDS_MULTIPOINT: multiPointLeds(); @@ -971,7 +1002,7 @@ void updateLeds() //Display the point-to-point LED pattern case LEDS_P2P: - blinkRadioRssiLed(); + p2pLeds(); break; //Display the multipoint LED pattern @@ -984,11 +1015,6 @@ void updateLeds() updateCylonLEDs(); break; - //Display the RSSI LED pattern - case LEDS_RSSI: - blinkRadioRssiLed(); - break; - //Display the radio use LED pattern case LEDS_RADIO_USE: radioLeds(); From 3044bc030714a8310f4b000467c067c49199f2d2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 13 Jan 2023 09:28:15 -1000 Subject: [PATCH 447/594] Bug Fix: memory corruption, clockSyncIndex got larger than array size --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index d0d5abc1..a26a1e49 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3436,7 +3436,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) clockSyncData[clockSyncIndex].lclHopTimeMsec = lclHopTimeMsec; clockSyncData[clockSyncIndex].timeToHop = timeToHop; clockSyncIndex += 1; - if(clockSyncIndex >= sizeof(clockSyncData)) clockSyncIndex = 0; + clockSyncIndex %= sizeof(clockSyncData) / sizeof(clockSyncData[0]); //Restart the channel timer timeToHop = false; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index aed1816f..b656a2bc 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2754,7 +2754,7 @@ void dumpClockSynchronization() petWDT(); for (uint8_t x = 0; x < (sizeof(clockSyncData) / sizeof(clockSyncData[0])); x++) { - uint8_t index = (x + clockSyncIndex) % clockSyncIndex; + uint8_t index = (x + clockSyncIndex) % (sizeof(clockSyncData) / sizeof(clockSyncData[0])); if (clockSyncData[index].frameAirTimeMsec) { systemPrint("Lcl: "); From ab96b00edf20c5ea697c91625f21736dd3118c31 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 13 Jan 2023 09:32:03 -1000 Subject: [PATCH 448/594] Display the case number in syncChannelTimer when debugSync is enabled --- Firmware/LoRaSerial_Firmware/Radio.ino | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index a26a1e49..d57ab1b5 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3204,6 +3204,7 @@ void stopChannelTimer() void syncChannelTimer(uint32_t frameAirTimeUsec) { int16_t adjustment; + uint8_t caseNumber; unsigned long currentMillis; int8_t delayedHopCount; uint16_t frameAirTimeMsec; @@ -3307,6 +3308,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) //Determine if the local system has just hopped if (((unsigned long)lclHopTimeMsec) <= CHANNEL_TIMER_SYNC_MSEC) { + caseNumber = 1; //Case 1 above, both systems using the same channel // //Remote system @@ -3334,6 +3336,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) } else { + caseNumber = 2; //Case 2 above, the local system did not hop // //Remote system @@ -3368,6 +3371,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) //Determine if the local system has just hopped if (((unsigned long)lclHopTimeMsec) <= CHANNEL_TIMER_SYNC_MSEC) { + caseNumber = 3; //Case 3 above, extend the channel timer value // // channelTimerStart Channel timer fires @@ -3396,6 +3400,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) } else { + caseNumber = 4; //Case 1 above, both systems using the same channel // // channelTimerStart Channel timer fires @@ -3460,6 +3465,9 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) //Display the channel sync timer calculations if (settings.debugSync) { + systemPrint("Case #"); + systemPrint(caseNumber); + systemPrint(", "); systemPrint(delayedHopCount); systemPrint(" Hops, "); systemPrint(msToNextHopRemote); From 6161f0380422ea9cc32b2b3638410cfba7446540 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 13 Jan 2023 09:45:44 -1000 Subject: [PATCH 449/594] Remove "case 3" from syncChannelTimer --- Firmware/LoRaSerial_Firmware/Radio.ino | 80 ++++++++------------------ 1 file changed, 23 insertions(+), 57 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index d57ab1b5..745b145d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3367,63 +3367,29 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) } else { - //The remote system has not hopped - //Determine if the local system has just hopped - if (((unsigned long)lclHopTimeMsec) <= CHANNEL_TIMER_SYNC_MSEC) - { - caseNumber = 3; - //Case 3 above, extend the channel timer value - // - // channelTimerStart Channel timer fires - // |------...-------------------------------------------->| - // Channel Timer value | - // |<--------------------------------->| - // | rmtHopTimeMsec | - // | |---->| - // | | - // | Current Time - // |<------------>|<------------>| - // txTimeUsec rxTimeUsec | - // | - //Local system | - // | New timer value Extend this value - // |---->|------...----------------------->| - // | - // lclHopTimeMsec | - // |-->| - // |------...-------------------------------------->|------...----------------------->| - // channelTimerStart Channel timer fires - // - //No hop is needed - //Extend the timer value - adjustment += settings.maxDwellTime; - } - else - { - caseNumber = 4; - //Case 1 above, both systems using the same channel - // - // channelTimerStart Channel timer fires - // |------...-------------------------------------------->| - // Channel Timer value | - // |<--------------------------------->| - // | | - // | rmtHopTimeMsec | - // | |---->| - // | Current Time - // |<------------>|<------------>| - // txTimeUsec rxTimeUsec | - // | - //Local system | - // | New timer value - // |---->| - // |------...-------------------------------------->| - // channelTimerStart | Channel timer fires - // |--------->| - // lclHopTimeMsec - // - //No hop is necessary - } + caseNumber = 3; + //Case 3, both systems using the same channel + // + // channelTimerStart Channel timer fires + // |------...-------------------------------------------->| + // Channel Timer value | + // |<--------------------------------->| + // | | + // | rmtHopTimeMsec | + // | |---->| + // | Current Time + // |<------------>|<------------>| + // txTimeUsec rxTimeUsec | + // | + //Local system | + // | New timer value + // |---->| + // |------...-------------------------------------->| + // channelTimerStart | Channel timer fires + // |--------->| + // lclHopTimeMsec + // + //No hop is necessary } //Compute the next hop time From 56cd33926e0bacffae8de88800278ac5cac347b6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 11 Jan 2023 15:55:30 -1000 Subject: [PATCH 450/594] P2P: Use transmit measurements for syncChannelTimer calcualtions --- .../LoRaSerial_Firmware.ino | 9 +- Firmware/LoRaSerial_Firmware/States.ino | 115 ++---------------- 2 files changed, 9 insertions(+), 115 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 89a09406..60a33275 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -582,11 +582,7 @@ char platformPrefix[25]; //Used for printing platform specific device name, ie " bool clockSyncReceiver; //Receives and processes the clock synchronization bool startChannelTimerPending; //When true, call startChannelTimer in transactionCompleteISR -//RX and TX time measurements -uint32_t rxTimeUsec; -uint32_t txTimeUsec; -uint16_t txRxTimeMsec; - +//TX time measurements uint32_t txSetChannelTimerMicros; //Timestamp when millis is read in TX routine to set channel timer value uint32_t transactionCompleteMicros; //Timestamp at the beginning of the transactionCompleteIsr routine uint32_t txDataAckUsec; //Time in microseconds to transmit the DATA_ACK frame @@ -599,9 +595,6 @@ unsigned long remoteSystemMillis; //Millis value contained in the received messa #define VC_DELAY_HEARTBEAT_MSEC 5 -bool rxFirstAck; //Set true when first ACK is received -bool txFirstAck; //Set true when first ACK is transmitted - //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Architecture variables diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b656a2bc..7eef9d66 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -56,57 +56,6 @@ timestampOffset >>= rShift; \ } -#define COMPUTE_RX_TIME(millisBuffer, rShift, frameAirTimeUsec) \ - { \ - currentMillis = millis(); \ - long deltaUsec = micros() - transactionCompleteMicros; \ - if ((!rxFirstAck) || (deltaUsec < rxTimeUsec)) \ - { \ - rxFirstAck = true; \ - rxTimeUsec = deltaUsec; \ - txRxTimeMsec = (txTimeUsec + rxTimeUsec) / 1000; \ - \ - /*Display the results*/ \ - if (settings.debugSync) \ - { \ - systemPrintTimestamp(radioCallHistory[RADIO_CALL_transactionCompleteISR] + timestampOffset); \ - systemPrint(" RX Time: "); \ - systemPrint(rxTimeUsec); \ - systemPrintln(" uSec"); \ - systemPrint(" TX + RX Time: "); \ - systemPrint(txRxTimeMsec); \ - systemPrintln(" mSec"); \ - } \ - } \ - \ - /*Adjust the timestamp offset*/ \ - COMPUTE_TIMESTAMP_OFFSET(millisBuffer, rShift, frameAirTimeUsec); \ - } - -#define COMPUTE_TX_TIME() \ - { \ - currentMillis = millis(); \ - long deltaUsec = transactionCompleteMicros - txDatagramMicros; \ - if ((!txFirstAck) || (deltaUsec < txTimeUsec)) \ - { \ - txFirstAck = true; \ - txTimeUsec = deltaUsec; \ - txRxTimeMsec = (txTimeUsec + rxTimeUsec) / 1000; \ - \ - /*Display the results*/ \ - if (settings.debugSync) \ - { \ - systemPrintTimestamp(currentMillis + timestampOffset); \ - systemPrint(" TX Time: "); \ - systemPrint(txTimeUsec); \ - systemPrintln(" uSec"); \ - systemPrint(" TX + RX Time: "); \ - systemPrint(txRxTimeMsec); \ - systemPrintln(" mSec"); \ - } \ - } \ - } - //Process the radio states void updateRadioState() { @@ -354,8 +303,7 @@ void updateRadioState() case DATAGRAM_FIND_PARTNER: //Received FIND_PARTNER - //Compute the receive time - COMPUTE_RX_TIME(rxData, 1, calcAirTimeUsec(headerBytes + P2P_FIND_PARTNER_BYTES + trailerBytes)); + COMPUTE_TIMESTAMP_OFFSET(rxData, 1, txFindPartnerUsec); //This system is the source of clock synchronization clockSyncReceiver = false; //P2P clock source @@ -363,8 +311,8 @@ void updateRadioState() //Display the channelTimer source if (settings.debugSync) { - systemPrint("Sourcing channelTimer, TX + RX mSec: "); - systemPrintln(txRxTimeMsec); + systemPrintTimestamp(); + systemPrintln("Sourcing channelTimer"); } //Acknowledge the FIND_PARTNER with SYNC_CLOCKS @@ -398,7 +346,6 @@ void updateRadioState() //Determine if a FIND_PARTNER has completed transmission if (transactionComplete) { - COMPUTE_TX_TIME(); triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag irqFlags = radio.getIRQFlags(); @@ -446,7 +393,7 @@ void updateRadioState() case DATAGRAM_FIND_PARTNER: //Received FIND_PARTNER //Compute the receive time - COMPUTE_RX_TIME(rxData + 1, 1, calcAirTimeUsec(headerBytes + P2P_FIND_PARTNER_BYTES + trailerBytes)); + COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 1, txFindPartnerUsec); //Acknowledge the FIND_PARTNER triggerEvent(TRIGGER_TX_SYNC_CLOCKS); @@ -462,12 +409,12 @@ void updateRadioState() //Display the channelTimer sink if (settings.debugSync) { - systemPrint("Syncing channelTimer, TX + RX mSec: "); - systemPrintln(txRxTimeMsec); + systemPrintTimestamp(); + systemPrintln("Syncing channelTimer"); } //Compute the receive time - COMPUTE_RX_TIME(rxData + 1, 1, txSyncClocksUsec); + COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 1, txSyncClocksUsec); //Hop to the next channel hopChannel(); @@ -516,18 +463,6 @@ void updateRadioState() //Determine if a SYNC_CLOCKS has completed transmission if (transactionComplete) { - //Compute the SYNC_CLOCKS frame transmit frame - if ((!txSyncClocksUsec) && (txControl.datagramType == DATAGRAM_SYNC_CLOCKS)) - { - txSyncClocksUsec = transactionCompleteMicros - txSetChannelTimerMicros; - if (settings.debugSync) - { - systemPrint("txSyncClocksUsec: "); - systemPrintln(txSyncClocksUsec); - } - } - - COMPUTE_TX_TIME(); triggerEvent(TRIGGER_TX_DONE); transactionComplete = false; //Reset ISR flag irqFlags = radio.getIRQFlags(); @@ -568,10 +503,6 @@ void updateRadioState() break; case DATAGRAM_ZERO_ACKS: - //Received ACK 2 - //Compute the receive time - COMPUTE_RX_TIME(rxData, 1, calcAirTimeUsec(headerBytes + P2P_ZERO_ACKS_BYTES + trailerBytes)); - setHeartbeatLong(); //We sent SYNC_CLOCKS and they sent ZERO_ACKS, so don't be the first to send heartbeat //Bring up the link @@ -618,9 +549,7 @@ void updateRadioState() if (transactionComplete) { transactionComplete = false; //Reset ISR flag - COMPUTE_TX_TIME(); irqFlags = radio.getIRQFlags(); - setHeartbeatShort(); //We sent the last ack so be responsible for sending the next heartbeat //Bring up the link @@ -701,34 +630,6 @@ void updateRadioState() if (transactionComplete) { transactionComplete = false; //Reset ISR flag - - //Compute the ACK frame transmit frame - if ((!txDataAckUsec) && (txControl.datagramType == DATAGRAM_HEARTBEAT)) - { - txDataAckUsec = transactionCompleteMicros - txSetChannelTimerMicros; - if (settings.debugSync) - { - systemPrint("txDataAckUsec: "); - systemPrintln(txDataAckUsec); - } - } - - //Compute the HEARTBEAT frame transmit frame - if ((!txHeartbeatUsec) && (txControl.datagramType == DATAGRAM_HEARTBEAT)) - { - txHeartbeatUsec = transactionCompleteMicros - txSetChannelTimerMicros; - if (settings.debugSync) - { - systemPrint("txHeartbeatUsec: "); - systemPrintln(txHeartbeatUsec); - } - } - - //Determine if an ACK was transmitted - if (txControl.datagramType == DATAGRAM_DATA_ACK) - { - COMPUTE_TX_TIME(); - } triggerEvent(TRIGGER_TX_DONE); irqFlags = radio.getIRQFlags(); @@ -827,7 +728,7 @@ void updateRadioState() case DATAGRAM_DATA_ACK: //Adjust the timestamp offset - COMPUTE_RX_TIME(rxData, 1, txDataAckUsec); + COMPUTE_TIMESTAMP_OFFSET(rxData, 1, txDataAckUsec); //The datagram we are expecting syncChannelTimer(txDataAckUsec); //Adjust freq hop ISR based on remote's remaining clock From 0a2bc5411beab32a1de8b0ff5e7451a24fe66e1e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 13 Jan 2023 10:13:17 -1000 Subject: [PATCH 451/594] Add timestamps to the ATI12 command Remove the confusing " @ time" --- Firmware/LoRaSerial_Firmware/Commands.ino | 31 ++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 7e549c7d..d4372a6a 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -620,39 +620,39 @@ bool commandAT(const char * commandString) return true; case ('2'): //ATI12 - Display the VC details + systemPrintTimestamp(); systemPrint("VC "); systemPrint(cmdVc); systemPrint(": "); if (!vc->flags.valid) - { - systemPrint("Down, Not valid @ "); - systemPrintTimestamp(millis() + timestampOffset); - systemPrintln(); - } + systemPrintln("Down, Not valid"); else { - systemPrint(vcStateNames[vc->vcState]); - systemPrint(" @ "); - systemPrintTimestamp(millis() + timestampOffset); - systemPrintln(); + systemPrintln(vcStateNames[vc->vcState]); + systemPrintTimestamp(); systemPrint(" ID: "); systemPrintUniqueID(vc->uniqueId); systemPrintln(vc->flags.valid ? " (Valid)" : " (Invalid)"); //Heartbeat metrics + systemPrintTimestamp(); systemPrintln(" Heartbeats"); if (cmdVc == myVc) { + systemPrintTimestamp(); systemPrint(" Next TX: "); systemPrintTimestamp(heartbeatTimer + heartbeatRandomTime + timestampOffset); systemPrintln(); } + systemPrintTimestamp(); systemPrint(" Last: "); systemPrintTimestamp(vc->lastTrafficMillis + timestampOffset); systemPrintln(); + systemPrintTimestamp(); systemPrint(" First: "); systemPrintTimestamp(vc->firstHeartbeatMillis + timestampOffset); systemPrintln(); + systemPrintTimestamp(); systemPrint(" Up Time: "); deltaMillis = vc->lastTrafficMillis - vc->firstHeartbeatMillis; if (deltaMillis < 0) @@ -663,37 +663,50 @@ bool commandAT(const char * commandString) petWDT(); //Transmitter metrics + systemPrintTimestamp(); systemPrintln(" Sent"); + systemPrintTimestamp(); systemPrint(" Frames: "); systemPrintln(vc->framesSent); + systemPrintTimestamp(); systemPrint(" Messages: "); systemPrintln(vc->messagesSent); outputSerialData(true); petWDT(); //Receiver metrics + systemPrintTimestamp(); systemPrintln(" Received"); + systemPrintTimestamp(); systemPrint(" Frames: "); systemPrintln(vc->framesReceived); + systemPrintTimestamp(); systemPrint(" Messages: "); systemPrintln(vc->messagesReceived); + systemPrintTimestamp(); systemPrint(" Bad Lengths: "); systemPrintln(vc->badLength); + systemPrintTimestamp(); systemPrint(" Link Failures: "); systemPrintln(linkFailures); outputSerialData(true); petWDT(); //ACK Management metrics + systemPrintTimestamp(); systemPrintln(" ACK Management"); + systemPrintTimestamp(); systemPrint(" Last RX ACK number: "); systemPrintln(vc->rxAckNumber); + systemPrintTimestamp(); systemPrint(" Next RX ACK number: "); systemPrintln(vc->rmtTxAckNumber); + systemPrintTimestamp(); systemPrint(" Last TX ACK number: "); systemPrintln(vc->txAckNumber); if (txDestVc == cmdVc) { + systemPrintTimestamp(); systemPrint(" ackTimer: "); if (ackTimer) systemPrintTimestamp(ackTimer + timestampOffset); From d07417e26c9f5c563fcc5377bd40b93bdf7b1d1a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 13 Jan 2023 10:31:08 -1000 Subject: [PATCH 452/594] Set verifyRxNetID to true by default --- Firmware/LoRaSerial_Firmware/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 52225e10..7306c2ce 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -430,7 +430,7 @@ typedef struct struct_settings { bool usbSerialWait = WAIT_SERIAL_DEFAULT; //Wait for USB serial initialization bool printRfData = false; //Print RX and TX data bool printPktData = false; //Print data, before encryption and after decryption - bool verifyRxNetID = false; //Verify RX netID value when not operating in point-to-point mode + bool verifyRxNetID = true; //Verify RX netID value when not operating in point-to-point mode uint8_t triggerWidth = 25; //Trigger width in microSeconds or multipler for trigger width bool triggerWidthIsMultiplier = true; //Use the trigger width as a multiplier uint32_t triggerEnable = 0; //Determine which triggers are enabled: 31 - 0 From 45927f3d0eac87bc6c45b7bead3d873fd7bc2d5c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 14 Jan 2023 07:49:06 -1000 Subject: [PATCH 453/594] Delay transmission of partial escape sequence to remote radio Handle the following cases: 1. A single escape character starts the input stream 2. A single escape character is in the middle of the input stream 3. A single escape character is at the end of the input stream 4. Two escape characters start the input stream 5. Two escape characters are in the middle of the input stream 6. Two escape characters are at the end of the input stream 7. Full escape sequence entered in middle of input stream 8. Full escape sequence entered at end of input stream 9. Only one escape character is entered after two seconds of idle time 10. Only 2 escape characters are entered after two seconds of idle time 11. The full escape sequence is entered after two seconds of idle time --- Firmware/LoRaSerial_Firmware/Serial.ino | 77 ++++++++++++++++--------- 1 file changed, 51 insertions(+), 26 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 7a13f2d4..7bbe02be 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -321,10 +321,12 @@ void updateSerial() void processSerialInput() { uint16_t radioHead; + static uint32_t lastEscapeCharEnteredMillis; //Process the serial data radioHead = radioTxHead; - while (availableRXBytes() && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - 1)) + while (availableRXBytes() + && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - maxEscapeCharacters)) && (transactionComplete == false)) { //Take a break if there are ISRs to attend to @@ -380,45 +382,68 @@ void processSerialInput() else { //Check general serial stream for command characters - if (incoming == escapeCharacter) + //Ignore escape characters received within 2 seconds of serial traffic + //Allow escape characters received within first 2 seconds of power on + if ((incoming == escapeCharacter) + && ((millis() - lastByteReceived_ms > minEscapeTime_ms) + || (millis() < minEscapeTime_ms))) { - //Ignore escape characters received within 2 seconds of serial traffic - //Allow escape characters received within first 2 seconds of power on - if (millis() - lastByteReceived_ms > minEscapeTime_ms || millis() < minEscapeTime_ms) + escapeCharsReceived++; + lastEscapeCharEnteredMillis = millis(); + if (escapeCharsReceived == maxEscapeCharacters) { - escapeCharsReceived++; - if (escapeCharsReceived == maxEscapeCharacters) - { - systemPrintln("\r\nOK"); - outputSerialData(true); + systemPrintln("\r\nOK"); + outputSerialData(true); - inCommandMode = true; //Allow AT parsing. Prevent received RF data from being printed. - forceRadioReset = false; //Don't reset the radio link unless a setting requires it - writeOnCommandExit = false; //Don't record settings changes unless user commands it + inCommandMode = true; //Allow AT parsing. Prevent received RF data from being printed. + forceRadioReset = false; //Don't reset the radio link unless a setting requires it + writeOnCommandExit = false; //Don't record settings changes unless user commands it - tempSettings = settings; + tempSettings = settings; - escapeCharsReceived = 0; - lastByteReceived_ms = millis(); - return; //Avoid recording this incoming command char - } - } - else //This is just a character in the stream, ignore - { + escapeCharsReceived = 0; lastByteReceived_ms = millis(); - escapeCharsReceived = 0; //Update timeout check for escape char and partial frame + return; //Avoid recording this incoming command char } } + + //This is just a character in the stream, ignore else { + //Update timeout check for escape char and partial frame lastByteReceived_ms = millis(); - escapeCharsReceived = 0; //Update timeout check for escape char and partial frame + + //Replay any escape characters if the sequence was not entered in the + //necessary time + while (escapeCharsReceived) + { + escapeCharsReceived--; + radioTxBuffer[radioTxHead++] = escapeCharacter; + radioTxHead %= sizeof(radioTxBuffer); + } + + //The input data byte will be sent over the long range radio + radioTxBuffer[radioTxHead++] = incoming; + radioTxHead %= sizeof(radioTxBuffer); } + } //End process rx buffer + } - //This data byte will be sent over the long range radio - radioTxBuffer[radioTxHead++] = incoming; + //Check for escape timeout, more than two seconds have elapsed without entering + //command mode. The escape characters must be part of the input stream but were + //the last characters entered. Send these characters over the long range radio. + if (escapeCharsReceived && (millis() - lastEscapeCharEnteredMillis > minEscapeTime_ms) + && (availableRXBytes() < (sizeof(radioTxBuffer) - maxEscapeCharacters))) + { + //Replay the escape characters + while (escapeCharsReceived) + { + escapeCharsReceived--; + + //Transmit the escape character over the long range radio + radioTxBuffer[radioTxHead++] = escapeCharacter; radioTxHead %= sizeof(radioTxBuffer); - } //End process rx buffer + } } //Print the number of bytes placed into the rxTxBuffer From acf91b8de45ebf62ee84d4e24cfbdde46ffe2ff5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 17:14:21 -1000 Subject: [PATCH 454/594] Display the command which received the error --- Firmware/LoRaSerial_Firmware/Commands.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index d4372a6a..2a308c30 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -833,7 +833,10 @@ void checkCommand() if (success) reportOK(); else + { + systemPrintln(commandString); systemPrintln("ERROR"); + } outputSerialData(true); petWDT(); From 90e5a6d90de75e6f4d7d3ade3d6f1747dab13757 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 10:51:08 -1000 Subject: [PATCH 455/594] Add printing of uint32 values --- Firmware/LoRaSerial_Firmware/System.ino | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 11b02657..451954af 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -48,6 +48,26 @@ void systemPrint(int value, uint8_t printType) systemPrint(temp); } +//Print a uint32_t value as HEX or decimal +void systemPrint(uint32_t value, uint8_t printType) +{ + char temp[20]; + + if (printType == HEX) + sprintf(temp, "%08x", value); + else if (printType == DEC) + sprintf(temp, "%lu", value); + + systemPrint(temp); +} + +//Print a uint32_t value as HEX or decimal +void systemPrintln(uint32_t value, uint8_t printType) +{ + systemPrint(value, printType); + systemPrintln(); +} + //Print an integer value with a carriage return and line feed void systemPrintln(int value) { From a8c9f064b52d5dd132cdfc8f4e798bae439a0abe Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 22:00:21 -1000 Subject: [PATCH 456/594] Virtual Circuit: Update the header file comments --- Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index aaa4000a..424d14e2 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -16,6 +16,8 @@ //address spaces: // // 0 - Data communications +// 1 - Remote commands +// 6 - Remote responses // 7 - Special virtual circuits //The following addresses are reserved for data communications: 0 - VCAB_NUMBER_MASK @@ -37,7 +39,7 @@ #define PC_DATA_NACK (PC_DATA_ACK + 1) //Indicate data delivery failure #define PC_SERIAL_RECONNECT (PC_DATA_NACK + 1) //Disconnect/reconnect the serial port over LoRaSerial CPU reset -//Address space 1 and 2 are reserved for the host PC interface to support remote +//Address space 1 and 6 are reserved for the host PC interface to support remote //command processing. The radio removes these bits and converts them to the //appropriate datagram type. Upon reception of one of these messages the bit is //added back into the VC header and the message is delivered to the host PC. From 05df87804f83780b03a9ea28883528f0788bb979 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 17 Jan 2023 01:08:35 -1000 Subject: [PATCH 457/594] Update the VcServerTest application Integrate changes from SprinklerServer --- Firmware/Tools/RadioV2.c | 6 ++--- Firmware/Tools/Sockets.c | 14 +++++------ Firmware/Tools/System.c | 4 +-- Firmware/Tools/Terminal.c | 18 +++++++------- Firmware/Tools/VcServerTest.c | 47 +++++++++++++++++++---------------- Firmware/Tools/settings.h | 2 +- 6 files changed, 48 insertions(+), 43 deletions(-) diff --git a/Firmware/Tools/RadioV2.c b/Firmware/Tools/RadioV2.c index 2a2ffd91..18f40f40 100644 --- a/Firmware/Tools/RadioV2.c +++ b/Firmware/Tools/RadioV2.c @@ -42,7 +42,7 @@ FRAME_TYPE rcvDatagram() { remoteAddrLength = sizeof(remoteAddr); memset(&remoteAddr, 0, remoteAddrLength); - rxBytes = recvfrom(tty, rxBuffer, sizeof(rxBuffer), MSG_WAITALL, + rxBytes = recvfrom(radio, rxBuffer, sizeof(rxBuffer), MSG_WAITALL, (struct sockaddr *) &remoteAddr, &remoteAddrLength); if (rxBytes < 0) { @@ -110,9 +110,9 @@ void transmitDatagram(const struct sockaddr * addr, int addrLen) //Send this datagram if (usingTerminal) - bytesSent = write(tty, (const void *)txBuffer, txBytes); + bytesSent = write(radio, (const void *)txBuffer, txBytes); else - bytesSent = sendto(tty, (const char *)txBuffer, txBytes, MSG_CONFIRM, + bytesSent = sendto(radio, (const char *)txBuffer, txBytes, MSG_CONFIRM, addr, addrLen); if (bytesSent < 0) perror("Failed to send datagram!"); diff --git a/Firmware/Tools/Sockets.c b/Firmware/Tools/Sockets.c index f79f2a94..7d458e2b 100644 --- a/Firmware/Tools/Sockets.c +++ b/Firmware/Tools/Sockets.c @@ -7,8 +7,8 @@ int openSocket() int status; //Open the socket - tty = socket(AF_INET, SOCK_DGRAM, 0); - if (tty < 0) + radio = socket(AF_INET, SOCK_DGRAM, 0); + if (radio < 0) { status = errno; perror("Failed to open the socket!"); @@ -28,7 +28,7 @@ int openSocket() localAddr.sin_port = htons(PORT); // Bind the socket with the server address - status = bind(tty, (const struct sockaddr *)&localAddr, sizeof(localAddr)); + status = bind(radio, (const struct sockaddr *)&localAddr, sizeof(localAddr)); if (status < 0 ) { perror("Failed to bind the socket!"); @@ -36,20 +36,20 @@ int openSocket() } //Support broadcasting - status = setsockopt (tty, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable)); + status = setsockopt (radio, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable)); if (status < 0) { perror("setsockopt failed"); - close(tty); + close(radio); return status; } //Set the socket to non-blocking - status = ioctl(tty, FIONBIO, (char *)&non_blocking); + status = ioctl(radio, FIONBIO, (char *)&non_blocking); if (status < 0) { perror("ioctl failed"); - close(tty); + close(radio); return status; } return 0; diff --git a/Firmware/Tools/System.c b/Firmware/Tools/System.c index 53f38747..34884261 100644 --- a/Firmware/Tools/System.c +++ b/Firmware/Tools/System.c @@ -172,8 +172,8 @@ int processData() timeout.tv_usec = 1000; //Wait for receive data or timeout - FD_SET(tty, &readfds); - status = select(tty + 1, &readfds, &writefds, &exceptfds, &timeout); + FD_SET(radio, &readfds); + status = select(radio + 1, &readfds, &writefds, &exceptfds, &timeout); //Update the milliseconds since started gettimeofday(&stop, NULL); diff --git a/Firmware/Tools/Terminal.c b/Firmware/Tools/Terminal.c index 3a54c629..fa95b4d0 100644 --- a/Firmware/Tools/Terminal.c +++ b/Firmware/Tools/Terminal.c @@ -1,18 +1,18 @@ #include "settings.h" -int openLoRaSerial(const char *ttyName) +int openLoRaSerial(const char *terminal) { struct termios params; int status; - tty = open (ttyName, O_RDWR); - if (tty < 0) + radio = open (terminal, O_RDWR); + if (radio < 0) { - perror ("ERROR: Failed to open the tty"); + perror ("ERROR: Failed to open the terminal"); return errno; } - if (tcgetattr(tty, ¶ms) != 0) { + if (tcgetattr(radio, ¶ms) != 0) { perror("ERROR: tcgetattr failed!"); return errno; } @@ -30,7 +30,7 @@ int openLoRaSerial(const char *ttyName) cfsetispeed(¶ms, B57600); cfsetospeed(¶ms, B57600); - if (tcsetattr(tty, TCSANOW, ¶ms) != 0) { + if (tcsetattr(radio, TCSANOW, ¶ms) != 0) { perror("ERROR: tcsetattr failed!"); return errno; } @@ -45,7 +45,7 @@ int readLoRaSerial(uint8_t * buffer, int bufferLength) int length; //Read the frame type - bytes = read(tty, buffer, 1); + bytes = read(radio, buffer, 1); if (bytes < 0) { perror("Failed to read frame type!"); @@ -54,7 +54,7 @@ int readLoRaSerial(uint8_t * buffer, int bufferLength) rxBytes = bytes; //Read the length in bytes - bytes = read(tty, &buffer[rxBytes], 1); + bytes = read(radio, &buffer[rxBytes], 1); if (bytes < 0) { perror("Failed to read length byte!"); @@ -68,7 +68,7 @@ int readLoRaSerial(uint8_t * buffer, int bufferLength) //Read the length in bytes while (length - rxBytes) { - bytes = read(tty, &buffer[rxBytes], length - rxBytes); + bytes = read(radio, &buffer[rxBytes], length - rxBytes); if (bytes < 0) { perror("Failed to read remaining data!"); diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index cce170cc..bc7f9b56 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -2,6 +2,7 @@ #include "settings.h" #define BUFFER_SIZE 2048 +#define INPUT_BUFFER_SIZE BUFFER_SIZE #define MAX_MESSAGE_SIZE 32 #define STDIN 0 #define STDOUT 1 @@ -29,7 +30,7 @@ typedef struct _VIRTUAL_CIRCUIT bool findMyVc; int myVc = VC_UNASSIGNED; int remoteVc; -uint8_t inputBuffer[BUFFER_SIZE]; +uint8_t inputBuffer[INPUT_BUFFER_SIZE]; uint8_t outputBuffer[VC_SERIAL_HEADER_BYTES + BUFFER_SIZE]; int timeoutCount; VIRTUAL_CIRCUIT virtualCircuitList[MAX_VC]; @@ -56,7 +57,7 @@ int cmdToRadio(uint8_t * buffer, int length) printf("Sending LoRaSerial command: %s\n", buffer); //Send the header - bytesWritten = write(tty, (uint8_t *)&header, VC_SERIAL_HEADER_BYTES); + bytesWritten = write(radio, (uint8_t *)&header, VC_SERIAL_HEADER_BYTES); if (bytesWritten < (int)VC_SERIAL_HEADER_BYTES) { perror("ERROR: Write of header to radio failed!"); @@ -67,7 +68,7 @@ int cmdToRadio(uint8_t * buffer, int length) bytesSent = 0; while (bytesSent < length) { - bytesWritten = write(tty, buffer, length); + bytesWritten = write(radio, buffer, length); if (bytesWritten < 0) { perror("ERROR: Write of data to radio failed!"); @@ -100,7 +101,7 @@ int hostToRadio(uint8_t destVc, uint8_t * buffer, int length) } //Send the header - bytesWritten = write(tty, (uint8_t *)&header, VC_SERIAL_HEADER_BYTES); + bytesWritten = write(radio, (uint8_t *)&header, VC_SERIAL_HEADER_BYTES); if (bytesWritten < (int)VC_SERIAL_HEADER_BYTES) { perror("ERROR: Write of header to radio failed!"); @@ -111,7 +112,7 @@ int hostToRadio(uint8_t destVc, uint8_t * buffer, int length) bytesSent = 0; while (bytesSent < length) { - bytesWritten = write(tty, buffer, length); + bytesWritten = write(radio, buffer, length); if (bytesWritten < 0) { perror("ERROR: Write of data to radio failed!"); @@ -139,7 +140,7 @@ int stdinToRadio() do { //Read the console input data into the local buffer. - bytesRead = read(STDIN, inputBuffer, BUFFER_SIZE); + bytesRead = read(STDIN, inputBuffer, INPUT_BUFFER_SIZE); if (bytesRead < 0) { perror("ERROR: Read from stdin failed!"); @@ -371,7 +372,7 @@ int radioToHost() { //Read the virtual circuit header into the local buffer. bytesToRead = &outputBuffer[sizeof(outputBuffer)] - dataEnd; - bytesRead = read(tty, dataEnd, bytesToRead); + bytesRead = read(radio, dataEnd, bytesToRead); if (bytesRead == 0) break; if (bytesRead < 0) @@ -484,12 +485,13 @@ main ( ) { bool breakLinks; + fd_set currentfds; int maxfds; + int numfds; bool reset; int status; char * terminal; struct timeval timeout; - uint8_t * vcData; maxfds = STDIN; status = 0; @@ -536,8 +538,6 @@ main ( status = openLoRaSerial(terminal); if (status) break; - if (maxfds < tty) - maxfds = tty; //Determine the options reset = false; @@ -558,7 +558,7 @@ main ( cmdToRadio((uint8_t *)LINK_RESET_COMMAND, strlen(LINK_RESET_COMMAND)); //Allow the device to reset - close(tty); + close(radio); do { sleep(1); @@ -577,11 +577,11 @@ main ( cmdToRadio((uint8_t *)BREAK_LINKS_COMMAND, strlen(BREAK_LINKS_COMMAND)); //Initialize the fd_sets - if (maxfds < tty) - maxfds = tty; - FD_ZERO(&exceptfds); + if (maxfds < radio) + maxfds = radio; FD_ZERO(&readfds); - FD_ZERO(&writefds); + FD_SET(STDIN, &readfds); + FD_SET(radio, &readfds); while (1) { @@ -590,12 +590,17 @@ main ( timeout.tv_usec = 1000; //Wait for receive data or timeout - FD_SET(STDIN, &readfds); - FD_SET(tty, &readfds); - status = select(maxfds + 1, &readfds, &writefds, &exceptfds, &timeout); + memcpy((void *)¤tfds, (void *)&readfds, sizeof(readfds)); + numfds = select(maxfds + 1, ¤tfds, NULL, NULL, &timeout); + if (numfds < 0) + { + perror("ERROR: select call failed!"); + status = errno; + break; + } //Check for timeout - if ((status == 0) && (timeoutCount++ >= 1000)) + if ((numfds == 0) && (timeoutCount++ >= 1000)) { timeoutCount = 0; if (myVc == VC_UNASSIGNED) @@ -607,7 +612,7 @@ main ( } //Determine if console input is available - if (FD_ISSET(STDIN, &readfds)) + if (FD_ISSET(STDIN, ¤tfds)) { //Send the console input to the radio status = stdinToRadio(); @@ -615,7 +620,7 @@ main ( break; } - if (FD_ISSET(tty, &readfds)) + if (FD_ISSET(radio, ¤tfds)) { //Process the incoming data from the radio status = radioToHost(); diff --git a/Firmware/Tools/settings.h b/Firmware/Tools/settings.h index c8c70ce2..5d872edd 100644 --- a/Firmware/Tools/settings.h +++ b/Firmware/Tools/settings.h @@ -81,6 +81,7 @@ int8_t destAddr; fd_set exceptfds; int8_t myAddr; uint8_t myUniqueId[UNIQUE_ID_BYTES]; +int radio; fd_set readfds; uint8_t rxBuffer[255]; int rxBytes; @@ -92,7 +93,6 @@ bool transactionComplete; uint8_t txBuffer[255]; int txBytes; uint8_t * txData; -int tty; bool usingTerminal; fd_set writefds; From 52e4ea44ca79ebf9578975f93de7595bdefa4b15 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 15 Jan 2023 08:33:19 -1000 Subject: [PATCH 458/594] Properly display mSec in getTxTime --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4c65e61b..292db712 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3154,7 +3154,7 @@ bool getTxTime(bool (*transmitFrame)(), uint32_t * txFrameUsec, const char * fra systemPrint("TX "); systemPrint(frameName); systemPrint(": "); - systemPrint(*txFrameUsec); + systemPrint(((float)*txFrameUsec) / 1000., 5); systemPrintln(" mSec"); outputSerialData(true); } From a1371cd34ee71842d59df531585d799a410fe75a Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 15 Jan 2023 08:35:35 -1000 Subject: [PATCH 459/594] Set txSetChannelTimerMicros even when frequencyHop is false Enable the proper calculation of TX time for the various frames --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 292db712..b1941d43 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2683,6 +2683,8 @@ bool transmitDatagram() hopChannel(); } } + else + txSetChannelTimerMicros = micros(); /* Header ------. endOfTxData ---. From 2e813c23c40d0ac3dd6f4f78cdad963db91fa389 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 20 Jan 2023 13:56:46 -0700 Subject: [PATCH 460/594] Add max timeouts to the receiveInProgress bits --- .../LoRaSerial_Firmware.ino | 6 +++ Firmware/LoRaSerial_Firmware/Radio.ino | 50 +++++++++++++++++-- Firmware/LoRaSerial_Firmware/States.ino | 4 +- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 60a33275..bb1a4d08 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -422,6 +422,12 @@ unsigned long startReceiveFailureMillis; int startReceiveFailureState; unsigned long lastReceiveInProcessTrue; uint8_t lastModemStatus; +unsigned long lastRIPBit0 = 0; +unsigned long lastRIPBit1 = 0; +unsigned long lastRIPBit3 = 0; +uint16_t maxRIPBit0; +uint16_t maxRIPBit1; +uint16_t maxRIPBit3; unsigned long txSuccessMillis; unsigned long txFailureMillis; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index b1941d43..6b8a7949 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -67,6 +67,11 @@ bool configureRadio() systemDescriptionAirTime = calcAirTimeMsec(headerBytes + P2P_SYNC_CLOCKS_BYTES + trailerBytes); //Used for response timeout during 3-way handshake maxPacketAirTime = calcAirTimeMsec(MAX_PACKET_SIZE); + //Babysit the radio's receive in process bits + maxRIPBit0 = (calcSymbolTimeUsec() * settings.radioPreambleLength) / 1000; //In ms + maxRIPBit1 = (calcSymbolTimeUsec() * settings.radioPreambleLength) / 1000; //In ms + maxRIPBit3 = maxPacketAirTime; //In ms + if ((settings.debug == true) || (settings.debugRadio == true)) { systemPrint("Freq: "); @@ -581,14 +586,48 @@ unsigned long mSecToChannelZero() //Returns true if the radio indicates we have an ongoing reception bool receiveInProcess() { - //triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_START); - uint8_t radioStatus = radio.getModemStatus(); if (radioStatus & RECEIVE_IN_PROCESS_MASK) { - //If any bits are set there is a receive in progress + //If any bits are set there is a receive in process if ((lastModemStatus & RECEIVE_IN_PROCESS_MASK) == 0) lastReceiveInProcessTrue = millis(); + + triggerEvent(TRIGGER_RECEIVE_IN_PROCESS); + + //Determine which phase we are in, 0b1011 0b0011 or 0b0001 + if (radioStatus & 0b1011) + { + //All bits are set + if (lastRIPBit3 == 0) + lastRIPBit3 = millis(); + else if (millis() - lastRIPBit3 > maxRIPBit3) + { + triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_BIT3); + return (false); //We received valid header, with a packet that never completed + } + } + else if (radioStatus & 0b0011) + { + if (lastRIPBit1 == 0) + lastRIPBit1 = millis(); + else if (millis() - lastRIPBit1 > maxRIPBit1) + { + triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_BIT1); + return (false); //We received a preamble that ended, with a packet that never completed + } + } + else + { + if (lastRIPBit0 == 0) + lastRIPBit0 = millis(); + else if (millis() - lastRIPBit0 > maxRIPBit0) + { + triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_BIT0); + return (false); //We received the start of a preamble that never ended + } + } + return (true); } lastModemStatus = radioStatus; @@ -1669,6 +1708,11 @@ PacketType rcvDatagram() framesReceived++; int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); + //Reset receiveInProcess Babysitter + lastRIPBit0 = 0; + lastRIPBit1 = 0; + lastRIPBit3 = 0; + printPacketQuality(); //Display the RSSI, SNR and frequency error values if (state == RADIOLIB_ERR_NONE) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 7eef9d66..8b61618e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1457,7 +1457,7 @@ void updateRadioState() } } - //Determine if a receive is in progress + //Determine if a receive is in process else if (receiveInProcess()) { if (!trainingPreviousRxInProgress) @@ -1600,7 +1600,7 @@ void updateRadioState() } } - //Determine if a receive is in progress + //Determine if a receive is in process else if (receiveInProcess()) if (!trainingPreviousRxInProgress) { From 0082c513495c91bd7adb82a3bbd9278545452b1c Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 20 Jan 2023 13:57:13 -0700 Subject: [PATCH 461/594] Update settings.h --- Firmware/LoRaSerial_Firmware/settings.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 7306c2ce..2a1ffe69 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -255,6 +255,10 @@ enum TRIGGER_MP_TX_DATA, TRIGGER_NETID_MISMATCH, TRIGGER_RADIO_RESET, + TRIGGER_RECEIVE_IN_PROCESS, + TRIGGER_RECEIVE_IN_PROCESS_BIT0, + TRIGGER_RECEIVE_IN_PROCESS_BIT1, + TRIGGER_RECEIVE_IN_PROCESS_BIT3, TRIGGER_RETRANSMIT, TRIGGER_RETRANSMIT_FAIL, TRIGGER_RTR_255BYTE, From 869441c5d75934941da3b27bbc0575593e8bdfde Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 20 Jan 2023 20:48:30 -0700 Subject: [PATCH 462/594] Whitespace changes --- Firmware/LoRaSerial_Firmware/Radio.ino | 6 +- Firmware/LoRaSerial_Firmware/States.ino | 84 ++++++++++++------------- Firmware/LoRaSerial_Firmware/settings.h | 12 ++-- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6b8a7949..4bb40deb 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2708,7 +2708,7 @@ bool transmitDatagram() systemPrint("TX msToNextHop: "); systemPrint(msToNextHop); systemPrintln(" mSec"); - break; + break; } } } //ENABLE_DEVELOPER @@ -3139,7 +3139,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) //Compute the interval before a retransmission occurs in milliseconds, //this value increases with each transmission retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime) - * (frameSentCount + 1) * 3 / 2; + * (frameSentCount + 1) * 3 / 2; //BLink the TX LED blinkRadioTxLed(true); @@ -3453,7 +3453,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) clockSyncData[clockSyncIndex].lclHopTimeMsec = lclHopTimeMsec; clockSyncData[clockSyncIndex].timeToHop = timeToHop; clockSyncIndex += 1; - if(clockSyncIndex >= (sizeof(clockSyncData) / sizeof(CLOCK_SYNC_DATA)) ) clockSyncIndex = 0; + if (clockSyncIndex >= (sizeof(clockSyncData) / sizeof(CLOCK_SYNC_DATA)) ) clockSyncIndex = 0; //Restart the channel timer timeToHop = false; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 8b61618e..1dcd753f 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -20,7 +20,7 @@ { \ /*Compute the frequency correction*/ \ frequencyCorrection += radio.getFrequencyError() / 1000000.0; \ - \ + \ /*Send the ACK to the remote system*/ \ triggerEvent(trigger); \ if (xmitDatagramP2PAck() == true) \ @@ -35,7 +35,7 @@ { \ /*Start the ACK timer*/ \ ackTimer = datagramTimer; \ - \ + \ /*Since ackTimer is off when equal to zero, force it to a non-zero value*/ \ /*Subtract one so that the comparisons result in a small number*/ \ if (!ackTimer) \ @@ -192,7 +192,7 @@ void updateRadioState() else //Start receiving returnToReceiving(); - changeState(RADIO_DISCOVER_BEGIN); + changeState(RADIO_DISCOVER_BEGIN); break; //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -209,7 +209,7 @@ void updateRadioState() | | | Channel 0 | | Channel 0 | Stop HOP Timer | | Stop HOP Timer | - clockSyncReceiver | = true | clockSyncReceiver = true | + clockSyncReceiver | = true | clockSyncReceiver = true | | | | V V | .----> P2P_NO_LINK P2P_NO_LINK <------------------. | @@ -974,43 +974,43 @@ void updateRadioState() | | | | V V - .--------------> RADIO_MP_STANDBY RADIO_DISCOVER_BEGIN - | | | - | No TX | | - | Do ??? RX other V | - +<--------+<------------+ V - ^ | | .--------> RADIO_DISCOVER_SCANNING - | | TX Resp | | TX done | - | | | | | - | | | WAIT_TX_FIND_PARTNER_DONE | - | .---' | ^ | - | | RX | | | - | | FIND_PARTNER | < - - - - - - | TX FIND_PARTNER | - | | | | | - | | | | Delay | - | | | | 10 Sec | - | | | +<----------. | - | | | ^ | | - | | | | Yes | No | - | | | + <---- Loops < 10 | - | | | ^ ^ | - | | | No | Yes | | - | | | Channel 0 ------' | - | | | ^ | - | | | | Hop reverse | - | | | | RX timeout V - | | | '---------------------+ - | | | | - | | | | - | | TX | | - | | SYNC_CLOCKS | - - - - - - - - - - - - - - - - - > | RX SYNC_CLOCKS - | | | | - | | | | Sync clocks - | | v | Update channel # - | '-----> RADIO_MP_WAIT_TX_DONE | - | | | - | v | - `-----------------------+<--------------------------------------' + .--------------> RADIO_MP_STANDBY RADIO_DISCOVER_BEGIN + | | | + | No TX | | + | Do ??? RX other V | + +<--------+<------------+ V + ^ | | .--------> RADIO_DISCOVER_SCANNING + | | TX Resp | | TX done | + | | | | | + | | | WAIT_TX_FIND_PARTNER_DONE | + | .---' | ^ | + | | RX | | | + | | FIND_PARTNER | < - - - - - - | TX FIND_PARTNER | + | | | | | + | | | | Delay | + | | | | 10 Sec | + | | | +<----------. | + | | | ^ | | + | | | | Yes | No | + | | | + <---- Loops < 10 | + | | | ^ ^ | + | | | No | Yes | | + | | | Channel 0 ------' | + | | | ^ | + | | | | Hop reverse | + | | | | RX timeout V + | | | '---------------------+ + | | | | + | | | | + | | TX | | + | | SYNC_CLOCKS | - - - - - - - - - - - - - - - - - > | RX SYNC_CLOCKS + | | | | + | | | | Sync clocks + | | v | Update channel # + | '-----> RADIO_MP_WAIT_TX_DONE | + | | | + | v | + `-----------------------+<--------------------------------------' */ @@ -2818,7 +2818,7 @@ void vcChangeState(int8_t vcIndex, uint8_t state) //Determine if the VC is connecting vcBit = 1 << vcIndex; if (((state > VC_STATE_LINK_ALIVE) && (state < VC_STATE_CONNECTED)) - || (vc->flags.wasConnected && (vc->vcState == VC_STATE_LINK_ALIVE))) + || (vc->flags.wasConnected && (vc->vcState == VC_STATE_LINK_ALIVE))) vcConnecting |= vcBit; else vcConnecting &= ~vcBit; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 2a1ffe69..27159fdf 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -74,7 +74,7 @@ const RADIO_STATE_ENTRY radioStateTable[] = // State RX Name Description {RADIO_DISCOVER_BEGIN, 0, "DISCOVER_BEGIN", "Disc: Setup for scanning"}, // 9 {RADIO_DISCOVER_SCANNING, 0, "DISCOVER_SCANNING", "Disc: Scanning for servers"}, //10 - {RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE,0,"DISCOVER_WAIT_TX_FIND_PARTNER_DONE","Disc: Wait for FIND_PARTNER to xmit"}, //11 + {RADIO_DISCOVER_WAIT_TX_FIND_PARTNER_DONE, 0, "DISCOVER_WAIT_TX_FIND_PARTNER_DONE", "Disc: Wait for FIND_PARTNER to xmit"}, //11 //Multi-Point data exchange // State RX Name Description @@ -83,14 +83,14 @@ const RADIO_STATE_ENTRY radioStateTable[] = //Training client states // State RX Name Description - {RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE,0, "TRAIN_WAIT_TX_FIND_PARTNER_DONE","Train: Wait TX training FIND_PARTNER done"}, //14 + {RADIO_TRAIN_WAIT_TX_FIND_PARTNER_DONE, 0, "TRAIN_WAIT_TX_FIND_PARTNER_DONE", "Train: Wait TX training FIND_PARTNER done"}, //14 {RADIO_TRAIN_WAIT_RX_RADIO_PARAMETERS, 1, "TRAIN_WAIT_RX_RADIO_PARAMETERS", "Train: Wait for radio parameters"}, //15 {RADIO_TRAIN_WAIT_TX_PARAM_ACK_DONE, 0, "TRAIN_WAIT_TX_PARAM_ACK_DONE", "Train: Wait for TX param ACK done"}, //16 //Training server states // State RX Name Description {RADIO_TRAIN_WAIT_FOR_FIND_PARTNER, 1, "TRAIN_WAIT_FOR_FIND_PARTNER", "Train: Wait for training FIND_PARTNER"}, //17 - {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE,0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE","Train: Wait for TX params done"}, //18 + {RADIO_TRAIN_WAIT_TX_RADIO_PARAMS_DONE, 0, "TRAIN_WAIT_TX_RADIO_PARAMS_DONE", "Train: Wait for TX params done"}, //18 //Virtual circuit states // State RX Name Description @@ -351,12 +351,12 @@ typedef struct _COMMAND_ENTRY typedef enum { LEDS_MULTIPOINT = 0, // 0: Green1: RX, Green2: Sync, Green3: RSSI, Green4: TX - // Blue: Hop, Yellow: HEARTBEAT RX/TX + // Blue: Hop, Yellow: HEARTBEAT RX/TX LEDS_P2P, // 1: Green: RSSI, Blue: Serial TX, Yellow: Serial RX LEDS_VC, // 2; Green1: RX, Green2: Sync, Green3: RSSI, Green4: TX - // Blue: Hop, Yellow: HEARTBEAT RX/TX + // Blue: Hop, Yellow: HEARTBEAT RX/TX LEDS_RADIO_USE, // 3: Green1: RX, Green2: Link, Green3: RSSI, Green4: TX - // Blue: Bad frames, Yellow: Bad CRC + // Blue: Bad frames, Yellow: Bad CRC LEDS_RSSI, // 4: Green: RSSI, Blue: Serial TX, Yellow: Serial RX LEDS_RESERVED_1, // 5 LEDS_RESERVED_2, // 6 From 7853e1323578b1f0023deaa73be6aa5aac990c90 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 20 Jan 2023 20:50:45 -0700 Subject: [PATCH 463/594] Remove individual receiveInProgress bits --- .../LoRaSerial_Firmware.ino | 6 -- Firmware/LoRaSerial_Firmware/Radio.ino | 82 +++++++++---------- Firmware/LoRaSerial_Firmware/settings.h | 2 + 3 files changed, 40 insertions(+), 50 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index bb1a4d08..60a33275 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -422,12 +422,6 @@ unsigned long startReceiveFailureMillis; int startReceiveFailureState; unsigned long lastReceiveInProcessTrue; uint8_t lastModemStatus; -unsigned long lastRIPBit0 = 0; -unsigned long lastRIPBit1 = 0; -unsigned long lastRIPBit3 = 0; -uint16_t maxRIPBit0; -uint16_t maxRIPBit1; -uint16_t maxRIPBit3; unsigned long txSuccessMillis; unsigned long txFailureMillis; diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 4bb40deb..1ba34611 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -67,11 +67,6 @@ bool configureRadio() systemDescriptionAirTime = calcAirTimeMsec(headerBytes + P2P_SYNC_CLOCKS_BYTES + trailerBytes); //Used for response timeout during 3-way handshake maxPacketAirTime = calcAirTimeMsec(MAX_PACKET_SIZE); - //Babysit the radio's receive in process bits - maxRIPBit0 = (calcSymbolTimeUsec() * settings.radioPreambleLength) / 1000; //In ms - maxRIPBit1 = (calcSymbolTimeUsec() * settings.radioPreambleLength) / 1000; //In ms - maxRIPBit3 = maxPacketAirTime; //In ms - if ((settings.debug == true) || (settings.debugRadio == true)) { systemPrint("Freq: "); @@ -587,49 +582,24 @@ unsigned long mSecToChannelZero() bool receiveInProcess() { uint8_t radioStatus = radio.getModemStatus(); + + //If any bits are set there is a receive in process if (radioStatus & RECEIVE_IN_PROCESS_MASK) { - //If any bits are set there is a receive in process if ((lastModemStatus & RECEIVE_IN_PROCESS_MASK) == 0) lastReceiveInProcessTrue = millis(); - triggerEvent(TRIGGER_RECEIVE_IN_PROCESS); - - //Determine which phase we are in, 0b1011 0b0011 or 0b0001 - if (radioStatus & 0b1011) - { - //All bits are set - if (lastRIPBit3 == 0) - lastRIPBit3 = millis(); - else if (millis() - lastRIPBit3 > maxRIPBit3) - { - triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_BIT3); - return (false); //We received valid header, with a packet that never completed - } - } - else if (radioStatus & 0b0011) - { - if (lastRIPBit1 == 0) - lastRIPBit1 = millis(); - else if (millis() - lastRIPBit1 > maxRIPBit1) - { - triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_BIT1); - return (false); //We received a preamble that ended, with a packet that never completed - } - } - else + if (millis() - lastReceiveInProcessTrue > maxPacketAirTime) { - if (lastRIPBit0 == 0) - lastRIPBit0 = millis(); - else if (millis() - lastRIPBit0 > maxRIPBit0) - { - triggerEvent(TRIGGER_RECEIVE_IN_PROCESS_BIT0); - return (false); //We received the start of a preamble that never ended - } + //We received valid header, with a packet that never completed + //or we received a preamble that ended, with a packet that never completed + //or we received the start of a preamble that never ended + dummyRead(); } return (true); } + lastModemStatus = radioStatus; return (false); //No receive in process } @@ -1525,7 +1495,7 @@ bool xmitVcSyncAcks(int8_t destVc) vcHeader->srcVc = myVc; endOfTxData += VC_RADIO_HEADER_BYTES; - if(settings.debugSync) + if (settings.debugSync) { systemPrint(" channelNumber: "); systemPrintln(channelNumber); @@ -1708,11 +1678,6 @@ PacketType rcvDatagram() framesReceived++; int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); - //Reset receiveInProcess Babysitter - lastRIPBit0 = 0; - lastRIPBit1 = 0; - lastRIPBit3 = 0; - printPacketQuality(); //Display the RSSI, SNR and frequency error values if (state == RADIOLIB_ERR_NONE) @@ -3093,6 +3058,16 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) if (state == RADIOLIB_ERR_NONE) { + if (receiveInProcess() == true) + { + //Edge case: if we have started TX, but during the SPI transaction a preamble was detected + //then return false. This will cause the radio to transmit, then a transactionComplete ISR will trigger. + //The state machine will then process what the radio receives. The received datagram will be corrupt, + //or it will be successful. Either way, the read will clear the RxDone bit within the SX1276. + triggerEvent(TRIGGER_DOUBLE_INTERRUPT); + return (false); + } + xmitTimeMillis = millis(); triggerEvent(TRIGGER_TX_SPI_DONE); txSuccessMillis = xmitTimeMillis; @@ -3745,3 +3720,22 @@ void displayRadioCallHistory() } petWDT(); } + +//Read from radio to clear receiveInProcess bits +void dummyRead() +{ + triggerEvent(TRIGGER_DUMMY_READ); + systemPrintln("Dummy read"); + + int state = radio.readData(incomingBuffer, MAX_PACKET_SIZE); + + if (state != RADIOLIB_ERR_NONE) + { + if (settings.debug || settings.debugDatagrams || settings.debugReceive) + { + systemPrint("Receive error: "); + systemPrintln(state); + outputSerialData(true); + } + } +} diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 27159fdf..11571104 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -242,6 +242,8 @@ enum TRIGGER_BAD_PACKET, TRIGGER_CHANNEL_TIMER_ISR, TRIGGER_CRC_ERROR, + TRIGGER_DOUBLE_INTERRUPT, + TRIGGER_DUMMY_READ, TRIGGER_FREQ_CHANGE, TRIGGER_HANDSHAKE_COMPLETE, TRIGGER_HANDSHAKE_SEND_FIND_PARTNER_COMPLETE, From 2ac0c65e6565951ba000839b97866df9a4256449 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 20 Jan 2023 20:54:37 -0700 Subject: [PATCH 464/594] Remove extra triggers --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 1ba34611..6097ace2 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3064,7 +3064,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) //then return false. This will cause the radio to transmit, then a transactionComplete ISR will trigger. //The state machine will then process what the radio receives. The received datagram will be corrupt, //or it will be successful. Either way, the read will clear the RxDone bit within the SX1276. - triggerEvent(TRIGGER_DOUBLE_INTERRUPT); + triggerEvent(TRIGGER_RECEIVE_IN_PROCESS); return (false); } diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 11571104..881a4362 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -242,7 +242,7 @@ enum TRIGGER_BAD_PACKET, TRIGGER_CHANNEL_TIMER_ISR, TRIGGER_CRC_ERROR, - TRIGGER_DOUBLE_INTERRUPT, + TRIGGER_RECEIVE_IN_PROCESS, TRIGGER_DUMMY_READ, TRIGGER_FREQ_CHANGE, TRIGGER_HANDSHAKE_COMPLETE, @@ -257,10 +257,6 @@ enum TRIGGER_MP_TX_DATA, TRIGGER_NETID_MISMATCH, TRIGGER_RADIO_RESET, - TRIGGER_RECEIVE_IN_PROCESS, - TRIGGER_RECEIVE_IN_PROCESS_BIT0, - TRIGGER_RECEIVE_IN_PROCESS_BIT1, - TRIGGER_RECEIVE_IN_PROCESS_BIT3, TRIGGER_RETRANSMIT, TRIGGER_RETRANSMIT_FAIL, TRIGGER_RTR_255BYTE, From ce0dc04f6756c2805e433188b518e508a5780a46 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 21 Jan 2023 11:41:31 -1000 Subject: [PATCH 465/594] VC: Assign blocks of messages for third party use --- .../LoRaSerial_Firmware/Virtual_Circuit_Protocol.h | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 424d14e2..2ea80120 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -17,6 +17,10 @@ // // 0 - Data communications // 1 - Remote commands +// 2 - 3rd party commands +// 3 - Reserved +// 4 - Reserved +// 5 - 3rd Party PC responses // 6 - Remote responses // 7 - Special virtual circuits @@ -46,8 +50,10 @@ //As a result, any host may send commands to any other host! #define MIN_RX_NOT_ALLOWED VC_RSVD_SPECIAL_VCS #define PC_REMOTE_RESPONSE ((int8_t)(6 << VCAB_NUMBER_BITS)) -//Address spaces 2 - 5 are not currently defined -#define MIN_TX_NOT_ALLOWED ((int8_t)(2 << VCAB_NUMBER_BITS)) //Marker only +#define THIRD_PARTY_RESP ((int8_t)(5 << VCAB_NUMBER_BITS)) +//Address spaces 3 - 4 are not currently defined +#define MIN_TX_NOT_ALLOWED ((int8_t)(3 << VCAB_NUMBER_BITS)) //Marker only +#define THIRD_PARTY_COMMAND ((int8_t)(2 << VCAB_NUMBER_BITS)) #define PC_REMOTE_COMMAND ((int8_t)(1 << VCAB_NUMBER_BITS)) //Field offsets in the VC HEARTBEAT frame From 428f5d7031889733bb21615b63d881cfcac7bc97 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 21 Jan 2023 11:50:32 -1000 Subject: [PATCH 466/594] VcServerTest: Display unknown VC messages received from the radio --- Firmware/Tools/VcServerTest.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index bc7f9b56..b65940b5 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -463,11 +463,21 @@ int radioToHost() radioDataNack(data, length); //Display received messages + else if ((header->radio.destVc == myVc) || (header->radio.destVc == VC_BROADCAST)) + { + //Output this message + status = hostToStdout(data, length); + } + + //Unknown messages else { - if ((header->radio.destVc == myVc) || (header->radio.destVc == VC_BROADCAST)) - //Output this message - status = hostToStdout(data, length); + printf("Unknown message, VC Header:\n"); + printf(" length: %d\n", header->radio.length); + printf(" destVc: %d\n", header->radio.destVc); + printf(" srcVc: %d\n", header->radio.srcVc); + if (length > 0) + dumpBuffer(data, length); } //Continue processing the rest of the data in the buffer From 6b66cec6ee613dea6e456e1973045c284d749066 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 21 Jan 2023 18:54:53 -1000 Subject: [PATCH 467/594] VC: ATB changes state to RADIO_RESET as part of breaking the link --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 2a308c30..0cefc2b9 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -113,6 +113,7 @@ bool commandAT(const char * commandString) timer = millis(); while ((millis() - timer) < ((VC_LINK_BREAK_MULTIPLIER + 2) * settings.heartbeatTimeout)) petWDT(); + changeState(RADIO_RESET); //Display the end of the delay systemPrintln("Delay done"); From 2fa6dd03e654fb8367925dc078663d46ccef5a43 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 22 Jan 2023 19:12:16 -1000 Subject: [PATCH 468/594] Blink error LED and regularly output an error message Add an error message to waitForever --- Firmware/LoRaSerial_Firmware/Begin.ino | 7 +- Firmware/LoRaSerial_Firmware/Radio.ino | 52 ++++----------- Firmware/LoRaSerial_Firmware/States.ino | 21 +++--- Firmware/LoRaSerial_Firmware/System.ino | 86 +++++++++++++++++-------- 4 files changed, 81 insertions(+), 85 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 0cf06f9a..ed64b20e 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -34,12 +34,7 @@ void beginLoRa() { systemPrint("Radio init failed with code: "); systemPrintln(state); - outputSerialData(true); - while (1) - { - petWDT(); - delay(100); - } + waitForever("Radio init failed!"); } changeState(RADIO_RESET); diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index b1941d43..2b980e24 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -459,7 +459,7 @@ void generateHopTable() { systemPrint("ERROR - Wrong AES IV size in bytes, please set AES_IV_BYTES = "); systemPrintln(gcm.ivSize()); - waitForever(); + waitForever("ERROR - Fix the AES_IV_BYTES value!"); } //Verify the AES key length @@ -467,7 +467,7 @@ void generateHopTable() { systemPrint("ERROR - Wrong AES key size in bytes, please set AES_KEY_BYTES = "); systemPrintln(gcm.keySize()); - waitForever(); + waitForever("ERROR - Fix the AES_KEY_BYTES value!"); } //Set new initial values for AES using settings based random seed @@ -861,11 +861,7 @@ bool xmitDatagramP2PFindPartner() //Verify the data length if ((endOfTxData - startOfData) != P2P_FIND_PARTNER_BYTES) - { - systemPrintln("ERROR - Fix the P2P_FIND_PARTNER_BYTES value!"); - outputSerialData(true); - waitForever(); - } + waitForever("ERROR - Fix the P2P_FIND_PARTNER_BYTES value!"); txControl.datagramType = DATAGRAM_FIND_PARTNER; return (transmitDatagram()); @@ -903,11 +899,7 @@ bool xmitDatagramP2PSyncClocks() //Verify the data length if ((endOfTxData - startOfData) != P2P_SYNC_CLOCKS_BYTES) - { - systemPrintln("ERROR - Fix the P2P_SYNC_CLOCKS_BYTES value!"); - outputSerialData(true); - waitForever(); - } + waitForever("ERROR - Fix the P2P_SYNC_CLOCKS_BYTES value!"); txControl.datagramType = DATAGRAM_SYNC_CLOCKS; return (transmitDatagram()); @@ -938,11 +930,7 @@ bool xmitDatagramP2PZeroAcks() //Verify the data length if ((endOfTxData - startOfData) != P2P_ZERO_ACKS_BYTES) - { - systemPrintln("ERROR - Fix the P2P_ZERO_ACKS_BYTES value!"); - outputSerialData(true); - waitForever(); - } + waitForever("ERROR - Fix the P2P_ZERO_ACKS_BYTES value!"); txControl.datagramType = DATAGRAM_ZERO_ACKS; return (transmitDatagram()); @@ -1037,11 +1025,7 @@ bool xmitDatagramP2PHeartbeat() //Verify the data length if ((endOfTxData - startOfData) != P2P_HEARTBEAT_BYTES) - { - systemPrintln("ERROR - Fix the P2P_HEARTBEAT_BYTES value!"); - outputSerialData(true); - waitForever(); - } + waitForever("ERROR - Fix the P2P_HEARTBEAT_BYTES value!"); txControl.datagramType = DATAGRAM_HEARTBEAT; return (transmitDatagram()); @@ -1072,11 +1056,7 @@ bool xmitDatagramP2PAck() //Verify the data length if ((endOfTxData - startOfData) != P2P_ACK_BYTES) - { - systemPrintln("ERROR - Fix the P2P_ACK_BYTES value!"); - outputSerialData(true); - waitForever(); - } + waitForever("ERROR - Fix the P2P_ACK_BYTES value!"); txControl.datagramType = DATAGRAM_DATA_ACK; return (transmitDatagram()); @@ -1128,11 +1108,7 @@ bool xmitDatagramMpHeartbeat() //Verify the data length if ((endOfTxData - startOfData) != MP_HEARTBEAT_BYTES) - { - systemPrintln("ERROR - Fix the MP_HEARTBEAT_BYTES value!"); - outputSerialData(true); - waitForever(); - } + waitForever("ERROR - Fix the MP_HEARTBEAT_BYTES value!"); txControl.datagramType = DATAGRAM_HEARTBEAT; return (transmitDatagram()); @@ -1402,11 +1378,7 @@ bool xmitVcHeartbeat(int8_t addr, uint8_t * id) //Verify the data length if ((endOfTxData - startOfData) != VC_HEARTBEAT_BYTES) - { - systemPrintln("ERROR - Fix the VC_HEARTBEAT_BYTES value!"); - outputSerialData(true); - waitForever(); - } + waitForever("ERROR - Fix the VC_HEARTBEAT_BYTES value!"); txControl.datagramType = DATAGRAM_VC_HEARTBEAT; txControl.ackNumber = 0; @@ -3641,14 +3613,14 @@ const I16_TO_STRING radioCallName[] = }; //Verify the RADIO_CALLS enum against the radioCallName -bool verifyRadioCallNames() +const char * verifyRadioCallNames() { bool valid; valid = ((sizeof(radioCallName) / sizeof(radioCallName[0])) == RADIO_CALL_MAX); if (!valid) - systemPrintln("ERROR - Please update the radioCallName"); - return valid; + return "ERROR - Please update the radioCallName"; + return NULL; } //Convert a radio call value into a string, return NULL if not found diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 7eef9d66..5d21551e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -79,7 +79,7 @@ void updateRadioState() { systemPrint("Unknown state: "); systemPrintln(radioState); - waitForever(); + waitForever("ERROR - Unknown radio state!"); } break; @@ -2364,7 +2364,7 @@ bool isMultiPointSync() } //Verify the radio state definitions against the radioStateTable -bool verifyRadioStateTable() +const char * verifyRadioStateTable() { int expectedState; unsigned int i; @@ -2375,10 +2375,8 @@ bool verifyRadioStateTable() int * order; unsigned int tableEntries; int temp; - int valid; //Verify that all the entries are in the state table - valid = true; tableEntries = sizeof(radioStateTable) / sizeof(radioStateTable[0]); for (index = 0; index < tableEntries; index++) { @@ -2394,10 +2392,7 @@ bool verifyRadioStateTable() //Try to build a valid table order = (int *)malloc(tableEntries * sizeof(*order)); if (!order) - { - systemPrintln("ERROR - Failed to allocate the order table from the heap!"); - waitForever(); - } + waitForever("ERROR - Failed to allocate the order table from the heap!"); //Assume the table is in the correct order maxDescriptionLength = 0; @@ -2532,20 +2527,20 @@ bool verifyRadioStateTable() systemPrintln(expectedState++); } systemPrintln("};"); - valid = false; + return "ERROR - Please update the radioStateTable"; } - return valid; + return NULL; } //Verify the PacketType enums against the radioDatagramType -bool verifyRadioDatagramType() +const char * verifyRadioDatagramType() { bool valid; valid = ((sizeof(radioDatagramType) / sizeof(radioDatagramType[0])) == MAX_DATAGRAM_TYPE); if (!valid) - systemPrintln("ERROR - Please update the radioDatagramTable"); - return valid; + return "ERROR - Please update the radioDatagramTable"; + return NULL; } //Change states and print the new state diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 451954af..8f9d6567 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -312,17 +312,14 @@ void systemReset() } //Display any debug serial data and then loop forever -void waitForever() +void waitForever(const char * errorMessage) { - //Output the remaining serial data - outputSerialData(true); - - //Empty the USB serial device - systemFlush(); - //Wait forever while (1) + { petWDT(); + blinkErrorLed(errorMessage); + } } //Encrypt a given array in place @@ -886,6 +883,38 @@ void blinkChannelHopLed(bool on) } } +//Blink the error LED +void blinkErrorLed(const char * errorMessage) +{ + uint32_t currentMillis; + static uint32_t lastToggleMillis; + static uint8_t flashCount; + +#define ERROR_FLASHS_PER_SECOND 10 +#define DELAY_BETWEEN_MESSAGES_SECS 15 + + //Ouptut the error message on a regular interval + if (!flashCount) + { + flashCount = DELAY_BETWEEN_MESSAGES_SECS * (ERROR_FLASHS_PER_SECOND << 1); + if (errorMessage) + { + systemPrintln(errorMessage); + outputSerialData(true); + } + } + + //Determine if it is time to toggle the error LED + currentMillis = millis(); + if ((currentMillis - lastToggleMillis) >= (1000 / (ERROR_FLASHS_PER_SECOND << 1))) + { + //Toggle the error LED + lastToggleMillis = currentMillis; + digitalWrite(YELLOW_LED, !digitalRead(YELLOW_LED)); + flashCount -= 1; + } +} + //Display the multi-point LED pattern void multiPointLeds() { @@ -1173,39 +1202,44 @@ int16_t getReceiveCompletionOffset() } //Verify the VC_STATE_TYPE enums against the vcStateNames table -bool verifyVcStateNames() +const char * verifyVcStateNames() { bool valid; //Verify the length of the vcStateNames table valid = (VC_STATE_MAX == (sizeof(vcStateNames) / sizeof(vcStateNames[0]))); if (!valid) - systemPrintln("ERROR: Fix difference between VC_STATE_TYPE and vcStateNames"); - return valid; + return "ERROR: Fix difference between VC_STATE_TYPE and vcStateNames"; + return NULL; } //Verify the enums .vs. name tables, stop on failure to force software fix void verifyTables() { - bool valid; - - valid = true; + const char * errorMessage; - //Verify that the state table contains all of the states in increasing order - valid &= verifyRadioStateTable(); + do + { + //Verify that the state table contains all of the states in increasing order + errorMessage = verifyRadioStateTable(); + if (errorMessage) + break; - //Verify that the datagram type table contains all of the datagram types - valid &= verifyRadioDatagramType(); + //Verify that the datagram type table contains all of the datagram types + errorMessage = verifyRadioDatagramType(); + if (errorMessage) + break; - //Verify the VC state name table - valid &= verifyVcStateNames(); + //Verify the VC state name table + errorMessage = verifyVcStateNames(); + if (errorMessage) + break; - //Verify the RADIO_CALLS enum against the radioCallName - valid &= verifyRadioCallNames(); + //Verify the RADIO_CALLS enum against the radioCallName + errorMessage = verifyRadioCallNames(); + } while (0); - if (!valid) - { - outputSerialData(true); - waitForever(); - } + //Handle the error + if (errorMessage) + waitForever(errorMessage); } From ac54bffdc5962456acbefbbb4badbd49dedc7e3d Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 13:05:27 -0700 Subject: [PATCH 469/594] Change Serial.print to systemPrint --- Firmware/LoRaSerial_Firmware/Begin.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial_Firmware/Begin.ino index 0cf06f9a..e926ae35 100644 --- a/Firmware/LoRaSerial_Firmware/Begin.ino +++ b/Firmware/LoRaSerial_Firmware/Begin.ino @@ -89,7 +89,7 @@ void petWDT() void beginChannelTimer() { if (channelTimer.attachInterruptInterval_MS(settings.maxDwellTime, channelTimerHandler) == false) - Serial.println("Error starting ChannelTimer!"); + systemPrintln("Error starting ChannelTimer!"); stopChannelTimer(); //Start timer in state machine - beginChannelTimer } From ada133b0c9d332fc18532c3c2dc35b0070542147 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 13:07:18 -0700 Subject: [PATCH 470/594] Add channelNumber move to MP linkup --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 3 +++ Firmware/LoRaSerial_Firmware/States.ino | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 60a33275..cdab7253 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -64,7 +64,7 @@ const int FIRMWARE_VERSION_MINOR = 0; #define UNIQUE_ID_BYTES 16 //Number of bytes in the unique ID //Frame lengths -#define MP_HEARTBEAT_BYTES 0 //Number of data bytes in the MP_HEARTBEAT frame +#define MP_HEARTBEAT_BYTES 1 //Number of data bytes in the MP_HEARTBEAT frame #define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame #define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6097ace2..ceadf4bd 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1124,6 +1124,9 @@ bool xmitDatagramMpHeartbeat() startOfData = endOfTxData; + memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); + endOfTxData += sizeof(channelNumber); + /* endOfTxData ---. | diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 1dcd753f..951772e4 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1265,6 +1265,9 @@ void updateRadioState() syncChannelTimer(txHeartbeatUsec); systemPrint("HEARTBEAT TX mSec: "); systemPrintln(frameAirTime); + + //Move to this new channel + channelNumber = rxData[0]; } //Received heartbeat - do not ack. From 3de8c0d22ece1397db6dc7ae4b95f9ca23972d68 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 13:08:09 -0700 Subject: [PATCH 471/594] Moving print to within debug guard --- Firmware/LoRaSerial_Firmware/States.ino | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 951772e4..69ea3adb 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1263,8 +1263,11 @@ void updateRadioState() //Adjust freq hop ISR based on server's remaining clock syncChannelTimer(txHeartbeatUsec); - systemPrint("HEARTBEAT TX mSec: "); - systemPrintln(frameAirTime); + if (settings.debugSync) + { + systemPrint("HEARTBEAT TX mSec: "); + systemPrintln(frameAirTime); + } //Move to this new channel channelNumber = rxData[0]; From 5c48f0e432049a22b2edec0779e2bca978d4abfe Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 13:09:53 -0700 Subject: [PATCH 472/594] Move dumpClockSynchronization() within debug guard --- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 69ea3adb..4d43a89a 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1339,8 +1339,8 @@ void updateRadioState() { systemPrintln("HEARTBEAT Timeout"); outputSerialData(true); + dumpClockSynchronization(); } - dumpClockSynchronization(); changeState(RADIO_DISCOVER_BEGIN); } } From 2a682d7836e1d8a511e52361ca8d11887d202cb3 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 13:21:36 -0700 Subject: [PATCH 473/594] Fix bug in RADIO_DISCOVER_SCANNING. Remove channel number from heartbeat. --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 3 --- Firmware/LoRaSerial_Firmware/States.ino | 3 ++- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index cdab7253..60a33275 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -64,7 +64,7 @@ const int FIRMWARE_VERSION_MINOR = 0; #define UNIQUE_ID_BYTES 16 //Number of bytes in the unique ID //Frame lengths -#define MP_HEARTBEAT_BYTES 1 //Number of data bytes in the MP_HEARTBEAT frame +#define MP_HEARTBEAT_BYTES 0 //Number of data bytes in the MP_HEARTBEAT frame #define P2P_FIND_PARTNER_BYTES sizeof(unsigned long) //Number of data bytes in the FIND_PARTNER frame #define P2P_SYNC_CLOCKS_BYTES (sizeof(uint8_t) + sizeof(unsigned long)) //Number of data bytes in the SYNC_CLOCKS frame #define P2P_ZERO_ACKS_BYTES sizeof(unsigned long) //Number of data bytes in the ZERO_ACKS frame diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index ceadf4bd..6097ace2 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -1124,9 +1124,6 @@ bool xmitDatagramMpHeartbeat() startOfData = endOfTxData; - memcpy(endOfTxData, &channelNumber, sizeof(channelNumber)); - endOfTxData += sizeof(channelNumber); - /* endOfTxData ---. | diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 4d43a89a..2b8411c6 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1089,7 +1089,8 @@ void updateRadioState() if (!settings.server) { //Change to the server's channel number - channelNumber = rxVcData[0]; + channelNumber = rxData[0]; + setRadioFrequency(false); //Update the timestamp COMPUTE_TIMESTAMP_OFFSET(rxData + 1, 0, txSyncClocksUsec); From e28295615ae85936492a311c2282deb4788ae5b8 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 13:24:30 -0700 Subject: [PATCH 474/594] Remove channelNumber update from HEARTBEAT recv. --- Firmware/LoRaSerial_Firmware/States.ino | 3 --- 1 file changed, 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 2b8411c6..838fae72 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1269,9 +1269,6 @@ void updateRadioState() systemPrint("HEARTBEAT TX mSec: "); systemPrintln(frameAirTime); } - - //Move to this new channel - channelNumber = rxData[0]; } //Received heartbeat - do not ack. From 3cd07c63f4e588086680f8d10199c75d6fa71f9b Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 13:36:00 -0700 Subject: [PATCH 475/594] MP: Reset heartbeat timeout when in standby. --- Firmware/LoRaSerial_Firmware/States.ino | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 838fae72..33dd7628 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1278,6 +1278,8 @@ void updateRadioState() lastPacketReceived = millis(); //Update timestamp for Link LED + setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer + blinkHeartbeatLed(true); changeState(RADIO_MP_STANDBY); break; From fd9db677cfc241e6992b4311feb7c4b33a1777db Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 14:29:44 -0700 Subject: [PATCH 476/594] Use Modem Clear and RX On-going for receiveInProcess --- Firmware/LoRaSerial_Firmware/Radio.ino | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6097ace2..f7fa9898 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -583,25 +583,10 @@ bool receiveInProcess() { uint8_t radioStatus = radio.getModemStatus(); - //If any bits are set there is a receive in process - if (radioStatus & RECEIVE_IN_PROCESS_MASK) - { - if ((lastModemStatus & RECEIVE_IN_PROCESS_MASK) == 0) - lastReceiveInProcessTrue = millis(); - - if (millis() - lastReceiveInProcessTrue > maxPacketAirTime) - { - //We received valid header, with a packet that never completed - //or we received a preamble that ended, with a packet that never completed - //or we received the start of a preamble that never ended - dummyRead(); - } - - return (true); - } - - lastModemStatus = radioStatus; - return (false); //No receive in process + //If Modem Clear or RX On-going are set, no receive in process + if (radioStatus & 0b10100) //Check Modem Clear and RX On-going bits + return (false); //No receive in process + return (true); } //Convert the user's requested dBm to what the radio should be set to, to hit that power level From a799d077ad4e9fa2ce188e41dc8e9910cbb7caf6 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 15:40:55 -0700 Subject: [PATCH 477/594] Change receiveInprogress to test for known good states. --- Firmware/LoRaSerial_Firmware/Radio.ino | 29 +++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index f7fa9898..20408ea7 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -582,11 +582,30 @@ unsigned long mSecToChannelZero() bool receiveInProcess() { uint8_t radioStatus = radio.getModemStatus(); - - //If Modem Clear or RX On-going are set, no receive in process - if (radioStatus & 0b10100) //Check Modem Clear and RX On-going bits - return (false); //No receive in process - return (true); + radioStatus &= 0b11011; //Get bits 0, 1, 3, and 4 + + //Known states where a reception is in progress + if (radioStatus == 0b00001) + return (true); + else if (radioStatus == 0b00011) + return (true); + else if (radioStatus == 0b01011) + return (true); + +// switch (radioStatus) +// { +// default: +// Serial.print("Unknown status: 0b"); +// Serial.println(radioStatus, BIN); +// break; +// case (0b00000): +// //No receive in process +// case (0b10000): +// //Modem clear. No receive in process +// break; +// } + + return (false); } //Convert the user's requested dBm to what the radio should be set to, to hit that power level From ded63560cb659a4fc1115bc3dac8fbef1a7c7f0d Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 15:42:55 -0700 Subject: [PATCH 478/594] Add comment --- Firmware/LoRaSerial_Firmware/Radio.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 20408ea7..681df3dc 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3506,6 +3506,7 @@ void setHeartbeatLong() } //Only the server sends heartbeats in multipoint mode +//But the clients still need to update their timeout //Not random, just the straight timeout void setHeartbeatMultipoint() { From 0028645612d64a3ee4dcd553b1212f6ec19c6ba5 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 23 Jan 2023 20:03:31 -0700 Subject: [PATCH 479/594] Add MP to uptime calc --- Firmware/LoRaSerial_Firmware/States.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 33dd7628..23628c69 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2587,7 +2587,7 @@ void displayState(RadioStates newState) systemPrint(radioStateTable[radioState].name); } - if (newState == RADIO_P2P_LINK_UP) + if (newState == RADIO_P2P_LINK_UP || newState == RADIO_MP_STANDBY) { unsigned int seconds = (millis() - lastLinkUpTime) / 1000; unsigned int minutes = seconds / 60; From 955fffb7b3a61ff9b430b42c70b65de51b76732c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 23 Jan 2023 17:54:49 -1000 Subject: [PATCH 480/594] MP: Remove display of uninitialized variable --- Firmware/LoRaSerial_Firmware/States.ino | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 23628c69..ef2bc58b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1259,17 +1259,8 @@ void updateRadioState() case DATAGRAM_HEARTBEAT: //Sync clock if server sent the heartbeat if (settings.server == false) - { - uint16_t frameAirTimeMsec; - //Adjust freq hop ISR based on server's remaining clock syncChannelTimer(txHeartbeatUsec); - if (settings.debugSync) - { - systemPrint("HEARTBEAT TX mSec: "); - systemPrintln(frameAirTime); - } - } //Received heartbeat - do not ack. triggerEvent(TRIGGER_RX_HEARTBEAT); From 7a7a0c218c37d7293cf0b74a6c25dff9ac9dda20 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 25 Jan 2023 15:08:55 -0700 Subject: [PATCH 481/594] Add link uptime timer to MP --- Firmware/LoRaSerial_Firmware/States.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 23628c69..eb14746f 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1027,6 +1027,9 @@ void updateRadioState() multipointChannelLoops = 0; multipointAttempts = 0; + //Mark start time for uptime calculation + lastLinkUpTime = millis(); + triggerEvent(TRIGGER_MP_SCAN); changeState(RADIO_DISCOVER_SCANNING); break; From fadaaa78a5a181b0d4a0add0ce6ca1cf8bcc6305 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 25 Jan 2023 19:51:53 -0700 Subject: [PATCH 482/594] Fix developer guards --- Firmware/LoRaSerial_Firmware/settings.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 881a4362..3d1e19b5 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -396,7 +396,7 @@ typedef struct struct_settings { bool encryptData = true; //AES encrypt each packet uint8_t encryptionKey[AES_KEY_BYTES] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; bool dataScrambling = false; //Use IBM Data Whitening to reduce DC bias -#if defined(ENABLE_DEVELOPER) +#if (ENABLE_DEVELOPER == true) #define TX_POWER_DB 14 #else //ENABLE_DEVELOPER #define TX_POWER_DB 30 @@ -424,7 +424,7 @@ typedef struct struct_settings { bool debugRadio = false; //Print radio info bool debugStates = false; //Print state changes bool debugTraining = false; //Print training info -#if defined(ENABLE_DEVELOPER) +#if (ENABLE_DEVELOPER == true) #define WAIT_SERIAL_DEFAULT true #else //ENABLE_DEVELOPER #define WAIT_SERIAL_DEFAULT false From b5c2d7b185c92c6f03c41b678849e9057ea3f451 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 25 Jan 2023 21:24:18 -0700 Subject: [PATCH 483/594] Change heartbeat timeouts to timed transmits. --- Firmware/LoRaSerial_Firmware/Radio.ino | 33 +++++++++++++------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index f79aecce..8ab8df3f 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -592,18 +592,18 @@ bool receiveInProcess() else if (radioStatus == 0b01011) return (true); -// switch (radioStatus) -// { -// default: -// Serial.print("Unknown status: 0b"); -// Serial.println(radioStatus, BIN); -// break; -// case (0b00000): -// //No receive in process -// case (0b10000): -// //Modem clear. No receive in process -// break; -// } + // switch (radioStatus) + // { + // default: + // Serial.print("Unknown status: 0b"); + // Serial.println(radioStatus, BIN); + // break; + // case (0b00000): + // //No receive in process + // case (0b10000): + // //Modem clear. No receive in process + // break; + // } return (false); } @@ -3462,7 +3462,7 @@ void setHeartbeatShort() //Slow datarates can have significant ack transmission times //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + heartbeatRandomTime += (txHeartbeatUsec / 1000) + (txDataAckUsec / 1000) + settings.overheadTime + getReceiveCompletionOffset(); } void setHeartbeatLong() @@ -3474,7 +3474,7 @@ void setHeartbeatLong() //Slow datarates can have significant ack transmission times //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + heartbeatRandomTime += (txHeartbeatUsec / 1000) + (txDataAckUsec / 1000) + settings.overheadTime + getReceiveCompletionOffset(); } //Only the server sends heartbeats in multipoint mode @@ -3487,9 +3487,8 @@ void setHeartbeatMultipoint() heartbeatRandomTime = settings.heartbeatTimeout; - //Slow datarates can have significant ack transmission times - //Add the amount of time it takes to send an ack - heartbeatRandomTime += frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + //MP does not use acks + heartbeatRandomTime += (txHeartbeatUsec / 1000) + settings.overheadTime + getReceiveCompletionOffset(); } //Determine the delay for the next VC HEARTBEAT From 0d2dc36eca46125e2766e7fbbea5650ecd74f6ce Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 25 Jan 2023 21:24:39 -0700 Subject: [PATCH 484/594] Fix bug in return of getReceiveCompletionOffset --- Firmware/LoRaSerial_Firmware/System.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 8f9d6567..45247f75 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -1179,6 +1179,7 @@ int16_t getReceiveCompletionOffset() switch (settings.airSpeed) { default: + return (0); break; case (40): return (26); //Tsym: 32. Measured: 26 ms between a TX complete and the RX complete From f2472bf7c5f65dfdfbe29eeb79913548f69d361f Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 27 Jan 2023 13:27:46 -0700 Subject: [PATCH 485/594] Add debug guard to clocksyncDataDump() --- Firmware/LoRaSerial_Firmware/States.ino | 55 +++++++++++++------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d4e9784a..5846ca95 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2646,36 +2646,39 @@ void displayRadioStateHistory() //Dump the clock synchronization data void dumpClockSynchronization() { - //Dump the clock sync data - petWDT(); - for (uint8_t x = 0; x < (sizeof(clockSyncData) / sizeof(clockSyncData[0])); x++) + if (settings.debugSync) { - uint8_t index = (x + clockSyncIndex) % (sizeof(clockSyncData) / sizeof(clockSyncData[0])); - if (clockSyncData[index].frameAirTimeMsec) + //Dump the clock sync data + petWDT(); + for (uint8_t x = 0; x < (sizeof(clockSyncData) / sizeof(clockSyncData[0])); x++) { - systemPrint("Lcl: "); - systemPrint(clockSyncData[index].lclHopTimeMsec); - systemPrint(", Rmt: "); - systemPrint(clockSyncData[index].msToNextHopRemote); - systemPrint(" - "); - systemPrint(clockSyncData[index].frameAirTimeMsec); - systemPrint(" = "); - systemPrint(clockSyncData[index].msToNextHopRemote - clockSyncData[index].frameAirTimeMsec); - systemPrint(" + "); - systemPrint(clockSyncData[index].adjustment); - systemPrint(" = "); - systemPrint(clockSyncData[index].msToNextHop); - systemPrint(" msToNextHop"); - if (clockSyncData[index].delayedHopCount) + uint8_t index = (x + clockSyncIndex) % (sizeof(clockSyncData) / sizeof(clockSyncData[0])); + if (clockSyncData[index].frameAirTimeMsec) { - systemPrint(", timeToHop: "); - systemPrint(clockSyncData[index].timeToHop); - systemPrint(", Hops: "); - systemPrint(clockSyncData[index].delayedHopCount); + systemPrint("Lcl: "); + systemPrint(clockSyncData[index].lclHopTimeMsec); + systemPrint(", Rmt: "); + systemPrint(clockSyncData[index].msToNextHopRemote); + systemPrint(" - "); + systemPrint(clockSyncData[index].frameAirTimeMsec); + systemPrint(" = "); + systemPrint(clockSyncData[index].msToNextHopRemote - clockSyncData[index].frameAirTimeMsec); + systemPrint(" + "); + systemPrint(clockSyncData[index].adjustment); + systemPrint(" = "); + systemPrint(clockSyncData[index].msToNextHop); + systemPrint(" msToNextHop"); + if (clockSyncData[index].delayedHopCount) + { + systemPrint(", timeToHop: "); + systemPrint(clockSyncData[index].timeToHop); + systemPrint(", Hops: "); + systemPrint(clockSyncData[index].delayedHopCount); + } + systemPrintln(); + outputSerialData(true); + petWDT(); } - systemPrintln(); - outputSerialData(true); - petWDT(); } } } From 2740e7736799772573cc2516f235ee45c2ec8619 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 27 Jan 2023 13:30:18 -0700 Subject: [PATCH 486/594] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 01f377f7..2c681136 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ ehthumbs.db *.b#? *.l#? *.lck +.vscode/* +/Firmware/build/* # Folder config file Desktop.ini From 39dddcdc2a87c10b63b91a68fa1f6bc5387797cb Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Fri, 27 Jan 2023 13:30:58 -0700 Subject: [PATCH 487/594] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2c681136..e57e1b2a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ ehthumbs.db *.l#? *.lck .vscode/* +/Firmware/LoRaSerial_Firmware/.vscode/* /Firmware/build/* # Folder config file From 22b99f9ebc2dd610d9e659b22b73349d735d3a62 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 28 Jan 2023 15:48:15 -0700 Subject: [PATCH 488/594] Fix calcAirTime to use LDRO and Implicit header. --- Firmware/LoRaSerial_Firmware/Radio.ino | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 8ab8df3f..017b46ea 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -259,6 +259,7 @@ void returnToReceiving() } //Given spread factor, bandwidth, coding rate and number of bytes, return total Air Time in ms for packet +//See datasheet for the nPayload formula / section 4.1.1.6 float calcAirTimeUsec(uint8_t bytesToSend) { radioCallHistory[RADIO_CALL_calcAirTimeUsec] = millis(); @@ -268,9 +269,20 @@ float calcAirTimeUsec(uint8_t bytesToSend) //See Synchronization section float tPreambleUsec = (settings.radioPreambleLength + 2 + 2.25) * tSymUsec; - // Rb = SF * (1 / ((2^SF) / B) = (SF * B) / 2^SF bit rate - // - float p1 = (8 * bytesToSend - 4 * settings.radioSpreadFactor + 28 + 16 * 1 - 20 * 0) / (4.0 * (settings.radioSpreadFactor - 2 * 0)); + const uint8_t useHardwareCRC = 1; //0 to disable + + //With SF = 6 selected, implicit header mode is the only mode of operation possible. + uint8_t explicitHeader = 0; //1 for implicit header + if(settings.radioSpreadFactor == 6) explicitHeader = 1; + + //LowDataRateOptimize increases the robustness of the LoRa link at low effective data rates. Its use is mandated when the symbol duration exceeds 16ms. + uint8_t useLowDataRateOptimization = 0; //0 to disable. + + //We choose to enable LDRO for airSpeed of 400 even though TSym is 8.2ms. + if(settings.airSpeed <= 400 && settings.airSpeed >= 40) useLowDataRateOptimization = 1; + if(tSymUsec > 16000) useLowDataRateOptimization = 1; //Catch custom bandwidth/spread/coding setups + + float p1 = (8 * bytesToSend - 4 * settings.radioSpreadFactor + 28 + 16 * useHardwareCRC - 20 * explicitHeader) / (4.0 * (settings.radioSpreadFactor - 2 * useLowDataRateOptimization)); p1 = ceil(p1) * settings.radioCodingRate; if (p1 < 0) p1 = 0; uint16_t payloadBytes = 8 + p1; From bd25d321d214b64059c0afaf370df79c5b7a1aea Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 28 Jan 2023 15:48:33 -0700 Subject: [PATCH 489/594] Fix roll-over bug in syncChannelTimer --- Firmware/LoRaSerial_Firmware/Radio.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 017b46ea..097b952b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3404,6 +3404,9 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) //Compute the next hop time msToNextHop = rmtHopTimeMsec + adjustment; + //For low airspeeds multiple hops may occur resulting in a negative value + while(msToNextHop < 0) msToNextHop += settings.maxDwellTime; + //When the ISR fires, reload the channel timer with settings.maxDwellTime reloadChannelTimer = true; From aed67ebc70f032750ef14e66285ffdf368830c61 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 28 Jan 2023 15:48:57 -0700 Subject: [PATCH 490/594] Fix starting channel number --- Firmware/LoRaSerial_Firmware/States.ino | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 5846ca95..a0286f43 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2731,6 +2731,10 @@ void enterLinkUp() //Bring up the link triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); + + //For very low airspeeds (150 and below) the hop timer will expire during the during handshake. + //Reset channel number to insure both radios are on same channel. + channelNumber = 0; hopChannel(); //Leave home //Synchronize the ACK numbers From 5e4b620006b03f8e85e37ef1e245d998e6827ef2 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Sat, 28 Jan 2023 16:24:00 -0700 Subject: [PATCH 491/594] Allow msToNextHop to be negative --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 097b952b..03032521 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3218,7 +3218,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) int8_t delayedHopCount; uint16_t frameAirTimeMsec; int16_t lclHopTimeMsec; - uint16_t msToNextHop; + int16_t msToNextHop; int16_t rmtHopTimeMsec; currentMillis = millis(); From a13b4aaebdbdb3f0ceecba1f2cef8f8aaebe0f17 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 27 Jan 2023 14:22:35 -1000 Subject: [PATCH 492/594] VC: Process one command at a time --- Firmware/LoRaSerial_Firmware/Serial.ino | 55 +++++++++++-------------- 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 7bbe02be..fe7851db 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -17,6 +17,8 @@ bool isCTS() return (digitalRead(pin_cts) == HIGH) ^ settings.invertCts; } +#define NEXT_RX_TAIL(n) ((rxTail + n) % sizeof(serialReceiveBuffer)) + //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= //Serial TX - Data being sent to the USB or serial port //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -74,6 +76,8 @@ uint16_t availableRadioTXBytes() return (sizeof(radioTxBuffer) - radioTxTail + radioTxHead); } +#define NEXT_RADIO_TX_HEAD(n) ((radioTxHead + n) % sizeof(radioTxBuffer)) + //If we have data to send, get the packet ready //Return true if new data is ready to be sent bool processWaitingSerial(bool sendNow) @@ -692,10 +696,10 @@ void vcProcessSerialInput() if (timeToHop == true) hopChannel(); //Skip any garbage in the input stream - data = serialReceiveBuffer[rxTail++]; - rxTail %= sizeof(radioTxBuffer); + data = serialReceiveBuffer[rxTail]; if (data != START_OF_VC_SERIAL) { + rxTail = NEXT_RX_TAIL(1); if (settings.debugSerial) { systemPrint("vcProcessSerialInput discarding 0x"); @@ -711,8 +715,8 @@ void vcProcessSerialInput() //Get the virtual circuit header //The start byte has already been removed - length = serialReceiveBuffer[rxTail]; - if (length <= VC_SERIAL_HEADER_BYTES) + length = serialReceiveBuffer[NEXT_RX_TAIL(1)]; + if (length < VC_SERIAL_HEADER_BYTES) { if (settings.debugSerial) { @@ -729,26 +733,17 @@ void vcProcessSerialInput() continue; } - //Skip if there is not enough data - if (dataBytes < length) - { - if (settings.debugSerial) - { - systemPrint("ERROR - Invalid length "); - systemPrint(length); - systemPrint(", discarding 0x"); - systemPrint(data, HEX); - systemPrintln(); - outputSerialData(true); - } - dataBytes = availableRXBytes(); - continue; - } + //Skip if there is not enough data in the buffer + if (dataBytes < (length + 1)) + break; + + //Remove the START_OF_VC_SERIAL byte + rxTail = NEXT_RX_TAIL(1); //Get the source and destination virtual circuits - index = (rxTail + 1) % sizeof(serialReceiveBuffer); + index = NEXT_RX_TAIL(1); vcDest = serialReceiveBuffer[index]; - index = (rxTail + 2) % sizeof(serialReceiveBuffer); + index = NEXT_RX_TAIL(2); vcSrc = serialReceiveBuffer[index]; //Process this message @@ -769,7 +764,7 @@ void vcProcessSerialInput() } //Discard this message - rxTail = (rxTail + length) % sizeof(radioTxBuffer); + rxTail = NEXT_RX_TAIL(length); break; } @@ -785,7 +780,7 @@ void vcProcessSerialInput() } //Discard this message - rxTail = (rxTail + length) % sizeof(radioTxBuffer); + rxTail = NEXT_RX_TAIL(length + 1); break; } @@ -801,9 +796,9 @@ void vcProcessSerialInput() //Place the data in radioTxBuffer for (; length > 0; length--) { - radioTxBuffer[radioTxHead++] = serialReceiveBuffer[rxTail++]; - radioTxHead %= sizeof(radioTxBuffer); - rxTail %= sizeof(serialReceiveBuffer); + radioTxBuffer[radioTxHead] = serialReceiveBuffer[rxTail]; + rxTail = NEXT_RX_TAIL(1); + radioTxHead = NEXT_RADIO_TX_HEAD(1); } break; @@ -821,19 +816,19 @@ void vcProcessSerialInput() } //Discard this message - rxTail = (rxTail + length) % sizeof(radioTxBuffer); + rxTail = NEXT_RX_TAIL(length); break; } //Discard the VC header length -= VC_RADIO_HEADER_BYTES; - rxTail = (rxTail + VC_RADIO_HEADER_BYTES) % sizeof(radioTxBuffer); + rxTail = NEXT_RX_TAIL(VC_RADIO_HEADER_BYTES); //Move this message into the command buffer for (cmd = commandBuffer; length > 0; length--) { - *cmd++ = toupper(serialReceiveBuffer[rxTail++]); - rxTail %= sizeof(serialReceiveBuffer); + *cmd++ = toupper(serialReceiveBuffer[rxTail]); + rxTail = NEXT_RX_TAIL(1); } commandLength = cmd - commandBuffer; From c2f26e7d0e32210f4b6304bf5f64d7ab9c12acd6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 28 Jan 2023 06:53:35 -1000 Subject: [PATCH 493/594] VC: Send command complete to the PC --- Firmware/LoRaSerial_Firmware/Commands.ino | 21 ++++++++++++++++++- .../Virtual_Circuit_Protocol.h | 11 +++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 0cefc2b9..18ee8cc0 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -808,6 +808,9 @@ void checkCommand() for (index = 0; index < commandLength; index++) commandString[index] = toupper(commandString[index]); + //Echo the command + systemPrintln(commandString); + //Verify the command length if (commandLength >= 2) { @@ -835,8 +838,8 @@ void checkCommand() reportOK(); else { - systemPrintln(commandString); systemPrintln("ERROR"); + commandComplete(false); } outputSerialData(true); petWDT(); @@ -848,6 +851,22 @@ void checkCommand() void reportOK() { systemPrintln("OK"); + commandComplete(true); +} + +void commandComplete(bool success) +{ + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Output the serial message header + systemWrite(START_OF_VC_SERIAL); + systemWrite(sizeof(VC_RADIO_MESSAGE_HEADER) + sizeof(VC_COMMAND_COMPLETE_MESSAGE)); + systemWrite(PC_COMMAND_COMPLETE); + systemWrite(myVc); + + //Output the command complete message + systemWrite(success ? VC_CMD_SUCCESS : VC_CMD_ERROR); + } } //Remove any preceeding or following whitespace chars diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index 2ea80120..bccad830 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -42,6 +42,7 @@ #define PC_DATA_ACK (PC_LINK_STATUS + 1)//Indicate data delivery success #define PC_DATA_NACK (PC_DATA_ACK + 1) //Indicate data delivery failure #define PC_SERIAL_RECONNECT (PC_DATA_NACK + 1) //Disconnect/reconnect the serial port over LoRaSerial CPU reset +#define PC_COMMAND_COMPLETE (PC_SERIAL_RECONNECT + 1)//Command complete //Address space 1 and 6 are reserved for the host PC interface to support remote //command processing. The radio removes these bits and converts them to the @@ -162,9 +163,17 @@ typedef enum typedef struct _VC_DATA_ACK_NACK_MESSAGE { - uint8_t msgDestVc; //message destination VC + uint8_t msgDestVc; //Message destination VC } VC_DATA_ACK_NACK_MESSAGE; +#define VC_CMD_SUCCESS 0 +#define VC_CMD_ERROR 1 + +typedef struct _VC_COMMAND_COMPLETE_MESSAGE +{ + uint8_t cmdStatus; //Command status +} VC_COMMAND_COMPLETE_MESSAGE; + //------------------------------------------------------------------------------ // Macros //------------------------------------------------------------------------------ From 10f421f5c474318eabcd6f85d7076fdc29ea8c2d Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 28 Jan 2023 07:47:41 -1000 Subject: [PATCH 494/594] Redirect all output to SERIAL or RF --- Firmware/LoRaSerial_Firmware/Serial.ino | 14 ++++++++++++-- Firmware/LoRaSerial_Firmware/System.ino | 16 ++-------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index fe7851db..cf8d9009 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -53,8 +53,18 @@ void serialBufferOutput(uint8_t * data, uint16_t dataLength) //Add a single byte to the output buffer void serialOutputByte(uint8_t data) { - serialTransmitBuffer[txHead++] = data; - txHead %= sizeof(serialTransmitBuffer); + if (printerEndpoint == PRINT_TO_SERIAL) + { + //Add this byte to the serial output buffer + serialTransmitBuffer[txHead++] = data; + txHead %= sizeof(serialTransmitBuffer); + } + else + { + //Add this byte to the command response buffer + commandTXBuffer[commandTXHead++] = data; + commandTXHead %= sizeof(commandTXBuffer); + } } //Update the output of the RTS pin (host says it's ok to send data when assertRTS = true) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 45247f75..7ac3c97d 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -4,20 +4,8 @@ void systemPrint(const char* string) uint16_t length; length = strlen(string); - if (printerEndpoint == PRINT_TO_SERIAL) - { - for (uint16_t x = 0 ; x < length ; x++) - serialOutputByte(string[x]); - } - else if (printerEndpoint == PRINT_TO_RF) - { - //Move these characters into the transmit buffer - for (uint16_t x = 0 ; x < length ; x++) - { - commandTXBuffer[commandTXHead++] = string[x]; - commandTXHead %= sizeof(commandTXBuffer); - } - } + for (uint16_t x = 0 ; x < length ; x++) + serialOutputByte(string[x]); } //Print a string with a carriage return and linefeed From 6d2defddbcde086b2217b52804427acfe8186873 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 28 Jan 2023 09:24:50 -1000 Subject: [PATCH 495/594] VC: Properly return command status to remote system's host --- Firmware/LoRaSerial_Firmware/Serial.ino | 13 ++++++++++--- Firmware/LoRaSerial_Firmware/States.ino | 18 ++++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index cf8d9009..54885712 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -202,15 +202,22 @@ void readyLocalCommandPacket() //Send a portion of the commandTXBuffer to outgoingPacket uint8_t readyOutgoingCommandPacket(uint16_t offset) { - uint16_t bytesToSend = availableTXCommandBytes(); + uint16_t bytesToSend; uint16_t length; uint16_t maxLength; - maxLength = maxDatagramSize - offset; + //Determine the amount of data in the buffer bytesToSend = availableTXCommandBytes(); + if ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + && (commandTXBuffer[commandTXTail] != START_OF_VC_SERIAL)) + bytesToSend -= VC_SERIAL_HEADER_BYTES + sizeof(VC_COMMAND_COMPLETE_MESSAGE); + + //Limit the length to the frame size + maxLength = maxDatagramSize - offset; if (bytesToSend > maxLength) bytesToSend = maxLength; + //Display the amount of data being sent if (settings.debugSerial) { systemPrint("Moving "); @@ -219,7 +226,7 @@ uint8_t readyOutgoingCommandPacket(uint16_t offset) outputSerialData(true); } - //Determine the number of bytes to send + //Determine if the data wraps around to the beginning of the buffer length = 0; if ((commandTXTail + bytesToSend) > sizeof(commandTXBuffer)) { diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index a0286f43..21d5dabb 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1935,7 +1935,7 @@ void updateRadioState() if (settings.debugSerial) { systemPrint("RX: checkCommand placed "); - systemPrint(commandLength); + systemPrint(length); systemPrintln(" bytes into commandTXBuffer"); dumpCircularBuffer(commandTXBuffer, commandTXTail, sizeof(commandTXBuffer), length); outputSerialData(true); @@ -1946,11 +1946,22 @@ void updateRadioState() case DATAGRAM_REMOTE_COMMAND_RESPONSE: triggerEvent(TRIGGER_RX_COMMAND_RESPONSE); + //Determine if this is the VC_COMMAND_COMPLETE_MESSAGE + length = rxDataBytes; + if (*rxVcData == START_OF_VC_SERIAL) + { + //Remove the VC_RADIO_MESSAGE_HEADER and START_OF_VC_SERIAL byte + length -= VC_RADIO_HEADER_BYTES + 1; + rxData += VC_RADIO_HEADER_BYTES + 1; + } + else + vcHeader->destVc |= PC_REMOTE_RESPONSE; + //Debug the serial path if (settings.debugSerial) { systemPrint("Moving "); - systemPrint(rxDataBytes); + systemPrint(length); systemPrintln(" from incomingBuffer to serialTransmitBuffer"); dumpBuffer(rxData, rxDataBytes); outputSerialData(true); @@ -1958,8 +1969,7 @@ void updateRadioState() //Place the data in to the serialTransmitBuffer serialOutputByte(START_OF_VC_SERIAL); - vcHeader->destVc |= PC_REMOTE_RESPONSE; - serialBufferOutput(rxData, rxDataBytes); + serialBufferOutput(rxData, length); frequencyCorrection += radio.getFrequencyError() / 1000000.0; From c92476b1d6f652a2587e8b859204d58113011eb5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 28 Jan 2023 09:45:46 -1000 Subject: [PATCH 496/594] VcServerTest: Enable/disable display of command complete --- Firmware/Tools/VcServerTest.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index b65940b5..2ad9c807 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -16,6 +16,7 @@ #define DEBUG_LOCAL_COMMANDS 0 #define DEBUG_PC_TO_RADIO 0 #define DEBUG_RADIO_TO_PC 0 +#define DISPLAY_COMMAND_COMPLETE 0 #define DISPLAY_DATA_ACK 0 #define DISPLAY_DATA_NACK 1 #define DISPLAY_VC_STATE 0 @@ -27,6 +28,7 @@ typedef struct _VIRTUAL_CIRCUIT int vcState; } VIRTUAL_CIRCUIT; +bool commandStatus; bool findMyVc; int myVc = VC_UNASSIGNED; int remoteVc; @@ -34,6 +36,8 @@ uint8_t inputBuffer[INPUT_BUFFER_SIZE]; uint8_t outputBuffer[VC_SERIAL_HEADER_BYTES + BUFFER_SIZE]; int timeoutCount; VIRTUAL_CIRCUIT virtualCircuitList[MAX_VC]; +volatile bool waitingForCommandComplete; +uint8_t remoteCommandVc; int cmdToRadio(uint8_t * buffer, int length) { @@ -330,6 +334,14 @@ void radioToPcLinkStatus(VC_SERIAL_MESSAGE_HEADER * header, uint8_t length) printf("======= VC %d CONNECTED ======\n", srcVc); break; } + + //Clear the waiting for command complete if the link fails + if (waitingForCommandComplete && (newState != VC_STATE_CONNECTED) + && (srcVc == remoteCommandVc)) + { + commandStatus = VC_CMD_ERROR; + waitingForCommandComplete = false; + } } void radioDataAck(uint8_t * data, uint8_t length) @@ -350,6 +362,18 @@ void radioDataNack(uint8_t * data, uint8_t length) printf("NACK from VC %d\n", vcMsg->msgDestVc); } +void radioCommandComplete(uint8_t srcVc, uint8_t * data, uint8_t length) +{ + VC_COMMAND_COMPLETE_MESSAGE * vcMsg; + + vcMsg = (VC_COMMAND_COMPLETE_MESSAGE *)data; + if (DISPLAY_COMMAND_COMPLETE) + printf("Command complete from VC %d: %s\n", srcVc, + (vcMsg->cmdStatus == VC_CMD_SUCCESS) ? "OK" : "ERROR"); + commandStatus = vcMsg->cmdStatus; + waitingForCommandComplete = false; +} + int radioToHost() { int bytesRead; @@ -454,6 +478,10 @@ int radioToHost() else if (header->radio.destVc == (PC_REMOTE_RESPONSE | myVc)) status = hostToStdout(data, length); + //Display command completion status + else if (header->radio.destVc == PC_COMMAND_COMPLETE) + radioCommandComplete(header->radio.srcVc, data, length); + //Display ACKs for transmitted messages else if (header->radio.destVc == PC_DATA_ACK) radioDataAck(data, length); From 552a7c89a0074034281b3898668f6fbdc9bcd2e1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sat, 28 Jan 2023 10:08:55 -1000 Subject: [PATCH 497/594] VcServerTest: Wait for command execution --- Firmware/Tools/VcServerTest.c | 92 +++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 2ad9c807..60719cca 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -139,44 +139,74 @@ int stdinToRadio() int maxfds; int status; struct timeval timeout; + static int index; status = 0; - do + if (!waitingForCommandComplete) { - //Read the console input data into the local buffer. - bytesRead = read(STDIN, inputBuffer, INPUT_BUFFER_SIZE); - if (bytesRead < 0) - { - perror("ERROR: Read from stdin failed!"); - status = bytesRead; - break; - } - - //Send this data over the VC - bytesSent = 0; - while (bytesSent < bytesRead) + if (remoteVc == VC_COMMAND) { - //Break up the data if necessary - bytesToSend = bytesRead - bytesSent; - if (bytesToSend > MAX_MESSAGE_SIZE) - bytesToSend = MAX_MESSAGE_SIZE; - - //Send the data - if (remoteVc == VC_COMMAND) - bytesWritten = cmdToRadio(&inputBuffer[bytesSent], bytesToSend); - else - bytesWritten = hostToRadio(remoteVc, &inputBuffer[bytesSent], bytesToSend); - if (bytesWritten < 0) + do { - perror("ERROR: Write to radio failed!"); - status = bytesWritten; - break; - } + do + { + //Read the console input data into the local buffer. + bytesRead = read(STDIN, &inputBuffer[index], 1); + if (bytesRead < 0) + { + perror("ERROR: Read from stdin failed!"); + status = bytesRead; + break; + } + index += bytesRead; + } while (bytesRead && (inputBuffer[index - bytesRead] != '\n')); + + //Check for end of data + if (!bytesRead) + break; - //Account for the bytes written - bytesSent += bytesWritten; + //Send this command the VC + bytesWritten = cmdToRadio(inputBuffer, index); + waitingForCommandComplete = true; + remoteCommandVc = myVc; + index = 0; + } while (0); } - } while (0); + else + do + { + //Read the console input data into the local buffer. + bytesRead = read(STDIN, inputBuffer, BUFFER_SIZE); + if (bytesRead < 0) + { + perror("ERROR: Read from stdin failed!"); + status = bytesRead; + break; + } + + //Send this data over the VC + bytesSent = 0; + while (bytesSent < bytesRead) + { + //Break up the data if necessary + bytesToSend = bytesRead - bytesSent; + if (bytesToSend > MAX_MESSAGE_SIZE) + bytesToSend = MAX_MESSAGE_SIZE; + + //Send the data + bytesWritten = hostToRadio(remoteVc, &inputBuffer[bytesSent], bytesToSend); + if (bytesWritten < 0) + { + perror("ERROR: Write to radio failed!"); + status = bytesWritten; + break; + } + + //Account for the bytes written + bytesSent += bytesWritten; + } + } while (0); + } return status; } From dd95a6b3091937ef93a1a8c8278733fe9fd40a03 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 14:48:30 -1000 Subject: [PATCH 498/594] Reorder the settings --- Firmware/LoRaSerial_Firmware/Commands.ino | 6 +- Firmware/LoRaSerial_Firmware/Radio.ino | 22 ++-- Firmware/LoRaSerial_Firmware/Train.ino | 8 +- Firmware/LoRaSerial_Firmware/settings.h | 140 +++++++++++++++------- 4 files changed, 114 insertions(+), 62 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 18ee8cc0..f563086e 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1120,13 +1120,12 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugNvm", &tempSettings.debugNvm}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugRadio", &tempSettings.debugRadio}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugReceive", &tempSettings.debugReceive}, + {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &tempSettings.debugSerial}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugStates", &tempSettings.debugStates}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSync", &tempSettings.debugSync}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugTraining", &tempSettings.debugTraining}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugTransmit", &tempSettings.debugTransmit}, - {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSerial", &tempSettings.debugSerial}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "DisplayRealMillis", &tempSettings.displayRealMillis}, - {'D', 0, 1, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &tempSettings.overheadTime}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintAckNumbers", &tempSettings.printAckNumbers}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintChannel", &tempSettings.printChannel}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintFrequency", &tempSettings.printFrequency}, @@ -1136,7 +1135,6 @@ const COMMAND_ENTRY commands[] = {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintRfData", &tempSettings.printRfData}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintTimestamp", &tempSettings.printTimestamp}, {'D', 1, 0, 0, 1, 0, TYPE_BOOL, valInt, "PrintTxErrors", &tempSettings.printTxErrors}, - {'D', 1, 0, 0, 255, 0, TYPE_U8, valInt, "SelectLedUse", &tempSettings.selectLedUse}, /*Radio parameters Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ @@ -1165,6 +1163,8 @@ const COMMAND_ENTRY commands[] = {'R', 0, 0, 0, 255, 0, TYPE_U8, valInt, "MaxResends", &tempSettings.maxResends}, {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "NetID", &tempSettings.netID}, {'R', 0, 1, 0, 2, 0, TYPE_U8, valInt, "OperatingMode", &tempSettings.operatingMode}, + {'R', 0, 1, 0, 1000, 0, TYPE_U16, valInt, "OverHeadtime", &tempSettings.overheadTime}, + {'R', 1, 0, 0, 255, 0, TYPE_U8, valInt, "SelectLedUse", &tempSettings.selectLedUse}, {'R', 0, 0, 0, 1, 0, TYPE_BOOL, valServer, "Server", &tempSettings.server}, {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "VerifyRxNetID", &tempSettings.verifyRxNetID}, diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 03032521..df12007b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -274,14 +274,14 @@ float calcAirTimeUsec(uint8_t bytesToSend) //With SF = 6 selected, implicit header mode is the only mode of operation possible. uint8_t explicitHeader = 0; //1 for implicit header if(settings.radioSpreadFactor == 6) explicitHeader = 1; - + //LowDataRateOptimize increases the robustness of the LoRa link at low effective data rates. Its use is mandated when the symbol duration exceeds 16ms. uint8_t useLowDataRateOptimization = 0; //0 to disable. //We choose to enable LDRO for airSpeed of 400 even though TSym is 8.2ms. if(settings.airSpeed <= 400 && settings.airSpeed >= 40) useLowDataRateOptimization = 1; if(tSymUsec > 16000) useLowDataRateOptimization = 1; //Catch custom bandwidth/spread/coding setups - + float p1 = (8 * bytesToSend - 4 * settings.radioSpreadFactor + 28 + 16 * useHardwareCRC - 20 * explicitHeader) / (4.0 * (settings.radioSpreadFactor - 2 * useLowDataRateOptimization)); p1 = ceil(p1) * settings.radioCodingRate; if (p1 < 0) p1 = 0; @@ -1207,37 +1207,36 @@ void updateRadioParameters(uint8_t * rxData) //Update the radio parameters tempSettings.airSpeed = params.airSpeed; tempSettings.autoTuneFrequency = params.autoTuneFrequency; - tempSettings.radioBandwidth = params.radioBandwidth; - tempSettings.radioCodingRate = params.radioCodingRate; tempSettings.frequencyHop = params.frequencyHop; tempSettings.frequencyMax = params.frequencyMax; tempSettings.frequencyMin = params.frequencyMin; tempSettings.maxDwellTime = params.maxDwellTime; tempSettings.numberOfChannels = params.numberOfChannels; + tempSettings.radioBandwidth = params.radioBandwidth; + tempSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; + tempSettings.radioCodingRate = params.radioCodingRate; tempSettings.radioPreambleLength = params.radioPreambleLength; tempSettings.radioSpreadFactor = params.radioSpreadFactor; tempSettings.radioSyncWord = params.radioSyncWord; - tempSettings.radioBroadcastPower_dbm = params.radioBroadcastPower_dbm; //Update the radio protocol parameters tempSettings.dataScrambling = params.dataScrambling; tempSettings.enableCRC16 = params.enableCRC16; tempSettings.encryptData = params.encryptData; memcpy(tempSettings.encryptionKey, params.encryptionKey, sizeof(tempSettings.encryptionKey)); - tempSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; + tempSettings.framesToYield = params.framesToYield; tempSettings.heartbeatTimeout = params.heartbeatTimeout; tempSettings.maxResends = params.maxResends; tempSettings.netID = params.netID; tempSettings.operatingMode = params.operatingMode; tempSettings.overheadTime = params.overheadTime; + tempSettings.selectLedUse = params.selectLedUse; tempSettings.server = params.server; tempSettings.verifyRxNetID = params.verifyRxNetID; - tempSettings.framesToYield = params.framesToYield; //Update the debug parameters if (params.copyDebug) { - tempSettings.debug = params.debug; tempSettings.copyDebug = params.copyDebug; tempSettings.debug = params.debug; tempSettings.debugDatagrams = params.debugDatagrams; @@ -1245,21 +1244,21 @@ void updateRadioParameters(uint8_t * rxData) tempSettings.debugNvm = params.debugNvm; tempSettings.debugRadio = params.debugRadio; tempSettings.debugReceive = params.debugReceive; + tempSettings.debugSerial = params.debugSerial; tempSettings.debugStates = params.debugStates; tempSettings.debugSync = params.debugSync; tempSettings.debugTraining = params.debugTraining; tempSettings.debugTransmit = params.debugTransmit; - tempSettings.debugSerial = params.debugSerial; - tempSettings.printPacketQuality = params.printPacketQuality; tempSettings.displayRealMillis = params.displayRealMillis; tempSettings.printAckNumbers = params.printAckNumbers; + tempSettings.printChannel = params.printChannel; tempSettings.printFrequency = params.printFrequency; tempSettings.printLinkUpDown = params.printLinkUpDown; + tempSettings.printPacketQuality = params.printPacketQuality; tempSettings.printPktData = params.printPktData; tempSettings.printRfData = params.printRfData; tempSettings.printTimestamp = params.printTimestamp; tempSettings.printTxErrors = params.printTxErrors; - tempSettings.selectLedUse = params.selectLedUse; } //Update the serial parameters @@ -1271,6 +1270,7 @@ void updateRadioParameters(uint8_t * rxData) tempSettings.invertCts = params.invertCts; tempSettings.invertRts = params.invertRts; tempSettings.serialSpeed = params.serialSpeed; + tempSettings.serialTimeoutBeforeSendingFrame_ms = params.serialTimeoutBeforeSendingFrame_ms; tempSettings.usbSerialWait = params.usbSerialWait; } diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 2b0f046b..773f417f 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -118,13 +118,14 @@ void commonTrainingInitialization() settings.dataScrambling = true; //Scramble the data settings.enableCRC16 = true; //Use CRC-16 settings.encryptData = true; //Enable packet encryption + memcpy(&settings.encryptionKey, &tempSettings.trainingKey, AES_KEY_BYTES); //Common encryption key settings.frequencyHop = false; //Stay on the training frequency + settings.maxResends = 1; //Don't waste time retransmitting settings.netID = 'T'; //NetID for training settings.operatingMode = MODE_MULTIPOINT; //Use datagrams settings.radioBroadcastPower_dbm = 14; //Minimum, assume radios are near each other settings.selectLedUse = LEDS_CYLON; //Display the CYLON pattern on the LEDs settings.verifyRxNetID = true; //Disable netID checking - memcpy(&settings.trainingKey, &tempSettings.trainingKey, AES_KEY_BYTES); //56: Common training key //Determine the components of the frame header and trailer selectHeaderAndTrailerBytes(); @@ -136,18 +137,21 @@ void commonTrainingInitialization() //Ignore copyDebug settings.debug = tempSettings.debug; settings.debugDatagrams = tempSettings.debugDatagrams; + settings.debugHeartbeat = tempSettings.debugHeartbeat; settings.debugNvm = tempSettings.debugNvm; settings.debugRadio = tempSettings.debugRadio; settings.debugReceive = tempSettings.debugReceive; settings.debugSerial = tempSettings.debugSerial; settings.debugStates = tempSettings.debugStates; + settings.debugSync = tempSettings.debugSync; settings.debugTraining = tempSettings.debugTraining; settings.debugTransmit = tempSettings.debugTransmit; - settings.printPacketQuality = tempSettings.printPacketQuality; settings.displayRealMillis = tempSettings.displayRealMillis; settings.printAckNumbers = tempSettings.printAckNumbers; + settings.printChannel = tempSettings.printChannel; settings.printFrequency = tempSettings.printFrequency; settings.printLinkUpDown = tempSettings.printLinkUpDown; + settings.printPacketQuality = tempSettings.printPacketQuality; settings.printPktData = tempSettings.printPktData; settings.printRfData = tempSettings.printRfData; settings.printTimestamp = tempSettings.printTimestamp; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 3d1e19b5..e7f5fa61 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -389,80 +389,128 @@ typedef struct struct_settings { uint16_t sizeOfSettings = 0; //sizeOfSettings **must** be the first entry and must be int uint16_t strIdentifier = LRS_IDENTIFIER; // strIdentifier **must** be the second entry - uint32_t serialSpeed = 57600; //Default to 57600bps to match RTK Surveyor default firmware + //---------------------------------------- + //Radio parameters + //---------------------------------------- + uint32_t airSpeed = 4800; //Default to ~523 bytes per second to support RTCM. Overrides spread, bandwidth, and coding - uint8_t netID = 192; //Both radios must share a network ID - uint8_t operatingMode = MODE_POINT_TO_POINT; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. - bool encryptData = true; //AES encrypt each packet - uint8_t encryptionKey[AES_KEY_BYTES] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; - bool dataScrambling = false; //Use IBM Data Whitening to reduce DC bias + float frequencyMin = 902.0; //MHz + float frequencyMax = 928.0; //MHz + float radioBandwidth = 500.0; //kHz 125/250/500 generally. We need 500kHz for higher data. + + bool frequencyHop = true; //Hop between frequencies to avoid dwelling on any one channel for too long + uint8_t numberOfChannels = 50; //Divide the min/max freq band into this number of channels and hop between. + uint16_t maxDwellTime = 400; //Max number of ms before hopping (if enabled). Useful for configuring radio to be within regulator limits (FCC = 400ms max) + #if (ENABLE_DEVELOPER == true) #define TX_POWER_DB 14 #else //ENABLE_DEVELOPER #define TX_POWER_DB 30 #endif //ENABLE_DEVELOPER uint8_t radioBroadcastPower_dbm = TX_POWER_DB; //Transmit power in dBm. Max is 30dBm (1W), min is 14dBm (25mW). - float frequencyMin = 902.0; //MHz - float frequencyMax = 928.0; //MHz - uint8_t numberOfChannels = 50; //Divide the min/max freq band into this number of channels and hop between. - bool frequencyHop = true; //Hop between frequencies to avoid dwelling on any one channel for too long - uint16_t maxDwellTime = 400; //Max number of ms before hopping (if enabled). Useful for configuring radio to be within regulator limits (FCC = 400ms max) - float radioBandwidth = 500.0; //kHz 125/250/500 generally. We need 500kHz for higher data. - uint8_t radioSpreadFactor = 9; //6 to 12. Use higher factor for longer range. uint8_t radioCodingRate = 8; //5 to 8. Higher coding rates ensure less packets dropped. + uint8_t radioSpreadFactor = 9; //6 to 12. Use higher factor for longer range. uint8_t radioSyncWord = 18; //18 = 0x12 is default for custom/private networks. Different sync words does *not* guarantee a remote radio will not get packet. + uint16_t radioPreambleLength = 8; //Number of symbols. Different lengths does *not* guarantee a remote radio privacy. 8 to 11 works. 8 to 15 drops some. 8 to 20 is silent. + bool autoTuneFrequency = false; //Based on the last packets frequency error, adjust our next transaction frequency + + //---------------------------------------- + //Radio protocol parameters + //---------------------------------------- + + uint8_t operatingMode = MODE_POINT_TO_POINT; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. + + uint8_t selectLedUse = LEDS_RSSI; //Select LED use + bool server = false; //Default to being a client, enable server for multipoint, VC and training + uint8_t netID = 192; //Both radios must share a network ID + bool verifyRxNetID = true; //Verify RX netID value when not operating in point-to-point mode + + uint8_t encryptionKey[AES_KEY_BYTES] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; + + bool encryptData = true; //AES encrypt each packet + bool dataScrambling = false; //Use IBM Data Whitening to reduce DC bias + bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive + uint8_t framesToYield = 3; //If remote requests it, supress transmission for this number of max packet frames + + uint16_t heartbeatTimeout = 5000; //ms before sending HEARTBEAT to see if link is active + uint16_t overheadTime = 10; //ms added to ack and datagram times before ACK timeout occurs + + uint8_t maxResends = 0; //Attempt resends up to this number, 0 = infinite retries + + //---------------------------------------- + //Serial parameters + //---------------------------------------- + + bool copySerial = false; //Copy the serial parameters to the training client + bool invertCts = false; //Invert the input of CTS + bool invertRts = false; //Invert the output of RTS + + uint32_t serialSpeed = 57600; //Default to 57600bps to match RTK Surveyor default firmware + uint16_t serialTimeoutBeforeSendingFrame_ms = 50; //Send partial buffer if time expires - bool debug = false; //Print basic events: ie, radio state changes bool echo = false; //Print locally inputted serial - uint16_t heartbeatTimeout = 5000; //ms before sending HEARTBEAT to see if link is active bool flowControl = false; //Enable the use of CTS/RTS flow control signals - bool autoTuneFrequency = false; //Based on the last packets frequency error, adjust our next transaction frequency - bool printPacketQuality = false; //Print RSSI, SNR, and freqError for received packets - uint8_t maxResends = 0; //Attempt resends up to this number, 0 = infinite retries - bool printFrequency = false; //Print the updated frequency - bool debugRadio = false; //Print radio info - bool debugStates = false; //Print state changes - bool debugTraining = false; //Print training info #if (ENABLE_DEVELOPER == true) #define WAIT_SERIAL_DEFAULT true #else //ENABLE_DEVELOPER #define WAIT_SERIAL_DEFAULT false #endif //ENABLE_DEVELOPER bool usbSerialWait = WAIT_SERIAL_DEFAULT; //Wait for USB serial initialization - bool printRfData = false; //Print RX and TX data - bool printPktData = false; //Print data, before encryption and after decryption - bool verifyRxNetID = true; //Verify RX netID value when not operating in point-to-point mode + + //---------------------------------------- + //Training parameters + //---------------------------------------- + + uint8_t clientFindPartnerRetryInterval = 3; //Number of seconds before retransmiting the client FIND_PARTNER + + uint8_t trainingKey[AES_KEY_BYTES] = { 0x53, 0x70, 0x61, 0x72, 0x6b, 0x46, 0x75, 0x6E, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67 }; + + uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training + + //---------------------------------------- + //Trigger parameters + //---------------------------------------- + + bool copyTriggers = false; //Copy the trigger parameters to the training client uint8_t triggerWidth = 25; //Trigger width in microSeconds or multipler for trigger width bool triggerWidthIsMultiplier = true; //Use the trigger width as a multiplier + uint32_t triggerEnable = 0; //Determine which triggers are enabled: 31 - 0 uint32_t triggerEnable2 = 0; //Determine which triggers are enabled: 63 - 32 - bool debugReceive = false; //Print receive processing - bool debugTransmit = false; //Print transmit processing - bool printTxErrors = false; //Print any transmit errors - bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds - bool debugDatagrams = false; //Print the datagrams - uint16_t overheadTime = 10; //ms added to ack and datagram times before ACK timeout occurs - bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive - bool displayRealMillis = false; //true = Display the millis value without offset, false = use offset - bool server = false; //Default to being a client, enable server for multipoint, VC and training - uint8_t clientFindPartnerRetryInterval = 3; //Number of seconds before retransmiting the client FIND_PARTNER + + + //---------------------------------------- + //Debug parameters + //---------------------------------------- + bool copyDebug = false; //Copy the debug parameters to the training client - bool copySerial = false; //Copy the serial parameters to the training client - bool copyTriggers = false; //Copy the trigger parameters to the training client - uint8_t trainingKey[AES_KEY_BYTES] = { 0x53, 0x70, 0x61, 0x72, 0x6b, 0x46, 0x75, 0x6E, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67 }; - bool printLinkUpDown = false; //Print the link up and link down messages - bool invertCts = false; //Invert the input of CTS - bool invertRts = false; //Invert the output of RTS - uint8_t selectLedUse = LEDS_RSSI; //Select LED use - uint8_t trainingTimeout = 1; //Timeout in minutes to complete the training + bool debug = false; //Print basic events: ie, radio state changes + bool debugDatagrams = false; //Print the datagrams + bool debugHeartbeat = false; //Print the HEARTBEAT timing values + + bool debugNvm = false; //Debug NVM operation + bool debugRadio = false; //Print radio info + bool debugReceive = false; //Print receive processing bool debugSerial = false; //Debug the serial input + + bool debugStates = false; //Print state changes bool debugSync = false; //Print clock sync processing - bool debugNvm = false; //Debug NVM operation + bool debugTraining = false; //Print training info + bool debugTransmit = false; //Print transmit processing + + bool displayRealMillis = false; //true = Display the millis value without offset, false = use offset bool printAckNumbers = false; //Print the ACK numbers - bool debugHeartbeat = false; //Print the HEARTBEAT timing values - uint8_t framesToYield = 3; //If remote requests it, supress transmission for this number of max packet frames bool printChannel = false; //Print the channel number + bool printFrequency = false; //Print the updated frequency + + bool printLinkUpDown = false; //Print the link up and link down messages + bool printPacketQuality = false; //Print RSSI, SNR, and freqError for received packets + bool printPktData = false; //Print data, before encryption and after decryption + bool printRfData = false; //Print RX and TX data + + bool printTimestamp = false; //Print a timestamp: days hours:minutes:seconds.milliseconds + bool printTxErrors = false; //Print any transmit errors //Add new parameters immediately before this line //-- Add commands to set the parameters From a37e356793b1a7f595fe2d8530e94236521efca5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 15:12:10 -1000 Subject: [PATCH 499/594] Replace TX_TO_RX_USEC with settings.txToRxUsec --- Firmware/LoRaSerial_Firmware/Commands.ino | 1 + Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 -- Firmware/LoRaSerial_Firmware/Radio.ino | 4 +++- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/settings.h | 1 + 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index f563086e..c274ef63 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1151,6 +1151,7 @@ const COMMAND_ENTRY commands[] = {'R', 0, 1, 6, 12, 0, TYPE_U8, valOverride, "SpreadFactor", &tempSettings.radioSpreadFactor}, {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &tempSettings.radioSyncWord}, {'R', 0, 1, 14, 30, 0, TYPE_U8, valInt, "TxPower", &tempSettings.radioBroadcastPower_dbm}, + {'R', 0, 1, 0, 999999, 0, TYPE_U32, valInt, "TxToRxUsec", &tempSettings.txToRxUsec}, /*Radio protocol parameters Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 60a33275..98dc4cbe 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -72,8 +72,6 @@ const int FIRMWARE_VERSION_MINOR = 0; #define P2P_ACK_BYTES sizeof(unsigned long) //Number of data bytes in the ACK frame #define VC_HEARTBEAT_BYTES 1 + 1 + 1 + 16 + 4 //Number of data bytes in the VC_HEARTBEAT frame -#define TX_TO_RX_USEC 680 //Time from TX to RX transactionComplete in microseconds - //Bit 0: Signal Detected indicates that a valid LoRa preamble has been detected //Bit 1: Signal Synchronized indicates that the end of Preamble has been detected, the modem is in lock //Bit 3: Header Info Valid toggles high when a valid Header (with correct CRC) is detected diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index df12007b..0e1ca99b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -441,6 +441,7 @@ void generateHopTable() //Use settings that must be identical to have a functioning link. //For example, we do not use coding rate because two radios can communicate with different coding rate values myRandSeed = settings.airSpeed + + settings.txToRxUsec + settings.netID + settings.operatingMode + settings.encryptData @@ -1218,6 +1219,7 @@ void updateRadioParameters(uint8_t * rxData) tempSettings.radioPreambleLength = params.radioPreambleLength; tempSettings.radioSpreadFactor = params.radioSpreadFactor; tempSettings.radioSyncWord = params.radioSyncWord; + tempSettings.txToRxUsec = params.txToRxUsec; //Update the radio protocol parameters tempSettings.dataScrambling = params.dataScrambling; @@ -3298,7 +3300,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) //Compute the remote system's channel timer firing time offset in milliseconds //using the channel timer value and the adjustments for transmit and receive //time (time of flight) - frameAirTimeMsec = (frameAirTimeUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000; + frameAirTimeMsec = (frameAirTimeUsec + settings.txToRxUsec + micros() - transactionCompleteMicros) / 1000; rmtHopTimeMsec = msToNextHopRemote - frameAirTimeMsec; //Compute the when the local system last hopped diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 21d5dabb..1aa0ae5e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -3016,7 +3016,7 @@ void vcReceiveHeartbeat(uint32_t rxMillis) //get offset by the transmit setup time and the receive overhead time. memcpy(×tampOffset, &rxVcData[UNIQUE_ID_BYTES], sizeof(timestampOffset)); timestampOffset -= millis(); - timestampOffset += (txHeartbeatUsec + TX_TO_RX_USEC + micros() - transactionCompleteMicros) / 1000; + timestampOffset += (txHeartbeatUsec + settings.txToRxUsec + micros() - transactionCompleteMicros) / 1000; } triggerEvent(TRIGGER_RX_VC_HEARTBEAT); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index e7f5fa61..6744371a 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -397,6 +397,7 @@ typedef struct struct_settings { float frequencyMin = 902.0; //MHz float frequencyMax = 928.0; //MHz float radioBandwidth = 500.0; //kHz 125/250/500 generally. We need 500kHz for higher data. + uint32_t txToRxUsec = 657; //TX transactionComplete to RX transactionComplete in microseconds bool frequencyHop = true; //Hop between frequencies to avoid dwelling on any one channel for too long uint8_t numberOfChannels = 50; //Divide the min/max freq band into this number of channels and hop between. From 6390740e59098d006b5bc0fb6ee2ae49db4e7ac9 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 15:37:26 -1000 Subject: [PATCH 500/594] Adjust settings based upon airSpeed during command processing Pass airSpeed to convertAirSpeedToSettings --- Firmware/LoRaSerial_Firmware/Commands.ino | 7 ++++++- Firmware/LoRaSerial_Firmware/Radio.ino | 8 ++++---- Firmware/LoRaSerial_Firmware/States.ino | 2 -- Firmware/LoRaSerial_Firmware/Train.ino | 1 - 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index c274ef63..3a535e36 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1082,8 +1082,13 @@ bool valSpeedAir (void * value, uint32_t valMin, uint32_t valMax) || (settingValue == 19200) || (settingValue == 28800) || (settingValue == 38400)); + if (valid) + { + //Adjust the settings to match the requested airSpeed + convertAirSpeedToSettings(settingValue); + } if (valid && (settings.airSpeed == 0) && (settingValue != 0)) - systemPrintln("Warning: AirSpeed override of bandwidth, spread factor, and coding rate"); + systemPrintln("Warning: AirSpeed overrides bandwidth, spread factor, and coding rate"); return valid; } diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 0e1ca99b..d93c093d 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -100,12 +100,12 @@ bool configureRadio() } //Update the settings based upon the airSpeed value -void convertAirSpeedToSettings() +void convertAirSpeedToSettings(uint16_t airSpeed) { //Determine if we are using AirSpeed or custom settings - if (settings.airSpeed != 0) + if (airSpeed != 0) { - switch (settings.airSpeed) + switch (airSpeed) { case (0): //Custom settings - use settings without modification @@ -164,7 +164,7 @@ void convertAirSpeedToSettings() if ((settings.debug == true) || (settings.debugRadio == true)) { systemPrint("Unknown airSpeed: "); - systemPrintln(settings.airSpeed); + systemPrintln(airSpeed); outputSerialData(true); } break; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 1aa0ae5e..b1696a6b 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -107,8 +107,6 @@ void updateRadioState() outputSerialData(true); } - convertAirSpeedToSettings(); //Update the settings based upon the air speed - generateHopTable(); //Generate frequency table based on user settings. selectHeaderAndTrailerBytes(); //Determine the components of the frame header and trailer diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial_Firmware/Train.ino index 773f417f..7d0b7543 100644 --- a/Firmware/LoRaSerial_Firmware/Train.ino +++ b/Firmware/LoRaSerial_Firmware/Train.ino @@ -168,7 +168,6 @@ void commonTrainingInitialization() startCylonLEDs(); petWDT(); - convertAirSpeedToSettings(); //Update the settings based upon the air speed generateHopTable(); //Generate frequency table based on current settings From e8364d846ecbfbf4854d92efa0e479c345458011 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 16:15:22 -1000 Subject: [PATCH 501/594] Set txToRxUsec based upon airSpeed --- Firmware/LoRaSerial_Firmware/Radio.ino | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index d93c093d..6dace7ca 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -114,51 +114,71 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 11; settings.radioBandwidth = 62.5; settings.radioCodingRate = 8; + //uSec: 26018 26026 26026 26025 26020 26038 ==> ~26026 + settings.txToRxUsec = 26026; break; case (150): settings.radioSpreadFactor = 10; settings.radioBandwidth = 62.5; settings.radioCodingRate = 8; + //uSec: 12187 12188 12189 12190 12191 12194 ==> ~12190 + settings.txToRxUsec = 12190; break; case (400): settings.radioSpreadFactor = 10; settings.radioBandwidth = 125; settings.radioCodingRate = 8; + //uSec: 6072 6070 6072 6070 6069 6067 ==> ~6070 + settings.txToRxUsec = 6070; break; case (1200): settings.radioSpreadFactor = 9; settings.radioBandwidth = 125; settings.radioCodingRate = 8; + //uSec: 2770 2777 2772 2773 2771 2773 ==> ~2773 + settings.txToRxUsec = 2773; break; case (2400): settings.radioSpreadFactor = 10; settings.radioBandwidth = 500; settings.radioCodingRate = 8; + //uSec: 1495 1481 1482 1481 1482 1481 ==> ~1484 + settings.txToRxUsec = 1484; break; case (4800): settings.radioSpreadFactor = 9; settings.radioBandwidth = 500; settings.radioCodingRate = 8; + //uSec: 657 657 657 658 657 657 ==> ~657 + settings.txToRxUsec = 657; break; case (9600): settings.radioSpreadFactor = 8; settings.radioBandwidth = 500; settings.radioCodingRate = 7; + //uSec: 279 279 281 280 280 279 ==> ~280 + settings.txToRxUsec = 280; break; case (19200): settings.radioSpreadFactor = 7; settings.radioBandwidth = 500; settings.radioCodingRate = 7; + //uSec: 119 118 118 119 120 119 ==> ~119 + settings.txToRxUsec = 119; break; case (28800): settings.radioSpreadFactor = 6; settings.radioBandwidth = 500; settings.radioCodingRate = 6; + //uSec: ??? + settings.txToRxUsec = 0; break; case (38400): settings.radioSpreadFactor = 6; settings.radioBandwidth = 500; settings.radioCodingRate = 5; + //uSec: ??? + settings.txToRxUsec = 0; break; default: if ((settings.debug == true) || (settings.debugRadio == true)) From f250eaa0dfe85923c33c2dfbc7c042aa006ab127 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 15:52:53 -1000 Subject: [PATCH 502/594] Remove settings.airSpeed, zero airSpeed not supported --- Firmware/LoRaSerial_Firmware/Commands.ino | 36 +--- .../LoRaSerial_Firmware.ino | 2 + Firmware/LoRaSerial_Firmware/Radio.ino | 189 +++++++++--------- Firmware/LoRaSerial_Firmware/Serial.ino | 10 +- Firmware/LoRaSerial_Firmware/States.ino | 2 +- Firmware/LoRaSerial_Firmware/System.ino | 25 +-- Firmware/LoRaSerial_Firmware/settings.h | 1 - 7 files changed, 103 insertions(+), 162 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 3a535e36..e8eaacb3 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -946,15 +946,8 @@ bool valBandwidth (void * value, uint32_t valMin, uint32_t valMax) UNUSED(valMin); UNUSED(valMax); - if ((settings.airSpeed != 0) && (doubleSettingValue != 0)) - { - systemPrintln("AirSpeed is overriding"); - return false; - } - //Some doubles get rounded incorrectly - return ((settings.airSpeed != 0) - || ((doubleSettingValue * 100) == 1040) + return (((doubleSettingValue * 100) == 1040) || (doubleSettingValue == 15.6) || ((doubleSettingValue * 100) == 2080) || (doubleSettingValue == 31.25) @@ -1024,20 +1017,6 @@ bool valKey (void * value, uint32_t valMin, uint32_t valMax) return false; } -//Determine if the AirSpeed value is overriding the parameter value -bool valOverride (void * value, uint32_t valMin, uint32_t valMax) -{ - uint32_t settingValue = *(uint32_t *)value; - - if (settings.airSpeed != 0) - { - systemPrintln("AirSpeed is overriding"); - return false; - } - - return ((settingValue >= valMin) && (settingValue <= valMax)); -} - //Validate the server value bool valServer (void * value, uint32_t valMin, uint32_t valMax) { @@ -1071,8 +1050,7 @@ bool valSpeedAir (void * value, uint32_t valMin, uint32_t valMax) UNUSED(valMin); UNUSED(valMax); - valid = ((settingValue == 0) - || (settingValue == 40) + valid = ((settingValue == 40) || (settingValue == 150) || (settingValue == 400) || (settingValue == 1200) @@ -1086,9 +1064,9 @@ bool valSpeedAir (void * value, uint32_t valMin, uint32_t valMax) { //Adjust the settings to match the requested airSpeed convertAirSpeedToSettings(settingValue); - } - if (valid && (settings.airSpeed == 0) && (settingValue != 0)) + airSpeed = 0; systemPrintln("Warning: AirSpeed overrides bandwidth, spread factor, and coding rate"); + } return valid; } @@ -1143,17 +1121,17 @@ const COMMAND_ENTRY commands[] = /*Radio parameters Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ - {'R', 0, 1, 0, 0, 0, TYPE_SPEED_AIR, valSpeedAir, "AirSpeed", &tempSettings.airSpeed}, + {'R', 0, 1, 0, 0, 0, TYPE_SPEED_AIR, valSpeedAir, "AirSpeed", &airSpeed}, {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "AutoTune", &tempSettings.autoTuneFrequency}, {'R', 0, 1, 0, 0, 2, TYPE_FLOAT, valBandwidth, "Bandwidth", &tempSettings.radioBandwidth}, - {'R', 0, 1, 5, 8, 0, TYPE_U8, valOverride, "CodingRate", &tempSettings.radioCodingRate}, + {'R', 0, 1, 5, 8, 0, TYPE_U8, valInt, "CodingRate", &tempSettings.radioCodingRate}, {'R', 0, 1, 0, 1, 0, TYPE_BOOL, valInt, "FrequencyHop", &tempSettings.frequencyHop}, {'R', 0, 1, 0, 931, 3, TYPE_FLOAT, valFreqMax, "FrequencyMax", &tempSettings.frequencyMax}, {'R', 0, 1, 900, 0, 3, TYPE_FLOAT, valFreqMin, "FrequencyMin", &tempSettings.frequencyMin}, {'R', 0, 1, 10, 65535, 0, TYPE_U16, valInt, "MaxDwellTime", &tempSettings.maxDwellTime}, {'R', 0, 1, 1, 255, 0, TYPE_U8, valInt, "NumberOfChannels", &tempSettings.numberOfChannels}, {'R', 0, 1, 6, 65535, 0, TYPE_U16, valInt, "PreambleLength", &tempSettings.radioPreambleLength}, - {'R', 0, 1, 6, 12, 0, TYPE_U8, valOverride, "SpreadFactor", &tempSettings.radioSpreadFactor}, + {'R', 0, 1, 6, 12, 0, TYPE_U8, valInt, "SpreadFactor", &tempSettings.radioSpreadFactor}, {'R', 0, 1, 0, 255, 0, TYPE_U8, valInt, "SyncWord", &tempSettings.radioSyncWord}, {'R', 0, 1, 14, 30, 0, TYPE_U8, valInt, "TxPower", &tempSettings.radioBroadcastPower_dbm}, {'R', 0, 1, 0, 999999, 0, TYPE_U32, valInt, "TxToRxUsec", &tempSettings.txToRxUsec}, diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 98dc4cbe..d9defa23 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -144,6 +144,8 @@ SX1276 radio = NULL; //We can't instantiate here because we don't yet know what float *channels; uint8_t channelNumber = 0; +uint32_t airSpeed; + //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- //Encryption diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6dace7ca..08d5992a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -102,93 +102,83 @@ bool configureRadio() //Update the settings based upon the airSpeed value void convertAirSpeedToSettings(uint16_t airSpeed) { - //Determine if we are using AirSpeed or custom settings - if (airSpeed != 0) - { - switch (airSpeed) - { - case (0): - //Custom settings - use settings without modification - break; - case (40): - settings.radioSpreadFactor = 11; - settings.radioBandwidth = 62.5; - settings.radioCodingRate = 8; - //uSec: 26018 26026 26026 26025 26020 26038 ==> ~26026 - settings.txToRxUsec = 26026; - break; - case (150): - settings.radioSpreadFactor = 10; - settings.radioBandwidth = 62.5; - settings.radioCodingRate = 8; - //uSec: 12187 12188 12189 12190 12191 12194 ==> ~12190 - settings.txToRxUsec = 12190; - break; - case (400): - settings.radioSpreadFactor = 10; - settings.radioBandwidth = 125; - settings.radioCodingRate = 8; - //uSec: 6072 6070 6072 6070 6069 6067 ==> ~6070 - settings.txToRxUsec = 6070; - break; - case (1200): - settings.radioSpreadFactor = 9; - settings.radioBandwidth = 125; - settings.radioCodingRate = 8; - //uSec: 2770 2777 2772 2773 2771 2773 ==> ~2773 - settings.txToRxUsec = 2773; - break; - case (2400): - settings.radioSpreadFactor = 10; - settings.radioBandwidth = 500; - settings.radioCodingRate = 8; - //uSec: 1495 1481 1482 1481 1482 1481 ==> ~1484 - settings.txToRxUsec = 1484; - break; - case (4800): - settings.radioSpreadFactor = 9; - settings.radioBandwidth = 500; - settings.radioCodingRate = 8; - //uSec: 657 657 657 658 657 657 ==> ~657 - settings.txToRxUsec = 657; - break; - case (9600): - settings.radioSpreadFactor = 8; - settings.radioBandwidth = 500; - settings.radioCodingRate = 7; - //uSec: 279 279 281 280 280 279 ==> ~280 - settings.txToRxUsec = 280; - break; - case (19200): - settings.radioSpreadFactor = 7; - settings.radioBandwidth = 500; - settings.radioCodingRate = 7; - //uSec: 119 118 118 119 120 119 ==> ~119 - settings.txToRxUsec = 119; - break; - case (28800): - settings.radioSpreadFactor = 6; - settings.radioBandwidth = 500; - settings.radioCodingRate = 6; - //uSec: ??? - settings.txToRxUsec = 0; - break; - case (38400): - settings.radioSpreadFactor = 6; - settings.radioBandwidth = 500; - settings.radioCodingRate = 5; - //uSec: ??? - settings.txToRxUsec = 0; - break; - default: - if ((settings.debug == true) || (settings.debugRadio == true)) - { - systemPrint("Unknown airSpeed: "); - systemPrintln(airSpeed); - outputSerialData(true); - } - break; - } + switch (airSpeed) + { + default: + systemPrint("Unknown airSpeed: "); + systemPrintln(airSpeed); + waitForever("ERROR - Invalid airSpeed value"); + break; + case (40): + settings.radioSpreadFactor = 11; + settings.radioBandwidth = 62.5; + settings.radioCodingRate = 8; + //uSec: 26018 26026 26026 26025 26020 26038 ==> ~26026 + settings.txToRxUsec = 26026; + break; + case (150): + settings.radioSpreadFactor = 10; + settings.radioBandwidth = 62.5; + settings.radioCodingRate = 8; + //uSec: 12187 12188 12189 12190 12191 12194 ==> ~12190 + settings.txToRxUsec = 12190; + break; + case (400): + settings.radioSpreadFactor = 10; + settings.radioBandwidth = 125; + settings.radioCodingRate = 8; + //uSec: 6072 6070 6072 6070 6069 6067 ==> ~6070 + settings.txToRxUsec = 6070; + break; + case (1200): + settings.radioSpreadFactor = 9; + settings.radioBandwidth = 125; + settings.radioCodingRate = 8; + //uSec: 2770 2777 2772 2773 2771 2773 ==> ~2773 + settings.txToRxUsec = 2773; + break; + case (2400): + settings.radioSpreadFactor = 10; + settings.radioBandwidth = 500; + settings.radioCodingRate = 8; + //uSec: 1495 1481 1482 1481 1482 1481 ==> ~1484 + settings.txToRxUsec = 1484; + break; + case (4800): + settings.radioSpreadFactor = 9; + settings.radioBandwidth = 500; + settings.radioCodingRate = 8; + //uSec: 657 657 657 658 657 657 ==> ~657 + settings.txToRxUsec = 657; + break; + case (9600): + settings.radioSpreadFactor = 8; + settings.radioBandwidth = 500; + settings.radioCodingRate = 7; + //uSec: 279 279 281 280 280 279 ==> ~280 + settings.txToRxUsec = 280; + break; + case (19200): + settings.radioSpreadFactor = 7; + settings.radioBandwidth = 500; + settings.radioCodingRate = 7; + //uSec: 119 118 118 119 120 119 ==> ~119 + settings.txToRxUsec = 119; + break; + case (28800): + settings.radioSpreadFactor = 6; + settings.radioBandwidth = 500; + settings.radioCodingRate = 6; + //uSec: ??? + settings.txToRxUsec = 0; + break; + case (38400): + settings.radioSpreadFactor = 6; + settings.radioBandwidth = 500; + settings.radioCodingRate = 5; + //uSec: ??? + settings.txToRxUsec = 0; + break; } } @@ -295,14 +285,13 @@ float calcAirTimeUsec(uint8_t bytesToSend) uint8_t explicitHeader = 0; //1 for implicit header if(settings.radioSpreadFactor == 6) explicitHeader = 1; - //LowDataRateOptimize increases the robustness of the LoRa link at low effective data rates. Its use is mandated when the symbol duration exceeds 16ms. - uint8_t useLowDataRateOptimization = 0; //0 to disable. - + //LowDataRateOptimize increases the robustness of the LoRa link at low effective data + //rates. Its use is mandated when the symbol duration exceeds 16ms. //We choose to enable LDRO for airSpeed of 400 even though TSym is 8.2ms. - if(settings.airSpeed <= 400 && settings.airSpeed >= 40) useLowDataRateOptimization = 1; - if(tSymUsec > 16000) useLowDataRateOptimization = 1; //Catch custom bandwidth/spread/coding setups - - float p1 = (8 * bytesToSend - 4 * settings.radioSpreadFactor + 28 + 16 * useHardwareCRC - 20 * explicitHeader) / (4.0 * (settings.radioSpreadFactor - 2 * useLowDataRateOptimization)); + uint8_t useLowDataRateOptimization = (tSymUsec >= 8192); + float p1 = ((8 * bytesToSend) - (4 * settings.radioSpreadFactor) + 28 + + (16 * useHardwareCRC) - (20 * explicitHeader)) + / (4.0 * (settings.radioSpreadFactor - (2 * useLowDataRateOptimization))); p1 = ceil(p1) * settings.radioCodingRate; if (p1 < 0) p1 = 0; uint16_t payloadBytes = 8 + p1; @@ -460,7 +449,9 @@ void generateHopTable() //Feed random number generator with our specific platform settings //Use settings that must be identical to have a functioning link. //For example, we do not use coding rate because two radios can communicate with different coding rate values - myRandSeed = settings.airSpeed + myRandSeed = (uint16_t)settings.radioBandwidth + + settings.radioSpreadFactor //Radio settings + + settings.radioCodingRate + settings.txToRxUsec + settings.netID + settings.operatingMode @@ -471,8 +462,6 @@ void generateHopTable() + settings.numberOfChannels + settings.frequencyHop + settings.maxDwellTime - + (uint16_t)settings.radioBandwidth - + settings.radioSpreadFactor + settings.verifyRxNetID + settings.overheadTime + settings.enableCRC16 @@ -1226,7 +1215,6 @@ void updateRadioParameters(uint8_t * rxData) memcpy(¶ms, rxData, sizeof(params)); //Update the radio parameters - tempSettings.airSpeed = params.airSpeed; tempSettings.autoTuneFrequency = params.autoTuneFrequency; tempSettings.frequencyHop = params.frequencyHop; tempSettings.frequencyMax = params.frequencyMax; @@ -2920,8 +2908,9 @@ bool transmitDatagram() outputSerialData(true); } - //If we are trainsmitting at high data rates the receiver is often not ready for new data. Pause for a few ms (measured with logic analyzer). - if (settings.airSpeed == 28800 || settings.airSpeed == 38400) + //If we are trainsmitting at high data rates the receiver is often not ready + //for new data. Pause for a few ms (measured with logic analyzer). + if (settings.txToRxUsec < 100) delay(2); //Reset the buffer data pointer for the next transmit operation diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 54885712..63b41f58 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -888,13 +888,9 @@ void resetSerial() outputSerialData(true); //Determine the amount of time needed to receive a character - delayTime = 200; - if (settings.airSpeed) - { - delayTime = (1000 * 8 * 2) / settings.airSpeed; - if (delayTime < 200) - delayTime = 200; - } + delayTime = (1000 * 8 * 2) / settings.serialSpeed; + if (delayTime < 200) + delayTime = 200; //Enable RTS updateRTS(true); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index b1696a6b..9df5c0ac 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -2740,7 +2740,7 @@ void enterLinkUp() //Bring up the link triggerEvent(TRIGGER_HANDSHAKE_COMPLETE); - //For very low airspeeds (150 and below) the hop timer will expire during the during handshake. + //For very low airspeeds (150 and below) the hop timer will expire during the handshake. //Reset channel number to insure both radios are on same channel. channelNumber = 0; hopChannel(); //Leave home diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 7ac3c97d..80b08718 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -1164,30 +1164,7 @@ void triggerFrequency(uint16_t frequency) //Offsets were found using a logic analyzer but it looks like (1 * Tsym) or calcSymbolTime() int16_t getReceiveCompletionOffset() { - switch (settings.airSpeed) - { - default: - return (0); - break; - case (40): - return (26); //Tsym: 32. Measured: 26 ms between a TX complete and the RX complete - break; - case (150): - return (12); //Tsym: 16 - break; - case (400): - return (6); //Tsym: 8 - break; - case (1200): - return (3); //Tsym: 4 - break; - case (2400): - return (1); //Tsym: 2 - break; - case (4800): - return (0); //Tsym: 1 - break; - } + return settings.txToRxUsec / 1000; } //Verify the VC_STATE_TYPE enums against the vcStateNames table diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 6744371a..d7a3b2d2 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -393,7 +393,6 @@ typedef struct struct_settings { //Radio parameters //---------------------------------------- - uint32_t airSpeed = 4800; //Default to ~523 bytes per second to support RTCM. Overrides spread, bandwidth, and coding float frequencyMin = 902.0; //MHz float frequencyMax = 928.0; //MHz float radioBandwidth = 500.0; //kHz 125/250/500 generally. We need 500kHz for higher data. From 82f052278e340e6fced013badc06c95280210283 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 16:40:05 -1000 Subject: [PATCH 503/594] Compute the time delay to the next HEARTBEAT transmission The HEARTBEAT frame to be received within the heartbeatTimeout period. --- Firmware/LoRaSerial_Firmware/Radio.ino | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 08d5992a..5e46c87b 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3484,11 +3484,13 @@ void setHeartbeatShort() heartbeatTimer = millis(); radioCallHistory[RADIO_CALL_setHeartbeatShort] = heartbeatTimer; - heartbeatRandomTime = random(settings.heartbeatTimeout * 2 / 10, settings.heartbeatTimeout / 2); //20-50% + //Ensure that the heartbeat can be received within the specified time + heartbeatRandomTime = settings.heartbeatTimeout; + heartbeatRandomTime -= (txHeartbeatUsec + txDataAckUsec + (2 * settings.txToRxUsec)) / 1000; + heartbeatRandomTime -= 2 * settings.overheadTime; - //Slow datarates can have significant ack transmission times - //Add the amount of time it takes to send an ack - heartbeatRandomTime += (txHeartbeatUsec / 1000) + (txDataAckUsec / 1000) + settings.overheadTime + getReceiveCompletionOffset(); + //Determine the transmit interval + heartbeatRandomTime = random(heartbeatRandomTime * 2 / 10, heartbeatRandomTime / 2); //20-50% } void setHeartbeatLong() @@ -3496,11 +3498,12 @@ void setHeartbeatLong() heartbeatTimer = millis(); radioCallHistory[RADIO_CALL_setHeartbeatLong] = heartbeatTimer; - heartbeatRandomTime = random(settings.heartbeatTimeout * 8 / 10, settings.heartbeatTimeout); //80-100% + //Ensure that the heartbeat can be received within the specified time + heartbeatRandomTime = settings.heartbeatTimeout; + heartbeatRandomTime -= (txHeartbeatUsec + txDataAckUsec + (2 * settings.txToRxUsec)) / 1000; + heartbeatRandomTime -= 2 * settings.overheadTime; - //Slow datarates can have significant ack transmission times - //Add the amount of time it takes to send an ack - heartbeatRandomTime += (txHeartbeatUsec / 1000) + (txDataAckUsec / 1000) + settings.overheadTime + getReceiveCompletionOffset(); + heartbeatRandomTime = random(heartbeatRandomTime * 8 / 10, heartbeatRandomTime); //80-100% } //Only the server sends heartbeats in multipoint mode @@ -3511,10 +3514,11 @@ void setHeartbeatMultipoint() heartbeatTimer = millis(); radioCallHistory[RADIO_CALL_setHeartbeatMultipoint] = heartbeatTimer; - heartbeatRandomTime = settings.heartbeatTimeout; - + //Ensure that the heartbeat can be received within the specified time //MP does not use acks - heartbeatRandomTime += (txHeartbeatUsec / 1000) + settings.overheadTime + getReceiveCompletionOffset(); + heartbeatRandomTime = settings.heartbeatTimeout; + heartbeatRandomTime -= (txHeartbeatUsec + settings.txToRxUsec) / 1000; + heartbeatRandomTime -= settings.overheadTime; } //Determine the delay for the next VC HEARTBEAT From 670841f5627f6cdfefe38a404bc45afed7bc559b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 16:42:55 -1000 Subject: [PATCH 504/594] Remove extra calculation of frameAirTime --- Firmware/LoRaSerial_Firmware/Radio.ino | 3 --- 1 file changed, 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 5e46c87b..840d2f86 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2916,9 +2916,6 @@ bool transmitDatagram() //Reset the buffer data pointer for the next transmit operation endOfTxData = &outgoingPacket[headerBytes]; - //Compute the time needed for this frame. Part of ACK timeout. - frameAirTime = calcAirTimeMsec(txDatagramSize); - //Transmit this datagram frameSentCount = 0; //This is the first time this frame is being sent return (retransmitDatagram(vc)); From debb226edd8e41a74263bea8ed2ca4d47c42fab4 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 17:07:23 -1000 Subject: [PATCH 505/594] Add timeoutMsec to compute the timeout delay --- Firmware/LoRaSerial_Firmware/States.ino | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 9df5c0ac..a04522ed 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -70,6 +70,7 @@ void updateRadioState() uint16_t length; uint8_t radioSeed; bool serverLinkBroken; + unsigned long timeoutMsec; VIRTUAL_CIRCUIT * vc; VC_RADIO_MESSAGE_HEADER * vcHeader; @@ -430,7 +431,8 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - if ((millis() - datagramTimer) >= (frameAirTime + systemDescriptionAirTime + settings.overheadTime + getReceiveCompletionOffset())) + timeoutMsec = frameAirTime + systemDescriptionAirTime + settings.overheadTime + getReceiveCompletionOffset(); + if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) { @@ -511,7 +513,8 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) { @@ -1128,7 +1131,8 @@ void updateRadioState() else if (receiveInProcess() == false) { //Check for a receive timeout - if ((millis() - datagramTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) { @@ -2011,6 +2015,7 @@ void updateRadioState() { //Verify that the link is still up txDestVc = rexmtTxDestVc; + timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); if ((txDestVc != VC_BROADCAST) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { @@ -2019,7 +2024,7 @@ void updateRadioState() } //Check for retransmit needed - else if ((currentMillis - ackTimer) >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + else if ((currentMillis - ackTimer) >= timeoutMsec) { //Determine if another retransmit is allowed if ((!settings.maxResends) || (rexmtFrameSentCount < settings.maxResends)) @@ -2116,6 +2121,7 @@ void updateRadioState() if (vcConnecting & (1 << index)) { //Determine if UNKNOWN_ACKS needs to be sent + timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); if (virtualCircuitList[index].vcState <= VC_STATE_SEND_UNKNOWN_ACKS) { //Send the UNKNOWN_ACKS datagram, first part of the 3-way handshake @@ -2132,8 +2138,7 @@ void updateRadioState() //Check for a timeout waiting for the SYNC_ACKS else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_SYNC_ACKS) { - if ((currentMillis - virtualCircuitList[index].timerMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + if ((currentMillis - virtualCircuitList[index].timerMillis) >= timeoutMsec) { //Retransmit the UNKNOWN_ACKS if (xmitVcUnknownAcks(index)) @@ -2149,8 +2154,7 @@ void updateRadioState() //Check for a timeout waiting for the ZERO_ACKS else if (virtualCircuitList[index].vcState == VC_STATE_WAIT_ZERO_ACKS) { - if ((currentMillis - virtualCircuitList[index].timerMillis) - >= (frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset())) + if ((currentMillis - virtualCircuitList[index].timerMillis) >= timeoutMsec) { //Retransmit the SYNC_CLOCKS if (xmitVcSyncAcks(index)) From 426ea2e7a7479b4a7b2c577c9550ddd0a0b314fe Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 17:22:07 -1000 Subject: [PATCH 506/594] Remove use of systemDescriptionAirTime --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/Radio.ino | 1 - Firmware/LoRaSerial_Firmware/States.ino | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index d9defa23..8f5ad120 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -378,7 +378,6 @@ uint16_t radioBand = 0; //In MHz. Detected radio module type. Sets the upper/low uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission uint16_t ackAirTime = 0; //Recalc'd with each change of settings -uint16_t systemDescriptionAirTime = 0; //Pre-calc'd amount of time for xmitDatagramP2PSyncClocks to get to receiver uint16_t maxPacketAirTime = 0; //Recalc'd with each change of settings uint8_t frameSentCount = 0; //Increases each time a frame is sent diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 840d2f86..95e609a7 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -64,7 +64,6 @@ bool configureRadio() //Precalculate the packet times ackAirTime = calcAirTimeMsec(headerBytes + CHANNEL_TIMER_BYTES + trailerBytes); //Used for response timeout during ACK - systemDescriptionAirTime = calcAirTimeMsec(headerBytes + P2P_SYNC_CLOCKS_BYTES + trailerBytes); //Used for response timeout during 3-way handshake maxPacketAirTime = calcAirTimeMsec(MAX_PACKET_SIZE); if ((settings.debug == true) || (settings.debugRadio == true)) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index a04522ed..79a8c104 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -431,7 +431,7 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - timeoutMsec = frameAirTime + systemDescriptionAirTime + settings.overheadTime + getReceiveCompletionOffset(); + timeoutMsec = frameAirTime + (txSyncClocksUsec / 1000) + settings.overheadTime + getReceiveCompletionOffset(); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) From 2d5f46372b7e241589d3d1973ce855324d419d27 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 17:32:29 -1000 Subject: [PATCH 507/594] Replace maxPacketAirTime with maxFrameAirTimeMsec --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 3 +-- Firmware/LoRaSerial_Firmware/Radio.ino | 3 +-- Firmware/LoRaSerial_Firmware/States.ino | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 8f5ad120..c526f2bd 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -378,7 +378,6 @@ uint16_t radioBand = 0; //In MHz. Detected radio module type. Sets the upper/low uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission uint16_t ackAirTime = 0; //Recalc'd with each change of settings -uint16_t maxPacketAirTime = 0; //Recalc'd with each change of settings uint8_t frameSentCount = 0; //Increases each time a frame is sent unsigned long lastPacketReceived = 0; //Controls link LED in broadcast mode @@ -589,7 +588,7 @@ uint32_t txFindPartnerUsec; //Time in microseconds to transmit the FIND_PARTNER uint32_t txHeartbeatUsec; //Time in microseconds to transmit the HEARTBEAT frame uint32_t txSyncClocksUsec; //Time in microseconds to transmit the SYNC_CLOCKS frame uint32_t txDatagramMicros; //Timestamp at the beginning of the transmitDatagram routine -uint16_t maxFrameAirTime; //Air time of the maximum sized message +uint32_t maxFrameAirTimeMsec; //Air time of the maximum sized message unsigned long remoteSystemMillis; //Millis value contained in the received message #define VC_DELAY_HEARTBEAT_MSEC 5 diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 95e609a7..22bb45fc 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -64,7 +64,6 @@ bool configureRadio() //Precalculate the packet times ackAirTime = calcAirTimeMsec(headerBytes + CHANNEL_TIMER_BYTES + trailerBytes); //Used for response timeout during ACK - maxPacketAirTime = calcAirTimeMsec(MAX_PACKET_SIZE); if ((settings.debug == true) || (settings.debugRadio == true)) { @@ -421,7 +420,7 @@ float calcSymbolTimeMsec() //Given spread factor, bandwidth, coding rate and frame size, return most bytes we can push per second uint16_t calcMaxThroughput() { - uint8_t mostFramesPerSecond = 1000 / maxPacketAirTime; + uint8_t mostFramesPerSecond = 1000 / maxFrameAirTimeMsec; uint16_t mostBytesPerSecond = maxDatagramSize * mostFramesPerSecond; return (mostBytesPerSecond); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 79a8c104..38aa2850 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -118,7 +118,7 @@ void updateRadioState() configureRadio(); //Setup radio, set freq to channel 0, calculate air times //Determine the maximum frame air time - maxFrameAirTime = calcAirTimeMsec(MAX_PACKET_SIZE); + maxFrameAirTimeMsec = (calcAirTimeUsec(MAX_PACKET_SIZE) + 500) / 1000; //Start the TX timer: time to delay before transmitting the FIND_PARTNER setHeartbeatShort(); //Both radios start with short heartbeat period @@ -931,7 +931,7 @@ void updateRadioState() { //Check if we are yielding for 2-way comm if (requestYield == false || - (requestYield == true && (millis() - yieldTimerStart > (settings.framesToYield * maxPacketAirTime))) + (requestYield == true && (millis() - yieldTimerStart > (settings.framesToYield * maxFrameAirTimeMsec))) ) { //Yield has expired, allow transmit. From 6260af7636a3f6915ddf84ab500eae3d3a93235e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 19:08:08 -1000 Subject: [PATCH 508/594] Remove getReceiveCompletionOffset --- Firmware/LoRaSerial_Firmware/States.ino | 10 +++++----- Firmware/LoRaSerial_Firmware/System.ino | 8 -------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 38aa2850..5f25d352 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -431,7 +431,7 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - timeoutMsec = frameAirTime + (txSyncClocksUsec / 1000) + settings.overheadTime + getReceiveCompletionOffset(); + timeoutMsec = frameAirTime + (txSyncClocksUsec / 1000) + settings.overheadTime + (settings.txToRxUsec / 1000); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) @@ -513,7 +513,7 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + (settings.txToRxUsec / 1000); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) @@ -1131,7 +1131,7 @@ void updateRadioState() else if (receiveInProcess() == false) { //Check for a receive timeout - timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + (settings.txToRxUsec / 1000); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) @@ -2015,7 +2015,7 @@ void updateRadioState() { //Verify that the link is still up txDestVc = rexmtTxDestVc; - timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + (settings.txToRxUsec / 1000); if ((txDestVc != VC_BROADCAST) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { @@ -2121,7 +2121,7 @@ void updateRadioState() if (vcConnecting & (1 << index)) { //Determine if UNKNOWN_ACKS needs to be sent - timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + getReceiveCompletionOffset(); + timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + (settings.txToRxUsec / 1000); if (virtualCircuitList[index].vcState <= VC_STATE_SEND_UNKNOWN_ACKS) { //Send the UNKNOWN_ACKS datagram, first part of the 3-way handshake diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 80b08718..15749985 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -1159,14 +1159,6 @@ void triggerFrequency(uint16_t frequency) digitalWrite(pin_trigger, HIGH); } -//The difference between when a transmitter is finished and when the receiver is finished -//is different between airSpeeds and affects things like ACK timeouts at lower airSpeeds -//Offsets were found using a logic analyzer but it looks like (1 * Tsym) or calcSymbolTime() -int16_t getReceiveCompletionOffset() -{ - return settings.txToRxUsec / 1000; -} - //Verify the VC_STATE_TYPE enums against the vcStateNames table const char * verifyVcStateNames() { From 85f82e72ec704835ee90cecc6ef9b1f677247b7b Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 19:57:41 -1000 Subject: [PATCH 509/594] Properly initialize the heartbeat timeout during RADIO_RESET --- Firmware/LoRaSerial_Firmware/States.ino | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 5f25d352..c91659d1 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -120,8 +120,6 @@ void updateRadioState() //Determine the maximum frame air time maxFrameAirTimeMsec = (calcAirTimeUsec(MAX_PACKET_SIZE) + 500) / 1000; - //Start the TX timer: time to delay before transmitting the FIND_PARTNER - setHeartbeatShort(); //Both radios start with short heartbeat period randomTime = random(ackAirTime, ackAirTime * 2); //Fast FIND_PARTNER sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet @@ -139,6 +137,7 @@ void updateRadioState() getTxTime(xmitDatagramP2PSyncClocks, &txSyncClocksUsec, "SYNC_CLOCKS"); getTxTime(xmitDatagramP2PHeartbeat, &txHeartbeatUsec, "HEARTBEAT"); getTxTime(xmitDatagramP2PAck, &txDataAckUsec, "ACK"); + setHeartbeatShort(); //Both radios start with short heartbeat period //Start receiving returnToReceiving(); @@ -151,6 +150,7 @@ void updateRadioState() { //Determine transmit frame time for HEARTBEAT getTxTime(xmitVcHeartbeat, &txHeartbeatUsec, "HEARTBEAT"); + setVcHeartbeatTimer(); if (settings.server) { //Reserve the server's address (0) @@ -175,6 +175,7 @@ void updateRadioState() getTxTime(xmitDatagramP2PSyncClocks, &txSyncClocksUsec, "SYNC_CLOCKS"); getTxTime(xmitDatagramMpHeartbeat, &txHeartbeatUsec, "HEARTBEAT"); getTxTime(xmitDatagramP2PAck, &txDataAckUsec, "ACK"); + setHeartbeatMultipoint(); //Start receiving returnToReceiving(); From 121b667cd35f8104f388ec5f75d0f7a999481851 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 20:15:10 -1000 Subject: [PATCH 510/594] Replace ackAirTime with txDataAckUsec --- .../LoRaSerial_Firmware.ino | 1 - Firmware/LoRaSerial_Firmware/Radio.ino | 7 +------ Firmware/LoRaSerial_Firmware/States.ino | 20 +++++++++---------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index c526f2bd..eb28181e 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -377,7 +377,6 @@ bool cylonPatternGoingLeft; uint16_t radioBand = 0; //In MHz. Detected radio module type. Sets the upper/lower frequency bounds. uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission -uint16_t ackAirTime = 0; //Recalc'd with each change of settings uint8_t frameSentCount = 0; //Increases each time a frame is sent unsigned long lastPacketReceived = 0; //Controls link LED in broadcast mode diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 22bb45fc..3354b13a 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -62,9 +62,6 @@ bool configureRadio() if (radio.setFHSSHoppingPeriod(hoppingPeriod) != RADIOLIB_ERR_NONE) success = false; - //Precalculate the packet times - ackAirTime = calcAirTimeMsec(headerBytes + CHANNEL_TIMER_BYTES + trailerBytes); //Used for response timeout during ACK - if ((settings.debug == true) || (settings.debugRadio == true)) { systemPrint("Freq: "); @@ -84,8 +81,6 @@ bool configureRadio() systemPrintln(" mSec"); systemPrint("HoppingPeriod: "); systemPrintln(hoppingPeriod); - systemPrint("ackAirTime: "); - systemPrintln(ackAirTime); outputSerialData(true); } @@ -3107,7 +3102,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) //Compute the interval before a retransmission occurs in milliseconds, //this value increases with each transmission - retransmitTimeout = random(ackAirTime, frameAirTime + ackAirTime) + retransmitTimeout = random(txDataAckUsec / 1000, frameAirTime + (txDataAckUsec / 1000)) * (frameSentCount + 1) * 3 / 2; //BLink the TX LED diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c91659d1..d0ef4efb 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -120,8 +120,6 @@ void updateRadioState() //Determine the maximum frame air time maxFrameAirTimeMsec = (calcAirTimeUsec(MAX_PACKET_SIZE) + 500) / 1000; - randomTime = random(ackAirTime, ackAirTime * 2); //Fast FIND_PARTNER - sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet petWDT(); @@ -139,6 +137,8 @@ void updateRadioState() getTxTime(xmitDatagramP2PAck, &txDataAckUsec, "ACK"); setHeartbeatShort(); //Both radios start with short heartbeat period + randomTime = random(txDataAckUsec, txDataAckUsec * 2) / 1000; //Fast FIND_PARTNER + //Start receiving returnToReceiving(); changeState(RADIO_P2P_LINK_DOWN); @@ -447,10 +447,10 @@ void updateRadioState() setHeartbeatShort(); //Slow down FIND_PARTNERs - if (ackAirTime < settings.maxDwellTime) + if ((txDataAckUsec / 1000) < settings.maxDwellTime) randomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); else - randomTime = random(ackAirTime * 4, ackAirTime * 8); + randomTime = random(txDataAckUsec * 4, txDataAckUsec * 8) / 1000; sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet returnToReceiving(); @@ -514,7 +514,7 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + (settings.txToRxUsec / 1000); + timeoutMsec = frameAirTime + settings.overheadTime + ((txDataAckUsec + settings.txToRxUsec) / 1000); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) @@ -533,10 +533,10 @@ void updateRadioState() setHeartbeatShort(); //Slow down FIND_PARTNERs - if (ackAirTime < settings.maxDwellTime) + if ((txDataAckUsec / 1000) < settings.maxDwellTime) randomTime = random(settings.maxDwellTime * 2, settings.maxDwellTime * 4); else - randomTime = random(ackAirTime * 4, ackAirTime * 8); + randomTime = random(txDataAckUsec * 4, txDataAckUsec * 8) / 1000; sf6ExpectedSize = headerBytes + CLOCK_MILLIS_BYTES + trailerBytes; //Tell SF6 to receive FIND_PARTNER packet returnToReceiving(); @@ -1132,7 +1132,7 @@ void updateRadioState() else if (receiveInProcess() == false) { //Check for a receive timeout - timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + (settings.txToRxUsec / 1000); + timeoutMsec = frameAirTime + settings.overheadTime + ((txDataAckUsec + settings.txToRxUsec) / 1000); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) @@ -2016,7 +2016,7 @@ void updateRadioState() { //Verify that the link is still up txDestVc = rexmtTxDestVc; - timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + (settings.txToRxUsec / 1000); + timeoutMsec = frameAirTime + settings.overheadTime + ((txDataAckUsec + settings.txToRxUsec) / 1000); if ((txDestVc != VC_BROADCAST) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { @@ -2122,7 +2122,7 @@ void updateRadioState() if (vcConnecting & (1 << index)) { //Determine if UNKNOWN_ACKS needs to be sent - timeoutMsec = frameAirTime + ackAirTime + settings.overheadTime + (settings.txToRxUsec / 1000); + timeoutMsec = frameAirTime + settings.overheadTime + ((txDataAckUsec + settings.txToRxUsec) / 1000); if (virtualCircuitList[index].vcState <= VC_STATE_SEND_UNKNOWN_ACKS) { //Send the UNKNOWN_ACKS datagram, first part of the 3-way handshake From 0d3eccc618addcaf6a96a1a85def0d0a0a59a408 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 29 Jan 2023 20:40:19 -1000 Subject: [PATCH 511/594] Replace frameAirTime with frameAirTimeUsec --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- Firmware/LoRaSerial_Firmware/Radio.ino | 12 ++++++------ Firmware/LoRaSerial_Firmware/States.ino | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index eb28181e..ccc35e92 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -376,7 +376,7 @@ bool cylonPatternGoingLeft; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- uint16_t radioBand = 0; //In MHz. Detected radio module type. Sets the upper/lower frequency bounds. uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver -uint16_t frameAirTime = 0; //Recalc'd with each new packet transmission +uint16_t frameAirTimeUsec; //Recalc'd with each new packet transmission uint8_t frameSentCount = 0; //Increases each time a frame is sent unsigned long lastPacketReceived = 0; //Controls link LED in broadcast mode diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 3354b13a..45e19bc4 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3006,8 +3006,8 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); - frameAirTime = calcAirTimeMsec(txDatagramSize); //Calculate frame air size while we're transmitting in the background - uint16_t responseDelay = frameAirTime / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond + frameAirTimeUsec = calcAirTimeUsec(txDatagramSize); //Calculate frame air size while we're transmitting in the background + uint16_t responseDelay = frameAirTimeUsec / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond //Drop this datagram if the receiver is active if ( @@ -3068,8 +3068,8 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) { systemPrintTimestamp(); systemPrint("TX: frameAirTime "); - systemPrint(frameAirTime); - systemPrintln(" mSec"); + systemPrint(frameAirTimeUsec); + systemPrintln(" uSec"); outputSerialData(true); if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); @@ -3097,12 +3097,12 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) } } - frameAirTime += responseDelay; + frameAirTimeUsec += responseDelay; datagramTimer = millis(); //Move timestamp even if error //Compute the interval before a retransmission occurs in milliseconds, //this value increases with each transmission - retransmitTimeout = random(txDataAckUsec / 1000, frameAirTime + (txDataAckUsec / 1000)) + retransmitTimeout = random(txDataAckUsec / 1000, ((frameAirTimeUsec + txDataAckUsec) / 1000)) * (frameSentCount + 1) * 3 / 2; //BLink the TX LED diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d0ef4efb..c2113bfd 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -432,7 +432,7 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - timeoutMsec = frameAirTime + (txSyncClocksUsec / 1000) + settings.overheadTime + (settings.txToRxUsec / 1000); + timeoutMsec = ((frameAirTimeUsec + txSyncClocksUsec) / 1000) + settings.overheadTime + (settings.txToRxUsec / 1000); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) @@ -514,7 +514,7 @@ void updateRadioState() else { //If we timeout during handshake, return to link down - timeoutMsec = frameAirTime + settings.overheadTime + ((txDataAckUsec + settings.txToRxUsec) / 1000); + timeoutMsec = settings.overheadTime + ((frameAirTimeUsec + txDataAckUsec + settings.txToRxUsec) / 1000); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) @@ -1132,7 +1132,7 @@ void updateRadioState() else if (receiveInProcess() == false) { //Check for a receive timeout - timeoutMsec = frameAirTime + settings.overheadTime + ((txDataAckUsec + settings.txToRxUsec) / 1000); + timeoutMsec = settings.overheadTime + ((frameAirTimeUsec + txDataAckUsec + settings.txToRxUsec) / 1000); if ((millis() - datagramTimer) >= timeoutMsec) { if (settings.debugDatagrams) @@ -2016,7 +2016,7 @@ void updateRadioState() { //Verify that the link is still up txDestVc = rexmtTxDestVc; - timeoutMsec = frameAirTime + settings.overheadTime + ((txDataAckUsec + settings.txToRxUsec) / 1000); + timeoutMsec = settings.overheadTime + ((frameAirTimeUsec + txDataAckUsec + settings.txToRxUsec) / 1000); if ((txDestVc != VC_BROADCAST) && (virtualCircuitList[txDestVc & VCAB_NUMBER_MASK].vcState == VC_STATE_LINK_DOWN)) { @@ -2122,7 +2122,7 @@ void updateRadioState() if (vcConnecting & (1 << index)) { //Determine if UNKNOWN_ACKS needs to be sent - timeoutMsec = frameAirTime + settings.overheadTime + ((txDataAckUsec + settings.txToRxUsec) / 1000); + timeoutMsec = settings.overheadTime + ((frameAirTimeUsec + txDataAckUsec + settings.txToRxUsec) / 1000); if (virtualCircuitList[index].vcState <= VC_STATE_SEND_UNKNOWN_ACKS) { //Send the UNKNOWN_ACKS datagram, first part of the 3-way handshake From d8bb05ca0e7479805beecbed580f625d3b6ac9d3 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 30 Jan 2023 10:51:11 -0700 Subject: [PATCH 512/594] Increase sizeof frameAirTimeUsec to handle > 65ms frames. --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index ccc35e92..e144e64f 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -376,7 +376,7 @@ bool cylonPatternGoingLeft; //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- uint16_t radioBand = 0; //In MHz. Detected radio module type. Sets the upper/lower frequency bounds. uint8_t outgoingPacket[MAX_PACKET_SIZE]; //Contains the current data in route to receiver -uint16_t frameAirTimeUsec; //Recalc'd with each new packet transmission +uint32_t frameAirTimeUsec; //Recalc'd with each new packet transmission uint8_t frameSentCount = 0; //Increases each time a frame is sent unsigned long lastPacketReceived = 0; //Controls link LED in broadcast mode From 9d59aff9d548d1814cc53b2a72b05fe39da0caf9 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 30 Jan 2023 10:54:00 -0700 Subject: [PATCH 513/594] Whitespace formatting changes --- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 2 +- Firmware/LoRaSerial_Firmware/Commands.ino | 4 +- Firmware/LoRaSerial_Firmware/Radio.ino | 9 ++-- Firmware/LoRaSerial_Firmware/Serial.ino | 54 +++++++++---------- Firmware/LoRaSerial_Firmware/System.ino | 38 ++++++------- .../Virtual_Circuit_Protocol.h | 4 +- 6 files changed, 56 insertions(+), 55 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 7d692b39..9fa78171 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -231,7 +231,7 @@ void samdSystemReset() //Get the CPU's unique ID value void samdUniqueID(uint8_t * unique128_BitID) { - uint32_t id[UNIQUE_ID_BYTES/4]; + uint32_t id[UNIQUE_ID_BYTES / 4]; //Read the CPU's unique ID value id[0] = *(uint32_t *)0x0080a00c; diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index e8eaacb3..c4a828ff 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -885,7 +885,7 @@ char * trimCommand() commandString[commandLength] = 0; //Remove the trailing white space - while (commandLength && isspace(commandString[commandLength -1])) + while (commandLength && isspace(commandString[commandLength - 1])) commandString[--commandLength] = 0; //Return the remainder as the command @@ -1165,7 +1165,7 @@ const COMMAND_ENTRY commands[] = /*Training parameters Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ - {'R', 0, 1, 1, 255, 0, TYPE_U8, valInt,"ClientFindPartnerRetryInterval",&tempSettings.clientFindPartnerRetryInterval}, + {'R', 0, 1, 1, 255, 0, TYPE_U8, valInt, "ClientFindPartnerRetryInterval", &tempSettings.clientFindPartnerRetryInterval}, {'R', 0, 0, 0, 0, 0, TYPE_KEY, valKey, "TrainingKey", &tempSettings.trainingKey}, {'R', 0, 0, 1, 255, 0, TYPE_U8, valInt, "TrainingTimeout", &tempSettings.trainingTimeout}, diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 45e19bc4..6d3f627f 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -276,15 +276,15 @@ float calcAirTimeUsec(uint8_t bytesToSend) //With SF = 6 selected, implicit header mode is the only mode of operation possible. uint8_t explicitHeader = 0; //1 for implicit header - if(settings.radioSpreadFactor == 6) explicitHeader = 1; + if (settings.radioSpreadFactor == 6) explicitHeader = 1; //LowDataRateOptimize increases the robustness of the LoRa link at low effective data //rates. Its use is mandated when the symbol duration exceeds 16ms. //We choose to enable LDRO for airSpeed of 400 even though TSym is 8.2ms. uint8_t useLowDataRateOptimization = (tSymUsec >= 8192); float p1 = ((8 * bytesToSend) - (4 * settings.radioSpreadFactor) + 28 - + (16 * useHardwareCRC) - (20 * explicitHeader)) - / (4.0 * (settings.radioSpreadFactor - (2 * useLowDataRateOptimization))); + + (16 * useHardwareCRC) - (20 * explicitHeader)) + / (4.0 * (settings.radioSpreadFactor - (2 * useLowDataRateOptimization))); p1 = ceil(p1) * settings.radioCodingRate; if (p1 < 0) p1 = 0; uint16_t payloadBytes = 8 + p1; @@ -3007,6 +3007,7 @@ bool retransmitDatagram(VIRTUAL_CIRCUIT * vc) hopChannel(); frameAirTimeUsec = calcAirTimeUsec(txDatagramSize); //Calculate frame air size while we're transmitting in the background + uint16_t responseDelay = frameAirTimeUsec / responseDelayDivisor; //Give the receiver a bit of wiggle time to respond //Drop this datagram if the receiver is active @@ -3406,7 +3407,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) msToNextHop = rmtHopTimeMsec + adjustment; //For low airspeeds multiple hops may occur resulting in a negative value - while(msToNextHop < 0) msToNextHop += settings.maxDwellTime; + while (msToNextHop < 0) msToNextHop += settings.maxDwellTime; //When the ISR fires, reload the channel timer with settings.maxDwellTime reloadChannelTimer = true; diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 63b41f58..40cc2c30 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -70,9 +70,9 @@ void serialOutputByte(uint8_t data) //Update the output of the RTS pin (host says it's ok to send data when assertRTS = true) void updateRTS(bool assertRTS) { - rtsAsserted = assertRTS; - if (settings.flowControl && (pin_rts != PIN_UNDEFINED)) - digitalWrite(pin_rts, assertRTS ^ settings.invertRts); + rtsAsserted = assertRTS; + if (settings.flowControl && (pin_rts != PIN_UNDEFINED)) + digitalWrite(pin_rts, assertRTS ^ settings.invertRts); } //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= @@ -209,7 +209,7 @@ uint8_t readyOutgoingCommandPacket(uint16_t offset) //Determine the amount of data in the buffer bytesToSend = availableTXCommandBytes(); if ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - && (commandTXBuffer[commandTXTail] != START_OF_VC_SERIAL)) + && (commandTXBuffer[commandTXTail] != START_OF_VC_SERIAL)) bytesToSend -= VC_SERIAL_HEADER_BYTES + sizeof(VC_COMMAND_COMPLETE_MESSAGE); //Limit the length to the frame size @@ -258,7 +258,7 @@ void updateSerial() //Assert RTS when there is enough space in the receive buffer if ((!rtsAsserted) && (availableRXBytes() < (sizeof(serialReceiveBuffer) / 2)) - && (availableTXBytes() < (sizeof(serialTransmitBuffer) / 4))) + && (availableTXBytes() < (sizeof(serialTransmitBuffer) / 4))) updateRTS(true); //Attempt to empty the serialTransmitBuffer @@ -347,8 +347,8 @@ void processSerialInput() //Process the serial data radioHead = radioTxHead; while (availableRXBytes() - && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - maxEscapeCharacters)) - && (transactionComplete == false)) + && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - maxEscapeCharacters)) + && (transactionComplete == false)) { //Take a break if there are ISRs to attend to petWDT(); @@ -406,8 +406,8 @@ void processSerialInput() //Ignore escape characters received within 2 seconds of serial traffic //Allow escape characters received within first 2 seconds of power on if ((incoming == escapeCharacter) - && ((millis() - lastByteReceived_ms > minEscapeTime_ms) - || (millis() < minEscapeTime_ms))) + && ((millis() - lastByteReceived_ms > minEscapeTime_ms) + || (millis() < minEscapeTime_ms))) { escapeCharsReceived++; lastEscapeCharEnteredMillis = millis(); @@ -454,7 +454,7 @@ void processSerialInput() //command mode. The escape characters must be part of the input stream but were //the last characters entered. Send these characters over the long range radio. if (escapeCharsReceived && (millis() - lastEscapeCharEnteredMillis > minEscapeTime_ms) - && (availableRXBytes() < (sizeof(radioTxBuffer) - maxEscapeCharacters))) + && (availableRXBytes() < (sizeof(radioTxBuffer) - maxEscapeCharacters))) { //Replay the escape characters while (escapeCharsReceived) @@ -621,7 +621,7 @@ bool vcSerialMessageReceived() //Verify that the destination link is connected if ((vcDest != VC_BROADCAST) - && (virtualCircuitList[vcIndex].vcState < VC_STATE_CONNECTED)) + && (virtualCircuitList[vcIndex].vcState < VC_STATE_CONNECTED)) { if (settings.debugSerial || settings.debugTransmit) { @@ -655,7 +655,7 @@ bool vcSerialMessageReceived() readyOutgoingPacket(msgLength); channel = GET_CHANNEL_NUMBER(vcDest); if ((vcDest != VC_BROADCAST) && ((vcDest & VCAB_NUMBER_MASK) == myVc) - && (channel == 0)) + && (channel == 0)) { if (settings.debugSerial) systemPrintln("VC: Sending data to ourself"); @@ -676,20 +676,20 @@ bool vcSerialMessageReceived() //Notify the PC of the message delivery failure void vcSendPcAckNack(int8_t vcIndex, bool ackMsg) { - //Build the VC state message - VC_DATA_ACK_NACK_MESSAGE message; - message.msgDestVc = vcIndex; - - //Build the message header - VC_SERIAL_MESSAGE_HEADER header; - header.start = START_OF_VC_SERIAL; - header.radio.length = VC_RADIO_HEADER_BYTES + sizeof(message); - header.radio.destVc = ackMsg ? PC_DATA_ACK : PC_DATA_NACK; - header.radio.srcVc = myVc; - - //Send the VC state message - systemWrite((uint8_t *)&header, sizeof(header)); - systemWrite((uint8_t *)&message, sizeof(message)); + //Build the VC state message + VC_DATA_ACK_NACK_MESSAGE message; + message.msgDestVc = vcIndex; + + //Build the message header + VC_SERIAL_MESSAGE_HEADER header; + header.start = START_OF_VC_SERIAL; + header.radio.length = VC_RADIO_HEADER_BYTES + sizeof(message); + header.radio.destVc = ackMsg ? PC_DATA_ACK : PC_DATA_NACK; + header.radio.srcVc = myVc; + + //Send the VC state message + systemWrite((uint8_t *)&header, sizeof(header)); + systemWrite((uint8_t *)&message, sizeof(message)); } //Process serial input when running in MODE_VIRTUAL_CIRCUIT @@ -706,7 +706,7 @@ void vcProcessSerialInput() //Process the serial data while there is space in radioTxBuffer dataBytes = availableRXBytes(); while (dataBytes && (availableRadioTXBytes() < (sizeof(radioTxBuffer) - 256)) - && (transactionComplete == false)) + && (transactionComplete == false)) { //Take a break if there are ISRs to attend to petWDT(); diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 15749985..4217b146 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -622,23 +622,23 @@ void blinkRadioRssiLed() { lastCylonBlink = currentMillis; - //The following shows the cylon pattern in four LEDs - // - // LED3 LED2 LED1 LED0 - // X - // X - // X - // X - // X - // X - // X - // X - // ... - // + //The following shows the cylon pattern in four LEDs + // + // LED3 LED2 LED1 LED0 + // X + // X + // X + // X + // X + // X + // X + // X + // ... + // //Cylon the RSSI LEDs setRSSI(cylonLedPattern); - //Determine if a change in direction is necessary + //Determine if a change in direction is necessary if ((cylonLedPattern == 0b1000) || (cylonLedPattern == 0b0001)) cylonPatternGoingLeft = cylonPatternGoingLeft ? false : true; @@ -714,14 +714,14 @@ void blinkSerialRxLed(bool illuminate) digitalWrite(pin_rxLED, HIGH); else digitalWrite(pin_rxLED, LOW); - break; + break; case LEDS_VC: if (illuminate == true) digitalWrite(RADIO_USE_LINK_LED, HIGH); else digitalWrite(RADIO_USE_LINK_LED, LOW); - break; + break; } } @@ -968,13 +968,13 @@ void vcLeds() //Turn on the RSSI LED else if (((currentMillis - blinkSyncMillis) >= (VC_SYNC_BLINK_RATE >> 1)) - && (digitalRead(RADIO_USE_RSSI_LED) == LED_OFF)) + && (digitalRead(RADIO_USE_RSSI_LED) == LED_OFF)) digitalWrite(RADIO_USE_RSSI_LED, LED_ON); //Turn off the RSSI LED else if ((!virtualCircuitList[VC_SERVER].vcState) - && (((currentMillis - blinkSyncMillis) >= VC_SYNC_BLINK_RATE)) - && (digitalRead(RADIO_USE_RSSI_LED) == LED_ON)) + && (((currentMillis - blinkSyncMillis) >= VC_SYNC_BLINK_RATE)) + && (digitalRead(RADIO_USE_RSSI_LED) == LED_ON)) { digitalWrite(RADIO_USE_RSSI_LED, LED_OFF); blinkSyncMillis = currentMillis; diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h index bccad830..bf59bc8c 100644 --- a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h +++ b/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h @@ -73,11 +73,11 @@ // Protocol Exchanges //------------------------------------------------------------------------------ /* -Host Interaction using Virtual-Circuits + Host Interaction using Virtual-Circuits Host A LoRa A LoRa B Host B -All output goes to serial . + All output goes to serial . . +++ ----> . <---- OK From 30b63af5ad35af55ec3de27a8d64c1e0b828fda5 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Mon, 30 Jan 2023 13:57:22 -0700 Subject: [PATCH 514/594] Prevent channel timer from starting if P2P link times out. --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 6d3f627f..5f21ad51 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -2610,7 +2610,7 @@ bool transmitDatagram() //Add the clock sync information if (settings.frequencyHop == true) { - //Hake sure that the transmitted msToNextHop is in the range 0 - maxDwellTime + //Make sure that the transmitted msToNextHop is in the range 0 - maxDwellTime if (timeToHop) hopChannel(); diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index c2113bfd..d55b125e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -442,6 +442,9 @@ void updateRadioState() outputSerialData(true); } + stopChannelTimer(); //Stop channel timer if no response received + startChannelTimerPending = false; + //Start the TX timer: time to delay before transmitting the FIND_PARTNER triggerEvent(TRIGGER_HANDSHAKE_SYNC_CLOCKS_TIMEOUT); setHeartbeatShort(); From 7050a87f891439c26b758f0f96130ce75ab88ab6 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 30 Jan 2023 17:01:42 -1000 Subject: [PATCH 515/594] Define and use NEXT_COMMAND_TX_TAIL macro --- Firmware/LoRaSerial_Firmware/Serial.ino | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 40cc2c30..2f453d52 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -160,6 +160,8 @@ uint16_t availableTXCommandBytes() return (sizeof(commandTXBuffer) - commandTXTail + commandTXHead); } +#define NEXT_COMMAND_TX_TAIL(n) ((commandTXTail + n) % sizeof(commandTXBuffer)) + //Send a portion of the commandTXBuffer to serialTransmitBuffer void readyLocalCommandPacket() { @@ -194,8 +196,8 @@ void readyLocalCommandPacket() } for (length = 0; length < bytesToSend; length++) { - serialOutputByte(commandTXBuffer[commandTXTail++]); - commandTXTail %= sizeof(commandTXBuffer); + serialOutputByte(commandTXBuffer[commandTXTail]); + commandTXTail = NEXT_COMMAND_TX_TAIL(1); } } @@ -238,8 +240,7 @@ uint8_t readyOutgoingCommandPacket(uint16_t offset) //Copy the remaining portion of the buffer memcpy(&outgoingPacket[headerBytes + offset + length], &commandTXBuffer[commandTXTail], bytesToSend - length); - commandTXTail += bytesToSend - length; - commandTXTail %= sizeof(commandTXBuffer); + commandTXTail = NEXT_COMMAND_TX_TAIL(bytesToSend - length); endOfTxData += bytesToSend; return (uint8_t)bytesToSend; } From 3a6e69a8914ed0718a233129f531ad3f851efcfe Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 30 Jan 2023 17:09:19 -1000 Subject: [PATCH 516/594] Define and use the NEXT_RADIO_TX_TAIL macro --- Firmware/LoRaSerial_Firmware/Serial.ino | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 2f453d52..9b05716c 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -87,6 +87,7 @@ uint16_t availableRadioTXBytes() } #define NEXT_RADIO_TX_HEAD(n) ((radioTxHead + n) % sizeof(radioTxBuffer)) +#define NEXT_RADIO_TX_TAIL(n) ((radioTxTail + n) % sizeof(radioTxBuffer)) //If we have data to send, get the packet ready //Return true if new data is ready to be sent @@ -133,8 +134,7 @@ void readyOutgoingPacket(uint16_t bytesToSend) //Copy the remaining portion of the buffer memcpy(&outgoingPacket[headerBytes + length], &radioTxBuffer[radioTxTail], bytesToSend - length); - radioTxTail += bytesToSend - length; - radioTxTail %= sizeof(radioTxBuffer); + radioTxTail = NEXT_RADIO_TX_TAIL(bytesToSend - length); endOfTxData += bytesToSend; } @@ -520,9 +520,7 @@ uint8_t vcSerialMsgGetVcDest() uint16_t index; //Get the destination address byte - index = radioTxTail + 1; - if (index >= sizeof(radioTxBuffer)) - index -= sizeof(radioTxBuffer); + index = NEXT_RADIO_TX_TAIL(1); return radioTxBuffer[index]; } @@ -590,9 +588,7 @@ bool vcSerialMessageReceived() } //Discard this message - radioTxTail += msgLength; - if (radioTxTail >= sizeof(radioTxBuffer)) - radioTxTail -= sizeof(radioTxBuffer); + radioTxTail = NEXT_RADIO_TX_TAIL(msgLength); break; } @@ -606,9 +602,7 @@ bool vcSerialMessageReceived() } //Discard this message, it is too long to transmit over the radio link - radioTxTail += msgLength; - if (radioTxTail >= sizeof(radioTxBuffer)) - radioTxTail -= sizeof(radioTxBuffer); + radioTxTail = NEXT_RADIO_TX_TAIL(msgLength); //Nothing to do for invalid addresses or the broadcast address if (((uint8_t)vcDest >= (uint8_t)MIN_TX_NOT_ALLOWED) && (vcDest != VC_BROADCAST)) @@ -633,9 +627,7 @@ bool vcSerialMessageReceived() } //Discard this message - radioTxTail += msgLength; - if (radioTxTail >= sizeof(radioTxBuffer)) - radioTxTail -= sizeof(radioTxBuffer); + radioTxTail = NEXT_RADIO_TX_TAIL(msgLength); //If the PC is trying to send this message then notify the PC of the delivery failure if ((uint8_t)vcDest < (uint8_t)MIN_TX_NOT_ALLOWED) From 3c196ea23613af2c2d02bb269da31ba176fc816c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 30 Jan 2023 17:33:46 -1000 Subject: [PATCH 517/594] Output a line at a time in dumpCircularBuffer --- Firmware/LoRaSerial_Firmware/System.ino | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 4217b146..4f7f7e2f 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -517,11 +517,6 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, systemPrint((uint16_t)(offset % bufferLength), HEX); systemPrint(": "); - //Determine the number of bytes to display - bytes = length; - if (bytes > displayWidth) - bytes = displayWidth; - //Adjust for the offset delta = offset % displayWidth; if (delta) @@ -536,6 +531,11 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, } } + //Determine the number of bytes to display + bytes = length; + if (bytes > (displayWidth - delta)) + bytes = displayWidth - delta; + //Display the data bytes in hex for (index = 0; index < bytes; index++) { @@ -568,6 +568,7 @@ void dumpCircularBuffer(uint8_t * buffer, uint16_t tail, uint16_t bufferLength, if (timeToHop == true) //If the channelTimer has expired, move to next frequency hopChannel(); petWDT(); + outputSerialData(true); offset += bytes; length -= bytes; } From d324a857782feb7b996ff5cf353d4aa53d5da89e Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 30 Jan 2023 17:44:05 -1000 Subject: [PATCH 518/594] Remove vcSerialMsgGetVcDest --- Firmware/LoRaSerial_Firmware/Serial.ino | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 9b05716c..23044937 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -514,16 +514,6 @@ bool vcSerialMsgGetLengthByte(uint8_t * msgLength) return true; } -//Get the destination virtual circuit byte from the serial buffer -uint8_t vcSerialMsgGetVcDest() -{ - uint16_t index; - - //Get the destination address byte - index = NEXT_RADIO_TX_TAIL(1); - return radioTxBuffer[index]; -} - //Determine if received serial data may be sent to the remote system bool vcSerialMessageReceived() { @@ -573,7 +563,7 @@ bool vcSerialMessageReceived() //vcProcessSerialInput validates the vcDest value, this check validates //that internally generated traffic uses valid vcDest values. Only messages //enabled for receive on a remote radio may be transmitted. - vcDest = vcSerialMsgGetVcDest(); + vcDest = radioTxBuffer[NEXT_RADIO_TX_TAIL(1)]; vcIndex = -1; if ((uint8_t)vcDest < (uint8_t)MIN_RX_NOT_ALLOWED) vcIndex = vcDest & VCAB_NUMBER_MASK; From f13c3651a63f32efdb369d962e46a6c90072cb1f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 31 Jan 2023 07:16:56 -1000 Subject: [PATCH 519/594] Display the bytes placed into outgoingPacket --- Firmware/LoRaSerial_Firmware/Serial.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 23044937..5d026fac 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -631,7 +631,7 @@ bool vcSerialMessageReceived() systemPrint("Readying "); systemPrint(msgLength); systemPrintln(" byte for transmission"); - outputSerialData(true); + dumpCircularBuffer(radioTxBuffer, radioTxTail, sizeof(radioTxBuffer), msgLength); } //If sending to ourself, just place the data in the serial output buffer From c4bd0b7658559596dc5324b590342f6e590ca6a5 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 31 Jan 2023 07:20:05 -1000 Subject: [PATCH 520/594] VC: Fix infinite output, START_OF_VC_SERIAL already removed --- Firmware/LoRaSerial_Firmware/Serial.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 5d026fac..858901a2 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -780,7 +780,7 @@ void vcProcessSerialInput() } //Discard this message - rxTail = NEXT_RX_TAIL(length + 1); + rxTail = NEXT_RX_TAIL(length); break; } From 0be639427a0778e86865747da634147ddd3ccca1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 31 Jan 2023 06:08:49 -1000 Subject: [PATCH 521/594] VC: Ensure the entire command complete message is contained in one frame --- Firmware/LoRaSerial_Firmware/Serial.ino | 43 +++++++++++++++++++++---- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial_Firmware/Serial.ino index 858901a2..f9cdcdd3 100644 --- a/Firmware/LoRaSerial_Firmware/Serial.ino +++ b/Firmware/LoRaSerial_Firmware/Serial.ino @@ -208,24 +208,53 @@ uint8_t readyOutgoingCommandPacket(uint16_t offset) uint16_t length; uint16_t maxLength; - //Determine the amount of data in the buffer - bytesToSend = availableTXCommandBytes(); - if ((settings.operatingMode == MODE_VIRTUAL_CIRCUIT) - && (commandTXBuffer[commandTXTail] != START_OF_VC_SERIAL)) - bytesToSend -= VC_SERIAL_HEADER_BYTES + sizeof(VC_COMMAND_COMPLETE_MESSAGE); - //Limit the length to the frame size maxLength = maxDatagramSize - offset; + bytesToSend = availableTXCommandBytes(); if (bytesToSend > maxLength) + { bytesToSend = maxLength; + //checkCommand delivers the entire command response to the commandTXBuffer. + //The response to be broken into multiple frames for transmission to the remote + //radio and host. The code below separates the commnad response from the + //VC_COMMAND_COMPLETE_MESSAGE which follows the response. This separation + //ensures that the entire VC_COMMAND_COMPLETE_MESSAGE is delivered within a + //single frame. The result enables easy detection by the remote radio. + // + //Determine the number of command response bytes to send + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + { + //Reserve the bytes for the VC heeader + bytesToSend -= VC_SERIAL_HEADER_BYTES; + + //Determine if the VC_COMMAND_COMPLETE_MESSAGE is split across two buffers + + if (commandTXBuffer[commandTXTail] != START_OF_VC_SERIAL) + { + //OK if the entire VC_COMMAND_COMPLETE_MESSAGE is in the buffer. Start + //the search one byte into the VC_COMMAND_COMPLETE_MESSAGE position. + for (length = bytesToSend - VC_SERIAL_HEADER_BYTES + - sizeof(VC_COMMAND_COMPLETE_MESSAGE) + 1; + length < bytesToSend; length++) + if (commandTXBuffer[NEXT_COMMAND_TX_TAIL(length)] == START_OF_VC_SERIAL) + { + //Exclude the partial piece of the VC_COMMAND_COMPLETE_MESSAGE from + //this command response frame. + bytesToSend = length; + break; + } + } + } + } + //Display the amount of data being sent if (settings.debugSerial) { systemPrint("Moving "); systemPrint(bytesToSend); systemPrintln(" bytes from commandTXBuffer into outgoingPacket"); - outputSerialData(true); + dumpCircularBuffer(commandTXBuffer, commandTXTail, sizeof(commandTXBuffer), bytesToSend); } //Determine if the data wraps around to the beginning of the buffer From 4e84329423134231f30fbe7be625463b6c31b226 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 30 Jan 2023 20:47:20 -1000 Subject: [PATCH 522/594] VC: Split out VC_COMMAND_COMPLETE_MESSAGE from the command response --- Firmware/LoRaSerial_Firmware/States.ino | 65 +++++++++++++++++-------- 1 file changed, 46 insertions(+), 19 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d55b125e..3be002e3 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1952,30 +1952,57 @@ void updateRadioState() case DATAGRAM_REMOTE_COMMAND_RESPONSE: triggerEvent(TRIGGER_RX_COMMAND_RESPONSE); - //Determine if this is the VC_COMMAND_COMPLETE_MESSAGE - length = rxDataBytes; - if (*rxVcData == START_OF_VC_SERIAL) + //The VC_COMMAND_COMPLETE_MESSAGE is located at the end of the command + //response. This method of splitting the message works because the + //VC_COMMAND_COMPLETE_MESSAGE is delivered via a separate frame. As + //such, the reception is transactional, the frame is either present or + //it is not. Determine if the VC_COMMAND_COMPLETE_MESSAGE is contained + //in this response. + length = rxDataBytes - VC_SERIAL_HEADER_BYTES - sizeof(VC_COMMAND_COMPLETE_MESSAGE); + if ((length > rxDataBytes) || (rxData[length] != START_OF_VC_SERIAL)) + //The VC_COMMAND_COMPLETE_MESSAGE is not in this portion of the response + length = rxDataBytes; + + //Transfer the response data to the host + if (length) { - //Remove the VC_RADIO_MESSAGE_HEADER and START_OF_VC_SERIAL byte - length -= VC_RADIO_HEADER_BYTES + 1; - rxData += VC_RADIO_HEADER_BYTES + 1; - } - else vcHeader->destVc |= PC_REMOTE_RESPONSE; + vcHeader->length = length; - //Debug the serial path - if (settings.debugSerial) - { - systemPrint("Moving "); - systemPrint(length); - systemPrintln(" from incomingBuffer to serialTransmitBuffer"); - dumpBuffer(rxData, rxDataBytes); - outputSerialData(true); + //Debug the serial path + if (settings.debugSerial) + { + systemPrint("Moving "); + systemPrint(length); + systemPrintln(" from incomingBuffer to serialTransmitBuffer"); + dumpBuffer(rxData, length); + outputSerialData(true); + } + + //Place the data in to the serialTransmitBuffer + serialOutputByte(START_OF_VC_SERIAL); + serialBufferOutput(rxData, length); } - //Place the data in to the serialTransmitBuffer - serialOutputByte(START_OF_VC_SERIAL); - serialBufferOutput(rxData, length); + //Transfer the VC_COMMAND_COMPLETE_MESSAGE + if (length < rxDataBytes) + { + index = length; + length = rxDataBytes - length; + + //Debug the serial path + if (settings.debugSerial) + { + systemPrint("Moving "); + systemPrint(length); + systemPrintln(" from incomingBuffer to serialTransmitBuffer"); + dumpBuffer(&rxData[index], length); + outputSerialData(true); + } + + //Place the data in to the serialTransmitBuffer + serialBufferOutput(&rxData[index], length); + } frequencyCorrection += radio.getFrequencyError() / 1000000.0; From ad21c7239fec1922f41607034a6b85dbde4ee2c7 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 31 Jan 2023 08:22:02 -1000 Subject: [PATCH 523/594] VcServerTest: Support remote command scripts, one command at a time --- Firmware/Tools/VcServerTest.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 60719cca..96c1dae9 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -184,6 +184,13 @@ int stdinToRadio() break; } + //Determine if this is a remote command + if ((remoteVc >= PC_REMOTE_COMMAND) && (remoteVc < THIRD_PARTY_COMMAND)) + { + remoteCommandVc = remoteVc & VCAB_NUMBER_MASK; + waitingForCommandComplete = true; + } + //Send this data over the VC bytesSent = 0; while (bytesSent < bytesRead) From 5d70b20e964d972256795d3ed2800e69fd205744 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 31 Jan 2023 08:37:34 -1000 Subject: [PATCH 524/594] Sprinkler Controller: Set the default setting values --- Firmware/LoRaSerial_Firmware/settings.h | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index d7a3b2d2..b26db355 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -419,9 +419,9 @@ typedef struct struct_settings { //Radio protocol parameters //---------------------------------------- - uint8_t operatingMode = MODE_POINT_TO_POINT; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. + uint8_t operatingMode = MODE_VIRTUAL_CIRCUIT; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. - uint8_t selectLedUse = LEDS_RSSI; //Select LED use + uint8_t selectLedUse = LEDS_VC; //Select LED use bool server = false; //Default to being a client, enable server for multipoint, VC and training uint8_t netID = 192; //Both radios must share a network ID bool verifyRxNetID = true; //Verify RX netID value when not operating in point-to-point mode @@ -429,8 +429,8 @@ typedef struct struct_settings { uint8_t encryptionKey[AES_KEY_BYTES] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; bool encryptData = true; //AES encrypt each packet - bool dataScrambling = false; //Use IBM Data Whitening to reduce DC bias - bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive + bool dataScrambling = true; //Use IBM Data Whitening to reduce DC bias + bool enableCRC16 = true; //Append CRC-16 to packet, check CRC-16 upon receive uint8_t framesToYield = 3; //If remote requests it, supress transmission for this number of max packet frames uint16_t heartbeatTimeout = 5000; //ms before sending HEARTBEAT to see if link is active @@ -473,7 +473,7 @@ typedef struct struct_settings { //---------------------------------------- bool copyTriggers = false; //Copy the trigger parameters to the training client - uint8_t triggerWidth = 25; //Trigger width in microSeconds or multipler for trigger width + uint8_t triggerWidth = 10; //Trigger width in microSeconds or multipler for trigger width bool triggerWidthIsMultiplier = true; //Use the trigger width as a multiplier uint32_t triggerEnable = 0; //Determine which triggers are enabled: 31 - 0 From 20a02f2e57042f04f3d352e1cb784166632cf2bb Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 10:46:50 -1000 Subject: [PATCH 525/594] Sprinkler Controller: Remove use of the serial port (Serial1) --- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 9fa78171..4823bca0 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -21,8 +21,10 @@ WDTZero myWatchDog; +--------------+ | | SAMD | | | | | - TTL Serial <-->| Serial1 | +--------------+ V - | SPI |<----->| SX1276 Radio |<---> Antenna + | Sprinkler | | + | Controller | | + | | +--------------+ V + Debug | SPI |<----->| SX1276 Radio |<---> Antenna USB Serial <-->| Serial | +--------------+ +--------------+ @@ -156,8 +158,6 @@ void samdBeginSerial(uint16_t serialSpeed) if (settings.usbSerialWait) //Wait for serial to come online for debug printing while (!Serial); - - Serial1.begin(serialSpeed); } //Initialize the watch dog timer @@ -194,14 +194,13 @@ Module * samdRadio() //Determine if serial input data is available bool samdSerialAvailable() { - return (Serial.available() || Serial1.available()); + return (Serial.available()); } //Ensure that all serial output data has been sent over USB and via the UART void samdSerialFlush() { Serial.flush(); - Serial1.flush(); } //Read in the serial input data @@ -210,8 +209,6 @@ uint8_t samdSerialRead() byte incoming = 0; if (Serial.available()) incoming = Serial.read(); - else if (Serial1.available()) - incoming = Serial1.read(); return (incoming); } @@ -219,7 +216,6 @@ uint8_t samdSerialRead() void samdSerialWrite(uint8_t value) { Serial.write(value); - Serial1.write(value); } //Reset the CPU From 2ffcd442ddf0f04a4e4197e1baf9ad010412bf40 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 11:03:25 -1000 Subject: [PATCH 526/594] Sprinkler Controller: Add I2C support --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index e144e64f..85c63a79 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -180,6 +180,11 @@ uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps r unsigned long lastPet = 0; //Remebers time of last WDT pet. //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +//I2C +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#include +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + //Global variables - Serial //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- const uint8_t escapeCharacter = '+'; @@ -628,6 +633,8 @@ void setup() arch.uniqueID(myUniqueId); //Get the unique ID + Wire.begin(); //Start I2C + beginLoRa(); //Start radio beginButton(); //Start watching the train button From 8302c0c4d946a9002cac7bac04207dcd1e51c51f Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 11:07:13 -1000 Subject: [PATCH 527/594] Sprinkler Controller: Add support for the quad relay board --- Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino | 12 ++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 1 + 2 files changed, 13 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 85c63a79..bfb8aa4b 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -185,6 +185,14 @@ unsigned long lastPet = 0; //Remebers time of last WDT pet. #include //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Quad Relay Board +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +#include + +Qwiic_Relay quadRelay(0x6d); + +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + //Global variables - Serial //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- const uint8_t escapeCharacter = '+'; @@ -635,6 +643,10 @@ void setup() Wire.begin(); //Start I2C + //Verify connection to quad relay board + if(quadRelay.begin()) + online.quadRelay = true; + beginLoRa(); //Start radio beginButton(); //Start watching the train button diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index b26db355..87d206cc 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -522,6 +522,7 @@ Settings settings; struct struct_online { bool radio = false; bool eeprom = false; + bool quadRelay = false; } online; #include //Click here to get the library: http://librarymanager/All#RadioLib v5.5.0 From 66511dad50047d356d9a0ab968eff8b841326abd Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 11:11:07 -1000 Subject: [PATCH 528/594] Sprinkler Controller: Add the global variables --- .../LoRaSerial_Firmware.ino | 46 +++++++++++++++++++ Firmware/LoRaSerial_Firmware/settings.h | 12 +++++ 2 files changed, 58 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index bfb8aa4b..7da7f05c 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -517,6 +517,52 @@ unsigned long retransmitTimeout = 0; //Throttle back re-transmits //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//Sprinkler controller variables +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + +//Days of week +const char dayLetter[7] = {'U', 'M', 'T', 'W', 'R', 'F', 'S'}; +const char * day3Letter[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +const char * dayName[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; + +//Display support +ZONE_MASK zoneOn; +ZONE_MASK relayOn; + +//Sprinkler solenoid management +uint8_t zoneNumber; //0 = None, 1 - ZONE_NUMBER_MAX +ZONE_MASK latchingSolenoid = 0xff;//Mask of latching solenoids +ZONE_MASK zoneActive; //The current zone that is on or off +ZONE_MASK zoneManualOn; //Mask of zones on/off, set only one bit! +ZONE_MASK zoneManualPreviousOn; //Previous mask of zones on and off +uint32_t pulseDuration; //Length of the pulse in milliseconds, off = 0 +uint32_t pulseStartTime; //Time the pulse started +uint32_t onTime; //Time the zone was turned on +bool enableSprinklerController; //Enable the sprinkler controller +bool turnWaterOff; //Set true when zone is being turned off + +//Sprinkler controller schedule for today +uint32_t startOfDay; //Number of milliseconds at midnight +uint32_t timeOfDay; //Number of milliseconds from midnight +uint8_t dayOfWeek; //Day of week 0 - 6 +bool scheduleCopied; //True after daily schedule copied from week to today; +bool scheduleActive; //True while an entry is active in the schedule + +CONTROLLER_SCHEDULE week[7]; +CONTROLLER_SCHEDULE today; //Active schedule +uint32_t zoneOnTime; //Time when the zone was turned on +uint32_t zoneOnDuration; //Amount of time that the zone should be on + +#define MILLISECONDS_IN_A_SECOND 1000 +#define MILLISECONDS_IN_A_MINUTE (60 * MILLISECONDS_IN_A_SECOND) +#define MILLISECONDS_IN_AN_HOUR (60 * MILLISECONDS_IN_A_MINUTE) +#define MILLISECONDS_IN_A_DAY (24 * MILLISECONDS_IN_AN_HOUR) + +//Sprinkler commands +uint8_t commandZone; //Zone number for commands, 0 - ZONE_NUMBER_MAX +uint8_t commandDay; //Day of week number or commands, 0 - 6 +//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- + //Global variables //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- const Settings defaultSettings; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 87d206cc..089b0502 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -525,6 +525,18 @@ struct struct_online { bool quadRelay = false; } online; +//Increasing above 4 requires adding support for second quad relay board +//Increasing above 8 requires more relay boards an changing ZONE_MASK type +#define ZONE_NUMBER_MAX 4 //0 = No zone (off), 1 - 8 = Zone number + +typedef uint8_t ZONE_MASK; //0 = No zone (off), bit # + 1: 1 - 8 = Zone number + +typedef struct _CONTROLLER_SCHEDULE +{ + uint32_t scheduleStartTime; //Schedule start time offset in milliseconds + uint32_t zoneScheduleDuration[ZONE_NUMBER_MAX]; //Scheduled duration for the zone +} CONTROLLER_SCHEDULE; + #include //Click here to get the library: http://librarymanager/All#RadioLib v5.5.0 typedef void (* ARCH_BEGIN_BOARD)(); From e7398385c794fadfaf43ff91ab8dd5be5b8f5c61 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 11:20:44 -1000 Subject: [PATCH 529/594] Sprinkler Controller: Add the sprinkler controller specific settings --- Firmware/LoRaSerial_Firmware/Commands.ino | 18 +++++++++++++++++- Firmware/LoRaSerial_Firmware/settings.h | 7 +++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index c4a828ff..9656d44c 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -73,6 +73,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATT - Enter training mode"); systemPrintln(" ATV - Display virtual circuit settings"); systemPrintln(" ATW - Save current settings to NVM"); + systemPrintln(" ATY - Display the sprinkler controller settings"); systemPrintln(" ATZ - Reboot the radio"); systemPrintln(" AT-Param=xxx - Set parameter's value to xxx by name (Param)"); systemPrintln(" AT-Param? - Print parameter's current value by name (Param)"); @@ -780,6 +781,7 @@ const COMMAND_PREFIX prefixTable[] = { {"ATR", 1, commandDisplayRadio}, {"ATS", 1, commandDisplaySerial}, {"ATV", 1, commandDisplayVirtualCircuit}, + {"ATY", 1, commandDisplaySprinklerController}, {"AT-?", 1, commandDisplayAll}, {"AT-", 1, commandSetByName}, {"AT", 1, commandAT}, @@ -927,6 +929,13 @@ bool commandDisplaySerial(const char * commandString) return true; } +//Display only the sprinkler controller commands +bool commandDisplaySprinklerController(const char * commandString) +{ + displayParameters('Y', false); + return true; +} + //Display only the virtual circuit commands bool commandDisplayVirtualCircuit(const char * commandString) { @@ -1179,7 +1188,14 @@ const COMMAND_ENTRY commands[] = /*Virtual circuit parameters Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ - {'V', 0, 0, 0, MAX_VC - 1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, + {'V', 0, 0, 0, MAX_VC - 1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, + + //Define any user parameters + + /*Sprinkler Controller parameters + Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ + {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSprinklers", &settings.debugSprinklers}, + {'Y', 0, 0, 100, 1000, 0, TYPE_U16, valInt, "PulseDuration", &settings.pulseDuration}, }; const int commandCount = sizeof(commands) / sizeof(commands[0]); diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 089b0502..d50a5b6b 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -515,6 +515,13 @@ typedef struct struct_settings { //Add new parameters immediately before this line //-- Add commands to set the parameters //-- Add parameters to routine updateRadioParameters + + //---------------------------------------- + //Sprinkler Parameters + //---------------------------------------- + + bool debugSprinklers = false; //Enable debugging of sprinkler controller + uint16_t pulseDuration = 250; //Milliseconds for latching solenoid pulse duration } Settings; Settings settings; From 36285e58d2aff98582c5a8c2cffd7b2329bc409c Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 11:39:33 -1000 Subject: [PATCH 530/594] Sprinkler Controller: Add new command types --- Firmware/LoRaSerial_Firmware/Commands.ino | 155 ++++++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 9656d44c..28c17434 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -17,6 +17,14 @@ enum { TYPE_U8, TYPE_U16, TYPE_U32, + + //Sprinkler Controller types + TYPE_DAY, + TYPE_DURATION, + TYPE_START, + TYPE_TIME, + TYPE_ZONE, + TYPE_ZONE_MASK, }; typedef bool (* COMMAND_ROUTINE)(const char * commandString); @@ -1207,12 +1215,57 @@ const int commandCount = sizeof(commands) / sizeof(commands[0]); //Display a command void commandDisplay(const COMMAND_ENTRY * command) { + uint32_t hours; + uint32_t minutes; + CONTROLLER_SCHEDULE * schedule; + uint32_t seconds; + char * string; + char timeString[12]; + uint32_t value; + //Print the setting value switch (command->type) { case TYPE_BOOL: systemPrint((uint8_t)(*(bool *)(command->setting))); break; + case TYPE_DURATION: + if (!commandZone) + break; + schedule = (CONTROLLER_SCHEDULE *)command->setting; + value = schedule[commandDay].zoneScheduleDuration[commandZone-1]; + + //Compute the time + seconds = value; + hours = seconds / MILLISECONDS_IN_AN_HOUR; + seconds -= hours * MILLISECONDS_IN_AN_HOUR; + minutes = seconds / MILLISECONDS_IN_A_MINUTE; + seconds -= minutes * MILLISECONDS_IN_A_MINUTE; + seconds /= MILLISECONDS_IN_A_SECOND; + + //Build the string + string = timeString; + if (hours > 9) + *string++ = '0' + (hours / 10); + *string++ = '0' + (hours % 10); + *string++ = ':'; + *string++ = '0' + (minutes / 10); + *string++ = '0' + (minutes % 10); + *string++ = ':'; + *string++ = '0' + (seconds / 10); + *string++ = '0' + (seconds % 10); + *string++ = 0; + + //Display the time + systemPrint(value); + systemPrint(" ("); + systemPrint(dayName[commandDay]); + systemPrint(" zone "); + systemPrint(commandZone); + systemPrint(" "); + systemPrint(timeString); + systemPrint(")"); + break; case TYPE_FLOAT: systemPrint(*((float *)(command->setting)), command->digits); break; @@ -1230,6 +1283,78 @@ void commandDisplay(const COMMAND_ENTRY * command) case TYPE_U32: systemPrint(*(uint32_t *)(command->setting)); break; + case TYPE_START: + schedule = (CONTROLLER_SCHEDULE *)command->setting; + value = schedule[commandDay].scheduleStartTime; + + //Compute the time + seconds = value; + hours = seconds / MILLISECONDS_IN_AN_HOUR; + seconds -= hours * MILLISECONDS_IN_AN_HOUR; + minutes = seconds / MILLISECONDS_IN_A_MINUTE; + seconds -= minutes * MILLISECONDS_IN_A_MINUTE; + seconds /= MILLISECONDS_IN_A_SECOND; + + //Build the string + string = timeString; + if (hours > 9) + *string++ = '0' + (hours / 10); + *string++ = '0' + (hours % 10); + *string++ = ':'; + *string++ = '0' + (minutes / 10); + *string++ = '0' + (minutes % 10); + *string++ = ':'; + *string++ = '0' + (seconds / 10); + *string++ = '0' + (seconds % 10); + *string++ = 0; + + //Display the time + systemPrint(value); + systemPrint(" ("); + systemPrint(dayName[commandDay]); + systemPrint(" @ "); + systemPrint(timeString); + systemPrint(")"); + break; + case TYPE_TIME: + //Compute the time + seconds = *(uint32_t *)(command->setting) - startOfDay; + hours = seconds / MILLISECONDS_IN_AN_HOUR; + seconds -= hours * MILLISECONDS_IN_AN_HOUR; + minutes = seconds / MILLISECONDS_IN_A_MINUTE; + seconds -= minutes * MILLISECONDS_IN_A_MINUTE; + seconds /= MILLISECONDS_IN_A_SECOND; + + //Build the string + string = timeString; + if (hours > 9) + *string++ = '0' + (hours / 10); + *string++ = '0' + (hours % 10); + *string++ = ':'; + *string++ = '0' + (minutes / 10); + *string++ = '0' + (minutes % 10); + *string++ = ':'; + *string++ = '0' + (seconds / 10); + *string++ = '0' + (seconds % 10); + *string++ = 0; + + //Display the time + systemPrint((uint32_t)(*(uint32_t *)(command->setting))); + systemPrint(" ("); + systemPrint(timeString); + systemPrint(")"); + break; + case TYPE_DAY: + systemPrint(*(uint8_t *)(command->setting)); + systemPrint(" ("); + systemWrite(dayLetter[*(uint8_t *)(command->setting)]); + systemPrint(", "); + systemPrint(dayName[*(uint8_t *)(command->setting)]); + systemPrint(")"); + break; + case TYPE_ZONE_MASK: + systemPrintln(((*(ZONE_MASK *)(command->setting)) >> commandZone) & 1); + break; } systemPrintln(); } @@ -1285,6 +1410,9 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer { const char * digit; double doubleSettingValue; + int index; + uint32_t number; + CONTROLLER_SCHEDULE * schedule; uint32_t settingValue; bool valid; @@ -1330,6 +1458,15 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer if (valid) *(bool *)(command->setting) = (bool)settingValue; break; + case TYPE_DURATION: + valid = command->validate((void *)&settingValue, command->minValue, command->maxValue) + && commandZone; + if (valid) + { + schedule = (CONTROLLER_SCHEDULE *)command->setting; + schedule[commandDay].zoneScheduleDuration[commandZone- 1] = settingValue; + } + break; case TYPE_FLOAT: valid = command->validate((void *)&doubleSettingValue, command->minValue, command->maxValue); if (valid) @@ -1343,11 +1480,21 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer break; case TYPE_SPEED_AIR: case TYPE_SPEED_SERIAL: + case TYPE_TIME: case TYPE_U32: valid = command->validate((void *)&settingValue, command->minValue, command->maxValue); if (valid) *(uint32_t *)(command->setting) = settingValue; break; + case TYPE_START: + valid = command->validate((void *)&settingValue, command->minValue, command->maxValue); + if (valid) + { + schedule = (CONTROLLER_SCHEDULE *)command->setting; + schedule[commandDay].scheduleStartTime = settingValue; + } + break; + case TYPE_DAY: case TYPE_U8: valid = command->validate((void *)&settingValue, command->minValue, command->maxValue); if (valid) @@ -1358,6 +1505,14 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer if (valid) *(uint16_t *)(command->setting) = (uint16_t)settingValue; break; + case TYPE_ZONE_MASK: + valid = command->validate((void *)&settingValue, command->minValue, command->maxValue); + if (valid) + { + *(ZONE_MASK *)(command->setting) &= ~(1 << (commandZone- 1)); + *(ZONE_MASK *)(command->setting) |= settingValue << (commandZone - 1); + } + break; } if (valid == false) break; From b4155d9755c49c11d89d96a95f720291d1883ed1 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 12:01:21 -1000 Subject: [PATCH 531/594] Sprinkler Controller: Add sprinkler controller commands Add the following commands: * ATH - Clear watering schedule * ATI88 - Display the controller schedule * ATI99 - Display the current time * AT-CommandDay - Set day for the next command * AT-CommandZone - Set zone (1 - 4) for the next command * AT-DayOfWeek - Specify current day of week * AT-EnableController - Enable or disable the sprinkler controller * AT-LatchingSolenoid - CmdZone: 1 = latching solenoid, 0 = AC solenoid * AT-StartTime - CmdZone: Start time * AT-Time - Specify current time * AT-ZoneDuration - CmdZone: On time for the zone in milliseconds * AT-ZoneManualOn - CmdZone: 1 = On, 0 = Off --- Firmware/LoRaSerial_Firmware/Commands.ino | 177 +++++++++++++++++++++- 1 file changed, 176 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 28c17434..aece039f 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -42,13 +42,22 @@ typedef struct //Process the AT commands bool commandAT(const char * commandString) { + uint32_t currentTime; + int days; uint32_t delayMillis; long deltaMillis; + uint32_t hours; uint8_t id[UNIQUE_ID_BYTES]; + uint32_t minutes; + bool printStartTime; + uint32_t seconds; const char * string; unsigned long timer; + char timeString[16]; + uint32_t value; VIRTUAL_CIRCUIT * vc = &virtualCircuitList[cmdVc]; uint8_t vcIndex; + int zone; //'AT' if (commandLength == 2) @@ -71,6 +80,7 @@ bool commandAT(const char * commandString) systemPrintln(" ATD - Display the debug settings"); systemPrintln(" ATF - Restore factory settings"); systemPrintln(" ATG - Generate new netID and encryption key"); + systemPrintln(" ATH - Clear watering schedule"); systemPrintln(" ATI - Display the radio version"); systemPrintln(" ATI? - Display the information commands"); systemPrintln(" ATIn - Display system information"); @@ -157,6 +167,13 @@ bool commandAT(const char * commandString) generateRandomKeysID(); return true; + case ('H'): //Clear the watering schedule + memset(&week, 0, sizeof(week)); + enableSprinklerController = false; + scheduleCopied = false; + return true; + break; + case ('I'): //ATI //Shows the radio version systemPrint("SparkFun LoRaSerial "); @@ -250,6 +267,8 @@ bool commandAT(const char * commandString) systemPrintln(" ATI13 - Display the SX1276 registers"); systemPrintln(" ATI14 - Dump the radioTxBuffer"); systemPrintln(" ATI15 - Dump the NVM unique ID table"); + systemPrintln(" ATI88 - Display the sprinkler schedule"); + systemPrintln(" ATI89 - Display current time and date"); return true; case ('0'): //ATI0 - Show user settable parameters @@ -299,6 +318,8 @@ bool commandAT(const char * commandString) return true; } } + + //ATI1x if ((commandString[2] == 'I') && (commandString[3] == '1') && (commandLength == 5)) { switch (commandString[4]) @@ -758,6 +779,151 @@ bool commandAT(const char * commandString) } } + //ATI8x + if ((commandString[2] == 'I') && (commandString[3] == '8') && (commandLength == 5)) + { + switch (commandString[4]) + { + default: + return false; + + case ('8'): //ATI88 - Display the sprinkler schedule + //Determine if the controller is enabled + systemPrint("Controller: "); + if (!online.quadRelay) + systemPrintln("Broken - Quad relay offline!"); + else + systemPrintln(enableSprinklerController ? "Enabled" : "Off - Disabled"); + + //Determine if any of the zones are enabled + systemPrintln("Schedule"); + for (int index = 0; index < 7; index++) + { + for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) + if (week[index].zoneScheduleDuration[zone]) + break; + if (zone < ZONE_NUMBER_MAX) + break; + } + if (zone >= ZONE_NUMBER_MAX) + systemPrintln(" Controller Off: No schedule programmed"); + else + { + //Display the schedule + for (int index = 0; index < 7; index++) + { + //Determine if any of the zones are enabled + for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) + if (week[index].zoneScheduleDuration[zone]) + break; + + //Display the day of the week + if (zone < ZONE_NUMBER_MAX) + { + systemPrint(" "); + systemPrint(dayName[index]); + + //Display the starting time + systemPrint(" starting at "); + seconds = week[index].scheduleStartTime; + hours = seconds / (60 * 60 * 1000); + if ((hours % 12) == 0) + systemPrint(12); + else if ((hours % 12) < 10) + { + systemPrint("0"); + systemPrint(hours % 12); + } + else + systemPrint(hours % 12); + seconds -= hours * 60 * 60 * 1000; + minutes = seconds / (60 * 1000); + systemPrint(":"); + if (minutes < 10) + systemPrint("0"); + systemPrint(minutes); + seconds -= minutes * 60 * 1000; + seconds /= 1000; + systemPrint(":"); + if (seconds < 10) + systemPrint("0"); + systemPrint(seconds); + systemPrintln(hours < 12 ? " AM" : " PM"); + + //Display the zones + for (zone=0; zone < ZONE_NUMBER_MAX; zone++) + { + seconds = week[index].zoneScheduleDuration[zone]; + if (seconds) + { + systemPrint(" Zone "); + systemPrint(zone + 1); + systemPrint(": "); + hours = seconds / (60 * 60 * 1000); + if (hours < 10) + systemPrint("0"); + systemPrint(hours); + seconds -= hours * 60 * 60 * 1000; + minutes = seconds / (60 * 1000); + systemPrint(":"); + if (minutes < 10) + systemPrint("0"); + systemPrint(minutes); + seconds -= minutes * 60 * 1000; + seconds /= 1000; + systemPrint(":"); + if (seconds < 10) + systemPrint("0"); + systemPrintln(seconds); + } + } + } + } + } + + //Display the zone configuration + systemPrintln("Zones"); + for (zone=0; zone < ZONE_NUMBER_MAX; zone++) + { + systemPrint(" "); + systemPrint(zone + 1); + systemPrint(": "); + systemPrint((latchingSolenoid & (1 << zone)) ? "Latching" : "AC"); + systemPrintln(" solenoid"); + } + return true; + + case ('9'): //ATI89 - Display current time and date + systemPrint(dayName[dayOfWeek]); + systemPrint(", "); + seconds = (timeOfDay + timestampOffset) % (24 * 60 * 60 * 1000); + hours = seconds / (60 * 60 * 1000); + if ((hours % 12) == 0) + systemPrint(12); + else if ((hours % 12) < 10) + { + systemPrint("0"); + systemPrint(hours % 12); + } + else + systemPrint(hours % 12); + seconds -= hours * 60 * 60 * 1000; + minutes = seconds / (60 * 1000); + systemPrint(":"); + if (minutes < 10) + systemPrint("0"); + systemPrint(minutes); + seconds -= minutes * 60 * 1000; + seconds /= 1000; + systemPrint(":"); + if (seconds < 10) + systemPrint("0"); + systemPrint(seconds); + systemPrintln(hours < 12 ? " AM" : " PM"); + return true; + } + } + //Invalid command return false; } @@ -1202,8 +1368,17 @@ const COMMAND_ENTRY commands[] = /*Sprinkler Controller parameters Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ + {'Y', 0, 0, 0, 6, 0, TYPE_DAY, valInt, "CommandDay", &commandDay}, + {'Y', 0, 0, 0, ZONE_NUMBER_MAX, 0, TYPE_U8, valInt, "CommandZone", &commandZone}, + {'Y', 0, 0, 0, 6, 0, TYPE_DAY, valInt, "DayOfWeek", &dayOfWeek}, {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSprinklers", &settings.debugSprinklers}, - {'Y', 0, 0, 100, 1000, 0, TYPE_U16, valInt, "PulseDuration", &settings.pulseDuration}, + {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "EnableController", &enableSprinklerController}, + {'Y', 0, 0, 0, 1, 0, TYPE_ZONE_MASK, valInt, "LatchingSolenoid", &latchingSolenoid}, + {'Y', 0, 0, 100, 1000, 0, TYPE_U16, valInt, "PulseDuration", &settings.pulseDuration}, + {'Y', 0, 0, 0, 86399999, 0, TYPE_START, valInt, "StartTime", &week}, + {'Y', 0, 0, 0, 86399999, 0, TYPE_TIME, valInt, "TimeOfDay", &timeOfDay}, + {'Y', 0, 0, 0, 7200000, 0, TYPE_DURATION, valInt, "ZoneDuration", &week}, + {'Y', 0, 0, 0, 1, 0, TYPE_ZONE_MASK, valInt, "ZoneManualOn", &zoneManualOn}, }; const int commandCount = sizeof(commands) / sizeof(commands[0]); From 0d10616d3248ef651975d1fb321829a7dcb8bcdb Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 14:31:09 -1000 Subject: [PATCH 532/594] Sprinkler Controller: Update time of day --- .../LoRaSerial_Firmware.ino | 2 ++ .../Sprinkler_Controller.ino | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 7da7f05c..3f94e163 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -717,6 +717,8 @@ void loop() { petWDT(); + updateTimeOfDay(); + updateButton(); //Check if train button is pressed updateSerial(); //Store incoming and print outgoing diff --git a/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino b/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino new file mode 100644 index 00000000..a113772c --- /dev/null +++ b/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino @@ -0,0 +1,24 @@ +/* + January 16th, 2023 + Lee Leahy + + Sprinkler controller support routines. +*/ + +void updateTimeOfDay() +{ + uint32_t currentTime; + static uint32_t previousTime; + + //Update the time + currentTime = millis(); + timeOfDay += currentTime - previousTime; + previousTime = currentTime; + if (timeOfDay >= MILLISECONDS_IN_A_DAY) + { + startOfDay += MILLISECONDS_IN_A_DAY; + timeOfDay -= MILLISECONDS_IN_A_DAY; + scheduleCopied = false; + } +} + From 85a480c8614ffb90679d2073fe193420c6fd5ba2 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 14:48:00 -1000 Subject: [PATCH 533/594] Sprinkler Controller: Support control of the zones --- .../LoRaSerial_Firmware.ino | 2 + .../Sprinkler_Controller.ino | 330 ++++++++++++++++++ 2 files changed, 332 insertions(+) diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 3f94e163..6fb48423 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -727,5 +727,7 @@ void loop() updateLeds(); //Update the LEDs on the board + updateZones(); //Turn on or off the sprinkler zones + updateHopISR(); //Clear hop ISR as needed } diff --git a/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino b/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino index a113772c..73d10c71 100644 --- a/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino +++ b/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino @@ -22,3 +22,333 @@ void updateTimeOfDay() } } +/* + Day + Sun | Mon + 1 ----- ---------------------------------------------- scheduleCopied + 0 \_____/: + : + 1 __: + 0 _________/ \_____________________________________________ waterToday + : + 1 :------------------------------------------ + 0 ____________/: \__ scheduleActive + : + 1 :__ + 0 _____________/ \_________________________________________ today.zoneScheduleDuration 1 + : : + 1 :__:_________ + 0 _____________/ : \_______________________________ today.zoneScheduleDuration 2 + : : : + : : : Zone 3 + : : : Suspended + 1 :__:_________:_________ ____ + 0 _____________/ : : \_____/ :\__________ today.zoneScheduleDuration 3 + : : : : : :: + 1 : : : : : :: + 0 ________________:_________:_________:_____:___::__________ today.zoneScheduleDuration 4 + : : : : :: + 1 :------- :------- :--- : ::----- + 0 ________________/ 1 \_/ 2 \_/ 3 \_:___:/ 3 \____ zoneOnDuration + : : + 1 :---: + 0 __________________________________________/ \___________ manualOn + +*/ + +void updateZones() +{ + uint32_t currentTime; + static uint32_t previousTime; + uint32_t deltaTime; + bool waterToday; + int zone; + + if (!online.quadRelay) + quadRelayOffline(); + + //Verify that the previous schedule has completed. If completed, check for + //a watering schedule for today + waterToday = false; + for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) + { + //Verify that the previous watering schedule was completed + if (today.zoneScheduleDuration[zone]) + { + waterToday = false; + break; + } + + //Determine if watering is necessary today + if (week[dayOfWeek].zoneScheduleDuration[zone]) + waterToday = true; + } + + //---------------------------------------- + //First finish turning on or off any of the latching relays + //---------------------------------------- + currentTime = millis(); + if (pulseDuration) + { + //When the pulse duration is reached, turn off the relay + if ((currentTime - pulseStartTime) >= pulseDuration) + turnOffRelay(); + } + + //---------------------------------------- + //Second, process any change in manual on/off + //---------------------------------------- + else if (zoneManualOn || (zoneManualPreviousOn != zoneManualOn)) + { + if (zoneManualPreviousOn != zoneManualOn) + { + //Turn off the previous zone first + if (zoneManualPreviousOn) + { + //Reduce the zone's watering schedule + zone = zoneNumber - 1; + if (today.zoneScheduleDuration[zone]) + { + deltaTime = currentTime - onTime; + today.zoneScheduleDuration[zone] = (today.zoneScheduleDuration[zone] < deltaTime) + ? 0 : today.zoneScheduleDuration[zone] - deltaTime; + } + + //Turn off this zone + turnOffZone(); + } + else + { + //No manual operation is active, if a scheduled operation is in progress + //suspend the scheduled operation. + if (zoneOnDuration) + { + //Remember the remaining time + zone = zoneNumber - 1; + deltaTime = zoneOnDuration - (currentTime - onTime); + today.zoneScheduleDuration[zone] = deltaTime; + zoneOnDuration = 0; + + //Turn off this zone + turnOffZone(); + } + else + { + //Turn on specified relay + onTime = currentTime; + turnOnRelay(zoneManualOn); + + //Remember the state change + zoneManualPreviousOn = zoneManualOn; + } + } + } + } + + //---------------------------------------- + //Third, turn off the zone + //---------------------------------------- + + else if (zoneOnDuration) + { + if ((timeOfDay - zoneOnTime) >= zoneOnDuration) + { + //Turn off the zone + turnOffZone(); + zoneOnDuration = 0; + } + } + + //---------------------------------------- + //Fourth, execute the schedule + //---------------------------------------- + else if (scheduleActive) + { + if ((timeOfDay - startOfDay) >= today.scheduleStartTime) + { + zoneOnDuration = 0; + for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) + { + //Start this zone + if (today.zoneScheduleDuration[zone]) + { + //Turn on the zone + onTime = currentTime; + turnOnRelay(1 << zone); + + //Only water this zone once + zoneOnTime = timeOfDay; + zoneOnDuration = today.zoneScheduleDuration[zone]; + today.zoneScheduleDuration[zone] = 0; + break; + } + } + + //Done if watering is done for all of the zones + if (!zoneOnDuration) + scheduleActive = false; + } + } + + //---------------------------------------- + //Last, copy the schedule + //---------------------------------------- + else if (waterToday && (!scheduleCopied) && enableSprinklerController) + { + //Copy the schedule + today = week[dayOfWeek]; + scheduleActive = true; + scheduleCopied = true; + } + + previousTime = currentTime; +} + +void quadRelayOffline() +{ + //Report error back to the server + //Delay for an hour + //Reboot +} + +uint8_t zoneMaskToZoneNumber(ZONE_MASK zoneMask) +{ + uint8_t zone; + + //Determine which zone is enabled + if (zoneMask) + for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) { + if (zoneMask & (1 << zone)) + return zone + 1; + } + + //No zone enabled + return 0; +} + +void turnOnRelay(uint8_t zoneMask) +{ + ZONE_MASK previousZoneActive; + + //Get the zone number: 1 - 8 + zoneNumber = zoneMaskToZoneNumber(zoneMask); + + //Set the active zone + previousZoneActive = zoneActive; + zoneActive = 1 << (zoneNumber - 1); + + //Update the display + if (latchingSolenoid & zoneActive) + relayOn |= zoneActive; + if (!previousZoneActive) + zoneOn |= zoneActive; + + //Output the debug messages + if (settings.debugSprinklers) + { + //Update the relay status + if (!previousZoneActive) + systemPrintln("--------------------------------------------------"); + systemPrintTimestamp(); + systemPrint(" Relay "); + systemPrint(zoneNumber); + systemPrint(" ON driving "); + systemPrint((latchingSolenoid & zoneActive) ? "DC latching" : "AC"); + systemPrintln(" solenoid"); + + if (!previousZoneActive) + { + //Update the zone status + systemPrintTimestamp(); + systemPrint(" Zone "); + systemPrint(zoneNumber); + systemPrintln(" turning ON"); + } + } + + //Turn on the relay + if (online.quadRelay) + quadRelay.turnRelayOn(zoneNumber); + + //Determine the solenoid type and pulse duration + pulseDuration = 0; + if (latchingSolenoid & zoneActive) + { + //A latching solenoid is in use, apply power for a short pulse. + pulseDuration = settings.pulseDuration; + pulseStartTime = millis(); + } +} + +void turnOffZone() +{ + //AC solenoids require power during the entire time the zone is on + // + // ON OFF + // __________________________________ 24V + //AC ______________/ \______________ 0V + // + //DC latching solenoids need a short pulse to turn on/off the zone + // + // ON OFF + // ___ ___ 9V + //DC ______________/ \______________________________/ \__________ 0V + + //Determine the solenoid type and pulse duration + zoneManualPreviousOn = 0; + turnWaterOff = true; + + //Update the display + zoneOn &= ~zoneActive; + + //Turn off the zone + if (latchingSolenoid & zoneActive) + turnOnRelay(zoneActive); + else + turnOffRelay(); +} + +void turnOffRelay() +{ + uint8_t zone; + + //Get the zone number + zone = zoneMaskToZoneNumber(zoneActive); + + //Turn off the relay + if (online.quadRelay) + quadRelay.turnRelayOff(zoneNumber); + + //Update the display + relayOn &= ~zoneActive; + + //Output the debug messages + if (settings.debugSprinklers) + { + if (turnWaterOff) + { + //Update the zone status + systemPrintTimestamp(); + systemPrint(" Zone "); + systemPrint(zone); + systemPrintln(" turning OFF"); + } + + //Update the relay status + systemPrintTimestamp(); + systemPrint(" Relay "); + systemPrint(zone); + systemPrintln(" OFF"); + } + + //Get ready for the next relay operation + if (turnWaterOff) + { + turnWaterOff = false; + zoneNumber = 0; + zoneActive = 0; + onTime = 0; + } + pulseDuration = 0; + pulseStartTime = 0; +} From d30b4e78a65aea08409c0aad7b76bc20e3c9afd8 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 16:40:44 -1000 Subject: [PATCH 534/594] Sprinkler Controller: Add the test script --- Firmware/LoRaSerial_Firmware/Test_Script.txt | 51 ++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 Firmware/LoRaSerial_Firmware/Test_Script.txt diff --git a/Firmware/LoRaSerial_Firmware/Test_Script.txt b/Firmware/LoRaSerial_Firmware/Test_Script.txt new file mode 100644 index 00000000..10f7c6a0 --- /dev/null +++ b/Firmware/LoRaSerial_Firmware/Test_Script.txt @@ -0,0 +1,51 @@ +at-DayOfWeek=1 +at-TimeOfDay=59310000 + +ath +ati88 + +at-CommandZone=1 +at-LatchingSolenoid=0 + +at-CommandDay=1 +at-StartTime=81000000 +at-CommandZone=1 +at-ZoneDuration=300000 + +at-CommandDay=3 +at-StartTime=81000000 +at-CommandZone=2 +at-ZoneDuration=300000 + +at-CommandDay=5 +at-StartTime=81000000 +at-CommandZone=1 +at-ZoneDuration=300000 +at-CommandZone=2 +at-ZoneDuration=300000 + +ati88 + + + +================================================================================ +===== Testing ===== +================================================================================ + +at-DayOfWeek=5 +at-TimeOfDay=81000000 + +ath + +at-CommandDay=5 +at-StartTime=81010000 +at-CommandZone=1 +at-LatchingSolenoid=0 +at-ZoneDuration=7000 +at-CommandZone=2 +at-ZoneDuration=7000 + +at-EnableController=1 + +ati89 +ati88 From ec6c19e13ea4d6a5bc10c2e34050c77e54a9e6ec Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 16 Jan 2023 11:34:08 -1000 Subject: [PATCH 535/594] Sprinkler Controller: Add support for the OLED display --- Firmware/LoRaSerial_Firmware/Commands.ino | 3 + Firmware/LoRaSerial_Firmware/Display.ino | 317 ++++++++++++++++++ .../LoRaSerial_Firmware.ino | 9 + Firmware/LoRaSerial_Firmware/icons.h | 65 ++++ Firmware/LoRaSerial_Firmware/settings.h | 4 + 5 files changed, 398 insertions(+) create mode 100644 Firmware/LoRaSerial_Firmware/Display.ino create mode 100644 Firmware/LoRaSerial_Firmware/icons.h diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index aece039f..707b00d8 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1372,9 +1372,12 @@ const COMMAND_ENTRY commands[] = {'Y', 0, 0, 0, ZONE_NUMBER_MAX, 0, TYPE_U8, valInt, "CommandZone", &commandZone}, {'Y', 0, 0, 0, 6, 0, TYPE_DAY, valInt, "DayOfWeek", &dayOfWeek}, {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSprinklers", &settings.debugSprinklers}, + {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "DisplayMilliseconds", &settings.displayMilliseconds}, + {'Y', 0, 0, 50, 1000, 0, TYPE_U16, valInt, "DisplayUpdate", &settings.displayUpdate}, {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "EnableController", &enableSprinklerController}, {'Y', 0, 0, 0, 1, 0, TYPE_ZONE_MASK, valInt, "LatchingSolenoid", &latchingSolenoid}, {'Y', 0, 0, 100, 1000, 0, TYPE_U16, valInt, "PulseDuration", &settings.pulseDuration}, + {'Y', 0, 0, 0, 15000, 0, TYPE_U16, valInt, "SplashScreenDelay", &settings.splashScreenDelay}, {'Y', 0, 0, 0, 86399999, 0, TYPE_START, valInt, "StartTime", &week}, {'Y', 0, 0, 0, 86399999, 0, TYPE_TIME, valInt, "TimeOfDay", &timeOfDay}, {'Y', 0, 0, 0, 7200000, 0, TYPE_DURATION, valInt, "ZoneDuration", &week}, diff --git a/Firmware/LoRaSerial_Firmware/Display.ino b/Firmware/LoRaSerial_Firmware/Display.ino new file mode 100644 index 00000000..7270b4c2 --- /dev/null +++ b/Firmware/LoRaSerial_Firmware/Display.ino @@ -0,0 +1,317 @@ +//---------------------------------------- +// Locals +//---------------------------------------- + +static QwiicMicroOLED oled; +static uint32_t displayTimer; + +//Fonts +#include +#include +#include + +//Icons +#include "icons.h" + +//---------------------------------------- +// Routines +//---------------------------------------- + +void beginDisplay() +{ + if (oled.begin() == true) + { + online.display = true; + if (settings.splashScreenDelay) + displaySplash(); + } +} + +void displaySplash() +{ + int yPos; + int fontHeight; + + if (online.display == true) + { + //Erase the buffer + oled.erase(); + + //Add the splash screen text to the buffer + fontHeight = 8; + yPos = 0; + printTextCenter("Sprinkler", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight + 1; + printTextCenter("Controller", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight + 1; + printTextCenter("By", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + yPos += fontHeight + 1; + printTextCenter("Lee Leahy", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + //Display the buffer + oled.display(); + displayTimer = millis(); + } +} + +void updateDisplay() +{ + uint32_t currentTime; + int days; + int fontHeight; + uint32_t hours; + int index; + static uint32_t lastDisplayTime = 0; + uint32_t minutes; + uint32_t seconds; + char * string; + char timeString[16]; + int xPos; + int yPos; + + //Don't do anything if the display is not attached + currentTime = millis(); + if (online.display == false) + return; + + //Turn off the splash screen + if (displayTimer) + { + if ((currentTime - displayTimer) >= settings.splashScreenDelay) + { + //Erase the screen + oled.erase(); + oled.display(); + displayTimer = 0; + } + } + else + { + //Update the display on a periodic basis + if ((currentTime - lastDisplayTime) >= settings.displayUpdate) + { + lastDisplayTime = currentTime; + + //Compute the time + seconds = timeOfDay; + hours = seconds / MILLISECONDS_IN_AN_HOUR; + seconds -= hours * MILLISECONDS_IN_AN_HOUR; + minutes = seconds / MILLISECONDS_IN_A_MINUTE; + seconds -= minutes * MILLISECONDS_IN_A_MINUTE; + seconds /= MILLISECONDS_IN_A_SECOND; + + //Erase the buffer + oled.erase(); + + //Display the time + string = timeString; + *string++ = dayLetter[dayOfWeek]; + *string++ = ' '; + *string++ = (hours > 9) ? '0' + (hours / 10) : ' '; + *string++ = '0' + (hours % 10); + *string++ = ':'; + *string++ = (minutes > 9) ? '0' + (minutes / 10) : '0'; + *string++ = '0' + (minutes % 10); + *string++ = ':'; + *string++ = (seconds > 9) ? '0' + (seconds / 10) : '0'; + *string++ = '0' + (seconds % 10); + *string++ = 0; + + //Add the time to the buffer + fontHeight = 8; + yPos = 0; + printTextCenter(timeString, yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + + //Skip to the next line + yPos += fontHeight + 2; + + //Display the zone numbers + xPos = 8; + for (index = 0; index < ZONE_NUMBER_MAX; index++) + { + printChar(xPos, yPos, '1' + index); + xPos += Drop_Width + 5; + } + + //Display the drops + yPos += fontHeight + 2; + xPos = 6; + for (index = 0; index < ZONE_NUMBER_MAX; index++) + { + if (relayOn & (1 << index)) + { + if (zoneOn & (1 << index)) + displayBitmap(xPos, yPos, UpArrow_Width, UpArrow_Height, UpArrow); + else + displayBitmap(xPos, yPos, DownArrow_Width, DownArrow_Height, DownArrow); + } + else if (zoneOn & (1 << index)) + displayBitmap(xPos, yPos, Drop_Width, Drop_Height, Drop); + xPos += Drop_Width + 5; + } + + //Skip to the next line + yPos += Drop_Height + 2; + + //Display the duration + if (zoneManualOn) + printTextCenter("Manual", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + else if (zoneOn) + { + lastDisplayTime = currentTime; + + //Compute the time + seconds = currentTime - onTime + MILLISECONDS_IN_A_SECOND; + days = seconds / MILLISECONDS_IN_A_DAY; + seconds -= days * MILLISECONDS_IN_A_DAY; + hours = seconds / MILLISECONDS_IN_AN_HOUR; + seconds -= hours * MILLISECONDS_IN_AN_HOUR; + minutes = seconds / MILLISECONDS_IN_A_MINUTE; + seconds -= minutes * MILLISECONDS_IN_A_MINUTE; + seconds /= MILLISECONDS_IN_A_SECOND; + + string = timeString; + if (days) + { + *string++ = '0' + days; + *string++ = ' '; + } + if (hours > 9) + *string++ = '0' + (hours / 10); + if (hours) + { + *string++ = '0' + (hours % 10); + *string++ = ':'; + } + if ((minutes > 9) || hours) + *string++ = '0' + (minutes / 10); + if (minutes) + { + *string++ = '0' + (minutes % 10); + *string++ = ':'; + } + if ((seconds > 9) || minutes) + *string++ = '0' + (seconds / 10); + *string++ = '0' + (seconds % 10); + *string++ = 0; + + //Display the time the valve is on + printTextCenter(timeString, yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + } + + //Skip to the next line + yPos += fontHeight + 2; + + //Display the milliseconds + if (settings.displayMilliseconds && (zoneOn | relayOn)) + { + string = timeString; + seconds = (((currentTime - onTime) / settings.displayUpdate) * settings.displayUpdate) % 1000; + if (seconds > 99) + *string++ = '0' + (seconds / 100); + if (seconds > 9) + *string++ = '0' + ((seconds % 100) / 10); + *string++ = '0' + (seconds % 10); + *string++ = 0; + printTextCenter(timeString, yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + } + else if (zoneManualOn) + { + //Compute the time + seconds = currentTime - onTime + MILLISECONDS_IN_A_SECOND; + days = seconds / MILLISECONDS_IN_A_DAY; + seconds -= days * MILLISECONDS_IN_A_DAY; + hours = seconds / MILLISECONDS_IN_AN_HOUR; + seconds -= hours * MILLISECONDS_IN_AN_HOUR; + minutes = seconds / MILLISECONDS_IN_A_MINUTE; + seconds -= minutes * MILLISECONDS_IN_A_MINUTE; + seconds /= MILLISECONDS_IN_A_SECOND; + + string = timeString; + if (days) + { + *string++ = '0' + days; + *string++ = ' '; + } + if (hours > 9) + *string++ = '0' + (hours / 10); + if (hours) + { + *string++ = '0' + (hours % 10); + *string++ = ':'; + } + if ((minutes > 9) || hours) + *string++ = '0' + (minutes / 10); + if (minutes) + { + *string++ = '0' + (minutes % 10); + *string++ = ':'; + } + if ((seconds > 9) || minutes) + *string++ = '0' + (seconds / 10); + *string++ = '0' + (seconds % 10); + *string++ = 0; + + //Display the time the valve is on + printTextCenter(timeString, yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted + } + + //Display the buffer + oled.display(); + } + } +} + +uint8_t getFontWidth(QwiicFont & fontType) +{ + uint8_t fontWidth = fontType.width; + if (fontWidth == 8) fontWidth = 7; //8x16, but widest character is only 7 pixels. + return fontWidth; +} + +//Given text, and location, print text center of the screen +void printTextCenter(const char *text, uint8_t yPos, QwiicFont & fontType, uint8_t kerning, bool highlight) //text, y, font type, kearning, inverted +{ + oled.setFont(fontType); + uint8_t fontWidth = getFontWidth(fontType); + uint8_t xPos = (oled.getWidth() / 2) - ((strlen(text) * (fontWidth + kerning)) / 2) + 1; + printText(text, xPos, yPos, fontType, kerning, highlight); +} + +//Given text, a position, and kerning, print text to display +//This is helpful for squishing or stretching a string to appropriately fill the display +void printText(const char *text, uint8_t xPos, uint8_t yPos, QwiicFont & fontType, uint8_t kerning, bool highlight) +{ + oled.setFont(fontType); + oled.setDrawMode(grROPXOR); + uint8_t fontWidth = getFontWidth(fontType); + int8_t xStart = xPos; + for (int x = 0 ; x < strlen(text) ; x++) + { + printChar(xPos, yPos, text[x]); + xPos += fontWidth + kerning; + } + if (highlight) //Draw a box, inverted over text + { + uint8_t textPixelWidth = strlen(text) * (fontWidth + kerning); + + //Error check + int xBoxStart = xStart - 5; + if (xBoxStart < 0) xBoxStart = 0; + int xBoxEnd = textPixelWidth + 9; + if (xBoxEnd > oled.getWidth() - 1) xBoxEnd = oled.getWidth() - 1; + + oled.rectangleFill(xBoxStart, yPos, xBoxEnd, 12, 1); //x, y, width, height, color + } +} + +void printChar(uint8_t xPos, uint8_t yPos, char text) +{ + oled.setCursor(xPos, yPos); + oled.print(text); +} + +void displayBitmap(uint8_t x, uint8_t y, uint8_t imageWidth, uint8_t imageHeight, const uint8_t *imageData) +{ + oled.bitmap(x, y, x + imageWidth, y + imageHeight, (uint8_t *)imageData, imageWidth, imageHeight); +} diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 6fb48423..f91cbdd7 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -185,6 +185,11 @@ unsigned long lastPet = 0; //Remebers time of last WDT pet. #include //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- +//External Display +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +#include //http://librarymanager/All#SparkFun_Qwiic_Graphic_OLED +//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + //Quad Relay Board //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- #include @@ -693,6 +698,8 @@ void setup() if(quadRelay.begin()) online.quadRelay = true; + beginDisplay(); //Start display first to be able to display any errors + beginLoRa(); //Start radio beginButton(); //Start watching the train button @@ -729,5 +736,7 @@ void loop() updateZones(); //Turn on or off the sprinkler zones + updateDisplay(); + updateHopISR(); //Clear hop ISR as needed } diff --git a/Firmware/LoRaSerial_Firmware/icons.h b/Firmware/LoRaSerial_Firmware/icons.h new file mode 100644 index 00000000..100ff99b --- /dev/null +++ b/Firmware/LoRaSerial_Firmware/icons.h @@ -0,0 +1,65 @@ +/* + Drop [9, 8] + + 123456789 + .---------. + 0x01| * | + 0x02| *** | + 0x04| ***** | + 0x08| ***** | + 0x10| ******* | + 0x20| ******* | + 0x40| ******* | + 0x80| ***** | + '---------' +*/ + +const int Drop_Width = 9; +const int Drop_Height = 8; +const uint8_t Drop [] = { + 0x00, 0x70, 0xfc, 0xfe, 0xff, 0xfe, 0xfc, 0x70, 0x00 +}; + +/* + DownloadArrow [9, 8] + + 123456789 + .---------. + 0x01| ** | + 0x02| ** | + 0x04| ** | + 0x08| ** | + 0x10|** ** ** | + 0x20| ****** | + 0x40| **** | + 0x80| ** | + '---------' +*/ + +const int DownArrow_Width = 9; +const int DownArrow_Height = 8; +const uint8_t DownArrow [] = { + 0x10, 0x30, 0x60, 0xff, 0xff, 0x60, 0x30, 0x10, 0x00 +}; + +/* + UploadArrow [9, 8] + + 123456789 + .---------. + 0x01| ** | + 0x02| **** | + 0x04| ****** | + 0x08|** ** ** | + 0x10| ** | + 0x20| ** | + 0x40| ** | + 0x80| ** | + '---------' +*/ + +const int UpArrow_Width = 9; +const int UpArrow_Height = 8; +const uint8_t UpArrow [] = { + 0x08, 0x0c, 0x06, 0xff, 0x0f, 0x06, 0x0c, 0x08, 0x00 +}; diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index d50a5b6b..8ef92a62 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -521,7 +521,10 @@ typedef struct struct_settings { //---------------------------------------- bool debugSprinklers = false; //Enable debugging of sprinkler controller + bool displayMilliseconds = false; //Show the milliseconds on the display + uint16_t displayUpdate = 125; //Milliseconds to update the display uint16_t pulseDuration = 250; //Milliseconds for latching solenoid pulse duration + uint16_t splashScreenDelay = 3000; //Milliseconds to display the splash screen } Settings; Settings settings; @@ -530,6 +533,7 @@ struct struct_online { bool radio = false; bool eeprom = false; bool quadRelay = false; + bool display = false; } online; //Increasing above 4 requires adding support for second quad relay board From c620e8808f416f6abfe868fa96a19074431d4bbb Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Tue, 31 Jan 2023 15:41:51 -0700 Subject: [PATCH 536/594] Remove heartbeat timeout update from MP data packets --- Firmware/LoRaSerial_Firmware/States.ino | 3 --- 1 file changed, 3 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index d55b125e..a019114e 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1288,8 +1288,6 @@ void updateRadioState() //Received data - do not ack. triggerEvent(TRIGGER_RX_DATA); - setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer - //Place any available data in the serial output buffer serialBufferOutput(rxData, rxDataBytes); @@ -1325,7 +1323,6 @@ void updateRadioState() if (xmitDatagramMpData() == true) { triggerEvent(TRIGGER_MP_TX_DATA); - setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer changeState(RADIO_MP_WAIT_TX_DONE); } } From 6689be2ac2af89b89110376a24e84151d91fc790 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Tue, 31 Jan 2023 15:42:18 -0700 Subject: [PATCH 537/594] Fix typo --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 5f21ad51..30b18924 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3303,7 +3303,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) frameAirTimeMsec = (frameAirTimeUsec + settings.txToRxUsec + micros() - transactionCompleteMicros) / 1000; rmtHopTimeMsec = msToNextHopRemote - frameAirTimeMsec; - //Compute the when the local system last hopped + //Compute when the local system last hopped lclHopTimeMsec = currentMillis - channelTimerStart; adjustment = 0; From 526a9bc9bab35962ac2ffde80aef7c55197b15de Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Tue, 31 Jan 2023 15:50:12 -0700 Subject: [PATCH 538/594] Remove extra code --- Firmware/LoRaSerial_Firmware/Display.ino | 317 ---------------- .../LoRaSerial_Firmware.ino | 34 +- .../Sprinkler_Controller.ino | 354 ------------------ Firmware/LoRaSerial_Firmware/icons.h | 65 ---- 4 files changed, 1 insertion(+), 769 deletions(-) delete mode 100644 Firmware/LoRaSerial_Firmware/Display.ino delete mode 100644 Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino delete mode 100644 Firmware/LoRaSerial_Firmware/icons.h diff --git a/Firmware/LoRaSerial_Firmware/Display.ino b/Firmware/LoRaSerial_Firmware/Display.ino deleted file mode 100644 index 7270b4c2..00000000 --- a/Firmware/LoRaSerial_Firmware/Display.ino +++ /dev/null @@ -1,317 +0,0 @@ -//---------------------------------------- -// Locals -//---------------------------------------- - -static QwiicMicroOLED oled; -static uint32_t displayTimer; - -//Fonts -#include -#include -#include - -//Icons -#include "icons.h" - -//---------------------------------------- -// Routines -//---------------------------------------- - -void beginDisplay() -{ - if (oled.begin() == true) - { - online.display = true; - if (settings.splashScreenDelay) - displaySplash(); - } -} - -void displaySplash() -{ - int yPos; - int fontHeight; - - if (online.display == true) - { - //Erase the buffer - oled.erase(); - - //Add the splash screen text to the buffer - fontHeight = 8; - yPos = 0; - printTextCenter("Sprinkler", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - yPos += fontHeight + 1; - printTextCenter("Controller", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - yPos += fontHeight + 1; - printTextCenter("By", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - yPos += fontHeight + 1; - printTextCenter("Lee Leahy", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - - //Display the buffer - oled.display(); - displayTimer = millis(); - } -} - -void updateDisplay() -{ - uint32_t currentTime; - int days; - int fontHeight; - uint32_t hours; - int index; - static uint32_t lastDisplayTime = 0; - uint32_t minutes; - uint32_t seconds; - char * string; - char timeString[16]; - int xPos; - int yPos; - - //Don't do anything if the display is not attached - currentTime = millis(); - if (online.display == false) - return; - - //Turn off the splash screen - if (displayTimer) - { - if ((currentTime - displayTimer) >= settings.splashScreenDelay) - { - //Erase the screen - oled.erase(); - oled.display(); - displayTimer = 0; - } - } - else - { - //Update the display on a periodic basis - if ((currentTime - lastDisplayTime) >= settings.displayUpdate) - { - lastDisplayTime = currentTime; - - //Compute the time - seconds = timeOfDay; - hours = seconds / MILLISECONDS_IN_AN_HOUR; - seconds -= hours * MILLISECONDS_IN_AN_HOUR; - minutes = seconds / MILLISECONDS_IN_A_MINUTE; - seconds -= minutes * MILLISECONDS_IN_A_MINUTE; - seconds /= MILLISECONDS_IN_A_SECOND; - - //Erase the buffer - oled.erase(); - - //Display the time - string = timeString; - *string++ = dayLetter[dayOfWeek]; - *string++ = ' '; - *string++ = (hours > 9) ? '0' + (hours / 10) : ' '; - *string++ = '0' + (hours % 10); - *string++ = ':'; - *string++ = (minutes > 9) ? '0' + (minutes / 10) : '0'; - *string++ = '0' + (minutes % 10); - *string++ = ':'; - *string++ = (seconds > 9) ? '0' + (seconds / 10) : '0'; - *string++ = '0' + (seconds % 10); - *string++ = 0; - - //Add the time to the buffer - fontHeight = 8; - yPos = 0; - printTextCenter(timeString, yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - - //Skip to the next line - yPos += fontHeight + 2; - - //Display the zone numbers - xPos = 8; - for (index = 0; index < ZONE_NUMBER_MAX; index++) - { - printChar(xPos, yPos, '1' + index); - xPos += Drop_Width + 5; - } - - //Display the drops - yPos += fontHeight + 2; - xPos = 6; - for (index = 0; index < ZONE_NUMBER_MAX; index++) - { - if (relayOn & (1 << index)) - { - if (zoneOn & (1 << index)) - displayBitmap(xPos, yPos, UpArrow_Width, UpArrow_Height, UpArrow); - else - displayBitmap(xPos, yPos, DownArrow_Width, DownArrow_Height, DownArrow); - } - else if (zoneOn & (1 << index)) - displayBitmap(xPos, yPos, Drop_Width, Drop_Height, Drop); - xPos += Drop_Width + 5; - } - - //Skip to the next line - yPos += Drop_Height + 2; - - //Display the duration - if (zoneManualOn) - printTextCenter("Manual", yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - else if (zoneOn) - { - lastDisplayTime = currentTime; - - //Compute the time - seconds = currentTime - onTime + MILLISECONDS_IN_A_SECOND; - days = seconds / MILLISECONDS_IN_A_DAY; - seconds -= days * MILLISECONDS_IN_A_DAY; - hours = seconds / MILLISECONDS_IN_AN_HOUR; - seconds -= hours * MILLISECONDS_IN_AN_HOUR; - minutes = seconds / MILLISECONDS_IN_A_MINUTE; - seconds -= minutes * MILLISECONDS_IN_A_MINUTE; - seconds /= MILLISECONDS_IN_A_SECOND; - - string = timeString; - if (days) - { - *string++ = '0' + days; - *string++ = ' '; - } - if (hours > 9) - *string++ = '0' + (hours / 10); - if (hours) - { - *string++ = '0' + (hours % 10); - *string++ = ':'; - } - if ((minutes > 9) || hours) - *string++ = '0' + (minutes / 10); - if (minutes) - { - *string++ = '0' + (minutes % 10); - *string++ = ':'; - } - if ((seconds > 9) || minutes) - *string++ = '0' + (seconds / 10); - *string++ = '0' + (seconds % 10); - *string++ = 0; - - //Display the time the valve is on - printTextCenter(timeString, yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - } - - //Skip to the next line - yPos += fontHeight + 2; - - //Display the milliseconds - if (settings.displayMilliseconds && (zoneOn | relayOn)) - { - string = timeString; - seconds = (((currentTime - onTime) / settings.displayUpdate) * settings.displayUpdate) % 1000; - if (seconds > 99) - *string++ = '0' + (seconds / 100); - if (seconds > 9) - *string++ = '0' + ((seconds % 100) / 10); - *string++ = '0' + (seconds % 10); - *string++ = 0; - printTextCenter(timeString, yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - } - else if (zoneManualOn) - { - //Compute the time - seconds = currentTime - onTime + MILLISECONDS_IN_A_SECOND; - days = seconds / MILLISECONDS_IN_A_DAY; - seconds -= days * MILLISECONDS_IN_A_DAY; - hours = seconds / MILLISECONDS_IN_AN_HOUR; - seconds -= hours * MILLISECONDS_IN_AN_HOUR; - minutes = seconds / MILLISECONDS_IN_A_MINUTE; - seconds -= minutes * MILLISECONDS_IN_A_MINUTE; - seconds /= MILLISECONDS_IN_A_SECOND; - - string = timeString; - if (days) - { - *string++ = '0' + days; - *string++ = ' '; - } - if (hours > 9) - *string++ = '0' + (hours / 10); - if (hours) - { - *string++ = '0' + (hours % 10); - *string++ = ':'; - } - if ((minutes > 9) || hours) - *string++ = '0' + (minutes / 10); - if (minutes) - { - *string++ = '0' + (minutes % 10); - *string++ = ':'; - } - if ((seconds > 9) || minutes) - *string++ = '0' + (seconds / 10); - *string++ = '0' + (seconds % 10); - *string++ = 0; - - //Display the time the valve is on - printTextCenter(timeString, yPos, QW_FONT_5X7, 1, false); //text, y, font type, kerning, inverted - } - - //Display the buffer - oled.display(); - } - } -} - -uint8_t getFontWidth(QwiicFont & fontType) -{ - uint8_t fontWidth = fontType.width; - if (fontWidth == 8) fontWidth = 7; //8x16, but widest character is only 7 pixels. - return fontWidth; -} - -//Given text, and location, print text center of the screen -void printTextCenter(const char *text, uint8_t yPos, QwiicFont & fontType, uint8_t kerning, bool highlight) //text, y, font type, kearning, inverted -{ - oled.setFont(fontType); - uint8_t fontWidth = getFontWidth(fontType); - uint8_t xPos = (oled.getWidth() / 2) - ((strlen(text) * (fontWidth + kerning)) / 2) + 1; - printText(text, xPos, yPos, fontType, kerning, highlight); -} - -//Given text, a position, and kerning, print text to display -//This is helpful for squishing or stretching a string to appropriately fill the display -void printText(const char *text, uint8_t xPos, uint8_t yPos, QwiicFont & fontType, uint8_t kerning, bool highlight) -{ - oled.setFont(fontType); - oled.setDrawMode(grROPXOR); - uint8_t fontWidth = getFontWidth(fontType); - int8_t xStart = xPos; - for (int x = 0 ; x < strlen(text) ; x++) - { - printChar(xPos, yPos, text[x]); - xPos += fontWidth + kerning; - } - if (highlight) //Draw a box, inverted over text - { - uint8_t textPixelWidth = strlen(text) * (fontWidth + kerning); - - //Error check - int xBoxStart = xStart - 5; - if (xBoxStart < 0) xBoxStart = 0; - int xBoxEnd = textPixelWidth + 9; - if (xBoxEnd > oled.getWidth() - 1) xBoxEnd = oled.getWidth() - 1; - - oled.rectangleFill(xBoxStart, yPos, xBoxEnd, 12, 1); //x, y, width, height, color - } -} - -void printChar(uint8_t xPos, uint8_t yPos, char text) -{ - oled.setCursor(xPos, yPos); - oled.print(text); -} - -void displayBitmap(uint8_t x, uint8_t y, uint8_t imageWidth, uint8_t imageHeight, const uint8_t *imageData) -{ - oled.bitmap(x, y, x + imageWidth, y + imageHeight, (uint8_t *)imageData, imageWidth, imageHeight); -} diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index f91cbdd7..61b58ca7 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -43,7 +43,7 @@ const int FIRMWARE_VERSION_MAJOR = 2; const int FIRMWARE_VERSION_MINOR = 0; #define RADIOLIB_LOW_LEVEL //Enable access to the module functions -#define ENABLE_DEVELOPER true //Uncomment this line to enable special developer modes +//#define ENABLE_DEVELOPER true //Uncomment this line to enable special developer modes #define UNUSED(x) (void)(x) @@ -180,24 +180,6 @@ uint16_t petTimeout = 0; //A reduced amount of time before WDT triggers. Helps r unsigned long lastPet = 0; //Remebers time of last WDT pet. //=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -//I2C -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -#include -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -//External Display -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -#include //http://librarymanager/All#SparkFun_Qwiic_Graphic_OLED -//=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= - -//Quad Relay Board -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -#include - -Qwiic_Relay quadRelay(0x6d); - -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - //Global variables - Serial //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- const uint8_t escapeCharacter = '+'; @@ -692,14 +674,6 @@ void setup() arch.uniqueID(myUniqueId); //Get the unique ID - Wire.begin(); //Start I2C - - //Verify connection to quad relay board - if(quadRelay.begin()) - online.quadRelay = true; - - beginDisplay(); //Start display first to be able to display any errors - beginLoRa(); //Start radio beginButton(); //Start watching the train button @@ -724,8 +698,6 @@ void loop() { petWDT(); - updateTimeOfDay(); - updateButton(); //Check if train button is pressed updateSerial(); //Store incoming and print outgoing @@ -734,9 +706,5 @@ void loop() updateLeds(); //Update the LEDs on the board - updateZones(); //Turn on or off the sprinkler zones - - updateDisplay(); - updateHopISR(); //Clear hop ISR as needed } diff --git a/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino b/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino deleted file mode 100644 index 73d10c71..00000000 --- a/Firmware/LoRaSerial_Firmware/Sprinkler_Controller.ino +++ /dev/null @@ -1,354 +0,0 @@ -/* - January 16th, 2023 - Lee Leahy - - Sprinkler controller support routines. -*/ - -void updateTimeOfDay() -{ - uint32_t currentTime; - static uint32_t previousTime; - - //Update the time - currentTime = millis(); - timeOfDay += currentTime - previousTime; - previousTime = currentTime; - if (timeOfDay >= MILLISECONDS_IN_A_DAY) - { - startOfDay += MILLISECONDS_IN_A_DAY; - timeOfDay -= MILLISECONDS_IN_A_DAY; - scheduleCopied = false; - } -} - -/* - Day - Sun | Mon - 1 ----- ---------------------------------------------- scheduleCopied - 0 \_____/: - : - 1 __: - 0 _________/ \_____________________________________________ waterToday - : - 1 :------------------------------------------ - 0 ____________/: \__ scheduleActive - : - 1 :__ - 0 _____________/ \_________________________________________ today.zoneScheduleDuration 1 - : : - 1 :__:_________ - 0 _____________/ : \_______________________________ today.zoneScheduleDuration 2 - : : : - : : : Zone 3 - : : : Suspended - 1 :__:_________:_________ ____ - 0 _____________/ : : \_____/ :\__________ today.zoneScheduleDuration 3 - : : : : : :: - 1 : : : : : :: - 0 ________________:_________:_________:_____:___::__________ today.zoneScheduleDuration 4 - : : : : :: - 1 :------- :------- :--- : ::----- - 0 ________________/ 1 \_/ 2 \_/ 3 \_:___:/ 3 \____ zoneOnDuration - : : - 1 :---: - 0 __________________________________________/ \___________ manualOn - -*/ - -void updateZones() -{ - uint32_t currentTime; - static uint32_t previousTime; - uint32_t deltaTime; - bool waterToday; - int zone; - - if (!online.quadRelay) - quadRelayOffline(); - - //Verify that the previous schedule has completed. If completed, check for - //a watering schedule for today - waterToday = false; - for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) - { - //Verify that the previous watering schedule was completed - if (today.zoneScheduleDuration[zone]) - { - waterToday = false; - break; - } - - //Determine if watering is necessary today - if (week[dayOfWeek].zoneScheduleDuration[zone]) - waterToday = true; - } - - //---------------------------------------- - //First finish turning on or off any of the latching relays - //---------------------------------------- - currentTime = millis(); - if (pulseDuration) - { - //When the pulse duration is reached, turn off the relay - if ((currentTime - pulseStartTime) >= pulseDuration) - turnOffRelay(); - } - - //---------------------------------------- - //Second, process any change in manual on/off - //---------------------------------------- - else if (zoneManualOn || (zoneManualPreviousOn != zoneManualOn)) - { - if (zoneManualPreviousOn != zoneManualOn) - { - //Turn off the previous zone first - if (zoneManualPreviousOn) - { - //Reduce the zone's watering schedule - zone = zoneNumber - 1; - if (today.zoneScheduleDuration[zone]) - { - deltaTime = currentTime - onTime; - today.zoneScheduleDuration[zone] = (today.zoneScheduleDuration[zone] < deltaTime) - ? 0 : today.zoneScheduleDuration[zone] - deltaTime; - } - - //Turn off this zone - turnOffZone(); - } - else - { - //No manual operation is active, if a scheduled operation is in progress - //suspend the scheduled operation. - if (zoneOnDuration) - { - //Remember the remaining time - zone = zoneNumber - 1; - deltaTime = zoneOnDuration - (currentTime - onTime); - today.zoneScheduleDuration[zone] = deltaTime; - zoneOnDuration = 0; - - //Turn off this zone - turnOffZone(); - } - else - { - //Turn on specified relay - onTime = currentTime; - turnOnRelay(zoneManualOn); - - //Remember the state change - zoneManualPreviousOn = zoneManualOn; - } - } - } - } - - //---------------------------------------- - //Third, turn off the zone - //---------------------------------------- - - else if (zoneOnDuration) - { - if ((timeOfDay - zoneOnTime) >= zoneOnDuration) - { - //Turn off the zone - turnOffZone(); - zoneOnDuration = 0; - } - } - - //---------------------------------------- - //Fourth, execute the schedule - //---------------------------------------- - else if (scheduleActive) - { - if ((timeOfDay - startOfDay) >= today.scheduleStartTime) - { - zoneOnDuration = 0; - for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) - { - //Start this zone - if (today.zoneScheduleDuration[zone]) - { - //Turn on the zone - onTime = currentTime; - turnOnRelay(1 << zone); - - //Only water this zone once - zoneOnTime = timeOfDay; - zoneOnDuration = today.zoneScheduleDuration[zone]; - today.zoneScheduleDuration[zone] = 0; - break; - } - } - - //Done if watering is done for all of the zones - if (!zoneOnDuration) - scheduleActive = false; - } - } - - //---------------------------------------- - //Last, copy the schedule - //---------------------------------------- - else if (waterToday && (!scheduleCopied) && enableSprinklerController) - { - //Copy the schedule - today = week[dayOfWeek]; - scheduleActive = true; - scheduleCopied = true; - } - - previousTime = currentTime; -} - -void quadRelayOffline() -{ - //Report error back to the server - //Delay for an hour - //Reboot -} - -uint8_t zoneMaskToZoneNumber(ZONE_MASK zoneMask) -{ - uint8_t zone; - - //Determine which zone is enabled - if (zoneMask) - for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) { - if (zoneMask & (1 << zone)) - return zone + 1; - } - - //No zone enabled - return 0; -} - -void turnOnRelay(uint8_t zoneMask) -{ - ZONE_MASK previousZoneActive; - - //Get the zone number: 1 - 8 - zoneNumber = zoneMaskToZoneNumber(zoneMask); - - //Set the active zone - previousZoneActive = zoneActive; - zoneActive = 1 << (zoneNumber - 1); - - //Update the display - if (latchingSolenoid & zoneActive) - relayOn |= zoneActive; - if (!previousZoneActive) - zoneOn |= zoneActive; - - //Output the debug messages - if (settings.debugSprinklers) - { - //Update the relay status - if (!previousZoneActive) - systemPrintln("--------------------------------------------------"); - systemPrintTimestamp(); - systemPrint(" Relay "); - systemPrint(zoneNumber); - systemPrint(" ON driving "); - systemPrint((latchingSolenoid & zoneActive) ? "DC latching" : "AC"); - systemPrintln(" solenoid"); - - if (!previousZoneActive) - { - //Update the zone status - systemPrintTimestamp(); - systemPrint(" Zone "); - systemPrint(zoneNumber); - systemPrintln(" turning ON"); - } - } - - //Turn on the relay - if (online.quadRelay) - quadRelay.turnRelayOn(zoneNumber); - - //Determine the solenoid type and pulse duration - pulseDuration = 0; - if (latchingSolenoid & zoneActive) - { - //A latching solenoid is in use, apply power for a short pulse. - pulseDuration = settings.pulseDuration; - pulseStartTime = millis(); - } -} - -void turnOffZone() -{ - //AC solenoids require power during the entire time the zone is on - // - // ON OFF - // __________________________________ 24V - //AC ______________/ \______________ 0V - // - //DC latching solenoids need a short pulse to turn on/off the zone - // - // ON OFF - // ___ ___ 9V - //DC ______________/ \______________________________/ \__________ 0V - - //Determine the solenoid type and pulse duration - zoneManualPreviousOn = 0; - turnWaterOff = true; - - //Update the display - zoneOn &= ~zoneActive; - - //Turn off the zone - if (latchingSolenoid & zoneActive) - turnOnRelay(zoneActive); - else - turnOffRelay(); -} - -void turnOffRelay() -{ - uint8_t zone; - - //Get the zone number - zone = zoneMaskToZoneNumber(zoneActive); - - //Turn off the relay - if (online.quadRelay) - quadRelay.turnRelayOff(zoneNumber); - - //Update the display - relayOn &= ~zoneActive; - - //Output the debug messages - if (settings.debugSprinklers) - { - if (turnWaterOff) - { - //Update the zone status - systemPrintTimestamp(); - systemPrint(" Zone "); - systemPrint(zone); - systemPrintln(" turning OFF"); - } - - //Update the relay status - systemPrintTimestamp(); - systemPrint(" Relay "); - systemPrint(zone); - systemPrintln(" OFF"); - } - - //Get ready for the next relay operation - if (turnWaterOff) - { - turnWaterOff = false; - zoneNumber = 0; - zoneActive = 0; - onTime = 0; - } - pulseDuration = 0; - pulseStartTime = 0; -} diff --git a/Firmware/LoRaSerial_Firmware/icons.h b/Firmware/LoRaSerial_Firmware/icons.h deleted file mode 100644 index 100ff99b..00000000 --- a/Firmware/LoRaSerial_Firmware/icons.h +++ /dev/null @@ -1,65 +0,0 @@ -/* - Drop [9, 8] - - 123456789 - .---------. - 0x01| * | - 0x02| *** | - 0x04| ***** | - 0x08| ***** | - 0x10| ******* | - 0x20| ******* | - 0x40| ******* | - 0x80| ***** | - '---------' -*/ - -const int Drop_Width = 9; -const int Drop_Height = 8; -const uint8_t Drop [] = { - 0x00, 0x70, 0xfc, 0xfe, 0xff, 0xfe, 0xfc, 0x70, 0x00 -}; - -/* - DownloadArrow [9, 8] - - 123456789 - .---------. - 0x01| ** | - 0x02| ** | - 0x04| ** | - 0x08| ** | - 0x10|** ** ** | - 0x20| ****** | - 0x40| **** | - 0x80| ** | - '---------' -*/ - -const int DownArrow_Width = 9; -const int DownArrow_Height = 8; -const uint8_t DownArrow [] = { - 0x10, 0x30, 0x60, 0xff, 0xff, 0x60, 0x30, 0x10, 0x00 -}; - -/* - UploadArrow [9, 8] - - 123456789 - .---------. - 0x01| ** | - 0x02| **** | - 0x04| ****** | - 0x08|** ** ** | - 0x10| ** | - 0x20| ** | - 0x40| ** | - 0x80| ** | - '---------' -*/ - -const int UpArrow_Width = 9; -const int UpArrow_Height = 8; -const uint8_t UpArrow [] = { - 0x08, 0x0c, 0x06, 0xff, 0x0f, 0x06, 0x0c, 0x08, 0x00 -}; From a02502b3cef69c513af80368f70647d1909ec9b7 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Tue, 31 Jan 2023 15:55:13 -0700 Subject: [PATCH 539/594] Revert settings.h --- Firmware/LoRaSerial_Firmware/settings.h | 34 ++++--------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial_Firmware/settings.h index 8ef92a62..d7a3b2d2 100644 --- a/Firmware/LoRaSerial_Firmware/settings.h +++ b/Firmware/LoRaSerial_Firmware/settings.h @@ -419,9 +419,9 @@ typedef struct struct_settings { //Radio protocol parameters //---------------------------------------- - uint8_t operatingMode = MODE_VIRTUAL_CIRCUIT; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. + uint8_t operatingMode = MODE_POINT_TO_POINT; //Receiving unit will check netID and ACK. If set to false, receiving unit doesn't check netID or ACK. - uint8_t selectLedUse = LEDS_VC; //Select LED use + uint8_t selectLedUse = LEDS_RSSI; //Select LED use bool server = false; //Default to being a client, enable server for multipoint, VC and training uint8_t netID = 192; //Both radios must share a network ID bool verifyRxNetID = true; //Verify RX netID value when not operating in point-to-point mode @@ -429,8 +429,8 @@ typedef struct struct_settings { uint8_t encryptionKey[AES_KEY_BYTES] = { 0x37, 0x78, 0x21, 0x41, 0xA6, 0x65, 0x73, 0x4E, 0x44, 0x75, 0x67, 0x2A, 0xE6, 0x30, 0x83, 0x08 }; bool encryptData = true; //AES encrypt each packet - bool dataScrambling = true; //Use IBM Data Whitening to reduce DC bias - bool enableCRC16 = true; //Append CRC-16 to packet, check CRC-16 upon receive + bool dataScrambling = false; //Use IBM Data Whitening to reduce DC bias + bool enableCRC16 = false; //Append CRC-16 to packet, check CRC-16 upon receive uint8_t framesToYield = 3; //If remote requests it, supress transmission for this number of max packet frames uint16_t heartbeatTimeout = 5000; //ms before sending HEARTBEAT to see if link is active @@ -473,7 +473,7 @@ typedef struct struct_settings { //---------------------------------------- bool copyTriggers = false; //Copy the trigger parameters to the training client - uint8_t triggerWidth = 10; //Trigger width in microSeconds or multipler for trigger width + uint8_t triggerWidth = 25; //Trigger width in microSeconds or multipler for trigger width bool triggerWidthIsMultiplier = true; //Use the trigger width as a multiplier uint32_t triggerEnable = 0; //Determine which triggers are enabled: 31 - 0 @@ -515,16 +515,6 @@ typedef struct struct_settings { //Add new parameters immediately before this line //-- Add commands to set the parameters //-- Add parameters to routine updateRadioParameters - - //---------------------------------------- - //Sprinkler Parameters - //---------------------------------------- - - bool debugSprinklers = false; //Enable debugging of sprinkler controller - bool displayMilliseconds = false; //Show the milliseconds on the display - uint16_t displayUpdate = 125; //Milliseconds to update the display - uint16_t pulseDuration = 250; //Milliseconds for latching solenoid pulse duration - uint16_t splashScreenDelay = 3000; //Milliseconds to display the splash screen } Settings; Settings settings; @@ -532,22 +522,8 @@ Settings settings; struct struct_online { bool radio = false; bool eeprom = false; - bool quadRelay = false; - bool display = false; } online; -//Increasing above 4 requires adding support for second quad relay board -//Increasing above 8 requires more relay boards an changing ZONE_MASK type -#define ZONE_NUMBER_MAX 4 //0 = No zone (off), 1 - 8 = Zone number - -typedef uint8_t ZONE_MASK; //0 = No zone (off), bit # + 1: 1 - 8 = Zone number - -typedef struct _CONTROLLER_SCHEDULE -{ - uint32_t scheduleStartTime; //Schedule start time offset in milliseconds - uint32_t zoneScheduleDuration[ZONE_NUMBER_MAX]; //Scheduled duration for the zone -} CONTROLLER_SCHEDULE; - #include //Click here to get the library: http://librarymanager/All#RadioLib v5.5.0 typedef void (* ARCH_BEGIN_BOARD)(); From 3f2da492d5198a2d7c51a744ad76a975508fe662 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Tue, 31 Jan 2023 15:58:40 -0700 Subject: [PATCH 540/594] Remove sprinkler code --- Firmware/LoRaSerial_Firmware/Commands.ino | 351 +----------------- .../LoRaSerial_Firmware.ino | 46 --- 2 files changed, 1 insertion(+), 396 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index 707b00d8..c4a828ff 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -17,14 +17,6 @@ enum { TYPE_U8, TYPE_U16, TYPE_U32, - - //Sprinkler Controller types - TYPE_DAY, - TYPE_DURATION, - TYPE_START, - TYPE_TIME, - TYPE_ZONE, - TYPE_ZONE_MASK, }; typedef bool (* COMMAND_ROUTINE)(const char * commandString); @@ -42,22 +34,13 @@ typedef struct //Process the AT commands bool commandAT(const char * commandString) { - uint32_t currentTime; - int days; uint32_t delayMillis; long deltaMillis; - uint32_t hours; uint8_t id[UNIQUE_ID_BYTES]; - uint32_t minutes; - bool printStartTime; - uint32_t seconds; const char * string; unsigned long timer; - char timeString[16]; - uint32_t value; VIRTUAL_CIRCUIT * vc = &virtualCircuitList[cmdVc]; uint8_t vcIndex; - int zone; //'AT' if (commandLength == 2) @@ -80,7 +63,6 @@ bool commandAT(const char * commandString) systemPrintln(" ATD - Display the debug settings"); systemPrintln(" ATF - Restore factory settings"); systemPrintln(" ATG - Generate new netID and encryption key"); - systemPrintln(" ATH - Clear watering schedule"); systemPrintln(" ATI - Display the radio version"); systemPrintln(" ATI? - Display the information commands"); systemPrintln(" ATIn - Display system information"); @@ -91,7 +73,6 @@ bool commandAT(const char * commandString) systemPrintln(" ATT - Enter training mode"); systemPrintln(" ATV - Display virtual circuit settings"); systemPrintln(" ATW - Save current settings to NVM"); - systemPrintln(" ATY - Display the sprinkler controller settings"); systemPrintln(" ATZ - Reboot the radio"); systemPrintln(" AT-Param=xxx - Set parameter's value to xxx by name (Param)"); systemPrintln(" AT-Param? - Print parameter's current value by name (Param)"); @@ -167,13 +148,6 @@ bool commandAT(const char * commandString) generateRandomKeysID(); return true; - case ('H'): //Clear the watering schedule - memset(&week, 0, sizeof(week)); - enableSprinklerController = false; - scheduleCopied = false; - return true; - break; - case ('I'): //ATI //Shows the radio version systemPrint("SparkFun LoRaSerial "); @@ -267,8 +241,6 @@ bool commandAT(const char * commandString) systemPrintln(" ATI13 - Display the SX1276 registers"); systemPrintln(" ATI14 - Dump the radioTxBuffer"); systemPrintln(" ATI15 - Dump the NVM unique ID table"); - systemPrintln(" ATI88 - Display the sprinkler schedule"); - systemPrintln(" ATI89 - Display current time and date"); return true; case ('0'): //ATI0 - Show user settable parameters @@ -318,8 +290,6 @@ bool commandAT(const char * commandString) return true; } } - - //ATI1x if ((commandString[2] == 'I') && (commandString[3] == '1') && (commandLength == 5)) { switch (commandString[4]) @@ -779,151 +749,6 @@ bool commandAT(const char * commandString) } } - //ATI8x - if ((commandString[2] == 'I') && (commandString[3] == '8') && (commandLength == 5)) - { - switch (commandString[4]) - { - default: - return false; - - case ('8'): //ATI88 - Display the sprinkler schedule - //Determine if the controller is enabled - systemPrint("Controller: "); - if (!online.quadRelay) - systemPrintln("Broken - Quad relay offline!"); - else - systemPrintln(enableSprinklerController ? "Enabled" : "Off - Disabled"); - - //Determine if any of the zones are enabled - systemPrintln("Schedule"); - for (int index = 0; index < 7; index++) - { - for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) - if (week[index].zoneScheduleDuration[zone]) - break; - if (zone < ZONE_NUMBER_MAX) - break; - } - if (zone >= ZONE_NUMBER_MAX) - systemPrintln(" Controller Off: No schedule programmed"); - else - { - //Display the schedule - for (int index = 0; index < 7; index++) - { - //Determine if any of the zones are enabled - for (zone = 0; zone < ZONE_NUMBER_MAX; zone++) - if (week[index].zoneScheduleDuration[zone]) - break; - - //Display the day of the week - if (zone < ZONE_NUMBER_MAX) - { - systemPrint(" "); - systemPrint(dayName[index]); - - //Display the starting time - systemPrint(" starting at "); - seconds = week[index].scheduleStartTime; - hours = seconds / (60 * 60 * 1000); - if ((hours % 12) == 0) - systemPrint(12); - else if ((hours % 12) < 10) - { - systemPrint("0"); - systemPrint(hours % 12); - } - else - systemPrint(hours % 12); - seconds -= hours * 60 * 60 * 1000; - minutes = seconds / (60 * 1000); - systemPrint(":"); - if (minutes < 10) - systemPrint("0"); - systemPrint(minutes); - seconds -= minutes * 60 * 1000; - seconds /= 1000; - systemPrint(":"); - if (seconds < 10) - systemPrint("0"); - systemPrint(seconds); - systemPrintln(hours < 12 ? " AM" : " PM"); - - //Display the zones - for (zone=0; zone < ZONE_NUMBER_MAX; zone++) - { - seconds = week[index].zoneScheduleDuration[zone]; - if (seconds) - { - systemPrint(" Zone "); - systemPrint(zone + 1); - systemPrint(": "); - hours = seconds / (60 * 60 * 1000); - if (hours < 10) - systemPrint("0"); - systemPrint(hours); - seconds -= hours * 60 * 60 * 1000; - minutes = seconds / (60 * 1000); - systemPrint(":"); - if (minutes < 10) - systemPrint("0"); - systemPrint(minutes); - seconds -= minutes * 60 * 1000; - seconds /= 1000; - systemPrint(":"); - if (seconds < 10) - systemPrint("0"); - systemPrintln(seconds); - } - } - } - } - } - - //Display the zone configuration - systemPrintln("Zones"); - for (zone=0; zone < ZONE_NUMBER_MAX; zone++) - { - systemPrint(" "); - systemPrint(zone + 1); - systemPrint(": "); - systemPrint((latchingSolenoid & (1 << zone)) ? "Latching" : "AC"); - systemPrintln(" solenoid"); - } - return true; - - case ('9'): //ATI89 - Display current time and date - systemPrint(dayName[dayOfWeek]); - systemPrint(", "); - seconds = (timeOfDay + timestampOffset) % (24 * 60 * 60 * 1000); - hours = seconds / (60 * 60 * 1000); - if ((hours % 12) == 0) - systemPrint(12); - else if ((hours % 12) < 10) - { - systemPrint("0"); - systemPrint(hours % 12); - } - else - systemPrint(hours % 12); - seconds -= hours * 60 * 60 * 1000; - minutes = seconds / (60 * 1000); - systemPrint(":"); - if (minutes < 10) - systemPrint("0"); - systemPrint(minutes); - seconds -= minutes * 60 * 1000; - seconds /= 1000; - systemPrint(":"); - if (seconds < 10) - systemPrint("0"); - systemPrint(seconds); - systemPrintln(hours < 12 ? " AM" : " PM"); - return true; - } - } - //Invalid command return false; } @@ -955,7 +780,6 @@ const COMMAND_PREFIX prefixTable[] = { {"ATR", 1, commandDisplayRadio}, {"ATS", 1, commandDisplaySerial}, {"ATV", 1, commandDisplayVirtualCircuit}, - {"ATY", 1, commandDisplaySprinklerController}, {"AT-?", 1, commandDisplayAll}, {"AT-", 1, commandSetByName}, {"AT", 1, commandAT}, @@ -1103,13 +927,6 @@ bool commandDisplaySerial(const char * commandString) return true; } -//Display only the sprinkler controller commands -bool commandDisplaySprinklerController(const char * commandString) -{ - displayParameters('Y', false); - return true; -} - //Display only the virtual circuit commands bool commandDisplayVirtualCircuit(const char * commandString) { @@ -1362,26 +1179,7 @@ const COMMAND_ENTRY commands[] = /*Virtual circuit parameters Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ - {'V', 0, 0, 0, MAX_VC - 1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, - - //Define any user parameters - - /*Sprinkler Controller parameters - Ltr, All, reset, min, max, digits, type, validation, name, setting addr */ - {'Y', 0, 0, 0, 6, 0, TYPE_DAY, valInt, "CommandDay", &commandDay}, - {'Y', 0, 0, 0, ZONE_NUMBER_MAX, 0, TYPE_U8, valInt, "CommandZone", &commandZone}, - {'Y', 0, 0, 0, 6, 0, TYPE_DAY, valInt, "DayOfWeek", &dayOfWeek}, - {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "DebugSprinklers", &settings.debugSprinklers}, - {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "DisplayMilliseconds", &settings.displayMilliseconds}, - {'Y', 0, 0, 50, 1000, 0, TYPE_U16, valInt, "DisplayUpdate", &settings.displayUpdate}, - {'Y', 0, 0, 0, 1, 0, TYPE_BOOL, valInt, "EnableController", &enableSprinklerController}, - {'Y', 0, 0, 0, 1, 0, TYPE_ZONE_MASK, valInt, "LatchingSolenoid", &latchingSolenoid}, - {'Y', 0, 0, 100, 1000, 0, TYPE_U16, valInt, "PulseDuration", &settings.pulseDuration}, - {'Y', 0, 0, 0, 15000, 0, TYPE_U16, valInt, "SplashScreenDelay", &settings.splashScreenDelay}, - {'Y', 0, 0, 0, 86399999, 0, TYPE_START, valInt, "StartTime", &week}, - {'Y', 0, 0, 0, 86399999, 0, TYPE_TIME, valInt, "TimeOfDay", &timeOfDay}, - {'Y', 0, 0, 0, 7200000, 0, TYPE_DURATION, valInt, "ZoneDuration", &week}, - {'Y', 0, 0, 0, 1, 0, TYPE_ZONE_MASK, valInt, "ZoneManualOn", &zoneManualOn}, + {'V', 0, 0, 0, MAX_VC - 1, 0, TYPE_U8, valInt, "CmdVC", &cmdVc}, }; const int commandCount = sizeof(commands) / sizeof(commands[0]); @@ -1393,57 +1191,12 @@ const int commandCount = sizeof(commands) / sizeof(commands[0]); //Display a command void commandDisplay(const COMMAND_ENTRY * command) { - uint32_t hours; - uint32_t minutes; - CONTROLLER_SCHEDULE * schedule; - uint32_t seconds; - char * string; - char timeString[12]; - uint32_t value; - //Print the setting value switch (command->type) { case TYPE_BOOL: systemPrint((uint8_t)(*(bool *)(command->setting))); break; - case TYPE_DURATION: - if (!commandZone) - break; - schedule = (CONTROLLER_SCHEDULE *)command->setting; - value = schedule[commandDay].zoneScheduleDuration[commandZone-1]; - - //Compute the time - seconds = value; - hours = seconds / MILLISECONDS_IN_AN_HOUR; - seconds -= hours * MILLISECONDS_IN_AN_HOUR; - minutes = seconds / MILLISECONDS_IN_A_MINUTE; - seconds -= minutes * MILLISECONDS_IN_A_MINUTE; - seconds /= MILLISECONDS_IN_A_SECOND; - - //Build the string - string = timeString; - if (hours > 9) - *string++ = '0' + (hours / 10); - *string++ = '0' + (hours % 10); - *string++ = ':'; - *string++ = '0' + (minutes / 10); - *string++ = '0' + (minutes % 10); - *string++ = ':'; - *string++ = '0' + (seconds / 10); - *string++ = '0' + (seconds % 10); - *string++ = 0; - - //Display the time - systemPrint(value); - systemPrint(" ("); - systemPrint(dayName[commandDay]); - systemPrint(" zone "); - systemPrint(commandZone); - systemPrint(" "); - systemPrint(timeString); - systemPrint(")"); - break; case TYPE_FLOAT: systemPrint(*((float *)(command->setting)), command->digits); break; @@ -1461,78 +1214,6 @@ void commandDisplay(const COMMAND_ENTRY * command) case TYPE_U32: systemPrint(*(uint32_t *)(command->setting)); break; - case TYPE_START: - schedule = (CONTROLLER_SCHEDULE *)command->setting; - value = schedule[commandDay].scheduleStartTime; - - //Compute the time - seconds = value; - hours = seconds / MILLISECONDS_IN_AN_HOUR; - seconds -= hours * MILLISECONDS_IN_AN_HOUR; - minutes = seconds / MILLISECONDS_IN_A_MINUTE; - seconds -= minutes * MILLISECONDS_IN_A_MINUTE; - seconds /= MILLISECONDS_IN_A_SECOND; - - //Build the string - string = timeString; - if (hours > 9) - *string++ = '0' + (hours / 10); - *string++ = '0' + (hours % 10); - *string++ = ':'; - *string++ = '0' + (minutes / 10); - *string++ = '0' + (minutes % 10); - *string++ = ':'; - *string++ = '0' + (seconds / 10); - *string++ = '0' + (seconds % 10); - *string++ = 0; - - //Display the time - systemPrint(value); - systemPrint(" ("); - systemPrint(dayName[commandDay]); - systemPrint(" @ "); - systemPrint(timeString); - systemPrint(")"); - break; - case TYPE_TIME: - //Compute the time - seconds = *(uint32_t *)(command->setting) - startOfDay; - hours = seconds / MILLISECONDS_IN_AN_HOUR; - seconds -= hours * MILLISECONDS_IN_AN_HOUR; - minutes = seconds / MILLISECONDS_IN_A_MINUTE; - seconds -= minutes * MILLISECONDS_IN_A_MINUTE; - seconds /= MILLISECONDS_IN_A_SECOND; - - //Build the string - string = timeString; - if (hours > 9) - *string++ = '0' + (hours / 10); - *string++ = '0' + (hours % 10); - *string++ = ':'; - *string++ = '0' + (minutes / 10); - *string++ = '0' + (minutes % 10); - *string++ = ':'; - *string++ = '0' + (seconds / 10); - *string++ = '0' + (seconds % 10); - *string++ = 0; - - //Display the time - systemPrint((uint32_t)(*(uint32_t *)(command->setting))); - systemPrint(" ("); - systemPrint(timeString); - systemPrint(")"); - break; - case TYPE_DAY: - systemPrint(*(uint8_t *)(command->setting)); - systemPrint(" ("); - systemWrite(dayLetter[*(uint8_t *)(command->setting)]); - systemPrint(", "); - systemPrint(dayName[*(uint8_t *)(command->setting)]); - systemPrint(")"); - break; - case TYPE_ZONE_MASK: - systemPrintln(((*(ZONE_MASK *)(command->setting)) >> commandZone) & 1); - break; } systemPrintln(); } @@ -1588,9 +1269,6 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer { const char * digit; double doubleSettingValue; - int index; - uint32_t number; - CONTROLLER_SCHEDULE * schedule; uint32_t settingValue; bool valid; @@ -1636,15 +1314,6 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer if (valid) *(bool *)(command->setting) = (bool)settingValue; break; - case TYPE_DURATION: - valid = command->validate((void *)&settingValue, command->minValue, command->maxValue) - && commandZone; - if (valid) - { - schedule = (CONTROLLER_SCHEDULE *)command->setting; - schedule[commandDay].zoneScheduleDuration[commandZone- 1] = settingValue; - } - break; case TYPE_FLOAT: valid = command->validate((void *)&doubleSettingValue, command->minValue, command->maxValue); if (valid) @@ -1658,21 +1327,11 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer break; case TYPE_SPEED_AIR: case TYPE_SPEED_SERIAL: - case TYPE_TIME: case TYPE_U32: valid = command->validate((void *)&settingValue, command->minValue, command->maxValue); if (valid) *(uint32_t *)(command->setting) = settingValue; break; - case TYPE_START: - valid = command->validate((void *)&settingValue, command->minValue, command->maxValue); - if (valid) - { - schedule = (CONTROLLER_SCHEDULE *)command->setting; - schedule[commandDay].scheduleStartTime = settingValue; - } - break; - case TYPE_DAY: case TYPE_U8: valid = command->validate((void *)&settingValue, command->minValue, command->maxValue); if (valid) @@ -1683,14 +1342,6 @@ bool commandSetOrDisplayValue(const COMMAND_ENTRY * command, const char * buffer if (valid) *(uint16_t *)(command->setting) = (uint16_t)settingValue; break; - case TYPE_ZONE_MASK: - valid = command->validate((void *)&settingValue, command->minValue, command->maxValue); - if (valid) - { - *(ZONE_MASK *)(command->setting) &= ~(1 << (commandZone- 1)); - *(ZONE_MASK *)(command->setting) |= settingValue << (commandZone - 1); - } - break; } if (valid == false) break; diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino index 61b58ca7..aec8011a 100644 --- a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino +++ b/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino @@ -504,52 +504,6 @@ unsigned long retransmitTimeout = 0; //Throttle back re-transmits //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- -//Sprinkler controller variables -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - -//Days of week -const char dayLetter[7] = {'U', 'M', 'T', 'W', 'R', 'F', 'S'}; -const char * day3Letter[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; -const char * dayName[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; - -//Display support -ZONE_MASK zoneOn; -ZONE_MASK relayOn; - -//Sprinkler solenoid management -uint8_t zoneNumber; //0 = None, 1 - ZONE_NUMBER_MAX -ZONE_MASK latchingSolenoid = 0xff;//Mask of latching solenoids -ZONE_MASK zoneActive; //The current zone that is on or off -ZONE_MASK zoneManualOn; //Mask of zones on/off, set only one bit! -ZONE_MASK zoneManualPreviousOn; //Previous mask of zones on and off -uint32_t pulseDuration; //Length of the pulse in milliseconds, off = 0 -uint32_t pulseStartTime; //Time the pulse started -uint32_t onTime; //Time the zone was turned on -bool enableSprinklerController; //Enable the sprinkler controller -bool turnWaterOff; //Set true when zone is being turned off - -//Sprinkler controller schedule for today -uint32_t startOfDay; //Number of milliseconds at midnight -uint32_t timeOfDay; //Number of milliseconds from midnight -uint8_t dayOfWeek; //Day of week 0 - 6 -bool scheduleCopied; //True after daily schedule copied from week to today; -bool scheduleActive; //True while an entry is active in the schedule - -CONTROLLER_SCHEDULE week[7]; -CONTROLLER_SCHEDULE today; //Active schedule -uint32_t zoneOnTime; //Time when the zone was turned on -uint32_t zoneOnDuration; //Amount of time that the zone should be on - -#define MILLISECONDS_IN_A_SECOND 1000 -#define MILLISECONDS_IN_A_MINUTE (60 * MILLISECONDS_IN_A_SECOND) -#define MILLISECONDS_IN_AN_HOUR (60 * MILLISECONDS_IN_A_MINUTE) -#define MILLISECONDS_IN_A_DAY (24 * MILLISECONDS_IN_AN_HOUR) - -//Sprinkler commands -uint8_t commandZone; //Zone number for commands, 0 - ZONE_NUMBER_MAX -uint8_t commandDay; //Day of week number or commands, 0 - 6 -//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- - //Global variables //-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- const Settings defaultSettings; From 6e4b3f0425d4974a0825404d4904d15a7187c0b6 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 1 Feb 2023 08:49:06 -0700 Subject: [PATCH 541/594] Remove heartbeat timeout update from MP data packets --- Firmware/LoRaSerial_Firmware/Radio.ino | 2 +- Firmware/LoRaSerial_Firmware/States.ino | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 5f21ad51..30b18924 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -3303,7 +3303,7 @@ void syncChannelTimer(uint32_t frameAirTimeUsec) frameAirTimeMsec = (frameAirTimeUsec + settings.txToRxUsec + micros() - transactionCompleteMicros) / 1000; rmtHopTimeMsec = msToNextHopRemote - frameAirTimeMsec; - //Compute the when the local system last hopped + //Compute when the local system last hopped lclHopTimeMsec = currentMillis - channelTimerStart; adjustment = 0; diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial_Firmware/States.ino index 3be002e3..f92d0668 100644 --- a/Firmware/LoRaSerial_Firmware/States.ino +++ b/Firmware/LoRaSerial_Firmware/States.ino @@ -1288,8 +1288,6 @@ void updateRadioState() //Received data - do not ack. triggerEvent(TRIGGER_RX_DATA); - setHeartbeatMultipoint(); //We're sync'd so reset heartbeat timer - //Place any available data in the serial output buffer serialBufferOutput(rxData, rxDataBytes); @@ -1325,7 +1323,6 @@ void updateRadioState() if (xmitDatagramMpData() == true) { triggerEvent(TRIGGER_MP_TX_DATA); - setHeartbeatMultipoint(); //We're sending something with clock data so reset heartbeat timer changeState(RADIO_MP_WAIT_TX_DONE); } } From c439675468b7ff6cf4f0500ca42e197239facd30 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 2 Feb 2023 08:47:36 -0700 Subject: [PATCH 542/594] Revert Arch_SAMD.h --- Firmware/LoRaSerial_Firmware/Arch_SAMD.h | 14 ++++-- Firmware/LoRaSerial_Firmware/Test_Script.txt | 51 -------------------- 2 files changed, 9 insertions(+), 56 deletions(-) delete mode 100644 Firmware/LoRaSerial_Firmware/Test_Script.txt diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h index 4823bca0..9fa78171 100644 --- a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h +++ b/Firmware/LoRaSerial_Firmware/Arch_SAMD.h @@ -21,10 +21,8 @@ WDTZero myWatchDog; +--------------+ | | SAMD | | | | | - | Sprinkler | | - | Controller | | - | | +--------------+ V - Debug | SPI |<----->| SX1276 Radio |<---> Antenna + TTL Serial <-->| Serial1 | +--------------+ V + | SPI |<----->| SX1276 Radio |<---> Antenna USB Serial <-->| Serial | +--------------+ +--------------+ @@ -158,6 +156,8 @@ void samdBeginSerial(uint16_t serialSpeed) if (settings.usbSerialWait) //Wait for serial to come online for debug printing while (!Serial); + + Serial1.begin(serialSpeed); } //Initialize the watch dog timer @@ -194,13 +194,14 @@ Module * samdRadio() //Determine if serial input data is available bool samdSerialAvailable() { - return (Serial.available()); + return (Serial.available() || Serial1.available()); } //Ensure that all serial output data has been sent over USB and via the UART void samdSerialFlush() { Serial.flush(); + Serial1.flush(); } //Read in the serial input data @@ -209,6 +210,8 @@ uint8_t samdSerialRead() byte incoming = 0; if (Serial.available()) incoming = Serial.read(); + else if (Serial1.available()) + incoming = Serial1.read(); return (incoming); } @@ -216,6 +219,7 @@ uint8_t samdSerialRead() void samdSerialWrite(uint8_t value) { Serial.write(value); + Serial1.write(value); } //Reset the CPU diff --git a/Firmware/LoRaSerial_Firmware/Test_Script.txt b/Firmware/LoRaSerial_Firmware/Test_Script.txt deleted file mode 100644 index 10f7c6a0..00000000 --- a/Firmware/LoRaSerial_Firmware/Test_Script.txt +++ /dev/null @@ -1,51 +0,0 @@ -at-DayOfWeek=1 -at-TimeOfDay=59310000 - -ath -ati88 - -at-CommandZone=1 -at-LatchingSolenoid=0 - -at-CommandDay=1 -at-StartTime=81000000 -at-CommandZone=1 -at-ZoneDuration=300000 - -at-CommandDay=3 -at-StartTime=81000000 -at-CommandZone=2 -at-ZoneDuration=300000 - -at-CommandDay=5 -at-StartTime=81000000 -at-CommandZone=1 -at-ZoneDuration=300000 -at-CommandZone=2 -at-ZoneDuration=300000 - -ati88 - - - -================================================================================ -===== Testing ===== -================================================================================ - -at-DayOfWeek=5 -at-TimeOfDay=81000000 - -ath - -at-CommandDay=5 -at-StartTime=81010000 -at-CommandZone=1 -at-LatchingSolenoid=0 -at-ZoneDuration=7000 -at-CommandZone=2 -at-ZoneDuration=7000 - -at-EnableController=1 - -ati89 -ati88 From ee7b44000f4613dc219e49a0bf78184fd069cdba Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Fri, 27 Jan 2023 16:51:52 -1000 Subject: [PATCH 543/594] VcServerTest: Enable/disable DISPLAY_UNKNOWN_COMMANDS --- Firmware/Tools/VcServerTest.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 96c1dae9..4a42b866 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -19,6 +19,7 @@ #define DISPLAY_COMMAND_COMPLETE 0 #define DISPLAY_DATA_ACK 0 #define DISPLAY_DATA_NACK 1 +#define DISPLAY_UNKNOWN_COMMANDS 0 #define DISPLAY_VC_STATE 0 #define SEND_ATC_COMMAND 1 #define DISPLAY_STATE_TRANSITION 0 @@ -537,12 +538,15 @@ int radioToHost() //Unknown messages else { - printf("Unknown message, VC Header:\n"); - printf(" length: %d\n", header->radio.length); - printf(" destVc: %d\n", header->radio.destVc); - printf(" srcVc: %d\n", header->radio.srcVc); - if (length > 0) - dumpBuffer(data, length); + if (DISPLAY_UNKNOWN_COMMANDS) + { + printf("Unknown message, VC Header:\n"); + printf(" length: %d\n", header->radio.length); + printf(" destVc: %d\n", header->radio.destVc); + printf(" srcVc: %d\n", header->radio.srcVc); + if (length > 0) + dumpBuffer(data, length); + } } //Continue processing the rest of the data in the buffer From 6ab58bad186798a57779f8e4c263fb0c74823cad Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 5 Feb 2023 14:21:34 -1000 Subject: [PATCH 544/594] Adjust heartbeatTimeout depending upon airSpeed --- Firmware/LoRaSerial_Firmware/Commands.ino | 3 ++- Firmware/LoRaSerial_Firmware/Radio.ino | 23 +++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial_Firmware/Commands.ino index c4a828ff..8184a1e1 100644 --- a/Firmware/LoRaSerial_Firmware/Commands.ino +++ b/Firmware/LoRaSerial_Firmware/Commands.ino @@ -1065,7 +1065,8 @@ bool valSpeedAir (void * value, uint32_t valMin, uint32_t valMax) //Adjust the settings to match the requested airSpeed convertAirSpeedToSettings(settingValue); airSpeed = 0; - systemPrintln("Warning: AirSpeed overrides bandwidth, spread factor, and coding rate"); + systemPrintln("Warning: AirSpeed overrides bandwidth, coding rate, spread factor,"); + systemPrintln("heartbeatTimeout and txToRxUsec"); } return valid; } diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial_Firmware/Radio.ino index 30b18924..8fd6c9ea 100644 --- a/Firmware/LoRaSerial_Firmware/Radio.ino +++ b/Firmware/LoRaSerial_Firmware/Radio.ino @@ -106,6 +106,14 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 11; settings.radioBandwidth = 62.5; settings.radioCodingRate = 8; + //HEARTBEAT bytes worst case + // P2P - 13, + // MP - 7, + // VC - 30, + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + settings.heartbeatTimeout = 60 * 1000; + else + settings.heartbeatTimeout = 25 * 1000; //uSec: 26018 26026 26026 26025 26020 26038 ==> ~26026 settings.txToRxUsec = 26026; break; @@ -113,6 +121,10 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 10; settings.radioBandwidth = 62.5; settings.radioCodingRate = 8; + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + settings.heartbeatTimeout = 15 * 1000; + else + settings.heartbeatTimeout = 8 * 1000; //uSec: 12187 12188 12189 12190 12191 12194 ==> ~12190 settings.txToRxUsec = 12190; break; @@ -120,6 +132,10 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 10; settings.radioBandwidth = 125; settings.radioCodingRate = 8; + if (settings.operatingMode == MODE_VIRTUAL_CIRCUIT) + settings.heartbeatTimeout = 9 * 1000; + else + settings.heartbeatTimeout = 5 * 1000; //uSec: 6072 6070 6072 6070 6069 6067 ==> ~6070 settings.txToRxUsec = 6070; break; @@ -127,6 +143,7 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 9; settings.radioBandwidth = 125; settings.radioCodingRate = 8; + settings.heartbeatTimeout = 5 * 1000; //uSec: 2770 2777 2772 2773 2771 2773 ==> ~2773 settings.txToRxUsec = 2773; break; @@ -134,6 +151,7 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 10; settings.radioBandwidth = 500; settings.radioCodingRate = 8; + settings.heartbeatTimeout = 5 * 1000; //uSec: 1495 1481 1482 1481 1482 1481 ==> ~1484 settings.txToRxUsec = 1484; break; @@ -141,6 +159,7 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 9; settings.radioBandwidth = 500; settings.radioCodingRate = 8; + settings.heartbeatTimeout = 5 * 1000; //uSec: 657 657 657 658 657 657 ==> ~657 settings.txToRxUsec = 657; break; @@ -148,6 +167,7 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 8; settings.radioBandwidth = 500; settings.radioCodingRate = 7; + settings.heartbeatTimeout = 5 * 1000; //uSec: 279 279 281 280 280 279 ==> ~280 settings.txToRxUsec = 280; break; @@ -155,6 +175,7 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 7; settings.radioBandwidth = 500; settings.radioCodingRate = 7; + settings.heartbeatTimeout = 5 * 1000; //uSec: 119 118 118 119 120 119 ==> ~119 settings.txToRxUsec = 119; break; @@ -162,6 +183,7 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 6; settings.radioBandwidth = 500; settings.radioCodingRate = 6; + settings.heartbeatTimeout = 5 * 1000; //uSec: ??? settings.txToRxUsec = 0; break; @@ -169,6 +191,7 @@ void convertAirSpeedToSettings(uint16_t airSpeed) settings.radioSpreadFactor = 6; settings.radioBandwidth = 500; settings.radioCodingRate = 5; + settings.heartbeatTimeout = 5 * 1000; //uSec: ??? settings.txToRxUsec = 0; break; From 14958ff100edc3165eb7fe221be15efae41ccb96 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 5 Feb 2023 14:40:28 -1000 Subject: [PATCH 545/594] Add Atmel SAM D21G datasheet link --- Documents/Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Documents/Readme.md b/Documents/Readme.md index 30c127ee..39fcefc1 100644 --- a/Documents/Readme.md +++ b/Documents/Readme.md @@ -9,5 +9,6 @@ The available airspeeds (40 to 38400) used in LoRaSerial were picked to, as clos Additionally, this folder contains the various datasheets for the SX1276 IC, common to all LoRaSerial modules, as well as the individual datasheets for the different module types (915, 868, 433, etc). +[Atmel SAM D21G Datasheet](https://cdn.sparkfun.com/datasheets/Dev/Arduino/Boards/Atmel-42181-SAM-D21_Datasheet.pdf) From 8380a775785b13913d10ea9d1ea54beda5a61991 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 5 Feb 2023 17:03:22 -1000 Subject: [PATCH 546/594] Add some virtual circuit documentation --- Documents/Virtual_Circuits.md | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 Documents/Virtual_Circuits.md diff --git a/Documents/Virtual_Circuits.md b/Documents/Virtual_Circuits.md new file mode 100644 index 00000000..a244c7d6 --- /dev/null +++ b/Documents/Virtual_Circuits.md @@ -0,0 +1,62 @@ +Virtual Circuit Mode +==================== + +Multiple radios can communicate to each other in Virtual Circuit mode. This mode provides guaranteed message delivery or the link breaks. Virtual circuit mode supports: +* Up to 32 radios +* Virtual circuit serial interface +* Local command support +* Remote command support +* [Header file](https://github.com/sparkfun/SparkFun_LoRaSerial/blob/release_candidate/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h) for C and C++ + +## Virtual Circuit Network Configuration + +A LoRaSerial virtual circuit network consists of a single server radio and up to 31 client radios. The server transmits HEARTBEAT frames that provide the synchronization signal for the channel hop timer. Each time the timer fires, the radio switches to a new center frequency (channel). + +Clients wait to receive a HEARTBEAT frame from the server on channel 0. If the selectLedUse is set to LED_VC (2) then the blue LED will flash upon when the server's HEARTBEAT frame is received. After receiving the server's HEARTBEAT frame the client synchronizes its hop timer and starts hopping channels. If the selectLedUse is set to LED_VC (2) then each hop is indicated by a flash of the yellow LED. + +The virtual circuit server assigns virtual circuit ID numbers to all of the client radios. This may be done during radio training or during the first time the radio is connected to the network. After receiving a HEARTBEAT frame from the server, the client sends a HEARTBEAT frame with the source VC address of VC_UNASSIGNED and the radio's 16-byte unique ID. The server echos this HEARTBEAT replacing the source VC with the assigned VC number. While the client is using VC_UNASSIGNED as it's VC number, the client radio compares the unique ID in the HEARTBEAT frame to the local radio's unique ID value. If the values match then the client replaces its VC number with the assigned VC number. + +For consistent processing, the server records the client unique ID values in the non-volatile memory. These values are read and loaded into the virtual circuit table when the system boots again. + +### Radio Parameters + +Select a LoRaSerial radio for the virtual circuit server. Set the following parameters on the server radio. + +The following parameters should be set to enable virtual circuit mode: +* AT=OperatingMode=2 - Select virtual circuit mode +* AT-NetID=n - Choose a unique value (n) for your network +* AT-VerifyNetID=1 - Eliminate communications from other radios which are not using the NetID value +* AT-EnableCRC16=? - Enable software CRC-16 by setting the value to 1, otherwise set the value to zero to disable software CRC +* AT-SelectLedUse=2 - The use of LEDS_VC (2) flashes the blue LED when server HEARTBEAT frames are received by the clients, providing a visual indication that the radio is synchronizing with the server +* AT-Server=1 - Define this radio as the virtual circuit server +* ATW - Write the parameters to the non-volatile memory (NVM). + +Now enter training mode with the ATT command. The training enables the server to pass these parameters (except for server) to the client radios. On the client radios, with power applied, press the button at the top of the client radio to enter training mode. The radio will obtain the parameters from the server, write them to NVM and then reboot using the new parameters. + +After all of the client radios are trained, the ATZ command may be entered on the server radio to cause it to reboot. + +## Virtual Circuit Communications + +Communication is possible between the server and client or between clients. A three-way handshake must be performed prior to normal data communications to synchronize the ACK numbers. By default, the three-way handshake is not performed. The user or application initiates the handshake using the AT-CmdVc=n to select the client or server radio (n) for communications. Next the ATC command initiates the three-way handshake between the local radio and the remote radio (n). + +After the three-way handshake communications between the two radios is possible in virtual circuit mode. + +## Virtual Circuit Number + +The virtual circuit number of the local radio is obtained by issuing the ATI11 command to the local radio. The response returns a number in the range of zero (0) to MAX_VC. The value VC_UNASSIGNED is returned between reset until the server assigns a virtual circuit number to the local radio. + +## Virtual Circuit Serial Interface + +In virtual circuit mode all communications over the serial port must be proceeded by a VC_SERIAL_MESSAGE_HEADER. Data not proceeded by a VC_SERIAL_MESSAGE_HEADER data structure is discarded! The VC_SERIAL_MESSAGE_HEADER is defined in the [Virtual_Circuit_Protocol.h](https://github.com/sparkfun/SparkFun_LoRaSerial/blob/release_candidate/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h) header file. This data structure is four (4) bytes long starting with the value START_OF_VC_SERIAL (2). The next byte specifies the length of the binary data following the VC_SERIAL_MESSAGE_HEADER plus the size of VC_RADIO_MESSAGE_HEADER (3). The other two bytes are virtual circuit numbers for the destination and source virtual circuits. + +The server radio is identified by VC_SERVER (0) and client radios have VC numbers between 1 and MAX_VC. A data message sent from the server to client 2 would have the source VC set to VC_SERVER (0) and the destination VC set to 2. + +## Local Command Support + +One pair of virtual circuit numbers allows the local host to communicate with the command interface on the local radio. The data portion of the message contains the command to be executed. +* VC_COMMAND: Destination VC used by the host computer to send a command to the local radio to be executed immediately +* PC_COMMAND: Source VC for the local command message + +## Remote Command Support + +The VC number range from 32 to 63 is reserved for remote command execution. The VC number equals the target radio number (0 - 31) or-ed with PC_REMOTE_COMMAND. This VC number is placed in the destination VC field and the local radio VC number is placed in the source VC field. From 8b6aba0f3a48a462dd24cfd53ce4f01bc42e61ee Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Sun, 5 Feb 2023 17:41:59 -1000 Subject: [PATCH 547/594] Add LoRaSerial mode description and pointer to VC documentation --- Documents/Readme.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Documents/Readme.md b/Documents/Readme.md index 30c127ee..10da0d6a 100644 --- a/Documents/Readme.md +++ b/Documents/Readme.md @@ -9,5 +9,15 @@ The available airspeeds (40 to 38400) used in LoRaSerial were picked to, as clos Additionally, this folder contains the various datasheets for the SX1276 IC, common to all LoRaSerial modules, as well as the individual datasheets for the different module types (915, 868, 433, etc). +## Modes of Operation +The LoRaSerial radio operates in one of three modes: +* Point-to-Point (default) +* Multipoint +* Virtual Circuit +Point-to-Point mode provides guaranteed message delivery or the link breaks. The radio performs data retransmission if either the data frame was lost or its acknowledgement was lost. This can continue indefinitely if MaxResends equals zero (default) or for a limited number of retries in the range of (1 - 255). + +Multipoint mode provides a datagram service. The LoRaSerial radios will send the data frame without a guarantee that the frame will be received by the remote radio. Lost frames are lost, the radio does no perform retransmission. If the application is not able to tolerate the lost frames then another protocol layer needs to be implemented on the host computer between the radio and the application that provides the necessary services to the application. + +Virtual circuit mode enables a group of radios to communicate with each other. The radio links provide guaranteed message delivery or the link is broken. One radio in the group is designated as the server and provides the channel timer synchronization for the client radios, think of a star configuration with the server at the center. Data communications with the virtual circuit mode is all point-to-point. Communications between the radio and the host CPU use a special virtual circuit header to identify where to send the host to radio data, or where to deliver the radio to host data. More information is available [here](https://github.com/sparkfun/SparkFun_LoRaSerial/blob/release_candidate/Docs/Virtual_Circuits.md). From a99bd3f84b031b30ad9b4bb919c1d9330d0046e3 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 6 Feb 2023 13:11:41 -1000 Subject: [PATCH 548/594] VC Doc: Add radio responses --- Documents/Virtual_Circuits.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documents/Virtual_Circuits.md b/Documents/Virtual_Circuits.md index a244c7d6..0b8dc717 100644 --- a/Documents/Virtual_Circuits.md +++ b/Documents/Virtual_Circuits.md @@ -51,6 +51,14 @@ In virtual circuit mode all communications over the serial port must be proceede The server radio is identified by VC_SERVER (0) and client radios have VC numbers between 1 and MAX_VC. A data message sent from the server to client 2 would have the source VC set to VC_SERVER (0) and the destination VC set to 2. +### Radio Responses + +The LoRaSerial radio will return the following responses: + +* Data - Returns VC_DATA_ACK_NACK_MESSAGE. The response is sent to PC_DATA_ACK when the data is acknowledged by the remote radio. A response of PC_DATA_NACKis returned when the link is broken. +* Local Command - A response is sent to PC_COMMAND_COMPLETE upon command completion with the status VC_CMD_SUCCESS or VC_CMD_ERROR. +* Remote Command - The command is acknowledged with a VC_DATA_ACK_NACK_MESSAGE sent to the PC_DATA_ACK or PC_DATA_NACK destination port. The actual command response is sent to the local radio's VC ored with PC_REMOTE_RESPONSE. After the command response text is delivered, the command status is returned to the destination VC of PC_COMMAND_COMPLETE with the status of VC_CMD_SUCCESS or VC_CMD_ERROR. + ## Local Command Support One pair of virtual circuit numbers allows the local host to communicate with the command interface on the local radio. The data portion of the message contains the command to be executed. From 62bcf0ce029d3466a13df1a6db4887fa6a2e57cf Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 6 Feb 2023 13:06:04 -1000 Subject: [PATCH 549/594] Fix the link to Virtual_Circuits.md --- Documents/Readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Documents/Readme.md b/Documents/Readme.md index 45d2cb59..df543bdf 100644 --- a/Documents/Readme.md +++ b/Documents/Readme.md @@ -12,7 +12,6 @@ Additionally, this folder contains the various datasheets for the SX1276 IC, com [Atmel SAM D21G Datasheet](https://cdn.sparkfun.com/datasheets/Dev/Arduino/Boards/Atmel-42181-SAM-D21_Datasheet.pdf) ## Modes of Operation -======= The LoRaSerial radio operates in one of three modes: * Point-to-Point (default) @@ -23,4 +22,4 @@ Point-to-Point mode provides guaranteed message delivery or the link breaks. Th Multipoint mode provides a datagram service. The LoRaSerial radios will send the data frame without a guarantee that the frame will be received by the remote radio. Lost frames are lost, the radio does no perform retransmission. If the application is not able to tolerate the lost frames then another protocol layer needs to be implemented on the host computer between the radio and the application that provides the necessary services to the application. -Virtual circuit mode enables a group of radios to communicate with each other. The radio links provide guaranteed message delivery or the link is broken. One radio in the group is designated as the server and provides the channel timer synchronization for the client radios, think of a star configuration with the server at the center. Data communications with the virtual circuit mode is all point-to-point. Communications between the radio and the host CPU use a special virtual circuit header to identify where to send the host to radio data, or where to deliver the radio to host data. More information is available [here](https://github.com/sparkfun/SparkFun_LoRaSerial/blob/release_candidate/Docs/Virtual_Circuits.md). +Virtual circuit mode enables a group of radios to communicate with each other. The radio links provide guaranteed message delivery or the link is broken. One radio in the group is designated as the server and provides the channel timer synchronization for the client radios, think of a star configuration with the server at the center. Data communications with the virtual circuit mode is all point-to-point. Communications between the radio and the host CPU use a special virtual circuit header to identify where to send the host to radio data, or where to deliver the radio to host data. More information is available [here](https://github.com/sparkfun/SparkFun_LoRaSerial/blob/release_candidate/Documents/Virtual_Circuits.md). From 207d5a546b80b0a6ed18999755cd60d40341d049 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Mon, 6 Feb 2023 13:30:34 -1000 Subject: [PATCH 550/594] Add VcServerTest program documentation --- Documents/Virtual_Circuits.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documents/Virtual_Circuits.md b/Documents/Virtual_Circuits.md index 0b8dc717..fdb4c065 100644 --- a/Documents/Virtual_Circuits.md +++ b/Documents/Virtual_Circuits.md @@ -68,3 +68,7 @@ One pair of virtual circuit numbers allows the local host to communicate with th ## Remote Command Support The VC number range from 32 to 63 is reserved for remote command execution. The VC number equals the target radio number (0 - 31) or-ed with PC_REMOTE_COMMAND. This VC number is placed in the destination VC field and the local radio VC number is placed in the source VC field. + +## Example Program + +The [VcServerTest.c](https://github.com/sparkfun/SparkFun_LoRaSerial/blob/release_candidate/Firmware/Tools/VcServerTest.c) is an example C program that communicates with the VC server using the virtual circuit serial interface. The first parameter is the device path to the local LoRaSerial radio. The second parameter specifies the destination VC number to send commands or data. From 488565d2bada5a87f89f519f74d07cc76cc03abb Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 7 Feb 2023 15:30:33 -1000 Subject: [PATCH 551/594] Get the cylon pattern to display during training --- Firmware/LoRaSerial_Firmware/System.ino | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial_Firmware/System.ino index 4f7f7e2f..7daf84b4 100644 --- a/Firmware/LoRaSerial_Firmware/System.ino +++ b/Firmware/LoRaSerial_Firmware/System.ino @@ -602,7 +602,7 @@ void setRSSI(uint8_t ledBits) void startCylonLEDs() { cylonLedPattern = 0b0001; - cylonPatternGoingLeft = true; + cylonPatternGoingLeft = false; } //Update the RSSI LED or LEDs @@ -625,16 +625,16 @@ void blinkRadioRssiLed() //The following shows the cylon pattern in four LEDs // - // LED3 LED2 LED1 LED0 + // LED3 LED2 LED1 LED0 // X - // X - // X - // X - // X - // X + // X + // X + // X + // X + // X // X - // X - // ... + // X + // ... // //Cylon the RSSI LEDs setRSSI(cylonLedPattern); From d50d449942ac68e393df0753b09229a20c716b46 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 8 Feb 2023 16:27:01 -0700 Subject: [PATCH 552/594] Remove '_Firmware' from file name and directory. --- Firmware/{LoRaSerial_Firmware => LoRaSerial}/Arch_ESP32.h | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/Arch_SAMD.h | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/Begin.ino | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/Commands.ino | 0 .../LoRaSerial_Firmware.ino => LoRaSerial/LoRaSerial.ino} | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/NVM.ino | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/Radio.ino | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/Serial.ino | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/States.ino | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/System.ino | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/Train.ino | 0 .../Virtual_Circuit_Protocol.h | 0 Firmware/{LoRaSerial_Firmware => LoRaSerial}/settings.h | 0 13 files changed, 0 insertions(+), 0 deletions(-) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/Arch_ESP32.h (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/Arch_SAMD.h (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/Begin.ino (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/Commands.ino (100%) rename Firmware/{LoRaSerial_Firmware/LoRaSerial_Firmware.ino => LoRaSerial/LoRaSerial.ino} (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/NVM.ino (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/Radio.ino (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/Serial.ino (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/States.ino (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/System.ino (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/Train.ino (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/Virtual_Circuit_Protocol.h (100%) rename Firmware/{LoRaSerial_Firmware => LoRaSerial}/settings.h (100%) diff --git a/Firmware/LoRaSerial_Firmware/Arch_ESP32.h b/Firmware/LoRaSerial/Arch_ESP32.h similarity index 100% rename from Firmware/LoRaSerial_Firmware/Arch_ESP32.h rename to Firmware/LoRaSerial/Arch_ESP32.h diff --git a/Firmware/LoRaSerial_Firmware/Arch_SAMD.h b/Firmware/LoRaSerial/Arch_SAMD.h similarity index 100% rename from Firmware/LoRaSerial_Firmware/Arch_SAMD.h rename to Firmware/LoRaSerial/Arch_SAMD.h diff --git a/Firmware/LoRaSerial_Firmware/Begin.ino b/Firmware/LoRaSerial/Begin.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/Begin.ino rename to Firmware/LoRaSerial/Begin.ino diff --git a/Firmware/LoRaSerial_Firmware/Commands.ino b/Firmware/LoRaSerial/Commands.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/Commands.ino rename to Firmware/LoRaSerial/Commands.ino diff --git a/Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino b/Firmware/LoRaSerial/LoRaSerial.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/LoRaSerial_Firmware.ino rename to Firmware/LoRaSerial/LoRaSerial.ino diff --git a/Firmware/LoRaSerial_Firmware/NVM.ino b/Firmware/LoRaSerial/NVM.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/NVM.ino rename to Firmware/LoRaSerial/NVM.ino diff --git a/Firmware/LoRaSerial_Firmware/Radio.ino b/Firmware/LoRaSerial/Radio.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/Radio.ino rename to Firmware/LoRaSerial/Radio.ino diff --git a/Firmware/LoRaSerial_Firmware/Serial.ino b/Firmware/LoRaSerial/Serial.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/Serial.ino rename to Firmware/LoRaSerial/Serial.ino diff --git a/Firmware/LoRaSerial_Firmware/States.ino b/Firmware/LoRaSerial/States.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/States.ino rename to Firmware/LoRaSerial/States.ino diff --git a/Firmware/LoRaSerial_Firmware/System.ino b/Firmware/LoRaSerial/System.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/System.ino rename to Firmware/LoRaSerial/System.ino diff --git a/Firmware/LoRaSerial_Firmware/Train.ino b/Firmware/LoRaSerial/Train.ino similarity index 100% rename from Firmware/LoRaSerial_Firmware/Train.ino rename to Firmware/LoRaSerial/Train.ino diff --git a/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h b/Firmware/LoRaSerial/Virtual_Circuit_Protocol.h similarity index 100% rename from Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h rename to Firmware/LoRaSerial/Virtual_Circuit_Protocol.h diff --git a/Firmware/LoRaSerial_Firmware/settings.h b/Firmware/LoRaSerial/settings.h similarity index 100% rename from Firmware/LoRaSerial_Firmware/settings.h rename to Firmware/LoRaSerial/settings.h From d205ee91fa17973a4cc9e0fd42e83cafd60a1ffd Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Wed, 8 Feb 2023 16:27:13 -0700 Subject: [PATCH 553/594] Adding compile action --- .github/workflows/compile-loraserial.yml | 110 +++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 .github/workflows/compile-loraserial.yml diff --git a/.github/workflows/compile-loraserial.yml b/.github/workflows/compile-loraserial.yml new file mode 100644 index 00000000..8a398f14 --- /dev/null +++ b/.github/workflows/compile-loraserial.yml @@ -0,0 +1,110 @@ +name: Build LoRaSerial +on: + workflow_dispatch: + branches: + +env: + FILENAME_PREFIX: SparkFun_LoRaSerial + VERSION_MAJOR: 2 + VERSION_MINOR: 0 + +jobs: + build: + + name: Build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Setup Arduino CLI + uses: arduino/setup-arduino-cli@v1 + + - name: Start config file + run: arduino-cli config init --additional-urls "https://raw.githubusercontent.com/sparkfun/Arduino_Boards/main/IDE_Board_Manager/package_sparkfun_index.json" + + - name: Update index + run: arduino-cli core update-index + + - name: Install Arduino SAMD platform + run: arduino-cli core install arduino:samd + + - name: Install SparkFun SAMD platforms + run: arduino-cli core install SparkFun:samd@1.8.9 + + - name: Enable external libs + run: arduino-cli config set library.enable_unsafe_install true + + - name: Get Libraries + run: arduino-cli lib install --git-url + https://github.com/jgromes/RadioLib.git + https://github.com/OperatorFoundation/Crypto.git + https://github.com/JChristensen/JC_Button.git + https://github.com/khoih-prog/SAMD_TimerInterrupt.git + https://github.com/khoih-prog/FlashStorage_SAMD.git + https://github.com/javos65/WDTZero.git + + #Original Crypto library: https://github.com/rweather/arduinolibs.git + + - name: Compile Sketch + run: arduino-cli compile --fqbn SparkFun:samd:LoRaSerial ./Firmware/LoRaSerial/LoRaSerial.ino --export-binaries + + - name: Get current date + id: date + run: echo "date=$(date +'%b_%d_%Y')" >> $GITHUB_OUTPUT + + - name: Get current date + id: dateNoScores + run: echo "dateNoScores=$(date +'%b %d %Y')" >> $GITHUB_OUTPUT + + - name: Extract branch name + run: echo "BRANCH=${{github.ref_name}}" >> $GITHUB_ENV + + #File_Name_v3_1.bin + #File_Name_RC-Jan_26_2023.bin + - name: Create file ending based on branch + run: | + if [[ $BRANCH == 'main' ]]; then + echo "FILE_ENDING_UNDERSCORE=_v${{ env.VERSION_MAJOR }}_${{ env.VERSION_MINOR }}" >> "$GITHUB_ENV" + echo "FILE_ENDING_NOUNDERSCORE=_v${{ env.VERSION_MAJOR }}.${{ env.VERSION_MINOR }}" >> "$GITHUB_ENV" + else + echo "FILE_ENDING_UNDERSCORE=_RC-${{ steps.date.outputs.date }}" >> "$GITHUB_ENV" + echo "FILE_ENDING_NOUNDERSCORE=_RC-${{ steps.dateNoScores.outputs.dateNoScores }}" >> "$GITHUB_ENV" + fi + + - name: Rename binary + run: | + cd Firmware/LoRaSerial/build/SparkFun.samd.LoRaSerial/ + mv LoRaSerial.ino.bin ${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}.bin + mv LoRaSerial.ino.with_bootloader.bin ${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}_with_bootloader.bin + + - name: Upload binary to action + uses: actions/upload-artifact@v3 + with: + name: ${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }} + path: ./Firmware/LoRaSerial/build/SparkFun.samd.LoRaSerial/${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}.bin + + - name: Push Firmware Update Binary to repo + uses: dmnemec/copy_file_to_another_repo_action@main + env: + API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_LORASERIAL_FILE_TOKEN }} + with: + source_file: ./Firmware/LoRaSerial/build/SparkFun.samd.LoRaSerial/${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}.bin + destination_repo: 'sparkfun/SparkFun_LoRaSerial/Binaries' + destination_folder: '' + user_email: 'nathan@sparkfun.com' + user_name: 'nseidle' + commit_message: 'Github Action - Updating Binary ${{ steps.dateNoScores.outputs.dateNoScores }}' + + - name: Push Production Firmware Binary to repo + uses: dmnemec/copy_file_to_another_repo_action@main + env: + API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_LORASERIAL_FILE_TOKEN }} + with: + source_file: ./Firmware/LoRaSerial/build/SparkFun.samd.LoRaSerial/${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}_with_bootloader.bin + destination_repo: 'sparkfun/SparkFun_LoRaSerial/Binaries' + destination_folder: '' + user_email: 'nathan@sparkfun.com' + user_name: 'nseidle' + commit_message: 'Github Action - Updating Binary ${{ steps.dateNoScores.outputs.dateNoScores }}' From aea4ec6472d5bbf7b1553314ba336568b67af515 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Tue, 7 Feb 2023 13:39:22 -1000 Subject: [PATCH 554/594] VC: Add more example code documentation --- Documents/Virtual_Circuits.md | 213 ++++++++++++++++++++++++++++++++-- 1 file changed, 204 insertions(+), 9 deletions(-) diff --git a/Documents/Virtual_Circuits.md b/Documents/Virtual_Circuits.md index fdb4c065..00b45cc5 100644 --- a/Documents/Virtual_Circuits.md +++ b/Documents/Virtual_Circuits.md @@ -27,6 +27,7 @@ The following parameters should be set to enable virtual circuit mode: * AT-NetID=n - Choose a unique value (n) for your network * AT-VerifyNetID=1 - Eliminate communications from other radios which are not using the NetID value * AT-EnableCRC16=? - Enable software CRC-16 by setting the value to 1, otherwise set the value to zero to disable software CRC +* AT-EncryptionKey=? - Choose a unique encryption key for the network * AT-SelectLedUse=2 - The use of LEDS_VC (2) flashes the blue LED when server HEARTBEAT frames are received by the clients, providing a visual indication that the radio is synchronizing with the server * AT-Server=1 - Define this radio as the virtual circuit server * ATW - Write the parameters to the non-volatile memory (NVM). @@ -35,6 +36,19 @@ Now enter training mode with the ATT command. The training enables the server t After all of the client radios are trained, the ATZ command may be entered on the server radio to cause it to reboot. +#### Example Virtual Circuit Initialization Script for Training + + +++ + AT=OperatingMode=2 + AT-NetID=3 + AT-VerifyNetID=1 + AT-EnableCRC16=1 + AT-EncryptionKey=54637374546373745463737454637374 + AT-SelectLedUse=2 + AT-Server=1 + ATW + ATT + ## Virtual Circuit Communications Communication is possible between the server and client or between clients. A three-way handshake must be performed prior to normal data communications to synchronize the ACK numbers. By default, the three-way handshake is not performed. The user or application initiates the handshake using the AT-CmdVc=n to select the client or server radio (n) for communications. Next the ATC command initiates the three-way handshake between the local radio and the remote radio (n). @@ -49,26 +63,207 @@ The virtual circuit number of the local radio is obtained by issuing the ATI11 c In virtual circuit mode all communications over the serial port must be proceeded by a VC_SERIAL_MESSAGE_HEADER. Data not proceeded by a VC_SERIAL_MESSAGE_HEADER data structure is discarded! The VC_SERIAL_MESSAGE_HEADER is defined in the [Virtual_Circuit_Protocol.h](https://github.com/sparkfun/SparkFun_LoRaSerial/blob/release_candidate/Firmware/LoRaSerial_Firmware/Virtual_Circuit_Protocol.h) header file. This data structure is four (4) bytes long starting with the value START_OF_VC_SERIAL (2). The next byte specifies the length of the binary data following the VC_SERIAL_MESSAGE_HEADER plus the size of VC_RADIO_MESSAGE_HEADER (3). The other two bytes are virtual circuit numbers for the destination and source virtual circuits. -The server radio is identified by VC_SERVER (0) and client radios have VC numbers between 1 and MAX_VC. A data message sent from the server to client 2 would have the source VC set to VC_SERVER (0) and the destination VC set to 2. +The server radio is identified by VC_SERVER (0) and client radios have VC numbers between 1 and MAX_VC. A data message sent from the server to client 3 would have the source VC set to VC_SERVER (0) and the destination VC set to 3. Example: + + START_OF_VC_SERIAL + +-----+-----+-----+-----+-----+-----+-----+-----+-----+ + | 2 | 8 | 3 | 0 | H | e | l | l | o | + +-----+-----+-----+-----+-----+-----+-----+-----+-----+ + Length Dest Src Data ### Radio Responses The LoRaSerial radio will return the following responses: -* Data - Returns VC_DATA_ACK_NACK_MESSAGE. The response is sent to PC_DATA_ACK when the data is acknowledged by the remote radio. A response of PC_DATA_NACKis returned when the link is broken. -* Local Command - A response is sent to PC_COMMAND_COMPLETE upon command completion with the status VC_CMD_SUCCESS or VC_CMD_ERROR. -* Remote Command - The command is acknowledged with a VC_DATA_ACK_NACK_MESSAGE sent to the PC_DATA_ACK or PC_DATA_NACK destination port. The actual command response is sent to the local radio's VC ored with PC_REMOTE_RESPONSE. After the command response text is delivered, the command status is returned to the destination VC of PC_COMMAND_COMPLETE with the status of VC_CMD_SUCCESS or VC_CMD_ERROR. +* Data response +* Local command response +* Remote command response +* VC state response +* Reconnect serial response + +#### Data Response + +When sending a data message, the radio returns a VC_DATA_ACK_NACK_MESSAGE. The response is sent to PC_DATA_ACK (0xe2) when the data is acknowledged by the remote radio. A response of PC_DATA_NACK (0xe3) is returned when the link is broken. + + START_OF_VC_SERIAL + +-----+-----+-----+-----+-----+-----+-----+-----+-----+ + | 2 | 8 | B | A | H | e | l | l | o | + +-----+-----+-----+-----+-----+-----+-----+-----+-----+ + Length Dest Src Data + + Host A Radio A Radio B Host B + Data Message ---> + Data Message ---> + <--- ACK + Data Message ---> + <--- VC_DATA_ACK_NACK_MESSAGE -## Local Command Support + START_OF_VC_SERIAL + +-----+-----+--------+-----+ + | 2 | 3 | 0xe2 | B | + +-----+-----+--------+-----+ + Length Dest Src + +#### Local Command Response +When sending a local command, the radio responds with VC_COMMAND_COMPLETE_MESSAGE sent to PC_COMMAND_COMPLETE (0xe5) with the status VC_CMD_SUCCESS (0) or VC_CMD_ERROR (1). One pair of virtual circuit numbers allows the local host to communicate with the command interface on the local radio. The data portion of the message contains the command to be executed. -* VC_COMMAND: Destination VC used by the host computer to send a command to the local radio to be executed immediately -* PC_COMMAND: Source VC for the local command message -## Remote Command Support +* VC_COMMAND (0xfe): Destination VC used by the host computer to send a command to the local radio to be executed immediately + +* PC_COMMAND (0xe0): Source VC for the local command message + + + START_OF_VC_SERIAL + +-----+-----+------+------+-----+-----+-----+-----+-----+------+------+ + | 2 | 8 | 0xe5 | 0xe0 | A | T | I | 1 | 1 | 0x0d | 0x0a | + +-----+-----+------+------+-----+-----+-----+-----+-----+------+------+ + Length Dest Src Command + + Host A Radio A + Command Message ---> + <--- Command response (ASCII text) + +------+------+---+---+---+---+---+---+...+------+------+---+---+------+------+ + | 0x0d | 0x0a | m | y | V | c | : | | A | 0x0d | 0x0a | O | K | 0x0d | 0x0a | + +------+------+---+---+---+---+---+---+...+...+--+------+---+---+------+------+ + <--- VC_COMMAND_COMPLETE_MESSAGE + + START_OF_VC_SERIAL + +-----+-----+--------+-----+-----+ + | 2 | 4 | 0xe2 | A | 0 | + +-----+-----+--------+-----+-----+ + Length Dest Src VC_CMD_SUCCESS + +#### Remote Command Response + +A command sent to a remote radio is acknowledged with a VC_DATA_ACK_NACK_MESSAGE sent to the PC_DATA_ACK (0xe2) or PC_DATA_NACK (0xe3) destination port. The actual command response is sent to the local radio's VC ored with PC_REMOTE_RESPONSE (0xc0). After the command response text is delivered, the command status is returned to the destination VC of PC_COMMAND_COMPLETE (0xe5) with the status of VC_CMD_SUCCESS (0) or VC_CMD_ERROR (1). + +The VC number range from 32 to 63 is reserved for remote command execution. The VC number equals the target radio number (0 - 31) or-ed with PC_REMOTE_COMMAND (0x20). This VC number is placed in the destination VC field and the local radio VC number is placed in the source VC field. + +The following example sends an ATI11 command from radio A to radio B to get radio +B's VC number: + + START_OF_VC_SERIAL + +-----+------+----------+-----+-----+-----+-----+-----+-----+------+------+ + | 2 | 10 | 0x20 + B | A | A | T | I | 1 | 1 | 0x0d | 0x0a | + +-----+------+----------+-----+-----+-----+-----+-----+-----+------+------+ + Length Dest Src Command + + Host A Radio A Radio B + Command Message ---> + Remote Command ---> + <--- ACK + <--- VC_DATA_ACK_NACK_MESSAGE + + START_OF_VC_SERIAL + +-----+-----+--------+-----+ + | 2 | 3 | 0xe2 | B | + +-----+-----+--------+-----+ + Length Dest Src + + <--- Remote Command Response + <--- Remove Command Response + +---+----+----------+---+------+------+---+---+---+---+---+---+...+------+------+---+---+------+------+ + | 2 | 18 | 0xc0 + A | B | 0x0d | 0x0a | m | y | V | c | : | | B | 0x0d | 0x0a | O | K | 0x0d | 0x0a | + +---+----+----------+---+------+------+---+---+---+---+---+---+...+...+--+------+---+---+------+------+ + + <--- VC_COMMAND_COMPLETE_MESSAGE + <--- VC_COMMAND_COMPLETE_MESSAGE + + START_OF_VC_SERIAL + +-----+-----+--------+-----+-----+ + | 2 | 4 | 0xe5 | B | 0 | + +-----+-----+--------+-----+-----+ + Length Dest Src VC_CMD_SUCCESS + +#### VC State Response + +As VC links change states, the host is notified of the state changes with a VC_STATE_MESSAGE sent to PC_LINK_STATUS (0xe1). An example where radio A detects the initial HEARTBEAT frame from radio B and notifies host A of the VC link change: -The VC number range from 32 to 63 is reserved for remote command execution. The VC number equals the target radio number (0 - 31) or-ed with PC_REMOTE_COMMAND. This VC number is placed in the destination VC field and the local radio VC number is placed in the source VC field. + Host A Radio A Radio B + + <--- HEARTBEAT + <--- VC_STATE_MESSAGE + + START_OF_VC_SERIAL + +-----+-----+--------+-----+----------+ + | 2 | 4 | 0xe1 | B | Status | + +-----+-----+--------+-----+----------+ + Length Dest Src VC_CMD_SUCCESS + +The status values are: + +* VC_STATE_LINK_DOWN (0): HEARTBEATs not received +* VC_STATE_LINK_ALIVE (1): Receiving HEARTBEATs, waiting for UNKNOWN_ACKS +* VC_STATE_SEND_UNKNOWN_ACKS (2): ATC command received, sending UNKNOWN_ACKS +* VC_STATE_WAIT_SYNC_ACKS (3): UNKNOWN_ACKS sent, waiting for SYNC_ACKS +* VC_STATE_WAIT_ZERO_ACKS (4): SYNC_ACKS sent, waiting for ZERO_ACKS +* VC_STATE_CONNECTED (5): ZERO_ACKS received, ACKs cleared, ready to send data + +#### Reconnect Serial Response + +The ATZ command causes the system to reboot. Prior to the reboot, the radio responds with a message sent to PC_SERIAL_RECONNECT (0xe4). The local radio takes a couple of seconds to reset and causes the USB serial device to go off-line. The timing is critical here, the application must close the serial connection before the LoRaSerial USB device goes off-line. If the host is still holding the USB serial connection open when the LoRaSerial USB serial port goes off-line then the host may not be able to release it in time before the LoRaSerial attempts to connect its USB serial port. In this case, the LoRaSerial serial port may show up as a new device on the host system and +further communications with the radio would no longer be possible using the previous device path. + +An example is below: + + START_OF_VC_SERIAL + +-----+-----+------+------+-----+-----+-----+------+------+ + | 2 | 8 | 0xe5 | 0xe0 | A | T | Z | 0x0d | 0x0a | + +-----+-----+------+------+-----+-----+-----+------+------+ + Length Dest Src Command + + Host A Radio A + Command Message ---> + <--- Command response (ASCII text) + + +---+---+------+------+ + | O | K | 0x0d | 0x0a | + +---+---+------+------+ + + <--- Reconnect message + + START_OF_VC_SERIAL + +-----+-----+--------+-----+ + | 2 | 3 | 0xe4 | A | + +-----+-----+--------+-----+ + Length Dest Src + + <--- VC_COMMAND_COMPLETE_MESSAGE + + START_OF_VC_SERIAL + +-----+-----+--------+-----+-----+ + | 2 | 4 | 0xe2 | A | 0 | + +-----+-----+--------+-----+-----+ + Length Dest Src VC_CMD_SUCCESS ## Example Program The [VcServerTest.c](https://github.com/sparkfun/SparkFun_LoRaSerial/blob/release_candidate/Firmware/Tools/VcServerTest.c) is an example C program that communicates with the VC server using the virtual circuit serial interface. The first parameter is the device path to the local LoRaSerial radio. The second parameter specifies the destination VC number to send commands or data. + +The example program has the following features: + +* --reset command line option +* --break command line option +* Gets the local radio's virtual circuit number +* Opens a virtual circuit to the target VC +* Passes entered serial data to the target VC +* Displays responses from the target VC + +### Debug Defines + +There are some useful defines that may be set to one (1) to display the host's interaction with the radio. These defines are: + +* DEBUG_PC_TO_RADIO +* DEBUG_RADIO_TO_PC +* DUMP_RADIO_TO_PC + +Setting these defines will display the radio communications in hexadecimal and ASCII. + +### --reset + +The reset command line option is not used very often, but sends an ATZ command to the local radio. + +### --break + +The break command line option sends an ATB command to the local radio causing it to delay for 5 times the heartbeatTimeout interval. This delay is sufficient to break all links with any remote virtual circuits. Following the delay, the radio returns to the RADIO_RESET state and brings up links to the other radios. From 225d0953c29c0e4b1f3e747b73f136080c310847 Mon Sep 17 00:00:00 2001 From: Lee Leahy Date: Wed, 8 Feb 2023 21:04:44 -1000 Subject: [PATCH 555/594] VcServerTest: Add DUMP_RADIO_TO_PC define --- Firmware/Tools/VcServerTest.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Firmware/Tools/VcServerTest.c b/Firmware/Tools/VcServerTest.c index 4a42b866..08cda76e 100644 --- a/Firmware/Tools/VcServerTest.c +++ b/Firmware/Tools/VcServerTest.c @@ -19,10 +19,11 @@ #define DISPLAY_COMMAND_COMPLETE 0 #define DISPLAY_DATA_ACK 0 #define DISPLAY_DATA_NACK 1 +#define DISPLAY_STATE_TRANSITION 0 #define DISPLAY_UNKNOWN_COMMANDS 0 #define DISPLAY_VC_STATE 0 +#define DUMP_RADIO_TO_PC 0 #define SEND_ATC_COMMAND 1 -#define DISPLAY_STATE_TRANSITION 0 typedef struct _VIRTUAL_CIRCUIT { @@ -446,7 +447,9 @@ int radioToHost() dataEnd += bytesRead; //Display the data received from the radio -// if (bytesRead) dumpBuffer(dataStart, dataEnd - dataStart); + if (DUMP_RADIO_TO_PC) + if (bytesRead) + dumpBuffer(dataStart, dataEnd - dataStart); //The data read is a mix of debug serial output and virtual circuit messages //Any data before the VC_SERIAL_MESSAGE_HEADER is considered debug serial output From 39658c1fed364684c7bbfbd4b9d9ef094aca868d Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 9 Feb 2023 08:31:31 -0700 Subject: [PATCH 556/594] Update compile-loraserial.yml --- .github/workflows/compile-loraserial.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/compile-loraserial.yml b/.github/workflows/compile-loraserial.yml index 8a398f14..f1321d55 100644 --- a/.github/workflows/compile-loraserial.yml +++ b/.github/workflows/compile-loraserial.yml @@ -48,7 +48,7 @@ jobs: #Original Crypto library: https://github.com/rweather/arduinolibs.git - name: Compile Sketch - run: arduino-cli compile --fqbn SparkFun:samd:LoRaSerial ./Firmware/LoRaSerial/LoRaSerial.ino --export-binaries + run: arduino-cli compile --fqbn SparkFun:samd:LoRaSerial ./Firmware/LoRaSerial/LoRaSerial.ino --export-binaries -v - name: Get current date id: date @@ -91,8 +91,8 @@ jobs: API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_LORASERIAL_FILE_TOKEN }} with: source_file: ./Firmware/LoRaSerial/build/SparkFun.samd.LoRaSerial/${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}.bin - destination_repo: 'sparkfun/SparkFun_LoRaSerial/Binaries' - destination_folder: '' + destination_repo: 'sparkfun/SparkFun_LoRaSerial' + destination_folder: '/Binaries/' user_email: 'nathan@sparkfun.com' user_name: 'nseidle' commit_message: 'Github Action - Updating Binary ${{ steps.dateNoScores.outputs.dateNoScores }}' @@ -103,8 +103,8 @@ jobs: API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_LORASERIAL_FILE_TOKEN }} with: source_file: ./Firmware/LoRaSerial/build/SparkFun.samd.LoRaSerial/${{ env.FILENAME_PREFIX }}${{ env.FILE_ENDING_UNDERSCORE }}_with_bootloader.bin - destination_repo: 'sparkfun/SparkFun_LoRaSerial/Binaries' - destination_folder: '' + destination_repo: 'sparkfun/SparkFun_LoRaSerial' + destination_folder: '/Binaries/' user_email: 'nathan@sparkfun.com' user_name: 'nseidle' commit_message: 'Github Action - Updating Binary ${{ steps.dateNoScores.outputs.dateNoScores }}' From 44f75c76233d5c610bdbad6dc10941e422915408 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 9 Feb 2023 11:03:30 -0700 Subject: [PATCH 557/594] Change to jlink programmer --- Binaries/Bootloader_Combined/batch_program.bat | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Binaries/Bootloader_Combined/batch_program.bat b/Binaries/Bootloader_Combined/batch_program.bat index f1b7b0a4..84a89de0 100644 --- a/Binaries/Bootloader_Combined/batch_program.bat +++ b/Binaries/Bootloader_Combined/batch_program.bat @@ -9,13 +9,13 @@ if [%1]==[] goto usage @echo Programming binary: %1 @echo Unlock bootloader -atprogram.exe -t atmelice -i swd -cl 20mhz -d atsamd21g18a write --fuses --offset 0x804000 --values 0xFFC7E0D8 +atprogram.exe -t samice -i swd -cl 20mhz -d atsamd21g18a write --fuses --offset 0x804000 --values 0xFFC7E0D8 @echo Programming firmware -atprogram.exe -t atmelice -i swd -cl 20mhz -d atsamd21g18a chiperase program -f %1 --verify +atprogram.exe -t samice -i swd -cl 20mhz -d atsamd21g18a chiperase program -f %1 --verify @echo Lock bootloader -atprogram.exe -t atmelice -i swd -cl 20mhz -d atsamd21g18a write --fuses --offset 0x804000 --values 0xFAC7E0D8 +atprogram.exe -t samice -i swd -cl 20mhz -d atsamd21g18a write --fuses --offset 0x804000 --values 0xFAC7E0D8 @echo Done programming! Ready for next board. @pause From 8f1c169cff18ef51b8c155ac3f420d64c26f9f50 Mon Sep 17 00:00:00 2001 From: Nathan Seidle Date: Thu, 9 Feb 2023 11:12:52 -0700 Subject: [PATCH 558/594] Clean up binary programming folder --- .../LoRaSerial_Firmware_v10_Combined.hex | 16388 ---------------- Binaries/Bootloader_Combined/readme.md | 30 - Binaries/LoRaSerial_Firmware_v10.bin | Bin 75504 -> 0 bytes ...aSerial_RC-Feb_09_2023_with_bootloader.bin | Bin 0 -> 131592 bytes .../atprogram.exe | Bin .../batch_program.bat | 0 .../read.bat | 0 ...aSerial_RC-Feb_09_2023_with_bootloader.bin | Bin 0 -> 131592 bytes Binaries/bossac.exe | Bin 11804064 -> 0 bytes Binaries/program.bat | 28 - 10 files changed, 16446 deletions(-) delete mode 100644 Binaries/Bootloader_Combined/LoRaSerial_Firmware_v10_Combined.hex delete mode 100644 Binaries/Bootloader_Combined/readme.md delete mode 100644 Binaries/LoRaSerial_Firmware_v10.bin create mode 100644 Binaries/Programming_Tools/SparkFun_LoRaSerial_RC-Feb_09_2023_with_bootloader.bin rename Binaries/{Bootloader_Combined => Programming_Tools}/atprogram.exe (100%) rename Binaries/{Bootloader_Combined => Programming_Tools}/batch_program.bat (100%) rename Binaries/{Bootloader_Combined => Programming_Tools}/read.bat (100%) create mode 100644 Binaries/SparkFun_LoRaSerial_RC-Feb_09_2023_with_bootloader.bin delete mode 100644 Binaries/bossac.exe delete mode 100644 Binaries/program.bat diff --git a/Binaries/Bootloader_Combined/LoRaSerial_Firmware_v10_Combined.hex b/Binaries/Bootloader_Combined/LoRaSerial_Firmware_v10_Combined.hex deleted file mode 100644 index 24df383c..00000000 --- a/Binaries/Bootloader_Combined/LoRaSerial_Firmware_v10_Combined.hex +++ /dev/null @@ -1,16388 +0,0 @@ -:10000000D82D002089020000850200008702000030 -:1000100000000000000000000000000000000000E0 -:100020000000000000000000000000008502000049 -:100030000000000000000000850200007D020000BA -:100040008502000085020000850200008502000094 -:100050008502000085020000850200008502000084 -:100060008502000085020000850200008502000074 -:100070008502000085020000850200008502000064 -:100080008502000085020000850200008502000054 -:100090008502000000000000000000008502000052 -:1000A0008502000085020000850200008502000034 -:1000B00000000000074B1A00197DC907FCD520215C -:1000C000FF31198304494008D8611980137DDB078B -:1000D000FCD570470040004102A5FFFF10B50400A9 -:1000E000044B9C4200D910BD20000134FFF7E2FF11 -:1000F000FF34F5E7FFFF0300002330B59A4200D13B -:1001000030BD9C000D5901330551F7E7F0B580254E -:10011000104B114E5C68114FAC435C60002A00D15B -:10012000F0BD1400102A00D91024121B1E801D7D62 -:10013000ED07FCD5A400A44600240D590551043454 -:100140006445FAD1001909191F801C7DE407FCD50C -:10015000E4E7C0460040004144A5FFFF04A5FFFFBF -:10016000802270B5002304000D005200E958E058C9 -:10017000814203D104339342F8D170BD2000FFF7D0 -:1001800099FF402229002000FFF7C0FFF5E700009B -:10019000024A53785BB2002BFBDB7047000C004037 -:1001A00010230249CA681A42FCD070470008004078 -:1001B0001E21F8B50226254A254C5368254D8B4350 -:1001C00033435360A660FFF7E3FF234B6360FFF701 -:1001D000DFFFAE84FFF7E4FF204B1B689B0E3F2B35 -:1001E00000D1203B002602271D4A9B0213431D4AD3 -:1001F000EA62AB62AE84FFF7D3FF1B4BAB84FFF721 -:10020000CFFFAB8C3B43AB84FFF7CAFF3F22638831 -:10021000934380221F4367806388D2011343638026 -:10022000A660FFF7B5FF114B6360FFF7B1FFC02178 -:100230000F4A104B10485360036A09061B021B0A41 -:100240000B430362072396601360F8BD0040004132 -:10025000000C0040000800400600010024608000FF -:10026000FF01000080BB0A1C2405000000070300FA -:1002700010E000E0E703000000ED00E010B500F042 -:10028000EDF810BDFEE7FEE71E4A1F4B70B51F4993 -:100290009A4232D100211E4B1E4A934230D3FF2294 -:1002A0001D4B032193431D4A0C2593600222082411 -:1002B00030261B4B1B485A62C3788B431343C370D1 -:1002C000C378AB432343C370174B987BB0430600FE -:1002D000202030439873987BA84304439C73987BF9 -:1002E000884302439A738023104A51680B4353603A -:1002F00000F07EFEFEE701CA01C38B42FBD3C9E7D3 -:1003000002C3CAE79C1C000000000020CC010020B2 -:10031000CC010020D40D00200000000000ED00E022 -:10032000FC70004100500041004800410040004185 -:10033000F0B5124E7388002B17D108241A0292B21E -:1003400015B2AC4667465500AAB2002F02DA0C4A35 -:100350005540AAB2013CA4B2002CF1D15C0032534A -:100360008022013352009342E7D10B0A43405B00E5 -:10037000F05A0902484080B2F0BDC046CC010020CE -:100380002110000001230002984200D87047C046A7 -:100390000133F9E770B505001C2400200F260B007F -:1003A000E3403340002A09D1002B07D1002C0BD0A9 -:1003B000002809D1043C231DF1D170BD092B03DDB8 -:1003C00037332B540130F5E73033FAE7054B064A53 -:1003D0001A60BFF34F8F054B054ADA60BFF34F8FAA -:1003E000FEE7C046FC7F0020EF6926F000ED00E04C -:1003F0000400FA0510B50B4A1368002B0ED10A4908 -:1004000011600A490A6801320A600949086800282F -:1004100006D0824204D30B60FFF7D8FF013B136084 -:1004200010BDC046D4030020DC050000000A0020F7 -:10043000040A0020054B064A1A60BFF34F8F054B94 -:10044000054ADA60BFF34F8FFEE7C046FC7F00200D -:10045000EF6916F000ED00E00400FA0510B5194A46 -:1004600019491368013313600A68002A0CD0174831 -:100470001018834203D180241548240584619342D7 -:1004800001D100230B6010BD1248DBB20278002BB3 -:1004900010D180210E4B0905996111000A39C9B2AA -:1004A0000D4BF02902D91978494219701B78D218DE -:1004B0000270E8E79342E6D18022054B12055A61AB -:1004C000E1E7C046CC030020D003002018FCFFFF6A -:1004D000004400410100002000000020074B084AB2 -:1004E0001B681168994207D2FA21C9005B18136092 -:1004F0008022044B12055A617047C046CC0300208D -:10050000D0030020004400418022024B12059A6073 -:100510009A6170470044004170470000202370B585 -:10052000354A0F20D1690B43D361012233490B783F -:1005300013430B70324B19788143197006211C78D4 -:10054000214319702F490C7822430A701A7802400F -:100550001A70602219780A431A702B4B2B4A5A8062 -:100560005A78D209FCD12A4B013219780A431A7001 -:100570009A78D207FCD41F2026490A68520B024001 -:10058000824200D105221C8D234D024092012C4055 -:1005900022431A850A68920C02401F2A00D1023AAF -:1005A0001F24188D2240A043024307201A850A68A1 -:1005B000D20D0240824200D10322198D0240174819 -:1005C000120301400A437F211A851A7814480A4011 -:1005D0001A70042219780A430C211A7058621A8979 -:1005E0008A431A811A890B398A431A818022002191 -:1005F000520001F03EFA70BD000400405844004132 -:100600003C44004159440041000C004006400000B9 -:1006100000500041246080003FF8FFFFFF8FFFFF84 -:100620004C0A0020F7B51C00314B06005B6A0F0036 -:1006300015000193002C05D1443454432D4BE4188C -:10064000FFF7D8FE63782278934210D2D51ABD42C4 -:1006500000D93D00002E08D0211DC9182A00300005 -:1006600001F0FEF963785B1963702800FEBD019B01 -:10067000A1786A01D318002911D1211D1960596888 -:100680001D48890B890359605968014059601B490D -:10069000551840212879014329710121A17014497D -:1006A00000255118019116495218127AD207DCD54B -:1006B0005D68A278EDB22570022A04D11968201D68 -:1006C0002A0001F0CDF9BD4200D93D00002E0CD02A -:1006D0006570211D2A00300001F0C2F90122019B42 -:1006E000FF331A720023A370BFE76670F6E7C046B7 -:1006F000005000414C0B0020FF3F00F00051004132 -:10070000FF50004110B50023FFF78CFF10BDF8B576 -:1007100005000C0016001F00002C00D1F8BD2100C0 -:1007200028003B003200FFF77DFF241A2D18F3E765 -:10073000F8B50E001F4955014C6A2C19002B1BD02F -:1007400063695B005B08636120616369B2049B0BB2 -:10075000920C9B03134363616369174A1748134064 -:1007600063612A18022380211500137291712A7A7D -:100770001A42FCD03000F8BD012163695B005B0FB9 -:1007800003339940B14208D8043A531E9A41636931 -:10079000D2075B005B081343D5E744275743084A59 -:1007A0000437BF1801003200380001F059F9380051 -:1007B000CAE7C04600500041FF3F00F0FF50004133 -:1007C0004C0B002010B50023FFF7B2FF10BD000056 -:1007D00010B5054B1B888B4200D90B000022190075 -:1007E000FFF7F0FF10BDC046FA03002007B5002157 -:1007F0006B460A00D81D0170FFF7E4FF07BD00003B -:10080000F0B59BB0FFF7F6FD0822C04CA38B1A4051 -:100810000392039BBE4A002B2ED00823A383802380 -:1008200011214020A372C024517090715371B94BB3 -:10083000B94D5968A4052940214359605969294097 -:100840000C43B6495C611960B549B64C19615968E9 -:100850000C4080218902214359605968890B890322 -:10086000596050710022B04B1A70AF4B1878431E7C -:100870009841C0B21BB0F0BD1021137A8C460B42D8 -:10088000F3D01172A949A94D8979A84B0191A749C3 -:100890000198CF799B883902014309B202916946D8 -:1008A0000989A2480091067AC07AAD7A00022843ED -:1008B0009E499F4D497A2880402509020E439D4854 -:1008C00036B2B1B25571834200D1B4E13AD8812039 -:1008D0004000834200D1C3E117D8822B00D191E1BF -:1008E00008D8802B00D188E1812B00D185E120231D -:1008F0009371BAE7802149008B42F8D00221FF3181 -:100900008B42F4D1FFF772FFAFE78B48834204D0EC -:100910000BD8A221FF318B42E9D10021082208A87F -:1009200001F0A7F8082108A8CDE0C020800083428C -:10093000DDD0E7D840388342D9D10729D7D1A331B8 -:100940007E48C0E07E498B42DCD000D99AE07D49E8 -:100950008B4214D801398B4200D3ADE07A498B42E7 -:10096000DBD06031FF318B42C1D1FFF73FFF8023E5 -:10097000029A5B421343654ADBB2937275E78821A2 -:10098000694809018B4200D1A2E180318B42AED18E -:10099000029B30270370FFF729FFA02303225B008F -:1009A000E254C0225B4B5C49586C920508401043EE -:1009B00058644620FF3025546448802618649020EF -:1009C00040002754586B0840104358639220400061 -:1009D00026545F4880261863B02040002554586F85 -:1009E00008405867B2204000265403263C30265465 -:1009F000FC3884469C44604606680E4016430660F8 -:100A0000A626FF36A5551E005248A0363060C026E7 -:100A10007600A7551F0094373E6880200E4016438D -:100A20003E60C2267600A0551E004B4F90363760C0 -:100A3000E02644277600A7550436A555A055C420C6 -:100A400084469C44604606680E4016430660D420E7 -:100A500084469C44604606680E4016430660F026B5 -:100A600080207600A7550436A555A0551C00E43417 -:100A70002068F433084010432060186801400A439E -:100A80001A60F2E635498B4200D13BE705D834497C -:100A90008B4200D136E7334932E733498B4200D1EC -:100AA00030E732498B4200D10FE131498B4200D00F -:100AB0001DE708212F4806E0802300995B0099423A -:100AC00004D112212C48FFF783FECEE68023009943 -:100AD0009B00994202D199212848F4E7032F7CD149 -:100AE000019B032B00D902E708AC4822002120001B -:100AF00000F0BFFF019B6770002B43D104332370CC -:100B00001F4B638021782000DDE7C0460050004184 -:100B1000FF5000414C0A0020FFFFFF8F0C0A00200D -:100B2000500B0020FF3F00F0D8030020080A0020EF -:100B3000FA03002002030000A1020000F0000020E0 -:100B40002109000081060000A1030000D80B00204D -:100B5000940B0020A40C0020600C00202120000039 -:100B6000210A0000210B000021220000A1FE00004C -:100B7000A1210000E8000020A01A00004000002091 -:100B80000904000004AE3200524B23CB23C20025DF -:100B90002F001B681360AB004F4AF358D019196837 -:100BA0000122FFF7F7FB01353F18042DF3D1002395 -:100BB000494AD355494B4A4A9B799B009D58280086 -:100BC00000F05FFF23000130400020702A780233DC -:100BD0000135002A96D01A70F8E70F2F02D139217B -:100BE000404870E7212F02D109213F486BE7222FAF -:100BF00000D07CE621213D4865E7002308A803805A -:100C0000022160E7039B08A803800823194200D053 -:100C10006DE6073319400831413349011E4205D0C2 -:100C2000334BC9188B799B06DB0FE8E7304BC918AB -:100C30008B79DB06F8E70F230B4000D157E608203D -:100C400001400098084300D051E60833284A5B0170 -:100C50009B18320602D520225A7153E66246FBE702 -:100C60000F230B4000D142E60820014000980843C2 -:100C700000D03CE65B01320613D51E491C4A9A1887 -:100C80005B182021D879084200D13BE659711B7AC4 -:100C90002B4200D136E613004022FF331A723E3A4F -:100CA000DAE760461349124A9A185B18D97901426B -:100CB00000D127E6202158711B7A0B4200D121E692 -:100CC0001300FF3319720122C6E7002308A803802E -:100CD0000121F8E6901A0000D9030020080A00203C -:100CE000F01A000004000020DC000020B41A00000C -:100CF00000500041FF50004110B5FFF70FFC0022EB -:100D0000034B1A700223034A11780B43137010BD72 -:100D1000D80300200050004110B50C000122FFF75D -:100D200051FD200010BD10B50C000122FFF74AFD57 -:100D3000200010BD70B504000D00FFF761FD030039 -:100D40000020834204D0022229002000FFF7DAFCB1 -:100D500070BD70B505000C00FFF752FD002807D0EC -:100D60000023022221002800FFF7D1FC200070BDE3 -:100D70000400FBE730B500232025934200DB30BDA3 -:100D80000C78002C03D00131C4540133F5E72C005A -:100D9000FAE70000F8B580220C000500920000215F -:100DA000200000F066FE002D0CD13E224B492000B1 -:100DB00000F056FEFF2355225B00E254484B921888 -:100DC000E254F8BD7E2D24D86B1E3E2B00D93F3B4C -:100DD000002B11D01B02581C424D434EFF301A1FEE -:100DE000591CAA4203D8B34211D08BB223800B0006 -:100DF00002348142F3D1E4E7F0222100FF20227087 -:100E00000A31621C9142E5D010700132FAE7374B8B -:100E1000ECE7822D31D87F2DD3D130490B222B31F5 -:100E20002000FFF7A7FF80272823314EE3727D3D86 -:100E30003F03F36820343800002B02D0180000F084 -:100E400020FE030A6377030C2077A377000E2B0A9A -:100E5000E077A576E3760B2231002000FFF78AFFCA -:100E600067224D2301355242ADB22274637422765B -:100E700063761036052DDCD1A3E72B00833B012BD5 -:100E80000CD81B4A1B01D318DD68280000F0F9FDBF -:100E900029000200200000F0E3FD92E7154B853D9C -:100EA0002902994200D98CE7134B144A2360144B52 -:100EB0006360FE235B00E2508023DB00A36180239C -:100EC000A06880229B0103432000A3600D4B5200C9 -:100ED0006561E1602261E3612030DCE7001B000016 -:100EE000FF010000FF03000003040000FFFF0000FB -:100EF000BC1B0000FFFF03005546320A306FB10AE9 -:100F000057515D9E882BED6870B516001D000A686C -:100F10002E4B0C009A4251D12D4B4A689A424DD12A -:100F2000FE2252002B4B8A589A4247D18B689A0472 -:100F300003D5294AC969914240D1DB0711D48023E6 -:100F400022695B009A420CD1E068C3B2002B08D141 -:100F5000224B234AC318934203D821002031FFF7C4 -:100F6000FFF8002D2BD0A369002B28D029681D4A3B -:100F70008B4206D0934201D8002901D001235B4265 -:100F80002B606369934219D80721012219408A40D6 -:100F9000DB08EB18197AD2B20A4204D168680A4316 -:100FA00001301A7268606A682B689A4206D3002E74 -:100FB00004D10D4B1B681E330C4A136070BD002E0C -:100FC000FCD1094B1B682D33FF33F5E75546320A38 -:100FD00057515D9E306FB10A882BED6800E0FFFF2E -:100FE000FFDF030063040000000A0020040A002061 -:100FF000F8B53A4DAB68002B00D0FEE7FFF784FA56 -:10100000374B1E68374BF218F8239B029A4213D8CD -:10101000354B364A1968364C364B914232D11978E5 -:10102000354AC90702D4216891422BD0334B226044 -:101030001B68334A32331360FFF7BAF8BFF35F8F90 -:1010400062B6FFF759FE4020FFF766FA80270A23B1 -:1010500001262C4DFF012B70FFF7D2FB2A4C0028F4 -:101060000AD02378DBB2002B05D1254A3800136063 -:10107000FFF752FA2E7026702378002B24D000F050 -:10108000BDFAFCE71B78DB07DB0F0DD000238022C5 -:1010900023601E4B12055A6180239B011A6882F35C -:1010A0000888AB603047C7E72168194A914201D1EF -:1010B0002360C1E72168104B9942E7D0FA202260F3 -:1010C0004000FFF75FF9E1E72378002BC4D1FF333D -:1010D000C046013B002BFBD1BEE7C04600ED00E05F -:1010E0000420000000E0FFFFB42000007CB0EE8789 -:1010F000FC7F002038040040EF6926F0000A002041 -:10110000040A002000000020FC03002000440041ED -:10111000EF6916F010B5054C12220021200000F0F6 -:10112000A8FCF0232370E63BE37110BD3B060020D2 -:10113000F8B505000E0017000024AB68A34200D8E4 -:10114000F8BD8021AA7833008900E868FFF7DFFA4C -:1011500080235B01E01801223B00E968FFF7D4FE21 -:101160000134EAE770B5130006000C0015000020FA -:101170000A000121FFF756FA0023984206D02B00FF -:1011800022001F213000FFF74DFA0123180070BD27 -:1011900010B504220D210248FFF714FB10BDC04614 -:1011A0009A01002010B50B480B49427903791202CD -:1011B0001A4383791B041A43C3791B0613431A0A83 -:1011C0000B714A711A0C1B0E8A71CB71FFF7E0FF8D -:1011D00010BDC0461C0600209A01002070B50422F4 -:1011E0000D00FFF7EFFA164C85420FD0FFF792FF84 -:1011F000012304222373134B9A700022DA701A71B0 -:101200005A719A711A735A73FFF7CCFFFFF782FF76 -:1012100000232373637A217A1B020B43A17A09040A -:101220000B43E17A09061943491B0B0A21726372C9 -:101230000B0C090EA372E172FFF7B4FF70BDC0463C -:101240009A0100203B06002070B5184C14220500BE -:101250000021200000F00DFC154A002D1FD02100B8 -:101260000823D07D08313F26527C32401C2A01D011 -:10127000B24205D10F4A0C330A800522DBB2CA7094 -:10128000191C834200D9011CC9B2002D0BD0023BAE -:101290009BB21B0223802000FFF7A0FF70BDD07C13 -:1012A000211D0423DFE7013B2370F4E70006002043 -:1012B0001C0600201C0A0000F0B5070000242B4B80 -:1012C00085B0597C03AAD170997C9170D97C5170FA -:1012D000197D1170997D02AAD170DB7D93705388BE -:1012E0000193019BA34208D8FFF714FF0022204B73 -:1012F0001A73FFF757FF05B0F0BDFFF781FA00281A -:10130000F9D0039B1B4EE518002F1FD03100280099 -:10131000FFF740FD8021042289003000FFF752FAD8 -:10132000134A0134517A137A09021943937A1B0440 -:101330001943D37A1B060B430F495B18190A137222 -:101340005172190C1B0E9172D372CAE780213B00B7 -:10135000300005228900FFF7DAF9084B3A00310026 -:101360002800FFF7D1FDFFF7B9F8D9E71C060020E8 -:101370009A0100200004002000FEFFFF500600201C -:10138000F0B585B000F03CFB454E00220521300051 -:10139000FFF7E8FE002858D0737A357A1B022B43FA -:1013A000B57A404C2D042B43F57A00272D061D43BA -:1013B0002B0A63722B0CA3722B0E2572E372F37B44 -:1013C0001E2B1BD021D8122B41D013D8BB4215D0D5 -:1013D000032B31D0FFF79EFE012305222373324BEE -:1013E0009A700022DA701A715A719A711A73203247 -:1013F0005A7307E01A2B39D01B2BEBD1FFF78AFE6B -:1014000000232373FFF7CEFE1FE0282B34D00BD828 -:10141000232B35D0252BDDD1802324489B0243602C -:10142000234B082103600EE02F2BE7D05A2B21D04D -:101430002A2BCFD1380020E0F37C191C122B00D9C5 -:1014400012211948C9B2FFF7C9FE05B0F0BD194C09 -:101450001949200018220830FFF78CFCF37C191C76 -:10146000242B00D92421C9B22000ECE73800FFF773 -:10147000EBFEEAE70120FAE70120FFF71DFFE4E7B2 -:101480000C260E49320001A800F0EAFAB54203D951 -:1014900026726772A772E7720C2101A8D3E7C046D3 -:1014A0001C0600209A0100203B06002014060020A4 -:1014B00000003E7FA8010020DE1A0000551C00003D -:1014C00072B6BFF35F8F044B044A9A829A8300225C -:1014D000034B9A607047C04600500041FF03000074 -:1014E00000ED00E0F0B5C7B00FAF0400FFF7E8FF74 -:1014F00044220021380000F0BCFA9822002120A8E4 -:1015000000F0B7FA02230F21BB70A37862780B407A -:10151000A37063680A400893002307AD264E6270EB -:1015200009932B73012220ABE16824480796FFF74B -:10153000EBFC20AA39002000FFF7FAFD62780123B6 -:101540000D212800FFF7F4F80196002607ADA178D9 -:101550003A002800FFF706FE002823D0019B03AEC7 -:1015600003936B687360AB68B36000233373EB7BEA -:10157000002B10D02A2B1CD1ED8A02AB6DBA5D80F6 -:10158000ADB2A56020AA39002000FFF7D1FDB368F5 -:101590006D025D1BB560627801230D213000FFF7FD -:1015A000C7F8D2E7064B9E4201D9FEF70FFF01367E -:1015B000CCE7FEF73FFFEEE755534253FF0F000025 -:1015C000F824010030B5EFF30883054C236003686D -:1015D00083F308884568A847236883F3088830BDEB -:1015E000C80D002007B5010001226846FEF7D2FEB3 -:1015F00008216846FFF790FB07BD0000F7B57A2386 -:101600000022804C804922600B7000920193FFF70A -:10161000B7FE7E4D40212800FFF78CFB7C4B186005 -:10162000009B2B54002801D0FEF758FF794B009AFD -:101630001D60794B1A607848754B02681F68BA4282 -:10164000E5D2744D29680B78FF2B36D0734E232BCF -:1016500000D0B3E06C4B1B78532B33D1336801328D -:10166000013102602960BA1A9A4200D91A006C4D01 -:1016700020682A6000F0F4F9674829680368654A21 -:10168000CB18013B036010684B1EC3181360634BFB -:10169000644D1B682970C9B28B4203D9591A20685E -:1016A000FFF757FBC046584B019A1A705B4B009AE4 -:1016B0001A60584A136801331360574A136801339C -:1016C0001360B8E7522B04D131682068FFF72BFB79 -:1016D000E9E74F2B03D1336822681370E3E7482B07 -:1016E00003D1336822681380DDE7572B0AD14E4BB4 -:1016F00022689A4202D14D48FEF70EFF23683268F5 -:101700001A60D0E76F2B04D101212068FFF704FB9A -:10171000C9E7682B05D1022123681B88336030009C -:10172000F4E7772B04D1236804211B683360F6E7C4 -:10173000472B09D13068FFF745FF3D4B1B78002B45 -:10174000B1D001213B48E1E7542BACD04E2BAAD0BD -:10175000562B02D12A213848D8E7582B05D13068BA -:10176000FEF7BCFC03213548D0E7592B0DD1326878 -:101770002068334B002A03D1186003213148C5E7A4 -:1017800092081968FEF7C2FCF7E75A2B8BD137682D -:1017900000262568EF19AF4209D101212A48FFF739 -:1017A000BBFA3000FFF71EFF03212848AEE7287878 -:1017B0003100FEF7BDFD01350600ECE71A00303AB6 -:1017C000D1B2092904D833681B011343336070E791 -:1017D0001A00413A052A03D83268373B1201F4E770 -:1017E0001A00613A052A03D83268573B1201ECE728 -:1017F0002C2B03D133682360009BE7E7024A137068 -:10180000FAE7C046C40D0020780D0020800D0020AE -:10181000700D0020740D00207C0D0020CC0D0020E8 -:10182000D00D00206C0D00200CED00E040004000C9 -:10183000E80600208B1C0000611C00008D1C0000CD -:10184000EC060020911C0000951C0000971C000075 -:10185000F8B50300473304001A7840210300002044 -:10186000FEF7E0FE002801D10020F8BD0023257915 -:1018700023706BB2002BF7DB3F272000A61DF38FF0 -:101880002F404830C018611D3A0000F0E9F83F23AE -:10189000F08F9D43C01980B2002D01D1F087E3E79E -:1018A0000023F387E1E7F0B506000C001F0093B0BA -:1018B00001923F25AC4203DC2500002F00D14037C8 -:1018C0003B0002AA2B43137002AB31002A00581CC4 -:1018D00000F0C6F86B4640211A7902A80123FEF7F2 -:1018E00027FF7619641BE4D113B0F0BD030010B5D7 -:1018F00047331A78043148300023FFF7D4FF10BD76 -:10190000F7B50400FFF7A4FF002841D023004833B7 -:101910009A88A06C1A80002201385A80082865D85D -:1019200000F094F815052327252A473E3700314E4D -:10193000300000F0A6F80500200031004C302A00ED -:1019400000F08EF829002000FFF7D0FF20E00123EF -:10195000E364FF3323658023DB006365A0235B0022 -:10196000A365254B1421E365EDE7FEF72FFD00216C -:10197000E9E7FEF75FFDFAE720000021FFF7B6FF79 -:10198000206D1E4B984203D921005431FEF7E8FB2D -:10199000F7BD2100626D5831206DFEF7ADFBE6E723 -:1019A0002000656D216D4C302A00FEF7A5FBA900D3 -:1019B000C9E70027236D666D5D1CFF35BE4201DC63 -:1019C0007100C0E700216B1EFF3B5A1C1878019282 -:1019D000FEF7AEFC019B0100AB42F6D123007A007A -:1019E0004C33013598520137FF35E7E701225A8021 -:1019F000BDE7C046EC1B0000882BED68FF1F000010 -:101A000007480622030010B547331A70FFF778FF26 -:101A100004480722030047331A70FFF771FF10BD17 -:101A2000F00600207808002010B5E2B00400FFF7AF -:101A300047FDC42200215200684600F01AF84723EF -:101A40006B441C706846FFF75BFFFBE702B471460E -:101A500049084900095C49008E4402BC7047C046F1 -:101A6000002310B59A4200D110BDCC5CC4540133A0 -:101A7000F8E703001218934200D170471970013340 -:101A8000F9E70023C25C0133002AFBD1581E7047DE -:101A90000CA0800040A0800044A0800048A08000EE -:101AA00012011002EF0201404F1B2A000142010205 -:101AB000030100000697FF0901A101150026FF00A0 -:101AC000750895400901810295400901910295012F -:101AD0000901B102C0537061726B46756E005341CB -:101AE0004D443231204C6F526153657269616C0014 -:101AF00000000000D51A0000DE1A0000D903002003 -:101B0000EB3C905546322055463220000201010040 -:101B10000240007E3EF03F00010001000000000096 -:101B20000000000000002942004200535041524B87 -:101B300046554E00000046415431362020203C21BD -:101B4000646F63747970652068746D6C3E0A3C68DC -:101B5000746D6C3E3C626F64793E3C736372697075 -:101B6000743E0A6C6F636174696F6E2E7265706C7F -:101B7000616365282268747470733A2F2F777777C2 -:101B80002E737061726B66756E2E636F6D2F70723F -:101B90006F64756374732F313933313122293B0AF5 -:101BA0003C2F7363726970743E3C2F626F64793EA0 -:101BB0003C2F68746D6C3E0A00000000494E464F91 -:101BC0005F55463254585400EC1B0000494E4445C2 -:101BD0005820202048544D003E1B000043555252CF -:101BE000454E542055463200000000005546322034 -:101BF000426F6F746C6F616465722076332E342E81 -:101C0000302D37302D6762333933636231205346CC -:101C10004857524F0D0A4D6F64656C3A2053414D41 -:101C2000443231204C6F526153657269616C0D0A08 -:101C3000426F6172642D49443A2053414D44323120 -:101C4000473138412D4C6F526153657269616C2D7B -:101C500076310D0A000000000800003E80020002FC -:101C60000076312E31205B41726475696E6F3A588F -:101C7000595A5D20466562202031203230323220B0 -:101C800031353A31333A32350A0D000600580A0D23 -:101C900000590A0D005A00230A0D000001C8000077 -:101CA000050F3900021810050038B60834A909A03C -:101CB000478BFDA0768815B665000101001C100554 -:101CC00000DF60DDD88945C74C9CD2659D9E648A43 -:101CD0009F00000306AA000200000000090299000C -:101CE00005010080FA080B0002020201000904004D -:101CF000000102020100052400100104240206056F -:101D0000240600010524010301070583030800FFE1 -:101D100009040100020A00000007058102400000DA -:101D200007050202400000090402000208065000F4 -:101D30000705840240000007050502400000090471 -:101D4000030002030000000921000100012221001C -:101D5000070586034000010705060340000109044A -:101D6000040002FF2A010007058703400001070560 -:101D70000703400001000000090403000203000003 -:101D80000300000000C20100000008000A0000007B -:101D900000000306AA00080002000400A0001400CE -:101DA000030057494E555342000000000000000058 -:101DB00000008400040007002A004400650076004B -:101DC00069006300650049006E00740065007200E0 -:101DD000660061006300650047005500490044004B -:101DE0007300000050007B003900320043004500C2 -:101DF00036003400360032002D0039004300370031 -:101E000037002D0034003600460045002D00390013 -:101E10003300330042002D00330031004300420004 -:101E200039004300350042004200330042003900CF -:101E30007D000000000055534253000000000000E8 -:101E400000000000008002022000000000000000EE -:101E50000000000000000000000000000000000082 -:101E600000000000312E3030FFFFFFFFFFFFFFFFBB -:101E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:101E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:101E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:101EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:101EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:101EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:101ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:101EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:101EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:101F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:101F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:101F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:101F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:101F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:101F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:101F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:101F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:101F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:101F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:101FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:101FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:101FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:101FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:101FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:101FF00000000000291A0000E5140000EC1B00009E -:1020000000800020A19E0000899E0000899E0000A3 -:1020100000000000000000000000000000000000C0 -:10202000000000000000000000000000899E000089 -:102030000000000000000000899E0000F59E0000E6 -:10204000899E0000899E00002D870000899E000067 -:102050004D9E0000899E0000899E00008D9E00001C -:10206000899E00001D880000899E0000899E000056 -:10207000899E0000899E00002D880000899E000036 -:10208000899E0000899E0000899E0000899E0000B4 -:10209000D5AA0000899E0000899E0000899E00004C -:1020A000899E0000899E0000899E0000899E000094 -:1020B0000000000010B5064C2378002B07D1054B1B -:1020C000002B02D0044800E000BF0123237010BDA4 -:1020D0004003002000000000B0430100044B10B595 -:1020E000002B03D00349044800E000BF10BDC046E8 -:1020F0000000000044030020B04301000122014B16 -:102100001A7070477A3200200122014B1A70704712 -:10211000A800002030B5094D094CAB685B035B0F8C -:102120009B001C590460AB6801619BB24360634330 -:10213000A4008360C460426130BDC04600400041DD -:1021400000360100F7B5802644690191E51C1A4C60 -:10215000AD0867683E436660002D2BD0174E268081 -:10216000267DF107FCD52E000768A91BBF088F420A -:1021700019D9002E17D0FF2117788B433B43104904 -:1021800057780B403F023B439778013E39040D4F8F -:102190001F40D378BFB20F431B0601993B4308C1D0 -:1021A00001910432E0E7084D2580257DE907FCD543 -:1021B0003500D1E7F7BDC0460040004144A5FFFF10 -:1021C000FF00FFFFFFFF00FF04A5FFFF130010B596 -:1021D000426918000DF07BF810BD0000044B054A61 -:1021E0004908D9611A801A7DD207FCD57047C046CC -:1021F0000040004102A5FFFF70B504000D0016006D -:10220000E36829002000B34205D2FFF7E7FFE36847 -:10221000ED18F61AF4E7FFF7E1FF70BDF8B5812578 -:102220000400071D0A483A000169ED00FFF7CEFFE0 -:10223000084E6359B34206D08022FF21D2003800F5 -:102240000DF061F866510123034AA3542370F8BDD1 -:10225000680D0020EDDEEDFE04040000F8B510234B -:102260007E4A0224137005227D4B26201A707D4B76 -:1022700002321A707C4B03321A707C4B073A1C7086 -:102280007B4B7C4F1A707C4B03321A701E237B4AA7 -:102290007B4D13707B4B7C4A18701F2313700C3BD3 -:1022A0007A4A3B70023B2B70013313700822784B43 -:1022B00001211A700923774A1370774A053B13707E -:1022C000764E0A33337008F037F86E4B01211878D8 -:1022D00008F07EF8694B2100187808F02DF828786E -:1022E000012108F029F82878002108F071F8674BDF -:1022F0000121187808F020F8644B0021187808F0C4 -:1023000067F8634B0121187808F016F8604B00213C -:10231000187808F05DF85F4B0121187808F00CF888 -:102320005C4B0021187808F053F8574B01211878BE -:1023300008F002F8544B0021187808F049F8387872 -:10234000012107F0F9FF3878002108F041F8524BDD -:102350002100187807F0F0FF3078012107F0ECFF3A -:102360003078012108F034F8082500243B4B013D6A -:10237000187807F07BFF2418002DF7D10723E0170A -:1023800018400019C0100CF0B5FD454A454B0BF044 -:10239000A5FF0022444B0BF0A1FF0CF069FE0CF0EE -:1023A00015FE424A424B06000F000AF0B1F8414CBC -:1023B000002808D03900404A404B30000AF094F819 -:1023C0003F49002800D13F4920000DF03DFD032585 -:1023D00001242B4F2100387807F0FAFF2B4B210006 -:1023E000187807F0F5FF2A4B2100187807F0F0FF66 -:1023F000284B2100187807F0EBFF234B21001878B9 -:1024000007F0E6FF1B4E2100307807F0E1FF322095 -:1024100007F0B4FD3878002107F0DAFF1B4B0021EC -:10242000187807F0D5FF1A4B0021187807F0D0FF75 -:10243000184B0021187807F0CBFF134B00211878B8 -:1024400007F0C6FF3078002107F0C2FF013D3220BF -:1024500007F094FD002DBCD1F8BDC0464C00002013 -:102460004D0000204F000020500000205C000020A4 -:102470005800002057000020550000204E0000208A -:1024800051000020560000205B0000205200002078 -:102490005300002054000020590000205A00002062 -:1024A0006666666666660A400000503F04560E2D5A -:1024B000B29DF73FF610002077BE9F1A2FDDFC3F3C -:1024C0005D2B01006E2B010010B5094C2378FF2B0A -:1024D0000DD0182009F0F6FD23780370192343600E -:1024E000183B03724372034B186006F03FF910BDAE -:1024F00059000020643200208A2110B5C900034829 -:1025000006F08EF87D22024B1A8010BDE00F0020ED -:10251000F410002010B507F0FBFC074C2368C01A2C -:10252000064B1B88984205D907F0F2FC206004484E -:1025300006F0EEF810BDC046D40F0020F4100020C5 -:10254000E00F002070B5050007F0E2FC040007F082 -:10255000DFFC001BA84205D2012007F00FFDFFF7AA -:10256000D9FFF4E770BD02780023412A04D14378F3 -:10257000543B58424341DBB21800704702780023B5 -:10258000522A04D14378543B58424341DBB21800ED -:102590007047000010B5084C20780CF04BFE002866 -:1025A00008D0611C20000DF04FFC044A1378013B59 -:1025B0001370F0E710BDC0467C070020E007002044 -:1025C000D0B50D4C2300383318780CF0CDFC0B003F -:1025D00080210200C905002006F058FA0600606B51 -:1025E0000F000CF0F3FC02000B00300039000BF080 -:1025F0006BFA0CF03DFDD0BD60000020F0B585B059 -:102600000400FFF7DDFF2E4D01902B003B331878BF -:102610000CF070FC00222B4B0AF03AFF06000198E8 -:102620000F000CF0D3FC02000B00300039000BF05F -:1026300055FE0CF01DFD2B0038331E786400071C7E -:10264000A01B0B3080000CF055FC02900391300071 -:102650000CF050FC00221C4B0BF040FE02000B0063 -:10266000029803990BF030FA0CF002FD06F0C4F961 -:102670002B003933041C18780AF080FE211C0AF064 -:1026800067FB0021041C09F069FF002800D000242A -:102690008221201CC90509F0E9FF09F0B1FF80B2D1 -:1026A0000AF06CFE01990AF053FB391C09F0DEFFB9 -:1026B00006F0A2F909F0A4FF80B205B0F0BDC04653 -:1026C00060000020000011400000104010B5074CD1 -:1026D0003C342078FFF792FF0100FA20800009F0D7 -:1026E00001FE2478C0B2604310BDC04660000020E7 -:1026F000094A10B51088094CC11083104B40434063 -:1027000041114B40012140101940DB03184380B2B6 -:102710002170108010BDC046DE0F0020DC0F0020AD -:1027200070B505000E000024731E9C420FDAFFF7FF -:10273000DFFF310009F0BCFEA30089006918EB1827 -:102740000A68186801340860E4B21A60ECE770BDEA -:102750000220034B1B889842804140427047C0468C -:10276000F010002070B5164C200005F0ABF80022E8 -:10277000144B1A70144B45331A78144B002A17D097 -:10278000134A1D681278022311002000994304297E -:1027900001D0082A0ED105F08BF80E4B8000196885 -:1027A00040590AF007FC011C200003F087F970BDB6 -:1027B00020001D6805F07CF880004159F4E7C04610 -:1027C00010110020A80000206000002078070020E1 -:1027D00058120020840D002010B5034804F0DBF9E6 -:1027E0000123184010BDC046101100200E38C3B29E -:1027F0000320102B01D8014AD05C70474C2B0100FC -:10280000064B1888064B1B88984202D3C01A80B228 -:102810007047802252018018F8E7C0465C12002001 -:102820005E1200200020084B1B78834207D1074B23 -:102830001888074B1B88984202D3C01A80B2704791 -:10284000802252018018F8E7C00E00207C32002060 -:102850007E320020064B1888064B1B88984202D314 -:10286000C01A80B27047AF2292008018F8E7C046C5 -:102870009E0A0020A00A0020064B1888064B1B88E1 -:10288000984202D3C01A80B27047AF2292008018DB -:10289000F8E7C0465E0D0020600D002010B5094B22 -:1028A00001201A78FF2A0BD0074B44331B78002BEA -:1028B00006D0100007F0B8FD013843425841C0B2BD -:1028C00010BDC0464E00002060000020F8B5FFF7A4 -:1028D00097FF144A13003C3319780B1C814200D92E -:1028E000031C3832127899B2062A03D1FC2900D988 -:1028F000FC2399B2002217000B4B0C48C9B2197087 -:102900000B4D03880B4ED4B2A14207D9F75C0133BB -:102910001B05AF541B0D01320127F4E7002F00D037 -:102920000380F8BD60000020EB1000205E12002044 -:10293000E80F00206112002010B5FFF761FF0B4C7B -:1029400023003C331B78834203D8FFF7BFFF0120ED -:1029500010BD002801D10020FAE707F0D9FA044B96 -:102960001B68C01AE38F9842F5D3EEE760000020A1 -:10297000C40E0020F8B5FFF77FFF164A13003C3362 -:1029800019780B1C814200D9031C383212789CB292 -:10299000062A03D1FC2C00D9FC239CB200250E4B47 -:1029A000E4B21C702B000D4E0D4F3088EAB29442F9 -:1029B0000CD9AF213B5C0B4A0130535580B28900E2 -:1029C00009F08CFC013588B20123EFE7002B00D021 -:1029D0003080F8BD60000020EB100020600D00206A -:1029E000A20A0020E80F002003220020034B1B78DE -:1029F000043BDBB29A424041C0B2704758120020FB -:102A0000F8B5134B05001B78002B08D101001148C5 -:102A100007F0ABFD2900104807F0A7FDF8BD012B1A -:102A2000FCD100240D4E0E4F28000DF015FA844203 -:102A3000F4D2AF2133888900581C80B230802A5DDF -:102A40000134FA5409F04AFCA4B23180ECE7C046E4 -:102A50000F110020843800207C3500205E0D0020FE -:102A6000A20A002010B5FFF7CBFF0248FFF7C8FF0E -:102A700010BDC046E92D010010B50248FFF7F2FF76 -:102A800010BDC046E62D010010B50248FFF7EAFF71 -:102A900010BDC0463C2D0100F8B5FFF7A5FF061E8E -:102AA00015D00E4B0E4D1F7800231C002888A7421E -:102AB00010DDAF210B4B0C4AE35C8900135401304D -:102AC00080B209F00BFC013488B23300EFE7FFF766 -:102AD000DBFFF8BD002BFCD02880FAE7E0070020E0 -:102AE0005E0D00207C070020A20A00207FB50200B6 -:102AF000044901A80DF07EF901A8FFF781FF07B096 -:102B000000BDC0464C2D010010B5FFF7EFFF024895 -:102B1000FFF776FF10BDC046E92D01007FB502002A -:102B2000102908D1064901A80DF064F901A8FFF7A2 -:102B300067FF07B000BD0A29F8D10249F3E7C04694 -:102B4000472D01004C2D010010B588B00C000CF091 -:102B50003DFA220000900191044903A80DF04AF9C2 -:102B600003A8FFF74DFF08B010BDC046422D01007D -:102B700010B5FFF7E9FF0248FFF742FF10BDC0465E -:102B8000E92D0100F0B5A74C85B0E368002B49D0D2 -:102B900096221201934200D133E11ED8962B00D128 -:102BA00029E10DD8282B39D0230040331B78002B86 -:102BB00038D09D48FFF724FFE068FFF7A5FF31E01C -:102BC000C8225200934200D119E19622D2009342CA -:102BD000EAD1964B6363964B23E09622D20193424F -:102BE00000D112E10DD896225201934200D10AE1A0 -:102BF000962292019342D7D18E4B6363E123DB008F -:102C00000FE0E122D201934200D102E196221202AA -:102C10009342C9D1874B6363874B02E0874B636361 -:102C2000874B2387874F884D3B682800196802F03F -:102C300045FF0190A08C0026C0B2FFF7D7FD00220F -:102C400041B2280003F05EF90D3004D0019E0C362D -:102C5000731E9E41F6B2616B280003F043FB0830FF -:102C600000D10026230038331978280003F0D0FC67 -:102C7000093000D10026230039331978280003F0E9 -:102C80002FFD0A3000D1002623003A33197828009E -:102C900004F0BFF843425841230040423B33064012 -:102CA0001978280004F0D3F8123000D10026230050 -:102CB00038331B78062B00D0AFE0FF21280003F04B -:102CC0005AFD002800D000266049280003F096FD38 -:102CD0005F49280003F09BFD5E4B1978FF2904D063 -:102CE0005D4B28001A7803F0A1FD638E0293FFF775 -:102CF00067FC23000022039031331B78019293423A -:102D00000ED002980AF03AFB039909F049FE09F047 -:102D100077FC80B2031CFF2800D9FF239BB20193EC -:102D20006B462800197904F0B3FD434258414042F4 -:102D300006400220FFF762FCA179050009F0D2FAF3 -:102D4000464B2D181D80230040331B78002B45D0A7 -:102D50004348FFF755FE3B6803211868FFF708FF5B -:102D60004048FFF74DFE606B0AF0E8FAFFF7CCFE33 -:102D70003D48FFF745FE230038331878FFF7C4FEBF -:102D80003A48FFF73DFE230039331878FFF7BCFEC1 -:102D90003748FFF735FE23003A331878FFF7B4FEC3 -:102DA0003448FFF72DFE23003B331878FFF7ACFEC5 -:102DB0003148FFF725FEFFF703FC0AF0BFFAFFF7E3 -:102DC000A3FE2E48FFF71CFE0198FFF79DFE2C483E -:102DD000FFF716FE214B1888FFF796FE002E08D14C -:102DE0004034FFF751FE2378002B02D02548FFF72F -:102DF00039FE05B0F0BD114B6363234B11E70B4B5C -:102E0000FAE70C4BF8E70B4BE4E60A4B63631F4B06 -:102E100007E7084B63631E4B03E7280003F0B1FC90 -:102E20004FE7C04660000020EC2D01000000FA4290 -:102E3000090800000000FA430605000000007A427D -:102E40000B0800007807002010110020FD20000072 -:102E500009210000580000205C000020640D0020C3 -:102E6000FF2D0100062E0100172E01002B2E010060 -:102E70003D2E01004D2E0100632E0100742E010035 -:102E8000842E01009B2E01000A08000007070000A5 -:102E90000606000010B50248FFF7B2FD10BDC0469F -:102EA000E92D010070B500265E4BF5B21B785E4837 -:102EB000012B00D05D48FFF7A3FD5D48FFF7A0FDA3 -:102EC0003000FFF713FE013D5A48FFF799FD5A48BD -:102ED000192D10D8280009F05DF90D26282A2C2E6E -:102EE00030323436383A3C3E40424446484A4C4EF2 -:102EF000505254565148FFF783FD5148FFF780FD6B -:102F0000504C192D41D8280009F044F94345474950 -:102F10004B5B5E6065686B6D6F727476787A817CEE -:102F20007E80838587894848E5E74848E3E7484845 -:102F3000E1E74848DFE74848DDE74848DBE748483D -:102F4000D9E74848D7E74848D5E74848D3E748484D -:102F5000D1E74848CFE74848CDE74848CBE748485D -:102F6000C9E74848C7E74848C5E74848C3E748486D -:102F7000C1E74848BFE74848BDE74848BBE748487D -:102F8000B9E74848B7E74848B5E7A068FFF7AEFD9E -:102F900010E0E068FAE7207CF8E7607CF6E7A07CC8 -:102FA000F4E700256319D87C10210135FFF7B6FD41 -:102FB000102DF7D10136FFF76DFF1B2E00D073E700 -:102FC00070BD0434E07FE1E7A08CDFE70321A06A55 -:102FD000FFF7BAFDEEE70321E06AF9E73034207825 -:102FE000D4E73134FBE7608ED0E70221606BEFE776 -:102FF0003834F4E73934F2E73A34F0E73B34EEE7BB -:103000003C34ECE74034EAE74134E8E70434E08F4D -:10301000BCE74434E3E74534E1E74634DFE74734CF -:10302000DDE7C0460F110020402D0100042F0100F4 -:10303000062F0100F42E0100092F0100152F0100B9 -:1030400049300100600000201E2F0100242F0100E4 -:10305000312F01003D2F01004B2F01005A2F01009D -:10306000622F01006F2F01007C2F01008D2F0100C6 -:103070009A2F0100A72F0100B12F0100BE2F0100E0 -:10308000C92F0100D22F0100E12F0100EB2F010019 -:10309000F82F0100FE2F0100033001001430010061 -:1030A00020300100293001003E300100F0B5574EBC -:1030B00085B03068002801D00CF0DEF8544C2300B5 -:1030C00030331D78A8000CF0CDF8E36A306003932C -:1030D000A36A0090191C0398019309F06BFF041C6C -:1030E000A81C0AF04BF9011C201C09F059FCFC211A -:1030F0008905029009F02CFE019909F0B7FA002425 -:10310000061CA5420DDD20000AF038F9029909F0ED -:103110001FFE311C09F0AAFA009BA700D851013408 -:10312000EFE73B4C267CE368608E9E193018667C86 -:10313000231DA77C3618D87FF6198619019809F047 -:103140005FFA8619039809F05BFA23003133861978 -:10315000187876198619606B09F052FA2300383313 -:1031600080191E782B4A301880B21080002F08D0AA -:103170000023E118CE7C0133801980B2102BF8D1E6 -:10318000108029000098FFF7CBFA0025FFF7B0FA6E -:10319000214EA85501350C2DF8D1230040331B7862 -:1031A000002B30D01D48FFF72BFC03210298FFF7BE -:1031B000DFFC1B48FFF756FC0025230030331B784B -:1031C000AB4210DD2800FFF791FC1648FFF718FC12 -:1031D0000E4BAA001B680321D058FFF7B5FC013540 -:1031E000FFF758FEE9E71048FFF70AFC00240F48F4 -:1031F000FFF706FC305D10210134FFF78FFC0C2C2B -:10320000F5D1FFF747FE05B0F0BDC04678070020B6 -:1032100060000020DE0F00205C030020BE2E0100B5 -:10322000CF2E01002C2D0100DE2E0100E62E010024 -:1032300010B5244B244C403320701B78002B10D049 -:103240000E2837D808F0A6FF0810120E14242622E4 -:10325000282C2A322E3034001C48FFF7D1FBFFF710 -:1032600019FE10BD1A48F8E71A48F6E71A48F4E7BD -:103270001A48FFF7C5FB1A4B1A481C6804F018FBE4 -:10328000800003210059FFF75FFCE8E71648F0E7EC -:103290001648EEE71648ECE71648EAE71648E8E76E -:1032A0001648E6E71648D8E71648D6E71648D4E7A2 -:1032B0001648D2E71648FFF7A3FB2078FFF716FC65 -:1032C000CDE7C0466000002058120020782B010096 -:1032D0009B2B0100BC2B0100DA2B0100F42B010019 -:1032E00078070020101100200E2C0100262C010070 -:1032F0003B2C01004C2C0100682C0100822C0100A9 -:10330000992C0100AE2C0100C92C0100E92C010010 -:10331000052D010070B50C4C050020000E0004F0D6 -:1033200081FE0A4902001331200004F085FE0C22C0 -:103330000749200004F04AFF33002A00290020003A -:1033400004F090FE70BDC046880D00206000002093 -:103350005C03002070B50C4C050020000E0004F04A -:1033600061FE0A4902001331200004F065FE0C22C0 -:103370000749200004F02AFF33002A00290020001A -:1033800004F0CCFE70BDC046880D00206000002017 -:103390005C0300200122F0B5FF2300241700A5B232 -:1033A000A94213D9055D94465D400555082566463A -:1033B0005A115A40F6015B10013D3A403343EDB2D9 -:1033C000DBB29446002DF2D10134E8E7F0BD0000F5 -:1033D00070B50D4C05002078FF2814D00B4B4033FE -:1033E0001B78002B0FD0002106F0F2FF002D06D035 -:1033F00030200321684308F0EBFE0138FDD120782E -:10340000012106F0E5FF70BD5A0000206000002099 -:10341000F8B5254B254E1D6833004533244C1B78E9 -:103420002000002B19D004F043FA224B80001968C9 -:10343000405909F0BFFD011C0025200002F03EFBB1 -:103440001D4B1D70330038331B78062B0AD905221B -:103450002900200003F0F4FFF8BD04F029FA8000F1 -:103460004159E9E7154F3B78002B10D0737C002BB6 -:103470000DD00221200003F07EF905220221200058 -:1034800003F0DEFF4B20FFF7A3FF3D70E4E7FF21D1 -:10349000200003F070F90522FF21200003F0D0FF87 -:1034A0006420FFF795FFD7E7780700206000002031 -:1034B00010110020840D0020A8000020810D0020A4 -:1034C00070B50D000DF087FBC4B228000DF083FB32 -:1034D0002300303BDBB2C2B2092B06D92300413BAB -:1034E0000020052B10D8373CE3B210003038C0B2B2 -:1034F000092806D9110041390020052904D8373A96 -:10350000D0B21B011843C0B270BD000010B504005A -:10351000124B0121187823000B401DD006F058FFF4 -:1035200002210F4B21401878002900D0012106F01C -:103530004FFF04210B4B21401878002900D00121B6 -:1035400006F046FF0821084B21401878002900D0DA -:10355000012106F03DFF10BD1900DFE751000020FA -:1035600052000020530000205400002010B500211C -:103570000E4802F051FE09F0E1FE0300040095330D -:1035800013DB0120FFF7C2FF230045330DDB0320CF -:10359000FFF7BCFF2300313307DB0720FFF7B6FF3F -:1035A000133402DB0F20FFF7B1FF10BD1011002014 -:1035B00010B5054B01001878FF2804D0002900D071 -:1035C000012106F005FF10BD5B00002010B5054B82 -:1035D00001001878FF2804D0002900D0012106F04E -:1035E000F7FE10BD5700002070B504000D004269C1 -:1035F0000169FEF701FE2A0021692000FEF7A2FD05 -:1036000070BD000010B503780400002B01D1FEF757 -:1036100005FE074BE25C002A08D00022E254054B6D -:103620000132E254211D0448FFF7DEFF10BDC04601 -:103630000C04000004040000680D0020482370B54D -:10364000124D134C2B802378002B02D12000FEF763 -:10365000E5FD0023E95CE21801331171482BF9D133 -:103660000C4BE25C0C4B002A0CD00022E2540B4BBA -:1036700001320B490B48E254FFF7B6FF2000FFF779 -:10368000C1FF70BD0122E254F8E7C046600000208F -:10369000680300200D0400000C0400000404000076 -:1036A0006C030020680D002070B5424C05004822D4 -:1036B000414920000BF00BFE002D00D16EE03F4B86 -:1036C0001B78112B25D100233D4AD05CE118013332 -:1036D000C874102BF9D1137C2374230040331B785A -:1036E000002B16D03748FFF78BF900256319D87CDB -:1036F0001021FFF713FA01353348FFF781F9102D38 -:10370000F4D1FFF7C7FB3148FFF77AF9207CFFF7C8 -:10371000FBF9FFF793FFFFF7C9FCFFF733FAFFF759 -:1037200077FE0020FFF7F2FE6420FEF70BFF092072 -:10373000FFF7ECFEFA204000FEF704FF0620FFF73B -:10374000E5FEFA204000FEF7FDFE0F20FFF7DEFE4B -:10375000FA204000FEF7F6FE0020FFF7D7FEFA2021 -:103760004000FEF7EFFE0F20FFF7D0FE1848FEF7EF -:10377000E9FE0020FFF7CAFEFA20C000FEF7E2FED5 -:10378000637C0020002B00D10820FFF751FD0022B0 -:10379000104B11481A70FFF765F970BD0F496A5C4C -:1037A00063190135DA74102DF9D10D4B1B78237490 -:1037B000AFE7C0466000002004000020D00F0020CA -:1037C000CC0E00201C2D0100A0430100262D01007D -:1037D000DC050000601200202F2D0100683200205F -:1037E0007832002070B5037804000D001600002B1D -:1037F00001D1FEF713FDABB2E3181B790135ADB271 -:103800003370641923793000737070BD002373B571 -:10381000204C01932378002B02D12000FEF7FEFC00 -:10382000227901AB1A7062795A70A2799A70E279A2 -:10383000DA70019B013301D1FFF700FF00256B46D1 -:103840006A46290020001D80FFF7CCFF6B461B88CD -:10385000482B01D0FFF7F2FE6B469E1C320002217E -:1038600020005D80FFF7BEFF3388102B01D0FFF7EB -:10387000E5FE2378002B02D12000FEF7CFFC0023C9 -:10388000054AE118097999540133482BF9D1FFF71A -:10389000D5FE73BD6803002060000020812270B552 -:1038A000D20011491148FEF735FC00210120104BD0 -:1038B000104A19709954104A9854104802F0F4F8BC -:1038C0000F4C200004F086FD0E4D0F4B28002360A6 -:1038D00004F04CF965600D4A0D4920000BF06AFCBC -:1038E0000C4804F09BFE70BD00310100680D002003 -:1038F000680300200C0400000D04000010110020DB -:10390000880D0020000E0020D02A010000000020B9 -:10391000E9550000E00F002070B58420DAB008F00F -:10392000D1FB040059485A4B00785A4A5A4908AD0D -:103930001B78127809780090200001F0C3FE210066 -:10394000280002F0B1F8554C6B6810226360AB6838 -:10395000FF32A3602B7B23736B7B6373AB7BA373FF -:103960000F23E95CE15401339342FAD123004C9ACE -:10397000FC3318215A614D9A02A807929A612200DD -:10398000FF310918497EFF3251764F9A07921A6229 -:1039900090225200A95CA154519A9A62529ADA621A -:1039A000539A07921A6398225200A95CA1543222BA -:1039B000FF32A95CA154559A07929A639C22520047 -:1039C000A95CA1543A22FF32A95CA154579A0792EC -:1039D0001A64589A5A64A2235B00EA5CE2544623B4 -:1039E000FF33EA5CE2540133EA5CE2544823FF33DC -:1039F000EA5C2B4DE254AE6AE86A311C09F0DAFA4F -:103A0000FC21890509F0A4F9011C301C08F02EFEE8 -:103A10000023049308330393023302930833019382 -:103A20000B3B011C00931F4A0233200002F066F892 -:103A3000061E0BD01C48FEF7E3FF3000FFF764F8CA -:103A4000FEF768FD642006F099FAF9E7200003F01C -:103A5000B7FA040008F0F8FA2B0040331B78002B6B -:103A600005D01248FEF7CCFF2000FFF74DF8FFF716 -:103A70001DFBFFF787F8FFF7CBFC6B7C0020002BCA -:103A800000D10820FFF7D4FB5AB070BD50000020D1 -:103A9000550000204F0000204D0000201011002094 -:103AA000600000200000FA426C3001008A30010002 -:103AB000F8B51A4803F084FA08F0C6FA802100200D -:103AC000490008F0D2FA80270024154E7F0030709C -:103AD0003900002008F0C9FA124D60550134102C4D -:103AE000F6D1114B40331B78002B15D00F48FEF751 -:103AF00087FF3078FFF708F80D48FEF781FF0024B4 -:103B00000C48FEF77DFF285D10210134FFF706F811 -:103B1000102CF5D1FFF7BEF9F8BDC04610110020FA -:103B20007832002068320020600000204B30010015 -:103B300058300100E62E010010B5040001000448D1 -:103B400007F092F82100034808F064F810BDC04661 -:103B5000843800207C350020E12010B50002FFF7FA -:103B6000EBFFFFF753FE084B986880B2FFF7E4FFC6 -:103B7000FEF774FBFFF7D0FEFEF7A6FCFEF7BCFCD9 -:103B80000248FEF76FFF10BD6000002020360100E4 -:103B900070B53A4CC0B0FF226946200003F012FE17 -:103BA0002000012102F07CFF354E0500331DDB7F34 -:103BB000C4B2002B03D0A1B26846FFF7EBFBB37C85 -:103BC000002B03D021006846FFF7C4FB012C3BD932 -:103BD000E8B26844421E15782A4A831E15703200E6 -:103BE000383212781B78062A05D1022C03D003380C -:103BF0000478013CE4B2327C9A4203D0737C012009 -:103C0000002B22D10E232B401800022B1DD0023C8A -:103C1000E4B2EB070BD51C4B1A78A24207D16946D8 -:103C20001A480BF045FB03000320002B0DD0102693 -:103C30002E40002C10D10A20002E06D10638054255 -:103C400005D00230AB0700D4002040B070BD08237F -:103C50001D42FAD01800F5E7220069460B480BF028 -:103C600036FB094B0B201C70002EEED104386B077D -:103C7000EBD402382B07E8D50430E6E7101100201A -:103C80006000002059120020D00F0020CC0E002030 -:103C9000F7B52D4D2D4E29782D4C7318207C9A1E8A -:103CA00010702C4A013B12781A70D3070DD4A37CF4 -:103CB000002B02D03000FFF72DFB231DDB7F002BF4 -:103CC00003D029783000FFF765FBE122E368D201D9 -:103CD000934203D096221202934202D1022006F0B0 -:103CE0004DF91D4B1D4F1B683800019303F0E0FD9B -:103CF000019B8000C158380001F0E0FE2A780023C3 -:103D00003100380003F0A2FC002818D1144E33789B -:103D1000002B01D0FEF726FD2878FEF76FFCA17975 -:103D2000050008F0DFFA0F4B0F4A2D181D8013789D -:103D3000013313703378002B01D0FEF713FD06F02A -:103D4000E7F80A4B1860F7BDEB100020E80F0020E1 -:103D5000600000205A120020780700201011002077 -:103D6000A8000020E8100020EA100020EC1000203D -:103D70000221E2200D4A10B513788B4303401370E3 -:103D80000B4A0C4B1170002119700B4B38331B7808 -:103D9000062B05D1094B0331FC331970FF23137037 -:103DA0000122074B1A70FFF773FF10BD5A12002053 -:103DB000EB100020EA10002060000020E80F002037 -:103DC000810D0020E02100200D4A10B513780B4032 -:103DD00013700C4A1378991C11700B4908700B4929 -:103DE00038310978062905D109490333FC310B70B4 -:103DF000FF2313700122074B1A70FFF749FF10BD14 -:103E00005A120020EB100020EA1000206000002071 -:103E1000E80F0020810D002002221020064910B575 -:103E20000B789343013A134383430B70034B1A708F -:103E3000FFF72EFF10BDC0465A120020810D002052 -:103E40000222E220074910B50B78134303400B70A0 -:103E5000054B1A700023054A1370054A1370FFF7CB -:103E600017FF10BD5A120020EB100020EA100020AE -:103E7000810D00200220012110B50A4C23788343D4 -:103E80008B431A0010231343F322134000222370A4 -:103E9000054B1870054B1A70054B1970FFF7F8FEAB -:103EA00010BDC0465A120020EB100020EA1000207E -:103EB000810D0020F0B5482495B02200244902A8C5 -:103EC0000BF005FA234F02A9220038000BF0FFF98E -:103ED0003A000023313213707B740E33BB84FFF73A -:103EE000E5F8FEF74FFE3B00BC6A30331D781A4BF5 -:103EF000211CF86A1E6809F05DF8071CA81C09F06F -:103F00003DFA011C381C08F04BFD012701902900E7 -:103F1000380008F0CDFA080009F030FA011C0198C9 -:103F200008F016FF211C08F0A1FB3060FFF7A2FF8C -:103F30001320FEF763FB0122084B52421880084B06 -:103F40000C201F60074B1A60FFF772F915B0F0BD27 -:103F5000042B01006000002078070020E8100020FA -:103F6000B0000020AC00002010B5482203490448EE -:103F70000BF0ADF9FFF79EFF10BDC04660000020BA -:103F80000400002010B5482492B02200064968467B -:103F90000BF09DF92200694604480BF098F9FFF7F1 -:103FA00089FF12B010BDC046042B010004000020A0 -:103FB00070B5344D286800282ED004F0F6FB324C42 -:103FC0002378002B29D1FA212868C90004F019FCB4 -:103FD000002822D00123237005F09AFF2B4B186094 -:103FE0002378012B43D105F093FF284C2368C01A96 -:103FF000FA235B0098420FD92549264806F0C2FAF9 -:1040000005F086FF244B2060187806F00DFA012891 -:104010002BD10020FFF77AFA70BD2378012B17D03F -:104020002378012B06D11D49286804F0EAFB0223FE -:10403000002818D12378022BD2D1286804F0DAFBAB -:104040000028CDD00F20FFF761FAFFF79BFF09E0B2 -:10405000286804F0CFFB0028E2D00F20FFF756FAC3 -:10406000FFF782FF00232370BAE70F20D2E7022B6D -:10407000D2D105F04DFF054C2368C01A6428CBD976 -:10408000BEE7C0466432002079320020D80F0020FD -:10409000D02D01008438002051000020881300003A -:1040A000FC220F4910B50B780E481A401023134319 -:1040B000F02213400B7000230B4A195CD1540133DA -:1040C000102BFAD1094B1B7813741322084B1A706A -:1040D0000023084A1370084A1370FFF7D9FD10BD7A -:1040E0005A12002068320020E80F002078320020A9 -:1040F000EB100020EA100020810D00200221EE22AA -:10410000094810B503780B431A40042313430822CF -:1041100093430370054B064A197000231370054A38 -:104120001370FFF7B5FD10BD5A120020EB100020F0 -:10413000EA100020810D0020EC22104900200B78AD -:1041400010B51A4004231343082293430C4A0B7002 -:104150001378991C11700B4908700B493831097894 -:10416000062905D109490333FC310B70FF23137075 -:104170000122074B1A70FFF78BFD10BD5A12002069 -:10418000EB100020EA10002060000020E80F002063 -:10419000810D00200221EA22084810B503780B4364 -:1041A0001340E23A13430370054B064A197000238B -:1041B0001370054A1370FFF76BFD10BD5A120020F3 -:1041C000EB100020EA100020810D0020E8220F49AA -:1041D00000200B7810B51A40082313430C4A0B70CB -:1041E0001378991C11700B4908700B493831097804 -:1041F000062905D109490333FC310B70FF231370E5 -:104200000122074B1A70FFF743FD10BD5A12002020 -:10421000EB100020EA10002060000020E80F0020D2 -:10422000810D0020F8B5B34B18780E2801D900F0A5 -:1042300064FC07F0C3FF0F00450060008400A2008B -:104240000B011E017D01B50207031A030704DC03FD -:10425000E7033204A84D2B78002B03D000232B70EA -:1042600003202AE0A54B1C78E4B2002C02D0FEF714 -:1042700079FAF8BD05F04CFEFA21A14B89001F68C0 -:10428000A04BC71B04332000DE8F07F0EEFE36186C -:10429000B74203D89C4B1B78002BEAD1FEF79CFA5F -:1042A000061EE6D146200124FF30FFF791F8964B19 -:1042B0001C70FFF75DFD2E702000FEF7B9FFD8E7F8 -:1042C0008D4B1A78002A00D1C9E200221A708F4B58 -:1042D0001C7894420BD1AF204000FFF779F88C4B4B -:1042E0001C80FFF743F9FFF793F80420E5E7FFF799 -:1042F0008FF80220E1E7804B1A78002A02D00022D2 -:104300001A70ADE77D4B1C78E4B2002CAFD105F0FC -:10431000FFFD7B4B7F4A1B681288C01A7E4B1B88AF -:104320009B189842A5D97D4802F04AFE07F08CFE02 -:10433000C8204000FFF74CF8FFF76AF8BCE7FFF72A -:1043400027FC012803D8FFF763F80020B5E702280F -:1043500003D100226E4B1A80C3E7032803D1FFF775 -:104360006FFD0120A9E7042800D082E7AA20FF30D2 -:10437000FFF72EF8FFF7FAF8F1E7FEF7E9F9051E67 -:1043800003D00020FFF7C2F8DDE75B4C2378002B59 -:1043900006D096204000FFF71BF8257007208CE719 -:1043A000564B1C78E4B2002C00D060E705F0B0FD5D -:1043B000FA21534B89001E68524B861B04332000A0 -:1043C000DD8F07F052FE2D18AE4210D9FEF704FA29 -:1043D000002800D04DE7514802F0F2FD07F034FE0E -:1043E0007D20FEF7F5FFFFF7C3FC052065E7FEF72C -:1043F000F3F9002800D03CE7FEF702FA00280BD0C2 -:10440000FEF79AFA002800D133E71420FF30FEF7B8 -:10441000DFFFFFF7D7FCE8E7FEF72EFA002800D110 -:1044200027E7FEF7A7FA1420FF30FEF7D1FF3C4C38 -:104430002378012B07D1FFF7C9FEFEF71DFA0028EC -:10444000D3D12070D1E7FFF777FEF6E72A4B1A7831 -:10445000002A00D103E200221A702C4B1B789342F1 -:1044600003D0FEF7D5FF062027E79620FEF7B0FF22 -:1044700039E7214B1A78002A21D0002207201A7030 -:10448000FEF7D6FE05F044FD1D4B224A1B6812883C -:10449000C01A214B1B889B18984200D8E9E6194B9B -:1044A00047331A781F4B1B789A420FD2184A138849 -:1044B000013313801C4A13880133138013E70F4B19 -:1044C0001B78002BDED0FEF74DF9DBE7FEF784F911 -:1044D000002809D1FA30FEF77BFF144A1388013314 -:1044E0001380FFF799FC80E7FA204000FEF770FF89 -:1044F000BFE6C046581200207A320020A8000020F3 -:10450000EC1000206000002060120020810D0020CF -:10451000F0100020640D0020E81000201011002091 -:104520000F110020EA10002062320020F21000205B -:10453000FFF72EFB04240300A343DBB2022B01D0C0 -:1045400008284DD11920FEF743FFD94BD94C4633EB -:104550001B78002B23D0FEF79DFCD748FEF750FABE -:104560000021200001F058FE08F0E8FEFEF7BEFA38 -:10457000D248FEF745FA200002F05EFA08F0DEFEAF -:10458000FEF7B4FACE48FEF73BFA0021200002F015 -:104590008FF908F0D3FEFEF7A9FAFEF77BFC0025A1 -:1045A000C84B1D80FEF7E2FF2900200002F080F9D1 -:1045B000C54C051C20680AF009FD0600281C0F00E8 -:1045C0000AF004FD0022C14B09F07EFA02000B0044 -:1045D0003000390008F05CFF0AF04AFD206082E6F6 -:1045E000032825D1BA48FEF7F3FE0024B54B1C8002 -:1045F000FEF7BCFF2100AF4802F05AF9B24C051C8F -:1046000020680AF0E3FC0600281C0F000AF0DEFC1C -:104610000022AE4B09F058FA02000B0030003900BE -:1046200008F036FF0AF024FD2060FFF709FCDCE605 -:10463000042801D1A748D6E7052840D19C4B463332 -:104640001B78002B24D0FEF725FC9B48FEF7D8F9F9 -:10465000984C0021200001F0DFFD08F06FFEFEF70E -:1046600045FA9648FEF7CCF9200002F0E5F908F08B -:1046700065FEFEF73BFA9248FEF7C2F900212000E2 -:1046800002F016F908F05AFEFEF730FAFEF702FCC7 -:10469000002701203A00904B90491E78904C0B88DF -:1046A000904D964207DDAF5C0132E75401331B05A4 -:1046B0001B0D0700F5E7002F97D00B8095E7072823 -:1046C0003BD1854B884D1F7800231C002888854EE0 -:1046D000A7420CDDAF21335D844A890013540130B9 -:1046E00080B207F0FBFD013488B20123F0E7002B14 -:1046F00000D028800024734B1C80FEF737FF210078 -:104700006C4802F0D5F8704C051C20680AF05EFC7D -:104710000600281C0F000AF059FC00226B4B09F020 -:10472000D3F902000B003000390008F0B1FE0AF0A6 -:104730009FFC2060FFF7E2FC57E609282ED10024F9 -:10474000654B1B78A34206DD664B6948195D06F090 -:10475000FBF90134F4E700245A4B1C80FEF706FFF6 -:104760002100544802F0A4F8574C051C20680AF0B8 -:104770002DFC0600281C0F000AF028FC0022534BD9 -:1047800009F0A2F902000B003000390008F080FEA9 -:104790000AF06EFC2060FFF7FDFC26E65548C9E5EF -:1047A000554C2378002B09D0DC20FF30FEF710FE9B -:1047B00000230A202370FEF73BFD05E04F4B1B78DA -:1047C000002B1DD0FDF7CEFF05F0A2FB4C4B4D4C4E -:1047D0001B68C01AA04229D805F09AFB4A4C2368EE -:1047E000C01AFA2800D844E505F092FB474B206038 -:1047F000187805F019FE012816D1FEF787FE38E576 -:10480000FDF7EAFF0028DFD1FDF7FAFF0028DBD033 -:10481000FEF792F80028D7D0E1204000FEF7D8FD3F -:10482000FFF7D0FA0920C6E70220E6E705F070FBA3 -:10483000374B1B68C01AA04202D8FEF797FE18E556 -:104840000020DAE72C4B1A78002A08D000221A70D0 -:10485000FEF7DEFD0820FEF7EBFC0120CDE7274B3D -:104860001B78002B00D104E501E5FFF791F9022840 -:1048700050D100210F4802F01BF8134C051C206892 -:104880000AF0A4FB0600281C0F000AF09FFB002280 -:104890000E4B09F019F902000B003000390008F046 -:1048A000F7FD0AF0E5FB2060FEF7B2FD082004E505 -:1048B0006000002010110020EA2E0100ED2E010002 -:1048C000F12E0100F0100020840D002080842E4184 -:1048D000260200003F020000D00F00207C320020A2 -:1048E00061220020CC0E00209E0A0020E10700205B -:1048F000843800200D0200007A320020A800002039 -:10490000EC10002087130000C80E00205200002089 -:10491000CC0F0020C31EDBB2012BAAD90528C3D1BE -:10492000784B46331B78002B24D0FEF7B3FA764839 -:10493000FEF766F8754C0021200001F06DFC08F0D0 -:10494000FDFCFEF7D3F87248FEF75AF8200002F09B -:1049500073F808F0F3FCFEF7C9F86E48FEF750F85C -:104960000021200001F0A4FF08F0E8FCFEF7BEF8EB -:10497000FEF790FA002701203A00674B67491E783E -:10498000674C0B88674D964207DDAF5C0132E754F8 -:1049900001331B051B0D0700F5E7002F00D00B802E -:1049A000FEF7E4FD0021594801F082FF5E4C051C32 -:1049B00020680AF00BFB0600281C0F000AF006FB1B -:1049C00000225A4B09F080F80B0002003900300039 -:1049D00008F05EFD0AF04CFB2060FEF719FD082090 -:1049E000FEF726FC05F094FA514B186041E4514B58 -:1049F0001A78002A00D13CE400221A70FEF708FD64 -:104A00000D205AE44B4B1A78002A04D000220E20C5 -:104A10001A70FEF70DFC05F07BFA474B474A1B68FE -:104A20001288C01A464B1B889B18984201D8FFF782 -:104A300020FC4448FEF7CCFCFFF73AF8FEF7E8FC10 -:104A40000B203AE43B4B1A78002A03D000220E20B8 -:104A50001A7032E405F05CFA3B4C2368C01A4B280C -:104A600001D8FFF706FC05F053FA2060374C2368A5 -:104A7000D8B2FEF74BFD2368354A082B01D0012B35 -:104A800002D11168494211601268002A03DD5B00FF -:104A90002360FFF7EEFB5B10FAE7FFF779F80A28CF -:104AA0001CD12C48FEF794FC00222B4B1A80FFF7F8 -:104AB000F7FA204B1B78002B0BD105F029FA1E4B7F -:104AC0001E4A1B681288C01A1D4B1B889B189842EF -:104AD000EFD90020FEF7E8FDFFF7CBFB0B2807D14D -:104AE0001E48FEF775FC00221B4B01201A80F1E7DF -:104AF00096208000FEF76CFCA0E71948FDF7B2FF96 -:104B0000FFF7B7FB60000020EA2E01001011002023 -:104B1000ED2E0100F12E0100D00F00207C3200208C -:104B200061220020CC0E0020840D002080842E41C4 -:104B3000CC0F00207A320020EC100020640D002001 -:104B4000E8100020A3020000D80F0020B0000020D1 -:104B5000AC00002071020000F01000208A0200006A -:104B6000F62E010010B504000100044805F0ECFF2A -:104B70002100034807F0E8F910BDC0468438002042 -:104B80007C35002010B5034805F0EEFF024807F021 -:104B900058F910BD843800207C350020F0B597B05E -:104BA000FEF778F9FDF7F6FCA64B1C78012C02D82D -:104BB000FDF76AFF0EE0A44D2800FDF7D4FC0700C6 -:104BC0002800FDF7DBFC0600002F08D10028EFD0FD -:104BD000FDF762FF00239B4A137017B0F0BD002859 -:104BE000F6D1022C02D1FDF747FFF3E7AB78032C97 -:104BF00050D14F2B2AD018D8462B46D0492BD7D18D -:104C0000FDF73AFF9148FDF7FBFE9148FDF7F8FEEE -:104C10009048FDF7F5FE0120FDF768FF8E48FDF78F -:104C2000EFFE0020FDF770FFD4E7542B28D05A2B5D -:104C3000BED1FDF721FFFFF7A5FFBFF34F8F874BD5 -:104C4000874ADA60BFF34F8FC046FDE7854B1B787C -:104C5000012B02D1844A1370C5E7FEF727FAFDF74E -:104C600091FF3000FEF752FC804B30005B7C002B44 -:104C700000D10820FEF7DCFA00227D4B1A70B2E763 -:104C8000FDF7FAFEFFF770F9A4E7FDF7F5FEFFF771 -:104C900079F99FE7492B39D1042C00D088E7E878CF -:104CA0003038072800D983E707F06AFA0407B511FE -:104CB000181C1F2CFEF7F6F88CE76448FDF7A0FEE1 -:104CC0006348FDF79DFE6B48FDF79AFE82E70021E1 -:104CD000694801F0A1FA08F031FBA3E7664802F049 -:104CE0006FF99FE7FDF7F2FC9CE700245F4B102172 -:104CF0001B19D87C0134FDF711FF102CF6D1FEF7FB -:104D0000C9F867E75C4802F0D3FD8BE7262B20D17A -:104D1000042C13D1EB78462B05D0572B00D047E756 -:104D2000FEF78CFC5FE74822544904A80AF0CFFA4A -:104D3000482204A94D480AF0CAFAF1E75049280070 -:104D40000BF078F8002800D032E7484B01224633B8 -:104D500094E7532B00D02BE72B793F2B03D06A79B4 -:104D60003F2A00D0B5E0E8783038092800D91FE79D -:104D700007F010FA050841919497A2A6A9AC3B4B05 -:104D800098684FE7303B18000F2B00D910E707F069 -:104D90000BFA130017001A001D0020002300260044 -:104DA00029002C002F000FFF0FFF0FFF0FFF0FFF39 -:104DB00010002E4BD86835E72C4B3033187831E78C -:104DC0002A4B3133FAE7294B588E2BE7274B586B88 -:104DD00081E7264B3833F1E7244B3933EEE7234B99 -:104DE0003A33EBE7214B3B33E8E7204B3C33E5E735 -:104DF0001E4BD88F16E7303B18000F2B00D9D7E693 -:104E000007F0D2F91300160019001C0040004300FF -:104E10004600D6FED6FED6FED6FED6FED6FED6FE80 -:104E2000D6FE1000114B187CFCE6104B4033C5E752 -:104E30000E4B4133C2E70D4B0433DAE70B4B4433DF -:104E4000BCE7C046E00700207C0700204F2D010092 -:104E5000F6100020642D0100BC2E010000ED00E0E2 -:104E60000400FA050F110020620D002060000020F0 -:104E7000C00E0020E92D010010110020042B0100BC -:104E8000672D0100B34B453398E7B24B463395E7A6 -:104E9000B04B473392E7AF4B587CC3E6AD4B987CA1 -:104EA000C0E60024AB4B10211B19D87C0134FDF760 -:104EB00035FE102CF6D18DE6A64B0433D87FB1E633 -:104EC000A44B988CAEE6A34B986A04E7A14BD86A32 -:104ED00001E73D2B02D03D2A00D069E6290004A855 -:104EE0000AF0B2FF9C4C04A821000BF01FFE210029 -:104EF00000200BF01BFE03A901900BF005FE06003D -:104F00000F0009F0C1FF0400E8783038092800D903 -:104F10004EE607F053F90A002E00450197019F0164 -:104F2000A701BD01C601CE01E80196231B019C42E9 -:104F30001CD096235B019C4218D096239B019C4277 -:104F400014D0E1239B019C4210D09623DB019C42AC -:104F50000CD096231B029C4208D0E1231B029C42EA -:104F600004D0E1235B029C4200D021E6794B9C6097 -:104F700039E6287930380D2800D919E607F014F9F8 -:104F80004000480050005900B700C000CA00D000DF -:104F9000D800E00018FE18FE18FE0E00002C24D0E9 -:104FA000282C22D0962C20D0C8235B009C421CD0F9 -:104FB0009623DB009C4218D096231B019C4214D000 -:104FC00096235B019C4210D096239B019C420CD0FF -:104FD0009623DB019C4208D0E123DB019C4204D0F4 -:104FE00096231B029C4200D0E2E55A4DEB68002B51 -:104FF00004D1002C02D05948FDF734FDEC60F2E5F5 -:10500000631E312B00D9D3E5524B30331C70EAE5D7 -:10501000012C00D9CCE501231C404E4B3133F5E780 -:1050200023004F4A0A3B934200D9C1E5494B5C86B5 -:10503000D9E5484BDB68002B03D04A48FDF712FD49 -:10504000B6E50022484B3000390009F047F900224C -:10505000464B04000D0007F041FA002840D1444AB5 -:10506000444B3000390007F039FA002838D10022CB -:10507000414B2000290007F031FA002830D10022EE -:105080003E4B3000390007F029FA002828D13C4A6D -:105090003C4B3000390007F021FA002820D10022D3 -:1050A000394B3000390007F019FA002818D10022D6 -:1050B000364B3000390007F011FA002810D10022D9 -:1050C000334B3000390007F009FA002808D10022DC -:1050D000304B3000390007F001FA002800D167E5B5 -:1050E0003000390009F0C4FF1A4B58637BE5194BB7 -:1050F000DA68002AA1D1A21F3833062A86D957E5DB -:10510000144BDA68002A98D1621F3933032A00D879 -:105110007CE74DE5FF2C00D94AE50E4B3A3375E7A5 -:105120001D4AA31F934200D942E50A4B3B336DE76A -:105130002300103BEE2B00D93AE5064B3C3365E7E4 -:105140002300164A0A3B934200D931E5014BDC8724 -:1051500049E5C0466000002049300100712D010082 -:10516000F5FF0000B92D01000000594000409040BB -:105170003333333333332F400040A04000403F40AF -:105180009A99999999D9444000404F4000405F4016 -:1051900000406F4000407F40F9FF0000C60700005C -:1051A000287930380D2800D902E506F0FDFF1400FB -:1051B0001C0024002D0035003D00450001FD01FDCF -:1051C00001FD01FD01FD01FD0E00FF2C00D9EFE402 -:1051D000524B1C7407E5012C00D9E9E401231C4063 -:1051E0004E4B403312E7012C00D9E1E401231C406F -:1051F0004A4B41330AE72300494AFA3B934200D91C -:10520000D6E4464B0433A2E7012C00D9D0E40123B5 -:105210001C40424B4433F9E6012C00D9C8E4012379 -:105220001C403E4B4533F1E6012C00D9C0E401237C -:105230001C403A4B4633E9E6FF2C00D9B8E4374B23 -:105240004733E3E6012C00D9B2E401231C40334B81 -:105250005C74C8E4012C00D9AAE401231C402F4B44 -:105260009C74C0E401980AF0F7FD202800D09FE468 -:1052700000242A4D019B1B195978019B185DFEF7EC -:105280001FF96310EB180234D874202CF2D1AAE471 -:10529000012C00D98CE401231C40204B0433DC7723 -:1052A000A1E423000E3B102B00D981E41B4B9C840E -:1052B00099E400221B4B3000390007F033F9002835 -:1052C00000D175E4154CE06A09F080FE32003B0025 -:1052D00007F028F9002800D16AE43000390009F00D -:1052E000C7FEA0627FE40D4CA06A09F06FFE320099 -:1052F0003B0007F003F9002800D159E400220A4BD3 -:105300003000390007F0FAF8002800D150E43000EE -:10531000390009F0ADFEE06265E4C046600000209F -:1053200005FF000000308C4000008D4070B50B4D33 -:10533000280005F013FC041E04D0280005F036FCFC -:10534000C0B270BD064D280006F088FD03002000A5 -:10535000002BF6D0280006F051FEF1E7843800203B -:105360007C350020864BF7B51B78002B34D1854E59 -:105370003478E4B2002C2FD1FDF754FA00282BD05A -:10538000FDF78CFA002827D00120FEF711F9FDF770 -:1053900049FAA0421DDDFDF7BDF8794B1B78002BC3 -:1053A00017D13378002B01D0FDF7DCF9FDF776FA41 -:1053B00000280ED0744D754A2B880134D05CFFF75D -:1053C000D1FBFFF7DFFB2B8801331B051B0D2B8067 -:1053D000DDE70020FEF7ECF86D4805F0BFFB002884 -:1053E00009D0674B1B78002B0BD0FDF733FA002850 -:1053F00000D09BE0F7BD674806F030FD0028F0D1F3 -:10540000F3E70120FEF7E2F8FDF784F85D4B1B7827 -:10541000002B01D0FDF7A6F9FDF7F2F95E4B020073 -:1054200018785E4B9A421ED1FF2805D05C4B0021B4 -:1054300044331B78002B1ED1FFF778FF594B050032 -:105440001B780093002B2CD00D2817D1564B1B78BE -:10545000002B15D00022554B1A70FFF79FFB002040 -:10546000FEF7B4F8B8E7FF28E6D04D4B44331B787D -:10547000002BE1D0012104F0ABFFDDE70A28EED0DC -:105480002800FFF76FFB28000BF0A5FB464C484AAD -:1054900023786421D0540133D8B206F01FFF217065 -:1054A000DDE73F4C434F2379434E019383420CD1B8 -:1054B00004F02EFD3B68C01AFA23DB0098421AD88C -:1054C00004F026FD3D4B984215D904F021FD6B46B2 -:1054D0001B783860337041342378002B02D02800C9 -:1054E000FFF740FB364A37491388CD5401331B057B -:1054F0001B0D1380B3E733780133DBB2337062796D -:105500009A42E8D141342378002B02D00198FFF76A -:1055100029FB2D48FDF7A6FA0123224A1370002328 -:10552000337004F0F5FC234B186063E71D4B1B78C8 -:10553000002B00D05EE71D00244F1B4AC4B21C4E56 -:1055400038881470AC420CDDAF21214B89001B5C04 -:105550000130735580B206F0C1FE013588B20123D7 -:10556000F0E7002B00D038803378522B00D041E791 -:105570000122113B33700D4B1A70FFF70FFB39E717 -:105580007A320020A80000207E3200206122002014 -:10559000843800207C35002056000020FF0F0000DA -:1055A00060000020C00E0020E00700200F11002046 -:1055B0007C070020C40E0020800D0020CF070000D3 -:1055C0005C12002061120020E42D0100A00A0020DE -:1055D000E107002010B5FCF79DFFFEF7E9FCFFF79F -:1055E000C1FEFEF71FFE10BD10B50400044B0360A2 -:1055F000783002F061FA200002F08EFE200010BD2B -:10560000D02A010010B50400FFF7EEFF200006F0DD -:105610005DFD200010BD10B5002806D03023032109 -:10562000584306F0D5FD0138FDD110BD012210B55B -:10563000016CC36B0A40002B01D1002A06D049102F -:105640004018002A01D00268D358984710BD70B5A1 -:105650000124856C426C03002C40002A02D1FF20FB -:10566000002C06D068101818002C01D003689A5836 -:10567000904770BD012210B5016DC36C0A40002B2C -:1056800001D1002A06D049104018002A01D0026832 -:10569000D358984710BD012210B5816D436D0A4063 -:1056A000002B01D1002A06D049104018002A01D051 -:1056B0000268D358984710BD10B5C06E02F078F953 -:1056C00010BD000010B54366029B026683661F4B47 -:1056D0000200036700231E4C7032C16504805371C1 -:1056E000137153720122011D5242C267CA67194ADF -:1056F0000121C266020078321170174A4260174ACF -:10570000C260174A8260174A0261174A4261174A0B -:105710008261174AC261174A0262174A8363426272 -:10572000164A03648262164A8364C262154A03659C -:105730000263154A83654263144AC263144A426491 -:10574000144AC264144A426510BDC04680841E00DB -:105750000080FFFF8032002039A3000029A4000050 -:10576000D1A3000045A9000029A90000A99C0000C0 -:10577000D99D0000AD9F00007D9F000017560000DE -:10578000119F00001D9F0000B95600002D5600001B -:105790004F560000755600009756000003000800A1 -:1057A00010B5411C04D05B68002B01D01100984754 -:1057B00010BD0300080010B5411C04D09B68002BED -:1057C00001D01100984710BD10B50400C16D012231 -:1057D000FFF7E4FF0122E16D2000FFF7EAFF23005D -:1057E00078331B78002B08D0A26B636B5010201805 -:1057F000D20701D50268D358984710BD10B50023D1 -:105800004A1C05D0C268131E02D00800904703004E -:10581000180010BD10B5040008001100421C04D08F -:10582000A469002C01D01A00A04710BD036A10B56E -:10583000002B00D0984710BD436A10B5002B01D053 -:105840000800984710BD036B10B5181E00D098478C -:1058500010BD10B5016F426FC06E02F0D9F810BDD7 -:1058600010B5C06E02F004F910BD10B5C06E02F0A4 -:10587000EBF810BDF7B504000E001F0008AB20CBFD -:1058800001921B780093FFF7E4FF0022E16D2000F6 -:10589000FFF78FFF019920003143FFF7E1FF6378A5 -:1058A000B3420CD1002F1BD03D00009AEB1B9A4253 -:1058B00016D929782000FFF7D3FF0135F5E72378C3 -:1058C000B3420DD1002D0BD02E00009A731B9A42CB -:1058D00006D900212000FFF7C3FF30700136F4E73E -:1058E0000122E16D2000FFF764FF2000FFF7BDFFFC -:1058F000F7BD13B50C00019200930178002322003C -:10590000FFF7B8FF13BD30B50F240023012585B084 -:105910006C4423700A00017801950094FFF7AAFFF8 -:10592000207805B030BD70B515001C00072A0FD8CF -:10593000072B0DD89A420BD3FFF7E5FFFF23072271 -:105940001900521B1141A3400B401840C0B270BD5A -:105950000B204042FBE713B50C0001930023417874 -:10596000009313002200FFF785FF13BD1FB50F231F -:105970006B441A7001220C000192002241780092BF -:105980002200FFF777FF1FBDF0B585B01E000AAB00 -:105990001F780BAB1B78050002930CAB1B780191B1 -:1059A00014000393072E30D8072F2ED8BE422CD3D5 -:1059B000FFF7A9FFFF2208211300CF1B01363A4150 -:1059C000B3401343DB43DBB298431C400443E4B2CF -:1059D000220001992800FFF7C9FF2800FFF733FFD5 -:1059E000FA260700029BB6005E432800FFF72BFF54 -:1059F000C01BB0420CD201992800FFF784FF039B23 -:105A000060401840F1D105B0F0BD0B204042FAE7EC -:105A10001020FBE710B5C06E01F0F2FF10BD0300CF -:105A200010B578331B78002B04D0C36E002B01D047 -:105A3000FFF7F0FF10BD3322030900011843C0B285 -:105A400083108000904313400343DBB2223258108E -:105A50005B00104093431843C0B27047030070B519 -:105A600001251600040079331D70031DC167DA6734 -:105A70002A00FFF793FE2A0031002000FFF78EFE78 -:105A800070BD030070B579331B7804001500002B3E -:105A900009D00A00C16FFFF78CFE231D2A00D96FC1 -:105AA0002000FFF786FE70BD10B5040000F016FA66 -:105AB000014B2000236010BD2C36010070B50D1C79 -:105AC00004000C49281C06F067FD00280ED00A4986 -:105AD000281C06F04DFD002808D0291C200001F0EC -:105AE000F7F8002801D1FC34E56270BD0C2040427B -:105AF000FBE7C0460000094300007F44F0B585B0D5 -:105B00001D000AAB1B78171C01930BAB1A780CAB6A -:105B10001B780E1C0EA95BB2097802930DAB0391A2 -:105B20001B881221040001F091FA002822D1391CAF -:105B3000200000F0D7FB00281CD1311C2000FFF70B -:105B4000BDFF002816D12900200000F061FD0028CB -:105B500010D10199200000F0C3FD00280AD10200F5 -:105B60000299200000F0CEF9002803D1039920000B -:105B700000F057FA05B0F0BDFC3010B50400806AA3 -:105B80000122416EFFF70AFEA06A0022416EFFF774 -:105B900010FEA06A0121FFF74FFEA06A0122416EAC -:105BA000FFF707FE0521A06AFFF746FE10BD0000C3 -:105BB000F0B58BB00890099100F071FE802800D0FC -:105BC0004EE1089CFC34206B09F000FA0022A74B40 -:105BD00008F0F6FD49004F08A54AA64B390006F02B -:105BE0008DFC002816D0E56AA349281C06F0D4FCD9 -:105BF000002831D0A149281C06F0BAFC00282BD07F -:105C000002223621A06AFFF7B1FE64223A21A06A7F -:105C1000FFF7ACFEE36A206B059309F0D7F9984AC9 -:105C2000984B0690079108F0CBFD49004D082900DC -:105C30008F4A904B06F062FC051E20D0059809F0B3 -:105C4000C5F9914A914B07F023FC09F011FA0024A1 -:105C5000482505902600D1E08D49281C06F09CFCC3 -:105C60000028D7D08B49281C06F082FC0028D1D010 -:105C700002223621A06AFFF779FE7F22C6E7864A14 -:105C8000864B0698079908F09BFD49004F08784A13 -:105C9000784B390006F032FC041E0BD0059809F051 -:105CA00095F97F4A7F4B07F0F3FB09F0E1F92C00EF -:105CB000059018E0724A7C4B0698079908F080FD21 -:105CC00049004F086A4A6B4B390006F017FC051E65 -:105CD0000BD0059809F07AF96B4A744B07F0D8FBA2 -:105CE00009F0C6F905904425B4E76B4A704B069855 -:105CF000079908F065FD49004F085D4A5D4B390082 -:105D000006F0FCFB041E05D0059809F05FF9694A0E -:105D1000694BC8E7002206980799684B08F050FDC8 -:105D200049004F08524A534B390006F0E7FB051E65 -:105D300005D0F4210598890506F098FCD2E7604A61 -:105D4000604B0698079908F03BFD49004F08484A08 -:105D5000484B390006F0D2FB041E05D0059809F027 -:105D600035F9594A594B9EE7002206980799584B36 -:105D700008F026FD49004F083D4A3E4B390006F029 -:105D8000BDFB051E00D06EE0069807990022514B1E -:105D900008F016FD49004F08354A364B390006F029 -:105DA000ADFB041E5ED10698079900224A4B08F00D -:105DB00007FD49004F082E4A2E4B390006F09EFB86 -:105DC000002850D1069807990022284B08F0F8FCCB -:105DD00049004D08264A274B290006F08FFB002872 -:105DE0003ED0089C2F21FC34A06AFFF78CFD3021A7 -:105DF0000500A06AFFF787FD40260400089B08986D -:105E00001B685B689847089F099BFC370599002B26 -:105E100000D1F96A089800F05BFF0721FF230222F6 -:105E20000293019200910B0032002A31B86AFFF709 -:105E3000ABFD0026FF230222029301920096F83B5D -:105E40002A002F21B86AFFF79FFDFF230222029349 -:105E500001920096F83B22003021B86AFFF794FDCA -:105E60000BB0F0BD2C004025F4E6C04600407F405A -:105E7000FCA9F1D24D62503F0080574400007F449E -:105E80003333333333331F4093E34EE960FD7F3FB9 -:105E90000000CD4300400344CDCCCCCCCCCC24403E -:105EA0004968CBB91457853F33332F4060FD8F3F8E -:105EB000CCCC3440DC4B1AA37554953F00403F4096 -:105EC0009A99999999D9444012DA722EC555A53FED -:105ED00000404F4000405F4000406F4010B504005C -:105EE00000F062FC064B012208332360A3235B0011 -:105EF000E25448230022FF332000E25410BDC04684 -:105F000070360100F0B587B007000D000492002A3A -:105F100006D0CB1CDBB2122B08D90D2040423BE04F -:105F2000142903D08B1EDBB20F2BF6D8380000F0FB -:105F30005BFE049B0590FC37002B2FD07022002DB8 -:105F400001DAEA1C20251543ECB20226FF250723BF -:105F500002950093019600220921B86AFFF714FD0B -:105F60000023029500930196220004900633092134 -:105F7000B86AFFF709FD0023049C059A04432243F5 -:105F800014B2029501960093042233004D21B86AA1 -:105F9000FFF7FAFC204300B207B0F0BD0226FF2352 -:105FA000B86A02930196142D1CD0F83B009380220E -:105FB0000921FFF7E9FC7022023D2A43FF25049BDB -:105FC00002950093019604000623D2B20921B86A13 -:105FD000FFF7DAFC059B044323431CB202950196AC -:105FE000049BD0E7072580222B0009210095FFF7AD -:105FF000CBFCFF23049A0293009201960400F93B24 -:106000007F220921B86AFFF7BFFCFF23059A0443EA -:106010000293049B224314B20093019633002A009A -:10602000B4E7F0B587B00400049106295BD800F00E -:10603000DBFD0600200000F032FC050080282BD19B -:10604000FF27049B7E3DFC34A06A029701950095D2 -:10605000002B06D12B0004222621FFF795FC0643D6 -:1060600016E02B0000222621FFF78EFC0322049B62 -:1060700005905B011A430023D2B200930297019569 -:1060800007330C21A06AFFF77FFC059B03431E43E7 -:1060900036B2300007B0F0BD0028FAD1049BFC34C2 -:1060A000A06A002B08D1FF330293FD3B019301331B -:1060B000009308220D21D0E70327FF230222029339 -:1060C00001923B0006320D210097FFF75DFCFF2394 -:1060D000049902934A01FD3B3A4301930590D2B2E1 -:1060E00000950533CDE713267642D2E77FB505004C -:1060F0000E0000F0D4FB002834D19C235B00EB5C45 -:10610000002B32D0280000F06FFD0400012E16D0C5 -:10611000002E03D0022E1CD06A2024E0FF23320080 -:106120000293FD3B019303330093FC3501330A21B5 -:10613000A86AFFF729FC204300B204B070BDFF231A -:106140000293FD3B019303330093FC35013320227E -:10615000EDE7FF230293FA3B0093FC3501960133F0 -:106160004022E4E714204042E7E76B20FBE730B52C -:1061700085B004000D0000F092FB002845D19C235F -:106180005B00E35C002B07D0002D41D129002000EB -:10619000FFF7ACFF05B030BD200000F025FD002862 -:1061A000F8D1042D36D8280005F0F4FF03111B3474 -:1061B0002500FF2300220293FD3B0193033300934C -:1061C000FC3401330A21A06AFFF7DEFBE2E7FF237C -:1061D0000293FD3B019303330093FC3401336022AF -:1061E000F0E7FF230293FD3B019303330093FC345C -:1061F00001334022E6E7FF230293FD3B0193033383 -:106200000093FC3401332022DCE714204042C1E734 -:106210006B20FBE76A20F9E770B504000E0025004B -:1062200000F03DFBFC35802827D12149E86A06F0C3 -:1062300095F9AE6A031E19D0002307221A213000F7 -:10624000FFF771FBA43807F099F8051C200000F057 -:10625000F3FB0021041C06F081F9002804D0281C5F -:10626000211C06F003FA051C281C70BD07221A2108 -:106270003000FFF758FB9D38E5E7002E04D10522DA -:106280003100200001F0DCF8002307221121A86A68 -:10629000FFF749FB07F072F8BF21090606F058FD29 -:1062A000051C002EE0D1236820005B689847DBE7DF -:1062B000000059447FB50E00050000F095FCFF2357 -:1062C0000293FD3B019302330093FC35040003333A -:1062D00032001D21A86AFFF757FB204300B204B02B -:1062E00070BD0000F0B587B00600059100F0D7FA48 -:1062F000802800D0EEE0059808F068FE774A784BD9 -:1063000004000D0008F05CFA4B009C466746754A95 -:10631000754B790806F0F2F8002800D097E0734A30 -:10632000734B2000290008F04BFA4B009C4667464F -:106330006C4A6D4B790806F0E1F8002800D0B5E012 -:10634000664A6C4B2000290008F03AFA4B009C4644 -:106350006746644A644B790806F0D0F8002800D0FC -:10636000A6E0624A644B2000290008F029FA4B009D -:106370009C4667465B4A5C4B790806F0BFF80028EC -:1063800000D097E000225D4B2000290008F018FAA9 -:106390004B009C466746534A534B790806F0AEF8CB -:1063A000002800D088E0564A564B2000290008F00B -:1063B00007FA4B009C4667464A4A4B4B790806F061 -:1063C0009DF8002800D079E000224F4B20002900E2 -:1063D00008F0F6F94B009C466746424A424B790862 -:1063E00006F08CF8002800D06AE00022474B20001D -:1063F000290008F0E5F94B009C466746394A3A4BBC -:10640000790806F07BF800285CD10022404B200080 -:10641000290008F0D5F94B009C466746314A324BBB -:10642000790806F06BF800284ED10022394B29007C -:10643000200008F0C5F949004D082900294A2A4BD7 -:1064400006F05CF89021002802D1082443E00021E6 -:106450003000FFF72FFF041E25D13300059AFC33CF -:106460001A63A3225200B25C002A1CD09D6A9823B2 -:1064700001205B00F35C984006F0CAFF059906F026 -:106480008FFA8321C90506F087F8FF2202230292C2 -:10649000019300281AD001330093F73A26212800EF -:1064A000FFF772FA0400200007B0F0BD1021CFE71B -:1064B0002021CDE73021CBE74021C9E75021C7E7B4 -:1064C0006021C5E77021C3E78021C1E703232200D3 -:1064D0000093E3E714246442E5E7C04633333333E3 -:1064E00033331F40FCA9F1D24D62503FCDCCCCCC10 -:1064F000CCCC244033332F40CCCC344000403F4000 -:106500009A99999999D9444000404F4000405F4082 -:1065100000406F4000407F40F0B587B00490059187 -:1065200000F062FB049D059B0400FC350227602BF4 -:106530003AD19F33029300230197009301221D213A -:10654000A86AFFF721FA3A22049BFF329B5C0443BE -:1065500024B2A86A002B01D0642305930226FF27EA -:106560000723059A1E21029701960096FFF70CFA61 -:106570000023029700930196044333000522312142 -:10658000A86AFFF701FA00230C2224B20443009307 -:1065900024B20297019607333721A86AFFF7F4F96E -:1065A000204300B207B0F0BD0026FF2301970293FD -:1065B0000096330032001D21A86AFFF7E5F93A2161 -:1065C000049AFF31525C0599531E9A41FF230197AB -:1065D0000293009792000A430443F83B1E21A86AE5 -:1065E000FFF7D2F9FF2301970293009624B20443E8 -:1065F0003B0003223121A86AFFF7C6F9FF2324B22A -:106600000443029324B201970096F83B0A22C3E7A1 -:10661000F0B585B004000D0000F041F9802843D1A9 -:10662000A81F062843D805F0B5FD04392F313335AE -:10663000370060212000FFF76FFF061E21D198234D -:106640005B00E5541633E35C002B1AD00120FC34C8 -:10665000A84006F0DDFE216B06F0A2F98321C905F2 -:1066600005F09AFFFF220223A76A029201930028F5 -:1066700016D001330093F73A26213800FFF784F94A -:106680000600300005B0F0BD8021D3E79021D1E7AE -:10669000A021CFE7B021CDE7C021CBE77021C9E72A -:1066A000032332000093E7E714267642E9E7092640 -:1066B000FBE77FB50E00050000F096FAFF2302937A -:1066C000FD3B0193013B0093FC3504000233320093 -:1066D0001D21A86AFFF758F9204300B204B070BD2D -:1066E00070B504000D0000F0DAF8802816D1681F9C -:1066F000032816D805F04EFD10020C0E04212000D0 -:10670000FFF7D7FF002802D13223FF33E55470BDD5 -:106710000621F4E72900F2E70221F0E714204042C5 -:10672000F5E70A20FBE7F0B587B0040005911500F6 -:1067300000F0B5F880281BD1FF2602270023FC3487 -:10674000029601970093059A1D21A06AFFF71CF994 -:1067500000280BD1029601970090EAB2072322216C -:10676000A06AFFF711F9002800D1256407B0F0BD39 -:1067700014204042FAE710B50A000121FFF7D3FFC9 -:1067800010BD10B5FF220021FFF7CDFF10BDFC307A -:10679000806A704710B50400080003F045FEC1B2DE -:1067A000200001F0ADF810BD70B504000D00FF220F -:1067B000104901F09BF8104B2200083321002360A0 -:1067C0000023FC32FE319C20D36213634B860021F0 -:1067D0004000916323543A20FF3023545164A22196 -:1067E0004900136463544623C439FF33E154200045 -:1067F000956270BD00247442BC36010010B5FC30B7 -:10680000806A0A000423016EFFF704F810BDFC3013 -:10681000806A0A00816E10B54B1C02D00423FEF77B -:10682000F9FF10BD10B5FFF7F2FF10BD10B5FC3039 -:10683000806AFFF713F910BD10B5FC300023072262 -:106840004221806AFFF76FF810BD1FB5FF230022B9 -:106850000293FD3B01930092FC3005332421806AB2 -:10686000FFF792F805B000BD70B505000E000024DA -:106870002B682800DB6B98472800FFF7DDFFB0424C -:106880000BD02B000134FC330A21986AE4B2FEF7E6 -:10689000D3FF0A2CECD1002070BD0120FCE70723B8 -:1068A00010B5FC301A000121806AFFF73CF810BDDA -:1068B000F8B504000D00FFF7F2FF0600802800D0B5 -:1068C00075E0FC34002303222821A06AFFF72BF88F -:1068D0000023070007222921A06AFFF724F80023DC -:1068E000060007222A21A06AFFF71DF83F0407438C -:1068F000B0B20002384303033DD5474B1843404232 -:1069000006F086FD9721C90506F022FA08F05EFB25 -:106910000022424B07F0D8F80600206B0F0008F069 -:1069200055FB00223E4B07F0CFF802000B00300071 -:10693000390007F0D3FC0200802000060B18190074 -:10694000100008F095FB061C002D12D008F03EFB4D -:106950000022344B07F0C2FC334A344B07F0BEFC34 -:1069600008F086FB05F04CFE2721C2B2A06AFEF7B4 -:10697000FDFF301CF8BD06F04BFD9721C90506F060 -:10698000E7F908F023FB0022244B07F09DF80600EE -:10699000206B0F0008F01AFB0022214B07F094F83F -:1069A00002000B003000390007F098FCC9E700280E -:1069B00030D1FC34030007221D21A06AFEF7B3FF8B -:1069C0003300050007221E21A06AFEF7ACFF2B0250 -:1069D00003431BB298B2002B13DA404280B206F098 -:1069E00017FD08F0F3FA0022114B07F077FC0200C4 -:1069F000802000060B181900100008F039FB061C57 -:106A0000B7E706F005FD08F0E1FA0022084B07F0B1 -:106A100065FCF2E7074EACE70000F0FF80847E41A2 -:106A200000407F400000A03F666666666666EE3FF7 -:106A300080844E40000080BF10B50400FFF72FFF98 -:106A4000802810D1FC34002307221921A06AFEF708 -:106A50006AFF40B208F04EFA0022044B07F03EFCF9 -:106A600008F006FB10BD0020FCE7C0460000D03F48 -:106A700030B585B004000D00FFF711FF00280CD1E0 -:106A8000FF230293FD3BFC34019300902A000D216B -:106A9000A06AFEF779FF05B030BD14204042FAE746 -:106AA00070B505000E002C00FFF7F9FEFC348028BD -:106AB00009D198235B00EB5C062B1AD01321A06A46 -:106AC000FEF721FF70BD002813D1A2235B00E95C13 -:106AD00000290ED1002E0CD04623FF33EB5CA06AB8 -:106AE000802B08D1FEF70FFFA22301225B00206458 -:106AF000EA54206CE6E73221F4E730B585B00400B3 -:106B00000D00FFF7CCFE002829D1012D14D0002D57 -:106B100003D0022D1AD0172022E0FF232A0002936F -:106B2000FD3B019303330093FC3401333021A06A11 -:106B3000FEF72AFF05B030BDFF230293FD3B019312 -:106B400003330093FC3401332022EFE7FF23029349 -:106B5000FA3B0093FC34019501334022E6E7142010 -:106B60004042E7E770B50500FFF799FEFC35802845 -:106B700005D11221A86AFEF7C6FE80B270BD3F2182 -:106B8000A86AFEF7C0FE000284B23E21A86AFEF7A2 -:106B9000BAFE2043F1E710B50400FFF780FE0300C2 -:106BA0000020802B04D1FC341821A06AFEF7ABFE34 -:106BB00010BD30B585B004000D00FFF770FEFF2357 -:106BC000002803D16B1F5A1E9341FE330293052305 -:106BD00001930023FC3400932A0002330121A06AB0 -:106BE000FEF7D2FE05B030BD0300002210B5040050 -:106BF000FC331100986AFEF744FF01212000FFF7E3 -:106C0000D8FF10BDF0B5012185B00500FFF7D1FF19 -:106C100000281DD102242E00FF27FC360297019484 -:106C20000094020005234021B06AFEF7ADFE002863 -:106C30000ED101002800FFF71BFF002808D1062312 -:106C400002970194009302003121B06AFEF79CFE86 -:106C500005B0F0BDF0B50F002D3FFBB285B00400CC -:106C60000D00C32B01D900292FD101212000FFF7EE -:106C7000A0FF0600002D10D1FF232A000293FD3B48 -:106C800001930333FC3400930B21A06AFEF77CFED2 -:106C9000064336B2300005B0F0BD782D0FD805217F -:106CA000380005F01FFBFF2320220293FD3B0193D8 -:106CB000002302430093D2B2FC340533E4E7F02D05 -:106CC000E8D828000A211E30EBE711267642E1E7DA -:106CD000F0B50600FC3685B005000F1C0023072226 -:106CE0002421B06AFEF71FFE0024A04204D1012136 -:106CF0002800FFF75EFF04009221381CC90506F04A -:106D000027F808F063F90022184B07F0E7FA05F0BE -:106D10008FFCFF2702230021019300910297020CB0 -:106D20000500D2B205330631B06AFEF72DFE02230C -:106D3000002101930091029705332A0A19000443A8 -:106D4000D2B2B06AFEF720FE022300210193009127 -:106D5000029724B20443EAB205330831B06AFEF761 -:106D600013FE24B2204300B205B0F0BD0000A03FE6 -:106D700070B50D000400FFF792FD011E28D126001A -:106D8000FC360122B06AFEF77CFE002D15D02A0CDD -:106D9000D2B20621B06AFEF7E9FD2A0AD2B2072173 -:106DA000B06AFEF7E3FDEAB20821B06AFEF7DEFD45 -:106DB00003212000FFF7FDFE70BD2000FFF722FF3A -:106DC0000028F9D1236801001B6C20009847EFE7E9 -:106DD00014204042F0E710B50400FFF760FD021EEA -:106DE00014D12300FC33986A0121FEF74AFE2000EB -:106DF000FFF708FF002808D12368012120001B6C41 -:106E0000984705212000FFF7D4FE10BD1420404212 -:106E1000FBE730B585B004000D00FFF740FD80288A -:106E200012D101212000FFF7C4FEFF230293FD3B96 -:106E300001930023FC3400932A0007333921A06A10 -:106E4000FEF7A2FD05B030BD14204042FAE7F0B5D0 -:106E50000C0087B001210600FFF7ABFE051E21D113 -:106E60003000FFF71CFDC0B280281ED1052C34D99C -:106E7000FF230227029301970095FC36220AF83B74 -:106E80002021B06AFEF780FDFF23212102930590A7 -:106E9000E2B201970095F83BB06AFEF775FD059FD9 -:106EA00007433DB2280007B0F0BD002818D1FF23EA -:106EB0000227029301970095FC36E20AF83B252150 -:106EC000B06AFEF761FDFF23E40802930590E2B289 -:106ED00001970095F83B2621DEE712256D42E1E798 -:106EE0000125FBE7F0B587B005000E1CFFF7D7FCC6 -:106EF000002855D10021301C05F030FB341C00283F -:106F000000D02A4C2F00FC37B86B08F05FF8002245 -:106F1000274B07F0E3F904900591201C08F056F880 -:106F200002000B000498059906F0B2FA0022214BEA -:106F300005F0E4FA002836D01F49301C05F018FB94 -:106F4000002830D028000121FFF733FE051E24D190 -:106F50009221201CC90505F0FBFE184905F020FD13 -:106F600005F04EFB0226FF23019602930095020ACC -:106F70000400D2B2FA3B0421B86AFEF705FDFF23F4 -:106F80000196029300950490E2B2F83B0521B86A9D -:106F9000FEF7FAFC049E064335B2280007B0F0BDA8 -:106FA00014256D42F9E76625FBE7C0469A99193F1B -:106FB0000000E03F00406F40000048430000FA46F8 -:106FC000F7B50500FFF76BFC2C230193002801D1D6 -:106FD0001B3B01932F00002405212800FC37FFF7FD -:106FE000E8FD0A21B86AFEF727FC26000199B86A75 -:106FF000FEF789FC01231840A040E4180643F6B2CE -:10700000082CF3D128001900FFF7D3FD3000FEBD96 -:107010007FB50E0000210500FFF7CBFDFF23040024 -:1070200028000293FA3B019302330093FC303200B4 -:107030000121806AFEF7A8FC012104432800FFF724 -:10704000B8FD24B2204300B204B070BDF7B505000E -:107050000F000400FC35A86A01931600FEF7B4FB8C -:10706000A86A0022016EFEF799FBA86A0022816ED1 -:10707000FEF794FB39002000FFF7F6FB002805D14E -:10708000A86AFEF7CCFC02204042FEBD2368200027 -:107090005B6898470028F8D12000FFF7D6FB00284E -:1070A000F3D12000FFF7FBFB802814D13100200032 -:1070B000FFF7AFFE0028E8D13C212000FFF7CAFD12 -:1070C0000028E2D101992000FFF7C1FE0028DCD1A1 -:1070D00000236B64D9E780212000FFF799FF002887 -:1070E000E4D0D2E710B50400FFF7D9FB802806D121 -:1070F000FF221221FC34A06AFEF738FC10BD0028E4 -:10710000FCD1FC34FF223E21A06AFEF72FFCFF22B7 -:107110003F21F0E7F0B50E008DB00121070000928D -:107120000893FFF746FD002800D06FE13800FFF715 -:10713000B6FB802800D0F5E098233D005B00FB5CA7 -:107140007F389840FC3506F063F9296B05F028FC80 -:107150008321C905029005F01FFA0023002801D001 -:10716000FE239B05049300231D211A00A86AFEF745 -:10717000DAFB02230A901A001E21A86AFEF7D3FB4D -:1071800000230B9007222021A86AFEF7CCFB0722E0 -:10719000212100230400A86AFEF7C5FB2402204336 -:1071A00006F0ECF8982309905B00F85C06F030F9E3 -:1071B00007F00CFF0022974B07F090F8069007911C -:1071C000009806F025F907F001FF0022924B07F026 -:1071D00085F8069A079B07F0F3FA00228F4B06F01A -:1071E00057F90B9B040098100D0006F0C7F807F044 -:1071F000EDFE00228A4B07F071F802000B00200020 -:10720000290006F045F904000A980D0006F0B6F8CA -:1072100007F0DCFE0022834B07F060F802000B0051 -:107220002000290007F0CCFA040004980D0007F0B4 -:10723000CDFE0022784B07F051F802000B000698B3 -:10724000079907F0BDFA02000B002000290006F0A4 -:107250003BFC01F03DFB32230D000400FF33F85CE2 -:1072600006F0D6F807F0B2FE22002B0007F036F841 -:107270000022002304000D0005F04AF900284ED03A -:107280000022654B2000290006F002F907F0F0FE0D -:10729000011C099805F0EAF907F098FE0022624BFC -:1072A00006F0F6F8040002980D0007F08FFE0200C9 -:1072B0000B002000290007F011F800225B4B07F0BB -:1072C0000DF801F005FB3B6802900391DC68009A21 -:1072D000089B31003800A047002800D096E03C0011 -:1072E000FC34A06AFEF7AFFA0600A06A016EFEF752 -:1072F00085FA002862D1A06AFEF798FAA06AFEF724 -:10730000A2FA05000298039905F092F9AD1BA84274 -:10731000EBD23800FFF7E6FE0520404276E08221FE -:10732000C905B6E7002800D072E03C00FC34A36B2E -:10733000009A02933B683100DD683800089BA8473B -:10734000002863D1A06AFEF77EFA0600009BDD00EC -:107350003C00FC34A06A016EFEF750FA00282DD1E3 -:10736000A06AFEF763FAA06AFEF76DFA040028002F -:1073700006F04EF807F02AFE04900591029807F0F7 -:1073800025FE00222A4B06F0A9FF02000B000498FC -:10739000059906F099FB274B002206F09FFF05F0A8 -:1073A00047F9254BA41BC018A042D1D23800FFF7E3 -:1073B00099FE3B6838005B689847ADE73D00FC35B7 -:1073C000A86AFEF740FA0400009807F0CDFD0022FD -:1073D000114B06F083FF00900191A01B06F018F8F6 -:1073E00007F0F4FD0022154B06F06EFB0B000200C7 -:1073F0000098019906F068FB07F03AFE68643800CF -:10740000FFF770FE3B6838005B6898470DB0F0BD31 -:10741000012082E7000010400000204000003C40B6 -:1074200000003040000034400000114000709740E0 -:1074300000408F40D0125341404B4C0080842E417D -:10744000F0B50F0087B0012105000592FFF7B1FBF1 -:10745000041E55D128002E00FFF721FAFC368028A3 -:1074600056D1230007222421B06AFEF75CFAFF22DE -:10747000022302920193002845DDDB180093EF3AC6 -:1074800003334021B06AFEF77FFA98235B00EB5C80 -:107490000400062B0DD1F9330293FD3B0193002329 -:1074A00000933A0007332221B06AFEF76DFA0443D5 -:1074B00024B22B6801211B6C280098472800FFF795 -:1074C00011FE0026FF2302222F0002930192009654 -:1074D000FC37F83B32000F21B86AFEF755FAFF235C -:1074E00002220293019200960443F83B32000D21E0 -:1074F000B86AFEF749FA24B2044324B2B4421ED05B -:10750000200007B0F0BD0423220000930333B8E746 -:10751000002814D1FF230293FD3B01930433009311 -:10752000220001334021B06AFEF72EFA041EE7D193 -:107530002800FFF7D7FD059B1900052B07D02B006E -:10754000FC3301210022986AFEF79BFA0599280076 -:10755000FFF72FFB0400D3E7F0B50F0087B0012140 -:1075600004001500FFF725FB002821D12000FFF7BC -:1075700096F980281ED1E9B206222000FFF760FFAD -:10758000002815D12600FC36B06A016EFEF736F9E8 -:10759000002850D1B06AFEF749F9B06A816EFEF753 -:1075A0002DF90028EED02000FFF79CFD0620404278 -:1075B00007B0F0BD00283ED12600FC36B36BE9B21F -:1075C000052220000293FFF73BFF0028F0D1B06AAC -:1075D000FEF739F9EB00039001932600FC36B06A00 -:1075E000016EFEF70BF9002825D1B06AFEF71EF9EF -:1075F000B06AFEF728F90600019805F009FF07F0C8 -:10760000E5FC04900591029807F0E0FC00220D4B88 -:1076100006F064FE02000B000498059906F054FA87 -:10762000094B002206F05AFE05F002F8039BF61AF9 -:10763000B042D2D2B7E723682A001B693900200084 -:107640009847B5E700408F40D0125341F0B589B05C -:1076500006910121040004920793FFF7AAFA05900E -:1076600007002000FFF71BF90600802869D1049B62 -:10767000FF2B00D995E02500FC35002307222421AB -:10768000A86AFEF750F9FF22022302920193002814 -:1076900051DD4021DB180093AF3A0333A86AFEF7AF -:1076A00073F9236800211B6C200098472000FFF726 -:1076B00019FD6B4600271A7C0221FF232600029346 -:1076C00001910097FC36F83B2031B06AFEF75CF977 -:1076D000FF2302210293019100973A000500F83B35 -:1076E0000C31B06AFEF750F9FF230221059A0543D9 -:1076F0000293019100972A4315B2F83B3A000B31EF -:10770000B06AFEF741F905432FB225006B46FC3500 -:107710001B7C069A0021A86AFEF71DF901220021B0 -:10772000A86AFEF7AEF903212000FFF742FA3843BA -:1077300000B209B0F0BD06234022009311000133CE -:10774000ACE70028E1D1049B3F2B2AD8FF2325007A -:107750000293FD3B019304330093FC350133020097 -:107760004021A86AFEF710F92000FFF7BBFC462372 -:10777000FF33E35C802B05D16B4631001A7CA86A8D -:10778000FEF7F4F8302101230222A86AFEF7CBF8B5 -:10779000811EFD200140B8D1079AA86AFEF7E6F8DD -:1077A000B3E704204042C4E770B505000C00002C8C -:1077B00007D02B00FC330021986AFEF7A4F8013CA7 -:1077C000F5E770BDF7B5040001911500FFF767F804 -:1077D000236800905B6820009847236801219B6A1A -:1077E000200098470600002D04D0A8421AD9431B58 -:1077F0002E001D002700009BFC37B86A802B20D18B -:107800003A23FF33E35C002B0ED006231C211A0021 -:10781000FEF789F8002807D12000FFF763FC182045 -:107820004042FEBD0025E5E7052312211A00B86A93 -:10783000FEF779F8202814D12000FFF753FC072029 -:10784000EEE7009B002B0CD1013302223021FEF722 -:107850006AF8FD230238184203D10099B86AFEF78E -:1078600052F82000FC30F2B2019B0021806AFEF742 -:1078700040F8002D03D029002000FFF795FFA22338 -:1078800000255B002000E554FFF72CFC2800C8E72A -:107890001FB5FF230293FD3B01930023FC300093AF -:1078A0000A0007332421806AFEF76EF805B000BD98 -:1078B000FC3010B5002305221C21806AFEF733F846 -:1078C000C0B210BD70B50500FEF7E9FF80280CD1ED -:1078D0002B002800FC339C6AFFF744F90222024384 -:1078E000D2B212212000FEF741F870BD034B416077 -:1078F00003600023826083817047C0461037010017 -:1079000070B590256D000400405D00280FD1220065 -:10791000FC3253695B001943136A51611940936942 -:10792000994203D101236355A081A07370BD01204A -:10793000237B0F229840657B40B26519AA56002927 -:1079400010D002430133DBB2EA732373082BEDD16D -:10795000E87BFEF770F8E873637B01336373002301 -:107960002373E3E78243EDE7437A002B04D183726C -:10797000C372C36001334372704730B5040085B0F1 -:1079800002AD00680291039202F0F3FE6B79227A55 -:10799000E17900932068002302F08CFF2979029A94 -:1079A000206802F0B7FE206802F0D9FE05B030BDB5 -:1079B00070B518260400FFF7D7FF3300082120799F -:1079C0000D4D4343EB18595602F046FD3300082194 -:1079D000A0794343EB18595602F03EFD0821607927 -:1079E0004643AD19695602F037FD044B200006CB23 -:1079F000FFF7C3FF70BDC0467C3A01009032002003 -:107A000010B50400006802F09DFE0023637210BDF3 -:107A100007B5837A00910192002B07D09A070AD507 -:107A2000EFF310835A425341C37272B60099019A20 -:107A3000FFF7A3FF07BDDB07F8D5C368014A9BB278 -:107A40009360F3E700180040837A002B05D09A0773 -:107A500004D5C37A002B00D062B67047DB07FCD593 -:107A6000C368024A9BB2D360F7E7C04600180040E3 -:107A700010B5006802F086FE10BD0000084B094AF0 -:107A800009491A6000225A711A71084B1960084995 -:107A90005A7299800D21DA71033299711A72704706 -:107AA0009032002000093D00BC37002080320020C9 -:107AB0000C0B00001020704710B50400054BB021DE -:107AC000083303600C3000F094FA200000F076F8E0 -:107AD000200010BD4C37010010B50400FFF7ECFF8B -:107AE000200004F0F3FA200010BD70B5060000205D -:107AF000102A29D134000C34200007F0E8FB0125BE -:107B00000423AC36042B20D1210020002A000C31A4 -:107B1000103000F031FA237C227801355340237471 -:107B20006278637CEDB253406374A278A37C5340C7 -:107B3000A374E278E37C5340E374002301330434FC -:107B4000DBB2A642DED1012070BD2178227B4A4003 -:107B500022746178627B4A406274A178A27B4A40B9 -:107B6000A274E178E27B4A40E274E7E710B50400D2 -:107B700000F016F8044B2000083323600A23237119 -:107B800023000C33A36010BD4C3701001020704758 -:107B900010B5017980680131090100F02AFA10BDA1 -:107BA00010B5040000F016FA034B200008332360E0 -:107BB00000232371A36010BD7C39010010B50400BF -:107BC000034B0833036000F00BFA200010BDC046E1 -:107BD0007C390100184B0A789A5C02704A799A5CE9 -:107BE00042708A7A9A5C8270CA7B9A5CC2700A7907 -:107BF0009A5C02714A7A9A5C42718A7B9A5C8271C1 -:107C0000CA789A5CC2710A7A9A5C02724A7B9A5C60 -:107C100042728A789A5C8272CA799A5CC2720A7BD2 -:107C20009A5C02734A789A5C42738A799A5C82738E -:107C3000CA7A9B5CC37370477C380100184B0A7882 -:107C40009A5C02704A7B9A5C42708A7A9A5C827073 -:107C5000CA799A5CC2700A799A5C02714A789A5C15 -:107C600042718A7B9A5C8271CA7A9A5CC2710A7A82 -:107C70009A5C02724A799A5C42728A789A5C827241 -:107C8000CA7B9A5CC2720A7B9A5C02734A7A9A5CDB -:107C900042738A799A5C8273CA789B5CC37370471B -:107CA000743701001B23F7B50E784D787200171258 -:107CB0005F435740FAB294466F003A125A438C78A9 -:107CC0007A40D2B2670000923A125A43C9787A4099 -:107CD000D2B24F0001923A1253437B402F006246CA -:107CE00067404F405740009A754057400770370033 -:107CF00067404F405740019A69405740DBB24A40C5 -:107D00005A40827062466C40544063404770C37072 -:107D1000F7BDF0B500230E00846889B0E55CD15C46 -:107D200069406D4659550133102BF7D1077910344E -:107D300004AD69462800012F1FD9FFF74BFF29002A -:107D40006846FFF7AFFF05A901A8FFF7ABFF06A93B -:107D500002A8FFF7A7FF07A903A8FFF7A3FF0023C7 -:107D60006A46E15CD25C4A406946CA540133102B32 -:107D7000F6D1013F1034FFB2DAE7FFF72BFF002303 -:107D8000E15CEA5C4A40F2540133102BF8D109B0AF -:107D9000F0BD0000F0B50B788DB004934B780390E4 -:107DA00005938B780193CB7802936B461D8A1B2336 -:107DB00069000A125A434A40D2B206926A46948A2D -:107DC00061000A125A434A40D2B207926A46918829 -:107DD0004A0016125E435640F2B208926A46128971 -:107DE000560037127B437340DBB20993A3009C46D5 -:107DF00066466046394B36129E5DAF004640F0B293 -:107E00000A90880006129E5DED004640F0B20B908D -:107E1000900006129E5DE4004640F0B22E129E5D78 -:107E20008446754026129E5DC90074400E129E5D08 -:107E3000D200714016129E5D019872403E129B5D09 -:107E4000E4B25F40059BC9B243400298D2B24340BE -:107E50000B98EDB24340634006984B40534043407B -:107E600007986F40FFB2434003987B40037001982E -:107E7000049B029E4340029843400A984340604658 -:107E800043406B40634007984B4053404340089841 -:107E90004340039843700598049B43400B985E4011 -:107EA0004640664008984E40564046400998464035 -:107EB00003987740877001980A9E43405E4063466E -:107EC0005E4075406C40069B61404A405A40099BA9 -:107ED0005A40039BDA700DB0F0BDC0467438010003 -:107EE000F0B58BB001910479836824011C19050059 -:107EF0000023E05CD15C414002A819540133102BEF -:107F0000F7D106AF01003800FFF798FE2D79230066 -:107F1000103B00930023012D21D906AE1A00103A20 -:107F2000995DA25C4A409A550133102BF6D131007D -:107F300002A8FFF72FFF07A903A8FFF72BFF08A947 -:107F400004A8FFF727FF09A905A8FFF723FF013DB4 -:107F500002A93000FFF772FEEDB2009CD7E71A00CD -:107F6000103AF95CA25C4A400199CA540133102BC3 -:107F7000F5D10BB0F0BD000030B5084B4C78084D82 -:107F80001C5DAA5C624002708A789A5C4270CA7872 -:107F90009A5C82700A789B5CC37030BD7C380100AB -:107FA000A439010010B5040000F01CF8024B2000B9 -:107FB0000833236010BDC046B039010010B504007D -:107FC000034B0833036000F013F8200010BDC046D7 -:107FD000B0390100014B083303607047EC390100F0 -:107FE00070470000014B083303607047143A0100EA -:107FF000704700234118884202D003700130FAE72D -:108000007047002370B51C009A4205D0C55CCE5C59 -:10801000013375402C43F7E780204000001BC0056A -:10802000C00F70BD10B540680368DB68984710BD8D -:108030000C2070471020704710B5406803681B691A -:10804000984710BDC37B827B0133C3731B0A9B1807 -:10805000427B83731B0A9B18027B43731B0A9B188A -:1080600003737047F0B51F000300050087B00833A5 -:1080700005910192039370352B780400002B04D1F5 -:10808000039800F07CFA01232B703D00059B0093C0 -:10809000002D34D02300260040330493713633780A -:1080A0000F2B0DD92300303318000293FFF7CAFFBE -:1080B0006068029A036804995B69984700233370EB -:1080C0002000102271300378D21AD2B2AA4200D90D -:1080D000EAB20499AD1AC9189B18037000239342A1 -:1080E00005D1019AD2180192009AD318CFE70198CE -:1080F000CE5CC05C7040009EF0540133EFE73A0064 -:108100000599039800F00CFAA06EE16E3A002B007E -:1081100012184B41A266E36607B0F0BDF0B5070048 -:10812000060085B00291019270371D003B78040073 -:108130000836002B04D1300000F021FA01233B70F7 -:108140002A000199300000F0EBF9A06EE16E2A00E0 -:10815000002312184B41A266E366002D34D02300A1 -:1081600027004033039371373B780F2B0CD926003F -:1081700030363000FFF766FF60683200036803990D -:108180005B69984700233B70200010227130037810 -:10819000D21AD2B2AA4200D9EAB20399AD1AC918CA -:1081A0009B1803700023934206D1029AD2180292C0 -:1081B000019AD3180193D0E70198CE5CC05C70405F -:1081C000029EF0540133EEE705B0F0BD0300F0B5B8 -:1081D0000500060089B03033040003910192503548 -:1081E000083602930C2A27D1029807F070F88023F2 -:1081F0005B04E3630022002322666366A266E366F3 -:10820000230000217033103219705A70280007F0D3 -:108210007AF860682A00036829005B69984729009A -:10822000300000F06FF96068029A036829005B690A -:108230009847012009B0F0BD4768102200212800AE -:1082400007F061F83B682A005B692900380098470D -:108250002900300000F056F9019A0399300000F02F -:108260005FF9300000F08BF90822002104A807F024 -:108270004AF8019B04A95A0F1206DB001BBA0692AA -:1082800030001022079300F04BF904A81021FFF7EB -:10829000B0FE10220299300000F07DF9AAE710B577 -:1082A000040040680368DB6998472000083000F04C -:1082B00082F9200048213030FFF79BFE10237134F3 -:1082C000237010BD030070B570331D7804001600D4 -:1082D000002D0AD1083000F023F9206E616E3200C3 -:1082E0002B0012184B412266636670BDF8B503007F -:1082F0000D0014000020102A0CD81E001A68303619 -:10830000D76A310010221800B84722002900300037 -:10831000FFF777FEF8BD000010B50400074B4821B9 -:10832000083303603030FFF764FE2000083000F0AF -:10833000E2F82000FFF742FE200010BD403A0100A5 -:1083400010B50400FFF7E8FF200003F0BFFE200097 -:1083500010BDF0B5040007002600083787B03800CC -:108360000191150000F00BF9226E636E510FDB00D6 -:108370000B43D20012BA1BBA02930392E36EA26EB1 -:10838000DB00510FD2000B4312BA1BBA059202A9AF -:10839000102238000493403600F0C2F8102102A8E1 -:1083A000FFF727FE10223100380000F0F4F8330008 -:1083B00050341A78197C4A401A700133A342F8D11C -:1083C0002A00102D00D910223100019806F07FFFFD -:1083D00007B0F0BD70B504000025FFF7E3FD2000F5 -:1083E000094B656008332360083000F080F80022F4 -:1083F000002322666366A266E366230010327033B0 -:108400001D7020005A7070BD403A010010B5040084 -:10841000102206F05CFF23681BBA236063681BBA56 -:108420006360A3681BBAA360E3681BBAE36010BD76 -:108430000300F0B5CA680E684D688C68002189B0E9 -:10844000103300900793059104910391009B1B78D2 -:108450000693082302933700069BDB115B421F4003 -:10846000BC4601936346039F5F4003972F00019B27 -:108470001F40BC466346049F5F4004972700019B52 -:108480001F40BC466346059F5F40019B0597134014 -:1084900059400123E12713403F065B423B4052080D -:1084A000E7073A436408EF073C43F70776085E4066 -:1084B000069B6D083D435F00FBB20693029B013BA8 -:1084C000DBB20293002BC6D1009B079F01330093C0 -:1084D000BB42BBD1039B1BBA0360049B1BBA436026 -:1084E000059B1BBA83600BBAC36009B0F0BD0022C4 -:1084F000431CDA77704710B504002421FFF779FD9B -:10850000200010BD10B50400FFF780FF20001022EE -:108510000021103006F0F7FE00230134E37710BD90 -:10852000F7B506000F001400002C27D01023751C8F -:10853000E97F5B1ADBB2A34200D9E3B2300010300E -:10854000411800228C460190D1B28B4207D9614676 -:10855000B85C895C4140604681540132F4E7EA7FAF -:10856000E41A9A18D2B2EA77FF18102ADCD1310047 -:108570000198FFF75DFF0023EB77D5E7F7BD10B556 -:10858000441CE37F0100002B04D01030FFF750FFA4 -:108590000023E37710BD70B5140005000E00FFF74F -:1085A000EEFF2200102C00D91022290030001031DB -:1085B00006F08DFE70BD10B52421FFF71AFD10BD29 -:1085C00010B504004021FFF714FD20004021403089 -:1085D000FFF70FFD200010BD0300802280339201C1 -:1085E0001A800200002308320249D36780321160EA -:1085F0009382704780EE360010B5054C2000FFF7DF -:10860000EBFF044A0449200006F0D4FD10BDC0462B -:108610009832002000000020C18500007047000053 -:1086200070B500290FD10423314A3249D367C022E3 -:1086300052008B500221304B1A788A431A70DA7933 -:10864000D209FCD170BD0F230A001A40C2710A0979 -:108650001A4082710A0A13400A0B0124150009040A -:10866000490F25408C4021000460244C4371216057 -:1086700002211B021943224B05719960214959605F -:108680005978C909FCD12049194C59800421E16766 -:10869000C024C025174B64001951AD005C591B4E16 -:1086A000344001265C511960134B32405C7915004F -:1086B00022000F24B2432A435A719A78C579A24303 -:1086C00025402A439A705A788079A2432040024379 -:1086D0005A701A788A431A70DA79D209FCD119785B -:1086E00002320A431A70DA79D209FCD1AAE7C046ED -:1086F00004E100E000E100E0001000403833002019 -:10870000000C00400203110003420000FFFF00FFC5 -:10871000A522044B1A72DA79D209FCD10268024B05 -:108720001A60704700100040383300200C4A10B522 -:1087300013680C4C013B1360002B07DC0A4B1B68D1 -:10874000002B00D09847FF232372FEE70123A27974 -:108750001343A371A5232372E379DB09FCD110BD78 -:1087600038330020001000403C33002010B50400D6 -:10877000217A00784B1E9941490001F0DDFD2078F7 -:1087800001F052FE03005A1E9341A372637A002B3C -:1087900002D043425841A07201F0BAFBA37A206193 -:1087A000E37200236061237310BD70B5040001F013 -:1087B000AFFB0500207801F037FE637A002B0CD167 -:1087C000431E984163696268C0B2EB1A934207D2B4 -:1087D000002323732561A07A70BD43425841F1E71D -:1087E0000200A37AA0725A40E37222739842F1D039 -:1087F0006561EFE70023827A9A4200D1037B18007B -:108800007047837A002B06D043690269D21A00238D -:108810008A425B41DBB218007047000010B5024885 -:1088200003F037FB10BDC0467C35002010B5024870 -:1088300003F02FFB10BDC04640330020F7B50124E4 -:108840000326154F1549380001F0E6FE1449154876 -:1088500001F0E2FE1449154801F0DEFE1449154806 -:1088600001F0DAFE1449154801F0D6FE144D154901 -:10887000280001F0D1FE3900009623000194002267 -:10888000114803F09BFA0096290001941E231F2231 -:108890000E4803F093FAF7BDB837002000080042F5 -:1088A000000C0042BC37002000100042C0370020FE -:1088B00000140042C437002000180042C8370020CE -:1088C000CC370020001C00427C35002040330020C3 -:1088D000434AF0B54F4691464B005B0D99444646DE -:1088E000D6464A46C0B505000C000E008846070073 -:1088F000132A25DC002A56DB3A4B13419A460B001B -:1089000052461340034316D0374A384B04F0C0FD9B -:108910000022002303F0FCFD00280AD0002C04DD17 -:1089200080234A465B03134198444646534600273A -:108930009E43380031001CBC90469946A246F0BDCB -:10894000332A08DD8023DB009945F4D102000B00B7 -:1089500004F09EFDEFE7264A94460122634452420A -:10896000DA4092460242E6D01F4A204B04F090FDC6 -:108970000022002303F0CCFD0028DAD0002C0EDD0D -:108980004B46142B22D034234A4601249B1A9C4088 -:108990006519BD4222D201239C462F00E044534674 -:1089A00046469F43C5E7104A104B04F071FD002274 -:1089B000002303F0ADFD0028BBD0002C0ADB2C43C4 -:1089C00000270026002CB4D00A4EB2E7133B9C4689 -:1089D000E044E4E7802600273606AAE72F00DEE71A -:1089E00001FCFFFFFFFF0F009C7500883CE4377E11 -:1089F000EDFBFFFF0000F03FF0B5C64643005F0807 -:108A00001B0E7F3B00B5051C06001C00162B1CDC52 -:108A10008046002B21DB1A4F1F41074211D0194914 -:108A200003F024FE002103F0ADFD002809D0002D45 -:108A300004DD80231B0423419846A8444346BB43DE -:108A40001E00301C04BC9046F0BD0F4B9F42F8D96D -:108A5000011C03F00BFE061CF3E70A4903F006FEB7 -:108A6000002103F08FFD0028EBD0002D04DB002F48 -:108A7000E7D0FE26B605E4E780263606E1E7C046E5 -:108A8000FFFF7F00CAF24971FFFF7F7FF0B55746B5 -:108A90004E46DE464546E0B58FB01D0006000F008D -:108AA000140000F0ADF9CE4B009001919A461B786E -:108AB0005BB2994601333ED022002B0020002900F2 -:108AC00006F0C6F98346002835D132003B0030005D -:108AD000390006F0BDF900220390002800D06FE0B5 -:108AE00000233000390003F0F9FC00282CD00022CC -:108AF00000232000290003F0F1FC8346002800D168 -:108B000092E004AB9846012342460493B54B536070 -:108B10004346039A1C615D611A629E60DF604246B3 -:108B2000002300249361D4614B46002B67D00023BF -:108B3000AD4C00930194009801990FB03CBC904655 -:108B40009946A246AB46F0BD0098019900F09AFF05 -:108B50008146002800D19AE0009801990022002364 -:108B600003F0BCFC0028E6D03000390000F08AFF9A -:108B70000028E0D02000290000F084FF0028DAD08F -:108B800004AB9846042342460493964B1461556106 -:108B9000536000230024136200239361D461534681 -:108BA0001B789660D7605BB2022B04D0404600F081 -:108BB00073FF002829D106F013FB2223036024E071 -:108BC00000232000290003F089FC0028B3D004AB67 -:108BD0009846012342460493824B414653604346E4 -:108BE0005A461A6200229E60DF601C615D617E4B06 -:108BF0008A61CB61009201934B46022B9BD0404689 -:108C000000F04AFF002800D18AE043461B6A002B8F -:108C100004D006F0E5FA43461B6A03604346DC696C -:108C20009B690093019486E72000290000F02AFF49 -:108C3000002800D17FE7002200232000290003F054 -:108C400053FC002800D176E704AB98460123424646 -:108C50000493644B536043465A469E60DF601A6239 -:108C60001C615D6153461B785BB2002B5DD0002018 -:108C700042465E499061D161022B5BD106F0B0FAA9 -:108C80002123036043461B6A002BC7D0C1E7300095 -:108C9000390000F0F7FE002800D15DE72000290030 -:108CA00000F0F0FE002800D156E7009801995346E5 -:108CB0001B7802005BB29B460B0006F0C9F804ABC0 -:108CC0009846002862D1032342460493454B146121 -:108CD0005561536010629660D760454B20002900B3 -:108CE000002205F0FBFA5B4604000D00002B27D1A3 -:108CF000E02241463F4B12068A61CB6130000022E0 -:108D00000023390003F0F0FB00282AD1404600F090 -:108D1000C3FE0028B6D106F063FA22230360B1E750 -:108D200006F05EFA212303606FE742460023002429 -:108D30009361D461404600F0AFFE0028A2D19DE7C8 -:108D4000002241462C4B30008A61CB610022002377 -:108D5000390003F0C9FB002835D15B46022BDAD07D -:108D6000D4E72000290000F09FFE22002B0003F032 -:108D7000B5FB002805D1E022204B120641468A614E -:108D8000CB6153461B785BB29B46E6E70123424624 -:108D90000493144B536043464A469E60DF601A6258 -:108DA0001C615D615B46002BBFD0002300221900CF -:108DB000100004F089FE43469861D9615B46022B9E -:108DC00000D15BE7B6E72000290000F06DFE22002D -:108DD0002B0003F083FB0028D3D10022034BCDE707 -:108DE000B40000209C3E01000000F03F0000F0FFB6 -:108DF0000000E03FFFFFEF470000F07FFFFFEFC7FD -:108E0000F0B55746DE464E4645465C006408E0B580 -:108E100098461F00230093B083468A4616001343EA -:108E20000CD07D4B4D00029181466D089D4212DDB4 -:108E30007A4B9C465B4665441D4361D10023784CC8 -:108E4000009301940098019913B03CBC9046994658 -:108E5000A246AB46F0BD9D4250D09C42E8DC6E4B32 -:108E60009C426BD0002304935346002B69DB002AFD -:108E700010D1694B9C424CD0694B9C4200D1A3E07D -:108E80008023DB05984500D1BCE0664B984501D1B5 -:108E900000F0D6FB5846514600F0F0FD4B460090DE -:108EA0000191002B07D1002D67D053465C4A9B00EF -:108EB0009B08934261D05346DB0F013B99464A46DB -:108EC000049B134300D190E0574B9C4200DCBBE075 -:108ED000564B9C4201DC00F010FC554B9D4201DCDE -:108EE00000F004FC4346002B23DD524A524B100095 -:108EF000190005F0F3F900900191A3E7002805D0CF -:108F00004E4800F0CBFD009001919BE7AC42F7DCAE -:108F1000A5E75A46414BEB18134300D18EE7444B6B -:108F20009D4200DC99E3434600960197002B00DB4D -:108F300088E7002300240093019483E7002A91D05E -:108F400076E73F4B9C423CDC394B9C428FDD3D49F0 -:108F500023158C466344142B00DCB2E33421CB1A76 -:108F60003100D94008009840904200D07FE70123AB -:108F7000194001335B1A049379E74346002B07DA63 -:108F8000009A019B0020264904F09EFD009001916B -:108F9000029B002B00DB55E7204B9C46049B65445D -:108FA0001D4301D100F00EFC049B012B00D049E7CA -:108FB000009C019D8021220009066B1800920193FC -:108FC00040E70223049352E7584651464346009037 -:108FD0000191002B00DB35E75A465346002010492B -:108FE00004F072FD009001912CE75A465346584612 -:108FF000514605F0E5FB02000B0004F065FD009012 -:1090000001911FE75A4653465846514605F066F906 -:109010000090019116E7C0460000F07F000010C0EC -:109020000000F03F0000E03F0000E0410000F0439E -:10903000FFFFEF3F9C7500883CE4377EA03E0100B7 -:10904000FFFF3F4301FCFFFFE44B00229D420ADC8F -:10905000009801990022E24B05F040F935220D00FD -:10906000009001915242DF492B158C46634498468B -:109070002D03DD4CDD4B2D0B90442C439D420ADD2E -:10908000DB4B9D4201DC00F001FC01239C46D94BE7 -:10909000E0449C466444002200230E920F931092F9 -:1090A000119300220025D04B069207930098019956 -:1090B000069A079B21000890099105F081FB069A0A -:1090C000079B0A900B910898099904F0E1F90200B6 -:1090D0000B000020C44904F0F7FC0A000100130053 -:1090E0000C910D9202000A980B9905F0F7F80A000E -:1090F0000100130002910392002200920193802349 -:1091000064109B05234380242403A4460020634469 -:10911000591904000D0002000B000098019905F098 -:10912000DDF802000B000A980B9905F049FB069A3E -:10913000079B0A900B912000290005F041FB0200DB -:109140000B000898099905F03BFB009A019B05F07C -:10915000C5F802000B000A980B9905F031FB0C9A38 -:109160000D9B05F0BBF8029C039D0890099122001D -:109170002B002000290005F0B1F804000D009E4AE4 -:109180009E4B05F0ABF89E4A9E4B04F081F92200FD -:109190002B0005F0A3F89C4A9C4B04F079F92200BF -:1091A0002B0005F09BF89A4A9A4B04F071F92200C3 -:1091B0002B0005F093F8984A984B04F069F92200C7 -:1091C0002B0005F08BF8964A964B04F061F92200CB -:1091D000069007912B002000290005F07FF802007F -:1091E0000B000698079905F079F8009A019B040096 -:1091F0000D000298039904F04BF9089A099B05F0B9 -:109200006DF822002B0004F043F9009C019D0A90A8 -:109210000B9122002B002000290005F05FF80022AE -:10922000814B04000D0004F033F90A9A0B9B04F003 -:109230002FF900220B0011001A00069107920A0074 -:109240000098019905F04AF800220C900D910698BB -:109250000799754B05F0B4FA22002B0005F0B0FA1F -:1092600002000B000A980B9905F0AAFA029A039BD8 -:1092700005F034F8069A079B04000D000898099938 -:1092800005F02CF802000B002000290004F000F982 -:1092900004000D0002000B000C980D9904F0F8F882 -:1092A00000221000E022614B12060090019105F0AF -:1092B00015F80C9A0D9B029003910098019905F006 -:1092C0007FFA02000B002000290005F079FA584AC5 -:1092D000564B05F003F8574A04000D000098019919 -:1092E000554B04F0FBFF02000B002000290004F0A6 -:1092F000CFF8109A119B04F0CBF8069007914046E6 -:1093000005F0F8FD069A079B04000D0002980399EA -:1093100004F0BEF80E9A0F9B04F0BAF822002B005E -:1093200004F0B6F8002022002B000090019105F017 -:1093300047FA0E9A0F9B05F043FA029A039B05F039 -:109340003FFA02000B000698079905F039FA0690DB -:1093500007914A46049B013B134300D188E1002357 -:10936000214C0293039400223B003000390014008A -:1093700005F026FA009A019B04F0B0FF32003B0092 -:10938000049005910698079904F0A8FF02000B00CD -:109390000498059904F07CF83D0006000F00009841 -:1093A000019922002B000496059704F097FF020014 -:1093B0000B00300039000092019304F069F81F4B54 -:1093C00004000D008B460200994200DC55E11C4B65 -:1093D000CB18034300D0FBE134E0C046FFFF0F0091 -:1093E0000000404301FCFFFF0000F03F8E980300A7 -:1093F00079B60B000000F0FFEF4E454A287ECA3FC9 -:1094000065DBC9934A86CD3F01411DA96074D13FF8 -:109410004D268F515555D53FFFAB6FDBB66DDB3F0A -:10942000033333333333E33F0000084009C7EE3FD3 -:10943000FD033ADCF5015B14E02F3EBEFFFF8F40D9 -:10944000000070BF04980599E84AE94B04F020F841 -:10945000009A019B06000F002000290005F0B0F9DA -:1094600002000B003000390003F052F8AA46002831 -:1094700000D0ADE18024DF4A5346944664032200C5 -:109480001B1563441A411300DB495B445A001E005C -:109490001B038C461B0B23431424520DD74D6244EF -:1094A0001541A21A134199465B46AE430020310094 -:1094B000002B02DA4B465B42994602000B000098F3 -:1094C000019905F07DF94B461B05009001910200C2 -:1094D00098460B000498059903F0DAFF0D0000246C -:1094E0000022C74B2000290004F0F8FE009A019BDF -:1094F000069007912000290005F062F902000B0098 -:109500000498059905F05CF9BE4ABF4B04F0E6FEED -:10951000BE4A06000F00BE4B2000290004F0DEFE0C -:1095200002000B003000390003F0B2FF04000D0010 -:1095300002000B000698079903F0AAFF069A079B02 -:1095400006000F0005F03CF902000B002000290086 -:1095500005F036F93200009001913B0030003900EF -:1095600004F0BCFE04000D00AA4AAB4B04F0B6FEAA -:10957000AA4AAB4B05F024F922002B0004F0AEFE02 -:10958000A84AA94B03F084FF22002B0004F0A6FE9A -:10959000A64AA74B05F014F922002B0004F09EFE0A -:1095A000A44AA54B03F074FF22002B0004F096FEA2 -:1095B00002000B003000390005F002F904000D0034 -:1095C00002000B003000390004F088FE8023049074 -:1095D00005910022DB052000290005F0F1F80200CA -:1095E0000B000498059904F06FFA009A019B04009F -:1095F0000D003000390004F071FE009A019B03F069 -:1096000047FF02000B002000290005F0D9F83200C6 -:109610003B0005F0D5F802000B000020874905F05B -:10962000CFF843465B181A15002A00DC3FE1190009 -:10963000029A039B04F052FE00900191FFF702FC96 -:109640005346002B01DAFFF725FC5846514600F03F -:109650003DF900900191FFF7F5FB4346002B00DB3D -:1096600067E480231B06FB1800960193FFF7EAFBD3 -:109670000023734C0293039475E64B005B089A46F3 -:10968000704B9A4500DCF9E06F4B5B441A430AD1FA -:10969000009A019B05F094F8049A059B02F042FFA2 -:1096A000002800D1E6E602980399684A684B04F066 -:1096B00015FE664A664B04F011FE00900191FFF71B -:1096C000C1FB002A01D0FFF7E5FB1432D31A2200B8 -:1096D0001A4111009940A14201D0FFF7CDFB0123AF -:1096E0001A40DB189B1A0493FFF7C6FB4346002B76 -:1096F00001DBFFF71EFCFFF7F8FB564B9D42F5DD43 -:109700004E4B9D4201DDFFF7EDFB009801990022D1 -:109710004A4B05F055F8C0224F4BD20504000D000E -:1097200004F0DCFD4D4A029003914D4B20002900CE -:1097300004F0D4FD0022009001914A4B2000290042 -:1097400004F0CCFD02000B004748484905F038F80A -:1097500022002B0004F0C2FD02000B00002044494F -:1097600005F02EF82200069007912B00200029001A -:1097700004F0B4FD02000B000698079904F0AEFD5A -:109780001A4A354B04F0AAFD02000B00009801991B -:1097900005F016F802000B0004000D000298039972 -:1097A00003F076FE029A039B00200090019105F0E1 -:1097B00007F802000B002000290005F001F80690D0 -:1097C0000791C6E50098019902000B00FFF711FC14 -:1097D000284A294B0298039904F080FD254A264B1C -:1097E00004F07CFD00900191FFF72CFBFE822B65BD -:1097F0004715973C02FCFFFF01FCFFFFFFFF0F0036 -:10980000432EE63FEF39FAFE422EE63F396CA80CB4 -:10981000615C20BED0A4BE726937663EF16BD2C5D2 -:1098200041BDBB3E2CDE25AF6A56113F93BDBE162F -:109830006CC1663F3E5555555555C53F0000F03F3C -:109840000000F0BFFFCB904000346F3F59F3F8C2E7 -:109850001F6EA501FEFFEF3F4715F73F44DF5DF8A0 -:109860000BAE543E0000D03F555555555555D53F8C -:109870000000E03F9C7500883CE4377E00220E4BE0 -:10988000904691469A4500DC29E6F3E580220B4B91 -:10989000D2050E920F930A4A0A4B8025109211931B -:1098A0000022094BED0206920793FFF7FFFB4A46A1 -:1098B00000F08AF9BCE6C0460000E03F03B8E23F92 -:1098C00006D0CF43EBFD4C3E0000F83FF8B54746CD -:1098D000CE46674B80B50A000F00190005003940DD -:1098E000994200D19BE004000100002F69DD3F1583 -:1098F00000D1B4E05F4B12039C468023120B5B0344 -:1099000013435B00CA0F67449B184A00F80703D54E -:10991000D20F5B009B188A0079108C4600218020B2 -:109920001624894680030D189D4202DC29185B1B12 -:109930008144D50F5B00013C5B1952004008002CAC -:10994000F1D100208046802020250027000609E074 -:109950008B425CD0D40F5B00013DE31852004008FD -:10996000002D16D0C4198B42F2DD27180E00002CF2 -:1099700049DB5B1AA242894149425B1A121BD40F90 -:109980005B00013D80443100E31852004008002D87 -:10999000E8D113434ED143465B0837494A468846CF -:1099A000494652104244C90702D5802109060B439B -:1099B00061460F05BD18180029000CBC9046994659 -:1099C000F8BD7B005B0803433900002BF5D0002F66 -:1099D0003CD1E30A153A6405002BFAD080204003FD -:1099E00003423FD1002700E00F005B00791C0342D7 -:1099F000FAD0250020208D40401A2900D71B2200D4 -:109A0000C2401A4376E7FE43F60F8E19B1E794423F -:109A1000A0D82718002CF6DB1E000023AEE70200BA -:109A20003B002800390004F059FC2A003B0003F0F9 -:109A30002FFDC2E74346013303D0434601335B08A1 -:109A4000ABE7012398460023C144A6E73B00020090 -:109A500004F0B6FE02000B0004F036F8ADE7130088 -:109A60000022BBE70127210020207F42C6E7C04635 -:109A70000000F07F01FCFFFF0000E03F49004B08C1 -:109A800019007047034B48009C4640086044C00FD3 -:109A90007047C04600001080002070470020014938 -:109AA0007047C0460000F87FF0B5C64600B5CB0F42 -:109AB0009846404A4B005B0D9A1882B00D00040096 -:109AC000132A2FDC002A49DB3B4F17413E000E4092 -:109AD000064323D07F08390029400C430AD0374978 -:109AE000BD438C4663445C425C418023DB021341EE -:109AF000E4071D434146CE0031492B008E19220058 -:109B0000346875682000290003F0C2FC00900191C0 -:109B10000098019922002B0004F052FE02B004BC10 -:109B20009046F0BD332A08DD8023DB009A42F5D150 -:109B300002000B0003F0ACFCF0E7224A944601223D -:109B400063445242DA400242E8D052080242D1D085 -:109B500094438022D2051A411443CBE74B005B08A3 -:109B60000343DBD00B031B0B1C43634223438024C2 -:109B70004D0C24031B0B23406D042B43190043465B -:109B8000DA000F4B9B181C685D6802000B00200078 -:109B9000290003F07DFC00900191009801992200BA -:109BA0002B0004F00DFE42464B005B08D607334302 -:109BB0001900B3E701FCFFFFFFFF0F00EEFBFFFF03 -:109BC000A83E0100EDFBFFFF4B0070B55B0D1400DC -:109BD0000A00002B1AD14B005B08034315D0234B1E -:109BE000002204F07BFB224B9C422DDB0A004B0041 -:109BF0005B0D363B1F4DE318AB422ADC002B0DDD1D -:109C00001D4C1B0522401A43110070BD1B4DAB4279 -:109C1000F0D102000B0003F03BFCF6E71D003535E8 -:109C200019DA174B02009C4209DD0B001548164952 -:109C300000F032F8134A144B04F050FBE5E70B0038 -:109C40001248134900F028F8104A114B04F046FB63 -:109C5000DBE70200E9E7084C363322401B051343DB -:109C6000190000220B4B04F039FBCEE700005043F3 -:109C7000B03CFFFFFE070000FFFF0F80FF07000062 -:109C800050C300009C7500883CE4377E59F3F8C24D -:109C90001F6EA5010000903C30B54A00DB0FDB07CA -:109CA00052081A43110030BDF7B50192182216244C -:109CB00042433F4B07009B181C570091631C6ED01A -:109CC000102C6CD03B4B3C4E1D68002D27D1290039 -:109CD00044223A4805F017FB29004422384805F091 -:109CE00012FB1022374B38491D60CA67C021C120C2 -:109CF000364B49005A5080001958FF35A943195076 -:109D00001A60334B334A5A8002233278134333703C -:109D10007378DB09012BFBD00122264B1A60012549 -:109D2000A540736900212B437361380000F094FB58 -:109D3000009B002B31D00027224B20499C461B68FA -:109D4000BA00BB421DD16046013303608D501C4BED -:109D500000999950072C19DD0123083CA400072124 -:109D6000A140154A9B009B189A698A439A61019AFF -:109D7000042A12D81000996902F00CFA0B1214167A -:109D800018008858A842E1D00137D9E7A400002381 -:109D9000E5E70522A2400A439A61ADB2F560F7BD3E -:109DA0000422F7E70322F5E70222F3E70122F1E7B5 -:109DB0007C3A010058380020001800401438002078 -:109DC000D03700205C38002004E100E000E100E032 -:109DD000000C00400540000018234343164870B5AE -:109DE000C0181623C3565A1C0FD0102B0DD00120BB -:109DF0009840124B82B29A605A69114D82435A615F -:109E0000002229680F4C914200D170BD93001E5969 -:109E1000864205D10C480139914203D82960F4E704 -:109E20000132F0E7E61876680132E650C618766827 -:109E30001E500433F0E7C0467C3A01000018004091 -:109E40005C38002014380020D0370020F8B50024FA -:109E5000094B1B68A34200D8F8BD084E084FA50067 -:109E60003369EA591A4204D0064BEB589847EB592C -:109E700033610134ECE7C0465C3800200018004034 -:109E800014380020D0370020FEE70000034B10B547 -:109E90001B68002B00D0984710BDC04660380020DA -:109EA00070B50F490F4CA14204D100F083F802F0C5 -:109EB000E9F8FEE70C4D00238D42F6D0CA18E818E9 -:109EC000A24205D3094B8242EFD00949002204E0A7 -:109ED000006804331060F1E704C38B42FCD3E4E76D -:109EE000000000203C030020B44301004003002098 -:109EF000783E002010B500F05AF8002801D100F09B -:109F00004BF810BD014B18607047C0466038002008 -:109F1000014B18687047C04664380020F7B51348F5 -:109F2000012284461249134C8E68636805689B0EB3 -:109F30001340019288686268019F920E3A4067461A -:109F40003F6893420ED1BD420CD186420AD35B19C1 -:109F5000FA254868AD00861B07486B437043000D27 -:109F6000C018FEBD3D0013000600E3E76438002082 -:109F700010E000E000ED00E05555000070B5041E53 -:109F800007D0044D2E6800F011F82B689B1BA342EC -:109F9000F9D370BD6438002010B5034A136801334B -:109FA000136000F007FB10BD6438002070470020EC -:109FB000704700001E2270B54648474943689343E6 -:109FC0001C3A1343436008238C6923438B61434C41 -:109FD000434B9C829C8A14439C82DC681442FCD0D4 -:109FE0000124404A14701578254203D055786DB28B -:109FF000002DF8DB01249460547864B2002CFBDB64 -:10A00000394C5460547864B2002CFBDB8224E401A8 -:10A010005480547864B2002CFBDB02249C840E3400 -:10A02000DD682542FCD0314CDC621024DD6825421D -:10A03000FCD09D8C2E4C2C439C841024DD68254242 -:10A04000FCD002249D8C80262C439C844024DD6817 -:10A050003542FCD0DD682542F9D01024DD68254268 -:10A06000FCD000249460547864B2002CFBDB214CBB -:10A070005460547864B2002CFBDB1C6A1E4D2C40EB -:10A0800080251C621C6AAC431C62032393601B4B3B -:10A09000536053785BB2002BFBDB0023FF240B7271 -:10A0A000174A4B728B72CB72164B1A60164B174ABB -:10A0B0001B6811689A065B01C90E2340520F1202F9 -:10A0C0000B431343124A13858023426813434360B2 -:10A0D00070BDC04600400041000400400C06000076 -:10A0E00000080040000C004001050100B905FF7D9B -:10A0F000040A000000070300FFFCFFFF0306010045 -:10A10000006CDC02B8000020246080002060800029 -:10A1100000400042FA212D4B10B51868890002F06A -:10A1200057F82B4B013898424FD82A492A4A4860A1 -:10A13000C020136A00061B021B0A0343136200239C -:10A140008B6007330B608021136A09061B021B0A10 -:10A150000B431362FC22214B196A0A431A62FC2248 -:10A16000196A92010A431A62A022196AD2020A43AA -:10A170001A621B4B1C005A7852B2002AFBDB194AA8 -:10A180005A80194B1A00597EC9090129FBD0E421D4 -:10A19000C90099803F21D970537EDB09012BFBD088 -:10A1A000C02300205B011361907000F02DF86378EC -:10A1B0005BB2002BFBDB0D4B0D4A6380D379DB09CF -:10A1C000012BFBD04123537010BDFEE7B8000020E7 -:10A1D000FFFFFF0010E000E000ED00E000040040A1 -:10A1E000000C00401E4000000040004221400000E2 -:10A1F00000480042914202D002D98A1AD0407047EA -:10A20000521A9040FBE70000164A00B51300517E39 -:10A21000C9090129FBD00138042816D81969124A46 -:10A220000A400F211A615A788A4301F0B3FF0308EC -:10A23000060B0300110001220A435A7000BD1100F1 -:10A240000322F9E711000222F6E7F02311691B054A -:10A250000B430F21136153788B43190002230B43E7 -:10A260005370EBE700400042FFFFFFF010B5040021 -:10A270000D2800D80E340121200000F0EDF80E2C3E -:10A280000DD1284B1900DA79D209012AFBD002201E -:10A290001A7882431A70CB79DB09012BFBD0224953 -:10A2A0000B004A7ED209012AFBD0182262431F4CC0 -:10A2B000A418207C1F240A692040A24302430A619B -:10A2C0005A7ED209012AFBD0022219780A431A7059 -:10A2D0005A7ED209012AFBD00222197B0A431A7343 -:10A2E00001221A765A7ED209012AFBD00222197B5A -:10A2F0000A431A731A7ED207FCD5588B80B25A7E55 -:10A30000D209012AFBD002211A788A431A705A7E98 -:10A31000D209012AFBD0064B1A68064B1968FFF7D1 -:10A3200069FF10BD00480042004000427C3A010035 -:10A33000C0000020BC000020182330B51D00040020 -:10A3400045430820204A55192856013014D00329C6 -:10A3500012D808006343D156D31801F01BFF02281E -:10A360000E1B5868CB010221184A9B181A1840325C -:10A370001170012282405A6030BD5868CB0106211D -:10A38000124A9B181A1840321170012282405A60FA -:10A390009A61F1E75868CB0106210C4A9B181A18FC -:10A3A00040321170012282405A605A61E4E75868D5 -:10A3B000CB010221054A9B181A1840321170012264 -:10A3C00082409A60D8E7C0467C3A010000440041D0 -:10A3D0001823F0B508245843114A13181C570134A8 -:10A3E0001BD0012635005F688356BD400D4ADB0156 -:10A3F0009A18906805420BD10C00601E8441D21956 -:10A400004032107834400336A400B043204310702B -:10A41000044A9B18002901D15D61F0BD9D61FCE7F4 -:10A420007C3A010000440041182310B5082443433E -:10A4300007490020CA181457013408D05B56054953 -:10A44000DB015B18186A5368D8400123184010BD1F -:10A450007C3A01000044004118224243F8B52C4EDA -:10A460000300B218127A52B2140001320ED0481C06 -:10A4700000240C280AD801F08DFE071F1F1F1F1F84 -:10A480001F1F1F0B0B0B0B00012464422000F8BDA3 -:10A49000092905D100211800FFF74EFF0024F5E738 -:10A4A0000A2901D10839F6E700240B29EED10A392F -:10A4B0001800FFF741FFE9E718225343F2185568E7 -:10A4C00001202C00F356134ADB016E089B18C9B219 -:10A4D00004409E19002C0DD00F243036327809012B -:10A4E00022401143C9B25B19317040331A781043CE -:10A4F0001870D3E732000F27303216785B19BE434D -:10A500003143117040331A7810431870BEE7C046CB -:10A510007C3A010000440041F7B506000D000C0034 -:10A520008B180193019B671B9C4207D033682178ED -:10A530001B683000984701340028F3D13800FEBD75 -:10A5400070B5040008000D0005F086FC23680200C9 -:10A5500029005B682000984770BD10B50B1E02D023 -:10A56000FFF7EEFF0300180010BD10B5FFF7F5FF71 -:10A5700010BD000010B50249FFF7E2FF10BDC04654 -:10A58000E92D010070B50400FFF7E7FF050020008A -:10A59000FFF7F0FF401970BD014B18607047C046CF -:10A5A000C40000200122024B52421A607047C0468C -:10A5B000C4000020124A1368591C20D0013B1360CC -:10A5C000002B1CD172B60F4B01221900187D1042CE -:10A5D000FCD02022188BFF3202431A830A4ADA6128 -:10A5E0000A4A1A8001230A7D1A42FCD0BFF34F8F1A -:10A5F000074B084ADA60BFF34F8FC046FDE770474C -:10A60000C4000020004000410210000002A5FFFF2E -:10A6100000ED00E00400FA05016070479446F0B5D3 -:10A620009E1E721E96416246056892072F68360686 -:10A630003A4332432A60072505AC24780068A40118 -:10A640002940214342680024022B00D05C031143BF -:10A6500021434160F0BD30B5C0250368AD031C68DF -:10A6600012052A402243C0240904A40221401143B8 -:10A67000196003685A6814435C6030BD01230268A6 -:10A6800011680B43136003681A68D207FCD4DA69B7 -:10A69000D207F9D470470223026811680B43136094 -:10A6A0000268D3699B07FCD4704703681A7ED207FF -:10A6B00002D41A7E9207FCD57047002203685A83A1 -:10A6C00070470368187E4007C00F70470368187E04 -:10A6D000C0097047802302685B42117E0B431376EA -:10A6E00070470368187EC007C00F70470368188D55 -:10A6F000C0B2704703681A7ED207FCD589B2012028 -:10A7000019857047012203689A7570470122036812 -:10A710001A75704770B5012604684B083340314004 -:10A72000256809075B070B432B432360046851002E -:10A73000034801F04DFD0138C0B2207370BDC04622 -:10A74000006CDC020123026811680B43136003688C -:10A750001A68D207FCD4DA69D207F9D47047022309 -:10A76000026811680B4313600268D3699B07FCD42D -:10A7700070470368DA699207FCD402211A688A4399 -:10A780001A6070470268936A5B0A5B021943916220 -:10A7900003681A7E5207FCD5986AC0B27047000061 -:10A7A0000368214A30B593422FD0204A93422FD0DC -:10A7B0001F4A93422FD01F4A93422FD01E4A9342E2 -:10A7C0002FD01E4A93421FD10E23192201210325A7 -:10A7D00099401B48C024016099082B40AB40FC35D0 -:10A7E0009D4089000918A4000859A843C0259D4030 -:10A7F0002B0003430B518023DB011A43114B5A807A -:10A800005A7852B2002AFBDB30BD09231422DDE75F -:10A810000A231522DAE70B231622D7E70C23172287 -:10A82000D4E70D231822D1E700080042000C0042B3 -:10A83000001000420014004200180042001C0042B8 -:10A8400000E100E0000C0040F8B504000F00160025 -:10A850001D00FFF7A5FF2000FFF710FF1C21BB0024 -:10A860000B4071072268090C0B4313608423226894 -:10A870009375012F1AD11021012E00D008390C4BED -:10A8800069431868C00001F0A3FC07222168024058 -:10A890008B895203DB04DB0C13438B812268030496 -:10A8A0009089DB0C400B400318439081F8BDC046F3 -:10A8B000B8000020F7B50600019308AB1F780D0023 -:10A8C0001400FFF73FFF3000FFF76AFFC0222405A6 -:10A8D00092031440BA070C2717433C43C0270722B2 -:10A8E000BF022D0433683D4025431D60019B316844 -:10A8F00013401A0080239B0213434B60F7BD0000F6 -:10A900000221084B1A888A431A80DA7BD209FCD1CB -:10A9100001321A80DA7BD209FCD11A88D207FCD422 -:10A920007047C0460034004210B50400FFF7E8FF4E -:10A9300000212000FFF74CFD0022014B1A7010BDD2 -:10A94000803800208023F7B50192504A5B030091C4 -:10A95000D367C0224E4D5200AB504E4B06001A78C2 -:10A96000002A0DD101321A70C522FF219200AB5886 -:10A970008B43AB50484B494A5A805A78D209FCD194 -:10A98000474B1B78002B05D0464B1868B04201D0CE -:10A99000FFF7CAFF444B0099186801F019FC434ABD -:10A9A000002394460427441E64450FD9040019006F -:10A9B0005A1CD440B943C9B2013C032902D0D2B2D7 -:10A9C000062A01D10233DAB21300EDE7013B00277A -:10A9D000092B02D8364A5B009F5A019B002B51D0AD -:10A9E000FA215800009B89004343180001F0F0FB56 -:10A9F000020000232F490A604B60FFF781FF2023EC -:10AA00002D4A11880B431F4317801700FB7BDB097E -:10AA1000FCD1A4B23C83F97BC909FCD11820274C96 -:10AA20007043A446264B274CC356DB019C44624628 -:10AA30002260254A254C9B1823600124204B1A18BC -:10AA4000536822009A40224B1A6010237A7B1343EA -:10AA50007B73144B1A68B24209D030001E60FFF7B6 -:10AA6000B7FC21003000FFF767FC0D4B1C70022380 -:10AA70003A8813433B80FB7BDB09FCD180235B03DB -:10AA80002B60F7BD01225242B3E7C04604E100E06B -:10AA900000E100E068380020000C00401C4000008D -:10AAA00080380020C8000020CC000020FFFF0000FC -:10AAB000B83E010078380020003400421C440041B8 -:10AAC0007C3A0100743800201444004170380020A2 -:10AAD0006C380020154B70B5186859680500144C87 -:10AAE0000D4319D0134A216812681160186859681B -:10AAF000002902DC0AD1002808D01868596801240E -:10AB00006442E517001969411860596010230A4A28 -:10AB1000917B0B43937370BDFFF7F2FE074B2268E6 -:10AB20001B681A60064B1D70F5E7C0467838002098 -:10AB30006C38002074380020003400427038002047 -:10AB4000803800203F20704713B56B460268D971EA -:10AB50000733546819000122A04716BD10B5022121 -:10AB6000006900F051FC10BD10B50321006900F030 -:10AB700061FC10BD10B5130004000A000069032138 -:10AB800000F050FD002801D10123636010BD0000DA -:10AB900010B5054C23685A1C04D103685B699847BB -:10ABA00003002060180010BD20010020074B0200A8 -:10ABB000186810B5411C03D0012252421A6010BD22 -:10ABC0000221106900F020FDF9E7C04620010020B5 -:10ABD00010B503784222023303700249024800F0A4 -:10ABE000CDFD10BDDC000020A038002010B501789C -:10ABF000020043780020A12908D1212B05D107228A -:10AC00001649174800F0BAFD012010BD2129FCD1DA -:10AC1000202B13D107221149114800F04FFC962236 -:10AC20000E4BD200196891420ED10124D87904400C -:10AC30000AD1FA20FFF7B0FC0020E6E7222B06D16C -:10AC40009278064BDA71EAE7FFF7ACFCF4E7232BC6 -:10AC5000DBD15288034B1A60D7E7C046D4000020EE -:10AC6000A0380020D000002070470000FA22002108 -:10AC7000054B92009A60054A596008321A60044AEE -:10AC800019761A617047C04684380020CC3E010016 -:10AC9000A038002070B50F26CB1D013902003240CC -:10ACA000D5B22C0000093034092A00DD07341C70AD -:10ACB000013B8B42F2D170BD70B582291FD1C3B266 -:10ACC0002B4D5A01AA1891692A4C80010C40C021D1 -:10ACD000890521439161284908334018802150613A -:10ACE0002A685B01D0180479494221430171702020 -:10ACF000995C8143080030210143995470BD0029BB -:10AD0000FCD1C024C3B21A4E590171188A68194D7A -:10AD1000A4052A4022438A60184A800180183268BC -:10AD20000726083348605B01985CB04306000120A9 -:10AD300030439854886905402C438C6170240E4838 -:10AD40004861985CA0430400102020439854886810 -:10AD50000B4CD318044080204022400320438860DD -:10AD60008868800B8003886059790A435A71C5E767 -:10AD7000583D0020FFFFFF8FCC3800208C3A002088 -:10AD8000FF3F00F070B50400012304CC05001178EA -:10AD900020000B43137080220021520004F0B3FA0C -:10ADA0002B689A78D207FCD45C6270BD1F22144BCA -:10ADB000F0B51B68590B9C0C9B01114014405B0FB4 -:10ADC000914200D105211F2C00D1023C072B00D15C -:10ADD000043B02688F01158D0A491F26294039431B -:10ADE000118505683440298D0A00B24322432A8523 -:10ADF00000681903028D044B13400B430385F0BD1B -:10AE0000246080003FF8FFFFFF8FFFFFEFF3108308 -:10AE100003600123436072B670470368002B02D1C0 -:10AE200062B6BFF36F8F704737B505690400684697 -:10AE3000002D14D1FFF7EAFF019B621D013B02D3F5 -:10AE4000D57FEDB2FAE768460193FFF7E6FF002DE4 -:10AE500001D100203EBD206AE369C01AFAE7FFF77E -:10AE6000D5FF22000025019B3432013B02D3157827 -:10AE7000EDB2FAE768460193FFF7CFFF002DE8D067 -:10AE8000206BE36AE9E70000FF2130B54368164D07 -:10AE90001A6883680B405B01D21801235218947A18 -:10AEA00023439372836842680B405B01D418C268E5 -:10AEB000A36892042B4012091343A360836842687D -:10AEC0000B405B01D3189A68920B92039A60436817 -:10AED0001A6883680B4008335B01D3184022197944 -:10AEE0000A431A7130BDC046FF3F00F0F7B5036951 -:10AEF00004000E0017006846002B45D1FFF786FFBF -:10AF0000019B611D013B5A1C0CD0CD7F5A1EEDB236 -:10AF1000002D05D168460193FFF77FFF2800FEBD95 -:10AF20001300F0E7684601930025FFF776FFE36919 -:10AF3000AF420AD0226A9A4207D95A1CE261A2693A -:10AF4000D3181B7873550135F1E7226A9A42E5D18F -:10AF50000023E361013323616846FFF757FF2200B6 -:10AF60000021019B671D013B3532581CD2D0F97777 -:10AF700010785E1E002805D0117020000193FFF7A5 -:10AF800083FFECE73300F0E7FFF740FF2100019B70 -:10AF90003431013B5A1C06D00D785A1EEDB2002DFB -:10AFA000B8D01300F6E7684601930025FFF735FF98 -:10AFB000E36ABD4202D0226B9A421CD8226B9A42AD -:10AFC000ACD100236846E3622361FFF71FFF27002F -:10AFD00022000021019B3437013B3532581C99D0A7 -:10AFE000397010785E1E00280DD01170200001937A -:10AFF000FFF74AFFEBE75A1CE262A26AD3181B78FC -:10B0000073550135D4E73300E8E7F7B5FF218268CF -:10B0100043680A401B68083252019A18D379040029 -:10B02000DB072AD50125D571836842680B405B0197 -:10B03000D3189A6840699204920C002821D12262A8 -:10B04000226A002A3ED0A26A656168465A60FFF70C -:10B05000DDFE27002100019B661D013B34373531A1 -:10B060005A1C2BD0F5773A78581ED2B2002A05D058 -:10B07000019368460D70FFF7D0FEF7BD0A7003001C -:10B08000EEE72263226B002A1CD000226261A269D3 -:10B0900068465A60FFF7BAFE26002100019B34364D -:10B0A000013B671D35315A1C08D03570FA7F581E98 -:10B0B000D2B2002ADCD10A700300F4E7684601939B -:10B0C000FFF7ABFE2000FFF7DFFED6E737B56B4694 -:10B0D000DC1D00230D0020002370FFF779FD2B6895 -:10B0E0001818286001F03AF8210000F0F3FF2B68EF -:10B0F0001818286020783EBD03290ED180220E4BFF -:10B1000062311B68FF31585C524202435A54682234 -:10B110006339FF32FF39995470470129FCD18022ED -:10B12000054B24311B68FF31585C524202435A548C -:10B1300028222339FF32EDE7583D002070B5002565 -:10B14000304B012104001A201D70FFF7F5F8012192 -:10B150001A20FFF73DF92C4B012119201D70FFF734 -:10B16000EBF801211920FFF733F92023274A0F209C -:10B17000D1690B43D361012225490B7813430B702E -:10B18000244B19788143197006211D7829431970C1 -:10B1900021490D782A430A701A7802401A706022F9 -:10B1A00019780A431A701D4B1D4A5A805A78D209E1 -:10B1B000FCD11C48FEF7A6FE1B4D2800FFF7E2FD60 -:10B1C0002800FFF7F3FD7F212A68C1201378800053 -:10B1D0000B40137004232A6811780B430C21137061 -:10B1E0002A6813898B43114913810B581B021B0AD0 -:10B1F0000B5080230B6011787E3B0B4313700123AF -:10B20000237070BDC1380020C03800200004004009 -:10B21000584400413C44004159440041000C004066 -:10B2200006400000F1BB0000583D002000E100E0B6 -:10B230000078002810D00121084B1B681A898A4326 -:10B240001A810822198B0A431A830422198B0A4394 -:10B250001A830022024B1A607047C046583D0020F6 -:10B26000503D002010B50F4A93699B0B9B039361DF -:10B2700013680222FF331A729879823A02439A7154 -:10B280001A7A9207FCD57F24074A2140937A080056 -:10B29000A3431900802301439172917A5B420B43CF -:10B2A000937210BD583D00200050004110B50B793D -:10B2B0000C00002B0CD10800FFF798FC002801D0EF -:10B2C000012010BD094A93699B0B9B039361F7E72B -:10B2D00000F044FF210000F030FF0028F0D1034AC5 -:10B2E00093699B0B9B039361EBE7C046583D00209D -:10B2F000F7B50D00832A19D1CBB2354C5A01A218EB -:10B3000091693448AD010840C0218905014391612C -:10B31000314920686D187021083355615B011A5C52 -:10B320008A43110040220A431A54F7BD022A4AD127 -:10B330008B000193294B8A00D658002EF5D1382076 -:10B3400000F0C0FE264B224F036080235B00C360E9 -:10B35000431D040085600662476006614661C66160 -:10B36000DE77C662066380202F331E7040005E7059 -:10B3700003F078FFA0618020400003F073FFFF23FB -:10B380001D406A01A062BA18916812480835084049 -:10B39000C021890501430720916039686D016A5C0D -:10B3A00082431000032202436A54A2682000134023 -:10B3B00062685B01D318A2695A60FFF765FD074B0D -:10B3C000019AD450B1E711002800FFF775FCACE7F3 -:10B3D000583D0020FFFFFF8FCC380020A43800200C -:10B3E000F83E010070B505000124064BA200D258BA -:10B3F000002A05D021002800FFF77AFF0134F4E786 -:10B4000070BDC04624010020084B8A00D05810B5FA -:10B41000002803D003689B68984710BDC9B2044B4D -:10B420004901591888688004800CF6E7A438002088 -:10B43000583D002010B50C00FFF7E6FF00280CD0A7 -:10B44000064BE4B21B68083464011C19802362793E -:10B450005B42134363710223E37110BD583D00202A -:10B4600070B5CBB2124A13485C01890189180219E0 -:10B4700051609168104D08330D40802189022943A5 -:10B48000402591609168890B8903916001685A0198 -:10B490008A1816793543157195796D06FCD55B01CF -:10B4A000C918CB79DB07FCD500198068C0B270BD24 -:10B4B0008C3A0020583D0020FF3F00F070B514008A -:10B4C00040220B4B0D001B68FF3359790A435A7118 -:10B4D0000021FFF7C5FFA04200D920000023054945 -:10B4E000A34203D0CA5CEA540133F9E770BDC046F9 -:10B4F000583D00208C3A002010B5C9B20B484901D4 -:10B50000421893689B049B0C3F2B0BD99468403BDB -:10B510009B04A40B9B0CA4032343936041188868ED -:10B52000C0B210BD93689B0B9B03F6E7583D00200B -:10B53000F0B51C002F4B85B01B6806000D00019272 -:10B54000002B54D02C4F3978C9B2002902D11920D0 -:10B55000FEF73EFF64233B70284BAA00D05800281A -:10B5600008D0036822005B68019998470400200016 -:10B5700005B0F0BD29003000FFF746FF844204D932 -:10B5800029003000FFF740FF040029003000FFF7DA -:10B59000B3FF0121EBB21A4F03935B010293029AAE -:10B5A0003B6801989B18FF335A7A0A435A72154B2D -:10B5B000A901C918220003F08AFE002CD7D0290067 -:10B5C0003000FFF721FF0028D1D14021039B3A68CA -:10B5D00008335B01D3181879014319710121D9711E -:10B5E000029BD318FF339A7A0A439A72BFE7012469 -:10B5F0006442BCE7503D0020C0380020A438002041 -:10B60000583D00208C3A002013B56B46DC1D22000B -:10B610000123FFF78DFF012801D1207816BD0120FD -:10B620004042FBE7F0B51E003A4B85B01B680F00A7 -:10B630000192002B3FD08023DB019E423BD8364C49 -:10B640002178C9B2002902D11A20FEF7C1FE642375 -:10B6500000252370314BBA01D3180293002E2CD051 -:10B660002F4B1B680393FF233B401C0000930834BF -:10B67000039B64011C19A379DB0921D0294B2A49BA -:10B68000186800F0A5FD294B1721584300F0A0FDD4 -:10B69000274BE279920713D4DA5D002A01D10138F1 -:10B6A000F7D20122DA55009B5A011D4B9B189A696B -:10B6B000920B92039A6101256D42280005B0F0BDFE -:10B6C00000221B4B3400DA553F2E00D93F242200C4 -:10B6D0000199029803F0FBFD009BA1045A01104B55 -:10B6E000890C9B18029A2D195A619A69361B920B84 -:10B6F00092030A439A61009B039A08335B01D318B3 -:10B700000222DA715979823A0A435A71019B1B1954 -:10B710000193A3E7503D0020C1380020CC38002021 -:10B72000583D0020B800002040420F007011010079 -:10B73000C338002070B50D001C000D4BAE01F6188B -:10B7400011003000220003F0C2FDEDB209496D0185 -:10B750004D196E61AB69084A20001340AB61AB69BB -:10B76000A2049B0B920C9B031343AB6170BDC046BC -:10B77000CC380020583D0020FF3F00F0F7B5104BBB -:10B7800001901B780F001500002B0CD10D4B1600FB -:10B790001C78002C09D00C4E0C48348820186419F1 -:10B7A00003F095FD34802800FEBD002EFBD03A1931 -:10B7B000330000210198FFF7BDFF2418361AF4E783 -:10B7C0004C3C00204D3D00204E3D00204D3C0020D3 -:10B7D000F8B5002000AF0E001500012A27D9D31DAF -:10B7E0006A46DB08DB00D31A9D46080004F034FBF0 -:10B7F0006C46032302220021013040002070637058 -:10B80000AA420DD2307800280AD0531CDBB2013690 -:10B81000A0549D4203D00232E154D2B2F0E72A0094 -:10B8200021000448FFF7AAFF431E9841C0B2BD465D -:10B83000F8BDC046A0380020F0B50024012685B030 -:10B84000184B0F0069461E7005000094FFF73EFC80 -:10B85000154B01A90B806A3BFF3BCB715A330B722E -:10B86000009B8C7109334B800E4B4E7108711C701C -:10B870003A00092F0FD00D4F09223E700C4E2800C0 -:10B8800000943480FFF77AFF69462800FFF71EFC1A -:10B89000328808493C702800FFF770FF012005B08E -:10B8A000F0BDC0464C3C0020090200004D3D002088 -:10B8B0004E3D00204D3C002070B5CE7892B0050082 -:10B8C0000C00022E08D1C9882B48FFF7B5FF431E94 -:10B8D0009841C0B212B070BD00F040FC210000F0F1 -:10B8E0000DFC002803D0C317181AC00FF2E7012E71 -:10B8F0000BD1E2882149112A10D8D2B2002A0DD0EA -:10B900002800FFF73BFF0120E4E7032EE2D1A378F4 -:10B91000002B05D1E2881A49032AEED90A78EFE70D -:10B92000022B05D1A27917492800FFF751FFD1E773 -:10B93000012B02D1A2791449F6E7032BCAD1134B8C -:10B9400001A91868FFF7A6F9114B03A91868FFF7BA -:10B95000A1F9104B05A91868FFF79CF90E4B07A930 -:10B960001868FFF797F900F0F9FB09A900F0D6FB7A -:10B97000A27901A9D8E7C046A03800201D3F0100E8 -:10B98000103F0100043F0100143F01000CA08000A3 -:10B9900040A0800044A0800048A0800073B506004D -:10B9A00048780D000B2810D800F0F4FB061B0F2B75 -:10B9B0000F40456D494C6669097801AA002906D1F6 -:10B9C000022311803000FFF7B5FE012076BD002371 -:10B9D00013802E4B1B78012B00D113700223002102 -:10B9E000F0E78A780023012A03D101AA1380284BAB -:10B9F000F0E7264A1370274A93699B0B9B039361D8 -:10BA0000E3E78C78012C06D10021214B01AA1C70A0 -:10BA100011702300D6E7002C08D101201B4B1D4AD2 -:10BA2000187093699B0B9B039361CFE7A978300053 -:10BA3000FFF718FCC9E73000FFF73EFFC6E7012318 -:10BA4000154ACCE70B780020DB06BFD13000FFF7AA -:10BA5000C9FC2A200221AA780F4BFF301A600D4A38 -:10BA600013681C5C21434A241954FF34195D29389A -:10BA7000FF3801431955D4E70123084AAFE78A7814 -:10BA8000064B1A60B7E70020A0E7C046C238002086 -:10BA9000CA380020583D0020503D0020543D002071 -:10BAA0004B4BF7B51C780700002C00D083E0494EC3 -:10BAB00033689B8B1B070ED521002000FFF7FCF895 -:10BAC000326810231100FF31887A03438B72424B96 -:10BAD0001C600823938333689A8B52071FD5042276 -:10BAE0009A833E4A1378002B0AD01378013BDBB2CD -:10BAF00013701378002B03D101211A20FEF768FC84 -:10BB0000374A1378002B0AD01378013BDBB213704D -:10BB10001378002B03D101211920FEF759FC33685B -:10BB2000FF331A7AD2061ED510221A725979303292 -:10BB30000A435A7160232B4938000A781A423BD1D4 -:10BB4000FFF72CFF3368FF33002838D08022997923 -:10BB500052420A439A711A7A520604D540221A7246 -:10BB6000597A0A435A72336800241D8C01239D437D -:10BB7000EDB20193E1B2002D1DD02B002341019ABB -:10BB8000134215D02300326808335B01D318DA79E9 -:10BB9000D20702D4DB799B0707D5134BA200D058FC -:10BBA000002810D003681B689847019BA3409D4361 -:10BBB0000134092CDED1F7BDFFF778FBC2E7202165 -:10BBC0000A4A9171C7E73800FFF796FAEDE7C046D9 -:10BBD0004D3D0020583D0020503D0020C138002040 -:10BBE000C03800208C3A0020A4380020FF500041CB -:10BBF00010B50248FFF754FF10BDC046A038002022 -:10BC0000014B024A1A607047583D00200050004125 -:10BC10000120704710B50368014A1B6A984710BDA0 -:10BC2000130400000300FC33D9699A69914203D0E0 -:10BC3000DB69C018007D704701204042FBE710B56A -:10BC400004000069FEF71AFD22000023FC32936114 -:10BC5000D36186229200A4186360A36010BDF02017 -:10BC600001403039484248417047F0231B011940D8 -:10BC7000802306209B0099420AD00133FF33013014 -:10BC8000994205D0802302385B00994200D0002001 -:10BC900070470F2001400020012903D00239481EBF -:10BCA0008141481C704700008923F0B5182685B0F3 -:10BCB00002919B000400C05C330008214343324DD5 -:10BCC0001700EB185956FEF7C7FB304B0821E05C14 -:10BCD00033004343EB185956FEF7BEFB2C4BE25C96 -:10BCE0000393022A09D11133E05CFF2805D0082113 -:10BCF0004643AE197156FEF7AFFB8A26B600A05D2B -:10BD0000FF2819D00121FEF717FB1822A35D214956 -:10BD10005A43AB56AD18DB0158188B21890060508F -:10BD20001D4901225B188C218900635069688A4093 -:10BD30008D21890062501A600122029B1100206946 -:10BD4000FEF782FD39002000FFF78FFF3900050064 -:10BD50002000FFF79EFF390006002000FFF77FFF5D -:10BD600033000090290020690122FEF757FC0B4B9D -:10BD70002069E25C039BE15CFEF76DFC2069FEF745 -:10BD80008AFC05B0F0BDC0467C3A010025020000E7 -:10BD9000270200001844004114440041260200001C -:10BDA000802210B504005200002103F0ACFA2300F9 -:10BDB0000022FC335A6020009A6010BDF7B51D00C8 -:10BDC00008AB1B780400009309AB1B7816000193A5 -:10BDD00000234360FA239B008360114B0F0008335C -:10BDE00003601430FFF7DCFF20001D30FF30FFF749 -:10BDF000D7FF89236A469B002761E654094B1278D6 -:10BE0000E554094B2000E2546A46084B1279E2548B -:10BE10008A22293BFF3B9200A3541032A354FEBD5B -:10BE2000303F01002502000026020000270200002A -:10BE30000300FC3358689B68C01A01D50130FF30FD -:10BE4000704710B5040020001D30FF30FFF7F0FFF1 -:10BE50000028F8D12069FEF728FC10BD10B5143079 -:10BE6000FFF7E6FF10BD0300FC3359689A68914262 -:10BE700004DB5A689868801AFF30704798685B68DE -:10BE8000C01A0138F9E710B51D30FF30FFF7EBFF9E -:10BE900010BD10B504000069FEF713FC002820D087 -:10BEA0002069FEF723FC2200FC329369D16901333B -:10BEB000DBB28B4203D091696118087593618A23C4 -:10BEC0009B00E35CFF2B0CD020001430FFF7CBFF6E -:10BED000092806DC8B238D229B009200E358A25890 -:10BEE0001A602069FEF7FDFB00281AD020001D30E3 -:10BEF000FF30FFF79DFF002822D086239B00E31828 -:10BF000099685A68914218D08E219A684900A218FF -:10BF1000515C9A680132D2B29A60C9B22069FEF7C8 -:10BF2000E9FB2069FEF7D2FB002805D02069FEF767 -:10BF3000D1FB2069FEF7C1FB10BD01214942ECE7AE -:10BF40002069FEF7E3FBECE770B5040000690D0023 -:10BF5000FEF7C7FB00283ED1FF2686239B00E3188F -:10BF60005A689968013232408A4223D1EFF3108334 -:10BF7000DB0710D41E4B5B68DB05DB0DEDD0103BFF -:10BF80005BB2002B10DA0F221340083B9B08194AC2 -:10BF90009B009B181B682069FEF7A3FB0028DCD0E0 -:10BFA0002000FFF776FFD8E79B08C033124A9B00BA -:10BFB0009B58F0E75A68013216409A68964205D0BD -:10BFC0008E215A684900A21855545E602069FEF718 -:10BFD00099FB012070BD20001D30FF30FFF728FFC6 -:10BFE0000028B9D129002069FEF784FBF1E7C0469B -:10BFF00000ED00E01CED00E000E100E0030070B5A2 -:10C00000FC33D9699A69040091421AD0DA6982181E -:10C01000157DDA690132D2B2DA618A239B00E35CD2 -:10C02000FF2B0CD020001430FFF71DFF0A2806DD7F -:10C030008C238D229B009200E358A2581A6028009E -:10C0400070BD01256D42E8E710B5002801D003F06E -:10C0500069FE10BD10B5041E05D003F087FE210057 -:10C0600000F026FA0C00200010BD10B50400884234 -:10C0700003DA081AFFF7EEFF2418200010BDFEE7D0 -:10C080007047000010B5FEF745F803F0AFF8FFF772 -:10C09000F7FF0120FDF772FF084C2000FFF74EF874 -:10C0A0002000FFF7C5F8F7F757FDF9F793FA044BAF -:10C0B000002BFAD000E000BFF7E7C046A038002010 -:10C0C0000000000010B503F0CDF810BD10B503F06E -:10C0D000D3F810BD70B50E0000254468002C0BD0BD -:10C0E000236831005B6820009847002802DB2D1888 -:10C0F000E468F3E701256D42280070BD70B50D00BE -:10C100004468002C08D0236829009B6820009847C9 -:10C11000002802D1E468F4E7200070BD70B50D007E -:10C120004468002C07D0236829002000DB6898476A -:10C13000E4682D18F5E72C7070BD70B50D004468EB -:10C14000002C08D0236829001B682000984700288D -:10C1500002D1E468F4E7200070BD00000121054A27 -:10C1600010B5136804480B4003D1044C43600480AD -:10C17000116010BD5C3E0020603E00200204000003 -:10C1800002B4714649084900095649008E4402BC70 -:10C190007047C04602B4714649084900095C49002D -:10C1A0008E4402BC7047C04603B471464908400043 -:10C1B0004900095E49008E4403BC704703B47146D0 -:10C1C000490840004900095A49008E4403BC7047A1 -:10C1D000002243088B4274D303098B425FD3030AC6 -:10C1E0008B4244D3030B8B4228D3030C8B420DD3D9 -:10C1F000FF22090212BA030C8B4202D31212090267 -:10C2000065D0030B8B4219D300E0090AC30B8B42A4 -:10C2100001D3CB03C01A5241830B8B4201D38B0352 -:10C22000C01A5241430B8B4201D34B03C01A5241F7 -:10C23000030B8B4201D30B03C01A5241C30A8B423A -:10C2400001D3CB02C01A5241830A8B4201D38B0225 -:10C25000C01A5241430A8B4201D34B02C01A5241C9 -:10C26000030A8B4201D30B02C01A5241CDD2C3093B -:10C270008B4201D3CB01C01A524183098B4201D3B7 -:10C280008B01C01A524143098B4201D34B01C01AA2 -:10C29000524103098B4201D30B01C01A5241C3081A -:10C2A0008B4201D3CB00C01A524183088B4201D389 -:10C2B0008B00C01A524143088B4201D34B00C01A75 -:10C2C0005241411A00D20146524110467047FFE7E1 -:10C2D00001B5002000F0F0F802BDC0460029F7D0FB -:10C2E00076E7704703460B437FD4002243088B4216 -:10C2F00074D303098B425FD3030A8B4244D3030BED -:10C300008B4228D3030C8B420DD3FF22090212BAB1 -:10C31000030C8B4202D31212090265D0030B8B422D -:10C3200019D300E0090AC30B8B4201D3CB03C01A17 -:10C330005241830B8B4201D38B03C01A5241430BF2 -:10C340008B4201D34B03C01A5241030B8B4201D3E2 -:10C350000B03C01A5241C30A8B4201D3CB02C01A4D -:10C360005241830A8B4201D38B02C01A5241430AC5 -:10C370008B4201D34B02C01A5241030A8B4201D3B4 -:10C380000B02C01A5241CDD2C3098B4201D3CB015B -:10C39000C01A524183098B4201D38B01C01A52410A -:10C3A00043098B4201D34B01C01A524103098B420E -:10C3B00001D30B01C01A5241C3088B4201D3CB00F9 -:10C3C000C01A524183088B4201D38B00C01A5241DC -:10C3D00043088B4201D34B00C01A5241411A00D28C -:10C3E00001465241104670475DE0CA0F00D04942F5 -:10C3F000031000D34042534000229C4603098B4265 -:10C400002DD3030A8B4212D3FC22890112BA030AEC -:10C410008B420CD3890192118B4208D3890192116E -:10C420008B4204D389013AD0921100E08909C309F3 -:10C430008B4201D3CB01C01A524183098B4201D3F5 -:10C440008B01C01A524143098B4201D34B01C01AE0 -:10C45000524103098B4201D30B01C01A5241C30858 -:10C460008B4201D3CB00C01A524183088B4201D3C7 -:10C470008B00C01A5241D9D243088B4201D34B00E2 -:10C48000C01A5241411A00D20146634652415B1024 -:10C49000104601D34042002B00D549427047634605 -:10C4A0005B1000D3404201B5002000F005F802BD4A -:10C4B0000029F8D016E770477047C0468446101C24 -:10C4C00062468C46191C634600E0C0461FB501F069 -:10C4D0009DFE002801D40021C8421FBD10B501F007 -:10C4E000F5FD4042013010BD10B501F08FFE00286F -:10C4F00001DB002010BD012010BDC04610B501F0C9 -:10C5000085FE002801DD002010BD012010BDC046C1 -:10C5100010B501F017FE002801DC002010BD01203D -:10C5200010BDC04610B501F00DFE002801DA002054 -:10C5300010BD012010BDC0468446081C6146FFE7BF -:10C540001FB500F0B5FB002801D40021C8421FBD73 -:10C5500010B500F037FB4042013010BD10B500F0BF -:10C56000A7FB002801DB002010BD012010BDC04644 -:10C5700010B500F09DFB002801DD002010BD01205A -:10C5800010BDC04610B500F045FB002801DC0020BE -:10C5900010BD012010BDC04610B500F03BFB0028C7 -:10C5A00001DA002010BD012010BDC046F0B5CE4616 -:10C5B000474615042D0C2E0080B50704140C3F0CC3 -:10C5C0009946030C7E435D43674363437F19340CF4 -:10C5D000E4199C46A54203D980235B029846C444D3 -:10C5E0004B4643435143250C36046544360C240422 -:10C5F000A4195B19591820000CBC90469946F0BD4F -:10C600009E2110B5C905041CFFF7C6FF002803D101 -:10C61000201C00F093FE10BD9E21201CC90500F0D7 -:10C62000C9FC00F08BFE80231B069C466044F2E7A9 -:10C6300070B500220C4B04000D00FFF773FF0028BB -:10C6400004D12000290002F01FFC70BD064B00221F -:10C650002000290002F0B4F802F016FC80231B062B -:10C660009C466044F1E7C0460000E041F8B547460B -:10C67000CE4643025B0A4400C20F9C464800DD00E0 -:10C680004B02240E5B0A000E80B5984626009146A8 -:10C69000C90FDB00271A8A4229D0002F15DD002898 -:10C6A0004AD1002B00D095E0ED08FF2C00D188E0A6 -:10C6B0006B025B0AE6B25B02F605580A3043D2070A -:10C6C00010430CBC90469946F8BD002F00D087E07F -:10C6D000601CC0B2012800DCB6E0EE1A720100D581 -:10C6E000C5E0002E3DD1002200260023E3E7002F05 -:10C6F00000DC96E000285DD0FF2C60D08022D204C0 -:10C7000013431B2F00DDECE02022D21B18009340C6 -:10C71000F8405A1E93410343ED186B017BD5013459 -:10C72000FF2C00D1B7E0012207262A406B089A4D62 -:10C730001D4015432E4029E0FF2CB5D08022D204A5 -:10C7400013431B2F00DDB2E02022D21B19009340BF -:10C75000F9405A1E93410B43ED1A6B015BD5AD01B5 -:10C76000AE09300002F008FD05388640844265DCE1 -:10C77000041B330020200134E340041BA640751E37 -:10C78000AE41334307261D0000241E4001224B46C4 -:10C790001A40002E04D00F232B40042B00D0043568 -:10C7A0006B0100D480E70134E6B2FF2C2FD1FF26C5 -:10C7B000002380E7002B52D1FF2C00D074E70A0041 -:10C7C000ED08002DF3D08023DB032B435B025B0AD3 -:10C7D000FF2670E7013F002FBED0FF2CB1D163E7E9 -:10C7E000002C47D0FF2869D08024E4047A422543F6 -:10C7F0001B2A00DDC5E02C002026D440B21A95404B -:10C800006A1E954125435D1B04008946A5E7AB01DF -:10C810005B0A50E7002401224B461A406B07BAD14D -:10C8200042E7002F3BD10134E0B201284ADDFF2C62 -:10C83000BDD00726ED186D082E40A7E70723574DFA -:10C84000241A35401E40A1E7002C1BD1002D6ED1CB -:10C85000002B00D19AE00A001D003C0024E7013FB4 -:10C86000002F00D158E7FF2C00D04AE7A7E75E1B56 -:10C87000894676E7002D1CD10A00FF281FD004004E -:10C880001D0011E7002D5DD1002B17D18023002260 -:10C89000DB03FF260FE70A00FF2600230BE7002C2F -:10C8A00021D1002D66D1FF28E9D11D0087E70123A2 -:10C8B00052E77A1CA7D0FA43FF2899D10A001D003D -:10C8C000FF24F1E6002E21D1002D4FD0002B4CD0BB -:10C8D000ED186B019ED5314B07362E4001241D40CB -:10C8E00054E7012318E7FF28DFD08024E4047F42C7 -:10C8F00025431B2F4DDC2026F61B2C00B540FC40A9 -:10C900006A1E95412543ED18040006E7002DCCD0A2 -:10C91000002B00D153E780216046C903084203D0B1 -:10C920004046084200D11D000121114047E7002B7D -:10C9300000D1B9E6EA1A500125D507265D1B2E4025 -:10C94000894623E7FF24002B00D1ADE68022604614 -:10C95000D203104204D04046104201D11D00894646 -:10C9600001224B46FF241A409EE62B00DD080A00F8 -:10C9700000249DE67A1CC6D0FF43FF28B9D11D00D4 -:10C980001DE701253FE7151E00D044E700220023E4 -:10C9900091E60125B7E7C046FFFFFF7DFFFFFFFBE4 -:10C9A000F8B557464E464546DE464402E0B54600D9 -:10C9B0008846640A360EC70F002E63D0FF2E24D09F -:10C9C0008023E400DB041C43002399469B467F3E02 -:10C9D000434642465D02D20F5B006D0A1B0E904635 -:10C9E000924665D0FF2B55D080220021ED00D20465 -:10C9F0007F3B1543F61A43464A467B400F2A00D92F -:10CA00008DE06D48920082589746002C54D108233F -:10CA10009946063BFF269B46DAE700255346022946 -:10CA20001BD0032900D1BFE0012928D030007F307E -:10CA3000002820DD6A0704D00F222A40042A00D0F3 -:10CA400004352A0103D530005C4A80301540FE28A9 -:10CA500003DCAC01640AC2B201E0FF2200246402DC -:10CA6000D205600ADB07104318433CBC9046994648 -:10CA7000A246AB46F8BD0122101A1B287CDD00221D -:10CA80000024ECE7002C1DD104239946033B00262B -:10CA90009B469DE7FF3E002D20D1022143464A469A -:10CAA0007B400A430F2AD8D84548920082589746BF -:10CAB000002D19D10121F1E70C239946093BFF26EE -:10CAC0009B4685E7200002F057FB7626431F9C40DB -:10CAD00000237642361A99469B4679E74A46032355 -:10CAE0001A439146032186E7280002F045FB431FC5 -:10CAF00036189D40763600217DE780240023E4032C -:10CB0000FF22ACE700258023DB031C4228D01D4216 -:10CB100026D12B435C02640A4346FF229FE7620151 -:10CB20006C01A24224D21B210025013E01271000E6 -:10CB30006D005200002801DB944201D8121B3D43D6 -:10CB400001390029F3D11400621E944125436DE799 -:10CB5000BA46594625005346022900D061E77CE7D2 -:10CB60008023DB031C436402640A3B00FF2276E758 -:10CB7000121B1A210125D9E79E362A00B5402C0048 -:10CB8000C240651EAC411443620704D00F2222400C -:10CB9000042A00D00434620103D4A401640A0022F0 -:10CBA0005DE7012200245AE78024E4032C43640259 -:10CBB000640AFF2253E7C046643F0100FFFFFFF70E -:10CBC000A43F010070B542004E024C0045026D0AC0 -:10CBD000120EC30F760A240EC90FFF2A0FD0FF2CA6 -:10CBE00011D00120A24200D070BDB542FCD18B42D1 -:10CBF0000DD0002AF8D12800451EA841F4E70120F5 -:10CC0000002DF1D1EBE70120002EEDD1E9E7002066 -:10CC1000EAE7C04670B54A004E02450244006D0A7C -:10CC2000240EC30F760A120EC90FFF2C15D0FF2A4F -:10CC30000ED0002C15D1002A01D1002E1CD0002DC1 -:10CC400014D08B4227D00220013B1840013870BD20 -:10CC5000002EEED002204042F9E7002DFAD1FF2A43 -:10CC60000ED0002A0ED1002EEDD00BE001230139A9 -:10CC7000994308000130EAE70020002DE7D0E2E701 -:10CC8000002EE7D18B42DED1944205DD0221581EF1 -:10CC900008400138DBE70024A24204DCB542D2D8C8 -:10CCA0000020B542D3D2581E012398430130CEE76D -:10CCB00030B5420044024D02C30F4800640A120E10 -:10CCC0006D0A000EC90FFF2A12D0FF280CD0002ACF -:10CCD00012D1002819D1002D17D1002C2BD0022001 -:10CCE000013B1840013826E0002DF0D0022022E060 -:10CCF000002CFBD1FF281FD000281FD1002D1DD1F3 -:10CD00000220013B1840013815E0002C0ED08B4268 -:10CD1000E5D10022904204DCAC42E0D80020AC42D5 -:10CD200009D2581E01239843013004E00123013940 -:10CD300099430800013030BD002DD7D18B42CED1B0 -:10CD40008242E7DD0221581E08400138F3E7C04661 -:10CD5000F0B54E4657464546DE46E0B543025B0A0F -:10CD6000450083B00F1C99462D0EC60F002D57D0DD -:10CD7000FF2D24D08020DB00C0041843002381460F -:10CD80009A469B467F3D7C027A00FB0F640A120E96 -:10CD9000984623D0FF2A4BD0E30080240020E404EF -:10CDA0007F3A1C43AD186B1C4746019353467740AE -:10CDB0003A000F2B48D87D499B00CB589F46002B4B -:10CDC00000D085E008339A46063B9B467C027A00F9 -:10CDD000FB0FFF25640A120E9846DBD1002C00D011 -:10CDE00090E0524601231A4392460120DBE74C466D -:10CDF00058461700022824D0032800D1CFE0002293 -:10CE0000002301284DD15802D205400AFF071043E4 -:10CE1000384303B03CBC90469946A246AB46F0BDB1 -:10CE2000002B5BD104239A46033B00259B46AAE7CF -:10CE3000FF35002C60D1524602231A43924602204D -:10CE4000B1E7FF220023DEE74B461B0C9C464B4616 -:10CE50002604360C180461463300220C6446000C8C -:10CE600043434E435043544380191A0C12189642C0 -:10CE700003D9802149028C4664441B041B0C100416 -:10CE8000C01883015E1EB341800E1843130C1B199A -:10CE90009B0103431C00230179D5012362081C4038 -:10CEA0001443019A7F32002A4DDD630704D00F231B -:10CEB0002340042B00D00434230103D53C4B019ABA -:10CEC0001C408032FE2ABCDCA3015B0AD2B29AE786 -:10CED0000C239A46093BFF259B4654E7180002F0B5 -:10CEE0004BF94A46431F76259A4000236D429146EE -:10CEF0002D1A9A469B4646E7524603231A4392460A -:10CF0000032050E7200002F037F9431F2D1A9C4000 -:10CF1000763D002047E780230027DB03FF2272E7EE -:10CF2000424666E74C463200584662E780234A464E -:10CF3000DB031A4222D01C4220D123435B025B0A4E -:10CF40004746FF225FE701239A1A1B2A21DC2300B0 -:10CF50000199D3409E318C401A0023005C1EA341EE -:10CF600013435A0704D00F221A40042A00D0043376 -:10CF70005A0111D49B015B0A002244E780234A46F0 -:10CF8000DB0313435B025B0A3700FF223BE701959B -:10CF900087E70022002336E70122002333E78023BE -:10CFA000DB0323435B025B0AFF222CE7E43F010023 -:10CFB000FFFFFFF7F8B54746CE464400C20F80B5E5 -:10CFC00047024802400A8446664648007F0A240E0B -:10CFD000F60025009046FB00000EC90FB146FF2861 -:10CFE00000D185E001267140261A914257D0002ECB -:10CFF00043DD002800D07FE04946002900D1AAE0A7 -:10D00000013E002E00D0F7E05B1A5A0100D48BE0FD -:10D010009B019C09200002F0AFF80538844085424E -:10D0200000DDD3E0451B230020200135EB40451BEC -:10D03000AC40621E94412343072400251C4001227A -:10D0400041460A40002C04D00F211940042900D089 -:10D050000433590100D480E00135ECB2FF2D00D03B -:10D06000A3E0FF2400235B02E405580AD207204313 -:10D0700010430CBC90469946F8BD002E74D1601C3C -:10D08000C0B2012800DCA7E04A469C1A620100D524 -:10D09000B6E0002CBED1002200240023E3E7002EDE -:10D0A00000DC85E0002846D0FF2C49D0802248468D -:10D0B000D2041043814601221B2E09DC20204C465D -:10D0C000801B84404A462000F240441EA041024397 -:10D0D0009B185A0128D50135FF2D00D1A8E0012267 -:10D0E000072494491A405B080B4013431C40A6E7F1 -:10D0F000002E00D078E775E7FF2C54D080224946F7 -:10D10000D2041143894601221B2E09DC2021484606 -:10D11000891B88404A460100F240481E81410A436B -:10D120009B1A5A0100D573E7012241460A4059076C -:10D1300000D089E711E04846002858D1FF2C0CD1D7 -:10D14000DB08002B00D18CE78020C00303435B0287 -:10D150005B0AFF2487E7FF2C25D0DB08FF2DF0D0EA -:10D160005B025B0AECB27EE7002C4DD0FF2818D0A2 -:10D170008024E404724223431B2A00DDC4E01C0027 -:10D180002025D440AA1A93405A1E934123434A466D -:10D190000500D31A884638E7721CF8D0F243FF28FE -:10D1A000EAD10A004B46FF25D7E79B015B0A5AE705 -:10D1B000002E41D1651CE9B2012945DDFF2D00D1CA -:10D1C0004FE707244B445B081C4038E707225A4BC3 -:10D1D0002D1A2340144032E7002C1DD1002B7AD1A8 -:10D1E0004B46002B00D191E00A000025B5E7013E37 -:10D1F000002E19D14B446CE7FF2C84D1FF25ACE7FE -:10D200004A468846D41A05E7002BC5D10A00FF28F4 -:10D21000C8D005004B46A0E7002B49D14B46002B58 -:10D2200077D00A00FF2598E7FF2C00D043E787E777 -:10D230000A00FF24002316E7002C15D1002B57D13C -:10D24000FF28E6D14B467BE7002C20D1002B57D09E -:10D250004946002953D04B445A0168D50724364A21 -:10D260001C4001251340EAE6FF28EBD08022D204BF -:10D27000764213431B2E53DC2025AD1B1A00AB4016 -:10D28000F2405C1EA34113434B44050021E7002BF1 -:10D29000D8D04946002900D152E78021C9030F4266 -:10D2A00000D14DE76046084200D049E74B4647E7CA -:10D2B0004846FF25002800D14FE78022D2031742BD -:10D2C00004D06046104201D14B46884601224146B7 -:10D2D000FF250A4041E7484600281FD01A1A50018E -:10D2E00020D54A460724D31A1C4088460025A6E6C6 -:10D2F000741CC9D0F643FF28BCD14B4620E79946A1 -:10D300004B460025DB082BE7012340E700220023E2 -:10D31000A9E680230022DB03FF24A4E600251CE706 -:10D320000123B1E7002AF1D013000025FCE6002517 -:10D33000FAE6C046FFFFFF7DFFFFFFFB4102420010 -:10D34000C30F490A120E00207E2A0DD99D2A0CD83F -:10D35000802000040143952A0ADC9620821AD140DD -:10D360004842002B00D108007047034A9818FBE799 -:10D37000963A9140F4E7C046FFFFFF7F70B5002862 -:10D380003DD0C317C5185D40C40F280001F0F4FE5E -:10D390009E22121A962A07DCD2B2082833DD0838FA -:10D3A00085406802400A23E0992A0BDD0523290005 -:10D3B0001B1AD94003001B339D402B005D1EAB415F -:10D3C00019430D00052801DD431F9D402B000F4927 -:10D3D0000B406E0709D00F263540042D05D00433CD -:10D3E0005D0102D59F220B40121A9B01580AD2B24E -:10D3F0004002D205400AE4071043204370BD0024D8 -:10D4000000220020F4E76802400AF1E7FFFFFFFB7B -:10D4100070B5041E34D001F0AFFE9E22121A962A77 -:10D4200007DCD2B208282EDD083884406002400AAA -:10D4300021E0992A09DD030021001B3399404B1E8E -:10D44000994105231B1ADC400C43052801DD431FCD -:10D450009C4023000D490B40650709D00F252C4047 -:10D46000042C05D004335C0102D59F220B40121A14 -:10D470009B01580AD2B24002D205400A104370BD47 -:10D4800000220020F7E76002400AF4E7FFFFFFFBFD -:10D49000F0B54F464646D6460C000903C0B5490ACA -:10D4A000470F5E0039431F03DB0F9C4665007B0A74 -:10D4B000570F1F436D0DE40F760DA146C000B8460F -:10D4C000D200AB1B64457BD0002B5FDD002E00D06B -:10D4D000A4E03E00164300D112E15E1E002E00D0F3 -:10D4E0009EE1871A4346B84280410125C91A40424D -:10D4F000091A0B0200D431E149024E0A002E00D174 -:10D500006EE1300001F038FE0300083B1F2B00DD08 -:10D5100061E120223900D21A9E40D1409F400E4343 -:10D520009D4200DD51E15D1B6B1C1F2B00DD7CE18A -:10D530002021C91A3D0030008F408840DD40791E0F -:10D540008F41310007222843D940002507433A4044 -:10D55000002A09D00F233B40042B05D03B1DBB42C2 -:10D56000BF417F42C9191F000B0200D426E26A1C8A -:10D57000C64B55056D0D9A4200D106E1C44AFF081D -:10D580000A40530752023B43120B8EE0002B00D09F -:10D59000B8E06B1C5B055B0D012B00DC30E1871AEA -:10D5A0004346B842B641CB1A76429E1B330200D5A1 -:10D5B0004CE13B003343A1D100220024002570E060 -:10D5C000002B00DCE5E0002E00D183E0AF4EB54239 -:10D5D00060D0802636043743B846382B00DC3EE165 -:10D5E000434613431F007A1E97413F1887428041EC -:10D5F000404209180B0200D4B0E0A44B01359D4213 -:10D6000000D1C3E0A24A7B080A4001210F401F431A -:10D610005108D30707221F433A4099E79B4EB54272 -:10D6200038D0802636043743B846382B00DDDCE09E -:10D630001F2B00DC30E11E004746203EF740BC4671 -:10D64000202B04D04026F31A46469E403243170052 -:10D6500063467A1E97411F43CCE0002B00D104E2C1 -:10D660004346134300D159E14B07C00818438023B8 -:10D67000C9081B03194208D04546ED081D4204D1D4 -:10D680004346D008590708432900420FC9007F4D7F -:10D690001143C0004B07CA087C49C00803438D42B0 -:10D6A00068D012036D05120B6D0D002112031800D6 -:10D6B000130B0A0D12051A43764B2D0513402B430D -:10D6C0005B00E4075B08234319001CBC90469946A5 -:10D6D000A246F0BD3E00164312D05E1E002E00D0C2 -:10D6E00000E18718874280414144404209180125E2 -:10D6F0000B0233D5022585E764463300414610000E -:10D700001D00C7E7002D00D0DAE00C000443F3D081 -:10D710005C1C00D19FE15D4CA64200D12FE1DB43B0 -:10D72000382B00DD66E11F2B00DD83E1202405009E -:10D73000E41A0F00DD40D940A0404346A7405B1AE1 -:10D74000441EA04198462F433843171ABA429241CB -:10D7500043465242991A64463500CAE607223A40C7 -:10D76000002A00D0F6E64B07CA084849FF083B43A9 -:10D770008D4296D11900114300D19EE18021090309 -:10D780000A431203120B414D8FE7150000220023BC -:10D790008BE7002B00D0C7E06B1C5F057F0D012FCE -:10D7A00000DCF1E0394DAB4200D1B9E085180A0048 -:10D7B000854289414244494251180722CF076D08EA -:10D7C0002F4349083A401D00C2E607223049ED1AAE -:10D7D00031403A40BCE63E002838864000279FE6AC -:10D7E000380001F0C9FC20308EE6434613431F0089 -:10D7F0007A1E9741C71BB84280414042091A78E619 -:10D800000E003B0006431343002D61D1002E00D0D3 -:10D81000F4E0002B00D11BE164463900100039E729 -:10D820001A4FBB427AD03300FFE630001F3DE8407C -:10D83000202B03D04021CB1A9E4037437B1E9F41B3 -:10D84000072207433A400021002589E7171A43467B -:10D85000BA42B641591A76428E1B64464EE61F2BD9 -:10D8600000DDADE020264746F61AB740B94617005E -:10D87000B246DF404E463E4337005646B240561E43 -:10D88000B24117434246DA408918AEE6FF0700006E -:10D89000FFFF7FFFFFFF0F8020264746F61AB740A5 -:10D8A000B9461700B246DF404E463E433700564663 -:10D8B000B240561EB24117434246DA40891A99E7F0 -:10D8C0007F4CA6425BD0802424045B42214327E79F -:10D8D000002E0CD1002B00D1CBE0644639001000A3 -:10D8E000774DD7E6764FBB4218D0330075E6002B54 -:10D8F00014D04B07C00818438023C9081B031942E2 -:10D9000007D0FC081C4204D17907D0080843E1463F -:10D9100021004C46420FC9001143C000684DB9E6D2 -:10D920001D0000220023C0E6002D5BD10D00054341 -:10D9300000D1E2E65D1C00D1B0E0614DAE421FD0E7 -:10D94000DB43382B71DC1F2B00DD96E020250F0018 -:10D95000ED1AAF40B9460700AA46DF404D463D43A9 -:10D960002F005546A840D940451EA841884407438A -:10D97000BF18974292415142414435003AE664460D -:10D9800035004146100085E60B000343002D00D012 -:10D9900063E6002BF5D04346134300D17AE687189F -:10D9A0008742804107224144404209183A400B0215 -:10D9B00000D4D5E6434B01351940C9E5380069E686 -:10D9C0001E004746203EF740BC46202B04D0402690 -:10D9D000F31A46469E403243170063467A1E97412B -:10D9E0001F4302E6364DAE42CAD080252D045B426D -:10D9F0002943A6E70843411E8841A6E6002B00D133 -:10DA000048E6871A4346B842B641CB1A76429E1B77 -:10DA100033024BD5171A4346BA429241591A524221 -:10DA2000891A072264463A4092E501430F00791EA5 -:10DA30008F419DE71C000F00203CE740202B03D0C6 -:10DA40004024E31A99400843411E884138437CE64C -:10DA50000022002425E6171A4346BA429241591A79 -:10DA60005242891A6446350043E541461000144D80 -:10DA700010E680220024120380E61D000F00203DE6 -:10DA8000EF40BC46202B03D04025EB1A99400843B9 -:10DA900007006346781E87411F4369E7871897424E -:10DAA0009B4141445B42C9183500A3E53B00334329 -:10DAB000CED0072231003A4052E600231A00F4E5A6 -:10DAC000FF070000FFFF7FFFF0B55746DE464E46DA -:10DAD0004546E0B5834607000E03480085B09246F0 -:10DAE0001C00360B400DCD0F002800D19DE0954B5A -:10DAF000984239D08023F6001B041E43924A7B0FC4 -:10DB00003343994694460300634400930023002660 -:10DB1000FF00029323031B0B98466300E40F524659 -:10DB20005B0D019400D1B3E086498B4200D19EE0A9 -:10DB30004246D100802212040A435146490F114344 -:10DB40008B46814952468C4600996344CB1A00218A -:10DB5000D20000932B0063409A460F2E00D905E1B6 -:10DB60007A4BB6009B599F465B463343994600D09B -:10DB7000B8E002230826002700900293CAE7CB46AC -:10DB80003A0002990195019B9A46022927D0032960 -:10DB900000D180E2012944D06D49009B8C4663444A -:10DBA0001C00002C38DD530700D013E2D2085B467E -:10DBB000DB0109D55946674B19408B468021C900C6 -:10DBC0008C46009B63441C00634B9C4207DC5B4615 -:10DBD00064055F075B0217431B0B620D02E0002325 -:10DBE0000027584A00211B031C0B0B0D1B05234368 -:10DBF00014055A4A380013401C4353466400DB079F -:10DC000064081C43210005B03CBC90469946A246DE -:10DC1000AB46F0BD0122524201231B1B382B00DC16 -:10DC2000ADE1002200230027DCE75B463343994641 -:10DC30005ED0002E00D18AE1300001F09DFA030091 -:10DC40000B3B1C2B00DD7BE11D22D31A5A46010041 -:10DC5000DA4008398E4013005F46334399468F40BF -:10DC60003F4B00261B1A00930023029352E74146C4 -:10DC700053460B433B499B468C46009B63440093B1 -:10DC80005B46002B3BD1022300221E43022161E7A9 -:10DC9000434613439B4637D04346002B00D162E1F5 -:10DCA000404601F069FA03000B3B1C2B00DD53E1F9 -:10DCB00002004146083A914088461D21CB1A514640 -:10DCC000D9400B0041460B439B46534693401A00F4 -:10DCD000009B25499C46604403008C4663440093A6 -:10DCE000002137E70323B14600900C26029311E789 -:10DCF0000023009301330426002702930AE703233D -:10DD0000C3461E43032125E701331E4300220121A0 -:10DD100020E700239A46802300271B03094A61E776 -:10DD2000802349461B03194200D1E2E059461942BB -:10DD300000D0DEE00B431B0317001B0BA246014A79 -:10DD400050E7C046FF07000001FCFFFF2440010030 -:10DD5000FF030000FFFFFFFEFE070000FFFF0F8034 -:10DD60000DFCFFFF01F8FFFFF3030000D94500D9C8 -:10DD7000CBE000D1C6E03C0048460027009B013BB9 -:10DD800000935B46160E1B021E4313029846330493 -:10DD90001B0C9946310C0191FEF7A0FA4A4642430A -:10DDA0000B04210C050019438A4207D98919013D4A -:10DDB0008E4203D88A4201D9851E8919881A019991 -:10DDC000FEF78CFA09048C464A4621046446424315 -:10DDD000090C030021438A4204D98919013B8E4270 -:10DDE00000D8F1E02D041D43AB464346891A424654 -:10DDF000280C12041D0C5B46140C22001B041B0C87 -:10DE00005A4303946B434443029568431B19150C12 -:10DE1000EB189C4203D980246402A44660441C0C85 -:10DE200015041B042D0C20185D19814277D373D083 -:10DE30000C1AA24A7D1BAF42BF419446009B7F4211 -:10DE40006344E01B1C00864200D1DBE00199FEF731 -:10DE500045FA4A4642430B04290C070019438A42FB -:10DE600007D98919013F8E4203D88A4201D9871EFA -:10DE70008919881A0199FEF731FA09044A46894638 -:10DE800029044D464243090C030029438A4207D91D -:10DE90008919013B8E4203D88A4201D9831E891910 -:10DEA0003F04891A3A00039F1A43380013041B0CDD -:10DEB000584381460298150C6F43434345434846F7 -:10DEC000000C8446DB1963449F4203D98020400242 -:10DED0008446654448461F0C00041B04000C7D1951 -:10DEE0001818A94200D284E000D17FE001231A4330 -:10DEF00057E680234A461B0313431B031B0BAA460A -:10DF00006F4A6FE6BA4200D935E74B46DC0758083E -:10DF10007B081C43FF0734E70024AF4289D2474403 -:10DF20004745A4415B466442A4196418013BA642DC -:10DF30001ED2A0426DD800D1B6E0241A9B4678E7E5 -:10DF400003005A46283B9A400027914688E65846E7 -:10DF500001F012F9203072E603005246283B9A4045 -:10DF600093460022B4E6504601F006F920309AE6C6 -:10DF7000A642E2D1B845DCD9341A9B4659E71F2B9B -:10DF800065DC504C0099A4465C46614408008C4016 -:10DF900011008240D940501E82410C4314435A461E -:10DFA000DA401300620709D00F222240042A05D06C -:10DFB0002200141D9442894149425B181A0262D51D -:10DFC0000122002300270DE68A4200D80AE7831EBB -:10DFD000891907E70F231340042B00D1E6E5171D2D -:10DFE0009742924153429B44FA08E0E5002800D151 -:10DFF000D7E57118531EB14227D3A94215D358D083 -:10E000001A0073E7002B00DC04E6012300229B4486 -:10E01000CDE502234744474589415B429C4649423E -:10E0200089190C19E344241A03E743465F00474566 -:10E030009B41B8465B429E19023A8919A94200D019 -:10E0400054E7404500D051E7ABE51A00F6E71F2141 -:10E050005F4649420C1BE740202B07D01A49009B22 -:10E060008C46634418005B4683401A43501E82412D -:10E070003A4307270023174009D00F2100231140FE -:10E080001400042995D122005F075B021B0BD20804 -:10E0900017430022A6E5802359461B030B431B03AD -:10E0A00017001B0B064A9DE5BD42B2D89B460024D3 -:10E0B000BFE68045B9D31A00C3E7C046FF0300009E -:10E0C000FF0700001E0400003E040000F0B54F46AC -:10E0D0004646D6468446C0B58046194E18030F03FF -:10E0E0004D00000B5C0082463F0B6D0DC90F914641 -:10E0F000640DDB0F0120B5420AD0B44203D0A54223 -:10E1000001D157450CD01CBC90469946A246F0BDA3 -:10E1100066463E43F7D1AC42F5D154461443F2D1A2 -:10E120000120C845EFD1994207D0002DEBD16346BD -:10E130001F433800471EB841E5E70020E3E7C0462B -:10E14000FF070000F0B54F464646D6464D00C0B525 -:10E150000E03C90F8A462C491F035C008046360B0C -:10E160006D0D91463F0B640DDB0F8D421ED08C422E -:10E1700016D0002D1ED130438446002C01D13A43E5 -:10E1800023D06246002A1AD09A4529D0514602204F -:10E190000139084001381CBC90469946A246F0BDA2 -:10E1A00039001143E5D002204042F4E73043FAD170 -:10E1B000AC420FD0002C0FD13A43E7D00CE0012243 -:10E1C000013B934318000130E5E763460020002B34 -:10E1D000E1D0DBE73A43E6D19A45D7D1A542D5DC79 -:10E1E000A54205DBBE42D1D808D00020BE42D2D223 -:10E1F00050460123013898430130CCE7C845C5D8C3 -:10E200000020C845F4D3C6E7FF070000F0B54F462D -:10E210004646D6464D00C0B50E03C90F8A462E4964 -:10E220001F035C008046360B6D0D91463F0B640D5D -:10E23000DB0F8D4218D08C4211D0002D18D1304305 -:10E240008446002C1ED13A431CD163460020002B8B -:10E2500030D0514602200139084001382AE0390007 -:10E260001143EAD0022025E03043FBD1AC4226D056 -:10E27000002C26D13A4324D15146022001390840CE -:10E28000013817E06246002A0FD09A45E1D1A54235 -:10E2900005DBBE42DDD819D00020BE420AD250466E -:10E2A000012301389843013004E00122013B9343EC -:10E2B000180001301CBC90469946A246F0BD3A4376 -:10E2C000D0D19A45C5D1A542C3DCE0E7C845C0D846 -:10E2D0000020C845E3D3EDE7FF070000F0B557463F -:10E2E000DE464E464546E0B5834606000F0348002D -:10E2F00087B092461D003F0B400DCC0F002800D187 -:10E300006FE0DE4B984238D08023FF001B041F4390 -:10E31000730F3B430193DA4B0027994600239B463A -:10E32000F60081442B0369001B0B52469846490DA9 -:10E33000ED0F002900D185E0D04B994200D173E068 -:10E340004346DA0080231B0413435246CC48520F45 -:10E3500084461343524600206144D2008944210080 -:10E36000694000918C46012149448A460F2F00D90B -:10E3700090E0C449BF00CF59BF465B463B43019381 -:10E3800000D06AE102230827002681469B46C9E7A0 -:10E3900032005846019B61460091022800D175E089 -:10E3A000032800D1FEE1012800D02CE10023002742 -:10E3B000002600253F032A0D3F0BB34812053A43C0 -:10E3C00002401B051343009A5B00D1075B080B4317 -:10E3D0003000190007B03CBC90469946A246AB46B7 -:10E3E000F0BD5B463B43019300D12FE1002F00D1EC -:10E3F000A5E1380000F0C0FE03000B3B1C2B00DD44 -:10E4000096E11D22D31A5A460100DA405E460839C9 -:10E410008F4013008E403B4301939C4B00271B1AF7 -:10E42000994600239B467DE7414653460B4393495B -:10E430008C46E144002B00D01AE10222022017434F -:10E4400000228CE7134300D10DE14346002B00D19D -:10E4500081E1404600F090FE02000B3A1C2A00DDEC -:10E4600072E10100434608398B4098461D239A1AF1 -:10E470005346D3401A004346134352468A40494606 -:10E48000081A824989468144002068E77B4B0027AF -:10E4900000268EE7140C1204120C1100370C3604FF -:10E4A000350C794328008C462E0060436044834637 -:10E4B00056432100300C804658467943404402912F -:10E4C000844506D98846802149028C46E04441466D -:10E4D00002913604010C360C00048B4681191E0C87 -:10E4E0001B041B0C0391190079438C4628007543CB -:10E4F0006544A8465843050C45447743A94203D9CF -:10E50000802149028C466744290C8C46390000045E -:10E51000000C2D042D186144AB440591594604911B -:10E5200001990F043F0C080C3900514342439046B7 -:10E5300002008C46090C8B4662437C4344445C4495 -:10E54000A04503D98021490288464244210C8846CF -:10E5500061460904090C8C46390059434343704312 -:10E560007E430F0CF6182404BE19644442448C46C2 -:10E57000B34203D980235B0298464044029B614624 -:10E580009846049B370443449B46AB45AD416B42E0 -:10E590000D0405992D0C8C467F196744FD18A8467B -:10E5A0005D462D19A542A44193466442A446C34446 -:10E5B000DC448F42BF4198459B4193459241A4451D -:10E5C000A4415B427F421F43360C52426442BF1952 -:10E5D0002243BF18624638184302D20D03991343F1 -:10E5E0006A020A43501E82416146ED0D2A434E02E3 -:10E5F0003243D90100D4B3E001265008324002432F -:10E60000DE0732435B08224C5444002C62DD510784 -:10E6100009D00F201040042805D0101D90429241CF -:10E6200052429B180200D90104D580241948E40005 -:10E63000034054441848844200DD27E75E075B022C -:10E64000D2081F0B630516435B0DB2E60023994603 -:10E650000133042700269B4664E603230197814685 -:10E660000C279B465EE6012201201743002276E636 -:10E67000032303201F43434671E6C046FF07000003 -:10E6800001FCFFFF64400100FFFF0F800DFCFFFF56 -:10E69000FF030000FFFFFFFEFE07000000238027AE -:10E6A00000933F030026434B83E6019B3200A446C0 -:10E6B000584670E6AC466EE6802701993F03394222 -:10E6C0002DD03B422BD11F433F033F0B009516003B -:10E6D000384B6EE601252D1B382D00DD66E61F2D1B -:10E6E00040DC35481C005044160082408440EE4017 -:10E6F000501E824134431443EB40620709D00F227D -:10E700002240042A05D02200141D94428041404238 -:10E710001B181A023ED501230027002649E6802750 -:10E72000019B3F031F433F033F0B0094214B40E6F7 -:10E7300003005A46283B9A40002601926DE658464F -:10E7400000F01AFD203057E603005246283B9A405D -:10E750001300002293E6504600F00EFD20307BE6C9 -:10E76000CA4650E71F201E004042041BE640202DF1 -:10E7700003D0124C5444A3401A43501E82413243EA -:10E7800007260027164009D00F2000231040140050 -:10E790000428B9D122005E075B021F0BD208164382 -:10E7A000002306E680273F031F433F033F0B16006D -:10E7B000004BFEE5FF0700001E0400003E040000C1 -:10E7C000F8B557464E464546DE460C000903E0B50F -:10E7D000490A460F5F0031431E03DB0F760A9B4652 -:10E7E000530F3343C84E6500C000E40FD2006D0DD7 -:10E7F000A24681467F0D9C469046B74200D1B9E0C3 -:10E800005B46012673409B46EE1BA34500D183E087 -:10E81000002E63DD002F00D0B1E06346134300D12A -:10E8200023E1731E002B00D0BAE1861A6346B04282 -:10E8300080410125C91A4042091A0B0200D447E160 -:10E8400049024B0A98464346002B00D189E14046D5 -:10E8500000F092FC0300083B1F2B00DD7CE120222E -:10E860003000D21A4146D040994002009E400A43EF -:10E870009D4200DD6AE15D1B6B1C1F2B00DD94E1F6 -:10E88000202110003500C91A8E40DA408840DD4052 -:10E89000711E8E4111000722284300250643324095 -:10E8A000002A09D00F233340042B05D0331DB34277 -:10E8B000B641764289191E000B0200D43DE26A1C63 -:10E8C000914B55056D0D9A4200D119E18F4AF6081A -:10E8D0000A40570752023743120B9BE0002E00D02C -:10E8E000C5E06E1C7605760D012E00DC48E167461A -:10E8F000861ACB1BB042BF417F42B8461F00434639 -:10E90000FF1A3B00B8461B0200D55FE137439AD19E -:10E9100000220024002579E0002E00DCFAE0002F20 -:10E9200000D18DE0784B9D4267D0802367461B0461 -:10E930001F43BC46382E00DC52E1634613435A1E87 -:10E9400093411E1886428041404209180B0200D4B0 -:10E95000BEE06D4B01359D4200D1D2E06B4A730899 -:10E960000A4001210E401E435108D30707221E43CF -:10E97000324095E71E00164300D045E740E7624B62 -:10E980009D423AD0802367461B041F43BC46382E65 -:10E9900000DDEBE01F2E00DC3AE133006746203B50 -:10E9A000DF403B00202E05D04027BF1B6646BE40FF -:10E9B000324390464646721E96413343DAE0002BBE -:10E9C00000D114E26346134300D168E180234E076F -:10E9D000C008C9081B030643194208D06046C00896 -:10E9E000184204D163460100D2085E071643F300C3 -:10E9F0009946C900720F444D11434B46DE08424B05 -:10EA00004F073743CA089D4200D16EE012036D05DF -:10EA1000120B6D0D00211203130B0A0D12051A4380 -:10EA20003B4B2D0513402B435B00E4075B0823435E -:10EA3000380019003CBC90469946A246AB46F8BD4A -:10EA40006346134311D0731E002B00D007E18618D4 -:10EA50008642804161444042091801250B0237D5A6 -:10EA600002257BE73E00614691463500C5E75C46DE -:10EA7000002D00D0E1E00B000343F3D0731C00D164 -:10EA8000ACE1214B9F4200D13AE1F343382B00DD4A -:10EA90006FE11F2B00DD8CE120250E00ED1AAE404A -:10EAA000B0460600AA46DE40454635432E00554690 -:10EAB000D940A8406346451EA8415B1A9C46304396 -:10EAC000161AB242924163465242991A3D00B4E688 -:10EAD00007223240002A00D0E4E60B4BF6084F072D -:10EAE0003743CA089D4200D090E73B00134300D152 -:10EAF000A6E180231B031A431203120B024D89E780 -:10EB000015000022002785E7FF070000FFFF7FFFB9 -:10EB1000FFFF0F80002E00D0C7E06B1C5E05760D56 -:10EB2000012E00DCF0E0C84DAB4200D1B9E0851801 -:10EB30000A00854289416244494251180722CE07A2 -:10EB40006D082E43490832401D00A9E6BF49ED1A61 -:10EB5000114007223240A3E63200283882400026C6 -:10EB600086E6300000F008FB203073E6634613436E -:10EB70005A1E9341C61AB04280414042091A5CE6CF -:10EB80000E00674606431743002D5ED1002E00D0CD -:10EB9000F3E0002F00D11EE15C46614691462CE770 -:10EBA000A94FBE427BD01E00F1E610001F3DE84099 -:10EBB000202B03D04021CB1A9A401643731E9E414E -:10EBC0000722064332400021002583E7161A6346D8 -:10EBD000B2428041591A40420B1A98465C4632E6CE -:10EBE0001F2E00DDABE02027BB1B9A46634657462D -:10EBF000BB40994613004F46F3401F433B00574626 -:10EC0000BA40571EBA4113436246F240891898E64B -:10EC10002027BB1B9A4663465746BB4099461300C4 -:10EC20004F46F3401F433B005746BA40571EBA4178 -:10EC300013436246F240891A9CE7834B9F425FD0A0 -:10EC400080252D047342294320E7002E0CD1002F8C -:10EC500000D1D0E05C46614691467B4DCDE67A4FCF -:10EC6000BE421CD01E0065E6002F18D0C0084E071B -:10EC700006438020C9080003014208D06346DC082F -:10EC8000044204D12100DA46D2085E071643F3009D -:10EC9000994601245346C900720F11431C406A4D26 -:10ECA000ABE61D0000220027B4E6002D59D10B0071 -:10ECB000034300D1D6E6731C00D1B2E0624B9F4201 -:10ECC0001ED0F343382B6FDC1F2B00DD97E020258F -:10ECD0000E00ED1AAE40B0460600AA46DE4045469C -:10ECE00035432E005546A840D940451EA8418C44C6 -:10ECF0000643B61896429241514261443D0025E6D2 -:10ED00003D006146914678E60B000343002D00D09C -:10ED100055E6002BF5D06346134300D16DE6861807 -:10ED20008642804161444042091800220B0200D40F -:10ED3000D0E6464B01351940B2E5B1465DE63300F9 -:10ED40006746203BDF403B00202E05D04027BF1BFD -:10ED50006646BE40324390464646721E9641334355 -:10ED6000EFE5394B9F42CBD080252D0473422943D8 -:10ED7000A8E70843411E8841A2E6002F00D13CE6E7 -:10ED80006346861ACF1AB0429B415B42FB1A9846F3 -:10ED90001B024ED5161A6346B2429241591A52428C -:10EDA000891A5C4600227BE501430E00711E8E41EC -:10EDB0009FE71D000E00203DEE40B046202B04D002 -:10EDC0004025EB1A99400843814648464346411E78 -:10EDD0008841184374E60022002417E6161A634699 -:10EDE000B2429241591A5242891A3D0025E56146C4 -:10EDF0009146154D01E680220024120379E61D009C -:10EE00000E00203DEE40B046202B04D04025EB1AEA -:10EE10009940084381464E464346711E8E411E432B -:10EE200067E7861896429B4161445B42C9183D00E2 -:10EE30008CE547463743CED007224146324049E66B -:10EE400000273A00E6E5C046FF070000FFFF7FFF0E -:10EE500070B50C4E0D031C0349005B002D0B490DD2 -:10EE6000240B5B0DB14208D0064900208B4203D130 -:10EE700014432000441EA04170BD05430120002D15 -:10EE8000FAD1F1E7FF07000030B5144D0A034B003B -:10EE9000120B5B0DC90F0024AB4211DD104CA342D5 -:10EEA00010DC8024640322430E4CE41A1F2C0CDD7A -:10EEB0000D48C31ADA4013005C42002900D11C003F -:10EEC000200030BD094BCC18FAE7094DE040AC46B4 -:10EED00063449A4013000343EEE7C046FE0300007C -:10EEE0001D0400003304000013040000FFFFFF7F37 -:10EEF000EDFBFFFF70B500282DD0C317C5185D408E -:10EF0000C40F280000F038F9154B1B1A5B055B0D88 -:10EF10000A2815DD0B38854000222D032D0B00211A -:10EF200010002D030A0D2D0B12052A430D4D1B0554 -:10EF30002A4013435B00E4075B082343190070BDBC -:10EF400002002900153291400A000B21081AC54021 -:10EF50002D032D0BE3E70024002300250022DEE72C -:10EF60001E040000FFFF0F8010B5041E25D000F026 -:10EF700003F9144B1B1A5B055B0D0A2812DD0B38D5 -:10EF8000844000222403240B0021100024030A0DD6 -:10EF9000240B120522430C4C1B05224013435B003B -:10EFA000590810BD02002100153291400A000B21C2 -:10EFB000081AC4402403240BE6E70023002400229F -:10EFC000E2E7C0461E040000FFFF0F804100090E6B -:10EFD0004B1C70B5DBB24602750AC40F012B14DD61 -:10EFE000E0239B006D07360BCB1800210A0D28008B -:10EFF00012051C4D32435B052A405B0813435B003E -:10F00000E4075B082343190070BD002914D1002DCB -:10F010001ED0280000F0B0F80A281CDC0B232A00C0 -:10F020001B1ADA40030015339D400F4B12031B1AC5 -:10F030005B05160B5B0DD8E7002D06D0320B802642 -:10F0400036036D071643094BCFE7084B0026CCE784 -:10F0500000230026C9E703002A000B3B9A40002545 -:10F06000E3E7C046FFFF0F8089030000FF070000B1 -:10F07000F0B54C00640D0B03621C5B0A460F520591 -:10F08000C90F1E43C500520D012A29DD374BE71871 -:10F09000FE2F1CDC002F3BDD8001431E9841072220 -:10F0A000F3006D0F03432B431A40002A04D00F22B4 -:10F0B0001A40042A00D004338022D2041A4024D0FB -:10F0C0000137FAB2FF2F02D09B01580A01E0FF225C -:10F0D00000204002D205400AC90710430843F0BD92 -:10F0E0003543002C04D1002D0AD100220020F0E786 -:10F0F000002DECD08020C0033043FF22E9E700243C -:10F1000000235B02580AE2B2E3E7DB083C00F8E7C1 -:10F110003B001733F3DB80231B0433431E26F61B0F -:10F120001F2E14DD02225242D71B1A00FA4017008C -:10F13000202E04D00E4A94466444A3401D432B0065 -:10F140005D1EAB4107223B431A400027ADE7094A49 -:10F150002800A218954093406C1EA541F04007225C -:10F160002B4303431A4000279FE7C04680FCFFFF64 -:10F17000A2FCFFFF82FCFFFF1C2101231B0498421D -:10F1800001D3000C10391B0A984201D3000A083938 -:10F190001B09984201D30009043902A2105C4018EF -:10F1A0007047C04604030202010101010000000093 -:10F1B000000000000B0010B50100180000F002F87C -:10F1C00010BD000010B5040004481300002804D04E -:10F1D0000A000220210000E000BF10BD0000000076 -:10F1E000014B18687047C0464C01002070B50026DE -:10F1F0000C4D0D4C641BA410A64209D1002603F04F -:10F2000057FC0A4D0A4C641BA410A64205D170BDE0 -:10F21000B300EB5898470136EEE7B300EB58984738 -:10F220000136F2E71C0300201C0300201C03002011 -:10F230003803002010B5040000F006F80823001978 -:10F240004078184010BD0000044B1B681B6A002B5F -:10F2500000D1034BEC3318687047C0464C010020C6 -:10F26000B001002010B5034B0100186800F09EF8B3 -:10F2700010BDC0464C01002010B5034B01001868BA -:10F2800000F04AF810BDC0464C01002082B00029B1 -:10F2900000D101A9101E06D0002B06D013780B60F8 -:10F2A0001078431E984102B0704702204042FAE7AE -:10F2B00030B50024A24201D1002005E0035D651CA9 -:10F2C0000C5DA34201D0181B30BD2C00F2E70023D7 -:10F2D00010B59A4200D110BDCC5CC4540133F8E79C -:10F2E00010B5884202D98B18984203D3002307E057 -:10F2F0008B5C8354013AFBD210BDCC5CC454013307 -:10F300009A42FAD1F8E703001218934200D17047ED -:10F3100019700133F9E7000070B50500002910D01D -:10F320000C1F2368002B00DAE418280002F01DFEF1 -:10F330001D4A1368002B05D163601460280002F099 -:10F3400015FE70BDA34209D9216860188342F3D12C -:10F3500018685B6841182160EEE713005A68002ABC -:10F3600001D0A242F9D919685818A0420BD12068DF -:10F370000918581819608242E0D110685268411883 -:10F3800019605A60DAE7A04202D90C232B60D5E756 -:10F3900021686018824203D11068526841182160C8 -:10F3A00062605C60CAE7C046683E0020032370B517 -:10F3B000CD1C9D43083506000C2D1ED20C25A942FC -:10F3C0001DD8300002F0D1FD254A14682100002923 -:10F3D00019D1244C2368002B03D1300000F0F8FC35 -:10F3E00020602900300000F0F3FC431C2BD10C23DB -:10F3F0003000336002F0BAFD03E0002DDFDA0C23A9 -:10F400003360002070BD0B685B1B19D40B2B03D934 -:10F410000B60CC18256003E04B688C420DD1136063 -:10F42000300002F0A3FD200007220B30231D904383 -:10F43000C31AE7D05A42E250E4E763600C00EFE7FA -:10F440000C004968C3E70323C41C9C43A042E1D0DD -:10F45000211A300000F0BCFC431CDBD1C7E7C046DA -:10F46000683E00206C3E0020F0B58BB014001D1EDD -:10F47000129953DA802424061B191D0014002D2331 -:10F480000B702023149E03229E43462E07D0330088 -:10F49000453B59424B411099013ACB18109309ABA7 -:10F4A000049308AB0393139B00920293109B2200DA -:10F4B00001932B0001F02EFC0700472E02D1119B77 -:10F4C000DB0724D5109BFB180793462E14D13B78FD -:10F4D000302B0CD10022002320002900FCF7FEFF76 -:10F4E000002804D10123109A9B1A139A1360139BCE -:10F4F000079A1B68D31807930022002320002900D5 -:10F50000FCF7ECFF302200280DD0079B0993380050 -:10F51000099B159ADB1B13600BB0F0BD0023AFE70E -:10F52000591C09911A70099B07998B42F8D3EEE791 -:10F53000F0B5831C85B0019306000C0002702B23EC -:10F54000002901DA2D234C427370092C22DD02AB15 -:10F55000DD1D20000A21FCF7ABFF6F1E3031397032 -:10F5600020000A21FCF7BEFE040009280BDC023D46 -:10F570003034019B2C7002AA07321800AA4204D82A -:10F58000801B05B0F0BD3D00E3E72A7801351A7015 -:10F590000133F0E73023E418B370301DF470EFE767 -:10F5A000F0B595B00C000A920B931A9D099002F0E9 -:10F5B000C5FC036818000C9300F04EFC0023129366 -:10F5C00023680E900D930D99277E2B680722C9059D -:10F5D00045D59B1893431A0008322A601A685B6865 -:10F5E000A264E3640122E56CA66C6B005B080F93D8 -:10F5F0005242AC4B30000F99FFF72AFC002830D163 -:10F600000122A84B524230000F99FCF777FF0028E7 -:10F6100027D10022002330002900FCF765FF0028D5 -:10F6200003D023002D2243331A709F4D472F00D85B -:10F630009E4D032300260D9A236101339A432260D5 -:10F640000B9B13AA009321000A9B099800F0F4F980 -:10F65000431C00D093E00120404215B0F0BD0733B9 -:10F66000B8E732002B0030002900FFF7F1FB00283B -:10F6700004D08F4D472FDCD88E4DDAE780230D99CB -:10F680006268DB000B43511C43D10732626012AA4F -:10F690000021059211AA039223222360069101936F -:10F6A00008A9636852180292009332002B00049755 -:10F6B0000998FFF7D9FE20233A0005009A43472A0C -:10F6C00007D1119BDA1C02DB6268934245DD023FE1 -:10F6D000FFB21199652F25D8200001393A0050302A -:10F6E0001191FFF725FF129A060013182361012AD2 -:10F6F00002DC2268D20701D501332361232308AA43 -:10F700009B181B78002B9BD023002D2243331A70AB -:10F7100096E7672F00D176E1472FB8D1002AB6D1FE -:10F720000122B3E7662F19D1636800290CDD21613E -:10F73000002B02D12268D20702D501335B18236166 -:10F74000119B0026A365D9E7002B03D10122216874 -:10F75000114200D09A1C2261F2E76727119B129A8E -:10F76000934205DB22682361D207E9D50133E6E73E -:10F770000121002B01DC4918C91A5218EBE7226855 -:10F78000530508D423692A000A9909980B9DA847B4 -:10F79000431C29D15FE7652F00D8E0E0A06CE16C45 -:10F7A00000220023FCF79AFE002834D00123424AAD -:10F7B0000A9909980B9DA847431C00D14BE7119B60 -:10F7C000129A934202DB2368DB070DD50B9D0E9B3B -:10F7D0000C9A0A990998A8470025431C00D13AE7DA -:10F7E000129B013BAB420ADC23689B0700D508E172 -:10F7F000139BE068984200DB2FE718002DE72200FA -:10F8000001231A320A9909980B9EB047431C00D174 -:10F8100021E70135E4E7119B002B2DDC0123264A6B -:10F820000A9909980B9EB047431C00D113E7119B1E -:10F83000002B05D1129B002B02D12368DB07D3D507 -:10F840000B9E0E9B0C9A0A990998B0470026431C00 -:10F8500000D100E7119B5B42B34201DC129B92E7AF -:10F86000220001231A320A9909980B9FB847431CBA -:10F8700000D1F0E60136EDE7A36D129F9F4200DD57 -:10F880001F00002F08DD3B002A000A9909980B9EF3 -:10F89000B047431C00D1DEE600230D93FB43DB178A -:10F8A0000F9318E0FFFFEF7FB0400100AC40010074 -:10F8B000B8400100B4400100BC4001002200012317 -:10F8C0001A320A9909980B9EB047431C00D1C2E630 -:10F8D0000D9B01330D930F9BA66D0D9A3B40F31AC0 -:10F8E0009342EBDC119B129AAD19934210DB236813 -:10F8F000DB070DD4129F119BBE1BFF1AB74200DD20 -:10F900003700002F0DDCFE43F31700250C931CE09D -:10F910000E9B0C9A0A9909980B9FB847431CE9D192 -:10F9200099E63B002A000A9909980B9DA847431CB9 -:10F93000E9D190E6220001231A320A9909980B9E18 -:10F94000B047431C00D186E60135119A129B0C99F1 -:10F950009B1A3A000A409B1AAB42EBDC44E7129B2D -:10F96000012B02DC01231A4232D001232A000A991A -:10F9700009980B9FB847431C00D16CE60E9B0C9A6C -:10F980000A9909980B9F0135B847431C00D162E6DC -:10F99000A06CE16C129B00225F1E0023FCF79EFD11 -:10F9A0003B00002814D000250AE0220001231A326F -:10F9B0000A9909980B9FB847431C00D14BE60135C3 -:10F9C000129B013BAB42F0DC220033005032DBE6FD -:10F9D0002A000A9909980B9DA847431CF4D13AE6DE -:10F9E0002200012319320A9909980B9EB047431C43 -:10F9F00000D130E60135E368139A9B1AAB42EFDC85 -:10FA0000F6E60025F7E7002A00D189E600210691F5 -:10FA100012A9059111A90391232108A809182360AF -:10FA2000019300922B000497029132000998FFF78E -:10FA30001BFD050045E6C046F7B5150001938A6831 -:10FA40000B6900900C00934200DA130022002B6037 -:10FA500043321278002A01D001332B6023689B06C1 -:10FA600002D52B6802332B60062723681F4027D05E -:10FA7000230043331B785A1E93412268920630D4E8 -:10FA80002200019943320098089EB047431C25D0BC -:10FA9000062320682A68E16803400025042B03D16F -:10FAA0008D1AEB43DB171D40A3682269934201DDE9 -:10FAB0009B1AED180027BD4220D1002010E001372D -:10FAC000E3682A689B1ABB42D2DD22000123193267 -:10FAD00001990098089EB047431CF0D10120404294 -:10FAE000FEBD3020E1184331087021005A1C453119 -:10FAF0000978A218433202331170C1E722000123B2 -:10FB00001A3201990098089EB047431CE6D001378D -:10FB1000D1E70000F0B589B004920A0043320593A2 -:10FB2000039002920A7E0C000E9B6E2A00D186E0A2 -:10FB30001FD8632A33D008D8002A00D18CE0582A75 -:10FB40004DD0250042352A7030E0642A01D0692A60 -:10FB5000F7D1196825680A1D280629D508681A6092 -:10FB6000002803DA2D23029A404213706B4E0A27B5 -:10FB70004FE0732A74D008D86F2A1FD0702AE0D1C2 -:10FB8000202209680A43226003E0752A16D0782AE9 -:10FB9000D7D12200782145321170614E22E0250034 -:10FBA0001A684235111D196013682B70012365E036 -:10FBB00008681A606906D3D500B2D1E719682568CC -:10FBC000081D186008682E0605D5544E08276F2AB0 -:10FBD0001BD00A2719E06D06F7D580B2F5E745314D -:10FBE0004E4E0A7018682268011D006819601506DB -:10FBF00021D5D30702D520231A43226010270028DD -:10FC000003D1202322689A43226023000022433339 -:10FC10001A706368A360002B58DB042221689143AB -:10FC20002160002854D1029D002B5AD02500337842 -:10FC300042352B7055E05506DBD580B2D9E71A68FE -:10FC40000D68101D4969186013682E0601D51960EA -:10FC500002E06D06FBD519800023029D23614FE071 -:10FC60001A68111D1960156800216268280002F0E9 -:10FC700071F9002801D0401B606063682361002394 -:10FC8000029A13703CE023692A0004990398059DA9 -:10FC9000A847431C3ED023689B0715D4079BE06808 -:10FCA000984239DA180037E022000123193204990A -:10FCB0000398059EB047431C2CD00135E368079A92 -:10FCC0009B1AAB42F0DCE9E70025F7E70028ADD04E -:10FCD000029D3900FCF702FB735C013D2B7000288C -:10FCE000F7D1082F09D12368DB0706D5636822699D -:10FCF000934202DC3023013D2B70029B5B1B23618E -:10FD0000059B07AA00932100049B0398FFF794FE2C -:10FD1000431CB8D10120404209B0F0BDBE400100F3 -:10FD2000CF4001000D4B70B51C680500A36B002B84 -:10FD30000FD11820FFF796FA094BA0630360094B17 -:10FD400001224360084B83600B23838100230261FF -:10FD500043610022A36B1D615A6170BD4C010020FC -:10FD60000E33CDAB34126DE6ECDE0500124B10B550 -:10FD70001C68A36B002B0FD11820FFF773FA0F4BF1 -:10FD8000A06303600E4B012243600E4B83600B2384 -:10FD90008381002302614361A46B0B4A206961697E -:10FDA0000A4BFCF703FC0122002380185941206113 -:10FDB00061614800400810BD4C0100200E33CDABFE -:10FDC00034126DE6ECDE05002D7F954C2DF4515874 -:10FDD000002370B5064C05000800236002F05AFEAF -:10FDE000431C03D12368002B00D02B6070BDC0469C -:10FDF000743E00200EB400B50B499CB01DAB0290C0 -:10FE00000690079104910948094904CB05910068BF -:10FE100002A9019302F038FD0023029A13701CB06E -:10FE200008BC03B01847C046FFFFFF7F4C0100200D -:10FE30000802FFFF02780B78002A03D0013001315D -:10FE40009A42F7D0D01A704703000A7801311A702D -:10FE50000133002AF9D170470023C25C0133002A24 -:10FE6000FBD1581E7047002330B59A420AD0013AA0 -:10FE7000C45CCD5CAC4204D1934202D00133002C6F -:10FE8000F6D1631B180030BD70B516000D0002F0EE -:10FE90003FFB002E0DD06B236A00520D9B1A002BE6 -:10FEA00007DD0024034A1B059D182B002200FEF7E6 -:10FEB00015FA70BD0000F03FF0B5A7B0079300231E -:10FEC00004900798229306911D9202F034F80500E1 -:10FED0000068FFF7C1FF00260027069B05902193CD -:10FEE000219B1A780D2A39D8092A3DD2002A44D0FC -:10FEF000002313930022219C0C922378302B00D0F6 -:10FF00007EE06378582B02D0782B00D06EE0079B00 -:10FF1000964A0293139B21A9019322AB0093049864 -:10FF200023AB01F020FD07250590054005D0062DE7 -:10FF30002FD100230134219413931D9B002B02D059 -:10FF40001D9B219A1A60139B002B1DD08023320029 -:10FF50001B06FB181000190027B0F0BD2B2A16D085 -:10FF60002D2A03D0202AC3D10133B8E701221392EE -:10FF70005A1C21925B78002BBCD1069B2193002355 -:10FF800013931D9B002BDBD132003B00E2E70022E4 -:10FF9000EDE7229A002A07D0352124A802F0A5FB1C -:10FFA0002299049802F01BF8681E042806D8FCF772 -:10FFB000F1F80C0317190C00249E259F059B1B07C5 -:10FFC000BBD580231B061F43B7E7259B684A6949B9 -:10FFD0001A40239B249E5B181B051A431700EDE76C -:10FFE000654FEBE70126654F7642E7E7219B5A1CF8 -:10FFF00021925B78302BF9D0002B9ED001230C93FB -:020000021000EC -:10000000219B0A22129300230B9308930A932198B1 -:1000100004782300303BD9B2092934D9059A2968DC -:10002000FFF721FF002840D00020230084460A9AD1 -:10003000079005922022190091430A000021452AC9 -:1000400000D0BFE0059B0C9A034313438B4294D02E -:10005000219B069301332193069B5B782B2B00D1C8 -:1000600082E00C002D2B05D10124069B0233219345 -:10007000069B9B781A00303A092A00D884E0069A39 -:10008000002121929EE00A99082909DC0899514330 -:10009000CB1808930A9B013001330A932190B6E7ED -:1000A0000B995143CB180B93F4E7219A059B944687 -:1000B0000A9A634421931B78002A36D0844605921D -:1000C0001A00303A092A12D901220792B2E7219B7D -:1000D00001305A1C21925B78302BF8D01A00313A4B -:1000E000082A3CD884460020219A05901292303B81 -:1000F000421C002B13D00A219444059A8518AA4269 -:1001000015D10599059A0131091812180591082A87 -:100110001BDC0A2208994A439B1800220893219B62 -:100120001000591C21915B78CAE70A98D4E7541C47 -:10013000082A04DC089A4A4308922200DFE7102CC0 -:10014000FBDC0B9A4A430B92F7E705990022102932 -:10015000E5DC0A210B9841435B180B93DFE7002293 -:10016000944605920132B0E70C007EE7E0400100C2 -:10017000FFFFEFFF330400000000F07FFFFFFF7F71 -:10018000219B5A1C21925B78302BF9D01A00313A0E -:100190000021082A16D8303B219D0E93219B591C23 -:1001A00021915B781A00303A092A32D94A1BAD4DA9 -:1001B0002900082A03DC0E99A94200DD2900002C41 -:1001C00000D04942059A002A49D10C9A104300D028 -:1001D000B3E6079A002A00D0CFE64E2B25D01FDCCD -:1001E000492B00D0C9E6A04921A801F0F9FD00285B -:1001F00000D1C2E6219B9D49013B21A8219301F03A -:10020000EFFD002802D1219B01332193984F002656 -:1002100093E60A220E994A43D218303A0E92BDE76D -:10022000692BE0D06E2B00D0A7E6924921A801F0FF -:10023000D7FD002800D1A0E6219B1B78282B0CD1EC -:1002400024AA8D4921A801F0DFFD052805D1259BB1 -:10025000874A249E1A4317006FE6884FD7E7634604 -:10026000CB1A07930A9B002B01D1059B0A93059C8F -:10027000102C00DD10240898FEF776FE059B060082 -:100280000F00092B15DC079B002B00D155E6079BBF -:10029000002B00DC7DE0162B62DCDC00784B3200AA -:1002A00019193B0008684968FEF718F806000F00A6 -:1002B00043E62200724B093AD2009B181A685B6829 -:1002C000FEF70CF806000B980F00FEF74DFE0B0032 -:1002D000020039003000FDF7DBF8059B06000F0037 -:1002E0000F2BD0DD059B1C1B079BE418002C00DCAA -:1002F00094E00F2323400AD06149DB00C918320083 -:10030000086849683B00FDF7E9FF06000F000F236E -:100310009C436ED02633FF339C4248DD0024059475 -:100320000B940A9422230026049A514F13600B9BCE -:10033000B34200D101E62299049801F050FE0A99D7 -:10034000049801F04CFE0599049801F048FE0B99C1 -:10035000049801F044FE2100049801F040FEECE511 -:100360002523059A9B1A079A9342BBDB0F23059A14 -:10037000434C9D1AE90061183200086849683B0047 -:10038000FDF7ACFF079B5E1BF600A6193268736889 -:100390008AE7079B1633A5DB079B3000DA00384B52 -:1003A00039009B1A1A685B68FDF78EFB7EE7002315 -:1003B000300039001D002411012C1DDC002B01D060 -:1003C00006000F002F4BED00FF182F4B32005D1978 -:1003D000286869683B00FDF781FF254B2B4A060022 -:1003E0000B4093429AD82A4A934213D90126294FA7 -:1003F0007642002306937BE00122144207D0224A72 -:10040000EB009B181A685B68FDF768FF0123013554 -:100410006410D1E7D4239B04CF18EAE7002CE8D07E -:100420000F23644223400AD0154ADB00D318300062 -:100430001A685B683900FDF747FB06000F002411BE -:10044000D7D01F2C28DD002405940B940A94222376 -:10045000049A002613600B9B0027002B00D06AE74C -:100460006BE5C0461F4E0000B14001003442010060 -:100470000000F07FB9400100F44001000000F8FFE7 -:10048000C84201000000B0FCA04201000000A07CB6 -:100490000000907CFFFFEF7F10232340069301D0E4 -:1004A0006A2306930023300039001D00002C00DD74 -:1004B0000EE1002B01D006000F00069B002B0FD091 -:1004C0006B237900490D3A005B1A002B08DD1F2BC6 -:1004D00000DC12E10026342B00DC07E1DC27BF043E -:1004E0000022002330003900FBF7F8FF0028AAD1D2 -:1004F000089B0A9A00931299059B049801F0C6FD87 -:100500000B90002800D109E7079B079ADB1752429E -:1005100013401593079B0024DB43079ADB171A400F -:100520001C9205940B9B0498596801F020FD0A90D9 -:10053000002800D1F6E60B9B0B991B690C319A1C25 -:1005400092000C300893FEF7C2FE24AB019323AB5C -:10055000009332003B0004980E960F9702F04AF881 -:100560002290002800D1DDE60121049801F015FE5B -:100570000590002800D1D5E6239B002B00DAC1E0CE -:10058000159A9D181C9A94460699249A5B1A36214E -:100590009B188A1AB249013B01208B4200DBBDE067 -:1005A000C91A521A1F2900DDB1E0884000231490B7 -:1005B0001093AB1808931300069A6344D318089A53 -:1005C0000C932B00954200DD13000C9A934200DD42 -:1005D0001300002B06DD089AED1AD21A08920C9A25 -:1005E000D21A0C92159B002B17D01A00059904986B -:1005F00001F06CFE0590002800D193E60100229ADC -:10060000049801F0D3FD1290002800D18AE62299C7 -:10061000049801F0E4FC129B2293089B002B00DD60 -:100620007FE0079B002B08DD1C9A0A99049801F0D3 -:100630004DFE0A90002800D174E60C9B002B08DDCB -:100640001A000A99049801F093FE0A90002800D13C -:1006500068E6002D08DD2A000599049801F088FE5F -:100660000590002800D15DE60A9A2299049801F0CD -:10067000EBFE041E00D155E60025C3680599C56050 -:10068000129301F0C7FEA84255DA129BAB421BD170 -:10069000AE4219D13B03AB4216D1D622714BD204E4 -:1006A0003B40934210D96369AB4202D12369012BCD -:1006B0000ADD21000122049801F05AFE0599040088 -:1006C00001F0A8FE00286BDC069B002B78D132E6F7 -:1006D0000122144207D0644AEB00D3181A685B6801 -:1006E000FDF7FCFD012301356410DFE60121203B0D -:1006F000494299400A401700F2E6012252429A40CC -:100700001640EDE61C9A159DD21A94463CE75749CF -:10071000CB1A01009940109114904AE700231093DE -:10072000FAE7089A2299049801F022FE2290002804 -:1007300000D076E7F6E50C97002800D0A0E0129AEA -:100740003B031B0B002A27D0494A934257D1012271 -:10075000069B524231001000002B0BD0D425414B98 -:10076000ED043B401000AB4204D86C301B0DC31AA3 -:100770009A401000814242D13E4B0C9A9A4202D1DB -:100780004B1C00D1CEE5374B0C9A00261A401300C3 -:10079000802252039F1897E7002B30D1002E2ED1D4 -:1007A00006993B002F4A002920D01100D62039405D -:1007B000C00481421ADCDC239B04994200DC46E63B -:1007C0002D4B30001B9300231A931A9A1B9B390060 -:1007D000FDF784FD06000F000B1E00D0ABE50028DE -:1007E00000D0A8E5049A22331360A4E50126134043 -:1007F000224A76429B181E4A1A43170064E7109B50 -:10080000002B15D00C9B109A134200D15CE7129B71 -:10081000069A30003900002B0FD0FFF735FB02009D -:100820000B000E980F99FCF733FE06000F004BE704 -:10083000149B334200D147E7E9E7FFF725FB0200AD -:100840000B000E980F99FDF7BBFF00220023060056 -:100850000F00FBF743FE002800D0F8E534E7C04660 -:1008600002FCFFFF0000F07F08410100E2FBFFFFF8 -:10087000FFFF0F00FFFFEF7F000050390000F0FF87 -:100880000599200001F00AFF80230022DB05089073 -:100890000991FBF733FE002800D17FE0129B002B6B -:1008A0004BD000228A4B10921193894B0893894AAE -:1008B0000C9B13401493149A874B9A4200D0BAE0D1 -:1008C0000E9A0F9B1A921B93844A0C9B9446634486 -:1008D0001F00109A119B300039000C920D9301F00B -:1008E00017FE02000B000C980D99FDF7F7FC320083 -:1008F0003B00FCF7CDFD774A794B06000A409A424F -:1009000064D90F9B774A934203D10E9B013300D1E8 -:1009100008E50126734F76422299049801F05FFBA7 -:100920000A99049801F05BFB0599049801F057FBC4 -:100930002100049801F053FBF4E5002E1ED10C9B1E -:100940001B0321D1089809990022614BFBF7CCFDCC -:1009500000281FD1089809990022634BFDF7BEFCBF -:1009600005000891802212069446089B1E95634458 -:100970001F931E9A1F9B1092119398E7012E03D18B -:100980000C9B002B00D162E50022584B00251092F1 -:1009900011938AE7544B129D0893E3E7524B089852 -:1009A00009990022FDF79AFC129B05000891002B83 -:1009B00007D180231B06CB1816901793169A179B06 -:1009C000D9E70200089B16921793F7E7D4239B04FC -:1009D000CF18069B002B9FD13E4B149A0C973B409F -:1009E0009A4299D108992800FEF74EFAFEF782FA4A -:1009F0000B00020008992800FDF7E2FE129B089008 -:100A00000991002B04D1002E02D10C9B1B0358D05E -:100A100008980999364A374BFBF766FD002800D045 -:100A200089E408980999344A2F4BFBF771FD002897 -:100A300000D171E77FE4069B002B2AD0D423149ABF -:100A4000DB049A4225D82D4A2D4B28000899FBF744 -:100A500055FD002816D028000899FBF7E9FD00286D -:100A600000D10130FEF780FA129B05000891002B9F -:100A700022D180231B06CB1818901993189A199B22 -:100A800010921193D622119BD2049B18149A9B1A90 -:100A900011930E980F99109E119F01F039FD0200DD -:100AA0000B0030003900FDF719FC0E9A0F9BFCF784 -:100AB000EFFC06000F008CE70200089B18921993C8 -:100AC000DCE70B4A0F4B08980999FBF70DFDAEE7E1 -:100AD0000000F03F0000F07F0000E07F0000B0FC6D -:100AE000FFFF9F7CFFFFEF7F0000E03F0000F0BFB3 -:100AF0009535A094FFFFDF3F35E5AF350000C0FF1F -:100B0000FFFFDF41FFFFCF3F064B10B50400186821 -:100B10000A00036A002B00D1034B2100FFF7CCF938 -:100B200010BDC0464C010020B0010020F8B5124BAA -:100B300006001D680F00AC6D002C16D15020FEF78A -:100B400091FBA865046044608460C46004614461F2 -:100B500084618462C462046344638463C463046420 -:100B600044648464C46404774462AA6D390030002C -:100B7000012300F003F8F8BD4C010020F0B5002877 -:100B800004D11068002801D1F0BD20000E000578C6 -:100B9000441C3778002F04D1002D10D11560280097 -:100BA000F2E70136BD42F4D1002BEED114600370A0 -:100BB000EAE733000DE00135002F03D11C000D00E2 -:100BC0002678631C2F78BE42F5D1002EF1D000218B -:100BD00021701360D8E710B50400FEF735FB03223F -:100BE000001943781340022B00D1203C200010BD97 -:100BF0000B1E04D0FF2A04D98A2303608B3B180004 -:100C000070470A700123FAE7F0B503690C6989B0EF -:100C1000070005910026A34265DB0B00013C14335D -:100C2000A50003935B1904930300143302935D1929 -:100C3000049B28681B68591C0193FBF7C9FA0190B3 -:100C4000B04229D0039B02989C460696624608CA89 -:100C50009446019A99B251431B0C534389190A0CCB -:100C60009B1807931E0C036889B29AB2069BD21890 -:100C70000368521A190C6B469B8BCB1A11145B1824 -:100C8000191492B21B041A43049B069104C06345D5 -:100C9000DCD22B68002B2DD00599380001F0BAFB6F -:100CA00000281FDB0025019B029801330399019363 -:100CB000026808C992B255199AB2AA1A05681B0CA3 -:100CC0002D0CEB1A15145B191D1492B21B0413435F -:100CD00008C0049B8B42EBD2029AA300D3181A6877 -:100CE000002A11D0019E300009B0F0BD2B68002B06 -:100CF00004D1013C029B043DAB42F7D33C61CBE7FE -:100D00001A68002A04D1013C029A043B9A42F7D3A4 -:100D10003C61E7E7F0B516001F00446A9BB0029003 -:100D2000239D06960797002C08D11020FEF79AFA0B -:100D3000029B5862446084600460C460029B5B6AEA -:100D4000196800290BD05A68012393404A608B60D0 -:100D5000029801F044F90022029B5B6A1A60002F9E -:100D600020DA01232B607B005B080793079CB24BC2 -:100D7000220017931A409A4217D1B04B229A13605F -:100D8000069B002B03D1240301D100F087FDAC4B5F -:100D9000249A0893002A02D0AA4B249A1360089838 -:100DA0001BB0F0BD00232B60E0E7069E079F0022EA -:100DB000002330003900FBF791FB051E0BD0012307 -:100DC000229A1360A04B249A0893002AE7D09F4AE6 -:100DD00024990A60E3E718AB019319AB0093320042 -:100DE0003B00029801F006FC630003905B0D74D099 -:100DF0003A03974C120B14432100964A3000169583 -:100E00009E180022944BFDF7DBFC944A944BFDF7AF -:100E100065FA944A944BFCF73BFB040030000D004C -:100E2000FEF768F8914A924BFDF758FA02000B0062 -:100E300020002900FCF72CFB04000D00FEF724F82D -:100E400000220700002320002900FBF74DFB0028AB -:100E500009D03800FEF74EF82B002200FBF73EFBCE -:100E6000434258413F1A01231493162F0DD881494C -:100E7000FB00C91808684968069A079BFBF748FBFE -:100E8000002846D00023013F1493189B9E1B00238B -:100E90000A933300013B0B9304D501239B1B0A9358 -:100EA00000230B93002F36DB0B9B1197DB190B9361 -:100EB00000230493209B0124092B00D984E0052BF7 -:100EC00002DD0024043B2093209B981E032800D9B8 -:100ED00084E0FBF75FF97577286A189B199A9E18CA -:100EE000654BF218202A0FDD40239B1A9C40634B70 -:100EF0000698F318D8402043FEF736F80123604CDB -:100F0000013E091916937CE7202306989B1A984006 -:100F1000F2E71490B9E70A9BDB1B0A937B42049328 -:100F200000231193C6E701230D93219B002B59DD6C -:100F30000C930993029A556A00226A6004321000E9 -:100F400014306968984252D9029801F010F8029B57 -:100F500028605B6A1B680893099B0E2B00D90AE185 -:100F6000002C00D107E1069B079C12931394002FDD -:100F700000DC89E00F213A003E4B0A40D2009B186A -:100F80001D685E6802233C110E93E3060BD53D4BB2 -:100F90000C401A6A5B6A12981399FCF795FD0323BB -:100FA000069007910E9300233AE001230D93219BB5 -:100FB000FB180C9301330993002BBBDC0123B9E729 -:100FC0000023B1E70023F1E700230D942093013BB8 -:100FD00000220C93099313332192ABE701230D9365 -:100FE000023BF5E701230C9309931A00F4E7013162 -:100FF00069605200A3E701231C420ED00E9B214AD8 -:1010000001330E93109B2800DB009B1831001A68F7 -:101010005B68FDF763F905000E00109B6410013357 -:101020001093002CE7D1069807992A003300FCF7AB -:101030004BFD0690079142E00000F07F0F27000073 -:101040003A4201003D420100BC400100BD400100A8 -:101050000000F03F01FCFFFF0000F83F61436F63B9 -:10106000A787D23FB3C8608B288AC63FFB799F50C1 -:101070001344D33FC84201003204000012040000B0 -:10108000000010FEA042010002230E93002F16D094 -:10109000129813997C420D332340CE4ADB00D318BB -:1010A0001A685B68FDF71AF9002301261D000690F7 -:1010B00007912411002C00D08CE0002BB9D1149B97 -:1010C000002B00D196E0069C079D0022C24B200019 -:1010D0002900FBF709FA002800D18BE0099B002BBF -:1010E00000D187E00C9B002B41DDBC4B002220008F -:1010F0002900FDF7F3F80E9B0690079101330E933C -:101100000C9B7E1E0E981093FDF7F4FE069A079B2B -:10111000FDF7E4F80022B24BFCF7BAF9B14A0E90A1 -:101120000F910E9C0F9D94460F9B634415931D00D9 -:10113000109B002B61D1069807990022AA4BFDF75E -:101140003FFB2200159B06900791FBF7E1F9002871 -:1011500000D0A8E2A5480F99844661440B00069888 -:1011600007992200FBF7C0F9002800D095E2129BF6 -:10117000139C06930794199B002B00DA5CE10E2F59 -:1011800000DD59E1934BFA009B185C681B680493DF -:101190000594219B002B00DBD8E0099B002B00DD90 -:1011A000D4E000D07DE20498059900228E4BFDF733 -:1011B00095F8069A079BFBF7B5F9099B04931E0067 -:1011C000002800D04FE2089B089A5D1C31230137AC -:1011D00013704BE234420AD00E9B854A01330E93C2 -:1011E000EB009B181A685B68FDF778F83300641011 -:1011F00001355FE73E00099B84E7109B0D995A1E5D -:10120000744BD2009B181A685B68002949D00020F3 -:101210007849FCF759FC2B002200FDF7D1FA089B16 -:101220000E900F91159306980799FDF72DFE040077 -:10123000FDF760FE02000B0006980799FDF7C0FA63 -:10124000159B30345D1C1C700E9A0F9B0690079105 -:10125000FBF74AF900286CD1069A079B00205E49EB -:10126000FDF7AEFA0E9A0F9BFBF73EF9002800D06F -:10127000C3E0089B109AEB1A9A4200DC77E70E98BD -:101280000F990022554BFDF729F800220E900F917F -:1012900006980799514BFDF721F81595069007918F -:1012A000C1E720002900FDF719F8089C109A2300D7 -:1012B00094460E900F916344159306980799FDF795 -:1012C000E3FD0500FDF716FE0B00020006980799E6 -:1012D000FDF776FA3035159B25700134069007919D -:1012E000A3422AD10E980F99089A109B9446634402 -:1012F00000221D003F4BFCF7CBF802000B000698C4 -:101300000799FBF705F9002800D076E00E9A0F9BAD -:1013100000203849FDF754FA02000B00069807999F -:10132000FBF7E2F8002800D121E76B1E1A78302A7B -:1013300001D037003FE01D00F7E70698079900222B -:10134000264BFCF7CBFF06900791B6E7089E049A60 -:10135000059B06980799FCF7B7FBFDF795FD040080 -:10136000FDF7C8FD049A059BFCF7B8FF02000B00CF -:1013700006980799FDF724FA230030333370099A51 -:10138000089B751CEB1A9A4248D102000B00FCF72F -:101390007FF8049A059B06900791FBF7B9F800289F -:1013A0002AD1049A059B06980799FBF797F800281D -:1013B00001D0E30720D40399029800F010FE002327 -:1013C0002B70229B01371F60249B002B00D1E6E489 -:1013D0001D60E4E4C84201000000F03F000024402A -:1013E00000001C400000C0FC000014400000C07C55 -:1013F000A04201000000E03F3E006B1E1A78392A2F -:1014000006D1089A9A4207D130231370130001368F -:101410001A7801321A708CE71D00EEE70022A44B07 -:10142000FCF75CFF00220023069007912E00FBF7DB -:1014300055F800288BD0BEE70D9A002A00D1DAE0DB -:10144000209A012A00DDBDE0169A002A00D1B5E0FD -:10145000984A9B18049D0A9C0A9A0121D2180A9264 -:101460000B9A0298D3180B9300F097FE0600002CFD -:101470000EDD0B9B002B0BDD0B9A2300944200DD4D -:1014800013000A9AE41AD21A0A920B9AD31A0B93EF -:10149000049B002B1FD00D9B002B00D1AFE0002D33 -:1014A00011D031002A00029800F010FF039A0100C9 -:1014B0000600029800F07AFE03991090029800F05E -:1014C0008EFD109B0393049B5A1B04D00399029832 -:1014D00000F0FCFE03900121029800F05EFE119BDB -:1014E0000490002B00D1E3E11A000100029800F003 -:1014F000EDFE209B0490012B00DD82E00025069B81 -:10150000AB420ED1079B1B03AB420AD1179B079A34 -:10151000134206D00A9B013501330A930B9B01331A -:101520000B93119B0120002B6CD10B9B18181F23D0 -:1015300018407ED001331B1A042B70DD1C23181AAF -:101540000A9B24181B180A930B9B1B180B930A9BCE -:10155000002B05DD1A000399029800F009FF0390A3 -:101560000B9B002B05DD1A000499029800F000FF88 -:101570000490149B002B5ED00499039800F04AFF5E -:10158000002858DA00230A220399029800F040FD4F -:101590000D9B013F0390002B00D190E1002331000F -:1015A0000A22029800F034FD0C9B0600002B00DD9F -:1015B00088E0209B022B00DC84E044E03623189A6C -:1015C0009B1A47E7099B5D1E049BAB4208DB5D1B32 -:1015D000099B002B0CDA0A9B099A9C1A00233BE713 -:1015E000049B0495EA1A119B00259B181193EFE7C1 -:1015F0000A9C099B30E7049D0A9C0D9E37E7049ADC -:1016000064E70025049B049A1B69109303339B0035 -:10161000D318586800F078FD2023181A85E7042BAA -:1016200095D00A9A1C33D2180A920B9AE418D31850 -:101630008CE70300F5E7099B002B3DDC209B022B88 -:101640003ADD099B0C930C9B002B0CD104990522CD -:10165000029800F0DDFC04900100039800F0DAFE2F -:10166000002800DDAFE5219B089DDF43002309939F -:101670000499029800F0B3FC002E00D19BE6099B70 -:10168000002B05D0B34203D01900029800F0A7FC4C -:101690003100029800F0A3FC8DE6109B04931E001D -:1016A000E1E70023FAE7109B370004931E008AE568 -:1016B00000002440330400000D9B002B00D1B2E059 -:1016C000099B0C93002C05DD31002200029800F0EC -:1016D0004FFE06003000002D11D07168029800F016 -:1016E00046FC3100040033690C319A1C92000C3026 -:1016F000FDF7EDFD01222100029800F039FE099668 -:101700000600089B0A9304990398FFF77DFA09994C -:101710000D900400039800F07DFE320010900499B3 -:10172000029800F091FE01230B93C368303405004A -:10173000002B04D10100039800F06CFE0B902900EF -:10174000029800F04CFC0B9B209A13430ED1069A92 -:10175000013313420AD1392C26D0109B002B01DD16 -:101760000D9C31340A9B5D1C1C7081E7109B002B83 -:1017700007DB1D00209B1D4322D10123069A134243 -:101780001ED10B9B002BEDDD03990122029800F086 -:10179000EFFD0499039000F03DFE002802DCE1D14A -:1017A000E307DFD5392CDBD10A9B0A9A5D1C39236C -:1017B00013706B1E1A78392A67D001321A7057E7F6 -:1017C0000A9B5D1C0B9B002B04DD392CECD00134F3 -:1017D0000A9BC9E70A9B0C9A1C70089BEB1A934260 -:1017E0003ED000230A220399029800F011FC099BC5 -:1017F0000390B34209D100230A220999029800F00C -:1018000007FC099006000A957DE7099900230A2242 -:10181000029800F0FDFB002309900A223100029893 -:1018200000F0F6FBEEE7099B0C93089B06930499E6 -:101830000398FFF7E9F9069B30305D1C18700C9A8D -:10184000089B0400EB1A9A4208DD00230A22039940 -:10185000029800F0DDFB06950390E8E7002309936A -:1018600003990122029800F083FD0499039000F08F -:10187000D1FD00289DDC01D1E3079AD46B1E1A78B4 -:10188000302A00D0F4E61D00F8E7089A9A4203D106 -:10189000312301371370EBE61D008AE70B4B249AC6 -:1018A00008930B4B002A01D0FFF777FAFFF777FA7E -:1018B000209B012B00DC21E6119D012035E60C9BCD -:1018C000002BB2DC209B022BAFDDBCE631420100D5 -:1018D00039420100F0B50400056914344B112600AB -:1018E0009D4214DD1F22AD009B0011406519E318D5 -:1018F000220000291ED004CB2027CA40A4467F1A0C -:1019000066469D420BD83260002A00D00436331B55 -:101910009B100361A64201D100234361F0BD1E6804 -:10192000BE403243664604C604CBB446CA40E7E72D -:1019300002CB02C216009D42FAD8E8E70300303B12 -:10194000092B03D82038C3B2180070470300613B4D -:10195000052B01D84738F6E70200413A0023052A53 -:10196000F2D82738EFE7F0B58DB0059014980991BB -:1019700002920A9300F0DFFA036818000793FEF75B -:101980006BFA0222079B52421B18013B1B78049002 -:101990000B93099B19688B1C521AD118019308915B -:1019A0000199013308783028F7D0FFF7C7FF071EE9 -:1019B00000D06FE0049A07990198FEF754FA0028C6 -:1019C00000D06DE0019B049A9C182078FFF7B6FFC9 -:1019D000002869D00194019B1878302809D0FFF7BE -:1019E000ADFF434243410393012326000893019C2A -:1019F00004E0019B01330193EDE701342078FFF708 -:101A00009DFF0028F9D1049A07992000FEF72BFAD0 -:101A1000002809D1002E0AD1049BE4182600207862 -:101A2000FFF78CFF002839D10027BE4201D0371BB9 -:101A3000BF002378502B01D0702B42D163782B2B21 -:101A400034D02D2B36D00023661C06933078FFF758 -:101A500075FF431EDBB2182B33D810380500013652 -:101A60003078FFF76BFF431EDBB2182B24D9069B9F -:101A7000002B00D06D427F19099B1E60039B002B39 -:101A800021D0089B06265A4253415B421E4030003B -:101A90000DB0F0BD002303931E00A8E70134BEE79C -:101AA000019C01230393C4E70700FAE70023069390 -:101AB000A61CCBE70123FAE70A235D432D18103D4E -:101AC000CDE72600D8E7019B0399E31A013B072BDA -:101AD00047DC059800F04BFA030014330393089396 -:101AE000002305001E000693019BA3423CD3089CE3 -:101AF000039B40C4E41AA4102C61300000F004FBE6 -:101B0000029B64011B68261A01939E425FDDF61A50 -:101B10003100280000F0FFFD041E0FD001241F211A -:101B20002000731E1940884001005A11039892004A -:101B300012580A4202D0A34241DC0224310028009C -:101B4000FFF7C8FEBF19029B9B680493BB424EDAA5 -:101B50002900059800F043FA0023129AA326136087 -:101B600095E701315B10B2E7631E09930B9A1B786E -:101B7000934215D0069B202B05D1089B1E600026A2 -:101B8000043308930696099B1878FFF7D7FE0F23B6 -:101B90001840069B099C9840043306430693A3E72C -:101BA0000123049A9C1A099B1C19019B9C42E1D3B6 -:101BB00007992000FEF757F90028DBD194E7B11E02 -:101BC000280000F0A8FD0028B7D00324B6E7019B49 -:101BD00000249E42B7DA9E1B29003200059800F0CF -:101BE000C7FB030014330500BF1B0393ABE7029B45 -:101BF0005E68BE426BDD019BF61BB34232DC029B8A -:101C0000DB68022B26D0032B28D0012B19D1019B96 -:101C1000B34216D1012B0DDC029B62265B68019357 -:101C2000019A0A9B1A600123039A2B611360129B8D -:101C30001D602CE701992800013900F06CFD002897 -:101C4000EAD12900059800F0CAF90023129A50261B -:101C500013601CE7139B002BDED0F2E7139B002BD5 -:101C6000DAD1EEE7771E002C2FD1002F04D03900F7 -:101C7000280000F050FD04001F237A111F401E3B76 -:101C8000BB400399920052581A4201D002231C43D0 -:101C9000019B31009B1B022628000193FFF71AFECF -:101CA000029B5F68002C40D0029BDB68022B10D0A7 -:101CB000032B12D0012B06D1A20704D5039A126878 -:101CC00014431C420CD110232EE00124D4E701263A -:101CD000E8E70123139A9B1A1393139B002BF2D06E -:101CE0002C69A30004932B00A2001433991800223E -:101CF00094461A68501C1DD001321A602B00143310 -:101D0000022E3AD1029A01991268013A8A420AD106 -:101D10004A111F21019892000140012004008C40CB -:101D2000D358234248D120231E43129B1D600A9B97 -:101D30001F60ACE6624604C39942DAD8AB689C42A5 -:101D400012DB6B680598591C00F011F929002B690A -:101D500003909A1C92000C310C30FDF7B8FA290060 -:101D6000059800F03CF9039D2B695A1C2A61012259 -:101D700004339B00EB185A60C0E72A69A2420ADDCF -:101D800001212800FFF7A6FD029B01379B68012671 -:101D9000BB42C8DADCE61F24019A01262240141E49 -:101DA000C1D0049A9B18043B186800F0ADF92021BB -:101DB0000C1BA042E4DBB6E70600B4E7082370B5CD -:101DC00020259A1A9200AD1A436806681C00AC40A0 -:101DD000D34034430460436004308842F4D370BD80 -:101DE00030B503680C780133002C02D10360012068 -:101DF00030BD1A781500413D192D00D8203201312F -:101E0000A242EFD00020F3E7F0B58BB001920A6850 -:101E1000019953119B00CB1804931F231A4008907B -:101E2000069202D0049B04330493049B00251E1FDA -:101E300037003400089B35601B68079505930295B1 -:101E4000059B01330993059B5B780393002B28D0F6 -:101E50000398FFF773FD002845D1039B202B19D869 -:101E6000029B079A934212DDBC4206D2072D04DC86 -:101E70002A0039002000FFF7A1FF019B08259C42A2 -:101E800005D9271F00253C00029B3D600793099B55 -:101E90000593D5E7039B292B4AD1059B089A02336A -:101EA0001360029B002B43D0BC4206D2072D04DCFA -:101EB0002A0039002000FFF781FF019B9C4226D9B0 -:101EC00004CC1A600433A642FAD2002204C39E4214 -:101ED000FCD23368002B04D1019B9E4226D1012302 -:101EE0003360052025E0029B013501330293082D64 -:101EF00006DD019B9C42CAD900230125043C2360D6 -:101F00000F22236810401B0118432060BFE7069B87 -:101F1000002BDED02021069A049B891A012252420E -:101F2000CA40043B19680A401A60D2E7043ED0E771 -:101F300004200BB0F0BDF03070470000034B1B686D -:101F4000186A002800D10248F03070474C01002088 -:101F5000B0010020C9B28218904201D10020704720 -:101F600003788B42FBD00130F6E77047704770B5BD -:101F7000466A04000D00002E07D11020FDF772F90B -:101F80006062466086600660C660666AF368002B21 -:101F900013D0636AAA00DB689B181868002818D160 -:101FA00001210E00AE40721D9200200000F0D5FB12 -:101FB00000280CD0456086600DE02122042120001D -:101FC00000F0CBFB636AF060DB68002BE1D10020FE -:101FD00070BD02681A6000230361C360F8E770B542 -:101FE000446A06000D00002C07D11020FDF73AF9D5 -:101FF0007062446084600460C460002D07D0736A1E -:102000006A68DB6892009B181A682A601D6070BDC0 -:10201000F7B51E0014239C4607000C0000200D6934 -:102020008C4463461B68013099B263461B68514378 -:102030001B0C534389190E0C9B1989B21E0C1B04EF -:102040005B18614608C18C468542EADC002E1BD035 -:10205000A368AB4212DC63683800591CFFF787FFA6 -:102060002100236901909A1C92000C310C30FDF77D -:102070002EF921003800FFF7B2FF019C2B1D9B00B9 -:10208000E31801355E6025612000FEBDF7B506004E -:1020900018000C000830092101931500FAF722F905 -:1020A0000123002183421FDB3000FFF760FF089B04 -:1020B000436101230361092D19DD230009331F004A -:1020C000009364193B780100303B0A22300001374D -:1020D000FFF79EFFA742F5D1009B5C19083C2F003B -:1020E000019B9F4206DBFEBD5B000131DAE70A344B -:1020F0000925F4E7631BDB5D0100303B0A22300059 -:10210000FFF786FF0137EBE70300020C0020824255 -:1021100001D11B0410301A0E01D108301B021A0F16 -:1021200001D104301B019A0F01D102309B00002B1A -:1021300003DB01305B0000D42020704707220368D6 -:1021400010B501001A400DD001240020234205D112 -:102150000230034203D0E34020000B6010BD9B0817 -:102160000B60FBE79CB21000002C01D110201B0C6F -:10217000DAB2002A01D108301B0A1A0701D1043053 -:102180001B099A0701D102309B08DA07E8D45B08E3 -:1021900001D00130E4E72020E0E710B50C00012178 -:1021A000FFF7E5FE01234461036110BDF0B50B6943 -:1021B0001400126989B00D00934201DA25000C0069 -:1021C0002F6926696968BB190293AB68BA199342F3 -:1021D00000DA0131FFF7CBFE0200BB1914329B007D -:1021E000D3189C4600211300049063451DD3143579 -:1021F000BF00EB191434B6000593A319039501949D -:102200000793079B0199994211D3029B002B06DD8E -:1022100004235B429C4463461B68002B51D0049B03 -:10222000029A18001A6109B0F0BD02C3DDE7019BF4 -:102230001B6898B200281CD015000026039F02CF0F -:102240002C688BB24343090C4143A4B21B199B1960 -:102250000E0029680695090C71181E0C89190E0CC0 -:102260009BB209040B4308C5059BBB42E7D8069BFC -:102270005E60019B1B681D0C1ED0100000271368B8 -:10228000039E3168046889B26943240C0919C9198D -:102290000F049BB23B43036080CE43683F0C6F4307 -:1022A0009BB2FB18090C5B18059906901F0C0430B3 -:1022B000B142E6D806994B60019B04320433019386 -:1022C0009FE7029B013B02939FE700000323F7B5C2 -:1022D000134005000E001400002B08D02249013BDA -:1022E0009A00525800233100FFF792FE0600A31017 -:1022F000019336D06C6A002C07D11020FCF7B2FF96 -:102300006862446084600460C4606F6ABC68002CCA -:1023100007D116492800FFF740FF00230400B860EA -:1023200003600122019B13420AD0310022002800E1 -:10233000FFF73CFF070031002800FFF750FE3E008A -:10234000019B5B1001930CD02068002807D122006C -:1023500021002800FFF72AFF00232060036004000B -:10236000DFE73000FEBDC04690430100710200006F -:10237000F0B50D0017002B69541185B0E3180193D7 -:10238000013300934968AB680290009A934235DBB1 -:102390000298FFF7ECFD002302000600190014323A -:1023A000A3422EDBE343DB171C402B00A4001419CF -:1023B0002A69143392009A1894461F22174024D099 -:1023C0000132D21B0392002219682000B9400A434F -:1023D00004C404CB0399CA409C45F5D84260002A46 -:1023E00002D0019B02330093009B0298013B3361B2 -:1023F0002900FFF7F4FD300005B0F0BD01315B00AE -:10240000C3E7980011500133CAE704CB04C49C45CC -:10241000FBD8E9E70A69036930B59B1A0DD1920030 -:102420001430143184188918043C043925680A686A -:10243000954204D00123954204D3180030BDA04238 -:10244000F2D3FAE75B42F8E7F0B50D0085B007007C -:10245000110028001400FFF7DDFF061E08D101005F -:102460003800FFF784FD01234661036105B0F0BD2C -:1024700001230193002804DB23002C001D0000230E -:10248000019361683800FFF772FD2F000022019B65 -:102490002669C3601434B300E31802932B69143720 -:1024A0009B00FB18039303009446143320CC04CF05 -:1024B0000192AAB294446A46614692882D0C8A1A07 -:1024C0000199090C691A151449190D1492B20904DD -:1024D0000A431A60039AAC460433BA42E6D802991A -:1024E0001A008C4205D3043A1368002B0ED0066103 -:1024F000BCE704CC91B261440D14120C52191514AE -:1025000089B212040A43AC4604C3E8E7013EEAE795 -:102510000B00104910B519400F4BC918002904DDF4 -:1025200000220B001000190010BD494209150022BD -:102530000023132904DC8020000308410300F1E795 -:10254000143901201E2902DC1F24611A8840020070 -:10255000E8E7C0460000F07F0000C0FCF7B50600C9 -:10256000036914369B00F3181D1F2C680F00200010 -:102570000193FFF7C9FD20231B1A3B601C490A2861 -:1025800016DC0B2327001B1ADF4039439C460B0047 -:102590000021AE4202D20199083909681530844001 -:1025A0006046C1400C4322001000190003B0F0BD8A -:1025B0000027AE4202D2019D083D2F680B388446A9 -:1025C000031E12D09C40202021433C00C01AC4406E -:1025D00021430B000021B54201D9043D296864461E -:1025E000C140A7400F433A00DEE721430B00FAE762 -:1025F0000000F03FF7B51D0001211400089FFFF710 -:10260000B6FC2B036D0006001B0B6D0D24D101934E -:10261000002C27D068460094FFF790FD009C00280E -:102620001ED0019B20211A00091A8A40C340224370 -:1026300072610193019CB461631E9C410134346159 -:10264000002D18D0124BED1835232D183D60181AA7 -:10265000099B18603000FEBD802252031343D6E769 -:102660007461E7E701A8FFF769FD0124019B34616C -:1026700073612030E4E7074BC018074B3860E3185C -:102680009B00F3185869FFF73FFD6401201ADFE74C -:10269000CDFBFFFFCEFBFFFFFFFFFF3FF0B585B097 -:1026A0000F0002A90600FFF759FF04000D003800D3 -:1026B00003A900940195FFF751FF36693F690200B5 -:1026C000F71B7E01B446039F029E0B00F61B664477 -:1026D0003705002E07DD7919019100980199FBF764 -:1026E000F3F905B0F0BDCB1BF7E71300013910B5C6 -:1026F00049111269013189001433920041189A1866 -:102700009A4203D80023814203D810BD10CB10C0D9 -:10271000F6E708C0F7E70200006910B514324B1164 -:10272000984212DB0BDD1F20014008D0980084582E -:102730002000C8408840010001208C4204D19B0049 -:10274000D318934203D8002010BD0300F7E7043BE1 -:1027500019680029F5D00120F6E74A4370B5110049 -:102760001400FCF723FE051E03D022000021FCF715 -:10277000CAFD280070BDF8B507000D00160000293D -:1027800005D11100FCF712FE04002000F8BD002A5C -:1027900003D1FCF7C1FD3400F7E700F073F92C001A -:1027A000B042F2D231003800FCF700FE041EECD03B -:1027B00029003200FCF78BFD29003800FCF7ACFD46 -:1027C000E3E70000F0B58E6885B007000C000392C7 -:1027D00001939E423CD890238A89DB001A4234D070 -:1027E0000325096923685B1A0293636902985D43B4 -:1027F000EB0F5D19019B6D1001331B18AB4200D923 -:102800001D00530532D529003800FCF7CFFD061E08 -:1028100009D10C233B6040230120A289404213438D -:10282000A38105B0F0BD029A2169FCF750FDA38990 -:10283000144A1A4080231343A381029B2661F61891 -:1028400026606561019EED1AA560019B9E4200D93C -:10285000019E320003992068FCF742FDA368002026 -:102860009B1BA36023689E192660DAE72A003800C4 -:10287000FFF781FF061EE0D121693800FCF74CFD0F -:10288000C7E7C0467FFBFFFFF0B59FB002900593FE -:102890008B890F0016001B0611D50B69002B0ED17A -:1028A0004021FCF783FD38603861002805D10C23F6 -:1028B000029A013813601FB0F0BD40237B610023F2 -:1028C00006AC6361203363761033A37635002B7832 -:1028D000002B01D0252B46D1AB1B04930CD032002A -:1028E00039000298FFF76EFF431C00D1AEE0626929 -:1028F000049B9446634463612B78002B00D1A5E0D0 -:10290000012200235242626002A9543252186E1C06 -:102910002360E360A3601370A365317805225448F7 -:10292000FFF718FB751C00281FD12268D30604D5B9 -:10293000532302A95B1820211970130704D55323D0 -:1029400002A95B182B21197033782A2B16D0350079 -:1029500000210A20E3682A786E1C303A092A4ED9F1 -:10296000002918D010E00135B1E7414B2268C01AA8 -:1029700001238340134323602E00CEE7059B191DDE -:102980001B680591002B01DB099304E05B42E360C7 -:102990000223134323602B782E2B0AD16B782A2B2A -:1029A00035D1059B02351A1D1B680592002B2BDBC8 -:1029B0000793304E297803223000FFF7CBFA002826 -:1029C00006D04023801B83402268013513432360D7 -:1029D0002978062228486E1C2176FFF7BBFA0028CA -:1029E0003CD0264B002B25D10722059B0733934370 -:1029F000083305936369039A9B18636166E7434351 -:102A000001219B183500A6E701235B42D0E7002394 -:102A10000A201A000135636029786E1C30390929B3 -:102A200003D9002BC5D00792C3E7424301235218B4 -:102A30003500F1E705AB00933A00114B21000298F5 -:102A4000FCF7AEFD0390039B0133D3D1BB890B98F8 -:102A50005B0600D42FE7012040422CE705AB009332 -:102A60003A00074B21000298FDF754F8EAE7C04608 -:102A70009C430100A2430100A6430100A1F5000010 -:102A8000C52701000B1F1B68181F002B01DA0B580C -:102A9000C0187047044A1368002B00D1034B181864 -:102AA000106018007047C046703E0020783E00203D -:102AB000F8B5C046F8BC08BC9E467047F8B5C0469D -:102AC000F8BC08BC9E4670470000000000000000F3 -:102AD000E955000005560000258000003180000007 -:102AE00039800000CD810000658000001D8100005C -:102AF0009F82000035800000C582000053830000E3 -:102B0000ED820000000010002B03040000E1000033 -:102B1000C0120000C0010137782141A665734E4400 -:102B200075672AE6308308001E00000000806144BB -:102B300000006844320190010000FA4309061208BF -:102B4000FD003200000088130000000202030405AB -:102B500006070708090A0B0C0D1414141453414DF1 -:102B6000443231203157203931354D487A005341B4 -:102B70004D4432312031570053746174653A205B03 -:102B80004E6F204C696E6B5D205265636569766996 -:102B90006E67205374616E6462790053746174656A -:102BA0003A205B4E6F204C696E6B5D205265636509 -:102BB00069766564205061636B6574005374617459 -:102BC000653A205B4E6F204C696E6B5D20547261DC -:102BD0006E736D697474696E670053746174653ADD -:102BE000205B4E6F204C696E6B5D2041636B2057FC -:102BF0006169740053746174653A20526563656954 -:102C000076696E67205374616E6462792000537434 -:102C10006174653A20526563656976656420506128 -:102C2000636B6574200053746174653A205472615B -:102C30006E736D697474696E672000537461746596 -:102C40003A2041636B2057616974200053746174AA -:102C5000653A20422D526563656976696E67205337 -:102C600074616E646279200053746174653A204225 -:102C70002D5265636569766564205061636B657488 -:102C8000200053746174653A20422D5472616E7352 -:102C90006D697474696E67200053746174653A20BD -:102CA0005B547261696E696E675D2054580053749D -:102CB0006174653A205B547261696E696E675D206C -:102CC00041636B20576169740053746174653A20E5 -:102CD0005B547261696E696E675D20525820486569 -:102CE00072652046697273740053746174653A208A -:102CF0005B547261696E696E675D20525820506145 -:102D0000636B6574004368616E676520537461741A -:102D10006520556E6B6E6F776E3A20004E6577209A -:102D20004B65793A20004E65772049443A20004CA3 -:102D3000494E4B20545241494E4544004552524F52 -:102D40005200252E2A660025303258002564005393 -:102D50007061726B46756E204C6F5261536572697B -:102D6000616C2000207600415426543D5253534953 -:102D7000005761726E696E673A20416972537065DF -:102D80006564206F76657272696465206F66206283 -:102D9000616E6477696474682C2073707265616415 -:102DA00020666163746F722C20616E6420636F64AF -:102DB000696E672072617465004169725370656560 -:102DC00064206973206F766572726964696E67004A -:102DD000547261696E20426C696E6B696E67204C3B -:102DE000454473000D0A4F4B000D0A00556E6B6E83 -:102DF0006F776E2061697253706565643A20004692 -:102E00007265713A2000726164696F42616E647725 -:102E1000696474683A2000726164696F5370726506 -:102E20006164466163746F723A2000726164696F15 -:102E3000436F64696E67526174653A200072616421 -:102E4000696F53796E63576F72643A2000726164E0 -:102E5000696F507265616D626C654C656E67746810 -:102E60003A200063616C6353796D626F6C54696DD5 -:102E7000653A2000486F7070696E67506572696FBF -:102E8000643A2000636F6E74726F6C5061636B659F -:102E90007441697254696D653A2000526164696FCA -:102EA00020696E6974206661696C65642E204368D0 -:102EB00065636B2073657474696E67732E00636855 -:102EC000616E6E656C53706163696E673A20004392 -:102ED00068616E6E656C207461626C653A00414594 -:102EE000532049563A0020307800523A0009533AAC -:102EF000000966453A00556E6B6E6F776E207374ED -:102F000061746500410054530053657269616C53EC -:102F100070656564004169725370656564006E6533 -:102F200074494400506F696E74546F506F696E74C9 -:102F300000456E63727970744461746100456E631C -:102F400072797074696F6E4B657900446174615376 -:102F50006372616D626C696E67005478506F77655B -:102F600072004672657175656E63794D696E0046D3 -:102F700072657175656E63794D6178004E756D622D -:102F800065724F664368616E6E656C73004672656C -:102F90007175656E6379486F70004D61784477652F -:102FA0006C6C54696D650042616E64776964746825 -:102FB00000537072656164466163746F7200436FA1 -:102FC00064696E67526174650053796E63576F72FE -:102FD0006400507265616D626C654C656E67746803 -:102FE000004672616D6553697A65004672616D6570 -:102FF00054696D656F75740044656275670045635B -:10300000686F0048656172744265617454696D65EA -:103010006F757400466C6F77436F6E74726F6C00DF -:103020004175746F54756E6500446973706C617995 -:103030005061636B65745175616C697479004D61A1 -:1030400078526573656E6473003D00747261696ED9 -:103050004E657449443A2000747261696E456E632E -:1030600072797074696F6E4B65793A005261646968 -:103070006F20696E6974206661696C656420776988 -:10308000746820636F64653A2000526164696F530D -:103090006565643A200000000000000000000000A8 -:1030A0000000000000000000000000000000000020 -:1030B0000000000000000000000000000000000010 -:1030C0000000000000000000000000000000000000 -:1030D00000000000000000000000000000000000F0 -:1030E00000000000000000000000000000000000E0 -:1030F00000000000000000000000000000000000D0 -:10310000480010002B03040000E10000C012000082 -:10311000C0010137782141A665734E4475672AE6E0 -:10312000308308001E0000000080614400006844F5 -:10313000320190010000FA4309061208FD00320036 -:103140000000881300000002FFFFFFFFFFFFFFFFEA -:10315000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10316000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10317000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10318000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10319000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1031A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1031B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1031C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1031D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1031E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1031F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10320000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10321000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10322000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10323000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10324000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10325000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10326000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10327000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10328000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10329000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1032A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1032B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1032C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1032D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1032E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1032F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10330000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10331000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10332000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10333000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10334000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10335000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10336000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10337000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10338000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10339000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1033A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1033B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1033C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1033D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1033E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1033F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10340000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10341000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10342000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10343000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10344000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10345000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10346000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10347000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10348000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10349000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1034A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1034B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1034C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1034D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1034E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1034F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1035000001000000EDDEEDFEFFFFFFFFFFFFFFFF0C -:10351000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10352000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10353000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10354000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10355000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10356000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10357000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10358000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10359000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1035A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1035B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1035C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1035D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1035E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1035F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:103600000800000010000000200000004000000042 -:103610008000000000010000000200000004000023 -:103620004C52530000000000000000001571000023 -:10363000E96B0000597500004D760000C577000069 -:10364000716D0000D76D0000E56E00006F61000035 -:10365000FB6A0000A16A0000C16F0000256800003D -:10366000956700008F670000795B0000B15B000088 -:10367000000000000000000015710000E96B000070 -:10368000597500004D760000C5770000716D00008F -:10369000D76D0000E56E00006F610000FB6A00005E -:1036A000A16A0000C16F0000256800009567000056 -:1036B0008F670000795B0000B15B00000000000034 -:1036C0000000000015710000E96B00005975000052 -:1036D0004D760000C5770000716D0000D76D0000C9 -:1036E000E56E00007FC00000FB6A0000A16A0000D8 -:1036F000C16F000025680000956700008F6700001B -:103700007FC000007FC0000000000000000000003B -:103710007FC000007FC000007FC000007FC00000AD -:103720007FC000007FC000007FC000007FC000009D -:103730007FC000007FC000007FC000007FC000008D -:103740007FC000007FC000007FC0000000000000BC -:1037500000000000B97A0000D97A00008D7B0000DB -:10376000B57A0000EB7A0000137D0000E17E0000D6 -:10377000917B000052096AD53036A538BF40A39E20 -:1037800081F3D7FB7CE339829B2FFF87348E434440 -:10379000C4DEE9CB547B9432A6C2233DEE4C950B9C -:1037A00042FAC34E082EA16628D924B2765BA249FC -:1037B0006D8BD12572F8F66486689816D4A45CCC1B -:1037C0005D65B6926C704850FDEDB9DA5E154657EE -:1037D000A78D9D8490D8AB008CBCD30AF7E4580524 -:1037E000B8B34506D02C1E8FCA3F0F02C1AFBD0330 -:1037F00001138A6B3A9111414F67DCEA97F2CFCE01 -:10380000F0B4E67396AC7422E7AD3585E2F937E89B -:103810001C75DF6E47F11A711D29C5896FB7620EDD -:10382000AA18BE1BFC563E4BC6D279209ADBC0FEBE -:1038300078CD5AF41FDDA8338807C731B11210596B -:103840002780EC5F60517FA919B54A0D2DE57A9F5D -:1038500093C99CEFA0E03B4DAE2AF5B0C8EBBB3C52 -:1038600083539961172B047EBA77D626E1691463D6 -:1038700055210C7D001B362D6C775A41637C777B7C -:10388000F26B6FC53001672BFED7AB76CA82C97D5C -:10389000FA5947F0ADD4A2AF9CA472C0B7FD9326ED -:1038A000363FF7CC34A5E5F171D8311504C723C3F1 -:1038B0001896059A071280E2EB27B27509832C1A35 -:1038C0001B6E5AA0523BD6B329E32F8453D100ED8F -:1038D00020FCB15B6ACBBE394A4C58CFD0EFAAFB73 -:1038E000434D338545F9027F503C9FA851A3408F3B -:1038F000929D38F5BCB6DA2110FFF3D2CD0C13EC53 -:103900005F974417C4A77E3D645D197360814FDCE7 -:10391000222A908846EEB814DE5E0BDBE0323A0ACB -:103920004906245CC2D3AC629195E479E7C8376D4F -:103930008DD54EA96C56F4EA657AAE08BA78252E74 -:103940001CA6B4C6E8DD741F4BBD8B8A703EB566FD -:103950004803F60E613557B986C11D9EE1F89811EE -:1039600069D98E949B1E87E9CE5528DF8CA1890DDD -:10397000BFE6426841992D0FB054BB16000000000D -:103980000000000000000000000000008D7B00002F -:103990007FC000007FC00000137D0000E17E0000BA -:1039A000917B00000001020408102040801B3600BB -:1039B0000000000000000000000000000000000007 -:1039C0007FC000007FC000007FC000007FC00000FB -:1039D0007FC000007FC000007FC000007FC00000EB -:1039E0007FC000007FC000007FC00000000000001A -:1039F0000000000000000000000000007FC0000088 -:103A00007FC000007FC000007FC000007FC00000BA -:103A10007FC0000000000000000000000000000067 -:103A2000000000007FC000007FC000007FC00000D9 -:103A30007FC000007FC000007FC000007FC000008A -:103A40000000000000000000198300004183000016 -:103A5000258000003180000039800000CD81000009 -:103A6000658000001D8100009F82000035800000FD -:103A7000C582000053830000ED82000000000000BA -:103A80000B0000000200000004000000FF00FFFF28 -:103A9000FFFF0B00000000000A0000000200000011 -:103AA00004000000FF00FFFFFFFF0A00000000000D -:103AB0000E0000000800000004000000FF00FFFFEF -:103AC000FFFF0E00000000000900000004000000DD -:103AD0001C000000FF0001000100090000000000C0 -:103AE00008000000040000001C000000FF000000AF -:103AF00000001000000000000F00000004000000A3 -:103B00001C000000FF00010301030F000000000083 -:103B100014000000050000002C000000FF0002005F -:103B20000200040000000000150000000800000072 -:103B300004000000FF00FFFFFFFF05000000000081 -:103B400006000000040000001C000000FF0000014F -:103B50000001060000000000070000000400000053 -:103B60001C000000FF00010101010700000000002F -:103B700012000000020000001C000000FF00000313 -:103B8000000302000000000010000000020000001E -:103B90001C000000FF000002000200000000000006 -:103BA00013000000020000001C000000FF000300E2 -:103BB00003000300000000001100000002000000EC -:103BC0000C000000FF000102FFFF010000000000E8 -:103BD0000200000001000000020000000000FFFFE2 -:103BE000FFFF0200010000000800000001000000CB -:103BF000020000000200FFFFFFFF080001000000BC -:103C00000900000001000000020000000300FFFFA7 -:103C1000FFFF090000000000040000000100000098 -:103C2000020000000400FFFFFFFF0400000000008E -:103C30000500000001000000020000000500FFFF79 -:103C4000FFFF05000100000002000000010000006D -:103C5000020000000A00FFFFFFFF0200000000005A -:103C6000160000000200000004000000FF00FFFF3B -:103C7000FFFF060000000000170000000200000027 -:103C800004000000FF00FFFFFFFF0700000000002E -:103C90000C0000000300000004000000FF00FFFF14 -:103CA000FFFF0C00010000000A00000003000000FC -:103CB00004000000FF00FFFFFFFF0A0001000000FA -:103CC0000B0000000300000004000000FF00FFFFE5 -:103CD000FFFF0B0001000000030000000B000000CC -:103CE00004000000FF00FFFFFFFFFF0000000000D6 -:103CF0001B0000000B00000004000000FF00FFFF9D -:103D0000FFFFFF00000000001C0000000600000094 -:103D100000000000FF00FFFFFFFFFF0000000000A9 -:103D2000180000000600000000000000FF00FFFF78 -:103D3000FFFFFF0000000000190000000600000067 -:103D400000000000FF00FFFFFFFFFF000100000078 -:103D5000160000000300000000000000FF00FFFF4D -:103D6000FFFFFF000100000017000000030000003B -:103D700000000000FF00FFFFFFFFFF000000000049 -:103D8000160000000200000000000000FF00FFFF1E -:103D9000FFFFFF000000000017000000020000000D -:103DA00000000000FF00FFFFFFFFFF000000000019 -:103DB000130000000200000000000000FF00FFFFF1 -:103DC000FFFFFF00000000001000000002000000E4 -:103DD00000000000FF00FFFFFFFFFF0000000000E9 -:103DE000120000000200000000000000FF00FFFFC2 -:103DF000FFFFFF00000000001100000002000000B3 -:103E000000000000FF00FFFFFFFFFF0000000000B8 -:103E10000D000000040000000C000000FF00010085 -:103E2000FFFF0D000000000015000000050000006D -:103E30000C000000FF000300FFFFFF000000000077 -:103E400006000000040000000C000000FF0000015C -:103E5000FFFFFF000000000007000000040000005A -:103E60000C000000FF000101FFFFFF000000000048 -:103E7000030000000100000002000000FF00FFFF3F -:103E8000FFFFFF0000000000020000000100000032 -:103E9000020000001400FFFFFFFF0200706F7700B8 -:103EA000000000000000000000000000000030439F -:103EB00000000000000030C3000100020003000405 -:103EC00000000005000000060000000700000000E0 -:103ED0000000000049AB000075AB000045AB0000DE -:103EE00069AB00005DAB0000ADAB000091AB000022 -:103EF00000000000000000000BB00000EDAE00006C -:103F000029AE00004C6F526153657269616C00000C -:103F100004030904537061726B46756E0012010050 -:103F200002EF0201404F1B2A0000010102030100C1 -:103F3000000000000000000049BF000019A50000BB -:103F400087BE000043BE00005DBE0000FDBF000054 -:103F500025BC000015BC0000A9BC00003FBC00004F -:103F600011BC00001ECB00005ACA00007ECA00002F -:103F70001CCA00007ECA0000FACA00007ECA000007 -:103F80001CCA00005ACA00005ACA0000FACA00003F -:103F90001CCA000050CB000050CB000050CB0000EA -:103FA00006CB00005ACA00005ACA00007ECA0000B0 -:103FB0001ACA00007ECA0000FACA00007ECA0000C9 -:103FC0001ACA00005ACA00005ACA0000FACA000001 -:103FD0001ACA000050CB000050CB000050CB0000AC -:103FE00004CB000048CE0000F2CD0000F2CD00006E -:103FF00020CF0000EECD0000EECD000016CF000077 -:1040000020CF0000EECD000016CF0000EECD000066 -:1040100020CF000024CF000024CF000024CF0000D8 -:104020002CCF00006CDD0000DEDB000022DC000095 -:1040300086DB000022DC000012DD000022DC000034 -:1040400086DB0000DEDB0000DEDB000012DD0000AE -:1040500086DB00007EDB00007EDB00007EDB0000F4 -:1040600020DD000094E4000096E3000096E30000E9 -:10407000B4E6000090E3000090E300009CE600003E -:10408000B4E6000090E300009CE6000090E300002E -:10409000B4E60000AAE60000AAE60000AAE60000D6 -:1040A000B8E600004300504F53495800494E4600BF -:1040B000696E66004E414E006E616E003000303118 -:1040C000323334353637383941424344454600307F -:1040D00031323334353637383961626364656600AE -:1040E00035000000CEFBFFFFCB0300000100000005 -:1040F0000000000034000000CEFBFFFFCB030000F7 -:104100000100000000000000BC89D897B2D29C3C9E -:1041100033A7A8D523F649393DA7F444FD0FA532AE -:104120009D978CCF08BA5B25436FAC642806681155 -:104130000020202020202020202028282828282077 -:10414000202020202020202020202020202020206F -:1041500020881010101010101010101010101010D7 -:1041600010040404040404040404041010101010C7 -:104170001010414141414141010101010101010191 -:1041800001010101010101010101010110101010E3 -:104190001010424242424242020202020202020263 -:1041A00002020202020202020202020210101010B7 -:1041B00020000000000000000000000000000000DF -:1041C00000000000000000000000000000000000EF -:1041D00000000000000000000000000000000000DF -:1041E00000000000000000000000000000000000CF -:1041F00000000000000000000000000000000000BF -:1042000000000000000000000000000000000000AE -:10421000000000000000000000000000000000009E -:10422000000000000000000000000000000000008E -:1042300000496E66696E697479004E614E00000037 -:10424000000000000000000000000000000000006E -:10425000000000000000000000000000000000005E -:10426000000000000000000000000000000000004E -:10427000000000000000000000000000000000003E -:10428000000000000000000000000000000000002E -:10429000000000000000000000000000000000001E -:1042A0000080E03779C34143176E05B5B5B8934632 -:1042B000F5F93FE9034F384D321D30F94877825AFE -:1042C0003CBF737FDD4F1575000000000000F03F1C -:1042D00000000000000024400000000000005940E1 -:1042E0000000000000408F40000000000088C34034 -:1042F00000000000006AF8400000000080842E41A9 -:1043000000000000D01263410000000084D79741F4 -:104310000000000065CDCD41000000205FA00242FA -:10432000000000E876483742000000A2941A6D426F -:10433000000040E59C30A2420000901EC4BCD64262 -:1043400000003426F56B0C430080E03779C341430D -:1043500000A0D8855734764300C84E676DC1AB4383 -:10436000003D9160E458E143408CB5781DAF1544A1 -:1043700050EFE2D6E41A4B4492D54D06CFF080447C -:10438000F64AE1C7022DB544B49DD9794378EA4491 -:1043900005000000190000007D000000232D302BD7 -:1043A0002000686C4C0065666745464700000000C9 -:1043B0000000000000000000000010002B030400BB -:1043C00000E10000C0120000C0010137782141A6C1 -:1043D00065734E4475672AE6308308001E000000AE -:1043E0000080614400006844320190010000FA43FB -:1043F00009061208FD0032000000881300000002C8 -:10440000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10441000FF000000000010002B03040000E100007A -:10442000C0120000C0010137782141A665734E44D7 -:1044300075672AE6308308001E0000000080614492 -:1044400000006844320190010000FA430906120896 -:10445000FD0032000000881300000002010000008F -:10446000FFFFFFFF010000000100000040420F00BD -:104470000A0000000A000000FFFFFFFFFFFFFFFF30 -:1044800000366E01FFFFFFFF00C2010000000800C0 -:10449000080B0002020200000904000001020200F1 -:1044A000000524001001042402060524060001056D -:1044B0002401010107058103100010090401000215 -:1044C0000A000000070502024000000705830240C1 -:1044D00000000000FFFFFFFF00000000830000005D -:1044E0000200000082000000000000000000000048 -:1044F00000000000000000000000000000000000BC -:1045000050010020000000006042010080420100D4 -:104510004042010000000000000000000000000018 -:10452000000000000000000000000000000000008B -:10453000000000000000000000000000000000007B -:10454000000000000000000000000000000000006B -:10455000000000000000000000000000000000005B -:104560000000000043000000000000000000000008 -:10457000000000000000000000000000000000003B -:1045800000000000430000000000000000000000E8 -:10459000000000000000000000000000000000001B -:1045A00000000000430000000000000000000000C8 -:1045B00000000000000000000000000000000000FB -:1045C00000000000430000000000000000000000A8 -:1045D00000000000000000000000000000000000DB -:1045E0000000000043000000000000000000000088 -:1045F00000000000000000000000000000000000BB -:104600000000000043000000000000000000000067 -:10461000000000000000000000000000000000009A -:104620000000000043000000000000000000000047 -:10463000000000000000000000000000000000007A -:1046400000000000F10B01008DF2000000000000EE -:1046500030410100BC2E0100EB2D0100EB2D0100CB -:10466000EB2D0100EB2D0100EB2D0100EB2D0100E6 -:10467000EB2D0100EB2D0100EB2D0100FFFFFFFFF3 -:10468000FFFFFFFFFFFFFFFFFFFF0000010041539F -:104690004349490000000000000000000000000045 -:1046A0000000000000000000000000000000415376 -:1046B0004349490000000000000000000000000025 -:1046C00000000000000000000000000000000000EA -:1046D000DD2000009D3800007D7A0000F985000093 -:1046E0003D8800006DAC000001BC0000B52000005A -:1046F00000000000000000000000000000000000BA -:10470000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10471000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10472000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10473000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10474000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10475000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10476000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10477000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10478000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10479000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1047A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1047B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1047C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1047D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1047E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1047F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10480000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10481000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10482000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10483000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10484000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10485000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10486000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10487000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10488000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10489000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1048A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1048B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1048C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1048D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1048E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1048F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10490000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10491000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10492000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10493000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10494000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10495000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10496000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10497000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10498000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10499000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1049A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1049B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1049C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1049D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1049E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1049F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:104A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:104A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:104A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:104A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:104A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:104A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:104A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:104A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:104A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:104A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:104AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:104AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:104AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:104AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:104AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:104AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:104B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:104B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:104B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:104B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:104B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:104B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:104B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:104B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:104B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:104B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:104BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:104BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:104BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:104BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:104BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:104BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:104C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:104C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:104C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:104C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:104C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:104C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:104C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:104C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:104C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:104C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:104CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:104CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:104CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:104CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:104CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:104CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:104D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:104D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:104D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:104D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:104D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:104D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:104D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:104D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:104D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:104D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:104DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:104DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:104DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:104DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:104DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:104DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:104E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:104E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:104E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:104E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:104E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:104E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:104E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:104E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:104E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:104E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:104EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:104EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:104EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:104ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:104EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:104EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:104F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:104F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:104F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:104F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:104F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:104F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:104F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:104F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:104F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:104F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:104FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:104FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:104FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:104FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:104FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:104FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10500000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10501000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10502000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10503000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10504000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10505000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10506000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10507000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10508000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10509000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1050A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1050B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1050C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1050D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1050E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1050F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10510000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10511000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10512000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10513000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10514000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10515000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10516000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10517000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10518000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10519000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1051A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1051B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1051C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1051D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1051E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1051F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10520000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10521000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10522000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10523000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10524000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10525000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10526000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10527000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10528000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10529000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1052A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1052B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1052C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1052D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1052E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1052F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10530000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10531000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10532000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10533000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10534000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10535000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10536000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10537000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10538000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10539000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1053A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1053B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1053C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1053D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1053E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1053F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10540000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10541000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10542000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10543000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10544000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10545000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10546000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10547000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10548000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10549000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1054A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1054B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1054C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1054D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1054E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1054F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10550000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10551000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10552000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10553000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10554000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10555000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10556000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10557000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10558000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10559000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1055A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1055B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1055C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1055D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1055E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1055F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10560000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10561000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10562000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10563000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10564000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10565000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10566000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10567000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10568000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10569000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1056A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1056B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1056C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1056D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1056E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1056F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10570000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10571000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10572000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10573000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10574000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10575000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10576000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10577000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10578000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10579000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1057A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1057B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1057C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1057D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1057E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1057F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10580000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10581000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10582000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10583000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10584000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10585000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10586000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10587000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10588000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10589000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1058A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1058B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1058C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1058D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1058E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1058F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10590000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10591000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10592000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10593000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10594000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10595000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10596000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10597000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10598000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10599000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1059A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1059B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1059C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1059D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1059E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1059F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:105A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:105A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:105A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:105A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:105A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:105A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:105A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:105A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:105A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:105A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:105AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:105AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:105AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:105AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:105AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:105AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:105B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:105B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:105B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:105B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:105B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:105B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:105B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:105B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:105B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:105B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:105BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:105BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:105BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:105BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:105BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:105BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:105C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:105C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:105C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:105C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:105C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:105C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:105C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:105C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:105C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:105C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:105CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:105CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:105CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:105CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:105CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:105CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:105D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:105D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:105D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:105D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:105D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:105D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:105D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:105D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:105D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:105D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:105DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:105DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:105DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:105DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:105DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:105DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:105E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:105E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:105E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:105E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:105E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:105E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:105E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:105E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:105E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:105E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:105EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:105EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:105EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:105ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:105EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:105EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:105F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:105F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:105F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:105F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:105F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:105F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:105F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:105F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:105F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:105F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:105FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:105FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:105FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:105FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:105FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:105FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10600000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10601000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10602000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10603000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10604000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10605000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10606000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10607000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10608000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10609000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1060A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1060B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1060C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1060D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1060E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1060F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10610000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10611000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10612000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10613000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10614000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10615000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10616000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10617000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10618000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10619000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1061A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1061B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1061C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1061D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1061E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1061F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10620000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10621000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10622000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10623000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10624000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10625000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10626000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10627000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10628000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10629000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1062A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1062B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1062C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1062D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1062E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1062F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10630000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10631000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10632000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10633000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10634000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10635000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10636000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10637000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10638000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10639000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1063A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1063B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1063C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1063D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1063E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1063F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10640000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10641000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10642000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10643000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10644000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10645000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10646000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10647000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10648000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10649000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1064A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1064B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1064C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1064D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1064E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1064F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10650000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10651000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10652000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10653000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10654000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10655000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10656000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10657000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10658000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10659000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1065A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1065B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1065C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1065D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1065E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1065F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10660000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10661000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10662000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10663000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10664000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10665000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10666000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10667000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10668000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10669000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1066A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1066B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1066C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1066D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1066E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1066F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10670000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10671000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10672000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10673000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10674000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10675000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10676000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10677000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10678000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10679000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1067A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1067B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1067C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1067D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1067E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1067F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10680000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10681000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10682000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10683000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10684000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10685000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10686000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10687000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10688000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10689000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1068A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1068B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1068C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1068D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1068E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1068F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10690000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10691000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10692000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10693000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10694000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10695000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10696000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10697000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10698000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10699000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1069A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1069B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1069C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1069D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1069E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1069F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:106A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:106A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:106A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:106A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:106A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:106A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:106A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:106A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:106A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:106A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:106AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:106AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:106AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:106AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:106AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:106AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:106B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:106B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:106B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:106B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:106B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:106B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:106B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:106B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:106B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:106B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:106BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:106BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:106BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:106BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:106BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:106BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:106C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:106C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:106C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:106C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:106C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:106C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:106C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:106C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:106C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:106C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:106CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:106CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:106CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:106CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:106CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:106CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:106D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:106D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:106D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:106D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:106D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:106D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:106D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:106D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:106D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:106D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:106DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:106DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:106DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:106DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:106DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:106DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:106E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:106E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:106E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:106E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:106E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:106E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:106E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:106E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:106E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:106E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:106EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:106EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:106EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:106ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:106EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:106EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:106F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:106F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:106F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:106F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:106F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:106F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:106F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:106F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:106F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:106F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:106FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:106FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:106FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:106FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:106FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:106FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10700000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10701000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10702000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10703000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10704000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10705000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10706000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10707000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10708000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10709000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1070A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1070B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1070C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1070D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1070E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1070F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10710000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10711000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10712000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10713000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10714000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10715000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10716000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10717000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10718000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10719000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1071A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1071B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1071C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1071D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1071E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1071F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10720000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10721000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10722000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10723000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10724000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10725000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10726000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10727000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10728000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10729000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1072A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1072B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1072C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1072D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1072E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1072F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10730000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10731000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10732000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10733000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10734000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10735000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10736000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10737000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10738000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10739000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1073A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1073B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1073C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1073D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1073E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1073F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10740000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10741000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10742000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10743000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10744000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10745000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10746000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10747000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10748000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10749000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1074A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1074B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1074C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1074D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1074E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1074F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10750000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10751000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10752000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10753000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10754000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10755000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10756000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10757000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10758000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10759000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1075A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1075B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1075C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1075D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1075E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1075F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10760000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10761000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10762000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10763000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10764000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10765000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10766000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10767000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10768000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10769000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1076A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1076B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1076C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1076D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1076E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1076F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10770000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10771000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10772000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10773000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10774000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10775000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10776000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10777000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10778000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10779000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1077A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1077B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1077C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1077D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1077E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1077F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10780000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10781000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10782000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10783000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10784000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10785000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10786000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10787000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10788000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10789000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1078A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1078B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1078C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1078D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1078E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1078F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10790000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10791000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10792000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10793000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10794000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10795000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10796000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10797000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10798000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10799000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1079A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1079B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1079C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1079D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1079E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1079F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:107A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:107A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:107A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:107A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:107A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:107A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:107A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:107A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:107A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:107A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:107AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:107AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:107AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:107AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:107AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:107AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:107B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:107B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:107B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:107B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:107B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:107B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:107B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:107B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:107B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:107B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:107BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:107BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:107BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:107BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:107BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:107BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:107C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:107C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:107C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:107C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:107C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:107C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:107C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:107C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:107C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:107C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:107CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:107CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:107CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:107CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:107CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:107CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:107D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:107D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:107D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:107D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:107D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:107D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:107D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:107D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:107D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:107D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:107DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:107DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:107DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:107DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:107DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:107DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:107E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:107E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:107E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:107E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:107E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:107E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:107E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:107E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:107E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:107E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:107EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:107EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:107EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:107ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:107EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:107EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:107F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:107F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:107F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:107F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:107F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:107F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:107F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:107F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:107F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:107F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:107FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:107FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:107FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:107FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:107FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:107FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10800000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10801000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10802000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10803000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10804000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10805000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10806000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10807000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10808000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10809000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1080A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1080B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1080C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1080D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1080E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:1080F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10810000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10811000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10812000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10813000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10814000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10815000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10816000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10817000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10818000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10819000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1081A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1081B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1081C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1081D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1081E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:1081F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10820000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10821000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10822000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10823000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10824000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10825000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10826000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10827000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10828000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10829000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1082A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1082B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1082C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1082D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1082E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:1082F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10830000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10831000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10832000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10833000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10834000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10835000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10836000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10837000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10838000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10839000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1083A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1083B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1083C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1083D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1083E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:1083F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10840000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10841000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10842000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10843000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10844000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10845000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10846000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10847000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10848000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10849000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1084A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1084B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1084C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1084D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1084E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:1084F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10850000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10851000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10852000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10853000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10854000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10855000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10856000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10857000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10858000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10859000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1085A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1085B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1085C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1085D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1085E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:1085F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10860000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10861000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10862000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10863000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10864000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10865000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10866000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10867000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10868000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10869000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1086A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1086B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1086C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1086D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1086E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:1086F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10870000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10871000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10872000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10873000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10874000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10875000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10876000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10877000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10878000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10879000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1087A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1087B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1087C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1087D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1087E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:1087F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10880000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10881000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10882000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10883000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10884000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10885000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10886000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10887000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10888000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10889000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1088A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1088B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1088C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1088D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1088E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:1088F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10890000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10891000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10892000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10893000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10894000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10895000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10896000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10897000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10898000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10899000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1089A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1089B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1089C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1089D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1089E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:1089F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:108A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:108A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:108A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:108A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:108A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:108A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:108A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:108A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:108A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:108A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:108AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:108AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:108AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:108AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:108AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:108AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:108B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:108B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:108B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:108B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:108B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:108B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:108B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:108B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:108B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:108B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:108BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:108BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:108BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:108BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:108BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:108BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:108C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:108C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:108C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:108C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:108C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:108C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:108C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:108C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:108C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:108C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:108CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:108CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:108CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:108CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:108CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:108CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:108D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:108D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:108D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:108D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:108D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:108D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:108D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:108D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:108D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:108D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:108DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:108DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:108DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:108DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:108DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:108DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:108E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:108E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:108E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:108E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:108E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:108E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:108E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:108E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:108E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:108E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:108EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:108EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:108EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:108ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:108EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:108EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:108F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:108F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:108F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:108F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:108F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:108F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:108F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:108F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:108F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:108F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:108FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:108FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:108FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:108FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:108FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:108FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10900000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10901000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10902000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10903000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10904000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10905000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10906000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10907000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10908000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10909000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1090A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1090B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1090C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1090D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:1090E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:1090F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10910000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10911000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10912000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10913000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10914000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10915000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10916000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10917000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10918000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10919000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1091A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1091B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1091C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1091D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:1091E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:1091F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10920000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10921000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10922000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10923000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10924000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10925000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10926000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10927000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10928000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10929000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1092A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1092B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1092C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1092D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:1092E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:1092F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10930000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10931000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10932000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10933000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10934000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10935000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10936000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10937000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10938000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10939000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1093A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1093B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1093C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1093D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:1093E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:1093F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10940000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10941000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10942000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10943000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10944000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10945000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10946000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10947000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10948000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10949000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1094A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1094B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1094C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1094D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:1094E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:1094F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10950000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10951000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10952000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10953000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10954000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10955000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10956000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10957000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10958000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10959000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1095A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1095B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1095C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1095D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:1095E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:1095F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10960000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10961000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10962000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10963000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10964000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10965000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10966000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10967000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10968000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10969000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1096A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1096B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1096C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1096D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:1096E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:1096F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10970000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10971000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10972000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10973000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10974000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10975000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10976000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10977000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10978000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10979000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1097A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1097B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1097C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1097D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:1097E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:1097F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10980000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10981000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10982000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10983000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10984000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10985000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10986000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10987000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10988000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10989000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1098A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1098B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1098C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1098D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:1098E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:1098F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10990000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10991000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10992000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10993000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10994000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10995000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10996000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10997000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10998000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10999000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1099A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1099B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1099C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1099D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:1099E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:1099F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:109A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:109A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:109A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:109A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:109A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:109A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:109A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:109A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:109A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:109A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:109AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:109AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:109AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:109AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:109AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:109AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:109B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:109B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:109B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:109B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:109B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:109B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:109B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:109B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:109B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:109B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:109BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:109BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:109BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:109BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:109BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:109BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:109C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:109C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:109C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:109C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:109C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:109C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:109C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:109C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:109C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:109C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:109CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:109CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:109CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:109CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:109CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:109CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:109D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:109D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:109D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:109D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:109D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:109D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:109D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:109D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:109D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:109D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:109DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:109DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:109DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:109DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:109DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:109DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:109E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:109E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:109E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:109E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:109E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:109E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:109E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:109E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:109E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:109E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:109EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:109EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:109EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:109ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:109EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:109EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:109F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:109F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:109F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:109F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:109F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:109F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:109F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:109F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:109F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:109F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:109FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:109FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:109FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:109FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:109FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:109FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10A00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10A01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10A02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10A03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10A04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10A05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10A06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10A07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10A08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10A09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10A0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10A0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10A0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10A0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10A0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10A0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10A10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10A11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10A12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10A13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10A14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10A15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10A16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10A17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10A18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10A19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10A1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10A1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10A1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10A1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10A1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10A1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10A20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10A21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10A22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10A23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10A24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10A25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10A26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10A27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10A28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10A29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10A2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10A2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10A2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10A2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10A2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10A2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10A30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10A31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10A32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10A33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10A34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10A35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10A36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10A37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10A38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10A39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10A3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10A3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10A3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10A3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10A3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10A3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10A40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10A41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10A42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10A43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10A44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10A45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10A46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10A47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10A48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10A49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10A4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10A4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10A4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10A4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10A4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10A4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10A50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10A51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10A52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10A53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10A54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10A55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10A56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10A57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10A58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10A59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10A5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10A5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10A5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10A5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10A5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10A5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10A60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10A61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10A62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10A63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10A64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10A65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10A66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10A67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10A68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10A69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10A6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10A6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10A6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10A6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10A6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10A6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10A70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10A71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10A72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10A73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10A74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10A75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10A76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10A77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10A78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10A79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10A7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10A7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10A7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10A7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10A7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10A7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10A80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10A81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10A82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10A83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10A84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10A85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10A86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10A87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10A88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10A89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10A8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10A8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10A8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10A8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10A8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10A8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10A90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10A91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10A92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10A93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10A94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10A95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10A96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10A97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10A98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10A99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10A9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10A9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10A9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10A9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10A9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10A9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10AA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10AA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10AA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10AA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10AA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10AA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10AA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10AA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10AA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10AA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10AAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10AAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10AAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10AAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10AAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10AAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10AB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10AB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10AB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10AB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10AB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10AB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10AB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10AB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10AB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10AB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10ABA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10ABB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10ABC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10ABD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10ABE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10ABF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10AC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10AC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10AC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10AC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10AC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10AC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10AC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10AC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10AC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10AC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10ACA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10ACB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10ACC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10ACD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10ACE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10ACF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10AD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10AD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10AD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10AD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10AD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10AD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10AD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10AD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10AD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10AD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10ADA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10ADB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10ADC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10ADD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10ADE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10ADF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10AE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10AE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10AE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10AE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10AE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10AE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10AE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10AE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10AE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10AE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10AEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10AEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10AEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10AED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10AEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10AEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10AF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10AF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10AF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10AF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10AF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10AF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10AF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10AF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10AF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10AF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10AFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10AFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10AFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10AFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10AFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10AFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10B00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10B01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10B02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10B03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10B04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10B05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10B06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10B07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10B08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10B09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10B0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10B0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10B0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10B0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10B0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10B0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10B10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10B11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10B12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10B13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10B14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10B15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10B16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10B17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10B18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10B19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10B1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10B1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10B1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10B1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10B1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10B1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10B20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10B21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10B22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10B23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10B24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10B25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10B26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10B27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10B28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10B29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10B2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10B2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10B2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10B2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10B2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10B2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10B30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10B31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10B32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10B33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10B34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10B35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10B36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10B37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10B38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10B39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10B3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10B3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10B3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10B3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10B3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10B3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10B40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10B41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10B42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10B43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10B44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10B45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10B46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10B47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10B48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10B49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10B4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10B4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10B4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10B4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10B4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10B4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10B50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10B51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10B52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10B53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10B54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10B55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10B56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10B57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10B58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10B59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10B5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10B5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10B5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10B5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10B5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10B5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10B60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10B61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10B62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10B63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10B64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10B65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10B66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10B67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10B68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10B69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10B6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10B6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10B6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10B6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10B6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10B6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10B70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10B71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10B72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10B73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10B74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10B75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10B76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10B77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10B78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10B79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10B7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10B7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10B7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10B7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10B7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10B7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10B80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10B81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10B82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10B83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10B84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10B85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10B86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10B87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10B88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10B89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10B8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10B8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10B8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10B8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10B8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10B8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10B90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10B91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10B92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10B93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10B94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10B95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10B96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10B97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10B98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10B99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10B9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10B9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10B9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10B9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10B9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10B9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10BA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10BA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10BA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10BA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10BA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10BA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10BA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10BA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10BA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10BA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10BAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10BAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10BAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10BAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10BAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10BAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10BB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10BB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10BB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10BB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10BB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10BB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10BB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10BB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10BB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10BB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10BBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10BBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10BBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10BBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10BBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10BBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10BC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10BC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10BC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10BC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10BC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10BC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10BC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10BC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10BC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10BC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10BCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10BCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10BCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10BCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10BCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10BCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10BD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10BD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10BD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10BD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10BD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10BD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10BD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10BD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10BD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10BD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10BDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10BDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10BDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10BDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10BDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10BDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10BE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10BE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10BE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10BE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10BE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10BE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10BE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10BE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10BE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10BE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10BEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10BEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10BEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10BED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10BEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10BEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10BF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10BF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10BF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10BF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10BF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10BF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10BF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10BF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10BF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10BF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10BFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10BFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10BFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10BFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10BFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10BFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10C00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10C01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10C02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10C03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10C04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10C05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10C06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10C07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10C08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10C09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10C0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10C0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10C0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10C0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10C0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10C0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10C10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10C11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10C12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10C13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10C14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10C15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10C16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10C17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10C18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10C19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10C1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10C1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10C1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10C1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10C1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10C1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10C20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10C21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10C22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10C23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10C24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10C25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10C26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10C27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10C28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10C29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10C2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10C2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10C2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10C2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10C2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10C2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10C30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10C31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10C32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10C33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10C34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10C35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10C36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10C37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10C38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10C39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10C3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10C3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10C3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10C3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10C3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10C3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10C40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10C41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10C42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10C43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10C44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10C45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10C46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10C47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10C48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10C49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10C4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10C4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10C4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10C4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10C4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10C4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10C50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10C51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10C52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10C53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10C54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10C55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10C56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10C57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10C58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10C59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10C5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10C5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10C5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10C5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10C5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10C5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10C60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10C61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10C62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10C63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10C64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10C65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10C66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10C67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10C68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10C69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10C6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10C6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10C6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10C6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10C6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10C6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10C70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10C71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10C72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10C73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10C74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10C75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10C76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10C77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10C78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10C79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10C7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10C7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10C7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10C7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10C7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10C7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10C80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10C81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10C82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10C83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10C84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10C85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10C86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10C87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10C88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10C89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10C8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10C8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10C8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10C8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10C8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10C8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10C90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10C91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10C92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10C93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10C94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10C95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10C96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10C97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10C98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10C99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10C9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10C9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10C9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10C9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10C9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10C9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10CA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10CA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10CA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10CA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10CA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10CA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10CA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10CA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10CA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10CA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10CAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10CAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10CAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10CAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10CAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10CAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10CB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10CB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10CB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10CB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10CB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10CB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10CB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10CB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10CB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10CB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10CBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10CBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10CBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10CBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10CBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10CBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10CC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10CC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10CC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10CC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10CC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10CC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10CC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10CC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10CC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10CC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10CCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10CCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10CCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10CCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10CCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10CCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10CD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10CD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10CD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10CD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10CD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10CD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10CD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10CD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10CD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10CD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10CDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10CDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10CDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10CDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10CDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10CDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10CE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10CE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10CE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10CE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10CE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10CE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10CE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10CE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10CE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10CE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10CEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10CEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10CEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10CED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10CEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10CEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10CF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10CF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10CF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10CF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10CF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10CF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10CF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10CF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10CF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10CF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10CFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10CFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10CFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10CFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10CFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10CFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10D00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10D01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10D02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10D03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10D04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10D05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10D06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10D07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10D08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10D09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10D0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10D0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10D0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10D0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10D0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10D0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10D10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10D11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10D12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10D13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10D14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10D15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10D16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10D17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10D18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10D19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10D1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10D1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10D1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10D1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10D1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10D1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10D20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10D21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10D22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10D23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10D24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10D25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10D26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10D27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10D28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10D29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10D2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10D2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10D2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10D2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10D2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10D2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10D30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10D31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10D32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10D33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10D34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10D35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10D36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10D37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10D38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10D39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10D3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10D3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10D3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10D3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10D3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10D3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10D40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10D41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10D42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10D43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10D44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10D45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10D46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10D47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10D48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10D49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10D4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10D4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10D4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10D4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10D4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10D4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10D50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10D51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10D52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10D53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10D54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10D55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10D56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10D57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10D58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10D59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10D5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10D5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10D5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10D5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10D5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10D5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10D60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10D61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10D62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10D63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10D64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10D65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10D66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10D67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10D68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10D69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10D6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10D6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10D6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10D6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10D6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10D6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10D70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10D71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10D72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10D73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10D74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10D75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10D76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10D77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10D78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10D79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10D7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10D7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10D7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10D7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10D7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10D7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10D80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10D81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10D82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10D83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10D84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10D85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10D86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10D87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10D88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10D89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10D8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10D8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10D8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10D8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10D8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10D8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10D90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10D91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10D92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10D93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10D94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10D95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10D96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10D97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10D98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10D99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10D9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10D9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10D9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10D9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10D9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10D9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10DA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10DA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10DA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10DA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10DA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10DA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10DA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10DA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10DA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10DA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10DAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10DAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10DAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10DAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10DAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10DAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10DB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10DB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10DB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10DB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10DB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10DB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10DB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10DB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10DB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10DB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10DBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10DBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10DBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10DBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10DBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10DBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10DC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10DC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10DC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10DC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10DC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10DC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10DC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10DC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10DC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10DC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10DCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10DCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10DCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10DCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10DCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10DCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10DD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10DD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10DD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10DD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10DD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10DD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10DD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10DD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10DD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10DD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10DDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10DDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10DDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10DDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10DDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10DDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10DE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10DE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10DE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10DE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10DE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10DE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10DE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10DE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10DE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10DE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10DEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10DEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10DEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10DED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10DEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10DEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10DF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10DF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10DF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10DF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10DF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10DF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10DF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10DF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10DF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10DF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10DFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10DFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10DFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10DFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10DFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10DFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10E00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10E01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10E02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10E03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10E04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10E05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10E06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10E07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10E08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10E09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10E0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10E0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10E0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10E0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10E0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10E0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10E10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10E11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10E12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10E13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10E14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10E15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10E16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10E17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10E18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10E19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10E1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10E1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10E1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10E1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10E1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10E1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10E20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10E21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10E22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10E23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10E24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10E25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10E26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10E27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10E28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10E29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10E2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10E2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10E2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10E2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10E2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10E2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10E30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10E31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10E32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10E33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10E34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10E35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10E36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10E37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10E38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10E39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10E3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10E3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10E3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10E3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10E3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10E3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10E40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10E41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10E42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10E43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10E44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10E45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10E46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10E47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10E48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10E49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10E4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10E4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10E4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10E4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10E4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10E4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10E50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10E51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10E52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10E53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10E54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10E55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10E56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10E57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10E58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10E59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10E5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10E5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10E5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10E5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10E5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10E5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10E60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10E61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10E62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10E63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10E64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10E65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10E66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10E67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10E68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10E69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10E6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10E6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10E6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10E6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10E6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10E6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10E70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10E71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10E72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10E73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10E74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10E75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10E76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10E77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10E78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10E79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10E7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10E7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10E7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10E7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10E7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10E7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10E80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10E81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10E82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10E83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10E84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10E85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10E86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10E87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10E88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10E89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10E8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10E8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10E8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10E8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10E8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10E8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10E90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10E91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10E92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10E93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10E94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10E95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10E96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10E97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10E98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10E99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10E9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10E9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10E9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10E9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10E9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10E9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10EA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10EA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10EA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10EA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10EA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10EA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10EA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10EA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10EA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10EA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10EAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10EAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10EAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10EAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10EAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10EAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10EB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10EB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10EB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10EB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10EB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10EB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10EB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10EB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10EB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10EB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10EBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10EBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10EBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10EBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10EBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10EBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10EC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10EC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10EC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10EC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10EC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10EC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10EC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10EC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10EC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10EC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10ECA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10ECB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10ECC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10ECD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10ECE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10ECF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10ED0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10ED1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10ED2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10ED3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10ED4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10ED5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10ED6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10ED7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10ED8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10ED9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10EDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10EDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10EDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10EDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10EDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10EDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10EE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10EE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10EE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10EE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10EE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10EE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10EE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10EE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10EE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10EE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10EEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10EEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10EEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10EED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10EEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10EEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10EF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10EF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10EF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10EF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10EF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10EF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10EF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10EF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10EF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10EF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10EFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10EFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10EFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10EFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10EFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10EFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10F00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10F01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10F02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10F03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10F04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10F05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10F06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10F07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10F08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10F09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10F0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10F0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10F0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10F0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10F0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10F0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10F10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10F11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10F12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10F13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10F14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10F15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10F16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10F17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10F18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10F19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10F1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10F1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10F1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10F1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10F1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10F1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10F20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10F21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10F22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10F23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10F24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10F25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10F26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10F27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10F28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10F29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10F2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10F2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10F2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10F2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10F2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10F2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10F30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10F31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10F32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10F33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10F34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10F35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10F36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10F37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10F38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10F39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10F3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10F3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10F3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10F3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10F3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10F3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10F40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10F41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10F42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10F43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10F44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10F45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10F46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10F47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10F48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10F49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10F4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10F4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10F4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10F4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10F4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10F4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10F50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10F51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10F52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10F53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10F54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10F55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10F56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10F57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10F58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10F59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10F5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10F5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10F5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10F5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10F5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10F5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10F60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10F61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10F62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10F63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10F64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10F65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10F66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10F67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10F68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10F69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10F6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10F6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10F6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10F6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10F6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10F6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10F70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10F71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10F72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10F73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10F74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10F75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10F76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10F77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10F78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10F79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10F7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10F7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10F7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10F7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10F7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10F7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10F80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10F81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10F82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10F83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10F84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10F85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10F86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10F87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10F88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10F89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10F8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10F8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10F8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10F8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10F8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10F8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10F90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10F91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10F92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10F93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10F94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10F95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10F96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10F97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10F98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10F99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10F9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10F9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10F9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10F9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10F9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10F9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10FA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10FA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10FA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10FA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10FA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10FA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10FA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10FA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10FA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10FA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10FAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10FAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10FAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10FAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10FAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10FAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10FB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10FB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10FB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10FB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10FB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10FB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10FB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10FB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10FB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10FB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10FBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10FBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10FBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10FBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10FBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10FBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10FC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10FC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10FC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10FC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10FC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10FC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10FC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10FC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10FC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10FC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10FCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10FCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10FCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10FCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10FCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10FCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10FD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10FD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10FD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10FD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10FD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10FD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10FD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10FD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10FD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10FD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10FDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10FDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10FDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10FDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10FDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10FDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10FE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10FE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10FE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10FE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10FE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10FE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10FE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10FE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10FE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10FE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10FEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10FEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10FEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10FED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10FEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10FEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10FF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10FF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10FF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10FF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10FF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10FF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10FF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10FF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10FF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10FF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10FFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10FFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10FFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10FFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10FFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10FFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:020000022000DC -:10000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10001000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10002000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10003000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10004000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10005000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10006000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10007000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10008000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10009000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:1000A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:1000B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:1000C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:1000D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1000E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1000F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10010000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10011000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10012000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10013000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10014000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10015000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10016000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10017000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10018000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10019000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:1001A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:1001B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:1001C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1001D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1001E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1001F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10020000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10021000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10022000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10023000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10024000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10025000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10026000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10027000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10028000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10029000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:1002A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:1002B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:1002C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1002D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1002E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1002F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10030000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10031000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10032000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10033000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10034000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10035000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10036000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10037000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10038000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10039000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:1003A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:1003B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:1003C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1003D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1003E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1003F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10040000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10041000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10042000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10043000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10044000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10045000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10046000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10047000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10048000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10049000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:1004A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:1004B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:1004C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1004D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1004E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1004F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10050000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10051000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10052000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10053000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10054000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10055000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10056000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10057000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10058000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10059000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:1005A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:1005B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:1005C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1005D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1005E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1005F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10060000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10061000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10062000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10063000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10064000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10065000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10066000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10067000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10068000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10069000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:1006A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:1006B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:1006C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:1006D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1006E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1006F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10070000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10071000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10072000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10073000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10074000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10075000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10076000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10077000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10078000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10079000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:1007A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:1007B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:1007C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:1007D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1007E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1007F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10080000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10081000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10082000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10083000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10084000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10085000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10086000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10087000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10088000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10089000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:1008A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:1008B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:1008C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:1008D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1008E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1008F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10090000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10091000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10092000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10093000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10094000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10095000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10096000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10097000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10098000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10099000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:1009A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:1009B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:1009C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:1009D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1009E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1009F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:100A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:100A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:100A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:100A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:100A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:100A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:100A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:100A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:100A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:100A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:100AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:100AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:100AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:100AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:100AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:100AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:100B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:100B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:100B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:100B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:100B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:100B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:100B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:100B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:100B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:100B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:100BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:100BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:100BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:100BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:100BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:100BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:100C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:100C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:100C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:100C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:100C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:100C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:100C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:100C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:100C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:100C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:100CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:100CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:100CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:100CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:100CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:100CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:100D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:100D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:100D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:100D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:100D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:100D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:100D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:100D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:100D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:100D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:100DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:100DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:100DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:100DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:100DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:100DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:100E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:100E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:100E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:100E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:100E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:100E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:100E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:100E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:100E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:100E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:100EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:100EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:100EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:100ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:100EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:100EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:100F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:100F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:100F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:100F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:100F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:100F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:100F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:100F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:100F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:100F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:100FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:100FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:100FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:100FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:100FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:100FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10100000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10101000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10102000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10103000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10104000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10105000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10106000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10107000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10108000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10109000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:1010A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:1010B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:1010C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1010D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1010E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1010F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10110000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10111000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10112000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10113000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10114000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10115000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10116000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10117000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10118000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10119000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:1011A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:1011B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1011C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1011D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1011E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1011F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10120000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10121000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10122000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10123000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10124000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10125000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10126000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10127000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10128000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10129000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:1012A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:1012B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1012C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1012D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1012E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1012F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10130000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10131000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10132000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10133000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10134000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10135000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10136000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10137000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10138000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10139000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:1013A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:1013B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1013C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1013D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1013E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1013F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10140000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10141000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10142000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10143000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10144000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10145000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10146000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10147000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10148000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10149000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:1014A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:1014B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1014C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1014D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1014E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1014F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10150000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10151000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10152000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10153000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10154000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10155000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10156000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10157000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10158000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10159000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:1015A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:1015B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1015C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1015D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1015E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1015F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10160000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10161000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10162000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10163000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10164000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10165000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10166000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10167000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10168000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10169000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:1016A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:1016B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:1016C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1016D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1016E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1016F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10170000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10171000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10172000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10173000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10174000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10175000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10176000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10177000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10178000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10179000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:1017A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:1017B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:1017C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1017D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1017E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1017F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10180000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10181000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10182000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10183000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10184000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10185000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10186000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10187000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10188000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10189000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:1018A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:1018B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:1018C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1018D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1018E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1018F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10190000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10191000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10192000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10193000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10194000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10195000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10196000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10197000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10198000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10199000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:1019A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:1019B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:1019C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1019D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1019E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1019F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:101A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:101A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:101A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:101A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:101A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:101A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:101A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:101A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:101A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:101A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:101AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:101AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:101AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:101AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:101AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:101AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:101B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:101B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:101B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:101B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:101B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:101B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:101B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:101B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:101B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:101B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:101BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:101BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:101BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:101BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:101BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:101BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:101C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:101C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:101C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:101C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:101C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:101C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:101C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:101C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:101C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:101C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:101CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:101CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:101CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:101CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:101CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:101CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:101D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:101D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:101D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:101D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:101D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:101D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:101D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:101D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:101D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:101D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:101DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:101DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:101DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:101DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:101DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:101DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:101E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:101E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:101E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:101E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:101E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:101E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:101E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:101E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:101E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:101E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:101EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:101EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:101EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:101ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:101EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:101EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:101F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:101F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:101F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:101F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:101F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:101F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:101F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:101F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:101F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:101F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:101FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:101FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:101FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:101FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:101FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:101FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10200000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10201000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10202000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10203000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10204000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10205000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10206000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10207000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10208000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10209000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:1020A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:1020B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1020C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1020D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1020E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1020F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10210000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10211000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10212000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10213000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10214000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10215000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10216000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10217000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10218000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10219000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:1021A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1021B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1021C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1021D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1021E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1021F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10220000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10221000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10222000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10223000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10224000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10225000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10226000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10227000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10228000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10229000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:1022A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1022B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1022C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1022D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1022E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1022F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10230000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10231000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10232000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10233000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10234000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10235000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10236000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10237000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10238000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10239000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:1023A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1023B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1023C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1023D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1023E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1023F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10240000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10241000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10242000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10243000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10244000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10245000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10246000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10247000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10248000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10249000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:1024A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1024B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1024C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1024D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1024E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1024F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10250000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10251000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10252000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10253000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10254000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10255000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10256000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10257000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10258000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10259000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:1025A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1025B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1025C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1025D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1025E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1025F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10260000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10261000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10262000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10263000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10264000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10265000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10266000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10267000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10268000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10269000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:1026A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:1026B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1026C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1026D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1026E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1026F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10270000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10271000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10272000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10273000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10274000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10275000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10276000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10277000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10278000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10279000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:1027A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:1027B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1027C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1027D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1027E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1027F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10280000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10281000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10282000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10283000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10284000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10285000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10286000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10287000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10288000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10289000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:1028A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:1028B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1028C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1028D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1028E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1028F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10290000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10291000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10292000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10293000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10294000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10295000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10296000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10297000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10298000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10299000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:1029A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:1029B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1029C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1029D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1029E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1029F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:102A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:102A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:102A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:102A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:102A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:102A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:102A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:102A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:102A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:102A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:102AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:102AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:102AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:102AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:102AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:102AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:102B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:102B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:102B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:102B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:102B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:102B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:102B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:102B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:102B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:102B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:102BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:102BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:102BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:102BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:102BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:102BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:102C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:102C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:102C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:102C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:102C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:102C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:102C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:102C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:102C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:102C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:102CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:102CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:102CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:102CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:102CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:102CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:102D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:102D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:102D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:102D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:102D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:102D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:102D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:102D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:102D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:102D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:102DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:102DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:102DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:102DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:102DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:102DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:102E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:102E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:102E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:102E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:102E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:102E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:102E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:102E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:102E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:102E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:102EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:102EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:102EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:102ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:102EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:102EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:102F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:102F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:102F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:102F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:102F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:102F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:102F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:102F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:102F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:102F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:102FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:102FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:102FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:102FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:102FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:102FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10300000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10301000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10302000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10303000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10304000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10305000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10306000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10307000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10308000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10309000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:1030A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1030B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1030C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1030D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1030E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1030F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10310000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10311000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10312000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10313000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10314000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10315000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10316000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10317000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10318000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10319000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1031A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1031B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1031C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1031D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1031E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1031F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10320000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10321000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10322000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10323000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10324000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10325000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10326000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10327000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10328000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10329000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1032A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1032B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1032C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1032D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1032E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1032F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10330000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10331000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10332000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10333000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10334000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10335000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10336000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10337000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10338000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10339000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1033A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1033B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1033C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1033D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1033E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1033F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10340000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10341000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10342000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10343000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10344000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10345000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10346000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10347000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10348000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10349000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1034A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1034B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1034C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1034D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1034E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1034F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10350000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10351000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10352000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10353000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10354000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10355000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10356000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10357000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10358000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10359000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1035A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1035B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1035C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1035D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1035E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1035F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10360000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10361000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10362000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10363000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10364000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10365000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10366000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10367000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10368000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10369000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:1036A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1036B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1036C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1036D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1036E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1036F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10370000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10371000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10372000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10373000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10374000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10375000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10376000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10377000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10378000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10379000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:1037A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1037B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1037C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1037D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1037E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1037F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10380000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10381000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10382000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10383000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10384000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10385000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10386000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10387000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10388000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10389000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:1038A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1038B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1038C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1038D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1038E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1038F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10390000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10391000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10392000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10393000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10394000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10395000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10396000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10397000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10398000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10399000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:1039A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1039B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1039C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1039D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1039E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1039F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:103A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:103A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:103A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:103A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:103A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:103A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:103A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:103A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:103A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:103A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:103AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:103AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:103AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:103AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:103AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:103AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:103B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:103B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:103B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:103B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:103B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:103B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:103B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:103B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:103B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:103B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:103BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:103BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:103BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:103BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:103BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:103BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:103C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:103C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:103C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:103C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:103C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:103C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:103C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:103C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:103C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:103C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:103CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:103CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:103CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:103CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:103CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:103CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:103D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:103D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:103D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:103D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:103D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:103D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:103D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:103D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:103D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:103D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:103DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:103DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:103DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:103DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:103DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:103DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:103E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:103E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:103E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:103E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:103E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:103E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:103E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:103E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:103E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:103E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:103EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:103EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:103EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:103ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:103EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:103EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:103F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:103F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:103F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:103F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:103F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:103F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:103F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:103F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:103F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:103F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:103FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:103FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:103FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:103FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:103FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:103FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10400000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10401000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10402000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10403000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10404000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10405000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10406000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10407000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10408000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10409000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1040A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1040B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1040C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1040D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1040E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1040F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10410000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10411000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10412000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10413000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10414000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10415000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10416000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10417000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10418000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10419000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1041A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1041B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1041C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1041D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1041E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1041F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10420000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10421000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10422000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10423000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10424000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10425000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10426000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10427000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10428000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10429000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1042A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1042B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1042C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1042D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1042E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1042F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10430000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10431000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10432000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10433000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10434000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10435000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10436000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10437000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10438000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10439000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1043A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1043B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1043C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1043D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1043E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1043F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10440000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10441000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10442000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10443000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10444000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10445000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10446000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10447000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10448000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10449000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1044A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1044B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1044C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1044D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1044E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1044F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10450000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10451000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10452000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10453000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10454000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10455000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10456000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10457000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10458000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10459000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1045A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1045B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1045C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1045D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1045E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1045F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10460000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10461000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10462000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10463000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10464000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10465000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10466000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10467000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10468000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10469000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1046A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1046B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1046C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1046D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1046E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1046F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10470000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10471000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10472000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10473000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10474000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10475000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10476000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10477000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10478000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10479000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1047A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1047B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1047C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1047D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1047E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1047F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10480000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10481000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10482000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10483000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10484000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10485000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10486000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10487000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10488000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10489000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1048A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1048B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1048C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1048D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1048E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1048F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10490000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10491000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10492000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10493000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10494000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10495000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10496000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10497000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10498000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10499000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1049A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1049B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1049C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1049D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1049E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1049F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:104A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:104A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:104A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:104A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:104A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:104A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:104A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:104A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:104A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:104A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:104AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:104AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:104AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:104AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:104AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:104AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:104B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:104B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:104B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:104B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:104B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:104B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:104B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:104B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:104B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:104B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:104BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:104BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:104BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:104BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:104BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:104BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:104C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:104C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:104C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:104C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:104C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:104C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:104C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:104C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:104C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:104C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:104CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:104CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:104CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:104CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:104CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:104CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:104D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:104D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:104D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:104D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:104D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:104D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:104D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:104D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:104D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:104D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:104DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:104DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:104DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:104DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:104DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:104DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:104E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:104E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:104E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:104E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:104E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:104E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:104E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:104E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:104E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:104E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:104EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:104EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:104EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:104ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:104EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:104EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:104F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:104F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:104F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:104F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:104F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:104F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:104F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:104F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:104F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:104F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:104FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:104FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:104FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:104FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:104FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:104FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10500000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10501000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10502000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10503000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10504000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10505000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10506000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10507000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10508000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10509000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1050A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1050B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1050C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1050D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1050E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1050F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10510000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10511000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10512000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10513000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10514000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10515000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10516000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10517000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10518000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10519000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1051A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1051B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1051C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1051D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1051E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1051F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10520000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10521000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10522000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10523000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10524000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10525000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10526000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10527000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10528000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10529000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1052A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1052B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1052C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1052D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1052E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1052F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10530000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10531000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10532000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10533000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10534000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10535000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10536000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10537000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10538000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10539000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1053A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1053B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1053C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1053D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1053E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1053F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10540000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10541000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10542000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10543000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10544000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10545000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10546000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10547000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10548000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10549000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1054A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1054B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1054C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1054D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1054E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1054F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10550000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10551000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10552000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10553000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10554000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10555000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10556000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10557000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10558000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10559000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1055A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1055B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1055C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1055D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1055E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1055F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10560000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10561000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10562000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10563000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10564000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10565000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10566000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10567000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10568000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10569000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1056A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1056B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1056C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1056D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1056E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1056F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10570000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10571000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10572000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10573000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10574000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10575000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10576000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10577000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10578000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10579000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1057A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1057B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1057C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1057D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1057E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1057F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10580000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10581000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10582000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10583000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10584000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10585000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10586000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10587000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10588000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10589000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1058A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1058B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1058C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1058D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1058E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1058F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10590000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10591000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10592000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10593000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10594000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10595000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10596000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10597000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10598000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10599000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1059A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1059B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1059C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1059D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1059E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1059F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:105A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:105A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:105A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:105A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:105A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:105A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:105A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:105A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:105A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:105A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:105AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:105AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:105AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:105AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:105AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:105AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:105B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:105B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:105B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:105B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:105B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:105B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:105B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:105B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:105B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:105B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:105BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:105BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:105BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:105BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:105BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:105BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:105C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:105C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:105C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:105C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:105C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:105C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:105C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:105C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:105C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:105C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:105CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:105CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:105CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:105CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:105CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:105CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:105D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:105D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:105D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:105D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:105D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:105D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:105D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:105D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:105D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:105D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:105DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:105DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:105DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:105DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:105DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:105DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:105E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:105E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:105E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:105E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:105E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:105E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:105E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:105E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:105E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:105E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:105EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:105EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:105EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:105ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:105EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:105EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:105F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:105F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:105F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:105F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:105F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:105F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:105F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:105F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:105F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:105F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:105FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:105FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:105FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:105FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:105FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:105FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10600000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10601000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10602000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10603000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10604000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10605000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10606000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10607000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10608000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10609000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1060A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1060B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1060C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1060D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1060E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1060F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10610000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10611000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10612000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10613000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10614000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10615000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10616000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10617000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10618000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10619000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1061A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1061B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1061C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1061D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1061E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1061F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10620000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10621000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10622000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10623000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10624000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10625000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10626000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10627000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10628000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10629000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1062A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1062B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1062C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1062D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1062E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1062F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10630000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10631000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10632000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10633000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10634000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10635000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10636000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10637000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10638000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10639000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1063A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1063B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1063C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1063D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1063E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1063F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10640000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10641000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10642000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10643000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10644000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10645000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10646000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10647000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10648000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10649000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1064A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1064B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1064C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1064D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1064E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1064F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10650000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10651000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10652000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10653000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10654000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10655000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10656000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10657000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10658000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10659000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1065A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1065B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1065C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1065D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1065E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1065F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10660000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10661000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10662000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10663000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10664000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10665000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10666000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10667000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10668000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10669000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1066A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1066B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1066C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1066D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1066E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1066F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10670000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10671000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10672000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10673000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10674000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10675000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10676000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10677000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10678000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10679000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1067A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1067B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1067C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1067D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1067E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1067F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10680000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10681000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10682000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10683000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10684000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10685000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10686000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10687000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10688000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10689000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1068A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1068B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1068C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1068D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1068E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1068F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10690000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10691000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10692000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10693000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10694000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10695000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10696000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10697000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10698000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10699000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1069A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1069B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1069C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1069D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1069E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1069F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:106A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:106A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:106A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:106A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:106A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:106A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:106A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:106A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:106A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:106A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:106AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:106AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:106AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:106AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:106AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:106AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:106B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:106B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:106B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:106B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:106B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:106B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:106B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:106B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:106B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:106B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:106BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:106BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:106BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:106BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:106BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:106BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:106C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:106C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:106C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:106C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:106C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:106C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:106C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:106C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:106C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:106C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:106CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:106CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:106CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:106CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:106CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:106CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:106D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:106D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:106D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:106D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:106D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:106D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:106D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:106D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:106D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:106D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:106DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:106DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:106DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:106DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:106DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:106DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:106E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:106E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:106E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:106E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:106E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:106E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:106E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:106E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:106E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:106E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:106EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:106EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:106EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:106ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:106EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:106EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:106F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:106F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:106F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:106F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:106F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:106F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:106F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:106F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:106F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:106F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:106FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:106FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:106FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:106FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:106FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:106FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10700000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10701000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10702000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10703000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10704000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10705000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10706000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10707000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10708000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10709000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1070A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1070B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1070C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1070D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1070E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1070F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10710000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10711000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10712000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10713000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10714000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10715000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10716000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10717000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10718000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10719000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1071A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1071B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1071C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1071D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1071E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1071F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10720000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10721000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10722000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10723000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10724000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10725000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10726000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10727000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10728000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10729000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1072A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1072B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1072C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1072D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1072E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1072F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10730000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10731000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10732000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10733000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10734000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10735000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10736000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10737000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10738000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10739000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1073A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1073B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1073C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1073D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1073E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1073F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10740000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10741000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10742000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10743000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10744000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10745000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10746000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10747000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10748000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10749000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1074A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1074B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1074C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1074D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1074E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1074F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10750000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10751000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10752000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10753000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10754000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10755000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10756000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10757000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10758000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10759000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1075A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1075B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1075C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1075D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1075E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1075F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10760000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10761000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10762000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10763000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10764000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10765000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10766000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10767000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10768000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10769000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1076A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1076B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1076C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1076D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1076E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1076F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10770000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10771000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10772000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10773000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10774000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10775000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10776000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10777000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10778000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10779000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1077A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1077B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1077C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1077D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1077E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1077F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10780000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10781000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10782000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10783000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10784000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10785000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10786000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10787000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10788000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10789000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1078A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1078B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1078C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1078D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1078E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1078F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10790000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10791000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10792000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10793000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10794000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10795000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10796000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10797000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10798000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10799000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1079A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1079B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1079C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1079D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1079E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1079F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:107A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:107A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:107A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:107A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:107A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:107A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:107A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:107A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:107A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:107A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:107AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:107AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:107AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:107AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:107AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:107AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:107B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:107B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:107B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:107B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:107B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:107B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:107B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:107B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:107B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:107B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:107BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:107BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:107BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:107BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:107BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:107BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:107C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:107C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:107C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:107C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:107C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:107C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:107C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:107C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:107C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:107C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:107CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:107CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:107CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:107CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:107CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:107CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:107D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:107D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:107D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:107D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:107D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:107D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:107D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:107D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:107D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:107D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:107DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:107DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:107DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:107DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:107DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:107DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:107E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:107E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:107E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:107E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:107E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:107E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:107E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:107E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:107E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:107E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:107EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:107EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:107EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:107ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:107EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:107EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:107F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:107F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:107F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:107F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:107F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:107F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:107F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:107F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:107F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:107F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:107FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:107FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:107FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:107FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:107FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:107FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10800000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10801000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10802000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10803000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10804000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10805000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10806000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10807000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10808000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10809000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1080A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1080B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1080C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1080D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1080E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:1080F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10810000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10811000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10812000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10813000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10814000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10815000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10816000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10817000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10818000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10819000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1081A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1081B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1081C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1081D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1081E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:1081F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10820000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10821000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10822000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10823000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10824000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10825000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10826000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10827000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10828000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10829000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1082A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1082B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1082C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1082D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1082E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:1082F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10830000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10831000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10832000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10833000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10834000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10835000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10836000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10837000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10838000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10839000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1083A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1083B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1083C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1083D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1083E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:1083F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10840000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10841000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10842000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10843000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10844000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10845000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10846000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10847000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10848000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10849000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1084A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1084B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1084C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1084D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1084E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:1084F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10850000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10851000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10852000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10853000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10854000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10855000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10856000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10857000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10858000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10859000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1085A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1085B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1085C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1085D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1085E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:1085F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10860000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10861000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10862000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10863000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10864000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10865000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10866000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10867000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10868000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10869000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1086A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1086B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1086C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1086D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1086E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:1086F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10870000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10871000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10872000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10873000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10874000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10875000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10876000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10877000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10878000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10879000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1087A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1087B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1087C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1087D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1087E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:1087F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10880000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10881000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10882000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10883000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10884000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10885000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10886000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10887000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10888000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10889000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1088A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1088B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1088C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1088D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1088E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:1088F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10890000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10891000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10892000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10893000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10894000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10895000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10896000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10897000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10898000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10899000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1089A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1089B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1089C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1089D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1089E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:1089F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:108A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:108A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:108A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:108A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:108A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:108A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:108A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:108A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:108A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:108A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:108AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:108AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:108AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:108AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:108AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:108AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:108B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:108B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:108B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:108B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:108B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:108B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:108B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:108B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:108B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:108B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:108BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:108BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:108BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:108BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:108BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:108BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:108C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:108C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:108C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:108C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:108C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:108C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:108C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:108C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:108C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:108C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:108CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:108CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:108CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:108CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:108CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:108CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:108D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:108D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:108D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:108D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:108D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:108D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:108D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:108D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:108D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:108D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:108DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:108DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:108DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:108DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:108DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:108DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:108E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:108E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:108E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:108E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:108E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:108E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:108E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:108E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:108E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:108E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:108EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:108EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:108EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:108ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:108EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:108EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:108F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:108F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:108F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:108F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:108F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:108F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:108F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:108F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:108F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:108F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:108FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:108FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:108FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:108FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:108FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:108FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10900000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10901000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10902000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10903000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10904000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10905000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10906000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10907000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10908000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10909000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1090A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1090B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1090C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1090D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:1090E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:1090F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10910000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10911000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10912000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10913000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10914000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10915000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10916000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10917000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10918000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10919000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1091A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1091B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1091C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1091D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:1091E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:1091F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10920000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10921000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10922000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10923000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10924000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10925000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10926000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10927000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10928000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10929000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1092A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1092B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1092C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1092D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:1092E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:1092F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10930000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10931000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10932000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10933000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10934000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10935000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10936000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10937000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10938000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10939000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1093A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1093B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1093C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1093D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:1093E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:1093F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10940000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10941000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10942000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10943000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10944000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10945000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10946000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10947000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10948000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10949000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1094A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1094B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1094C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1094D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:1094E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:1094F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10950000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10951000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10952000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10953000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10954000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10955000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10956000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10957000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10958000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10959000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1095A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1095B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1095C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1095D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:1095E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:1095F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10960000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10961000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10962000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10963000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10964000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10965000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10966000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10967000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10968000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10969000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1096A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1096B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1096C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1096D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:1096E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:1096F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10970000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10971000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10972000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10973000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10974000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10975000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10976000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10977000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10978000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10979000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1097A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1097B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1097C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1097D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:1097E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:1097F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10980000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10981000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10982000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10983000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10984000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10985000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10986000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10987000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10988000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10989000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1098A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1098B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1098C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1098D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:1098E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:1098F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10990000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10991000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10992000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10993000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10994000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10995000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10996000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10997000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10998000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10999000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1099A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1099B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1099C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1099D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:1099E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:1099F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:109A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:109A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:109A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:109A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:109A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:109A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:109A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:109A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:109A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:109A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:109AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:109AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:109AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:109AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:109AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:109AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:109B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:109B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:109B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:109B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:109B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:109B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:109B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:109B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:109B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:109B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:109BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:109BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:109BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:109BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:109BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:109BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:109C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:109C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:109C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:109C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:109C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:109C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:109C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:109C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:109C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:109C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:109CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:109CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:109CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:109CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:109CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:109CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:109D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:109D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:109D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:109D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:109D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:109D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:109D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:109D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:109D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:109D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:109DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:109DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:109DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:109DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:109DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:109DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:109E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:109E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:109E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:109E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:109E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:109E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:109E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:109E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:109E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:109E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:109EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:109EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:109EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:109ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:109EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:109EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:109F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:109F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:109F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:109F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:109F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:109F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:109F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:109F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:109F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:109F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:109FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:109FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:109FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:109FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:109FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:109FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10A00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10A01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10A02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10A03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10A04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10A05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10A06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10A07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10A08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10A09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10A0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10A0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10A0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10A0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10A0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10A0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10A10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10A11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10A12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10A13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10A14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10A15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10A16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10A17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10A18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10A19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10A1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10A1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10A1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10A1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10A1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10A1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10A20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10A21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10A22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10A23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10A24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10A25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10A26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10A27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10A28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10A29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10A2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10A2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10A2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10A2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10A2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10A2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10A30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10A31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10A32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10A33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10A34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10A35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10A36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10A37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10A38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10A39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10A3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10A3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10A3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10A3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10A3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10A3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10A40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10A41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10A42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10A43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10A44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10A45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10A46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10A47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10A48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10A49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10A4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10A4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10A4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10A4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10A4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10A4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10A50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10A51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10A52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10A53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10A54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10A55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10A56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10A57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10A58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10A59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10A5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10A5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10A5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10A5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10A5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10A5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10A60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10A61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10A62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10A63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10A64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10A65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10A66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10A67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10A68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10A69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10A6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10A6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10A6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10A6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10A6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10A6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10A70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10A71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10A72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10A73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10A74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10A75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10A76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10A77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10A78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10A79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10A7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10A7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10A7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10A7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10A7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10A7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10A80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10A81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10A82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10A83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10A84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10A85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10A86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10A87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10A88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10A89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10A8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10A8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10A8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10A8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10A8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10A8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10A90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10A91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10A92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10A93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10A94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10A95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10A96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10A97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10A98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10A99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10A9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10A9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10A9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10A9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10A9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10A9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10AA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10AA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10AA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10AA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10AA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10AA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10AA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10AA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10AA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10AA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10AAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10AAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10AAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10AAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10AAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10AAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10AB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10AB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10AB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10AB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10AB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10AB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10AB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10AB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10AB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10AB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10ABA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10ABB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10ABC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10ABD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10ABE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10ABF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10AC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10AC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10AC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10AC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10AC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10AC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10AC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10AC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10AC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10AC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10ACA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10ACB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10ACC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10ACD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10ACE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10ACF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10AD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10AD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10AD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10AD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10AD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10AD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10AD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10AD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10AD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10AD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10ADA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10ADB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10ADC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10ADD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10ADE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10ADF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10AE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10AE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10AE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10AE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10AE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10AE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10AE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10AE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10AE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10AE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10AEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10AEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10AEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10AED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10AEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10AEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10AF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10AF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10AF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10AF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10AF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10AF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10AF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10AF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10AF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10AF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10AFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10AFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10AFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10AFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10AFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10AFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10B00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10B01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10B02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10B03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10B04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10B05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10B06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10B07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10B08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10B09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10B0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10B0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10B0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10B0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10B0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10B0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10B10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10B11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10B12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10B13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10B14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10B15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10B16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10B17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10B18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10B19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10B1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10B1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10B1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10B1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10B1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10B1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10B20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10B21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10B22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10B23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10B24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10B25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10B26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10B27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10B28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10B29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10B2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10B2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10B2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10B2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10B2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10B2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10B30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10B31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10B32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10B33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10B34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10B35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10B36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10B37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10B38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10B39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10B3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10B3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10B3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10B3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10B3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10B3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10B40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10B41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10B42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10B43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10B44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10B45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10B46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10B47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10B48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10B49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10B4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10B4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10B4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10B4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10B4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10B4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10B50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10B51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10B52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10B53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10B54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10B55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10B56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10B57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10B58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10B59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10B5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10B5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10B5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10B5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10B5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10B5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10B60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10B61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10B62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10B63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10B64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10B65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10B66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10B67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10B68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10B69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10B6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10B6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10B6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10B6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10B6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10B6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10B70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10B71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10B72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10B73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10B74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10B75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10B76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10B77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10B78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10B79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10B7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10B7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10B7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10B7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10B7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10B7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10B80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10B81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10B82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10B83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10B84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10B85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10B86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10B87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10B88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10B89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10B8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10B8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10B8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10B8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10B8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10B8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10B90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10B91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10B92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10B93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10B94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10B95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10B96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10B97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10B98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10B99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10B9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10B9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10B9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10B9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10B9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10B9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10BA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10BA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10BA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10BA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10BA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10BA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10BA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10BA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10BA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10BA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10BAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10BAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10BAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10BAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10BAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10BAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10BB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10BB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10BB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10BB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10BB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10BB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10BB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10BB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10BB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10BB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10BBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10BBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10BBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10BBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10BBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10BBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10BC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10BC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10BC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10BC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10BC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10BC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10BC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10BC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10BC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10BC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10BCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10BCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10BCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10BCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10BCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10BCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10BD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10BD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10BD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10BD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10BD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10BD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10BD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10BD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10BD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10BD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10BDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10BDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10BDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10BDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10BDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10BDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10BE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10BE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10BE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10BE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10BE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10BE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10BE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10BE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10BE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10BE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10BEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10BEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10BEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10BED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10BEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10BEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10BF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10BF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10BF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10BF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10BF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10BF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10BF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10BF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10BF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10BF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10BFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10BFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10BFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10BFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10BFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10BFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10C00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10C01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10C02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10C03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10C04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10C05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10C06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10C07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10C08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10C09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10C0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10C0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10C0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10C0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10C0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10C0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10C10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10C11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10C12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10C13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10C14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10C15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10C16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10C17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10C18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10C19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10C1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10C1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10C1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10C1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10C1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10C1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10C20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10C21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10C22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10C23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10C24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10C25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10C26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10C27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10C28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10C29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10C2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10C2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10C2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10C2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10C2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10C2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10C30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10C31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10C32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10C33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10C34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10C35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10C36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10C37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10C38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10C39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10C3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10C3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10C3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10C3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10C3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10C3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10C40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10C41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10C42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10C43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10C44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10C45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10C46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10C47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10C48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10C49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10C4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10C4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10C4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10C4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10C4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10C4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10C50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10C51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10C52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10C53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10C54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10C55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10C56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10C57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10C58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10C59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10C5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10C5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10C5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10C5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10C5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10C5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10C60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10C61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10C62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10C63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10C64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10C65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10C66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10C67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10C68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10C69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10C6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10C6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10C6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10C6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10C6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10C6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10C70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10C71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10C72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10C73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10C74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10C75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10C76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10C77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10C78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10C79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10C7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10C7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10C7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10C7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10C7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10C7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10C80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10C81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10C82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10C83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10C84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10C85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10C86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10C87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10C88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10C89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10C8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10C8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10C8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10C8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10C8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10C8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10C90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10C91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10C92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10C93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10C94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10C95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10C96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10C97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10C98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10C99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10C9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10C9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10C9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10C9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10C9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10C9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10CA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10CA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10CA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10CA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10CA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10CA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10CA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10CA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10CA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10CA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10CAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10CAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10CAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10CAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10CAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10CAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10CB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10CB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10CB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10CB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10CB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10CB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10CB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10CB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10CB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10CB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10CBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10CBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10CBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10CBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10CBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10CBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10CC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10CC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10CC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10CC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10CC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10CC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10CC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10CC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10CC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10CC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10CCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10CCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10CCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10CCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10CCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10CCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10CD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10CD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10CD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10CD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10CD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10CD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10CD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10CD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10CD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10CD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10CDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10CDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10CDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10CDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10CDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10CDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10CE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10CE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10CE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10CE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10CE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10CE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10CE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10CE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10CE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10CE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10CEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10CEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10CEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10CED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10CEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10CEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10CF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10CF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10CF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10CF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10CF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10CF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10CF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10CF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10CF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10CF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10CFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10CFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10CFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10CFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10CFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10CFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10D00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10D01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10D02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10D03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10D04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10D05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10D06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10D07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10D08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10D09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10D0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10D0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10D0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10D0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10D0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10D0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10D10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10D11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10D12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10D13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10D14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10D15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10D16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10D17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10D18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10D19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10D1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10D1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10D1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10D1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10D1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10D1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10D20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10D21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10D22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10D23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10D24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10D25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10D26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10D27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10D28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10D29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10D2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10D2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10D2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10D2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10D2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10D2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10D30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10D31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10D32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10D33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10D34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10D35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10D36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10D37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10D38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10D39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10D3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10D3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10D3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10D3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10D3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10D3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10D40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10D41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10D42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10D43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10D44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10D45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10D46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10D47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10D48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10D49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10D4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10D4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10D4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10D4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10D4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10D4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10D50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10D51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10D52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10D53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10D54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10D55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10D56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10D57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10D58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10D59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10D5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10D5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10D5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10D5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10D5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10D5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10D60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10D61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10D62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10D63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10D64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10D65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10D66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10D67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10D68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10D69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10D6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10D6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10D6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10D6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10D6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10D6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10D70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10D71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10D72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10D73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10D74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10D75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10D76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10D77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10D78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10D79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10D7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10D7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10D7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10D7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10D7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10D7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10D80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10D81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10D82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10D83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10D84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10D85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10D86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10D87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10D88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10D89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10D8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10D8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10D8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10D8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10D8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10D8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10D90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10D91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10D92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10D93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10D94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10D95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10D96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10D97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10D98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10D99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10D9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10D9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10D9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10D9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10D9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10D9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10DA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10DA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10DA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10DA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10DA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10DA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10DA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10DA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10DA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10DA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10DAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10DAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10DAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10DAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10DAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10DAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10DB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10DB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10DB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10DB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10DB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10DB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10DB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10DB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10DB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10DB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10DBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10DBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10DBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10DBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10DBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10DBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10DC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10DC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10DC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10DC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10DC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10DC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10DC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10DC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10DC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10DC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10DCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10DCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10DCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10DCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10DCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10DCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10DD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10DD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10DD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10DD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10DD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10DD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10DD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10DD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10DD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10DD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10DDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10DDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10DDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10DDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10DDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10DDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10DE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10DE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10DE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10DE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10DE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10DE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10DE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10DE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10DE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10DE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10DEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10DEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10DEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10DED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10DEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10DEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10DF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10DF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10DF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10DF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10DF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10DF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10DF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10DF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10DF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10DF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10DFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10DFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10DFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10DFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10DFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10DFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10E00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10E01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10E02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10E03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10E04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10E05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10E06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10E07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10E08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10E09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10E0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10E0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10E0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10E0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10E0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10E0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10E10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10E11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10E12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10E13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10E14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10E15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10E16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10E17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10E18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10E19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10E1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10E1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10E1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10E1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10E1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10E1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10E20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10E21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10E22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10E23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10E24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10E25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10E26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10E27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10E28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10E29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10E2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10E2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10E2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10E2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10E2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10E2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10E30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10E31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10E32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10E33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10E34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10E35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10E36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10E37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10E38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10E39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10E3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10E3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10E3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10E3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10E3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10E3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10E40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10E41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10E42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10E43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10E44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10E45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10E46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10E47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10E48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10E49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10E4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10E4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10E4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10E4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10E4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10E4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10E50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10E51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10E52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10E53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10E54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10E55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10E56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10E57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10E58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10E59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10E5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10E5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10E5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10E5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10E5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10E5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10E60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10E61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10E62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10E63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10E64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10E65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10E66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10E67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10E68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10E69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10E6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10E6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10E6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10E6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10E6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10E6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10E70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10E71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10E72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10E73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10E74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10E75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10E76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10E77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10E78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10E79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10E7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10E7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10E7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10E7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10E7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10E7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10E80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10E81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10E82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10E83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10E84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10E85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10E86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10E87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10E88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10E89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10E8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10E8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10E8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10E8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10E8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10E8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10E90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10E91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10E92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10E93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10E94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10E95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10E96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10E97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10E98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10E99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10E9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10E9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10E9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10E9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10E9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10E9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10EA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10EA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10EA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10EA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10EA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10EA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10EA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10EA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10EA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10EA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10EAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10EAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10EAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10EAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10EAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10EAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10EB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10EB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10EB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10EB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10EB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10EB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10EB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10EB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10EB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10EB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10EBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10EBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10EBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10EBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10EBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10EBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10EC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10EC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10EC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10EC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10EC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10EC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10EC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10EC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10EC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10EC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10ECA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10ECB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10ECC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10ECD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10ECE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10ECF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10ED0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10ED1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10ED2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10ED3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10ED4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10ED5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10ED6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10ED7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10ED8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10ED9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10EDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10EDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10EDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10EDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10EDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10EDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10EE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10EE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10EE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10EE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10EE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10EE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10EE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10EE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10EE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10EE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10EEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10EEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10EEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10EED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10EEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10EEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10EF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10EF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10EF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10EF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10EF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10EF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10EF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10EF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10EF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10EF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10EFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10EFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10EFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10EFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10EFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10EFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10F00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10F01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10F02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10F03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10F04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10F05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10F06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10F07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10F08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10F09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10F0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10F0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10F0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10F0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10F0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10F0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10F10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10F11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10F12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10F13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10F14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10F15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10F16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10F17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10F18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10F19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10F1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10F1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10F1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10F1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10F1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10F1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10F20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10F21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10F22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10F23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10F24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10F25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10F26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10F27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10F28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10F29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10F2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10F2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10F2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10F2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10F2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10F2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10F30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10F31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10F32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10F33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10F34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10F35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10F36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10F37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10F38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10F39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10F3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10F3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10F3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10F3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10F3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10F3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10F40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10F41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10F42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10F43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10F44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10F45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10F46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10F47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10F48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10F49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10F4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10F4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10F4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10F4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10F4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10F4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10F50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10F51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10F52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10F53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10F54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10F55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10F56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10F57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10F58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10F59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10F5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10F5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10F5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10F5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10F5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10F5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10F60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10F61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10F62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10F63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10F64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10F65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10F66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10F67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10F68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10F69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10F6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10F6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10F6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10F6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10F6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10F6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10F70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10F71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10F72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10F73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10F74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10F75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10F76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10F77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10F78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10F79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10F7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10F7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10F7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10F7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10F7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10F7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10F80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10F81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10F82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10F83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10F84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10F85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10F86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10F87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10F88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10F89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10F8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10F8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10F8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10F8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10F8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10F8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10F90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10F91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10F92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10F93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10F94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10F95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10F96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10F97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10F98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10F99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10F9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10F9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10F9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10F9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10F9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10F9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10FA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10FA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10FA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10FA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10FA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10FA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10FA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10FA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10FA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10FA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10FAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10FAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10FAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10FAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10FAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10FAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10FB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10FB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10FB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10FB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10FB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10FB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10FB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10FB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10FB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10FB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10FBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10FBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10FBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10FBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10FBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10FBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10FC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10FC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10FC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10FC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10FC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10FC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10FC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10FC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10FC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10FC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10FCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10FCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10FCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10FCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10FCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10FCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10FD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10FD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10FD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10FD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10FD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10FD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10FD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10FD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10FD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10FD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10FDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10FDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10FDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10FDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10FDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10FDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10FE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10FE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10FE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10FE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10FE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10FE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10FE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10FE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10FE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10FE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10FEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10FEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10FEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10FED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10FEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10FEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10FF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10FF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10FF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10FF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10FF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10FF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10FF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10FF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10FF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10FF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10FFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10FFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10FFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10FFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10FFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10FFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:020000023000CC -:10000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10001000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10002000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10003000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10004000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10005000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10006000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10007000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10008000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10009000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:1000A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:1000B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:1000C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:1000D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1000E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1000F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10010000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10011000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10012000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10013000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10014000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10015000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10016000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10017000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10018000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10019000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:1001A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:1001B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:1001C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1001D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1001E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1001F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10020000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10021000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10022000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10023000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10024000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10025000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10026000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10027000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10028000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10029000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:1002A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:1002B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:1002C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1002D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1002E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1002F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10030000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10031000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10032000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10033000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10034000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10035000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10036000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10037000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10038000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10039000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:1003A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:1003B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:1003C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1003D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1003E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1003F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10040000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10041000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10042000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10043000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10044000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10045000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10046000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10047000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10048000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10049000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:1004A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:1004B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:1004C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1004D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1004E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1004F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10050000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10051000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10052000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10053000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10054000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10055000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10056000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10057000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10058000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10059000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:1005A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:1005B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:1005C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1005D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1005E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1005F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10060000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10061000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10062000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10063000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10064000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10065000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10066000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10067000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10068000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10069000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:1006A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:1006B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:1006C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:1006D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1006E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1006F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10070000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10071000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10072000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10073000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10074000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10075000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10076000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10077000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10078000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10079000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:1007A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:1007B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:1007C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:1007D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1007E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1007F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10080000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10081000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10082000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10083000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10084000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10085000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10086000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10087000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10088000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10089000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:1008A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:1008B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:1008C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:1008D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1008E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1008F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10090000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10091000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10092000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10093000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10094000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10095000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10096000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10097000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10098000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10099000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:1009A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:1009B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:1009C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:1009D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1009E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1009F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:100A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:100A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:100A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:100A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:100A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:100A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:100A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:100A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:100A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:100A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:100AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:100AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:100AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:100AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:100AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:100AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:100B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:100B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:100B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:100B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:100B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:100B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:100B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:100B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:100B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:100B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:100BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:100BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:100BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:100BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:100BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:100BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:100C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:100C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:100C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:100C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:100C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:100C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:100C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:100C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:100C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:100C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:100CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:100CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:100CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:100CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:100CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:100CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:100D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:100D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:100D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:100D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:100D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:100D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:100D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:100D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:100D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:100D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:100DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:100DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:100DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:100DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:100DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:100DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:100E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:100E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:100E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:100E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:100E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:100E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:100E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:100E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:100E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:100E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:100EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:100EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:100EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:100ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:100EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:100EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:100F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:100F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:100F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:100F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:100F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:100F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:100F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:100F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:100F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:100F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:100FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:100FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:100FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:100FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:100FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:100FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10100000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10101000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10102000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10103000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10104000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10105000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10106000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10107000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10108000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10109000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:1010A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:1010B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:1010C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1010D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1010E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1010F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10110000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10111000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10112000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10113000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10114000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10115000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10116000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10117000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10118000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10119000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:1011A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:1011B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1011C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1011D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1011E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1011F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10120000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10121000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10122000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10123000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10124000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10125000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10126000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10127000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10128000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10129000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:1012A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:1012B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1012C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1012D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1012E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1012F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10130000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10131000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10132000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10133000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10134000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10135000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10136000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10137000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10138000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10139000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:1013A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:1013B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1013C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1013D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1013E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1013F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10140000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10141000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10142000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10143000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10144000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10145000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10146000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10147000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10148000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10149000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:1014A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:1014B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1014C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1014D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1014E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1014F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10150000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10151000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10152000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10153000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10154000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10155000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10156000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10157000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10158000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10159000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:1015A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:1015B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1015C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1015D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1015E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1015F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10160000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10161000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10162000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10163000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10164000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10165000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10166000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10167000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10168000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10169000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:1016A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:1016B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:1016C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1016D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1016E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1016F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10170000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10171000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10172000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10173000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10174000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10175000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10176000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10177000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10178000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10179000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:1017A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:1017B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:1017C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1017D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1017E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1017F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10180000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10181000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10182000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10183000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10184000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10185000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10186000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10187000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10188000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10189000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:1018A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:1018B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:1018C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1018D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1018E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1018F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10190000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10191000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10192000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10193000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10194000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10195000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10196000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10197000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10198000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10199000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:1019A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:1019B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:1019C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1019D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1019E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1019F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:101A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:101A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:101A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:101A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:101A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:101A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:101A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:101A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:101A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:101A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:101AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:101AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:101AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:101AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:101AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:101AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:101B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:101B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:101B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:101B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:101B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:101B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:101B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:101B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:101B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:101B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:101BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:101BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:101BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:101BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:101BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:101BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:101C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:101C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:101C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:101C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:101C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:101C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:101C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:101C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:101C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:101C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:101CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:101CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:101CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:101CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:101CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:101CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:101D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:101D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:101D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:101D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:101D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:101D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:101D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:101D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:101D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:101D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:101DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:101DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:101DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:101DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:101DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:101DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:101E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:101E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:101E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:101E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:101E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:101E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:101E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:101E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:101E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:101E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:101EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:101EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:101EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:101ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:101EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:101EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:101F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:101F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:101F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:101F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:101F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:101F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:101F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:101F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:101F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:101F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:101FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:101FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:101FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:101FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:101FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:101FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10200000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10201000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10202000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10203000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10204000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10205000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10206000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10207000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10208000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10209000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:1020A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:1020B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1020C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1020D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1020E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1020F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10210000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10211000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10212000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10213000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10214000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10215000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10216000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10217000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10218000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10219000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:1021A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1021B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1021C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1021D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1021E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1021F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10220000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10221000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10222000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10223000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10224000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10225000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10226000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10227000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10228000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10229000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:1022A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1022B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1022C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1022D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1022E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1022F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10230000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10231000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10232000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10233000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10234000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10235000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10236000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10237000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10238000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10239000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:1023A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1023B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1023C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1023D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1023E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1023F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10240000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10241000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10242000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10243000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10244000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10245000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10246000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10247000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10248000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10249000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:1024A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1024B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1024C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1024D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1024E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1024F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10250000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10251000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10252000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10253000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10254000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10255000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10256000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10257000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10258000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10259000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:1025A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1025B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1025C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1025D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1025E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1025F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10260000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10261000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10262000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10263000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10264000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10265000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10266000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10267000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10268000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10269000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:1026A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:1026B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1026C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1026D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1026E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1026F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10270000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10271000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10272000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10273000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10274000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10275000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10276000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10277000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10278000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10279000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:1027A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:1027B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1027C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1027D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1027E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1027F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10280000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10281000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10282000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10283000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10284000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10285000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10286000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10287000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10288000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10289000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:1028A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:1028B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1028C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1028D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1028E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1028F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10290000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10291000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10292000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10293000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10294000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10295000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10296000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10297000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10298000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10299000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:1029A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:1029B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1029C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1029D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1029E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1029F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:102A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:102A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:102A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:102A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:102A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:102A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:102A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:102A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:102A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:102A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:102AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:102AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:102AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:102AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:102AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:102AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:102B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:102B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:102B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:102B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:102B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:102B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:102B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:102B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:102B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:102B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:102BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:102BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:102BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:102BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:102BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:102BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:102C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:102C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:102C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:102C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:102C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:102C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:102C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:102C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:102C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:102C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:102CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:102CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:102CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:102CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:102CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:102CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:102D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:102D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:102D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:102D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:102D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:102D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:102D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:102D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:102D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:102D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:102DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:102DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:102DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:102DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:102DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:102DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:102E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:102E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:102E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:102E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:102E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:102E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:102E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:102E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:102E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:102E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:102EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:102EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:102EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:102ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:102EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:102EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:102F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:102F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:102F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:102F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:102F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:102F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:102F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:102F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:102F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:102F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:102FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:102FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:102FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:102FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:102FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:102FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10300000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10301000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10302000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10303000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10304000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10305000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10306000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10307000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10308000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10309000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:1030A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1030B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1030C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1030D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1030E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1030F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10310000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10311000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10312000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10313000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10314000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10315000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10316000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10317000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10318000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10319000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:1031A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1031B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1031C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1031D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1031E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1031F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10320000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10321000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10322000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10323000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10324000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10325000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10326000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10327000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10328000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10329000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:1032A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1032B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1032C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1032D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1032E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1032F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10330000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10331000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10332000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10333000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10334000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10335000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10336000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10337000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10338000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10339000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:1033A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1033B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1033C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1033D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1033E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1033F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10340000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10341000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10342000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10343000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10344000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10345000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10346000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10347000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10348000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10349000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:1034A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1034B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1034C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1034D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1034E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1034F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10350000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10351000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10352000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10353000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10354000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10355000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10356000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10357000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10358000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10359000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:1035A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1035B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1035C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1035D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1035E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1035F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10360000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10361000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10362000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10363000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10364000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10365000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10366000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10367000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10368000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10369000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:1036A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1036B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1036C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1036D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1036E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1036F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10370000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10371000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10372000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10373000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10374000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10375000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10376000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10377000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10378000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10379000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:1037A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1037B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1037C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1037D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1037E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1037F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10380000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10381000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10382000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10383000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10384000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10385000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10386000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10387000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10388000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10389000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:1038A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1038B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1038C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1038D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1038E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1038F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10390000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10391000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10392000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10393000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10394000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10395000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10396000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10397000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10398000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10399000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:1039A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1039B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1039C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1039D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1039E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1039F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:103A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:103A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:103A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:103A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:103A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:103A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:103A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:103A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:103A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:103A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:103AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:103AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:103AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:103AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:103AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:103AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:103B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:103B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:103B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:103B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:103B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:103B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:103B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:103B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:103B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:103B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:103BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:103BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:103BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:103BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:103BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:103BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:103C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:103C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:103C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:103C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:103C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:103C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:103C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:103C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:103C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:103C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:103CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:103CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:103CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:103CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:103CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:103CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:103D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:103D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:103D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:103D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:103D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:103D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:103D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:103D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:103D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:103D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:103DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:103DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:103DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:103DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:103DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:103DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:103E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:103E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:103E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:103E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:103E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:103E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:103E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:103E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:103E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:103E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:103EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:103EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:103EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:103ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:103EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:103EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:103F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:103F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:103F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:103F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:103F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:103F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:103F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:103F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:103F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:103F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:103FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:103FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:103FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:103FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:103FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:103FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10400000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10401000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10402000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10403000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10404000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10405000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10406000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10407000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10408000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10409000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:1040A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1040B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1040C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1040D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1040E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1040F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10410000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10411000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10412000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10413000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10414000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10415000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10416000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10417000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10418000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10419000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:1041A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1041B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1041C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1041D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1041E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1041F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10420000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10421000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10422000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10423000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10424000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10425000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10426000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10427000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10428000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10429000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:1042A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1042B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1042C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1042D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1042E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1042F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10430000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10431000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10432000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10433000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10434000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10435000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10436000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10437000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10438000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10439000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:1043A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1043B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1043C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1043D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1043E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1043F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10440000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10441000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10442000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10443000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10444000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10445000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10446000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10447000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10448000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10449000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:1044A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1044B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1044C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1044D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1044E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1044F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10450000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10451000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10452000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10453000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10454000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10455000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10456000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10457000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10458000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10459000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:1045A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1045B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1045C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1045D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1045E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1045F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10460000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10461000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10462000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10463000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10464000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10465000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10466000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10467000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10468000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10469000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:1046A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1046B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1046C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1046D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1046E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1046F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10470000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10471000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10472000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10473000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10474000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10475000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10476000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10477000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10478000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10479000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:1047A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1047B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1047C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1047D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1047E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1047F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10480000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10481000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10482000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10483000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10484000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10485000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10486000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10487000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10488000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10489000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:1048A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1048B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1048C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1048D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1048E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1048F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10490000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10491000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10492000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10493000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10494000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10495000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10496000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10497000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10498000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10499000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:1049A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1049B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1049C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1049D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1049E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1049F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:104A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:104A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:104A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:104A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:104A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:104A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:104A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:104A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:104A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:104A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:104AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:104AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:104AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:104AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:104AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:104AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:104B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:104B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:104B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:104B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:104B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:104B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:104B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:104B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:104B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:104B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:104BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:104BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:104BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:104BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:104BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:104BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:104C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:104C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:104C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:104C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:104C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:104C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:104C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:104C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:104C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:104C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:104CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:104CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:104CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:104CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:104CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:104CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:104D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:104D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:104D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:104D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:104D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:104D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:104D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:104D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:104D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:104D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:104DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:104DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:104DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:104DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:104DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:104DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:104E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:104E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:104E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:104E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:104E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:104E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:104E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:104E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:104E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:104E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:104EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:104EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:104EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:104ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:104EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:104EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:104F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:104F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:104F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:104F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:104F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:104F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:104F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:104F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:104F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:104F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:104FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:104FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:104FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:104FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:104FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:104FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10500000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10501000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10502000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10503000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10504000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10505000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10506000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10507000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10508000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10509000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:1050A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1050B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1050C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1050D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1050E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1050F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10510000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10511000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10512000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10513000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10514000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10515000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10516000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10517000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10518000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10519000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:1051A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1051B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1051C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1051D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1051E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1051F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10520000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10521000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10522000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10523000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10524000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10525000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10526000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10527000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10528000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10529000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:1052A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1052B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1052C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1052D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1052E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1052F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10530000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10531000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10532000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10533000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10534000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10535000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10536000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10537000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10538000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10539000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:1053A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1053B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1053C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1053D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1053E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1053F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10540000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10541000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10542000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10543000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10544000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10545000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10546000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10547000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10548000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10549000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:1054A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1054B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1054C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1054D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1054E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1054F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10550000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10551000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10552000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10553000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10554000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10555000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10556000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10557000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10558000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10559000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:1055A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1055B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1055C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1055D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1055E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1055F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10560000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10561000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10562000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10563000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10564000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10565000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10566000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10567000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10568000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10569000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:1056A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1056B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1056C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1056D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1056E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1056F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10570000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10571000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10572000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10573000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10574000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10575000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10576000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10577000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10578000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10579000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:1057A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1057B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1057C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1057D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1057E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1057F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10580000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10581000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10582000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10583000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10584000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10585000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10586000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10587000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10588000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10589000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:1058A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1058B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1058C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1058D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1058E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1058F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10590000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10591000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10592000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10593000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10594000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10595000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10596000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10597000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10598000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10599000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:1059A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1059B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1059C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1059D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1059E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1059F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:105A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:105A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:105A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:105A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:105A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:105A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:105A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:105A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:105A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:105A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:105AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:105AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:105AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:105AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:105AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:105AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:105B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:105B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:105B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:105B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:105B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:105B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:105B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:105B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:105B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:105B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:105BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:105BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:105BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:105BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:105BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:105BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:105C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:105C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:105C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:105C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:105C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:105C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:105C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:105C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:105C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:105C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:105CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:105CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:105CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:105CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:105CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:105CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:105D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:105D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:105D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:105D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:105D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:105D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:105D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:105D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:105D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:105D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:105DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:105DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:105DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:105DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:105DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:105DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:105E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:105E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:105E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:105E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:105E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:105E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:105E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:105E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:105E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:105E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:105EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:105EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:105EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:105ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:105EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:105EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:105F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:105F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:105F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:105F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:105F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:105F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:105F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:105F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:105F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:105F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:105FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:105FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:105FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:105FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:105FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:105FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10600000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10601000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10602000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10603000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10604000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10605000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10606000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10607000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10608000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10609000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:1060A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1060B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1060C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1060D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1060E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1060F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10610000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10611000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10612000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10613000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10614000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10615000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10616000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10617000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10618000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10619000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:1061A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1061B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1061C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1061D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1061E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1061F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10620000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10621000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10622000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10623000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10624000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10625000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10626000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10627000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10628000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10629000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:1062A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1062B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1062C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1062D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1062E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1062F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10630000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10631000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10632000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10633000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10634000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10635000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10636000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10637000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10638000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10639000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:1063A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1063B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1063C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1063D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1063E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1063F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10640000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10641000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10642000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10643000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10644000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10645000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10646000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10647000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10648000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10649000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:1064A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1064B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1064C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1064D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1064E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1064F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10650000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10651000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10652000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10653000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10654000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10655000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10656000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10657000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10658000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10659000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:1065A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1065B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1065C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1065D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1065E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1065F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10660000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10661000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10662000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10663000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10664000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10665000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10666000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10667000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10668000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10669000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:1066A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1066B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1066C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1066D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1066E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1066F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10670000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10671000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10672000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10673000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10674000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10675000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10676000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10677000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10678000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10679000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:1067A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1067B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1067C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1067D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1067E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1067F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10680000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10681000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10682000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10683000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10684000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10685000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10686000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10687000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10688000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10689000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:1068A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1068B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1068C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1068D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1068E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1068F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10690000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10691000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10692000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10693000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10694000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10695000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10696000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10697000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10698000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10699000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:1069A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1069B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1069C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1069D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1069E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1069F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:106A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:106A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:106A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:106A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:106A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:106A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:106A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:106A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:106A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:106A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:106AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:106AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:106AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:106AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:106AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:106AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:106B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:106B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:106B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:106B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:106B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:106B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:106B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:106B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:106B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:106B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:106BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:106BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:106BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:106BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:106BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:106BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:106C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:106C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:106C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:106C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:106C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:106C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:106C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:106C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:106C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:106C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:106CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:106CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:106CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:106CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:106CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:106CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:106D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:106D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:106D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:106D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:106D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:106D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:106D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:106D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:106D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:106D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:106DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:106DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:106DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:106DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:106DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:106DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:106E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:106E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:106E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:106E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:106E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:106E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:106E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:106E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:106E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:106E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:106EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:106EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:106EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:106ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:106EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:106EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:106F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:106F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:106F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:106F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:106F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:106F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:106F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:106F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:106F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:106F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:106FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:106FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:106FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:106FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:106FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:106FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10700000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10701000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10702000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10703000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10704000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10705000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10706000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10707000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10708000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10709000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:1070A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1070B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1070C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1070D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1070E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1070F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10710000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10711000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10712000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10713000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10714000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10715000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10716000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10717000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10718000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10719000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:1071A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1071B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1071C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1071D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1071E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1071F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10720000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10721000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10722000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10723000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10724000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10725000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10726000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10727000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10728000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10729000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:1072A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1072B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1072C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1072D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1072E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1072F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10730000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10731000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10732000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10733000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10734000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10735000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10736000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10737000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10738000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10739000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:1073A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1073B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1073C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1073D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1073E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1073F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10740000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10741000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10742000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10743000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10744000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10745000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10746000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10747000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10748000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10749000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:1074A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1074B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1074C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1074D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1074E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1074F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10750000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10751000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10752000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10753000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10754000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10755000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10756000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10757000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10758000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10759000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:1075A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1075B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1075C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1075D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1075E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1075F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10760000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10761000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10762000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10763000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10764000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10765000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10766000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10767000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10768000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10769000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:1076A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1076B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1076C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1076D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1076E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1076F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10770000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10771000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10772000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10773000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10774000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10775000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10776000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10777000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10778000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10779000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:1077A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1077B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1077C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1077D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1077E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1077F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10780000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10781000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10782000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10783000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10784000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10785000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10786000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10787000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10788000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10789000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:1078A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1078B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1078C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1078D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1078E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1078F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10790000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10791000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10792000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10793000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10794000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10795000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10796000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10797000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10798000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10799000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:1079A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1079B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1079C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1079D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1079E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1079F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:107A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:107A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:107A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:107A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:107A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:107A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:107A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:107A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:107A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:107A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:107AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:107AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:107AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:107AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:107AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:107AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:107B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:107B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:107B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:107B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:107B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:107B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:107B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:107B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:107B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:107B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:107BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:107BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:107BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:107BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:107BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:107BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:107C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:107C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:107C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:107C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:107C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:107C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:107C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:107C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:107C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:107C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:107CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:107CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:107CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:107CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:107CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:107CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:107D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:107D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:107D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:107D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:107D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:107D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:107D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:107D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:107D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:107D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:107DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:107DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:107DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:107DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:107DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:107DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:107E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:107E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:107E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:107E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:107E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:107E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:107E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:107E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:107E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:107E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:107EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:107EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:107EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:107ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:107EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:107EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:107F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:107F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:107F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:107F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:107F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:107F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:107F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:107F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:107F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:107F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:107FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:107FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:107FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:107FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:107FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:107FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10800000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10801000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10802000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10803000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10804000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10805000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10806000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10807000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10808000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10809000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:1080A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1080B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1080C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1080D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1080E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:1080F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10810000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10811000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10812000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10813000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10814000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10815000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10816000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10817000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10818000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10819000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:1081A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1081B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1081C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1081D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1081E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:1081F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10820000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10821000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10822000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10823000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10824000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10825000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10826000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10827000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10828000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10829000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:1082A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1082B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1082C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1082D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1082E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:1082F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10830000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10831000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10832000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10833000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10834000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10835000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10836000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10837000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10838000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10839000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:1083A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1083B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1083C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1083D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1083E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:1083F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10840000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10841000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10842000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10843000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10844000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10845000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10846000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10847000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10848000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10849000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:1084A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1084B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1084C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1084D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1084E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:1084F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10850000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10851000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10852000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10853000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10854000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10855000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10856000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10857000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10858000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10859000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:1085A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1085B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1085C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1085D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1085E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:1085F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10860000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10861000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10862000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10863000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10864000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10865000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10866000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10867000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10868000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10869000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:1086A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1086B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1086C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1086D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1086E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:1086F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10870000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10871000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10872000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10873000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10874000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10875000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10876000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10877000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10878000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10879000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:1087A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1087B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1087C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1087D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1087E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:1087F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10880000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10881000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10882000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10883000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10884000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10885000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10886000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10887000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10888000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10889000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:1088A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1088B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1088C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1088D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1088E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:1088F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10890000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10891000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10892000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10893000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10894000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10895000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10896000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10897000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10898000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10899000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:1089A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1089B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1089C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1089D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1089E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:1089F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:108A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:108A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:108A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:108A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:108A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:108A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:108A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:108A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:108A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:108A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:108AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:108AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:108AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:108AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:108AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:108AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:108B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:108B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:108B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:108B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:108B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:108B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:108B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:108B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:108B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:108B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:108BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:108BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:108BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:108BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:108BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:108BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:108C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:108C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:108C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:108C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:108C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:108C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:108C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:108C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:108C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:108C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:108CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:108CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:108CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:108CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:108CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:108CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:108D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:108D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:108D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:108D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:108D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:108D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:108D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:108D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:108D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:108D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:108DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:108DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:108DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:108DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:108DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:108DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:108E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:108E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:108E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:108E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:108E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:108E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:108E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:108E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:108E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:108E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:108EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:108EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:108EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:108ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:108EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:108EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:108F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:108F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:108F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:108F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:108F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:108F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:108F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:108F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:108F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:108F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:108FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:108FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:108FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:108FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:108FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:108FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10900000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10901000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10902000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10903000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10904000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10905000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10906000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10907000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10908000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10909000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:1090A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:1090B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:1090C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:1090D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:1090E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:1090F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10910000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10911000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10912000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10913000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10914000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10915000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10916000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10917000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10918000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10919000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:1091A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:1091B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:1091C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:1091D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:1091E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:1091F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10920000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10921000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10922000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10923000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10924000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10925000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10926000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10927000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10928000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10929000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:1092A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:1092B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:1092C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:1092D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:1092E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:1092F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10930000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10931000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10932000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10933000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10934000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10935000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10936000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10937000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10938000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10939000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:1093A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:1093B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:1093C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:1093D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:1093E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:1093F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10940000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10941000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10942000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10943000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10944000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10945000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10946000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10947000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10948000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10949000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:1094A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:1094B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:1094C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:1094D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:1094E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:1094F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10950000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10951000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10952000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10953000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10954000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10955000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10956000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10957000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10958000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10959000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:1095A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:1095B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:1095C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:1095D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:1095E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:1095F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10960000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10961000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10962000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10963000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10964000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10965000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10966000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10967000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10968000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10969000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:1096A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:1096B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:1096C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:1096D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:1096E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:1096F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10970000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10971000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10972000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10973000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10974000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10975000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10976000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10977000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10978000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10979000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:1097A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:1097B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:1097C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:1097D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:1097E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:1097F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10980000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10981000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10982000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10983000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10984000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10985000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10986000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10987000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10988000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10989000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:1098A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:1098B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:1098C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:1098D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:1098E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:1098F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10990000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10991000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10992000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10993000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10994000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10995000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10996000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10997000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10998000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10999000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:1099A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:1099B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:1099C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:1099D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:1099E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:1099F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:109A0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:109A1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:109A2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:109A3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:109A4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:109A5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:109A6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:109A7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:109A8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:109A9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:109AA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:109AB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:109AC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:109AD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:109AE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:109AF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:109B0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:109B1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:109B2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:109B3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:109B4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:109B5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:109B6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:109B7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:109B8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:109B9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:109BA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:109BB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:109BC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:109BD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:109BE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:109BF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:109C0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:109C1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:109C2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:109C3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:109C4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:109C5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:109C6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:109C7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:109C8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:109C9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:109CA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:109CB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:109CC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:109CD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:109CE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:109CF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:109D0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:109D1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:109D2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:109D3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:109D4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:109D5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:109D6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:109D7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:109D8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:109D9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:109DA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:109DB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:109DC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:109DD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:109DE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:109DF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:109E0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:109E1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:109E2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:109E3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:109E4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:109E5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:109E6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:109E7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:109E8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:109E9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:109EA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:109EB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:109EC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:109ED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:109EE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:109EF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:109F0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:109F1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:109F2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:109F3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:109F4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:109F5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:109F6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:109F7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:109F8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:109F9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:109FA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:109FB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:109FC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:109FD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:109FE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:109FF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10A00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10A01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10A02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10A03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10A04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10A05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10A06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10A07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10A08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10A09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10A0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10A0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10A0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10A0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10A0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10A0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10A10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10A11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10A12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10A13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10A14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10A15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10A16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10A17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10A18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10A19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10A1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10A1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10A1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10A1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10A1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10A1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10A20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10A21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10A22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10A23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10A24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10A25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10A26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10A27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10A28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10A29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10A2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10A2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10A2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10A2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10A2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10A2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10A30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10A31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10A32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10A33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10A34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10A35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10A36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10A37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10A38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10A39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10A3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10A3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10A3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10A3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10A3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10A3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10A40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10A41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10A42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10A43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10A44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10A45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10A46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10A47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10A48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10A49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10A4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10A4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10A4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10A4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10A4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10A4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10A50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10A51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10A52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10A53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10A54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10A55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10A56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10A57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10A58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10A59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10A5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10A5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10A5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10A5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10A5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10A5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10A60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10A61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10A62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10A63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10A64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10A65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10A66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10A67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10A68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10A69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10A6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10A6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10A6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10A6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10A6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10A6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10A70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10A71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10A72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10A73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10A74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10A75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10A76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10A77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10A78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10A79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10A7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10A7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10A7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10A7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10A7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10A7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10A80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10A81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10A82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10A83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10A84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10A85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10A86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10A87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10A88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10A89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10A8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10A8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10A8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10A8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10A8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10A8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10A90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10A91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10A92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10A93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10A94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10A95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10A96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10A97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10A98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10A99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10A9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10A9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10A9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10A9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10A9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10A9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10AA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10AA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10AA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10AA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10AA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10AA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10AA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10AA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10AA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10AA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10AAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10AAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10AAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10AAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10AAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10AAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10AB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10AB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10AB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10AB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10AB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10AB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10AB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10AB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10AB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10AB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10ABA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10ABB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10ABC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10ABD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10ABE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10ABF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10AC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10AC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10AC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10AC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10AC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10AC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10AC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10AC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10AC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10AC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10ACA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10ACB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10ACC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10ACD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10ACE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10ACF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10AD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10AD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10AD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10AD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10AD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10AD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10AD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10AD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10AD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10AD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10ADA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10ADB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10ADC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10ADD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10ADE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10ADF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10AE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10AE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10AE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10AE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10AE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10AE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10AE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10AE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10AE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10AE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10AEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10AEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10AEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10AED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10AEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10AEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10AF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10AF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10AF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10AF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10AF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10AF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10AF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10AF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10AF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10AF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10AFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10AFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10AFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10AFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10AFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10AFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10B00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10B01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10B02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10B03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10B04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10B05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10B06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10B07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10B08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10B09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10B0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10B0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10B0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10B0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10B0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10B0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10B10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10B11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10B12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10B13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10B14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10B15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10B16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10B17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10B18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10B19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10B1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10B1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10B1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10B1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10B1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10B1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10B20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10B21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10B22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10B23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10B24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10B25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10B26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10B27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10B28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10B29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10B2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10B2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10B2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10B2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10B2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10B2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10B30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10B31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10B32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10B33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10B34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10B35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10B36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10B37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10B38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10B39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10B3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10B3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10B3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10B3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10B3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10B3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10B40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10B41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10B42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10B43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10B44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10B45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10B46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10B47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10B48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10B49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10B4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10B4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10B4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10B4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10B4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10B4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10B50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10B51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10B52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10B53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10B54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10B55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10B56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10B57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10B58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10B59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10B5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10B5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10B5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10B5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10B5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10B5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10B60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10B61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10B62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10B63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10B64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10B65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10B66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10B67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10B68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10B69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10B6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10B6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10B6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10B6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10B6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10B6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10B70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10B71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10B72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10B73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10B74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10B75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10B76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10B77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10B78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10B79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10B7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10B7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10B7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10B7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10B7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10B7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10B80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10B81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10B82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10B83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10B84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10B85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10B86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10B87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10B88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10B89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10B8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10B8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10B8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10B8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10B8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10B8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10B90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10B91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10B92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10B93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10B94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10B95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10B96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10B97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10B98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10B99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10B9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10B9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10B9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10B9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10B9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10B9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10BA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10BA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10BA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10BA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10BA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10BA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10BA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10BA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10BA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10BA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10BAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10BAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10BAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10BAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10BAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10BAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10BB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10BB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10BB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10BB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10BB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10BB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10BB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10BB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10BB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10BB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10BBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10BBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10BBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10BBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10BBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10BBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10BC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10BC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10BC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10BC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10BC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10BC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10BC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10BC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10BC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10BC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10BCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10BCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10BCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10BCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10BCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10BCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10BD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10BD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10BD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10BD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10BD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10BD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10BD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10BD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10BD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10BD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10BDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10BDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10BDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10BDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10BDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10BDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10BE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10BE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10BE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10BE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10BE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10BE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10BE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10BE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10BE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10BE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10BEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10BEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10BEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10BED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10BEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10BEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10BF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10BF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10BF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10BF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10BF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10BF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10BF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10BF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10BF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10BF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10BFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10BFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10BFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10BFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10BFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10BFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10C00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10C01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10C02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10C03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10C04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10C05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10C06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10C07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10C08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10C09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10C0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10C0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10C0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10C0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10C0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10C0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10C10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10C11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10C12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10C13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10C14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10C15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10C16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10C17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10C18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10C19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10C1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10C1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10C1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10C1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10C1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10C1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10C20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10C21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10C22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10C23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10C24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10C25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10C26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10C27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10C28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10C29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10C2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10C2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10C2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10C2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10C2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10C2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10C30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10C31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10C32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10C33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10C34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10C35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10C36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10C37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10C38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10C39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10C3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10C3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10C3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10C3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10C3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10C3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10C40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10C41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10C42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10C43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10C44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10C45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10C46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10C47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10C48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10C49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10C4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10C4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10C4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10C4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10C4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10C4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10C50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10C51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10C52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10C53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10C54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10C55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10C56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10C57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10C58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10C59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10C5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10C5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10C5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10C5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10C5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10C5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10C60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10C61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10C62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10C63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10C64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10C65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10C66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10C67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10C68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10C69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10C6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10C6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10C6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10C6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10C6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10C6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10C70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10C71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10C72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10C73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10C74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10C75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10C76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10C77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10C78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10C79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10C7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10C7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10C7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10C7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10C7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10C7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10C80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10C81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10C82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10C83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10C84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10C85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10C86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10C87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10C88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10C89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10C8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10C8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10C8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10C8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10C8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10C8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10C90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10C91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10C92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10C93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10C94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10C95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10C96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10C97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10C98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10C99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10C9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10C9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10C9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10C9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10C9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10C9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10CA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10CA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10CA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10CA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10CA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10CA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10CA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10CA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10CA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10CA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10CAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10CAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10CAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10CAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10CAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10CAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10CB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10CB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10CB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10CB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10CB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10CB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10CB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10CB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10CB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10CB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10CBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10CBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10CBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10CBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10CBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10CBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10CC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10CC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10CC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10CC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10CC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10CC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10CC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10CC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10CC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10CC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10CCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10CCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10CCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10CCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10CCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10CCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10CD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10CD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10CD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10CD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10CD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10CD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10CD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10CD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10CD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10CD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10CDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10CDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10CDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10CDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10CDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10CDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10CE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10CE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10CE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10CE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10CE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10CE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10CE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10CE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10CE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10CE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10CEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10CEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10CEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10CED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10CEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10CEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10CF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10CF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10CF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10CF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10CF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10CF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10CF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10CF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10CF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10CF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10CFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10CFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10CFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10CFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10CFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10CFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10D00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10D01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10D02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10D03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10D04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10D05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10D06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10D07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10D08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10D09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10D0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10D0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10D0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10D0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10D0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10D0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10D10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10D11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10D12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10D13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10D14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10D15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10D16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10D17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10D18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10D19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10D1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10D1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10D1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10D1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10D1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10D1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10D20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10D21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10D22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10D23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10D24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10D25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10D26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10D27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10D28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10D29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10D2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10D2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10D2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10D2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10D2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10D2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10D30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10D31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10D32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10D33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10D34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10D35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10D36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10D37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10D38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10D39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10D3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10D3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10D3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10D3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10D3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10D3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10D40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10D41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10D42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10D43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10D44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10D45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10D46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10D47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10D48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10D49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10D4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10D4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10D4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10D4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10D4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10D4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10D50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10D51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10D52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10D53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10D54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10D55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10D56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10D57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10D58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10D59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10D5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10D5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10D5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10D5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10D5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10D5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10D60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10D61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10D62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10D63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10D64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10D65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10D66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10D67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10D68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10D69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10D6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10D6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10D6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10D6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10D6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10D6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10D70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10D71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10D72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10D73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10D74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10D75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10D76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10D77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10D78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10D79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10D7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10D7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10D7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10D7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10D7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10D7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10D80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10D81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10D82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10D83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10D84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10D85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10D86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10D87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10D88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10D89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10D8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10D8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10D8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10D8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10D8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10D8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10D90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10D91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10D92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10D93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10D94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10D95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10D96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10D97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10D98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10D99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10D9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10D9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10D9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10D9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10D9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10D9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10DA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10DA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10DA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10DA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10DA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10DA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10DA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10DA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10DA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10DA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10DAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10DAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10DAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10DAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10DAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10DAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10DB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10DB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10DB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10DB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10DB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10DB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10DB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10DB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10DB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10DB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10DBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10DBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10DBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10DBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10DBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10DBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10DC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10DC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10DC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10DC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10DC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10DC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10DC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10DC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10DC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10DC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10DCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10DCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10DCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10DCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10DCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10DCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10DD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10DD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10DD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10DD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10DD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10DD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10DD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10DD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10DD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10DD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10DDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10DDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10DDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10DDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10DDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10DDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10DE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10DE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10DE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10DE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10DE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10DE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10DE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10DE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10DE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10DE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10DEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10DEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10DEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10DED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10DEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10DEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10DF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10DF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10DF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10DF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10DF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10DF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10DF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10DF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10DF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10DF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10DFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10DFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10DFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10DFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10DFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10DFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10E00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10E01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10E02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10E03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10E04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10E05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10E06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10E07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10E08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10E09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10E0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10E0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10E0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10E0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10E0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10E0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10E10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10E11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10E12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10E13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10E14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10E15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10E16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10E17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10E18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10E19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10E1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10E1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10E1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10E1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10E1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10E1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10E20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10E21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10E22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10E23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10E24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10E25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10E26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10E27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10E28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10E29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10E2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10E2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10E2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10E2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10E2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10E2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10E30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10E31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10E32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10E33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10E34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10E35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10E36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10E37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10E38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10E39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10E3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10E3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10E3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10E3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10E3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10E3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10E40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10E41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10E42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10E43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10E44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10E45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10E46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10E47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10E48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10E49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10E4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10E4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10E4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10E4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10E4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10E4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10E50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10E51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10E52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10E53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10E54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10E55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10E56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10E57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10E58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10E59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10E5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10E5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10E5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10E5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10E5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10E5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10E60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10E61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10E62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10E63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10E64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10E65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10E66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10E67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10E68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10E69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10E6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10E6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10E6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10E6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10E6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10E6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10E70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10E71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10E72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10E73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10E74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10E75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10E76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10E77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10E78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10E79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10E7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10E7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10E7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10E7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10E7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10E7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10E80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10E81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10E82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10E83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10E84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10E85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10E86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10E87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10E88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10E89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10E8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10E8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10E8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10E8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10E8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10E8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10E90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10E91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10E92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10E93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10E94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10E95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10E96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10E97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10E98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10E99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10E9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10E9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10E9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10E9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10E9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10E9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10EA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10EA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10EA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10EA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10EA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10EA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10EA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10EA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10EA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10EA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10EAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10EAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10EAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10EAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10EAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10EAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10EB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10EB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10EB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10EB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10EB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10EB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10EB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10EB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10EB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10EB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10EBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10EBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10EBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10EBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10EBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10EBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10EC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10EC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10EC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10EC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10EC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10EC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10EC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10EC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10EC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10EC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10ECA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10ECB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10ECC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10ECD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10ECE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10ECF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10ED0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10ED1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10ED2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10ED3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10ED4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10ED5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10ED6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10ED7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10ED8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10ED9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10EDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10EDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10EDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10EDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10EDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10EDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10EE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10EE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10EE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10EE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10EE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10EE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10EE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10EE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10EE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10EE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10EEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10EEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10EEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10EED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10EEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10EEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10EF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:10EF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10EF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10EF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10EF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10EF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10EF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10EF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10EF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10EF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10EFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10EFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10EFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10EFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10EFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10EFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10F00000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF10 -:10F01000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00 -:10F02000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0 -:10F03000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE0 -:10F04000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD0 -:10F05000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC0 -:10F06000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB0 -:10F07000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA0 -:10F08000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF90 -:10F09000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF80 -:10F0A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF70 -:10F0B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF60 -:10F0C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF50 -:10F0D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF40 -:10F0E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF30 -:10F0F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF20 -:10F10000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0F -:10F11000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF -:10F12000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEF -:10F13000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDF -:10F14000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCF -:10F15000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBF -:10F16000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAF -:10F17000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9F -:10F18000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8F -:10F19000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7F -:10F1A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6F -:10F1B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5F -:10F1C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4F -:10F1D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3F -:10F1E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F -:10F1F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1F -:10F20000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0E -:10F21000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE -:10F22000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEE -:10F23000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDE -:10F24000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCE -:10F25000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBE -:10F26000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAE -:10F27000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9E -:10F28000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8E -:10F29000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E -:10F2A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6E -:10F2B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5E -:10F2C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4E -:10F2D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3E -:10F2E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2E -:10F2F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1E -:10F30000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0D -:10F31000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD -:10F32000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFED -:10F33000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDD -:10F34000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCD -:10F35000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBD -:10F36000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAD -:10F37000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9D -:10F38000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8D -:10F39000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7D -:10F3A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6D -:10F3B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D -:10F3C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4D -:10F3D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3D -:10F3E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2D -:10F3F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1D -:10F40000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0C -:10F41000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC -:10F42000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEC -:10F43000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDC -:10F44000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCC -:10F45000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBC -:10F46000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAC -:10F47000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9C -:10F48000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8C -:10F49000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7C -:10F4A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C -:10F4B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5C -:10F4C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4C -:10F4D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3C -:10F4E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2C -:10F4F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1C -:10F50000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0B -:10F51000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB -:10F52000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEB -:10F53000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDB -:10F54000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCB -:10F55000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBB -:10F56000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAB -:10F57000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9B -:10F58000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8B -:10F59000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7B -:10F5A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6B -:10F5B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5B -:10F5C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4B -:10F5D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3B -:10F5E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2B -:10F5F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1B -:10F60000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0A -:10F61000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA -:10F62000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEA -:10F63000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFDA -:10F64000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFCA -:10F65000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBA -:10F66000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFAA -:10F67000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9A -:10F68000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8A -:10F69000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7A -:10F6A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6A -:10F6B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5A -:10F6C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4A -:10F6D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3A -:10F6E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2A -:10F6F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1A -:10F70000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF09 -:10F71000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9 -:10F72000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE9 -:10F73000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD9 -:10F74000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC9 -:10F75000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB9 -:10F76000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA9 -:10F77000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF99 -:10F78000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF89 -:10F79000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF79 -:10F7A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF69 -:10F7B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF59 -:10F7C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF49 -:10F7D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF39 -:10F7E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF29 -:10F7F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF19 -:10F80000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF08 -:10F81000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8 -:10F82000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE8 -:10F83000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD8 -:10F84000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC8 -:10F85000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB8 -:10F86000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA8 -:10F87000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF98 -:10F88000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF88 -:10F89000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF78 -:10F8A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF68 -:10F8B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF58 -:10F8C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF48 -:10F8D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF38 -:10F8E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF28 -:10F8F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF18 -:10F90000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF07 -:10F91000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7 -:10F92000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE7 -:10F93000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD7 -:10F94000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7 -:10F95000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB7 -:10F96000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA7 -:10F97000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF97 -:10F98000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF87 -:10F99000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF77 -:10F9A000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF67 -:10F9B000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF57 -:10F9C000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF47 -:10F9D000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF37 -:10F9E000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF27 -:10F9F000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF17 -:10FA0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF06 -:10FA1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6 -:10FA2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE6 -:10FA3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD6 -:10FA4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC6 -:10FA5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB6 -:10FA6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA6 -:10FA7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF96 -:10FA8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF86 -:10FA9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF76 -:10FAA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF66 -:10FAB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF56 -:10FAC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF46 -:10FAD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF36 -:10FAE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF26 -:10FAF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16 -:10FB0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF05 -:10FB1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5 -:10FB2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE5 -:10FB3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD5 -:10FB4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC5 -:10FB5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB5 -:10FB6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA5 -:10FB7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF95 -:10FB8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF85 -:10FB9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF75 -:10FBA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF65 -:10FBB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF55 -:10FBC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF45 -:10FBD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF35 -:10FBE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF25 -:10FBF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF15 -:10FC0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF04 -:10FC1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF4 -:10FC2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE4 -:10FC3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD4 -:10FC4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC4 -:10FC5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB4 -:10FC6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA4 -:10FC7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF94 -:10FC8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF84 -:10FC9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF74 -:10FCA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF64 -:10FCB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF54 -:10FCC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF44 -:10FCD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF34 -:10FCE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF24 -:10FCF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF14 -:10FD0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF03 -:10FD1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF3 -:10FD2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE3 -:10FD3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD3 -:10FD4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC3 -:10FD5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB3 -:10FD6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA3 -:10FD7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF93 -:10FD8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF83 -:10FD9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF73 -:10FDA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF63 -:10FDB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF53 -:10FDC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF43 -:10FDD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF33 -:10FDE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF23 -:10FDF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF13 -:10FE0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02 -:10FE1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2 -:10FE2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE2 -:10FE3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD2 -:10FE4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC2 -:10FE5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB2 -:10FE6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA2 -:10FE7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF92 -:10FE8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF82 -:10FE9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF72 -:10FEA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF62 -:10FEB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF52 -:10FEC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF42 -:10FED000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF32 -:10FEE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF22 -:10FEF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF12 -:10FF0000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF01 -:10FF1000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF1 -:10FF2000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE1 -:10FF3000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD1 -:10FF4000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC1 -:10FF5000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFB1 -:10FF6000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA1 -:10FF7000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF91 -:10FF8000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF81 -:10FF9000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF71 -:10FFA000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF61 -:10FFB000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF51 -:10FFC000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF41 -:10FFD000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF31 -:10FFE000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF21 -:10FFF000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF11 -:00000001FF diff --git a/Binaries/Bootloader_Combined/readme.md b/Binaries/Bootloader_Combined/readme.md deleted file mode 100644 index 01088e63..00000000 --- a/Binaries/Bootloader_Combined/readme.md +++ /dev/null @@ -1,30 +0,0 @@ -Programming ATtiny84x in Circuit -=========================================================== - -The Qwiic Twist uses an ATtiny84A that must be programmed with firmware. This is done with spring pins aka Pogo Pins and an AVR programmer of your choice. We've had success with using an [Arduino as the ISP programmer](https://www.arduino.cc/en/Tutorial/BuiltInExamples/ArduinoISP) but we use the [Tiny AVR Programmer](https://www.sparkfun.com/products/11801) for day to day work. - -1) Get the Tiny AVR Programmer and the [necessary drivers](https://learn.sparkfun.com/tutorials/tiny-avr-programmer-hookup-guide#driver-installation) installed. -2) Clear the VCC solder jumper from the Tiny AVR Programmer. This jumper is closed by default and will power the target at 5V. While 5V will not harm the ATtiny targets, but many Qwiic boards have 3.3V sensitive sensors and peripherals so it's best to power the target through a Qwiic cable. -3) Solder 6-pin [female header](https://www.sparkfun.com/products/115) to the bottom of the programmer -4) Assemble the [SparkFun ISP Pogo Adapter](https://www.sparkfun.com/products/11591) -5) Attach the pogo adapter to the bottom of the programmer. - -![Programming ISP via Pogo Pins on ATtiny84A](https://github.com/sparkfun/Qwiic_Twist/blob/master/Programming/Images/ATtiny%20Pogo%20ISP%20-%202.jpg?raw=true) - - -6) Identify and mark pin 1 on the bottom of the adapter. You'll need to know where pin 1 is to align it on the target ISP holes. - -![Programming ISP via Pogo Pins on ATtiny84A](https://github.com/sparkfun/Qwiic_Twist/blob/master/Programming/Images/ATtiny%20Pogo%20ISP%20-%203.jpg?raw=true) - -7) Attach Tiny AVR Programmer to a USB extension cable to allow free movement of the programmer. Confirm that avrdude can see the programmer. -8) Connect the target device to a development board using a Qwiic cable so that it is powered. -9) Download the contents of the 'Programming' folder so that you have **avrdude.exe**, **avrdude.conf**, and **program_qwiic_twist.bat** in a folder. -9) Open a command line, navigate to 'program_qwiic_twist.bat' and run. It should fail because no ATtiny was detected. If it failed because an attinyusb was not found then you may need to troubleshoot your Tiny AVR Programmer and/or edit the batch file to match your programmer type. -10) Press and hold the pogo pins against the 6 exposed pins on the target. Your mark on the pogo pins should match with the small white silkscreen on the ISP on the target. You may need to wiggle the Pogo adapter until you hear all 6 pins seat into the ISP footprint. - -![Programming ISP via Pogo Pins on ATtiny84A](https://raw.githubusercontent.com/sparkfun/Qwiic_Twist/master/Programming/Images/ATtiny%20Pogo%20ISP%20-%201.jpg) - - -11) Press enter with your free hand to begin programming. -![Programming ISP via Pogo Pins on ATtiny84A](https://github.com/sparkfun/Qwiic_Twist/blob/master/Programming/Images/avrdude%20output.jpg?raw=true) - diff --git a/Binaries/LoRaSerial_Firmware_v10.bin b/Binaries/LoRaSerial_Firmware_v10.bin deleted file mode 100644 index 5a17479f399b629c3d71eac8bb850b9fb52c7bfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75504 zcmb5X34Bvk+CP5o&DuRsmUJac3u!|kGzAJ3NfRt>(gF%9OTj=9N~;(aH5HTysJP5n zs6c5^5p)C{MBhFANBuyO}Uas;&s$r6ZflxKX2PF^$)gF@;}7Uv`=69 z@4SO$=$HPfy5zNbH!&*wG6ylTK4~@SBX2W$y-U??kq`~dbJ)fZsYBI8j1r%uCZ0pp zeWZ$}%|ITS&gd9Nw#zm1zEYwY^dBTdW9mw(OR9ajagk(EMkmQ#Aa%an$Sm#j+MErx z=gAi5drs?86RpcaD#;{<1iCz&c)e|flav^JBWI2FjK2SSMdZ_Q zMA3LC=WXSa)~sGKZlq7pj&wh6tFWnRZgrvyI5C|N8r0{2AW~{5!B#qw?RCN=KUuLgS z_Abr#WZ&O|vaXr&KC{T{>XMQ)GJ>3IEW(7nW|j2_0ZusA_>KBX_D`G;I3u{agIy06 zN{J#bxq2#LR((T+z&`}#tcC@zS>-;DPSEy{aq(r*{Rr8`Fs9n=ihJ zNu`7YyHxrI>})}%OQyTuF_>n0);P`-Zwq8fRC# zM(HzMsTKFxD_m@a(Z!fET}qwX!DzExN&0*3nXY^7vWiUCJ+qR{W|xwm99)ewwOh$Q z5M1TZ66z}n8r+Ol$rs^E@LdyJnaHCFE_VHB5p*`nTT5)ye$!z8mv#m%?;j~ zh+~8E5^*)ba@4QjLqfS&e`W9{v1J9%3nnq;Yw@e(%Yw>6Qc&Swrm5WueuGe;CM5yC zLfyUKv5_IG;Z$8ozzKpe&8OBR@mHdHx5SmDw>i}nlC`$A5{KF; z=ck8M-L&52*zb>NRb9u3q*~7ZI(Wa1b!2-~-F+EE^AqY;bt(9Ph@w{CcqCiy$l2Cz zmGvt4A0is36#2i!-a1nCNg^*TnKbsM|B59Ti?UYZPURgjy4p)~>DtbYNbQ+!1`yA4wEc|= zqUnS4b*0tnNx4~#{2xbBh)2tkV=|(V^Yg=U;%v?!sr-)-mZT69nL<+d=5Q+D#MkY_ zJn&r)9hPEOpBBc|F|9^YA=cuG5S`JH%uf~MglVVck6%;%X24`#)QA@I-OGt0cVAA5 z$%8yMN7z=$b`yMaP(BwBY>ClU*>=gE6yAhZ*{Gay47A+rf~~_9Rfqb8V^b0nOK4Qh}UVOb4K$f*;QStZArE7sJ5sa7MrDV zh{M8YEvhWbDT&(V@oHTvK*Eu#FR-qkC)PHdkb-sjd#xEKg}|4>1l0Ow>;QR`tWsZA zCpW4anKGsGyf@qVHC<_qM1&YmrNv6|Ef08db&aUi5E=h+km!bi;tX@#V9s_AbCBYZ zzD#>w<4`xNAA(`*u}O;f8|amnvJDI z^8nm`*ZHb$2Z`7DOfu2zgZsboEJ6RB;uw+~j3(PH6sPzUjv3}0(0sX1guILdPHM5(2x0VfIXNojJ>$lKY^Q^h$EeONYAmc;&?sf24@?M)Z5UL^qP8 zI8v*Rd9z$8b&_oKSv6?A3WWX-p|I<|mGfCq9WMDDIWZSVuY+7XcGX z@l~O%r9uk!-gW5L7OUih5O`Z)G@!c%#!ob1PQmwvrH&lnkaJJATYo>!gR-ZZ?4No(q^U2Hf_7)eaf2J`>M90KE=+M=HBe_J_=ZT zBEn4E=4JHXal}Y`rfd33=Wsvfn)X?ugTpFHD^7VktQO3iYbJ%2U{|I?M(E5c{R|mW zt>9md77-P`FGhpij4|RT1!=R=XB&WnhKILWf9iQvYu@%KnV;+NnmomenX>S_B;Fo* z-aEnbHDD$KaJID^urLwxm1v*N)rSIGEd>59(DnMNgyL%&{y_|1xEj7BhOa}I&e02k zJb$<^-H~253YapN7`DA=Rh@oSi?y?#tACVa&SOj-ZxWvpVam37&Ez}qEvJNFH!((h z22Yry_>uO3;z~LXo4QyctzizP@ehQUgF@gzL2;O%Keb}}Lf|{GkM~^dBh9ZWQu^g$ zndM@cm13FXqM)Cx`4QutiL&wX+YYA!`%Me$2x$e^RwF?>_K*ba)CtPNbllcNJ70oVNjqUMPVwo9MTGxy-_w}40->*w z&d-Ftzw2%yG=J8aUAi9lF8Oyk8X9&v^nD16H4Q}#yRfHeu4Lre0pDl-UnAEU{V9^s zcP5dKMcPod&3erL2+|&Luzm5b{*gy!Z>y_lGyog1{0#!ry6*`x`iNHud=@3TO0NN! zX-O!}B=s@Rq)u9w|4ii-@6u)(Ya39Sh@Xt6^eM|tV|#%=l<>B4(->Bjw)? z(w&X*S}V+Q3oR$*j|YcdttrPX2b{m!CunJn3!@_4811Z~pjcauv!l(L?ql0Q>jorylK7ff5`S;Ra}~zjBIRF;(q37tato^w0?Q)LwaBpddcswX5_O(uaCL6**|0h{$0`~nS~XTk zDOeD$i#eN<`HNv8@KJ0U!Z*a?6k7!X*e( z+AIWq8=HV|VN3`}BFX&QsPRB-9Aa`~OuGWv=G=8Bjc2QOPLV3F-D~& z)>nwsDKSbnhd3G<^o}81iX+UWI)eC~zmSZ$Q4R&C#5dm|;plw+J~A6HPVv^)fL%hu z)+8l9By5w*==VNr3c{4`Q2G+p5R#-Ngz|Wl*IS-OGy=-W$1!9ioDpssTpe5k+r79*EN4eNhwmm&_P5Z6QK? zq6@3DN>nEdSeeD4w9$s*vExjoWs}OMUU$94I-{!EUOQ{fP4jNK5qY;n>8~d`r#7pk zQYNzjbqUg=k1Nnciy#j{XOpc9^CkVXUjv6JX&4d&sA5SgF6HFCERJaQ*ga- zpTT_!cO1?S_fNQw;XZ^r2KP6(_u-Dhy#x0)TsPcb;QkEvC%7(dU!xHCT~yWA*BItB z8-tv4;}ve-Mo^)`hO84CRa$1E5cp$M1-XHlBn0jha>fzgI&d7)vc64cxP4E2$w{>P znuNd`(Qo1FYYK5D@Is$(6UqiS=dXPnt93Si$Ca1;#7!ytfty;!a}{OZa@Uo8%~{GW zag}8kIn?yzo7{}DbKDJOXSk}eFS(gzrxQ5cf_0RR$6CVug!M|ocQ{;)i6Ks`bw2zB za1-FB!8za-!YzZl3vMIaui!f1UWEHS+*@$R;CkTBz+Hw5!o}busCO8g2CfM1dN{u8 z)>#i7HZ_96`cnBXg7ZLqP3AoJLEznwgXfw|PD$H=CeoUfD`{uilK5spntKpDrXvLY z3Ebm?NJRUnjBQQg$AYI!;{OyT1@hb%tZAn;kfq8)EaFDRCLrfaVbb{`IdLOX_8nJI zJ9^`}M(3}e$ug-|Y}DtyxN(yD#HK4*4`(s$N&Ic$hqEO3E(+_(1oOjLYWF=zaW*IM zHzB6n?8(Y^8|@~w$E)&?@k$@jpHcr~BZGLv7=wP5r-%^j-4Da0PzW3jYiAw2nVFbc z67&z^6q7FxkZO!bv@G&{8OcHDmIx`NW6yEF42o#L9@rikfp~Q!-SHC10rv6D7Xn@2 z41~b^*jJniX~_{%kb$o}Vp`F2T{b3vegk z{ss35Tn`-f{{ZFlM?^kO;3vw?ScCp z+%dSX;A9x9t1Gn@A{0pn<#{lcT*H#g2^#nxilyolHPmMp%%qr5&54w6jk!SEx247B zj?UY)<(FPl{`RZoi?1pF(SMa^bbObfCj}$P3*JN4%wE~zr>YiPWH&1dS5z$=ve0sc zS)^j7O|_V9-*``XpQ<7yKlmB!UmiLi7vw?o-7Oe~`BEHlo*~0Wv~vCjft1L7CXGbf zXjAf+V@!F}uYs0A&i6ytLuGBc_Q9?~N8zlzMl-p=Tu__i21++(`J}uptV_rvN&MSU zCYKay2+LnTkmtyAnE~gNDkt~KbaYN6{7>M*qC<40#36YYW=FC$JM))q4`v!B`DGB^RIHiF9k3wb{dnyCI z-=j`Cc2$?m!9cqu7^J?zKFN?q^(X&RS^)cx>RaOaKIq#*%mG103v{ia`o;FX49ruOEucjg+)$LI|U$x%VHMB0X*60!W z3qP%~KT7mp)t^-}<{@jUjUj(3YD`Ul&&17X^htIGfhI^&}!yJ z{rW-tIl@PIs>efvbQJtTn3-rs+=C%i$xpz4e;c4&)qNq&MjPT-Y^ydhK005`RnO#>61u||Bke80ybXE>r1ZymQ4v=D86kR^{fs)P zWzRguOrb>xd>UKVO<{n-!hccpHdb<11ze z{xn}fF&WqCZPlDStmORzQnH|Wp@X;=+85R+U&(O~btkz=tA;TA(U=y(2MfLI&9%*q zDqRE4353!EPU8msy5t5b)U|j}w!x@aO%^C(pukDDK)!Kzi>pIwjiX9zGbwrqu zq^@~DD82EX*&T`<*~?lX&mF3NZr+U@9m{U&IJs;tjcsU8#FpxWQkr@*Vmli4CSs>1 zVsE8+m(TB5xO~5U5xG=& z#fVJT;Ddz&YCH{UlEk&r}%QQ9_Iwgc`pyf0*%q-&^GP~hd7GY z{}h%IB_9kM6Y{FeHx#))68CytPzdCKCwK(k4jBK3lF=xsg9WHg{i)n>W%68 zP&lA+#~tAR9)k;ZWmil1tzjkq*RV%RG&N|gC+rzR3Wm?xL`<$J$gkv&g@@KTZ)VC= zW3Tw3-~)9c#^JsU^yCp}t09*K)8KzlAO)%T-Y*yu9FaVz5;>x`LR5cE+o$`7?jza{ z2il(x7w3bd1ROE+Bs+py=#iEDa#1flsQWkrY)n`oa2dE{ZcOQDZ}dQ7J}<-q>oEhq zSS;|HNG$MXghl&z0khKjH8@Lhbxo~3a#+x{y(Hl}p{s zb?3dM8#}BL-@1(qaF5QPt~XlLgf-^*SfpK6qWJ#;W|u8Zu7qmzT?|G9PPtSwTJ_8VgoXsn4^9eQNSErz5MXQc!l%K$0Uxlj|bm zzV%DXn-!NoVatt*i=RqO7r4RoT`=?)To&jmsVkXP+L?->?rhg_ zMcO9^FkaS2A+KaWkJ+H3FLKlH?E`0Y=NhTTLqc^Z&||QE7I0Y?0*5j?r?H$cZ4$mO z%5-|jJM$6Z`(nk=Dowx{mW1CQAqKU7*eo;Pgz?1!qoW2l)%*poZNl~SXGE1!_$mKUMU8i_eFA?4rdtoc9cWOXWyyhhcP;%>7sQ!{N_Y$+~@ z*pqQ0A3*y;+h%oVH3~6bCdQNEk|7Vp>NxX*69OauqYbf)hQaAeGR|wZ*dDiK6O}&G z_M2Rq3ciA{V@E$g;RA^>*zUj3jYc?jAz6-r1>ZvM_R!Sj7MJ6Se3#*9in@O=4?mK zM}?uH?vU0?^SZjq>I{&8$AKrmGEgd>gBSV3T0`S8(7`OgvCcZdKPWB~$GwuK%xl@A3br^V?bKqK65|Q#VRD(5{2}02`4F)9;`D0k4k(*Z*)mC#eFY0a^2^>WGoBJ z+7b?S%lO(@So@sgQ8KD=pW|;iQt%dEJgMNnjZISj2`4Fgv7W?u3DU^kusr{Qze>Xt z2qx$_g1a2KSl7kTaV{!r{8>1`{R)%{qPYLqS)$8!J&Bf{vZnZ71&1yKT7pkFay>%8 zMtS<6RMU_1fhU8k<_zZ%ORYmoSfZZ%Gmk8%?Dg}_}=MstXh z;%sv>_mYN=VjRkbgWq$I1wyiX%o29byTQ}kB5^OveY?aKa)N~}h2{zFmPFr(&&kR7 z!_lyIF6J%2@g~Q^;=G;4yxkNlQUA`W>V-@arrPj2?ndiY;)HoLS6q3>yZWnrTSeU|T#avpDNUxZPGu&MwjfOP))aSB{)C=q zQF^-!`G3F{>o*`RkdOnmQ(hLlg3?gu&%(%TLa(E@wo2qQ_2|#jk9}^*jwQq#NH65yz~Cx%>V6(WVy_>Loqe;_<~%CF?>g=F3y)i^g_kQfU6Z=n7kW2B%9 zy8%3>pe3&YJ{E?&iMq~32J89)XZOEDUGuN0YZB@zKwUy0Df0ORDNZ+Egf32Iu=w{n z;^QsAcnc$)Xyrtxy*57LFQZESE{x-b@GiuE6r#QF$JlO1Tt#>X;@%C>oGd>Au?rDv z!id=j8N+C%l2%7+O}@LeHf39D-7c%*^sDtuDF)y zfSG8gSRGHD7o5#0WHi$3;VBW!Gji7i!DSWqc7ZCeXRdx5i2D5J{NbV3i4U9 zM(@HLMrD1oMd;7-MPZjEj?`MUpPtgSB}+O0RC|eOcqbH z_gUE!vB2(_x}RuHa%06EO*KYzKYtNbVn1AtFq)6KF=7viU)s;~1Am8D!yG@+W?)8g zf|-dKd46CJ`koyix^&D))-^K{h$i#>0~%)x=Rr!1w4d%(x-Tz57yS*~_i)t4g6af4 zTgO$7W(k=NM|C6A9swe@s3W0v2WP;OQQjgA>DTW6N5E(N9XbH2tNRG$j^>~8GRBvB zWrm}E_cbv7TKFEo?TX;t2^haKqU58H;*JN`B7R|zLi4(y681K+z_H+J#LWrDcf<1n z_CI3Z0yo+kj>Gs$$4bEXO2GKa{{_YgU_2H$A0^ra)eOH`Sdy5rl&do)$BZq+jCq2v zw1It(3Eb-v%vh~BV_D)#jE>0or@}Xa7py#gV**28cRs#W%4nKq;dzG`3w@$4UBt*~ zD6t_t(=juFk*g9InE{>CL}p-IA3-}d6=S&=NzqI_Um=zUB@@e+o|p0U;R+G<90poO zj{4??HO|S1JK>E598rDaM7t8$lQFXXA&oWGAd-58`32LMl$cOB&Q z*C0$`@-cj`(0ZX)rn&+d|26P|1UiF@utYmjHK;3~wmLHYTUZ4s`1wc~jOV*{AC3jw zp=jVrU)pW)JpN9!7LbF|74tNKr*{;y@@cj&5OXyjy#yG_<9ZP)M zxQlL6xdLbLTVPGo6qYKwhNyE#jg1Aa1fSrRqj&pa4fuUJ){Nhd*k%rDZS0iiKv^dY z9N<;}&)kLjqJbYGdx4X0bEvdAI7?;9qJbd-)eX^ox{|9$zT?ryxzR=q+RgM|c2^l? zzFTq9JQJgMB61s7t(#XPJ($e@GhD5c9;&NO=06C3&&`3YK??5%z9haILC>`iNLugSOiP}&o#ju=87Eie{BS|o!|T* zHyV0`@3{iU6!W*7-Z9mD85~!I`68!rTxag*@*O9h9C5Y9&n_tVo=_$}?ZER*kcB8hL%kg~^ za8JG=eNqyZQ66d;@H5?KM}TKV`ya$nnRX@ez6N`^cFqe-yOTQ%j&BF|s$>4fqZeLr zv~N6effT%a&ALsD(RKUPh2NkDCpP{5!t;*yCw_O~fTO)h%01^e(Zq1mpdp%uSzmRb zh|o3j3VUm%2enwKQP9^yKH_s?l0!@@GzT$iSDVDYBS^{M3eCcd%nc=zapVSOH}fp> zBGb+MmH7vAk=dosVfI>ceY^CF@Kxj6iSIDq8S$$^_!Ppa_~x*CtxCkxuN2=!_!6J? zLW^UsbIJuni>W*fu#kn8hJ`Gon#>^!$e+j&@&ODWPr#I29i#uj>Tqm<&oEZ!+cj3> z`#a?Jc*$i+=6= zp}!%20wC{r|4o3WT-T+32DWD(^)vch%Pc_F&zzpjwkKD#!IJKs{u)R9!@d30j#H>d z;2H{H^)2Y3n?U{}$V~ zY}5I)+I`4zS0aZ*%&`D9B`0bMQylJ}lPL2_tZ&)v=i}=r!#WVt6Ba^#WpQ<-fbPop z%!o8!>Z86%5qfegN35LR7RAvLPS~y>MUFGx@3}!b{;(s^MKu~_{)c9jiJFk{prZ2t zp+?Z_C-5EPV?Y&$fYR55_gV8!M+4o#7Lm^x>5prhqk+$ndJpoZ`_|XpZ8vUX%6#>4 z8;dUo5E5l0qvte7`JbpM8+y1T-^w_@gqF;7Pxw0f0^KX2`!m6Ej`#n_(U#8! zqk*l##bQndIe&w;eh4jbusCU6;^?sE`vrr{NAE(!t#Ac<>ohmL);3$;Vw=UZZLy~MrTn;P1!SBsY!}%vyJcXKeR56awvQ9?W2Wad&I~>NDV%*SYw56F^bh8yHgrV; zCBSRH2{U>fxEP{0yQKWj5k0u)1`a$XR+0FYaCxL)c8>i$Z|^fEd1 zjA3kZtVcnMPI*wN zL*jIsSbnLffB1DwLsZ@L+>@q(>&6+KoL?NJu{gUjz*a;r(ayKY`A=bO9rdelPOs`> z?l{_!CRwywi5)|DHqDb!0WmyTL6J^xw9E?POPMSuqL_D_KeO z`L(XncW#$@=B~+HGj}bcWAbjYzPs{+m05O1`|b*sxJ!q)M%t~;k@k1tHp8(?5r*3g z_bwc}Y@~flgLT;myJY!h{BBwP?y})_$?a^zaQl|qtqsHM@4{J^;|piKo$~XZt@(s_ z1i^!~oN|DWqUQDG)|nEb7FQoiqUGmT(Ur?4`)JEeC;=#@y|9UX z=)dB9YF2fr^W!O4+q7=xaI|MU*dVK4f-aPr)wtJnlWk zd;O%q?9q@3HhKrG!TlQcJQ<(kn?^{-<(v-dh~6UDBng4z0y7cZ3DVR2q$I^R4(aQV zF7G8;ntoI;xtK{mVZX&|T@Fwb0^RsOvn!P_J4hRY(?6WQ2VNI4yG>mg1w_vjK6Lnw z8G2VTVcg74@{|yGNzk}uhfRR%;h?;V;*1jtW7>%hYszt)ED3^Mq01sSchIzZg;Cw~E>1p4etDET z<>ef=jIeE~_%hpv5lNe3oauyO{J0ZuS;wN?LxP}FYBR|{bC^j}D_^r@+D<_i*>20V zNNwl6I>PLsSnn82m|?*pghwj~+xbVb#74tL;YiM5YidtC&sIUw=|sQ3_fwl-+Ve1{ z(hP8Pr5M}pb+2@Z`MhYh-f#p1!j${woHozebAN8S1lqcOPfu! zr~)(r9~6$jxmAlf&n3xwXXVM2#pp#F?2jgZRtkY%$6RQ`)ztzhm6GA0o=LuB^3GjC z;E9+%ubA}SMRPrjTok?uKO)S4f7Clw-DAbG^0ZtjNv$#D%K4R`X6e3U(O(C@(pQvA zHLn3scE%qm>tE*TP8HV%cOXfUjgU<<=$z+a&U4%&b7AT5a<-e!d3tX8p+(*#H`|u> zvSfSCwxL=`@BDVqPMv=W>LmPUpsTt53Gh>hBG9E#?JVJ!qFt4rLNDQu!k_GC2`h&F z0gglBmx%sb!*sSICORTd6QiqK4cpAvz9#U-@TuTqv;xj7@fBkfF_~!BTLmnRin|^^ ztZYr%PWUx352*R9Gs?Y@GEH+5Paspj=uqPBTMe15e8K)3#}wf6U*SY>gs;EpfH*Us zAT+`Eoalc8exr}oO%iMWeQddIs_6S|>|B$kSqA&r&qD3?c89!Gv0cf3h+6BEMdV3) zt3%E&i^%ddJ{jy@-w$oKZ+9qoeI$toPhL=6C~F-}{C&R?+t`aXKJz^;`ZvSxzuI0) zY?JP<+{5;V9rAX?9wpx?_S$WCi@h!dhm|7MzcaMSz6tgJH~^cu&{!gCRrjs6Z%mZ( zh^3nB&0?vM=vBPbeW7OidXzeWQhyD}T4xmgforzA5~XezOD(o97E472r1>(jt=mGY z?5iAq=ZKE>sS$HIN6dMXeXf|38;~LAPg+^~qcS<)9_itnc4wl_3NdH3y;{uKbG6Qi zp#}B@j%N~e&PSc2#hlaZ*NZv3Q77@O?xQUd-Hk{eA*N5XPZHB#K}}2i@l-of6=Lc* z`#3T689?HkM5+O)!3bTY(e^QpBCP$BNWI>t+&f+yDWjI*Pee^x8KJ&MqL$IX@tw-{ zoW@0s?K73nCUr(8%l1(HOsGlOPWVNFLM!N59oq(6xjluHlHv)kqvXp`GWu%#u3$wm z8Ly@B&ql9})e(~z`&2Xqus)Vp3V*`63zcgkhraKrTQ+Ap@!UFt?!mY`G|JL;Zq`@M z=%(p}d~72Y$&8ObqJIUJn#K?RJnYtorkl;Lvj+8JbFXd_qhugmP zvZIAUg`V9B%Cd{-lEm>3gEeG4Mn|Nra&tU339-_>vJR>5^*>kC$pHV^6*z}LBS?6hK=P$=z;*DfV;eUCR@`^ku`Q)<)O za>bgZojy)c=+PPJ&5WJY?n=;0JJ>d|gV`k|p^8n3p1zHE+9S42+DUswM?r0z|G?4N z0q?k7UbN^0;ZNb*iQ>1?h>Un(Eg+1Mc0MqT!WX+6G;=3z==qIAsvC`d?u^K7Bl?Vl zh@oFcBsw~Gi#K(@y^)+b<`UE{J7{zuaXg4yEF(+#e z0@jPf6_Uc<34QptbdByCj>d(tR>;4LF6%sIY*p+lP#9o!rUeP`M#lTsy z?Q2tK-6{pdCF81;rv6oGn5a$AQN+*Tr#k7IQM2}&NUt4prNzU&K{jN!p zpM|r1g)Uo5Ew9!LJYTNh_XKe+iQGSm_j%>~gP?ejk#n`6&62Pz*O2S#9xO3`;OX*y zj*OG~Wje|SF#P91THg?Hc54O{{Mz8#+9WybucNYDGOo|BZ`2Tqr%rLuqs-XUl}=#h z%J0N_$7g~S*GEbu=2DU;)^h6r-9>bbkP#3pCo<5?&%%^ue%mL5T~A}|UQipVt-^U} z*g^JIt*pA+ez&7V30rwlr@Omvoqe6CuY!G0BF7!}JH;Gl#2j6HE9`e9a!gI+SZ-e~ z=J1I*e$%%MVXE`OI|PXwi|qAcj<*x75Z&TL4s{~OTu>a^kKc(ow)Nd&za^1Fk;q}U zJH#AMi#b~Qh;DWw$CXfgEcD#~TE`P&jwa;5`JdR2{zQ%`_Niiy4aiXs-I*nk{%In8 zg1uBsZ$|noq)!1=(O?|!hvK|Ik-ZqaKz*?QD9`mh-PEpUaMS_Z}wI6!v zLE17%a|US&r6rV(TuVQ$r5lt^iCC|d!pyP(>C?cdBr=g5<1|k7KMPz0Sz$LYq%IC? z+VE6hHleh$UTQ1w9t6f>e9QH+HsJ3u3SiZIEim0KvP%f0hCvT#$z4(#=0`U{gjMJ& z^{Ki7uhd4`3cNJ0!aP|_`6)C=3n;M4U}z%gu-{BrX0U8njJ&*kur7{g#=kgo`{2U zf%Q`SVqEJbuR^aAV~2?P#=_;4u2EV5DXR-ICMe`_$Wpi+tt0S%41dJrX>!e^e2WrR zsW-&l@lx#g3S{Q^I0RUoF??rG<oz~7r9n9t19ke!f&NWpsRo8Wt(CqjG4M5<>Xc}USpJpABl6--6& zu8uwy2*rCS-qeog$NdRWnva+$&bDejpgnh?>?nF?DM6XYj)+NSE)dgp1ZB8oP2b1> z?JY3k9`UK>_L+2NpB4^VQ+!6kltn>RrRO`Jkc+swvX(N?ZQ zD?xER>CJID_z&W{yDw5k^b(yxoM|JaBSfJq5<>+D$q_0RLwUjJQibiXm$2nFiDcTR zGaYIh?(^dAz-|!(>2PYe1n22iDmMhd<>2iMC4Vzwu1Cy2gM)S;3h_M*50qso>MWu{ zo}B~oTv9UBr^J0IqANr0cOzyy{k{bYfrkgEz1tY=yWV|Gu|QNXZahI?dqdqkHnhJGs;S&1#j+YTG? z%##)?)kBVi9nO!C42Ph$d-0CPwf&xge*cM+^D^W)gFH&)xjpg&r#O9-li}r1jY*7WSVxP<0mXMp9B#&bV;ViJOV3`HuT-hh-G?kyVC?M3+@)7)`%He zM6^FcV6+sEpTPd+yBO_5hiM=ESd%zE10KrKZ8RA zA&`Tdco_lI40+mEGC|C-SG-BecV&>0=>y(4B{^z@-DuHxf4seY*teuag?0aBc4>c8 z#J+Bf$wdi(QUrHN#tE6W9Gvf{0YR@B#4LIiRIaOP$15|-C(!Yi#C}PUnTwONh}72Or74e-$lG?(r%_|GDTTuTu1V5tWFm z7KKKPuAUC75CV71keoX-(8PWxLL6QQ65+=gW%GyO)=|XV4xLggV2{xa}LyPNEcOwuI>D>UM`JkTC!wSeE&vy>Ov8LXgl za(7hrtim=8H#_4Jp_*cgtEJDPbfH*!DN3JLUR!#!_6u%k;hlg7rZ7&uL49(mWh*h< z3=1?%J+*Ao4X$4fEpV1-PK4M(KU$ZZo7qaKD)gt!$L#gVFxJz3C?^g+jQmxYjO8}3C_ z1w`D^92rY@J~W*X(s8_{5bLJg6@iurngaAriV339EQ9-CkZ4V%A|;Lm4n}Z>Cu)>mkH+Df@>;Yn z8G!D{#Dh&t+X#ZQzX5Q=I%ah&ut)Sg2;Wl!xO|KHuVC(Eeu@h}im-sZSoffmN2Okx z<6p?}qv#tL@EC^qsjMgjUIiAq=)X>c+7BQhpA|K(^rVXzKF3Ya=HSq2j9nI4t@nF#_`$?CC(q zY(mF+5_c$)TeAt}&B5&q;3h+>LZ=Z!9}OsUS}=7|G3WaO1*`_AZ(j%7^+~OZNU$Df zwjl+dij$JvfYR0=)y$H1IbVoa2C+L3`%qBcq3DFZ6wvx4q_bN?DUe;-RcX)%{w=8B zzljmuW5~BgglsAg%J~~YQj*+GbNmrGK1Yr%$l<)2L&1Lp8BB^C3jSlHx{-RLm`e4M z3cg#k7LM0d5n-S)DAkKt&P@CKFVyLc>7ZIv@RP8^==jor)$QPlXW^{)c2Vk-^DIWe z$C%utb{T&O{&uvq3?sM_1ZOX{{iOJ!7;Q(v4?#_f(2ukr!^;LL8tLm}pEr^YWhcGS zwQnX}v0Z)F5voCo1zyElXwatv`X0jWUFN58?P2=6Ka8^$yio@G*w8C@x$SZfUF8cg z86fhWzRg(8KZ5HbzO`aq2bu|VXBCYT$Z6atQ+6IiI}KuO51<9od78vqq%Fbr+fX8= zHHa|IvfboF44$cGzS(yy65Z7+V%uiZ;L+4gn9JPen z+frIjF}{q*cHovewAD8QpMZY|Xv2|79>Xo5KZ8`bKKzS}f5YihYSQ-IREasxTWku) z`8sGajcNW+Lf?ly)HanU%sLUzV+J8#8$r2p?l6GbQCdDE7S~6Q2cCkadRJs}u%eV? zh&bkcyfEiTQP`^{@m_%Ct#=^KhP)=;64q zG@rxR=(|U<&MYD`Dj>T=z-18~<)}KtGV$bX{D71k2M&qP3q94si#@<(S`m}&3QY?u%RiGwbEIB1~nMhihHp*MXYfV8Ng<@JhYK;X9kh&S{vLr#l_eTaHu@6?VX@HaE z;x3Cr;=FK@2#E`0GM)`eI7}r&73K)&dk$Lk7*;`}E5MRxXI~*8H_n|=EI23h3tZfI+M=b9@hTvLrmYL<8wu8% zwnS^nf{h0mlf99}>BMNd=G)2c$RzL)BDYl!odyd&g2qmTWE8AO730wY;$t=2`*mbTBN9A0M=^-+#n>N{x7?zQ33pgb}LmYY# zXUo-A+=CLijhkQqw+@?y5y3%a+-6DMWZ-}oC=O0Bb{%JxduDx^-1N{O_vfFAHq<7P?2T2;5A zYs-CPJ1{A*TZba~)9-#%ZCPlYTX}Tt+CP##Ob30bMbUjq5&ZOMBV+IMwmE;~U^HSu zDcDSRD5Rerg^&1_{n0h^6y6*@+Q=f;ac6+a|Hy~cLu(INJZajKYb2$6)?{OrXdg+^H1g(TNW$=^rhqzW;yN;!z{M)$9MnwGw!qao?Xw0d?;)ff^;S5d1SI)zeZ0|%?Du5 zAof`t7t`WtWF{oxSh6@wwS!n6*gZ%RWdKoYX#R~;;$xIJ=nN3pjngq=T@kN#B)dE6 zS(m+~qu%Oz?7oir&8`nNbkx7=N=6L3#*P^4n#T~cdCdpN`R$=Afvw7VI_jT4EUFVv)vHY5LXt}|5Xt}|5Xt}|5(8ASrkmqVU zD0{UX+ONTO-0^nSV=Z&^fv$9SoqH`AIrKK$t(Gv}Ji3ca9lFr=g!?{Ak9Q^bbQ(G) zhGxUMXX?ycFRJ`X4kT+J1p}o4VIa% zqpkq7Aw-`}wl+R`xWC?CzshoF)q#{;;D(NuM&zNg-9SylA`>oy`s=o>MV(%LG9&WoXOh~HXuw`DZG$H;n1I=($*jb#MBO#52P^+P)spZDHok#!#P z+AM5)4}MeHzrk;VMc(eP%(d7oiuSuK0%9cX8!QqR(~cACXJ}rVB^l0HWwSIRw<3{S zi8x8S%_T!VTGtXwm1TuR**?p13w%lKRY<+p@(AilMlDM$4OL4lcU$hT$UB!>=vpRq zF0@EGPr4{4d!%YTV3gX+;5E*cEsX_CZWc}r^?g{?N(*~1OPuq7U;Zisi9rao$4XkL zbvL1>J0;p)`S0*D!PGsA_hWrJLzf`}>!n)|Lv%2VR-0VuGu?ZlVxJQ}H^?S%0Nv+@|6~7WikL4R&xkm|HS=raam^%TFgy%6A7Oo^dYhH?VKq!) zj)q-bxugRxdAwi$54?bTw`Hs?zr-!%%hj55dxO0x{KMbhS)XoS5NrHB&Pnrwu% zt)uXN8V*~ipq8P>aPR+*pa=uv~7bsSg!lVjTdQGcY~Wg*Y-SpDxf z@Q8b84^qSXR-ait6@+W3+EKZG#km!f&*r-nL{4cm`s|vVM=H=~+9y{>Q- zL#skVmqWCMV!+p64e^nT!AK-+YbIAn63^IaVYD~{w_j_MaBfOZ*r{ODnm8E4_>^_+>yNw6d=9^tFOr^X zpfb2_MZ*d@PCc|eF0-mM%eBx2JsW>t&liwB^IrsZ)XgaxFy7NNW^)$EktX0JfuW!=|Kf=4>Fz7Lkly8%1@D;H{?4^ zSU*J@!5fXzgDPmz4#!kFRxEjc=p~V2im->ifM$X=Zw4n_tu^5ueP>LiUsI;XzxhBd z2wiyHLrPz`DoOU!eEN`P_0rX)i4~=zrT?CR1e%9oA{UT)BA;Y8nkIeLGyy$Olh|!f;}l7;^^l=&Q= z&U5%Z2fl;C*awa2jdhJrKrVaqu*ytY6>YNZ((RJ%q!TXF-odv)x7pyz(}-lfhem(5o8%>L;CwXBkLcf3g0>FX_yAL*kU; z*XESrcZ)OSjiXMHw(4xTC-04Oj|G6X7R*qb$HHj~){w%q$|UCl;D(v9bN5nPWqL;p zFf(?HPo=**_p42dvQK#r0Mndh|LfqxiAV$oVqM}{~i3GwJW<{eD72Ra|U zvXrekzS2`V#M!nPFT8&{32-Z`rI37CklLj+)bjrm)OMt*o$}k*A@4jyMB3r%B1IW~ zsUEfEiN5(_ICLL^p#e@TT1FW}rVdu@DNY$N_% z2q!3HB5x{vx2Z9;7XJ!^6Y|mL$+!zoWu5k!f?mHi#xeT+Et$3hYhFO;T+1npOVus5 zf%4a(Es86&_Yt=ve@|r%&h<}6@%|=W?~o9Bes^GP10*NbXTv+w`_?l28v_UK(=0u= zmgt>J_3J21P&?#T2k7gb3_m$WbEo=k`R$brtClq$@m|huvnqP1#96p>-+iubTF>_! zOAg$(Z|y?MxwX1=?bh#ow=cyz@%qj8b>KB{$%g+&-n)lKRh|E%YtNp^OfnO41?G~= zTynoaAlw6FXYa`-GeZyqC?udsK%HRGfRzTRGF(&ws6f<`44?(HDrz-0)~eA|sx4yM zJqc7GiJb^!+FIJNDhVWH&S&jO5c~a}=XcKYoc~UqXEJ;CUVE)~y=$%aeb;+i^b`uZ zbBmCKIz?Tlk=SxV2&T=V(9nh7+n;3ex3vX{|aD#`h|2vT3 zd|G6ztL+v1d2YX`;QOUiC*s+Hdq?a44E3K!{o{IFVq&gUnub=GG_Ua>cbB1^M?!Yn zL2)wi+n0!<_?+-@Al4a+x7Uxp{TkkW4{t|x=Lt~eWRSdHNl}Yh0|`Y3MZf)Gz>`5T zXGFXbm`uJ1FvIkGpcr!SL9%o1#hH&>9>Odq!Qx=sC!#>^)R@L?DI#sEqzE8!LT*xz zMaN-UW5iZt*Pd8jSG0W2VRe_Erj>h(^QKd^TyoKS#TV^r*{Z^owmxgw)o}W z@?3f!E%zg59p-#+7qql~i9$evJ8*qoqOB|C)}M7H)-0=8QcYV-sDjTN{JIm@BzG)?TZK|3|Vb#bsG8??oT6Hc}uP>+?Q>x5|Q?%QZ&{QEA0#I)g!K5 zHK%Ie``hFa6}euyMki_{zFmGnG{JLCiLAGmuF-22Z}-%B>u7%;3xr|Lk;`s@H~Uz- zOVBRyZ47*m#WgY(D@EF-v6i+|gmI#-Rh8I*tB7N8iVH7Q6{s~0ObB6xC;wo2_etc=0ZQ4Ge`Qk<1yw*)*}VT-+1 zCCXl4xtTt{wL~m-#Ya#)Kr%%?=9i^S)=i{9S~E|P}3XR#a-eA zf*fMf!>)3o*tb<&r#ZyO_deu0C{~Gg*^`AD*jgKItcHXitcFAUV(B3Nx1Rf42l;ye zxv~kgGq@in;aj$w+LLEDwy1cCVW`fsQ8^}Of54c|-;(r*kea~+bel*0nr zw)IvxD_pU#bgP2MfrWaHLUXT@7L^giw$R_+z^nW~&kKP{vBdS2p4R$gz>YddPxA%T zK>hr3N%u~0Pm)I0#P$Hm*(Yrb6r;ttJ+txusor@G+xR`=YI|ZX>1n+%&2q{(g`DFw zHIbjcL~ixRDyTDL8PMnI{8TZibcsJ+^Kngfx|O2h}zejz>&d2DN9y(*jFNlRmo0bi@_D-4uU$n~H#Rj_xE zUWeaz+-Q$d7FDoB#v^T86)YN}BdH30d{g-;L%Nn|+NqpZRyxhICl9b>)7w}f-~<0&RO-xnBEu3`#o#McHj-ZJ^Q8UNxkP+g zT*F$4Y5Z!tu_S?XJF4uJB!!sRJ)%i#6B5%ZqCYRMwT~}3VoWG$2G4n2Am%C9DJ#yq z#45fSeMV2@+4t~OmuOPa@llWikPIk1EY2^)3`ir^-^+3U=F$koI~}1gY_&3Mv@G&f zUH!oMA>i*H`M^n`|6uf;FJxPI82(mx4Tk3?9WOTcN{G%IT3QvHE7ujScgEGJ@>~gz zxDsc@(f3|)9!LFekI-*bFbyk}uDaJpUUI(dLOyHk^6BpvM_zRP0?!jipC2AM=6nIq ze?0p9*&Fb{9)Dx4Q23O=og*F_olV5rtqMc(wY;*zGwS~EtOF$B((7exu)zg4)`kl4 z+H^*$kPlDBt@&|yu>E;Z5UcdY@i30{6!#ejo%KrWSt+(d$k#{tI&&T#4#Q!tJx}H! zp@Hv)D1Rf}_gut&A!0}m{t5l; z(|dm$Vr)jPTki$G4GnxEL&S8422zk0VQ}a+aGvja7g7Gx8ydJgMCTH*I)VLVgB^&b z=~U5iQwDpkmpduFDIc8^r|B*XZ97MQe|DW>Ye88Vur+IxW_%>yBW649O~SqJsj?F1 zYmQN6jQ@3`b2@tSO1Qi{nmb| zGo0njcG35gLC^JcX9nv2sm$eJUmqITmV4KN@jsiZ0KXpF7YdX5i~l|Q+6a&D**|(o zSF_UmfpqMH4Rtf`p8xf|ogbI9zc)Yci}YWvy)CY|sr`cqKl<(6#q*cZzmGu^WSR2A z(5bx~nRb8iif;x#O1(Thp7b#Hk91vDQ0^ZZ2n{(Q7w)?HK8+#gA&Z#DnkW< zV3+qbVii21$81i=PCO#gznfnvou(z`UC_%V@+U~YGwkO7)0?S>XJwwFyUKWkZ!B@} zRmONzxTMEEg^ZQ{n=e6Fg3($ zVEbmQBUZrfGWR=)OrGnz5H)_z8WqXCCV9*p%ksrOK<1Bux8kNG7TMX;iP`)F#~HK9 z=O!!ph%*Ws10%wfm?sTy2A<}hKbwhM9ap+j2(i5m?NJd(Hl;2rhI4Su1}9(t2$CyP z$V&8omF75PTI|7WzqP*@D1zKXX&jx4O8EM{7D&Jj>%9Pz3G2Ff3;)TeTuJ919X|(g zX$;Unv92;$COJ*D9oD7=bZlci#00(2v7A{n!l(E@y=T}bUOcnF6Sl4;8dsMf(@i9I*~+S9vFS$md8srNaf1l z<{>L~C;8n`gko++QEK@G{c%302O0f)lNXv)R?LAoQr~VWKh6h7dssAl1zB#UPdA*O zh@2`1`N&=^8DBQRfym#%1ZN`X%Hp9*0WFCyOTfLX!B}S;?ukR)m@|J5Xco>Wv~ng@ z0^o(bh$pj${uCf&vGB&+P-sL=b*(X;5WW|mAUAYVWAp>;b8bPKlXS;)t1C1VwgTwk znf-d}gfw0#cB!#KlWs?#Q__7>Y~GKgBG+7;SVZzO5kYIW&Ozp*i9_^i7OvulVy!b> zvmD9QpWf49`K@_|%Rwp^<7Cz4q3JIRhQ-F}LRX=)(xb3VZAcIb>mO+7j%0F&B*vNn z-7k~=0{1{2vE&L5)KCk|(?itTX^KQLtuC|UplEWxOWxy?ebsbDR6nse$wiu*7pwCO zy^KvW_ZU%Ie$I`jyH zo>tZ)TolT_*7_aJ6;N6S0<87BfWnGQP1dgi)NcpJ7f}5tmxyl(`p=d4zM*C6{36(X(n(dBRxDQmazP*X0 ztauC-Q(YM|dmr*}XICSC%G6+>U%%1KOzmI%3BFSnD+_~I#}p0<18sW@j=?sPhFt9K zqdYk~*sfWmx#a~?Soe<=M&zN3aD9agL0_#XtYL8C9L|>guRe?f2w`zv`YS@l zroJu45wq~;h}|H;?SqQo-v>tmx^iNBg1;hts{fn*2(RjjbZz7REk!yo*Zst;ER3!G zI-oqUn;-7Fh+2txE=$b65*WhayJ0@nW0PPdyxi!{+J%@h2HBk`ouTdF+D)(F8Vmh) zLK$P-A;vor7Cz}-?N+(H;}SS#|4w(Un|W^kxH;TCVzC=%Nc!55rRFg{s;kOyExpYB z8hOm5F1uH#7f5u6ex!XK_Ms8vLV(s8>nDDi7jfA#f)%SwQA^75Np1pv=3KYebdl|)y~Clzbd;v$u1aIi z$xW?UZfY%QZAy6_=SfehOIK?KRHb5>dsR9B1PyV&WBAAwh_DCOqn~@otKVddwTbkrL|Cr;EVoYw!zef>AN# zH>%LXN)Sk*W=RMgG;{!x$O9n6umYOox9P@2DH$;V98yBm?u_x8bD7c=Vsf6Lh% zM~<_sy2G5XmPD8orYKgRTH4*!>OyU`$C9u%Qoaj#YKIXgVpt2i6Rr$v_cQeBd0cfW z=#|NpL@FC9T1n1Ow*i#L;4+dGkDQETvWKLLmXoB}Q*1d`zvPiH9qO>*Y;-1}W^X;g zdG@PEaTD!|cCPkx!Nx>sRf%m0tl%R7+7BrE$=DpDIG}w`;m~LF%R;})2)_^BABuCk zj1pJBpu)J98^S7?;3RDc;JT4CaYp+E@-FsBP{X%b-$(N>p=L4?!TT?)?AdIz>oO{Pg<(a zj?rT~Dbd@4ZJM;@LYy-JJd&Aal9-I?D zHcaE#8OO*Qf?0^ISE6o`&G=T;1lteQ49on6$o1k>`ZPgMJBayHeIjbsNL9C~9NkQn zUcYvU`z-ACdJcYxlrPZoR$drv%Ui^fR(urhJM12EB8yNHR-dJ9Lt7!`m^)aAP3)hhsr~%~t*H&{TzB z+#cmTUlZjtdKg>O!$v%zd79NO%}p7b>le`aFtBn_4bX(bycScH=5AYxQI(pmRw zY?3=(mBU$uj%l6#+p2RzOGY!^>qCLWP%ud zf?RcVUOd4r!A;k#;Y-P-bN9z|bsHx?_@V61)$Xb@6MEa*fZVH#~j#O8vwL-37t5&*iMfPqc zSdM&%9j#{ggM-(AfBe7g$N4(+;IzVGbng=As4Cn|YpbHF;1{v4miDp&{X4LhLJFD4 zZE+T~HSv=rvy=W$z3G37d`5t(*~!x{1VV??v! zC7~nz5SL3>L3=CqLde1s=c@FKaiW6uNL!*6IXP|Pt;pTHUO2Z)3NP({ij{ z*t>NQ_@+FLu1%&%gIsykn=3R>ET2WUHq>EFQ|)IHxp&pN%!$HWp(9gm{ZNswEOgJ~ z(uL$|#yiEGz!{0h=2avJE=bpy^EkWHxD(^xIlr0uvM^J}u8ceBjFk7E(+E#_7osUc z1e_~0un;;R201)q+dYuWJn&D3ubs1sCHtt4%%buNa(wY3R|HNTCppoy&iapWqUnBe zeEDwS_~J5`1AM=-fws56WkOC-l_Wy$d_&e+eHyZ37om4EIL%3L65G?FGSi;2rZnc9 z5;~2_q|@AQK+%}FY_>vz7jO>aQxu+WO(2&8$b*p?nX5QE!&yb1tAt%C_g3keWjUff z8i^q2f#u+k0(*?fj;Lx^HO{{bnQi&bxSd)dRha0RNQO33pOs^+bGKjJOucgS=FEll z!Z9(!mAUZ5ZOA8WuNAG;>CW_pUcIfFNi(~Q`Xz2FY2mDp&SGs3ZB7t2KadFC*~&-4 z;+=vvem!h~#*v=89*whm6t+1#6NDm9xFvWavbh)q##+I@1=_3ZgKUfs0^DT3oFtAA zir+tA%s<12Y7O4Oez$=>$CxC6&rp8zWBg9Ubv?d+gwJ>vq9oia+$xf|Fj_E32~LYU z-W^v>?~!ap544QP4@iGeu5)Q3G1Qglt5={0QO&ba*! z@X>Z@{j@#WK5dV}S8T3dkkEFosBoWKuP$8bHb6dle!&EA0ELCrk>IU}G)0bo`MO5F zPJrY9L9`C{kYkSSL>Sv@c-K+hK+ndiOoC^un|fek%Tld}kD8ewW;M;n>!uk&S?o_K zM~H1#SYpwF`TAABN6XODtaz)UkgZoZ6tHdn5nf$Az}@qJ6PY<16hvqs=AUP4d=^9@ zsjU+n6H)5j>x?+8+U zE4K<7WT#_<=B4%R1U>FZJJD)fw1Qc$-{m%xACvjju4=v0!+7*l-RcZYVOez*b718g z0;rC;Ksc~;XPpW@(tB#^)5evR)f`y)yr3kpW;1=_)-LSQD>-Q1tQ>0=c2@6nd$b;Q zXZ4CrkeJth99TY2meHU34E@|Q2iL_UNclfwQ+}o*aSb@#R!FG#08CnpB`u0?7qAQ3U@(S%iotKH@61l1vx^q`qQuByUF;@+50A-QF{S-6)i6}>EMm9HF zhO-)7Vp+ z=eL{JUZ_KkX3UB+uw0OI#yf2)Ej_;B`_g|#>Ay+ldoQg0JN6$TOF^69o=|RdZ+C0C zDh58=whH~z57~sK4cpzkeDAdWBll7Feq}Bf9|=FZ>aqJ>`WE+o?S8hUT7Vou-`5mY zF-a>kGg)D4qw}SoC2WnMnJQTZ(=>hJMtbAC*Zu>5p3~Q{c6qukwX^vn7-T z`7VJ3<=#<$+Kr(Lb-(1JBopR9=fo$9lJN{y)AswS^ED~wCENu)Y@u71F4}*5^izY%y5g%((L+hi_te) zbcVhP0QTA=YsX+e4G8`g7-$Pwwz&m&6lZd4rdD95(6vcPy45*nnDjI2Z^kuOw_mwm zMyCv z_4EYPf%d3+mi#MVU2VOWW(OnITHrHW%ex~PaxK$lFh%zy(^@pr^!jUC!nO4H=$qe; z&bs3xlq3Ijz(@4h%YA+FQ^-(25f$#MVIoY3Hb zyp>R|wB@+taHgTol2D)HPH5aM#5oeSYL`>3g?6#Wrmc=^c%+EZ83rda*0=u_w5S6g zFePp|_~=}GR^UT*2NB@GU%y8PrCYMB*MJx~cXhqSvTP3^RBO%G{su9^Yz z86)i5u66_esa9M9*t0B#h6V*a8qB^T1Y8O;T z)QB~o)O_)iPiiApd(4vyQ|dW#M?D*C2t8i|{s4=^1IQqq%&ldfuYFg)&mCW;nLY-G z+PtrNwtF43cI7r97CIJ%uy*MY>?;$vTJW7n#~;>7duW*a~_vZ4IKUJhs|RReXZ5dcDGSVSRYq>f27W zV!v9pl4)cb^$NqS)%)DB<(k51&SPm*WD8vSKHO>4HnRJw8FwYKOFv=LbYcAZXEv>i z7(N3%pB}m(%n)Mmz{k11o$^cyLzr(SVr;aoXo59d80N4vliJ6j=i#4khNP=<9EINV z)0O`~v=4GxqObH_T1RZ#$yRNdgW>f}Zm-g-E6&xPd)mzvd6mbsUfnSke5V?5QfM=_ z9VIY#ugJI!B35d$>(Mt%ZmJ)kak5LLyjx~BoNR?;>7yqB&JV3YTu_$l72uP~nJgsa zDmT-7V1W`#h{oxJ<4nZR4qkaKe9ghPAbj-}@-ltZYP^MQs$vvPik2cbbHJ+)6;?h3 z`C-F&*UP|&6?13T2+8{#anw|{wqUONe4K^#&T)r1pMDb~7}hhEbL$n}F1+1>w_7%R z7@+d*`vJNp>3SazD$H+Y{mgxTR zFkf_b#zM`J!DDNKymIi;Q5(@naJheQqJ$!SAB5?SOLKph1fI2IAebKC;l|8RuHGUb8-_eh!J8 z93w^F5^0@ZuUnLbx2Ab;-f)OU)l$!lsXH}}xrjtVUXl=FrX$=T(mqBi2)VhR^=*G@ z*pDn^z!`PEpLO1?UHb(~8rU;XSV30@tFe3|;T9@)gaO!c)C zhsnhZiN!~WPa;0L=RJhO$Bs`KJ`X_8Q3f4{HY*mHm%kl~FvaA?aHZ=U3-7LrY+zm? z*%3a4EzUz}K-&mo-q?;ai#?N|QDCqWNWgBJAe+&y2E;PuAdA5(37#xh5U~jf^_e&L z;)TE-oGTXrovX#gypmKOS8pSK36MOEk1?Adb?EEIIQO3eFE`MZ^vFXh21=Bn< zFg!Tb9czmtp(#5bVujrscvli^VNKf$-7UK-)Tw8b)|Tz_aUMF!jgoM7JEFxBa6&Uq zukukzZDhbbJoEqNDW&8qh6}-0j1P52q@0Ph8aJ%iRDfur%JtQ@TtvJ?H5mCK>>=_( zv=mOal04StO%*)o3#6Kg2;~`$z9ZA^LTE^n_b|3Y!sS+`sSmr1WaN&ql`t&{JgoF& z>L}fY9RwQA?oD2``b+}&HRT|&;2ak!aX=AQNd*O;^oh*8Sv|9&v-v zP6W*iS1`MbFDpS5FitEpa2{KP*$Z#QDCB_+6*2|Qh&zV9c2*VjU_^XC{el?01GtcP zT`{{XWot=U64f0v!mgnQy;1D*LQL&gNpoEY!qoDKgl4}O8TkEBflFiY<9;~*1oV5?CU4)$k%4u?^zB{4VH!vESm~f~nfT}-dsBT@ z2ChAF*2W4@eR!Q*v%#BUxUjO?3>^xMO-#laD)i(G(iDmB36M>RO{l(R^OEh=aiGjJ zH$-AVXrKog+2jIx`t;|aM&u;$lFXX^rID`qhD-QQbK$QQHLmzgUoPEc%?9n>PmV5) zoqBZXNP7Y35`~MxLSdtGVL9Z(4kL4hL5<;9%CXuJUqExWpo|ZmS|nJVVakvsm@nNGp+2#EZkRD zqt`sxRI>{`y|6Nrt3yl;ZR=DhGAp8Ix9th5m+U7OmLFMetfnI8X0MON-k& z0DXF4=$`?q!y$Figw8~qx`6z^;rc7x&8TlGw~M#=sD7ne9eX;(Rpsthbw^*8VvA^8 zOzim>YXYY!@@fpSuZYP_Ydwm3dH^%kJDd%8H64Yp6%z0AV4xB;PT*SH&tmO7X{nUq zUbgfOxu?xTSi$XMJ3rQb{rHE^ z*U|o;{~x$b4n<`}^zQiqZnNMuL}jtRVFc-Jj1ATyvkOKL=VY+K$44ySz!}$cSPS8C z-O01(HAZR^OViVPBQ*a2iEYC<(SvO8mxDOp6}uU{Q# zx~6ndzcxVs;lL;lCHMoq3jPs(9Q3;j-}N+yVY=N-^0bl-x!S@OIb?b6&evab!;eOm z9;ps7n$S`jl*%Sv*=|R3L0@!#8!)41W6#y{b2FhC5H7m&_1^{ZuxlQB8h#sN2CLuA zZxh16QcEL7m1Qts_>>EUYNaO*>vrXQD0T5sV9F?dwO({+93=8K2YJ%2c%SM*Z`@n? z{XJGL#}SmggWhFP@}_&0WA}(v(Cp@a+{H%j=I`ucqjvN5E>^pn&+1}zyZPv@kTp^f z|1P~&bj8O}yIww(U|H)dA(KaD~B5YNVv zE{N0;8qtYGn4u?dCP3-%5PXk@m+Ce`DnTSL1iC^M8)u zYK&o3Y^=xtK9Wn$IQaSntHmlA-tefZFsdYbP?~@-m2C!MY*MYbM>Hg@775}VQrj4s z$100CO)P)lQ$Y`y;CI(Ko++TTfeAiEpLJvJk9}<{<_Ep^D81MF!+R6yy`CT5i>CL^ ze}8X>s7b03L&#S~Z3p{%c+9<^V@ppEYCAD%pyX-S`Y06`UD#HrU-mv#@u>G(Fnh=!|xp6-MuV8)VBob@_4L>aBu_eKi!^qx& z$O2QuL9yEYAMiOu`nafPd}x5iqAGpVXH^kQ9Y*eac$oItBNAyJcvBZAp69V$dQq0+{I_Zmw9|I$#VKI7o=zWmo+$X z)VI@rFW&BNtx{Z3EsrV?I@Phlm>)EbNtPb>S0{C}GFhbOZU4WF8yIrtV?UWR53!IW z1Dg2&%Je+zCt0hcP+m3U^-g7iV46&N|LHedJ55MC$PIEd7M~R^>+fYYpJAraR)6hh ztm=#$?p{qxc``R%n8aoKLwRjCz27RoZ@~Lk<@dG1W#hd})tT3r*CPvn9nE#stjD+= z{9Qdt6N1#u%(M%f()1$tV9M(yg@QUG8IdX}9AkBfMh@qqNHMltPKkair{Bwv)1Ck` zt-m#tHyKg|v3UX{x55Xwq#p;O$Yl-5|Ay*g*j%9sxCv#{2bf|q`2+OuP2XzlG{@s^M9>=b+BJfbsFQjWiD61KDz}q10>wYqcmbyhExeFyaZimlIaj9oa;+V8# zZYkc;!DB1#{G||r%?aGOrM7+jO!}`O+rT3CFg`Qj_&)SA*;%mYZz+IG-n&+f8EVbplsR;U<}mD;0Ld(q>xe`1uAkzoXstcalHLxIqI zl5h4YEMX|4gqWPOXG_PR%r{`~{xy74n#D*N(nley!B2B&V}_7=kR!H@{0t+-#ioE0 zx-O%zPh>5R1(3%YG*(ERp+CfJoIxxP2Rv!Sc99KPUqw&p`bpxmq+~wBmn*DPtceoD zkoCDhE%PJh2%pwzvcQHppUI3R>j?H@py*HY%-ML;Jb^)|!8g7{Qk(KL*3r7ayW$El zolwjXsV4B#fLyb%1~nFukac4q-ej~e>E{B6XXIf1<&lrLyHZ|7v_P$lQ)?Kf(`p&^ z^fBNOCj-Ijo1|3&+KX(STu;cl1oLAJ_GOqK1IO@P-^Pq~c_+r>>{kY-EWl=mGZS|6nef|Wiu4>a3wpOn|H3a0 zIm=1@HJ@BQhn~oAS2c$1?3RD14ec^kWSM@3^%TqL{1*bfv z9pRNdZG7ihVs7KRI+v#)%0yra{)AH-cw$$SD^A3eku~V;I`9tK*LYAl9r_jz-wp$) z!9<*Mj?)cC;mYun{2+c={qT7PHA$67Rw_#2tb@@K#k0emiqV%3CF#ae5&^XEJyhR=~jx%%(GptyYu7B??T-mZ>DCymvKSAvRl> zFHW<8>db}pD~a<=9m#pW=O{QAcI}`GEtD<*)};aDj6eB@u*2tC*WtXh%@PGsGHtPghsYg5T`u@-loVmE8#tx^(i z_Ss~9y8v|JPVkr(qbbMIV}t#ni#t4J54_B5WM#_Hk_4erUfIU*$}X&A;!b-3E3w)~ z>q7(@=E+P@hoS)U%;Pc_m_b*)tq>);*>F1{YhK{u=$a49QB;QVv@KASDY&Q|$m>`} zwqUhhA?akVI7H~GcvFN6gYQv4ld+!5Fz(}T$LV$gWGZAL@H?T#W@h?VT&d|*j#4_CpiU#{Nsw4d#3&AV za-sEO1Vi1U{rl6g5~FrBYkLwWz6fXCdyvSA42Hb7t0j`lUdg!6sMBURF-Ou9 zxPSDAkO7Fw+Z$mYHceX6V@`j~WF*JA|8U_|;yqd7IJ4mC?N{{1u>ua30VP1)iO= zkNY+(-)+HODr+8WQy=SBaleb9nX$edIc$wLKAWTN>1@?x74~XoJ<7SUzHR>I9Br@4 zmQ75?a~w-dh18RGRm$-ag8<)i>?Va&dMC!AhIP;uU%f%bL z3|3$(CsX>1p!8+|+#2!m#0*awwDEO-%A3$*rc9HNXY2=%Oadf3xN4NL0FuaFmRJBo zGe6tNTnv_BA_HGeC&PqTN?tX(=1 z$#%3kq87y5L1qi7Y&Z6ccahCex5uOk>!L5lY>fW7*g@VAJIQg=n$pdX+wbh9Yx|gg zKAC{r3-NvMv%>GgeUC%heTVmyFzF|a93N@aD#+<6eGbKIzboNtgN zoa0EkT9|`V@6m5qyOr?iElPfm|FC2Wf6|ajXsa1tu1)&HwM?}xx&`w(qVIRkkS2;- zqOMRB3Gvz|{g^4zJN|oY-`CgM;|CT1R|;7mn&%H7m!mlsyPb@0 zjG}m$n<`8st3}#Ys_+?w%%}X4woiO3`dsP`jb8BBPHIf7f!9c-;$=-TqWT0;(yUNx zglxjcoFrSyl_d# zLvEUAe3#!}B8U}yvvG|$oqXD_w$k}Pq$~ZjM`ezXrU*TH)@LAbz{%l0M0CR&A)bxb zmXtpg7!TVH;>kb*iS&ESf;&}Pq8aEbzV^>XTrT#V{zvsuALhu%IFk8X|18Yzlf&kMC;Pvj3BNbd^+4A`L;pB} z^K%hHNAhbRr4#8}dE88WYsA@b^`9Lu-tr#*kW@%Q{RJd--rqQLF80L8Yjl{KDQr%8 zu0(B>XFc{uje&_+Y47rHm#n~SebTf>T+~nT_L|?Eu|T|H{5AI*$eyj@U%5r%d|@WJ zO>E_EOKB~cDDdKWu2KJbKaB@Q9ypMp7|;KscZ|(}Ef}dPaMd(r6+C=1!5?+tL&SH= zAMU|V%0KVlh(74ihX?yfZld(VnW&bAHA$F*7wkUKI=E9$B@g$U!tFC@(TJ{F3uAslV##d zm@jm{wbQN^KGgpVv*cF3#6F$O>8JCA&ca9gwS>hO5NS1F{0_18bk#i6AD+8vYk2m~ z;poae(=TA<8n}~}W|6x7|MN;z^v?jKpGuzeeBdZscrl<6N>X-~yoi>FudF{zAHSzD zt~23r+>ddr;2Vu<>l$$mxBn6KmF4h51B>0zE52+xJI3&v(HT;nX^r~SZIT6+Hv zYoNUpF42A_md8R%Lek<{4={^b=KWiyA(&I?U&-~TK|vZ)9tQ=f3p@^7c}q&{Rs2c) zbaM0(rF?&q)Yd6v4p-4nY)10!rLj5leI7G^+jii899xFW&HKe9GDFKn|1pNj42S&B zxDN2Mp)G%Q(dr+L3|mJISt+Yq6DKwM2O##D%)#7`67TmHu! zyRilj`+3m^n{2!?O1M>Y#KZ}^#jTDC=%7jSYWpc8v2?-~n@d)6En*Bjl2m;Ux!xw; z70)4m4Q+|icZE%Z{ETF;qUqDXDRB-_G~={DZwoXAnX<0M^CgpQm)1GpIfKjv@S4h- z)w=|8XTq1AnkuoIi9E9cIW7#`ZV?g^LCaoRYBeJK)jd_>8r7ZZD#b*>q^*rs_|c=x z>9LqmqDAze$0jz?S%V#zL!=xF>)yXY#JBW?Un`i{iNvHm zA~Md!RF(tXqB$+nIOt?pO2a}UV>u3@ai4*I{WrLOu9TcR{JdF|D!mqabO-bZ2;HTS9p(!1OI$Njy9_r#Ts2{%p^%Owu6dT2CW%(%7*<*bK z)ZspEN9kf^qj3&lpEC-WwSadyc7KWk<8}87f>0^|i@F_Rw_&yYISMrr0PjF9G>5D*l*Zjh-obkx%J-o?myC_}=!X8NFgd0J(d`tRiR^GWUr) z`7PHE4%X?S}@AT{@{p2Z7E^D%Ly zSYy|aJzN~w25zx9Wm}0(NE9l;852k?m8GpzH>i@VBr*BvaQ+y*QBiw_>Iq0@bpI}l zbX0(1OdW`^B~UGium94-WXf%p@Lu~hjsFs*fuCPmuV0e#CZE~$GC#g+xnv{{P>G{Q z+0S4NdhLI_q_ELmHG(g0UV zq+0{WFBuW1kZQRT@90dFH>f4!C=Jt-Dsek(w}(4}_1^{iJUX&?4`N`KR3Bq!*)qiYRoF9mk=`S^_O z9d;n&uSYa2(z7vaIU(AWWSdwF3G!sjK5z<_I>aRwOw_siibqx>bZWayHd~ zeh<%VNFtYHjVey86|MoY7-(Q)@Mb{LLBGKiHPOhvf=hwbB6O;`eNuN|j?nLGDgE5{ z0)M9G1zy$t0q;Ajfp66ZIJ*lo(T02m(4~GB_(SQ(&~g5u^xLja0u+z`vmcZG*M8{H zk2v&0KiZEJK$8UA(Dn;%>P3#9LjMmypYAUZh5&m8V9!A5G~f4qo#>|+hye!r05OHO z_pCvNHtHs5p?$s4*YB}2N089dkMZ7#9zXiiA@}D(*X7avjO8|^G66ERK_++{GC?va zhD`98EEC-C{{k`rb~&~X=wYhxKunRqG@W;mOmJ(PSG+>D+H=`DQ2Pq;far+697FYH zkNPE7Gicreewx7tc23w4E5y&rDq?nke^mezHTF)@Xt$6$9LSCQ@D_)Cml!jdoZIXF zo#R9D=M^79BA_i1X>A`Q0%URC3cch>#HNFWcxyzQK8w}*pr7h#NBUsVl=AxMN+%g4 zy_n63F4n$P7O$EkDC8_Vq@{}Zun*D-1&9$<|A3p1wu9Xate?IY`etFEJFwpkbM9wv z;@o$Ln%Pf*-aN@s$PNMrCRkD^79Jku+)a)>VxJxKLNfN@bS*Pot$PAgR*mipbL_3) z*d(}j1lf${B*E6Wp^rh9BhBpH+yOBeTGAzA7q~R#@)hDP&LL`y+Z>JJRy#4f1Dnwz z$#^o52p$!$iz72(wVtlqBxHe?Pa@SqwSBvI8>E#2od1t%@r1p{{uFrc0np*5()}gJD0kww z@TtNKT?M~}|IlE?4x?6_r56;P(QI>quogK-DBZ0QFGuGHixg9ZbI{Me1)cC>%~Vby ze4(ByRB5aWm)c2QZSSp=)0ks9CA{5YllOvmRPgYhC+8UD8Bdy_>>^>grFMPU4roO( z19yOCcWP3#t5p+(s_0foY#VpAXT-*B@?^JRO<6htYs(4?&CzEL|W@q#HQW2l) zOBIUQVl8=Q0-5Ey4;IvX)2w{#+xt`Vxt{4rI!bWWgW12Xe>vv5@w&O-_8a^jdO~Xx_&sv=;{N9T zPsPQ7RC5a9c%`YIGvn;Lc;NczC^wu6+ds*iK)Ior*kUO+gjLu|C}vZ^4WY@hMF5I1 z7;wIcYee4jjPbnh?ZZ@K=7+{ijJcCc5mKNr+ZY9@7(1LlEV_$yU^mlA)(|7v*;YU_I-g|7E^`yMl{7T@?U zJ=y~3(OgjnAX`#ty-{BK`-jJLY0#Eg3!qCY5hbpfQ;@^RYen^GYj5b&o`yaxmP{un zjZbptWsTbUlqZqFk!sXPdW0}_RG(HT>(dI8--32wBV>{kS)Vo!`m}vF^l65^r<_-y z$GRQ*w3*PSd8t-R`q+P8(ni(BXyhB{DeJpOe9U;t^oFdJdk2}WVdXUzL5paDX6??t znL^8`UTqV7x96IlQp_j(4agCB4>-g2T#&*l=p(D1HX)H8)ok=YxXQ2))EwlO_h(SPaLW`V!Vy^;5Ru${IY zCxz#Ty6-U=7d1C@Z8M>3+Z>orYLMk|I{7%T67j=C>hn`=$2Zag(4)NCXTiAv#5bq! z8(G(OzpQK9(`QZ}2VKhR+#BHAuZUl8rQ&p95_Bo&xRo$-43R*BaXNYvMU+cP}xzRgmZ=i@!KJ_B7WMYl7)=g2~7}mOFJ}S!#W4b3gZ>R?D zj?sA&^#9?%G;p<}8n{0}12+j8xT(;t_Gh{A! zpnq(}e1cO{!}_Z%6jS>#mwJ!;&6)3+TEx|`Ouxr9iwWcw z@f+xRi^%QbyWH(5@4`x1EB*&MxC8zF4@(*k2DW3|?v%Ak4?wGgeKXa(VZBKHFzlPl z|JeT!pictyclDFWdZ|ORNF5<~%qKO3WiUTii#-}8=BI_67r&pMOxhB8j>f@Wx3>Iv zSOb^&D`?^W!6j$~CpV#QWFUAw`_<#hejx9Ju|vq`LjRFQnGBRWl675yg0 zIs$p4;JNQh6wHL`%nv3&-B*!5wRp@+L@RSw)g}oVsT!L_1 zg2T|DJCOAJfb#!ubHMG>t1@zTmR^!T(kz{X03jfq?hr}BCg5KrqTM9H zZk8mnhM7@OaTIZ8LON*DH!LHrpbito(NPqInNiWPn`I0n85@E0IJjh7=#cgO>UILC zGw=U@@B6>^zW04|^Hpx&bL-Y}>QvRKQ>V@mr3pEuA*VFtWOAqktDH2aTV=5Ef!(%i zc^tFR_+3%cp*rOPm)S)!P%QLVoB8k{G*Ij0o13bQsmC*4c<21$KWqoh=P^ zGkaTyCbE9IyD8|LK?X#1S>D4Ar_9lPIL`s?1k?*QU$E2IjdFhu{hK2qt7FIneJ8Y2 zIlDyhY3un=yKFk?!7MR4_nUUQqRVxpKua)nLP94CdFaOwV)oibV=m_W(Xjkl2_7!l z5!r*4nhI$7#=H=cmyX35lASiMZFS01h25A7EDn+IZV%dN3;5e75bj4vGD5=2H9?D= zsMPs-h44!u%h$_=Uve&=eL{VkO+)pZIT6b;xXAFO#EA!nXwXh9PCF1h!Q=L9aGqm1 z?AEgeBOy2;LEk_wIc(2z4nuHmm1L;qM$M_zKdFA&E)S3lQTHwVin)qmh!}QNULw>^ zCo}oW(8#Z^LrE|~GZLoaH_eZ9OL}BxqP5jgt8Z91;he7?{$of$RQ7pp#D*r#^B@Aw zWt5@3IO)3^<;8i<@4-s1!tOsv`!w^MIz@v0TnSF>DV^#%N(bygSjmD`SugazsBRdv z&kW+AXjxT~YS}(mP15MChsTkxL4E2&K5!H2!ynvs3}?Q3a4rSDF3)V&G}%QbiCG%l ze%k|!nQ46|Mq>Evy@4LkYn;Z5?`m(FMz>S*fc=|*24|WIHAd%`p?I>?O>L7uVZU-l>t3SS3#)qu zw_LMCqjlbI4+hlkg@~zjE_I#_?L`??W{dN5h^BKt#>vQAqjS0QHs|}G9XHO4h|INz zK1|x>kmX6ROiekVN`KCt+(z|_1F-d-;6}S=Y6EGoA4E?$Qof_7-{7QY#9{lQ-;rbq zsLWQ9>ttc?M+A+yPtK~$xlYik!z@LW8y&7ew zepnMoa!OqDqO;holjya{u-!G@tgJPd6LyPCyB)<7U}2BUD!A2NR;X@frXAO)ajK1j zoiC9$9>=*@ubRjCZtSiw-4pty*9U!&Ujwx)4NY1_O&>l4_QrQY?NQh%e_;HoKN(%>*Jk*msay{LboQ# zcj#*6ytH|{z31eYq3%NuBt3YL@uCF?TWc}HR^r+5<~jCtweoSMPWC`&vVka!{qO}` zlVZ)zuZ=vtImi+DSY-!0!#auK$JMg?r8ye1-_Esd(bMyPGsyW+7CcTUPQN+3r8|!^#~|!DHsUe)t;on%&X$QQoNLG51*9Qqz0l zS*Ll8*P$b_oGhmfwD?5j`AWugarM)ySHXgGjCY~^d3Y;|X&wq^;e4Gnz?hzeZ9xj@ z4{rjt#t7K63fQZnCj$ItGT!9K)z$8qd*ejXwtKD!BYc$43@bN2hsmkrO+>wN&AByuyNW2DR&SE=Eur!FXi?GpS zHp}+&u-`-A32UU5kA?08U$*hwjT2vmmg_6P&r6{d6SaW~SPoNl8eP;T5>0_c3gt*K zmM0nes?I=$jx)uuU>6ISxG%}%`Z{#ym{>l}2M@IECb!vjLi!oL=n64L_)F+0(Y&Wy zsJRp}xfh9Lr_7!#qG*;kDR(`4qa!IZa7U;Lm<|T_u0FS#(rJI#htns}$}!tZuf-#Q zvQQ)kw%zP*mqwjd?uzDBJ{sfM8%a7^A}}o!hdrhP@XKR%^HOqJb}pU|x|{5zeIm3Y z$A{3L%(@&Q&D{r)HwhR+Ox^{gODy9%7;{D>zu|ZB0$d!3!&^2$^E`Q4xqBqfgEvA> zgA@p2mD^u}@1;1t;rbl*yej0--Nj7)BD8N!+#am^I#XenU)EUwe&PC3{1vjKC~u#m zeRPah6NF`|dk;cJ^gP0=2rnbNf$-zfzta`PGr3XT82)J}*%a8mfltIs9TZD1XuWoI z1W%{p_QdjU>W6fYfOJkl{!tx+j&DYjbd@(LI8qy>P}i==lk<6 zCNI_($E%uo>{cmkyzT(M+$BC z-2(-f)f47`QL-cMIq{Cz=R`ZqM%lA+joAqabp!2p?2a5l`<{M4OlCoL7d`|F z6W&MMN)L8c3FgkT@n{c=(~%Pgn}@K=lWTE;<|OQCi<`x7$r!Uc)iwtG%#o{yZ7Xk~ z$=x8aoAUt%`6v)t$0BJhqJ`qVFUmp}!{wta4s#|uv#yzUDP8FSbU zZ9{`P$V@qp*u3s_`-x79A8ihNcnqi2jlCP29VeI#=QNINS*;Pa6K7@1>z>d!a@gG1 z+P8;k^V%0(;3fl@`@0WqJIKMekt0X!71gs1ihEQfnul}ez|VuJZ53I$epwsE*rUNE zS##k}!Jd~@YYkSS)}E|zKI&1!AD?tM;TS<=p7G~3}Yq6Uit>@H$iikjxu z+4C&5HwSN)v?Kj9A)PzQtxhiSXi&Zh66Unl$s+H*)7mZA@84y1%@69_wDgJa6};R{ z`K7zA;Piywrgy@-u-S!JzWCbdK{^6*@Wd?^`q&LPZ_7&)Pf3NbD*vp8OcAJGnwb0;lxn>g^)`UMyd;GmS=ZydMrX>VC zqGXPk$cPzy-cr;@gxX{3^jn;@;Jw#_7mI$G98#9F985mI)>F?k+z$C{c?ox1yo0#e z=NK>BJ#`?`GD!zzZ-@?Rum*u-{$84=j2#+c>tOLC3pOaNOt#M+_WO0vZiPJzCY$mv zBH+w+k5cwxm3@5fyZRN&m56{o33;Y=ah zJ+qHYi1M#8uf&>=*TF+xCrJ~P>czV>7K|^0;l$ortTw3s(q|5Wi!*ce(k7EvvLm^H zg=aDF?6Ai2_7HZ-#z=T^z0qsrb#+l$`EBy787D|q5quHGgZDGGs2a@R6^Z|zU`5>$ z3+u1L@9LnGf2@=Rw`l|xOb3XUYcd~TU|mz1F0L2b#D2;6iLkfFOfojcdz0xb=uhU2 zJ{|Zvcmz5 za^oyWTHgwa!F9kIzGJ(+(Qo7>(62FiC470E1*^gPp;tK`>m}G9<5>aD*Xf9L8ti$t zZ4F9%#As~T9aJ>SGUXkz=9?VHH7ExU%F0D)>R$;d1(?$NZO90ZP>m=-MUl}9o-J## ze`~S0A(B?y8EE=9w4#QLb)=3x&~3<`Cd-=MhP`$JzOIjJuol>ph$Y^yFg6y6eGh8( z+Y!j+)$>WZY3+_kKvxbSktsNtVTR$b6*^ zS%Zmu5>k)r>rmoJ*$@ti;XO*7d9g!bR5lel$~8*wCuq+`l#$6fh;`CEp&-T*@G?VG z9t9e{m-eaa+k}>v7-dIy0dgM$dGM zK`|u(l~0Xbg_pXdcq0Cfy5?EZ{iz*rbK`%nYj271S*H66EX|U@J6%S8R|!*KeAeh? zy1xR94FGRwo2L0B!`r1WMWg;FG}dX+4TnP>OVD49G@j{d5YqTqNMkS3(ASN{S}Se; zF9i4}17i3!P3IpfP1{W=ucqta`5!|k;kZtU_5{xx5c3MJxCOqW{!XzDTM7Du%|1D0u8?6WcQh#QrSibfMwJe=>Q`B)Y5FF{JcbkSbPbT1fG2=_UI z@Y#pw*@H^qUIe=r^w~D}`Nedz7;}^q>wwqWWANKCXP53X;<_&9=ZB>H?fT2)X>E>s zo%@lHlpmG*RJ)Y7lC#*&%qP5#)^i~I%RZbU!TLR)B$KpQW?VA-_b`51>WO7vkRi=P zU~8FntlQ(J^|@~-z06Vy%-FnkUt1PzgvXy@GBPm^U`NsWQ)p8pK&Qo2dTAJs17Y_N zG(++#)q&6{GtYgb!5O`Ae}gW2rJdp{u@qLsG*<8<e`6obIW6{Mf9klg*I|nD zkqn9_?4N>aR)$tVHk>+-kl%v#C)pW+?q9?2ZxIOT8G2WM9qivL-1q(r=5l^U`F%B7 z2_uPrB}zL|YqVcy^tq8*OF%O*m?<`slBj8<9CL-N6MDEEapy?JB;2LvI>x-kVbTzA z))(%|%^G zzr;bY@O-)pVp&L)-VP@Ec4?A)jm47YG+()HvJt+=bgk%}45a0x{csTLL|tko>HKZ* zVH+_g)T_M*Fhatg493Be8ktW8dB{$T1@LqV8l!he&N^kjFLWn#vIdp!gih6vh`dH) zG>lN`>fthYyRd@t(Ndiph!aZn$S_@B=#S$~UKuT6=e?K{DQ1(fuB3724n}aWzDp&* zby&AixOW)t#y*X#Gix}nhllf8)}`=8`pl(4E`|1Vl77@JZvKNa4r9=t2A{Rxd^V0h zVAls;v}*$eq679(fxnpUDm%dyfPU3yDm(}5SaEt7i=xv=7!%ELDeZNT1wD(k@v06= z4RwJWXc|zut^{456vl^vaQ{Gru0KH=7z#@4%{B$(xL+gYLOXWRY+rOG`4V@X&@~oM z58YC7T*r`Rn|en=eJPbDdV52%;gXvMg|>u6k;@lVkArIbdxLSl0%#GEtTFx)^srkX z0g&(u?b*DY}7}~Rj!g+nvCs}k8l?~+%(p1Yr34AVM-Wd7}7K_ubz8?s`K16#O zjnfx)phbwk=i(&D|ERA)I@iex8S*LiUa%KJ=fHajS%7T1hs4d0)O0I`wm|!^T}*xC z_8~8P4}DE4XDEYR;77`+C%|oGGViuh6(4U)@Fw^kDubNY|N9{I#;R-3d$;0EBX)mM z3H9$v>!50g0&R_^nn;9tlN|_$`)S|o4}UnYsdRIh1nYF4z4A;9zX@$83PfWL#7Qk+ zX&pw%2({kP7oqO*zDVD>r7tqFFY5dDBAJj9Q1N<^F+ly;5|gW}FZ=;!$JF8Hin4+5 zoBiO#cR*iU(pW6xQ+A?!-V|RdPwUeJYlI@CKxe+?7l~;}84)}lJ`!477Pm|0Js(_d;FDS;n`l?1)yxMo!SXzwWBX#PxyB{F_g!)R}Kc-*EvVGrml!KZq>05xn5 z$eLk4GCZ#rw#s_x3`w5{!k-St@f6?i`3_2Bg}N;9(;O$4PC;E((3#%9?E-~NDxC<( zUBcXdSLoggG>IKAI`P+Kj^>%9%j32>ywCg!NlD z_8~b)`mrJobRRZgFIHSvlr*J|Or$$j>Oi7%EcP^*$%pOV;!Mkzfdr?h_V2LO9fi}i zWTFVWpHU>Et|Dn--J8%?B@_PP2RB_C4}U*8(HPrPBefv68FQ#hP2 z=gvH07CE;UXmBo30l7jiJnZ34B=XNK`*%wNiU_?w%c`4S2Ie z@N^y7R~d)5^j%sHF-Zv#c3b+wbJ1FIKfEHrZW~>#_l0lhbJ&*@&eSpH1n;PcQEm5? z^nvT9{5~)do}pM)_ptm}XZcFl!}iMgXp~TK4m@i> zdQQSiTjIR&jnMh%*cOa2P9ryzDQt8NlkC3T&6=mjMtjdol&5EuscgEH@2 z?PDMLd6Oep?z{AG0I6C1zJ+aSTo? zC3=&A6=HD-_#meJ=oK^jOe9fAtuK5IvQ6+?pFTA^rg2gf}}sjpO56 zBwlHwmX_@7ZRet4d+OG6-m+d`|2B-PoLAgP>5R0uMM~d!DoFfye@cCL6Voq|-_D*$ z&pC8O&U4-rLUWw|sf?HTmL+AInJMwLWuH!hq%E3%P-xq0>$|$1T!I$Oy94@oa(*j# zzo_P=DeK|iC9;wxnI(|&)RpwZ?k(-T1K~igjHeh#^wN3^gvVnSb0F*#II0NbNN*x< zowO_|sg};pv~~KzrKs!b04*gk++H#eezBLn!&o5T;Al_=Zu623+*`+{5S{CZ>1Na` z6IiEt(R6RZv#}>}Cr$6|5N%V;|2?u6wa-i%i)k#{7LgL1$9}{>c$Bd6k3cr|TJsY5 z^jc;T{eGW4D?oFR`P2e`NKDZFN9!K%p}YnCqAx5KO14qpC5d4w%Dh+@>mqMI?V;3U z46RDh)mb6qpF(B^lSw#4-x7F1ZmrxW<2NKxY|IYOxBcOJ1<&UZOumMkvjQV&_JtqEGvhxmHJlQ`$~oy) z4`ODhHSx@pRA(Bcnn2fuWaotoku^2tgVJn-Bq1E>CnSO1e->G12WQ?uXnpdtBmo)L zA<}@=49m!bnXId$-l`sjmf4q(UOkE0-4@~OQNszKBYzkf0)YF5`%=r0GG>u+EPVy{ z@*z<@tu2LcLW_|huAYurxBKZ%2I($^Ok&Ad%zCEC7q$WHU(OT>Wv)L*%-KX?qNScR zpn|LnHR*gfXyC!kb!-h51t*&|;Fxa=$@yg|9BLwKU#6#Hp6V=i<9#hKK~oUocy8z4 zb=c2M=QXk3uR@9zId2Wo7K++ECB*qEv2%hrKD|fn73K2anU+9Lr*6Q2x;+@;EIa)Q zUs7(QB`;`BY958I%y-Cl5g*%e@giywndJ+W_B1(-He|Cc+PIp@{!Xf3|Sn*Z+$ywJ^S&!_3x;v1$sTqunDH zDNR%PX@o*rF-k%gVw|`$8zUA?VLqq~8YrNfyWI4-7%6h_LOdd?83=zdfPH!)ZvLSB zF$q>r3Y3cO{@mRwZ;qb6e8{sQG7g&3;`m9Xqg^O)1G zIw~cY z!P^SB`)UAXsVChtfH}6~T;RO)lhgU*+5-y6t(GO_qnAed?VWzwPj3OIvMhhPl-38< zlFjmZ1S4c8co%hL)3i zN9y&!aJ>Rp6YlC@P%qypS`#Z|)!#x7wFhz*A6DU!IqVW3kA##0U#l#m`^iJ$O}#XL z_XG7CyJB}ShCD$A)hf{8Z#v)T$_5{ikI;?PkJytisVp{w?6N0xrkY+LJc+ecY0txV z2FXnEbH@#1-nTtje6Mar@h-cvEyqb^=G^35J+BtIo~HU`W(Pc=rnsrb{fc7lkKFN> zi{H0}LhNz&ol!`&x9xA}3DyF+Mbu`PdbE|Uq(sD3Ahs}VH%`5b^=aWW_#esox^2u< zs>Sn*gW~#xCixERCSxr8q?c;(NFg^|)ytZskQwQ5x)heY>y_XfweIan3v~f_9+lzz z5tA#cvyKe|1AiKlLaI{}I_3sMAiOi%WDcPRYs;9FXj=fS(ZZl8BHlhFJfz0l3_ z?SmwICwT61b!(_1MebGl^mSg?`CEg621WvrjEz_KF$5+tvT zB?@nrXTWQXm+>ihSi3iid=^){GmcMojteo6A2Io)do}X?JFJV-8ov(?Ji9 zdNu<&z&o05kmc`yg>IWaQd0Oru*9LHlE4#E?zG4k2T!s|@V%}!PY5z;@G6n zv@>I43>?m5pIl-FZ)Py3IB4A!e5&*LuI0tt(PsPEwgc`KdtCb}x47w6Sj~;Sl1VHc zQN6Gykgc)~gkSBA;AZ+2(JqT;X;PL8l55Pdmo!VAM?zViC+vz&#z<-C=>Zm89Pef_ zUbnw{wxcUGjqx!VCR;+v+kVamj;era&%SWQU{vY%+Ap-f5|WUYLo)338`6Ec2J(W= z;g-0QpdoeAHp#&`9Zt^ru>ISUwTG?n<)EsAWOE!F5INz$@UX5fkrd9*jwfw~947k- z`&Z!lzrhH#{UFnzfS;m8U>1&RIB%+1XOq=S8dyL0gItbsX6Y%ZQ{ebg#^*WpZs|_3 zm)$AGXd?HDvD+)cnslt6&ZFx<7qbE)P`S2^1yAL*$92A8mz3JzlP^M zS$d0Id}d54NuN$weuh(uu}|zh9TM@GmQw5&%6u&3)-2l5;s4?H_u=~%d&U{=19*|l zecIl8f|$6YyY2la;A?}<(fn@M@?@ajDmJ7Cfn9KPtXDQ(0@`n}zkden$#ibI*%@Op zyVW*5qj4CVZ-wai^jE*ZDDiRpHXYp|c;ChqXx$2%&Uqrl-QW*#2C5m_4etenh@kVV zSqYL3u(K(I>PJvH;d`z3Yw86ZWy}e6+wFbOl(-zCHmhq76|ba{6EZ;xc@pg0UvVxk zcK+NcasJV{V&yj|dAz@3<$hS22eS#epa!;1qsfc5waE>@8tFa_d5OwD3tvP9jV2DG z6X`zQSFy6&*^Y0|K_?0)5~$XZS>!tE?18oBqLtK76&V-FZ`zgGtI{hnUg}@Ba;3Z7 z-5{(0vz*D$5DCNAQ-ZmrSPK1CVu{$-#f?zy?>wkfIY6G!aWuH=0o9!hKvNeHrMru30I7SdMjxiVp=Rvp*XK$@@{vg zyDKM$#3KOKOQ>gciXQ(ukzfCKfJ`tV>_t+)vgNMrrlYKU%m?TgCHdLW+UpDfE-~)2X zCCufliWu%ktLZtlRTmAf<5oSZi(vwsRnPcvXRSQAKT$hlEhprZ0iH^vCK0@`GDK&K z!dyWNX1f@%MBrx6+YAJ{>xX%n(%BbkX~ljJPW!73devEY9TL)3=%hm0av$zUTXt6Z zw^QXZXwF$LgbaotWVkBAz?<1caGymkkqh2b4kF~Z+$eZ{QglL>imkxSZyNv{yGcb`_YlWBxh zl4qKm?Xf3mKa+HFX4riY+MIe1=6KepAvzb8p0YSj2R>#G<}`Q~#U1?-ewgEJRCbksmXi5R*hlqYZlUX_p>P9M2K&^Ecd~!=%1D3c z2Nk-n_J^94Z(yd821b?85!p5bNh5jHi80f}@ZjmO4uzUH-b}}FrO3>`VDJXeS2@yz z{}J?7DRlQDzV)*3wE^F9&=IkMvX2zkI~Z~oi|z`a6QeOtERKEfAZQ=5JmM{JA8@nv z1mk30JB#u2bE7vA_}a$=x|5Nr4Y3p|5Yn4s0_;(miZr;YamIFDJ_M}q)9GV0rAI`eWA%SkJrp_YQU*#oRs3}K6bq0FQN6= zPhp-N3VQ~aGr&q+WZx57hW6sSq`9BE{BQeSM7Ju^Kl3G44+UM|K+v|C9o>sJ&_5o2!c+^l7G zkR}GQy>G5isynU^<2(+NuRc@~8|PuNw}JC(AcY#A25ZhXV!jXm;UnU>`pD&w>_`}^ zH{wwrO2{dTZ32g<>`XN0A~#`ru@=P`<{x38t6J^ zdRx8uk!|fnXP@L3rOQ2Lce#%BR(RN^WsXZR%dnDI1IfN#a{zXalF1@*&gn>M9}P%S zd^0l9{6r;22MF;38TS%7#V^AqleaM&cL70U+zVFcOTk= z-8Qjz`?eV1MK6gx4b5^(G`w2&haVW2>YVC=2PTvay!zC5XDZ5MGyl|u72)$0E<>_2iztdQLBgO$%Ge!%RQVR1+H+>n6QQdJt$&cXkYGR zpIe->P*a^`cAj_cbH>;1tIDnun-;@Ynx=0<65Z7|xlh8U+afdjT)DB*%QDK3EoGy^0G;%@>LjY;IPVO})cB#Np7+~OA-u`M+m-nNoFFwjCy+mf#Jo0H z@5X+sg=vRmELd(o?__~nv(r*H!J-JPLYjHtgV{VCjR#+&mUgMvIxn+M1YQTTJXY%x zzs2AXD$5w&hze4?cnLG{UdHBjgWb z7@|xCADs#;oK-G4qN%-+6VBMtlY|oY@E81{pR+sew8Dt2!wz!I*U|f{LK)gwM9db#WYyka{kcXjmMOaqEi6v60 zOs-I>q7rZ|Dx6!ClWoYp#gLz!H+S|=$W;JYwBEJ8YMP;ZUX5Xnr+U>g!@{bHD$hnw z^_>R1s;*qV`Cmn;sx&NcRjjI7|KkXY*Sf0Lt?{g1k7R!QwyUTfN(J=B& zZ6E(nQ!fY9Q;qs8Gb~;*QoieeBf0;RR|}UI zW>>APG8B8(uDe?4>psQ%VMIFIuIf9h3__b2ZmwQcU9+irnt{x#+GHrJ+C2QLqzFIe zl*}tLEM8bxGH+H9nYD1?{DmZaLe>iS0nb@N(kscLTG!fD#T%*(b7~g47FDhFxKVnvGR!*Lo_e3^gkZ%hB|kJeBL+;|%L+ z*H*bI4J%v~>uc7IGvI}xqNb7($XbvNx%#Q#dT*En|KE-RR?!s-YtI|_rAIW^z3ccBE+ zNa3tShLZo7MhweHSH=U*3n)_3d!O{B+^(yi(JJmsjgaIQbZQic&gVg zt`TlzR&~YN&9&=`T*@+HqZ4`U-#gu zBl9+_Szfhv{)*v3ueM{>#C|N*O3`yJ?*LUD8|qb!ox7GBKVm`e$=3bO`i2` zO4XDqgtv>(A@KX(*S|6FZw&k!1OLXr|7i@!kO3rd#DHK!*n&X7-4Yf7Eb*L$izr06 z67M49vsHxLx&gk}H-d-WM95uh2>BS{Vl^Q(E<*ZN67p+=w`;(Iy9wEOCn3K=*j$b? zAP6JZ|IM`SJ@T*1Mc>gda*aIy@7zbyc!^^EX~pAvEi_e%)zcVl^R4PxdE{&6Y4dVgjN`}S`RiK_PzSN#{=hh$ zzIy52g#~|kDf9fqt&0NW3q${J%U2$MGrx4sl8^jf?eBBU&3$mz753f*)Au}k(>~j; z8g@1BlOuBR8mu$)Y7ux-&#{{9De8j1g3x&QFvfA!6OH6Hzb zbZ8wekBErqQ>0Ya!D+_AB?uPqD-s{k@kg&%1R4dqbhN%Wl}!ij2yA5d#v5F$Fr3q} zkHA?0E)$+9-bUae0Zt*2I*q_N0Up5vt@Q|8EWn}V0T?aI$Q?eSgnR*_GEU*$wf2hO zXY`NY5&Y2l&>CKgk6kc4)7Jk!Tp_?IwR|6ra|^>T`tDjfLwF)YkMPgn6u*&@QGC+! zN8-cE)<1=7{|TJtOY0kHp=;?-IIZsw;Q(F55B+_8z9abgA$)`$eh8;?_%O+QFP#y3 z`(gYM`uQOod&L6IhUp5JppXdt)3%Sm1xgX%BlI7E6J`W&BlJI<&Q&-F`$ zo_?@@go`D$HDEBlcUc{gN9$1}!UCbRd1p2qTyB zAY^|pLI96&O;&@ti{#vh5QoGeldo9DHXmMd3B#}=TJp#RBi=;&6d@12Nr(es8NvWy z>2Cm62+twRM2JO5IZnvZ_X)WNA!+#Dd)+gAM+1e?_X`kzBuIw8rSIwYy5T#GJMv85 z(LiB3gpqIQokN)YXF|UIFcJ(O5%MK|(=ZCZN1o|#3Zu_y*M)KTy}FZ-zqVt0u7i-D z58vYe8+oShXrM6q{+{cC0kFmkggkaW5@5%Z)Z;e|E%-h1On*}tecpdvcon}LWWoGJ zB}+)jykg?1UP0y+&Lh>XYUp`dvvYDMIPADX}{Bae+ee1`M3-_uIKcsiV9eY_j?tj}jdkVcj z(WqO4yX|6&^$FbTQ;xmy7u+Xhe78#FAj9P(d)pqoWzt3md3WBOYu-NKATxG2FDyCd zuswRHF8Mc!MGL#qNvJqtS0e(w@;`0q)ZL0U9zdZ)o=j}hi^#W0+{Ba^G? z+%QDmp)q7ch6AcB3w{nGJ41hn(}W?37( zJwR`fJn8)b1f)Rjrw{+)TEGwkedUDS@D>Cc!hiSW_)il2|NZ?xlnnXB*O5wE3vm`~ z0z)p1|IY($_iGoWk-|lel9K-kw*IsCgmfAR*))<3+@Nz$h2ExiD_wVdR%&^9f!4fi611WS|Cf^yMv8&e9J3Q8yl0YRdu zRB=Y2j#FC4hY_7Y@Pc*fIF21V>dc7a%p{;QE$CQ8i8?x-5jAPrOb0 zCRs{8X&h|4)1e3{ZS9U|$OoLH$PvkIDJ2x@lfDiU!n&9FS9cTZ@OiW&gV?n^>w42` zaEt`uR}*`oIoiWcDs~swFY^{R9B|s4%RE2#NRf*YFEr!(zVyu9QshI46O7)jtoc?F z;%dkwc5;Eci_LQuxIbNYave=^Rx%1F@%^iG@_`yBh2E6rTbkS2odL^4=S_j;wlhqg zvpGP^?vZljvY(Us%-L?OpsicxQQVxCVq}e)bf;onD*v!2&lByX?WQ6{o6Ktm9K&Z%huPQO3_oI`vx;zUF{AG7 zObiJ91CAp;wCNhSIykgX?8#lcwT)OOgw#a__Hg&O?x$*Rs$D@&bd#yvE>@>u{wl z7FG!BA2@yUWwdB^F?`HkPstpCqSwfoXNK+2c%s$+(DSi&D-<`+6)N%@eS&2=DZCHesE?k6*&^zMkpsY%bK2?mXMXSZuCz zf1WelS1suMcRKaH6tl^1#&@bK!*6kFdeqH9L=M!=zqxCC)DLC7`w)~*KD}Q;Loqqpnj!Xe}>uMYg}iz?|!G@PQ!ia zv*|ZrA?Bro5$DBxy+ldujcDgwxRr2p-KN;lYnbbF-Oi6nlJuY?)nmLPJtk5mq>$A& zXOnS+X(#2X(86iUofX1|I?R(qv)|%xcWBSNW9S8~y^4@2>Y%ycw62U)qPE0PvwyX| z*;hgmy-e57{5Laab)+;WWG>KqR(cES?n}Q>T-9TNqzU7wcf}4pLZAg*fM2E@8t^0P{W^x#_G)oZt$5AM(cfbh_NtT zlFiUc_+!3T!Z-T9!Cay3t4}AKG@pp%TCzZbA9RS9A`eK~-Y8aH1{qH#5>ie=(Oxqt zAvL6$NRd5Kk)c9&IUMcDA`@LJnEIQ`^~7onHn*%MtJTd-TvKyPx3Vk!a7LE`bQ8V_ z?g@2y_U5KC9m@yI<49iXp)F;*C?&5X(O$+Z=gd~vj@X}bsQZ<3mKAm`Xl^-^O3TZ0 zz7t+J`Ff|?t(^IajsXU<(4v(?v&fstHcMGwqH zg!k3Pus%|IhW>{T*cFErUB7h`>y)6HKejKx z=a}u>vDUt#Ykdc|EZg#H;nxi-y^pI(?Pe#&tfkbR;e5fhrb+MValKHlZ(8Zium{Zd zv2v_;bY!pDLM*2QgYQ{E=XVx%Ea>Yto)UYt=Db}G)TTQw2Xk7VbUoQ{-_GL#bLbZ? zrsJ6j5Dp9J=9X0cHd)ll?hob-?j%kPLJV?g5nF&&)>upM8387kuY~o+?_)?@-HpDZsOxs~vgH z4es;Z?X9X$(?_9=i}gs%JuMSLI{{MyFId$}23Z$p_2(EUSezuEtG=RcgD| z*|@%zlPZgsudfSC(yc#;_D){jxKWO2J`CTa+D8_8YE!&f_VLaX&(9}GGoDycNS4+eol)uOo3x$$V#P%A(9C(>07p~I^Sp|% zqevkvIXGjcXK>QO$ux};xsI-Qo%`>lz5PAEI4Qt=o7zM^oW^;-vQca~j}9vybkg(X zrRnG=5~>NS`lrdzjIhmB4M#&O!`1RP`$84{1`EP!^VGVO!2nN=tR_bs%22MkyiSVj zOZ?>$4eLS}8#-bxsrxFdn7TW}bSbQ4fDNmkssr`oFyUX3n68_A#Oeee#``7;%>lEY zBkzr^E4rpY^C&R6DVZOn91_HfYPi-xnO zvZ2{#dz~+A29#!=U$-%AnM#7YFPY6()oln9UVU5 zu(_B*hbm|jPU{Zr>`FPG{c*TbhR>&G)2{xtCTm)hT)J6uM0APTgu9XYSvrrsIEUEE1(&d@`&9+ zDfhfN=fa-aJXfxqa$qNe*vXeTb0bow12!Y2WYf1YmOmuh!^`)lJI(e{q{+_KLHxgryk zwJDB4=P1=+lRW&n5nnIm-ZBuoG)_uVgM?=-bQh5#p%ISwdX;22T#5aLD{gm?^Oe0B zep0RoGF7iHXKY`G%Sj9}K(r@d$sj6n7}Ew*NW3vYDl$UzEP{*ru1iS#3Yjw035Rtb zERXiyQBl;tv2Y&d`TQDU%?TRWMw6q#xmc<+Ok8O?;ZXEH@5l)r=;j0Igq?P$<8tW3 zun4Rq&f?Qltuq~Rd=%9BnCe(~rtn$#u8OMuy%lFem9BJ)wTen6zmNixOZckA7hCsS zHD7Vr%T1d_h?NcMfDM;WXqtB@GPoVbc91NqW^l(7+b?cUw^X4UrO5r#O_m0pnZDHV zbhl+&^ES)&=Iz=Us)~0*$HG>!uZFQ^^c&qqa0Ksp>0BO)ml75^iFh%7I6O|c=c-cn z2==lE!g)fzoA=}k&7K4gs(4L+KTnP|1^A=hfTfF3I5~36lPN4E8=5Y7EKL`ixXO3K z#7JcMgNl0!OagF+(&@N1R3W&% zosNc3!m^ZDh5H*j9g9K-yq!wLJEiW^Er%TT(D~Gyw*$Nd`+uh+BQ)rJ4N{vrWD$7x zYr1yyxtVZPQbdZ93TyCK3$d02OT7i|1CEp+xZZwJ$^;pHwZrOZZfSSu`%}$~MQ^{~ zeqQ}goqpGuZVf1#*Q;?dm4;TY=Vq@J*rc2#Q*F%yG5){_JaOk&`o>O@TlZp1jFY(=d>RO@SDYy~3?!gCt z(K22H4^^L=+%1leh&=DRRC95Qwzmf2CFKya$OJ-)PI`|)W{dU)%qRRdav%BF3+X!+ z;8!z)I>u5%K7s%31T*bZ$a8rl1vFtC>d_F!EJd!15xxU6!AdM-qPY@nXoxYq4)g&! z3rnV%#g3LgO!>A>$cT{F>e5|~or;~gtiic&r%`EezP8htlIJvN(zm#`7&44m_iy>r z7AZ0)^Li%WN%MhDStyD=kIubQ#k%=GNxc^o+EuW33ljrN}QOUlKps zFTNgKeZB=kq8Z&OW{!+=lZuH!Gg0%wJ?g6Yla}tTwwubBV7EgR%64biGy#?6eD|9^ zy=foTH)A^8O)AoZY)x0U5$rMttdFAn;BIq)l7BzU)bPHq!stYfw#|e+RqRQ{7+8gS zPOaRnI1%z4*?q`2u=`q{eov0?$Q~9U=l8VxmbEeMOxH2+*XU1)SUACemR>KZz=2AU zJ7c6|D5&T%F8)YIrcMl2$#mKt515sf`yfNmxtix@rd_)$Z&wlzBLq#9uIU&9tFvd9y}m+eb$-2zOs+^Na9TCZt=5cV zt=6pK){Cu|Z!d%lV(!niE(qrJ|Hd&csIXRHoF7dvyb9x7{k?JKs#k4I9W+>H2aVQ$ zf+M{@LC0H){3S7Qu*5nI+%IpP9xS%L7Ygu2bzQ8}!wYJ&!jm;P)fx`2vYLZs*1v{k z5pMAEZoMfNJ5H`yUw67&ikwc+KGoOju}A4F>PpB{BQ#AB(iE9bcXQ;k@GSJpKNFvZ z87>AVMGB-3!x@vZT-Ag*PQ*`RJr2ddq}H2Sf6*Fl%`h{^&teUa^%U$a+ZSGN;B_325A*ZI=gty>ujck+U_s4n$k;6K8h zm<=?fNG!h2&+wUMy0?E7WgaGzQlI{re~vtdQZR;m5WMFziuR4(t)7$K=RJhCZT;u% z&~Ts+O`*^4XHVYp(%Qh&-A{PNN({xH_uj&;HTHN$OQtjMkI^&X6yZ?^Gmwa6OU#V7 z!U@O`{|c{I=2#|WgI6eGNM+9#m(}}J^}I*n-3N+!wfFcUUWO`r-+(SFwTA&O3Z z&Lq@kZ>&w)7=L! z7gicL(9r9L`)V5nrqkeCjrt9qM(D1nM(O(G_r>s1t1?vOrD?6hpM;lK6{y+b>Ih z*Nwq}_rlj(HKE0V1^Xx)YN$&@vLxPH?KOCU;myd;m}!3H@aeD@dlhTuy~Z}1r;>aX z-ho)&t2ATh^L!ItWK9cIBIVW6RcIx#J{M%nHFX!l%dJlZ7h6vRud}kDGV`pu{%|A8 zsq`iyFOMk9)9ZramDbEqHL$b^q)ZXgc@*ZVy5ENntMy3p>PRHK%6cF)(@Q25d%w^> z>qrAeWpz^IH!(U33MB<3nrFf#{wk7)^v0R#DOgViQaLBoomQ&sw|R}w*4jKige{n( zDpT}VduP_pl=NQ@|7YQHxkq@6YdFlh^L#O^vDYz2{BQv{AKW&$-EeQg1>rWq zWndlIA+danHFyB)t_kjkaG$|{2;U#W{RA%BqacfHN@DQa&6GP%L%CwrXX6OqZ% zcKv$ehoL-BCupC07TFAbWmR1z)%d)P-7*4RUWz;}k&*>Lp48bo+m3EB1c-&1wJM-4 z+0akKhoa+D{N&=YMe}Bos>PMS^XE|&C_ojduPlS_N6}IJeNf^C8UEsW$;NM>+-dWM zqu^AJZ73G<%@;R(j&(aVPRyU+dqX@H=CD&rkryQHmO`NebKOjqS0y4J4@aPLT6arh z6?aP_^7QZ}q}y}QLaAnzN#|z{ zCnC>`FkK8e>G&qZlU<}95@E+uW~MYf9A+%50=*dT&%gsy{!oINE441glI$1oY<@{u zmkK|63BE1p9}(m0S5qG3gHyDp7R6r&-CRn%wL~;T7x5bxntYU^td@qu>@T;}y3{U` zw#G$%w2*ySro+5eK4;PPzO{zRI8iA7bHN{zkZ+ORN44FAe{~IGBW1&(#zNnsjH3Z! zhg%3&2bare*)LeezQhPkpMzl)3?V5$kNhWKTDGCJrD+qPfA3)Lp!)}v5&t>&AlPP(I>IsxVeRCO zdE$Hmib<0u|4JRiL6%*!V5v~MVll#M7c8%u!ExMN^ZL!r+jjakaNcdVY?`N;D}Uy$ z+q{0~yt!MOTU&hF=4m!TOUw7F^hhE{I7ZOXJMl@+t9DsF0; zF?06%bsM;4wRP7oU6QG3*t~wjra9dI=)O#iV{=pM`U=;=vB*VLvldi*+rJgtt1`iz z5gY-Lc>xx}-1e#|Rov;{1WC3LR8ta39Hkp$gCIC)1FK_y68as==Qd8FKi$fEEB@t_q^AC-_?Jt zf8@-DpFX<&{{MogmvRF2;xCOoLR1V1#MF>`S(YJG*}_mjRYbImvWQJl72%*tRm3Qm zRMkF(nn-C%%mLbhRMnlVoqkyQNmHUN>Xd8Xzk4)TH5#lSR2xLn%yhw&7d63Ni&mIX^0vk8vUc_tI+@@IZE=0f?V%fjLlw-l7H?- zOe$#ILb8EuCoQBIUl+NRY{R#e+(??_kVRw#LKl*)$a4+3oy>*~*-5S@GsrZ!YEpp^ zCz*-=3OS?}aWu?^e+M8N=$jJ|O*I`~ha5he?2&Px+{d>VkpRnb-|>DQteR3;2}$(- z|NH+y574nG1?~GNAA2`w1PKss_6&ybN%#G+`>vm+8T+H~55JRd3sw~kJ2L&-vWk=O z&n4r=>Y(4>{U5?;_{zd>^M5z}7)Ny*$0u~>vkteaeMGyK!wpO}ol3I6g> zfx95-zXARt#2b?S>)|)SKOyN~0)IaI`APpG_%lKqYl)?BC$y46DeAZiVI-7dy4(#( zw<7_rex;q*587?=-&DXy;pW>rkKk`$A>bu&H^6O%+iMT@q<9y3GlYE7=*<^@6tBj4 zSFUJ5sp&zEo3?T5Q5WzG>xh63H(T_;<* zy7_&hwNv@qDc8Rr^=AgJlHnbIQ-giq!DTghF+4|(+lsi1;0zgV2AmQ+dL^Bme2^0nlOe~&MAsZ6uHx#EXW##Ca;6$kNk*X4@uMsvmEac0)3CT{&PuQ6rh?W=`m zK<8B>Z$~jIwA`0bZf0<<3_k@}9+fL~tR`<^RA{>6h@+!I;SRv^s3gls$v;#l~Z9r`EXQn?Qx(YY4OhwN3ZTOPc*;bvC}$p&R@k-s-Zy^Bk*93@lht<(m-oMa&X3a$2U@xp`w>zM_XAw6_Vb*!i%G87NKjFDbfPGJpDPco3 zLV@S`FbmBK!#x|G4PPvLxsbI+`sZ7w{5MR?fob`7F_kIVI&Zr?tH&_9Cjp!{o@=Jz zt8tn)9BNL?~A{8K&cW^27;O(GtQH{evOpaK0BAvSP*b1p?qN-vED2ZN%V4 z-eU;4KnAJp4z)3&XKx?Ey$d%82fdX^S(hVLNJ=+zka%-FP3-|C zoIKGVY8RLXF2>QK!``_9dVZ=mxw9C2eSxNK4q;k8hot>4Nxisvrrkyq3Xf<+0_wFWUPt`snk2e4c=i0bWPtcBu!N3<1L)SQx$4@RJYn?^ym81{Nx}?J0nnc2#@6_~%&aDbo9WJplAd#Y~Z9iJ8pu=ZNQ`oM0w8 zK<`8+CZS-oAPM~`YDz+Xh#D4~NhQ+#E?NzIa}%@t#GE6(6x9mEO!N0dvy;%NXl4>R z8O=ySKaW<*=^u+SSJC`m#!1D?unX#|%|KfqqHa{@ zTlP(;1T!urwwKz9aT9x5pye6F`OwX^XVyq%st{w-TBFeGsW4g$HykEpZ_lP$6~VbO z&XTAG`6KMm*zq{8Get1ybUN$@Z>@XP?5(WK@vuAdoQf@bdrKUYkIbq+dPlxk5KSim zGfPfGy_P9Dz#FB3H+pL*g$#PLfy?|TrP-sr^{?Xj;(gIzFXbnR{Xsk3JGA0wBQ{|@ z=||taBUKA3Q^x_eGEe*!PJX5e;Mrp-V6bUPhASoIiH{8Bi|eD4gjCa`cN{hTa$>Wi zd4Z~Eyk2h1g=)2bZtgBZ19VtKnmXRkEmiESud4?O)t6g$$v z9LW{Ez-~@7Q+#+>D=1BkcjSt{9x`JOJ(?h9CBBd0yu@kYN01GRB zqf^`aGUm>UaA|JLvbMDP-)Z-01-sL}AV;t>X1i8zKd;a^1KnnyR$l9WLE5+As09P& zbhO3y*gfc(r2L zNSa&YTDB+6&(y3otk-vSa|SA$)rS4ZP3dybugn#jT)hXfR5Pdp+V z-aFmgxr05CyN5k)xJ}uart9=5I#+qv6KC!}cNcqN>7G@$sXEWWpL>U4JN$R7!tdb& zo%`^6MSRw2SZ-n%T%4);dpS>{?mI`$ssJ_Isqx3 zJiXUY5ZYlnk&71Z4jKq!_R95mt7z>lfNxILaWmkJNqD22rT|!N0(46<^cKLkCgEEF zZ%)FS<TQ5-ONQPCczY7w4tPfr-XW*vklGJu zXEJmr;9W_07vS5I@a=MHGg9vcv?m$52k=!E_DNWcq8Cc#UZ~}+La3gwPZ2%w*H)>0 zi#R=@@FKwTlJGph^ONv=z_~{{3z$Rt^=6V%b^5^&Mk60})uAL8>SB-6K6SyjDp~U? zz^g5}a=p27y{i#o=+r-HxRE(*>|Avt9nUogTPvqp3r#zJirzw?>rN{kh{epfgHW&Y z+M8m2y|bq<^o!Au2hPZkQ?Iy zbdB9@99>^@4QN4QzrtQ~7s6;AD@W^S7_Fn@J9Su-bv%$Le^6uQ44WQIZ&**1#>czM z1A{1CBbo6#I8^e#)14X|eOxJ? z^nON)>l|6CgBjxucf<1I8gFGUW7Au|!k#k}UicS={HHXNlWFe$6GKu2T^o1{?5phA zKJ=AuQNVJB1awj8dLFMJ+*5Gc9*w|oM(o;IL3O=_EUKonMh*KWtr!|%W;vl>m^Y30 zH`=wm&}++d>;F{lQf9Q=4D>e#7vLj3+6L@7ZP~=3Tg{N7PKWwElOaQ}5S^|HJInPr zf42s#ZB=?`Z#q+i4&5AMo>-q?y585%H5RFgGRp&zURqvH72(wKiK9A1d9A#*H*lI_ zR2eg=jzZ;DA>?bc$qafomR@T>+cdO&>4K8ZASk}yH*^BUFabCkmkvOi}q;w zoo==$&!@8Zg-M*j=NH&&gKvkRkbhI>n0(uX9gD|83i<8s?G1S}%&qfH)eYVv$) z3tMBu`Hk^GE53w~q@Jhq5){HzUt(SQ|tr!@t^fP9y zxEo)B@9V=QvVR-C)c^AE4^X1cuN5~AuaWsYt=K)hDjC)^+?WhIKD;y;cKz@*$*@O< z7bn9ShU=4I4*@cR7uAZ+VIdhiZ@4ZA-7`E(u6;VbwDvv2>S9tR!!_=?;8w1Q6H5)g_3=D0e2WPrx z%DO?VNagO-;B;ZSdxpyps05F5DHiRW=AM>}cOpJBSnaM(`X|G$3U*sc1gY8(nBw{< zJjFdF8CM~vEq9kE{W)^l0o=to45^^R_1AET>(7{LDHw0c(^KAF0|%=pE)S0K#+2u$ zYlZU2bY6_{$7B35<$tL?6gVdCM)^+z{^pwb+^JUYq0gi9)tJYt*4Mnh3Qq5pEey?ipWa_*()qM|s1=8zO7A;#M@lIR%RUCM(VyT>U6be z+vwT42q|Df&_XhV3`mHeb3e^9o^+(2%Wg|se7mWuJ3TPYPwCj#E7Ai@^%r4+8#)GG z0{unhl`$H&6a6+;PsaD^nF}kHMyFCR>Skd#1^=si6*RH0mdX~5a8&-y7jGQ8r+b6` zDd>zjP`s6KW?B@yE9IxZkf^=gXEN<8d5R%3g;{Q6o3E$cF)=iwex_@Kx5T6s-x$G) z2~C5ROH1{(=*?=O+4F?AQV#jE99u5K&&#pp!ZDApcffKOx4#NQMfIi0avqmcPLfl0 z%PB2#ITktONjbJqh93m{UeF|*3wK+jYFbJj?5jqxxl>Lz;YvCtI8H7lR}Q&Xj?I$c zO@LntW(mIvXQ7q*rC)}}))O61SaK{Recpv6fG?rn>lfmD0Pvrkgh+5_;4V1Vbu)za zqZC=5$QM+^=*PA?HOP*&SW8!4g z#eh?40BJRJKWxKS;h_r}i!fdCys33w#V* zDBnZ*Gum#-^Sty+hWrJNwsEW-c_QxNp`AZX&~|>?j;YCZP+1KU&~(nPHPaie7o?}; zyQQ>!=jHYVuC(vJm;01l?w0SBJNG;N6PELPuH@JDw4KTT*K&K*La0QnbSaKg`Bwq_ z8WQSLv>(foefx==*Ks8;>G|uaZ}Z$L=dmT0$YWP5=b>w`>`Ho!J+a@OTB7L(FFB-D zW`K1Zr_oy&i2i!N2~*ojCG2Jzq?nn5 z?5Y*Hp=mC~LFi~j%}|vKMlOJIhIG8sdD+0U&*fw0A2?q!-r!VpsgRQ+ z?BVx8(eE9aT%SeKDryGkZ2z@9#=$r!zLo8#5w8%CnnZY-)nJJjxnKSy>S*X zN97#9ilbrrKL-O>0-I{RMf3%x|30_$voHaBJ;PS3Ip0;z}x)ND)ES_RNZ@3ROW3;ughe?<|DXYre!xn_T{W zC=of3NXPk9UoD;QEx_@0>>nw=rugj#e!H*WH(tiCK*n$U{{#HSU%^kC#4kTc>6|K_ zbp^lS1f@YEBN_Z!r$$(-_j;ws*CTX|GSkMF4+6WnSOvK0jXmqTxXt}Dr4yQZp6r%s zTxM|Fv~2OEVIHfT#o6;{>)%RHD)nd3C%TTu+7;#=cnv~c0Ume4q|e~b;8!_k30=Al+`^rK9FLErmQM?1i+2sj%r?kS1>U)nuQt*X zi(aV`GnPUxN;SmkRFcaE>^!)cnJqpmSrNC$Nn@Xsbj4RYKQyM2$#u6GrO53glNP5< z&FrT$>b4OwRV{1(d?TrYjov?mGs@oxz9`%1Hpp{s{YY8`E2q6-gpQ{pKA}OT@gr&F zY(Jf`%SPxNoFR{P1FX*IDxh;?(Fk1yW${Cpo3u~Hy@q~7-=4sHq<#7jX#02P>BxE)E;j_ShF>a~_M z7e~^?zm8;BQf26&IE>a{7eGVly`R3?I`Bnv=^TJw{`>i!zmm`YoqUG)|I9bO9@!yd z6$kU(dNTgjiB@)in|=_7bw1rq%-P}+(1ny> zC8scZo!R1v1T!lh;vjuwiw?OS%5T&0&|;svJ~mDm2kG449VZ;L8E~_PYRJ-{Av5>} z@7IbaCD<7ce{g@cU`*$&%oa}%X@oZD63bEMOi3-T>$ z$buS)Q$b5Qb55|^-VHq{`aDm(G)#D|Bt2A2xam*eREe%3=<`dI=b35Wghy|m5Plf9 zRp_0Ovyu`NhQ1H+VRsS3Ibo;KEB0ji_hUze3|FBFUhS$xsZS2$gj(KJfxI9 z&@P(mcR?F0);N@NtmRqblYXy%uljwx6ls@87dw5H@1*0)&k0ygF6lKx|; z@sCOPr+`s&;H_|}eD?O}kn*7FP_{TlQXKSvGa&=}!+gtQ-X?Fh_}0iTo_n>wfACWO zpFd?t1?T`>|7mquxJC{dKh$h|6q+B(qe&6%FdfMrP_N54d)qhc>n)|N{H6A)+o|brj zv0H7$xil=vp=a?gu;=)wzKjj7Ce#9JwRSjM=qjwIIVV`P0|kNxcYQVHhe0by!JV?^A_G+x}UXafL z=vt!s37p*})3VmIpwgNmryomu6loR0H_1BbSs&GHXnXRo&(rc1*35xa+%dq(B34smUJ?NtP*WM&Um43NtZ`ATig}hh5c5{picm55p&_gM zb2+pWp_ecYwEwTcSli*|!pZGI*fhZPaI@fOUlz&hF*`Xj(4C6Ly$#6Kmh7?JFf=zvNv6B_&Y(3iY=Qq=l{boQRmm_{>-cXtk%S7}z?ndA@6MNqw3E0T>Z5omy)6nntCe&9-Q4XZY zYXj>a)+$n@L*4^u`)OID=gu_zMY+6>5>g}&C2aT(9!j)_o_J@AbLA3tpoFo!Q5ly` zIqx5l&jtAzG7I%g zqo>dzs{PCs*`a@gr%x-wDfPLK)qB}6Wc}NKMowiOVoHKYWd>L9MRbbiP4pAh+l=8> zi9V+~Ln_Zc8K-TmmGRsPNm1J~v%cAMZto22vWFanxb;3giPf7?+_I5d`}=5h66%Xq zCZXR%rzW8nq7_N#xhOMjaBp@=T0ig@O3%ufpjNB%HHHP8rD#%#6xkTp;>KVRZaGL1 z7bH$8vL5d>Ns;HJFY&u3@kJPV1G%jaL_fi8gN*CuDCmz2-5gC7{w~u3E!zKmI!@;~9dl~OmI>>IJaHZDF#gzG0%&al_mHQ; zI$#Df{l6SyP~uH?ot-n__6^QU#Gap=VqklyIzeSY2kyy|;*BN~fhlGAgrLb{GOM0B zx%L-pNjvG-8YXP`tOX*64qB#jzF4w2#81a~+rG1DCte+f+hU-A0i7}5qk2|eTReZi38e=;P9 z{nqsl2DfQI>4_BiA+$wwW-R3B{hE7|zH4OP-lT7-?DHpm%Vgh{r0+V}ceCt^&^xBZ z*)mU6uo9*+6#yR_q%ll zm95-gro(0aa#ipV=T+s{Ij4UOJ1Nto8=aZLH@Y1yvtaKw0o+N4BRyn*p6nh7}AfV}?T| z#Eh|f0;P}Izl_?x=yW+A#!^|S*x7!?{zc)XyTJj6>1?~h(4XaItAl%?Sqw3i#F`sK zrDz#F-(iDP)_M~eS*@LlZDlG~!fge8YH1RJ=zcejVH3$!{3vDnso# z;&w^FlcL8F5VRd zZ%gM9_TN6ZHrQ+6j~n;rQQJKf?a-SM5`?Y_<2yFy)aH!#Gv(csxRW$1fC(b2bv zs4Vquewz2Q1dI5aVLvh!?(0d1<|9KKNGJ=HkXkatkpz{8QV^OYu8UICiS--AWQo?H ze}~8N(LL(|a2Ul)iL#$bn1NrG_*<0zZM_l9>n!m{F}%G{-{_gqzaX!pSu)Y9;u7Ffev5TO_wo#G zR3_hz{fv&89rJR3|`XYg#|m zM^RlABp7+$_&vtShjB`}!pnn8q`FjFB`HbEgcn3IqjJj$4)HZ;g=SONPQ1;tf)O&#SivzNPUG;hl?5)_fpS;(&DsZ-s^=vJ=`o-@2XE4Xb>Q?d4^dIGzH(?h_b;y*mv*@D_FrJ@+ zBcpwkfW83dDbiMX52XF@I{N$_xKH8CCu#p@;jWQc=i32K2 zL0y`-1apd1Ob8kwX~B7nupZ}0i#%S6&@HfJ!AmuGi!l*t9inq=r`#Wbgs=DFUdDPM zl(RUap~_!E)Z*Wv`K|J+ENNo#h|RAe<2W=Yl#i8tGA{8p-=D&CZBrcSTHXdd+l!dr zFT-_U9d{s{;%bgUS}5v(MlBxMAEW)VaEMZn1w-T9$H?CrKLRgh2<9QAY)EUS7~K)0 zeZ>R2cqD5?YfeONi&6g2FhqI6V!01X67h&9#!TB%ejaaOQaq|KUz~K#yu*-2e2*Vz zNFDSl5xm(4+k|4~;KTg->3BEGp~L$=TWmVm(Pm*iWR6%-_Gi&qDEpIWDPX+kfDw;C z-bnP8kWBF#=|VVQ;p>+=c)S1!9bi|-qP-Ttb{^V{&x3{F{rkG9mM$I{AB#YvVelCQ z174p54L8_4xv0$q>F4jL?Qg@h?LBgB-BH@=XQP#%xL=Osh<}MedtlLuby1Zi5!n#S zLhLg#F3&;F`eJfjeDDTCuKo?qPSDRE!%;nCK4{A42&Z(E+QW|0(Kz;>;)Y`3)4fJN zTgHSGcwgntZjnH{zB4c;xqS zE7>g1oZKMA`=;pT{vM3cvga7`G``f9f!Zwjdgutuh{w1>A;9Q2K@TykByW_E zGj9|zTs-nzJX8ESkwTzP8&P3bjz?Bu)zpq$1_fI&lnK3QzFAYwcDUJBb0Vi`zmUeLG3eSe1Y?rO$W7&p!6~Wwo^)? z^JR*UB{afgBzTPmuNJq+mSu{NFG)LT?|mw-zTe^ei0(=B3`&P}6YZITIYT+06=>UM zQ8Eedi7qMdXY;q|3+#%Twa(r8<~Cw=1Sf<0`jM2v5B?w#@kNW^+b?~5Ln3l>)B^ZU z$dC?upGW}zNQIgis=cSY(u+2!9X9!LqGO=-MHjS9|Sic4IcU~0Z;sL z-(&}O%hXH--V5gkLz%!(EoI<0Pv!!~V^=ws$l>UoNB8q-vYbNkqvv#|F!vS$N4jPf zV?Al*IX5z_#+-o*{HHJF-Nx*P=v#16-BA0d3@#aJZx!++6 zP7%iNR+d(URAuT=8E}0zLFwFKsRZz^5_I0bG(4Bz=)4Ri9m7qp{#jwZ@3`IKW=mBA zl{VwfuGGozFy+OeJg?5F^{C2ILvwl7Ow(7|3_J1C>lP||BqBe=JFlwJ)KEUk-6PlG zk#g(a3X{_GP!7Vj%3;?@*>$gm^St|Tx3NFWl>RnE`)`9BS0`!eei^3n4ec}QQw*67 zR|LNc-xY8-!QBRT4_rH(uV-%2LZ=!QA}0Nq1}PtEXa7+ydl*k$z|yw!p|Qrqy)>yCkYooiVKX^oh> zbcT%_VX6(yQUrLq8E*7l7%5@|S3}b>uy#BT$&-$89OF%C4Q&e@_Xy-Q=v5}oUP7h6 zt=+S93{1>u@19Rhdq-G=L3QB2J6y%-;})`&+mvb9D$ z5|HK+BjQfPwbN!%OOBcG$VlQBu;hrNzvzs3E~en@j^BhC8)yB!JbI7CY4{T{misbH zXN#nJ4ICqtIXB{Nay9mEya?kv8fLf;uy2fw;cu}oN8wT++vm&jeU2=Xt3%~Bx|dN( zdYfFbKb9#j9MNFyy=JI_ZX4F1o&uDbDK3&0I2J6R(0H5%(6_mKy}9V)e7I3vq>_w8 z4ktL^@>gjH?;V~$H4?cLS3k{wJ52>&@2ubG)moZyS4*ls=CR(+Z{2u%^VXBMAKRJ@ ze(?f0p>{_M@0Zkfj@WeORZR_@?D5pYolO=Z!HV5=hg5y-_9dOEhYjlvT9%S0*Zgex zwVj>omUf<7w~U51cO*lrVK+r%S9EqZryhPJ89FB!%E)oocN*5G9&YShwf-@@*YfF_ z50=;Rl8t^D`L{0Vn;@+9c6duoS>hLo5%YbjEb-TIdJ8m5bYgZY+toHjJ8NU{W!lqg z7!zrS)rd{wP8E*~DGSxXG>q>{Bb4L&T|5;hK%2mSem1fiw%?R@qU(#0n4P}~17XJ_uF>A3*D?}XIcfXA>u z)%<{^WBbbOONoB)LU9WBIrEfbN$}y~3ic_-YoP&;%1mQjgXxye?d7;PuePk(er_*) zi@DTo*bmp_>n(K;g*AvT`cAweRD}2<_rJn1SS}8QX-*BWJGb}9b{4N8O+@TO_xs^| zcTCQkpUkU8xWRokoCfUFX2X73`vmu&!gvcC@PMar`wq+N;Zex2pICk?>r6e^SGK~@ z{%nIm>N_w_1q?g3T@KHul5}DNA;+6=Rc8(%k4`0cs*8{pz#zVbP3I7c7dW7gDY06D z4_mq58<;br_tMYcOYf!U%JGYmDy_Zh!r_+1s^s9s)YXNH2j8F&Ua2@Ul z;Z62Bg9mf~=YPK3e_uiG-V@z!r3i_X9B zn2^+jgpT62j8WEvG;&%I_X!Kx&?A@!PFT@oOfG;nSJ6I?-0D0@KDwq|cvNrmkF#d? z`@A{gha+s)J_k9&9y|pr6IC!rJU^n4Llg&@>EkOdV$BtkM%%q^RXg(#PnO%1ct20s z(Si3vGbT5BnMW5nU#`uV+*CWmd*7NJ&R5p_+QFV-j{Lx*_9$(5*{w^}p~U>(=NN?a z`PePXoLk*^2Hzs!jnReBZOYa?MXhMR6S629+yZDG^av!r8PM!u`a5IzR{YwANl^x0 z=3VA&cK>b7INSq%X>c<6xuf(wFJi|JH=7=H6ufsMVC^tnQmaw$BMtOWd7%<5G_x>j~BbJn`g z>ehH^xff#BAnqSXmm^+1l0w+7M;+y7>Jj#KjF}M$3xNI_qjmfjM!gokH&8<)Orh5h zu8_m+@VzXDF97tb7%XVstpU^>yU9drd2#r+j^DgDmpm4pgBPaak)NYJ@XfJVNOM%K zJ9Pvv$#gy9P@btk*u#)}BH;{+vTK>s2wWfWlJb1KD-IpIob%oo(BWH;PXa0|h|_v1Sa->&1MJf8zFmBHxw_AX5ro*b0kx|MIuEhCP*mnanLaRUwnND zT$I)N|Ct%~MNmu!7-SYu6jT&c6io!L!MuR2rlLh|jhaeUHfk!R724uL3reMBo0&JG ztZp}pmV2wK-DYgVrNTtUwqLI;zWdDo`&y&U2pUIsapr zclHs#P)FI4#d>hypJN?Yn2LW1%mYnYzqNZyanV?7rzhK+40yXIeb0K=;(OBT*EgnH zt!HfCY;dtcYxjmRNMrS0NWUlD)RHX5BAo@kCFELnPx`8KXI5+amm8*6U9h_N8R-+C zimE`nwn8s1v5}1=iJMfGQ)r)K8l4IK%hX|#AJ1EcVg)VL%P||`#qDYm?5NTl+J*gZ zytr94q}}QyK)hS0IOIceQz2bD9w|2S6f2S9L3M29&jDRx`k311uQ82D8ZHC6!(0GZ zv}0lUT(?h`4yz+*j1`N++yb7KmXFvi3d^0pZhv{duXX8wO7u&)IvCGqDH*~Guu*Ms z_+}c?1=TdhkQ}hOr%SukY`m{h&e%FO7}9i=W>z_J$!9r5opV6bAmt7YkdWpnCJ_-6E1AB-|;WogLoYm9t55W-) zKk_9!L-^ltr@{4Q<{;$!bmSJS0|=*jUxU0MO2gD$NC&*6d=hq|AUsh-_NU%MgY8oKln zPJRh4Ud3L~hx#t~hG^kD%%eYhd?nwrMEi&)e4l^+Ps0Ho?k8%Jm>X(zfT{*>c~q!z zFv~?R-o6T!4X~3G^T2d5jX^J7ojUxF{}{t?j+s89=I@uoJ-&qVkM--^=KbI=s%n;{a_@b4wGg2?T>w85-(SPW()4{Y0#xc zq?xK~(gdCA@fh4n!U3Swzk?$h-L!2i_zlFrxuUwcyc&807RU0@rqX`U#ot}-@RKdY zC43K<3d%K4t3*k$;H`jf#A|ek@s|jPHYmj#N#e<1|G`s)a<{cUyjuW2>F}R<3_u<+ zS}Df=Y>4vsNFI;yx1l_6Lb~~g{j87b&F{2cP9BoC}qpoCmRQ*Kuy-VtL-%ib|lP z!SWOE<+Iqc>$oq7>T(Bm>^dZxEH0!eHWgaS^0-CiN0Dv^ujfvEdm!8|ANW%v`{(B( z{G-F*wX~yuNyda^Bfdr4j6K2Sphf6>wFPsy58{*H1h^~UnvnJu$V`5dRoFmlhaKf# z6)MJFg;uJMX zT4w6f#L)bhs4r+<66}o$Ex`RbQ6~(kOie<3ICelmsZj69B^NL~U%usVIe4Mejw|8c z0GITsiQNwWemLpSJ=05J{a2TQ;DwI|mXnRUU}>?q<1Rjag&3zPa%1mUYTA1Ddg#!t zuQ>{9gssu7(5t&cn8wG=?5C2WeRkmFNjNRRyw>ofndQ&&B|}E{7M@EbF!@T%YD3xy z-bi2l-M|XoC(QqRXOg(vSjJ_2R9KT~DQ6u|bn4Q}&_qM{cuOa96?jXU7S__OBM)@u zyV@N^xYehzE#8zYR;#4R-_&Mu{Srthc*GZ1kmkSE9cxcRo#J2xz|PVDN#`{?@YDZj zRL37Rs$+}R7^tbaLdf$jaM+Mrdt0(NSRLAMTOhxn$e#p_1);VGvM%8onxQ4q3ce4d zh)No>8rmHtP4T8;-V@2HN*sElJ7~q5L|*$o+Cjho@osboq@Kgv;c$TYeh7}{KH(wa-;s<#0brv5FmaQe z*~xwa>7>+P&rI@t&sT-<)u#{_gZZ#G?`9DSi=LYR9j|J2ok>gOyH?Z>#!scu0VLGd#Jv;F(NM4%i^$E^Tr9-B{ z(JYiRj#YqTVQSrvJxNmUEN)Try`$OuviycP*P_0Q%8r>9q?$_mS6o_lvaPFaGoQYEu&+}G7ci1&?04$-imrjhWtN9h3bIbQkj8B|d+Tzgs!7`*VJ`+~WF{FT{isXZ zAw@(OjWER&q}YHI9f5&JQD9hFTQh&QhVxGN+T|@_r49Dd>tI1T74L1(*gB89Dt@-= z(mBXR_2e=Lxy%VZ83@y-#P=s!U^&(iY9p;>E8xPq5Xq1quxvA9%VxqnyW{K0$XC?O zJBgRkBBu;)YNI+X4Nk!GE`3Bn*PvlAD_33O-wY{#0s?srBhh+%WgG zjjP6v250Bo$sz3T=_&b1rwEh&RnP8C z^O~YbzZ<4;zXUSz=1H9mdA&U91mzvV*K&XjZb&zSk{!lN013o!BFT+Pdd={BV_X9kEKV)C*KU*X&wrhbVwTf zj@v9&j3z9V0*VJX9vll@$_B-wW6f`Lj&E|YDC*hrq%;(R_o2UO)5ov2NKw%c{wkWVFb@vQ=#j}TF+Zk#5Pdx$?nYbxtVF6 z;nBmBGt(zEcDAL}lO7?RGZ6groGs8 z{_9qo!~Ey)GX0Q5LBU+pnnGIV-Oa2p0&txXmz~F-bA(h zHfU~jG+vMT{RK9RDQ!k*3U~-4MP_NyrBSMB@c6*XS9oj&HUxwBnY%y6=dvhC#^{>Li5^e0Bv4KvQIu{YAp zcF-~MzP2>PJ`JYf#PpiXtomR#KkotFr+IS*aPbRVJn#z5rMd7&!KGoZN;vvngj?aB zfg>LCTkw1IWTite=^WZPJ$exHtDV@3D0lKT|M$)??ZJG%bZU~W(MXuE4!j;(E3CzA z`!Yg>m`9}fR=K^EX^q}U&A$XK^9Zbh6K?qdvu++{oFP3K`d46?`|~-Lf~1aRk(BTH zZbGXXRf4ylua{|n?%qCm<)6q4;)5*5RdjqT5 zZn4FjV8eg)U4c|nP*X^F4^Q(=M{EMGOO{rHaJky(EECEwa>pQR3SCR!C_|crc{Z&! ztEsSQ+VpnEc+9`Lxu%JwpaGJ^4`KKGKy+8zfn?AM1;6+Ug~GwJ&e4$Z&A=|~Eez}_9bUp;b-JS_ z6+0yd*U4ID7*fB`dVV7iB>0VBw?9UZ^j%O3Rea<7mjEbqDO8=_yM>cyH2ko{20EM57lw1Gs&+(uWT0PT5N|VJs-Rl zA#(@pG(jh?aR|wFGzG7!4za;aMB5gC!)Qnw>N3?TWN6Yj$lF{A{A0?=sOei;+E4&`J|BG5eB3Zz!d1su_u zq~9Ke^D~@5I%q7`LEczAwE18xw9D=cEfLJytT!&^{l)wrc6w@ypp(V??{zM!RHbHP zkx;xi3-=-}ZNu4vxB~jF^Pn-9>FLS{q?cHe#HY2T)1tOzZh36lT*w!+UN>)AO!H$I zb%J?UrswIl&oVOGqMKc9=KKMt2Dqf_=1+^>g`30%?}A6KYw3c}m40$dPTNy$ z7LsH=@wqjp4Yu3|vmxhUS3b#sJIP>K$>N<=5!+lT%(gC__W0(3cRt*>aN0GES@%s9 zKDlz~wDi^6SMJ;r+v;zeP-QAg#=SsatYprV{2Mwf4G9f)&|rhX3y6}(3xze1rEXok zt{ruMO*cU!Yz#&#N&H5Erpm6N;G-{@Hd@%RIipdP1{=A+;q#$?Nb+$@r1P(kc4?$QCnXtB;lyedOHIy!3%&x<_ja|1U{Q&tM>lR%)DrX}G zeXo&m*jH%MH{b^7(z~(^;f_!ye!qZ(A?S7N!!^jO#)DcgH#~;D1<9dI!C2Egm^{`vld!mF=ByMQx)apU%)y_x;Hd5$|negEW;ZkmS|_Tupz8jlXfbmZLW2H zhqMR-$23nf?MxEK1(U>udShFx4adaCLqpzaQ`)E|Lv*THn43oRBWk0Ehk3bg3`QX- zRE)cVGhv%dm)@2~>6&JZjr8;v82SJ-M4JVjRcD2KG+nL+@6ipoMTJ>CdZ0Udo?e_0%uB>lv z_usBTHYh*B0c8IBJNKub+%Sx3x1*JiX;}=r8^e(65`>pT!b=fe8VTQo@J*5MGK7~! z!pjj}9tp2Ncts@KfN(=3d^5r~BkY2>k!Ugj|c2n#gMN2t~qL=}@j%`B6!Zz^A62&zA<>@8$FIUEh>H0zU%fibw*2Xm0DWw~J z_E(0SvEm1?|ENQz>BZ^^9ya27|9&CMo>32e6fFGit&A31APM$^{48nC)bB<<do%HIbj}k`RN``H1n+iehTY%S$rhxNq0kq+s~l; zNKbU>ad}Q}%a$rV-^TdnqMU2=J`PHAA8<=JAHv7fe+>7C_4EKe3-i8*fwr3!x*T7Y z@czs+Di9tW>CbzNGQ`I7)=pF|?Jczs{kh!e$t@c7>gmrXImR8VOcX|YGa=RP1($sd zFFm2RFQa=)Xa2PTO-c{ZsvyHIm?oXnT?qfGg~xN6L0DYD9u;yo@}S=GMR{$pDh)SW zXcc7d(JQ14y8AmycTT<{g2#!jBiUEN<0aq?-Vb=(9N{HCKOAv5?Eg4l{UL-0Ja1wv z-!-ux!Oyz@cO~KuxWRBlFFb}cd*CdHlOE%-kHWn95r`MTwIhEwe3I6+_*_{ei*&88 z)HB;Q0khoHd5lndaHXLupky?R&qU(g@nITGJH zGyd$EQRtbt;6}7Jpca8w`J>egYq~UF!!Z+&jneNxiZ9i0f8B%`tAkEl87Uq6i4e`0 z{yNQ=i|1<4nhftaVT5-lY>zSD&d$PWhbPY4n0{CJ z@{?|xvp&H)tm>Ac1b)`l7j>O#5M}04b3yfM(okNj!jQE$&zw-UkXx1QaSAE*CpK6f zbJduKF< zrZX^3uLHh9A;y9qnb-onYlI^>d;tCvaCAOKa7;A)0mKO&Y3(!bGqHnsPCTJBgiGPV z@Q2%tQT}oL5wz+J#pR(^?NXZ&ds%5lZSuYC8Cb8??RfGU_OUu_3vK1CW&S~Db?L=m z=Go1{j?9dPy8>&yn=361Z+JHg)&|X%SkHzHMvv{+@4>fQloI5;UEPH6CIwn);DS`x z-2OZHJ6cg7(`a9Vr|;X!{p%3lz)QO;7(TS`*PopSY4LZp zIp~pz>Z`!Z-NNDES#{Zx9*Z0+FS2O{g^%xC44L690+{|_~5g`-^;Lvv{uAHHkMP%UML3}*D30@OMrm_+X_J+E5uPDiTz2nmW9j7C z-avZ&Q5c(Rc<;K8HWuG}t{=Nm&5z9Dk;uP~b{%*qKZkzYV9rnX&k;iQ@C-5e zI!ml&8hG?G@&4=3Ou^u9PS{MoZ}^+P$#%Oc6&qKQeE&>UIubfF=ww?|{`JQ-=~3Pn zU+O(II-8LUK$i5J%y^maw(m@(THcw3J=t;Qx zSpgMyr!H`1W?;SB6$9D}a$86hgH$uRe#G0?xYR-TeJp=}J>J7kiEmohNxXmdqW8E> z65mh9dkfx=?ION6T>XBG*Zz5~z>k=#kNL9E?i+Z&nEhD@*M;a8`gNFrrE(_1SBBV# zCSN*2HK9!b4ReO%1&9`U9`mRX{=up+Zlw7b|3Vmd_RuTAkZVvc4ugt$z$o7Vy zv0v&xgZW3dJ$Z#)Gp@3w__7@nU46@DRoo`j@lzJ6?`JlPk7$jAd>8c2msGXJnVRPb z%PTXy%oQWnsZoPUeF@@XV=3m>Qk*yUoD>n~miR3&z z)1jS0yKycHQ)ff<%b z^71|l@_S7EEs!AQ8kbkw>q`)SNOgK@F~7zL#Ss|;Hp1-tZLz87w;QS=a4|A+b~F@Y zHC364GF-tTz^e*f%5ubp@e+q?uHIgCXqq%qnTU6Nf=j2hG^ya_j7RJep3#Cy={84Ltp^)dXc}zp>)UkRV;2=9g*!2gS`r}Lmfq&tT+q>Df}H`N~A zGcbgs;0%2G65QY6+Ts2I_aea$-c7`}#}My=`vH#lflv?OcD*+N&cs7s*W7!Mxac%5 zA5y9I$1w{M#Cdv$aNKQObX+)z8OD6s-}Uu=1+FJ<-gTIpXU+IRNN)h`iF-Tha9(+& z&7L3MG0`Pd_^RrxthsIp+X*VXNSOCY)hD%yo_^?$#QIU5e+m}+k&J&;ooq{NXw67* zeuNNs&)$r(C{SX*o@TqoVK$k|h8OlTZLG+sNN7lLz5`8zw6ZAF;iuCos|t_5vDK*X3OMPFrL{4!G_guX*KMYA~WnV5&i`%8wc<<)p+{9aJCA2 zC4`x8RQFat<92z&QbsEDmMNT>Vkt!(I{u0}e86Xl1yU2?ev_50C_lVMpq)V&dkpsB z+t5QO>jW&@sj%g^1L4oHccoZfa4TXbBhpE_^rfOm{efk|xxh9|YoRBdwPzTR(cNooup!qyp{4kGgD!7SrFq5zgjb{16#K-WlY>T{ z@5bJIy{P+Z#$a}hP&3`?#ck#dRKr_s;j_t*Udm4$#-8q}N4+u*qxX!QAdbZZ(1*bOAz3^X8Y}#63%5NLwX6wUhPo8!c}=QR z#P>-Pl%c&)K0+BL46bbjzj-Xq-0^!SM$`XCCrN0Wgl?w8JEk&9 zToW3Ek#Duyq0g5Q!7R+bu@n56&!sP5aFC3 zh1W=0=lZ?$=f2;La4f#dhfixA>2uP#0G%1o9^)f?OZ)}eV~j(Z%i&b%G5rJ7W~;{R z?{iB3HvfBwKdjO3Ce6}nDO%xgNBDkDk8bU4+a~$%)o)Gj*(Wac^|aY5zwR#aJT&d~ zs@H|AggeWxK9=CkG%qPzIPFSyWu0l%RoHF+iW_^)%?Ab6{Kr|gtAs;&p`D;Sts=1@ zvAYaoq$_tc42m@-McWxQAJ6C7Wna4acuhLfCb}d)gKHrxu@9r z)<-w=^;FtY>k}Kc)ogRah63h5KBrI#FUHCD@p*MwFYJ|L{v`+RMTtMDn?PZz(!aG$ zh@DmMT|%5r`*1 zVd5eX?xXxZ?9J~jXM$PsFafC8Z=mScEwR~oUhFU#?ZF;(~TEyK^;$hnB5d8i3 zeuvX~!qM74?oa~m-AS%a z7IHn-8%*1xoThywQRXqQ)6c@i3Yk%`-4crvMg8J_r?$MN{n~|me9gQk6-#9`B`!15pHw5_+%*DH`e*#Hutn8_oBAI_MrGc zB;CwNx}KW${jYSZLy6UixJ9jHeqxo~lPnfvq+Fi(s#MI1a0&5|HvSGt(m%(G^b*2# zf}I2CHyno!S*))QXp-mUc(FEQ298ZVoMOKKzIGQMuSD3rb+ClWoKeZ5Nk0QyYjJ`Z zG{%_?bT0r&>IQ-1$THd&u#84)-|r|R3h)&TCmwoLxc6WNEDx-~*>KzRjox+FT0kBC z_4MRYE6dC`9XE+Lfp!oy8(R{0J?^l675rTl6N5-&wpd&od{d_FOyp;6`TXpuNrz3zY_obJ@g2| z=lSpdgdRrtNB(;bRGZh22&E@YZ+zzH(6nKVVZHRNFHo!8+OKwULCAxDyv(SS1(oit~<+BDegutS6A zZfF8+4Bi!(ftFQ-=(j9%4Spwu%%I5a@>K|r3o(Q*l~)9=K&cSh^DlutwCRv2zpSbZ z(&WkF5w#ljh@V26Z7AbIy<8yc8O%QeZ2{L)x;$j(mS)TOY98fV?<}brjhej#o0U%o zE1=&u5z;z81xoq*1Yb#SI^39wbghsXeoR-ukH-!Zyy8T^B3-Uq?21&Qcj4EqJMdhA zJqa3{->}a-ugW+V2;0Ibk7xD=|88Qt;ogNi1E<2d-ZHUia5us|4EH+RX}IXaCYB91 z4Q?UaX1HhI{tfpVT+-Vn=7wv6qcw@(=tYEIgX8=-_;l9$7@m{t`6+xQF51g*yRfz-6~%T;P_# z-2?X=+`r*|f$JFqx;KL8a@xz&I!icqz`NL=!#|H&y=wsf>muFnXvN+N^v#yg&)A8Z z@<;i9!uywvpM;;lUx75u@gEU?-JsuRjUQ^9o_tRWzIhDaG=u_p+G+%9ot`58bUU6d z3=K!@=Zx6Tzr)-6VP!y(?#A<39HX>g#`=P%@a6&I6fBh~(puvr_$dnH0#87%P?2si zz#%|akWy&U`;&zBpNcCK-Zznu@e5Q;Rz_>{-y1pkc`(~W=g z*fqgV5UVtf@>p%~2x3!=BRqCl@Lz~cFh1t7vfxLE6&WA#*!bXwh!q(B!k}Y@`Pf{S6GM2~t2TR7hi6=2e9^NJg-$0ld zuOnaLHK^Z(N8z`iB7Mk9i9sos1`i^Yrqk~a`b)eHM~%Po9uxHgc>1k=fIoeg_i(rV z0-k-Lzrdfp&YzvqpTn~w`g8o*0sicm{xqJwt7H7NX!O||DE0o}9z1(px1&B$C!Rp~ zWzZEf%N1!K-mVL_;>q)Pat`CZ2TyM0wR}a9r5_Dig`U@G<#|+p2sZ{{+zeWQw&^=~ zY^AZC*SQIJZ4=;PiSZyGhCa>uVeDNs0xPp)LD*bMx zSg7C4Q(R_jLhL$yBafBw^w%Q&6<86*^Yqv0>yV;GUni{Nd_v~C1M#bHPO9L{crE`f z2OJFr`m8v4YEq;uo=Y)uxeK`rFc$E7PS9^bnxXnFJWZmp5;3O^u0Otr;$LLqi`DqT zfQ$pZqj{Q(zL{$*M%oPBgWq`F!!dV?aU)`8eNnB&QRq>mCyj-Ot6IIVU^-4+VOejK zr)6e>_?2whMzU+4cV3V8Kj8MS381+x4zj$bz((b(*yE1?Jd?c`!poBpCRi^!2U%R; z{3{SAUiSj{q^EN`{4H?JaJ2V-4nFbj-hlrq-v1kZ3F5@#K8Y}$EB*|hbnndX0hi!; z7JSmTn*jeO_*cU}2>&Md8{lY-cntpE;J$<-9U&+9IY!dG$bvrvj_~Ao_+%5X2L2t0lZ@jrjKEOs z7$1Q`jL=Su&v@h0ssu4gH|-mWIE_2?QNviRl*}bs)qr;gel5S zhcU%Bm~m}7VUe%ZHzM!GM&9}KSMl7hzFJH3>lLlgBjX-~7~FwE81f%H_xDsq+M&OQ zbcfX!Bj0C4zCVb(H*vi2l1g}k%1w>DdrE&C-|SVN;(6cA^Ip}P_oF=ThmiLIeHYSg zCGH6S{`bysz3)QaGjzfjcd8HLog&?<5$0H*gJtk6mgx4ZGmeM1<8sS3Ki{ETu>7VsQu&d5| z3k55BHwSzJRT>hKA)9hDQrOTJTYwK#DGSf8(U##^KRok6$9s~x1TjO2I!oMd%UQw_ zQ7U1F-;^6+vzp~k@hw8^C!As<_Zf=xiMjysE}YZn@m2dITKbY!k7p;8>mfZs3TWtMK&iN-0}faV4kh zuW*Mk%sYx{j2%{#U+zxY!X~hhk zOso+~T0oI*(8>_LAN8M%`iG<|K&M_Afit=vcs9b+N z=Mdk(r?ZIf;gjrA;y*!m!Y6s8VFTkvmze-3^R{BKA6Y2c|t@LS-23ZG=5 zy5MVYDIY?25^fsYY`7J0Tj8FEdmrv|xbNQsj)CjJYjpl+pbWxEf8aUUo{Gb>{&0i` zZ#rUPSqOLITcVW}=o>wS-iTHv4rcb{6(tzYv&wEf(d8$|77A=UnVM~@yJEXiqW!eq z24VRdwmKEu8UYU6qmgm>O;#imhoDZG@Wdg=m+z<~>rm{A?fB{p-k;aDA@q&RoLhxA zXJmRGtgXZScPZFQbZG=F@5}dN^@5JA6Bi`?o^B0fi)ZQ*UTMqYzMiBC-JZv%2XS_eK-8!>Ofeaq#a1Ieq7&1S9 zcv_>?b35v|o5RO?`P^dAhFIetg>@CI|FUNZXaby5g4c&{-ol$L8bQYG_~vfdQUUzS zc}TMqZ`Q$*ih}e1=MkTe_-z`kl~>E>5U)dgnKlRUYPpVMnk#v(vykg{yt`hzmcIk; z&}bhx9`*>rwA-b4d%1Q6o{hw_F#T4nl#EfNzd%0-FfJ1fm#;9V8z-X7rOnt?P8LC~ zK@E#Ff`&AC8pnIFKI&rydOwPE5`2yGGQm<5ZP=hg5|Sp$mm;1ZPZDfRvh;hf-DzR{ zOb$jmYJXtISO6`58T)?*T~Ap$7oz)(2zD=^lf$_v|4@ca}YS(et~j5FD127gb6 zEe!MXfJtD7J$zUGEhIH!;0vyp3L`Td;nCOQF`1inFOcV6b0|YBQutTu)1k&~BvK>Hp z8Q|z$_zj0#HF;Pw`uU>8Rhom_0893(jy%uD&>z@Ix4QUDE}PrckOoa=6}CYkpOWwL zxu6py`|~`}xM!8+{pRZr9zlQb2%zz!LKD4;B+8S-AJx9@Gd7(66`#1BY`&klU4=fa zEH%lhR1IPKa*cAh9yxsJGrL?3p9YSi-hJahJHRJAMSI1nU(}A%uA=G4q?uIHRhrdw_}d*QZ}NL39K`{4@y~wc8+F?r`gfY z+fiad)t=rG6Cx#+C=T$UiSJ46pt5_)g|%UEr;*}}5n`)mE(mk$;_y|Wvl)EbAkNVL zaE+`jtoaR=B4d21PViPx{x>8A;D@}F2fC@S$4+nUeiJbB zq+s6kp{QHxs1nSV<4T;^-zfE1Gh9MRy=}1_U*uTh#8d-3vWWJQ-5M`WQ%V8dCH2|J zp#f5c8P#WISc>BOsm`IEew9h$vnbExFY-cD&py;ca%g>_tCo7QPsB|SG@Qhlke)b__Q=0v0a zS?@BG7w?W2T6lSJ7nkRXargBm;ylBEo@;6^ob|L@I^?gmdoRfw*by&YD`&G#c8|$0 zJ!;->7R`e#A@ldqmYB3ySM2<_)o~BTosTocKOWx}FU1c`_yF~H!fJpl4TZci&f9UQ z;R)iLFvy(m2=74K>>c5w2^sYkad;>ieY;6}3upI#TH6WRH;VJUfqa6nV=P1Oo_Zk( zyuU9&Q7!_dJ6JWjUUst~WI^L{BV?DPlfl1M#f!7m1Nc3UJw!I>_7`C-=0%|&8(ye^E_!~Bf1i6_)eETSN7_F33q16`HW~tt`|#{-?fL3` zZU+y)p~+H$V)slzol9}I-)8V|uaoyeOHq~jX(qd)6ZcdhKl*kzzTJcLZN}rm6R`gM z1mv6g`$pkDDph(Sc$bjZkljG?xhBw|4}tz0#qvP^oiS*BzpZaMZ9s4Fx4_-*d#dh1 zPHTC6NRnxlcDH*|)!hi)jNFGn%Ki)WZtz;j(#$d;y8&xDo*hw>kk2kX-u@L<(hpQM z(8$*bXvo{`4Jzo9bfllO`p;FJyV_0*X_$zLjj|ES?5dB#QD*)eSwx?gm<#Z+pwg!?^i1Nu& zW@t9biqpp=>^eAqUXUD$o9D{s@A|Dga=Zns)Ubw4(+(FoKf4s;P+9}NqW(k=*e>3vW{gt|5YOQ;oHxD*M{r3zN75@G zc>Ng9rhq5S!Zr@4z6Ch`2x;gHvjy*oKhXGNm=Cb{TzI9%miY>w3prL3PAd|{SAxSmapKRA;ELh5wZ@70!2_B5g|c*# z*BXzUuENT81y%~?i-YwO)$t6;e|etabUD_cpz3JFEx(&t-r_hhIk8dHkvg7*qde1;S@wp#BA`?fFowibOGhCH*;5A%Cgw;3&-g}H1AY0^vjodMNx zqp+mf>bV}bOl)TD*8IioXy-%5@~!uz$BEU-4ZOVvF~^q%E^hH!$RWs*hqrhs_Sl<4 zCTG2nz+3!GP?DYu=6LQ2RPtLSnZE`W&}Wc7Fiz^PL2V`2eR(@@4X-ERs;Qw#k(4D! zS;TX>D_8~I{{W>*nC7-N3aAHRBiQP~t>N+BI;$CY7cvlXtbNJ{C~_uwvH8>t}|W2{`brnwE*s){Xs`RM(X*Vb%DKq#} z04H97RJ$C=L_7^E*Jky*0KpBxMh}faJi3cvhhzkaPe-zSS&(}rejdRh!7{-#!83<_ zz|KS(zhnF*emopJ~A!xA~1T8bxOdi;f;?7h%y~>m5Py@+ED^YOJXSHkktn{S z+3QCjhgKsf&Ehr#JeYkuj5w?$w<+@i>3H{m@ejyjzXN(t!d*qMN9}kUr7SlNM@kWT zOPPjJ{)ST8FDm5;lyW~xDYnB7tf+)usop2-ugM1gD$4n7=00JvH61rzB#Qg=;htFW z3{G4LCzJrn9BAcJkye5Op96T22cSNGG4?`MR+4Tu9IO>J8EUjfY7*016W9@TK7yL8 zy{IO$P?O706RCS3a&a14g&j46ym?Jc(>JEGoCWy_UoR+rP{_%68un|PY&`n!dY!rQ z8}pmSdtg0m*S$h~dW&^*W}x|x{a?x3*YMxI1iQpHVRq-&lO|+Sw+Z)H<=1cO9^vs+vk}bS*cmI1 z3oV!qx;$e6(qZm*+y@XpZTtkxP zqD|wgO3?7#1zsiB6ufQCO$gZ zj37I!w2l&AfaI2%fD35-zZYqB_?zLA3{n$(!Wo2DNFJEZ8kPVbkUTQ+&Im88L7Zd@ z$<~AYp?jizZ=uKN?j7)iFw6f2sVs7_qB982d3_(u!tGju9e19q2dno&Ly@FM zk`R9ZwD>&zU<89d-wFG5N;FH$y95}#Slw+J#de$Mzd!f;v;Y3zZBo?{4Af<@EYrT0Qhuf`iZx$=c1u#1*#i+NSu{qLv4Ow_>LT znv?h7o7(NXKWKIPtHvC+0V{o@-WtJ5f0ui(@N1wu8iaVUFqp{9z`ara@ zztC8Mp05U;sHHtJT1a<^FXEIB zlA!hur4DsmjQtbNi6IT~qCqQt89}-~4C5J|gkb}YGv~x=z`Mr;Nu*fF|3@>7zfmJN zJ!P3RW1^2G*kf@|^#y5Th%Cd*gA6#0)>v%}`sGfQZgGA}8O~_~dOi$TcP!`#vzTB^ z;#AL_`l;^1-oCMG&d9tu(V2`K>S2NFDDH0x&z?(^jcg(QKZJXPhZg={v!{`rR8m5; z`j@K-k=36{{u#5lM>;OQfTKY)7(?qZslXknt2X)W)e zkBK%WdEV{#j@ImpVgE6FPisHHKf&>{NJDp|6Ycyq!pw0FbKsN`;Bz2B{97nI2NJ|r zVO#tR=D-N!&y_d{^8aecUJp4(a4CuY_)Iw&;fFAPMK|mXV&$NAu2zXU{y>@2`B~(> z#qY<;tV!)UQ39`n2KX2y?^ZsJlzc~T$+x5A4^Z-X{m+s=RT6MNJp}v@V9JG*d?icJyxM zT8Y&D?T{72!643#6g-E_l0kVLDsO}p6ba|Q#Gy1G3mNXM@01k#je%{w^RH8Ruy?M0 z4o>w3K38AWw(z-{uRVD7p5FH#;C&vabdwc==O?u=9dFfAQ2!zF9X!{;up+t^YqEy> zjT5l8X9aGqNp%)@$dX4Q?61&zpB8Z72E^1+w_wE^E88(~E=Z4u@;s@9LEKXsuIU3x zD(uVYp$m|eVtEO^Md5m&R~i+pU=4#&X0;qEUZu^4?$=!zS5;bImpa|6x>}h~0Zu#m zX{j=o*D2LEr|JkKR+9X6JRav??@R#YrAiM17rX}!`wUnbXwyQ_xpVinw;Js&<^55h zfluE!1Qf{UT9|5|3A>7r=|U_22*&4)_9dcK&v%a%CReB9UWn9DbQ|HDs5gybo-(XZ zmTr+JVVp8_JCh)XU5c<%9}^x2jN$;Lh)ar4`c!39_&LG}$_4C+a26nn947 zB1`)_0i;n&Dm|0MTA57yzfMSH2kMFjIrdy|7G4HD&&y?_h@syyeX` z57z!TW1Qzi+qc7FJF-FNWsRO)u?cdGY}f1?EG2#O(mGOj!Csmua**yv3M%;T~YDodqtcUFu*yIysr3d53iC;ph@B^LJ8mirm zs2%gy1&WdT&fxxl1OCG(h2FuIDd)X&KE6H5k%zO{rC8x7x?Y$uyJ8?l&E@Xr*>%Ij zfE{10iG1}o&vA5sT70!iIZ09s{Am8YpydXfNIgGzp6{)D_Qu4oB0d$Z!NlXI^EZM? zT7QX_ruCQBXoAfs*aOXi+k!CB-aYgz(7l6ig6vKyMwv$w?7l?zp@HYhMiX!UOguO~5 zR-Y?P=}l`Yi%{c5oNsA&ng^3M4J+c_NPF4tOJ0Wu+NtY>@&B*y(a8hL4HxPpa9(#y=Ey|LXzBJ+xQmW zFiz>ejrH+`8K1RfL-KQA_e9)anj67esogo%Lp*FG^B?PEgPpRMRIo|Ds`)naE54KXiti+dj~FX9Xv8_a7re09XY)Bf zz(+D!SSfdv&p&P|Ds8wR-Gklw(W;}B31WwKR5)6j>0L4gH%VfBfNLoA9)V^7aqY8w zz{lE081HNulA(Yplk!i*tk7cnM}#XN;jFKySEti;9OpM2;xdfG0<+UPc%scd%+fKj zaAw7uLc10A8bCosV=pg3#+x80Up|ETv;4lkSKY4qu$?5mtF7_^__~E$KTX2^|0Tpv zg+2`sC*+4fvD5783(KE$>IL~+rK{@#Y}B*7iN0rrXWd!eG|$7ebT;_3a3CGBg!ZKk zdjT{1!M(g(vlQmxtO>XcwJ1a_Vo?i{IcVm!u=-r}7ow_H1~zi|TYHa6gx1s>5y3rok6rW1r~6maZbUw`>@sk_4aXxJg)2WA!Y> zn9d)#FYL$ROd1IsA)VxD^DNnZ9ef{!BHUr+S>Hs_L@a ze+Q<$2{PRyyJ(G{9?nZt1Y8kp2gOh>9w`9GbS_?vSV!t;(m2xFHWw)*ad?nSV2!i&-8*6gB`XEInnVL zK@~DE7opj@?5?>MW z{ul17zaSk5jY9ZXu+dq_ap_2oOWPGXgZxChAVq^3y(B_Qeu(q8{NM%bg?}Y_7c|)0 z+9-Q{7r}4Oevo)O#P=co5Al77|3iEq9X|1XF6QwNeDbvp&xr@r!}B5jF`e-eo*>vJ zSSDF2!WD!!dT_=6h97j;xu7{a3UlFrN^|zXU@m5Y3p_%c<&?VW|G?c8A?Wu$)m?!Z z{s&g=|M88q2Le4OV^M(TnMwwF<}bk*G0RAIr*rOfI=Iv67oFxLh)0#-o)|F(T$S)? zPOLapIgr^m!nuV^oi-V@Sc1KN3raD8!Wy83oC!h&ldZ~udvR2k5hL#^N$iN_1;3WI1oDyi#Qd#tklDFe>uGi zN&geF!^&g6K~G`zAF?uqE{kJkMXdOXY$`GQsfSI@)I)c!s92FuM*YKv#rUdH%dCV6 z&7RL@wp@l&_d}+f_f}X~RN+5X6qFGs1)1X9%adxgvI3LO;@LHG+hq-h%_!5tnnw5a zn4ED%C;vY=hEs0iDeo`J^!)E-rygotY!*rXEcMX4i_J?YR?A})THPC}M+j$uhb%Wl zwdPb;+>j&06~BK2b4ArVtKX}?%XVk|rbb8AXSd_(`rC($Zuq$2Jy?;+Y8q8B-*V%| z8$Y`-vHre#lLMnWdgBchS;ud;3LEEJ1~huaAE zK80_D5BJ%P7BAMVyK~iF?CBup~R5+hvRMIai!netD|hdMwuTMbFqv_@WQXW^w!N zM@&1gjyGGtVQjgKMZP`~GVOGEMpe`yua?W^uYRQ*{MnINzDbPj`VsepTaQBm$QyoZ z8uNR=g%o|bo$e7xaSKw!A_a4VQxrCs#{LE?z2TZY9&#bqR+J_kPCJyuOgkXIu$sAW zw<5TYX@?6BrQ|S|DfjM!H%)alBrwxz)5Gi`+%yU9crLKf(@Efu|Ify0f&-Nznr~75i%}MZ0+xs(1bL@!1j@XeS zI^HfXKx@_mkHtCC*r%DMu@k1eQkGV6vf^Z0ONFg0s-mmS$xJ)x9@8fv4;X?5nL0QY zH?zIQuB)K**i)mhA5^m@o z6-v)ir0X)>f=k`;ZlU5O>V;tt>S4=BPPFIe0E;L;UqpM+NUu_)^0aUCD>u2RW%EC z76+~|$(C(Gy=b(uFj|@3fi`g4_u~d#g!|a~?5k^w_nMoN_gl7SHuZCW-VtxX%H;Bo zM_Ei9gB2mm57@EQEg{%pc>}=Rp*L?MRjS{#lfrhaz6pLaGxONbphqlz3lFbS`OOc7 zRIkP*CZ}m}2zEyHEP(}NghB|J93j|4h0mW{FrpoMmV~7DgM`t0`XWaB6g?O2ix}|` z!aaRa_rLW;nPsYY=)Qw-+v2w~+>Fo&`mOeK++%vNeQCV-9`@bOVej&WT*aoxJ>z~> z7?1U5I`SRhJG*2rdOSva1hG-RCwce|g!6r7=U861s^9|W1Rk0iJhQ~UG`cxn+^M&? zTZEWxvD=|{j9hEthO({hZ9 z1H6puP{voj`*?UN!v5a!`UW>R{~5T)eUA{+61y{AoXczNb$fZOe-B0tO62)pt8Z{` zK>ml2|6aX-MQ^iqu5{lK`Kp|MwZy%Ye|0<<9r6n(Lm+zj_;GMf0*8`eOHD;Uh>JP@O#J%YHoNmF}x}%6-T`+V{I7x@B{8j5s@V zEKu*R=Xu77Mu>X7(p||@KGd71q|I{A5}t_UIa7!e&+(L#+?Vo{8;~dSE$^ffnez&~ z@8IvpxX1GME0EK5{_tBj-hRyAj&P6QZ|gBfFOR&YHsK|~G;gK$mNA86U{bUxkgJ`fLwv%oB^5c#t0}GXQ_8$tI2a9bf z`4>7$fJZXqsMhFhI__f|>1%M}Oz^}HHhbapluQ5DXL{%z)mk-j;~S;T7iH-~k51#2T470A)`#uALcBmIZt z^t+TbHzHq1jOz_f^Q2)Fq1>p}aPF}2<~r6E&i!#cx;3i8Rbk^fN45F_vAK=Td=~Fu z4~L`&^$>0^H8-;zrbnWfQo12h)4TATYGiJXYNc9HD~e!E`_%y5HVayJd(6MyLg*zSD-e>f21%t08`2GkY(f&XLr^>5 z(gBwMqqg99S#(T@qgg-^oEg^{Zr>a4j84WVS7*?1rW<4g0y9C-cEqJzvvs1s>q%6eGEiX>3yg=emea zn%Gy!nEG^mM8H|3+U6S0EHOaKTImaYX`+gC9DrJovZUG2xKrC%-cU%>N#!E8U`WB4 z$s^?yV+z)Mwj^sXEaZ;Ow{k$4WMxk&{T6ClJKyT!M#|?)T&uPjwftOL`hGZV>b;H= z;Rivl#sRZCfd9)v2QV8h!+X92zF1724tqpy`oticE(H1MnZZq#>^{X_0uF%c6;N+v z7FQZ?eIBFw`7n);=BTe5F#OAVi|varr43=MkpD@pRJ2`!R#J3?!<#|Vi$Sq8m}xLJ z0u0xxFkDnU_}R2dl?GvSL3fM0i2_~1`W*;qrp-Q`ht>S*kdTs5mctNi1*`H3RAmGe zt?6fnKT_+y`i;jX|F1OB_R0TBv)In5P`;@`xydpZ_JOvIV>y80rQygde_n3LTJ`BB zoqR*&*PDc_bl6MlZDz#vSNh|IpPQ?fYldO{gWPoyX4QbwNNArXn3rkA{o4^OVwL;4 z$G(t<4$j4SFVi(4Ep!+}{SDAO>TM}%?_R);T-op&);OIWt4*89ruTtXTNvS~-Y~du z&_F%T=t^W0*)^!KMujgmG~cdZT}M{WEmz8)-$dtk;rxL46cND2vM?J{hKXt`(94Xz zEt(MQMf9?-<`?5F56!}tA$t3A?AvC6Mdy$@6gx`q1B^fTF`6#FwyHT`-p0OK5!o>9nA!T+!t}P1J2u=_qgtH zZ!_rNX#xF6@TtD7uC3sA@{!}Oqd9JO-J#~VMa|LGcdP66(Hwsm&9Tw7QO&Vh&GAc| z;W(+$v4*K(?(DPGtdD3Z}Ja0fVZ^q2hS#y{N zD^Y>Xnl8&4Y6JfMc=g0m!k-v5OQh_1jBMSYb+Zn31+kIM8lsW?Rcu9r4;~xaxrDdi z`5QaK&2K#!ZnlQPhcL!RF~&U@V;{y?0RCsK;qZ7+kAH^arVd|O`;$OvMk$ht>2dD% zm9>9p91940yh*|H1rIxyZz$dEWi+s1JJKJFvM{!PBc#4{P--E~{^P3U(y`F+Tr zpqu%&(|-p8vKq`SJs(vDZsV#g5O|IES#yp3y^4IGTx2* zmv+nYWp?N$jIs)eFLdg)$Jj80O*m!>faQF#?lB)#E(>$ z?T-{|OS9E?VO2&ZIT(CJb|G1X-O;v~Fvof2s3ReWGnvaGpwDgSu_*S^~rIvInY8i<23ADUA@K!8%PhX^r*#%o3MlwoXNp?Nc+w#??<#^KJsX%>N ztW;?9($N6p=SqTb&8fw1lO#HH@>%666~o;dF~|X&sO*4DRD|6w%jRU@(if1j5GnTI zv7K6+p`-B355;AIDV-US=bt0GOg1ewXuz53%vOfn6O~dIT~if2J|OUXU^ZZP@KgJ1aW;0iC=V5G+P%DJbo|=)c1K7~uY|uufi&JhPC;fIPp8 zeiPO&e;Kn+R{Re?P)WF!u|pNNs{g*M@ISm9&;B=P4JiFH6hCwUGy>3CsW3bd6=8XM z7;?4-rR|4>cQsDnf{q>dp0QxX7i$_;tY~B>1 zuEA)e%%2yh;`n&0gK_~FV<=R7={NRkEIC=cA zowL<2%f`=ETW4`k8EBjJHjDZc7qw#H z>TOyzCm)^0=SezwRkY1cbqUSETIe0nS6k3;+tF`7j3hIXA$0OSq~3vaoTWwVA(oc|8>|>lqXVuq$ZH}(yH;L-*%rkf^0jC|EuiDoy1!S~SFE1ZEQi1iYlYcR? zQ(e{jhDs4FHP^~D)7xuDxMH3B)abkWhctFQ0*Dl=<;;Q9ZiIeq*VG@hPK=9!xM5rw zMT^$kO4J-v;T1A3vp7>Le;MV_lFLzDW*>U?cO#sddJ%1G0p@sK#T<{KjY(*M@k(#% z;l~82*WDWCfZ?CO3^ZabS@2tcUon2*c~}bgp2tP9WBzfi>=PNY&jQcssU7w7~T`?`?k`a<(w4qeB zwa?)6Fkg3tdfy(wNg;uS>U}F@@2^22)%8CjpaFiw+KiN$)Vd#z7@_%&@hGH20Zo8T zaPySvtk%jXI5VxC)(kL4D_}3>)NlS4A$SGQBye%1ZO{xx`|C$IEAcUBfb(UX<@eL0 zq(?mgoH04XXHREJZe%t%u_xFQJk#1mb(IAmPmK$4Am&)n=rKb`2W}2|T6s28&G9U1 zl2KkVn^rL#h(URbVtVDz2*v7q7IcM8GXgLD*tZ02f(q$Fkk(21rc#=wC7;$>=VGqg z!Rc5=K0eX{j)f?g|Kf^Z$^&3+pJ3zb`wTSRM`AT!p22TW6r@;S;m-@ux6lvoK*UlSy=ecZ!>l<6EA;tI#9RCx@{Kb@ ztM9MF^!E&+IFW_-yRbE6Ha+~`wQ}AF>-@s}yyM~tRf=tBTdc(jS=q<(t{<&2|Nl~> zBb39mxl~US*45!?EWI4EEKacjFkWQSd6iQt8`3#qKnQ ze-?H9(yAirasZInL(?(tf;!sY4>QQsw!{+wo%%d^*cN{xpjYoF4Sy6csCTKb6rkC# zE~@FH>ra4DzYqFUx=1@8wEpfw$;qSr&STa5X_!XmHslmST6O)*gFc;j+8^~QO8@F% ze_rb7e9ylkp^vRfed#=Pt|vejFh+%C@P+?&l*2wU>_VZ9wT4KmQ9Rre@aJWPv_)lS zfNgT2g?ozqJTluBKTsZ{d;c6#>{dia*EWAdGR)5h%4s(KeMtL^Ub-fbr%H|RX(cY; z%GV#^>m0QQ-o@9K3|D_W)%y2vYR(;4M_f*f>jzhvsUYGI&6ET7h?VqJd$h%wtWZQm z2C_L?5-iR%$Zrp1yRzL{c|B4pg0Xe>(ugLjAVhCpRjC|2#j14j^r%+u8{QS*bItLZ z)=}B)iO8&ZYydiwX#azVY+(M$Sh}`(pj=S#;#Rd)3vi+?3;nu2CJ}388X4vWGEW4nF(ai_12M!1wU^xtOgN2Q`p@Er5Tm3csd}B=!KXg%vHmgfeCar>HfX07Wr$cqG#lk7%>9 zL;I~3;1{ht95q#GWece6@?p}Ya!*2r{W%}60bUs@*Ff7Eg_Yn~x(4GoP_9)|hX*xJ z>!ewuqc|R;G8n!UqtfH-XitZZx(|BhyU?yB68veM{a6c1|D>TJ!vCPM8PFKcfII-} zL$nGzpOJXA)MJBFu`fuq-aN?JP2wT(l1V515HJ*d8_HwpC?g2LBMNy7(gZa1O8=io zqlpe8jUv~o>Y0j5MkR(927$}e{luogH?lDapX&x!ZPUQGjz55lU*zuznYcW>gr=bPvDtrf& z=_7T|411YKKNn*5%^S}^D?;D51Iu0to)}%z@Jr3pLCcn^?np%Y$3ePec@a=p#kNpg znOazWRY3kFI!3Ef%e4CyoCUARx*mN5+KA{fk=g#dmB0*R6y{H`g&Nb}RGyFN^n}tF z-JCc`dVo9%>;xQwo#9c) zjt^+E!f2Dy->7IiwS{q^c`66JdQhwG3%@(8VIRW3Hd4w%endodoNL0h#ORmO|2k&< zLUmkvp%bVGeI3x*sK4vc-zA_Oky*dl#F}^D^o(Rxs<>Fo6%*GV8pz+Qm7g3gz{=Q+ zmGR~vYsTpu{XZEX9+PItN@xrgs&nEN%$LdrcV+^ljN1o5VKUkrpP!*xOo~pOEm55D zg3%8`;>gP$of~frejU(5zeM>m0nKF<8p1BF0j>RJiGf?Cdb03kjE2obJ!(m#H0=Gb z9r0Q^8)zT4Z87}o*qJ+I0YEv4Ghn< zu&1=Zkn>rhn*O<(uItc3f5o*FNbIQzz#Q{gg8KHnqLrovrn1e>$n1>!^&Jdz1SetQ zoLQ>#mH|Os7rjGy;7sUDV4YpGvDOsKi~mF~ex#16Hwwrwr?u^oV$w^B`K4!K^O|Ol zALYHMIPFuJ0__)81NeBf!D_yrYTBq2TRF^Ax-VBA${Mk7q7a&oxi!+i7BfvOJW3Qz z-9gYo*Xg&-QXzau$%5YSa9F2my|~hM!y1h7E2ArSN^}e}&>l9?epaN1ej`%KFgr(Z zS}wQX`>+rErv|!t0otl4+HCjo%`y8D@Kn~$%`~2fr8rF!eBP0LM#|uRKAGi$$BoP} zvsB>75uB#OrdohQ_24M^&fh}DRW3<)%W_-wmd?|e_r#H;w zZ0E!fSIXOptt0(^h>qC#xh97Y;(>8T`hSTY#fTJ}w=t{GNj=z#lfMxAUY$cvDu=PF z0vQ^+f=d4jh*?QIBWZnN{RIA@n6<_OW02lw1m4|R*hWx4AR3S2hpb-+EFCF501eU~ z2Z1BNo2Fu%M{q8>uR9(6=vy573p*{?+>m9B@n6!f&I1k6V*1hlSUPNt+?5_fsQ~@h zPS37~e2brhp3B$OoO1_oy?7&WN;VcLS=CU((xtRb*Vm4q?G1dk)xOnG_(kZ`9V7i8L_ghHzk@5S-^%Rvtrx>P zr7yzLb~%@*I{Vs(`HSD2S?5%%+P!|>*9w(S>Sy#t?VJHoa^^0)CF z7B4SP8>YY4sr;6&%Ytp z7_`8u$p^^_?JdX;m9fM!@}gNf!sTe}FVEH>Zju$%6aY6FQP#{M`%i35+^kii{q7A- z^*{L{uu81I`*72dpR5v(-2GA0MO-i5jXi`?O<8Z?myW-OIOrp-O|~5_(RO!T(*|5O z-2H6RVO$U2eWB?iTtB)ytYQHsPaL3;puO~XyS>rQ5=)Q38b^g$Z5OxA@8xF3!8E4; zy`+whJaI@E{RT6tk@6RP7i**7R%f)Ffc6_{T#;^a$2WD<@;=wLj#`Ir@Ai(`oxXQ> zbkx4(i$@CI=t7F4aW7JKHol9TZ#Bjvh2P>riet-Or0m@CE^@xLB_1jKRu@tnTlXSm z=hk<(ch(-;>RVj&?N#;AcVqR?cVqR?cVqRShAZ_T&y{-c?Ui~6uCaQWV)bNy!?6r| zf7iICWldYyq|}?G8^oc&6VAKX{M2gc(WdRX>eF?wbDN*KO4=dLU>lsb z!It3$k#*dIzk1&s7JKG88t~j3aEaC8PTvOS?c!>&UR>-u=IaOcW%hCGVeg)!AJ>Lz z8^k-D>(_9l#=`!Pew(pO^GbsgeDaJbXOw=olFz(OrT;gI_66>F;(K&2@VNd;(Z3+z zdIXxlP0kCoCu&_{gY(_hRLTz_uEJQR1kQ$){zAOJQ2QC)extq{2oWtKy<YlZ1PU;S6E7}^@dIWomw}dG6V%Pv7_(<`m^XeDiLm!_yb)Q$lmI`gBQsnvV2` zo%e`SaXrrN7RTXwf;Ea0apl^!hznCYZ@d(^S=4qO4@e^4b^?DB+P=WwdQsQr7FUUq zsBgPVRFERH?GObY*9L2grzx)_#^Xol%5Fw({b+6j(u6k2r$s(0>n71D-YOc}mWyj~ z7uV**+xx_SLpkv%-XGHa29bh+E83#dw%M5x941!wH0v4Nug%Ojzx~F;7@rPx z2K)f-jK+7y==c^Yh8Hw=&Q;=G(Bf)WxNGEz@25YGVu01yX--pPW z2im+YgL`R{VOi#lFV=1lc^|DhhmTg{PG3CDeP3}mt+Cak>zv7%16SMbdH%=xPg5~p zkCW0l6({%>AHY4R;Fah9jy>BeO}ia9-OI-n4~6mG?(52A9S&{q?b^T8a;5i(MN)s* zPin`E9&Jyu4>(Bl3f^9o-X;=`Qt=d}Wk3^S>^`<*C#c=Atr_v1CdZCl>XpZ&qh|rQ zLhhOZJ6|ni5ksjKv_)w3%_?jH-2p`u+L-(kLnwf>+aCgPO@V09en~SS&Q1FrNs#=wN3)3uxM3F&{UfW*@V+ zIylF&3#6P?!4(AdS+~*(GsDxaZWi} zl6Yn+E1pgB<5skzgb7|@v>oODmv#vM(hgmSM?1jBq8(go@JdT~wIyGMiPsV^+qHmr z&(Y#iSW*D$6|)SpVSihY8{*sa@&|){dyVE|J%bkBc5D5u)J{93WM8SVB;8l-gT4h( z1wGyZK$g#_hv^hzCf|iL>h)L}XCW-L82-ZyvbkP<8*u{*kUJl%XzYu%gItH8_RG!i zdSf^Z!GPSF=k@Zh2f0qo3)RV~x?Yy?nr?qF#!uhMWcQ%vc_XwBN25j?uYP&|;EO87 zROcRG0nG$$-U(X1#9Cae^mj&$cKDb3jA*y7%U3!XqXVtvwD=I^vkzMCtiut-u@R-# z`mi@BVDDkP$^|5z&SGY7@$3&b%>)D{Xee#i!=SOb5Ah0_%0KAcOH}@XYTF3jm&*M} zm3JI?g5G&_4Jz;}BJYI{BWfx*4?SkEVa~rDg|(Zy=bSKTyR-hz>yvxo%XEUJzLxPC z&Z6Fb@y^3{_Ok1fPau`jwEOuNjPNYfw2HHt{MLZcO4ueBJwlP_nEc&9DO*uyZc6`& zON2#t=@Hl+7ck>Ac7}3r7~zWpG;e*x)8$ZnIz%?gH;ckkGZ@!NDfLbbo;6S9UU}Bynd{U( zl_stiIs1j&G_gWg<0uzb8p8Vd(niBq`UO(I{$9gP!1Q+*u90qL!vP&m^5)91W8TUN z-+_me{u#1Nc-`?j&M(Y(P4H;&SMq4_cb6yOwPPNYw%Vl3v-icg$85~DZKIwg#4GwS zhBT**#jwPJ-w{`K@jfGTK|1JyFlUN#gGT#3ng4vS#@q{^Om^lX^!&!+N)xj^>g1Y3 zfqn1=sB`|db`PkF$NA7|uH?J$XAfR7cbHBDKD$pb84J#VesFC{|Baygm+w1;_-Mwi zMVUBVo-iRS!WD`kV5eGrUE_f-#{sG@VO(G=q7kOqX`HE(zXtzq>>}vR*f}NsP{jRs zv_!OuC?i>Ilfe8of9ZbC?B{`#3?+Qk$=m!TDW2AyzQz78XJdYAmy+auS<$#OhWpk@Te}W{WoVr8X zece>f*I}L=*?wqCwRmxhZEKt3>)@iN6Bpi;$Nz7g!&opOJ~TyD%<^Oyq?WCO8;V6sG(Fd+6pZ zv3BuZ9zNxu=A(HNUB<37Z{kv}Jk0}H?p(a;_edTZiru~?3J=S}+tQ>XjlaOt#cjQ4 zm$6%|1K;~l7oyNE;pJoMoH)94?D=)|`EI<29xYp#I^ntrRb`iqhZxIb=O%U=Fm` zL2@73N$rcA&c8Wb&K_0IGLG$b z>RDPr&N)`Fws*cet+Kp4bZp62U(Hkyx;C})(B~p zZ76FM^jl9$bJR!RvOCQO#9Pw49kvsk{eW`@>sz$S z>2y9Ly&X2XjQIM)(XZ3+_3QXLzPCt%+Gi>&`p;ne@<2GP{D8A>(WS6|D$AX%doer% z)|OmE!M|j7;2ySn!KFD5d@+n!!{iFN>m8@WZZ>3$d$62!X)|@`#EFHO=lo7qXzev$ zH>FbVNC~D_3G!YAnjV?zjKj_{^+<-sUfJe+ev#ySUCLs`nHuzl+@0)x#xZb)%M|2B zMVE;)iSh02>bKuR!*rRx?PW8(aGRqRIDq%GWZunqu^RPoaANP?&L}r!t$sP15efqMZ>$CEN>QKxZ(^|M!p($ z?5YduNh5dlF*~IR^38yu2(|{yPG1S~4N>}vh4%MpUTSl4c862b+T~v5k(@$DX5Ec- z<_%AK{^4;rq2bPMt^1(PjefZe*1E4Oh4%Rg*2c9bA6+my3tmF&DTS=MA5u&0=u@B7 zv9_V+Pl5ymqrS}R3laoOepsCYRJ%s4{Ym%ux;1sHJXB-a2Jo$@bro9tq~}9VO@Ie= z5S>ikim&oRI(b{fW*0o!XkV;7UyZQ5{|NrrmYwcOC$*;9Y2LtW)D~*h1Ma>kT_}--_V!863|KvCf;!n9p?d*epKIuBp!Plzj=@}@=6w)e7-RMR4)wCo zz2P5g-l+CswUg>JZ=hPK6%-}q#vrpeoHtc9*M;i%!Wy(KdnN zL0Fg@>a$fkE8M9%>IZP8Xa~)XwP-~_i2C;!YI_xs9UrFt{dZVC{r51zcvtwxxj^~I zcI9yBAbO(GS>!HeYQK(_HUp*23e}*8&jvpjZN)QDnz4eMfp=O!VF3)4PWmd=JDtu? zfRAYwj#vB7byfe>VD7zvxyNi|OQw;(QY2?v-I+S7f#QF>I$FmaqjmffJ4q*D>nph5 z4`*`BvUHFGIhQN&!`knwP?WDA5y}tZ`bS!Gf5)o3)>+Fo8v450oKsLy>(O1#z0Qd& zO-c;j?_S3EmWP~o7!Ep8&u@1haMn0)Su|Oy!~Rs$mDP~;FRS68V@2?Qzi50RI~TZEW_`~* zlbsO_tW9iou;7Cj;dGHJtubp_w}lBR{aMxlYXZyEDxw<%1!><|@0zd${jBMvex^Pd z7twTb(5P00*#YjL-)duVy3Hn9$6^>3Wvwg2)MqGB?Jw3l3#~wpTZ}08bd*wy)D+64 zmEEefkp{MY^k&LcZ~i<)DHBOgi}}Hru_rcfuuseSDseAi&Qy%ik`vG)w1(R+UTMq{VeF8Q;wh3C*;^^4Z0 z6Q)qhL2X)DQmrdv-~Z-HeS}%G@~_lbY*e=v=g!ecYURSK@;?_|eIb%;M893+^> zsFqfqG(`10?RpwLl`=$EjT|1L_gXn_=z!}0p8qlW%nfz8I*9&{J`WC(KcPnc>*({> zgHO7i#Pc8VOkRdD%BaPeOw;JMt)(4W)}73 zk1)OsyOxJk?+T6FJ`~@P(5XShY-USfDa_1tE&BcOjK%~u6l4L@oYX>f-GdgHRck*j z@H;^e>pf!x`UBM1F-W6Eki>d!zX~Pws7Ah3?G^eau`LNu-h|qWtj|^lY)LaoEAJSj zKINN*mRQXn4^!^gc>H5{9egaba>L*aE-IgLtif~LRW-~SGIpe(9W#fhr$nrVo0Ktl zc(rG;hsJdzBZo!pU5%VMgcHBeN3r@}8K$>d`Gw(FYkr~DAM4|1hN(=#N#MAFKXzYW zcI#3|`QUS7ya^;a#M6zSAi0@-eRM#i10?A)o_a zEx*>4_rSXqxAY!wbv^Qa1y?!w1<&1~ie*zDzkbb{Qx(cfclEw{M{k9oI$&JH`ZtJi zfBdpyM3{f>d#+}D-;9NSP?X0-#(ZxO%%8gci5d8aI?IS}i;c)cwRd71*l!14B;F|a z$_0P+KU}MvPdGOUHkL7=ev!E{jrCU5EUIQ%EJJwInUP?Z#%Hfg`f%BXMH4Cyo6{=W zL34gBQOtcvn`hn58b>?YjFzag@8Xc#nW3fe;aM9vgIFJmgO~x?%=Sl>55PDNP`}d% z#bEoBx>6~xR(@;f53V;bQ_ zUNi8V>lb)`R(-FLBg)te(8}H`YlZq(BY&y*?KGR1tydd^a>HYS z@PYh_+QL}=3P4^CD9Y9E5dQ?W4YP(MXN}1`0UKlIDlVimn)Mp_w<@iIw^xMdI(L@N z*N#!`YhVY#wJ10zj_4cfz3md@1>LX-^b@+<&#=R|Rb3JHd;$s0(BbsCJFaEt2K(-E z*DNwi@uAq-7#h@cz>iUW9<~iE^vNf*GQBP@dSZuitLs)=5f4+X=gr|*Y`~9)$CsQm z+nBjBg|&;%I>@{K{b9~-7JE&dptpz?phCp;D*aiANINjR7L@1v^UI0;oJS1OVVX;H z{*hLGeXs|fSSPg9TN>G+EOQaQS*FejjXa-ZMAYpXeJ)YQWbu+E=&h#*)zvEJscDGh zgtRA+_9Ir-$Y%9jYz~cA%&u8zjWL#Aoy|Xq;Q^X!Xtf}pQ>f3epirk)zC(Q*8?W02 zsoZxyCdg@Cz+OI*=gN1-@YpBO9M@Eo4NCyEr;3K7I}2}FJmIMYJm~e9TqsTdq~fc2 zLs18=^KO4Wv98wghv0L6*wQfP)`eehKJj*C_v;Ib{+{!jO>0vsTD$)+@%A@wtys8* z{=E&Hpz@S&DDO3ktn99e_1_M>HRaAL2I+f+y;ahycSb@P&H^e@{NpC`b71 zM-{r@U!}_k z4mkbx)C5TOQVn}I`L;!!CKnsf3#U#b*RW30UbfBnC@ewzmWkPWoPHt6bkM<{%Qw|z zJhbQ(a&QZ3(pNGtc12%@n8ok(TP!({#C36f`y92w4=Beq7fou8X%mD0oxacU&G}pt ztkH_9dTY#w9eXOP95v?Dj95FS<=pM8a;`Kw79GbKeSUay zCP;P8WNo*}1ZtV6N}mJWJFt!f3DRZZ_dzyeLFi)qg!vn_tZ*pkw}^t^s0abFzGPOz zwq**Ap0Cs_`Jp$9G5fEfi7Uqf zB<%9WNCEC~Vn*~WaIRdb5Tw5~hL^z8LI$(Hx#*YSa_~)r<7h5wWh>Lh#cLBl+jFD6Ry!8ujk67l1;j@n8{ z8hl#iCH-Wn()kxQ6UR3HEZxS+y!XH-mNWu+>N=sd6kEf;RrsSZ&BmCHr^E5Rs^byl z)TkV`U8UpU9iIkQ8hA%2_=M!QozR*q$4l>p3Bw6eZ!kf~NXV4x9mHoYOU%ZTUA#k6 zD5W#Wd6p$3zFemFMlbbR43dkgJn|zXTNMZCvkZUJm*SiMxT4ef61tpq;K50!jc3$zv&*L}yJ&jJQE-kGCY_(WG(&5{97til z?u=zGIpon6K0ES5cru$cEB)*wH_r|@;?5_q2}>qcG5CU{xyA#pyc+p9oWN3-q#-SD z!01Xr+RG8x53zrS4NGU2+QbZP8sG)Lh&Rtf{t{+vg>-a*qD1wiyG-^gxUM+HuIQ%5 z@CV4}g5oY0>rL*}uQU*B#i|ZI3Ch5I*!ki*Z_7B~pp)pVO&Q)|vV|%m>q^+)sTyYk za3^AG48dv&BO9}uB7J#677wcMr7{-b22={VO!)4pabU=zC_~-fq|>*Q{ zqAdb1zp2oUZ+}-7q7Y>?VF#d9%Ss{3Pr`cn^PszmEU-A+-H^anI;z}rnHA|huoz87 z>b35b4oczWB1gX|CqY82@Y};EMhTt^J9D(8jcamijYyBo% z%I4GhkC4|9FV~I4+qmYK9|g`n=+vaM!dHV_n^u-p3KiorCPs-rg1sqrC$3YnO(+e! zHSiiJJQSRO8e+LxhGS*Up)wiQOpslR+*I1}t4jN7gi1RX_NsE{R}bDEyH@&Q*Eb2L zfaMeCbU}C6N%a(>q(#cY?m1oXb3mE}V+219q|U=RRShIlOuRv z5i-50+5*ug^<>9sp5v`zij?jVyw%?EQmv=B8@7br6ffc!BF*eQp^toUw&8?+@B`ms z*xZi3LCmU#PP_q5^qLJbRKSHcl(dV|O%I5A!jp8@6NjA~d|l&6c>pEf zJDNK7{xOPN6e}rmYEOW(r2o{nm_tb&_r$JdTp2J2H&x?>!GJhZ%<;zQYxD_Hp=9@H zyw@`U`uOr8Zf;M&0^2v>=G)Kl|8HAzwgGMUfVY~x*n>e&^WN4?HSsmrS83Tm(5*xZ z+cpubj0s+=tGKJxF(YVk(cdXo{moME>F<=#ID5K{vkE!wV#bo5Y|hheC=_23BeM>Q zIOEIo95bwcUh2s?C_?8ZC0q~Ok7yYw?rMK^)5-uV8160ZjvmY zHtKEOxI`M~hwV0R>pk2IbI54r;5lfRm*{SgOx_->pO!(r&FbaFeS204y_(*{G-=;M zHm{&*-qV{XCWwMbYJP~$%zKWbeHKKmZ8ad87VTUs{S^w~lub&HEnB-^7%#r2Pt2Vp zEs%P0^|m+oX_`{+HDZo5*~4|t^rnes=C^n9X_DL1k3DVPYH*u(V;uZvchFuKB5IG` znRnBiROxLQbI_h5?P)Ws1v@r~hiu~z0n^xR!G4P$wpg$$aMcK`h4$E%FI&l8TCv=% zV`ju3O`e)iJ3ebP%Smbj~2)bmu|Vs{4OUulDgQtmyfa_R3Xi`=Z;bF*WeD?)iYUgm-?jVoKxMoOOLJX)QfFlkX{wxm%1sP#{PX0EUgqZ8Reg5t=soWo{36>W^Ax43-t#tec zc%iYqJq_()VNPX2nx_7>V9c}eBQEJ~;5HMz$CzX;VwGgGX~gq*+$Z8{z;A*Z7NOqt zUM(BHG)YPirn#)%RBwuh(pWx!4pJ3jV$l;vQ57b!^oB~4XFXaF?`qkRAhlv;EB)U@ z^*B2s!)x~F+>?FZ13s#k%BT9MeyWe)s{n77*0tHmd(SlLOILf-8D~4Rcp@k%Z)FWk zz6Jh$pr-2mJ?ecLurD~12BgUwiAs?@Ei4JZ>0uAr?yb&*6_A&#O$27~m|$1Yg!b#f zd6>Ku8BhpPpP|pYd8-jMl#L7mKFWi7njAL1RPgatJaoAiupeHAbdlih;+l9SH8IPx z`G$~H_0pVJH3_-?GQwGm-PU~UmCy4HIPIP*s1O25}!M{AMJq}h!* zK<)8$B~N{CGB=NBCE}?-5o2-1WpM@UyuK{c$CX3IP-DdH%%2iE7ARa+&An)9X((9g z=Mu#p>mIFDYN^Ys0+KiRtt;vZyfs|Vq}f*Guu5E`&(dD|kYs?DD<`$D_H{G#Oi%W) zfO+|PuFwMYc zmMp1jt34!Xn9*XPH{OJ$CrujBZxL;xV3Brvc6!6x(>W+?C;7pKu_t zlS>iDi#5qDko&aODe$x67wEA|qABOSk8=|qi?5n8HD8=3rTkRuEfez}i)R7JCKddY zdD8{{F^xM-Opt`1%H7!>KdXS_g7P>t-mrfbJe*zfY=K3yfC$EXXv3>-e0$g9zQuF8 zv<(_qfU;@h5l{9rh3nifnJec{o_g?$z);u=%NM2p69w_DASW*DwrslC&|@Bg-RCLT zbg&$*v#f-2PiXovH=@~1`!s0Td~wr1!{C#x#a)tj;xe;$mp4JI;b6yRUuk+`yIpGC zvdimG(>^f0hSJQ*e`T=Byb$`y`i**J82Sa>F9NHV=?Fp z^>crhfHsT+H;Q%?UW;}-f_BuQ9o3nM!S^4m&VnW5G;Ju>ng6H-SHN$HtS;=*nPHxk%UVXtSF6UI^)qO2C?APo^h)8S5w!yZE6)X%M z-gFCETn*ZDKAOv@9k-&*WpifDtj;V9et+Nmrgd3}tyQ26a;;uxk`JD1(f;mupTNEh z+v;tbDdG;ZZ9spfE)ZatvmZ)a^%_U$nk}`j$wn7Q;d@iG|X(7?BnD+hB>PlShQ=n z2&w66T0PRzk@nKC#h&X*wuBlIq?2`=Ee9thr{Q$wY@A&4kd*GzStmE;<7%x+YATT8 zVJ*YM?#*LMZz4OH$?nNlIEzf!o*Wp+SVq#rMQF;X-$6cWobT*y}(Q7 z=O$Xydyu@{&w+rxF zj~~%$9ccBz85|?LrSgPLhvjV>0!i9$=cJY`E!%4x zpxN-#Q0rd7Dk0f!gC7wrs|;xM!;ts5AknwDE2Z~?US&mStmE-k4hdJ@Tn62ua>%E9 zS3!E{y}+!c)!boFjJvN(1zDcEjV}DFl!7By`{x7&wB^AsdlGiL=+@m z+O+yG^fzhZ2GFyUro#=i)@~WmySa`JR{VQ71+u5oe^2D)f1xjnFsA%?7k7xXy^U(?U@2#{7+r2Q=;~ZoLUKOyf`dAxz~_ zA5vr-726|@tImBX0DMul5fZtZ8VfF4xaO5ai$ z#;)Umge6s+!_?~S)O03R6egT`!Yh_{YMx8zOngoN-SNP79CaGIkppx02eofy*pJU} z|MX~vR|oA2`3|iP7F{>5>39GfXoyz&tAiV1E1l=(0EhbF{*)GRy zZ=K4+4$Ya^y$CHw><(wOSuY37*9omP9N)@!lzX|hPLonvy&e3+mI>|`&?ATqDQ|?| z{D<&HBMHe0Yn4z)grC}cZ;bMpwsBH=BWFF+$akK^*B$t}W6K+1;){PDrZq|HeQZ3b zKZwsgHyPfhcBTI@g#+hmX`0y0!4r*q%d^;gwav+Diz|X8XFlyHKRtV?b+qM#_vWGH zEXVGf>3$7!6t=3LHLSCmn+*M7M}tb|>o|B==c#?kY#Ht{c6Lq0 zbatj98}PUDZP*1GVx7-wsyU;3PhzKu@0gZeUDwetJA=MPPt>tap(EB)yzOk`df+1B z&bZEubguP?+CPL7iASUPLb9poNM^nij@2U#F?k^(C%J#k{a}L54m1~9;Gr)9aaDJ4 z%hKFC~rwcD3Em8_hO<7yf)DvwQ&U}Rdl1>x@r^g3ZWkQ=b z`_SrxSTTIbh1Dkla<~9gShED@DJo>k+7sw@lB`ZVy4=OGxv4)rYH}yD24|KJ-X@6W zy)ZJt&ApJ|--MWo?=@DJCTwAyk2c1Yy8Ywa%Nq@inZEffZbou2>AE=ELSN$YJbYE= zSNgXrDhTJ`S0;5fHly?Y9=f=&*^y}-#w2M%xJUN{X}M16P$xEZJIupazyst-#%u-Q!WE7FCS)#~P6%=D%B z?Cfu0R%8fqmJDzmCf^w4{%iOJoNfVM?#OZ2uVJi}cd#ci!S&~G0wm8~-5Y8gjN_34 z3vYd~DcP+Qn>|UcG;e0DRdQvguU5*^Z_J;o%mrpz^8vmzxPAlJbdV|kE6gm1&?g(v zC!6r2HAgt=JN(@4{;td>J$95>a93Z|@=|Z-z6|}iQ&voKoVPhp;x*b+m@;$sc0t;= z1?Rtc4EEMtrQVKxEA>-OX>1+47P`_fs`0_JCeq2K`REC~0Zxs{ebG7p4{y`)HWV!d zT`_Mzq02gDw3)Z8-&PE(tdHxk10JecMV0@C&$_l{(ES zk-ky!b}4kJlON^m2c<6pT+P}1ULd;_=;N130uBZ^ zAud{;B}iqBgOwHLRznEUDSF|1A_Q9+>P&`vTI=?rr59_J!bI2_Q(f;Vae2CP`|OX| zI@x}9aoyo{W)GEsC`^ddItJxIKxqp;Lf@L&v2W4hFa>{R?eJEy!=~$NJ8-`Ai9mIvm!J1pod3@-9To%~<)>-gcB% zBJOqAL!@8n)f+#^a@Tl!wY^DS1dZi2uopI-rPHJh<(-Cfvivk0g)}sxXHy{va=AA)f(e1%<_k!hFbnPFG%TFX{GK`$iBS21oCpJZ);O;99K9DJ?T_X4ld^x z;?@RcEtIy_&4ro90-Sly3TFmcuFY+!I^fLkzQ$g6Ob&TyC*SkficDzW+gIp|($9nb zEO?ITt(CUWo;I>}=w2s|B&3d?Xkxq9J@2>>%-iTRl&{0Ijvrow#cyJwh-+ju^qRPi>A*>*)*(GV> z5?@!3X`-FZq)HAN3zHztwu!0kotS;q4JpfZO4W7DmhVkoN?JZ&cPqEC35%^)4iAxpR9NX*Q^zqQW@h+a1abs_ZS@D4YT9Gj~~I=>I2<|<%a8i z!AcuGTW@y28s2>wCkS0$Us}hNeU9(xyQ|wkk|7t=;A~IT4-4lSXynErEBf3RO+;2T zmF;#2XANfBd(Jk+(lVK3=G(3}vy62TgC2&Z8@r?7XjpI?p~+;UC#>{1A4Pr8`&K0{ zeHO9bn5-RS@HgO__||G4?$ZO#gBEA-{vj>OO-j`))`=|ee7ysxlFGW1I} zOFoGu^_ZgF*T5E6$1aAcOk*GGqxH6C4Z}*K-Du6S>0TzM3>11hGmbb0Px`vmd|d+g zB(5~g!{jPp7D@xn!;i2llV=PH@-R-q9RQz7`}KyUbdp*R^j^{aIm$OUVP~-a%^{7O zY++F28~qqRqV4Dp23jts$Mz}YDY%+wPb+7gG||nndrXpd!gU8l)#uHTFKTY!TIPCF ztKeA*9afSRZIb_eIN5cH6Cj-)GI1SD&Ol!#1&8cI*7&8J+#+cYo9n$~V)DUZ2ub*^ zeK^`pb8k#$vVe~Lsf57?T;oj?pK$0x*v%>&2&MMXH8~hd9S9-%l{ZQJImP`eI{;r# zX5srRnXJ#0S=^mv^L06wEUuEgty1RvG)V%F_h^G%+E*tnmD~q*9@slxIA4NY?AZOmN2f0>k`6pP_RMD=eAGXcQbgpKh1`^HFgW&}T~(X)s@gI)Wi2Zb z{W+N%GB>>}Wo{7ct)2|{K&(=IMuvNtUCB<^GRfuMvTw_KTP6a#O>|FkWnjm=LkB&` zIJ>wqIhBXjPJ%w=r+&M`OFcm{B@>~mgYGU-hXFU>2{?w+a_o0n<$eveqnKV`3T%-C z9mi;2o5qIp{uca@+c9LFhSg~D?!ox|Zm7`B&24jBceLD1O>t@};MovCjJ5Pl(2?OTebau9?hfJZ1W$Jv?o2ke8=VeJp6)iY^$#46 zKEyW7m(u!Ohhm z)^>VBsw-Yhwwmjz;YEpv{X>F5mmshd?E8W5_p$C2=;8*UZR2e}gk^h5Lxwg%O17re zt!ml_y(Mo?kXHFFhpBGBB@^eCl*dkwE90yd^z9b|y^EI*l&9p7LP4@S=MP+YV~u%Zjy*#P^XlB`kYpRcfl;4P8)zNBj6QxIKa;%}+@^+ID;X>n z*P8UawOsM|ppc(>+Q90i(Aty00rwB`dE+wHdEeuBtBFoGHi;!EIE`zkSPi=nO4nJz z!~9EnN8Q)q*qg-v@us@&lKO_XYX9X;hlk{6mA(PDm>?Gzve!u|t~Br*Tvvf# z?A^7tTTF8GVi%*?t92xV5}7CK;mQ-~Vu`V-OcZ86$Bs%C$>`IdY?jZ3Zmg5L57tFg z`pwjGjVM==^i|CAu4z0hMNuA=1v3z$;3 zuMCW5T*LLMu&wBbjRguA$yy^|I1MPJbF{9{4;E_t&+Ymt?_b}BKGatG_>qaL#GKETJh<0R+JP>&Mux^(iF$p zV2yJ;P?J8>CzhI}Ro=_9-Pib^zjCXY1l!WufWBf0$ctIaEY)I>)7=}5EysRi8h?BNPt=SOmzIU4G zk{7&{s`2x|?k$N?E(3Dq8A zp8DxZTJ4W%f|^fc$axc`3dng?TJ=~Ovq^@1j=N6^ar+!MpA_QvIToE1681UrP6~

ZuOpWb0-tj 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