From 93957065241c7ed33b19cb16d8ba12e922c4c986 Mon Sep 17 00:00:00 2001 From: hasanabs Date: Thu, 3 Oct 2024 14:30:13 +0300 Subject: [PATCH] Init fork from Stuart Robinson's repo --- .../ESP32/30_I2C_Scanner/30_I2C_Scanner.ino | 72 + .../43_SD_Card_Test_ESP32.ino | 262 + .../44_SD_Card_Test_With_FS_ESP32.ino | 99 + .../testfile.txt | 1062 +++ .../45_Battery_Voltage_Read_Test_ESP32.ino | 104 + .../47_DeepSleep_Timed_Wakeup_ESP32.ino | 90 + .../50_LightSleep_Timed_Wakeup_ESP32.ino | 91 + .../51_DeepSleep_Switch_Wakeup_ESP32.ino | 86 + .../64_WiFi_Scanner_Display_ESP32.ino | 131 + examples/ReadMe.md | 636 ++ .../103_LoRa_Transmitter_Detailed_Setup.ino | 175 + .../Settings.h | 42 + .../104_LoRa_Receiver_Detailed_Setup.ino | 240 + .../Settings.h | 44 + .../14_LoRa_Structure_TX.ino | 144 + .../Basics/14_LoRa_Structure_TX/Settings.h | 30 + .../15_LoRa_Structure_RX.ino | 193 + .../Basics/15_LoRa_Structure_RX/Settings.h | 30 + .../Basics/1_LED_Blink/1_LED_Blink.ino | 61 + .../2_Register_Test/2_Register_Test.ino | 358 + .../3_LoRa_Transmitter/3_LoRa_Transmitter.ino | 174 + .../Basics/3_LoRa_Transmitter/Settings.h | 34 + .../3_LoRa_TransmitterIRQ.ino | 123 + .../4_LoRa_Receiver/4_LoRa_Receiver.ino | 237 + .../Basics/4_LoRa_Receiver/Settings.h | 35 + .../4_LoRa_ReceiverIRQ/4_LoRa_ReceiverIRQ.ino | 168 + .../52_FLRC_Transmitter.ino | 175 + .../Basics/52_FLRC_Transmitter/Settings.h | 31 + .../53_FLRC_Receiver/53_FLRC_Receiver.ino | 213 + .../Basics/53_FLRC_Receiver/Settings.h | 33 + .../238_StuartCAM_LoRa_Remote_Camera.ino | 158 + .../ARDUCAM_LICENSE.txt | 458 ++ .../238_StuartCAM_LoRa_Remote_Camera/OV2640.h | 253 + .../Settings.h | 72 + .../239_StuartCAM_LoRa_Receiver.ino | 142 + .../239_StuartCAM_LoRa_Receiver/Settings.h | 60 + .../240_StuartCAM_ESP32CAM.ino | 447 ++ .../Camera/240_StuartCAM_ESP32CAM/Settings.h | 92 + .../241_StuartCAM_ESP32CAM_LoRa_Receiver.ino | 222 + .../Settings.h | 63 + ...tCAM_ESP32CAM_LoRa_Receiver_PCTransfer.ino | 253 + .../Settings.h | 69 + .../YModemArray.h | 280 + ...245_StuartCAM_LoRa_Receiver_PCTransfer.ino | 200 + .../DTSDlibrary.h | 461 ++ .../SDtransferDisplay.h | 1557 +++++ .../Settings.h | 71 + .../YModem.h | 264 + .../SX128x_examples/DataTransfer/$50SATL.JPG | Bin 0 -> 63091 bytes .../SX128x_examples/DataTransfer/$50SATS.JPG | Bin 0 -> 6880 bytes .../SX128x_examples/DataTransfer/$50SATT.JPG | Bin 0 -> 1068 bytes .../221_LoRa_Packet_Monitor.ino | 166 + .../221_LoRa_Packet_Monitor/Settings.h | 31 + .../222_FLRC_Packet_Monitor.ino | 162 + .../222_FLRC_Packet_Monitor/Settings.h | 34 + ...230_Arduino_to_PC_File_Transfer_YModem.ino | 103 + .../YModem.h | 264 + .../231_Data_Transfer_Test_Transmitter.ino | 202 + .../DTLibrarySIM.h | 973 +++ .../DTSettings.h | 60 + .../232_Data_Transfer_Test_Receiver.ino | 134 + .../DTLibrarySIM.h | 972 +++ .../DTSettings.h | 60 + .../233_SDfile_Transfer_Transmitter.ino | 189 + .../DTSettings.h | 64 + .../234_SDfile_Transfer_Receiver.ino | 164 + .../234_SDfile_Transfer_Receiver/DTSettings.h | 64 + .../235_Array_Transfer_Transmitter.ino | 846 +++ .../235_Array_Transfer_Transmitter/Settings.h | 50 + .../Variables.h | 35 + .../236_SDfile_Transfer_TransmitterIRQ.ino | 191 + .../DTSettings.h | 60 + .../237_SDfile_Transfer_ReceiverIRQ.ino | 165 + .../DTSettings.h | 61 + .../Data_transfer_packet_definitions.md | 340 + .../Data_transfer_packet_definitions.txt | 340 + .../SX128x_examples/DataTransfer/ReadMe.md | 79 + .../10_LoRa_Link_Test_Transmitter.ino | 258 + .../10_LoRa_Link_Test_Transmitter/Settings.h | 39 + .../11_LoRa_Packet_Logger_Receiver.ino | 213 + .../11_LoRa_Packet_Logger_Receiver/Settings.h | 33 + .../16_LoRa_RX_Frequency_Error_Check.ino | 171 + .../Settings.h | 35 + .../20_LoRa_Link_Test_Receiver.ino | 308 + .../20_LoRa_Link_Test_Receiver/Settings.h | 36 + .../33_LoRa_RSSI_Checker_With_Display.ino | 274 + .../Settings.h | 33 + ..._FLRC_Data_Throughput_Test_Transmitter.ino | 278 + .../Settings.h | 38 + ..._LoRa_Data_Throughput_Test_Transmitter.ino | 274 + .../Settings.h | 38 + ...C_Data_Throughput_Acknowledge_Receiver.ino | 177 + .../Settings.h | 33 + ...a_Data_Throughput_Acknowledge_Receiver.ino | 165 + .../Settings.h | 33 + ..._LoRa_Transmitter_Detailed_Setup_ESP32.ino | 183 + .../Settings.h | 45 + ...104_LoRa_Receiver_Detailed_Setup_ESP32.ino | 223 + .../Settings.h | 40 + .../ESP32/ESP32_Bare_Bones_Schematic.jpg | Bin 0 -> 38883 bytes examples/SX128x_examples/ESP32/ReadMe.md | 111 + .../8_LoRa_LowMemory_TX.ino | 153 + .../LowMemory/8_LoRa_LowMemory_TX/Settings.h | 30 + .../8_LoRa_LowMemory_TXIRQ.ino | 106 + .../8_LoRa_LowMemory_TXIRQ/Settings.h | 28 + .../9_LoRa_LowMemory_RX.ino | 192 + .../LowMemory/9_LoRa_LowMemory_RX/Settings.h | 32 + .../9_LoRa_LowMemory_RXIRQ.ino | 169 + .../9_LoRa_LowMemory_RXIRQ/Settings.h | 27 + .../Pictures/Calibration_Values.jpg | Bin 0 -> 20046 bytes .../Pictures/IMG_2531_Reduced.jpg | Bin 0 -> 227865 bytes .../Pictures/SX128XLT_Ranging_100m.jpg | Bin 0 -> 22942 bytes .../SX128X_Ranging_Calibration_Values.jpg | Bin 0 -> 18784 bytes ...223_Ranging_Transmitter_Data_Requestor.ino | 427 ++ .../Settings.h | 39 + .../224_Ranging_Receiver_Data_Requestor.ino | 322 + .../Settings.h | 39 + .../54_Ranging_Master/54_Ranging_Master.ino | 224 + .../Ranging/54_Ranging_Master/Settings.h | 42 + .../55_Ranging_Slave/55_Ranging_Slave.ino | 137 + .../Ranging/55_Ranging_Slave/Settings.h | 34 + .../56_Ranging_Calibration_Checker.ino | 190 + .../56_Ranging_Calibration_Checker/Settings.h | 40 + .../SX128x_examples/Ranging/85km Ranging.jpg | Bin 0 -> 227865 bytes .../Ranging/Calibration_Values.jpg | Bin 0 -> 20046 bytes examples/SX128x_examples/Ranging/ReadMe.md | 113 + .../Ranging/SX128XLT_Ranging_100m.jpg | Bin 0 -> 22942 bytes .../SX128X_Ranging_Calibration_Values.jpg | Bin 0 -> 18784 bytes examples/SX128x_examples/ReadMe.md | 395 ++ .../201_Reliable_Transmitter.ino | 160 + .../202_Reliable_Receiver.ino | 195 + ...iable_Transmitter_Controller_Structure.ino | 155 + ...Reliable_Receiver_Controller_Structure.ino | 197 + ...eliable_Transmitter_Controller_ArrayRW.ino | 149 + ...6_Reliable_Receiver_Controller_ArrayRW.ino | 202 + .../207_Reliable_SXTransmitter_Controller.ino | 150 + .../208_Reliable_SXReceiver_Controller.ino | 200 + .../209_Reliable_Transmitter_AutoACK.ino | 153 + .../210_Reliable_Receiver_AutoACK.ino | 153 + .../211_Reliable_SXTransmitter_AutoACK.ino | 157 + .../212_Reliable_SXReceiver_AutoACK.ino | 212 + ...iable_Transmitter_Controller_ManualACK.ino | 163 + ...Reliable_Receiver_Controller_ManualACK.ino | 215 + .../215_Reliable_Transmitter_ACK_withData.ino | 148 + .../216_Reliable_Receiver_ACK_withData.ino | 172 + ...17_Reliable_Transmitter_Data_Requestor.ino | 238 + .../Settings.h | 29 + .../218_Reliable_Receiver_Data_Requestor.ino | 249 + .../Settings.h | 37 + ...Reliable_Transmitter_Data_RequestorIRQ.ino | 240 + .../Settings.h | 28 + ...20_Reliable_Receiver_Data_RequestorIRQ.ino | 252 + .../Settings.h | 36 + .../221_LoRa_Packet_Monitor.ino | 157 + .../221_LoRa_Packet_Monitor/Settings.h | 29 + .../222_FLRC_Packet_Monitor.ino | 157 + .../222_FLRC_Packet_Monitor/Settings.h | 31 + examples/SX128x_examples/Reliable/ReadMe.md | 389 ++ .../21_On_Off_Transmitter.ino | 306 + .../21_On_Off_Transmitter/Settings.h | 42 + .../22_On_Off_Receiver/22_On_Off_Receiver.ino | 272 + .../22_On_Off_Receiver/Settings.h | 41 + .../35_Remote_Control_Servo_Transmitter.ino | 189 + .../Settings.h | 41 + .../36_Remote_Control_Servo_Receiver.ino | 244 + .../Settings.h | 39 + .../17_Sensor_Transmitter.ino | 314 + .../Sensor/17_Sensor_Transmitter/Settings.h | 38 + .../18_Sensor_Receiver/18_Sensor_Receiver.ino | 357 + .../Sensor/18_Sensor_Receiver/Settings.h | 38 + .../105_LoRa_Serial_Bridge_Transmitter.ino | 189 + .../106_LoRa_Serial_Bridge_Receiver.ino | 218 + .../107_LoRa_Serial_Bridge_GPS_Receiver.ino | 256 + .../5_LoRa_TX_Sleep_Timed_Wakeup_Atmel.ino | 233 + .../Settings.h | 31 + .../62_LoRa_Wake_on_RX_Atmel.ino | 215 + .../Sleep/62_LoRa_Wake_on_RX_Atmel/Settings.h | 40 + .../23_GPS_Tracker_Transmitter.ino | 399 ++ .../23_GPS_Tracker_Transmitter/Settings.h | 63 + .../24_GPS_Tracker_Receiver.ino | 313 + .../24_GPS_Tracker_Receiver/Settings.h | 37 + ..._Tracker_Receiver_With_Display_and_GPS.ino | 613 ++ .../Settings.h | 54 + .../Tracker/38_lora_Relay/38_lora_Relay.ino | 156 + .../Tracker/38_lora_Relay/Settings.h | 43 + keywords.txt | 283 + library.properties | 9 + src/AFSKRTTY.h | 75 + src/AFSKRTTY2.h | 180 + src/ARtransfer.h | 1570 +++++ src/ARtransferIRQ.h | 1571 +++++ src/DTSDlibrary.h | 461 ++ src/EEPROM_Memory.h | 88 + src/LICENSE.txt | 25 + src/ProgramLT_Definitions.h | 155 + src/SDtransfer.h | 1522 +++++ src/SDtransferIRQ.h | 1518 +++++ src/SX128XLT.cpp | 5969 +++++++++++++++++ src/SX128XLT.h | 259 + src/SX128XLT_Definitions.h | 439 ++ src/arrayRW.h | 270 + 201 files changed, 45709 insertions(+) create mode 100644 examples/Hardware_Checks/ESP32/30_I2C_Scanner/30_I2C_Scanner.ino create mode 100644 examples/Hardware_Checks/ESP32/43_SD_Card_Test_ESP32/43_SD_Card_Test_ESP32.ino create mode 100644 examples/Hardware_Checks/ESP32/44_SD_Card_Test_With_FS_ESP32/44_SD_Card_Test_With_FS_ESP32.ino create mode 100644 examples/Hardware_Checks/ESP32/44_SD_Card_Test_With_FS_ESP32/testfile.txt create mode 100644 examples/Hardware_Checks/ESP32/45_Battery_Voltage_Read_Test_ESP32/45_Battery_Voltage_Read_Test_ESP32.ino create mode 100644 examples/Hardware_Checks/ESP32/47_DeepSleep_Timed_Wakeup_ESP32/47_DeepSleep_Timed_Wakeup_ESP32.ino create mode 100644 examples/Hardware_Checks/ESP32/50_LightSleep_Timed_Wakeup_ESP32/50_LightSleep_Timed_Wakeup_ESP32.ino create mode 100644 examples/Hardware_Checks/ESP32/51_DeepSleep_Switch_Wakeup_ESP32/51_DeepSleep_Switch_Wakeup_ESP32.ino create mode 100644 examples/Hardware_Checks/ESP32/64_WiFi_Scanner_Display_ESP32/64_WiFi_Scanner_Display_ESP32.ino create mode 100644 examples/ReadMe.md create mode 100644 examples/SX128x_examples/Basics/103_LoRa_Transmitter_Detailed_Setup/103_LoRa_Transmitter_Detailed_Setup.ino create mode 100644 examples/SX128x_examples/Basics/103_LoRa_Transmitter_Detailed_Setup/Settings.h create mode 100644 examples/SX128x_examples/Basics/104_LoRa_Receiver_Detailed_Setup/104_LoRa_Receiver_Detailed_Setup.ino create mode 100644 examples/SX128x_examples/Basics/104_LoRa_Receiver_Detailed_Setup/Settings.h create mode 100644 examples/SX128x_examples/Basics/14_LoRa_Structure_TX/14_LoRa_Structure_TX.ino create mode 100644 examples/SX128x_examples/Basics/14_LoRa_Structure_TX/Settings.h create mode 100644 examples/SX128x_examples/Basics/15_LoRa_Structure_RX/15_LoRa_Structure_RX.ino create mode 100644 examples/SX128x_examples/Basics/15_LoRa_Structure_RX/Settings.h create mode 100644 examples/SX128x_examples/Basics/1_LED_Blink/1_LED_Blink.ino create mode 100644 examples/SX128x_examples/Basics/2_Register_Test/2_Register_Test.ino create mode 100644 examples/SX128x_examples/Basics/3_LoRa_Transmitter/3_LoRa_Transmitter.ino create mode 100644 examples/SX128x_examples/Basics/3_LoRa_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/Basics/3_LoRa_TransmitterIRQ/3_LoRa_TransmitterIRQ.ino create mode 100644 examples/SX128x_examples/Basics/4_LoRa_Receiver/4_LoRa_Receiver.ino create mode 100644 examples/SX128x_examples/Basics/4_LoRa_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Basics/4_LoRa_ReceiverIRQ/4_LoRa_ReceiverIRQ.ino create mode 100644 examples/SX128x_examples/Basics/52_FLRC_Transmitter/52_FLRC_Transmitter.ino create mode 100644 examples/SX128x_examples/Basics/52_FLRC_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/Basics/53_FLRC_Receiver/53_FLRC_Receiver.ino create mode 100644 examples/SX128x_examples/Basics/53_FLRC_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/238_StuartCAM_LoRa_Remote_Camera.ino create mode 100644 examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/ARDUCAM_LICENSE.txt create mode 100644 examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/OV2640.h create mode 100644 examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/Settings.h create mode 100644 examples/SX128x_examples/Camera/239_StuartCAM_LoRa_Receiver/239_StuartCAM_LoRa_Receiver.ino create mode 100644 examples/SX128x_examples/Camera/239_StuartCAM_LoRa_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Camera/240_StuartCAM_ESP32CAM/240_StuartCAM_ESP32CAM.ino create mode 100644 examples/SX128x_examples/Camera/240_StuartCAM_ESP32CAM/Settings.h create mode 100644 examples/SX128x_examples/Camera/241_StuartCAM_ESP32CAM_LoRa_Receiver/241_StuartCAM_ESP32CAM_LoRa_Receiver.ino create mode 100644 examples/SX128x_examples/Camera/241_StuartCAM_ESP32CAM_LoRa_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer.ino create mode 100644 examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/Settings.h create mode 100644 examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/YModemArray.h create mode 100644 examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/245_StuartCAM_LoRa_Receiver_PCTransfer.ino create mode 100644 examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/DTSDlibrary.h create mode 100644 examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/SDtransferDisplay.h create mode 100644 examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/Settings.h create mode 100644 examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/YModem.h create mode 100644 examples/SX128x_examples/DataTransfer/$50SATL.JPG create mode 100644 examples/SX128x_examples/DataTransfer/$50SATS.JPG create mode 100644 examples/SX128x_examples/DataTransfer/$50SATT.JPG create mode 100644 examples/SX128x_examples/DataTransfer/221_LoRa_Packet_Monitor/221_LoRa_Packet_Monitor.ino create mode 100644 examples/SX128x_examples/DataTransfer/221_LoRa_Packet_Monitor/Settings.h create mode 100644 examples/SX128x_examples/DataTransfer/222_FLRC_Packet_Monitor/222_FLRC_Packet_Monitor.ino create mode 100644 examples/SX128x_examples/DataTransfer/222_FLRC_Packet_Monitor/Settings.h create mode 100644 examples/SX128x_examples/DataTransfer/230_Arduino_to_PC_File_Transfer_YModem/230_Arduino_to_PC_File_Transfer_YModem.ino create mode 100644 examples/SX128x_examples/DataTransfer/230_Arduino_to_PC_File_Transfer_YModem/YModem.h create mode 100644 examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/231_Data_Transfer_Test_Transmitter.ino create mode 100644 examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/DTLibrarySIM.h create mode 100644 examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/DTSettings.h create mode 100644 examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/232_Data_Transfer_Test_Receiver.ino create mode 100644 examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/DTLibrarySIM.h create mode 100644 examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/DTSettings.h create mode 100644 examples/SX128x_examples/DataTransfer/233_SDfile_Transfer_Transmitter/233_SDfile_Transfer_Transmitter.ino create mode 100644 examples/SX128x_examples/DataTransfer/233_SDfile_Transfer_Transmitter/DTSettings.h create mode 100644 examples/SX128x_examples/DataTransfer/234_SDfile_Transfer_Receiver/234_SDfile_Transfer_Receiver.ino create mode 100644 examples/SX128x_examples/DataTransfer/234_SDfile_Transfer_Receiver/DTSettings.h create mode 100644 examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/235_Array_Transfer_Transmitter.ino create mode 100644 examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/Variables.h create mode 100644 examples/SX128x_examples/DataTransfer/236_SDfile_Transfer_TransmitterIRQ/236_SDfile_Transfer_TransmitterIRQ.ino create mode 100644 examples/SX128x_examples/DataTransfer/236_SDfile_Transfer_TransmitterIRQ/DTSettings.h create mode 100644 examples/SX128x_examples/DataTransfer/237_SDfile_Transfer_ReceiverIRQ/237_SDfile_Transfer_ReceiverIRQ.ino create mode 100644 examples/SX128x_examples/DataTransfer/237_SDfile_Transfer_ReceiverIRQ/DTSettings.h create mode 100644 examples/SX128x_examples/DataTransfer/Data_transfer_packet_definitions.md create mode 100644 examples/SX128x_examples/DataTransfer/Data_transfer_packet_definitions.txt create mode 100644 examples/SX128x_examples/DataTransfer/ReadMe.md create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/10_LoRa_Link_Test_Transmitter/10_LoRa_Link_Test_Transmitter.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/10_LoRa_Link_Test_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/11_LoRa_Packet_Logger_Receiver/11_LoRa_Packet_Logger_Receiver.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/11_LoRa_Packet_Logger_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/16_LoRa_RX_Frequency_Error_Check/16_LoRa_RX_Frequency_Error_Check.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/16_LoRa_RX_Frequency_Error_Check/Settings.h create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/20_LoRa_Link_Test_Receiver/20_LoRa_Link_Test_Receiver.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/20_LoRa_Link_Test_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/33_LoRa_RSSI_Checker_With_Display/33_LoRa_RSSI_Checker_With_Display.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/33_LoRa_RSSI_Checker_With_Display/Settings.h create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/42_FLRC_Data_Throughput_Test_Transmitter/42_FLRC_Data_Throughput_Test_Transmitter.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/42_FLRC_Data_Throughput_Test_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/42_LoRa_Data_Throughput_Test_Transmitter/42_LoRa_Data_Throughput_Test_Transmitter.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/42_LoRa_Data_Throughput_Test_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/43_FLRC_Data_Throughput_Acknowledge_Receiver/43_FLRC_Data_Throughput_Acknowledge_Receiver.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/43_FLRC_Data_Throughput_Acknowledge_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/43_LoRa_Data_Throughput_Acknowledge_Receiver/43_LoRa_Data_Throughput_Acknowledge_Receiver.ino create mode 100644 examples/SX128x_examples/Diagnostic_and_Test/43_LoRa_Data_Throughput_Acknowledge_Receiver/Settings.h create mode 100644 examples/SX128x_examples/ESP32/Basics/103_LoRa_Transmitter_Detailed_Setup_ESP32/103_LoRa_Transmitter_Detailed_Setup_ESP32.ino create mode 100644 examples/SX128x_examples/ESP32/Basics/103_LoRa_Transmitter_Detailed_Setup_ESP32/Settings.h create mode 100644 examples/SX128x_examples/ESP32/Basics/104_LoRa_Receiver_Detailed_Setup_ESP32/104_LoRa_Receiver_Detailed_Setup_ESP32.ino create mode 100644 examples/SX128x_examples/ESP32/Basics/104_LoRa_Receiver_Detailed_Setup_ESP32/Settings.h create mode 100644 examples/SX128x_examples/ESP32/ESP32_Bare_Bones_Schematic.jpg create mode 100644 examples/SX128x_examples/ESP32/ReadMe.md create mode 100644 examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TX/8_LoRa_LowMemory_TX.ino create mode 100644 examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TX/Settings.h create mode 100644 examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TXIRQ/8_LoRa_LowMemory_TXIRQ.ino create mode 100644 examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TXIRQ/Settings.h create mode 100644 examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RX/9_LoRa_LowMemory_RX.ino create mode 100644 examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RX/Settings.h create mode 100644 examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RXIRQ/9_LoRa_LowMemory_RXIRQ.ino create mode 100644 examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RXIRQ/Settings.h create mode 100644 examples/SX128x_examples/Pictures/Calibration_Values.jpg create mode 100644 examples/SX128x_examples/Pictures/IMG_2531_Reduced.jpg create mode 100644 examples/SX128x_examples/Pictures/SX128XLT_Ranging_100m.jpg create mode 100644 examples/SX128x_examples/Pictures/SX128X_Ranging_Calibration_Values.jpg create mode 100644 examples/SX128x_examples/Ranging/223_Ranging_Transmitter_Data_Requestor/223_Ranging_Transmitter_Data_Requestor.ino create mode 100644 examples/SX128x_examples/Ranging/223_Ranging_Transmitter_Data_Requestor/Settings.h create mode 100644 examples/SX128x_examples/Ranging/224_Ranging_Receiver_Data_Requestor/224_Ranging_Receiver_Data_Requestor.ino create mode 100644 examples/SX128x_examples/Ranging/224_Ranging_Receiver_Data_Requestor/Settings.h create mode 100644 examples/SX128x_examples/Ranging/54_Ranging_Master/54_Ranging_Master.ino create mode 100644 examples/SX128x_examples/Ranging/54_Ranging_Master/Settings.h create mode 100644 examples/SX128x_examples/Ranging/55_Ranging_Slave/55_Ranging_Slave.ino create mode 100644 examples/SX128x_examples/Ranging/55_Ranging_Slave/Settings.h create mode 100644 examples/SX128x_examples/Ranging/56_Ranging_Calibration_Checker/56_Ranging_Calibration_Checker.ino create mode 100644 examples/SX128x_examples/Ranging/56_Ranging_Calibration_Checker/Settings.h create mode 100644 examples/SX128x_examples/Ranging/85km Ranging.jpg create mode 100644 examples/SX128x_examples/Ranging/Calibration_Values.jpg create mode 100644 examples/SX128x_examples/Ranging/ReadMe.md create mode 100644 examples/SX128x_examples/Ranging/SX128XLT_Ranging_100m.jpg create mode 100644 examples/SX128x_examples/Ranging/SX128X_Ranging_Calibration_Values.jpg create mode 100644 examples/SX128x_examples/ReadMe.md create mode 100644 examples/SX128x_examples/Reliable/201_Reliable_Transmitter/201_Reliable_Transmitter.ino create mode 100644 examples/SX128x_examples/Reliable/202_Reliable_Receiver/202_Reliable_Receiver.ino create mode 100644 examples/SX128x_examples/Reliable/203_Reliable_Transmitter_Controller_Structure/203_Reliable_Transmitter_Controller_Structure.ino create mode 100644 examples/SX128x_examples/Reliable/204_Reliable_Receiver_Controller_Structure/204_Reliable_Receiver_Controller_Structure.ino create mode 100644 examples/SX128x_examples/Reliable/205_Reliable_Transmitter_Controller_ArrayRW/205_Reliable_Transmitter_Controller_ArrayRW.ino create mode 100644 examples/SX128x_examples/Reliable/206_Reliable_Receiver_Controller_ArrayRW/206_Reliable_Receiver_Controller_ArrayRW.ino create mode 100644 examples/SX128x_examples/Reliable/207_Reliable_SXTransmitter_Controller/207_Reliable_SXTransmitter_Controller.ino create mode 100644 examples/SX128x_examples/Reliable/208_Reliable_SXReceiver_Controller/208_Reliable_SXReceiver_Controller.ino create mode 100644 examples/SX128x_examples/Reliable/209_Reliable_Transmitter_AutoACK/209_Reliable_Transmitter_AutoACK.ino create mode 100644 examples/SX128x_examples/Reliable/210_Reliable_Receiver_AutoACK/210_Reliable_Receiver_AutoACK.ino create mode 100644 examples/SX128x_examples/Reliable/211_Reliable_SXTransmitter_AutoACK/211_Reliable_SXTransmitter_AutoACK.ino create mode 100644 examples/SX128x_examples/Reliable/212_Reliable_SXReceiver_AutoACK/212_Reliable_SXReceiver_AutoACK.ino create mode 100644 examples/SX128x_examples/Reliable/213_Reliable_Transmitter_Controller_ManualACK/213_Reliable_Transmitter_Controller_ManualACK.ino create mode 100644 examples/SX128x_examples/Reliable/214_Reliable_Receiver_Controller_ManualACK/214_Reliable_Receiver_Controller_ManualACK.ino create mode 100644 examples/SX128x_examples/Reliable/215_Reliable_Transmitter_ACK_withData/215_Reliable_Transmitter_ACK_withData.ino create mode 100644 examples/SX128x_examples/Reliable/216_Reliable_Receiver_ACK_withData/216_Reliable_Receiver_ACK_withData.ino create mode 100644 examples/SX128x_examples/Reliable/217_Reliable_Transmitter_Data_Requestor/217_Reliable_Transmitter_Data_Requestor.ino create mode 100644 examples/SX128x_examples/Reliable/217_Reliable_Transmitter_Data_Requestor/Settings.h create mode 100644 examples/SX128x_examples/Reliable/218_Reliable_Receiver_Data_Requestor/218_Reliable_Receiver_Data_Requestor.ino create mode 100644 examples/SX128x_examples/Reliable/218_Reliable_Receiver_Data_Requestor/Settings.h create mode 100644 examples/SX128x_examples/Reliable/219_Reliable_Transmitter_Data_RequestorIRQ/219_Reliable_Transmitter_Data_RequestorIRQ.ino create mode 100644 examples/SX128x_examples/Reliable/219_Reliable_Transmitter_Data_RequestorIRQ/Settings.h create mode 100644 examples/SX128x_examples/Reliable/220_Reliable_Receiver_Data_RequestorIRQ/220_Reliable_Receiver_Data_RequestorIRQ.ino create mode 100644 examples/SX128x_examples/Reliable/220_Reliable_Receiver_Data_RequestorIRQ/Settings.h create mode 100644 examples/SX128x_examples/Reliable/221_LoRa_Packet_Monitor/221_LoRa_Packet_Monitor.ino create mode 100644 examples/SX128x_examples/Reliable/221_LoRa_Packet_Monitor/Settings.h create mode 100644 examples/SX128x_examples/Reliable/222_FLRC_Packet_Monitor/222_FLRC_Packet_Monitor.ino create mode 100644 examples/SX128x_examples/Reliable/222_FLRC_Packet_Monitor/Settings.h create mode 100644 examples/SX128x_examples/Reliable/ReadMe.md create mode 100644 examples/SX128x_examples/RemoteControl/21_On_Off_Transmitter/21_On_Off_Transmitter.ino create mode 100644 examples/SX128x_examples/RemoteControl/21_On_Off_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/RemoteControl/22_On_Off_Receiver/22_On_Off_Receiver.ino create mode 100644 examples/SX128x_examples/RemoteControl/22_On_Off_Receiver/Settings.h create mode 100644 examples/SX128x_examples/RemoteControl/35_Remote_Control_Servo_Transmitter/35_Remote_Control_Servo_Transmitter.ino create mode 100644 examples/SX128x_examples/RemoteControl/35_Remote_Control_Servo_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/RemoteControl/36_Remote_Control_Servo_Receiver/36_Remote_Control_Servo_Receiver.ino create mode 100644 examples/SX128x_examples/RemoteControl/36_Remote_Control_Servo_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Sensor/17_Sensor_Transmitter/17_Sensor_Transmitter.ino create mode 100644 examples/SX128x_examples/Sensor/17_Sensor_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/Sensor/18_Sensor_Receiver/18_Sensor_Receiver.ino create mode 100644 examples/SX128x_examples/Sensor/18_Sensor_Receiver/Settings.h create mode 100644 examples/SX128x_examples/SerialBridge/105_LoRa_Serial_Bridge_Transmitter/105_LoRa_Serial_Bridge_Transmitter.ino create mode 100644 examples/SX128x_examples/SerialBridge/106_LoRa_Serial_Bridge_Receiver/106_LoRa_Serial_Bridge_Receiver.ino create mode 100644 examples/SX128x_examples/SerialBridge/107_LoRa_Serial_Bridge_GPS_Receiver/107_LoRa_Serial_Bridge_GPS_Receiver.ino create mode 100644 examples/SX128x_examples/Sleep/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel.ino create mode 100644 examples/SX128x_examples/Sleep/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel/Settings.h create mode 100644 examples/SX128x_examples/Sleep/62_LoRa_Wake_on_RX_Atmel/62_LoRa_Wake_on_RX_Atmel.ino create mode 100644 examples/SX128x_examples/Sleep/62_LoRa_Wake_on_RX_Atmel/Settings.h create mode 100644 examples/SX128x_examples/Tracker/23_GPS_Tracker_Transmitter/23_GPS_Tracker_Transmitter.ino create mode 100644 examples/SX128x_examples/Tracker/23_GPS_Tracker_Transmitter/Settings.h create mode 100644 examples/SX128x_examples/Tracker/24_GPS_Tracker_Receiver/24_GPS_Tracker_Receiver.ino create mode 100644 examples/SX128x_examples/Tracker/24_GPS_Tracker_Receiver/Settings.h create mode 100644 examples/SX128x_examples/Tracker/25_GPS_Tracker_Receiver_With_Display_and_GPS/25_GPS_Tracker_Receiver_With_Display_and_GPS.ino create mode 100644 examples/SX128x_examples/Tracker/25_GPS_Tracker_Receiver_With_Display_and_GPS/Settings.h create mode 100644 examples/SX128x_examples/Tracker/38_lora_Relay/38_lora_Relay.ino create mode 100644 examples/SX128x_examples/Tracker/38_lora_Relay/Settings.h create mode 100644 keywords.txt create mode 100644 library.properties create mode 100644 src/AFSKRTTY.h create mode 100644 src/AFSKRTTY2.h create mode 100644 src/ARtransfer.h create mode 100644 src/ARtransferIRQ.h create mode 100644 src/DTSDlibrary.h create mode 100644 src/EEPROM_Memory.h create mode 100644 src/LICENSE.txt create mode 100644 src/ProgramLT_Definitions.h create mode 100644 src/SDtransfer.h create mode 100644 src/SDtransferIRQ.h create mode 100644 src/SX128XLT.cpp create mode 100644 src/SX128XLT.h create mode 100644 src/SX128XLT_Definitions.h create mode 100644 src/arrayRW.h diff --git a/examples/Hardware_Checks/ESP32/30_I2C_Scanner/30_I2C_Scanner.ino b/examples/Hardware_Checks/ESP32/30_I2C_Scanner/30_I2C_Scanner.ino new file mode 100644 index 0000000..61bd0ae --- /dev/null +++ b/examples/Hardware_Checks/ESP32/30_I2C_Scanner/30_I2C_Scanner.ino @@ -0,0 +1,72 @@ +// -------------------------------------- +// i2c_scanner +// from: https://playground.arduino.cc/Main/I2cScanner/ + +// This sketch tests the standard 7-bit addresses +// Devices with higher bit address might not be seen properly. + + +#include +uint16_t counter; + +#define SDA 21 +#define SCL 22 + +void setup() +{ + Wire.begin(SDA, SCL); //format is Wire.begin(SDA,SCL); + + Serial.begin(9600); + Serial.println(F("I2C Scanner starting")); + Serial.println(); +} + + +void loop() +{ + uint8_t error, address; + int16_t nDevices; + + counter++; + Serial.print(counter); + Serial.println(F(" Scanning...")); + + nDevices = 0; + for (address = 1; address < 127; address++ ) + { + // The i2c_scanner uses the return value of + // the Write.endTransmisstion to see if + // a device did acknowledge to the address. + Wire.beginTransmission(address); + error = Wire.endTransmission(); + + if (error == 0) + { + Serial.print(F("I2C device found at address 0x")); + if (address < 16) + Serial.print(F("0")); + Serial.println(address, HEX); + nDevices++; + } + else if (error == 4) + { + Serial.print(F("Unknown error at address 0x")); + if (address < 16) + Serial.print(F("0")); + Serial.println(address, HEX); + } + } + + if (nDevices == 0) + { + Serial.println(F("No I2C devices found")); + } + else + { + Serial.println(); + Serial.println(F("Done")); + Serial.println(); + } + + delay(5000); // wait 5 seconds for next scan +} diff --git a/examples/Hardware_Checks/ESP32/43_SD_Card_Test_ESP32/43_SD_Card_Test_ESP32.ino b/examples/Hardware_Checks/ESP32/43_SD_Card_Test_ESP32/43_SD_Card_Test_ESP32.ino new file mode 100644 index 0000000..032d1d5 --- /dev/null +++ b/examples/Hardware_Checks/ESP32/43_SD_Card_Test_ESP32/43_SD_Card_Test_ESP32.ino @@ -0,0 +1,262 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 04/06/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This test program has been written to check that a connected SD card adapter, Micro + or standard, is functional. + + When the program runs it will attempts to create a file that is next in sequence to Log0000.txt, thus + if this is the first time the program has run on the SD card it will create file Log0001.txt. If the + file Log0001.txt exists it will create Log0002.txt etc. This ensures that everytime the program starts + it creates a new file for testing. + + Next the program checks if the Logxxxx.txt file exists and if so attempts to delete it. + + Then the program starts a loop. First the same file is again opened for append, it will be created if it + does not exist and then the line '1 Hello World' is writtent to the file. The file is closed and the + contents of the file are dumped to the serial monitor. The loop restarts, and this time the line + '2 Hello World' is appended to the file. + + As the program progresses the file will grow in size and after 4 iterrations of the open,write,close + and dump loop the file dump on serial monitor will look like this + + 1 Hello World + 2 Hello World + 3 Hello World + 4 Hello World + + This file dump will grow if you let the program run. If an error with the SD card is detected at any + time the LED will rapid flash continuously and the message 'X Card failed, or not present' is printed + to serial monitor. The number X will allow you to check the program listing for where the error occured. + + 1 Card failed = SD card did not initialise + 2 Card failed = Could nt setup logFile for new name + 3 Card failed = Could not open file for append + 4 Card failed = Failure to dump file to serial monitor + + Note: At the time of writing, 04/06/20, the function that the dumpFile() routine uses to check if an SD + card is still present, SD.exists(filename), does not work on the SD.h file included in the current Expressif + core for Arduino. If the SD card is removed, the SD.exists(filename) function returns true so the following + dump of the file locks up the ESP32. + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include +#include + +//pin definitions. You also need to connect the card SCK to pin 18, MISO to pin 19 and MOSI to pin 23 +#define LED1 2 //pin number for LED +#define SDCS 13 //pin number for device select on SD card module + +File logFile; + +char filename[] = "/LOG0000.TXT"; //filename used as base for creating logfile, 0000 replaced with numbers + +uint16_t linecount; + + +void loop() +{ + Serial.println(); + Serial.println("Initializing SD card"); + + if (!SD.begin(SDCS)) + { + cardFail(1); + } + + Serial.println("Card initialized"); + + logFile = SD.open("/"); + Serial.println("Card directory"); + Serial.println(); + printDirectory(logFile, 0); + Serial.println(); + Serial.println(); + + if (!setupSDLOG(filename)) //setup logfile name for writing + { + cardFail(2); + } + + Serial.print(F("logFile name is ")); + Serial.println(filename); + + if (SD.exists(filename)) + { + Serial.print(filename); + Serial.println(" exists - delete"); + SD.remove(filename); + } + + if (!SD.exists(filename)) + { + Serial.print(filename); + Serial.println(" does not exist"); + } + + while (1) + { + logFile = SD.open(filename, FILE_APPEND); + + if (!logFile) + { + cardFail(3); + } + + linecount++; + logFile.print(linecount); + logFile.println(" Hello World"); + logFile.close(); + + Serial.println(); + Serial.print("Dump file "); + Serial.println(filename); + Serial.println(); + + digitalWrite(LED1, HIGH); + + if (dumpFile(filename)) + { + Serial.println(); + Serial.println("Finished File Dump"); + } + else + { + cardFail(4); + } + + digitalWrite(LED1, LOW); + delay(2000); + } +} + + +void printDirectory(File dir, int numTabs) +{ + while (true) + { + + File entry = dir.openNextFile(); + if (! entry) + { + //no more files + break; + } + for (uint8_t i = 0; i < numTabs; i++) { + Serial.print('\t'); + } + Serial.print(entry.name()); + if (entry.isDirectory()) + { + Serial.println("/"); + printDirectory(entry, numTabs + 1); + } + else + { + //files have sizes, directories do not + Serial.print("\t\t"); + Serial.println(entry.size(), DEC); + } + entry.close(); + } +} + + +bool dumpFile(char *buf) +{ + //Note, this function will return true if the SD card is remove. See note at program start. + if (SD.exists(buf)) + { + Serial.print(buf); + Serial.println(" found"); + } + else + { + Serial.print(filename); + Serial.println(" not found"); + return false; + } + + logFile = SD.open(buf); + + if (logFile) //if the file is available, read from it + { + while (logFile.available()) + { + Serial.write(logFile.read()); + } + logFile.close(); + return true; + } + + return false; +} + + +uint8_t setupSDLOG(char *buf) +{ + //creats a new filename + + uint16_t index; + + for (index = 1; index <= 9999; index++) { + buf[4] = index / 1000 + '0'; + buf[5] = ((index % 1000) / 100) + '0'; + buf[6] = ((index % 100) / 10) + '0'; + buf[7] = index % 10 + '0' ; + if (! SD.exists(filename)) { + // only open a new file if it doesn't exist + logFile = SD.open(buf, FILE_WRITE); + break; + } + } + + if (!logFile) + { + return 0; + } + + return index; //return number of logfile created +} + + +void cardFail(uint8_t num) +{ + while (1) + { + Serial.print(num); //so we can tell where card failed + Serial.println(" Card failed, or not present"); + led_Flash(100, 25); + } +} + + +void led_Flash(unsigned int flashes, unsigned int delaymS) +{ + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //for PCB LED + led_Flash(4, 125); + + Serial.begin(9600); + Serial.println(F("43_SD_Card_Test_ESP32 Starting")); +} diff --git a/examples/Hardware_Checks/ESP32/44_SD_Card_Test_With_FS_ESP32/44_SD_Card_Test_With_FS_ESP32.ino b/examples/Hardware_Checks/ESP32/44_SD_Card_Test_With_FS_ESP32/44_SD_Card_Test_With_FS_ESP32.ino new file mode 100644 index 0000000..8f5a324 --- /dev/null +++ b/examples/Hardware_Checks/ESP32/44_SD_Card_Test_With_FS_ESP32/44_SD_Card_Test_With_FS_ESP32.ino @@ -0,0 +1,99 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/01/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This test program has been written to check that a connected SD card adapter, Micro + or standard, is funtional with the FS functions. To use the program first copy the file (in this programs + directory) called testfile.txt to the root directory of the SD card. + + When the program runs it will attempt to open 'testfile.txt' and spool the contents to the Arduino IDE + serial monitor. The testfile is part of the source code for the Apollo 11 Lunar Lander navigation and + guidance computer. There are LED flashes at power up or reset, then at start of every loop of the test. + The LED is on whilst the testfile is being read. If the LED flashes very rapidly then there is a problem + accessing the SD card. + + The program also has the option of using a logic pin to control the power to the lora and SD card + devices, which can save power in sleep mode. If the hardware is fitted to your board these devices are + powered on by setting the VCCPOWER pin low. If your board does not have this feature set VCCPOWER to -1. + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include "FS.h" +#include "SD.h" +#include "SPI.h" + + +#define SWITCH1 0 //pin number to attach switch +#define LED1 2 //pin number for LED +#define SDCS 13 //ESP32 pin number for device select on SD card module + + +void loop() +{ + Serial.println(F("LED Flash")); + Serial.println(); + led_Flash(2, 50); + readFile(SD, "/testfile.txt"); + delay(1000); +} + + +void readFile(fs::FS &fs, const char * path) { + Serial.printf("Reading file: %s\n", path); + + File file = fs.open(path); + if (!file) { + Serial.println(); + Serial.println("Failed to open file for reading"); + Serial.println(); + return; + } + + Serial.print("Read from file: "); + digitalWrite(LED1, HIGH); + while (file.available()) { + Serial.write(file.read()); + } + file.close(); + digitalWrite(LED1, LOW); +} + + +void led_Flash(unsigned int flashes, unsigned int delaymS) +{ + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //for PCB LED + led_Flash(4, 125); + + Serial.begin(9600); + + Serial.println(F("44_SD_Card_Test_With_FS_ESP32 Starting")); + Serial.print("Initializing SD card..."); + + if (!SD.begin(SDCS)) { + Serial.println("Card failed, or not present."); + led_Flash(100, 25); + return; //loop if no card found + } + + Serial.println("Card initialized."); +} diff --git a/examples/Hardware_Checks/ESP32/44_SD_Card_Test_With_FS_ESP32/testfile.txt b/examples/Hardware_Checks/ESP32/44_SD_Card_Test_With_FS_ESP32/testfile.txt new file mode 100644 index 0000000..3f10610 --- /dev/null +++ b/examples/Hardware_Checks/ESP32/44_SD_Card_Test_With_FS_ESP32/testfile.txt @@ -0,0 +1,1062 @@ +# Copyright: Public domain. +# Filename: BURN_BABY_BURN--MASTER_IGNITION_ROUTINE.agc +# Purpose: Part of the source code for Luminary 1A build 099. +# It is part of the source code for the Lunar Module's (LM) +# Apollo Guidance Computer (AGC), for Apollo 11. +# Assembler: yaYUL +# Contact: Ron Burkey . +# Website: www.ibiblio.org/apollo. +# Pages: 731-751 +# Mod history: 2009-05-19 RSB Adapted from the corresponding +# Luminary131 file, using page +# images from Luminary 1A. +# 2009-06-07 RSB Corrected 3 typos. +# 2009-07-23 RSB Added Onno's notes on the naming +# of this function, which he got from +# Don Eyles. +# +# This source code has been transcribed or otherwise adapted from +# digitized images of a hardcopy from the MIT Museum. The digitization +# was performed by Paul Fjeld, and arranged for by Deborah Douglas of +# the Museum. Many thanks to both. The images (with suitable reduction +# in storage size and consequent reduction in image quality as well) are +# available online at www.ibiblio.org/apollo. If for some reason you +# find that the images are illegible, contact me at info@sandroid.org +# about getting access to the (much) higher-quality images which Paul +# actually created. +# +# Notations on the hardcopy document read, in part: +# +# Assemble revision 001 of AGC program LMY99 by NASA 2021112-61 +# 16:27 JULY 14, 1969 + +# Page 731 +## At the get-together of the AGC developers celebrating the 40th anniversary +## of the first moonwalk, Don Eyles (one of the authors of this routine along +## with Peter Adler) has related to us a little interesting history behind the +## naming of the routine. +## +## It traces back to 1965 and the Los Angeles riots, and was inspired +## by disc jockey extraordinaire and radio station owner Magnificent Montague. +## Magnificent Montague used the phrase "Burn, baby! BURN!" when spinning the +## hottest new records. Magnificent Montague was the charismatic voice of +## soul music in Chicago, New York, and Los Angeles from the mid-1950s to +## the mid-1960s. +# BURN, BABY, BURN -- MASTER IGNITION ROUTINE + + BANK 36 + SETLOC P40S + BANK + EBANK= WHICH + COUNT* $$/P40 + +# THE MASTER IGNITION ROUTINE IS DESIGNED FOR USE BY THE FOLLOWING LEM PROGRAMS: P12, P40, P42, P61, P63. +# IT PERFORMS ALL FUNCTIONS IMMEDIATELY ASSOCIATED WITH APS OR DPS IGNITION: IN PARTICULAR, EVERYTHING LYING +# BETWEEN THE PRE-IGNITION TIME CHECK -- ARE WE WITHIN 45 SECONDS OF TIG? -- AND TIG + 26 SECONDS, WHEN DPS +# PROGRAMS THROTTLE UP. +# +# VARIATIONS AMONG PROGRAMS ARE ACCOMODATED BY MEANS OF TABLES CONTAINING CONSTANTS (FOR AVEGEXIT, FOR +# WAITLIST, FOR PINBALL) AND TCF INSTRUCTIONS. USERS PLACE THE ADRES OF THE HEAD OF THE APPROPRIATE TABLE +# (OF P61TABLE FOR P61LM, FOR EXAMPLE) IN ERASABLE REGISTER `WHICH' (E4). THE IGNITION ROUTINE THEN INDEXES BY +# WHICH TO OBTAIN OR EXECUTE THE PROPER TABLE ENTRY. THE IGNITION ROUTINE IS INITIATED BY A TCF BURNBABY, +# THROUGH BANKJUMP IF NECESSARY. THERE IS NO RETURN. +# +# THE MASTER IGNITION ROUTINE WAS CONCEIVED AND EXECUTED, AND (NOTA BENE) IS MAINTAINED BY ADLER AND EYLES. +# +# HONI SOIT QUI MAL Y PENSE +# +# *********************************************** +# TABLES FOR THE IGNITION ROUTINE +# *********************************************** +# +# NOLI SE TANGERE + +P12TABLE VN 0674 # (0) + TCF ULLGNOT # (1) + TCF COMFAIL3 # (2) + TCF GOCUTOFF # (3) + TCF TASKOVER # (4) + TCF P12SPOT # (5) + DEC 0 # (6) NO ULLAGE + EBANK= WHICH + 2CADR SERVEXIT # (7) + + TCF DISPCHNG # (11) + TCF WAITABIT # (12) + TCF P12IGN # (13) + +P40TABLE VN 0640 # (0) + TCF ULLGNOT # (1) + TCF COMFAIL4 # (2) + TCF GOPOST # (3) + TCF TASKOVER # (4) + TCF P40SPOT # (5) +# Page 732 + DEC 2240 # (6) + EBANK= OMEGAQ + 2CADR STEERING # (7) + + TCF P40SJUNK # (11) + TCF WAITABIT # (12) + TCF P40IGN # (13) + TCF REP40ALM # (14) + +P41TABLE TCF P41SPOT # (5) + DEC -1 # (6) + EBANK= OMEGAQ + 2CADR CALCN85 # (7) + + TCF COMMON # (11) + TCF TIGTASK # (12) + +P42TABLE VN 0640 # (0) + TCF WANTAPS # (1) + TCF COMFAIL4 # (2) + TCF GOPOST # (3) + TCF TASKOVER # (4) + TCF P42SPOT # (5) + DEC 2640 # (6) + EBANK= OMEGAQ + 2CADR STEERING # (7) + + TCF P40SJUNK # (11) + TCF WAITABIT # (12) + TCF P42IGN # (13) + TCF P42STAGE # (14) + +P63TABLE VN 0662 # (0) + TCF ULLGNOT # (1) + TCF COMFAIL3 # (2) + TCF V99RECYC # (3) + TCF TASKOVER # (4) + TCF P63SPOT # (5) + DEC 2240 # (6) + EBANK= WHICH + 2CADR SERVEXIT # (7) + + TCF DISPCHNG # (11) + TCF WAITABIT # (12) +# Page 733 + TCF P63IGN # (13) + +ABRTABLE VN 0663 # (0) + TCF ULLGNOT # (1) + TCF COMFAIL3 # (2) + TCF GOCUTOFF # (3) + TCF TASKOVER # (4) + NOOP # (5) + NOOP # (6) + NOOP # (7) + NOOP + TCF DISPCHNG # (11) + TCF WAITABIT # (12) + TCF ABRTIGN # (13) + +# ********************************* +# GENERAL PURPOSE IGNITION ROUTINES +# ********************************* + +BURNBABY TC PHASCHNG # GROUP 4 RESTARTS HERE + OCT 04024 + + CAF ZERO # EXTIRPATE JUNK LEFT IN DVTOTAL + TS DVTOTAL + TS DVTOTAL +1 + + TC BANKCALL # P40AUTO MUST BE BANKCALLED EVEN FROM ITS + CADR P40AUTO # OWN BANK TO SET UP RETURN PROPERLY + +B*RNB*B* EXTEND + DCA TIG # STORE NOMINAL TIG FOR OBLATENESS COMP. + DXCH GOBLTIME # AND FOR P70 OR P71. + + INHINT + TC IBNKCALL + CADR ENGINOF3 + RELINT + + INDEX WHICH + TCF 5 + +P42SPOT = P40SPOT # (5) +P12SPOT = P40SPOT # (5) +P63SPOT = P41SPOT # (5) IN P63 CLOKTASK ALREADY GOING +P40SPOT CS CNTDNDEX # (5) +# Page 734 + TC BANKCALL # MUST BE BANKCALLED FOR GENERALIZED + CADR STCLOK2 # RETURN +P41SPOT TC INTPRET # (5) + DLOAD DSU + TIG + D29.9SEC + STCALL TDEC1 + INITCDUW + BOFF CALL + MUNFLAG + GOMIDAV + CSMPREC + VLOAD MXV + VATT1 + REFSMMAT + VSR1 + STOVL V(CSM) # CSM VELOCITY -- M/CS*2(7) + RATT1 + VSL4 MXV + REFSMMAT + STCALL R(CSM) # CSM POSITION -- M*2(24) + MUNGRAV + STODL G(CSM) # CSM GRAVITY VEC. -- M/CS*2(7) + TAT + STORE TDEC1 # RELOAD TDEC1 FOR MIDTOAV. +GOMIDAV CALRB + MIDTOAV1 + TCF CALLT-35 # MADE IT IN TIME. + + EXTEND # TIG WAS SLIPPED, SO RESET TIG TO 29.9 + DCA PIPTIME1 # SECONDS AFTER THE TIME TO WHICH WE DID + DXCH TIG # INTEGRATE. + EXTEND + DCA D29.9SEC + DAS TIG + +CALLT-35 DXCH MPAC + DXCH SAVET-30 # DELTA-T UNTIL TIG-30 + EXTEND + DCS 5SECDP + DAS SAVET-30 # DELTA-T UNTIL TIG-35 + EXTEND + DCA SAVET-30 + TC LONGCALL + EBANK= TTOGO + 2CADR TIG-35 + + TC PHASCHNG + OCT 20254 # 4.25SPOT FOR TIG-35 RESTART. +# Page 735 + TC CHECKMM + DEC 63 + TCF ENDOFJOB # NOT P63 + CS CNTDNDEX # P63 CAN START DISPLAYING NOW. + TS DISPDEX + TC INTPRET + VLOAD ABVAL + VN1 + STORE ABVEL # INITIALIZE ABVEL FOR P63 DISPLAY + EXIT + TCF ENDOFJOB + +# ******************************** + +TIG-35 CAF 5SEC + TC TWIDDLE + ADRES TIG-30 + + TC PHASCHNG + OCT 40154 # 4.15SPOT FOR TIG-30 RESTART + + CS BLANKDEX # BLANK DSKY FOR 5 SECONDS + TS DISPDEX + + INDEX WHICH + CS 6 # CHECK ULLAGE TIME. + EXTEND + BZMF TASKOVER + CAF 4.9SEC # SET UP TASK TO RESTORE DISPLAY AT TIG-30 + TC TWIDDLE + ADRES TIG-30.1 + + CAF PRIO17 # A NEGATIVE ULLAGE TIME INDICATES P41, IN + TC NOVAC # WHICH CASE WE HAVE TO SET UP A JOB TO + EBANK= TTOGO # BLANK THE DSKY FOR FIVE SECONDS, SINCE + 2CADR P41BLANK # CLOKJOB IS NOT RUNNING DURING P41. + + TCF TASKOVER + +P41BLANK TC BANKCALL # BLANK DSKY. + CADR CLEANDSP + TCF ENDOFJOB + +TIG-30.1 CAF PRIO17 # SET UP JOB TO RESTORE DISPLAY AT TIG-30 + TC NOVAC + EBANK= TTOGO + 2CADR TIG-30A + + TCF TASKOVER +# Page 736 +TIG-30A CAF V16N85B + TC BANKCALL # RESTORE DISPLAY. + CADR REGODSP # REGODSP DOES A TCF ENDOFJOB + +# ******************************** + +TIG-30 CAF S24.9SEC + TC TWIDDLE + ADRES TIG-5 + + CS CNTDNDEX # START UP CLOKTASK AGAIN + TS DISPDEX + + INDEX WHICH # PICK UP APPROPRIATE ULLAGE -- ON TIME + CA 6 # Was CAF --- RSB 2009. + EXTEND + BZMF ULLGNOT # DON'T SET UP ULLAGE IF DT IS NEG OR ZERO + TS SAVET-30 # SAVE DELTA-T FOR RESTART + TC TWIDDLE + ADRES ULLGTASK + + CA THREE # RESTART PROTECT ULLGTASK (1.3SPOT) + TS L + CS THREE + DXCH -PHASE1 + CS TIME1 + TS TBASE1 + + INDEX WHICH + TCF 1 + +WANTAPS CS FLGWRD10 # (1) FOR P42 ENSURE APSFLAG IS SET. IF IT + MASK APSFLBIT # WASN'T SET, DAP WILL BE INITIALIZED TO + ADS FLGWRD10 # ASCENT VALUES BY 1/ACCS IN 2 SECONDS. + +ULLGNOT EXTEND # (1) + INDEX WHICH + DCA 7 # LOAD AVEGEXIT WITH APPROPRIATE 2CADR + DXCH AVEGEXIT + + CAF TWO # 4.2SPOT RESTARTS IMMEDIATELY AT REDO4.2 + TS L + CS TWO # AND ALSO AT TIG-5 AT THE CORRECT TIME. + DXCH -PHASE4 + + CS TIME1 + TS TBASE4 # SET TBASE4 FOR TIG-5 RESTART + +REDO2.17 EXTEND +# Page 737 + DCA NEG0 # CLEAR OUT GROUP 2 SO LAMBERT CAN START + DXCH -PHASE2 # IF NEEDED. + +REDO4.2 CCS PHASE5 # IF SERVICER GOING? + TCF TASKOVER # YES, DON'T START IT UP AGAIN. + + TC POSTJUMP + CADR PREREAD # PREREAD END THIS TASK + +# ********************************* + +ULLGTASK TC ONULLAGE # THIS COMES AT TIG-7.5 OR TIG-3.5 + TC PHASCHNG + OCT 1 + TCF TASKOVER + +# ********************************* + +TIG-5 EXTEND + DCA NEG0 # INSURE THAT GROUP 3 IS INACTIVE. + DXCH -PHASE3 + + CAF 5SEC + TC TWIDDLE + ADRES TIG-0 + + TC DOWNFLAG # RESET IGNFLAG AND ASINFLAG + ADRES IGNFLAG # FOR LIGHT-UP LOGIC. + TC DOWNFLAG + ADRES ASTNFLAG + + INDEX WHICH + TCF 11 + +P40SJUNK CCS PHASE3 # (11) P40 AND P42. S40.13 IN PROGRESS? + TCF DISPCHNG # YES + + CAF PRIO20 + TC FINDVAC + EBANK= TTOGO + 2CADR S40.13 + + TC PHASCHNG # 3.5SPOT FOR S40.13 + OCT 00053 +DISPCHNG CS VB99DEX # (11) + TS DISPDEX + +# Page 738 +COMMON TC PHASCHNG # RESTART TIG-0 (4.7SPOT) + OCT 40074 + TCF TASKOVER + +# ********************************* + +TIG-0 CS FLAGWRD7 # SET IGNFLAG SINCE TIG HAS ARRIVED + MASK IGNFLBIT + ADS FLAGWRD7 + + TC CHECKMM # IN P63 CASE, THROTTLE-UP IS ZOOMTIME + DEC 63 # AFTER NOMINAL IGNITION, NOT ACTUAL + TCF IGNYET? + CA ZOOMTIME + TC WAITLIST + EBANK= DVCNTR + 2CADR P63ZOOM + + TC 2PHSCHNG + OCT 40033 + + OCT 05014 + OCT 77777 + +IGNYET? CAF ASTNBIT # CHECK ASTNFLAG: HAS ASTRONAUT RESPONDED + MASK FLAGWRD7 # TO OUR ENGINE ENABLE REQUEST? + EXTEND + INDEX WHICH + BZF 12 # BRANCH IF HE HAS NOT RESPONDED YET + +IGNITION CS FLAGWRD5 # INSURE ENGONFLG IS SET. + MASK ENGONBIT + ADS FLAGWRD5 + CS PRIO30 # TURN ON THE ENGINE. + EXTEND + RAND DSALMOUT + AD BIT13 + EXTEND + WRITE DSALMOUT + EXTEND # SET TEVENT FOR DOWNLINK + DCA TIME2 + DXCH TEVENT + + EXTEND # UPDATE TIG USING TGO FROM S40.13 + DCA TGO + DXCH TIG + EXTEND + DCA TIME2 + DAS TIG + +# Page 739 + CS FLUNDBIT # PERMIT GUIDANCE LOOP DISPLAYS + MASK FLAGWRD8 + TS FLAGWRD8 + + INDEX WHICH + TCF 13 + +P63IGN EXTEND # (13) INITIATE BURN DISPLAYS + DCA DSP2CADR + DXCH AVGEXIT + + CA Z # ASSASSINATE CLOKTASK + TS DISPDEX + + CS FLAGWRD9 # SET FLAG FOR P70-P71 + MASK LETABBIT + ADS FLAGWRD9 + + CS FLAGWRD7 # SET SWANDISP TO ENABLE R10. + MASK SWANDBIT + ADS FLAGWRD7 + + CS PULSES # MAKE SURE DAP IS NOT IN MINIMUM-IMPULSE + MASK DAPBOOLS # MODE, IN CASE OF SWITCH TO P66 + TS DAPBOOLS + + EXTEND # INITIALIZE TIG FOR P70 AND P71. + DCA TIME2 + DXCH TIG + + CAF ZERO # INITIALIZE WCHPHASE, AND FLPASS0 + TS WCHPHASE + TS WCHPHOLD # ALSO WHCPHOLD + CA TWO + TS FLPASS0 + + TCF P42IGN +P40IGN CS FLAGWRD5 # (13) + MASK NOTHRBIT + EXTEND + BZF P42IGN + CA ZOOMTIME + TC WAITLIST + EBANK= DVCNTR + 2CADR P40ZOOM + +P63IGN1 TC 2PHSCHNG + OCT 40033 # 3.3SPOT FOR ZOOM RESTART. + OCT 05014 # TYPE C RESTARTS HERE IMMEDIATELY + OCT 77777 + +# Page 740 + TCF P42IGN +P12IGN CAF EBANK6 + TS EBANK + EBANK= AOSQ + + CA IGNAOSQ # INITIALIZE DAP BIAS ACCELERATION + TS AOSQ # ESTIMATES AT P12 IGNITION. + CA IGNAOSR + TS AOSR + + CAF EBANK7 + TS EBANK + EBANK= DVCNTR + +ABRTIGN CA Z # (13) KILL CLOKTASK + TS DISPDEX + + EXTEND # CONNECT ASCENT GYIDANCE TO SERVICER. + DCA ATMAGADR + DXCH AVGEXIT + + CS FLAGWRD7 # ENABLE R10. + MASK SWANDBIT + ADS FLAGWRD7 + +P42IGN CS DRIFTBIT # ENSURE THAT POWERED-FLIGHT SWITCHING + MASK DAPBOOLS # CURVES ARE USED. + TS DAPBOOLS + CAF IMPULBIT # EXAMINE IMPULSE SWITCH + MASK FLAGWRD2 + CCS A + TCF IMPLBURN + +DVMONCON TC DOWNFLAG + ADRES IGNFLAG # CONNECT DVMON + TC DOWNFLAG + ADRES ASTNFLAG + TC DOWNFLAG + ADRES IDLEFLAG + + TC PHASCHNG + OCT 40054 + + TC FIXDELAY # TURN ULLAGE OFF HALF A SECOND AFTER + DEC 50 # LIGHT UP. + +ULLAGOFF TC NOULLAGE + +WAITABIT EXTEND # KILL GROUP 4 + DCA NEG0 +# Page 741 + DXCH -PHASE4 + + TCF TASKOVER + +TIGTASK TC POSTJUMP # (12) + CADR TIGTASK1 + +# ******************************** + + BANK 31 + SETLOC P40S3 + BANK + COUNT* $$/P40 + +TIGTASK1 CAF PRIO16 + TC NOVAC + EBANK= TRKMKCNT + 2CADR TIGNOW + + TC PHASCHNG + OCT 6 # KILL GROUP 6 + + TCF TASKOVER + +# ******************************** + +P63ZOOM EXTEND + DCA LUNLANAD + DXCH AVEGEXIT + + TC IBNKCALL + CADR FLATOUT + TCF P40ZOOMA + +P40ZOOM CAF BIT13 + TS THRUST + CAF BIT4 + + EXTEND + WOR CHAN14 + +P40ZOOMA TC PHASCHNG + OCT 3 + TCF TASKOVER + + EBANK= DVCNTR +LUNLANAD 2CADR LUNLAND + +# Page 742 +ZOOM = P40ZOOMA + BANK 36 + SETLOC P40S + BANK + COUNT* $$/P40 + +# ******************************** + +COMFAIL TC UPFLAG # (15) + ADRES IDLEFLAG + TC UPFLAG # SET FLAG TO SUPPRESS CONFLICTING DISPLAY + ADRES FLUNDISP + CAF FOUR # RESET DVMON + TS DVCNTR + CCS PHASE6 # CLOCKTASK ACTIVE? + TCF +3 # YES + TC BANKCALL # OTHERWISE, START IT UP + CADR STCLOK1 + +3 CS VB97DEX + TS DISPDEX + TC PHASCHNG # TURN OFF GROUP 4. + OCT 00004 + TCF ENDOFJOB + +COMFAIL1 INDEX WHICH + TCF 2 + +COMFAIL3 CA Z # (15) KILL CLOKTASK USING Z + TCF +2 + +COMFAIL4 CS CNTDNDEX + TS DISPDEX + + TC DOWNFLAG # RECONNECT DV MONITOR + ADRES IDLEFLAG + TC DOWNFLAG # PERMIT GUIDANCE LOOP DISPLAYS + ADRES FLUNDISP + TCF ENDOFJOB + +COMFAIL2 TC PHASCHNG # KILL ZOOM RESTART PROTECTION + OCT 00003 + + INHINT + TC KILLTASK # KILL ZOOM IN CASE IT'S STILL TO COME + CADR ZOOM + TC IBNKCALL # COMMAND ENGINE OFF + CADR ENGINOF4 + TC UPFLAG # SET THE DRIFT BIT FOR THE DAP. + ADRES DRIFTDFL +# Page 743 + TC INVFLAG # USE OTHER RCS SYSTEM + ADRES AORBTFLG + TC UPFLAG # TURN ON ULLAGE + ADRES ULLAGFLG + CAF BIT1 + INHINT + TC TWIDDLE + ADRES TIG-5 + TCF ENDOFJOB + +# *********************************** +# SUBROUTINES OF THE IGNITION ROUTINE +# *********************************** + +INVFLAG CA Q + TC DEBIT + COM + EXTEND + RXOR LCHAN + TCF COMFLAG + +# *********************************** + +NOULLAGE CS ULLAGER # MUST BE CALLED IN A TASK OR UNDER INHINT + MASK DAPBOOLS + TS DAPBOOLS + TC Q + +# *********************************** + +ONULLAGE CS DAPBOOLS # TURN ON ULLAGE. MUST BE CALLED IN + MASK ULLAGER # A TASK OR WHILE INHINTED. + ADS DAPBOOLS + TC Q + +# *********************************** + +STCLOK1 CA ZERO # THIS ROUTINE STARTS THE COUNT-DOWN +STCLOK2 TS DISPDEX # (CLOKTASK AND CLOKJOB). SETTING +STCLOK3 TC MAKECADR # SETTING DISPDEX POSITIVE KILLS IT. + TS TBASE4 # RETURN SAVE (NOT FOR RESTARTS). + EXTEND + DCA TIG + DXCH MPAC + EXTEND + DCS TIME2 +# Page 744 + DAS MPAC # HAVE TIG -- TIME2, UNDOUBTEDLY A + NUMBER + TC TPAGREE # POSITIVE, SINCE WE PASSED THE + CAF 1SEC # 45 SECOND CHECK. + TS Q + DXCH MPAC + MASK LOW5 # RESTRICT MAGNITUDE OF NUMBER IN A + EXTEND + DV Q + CA L # GET REMAINDER + AD TWO + INHINT + TC TWIDDLE + ADRES CLOKTASK + TC 2PHSCHNG + OCT 40036 # 6.3SPOT FOR CLOKTASK + OCT 05024 + OCT 13000 + + CA TBASE4 + TC BANKJUMP + +CLOKTASK CS TIME1 # SET TBASE6 FOR GROUP 6 RESTART + TS TBASE6 + + CCS DISPDEX + TCF KILLCLOK + NOOP + CAF PRIO27 + TC NOVAC + EBANK= TTOGO + 2CADR CLOKJOB + + TC FIXDELAY # WAIT A SECOND BEFORE STARTING OVER + DEC 100 + TCF CLOKTASK + +KILLCLOK EXTEND # KILL RESTART + DCA NEG0 + DXCH -PHASE6 + TCF TASKOVER + +CLOKJOB EXTEND + DCS TIG + DXCH TTOGO + EXTEND +# Page 745 + DCA TIME2 + DAS TTOGO + INHINT + CCS DISPDEX # IF DISPDEX HAS BEEN SET POSITIVE BY A + TCF ENDOFJOB # TASK OR A HIGHER PRIORITY JOB SINCE THE + TCF ENDOFJOB # LAST CLOKTASK, AVOID USING IT AS AN + COM # INDEX. + RELINT # ***** DISPDEX MUST NEVER B -0 ***** + INDEX A + TCF DISPNOT -1 # (-1 DUE TO EFFECT OF CCS) + +VB97DEX = OCT35 # NEGATIVE OF THIS IS PROPER FOR DISPDEX + + -35 CS ZERO # INDICATE VERB 97 PASTE + TS NVWORD1 + CA NVWORD +2 # NVWORD+2 CONTAINS V06 & APPROPRIATE NOUN + TC BANKCALL + CADR CLOCPLAY + TCF STOPCLOK # TERMINATE CLOKTASK ON THE WAY TO P00H + TCF COMFAIL1 + TCF COMFAIL2 + + # THIS DISPLAY IS CALLED VIA ASTNCLOK + -25 CAF V06N61 # IT IS PRIMARILY USED BY THE CREW IN P63 + TC BANKCALL # TO RESET HIS EVENT TIMER TO AGREE WITH + CADR REFLASH # TIG. + TCF STOPCLOK + TCF ASTNRETN + TCF -6 + +CNTDNDEX = LOW4 # OCT17: NEGATIVE PROPER FOR DISPDEX + + -17 INDEX WHICH # THIS DISPLAY COMES UP AT ONE SECOND + # Was CAF --- RSB 2009 + CA 0 # INTERVALS. IT IS NORMALLY OPERATED + TC BANKCALL # BETWEEN TIG-30 SECONDS AND TIG-5 SECONDS + CADR REGODSP # REGODSP DOES ITS OWN TCF ENDOFJOB + +VB99DEX = ELEVEN # OCT13: NEGATIVE PROPER FOR DISPDEX + +V99RECYC EQUALS + + -13 CS BIT9 # INDICATE VERB 99 PASTE + TS NVWORD1 + INDEX WHICH # THIS IS THE "PLEASE ENABLE ENGINE" + # Was CAF --- RSB 2004 + CA 0 # DISPLAY; IT IS INITIATED AT TIG-5 SEC. + TC BANKCALL # THE DISPLAY IS A V99NXX, WHERE XX IS + CADR CLOCPLAY # NOUN THAT HAD PREVIOUSLY BEEN DISPLAYED + TCF STOPCLOK # TERMINATE GOTOP00H TURNS OFF ULLAGE. + TCF *PROCEED + TCF *ENTER + +# Page 746 +BLANKDEX = TWO # NEGATIVE OF THIS IS PROPER FOR DISPDEX + + -2 TC BANKCALL # BLANK DSKY. THE DSKY IS BLANKED FOR + CADR CLEANDSP # 5 SECONDS AT TIG-35 TO INDICATE THAT +DISPNOT TCF ENDOFJOB # AVERAGE G IS STARTING. + +STOPCLOK TC NULLCLOK # STOP CLOKTASK & TURN OFF ULLAGE ON THE + TCF GOTOP00H # WAY TO P00 (GOTOP00H RELINTS) + +NULLCLOK INHINT + EXTEND + QXCH P40/RET + TC NOULLAGE # TURN OFF ULLAGE ... + TC KILLTASK # DON'T LET IT COME ON, EITHER ... + CADR ULLGTASK + TC PHASCHNG # NOT EVEN IF THERE'S A RESTART. + OCT 1 + CA Z # KILL CLOKTASK + TS DISPDEX + TC P40/RET + +ASTNRETN TC PHASCHNG + OCT 04024 + CAF ZERO # STOP DISPLAYING BUT KEEP RUNNING + TS DISPDEX + CAF PRIO13 + TC FINDVAC + EBANK= STARIND + 2CADR ASTNRET + + TCF ENDOFJOB + +*PROCEED TC UPFLAG + ADRES ASTNFLAG + + TCF IGNITE + +*ENTER INHINT + INDEX WHICH + TCF 3 + +GOPOST CAF PRIO12 # (3) MUST BE LOWER PRIORITY THAN CLOKJOB + TC FINDVAC + EBANK= TTOGO + 2CADR POSTBURN + +# Page 747 + INHINT # SET UP THE DAP FOR COASTING FLIGHT. + TC IBNKCALL + CADR ALLCOAST + TC NULLCLOK + TC PHASCHNG # 4.13 RESTART FOR POSTBURN + OCT 00134 + + TCF ENDOFJOB + +GOCUTOFF CAF PRIO17 # (3) + TC FINDVAC + EBANK= TGO + 2CADR CUTOFF + + TC DOWNFLAG + ADRES FLUNDISP + + INHINT # SET UP THE DAP FOR COASTING FLIGHT. + TC IBNKCALL + CADR ALLCOAST + TC NULLCLOK + TC PHASCHNG + OCT 07024 + OCT 17000 + EBANK= TGO + 2CADR CUTOFF + + TCF ENDOFJOB + +IGNITE CS FLAGWRD7 # (2) + MASK IGNFLBIT + CCS A + TCF IGNITE1 + CAF BIT1 + INHINT + TC TWIDDLE + ADRES IGNITION + + CAF OCT23 # IMMEDIATE RESTART AT IGNITION + TS L + COM + DXCH -PHASE4 + +IGNITE1 CS CNTDNDEX # RESTORE OLD DISPLAY. + TS DISPDEX + + TCF ENDOFJOB + +# Page 748 +# ******************************** + +P40ALM TC ALARM # PROGRAM SELECTION NOT CONSISTENT WITH + OCT 1706 # VEHICLE CONFIGURATION + +REP40ALM CAF V05N09 # (14) + TC BANKCALL + CADR GOFLASH + + TCF GOTOP00H # V34E TERMINATE + TCF +2 # PROCEED CHECK FOR P42 + TCF REP40ALM # V32E REDISPLAY ALARM + + INDEX WHICH # FOR P42, ALLOW CREW TO PROCEED EVEN + TCF 14 # THOUGH VEHICLE IS UNSTAGED. + +# ******************************** + + BANK 31 + SETLOC P40S2 + BANK + + COUNT* $$/P40 + +P40AUTO TC MAKECADR # HELLO THERE. + TS TEMPR60 # FOR GENERALIZED RETURN TO OTHER BANKS. +P40A/P TC BANKCALL # SUBROUTINE TO CHECK PGNCS CONTROL + CADR G+N,AUTO # AND AUTO STABILIZATION MODES + CCS A # +0 INDICATES IN PGNCS, IN AUTO + TCF TURNITON # + INDICATES NOT IN PGNCS AND/OR AUTO + CAF APSFLBIT # ARE WE ON THE DESCENT STAGE? + MASK FLGWRD10 + CCS A + TCF GOBACK # RETURN + CAF BIT5 # YES, CHECK FOR AUTO-THROTTLE MODE + EXTEND + RAND CHAN30 + EXTEND + BZF GOBACK # IN AUTO-THROTTLE MODE -- RETURN +TURNITON CAF P40A/PMD # DISPLAYS V50N25 R1=203 PLEASE PERFORM + TC BANKCALL # CHECKLIST 203 TURN ON PGNCS ETC. + CADR GOPERF1 + TCF GOTOP00H # V34E TERMINATE + TCF P40A/P # RECYCLE +GOBACK CA TEMPR60 + TC BANKJUMP # GOODBYE. COME AGAIN SOON. + +P40A/PMD OCT 00203 + +# Page 749 + BANK 36 + SETLOC P40S + BANK + + COUNT* $$/P40 + +# ********************************** +# CONSTANTS FOR THE IGNITION ROUTINE +# ********************************** + +SERVCADR = P63TABLE +7 + +P40ADRES ADRES P40TABLE + +P41ADRES ADRES P41TABLE -5 + +P42ADRES ADRES P42TABLE + + EBANK= DVCNTR +DSP2CADR 2CADR P63DISPS -2 + + EBANK= DVCNTR +ATMAGADR 2CADR ATMAG + +? = GOTOP00H + +D29.9SEC 2DEC 2990 + +S24.9SEC DEC 2490 + +4.9SEC DEC 490 + +OCT20 = BIT5 + +V06N61 VN 0661 + +# Page 750 +# KILLTASK +# MOD NO: NEW PROGRAM +# MOD BY: COVELLI +# +# FUNCTIONAL DESCRIPTION: +# +# KILLTASK IS USED TO REMOVE A TASK FROM THE WAITLIST BY SUBSTITUTING A NULL TASK CALLED `NULLTASK' (OF COURSE), +# WHICH MERELY DOES A TC TASKOVER. IF THE SAME TASK IS SCHEDULED MORE THAN ONCE, ONLY THE ONE WHICH WILL OCCUR +# FIRST IS REMOVED. IF THE TASK IS NOT SCHEDULED, KILLTASK TAKES NO ACTION AND RETURNS WITH NO ALARM. KILLTASK +# LEAVES INTERRUPTS INHIBITED SO CALLER MUST RELINT +# +# CALLING SEQUENCE +# L TC KILLTASK # IN FIXED-FIXED +# L+1 CADR ???????? # CADR (NOT 2CADR) OF TASK TO BE REMOVED. +# L+2 (RELINT) # RETURN +# +# EXIT MODE: AT L+2 OF CALLING SEQUENCE. +# +# ERASABLE INITIALIZATION: NONE. +# +# OUTPUT: 2CADR OF NULLTASK IN LST2 +# +# DEBRIS: ITEMP1 - ITEMP4, A, L, Q. + + EBANK= LST2 + BLOCK 3 # KILLTASK MUST BE IN FIXED-FIXED. + SETLOC FFTAG6 + BANK + COUNT* $$/KILL +KILLTASK CA KILLBB + INHINT + LXCH A + INDEX Q + CA 0 # GET CADR. + LXCH BBANK + TCF KILLTSK2 # CONTINUE IN SWITCHED FIXED. + + EBANK= LST2 +KILLBB BBCON KILLTSK2 + + BANK 27 + + SETLOC P40S1 + BANK + COUNT* $$/KILL + +KILLTSK2 LXCH ITEMP2 # SAVE CALLER'S BBANK +# Page 751 + INCR Q + EXTEND + QXCH ITEMP1 # RETURN 2ADR IN ITEMP1,ITEMP2 + + TS ITEMP3 # CADR IS IN A + MASK LOW10 + AD BIT11 + TS ITEMP4 # GENADR OF TASK + + CS LOW10 + MASK ITEMP3 + TS ITEMP3 # FBANK OF TASK + + ZL +ADRSCAN INDEX L + CS LST2 + AD ITEMP4 # COMPARE GENADRS + EXTEND + BZF TSTFBANK # IF THEY MATCH, COMPARE FBANKS +LETITLIV CS LSTLIM + AD L + EXTEND # ARE WE DONE? + BZF DEAD # YES -- DONE, SO RETURN + INCR L + INCR L + TCF ADRSCAN # CONTINUE LOOP. + +DEAD DXCH ITEMP1 + DTCB + +TSTFBANK CS LOW10 + INDEX L + MASK LST2 +1 # COMPARE FBANKS ONLY. + EXTEND + SU ITEMP3 + EXTEND + BZF KILLDEAD # MATCH -- KILL IT. + TCF LETITLIV # NO MATCH -- CONTINUE. + +KILLDEAD CA TCTSKOVR + INDEX L + TS LST2 # REMOVE TASK BY INSERTING TASKOVER + TCF DEAD + +LSTLIM EQUALS BIT5 # DEC 16 + + + diff --git a/examples/Hardware_Checks/ESP32/45_Battery_Voltage_Read_Test_ESP32/45_Battery_Voltage_Read_Test_ESP32.ino b/examples/Hardware_Checks/ESP32/45_Battery_Voltage_Read_Test_ESP32/45_Battery_Voltage_Read_Test_ESP32.ino new file mode 100644 index 0000000..042cd18 --- /dev/null +++ b/examples/Hardware_Checks/ESP32/45_Battery_Voltage_Read_Test_ESP32/45_Battery_Voltage_Read_Test_ESP32.ino @@ -0,0 +1,104 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/01/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This test program has been written to check that hardware for reading the battery + voltage has been assembled correctly such that it is funtional. The value defined as 'ADMultiplier' + in settings.h is used to adjust the value read from the 91K\11K resistor divider and convert into mV. + + There is also an option of using a logic pin to turn the resistor divider used to read battery voltage + on and off. This reduces current used in sleep mode. To use the feature set the define for pin BATVREADON + in 'Settings.h' to the pin used. If not using the feature set the pin number to -1. + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + + +#define ADMultiplier 10.872 //adjustment to convert AD value read into mV of battery voltage +#define BATVREADON 25 //used to turn on the resistor divider to measure voltage //this pin turns on the MOSFET that switches in the resistor divider +#define LED1 2 //pin for PCB LED +#define SupplyAD 36 //Resitor divider for battery connected here + + +void loop() +{ + Serial.println(F("LED Flash")); + led_Flash(4, 125); + printSupplyVoltage(); + Serial.println(); + delay(1500); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void printSupplyVoltage() +{ + //get and display supply volts on terminal or monitor + Serial.print(F("Supply Volts ")); + Serial.print(readSupplyVoltage()); + Serial.println(F("mV")); +} + + +uint16_t readSupplyVoltage() +{ + //ESP32 uses the 3.3V VCC as reference + + uint16_t temp; + uint16_t volts = 0; + byte index; + + if (BATVREADON >= 0) + { + digitalWrite(BATVREADON, HIGH); //turn on MOSFET connecting resitor divider in circuit + } + + temp = analogRead(SupplyAD); + + for (index = 0; index <= 4; index++) //sample AD 5 times + { + temp = analogRead(SupplyAD); + volts = volts + temp; + } + volts = ((volts / 5) * ADMultiplier); + + if (BATVREADON >= 0) + { + digitalWrite(BATVREADON, LOW); //turn off MOSFET connecting resitor divider in circuit + } + + return volts; +} + + +void setup() +{ + Serial.begin(9600); //setup Serial console ouput + Serial.println("45_Battery_Voltage_Read_Test_ESP32 Starting"); + + pinMode(LED1, OUTPUT); //for PCB LED + + if (BATVREADON >= 0) + { + pinMode(BATVREADON, OUTPUT); //controls MOSFET connecting resitor divider in circuit + } + +} diff --git a/examples/Hardware_Checks/ESP32/47_DeepSleep_Timed_Wakeup_ESP32/47_DeepSleep_Timed_Wakeup_ESP32.ino b/examples/Hardware_Checks/ESP32/47_DeepSleep_Timed_Wakeup_ESP32/47_DeepSleep_Timed_Wakeup_ESP32.ino new file mode 100644 index 0000000..368769d --- /dev/null +++ b/examples/Hardware_Checks/ESP32/47_DeepSleep_Timed_Wakeup_ESP32/47_DeepSleep_Timed_Wakeup_ESP32.ino @@ -0,0 +1,90 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program flashes a LED connected to the pin defined by LED1, and puts the ESP32 + to deep_sleep for a period determined by the TIME_TO_SLEEP variable (in seconds). + + The program also has the option of using a logic pin to control the power to the lora and SD card + devices, which can save power in sleep mode. If the hardware is fitted to your board these devices are + powered on by setting the VCCPOWER pin low. If your board does not have this feature set VCCPOWER to -1. + + Current in deep_sleep for a bare bones ESP32 with regulator and no other devices was 27uA. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#define LED1 2 //pin number for LED +#define VCCPOWER 14 //pin may control power to external devices, -1 if not used + +#define uS_TO_S_FACTOR 1000000 //Conversion factor for micro seconds to seconds +#define TIME_TO_SLEEP 15 //Time ESP32 will go to sleep (in seconds) + +RTC_DATA_ATTR int32_t bootCount = 0; +RTC_DATA_ATTR uint32_t sleepcount = 0; + + +void loop() +{ + Serial.print(F("Bootcount ")); + Serial.println(bootCount); + Serial.print(F("Sleepcount ")); + Serial.println(sleepcount); + Serial.println(F("LED Flash")); + led_Flash(4, 125); + Serial.println(F("LED On")); + digitalWrite(LED1, HIGH); + delay(2500); + Serial.println(F("LED Off")); + digitalWrite(LED1, LOW); + + esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); + Serial.println(F("Start Sleep")); + Serial.flush(); + sleepcount++; + esp_deep_sleep_start(); + Serial.println(); + Serial.println(); + Serial.println(F("Awake !")); +} + + +void led_Flash(unsigned int flashes, unsigned int delaymS) +{ + //flash LED to show tracker is alive + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, LOW); + delay(delaymS); + digitalWrite(LED1, HIGH); + delay(delaymS); + } +} + + +void setup() +{ + Serial.begin(9600); //setup Serial console ouput + Serial.println(F("47_DeepSleep_Timed_Wakeup_ESP32 - Starting")); + + if (bootCount == 0) //Run this only the first time + { + bootCount = bootCount + 1; + } + + pinMode(LED1, OUTPUT); //for PCB LED + + if (VCCPOWER >= 0) + { + pinMode(VCCPOWER, OUTPUT); + digitalWrite(VCCPOWER, HIGH); //VCCOUT off + } + +} + diff --git a/examples/Hardware_Checks/ESP32/50_LightSleep_Timed_Wakeup_ESP32/50_LightSleep_Timed_Wakeup_ESP32.ino b/examples/Hardware_Checks/ESP32/50_LightSleep_Timed_Wakeup_ESP32/50_LightSleep_Timed_Wakeup_ESP32.ino new file mode 100644 index 0000000..d21f827 --- /dev/null +++ b/examples/Hardware_Checks/ESP32/50_LightSleep_Timed_Wakeup_ESP32/50_LightSleep_Timed_Wakeup_ESP32.ino @@ -0,0 +1,91 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 03/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ +/* + +**************************************************************************************************************** + Program operation - The program flashes a LED connected to the pin defined by LED1, and puts the ESP32 to + light_sleep for a period determined by TIME_TO_SLEEP (in seconds). + + The program also has the option of using a logic pin to control the power to the lora.SD card and DS18B20 + devices, which can save power in sleep mode. If the hardware is fitted to your board these devices are + powered on by setting the VCCPOWER pin low. If your board does not have this feature set VCCPOWER to -1. + + Current in light_sleep mode was 1500uA +**************************************************************************************************************** +*/ + + +#define VCCPOWER 14 //when low supplies VCC power to external devices. Set to -1 if not used +#define LED1 2 //On board LED, high for on + + +#define uS_TO_S_FACTOR 1000000 //Conversion factor for micro seconds to seconds +#define TIME_TO_SLEEP 15 //Time ESP32 will go to sleep (in seconds) + +RTC_DATA_ATTR int16_t bootCount = 0; +RTC_DATA_ATTR uint16_t sleepcount = 0; + + +void loop() +{ + Serial.print(F("Bootcount ")); + Serial.println(bootCount); + Serial.print(F("Sleepcount ")); + Serial.println(sleepcount); + Serial.println(F("LED Flash")); + led_Flash(4, 125); + Serial.println(F("LED On")); + digitalWrite(LED1, HIGH); + delay(2500); + Serial.println(F("LED Off")); + digitalWrite(LED1, LOW); + + esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); + Serial.println(F("Start Sleep")); + Serial.flush(); + sleepcount++; + esp_light_sleep_start(); + Serial.println(); + Serial.println(); + Serial.println(F("Awake !")); +} + + +void led_Flash(unsigned int flashes, unsigned int delaymS) +{ + //flash LED to show tracker is alive + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, LOW); + delay(delaymS); + digitalWrite(LED1, HIGH); + delay(delaymS); + } +} + + +void setup() +{ + Serial.begin(9600); //setup Serial console ouput + Serial.println(F("50_LightSleep_Timed_Wakeup_ESP32 - Starting")); + + if (bootCount == 0) //Run this only the first time + { + bootCount = bootCount + 1; + } + + pinMode(LED1, OUTPUT); //for PCB LED + + if (VCCPOWER >= 0) + { + pinMode(VCCPOWER, OUTPUT); //For controlling power to external devices + digitalWrite(VCCPOWER, HIGH); //VCCOUT off, lora device and SD card off + } + +} diff --git a/examples/Hardware_Checks/ESP32/51_DeepSleep_Switch_Wakeup_ESP32/51_DeepSleep_Switch_Wakeup_ESP32.ino b/examples/Hardware_Checks/ESP32/51_DeepSleep_Switch_Wakeup_ESP32/51_DeepSleep_Switch_Wakeup_ESP32.ino new file mode 100644 index 0000000..c04fb40 --- /dev/null +++ b/examples/Hardware_Checks/ESP32/51_DeepSleep_Switch_Wakeup_ESP32/51_DeepSleep_Switch_Wakeup_ESP32.ino @@ -0,0 +1,86 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program flashes a LED connected to the pin defined by LED1, and puts the ESP32 + to deep_sleep. Pressing BOOT switch should wake up the ESP32 from sleep. + + Only the specific RTC IO pins can be used as a source for external wakeup. + These are pins: 0,2,4,12-15,25-27,32-39. + + Current in deep_sleep for a bare bones ESP32 with regulator and no other devices was 27uA. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#define LED1 2 //pin number for LED +#define SWITCH1 0 //pin number wakeup switch + + +RTC_DATA_ATTR int16_t bootCount = 0; +RTC_DATA_ATTR uint16_t sleepcount = 0; + + +void loop() +{ + Serial.print(F("Bootcount ")); + Serial.println(bootCount); + Serial.print(F("Sleepcount ")); + Serial.println(sleepcount); + Serial.println(F("LED Flash")); + led_Flash(4, 125); + Serial.println(F("LED On")); + digitalWrite(LED1, HIGH); + delay(2500); + Serial.println(F("LED Off")); + digitalWrite(LED1, LOW); + + //esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR); + + esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0); //wakeup on pin GPIO0 going low + + Serial.println(F("Start Sleep")); + Serial.flush(); + sleepcount++; + esp_deep_sleep_start(); + Serial.println(); + Serial.println(); + Serial.println(F("Awake ?")); //should not really see this, deep sleep wakeup causes reset .... +} + + +void led_Flash(unsigned int flashes, unsigned int delaymS) +{ + //flash LED to show tracker is alive + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, LOW); + delay(delaymS); + digitalWrite(LED1, HIGH); + delay(delaymS); + } +} + + +void setup() +{ + Serial.begin(9600); //setup Serial console ouput + Serial.println(); + Serial.println(); + Serial.println(F("51_DeepSleep_Timed_Wakeup_ESP32 - Starting")); + + if (bootCount == 0) //Run this only the first time + { + bootCount = bootCount + 1; + } + + pinMode(LED1, OUTPUT); //for PCB LED + pinMode(SWITCH1, INPUT_PULLUP); //for wakeup switch + +} diff --git a/examples/Hardware_Checks/ESP32/64_WiFi_Scanner_Display_ESP32/64_WiFi_Scanner_Display_ESP32.ino b/examples/Hardware_Checks/ESP32/64_WiFi_Scanner_Display_ESP32/64_WiFi_Scanner_Display_ESP32.ino new file mode 100644 index 0000000..ca83477 --- /dev/null +++ b/examples/Hardware_Checks/ESP32/64_WiFi_Scanner_Display_ESP32/64_WiFi_Scanner_Display_ESP32.ino @@ -0,0 +1,131 @@ +/******************************************************************************************************* + lora Programs for Arduino - Copyright of the author Stuart Robinson - 20/01/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - When the ESP32 turns on the WiFi function, there is a short high current pulse that + can cause the ESP32 brownout detect to operate. + + This test program at startup flashes an LED, leaves it on and then starts the WiFi. If the Wifi initiates + a brownout, you will see the LED flash again. The LED stays on when scanning, the program reports the + networks found to the serial console and displays them on an attached SSD1306 OLED. + + Thus if you see the LED continually doing short bursts of flashing the turn on\off the WiFi is causing + the ESP32 to reset. There will also be a message on the serial monitor that the brownout detector operated. + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include "WiFi.h" +#define LED1 2 //Arduino pin number for LED, when high LED should be on. + +#include //get library here > https://github.com/olikraus/u8g2 +//U8X8_SSD1306_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //use this line for standard 0.96" SSD1306 +U8X8_SH1106_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //use this line for 1.3" OLED often sold as 1.3" SSD1306 + + +void loop() +{ + Serial.println("Set WiFi to Station mode"); //Set WiFi to station mode + Serial.flush(); + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + delay(1000); + + Serial.println("Setup done"); + Serial.flush(); + + digitalWrite(LED1, HIGH); + Serial.println("WiFi scan start"); + Serial.flush(); + int n = WiFi.scanNetworks(); //WiFi.scanNetworks will return the number of networks found + digitalWrite(LED1, LOW); + delay(500); + disp.clear(); + disp.setCursor(0, 0); + + if (n == 0) { + Serial.println("No WiFi"); + disp.println("No WiFi"); + } else { + Serial.print(n); + disp.print(n); + Serial.println(" WiFi found"); + disp.println(" WiFi found"); + led_Flash(n, 500); + + if (n > 16) //only want to display first 16 networks + { + n = 16; + } + + for (int i = 0; i < n; ++i) { + //Print SSID and RSSI for each network found + Serial.print(i + 1); + Serial.print(": "); + Serial.print(WiFi.SSID(i)); + + if (i > 7) + { disp.clearLine(i - 8); + disp.setCursor(0, i - 8); + } + else + { + disp.clearLine(i); + disp.setCursor(0, i); + } + + disp.print(WiFi.SSID(i)); + Serial.print(" ("); + Serial.print(WiFi.RSSI(i)); + Serial.print(")"); + Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN) ? " " : "*"); + if (i == 7) //check if 8 lines have already been sent to display + { + delay(2000); //leave last 8 on display for a while + disp.clear(); + } + } + } + Serial.println(); + disp.println(); + + + // Wait a bit before scanning again + delay(5000); +} + + +void led_Flash(unsigned int flashes, unsigned int delaymS) +{ + //flash LED + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(5, 50); + digitalWrite(LED1, LOW); + delay(1000); + Serial.begin(9600); + + disp.begin(); + disp.setFont(u8x8_font_chroma48medium8_r); + disp.clear(); + disp.setCursor(0, 0); + disp.print(F("Scanner Ready")); +} diff --git a/examples/ReadMe.md b/examples/ReadMe.md new file mode 100644 index 0000000..92b5c05 --- /dev/null +++ b/examples/ReadMe.md @@ -0,0 +1,636 @@ +## SX12XX Library Example Programs + + +For the majority of the program examples you will need to define the pins used plus the frequency and the LoRa settings used in the Settings.h file. The default provided settings may not be optimised for long distance. See the 'What is LoRa' document for information on how LoRa settings affect range. + +Some of the examples use sleep mode on the processor and LoRa device to save power. Typical sleep currents may be mentioned in the description of the program. In most all cases a 'bare bones' Atmel based Arduino has been used to measure the sleep currents and you may not get even close to the quoted figures using standard Arduinos such as Pro Minis or similar. Optimising particular Arduino boards for low power sleep is outside of the scope of this library and examples. + + + + +#### 1\_LED\_Blink             (Basics folder) + +This program blinks an LED connected the pin number defined by LED1.The pin 13 LED, fitted to some Arduinos is blinked as well. The blinks should be close to one per second. Messages are sent to the Serial Monitor also. + +#### 2\_Register\_Test             (Basics folder) +This program is stand alone, it is not necessary to install the SX12XX-LoRa library to use it. + +The program checks that a lora device can be accessed by doing a test register write and read. If there is no device found a message is printed on the serial monitor. The contents of the registers from 0x00 to 0x7F are printed, there is a copy of a typical printout below. Note that the read back changed frequency may be different to the programmed frequency, there is a rounding error due to the use of floats to calculate the frequency. + + SX1276-79 Selected + LoRa Device found + Device version 0x12 + Frequency at reset 434000000 + Registers at reset + Reg 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0x00 00 09 1A 0B 00 52 6C 80 00 4F 09 2B 20 08 02 0A + 0x10 FF 70 15 0B 28 0C 12 47 32 3E 00 00 00 00 00 40 + 0x20 00 00 00 00 05 00 03 93 55 55 55 55 55 55 55 55 + 0x30 90 40 40 00 00 0F 00 00 00 F5 20 82 04 02 80 40 + 0x40 00 00 12 24 2D 00 03 00 04 23 00 09 05 84 32 2B + 0x50 14 00 00 10 00 00 00 0F E0 00 0C 04 06 00 5C 78 + 0x60 00 19 0C 4B CC 0D FD 20 04 47 AF 3F F2 3F D9 0B + 0x70 D0 01 10 00 00 00 00 00 00 00 00 00 00 00 00 00 + + + Changed Frequency 434099968 + Reg 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0x00 00 09 1A 0B 00 52 6C 86 66 4F 09 2B 20 08 02 0A + 0x10 FF 70 15 0B 28 0C 12 47 32 3E 00 00 00 00 00 40 + 0x20 00 00 00 00 05 00 03 93 55 55 55 55 55 55 55 55 + 0x30 90 40 40 00 00 0F 00 00 00 F5 20 82 04 02 80 40 + 0x40 00 00 12 24 2D 00 03 00 04 23 00 09 05 84 32 2B + 0x50 14 00 00 10 00 00 00 0F E0 00 0C 04 06 00 5C 78 + 0x60 00 19 0C 4B CC 0D FD 20 04 47 AF 3F F2 3F D9 0B + 0x70 D0 01 10 00 00 00 00 00 00 00 00 00 00 00 00 00 + + +#### 3\_LoRa\_Transmitter             (Basics folder) +This is a minimum setup LoRa test transmitter. A packet containing the ASCII text "Hello World 1234567890" is sent using the frequency and LoRa settings specified in the LT.setupLoRa() command. The pins to access the lora device need to be defined at the top of the program also. + +The details of the packet sent and any errors are shown on the Arduino IDE Serial Monitor, together with the transmit power used and the packet length. The matching receiver program, '4\_LoRa\_Receiver' can be used +to check the packets are being sent correctly, the frequency and LoRa settings (in the LT.setupLoRa() commands) must be the same for the transmitter and receiver programs. Sample Serial Monitor output; + +10dBm Packet> Hello World 1234567890 BytesSent,23 PacketsSent,6 + +For an example of a more detailed configuration for a transmitter, see program 103\_LoRa\_Transmitter. + +Serial monitor baud rate is set at 9600 + +#### 4\_LoRa\_Receiver             (Basics folder) + +This is a minimum setup LoRa test receiver. The program listens for incoming packets using the frequency and LoRa settings in the LT.setupLoRa() command. The pins to access the lora device need to be defined at the top of the program also. + +There is a printout on the Arduino IDE serial monitor of the valid packets received, the packet is assumed to be in ASCII printable text, if it's not ASCII text characters from 0x20 to 0x7F, expect weird things to happen on the Serial Monitor. Sample serial monitor output; + +8s Hello World 1234567890,RSSI,-44dBm,SNR,9dB,Length,23,Packets,7,Errors,0,IRQreg,50 + +If there is a packet error it might look like this, which is showing a CRC error; + +137s PacketError,RSSI,-89dBm,SNR,-8dB,Length,23,Packets,37,Errors,2,IRQreg,70,IRQ\_HEADER\_VALID,IRQ\_CRC\_ERROR,IRQ\_RX\_DONE + +If there are no packets received in a 10 second period then you should see a message like this; + +112s RXTimeout + +For an example of a more detailed configuration for a receiver, see program 104\_LoRa\_Receiver. + +Serial monitor baud rate is set at 9600. +
+ +#### 5\_LoRa\_TX\_Sleep\_Timed\_Wakeup\_Atmel             (Sleep folder) + +This program tests the sleep mode and register retention of the lora device in sleep mode, it assumes an Atmel ATMega328P processor is in use. The LoRa settings to use are specified in the 'Settings.h' file. + +A packet is sent, containing the text 'Before Device Sleep' and the LoRa device and Atmel processor are put to sleep. The processor watchdog timer should wakeup the processor in 15 seconds (approx) and register values should be retained. The device then attempts to transmit another packet 'After Device Sleep' without re-loading all the LoRa settings. The receiver should see 'After Device Sleep' for the first packet and 'After Device Sleep' for the second. + +Tested on a 'bare bones' ATmega328P board, the current in sleep mode was 6.5uA. + + +#### 6\_LoRa\_RX\_and\_Sleep\_Atmel             (Sleep folder) + + +The program listens for incoming packets using the LoRa settings in the 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + +When the program starts the LoRa device is set-up to receive packets with pin DIO0 set to go high when a packet arrives. The receiver remains powered (it cannot receive otherwise) and the processor (Atmel ATMega328P or 1284P) is put to sleep. When pin DIO0 does go high, indicating a packet is received, the processor wakes up and prints the packet. It then goes back to sleep. + +There is a printout of the valid packets received, these are assumed to be in ASCII printable text. The LED will flash for each packet received and the buzzer will sound,if fitted. + +Tested on a 'bare bones' ATmega328P board, the current in sleep mode was 12.26mA. + +#### 7\_LoRa\_TX\_Sleep\_Switch\_Wakeup\_Atmel             (Sleep folder) + + +This program tests the sleep mode and register retention of the lora device in sleep mode, it assumes an Atmel ATMega328P processor is in use. The LoRa settings to use are specified in the 'Settings.h' file. + +A packet is sent, containing the text 'Before Device Sleep' and the lora device and Atmel processor are put to sleep. The processor should remain asleep until the pin defined by SWITCH1 in the Settings.h file is connected to ground and the LoRa device register values should be retained. The LoRa device then attempts to transmit another packet 'After Device Sleep' without re-loading all the LoRa settings. The receiver should see 'After Device Sleep' for the first packet and 'After Device Sleep' for the second. + +Tested on a bare bones ATmega328P board, the current in sleep mode was 2.4uA. + +#### 8\_LoRa\_LowMemory\_TX             (LowMemory folder) + +The program transmits a packet without using a processor buffer, the LoRa device internal buffer is filled direct with variables. The program is a simulation of the type of packet that might be sent from a GPS tracker. Note that in this example a buffer of text is part of the transmitted packet, this does need a processor buffer which is used to fill the LoRa device internal buffer, if you don't need to transmit text then the uint8_t trackerID[] = "tracker1"; definition can be omitted. + +The matching receiving program '9\_LoRa\_LowMemory\_RX' can be used to receive and display the packet, though the program '15\_LoRa\_RX\_Structure' should receive it as well, since the packet contents are the same. + +The contents of the packet received, and printed to serial monitor, should be; + +"tracker1" (buffer) - trackerID +1+ (uint32\_t) - packet count +51.23456 (float) - latitude +-3.12345 (float) - longitude +199 (uint16\_t) - altitude +8 (uint8\_t) - number of satellites +3999 (uint16\_t) - battery voltage +-9 (int8_t) - temperature + +#### 9\_LoRa\_LowMemory\_RX             (LowMemory folder) + +The program receives a packet without using a processor buffer, the LoRa device internal buffer is read direct and copied to variables. The program is a simulation of the type of packet that might be received from a GPS tracker. Note that in this example a buffer of text is part of the received packet, this does need a processor buffer which is filled with data from the LoRa device internal buffer, if you don't need to send and receive text then the uint8_t receivebuffer[32]; definition can be omitted. + +The contents of the packet received, and printed to serial monitor, should be; + +"tracker1" (buffer) - trackerID +1+ (uint32\_t) - packet count +51.23456 (float) - latitude +-3.12345 (float) - longitude +199 (uint16\_t) - altitude +8 (uint8\_t) - number of satellites +3999 (uint16\_t) - battery voltage +-9 (int8_t) - temperature + + +#### 10\_LoRa\_Link\_Test\_Transmitter             (Diagnostics and Test folder) + +This is a program that can save you a great deal of time if your testing the effectiveness of a LoRa links or attached antennas. Simulations of antenna performance are no substitute for real world tests and this simple program allows both long distance link performance to be evaluated and antenna performance to be compared. + +The program sends short test packets that reduce in power by 1dBm at a time. The start power is defined by start\_power and the end power is defined by end\_power (see Settings.h file). Once the end\_power point is reached, the program pauses a short while and starts the transmit sequence again at start\_power. The packet sent contains the power used to send the packet. By listening for the packets with the basic LoRa receive program (4\_LoRa\_Receiver) you can see the reception results, which should look something like this; + + 11s 1*T+05,CRC,80B8,RSSI,-03dBm,SNR,9dB,Length,6, Packets,9,Errors,0,IRQreg,50 + 12s 1*T+04,CRC,9099,RSSI,-74dBm,SNR,9dB,Length,6, Packets,10,Errors,0,IRQreg,50 + 14s 1*T+03,CRC,E07E,RSSI,-75dBm,SNR,9dB,Length,6, Packets,11,Errors,0,IRQreg,50 + +Above shows 3 packets received, the first at +05dBm (+05 in printout), the second at 4dBm (+04 in printout) and the third at 3dBm (+03) in printout. + +If it is arranged so that reception of packets fails halfway through the sequence by attenuating either the transmitter (with an SMA attenuator for instance) or the receiver (by placing it in a tin perhaps) then if you swap transmitter antennas you can see the dBm difference in reception, which will be the dBm difference (gain) of the antenna. + +To start the sequence a packet is sent with the number 999, when received it looks like this; + +T*1999 + +This received packet could be used for the RX program to be able to print totals etc. + +LoRa settings to use for the link test are specified in the 'Settings.h' file. + + +#### 11\_LoRa\_Packet\_Logger\_Receiver             (Diagnostics and Test folder) + + +This is a useful packet logger program. It listens for incoming packets using the LoRa settings in the 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + +There is a printout of the valid packets received in HEX format. Thus the program can be used to receive and record non-ASCII packets. The LED will flash for each packet received and the buzzer will sound, if fitted. The measured frequency difference between the frequency used by the transmitter and the frequency used by the receiver is shown. If this frequency difference gets to 25% of the set LoRa bandwidth, packet reception will fail. The displayed error can be reduced by using the 'offset' setting in the 'Settings.h' file. + +#### 12\_ATmel\_Sleep\_with\_Switch\_Wakeup             (Hardware_Checks folder) + +This program tests the deep sleep mode and wakeup with a switch of an Atmel 328P or 1284P processor. The program starts, flashes the LED and then puts the processor into permanent sleep. It can be woken up with a switch press. Used as a base test routine for checking the sleep current of a board. + +Tested on an 'bare bones' ATmega328P board, the current in sleep mode was 1.7uA with a 3.3V MCP1700 regulator being used. + + +#### 13\_Frequency\_and\_Power\_Check\_TX             (Diagnostics and Test folder) + +This is a program that transmits a long LoRa packets lasting about 5 seconds that can be used to measure the frequency and power of the transmission using external equipment. The bandwidth of the transmission is only 10khz, so a frequency counter should give reasonable average result. + +The LoRa settings to use, including transmit power, are specified in the 'Settings.h' file. + + +#### 14\_LoRa\_Structure\_TX             (Basics folder) + +This program demonstrates the transmitting of a structure as a LoRa packet. The contents of the structure are the same as in the '8\_LoRa\_LowMemory\_TX' program. The packet sent is typical of what might be sent from a GPS tracker. The structure type is defined as trackerPacket and an instance called location1 is created. The structure which includes a character array (text) is filled with values and transmitted. + +The matching receiving program '15\_LoRa\_RX\_Structure' can be used to receive and display the packet, though the program '9\_LoRa\_LowMemory\_RX' should receive it as well, since the contents are the same. + +Note that the structure definition and variable order (including the buffer size) used in the transmitter need to match those used in the receiver. + +The contents of the packet transmitted should be; + +"tracker1" (buffer) - trackerID +1+ (uint32\_t) - packet count +51.23456 (float) - latitude +-3.12345 (float) - longitude +199 (uint16\_t) - altitude +8 (uint8\_t) - number of satellites +3999 (uint16\_t) - battery voltage +-9 (int8_t) - temperature + +#### 15\_LoRa\_Structure\_RX             (Basics folder) + + +This program demonstrates the receiving of a structure as a LoRa packet. The packet sent is typical of what might be sent from a GPS tracker. + +The structure type is defined as trackerPacket and an instance called location1 is created. The structure includes a character array (text). + +The matching receiving program is '15\_LoRa\_RX\_Structure' can be used to receive and display the packet, though the program '9\_LoRa\_LowMemory\_RX' should receive it as well, since the packet contents are the same. + +Not that the structure definition and variable order (including the buffer size) used in the transmitter need to match those used in the receiver. Good luck. + +The contents of the packet received, and printed to serial monitor, should be; + +"tracker1" (buffer) - trackerID +1+ (uint32\_t) - packet count +51.23456 (float) - latitude +-3.12345 (float) - longitude +199 (uint16\_t) - altitude +8 (uint8\_t) - number of satellites +3999 (uint16\_t) - battery voltage +-9 (int8_t) - temperature + + +#### 16\_LoRa\_RX\_Frequency\_Error\_Check             (Diagnostics and Test folder) + +This program can be used to check the frequency error between a pair of LoRa devices, a transmitter and receiver. This receiver measures the frequecy error between the receivers centre frequency and the centre frequency of the transmitted packet. The frequency difference is shown +for each packet and an average over 10 received packets reported. Any transmitter program can be used to give this program something to listen to, including example program '3\_LoRa\_Transmitter'. + + +#### 17\_Sensor\_Transmitter             (Sensor folder) + +The program transmits a LoRa packet without using a processor buffer, the LoRa devices internal buffer is filled directly with variables. + +The sensor used is a BME280. The pressure, humidity, and temperature are read and transmitted. There is also a 16bit value of battery mV (simulated) and and a 8 bit status value at the packet end. + +Although the LoRa packet transmitted and received has its own internal CRC error checking, you could still receive packets of the same length from another source. If this valid packet were to be used to recover the sensor values, you could be reading rubbish. To reduce the risk of this, when the packet is transmitted the CRC value of the actual sensor data is calculated and sent out with the packet. This CRC value is read by the receiver and used to check that the received CRC matches the supposed +sensor data in the packet. As an additional check there is some addressing information at the beginning of the packet which is also checked for validity. Thus we can be relatively confident when reading the +received packet that its genuine and from this transmitter. The packet is built and sent in the sendSensorPacket() function, there is a 'highlighted section' where the actual sensor data is added to the packet. + +Between readings the LoRa device, BME280 sensor, and Atmel micro controller are put to sleep in units of 8 seconds using the Atmel processor internal watchdog. + +The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. The Atmel watchdog timer is a viable option for a very low current sensor node. A bare bones ATmega328P with regulator and LoRa device has a sleep current of 6.6uA, add the LoRa devices and BME280 sensor module and the average sleep current only rises to 6.8uA. + +One of these transmitter programs is running on a long term test with a 175mAh battery, to see how long the battery actually lasts. + + +#### 18\_Sensor\_Receiver             (Sensor folder) + +The program receives a LoRa packet without using a processor buffer, the LoRa devices internal buffer is read direct for the received sensor data. + +The sensor used in the matching '17\_Sensor\_Transmitter' program is a BME280 and the pressure, humidity, and temperature are being and received. There is also a 16bit value of battery mV and and a 8 bit status value at the end of the packet. + +When the program starts, the LoRa device is set-up to set the DIO0 pin high when a packet is received, the Atmel processor is then put to sleep and will wake up when a packet is received. When a packet is received, its printed and assuming the packet is validated, the sensor results are printed to the serial monitor and screen. Between readings the sensor transmitter is put to sleep in units of 8 seconds using the Atmel +processor internal watchdog. + +For the sensor data to be accepted as valid the flowing need to match; + +The 16bit CRC on the received sensor data must match the CRC value transmitted with the packet. +The packet must start with a byte that matches the packet type sent, 'Sensor1' +The RXdestination byte in the packet must match this node ID of this receiver node, defined by 'This_Node' + +In total that's 16 + 8 + 8 = 32bits of checking, so a 1:4294967296 chance (approx) that an invalid packet is acted on and erroneous values displayed. + +The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. + +With a standard Arduino Pro Mini and SSD1306 display the current consumption was 20.25mA with the display and 16.6mA without the display. + + +#### 20\_LoRa\_Link\_Test\_Receiver             (Diagnostics and Test folder) + +The program listens for incoming packets using the LoRa settings in the 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + +The program is a matching receiver program for the '10\_LoRa\_Link\_Test\_TX'. The packets received are displayed on the serial monitor and analysed to extract the packet data which indicates the power used to send the packet. A count is kept of the numbers of each power setting received. When the transmitter sends the test mode packet at the beginning of the sequence (displayed as 999) the running totals of the powers received are printed. Thus you can quickly see at what transmit power levels the reception fails. + + +#### 21\_On\_Off\_Transmitter             (Remote Control folder) + +This program is a remote control transmitter. When one of four switches are made (shorted to ground) a packet is transmitted with single byte indicating the state of Switch0 as bit 0, Switch1 as bit 1 and Switch2 as bit 2. To prevent false triggering at the receiver the packet contains a +32 bit number called the TXIdentity which in this example is set to 1234554321. The receiver will only act on, change the state of the outputs, if the identity set in the receiver matches that of the transmitter. The chance of a false trigger is fairly remote. + +Between switch presses the LoRa device and Atmel micro controller are put to sleep. A switch press wakes up the processor from sleep, the switches are read and a packet sent. On a 'bare bones' Arduino set-up the transmitter has a sleep current of approx 2.2uA, so it's ideal for a battery powered remote control with a potential range of many kilometres. + + +The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. + + +#### 22\_On\_Off\_Receiver             (Remote Control folder) + +This program is a remote control receiver. When a packet is received an 8 bit byte (SwitchByte) is read and the four outputs (defined in Settings.h) are toggled according to the bits set in this byte. If the Switch1 byte has bit 0 cleared, then OUTPUT0 is toggled. If the Switch1 byte has bit 1 cleared, then OUTPUT1 is toggled. If the Switch1 byte has bit 2 cleared, then OUTPUT2 is toggled. + +To prevent false triggering at the receiver the packet also contains a 32 bit number called the TXIdentity which in this example is set to 1234554321. The receiver will only act on, change the state of the outputs, if the identity set in the receiver matches that of the transmitter. The chance of a false trigger is fairly remote. + +The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. + + +#### 23\_GPS\_Tracker\_Transmitter             (Tracker folder) + +This program is an example of a basic GPS tracker. The program reads the GPS, waits for an updated fix and transmits location and altitude, number of satellites in view, the HDOP value, the fix time of the GPS and the battery voltage. This transmitter can be also be used to investigate GPS performance. At start-up there should be a couple of seconds of recognisable text from the GPS printed to the serial monitor. If you see garbage or funny characters its likely the GPS baud rate is wrong. If the transmitter is turned on from cold, the receiver will pick up the cold fix time, which is an indication of GPS performance. The GPS will be powered on for around 4 seconds before the timing of the fix starts. Outside with a good view of the sky most GPSs should produce a fix in around 45 seconds. The number of satellites and HDOP are good indications to how well a GPS is working. + +The program writes direct to the LoRa devices internal buffer, no memory buffer is used. + +The LoRa settings are configured in the Settings.h file. + +The program has the option of using a pin to control the power to the GPS, if the GPS module being used has this feature. To use the option change the define in the Settings.h file; '#define GPSPOWER -1' from -1 to the pin number being used. Also set the GPSONSTATE and GPSOFFSTATE to the appropriate logic levels. + + +#### 24\_GPS\_Tracker\_Receiver             (Tracker folder) + +This program is an basic receiver for the '23\_Simple\_GPS\_Tracker\_Transmitter' program. + +The program reads the received packet from the tracker transmitter and displays the results on the serial monitor. The LoRa and frequency settings provided in the Settings.h file must match those used by the transmitter. + +The program receives direct from the LoRa devices internal buffer. + + +#### 25\_GPS\_Tracker\_Receiver\_with\_Display\_and\_GPS             (Tracker folder) + +This program is an example of a basic portable GPS tracker receiver. The program receives the location packets from the remote tracker and displays them on an OLED display. The program also reads a local GPS and when that has a fix, will display the distance and direction to the remote tracker. + +The program writes direct to the LoRa devices internal buffer, no memory buffer is used. + +The LoRa settings are configured in the Settings.h file. + +The received information is printed to screen in this order top to bottom; + +Latitude, Longitude, Altitude, HDOP, GPS Fixtime, Tracker battery mV, Number of received packets, Distance and direction to tracker, if local GPS fix. In addition if there is a recent tracker transmitter GPS fix a 'T' is shown on line 0 right of screen and if there is a recent local (receiver) GPS fix a 'R' is displayed line 1 right of screen. + +The received information is printed to the Serial Monitor as CSV data in this order; + +Packet Address information, Latitude, Longitude, Altitude, Satellites in use, HDOP, TX status byte, GPS Fixtime, Tracker battery mV, Number of received packets, Distance and direction to tracker, if local GPS fix. + +The program has the option of using a pin to control the power to the GPS, if the GPS module being used has this feature. To use the option change the define in Settings.h; '#define GPSPOWER -1' from -1 to the pin number being used. Also set the GPSONSTATE and GPSOFFSTATE to the appropriate logic levels. + +The program by default uses software serial to read the GPS, you can use hardware serial by commenting out this line in the Settings.h file; + + #define USE\_SOFTSERIAL\_GPS + +And then defining the hardware serial port you are using, which defaults to Serial1. + +#### 26\_GPS\_Echo             (Hardware_Checks folder) + +This is a simple program to test a GPS. It reads characters from the GPS using software serial and sends them (echoes) to the IDE serial monitor. If your ever having problems with a GPS (or just think you are) use this program first. + +If you get no data displayed on the serial monitor, the most likely cause is that you have the receive data pin into the Arduino (RX) pin connected incorrectly. + +If the data displayed on the serial terminal appears to be random text with odd symbols its very likely you have the GPS serial baud rate set incorrectly. + +Note that not all pins on all Arduinos will work with software serial, see here; + +https://www.arduino.cc/en/Reference/softwareSerial + +Serial monitor baud rate is set at 115200. + + +#### 28\_GPS\_Checker             (Hardware_Checks folder) + +This program is a portable GPS checker and display. At start-up the program starts checking the data coming from the GPS for a valid fix. It checks for 5 seconds and if there is no fix, prints a message on the serial monitor. During this time the data coming from the GPS is copied to the serial monitor also. + +When the program detects that the GPS has a fix, it prints the Latitude, Longitude, Altitude, Number of satellites in use and the HDOP value to the serial monitor. + +Serial monitor baud rate is set at 115200, GPS baud rate to 9600, both are configured in setup(). + + +#### 29\_GPS\_Checker\_With\_Display             (Hardware_Checks folder) + +This program is a GPS checker with a display output. It uses an SSD1306 or SH1106 128x64 I2C OLED display. At start-up the program starts checking the data coming from the GPS for a valid fix. It reads the GPS for 5 seconds and if there is no fix, prints a message on the serial monitor and updates the seconds without a fix on the display. During this time the data coming from the GPS is copied to the serial monitor also. + +When the program detects that the GPS has a fix, it prints the Latitude, Longitude, Altitude, Number of satellites in use, the HDOP value, time and date to the serial monitor. If the I2C OLED display is attached that is updated as well. Display is assumed to be on I2C address 0x3C. + + +#### 30\_I2C\_Scanner             (Hardware_Checks folder) + +The program scans the I2C bus and displays the addresses of any devices found. Useful first check when using I2C devices. + + +#### 31\_SSD1306\_OLED\_Checker             (Diagnostics and Test folder) + +This program is a simple test program for the SSD1306 and SH1106 OLEDs. The program prints a short message on each line, pauses, clears the screen, and starts again. + +OLED address is defined as 0x3C. + +#### 33\_LoRa\_RSSI\_Checker\_With\_Display             (Diagnostics and Test folder) + +The program listens for incoming packets using the LoRa settings in the 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + +There is a printout of the valid packets received, the packet is assumed to be in ASCII printable text, if its not ASCII text characters from 0x20 to 0x7F, expect weird things to happen on the Serial Monitor. The LED will flash for each packet received and the buzzer will sound, if fitted. + +Sample serial monitor output; + +1109s {packet contents} CRC,3882,RSSI,-69dBm,SNR,10dB,Length,19,Packets,1026,Errors,0,IRQreg,50 + +If there is a packet error it might look like this, which is showing a CRC error, + +1189s PacketError,RSSI,-111dBm,SNR,-12dB,Length,0,Packets,1126,Errors,1,IRQreg,70,IRQ\_HEADER\_VALID,IRQ\_CRC\_ERROR,IRQ\_RX\_DONE + +A summary of the packet reception is sent to the OLED display as well, useful for portable applications. + + +#### 34\_ATmel\_Sleep\_with\_Watchdog\_Wakeup             (Hardware_Checks folder) + +This program tests the sleep mode of an Atmel ATMega328P processor. + +At power up the flashes an LED 4 times, then turns on the LED for 5 seconds. Then the processor is put to sleep for 8 seconds. On wakeup the LED flashes twice, then is on for 5 seconds and the board goes to sleep again. And the sequence repeats. + +Sleep current for a 'bare bones' ATmega328 with a MCP1700 regulator @ 3.3V and using an external event such as a switch to wakeup from sleep should be around 2uA. Using the watchdog timer to wakeup raises the deep sleep current to circa 6.2uA. + +#### 35\_Remote\_Control\_Servo\_Transmitter             (Remote Control folder) + +This is a remote control transmitter that uses a LoRa link to transmit the positions from a simple joystick to a remote receiver. The receiver uses the sent joystick positions to adjust the positions of servos. The positions of the joysticks potentiometers on the transmitter are read with the analogueRead() function. + +If the joystick has a switch, often made by pressing on the joystick, then this can be used to remote control an output on the receiver. The switch is read by an interrupt, the interrupt routine sets a flag byte which is read in loop(). + +The program is intended as a proof of concept demonstration of how to remote control servos, the program is not designed as a practical remote control device for RC model cars for instance. + +It would be straight forward to make the transmitter program send packets continuously, but in most places in the world that would break a normal limitation of 10% duty cycle for unlicensed use. Therefore the program was designed to only transmit at a 10% duty cycle. Thus the fastest (lowest air time) packets are used, spreading factor 6 at a bandwidth of 500khz. This results in an air time for the 5 byte control packet of around 4mS, so there are around 25 sent per second. + +To have the transmitter program print out the values read from the joystick, comment in the line; + +//#define DEBUG + +Which is just above the loop() function. With the DEBUG enabled the transmission rate, the rate at which the control packets are transmitted will be slowed down. + +To reduce the risk of the receiver picking up LoRa packets from other sources, the packet sent contains a 'TXidentity' number, valid values are 0 - 255. The receiver must be set-up with the matching identity number or the received packets will be ignored. + +The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. These settings are not necessarily optimised for long range. + + +#### 36\_Remote\_Control\_Servo\_Receiver             (Remote Control folder) + +This is a remote control receiver that uses a LoRa link to control the positions of servos sent from a remote transmitter. + +If the transmitter joystick has a switch, often made by pressing on the joystick, then this can be used to remote control an output on the receiver. + +The program is intended as a proof of concept demonstration of how to remote control servos, the program is not designed as a practical remote control device for RC model cars for instance. + +It would be straight forward to make the transmitter program send packets continuously, but in most places in the world that would break a normal limitation of 10% duty cycle for unlicensed use. Therefore the program was designed to only transmit at a 10% duty cycle. Thus the fastest (lowest air time) packets are used, spreading factor 6 at a bandwidth of 500khz. This results in an air time for the 5 byte control packet of around 4mS, so there are around 25 sent per second. +To have the receiver program print out the joystick values (0-255) read from the received packet, comment in the line; + +//#define DEBUG + +Which is just above the loop() function. With the DEBUG enabled then there is a possibility that some transmitted packets will be missed. With the DEBUG line enabled to servos should also sweep to and fro 3 times at program start-up. + +To reduce the risk of the receiver picking up LoRa packets from other sources, the packet sent contains a 'TXidentity' number, valid values are 0 - 255. The receiver must be set-up with the matching RXIdentity number in Settings.h or the received packets will be ignored. + +The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. These settings are not necessarily optimised for long range. + + +#### 37\_Servo\_Sweep\_Tester             (Hardware_Checks folder) + +This program sweeps two servos from one end of their travel to the other. Useful to check servos are connected correctly and working. + + +#### 38\_lora\_Relay             (Tracker folder) + +This program will receive a lora packet and relay (re-transmit) it. The receiving and transmitting can use different frequencies and lora settings. The receiving and transmitting settings are in the 'Settings.h' file. For an example of it's use see this report; + +How to Search 500 Square Kilometres in 10 minutes.pdf in the libraries 'Test_Reports' folder. + + + +#### 40\_LoRa\_Transmitter\_ImplicitPacket             (Implicit folder) + +This is an example of the use of implicit or fixed length LoRa packets. +Implicit packets have no header so both transmitter and receiver need to be programmed with the packet length in use. The use of spreading factor 6 requires implicit packets and together with a bandwidth of 500khz, leads to the shortest possible and lowest air time packets. + +This example sends a buffer that is 23 characters long and that length must be defined in Settings.h as the constant 'PacketLength'. + +A packet containing ASCII text is sent according to the frequency and LoRa settings specified in the 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + +The details of the packet sent and any errors are shown on the Serial Monitor, together with the transmit power used, the packet length and the CRC of the packet. The matching receive program, '41\_LoRa\_Receiver\_ImplicitPackets' can be used to check the packets are being sent correctly, the frequency and LoRa settings (in Settings.h) must be the same for the Transmit and Receive program. + +Sample Serial Monitor output; + +10dBm Packet> {packet contents*} BytesSent,23 CRC,DAAB TransmitTime,8mS PacketsSent,1 + + +#### 41\_LoRa\_Receiver\_ImplicitPacket             (Implicit folder) + +This is an example of the use of implicit or fixed length LoRa packets. Implicit packets have no header so both transmitter and receiver need to be programmed with the packet length in use. The use of spreading factor 6 requires implicit packets and together with a bandwidth of 500khz, leads to the shortest possible and lowest air time packets. The program listens for incoming packets using the LoRa settings in the 'Settings.h'. + +This example receives a buffer that is 19 characters long and that length must be defined in Settings.h as the constant 'PacketLength'. + +The pins to access the lora device need to be defined in the 'Settings.h' file also. + +There is a printout of the valid packets received, the packet is assumed to be in ASCII printable text, if its not ASCII text characters from 0x20 to 0x7F, expect weird things to happen on the Serial Monitor. The LED will flash for each packet received and the buzzer will sound, if fitted. + +Sample serial monitor output; + +1109s {packet contents} CRC,3882,RSSI,-69dBm,SNR,10dB,Length,19,Packets,1026,Errors,0,IRQreg,50 + +If there is a packet error it might look like this, which is showing a CRC error, + +1189s PacketError,RSSI,-111dBm,SNR,-12dB,Length,0,Packets,1126,Errors,1,IRQreg,70,IRQ\_HEADER\_VALID,IRQ\_CRC\_ERROR,IRQ\_RX\_DONE + + +#### 42\_WiFi\_Scanner\_Display\_ESP32             (Hardware Checks\ESP32 folder) + +When the ESP32 turns on the WiFi function, there is a short high current pulse that can cause the ESP32 brownout detect to operate. + +This test program at startup flashes an LED, leaves it on and then starts the WiFi. If the Wifi initiates a brownout, you will see the LED flash again. The LED stays on when scanning, the program reports the networks found to the serial console and displays them on an attached SSD1306 OLED. + +Thus if you see the LED continually doing short bursts of flashing the turn on off the WiFi is causing the ESP32 to reset. There will also be a message on the serial monitor that the brownout detector operated. + + +#### 43\_SD\_Card\_Test\_ESP32             (Hardware Checks\ESP32 folder) + +This test program has been written to check that a connected SD card adapter, Micro or standard, is functional. To use the program first copy the file (in this programs directory) called testfile.txt to the root directory of the SD card. + +When the program runs it will attempt to open 'testfile.txt' and spool the contents to the Arduino IDE serial monitor. The testfile is part of the source code for the Apollo 11 Lunar Lander navigation and guidance computer. There are LED flashes at power up or reset, then at start of every loop of the test. The LED is on whilst the testfile is being read. If the LED flashes very rapidly then there is a problem accessing the SD card. + +The program also has the option of using a logic pin to control the power to the lora and SD card devices, which can save power in sleep mode. If the hardware is fitted to your board these devices are powered on by setting the VCCPOWER pin low. If your board does not have this feature set VCCPOWER to -1. + +#### 44\_SD\_Card\_Test\_With\_FS\_ESP322             (Hardware Checks\ESP32 folder) + +This test program has been written to check that a connected SD card adapter, Micro or standard, is functional with the FS functions. To use the program first copy the file (in this programs directory) called testfile.txt to the root directory of the SD card. + +When the program runs it will attempt to open 'testfile.txt' and spool the contents to the Arduino IDE serial monitor. The testfile is part of the source code for the Apollo 11 Lunar Lander navigation and guidance computer. There are LED flashes at power up or reset, then at start of every loop of the test. The LED is on whilst the testfile is being read. If the LED flashes very rapidly then there is a problem accessing the SD card. + +The program also has the option of using a logic pin to control the power to the lora and SD card devices, which can save power in sleep mode. If the hardware is fitted to your board these devices are powered on by setting the VCCPOWER pin low. If your board does not have this feature set VCCPOWER to -1. + + +#### 45\_Battery\_Voltage\_Read\_Test             (Hardware_Checks folder) + + +This test program has been written to check that hardware for reading the battery voltage has been assembled correctly such that it is funtional. The value defined as 'ADMultiplier' in settings.h is used to adjust the value read from the 91K\11K resistor divider and convert into mV. + +There is also an option of using a logic pin to turn the resistor divider used to read battery voltage on and off. This reduces current used in sleep mode. To use the feature set the define for pin BATVREADON in 'Settings.h' to the pin used. If not using the feature set the pin number to -1. + +#### 47\_DeepSleep\_Timed\_Wakeup\_ESP32             (Hardware_Checks\ESP32 folder) + +This program flashes a LED connected to the pin defined by LED1, and puts the ESP32 to deep_sleep for a period determined by the TIME_TO_SLEEP variable (in seconds). + +The program also has the option of using a logic pin to control the power to the lora and SD card devices, which can save power in sleep mode. If the hardware is fitted to your board these devices are powered on by setting the VCCPOWER pin low. If your board does not have this feature set VCCPOWER to -1. + +Current in deep_sleep for a bare bones ESP32 with regulator and no other devices was 27uA. + + +#### 48\_DS18B20\_Test             (Hardware_Checks folder) + + +The program reads a single DS18B20 temperature sensor and prints the result to the serial monitor. + +The program also has the option of using a logic pin to control the power to the lora and SD card devices, which can save power in sleep mode. If the hardware is fitted to your board then these devices are assumed to be powered on by setting the VCCPOWER pin low. If your board does not have this feature set VCCPOWER to -1. + + + +#### 50\_LightSleep\_Timed\_Wakeup\_ESP32             (ESP32 folder) + +The program flashes a LED connected to the pin defined by LED1, and puts the ESP32 to light_sleep for a period determined by TIME_TO_SLEEP (in seconds). + +The program also has the option of using a logic pin to control the power to the lora device, SD card and DS18B20 devices, which can save power in sleep mode. If the hardware is fitted to your board these devices are powered on by setting the VCCPOWER pin low. If your board does not have this feature set VCCPOWER to -1. + +#### 51\_DeepSleep\_Switch\_Wakeup\_ESP32             (ESP32 folder) + +The program flashes a LED connected to the pin defined by LED1, and puts the ESP32 to deep_sleep. Pressing BOOT switch should wake up the ESP32 from sleep. + +Only the specific RTC IO pins can be used as a source for external wakeup. +These are pins: 0,2,4,12-15,25-27,32-39. + +Current in deep sleep for a bare bones ESP32 with regulator and no other devices was 27uA. + +#### 52\_FLRC\_Transmitter             (SX128X\Examples\Basics folder) + +This is a test transmitter for the Fast Long Range Communication (FLRC) mode introduced in the SX128X devices. A packet containing ASCII text is sent according to the frequency and FLRC settings specified in the 'Settings.h' file. The pins to access the SX128X device need to be defined +in the 'Settings.h' file also. + +The details of the packet sent and any errors are shown on the Serial Monitor, together with the transmit power used, the packet length and the CRC of the packet. The matching receive program, '53_FLRC_Receiver' can be used to check the packets are being sent correctly, the frequency and FLRC settings (in Settings.h) must be the same for the Transmit and Receive program. Sample Serial Monitor output; + +10dBm Packet> {packet contents*} BytesSent,23 CRC,DAAB TransmitTime,54mS PacketsSent,1 + + +#### 53\_FLRC\_Receiver             (SX128X_Examples\Basics folder) + +This is a test receiver for the Fast Long Range Communication (FLRC) mode introduced in the SX128X devices. The program listens for incoming packets using the FLRC settings in the 'Settings.h' file. The pins to access the SX128X device need to be defined in the 'Settings.h' file also. + +There is a printout of the valid packets received, the packet is assumed to be in ASCII printable text, if its not ASCII text characters from 0x20 to 0x7F, expect weird things to happen on the Serial Monitor. The LED will flash for each packet received and the buzzer will sound, if fitted. + +Sample serial monitor output; + +3s Hello World 1234567890*,CRC,DAAB,RSSI,-73dB,Length,23,Packets,1,Errors,0,IRQreg,6 + +If there is a packet error it might look like this, which is showing a CRC error, + +6s PacketError,RSSI,-103dB,Length,119,Packets,3,Errors,1,IRQreg,46,IRQ\_RX\_DONE,IRQ\_SYNCWORD\_VALID,IRQ\_CRC\_ERROR + + +#### 58\_FM\_Tone             (Basics folder) + +Transmits a FM tone using the LoRa device that can be picked up on an FM UHF handheld receiver. The tones are not true FM but the UHF receiver does not know that. + + +#### 59\_Play\_Star\_Wars\_Tune             (Silly folder) + +A silly program really, but does demonstrate that you can shift a carrier generated by the LoRa device in FSK mode fast enough to play audio tones that can be picked up on an FM UHF handheld receiver. The tones are not true FM but the receiver does not know that. + + +#### 60\_LoRa\_Packet\_Logger\_Receiver\_SD             (Diagnostics and Test folder) + + +The program listens for incoming packets using the LoRa settings in the 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + +There is a printout and save to SD card of the valid packets received in HEX format. Thus the program can be used to receive and record non-ASCII packets. The LED will flash for each packet received and the buzzer will sound, if fitted. The measured frequency difference between the frequency used by the transmitter and the frequency used by the receiver is shown. If this frequency difference gets to 25% of the set LoRa bandwidth, packet reception will fail. The displayed error can be reduced by using the 'offset' setting in the 'Settings.h' file. + +There will be a limit to how fast the logger can receive packets, mainly caused by the delay in writing to SD card, so at high packet rates, packets will be lost. + + + +#### 103\_LoRa\_Transmitter\_Detailed\_Setup             (Basics folder) + +This is a program that demonstrates the detailed setup of a LoRa test transmitter. A packet containing ASCII text is sent according to the frequency and LoRa settings specified in the Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + +The details of the packet sent and any errors are shown on the Arduino IDE Serial Monitor, together with the transmit power used, the packet length and the CRC of the packet. The matching receive program, '104\_LoRa\_Receiver' can be used to check the packets are being sent correctly, the frequency and LoRa settings (in Settings.h) must be the same for the transmitter and receiver programs. Sample Serial Monitor output; + +10dBm Packet> Hello World 1234567890* BytesSent,23 CRC,DAAB TransmitTime,64mS PacketsSent,2 + +Serial monitor baud rate is set at 9600 + +#### 104\_LoRa\_Receiver\_Detailed\_Setup             (Basics folder) + +This is a program that demonstrates the detailed setup of a LoRa test receiver. The program listens for incoming packets using the LoRa settings in the 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + +There is a printout on the Arduino IDE Serial Monitor of the valid packets received, the packet is assumed to be in ASCII printable text, if it's not ASCII text characters from 0x20 to 0x7F, expect weird things to happen on the Serial Monitor. The LED will flash for each packet received and the buzzer will sound, if fitted. Sample serial monitor output; + +7s Hello World 1234567890*,CRC,DAAB,RSSI,-52dBm,SNR,9dB,Length,23,Packets,5,Errors,0,IRQreg,50 + +If there is a packet error it might look like this, which is showing a CRC error, + +968s PacketError,RSSI,-87dBm,SNR,-11dB,Length,23,Packets,613,Errors,2,IRQreg,70,IRQ\_HEADER\_VALID,IRQ\_CRC\_ERROR,IRQ\_RX\_DONE + +Serial monitor baud rate is set at 9600. + diff --git a/examples/SX128x_examples/Basics/103_LoRa_Transmitter_Detailed_Setup/103_LoRa_Transmitter_Detailed_Setup.ino b/examples/SX128x_examples/Basics/103_LoRa_Transmitter_Detailed_Setup/103_LoRa_Transmitter_Detailed_Setup.ino new file mode 100644 index 0000000..5e2fcb8 --- /dev/null +++ b/examples/SX128x_examples/Basics/103_LoRa_Transmitter_Detailed_Setup/103_LoRa_Transmitter_Detailed_Setup.ino @@ -0,0 +1,175 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a program that demonstrates the detailed setup of a LoRa test transmitter. + A packet containing ASCII text is sent according to the frequency and LoRa settings specified in the + 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + The details of the packet sent and any errors are shown on the Arduino IDE Serial Monitor, together with + the transmit power used, the packet length and the CRC of the packet. The matching receive program, + '104_LoRa_Receiver' can be used to check the packets are being sent correctly, the frequency and LoRa + settings (in Settings.h) must be the same for the transmitter and receiver programs. Sample Serial + Monitor output; + + 10dBm Packet> Hello World 1234567890* BytesSent,23 CRC,DAAB TransmitTime,64mS PacketsSent,2 + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint8_t TXPacketL; +uint32_t TXPacketCount, startmS, endmS; + +uint8_t buff[] = "Hello World 1234567890"; + +void loop() +{ + Serial.print(TXpower); //print the transmit power defined + Serial.print(F("dBm ")); + Serial.print(F("Packet> ")); + Serial.flush(); + + TXPacketL = sizeof(buff); //set TXPacketL to length of array + buff[TXPacketL - 1] = '*'; //replace null character at buffer end so its visible on receiver + + LT.printASCIIPacket(buff, TXPacketL); //print the buffer (the sent packet) as ASCII + + digitalWrite(LED1, HIGH); + startmS = millis(); //start transmit timer + if (LT.transmit(buff, TXPacketL, 10000, TXpower, WAIT_TX)) //will return packet length sent if OK, otherwise 0 if transmit error + { + endmS = millis(); //packet sent, note end time + TXPacketCount++; + packet_is_OK(); + } + else + { + packet_is_Error(); //transmit packet returned 0, there was an error + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(packet_delay); //have a delay between packets +} + + +void packet_is_OK() +{ + //if here packet has been sent OK + uint16_t localCRC; + + Serial.print(F(" BytesSent,")); + Serial.print(TXPacketL); //print transmitted packet length + localCRC = LT.CRCCCITT(buff, TXPacketL, 0xFFFF); + Serial.print(F(" CRC,")); + Serial.print(localCRC, HEX); //print CRC of transmitted packet + Serial.print(F(" TransmitTime,")); + Serial.print(endmS - startmS); //print transmit time of packet + Serial.print(F("mS")); + Serial.print(F(" PacketsSent,")); + Serial.print(TXPacketCount); //print total of packets sent OK +} + + +void packet_is_Error() +{ + //if here there was an error transmitting packet + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the the interrupt register + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); //print IRQ status + LT.printIrqStatus(); //prints the text of which IRQs set +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("103_LoRa_Transmitter_Detailed_Setup Starting")); + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, DIO2, DIO3, RX_EN, TX_EN, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //The function call list below shows the complete setup for the LoRa device using the information defined in the + //Settings.h file. + //The 'Setup LoRa device' list below can be replaced with a single function call; + //LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + //*************************************************************************************************** + //Setup LoRa device + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setRegulatorMode(USE_LDO); + LT.setPacketType(PACKET_TYPE_LORA); + LT.setRfFrequency(Frequency, Offset); + LT.setBufferBaseAddress(0, 0); + LT.setModulationParams(SpreadingFactor, Bandwidth, CodeRate); + LT.setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 255, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); + LT.setHighSensitivity(); + //*************************************************************************************************** + + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers, normally 0x900 to 0x9FF + Serial.println(); + Serial.println(); + + Serial.print(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/103_LoRa_Transmitter_Detailed_Setup/Settings.h b/examples/SX128x_examples/Basics/103_LoRa_Transmitter_Detailed_Setup/Settings.h new file mode 100644 index 0000000..c73ec3e --- /dev/null +++ b/examples/SX128x_examples/Basics/103_LoRa_Transmitter_Detailed_Setup/Settings.h @@ -0,0 +1,42 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 02/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. Some pins such as DIO2, +//DIO3, BUZZER are not used by this sketch so they do not need to be connected and +//should be set to -1. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 +#define DIO2 -1 //not used +#define DIO3 -1 //not used +#define RX_EN -1 //pin for RX enable, used on some SX1280 devices, set to -1 if not used +#define TX_EN -1 //pin for TX enable, used on some SX1280 devices, set to -1 if not used + +#define BUZZER -1 //connect a buzzer here if wanted + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const int8_t TXpower = 10; //LoRa transmit power in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets + diff --git a/examples/SX128x_examples/Basics/104_LoRa_Receiver_Detailed_Setup/104_LoRa_Receiver_Detailed_Setup.ino b/examples/SX128x_examples/Basics/104_LoRa_Receiver_Detailed_Setup/104_LoRa_Receiver_Detailed_Setup.ino new file mode 100644 index 0000000..26c00e4 --- /dev/null +++ b/examples/SX128x_examples/Basics/104_LoRa_Receiver_Detailed_Setup/104_LoRa_Receiver_Detailed_Setup.ino @@ -0,0 +1,240 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program that demonstrates the detailed setup of a LoRa test receiver. + The program listens for incoming packets using the lora settings in the 'Settings.h' file. The pins + to access the lora device need to be defined in the 'Settings.h' file also. + + There is a printout on the Arduino IDE Serial Monitor of the valid packets received, the packet is + assumed to be in ASCII printable text, if it's not ASCII text characters from 0x20 to 0x7F, expect + weird things to happen on the Serial Monitor. The LED will flash for each packet received and the + buzzer will sound, if fitted. + + Sample serial monitor output; + + 7s Hello World 1234567890*,CRC,DAAB,RSSI,-52dBm,SNR,9dB,Length,23,Packets,5,Errors,0,IRQreg,50 + + If there is a packet error it might look like this, which is showing a CRC error, + + 968s PacketError,RSSI,-87dBm,SNR,-11dB,Length,23,Packets,613,Errors,2,IRQreg,70,IRQ_HEADER_VALID,IRQ_CRC_ERROR,IRQ_RX_DONE + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio (SNR) of received packet + + +void loop() +{ + RXPacketL = LT.receive(RXBUFFER, RXBUFFER_SIZE, 60000, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + digitalWrite(LED1, HIGH); //something has happened + + if (BUZZER > 0) //turn buzzer on + { + digitalWrite(BUZZER, HIGH); + } + + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + PacketSNR = LT.readPacketSNR(); //read the received SNR value + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL is 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + if (BUZZER > 0) + { + digitalWrite(BUZZER, LOW); //buzzer off + } + + digitalWrite(LED1, LOW); //LED off + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t IRQStatus, localCRC; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + RXpacketCount++; + + printElapsedTime(); //print elapsed time to Serial Monitor + Serial.print(F(" ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL); //print the packet as ASCII characters + + localCRC = LT.CRCCCITT(RXBUFFER, RXPacketL, 0xFFFF); //calculate the CRC, this is the external CRC calculation of the RXBUFFER + Serial.print(F(",CRC,")); //contents, not the LoRa device internal CRC + Serial.print(localCRC, HEX); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + printElapsedTime(); //print elapsed time to Serial Monitor + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout")); + } + else + { + errors++; + Serial.print(F(" PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the device packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); //print the names of the IRQ registers set + } + + delay(250); //gives a longer buzzer and LED flash for error + +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("104_LoRa_Receiver_Detailed_Setup Starting")); + Serial.println(); + + if (BUZZER > 0) + { + pinMode(BUZZER, OUTPUT); + digitalWrite(BUZZER, HIGH); + delay(50); + digitalWrite(BUZZER, LOW); + } + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in the library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, DIO2, DIO3, RX_EN, TX_EN, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //The function call list below shows the complete setup for the LoRa device using the information defined in the + //Settings.h file. + //The 'Setup LoRa device' list below can be replaced with a single function call; + //LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + //*************************************************************************************************** + //Setup LoRa device + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setRegulatorMode(USE_LDO); + LT.setPacketType(PACKET_TYPE_LORA); + LT.setRfFrequency(Frequency, Offset); + LT.setBufferBaseAddress(0, 0); + LT.setModulationParams(SpreadingFactor, Bandwidth, CodeRate); + LT.setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 255, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); + LT.setHighSensitivity(); + //*************************************************************************************************** + + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operting settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers, normally 0x900 to 0x9FF + Serial.println(); + Serial.println(); + + Serial.print(F("Receiver ready - RXBUFFER_SIZE ")); + Serial.println(RXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/104_LoRa_Receiver_Detailed_Setup/Settings.h b/examples/SX128x_examples/Basics/104_LoRa_Receiver_Detailed_Setup/Settings.h new file mode 100644 index 0000000..3f4205d --- /dev/null +++ b/examples/SX128x_examples/Basics/104_LoRa_Receiver_Detailed_Setup/Settings.h @@ -0,0 +1,44 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. Some pins such as DIO2, +//DIO3, BUZZER are not used by this sketch so they do not need to be connected and +//should be set to -1. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for RX and TX done +#define DIO2 -1 //DIO2 pin on LoRa device, normally not used so set to -1 +#define DIO3 -1 //DIO3 pin on LoRa device, normally not used so set to -1 +#define RX_EN -1 //pin for RX enable, used on some SX128X devices, set to -1 if not used +#define TX_EN -1 //pin for TX enable, used on some SX128X devices, set to -1 if not used +#define LED1 8 //on board LED, high for on +#define BUZZER -1 //pin for buzzer, set to -1 if not used + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const uint16_t packet_delay = 1000; //mS delay between packets + +#define RXBUFFER_SIZE 32 //RX buffer size + + + diff --git a/examples/SX128x_examples/Basics/14_LoRa_Structure_TX/14_LoRa_Structure_TX.ino b/examples/SX128x_examples/Basics/14_LoRa_Structure_TX/14_LoRa_Structure_TX.ino new file mode 100644 index 0000000..7bd8c71 --- /dev/null +++ b/examples/SX128x_examples/Basics/14_LoRa_Structure_TX/14_LoRa_Structure_TX.ino @@ -0,0 +1,144 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 08/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program demonstrates the transmitting of a structure as a LoRa packet. The + contents of the structure are the same as in the '8_LoRa_LowMemory_TX' program. The packet sent is + typical of what might be sent from a GPS tracker. + + The structure type is defined as trackerPacket and an instance called location1 is created. The struture + which includes a character array (text) is filled with values and transmitted. + + The matching receiving program '15_LoRa_RX_Structure' can be used to receive and display the packet, + though the program '9_LoRa_LowMemory_RX' should receive it as well, since the contents are the same. + + Note that the structure definition and variable order (including the buffer size) used in the transmitter + need to match those used in the receiver. + + The contents of the packet transmitted should be; + + "tracker1" (buffer) - trackerID + 1+ (uint32_t) - packet count + 51.23456 (float) - latitude + -3.12345 (float) - longitude + 199 (uint16_t) - altitude + 8 (uint8_t) - number of satellites + 3999 (uint16_t) - battery voltage + -9 (int8_t) - temperature + + Good luck. + + Serial monitor baud rate is set at 9600. + +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint32_t TXpacketCount = 1; +uint32_t startmS, endmS; + +struct trackerPacket +{ + uint8_t trackerID[13]; + uint32_t txcount; + float latitude; + float longitude; + uint16_t altitude; + uint8_t satellites; + uint16_t voltage; + int8_t temperature; +} __attribute__((packed, aligned(1))); //remove structure padding so there is compatibility between 8bit and 32bit Arduinos + +struct trackerPacket location1; //define an instance called location1 of the structure trackerPacket + + +void loop() +{ + + //fill the defined structure with values + uint8_t buff[] = "tracker1"; //create the contents to be of location1.trackerID + memcpy (&location1.trackerID, &buff, sizeof(buff)); //copy the contents of buff[] into the structure + location1.txcount = TXpacketCount; + location1.latitude = 51.23456; + location1.longitude = -3.12345; + location1.altitude = 199; + location1.satellites = 8; + location1.voltage = 3999; + location1.temperature = -9; + + digitalWrite(LED1, HIGH); + startmS = millis(); + + if (LT.transmit((uint8_t *) &location1, sizeof(location1), 0, TXpower, WAIT_TX)) //will return packet length sent if OK, otherwise 0 + { + endmS = millis(); + digitalWrite(LED1, LOW); + TXpacketCount++; + Serial.print(TXpacketCount); + Serial.print(F(" ")); + Serial.print(sizeof(location1)); + Serial.print(F(" Bytes Sent")); + Serial.print(F(" ")); + Serial.print(endmS - startmS); + Serial.print(F("mS")); + } + else + { + Serial.print(F("Send Error - IRQreg,")); + Serial.print(LT.readIrqStatus(), HEX); + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(packet_delay); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/14_LoRa_Structure_TX/Settings.h b/examples/SX128x_examples/Basics/14_LoRa_Structure_TX/Settings.h new file mode 100644 index 0000000..e49838d --- /dev/null +++ b/examples/SX128x_examples/Basics/14_LoRa_Structure_TX/Settings.h @@ -0,0 +1,30 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 08/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +#define TXpower 10 //power for transmissions in dBm + +#define packet_delay 1000 //mS delay between packets diff --git a/examples/SX128x_examples/Basics/15_LoRa_Structure_RX/15_LoRa_Structure_RX.ino b/examples/SX128x_examples/Basics/15_LoRa_Structure_RX/15_LoRa_Structure_RX.ino new file mode 100644 index 0000000..f6ea2bd --- /dev/null +++ b/examples/SX128x_examples/Basics/15_LoRa_Structure_RX/15_LoRa_Structure_RX.ino @@ -0,0 +1,193 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 08/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program demonstrates the receiving of a structure as a LoRa packet. The packet + sent is typical of what might be sent from a GPS tracker. + + The structure type is defined as trackerPacket and an instance called location1 is created. The structure + includes a character array (text). + + The matching receiving program is '15_LoRa_RX_Structure' can be used to receive and display the packet, + though the program '9_LoRa_LowMemory_RX' should receive it as well, since the packet contents are the same. + + Not that the structure definition and variable order (including the buffer size) used in the transmitter + need to match those used in the receiver. Good luck. + + The contents of the packet received, and printed to serial monitor, should be; + + "tracker1" (buffer) - trackerID + 1+ (uint32_t) - packet count + 51.23456 (float) - latitude + -3.12345 (float) - longitude + 199 (uint16_t) - altitude + 8 (uint8_t) - number of satellites + 3999 (uint16_t) - battery voltage + -9 (int8_t) - temperature + + Serial monitor baud rate is set at 9600. + +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint8_t RXPacketL; //stores length of packet received +uint32_t RXpacketCount; //count of received packets +int16_t PacketRSSI; //RSSI of received packet +int8_t PacketSNR; //signal to noise ratio of received packet +uint32_t errors; //count of packet errors + + +struct trackerPacket +{ + uint8_t trackerID[13]; + uint32_t txcount; + float latitude; + float longitude; + uint16_t altitude; + uint8_t satellites; + uint16_t voltage; + int8_t temperature; +} __attribute__((packed, aligned(1))); //remove structure padding so there is compatibility between 8bit and 32bit Arduinos + +struct trackerPacket location1; //define an instance called location1 of the structure trackerPacket + + +void loop() +{ + RXPacketL = LT.receive( (uint8_t *) &location1, sizeof(location1), 0, WAIT_RX); //wait for a packet to arrive with no timeout + + digitalWrite(LED1, HIGH); //something has happened, what I wonder ? + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + Serial.println(); +} + + +void printlocation1() +{ + uint8_t buff[13]; //define a buffer to receive a copy from the structure + memcpy (&buff, &location1.trackerID, sizeof(buff)); //copy the contents of buffer in struture to buff[] + + //now print the contents of the structure + Serial.print((char*) buff); //cast to a char type for printing + Serial.print(F(",")); + Serial.print(location1.txcount); + Serial.print(F(",")); + Serial.print(location1.latitude, 5); + Serial.print(F(",")); + Serial.print(location1.longitude, 5); + Serial.print(F(",")); + Serial.print(location1.altitude); + Serial.print(F("m,")); + Serial.print(location1.satellites); + Serial.print(F("sats,")); + Serial.print(location1.voltage); + Serial.print(F("mV,")); + Serial.print(location1.temperature); + Serial.print(F("c ")); +} + + +void packet_is_OK() +{ + RXpacketCount++; + Serial.print(RXpacketCount); + Serial.print(F(" ")); + printlocation1(); + printpacketDetails(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F("RXTimeout")); + } + else + { + errors++; + Serial.print(F("PacketError")); + printpacketDetails(); + Serial.print(F("IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + +void printpacketDetails() +{ + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup(void) +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.print(F("Receiver ready")); + Serial.println(); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/15_LoRa_Structure_RX/Settings.h b/examples/SX128x_examples/Basics/15_LoRa_Structure_RX/Settings.h new file mode 100644 index 0000000..fce86d4 --- /dev/null +++ b/examples/SX128x_examples/Basics/15_LoRa_Structure_RX/Settings.h @@ -0,0 +1,30 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +#define TXpower 10 //power for transmissions in dBm + +#define packet_delay 1000 //mS delay between packets diff --git a/examples/SX128x_examples/Basics/1_LED_Blink/1_LED_Blink.ino b/examples/SX128x_examples/Basics/1_LED_Blink/1_LED_Blink.ino new file mode 100644 index 0000000..4f30bad --- /dev/null +++ b/examples/SX128x_examples/Basics/1_LED_Blink/1_LED_Blink.ino @@ -0,0 +1,61 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/02/20 + + This programs is supplied as is, it is up to the user of the program to decide if the programs are + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program blinks an LED connected the pin number defined below. The pin 13 LED, + fitted to some Arduinos is blinked as well. The blinks should be close to one per second. messages are + sent to the Serial Monitor also. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#define LED1 8 //pin number for LED, set logic level high for on + +uint16_t seconds; //used to display time elapsed on Serial Monitor + +void loop() +{ + Serial.print(seconds); + Serial.println(F(" Seconds")); //this message should print on console at close to once per second + seconds++; + digitalWrite(LED1, HIGH); + digitalWrite(13, HIGH); + delay(100); + digitalWrite(LED1, LOW); + digitalWrite(13, LOW); + delay(890); //should give approx 1 second flash +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + //general purpose routine for flashing LED as indicator + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); //LED on + digitalWrite(13, HIGH); //Arduino board LED on + delay(delaymS); + digitalWrite(LED1, LOW); //LED off + digitalWrite(13, LOW); //Arduino board LED off + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + pinMode(13, OUTPUT); //setup pin as output for some Arduino boards that include an LED on pin 13 + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + + Serial.println(F("1_LED_Blink Starting")); +} diff --git a/examples/SX128x_examples/Basics/2_Register_Test/2_Register_Test.ino b/examples/SX128x_examples/Basics/2_Register_Test/2_Register_Test.ino new file mode 100644 index 0000000..0d67740 --- /dev/null +++ b/examples/SX128x_examples/Basics/2_Register_Test/2_Register_Test.ino @@ -0,0 +1,358 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 11/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program is stand alone, it is not necessary to install the SX12XX-LoRa library + to use it. This test program is for the SX128X LoRa devices. + + The program checks that a SX128X LoRa device can be accessed by doing a test register write and read. + If there is no device found a message is printed on the serial monitor. The contents of the registers + from 0x00 to 0x7F are printed, there is a copy of a typical printout below. Note that the read back + changed frequency may be slightly different to the programmed frequency, there is a rounding error due + to the use of floats to calculate the frequency. + + The Arduino pin numbers that the NSS and NRESET pins on the LoRa device are connected to must be + specified in the hardware definitions section below. The LoRa device type in use, SX1280 or SX1281 + must be specified also. + + Typical printout; + + 2_Register_Test Starting + Reset device + LoRa Device found + Reset device + Registers at reset + Reg 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0x900 80 0C 7B 02 20 FA C0 00 00 80 00 00 00 00 00 FF + 0x910 FF FF 00 00 00 19 00 00 00 19 87 65 43 21 7F FF + 0x920 FF FF FF 0C 70 37 0A 50 D0 80 00 C0 5F D2 8F 0A + 0x930 00 C0 00 00 00 24 00 21 28 B0 30 09 1A 59 70 08 + 0x940 58 0B 32 0A 14 24 6A 96 00 18 00 00 00 00 00 00 + 0x950 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 0x960 00 00 00 00 00 00 00 00 00 00 FF FF FF FF FF FF + 0x970 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 04 + 0x980 00 0B 18 70 00 00 00 4C 00 F0 64 00 00 00 00 00 + 0x990 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 0x9A0 00 08 EC B8 9D 8A E6 66 06 00 00 00 00 00 00 00 + 0x9B0 00 08 EC B8 9D 8A E6 66 06 00 00 00 00 00 00 00 + 0x9C0 00 16 00 3F E8 01 FF FF FF FF 5E 4D 25 10 55 55 + 0x9D0 55 55 55 55 55 55 55 55 55 55 55 55 55 00 00 00 + 0x9E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 0x9F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + + Frequency at reset 2495996672hz + Change Frequency to 2445000000hz + Frequency now 2444999936hz + + Reg 0 1 2 3 4 5 6 7 8 9 A B C D E F + 0x900 80 0C 7B 02 20 FA BC 13 C1 80 00 00 00 00 00 61 + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +const uint16_t REG_RFFrequency23_16 = 0x906; +const uint16_t REG_RFFrequency15_8 = 0x907; +const uint16_t REG_RFFrequency7_0 = 0x908; +const uint8_t RADIO_WRITE_REGISTER = 0x18; +const uint8_t RADIO_READ_REGISTER = 0x19; +const uint8_t RADIO_SET_RFFREQUENCY = 0x86; //commnad to change frequency +const uint8_t RADIO_SET_PACKETTYPE = 0x8A; //commnad to set packet mode +const float FREQ_STEP = 198.364; +const uint8_t PACKET_TYPE_LORA = 0x01; + +//********* Setup hardware definitions here ! ***************** + +//These are the pin definitions for one of the Tracker boards, be sure to change them to match your +//own setup. You will also need to connect up the pins for the SPI bus, which on an Arduino Pro Mini are +//SCK pin 13, MISO pin 12, and MOSI pin 11. + +#define NSS 10 //SX128X device select +#define NRESET 9 //SX128X reset pin +#define RFBUSY 7 //SX128X busy pin +#define LED1 8 //for on board LED, put high for on + +//**************************************************************/ + +#include + +uint8_t saveddevice; + + +void setup() +{ + Serial.begin(9600); + Serial.println(F("2_Register_Test Starting")); + + SPI.begin(); + SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //The begin function setups the hardware pins used by device and then checks if device is found + //the DIO1 pin is not used in this example + + if (begin(NSS, NRESET, RFBUSY, 0)) + { + Serial.println(F("Device found")); + } + else + { + Serial.println(F("No device responding")); + } +} + + +void loop() +{ + uint32_t frequency; + resetDevice(); //reset the device + Serial.println(F("Registers at reset")); //show the all registers following a reset + printRegisters(0x0900, 0x09FF); + Serial.println(); + Serial.println(); + + frequency = getFreqInt(); //read the set frequency following a reset + Serial.print(F(" Frequency at reset ")); + Serial.print(frequency); + Serial.println(F("hz")); + + Serial.print(F("Change Frequency to 2445000000hz")); + setPacketType(PACKET_TYPE_LORA); //this is needed to ensure frequency change is reflected in register print + setRfFrequency(2445000000, 0); //change the frequency to 2445000000hertz + + frequency = getFreqInt(); //read back the changed frequency + Serial.println(); + Serial.print(F(" Frequency now ")); + Serial.print(frequency); //print the changed frequency, did the write work (allow for rounding errors) ? + Serial.println(F("hz")); + Serial.println(); + printRegisters(0x0900, 0x090F); //show the registers after frequency change + Serial.println(); + Serial.println(); + delay(5000); +} + + +void readRegisters(uint16_t address, uint8_t *buffer, uint16_t size) +{ + uint16_t index; + uint8_t addr_l, addr_h; + + addr_h = address >> 8; + addr_l = address & 0x00FF; + checkBusy(); + + digitalWrite(NSS, LOW); + SPI.transfer(RADIO_READ_REGISTER); + SPI.transfer(addr_h); //MSB + SPI.transfer(addr_l); //LSB + SPI.transfer(0xFF); + for (index = 0; index < size; index++) + { + *(buffer + index) = SPI.transfer(0xFF); + } + + digitalWrite(NSS, HIGH); + checkBusy(); +} + + +uint8_t readRegister(uint16_t address) +{ + uint8_t data; + + readRegisters(address, &data, 1); + return data; +} + + +void writeRegisters(uint16_t address, uint8_t *buffer, uint16_t size) +{ + uint8_t addr_l, addr_h; + uint8_t i; + + addr_l = address & 0xff; + addr_h = address >> 8; + checkBusy(); + + digitalWrite(NSS, LOW); + SPI.transfer(RADIO_WRITE_REGISTER); + SPI.transfer(addr_h); //MSB + SPI.transfer(addr_l); //LSB + + for (i = 0; i < size; i++) + { + SPI.transfer(buffer[i]); + } + + digitalWrite(NSS, HIGH); + + checkBusy(); +} + + +void writeRegister(uint16_t address, uint8_t value) +{ + writeRegisters(address, &value, 1 ); +} + + +uint32_t getFreqInt() +{ + //get the current set device frequency, return as long integer + uint8_t Msb, Mid, Lsb; + uint32_t uinttemp; + float floattemp; + Msb = readRegister(REG_RFFrequency23_16); + Mid = readRegister(REG_RFFrequency15_8); + Lsb = readRegister(REG_RFFrequency7_0); + floattemp = ((Msb * 0x10000ul) + (Mid * 0x100ul) + Lsb); + floattemp = ((floattemp * FREQ_STEP) / 1000000ul); + uinttemp = (uint32_t)(floattemp * 1000000); + return uinttemp; +} + + +void printRegisters(uint16_t Start, uint16_t End) +{ + //prints the contents of SX128x registers to serial monitor + + uint16_t Loopv1, Loopv2, RegData; + + Serial.print(F("Reg 0 1 2 3 4 5 6 7 8 9 A B C D E F")); + Serial.println(); + + for (Loopv1 = Start; Loopv1 <= End;) //32 lines + { + Serial.print(F("0x")); + Serial.print((Loopv1), HEX); //print the register number + Serial.print(F(" ")); + for (Loopv2 = 0; Loopv2 <= 15; Loopv2++) + { + RegData = readRegister(Loopv1); + if (RegData < 0x10) + { + Serial.print(F("0")); + } + Serial.print(RegData, HEX); //print the register number + Serial.print(F(" ")); + Loopv1++; + } + Serial.println(); + } +} + + +void setRfFrequency(uint32_t frequency, int32_t offset) +{ + frequency = frequency + offset; + uint8_t buffer[3]; + uint32_t freqtemp = 0; + freqtemp = ( uint32_t )( (float) frequency / (float) FREQ_STEP); + buffer[0] = ( uint8_t )( ( freqtemp >> 16 ) & 0xFF ); + buffer[1] = ( uint8_t )( ( freqtemp >> 8 ) & 0xFF ); + buffer[2] = ( uint8_t )( freqtemp & 0xFF ); + writeCommand(RADIO_SET_RFFREQUENCY, buffer, 3); + writeCommand(RADIO_SET_RFFREQUENCY, buffer, 3); +} + + +void checkBusy() +{ + uint8_t busy_timeout_cnt; + busy_timeout_cnt = 0; + + while (digitalRead(RFBUSY)) + { + delay(1); + busy_timeout_cnt++; + + if (busy_timeout_cnt > 10) //wait 10mS for busy to complete + { + busy_timeout_cnt = 0; + Serial.println(F("ERROR - Busy Timeout!")); + break; + } + } +} + + +void resetDevice() +{ + Serial.println(F("Reset device")); + delay(10); + digitalWrite(NRESET, LOW); + delay(2); + digitalWrite(NRESET, HIGH); + delay(25); + checkBusy(); +} + + + +bool begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, uint8_t device) +{ + saveddevice = device; + + pinMode(pinNSS, OUTPUT); + digitalWrite(pinNSS, HIGH); + pinMode(pinNRESET, OUTPUT); + digitalWrite(pinNRESET, HIGH); + pinMode(pinRFBUSY, INPUT); + + resetDevice(); + + if (checkDevice()) + { + return true; + } + + return false; +} + + +bool checkDevice() +{ + //check there is a device out there, writes a register and reads back + + uint8_t Regdata1, Regdata2; + Regdata1 = readRegister(0x0908); //low byte of frequency setting + writeRegister(0x0908, (Regdata1 + 1)); + Regdata2 = readRegister(0x0908); //read changed value back + writeRegister(0x0908, Regdata1); //restore register to original value + + if (Regdata2 == (Regdata1 + 1)) + { + return true; + } + else + { + return false; + } +} + + +void writeCommand(uint8_t Opcode, uint8_t *buffer, uint16_t size) +{ + uint8_t index; + checkBusy(); + + digitalWrite(NSS, LOW); + SPI.transfer(Opcode); + + for (index = 0; index < size; index++) + { + SPI.transfer(buffer[index]); + //Serial.println(buffer[index], HEX); + } + digitalWrite(NSS, HIGH); + + checkBusy(); +} + +void setPacketType(uint8_t packettype) +{ + writeCommand(RADIO_SET_PACKETTYPE, &packettype, 1); +} diff --git a/examples/SX128x_examples/Basics/3_LoRa_Transmitter/3_LoRa_Transmitter.ino b/examples/SX128x_examples/Basics/3_LoRa_Transmitter/3_LoRa_Transmitter.ino new file mode 100644 index 0000000..4f40dda --- /dev/null +++ b/examples/SX128x_examples/Basics/3_LoRa_Transmitter/3_LoRa_Transmitter.ino @@ -0,0 +1,174 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a simple LoRa test transmitter. A packet containing ASCII text is sent + according to the frequency and LoRa settings specified in the 'Settings.h' file. The pins to access + the lora device need to be defined in the 'Settings.h' file also. + + The details of the packet sent and any errors are shown on the Serial Monitor, together with the transmit + power used, the packet length and the CRC of the packet. The matching receive program, '4_LoRa_Receive' + can be used to check the packets are being sent correctly, the frequency and LoRa settings (in Settings.h) + must be the same for the Transmit and Receive program. Sample Serial Monitor output; + + 10dBm Packet> {packet contents*} BytesSent,23 CRC,DAAB TransmitTime,54mS PacketsSent,1 + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include //the SX128X device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint8_t TXPacketL; +uint32_t TXPacketCount, startmS, endmS; + +uint8_t buff[] = "Hello World 1234567890"; + +void loop() +{ + Serial.print(TXpower); //print the transmit power defined + Serial.print(F("dBm ")); + Serial.print(F("Packet> ")); + Serial.flush(); + + TXPacketL = sizeof(buff); //set TXPacketL to length of array + buff[TXPacketL - 1] = '*'; //replace null character at buffer end so its visible on reciver + + LT.printASCIIPacket(buff, TXPacketL); //print the buffer (the sent packet) as ASCII + + digitalWrite(LED1, HIGH); + startmS = millis(); //start transmit timer + if (LT.transmit(buff, TXPacketL, 10000, TXpower, WAIT_TX)) //will return packet length sent if OK, otherwise 0 if transmit, timeout 10 seconds + { + endmS = millis(); //packet sent, note end time + TXPacketCount++; + packet_is_OK(); + } + else + { + packet_is_Error(); //transmit packet returned 0, there was an error + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(packet_delay); //have a delay between packets +} + + +void packet_is_OK() +{ + //if here packet has been sent OK + uint16_t localCRC; + + Serial.print(F(" BytesSent,")); + Serial.print(TXPacketL); //print transmitted packet length + localCRC = LT.CRCCCITT(buff, TXPacketL, 0xFFFF); + Serial.print(F(" CRC,")); + Serial.print(localCRC, HEX); //print CRC of sent packet + Serial.print(F(" TransmitTime,")); + Serial.print(endmS - startmS); //print transmit time of packet + Serial.print(F("mS")); + Serial.print(F(" PacketsSent,")); + Serial.print(TXPacketCount); //print total of packets sent OK +} + + +void packet_is_Error() +{ + //if here there was an error transmitting packet + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the the interrupt register + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); //print IRQ status + LT.printIrqStatus(); //prints the text of which IRQs set +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("3_LoRa_Transmitter Starting")); + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, RX_EN, TX_EN, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //The function call list below shows the complete setup for the LoRa device using the information defined in the + //Settings.h file. + //The 'Setup LoRa device' list below can be replaced with a single function call; + //LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + //*************************************************************************************************** + //Setup LoRa device + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setRegulatorMode(USE_LDO); + LT.setPacketType(PACKET_TYPE_LORA); + LT.setRfFrequency(Frequency, Offset); + LT.setBufferBaseAddress(0, 0); + LT.setModulationParams(SpreadingFactor, Bandwidth, CodeRate); + LT.setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 255, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); + LT.setHighSensitivity(); + //*************************************************************************************************** + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers + Serial.println(); + Serial.println(); + + Serial.print(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/3_LoRa_Transmitter/Settings.h b/examples/SX128x_examples/Basics/3_LoRa_Transmitter/Settings.h new file mode 100644 index 0000000..85e1c3a --- /dev/null +++ b/examples/SX128x_examples/Basics/3_LoRa_Transmitter/Settings.h @@ -0,0 +1,34 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 +#define RX_EN -1 //pin for RX enable, used on some SX1280 devices, set to -1 if not used +#define TX_EN -1 //pin for TX enable, used on some SX1280 devices, set to -1 if not used +#define BUZZER -1 //pin for BUZZER, set to -1 if not used + + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets diff --git a/examples/SX128x_examples/Basics/3_LoRa_TransmitterIRQ/3_LoRa_TransmitterIRQ.ino b/examples/SX128x_examples/Basics/3_LoRa_TransmitterIRQ/3_LoRa_TransmitterIRQ.ino new file mode 100644 index 0000000..47812f5 --- /dev/null +++ b/examples/SX128x_examples/Basics/3_LoRa_TransmitterIRQ/3_LoRa_TransmitterIRQ.ino @@ -0,0 +1,123 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 21/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a minimum setup LoRa test transmitter. A packet containing the ASCII text + "Hello World 1234567890" is sent using the frequency and LoRa settings specified in the LT.setupLoRa() + command. The pins to access the lora device need to be defined at the top of the program also. + + This program does not need the DIO1 pin on the LoRa device connected. + + The details of the packet sent and any errors are shown on the Arduino IDE Serial Monitor, together with + the transmit power used and the packet length. The matching receiver program, '4_LoRa_Receiver' can be used + to check the packets are being sent correctly, the frequency and LoRa settings (in the LT.setupLoRa() + commands) must be the same for the transmitter and receiver programs. Sample Serial Monitor output; + + 10dBm Packet> Hello World 1234567890* BytesSent,23 PacketsSent,6 + + This is a version of example 3_LoRa_Transmitter.ino that does not require the use of the DIO1 pin to + check for transmit done. In addition no NRESET pin is needed either, so its a program for use with a + minimum pin count Arduino. Leave the DIO1 and NRESET pins on the LoRa device not connected. + + For an example of a more detailed configuration for a transmitter, see program 103_LoRa_Transmitter. + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 10 //LoRa transmit power in dBm + +uint8_t TXPacketL; +uint32_t TXPacketCount; + +uint8_t buff[] = "Hello World 1234567890"; //the message to send + + +void loop() +{ + Serial.print(TXpower); //print the transmit power defined + Serial.print(F("dBm ")); + Serial.print(F("Packet> ")); + Serial.flush(); + + TXPacketL = sizeof(buff); //set TXPacketL to length of array + buff[TXPacketL - 1] = '*'; //replace null character at buffer end so its visible on receiver + + LT.printASCIIPacket(buff, TXPacketL); //print the buffer (the sent packet) as ASCII + + if (LT.transmitIRQ(buff, TXPacketL, 10000, TXpower, WAIT_TX)) //will return packet length sent if OK, otherwise 0 if transmit error + { + TXPacketCount++; + packet_is_OK(); + } + else + { + packet_is_Error(); //transmit packet returned 0, there was an error + } + + Serial.println(); + delay(1000); //have a delay between packets +} + + +void packet_is_OK() +{ + //if here packet has been sent OK + Serial.print(F(" BytesSent,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(" PacketsSent,")); + Serial.print(TXPacketCount); //print total of packets sent OK +} + + +void packet_is_Error() +{ + //if here there was an error transmitting packet + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the the interrupt register + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); //print IRQ status + LT.printIrqStatus(); //prints the text of which IRQs set +} + + +void setup() +{ + Serial.begin(9600); + Serial.println(); + Serial.println(F("3_LoRa_TransmitterIRQ Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.print(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/4_LoRa_Receiver/4_LoRa_Receiver.ino b/examples/SX128x_examples/Basics/4_LoRa_Receiver/4_LoRa_Receiver.ino new file mode 100644 index 0000000..ba0b195 --- /dev/null +++ b/examples/SX128x_examples/Basics/4_LoRa_Receiver/4_LoRa_Receiver.ino @@ -0,0 +1,237 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 08/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program listens for incoming packets using the LoRa settings in the 'Settings.h' + file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + There is a printout of the valid packets received, the packet is assumed to be in ASCII printable text, + if its not ASCII text characters from 0x20 to 0x7F, expect weird things to happen on the Serial Monitor. + The LED will flash for each packet received and the buzzer will sound, if fitted. + + Sample serial monitor output; + + 1109s Hello World 1234567890*,CRC,DAAB,RSSI,-61dBm,SNR,9dB,Length,23,Packets,1026,Errors,0,IRQreg,50 + + If there is a packet error it might look like this, which is showing a CRC error, + + 1189s PacketError,RSSI,-111dBm,SNR,-12dB,Length,0,Packets,1126,Errors,1,IRQreg,70,IRQ_HEADER_VALID,IRQ_CRC_ERROR,IRQ_RX_DONE + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + + +void loop() +{ + RXPacketL = LT.receive(RXBUFFER, RXBUFFER_SIZE, 60000, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + digitalWrite(LED1, HIGH); //something has happened + + if (BUZZER > 0) //turn buzzer on + { + digitalWrite(BUZZER, HIGH); + } + + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + PacketSNR = LT.readPacketSNR(); //read the received SNR value + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL == 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + if (BUZZER > 0) + { + digitalWrite(BUZZER, LOW); //buzzer off + } + + digitalWrite(LED1, LOW); //LED off + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t IRQStatus, localCRC; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + RXpacketCount++; + + printElapsedTime(); //print elapsed time to Serial Monitor + Serial.print(F(" ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL); //print the packet as ASCII characters + + localCRC = LT.CRCCCITT(RXBUFFER, RXPacketL, 0xFFFF); //calculate the CRC, this is the external CRC calculation of the RXBUFFER + Serial.print(F(",CRC,")); //contents, not the LoRa device internal CRC + Serial.print(localCRC, HEX); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + printElapsedTime(); //print elapsed time to Serial Monitor + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout")); + } + else + { + errors++; + Serial.print(F(" PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); //print the names of the IRQ registers set + } + + delay(250); //gives a longer buzzer and LED flash for error + +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("4_LoRa_Receiver Starting")); + + if (BUZZER > 0) + { + pinMode(BUZZER, OUTPUT); + digitalWrite(BUZZER, HIGH); + delay(50); + digitalWrite(BUZZER, LOW); + } + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in the library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, RX_EN, TX_EN, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //The function call list below shows the complete setup for the LoRa device using the information defined in the + //Settings.h file. + //The 'Setup LoRa device' list below can be replaced with a single function call; + //LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + //*************************************************************************************************** + //Setup LoRa device + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setRegulatorMode(USE_LDO); + LT.setPacketType(PACKET_TYPE_LORA); + LT.setRfFrequency(Frequency, Offset); + LT.setBufferBaseAddress(0, 0); + LT.setModulationParams(SpreadingFactor, Bandwidth, CodeRate); + LT.setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 255, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); + LT.setHighSensitivity(); + //*************************************************************************************************** + + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operting settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers + Serial.println(); + Serial.println(); + + Serial.print(F("Receiver ready - RXBUFFER_SIZE ")); + Serial.println(RXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/4_LoRa_Receiver/Settings.h b/examples/SX128x_examples/Basics/4_LoRa_Receiver/Settings.h new file mode 100644 index 0000000..72af6ec --- /dev/null +++ b/examples/SX128x_examples/Basics/4_LoRa_Receiver/Settings.h @@ -0,0 +1,35 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 +#define RX_EN -1 //pin for RX enable, used on some SX1280 devices, set to -1 if not used +#define TX_EN -1 //pin for TX enable, used on some SX1280 devices, set to -1 if not used +#define BUZZER -1 //pin for BUZZER, set to -1 if not used + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets + +#define RXBUFFER_SIZE 255 //RX buffer size diff --git a/examples/SX128x_examples/Basics/4_LoRa_ReceiverIRQ/4_LoRa_ReceiverIRQ.ino b/examples/SX128x_examples/Basics/4_LoRa_ReceiverIRQ/4_LoRa_ReceiverIRQ.ino new file mode 100644 index 0000000..1416b69 --- /dev/null +++ b/examples/SX128x_examples/Basics/4_LoRa_ReceiverIRQ/4_LoRa_ReceiverIRQ.ino @@ -0,0 +1,168 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a minimum setup LoRa test receiver. The program listens for incoming packets + using the frequency and LoRa settings in the LT.setupLoRa() command. The pins to access the lora device + need to be defined at the top of the program also. + + This program does not need the DIO1 pin on the LoRa device connected. + + There is a printout on the Arduino IDE serial monitor of the valid packets received, the packet is assumed + to be in ASCII printable text, if it's not ASCII text characters from 0x20 to 0x7F, expect weird things to + happen on the Serial Monitor. Sample serial monitor output; + + 8s Hello World 1234567890*,RSSI,-44dBm,SNR,9dB,Length,23,Packets,7,Errors,0,IRQreg,50 + + If there is a packet error it might look like this, which is showing a CRC error; + + 137s PacketError,RSSI,-89dBm,SNR,-8dB,Length,23,Packets,37,Errors,2,IRQreg,70,IRQ_HEADER_VALID,IRQ_CRC_ERROR,IRQ_RX_DONE + + If there are no packets received in a 10 second period then you should see a message like this; + + 112s RXTimeout + + For an example of a more detailed configuration for a receiver, see program 104_LoRa_Receiver. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +#define RXBUFFER_SIZE 255 //RX buffer size + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio (SNR) of received packet + + +void loop() +{ + RXPacketL = LT.receiveIRQ(RXBUFFER, RXBUFFER_SIZE, 60000, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + PacketSNR = LT.readPacketSNR(); //read the received packets SNR value + + if (RXPacketL == 0) //if the LT.receive() function detects an error RXpacketL is 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t IRQStatus; + + RXpacketCount++; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + printElapsedTime(); //print elapsed time to Serial Monitor + + Serial.print(F(" ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL); //print the packet as ASCII characters + + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + printElapsedTime(); //print elapsed time to Serial Monitor + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout")); + } + else + { + errors++; + Serial.print(F(" PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); //print the names of the IRQ registers set + } +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void setup() +{ + Serial.begin(9600); + Serial.println(); + Serial.println(F("4_LoRa_ReceiverIRQ Starting")); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.print(F("Receiver ready - RXBUFFER_SIZE ")); + Serial.println(RXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/52_FLRC_Transmitter/52_FLRC_Transmitter.ino b/examples/SX128x_examples/Basics/52_FLRC_Transmitter/52_FLRC_Transmitter.ino new file mode 100644 index 0000000..3018965 --- /dev/null +++ b/examples/SX128x_examples/Basics/52_FLRC_Transmitter/52_FLRC_Transmitter.ino @@ -0,0 +1,175 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a test transmitter for the Fast Long Range Communication (FLRC) mode + introduced in the SX128X devices. A packet containing ASCII text is sent according to the frequency and + FLRC settings specified in the 'Settings.h' file. The pins to access the SX128X device need to be defined + in the 'Settings.h' file also. + + The details of the packet sent and any errors are shown on the Serial Monitor, together with the transmit + power used, the packet length and the CRC of the packet. The matching receive program, '53_FLRC_Receiver' + can be used to check the packets are being sent correctly, the frequency and FLRC settings (in Settings.h) + must be the same for the Transmit and Receive program. Sample Serial Monitor output; + + 10dBm Packet> {packet contents*} BytesSent,23 CRC,DAAB TransmitTime,54mS PacketsSent,1 + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include //the SX128X device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint8_t TXPacketL; +uint32_t TXPacketCount, startmS, endmS; + +uint8_t buff[] = "Hello World 1234567890"; + +void loop() +{ + Serial.print(TXpower); //print the transmit power defined + Serial.print(F("dBm ")); + Serial.print(F("Packet> ")); + Serial.flush(); + + TXPacketL = sizeof(buff); //set TXPacketL to length of array + buff[TXPacketL - 1] = '*'; //replace null character at buffer end so its visible on reciver + + LT.printASCIIPacket(buff, TXPacketL); //print the buffer (the sent packet) as ASCII + + digitalWrite(LED1, HIGH); + startmS = millis(); //start transmit timer + + TXPacketL = LT.transmit(buff, TXPacketL, 10000, TXpower, WAIT_TX); //will return 0 if transmit fails, timeout 10 seconds + + if (TXPacketL > 0) + { + endmS = millis(); //packet sent, note end time + TXPacketCount++; + packet_is_OK(); + } + else + { + packet_is_Error(); //transmit packet returned 0, so there was an error + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(packet_delay); //have a delay between packets +} + + +void packet_is_OK() +{ + //if here packet has been sent OK + uint16_t localCRC; + + Serial.print(F(" BytesSent,")); + Serial.print(TXPacketL); //print transmitted packet length + localCRC = LT.CRCCCITT(buff, TXPacketL, 0xFFFF); + Serial.print(F(" CRC,")); + Serial.print(localCRC, HEX); //print CRC of sent packet + Serial.print(F(" TransmitTime,")); + Serial.print(endmS - startmS); //print transmit time of packet + Serial.print(F("mS")); + Serial.print(F(" PacketsSent,")); + Serial.print(TXPacketCount); //print total of packets sent OK +} + + +void packet_is_Error() +{ + //if here there was an error transmitting packet + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the the interrupt register + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); //print IRQ status + LT.printIrqStatus(); //prints the text of which IRQs set +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("52_FLRC_Transmitter Starting")); + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("FLRC Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + delay(1000); + } + else + { + Serial.println(F("No FLRC device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + LT.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + + //The full details of the setupFLRC function call above are listed below + //*************************************************************************************************** + //Setup FLRC + //*************************************************************************************************** + //LT.setMode(MODE_STDBY_RC); + //LT.setRegulatorMode(USE_LDO); + //LT.setPacketType(PACKET_TYPE_FLRC); + //LT.setRfFrequency(Frequency, Offset); + //LT.setBufferBaseAddress(0, 0); + //LT.setModulationParams(BandwidthBitRate, CodingRate, BT); + //LT.setPacketParams(PREAMBLE_LENGTH_32_BITS, FLRC_SYNC_WORD_LEN_P32S, RADIO_RX_MATCH_SYNCWORD_1, RADIO_PACKET_VARIABLE_LENGTH, 127, RADIO_CRC_3_BYTES, RADIO_WHITENING_OFF); + //LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + //LT.setSyncWord1(Syncword); + //*************************************************************************************************** + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured modem settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers + Serial.println(); + Serial.println(); + + Serial.print(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/52_FLRC_Transmitter/Settings.h b/examples/SX128x_examples/Basics/52_FLRC_Transmitter/Settings.h new file mode 100644 index 0000000..c3603bb --- /dev/null +++ b/examples/SX128x_examples/Basics/52_FLRC_Transmitter/Settings.h @@ -0,0 +1,31 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//FLRC Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes + +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const int8_t TXpower = 0; //power for transmissions in dBm +const uint16_t packet_delay = 1000; //mS delay between packets diff --git a/examples/SX128x_examples/Basics/53_FLRC_Receiver/53_FLRC_Receiver.ino b/examples/SX128x_examples/Basics/53_FLRC_Receiver/53_FLRC_Receiver.ino new file mode 100644 index 0000000..fe02bee --- /dev/null +++ b/examples/SX128x_examples/Basics/53_FLRC_Receiver/53_FLRC_Receiver.ino @@ -0,0 +1,213 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a test receiver for the Fast Long Range Communication (FLRC) mode introduced + in the SX128X devices. The program listens for incoming packets using the FLRC settings in the 'Settings.h' + file. The pins to access the SX128X device need to be defined in the 'Settings.h' file also. + + There is a printout of the valid packets received, the packet is assumed to be in ASCII printable text, + if its not ASCII text characters from 0x20 to 0x7F, expect weird things to happen on the Serial Monitor. + The LED will flash for each packet received. + + Sample serial monitor output; + + 3s Hello World 1234567890*,CRC,DAAB,RSSI,-73dB,Length,23,Packets,1,Errors,0,IRQreg,6 + + If there is a packet error it might look like this, which is showing a CRC error, + + 6s PacketError,RSSI,-103dB,Length,119,Packets,3,Errors,1,IRQreg,46,IRQ_RX_DONE,IRQ_SYNCWORD_VALID,IRQ_CRC_ERROR + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet + + +void loop() +{ + RXPacketL = LT.receive(RXBUFFER, RXBUFFER_SIZE, 60000, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + digitalWrite(LED1, HIGH); //something has happened + + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL == 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); //LED off + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t IRQStatus, localCRC; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + RXpacketCount++; + + printElapsedTime(); //print elapsed time to Serial Monitor + Serial.print(F(" ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL); //print the packet as ASCII characters + + localCRC = LT.CRCCCITT(RXBUFFER, RXPacketL, 0xFFFF); //calculate the CRC, this is the external CRC calculation of the RXBUFFER + Serial.print(F(",CRC,")); //contents, not the LoRa device internal CRC + Serial.print(localCRC, HEX); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + printElapsedTime(); //print elapsed time to Serial Monitor + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout")); + } + else + { + errors++; + Serial.print(F(" PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); //print the names of the IRQ registers set + } + + +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("53_FLRC_Receiver Starting")); + Serial.println(); + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in the library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("FLRC Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No FLRC device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + + LT.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + + //The full details of the setupFLRC function call above are listed below + //*************************************************************************************************** + //Setup FLRC + //*************************************************************************************************** + //LT.setMode(MODE_STDBY_RC); + //LT.setRegulatorMode(USE_LDO); + //LT.setPacketType(PACKET_TYPE_FLRC); + //LT.setRfFrequency(Frequency, Offset); + //LT.setBufferBaseAddress(0, 0); + //LT.setModulationParams(BandwidthBitRate, CodingRate, BT); + //LT.setPacketParams(PREAMBLE_LENGTH_32_BITS, FLRC_SYNC_WORD_LEN_P32S, RADIO_RX_MATCH_SYNCWORD_1, RADIO_PACKET_VARIABLE_LENGTH, 127, RADIO_CRC_3_BYTES, RADIO_WHITENING_OFF); + //LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + //LT.setSyncWord1(Syncword); + //*************************************************************************************************** + LT.setFLRCPayloadLengthReg(127); //FLRC will filter packets on receive according to length, so set to longest packet + Serial.println(); + LT.printModemSettings(); //reads and prints the configured modem settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operting settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers + Serial.println(); + Serial.println(); + + Serial.print(F("Receiver ready - RXBUFFER_SIZE ")); + Serial.println(RXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/Basics/53_FLRC_Receiver/Settings.h b/examples/SX128x_examples/Basics/53_FLRC_Receiver/Settings.h new file mode 100644 index 0000000..a57a457 --- /dev/null +++ b/examples/SX128x_examples/Basics/53_FLRC_Receiver/Settings.h @@ -0,0 +1,33 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//FLRC Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes + +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword +const int8_t TXpower = 0; //power for transmissions in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets + +#define RXBUFFER_SIZE 127 //Max RX buffer size diff --git a/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/238_StuartCAM_LoRa_Remote_Camera.ino b/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/238_StuartCAM_LoRa_Remote_Camera.ino new file mode 100644 index 0000000..f5e58d0 --- /dev/null +++ b/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/238_StuartCAM_LoRa_Remote_Camera.ino @@ -0,0 +1,158 @@ +/******************************************************************************************************* + Based on the OV2640 Arducam programs. + + ArduCAM demo (C)2017 Lee + Web: http://www.ArduCAM.com + *******************************************************************************************************/ + +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a demonstration program that uses LoRa to transfer an image taken with an + OV2640 Arducam camera. The LoRa transfer is carried out using the data transfer functions of the + SX12XX-LoRa library. Program tested on Arduino DUE. + + The Arducam software takes an image and saves it to a SD card. The filename is then passed across to + the LoRa library which transfers the file already on SD across to the remote receiver, which is running + program 239_StuartCAM_LoRa_Receiver or program 234_SDfile_Transfer_Receiver. + + Serial monitor baud rate is set at 115200 +*******************************************************************************************************/ + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include //get library here > https://github.com/ArduCAM/Arduino +#include +#include +#include +#include "memorysaver.h" //part of Arducam library + +#include +#include +#include "Settings.h" //LoRa settings etc. + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa + +#define ENABLEMONITOR //enable monitor prints +#define PRINTSEGMENTNUM //enable this define to print segment numbers +//#define DEBUG //enable this define to print debug info for segment transfers +//#define DEBUGSD //enable this defien to print SD file debug info +#define ENABLEFILECRC //enable this define to uses and show file CRCs +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking + +#define SDLIB //DTSDlibrary.h needs to use SD.h + +#include "DTSDlibrary.h" //part of SX12XX library +#include "SDtransfer.h" //part of SX12XX library + +char FileName[13]; //filename passed to both Arducam and LoRa routines + +ArduCAM myCAM( OV2640, OV2640CS ); +#include "OV2640.h" //include local Arducam code for OV2640 camera + + +void loop() +{ + myCAMSaveToSDFile(FileName, sizeof(FileName)); //when finished FileName[] contains name of file saved on SD + Serial.print(FileName); + Serial.println(F(" Saved to SD card")); + SDsendFile(FileName, sizeof(FileName)); //transfer image via LoRa + Serial.println(); + Serial.println(); + delay(60000); //wait 60 seconds +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + SDsetLED(LED1); //setup LED pin for data transfer indicator + + digitalWrite(OV2640CS, HIGH); + pinMode(OV2640CS, OUTPUT); //set the camera CS as an output + digitalWrite(SDCS, HIGH); + pinMode(SDCS, OUTPUT); //set the SDCS as an output + + Serial.begin(115200); + Serial.println(__FILE__); + + Wire.begin(); + SPI.begin(); + + //Initialize SD Card + while (!SD.begin(SDCS)) + { + Serial.println(F("SD Card Error - program halted")); + while (1); + } + Serial.println(F("SD Card detected")); + + //Initialize camera + setupOV2640(OV2640resolution); //resolution choice, see Settings.h file + + //Initialise LoRa device + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa device found")); + led_Flash(2, 125); + } + else + { + Serial.println(F("LoRa device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + Serial.println(); + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + + LoRa.printOperatingSettings(); + Serial.println(); + LoRa.printModemSettings(); + Serial.println(); + Serial.println(); + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Serial.println(F("Payload CRC disabled")); + } + else + { + Serial.println(F("Payload CRC enabled")); + } +} diff --git a/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/ARDUCAM_LICENSE.txt b/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/ARDUCAM_LICENSE.txt new file mode 100644 index 0000000..77cec6d --- /dev/null +++ b/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/ARDUCAM_LICENSE.txt @@ -0,0 +1,458 @@ + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + diff --git a/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/OV2640.h b/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/OV2640.h new file mode 100644 index 0000000..8311aef --- /dev/null +++ b/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/OV2640.h @@ -0,0 +1,253 @@ +/******************************************************************************************************* + Based on the OV2640 Arducam programs by; + + ArduCAM demo (C)2017 Lee + Web: http://www.ArduCAM.com + *******************************************************************************************************/ +#define OV2640_CHIPID_HIGH 0x0A +#define OV2640_CHIPID_LOW 0x0B + +void setupOV2640(uint8_t resolution); +void myCAMSaveToSDFile2(); +void myCAMSaveToSDFile(char *ACfilename, uint8_t ACfilenamesize); + +void setupOV2640(uint8_t resolution) +{ + uint8_t vid, pid; + uint8_t temp; + //Reset the CPLD + myCAM.write_reg(0x07, 0x80); + delay(100); + myCAM.write_reg(0x07, 0x00); + delay(100); + + while (1) { + //Check if the ArduCAM SPI bus is OK + myCAM.write_reg(ARDUCHIP_TEST1, 0x55); + temp = myCAM.read_reg(ARDUCHIP_TEST1); + + if (temp != 0x55) { + Serial.println(F("OV2640 SPI interface Error!")); + delay(1000); continue; + } else { + Serial.println(F("OV2640 SPI interface OK")); break; + } + } + + while (1) { + //Check if the camera module type is OV2640 + myCAM.wrSensorReg8_8(0xff, 0x01); + myCAM.rdSensorReg8_8(OV2640_CHIPID_HIGH, &vid); + myCAM.rdSensorReg8_8(OV2640_CHIPID_LOW, &pid); + if ((vid != 0x26 ) && (( pid != 0x41 ) || ( pid != 0x42 ))) { + Serial.println(F("Can't find OV2640 module!")); + delay(1000); continue; + } + else { + Serial.println(F("OV2640 setup on I2C")); break; + } + } + + myCAM.set_format(JPEG); + myCAM.InitCAM(); + + myCAM.OV2640_set_JPEG_size(resolution); +} + + +void myCAMSaveToSDFile2() +{ + char str[8]; + byte buf[256]; + static int i = 0; + static int k = 0; + uint8_t temp = 0, temp_last = 0; + uint32_t length = 0; + bool is_header = false; + File outFile; + + myCAM.flush_fifo(); //Flush the FIFO + + myCAM.clear_fifo_flag(); //Clear the capture done flag + + myCAM.start_capture(); //Start capture + Serial.println(F("start Capture")); + + while (!myCAM.get_bit(ARDUCHIP_TRIG , CAP_DONE_MASK)); + Serial.println(F("Capture Done")); + + length = myCAM.read_fifo_length(); + + Serial.print(F("The fifo length is :")); + Serial.println(length, DEC); + + if (length >= MAX_FIFO_SIZE) //384K + { + Serial.println(F("Over size.")); + return ; + } + if (length == 0 ) //0 kb + { + Serial.println(F("Size is 0.")); + return ; + } + + k = k + 1; //Construct a file name + itoa(k, str, 10); + strcat(str, ".jpg"); + + outFile = SD.open(str, O_WRITE | O_CREAT | O_TRUNC); //Open the new file + + if (!outFile) { + Serial.println(F("File open failed")); + return; + } + myCAM.CS_LOW(); + myCAM.set_fifo_burst(); + + while ( length-- ) + { + temp_last = temp; + temp = SPI.transfer(0x00); + //Read JPEG data from FIFO + if ( (temp == 0xD9) && (temp_last == 0xFF) ) //If find the end ,break while, + { + buf[i++] = temp; //save the last 0XD9 + //Write the remain bytes in the buffer + myCAM.CS_HIGH(); + outFile.write(buf, i); + //Close the file + outFile.close(); + Serial.print(str); + Serial.println(F(" Image save OK")); + is_header = false; + i = 0; + } + if (is_header == true) + { + //Write image data to buffer if not full + if (i < 256) + buf[i++] = temp; + else + { + //Write 256 bytes image data to file + myCAM.CS_HIGH(); + outFile.write(buf, 256); + i = 0; + buf[i++] = temp; + myCAM.CS_LOW(); + myCAM.set_fifo_burst(); + } + } + else if ((temp == 0xD8) & (temp_last == 0xFF)) + { + is_header = true; + buf[i++] = temp_last; + buf[i++] = temp; + } + } +} + + +void myCAMSaveToSDFile(char *ACfilename, uint8_t ACfilenamesize) +{ + byte buf[256]; + char ACfilename2[ACfilenamesize+1]; + static uint16_t i = 0; + static uint16_t k = 0; + uint8_t temp = 0, temp_last = 0; + uint32_t length = 0; + bool is_header = false; + File dataFile; + memset(ACfilename, 0, ACfilenamesize); //fill ACfilename array with nulls. + myCAM.flush_fifo(); //Flush the FIFO + myCAM.clear_fifo_flag(); //Clear the capture done flag + myCAM.start_capture(); //Start capture + + Serial.println(F("Start capture ")); + + while (!myCAM.get_bit(ARDUCHIP_TRIG , CAP_DONE_MASK)); + Serial.println(F("Done")); + length = myCAM.read_fifo_length(); + Serial.print(F("FIFO length ")); + Serial.println(length, DEC); + + if (length >= MAX_FIFO_SIZE) //384K + { + Serial.println(F("FIFO over size")); + return ; + } + if (length == 0 ) //0 kb + { + Serial.println(F("FIFO size is 0")); + return ; + } + + //Construct a file name + k = k + 1; + itoa(k, ACfilename2, 10); + strcat(ACfilename2, ".jpg"); + + ACfilename[0] = '/'; + memcpy(ACfilename+1, ACfilename2, ACfilenamesize); + + //Serial.print(F("Adjusted filename ")); + //Serial.println(ACfilename); + + dataFile = SD.open(ACfilename, O_WRITE | O_CREAT | O_TRUNC); //Open the new file + + if (!dataFile) + { + Serial.print(ACfilename); + Serial.println(F(" File open failed")); + return; + } + + myCAM.CS_LOW(); + myCAM.set_fifo_burst(); + + while ( length-- ) + { + temp_last = temp; + temp = SPI.transfer(0x00); + + //Read JPEG data from FIFO + if ( (temp == 0xD9) && (temp_last == 0xFF) ) + { + buf[i++] = temp; //save the last 0XD9 + + myCAM.CS_HIGH(); + dataFile.write(buf, i); //Write the remain bytes in the buffer + + dataFile.close(); //Close the file + Serial.print(ACfilename); + Serial.println(); + is_header = false; + i = 0; + + } + + if (is_header == true) + { + //Write image data to buffer if not full + if (i < 256) + buf[i++] = temp; + else + { + //Write 256 bytes image data to file + myCAM.CS_HIGH(); + dataFile.write(buf, 256); + i = 0; + buf[i++] = temp; + myCAM.CS_LOW(); + myCAM.set_fifo_burst(); + } + } + else if ((temp == 0xD8) & (temp_last == 0xFF)) + { + is_header = true; + buf[i++] = temp_last; + buf[i++] = temp; + } + } +} diff --git a/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/Settings.h b/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/Settings.h new file mode 100644 index 0000000..3e931f3 --- /dev/null +++ b/examples/SX128x_examples/Camera/238_StuartCAM_LoRa_Remote_Camera/Settings.h @@ -0,0 +1,72 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done +#define LED1 8 //On board LED, high for on +#define SDCS 30 //select pin for SD card +#define OV2640CS 22 //select for SPI on camera + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +#define Monitorport Serial //Port where serial prints go + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet + +const uint32_t ACKdelaymS = 0; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 750; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 750; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t DTSendAttempts = 10; //number of attempts sending a packet before a restart +const uint8_t StartAttempts = 2; //number of attempts to start transfer before a fail +const uint8_t SendAttempts = 5; //number of attempts carrying out a process before a restart +const uint8_t Maxfilenamesize = 32; //size of DTfilename buffer +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif + + +/****************************************************************************** + Setup the resolution OV2640 required, the choices are; + + OV2640_160x120,OV2640_176x144,OV2640_320x240,OV2640_352x288,OV2640_640x480, + OV2640_800x600, OV2640_1024x768,OV2640_1280x1024,OV2640_1600x1200 +*******************************************************************************/ + +const uint8_t OV2640resolution = OV2640_640x480; diff --git a/examples/SX128x_examples/Camera/239_StuartCAM_LoRa_Receiver/239_StuartCAM_LoRa_Receiver.ino b/examples/SX128x_examples/Camera/239_StuartCAM_LoRa_Receiver/239_StuartCAM_LoRa_Receiver.ino new file mode 100644 index 0000000..f777d98 --- /dev/null +++ b/examples/SX128x_examples/Camera/239_StuartCAM_LoRa_Receiver/239_StuartCAM_LoRa_Receiver.ino @@ -0,0 +1,142 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program to receive images sent via LoRa using program + 238_StuartCAM_LoRa_Remote_Camera which uses an OV2640 Arducam camera to take pictures. + + The received images\files are saved onto an SD card. Arduino DUEs were used for this test. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include + +#include +#include +#include "Settings.h" //LoRa settings etc. + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa, required by SDtransfer.h + +//#define SDLIB //define SDLIB for SD.h or SDFATLIB for SDfat.h +#define SDFATLIB + +#define ENABLEMONITOR //enable monitor prints +#define PRINTSEGMENTNUM +#define ENABLEFILECRC //enable this define to uses and show file CRCs +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking +//#define DEBUG //see additional debug info + +#include //library of SD functions +#include //library of data transfer functions + + +void loop() +{ + SDreceiveaPacketDT(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + SDsetLED(LED1); //setup LED pin for data transfer indicator + +#ifdef ENABLEMONITOR + Monitorport.begin(115200); + Monitorport.println(); + Monitorport.println(F(__FILE__)); +#endif + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("LoRa device error")); +#endif + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.print(F("Initializing SD card...")); +#endif + + if (DTSD_initSD(SDCS)) + { + Monitorport.println(F("SD Card initialized.")); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("SD Card failed, or not present.")); +#endif + while (1) led_Flash(100, 50); + } + +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Monitorport.println(F("Payload CRC disabled")); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Payload CRC enabled")); +#endif + } + + SDDTSegmentNext = 0; + SDDTFileOpened = false; + +#ifdef ENABLEMONITOR + Monitorport.println(F("SDfile transfer receiver ready")); + Monitorport.println(); +#endif +} diff --git a/examples/SX128x_examples/Camera/239_StuartCAM_LoRa_Receiver/Settings.h b/examples/SX128x_examples/Camera/239_StuartCAM_LoRa_Receiver/Settings.h new file mode 100644 index 0000000..972e556 --- /dev/null +++ b/examples/SX128x_examples/Camera/239_StuartCAM_LoRa_Receiver/Settings.h @@ -0,0 +1,60 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done +#define LED1 8 //On board LED, high for on +#define SDCS 30 //select pin for SD card + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +#define Monitorport Serial //Port where serial prints go + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 10; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t Maxfilenamesize = 32; //size of DTfilename buffer +const uint8_t SendAttempts = 10; //number of attempts sending a packet or attempting a process before a restart of transfer +const uint8_t StartAttempts = 10; //number of attempts sending the file + +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/Camera/240_StuartCAM_ESP32CAM/240_StuartCAM_ESP32CAM.ino b/examples/SX128x_examples/Camera/240_StuartCAM_ESP32CAM/240_StuartCAM_ESP32CAM.ino new file mode 100644 index 0000000..e79d307 --- /dev/null +++ b/examples/SX128x_examples/Camera/240_StuartCAM_ESP32CAM/240_StuartCAM_ESP32CAM.ino @@ -0,0 +1,447 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/********* + Rui Santos + Complete project details at https://RandomNerdTutorials.com/esp32-cam-take-photo-save-microsd-card + Github repository at https://github.com/RuiSantosdotme/ESP32-CAM-Arduino-IDE + + IMPORTANT!!! + - Select Board "AI Thinker ESP32-CAM" + - GPIO 0 must be connected to GND to upload a sketch + - After connecting GPIO 0 to GND, press the ESP32-CAM on-board RESET button to put your board in flashing mode + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files. + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. +*********/ + + +/******************************************************************************************************* + Program Operation - This is a program using the ESP32CAM to take pictures and transmit those pictures via + LoRa radio to another remote Arduino. + + This program is for an ESP32CAM board that has an SPI LoRa module set up on the following pins; NSS 12, + NRESET 14, SCK 4, MISO 13, MOSI 2, RFBUSY 15, 3.3V VCC and GND. All other pins on the SX128X are not + connected. + + Note that the white LED on pin 4 or the transistor controlling it need to be removed so that the LoRa + device can properly use pin 4. + + The program wakes up, takes a picture and starts the transfer of the picture (from its memory array in + PSRAM) with LoRa, more details of the file transfer process will be found here; + + https://stuartsprojects.github.io/2021/09/20/Large-Data-Transfers-with-LoRa-Part3.html + + Note that if the camera fails then the program will attempt to send, and wait for the acknowledge, for a + DTinfo packet reporting the fail. + + Serial monitor baud rate is set at 115200 +*******************************************************************************************************/ + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include +#include "FS.h" //SD Card ESP32 +#include "SD_MMC.h" //SD Card ESP32 +#include "soc/soc.h" //disable brownout problems +#include "soc/rtc_cntl_reg.h" //disable brownout problems +#include "driver/rtc_io.h" + +#include +#include //get library here > https://github.com/StuartsProjects/SX12XX-LoRa +#include +#include "Settings.h" //LoRa and program settings +SX128XLT LoRa; //create a library class instance called LoRa, needed for ARtransferIRQ.h + +#define ENABLEMONITOR //enable define to see progress messages in ARtransferIRQ.h +#define PRINTSEGMENTNUM //enable to print segment numbers as transfer progresses +#define ENABLEARRAYCRC //enable this define to use and show CRCs +//#define DISABLEPAYLOADCRC //enable this define if you want to not use packet payload CRC checking +//#define DEBUG //enable this define to show data transfer debug info + +RTC_DATA_ATTR int16_t bootCount = 0; //variables to save in RTC ram +RTC_DATA_ATTR uint16_t sleepcount = 0; +RTC_DATA_ATTR uint16_t pictureNumber = 0; //number of picture taken, set to 0 on reset + +#include "esp_camera.h" +camera_config_t config; //stores the camera configuration parameters +#include //library of array transfer functions + +bool SDOK; + + +void loop() +{ + SDOK = false; + ARDTflags = 0; + + if (initMicroSDCard()) //need to setup SD card before camera + { + Serial.println(F("SD Card OK")); + SDOK = true; + } + else + { + Serial.println(F("****************************")); + Serial.println(F("ERROR - SD Card Mount Failed")); + Serial.println(F("****************************")); + bitSet(ARDTflags, ARNoFileSave); + } + + if (!configInitCamera()) + { + bitSet(ARDTflags, ARNoCamera); //set flag bit for no camera working + Serial.println(F("Camera config failed")); + Serial.println(F("Sending DTInfo packet")); + setupLoRaDevice(); + ARsendDTInfo(); + startSleep(); + } + else + { + if (takePhotoSend(PicturesToTake, PictureDelaymS)) + { + //picture taken OK + startSleep(); + } + else + { + //picture take failed + Serial.println("********************************"); + Serial.println("ERROR - Take picture send failed"); + Serial.println("********************************"); + Serial.println(); + Serial.println("Sending DTInfo packet"); + Serial.flush(); + bitSet(ARDTflags, ARNoCamera); //set flag bit for no camera working + setupLoRaDevice(); + ARsendDTInfo(); + startSleep(); + } + } + startSleep(); +} + +void startSleep() +{ + LoRa.setSleep(CONFIGURATION_RETENTION); + rtc_gpio_hold_en(GPIO_NUM_4); + rtc_gpio_hold_en(GPIO_NUM_12); //hold LoRa device off in sleep + esp_sleep_enable_timer_wakeup(SleepTimesecs * uS_TO_S_FACTOR); + Serial.print(F("Start Sleep ")); + Serial.print(SleepTimesecs); + Serial.println(F("s")); + Serial.flush(); + sleepcount++; + esp_deep_sleep_start(); + Serial.println("This should never be printed !!!"); +} + + + +bool initMicroSDCard() +{ + if (!SD_MMC.begin("/sdcard", true)) //use this line for 1 bit mode, pin 2 only, 4,12,13 not used + { + return false; + } + + uint8_t cardType = SD_MMC.cardType(); + + if (cardType == CARD_NONE) + { + Serial.println(F("Unknown SD card type")); + return false; + } + + return true; +} + + +void redFlash(uint16_t flashes, uint16_t ondelaymS, uint16_t offdelaymS) +{ + uint16_t index; + + pinMode(REDLED, OUTPUT); //setup pin as output + + for (index = 1; index <= flashes; index++) + { + digitalWrite(REDLED, LOW); + delay(ondelaymS); + digitalWrite(REDLED, HIGH); + delay(offdelaymS); + } + pinMode(REDLED, INPUT); //setup pin as input +} + +bool setupLoRaDevice() +{ + SPI.begin(SCK, MISO, MOSI, NSS); + + if (LoRa.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("LoRa device found")); + } + else + { + Serial.println(F("LoRa Device error")); + return false; + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Monitorport.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Monitorport.println(F("Using FLRC packets")); +#endif + + Serial.println(); + return true; +} + + +void setup() +{ + redFlash(4, 125, 125); + + //WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector + rtc_gpio_hold_dis(GPIO_NUM_4); + rtc_gpio_hold_dis(GPIO_NUM_12); //LoRa NSS back to normal control after sleep + + pinMode(2, INPUT_PULLUP); + digitalWrite(NSS, HIGH); + pinMode(NSS, OUTPUT); + + Serial.begin(115200); + Serial.println(); + Serial.println(F(__FILE__)); + + if (bootCount == 0) //run this only the first time after programming or power up + { + bootCount = bootCount + 1; + } + + Serial.println(F("Awake !")); + Serial.print(F("Bootcount ")); + Serial.println(bootCount); + Serial.print(F("Sleepcount ")); + Serial.println(sleepcount); + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Serial.println(F("Payload CRC disabled")); + } + else + { + Serial.println(F("Payload CRC enabled")); + } +} + + +//*********************************************************************************************** +// Start camera Code +//*********************************************************************************************** + +bool configInitCamera() +{ + Serial.println(F("Initialising the camera module ")); + + config.ledc_channel = LEDC_CHANNEL_0; + config.ledc_timer = LEDC_TIMER_0; + config.pin_d0 = Y2_GPIO_NUM; + config.pin_d1 = Y3_GPIO_NUM; + config.pin_d2 = Y4_GPIO_NUM; + config.pin_d3 = Y5_GPIO_NUM; + config.pin_d4 = Y6_GPIO_NUM; + config.pin_d5 = Y7_GPIO_NUM; + config.pin_d6 = Y8_GPIO_NUM; + config.pin_d7 = Y9_GPIO_NUM; + config.pin_xclk = XCLK_GPIO_NUM; + config.pin_pclk = PCLK_GPIO_NUM; + config.pin_vsync = VSYNC_GPIO_NUM; + config.pin_href = HREF_GPIO_NUM; + config.pin_sscb_sda = SIOD_GPIO_NUM; + config.pin_sscb_scl = SIOC_GPIO_NUM; + config.pin_pwdn = PWDN_GPIO_NUM; + config.pin_reset = RESET_GPIO_NUM; + config.xclk_freq_hz = 20000000; + config.pixel_format = PIXFORMAT_JPEG; //YUV422,GRAYSCALE,RGB565,JPEG + + //Select lower framesize if the camera doesn't support PSRAM + if (psramFound()) + { + Serial.println(F("PSRAM found")); + config.frame_size = FRAMESIZE_SVGA; //FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA, XUGA == 100K+, SVGA = 25K+ + config.jpeg_quality = 10; //10-63 lower number means higher quality + config.fb_count = 2; + } + else + { + Serial.println(F("No PSRAM")); + config.frame_size = FRAMESIZE_XGA; + config.jpeg_quality = 12; + config.fb_count = 1; + } + + esp_err_t err = esp_camera_init(&config); //Initialize the Camera + if (err != ESP_OK) + { + Serial.printf("Camera init failed with error 0x%x", err); + Serial.println(); + return false; + } + + sensor_t * s = esp_camera_sensor_get(); + s->set_brightness(s, 0); // -2 to 2 + s->set_contrast(s, 0); // -2 to 2 + s->set_saturation(s, 0); // -2 to 2 + s->set_special_effect(s, 0); // 0 to 6 (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia) + s->set_whitebal(s, 1); // 0 = disable , 1 = enable + s->set_awb_gain(s, 1); // 0 = disable , 1 = enable + s->set_wb_mode(s, 1); // 0 to 4 - if awb_gain enabled (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home) + s->set_exposure_ctrl(s, 1); // 0 = disable , 1 = enable + s->set_aec2(s, 1); // 0 = disable , 1 = enable + s->set_ae_level(s, 0); // -2 to 2 + s->set_aec_value(s, 450); // 0 to 1200 + s->set_gain_ctrl(s, 1); // 0 = disable , 1 = enable + s->set_agc_gain(s, 0); // 0 to 30 + s->set_gainceiling(s, (gainceiling_t)0); // 0 to 6 + s->set_bpc(s, 1); // 0 = disable , 1 = enable + s->set_wpc(s, 0); // 0 = disable , 1 = enable + s->set_raw_gma(s, 1); // 0 = disable , 1 = enable + s->set_lenc(s, 0); // 0 = disable , 1 = enable + s->set_hmirror(s, 0); // 0 = disable , 1 = enable + s->set_vflip(s, 0); // 0 = disable , 1 = enable + s->set_dcw(s, 1); // 0 = disable , 1 = enable + s->set_colorbar(s, 0); // 0 = disable , 1 = enable + return true; +} + + +uint16_t takePhotoSend(uint8_t num, uint32_t gapmS) +{ + uint8_t index = 1; + char filenamearray[32]; + bool sentOK = false; + String path; + camera_fb_t * fb = esp_camera_fb_get(); + + for (index = 1; index <= num; index++) //take a number of pictures, send last + { + pictureNumber++; + path = "/pic" + String(pictureNumber) + ".jpg"; + Serial.print("Next picture file name "); + Serial.println(path.c_str()); + + if (!fb) + { + Serial.println(F("*****************************")); + Serial.println(F("ERROR - Camera capture failed")); + Serial.println(F("*****************************")); + delay(1000); + pictureNumber--; //restore picture number + bitSet(ARDTflags, ARNoFileSave); + } + + Serial.println(F("Camera capture success")); + +#ifdef DEBUG + Serial.print(F("First 8 bytes ")); + printarrayHEX(fb->buf, 0, 8); + Serial.println(); + Serial.print(F("Last 8 bytes ")); + printarrayHEX(fb->buf, (fb->len - 8), 8); + Serial.println(); +#endif + + if (SDOK) + { + Serial.println(F("Save picture to SD card")); + fs::FS &fs = SD_MMC; //save picture to microSD card + File file = fs.open(path.c_str(), FILE_WRITE); + if (!file) + { + Serial.println(F("*********************************************")); + Serial.println(F("ERROR Failed to open SD file in writing mode")); + Serial.println(F("*********************************************")); + bitSet(ARDTflags, ARNoFileSave); + } + else + { + file.write(fb->buf, fb->len); // payload (image), payload length + Serial.printf("Saved file to path: %s\r\n", path.c_str()); + } + file.close(); + } + else + { + Serial.println(F("***************")); + Serial.println(F("No SD available")); + Serial.println(F("***************")); + } + } + + SD_MMC.end(); + if (setupLoRaDevice()) + { + Serial.print(F("Send with LoRa ")); + Serial.println(path.c_str()); + uint8_t tempsize = path.length(); + path.toCharArray(filenamearray, tempsize + 1); //copy file name to the local filenamearray + filenamearray[tempsize + 1] = 0; //ensure there is a null at end of filename in filenamearray + sentOK = ARsendArray(fb->buf, fb->len, filenamearray, tempsize + 1); //pass array pointer and length across to LoRa send function + } + else + { + Serial.println(F("LoRa device not available")); + } + + esp_camera_fb_return(fb); //return the frame buffer back to the driver for reuse + + delay(gapmS); + + if (sentOK) + { + Serial.print(filenamearray); + Serial.println(F(" Sent OK")); + return pictureNumber; + } + else + { + Serial.print(filenamearray); + Serial.println(F(" Send picture failed")); + } + return 0; +} + + +void printarrayHEX(uint8_t *buff, uint32_t startaddr, uint32_t len) +{ + uint32_t index; + uint8_t buffdata; + + for (index = startaddr; index < (startaddr + len); index++) + { + buffdata = buff[index]; + if (buffdata < 16) + { + Serial.print(F("0")); + } + Serial.print(buffdata, HEX); + Serial.print(F(" ")); + } +} diff --git a/examples/SX128x_examples/Camera/240_StuartCAM_ESP32CAM/Settings.h b/examples/SX128x_examples/Camera/240_StuartCAM_ESP32CAM/Settings.h new file mode 100644 index 0000000..abb84bb --- /dev/null +++ b/examples/SX128x_examples/Camera/240_StuartCAM_ESP32CAM/Settings.h @@ -0,0 +1,92 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//updated pinouts for 'ESP32CAM_Long_Range_Wireless_Adapter PCB dated 271121 +//Note transistor driving the White LED on pin 4, or the LED needs to be removed + +#define NSS 12 //select on LoRa device +#define NRESET 14 //reset pin on LoRa device +#define RFBUSY 15 //busy pin on LoRa device +#define SCK 4 //SCK on SPI3 +#define MISO 13 //MISO on SPI3 +#define MOSI 2 //MOSI on SPI3 +#define REDLED 33 //pin number for ESP32CAM on board red LED, set logic level low for on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after packet actioned and ack sent +const uint32_t ACKdelaystartendmS = 25; //ms delay before ack sent at array start wrie and end write +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 500; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 500; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 25; //ms delay if there has been an duplicate segment or command receipt +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer + +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc + +const uint8_t DTSegmentSize = 245; //number of bytes in each segment or payload +const uint8_t ARDTfilenamesize = 32; //size of filename buffer + +const uint8_t StartAttempts = 2; //number of attempts to start transfer before a fail +const uint8_t SendAttempts = 5; //number of attempts carrying out a process before a restart + +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +const uint16_t SleepTimesecs = 15; //sleep time in seconds after each TX loop +const uint32_t uS_TO_S_FACTOR = 1000000; //Conversion factor for micro seconds to seconds +const uint8_t PicturesToTake = 1; //number of pictures to take at each wakeup, only last is sent via LoRa +const uint32_t PictureDelaymS = 1000; //delay in mS between pictures + + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif + + +// Pin definition for CAMERA_MODEL_AI_THINKER +// Change pin definition if you're using another ESP32 with camera module +#define PWDN_GPIO_NUM 32 +#define RESET_GPIO_NUM -1 +#define XCLK_GPIO_NUM 0 +#define SIOD_GPIO_NUM 26 +#define SIOC_GPIO_NUM 27 +#define Y9_GPIO_NUM 35 +#define Y8_GPIO_NUM 34 +#define Y7_GPIO_NUM 39 +#define Y6_GPIO_NUM 36 +#define Y5_GPIO_NUM 21 +#define Y4_GPIO_NUM 19 +#define Y3_GPIO_NUM 18 +#define Y2_GPIO_NUM 5 +#define VSYNC_GPIO_NUM 25 +#define HREF_GPIO_NUM 23 +#define PCLK_GPIO_NUM 22 diff --git a/examples/SX128x_examples/Camera/241_StuartCAM_ESP32CAM_LoRa_Receiver/241_StuartCAM_ESP32CAM_LoRa_Receiver.ino b/examples/SX128x_examples/Camera/241_StuartCAM_ESP32CAM_LoRa_Receiver/241_StuartCAM_ESP32CAM_LoRa_Receiver.ino new file mode 100644 index 0000000..234b0e7 --- /dev/null +++ b/examples/SX128x_examples/Camera/241_StuartCAM_ESP32CAM_LoRa_Receiver/241_StuartCAM_ESP32CAM_LoRa_Receiver.ino @@ -0,0 +1,222 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a receiver program for an ESP32CAM board that has an SPI LoRa module set up + on the following pins; NSS 12, NRESET 14, SCK 4, MISO 13, MOSI 2, RFBUSY 15, 3.3V VCC and GND. All other + pins on the SX128X are not connected. The received pictures are saved to the ESP32CAMs SD card. + + Note that the white LED on pin 4 or the transistor controlling it need to be removed so that the LoRa + device can properly use pin 4. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include +#include "FS.h" //SD Card ESP32 +#include "SD_MMC.h" //SD Card ESP32 +#include "soc/soc.h" //disable brownout problems +#include "soc/rtc_cntl_reg.h" //disable brownout problems +#include "driver/rtc_io.h" +#include //SX12XX-LoRa library +#include //part of SX12XX-LoRa library +#include "Settings.h" //LoRa settings etc. + +#define ENABLEMONITOR //enable this define to monitor data transfer information, needed for ARtransferIRQ.h +#define ENABLEARRAYCRC //enable this define to check and print CRC of sent array +#define PRINTSEGMENTNUM //enable this define to print segment numbers during data transfer +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking +//#define DEBUG //enable more detail of transfer progress + + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa +#include + +uint8_t *PSRAMptr; //create a global pointer to the array to send, so all functions have access +bool SDOK; +bool savedtoSDOK; + + +void loop() +{ + uint32_t arraylength; + SDOK = false; + Serial.println(F("LoRa file transfer receiver ready")); + setupLoRaDevice(); + + //if there is a successful array transfer the returned length > 0 + + arraylength = ARreceiveArray(PSRAMptr, sizeof(ARDTarraysize), ReceiveTimeoutmS); + + SPI.end(); + + digitalWrite(NSS, HIGH); + digitalWrite(NRESET, HIGH); + + if (arraylength) + { + Serial.print(F("Returned picture length ")); + Serial.println(arraylength); + if (initMicroSDCard()) + { + SDOK = true; + Serial.println("SD Card OK"); + Serial.print(ARDTfilenamebuff); + Serial.println(F(" Save picture to SD card")); + + fs::FS &fs = SD_MMC; //save picture to microSD card + File file = fs.open(ARDTfilenamebuff, FILE_WRITE); + if (!file) + { + Serial.println("*********************************************"); + Serial.println("ERROR Failed to open SD file in writing mode"); + Serial.println("*********************************************"); + savedtoSDOK = false; + } + else + { + file.write(PSRAMptr, arraylength); // pointer to array and length + Serial.print(ARDTfilenamebuff); + Serial.println(" Saved to SD"); + savedtoSDOK = true; + } + file.close(); + SD_MMC.end(); + } + else + { + Serial.println("No SD available"); + } + } + else + { + Serial.println(F("Error receiving picture")); + if (ARDTArrayTimeout) + { + Serial.println(F("Timeout receiving picture")); + } + } + Serial.println(); +} + + +bool setupLoRaDevice() +{ + SPI.begin(SCK, MISO, MOSI, NSS); + + if (LoRa.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("LoRa device found")); + } + else + { + Serial.println(F("LoRa Device error")); + return false; + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Monitorport.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Monitorport.println(F("Using FLRC packets")); +#endif + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Serial.println(F("Payload CRC disabled")); + } + else + { + Serial.println(F("Payload CRC enabled")); + } + return true; +} + + +bool initMicroSDCard() +{ + if (!SD_MMC.begin("/sdcard", true)) //use this line for 1 bit mode, pin 2 only, 4,12,13 not used + { + Serial.println("*****************************"); + Serial.println("ERROR - SD Card Mount Failed"); + Serial.println("*****************************"); + return false; + } + + uint8_t cardType = SD_MMC.cardType(); + + if (cardType == CARD_NONE) + { + Serial.println("No SD Card found"); + return false; + } + return true; +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(REDLED, HIGH); + delay(delaymS); + digitalWrite(REDLED, LOW); + delay(delaymS); + } +} + + +void setup() +{ + uint32_t available_PSRAM_size; + uint32_t new_available_PSRAM_size; + + //WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector + pinMode(REDLED, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + ARsetDTLED(REDLED); //setup LED pin for data transfer indicator + + digitalWrite(NSS, HIGH); + pinMode(NSS, OUTPUT); //disable LoRa device for now + + Serial.begin(115200); //format is Serial.begin(baud-rate, protocol, RX pin, TX pin); + Serial.println(); + Serial.println(__FILE__); + + if (psramInit()) + { + Serial.println("PSRAM is correctly initialised"); + available_PSRAM_size = ESP.getFreePsram(); + Serial.println((String)"PSRAM Size available: " + available_PSRAM_size); + } + else + { + Serial.println("PSRAM not available"); + while (1); + } + + Serial.println("Allocate array in PSRAM"); + uint8_t *byte_array = (uint8_t *) ps_malloc(ARDTarraysize * sizeof(uint8_t)); + PSRAMptr = byte_array; //save the pointe to byte_array to global pointer + + new_available_PSRAM_size = ESP.getFreePsram(); + Serial.println((String)"PSRAM Size available: " + new_available_PSRAM_size); + Serial.print("PSRAM array bytes allocated: "); + Serial.println(available_PSRAM_size - new_available_PSRAM_size); + Serial.println(); +} diff --git a/examples/SX128x_examples/Camera/241_StuartCAM_ESP32CAM_LoRa_Receiver/Settings.h b/examples/SX128x_examples/Camera/241_StuartCAM_ESP32CAM_LoRa_Receiver/Settings.h new file mode 100644 index 0000000..96f4a8f --- /dev/null +++ b/examples/SX128x_examples/Camera/241_StuartCAM_ESP32CAM_LoRa_Receiver/Settings.h @@ -0,0 +1,63 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 12 //select on LoRa device +#define NRESET 14 //reset pin on LoRa device +#define RFBUSY 15 //busy pin on LoRa device +#define SCK 4 //SCK on SPI3 +#define MISO 13 //MISO on SPI3 +#define MOSI 2 //MOSI on SPI3 +#define REDLED 33 //pin number for ESP32CAM on board red LED, set logic level low for on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 3000000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after general packet actioned and ack sent +const uint32_t ACKdelaystartendmS = 25; //ms delay before ack sent at array start wrie and end write +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 25; //ms delay if there has been an duplicate segment or command receipt +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer + +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc + +const uint32_t ReceiveTimeoutmS = 60000; //mS waiting for array transfer before timeout +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 6 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t ARDTfilenamesize = 32; //size of DTfilename buffer used by array transfer functions +const uint32_t ARDTarraysize = 0x20000; //maximum file\array size to receive +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +const uint8_t StartAttempts = 2; //number of attempts to start transfer before a fail +const uint8_t SendAttempts = 5; //number of attempts carrying out a process before a restart + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer.ino b/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer.ino new file mode 100644 index 0000000..2f19b60 --- /dev/null +++ b/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer.ino @@ -0,0 +1,253 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a receiver program for an ESP32CAM board that has an SPI LoRa module set up + on the following pins; NSS 12, NRESET 14, SCK 4, MISO 13, MOSI 2, RFBUSY 15, 3.3V VCC and GND. All other + pins on the SX128X are not connected. The received pictures are saved to the ESP32CAMs SD card and also + transferred to a connected PC using the YModem protocol over the normal program upload port. Progress + or debug messages can be seen by connection an additional serial adapter to pin 33 on the ESP32CAM. + + For details of the PC upload process see here; + + https://stuartsprojects.github.io/2022/02/05/Long-Range-Wireless-Adapter-for-ESP32CAM.html + + Note that the white LED on pin 4 or the transistor controlling it need to be removed so that the LoRa + device can properly use pin 4. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include +#include "FS.h" //SD Card ESP32 +#include "SD_MMC.h" //SD Card ESP32 +#include "soc/soc.h" //disable brownout problems +#include "soc/rtc_cntl_reg.h" //disable brownout problems +#include "driver/rtc_io.h" +#include //SX12XX-LoRa library +#include //part of SX12XX-LoRa library +#include "Settings.h" //LoRa settings etc. + +#define ENABLEMONITOR //enable this define to monitor data transfer information, needed for ARtransferIRQ.h +#define ENABLEARRAYCRC //enable this define to check and print CRC of sent array +#define PRINTSEGMENTNUM //enable this define to print segment numbers during data transfer +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking +//#define DEBUG //enable more detail of transfer progress + + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa +#include + +uint8_t *PSRAMptr; //create a global pointer to the array to send, so all functions have access +bool SDOK; +bool savedtoSDOK; + +#include "YModemArray.h" + + +void loop() +{ + uint32_t arraylength, bytestransfered; + SDOK = false; + Monitorport.println(F("LoRa file transfer receiver ready")); + + setupLoRaDevice(); + + //if there is a successful array transfer the returned length > 0 + //arraylength = LocalARreceiveArray(PSRAMptr, sizeof(ARDTarraysize), ReceiveTimeoutmS); + + arraylength = ARreceiveArray(PSRAMptr, sizeof(ARDTarraysize), ReceiveTimeoutmS); + + SPI.end(); + + digitalWrite(NSS, HIGH); + digitalWrite(NRESET, HIGH); + + if (arraylength) + { + Monitorport.print(F("Returned picture length ")); + Monitorport.println(arraylength); + if (initMicroSDCard()) + { + SDOK = true; + Monitorport.println("SD Card OK"); + Monitorport.print(ARDTfilenamebuff); + Monitorport.println(F(" Save picture to SD card")); + + fs::FS &fs = SD_MMC; //save picture to microSD card + File file = fs.open(ARDTfilenamebuff, FILE_WRITE); + if (!file) + { + Monitorport.println("*********************************************"); + Monitorport.println("ERROR Failed to open SD file in writing mode"); + Monitorport.println("*********************************************"); + savedtoSDOK = false; + } + else + { + file.write(PSRAMptr, arraylength); // pointer to array and length + Monitorport.print(ARDTfilenamebuff); + Monitorport.println(" Saved to SD"); + savedtoSDOK = true; + } + file.close(); + SD_MMC.end(); + } + else + { + Monitorport.println("No SD available"); + } + } + else + { + Monitorport.println(F("Error receiving picture")); + if (ARDTArrayTimeout) + { + Monitorport.println(F("Timeout receiving picture")); + } + } + Monitorport.println(); + + if (arraylength) + { + Monitorport.println(F("File received - start YModem transfer to PC")); + + //bytestransfered = yModemSend(ARDTfilenamebuff, 1, 1); + bytestransfered = yModemSend(ARDTfilenamebuff, PSRAMptr, arraylength, 1, 1); + + if (bytestransfered > 0) + { + Monitorport.print(F("YModem transfer completed ")); + Monitorport.print(bytestransfered); + Monitorport.println(F(" bytes sent")); + } + else + { + Monitorport.println(F("YModem transfer FAILED")); + } + Monitorport.println(); + } +} + + +bool setupLoRaDevice() +{ + SPI.begin(SCK, MISO, MOSI, NSS); + + if (LoRa.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Monitorport.println(F("LoRa device found")); + } + else + { + Monitorport.println(F("LoRa Device error")); + return false; + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Monitorport.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Monitorport.println(F("Using FLRC packets")); +#endif + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Monitorport.println(F("Payload CRC disabled")); + } + else + { + Monitorport.println(F("Payload CRC enabled")); + } + return true; +} + + +bool initMicroSDCard() +{ + if (!SD_MMC.begin("/sdcard", true)) //use this line for 1 bit mode, pin 2 only, 4,12,13 not used + { + Monitorport.println("*****************************"); + Monitorport.println("ERROR - SD Card Mount Failed"); + Monitorport.println("*****************************"); + return false; + } + + uint8_t cardType = SD_MMC.cardType(); + + if (cardType == CARD_NONE) + { + Monitorport.println("No SD Card found"); + return false; + } + return true; +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(REDLED, HIGH); + delay(delaymS); + digitalWrite(REDLED, LOW); + delay(delaymS); + } +} + + +void setup() +{ + uint32_t available_PSRAM_size; + uint32_t new_available_PSRAM_size; + + //WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector + pinMode(REDLED, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + ARsetDTLED(REDLED); //setup LED pin for data transfer indicator + + digitalWrite(NSS, HIGH); + pinMode(NSS, OUTPUT); //disable LoRa device for now + + YModemSerial.begin(115200); + Monitorport.begin(115200, SERIAL_8N1, RXD2, TXD2); //monitor port, format is Monitorport.begin(baud-rate, protocol, RX pin, TX pin); + Monitorport.println(); + Monitorport.println(__FILE__); + + if (psramInit()) + { + Monitorport.println("PSRAM is correctly initialised"); + available_PSRAM_size = ESP.getFreePsram(); + Monitorport.println((String)"PSRAM Size available: " + available_PSRAM_size); + } + else + { + Monitorport.println("PSRAM not available"); + while (1); + } + + Monitorport.println("Allocate array in PSRAM"); + uint8_t *byte_array = (uint8_t *) ps_malloc(ARDTarraysize * sizeof(uint8_t)); + PSRAMptr = byte_array; //save the pointe to byte_array to global pointer + + new_available_PSRAM_size = ESP.getFreePsram(); + Monitorport.println((String)"PSRAM Size available: " + new_available_PSRAM_size); + Monitorport.print("PSRAM array bytes allocated: "); + Monitorport.println(available_PSRAM_size - new_available_PSRAM_size); + Monitorport.println(); +} diff --git a/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/Settings.h b/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/Settings.h new file mode 100644 index 0000000..7c0ec33 --- /dev/null +++ b/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/Settings.h @@ -0,0 +1,69 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 12 //select on LoRa device +#define NRESET 14 //reset pin on LoRa device +#define RFBUSY 15 //busy pin on LoRa device +#define SCK 4 //SCK on SPI3 +#define MISO 13 //MISO on SPI3 +#define MOSI 2 //MOSI on SPI3 +#define REDLED 33 //pin number for ESP32CAM on board red LED, set logic level low for on + +#define RXD2 40 //RX pin for monitor port, not used +#define TXD2 33 //TX pin for monitor port + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +#define YModemSerial Serial +#define Monitorport Serial2 + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 3000000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after general packet actioned and ack sent +const uint32_t ACKdelaystartendmS = 25; //ms delay before ack sent at array start wrie and end write +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 25; //ms delay if there has been an duplicate segment or command receipt +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer + +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc + +const uint32_t ReceiveTimeoutmS = 60000; //mS waiting for array transfer before timeout +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 6 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t ARDTfilenamesize = 32; //size of DTfilename buffer used by array transfer functions +const uint32_t ARDTarraysize = 0x20000; //maximum file\array size to receive +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +const uint8_t StartAttempts = 2; //number of attempts to start transfer before a fail +const uint8_t SendAttempts = 5; //number of attempts carrying out a process before a restart + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/YModemArray.h b/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/YModemArray.h new file mode 100644 index 0000000..63ddd72 --- /dev/null +++ b/examples/SX128x_examples/Camera/242_StuartCAM_ESP32CAM_LoRa_Receiver_PCTransfer/YModemArray.h @@ -0,0 +1,280 @@ +// Code below taken from https://gist.github.com/zonque/0ae2dc8cedbcdbd9b933 +// file xymodem-mini.c + +// MarkD modifcations +// - uses 128 byte packets for low RAM devices +// - supports batch upload of several files at once + +//Revised code 02/01/22, changed for ESP32; +//sprintf(spfBuff, "%ld", numBytesStillToSend); to sprintf(spfBuff, "%lu", numBytesStillToSend); +//uint32_t numBytesStillToSend = 0; to long unsigned int numBytesStillToSend = 0; +//numBytesThisPacket = min( numBytesStillToSend, sizeof(yPacket.payload)); to numBytesThisPacket = min( (uint32_t) numBytesStillToSend, sizeof(yPacket.payload)); + +//Revised code 05/01/22, changed; +//Organised YModem transfer to operate from previously filled memor array rather then and SD file + +//Revised code 28/01/22 to stop transfer hanging if PC receiving YModem stops responding + +#define X_SOH 0x01 +#define X_STX 0x02 +#define X_ACK 0x06 +#define X_NAK 0x15 +#define X_EOT 0x04 + +const uint32_t transfertimeoutmS = 5000; +const uint8_t ackerrorlimit = 16; + + +struct yModemPacket { + uint8_t start; + uint8_t block; + uint8_t block_neg; + uint8_t payload[128]; + uint16_t crc; +} __attribute__((packed)); + +#define CRC_POLY 0x1021 + +static uint16_t crc_update(uint16_t crc_in, int incr) +{ + uint16_t _xor = crc_in >> 15; + uint16_t _out = crc_in << 1; + + if (incr) + _out++; + + if (_xor) + _out ^= CRC_POLY; + + return _out; +} + + +static uint16_t crc16(const uint8_t *data, uint16_t size) +{ + uint16_t crc, i; + + for (crc = 0; size > 0; size--, data++) + for (i = 0x80; i; i >>= 1) + crc = crc_update(crc, *data & i); + + for (i = 0; i < 16; i++) + crc = crc_update(crc, 0); + + return crc; +} + + +static uint16_t swap16(uint16_t in) +{ + return (in >> 8) | ((in & 0xff) << 8); +} + +// Main YModem code. +// filename is pointer to null terminated string +// set waitForReceiver to 1 so that the upload begins when TeraTerm is ready +// set batchMode to 0 when sending 1 file +// set batchMode to 1 for each file sent apart from the last one. + + +static uint32_t yModemSend(const char *filename, uint8_t *arraydata, uint32_t arraysize, int waitForReceiver, int batchMode ) +{ + //*filename is pointer to filename + //*arraydata is pointer to the transferred array + //arraysize is the lengrth of data to transfer + + //uint32_t numBytesStillToSend = 0; + long unsigned int numBytesStillToSend = 0; + uint16_t numBytesThisPacket = 0; + uint8_t skip_payload; + uint8_t doNextBlock; + uint8_t answer = 0; + char spfBuff[16]; + struct yModemPacket yPacket; + uint32_t index; + uint32_t arraybytescopied = 0; + uint32_t startmS; + + uint8_t ackerrors = 0; + + if (arraysize > ARDTarraysize) + { + Monitorport.println("Array too big"); + return 0; + } + + // get the size of the file and convert to an ASCII representation for header packet + numBytesStillToSend = arraysize; + sprintf(spfBuff, "%lu", numBytesStillToSend); + + // wait here for the receiving device to respond + if (waitForReceiver) + { + Monitorport.println("Waiting for receiver ping ..."); + while ( YModemSerial.available() ) YModemSerial.read(); + + startmS = millis(); + + do + { + if (YModemSerial.available()) answer = YModemSerial.read(); + } + while ((answer != 'C') && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + } + + if (answer != 'C') + { + Monitorport.println("Timeout starting YModem transfer"); + return 0; + } + else + { + Monitorport.println("done"); + } + + Monitorport.print("Ymodem Sending "); + Monitorport.println(filename); + Monitorport.print(spfBuff); + Monitorport.println(" bytes"); + yPacket.start = X_SOH; + yPacket.block = 0; + + // copy the filename into the payload - fill remainder of payload with 0x00 + strncpy((char *) yPacket.payload, filename, sizeof(yPacket.payload)); + + // insert the file size in bytes as ASCII after the NULL of the filename string + strcpy( (char *)(yPacket.payload) + strlen(filename) + 1 , spfBuff ); //spfBuff is file size ? + + // first pass - don't read any file data as it will overwrite the file details packet + skip_payload = 1; + + while (numBytesStillToSend > 0) + { + doNextBlock = 0; + + // if this isn't the 1st pass, then read a block of up to 128 bytes from the file + if (skip_payload == 0) + { + numBytesThisPacket = min( (uint32_t) numBytesStillToSend, sizeof(yPacket.payload)); + + //now fill yPacket.payload with numBytesThisPacket from arraydata + for (index = 0; index < numBytesThisPacket; index++) + { + yPacket.payload[index] = arraydata[arraybytescopied]; + arraybytescopied++; + } + + if (numBytesThisPacket < sizeof(yPacket.payload)) + { + //pad out the rest of the payload block with 0x1A + memset(yPacket.payload + numBytesThisPacket, 0x1A, sizeof(yPacket.payload) - numBytesThisPacket); + } + } + + yPacket.block_neg = 0xff - yPacket.block; + + //calculate and insert the CRC16 checksum into the packet + yPacket.crc = swap16(crc16(yPacket.payload, sizeof(yPacket.payload))); + + //send the whole packet to the receiver - will block here + YModemSerial.write( (uint8_t*) &yPacket, sizeof(yPacket)); + + // wait for the receiver to send back a response to the packet + startmS = millis(); + while ((!YModemSerial.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + if (!YModemSerial.available()) + { + Monitorport.println("Timeout waiting YModem response"); + return 0; + } + + answer = YModemSerial.read(); + + switch (answer) + { + case X_NAK: + //something went wrong - send the same packet again? + Monitorport.print("N"); + ackerrors++; + break; + + case X_ACK: + //got ACK to move to the next block of data + Monitorport.print("."); + doNextBlock = 1; + break; + + default: + //unknown response + Monitorport.print("?"); + ackerrors++; + break; + } + + if (ackerrors >= ackerrorlimit) + { + Monitorport.println("Ack error limit reached"); + return 0; + } + + // need to handle the 'C' response after the initial file details packet has been sent + if (skip_payload == 1) + { + startmS = millis(); + while ((!YModemSerial.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + if (!YModemSerial.available()) + { + Monitorport.println("Timeout waiting YModem response"); + return 0; + } + + answer = YModemSerial.read(); + if (answer == 'C') + { + // good - start sending the data in the next transmission + skip_payload = 0; + } + else + { + // error? send the file details packet again? + doNextBlock = 0; + } + } + + // move on to the next block of data + if (doNextBlock == 1) + { + yPacket.block++; + numBytesStillToSend = numBytesStillToSend - numBytesThisPacket; + } + } + + // all done - send the end of transmission code + YModemSerial.write(X_EOT); + + // need to send EOT again for YMODEM + startmS = millis(); + while ((!YModemSerial.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + if (!YModemSerial.available()) + { + Monitorport.println("Timeout waiting YModem response"); + return 0; + } + + answer = YModemSerial.read(); + YModemSerial.write(X_EOT); + + if (batchMode == 0) + { + //and then a packet full of NULL seems to terminate the process + //and make TeraTerm close the receive dialog box + yPacket.block = 0; + yPacket.block_neg = 0xff - yPacket.block; + memset(yPacket.payload, 0x00, sizeof(yPacket.payload) ); + yPacket.crc = swap16(crc16(yPacket.payload, sizeof(yPacket.payload))); + YModemSerial.write( (uint8_t*)&yPacket, sizeof(yPacket)); + } + + Monitorport.println("done"); + return arraybytescopied; +} diff --git a/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/245_StuartCAM_LoRa_Receiver_PCTransfer.ino b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/245_StuartCAM_LoRa_Receiver_PCTransfer.ino new file mode 100644 index 0000000..0478a1c --- /dev/null +++ b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/245_StuartCAM_LoRa_Receiver_PCTransfer.ino @@ -0,0 +1,200 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program for receiving files transmitted by LoRa and saving them to SD card + and then optionally transfering them to a PC using a second serial port and the Ymodem protocol. Arduino + DUEs were used for testing the program, these have multiple hardware Serial ports. + + Progress messages on the transfer are sent to the IDE Serial monitor and printed on a connected ILI9341 + TFT display. + + For details of the PC upload process see here; + + https://stuartsprojects.github.io/2022/01/01/StuartCAM-ESP32CAM-Getting-the-Pictures-Onto-a-PC.html + + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#include +#include +#include + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets +#include "Settings.h" //LoRa settings etc. +SX128XLT LoRa; //create an SX128XLT library instance called LoRa, required by SDtransfer + +//#define SDLIB //define SDLIB for SD.h or SDFATLIB for SDfat.h +#define SDFATLIB +#include "DTSDlibrary.h" + +#include "Adafruit_GFX.h" //get library here > https://github.com/adafruit/Adafruit-GFX-Library +#include "Adafruit_ILI9341.h" //get library here > https://github.com/adafruit/Adafruit_ILI9341 +Adafruit_ILI9341 disp = Adafruit_ILI9341(DISPCS, DISPDC, DISPRESET); //for dispaly defaults, textsize and rotation, see Settings.h + +#define ENABLEMONITOR //enable monitor prints +#define PRINTSEGMENTNUM +#define ENABLEFILECRC //enable this define to uses and show file CRCs +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking +//#define DEBUG //see additional debug info +#define ENABLEPCTRANSFER //enable this define for YModem transfer to PC + +#include "SDtransferDisplay.h" //library of data transfer with display functions +#include "YModem.h" //YModem for the the save SD File + +uint32_t bytestransfered; //bytes transfered via YModem + + +void loop() +{ + SDreceiveaPacketDT(); + +#ifdef ENABLEPCTRANSFER + if (SDDTFileSaved) + { + Monitorport.println(F("File saved to SD - start YModem transfer to PC")); + setCursor(0, 4); + disp.print(F("Run YModem")); + digitalWrite(LED1, HIGH); + setCursor(0, 6); + disp.print(F(" ")); //clear segment number + + bytestransfered = yModemSend(SDDTfilenamebuff, 1, 1); + + if (bytestransfered > 0) + { + Monitorport.print(F("YModem transfer completed ")); + Monitorport.print(bytestransfered); + Monitorport.println(F(" bytes sent")); + } + else + { + Monitorport.println(F("YModem transfer FAILED")); + } + Monitorport.println(); + + setCursor(0, 4); + disp.print(F(" ")); + digitalWrite(LED1, LOW); + SDDTFileSaved = false; + //Monitorport.println(F("YModem transfer finished")); + //Monitorport.println(); + Monitorport.println(F("Wait for file")); + } +#endif +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + SDsetLED(LED1); //setup LED pin for data transfer indicator + + YModemSerial.begin(115200); + +#ifdef ENABLEMONITOR + Monitorport.begin(115200); + Monitorport.println(); + Monitorport.println(F(__FILE__)); +#endif + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("LoRa device error")); +#endif + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.print(F("Initializing SD card...")); +#endif + + if (DTSD_initSD(SDCS)) + { + Monitorport.println(F("SD Card initialized.")); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("SD Card failed, or not present.")); +#endif + while (1) led_Flash(100, 50); + } + +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Monitorport.println(F("Payload CRC disabled")); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Payload CRC enabled")); +#endif + } + + disp.begin(); + disp.fillScreen(ILI9341_BLACK); + disp.setTextColor(ILI9341_WHITE, ILI9341_BLACK); + disp.setRotation(rotation); + disp.setTextSize(textscale); + setCursor(0, 0); + disp.print(F("Waiting File")); + + SDDTSegmentNext = 0; + SDDTFileOpened = false; + SDDTFileSaved = false; //file has not been saved to SD yet + +#ifdef ENABLEMONITOR + Monitorport.println(F("SDfile transfer receiver ready")); + Monitorport.println(); +#endif +} diff --git a/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/DTSDlibrary.h b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/DTSDlibrary.h new file mode 100644 index 0000000..c7c1fae --- /dev/null +++ b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/DTSDlibrary.h @@ -0,0 +1,461 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This code is supplied as is, it is up to the user of the program to decide if the program is suitable + for the intended purpose and free from errors. +*******************************************************************************************************/ + +#ifdef SDFATLIB +#include +SdFat SD; +File dataFile; //name the file instance needed for SD library routines +#endif + + +#ifdef SDLIB +#include +File dataFile; //name the file instance needed for SD library routines +#endif + +#ifndef Monitorport +#define Monitorport Serial //output to Serial if no other port defined +#endif + +bool DTSD_dumpFileASCII(char *buff); +bool DTSD_dumpFileHEX(char *buff); +bool DTSD_dumpSegmentHEX(uint8_t segmentsize); +bool DTSD_initSD(uint8_t CSpin); +uint32_t DTSD_getFileSize(char *buff); +void DTSD_printDirectory(); +uint32_t DTSD_openFileRead(char *buff); +uint16_t DTSD_getNumberSegments(uint32_t filesize, uint8_t segmentsize); +uint8_t DTSD_getLastSegmentSize(uint32_t filesize, uint8_t segmentsize); +bool DTSD_openNewFileWrite(char *buff); +bool DTSD_openFileWrite(char *buff, uint32_t position); +uint8_t DTSD_readFileSegment(uint8_t *buff, uint8_t segmentsize); +uint8_t DTSD_writeSegmentFile(uint8_t *buff, uint8_t segmentsize); +void DTSD_seekFileLocation(uint32_t position); +uint16_t DTSD_createFile(char *buff); +uint16_t DTSD_fileCRCCCITT(); +void DTSD_fileFlush(); +void DTSD_closeFile(); +void printDirectorySD(File dir, int numTabs); + + +bool DTSD_dumpFileASCII(char *buff) +{ + + File dataFile = SD.open(buff); //open the test file note that only one file can be open at a time, + + if (dataFile) //if the file is available, read from it + { + while (dataFile.available()) + { + Monitorport.write(dataFile.read()); + } + dataFile.close(); + return true; + } + else + { + return false; + } +} + + +bool DTSD_dumpFileHEX(char *buff) +{ + //Note, this function will return true if the SD card is removed. + uint16_t Loopv1, Loopv2; + uint8_t fileData; + uint32_t filesize; + + if (!SD.exists(buff)) + { + return false; + } + + dataFile = SD.open(buff); + filesize = dataFile.size(); + filesize--; //file data locations are from 0 to (filesize -1); + Monitorport.print(F("Lcn 0 1 2 3 4 5 6 7 8 9 A B C D E F")); + Monitorport.println(); + + if (dataFile) //if the file is available, read from it + { + while (dataFile.available()) + { + for (Loopv1 = 0; Loopv1 <= filesize;) + { + Monitorport.print(F("0x")); + if (Loopv1 < 0x10) + { + Monitorport.print(F("0")); + } + Monitorport.print((Loopv1), HEX); + Monitorport.print(F(" ")); + for (Loopv2 = 0; Loopv2 <= 15; Loopv2++) + { + fileData = dataFile.read(); + if (fileData < 0x10) + { + Monitorport.print(F("0")); + } + Monitorport.print(fileData, HEX); + Monitorport.print(F(" ")); + Loopv1++; + } + Monitorport.println(); + } + } + dataFile.close(); + return true; + } + else + { + Monitorport.println(F("File not available")); + return false; + } +} + + +bool DTSD_initSD(uint8_t CSpin) +{ + if (SD.begin(CSpin)) + { + return true; + } + else + { + return false; + } +} + + +uint32_t DTSD_getFileSize(char *buff) +{ + uint32_t filesize; + + if (!SD.exists(buff)) + { + return 0; + } + + dataFile = SD.open(buff); + filesize = dataFile.size(); + dataFile.close(); + return filesize; +} + + +#ifdef SDFATLIB +void DTSD_printDirectory() +{ + dataFile = SD.open("/"); + Monitorport.println(F("Card directory")); + SD.ls("/", LS_R); +} +#endif + + +#ifdef SDLIB +void DTSD_printDirectory() +{ + dataFile = SD.open("/"); + + printDirectorySD(dataFile, 0); + + Monitorport.println(); +} + + +void printDirectorySD(File dir, int numTabs) +{ + + while (true) + { + File entry = dir.openNextFile(); + + if (! entry) + { + //no more files + break; + } + + for (uint8_t i = 0; i < numTabs; i++) + { + Monitorport.print('\t'); + } + + Monitorport.print(entry.name()); + + if (entry.isDirectory()) + { + Monitorport.println("/"); + printDirectorySD(entry, numTabs + 1); + } + else + { + // files have sizes, directories do not + Monitorport.print("\t\t"); + Monitorport.println(entry.size(), DEC); + } + entry.close(); + } +} +#endif + + +bool DTSD_dumpSegmentHEX(uint8_t segmentsize) +{ + uint16_t Loopv1, Loopv2; + uint8_t fileData; + + Monitorport.print(F("Print segment of ")); + Monitorport.print(segmentsize); + Monitorport.println(F(" bytes")); + Monitorport.print(F("Lcn 0 1 2 3 4 5 6 7 8 9 A B C D E F")); + Monitorport.println(); + + if (dataFile) //if the file is available, read from it + { + for (Loopv1 = 0; Loopv1 < segmentsize;) + { + Monitorport.print(F("0x")); + if (Loopv1 < 0x10) + { + Monitorport.print(F("0")); + } + Monitorport.print((Loopv1), HEX); + Monitorport.print(F(" ")); + for (Loopv2 = 0; Loopv2 <= 15; Loopv2++) + { + //stop printing if all of segment has been printed + if (Loopv1 < segmentsize) + { + fileData = dataFile.read(); + if (fileData < 0x10) + { + Monitorport.print(F("0")); + } + Monitorport.print(fileData, HEX); + Monitorport.print(F(" ")); + Loopv1++; + } + } + Monitorport.println(); + } + return true; + } + else + { + return false; + } +} + + +uint32_t DTSD_openFileRead2(char *buff) +{ + uint32_t filesize; + + dataFile = SD.open(buff); + filesize = dataFile.size(); + dataFile.seek(0); + return filesize; +} + + +uint32_t DTSD_openFileRead(char *buff) +{ + uint32_t filesize; + + if (SD.exists(buff)) + { + //Monitorport.println(F("File exists")); + dataFile = SD.open(buff); + filesize = dataFile.size(); + dataFile.seek(0); + return filesize; + } + else + { + //Monitorport.println(F("File does not exist")); + return 0; + } +} + + +uint16_t DTSD_getNumberSegments(uint32_t filesize, uint8_t segmentsize) +{ + uint16_t segments; + segments = filesize / segmentsize; + + if ((filesize % segmentsize) > 0) + { + segments++; + } + return segments; +} + + +uint8_t DTSD_getLastSegmentSize(uint32_t filesize, uint8_t segmentsize) +{ + uint8_t lastsize; + + lastsize = filesize % segmentsize; + if (lastsize == 0) + { + lastsize = segmentsize; + } + return lastsize; +} + + +bool DTSD_openNewFileWrite(char *buff) +{ + if (SD.exists(buff)) + { + //Monitorport.print(buff); + //Monitorport.println(F(" File exists - deleting")); + SD.remove(buff); + } + + if (dataFile = SD.open(buff, FILE_WRITE)) + { + //Monitorport.print(buff); + //Monitorport.println(F(" SD File opened")); + return true; + } + else + { + //Monitorport.print(buff); + //Monitorport.println(F(" ERROR opening file")); + return false; + } +} + + +bool DTSD_openFileWrite(char *buff, uint32_t position) +{ + dataFile = SD.open(buff, FILE_WRITE); //seems to operate as append + dataFile.seek(position); //seek to first position in file + + if (dataFile) + { + return true; + } + else + { + return false; + } +} + + +uint8_t DTSD_readFileSegment(uint8_t *buff, uint8_t segmentsize) +{ + uint8_t index = 0; + uint8_t fileData; + + while (index < segmentsize) + { + fileData = (uint8_t) dataFile.read(); + buff[index] = fileData; + index++; + }; + + if (index == segmentsize) + { + return segmentsize; //if all written return segment size + } + else + { + return index - 1; //if not all written return number bytes read + } +} + + +uint8_t DTSD_writeSegmentFile(uint8_t *buff, uint8_t segmentsize) +{ + uint8_t index, byteswritten = 0; + + for (index = 0; index < segmentsize; index++) + { + dataFile.write(buff[index]); + byteswritten++; + } + return byteswritten; +} + + +void DTSD_seekFileLocation(uint32_t position) +{ + dataFile.seek(position); //seek to position in file + return; +} + + +uint16_t DTSD_createFile(char *buff) +{ + //creats a new filename use this definition as the base; + //char filename[] = "/SD0000.txt"; //filename used as base for creating logfile, 0000 replaced with numbers + //the 0000 in the filename is replaced with the next number avaialable + + uint16_t index; + + for (index = 1; index <= 9999; index++) { + buff[3] = index / 1000 + '0'; + buff[4] = ((index % 1000) / 100) + '0'; + buff[5] = ((index % 100) / 10) + '0'; + buff[6] = index % 10 + '0' ; + if (! SD.exists(buff)) + { + // only open a new file if it doesn't exist + dataFile = SD.open(buff, FILE_WRITE); + break; + } + } + + if (!dataFile) + { + return 0; + } + + return index; //return number of logfile created +} + + +uint16_t DTSD_fileCRCCCITT(uint32_t fsize) +{ + uint32_t index; + uint16_t CRCcalc; + uint8_t j, filedata; + + CRCcalc = 0xFFFF; //start value for CRC16 + + for (index = 0; index < fsize; index++) + { + filedata = dataFile.read(); + CRCcalc ^= (((uint16_t) filedata ) << 8); + for (j = 0; j < 8; j++) + { + if (CRCcalc & 0x8000) + CRCcalc = (CRCcalc << 1) ^ 0x1021; + else + CRCcalc <<= 1; + } + } + + return CRCcalc; +} + + +void DTSD_fileFlush() +{ + dataFile.flush(); +} + + +void DTSD_closeFile() +{ + dataFile.close(); //close local file +} diff --git a/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/SDtransferDisplay.h b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/SDtransferDisplay.h new file mode 100644 index 0000000..6eec3de --- /dev/null +++ b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/SDtransferDisplay.h @@ -0,0 +1,1557 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + The functions expect the calling sketch to create an instance called LoRa, so that functions + are called like this; LoRa.getSDTXNetworkID(). + + This code is supplied as is, it is up to the user of the program to decide if the program is suitable + for the intended purpose and free from errors. + + There is a copy of this file in the SX12XX-LoRa library \src folder, but the file can be copied to the + sketch folder and used locally. In this way its possible to carry out custom modifications. + + Extensive use is made of #defines to allow the monotoring and debug prints to serial monitor to be + turned off, this is to allow for the circumstance where the primary serial port is in use for serial + file transfers to a PC or similar. To see the serial prints you need to have this set of #defines; + + #define Monitorport Serial + #define ENABLEMONITOR + +*******************************************************************************************************/ +/* + ToDo: + +*/ + +//110122 added local function printArrayHEX(uint8_t *buff, uint32_t len) +//130122 Made variable and function names unique so that the array transfer routines can be used in the same program +//130122 Converted all Serial prints to Monitorport.print() format +//140322 added #ifdef ENABLEMONITOR to serial prints + +#define SDUNUSED(v) (void) (v) //add SDUNUSED(variable); to avoid compiler warnings + +#ifndef Monitorport +#define Monitorport Serial +#endif + +#include +//#define DEBUG //enable this define to print additional debug info for segment transfers + +uint8_t SDRXPacketL; //length of received packet +uint8_t SDRXPacketType; //type of received packet, segment write, ACK, NACK etc +uint8_t SDRXHeaderL; //length of header +int16_t SDPacketRSSI; //stores RSSI of received packet +int8_t SDPacketSNR; //stores signal to noise ratio of received packet +uint16_t SDAckCount; //keep a count of acks that are received within timeout period +uint16_t SDNoAckCount; //keep a count of acks not received within timeout period +uint16_t SDDTDestinationFileCRC; //CRC of complete file received +uint32_t SDDTDestinationFileLength; //length of file written on the destination\receiver +uint16_t SDDTSourceFileCRC; //CRC returned of the remote saved file +uint32_t SDDTSourceFileLength; //length of file at source\transmitter +uint32_t SDDTStartmS; //used for timeing transfers +uint16_t SDDTSegment = 0; //current segment number +char SDDTfilenamebuff[Maxfilenamesize]; //global buffer to store current filename +uint8_t SDDTheader[16]; //header array +uint8_t SDDTdata[245]; //data/segment array +uint8_t SDDTflags = 0; //Flags byte used to pass status information between nodes +int SDDTLED = -1; //pin number for indicator LED, if -1 then not used +uint16_t SDDTErrors; //used for tracking errors in the transfer process + +uint16_t SDTXNetworkID; //this is used to store the 'network' number from packet received, receiver must have the same networkID +uint16_t SDTXArrayCRC; //should contain CRC of data array transmitted +uint8_t SDTXPacketL; //length of transmitted packet +uint16_t SDLocalPayloadCRC; //for calculating the local data array CRC +uint8_t SDDTLastSegmentSize; //size of the last segment +uint16_t SDDTNumberSegments; //number of segments for a file transfer +uint16_t SDDTSentSegments; //count of segments sent +bool SDDTFileTransferComplete; //bool to flag file transfer complete +uint32_t SDDTSendmS; //used for timing transfers +float SDDTsendSecs; //seconds to transfer a file + +uint16_t SDRXErrors; //count of packets received with error +uint8_t SDRXFlags; //SDDTflags byte in header, could be used to control actions in TX and RX +uint8_t SDRXDataarrayL; //length of data array\segment +bool SDDTFileOpened; //bool to flag when file has been opened +bool SDDTFileSaved; //bool to flag when file has been saved to SD +uint16_t SDDTSegmentNext; //next segment expected +uint16_t SDDTReceivedSegments; //count of segments received +uint16_t SDDTSegmentLast; //last segment processed + + +//Transmitter mode functions +uint32_t SDsendFile(char *filename, uint8_t namelength); +bool SDstartFileTransfer(char *filename, uint8_t filenamesize); +bool SDsendSegments(); +bool SDsendFileSegment(uint16_t segnum, uint8_t segmentsize); +bool SDendFileTransfer(char *filename, uint8_t filenamesize); +void SDbuild_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void SDbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum); +void SDbuild_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void SDprintLocalFileDetails(); +void SDprintSeconds(); +void SDprintAckBrief(); +void SDprintAckReception(); +void SDprintACKdetail(); +void SDprintdata(uint8_t *dataarray, uint8_t arraysize); +void SDprintPacketHex(); +bool SDsendDTInfo(); +void SDbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen); + +//Receiver mode functions +bool SDreceiveaPacketDT(); +void SDreadHeaderDT(); +bool SDprocessPacket(uint8_t packettype); +void SDprintPacketDetails(); +bool SDprocessSegmentWrite(); +bool SDprocessFileOpen(uint8_t *filename, uint8_t filenamesize); +bool SDprocessFileClose(); +void SDprintPacketRSSI(); +void SDprintSourceFileDetails(); +void SDprintDestinationFileDetails(); + +//Common functions +void SDsetLED(int8_t pinnumber); +void SDprintheader(uint8_t *header, uint8_t headersize); +void SDprintReliableStatus(); +void setCursor(uint8_t lcol, uint8_t lrow); + +//bit numbers used by SDDTErrors (16bits) and RXErrors (first 8bits) +const uint8_t SDNoFileSave = 0; //bit number of SDDTErrors to set when no file save, to SD for example +const uint8_t SDNothingToSend = 1; //bit number of SDDTErrors to set when nothing to send or unable to send image\file +const uint8_t SDNoCamera = 1; //bit number of SDDTErrors to set when camera fails +const uint8_t SDSendFile = 2; //bit number of SDDTErrors to set when file SD file image\file send fail +const uint8_t SDSendArray = 2; //bit number of SDDTErrors to set when file array image\file send fail +const uint8_t SDNoACKlimit = 3; //bit number of SDDTErrors to set when NoACK limit reached +const uint8_t SDSendPacket = 4; //bit number of SDDTErrors to set when sending a packet fails or there is no ack + +const uint8_t SDStartTransfer = 11; //bit number of SDDTErrors to set when StartTransfer fails +const uint8_t SDSendSegments = 12; //bit number of SDDTErrors to set when SendSegments function fails +const uint8_t SDSendSegment = 13; //bit number of SDDTErrors to set when sending a single Segment send fails +const uint8_t SDOpeningFile = 14; //bit number of SDDTErrors to set when opening file fails +const uint8_t SDendTransfer = 15; //bit number of SDDTErrors to set when end transfer fails + + + +//************************************************ +//Transmit mode functions +//************************************************ + +uint32_t SDsendFile(char *filename, uint8_t namelength) +{ + //This routine allows the file transfer to be run with a function call of sendFile(filename, sizeof(filename)); + memcpy(SDDTfilenamebuff, filename, namelength); //copy the name of file into global filename array for use outside this function + + uint8_t localattempts = 0; + + SDDTErrors = 0; //clear all error flags + SDDTDestinationFileCRC = 0; + SDDTSourceFileCRC = 0; + SDDTDestinationFileLength = 0; + SDDTSourceFileLength = 0; + + do + { + localattempts++; + SDNoAckCount = 0; + SDDTStartmS = millis(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Send file attempt ")); + Monitorport.println(localattempts); +#endif + + //opens the local file to send and sets up transfer parameters + if (SDstartFileTransfer(filename, namelength)) + { +#ifdef ENABLEMONITOR + Monitorport.print(filename); + Monitorport.println(F(" opened OK on remote")); + SDprintLocalFileDetails(); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("********************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR opening file")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("********************")); +#endif + SDDTFileTransferComplete = false; + delay(2000); + continue; + } + + delay(FunctionDelaymS); + + if (!SDsendSegments()) + { +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR in SDsendSegments()")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("***********************")); + Monitorport.println(); +#endif + SDDTFileTransferComplete = false; + continue; + } + + delay(FunctionDelaymS); + + if (SDendFileTransfer(filename, namelength)) //send command to close remote file + { + SDDTSendmS = millis() - SDDTStartmS; //record time taken for transfer + beginarrayRW(SDDTheader, 4); + SDDTDestinationFileLength = arrayReadUint32(); +#ifdef ENABLEMONITOR + Monitorport.print(filename); + Monitorport.println(F(" closed OK on remote")); + Monitorport.print(F("Acknowledged remote destination file length ")); + Monitorport.println(SDDTDestinationFileLength); +#endif + + if (SDDTDestinationFileLength != SDDTSourceFileLength) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("*******************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR file lengths do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("*******************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File lengths match")); +#endif + } + +#ifdef ENABLEFILECRC + SDDTDestinationFileCRC = arrayReadUint16(); +#ifdef ENABLEMONITOR + Monitorport.print(F("Acknowledged remote destination file CRC 0x")); + Monitorport.println(SDDTDestinationFileCRC, HEX); +#endif + + if (SDDTDestinationFileCRC != SDDTSourceFileCRC) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("****************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR file CRCs do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("****************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File CRCs match")); +#endif + } +#endif + //end of ENABLEFILECRC + SDDTFileTransferComplete = true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("******************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR close remote file failed")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("******************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + } + while ((!SDDTFileTransferComplete) && (localattempts < StartAttempts)); + + SDDTsendSecs = (float) SDDTSendmS / 1000; + +#ifdef ENABLEMONITOR + Monitorport.print(F("StartAttempts ")); + Monitorport.println(localattempts); + Monitorport.print(F("SDNoAckCount ")); + Monitorport.println(SDNoAckCount); + Monitorport.print(F("Transmit time ")); + Monitorport.print(SDDTsendSecs, 3); + Monitorport.println(F("secs")); + Monitorport.print(F("Transmit rate ")); + Monitorport.print( (SDDTDestinationFileLength * 8) / (SDDTsendSecs), 0 ); + Monitorport.println(F("bps")); +#endif + + if (localattempts == StartAttempts) + { + bitSet(SDDTErrors, SDSendFile); + return 0; + } + + return SDDTDestinationFileLength; +} + + +bool SDstartFileTransfer(char *filename, uint8_t filenamesize) +{ + //Start file transfer, open local file first then remote file. + + uint8_t ValidACK; + uint8_t localattempts = 0; + +#ifdef ENABLEMONITOR + Monitorport.print(F("Start file transfer for ")); + Monitorport.println(filename); +#endif + SDDTSourceFileLength = DTSD_openFileRead(filename); //get the file length + + if (SDDTSourceFileLength == 0) + { +#ifdef ENABLEMONITOR + Monitorport.print(F("Error - opening file")); + Monitorport.println(filename); +#endif + bitSet(SDDTErrors, SDOpeningFile); + return false; + } + +#ifdef ENABLEFILECRC + SDDTSourceFileCRC = DTSD_fileCRCCCITT(SDDTSourceFileLength); //get file CRC from position 0 to end +#endif + + SDDTNumberSegments = DTSD_getNumberSegments(SDDTSourceFileLength, SegmentSize); + SDDTLastSegmentSize = DTSD_getLastSegmentSize(SDDTSourceFileLength, SegmentSize); + SDbuild_DTFileOpenHeader(SDDTheader, DTFileOpenHeaderL, filenamesize, SDDTSourceFileLength, SDDTSourceFileCRC, SegmentSize); + SDLocalPayloadCRC = LoRa.CRCCCITT((uint8_t *) filename, filenamesize, 0xFFFF); + + do + { + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.println(F("Send open remote file request")); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + SDTXPacketL = LoRa.transmitDT(SDDTheader, DTFileOpenHeaderL, (uint8_t *) filename, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDTXNetworkID = LoRa.getTXNetworkID(SDTXPacketL); //get the networkID appended to packet + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDTXPacketL); //get the payload CRC appended to packet + Monitorport.print(F("Send attempt ")); + Monitorport.println(localattempts); + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.println(SDTXNetworkID, HEX); + Monitorport.print(F("SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); +#endif +#endif + + //Monitorport.println(); + //LoRa.printSXBufferHEX(0, 254); + //Monitorport.println(); + //Monitorport.println(); + + if (SDTXPacketL == 0) //if there has been a send and ack error, SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDT(SDDTheader, DTFileOpenHeaderL, ACKopentimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if ((ValidACK > 0) && (SDRXPacketType == DTFileOpenACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F(" Valid ACK ")); +#endif +#endif + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#ifdef DEBUG + SDprintACKdetail(); + Monitorport.print(F(" ACKPacket ")); + SDprintPacketHex(); +#endif +#endif + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDStartTransfer); + return false; + } + + return true; +} + + +bool SDsendSegments() +{ + //Start the file transfer at segment 0 + SDDTSegment = 0; + SDDTSentSegments = 0; + + dataFile.seek(0); //ensure at first position in file + + while (SDDTSegment < (SDDTNumberSegments - 1)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintSeconds(); +#endif +#endif + + if (SDsendFileSegment(SDDTSegment, SegmentSize)) + { + SDDTSentSegments++; + } + else + { + bitSet(SDDTErrors, SDSendSegment); + return false; + } + delay(FunctionDelaymS); + }; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Last segment")); +#endif + + if (!SDsendFileSegment(SDDTSegment, SDDTLastSegmentSize)) + { + bitSet(SDDTErrors, SDSendSegment); + return false; + } + + return true; +} + + +bool SDsendFileSegment(uint16_t segnum, uint8_t segmentsize) +{ + //Send file segment as payload in a DT packet + + uint8_t ValidACK; + uint8_t localattempts = 0; + + DTSD_readFileSegment(SDDTdata, segmentsize); + SDbuild_DTSegmentHeader(SDDTheader, DTSegmentWriteHeaderL, segmentsize, segnum); + +#ifdef ENABLEMONITOR + +#ifdef PRINTSEGMENTNUM + Monitorport.println(segnum); +#endif + +#ifdef DEBUG + SDprintheader(SDDTheader, DTSegmentWriteHeaderL); + Monitorport.print(F(" ")); + SDprintdata(SDDTdata, 16); //print first 16 bytes data array +#endif + +#endif + + do + { + localattempts++; + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + SDTXPacketL = LoRa.transmitDT(SDDTheader, DTSegmentWriteHeaderL, (uint8_t *) SDDTdata, segmentsize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + if (SDTXPacketL == 0) //if there has been an error SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDT(SDDTheader, DTSegmentWriteHeaderL, ACKsegtimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if (ValidACK > 0) + { + if (SDRXPacketType == DTSegmentWriteNACK) + { + SDDTSegment = SDDTheader[4] + (SDDTheader[5] << 8); //load what the segment number should be + SDRXHeaderL = SDDTheader[2]; + DTSD_seekFileLocation(SDDTSegment * SegmentSize); +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************************")); + Monitorport.print(F("Received restart request at segment ")); + Monitorport.println(SDDTSegment); + SDprintheader(SDDTheader, SDRXHeaderL); + Monitorport.println(); + Monitorport.print(F("Seek to file location ")); + Monitorport.println(SDDTSegment * SegmentSize); + Monitorport.println(F("************************************")); + Monitorport.println(); + Monitorport.flush(); +#endif + + } + + //ack is valid, segment was acknowledged if here + + if (SDRXPacketType == DTStartNACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Received restart request")); +#endif + return false; + } + + if (SDRXPacketType == DTSegmentWriteACK) + { + SDAckCount++; + SDDTSegment++; //increase value for next segment +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintAckBrief(); +#endif +#endif + return true; + } + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#endif + + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } + } + } while ((ValidACK == 0) && (localattempts < SendAttempts)) ; + + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDSendSegment); + return 0; + } + + return true; +} + + +bool SDendFileTransfer(char *filename, uint8_t filenamesize) +{ + //End file transfer, close local file first then remote file + + uint8_t ValidACK; + uint8_t localattempts = 0; + + DTSD_closeFile(); + SDbuild_DTFileCloseHeader(SDDTheader, DTFileCloseHeaderL, filenamesize, SDDTSourceFileLength, SDDTSourceFileCRC, SegmentSize); + + do + { + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.println(F("Send close remote file")); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + SDTXPacketL = LoRa.transmitDT(SDDTheader, DTFileCloseHeaderL, (uint8_t *) filename, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + SDTXNetworkID = LoRa.getTXNetworkID(SDTXPacketL); + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDTXPacketL); + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.print(SDTXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Monitorport.print(F(",SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Monitorport.println(); +#endif +#endif + + if (SDTXPacketL == 0) //if there has been a send and ack error, SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDT(SDDTheader, DTFileCloseHeaderL, ACKclosetimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if ((ValidACK > 0) && (SDRXPacketType == DTFileCloseACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintPacketHex(); +#endif +#endif + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#endif + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(); + Monitorport.print(F(" ACKPacket ")); + SDprintPacketHex(); + Monitorport.println(); +#endif +#endif + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDendTransfer); + return 0; + } + + return true; +} + + +void SDbuild_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //This builds the header buffer for the filename to send + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileOpen); //byte 0, write the packet type + arrayWriteUint8(SDDTflags); //byte 1, SDDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void SDbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum) +{ + //This builds the header buffer for a segment transmit + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTSegmentWrite); //write the packet type + arrayWriteUint8(SDDTflags); //SDDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint16(segnum); //write the DTsegment number + endarrayRW(); +} + + +void SDbuild_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //This builds the header buffer for the filename passed + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileClose); //byte 0, write the packet type + arrayWriteUint8(SDDTflags); //byte 1, SDDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void SDprintLocalFileDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Source file length ")); + Monitorport.print(SDDTSourceFileLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEFILECRC + Monitorport.print(F("Source file CRC is 0x")); + Monitorport.println(SDDTSourceFileCRC, HEX); +#endif + Monitorport.print(F("Segment Size ")); + Monitorport.println(SegmentSize); + Monitorport.print(F("Number segments ")); + Monitorport.println(SDDTNumberSegments); + Monitorport.print(F("Last segment size ")); + Monitorport.println(SDDTLastSegmentSize); +#endif +} + + +void SDprintSeconds() +{ +#ifdef ENABLEMONITOR + float secs; + secs = ( (float) millis() / 1000); + Monitorport.print(secs, 2); + Monitorport.print(F(" ")); +#endif +} + + +void SDprintAckBrief() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.println(F("dBm")); +#endif +} + + +void SDprintAckReception() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + SDPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F("SDAckCount,")); + Monitorport.print(SDAckCount); + Monitorport.print(F(",SDNoAckCount,")); + Monitorport.print(SDNoAckCount); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm,AckSNR,")); + Monitorport.print(SDPacketSNR); + Monitorport.print(F("dB")); + Monitorport.println(); +#endif +} + + +void SDprintACKdetail() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("ACKDetail")); + Monitorport.print(F(",RXNetworkID,0x")); + Monitorport.print(LoRa.getRXNetworkID(SDRXPacketL), HEX); + Monitorport.print(F(",RXPayloadCRC,0x")); + Monitorport.print(LoRa.getRXPayloadCRC(SDRXPacketL), HEX); + Monitorport.print(F(",SDRXPacketL,")); + Monitorport.print(SDRXPacketL); + Monitorport.print(F(" ")); + SDprintReliableStatus(); + Monitorport.println(); +#endif +} + + +void SDprintdata(uint8_t *dataarray, uint8_t arraysize) +{ + SDUNUSED(dataarray); //to prevent a compiler warning + SDUNUSED(arraysize); //to prevent a compiler warning +#ifdef ENABLEMONITOR + Monitorport.print(F("DataBytes,")); + Monitorport.print(arraysize); + Monitorport.print(F(" ")); + printarrayHEX((uint8_t *) dataarray, 16); //There is a lot of data to print so only print first 16 bytes +#endif +} + + +bool SDsendDTInfo() +{ + //Send array info packet, for this implmentation its really only the flags in ARDTflags that is sent + + uint8_t ValidACK = 0; + uint8_t localattempts = 0; + + SDbuild_DTInfoHeader(SDDTheader, DTInfoHeaderL, 0); + + do + { + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.print(F("Send DTInfo packet attempt ")); + Monitorport.println(localattempts); +#endif + SDTXPacketL = LoRa.transmitDT(SDDTheader, DTInfoHeaderL, (uint8_t *) SDDTdata, 0, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (SDTXPacketL == 0) //if there has been an error ARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + continue; + } + + ValidACK = LoRa.waitACKDT(SDDTheader, DTInfoHeaderL, ACKsegtimeoutmS); + + if (ValidACK > 0) + { + //ack is a valid reliable packet + SDRXPacketType = SDDTheader[0]; +#ifdef ENABLEMONITOR + Monitorport.print(F("ACK Packet type 0x")); + Monitorport.println(SDRXPacketType, HEX); +#endif + + if (SDRXPacketType == DTInfoACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK received")); +#endif + SDAckCount++; + SDRXPacketType = SDDTheader[0]; + return true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK not received")); +#endif + return false; + } + + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("No valid ACK received ")); +#endif + + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + return false; + } + } + delay(PacketDelaymS); + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDSendPacket); + return false; + } + return true; +} + + +void SDbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen) +{ + //This builds the header buffer for a info only header, faults problems etc + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTInfo); //write the packet type + arrayWriteUint8(SDDTflags); //ARDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + endarrayRW(); +} + + +//************************************************ +//Receiver mode functions +//************************************************ + + +bool SDreceiveaPacketDT() +{ + //Receive Data transfer packets + + SDRXPacketType = 0; + SDRXPacketL = LoRa.receiveDT(SDDTheader, HeaderSizeMax, (uint8_t *) SDDTdata, DataSizeMax, NetworkID, RXtimeoutmS, WAIT_RX); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + +#ifdef DEBUG + SDprintSeconds(); +#endif + + if (SDRXPacketL > 0) + { + //if the LT.receiveDT() returns a value > 0 for SDRXPacketL then packet was received OK + SDreadHeaderDT(); //get the basic header details into global variables SDRXPacketType etc + SDprocessPacket(SDRXPacketType); //process and act on the packet + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return true; + } + else + { + //if the LoRa.receiveDT() function detects an error RXOK is 0 + uint16_t IRQStatus = LoRa.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("RX Timeout")); +#endif + } + else + { + SDRXErrors++; +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("PacketError")); + SDprintPacketDetails(); + SDprintReliableStatus(); + Monitorport.println(); +#endif +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + } + } + return false; +} + + +void SDreadHeaderDT() +{ + //The first 6 bytes of the header contain the important stuff, so load it up + //so we can decide what to do next. + beginarrayRW(SDDTheader, 0); //start buffer read at location 0 + SDRXPacketType = arrayReadUint8(); //load the packet type + SDRXFlags = arrayReadUint8(); //SDDTflags byte + SDRXHeaderL = arrayReadUint8(); //load the header length + SDRXDataarrayL = arrayReadUint8(); //load the datalength + SDDTSegment = arrayReadUint16(); //load the segment number +} + + +bool SDprocessPacket(uint8_t packettype) +{ + //Decide what to do with an incoming packet + + if (packettype == DTSegmentWrite) + { + SDprocessSegmentWrite(); + return true; + } + + if (packettype == DTFileOpen) + { + SDprocessFileOpen(SDDTdata, SDRXDataarrayL); + return true; + } + + if (packettype == DTFileClose) + { + SDprocessFileClose(); + return true; + } + + return true; +} + + +void SDprintPacketDetails() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + SDPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F(" RSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm")); + +#ifdef DEBUG + Monitorport.print(F(",SNR,")); + Monitorport.print(SDPacketSNR); + Monitorport.print(F("dBm,RXOKCount,")); + Monitorport.print(SDDTReceivedSegments); + Monitorport.print(F(",RXErrs,")); + Monitorport.print(SDRXErrors); + Monitorport.print(F(" RX")); + SDprintheader(SDDTheader, SDRXHeaderL); +#endif +#endif +} + + +bool SDprocessSegmentWrite() +{ + //There is a request to write a segment to file on receiver + //checks that the sequence of segment writes is correct + + if (!SDDTFileOpened) + { + //something is wrong, have received a request to write a segment but there is no file opened + //need to reject the segment write with a restart NACK +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("***************************************************")); + Monitorport.println(F("Error - Segment write with no file open - send NACK")); + Monitorport.println(F("***************************************************")); + Monitorport.println(); +#endif + SDDTheader[0] = DTStartNACK; + delay(ACKdelaymS); + delay(DuplicatedelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTStartHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return false; + } + + if (SDDTSegment == SDDTSegmentNext) + { + DTSD_writeSegmentFile(SDDTdata, SDRXDataarrayL); + +#ifdef ENABLEMONITOR +#ifdef PRINTSEGMENTNUM + //Monitorport.print(F("Segment,")); + Monitorport.println(SDDTSegment); + setCursor(0, 6); + disp.print(SDDTSegment); +#endif + +#ifdef DEBUG + Monitorport.print(F("Bytes,")); + Monitorport.print(SDRXDataarrayL); + SDprintPacketRSSI(); + Monitorport.println(F(" SendACK")); +#endif +#endif + SDDTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTSegmentWriteHeaderL, TXpower); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + SDDTReceivedSegments++; + SDDTSegmentLast = SDDTSegment; //so we can tell if sequece has been received twice + SDDTSegmentNext = SDDTSegment + 1; + return true; + } + + if (SDDTSegment == SDDTSegmentLast) + { +#ifdef ENABLEMONITOR + Monitorport.print(F("ERROR segment ")); + Monitorport.print(SDDTSegment); + Monitorport.println(F(" already received ")); + delay(DuplicatedelaymS); +#ifdef DEBUG + SDprintPacketDetails(); + SDprintPacketRSSI(); +#endif +#endif + SDDTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTSegmentWriteHeaderL, TXpower); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return true; + } + + if (SDDTSegment != SDDTSegmentNext ) + { + SDDTheader[0] = DTSegmentWriteNACK; + SDDTheader[4] = lowByte(SDDTSegmentNext); + SDDTheader[5] = highByte(SDDTSegmentNext); + delay(ACKdelaymS); + delay(DuplicatedelaymS); //add an extra delay here to stop repeated segment sends + +#ifdef ENABLEMONITOR + Monitorport.print(F(" ERROR Received Segment ")); + Monitorport.print(SDDTSegment); + Monitorport.print(F(" expected ")); + Monitorport.print(SDDTSegmentNext); + Monitorport.print(F(" ")); + +#ifdef DEBUG + SDprintPacketDetails(); + SDprintPacketRSSI(); +#endif + + Monitorport.print(F(" Send NACK for segment ")); + Monitorport.print(SDDTSegmentNext); + Monitorport.println(); + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.print(F("Transmit restart request for segment ")); + Monitorport.println(SDDTSegmentNext); + SDprintheader(SDDTheader, SDRXHeaderL); + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.println(); + Monitorport.flush(); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTSegmentWriteHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return false; + } + + return true; +} + + +bool SDprocessFileOpen(uint8_t *filename, uint8_t filenamesize) +{ + //There is a request to open local file on receiver + + SDDTDestinationFileCRC = 0; //CRC of complete file received + SDDTDestinationFileLength = 0; //length of file written on the destination\receiver + + beginarrayRW(SDDTheader, 4); //start buffer read at location 4 + SDDTSourceFileLength = arrayReadUint32(); //load the file length of the remote file being sent + SDDTSourceFileCRC = arrayReadUint16(); //load the CRC of the source file being sent + memset(SDDTfilenamebuff, 0, Maxfilenamesize); //clear SDDTfilenamebuff to all 0s + memcpy(SDDTfilenamebuff, filename, filenamesize); //copy received SDDTdata into SDDTfilenamebuff + +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.print(F(" SD File Open request")); + Monitorport.println(); + +#ifdef DEBUG + SDTXNetworkID = LoRa.getTXNetworkID(SDRXPacketL); //get the networkID appended to packet + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDRXPacketL); //get the payload CRC appended to packet + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.println(SDTXNetworkID, HEX); + Monitorport.print(F("SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); +#endif + + SDprintSourceFileDetails(); + + if bitRead(SDRXFlags, SDNoFileSave) + { + Monitorport.println(F("Remote did not save file to SD")); + } +#endif + + if (DTSD_openNewFileWrite(SDDTfilenamebuff)) //open file for write at beginning, delete if it exists + { +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" DT File Opened OK")); + Monitorport.println(F("Waiting File")); +#endif + SDDTSegmentNext = 0; //since file is opened the next sequence should be the first + SDDTFileOpened = true; + SDDTStartmS = millis(); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" File Open fail")); +#endif + SDDTFileOpened = false; + return false; + } + + setCursor(0, 0); + disp.print(F("Receiving File ")); + setCursor(0, 1); + disp.print(F(" ")); //clear previous filename + setCursor(0, 1); + disp.print(SDDTfilenamebuff); + setCursor(0, 2); + disp.print(F(" ")); //clear previous file size + setCursor(0, 2); + disp.print(SDDTSourceFileLength); + disp.print(F(" bytes")); + setCursor(0, 3); + disp.print(F(" ")); //clear transfer time + setCursor(0, 5); + disp.print(F(" ")); + + SDDTStartmS = millis(); + delay(ACKdelaymS); + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("Sending ACK")); +#endif +#endif + + SDDTheader[0] = DTFileOpenACK; //set the ACK packet type + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTFileOpenHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + SDDTSegmentNext = 0; //after file open, segment 0 is next + + return true; +} + + +bool SDprocessFileClose() +{ + // There is a request to close a file on SD of receiver + +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" File close request")); +#endif + + SDDTFileSaved = false; + + if (SDDTFileOpened) //check if file has been opened, close it if it is + { + if (SD.exists(SDDTfilenamebuff)) //check if file exists + { + DTSD_closeFile(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Transfer time ")); + Monitorport.print(millis() - SDDTStartmS); + Monitorport.print(F("mS")); + Monitorport.println(); + Monitorport.println(F("File closed")); +#endif + + setCursor(0, 0); + disp.print(F("File Received ")); + setCursor(0, 3); + disp.print(F(" ")); + setCursor(0, 3); + disp.print(millis() - SDDTStartmS); + disp.print(F(" mS")); + + SDDTFileOpened = false; + SDDTDestinationFileLength = DTSD_openFileRead(SDDTfilenamebuff); + +#ifdef ENABLEFILECRC + SDDTDestinationFileCRC = DTSD_fileCRCCCITT(SDDTDestinationFileLength); +#endif + + beginarrayRW(SDDTheader, 4); //start writing to array at location 12 + arrayWriteUint32(SDDTDestinationFileLength); //write file length of file just written just written to ACK header + arrayWriteUint16(SDDTDestinationFileCRC); //write CRC of file just written to ACK header + +#ifdef ENABLEMONITOR + SDprintDestinationFileDetails(); +#endif + } + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File already closed")); +#endif + delay(DuplicatedelaymS); + } + + delay(ACKdelaymS); +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("Sending ACK")); +#endif +#endif + SDDTheader[0] = DTFileCloseACK; + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTFileCloseHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + + SDDTFileSaved = true; + return true; +} + + +void SDprintPacketRSSI() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + Monitorport.print(F(" RSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm")); +#endif +} + + +void SDprintSourceFileDetails() +{ + +#ifdef ENABLEMONITOR + Monitorport.print(F("Source file length is ")); + Monitorport.print(SDDTSourceFileLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEFILECRC + Monitorport.print(F("Source file CRC is 0x")); + Monitorport.println(SDDTSourceFileCRC, HEX); +#endif +#endif +} + + +void SDprintDestinationFileDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Destination file length ")); + Monitorport.print(SDDTDestinationFileLength); + Monitorport.println(F(" bytes")); + if (SDDTDestinationFileLength != SDDTSourceFileLength) + { + Monitorport.println(F("ERROR - file lengths do not match")); + } + else + { + Monitorport.println(F("File lengths match")); + } + +#ifdef ENABLEFILECRC + Monitorport.print(F("Destination file CRC is 0x")); + Monitorport.println(SDDTDestinationFileCRC, HEX); + if (SDDTDestinationFileCRC != SDDTSourceFileCRC) + { + Monitorport.println(F("ERROR - file CRCs do not match")); + } + else + { + Monitorport.println(F("File CRCs match")); + } +#endif + +#endif +} + +//************************************************ +//Common functions +//************************************************ + +void SDsetLED(int8_t pinnumber) +{ + if (pinnumber >= 0) + { + SDDTLED = pinnumber; + pinMode(pinnumber, OUTPUT); + } +} + + +void SDprintheader(uint8_t *header, uint8_t headersize) +{ + SDUNUSED(header); //to prevent a compiler warning + SDUNUSED(headersize); //to prevent a compiler warning + +#ifdef ENABLEMONITOR + Monitorport.print(F("HeaderBytes,")); + Monitorport.print(headersize); + Monitorport.print(F(" ")); + printarrayHEX(header, headersize); +#endif +} + + +void printArrayHEX(uint8_t *buff, uint32_t len) +{ + SDUNUSED(buff); //to prevent a compiler warning + SDUNUSED(len); //to prevent a compiler warning + +#ifdef ENABLEMONITOR + uint8_t index, buffdata; + for (index = 0; index < len; index++) + { + buffdata = buff[index]; + if (buffdata < 16) + { + Monitorport.print(F("0")); + } + Monitorport.print(buffdata, HEX); + Monitorport.print(F(" ")); + } +#endif +} + + +void SDprintReliableStatus() +{ + +#ifdef ENABLEMONITOR + + uint8_t reliableErrors = LoRa.readReliableErrors(); + uint8_t reliableFlags = LoRa.readReliableFlags(); + + if (bitRead(reliableErrors, ReliableCRCError)) + { + Monitorport.print(F(",ReliableCRCError")); + } + + if (bitRead(reliableErrors, ReliableIDError)) + { + Monitorport.print(F(",ReliableIDError")); + } + + if (bitRead(reliableErrors, ReliableSizeError)) + { + Monitorport.print(F(",ReliableSizeError")); + } + + if (bitRead(reliableErrors, ReliableACKError)) + { + Monitorport.print(F(",NoReliableACK")); + } + + if (bitRead(reliableErrors, ReliableTimeout)) + { + Monitorport.print(F(",ReliableTimeout")); + } + + if (bitRead(reliableFlags, ReliableACKSent)) + { + Monitorport.print(F(",ACKsent")); + } + + if (bitRead(reliableFlags, ReliableACKReceived)) + { + Monitorport.print(F(",ACKreceived")); + } +#endif +} + + +void SDprintPacketHex() +{ +#ifdef ENABLEMONITOR + uint8_t packetlen = LoRa.readRXPacketL(); + Monitorport.print(packetlen); + Monitorport.print(F(" bytes > ")); + if (packetlen > 0) + { + LoRa.printSXBufferHEX(0, packetlen - 1); + } +#endif +} + + +void setCursor(uint8_t lcol, uint8_t lrow) +{ + disp.setCursor((lcol * 6 * textscale), (lrow * 9 * textscale)); +} diff --git a/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/Settings.h b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/Settings.h new file mode 100644 index 0000000..c44b9ab --- /dev/null +++ b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/Settings.h @@ -0,0 +1,71 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done +#define LED1 8 //On board LED, high for on +#define SDCS 30 //select pin for SD card + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +#define DISPCS 23 //CS for ILI9341 +#define DISPDC 24 //DC for ILI9341 +#define DISPRESET 25 //RESET for ILI9341 +#define TOUCHCS 29 //ILI9341 may have touch ICs, so we need to disable it, set to -1 if not fitted + +#define YModemSerial Serial2 //define Serial port to use for Ymodem transfer here +#define Monitorport Serial //define Hardware Serial monitor port to use for monitoring + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 10; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t Maxfilenamesize = 32; //size of DTfilename buffer +const uint8_t SendAttempts = 10; //number of attempts sending a packet or attempting a process before a restart of transfer +const uint8_t StartAttempts = 10; //number of attempts sending the file + +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif + + +//******* ILI9341 Display settings here *************** + +const uint8_t textscale = 3; +const byte rotation = 1; diff --git a/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/YModem.h b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/YModem.h new file mode 100644 index 0000000..802abed --- /dev/null +++ b/examples/SX128x_examples/Camera/245_StuartCAM_LoRa_Receiver_PCTransfer/YModem.h @@ -0,0 +1,264 @@ +//29/12/21 Working for repeat transfer - source of this version not clear +// Code below taken from https://gist.github.com/zonque/0ae2dc8cedbcdbd9b933 +// file xymodem-mini.c + +// MarkD modifcations +// - uses 128 byte packets for low RAM devices +// - supports batch upload of several files at once + +//StuartsProjects - hard coded for Serial2 +//080222 - added timeouts + +#define X_SOH 0x01 +#define X_STX 0x02 +#define X_ACK 0x06 +#define X_NAK 0x15 +#define X_EOT 0x04 + +const uint32_t transfertimeoutmS = 5000; +const uint8_t ackerrorlimit = 16; + +struct yModemPacket { + uint8_t start; + uint8_t block; + uint8_t block_neg; + uint8_t payload[128]; + uint16_t crc; +} __attribute__((packed)); + +#define CRC_POLY 0x1021 + +static uint16_t crc_update(uint16_t crc_in, int incr) +{ + uint16_t _xor = crc_in >> 15; + uint16_t _out = crc_in << 1; + + if (incr) + _out++; + + if (_xor) + _out ^= CRC_POLY; + + return _out; +} + +static uint16_t crc16(const uint8_t *data, uint16_t size) +{ + uint16_t crc, i; + + for (crc = 0; size > 0; size--, data++) + for (i = 0x80; i; i >>= 1) + crc = crc_update(crc, *data & i); + + for (i = 0; i < 16; i++) + crc = crc_update(crc, 0); + + return crc; +} + + +static uint16_t swap16(uint16_t in) +{ + return (in >> 8) | ((in & 0xff) << 8); +} + +// Main YModem code. +// filename is pointer to null terminated string +// set waitForReceiver to 1 so that the upload begins when TeraTerm is ready +// set batchMode to 0 when sending 1 file +// set batchMode to 1 for each file sent apart from the last one. + +static int yModemSend(const char *filename, int waitForReceiver, int batchMode ) +{ + uint32_t numBytesStillToSend = 0; + uint16_t numBytesThisPacket = 0; + uint8_t skip_payload; + uint8_t doNextBlock; + uint8_t answer = 0; + char spfBuff[16]; + struct yModemPacket yPacket; + uint32_t filebytescopied = 0; + uint32_t startmS; + uint8_t ackerrors = 0; + + File srcFile = SD.open(filename, O_RDONLY); + + if (srcFile < 0) + { + Serial.println("Open error"); + return 0; + } + + numBytesStillToSend = srcFile.size(); + + if (numBytesStillToSend == 0) + { + Serial.println("SD file error"); + return 0; + } + + //convert the size of the file to an ASCII representation for header packet + sprintf(spfBuff, "%ld", numBytesStillToSend); + + // wait here for the receiving device to respond + if ( waitForReceiver ) + { + Serial.print("Waiting for receiver ping ... "); + while ( Serial2.available() ) Serial2.read(); + startmS = millis(); + + do + { + if ( Serial2.available() ) answer = Serial2.read(); + } + while ((answer != 'C') && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + + if (answer != 'C') + { + Serial.println("Timeout starting YModem transfer"); + return 0; + } + else + { + Serial.println("done"); + } + } + + Serial.print("YModem Sending "); + Serial.print(filename); + Serial.print(" "); + Serial.print(spfBuff); + Serial.println(" bytes"); + + yPacket.start = X_SOH; + yPacket.block = 0; + + // copy the filename into the payload - fill remainder of payload with 0x00 + strncpy((char *) yPacket.payload, filename, sizeof(yPacket.payload)); + + // insert the file size in bytes as ASCII after the NULL of the filename string + strcpy( (char *)(yPacket.payload) + strlen(filename) + 1 , spfBuff ); + + // first pass - don't read any file data as it will overwrite the file details packet + skip_payload = 1; + + while (numBytesStillToSend > 0) + { + doNextBlock = 0; + + // if this isn't the 1st pass, then read a block of up to 128 bytes from the file + if (skip_payload == 0) + { + numBytesThisPacket = min(numBytesStillToSend, sizeof(yPacket.payload)); + srcFile.read(yPacket.payload, numBytesThisPacket); + + filebytescopied = filebytescopied + numBytesThisPacket; + + if (numBytesThisPacket < sizeof(yPacket.payload)) { + // pad out the rest of the payload block with 0x1A + memset(yPacket.payload + numBytesThisPacket, 0x1A, sizeof(yPacket.payload) - numBytesThisPacket); + } + } + + yPacket.block_neg = 0xff - yPacket.block; + + // calculate and insert the CRC16 checksum into the packet + yPacket.crc = swap16(crc16(yPacket.payload, sizeof(yPacket.payload))); + + // send the whole packet to the receiver - will block here + startmS = millis(); + Serial2.write( (uint8_t*)&yPacket, sizeof(yPacket)); + + // wait for the receiver to send back a response to the packet + while ((!Serial2.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + + if ( ((uint32_t) (millis() - startmS) >= transfertimeoutmS)) + { + Serial.println("Timeout waiting YModem response"); + return 0; + } + + answer = Serial2.read(); + switch (answer) { + case X_NAK: + // something went wrong - send the same packet again? + Serial.print("N"); + ackerrors++; + break; + case X_ACK: + // got ACK to move to the next block of data + Serial.print("."); + doNextBlock = 1; + break; + default: + // unknown response + Serial.print("?"); + ackerrors++; + break; + } + + if (ackerrors >= ackerrorlimit) + { + Serial.println("Ack error limit reached"); + return 0; + } + + // need to handle the 'C' response after the initial file details packet has been sent + if (skip_payload == 1) { + + startmS = millis(); + while ((!Serial2.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + + if ( ((uint32_t) (millis() - startmS) >= transfertimeoutmS)) + { + Serial.println("Timeout waiting YModem response"); + return 0; + } + + answer = Serial2.read(); + if (answer == 'C') { + // good - start sending the data in the next transmission + skip_payload = 0; + } else { + // error? send the file details packet again? + doNextBlock = 0; + } + } + + // move on to the next block of data + if (doNextBlock == 1) { + yPacket.block++; + numBytesStillToSend = numBytesStillToSend - numBytesThisPacket; + } + } + + // all done - send the end of transmission code + Serial2.write( X_EOT ); + + // need to send EOT again for YMODEM + startmS = millis(); + while ((!Serial2.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + + if ( ((uint32_t) (millis() - startmS) >= transfertimeoutmS)) + { + Serial.println("Timeout waiting YModem response"); + return 0; + } + + answer = Serial2.read(); + Serial2.write( X_EOT ); + + if (batchMode == 0) { + // and then a packet full of NULL seems to terminate the process + // and make TeraTerm close the receive dialog box + yPacket.block = 0; + yPacket.block_neg = 0xff - yPacket.block; + memset(yPacket.payload, 0x00, sizeof(yPacket.payload) ); + yPacket.crc = swap16(crc16(yPacket.payload, sizeof(yPacket.payload))); + Serial2.write( (uint8_t*)&yPacket, sizeof(yPacket)); + } + Serial.println("done"); + + srcFile.close(); + return filebytescopied; +} diff --git a/examples/SX128x_examples/DataTransfer/$50SATL.JPG b/examples/SX128x_examples/DataTransfer/$50SATL.JPG new file mode 100644 index 0000000000000000000000000000000000000000..00c3b30c0a19520b0ccc615c7a2fbacfaec55402 GIT binary patch literal 63091 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�ND-qWdB?Qo#x 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 literal 0 HcmV?d00001 diff --git a/examples/SX128x_examples/DataTransfer/$50SATS.JPG b/examples/SX128x_examples/DataTransfer/$50SATS.JPG new file mode 100644 index 0000000000000000000000000000000000000000..dc7124311313d3f1e1cc6bd3fbbf8ca211494434 GIT binary patch literal 6880 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 +#include +#include "Settings.h" + +SX128XLT LoRa; + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + + +void loop() +{ + RXPacketL = LoRa.receiveSXBuffer(0, 60000, WAIT_RX); //returns 0 if packet error of some sort, timeout set at 60secs\60000mS + + digitalWrite(LED1, HIGH); //something has happened + + printSeconds(); + + PacketRSSI = LoRa.readPacketRSSI(); + PacketSNR = LoRa.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + Serial.println(); +} + + +void packet_is_OK() +{ + printReceptionDetails(); + Serial.print(RXPacketL); + Serial.print(F(" bytes > ")); + + if (RXPacketL > BytesToPrint) + { + LoRa.printSXBufferHEX(0, (BytesToPrint - 1)); + } + else + { + LoRa.printSXBufferHEX(0, (RXPacketL - 1)); + } +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LoRa.readIrqStatus(); //get the IRQ status + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F(" RXTimeout")); + } + else + { + Serial.print(F(" PacketError")); + printReceptionDetails(); + Serial.print(F(" Length,")); + Serial.print(LoRa.readRXPacketL()); //get the real packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + + +void printReceptionDetails() +{ + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB ")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void printSeconds() +{ + float secs; + + secs = ( (float) millis() / 1000); + Serial.print(secs, 3); +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(115200); + + Serial.println(F("221_LoRa_Packet_Monitor Starting")); + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1) + { + led_Flash(50, 50); + } + } + + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LoRa.printModemSettings(); + Serial.println(); + LoRa.printOperatingSettings(); + Serial.println(); + Serial.print(F("Packet monitor ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/DataTransfer/221_LoRa_Packet_Monitor/Settings.h b/examples/SX128x_examples/DataTransfer/221_LoRa_Packet_Monitor/Settings.h new file mode 100644 index 0000000..6b23d8c --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/221_LoRa_Packet_Monitor/Settings.h @@ -0,0 +1,31 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const uint8_t BytesToPrint = 16; //number of bytes of packet to print diff --git a/examples/SX128x_examples/DataTransfer/222_FLRC_Packet_Monitor/222_FLRC_Packet_Monitor.ino b/examples/SX128x_examples/DataTransfer/222_FLRC_Packet_Monitor/222_FLRC_Packet_Monitor.ino new file mode 100644 index 0000000..fc0647b --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/222_FLRC_Packet_Monitor/222_FLRC_Packet_Monitor.ino @@ -0,0 +1,162 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program listens for incoming packets using the FLRC settings in the 'Settings.h' + file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + There is a printout of the valid packets received in HEX format. Thus the program can be used to receive + and record non-ASCII packets. The LED will flash for each packet received. To keep up with fast transfers + only the first bytes defined by constant BytesToPrint in the Settings.h file are printed. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LoRa; + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet + + +void loop() +{ + RXPacketL = LoRa.receiveSXBuffer(0, 60000, WAIT_RX); //returns 0 if packet error of some sort, timeout set at 60secs\60000mS + + digitalWrite(LED1, HIGH); //something has happened + + printSeconds(); + + PacketRSSI = LoRa.readPacketRSSI(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + Serial.println(); +} + + +void packet_is_OK() +{ + printReceptionDetails(); + Serial.print(RXPacketL); + Serial.print(F(" bytes > ")); + + if (RXPacketL > BytesToPrint) + { + LoRa.printSXBufferHEX(0, (BytesToPrint - 1)); + } + else + { + LoRa.printSXBufferHEX(0, (RXPacketL - 1)); + } +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LoRa.readIrqStatus(); //get the IRQ status + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F(" RXTimeout")); + } + else + { + Serial.print(F(" PacketError")); + printReceptionDetails(); + Serial.print(F(" Length,")); + Serial.print(LoRa.readRXPacketL()); //get the real packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + + +void printReceptionDetails() +{ + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm ")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void printSeconds() +{ + float secs; + + secs = ( (float) millis() / 1000); + Serial.print(secs, 3); +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(115200); + + Serial.println(F("221_LoRa_Packet_Monitor Starting")); + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1) + { + led_Flash(50, 50); + } + } + + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + + Serial.println(); + LoRa.printModemSettings(); + Serial.println(); + LoRa.printOperatingSettings(); + Serial.println(); + Serial.print(F("Packet monitor ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/DataTransfer/222_FLRC_Packet_Monitor/Settings.h b/examples/SX128x_examples/DataTransfer/222_FLRC_Packet_Monitor/Settings.h new file mode 100644 index 0000000..9dcc76a --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/222_FLRC_Packet_Monitor/Settings.h @@ -0,0 +1,34 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes + + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword, needs to match transmitter + +const uint8_t BytesToPrint = 32; //number of bytes of packet to print diff --git a/examples/SX128x_examples/DataTransfer/230_Arduino_to_PC_File_Transfer_YModem/230_Arduino_to_PC_File_Transfer_YModem.ino b/examples/SX128x_examples/DataTransfer/230_Arduino_to_PC_File_Transfer_YModem/230_Arduino_to_PC_File_Transfer_YModem.ino new file mode 100644 index 0000000..68f9827 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/230_Arduino_to_PC_File_Transfer_YModem/230_Arduino_to_PC_File_Transfer_YModem.ino @@ -0,0 +1,103 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 14/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program that transfers a file from an Arduinos SD card to a folder on a + PC that is connected to the Arduino via a Serial port. The transfer process uses the YModem protocol + and the PC receives the file using the Tera Term Serial terminal program. + + This program was run on an Arduino DUE, with Serial2 used as the transfer port to the PC. + + Instructions for using the program are to be found here; + + https://stuartsprojects.github.io/2021/12/28/Arduino-to-PC-File-Transfers.html + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#include +#include +#include "YModem.h" + +File root; + +const uint16_t SDCS = 30; +const uint16_t LED1 = 8; //LED indicator etc. on during transfers +char FileName[] = "$50SATL.JPG"; //file length 63091 bytes, file CRC 0x59CE +uint16_t transferNumber = 0;; +uint32_t filelength; +uint32_t bytestransfered; + + +void loop() +{ + transferNumber++; + digitalWrite(LED1, HIGH); + Serial.print(F("Start transfer ")); + Serial.print(transferNumber); + Serial.print(F(" ")); + Serial.println(FileName); + Serial.flush(); + digitalWrite(LED1, HIGH); + + bytestransfered = yModemSend(FileName, 1, 1); //transfer filename with waitForReceiver and batchMode options set + + if (bytestransfered > 0) + { + Serial.print(F("YModem transfer completed ")); + Serial.print(bytestransfered); + Serial.println(F(" bytes sent")); + } + else + { + Serial.println(F("YModem transfer FAILED")); + } + Serial.println(); + + digitalWrite(LED1, LOW); + Serial.println(); + delay(10000); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(115200); + Serial2.begin(115200); //Serial port used for file transfers + Serial.println(); + Serial.println(F(__FILE__)); + Serial.flush(); + Serial.println("Using Serial2 for YModem comms @ 115200 baud."); + Serial.println("Initializing SD card..."); + + if (SD.begin(SDCS)) + { + Serial.println(F("SD Card initialized.")); + } + else + { + Serial.println(F("SD Card failed, or not present.")); + while (1) led_Flash(100, 25); + } + +} diff --git a/examples/SX128x_examples/DataTransfer/230_Arduino_to_PC_File_Transfer_YModem/YModem.h b/examples/SX128x_examples/DataTransfer/230_Arduino_to_PC_File_Transfer_YModem/YModem.h new file mode 100644 index 0000000..802abed --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/230_Arduino_to_PC_File_Transfer_YModem/YModem.h @@ -0,0 +1,264 @@ +//29/12/21 Working for repeat transfer - source of this version not clear +// Code below taken from https://gist.github.com/zonque/0ae2dc8cedbcdbd9b933 +// file xymodem-mini.c + +// MarkD modifcations +// - uses 128 byte packets for low RAM devices +// - supports batch upload of several files at once + +//StuartsProjects - hard coded for Serial2 +//080222 - added timeouts + +#define X_SOH 0x01 +#define X_STX 0x02 +#define X_ACK 0x06 +#define X_NAK 0x15 +#define X_EOT 0x04 + +const uint32_t transfertimeoutmS = 5000; +const uint8_t ackerrorlimit = 16; + +struct yModemPacket { + uint8_t start; + uint8_t block; + uint8_t block_neg; + uint8_t payload[128]; + uint16_t crc; +} __attribute__((packed)); + +#define CRC_POLY 0x1021 + +static uint16_t crc_update(uint16_t crc_in, int incr) +{ + uint16_t _xor = crc_in >> 15; + uint16_t _out = crc_in << 1; + + if (incr) + _out++; + + if (_xor) + _out ^= CRC_POLY; + + return _out; +} + +static uint16_t crc16(const uint8_t *data, uint16_t size) +{ + uint16_t crc, i; + + for (crc = 0; size > 0; size--, data++) + for (i = 0x80; i; i >>= 1) + crc = crc_update(crc, *data & i); + + for (i = 0; i < 16; i++) + crc = crc_update(crc, 0); + + return crc; +} + + +static uint16_t swap16(uint16_t in) +{ + return (in >> 8) | ((in & 0xff) << 8); +} + +// Main YModem code. +// filename is pointer to null terminated string +// set waitForReceiver to 1 so that the upload begins when TeraTerm is ready +// set batchMode to 0 when sending 1 file +// set batchMode to 1 for each file sent apart from the last one. + +static int yModemSend(const char *filename, int waitForReceiver, int batchMode ) +{ + uint32_t numBytesStillToSend = 0; + uint16_t numBytesThisPacket = 0; + uint8_t skip_payload; + uint8_t doNextBlock; + uint8_t answer = 0; + char spfBuff[16]; + struct yModemPacket yPacket; + uint32_t filebytescopied = 0; + uint32_t startmS; + uint8_t ackerrors = 0; + + File srcFile = SD.open(filename, O_RDONLY); + + if (srcFile < 0) + { + Serial.println("Open error"); + return 0; + } + + numBytesStillToSend = srcFile.size(); + + if (numBytesStillToSend == 0) + { + Serial.println("SD file error"); + return 0; + } + + //convert the size of the file to an ASCII representation for header packet + sprintf(spfBuff, "%ld", numBytesStillToSend); + + // wait here for the receiving device to respond + if ( waitForReceiver ) + { + Serial.print("Waiting for receiver ping ... "); + while ( Serial2.available() ) Serial2.read(); + startmS = millis(); + + do + { + if ( Serial2.available() ) answer = Serial2.read(); + } + while ((answer != 'C') && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + + if (answer != 'C') + { + Serial.println("Timeout starting YModem transfer"); + return 0; + } + else + { + Serial.println("done"); + } + } + + Serial.print("YModem Sending "); + Serial.print(filename); + Serial.print(" "); + Serial.print(spfBuff); + Serial.println(" bytes"); + + yPacket.start = X_SOH; + yPacket.block = 0; + + // copy the filename into the payload - fill remainder of payload with 0x00 + strncpy((char *) yPacket.payload, filename, sizeof(yPacket.payload)); + + // insert the file size in bytes as ASCII after the NULL of the filename string + strcpy( (char *)(yPacket.payload) + strlen(filename) + 1 , spfBuff ); + + // first pass - don't read any file data as it will overwrite the file details packet + skip_payload = 1; + + while (numBytesStillToSend > 0) + { + doNextBlock = 0; + + // if this isn't the 1st pass, then read a block of up to 128 bytes from the file + if (skip_payload == 0) + { + numBytesThisPacket = min(numBytesStillToSend, sizeof(yPacket.payload)); + srcFile.read(yPacket.payload, numBytesThisPacket); + + filebytescopied = filebytescopied + numBytesThisPacket; + + if (numBytesThisPacket < sizeof(yPacket.payload)) { + // pad out the rest of the payload block with 0x1A + memset(yPacket.payload + numBytesThisPacket, 0x1A, sizeof(yPacket.payload) - numBytesThisPacket); + } + } + + yPacket.block_neg = 0xff - yPacket.block; + + // calculate and insert the CRC16 checksum into the packet + yPacket.crc = swap16(crc16(yPacket.payload, sizeof(yPacket.payload))); + + // send the whole packet to the receiver - will block here + startmS = millis(); + Serial2.write( (uint8_t*)&yPacket, sizeof(yPacket)); + + // wait for the receiver to send back a response to the packet + while ((!Serial2.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + + if ( ((uint32_t) (millis() - startmS) >= transfertimeoutmS)) + { + Serial.println("Timeout waiting YModem response"); + return 0; + } + + answer = Serial2.read(); + switch (answer) { + case X_NAK: + // something went wrong - send the same packet again? + Serial.print("N"); + ackerrors++; + break; + case X_ACK: + // got ACK to move to the next block of data + Serial.print("."); + doNextBlock = 1; + break; + default: + // unknown response + Serial.print("?"); + ackerrors++; + break; + } + + if (ackerrors >= ackerrorlimit) + { + Serial.println("Ack error limit reached"); + return 0; + } + + // need to handle the 'C' response after the initial file details packet has been sent + if (skip_payload == 1) { + + startmS = millis(); + while ((!Serial2.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + + if ( ((uint32_t) (millis() - startmS) >= transfertimeoutmS)) + { + Serial.println("Timeout waiting YModem response"); + return 0; + } + + answer = Serial2.read(); + if (answer == 'C') { + // good - start sending the data in the next transmission + skip_payload = 0; + } else { + // error? send the file details packet again? + doNextBlock = 0; + } + } + + // move on to the next block of data + if (doNextBlock == 1) { + yPacket.block++; + numBytesStillToSend = numBytesStillToSend - numBytesThisPacket; + } + } + + // all done - send the end of transmission code + Serial2.write( X_EOT ); + + // need to send EOT again for YMODEM + startmS = millis(); + while ((!Serial2.available()) && ((uint32_t) (millis() - startmS) < transfertimeoutmS)); + + if ( ((uint32_t) (millis() - startmS) >= transfertimeoutmS)) + { + Serial.println("Timeout waiting YModem response"); + return 0; + } + + answer = Serial2.read(); + Serial2.write( X_EOT ); + + if (batchMode == 0) { + // and then a packet full of NULL seems to terminate the process + // and make TeraTerm close the receive dialog box + yPacket.block = 0; + yPacket.block_neg = 0xff - yPacket.block; + memset(yPacket.payload, 0x00, sizeof(yPacket.payload) ); + yPacket.crc = swap16(crc16(yPacket.payload, sizeof(yPacket.payload))); + Serial2.write( (uint8_t*)&yPacket, sizeof(yPacket)); + } + Serial.println("done"); + + srcFile.close(); + return filebytescopied; +} diff --git a/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/231_Data_Transfer_Test_Transmitter.ino b/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/231_Data_Transfer_Test_Transmitter.ino new file mode 100644 index 0000000..8c98f89 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/231_Data_Transfer_Test_Transmitter.ino @@ -0,0 +1,202 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program that simulates the transfer of a file using data transfer (DT) + packet functions from the SX128X library. No SD cards are needed for the simulation. The file length + used to simulate the transfer is defined by DTFileSize in the DTSettings.h file. Use with matching + receiver program 232_Data_Transfer_Test_Receiver.ino. + + DT packets can be used for transfering large amounts of data in a sequence of packets or segments, + in a reliable and resiliant way. The file open requests to the remote receiver, each segement sent and + the remote file close will all keep transmitting until a valid acknowledge comes from the receiver. + + On transmission the NetworkID and CRC of the payload are appended to the end of the packet by the library + routines. The use of a NetworkID and CRC ensures that the receiver can validate the packet to a high degree + of certainty. + + The transmitter sends the sequence of segments in order. If the sequence fails for some reason, the receiver + will return a NACK packet to the transmitter requesting the segment sequence it was expecting. + + Details of the packet identifiers, header and data lengths and formats used are in the file; + 'Data transfer packet definitions.md' in the \SX128X_examples\DataTransfer\ folder. + + The transfers can be carried out using LoRa packets, max segment size (defined by DTSegmentSize) is 245 bytes + for LoRa, or FLRC packets where 117 is maximum segment size. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include + +#include +#include +#include "DTSettings.h" //LoRa settings etc. +#include + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa + +#define PRINTSEGMENTNUM //enable this define to print segment numbers + +//#define DEBUG +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking + +#include "DTLibrarySIM.h" + +char DTFileName[] = "/Simulate.JPG"; //file name to simulate sending + + +void loop() +{ + Serial.println(("Transfer started")); + + do + { + DTStartmS = millis(); + + //opens the local file to send and sets up transfer parameters + if (startFileTransfer(DTFileName, sizeof(DTFileName), DTSendAttempts)) + { + Serial.print(DTFileName); + Serial.println(F(" opened OK on remote")); + printLocalFileDetails(); + Serial.println(); + NoAckCount = 0; + } + else + { + Serial.print(DTFileName); + Serial.println(F(" Error opening remote file - restart transfer")); + DTFileTransferComplete = false; + continue; + } + + delay(packetdelaymS); + + if (!sendSegments()) + { + Serial.println(); + Serial.println(F("**********************************************************")); + Serial.println(F("Error - Segment write with no file open - Restart received")); + Serial.println(F("**********************************************************")); + Serial.println(); + continue; + } + + if (endFileTransfer(DTFileName, sizeof(DTFileName))) //send command to close remote file + { + DTSendmS = millis() - DTStartmS; //record time taken for transfer + Serial.print(DTFileName); + Serial.println(F(" closed OK on remote")); + beginarrayRW(DTheader, 4); + DTDestinationFileLength = arrayReadUint32(); + Serial.print(F("Acknowledged remote destination file length ")); + Serial.println(DTDestinationFileLength); + DTFileTransferComplete = true; + } + else + { + DTFileTransferComplete = false; + Serial.println(F("ERROR send close remote destination file failed - program halted")); + } + } + while (!DTFileTransferComplete); + + + Serial.print(F("NoAckCount ")); + Serial.println( NoAckCount); + Serial.println(); + + DTsendSecs = (float) DTSendmS / 1000; + Serial.print(F("Transmit time ")); + Serial.print(DTsendSecs, 3); + Serial.println(F("secs")); + Serial.print(F("Transmit rate ")); + Serial.print( (DTFileSize * 8) / (DTsendSecs), 0 ); + Serial.println(F("bps")); + Serial.println(("Transfer finished")); + + Serial.println(("Program halted")); + while (1); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + setDTLED(LED1); //setup LED pin for data transfer indicator + + Serial.begin(115200); + Serial.println(); + Serial.println(F(__FILE__)); + Serial.flush(); + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("LoRa device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + + LoRa.printOperatingSettings(); + Serial.println(); + LoRa.printModemSettings(); + Serial.println(); + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Serial.println(F("Payload CRC disabled")); + } + else + { + Serial.println(F("Payload CRC enabled")); + } + + DTFileTransferComplete = false; + + Serial.println(F("File transfer ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/DTLibrarySIM.h b/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/DTLibrarySIM.h new file mode 100644 index 0000000..62c2dd8 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/DTLibrarySIM.h @@ -0,0 +1,973 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +uint8_t RXPacketL; //length of received packet +uint8_t RXPacketType; //type of received packet, segment write, ACK, NACK etc +uint16_t RXErrors; //count of packets received with error +uint8_t RXFlags; //DTflags byte in header, could be used to control actions in TX and RX +uint8_t RXHeaderL; //length of header +uint8_t RXDataarrayL; //length of data array\segment +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + +uint16_t TXNetworkID; //this is used to store the 'network' number, receiver must have the same networkID +uint16_t TXArrayCRC; //should contain CRC of data array sent +uint8_t TXPacketL; //length of transmitted packet + +uint16_t AckCount; //keep a count of acks that are received within timeout period +uint16_t NoAckCount; //keep a count of acks not received within timeout period +uint16_t LocalPayloadCRC; //for calculating the local data array CRC + +uint16_t DTDestinationFileCRC; //CRC of file received +uint16_t DTSourceFileCRC; //CRC of returned of the remote saved file + +uint32_t DTDestinationFileLength; //length of file written on this destination +uint32_t DTSourceFileLength; //length of file at source + +uint32_t DTStartmS; //used for timeing transfers +bool DTFileOpened; //bool to record when file has been opened + +uint16_t DTSegment = 0; //current segment number +uint16_t DTSegmentNext; //next segment expected +uint16_t DTReceivedSegments; //count of segments received +uint16_t DTSegmentLast; //last segment processed +uint8_t DTLastSegmentSize; //size of the last segment +uint16_t DTNumberSegments; //number of segments for a file transfer +uint16_t DTSentSegments; //count of segments sent + +bool DTFileTransferComplete; //bool to flag file transfer complete +uint32_t DTSendmS; //used for timing transfers +float DTsendSecs; //seconds to transfer a file + +char DTfilenamebuff[DTfilenamesize]; + +int DTLED = -1; //pin number for indicator LED, if -1 then not used + +bool sendFile(char *DTFileName, uint8_t namelength); +bool sendFileSegment(uint16_t segnum, uint8_t segmentsize); +bool startFileTransfer(char *buff, uint8_t filenamesize, uint8_t attempts); +bool endFileTransfer(char *buff, uint8_t filenamesize); +void build_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void build_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum); +void build_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void printLocalFileDetails(); +bool sendSegments(); +void printheader(uint8_t *hdr, uint8_t hdrsize); +void printSeconds(); +void printAckBrief(); +void printAckDetails(); +void printdata(uint8_t *dataarray, uint8_t arraysize); +void printACKdetail(); +void printPacketHex(); +void printPacketRSSI(); +void printPacketDetails(); +void readHeaderDT(); +void printSourceFileDetails(); +void printDestinationFileDetails(); +bool processFileClose(); +bool processFileOpen(uint8_t *buff, uint8_t filenamesize); +bool processSegmentWrite(); +bool processPacket(uint8_t packettype); +bool receiveaPacketDT(); +uint16_t getNumberSegments(uint32_t filesize, uint8_t segmentsize); +uint8_t getLastSegmentSize(uint32_t filesize, uint8_t segmentsize); +void setDTLED(int8_t pinnumber); + +uint8_t DTheader[16]; //header array +uint8_t DTdata[245]; //data/segment array + +bool sendFile(char *DTFileName, uint8_t namelength) +{ + //************************************************************************************************************** + // Start filesend process + // This routine allows the file transfer to be run with a function call of sendFile(FileName, sizeof(FileName)); + //************************************************************************************************************** + + do + { + + DTStartmS = millis(); + + //opens the local file to send and sets up transfer parameters + if (startFileTransfer(DTFileName, namelength, DTSendAttempts)) + { + Serial.print(DTFileName); + Serial.println(F(" opened OK on remote")); + printLocalFileDetails(); + Serial.println(); + NoAckCount = 0; + } + else + { + Serial.print(DTFileName); + Serial.println(F(" Error opening remote file - restart transfer")); + DTFileTransferComplete = false; + continue; + } + + delay(packetdelaymS); + + if (!sendSegments()) + { + Serial.println(); + Serial.println(F("**********************************************************")); + Serial.println(F("Error - Segment write with no file open - Restart received")); + Serial.println(F("**********************************************************")); + Serial.println(); + continue; + } + + if (endFileTransfer(DTFileName, namelength)) //send command to close remote file + { + DTSendmS = millis() - DTStartmS; //record time taken for transfer + Serial.print(DTFileName); + Serial.println(F(" closed OK on remote")); + beginarrayRW(DTheader, 4); + DTDestinationFileLength = arrayReadUint32(); + Serial.print(F("Acknowledged remote destination file length ")); + Serial.println(DTDestinationFileLength); + if (DTDestinationFileLength != DTSourceFileLength) + { + Serial.println(F("ERROR - file lengths do not match")); + } + else + { + Serial.println(F("File lengths match")); + } + DTFileTransferComplete = true; + } + else + { + DTFileTransferComplete = false; + Serial.println(F("ERROR send close remote destination file failed - program halted")); + } + } + while (!DTFileTransferComplete); + + Serial.print(F("NoAckCount ")); + Serial.println( NoAckCount); + Serial.println(); + DTsendSecs = (float) DTSendmS / 1000; + Serial.print(F("Transmit time ")); + Serial.print(DTsendSecs, 3); + Serial.println(F("secs")); + Serial.print(F("Transmit rate ")); + Serial.print( (DTDestinationFileLength * 8) / (DTsendSecs), 0 ); + Serial.println(F("bps")); + Serial.println(("Transfer finished")); + return true; +} + + +bool sendFileSegment(uint16_t segnum, uint8_t segmentsize) +{ + //**************************************************************** + //Send file segment as payload in a packet + //**************************************************************** + + uint8_t ValidACK; + + build_DTSegmentHeader(DTheader, DTSegmentWriteHeaderL, segmentsize, segnum); + +#ifdef PRINTSEGMENTNUM + //Serial.print(F("Segment,")); + Serial.println(segnum); +#endif + +#ifdef DEBUG + printheader(DTheader, DTSegmentWriteHeaderL); + Serial.print(F(" ")); + printdata(DTdata, segmentsize); //print segment size of data array only +#endif + + do + { + digitalWrite(DTLED, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTSegmentWriteHeaderL, (uint8_t *) DTdata, segmentsize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(DTLED, LOW); + + if (TXPacketL == 0) //if there has been an error TXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + ValidACK = LoRa.waitACKDT(DTheader, DTSegmentWriteHeaderL, ACKsegtimeoutmS); + RXPacketType = DTheader[0]; + + if (ValidACK > 0) + { + if (RXPacketType == DTSegmentWriteNACK) + { + DTSegment = DTheader[4] + (DTheader[5] << 8); //load what the segment number should be + RXHeaderL = DTheader[2]; + Serial.println(); + Serial.println(F("************************************")); + Serial.print(F("Received restart request at segment ")); + Serial.println(DTSegment); + printheader(DTheader, RXHeaderL); + Serial.println(); + Serial.print(F("Seek to file location ")); + Serial.println(DTSegment * DTSegmentSize); + Serial.println(F("************************************")); + Serial.println(); + Serial.flush(); + } + + //ack is valid, segment was acknowledged if here + + if (RXPacketType == DTStartNACK) + { + Serial.println(F("Received DTStartNACK ")); + return false; + } + + if (RXPacketType == DTSegmentWriteACK) + { + AckCount++; +#ifdef DEBUG + printAckBrief(); + //printAckDetails() +#endif + DTSegment++; //increase value for next segment + return true; + } + } + else + { + NoAckCount++; + Serial.print(F("Error no ACK received ")); + Serial.println(NoAckCount); + if (NoAckCount > NoAckCountLimit) + { + Serial.println(F("ERROR NoACK limit reached")); + return false; + } + } + } while (ValidACK == 0); + + return true; +} + + +bool startFileTransfer(char *buff, uint8_t filenamesize, uint8_t attempts) +{ + //********************************************************************* + //Start file transfer, simulate open local file first then remote file. + //********************************************************************* + + uint8_t ValidACK; + + Serial.print(F("Start file transfer ")); + Serial.println(buff); + + DTSourceFileLength = DTFileSize; //get the file length + + if (DTSourceFileLength == 0) + { + Serial.print(F("Error opening local file ")); + Serial.println(buff); + return false; + } + + DTNumberSegments = getNumberSegments(DTSourceFileLength, DTSegmentSize); + DTLastSegmentSize = getLastSegmentSize(DTSourceFileLength, DTSegmentSize); + + build_DTFileOpenHeader(DTheader, DTFileOpenHeaderL, filenamesize, DTSourceFileLength, DTSourceFileCRC, DTSegmentSize); + LocalPayloadCRC = LoRa.CRCCCITT((uint8_t *) buff, filenamesize, 0xFFFF); + + do + { + Serial.println(F("Send open remote file request")); + digitalWrite(DTLED, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTFileOpenHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(DTLED, LOW); + +#ifdef DEBUG + Serial.print(F("Send attempt ")); + Serial.println(DTSendAttempts - attempts + 1); +#endif + + attempts--; + TXNetworkID = LoRa.getTXNetworkID(TXPacketL); //get the networkID appended to packet + TXArrayCRC = LoRa.getTXPayloadCRC(TXPacketL); //get the payload CRC appended to packet + +#ifdef DEBUG + Serial.print(F("TXNetworkID,0x")); + Serial.print(TXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Serial.print(F(",TXarrayCRC,0x")); + Serial.println(TXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Serial.println(); +#endif + + if (TXPacketL == 0) //if there has been a send and ack error, TXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + Serial.print(F("Wait ACK ")); + ValidACK = LoRa.waitACKDT(DTheader, DTFileOpenHeaderL, ACKopentimeoutmS); + RXPacketType = DTheader[0]; + + if ((ValidACK > 0) && (RXPacketType == DTFileOpenACK)) + { +#ifdef DEBUG + printPacketHex(); +#endif + } + else + { + NoAckCount++; + Serial.println(F("No ACK received")); + Serial.println(NoAckCount); + if (NoAckCount > NoAckCountLimit) + { + Serial.println(F("ERROR NoACK limit reached")); + return false; + } + +#ifdef DEBUG + printACKdetail(); + Serial.print(F("ACKPacket ")); + printPacketHex(); +#endif + Serial.println(); + } + Serial.println(); + } + while ((ValidACK == 0) && (attempts != 0)); + + if (attempts == 0) + { + return false; + } + + return true; +} + + +bool endFileTransfer(char *buff, uint8_t filenamesize) +{ + //**************************************************************** + //End file transfer, close local file first then remote file. + //**************************************************************** + + uint8_t ValidACK; + + build_DTFileCloseHeader(DTheader, DTFileCloseHeaderL, filenamesize, DTSourceFileLength, DTSourceFileCRC, DTSegmentSize); + + do + { + printSeconds(); + Serial.println(F("Send close remote file")); + + digitalWrite(DTLED, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTFileCloseHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(DTLED, LOW); + + TXNetworkID = LoRa.getTXNetworkID(TXPacketL); + TXArrayCRC = LoRa.getTXPayloadCRC(TXPacketL); + +#ifdef DEBUG + Serial.print(F("TXNetworkID,0x")); + Serial.print(TXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Serial.print(F(",TXarrayCRC,0x")); + Serial.println(TXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Serial.println(); +#endif + + if (TXPacketL == 0) //if there has been a send and ack error, RXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + ValidACK = LoRa.waitACKDT(DTheader, DTFileCloseHeaderL, ACKclosetimeoutmS); + RXPacketType = DTheader[0]; + + if ((ValidACK > 0) && (RXPacketType == DTFileCloseACK)) + { +#ifdef DEBUG + printPacketHex(); +#endif + } + else + { + NoAckCount++; + Serial.print(F("Error no ACK received ")); + Serial.println(NoAckCount); + if (NoAckCount > NoAckCountLimit) + { + Serial.println(F("ERROR NoACK limit reached")); + return false; + } + +#ifdef DEBUG + Serial.println(); + Serial.print(F("ACKPacket ")); + printPacketHex(); + Serial.println(); +#endif + } + } + while (ValidACK == 0); + + return true; +} + + +void build_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //this builds the header buffer for the filename to send + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileOpen); //byte 0, write the packet type + arrayWriteUint8(0); //byte 1, initial DTflags byte, not used here + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void build_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum) +{ + //this builds the header buffer for a segment transmit + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTSegmentWrite); //write the packet type + arrayWriteUint8(0); //initial DTflags byte, not used here + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint16(segnum); //write the DTsegment number + endarrayRW(); +} + + +void build_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //this builds the header buffer for the filename passed + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileClose); //byte 0, write the packet type + arrayWriteUint8(0); //byte 1, initial DTflags byte, not used here + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void printLocalFileDetails() +{ + Serial.print(F("Source file length is ")); + Serial.print(DTSourceFileLength); + Serial.println(F(" bytes")); + Serial.print(F("Segment Size ")); + Serial.println(DTSegmentSize); + Serial.print(F("Number segments ")); + Serial.println(DTNumberSegments); + Serial.print(F("Last segment size ")); + Serial.println(DTLastSegmentSize); +} + + +bool sendSegments() +{ + //start the file transfer at segment 0 + DTSegment = 0; + DTSentSegments = 0; + + while (DTSegment < (DTNumberSegments - 1)) + { +#ifdef DEBUG + printSeconds(); +#endif + + if (sendFileSegment(DTSegment, DTSegmentSize)) + { + DTSentSegments++; + } + else + { + Serial.println(F("ERROR in sendFileSegment")); + Serial.println(); + return false; + } + + delay(packetdelaymS); + }; + + printSeconds(); + Serial.println(F("Last segment")); + + if (!sendFileSegment(DTSegment, DTLastSegmentSize)) + { + Serial.println(F("ERROR in sendFileSegment")); + return false; + } + + return true; +} + + +void printheader(uint8_t *hdr, uint8_t hdrsize) +{ + Serial.print(F("HeaderBytes,")); + Serial.print(hdrsize); + Serial.print(F(" ")); + printarrayHEX(hdr, hdrsize); +} + + +void printSeconds() +{ + float secs; + secs = ( (float) millis() / 1000); + Serial.print(secs, 2); + Serial.print(F(" ")); +} + + +void printdata(uint8_t *dataarray, uint8_t arraysize) +{ + Serial.print(F("DataBytes,")); + Serial.print(arraysize); + Serial.print(F(" ")); + printarrayHEX((uint8_t *) dataarray, 16); //There is a lot of data to print so only print first 16 bytes +} + + +void printAckDetails() +{ + PacketRSSI = LoRa.readPacketRSSI(); + PacketSNR = LoRa.readPacketSNR(); + Serial.print(F("AckCount,")); + Serial.print(AckCount); + Serial.print(F(",NoAckCount,")); + Serial.print(NoAckCount); + Serial.print(F(",AckRSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,AckSNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB")); + Serial.println(); +} + + +void printAckBrief() +{ + PacketRSSI = LoRa.readPacketRSSI(); + Serial.print(F(",AckRSSI,")); + Serial.print(PacketRSSI); + Serial.println(F("dBm")); +} + + +void printACKdetail() +{ + Serial.print(F("ACKDetail")); + Serial.print(F(",RXNetworkID,0x")); + Serial.print(LoRa.getRXNetworkID(RXPacketL), HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(LoRa.getRXPayloadCRC(RXPacketL), HEX); + Serial.print(F(",RXPacketL,")); + Serial.print(RXPacketL); + Serial.print(F(" ")); + LoRa.printReliableStatus(); + Serial.println(); +} + + +void printPacketHex() +{ + RXPacketL = LoRa.readRXPacketL(); + Serial.print(RXPacketL); + Serial.print(F(" bytes > ")); + if (RXPacketL > 0) + { + LoRa.printSXBufferHEX(0, RXPacketL - 1); + } +} + + +void printPacketDetails() +{ + PacketRSSI = LoRa.readPacketRSSI(); + PacketSNR = LoRa.readPacketSNR(); + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm")); + +#ifdef DEBUG + Serial.print(F(",SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dBm,RXOKCount,")); + Serial.print(DTReceivedSegments); + Serial.print(F(",RXErrs,")); + Serial.print(RXErrors); + Serial.print(F(" RX")); + printheader(DTheader, RXHeaderL); +#endif +} + + +void printPacketRSSI() +{ + PacketRSSI = LoRa.readPacketRSSI(); + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm")); +} + +void readHeaderDT() +{ + //the first 6 bytes of the header contain the important stuff, so load it up + //so we can decide what to do next. + beginarrayRW(DTheader, 0); //start buffer read at location 0 + RXPacketType = arrayReadUint8(); //load the packet type + RXFlags = arrayReadUint8(); //initial DTflags byte, not used here + RXHeaderL = arrayReadUint8(); //load the header length + RXDataarrayL = arrayReadUint8(); //load the datalength + DTSegment = arrayReadUint16(); //load the segment number +} + + +void printSourceFileDetails() +{ + Serial.print(DTfilenamebuff); + Serial.print(F(" Source file length is ")); + Serial.print(DTSourceFileLength); + Serial.println(F(" bytes")); + Serial.print(DTfilenamebuff); +} + + +void printDestinationFileDetails() +{ + Serial.print(F("Destination file length is ")); + Serial.print(DTDestinationFileLength); + Serial.println(F(" bytes")); + if (DTDestinationFileLength != DTSourceFileLength) + { + Serial.println(F("ERROR - file lengths do not match")); + } + else + { + Serial.println(F("File lengths match")); + } +} + + +bool processFileClose() +{ + //*********************************************************************************************** + // Code for closing local SD file and sending request to remote + //*********************************************************************************************** + + Serial.print(F("File close for ")); + Serial.println((char*) DTfilenamebuff); + + if (DTFileOpened) //check if file has been opened, close it if it is + { + if (1) //simulate file exists + { + + Serial.print(F("Transfer time ")); + Serial.print(millis() - DTStartmS); + Serial.print(F("mS")); + Serial.println(); + Serial.println(F("File closed")); + Serial.println(); + + DTFileOpened = false; + DTDestinationFileLength = DTFileSize; + beginarrayRW(DTheader, 4); //start writing to array at location 12 + arrayWriteUint32(DTDestinationFileLength); //write file length of file just written just written to ACK header + arrayWriteUint16(DTDestinationFileCRC); //write CRC of file just written to ACK header + + printDestinationFileDetails(); + } + } + + delay(ACKdelaymS); +#ifdef DEBUG + Serial.println(F("Sending ACK")); +#endif + DTheader[0] = DTFileCloseACK; + + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTFileCloseHeaderL, TXpower); + digitalWrite(DTLED, LOW); + + Serial.println(); + Serial.println(); + return true; +} + + +bool processFileOpen(uint8_t *buff, uint8_t filenamesize) +{ + //************************************************************************* + // Code for simulating opening local SD file and sending request to remote + //************************************************************************* + + beginarrayRW(DTheader, 4); //start buffer read at location 4 + DTSourceFileLength = arrayReadUint32(); //load the file length of the remote file being sent + DTSourceFileCRC = arrayReadUint16(); //load the CRC of the source file being sent + + memset(DTfilenamebuff, 0, DTfilenamesize); //clear DTfilenamebuff to all 0s + memcpy(DTfilenamebuff, buff, filenamesize); //copy received DTdata into DTfilenamebuff + + Serial.print(F("File Open request for ")); + Serial.print((char*) DTfilenamebuff); + Serial.println(); + printSourceFileDetails(); + + if (1) //simulate open file for write at beginning, delete if it exists + { + Serial.print((char*) DTfilenamebuff); + Serial.println(F(" File Opened OK")); + Serial.println(F("Waiting transfer")); + DTSegmentNext = 0; //since file is opened the next sequence should be the first + DTFileOpened = true; + DTStartmS = millis(); + } + else + { + Serial.print((char*) DTfilenamebuff); + Serial.println(F(" File Open fail")); + DTFileOpened = false; + return false; + } + + DTStartmS = millis(); + delay(ACKdelaymS); +#ifdef DEBUG + Serial.println(F("Sending ACK")); +#endif + DTheader[0] = DTFileOpenACK; //set the ACK packet type + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTFileOpenHeaderL, TXpower); + digitalWrite(DTLED, LOW); + DTSegmentNext = 0; //after file open, segment 0 is next + + return true; +} + + +bool processSegmentWrite() +{ + //*********************************************************************************************** + // Code for dealing with segment writes - checks that the sequence of segment writes is correct + //*********************************************************************************************** + + if (!DTFileOpened) + { + //something is wrong, have received a request to write a segment but there is no file opened + //need to reject the segment write with a restart NACK + Serial.println(); + Serial.println(F("***************************************************")); + Serial.println(F("Error - Segment write with no file open - send NACK")); + Serial.println(F("***************************************************")); + Serial.println(); + DTheader[0] = DTStartNACK; + delay(ACKdelaymS); + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTStartHeaderL, TXpower); + digitalWrite(DTLED, LOW); + return false; + } + + if (DTSegment == DTSegmentNext) + { + +#ifdef PRINTSEGMENTNUM + //Serial.print(F("Segment,")); + Serial.println(DTSegment); +#endif + +#ifdef DEBUG + Serial.print(F(",Bytes,")); + Serial.print(RXDataarrayL); + //printPacketDetails(); + printPacketRSSI(); + Serial.println(F(" SendACK")); +#endif + + DTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTSegmentWriteHeaderL, TXpower); + digitalWrite(DTLED, LOW); + DTReceivedSegments++; + DTSegmentLast = DTSegment; //so we can tell if sequece has been received twice + DTSegmentNext = DTSegment + 1; + return true; + } + + if (DTSegment == DTSegmentLast) + { + Serial.print(F("ERROR segment ")); + Serial.print(DTSegment); + Serial.print(F(" already received ")); + //printPacketDetails(); + printPacketRSSI(); + DTheader[0] = DTSegmentWriteACK; + Serial.print(F(" Send ACK")); + delay(ACKdelaymS); + delay(DuplicatedelaymS); + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTSegmentWriteHeaderL, TXpower); + digitalWrite(DTLED, LOW); + return true; + } + + if (DTSegment != DTSegmentNext ) + { + Serial.print(F(" ERROR Received Segment ")); + Serial.print(DTSegment); + Serial.print(F(" expected ")); + Serial.print(DTSegmentNext); + Serial.print(F(" ")); + //printPacketDetails(); + printPacketRSSI(); + DTheader[0] = DTSegmentWriteNACK; + DTheader[4] = lowByte(DTSegmentNext); + DTheader[5] = highByte(DTSegmentNext); + + Serial.print(F(" Send NACK for segment ")); + Serial.print(DTSegmentNext); + delay(ACKdelaymS); + delay(DuplicatedelaymS); + digitalWrite(DTLED, HIGH); + + Serial.println(); + Serial.println(); + Serial.println(F("*****************************************")); + Serial.print(F("Transmit restart request for segment ")); + Serial.println(DTSegmentNext); + printheader(DTheader, RXHeaderL); + Serial.println(); + Serial.println(F("*****************************************")); + Serial.println(); + Serial.flush(); + + LoRa.sendACKDT(DTheader, DTSegmentWriteHeaderL, TXpower); + digitalWrite(DTLED, LOW); + return false; + } + + return true; +} + + +bool processPacket(uint8_t packettype) +{ + //*********************************************************************************************** + // Decide what to do with an incoming packet + //*********************************************************************************************** + + if (packettype == DTSegmentWrite) + { + processSegmentWrite(); + return true; + } + + if (packettype == DTFileOpen) + { + processFileOpen(DTdata, RXDataarrayL); + return true; + } + + if (packettype == DTFileClose) + { + processFileClose(); + return true; + } + + return true; +} + + +bool receiveaPacketDT() +{ + //****************************** + //Receive Data transfer packets + //****************************** + + RXPacketType = 0; + + RXPacketL = LoRa.receiveDT(DTheader, HeaderSizeMax, (uint8_t *) DTdata, DataSizeMax, NetworkID, RXtimeoutmS, WAIT_RX); + digitalWrite(DTLED, HIGH); + +#ifdef DEBUG + printSeconds(); +#endif + + if (RXPacketL > 0) + { + //if the LT.receiveDT() returns a value > 0 for RXPacketL then packet was received OK + //then only action payload if destinationNode = thisNode + readHeaderDT(); //get the basic header details into global variables RXPacketType etc + processPacket(RXPacketType); //process and act on the packet + digitalWrite(DTLED, LOW); + return true; + } + else + { + //if the LoRa.receiveDT() function detects an error RXOK is 0 + Serial.print(F("PacketError")); + RXErrors++; + printPacketDetails(); +#ifdef DEBUG + LoRa.printReliableStatus(); + LoRa.printIrqStatus(); +#endif + Serial.println(); + digitalWrite(DTLED, LOW); + return false; + } +} + + +uint16_t getNumberSegments(uint32_t filesize, uint8_t segmentsize) +{ + uint16_t segments; + segments = filesize / segmentsize; + if ((filesize % segmentsize) > 0) + { + segments++; + } + return segments; +} + + +uint8_t getLastSegmentSize(uint32_t filesize, uint8_t segmentsize) +{ + uint8_t lastsize; + lastsize = filesize % segmentsize; + if (lastsize == 0) + { + lastsize = segmentsize; + } + return lastsize; +} + + +void setDTLED(int8_t pinnumber) +{ + if (pinnumber >= 0) + { + DTLED = pinnumber; + pinMode(pinnumber, OUTPUT); + } +} diff --git a/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/DTSettings.h b/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/DTSettings.h new file mode 100644 index 0000000..55e91cd --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/231_Data_Transfer_Test_Transmitter/DTSettings.h @@ -0,0 +1,60 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t packetdelaymS = 0; //mS delay between transmitted packets + +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t DTfilenamesize = 32; //size of DTfilename buffer + +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet +const uint8_t DTSendAttempts = 10; //number of attempts sending a packet before a restart + +const uint32_t DTFileSize = 65535; //size of file to simulate + +#ifdef USELORA +const uint8_t DTSegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t DTSegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/232_Data_Transfer_Test_Receiver.ino b/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/232_Data_Transfer_Test_Receiver.ino new file mode 100644 index 0000000..3f4f672 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/232_Data_Transfer_Test_Receiver.ino @@ -0,0 +1,134 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program that simulates the transfer of a file using data transfer (DT) + packet functions from the SX128X library. No SD cards are needed for the simulation. Use with matching + receiver program 231_Data_Transfer_Test_Transmitter.ino. + + DT packets can be used for transfering large amounts of data in a sequence of packets or segments, + in a reliable and resiliant way. The remote file open request, the segements sent and the remote file close + will be transmitted until a valid acknowledge comes from the receiver. + + Each DT packet contains a variable length header array and a variable length data array as the payload. + On transmission the NetworkID and CRC of the payload are appended to the end of the packet by the library + routines. The use of a NetworkID and CRC ensures that the receiver can validate the packet to a high degree + of certainty. The receiver will not accept packets that dont have the appropriate NetworkID or payload CRC + at the end of the packet. + + The transmitter sends a sequence of segments in order and the receiver keeps track of the sequence. If + the sequence fails for some reason, the receiver will return a NACK packet to the transmitter requesting + the segment sequence it was expecting. + + The transfers can be carried out using LoRa packets, max segment size (defined by DTSegmentSize) is 245 bytes + for LoRa, or FLRC packets where 117 is maximum segment size. + + Details of the packet identifiers, header and data lengths and formats used are in the file + Data_transfer_packet_definitions.md in the \SX128X_examples\DataTransfer\ folder. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include + +#include +#include +#include "DTSettings.h" //LoRa settings etc. +#include + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa + +#define PRINTSEGMENTNUM //enable this define to print segment numbers + +//#define DEBUG +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking + +#include "DTLibrarySIM.h" + + +void loop() +{ + receiveaPacketDT(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + setDTLED(LED1); //setup LED pin for data transfer indicator + + Serial.begin(115200); + Serial.println(); + Serial.println(F(__FILE__)); + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("LoRa device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + + + LoRa.printOperatingSettings(); + Serial.println(); + LoRa.printModemSettings(); + Serial.println(); + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Serial.println(F("Payload CRC disabled")); + } + else + { + Serial.println(F("Payload CRC enabled")); + } + + DTSegmentNext = 0; + DTFileOpened = false; + + Serial.println(F("File transfer receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/DTLibrarySIM.h b/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/DTLibrarySIM.h new file mode 100644 index 0000000..c88f92e --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/DTLibrarySIM.h @@ -0,0 +1,972 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +uint8_t RXPacketL; //length of received packet +uint8_t RXPacketType; //type of received packet, segment write, ACK, NACK etc +uint16_t RXErrors; //count of packets received with error +uint8_t RXFlags; //DTflags byte in header, could be used to control actions in TX and RX +uint8_t RXHeaderL; //length of header +uint8_t RXDataarrayL; //length of data array\segment +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + +uint16_t TXNetworkID; //this is used to store the 'network' number, receiver must have the same networkID +uint16_t TXArrayCRC; //should contain CRC of data array sent +uint8_t TXPacketL; //length of transmitted packet + +uint16_t AckCount; //keep a count of acks that are received within timeout period +uint16_t NoAckCount; //keep a count of acks not received within timeout period +uint16_t LocalPayloadCRC; //for calculating the local data array CRC + +uint16_t DTDestinationFileCRC; //CRC of file received +uint16_t DTSourceFileCRC; //CRC of returned of the remote saved file + +uint32_t DTDestinationFileLength; //length of file written on this destination +uint32_t DTSourceFileLength; //length of file at source + +uint32_t DTStartmS; //used for timeing transfers +bool DTFileOpened; //bool to record when file has been opened + +uint16_t DTSegment = 0; //current segment number +uint16_t DTSegmentNext; //next segment expected +uint16_t DTReceivedSegments; //count of segments received +uint16_t DTSegmentLast; //last segment processed +uint8_t DTLastSegmentSize; //size of the last segment +uint16_t DTNumberSegments; //number of segments for a file transfer +uint16_t DTSentSegments; //count of segments sent + +bool DTFileTransferComplete; //bool to flag file transfer complete +uint32_t DTSendmS; //used for timing transfers +float DTsendSecs; //seconds to transfer a file + +char DTfilenamebuff[DTfilenamesize]; + +int DTLED = -1; //pin number for indicator LED, if -1 then not used + +bool sendFile(char *DTFileName, uint8_t namelength); +bool sendFileSegment(uint16_t segnum, uint8_t segmentsize); +bool startFileTransfer(char *buff, uint8_t filenamesize, uint8_t attempts); +bool endFileTransfer(char *buff, uint8_t filenamesize); +void build_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void build_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum); +void build_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void printLocalFileDetails(); +bool sendSegments(); +void printheader(uint8_t *hdr, uint8_t hdrsize); +void printSeconds(); +void printAckBrief(); +void printAckDetails(); +void printdata(uint8_t *dataarray, uint8_t arraysize); +void printACKdetail(); +void printPacketHex(); +void printPacketRSSI(); +void printPacketDetails(); +void readHeaderDT(); +void printSourceFileDetails(); +void printDestinationFileDetails(); +bool processFileClose(); +bool processFileOpen(uint8_t *buff, uint8_t filenamesize); +bool processSegmentWrite(); +bool processPacket(uint8_t packettype); +bool receiveaPacketDT(); +uint16_t getNumberSegments(uint32_t filesize, uint8_t segmentsize); +uint8_t getLastSegmentSize(uint32_t filesize, uint8_t segmentsize); +void setDTLED(int8_t pinnumber); + +uint8_t DTheader[16]; //header array +uint8_t DTdata[245]; //data/segment array + +bool sendFile(char *DTFileName, uint8_t namelength) +{ + //************************************************************************************************************** + // Start filesend process + // This routine allows the file transfer to be run with a function call of sendFile(FileName, sizeof(FileName)); + //************************************************************************************************************** + + do + { + + DTStartmS = millis(); + + //opens the local file to send and sets up transfer parameters + if (startFileTransfer(DTFileName, namelength, DTSendAttempts)) + { + Serial.print(DTFileName); + Serial.println(F(" opened OK on remote")); + printLocalFileDetails(); + Serial.println(); + NoAckCount = 0; + } + else + { + Serial.print(DTFileName); + Serial.println(F(" Error opening remote file - restart transfer")); + DTFileTransferComplete = false; + continue; + } + + delay(packetdelaymS); + + if (!sendSegments()) + { + Serial.println(); + Serial.println(F("**********************************************************")); + Serial.println(F("Error - Segment write with no file open - Restart received")); + Serial.println(F("**********************************************************")); + Serial.println(); + continue; + } + + if (endFileTransfer(DTFileName, namelength)) //send command to close remote file + { + DTSendmS = millis() - DTStartmS; //record time taken for transfer + Serial.print(DTFileName); + Serial.println(F(" closed OK on remote")); + beginarrayRW(DTheader, 4); + DTDestinationFileLength = arrayReadUint32(); + Serial.print(F("Acknowledged remote destination file length ")); + Serial.println(DTDestinationFileLength); + if (DTDestinationFileLength != DTSourceFileLength) + { + Serial.println(F("ERROR - file lengths do not match")); + } + else + { + Serial.println(F("File lengths match")); + } + DTFileTransferComplete = true; + } + else + { + DTFileTransferComplete = false; + Serial.println(F("ERROR send close remote destination file failed - program halted")); + } + } + while (!DTFileTransferComplete); + + Serial.print(F("NoAckCount ")); + Serial.println( NoAckCount); + Serial.println(); + DTsendSecs = (float) DTSendmS / 1000; + Serial.print(F("Transmit time ")); + Serial.print(DTsendSecs, 3); + Serial.println(F("secs")); + Serial.print(F("Transmit rate ")); + Serial.print( (DTDestinationFileLength * 8) / (DTsendSecs), 0 ); + Serial.println(F("bps")); + Serial.println(("Transfer finished")); + return true; +} + + +bool sendFileSegment(uint16_t segnum, uint8_t segmentsize) +{ + //**************************************************************** + //Send file segment as payload in a packet + //**************************************************************** + + uint8_t ValidACK; + + build_DTSegmentHeader(DTheader, DTSegmentWriteHeaderL, segmentsize, segnum); + +#ifdef PRINTSEGMENTNUM + //Serial.print(F("Segment,")); + Serial.println(segnum); +#endif + +#ifdef DEBUG + printheader(DTheader, DTSegmentWriteHeaderL); + Serial.print(F(" ")); + printdata(DTdata, segmentsize); //print segment size of data array only +#endif + + do + { + digitalWrite(DTLED, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTSegmentWriteHeaderL, (uint8_t *) DTdata, segmentsize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(DTLED, LOW); + + if (TXPacketL == 0) //if there has been an error TXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + ValidACK = LoRa.waitACKDT(DTheader, DTSegmentWriteHeaderL, ACKsegtimeoutmS); + RXPacketType = DTheader[0]; + + if (ValidACK > 0) + { + if (RXPacketType == DTSegmentWriteNACK) + { + DTSegment = DTheader[4] + (DTheader[5] << 8); //load what the segment number should be + RXHeaderL = DTheader[2]; + Serial.println(); + Serial.println(F("************************************")); + Serial.print(F("Received restart request at segment ")); + Serial.println(DTSegment); + printheader(DTheader, RXHeaderL); + Serial.println(); + Serial.print(F("Seek to file location ")); + Serial.println(DTSegment * DTSegmentSize); + Serial.println(F("************************************")); + Serial.println(); + Serial.flush(); + } + + //ack is valid, segment was acknowledged if here + + if (RXPacketType == DTStartNACK) + { + Serial.println(F("Received DTStartNACK ")); + return false; + } + + if (RXPacketType == DTSegmentWriteACK) + { + AckCount++; +#ifdef DEBUG + printAckBrief(); + //printAckDetails() +#endif + DTSegment++; //increase value for next segment + return true; + } + } + else + { + NoAckCount++; + Serial.print(F("Error no ACK received ")); + Serial.println(NoAckCount); + if (NoAckCount > NoAckCountLimit) + { + Serial.println(F("ERROR NoACK limit reached")); + return false; + } + } + } while (ValidACK == 0); + + return true; +} + + +bool startFileTransfer(char *buff, uint8_t filenamesize, uint8_t attempts) +{ + //********************************************************************* + //Start file transfer, simulate open local file first then remote file. + //********************************************************************* + + uint8_t ValidACK; + + Serial.print(F("Start file transfer ")); + Serial.println(buff); + + DTSourceFileLength = DTFileSize; //get the file length + + if (DTSourceFileLength == 0) + { + Serial.print(F("Error opening local file ")); + Serial.println(buff); + return false; + } + + DTNumberSegments = getNumberSegments(DTSourceFileLength, DTSegmentSize); + DTLastSegmentSize = getLastSegmentSize(DTSourceFileLength, DTSegmentSize); + + build_DTFileOpenHeader(DTheader, DTFileOpenHeaderL, filenamesize, DTSourceFileLength, DTSourceFileCRC, DTSegmentSize); + LocalPayloadCRC = LoRa.CRCCCITT((uint8_t *) buff, filenamesize, 0xFFFF); + + do + { + Serial.println(F("Send open remote file request")); + digitalWrite(DTLED, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTFileOpenHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(DTLED, LOW); + +#ifdef DEBUG + Serial.print(F("Send attempt ")); + Serial.println(DTSendAttempts - attempts + 1); +#endif + + attempts--; + TXNetworkID = LoRa.getTXNetworkID(TXPacketL); //get the networkID appended to packet + TXArrayCRC = LoRa.getTXPayloadCRC(TXPacketL); //get the payload CRC appended to packet + +#ifdef DEBUG + Serial.print(F("TXNetworkID,0x")); + Serial.print(TXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Serial.print(F(",TXarrayCRC,0x")); + Serial.println(TXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Serial.println(); +#endif + + if (TXPacketL == 0) //if there has been a send and ack error, TXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + Serial.print(F("Wait ACK ")); + ValidACK = LoRa.waitACKDT(DTheader, DTFileOpenHeaderL, ACKopentimeoutmS); + RXPacketType = DTheader[0]; + + if ((ValidACK > 0) && (RXPacketType == DTFileOpenACK)) + { +#ifdef DEBUG + printPacketHex(); +#endif + } + else + { + NoAckCount++; + Serial.println(F("No ACK received")); + Serial.println(NoAckCount); + if (NoAckCount > NoAckCountLimit) + { + Serial.println(F("ERROR NoACK limit reached")); + return false; + } + +#ifdef DEBUG + printACKdetail(); + Serial.print(F("ACKPacket ")); + printPacketHex(); +#endif + Serial.println(); + } + Serial.println(); + } + while ((ValidACK == 0) && (attempts != 0)); + + if (attempts == 0) + { + return false; + } + + return true; +} + + +bool endFileTransfer(char *buff, uint8_t filenamesize) +{ + //**************************************************************** + //End file transfer, close local file first then remote file. + //**************************************************************** + + uint8_t ValidACK; + + build_DTFileCloseHeader(DTheader, DTFileCloseHeaderL, filenamesize, DTSourceFileLength, DTSourceFileCRC, DTSegmentSize); + + do + { + printSeconds(); + Serial.println(F("Send close remote file")); + + digitalWrite(DTLED, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTFileCloseHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(DTLED, LOW); + + TXNetworkID = LoRa.getTXNetworkID(TXPacketL); + TXArrayCRC = LoRa.getTXPayloadCRC(TXPacketL); + +#ifdef DEBUG + Serial.print(F("TXNetworkID,0x")); + Serial.print(TXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Serial.print(F(",TXarrayCRC,0x")); + Serial.println(TXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Serial.println(); +#endif + + if (TXPacketL == 0) //if there has been a send and ack error, RXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + ValidACK = LoRa.waitACKDT(DTheader, DTFileCloseHeaderL, ACKclosetimeoutmS); + RXPacketType = DTheader[0]; + + if ((ValidACK > 0) && (RXPacketType == DTFileCloseACK)) + { +#ifdef DEBUG + printPacketHex(); +#endif + } + else + { + NoAckCount++; + Serial.print(F("Error no ACK received ")); + Serial.println(NoAckCount); + if (NoAckCount > NoAckCountLimit) + { + Serial.println(F("ERROR NoACK limit reached")); + return false; + } + +#ifdef DEBUG + Serial.println(); + Serial.print(F("ACKPacket ")); + printPacketHex(); + Serial.println(); +#endif + } + } + while (ValidACK == 0); + + return true; +} + + +void build_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //this builds the header buffer for the filename to send + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileOpen); //byte 0, write the packet type + arrayWriteUint8(0); //byte 1, initial DTflags byte, not used here + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void build_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum) +{ + //this builds the header buffer for a segment transmit + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTSegmentWrite); //write the packet type + arrayWriteUint8(0); //initial DTflags byte, not used here + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint16(segnum); //write the DTsegment number + endarrayRW(); +} + + +void build_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //this builds the header buffer for the filename passed + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileClose); //byte 0, write the packet type + arrayWriteUint8(0); //byte 1, initial DTflags byte, not used here + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void printLocalFileDetails() +{ + Serial.print(F("Source file length is ")); + Serial.print(DTSourceFileLength); + Serial.println(F(" bytes")); + Serial.print(F("Segment Size ")); + Serial.println(DTSegmentSize); + Serial.print(F("Number segments ")); + Serial.println(DTNumberSegments); + Serial.print(F("Last segment size ")); + Serial.println(DTLastSegmentSize); +} + + +bool sendSegments() +{ + //start the file transfer at segment 0 + DTSegment = 0; + DTSentSegments = 0; + + while (DTSegment < (DTNumberSegments - 1)) + { +#ifdef DEBUG + printSeconds(); +#endif + + if (sendFileSegment(DTSegment, DTSegmentSize)) + { + DTSentSegments++; + } + else + { + Serial.println(F("ERROR in sendFileSegment")); + Serial.println(); + return false; + } + + delay(packetdelaymS); + }; + + printSeconds(); + Serial.println(F("Last segment")); + + if (!sendFileSegment(DTSegment, DTLastSegmentSize)) + { + Serial.println(F("ERROR in sendFileSegment")); + return false; + } + + return true; +} + + +void printheader(uint8_t *hdr, uint8_t hdrsize) +{ + Serial.print(F("HeaderBytes,")); + Serial.print(hdrsize); + Serial.print(F(" ")); + printarrayHEX(hdr, hdrsize); +} + + +void printSeconds() +{ + float secs; + secs = ( (float) millis() / 1000); + Serial.print(secs, 2); + Serial.print(F(" ")); +} + + +void printdata(uint8_t *dataarray, uint8_t arraysize) +{ + Serial.print(F("DataBytes,")); + Serial.print(arraysize); + Serial.print(F(" ")); + printarrayHEX((uint8_t *) dataarray, 16); //There is a lot of data to print so only print first 16 bytes +} + + +void printAckDetails() +{ + PacketRSSI = LoRa.readPacketRSSI(); + PacketSNR = LoRa.readPacketSNR(); + Serial.print(F("AckCount,")); + Serial.print(AckCount); + Serial.print(F(",NoAckCount,")); + Serial.print(NoAckCount); + Serial.print(F(",AckRSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,AckSNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB")); + Serial.println(); +} + + +void printAckBrief() +{ + PacketRSSI = LoRa.readPacketRSSI(); + Serial.print(F(",AckRSSI,")); + Serial.print(PacketRSSI); + Serial.println(F("dBm")); +} + + +void printACKdetail() +{ + Serial.print(F("ACKDetail")); + Serial.print(F(",RXNetworkID,0x")); + Serial.print(LoRa.getRXNetworkID(RXPacketL), HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(LoRa.getRXPayloadCRC(RXPacketL), HEX); + Serial.print(F(",RXPacketL,")); + Serial.print(RXPacketL); + Serial.print(F(" ")); + LoRa.printReliableStatus(); + Serial.println(); +} + + +void printPacketHex() +{ + RXPacketL = LoRa.readRXPacketL(); + Serial.print(RXPacketL); + Serial.print(F(" bytes > ")); + if (RXPacketL > 0) + { + LoRa.printSXBufferHEX(0, RXPacketL - 1); + } +} + + +void printPacketDetails() +{ + PacketRSSI = LoRa.readPacketRSSI(); + PacketSNR = LoRa.readPacketSNR(); + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm")); + +#ifdef DEBUG + Serial.print(F(",SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dBm,RXOKCount,")); + Serial.print(DTReceivedSegments); + Serial.print(F(",RXErrs,")); + Serial.print(RXErrors); + Serial.print(F(" RX")); + printheader(DTheader, RXHeaderL); +#endif +} + + +void printPacketRSSI() +{ + PacketRSSI = LoRa.readPacketRSSI(); + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm")); +} + +void readHeaderDT() +{ + //the first 6 bytes of the header contain the important stuff, so load it up + //so we can decide what to do next. + beginarrayRW(DTheader, 0); //start buffer read at location 0 + RXPacketType = arrayReadUint8(); //load the packet type + RXFlags = arrayReadUint8(); //initial DTflags byte, not used here + RXHeaderL = arrayReadUint8(); //load the header length + RXDataarrayL = arrayReadUint8(); //load the datalength + DTSegment = arrayReadUint16(); //load the segment number +} + + +void printSourceFileDetails() +{ + Serial.print(DTfilenamebuff); + Serial.print(F(" Source file length is ")); + Serial.print(DTSourceFileLength); + Serial.println(F(" bytes")); + Serial.print(DTfilenamebuff); +} + + +void printDestinationFileDetails() +{ + Serial.print(F("Destination file length is ")); + Serial.print(DTDestinationFileLength); + Serial.println(F(" bytes")); + if (DTDestinationFileLength != DTSourceFileLength) + { + Serial.println(F("ERROR - file lengths do not match")); + } + else + { + Serial.println(F("File lengths match")); + } +} + + +bool processFileClose() +{ + //*********************************************************************************************** + // Code for closing local SD file and sending request to remote + //*********************************************************************************************** + + Serial.print(F("File close for ")); + Serial.println((char*) DTfilenamebuff); + + if (DTFileOpened) //check if file has been opened, close it if it is + { + if (1) //simulate file exists + { + + Serial.print(F("Transfer time ")); + Serial.print(millis() - DTStartmS); + Serial.print(F("mS")); + Serial.println(); + Serial.println(F("File closed")); + Serial.println(); + + DTFileOpened = false; + DTDestinationFileLength = DTFileSize; + beginarrayRW(DTheader, 4); //start writing to array at location 12 + arrayWriteUint32(DTDestinationFileLength); //write file length of file just written just written to ACK header + arrayWriteUint16(DTDestinationFileCRC); //write CRC of file just written to ACK header + + printDestinationFileDetails(); + } + } + + delay(ACKdelaymS); +#ifdef DEBUG + Serial.println(F("Sending ACK")); +#endif + DTheader[0] = DTFileCloseACK; + + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTFileCloseHeaderL, TXpower); + digitalWrite(DTLED, LOW); + + Serial.println(); + Serial.println(); + return true; +} + + +bool processFileOpen(uint8_t *buff, uint8_t filenamesize) +{ + //************************************************************************* + // Code for simulating opening local SD file and sending request to remote + //************************************************************************* + + beginarrayRW(DTheader, 4); //start buffer read at location 4 + DTSourceFileLength = arrayReadUint32(); //load the file length of the remote file being sent + DTSourceFileCRC = arrayReadUint16(); //load the CRC of the source file being sent + + memset(DTfilenamebuff, 0, DTfilenamesize); //clear DTfilenamebuff to all 0s + memcpy(DTfilenamebuff, buff, filenamesize); //copy received DTdata into DTfilenamebuff + + Serial.print(F("File Open request for ")); + Serial.print((char*) DTfilenamebuff); + Serial.println(); + printSourceFileDetails(); + + if (1) //simulate open file for write at beginning, delete if it exists + { + Serial.print((char*) DTfilenamebuff); + Serial.println(F(" File Opened OK")); + Serial.println(F("Waiting transfer")); + DTSegmentNext = 0; //since file is opened the next sequence should be the first + DTFileOpened = true; + DTStartmS = millis(); + } + else + { + Serial.print((char*) DTfilenamebuff); + Serial.println(F(" File Open fail")); + DTFileOpened = false; + return false; + } + + DTStartmS = millis(); + delay(ACKdelaymS); +#ifdef DEBUG + Serial.println(F("Sending ACK")); +#endif + DTheader[0] = DTFileOpenACK; //set the ACK packet type + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTFileOpenHeaderL, TXpower); + digitalWrite(DTLED, LOW); + DTSegmentNext = 0; //after file open, segment 0 is next + + return true; +} + + +bool processSegmentWrite() +{ + //*********************************************************************************************** + // Code for dealing with segment writes - checks that the sequence of segment writes is correct + //*********************************************************************************************** + + if (!DTFileOpened) + { + //something is wrong, have received a request to write a segment but there is no file opened + //need to reject the segment write with a restart NACK + Serial.println(); + Serial.println(F("***************************************************")); + Serial.println(F("Error - Segment write with no file open - send NACK")); + Serial.println(F("***************************************************")); + Serial.println(); + DTheader[0] = DTStartNACK; + delay(ACKdelaymS); + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTStartHeaderL, TXpower); + digitalWrite(DTLED, LOW); + return false; + } + + if (DTSegment == DTSegmentNext) + { +#ifdef PRINTSEGMENTNUM + //Serial.print(F("Segment,")); + Serial.println(DTSegment); +#endif + +#ifdef DEBUG + Serial.print(F(",Bytes,")); + Serial.print(RXDataarrayL); + //printPacketDetails(); + printPacketRSSI(); + Serial.println(F(" SendACK")); +#endif + + DTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTSegmentWriteHeaderL, TXpower); + digitalWrite(DTLED, LOW); + DTReceivedSegments++; + DTSegmentLast = DTSegment; //so we can tell if sequece has been received twice + DTSegmentNext = DTSegment + 1; + return true; + } + + if (DTSegment == DTSegmentLast) + { + Serial.print(F("ERROR segment ")); + Serial.print(DTSegment); + Serial.print(F(" already received ")); + //printPacketDetails(); + printPacketRSSI(); + DTheader[0] = DTSegmentWriteACK; + Serial.print(F(" Send ACK")); + delay(ACKdelaymS); + delay(DuplicatedelaymS); + digitalWrite(DTLED, HIGH); + LoRa.sendACKDT(DTheader, DTSegmentWriteHeaderL, TXpower); + digitalWrite(DTLED, LOW); + return true; + } + + if (DTSegment != DTSegmentNext ) + { + Serial.print(F(" ERROR Received Segment ")); + Serial.print(DTSegment); + Serial.print(F(" expected ")); + Serial.print(DTSegmentNext); + Serial.print(F(" ")); + //printPacketDetails(); + printPacketRSSI(); + DTheader[0] = DTSegmentWriteNACK; + DTheader[4] = lowByte(DTSegmentNext); + DTheader[5] = highByte(DTSegmentNext); + + Serial.print(F(" Send NACK for segment ")); + Serial.print(DTSegmentNext); + delay(ACKdelaymS); + delay(DuplicatedelaymS); + digitalWrite(DTLED, HIGH); + + Serial.println(); + Serial.println(); + Serial.println(F("*****************************************")); + Serial.print(F("Transmit restart request for segment ")); + Serial.println(DTSegmentNext); + printheader(DTheader, RXHeaderL); + Serial.println(); + Serial.println(F("*****************************************")); + Serial.println(); + Serial.flush(); + + LoRa.sendACKDT(DTheader, DTSegmentWriteHeaderL, TXpower); + digitalWrite(DTLED, LOW); + return false; + } + + return true; +} + + +bool processPacket(uint8_t packettype) +{ + //*********************************************************************************************** + // Decide what to do with an incoming packet + //*********************************************************************************************** + + if (packettype == DTSegmentWrite) + { + processSegmentWrite(); + return true; + } + + if (packettype == DTFileOpen) + { + processFileOpen(DTdata, RXDataarrayL); + return true; + } + + if (packettype == DTFileClose) + { + processFileClose(); + return true; + } + + return true; +} + + +bool receiveaPacketDT() +{ + //****************************** + //Receive Data transfer packets + //****************************** + + RXPacketType = 0; + + RXPacketL = LoRa.receiveDT(DTheader, HeaderSizeMax, (uint8_t *) DTdata, DataSizeMax, NetworkID, RXtimeoutmS, WAIT_RX); + digitalWrite(DTLED, HIGH); + +#ifdef DEBUG + printSeconds(); +#endif + + if (RXPacketL > 0) + { + //if the LT.receiveDT() returns a value > 0 for RXPacketL then packet was received OK + //then only action payload if destinationNode = thisNode + readHeaderDT(); //get the basic header details into global variables RXPacketType etc + processPacket(RXPacketType); //process and act on the packet + digitalWrite(DTLED, LOW); + return true; + } + else + { + //if the LoRa.receiveDT() function detects an error RXOK is 0 + Serial.print(F("PacketError")); + RXErrors++; + printPacketDetails(); +#ifdef DEBUG + LoRa.printReliableStatus(); + LoRa.printIrqStatus(); +#endif + Serial.println(); + digitalWrite(DTLED, LOW); + return false; + } +} + + +uint16_t getNumberSegments(uint32_t filesize, uint8_t segmentsize) +{ + uint16_t segments; + segments = filesize / segmentsize; + if ((filesize % segmentsize) > 0) + { + segments++; + } + return segments; +} + + +uint8_t getLastSegmentSize(uint32_t filesize, uint8_t segmentsize) +{ + uint8_t lastsize; + lastsize = filesize % segmentsize; + if (lastsize == 0) + { + lastsize = segmentsize; + } + return lastsize; +} + + +void setDTLED(int8_t pinnumber) +{ + if (pinnumber >= 0) + { + DTLED = pinnumber; + pinMode(pinnumber, OUTPUT); + } +} diff --git a/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/DTSettings.h b/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/DTSettings.h new file mode 100644 index 0000000..4cbaac0 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/232_Data_Transfer_Test_Receiver/DTSettings.h @@ -0,0 +1,60 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t packetdelaymS = 0; //mS delay between transmitted packets + +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t DTfilenamesize = 32; //size of DTfilename buffer + +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet +const uint8_t DTSendAttempts = 10; //number of attempts sending a packet before a restart + +const uint32_t DTFileSize = 65535; //size of file to simulate + +#ifdef USELORA +const uint8_t DTSegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t DTSegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/DataTransfer/233_SDfile_Transfer_Transmitter/233_SDfile_Transfer_Transmitter.ino b/examples/SX128x_examples/DataTransfer/233_SDfile_Transfer_Transmitter/233_SDfile_Transfer_Transmitter.ino new file mode 100644 index 0000000..48aabef --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/233_SDfile_Transfer_Transmitter/233_SDfile_Transfer_Transmitter.ino @@ -0,0 +1,189 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program that transfers a file using data transfer (DT) packet functions + from the SX128X library to send a file from the SD card on one Arduino to the SD card on another Arduino. + Arduino DUEs were used for the test and this example transfers an JPG image. + + DT packets can be used for transfering large amounts of data in a sequence of packets or segments, + in a reliable and resiliant way. The file open requests to the remote receiver, each segement sent and + the remote file close will all keep transmitting until a valid acknowledge comes from the receiver. + Use this transmitter with the matching receiver program, 234_SDfile_Transfer_Receiver.ino. + + On transmission the NetworkID and CRC of the payload are appended to the end of the packet by the library + routines. The use of a NetworkID and CRC ensures that the receiver can validate the packet to a high degree + of certainty. + + The transmitter sends the sequence of segments in order. If the sequence fails for some reason, the receiver + will return a NACK packet to the transmitter requesting the segment sequence it was expecting. + + Details of the packet identifiers, header and data lengths and formats used are in the file; + 'Data transfer packet definitions.md' in the \SX128X_examples\DataTransfer\ folder. + + The transfer can be carried out using LoRa or FLRC packets, max segment size (defined by DTSegmentSize) is + 245 bytes for LoRa and 117 bytes for FLRC. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include + +#include +#include +#include "DTSettings.h" //LoRa settings etc. + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa, required by SDtransfer.h + +#define ENABLEMONITOR //enable monitor prints +#define PRINTSEGMENTNUM //enable this define to print segment numbers +#define ENABLEFILECRC //enable this define to uses and show file CRCs +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking +//#define DEBUG //see additional debug info + +//#define SDLIB //define SDLIB for SD.h or SDFATLIB for SDfat.h +#define SDFATLIB + +#include //library of SD functions +#include //library of data transfer functions + +//choice of files to send +//char FileName[] = "/$50SATL.JPG"; //file length 63091 bytes, file CRC 0x59CE +char FileName[] = "/$50SATS.JPG"; //file length 6880 bytes, file CRC 0x0281 +//char FileName[] = "/$50SATT.JPG"; //file length 1068 bytes, file CRC 0x6A02 + + +void loop() +{ + uint32_t filelength; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Transfer started")); +#endif + + filelength = SDsendFile(FileName, sizeof(FileName)); + + if (filelength) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transfer finished")); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transfer failed")); + Monitorport.println(); +#endif + } + + delay(15000); + +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + SDsetLED(LED1); //setup LED pin for data transfer indicator + +#ifdef ENABLEMONITOR + Monitorport.begin(115200); + Monitorport.println(); + Monitorport.println(F(__FILE__)); + Monitorport.flush(); +#endif + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("LoRa device error")); +#endif + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.print(F("Initializing SD card...")); +#endif + + if (DTSD_initSD(SDCS)) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("SD Card initialized.")); +#endif + } + else + { + Monitorport.println(F("SD Card failed, or not present.")); + while (1) led_Flash(100, 25); + } + +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Payload CRC disabled")); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Payload CRC enabled")); +#endif + } + + SDDTFileTransferComplete = false; + +#ifdef ENABLEMONITOR + Monitorport.println(F("SDfile transfer ready")); + Monitorport.println(); +#endif +} diff --git a/examples/SX128x_examples/DataTransfer/233_SDfile_Transfer_Transmitter/DTSettings.h b/examples/SX128x_examples/DataTransfer/233_SDfile_Transfer_Transmitter/DTSettings.h new file mode 100644 index 0000000..994dfaf --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/233_SDfile_Transfer_Transmitter/DTSettings.h @@ -0,0 +1,64 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission +#define SDCS 30 +#define Monitorport Serial //Port where serial prints go + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc + +const uint8_t StartAttempts = 2; //number of attempts to start transfer before a fail +const uint8_t SendAttempts = 5; //number of attempts carrying out a process before a restart +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer + +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t Maxfilenamesize = 32; //size of DTfilename buffer + +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/DataTransfer/234_SDfile_Transfer_Receiver/234_SDfile_Transfer_Receiver.ino b/examples/SX128x_examples/DataTransfer/234_SDfile_Transfer_Receiver/234_SDfile_Transfer_Receiver.ino new file mode 100644 index 0000000..85e836f --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/234_SDfile_Transfer_Receiver/234_SDfile_Transfer_Receiver.ino @@ -0,0 +1,164 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 10/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a test program for the use of a data transfer (DT) packet to send a file + from the SD card on one Arduino to the SD card on another Arduino, Arduino DUEs were used for the test. + + DT packets can be used for transfering large amounts of data in a sequence of packets or segments, + in a reliable and resiliant way. The remote file open request, the segements sent and the remote file close + will be transmitted until a valid acknowledge comes from the receiver. Use with the matching transmitter + program, 233_LoRa_SDfile_Transfer_Transmitter.ino. + + Each DT packet contains a variable length header array and a variable length data array as the payload. + On transmission the NetworkID and CRC of the payload are appended to the end of the packet by the library + routines. The use of a NetworkID and CRC ensures that the receiver can validate the packet to a high degree + of certainty. The receiver will not accept packets that dont have the appropriate NetworkID or payload CRC + at the end of the packet. + + The transmitter sends a sequence of segments in order and the receiver keeps track of the sequence. If + the sequence fails for some reason, the receiver will return a NACK packet to the transmitter requesting + the segment sequence it was expecting. + + The transfer can be carried out using LoRa or FLRC packets, max segment size (defined by DTSegmentSize) is + 245 bytes for LoRa and 117 bytes for FLRC. + + Details of the packet identifiers, header and data lengths and formats used are in the file + Data_transfer_packet_definitions.md in the \SX128X_examples\DataTransfer\ folder. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include + +#include +#include +#include "DTSettings.h" //LoRa settings etc. + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa, required by SDtransfer.h + +//#define SDLIB //define SDLIB for SD.h or SDFATLIB for SDfat.h +#define SDFATLIB + +#define ENABLEMONITOR //enable monitor prints +#define PRINTSEGMENTNUM +#define ENABLEFILECRC //enable this define to uses and show file CRCs +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking +//#define DEBUG //see additional debug info + +#include //library of SD functions +#include //library of data transfer functions + + + +void loop() +{ + SDreceiveaPacketDT(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + SDsetLED(LED1); //setup LED pin for data transfer indicator + +#ifdef ENABLEMONITOR + Monitorport.begin(115200); + Monitorport.println(); + Monitorport.println(F(__FILE__)); +#endif + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("LoRa device error")); +#endif + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.print(F("Initializing SD card...")); +#endif + + if (DTSD_initSD(SDCS)) + { + Monitorport.println(F("SD Card initialized.")); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("SD Card failed, or not present.")); +#endif + while (1) led_Flash(100, 50); + } + +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Monitorport.println(F("Payload CRC disabled")); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Payload CRC enabled")); +#endif + } + + + SDDTSegmentNext = 0; + SDDTFileOpened = false; + +#ifdef ENABLEMONITOR + Monitorport.println(F("SDfile transfer receiver ready")); + Monitorport.println(); +#endif + + +} diff --git a/examples/SX128x_examples/DataTransfer/234_SDfile_Transfer_Receiver/DTSettings.h b/examples/SX128x_examples/DataTransfer/234_SDfile_Transfer_Receiver/DTSettings.h new file mode 100644 index 0000000..5b1bf83 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/234_SDfile_Transfer_Receiver/DTSettings.h @@ -0,0 +1,64 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission +#define SDCS 30 +#define Monitorport Serial //Port where serial prints go + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc + +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t Maxfilenamesize = 32; //size of DTfilename buffer + +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet +const uint8_t SendAttempts = 10; //number of attempts sending a packet or attempting a process before a restart of transfer +const uint8_t StartAttempts = 10; //number of attempts sending the file + + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/235_Array_Transfer_Transmitter.ino b/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/235_Array_Transfer_Transmitter.ino new file mode 100644 index 0000000..beea9b3 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/235_Array_Transfer_Transmitter.ino @@ -0,0 +1,846 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a simulation test program for the use of a data transfer (DT) packet to send + the contents of a memory array (DTsendarray) on one Arduino to the SD card as a file on another Arduino, + Arduino DUEs were used for the test. For this example the memory array is first loaded from an SD file. + + DT packets can be used for transfering large amounts of data in a sequence of packets or segments, + in a reliable and resiliant way. The remote file open request, the segements sent and the remote file close + will be transmitted until a valid acknowledge comes from the receiver. Use with the matching receiver + program, 234_SDfile_Transfer_Receiver.ino or 236_SDfile_Transfer_ReceiverIRQ.ino. + + Each DT packet contains a variable length header array and a variable length data array as the payload. + On transmission the NetworkID and CRC of the payload are appended to the end of the packet by the library + routines. The use of a NetworkID and CRC ensures that the receiver can validate the packet to a high degree + of certainty. The receiver will not accept packets that dont have the appropriate NetworkID or payload CRC + at the end of the packet. + + The transfer can be carried out using LoRa or FLRC packets, max segment size (defined by DTSegmentSize) is + 245 bytes for LoRa and 117 bytes for FLRC. + + Details of the packet identifiers, header and data lengths and formats used are in the file + Data_transfer_packet_definitions.md in the \SX128X_examples\DataTransfer\ folder. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include +#include +#include +#include "Settings.h" //LoRa settings etc. +#include +#include "Variables.h" + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa + +#include +SdFat SD; +File dataFile; //name the file instance needed for SD library routines + +uint8_t DTheader[16]; //header array +uint8_t DTdata[245]; //data/segment array +uint8_t DTsendarray[0x10000]; //create a global array to hold data to transfer +uint8_t *ptrDTsendarray; //create a global pointer to the array to send, so all functions have access + +//choice of files to send +char DTfilenamebuff[] = "/$50SATS.JPG"; //file length 6880 bytes, file CRC 0x0281 +//char DTfilenamebuff[] = "/$50SATT.JPG"; //file length 1068 bytes, file CRC 0x6A02 + + +//#define DEBUG //enable define to see more detail for data transfer operation +//#define DEBUGSDLIB //enable define to see more detail for SD operation +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking + + +void loop() +{ + + DTLocalArrayLength = moveFileArray(DTfilenamebuff, DTsendarray, sizeof(DTsendarray)); //move the file to a global array to be sent + + if (DTLocalArrayLength == 0) + { + Serial.println("ERROR moving local file to array - program halted"); + while (1); + } + + doArrayTransfer(DTsendarray); + Serial.println("Array transfer complete - program halted"); + while (1); +} + + +void printSeconds() +{ + float secs; + secs = ( (float) millis() / 1000); + Serial.print(secs, 3); + Serial.print(F(" ")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +//********************************** +// Start Code for transfer of array +//********************************** + +bool doArrayTransfer(uint8_t *ptrarray) +{ + ptrDTsendarray = ptrarray; //set the global ptr for the the array to send + DTLocalArrayCRC = LoRa.CRCCCITT(ptrDTsendarray, DTLocalArrayLength, 0xFFFF); + + DTStartmS = millis(); + do + { + if (startArrayTransfer(DTfilenamebuff, sizeof(DTfilenamebuff))) //sends the DTsendarray to a remote filename + { + Serial.print(DTfilenamebuff); + Serial.println(" opened OK on remote"); + DTNumberSegments = getNumberSegments(DTLocalArrayLength, DTSegmentSize); + DTLastSegmentSize = getLastSegmentSize(DTLocalArrayLength, DTSegmentSize); + printLocalFileDetails(); + Serial.println(); + NoAckCount = 0; + } + else + { + Serial.print(DTfilenamebuff); + Serial.println("Error opening remote file - restart transfer"); + DTFileTransferComplete = false; + continue; + } + + delay(packetdelaymS); + + if (!sendSegments()) + { + Serial.println(); + Serial.println(F("**********************************************************")); + Serial.println(F("Error - Segment write with no file open - Restart received")); + Serial.println(F("**********************************************************")); + Serial.println(); + continue; + } + + if (endArrayTransfer(DTfilenamebuff, sizeof(DTfilenamebuff))) //send command to close remote file + { + //the header returned from file close contains a 16bit CRC of the file saved on the remotes SD + Serial.print(DTfilenamebuff); + Serial.println(" closed OK on remote"); + beginarrayRW(DTheader, 4); + DTRemoteFileLength = arrayReadUint32(); + DTRemoteFileCRC = arrayReadUint16(); + Serial.print(F("Acknowledged remote file length ")); + Serial.println(DTRemoteFileLength); + Serial.print(F("Acknowledged remote file CRC 0x")); + Serial.println(DTRemoteFileCRC, HEX); + } + else + { + DTFileTransferComplete = false; + Serial.println(F("ERROR send close remote file failed - program halted")); + } + + Serial.print(F("NoAckCount ")); + Serial.println( NoAckCount); + Serial.print(F("Total file transmit time ")); + Serial.print(millis() - DTStartmS); + Serial.println(F("mS")); + DTFileTransferComplete = true; + } + while (!DTFileTransferComplete); + + return true; +} + + +uint16_t getNumberSegments(uint32_t arraysize, uint8_t segmentsize) +{ + uint16_t segments; + segments = arraysize / segmentsize; + if ((arraysize % segmentsize) > 0) + { + segments++; + } + return segments; +} + + +uint8_t getLastSegmentSize(uint32_t arraysize, uint8_t segmentsize) +{ + uint8_t lastsize; + lastsize = arraysize % segmentsize; + if (lastsize == 0) + { + lastsize = segmentsize; + } + return lastsize; +} + + +bool startArrayTransfer(char *buff, uint8_t filenamesize) +{ + uint8_t ValidACK; + + Serial.print(F("Start Array transfer ")); + Serial.print(DTLocalArrayLength); + Serial.println(F(" bytes")); + + build_DTFileOpenHeader(DTheader, DTFileOpenHeaderL, filenamesize, DTLocalArrayLength, DTLocalArrayCRC, DTSegmentSize); + LocalPayloadCRC = LoRa.CRCCCITT((uint8_t *) buff, filenamesize, 0xFFFF); + + do + { + Serial.println(F("Transmit open remote file request")); + digitalWrite(LED1, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTFileOpenHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); + TXNetworkID = LoRa.getTXNetworkID(TXPacketL); //get the networkID appended to packet + TXArrayCRC = LoRa.getTXPayloadCRC(TXPacketL); //get the payload CRC appended to packet + +#ifdef DEBUG + Serial.print(F("TXNetworkID,0x")); + Serial.print(TXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Serial.print(F(",TXarrayCRC,0x")); + Serial.println(TXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Serial.println(); +#endif + + if (TXPacketL == 0) //if there has been a send and ack error, TXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + Serial.print(F("Wait ACK ")); + ValidACK = LoRa.waitACKDT(DTheader, DTFileOpenHeaderL, ACKtimeoutDTmS); + RXPacketType = DTheader[0]; + + if ((ValidACK > 0) && (RXPacketType == DTFileOpenACK)) + { + Serial.println(F(" - Valid ACK received")); +#ifdef DEBUG + printPacketHex(); +#endif + } + else + { + NoAckCount++; + Serial.println(F("No Valid ACK received")); +#ifdef DEBUG + printACKdetail(); + Serial.print(F("ACKPacket ")); + printPacketHex(); +#endif + Serial.println(); + } + Serial.println(); + } + while (ValidACK == 0); + + return true; +} + + +void build_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //this builds the header buffer for the filename passed + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileOpen); //byte 0, write the packet type + arrayWriteUint8(0); //byte 1, initial DTflags byte, not used here + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write dataarray (filename) CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +bool sendSegments() +{ + //start the array transfer at segment 0 + DTSegment = 0; + DTSentSegments = 0; + + DTarraylocation = 0; //ensure at first position in array + + while (DTSegment < (DTNumberSegments - 1)) + { + printSeconds(); + + if (sendArraySegment(DTSegment, DTSegmentSize)) + { + Serial.println(); + //DTSegment++; + DTSentSegments++; + } + else + { + Serial.println(F("ERROR in sendArraySegment")); + Serial.println(); + return false; + } + + delay(packetdelaymS); + }; + + Serial.println("Last segment"); + + if (!sendArraySegment(DTSegment, DTLastSegmentSize)) + { + Serial.println(F("ERROR in sendArraySegment")); + return false; + } + + return true; +} + + +bool sendArraySegment(uint16_t segnum, uint8_t segmentsize) +{ + uint8_t ValidACK; + uint8_t index; + uint8_t tempdata; + + for (index = 0; index < segmentsize; index++) + { + tempdata = ptrDTsendarray[DTarraylocation]; + DTdata[index] = tempdata; + DTarraylocation++; + } + + build_DTSegmentHeader(DTheader, DTSegmentWriteHeaderL, segmentsize, segnum); + + Serial.print(F("Send Segment,")); + Serial.print(segnum); + Serial.print(F(" ")); + printheader(DTheader, DTSegmentWriteHeaderL); + Serial.println(); + + do + { + digitalWrite(LED1, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTSegmentWriteHeaderL, (uint8_t *) DTdata, segmentsize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); + + if (TXPacketL == 0) //if there has been a send TXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + ValidACK = LoRa.waitACKDT(DTheader, DTSegmentWriteHeaderL, ACKtimeoutDTmS); + RXPacketType = DTheader[0]; + + if (ValidACK > 0) + { + if (RXPacketType == DTSegmentWriteNACK) + { + DTSegment = DTheader[4] + (DTheader[5] << 8); //load what the segment should be + RXHeaderL = DTheader[2]; + Serial.println(); + Serial.println(F("************************************")); + Serial.print(F("Received restart request at segment ")); + Serial.println(DTSegment); + printheader(DTheader, RXHeaderL); + Serial.println(); + Serial.print(F("Seek to file location ")); + Serial.println(DTSegment * DTSegmentSize); + Serial.println(F("************************************")); + Serial.println(); + Serial.flush(); + DTarraylocation = (DTSegment * DTSegmentSize); + } + + //ack is valid, segment was acknowledged if here + + if (RXPacketType == DTStartNACK) + { + Serial.println(F("Received DTStartNACK ")); + return false; + } + + if (RXPacketType == DTSegmentWriteACK) + { + readACKHeader(); + AckCount++; + printPacketDetails(); + DTSegment++; //increase value for next segment + return true; + } + } + else + { + NoAckCount++; + Serial.print(F("Error No Ack ")); + Serial.print(F("NoAckCount,")); + Serial.print(NoAckCount); + LoRa.printReliableStatus(); + Serial.println(); + } + } while (ValidACK == 0); + + return true; +} + + +void build_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum) +{ + //this builds the header buffer for the a segment transmit + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTSegmentWrite); //write the packet type + arrayWriteUint8(0); //initial DTflags byte, not used here + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint16(segnum); //write the DTsegment number + endarrayRW(); +} + + +void printPacketDetails() +{ + PacketRSSI = LoRa.readPacketRSSI(); + PacketSNR = LoRa.readPacketSNR(); + Serial.print(F("AckCount,")); + Serial.print(AckCount); + Serial.print(F(",NoAckCount,")); + Serial.print(NoAckCount); + Serial.print(F(",AckRSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,AckSNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB")); + Serial.println(); +} + + +void printLocalFileDetails() +{ + Serial.print(DTfilenamebuff); + Serial.print(F(" LocalFilelength is ")); + Serial.print(DTLocalFileLength); + Serial.println(F(" bytes")); + Serial.print(DTfilenamebuff); + Serial.print(F(" Array to send CRC is 0x")); + Serial.println(DTLocalArrayCRC, HEX); + Serial.print(F("DTSegmentSize ")); + Serial.println(DTSegmentSize); + Serial.print(F("Number Segments ")); + Serial.println(DTNumberSegments); + Serial.print(F("DTLastSegmentSize ")); + Serial.println(DTLastSegmentSize); +} + + +void printPacketHex() +{ + RXPacketL = LoRa.readRXPacketL(); + Serial.print(RXPacketL); + Serial.print(F(" bytes > ")); + if (RXPacketL > 0) + { + LoRa.printSXBufferHEX(0, RXPacketL - 1); + } +} + + +void printACKdetail() +{ + Serial.print(F("ACKDetail")); + Serial.print(F(",RXNetworkID,0x")); + Serial.print(LoRa.getRXNetworkID(RXPacketL), HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(LoRa.getRXPayloadCRC(RXPacketL), HEX); + Serial.print(F(",RXPacketL,")); + Serial.print(RXPacketL); + Serial.print(F(" ")); + LoRa.printReliableStatus(); + Serial.println(); +} + + +void printdata(uint8_t *dataarray, uint8_t arraysize) +{ + Serial.print(F("DataBytes,")); + Serial.print(arraysize); + Serial.print(F(" ")); + printarrayHEX((uint8_t *) dataarray, arraysize); +} + + +void printheader(uint8_t *hdr, uint8_t hdrsize) +{ + Serial.print(F("HeaderBytes,")); + Serial.print(hdrsize); + Serial.print(F(" ")); + printarrayHEX(hdr, hdrsize); +} + + +void readACKHeader() +{ + //the first 6 bytes of the segment write header contain the important stuff, so load it up + //so we can decide what to do next. + beginarrayRW(DTheader, 0); //start buffer read at location 0 + RXPacketType = arrayReadUint8(); //load the packet type + RXFlags = arrayReadUint8(); //initial DTflags byte, not used here + RXHeaderL = arrayReadUint8(); //load the header length + RXDataarrayL = arrayReadUint8(); //load the datalength + DTSegment = arrayReadUint16(); //load the segment number +} + + +bool endArrayTransfer(char *buff, uint8_t filenamesize) +{ + uint8_t ValidACK; + + Serial.print(F("End file transfer ")); + Serial.println(buff); + DTSD_closeFile(); + build_DTFileCloseHeader(DTheader, DTFileCloseHeaderL, filenamesize, DTLocalArrayLength, DTLocalArrayCRC, DTSegmentSize); + + do + { + Serial.println(F("Transmit close remote file request")); + digitalWrite(LED1, HIGH); + TXPacketL = LoRa.transmitDT(DTheader, DTFileCloseHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); + TXNetworkID = LoRa.getTXNetworkID(TXPacketL); + TXArrayCRC = LoRa.getTXPayloadCRC(TXPacketL); + +#ifdef DEBUG + Serial.print(F("TXNetworkID,0x")); + Serial.print(TXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Serial.print(F(",TXarrayCRC,0x")); + Serial.println(TXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Serial.println(); +#endif + + if (TXPacketL == 0) //if there has been a send and ack error, RXPacketL returns as 0 + { + Serial.println(F("Transmit error")); + } + + Serial.print(F("Wait ACK ")); + ValidACK = LoRa.waitACKDT(DTheader, DTFileCloseHeaderL, ACKtimeoutDTmS); + RXPacketType = DTheader[0]; + + if ((ValidACK > 0) && (RXPacketType == DTFileCloseACK)) + { + Serial.println(F(" - Valid ACK received")); +#ifdef DEBUG + printPacketHex(); +#endif + } + else + { + NoAckCount++; + Serial.println(F("No Valid ACK received")); + printACKdetail(); +#ifdef DEBUG + Serial.print(F("ACKPacket ")); + printPacketHex(); +#endif + Serial.println(); + } + Serial.println(); + } + while (ValidACK == 0); + + return true; +} + + +void build_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //this builds the header buffer for the filename passed + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileClose); //byte 0, write the packet type + arrayWriteUint8(0); //byte 1, initial DTflags byte, not used here + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write dataarray (filename) CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +//********************************* +// Start Code for dealing with SD +//********************************* + +uint32_t moveFileArray(char *filenamebuff, uint8_t *buff, uint32_t buffsize) +{ + uint32_t index; + + ptrDTsendarray = buff; //assign passed array ptr to global ptr + DTLocalFileLength = DTSD_getFileSize(filenamebuff); //get the file length + + if (DTLocalFileLength == 0) + { + Serial.print(F("Error - opening local file ")); + Serial.println(filenamebuff); + return 0; + } + + if (DTLocalFileLength > buffsize) + { + Serial.println(filenamebuff); + Serial.print(F("Error - file length of ")); + Serial.print(DTLocalFileLength); + Serial.print(F(" bytes exceeds array length of ")); + Serial.print(buffsize); + Serial.println(F(" bytes")); + return 0; + } + + DTSD_openFileRead(filenamebuff); + Serial.print(F("Opened local file ")); + Serial.print(filenamebuff); + Serial.print(F(" ")); + Serial.print(DTLocalFileLength); + Serial.println(F(" bytes")); + DTLocalFileCRC = DTSD_fileCRCCCITT(DTLocalFileLength); //get file CRC from position 0 to end + Serial.print(F("DTLocalFileCRC 0x")); + Serial.println(DTLocalFileCRC, HEX); + + //now tranfer SD file to global array + dataFile.seek(0); //ensure at first position in file + for (index = 0; index < DTLocalFileLength; index++) + { + buff[index] = dataFile.read(); + } + + Serial.println(F("DTsendarray loaded from SD")); + Serial.print(F("Last written location ")); + Serial.println(index); + Serial.print(F("First 16 bytes of array to send ")); + LoRa.printHEXPacket(buff, 16); + Serial.println(); + DTarraylocation = 0; + return DTLocalFileLength; +} + + +bool DTSD_initSD(uint8_t CSpin) +{ + if (SD.begin(CSpin)) + { + return true; + } + else + { + return false; + } +} + + +void DTSD_printDirectory() +{ + dataFile = SD.open("/"); + Serial.println("Card directory"); + SD.ls("/", LS_R); +} + + +uint32_t DTSD_openFileRead(char *buff) +{ + uint32_t filesize; + dataFile = SD.open(buff); + filesize = dataFile.size(); + dataFile.seek(0); + return filesize; +} + + +void DTSD_closeFile() +{ + dataFile.close(); //close local file +} + + +uint16_t DTSD_fileCRCCCITT(uint32_t fsize) +{ + //does a CRC calculation on the file open via dataFile + uint32_t index; + uint16_t CRCcalc; + uint8_t j, filedata; + + CRCcalc = 0xFFFF; //start value for CRC16 + dataFile.seek(0); //be sure at start of file position + + for (index = 0; index < fsize; index++) + { + filedata = dataFile.read(); +#ifdef DEBUGSDLIB + Serial.print(F(" 0x")); + Serial.print(filedata, HEX); +#endif + CRCcalc ^= (((uint16_t) filedata ) << 8); + for (j = 0; j < 8; j++) + { + if (CRCcalc & 0x8000) + CRCcalc = (CRCcalc << 1) ^ 0x1021; + else + CRCcalc <<= 1; + } + } + +#ifdef DEBUGSDLIB + Serial.print(F(" {DEBUGSDLIB} ")); + Serial.print(index); + Serial.print(F(" Bytes checked - CRC ")); + Serial.println(CRCcalc, HEX); +#endif + + return CRCcalc; +} + + +uint16_t DTSD_getNumberSegments(uint32_t filesize, uint8_t segmentsize) +{ + uint16_t segments; + segments = filesize / segmentsize; + if ((filesize % segmentsize) > 0) + { + segments++; + } + return segments; +} + + +uint8_t DTSD_getLastSegmentSize(uint32_t filesize, uint8_t segmentsize) +{ + uint8_t lastsize; + lastsize = filesize % segmentsize; + if (lastsize == 0) + { + lastsize = segmentsize; + } + return lastsize; +} + + +void DTSD_seekFileLocation(uint32_t position) +{ + dataFile.seek(position); //seek to position in file + return; +} + + +uint32_t DTSD_getFileSize(char *buff) +{ + uint32_t filesize; + + if (!SD.exists(buff)) + { + return 0; + } + + dataFile = SD.open(buff); + filesize = dataFile.size(); + dataFile.close(); + return filesize; +} + +//******************************* +// End Code for dealing with SD +//******************************* + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(115200); + + while (!Serial); // wait for serial port to connect. Needed for native USB + + Serial.println(); + Serial.println(F("235_Array_Transfer_Transmitter starting")); + Serial.flush(); + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + + Serial.print(F("Initializing SD card...")); + + if (DTSD_initSD(SDCS)) + { + Serial.println(F("Card initialized.")); + } + else + { + Serial.println(F("Card failed, or not present.")); + while (1) led_Flash(100, 25); + } + + Serial.println(); + DTSD_printDirectory(); + Serial.println(); + Serial.println(); + Serial.println(F("Array transfer ready")); + Serial.println(); + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Serial.println(F("Payload CRC disabled")); + } + else + { + Serial.println(F("Payload CRC enabled")); + } + + DTFileTransferComplete = false; + +} diff --git a/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/Settings.h b/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/Settings.h new file mode 100644 index 0000000..98c66ae --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/Settings.h @@ -0,0 +1,50 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/03/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission +#define SDCS 30 + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after packet actioned and ack sent +const uint32_t ACKtimeoutDTmS = 100; //mS to wait for receiving an ACK and re-trying TX +const uint32_t packetdelaymS = 0; //mS delay between transmitted packets +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +#ifdef USELORA +const uint8_t DTSegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t DTSegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/Variables.h b/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/Variables.h new file mode 100644 index 0000000..a2e1e3c --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/235_Array_Transfer_Transmitter/Variables.h @@ -0,0 +1,35 @@ +uint8_t RXPacketType; //type of received packet, segment write, ACK, NACK etc +uint8_t RXPacketL; //length of received packet +uint16_t RXErrors; //count of packets received with error +uint8_t RXFlags; //DTflags byte in header, could be used to control actions in TX and RX +uint8_t RXHeaderL; //length of header +uint8_t RXDataarrayL; //length of data array\segment +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + +uint16_t TXNetworkID; //this is used to identify a transaction, receiver must have the same DTnetworkID +uint16_t TXArrayCRC; //should contain CRC of data array sent +uint8_t TXPacketL; //length of transmitted packet + +uint16_t LocalPayloadCRC; //for calculating the local data array CRC +uint16_t DTLocalFileCRC; //CRC of file being transferred +uint16_t DTLocalArrayCRC; //CRC of array being transferred +uint32_t DTLocalFileLength; //length of file to transfer +uint32_t DTLocalArrayLength; //length of array to send +uint16_t DTSegment; //current segment number +uint16_t DTNumberSegments; //number of segments for a file transfer +uint8_t DTLastSegmentSize; //size of the last segment +uint16_t DTSegmentNext; //next segment to send\receive +uint16_t DTReceivedSegments; //count of segments received +uint16_t DTSegmentLast; //last segment to send\receive +uint16_t DTSentSegments; //count of segments sent +uint32_t DTarraylocation; //a global variable giving the location in the array last written to +uint16_t DTRemoteFileCRC; //CRC of returned of the remote saved file +uint32_t DTRemoteFileLength; //filelength returned of the remote saved file +uint32_t DTStartmS; //used for timeing transfers + +uint16_t AckCount; //keep a track of acks that are received within timeout period +uint16_t NoAckCount; //keep a track of acks not received within timeout period + +bool DTFileIsOpen; //bool to record if file open or closed +bool DTFileTransferComplete; //bool to flag file transfer complete diff --git a/examples/SX128x_examples/DataTransfer/236_SDfile_Transfer_TransmitterIRQ/236_SDfile_Transfer_TransmitterIRQ.ino b/examples/SX128x_examples/DataTransfer/236_SDfile_Transfer_TransmitterIRQ/236_SDfile_Transfer_TransmitterIRQ.ino new file mode 100644 index 0000000..fa19b9b --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/236_SDfile_Transfer_TransmitterIRQ/236_SDfile_Transfer_TransmitterIRQ.ino @@ -0,0 +1,191 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/02/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program that transfers a file using data transfer (DT) packet functions + from the SX128X library to send a file from the SD card on one Arduino to the SD card on another Arduino. + Arduino DUEs were used for the test and this example transfers an JPG image. + + This program uses routines that do not need to use the DIO1 pin on the LoRa device which is usually used + to indicate RXDONE or TXDONE. + + DT packets can be used for transfering large amounts of data in a sequence of packets or segments, + in a reliable and resiliant way. The file open requests to the remote receiver, each segement sent and + the remote file close will all keep transmitting until a valid acknowledge comes from the receiver. + Use this transmitter with the matching receiver program, 234_SDfile_Transfer_Receiver.ino. + + On transmission the NetworkID and CRC of the payload are appended to the end of the packet by the library + routines. The use of a NetworkID and CRC ensures that the receiver can validate the packet to a high degree + of certainty. + + The transmitter sends the sequence of segments in order. If the sequence fails for some reason, the receiver + will return a NACK packet to the transmitter requesting the segment sequence it was expecting. + + Details of the packet identifiers, header and data lengths and formats used are in the file; + 'Data transfer packet definitions.md' in the \SX128X_examples\DataTransfer\ folder. + + The transfer can be carried out using LoRa or FLRC packets, max segment size (defined by DTSegmentSize) is + 245 bytes for LoRa and 117 bytes for FLRC. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include + +#include +#include +#include "DTSettings.h" //LoRa settings etc. + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa, required by SDtransfer.h + +#define ENABLEMONITOR //enable monitor prints +#define PRINTSEGMENTNUM //enable this define to print segment numbers +#define ENABLEFILECRC //enable this define to uses and show file CRCs +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking + +//#define SDLIB //define SDLIB for SD.h or SDFATLIB for SDfat.h +#define SDFATLIB + +#include //library of SD functions +#include //library of data transfer functions + +//choice of files to send +//char FileName[] = "/$50SATL.JPG"; //file length 63091 bytes, file CRC 0x59CE +char FileName[] = "/$50SATS.JPG"; //file length 6880 bytes, file CRC 0x0281 +//char FileName[] = "/$50SATT.JPG"; //file length 1068 bytes, file CRC 0x6A02 + + +void loop() +{ + uint32_t filelength; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Transfer started")); +#endif + + filelength = SDsendFile(FileName, sizeof(FileName)); + + if (filelength) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transfer finished")); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transfer failed")); + Monitorport.println(); +#endif + } + + delay(15000); + +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + SDsetLED(LED1); //setup LED pin for data transfer indicator + +#ifdef ENABLEMONITOR + Monitorport.begin(115200); + Monitorport.println(); + Monitorport.println(F(__FILE__)); + Monitorport.flush(); +#endif + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("LoRa device error")); +#endif + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.print(F("Initializing SD card...")); +#endif + + if (DTSD_initSD(SDCS)) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("SD Card initialized.")); +#endif + } + else + { + Monitorport.println(F("SD Card failed, or not present.")); + while (1) led_Flash(100, 25); + } + +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Payload CRC disabled")); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Payload CRC enabled")); +#endif + } + + SDDTFileTransferComplete = false; + +#ifdef ENABLEMONITOR + Monitorport.println(F("SDfile transfer ready")); + Monitorport.println(); +#endif +} diff --git a/examples/SX128x_examples/DataTransfer/236_SDfile_Transfer_TransmitterIRQ/DTSettings.h b/examples/SX128x_examples/DataTransfer/236_SDfile_Transfer_TransmitterIRQ/DTSettings.h new file mode 100644 index 0000000..73c3f94 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/236_SDfile_Transfer_TransmitterIRQ/DTSettings.h @@ -0,0 +1,60 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/02/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define LED1 8 //LED used to indicate transmission +#define SDCS 30 +#define Monitorport Serial //Port where serial prints go + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 0; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc +const uint8_t StartAttempts = 2; //number of attempts to start transfer before a fail +const uint8_t SendAttempts = 5; //number of attempts carrying out a process before a restart +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer + +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t Maxfilenamesize = 32; //size of DTfilename buffer + +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/DataTransfer/237_SDfile_Transfer_ReceiverIRQ/237_SDfile_Transfer_ReceiverIRQ.ino b/examples/SX128x_examples/DataTransfer/237_SDfile_Transfer_ReceiverIRQ/237_SDfile_Transfer_ReceiverIRQ.ino new file mode 100644 index 0000000..41edec7 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/237_SDfile_Transfer_ReceiverIRQ/237_SDfile_Transfer_ReceiverIRQ.ino @@ -0,0 +1,165 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/02/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a test program for the use of a data transfer (DT) packet to send a file + from the SD card on one Arduino to the SD card on another Arduino, Arduino DUEs were used for the test. + + This program uses routines that do not need to use the DIO1 pin on the LoRa device which is usually used + to indicate RXDONE or TXDONE. + + DT packets can be used for transfering large amounts of data in a sequence of packets or segments, + in a reliable and resiliant way. The remote file open request, the segements sent and the remote file close + will be transmitted until a valid acknowledge comes from the receiver. Use with the matching transmitter + program, 233_LoRa_SDfile_Transfer_Transmitter.ino. + + Each DT packet contains a variable length header array and a variable length data array as the payload. + On transmission the NetworkID and CRC of the payload are appended to the end of the packet by the library + routines. The use of a NetworkID and CRC ensures that the receiver can validate the packet to a high degree + of certainty. The receiver will not accept packets that dont have the appropriate NetworkID or payload CRC + at the end of the packet. + + The transmitter sends a sequence of segments in order and the receiver keeps track of the sequence. If + the sequence fails for some reason, the receiver will return a NACK packet to the transmitter requesting + the segment sequence it was expecting. + + The transfer can be carried out using LoRa or FLRC packets, max segment size (defined by DTSegmentSize) is + 245 bytes for LoRa and 117 bytes for FLRC. + + Details of the packet identifiers, header and data lengths and formats used are in the file + Data_transfer_packet_definitions.md in the \SX128X_examples\DataTransfer\ folder. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ +#define USELORA //enable this define to use LoRa packets +//#define USEFLRC //enable this define to use FLRC packets + +#include + +#include +#include +#include "DTSettings.h" //LoRa settings etc. + +SX128XLT LoRa; //create an SX128XLT library instance called LoRa, required by SDtransfer.h + +//#define SDLIB //define SDLIB for SD.h or SDFATLIB for SDfat.h +#define SDFATLIB + +#define ENABLEMONITOR //enable monitor prints +#define PRINTSEGMENTNUM +#define ENABLEFILECRC //enable this define to uses and show file CRCs +//#define DISABLEPAYLOADCRC //enable this define if you want to disable payload CRC checking + +#include //library of SD functions +#include //library of data transfer functions + + +void loop() +{ + SDreceiveaPacketDT(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + SDsetLED(LED1); //setup LED pin for data transfer indicator + +#ifdef ENABLEMONITOR + Monitorport.begin(115200); + Monitorport.println(); + Monitorport.println(F(__FILE__)); +#endif + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("LoRa device error")); +#endif + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + +#ifdef USELORA + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.println(F("Using LoRa packets")); +#endif + +#ifdef USEFLRC + LoRa.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + Serial.println(F("Using FLRC packets")); +#endif + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.print(F("Initializing SD card...")); +#endif + + if (DTSD_initSD(SDCS)) + { + Monitorport.println(F("SD Card initialized.")); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("SD Card failed, or not present.")); +#endif + while (1) led_Flash(100, 50); + } + +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + +#ifdef DISABLEPAYLOADCRC + LoRa.setReliableConfig(NoReliableCRC); +#endif + + if (LoRa.getReliableConfig(NoReliableCRC)) + { + Monitorport.println(F("Payload CRC disabled")); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Payload CRC enabled")); +#endif + } + + + SDDTSegmentNext = 0; + SDDTFileOpened = false; + +#ifdef ENABLEMONITOR + Monitorport.println(F("SDfile transfer receiver ready")); + Monitorport.println(); +#endif + + +} diff --git a/examples/SX128x_examples/DataTransfer/237_SDfile_Transfer_ReceiverIRQ/DTSettings.h b/examples/SX128x_examples/DataTransfer/237_SDfile_Transfer_ReceiverIRQ/DTSettings.h new file mode 100644 index 0000000..03393dc --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/237_SDfile_Transfer_ReceiverIRQ/DTSettings.h @@ -0,0 +1,61 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 12/02/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define LED1 8 //LED used to indicate transmission +#define SDCS 30 +#define Monitorport Serial //Port where serial prints go + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power + +//******* Setup LoRa modem parameters here ! *************** +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//******* Setup FLRC modem parameters here ! *************** +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword + +const uint32_t TXtimeoutmS = 5000; //mS to wait for TX to complete +const uint32_t RXtimeoutmS = 60000; //mS to wait for receiving a packet +const uint32_t ACKdelaymS = 10; //ms delay after packet actioned and ack sent +const uint32_t ACKsegtimeoutmS = 75; //mS to wait for receiving an ACK before re-trying transmit segment +const uint32_t ACKopentimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file open +const uint32_t ACKclosetimeoutmS = 250; //mS to wait for receiving an ACK before re-trying transmit file close +const uint32_t DuplicatedelaymS = 10; //ms delay if there has been an duplicate segment or command receipt +const uint32_t NoAckCountLimit = 250; //if no NoAckCount exceeds this value - restart transfer +const uint32_t FunctionDelaymS = 0; //delay between functions such as open file, send segments etc +const uint32_t PacketDelaymS = 1000; //mS delay between transmitted packets such as DTInfo etc + +const uint8_t HeaderSizeMax = 12; //max size of header in bytes, minimum size is 7 bytes +const uint8_t DataSizeMax = 245; //max size of data array in bytes +const uint8_t Maxfilenamesize = 32; //size of DTfilename buffer + +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet +const uint8_t SendAttempts = 10; //number of attempts sending a packet or attempting a process before a restart of transfer +const uint8_t StartAttempts = 10; //number of attempts sending the file + +#ifdef USELORA +const uint8_t SegmentSize = 245; //number of bytes in each segment, 245 is maximum value for LoRa +#endif + +#ifdef USEFLRC +const uint8_t SegmentSize = 117; //number of bytes in each segment, 117 is maximum value for FLRC +#endif diff --git a/examples/SX128x_examples/DataTransfer/Data_transfer_packet_definitions.md b/examples/SX128x_examples/DataTransfer/Data_transfer_packet_definitions.md new file mode 100644 index 0000000..a7b8649 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/Data_transfer_packet_definitions.md @@ -0,0 +1,340 @@ +## Data Transfer packet Definitions + + 0xA0 Request to TX, DTSegmentWrite, Header Length 6, Data length Max, 245 + 0xA1 ACK to RX, DTSegmentWriteACK, Header Length 6, Data length Max, 245 + 0xA2 NACK to RX, DTSegmentWriteNACK, Header Length 6, Data length Max, 245 + + 0xA4 Request to TX, DTFileOpen, Header Length 12, Data length Max, 239 + 0xA5 ACK to RX, DTFileOpenACK, Header Length 12, Data length Max, 239 + 0xA6 NACK to RX, DTFileOpenNACK, Header Length 12, Data length Max, 239 + + 0xA8 Request to TX, DTFileClose, Header Length 12, Data length Max, 239 + 0xA9 ACK to RX, DTFileCloseACK, Header Length 12, Data length Max, 239 + 0xAA NACK to RX, DTFileCloseNACK, Header Length 12, Data length Max, 239 + + 0xAC Request to TX, DTFileSeek, Header Length 9, Data length Max, 242 + 0xAD ACK to RX, DTFileSeekACK, Header Length 9, Data length Max, 242 + 0xAE NACK to RX, DTFileSeekNACK, Header Length 9, Data length Max, 242 + + 0xB0 Request to TX, DTStart, Header Length 6, Data length Max, 245 + 0xB1 ACK to RX, DTStartACK, Header Length 6, Data length Max, 245 + 0xB2 NACK to RX, DTStartNACK, Header Length 6, Data length Max, 245 + + 0xB4 Request to TX, DTWake, Header Length 6, Data length Max, 245 + 0xB5 ACK to RX, DTWakeACK, Header Length 6, Data length Max, 245 + 0xB6 NACK to RX, DTWakeNACK, Header Length 6, Data length Max, 245 + + + 0xA0 + DTSegmentWrite, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xA0 + 1 Flags + 2 Header length + 3 Data length + 4 SegmentNum0 + 5 SegmentNum1 + Data + Byte Purpose + 6 DataArray Start + 7 More data etc + + + 0xA1 + DTSegmentWriteACK, Header Length 6 + Header + Byte Purpose + 0 0xA1 + 1 Flags + 2 Header length + 3 Data length + 4 SegmentNum0 + 5 SegmentNum1 + + 0xA2 + DTSegmentWriteACK, Header Length 6 + Header + Byte Purpose + 0 0xA2 + 1 Flags + 2 Header length + 3 Data length + 4 Required SegmentNum0 + 5 Required SegmentNum1 + + + 0xA4 + DTFileOpen, Header Length 12, Data length Max, 239 + Header + Byte Purpose + 0 0xA4 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + Data + Byte Purpose + 12 FilenameArray Start + 13 More FilenameArray etc + + + 0xA5 + DTFileOpenACK, Header Length 12 + Header + Byte Purpose + 0 0xA5 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + + 0xA6 + DTFileOpenNACK, Header Length 12 + Header + Byte Purpose + 0 0xA6 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + + + 0xA8 + DTFileClose, Header Length 12, Data length Max, 239 + Header + Byte Purpose + 0 0xA8 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + Data + Byte Purpose + 12 FilenameArray Start + 13 More FilenameArray etc + + + 0xA9 + DTFileCloseACK, Header Length 12 + Header + Byte Purpose + 0 0xA9 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + + 0xAA + DTFileCloseNACK, Header Length 12 + Header + Byte Purpose + 0 0xAA + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + + 0xAC + DTDataSeek, Header Length 9, Data length Max, 242 + Header + Byte Purpose + 0 0xAC + 1 Flags + 2 Header length + 3 Data length + 4 DataSeek0 + 5 DataSeek1 + 6 DataSeek2 + 7 DataSeek3 + 8Unused + Data + Byte Purpose + 9 FilenameArray Start + 13 More FilenameArray etc + + + 0xAD + DTDataSeekACK, Header Length 9, Data length Max, 242 + Header + Byte Purpose + 0 0xAD + 1 Flags + 2 Header length + 3 Data length + 4 DataSeek0 + 5 DataSeek1 + 6 DataSeek2 + 7 DataSeek3 + 8Unused + + + 0xAE + DTDataSeekNACK, Header Length 9, Data length Max, 242 + Header + Byte Purpose + 0 0xAE + 1 Flags + 2 Header length + 3 Data length + 4 DataSeek0 + 5 DataSeek1 + 6 DataSeek2 + 7 DataSeek3 + 8Unused + + + 0xB0 + DTStart, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB0 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + Data + Byte Purpose + 6 SegmentSize + 7 LastSegmentSize + 8 TXtimeoutmS0 + 9 TXtimeoutmS1 + 10 TXtimeoutmS2 + 11 TXtimeoutmS3 + 12 RXtimeoutmS0 + 13 RXtimeoutmS1 + 14 RXtimeoutmS2 + 15 RXtimeoutmS3 + 16 ACKtimeoutDTmS0 + 17 ACKtimeoutDTmS1 + 18 ACKtimeoutDTmS2 + 19 ACKtimeoutDTmS3 + 20 ACKdelaymS0 + 21 ACKdelaymS1 + 22 ACKdelaymS2 + 23 ACKdelaymS3 + 24 packetdelaymS0 + 25 packetdelaymS1 + 26 packetdelaymS2 + 27 packetdelaymS3 + 28 Frequency0 + 29 Frequency1 + 30 Frequency2 + 31 Frequency3 + 32 Offset0 + 33 Offset1 + 34 Offset2 + 35 Offset3 + 36 Spreading Factor + 37 Bandwidth + 38 Coding Rate + 39 Optimisation + 40 TXPower + 41 Implicit/Explicit + 42 TXattempts0 + 43 TXattempts1 + 44 HeaderSizeMax + 45 DataSizeMax + + + 0xB1 + DTStartACK, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB1 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + + 0xB2 + DTStartNACK, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB2 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + 0xB4 + DTWake, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB4 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + + 0xB5 + DTWakeACK, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB5 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + + 0xB6 + DTWakeNACK, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB6 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused \ No newline at end of file diff --git a/examples/SX128x_examples/DataTransfer/Data_transfer_packet_definitions.txt b/examples/SX128x_examples/DataTransfer/Data_transfer_packet_definitions.txt new file mode 100644 index 0000000..29e007a --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/Data_transfer_packet_definitions.txt @@ -0,0 +1,340 @@ +Data Transfer packet Definitions + + 0xA0 Request to TX, DTSegmentWrite, Header Length 6, Data length Max, 245 + 0xA1 ACK to RX, DTSegmentWriteACK, Header Length 6, Data length Max, 245 + 0xA2 NACK to RX, DTSegmentWriteNACK, Header Length 6, Data length Max, 245 + + 0xA4 Request to TX, DTFileOpen, Header Length 12, Data length Max, 239 + 0xA5 ACK to RX, DTFileOpenACK, Header Length 12, Data length Max, 239 + 0xA6 NACK to RX, DTFileOpenNACK, Header Length 12, Data length Max, 239 + + 0xA8 Request to TX, DTFileClose, Header Length 12, Data length Max, 239 + 0xA9 ACK to RX, DTFileCloseACK, Header Length 12, Data length Max, 239 + 0xAA NACK to RX, DTFileCloseNACK, Header Length 12, Data length Max, 239 + + 0xAC Request to TX, DTFileSeek, Header Length 9, Data length Max, 242 + 0xAD ACK to RX, DTFileSeekACK, Header Length 9, Data length Max, 242 + 0xAE NACK to RX, DTFileSeekNACK, Header Length 9, Data length Max, 242 + + 0xB0 Request to TX, DTStart, Header Length 6, Data length Max, 245 + 0xB1 ACK to RX, DTStartACK, Header Length 6, Data length Max, 245 + 0xB2 NACK to RX, DTStartNACK, Header Length 6, Data length Max, 245 + + 0xB4 Request to TX, DTWake, Header Length 6, Data length Max, 245 + 0xB5 ACK to RX, DTWakeACK, Header Length 6, Data length Max, 245 + 0xB6 NACK to RX, DTWakeNACK, Header Length 6, Data length Max, 245 + + + 0xA0 + DTSegmentWrite, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xA0 + 1 Flags + 2 Header length + 3 Data length + 4 SegmentNum0 + 5 SegmentNum1 + Data + Byte Purpose + 6 DataArray Start + 7 More data etc + + + 0xA1 + DTSegmentWriteACK, Header Length 6 + Header + Byte Purpose + 0 0xA1 + 1 Flags + 2 Header length + 3 Data length + 4 SegmentNum0 + 5 SegmentNum1 + + 0xA2 + DTSegmentWriteACK, Header Length 6 + Header + Byte Purpose + 0 0xA2 + 1 Flags + 2 Header length + 3 Data length + 4 Required SegmentNum0 + 5 Required SegmentNum1 + + + 0xA4 + DTFileOpen, Header Length 12, Data length Max, 239 + Header + Byte Purpose + 0 0xA4 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + Data + Byte Purpose + 12 FilenameArray Start + 13 More FilenameArray etc + + + 0xA5 + DTFileOpenACK, Header Length 12 + Header + Byte Purpose + 0 0xA5 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + + 0xA6 + DTFileOpenNACK, Header Length 12 + Header + Byte Purpose + 0 0xA6 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + + + 0xA8 + DTFileClose, Header Length 12, Data length Max, 239 + Header + Byte Purpose + 0 0xA8 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + Data + Byte Purpose + 12 FilenameArray Start + 13 More FilenameArray etc + + + 0xA9 + DTFileCloseACK, Header Length 12 + Header + Byte Purpose + 0 0xA9 + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + + 0xAA + DTFileCloseNACK, Header Length 12 + Header + Byte Purpose + 0 0xAA + 1 Flags + 2 Header length + 3 Data length + 4 Filelength0 + 5 Filelength1 + 6 Filelength2 + 7 Filelength3 + 8 FileCRC0 + 9 FileCRC1 + 10 SegmentSize + 11 Unused + + 0xAC + DTDataSeek, Header Length 9, Data length Max, 242 + Header + Byte Purpose + 0 0xAC + 1 Flags + 2 Header length + 3 Data length + 4 DataSeek0 + 5 DataSeek1 + 6 DataSeek2 + 7 DataSeek3 + 8Unused + Data + Byte Purpose + 9 FilenameArray Start + 13 More FilenameArray etc + + + 0xAD + DTDataSeekACK, Header Length 9, Data length Max, 242 + Header + Byte Purpose + 0 0xAD + 1 Flags + 2 Header length + 3 Data length + 4 DataSeek0 + 5 DataSeek1 + 6 DataSeek2 + 7 DataSeek3 + 8Unused + + + 0xAE + DTDataSeekNACK, Header Length 9, Data length Max, 242 + Header + Byte Purpose + 0 0xAE + 1 Flags + 2 Header length + 3 Data length + 4 DataSeek0 + 5 DataSeek1 + 6 DataSeek2 + 7 DataSeek3 + 8Unused + + + 0xB0 + DTStart, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB0 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + Data + Byte Purpose + 6 SegmentSize + 7 LastSegmentSize + 8 TXtimeoutmS0 + 9 TXtimeoutmS1 + 10 TXtimeoutmS2 + 11 TXtimeoutmS3 + 12 RXtimeoutmS0 + 13 RXtimeoutmS1 + 14 RXtimeoutmS2 + 15 RXtimeoutmS3 + 16 ACKtimeoutDTmS0 + 17 ACKtimeoutDTmS1 + 18 ACKtimeoutDTmS2 + 19 ACKtimeoutDTmS3 + 20 ACKdelaymS0 + 21 ACKdelaymS1 + 22 ACKdelaymS2 + 23 ACKdelaymS3 + 24 packetdelaymS0 + 25 packetdelaymS1 + 26 packetdelaymS2 + 27 packetdelaymS3 + 28 Frequency0 + 29 Frequency1 + 30 Frequency2 + 31 Frequency3 + 32 Offset0 + 33 Offset1 + 34 Offset2 + 35 Offset3 + 36 Spreading Factor + 37 Bandwidth + 38 Coding Rate + 39 Optimisation + 40 TXPower + 41 Implicit/Explicit + 42 TXattempts0 + 43 TXattempts1 + 44 HeaderSizeMax + 45 DataSizeMax + + + 0xB1 + DTStartACK, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB1 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + + 0xB2 + DTStartNACK, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB2 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + 0xB4 + DTWake, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB4 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + + 0xB5 + DTWakeACK, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB5 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused + + + 0xB6 + DTWakeNACK, Header Length 6, Data length Max, 245 + Header + Byte Purpose + 0 0xB6 + 1 Flags + 2 Header length + 3 Data length + 4 Unused + 5 Unused \ No newline at end of file diff --git a/examples/SX128x_examples/DataTransfer/ReadMe.md b/examples/SX128x_examples/DataTransfer/ReadMe.md new file mode 100644 index 0000000..a3d5df3 --- /dev/null +++ b/examples/SX128x_examples/DataTransfer/ReadMe.md @@ -0,0 +1,79 @@ +Several of the functions needed to make the data transfers work were already incorporated in the reliable packets functions, so I just had to add the library functions to transmit and receive the packets that consisted of a configurable header array and a data array. + +The additional functions needed were; + + uint8_t transmitDT(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t size, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait); + uint8_t receiveDT(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t size, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ); + uint8_t sendACKDT(uint8_t *header, uint8_t headersize, int8_t txpower); + uint8_t waitACKDT(uint8_t *header, uint8_t headersize, uint32_t acktimeout); + +The various sketch functions required such as sending an openfile command to the receiver, sending the the segments of the file and a close file command, would use the above core library functions with various different header formats. The detail of passing the file names across (for the receiver to save to SD with), calculating segment sizes and dealing with the segment sequencing would be dealt with at the Arduino sketch level where it would be easier to see exactly what is going on during a transfer. Perhaps at a later date it might be possible to move more of the required sketch code into a library function. + + +## Example Programs + +A simple test program was needed so that you could check a particular set of LoRa modem parameters to see how reliable they were at a chosen distance for sending the large packets. This is the purpose of the first two example sketches, '**231\_Data\_Transfer\_Test\_Transmitter.ino**' and '**232\_Data\_Transfer\_Test\_Receiver.ino**' + +The transmitter sends a test segment of size defined by DTSegmentSize in the Settings.h file where the LoRa modem settings can also be defined. + +The test program does implement a check on the segment sequence. If the receiver has just had segment 10, then it next expects segment 11. If something goes wrong and say segment 12 appears next then the receiver recognises this and sends a NACK packet back to the transmitter to restart the sequence from number 11. You can test this recovery at any time by resetting the transmitter. + +The program **221\_LoRa\_DTPacket\_Monitor.ino** or **222\_FLRC\_DTPacket\_Monitor.ino** can be used to monitor the progress of the test transmitter, be sure to use the same LoRa modem settings as the test transmitter and receiver. + +## File Transfer + +The purpose of the Data Transfer functions in the SX12XX Library is for applications such as moving files from one Arduino to another over a LoRa link. There are no guarantees in radio frequency reception of data packets, it's inevitable that some will be missed. Thus the data transfer functions need to deal with this as well as coping with packets from possible foreign sources or missed segments in the file. A single bit error in a graphic image for instance can render the image unreadable. + +The examples **233\_SDfile\_Transfer\_Transmitter.ino** and **234\_SDfile\_Transfer\_Receiver.ino** transfer a choice of files; $50SATL.JPG 63091 bytes, $50SATS.JPG 6880 bytes and $50SATT.JPG 1068 bytes) from the SD card on the transmitter Arduino to the SD card on another receiver Arduino. Arduino DUEs were used for testing these examples. The JPG image files above are located in the examples\SX128x_examples\DataTransfer folder and will need to be copied to the SD card on the transmitter. + +
+

+ +

+
+ +The transmitter starts the transfer by requesting that the file name chosen is opened on the remote receiver, the transmitter will keep sending this open file request till it succeeds. Then the actual segment\data transfer is started and each segment will be transmitted until it is accepted. If there is a segment sequence error at the receiver then the transmitter is notified and the transmission of segments restarts at the correct location. The last segment sent can be a different size to those in the main body of the file transfer. The transmitter then sends a file close request and the receiver then returns the file length and CRC of the file now on it's SD card data back to the transmitter. This is a further integrity check that the data has been transferred correctly. + +The transfer could be organised in such a way that the segment transmissions were blind, with no acknowledge, but that would then require the receiver to keep track of missed segments and later request re-transmission. There are some LoRa set-ups that use SSDV to transfer images from cameras, the image is processed and spilt into blocks and a part image can be displayed even if there are some blocks missing. However, the data transfer method described here has no processing that is dependant on an image or file type, it just treats the image or file as a string of bytes. + +## Transfer a memory array + +Example **235\_Array\_Transfer\_Transmitter.ino** is a version of **233\_SDfile\_Transfer\_Transmitter.ino** that demonstrates sending a memory array (DTsendarray) from a transmitter to a receiver that then saves the received array onto a file on an SD card. The DTsendarray is first populated with data from a file /$50SATS.JPG or /$50SATT.JPG by the transmitter. In this example the array is then sent as a sequence of segments, similar to the way a file would be read from SD and sent. + + +##Fine tuning for performance + +The speed of the data transfers is mainly dependant on the LoRa settings used, higher\faster data rates come from using a lower spreading factor +and a higher bandwidth, although of course the higher the data rate the shorter the distance covered. + +There are two program parameters in the example sketches that you may need to adjust. When the receiver has picked up a packet from the transmitter there is a programmable delay before the acknowledge is sent. This is the ACKdelaymS parameter. If the transmitter is particularly slow in changing from transmitting a packet and being ready to pick up the start of the acknowledge then it might miss it. Due to the delays in the receiver of writing a segment to SD an ACKdelaymS of 0 will likely work, but increase it if the transmitter is missing a lot of the acknowledge packets. + +A second parameter to adjust is the ACKtimeoutDTmS, and this is the period the transmitter waits for a valid acknowledge before sending the packet again. This time-out needs to be long enough to receive an acknowledge, but not too long or every missed acknowledge could slow down the transfer as it waits for the time-out period before re-transmitting. + + +## Achieved data rates + +With the segment length set at maximum for LoRa, 245 bytes, and LoRa settings of spreading factor 5, bandwidth 1600khz and coding rate 4:5 the 63019 byte $50SATL.JPG file took 4.54 seconds to transfer to the SD card on the receiver Arduino. That is an achieved data rate of; + +(63091 * 8) / 4.54 = **111,173bps**. + +The equivalent data sheet on air rate for the LoRa settings used is **203kbps**. + +In FLRC mode, at its fastest rate, the same 63019 image file took 2.48 seconds to transfer an achieved data rate of **203,601bps**, the on air rate for this fastest FLRC mode is **1,300,000bps** + +The Data Transfer examples will be found in the \examples\SX128x_examples\DataTransfer folder of the [**SX12XX-LoRa Library**](https://github.com/StuartsProjects/SX12XX-LoRa). + +## Conclusions + +Well, the data transfers work and the LoRa and FLRC are considerably faster than UHF LoRa devices. + +## Duty Cycle + +Whilst the described routines work well enough for SX127x in the UHF 433Mhz band in a lot of places in the World your limited to 10% duty cycle, so sending images continuously is not legal. However the library functions described here are for the 2.4Ghz SX128X library, and at 2.4Ghz there are few duty cycle restrictions. + +
+ + +**Stuart Robinson** + +**November 2021** \ No newline at end of file diff --git a/examples/SX128x_examples/Diagnostic_and_Test/10_LoRa_Link_Test_Transmitter/10_LoRa_Link_Test_Transmitter.ino b/examples/SX128x_examples/Diagnostic_and_Test/10_LoRa_Link_Test_Transmitter/10_LoRa_Link_Test_Transmitter.ino new file mode 100644 index 0000000..299d0d1 --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/10_LoRa_Link_Test_Transmitter/10_LoRa_Link_Test_Transmitter.ino @@ -0,0 +1,258 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program that can be used to test the effectiveness of a LoRa link or its + attached antennas. Simulations of antenna performance are no substitute for real world tests and this + simple program allows both long distance link performance to be evaluated and antenna performance to be + compared. + + The program sends short test packets that reduce in power by 1dBm at a time. The start power is defined + by start_power and the end power is defined by end_power (see Settings.h file). Once the end_power point + is reached, the program pauses a short while and starts the transmit sequence again at start_power. + The packet sent contains the power used to send the packet. By listening for the packets with the basic + LoRa receive program (4_LoRa_Receiver) you can see the reception results, which should look something + like this; + + 11s 1*T+05,CRC,80B8,RSSI,-73dBm,SNR,9dB,Length,6,Packets,9,Errors,0,IRQreg,50 + 12s 1*T+04,CRC,9099,RSSI,-74dBm,SNR,9dB,Length,6,Packets,10,Errors,0,IRQreg,50 + 14s 1*T+03,CRC,E07E,RSSI,-75dBm,SNR,9dB,Length,6,Packets,11,Errors,0,IRQreg,50 + + Above shows 3 packets received, the first at +05dBm (+05 in printout), the second at 4dBm (+04 in + printout) and the third at 3dBm (+03) in printout. + + If it is arranged so that reception of packets fails halfway through the sequence by attenuating either the + transmitter (with an SMA attenuator for instance) or the receiver (by placing it in a tin perhaps) then + if you swap transmitter antennas you can see the dBm difference in reception, which will be the dBm difference + (gain) of the antenna. + + To start the sequence a packet is sent with the number 999, when received it looks like this; + + T*1999 + + This received packet could be used for the RX program to be able to print totals etc. + + LoRa settings to use for the link test are specified in the 'Settings.h' file. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include +#include "Settings.h" + +SX128XLT LT; + +int8_t TestPower; +uint8_t TXPacketL; + + +void loop() +{ + Serial.println(F("Start Test Sequence")); + Serial.print(TXpower); + Serial.print(F("dBm ")); + Serial.print(F("Start Packet> ")); + + SendTest1ModePacket(); + + Serial.println(); + + for (TestPower = start_power; TestPower >= end_power; TestPower--) + { + Serial.print(TestPower); + Serial.print(F("dBm ")); + Serial.print(F("Test Packet> ")); + Serial.flush(); + SendTestPacket(TestPower); + Serial.println(); + delay(packet_delay); + } + + Serial.println(F("Finished Test Sequence")); + Serial.println(); +} + + +void SendTestPacket(int8_t lpower) +{ + //build and send the test packet in addressed form, 3 bytes will be added to begining of packet + int8_t temppower; + uint8_t buff[3]; //the packet is built in this buffer + TXPacketL = sizeof(buff); + + if (lpower < 0) + { + buff[0] = '-'; + } + else + { + buff[0] = '+'; + } + + if (TestPower == 0) + { + buff[0] = ' '; + } + + temppower = TestPower; + + if (temppower < 0) + { + temppower = -temppower; + } + + if (temppower > 19) + { + buff[1] = '2'; + buff[2] = ((temppower - 20) + 0x30); + } + else if (temppower > 9) + { + buff[1] = '1'; + buff[2] = ((temppower - 10) + 0x30); + } + else + { + buff[1] = '0'; + buff[2] = (temppower + 0x30); + } + + LT.printASCIIPacket(buff, sizeof(buff)); + + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitAddressed(buff, sizeof(buff), TestPacket, Broadcast, ThisNode, 5000, lpower, WAIT_TX); + digitalWrite(LED1, LOW); + + if (TXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } +} + + +void SendTest1ModePacket() +{ + //used to allow an RX to recognise the start off the sequence and possibly print totals + + uint8_t buff[3]; //the packet is built in this buffer + + buff[0] = '9'; + buff[1] = '9'; + buff[2] = '9'; + TXPacketL = sizeof(buff); + + LT.printASCIIPacket(buff, sizeof(buff)); + + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitAddressed(buff, sizeof(buff), TestMode1, Broadcast, ThisNode, 5000, start_power, WAIT_TX); + delay(mode_delaymS); //longer delay, so that the start test sequence is obvious + digitalWrite(LED1, LOW); + + if (TXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } +} + + +void packet_is_OK() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //get the IRQ status + Serial.print(F(" ")); + Serial.print(TXPacketL); + Serial.print(F(" Bytes SentOK")); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //get the IRQ status + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); + delay(packet_delay); //change LED flash so packet error visible + delay(packet_delay); + digitalWrite(LED1, HIGH); + delay(packet_delay); + delay(packet_delay); + digitalWrite(LED1, LOW); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("10_LoRa_Link_Test_Transmitter Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operting settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers + Serial.println(); + Serial.println(); + + Serial.print(F("Transmitter ready")); + Serial.println(); + +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/10_LoRa_Link_Test_Transmitter/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/10_LoRa_Link_Test_Transmitter/Settings.h new file mode 100644 index 0000000..bb1ae0f --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/10_LoRa_Link_Test_Transmitter/Settings.h @@ -0,0 +1,39 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define LED1 8 //on board LED, high for on +#define RFBUSY 7 //SX128X busy pin +#define DIO1 3 //DIO1 pin on LoRa device, used for RX and TX done + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const int8_t TXpower = 10; //Transmit power used when sending packet starting test sequence +const int8_t start_power = 10; //link test starts at this transmit power, maximum +12dBm +const int8_t end_power = -18; //and ends at this power, minimum -18dBm +const uint8_t ThisNode = 'T'; //this identifies the node in transmissions + + +#define packet_delay 250 //mS delay between packets +#define mode_delaymS 2000 //mS delay after sending start test sequence diff --git a/examples/SX128x_examples/Diagnostic_and_Test/11_LoRa_Packet_Logger_Receiver/11_LoRa_Packet_Logger_Receiver.ino b/examples/SX128x_examples/Diagnostic_and_Test/11_LoRa_Packet_Logger_Receiver/11_LoRa_Packet_Logger_Receiver.ino new file mode 100644 index 0000000..2b29f30 --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/11_LoRa_Packet_Logger_Receiver/11_LoRa_Packet_Logger_Receiver.ino @@ -0,0 +1,213 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 01/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program listens for incoming packets using the LoRa settings in the 'Settings.h' + file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + There is a printout of the valid packets received in HEX format. Thus the program can be used to receive + and record non-ASCII packets. The LED will flash for each packet received and the buzzer will sound, + if fitted. The measured frequency difference between the frequency used by the transmitter and the + frequency used by the receiver is shown. If this frequency difference gets to 25% of the set LoRa + bandwidth, packet reception will fail. The displayed error can be reduced by using the 'offset' + setting in the 'Settings.h' file. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" +#include //get the library here; https://github.com/PaulStoffregen/Time +time_t recordtime; //used to record the current time, preventing displayed rollover on printing + +SX128XLT LT; + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + + +void loop() +{ + RXPacketL = LT.receiveSXBuffer(0, 60000, WAIT_RX); //returns 0 if packet error of some sort, timeout set at 60secs\60000mS + + digitalWrite(LED1, HIGH); //something has happened + recordtime = now(); //stop the time to be displayed rolling over + printtime(); + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + if (BUZZER > 0) + { + delay(50); //lets have a slightly longer beep + digitalWrite(BUZZER, LOW); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); + + RXpacketCount++; + + if (BUZZER > 0) + { + digitalWrite(BUZZER, HIGH); + } + + Serial.print(F(" FreqErrror,")); + Serial.print(LT.getFrequencyErrorHz()); + Serial.print(F("hz ")); + + LT.printSXBufferHEX(0, (RXPacketL - 1)); + + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //get the IRQ status + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F(" RXTimeout")); + } + else + { + errors++; + Serial.print(F(" PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void printDigits(int8_t digits) +{ + //utility function for digital clock display: prints preceding colon and leading 0 + Serial.print(F(":")); + if (digits < 10) + Serial.print('0'); + Serial.print(digits); +} + + +void printtime() +{ + Serial.print(hour(recordtime)); + printDigits(minute(recordtime)); + printDigits(second(recordtime)); +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(9600); + Serial.println(F("11_LoRa_Packet_Logger_Receiver Starting")); + Serial.println(); + + if (BUZZER > 0) + { + pinMode(BUZZER, OUTPUT); + digitalWrite(BUZZER, HIGH); + delay(50); + digitalWrite(BUZZER, LOW); + } + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + if (LT.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); + Serial.println(); + LT.printOperatingSettings(); + Serial.println(); + Serial.println(); + printtime(); + Serial.print(F(" Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/11_LoRa_Packet_Logger_Receiver/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/11_LoRa_Packet_Logger_Receiver/Settings.h new file mode 100644 index 0000000..cf4396d --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/11_LoRa_Packet_Logger_Receiver/Settings.h @@ -0,0 +1,33 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 01/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 +#define BUZZER -1 //pin for buzzer, set to -1 if not used + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm + +#define packet_delay 1000 //mS delay between packets diff --git a/examples/SX128x_examples/Diagnostic_and_Test/16_LoRa_RX_Frequency_Error_Check/16_LoRa_RX_Frequency_Error_Check.ino b/examples/SX128x_examples/Diagnostic_and_Test/16_LoRa_RX_Frequency_Error_Check/16_LoRa_RX_Frequency_Error_Check.ino new file mode 100644 index 0000000..f272587 --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/16_LoRa_RX_Frequency_Error_Check/16_LoRa_RX_Frequency_Error_Check.ino @@ -0,0 +1,171 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program can be used to check the frequency error between a pair of LoRa + devices, a transmitter and receiver. This receiver measures the frequecy error between the receivers + centre frequency and the centre frequency of the transmitted packet. The frequency difference is shown + for each packet and an average over 10 received packets reported. Any transmitter program can be used + to give this program something to listen to, including example program '3_LoRa_Transmit'. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //a buffer is needed to receive packets + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet +int32_t totalHzError = 0; //used to keep a running total of hZ error for averaging + + +void loop() +{ + + RXPacketL = LT.receive(RXBUFFER, RXBUFFER_SIZE, 0, WAIT_RX); //wait for a packet to arrive + + digitalWrite(LED1, HIGH); //something has happened + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); + + RXpacketCount++; + Serial.print(F("PacketOK > ")); + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + Serial.println(); + printFrequencyError(); +} + + +void printFrequencyError() +{ + int32_t hertzerror, regdata; + regdata = LT.getFrequencyErrorRegValue(); + hertzerror = LT.getFrequencyErrorHz(); + Serial.print(F("ErrorRegValue,")); + Serial.print(regdata, HEX); + Serial.print(F(" PacketHertzError,")); + Serial.print(hertzerror); + Serial.println(F("hz")); + + totalHzError = totalHzError + hertzerror; + + if (RXpacketCount == 10) + { + Serial.print(F("******** AverageHertzerror ")); + Serial.print((totalHzError / 10)); + Serial.println(F("hz")); + RXpacketCount = 0; + totalHzError = 0; + delay(5000); + } +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //get the IRQ status + errors++; + Serial.print(F("PacketError,RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); + digitalWrite(LED1, LOW); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(F("16_LoRa_RX_Frequency_Error_Check Starting")); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/16_LoRa_RX_Frequency_Error_Check/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/16_LoRa_RX_Frequency_Error_Check/Settings.h new file mode 100644 index 0000000..3ce584d --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/16_LoRa_RX_Frequency_Error_Check/Settings.h @@ -0,0 +1,35 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm + +#define packet_delay 1000 //mS delay between packets + +#define RXBUFFER_SIZE 32 //RX buffer size diff --git a/examples/SX128x_examples/Diagnostic_and_Test/20_LoRa_Link_Test_Receiver/20_LoRa_Link_Test_Receiver.ino b/examples/SX128x_examples/Diagnostic_and_Test/20_LoRa_Link_Test_Receiver/20_LoRa_Link_Test_Receiver.ino new file mode 100644 index 0000000..61b22dc --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/20_LoRa_Link_Test_Receiver/20_LoRa_Link_Test_Receiver.ino @@ -0,0 +1,308 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program listens for incoming packets using the LoRa settings in the 'Settings.h' + file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + The program is a matching receiver program for the '10_LoRa_Link_Test_Transmitter'. The packets received + are displayed on the serial monitor and analysed to extract the packet data which indicates the power + used to send the packet. A count is kept of the numbers of each power setting received. When the transmitter + sends the test mode packet at the beginning of the sequence (displayed as 999) the running totals of the + powers received are printed. Thus you can quickly see at what transmit power levels the reception fails. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc +#include + +SX128XLT LT; //create a library class instance called LT + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + +uint32_t Test1Count[32]; //buffer where counts of received packets are stored, -18dbm to +12dBm +uint32_t Mode1_Cycles = 0; //count the number of cyles received +bool updateCounts = false; //update counts set to tru when first TestMode1 received, at sequence start + + +void loop() +{ + RXPacketL = LT.receiveAddressed(RXBUFFER, RXBUFFER_SIZE, 15000, WAIT_RX); //wait for a packet to arrive with 15seconds (15000mS) timeout + + digitalWrite(LED1, HIGH); //something has happened + + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + PacketSNR = LT.readPacketSNR(); //read the received SNR value + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL == 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + if (BUZZER > 0) + { + delay(25); //gives a slightly longer beep + digitalWrite(BUZZER, LOW); //buzzer off + } + + digitalWrite(LED1, LOW); //LED off + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t IRQStatus; + + if (BUZZER > 0) //turn buzzer on for a valid packet + { + digitalWrite(BUZZER, HIGH); + } + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + RXpacketCount++; + + printElapsedTime(); //print elapsed time to Serial Monitor + Serial.print(F(" ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL - 3); //print the packet as ASCII characters + + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + + processPacket(); +} + + +void processPacket() +{ + int8_t lTXpower; + uint8_t packettype; + uint32_t temp; + + packettype = LT.readRXPacketType(); //need to know the packet type so we can decide what to do + + if (packettype == TestPacket) + { + if (RXBUFFER[0] == ' ') + { + lTXpower = 0; + } + + if (RXBUFFER[0] == '+') + { + lTXpower = ((RXBUFFER[1] - 48) * 10) + (RXBUFFER[2] - 48); //convert packet text to power + } + + if (RXBUFFER[0] == '-') + { + lTXpower = (((RXBUFFER[1] - 48) * 10) + (RXBUFFER[2] - 48)) * -1; //convert packet text to power + } + + Serial.print(F(" (")); + + if (RXBUFFER[0] != '-') + { + Serial.write(RXBUFFER[0]); + } + + Serial.print(lTXpower); + Serial.print(F("dBm)")); + + if (updateCounts) + { + temp = (Test1Count[lTXpower + 18]); + Test1Count[lTXpower + 18] = temp + 1; + } + } + + if (packettype == TestMode1) + { + //this is a command to switch to TestMode1 also updates totals and logs + updateCounts = true; + Serial.println(); + Serial.println(F("End test sequence")); + + if (Mode1_Cycles > 0) + { + print_Test1Count(); + } + + Serial.println(); + Mode1_Cycles++; + } + +} + + +void print_Test1Count() +{ + //prints running totals of the powers of received packets + int8_t index; + uint32_t j; + + Serial.print(F("Test Packets ")); + Serial.println(RXpacketCount); + Serial.print(F("Test Cycles ")); + Serial.println(Mode1_Cycles); + + Serial.println(); + for (index = 30; index >= 0; index--) + { + Serial.print(index - 18); + Serial.print(F("dBm,")); + j = Test1Count[index]; + Serial.print(j); + Serial.print(F(" ")); + } + Serial.println(); + + Serial.print(F("CSV")); + for (index = 30; index >= 0; index--) + { + Serial.print(F(",")); + j = Test1Count[index]; + Serial.print(j); + } + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + printElapsedTime(); //print elapsed time to Serial Monitor + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout")); + } + else + { + errors++; + Serial.print(F(" PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); //print the names of the IRQ registers set + } +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(F("20_LoRa_Link_Test_Receiver Starting")); + Serial.println(); + + if (BUZZER > 0) + { + pinMode(BUZZER, OUTPUT); + digitalWrite(BUZZER, HIGH); + delay(50); + digitalWrite(BUZZER, LOW); + } + + //setup SPI, its external to library on purpose, so settings can be mixed and matched with other SPI devices + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //this function call sets up the device for LoRa using the settings from settings.h + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operting settings, useful check + Serial.println(); + Serial.println(); + + Serial.print(F("Receiver ready - RXBUFFER_SIZE ")); + Serial.println(RXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/20_LoRa_Link_Test_Receiver/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/20_LoRa_Link_Test_Receiver/Settings.h new file mode 100644 index 0000000..a0c559a --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/20_LoRa_Link_Test_Receiver/Settings.h @@ -0,0 +1,36 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define LED1 8 //on board LED, high for on +#define RFBUSY 7 //SX128X busy pin +#define DIO1 3 //DIO1 pin on LoRa device, used for RX and TX done +#define BUZZER -1 //pin for buzzer, set to -1 if not used + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const int8_t TXpower = 10; //LoRa transmit power in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets + +#define RXBUFFER_SIZE 32 //RX buffer size diff --git a/examples/SX128x_examples/Diagnostic_and_Test/33_LoRa_RSSI_Checker_With_Display/33_LoRa_RSSI_Checker_With_Display.ino b/examples/SX128x_examples/Diagnostic_and_Test/33_LoRa_RSSI_Checker_With_Display/33_LoRa_RSSI_Checker_With_Display.ino new file mode 100644 index 0000000..145c11f --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/33_LoRa_RSSI_Checker_With_Display/33_LoRa_RSSI_Checker_With_Display.ino @@ -0,0 +1,274 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 01/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program listens for incoming packets using the LoRa settings in the 'Settings.h' + file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + There is a printout of the valid packets received, the packet is assumed to be in ASCII printable text, + if its not ASCII text characters from 0x20 to 0x7F, expect weird things to happen on the Serial Monitor. + The LED will flash for each packet received and the buzzer will sound, if fitted. + + Sample serial monitor output; + + 1109s {packet contents} CRC,3882,RSSI,-69dBm,SNR,10dB,Length,19,Packets,1026,Errors,0,IRQreg,50 + + If there is a packet error it might look like this, which is showing a CRC error, + + 1189s PacketError,RSSI,-111dBm,SNR,-12dB,Length,0,Packets,1126,Errors,1,IRQreg,70,IRQ_HEADER_VALID,IRQ_CRC_ERROR,IRQ_RX_DONE + + A summary of the packet reception is sent to the OLED display as well, useful for portable applications. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +#include //get library here > https://github.com/olikraus/u8g2 +U8X8_SSD1306_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //use this line for standard 0.96" SSD1306 +//U8X8_SH1106_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //use this line for 1.3" OLED often sold as 1.3" SSD1306 + + +uint32_t RXpacketCount; +uint32_t RXpacketErrors; +uint16_t IRQStatus; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + + +void loop() +{ + RXPacketL = LT.receive(RXBUFFER, RXBUFFER_SIZE, 0, WAIT_RX); //wait for a packet to arrive with no timeout + + digitalWrite(LED1, HIGH); //something has happened + + if (BUZZER > 0) + { + digitalWrite(BUZZER, HIGH); //buzzer on + } + + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + PacketSNR = LT.readPacketSNR(); //read the received SNR value + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL == 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + if (BUZZER > 0) + { + digitalWrite(BUZZER, LOW); //buzzer off + } + + digitalWrite(LED1, LOW); //LED off + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t localCRC; + + RXpacketCount++; + + printElapsedTime(); //print elapsed time to Serial Monitor + Serial.print(F(" ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL); //print the packet as ASCII characters + + localCRC = LT.CRCCCITT(RXBUFFER, RXPacketL, 0xFFFF); //calculate the CRC, this is the external CRC calculation of the RXBUFFER + Serial.print(F(",CRC,")); //contents, not the LoRa device internal CRC + Serial.print(localCRC, HEX); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(RXpacketErrors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + + disp.clearLine(0); + disp.setCursor(0, 0); + disp.print(F("OK")); + dispscreen1(); +} + + +void packet_is_Error() +{ + printElapsedTime(); //print elapsed time to Serial Monitor + + RXpacketErrors++; + Serial.print(F(" PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(RXpacketErrors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); //print the names of the IRQ registers set + disp.clearLine(0); + disp.setCursor(0, 0); + disp.print(F("Packet Error")); + dispscreen1(); + + delay(500); //gives longer buzzer and LED falsh for error + +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void dispscreen1() +{ + disp.clearLine(1); + disp.setCursor(0, 1); + disp.print(F("RSSI ")); + disp.print(PacketRSSI); + disp.print(F("dBm")); + disp.clearLine(2); + disp.setCursor(0, 2); + disp.print(F("SNR ")); + + if (PacketSNR > 0) + { + disp.print(F("+")); + } + + disp.print(PacketSNR); + disp.print(F("dB")); + disp.clearLine(3); + disp.setCursor(0, 3); + disp.print(F("Length ")); + disp.print(LT.readRXPacketL()); + disp.clearLine(4); + disp.setCursor(0, 4); + disp.print(F("Packets ")); + disp.print(RXpacketCount); + disp.clearLine(5); + disp.setCursor(0, 5); + disp.print(F("Errors ")); + disp.print(RXpacketErrors); + disp.clearLine(6); + disp.setCursor(0, 6); + disp.print(F("IRQreg ")); + disp.print(IRQStatus, HEX); +} + + + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(F("33_LoRa_RSSI_Checker_With_Display Starting")); + Serial.println(); + + if (BUZZER > 0) + { + pinMode(BUZZER, OUTPUT); + digitalWrite(BUZZER, HIGH); + delay(50); + digitalWrite(BUZZER, LOW); + } + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + disp.begin(); + disp.setFont(u8x8_font_chroma48medium8_r); + + disp.clear(); + disp.setCursor(0, 0); + disp.print(F("Check LoRa")); + disp.setCursor(0, 1); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + disp.print(F("LoRa OK")); + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + disp.print(F("Device error")); + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //this function call sets up the device for LoRa using the settings from settings.h + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operting settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers + Serial.println(); + Serial.println(); + + Serial.print(F("Receiver ready - RXBUFFER_SIZE ")); + Serial.println(RXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/33_LoRa_RSSI_Checker_With_Display/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/33_LoRa_RSSI_Checker_With_Display/Settings.h new file mode 100644 index 0000000..1167a4a --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/33_LoRa_RSSI_Checker_With_Display/Settings.h @@ -0,0 +1,33 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 01/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 +#define BUZZER -1 //pin for BUZZER, set to -1 if not used + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets + +#define RXBUFFER_SIZE 32 //RX buffer size diff --git a/examples/SX128x_examples/Diagnostic_and_Test/42_FLRC_Data_Throughput_Test_Transmitter/42_FLRC_Data_Throughput_Test_Transmitter.ino b/examples/SX128x_examples/Diagnostic_and_Test/42_FLRC_Data_Throughput_Test_Transmitter/42_FLRC_Data_Throughput_Test_Transmitter.ino new file mode 100644 index 0000000..c78e608 --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/42_FLRC_Data_Throughput_Test_Transmitter/42_FLRC_Data_Throughput_Test_Transmitter.ino @@ -0,0 +1,278 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 15/05/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a program that can be used to test the throughput of a LoRa transmitter in + FLRC mode. + + Whilst the various LoRa calculators tell you the on air data rate, in practice the achievable data + rate will be less than that due to the overhead of the software routines to load and send a packet + and internal delays in the LoRa device itself. + + A buffer is filled with characters and that buffer is then transmitted. The total time for a number of + transmissions is recorded and the bit rate calculated. The packet size (1 - 127 bytes) and the number of + packets to send in the test are specified in the 'Settings.h' file, see the 'Setup packet parameters Here !' + section. The setting file also has the lora settings to use. A lower spreading factors and higher + bandwidths will result in higher bitrates. + + There is the option of turning on an a requirement for an acknowledgement from a remote receiver, before + the transmitter sends the next packet, set this; 'const bool waitforACK = true;' definition in the + settings file. The matching receiver program '43_FLRC_Data_Throughput_Acknowledge_Receiver' does then need + to be configured with same FLRC settings as this transmitter. When this option is set, the program will + keep running until the number of transmissions and acknowledgements has completed without any timeouts + in order to produce a valid average. + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint32_t startmS, endmS, sendtimemS, bitspersecond, bitsPerpacket; +uint32_t TXPacketCount; +float averagePacketTime; +uint8_t packetNumber; +uint8_t RXPacketL; //length of received packet +uint8_t PacketType; //for packet addressing, identifies packet type received +uint32_t packetCheck; +bool loopFail = false; + + +void loop() +{ + uint16_t index, index2; + uint8_t TXBUFFER[TXPacketL + 1]; //create buffer for transmitted packet + loopFail = false; + + Serial.println(F("Start transmit test")); + + startmS = millis(); //start transmit timer + + for (index = 0; index < numberPackets; index++) + { + //fill the buffer + for (index2 = 0; index2 < TXPacketL; index2++) + { + TXBUFFER[index2] = index2; + } + + TXBUFFER[0] = TestPacket; //set first byte to identify this test packet + TXBUFFER[1] = index; //put the index as packet number in second byte of packet + Serial.print(index); //print number of packet sent + + if (waitforACK) + { + packetCheck = ( (uint32_t) TXBUFFER[4] << 24) + ( (uint32_t) TXBUFFER[3] << 16) + ( (uint32_t) TXBUFFER[2] << 8) + (uint32_t) TXBUFFER[1]; + Serial.print(F(",Checkvalue,")); + Serial.print(packetCheck, HEX); + Serial.print(F(",")); + } + + digitalWrite(LED1, HIGH); + + if (LT.transmit(TXBUFFER, TXPacketL, 10000, TXpower, WAIT_TX)) //will return 0 if transmit error + { + digitalWrite(LED1, LOW); + + if (waitforACK) + { + if (!waitAck(packetCheck)) + { + Serial.print(F("NoACKreceived,")); + loopFail = true; + break; + } + } + + } + else + { + packet_is_Error(); //transmit packet returned 0, there was an error + loopFail = true; + } + + Serial.println(); + + } + + if (!loopFail) + { + endmS = millis(); //all packets sent, note end time + digitalWrite(LED1, LOW); + Serial.println(); + LT.printModemSettings(); + sendtimemS = endmS - startmS; + Serial.println(); + Serial.print(F("Total transmit time ")); + Serial.print(numberPackets); + Serial.print(F(" packets = ")); + Serial.print(sendtimemS); + Serial.println(F("mS")); + + averagePacketTime = (float) ((endmS - startmS) / numberPackets); + + if (waitforACK) + { + Serial.print(F("Average ")); + Serial.print(TXPacketL); + Serial.print(F(" byte packet transmit and acknowledge time = ")); + } + else + { + Serial.print(F("Average ")); + Serial.print(TXPacketL); + Serial.print(F(" byte packet transmit time = ")); + } + + Serial.print(averagePacketTime, 2); + Serial.println(F("mS")); + Serial.print(F("Packets per second ")); + Serial.println((float) (1000 / averagePacketTime)); + bitsPerpacket = (uint32_t) (TXPacketL * 8); + Serial.print(F("Bits per packet sent = ")); + Serial.println(bitsPerpacket); + + Serial.print(F("Data rate = ")); + Serial.print((bitsPerpacket / (averagePacketTime / 1000)), 0); + Serial.print(F("bps")); + Serial.println(); + Serial.println(); + delay(10000); //have a delay between loops so we can see result + } + else + { + Serial.println(F("Transmit test failed, trying again")); + Serial.println(); + delay(1000); + } + +} + + +bool waitAck(uint32_t TXnum) +{ + uint32_t RXnum; + uint16_t IRQStatus; + + RXPacketL = LT.receiveSXBuffer(0, 1000, WAIT_RX); //returns 0 if packet error of some sort + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F("RXTimeout,")); + return false; + } + else + { + Serial.print(F("ACKRX,")); + } + + LT.startReadSXBuffer(0); + PacketType = LT.readUint8(); + RXnum = LT.readUint32(); + RXPacketL = LT.endReadSXBuffer(); + + if ( (PacketType != ACK) || (RXnum != TXnum)) + { + Serial.print(F("NotValidACK,")); + return false; + } + else + { + Serial.print(RXnum, HEX); + Serial.print(F(",OK")); + return true; + } + +} + + + +void packet_is_Error() +{ + //if here there was an error transmitting packet + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the the interrupt register + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); //print IRQ status + LT.printIrqStatus(); //prints the text of which IRQs set +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("42_FLRC_Data_Throughput_Test_Transmitter Starting")); + + SPI.begin(); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //*************************************************************************************************** + //Setup FLRC + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setRegulatorMode(USE_LDO); + LT.setPacketType(PACKET_TYPE_FLRC); + LT.setRfFrequency(Frequency, Offset); + LT.setBufferBaseAddress(0, 0); + LT.setModulationParams(BandwidthBitRate, CodingRate, BT); + LT.setPacketParams(PREAMBLE_LENGTH_32_BITS, FLRC_SYNC_WORD_LEN_P32S, RADIO_RX_MATCH_SYNCWORD_1, RADIO_PACKET_VARIABLE_LENGTH, 127, RADIO_CRC_3_BYTES, RADIO_WHITENING_OFF); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + LT.setSyncWord1(Sample_Syncword); + //*************************************************************************************************** + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + + Serial.print(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/42_FLRC_Data_Throughput_Test_Transmitter/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/42_FLRC_Data_Throughput_Test_Transmitter/Settings.h new file mode 100644 index 0000000..468b826 --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/42_FLRC_Data_Throughput_Test_Transmitter/Settings.h @@ -0,0 +1,38 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 26/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define LED1 8 //on board LED, high for on +#define DIO1 3 //DIO1 pin on LoRa device, used for RX and TX done + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem FLRC mode Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes + +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +const uint8_t CodingRate = FLRC_CR_1_2; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Sample_Syncword = 0x01234567; //FLRC uses syncword +const int8_t TXpower = 10; //LoRa transmit power in dBm + + +//******* Setup packet parameters Here ! *************** +const uint8_t numberPackets = 50; //number of packets to send in transmit loop +const uint8_t TXPacketL = 16; //length of packet to send, 1 to 127 bytes +const bool waitforACK = false; //set to true to have transmit wait for ack before continuing diff --git a/examples/SX128x_examples/Diagnostic_and_Test/42_LoRa_Data_Throughput_Test_Transmitter/42_LoRa_Data_Throughput_Test_Transmitter.ino b/examples/SX128x_examples/Diagnostic_and_Test/42_LoRa_Data_Throughput_Test_Transmitter/42_LoRa_Data_Throughput_Test_Transmitter.ino new file mode 100644 index 0000000..71db9dc --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/42_LoRa_Data_Throughput_Test_Transmitter/42_LoRa_Data_Throughput_Test_Transmitter.ino @@ -0,0 +1,274 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 04/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a program that can be used to test the throughput of a LoRa transmitter. + Whilst the various LoRa calculators tell you the on air data rate, in practice the achievable data + rate will be less than that due to the overhead of the software routines to load and send a packet + and internal delays in the LoRa device itself. + + A buffer is filled with characters and that buffer is then transmitted. The total time for a number of + transmissions is recorded and the bit rate calculated. The packet size (1 - 255 bytes) and the number of + packets to send in the test are specified in the 'Settings.h' file, see the 'Setup packet parameters Here !' + section. The setting file also has the lora settings to use. A lower spreading factors and higher + bandwidths will result in higher bitrates. + + There is the option of turning on an a requirement for an acknowledgement from a remote receiver, before + the transmitter sends the next packet, set this; 'const bool waitforACK = true;' definition in the + settings file. The matching receiver program '43_LoRa_Data_Throughput_Acknowledge_Receiver' does then need + to be configured with same lora settings as this transmitter. When this option is set, the program will + keep running until the number of transmissions and acknowledgements has completed without any timeouts + in order to produce a valid average. + + The results of the test are printed out thus; + + SX1280,PacketMode_LORA,2444999936hz,SF5,BW1625000,CR4:5 + Total transmit time 50 packets = 199mS + Average 16 byte packet transmit time = 3.00mS + Packets per second 333.33 + Bits per packet sent = 128 + Data rate = 42667bps + + + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint32_t startmS, endmS, sendtimemS, bitspersecond, bitsPerpacket; +uint32_t TXPacketCount; +float averagePacketTime; +uint8_t packetNumber; +uint8_t RXPacketL; //length of received packet +uint8_t PacketType; //for packet addressing, identifies packet type received +uint32_t packetCheck; +bool loopFail = false; + + +void loop() +{ + uint16_t index, index2; + uint8_t TXBUFFER[TXPacketL + 1]; //create buffer for transmitted packet + loopFail = false; + + Serial.println(F("Start transmit test")); + + startmS = millis(); //start transmit timer + + for (index = 0; index < numberPackets; index++) + { + //fill the buffer + for (index2 = 0; index2 < TXPacketL; index2++) + { + TXBUFFER[index2] = index2; + } + + TXBUFFER[0] = TestPacket; //set first byte to identify this test packet + TXBUFFER[1] = index; //put the index as packet number in second byte of packet + Serial.print(index); //print number of packet sent + + if (waitforACK) + { + packetCheck = ( (uint32_t) TXBUFFER[4] << 24) + ( (uint32_t) TXBUFFER[3] << 16) + ( (uint32_t) TXBUFFER[2] << 8) + (uint32_t) TXBUFFER[1]; + Serial.print(F(",Checkvalue,")); + Serial.print(packetCheck, HEX); + Serial.print(F(",")); + } + + digitalWrite(LED1, HIGH); + + if (LT.transmit(TXBUFFER, TXPacketL, 10000, TXpower, WAIT_TX)) //will return 0 if transmit error + { + digitalWrite(LED1, LOW); + + if (waitforACK) + { + if (!waitAck(packetCheck)) + { + Serial.print(F("NoACKreceived,")); + loopFail = true; + break; + } + } + + } + else + { + packet_is_Error(); //transmit packet returned 0, there was an error + loopFail = true; + } + + Serial.println(); + + } + + if (!loopFail) + { + endmS = millis(); //all packets sent, note end time + digitalWrite(LED1, LOW); + Serial.println(); + LT.printModemSettings(); + sendtimemS = endmS - startmS; + Serial.println(); + Serial.print(F("Total transmit time ")); + Serial.print(numberPackets); + Serial.print(F(" packets = ")); + Serial.print(sendtimemS); + Serial.println(F("mS")); + + averagePacketTime = (float) ((endmS - startmS) / numberPackets); + + if (waitforACK) + { + Serial.print(F("Average ")); + Serial.print(TXPacketL); + Serial.print(F(" byte packet transmit and acknowledge time = ")); + } + else + { + Serial.print(F("Average ")); + Serial.print(TXPacketL); + Serial.print(F(" byte packet transmit time = ")); + } + + Serial.print(averagePacketTime, 2); + Serial.println(F("mS")); + Serial.print(F("Packets per second ")); + Serial.println((float) (1000 / averagePacketTime)); + bitsPerpacket = (uint32_t) (TXPacketL * 8); + Serial.print(F("Bits per packet sent = ")); + Serial.println(bitsPerpacket); + + Serial.print(F("Data rate = ")); + Serial.print((bitsPerpacket / (averagePacketTime / 1000)), 0); + Serial.print(F("bps")); + Serial.println(); + Serial.println(); + delay(10000); //have a delay between loops so we can see result + } + else + { + Serial.println(F("Transmit test failed, trying again")); + Serial.println(); + delay(1000); + } + +} + + +bool waitAck(uint32_t TXnum) +{ + uint32_t RXnum; + uint16_t IRQStatus; + + RXPacketL = LT.receiveSXBuffer(0, 1000, WAIT_RX); //returns 0 if packet error of some sort + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F("RXTimeout,")); + return false; + } + else + { + Serial.print(F("ACKRX,")); + } + + LT.startReadSXBuffer(0); + PacketType = LT.readUint8(); + RXnum = LT.readUint32(); + RXPacketL = LT.endReadSXBuffer(); + + if ( (PacketType != ACK) || (RXnum != TXnum)) + { + Serial.print(F("NotValidACK,")); + return false; + } + else + { + Serial.print(RXnum, HEX); + Serial.print(F(",OK")); + return true; + } + +} + + + +void packet_is_Error() +{ + //if here there was an error transmitting packet + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the the interrupt register + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); //print IRQ status + LT.printIrqStatus(); //prints the text of which IRQs set +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(F("42_LoRa_Data_Throughput_Test_Transmitter Starting")); + + SPI.begin(); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + + Serial.print(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/42_LoRa_Data_Throughput_Test_Transmitter/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/42_LoRa_Data_Throughput_Test_Transmitter/Settings.h new file mode 100644 index 0000000..dbff514 --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/42_LoRa_Data_Throughput_Test_Transmitter/Settings.h @@ -0,0 +1,38 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 26/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define LED1 8 //on board LED, high for on +#define DIO1 3 //DIO1 pin on LoRa device, used for RX and TX done + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes + +#define Bandwidth LORA_BW_1600 //LoRa bandwidth +#define SpreadingFactor LORA_SF5 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const int8_t TXpower = 10; //LoRa transmit power in dBm + + +//******* Setup packet parameters Here ! *************** +const uint8_t numberPackets = 50; //number of packets to send in transmit loop +const uint8_t TXPacketL = 16; //length of packet to send +const bool waitforACK = true; //set to true to have transmit wait for ack before continuing diff --git a/examples/SX128x_examples/Diagnostic_and_Test/43_FLRC_Data_Throughput_Acknowledge_Receiver/43_FLRC_Data_Throughput_Acknowledge_Receiver.ino b/examples/SX128x_examples/Diagnostic_and_Test/43_FLRC_Data_Throughput_Acknowledge_Receiver/43_FLRC_Data_Throughput_Acknowledge_Receiver.ino new file mode 100644 index 0000000..e062aac --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/43_FLRC_Data_Throughput_Acknowledge_Receiver/43_FLRC_Data_Throughput_Acknowledge_Receiver.ino @@ -0,0 +1,177 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 04/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a receiver program that can be used to test the throughput of a LoRa + transmitter. The matching program '42_FLRC_Data_Throughput_Test_Transmitter' is setup to send packets + that require an acknowledgement before sending the next packet. This will slow down the effective + throughput. Make sure the lora settings in the 'Settings.h' file match those used in the transmitter. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[255]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +uint8_t TXPacketL; //stores length of packet sent +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio (SNR) of received packet + +uint8_t PacketType; //for packet addressing, identifies packet type +uint32_t packetCheck; + +void loop() +{ + RXPacketL = LT.receive(RXBUFFER, 255, 60000, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + digitalWrite(LED1, HIGH); //something has happened + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL is 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); //LED off + + Serial.println(); +} + + +void packet_is_OK() +{ + RXpacketCount++; + Serial.print(RXBUFFER[1]); + Serial.print(F(" RX")); + packetCheck = ( (uint32_t) RXBUFFER[4] << 24) + ( (uint32_t) RXBUFFER[3] << 16) + ( (uint32_t) RXBUFFER[2] << 8 ) + (uint32_t) RXBUFFER[1]; + Serial.print(F(",SendACK")); + sendAck(packetCheck); +} + + +void sendAck(uint32_t num) +{ + //acknowledge the packet received + uint8_t len; + + LT.startWriteSXBuffer(0); //initialise buffer write at address 0 + LT.writeUint8(ACK); //identify type of packet + LT.writeUint32(num); //send the packet check, bytes 1 to 5 of packet + len = LT.endWriteSXBuffer(); //close buffer write + + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXBuffer(0, len, 10000, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(",RXTimeout")); + } + else + { + errors++; + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + Serial.print(F("Error")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,Len,")); + Serial.print(LT.readRXPacketL()); //get the device packet length + } +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(F("43_LoRa_Data_Throughput_Acknowledge_Receiver Starting")); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //*************************************************************************************************** + //Setup FLRC + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setRegulatorMode(USE_LDO); + LT.setPacketType(PACKET_TYPE_FLRC); + LT.setRfFrequency(Frequency, Offset); + LT.setBufferBaseAddress(0, 0); + LT.setModulationParams(BandwidthBitRate, CodingRate, BT); + LT.setPacketParams(PREAMBLE_LENGTH_32_BITS, FLRC_SYNC_WORD_LEN_P32S, RADIO_RX_MATCH_SYNCWORD_1, RADIO_PACKET_VARIABLE_LENGTH, 127, RADIO_CRC_3_BYTES, RADIO_WHITENING_OFF); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + LT.setSyncWord1(Sample_Syncword); + //*************************************************************************************************** + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + Serial.println(); + + Serial.print(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/43_FLRC_Data_Throughput_Acknowledge_Receiver/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/43_FLRC_Data_Throughput_Acknowledge_Receiver/Settings.h new file mode 100644 index 0000000..59dac84 --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/43_FLRC_Data_Throughput_Acknowledge_Receiver/Settings.h @@ -0,0 +1,33 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 25/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for RX and TX done + +#define LED1 8 //on board LED, high for on + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem FLRC mode Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes + +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +const uint8_t CodingRate = FLRC_CR_1_2; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Sample_Syncword = 0x01234567; //FLRC uses syncword +const int8_t TXpower = 10; //LoRa transmit power in dBm diff --git a/examples/SX128x_examples/Diagnostic_and_Test/43_LoRa_Data_Throughput_Acknowledge_Receiver/43_LoRa_Data_Throughput_Acknowledge_Receiver.ino b/examples/SX128x_examples/Diagnostic_and_Test/43_LoRa_Data_Throughput_Acknowledge_Receiver/43_LoRa_Data_Throughput_Acknowledge_Receiver.ino new file mode 100644 index 0000000..6eb22ab --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/43_LoRa_Data_Throughput_Acknowledge_Receiver/43_LoRa_Data_Throughput_Acknowledge_Receiver.ino @@ -0,0 +1,165 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 04/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a receiver program that can be used to test the throughput of a LoRa + transmitter. The matching program '42_LoRa_Data_Throughput_Test_Transmitter' is setup to send packets + that require an acknowledgement before sending the next packet. This will slow down the effective + throughput. Make sure the lora settings in the 'Settings.h' file match those used in the transmitter. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[255]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +uint8_t TXPacketL; //stores length of packet sent +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio (SNR) of received packet + +uint8_t PacketType; //for packet addressing, identifies packet type +uint32_t packetCheck; + +void loop() +{ + RXPacketL = LT.receive(RXBUFFER, 255, 60000, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + digitalWrite(LED1, HIGH); //something has happened + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL is 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); //LED off + + Serial.println(); +} + + +void packet_is_OK() +{ + RXpacketCount++; + Serial.print(RXBUFFER[1]); + Serial.print(F(" RX")); + packetCheck = ( (uint32_t) RXBUFFER[4] << 24) + ( (uint32_t) RXBUFFER[3] << 16) + ( (uint32_t) RXBUFFER[2] << 8 ) + (uint32_t) RXBUFFER[1]; + Serial.print(F(",SendACK")); + sendAck(packetCheck); +} + + +void sendAck(uint32_t num) +{ + //acknowledge the packet received + uint8_t len; + + LT.startWriteSXBuffer(0); //initialise buffer write at address 0 + LT.writeUint8(ACK); //identify type of packet + LT.writeUint32(num); //send the packet check, bytes 1 to 5 of packet + len = LT.endWriteSXBuffer(); //close buffer write + + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXBuffer(0, len, 10000, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(",RXTimeout")); + } + else + { + errors++; + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + Serial.print(F("Error")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,Len,")); + Serial.print(LT.readRXPacketL()); //get the device packet length + } +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(F("43_LoRa_Data_Throughput_Acknowledge_Receiver Starting")); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + Serial.println(); + + Serial.print(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Diagnostic_and_Test/43_LoRa_Data_Throughput_Acknowledge_Receiver/Settings.h b/examples/SX128x_examples/Diagnostic_and_Test/43_LoRa_Data_Throughput_Acknowledge_Receiver/Settings.h new file mode 100644 index 0000000..4ef6ec4 --- /dev/null +++ b/examples/SX128x_examples/Diagnostic_and_Test/43_LoRa_Data_Throughput_Acknowledge_Receiver/Settings.h @@ -0,0 +1,33 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 25/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for RX and TX done + +#define LED1 8 //on board LED, high for on + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes + +#define Bandwidth LORA_BW_1600 //LoRa bandwidth +#define SpreadingFactor LORA_SF5 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const int8_t TXpower = 10; //LoRa transmit power in dBm diff --git a/examples/SX128x_examples/ESP32/Basics/103_LoRa_Transmitter_Detailed_Setup_ESP32/103_LoRa_Transmitter_Detailed_Setup_ESP32.ino b/examples/SX128x_examples/ESP32/Basics/103_LoRa_Transmitter_Detailed_Setup_ESP32/103_LoRa_Transmitter_Detailed_Setup_ESP32.ino new file mode 100644 index 0000000..2c13e38 --- /dev/null +++ b/examples/SX128x_examples/ESP32/Basics/103_LoRa_Transmitter_Detailed_Setup_ESP32/103_LoRa_Transmitter_Detailed_Setup_ESP32.ino @@ -0,0 +1,183 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 04/04/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a program that demonstrates the detailed setup of a LoRa test transmitter. + A packet containing ASCII text is sent according to the frequency and LoRa settings specified in the + 'Settings.h' file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + The details of the packet sent and any errors are shown on the Arduino IDE Serial Monitor, together with + the transmit power used, the packet length and the CRC of the packet. The matching receive program, + '104_LoRa_Receiver' can be used to check the packets are being sent correctly, the frequency and LoRa + settings (in Settings.h) must be the same for the transmitter and receiver programs. Sample Serial + Monitor output; + + 10dBm Packet> Hello World 1234567890* BytesSent,23 CRC,DAAB TransmitTime,64mS PacketsSent,2 + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#define Program_Version "V1.1" + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint8_t TXPacketL; +uint32_t TXPacketCount, startmS, endmS; + +uint8_t buff[] = "Hello World 1234567890"; + +void loop() +{ + Serial.print(TXpower); //print the transmit power defined + Serial.print(F("dBm ")); + Serial.print(F("Packet> ")); + Serial.flush(); + + TXPacketL = sizeof(buff); //set TXPacketL to length of array + buff[TXPacketL - 1] = '*'; //replace null character at buffer end so its visible on reciver + + LT.printASCIIPacket(buff, TXPacketL); //print the buffer (the sent packet) as ASCII + + digitalWrite(LED1, HIGH); + startmS = millis(); //start transmit timer + if (LT.transmit(buff, TXPacketL, 10000, TXpower, WAIT_TX)) //will return packet length sent if OK, otherwise 0 if transmit error + { + endmS = millis(); //packet sent, note end time + TXPacketCount++; + packet_is_OK(); + } + else + { + packet_is_Error(); //transmit packet returned 0, there was an error + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(packet_delay); //have a delay between packets +} + + +void packet_is_OK() +{ + //if here packet has been sent OK + uint16_t localCRC; + + Serial.print(F(" BytesSent,")); + Serial.print(TXPacketL); //print transmitted packet length + localCRC = LT.CRCCCITT(buff, TXPacketL, 0xFFFF); + Serial.print(F(" CRC,")); + Serial.print(localCRC, HEX); //print CRC of transmitted packet + Serial.print(F(" TransmitTime,")); + Serial.print(endmS - startmS); //print transmit time of packet + Serial.print(F("mS")); + Serial.print(F(" PacketsSent,")); + Serial.print(TXPacketCount); //print total of packets sent OK +} + + +void packet_is_Error() +{ + //if here there was an error transmitting packet + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the the interrupt register + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); //print IRQ status + LT.printIrqStatus(); //prints the text of which IRQs set +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.print(F(__TIME__)); + Serial.print(F(" ")); + Serial.println(F(__DATE__)); + Serial.println(F(Program_Version)); + Serial.println(); + Serial.println(F("103_LoRa_Transmitter_Detailed_Setup Starting")); + + //SPI.begin(); //default setup can be used be used + SPI.begin(SCK, MISO, MOSI, NSS); //alternative format for SPI3, VSPI; SPI.begin(SCK,MISO,MOSI,SS) + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, DIO2, DIO3, RX_EN, TX_EN, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //The function call list below shows the complete setup for the LoRa device using the information defined in the + //Settings.h file. + //The 'Setup LoRa device' list below can be replaced with a single function call; + //LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + + //*************************************************************************************************** + //Setup LoRa device + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setRegulatorMode(USE_LDO); + LT.setPacketType(PACKET_TYPE_LORA); + LT.setRfFrequency(Frequency, Offset); + LT.setBufferBaseAddress(0, 0); + LT.setModulationParams(SpreadingFactor, Bandwidth, CodeRate); + LT.setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 255, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); + //*************************************************************************************************** + + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers, normally 0x900 to 0x9FF + Serial.println(); + Serial.println(); + + Serial.print(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/ESP32/Basics/103_LoRa_Transmitter_Detailed_Setup_ESP32/Settings.h b/examples/SX128x_examples/ESP32/Basics/103_LoRa_Transmitter_Detailed_Setup_ESP32/Settings.h new file mode 100644 index 0000000..f2acf88 --- /dev/null +++ b/examples/SX128x_examples/ESP32/Basics/103_LoRa_Transmitter_Detailed_Setup_ESP32/Settings.h @@ -0,0 +1,45 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 02/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, a ESP32 shield base with BBF board shield on +//top. Be sure to change the definitions to match your own setup. Some pins such as DIO2, DIO3, BUZZER +//may not be in used by this sketch so they do not need to be connected and should be included and be +//set to -1. + +#define NSS 5 //select pin on LoRa device +#define SCK 18 //SCK on SPI3 +#define MISO 19 //MISO on SPI3 +#define MOSI 23 //MOSI on SPI3 + +#define NRESET 27 //reset pin on LoRa device +#define RFBUSY 25 //busy line + +#define LED1 2 //on board LED, high for on +#define DIO1 35 //DIO1 pin on LoRa device, used for RX and TX done +#define DIO2 -1 //DIO2 pin on LoRa device, normally not used so set to -1 +#define DIO3 -1 //DIO3 pin on LoRa device, normally not used so set to -1 +#define RX_EN -1 //pin for RX enable, used on some SX128X devices, set to -1 if not used +#define TX_EN -1 //pin for TX enable, used on some SX128X devices, set to -1 if not used +#define BUZZER -1 //pin for buzzer, set to -1 if not used +#define VCCPOWER 14 //pin controls power to external devices +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //LoRa transmit power in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets diff --git a/examples/SX128x_examples/ESP32/Basics/104_LoRa_Receiver_Detailed_Setup_ESP32/104_LoRa_Receiver_Detailed_Setup_ESP32.ino b/examples/SX128x_examples/ESP32/Basics/104_LoRa_Receiver_Detailed_Setup_ESP32/104_LoRa_Receiver_Detailed_Setup_ESP32.ino new file mode 100644 index 0000000..f8a029e --- /dev/null +++ b/examples/SX128x_examples/ESP32/Basics/104_LoRa_Receiver_Detailed_Setup_ESP32/104_LoRa_Receiver_Detailed_Setup_ESP32.ino @@ -0,0 +1,223 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 04/04/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a program that demonstrates the detailed setup of a LoRa test receiver. + The program listens for incoming packets using the LoRa settings in the 'Settings.h' file. The pins + to access the lora device need to be defined in the 'Settings.h' file also. + + There is a printout on the Arduino IDE Serial Monitor of the valid packets received, the packet is + assumed to be in ASCII printable text, if it's not ASCII text characters from 0x20 to 0x7F, expect + weird things to happen on the Serial Monitor. The LED will flash for each packet received and the + buzzer will sound, if fitted. + + Sample serial monitor output; + + 7s Hello World 1234567890*,CRC,DAAB,RSSI,-52dBm,SNR,9dB,Length,23,Packets,5,Errors,0,IRQreg,50 + + If there is a packet error it might look like this, which is showing a CRC error, + + 968s PacketError,RSSI,-87dBm,SNR,-11dB,Length,23,Packets,613,Errors,2,IRQreg,70,IRQ_HEADER_VALID,IRQ_CRC_ERROR,IRQ_RX_DONE + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //the lora device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" //include the setiings file, frequencies, LoRa settings etc + +SX128XLT LT; //create a library class instance called LT + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio (SNR) of received packet + + +void loop() +{ + RXPacketL = LT.receive(RXBUFFER, RXBUFFER_SIZE, 60000, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + digitalWrite(LED1, HIGH); //something has happened + + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + PacketSNR = LT.readPacketSNR(); //read the received SNR value + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL is 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); //LED off + + Serial.println(); +} + + +void packet_is_OK() +{ + uint16_t IRQStatus, localCRC; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + RXpacketCount++; + + printElapsedTime(); //print elapsed time to Serial Monitor + Serial.print(F(" ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL); //print the packet as ASCII characters + + localCRC = LT.CRCCCITT(RXBUFFER, RXPacketL, 0xFFFF); //calculate the CRC, this is the external CRC calculation of the RXBUFFER + Serial.print(F(",CRC,")); //contents, not the LoRa device internal CRC + Serial.print(localCRC, HEX); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + + printElapsedTime(); //print elapsed time to Serial Monitor + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout")); + } + else + { + errors++; + Serial.print(F(" PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the device packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); //print the names of the IRQ registers set + } + + delay(250); //gives a longer buzzer and LED flash for error + +} + + +void printElapsedTime() +{ + float seconds; + seconds = millis() / 1000; + Serial.print(seconds, 0); + Serial.print(F("s")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("104_LoRa_Receiver_Detailed_Setup_ESP32 Starting")); + Serial.println(); + + //SPI.begin(); //default setup can be used be used + SPI.begin(SCK, MISO, MOSI, NSS); //alternative format for SPI3, VSPI; SPI.begin(SCK,MISO,MOSI,SS) + + + //SPI beginTranscation is normally part of library routines, but if it is disabled in the library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + //setup hardware pins used by device, then check if device is found + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + //The function call list below shows the complete setup for the LoRa device using the information defined in the + //Settings.h file. + //The 'Setup LoRa device' list below can be replaced with a single function call; + //LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + //*************************************************************************************************** + //Setup LoRa device + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setRegulatorMode(USE_LDO); + LT.setPacketType(PACKET_TYPE_LORA); + LT.setRfFrequency(Frequency, Offset); + LT.setBufferBaseAddress(0, 0); + LT.setModulationParams(SpreadingFactor, Bandwidth, CodeRate); + LT.setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 255, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); + //*************************************************************************************************** + + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operting settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers, normally 0x900 to 0x9FF + Serial.println(); + Serial.println(); + + Serial.print(F("Receiver ready - RXBUFFER_SIZE ")); + Serial.println(RXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/ESP32/Basics/104_LoRa_Receiver_Detailed_Setup_ESP32/Settings.h b/examples/SX128x_examples/ESP32/Basics/104_LoRa_Receiver_Detailed_Setup_ESP32/Settings.h new file mode 100644 index 0000000..2b2cf82 --- /dev/null +++ b/examples/SX128x_examples/ESP32/Basics/104_LoRa_Receiver_Detailed_Setup_ESP32/Settings.h @@ -0,0 +1,40 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 04/04/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, a ESP32 shield base with BBF board shield on +//top. Be sure to change the definitions to match your own setup. + +#define NSS 5 //select pin on LoRa device +#define SCK 18 //SCK on SPI3 +#define MISO 19 //MISO on SPI3 +#define MOSI 23 //MOSI on SPI3 + +#define NRESET 27 //reset pin on LoRa device +#define RFBUSY 25 //busy line + +#define LED1 2 //on board LED, high for on +#define DIO1 35 //DIO1 pin on LoRa device, used for RX and TX done + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //LoRa transmit power in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets + +#define RXBUFFER_SIZE 32 //RX buffer size diff --git a/examples/SX128x_examples/ESP32/ESP32_Bare_Bones_Schematic.jpg b/examples/SX128x_examples/ESP32/ESP32_Bare_Bones_Schematic.jpg new file mode 100644 index 0000000000000000000000000000000000000000..19b2873de7f359235804304b780c2826347e98c1 GIT binary patch literal 38883 zcmeEubwE|!w(q795Trp;T2e}y4Js`l-Jo=LBWyxZ8a5>%-6_%y(jZ;Z-QB(STYl%< zbM(9SJLkUp-uv$%Q}$+$HP;+-j{J?Wfd7Cmf*#09%1D9`5D-AGfqx+Q4Cpxs6&VE; z1sN3;1r-eq6&>RN2FAU67zEfjSPzH^NJxkYh=?9i&{I7mqa`OIqGqM0Wng^F{Fs!A z?Fk#x6MCk{Og|?VW{R7E&OLZAU5;vyj7BEZ{0 zR3H!n3b5Lr3;z2T0wNNyUNrQ37?=P-rEO$vVrph?Vd>=T;_Bw^;TiDmec*?WLBY`InAo`ZPYH<`nOWI6xq0~o zU&|{htEy{i>%O(McXW1j_w2GZPgR?*Jg$wY7h>VPcjP{c+1VlGrAmJjTJmN&f6IVieSV#!K;UVFIz@Y1#V5T(0|MBoY z-zQ^nO<4=>MAv=KDnkdHK2(NUjk{!PNzypU1H#dFBb;KoYkQOA=Dd*&vKZDH-BEwV zYAW@Kl%Pi3?vZ`kV%e)&rG2r?8#_gA)@O^*nI}7B1XoAZ8=doxoP(aA;f!D=(Epv0 zTWD}!bY9@4sr-o)HbN&~jV_Ed4J*uNd0NDFn&nP>cdx%99KH6*S{u6{ZnVGqdfrxI zh_1obP=6lRHkr~!xYjdJw!0FQt1VeBWu~8cSIBEYWqmR;b`D{YXSl z`%;Or8?*3b?$=si`sX`DK3!Qu&SQ99|Gs5w;(d6Zx-p*l8>F{N(t_4hLcn2?@vBU8_b0g=~U(S-X%I zao+UHX^PT~J6~qhjUI%)6R8*W=c=6vTtpR)t~jTiBzVFeX4^jSr2?Au&Xz`+PAoIo zmR?MEkAHwWxZ2F={#ub6(bs#AB5kE8&`ShtD+DU#gvvIivykZwc|LYhmBwM%T7Pbd z%tg!94W2F^Kdq+_9T2jZH>Xs>ctY;)F*_$}@X2YP3CZQHgQ2C7^NoJqIWpE?|@@XE+Q5wI+-foZ4BZ*)cnMF z#3C`qQFx*U`)Y6G9&V}e9A&2=M(?Y*ft6%G)#$dwNQZSff|9rXW_(=iq=>7Z{P%p4 zjmMLEOoA5-_}uuo(JAyFkq6S;6R$+jeZVDFfG6jXv#lN%&lfGXMn~r4*t%}!NL~=> z=GTSBPx&E{XmMaZc)dZuZGDF?>bd)c>%1(#X-xNX+t)LGNPV^euSu?~veN{AFl7XT z{!a&UNu~+K?LC6;+xg70Paj|_1>m&Ne7Th~!%klo!JgjvZcfTh8bxp1gTVPY%X4gR z={{jhq0WFf33$s1a^#ZAlVrixT+;dW*e0v$1P=0j;~s_@Tf7

5~d^d;Fzlj=uPt z6jvASdvS~wG`soJ?t@AGf)l;X3#F_$jSH3eIX$-6RY#j(XM93sy0GZSzJ(F7PT`H) z%|XIsgv~(}3C`Zw%h$9PE(Gr@Ne#mpIS`)hYknco;(sM`xb3JY$JJ}-f&@X*=c%o4DHNT;gV*&72FD^IrBj6;F4As_GWo_+}9o- zeKorEkTlwMuCGv+@#RVuZB*?S&(I08u(yOcQg4iSlofQfD_`z_Z1AN52)Mv zjIlk>w-mhbwVY=dKimrLl=mT=3Cx&J7k0i`Tq2GdB$V3KX1v2_VuXX5*5II(3h$5? za8SBNI~;T|+kER9yKn{vVTnq+-hyw83J*IAy&=?;m=H&6h9h)b}$gmscdNQ+=iHZ!fD=LPX`q8T^jJ2+mV>dBZ`6_w^XnN6*~#^_b76kLhee zhos4oHXSsq`d|;=3Jn+W^le1W&PrdDx7BV=M>qL;(gY4dj|+o?L;)39 z)Lpn$lMon-QGpsjZjl643i%()G9L`W5E><3Q=yw@2Y9Ev4M)2M$n-dFX|(xnZwBEY zKS0~m+)_<0;Ghp0NtsdH}-F1XzpH)o8!9hFe;bH^HE0Kau9YShjyUs}l z729t1+Tt}F3Ec0HQ)Vwp`3|ewyom-or*CgcvZQ;_Gq}0louICqJVstg>)D!QOq@P) zYhxpB*M94IvtLXX)>ah4Ewd|*Ee)#-{%nSNrFQp{R=GalZNW7lB);aB)2m1BAqxH@ z?$ZM%K~2iJ{n+g$USC?!)@UgdBs7L9n+dH18Bz;$ECe5ld)ZO6TQOv^uW7t$Ar}u_ zobYm|eK}fE5hmEnO()@66X$WUD}Exn7Rj`oiQrkCZTFT34uU!@L2giHAe-66@!T?C z?%@dL54z*df~vxxx0&ihpzyvy`{Xypa*JuARa#Gx zy>{DLaue-YN^b{ch_$=_q~^$Or3hFyTz(lEF9?gm8PA1H<+5(wzlGvXSrf8ockYAr(YslrgQ@tqrQe7EN7AI|l z8)%g;&QH?QhHwR9wo^$cWbp)~l|P#~WjL?Yt!ZK1+5iyP_j8rSduNr6{XOlRGSupk z8{|lnDa<={ouS81RF&dP`9YqE$AhG|PlxSpHI}AA^j<)Fhx$kok~}kn1LEkyHOMJH zl&#le9w2YV$7v9aJh#v1u^}U@i>Jm7=W6#c%;CY~u2|Oo^#FL}18J;NcUHGPpi9 zzoZR@%SHzC-T)Y>ZN4mUUQ{FbTBAnFPoC-t9a+chGcU1W2)FW3 z<&q&oRY$9#sp0rIk2+j(OIpB{-L5<%cEMRw<8~D#0}7;jeR^x9*w%zN%+rWB(vP>U z^w4q7i6)M2HJJq@z?fEawe55~r9B529n$>q^W=tt3BYcEb~cNsE?-pGU0tNYL5S;c z&~hFe^iEC^4oU@F+9A6gjJcxWhNBsVkiG5t(?)p~SSH5C#hTl?2-UE$wPLQnT+(1n z>p9R${mq1GJU$X?@s!wjw{IM(E6!Vnmw9il)AMy8oe|oOlCl~t(FiaZBJ)7e>(z{s z24GQ?VaUU?&&h|Rp%OG7q@KsV_CTgwPI7U$pWP5W+g;X1GW&fFRN^Fi$R3nyF)`uL z;kbi=j6JE0_+ww}HJF;&;`UL_8@dXuw7Q^ok-er+v2a1WqIRnEJHhde!GfPRTizCO zqW^hZ4DyP~30P&?KiY7unP=y?r1`sN)3HaS_y_DQ5lZw(M#S0uja+{e@!|*CiMcv@ zkgHKVyH}c>Z17`2@&NNW^GrMCQ}65-1vAZcUH&2>%nW+s``T;ise|s*#GRj>S_^q1 zF&f0@(>aqY7keafXN?@#=GHe@q`$yYgTqD`x0^=w zB<8U-QVQ&>TnvRk=`RO~VCikxG|ru49ZW`NJfHv$>Yhsjf&rG&)!0TQinX0DFkrFy3PleN`XPG*2gzm^Z?_&A-1?UQUYH(mY1ztS;IOxF&~v_{ zQmJdQF(nDD^q*HC&q033!a=|lYk$AOwt1Hx0$kuYaV7!wBOSS<-30CfP-jBAN0+IJ zn_(fvA>COi6Pjr?UKU!?T2X2VIC)s;;EzS#WH8Cp2X{7b(5aq7%9F_kCzmC6y9SOo zc2o%h$~H-R`0udV_PG$ooI)qCGjfThn{GUlVN=Q>(eejhn^kPrd#~>sVwxSPXH15F zQYUgvP%|3+bmFg3|-1vlX-^e%@JJt#}_ z9lvf0LHdTP+v{uG4zu_PWlSd9&a@*BD>`U_ zN^f}AJ}1(b&w5zPsNvQO%--)_MWuR%zCsv96qSeXIl9$07)Oz}O=FYWt=1Bi>VD-% zHJLmqmzE%30W(Wo4NB_f4#e|tr*L~a=Vm0lLij#kw2@#EY=qzYbyjBewt2fdbPw&$ z-(fH6K(Vy+7J-d8JwIJbf7(E|c$mZXO9wd~anEbaWGaUmtcy-7{AH1Hy#;qmf<(=7 zH%pbNryBYwj%u{cN~uPBbNFk z@~jf)ISrp4LyaC#ZI&DFWBi~tBY&8f^l+(3LtHdW4b#Th5~mT7#_M!<}sVI zOwn3G7SyCUvaIiIhpJfNk41a1@T8g2Zszxm;ybND73{{$ybJHtE0QPXeBfzUjri&D zxs54`6#f*ZC^s%H<2^A?&I6q>jqjqImr=_}R75+jaj6fS<%GPvrZU3kPOr4m)f^G= z&((WCuai@}66u*n^qVJDO40n|9@UJVO&pw>HNF*wgQDI*c8-Av4^T2T<>O_c895e? z$7VIyFyjI(B@NOmaZ-Udvu>p^&VwI)7)IGpiX+->5Sfq~k|5g z$GLHKxW^T=bQa+|d30nk$x_Zp-a`{{IMCxYn%b&+UZeA@#O4#~iyBje03xzZ4{NC7 zltAI6c35myA;tJw1>Te1B4`0r#E-XZgB11@V1^rrc)oad!9px2s^Opr_vSgO-mh-~ z9i}mDhJ#+;MBEXL7u&%>lt+*{aO|_oh1{!z(q*v3HB?E0(9};;^yiL9slY)ckTLbM z*Ow)H;WhyLFCRpl=^wA^5?O{ZVwHDT!N_N#%v$e z9KUqbX(v?}Yuy)`Bz8MOg(N_xzwMbce$Kv;M;ARq?si(LUjk`W%4#C0 zkipfHlZkAjy))9_+3GzHEpA>9myOBwdZs>ndLqR~SQG{C$?RS&5#S%4g%b0KBX_Av_vfL=cW@9K#qhe#-p(F+}?uu?9{3@@L zzuD0-x@J45a6P@&S8RALRQyUZXmu*E<>~HoL+ab6U2Mf{onaTVF?KV5&A7*}gSnZ_ z=yl_|uiuEH4NdT*VAsbjZA7=@Hym7K*@%8GcL-OEO-WXJcQ{`FZ3tRUT8nB?cQCWO zhm^+HyB96bXY+2W>neT)CYjUh?O;M!I;F}J{gP&#n*nt~n28qbHc^hR-|U_?V}>WZ;3C+8k1T8x3Nz7GYOD`-NL|i@ep=_>i4og|*Tq7AC5l3o+iudjr%5+ijJx~00 z8GOTwgSAf96QKVxn{|vQi%jbS2*~RvijfatUq0rgu#OR`zDBUH1W{dVhd}P@eKt^R z8gseX0R*GIhk)xjBKvw+G+hSL@SrFCfpu_JCffJal!#`#=MhK55$wW*OWkja@l_L2 zmVBYtgzrIy{SlEV-ZlH)g~JsTV%yX7foVe`wRP_qIKis3+`->3{f8Of9Z)Vs8EU*w zNcK(h$nWWwgM;ox1cEn>hMNxsIbh6R*B$b%va{npV5YK}d1z^AkyVB{Eh*yY>B@*q z1Z4FK7wN4+2cDW4jX@=4fT(VX^RVj<%aad=CILjDK#ua;Y{dgOu3<*{oYU%8$p^j- z^h;B>PfH!rwzy@^>nj)NlV94i8-vX3UV|vV)p>oUA=!5hEUO4++9S`y^$j8L(FtWf zMWT178p(evXbi#lcGwH;yx>##`l$B{AKnA;eSwm=WIP9XGS%We9jKe4QE>V28OJIA zZBTI>dPddfl~0d8q0xULv|b?%beiL7vw?GwjxJVX78hsoUoA>IAkp8o4t$Ve`#B1IU(YWuxwaA4yY$ zr%v9u3&NFNFZ08TXjmO)RQ2F60BUqeND$s{9OJ{AmBR9ypS!IUgP~57p&gol!!&n| zE;(T0d%=0h&cDED*XPi-_$ zb_+Bktk*O5yjyptFJK04w*tiyPHx8rIucGOO~vOSpFVslLtnH)9%RFjwU-v=Y*P2CCl#uSsd?36AZ?kyTK!|the9D%n@MpG8xcgqe zzz-#yUSP5JI=j37;w(h}`MGjIvWA}M9rki?&p1PkxAwp{%)m++L#TQzp&VYem{JYy zt(GulY+T)Kfa=I@$CwI>%-SGVr&!G1Tbtb{s%EoymJ=C*1^=&nAahkNa=f<7*n{SH z)-o;@KvyEgcumXbCUpkl(5+`@G2d4OnRxs4Ji7^MZD|oH-;>!%8?DOZnQ=|+4cw3< zDwD8T<*v_BRE&BGbv;UPmuL|Y*v!*(Lj2XpO7!q?@}YtwL`m%iJ{#%8u*;i(SH4`_5S^ZBh*P90HldDbw;xi4NhQ zZ-WHe=PJ68Z++uS$NT#>u2+sv-zKE6O`UwuOX#v*NvYiSq%SF6GQipLG?~w->u5Kq z=flsuR}vzv0Gf5EM868MOZ9TDO0vbq<} zXJ)h>S)&`)OucWfGg7+gT1`}S>|JSe^D?4I22X|YlRc`3TGvYu8VKWi?S$PDkO)*2 z(?MC^#BgeoMj#CM7**i=RiB~ejNVF+SM8mDnlcL-5fCmrj*7q}?(nlt~cK>nPi7SvVj6PRXNj#AVbOS;R-haly@ZJU0vQ>uLByp72x zQur^AItR*fHK@(ZFGQFv1JCE!cQ>kHzJ!}kKV`PJm&C?d5gRYfUlOIc%wP64YMV~^ zBueV;Pt;f$H}idl#d`j|Qv{tEX<`16TRWFn)ddwM@6{t_+*TQqC!6k17VnrNf}22> ziZX{)WqCS!JNF&T%y`=kWmcHA9();~-HKA~KO}CQ7_}%kD3v>pS7T+TMQ<>RUfW1E zLQ2hcoG#wG^O34qi$B(%wiL>S@H~>D|Wl!aO?6R#ab0(FdoL&~&EV)tZn!q}7omvlI98s}GO+ z5PUJQ?{zKd$*FwvBvV_R%rCT5uglIU1_!0@v-RjtC#%bm7uBYah3kU9x8B6f=bzq!P?1rAk%Ro9Q1Xn^6F{eI_ zG`0qe$c#1;O~|-!=Dki)1BH9!8iNZsFtW?)R#E=pGrI>?Rn<=srGggy*F8x4GHGs$ z{On0%lA}f^5gWfkaHm>XKW>SRn}(4y&9LNSD0vJxiftg3rWa(*XlDvrTXb5eKrQEZ z_HC3jBbIeu=vHVvigx(y8angdCaYP5%gZd>Q*CBXd17RIgX`Y2fOkQDrfn$v8)ei~ zvO_BzO0TpAE^y-zRTha_wKYQU8U%DB()X)fd@e_afhg(%QnU5fB|N63A5o?e6`&Wk9Oy<4C2U66L zPAQeS5}-F7OG(m+9PCz6c)MmHEL9rMWXm0rS2z3-TJqlvu^D=)YcVp+J$liH{lcqi zBjP+9f-wo?WgEiC=)(u%BydnrNe1ND(Qsy<7Abwdc)GK?niaZq+~xKi0WB08M|;b> zko0@cQc&t1mR%;EK1=r019HJ~vsJPtWi;}QV!9?`s1cpxyrj*?Pjo&~)H2N7ZWKSj zZWL(RTbpU*T7}uIz@-!u;cF? zCUsYu0|yN~8-&3@9GAlm$4wp6%<<<}saVlU*NYGy863&9QbS6-zOm(k>Gik#?`o5OKzsQR^jz1i< zO6Ud$yv-4 z79#*<{{ve2#g)w$Q6WLPOU=%r;UG!g=a-Y2z}!c~nRoa9Z5q?@7dcb(e;tMw5O>~z zzxP9MuQ|q`e*ibFM;o`nf~j{-und67uGC|uZOGZ+b#QPh(kXZ0q1@Q98a)i+xhRr3_#G9ypTNB0Wco{(c1rFdzO zE=3TyH)|etX1Uy9kVMA4w>68=29X%;*K8o4t&HZCjKD#ii6*gO{i8EG^weQlv^NY} zjs#|fk_67%(v(>mw3%<^D~{AdgczbBgzd`jrzna$dVQtl@t)&b+W}a^4qlAM$i|hq zg{+eM$84cS=YtD!zILjQa1dTaD#7fOh2W#$PM(O~DbvYEeiHQWeBabE=Dsw_;~ju} zo4N#!Qmhtos0v^f`ga+Zxr${Yti=ogx)#*$X$=$apWg#vgPrs!Lkgy?o+6$odQc)B zP#rvSPtYPMd@P0FhlSosQ7EHgt1v3EX_zjXsN7b}Q+uKieWbFSbzL08y`}8I^%7Xh z5sSld9OxuM_8J<@yo5$EHz2G|LpZ(aA65^PPqfL?B?w+2Xo~*yOKJ;%Uur%r&<0ct z1*o|&AHcq8EL^Lx0ZKM_$_;sV3Q2GLi7Al$LH>&y`x`yO4nPkFU2Cw}sj6J!e&+BO z_4_*n@sD7&(DSzp8s{el$qi}$4iZH6-HBg<}RQIOrS)$Zdvgv?l=W4v23u zuhbR8npa;=0u>%A2RJBWy0RGP^O)zjkUGvYAUlPFxOs1-Hk*}Yf5IFrzo;$1=Rb2_ zX6CmzLj$F)9Fl(+DPZNV5dLDIap!H;Wmrunmc$lLPqTG2D9MqZGraXM>81`MhIvwQ zPM2pkP8f&tQ8JAZ8b=w5U9%QweKL{6YB?69jGgZGm!2Zye=XkOWPi!65YE9dP*2T1 zKD|RZ>Wj2QDE_9xQT(D`k(u}W>!@8VBuYg#qMg|O>yMpl$Bui)=T+}n#idt3JZ(8b z>Zu=2t!^kZl*V}FB{640x|c#8On)LI61iNgH~zdanr)?}O`mKVRsO|;Qdx(z4^L6C zSTncx-F8n-#bgg*&ggD#W8kruoCo_NvV>53_6l0b1TF1hA&-(oXi?O&9nDd;z4f?d z*sx)kob<~=U(kl|ByFyH={Gk@s)PnURtK?Y--&aFBv*E?NYLxt{MSN0gm&LVsfG_4 z$c(E!^fNAepY+7ArY*HxoyW%CPVdfeh2aK^WymnOkxA2}RC3EEd5r z99S4(6^l`kg7#qM;e0-)Cj4xYn&}%KG;Wi->?OTtK)qhpMzT%CzI>2zuCmVPJq9?C zcnJg}!kqNromMdpqk9HruA?kyqaH1Qmc?ug7HYpIPTfOZ2%xq zm?wB>J|md9AL|cI`$cp9cEPXCvEcD7`apau6`&i;{fNJ*&;Jep^-mx46O{T(=!x;K zluVT)>>84g2%|kU$fy5VcqRM=O9c4*B@`nRMcXvfSd7p7qq^{~bQ<(en6Nm1LB}G| zyJPRP@O}}xF;sFh?bTeRp$)yOi8~HV;ywqg7NVjv-f_!wL>cA1ctb@4yFU~_5WJ5Y z>o@Sk`(sJwjfx^r(cdxn88K-o$LJEKK4<0*$t6mCp_>8VTb34Z(8(QOKOph9=CH9* zIB4T~@{f>fBDt?NmuvsuPz`^t|L}j6=ufg@E?;C3*?!!Yr_E}foe5MBtN4ME zd6<`t`DV8+lvMuNR&V#)@+z5CXMSrJb*kQSr{F}5gKJXREH&vuz&g(k1MZ}f2M)S| zRl-5deK4M>$}ldH6fGA_i7V#A$saB8to;1D;zhG71F_fOq5iRm3fViVP1i9s@SZ#v1}TOd9K%6j zRKG7_G{KE$U9T@jjk{oLrT7l$AgBU%j)=H@SCRf9E`MGe=;92P%?EipdguOWTN$sV!eTmy>CIDC?Wgg(CXyfd?9OaKD1^ zoPQ_bQhY6JDs9gxSZQPWts*W(kPR5B^BZuE`}<3a*mJiGa4T$u@tg~4v&6$jYd9rntX#jc)-chG8aqzSP3fjC z+Vz%LRL6%E=t%I+RnAp*6b?iXpmMiSQ?)yyJH0er`xo6TC~D)*H#QpbwO^V)%30Y5 z3Oq=n~SFJN?}ZRRgMuM`Y+Qwh@koSeLqcp9C8j{V z{nsfI_&)GxWw>XJ@EnI{YkGf172lbzG|%m!>~NGS>vMN&5hWXXlqf{OMSP;;#M^>b z{RO2C0kA~BFa9yJeel;8oiQ}U1Vw+oL&UH997}LT`=dnqoby1KGktEl-%d2z&@fi^ z=?_8$oOC4Wo8WW+Wk+;(diM3{gr_myW2Z-6@9=8D>g3)i&Zo-f_Kkb_>@j5PYa0J% z$KOWnYS4L+erUVq>~#JtyaM|iefwC_FB1eBaXv)569SGrmcUW!3&3R3onI!9`!NC@ zt_8537~o&eTU%ABhqASYveWLjgMW2{C_Md zG?x%bwQpMw?UBAz0iq)KUyDkB*l?gj*4FJAki7pw^?6d5cQ#I2>M5^SC1v_;mE@+r-A;_9nqZzd7A7?RdeoV} zi6>#UgsAj63m}lcvaW9SQTYDSoNL!^b)qr4UqInZT2L$HHduDcJZq~{wjZDQ6|`Ks zv+S_xy@}cV%BE;Tnjl2>O|@{ed|JAj#MJ+Mi;@7<Uzepl!yJ}qhWB_mPBT3N$Z+az2^ zhjcLt@eTBtySI?temDwBva!jcDE+lQM5)5UhG>3zdU#RU438VH+oOeQj<;EY^9qPR z|FWA7S5H^M!uAB{FKiEB?H|&>S+Tm$(*D@Si@Yo+*MvHpsT5Bdst17Ry%N+9io?mV z(Yw35MO>>*=pXaA(W^WjBtb;7L7cVR+7o{RFKyy}oQ^ zlx;nH8GYSvLOK~$T_sY&NnKF1@XcFxrD8i0o=mRZVQ3Kzh79@w-~QG0#Cn+N#6#<+ z$NV0|tR0f5>-6CwU(G|6wXut})M8ilGJ+o`{Hr}c`4{{1|3CX{+4)y{LrTdk5ict} zYeKm{PU0CL{5qnyc2AN;NQ>Q2PjFa!t}B6d|HsGubn=rSR6`FK(n#H&LXQjPbEH02 z13~GpQenN_OEIDTf_dA_x#s}(fabz+v%CPrFR##8Y!hwz7pt7I2_rcQo1WJ9wRlRwETj2lh_ zf^ZTOrkhyZkNv7MLoFR?kG3tduc$lXS$Hq!+!TC%w{VB)ISG>kfP`sq^*-SDbS9dQ z4d9^gi8Dgs(&kkuy5^+gCy?Jh8op}=pw4IWK!gni{Gw}hUS{mYZ2;7u|B8HVGKhn^ zGEwcYvtsw7Cda-&%K(35YWKr6ABwk9V-1QH-t=7`*cZBEXd2_@k~h{wjY_}sZ>41V zEsR>F9?GGOa>$-O{Cs#T@jW& z+a(C_1bx~w}N78uVZ0MW4jufzo2#)>_}z3Xrej zS#W0D&F8-{$;G^xneo!0A-Fo}rhUoxGOmTRyF_KF`Xi53pH-ibypCeZ-Wki4GJ1*9 z=g8UaR{sDfI`ziTda--ST=A_)x=vqZqKK4YSXnpSOaI)7D2>h(lIa5ex=rxbr))O* z^hTb~LLzf;5VDo~d;iUcNtFH*%8-KqsSlcbA5J^<11D&}2Y$NM9UdG-4SWzciy~be z(nn8+wdpto`&f*z;*?unuPz=I$TNNe>f&#C-?+FH>FQqDhgjH9l0+HCilL#Xa*~je z3Kt@&K;EVn`W(y{^j#0_?jP7#$7&z1W7>CNm_#xtR#}Sk7)!ng3KCrcs!rTprG)WC zQue%TqU&S=wwWJgOUk|?@*zYtwYV0j3ksJ(jC;?w%bz>3h9V)Yb}m07a9wc>7ZE`* zHyi+fa^P{;)zt=|@DskHQWb&7CNhPR3C&rS*z6C0mC_=iK?Syg`xbKg9JARfc62me z9ai=>ZFMeYYha05Z?zd^?aBH$E;x?lEJ{AbEZd=;$PIstG;X z6hI5my9IAY-wnSsDr8`@p$oJMFmsKIx`(Ozw7=$=_#&rmSEZGPkkQg)g~fN}AB8gO zsUobt$#!&i$L-{1qshvMeOBFNEWGbGqvr7V4->pwBA?sN{J7}OUzwn`5w)rcH=npC zT&omgD~XJhOzsfrcyIY@=3xdmBumDUmaB_a+|Fk0#f+O%wZm2TzT%f+8D}|$YIDP| z#n8^9cfpdXSAIQA*drd>k`}%pCE7eE&zjyqA5YANHEM1|ZKIMg-bJge9I@s53+z+Z zR{3itnrFp4#!f@DCkz_Ed4@tC{~>OEz0=}g<8+hq_f4e#Wa~_Dw2~UIme7m z695aMBW>dKgS|K_C=-VuINR6Bci}O0IL`CV`%+Hnx(m{bV;q@QpJFiG0`G^AVwcwV zcRA1#*Q}gmcq7V9QQF9E3z#_JMUm4#py9NsVO5Y}WE4$)g_`^@27f!A2zQ@bEO_7H zc&XekRUfRR!9wZ9atkAFL-z|N-FEsN>D?SIAiZ}bQKL9?;HUy|K4 zcD{}IhS2vJ63y2t4F|LCf%#e7p%ZP-Y#;}~Lp`R8?~kSFT|}O3byQr@h2<9fXk6}h z%M!PLQEX?$y=P{j_HA$VZKlKG>M0{qFsL$fn>eCaSYt#W*om`Bm9xT2sb#zQ2|sQA z*``%yiS3};h)oGYoR?5O~~53PUbgX=f%A z8yl}9(*pXJo9;7yh?4r?+}^3OTgHwf4D?*^#f8QOS9qs#-15!8Q7oCCu}M@{uYe5( z%aECWcNE z`ZMSSprG34&O~=Lio6NZA~g~lE@^)xsph`uiH)|(Vq1E>?*s=KMD=V_QrCYSs+OUH z*m8Z(Q6l|O0L5+DNbxn>nJ&`Wo{V;n=wnw%r5nVcDEtx|`V{SGzuS6atz{)~GpvDu z_+uMsP!DU8H85ZIjRvY?kBwIV69pnad)NCu5y^7=WG zxl?yQzK@l9snEa%6sq^Y0F;UWUJ^X4zC)`3S{iRzJO{SP`dm`xE;`#o7yZ%ssRO#H z{hwG`x*9f$4jur|ffMBLGw@hLz%=9z01>{qEZJS-03hwfrG|_Io0O+_Di95V)`0@q zKPP1Z*xvcJa7qb)4F`uM&|qH7(OuxqmuP%IukYmh=1ma*xmZ||x@FWS0w9X>Q~>XN z?;N9)r=Mze0VK}?&40aMN7ykLrbbOI7y2ucV9AjOBKB#?UgPeSNu-3*v5__EYEVYW z*Zmc5G!Z?{Ghh9k2@{~d?4K)FSv)EY%uQi>G|5V-rDr68k5$guf;0<7vDDm-iyy3- zmSoj%zfZ=n20PwIqMcQjYNxs)P6ryS<30nA8hshskL~r(6=0g$*#KDIVk#9FJ7WnG zoH*a#--q3QimLT|je~cbq)In!^=yj*i!TeBvl1djg|)_#vrV@* z;?_QqePFxnDG{|8ReK%XZ?3 zM$2a7Nh%XGcTFV)GM~dO`7i!10ph>-48t=gtICBDK5@VLMIUXPF*1Q#hI+B8h3w{2dq$R1_ALp!1H#X{dkvrYZ%s<<>Kkwn`ueH)XCZV?nbZQ%}P66uB60%mf=M+F=|Foz%Rvi z{X9Q-T{e09=t#e8nTpW;yi21_YpiOtzIHNc%0EA?P~CL~Vwn6sW%;6$mG&b13+@*9dhn!B0R?DmpyNe<7_v!IYe#a)r#f*f;E_9XnW@b6G7Y6h z6WM0KzLP)pi+9ALr_$mAs%0YM2iDV@2m;Gz(yIPdeF$M51LIsT`u+AH2qWPZH;NzR zJTOyyCKp|+fWp0CeB1!Au*D zVH%f^gvOUO@7bxLq>`Jf%Awq${!gs%f2X&uLrpspj3Bg<)qH^6rw?Wl&-~jAuK|k{ z3!_?hdT92o8OPmc;Aru~UitUFveFsm0Dcfkr=o~ZGFEWX(s*MQCDX)1z;|#mBpHr$ zkWk-4z6hQrU%YJ;=oC6DN=1JAWZc=yTY*BZy_$xs7Rf_Bh#-W4`-DG{k1T=$%{DjG zSrm6>?AJaso9)7m;K}Yb19ibTOiG^=@Wdf>m&5t zQ1tn??LG3(b}$QTi?Yj2?jvzpe>bEf>_TRNFlXKcxSSuker=DS|L!=D6oMw9*2Pct zgxRBu8m5Qoz^(IabRa&LK0i|CgIjOD8ri2R5M3my!`S%s?|(T5LwneFE1pwNx5I{f zxI*7J6oleO(S2LUu7l2Q`n5hn<%3I%#VVJtDpDKKMr6xt=Pp~T=UQK&w^|=_%-`m> z7j4_O*u3dNHjA1lOFKMUb)1sc9&1d(6Ey{!v#yqd$*Gn>#;A;y=u~HSZ$tE{DnsS4 z=M*L`cI`jm+KeHlML`$1osJ_E&UBV_adj_M#qIr;;$MA{QW=h!e}-O|$dKS+!Np>W&)S$>Y5~Hb*_kIZhPLe9VR-|g4%72l|YU}xm$h}{A3or`vTcZb#RF{ z{Z{ijGw=x8Z)qn?@J69wv}n&(PI6e$cKykNlUg}E5_>U`J2dTGXM+#=WTQOhyC|vC zuaN32KE=Pi=VP1IGKwPj1 zBdHp-6;{b-ElXFf+PN3TrHG-=bxEbP7u?$6r5KQN?cGJ;(yxCU@idiq&_=G=I+pZw zip|c2e>Xb}x;(_F_Q+p!8Fk4o*#+5|H*?8idgDf2qdIXeY{zF`*OsbYp@z< zXi-ue*}h-qtr2{hNjNw)V;q2jkpkev`Ds#+l>4<8S_Q68JQ=*V=^f~kIllf z2vLr!9jd!i<`N;OlBVE7D9?TD>ybgVvBm0|I&hsz^a3dzB7s?L%g&~ipb zsuHxL1*DoUQO`inUp3-|ucW)PT^)~gqSJ3>VzyQhMV90iwI!I0g00(%N z-U|phxYW;_d=Ldj?5+lH99Q(3YHG&}tB9g7 zacRpwah?zeF(Lgg+TJoMu5DWvEdmLy!CiwB+%*Ib?jGDyxVr=h1PXTx?hpu2xVr~; zcXtW0-pty2?Yrfj^G>_(-XGQGq*1M^QPk+8_s<3#_#u=TP{HM_n-o>pZJlF7Ar*1 z*6}UHnrT4XiRf)Um*o#x!l}rn%Hi4PrSI*F91Z43CDBbBrdS9d(+F5V&sdvpAht=O zt2z0|=jG?HemHNd$z;n?o{f}p?`B5ps^72{5U^TdlXn&x=|W=IInx;NOfa>g`A$x3 zJKozNVQKxl!=BF%?FUOIN0+7DVhzp%FWQ#M)eC_}#!j-7%~o~c?s2%h~{RNH+6@s)M~45NBTRM+zqlKtt~wza5}XXbeETP z@?3xEwqFh)Ch}^PWox=_30sgYpY{uRYD%rU$0FPGXDe0ZZH+{SV z)3to=g}pXaynlaw)ZP0>0f1CQr3D&OfKf(T$jSNA4 z@=KHc?4jL&xGG59q(IEM_m^crlSG;^Rdlu+nn=p&LJMKT>C1lVx@mia#S;Cu!V`pH zDnY5P90>?~#g4~Y0Kr&KEWT5cLjr$HIFvuCg;Vg z1y_3wKxfN4E$d1ArWlBJgorB3@8u{FXyRWXiL~JFRv?llAL@1*5SuYLlhB!(P)@M= zy@1Cd3)5n$U1wEfF6JdOtXj(2*SW=;Nt~3z>6yZmqwl1SeQEIvb|we=TRyFATXs6# zJNEbnw~Gr_+cLX^Y-r*F_}$8KceKI}NX?`@72!{3@-8wiwzufEaov;OhI)x{l~4G?Q|oy6o`jKir$3oC)MZdY15K?8 zQ%Ev0qNAFg_m5sW|95-^P8V~}l^dhL0FzS7akrl(k0XC{*}4fEX04My3~6Q|_6YZ# z%s0-g1^fIiF-#9U)!4O&ge854hwq-aWNyM!%t}Hekc;rk+o`YzCi5ev7anRgsikG| zpH*IrzS@DM4O*qDwdu1XUKouxX=<=EuZz{dw8k1BV#jaqSnl7t*=+$COjTA;oSTPP z5T}7FC>pF(VRz`ak9UiDH}9uMh-9G?%)22|4cZmvjo@8c?`qD-J2Qw-DWewPmOJe7 zyFT@J%&CaYw;eG*#?PPjEdv~bx#WLIfhz2PuRv@h5ZExs2;BXgO_X_c#N320Dg%`VzRIR?au_)Uo$XlERMcZS+HI z@9gb)w`O(qdM@sqW!|PvlqSZFUg#M0o@NctKN5_s+v&lHY zr?cSMCGTIXwZapO5zwAwCYOj{?LuZdb37JJm925nj`v<}QK!y)hfad8>4Vc^TqqIs z;YNpQ&d`>QNmyC=kw))=0n^}>$PLzH+f^MQ?OFAnA1=>Hl*r5@5Q<_6bz@}<`|w3R zrvc?ogazMrKroRyVhzn<%p%8I^6aKKr?1GTxI8{khhFLA9X|vH{;cfDcx6+qDi!f# zYnDQi>!DW>jk&9G0&PHK-}ie>gef^eea< zV~FIY1f_ohbW0(uu9vB^^uhzkRM1CRAg=1i`uU0GhiWUG9R<4wisxs(b)P2JO_0=6 zu<&@>$Ft{gr>LrQKmMulA+{x%0&6IS^}>|o1Uk0}w>0FjuNmuwz0i-Wd>2ol*tz*5 zZL^d-6UOE|T&AT=f25I?$!o7t#vz|ydVW@6Ic{3%SV+g$M~t3a^Mn;n{;#as$i<6t z!bL6a0$5Cc+F=d(EudJ~c7+IkBkd#Tal)IqnP5PrQf#REaY^-{Y$Nl6wF_WOC}E7z zmeNV*M;W1IQTX~YX6wwh2gp?xOt`ADNi;n7)D#pqYrFWPtez^K)@qs@%K9JPVu#_a zyX<9Bz5c3%L>z(IMa)L+Fn6`DpYIJWcE8n~Ix_#!b{9rG^8>Y7a>2n}tD-k|;c-38OVa0l7OX4C{+hyuz6qf-l-qt6LSbe&>H_*!zOfe<7s3@4UWVas zropSKwTXxF7`zDIIR~q^9M#2k2|D_Sf6aCQkI+kVLb_@h~=%#%GB9u>K(dT!w zcwj60A=>$xQ!)&apnF^2XWvij{JZ5&vMm|qOE<-4)@r9?EscI1!t*)xV?o8#eyCS2 zsY0^!pd|WY;wAKzwv{zsV&@)HZ$F5(H#$-W;4OTr6G@Vymbo=NT_Xd^wfUkhpI38V z8X;c!k=@KJza5TWzgI`w4G!=-Y1?8uaD{Q(H2RIjO`t&uoN+&ju-?tzPGhui}8mwJPPx-m6CS^U^l#OBJ<3lUe>~xw_`V{8dK#?d9y*Jek>1eQEFO{0CJ*S+O*!L-me6jn0~0%wf$5Qq>4m>zJ$+D{!vaSW zq`nVl>ZgSBRn_KyM|S@H9SDSi_WO$87#ZCC%<$9BL==3S-Jq>Y75HqhBJeb+;mmBj zssn9Eoionb8Itp>6IqIa+;V3}B|72uF7M0L~k zrTy*ICJ6qk>N|P*J;sCGo;!5>o*~WU+O^UXUPY9F9tBVd*Y`>dD2ng8VaIn<+t0QS z4upB8D>flov0y4iGi?xR6#pfBpp=m3gb%7wO^b+{%apPN$3JT8KWeN3SeTJ9t-)xf ztbFBZh4%F~z9F`SHs1S?3xS)B#=&io@0Du}Q~AM66)*Q<*?$PoMQYCnJQVUR_ivs+ ztk4UG8vsSEr*9?YqV*eXT#U$1M-hX(9lS(U{_tFy8)}&P?pnG_lCfPZ*&m~bB1-9- zKVwJ%`3lZo%Y`4S;)#4d^IfJyj0D4k#{`qp?|F=<-6dgWb>`3{O3NMKB2h;X#qmU6 zumkhctFT9!^*=yg$<&;97Fa^9u&aUQg)K)8#VC0MC(M~fEjEbw!6SqJ+#tmc+8;Hf zR9pjAo-@h!;0gC8GVxje`rsvRryQ2(60G9ou1+cz^pi@Xm#9dH-GjvQEs{Iot>oEA zYYI=83mYXP?2W4!;Z|JmQt)?(E*(e4fu4oHf2(-%zQ`dQ<-gL zb6lO2e=nx)LxY=M%ql?-?M?pf6R4K#;oc4q+Sz2ZbYm=P4WC=hYADx7lt2?$Bw7{cWH>f5cdJ_KaAJ-SPXI3+n;Zm&XN9oCXIGSjz-X^fWF9O^Hf8qY%H%|* za|V%v5cdytIOk1dOf0u0+~;u2jH1PkBtQ^dwHZ6GZG|~R^ylJe;>M!o)0!ppWK3`C zhPN}5KJiz}Ud*R_t=cz@kT1(p1E4!LJCkY!T=aZ@k>7VsV^?aR|qkQ`# z73bDW zFc)SKt-a_06sU4LdMDb4B`1xD`O%S5YaY*%1qqU2RE%iZL@@H>penrcH;2$8j?}bv zX{<3#*vgyQd0JTN`@*>JJ(UHxlmcmEZCyTlf3{JMu{9A2EnnmaDR9YJT$5U(tr%5>HoLYGF_uqXrvKLyWXn%ym5K^J|Gk^vtZ{|}bxxzzecRL;Q>`4- z%RKYKur?Hmdydzah0M0H?e~>2R)M$AI#XU%bqXyFE(GiL1;I@Rbz*yIA3dEGt{^r% zRb{>#KEaqL!vJ|zSawY~4;(9T5Tjhs^QEJ2LE--(gq0DtgNf`z3xngULT?dFCmL&O z!iTG$h7|w1;C?kP?X5|lra$|#$YqsMoULxmor)R1U^mPh8e$t z7j%$WYp~rt&xFz()v3JBpFPiUWpmWC`mhNplsrmirYL5} z+kH*Zpk6l%NW;F4FH>7;FR}^hgf6^Oed2p_7)7K}Bnu%3J4{De(mCp*%}$_cIJWlI zEAspa31$2~-uodp5w~(pr{wXoF1@56x5XPG2_7;VEEM^^<<+s<9}s>`DPSNME@vsF z22)pJzh-ibrNYFyh)*}!U9Nv(%quYk_LzT2F<#+?>#C?hp9is0*jQXqBJROZp~g*u z)9}%rO({>#1{aB1tqn7dvP=`1`V-_G)VbcJ&8QDfI>Pi*6<1qOZU&MiSy`Orrca-` zR*Og&1s*hxq&%6pcq`XKc=H})-h<1A#c2nE_h3kRJ;tz^JC3Zi2Bk^Xi~)KSs$j!!VDbFzCX}MC_ZHyZKuF65FJNvk~>9Ae-)C|JU5+rcWeIMx+Yfb)I4(Fpem)pu8Z9wl0%9xjB~D^o2PfmfyVjR)sKs zDYm~AdnRT@%AuhwncP%j6OU?#2nHUS?}@E_5oiC=o}Gw*OwZxHD-v|#=3e)T&9zl|OuS#gRb3 zSM1axB+}pUJTv3Brw5=nFMyqPqv&6MEcFa;)%@H0Dg6C?ladb>GJj+@F9;?k-nc0W z#DSqp_sae!sY+n4wY!n)lGtq1Iz694FG{^{fS+sCBtHS{3BoCR)#LRW9S z=U>?VGQAYVr7MXlo|EE#cRN$cu~ufTj^Z>4E3|*=W0_=QeXGPgiBk4PlsL-l@b@z} zp2t^wqV;vt$}9#hUjeM+E?+4&Ow0!O#aK5kWwc;~ZP*w>Tt;RYF(!1YbO!x5PW}B_Gpf(`UEz)53+s>SpjBeMpoNpp~bKK;RsYp6COyDIpqpN0>D2miMAC>YWf`x#z%7@=3}VNa`sSeq3V0)@t!Bk#v^gm;wxkWaJkIsi{QBNi9YqcJbFPPv0$( z_U5W-kj*%#`Iyr{;8@!V#my^EgDqE7V zu`Ts3QPI}Gg-ypoHn9?f+?V|U`eJzTRvfS>GXtoXoKbO7yN%Rc)wL8^`CX_~Z0|FX zS5l@n3!$6`qB&a}A!wD2*p#(@+Y9NMPS;y{iDVERr`vGEbHQaeF|w=-2!WH53J3SJ z3)bO9F2{QZ!3sI(cVekwAy7M_TU+m2zhBb5)x5q%mcUk}{vEXRzx!Y$(wsc0xc*8H z-m#>FAjWK|OKyjMH2N_M54E1JYyd=io!z9{LcSiuQauRN4g&Sa zdFD!s@ixdDS&gj^)qiXa9F>VIm{SH1L_N{qh#J*LlzI{eJRaCY3ZIu(e$wbjl}oD> zvg0Y3dKvV>W4S>T$kTxrP83;3828(q0Wd;XTA-Zl0gOErK=@|qMm?ecHRfxfhrznF zm;Tf0ne4dMfl_UL&V1aq0q2AsYh-RT9&;NFG-N?~e|Dj9;WzdFFRyg~XyBrW2bupq8nuyP70I0IePG-=l|jJ<{g5Z-){y7^dif@nckgnu$H zrgs5vtH^uEkXX#Y$H?9E5+}{2OagUrQR3yuRF%Ie?tZ;gW&lJI@i0oUGv(>~>J{=- z+bMuTZxmscN|@^&A~GLoR9bn!#fH$CM}Qm#PIa9Bw9@rjlafxny>8lP5fsT;m9Nd% za(}h<2u9l?j%BxhB??<1p?Ss}85tFREnYCUFxTghyAMUjsWE!(CA6XdKaSp+-}>|0 z^HNhL^0Mv34tN-MhS8pSTm0o4_PHT6t<6?@hPSYbO`UaCNf^|)Vyuz0&{3IuAk1IGY9TThiOg_yVj5oabbi5UqYkBV!k*R3Fx5tnS z6xqDL`ny*mU!3b*#O#P~DKX&u(byMr8G98!?Y7D0J2=RkNj9S@M$ z1i~2j?!WVH-Xq(4;U1MyeC@`=Lv#Y~tbgsndKrDP7(_8P65-H(s3uG!hBF%++%jy% zgmr+6a9%%Hoz^b~=c#@yAlpzoW6@xt5wM`~uoc%_3~Pn2_c0=9j5J;OZ{sWlQw$!! z2g;E0ys@L$GR2HJLH*E*9#f9KDK+hDA>guJPA+<&a-f0GV0CDUYMC-gz3T79 z6f>R!*$OE(6yn$hsK_r|w5>1$w$%Q=YKhdk3~552mH$A#?tsuq9^e$Ska9m_`GO*Q z!3H#rrjlZ$wIjNFcvZSO?=JT))3h`Yw$kLy?Z25+rpZJSyxZ|3m+ezRpzsQPb)!y! zXKPQ$7I*1|#y=C;w5YtN#AE(Ng zPd}~tH1APIewP&6?%8unl`J4X8mfyf5lkv3yc*dv#FW?Nb_Ac~E|U@JOzI>mX^$ zM29m~iTgff`i$ccYm?+)AaCPP6yrt&X6phIz1S*3_||$-@eLP5rsH+U^K}>2!=-f& zw^7Uj7d+)=mjw^;)$haMuyW&}$J&|J;>eppQAbM3rg2e?WNT~rPClWocGlC&drUv0jNwxd=kz7ue(Xl*Ee zH>=(-c}-+lvq-0NttIeOo3=xT$9wSv+FWSV`G4Ij7-Q~$`cB?r@!XXf^_(E1@8T$e zDzb$K>}zvQ$^fTU%wX6ZnfplZSvhrB1fcJ)v!c?~g@#WI5NdAS zz;^f9NqsTmDOS(osY~wYX+1;3SfaFWPZ1FgiI|Fu%4Vsbs^YfPpxe4(Isn@N;PZyx~d2o6Np)wK4HG% zD=>ea|0&)O>b>$wV$o6{3=Ttvn$#&>BOdH|cHpFUq=+SsoSchb`d(p08aQxf>v{~% z{-q57Hd6_v5I+TT033I%ORjDTp~Q4b^Z2LZ)1!)lK)#@-RI_Mj;x%XZNvOi~?B7`o z2hWP24eeS~NjVaOQ303{Eqf9{9D8;Mg1fF`i|bHN9>j36puUGSJ~eXrNXk1F?{E0HYv%3uxNln;lr z$rH8$f%ieKY*60KN#dEPa2LiFKIW0|P&cgDU^m-EBG%gjc-hhC=_bsTuc!))cSaC; z;R$6T8xD+t3nNdEltZRd4^Y1eXEc7Jf)YLst;ftiak-=t2Bmj*%#jp8uppDxgVDRh zH^MkCrgMSGtAvLHufT2G?R&~rj;ln?2g`vt%BGl z{>f^Xqo4sUmIU%Bz$~-(5GajGY>ChOTA9Cj2cF?-n3ZsQK+CvxU-Ga7DJmdQ(xJ>5hFrIf&RJP42$QVf00ooldX@ zgLmpuZ@GI(N!-fp4lifkHpM z&sK{y+YMj&+W}WG^U893NG0qtydafneGS^I4^80|jWQH2%IT&FWJSl(X!SThQ#2KewOqnuNMOp`K1jh`j=y6DNH;#vDq&>whDQ^PM_L8rX&W2G3RT^K_hSeAQIX8?>AsOEyO z3Ob+otz-_LtQ^{qxJGD|&U|CcjEd)$R>aIhv?%JOVujf*>G%NV#71w?Z$RqnbH`gi zqLA1qn|y2{d*isH?*fb`Mcu!*4b!te2e|0buhi`HQj!EHc^?CX7(4AapJ?CB9$is= z+jry7&j+Zpk|7&{2c?)b+-earZWtGH!RHfkgHy_+NMME}_aBOga>!&H;qrqYpzRhx z*OS{oHJ8h)X|g<#)j5rNMG^4DA&}bk+k*)$pPHrsYl0flj;mpL%CftOB=qbiz$9Ng zU~5tGHO0GA+p=yn7$zAJoix4r*jb6T`hdD2Y_vUxbd0?4#7_7h)ewS{-6m&v=gT9s zrCnOsIwJPnVo?7ZA+ z`#4??_9j>}BG?$gB$#>=IS%kz>-fd82G^x%epT#`Ko{rcYm zEB5uGiCO7g})s$0%&pz#}(klRp= zLbEP}+n6wbe+bE)cEPkNnv>wlX;ziRd}B_a$p;Hdnju3~NwBZt9v^(RQzFxj8ylekG7 z&ePdAg@=kdvENJE0l74lxO^0c3mU&2tF+#YFI#IkF6h;oS=Xm!k-zfseeU84%bhvu z&GjQ(>_ORB;boDdsACefNV9o?h~IhAi@?XXDc+f=QTHoH(Z+UWXHy|uQ=F<5nT^`J zd(tB7LFYh7Ri@EK;0@*K@%Nmz1;;sJ9wL}c2e_?thgj=A!7wKWZx^tJS~z+25_1`m ziC7W={y_gOIVrw*BW};BNkdM970pz3tz2SDTSWO3CMOSTI}c-jxQD10+n(pF>W3G< zk&*x3svww@3LDIQ#MdK4{Pj*3nUYh?zc$Gt)spm5#}!F-GBq~1WVprYTxl92&q_ACZl|+%20Q4JP zbgX;10097lhRN3y$c+^8SujQ=xeh1Sl;T%GkBaaG`$ErA=uSvk#58&_iDa56tnJt6n;!YpOVV^$$c6DK-SShgFiZ zOP2<6Cyw!=w3F5g?xxr5-I|*vFIq~fkCGil#>C3(3JuVn3)dEBN0PKmXD#k}JzPyu zK5##d#!=<+EpTiXpFA2jHSR#dyqOjzUQU3)D-v$FH%BR5u!j-n;ro_A6F~vZ*c^)vF5-pz*k$RJiuI<#=q{0)pJ(dVus1QBwc+y>W>Ny1C`2)Ph_FsB zu$Oq=@ThzceFz_s0LG4n^z8#&+cQLGpE3N-!^FeGqR>4~W4gi)sKmHJQP|R7@I!bJa_dj^ z%jpg(^v#A?bdSINE<_Z+ZH;=*d28;w?-Y(L+&w)x$u@N-Wq~A@;X`8S{FJ#hI^tMG zBfZf=c4#*`Dt?D^Z0B95fhJM;7rxu#%+iy`!ALGusl!`q=cDZSQjl5%h9}`}R@X6m z2XRGs%zy*ADXk-(ProK4H|n{?Sxp_w(c#WciA`G&`qL+ez^(f8pkFylk5fM_M5u8l z7}&u^ys1*i!#zx*;glS4L@7eEskj;>NSNSB$U-WA0=*%-G>s)?fZ!NgoJHbxRR1gL z{x#cP{s>JI32ePT*YiJX`y0qYvM!Kk{EqmF{s8T(CCvU?rOtnpUJ&U_DtS_91d=e> z*j?~z`lgvj*8OS%gVh8+_ZzF)lf7LT<#F3rxr=+2SXO;*0UQ(Ks5DP`KMq`DYnK>p zLm6$)WA%F#*4)rU_dEIwvp1B8x>AdTQ1gJumep5D@3Miq9JG#n)lgGgW7AJt2kC%2 zs~3H}r5KNv2b0l#-hYc@(?0I5LnPOBC;r@RWaZ}z&T<=Bq@~`h5OC>ndL}-Dg!&kv z=Fh|-gw^a(V=;|2+!muK3OW}F6L|}c*F1?RxrOT%6W+MHeb@~S*OYjuKVtV8`M7kS zYgrCu>s~g-3T%DYx04@fad|8)otpT}Y)3of9cOpZ+}o~{#=c_J->bBS!ZykhF6iQdo%g0Z<;r#9II(vSIm~%vLrY_>GR=lupnkQBUbumZ zcZ-yvt3Zs9j^Dg)w4xebSem>vu@40V?*M0lf4c)UFwa0)7tV1GTUoL?MX^I&O51K= z=Pxa#f2BSEZ_Y7U?#b-nxGDv`}&z&+*B%mArFK52#3TSya)eDGbsa45@tP4X(nbyFWEpqRL(@8YWuol z&(~J8kL!RfvZ`oF7le{o5U&}n9_QLzdHeN$QAI##7E1+FGfAnQw!)d76bwCsiDQ;> zw8WdE3N@+`8l0)7BJ04BO89ugqnm==QxjK?i>sSM?bWkrJvwHZKn7}KZ4vi_4mp!; zjYt`hq?zJncS?Z;iO(`m72IGC!(1cY zxeLoT>d@yY(^>>YxJugHa&6S#Zw#j+r5ulqwxR@0t zyHf`TY+K!ijTm&Pr~~l0@{1qepDl-E2~8T*cxRyowX@Bb<}+U@YqNlSyv@5Q*uJ6;oZ2#*P{mfr zocL>8(X}DouftoBRa-`Bkk62u1bX^+%r1QUQ#aq$iCV6?i^{mqQLNnHutWz9tSF;T zHkb=RS+`+&KGtQKZ_?+M^*Rc3ku7sGW@HAjD)K{E3p4pOGFS|CNw{#rU34!wze^NK zF!waVXK3sOH1Vp@Nba@J`;M9$awNjKuC!cAMSH~EH9boR z**rHF{th72aDIFL0)SbrPsyGG8L14Wgy+}CUiWOt;LVqte`saD|B@asr_l;%|Hy%m zsulncAD3V$XuCMw`7^KLFRJiAuKy0zFjiDBN=!5>=b%awas4!h)=#liQdybB^J<$} zfc)$Gm&219&8Lpm=~kf!t~HuGZ9PEH+37dG)%&))aGtxiXP+zdhd)4oK@L-jwIwO_ zVZ>$;Q#iM9xumakelAhO%SsZTEQ~Bc>Te(q7WzERmg_yh5}f-zKJ1a*KHEK0mL9O) zwO%SV*0Sy)(T3$N?~e4YN&4=#pziTb*2IB*wozYq5yn3gR=a!amWfl(uzh2ESvbgT z&rXgSkacRYDmdkbD2P|$&OfOdr7>mF^q^L9-uIe=!GzB)89k+%qwfqN@0u!9I=7F@ z9a~)!EgHk|_MN&YwXeEu?!3f+{7D4kJ1*;zN*6EZC16~e0^q*C#WVhg{+u!IKLn)l znR_!?U2heqNof8V5dA&&{eE&xIV9WB=2j2#x5wWyMSBbv-?Q0w{P1h4z2!DBCc#9|wv^Gkxrm$&HZ%P< z2`&<5Y_hJzoXVB;@UCl!{e>FOB1NxgM=#)hRm(Jd&6Q>#;e;8G5zAU1HubGn_zaGX zj}KnZgt+URMe1v3r*yW*mi)VpL}~_t{^ro_mtP2kw~%Phl0u)Ee3X0TEs9^(H>c@- zPYAzP8F|VOz$8%N>pP3~y(i#(Skf%(L&h2Df;rB=SRYU)B}bLhsAYPcQ;H`v^0W%|TnH>AU0R z9S)}Bh}07`I9UnYnUci5hkj}4Ps8W8o^E3}RZ<1Y25tqYJSu_Dv=cQgKXs*_O}Y1k zjUqmVxSY;MtvSNG0J`yhhOy`R!skG`%NNs5epn(~k~4mX0nf&bvR&sR38T5|Bq|e~ zuZ5<*CqT#t^T!Ib{6${N>T<}kOjE8p+iIxLgu92`Ft@kGzGYkV&eT@#+$2~~92rOB z$h;PR#zNJ7{`@;f@C6e<)jx?Yv=+=(Et4VGWExBUgt__3WE39v2Z;ICV@?u#{Zo8% z>-DoKu=Id!R%%A%FzOoTN10HBr~cwWFXl%oJCRXP!LU>MO@&C$tWIysdealLAVmqI zzSQ=P)zJ-_l->sg5v1|9JGXVlA-?j_fI_hydP8nxXUO|o4cH)+MYnD7V(en=7MG)6 z%-3(-S^UtKEG?g3bGOZB&lxYf`afNuAqer`^yo};22T-+-ny|#F!`!gQK&$gZ_IcF zGbPct7-84ngj!_ZQTBZLoYJW5#3*3;<2zY&J1pt+yt3>GVvVB`c=D_D=rW2|7P_{c zPn$!E{6cJezpiKfqOZWAZ2r}9^|80ssWsb(o`t~z%MscGW#pVS%L8;yWrF}Z-V;2o8`*$)_- zXtdsS;TntLgzw)NyfT?(wtL25|7kd0KO(iUXxLKvwtKHACp_mrn%sYGc>k;W|7%08 zZbKbMpOX=`QxATIfBoPmfwr%f0@d}s{4pbn8wAe%pW!tk8jbY};rs>bmZSN%9R+6i zqGX94pG4GklLLn}Xk{i9ofLkJ5Nh3jQPzRcGpI{u)`JN$!$s`iC{z*X5qu~$L4ZJ&??kH(p6WaDA^v()Rdl;hP0=^-~`Xm;KO zv)RInlAzWPRNMVj)E`kmGQHMKBkoj}xTm^1t^s1{iG!iECyUX(>ks*jm*EF()hy+v zrjwatN2VmvOWlTpV=HRp5%kZec7-spR+v5yR8`Q|W46cn(BWf(>)uz21)drq$(PcJN;IQI1$w<{W_m2W6kWDoaN0p@ z{t6BOI*K{V0zX>n0!Ih7tGG>L>wD2hCP64(YBx883DuH`hq>J`yC3DuVVq|1Z8_B7 z7GOV+sRr7F*}~x&=m^#?LkAQSW~y6-it3 zK|PP0DRQ{e6xT!VYQxm+DA*GwZu&8GTx(brXp%Yl9tp#HSOyZDb2HYv$-6tB2~2*N(JT~Bw(&bJ-`~dQpf0QEz^+nL8Mf!}ho^qf zOdr}LV*CSyVfkx0KctE!$c8U3On%WQXsZD6&{Z${=YwH>F_`Sv!&L=m+uzgN- z5@dDg%vBm#w*Lcj1)jIBvpF|oEX;yON3Ylkg3!UW!4~JnQ75o$#s2CE{-@Xf1HHq@ zjPs$8)PUSBIqJF^ul{0m&}lpM`Mx%@QNeQWtcuNr0uK{dBn?IdkD8+M_&}zYT|wxj z!8vyfOC>Q|4}&DOEPkC|IJa`2ucGkuyF;027F*ML++WUbsjY1Lf3S6;&NHkSqPT&H z?KuwHg@cdzS3g8)Cmr4HGcTMn+fu(%s#4kV=NN#kl+8b9K&WVDVm>jJ`3SgHWvE>K zMpV1OY1Be8#}7D`KTrWQV|06o%*8lLO#gxIB~oq7IRh0>@aK2hly8f)=wjJn|ISm( z`XO_6{Q<%iF6wSLTMgX|Ic{6WJv(yjvNw_*$!Un&(oiQxQ#er+h4(gZLBhzP#tc?N z0wIs1TVLo_BB2vorUp0j_vR!a4ere{6MvC;zL}e@ezz7ay&pK&<7|L)#=H`_p60`D zhMH5^5mtR!IauzlVVg=)zFHO zQ7wZQK?%Ke9A=mDw!rqY0!%F9zpdQQ9f0zhF{5=DV^RCV&;46Kc^wu*Hkd>O69Th# zfln$XbjJmm!C;cB`?Vlt{zk%bFzUKwGmGwI%>)GvU3Mt_6(kC*(o1Cwqo{R$gq+RE(@kezPi%%!djT&rpyy^7dE`Lqmv7uvOJRWl#E-cacn zA~SYUiUxzER2=^43F@bDnBQMvHJ28KN$ts%(R=(}X)|YVB@smZXDCDa1wyGNJAV0g zj9L?AA`*W(T0*~EnZe@znLgKJtk81Wa8SP@jo}c>3(PTmL2f5$tC~s4)lg2lE6bf& z-7vda##q9gU-p6nU-o#{)zAQS!I-1WTm?Va_E|2uEMnEXQ5Dr416e&IG zxz=`X_I2~^4fM57?$N|&Z6JaiDWs#1fZzz;+REyitb)R+lgeKUP_H+8ZINhM{e;kJ zvnrz-n(B!`-W2UK>+smV{=Ml3#P;A;6J9NR5OEtSZhM&+jFT5#3~PM?=Qgl8nQvV9 zr4rM?En3Ekq=MQBw^zokKf8Z7;v1avt`LKMY7+}qJXz!DI0|6I!0GQOwth9Ed(&)= zxzvLIWhI)1zr4)um4V;>eKwI9V72nvgVn<)3<0J zczN@GFFOCFrnYm6bWhBcbDk^PZks+VPiDyD&U=^2cL@yWFkIn_%2%-uG(8=4({RvE zXlRg5mg~5UPN&ckLW>T6EOxjuUDsEr@Fq&1;J=5J7SCd|CxS;J!XM=wBcCATt_dql z(~7Qdi_{S0U!pp86YxDH0vSk3(b7%=W(=+PJ2hkz!ciAw)DThGkOx!VemE=165tKDh4`)%}16J(Or&TMKc#Cc_7)?%MP8$OYbwm0QJq^Fxe>MM?UKIw1a_3`2p zRJ;~T)0oI~6ULdLw!)zT>FM~FG_=E1=sxrnz<6lIt2gghC24(RMQBRIJ{gl$NnG zFC*gy>8L>1yW4$*v{6nPbl;Gy@~!rrSyVcI+QQ4cXCMX+mKsxF*#5822l#XK{@W(> z>Qxg{kZPp-@^nkHtCJotw0DPL-~ip&2Khljs}R#b-x@O`P?Bw_^s@>(;;?udTtvWO zNAGB(T~ar9dEtNs5p#Oa!oGGq%iZO8lyz5iqMES&_p_~6W>i*_=3j^P^D+?$;9!re z+{lc#<7a2tyFa!xM)V(juld0gJ{&~N4@X8%uthw5zRAphzoRdyQQHWmu?RI0?uDgA z$%S(x1J+*m?S&j2fC5&{taqT@Bsz*Sg|pR5QF-TAT~{D?hs0g2Z zx**~k^%!+h(A>{PPhH>$8*in*bL&o={mIr`MSl@_Fz>?G@scB`NBa!*sWp4UN2c72 z-GiSy1?6)wzWuESF~sw_B$F)bU45J(DRM*DM@ICt8v#ebx|NRa*slcL;P-V}v8;4^ zbs92Ah^;iJc9~=0Zol!2%ArIMzm7@Qz-~xajZ>W5F~HT#xzAXJ>e@o$)Y`bga1N5O z^4uGO1A;Z`%8BNmh|$}x%Q`la#VE4L4&X}m85#x50z|ta9`mA<&_AdjI~@%(=VlUx zRS*mWfS2-8KwBY8_4tRe@YVyaOueU4c`+u;uc2yLjvf|n ze2W?LdM5Z=P7yIq-R%1@2x#R5&fQH4h#(Je6S>RamRp%X!Ot~l`ihoy)nIrfdF*;b zAQLORU$XB9TOv0XA$AFh44lkToRxfPET8>4k?K1DpXbZ@WMfMEZCp`#&ZE0Aa9O#wUG!u90Ad zJ>=njFkIY_`f6w(sQxiGW-{)Q)M=pw5ucxte(-z0@bsvTzV05sYhj5Ep3|aMSTbEx zqSVDtg>lIZ1+B|~St^{$eCs;?x)jpfLVYQr_;77A)qsFmzJ193zFe9vPcc@8u&vhV zAz(#sl5o=0IUZsku{H|n>9{b^qE0C2V_KtKR!F9;G)Nwn(r@U8Rhrbo5;Lt$NwDv6 z9410yM;K`5<$A|o!kETi@Qr!mKp=yAXGve`)3cfanvQzJT4EnjY2Ls5pZ>Q-#(x?m z(d-O8M4;sxa)_0Qx&)io7qHK-dzyS zGe*Vk*_#>uVG-*_8$7_5F-C2#uf(*jtUJ3_|IW7lN_6WK{ z(SAiv$E$iA6XPbud{YUirizRca4d{=ip%&$iX+EW(vY6(Vym0%x`K3e8e->ypeg%f z`tu(ku7;KfJ^VT8u&vZ5_-{;}kf`-&JY@ z%82}^2kt&(d!^+5)N@(X@LW*w@zM;Z$q+UmcQ3FC0vjzrS+dZRCyxSnVb{>S1Kq-Y<6vZ=FY$R{1_hBrcA)@RdNVv#zr zF*alc9x|c^=kDm)!rK^y+M=s!f!EV+Lu62H4&8q;X~QB7wamvKEAg|h`Sa0kEvQFsju1C4P3jF`pviS=b@HI# z%tLLp(l`0~1l5amP=!~#5aiDrvYb3RXQYT$McB;bbF&H<2@TO#4Hp+aLy$S;On#q7 z_d$$x2uc74(oU`NGded*| zOU5n^6lqb%^k*mh4%i{T#xEr_$a1qQU2HCSL9LodF%*?CUUEW?OJzfxrBexvDk6W4 zDsNK=V17G9IWi^a53$aEsYjKtg1t%B{s3Xm9k~A7^E68>3!qBvrQ1wVEBd(TzqTvr z3*Q2^B13z8TVUm&RS{$3h=F0z5N4o3+wjTdmkCKpVo;u3kx%v?ASlhdpj|P0DIj#;@2tQ=`mY-KV>7l*+!&s!3|Td9HB0P2`WTBYyd|A}2uG zsJ<>1V;lctUZ6tWNAIr6nPREj8s%msE1hm7S)>udkx%c7ThgqCOhNjgaKxmwi^@8$ zcG(2o;>2#;ozwcep;da#u|tT+VKIv*tq#FLkT=i9 zRHESg5J1Euy13aP5^21+cv#BV{NjLaLa2jX1i<$NP8>N1oBDsF!R6s(@pNaGuOKz;+WY*ArjVJ3E^$cV>Fga3y7)oqK zxP3s6PHNxIMk|@i%I$>8Fr@XrL0e?<-Md~0gR4tUIs}iYEn*d94vl_t7SYIjoMbXd zKvSPshEY90z{!4}#D>PBA?VcpjCnqxSTt6#q9e{RTu!u`QS52XSa9mP(a}D?%+;aIN#;)B7qEH#10yqZVJ$Lw!0sZt zy8{dDqx#s#%?sw9+;#53f)UZ4Maus)LPnf03xTx?SDo9P8ug z5Y^kL0|;9G`@*ZeL`|;yCVP;4Lv`f8Y=w8e^0Oh0J-s~&tghC zXz>3m9(e4U@sdS5_rE<`{IAve{vV$0pgkJ@?p(Kj5e)479yne9Pp=Q5Qdyxt-s{N{ z->g)R#nYZi>ZW)XDh7A@J@V;&wbUu)_lDK>cpIoE_=8ArHwtyo&vYWiJT<5UrOwWxWinX{r{T)-d$09 literal 0 HcmV?d00001 diff --git a/examples/SX128x_examples/ESP32/ReadMe.md b/examples/SX128x_examples/ESP32/ReadMe.md new file mode 100644 index 0000000..f33e6f5 --- /dev/null +++ b/examples/SX128x_examples/ESP32/ReadMe.md @@ -0,0 +1,111 @@ +## ESP32 - SX12XX Library Example Programs + +Having originally tested the SX127X part of the library on a ATMega328P based micro controller I decided to check that the library was code compatible with another popular processor that can be programmed in the Arduino IDE, the ESP32. + +Unfortunately there is frequently a misunderstanding about what an 'ESP32' is. The ESP32 is a single surface mountable module to which you need to add various components to make a usable board. However the many different types of 'ESP32' boards may use different types of components and connections which could conflict with the pins used in the library examples here. It is not practical to test the examples on all possible permutations of 'ESP32' assembled boards, there are over 60 different boards supported by the Arduino IDE and I would go poor buying them all. + +Thus the ESP32 examples presented here have been tested against a known standard which is an actual ESP32 Wroom module with no additional hardware, apart from the components shown in the schematic below. It is possible that the examples will not work against your particular 'ESP32' assembled board. If the example programs do not work for you, it is not an issue with the library but a difference between the reference hardware and your particular setup. I am not in a position to assist in resolving issues with particular ESP32 board versions. + +![Picture 1](/pictures/ESP32_Bare_Bones_Schematic.jpg) + + +As well as testing against the bare bones ESP32 schematic shown above, some of the examples will have been tested on a small portable unit I built which follows the same principles of the bare bones schematic. This PCB was developed to create a PCB that could be used for a small ESP32 based GPS tracker node, initially for use on The Things Network. The board has a GPS, I2C SSD1306 OLED and RFM98 lora device. There are options for a micro SD card, DS18B20 temperature sensor and a I2C FRAM. With it all assembled, the node consumes around 31uA in deep sleep with all devices connected. The 'Micro\_Node' contains additional circuitry to power off devices such as the lora module and GPS. Its highly unlikely that a standard ESP32 board will achieve a sleep current anywhere near the 'Micro_Node', this unit is pictured below; + +![Picture 1](/pictures/ESP32_Micro_Node.jpg) + + +### ESP32 Deep Sleep + +The ESP32 does some things with I\O pins when going into deep sleep that you might not expect. There are functions in this SX12XX library for putting the LoRa device into deep sleep, whilst still preserving register settings. The current then taken by the lora device is circa 0.5uA. However these functions will likely not work directly with most ESP32 boards due to the way the ESP32 handles the I\O pins in deep sleep mode. + +When designing a board where a very low deep sleep current it is important, you need to build the design in stages, checking the deep sleep current after adding each component. Debugging a high deep sleep current on a fully assembled board can be from extremely difficult to impossible. Achieving a low deep sleep current for particular ESP32 boards is well outside the scope of this SX12xx library. + +
+ +**3\_LoRa\_Transmitter and 4\_LoRa\_Receiver example programs** + +These example programs work when used with the ESP32 'bare bones' schematic shown earlier and the 'ESP32 Dev Module' board type selected in the Arduino IDE. + +The pins used for SPI were; + + SCK 18 + MISO 19 + MOSI 23 + NSS 5 + NRESET 27 + RFBUSY 25 (SX126X and SX128X devices only) + DIOX 35 (DIOX is DIO0 on the SX127X devices and DIO1 on SX126X and SX128X devices) + +In the example programs for ESP32, you can start the SPI with; + +SPI.begin(); + +And the default SPI pin outs for the ESP32 module shown above will be used. + +Alternatively you can uses this format to start SPI; + +SPI.begin(SCK, MISO, MOSI, NSS); + +And the pin definitions will be taken from those specified in the 'Settings.h' file. If you change these pin allocations from the defaults given you will need to be sure they are valid for your particular ESP32 board. + + +The SX12XX example programs are specifically written for and and tested on ATmega processors such as 328,1284 and 2560. However most will work, with minor modification, on the ESP32. + +This is a run through of the changes that were needed to have the tracker receiver examples; **25\_GPS\_Tracker\_Receiver\_With\_Display\_and\_GPS**, written for the ATmega328P run on the ESP32 bare bones type boards described above. + +The original SX12XX tracker program uses the SPI interface to talk to the lora device, I2C to talk to the OLED display and software serial to talk to the GPS. + +The pins used for SPI were; + + SCK 18 + MISO 19 + MOSI 23 + NSS 5 + NRESET 27 + RFBUSY 25 (SX126X and SX128X devices only) + DIOX 35 (DIOX is DIO0 on the SX127X devices and DIO1 on SX126X and SX128X devices) + +The ESP32 I2C pin connections were; + + SDA 21 + SCL 22 + +There is no need for software serial on the ESP32 as it has an available hardware serial port. These are the pin connections used; + + GPSTX 16 //this is data out from the GPS into the ESP32 + GPSRX 17 //this is data out from the ESP32 into the GPS + +The other pins used were; + + LED1 2 //On board indicator LED, logic high for on + GPSPOWER 26 //Pin that controls power to GPS, set to -1 if not used + +The only software changes required were to change the lines in the Settings.h file from; + + #define USE_SOFTSERIAL_GPS + #define HardwareSerialPort Serial2 + +to; + + //#define USE_SOFTSERIAL_GPS + #define HardwareSerialPort Serial2 + +This removes the definition USE\_SOFTSERIAL\_GPS from the sketch and the effect of this change is then to remove these two lines from the Sketch; + + #include + SoftwareSerial GPSserial(RXpin, Txpin); + +And include this one; + + #define GPSserial HardwareSerialPort + +Which sets the commands to read data from the GPS such as GPSserial.read() to be in effect Serial2.read(), which is the ESP32 hardware serial port used. + + +**Note:** The provided lora settings (in the Settings.h file) may not be optimised for long distance. See the 'What is LoRa' document for information on how LoRa settings affect range. + + +### Stuart Robinson + +### April 2020 + diff --git a/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TX/8_LoRa_LowMemory_TX.ino b/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TX/8_LoRa_LowMemory_TX.ino new file mode 100644 index 0000000..f43b22c --- /dev/null +++ b/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TX/8_LoRa_LowMemory_TX.ino @@ -0,0 +1,153 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 08/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program transmits a packet without using a processor buffer, the LoRa device + internal buffer is filled direct with variables. The program is a simulation of the type of packet + that might be sent from a GPS tracker. Note that in this example a buffer of text is part of the + transmitted packet, this does need a processor buffer which is used to fill the LoRa device internal + buffer, if you don't need to transmit text then the uint8_t trackerID[] = "Tracker1"; definition + can be ommited. + + The matching receiving program '9_LoRa_LowMemory_RX' can be used to receive and display the packet, + though the program '15_LoRa_RX_Structure' should receive it as well, since the packet contents are + the same. + + The contents of the packet received, and printed to serial monitor, should be; + + "tracker1" (buffer) - trackerID + 1+ (uint32_t) - packet count + 51.23456 (float) - latitude + -3.12345 (float) - longitude + 199 (uint16_t) - altitude + 8 (uint8_t) - number of satellites + 3999 (uint16_t) - battery voltage + -9 (int8_t) - temperature + + Serial monitor baud rate is set at 9600. + +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint32_t TXpacketCount = 0; +uint8_t TXPacketL; +uint32_t startmS, endmS; + +void loop() +{ + TXpacketCount++; + + if (Send_Test_Packet()) + { + Serial.print(TXpacketCount); + Serial.print(F(" ")); + Serial.print(TXPacketL); + Serial.print(F(" Bytes Sent")); + Serial.print(F(" ")); + Serial.print(endmS - startmS); + Serial.print(F("mS")); + } + else + { + Serial.print(F("Send Error - IRQreg,")); + Serial.print(LT.readIrqStatus(), HEX); + } + + Serial.println(); + delay(packet_delay); +} + + +uint8_t Send_Test_Packet() +{ + //The SX12XX buffer is filled with variables of a known type and order. Make sure the receiver + //uses the same variable type and order to read variables out of the receive buffer. + + float latitude, longitude; + uint16_t altitude, voltage; + uint8_t satellites; + int16_t temperature; + uint8_t len; + + //test data + uint8_t trackerID[] = "tracker1"; + latitude = 51.23456; + longitude = -3.12345; + altitude = 199; + satellites = 9; + voltage = 3999; + temperature = -9; + + LT.startWriteSXBuffer(0); //start the write at location 0 + LT.writeBuffer(trackerID, sizeof(trackerID)); //= 13 bytes (12 characters plus null (0) at end) + LT.writeUint32(TXpacketCount); //+4 = 17 bytes + LT.writeFloat(latitude); //+4 = 21 bytes + LT.writeFloat(longitude); //+4 = 25 bytes + LT.writeUint16(altitude); //+2 = 27 bytes + LT.writeUint8(satellites); //+1 = 28 bytes + LT.writeUint16(voltage); //+2 = 30 bytes + LT.writeInt8(temperature); //+1 = 31 bytes total to send + len = LT.endWriteSXBuffer(); + + digitalWrite(LED1, HIGH); + startmS = millis(); + + TXPacketL = LT.transmitSXBuffer(0, len, 5000, TXpower, WAIT_TX); //set a TX timeout of 5000mS + + endmS = millis(); + + digitalWrite(LED1, LOW); + + return TXPacketL; +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(9600); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TX/Settings.h b/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TX/Settings.h new file mode 100644 index 0000000..78b35aa --- /dev/null +++ b/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TX/Settings.h @@ -0,0 +1,30 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +#define TXpower 10 //power for transmissions in dBm + +#define packet_delay 1000 //mS delay between packets diff --git a/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TXIRQ/8_LoRa_LowMemory_TXIRQ.ino b/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TXIRQ/8_LoRa_LowMemory_TXIRQ.ino new file mode 100644 index 0000000..45bb01f --- /dev/null +++ b/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TXIRQ/8_LoRa_LowMemory_TXIRQ.ino @@ -0,0 +1,106 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program transmits a packet without using a processor buffer, the LoRa device + internal buffer is filled direct with variables. The program is a simulation of the type of packet + that might be sent from a GPS tracker. Note that in this example a buffer of text is part of the + transmitted packet. + + The matching receiving program '9_LoRa_LowMemory_RXIRQ' can be used to receive and display the packet + + The contents of the packet received, and printed to serial monitor, should be; + + TR1 (buffer) - trackerID + 51.23456 (float) - latitude + -3.12345 (float) - longitude + 199 (uint16_t) - altitude + 8 (uint8_t) - number of satellites + 3999 (uint16_t) - battery voltage + -9 (int8_t) - temperature + + Memory use on an Arduino Pro Mini; + Sketch uses 4958 bytes (15%) of program storage space. + Global variables use 224 bytes (10%) of dynamic memory, leaving 1824 bytes for local variables. + + This is a version of example 8_LoRa_LowMemory_TX.ino that does not require the use of the DIO1 pin to + check for transmit done. In addition no NRESET pin is needed either, so its a program for use with a + minimum pin count Arduino. Leave the DIO1 and NRESET pins on the LoRa device not connected. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include +#include "Settings.h" + +SX128XLT LoRa; + + +void loop() +{ + //The SX12XX buffer is filled with variables of a known type and order. Make sure the receiver + //uses the same variable type and order to read variables out of the receive buffer. + + char trackerID[] = "TR1"; + float latitude = 51.23456; + float longitude = -3.12345; + uint16_t altitude = 199; + uint8_t satellites = 8; + uint16_t voltage = 3999; + int16_t temperature = 9; + uint8_t TXPacketL = 0; + uint8_t BytesSent = 0; + + LoRa.startWriteSXBuffer(0); //start the write at SX12XX internal buffer location 0 + LoRa.writeBufferChar(trackerID, sizeof(trackerID)); //+4 bytes (3 characters plus null (0) at end) + LoRa.writeFloat(latitude); //+4 = 8 bytes + LoRa.writeFloat(longitude); //+4 = 12 bytes + LoRa.writeUint16(altitude); //+2 = 14 bytes + LoRa.writeUint8(satellites); //+1 = 15 bytes + LoRa.writeUint16(voltage); //+2 = 17 bytes + LoRa.writeInt8(temperature); //+1 = 18 bytes total to send + TXPacketL = LoRa.endWriteSXBuffer(); //closes packet write and returns the length of the packet to send + + BytesSent = LoRa.transmitSXBufferIRQ(0, TXPacketL, 5000, TXpower, WAIT_TX); //set a TX timeout of 5000mS + + if (BytesSent == 0) //if bytessent is 0, there has been a error + { + Serial.print(F("Send Error")); + } + else + { + Serial.print(BytesSent); + Serial.print(F(" Bytes Sent")); + } + + Serial.println(); + delay(packet_delay); +} + + +void setup() +{ + Serial.begin(9600); + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.flush(); +} diff --git a/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TXIRQ/Settings.h b/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TXIRQ/Settings.h new file mode 100644 index 0000000..873a464 --- /dev/null +++ b/examples/SX128x_examples/LowMemory/8_LoRa_LowMemory_TXIRQ/Settings.h @@ -0,0 +1,28 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes + +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //LoRa TX power in dBm +const uint16_t packet_delay = 1000; //mS delay between packets diff --git a/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RX/9_LoRa_LowMemory_RX.ino b/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RX/9_LoRa_LowMemory_RX.ino new file mode 100644 index 0000000..6160bbe --- /dev/null +++ b/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RX/9_LoRa_LowMemory_RX.ino @@ -0,0 +1,192 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 08/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program receives a packet without using a processor buffer, the LoRa device + internal buffer is read direct and copied to variables. The program is a simulation of the type of packet + that might be received from a GPS tracker. Note that in this example a buffer of text is part of the + received packet, this does need a processor buffer which is filled with data from the LoRa device internal + buffer, if you don't need to send and receive text then the uint8_t receivebuffer[32]; definition can be + ommited. + + The contents of the packet received, and printed to serial monitor, should be; + + "Tracker1" (buffer) - trackerID + 1+ (uint32_t) - packet count + 51.23456 (float) - latitude + -3.12345 (float) - longitude + 199 (uint16_t) - altitude + 8 (uint8_t) - number of satellites + 3999 (uint16_t) - battery voltage + -9 (int8_t) - temperature + + Serial monitor baud rate is set at 9600. + +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint32_t RXpacketCount; +uint16_t errors; + +uint8_t RXPacketL; //length of received packet +int16_t PacketRSSI; //RSSI of received packet +int8_t PacketSNR; //signal to noise ratio of received packet + + +void loop() +{ + + RXPacketL = LT.receiveSXBuffer(0, 0, WAIT_RX); //returns 0 if packet error of some sort, no timeout + + digitalWrite(LED1, HIGH); //something has happened + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + Serial.println(); +} + + +uint8_t packet_is_OK() +{ + float latitude, longitude; + uint16_t altitude, voltage; + uint8_t satellites; + int8_t temperature; + uint32_t txcount; + + char receivebuffer[16]; //create receive buffer, make sure this is big enough for buffer sent !!! + + //packet has been received, now read from the SX12xx Buffer using the same variable type and + //order as the transmit side used. + + RXpacketCount++; + Serial.print(RXpacketCount); + Serial.print(F(" ")); + + LT.startReadSXBuffer(0); //start buffer read at location 0 + LT.readBufferChar(receivebuffer); //read in the character buffer + txcount = LT.readUint32(); //read in the TXCount + latitude = LT.readFloat(); //read in the latitude + longitude = LT.readFloat(); //read in the longitude + altitude = LT.readUint16(); //read in the altitude + satellites = LT.readUint8(); //read in the number of satellites + voltage = LT.readUint16(); //read in the voltage + temperature = LT.readInt8(); //read in the temperature + RXPacketL = LT.endReadSXBuffer(); + + Serial.print((char*)receivebuffer); //print the received buffer, cast to char needed + Serial.print(F(",")); + Serial.print(txcount); + Serial.print(F(",")); + Serial.print(latitude, 5); + Serial.print(F(",")); + Serial.print(longitude, 5); + Serial.print(F(",")); + Serial.print(altitude); + Serial.print(F("m,")); + Serial.print(satellites); + Serial.print(F("sats,")); + Serial.print(voltage); + Serial.print(F("mV,")); + Serial.print(temperature); + Serial.print(F("c ")); + Serial.print(F(" RSSI")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB")); + return RXPacketL; +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F("RXTimeout")); + } + else + { + errors++; + Serial.print(F("PacketError")); + printpacketDetails(); + Serial.print(F("IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + + +void printpacketDetails() +{ + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(9600); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RX/Settings.h b/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RX/Settings.h new file mode 100644 index 0000000..9ebe410 --- /dev/null +++ b/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RX/Settings.h @@ -0,0 +1,32 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +#define TXpower 10 //power for transmissions in dBm + +#define packet_delay 1000 //mS delay between packets + +#define RXBUFFER_SIZE 255 //RX buffer size, not used in this program diff --git a/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RXIRQ/9_LoRa_LowMemory_RXIRQ.ino b/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RXIRQ/9_LoRa_LowMemory_RXIRQ.ino new file mode 100644 index 0000000..51670ef --- /dev/null +++ b/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RXIRQ/9_LoRa_LowMemory_RXIRQ.ino @@ -0,0 +1,169 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program receives a packet without using a memory buffer, the LoRa device + internal buffer is read direct for variables. The program is a simulation of the type of packet + that might be received from a GPS tracker. Note that in this example a buffer of text is part of the + received packet. + + The matching transmitter program '8_LoRa_LowMemory_TXIRQ' is used to transmit the packet. + + The contents of the packet received, and printed to serial monitor, should be; + + TR1 (buffer) - trackerID + 51.23456 (float) - latitude + -3.12345 (float) - longitude + 199 (uint16_t) - altitude + 8 (uint8_t) - number of satellites + 3999 (uint16_t) - battery voltage + -9 (int8_t) - temperature + + + + Memory use on an Arduino Pro Mini; + + Sketch uses 6290 bytes (19%) of program storage space. + Global variables use 237 bytes (11%) of dynamic memory, leaving 1811 bytes for local variables. + + This is a version of example 9_LoRa_LowMemory_RX.ino that does not require the use of the DIO1 pin to + check for receive done. In addition no NRESET pin is needed either, so its a program for use with a + minimum pin count Arduino. Leave the DIO1 and NRESET pins on the LoRa device not connected. + + + Serial monitor baud rate is set at 9600. + +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LoRa; + + +void loop() +{ + uint8_t RXPacketL; + + RXPacketL = LoRa.receiveSXBufferIRQ(0, 0, WAIT_RX); //returns 0 if packet error of some sort, no timeout + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + Serial.println(); +} + + +uint8_t packet_is_OK() +{ + char receivebuffer[4]; //create receive buffer, make sure this is big enough for buffer sent !!! + float latitude; + float longitude; + uint16_t altitude; + uint8_t satellites; + uint16_t voltage; + int8_t temperature; + uint8_t RXPacketL; + static uint8_t RXpacketCount; + + //packet has been received, now read from the SX12xx Buffer using the same variable type and + //order as the transmit side used. + + RXpacketCount++; + Serial.print(RXpacketCount); + Serial.print(F(" ")); + + LoRa.startReadSXBuffer(0); //start buffer read at location 0 + LoRa.readBufferChar(receivebuffer); //read in the character buffer + latitude = LoRa.readFloat(); //read in the latitude + longitude = LoRa.readFloat(); //read in the longitude + altitude = LoRa.readUint16(); //read in the altitude + satellites = LoRa.readUint8(); //read in the number of satellites + voltage = LoRa.readUint16(); //read in the voltage + temperature = LoRa.readInt8(); //read in the temperature + RXPacketL = LoRa.endReadSXBuffer(); //finish packet read, get received packet length + + Serial.print(receivebuffer); //print the received character buffer + Serial.print(F(",")); + Serial.print(latitude, 5); + Serial.print(F(",")); + Serial.print(longitude, 5); + Serial.print(F(",")); + Serial.print(altitude); + Serial.print(F("m,")); + Serial.print(satellites); + Serial.print(F("sats,")); + Serial.print(voltage); + Serial.print(F("mV,")); + Serial.print(temperature); + Serial.print(F("c ")); + printpacketDetails(); + return RXPacketL; +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LoRa.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F("RXTimeout")); + } + else + { + Serial.print(F("PacketError")); + printpacketDetails(); + Serial.print(F("IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + + +void printpacketDetails() +{ + int16_t PacketRSSI; //RSSI of received packet + int8_t PacketSNR; //signal to noise ratio of received packet + + PacketRSSI = LoRa.readPacketRSSI(); + PacketSNR = LoRa.readPacketSNR(); + + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB")); +} + + +void setup() +{ + Serial.begin(9600); + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("Device OK")); + } + else + { + Serial.println(F("Device error")); + while (1); + } + + Serial.flush(); + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); +} diff --git a/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RXIRQ/Settings.h b/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RXIRQ/Settings.h new file mode 100644 index 0000000..1644f81 --- /dev/null +++ b/examples/SX128x_examples/LowMemory/9_LoRa_LowMemory_RXIRQ/Settings.h @@ -0,0 +1,27 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes + +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //LoRa TX power in dBm +const uint16_t packet_delay = 1000; //mS delay between packets diff --git a/examples/SX128x_examples/Pictures/Calibration_Values.jpg b/examples/SX128x_examples/Pictures/Calibration_Values.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ca0893a6d42587e2cabfa50790959b7ed2890229 GIT binary patch literal 20046 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

JH(CgxD^RlGlI!$Lu^?=1 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�W_e`8zg_;b0252p9w4X(Ok|xiL zJf4eO984T`hJe6FvL<~-2phC^Q_bcmt|=WK#3;jnnYh^kLK$JI9E^O(XKS}QfmW>= zNV6@?ABx{od=P)MrOzyr{cPt+J+m5Q=1g|@o#`-a%mRlcB!?~+ij05}OqHx4l7(}7 zDCVR3LNl@DF&P`#QZ>iOZovdDmmH0o6Xi)hUY7=7aG`6WSk^igQBC|#XOH8}Fl+wm zw&IAP#G_r7nx>dA8c1&*>b*X3AhC|Jxo0h_70-xHRNLfmY2jA&Y~Kiw6KM~HjOV=a zRckrXP7M*cb}E8MN@uXMOQf^edtJ-%T+lx2TUGNz>3C0cj&b{gn?dR_TZ*PUQF$#7 zjx@!xozJ9p6MV=9`Jc*ffXj10^P*(Cz^7XjlBojVl3@ar8kCAWhfUiv?$JB%Tx!nsK7EX$8O& z^WiZtw>WSIABxL%+JVn*>LF~>cThk^FvhDR`L4GNY|7IE$0RU3ADwc!78|G&K=JCj z{{T&sCYu|5+UEYGE>9&~8WvCD!I?Q(Sd$ks8cA5$a7zQO+LT8XyAAM0EjJg7k}vJtdF`o9^5KqpD6vWQ2dwztv+X}BQ_g*K2E^ecbhFIK}mY3aANxJ$h;%3E}7m1Gj?00S}x5%zXj#bLE zB|5QBQb^`x>GbChIXpVOTluN8rj8tE7}*?|TS06@jmMr(^ZbxqSN_)rX>Q=-K;OH2 zQj<@^eD04UHLduD3J-6$HOJytp%|rxGHkE;PzHx9bUJgHbrb}U~tf0ADz%2P@n(gfRjLjI+S zDX|@mj2irgJUGV30N^O|Ufhq(L#boUCPtkxW85L-&L*}FET73S)3Aw@WaWmOvMKLx zXM28tENi(B#+w!|5u!QCW7u1J`wAR<(Zw&4#~sz0FVZHHJ4}il&d7rH-`*BSYe=-{ zW%$ii;v?ZsTw`!>r;BwT)Fv*_@TRvqE=aEDwcUPbO&2OgXJ_LVHQ4YPjk`cB0G==C z0yiEq`&Rz|s1sY_Dbs&xY53weuo~|vH77Ld`li@ggu-VHQyv_6PSD4XR}skWercu# zY#NL{E1ipe@$sb(@g{GSv{?LkL}o!2RymG#2e9)_94>54 z;h)Q(ewJq)jbv>Ro8yD-rm^!#dUGa8Gc%!lk9=`SwSmL#i+2O^3H5d(g%2YehDN09 z4O269&3-5>tydEgaTZLwAGdvNCdWMRK1CXNpoGfPak5_5%?lp#D#!H3$@j3!( z_6l|FFkR&PbK$$xSoB5VXJh8I=N4ZRW44pDiay+^e`4VF@FE_Sw0;VFd{}Vuy8;NHmn;T}WOG{WdDNDwSe~`Q2tRdD&>}>ihx6{+#ib3jG&B4W%cd)!R zt*soJAb(W`m#Z^ANn`kT#A6L`F60~hJr^W!4JHN4I3#sV(|WM$qzW_ywNh(FvpCVLMHM)3QGhux{Qw z*YjQeKigOvl70FeR~?NoH4J>#JmWZ#8@4z&RTZwQfevOVWbH0sAl27Df=!gu4y}UR zGb3T~JmN3A+#Us zd;6i^YRM5T9eJL-3Dp7 z0_yQ)z0o!Yv5^P<_P{%v>^NKKlHxy3FVo{Ki*fRhL9tx_03|asG3AM-Nh4#N&|Jqj zSR{7(r8}NAs9lU+?0tk6FxvsAcduUGnx;FPTpp+~jp>3(sYLxpA)24>A#8!Murzrc z&(S!;*YL8Zju^y+@;HXuM}Ge4eg?+sxrZ*>8>lqMX{7Vn5vn^I;N@-`;dHKPYeaGa z;%m@*uAGY-s8j3xhmwnFQDSSF(CTfHC=!utjhxEaUg7X@wZ>TL_m5%vsPVJ%F!YR4 zF{GMbx?1N^0F%#e`n1ALLk>o5c1}O921fJxt~e>;m93&&TwaMZS)g_VGGVu5M!;!k z;)(R_-7$QQkhgVM?Ow{UriVXWbdk00$1&{^hg%PCm&mIhBZ2SPCy{ZVeS&ePqk6Y< z`Y%;?^z>K-kZGLNeB$QbSyS`GTwFM@}NF z6>`#I@P5uUXP!5zU%lvk?UJhYGj zuFD$2Agdeuk=xx98?E;c+6Q#4uO-8h2<(i_4Jg_UkME0MTxTj29UG1h^kUcE*-nqOs|m zT=T-mvhnZOFIvQm^0#48i~JTrSOJln6=;Yg*!w|7cnE-d5P-=N4bLMIQ5=oFD(rrIkI5JIp{FybdNLhT3~rMhqalaXu#QLaKZ3ht z<6-MMXpbR{pAvSp)O$E^K}0(qBZ_hSHuxo*HUY-ks#(g?Ol}t2^v|0xvmYiKNc+Qs z^;^M?B!1}v^e{6_>YYbRm$M~vTYn?{leyt*BP0%2%=vKpSsV@)snM(xM+9N5UVO#Q zYrbh_>z08;OiNnY?6~z2U*L5hGT}IUm0~Dg9(NPt**$by;x_495@4_>_+$=4k(~MFB7*||4@n%W(F^2)0#IzNYPsoX^4X{d2@Y0Mc z=D&ia(Niwyevm5%YwM_;ruw;#(Ke`E-wA(1m9eOrDY=?ztWpdb4t$Gkkfc#hOE$#9} ziev|hR!Jnp&lb|s<5Tc@~vwc#=35GXDSsQ{$RBY_!#)xzP~^5$b|j*4fvbHbcFc&6GTB~F?;2A_)q01YGaS=J$Q;M*Bh-8&*?usd-f zPpE5u4;b81RC!W0ufTqZg7~Cs>bYG5PcgnW*&D*;+Fw+}^F)O0n#`lP=tB|ODX)nNPTaOHKUB*J>RMQkb z5J=%gkk1aB)#haQt`27*OD~vmqn>c(u^UWt(@iqG7kVd|(zUV%r5!Z(u&BC=D8-N* z4+`Eq;^#tS;Tct@Q>gGIAOSv1>?m=207kg)TNo%aFXT*(AFSlj)JigysnN zD%+V4#pxuU@T!zQyvu7xy1dK;8bd+#3!5wxjmNOv+c=(VZPT?!zDH08)T>U1)eyO& zHb@B_xj}WOPdie_e323z--XR|$TGCN^Ax4PUvi%Wqm~`~kdXW{=&c{AAd%uJ9068o z12VDrq6l9+OOY&law3r1d7xK8=zgymEo2hA&D)Z?Vxy889x|_s8)j(l#?BJQdu>`V zKh|-CZs&?yOSX9KyM(?B--FBye+?yZ&Jv5Fk!m{hE7WxNVq_Orl%w=q?+w5>(^I`y z8M&B+%@<*FEq7CXnJJI6uZ5v1bWTt0v#HMmGck#11k&f(+J0L#2Lecp9sox4XH^G* z6z=4!R&*@LZ-~)$tDp3Idm3p;DI$wJCI;@bE1o80Un%yZfcczS(LpbZM={JT=jgmw zhI7cDq8wf5cheAU@ty3u_swf!HWa2pPvI5&9 zK62pEySjTLu6?Dv5}4v>ab!8KkOetO#;D3JT?TaIj+aiScciuX85nwzZqisvw_)PL zj}U7sf#{Lzu4Hlt9zKh!E>G~emXv8NlI7yJQNtn4s~)P6lhV^glwQGXRw+7y97w32 z%%(bCsRp49w6vAv;>|3xu}TlQH0v*oE|V zbXewUnSzh4AgAd}4Ce>6jlZJxarr)_5Ph}WSYI3zOFPsMxV`dN!@PwuD=d8+=@YL%q_PCVbUVqGeGo6u^VpWdMkfU zh$w{Pjlx}pE;u6=MQ9x!v$*}#^K?ppx#5@CA6r z{{ZObk?FhnmBg_eHo12GnjRjX5I(KH&2wk{B0iY|jk&~orJ0yAc#W3@&57mvq~q`Z z0M|d7U>tnkUqqr^OHht?HMo12A5A>+-+uo9MdIUAc|H~X=Z`DpqcLFW4s%9S?UpXj zYr8w}MP_O9d^B<3N#JdDoxY;GC9c;s0V5-m8X+T^NMR&jf1k-Vhf&*$9J%{obBJjK z@qaJXK4|BQQfYs+8L4(Bhey<42f@f=F8=7Q;*q!*T8tSDGvt{Wwzk+~);wKIMH8ZSF>Qx(9eus~ehZ!w%ab1n^oi}N zT@3QzW=v!_0^mHRyA+G|R;B=N?HNd_?(goLW@kl=#`up7d-#kWQj(2BS=bN5Kn;2h z=l*2=Gs>g!s{O#5^f$u6FvQa>K7F7U?d9JxlS`pxxxvRZ@)307cSKjZXQpaGa$%gb zWv~My8+;$dAG^7`3V(kt8NbLmII#;fT;ToRA1G)5R^!L&nazrFF^p3>9}E zA2)Ek;0sRQPCi@X;IxDg!@gT@_E!niOy^klKuPfSikuEgueAB*Wxu9)# zVZctWiX4@xKPx|~E6dBnh3MR!P&RHOC&qkMSX|Q3M|Pxt=;MfRc2uRMp}&W^uw~BN ze0gP*`54mY5zhmZmkU;YUZ-r397nVFY$A=&v;5V3Y3Jc5kE9s#^f$(YLdK5n00Mo~ zfP;fr!~L;E~MA*c{guf&knv@vB_8;giV8#yen2-iP3T4?O!) z%WQx>y%0KN&Yw?(?r<)10Vccg-3_X09vs682f3MYvvhAQ1*5-z$oeLZ8M*K`A469X zIQ>=+Y9s4vu{2u!5;Eg$fkuUcT>k(HYouzBY64LhcyvzKcM9j_%C27;P+XHue!+1D z)OvnA+^J`Lk~GNB(g77hyM9RBFH3HmW-+ivhBDZYRWcP z=HAV2>`@kljSTFR5=&(9KrS`A-8gU#Wggv@38WUMm~n9*D`R|wSCyU3npBp2x#X0c zus10^vIppR(n{%0?e2c_<~{rW093mDg9t6f&-zg`?B%Qo=Kau>`Z2s)w$DP-q-L+R^O+x}4z|k)Or*mkL z${!m>#mC5GtPb57E^WB?_4PqzU}sA_uWodWYltmqBnJMeXT#^WGAJ9Dkp1P>_Bqts z70*72@kVYOW0OIOwMs?J%@Q<%1$%_2{U5}0uWQF7@Z((f^+({q?0hKWnrBM(gXed1 z&#l^&WkAdQ^kDY;;c-JQTy3Z^6|R}=JZSM?jwoA9ZP34;`J$Lfuab?jvCIx*gTeI% z!q86}fvFnpyc&0Js*`e)ZGoVIeBAT%TLZ!tET~>UWUO&LsleKn$pGCf?RExG%y<>U}-@kDfZpP<5G$ zM`+={uY}cHbNQ+q{%XP2K22LEfm>kb{{YajR~iGK>3!F-x;;`x`C4}h9-OVDAhD$P zD^|MQUBI%Xm_+#rfVr@ec88lS9)OUT1UpGTcx^v5J{wPn3%D0wB?NJe=FOuZjP@kZ zD+IS`#jViuIcW|6(C^73pUTodw|Q$Dq%nc=$D_jX-(u_j-=b}&fZZ1^g=&|ebM47i`k=uo>g`bkfC5_(b zmL3pd>>?v583$rNW5s@n{4d_XMIUupVWJ9mrpaxtZq^(m{t=cn?{He&%iUnwZ^ zo9?HD`(D=%ycPpTTgQ+~3tl@?A2qhy9nd{BnkK|+N#q3HM%xIbNU-*f&Gs{|jZ3FK z26x_05!wMfmLA9YE4Htw!KL9pCnekt8qxQj??!>@O8H9fqWU2jf{R?>2?X8^}MWuHUne*?tcAgeF!g+DjLQCb}ai??<%R z8501ogVyIePQ<=Q@|ivJwP!9*-TBbs(y=MmL3%viBu96(B^lDXEle?^f>RwS?Ak+5 zepEAJc)9I)PtSC2;X{^0U5eM9^pC)Jdp4hC0drZ3*55R>z;@U7QJiVo3;48WHw~{R zg#=iwkI^y8lmWsMee-G)m9eC0Bn>UPNVNzYNfaqsAafmHs57CBjod&Zx;W;T%NuIS zhCU=^)bK2!b$vCyACxAu<-jsaT3QOzF|V1UXCvK1Cq8M!+DVj$>8{_o_I`>v_hYid z72M-T96OvW{L)OkXy<<-*Je3NTbR68Y(O9ghi&@-`!BAw!$eHEF#jd9tj>eKrQFGHW zTO=R?w8aKbEYH|_5gu84 zMfY5(@p}=fbUSXz2YXV(01mKziB3DuaA<@}vy5WN?5N|+G|L1ob6d*tQ-!KFK}3b+ z$kgKK%8syN1`0f`U-pNf08UgbjaU`XIGTR_0)L?mqCM&x@Odn>90pc=^VGRW`?-!CW<%hA4h(nrueXr-ZWZE z(xK5v((l!D=-AA>KpX)CE=kJWp_)>U6M|@lcn^h;(g;r^r#FRb*ScqPj&?ISDMl|$ z=NAz1OE)BXC>G{JRS|*Clbf-E}2Wm`DMr9n0`?ySE>3oRIu8$i7#L7Z6 zwsP#uSs=@GIMt`R9HcV2x=r;in9O!zcFSsY)BPsmERu*ZmeB`#RsCim2fsZ6t)mFNvLyBg)Xh7UO zmr&I3G4ohUD|g9KHAv;dmC?KtA7$oF5{^%~0&`W_L&C;!1~>f*-%7^)nXNKQrZ}gQ zV+Oy(VBoAvg6yUM#+}2m^fBaVsjiq;TI}ljPFyqL##-TYvzdm;$akpqRKA^yD-tjz zaiQ~8{2f7$@{`J{n&m^kkmF6U4}|l6ik;V;HSw)8Lf<&$TJdLZi9n_@F|)-)U4<;z ztK|`zQ;yC7gNKugn$X*A9#>}Rxs2EhY`=PvjaNitGEA&!TWOg?7C_e;31OLE76>OE z#@X-|ZY>I_pOHLM8@l-`)w3ooz}i8oU^Y#{xk;NF#_e<@;@3c0ZlKv)vCidAWp|6P zlSL}S(dCCT#02e5@-dtk*#P8sr4Q8fZL~=qJZ6iBD{P78iJ`EL6fczcSIDIdI+S7M zl2xpuyB$AFD+^MW;skHLRyvkBWr!Gsl)ENk%$B|&NF_STk;ec$3pBcw&l`$b9!)zr z?p}eLnC~;_6xXNpY^^#*?wIXIGdf7@ya1qfts~|Xcv6fd2-2j4ToG)39C(3h z4cNzrnn8oEUjWvyXM=rTa&ybMiM^426t8 z?zhT6L4S&!@>_}yF=QW__k2iFMRBbM;kL+E-kiq`V0C_~nWe|RW{+jjbhyM= z;E_}=*Q*ss>541;ah4j!LO&2y1;)uK_f}>2`O6<_c5N)yEJmufNq!Ywd!sC^P0 zPF_uR?6HM8F1HjLikeeo@`rUIglC(bCy?ox`Z04U1>JG-C@qZ0$@{~O$^%#5@m?+g&JO3%H?H*@(e>!6SI7{A~Pgkd;Q;@5QPxA>ZLM0Nqf{)CU{ zp_}xRmrMoR7ZLs?f_Ni6InY0m<@~f_e*tIA(H}M4eflpPSD2!>-kdl zkvWe}jOEB!PZuk>ziTSlIZi5TO?v+T@C9vJ8BHX3n6f>(R0x7#kQhw}E%-{SlJrh0;CaCH%O$uKxf8 zo?H|0?bF{pn>;%XzHZ6vyD)vKsLRmF#q`Ho-W-{1$9zs@MD64eWcKa(AYaa_ zpYHn)PsKaLjKPZ>Pk!MWLu`&{1O{eiAc^kAd=t(^vNp&Gc?*vmqSpy4OZ|vi=uM>M zwnoFA3G=k>_pS|jT6z58D_!voOt)q+iE-xl9lsU)qU+gQU ztUS7j^!Bx@+*;Q=xbCsK+vpay94Ie7iP($%Cc3;C#{`jmtJAm1LhhzJiEf`e_yhrb z*uP}|0K#D;>>rZIiHEaC9k^Y;bH}Rtf82gi{28Xr4VE3QAH)@M7B)Vpk~qVfz|IAE zZii)KmEtIiq&s6{ONqV!0@Xz77|iNNWq6Dj<}dbw@6d8T$v=e_`KYZ^8W!%0i271V z)bTU+WbY0A;0>bQ2=flf%u>eucD;l-uK>2MkX3uDGqJ`b*p72d?rU5la5Yg66=wqR|zq0YShkvqYHhJt3j*W%k#h|zj5{{bAf@~;p<9mEqqHg93^6~TZ zTkD#%SP?ma?f8NaMMJ>8}r2}+A<5lLd7Mq}Z&NWcZIKBteL)j9^O z*Wk6G>H$ABOfrlma#k{bv0i?ws&8{n3pQ_{d9vSr>86)9Mn)ihbS;WoK2h4e4|G`o z_5=H&wLHl1v0pTy#1BUISILq&YA}LK7t2W-Wo5&P*7&Y#HyRh}fYb11%E@MKqZo@O zClumx5U~=J{v&pN%Ek9`teEkd$42RYdQJV?Kd*E~oFtizo&(`w zmXILZT;F~a{XJh3pJJ|0@PX>@%>>piZ?oJjx^&mW0Ta3*}% zc=9@npBr3A91=zFmURVMvuyVso~Yd^-}*Zk8@~;skzR@!rOy(ayRvfJZHJcv zcoo9f9>kB4Q3K8UEu46|TttnFy}(gCpI&~e0F&G-IFDZ8Yp@&N?!m9(R?KMpPqS@# z+d*h?BL4v03@V{73}CMIc~?v)_+MrEp?KZBvY+G^r}X#o{{Umu`g-{vH+9Po?YUeW zDbd1c9!Kt)Iri+U?2MYO=ZjBz$nNS1om3qhUA|GYe=t&4lE#OFL=FP>t>^Mcn z2%_LeqQM{I2T7vlwV%Z1GQOoI{{YGZC{6Z@EJOb18Yuhxi-+jota=Pgn+j0*0|AyQ z0AA;B0>4xFDp%XNbN{((BGsO7Z;2Z;m z8bJhOB+(>z0qnf9T&&t5G8C-fsKVL3&;UzMvOiCgY=JFdQ^xJC1p(H}J5985q;K9H z$UF&rMgV(`$fI@$L?x4@b9@Ys44Y}-E9g6#2=!c_4sQx}Jes1qUXPM8FxWu^*P``WJ+O&*=A>w|UtUMzmT-*W z#rq7ClW~9SbDdu(K^T>%%0+?@+AgyeQ_CFTX&xp+vwC^a6Z`yc-MAk0>HWV2MuSku z)Abh{J8gW!z>{hAjz;R6J6=O!K7k_E@Qjc-(DTAkr|bU!6$?X&Pc;0YDB9y-scC8U zJCgqZVx@zXE-p!p;P+NsS;UTOtCgKgNRCH%i~{@cqsE6|AdW(ml}R*7#+YVYr)Z+R z1hg*sN%b6)3&!KZ+YdFe00CM_*eBT9T&o~j8zUsJ0y`B1pty$uy(87tghNj>s`&WwL@k7?9-AzNGS~8{iV=>^tSv`UC@;|`ZlLL2h z&P1%o#UT1CK1MKkJPZ0N=DUF58_2%OxhEFIQAye>GwM&7#f>x+PM^VLlid&CJ}HSH z)v2X`WG+GuH}PvNz@%5M20mLpYVQ{C|bN^fLlGqM}>QmU54 zi=l5(#V~WWU3`^VK=!)a3cY5>B+arcrEk-Wg0`zk+M%5`Rg3sj!uyQ#%5RgRI<2j* zqF!(%gqsGJETGtui|kz{ru=!RrkIm_RCj&SjYku6tb=I13Nzvci=o|gx=^vBX^mjR zd}nh>rR2^-;X(-u%1&Ii#(2_aV#99u{N2_ji(x!)fW*vj$}RpB<<%n->QJ*Uk|^C_ zptDiMcbCmeoM80q*x6;KPzCl~!zMcf5V`*VQQ}VqSB)Kp1K0#`HGE$}Qo99{r%pyc zBo|k71}98U9z~wtL}Xn|+-8P6i|tCsrE2lzKpjHW=ZQzK)NawHou#%C((rgz-8t-Q z16o3^zcr-OjReC=k(r!QNJ9p7|PGg@IVZ*QWg>TL0% zusKqWX(dSuCibkcWAzR+qqqeUH8#JN07uDk9YND$OSz691!3upcYQ}Mb=!~S8}?Q) zZZ_F@mr;lxJ4LN@{a!Pa7$^jrRkuarNA*b?Gvbx6zf>l_0yfdrGGL1@ACL$kRrNeb z@R;qMU!rHz`jE(gG`p1_QsNwl2?X)%gz8Gip5w_Ctv4c8Wd8uPCAZtUai50{Y#wSZ zq=q5z?I!sO7&VC-kxnOVu34n|BBYY0LzAaEDGOS{T8P2M@SF>nRfif_W?*@uu{7Ab zfHVq@;o!)PnwPmw&cQtcau;tSA0O;&6 zmC^L4{6?O~IN2d}WNL7x2q!qT_ycsC8+F-jhn&fu;W$#?A|MF~WAU8X+W7n0^D$3+ zSc$a8dF+jis_v>9t~=wG#E7ee@SL1)_ku|z4}G=4TZ2zQmu8wX{8l%`3e?!cv38GS zfslMQIem6VXxfu9ZJwc91*Xu++on_chF8Oo_KpRlIN63wkidAgYwFlqavV>pszVbo z@LH=bu84Yw`3j9|->kOkdp(8=D7~B2O z#GIy(Vy%^om)nZl9>@*!2GH^nT#LqUrQgkc1_;tSU|70(4f6x@S1jp9E}J8+yu4aR z((nzT#1N(HEVB+Eji-_qZ%DVIgQ!j7WIf+BF<6k>iq`)CB%n3OcG%C!R&^OIABi4uV;d0!UvX~T~byC z#u#J*cXBCr?F(x(=$qN0g~##aub(lG-u9*5wAX*=mh)e^{)!pDOD8Uvv>UU}dr;?L zLzvb!V0^CJ+T)eGYyy2sQNL@k`F!mr^)vcYFR(~nYYuB_?vH~{ZJEN!zO6T zaJfL*9cQ;GW?q??>E9sFlr+4&Hq8P49{qw(E;ew@jscmGBV5D#&`D_I@!z_RJQujk zUMtfsRbcxi(CD~OO4%Zehd1u|j|v%bGIZ=X8tA3Vc_EH)CyqOw3SKl^w*XtHPFq9` z#NQ&l(&LMYe=0&HUdt$JYJwXN10M{$9YsF!e}CA8!o#4 z0QSB85f$Thxwu&Z08(9|( z0)nbDNE5qbHB$E$&<&6iS9jf5Ba%EQB9A8}fxrd>Bfu`Nud0r0ZVQT){{XPEib|8P z>#t9(uXpyOD9grlCT8SBV*dcv2aThjbu@2mtq)^^nt22pzr}O7NiB#-PpIwaunkeN z76BiKUc0{NQH)B~%EvNJTHr&6xINPU0NA>4v*0q%6|_|(Jzh6zI&dbV^MZGQT(k01bOB^0fSHsRQMWk8l(m=7MY4QguxdHituiDL+tHGfPW=xKkAukg zXtvk*0wWg+3}-mS8yS7b09uxg-P9I+sQzo6<(!o^Cnjq=ESZ}n4RcQvLKV7RARvr* z{ksHV^&czxED`_)sQpsLbBd(QWHdFjJAXBh1rkrtEo=e(*Q9YLv9Dl|Cg6kGtotuA z$lNRdJNqcb4o_ky|vl=rN z4%-LPyQw3BCJ;ja`UUO7@%k)>a!0y%l229?&AcYcD51}K?3rct1F`xuKTCF<1asL3-SKjg1I^9Y4S=iJ=h$D0{k@N3mR$aS!!a6bf( zr^}rbGDLSv@mS`ExR7wJJwp_;b!JSgvfm?ncBYH{SD*D3);$x?D#p%t_Pe5qup-Zi zl=ys)q5LPe{e0IJdv>^jzOB{t=(F_sapxXc7+QWy=5Cz=jA=zWCzk&Jn&JQm9VWGn-AA4M}y2yl5)u&gp0X_A(>3r+zaHCciC zTnO&0II?(=1(3w?yVJcZmq+GN@QJUIrVa?@GSYGmK2uyBL)~K?MZ__IB zI;h`e!2ZX2=NCmXX*#^V9yE^K6t3*YdV}l=>Up}eHR-=*Xn75mHxKo!sGV(%pWz4m zMK>E_$0|{$*}@$0gVq`GG#28`E85aW4jYXSwAVBU=ec46{!6TVuk_SXbgUUzF^0*e z)llcd_56(=&2#-9Ik{M|?&Z}->bx(h<8aCMW78uw#nbjwv2lJ8-fILWT7GLos2)OT zs%WPqZ;Ojc;?a&gmuAD~BggPqM%$`7J)Y3+#^Wbp>plAb~&k_sc!g)9xnJQK>CMAi=_kgN8^t9Dm1 z@@B+ygSZK2?YWGU-hUMDUyYJPNoloT!o+Whxxm-WRV0;-RH+ODrp%9!d0NAw$Yk6> zq{{|KV{mI*%ChP3I%@LJQIlv^{V^6E8Hfg!{)qFYzDCrDWI~@xut!({O zQu$N89bp(D^*x~O*3H}Ky zv3{gOquL$Wc`u$>9O4=+c?BQAoYU;-xVnT}!Q$162Q`ua1A^x7Yod_yQ(X@tX3h)G zl|vqRWL;7cGTzJltqyI3lT^VOQ8|azG{neki~{*to5R@(t8|HuM@b!$c~M}Q zH0EuT5CZ7hgM(uvi&gBbXftDQ;1y<^*J7yt1+eDFmfB;y%g|@biIC9ZcHeXxba^nR zGDhz&vM@F|zz+M!b9mieglYHezTnK<+yHynN%ApcYuwXFuAKec9v$!!?xLLH zl}RU6*-1W`TIvlp@53A)lCI%kOO2EtXS%&ZJXvhuf>m={62mFj+Y2|EFX9H|kz~Ti z5@WTGqG>^>W3iELt`cnycbBv|S@Yo%;sNVQ-!`^0O|5|FT7-DnEpG2$b$j02oNELS zLgblxbkWV?LQS;Vy!4iAlH&Mt0tok3j^km6`dJR63{Xno@&%;XIQZ}7q*`Uxki13W zFwqL31Lu1QX(d8X-PtC$MUw^I7%dOsCf8e(a#&$;tMoy(T)cPqkFeD5PswguM(QpK zU2O)fY%@=qOng9IP1maPvwTo@t+J>?Nog&m&rLon$%*OG}k5vXlhB|OX zVPpvaCza6&a#n@?gSu*YFtP((dru1b(j|x=bw-KJ4+UE^x!WWl1KCa5m9n_srYzR< z_RQ1AK8SrAB201MKd|hqc`*mHr4z)~as?IBT;imKw+3e?C^A92v0e`r961Sou^XR@ zy_aLzXF`0EK~r&F>xat6!b))$*Y!p9frmFqD&H~5hcsx~KV93B+AM9eLCwOmu@ z8z|o;LbggJz<%M#30{u)o08(!0Bt>f9>}C;p$dVO;^FFU3?{90vendhUlER6 z-s*)U4~A-ZCza8#B>0>*wfvGfTFsy`U|(FR<&E?g7Tuzr*o;_@$bZ~pOON2WW~(`p zXT8VF;z?b1uViMO(~>~mA2Wnw>D>N<^;}yq_aXL8;KjK%$Hx_91D7Uv3@(wQ=9WSM za8?}#4-+Az^1h1-I^te5NJMzR_CBhk)_GngVPjpxcq;7YjC>ap{4HF+Se(z6^LJ<3 zm6+^ad0IQwZG5QZAOdfMrWESZ3UK9DW+u~3PG3D)N#1YOV=^}qwOMz+qSP!**}c~7 z9PqRXRw80X8Y<(_Sq3>pJMOq}sqwX1dRHE1nerIhiV)KC?QQ^mq#)#$*{tl?vNin^ zuh1Jz^Ee*$Zn91gm%_f08*GbUa*}l#-*T@!{Q2;ry%G?U| zN-Gb>b=a>(-1OHU*pZSs@J0Shh2io|dv;yVN+tRnM*jfreZEQ=ze5rJmH-XkHc~8> zjC|Q9!El=)&K@jegS1zA7sObc&uhSgPS9Tf*W1?ltfxyCSI8w43*D>9qlbV-#;&zQO~Pm;gWDvb z(SqiR2EBP(!H1g;m?OrPHclOTfa1CNeAOq(F1WFGUxP!_IQM=epLn~t?tA(u@T7K| zsf|88u7ERev*xqU< z{=SE=V3so18{8<;LLz>TFbrV*o(AW=#>+`R{XG@g8V8hn9^T%l ziH-(|^!~ljt)KQr1~|xTpJX!Q9<+b5NrS3J9pY*XlOj-Ewc(=00BgV1Y<@eFa$88> zYTcpEpMYPGbFx{pEyAC)@#$Zn3O97t8{KUVXtI9s;GXJzKU#+}VG|;ihi>a|73s(7 zg?3&>lQC{vtbD2Z#4mkZ*{{R_HrKk8cO+9E6P}3sOu^pF)?XxnN(;*~LaQ^6R zep8hWjU>A_J;0K0x!c;2S$dOosD2I@<%QCKM2~22Z68tiDD|vaA!d~k+~UatTv*!% zw)ghztqV)j>5+}@|nBtL0rt8F*1>{Ome_Mqrb2I5~I;DoY|(%bdnE< zr}%IG0K2|x^{>GTs6{#Id?&TD7`6~d`X4Y7@=<7xx;Ikk3iLbPe(HKz3u= zJXst3M!y8NUCEdB#ug!z40*w9k(O9^{)))bLr0f{ms-Gk@}ka-(mD9ebG#OwB=#TC zPYzW)w=hNAt(s-%!VGsmN)f@6KnIb?Z>U0aXe`Xsn*RX4B=ZfSv~AwLs#ojT+f8Yb z@X$qAvvpUsW2Koe%g%h!Ou+YtH^ep%e%vnEERn-6xkYM9FOg^mj`qDk+2LvF#fJSB zONj1xUOyw6jk}7z-hpW6b6$%E&!el=c=zvD0K`B$zw$vn8KWlozeVp-XpdmMU5!!q zSOk$x=lQK+;$-*iydSpz0JSY_6Mmmng5lb!4qG)F2j6w6;`rfU6Y}lK@psV%>=byO z)!W%-{XH#dyLTqnnt5B0XplJ91!QwZ%8Jj=m67TCdLzu#P64l5`K@f)($BBtvJW?P z_#?TrM>LTHhjOG^u;*MJVs>#Q~e@{JwNpkT1yU=Dj{?r%O!9G-x1{feY9L-tP^=fa~=fQU3rW zzgoqYD;h}RE!;r^cV)hl3-It@hCI<14lAdGkHs@Wb#I-Pv?R$jG{<53}m0O(`C$yMcNJ6u8My7$*e_18z5;6dkw zD6g%dfFRKC^hB|as`;a44vnrYX>IoxT%y|<+8j4f;1SJNbUrhST;|^tc~ef=1~3Lw zupPm->*|S%PftkLaGu~xizbQ3{&27TQhblHafD2Csh3yymu4SZ~>Fq_$?f@f| zeCsJ4CsIu3p6z8&_h>x+Yq#m?Jo{dcFY7k^M_-g$AjUa(I8$A@@f-Qm+&`k@KW;rY z@YxXRaW~5#@Z{t8hi}vQF0F&899$hT&y~@^ z=Y65#!Xf66Oh-cR>D8dOo8* z;K_ok%dLoK*z?7wnogsSkVkFGmn61x+8*k7zY_s0?yBb(B*P(kgyYi0IdXF1Uikjx}?2+|GZe%vd8f)EAvRRXmNuP06 zj1<08VbpppS-p1YURxW83mCmYB%^nW`mTMGjQEKm4-R~>lF&#iq2W?r0C8>X>2vin zqUZiJlRP+{>dmCZwT&-5)aMQeE-vX7{53XEdg15`CJOB?)-^4cWh8RPS}m0OEf$cf-h6RX~pi%iv`kO1mKHYU(C z2J9mgv_}Z+eKq+F0vOAk@=e(cS!J|E6?ExMHWJar$${W|()vn0I~~!0SPEOJXc)4@;$vz27rwro1sxr$0$-+7aZNqjqPI{e z4Nb+*+S6*s)!3}lUf~r1=(+saa~Z9^V7z>orHW3@T$Y_kG`K=7!-eDtPt|GFT4~S5ww_0-sA+KJVCq#VcH3PN{CqAwLRqB>k_M#`hok9-4pY8jN6kH( zOphg&2r8x34b8-n@xD>SO;ZPc#0v$UmeNnR_MW<7q=3v7 z-RZuIByhfq$@-N{)^jp3!?`>vjQvkOHcLsAfqv?e$IC`iOLTn<2y=Kg3SFqNp@z6h zdNW&Qsi(8d3+t42xx7QUo{N?faFsFUtr*8>d;1bvB|@VbB|7FAy>2D56BWX(KErx3}_B3f>_=MevG>sNC9EK>Wi)8 z5056jRn_m@EOtusu$wc($n3gY*W0j7Siekhk7%wHrxn5R(MWeg3w4>+vOa-}pxZ z^Z`_l-d?Yb3M-9!CdVAwx%9R7Ej+X;VdZDo#!m=5**37>x1ubQXb}Ce$TZz%MSduzr*?L z7T^B>SbmF+>9LtI7{-fphyZ)9t9oJBxH>Ktp28{^n%}Fdllj{7{-HLYk_NA36GxS- z`!Urohe?!M+qf{A{Z~6~NAC=8pvQ6aT_-XVi_zNtGeO%1r?Q8zX&3y8<#}W}LTae` zF6?sU6)(u|(IFws@{U)F2Nv)+v^->%Il!J44TpRERRstlHVzzI<&Xg#?L`?bAQQ*T z_Czv?b$#DUL4ky0d2f$J+_aZpPr}xYd@g?_#9@w_NbOyhX6T&5HZ+*eW%y(kpZ7@Z z`X9|hFX^E@Fj)TS=GKqM#c6bM%N2vjbA^J4$c?O?RnlSrEsOtmMZg(kv1I zH`sm)$Hj$CPm`)VdB7MSPI-k)(p9|r)~ZW;MRi#Om88rzoPMIF64eGULL=R zmop(?lsVv#T+u_nu_>7&p6VHKZ9IYBuv%hj_&IaAx`GCIOi@*3>Y5lo3$^QVsPAjL_<2zEs!ZEihUku4% zfP!wj_6OV`f6@&oi}9>cH{rXr*RoBmIr9F=bN9W(*#&^^`@P9LEVonB{1&y^Y?K;Y z2EkmKJbnRR4}l-HrMCY7Y8gRMWWdz$^Bm~$-@LE~5wwmE)lZd?7A`yImV+2P#PdN5 zq{oWrSvE8cgviiH4FG{%ieJ>&*-{5h9P$WR5e62E+JtpCNOAyl z(J|${i-nMt!Wi>7%IO)Tn5cComPapn<9FOV3#4Wyt%s5)@Ek~pd9A-+>{4v}XlKcj z1&|zG1BO6vA5Z!z<_;_{K*qOe-~!+P-N$me^TPyjrmo-aAA&#!sW1S$l!7j{dR;BY zQR&7uxIN_96dT&Yn?5*kab%i2Ulr{Fn88Hz{&t4aVr-I0JLTQIpqzJ`F&}UG3r81= zEN*oF0OV!Du7|hD;@69a`5$DDQr4uz%49L(c*V>v@|v#va6VtEl5JVc#AD)sKOw=T z&a3D@sXei^29eeTdpX9Fxk`ppbk1N;s!v@q-)M>NIKBNBgau^3m9R zS3IEN>4BrigSYQ3K^yJd0=#5C-H=88r(Bmx$@tz-Cj3mvr` zx=}W>D<^zy!;pr&G&+wX-?D5-uYa13M=_w64hp^rLNDbT8?Mlyy6nQY;XR^>R<#vv>z-AuVxsp*RsWufcbf?(E=xU!spyz>%;+9y>v62%zG46uae%1e{}B zS+vPNY~`9KPk8P~B>Qpbn_yIDm%CZPaB$vi?&o3j7wn_bVSBX+ zGa49RmOm@3+3mpxw_=wz7`$z`aIfpIQv6W$&c6MJ6r(Qlcd)a^&*-&1{{Wd@O^d{z z9zfYxWMGkuA;qND;p~%ZvVdeX2p@R2&?XLbJf3*&t9s<1xx=tNw!JSGC1~ov=%w{_ ze$}#g@FTCuSL7gy90AII)nJW5hl@Du{-gRT`8M0+eSKHcL-eE7T^fLQ8z<}Th>7QA zS7Y4>Bq7vP(U(XIM+f<*Fvbj~vmNmUGTrJ$k@Q8#{m_Q*1d&6EKjxP~g_n~XWQHR6 z&D+OeL|G@(xbNe?!jt zynp^AiSl1vKZ4*n5=zndY(Ld9={-TxgB+LahSRY=GepO;<&Pp;A6Ccxk(hYw$DA1W z`Y)OK>rwM&pR<$0ZQz4ngN*&R=pVCsh~`4tgD`L4c6X@$zl!Duy84gpLq;0cOG*Pl zo*&*v?_;?9m&`M>WR7Xzg?|G#+{a&o*Mqb}>b>Z)yt~ISOCe=>V4F2c!$c@Tm8XhW zWIod-1)FIwCi{L$y@4jl1*gn#b3=kCC@vWb9M=QDT^FbF#zq@#@&y&m8O^IVN#=NS z_-5&2UdycP4~T_P)C{=pdl=_5JGckBxC6KXI~0dq#p8HrRZLl{M*je39WR^J8vIaV zhc-44>1Fg_zU%6qsL`9LbdFY<;$(M$Omn;LXyE=~P(Si;#g0Ci&zLlLkI33jtp@Nv zRn+x7*fi}wBQGuCkV3-e6YQ2Mo`l4cPE7d_i8nqr{piUQr@_jWH-?f`<~;)gQ0XW2 z9-bU0ZbQV)-C(l)y(+@3$)~Z-+|Cz;^$br8-{FyJ)`DTnbAW4#bEb{&Y3{2t#P~oX z(Ho=cWs$Gf zkQHuSc6hQW;O!}*N%UP` zMCkU*3ty1U&Oaqc=r_j(s2eC2PYdWBFvf9OTV0o@;NXm-%0;loGTBGU=*&k%TIgH( zE`8Nq8tg67;#+IbF83>=B#R2Q)M5_-`>2ZbU9-g&j+sI-RMEn;9YSa_{27S^8Wy^y zgPLuHrti9=>bgua&gn7eI_y%POV7#EC@NN+)_Bpt1KqTw-9yz4&yDfG?*6ORS>qU6XJ_1kS7l$UwG*A3_Op-*tr$xM zxKd>NNgX!#6WvQR4B5|h^ZrI(yFGMpHstI|*;qK(Yuv9a=0Zs!g?Th!s22YKCwgPx zld%gK7;$7aoX|q%k!!PJH*=g#C)yuYGc4_SqNJx57|l;Z*)%9gC5^A511Cb2k_W=` z!gCg&*9&Wb3ukJPGySC}ii>uP;~!x74w^OwN(rx37NabzJDF*e8JM{ovCS)dm?N2- zQB%$1xo#np8(ILy&3}Ro@}_QYh&H%>Xk(0Gy6l6KC9z4h+$yV#Re?$S4wnWAow&4G zDhL}KTS6ZXA~{80D53~F9uZt+sSTpLu4z9K9p$i9Qwyj56F+|17wo!g!*=qEHNvT8WSAr%_EJ-vTXPKlG=^E2u;xZXs>+~sCqmq1A7_*P%q zL=P!mW~|`vH4v|lKFQi(Jq3%IqfW@=B(NG?W$hE=f_VfgUM5s>fM^1(T4ej+4Zg}= zOdI)?gtbfpC=NXC1MZTK-a~17Err~U>oBm%AJIRJlH*KPx1dtmaV|U*CRlfPkK~QW zX`&D&PYu-NOp#CHA$u5UIWBW{N_j@+pAJ3+s{EM-A|Ax=Psna$19x@L7;yM8lf4MC zVSCAOB_h`{WWm#N9tj6-(e*qljoqXZ=}quu2tSG7q^^U8IY`f99a zJibSKy$~=P&o^o_?y^m+8tkyF!pI9y7T|KQrtNRZO3<*uX3-2`Bq3m9u9A#?)QU%a z*v{kq7jV&Mbj)j-@0#XaAEC)En*6fT80YK<=sEsjbgrAmEN0LN?7UAmHP26?Jic2! zteWX9efQ>YiT(4Khf8nS5 zBckmiIk5Oq#Z4@67Du@BLb}UMa7Q$ynpq`#T=R6$(ew%@kz$WDu4ZO5W;ty;a6FDF zFAk(cZX6YbV~ke^0y*rvE`i|bx&(P$r^#S_`;s^x)p0yW_vWlFv!=}H`g0Y9t^*%y zH(w6&ivD3oH|SzLCK4W8;^z_dRqUK;WYy_Ce zYa;_Rh&WuXo`#H|HmhP_q z0Pl7}V`RQfRuddjFIFQimfW9jzg| zNoo50{%fWx5sSL(^Zx+&WRmO>JbAKpT(-7pqT4OsG)101iLR%N$h6ts)>N%!hcHu? zFfiqZQp%O_2C?rYt_JK``TWuPq%NONnl>~&?qLD0>`I1gw>YHqs7XDcJQ$DEbciE- z(#H0X;^W-+@5;k9e3;l8booal6Gzjxr>bM4Ce3gqv5)C^YvWM<$Z3H~>IQ2nc9arKa_?AcEQQt$G`F@Gyau$jr z!S-CVd_?!jZ-yPNjfO;NQC4IK6JC8-@a9+ezRN;lX^W7xv zFuD@v0UN`C1bX-7V=Dk?ksP>QWPEK50#|?tBzo|T$-7c^fP8|IfESJxskeHHitTaBn}6^bW7um z{nlOfG=$31;g%Si;SKPAv9f%J(Efoe82vyP){Lkwh$J)~eLsb8!xS>xgzNf(lX^X) z`Fe{{ZqNmSDd3;WxJ^2MUXzjs_z!bmucG#yr}#g=os2n-XcE1@#CY^kYgqaDLuNFQ z_zq*5_PxE&HR!6`GIF7q>KM>X6f;QHNhziSc7N*9 z%@YSd4dJuR8?l2ZF@v+c`*!d7qjel6WHdN2qINX+Zz9WG?iBotc;0t%wEqAAblL4o z7A*W%MJ1Lyy>Z*JU83p_IXSqp7I-+$@Yq4~Z3K>gB>=$I=mDrSowap*-+tc7t|k;v zu(|AEoap*O_F03omEYk=hPpA<1- z`Kj7|y^N`Dz_Y2{Lxc!tEg)Do{9UVrRz3`o>iIbjVDR4<4R!KQm(#H9Q8Ve!ey0XY zWE&U=b2jqs{=S{lJ)zD4#)0`QKP5&nQ2bZ-4==!rrHwx_i2nd;Zm+*S>c-{k(isU#mjR}+aUFnI6V=&*bDG`#C?sQg!lZsi9M8k2pGW#HratQ)?0 z3&Ou7IE3A%?$2f8V1QPwX+46IZ#?%$iLCYTEqEIhUy}CI*#2u9d)WNY;uG9gK8Yr? zGutFBW8TnSFKD|S$<-V6LS||0%XDS#b9TFMrGFYxhD7Jk$%I1;a7G&oTEXnv9Hjk0 zG|{#{4bn6hfJ+6Q$MjE{!|7#V_@+kv-_x?#vT@1ITQW=q*qBED0CyKR{2>jcZ)T}H zLDF~vKPv#H1Lf`d{-~I`Nsc4UFcxaa^%Q*-pG4_-d=^EE8{XCmv&ZIpF3Hyi7)i?i z06vPx8+{>DT3l!}l8nRCXPx_4(&vALqn_X7uX=Y$M$LyVKx?~LC?CZ>Y%pQRvIv;L zUV%p!(X{gEqGU+SO-dc^ z*z9MPvb}f2Pw2tZ-Le366v%@^mziDgR4HH0zhx-2j;jhIFj!lpW4B*ny z>@MiLXzmi|TA=g{Vg7&FT{^&j>Bc+!SkOE1@Y<;zE}vb^tLIj=jnBYy znn^g37g_Y!0RBaHcpCB-^k<#*MSd)hMjm6adTM72v5-GjpZ@@5Z=q#7Bo{mHC^Xgy z^bDxa4HLwF{aF!u9y>AQ+vvV~{{SS4{w_=NM+boK$A|v_zQ&V1)8o22@51@J?Gr_Q zkFU(a-Lc90#(fI*zCEF+kTTCUYN{?nmlY`dwR6@{{XM5@&2EY;m!Lq)WqrR z`S^v3`7OwzR;JHo^#@6SYqHO($1BSO$qQz!M|GaX6ciB)>A;TZ{sf`%(L9A(Y3)g# z1f`B|&)pG+jWj_IWO9@gR>cW8{>VKo2i% zclrbT*AtgBD@N5K)G#D5PZPH>9iXQe%pNOT z6tb#xMxN|+$d0v+t(X-VrnnKR@vVVh}r(+ClW>_Cj z?409k%y=7>Fyyh$8d%9I z=v4|`LRYqoj{MRMPb)eSUkl5J&2~R6$<$pHmyp_|T}1Ok$bRATMPq6a%67~su4{qR z^RiIPphC3cXF-(^EpQH8({qElY>I!47O8Nt@)`TLQ9j8$_%q@>xu)8IW@b(^t&T2g zU^SsEJ7gYY9F&o*H%~xPY6Kmw;HYQIbD71cXAPEZ&FqK}hqi}xDAEJ&fgzFG?5`~3 zu;a9vURFp7HQ2{dJEI{iz?Jy&65bJc5)Q?|3fd&?1ZoWmwBL_DIV*F>h%Xx z0l6TLs!O8v8z!dD&(&I?hZ`y|<^j*LFD_W>L5i)Rq*JnX0w~%CI|Y;39n_Fw_)VrB z7oFlT`-NP2Il5t!PqbGuW68plIy)dM29=yLVgcMaM>R+Yc>K_?Mj)ElXeXRNwbw&T z_Yo1VWmU=6UyA2h;44I~62fcQbH1zw2R?@dMl+NoB=pNJm(-Btovu8lInhU(5e;{B z$uv_h#SOLvu7!a#krtP@u9*2L>PCxgYzj`Eb7gTh;G$-dapwl9Tw$5A07Vt)vD+aQ z0@*SB#qeWNT@{IOoC|jFi^+J06c=)f&X2=z+ART*AH+mj;#46Iw<=*#{2OVRk+s|7 zpi`MOIpu)0j;<9!kc@X4d8Bk%L4um=T`$ORkHzS^d~|rtG0Eim@|p>n7&q0Wu##uq zdlY6w0#^Cqk#!ye#0AX+l+C-b-T);RF3gCj%F>i3iAvh0 zQ8cW77DCCbIev`Vwv~-X4cP84i-Gb=#;-eO5%cvbP9D&y#;Nu+ZZwb{0Y60hL!Ln3 z90(sZ4xc1v7)bGab7`I%;E}f-S~6`zWBs~gX3-q1#Z3iA3#?MyG=SyHh3<@F~I)-ISO2D zMjV#_(kvq^X{$Hxi{qHwtXuD4aDTKMhMi|7IM0?Rcw)WBf7aJ$hf|LWNt$PoQAS-y z)`imf+g-_Y)5!N5d)!=2_a!VEl#(o!$uT^J-`P!!+jj_HY|>??N4{n@xwY(JiPaQc z`db*XFi4~BqwB1%4gn4Naucl@5f&qCdVWfU)){9bQjib24GvdazXcwIH)O9YHEc+) zqEicXDK@mKzCpJVo@){c;_TW%A+mv)81?pErJgBxSp@nlBRxxI3o8I}RIG06pm^l~ z5=1*D7;*2Btsba2g13F1>BCa$To_y~F!1;M+BrYTPb9de$;r{(bl#pCcSq^i+Xslq zgXI4JiQZ%RU496SBN#EKiuo%{Pi)xaJJ~8@cD>@LFPD35vQpc!o2U z6WRX&y0iKMxei=rH-R86Cz>m>>gBLtbxX^9Mz<;l5r6&xTyHt<$$O-f+>UjX^u9m- zxc>k|bg$BYoJT*=Vkbmj`|bOG-R`{M>`IKV{qIH4W6n3lE#fps3sB9h2hg8YykgGc zum*V5R=TbX-WN-0OmBJQG9G`z(ACLtqyfWsuN*G1(l`=o->_N9#B4iW;n}Vaad`Uh zKQN`4Tqy#rG<05z(>y2{mn&({7SE9PS1oT1li@j$_t-1z+#k(XHO))*ZX_NMYj-|? zui*oq=&VC2l43RPa19YzB-f(mgkqC`BNUT~P}A|F)FPW2Xuz3(?SHRyw@<>+ohHbNfalX|RmN+%? zML#Q~vg05`)<_Sts=qX{eFruhym@~H?jNG)t<-Sk<+HR`tF6DyOdYwQ%y4sk3Y+lY z`ea{=j$IsHPOxa#y)DK)u(*;4ze`*8DtjMW46=SK$8Q{{R5JGmpAA^FW$j@SWdjq4M=zgks@_jhGmrzy47E%gx2Z z{u~Tz{{Zrv{gmI++xBJPe&-w!yO3H%EEW<;r*Jqcvm8hd-ot~R{{RGkWf8!`C!8E( z^@YFLP5m{$^9T4E_HoeD>MVNjwh^2Aye_pSm!`OZHl2@N?O<>6L`SCSut`v0V~h23 z1CQoXFQ!ZVLoW~aIRs(R-7wh|+qcne;YqODAJ3^27hpJ;w`U6;{{Z}^{{Us7!oeb) zgN;Z30K)-MeKud}3-H(E0{V)`pT#J(pt`n3Q&5W7nzh!qK>R zEqFf#j@Ns(Fh8Q-%Vhrm%n#s~pBwq*IF3xdrMVRqmv)uS& zl@oCE_dJ(jWA676j#mCy{{Yl);Fsj%t%=uLd9KvFVk3|*>boe18KRks3rET@wjrEc z5t07@2uJy6{{ZwK;Fsj$?upfOMW3R`Ynndo+U&)U*BJ4C$QE0$3T#XtMub<>TK@nr ze}i9>j>kSjT~X64R{#zccV$b&jobjoiACmRI-4*ocepr8mfy{r=4O*|^+68~>8b7LyoinH!9}Orc(u(QQS!IrW z>2DTD@W3GM1F~=B`2PSV{t^c{i=;I-i*#YXSAea!@bNE0tPoy-vHD|6hrDio{7yg7G{)0#pcf7_O+Mj*(s}x;0yl=WnG!qw*sJ-{ zh7PdClPE&$Y=~M;-x0%resrEV@kgYGQHtoY%>++iW0=QZr6L^2;f1YWuaHY7)G_d( zIT=z@8VU09N%jFy_5QNV&2!_yiSWnr9%4SJrzqoA(A^h(Pg%skY4KzZm(h*nt#R^x zD;g<(#@)y9Ty^=mHg_@SpHQyd9j{{@JK?jkV#ksv5Dc#k1-<*9#4ap(@@AhNEg@Cj z#{EO0%d9}b1KQU|WCwp0*RSBXpH*Q^ho3YV5%_X6(H_gQW_6yArsQ~S%Yz^~x4J1> zLr3Szto80!OKPyioi?|J8`;C#6oK+Rp24G?-&INYE^m_TX9Ug%)#LqoHivEl9x?h~ z^;KsbxAo`doOM>C7JjppJ{t^J&UHrD8zF;++4rCleZlUsEU!r}DJD{l!3SUIApvAM2ehsnT$`Vxu7!JbE6F%mJryg9wyUB}ntwWYQlwMFBlqAt-j zCI-6S6H9)4yZaS)>lp-+X^-1L;jj;-xl+0n#ylMJD@9WW*7q)x_KS-i7ABhrZ9Riv z?0$nf+m+Tibq{;nU@6{b#Pt328RO<|Ko)Z51qvEP)e zAJuyXK*tV!SA+immE3%!)p9);w)BsidW!+tcB?)+fu^|8_a2F9^rhDR**bZ+;vCqABgrT9 zE0T8a=)RxD&G&Js9kJ_XJy(M9UcMJV#ut`uipQ@An9FP))QW-Ow1gehj3|tYHY9=Y zwNBGU!c(QsUPIY7MFeZ4WE^a!I1{v$(lKQ5@!H^Tg~{W6Z6NZmy)B#Kwykqd>WG38&$*X}zzqz0IjZl)}(e zLsG=sIeSM7xy42lX>=Ccp(H>Q9MK66BZMfMVF30`u-z1_qG+x*Q<7R{b?pY@>7k%qBk}a{#ZXBGtnl#D!heO3=RzE( zCpmU5n+=Nx$Uq?@)^tf^aV?UQ7H7&GKzgG$NjUcux*-{Hv{ETGMOOn#k|_neSE_&e zFiQh9j)=T64@yBTV%vTXr-=7W23OrpUw% z1j5VtuJ}SQ=rsEj@r6p6;qj#rl0D=VuS(*kDrowdV+lc{h9L?zW~!%%oHlAq*E1es=aJlnfllPz`FNPNeQ5>N1 za%6%{yzN$4T^qo#0+MYzA1S?st|yCxwb0bxMOJsYz&&~y9OJ=9>bL_LlsRe!C&e2_ zATE5EjhP{}CLkQyV$!0@mf4||nin%A);{+2HLQ=)k!?M@Bf&q z%bh~MBr-Dlfg9RDCxVI0gbE;cN;J(!#3I76#^%izJ=cdkO6@Xn{2b$2k4 z3-LiGyKRG!_|oHZi@rGaU6ZQZ*xA+5-!e&tD1Z57-~7L-uUxVcuth?(dtcK$SLJ(U3C8dUet$tWx~bn2a>K60*gg+#h;SbaNsl%^->xQ zrf?SC7%^EiZ;C_#T23^%x$xsbUM#K+8;AgW$@EuDeQQo>VoBw4IA#UH+eCcUB*=4x6ZBa^G3fzoqp~yYe^J}!gA5Vd z%HV&RSwGrdO!K-&!vp0!gHHbIl=#GR-qxmK?#77hvq|=Y-)C1pY56}6=#C~i_u=2& zK?mAiWCr1a80-8c!v6qO%KT>nKqYP-=C;Z7LXz|jCu08q&jzN^-4{Mkp3v5m{Hnf`*GC824Z5ge1TbK+jgqve(nrV{{Zlo{{SV$9U!6!Q5QJ4Z3LfS zvyjww9M86`YE!fb<36=~AHtH!KG=GQIGFMq^0t#-nOw|dK1%Uz&1l>0`KX-n*dfSe z1p7M^{@D719ouJqHbh_#J7N?`7CHC%DB1Sct~JE8!hNnFf12c>(mo*=bG$I?+FigW z)gRP&@Zo1i6fE$;$BFM~HUoUr`gp z&>#4r3z8R)PFBIda(kxc!NDD3Z?>MN+fB)VpYXume=t{v+b2^zQ}ZIPQQ$NCE>71H z)4?M9uRNCV=zeQ+X5fysFWVndJ&%qYpMjGzf^f`-3ByYS*DTMu{9eINkSN^RycFzUXsi<7Z2E+SgM^S|QGOaWz{(zswYVZ`xN-;c2nO zm#IqZ`6Lf>0izQ@BWWYl*fr@(pSM-U)3h&z)7|1~5e`sVi&hN5E0r@CD^O3KXknKw=pI;F0x%*@kNc329 z&!Rk-lyod#v|VAXB5eG}k$Y-d=jKs*f7)ic*#mMlDMTP&+-sfk?kxUmnTM6LUTm8y zb7rFbk@k($6FtsurI6O&W<0chh(nxw9F=Q+jI?v;~V60x|DYmPZW*@h!j zmZ-du^C}1C+r3ZJ+5yDf5Vh~`UsbJz1$<88X`n5l&7)0YFWmxuLxqc=jk5>*Q^0Y) zPajX3gyg=+aWsbxqAe98@rDxv1HLD5-+hSmq*Dn4ow%RXVWgLO5IN+n3%KB~T{O|V zK*Z0LuYK(|*m9&{Zs1D@;`^Z5!cBupQ@d}NpJYsCX*WE$cT9R_6O*fBL-Au3uWL;jHnBo&_?Em7dZYv zD<4|wGw16WG05_KA{hHLHxB%MHTW$3m$90ruZ5`OIX~|Y`U}|l zeq|Ybx9p@R@@bszxbC$Jao?4y_Dh)huPm=DW8RCyzjfu|-jg7ptVv5`3dak#MhdGj z+e&$+$_r}!JyslNAAjnd^zKoEDXcE5I_ZH zFCD^XqUDT?Hru?_%9}P3EHAR1Fg={&W`?_xR*fJ#lVEl}=*>EB7eyM3D`CQpz zLPf`=b3Pts$kWSD-T5nyZnF0@czgxSG?|?opo7ADywdkiI0g$G6mF%A*P8)(@|EnGw;C!rQ=cA*?wje@ua&!xVwX>V_vSQ! zXccV3YaqoNi0qZ)rD2R4u|9Y1TeNpdUgNuRb)OH8;#gA2VJzcql_#cX!r13cb>rCf zL%w-@eXRu-p+e`h2X;w-=X>uCb52;PG?2`6@?yDG&@#gPp)rA0LLDlH4wHgY0 z6>IE%*B0LZ$$R(-=TqjG-LxvkM2;~zz;daE+O|?|GSPa_^V^pO$3DtmtmzXv zZSj&CeN}sQexl||EyWpyss}W3nc-)q4cE#czXeoY^cJD6!KAQ9>F)G;!#{lm0#j_ zn^)-Wl4V)V$)omsX(lqehsI9HY)W)lG>|XrO;I&pjD4Xf=9sHCN=Y3+o2D^s++EusX}gFbuigIwuZS>|pw zT~rwMZS!7x3Ncyak|s^@;t^aN4>$r!!nX2rHNiXFyo8=96E(K$Y>HY>a;#VK__jcl zl$O6~4hwEs8H9Gf*1-+y?iM5dxyRI+1+bw(ePL#&TZUOTbJVUrHSU+9ei*DKTk>&H zVWLUDkslH_lqgc|R*7BGL}OB+R0+3;Zz(dr9CDiqG`}Uq2OxrWY>#!_GVhTve1Ds1 zM7P+SobcmrI6g^@d5xNC-NlB`n82M>ITviYmKOLzHN8LclkXzC0&%{ajLEHK_9vTp zAubRi+qIo*@`0ocbqaJ+0n84Jb-aYTu~AIH0JVh;A+HstyhZjWVa8k~EC!|#;X*}} z37p2f`*@e>CD-FRxO`a(X3(7`_hel0y`0y?rBf;#3|&S!NW3U%=@WJw`Ee7*Lx;8! z=zQ3z@T(F&CN5)_A>O>pLw~qjVSP)t4wDq9C~d{(o52iv3D%M(^w?`n1KHwqCbPmW z*ujs#R!G;fExYwA(YRRmS{N;D8~m-)^5v+QZpEuOZ+u%xK>m=8Ju!y5UFnSVX;Mr> zN^IaV)t=N-9Wxom>@sy>x}xUWw>sk}N~F`Xp18qLJCkeK-=u7s4|wGbN_NzurU$_y z)%0)5pl3(J7C+dlZAtBfvBA^npNhUKhdf<=j*O*Y3qV?RL}l)hs_ zc}ZS5wSU?7^K61$Eqv;?hx;egcotmDKxX|hh(gk|oK0ij0ow{9VB|i%hU6biVDM_( z8?*{|StFHKW)bleh)$Ul+O#0oAXHm?Q}Cn~SpT($L3J$u+F(Rr*Cimvsy~Hn4X^Ak+g*h>c|8;{qW;{Utaaj-C6~|a&`LO zye~(PJ@&yTypIL5PEDEsN0kA}WtmhD%Kkg5{2@Ht^pGlwhmZsvGs>*OU3RMiX z!6JQ<0sbCzK6LTm8~nmX84UAWla)4WAJp`$yneYHf%yk7-CY>?5lm;-3M01v_i*N2 zPY3|-Mvc2Fv-L)by9u`ko%Xj&t61Ya>nka+7uV%_4XH57xKb^)WqEx9y((jK4}*HaY&Hy#{7#y zE0V3gV+3cxq3q~gdODOj;UUc%cNE8yawn25Z!|beny(Xslo|o!8(eOgCv+n%wm#OW zAAO>H>Q7V_{tM}ob%rxOA|E?8JJ^*_nhfTq_OY`U?XGn?wJXBNKed;spSSH_ zlwoxabb8X$KsPJvuy}ULdKr=!IzR>&i$K*}c-tn|(WQL(|L9C7)sPFj0NOFOWWcC0 z`(%4X(gMBv1jfJrCb_T3<9Cr6{&)iuVuDdQt^J9|b63v5NNV+_eFZh^`k}pubK_u6 zePlan1%=UTwLOL3vTh{7tDAu?(PIXN5K1{<1n^$rcW2pmLACunTQ2{+6hdGUs$3IW zoQ++eEx$Ioc0?fEgKqzeSZwk+XlgiZ28x&_ue9N4!4y?=;PsXDdNB0QmXYG^R4#iF zg+@=0TJ!FYQbdNm8W?;Cx#8+eI1h*`WiADYG#wFC$POh|2Wv zp4_W&p|Q?2O#fgS<`NZ@TRAZGkzUzOi`^f^4y{Cmr{qbksbA-XIGe6gtZ(m?dABoP z1x5z!SM^$i9)iz2?Hn%1xBog9?C#(mTbcH7VaAX|lp+OMbP-yBgM@oXFlqbK?M?^& z?&V>v3FAk!VRw4x{nTFUe6HRfJ&71uLoOq*-+U{(_W9|z>B;a*7JYPXtbD+lq~6N? zLRdG!>C{5{$(1X=U$`WkhqDFcrwGCKg7OX$IK`)g=wY{#4}ZeibMi0k>Rjb_?s&#I zBFGw$SIHm!bnM#iV7hQ*YVV~6jDBQ4xE!0F7m=$#5JKSNoI8;q*okbsOEs8w?(rnzM^YB5Z`%RF}^gT)gWy?QE zYTE`YhN@1_)rMirMq(JCgANT|NLJ^2M!W$gMHj`E+^bVL;Q7wa)T}oi^G-kX$ENO| z!a>@NlASs)7}uBr1-m^Q)2vNkr0gui+pn60SF`frq|(zx5is<0KQH=rMuuZxU7u5S znpnr*)}a{g1-uSfvp{qp9fXrjVWvvt6EI}nYOW^#jaw$tAW&o0Z&w-ggtz>2xv9!V zGEt^T`bY-{K)DL9_LI69M zrB%^AD0%PX!WzQFBl?4L(%Hn}6RSokn8VrG)tIDr8N|gWbc+ZLgryS>(g(}=#{iOG z(aMIS`(6WBI-D~d1S3BSL)94tH1IChXUEyx@XMwJ*4r6e@w zkIv&Bx<44zt-djs_?p=%^gPq(P$(eXK784|ds`FmewqUb%qt(&7tPRQ8|HdA#)+bN zJKO0vMSnKllL*z>S80vh#jO2+M4)A(=V`Al#HYweEtGRZ?2vR@*4w(<7=jV6QF#Yy z+R~=R;*kC9W4U|D`b{}Eb8nP#m}UE-C`za&rpm%2JdV^M*`Vs@bGv!XLO%w17s2h8 zO5BQw;G1-#+VbQZ=j+cuNKK91!2s;@1VlR-3Sz`Z9+_CXcsa^^0loZOQNwy1LjzsN zWF>b^ho9p6dQ}guN?I`t-18O0&dMW0MS=4A{@Il5(f}Q*t#@y1?QA(W5uqS*+0u~h z4@vg;Dd!ymNwqs^m$rHb_n{1(P+Y9rbJgZV^ zV&fVVfdeyBe*U*h8O;DxxT+>yIy=PX3|u=Gw;LjAq_GRCo#bg2l%l-21zd9(%79E_ z3BP=qg?3V1;lSidAlkRH22j$0*+Vk=3#78t{4ZfvAkOkVAPSezNIx$4y}!7fYUso1 z-?Jo>(Yc0cW$E?`Bpl_d5HqJ6sBrAi)X7PpC4bcdWagz(!wgkgjBpliyYe5*&O1&` z#I!p=odpQZ%a8{h@Ha-hEVMN~kPaTk%nOVtZRo*jGoh3!Q{oIjHW> z2R8$d*=ifqk2vMtM~fEzVQun&O;vt6DuSCnmfy9J98VhE;i`LS2XAC!$ZspeFC_dln@7vji$JfdzJ~Z)m)M=zOA_9BsV0vpUeCPDZM%i za(sb`LJ%uJEAjhr~9d|%3_m&ZVyeEqNejTZ{vt{u z+&;>`V6ccQ(vVIRj)48x?!KX)#=|k@?efW~p@d_h$L2CB`J~JgV?+AF$7jzb$@+p{ z2M+HC_Qn=&>cF?U;DN;v#{&4)@lRL3y`o2yV(sz?68dSuIC92A6^G+{YxgKxTE!_0 zYq;`_ho(s8_(=nFWPA*3zT#}3%h{6j=MmKa)GAQB-C>NwyO!xOEFHT*8d6=ySYGmO&KFH94mpL>7dcGC7Og*L*-HxkVZ z>O6cEK_c21+{D|bBtxd4^_I_5O>*eY=95T54KA^@Peyx0f@@j&`-zWBYF=fL|3+kZ z`}cBLHhI%m@J*NX;|M}nP43DSi?gDM31($o+~|-^Ccmm|1{qq}fP0lVGMk)W4`6i2 zv-%5RHnPIKx4y7%E^LXFiUlpto)jv+fagY#6o`lSwenA%Obkhoo$YLGU%G&oLrY0$ z$wrApiCjYRzGv0h^alD%#SN#vr*CPV5+H;O(}Hh@R@$(WdiFM(D@5+zkOc1{fUN0- zjlERfFr5_TvZE}BDuCZCp{VoggT+?bWua9}vP^(holW1MxiQL7W%*|3-z7PCU|ZpN zvTHRpa8YahXy4FTO%JH%IB`wx|CP85_faVRl#dRzaPlT^6HkYsx~{XDK*>wivM^!4 z$xW+$x;p&rn;yTRjNFWU+dcr>z0y!Ks7K$rPBk`f_M)tIO=&RpqSXxwxJPIaDy!aIXLZ{2iPle=u%`B`j?z_z^BmYlBW8WU#r z<9kd8Tb}3KxNV84^^xDG0^pld+78;suRO)gpD2pH$x*{i&39YRd)u$7Ftv&*dK30n z)l9I$sr$7TO`)F0>xc~(UNX+3o}J_x$=++eBMNZd0&r}XRw}442iQpySyibfvKk8* zVaU(0Q)%Od>$QjT?Bo(N`R|dd1OOelW|UQUSgT54y9+z+F+gqOByTxt^^`Ql+SJJ(WGN z`QiesmFI?hD6W8+$+ST0$vd3#gwwt>B3D?v#qrJuA%*YNUr+=MC0xM=6fhf_cBjt$ zS&`K_RGh6%#4$~Nb64DOGJS{two2;sKT4<7KJebFPe zBbKmgrzSU@P>A@gP=4U8zFP7Mr)4%1!AP)?hF@DUn(?~Yx3v>y-&~tip^zw>X6Uc*vQGq6J?MON%WRavdpB zGn-R|q!*FDgRvV|VF0IDEGqWq^H z4nMy((<$1NIcq~ZSO!#-VMC={8GmHKC;eK^kquSZT@?6pqeD7dk9dV@qmNeeaY#i8 zsl(TT&Fvv8b+s@WKdJp;ADD5kUA@$A;Cn#;-1o>DG<1kPr-*o7KN-5(=kYu#KIn9t zSf-kpvZHEcG{OHuw$d0(|5eTU)Zp9kWo4@*g@#z^S+|lVN?%0n@bGm;=f(w}Sv zdih~1$5)XBjGWoIX#}N25a(mf;Ir|5-pQ5`WlH}TT~LkMx)%4(g^lo|1;W1Nci7Uu z9g<0wZS`dd!tB^n^2)|3$qwb#=h@1Ih~}oV{3J4#!=M4Zb}K0%dK}N@X(74ujuxRe z*`Q~dTr-GzVv6;#zeB|JcQ*I>fk)-*k@i1yo68-4pKEv%-+~{#z1Ps4>Koq)=E-Dd zwxsuVyzahsJzcRmqx(W)mabF0(;6qiJ;gu@=25M9wo@*hZ4!RKdK|+}Qo8Ks+TWFq zGasG3l>vQGLi!(_fM;roD!`_)tJNgs54y)1tsXkv^R?O&%?Cv$QLPxY){jv%2(8cT zZ*y9m3`GVF)(zimE7OAL6!Uece?tG!_1I&{~IJvx}?!M;y z;^lU@TfIzze5n7pDELb)qRLp+c0oo?n7zAuF(22Y{tkH0?* zF=alf{$gd;|DEAmXw-P-004Hls;^uBK>Lr9m8Pq~Z$f?PP@A-NrL3 z`Y4HoU4I~A!ag%tJah64=M8T&0paO_@acQ3kFevW#Jc|z4Eg5Ib+_zC&P0{^UEW=u zV5tz}pTOXjPUol;?+nJPK&mDHM6<9SD^=J#7dMdE+UY>8jN*uWo~c3Q*Vrwjl6rb} zWR;JxQw+3nAJV0n)X1^%}5QbMfQS4;VX zuTh??aBoG@&qo)LT^yP7&YhMF!gE-k^9U(4@mKmDnz}xCIUK~?Q{10KPM&dPeAWCM zqJ}s^#49sf(NMh9*g&6a5}wi3-JeQ~TD=H!&UAwLrK*8{kapl2;YIByKN46s>xf8Y zKv8er7xN1OT)>_D;;4}y^xXpYcmW*EZ9^UXh@%v2Z##GT+yt6#)?XBJ)kyr?w8q!X zQb~PSKEfn?;Ya$rW&hyqh{%Wzi_rA_!u62$f*Tl2Zb+|VZUN?i5y}I-R+gwWS&UE%^@c}0*)5UrQCr6S@%rXCFC=+NVg7yQ9F<4JXdMT(CdlhU5Zg%>?)4ea#21rC9Teqhe zNjKft2M+tT17qmE`e0cI*-#ROYb zDH@qFvsnScj!-tr+o@@GfG@_80*Rs5w+nrJtkg^{#PNtNuMVcAs-_=Q2yAhSWyIEGmul!iz!LtdDM z9kyh=!k%I~#mRvhlkQoHWSjtktbZ_P&@{_hH`D7e_wgfe%0LoGoAd!4K3<4Qk>U>+ zA|KMBJ54n;u`y~gbE}S#mz98NSx8NexMAjb^Mq%Q^_G4f3X7VJjQDrwAdVxDdlFKV z1&P*=KOfb-^n85R&Y4#%_HhlIsG*|}plZT2X~9a_M5qXP&bND);=ha4p8i@6)`c=( z_Rd``h5gbd`yM`6wC(>5^{xx#oPrdkLsFvp2+Wo3f@Um8I^;h~uN~B8cOxTNB!rV@ zOG!mTO0xnpONQrzLNmc7c9E2GmGG|E*mvjEQFQ6jz9>WtdJeGNRr408%{{*xmrpeNfpuUxj7ukKPe(1d}Z)@8y;8)?G zu9jeViG;?uODeqJfb2>saf{? zS3PAuBR=IdmX;Mh9XG8`e1%oBo7gE9??cUdEu|m;%!MJ*YQ%tjj*nb(7yf}6|HZ93 z$GDyaHHDM{!Dq#?rIPG2GIN0${Cpc#fh-Jo%}>hZ;cI%bq(wVKLEJeWmho>>pA{ps ztSC~9wkxxfB*X5t4^l*|SP|yCRvCL)V zp9N#^+4*haPV@<~X#hA9JfzOhoX4j^GzYq+Z)jM(MFwi`$=RdGuW=uOXe~asB&`;6 z(p+zi@fD>N)`+Cn!^+Y?(-6&fNU$!zA%LaAW_jAtSXf<}#{}INhFO%&ez>W8 z>yR!;J>4;@M;)Gt8__w1jVq7j~Pi0|nv!oevO92|E z>?RWs7*<*%6A%}wfoc(y3$o2|YDpsb7DZ1TEci+o5g(vFq;gdO|{%H=gDNoJ(($SkJYb|@9Ke5Llw_@gE z`xD;Mg6V_Al??oCDkF&nUuq0_D;DESh2!z*=O=s4b`<6miKzC})s+xx<70Y8RIq5g zkUyZr%h!D5W8V`0a3}d?S!q2=lTyL zJP{r`^H+D>0JGuBDpt#4EMa23y}NvDYG?fF?^Y^e@b1%CAg9DnG^)*+(mWWTH0h)f zgJ`%V4@ku<8H?nQHgAD^1_jHN70e4;W!ozFYm_r5{0|&{Ek)!tTro;`a(L6En2$EJ zM?rfo?aSkQy6$TdZKz{P2phSqKCa-yItgo0tqv{x_7ZHc=GzSqyAaAJZPh1xZ43$f z(&rZpQ3;G$mP(aR>2tYcdQ8N=@ydXljw%m)_#*Xdl#kDBiSNQ>;RD7QSm^+ycF{fK6tL^`Nz`vVsVX-CmWLz5k_kf9C|wA_|5FF6KR-3t8Q zi;>swkGoV;%1*4dIGq%^>`A+8;d#=$CgYN&R~{AW-%w6ksyS^CKTm2+ArEAKoG)r! z!t!nt+IY;Fc$Bxw;^Y&4&KVFA3A+&$-2MF!zhZLKB^16+j`j~mi;pHy`<{(8$0IoX zFR>Z-HyYS4E!gTm*aw&jnntw? z`9%91_y;H>6@>jJrW^AV)A zPY~ee^a9CQtP}CM8bo>P^Wq`epG_~H3qw&|5Bk|tA}j4Rs?6U=xh8yhOKPyI&55jh zH=)9x%nkk}hX4vGbcz6Nhc}_LL8waf;ddjZgYz=pI*DbTC>gS&%k7*HQ^=}4c z1x_B9rco=OjNE?>@&Cy0Gpun~@{ys}Ouek*M=d_%dZWvr9*3L!Dxyt2o-}_T0y%V- zEndTd`R--J_EW3IYsrSWqKgvSvVz_@mEbe?(I#ldoC7Xo0+HL#Q>S^~A0HN$1Wx{o zNUE6Z(hs@efS8~D@g^uQd0DQg3E1Hp_vb*37q&p{FKky2G9_rDapND&4 z6zCgx$CQ&V>EoI_*&|e0po-ph@!a{sq}(p-3=QkB3Vt5cdO?A|%e9Z9c4w;jDpbFi z-NWXUr?#ylDX8tF8w8@RcdXv%JCuN`CQ%Bgo0xwPSbSXM0Yle&C*l{cHg~QH@He%& z)}o35Las$mzIT#)=CQ%lDevWlkCK@Xc?Yem8OFAH-4Rd&xgXucwr~Sx$CYYc=KH|x z@XT*GnK2j7XwWLzh%!q;qKiFZ^sPzI5N1fKFvxL`_``IhDnw#$qe_@$pra?)B@eB%35WFzy~5o zZl!S;7V-k*jFW^3IVEMymGGI*${h1o`(f!~p6QJ6Nc7wETh)0m_w^r` z6EOJY)^HKP0d|8J{DYgTtD;{pG_;$r*CfAfO|cLh^i-*C@~?uAq?c1Rf3#kU$|XhQ zPl{slDyRVtI3+XHCXHwuJUr{yELQEEKg`tyt*`Sdy|E{chDKo3QbD9t2IKB1NDK|D zJdHf5NnzZG?V97qTSNXTY$NxK&F=U^q}Yed*5NW246F2R>xuW2&1A`f$Wq)eMbgoz zmu$Jggq3@J=R&?LPo$oTLqOUqr2I+MUjHaC2Z<0vwDW5Xc{D!g44p(KG{9;}N0`bJ z2^CTBPA?)MegD@NlSP3eyw;$CD;#grXO<@*?XR8T-oA*vd$>23z?I!D&PHgdDk;GD zpaA;Y73x7V$y4hS7O5g=TPI>RFgb$+Mts|E2Iw;^cDl&GP+W1VYxk~S3Ol?nJ2b@5 z`*F+1W=~sFzjM@LPkk-(WK7!@-i`g!{|LsEps)Vc$R2KqsEo$S&TzsuCaqt)Gbug&`HJ*<#qQi&y;!G2Oe# zfBUS>F`HL=C_7#fH)mF$8);4X>S&0okoIIxBR{SHTGJAs_$;F8rmiPcdMEt%AP+KV zsJQ?V2`e6ha?vSwxF4Lz-cuIR5Iu#ddQ6)z3m?R$A|)c>!fpiW%ahd~?H@mHniS1g zujeQX(o{ZI(yit-?;^~AO_PO3o{S*TJ7L`6Bodd4$2W4tQs2qP9*`N-CGygjd8b$r z7X_Zxhl#v|)ODMsMOv;)3Ck{0ouV>YxT$=l^!j7FeuylaOa#01Bmc3MD8XAps}FQh z!#}qn2`5hnZG_!kt%qP(icP?74mK4eU+Fcjp9A5YWlN`cr?Uej-T116@y?#s0yYjN zMhx`$b?zT^Yt)F-nXelyg0&Kl_=Bkj18C+gj7Qb@8WW13JYVBf0UOCrgf{wWrv)>EM($shT7^$4sj&ofZ;m<8}sm+aGsiX~Dr!i%tE^G1&~& zJgXF_Q}N^ZlAvN>GSRW}Niiwicj2k=NS=I9L$#fNBv&MpW6mF;b%BRNYGW96#TKYF z+lWD5Wk*}mT#m~}5b0Ea0LFu6w_0`DaO^>8cCUOdX6XiNC?I&xE zj3a~QSv$Vw^O$!gMl9y9Dn#zV{j}m~s7;GA@@&8J{e(+Pa8qZRZ|?FzN+`BRUG-`# zhODHcd;ZZ5&97f)cy@iS6z_14OUIiZ{U056P>G>XQ4{KWZujswz>!}b@8u(7B#i(&s`Xz-7jS(qus+qWp z)u#W)B7E(Wb}N}4j_cxKJ2Kn|{N}+4+&+?>&{l4$)#SwOVSRilg8$^hJY_OZizZ$>m+F2#y4)cJP-m0?6_hu@p0${+SS)SqDyed zCF_aIIV08w4bl40q!9@&AWg;$#7y9Ljwv?z#;vKJ*DMGvr)ss z4n$Kr=v}+Ykvm{ZIL^qlsfqJ&No(3I_6bj$ zbUAJzg23%~|5P6x)})epD_m4Fzb?H3I?r86VRZp9X#uUdoQDa|-z(o_{d%LR%Wu&Z zn#-HZbu+JRIdqCmnHHKJO)Wx<^d&A_(z5i@Cn?lS-=vWpj8QZ1U7ifEVO~e)JPBmS zox>hWhkIsYc7FMMSf8)pyHK-{6XWTQ5%g}8Q$~!g^H%Fmq~ZDf;%S1z*(@czO!{bA zfAfgqbRgDYJlhIVTr;t@*+cr(y4N+;qe{%D5?1b*(qS{=$lLDh<6KrE43d8ju@P(p zWg?N|wZaMc;CnMgi1M5NuXk_6Sq<40*+6ABx19R3A$P4L<}CHl4=vb|D{)Y|jD^pc_|F zs)pR87F(p9u&-&AO3S00b3lTc1p!$Z!ai5vA!Vi<6*pt!Qlo7&zd5qoXwyT$m;$Tv zNe(>&Zq4Y9$lQ@cm+=7MWPdE1OG(eVPU(}bBC9t7)x;dhr+7Cc#O;YOP95K7@;ai-WC zyXb1+7Ktzv^KkdEIrCv$z>V#f;p+gSq_ zh3iNcqC@(az3)$H)Mc>SEfZKKPfEM(Zf52wTl#{30V7kK@l$y60)!&v@|PuU=lIy4d=}cC#(2bbQYP z#JiiWk9Bn+o!idHxW`OUli|}oDLbhj4?~I+<=^9-CL4%AC9zD^rS_ZBKg7O9 zuVG*p5;}{vFfgMShLs*sywpsQv9Y)4?mPdj&n01&Siih{9V)P}eU+Hze}VA3{2tt#dCH@5y0bm_e(C-_ zN0?CIz$a0->lz03S3qFVLzk^i8vbxJ^YKO8Jt-MI&^~qtJ1ywKSLqR-{Z@9J%iX@k zr^~>qfEt?2PX42M2q5PFlW^@*1~+zymmfeO>8FcWTsEwtDNgMSzQZ7trCXm@K|e2R;Y) z`$=@wLu)A5#TtnIoKx`p8NRGhbFfjgtn0@CB~{_OiJkjHw{7}{B( zKi`CJ>!mv0h#k@coH7G(iXn}DBUKvSk%@JOURwPgjGi{tokpDww0i)U%bpOKSL)%M z*j~h1$7O;3upx}Wc75b`+jI;^vYmL~q1S_vz^X?8|38?doi#`HYXHFoiMM|`s%WJ~ zLB!})&_9^ItMjh&H&F;+=CbpZbvP%q7b$aV=vkIvbjoTFDl}?sy7Q{>4~Dw8Vdetu z3veA58X5n8C@Yiz^f%7)W;cu@m<^=%2faK6tiWxGXZb~2@BDa|e=q}D`~P4N_t^UZ z5fd{Jm!E)5H<;f*|6sy=AM)8d|G^yU$Gnk*_QEG~WqZc(_VxUo<7xlV-f7_5vM8eg zmAkwF9vTtH1N~Dd-l$)zha*CJKaf+j?U@ZeGbooLJ<-)Z$vhenry@c{KLKyW&=w)k z!M4fBCq)TsCNS64v^i$}!7xDn?lckliUI#?F| z{wio@pT@h+ISCK}MrE}c7w1;e?mwXTsrv+dLl|f~Nsvjkp3SH(&NF;Zzf?FjGbqg4 z>Ma4fxEeQ*LiWG!NyW~l9||a2&_Hz(XjL(QAo`=ghO~Af-VxN^Bk0%+7AQC!oQAbw z7=f2fx#ga0tO>V#fzoLO6b$(sk-d+-<1(494bEGb1Ne^49$OE+m(lvHQ7#2^8Xi%8 zELPb4CB4!MoFg>ZseOW~HyA>stc6=pPn+7WJHMJXYlBlBt(xP-%fH!S#K3>+O4ypF zA%~2+@~qt(OMz6t=6?m8d$gzG6N{TJ;otTwN5|D0mz)ojtDF1i#&cco;w`-lN_4Znc((;= zsalrKM`?1iJGfAam8>3$3?lCpy7eyQZl*+}5;%T=OjXbZ0pqP!kjx%CZr*6vUxV6o zkd+iqdU(QYTbIdZ>HClmcMl&)(eE3uLeQZ$9Zyl!yay|$A1Cf5b-xxXo%kxC>$eu0 z*_MzQWxnQkHSmKXX<|tsoBa6cqceo^*WO_mgh( z^)#3(dVSQoO#Z|3ZL?Joj@6A+A^IN-YvL~fwe&HkIzt)S!VAh3`flgl**u`7N9ZODC|djC=b)FtVm!$-C=%`l!CMsBOC9nTR^S!dGG>B|p@87s}n( zDU!LvRqnysM~k04O*zWIKdOLTU5;o=V#FolM-n4;9F?FLKhZRK@(H8R1 z!oaZN%$U4Mbr^qrco~c?;zgEzCQC5XS{C5qk{Ga><~6j1r(#rx9}ifJy3nuEtm@*5 zSv-7{A@Gc2we~3Z;Mdx&#k)#|ssMV&#j}tR>GW`|K#SH#GaZ@JUsNcy;Ct3P>7YrL>pm28%-`P=Pm7Qq z#BRgDqZaxayFx#k3Id|!!q#!;gPPJTm+BrzhnYsCb05eC@6?u| z!yEdAONsPC>iGjdunJakk@X*X43brvpVo|mIJa59v)q}Ik;}57JaFd-Qv}O( zoWQ@3>)Mnv-g`Y24Z!kwbGfW>7E&|R$W9n!r8hB~&%|>ZF5(_4?zPM?oJTqZLCrQ> zGw2+4B-qFJcmRCIUW%^7$5Ki>SQtJw;A#vXYthdg+pCj~gU+K!E{$u7Ba2<}r_!RP z?j`ZCT#D5t0^9|=THREQBz2cY`Zizy9n-y${zakpTI0HsF=H9B5Q3L@e>}L)02f^# zVg#LfNl7NB5DR(;vesRBnczwMD)V$S8u4F#)S5Cfj#$WR?PiK6QCK}CMWI=*v8*rs zWaBWGCukNgLF%nc!s05})~x4i5n!^upjDQ)(4Rv-yT7o7Y;MR=rnldV-bc>p7T-FG zHnuGTUlSI7Oc+Gnp;NEs6UOp6|L4mBiJ*-te?w*`Cv4yJpd)Haa`(xKTPlGn^`@jmLAj8=J zkLXWNHU%mdAz$Y6x#5bv9mD}>-XJbsIv=3des|;(K~(Ki*90o*r}s}W-S5TFz^$sQ zI^!=rhw21=-@&9R>~|0)OePl!LrXja8zy_I*58b<)Z8E;5RQik_bq&S8DVb6lcfDD zAWVRi4D09CJhLXnW*hD*ztZX}pMs=n7eC;G!j`w+JZ;v$acZZl^JIl#Q)pMNVx7^A zpOPrFUVV>m<6a+hnJN8#1B=NRP@Vt+IA3C^x7GW^=Zz#R;X{Z!hC#@p$#cWH+QCh zd0aRj3;u1fF>ji_`xiRpC)1G#597syEyibhAGQr5h;4J~#f)$j4*JSOrq)h&RDqA& z)X(9h!=#-hlba`Y%-^s%tO@1Jsx5JyhjucqHgIIOP%jrl`hotLCGvGHY&gs@A1L%^ zqEiLlZA~5Wr%pO&ewBYolG_)WK=z`4UfHcqQ68&wDcwDsI8Iv|1vrk~{&@029;IdV z`3IvxwH?)SB*=B`0GEZ8j)c!>5m${cKHtzC4Vb>DXra1pAPG1n<<6Be-$pNX=E_UH z7@aMSbHN+usjPEqjuTYOW5?ATF-YfX7<)b6^>rInE126+AOR^jyoQ6EV)Z&D{Bb$=0v z&l7%1#?3wIKz>~!8$C!HPZO=o*6FjE+4kOZC-6^ep1n%ZPujd*5rbZVaJt=VB~JyF z)2Z#TEzEH{_Hy}i1=%nU zc{^KPw+dk97jonK+9$!AfJ|?~21wvh_jfDEB>UUWQCmFJyfz_lM0NL*?I?#7BzP9qrRC!N!dhVUFfR;2R z_Gq{M9$mbY?_eTIipzq`0)D{bOX#d1^6{-bGq6`kRYLE*2NdJS9y70~5=~d{$;SWn zELWyX2n{3;8!aejS}NNeFtbT#*!nvu&d$7B-?tW(5U|g;1D1PR2FSBR#NLYR-k?ec z#r&uLU|vZJZYeICF&6&8JP3UF2NMfj#JQg#IM%TZpfATl$%cfqZjN0O;bfAP>4y72>?e9f{3K#F zqJ=a)74Wq&&`_aZ7EJrnKM6Hdc#nf#*oh#2Nv?qpfPXO4nq^8%M7wpfE869#1s=+I z8Kkc+2WcT&wN#{mUq^u|5IN}HDQJ%e>;|4gAG?hQMb4WA5wg)AQ)yP?qa_LHv-~IO z(6z5EJx7_~CXu;9k%>}z&rmK8+?+gt(x-I|xJ*JuP->z2m9;2!y$^8E#*F`93`8>5 z-STh=N41?{3~J%j#yn-RwaI;PT5msPoI;Kus+UmVtJe1nV-hq^s3h=PnC~dGVzvrh zt-FGQ#-DaLT+$8F&4Wh-Y73$2-*}JcPzl!L-j#dD-$#sPdZ8QM=~ckA0JY#A>f>|; z9EV1Izz1kx0sZLdhh}|XR{`~Fs`Zc-f_MB-jhIlReXplI_?j9h@w5ZHfZC@4Zf<0t z+Myf&V1Uo;L^mxv=SOZ(dLP&TRFSWr$1KK!vZr~Rv@Df&A~7DDw(~0TfF(3{eOfd-P4^@NL~nvq%eoPGs)^+Nxj!GV@cE3u90v z9MXHx<>oRB7nT53!n`v;n^5^3H^b~d{Pgi340OLjX8xO1;Co>#x*s{m%lyqECEC_? zX4-A(?Ty#em&cs{ni2(q{=Nr7$1EJFj%K>48~yX6S@4M8aVt|S!(F!asOLK-Uu4|q zoTJTmOz?j|Kj?h7dmZ*aJo23NsslU5R_dL4(nU)GJ^Cl($iJ%i(05R{Kx4vS;9tmS zzL49o_g@&x>G5l+L;qPEI$ajNw=tgV{Z>PLno#06$0bvQ_c zxD~Qe@SlpiH6s0l;`*2!jrddGj$o@)*;wwO|Hdd8KegV}Z_va0vq1Y#T^LONLI-l` zLWcJ(Xw-_VW??pQtv4XPf??yF3wMFyZ>b0Rgmf2coUX(tU5H(QG2p-csfxI&YKxGz zm#!HQJDL$sy36vpuAsg@q}4+1U4u!hD7YsfqSL~@SIwqK78-*tb)hYSN#T{*M7P!6 zmQqyvh0v-9MvIKg#v1~pu)c!s{i+SAfhDGfEi45TnxR3Qtn^1aC#4B0%cYi2gE9V( zb<7ym&9hJOk|*F6dl}xIF@>||47tF*xff}~zZOO!+&V4?+0U{|88*a&J}#7j&xN|* zQq053x5zlQkg3zAdzueie;Z#@9P$gU$)zYQWo~d0|AV>3QYMNGSW%$6dYb5%A&n1B z6ry3Zc0X+3i+fb=Y0eT{c_Vsf9(^BrZ6JQ}n}<6+rkmg}wR)mcW6$W#I$E$itxlHi z222hD5$Z$cZ2Zl}5*z~iu3wZ<*8{vk}Y?5etYt6GG}*(;$y)pj*MVy zdX6z5;E5g?;W^=ik}ms>%GaS_bxygn;c*>@i2e>l?Vty(1IhdpT^X6R@?uEt{efLW zq%(str^Ar>qdS@OE2GTIlNK&eJMzv^j*YafKNqL8sPy;=aU{M;9rvVp?&JReUW!qG z*S~yKXOy>C%BkGqdm(h~e2jq3;4hq&<8v~bM8;7#9U zdBgCag{^&G&2(m*WziO_i%HRUNY}VgS3G=Zql5=n2~-k>#ks$C-D+~*G1$^TLNcME z(%TuzVR$TP6oSacg^``2i9_V&M}-deyOaz$<2(@?k(cCWeXvR7k{QTvEnGwG4@&)= zuS(`Lh-UrcKOW=rUGdn@9K0F|r}m+uGg0Z6WWw8@6z}cHY482lPl=m`MfhRGDwp** zKIBw+fw(eb7-Dw!2MG&&&97tZgm9uk^j<6I5!hvG>atcAF`HhxrR9Q%Q7kUnResX7uPC&nadMmh7-v2Of?9Eu(`E-dZtdn+Bv_3=gCcNl6x-0_F48vo31)7Ls@IF zwCk`ODe`?ihs|@HHesdp1}00`*R~m&^4ALl0=~}7>MRTnmTtEDI9?NEXpCP3a1_=a zayi_2N}oqQ`&#JiEFPZ3&(ZNCf!HK9`x`~fKBwM~^HfO8b7M9^USVq4(!Xfqtooxj zMbBd+z|DoxLeKD@PJPu9NafP=v$V{M6646`kC?UR{bR&Zf}DGgbWUwc-oNXs$2-pl8fJ}QW(vYnXOqsVi&8nj*xM6Tk>Wu1=63GHfa zCr%aFS;Npc>>tN-NCcIc80mm4Bp-DqqX&mC0cQ?C(k%$WyJNdfhT2TUtr5%#`lB;6 zc&CWIAw;2Zp2-{l3HBp{BBFSuenWAs9P1}Ql026a?zCo-;V36^zdi67PWrSyNr9(= z5^&|VD?3qa2INW-;9nFE<_N{*oTWuh!8M=iPxrta7dsijSl z;xB@gN1;y(zUd)ae`xO?bCvIn=8GwG@@;4-LSDoJPL++iDUJ)@7X%c^xsevrsYwiQ z&U}Pgl;q>sl7jR=33=rk4-Fil@-bzLkO>MHXLZxKOO0;HRWPT&X(@JWmMN-v13=bL?^yu_w;SU9gQ}FB&bLiDVeej0SL+>bW8yc~p%jq(LFFz)GlT{ZqGNUD&HkI^K9tna9Cbl zK!vV0pNlJL%x$i&R~}JEG;!OjIjs1*nk;g#S+i%bT_ZE5;gnkieHC|9>ABbshh=hP z%G=P+E_f8|rG2n74f~Fsh&~VFc#x2cYaEKAY zA1?P=s2u4Vm|eSqRjrdYd=|VOKvSGJJ0~xe#@G(LYQD)=P{bdI@_m=Ebr!XywiLPK zWMVqGu)2JCxhi37sr0_5@R`07&y=3Z*{@-lalR)US*zi59YjfmoLt-C*1B&+YLV(V zLJ3GFs*3B$7MD1WACjYdwG)>R2ZE&au&An_Zxa;~SsU$Zow z^aZ4~=Wu#Dcy@4m6^ur0cgYo+K@9`Cte>A(Me}=+f`6}+OmHxIt^j|ucDV&w)vUK!Mew+-vl`hc7k?2-BYIe(oL2s5?*-aH* zR@_INDJF#)rkKg?%^qYf6ay4f`&wrvKx_7s-36*ygt?vjqZYd+iv|-%gecgh27CeR zB#z1snaVTbhP&@IFv}iN6!Et1{S<1pRP`x*D2;j}dS5rjZ8-(2u#iH7rzQ-XuyCo9 zs}4Q7E!vDtd}nA?9cLB^ab|*G3uI|)ntef5w#pslKA}ke{=?qnekX= z8994LA27Y&7cbp~KGAG)O;OIXAS@+cKFc-T?S74t?{~mYzwtNv{%H=qf@6`zt>E$% zdKsqE^PucyN_bk{g} z+CPQfp;&R>bGH50iIOT4;v{&fP|dSAFb3ZN0^H_1E5ne2>rEw$M+2YhN`;4zXV>1_ z;5kV{=F&OxE;7;5_lDw)y@*`nth8Bkr!uq?^j!hh@a(T;7mbcQlL61VcPG*otWZpyHD7(kQJIy^n2akEF;EVl~6iCAoqvM;XO{ z7lkH7Zy0+BCxutR)L(_Em|yovT=ebGl6yD8zzR2 zS?J7v2tHW{C0i7^J%*{I1@@Pfw+OP=BxJ6NP~f_6UbtlMV@=ZM%z?5ttB|eDBPDiN z`fE5(l@nY!wcA7fA4LuvQ&$k^{0R=H5e=XU?K;RY=tZNx)hS6nf|`2@=xtQyHddeE zG_~rsmqzm5>37+FJDCj6cdLqZt^h;*P|c|5wf_KwkC*znn>B2a+s=QQs$_lt0Ghk? z^m}#8s_a}yRD8bWdY&`5pu?b93Kh<$RiX*w=7)dZ?z>TBZhHmiEm~GdzWvb&p(+Ox zYIEvTE=3sB6ih`8?e3= zel`<_s^)m7%okPrI&X-_v{wSxo#(HC8M<~_FrsvLu8fWoy$czxX>QO8^P^)R-0BNV zJi(0nZ)M=e44Z7{l4;v55aS<*dkUj!Fv*LknrYffyXiWUGEUwtMXKPoS$7aAbV4?% zfJK87CZ|^^cDIgWm=HKm8t8GR3=ROH^<9#H1#+jui>7XXboptNJ0@Cj5=~yVleBmz z&A?5xM%11sqHMDG1$J05Mch*I5{+96;pGxWI3+Y}q`02TXHN|O0K0Jn{MS29J3wmH z3H1hII#wtMaw|K(x~*$&kmX7WxvN;ygYu z3R9}XQ?gW69JD*GJc%ip>OooAQNa-gl6r?MZe?y#>wvlwx48x@~&t0xubY-!KlF6 zEHwI8yjhzIM=6G-rAMx4IXLeC?_r{S52EPt2XieVFpLFb>2P+>TrmTxyB0R$b zpv{Q+NT+}wCo#=6 z!9vc>&Fx`%+j6vOr)5(cPjp4nNNFycDe`9qG2T{`DciZaN>CH*&o>7_DD+?!p{ zPPs@8rJ~V+kX;L7BhfL##M(=}(AcoCvH&X6*qOD0!pO;4N9!D1nOjj^e3HJIj%-6YcaD8*BGTp&A4$YqslhfWNRBfFrX}ipED5na>XldH-geV!AyE* zAWIMrPPGgM%?T|Wu6|rz$@jE;h1Hr{^qp%b9m1N|oxd^YQf1k`+PZ$RpwyyoEIu5S z`W41X`fn3A-NvMLMcO>Q*?|VB@xkZ#dv%m-zsoGJ^Q5kJl4>{eu=qh+uRyhBi^0L@G^r- zjTLt!J%E6@jy$E0Yk}?EbS)lnk%rTJ(kn~_f2gs%IebBW>%4u4YLRtcKD`O;-7qkRkO=OL z)L-DkK||9SMtwbvm;J0>N8fu1hFo#V+H&~kah*wR5=j*IWI59ND?W)v9{i{qqDL#O zluB`frM?L0Ds$u&2Hlm-W>$=X64;tuFs{j-xq&~$ZVV(XF|iyP2^6%NG}W=53~g%z zy@KB-QP_4gsPS9}k;)vLGWkQ<$Wu+-gLDm*JcO4v?Eqz*zT8TC-~hq6a+T^Ov35tB zLaKd)Ln$%SiiT*|_j!3JIHoUW29%Kk+i@jmNhNj!{S&wdV!1aj0q zKT@_=jmhLX)Gmw?EkD~&u^mbFSTF~Tjs&gdklA{CZzQx)LtyKX7lx7rrq6hpo8uuy zSh&3ut5nLqW+aijfoLq4rSSM_f18~gRv%WDKPj`5wYG}J`xdG8B!)SeCG$wrs7VVv z#0o{29$R6BCDgIzd-k=CuXJODN!uMe>@4dqE%KX5bG30Kl;P@+j{;8z?vcV;-5N=9 zt=F|)FoYGM)g&3G5iL;QuPX+XrpJ#M-{`!#WpZ{$Y%3z#+i!Je=`!;Hbc(3#mKj>W zc_m&nmI(CVTk00@-kKn^#2sIq;kE5!xm>Dzul1{WvufMbxo$d>n0X$`jh@5tK#ta$ zANwfL{Fl8cha(lQ0?14?EVsjlc5DS7MzA{&2O>I(58W1l!r7HE=Cxu?FHxNCaC^Ts zWzcom{8Gobl~vPY4QuI5gD{UXzp6M~Wh!W8cBr~)n*5P7l_ra;{6M?qB+iUvLB}=_ z*m8|Mo?}a?r41wo+j}~G7I-ZI#TI6ULCFeR)3BPwXQV~Xd?g*~C;mX^y_ zMg~0n*wU5f=n%~bV_F5JcslH@{{U-rJ<}|1q{flkT<{9yW7#1q z(p!^^=VRnCz~b4dfTh8(UlAeCu|HSIjUa^N^CIs`LdW--wkZKv9# zP8j9ed8Jmy)yy-wk_s!==j{~7S4`_{@U<)*uK4H!dlkYwLF;aCTjZw&BQJnhh!WRvN!l{aOFR4*&-w4sT%Gy z@aipd2@JK(iQ4;e7ujYYGK+gCGqWRwpt#XeVjcL6+d5 zGso=L?KR>W;d5Nq+lsSm(^DH6H$hbLUJFR|wb|px+0l+P58l(xn#b{S+UmZ2k~Ry! zb#&<*dS{5>7qNvMP(_1MoMUOSJGYf`tf_oi+~ew1_pNh0EGEP%%IAdHu&nzZ*_9OT z0ff?!6fSGPVL^mNoc+={3G!1+?awMEpayJKzN(+#qyzJ zMn(vCxIyCNjAt0#+SOLk%Jj#2bDNDX90HxVi3O_G?eylS_-?oxX)(s6#^q>Dtw395 zxG5eO5M^I$R=r`n;@~QNU~?o69l=i@lw3zmDJF|JmV-z{Wn(ih7>59CByr?kI6ld~ zmy#uqZSsaa#+j!6MOJ+cpBLotMplVOQ}N`55dj**j@hV?%$|9eoL}zu9puf zvoKq4m9JN5QT2~Uz9`xTY(KrnaqqYfyYxN3C9q|ejg?9C9B+C!#-STCF$AmtgUY6A zm|PdMe2TJZ8Acb$hm_LS0$N(`2qV#FKNY?q1-@4=rA}fj}Dzto@Yr2-Go|iIDrrfB-D==6Zfzza`~){x!e5 zA4fh&`#QFy{{Rn9aX!k0phLe_l409&w)F(1x?A>lAy;}n89eWha!T{?TWy@ucFV(E za0OcGr#c*SBMAh1C(UsXr8@R8%)xLTp-N4u=nrK}L+a;4KZQGqZPzO24n{tE1h9}C zxTd3tE*5ix@4pJu()xtaHo+yKbR}zHgq>45BXgo`z6{mv%0bk+KrZ;aab$DCj!cGU z4Zzfa9fO=(0gEG$IOQsmvt7Dk{+iTZl_|!Zr;a&R;RA7T-dZb!hXWfOxhEqYyYii6 z%w)rLdnHSZ?Q~~pY@q5=I(*4N?PXRxGY)YcgsWWpTaw`N0VOHaF+bLXgRw~roTSB0 zwu?7NY#DKJ%IO-@OqzDq`i0GUL!Ck)^j%g)hs4&Nct$;q7U`Ji_Qm<#`?A1131gd4;i#Je575h+tL$AySVh8eE>mj?JU(Iztwe|t7ZvOxi z0sR*&$@|=|OT~8tJOCZ{$WeF_JiW|mA%fjrflPRPs3d!s_iZExaA@~k$g(vH&iOAl zm6r}VOcO#4a-n$TP0ET-mt;|mPYPH?sJW#yJ^Adj3nn9l6rH(J@S>3VlmKOhf|?(Gkz!Q!4!s?%9vbHx#yRi$EV;?YG529u)1cT_fJn#K%jzg&GaLbX1JOf2RfrB}K z2egu=(=yy(7syuKC&8LAZ2+$-E%ErGQrTS6<9;r4cLD|WR*hC)4+Ok>A@x`o$r$#E zDqOH9(@6Ox$2$|Ia(taycWX3B4Yg(Qyhbs^s_vzY;p!xo*D1KKgajVJ9FLo^S7Q>! zWJkgC1lL7?#@crPDufx*M(-%#C%Sg1Udrs{EXv|CO6+l|<}wyJtURjZQ)C?A@J`#b zdm}kB9q7hkh|SVfD8`dwy^b+v@t^_S=_;QS!_E?qbu@Vmg}@U_Y`mDvVb&7z;VNAM zk817*7&svG`AdZ;^K#hyO9tr&q~!;^f#{*+TY3z;8Rp64qE%;A4{;tEH(tqfE)E@# zxHB^#g5nBTeb^7gBu5*WTp&o{Fb~5_e`}+feZnyK8n`*6-U{?e$#OV7gM_? zHtmjMXY1;M>5RXH)fz9z^?&t{%hqOV+u__QJ)%S?UFZ^xF2v=)Y?R^ zVFF8~$o}2*ABEN8)gW)~a@*%1c6jgkiv3r!^z7VnIC^C;PV{n)>&wf_-a1S#0J6Pn z1jQ^1wUT*Qu0kO}x@^WdN7t2TX%ArA+LbalZE#jyDsdzmPb4BRY+kC@(%HLY@QLr} zRZXUyVIQZ?VaaQo@5&%LEbI?y2arOd>zL*Yr%<74^M@$N#cP1{Ncx);J|7XF+C892 zS{?n7bS7_C_4w@`a4s~ zi&Qdv21|U~1(vvWHTo{a(cfwqePh)bS~r9u#F{5I9mmY`>wi#;A*-{V4l#Ew%I8$z zjSH@4jvxjepgeep?Pt(_HkE60$p|H?+18qT*?lJ<@2O}L?cM6=k7M~!{1tn_{jI0L zkjeq=vIn8{3&r}ckH;#=y@5eC*tbB{CUk^4>eaRlN{HRaC_N)Wjw&uTQ-|W?#M>ZP zUP_FdtVr@oh>XxqY2aVcdH7I0zDi4TvZ0N~VFHkGYQ?Vm%CcNlvpH90H4Hf-c`RAL z$Pt~g{;08>+<{J{4qPG61)M4JO=z2^v=yk9x-HR<$yPJuww=LQv7#m_cXY2))BH=? z5;c9B_bct4u`=jzwvSR|gJ1&F2I(vfsS44WUMmSAlRR;ulwYJOj|^mGhZc$jSs3PH zW6-Y4d`x&-%V}dRt$zS;kwrZ)p&oRx%HzOE6EoA@;?b45q}}apfA_0gOIp+T7^vai-Z-3HvI|S(KHYS@@AhO z&QB`1p&WSW9QKxl9aj^Md!zu-RjjrPW2tldf^1XM{fTw&P6Pi20?;bi9dPaMvnrC%R;dw=2Y6$e zR6N<|fVj29Idu7ib2PNos&`T8WHruXR+c^Ti8)>DPtv_tlZ&1iGtS%fE3D#m1T#d} z7Y;|d`QtsV#+bNRAswT24CYotWZ9)pvmC0w=S1Ol2wnyWg^i_i*fkk( zv7>y>C5|1_JPhYJ?~V4;^Im>m5yx>CBpexI&(ouxCjbKHdiP68*_|XKk7aEd=#D_w z{;7*IV4Z-vh$$5E!&GzWyGJR;c1E2$Lz>}7y6F8ktFlO1*SnMRTjyyw*^MMLCHVR` z!pIC{fE8n&EmPAUn@X8s)bX2@7Yey1Be98b?ekU`C#D8HL8wF~gwipW2m1*1v{SRP zZN`rwE*Z~koysFph49WphY4foKb78^lF+MVt4`EQs1l0QVs1=!2P(6tOZ(6P;aZGj zW3}V5oV?{-1gxi%5K*I>^V%c9A7D;>X&4=I%jcCweDb!tnGr)=vlf(f$F+0fe(X-&;aBr zCXW}3n9eu_bXyqsgz_oAtJKD+9Wg0AYnkI`mT^t=yH$j4u{VvID~}?|1fmpdO#+qb za#KhLx?v-!g(B2s-0}ya%pejUGV@OyEl+i@KvvHZLia1nKO_Y*J1jeft%nOo@@Zoo z6YNh;6ZIj?k+#fM7Z3Q41NxuAcO0g+Nmw1)_+0nwi%mFPHZoi)Oqnbb{ub~*UCW~g z43b7d@%*WB$j&^H`HXX4;NVxL)k+xcl4nZN0atxltvG?M_Egy*k{Jn)mX+qk@ocU+ zB_^ab(Ph}j^Vu#sgjqU^;Fi1aroi13Brf$9s%`xY_0Y3N)Egs2WbzfO4r-3AbIe8@ zZ2tgZYUPJ8F^Vdjl5|S%V@#9cMr)N=%m7SUdn+y%?{_sJ*74sI_6G?gE;&D_UsiyG zn;KfA(=$9Ns`#`Vq=9HS<=HvqRY#1S8cz0J@?mCnEsza`CYv9@VYqq(7J)f*TV7La z;j)9 zS$Nqj4~9@$;C+`F)NyipcU_kjR}$8nW534FLcMPTH7~U<_8SIG@n-p0ZOnzyk^9yT zK4Dih?97b^Scfkh?R0OtJ-%T4mATiwnGl`|17Fiykq5-WI6rs~LcJ+PNmDu+w02xg zUl&@?V9UpFF~~UwyHBf6(Ho_PM+LMBTqmM6$#ks8V9y~K(>PcshP%<<-~5+sjs6av z56O7)!S-8Tjx39d8^bV_t^lpWcSJ!*dUG$ve7yRr1a7vFysVC=*n^^ptrSl@B;80riIIBJs(gn1 zfb~3alEFMK=ei1JxZ@4o!kLx~qgBk8uGF)8&ufdLred@q_F0l~vbe zCtU{n6f#byV@_2BDf?V*?3`IWT^IFSn=Ep={{Y&9d`uh}2lz-_hb&d!b?SI-;9eaI zoG78@?g}PPsvcouh3KNknb~FLmaOcuX)sMac~QJtlDXk-+UCWNMS?(DK(!+cf#|f5 z*x_b0S&6+AQt%X(qmbB216Yb7r-VBc@vOiRkN1MV^lzEO4`t8b`{I{K`!@}KpwTDY zqCZ8&=6;6AxLOgA!(K^v}mNHP+8bM@fKf5o7>q!Pnj4}%w92bX+D~oQlLGo*7 zQpl1#s6k__sWBT^<)^aedTzS-TpH&ZSB)1?k1ihK%4tnEYuLT97O4(9vfS6zp+*%7 zX|{VPy6Op_Cn6Uc9MY?3T~j2pcv4-1M$;&NnGIF8SREWtPDOWEdtDrX@eeO`yUQng zWGTidh4BH!u6Fr7fyMt378?K7v$%W*d zBPk^IL#s(Kn1)!Qv=4lkrG@@#Om(Jhh`ocFRCx6{nVeYe8)+6iyqaAhTHebhEHXX^ zG0rM&GYsRo$8jYuZfm3S1Hn^i+3uew!za4v#makNb*l@_#&%?H9?5Pl4A}3n$OTxk zh{J%B+=MQbmdTvv$y}lK>9m64m0xDlYA%lxV7=Xzx=Y==bl$$`+3{j=u}T8gw1%{g z!AZ$RE;KbZJ&R~=;Yb{}hdA#dlphtbm1%x_=0+Wk=j5;ByN$7&t4NPl@Mgz+Mu;Ga zTq9A({hh63<2ZQ-af%WaIf845^y*MrbBn zN4taAF1Pk^F4swpUCeIae2V$^N6a|+u+H8&7P}uy>O&J%#K{NuQqy1fuTRd!_pmg( zMe@eue}kfRMn{7fC$gyI%Gu-G)5@{+jZ24M-GxQQnej*({?@)l!ML2$w&-h!Nb-`l z$z!=uzV?T)VY8Pw2U-}{6=d3LOo64*zQ=TAZhP8oWEgA{SMyP1Od0nmr-nOjOff(& zGYp>oRCdR?qul`fnthSVA(_+yE^a=BcE*@cI1ox?@Y5AKsRO1;WgMxcJC4gno7m7z zBRipC6ni8eXjoC}eKV1bK*+)3NAht00HPGygC&UACW;rubK@;>w2{yD=9(Myv*wAI zW0}lwq75w-qwNby{hh8#<~E|2&f59+RdLGu961(PDX(X2U8Bn#*N+|64`oo9`m7Qb z4+|%^x*;)qrdnL(k`F;?ZF;G3-df(C0 zQOow-qI_SXG!Za=(_eEd=4T= zBx}WdiZiOQHowFJgY2tuc_bDT@+x*iEG;wXF0K1h(%YOe^o9(EXJHsJ;&>vQM;}rx zRyq${-2~Xq>>a&>*j*T~R$Uq0u-qH>jJ z+EFBrF5PHZgpnI1Ni34Ycxh3~H;m1%Y5bGl+%#H!hv~UZdw|?6Ka2+eRXYzJSf>sj zcGb5Gmqb2_Q^w--1WxIj7qSyKF|A=CN0$t*c-v`yUOa{}({_={s=%(qx~8_`^41|S zvNCcV!8D}G(|>t<@~nD3n#MpJLg}>E#Iu*6d?_CkesXO`m5^O~R{D(0iTgzaCx|v~ z;YlyZi$biSlPtbcLsXGK_`ote{%U9EGjRk9OLZLA3)rf!ptQST^5w@9eF^tKok@kp z+!b>#Dp$I<6&@^d%OC|&!BMYAMbP!kc2?@)Ss>~xQb4e|oEm$hj2=QS5+``KYBEq; zVdo^M=@aWrb9{${21awGVJ#f0*qFuuB&S+sO3)9gYlBvT+>;hRJ0`F6I)Px$j zjpu@jix=WDSory}x;QyF$c=8sxzLz2A{p$5=&Zu_Iw#rXSab1z^j_BwM3YIzZVkWO zs^rL`shuf&-HmmeK`aiT%`QdMdRyC?Tv~RO+TxL!Fq)#P8s3)4oZwoc8;q=*RPB_V z38xM%-GYBO8JJ{_Y$aOT1}tI*cTBVwf2!729%|9)2Mp17UK0slav~ zX5il~rh{>n3kSkQ%Rek-XiaMmAaI50eM(rKStXJ4NwBgWG{JV7X!`?0s98dKOzqD% zCZ<`k40%Uz;G=P(m}ApPG?2?7g@w&1?wi%jnQekjY|@4~Rh){OrdjmNi6f8q)(6cK zHbtGy`>2ge6iJEuu|$NY78zR7$MQYWRy?<8Za$g2^B4~$O%`J$6}T$SvyDDHg~Ilv z!%~_Tj!9B@Dg0Qimt98*nG7xfTBPcbx)~fRu}^gEa6AUQqH}TL&X03y{z&7>@j8${ zp;5pt5`*b#`G)Z>1rx{$ukfQDN~Gsal(C_wvh8j;A!w{>iC-?!!mRbKOIwE=ZTrtV zkI*Z7aW}tpTT160cRPP-OA`;4FE8Wjx#J6(77KeR*)kC&gd~nHc_b+(b-1XSFW0*E zu%oIdvUH?7rQSyg{{T{Cd^VQw58fS+y)OpbV0}{Vx5=%M1IM!TI#gTHPm$Z4N#$rc zUIxXicI<#ivR$=>SF&BX=al0L<+h0D*9?KVB zQ~+eSlfvH$7i4Bd69tzz02EC$Y=vX!EMJJ%qm;JU5%CY<+x1H)#ajfOD?8?s12D9# zOla$DF}0tNw+Of%QcWK)PSDvYNz_l> zids2HaHVtwr?TU9Lq*%BbjE29+D{7L=tQ{gKBO*h($X0uX&s4O0A=`1ji=QnO-n=LX{(V{)M6}{owPws zbeTzLv{t=^x*Ks{%ZdKtKbg}#i3C(2=+)p?w&Kfvnbw&ntoA- z95{QcH;?f;X&~K&NyE7?oYCATx@JS8(tf;+0eT4$vvl$o?Vf)n~|@mpR06Qb@A)YFkO+$=sZ3OVQ13J4Y%Iq7@k@?>F1(KayV%W}}AeS-;x1?v+&y$k6p>WJFyYvAj4q zdLMN8aV(0NvSU(5chPiN^!6Df96yxe9tlHeT~UwJJwFW5ZEU~>%NYI_e{W<&Z}Sgj z^ImioJd}=Hk&IS-0dmG3c%aKqIepU)J>Y#}o2La)e~%7f5am$3;ls zc+y#Ks(q&87~a4~lqLE;TV?CxxOyTq z+%nARCI-P<=S;(qvDL~wEd#M2im2BsF7ng8k-2Q}=z+o(cJG9i`l-z}xg!={o7*cx ztC8xQ>Mijgb8on$m!cY6$Fz>guC*tD6OU?BZ{@MCRzsrUj7TItrBn4rHTtuUs4CT@ z=M0#L1qYR+wp@cFJEGSqv71d%Q~N+^ev67vp&@cS*om?MU9+$8oZg;gj#}l$vLCd# zSfYC`SHXW0xV9pdrKgnyr-XiEMcDOAr35chEa*lUc%`@&qKv68^Gp*vIbJRe2ZKu_ zsN_5%5*Lt4*@`l^j3UfW+$f|9PRmz|A4Gs7hq@uXlrhj#H}yrBnj7EUbPj+d=P{u7 zT>1*!7eo6w{{Zbsg5vW(^|Dj!>5%Hj4nZ8;wQrRcF0BcCHN6L}V7f_ITiI!(;z@%0 zB$oRX;iQ@;k~Fj>mkw-fg4sc<;HxK0<#=_?erGX`_F-t78eUbtZ>i+fJgN z^&<3(uUCU60U%w4&#<)C2@I?Qb*`C{7|qgb^W{}MEHc^L!P?ZaWZ@kdy%A$LjvaK3 zj$;wr3M^^fUer)I7|&yf9E8~NLQ$YDgPEEp6nm{9j2xztmd4ja@y#ZXxB}M`%9JTi z!QW$jQGmkHUW$vX>4eMTN5KVO3HX4FnjmoEdG?Bzv8) zw`>j2$0T+;r8y0}Ht9>^TMNWUX#JbS0Q zOX1iFt4RzTw;|Mdy%b-x?FHGI8?qS7lON67)75bP&LMqWtIdxtJK=b1n)a5F@?SG` zHlB3dYbFkMhn=U~dM|(Jd96H8(Oh!>04S+pc=x4+>=it{*?9L@{1GvNc~~3}R@Lma zHaIAS4>YWjBk-(IHk-8AwN=HFY=XNhCNzYwalu-~4vF;Ukgt6|k|)HXO@+@G<76Mn zSh`~|(M_?&npOwyShc)iJ}bZ+k8r3NEG&?;j?1S?(JMLZLZa)~f(*BD!CGq2vsyaO z{!aZ{1_arf9;RIET*mnuw771nuf%YhYky+8i*d5G92|(;>vP+SwC~&AueH~rOaB0q zCX)_lROIE%*1O?h+`va?6IQ;C%+;eT%x0Y=Ss8egWrFzwxgT0W5&Mwh)`&tWr^}<| zEh-rVp^{iEG(q%MhB?h(09tL=8dO?;Trx;ow+~Zbo<2J`_aC40L>)Xw9fq%%`ivis zpNi|WbILU;EAmOPyQB(zK3sg4akkJ}=x}LZ2u&Lu@q+6rG0nm}rWTTs+u0z~@}J^uw9ZV&GmsAUUx5k5saVq5OC$cJ+Yr7Q8JkGo1Ck-6~@M>MA! za<)y|dn+!4I&HN{&YOrDK_N!mLNW=i|+2VfEJ1#u?l@!RUXoID-a$n)d?&{By*1emTD8k7&+J@U8g1Tl7N5o(?-n5xJ zjgpJy5~b7_el*@Ad8DfsGvTLeNuk+Ub3u-G2Yu7-xz|`u>@3LKz;>4^;D0oPPl?m? z`5HiYu0pBxUau5cg^nX~WzspLGXe_dV<1=|Dl)fkU z5WS#zf=1~HEWG5|?1t#qvxCR@RdWYhjkCHrXb@|aWYgZTGBvT;OaAD zOR`%5bVwhMn;0zsf`rY`V3-rMTAZ@lsi9meqP-w)TRce5C?AP@GwmSQPn)AjjOvV? zsC=z0&ORY;AvP}~?d#FDgLLgpkz+{ts+hv#e;~N-xsqndmPea&++2R7+PiDH4ucLx zcHP3~S?{+M>0wGwkw~Bk4Y^5(!u*D`d#GG&_e}H%{9Lx0x+~Yi6Q(8G0iFgULB1Q$ z3Wd~}K0bT-un)St>N5y5=ep=sa91VmXccT2bCt$9xH zuz?8<9wd0v-O(jjbv9m|$FlRL)RD_5w1hdo>lmbtN=Dfj9vJQorg$^n=-uv>XF`8y zv8}o;TuoZBV=Yks03Jrg2)@N4>Mbq#9(V;=R)L1m3_;uq7Ng93ZD5Wv2?|)`8x_)xe~Y{hYR93gbfu`KfQ>Z{zXk1Z-it51|>V>$==c1fqk zb}mDK99qUcQN_p)2CS#KtY%&ym7jW)vc;|XB`=b{XiQ`dh}vVL=Xh-m2eC<7O}U<- zX^AOh_48J8(j=tQFIi4(kOzB(&bq4|(PXk#k10H>o*qb&0>XU~-CG&CSkGx5V2#$x zJ@f&j<8Q_fXyAl=VXT5i@Pp~;T@-JELEGICE4G<0wIJ=yBcmElO1+D7BL4tZE((pQ zP3S6Z02QW|j^H4*Ju#Zjoh~fgOwTlK{{RV3;^lZ`Qxx1}RB`9t*3A*8 z4tPv;ENJz74;nB$c8@UkRfwG?otXS-!8)QUr*&4iZZMOz@&5pczTaPE+wgN@&Bli(8R79rH#CLGdXG(JL?bg4TO2Icp;B}g zQNKybI9ad%0929Ol7HE_{{R=z`mZzU9w*#iay_G;E<4&gF_6&YB^^tc?}RM1wsu#A z8(s~yE6D9ExJN64x4`^GqPJYL*jQKGwVQG?8XCzTpJ(v zlyYO3Noo6~Z4(YO*3rR6!6P!Au>ItalWu{r_eNrf+D&&_b+|C%Na5_6Xl?j*G$^{x zHp{0tyRiu7Niv`1DLo;OUlYkGnA9B1x^E)+NwgR~9yxX%5c+(xJ5_UDJhr*hmp0-m zdL8xcPt#Jo4%Alz&how9tG#tpx6|{*?$yDyA^X=aOX=>@Ix&i*b&4eoC}?Ttl{Cm% zZoW&|iO`7H7XJVxs}z)+CZ)s=l8!^ib+NUh${`~4QZT62%qW1I8(2KGWxayKyHZmc zvVk_GZ`DHH>UNJsmUZ{29f{Z(-_()Zh~qxvl2X>}R+-)68eKXm8a@ zGj?-ZXm_G{usk;(M5=s$hs-V=iCEJ!OnDBB_6lstgqZ*nR+lX_C!m)<_dxoggLj@q zqW&DdF-L`%*w|WkE-R;?)3Ud#G0b?{DP|4MYi!k~Ip#T?$0{hI0iYGp3DiK)W;34? zn0tk3_@+AuUwTzjD`Q}>bhLh??X(XIrVS|3(S~x3q(5hcknqybq1PDLv9`y@3tcCO zyl3&kuXPr^7|0O^0(&ImzltZzR?3#J*#=SIkXCf~$$cm19z4be3bZN2IMQz^^4nAe z9!!|W98rQmwX`E}wv?+UJU&gn$gB*HJ4d3Ej)j^-jC@HYcys#%R-uq_p$uW9!wj@y z-6Yw-&nX?$u`6!OmaGm9LPkspKC1DgWeKs`UCO1$r*Ct2D|V9ziySuZg!4$zv@QEW z>FBk-p2&@W8JgY9`i1i=H{;|wCwXLz)V`SHz7sL5aq>Tcx>lQ% zFtcpegE#0`rS%MMc)7>49I>@!n?lF%NNEY)pc@n+{=qB5m=}UOBXuHV#jiuc`EGps zIiX3hEV{<`_;L3_W|nrUaN%chmy`h=Yr)U zB)HhCXqfID(U{M3JiDo4EO8Lp?i`@V4~^EtVO~&e=-k@qVcb|Bf)uXWY9;?d-=Cl0M1jp55x#re=cvu{Y3mEeqwZgp>hfSZvZ8+Ig!`(5{W-O7k zvmCL4iNut}kjDc;(2aGA_gl2kqv)}Ylcmll$7$n$mUS*CiI3s{HxFbdOJ-0JJCW|J zS&R}w=8<+N1k3ObLm&K{wWeuZa51NFV>4qg^V@fE`mX7rbe^H6{i6VrKy1J2n!%N> z({h+dZth7W8n2T149hl|skT^&WRbWzt_QIT=x&w!dB8eLBiC`J_~_eP;;piL&Gsp= z-r&8Q^rGrrU7q`G)bNfz6cEVKs93T*I|J{+PowFQ$EnYOl-EZrP9U|yis1ULD9WUjsOrg0-^=}nSyLRdnY;!VS-@0R@X1TKMap8Bu;}vBujBN*D zVf+kaAo3@4l9u-f?Mhq zZAs0Nq`Okb%3TzTaT-05NC|Z)%n=CPw!0ypE2M*TXnc9(b+qznJX3ozZS9a-M8qhw ze(IgEkuK*Uvw?HKVX&UXy)!y_*y}0I@gqU@O5ucv!8|F1yBcjqG2N3^FgY!Wo&6HL ztwxC;s4`{`Y2>96NL?Xm94?5)i$()C>b5AkM$EAzhR{hu!HU^l6O4R#aoJ|ACQRvR zAjv;L+GCzx9momnlY1lj8_7bx+ zab(B2m1Pg6wa{fzx+)4B8&9H9d$v|S*0HzZ7mr0%`!UNf>g;9uh0s3GvHl}Yf6;R8 zkRJ`Mx0)8aya{Q=ni`u&(z)*OG{8aUYNkgsjILwKv*J!B)0xfis~WU_50G9s3KLsq z%G_jEwGB2ypW?K-uDf4>_ozf^r3imXQ0+psLNU4BtOf`qI8+V~Y=~!*b9xJ&W`CN|geHOtN z%Zz&^#wH9mWKCPD(E28(?1#u)eRx`RCP-v{3qI&d^hX9wZ9EmxR~A#qns0*X9Rp3x zY@o#<_lPTUTO$Ajx z%QX=jn-iV#T|-iww-}a-;d$~(Gtl)Gmm`&Gn3)h`ZSdW6sri!2itW#~^19YU@no^= zdq66mtMs?+GE8#quSAL*XK$gU>H*!ut4TTBiB{awFt7Y*!{wYTr(@A^RVR( zXPasCSALhs$Zjx(Q0BOHR+7ay=#qP;be*kuu}0J~LL3MoR`IooF{SY4x>n2y;>v^& zMU~Aj5;ELF-$y6=Ns*A`#{_j@yPf#1WtQtDTl()fW$Ss8Fxxyr)}N5zDqPOzlDqs& zd}5IBBWUhbo26hQbIA2ofyrndrCxLhd>mGje2ZSr73}GX2J0NM%r?bflBi|Z-9GCa z&l51R_-~+9&ExoZ3WE}NLuR{dXWdWfXS!XeNkEJ4utih2?z>Spo>tNuE5UC-X3JCR zv-Ma?1IsHOi`VS13lBi)M_B5N9BL(oK~anzd?`rcF7w{uNuAq;r0% z&l&#!(apbQ&gJnZ6q;v0$a%)p0Q4n0#Kdk!RC2g2XVaTJ=2y@SJ<<5Cjkxb~58Vrv zGUS{@#~+da*0m7Vx+u>r2@aW+B)Pymk_ke#b3Um(pIS{cEHdo<)xcK9lv|4(K2ovH zr6_{|)M${a+6>M)L|<~5bt{WvEFOs`Pk-9S)f{n6IyOeRQY~7QvKEouIMB0h0LPvW zx>2j2BO0~Dd5w)Dib9l?F%zwgvT^og7mmd*m~6Y2rm+2E&r;Fsp`@b6_gLD4z> z!#pBdHQDawmDuo z6j7S{p4SGnno8LbpB6~Z-XMgK{{YxNDOB`r(>XpYZnNrAZm@>rb@nNafr%z8g^#hy z_f^CT0au=xL{ z>HSdVQzmfuhK~OL;vVb6`j3glbAr*upB;RW(y*mzh`4v9RMw+884k0>r!={7&>Z)UEq~uB*I2NlLV#TD(91@9| zT1S{IH29p|Kv!!?F?B{9iU-wPG??2i*+NJ`tciuIEI4kW+wQmONX*(yn|UB0A)2x_ zr;TXH#d8lf6J~!p3ORO#Hq$d7+!7HwZpkxTS_cIcnWcb7RR!;H*_?- z@)3Cgi9$)(uVN=mNaFxZ^9aT$eu(fbRBsEKB@P2C)veD)U zn4P?iR$bWpthciBl7NZ5)XeOm9Jx%}C^HhqPT;OR7VWx(MEiwMh-fI&Rc@u2N&*dHX*7Z_UE2nhUm!x??8 zBz&)KQ2>%HH^|9sSgkJMONXCA%>SeRQnO8 zL#Jv+wm9+-`mUmHYfmXOu6{mCs_AWr$K_;#?m~|W`31K_eKHv*^En$PIS!;oGdu`* z9*9|T&xzShTj&}{#zRY4RcU@Sa!HaJT0?Cao0{0-1cY{y8!TQX+T+!BVsdY-6^o^& zN37<29-QV!!?yY^Q#0nDQ!aTYo)<^fV|1*Lp30^)@@Cuzs_U*VMCGxjnIocTv&Aj) zwJ6?p5-+mV@x8d`Xy%n(TujGh7y4S&eU-Hc-?kr@15CS`eN$~47|7=F*(GdKnhx%+ zIt(5jKIaZsHhY9fH+>E)Gaq2@NlcsC;%Q8Bz?_5G83W;)gGn&PyBJRFr-Q&CrWl%| zqL0o|85l9v=76i(s4P1f-BT+~2Ww%vrmopzEgq_a)LK)uY#FwTk+$ZmW-gxTWAme8 z!ek6LU6k?i$1Y2=bC!>up?mS>+UslBd0jiFvm2>tVU8okXJu6TK+_wEs=Foz)N&g? zB8KTrQP^3mw|N|BudDI$tgur2ov@OUJ1&WtyBeH-c+MQG__DVn8Q$SgA(%rmGGjlw zkW;B%`hD?8C=pIyIp_G_V=HT?(ahn(MQ>4>X5^MKzzN2SEPw{cPbjTV3OF*nM*&4m zi7qvbq*|OM$}|o%R{D<-<1juuqJVKBpg*`>5?n}fqPhgC&a zzpY@&6WTzc6r~!|7C8A^IrrMGkMO#bdGNNO$!(GcXAb`W(NJ%N-#Uj$ez(#yz*tPs zj}|UDusePsa9LPmk<51l747(0IpeRv(Fs~KD;|Wbo)#;W+BxeMEnuE)d$)Ct%LKyS z7wmXicUt$Mfa>N}BF}PVZ+Px`!zt!Dem(<$Kxp_|{Io?*^4IY3hJTb?G z%O_9!k#wU=ZJi03u9yPDZ6d;!Gk#|PkaTpzD+QwL$H58KY~vk-kxS&Vw4V_ibz^9x zjm;NH#4cgQAbToK7I%*729>K!X>%x*9yCoom7QN2r+EQa{C7c-&m61Hu>cq9s`+>Z zVRgt|Nv4j8B-VnU$?+I3vOXrY%nk`IGYnug-O-WGSZe4ft-55tPh{B-;Dgy!HHL1S zZS_{^m?VfeekmqKGhF+tqNyK&w?vv`L}GG15m-1@^5PK~OlXUd*eP3=96*m$BPPt1 zB!wEZJ{UVkDJ}tXvPh9P)Ute8O?D_K;w?Mr43cS51ixX7u>iqD1T2{Cdw%j0FEQb? zJrdKGIdy}_)d*vqU5=o~FzL;Ct;HD*feYQ=l$#SWP)8*-W_*#34=EqqPON>vXOGkI zGqJD5OD|5cQ9;M^NshHRbvDX7Ow}^wmzFp;+@||;?XXH(v6MYJBaXKvdA%_tkG4pg z<>%xGt(Hm#9Y-&n%?g;KF2q!X-{|f{Qu0HBCVegO!S^Vp@#e^1`^I-uWaO~Jbgxpf zl%MG+kJDv*#_p|oSq4N-l86?(rzblCvOOa;!N3w}SlAmN4;|H05-`RGc1^G~1~K7| za7!s#vz?}hI>uVlpS#&|tuq>PsJ2*`>fLusuJr~V$KumF6su3bixM!|0Ng#*d|F!s zcPlMDJ3GDsjg!54t#!z}SVftkQ z8qcz-%Nc_#I=xkw#^0xI{{VF)mV|@2&@Mc!7Te`7)k9C3kU04+_{R(4=}oIkwi{5~ zUcq`j42^;U_Zka)dD%ES2>L3fv#B|nV~92Az0tXEmG<*tUyPFGQVZl4uHhVs2>GpJ zc)-H&!RDzy&4@_S_HwGam~*EC&=eytxXMQ-8*8yXunf(hPUFf8PcMc_J;FOF}XW;nI9(pOuDF{~|P&vc`!G8y7K+P+qb2ViOqBvEU-ub}?V^2hk&ZOj4(|IbHF#If)fll&BU8~sOS$i~Lh2Ixp9DjHB z`$_Co#s>#rf%0764UcZ5V`bTL=$NsX@mkk+q^`OmrJ}v8W|&y`n0&(sJZ_e9Hy?jQR_vw-JdL1+z0l3K(Gie5NXNQ^ zjEtLaEk;(*5$+o~Fr_hi#V+Y&H+QcZD&Xov%qX43$f{u}!gv#Ifp zp2Hv()T(W?bk3y~iQz{&AL zNOe4Rr2A6SnUR|$atmX)RSdj&w4CD`$=i0&Z znWKyKRlKZebj-l!+Y4Q7BhaYuGMrx7qctOW`m0?-t>E?oc7^D)q^XyZv_9z#Qawf= zM$FzG&tv_QZ9@c2D;-^m<+xMjXquZOh|}0=k7o~7mDMzzP7PBHnGhZi4r%Vw>I(2Y zuNB~zDD-h;g(3}VVFm#mxKzwG?`+SjyH5JkAhR5Rc}nya@6#UQEYjzP>JkR>8&a8ZGHN5RH4oafEGybxC+>NvbDTP8E;;um=PIm8XZGFNc8r`iO57g6UiqyF{Z&3e8o`O!>$ zB9E5S;}(x}B#^i{&2Xm#US8oLCxV51ypm0ly@;JC5{q9&Nn9+x-0P*g2rQy|Dhxta zj_jpy3mbSuAg8+CK3o>UGHpwZ3!LXr;CiBCYNdid6mzjagd6jFA;1kJRips}BXJ~w zz+2UPmhO8%rf^n(lCv=^X5JO2r(L%$H+vOTkGiqwF06)MxLo=72125 zGk4iN*F4cRX`__xy0vGARF@^~3qSX49R>p=}^Yi58kwit_$O}v4E(f~vWPUj$9?Cdoq7gYP-45Wo zYMqX#3SD|wNxIQnYUlZY>uSRO4FBjovb&xo%v9%P;aM8KXCY>hh~HJhkkbdW!~soP4rC;G0&x6D70b zG~Jq{4^(1!nESxx71}TZV9Ipq<$HrWU$+e;k zJw9A_?xF&>=_mJN=#;lIIcyQiW2Mbb+gvF($rD;;+K9U)Bz*D4NO9#_b080lkg2+^ zJDW8Q6z#1Pg!nK_=?9Dp3W8^v2$o~+@5&oCqjLhRB8SM_t&Q>Q;*0i6BzD;k9m;>F zv5vw7`yeI9W8^fKB&sY#>L;LZ807lWujESSWw>7!tnJ1o5*2ec4+?Y7DEQJ&#>EFH zkYv1XZ7E#v*iE6PjFD?uo=i3JRgzATnz>LOLz^KysN-P;D3dh9l3MQ8p6O0*U2PmH z%Ns~;ZD=zK7sPNNa-GGew6Jebz}CS^S*Lktl=1ak5`>h=w%gcDIC#;Yx(Q72qJ^U9 zD)uOm@&e*f*;2k3t)Q9U4J1S55ZUc)xe_$@1#5x1o1=1$RiwbWqN&v_ba-uydX#@m#O!=9 z;yEJGTE-E~nl{&z6nmm>K0eB-3X;iiNUk?l!t~6>pCp8H?zh!+))(-aF4;`1Z>^@^W|_{5G(t(> z(r)n{p@a_v(g&w-#vZWJ8&>48o!&MGTxCq?dekE$81dXH25fE&k}hx2bexW0A26vp z+#4n?1A@7r+-1bl?B^3|^5A7TlJ*5R!PYVJ8B$gD{ z?w#XytY|0Ds&{fq8K=Dt>Pb&4Ff(+l1J!yMVsu3o&X|K_JBU1>dfOw)$bklv?KUwM zBW!ZfYKzp_CT&7acm;Pq!C^bSZD4W2&m+3k#*i>Bbe>)UOEazBoN$lMe$~ zXj?5u@>%p)Q(@ngRn)o%9AfnB zjv}9u>pwx;`mS#)C)>u^Jg%b6;FOnq!B^gv%HIaZ9_o(VU-g4styubaK0MYmcD3W= zsYK+9vf+uO$Bi_Zj{>`_Vd|~R%ySrc94+?I~%6&^`$_5OLMb}F8QimHERC=1!j_5P-jYG z@1jRyjn{I#EkhlRJOV8-(y-fyjOUMQRGoJ%@6-&QfysMzgp=bMI+j-4ohC9lC80@y zABCnnk3^%VvV46eFyf!Kz5ujWWRp}0_hmL*V>Hq@_5~x&@t-!x$q}@xG_TrT#*XO@ zu{$wgbyoE#WNFeUnv7`=`enW=91k=RGjY4*QvQN-r^kEE@{G>7-5@=|R$8=1dx|8@ zXOD(9_X*EUw6_+IbzahP+bdlgyM=4g+R+!4M;w}$76|ub8M$5_Q&@Wy7F;I7+vZ`IOYCV(>ZPgnHp}SVR>+;c3lS-A&&6NkKPDAu+gKP&CJCBp66-`-Zb_* zkmmPSoR&zb3(4bdEN`jlt#e&0LdhPAnAW;1$peLbRo6n#5=0p>zfMv8&lf%kJ?oV4l$JFl z_J-w9(ah(ID@W8M#mixiSj`Z9Q~fhA#Umz#-2VU*N2v={fgPZ78 z2cu_m^5_+C*Q5gw?p{tuStt+oEZRi>02`(<`_iQKn|uiy5dJ4C2S((!Ogd@cEj4{4 z>Kc?!aJIDCX~yQPE$Fm#R6Y)k8LF_jSK72*A5`X78=Y(DjXoibg2oR(xEI>{E2w0D z_<$>~#ed=!ilmNVm@JZ)ymP{5lF~u1M7}`1xO80*1Lhp9`(G+2SR>hP6-G6?D>Ujn zaJ_5R;t#UHG`9zOBC#ec-C%8`7V0a(?hYx8GC59m13zTa>8+dmz8JfsXi`R2eLxU zS5D2N)ja8~K1@0HS*7A9y^zN0XtBEtx`8)EG1Vd4r3)Q`iYaPB)9O=gNul~XiT?l! zo^3SB=1Y~Lm!?G)K_iO@{H-P?#9cvArPUvSsu03}eS{$;lc(2-z6&2WzZjismf)m9nk4CkbFAv+o zSqDzVB!V`APpK;1hm8)0gxjIQq#&eD^8iN zjBAjLIQIano;0&N$_l+}nC!}C&rp)Xg%l^vRu*Y6wEY2T_)d_6C{`S1#}jrof(7%& z!A&C6Cnd4GInJe}88jHCORZp;VsQYKR)bG-p6Fz5=DUS>k0F!e5;@^}NWyH>InHSA zxjcN+wwa~6qk;8@PtT%e?kqk8oV5P{L{CR+M++sfNblV{j_a%arD8#mraA{S6WyoT za5-4~T$0CFb59HD{0}AMjZ3t3W0#}8=^6h31(M8$8-f1-cn{{R`s8h!4Y?50fffi{ zbE37;G(!_1-yllpnpUOo%=k=$c<BP4rymmv~pg zTHGC?O})JkI#%{-drj4^6H>ByBHUBglzTi>1Fu+wjp>nzJA9?0*t zDqOl|M0vsP2fE%mi|nGvqYf)Ysw_r7Y^|$O%<&yxWHvTI$AO|n>6|hdZ6O&{PH8(g z5JfA)z!rwp!(m`>9E841_u1%6l0z0P@H_Hu#N#kG|^UWtHc4qR*MIMs~w7#RkAZ%8DLAb z!F0}p%xt0f(mKD1p>l{HwZq+6v@GZ2PaLtDEjGNrGZ#3r6OElyDeT5}=8^a&S{`F$ z0Jy3&`irt1cfnR&2p@*A?7UXmbVB}GFWA#EX3!v{n&u%k=7gUERDM2w$2*HwdFF0J z0mGGPB+%-aRDrJELG(*Cmbl2;_f|<_4SSpdt!pxYI2T;4hDr7qy3-$NM2r!B$_U?6 z+q*5a=}E)<5Ob5b(WIV5rARSJv^z7fLTav+CtK72%B!wZyfF8I9PSl=U&LUacdexH zml;zX&Q(Z~H+Yco%FE#<8v~Snm5L4>{FHlubDK-D4AHexXFSz$r8%?#{O#&?9noy_0(& zFW*oXGv(BrKqr;bdKV$hb3^zC zg;$41@Ux4YEE2BzHXKoGEi_k5q^BMsif*X0YS?k)L&yfzQhWzvhk!hku9Cp_;M)z5 zJyLeutaNg=it^H*|8XO52q2&jn@{c&uxOaGv!hl<1{=jc)}d(^&R6@8DYR z$B|@(p)~X~c@D+_-0+g>7~3Ip+26WtHHhqX3tggU2tJ_&Dg*ApdPiG6H-jlR*z+UOcKbaQ03JM5b>T$G?RpxhM8^%voT@>N}Vzq@q~Lb*l( z!W(^+TUTb_@IvOz@qCdv_Es_KR=JK;KBvbx+LaN(?5^Sd7F{7ayGZ!FR^%{wfVFIG zDXIQL7fQ-5ogV!5n)0*EQCZKjd#&{7j4{gha7o&P%XPS}r3ew^!Rcj zHqpxRexZ}(pmTW*xEz zqT1}WXSyv%4bF_&ZwL;WaW8x?9DM=i0tBkI8TVo_I@*{OOB1K8vSD<(FyUq@ESd35UYbDv8m! z&x;@ToCNbLGs9^ODY8jj4<|`BV^x+scDcis6->zw%*Z>gl$zG6M1`&;2cu<~^FwQk zLlijtjM{W`UV($bVPOdFsmAc-+|k2=gQngg$YAYh?^4C$Vu#mcUiLFr3OT+UuEx$c zbV}%2Gt1x)^jzmT;kh^ST?eI({W-rxt`@lwhv1K?S>j`w?yG$}mo6Sjj#~{SSUQME zB0E)g9$4mhW9U34#VcDWCut;HOIss^jZ1ohCrgf9AKn~_L}3>)3UROkn`n|EJE`W4 zJ3@ZIJs+NO<8~%Uvfc%v^^Gh;8~icla)%9&;ml>=6xMwBvoZXn5)`sJT#8NavBz&0i}}&nJhK$EvR7XFfernoo6DzIr7WP@*2N(Ik#HOA|?FeiB}p zh~#ibnWc3-K3H?HJRFcY70OHgj+_lk#K&*$+D#>duck zz5}%lIlt?%Hr&Dg0C+#T>Uo+`ehDCQLRqH|hR2bv?h){3^7y@=!8o($d`zh!n6gkq zhXfUC(X~&8dp4cXJMRTAUT8xnE^O=w>QHw`#*e-8RO}q5G2l3M?ibVG&ZqrO+<7@8 zq3W7*GT|9G)O~nZ=?!@3xqcQ%al~=+fG=VH01)|mgs()^jyVl+E`{C4vTdqqjGiHr zib1h_D`dwR#+cHAk}UmQs>X3ahDN!M7ShvTh0WYVk8_W6>b7+9MIVCl;{ZIg{{RQf ze30@vtmGRD#m|aRl^k+|i!8ku(A?AYR?eYelL^3grBUd&2{NzMUuJ;M;=CRDA`%%pmEu7nm!vjUtgXV+7$QF1)Q9e`f?C{M>CR-}j$X$E3O0%<$ZxS6NsmB!kzd#0LeT-g|Ff>bi@ER@Usou1gp+&cvK z16zgi1_CWoq&^EIaqP9}^YJ>91ZSIGL&m!!#Ojbr$IROz&t+-NV0Mb?AHuojYh}gB z#w{n&biF;kHjp>}0EKd9RZR^zsPu6yLr(ECUg8MzD^7=t6quVGMAxc^sqw@Uz0CJj zy)#^V8jE9bG)*eg$f%bU70H%7uLlM~E1!@-{?eL9?18mu-l2dq@moA&xKi})hq_Sr zx3TqF5lzsJZ=^KojXmL>K1JVtQck+cnnwtr?S6{Wu4gk$!YyY8l?B?M%9xi6B(BGC zR#E9uY3S&mE|_7LxeE~Kj>F852AU?+ojoy)p}DN|-j(qDqI>=(~5 zBvVr?Q5G9$kf->}do2}cd2mKktVv%)wJCR`YF*W`S)3$cbYNPoXEJjsG)Pw5Z9VbD zLDmwi=$(%8=QD*Yk#ScP3%YyrpXa&cG{}za%wQ!qh+-| z4nOA8GlE+dWN7V*Q11|xed&lq7zMz3E_sKWK5Xrc{2QVdNyR@0q=NqdYgfxuDy~{W zjGXx!nESf4qf*rT^bHj@USv|p>Z>V6ULky~c;?bZYW6oOL@dh4eocUO`=awDk@14l z$V{>E85Tb0v|T7+k_WuCz#}=|L%)oFslJD(?IJl3D(2j{;K6jPtE*K8>Db~x@x}72 zI5_Q>9L2uD6-$n4pCl|K)|x6hj-u^Oc^W}23!n8?kV!9$d4lUs(7qob?r7koI^LI0 zeI{;qZUeWy3 zPf5?011YX+i(J#n`VSvGa;+Un+Oxmt*}Fg0vJc6QKglp|GaHlQH@|gB(KSeQTvm|a zB?FMGI2weEm8_hTTm$aBpQ!jQ4dunxv1Rc^=Sq}G<#1AJF+>LH7NOFq(fENT>P<+) z!8E*_aowEmZP>e2$s%p7?Q1x>o)`e03IVeWrf#(ZrP;w+ZrLu&4ptj8BWvkTjf7)| zYf{ZKR*Y0`DCsp120>i3qmma3ML6=0A_;Mv$-}nrpJKDb#;;mXMVADu1@sDIh9Y|p zgy2+hrxa{%nMvy{E93OctXGQ5^wF2Zf0B8sJu*V~seG2JU;=l+4k?N@ZLyh3%msbwu8LL9gS{6iTf|}{_)2SwD zK#g zBPG}BQ7wOmA!ny$=`q20cQmPwAkJ5!BF44blD%Mw8$j@n)Pas0W!kMAmq_x`x^bs1 zhO4qBmay~P7Y1`8BFagrz0BpM1&u4Fk)V^^Rp4?~#^U=reuK^SLZ1R#gR$U=>`#O#!t$(e8f$9um>~dI@=-C+}2EIl(Gu>I^b24%E zk`0C1kCYWluu2USY-wNMIgh((;?tPj<6$(8?OWz)VmGl zM2+sFl~dnErIDp!b2be4tAa|VbruAeyg=Fn_eSY6&fKm^+N^mIMV$!-i>Yu*{CV*zq z1KrCSe?)MlJYTvtHQE=Dt^1xUjzn)g>N z20srN9j4V!4W13Ja^V>%LGu z$0ayTi@4z~&y%`2+%Tm1ujKlz>?jTAm6rV0o%<;pVKXx_PkLJgsqVIKWTKf~?iah2 zv`ybD8a+w|J0IA#l<2g)%}9#g@e>coa(|-gGincm7(5=qE$E#%V03;KC~Y~h?|=MG ze&Z-9Lz5|EMSc!Qd}GUgis`3->`?Q=z)L61YfV0p_?dASs1Pky zQM4>{Ry7QXWY^jnPS91YnGJ^V6 zAL7L8DL!7F=VUMj^Vvnxna`gcpgP<=71I(CElBqA7c}uWyFr{BT4)So3_Ubg29;i1 zr#fdem1NcOUnGVmif9U_n-imyyWJS7{Xm*>=s~R;l%Ha@>8As!FcaLUHCZk~3Ik7_ z@nx~hKC1CrX%mUs@q=NO7q!EN zvEfy+^!L9ZlfYQ0f8M3LTw^7r12pc5 z)X~Bnx!dL1ZyHm)2)V6oq*a7 z=19lCxLsGMwC7`Gm$kk{R@K9%Zr0uWm!%BtWKxM3%B9)L@UuuXcpj?Vpz7|&c#Y?D z4*d{4N77k1*nhO}nCC<}m4JQUpV3!vvkORT&lbJ>8P-LkzaB*+p=#P|WoN`<(F9)# zshf=U^BLL!^Ecc4ldhiD{3m!h2XrTkO?7QCuV~1`Ahe6&QzR7D!zkEKM(8;>jb=m0 zEO@X-Vi!fiiZ{2+S2O9(p_faA*m)N0LU_LMAD8>7-cGk9TYMLS9ylQIzE{lg=Z`nb z+0DU5bjy60(TR5l14z7Qw|LsBnnrF!t$&C~wATk9$GgNmmn0MB?VBYoi$v21;U@W9 ztL;dcv7>*nD%^9Ql{UuS)m-~Z#;pyH^$uRPDMl$FZ*-0)l4->*nBAs%Aa6F7Oen8g z8PI`U-;%rv!~6V}T2;q9Yt?J?S#jc)0rJ;nf=2G^PQ`B}+SAc8F+4;9q;DMZp^Wx4ekZbpo3rtAJDp*xyB>Q5dNIM25WAvu>&fuY1W6oWHTf$|f+mo)Oqa{Y=c zU60|*48=Ye>=R86M`hwky&%AzDRCXN-wCFTnAis11(Xurf$cUY)Xez#jtwLWMrrzl z%pkeNYt-|{44x~himx+UfzN9g;dkh&lb|(6LK@ME5L|5+kDCUg0S>EF9L&-1fbQLTzUnNTUs7 zZC2;grXF?&G^>VQsR79>l971s#+{vlq!UpdUnWr-EbV5;5*(Un`SZZ=9(cc0_cuz0 zPc(IDMpmCBV%dBt%HD))Wtp>h@rNE#om4fvkQVwh#7D9BeYrvFo8Muaq>shXMNe$r z31awnFabnWE>YI~Huy4R&BA5G<8Em5T>}KkxQ7CbV4`M_hveb3mho#&4Rc&T z^$Y0mXO3vvEShK08XmAAXM;JSJ3HM&)UXWrNr&%zl}kp|VAXMhnBXL?MW=dxOX8WV zl-qj(SDEJc94{s=zXv{Cd{wT6_;JaI!XyWUxrzBMwvH9PZ7w{x!|1BJj0n8g^Im58+lMQd{h(MU;>2RgZLDPKOIWkb4va(6zFc4iVr8;RDq#%Qn-^C7q|D&?IhF-YrTbG7ftr$8%2Z zSDUDoovW8Bb9xp|-Hc8&a>^>hk#cFzFxaw$z=By`BnOV_&jf+7ZIitvb6DC)%Wlea z_@`r-;rXiGMv{DY76bS$mz4}Mf?7qY+|hV5kV5n^c`3ae9HYf1bKP%FltxVxUDCZD z8RsFOi=*tiZhj+YkM4u$mTB4{Wsul2L?9dN428pFK@)~H`jGdiYf53-5h}IzUtA_x%-}DdG$=S94Mvp z7LI*YGUID&qkB>*!yL<$*&;MSR+j@V5NYlLix*57!SW737L3zg$=n;;mBlGP(#A=3 z`4Y(&!ehDa6i;LZ7NsOJ+lks!UcY$9h6YA{s;`dmBx27grHxJl<@mk}(aQp?9B5Z7FVsm)`OxAJyOj#RO#V%{5iJ{c8;^aiZXZkL&mN!W( zXS$*4u(uLyjl4QTZd{%-aGaz06CR|58PB(rwcNQClIwC3`QdvQRUMHUXi@LT1>At2 zYmM70L&vq1xTWprN|IC(77C?dj~h)}mQ-RHb(53zn97vam^BM8=aonctL~ zCD(QCnB_Zz&nZN%v*?N$8jrp5TmB11{Z{ozuo)fJ#0pxD>to37fDa4zRxX0l&ac%t z7^jvxQNR8X$^9)?2LW}zu#GIqo0+J%f3lCmXZ%O}f&A1n%Y+*eZjs#JfHEgE4hTu! z&6B;Jg*MajJVzMc+#$7C#B%}y`K+*QG;zyn(a&{UQ;)8)Mv4Tc+MZLG*obmBTg34T zWCM_q>)VDPKA}g(o0C`QNV-i7bOFyaUIm1#o2BTE7xZq z=v-W=wI?uxupaBEs1S0*kAJrYu0wx;_eJr)BtW@Z>&Hn<8y;_89j}LFrmKj|%x4M&*9)9Wh2ZCy zp5856I(J`>Di+5p#_yUVQ`6Zjf@XlP2&E%6{$wV|WN_)_yG0R1d#)MQdMhzAACrPA zCmVAI@clkZw`-Dka1q*%JkGrw>d;b7M?cmQGs^WIznvv4AO)-+jGI0nX zi{T<_iy%BNVO(R!6Z%(W4R6E{>>yO0dGVkS$r+bRo|oTg{p0cggO@!4($*AeW2joN#rdUp?R)hlm% zO!VGlX!)s6Xq=A8(*wo8g`XvKU~V=j91ayHqZ~XIA7HGx^2;N3#tC0Lo!sc-Z(?nF zSIHxG-CDVH+{M~$deTJ(nCHd1bpHTIYhr-4sV6Uu9WwZ(`-yc;Jmq9=dE(KUZj%5Y zhKmb*LnL^f8%HHD!f%6*x#yZ)7;(yO**Pfmi8C@Ag^WvhpsCufsB>{<4R;_THO)jz zzBZdZl{X4v+=*gw;PAR~Nj(BDbc)ovjLb7N?mQoKfXkIMu6T2{t44N|>GK}e4jfeT zX_4Y$ZJ-LAXBa&$&C}R_Nl5dDPB+4Hmn)#~$Ti9vr12{q*X)VbW4AS+`l!lZGcJO9 zhYi|>hrOiU-s_oSY12oUd!4~ugB{hOmb?5bn@g6_sXd%stfw@e5wdk?FDdePOkpJt zsCba#?r&hF&z+&IJX+VrZcZDAeiuGIPP9%m-oZ5NlQU?OYSEzQhDqT#{FNt2cHGxS zQ<*`<$!xo*TIE+r_YM(eM~xqD=&YQd8x~0K%}d=iklf56$R?C*#B+5QHQ#iV z!I^vf20C^j(zIhfah6YYaF~2P8DmeP*THTeE=zJV#D&Zb=9Hs1mPrbg6`bbYQ;qX! zhMy~gHis(qr96z(wzsh;_~v|Nul(1c#iWq@P{MsnBN5~}%T(BXLk!1cv=7ZQ((&Nb zpdsKFQlWK*o^-uHn2yj-3OMseB>vpHGJ~r{`Y#GoaX48(}q^J0v-zw1dc9S3uVw)gStuI=VEOr+<#VYo% zq(bl~$2@nSiqqO3h*`yj;bw!6XoRuL9IUpsY<)&66}?Z10gl7@9)(iy-98+Jz`JNF z&w;BpSO|9q-7IN64{AY~*xRw4T!mG8or%7^)tehHdz{A}3Qexzk_+P&7ecQ>$>`?a zYA_8;O^YZ2;=#eM@UBSb5DQn(6X}i3$C4M094M|Eui!sT)iN=p6jP z%?m_ktsg)^G&$Iea3F`a!h=r3FlG__HmRK{syyO|w{scepGDHNS2HFs<6D=NTv^Qf zqFKjahRSSlGjU9EBmLm^NIH&x4k7}aw<;Y+b^()?3-~-J-bB%zrpiXyoa4$r3UJ4O z;<@?!>58AQh#Pj-j!-$cbtd82Hh62i?NW$D%srDhsIVvKU4g0$QW*5};~O*^+N&Cd zp6?hI-*ndicv${r8RLvqj+<_cvGQg3f(`9h)q|^jDKz<^lO!;ay3^dvIkLgP;Dt;o zr(-y!XsO|4Z+K}RC18P(vM~Aq8{u>3}xPf{Dk1j^Lw}q9RckuiO%}lF*V>q71O-5CyD6`4}sj zR$LrNC1@@HltZy`+vOplOC`w{hmPdASysi!Et{Sfm7UsDGIGtU$j2&L5?(Zu&<{;% z?uQV*LCQz1KxE5%xuCB=2(Sfl!lddGhf45^T%}|ir$89ex+l|n1m&2p=EeKaEhAyt z;z}|g;#i%`! zJ^L??RG65%S~olMRr0eev8a}QUB{x&<8B+{H}qBwE_X{14pq%8Fzs!tBux-PDR4o` z8fP0`#|($Yh0ON~y^|5gbc}E+!=>bn?P)7cr@|fqo-yc-NK>?HrD&qXlyLP1xOOF9 z;c(2t4q@h(ILdE|Hv#5pR{o5dNh8|ppsgHt7(R0<_tK*a~}LDw9yog-xG&E z$p>3Fmv3kAuXIGRWJnrG+7vh`MYb5lxg>+Sp9R@w&6@0~l0zmdsn8+iPEdg;kPYd4BDYqFiq{-VWXdY1-R<7(^j}7kf+FzZ6J{-l&2Wnj?bL0->I4d|q zG))J|oofdvnK6PWg161nqsfKT1=q5xbe2~FH=b3&vzTjl-7AhVmya$o=zCG`qnzrx zT-X^!>}lI-`O%qPJ|@R^2)vAFH3(>dXiX~#+J0R#?iK0cnwEg0#owR~qW=I?)Hm0Z z`mT4cDu~tFlM8R%o^;wkLxKruz zc)1O4&3d?^bww6^F}@Z!wi~OvrBLcTgDz|6*anruA>s@NpOmJU8L#{z1pxD96&q(O=oVLjzAOfvp5AiN(2bf`G8Q;`CCMMUktV%{YPfzN5;M9A2RV)nHk#pN z3E&o>$%HiS0dm39sTnVkHZ{=eD@p^MN zn{f+_tWv>CvRpu#X4x%5VTpma!v zSv5TxJBWFufO>q#$x!h!+{c!$iva&R+z0DWTnU4FfAJF{O$+O^UI%JbTKz7r)j{D6%#dtnX zi!3j@M?c$kaZO!;xef!eQ81(OPqmf-kn8tJ@)!@@UXCYe4u!IIr5ia@%G2z$YY1lV zMbe5R2e-{=6p!J7Mo|fe_E4#hBDhf=N#QavEMCD#JE-EZ^+iU(XLou^z-vU?hXHR0 zyS1|-dzwpHOF||%gJTYT0uN-Y1Xz%nWeg{1KBv(z!vZOGiXb1&0%X~HQ5bHw52~Ej zglB8$JaI0R$vtkB+9pREEVjz0{hT}v7SCYiZf;~G5nQjF%C2nZj9u6^Rz$KN4kiE- zeH#VhFW!2kgElb!0O32+bDH?*C2*5ekyf0x*#<_E#?RsuyIsjJ;<>E!36`s#BZDhk_fA^wLheoD$3*HafnYvNok8LLrA4UX{62Yb9F)&Qk_mFb6>3hQ#wR*5-piRu z$=Rh2FIxhQjD?0-XmM(-ovDnx(;E%7r61Or(qiE3j`t9$IyCNVh0pGN*G?>Uo<=f{ zrgfg1aBeHUh-qgN=a%29*Fi4*ocE7puR$_oYZBjJ<!ft13) z*B%x5_{?)e!mqmK`apIwg&$ROhc)fPr(b2qncHH+x(C%KYqv7{D$^sZqfYfiY7)4Y zyb={^9LWZuSH+`;AIn4jt&KGd5OPF@t!b0QblTxT)Ea}J#}98jD&$>2U*&U3?2IKO zId?EBQKnnrs}p~YV?qT=!tRREY`7WJ+5fs$ntzFU@0`__~@qW zt?O|3^Emeki`2a`$=GJaZrMj94^`9*Ep@rxscud)T;oH8{ytg4N>OChr)c5O%RUB1 z*V`P@x))3JPv#AMRh++!Ic4i-Le70g65eJPp0G+JP-lkucz@m$mLb+>x_=% zYG`+3ybwYU2+0ms*9rwtdTXiW#dBFHE|2EA9u{skp%8p78;_!=lj1|jj=K!w%~0`? zvF!5>>r;!dgvZ69BglMoAISGpPtlpONM z@)R0wNXf@LRQTAT?mTj*2H_?J*#=$#AHK#PG8ZKKT+&=7hf!-Qc-Zsv9lz`3yMz}B z+U`P4)_N;7ogX3=G#5Ffx8(0d+lwq=H8ebK4$qdzT9i&~4#vrrDF)XtmYYyo`V`Kz zXjT^e!ofr&WFE(|(1U+9A^I&=3A8TS;GSa09T-Rl1v1m+^8kA+%r6wv~}Fu)MdElp6b+B^w9iJ}f+(M;gPDW7W2wnTA$uHICv_f9S;m+f@|06gQ7s&r zq%+3UCB}V7NDImrY|h8r&%H~?H5b|)GS!`92#@zyI?8oqJQK+)rt44`u-)X_natQO}xpQ?skWspE4XK<9iUYxv!`mK{3Te$Xm zA8*Ej+qNC3<(x-4HahEt?H?dbFfSu094v zR+C&4Mf^wdP5J@WGcI{c4`b+u)AfeG$t7RSCcw)0r{r%umlA-<9t+&&9zu#q++;&0 z-Tq6SR1sxHOK7Ujp`?~Pxq(Hb>)Tzi?yC1zNOGXdXK`zbE(zd#vGhs7_KLorEAcX$ zd<$I*8b;;dfy5yXSBN^&B0-O-Yx46?yKGUyiJrcd^XsI&HT5HRy2w;!PY0P49uWQ4etW9m$& z%Q$PvI|T()$#*^1xr62MI&rJmGpllKp~Y`5Af*1GS@c+gG5)L6#EyugUZ$K!gcu3j z{^3!Hw;y$3>b$F;!@HqhMOy_~4sF>(JA-39bMB?S=q$(ZM_{pq^E%vAOy=!+LuQuE z;k1~>%gf(Y;D8K)*>12FN1_Y`)9klTc22qR8*P~FFZ_EC`uQ%#$$VIgPu>c-_A{k2 z9-WsaR@3rip~vvI{K;0_tRIG%&mVNJBR+0foPP%vXL2OabBq}q9C)>8)M6PJyV>5Y zP$qweD3P9ux+^tiJ~#7YkE$C|#V}=w8Q7;< z9R40dFSD1QROMnQq7)Wn=RJ}rXNilWyT)5Me?Tsd)xS_nw z4`_e7XQ0e<=|sieL){|Na)wMrm&oBAl={Qje3jUp6U8S7XtVWpl@__9{9ZU7GX>aO znVHjIOj_b>9#>cUKh5!=ekA7ju2lo=FG@Ogu;Rx4?n7Y&s#JO&UNw>t&mVn;=-TsV zA&)$#l0h8cIDl6=&@m-+;rxcHyjcZ}Yj2fDHPPo)pwPnz^7c{}2^mMajkC4D2k=y^ zXgq~Hm{iroMw%ePgvXd2d=+HLls6y0qD%!FUdx^59l(1oc()x$Xil`sS5QgfWxU!+ z{6WDN)CNemz1D%!-{ZtdPYvv}Vx&99$qi#|J!?KE5H-A$!l+DK)*Y(t_H~Z!P1_!P zty9mEK0Y^+UWA4>IeoNSv<{ARxDAjJ3ge~!0I9+vcWOpYEn~c} zS*7vP^YY}mIUc;cENQJAKR*cLNi0qubfmBo9zpa9-L4XK350@)Dz%XTY}Nw$zo+Hr ziZXwL$H^L%Aj#nq;*(s`14Pb}DW8uw-6w@BGs9+V2i0$tj>z661C@N3)YGw(p;o^E zWjEJh zP@cz{N8_>4;?o5h?vc-=oUFq6onzT_{m7?mY*D=zj*!e5np;@@O(@@^Ml~TO+XUJR zrb}@j;R3B&n&G)}%2di#HDr0RLyZXALV~ZIMC49c5wJa0ok`=(BV&Y`KFeGzXz;N~ zdAHK-@$n}fOl;FrmVk9VPvN(=?5lPgYU*PdL)`u!D`TgflVkdyR5>GIRT zE2c7dpD20+CvJ*X9%G|B{vP&+wRu`*=G zl*ceFBe*Ns@jSTajjqnMHFR~kBgWP-yhsS~E0B>&%#1lmYl|P+{SU~b`c9`NJKH1< zuW+uI*%-RkJvy5>9EI58q^M?-InK1#i3yRrNhvFbHhV6Gs_C0IMUc(Wp2yTGX04`4 zh~nYqi1u(?&le85CsY7K9T~Rs?xHb5S>B4$Kto9flq;M{Xp9j2XE}py5#2ywdE&WS zc%{E|m6+xs)_C_%J|P=O z-a$*-D}kU&+r%Uw%vhb96k2H!Fj*-Z=X8YG38U}NWOfm!rR+!H ze^oj3B#ybz+1?J8SbGw!7?}}B{$0sj_ogQi;yI-qCVa5Y-IrFrc2=`ANAB^{McXup zXtUKe(o30-z24TA==Gh1$4EH|);`BV=-cQO<0T04kgiT$bWr-wsEki^Z9kHtyIT&D z-g!>??+~*(Lb=9yb=X}P($BHf8&=M@(fN?%_po|`M$&rQdi&oH%D%jo9C%wUCT*0rZ##K)fAseC<222BD*sDFtb zx*F{p>9~g%Omt_^FLcJ98IdHLQzy?G@ho;I%_=st9FRCDW0TmA(kT6DxMcZ4QKV{e zLpTn-s`0B}GbPS(7FB+2BsX<4^i}x14OTyjY+XwnM({Tfs$D~(KN3SE0D<3yra8>S z)El+0*h;r&JmhKMxt8kXO~vDSGImzFXEmUR=lXJ<>N;y(Q$RTGX;X7xnhc*37B~yO z%Hh)a?wTJ#uq@EoAwFs(D(Au&@^|hBpH%i(%Nb5Qq`HKzg~F_|OARHxl z9X;ZnXj)@7RbmHwD6lnlu+XbjeA1*cm$Am5)6v2$+^Zg)E<6Xq+!cHFjD{N>h_s^z zB6+m~MAfKDJk#XGllEJ+rf%jwZ7YO0G>0^%k;*Bpd9zAa#MciiY?bH#01-~8R>V0u zCJ4>;LTGu-cBC8+sykT4J`;gzt>Hl|8DO!dt#o+lNjK~bw2r5g89XeLjuM=#pj;!g zUJJ|g;9uUAKR-9a)Ts8fR2;NgQf>oXLn+a+n7spwv4hCvNZ6ywg|6;Val#t`ZF(ew zNY2$I)80r~f33i^v zPo`wG&jH+mqRqo@emh>%MMsSbahvBomzmC_fbLCE#eUl~5vBDrvS4jv<&PD$*dI~( z{{S`3>*T(V`%=M+T};@mEmFB_F0t&mFRnR%csGQ@Kj0VPx=)x5`pYcXnbIDuR$JTrZUqFMD!=vu-J zB#~i5obAnMw5wO0<|08&`Xc23JCeGv;ZcpIRcMwxe}#1xqc=nw<^t1QPZf+{az!y= zun0`yvom+SgP6)M4^eYQ*YXl6n&w-#aHD2u2Ar27v?H5(E(~NjCv#{yC)e#(|1XHjzrfVT&Qv z>QGs^8#7%S`xM(YH(+W{jzLW)f~(z5(#(cR&s5w~lKsT(A$G059o{5eipG_$O%uhq z(kRweB_8yUO%H(M^;gAGTx3!^(>i{UuCpbfvJq*+B8~47GOcoLKOymuK_Of8ZDq|Z zYtIYL@_ZG`oHWFg4u)!MO{cgC6S58?-r0F*vWUdaWUK|HEb2J}F&_Rb@E0!IP?{Fq zl&xDJ$&n-d+)76~tdsLb=04m?TOqeEe++3Pb-^1z-O@CC)wdp@#Rj+yyH%|&Fzk?B z**n!{Y#1)?4RKzp(T1X0B|lf2#>;z&vPxfTd0t&3oqg0{Izci|l>4Q*Z{&cEk=sI7 zJGT;uQ470;xLP;A$!-xbp35V$*VSy%M#=y;Z#|T_u{vys<7gzffT9M0p-S2IVWy2o zt8m1E${q{7og6sH=4i}`d~ zx}OB$MjsLRr22lVv{Z#yHCbJqriYGvtCMOvk;BYo!x~sEEehZu$4$`gXECb(# z9L71?MGIW{BP>eSLPkyuq37g?M%Nm$tlww4;L~%!aq>8-k6YqNjzp{nZJ@M_(#OQ& zO0@7p%fV9Vm4w_RrxWV9wVZiz1cmMGeb%k&OgOPzUn6)P>xJZMc>@u?6s`>;wXN_o z4nqTEdB3Xpm;V5d$}(@Bk4^naOI&t8*R^)#YR{5B7<(-Yjg~v&(tL&nw78zjI(Am~ z5DyF0!Gr9mn&I& zC3iFynuO!xU$?MT_QgJ-s0gD6ZSq%SFh$E~Xxx34{+iF9Cp&jLZC~-TL*3z%=A}{6 zaiY^Ajfd|QV8e)bqC#rdCzBhVg_N7@Gd^gsp>&U{0aK&YpPiR26R>a-E~_#asfUkL zt44*p2@9_VXv-Fiwu<(&?YRylcS7U6?|TCpRih(Kj(Lf29n*a`M1`O@v;aL(mRQgt z+*w&>mywqCfYV9lul9KcucHUYQ0feCk@dsANBgS)pA!{aVX)|otGA2N-K4qk`qMOw3;Yg$91Yox~(&r$5E?gTiBU8_~nXn3$wn0-A8 zqHEd!kUDH;?T~Ub=vdzJgotvX_1VB z%8|Xhs6}onX7CVT2#o&W?NGP6-IiGD_F4|;7?qTc!aigO2;33oQlCOd=shiqE=(@) znoUx)MbpyC_PSo)%U`l~hru{_zRRJ>8)Fs#rSrL4dD|V z_nrzNv$f8qp|W1+-?#+1d1Uy+nkfGOXi(gba0G+uxn^ED+(jb zl-7pOC3SeyaE}&rO4NBF$Jw?Eaq<}>g%WLbongKvD+nBLxz?KDh9M2^O6g4=Hklbb z@Db(8T#j!{pmpfmrj8dfz{r2aFb5v%sr{ofZkTxNxmJ?T6E&cE6--~Dv8vJ08<^bDV(lWk^~rELwjg7Zhk_!D+X(s%GcmJ)1_~%`4XQXNXIgcS?ag`B-@+k1p=I z6r&}habC`Wmo^iL6x1#}@Umbct5prszxSJ+&ne$bLhOn6I@Geb)r%yTqV(rjT{o7 zo!%3+kyg8-OZ1PbD_J|)r@TASR3OTiG98idW7f78u9EiyQd9s(mwd_}V6XiNa=e3RP zYU!q8Gr?=$-j#juft9_(vSM{sM;Kr$m1E^4MoBkdy-zGLG=WyC*!hqbQL9e%V9eNw z2Fj($g(Mjz%_f(jiyEpZ)2lA&v21v=@Vs1y`9&wTWOm@BhZ6jx z8d#D-9l$o4;1?M8Ilq`n+wBH3YoQ)y46!(n2@BBU+GbPq?piw~x_+rH zB!Vo~IgTEuh4el*mkjZ^vEek%!3JcIv-eK6A4IEH)@6x~-Uta+m8g=&nBBdJv&0#( z7CZxEu}h!9#h2ujGdL@>DAM(3<;eKl3z+L^9_x~IewO_&S)LNo8cyc@3ho+C1Q;@( zAlglX?5h6&YubEScyVSx0g^8(tJSNQ6eWWv7t<0gsZ%)7UMpu0LKd{WBIXLms>xU@ z7}eIJJQX4Lmb{7f)o^*yH#r&^2Gp z8K=x)$Zs?WO3(|Id~KEog40A$RM*B?Gn+>JmBtr%kb}YDS2dUnc>(R<6eO(zTR`~i zFD0Y6N8-G(^m~NA7C{`li6`c!icj8sO5~h$MXN_*=q*%Dr<_?$nEPL{u;mdSm<|f$ z-7BaZO+qpqO?L%#cQoYTZf_>7cp32V$m^zaHBN;3jMTzGRXphojxLVnHJ)un?H&SWJQAV?%lLLqZ!IUT;OdCZNX6v=Evzy&(D=Yh2^d zvQ-Z$voccdKv&r%WU$+*Ry8NZxQ;DnCb}K12q(*MZ4|xpInK1v_!7-0ka31>P?1aY;Oe_Je3`l%zeD!vZB>`9!`cm-I26Jmzl*h?~cX|qJpvjAyuvX=EOoO5D3Pjy#z7FL}#w7Y03%cW{2Sc7N%RPv|#puwr5oNBl)!`vL( z`J_yTe3sz52Ct??nHenr+N=KnqT#X^JF;vU{Y$Z;y%mPXQGf@c%y{R&D$3m>{bj5J z%W3qQL=iv+(R8~$%TLOKRm7*!Br0!OX~BK;vzte-T~qx7WA=w#L#4Ae2LjtF zsinaEJ0!6E+gEsU*)_C~mTLH|Ai1GRFOSF5H|cg%de2W8FgAFHm~+CUWySG0+a5g4 z;dJeWNL?HG6sa243$dqrm^dlp@zV^c?G>#*7430h?dXfp}fG3rajob2Dtr^*DJ$+FzpH;A0g|mC010b%I_E5u&`Le5*`2qg`ufcO> z4bE#vs_nfmrne8L@frn4TfN^?{{TU8`MEekza+A{Lr$TXy@ABq_e$oNCuk>&P-lZI zRDBZY9&zQqSDfjpIBuPtM>`p_#3X3$6>C2i!hYrs>DNo=jLaR6K4yg`TrDmwa9s7m zL6__Yq&&5Ns}yf;OeLiy#)w87-$k|_V1|8!r|!{~hB#aCGOfHT8O|9H?0MxNnZes$ z6mn!ag4%m7WfvPrwmCTHcG$zgYlVWj{{UX}sFWOt8|EDF72ihGqRFNg4gHgxtR~F? zYrr6d(+P54xF?R)qljsx*w`-;Sa2SwZ8JCb@-@fr6>sYu5!ms)&^LqZsD?j<0nXs8 z_o7XO>JtoU41sh$kJWxJMspr{+v>U5jAld}c}{wNE8u4kgF(WSr0O;7^nRHnvs3Aq zpE%*_lg9JIiB&G9k@0FV%-^_5v~_kHw~U5^-t8mQ@y2G7AKmN-!yg?Z!U4Vxqr=(%fntk1x3ARaAqO!4q?Y#qT}H&2*L zjCrs^p!J3XgDk!_k_zSVf5V>#(I`GTW4$(UJ_9V@3oUcwoNXj>gz0#s5QfMToSc2K z0s^LeCq`=Bpq#fj;e%$#OZ0cTDc%k8ctqw-*zw!A@Sw%oTpZKN=JA_p*k^pp78XF? zMQNR;wtR-n-%e0?fVt%|f^yTf%05DHcM%b^yF4w1w*Z#17~EaWS}ImN&PeQ1e3nei zrv{TnSHy~H(=Cc|j^wPJx+^kV?T+N5F|@d}xE&#A+@{gnCNmu*u2TUfqmWu}!(BMj zZT<(Gp(pWF2d2X&c0rlo>>ZT4o~G<WgwrH)h{40p`^m62C@w$%CS8!3KY$orNA?^-~2K zJJ+I=_gR>hBTaWci&hUWD={AYEQ|at05f)r;TwoHxFG-=TV2s`8X-=72+p@>=d{{SXHjNa)RWT`yeIS(yPe z3GS-U>BWyNBe7Ncwk&0)iq4|r7G66?Yh7DNsWgbs`>UF3dUVGnk6^G0=-o10bfz`k zz*UdwLdh%8udB2->Y96+%L|!d^t|{PpuM)+?@Q<$Xxk)PNC<4r203GMhyeW;p^FbJ zRF3>!G;(3I_Ex%o8L-0FSE6SmuHW)d$kB>CJM5>0hDHYtSC^JA^qetVT>)e?NND=3 zV&gI=ZUIS{41ahaz9{klDPab*Z+i^X10Mmg=(*;%ha06NFn3ph9JvChp!4)X=*d0G zcADrMlPGV9csp7J(m$qO@Y2)xB_AG=_@3bIN1|Dl$uaUtXzlE!ZQMkWWYn{a3=e6v zRt`gTOkJm*6+8BToEW6<^HyGwX3qZrchN&8DvkRV_BqGEWKkX7iodCdOy-ENtXjT6 ziyg!tMb2{MnB=-b7)msZym_7rh*xbq z!1#dSX=HI@C^S}~Xoe!$0v-o5#UOT8J!ek_CptA*!iD1?wcz(fWKvC%c9jK9jpC8B zxklk>rZ(GxFB=+&DdbVL%mTSkmS2q^c2_c^@J92(5B(s9A+g7v%FU_bwBxu}G`ViS zjAO}m-U_Z29E)W+xUC}fft<;P;mSN@`G5+Y(^9y^8szpj9~++P6%{t0qHgGZ?DG^i z+k`B611y^` z!q<1Xj0d`ZE)%k`!~F`_b50Vcst@Va92QpRbnamQnIHvQv`uRpP-8%NWWL0w{X^67 zz9$zC%}}Q4Y_0B_OeL>xHSWFt02k^JlBt%4TI{~EXETWfx7`h^U`ME7%Naa9q$;MD z)RW-N@Os2fX&hUEx8q36h{U*Z2Q-hmy-SWr?`XcI(ZF?#H{j<>1H~3NgbypA{io^M za@m;17KsHvk5$jygH-4~%q(^*D;}sA$EwlcNVO{%h<8U|$w%gbcXhlD+VHSPpx7KK z8^<8>5b!uUKbo1P02aLuC9*^sTtW6zYS`jf3GZE!4I?DSI0UOv{p?`(v=`vTG}v(! z$WpP?c;QQ_z+uRDa0(f&pmPXyKJUIT;opx(Y6|E`~f6y!C#h&|9zJhp9sBXyYH3-2*>cH-45rwmi#Gto4~56k4U4Es*=gO8(`N2s4h}sJB^R(P zEQuqKwnM;8PT3h@S8`@9lG+hCS*4WM70;6#zf?)`u&G!hmvweQ<>ynX(4+z zG04Y~%UvEnlL=~Fhm+}>x(M^KL46PttbH>cCT}eQl6@8lSUgDqSn6F`Tzn@O?R7O8 z(R^^_zev|u&1sNF1Gop#4l=)YPAape<~mYcE3L%ebksX|?zr;H=fsvwoSCMp8)lB% z-B`LZXGe^WPm^f6z3tW%*Nis+$AvX zjzb&!CWGat*b}dd0iQ$v08nv^SsbdlrtXSP;dN~_t&%evs*0xdet_t*Ja_@F=8`7j z_{*R);_V#MRl@M*FKFPPd@@HJ!nf);+p+Gs0ac|AiNsRt$2&#wI(%5i6AkXi@f=3c z$>*{IHTLsJtQ^T5jVwoo0E2W=PGs=K>}h+xKqicSLp8k%W<0I2S-w_<434so(j(nA z>bhO{qE;h$AK>FZk;8q}7dBrKXCC~kZoMysiMFVo_Ni|+eygY0=7+l2k+j;-0nL@H zuJTeWAQ?{Qy&-#*E}8-H|~pB9hLz!vKBG0?6MCl7s9|;11$?1;i_6I4TUL zM;ROFT9ZqAXAJ~{^HKF&eiTe|cu|J?Ic1@@P2{&8akbu3ty#x=1dKMhMjl*|#@Do- z74N6DL79pe@(EikQ(6ydgPA!lq_b6(&`Km6a+%2<*(@cR>kF}p<&>Nn74$vQa>iaD z=J-r;@ywAr)l;M<*b5$MT=3;%URz+=Gm>vbV;0qveOE4R4^(WRpAPv7`=RwrIr!`W zz?xN_xzj!~4f_SUxK)0U!^yy7q@Bb7eN|wTqkhqpoLh<4wFslkfY<~Oxi4Di@?*Kr zji+aIc+mCur*VwD8soYvS;mht2Q|PDLe1{PoK8oePH4vC**ca2Udf~G)5=?^Y3|cC z65u?9ry6Ek1dU{swaGIpT%tA^II;FjOw=@5uQ&>;rop($1G{!tNMZ2fCN5)eNJc+_ zsJXyUC3(1Vag&2RNo7wW$^QT%KZW=S8~~wqnqo)$Kn8aT4Qx5`n(P*OS1d=6<`vb6 zHk^of*%We0KA9$5e+e{j4|P7cD^Dlcs!dWcHghE&g?p%5w?=!9dRDa3MARCSXB*T< ziIC&-Qb!?UTGn@1mzf{+Ti`-u$pLy@B2kfa>FkLci&7NuVn>?}_qTN?i{9+zF4*S22a?$X<@E4>WRvS!jL!)5`8Zy zGcY#$m1xY2Nsb&ntu|3>5o5H<@;6efYV$KXavbYuJ(px99B|m`%<1_O=VZQ8 z;#=sD>$p>7>R=X~irb^Y#xMo|nFygdO=)PqdR|<7iA%_GiXIK8Z_pNjj_iCO*MbMl zI~hA5b7E&CEwfAz<9SHnB-on7Mprkv3Rqz;yCyFMCf`V+^=6Nssb?LGbrPV``b#Sv z&GFbkD*o+G2OS`Q^K!GWeX6Yht} zjn9{CZL$|bO3F(~742#rnC(5uRP3%EeeGVgygZn(yh)o(CW52q3VGwzc6iNtDUYHk zGE7qkHPrg==u@pfF`G+^w%72LT9B>TH85Wl#jg0eIuX*ejaE!&G_l8Tx4N}A;q`2N z&7mc77a-HMNg;N}n;w9xPL-;;j%(s|5$S89GE8K~8up0EV{~&e$D#ELnd@3a+I*yt zT@mc+UBeSH4N?FfO`4K@Ye#G1a~#0tA4HOev$)1}301_)*RFrfioNGkR%=lonmtDB2 z38{b@cO;U?)`ioH$&@3-7schv6gZ$gZ&e~q*IcJM4yy~`Zuhq9-B5a}S2R0e!;Ycc-ddt@Sv@n{MybX0M@~ zl$Eqvo8ZJf0*5Oi7c}i4s!Y93$FO;YpPC*#%!yyUjwQaGJ`S+8&4wECXN?0x-tqGZckw2_z+DN{KlVRtk00cgxdB?L`*UQ2O%FL z+Wb;D?1TCIUqePabXE1-*9iDYb6Yg}c`?~?VXDSwlW4x__fP4pnijF*_~mp$QNyFX|0jzx`E2a zEPo^@B4bWNfgWH`+G`Frpjhr2Q$0c-0%0pV70Hqv@iab2Wq}s|07VXdaK)T7c1^Tq z2V(C&)~S6*mzN zt6+<;HLZqd-@BE(aITEQWYGu5Td7iUBx_^d3T340#hD@^e5fpLbFeP98LC3!3tR{) zwV9{L<8AI2LYa`}LL_Z&NGAoE8%u0`6>-JIjJ`WyJv*m8`3&(r4-2a@Cc~$+8sI8! zl^)re=C``Hx!bWodGt}0$&J6Cg<=790pm&2o?DcF-gQo`z84tO86nL)PDeZu593^B^)p5W5y+TTy@d$Kh0V}Ekd z*s2b*jU;pKm8Gsd$IW{9{D@^*?4x_8Dp(_xlD$gdH7f-AEtaoX#*X@|Q3NfIWuaLZ zXNM;gQNZ0K*w3c`nz6vNA9aEi6ZX6j-qpKFm`Mvj?_J8KG@2nh&@%S8G|@`HAFeSFt9EA$2$>8g4;{A~%EB@{us|5%}-md#*gO zv_?wN*tG1SE}jk8t3t#ZETkoU6O=M>9RrV@pUE_wd?duL2gs9xab1z8R7mKI4+qgP z61DLIbW$m`Xxu{y7Kn~w_^qp!F`HK)CXn_Fu))bpakFEUwRWlER}e#rGY23w^=X|& zQMo@wi!T+Kj3bLE99=E=t{`#{lj<*p<6$(NX}R7z2IRP5@!73j`xWQL@OT_0#!Zjs zg=fz2%T21jZqF2f%IwFD`Y52=6YH7AILT>B^x5Qb+JN6)J0jCi^l*Pv(TxLG99@>i5i73I{GjGH=yxuz_=j_gt5cDaL|>J05L%_X#X zg5TiehB3Sq<#_qlNGmxYxgusT6JP+nEN-d`SRJ(aijYpy;tdto#WZG^JEGd&LL9Jz zxNw=c2af6~Bjd712D&mai#cm;=79H7Kf#}qMwM?_$(kTr71zlw)3tZ8+hjHMUT2Zx zJLeJQ#TIR6RAwYWk&nqzHO)}wMd7l|pH*a-N1G8m5>9&CY1!6s+M&G6k|TIo31JK_Pyx;f)doIxeYSvCe) zW56enpG?T}@kn(OO0ZkDZNsu_iIC{)o)B$hjfofeE4rqD=J=tl;I4VA;FzUzTquIO z+e~xDL8~f-)qOl)E9Gwk=%)$1a;MOD9ix|)_ⅅI?7)>#0PC82XJXH)ZJ1!tvok? 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 literal 0 HcmV?d00001 diff --git a/examples/SX128x_examples/Ranging/223_Ranging_Transmitter_Data_Requestor/223_Ranging_Transmitter_Data_Requestor.ino b/examples/SX128x_examples/Ranging/223_Ranging_Transmitter_Data_Requestor/223_Ranging_Transmitter_Data_Requestor.ino new file mode 100644 index 0000000..ef58860 --- /dev/null +++ b/examples/SX128x_examples/Ranging/223_Ranging_Transmitter_Data_Requestor/223_Ranging_Transmitter_Data_Requestor.ino @@ -0,0 +1,427 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of 'Reliable' + packets to request information or operations from a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + The transmitter sends a request for the receiver which will respond with an acknowledge whach can include + any data requested. The transmitted request is a total of 6 or more bytes, with one byte for the payload + type and another byte for station the request is directed to. + + Two types af request are demonstrated here. The first request is for the receiver to return a set of GPS + co-ordinates. This information is included in the acknowledge the receiver sends if there is a match for + request type and station number. The orginal network ID and payload CRC are also returned with the + acknowledge so the transmitter can verify if the packet it receives in reply is geniune. + + The second request is for the receiver to go into ranging (distance measurment) mode. The period for the + receiver to stay in ranging mode goes out with the request sent by the transmitter. The transmitter then + goes into ranging mode and measures the distance between transmitter (master) and receiver (slave). + + The matching receiver program is 224_Ranging_Receiver_Data_Requestor. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" + +SX128XLT LT; //create a library class instance called LT + +uint8_t TXPacketL; //length of transmitted packet +uint8_t TXPayloadL; +uint8_t RXPayloadL; +uint8_t RXPacketL; //length of received acknowledge +uint16_t PayloadCRC; + +uint8_t RXBUFFER[16]; //create the buffer that received packets are copied into +uint8_t RequestType; //type of request to send +uint8_t RequestStation; //station request is aimed at + +float Latitude; //variables for GPS location +float Longitude; +float Altitude; +uint8_t TrackerStatus; + +//#define DEBUG + + +void loop() +{ + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + //***************************************************************************************** + //Request GPS location + //***************************************************************************************** + + RequestStation = 123; + + if (sendRequestGPSLocation(RequestStation, 5)) + { + Serial.println(F("Valid ACK received")); +#ifdef DEBUG + packet_is_OK(); +#endif + Serial.println(); + actionRequestGPSLocation(); + } + else + { + Serial.println(F("No valid ACK received")); + } + + Serial.println(); + Serial.println(); + delay(1000); + + //***************************************************************************************** + + + //***************************************************************************************** + //Request ranging operation + //***************************************************************************************** + + RequestStation = 123; + + if (sendRequestRanging(RequestStation, 5)) //send request for station 123, make 5 attempts + { + Serial.println(F("Valid ACK received")); +#ifdef DEBUG + packet_is_OK(); +#endif + Serial.println(); + //note that RequestStation = the ranging address + actionRanging(RequestStation, RangingCount, RangingTimeoutmS, RangingTXPower); + } + else + { + Serial.println(F("No valid ACK received")); + } + + Serial.println(); + delay(5000); + + //***************************************************************************************** +} + + +uint8_t sendRequestGPSLocation(uint8_t station, uint8_t sendcount) +{ + uint8_t ackrequesttype; + uint8_t ackstation; + uint8_t startcount; + + startcount = sendcount; + + do + { + Serial.print(startcount - sendcount + 1); + Serial.print(F(" Transmit request")); + printRequestType(RequestGPSLocation); + Serial.print(F(" to station ")); + Serial.println(station); + Serial.flush(); + + //build the request payload + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of request + LT.writeUint8(station); //station to reply to request + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + + //now transmit the request + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXReliable(0, TXPayloadL, NetworkID, TXtimeout, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); + RXPacketL = LT.waitSXReliableACK(0, NetworkID, PayloadCRC, ACKtimeout); + + if (RXPacketL > 0) + { + //if waitSXReliableACK() returns > 0 then valid ack was received + ackrequesttype = LT.getByteSXBuffer(0); + ackstation = LT.getByteSXBuffer(1); + + if ((ackrequesttype == RequestGPSLocation) && (ackstation == station)) + { + return RXPacketL; + } + delay(200); //small delay between tranmission attampts + } + sendcount--; + } + while ((RXPacketL == 0) && (sendcount > 0)); + + Serial.println(F("ERROR send request failed")); + return 0; +} + + +uint8_t sendRequestRanging(uint8_t station, uint8_t sendcount) +{ + uint8_t ackrequesttype; + uint8_t ackstation; + uint8_t startcount; + + startcount = sendcount; + + do + { + Serial.print(startcount - sendcount + 1); + Serial.print(F(" Transmit request")); + printRequestType(RequestRanging); + Serial.print(F(" to station ")); + Serial.println(station); + Serial.flush(); + + //build the request payload + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestRanging); //identify type of request + LT.writeUint8(station); //station to reply to request + LT.writeUint32(RangingUpTimemS); //time for slave to stay in ranging listen + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + + //now transmit the request + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXReliable(0, TXPayloadL, NetworkID, TXtimeout, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); + RXPacketL = LT.waitSXReliableACK(0, NetworkID, PayloadCRC, ACKtimeout); + + if (RXPacketL > 0) + { + //if waitSXReliableACK() returns > 0 then valid ack was received + ackrequesttype = LT.getByteSXBuffer(0); + ackstation = LT.getByteSXBuffer(1); + + if ((ackrequesttype == RequestRanging) && (ackstation == station)) + { + return RXPacketL; + } + delay(200); //small delay between tranmission attampts + } + sendcount--; + } + while ((RXPacketL == 0) && (sendcount > 0)); + + Serial.println(F("ERROR send request failed")); + return 0; +} + + +void printRequestType(uint8_t type) +{ + switch (type) + { + case 1: + Serial.print(F(" RequestGPSLocation")); + break; + + case 2: + Serial.print(F(" RequestRanging")); + break; + + default: + Serial.print(F(" not recognised")); + break; + } +} + + +void actionRequestGPSLocation() +{ + LT.startReadSXBuffer(0); //initialise SX buffer write at address 0 + RequestType = LT.readUint8(); //read type of request + RequestStation = LT.readUint8(); //who is the request reply from + Latitude = LT.readFloat(); //read latitude + Longitude = LT.readFloat(); //read longitude + Altitude = LT.readFloat(); //read altitude + TrackerStatus = LT.readUint8(); //read status byte + RXPayloadL = LT.endReadSXBuffer(); //close SX buffer read + + Serial.print(F("RequestGPSLocation reply > ")); + Serial.print(RequestStation); + Serial.print(F(",")); + Serial.print(Latitude, 6); + Serial.print(F(",")); + Serial.print(Longitude, 6); + Serial.print(F(",")); + Serial.print(Altitude, 1); + Serial.print(F(",")); + Serial.print(TrackerStatus); + Serial.println(); +} + + +void actionRanging(uint32_t rangingaddress, uint8_t number, uint32_t timeout, int8_t txpower) +{ + uint8_t index; + uint16_t rangeings_valid = 0; + float distance_sum = 0; + int16_t RangingRSSI; + uint16_t rangeing_errors = 0; + uint16_t rangeing_results = 0; + uint16_t IrqStatus; + uint32_t range_result_sum = 0; + uint32_t range_result_average; + float distance; + float distance_average; + int32_t range_result; + + Serial.print(F("Ranging address ")); + Serial.println(rangingaddress); + Serial.flush(); + + LT.setupRanging(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate, rangingaddress, RANGING_MASTER); + LT.setRangingCalibration(Calibration); //override automatic lookup of calibration value from library table + + for (index = 1; index <= number; index++) + { + Serial.print(F("Ranging attempt ")); + Serial.println(index); + + digitalWrite(LED1, HIGH); + LT.transmitRanging(rangingaddress, timeout, txpower, WAIT_TX); + digitalWrite(LED1, LOW); + + IrqStatus = LT.readIrqStatus(); + + if (IrqStatus & IRQ_RANGING_MASTER_RESULT_VALID) + { + rangeing_results++; + rangeings_valid++; + digitalWrite(LED1, HIGH); + Serial.print(F("RangingValid")); + range_result = LT.getRangingResultRegValue(RANGING_RESULT_RAW); +#ifdef DEBUG + Serial.print(F(",RAWRegisterValue,")); + Serial.print(range_result); + Serial.print(F(",")); +#endif + if (range_result > 800000) + { + range_result = 0; + } + range_result_sum = range_result_sum + range_result; + + distance = LT.getRangingDistance(RANGING_RESULT_RAW, range_result, DistanceAdjustment); + distance_sum = distance_sum + distance; + + Serial.print(F("Distance,")); + Serial.print(distance, 1); + RangingRSSI = LT.getRangingRSSI(); + Serial.print(F(",RSSI,")); + Serial.print(RangingRSSI); + Serial.print(F("dBm")); + digitalWrite(LED1, LOW); + } + else + { + rangeing_errors++; + distance = 0; + range_result = 0; + Serial.print(F("ERROR not valid")); + LT.printIrqStatus(); + } + delay(PacketDelaymS); + + if (index == number) + { + range_result_average = (range_result_sum / rangeing_results); + + if (rangeing_results == 0) + { + distance_average = 0; + } + else + { + distance_average = (distance_sum / rangeing_results); + } + + Serial.println(); + Serial.print(F("TotalValid,")); + Serial.print(rangeings_valid); + Serial.print(F(",TotalErrors,")); + Serial.print(rangeing_errors); + Serial.print(F(",AverageRAWResult,")); + Serial.print(range_result_average); + Serial.print(F(",AverageDistance,")); + Serial.print(distance_average, 1); + Serial.flush(); + } + Serial.println(); + } +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(115200); + Serial.println(__FILE__); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + } + else + { + Serial.println(F("ERROR No LoRa device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Ranging/223_Ranging_Transmitter_Data_Requestor/Settings.h b/examples/SX128x_examples/Ranging/223_Ranging_Transmitter_Data_Requestor/Settings.h new file mode 100644 index 0000000..f83bb49 --- /dev/null +++ b/examples/SX128x_examples/Ranging/223_Ranging_Transmitter_Data_Requestor/Settings.h @@ -0,0 +1,39 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +const uint8_t NSS = 10; //select pin on LoRa device +const uint8_t NRESET = 9; //reset pin on LoRa device +const uint8_t RFBUSY = 7; //busy pin on LoRa device +const uint8_t DIO1 = 3; //DIO1 pin on LoRa device, used for sensing RX and TX done +const uint8_t LORA_DEVICE = DEVICE_SX1280; //we need to define the device we are using +const uint8_t LED1 = 8; + +//******* Setup LoRa modem parameters here ! *************** +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 2; //LoRa transmit power +const uint8_t Bandwidth = LORA_BW_0800; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF8; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter +const uint8_t RequestGPSLocation = 1; //request type number for GPS location, so receiver knows what information to supply +const uint8_t RequestRanging = 2; //request type number for ranging + +const uint32_t ACKtimeout = 1000; //Acknowledge timeout in mS, set to 0 if ACK not used. +const uint32_t TXtimeout = 1000; //transmit timeout in mS. If 0 return from transmit function after send. + +const int8_t RangingTXPower = 10; //Ranging transmit power used +const uint16_t RangingTimeoutmS = 1000; //ranging master timeout in mS, time master waits for a reply +const uint16_t RangingUpTimemS = 2000; //time for slave to stay in ranging listen +const uint16_t PacketDelaymS = 0; //forced extra delay in mS between sending packets +const uint16_t RangingCount = 3; //number of times ranging is carried out for each distance measurment +const float DistanceAdjustment = 1.0000; //adjustment factor to calculated distance +const uint16_t Calibration = 11350; //Manual Ranging calibration value diff --git a/examples/SX128x_examples/Ranging/224_Ranging_Receiver_Data_Requestor/224_Ranging_Receiver_Data_Requestor.ino b/examples/SX128x_examples/Ranging/224_Ranging_Receiver_Data_Requestor/224_Ranging_Receiver_Data_Requestor.ino new file mode 100644 index 0000000..73ae38b --- /dev/null +++ b/examples/SX128x_examples/Ranging/224_Ranging_Receiver_Data_Requestor/224_Ranging_Receiver_Data_Requestor.ino @@ -0,0 +1,322 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 17/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of 'Reliable' + packets to request information or operations from a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + The transmitter sends a request for the receiver which will respond with an acknowledge whach can include + any data requested. The transmitted request is a total of 6 or more bytes, with one byte for the payload + type and another byte for station the request is directed to. + + Two types af request are demonstrated here. The first request is for the receiver to return a set of GPS + co-ordinates. This information is included in the acknowledge the receiver sends if there is a match for + request type and station number. The orginal network ID and payload CRC are also returned with the + acknowledge so the transmitter can verify if the packet it receives in reply is geniune. + + The second request is for the receiver to go into ranging (distance measurment) mode. The period for the + receiver to stay in ranging mode goes out with the request sent by the transmitter. The transmitter then + goes into ranging mode and measures the distance between transmitter (master) and receiver (slave). + + The matching transmitter program is 224_Ranging_transmitter_Data_Requestor. + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" + +SX128XLT LT; //create a library class instance called LT + +uint8_t RXBUFFER[251]; //create the buffer that received packets are copied into +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t TXPayloadL; //stores length of payload sent +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet +uint16_t StationNumber; +uint8_t RequestType; + +//#define DEBUG + + +void loop() +{ + uint8_t requesttype; + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); //need to be in LoRa mode to receiv requests + + requesttype = waitRequest(ThisStation, 5000); //listen for 5000mS, will return 0 if there is no request + + switch (requesttype) + { + case 0: + break; + + case RequestGPSLocation: + actionRequestGPSLocation(); + break; + + case RequestRanging: + actionRanging(ThisStation); + break; + + default: + Serial.println(F("Request not recognised")); + break; + } + Serial.println(); +} + + +uint8_t waitRequest(uint8_t station, uint32_t timeout) +{ + //wait for an incoming request, returns 0 if no request in timeout period + + Serial.println(F("Wait request")); + + RXPacketL = LT.receiveSXReliable(0, NetworkID, timeout, WAIT_RX); + + if (RXPacketL) + { +#ifdef DEBUG + Serial.print(F("Reliable packet received > ")); + packet_is_OK(); +#endif + RequestType = LT.getByteSXBuffer(0); + StationNumber = LT.getByteSXBuffer(1); + + Serial.print(F("Received ")); + printRequestType(RequestType); + Serial.print(F(" for station ")); + Serial.println(StationNumber); + + if (StationNumber == station) + { + return RequestType; + } + else + { + Serial.println(F("ERROR Request not for this station")); + } + } + else + { + if (LT.readIrqStatus() & IRQ_RX_TIMEOUT) + { + Serial.println(F("ERROR Timeout waiting for valid request")); + } + else + { + packet_is_Error(); + } + } + return 0; +} + + +bool actionRequestGPSLocation() +{ + uint16_t RXPayloadCRC; + + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); //fetch received payload crc to return with ack + + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of request + LT.writeUint8(ThisStation); //who is the request reply from + LT.writeFloat(TestLatitude); //add latitude + LT.writeFloat(TestLongitude); //add longitude + LT.writeFloat(TestAltitude); //add altitude + LT.writeUint8(TrackerStatus); //add status byte + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + + delay(ACKdelay); + + digitalWrite(LED1, HIGH); + LT.sendSXReliableACK(0, TXPayloadL, NetworkID, RXPayloadCRC, TXpower); + Serial.print(F("RequestGPSLocation Replied > ")); + Serial.print(ThisStation); + Serial.print(F(",")); + Serial.print(TestLatitude, 6); + Serial.print(F(",")); + Serial.print(TestLongitude, 6); + Serial.print(F(",")); + Serial.print(TestAltitude, 1); + Serial.print(F(",")); + Serial.print(TrackerStatus); + Serial.println(); + Serial.flush(); + digitalWrite(LED1, LOW); + return true; +} + + +void actionRanging(uint32_t rangingaddress) +{ + uint16_t IrqStatus; + uint16_t RangingUpTimemS; + uint16_t RXPayloadCRC; + uint32_t startmS; + + LT.startReadSXBuffer(2); //initialise SX buffer write at location 2 + RangingUpTimemS = LT.readUint16(); //the ranging listen time was sent with request. + LT.endReadSXBuffer(); + + Serial.print("RangingUpTimemS "); + Serial.println(RangingUpTimemS); + + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); //fetch received payload crc to return with ack + + LT.startWriteSXBuffer(0); //initialise SX buffer write at address startaddr + LT.writeUint8(RequestRanging); //identify type of request + LT.writeUint8(ThisStation); //who is the request reply from + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + + delay(ACKdelay); + + LT.sendSXReliableACK(0, TXPayloadL, NetworkID, RXPayloadCRC, TXpower); + Serial.println(F("Ranging request replied")); + + LT.setupRanging(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate, rangingaddress, RANGING_SLAVE); + LT.receiveRanging(rangingaddress, 0, RangingTXPower, NO_WAIT); + + startmS = millis(); + + digitalWrite(LED1, HIGH); + + do + { + if (digitalRead(DIO1)) + { + IrqStatus = LT.readIrqStatus(); + + if (IrqStatus & IRQ_RANGING_SLAVE_RESPONSE_DONE) + { + Serial.print("Ranging response sent"); + LT.receiveRanging(ThisStation, 0, RangingTXPower, NO_WAIT); + } + else + { + Serial.print("ERROR Ranging slave error,"); + Serial.print(",Irq,"); + Serial.print(IrqStatus, HEX); + LT.printIrqStatus(); + } + Serial.println(); + } + + } + while ( ((uint32_t) (millis() - startmS) < RangingUpTimemS)); + + Serial.println("Ranging receive timeout!!"); + digitalWrite(LED1, LOW); +} + + +void printRequestType(uint8_t type) +{ + switch (type) + { + case RequestGPSLocation: + Serial.print(F(" RequestGPSLocation")); + break; + + case RequestRanging: + Serial.print(F(" RequestRanging")); + break; + + default: + Serial.print(F(" Request type not recognised")); + break; + } +} + + +void packet_is_OK() +{ + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void printPacketDetails() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(LT.getRXNetworkID(RXPacketL), HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(LT.getRXPayloadCRC(RXPacketL), HEX); + LT.printReliableStatus(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(115200); + Serial.println(__FILE__); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + } + else + { + Serial.println(F("ERROR No LoRa device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Ranging/224_Ranging_Receiver_Data_Requestor/Settings.h b/examples/SX128x_examples/Ranging/224_Ranging_Receiver_Data_Requestor/Settings.h new file mode 100644 index 0000000..3cf6250 --- /dev/null +++ b/examples/SX128x_examples/Ranging/224_Ranging_Receiver_Data_Requestor/Settings.h @@ -0,0 +1,39 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +const uint8_t NSS = 10; //select pin on LoRa device +const uint8_t NRESET = 9; //reset pin on LoRa device +const uint8_t RFBUSY = 7; //busy pin on LoRa device +const uint8_t DIO1 = 3; //DIO1 pin on LoRa device, used for sensing RX and TX done +const uint8_t LED1 = 8; +const uint8_t LORA_DEVICE = DEVICE_SX1280; //we need to define the device we are using + + +//******* Setup LoRa modem parameters here ! *************** +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 2; //LoRa transmit power +const int8_t RangingTXPower = 10; //Transmit power used for ranging +const uint8_t Bandwidth = LORA_BW_0800; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF8; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint32_t ACKdelay = 200; //delay in mS before sending reply +const uint32_t RXtimeout = 5000; //receive timeout in mS. + +const uint8_t RequestGPSLocation = 1; //request type for GPS location +const uint8_t RequestRanging = 2; //request type for ranging + +//GPS co-ordinates to use for the GPS location request +const float TestLatitude = 51.48230; //GPS co-ordinates to use for test +const float TestLongitude = -3.18136; //Cardiff castle keep, used for testing purposes +const float TestAltitude = 25.5; +const uint8_t TrackerStatus = 1; //set status bit to represent tracker GPS has fix + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter +const uint8_t ThisStation = 123; //the number of this station for requests and ranging diff --git a/examples/SX128x_examples/Ranging/54_Ranging_Master/54_Ranging_Master.ino b/examples/SX128x_examples/Ranging/54_Ranging_Master/54_Ranging_Master.ino new file mode 100644 index 0000000..db917e9 --- /dev/null +++ b/examples/SX128x_examples/Ranging/54_Ranging_Master/54_Ranging_Master.ino @@ -0,0 +1,224 @@ +/***************************************************************************************************** + Programs for Arduino - Copyright of the author Stuart Robinson - 16/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +#ifdef ENABLEOLED +#include //https://github.com/olikraus/u8g2 +//U8X8_SSD1306_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //standard 0.96" SSD1306 +U8X8_SH1106_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //1.3" OLED often sold as 1.3" SSD1306 +#endif + +uint16_t rangeing_errors, rangeings_valid, rangeing_results; +uint16_t IrqStatus; +uint32_t endwaitmS, startrangingmS, range_result_sum, range_result_average; +float distance, distance_sum, distance_average; +bool ranging_error; +int32_t range_result; +int16_t RangingRSSI; + + +void loop() +{ + uint8_t index; + distance_sum = 0; + range_result_sum = 0; + rangeing_results = 0; //count of valid results in each loop + + for (index = 1; index <= rangeingcount; index++) + { + + startrangingmS = millis(); + + Serial.println(F("Start Ranging")); + + LT.transmitRanging(RangingAddress, TXtimeoutmS, RangingTXPower, WAIT_TX); + + IrqStatus = LT.readIrqStatus(); + + if (IrqStatus & IRQ_RANGING_MASTER_RESULT_VALID) + { + rangeing_results++; + rangeings_valid++; + digitalWrite(LED1, HIGH); + Serial.print(F("Valid")); + range_result = LT.getRangingResultRegValue(RANGING_RESULT_RAW); + Serial.print(F(",Register,")); + Serial.print(range_result); + + if (range_result > 800000) + { + range_result = 0; + } + range_result_sum = range_result_sum + range_result; + + distance = LT.getRangingDistance(RANGING_RESULT_RAW, range_result, distance_adjustment); + distance_sum = distance_sum + distance; + + Serial.print(F(",Distance,")); + Serial.print(distance, 1); + Serial.print(F(",RSSIReg,")); + Serial.print(LT.readRegister(REG_RANGING_RSSI)); + RangingRSSI = LT.getRangingRSSI(); + Serial.print(F(",RSSI,")); + Serial.print(RangingRSSI); + Serial.print(F("dBm")); + digitalWrite(LED1, LOW); + } + else + { + rangeing_errors++; + distance = 0; + range_result = 0; + Serial.print(F("NotValid")); + Serial.print(F(",Irq,")); + Serial.print(IrqStatus, HEX); + } + delay(packet_delaymS); + + if (index == rangeingcount) + { + range_result_average = (range_result_sum / rangeing_results); + + if (rangeing_results == 0) + { + distance_average = 0; + } + else + { + distance_average = (distance_sum / rangeing_results); + } + + Serial.print(F(",TotalValid,")); + Serial.print(rangeings_valid); + Serial.print(F(",TotalErrors,")); + Serial.print(rangeing_errors); + Serial.print(F(",AverageRAWResult,")); + Serial.print(range_result_average); + Serial.print(F(",AverageDistance,")); + Serial.print(distance_average, 1); + +#ifdef ENABLEDISPLAY + display_screen1(); +#endif + + delay(2000); + + } + Serial.println(); + } + +} + +#ifdef ENABLEDISPLAY +void display_screen1() +{ + disp.clear(); + disp.setCursor(0, 0); + disp.print(F("Distance ")); + disp.print(distance_average, 1); + disp.print(F("m")); + disp.setCursor(0, 2); + disp.print(F("RSSI ")); + disp.print(RangingRSSI); + disp.print(F("dBm")); + disp.setCursor(0, 4); + disp.print(F("OK,")); + disp.print(rangeings_valid); + disp.print(F(",Err,")); + disp.print(rangeing_errors); +} +#endif + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(4, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + Serial.println(F("54_Ranging_Master Starting")); + + SPI.begin(); + + led_Flash(2, 125); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast flash indicates device error + } + } + + LT.setupRanging(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate, RangingAddress, RANGING_MASTER); + + //LT.setRangingCalibration(Calibration); //override automatic lookup of calibration value from library table + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers, normally 0x900 to 0x9FF + Serial.println(); + Serial.println(); + + +#ifdef ENABLEDISPLAY + Serial.println("Display Enabled"); + disp.begin(); + disp.setFont(u8x8_font_chroma48medium8_r); + disp.setCursor(0, 0); + disp.print(F("Ranging RAW Ready")); + disp.setCursor(0, 1); + disp.print(F("Power ")); + disp.print(RangingTXPower); + disp.print(F("dBm")); + disp.setCursor(0, 2); + disp.print(F("Cal ")); + disp.print(Calibration); + disp.setCursor(0, 3); + disp.print(F("Adjust ")); + disp.print(distance_adjustment, 4); +#endif + + Serial.print(F("Address ")); + Serial.println(RangingAddress); + Serial.print(F("CalibrationValue ")); + Serial.println(LT.getSetCalibrationValue()); + Serial.println(F("Ranging master RAW ready")); + + delay(2000); +} diff --git a/examples/SX128x_examples/Ranging/54_Ranging_Master/Settings.h b/examples/SX128x_examples/Ranging/54_Ranging_Master/Settings.h new file mode 100644 index 0000000..74eda00 --- /dev/null +++ b/examples/SX128x_examples/Ranging/54_Ranging_Master/Settings.h @@ -0,0 +1,42 @@ +/***************************************************************************************************** + Programs for Arduino - Copyright of the author Stuart Robinson - 04/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions in hz +const int32_t Offset = 0; //offset frequency in hz for calibration purposes +const uint8_t Bandwidth = LORA_BW_0800; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF8; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint16_t Calibration = 11350; //Manual Ranging calibrarion value + +const int8_t RangingTXPower = 10; //Transmit power used +const uint32_t RangingAddress = 16; //must match address in recever + +const uint16_t waittimemS = 10000; //wait this long in mS for packet before assuming timeout +const uint16_t TXtimeoutmS = 5000; //ranging TX timeout in mS +const uint16_t packet_delaymS = 0; //forced extra delay in mS between ranging requests +const uint16_t rangeingcount = 5; //number of times ranging is cqarried out for each distance measurment +float distance_adjustment = 1.0000; //adjustment factor to calculated distance + + +#define ENABLEOLED //enable this define to use display +#define ENABLEDISPLAY //enable this define to use display diff --git a/examples/SX128x_examples/Ranging/55_Ranging_Slave/55_Ranging_Slave.ino b/examples/SX128x_examples/Ranging/55_Ranging_Slave/55_Ranging_Slave.ino new file mode 100644 index 0000000..b080b9f --- /dev/null +++ b/examples/SX128x_examples/Ranging/55_Ranging_Slave/55_Ranging_Slave.ino @@ -0,0 +1,137 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 16/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - + + Serial monitor baud rate is set at 9600 +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint32_t endwaitmS; +uint16_t IrqStatus; +uint32_t response_sent; + + +void loop() +{ + LT.receiveRanging(RangingAddress, 0, TXpower, NO_WAIT); + + endwaitmS = millis() + rangingRXTimeoutmS; + + while (!digitalRead(DIO1) && (millis() <= endwaitmS)); //wait for Ranging valid or timeout + + if (millis() >= endwaitmS) + { + Serial.println("Error - Ranging Receive Timeout!!"); + led_Flash(2, 100); //single flash to indicate timeout + } + else + { + IrqStatus = LT.readIrqStatus(); + digitalWrite(LED1, HIGH); + + if (IrqStatus & IRQ_RANGING_SLAVE_RESPONSE_DONE) + { + response_sent++; + Serial.print(response_sent); + Serial.print(" Response sent"); + } + else + { + Serial.print("Slave error,"); + Serial.print(",Irq,"); + Serial.print(IrqStatus, HEX); + LT.printIrqStatus(); + } + digitalWrite(LED1, LOW); + Serial.println(); + } + +} + + +void led_Flash(unsigned int flashes, unsigned int delaymS) +{ + //flash LED to show board is alive + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + Serial.begin(9600); //setup Serial console ouput + Serial.println(); + Serial.println("55_Ranging_Slave Starting"); + + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + //The function call list below shows the complete setup for the LoRa device for ranging using the information + //defined in the Settings.h file. + //The 'Setup LoRa device for Ranging' list below can be replaced with a single function call, note that + //the calibration value will be loaded automatically from the table in the library; + //LT.setupRanging(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate, RangingAddress, RangingRole); + + LT.setupRanging(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate, RangingAddress, RANGING_SLAVE); + + //*************************************************************************************************** + //Setup LoRa device for Ranging Slave + //*************************************************************************************************** + /* + LT.setMode(MODE_STDBY_RC); + LT.setPacketType(PACKET_TYPE_RANGING); + LT.setModulationParams(SpreadingFactor, Bandwidth, CodeRate); + LT.setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 0, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + LT.setRfFrequency(Frequency, Offset); + LT.setTxParams(TXpower, RADIO_RAMP_02_US); + LT.setRangingMasterAddress(RangingAddress); + LT.setRangingSlaveAddress(RangingAddress); + LT.setRangingCalibration(LT.lookupCalibrationValue(SpreadingFactor, Bandwidth)); + LT.setRangingRole(RANGING_SLAVE); + LT.writeRegister(REG_RANGING_FILTER_WINDOW_SIZE, 8); //set up window size for ranging averaging + LT.setHighSensitivity(); + */ + //*************************************************************************************************** + + LT.setRangingCalibration(11300); //override automatic lookup of calibration value from library table + + Serial.print(F("Calibration,")); + Serial.println(LT.getSetCalibrationValue()); //reads the calibratuion value currently set + delay(2000); +} diff --git a/examples/SX128x_examples/Ranging/55_Ranging_Slave/Settings.h b/examples/SX128x_examples/Ranging/55_Ranging_Slave/Settings.h new file mode 100644 index 0000000..041c542 --- /dev/null +++ b/examples/SX128x_examples/Ranging/55_Ranging_Slave/Settings.h @@ -0,0 +1,34 @@ +/***************************************************************************************************** + Programs for Arduino - Copyright of the author Stuart Robinson - 16/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions in hz +const int32_t Offset = 0; //offset frequency in hz for calibration purposes +const uint8_t Bandwidth = LORA_BW_0800; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF8; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint16_t Calibration = 11350; //Manual Ranging calibration value + +const int8_t TXpower = 10; //Transmit power used +const uint32_t RangingAddress = 16; //must match address in master + +const uint16_t rangingRXTimeoutmS = 0xFFFF; //ranging RX timeout in mS diff --git a/examples/SX128x_examples/Ranging/56_Ranging_Calibration_Checker/56_Ranging_Calibration_Checker.ino b/examples/SX128x_examples/Ranging/56_Ranging_Calibration_Checker/56_Ranging_Calibration_Checker.ino new file mode 100644 index 0000000..f3789ca --- /dev/null +++ b/examples/SX128x_examples/Ranging/56_Ranging_Calibration_Checker/56_Ranging_Calibration_Checker.ino @@ -0,0 +1,190 @@ +/***************************************************************************************************** + Programs for Arduino - Copyright of the author Stuart Robinson - 17/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +#include +#include +SX128XLT LT; +#include "Settings.h" + +uint8_t distance_zero_count; +uint16_t calvalue, calibrationstart, calibrationend; +uint16_t rangeing_error_count, rangeing_valid_count; +uint16_t IrqStatus; +uint32_t endwaitmS, startrangingmS, range_result; +float distance; + + +void loop() +{ + uint16_t index; + distance_zero_count = 0; + + for (index = calibrationstart; index <= calibrationend; index = index + 10) + { + digitalWrite(LED1, HIGH); + LT.setRangingCalibration(index); + Serial.print(F("TransmitRanging,Calibration,")); + Serial.print(index); + LT.transmitRanging(RangingAddress, TXtimeoutmS, TXpower, NO_WAIT); + digitalWrite(LED1, LOW); + + endwaitmS = millis() + waittimemS; + startrangingmS = millis(); + + while (!(digitalRead(DIO1)) && (millis() < endwaitmS)); //wait for Ranging valid or timeout + + delay(10); + + IrqStatus = LT.readIrqStatus(); + Serial.print(F(",IRQ,")); + Serial.print(IrqStatus, HEX); + + if (IrqStatus & IRQ_RANGING_MASTER_RESULT_TIMEOUT) + { + rangeing_error_count++; + Serial.print(F(", RangingTimeout! ")); + } + + if (millis() > endwaitmS) + { + Serial.print(F(",ProgramTimeout")); + } + + if (IrqStatus & IRQ_RANGING_MASTER_RESULT_VALID) + { + rangeing_valid_count++; + digitalWrite(LED1, HIGH); + Serial.print(F(",Valid")); + range_result = LT.getRangingResultRegValue(RANGING_RESULT_RAW); + Serial.print(F(",RAW,")); + Serial.print(range_result, HEX); + + distance = LT.getRangingDistance(RANGING_RESULT_RAW, range_result, 1); + + Serial.print(F(",Distance,")); + Serial.print(distance, 1); + Serial.print(F("m")); + Serial.print(F(",Time,")); + Serial.print(millis() - startrangingmS); + Serial.print("mS"); + digitalWrite(LED1, LOW); + } + + Serial.print(F(",OKCount,")); + Serial.print(rangeing_valid_count); + Serial.print(F(",ErrorCount,")); + Serial.print(rangeing_error_count); + + if (distance == 0) + { + Serial.print(F(", Distance is Zero!")); + distance_zero_count++; + } + + if (distance_zero_count >= 3) + { + delay(5000); + break; + } + + Serial.println(); + delay(packet_delaymS); + + } + + Serial.println(); + Serial.println(); + Serial.println(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + uint16_t Remainder; + Serial.println(); + Serial.println(); + Serial.begin(9600); //setup Serial console ouput + Serial.println(); + + Serial.println(F("56_Ranging_Calibration_Checker Starting")); + + pinMode(LED1, OUTPUT); + + led_Flash(2, 125); + + Serial.println(F("Checking device")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + //The function call list below shows the complete setup for the LoRa device for ranging using the information + //defined in the Settings.h file. + //The 'Setup LoRa device for Ranging' list below can be replaced with a single function call, note that + //the calibration value will be loaded automatically from the table in the library; + + //LT.setupRanging(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate, RangingAddress, RangingRole); + + //*************************************************************************************************** + //Setup LoRa device for Ranging Master + //*************************************************************************************************** + LT.setMode(MODE_STDBY_RC); + LT.setPacketType(PACKET_TYPE_RANGING); + LT.setModulationParams(SpreadingFactor, Bandwidth, CodeRate); + LT.setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 0, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + LT.setRfFrequency(Frequency, Offset); + LT.setTxParams(TXpower, RADIO_RAMP_02_US); + LT.setRangingMasterAddress(RangingAddress); + LT.setRangingSlaveAddress(RangingAddress); + LT.setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RANGING_MASTER_RESULT_VALID + IRQ_RANGING_MASTER_RESULT_TIMEOUT), 0, 0); //set for IRQ on RX done + LT.setRangingCalibration(LT.lookupCalibrationValue(SpreadingFactor, Bandwidth)); + LT.setRangingRole(RANGING_MASTER); + LT.writeRegister(REG_RANGING_FILTER_WINDOW_SIZE, 8); //set up window size for ranging averaging + LT.setHighSensitivity(); + //*************************************************************************************************** + + //LT.setRangingCalibration(Calibration); //override automatic lookup of calibration value from library table + + calvalue = LT.getSetCalibrationValue(); + Remainder = calvalue / 10; + calibrationstart = (Remainder * 10) - 1000; + calibrationend = (Remainder * 10) + 1000; + Serial.print(F("CalibrationStart,")); + Serial.print(calibrationstart); + Serial.print(F(",CalibrationEnd,")); + Serial.println(calibrationend); + Serial.println(); + + delay(2000); +} diff --git a/examples/SX128x_examples/Ranging/56_Ranging_Calibration_Checker/Settings.h b/examples/SX128x_examples/Ranging/56_Ranging_Calibration_Checker/Settings.h new file mode 100644 index 0000000..95f0c80 --- /dev/null +++ b/examples/SX128x_examples/Ranging/56_Ranging_Calibration_Checker/Settings.h @@ -0,0 +1,40 @@ +/***************************************************************************************************** + Programs for Arduino - Copyright of the author Stuart Robinson - 17/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions in hz +const int32_t Offset = 0; //offset frequency in hz for calibration purposes +const uint8_t Bandwidth = LORA_BW_0800; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF8; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint16_t Calibration = 11426; //Manual Ranging calibrarion value + +const uint8_t RangingRole = RANGING_SLAVE; //Ranging role, RANGING_MASTER or RANGING_SLAVE +const int8_t TXpower = 10; //Transmit power used +const uint32_t RangingAddress = 16; //must match address in recever + +const uint16_t waittimemS = 10000; //wait this long in mS for packet before assuming timeout +const uint16_t TXtimeoutmS = 5000; //ranging TX timeout in mS +const uint16_t RXtimeoutmS = 0xFFFF; //ranging RX timeout in mS +const uint16_t packet_delaymS = 250; //forced extra delay in mS between ranging requests +const uint16_t rangeingcount = 5; //number of times ranging is cqarried out for each distance measurment +float distance_adjustment = 1; //adjustment factor to calculated distance diff --git a/examples/SX128x_examples/Ranging/85km Ranging.jpg b/examples/SX128x_examples/Ranging/85km Ranging.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e445c9f619d9c6389e741f529879b8bcde6311fb GIT binary patch literal 227865 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
JH(CgxD^RlGlI!$Lu^?=1 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�W_e`8zg_;b0252p9w4X(Ok|xiL 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ⅅI?7)>#0PC82XJXH)ZJ1!tvok? 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 literal 0 HcmV?d00001 diff --git a/examples/SX128x_examples/ReadMe.md b/examples/SX128x_examples/ReadMe.md new file mode 100644 index 0000000..081ee1b --- /dev/null +++ b/examples/SX128x_examples/ReadMe.md @@ -0,0 +1,395 @@ +--- +layout: post +title: "Reliable Packets with LoRa" +--- + +## Reliable Packets + +It would be helpful, and in some cases essential, if you could be really sure that a received LoRa packet was actually destined for a particular receiver and that it also contained valid sensor or control data for the application the receiver is running. + +It is inevitable that at some stage a LoRa receiver will get a packet from a foreign or unwanted source, so some thought needs to be done to be sure these rogue packets are rejected. + +### Sending and receiving reliable packets with cyclic redundancy checks + +The examples in this folder use a series of SX12xx library routines that append to the transmitted packets the CRC of the data payload. The receiver can then read this CRC from the packet and then do a local CRC check on the received payload to check there is a match. If there is no match, the packet is automatically rejected. + +Additional protection is added to the packet by the use of a 16 bit NetworkID. The NetworkID number (0-65535) is defined for a particular application so the transmitter and receiver need to use the same NetworkID. The receiver will then reject packets that do not match its own defined NetworkID. The NetworkID can also be used to direct packets to particular receivers. + +The CRC and NetworkID checking operates within the library transmit and receive functions in the background and although there are library routines to extract the payload CRC and NetworkID, there in no compelling need for the Arduino sketch itself to do so. + +The packet structure used for a reliable packet is; + + + +The use of the payload CRC and NetworkID adds a high level of certainty that the packet received is valid and safe to act on. If you so wish the CRC checking can be disabled and the NetworkID alone used to check packet validity. + + +## Reliable packet demonstration examples + +There are a number of examples of using reliable packets in this folder. The simplest examples are 201\_Reliable\_Transmitter and 201\_Reliable\_Receiver which transmit and receive a short payload 'Hello World'. + +### Examples '201\_Reliable\_Transmitter' and '202\_Reliable\_Receiver'. + +The basic reliable transmit function used is; + +LT.transmitReliable(buff, sizeof(buff), NetworkID, TXtimeout, TXpower, WAIT\_TX)) + +The buff defined in the transmit function is the name of a byte array that has been loaded with the payload, this can either be a sequence of characters, numbers or a structure. The receiver function also needs to know the size of this byte array. NetworkID is the ID for a particular application and needs to match the ID in the receiver. TXtimeout is the time in mS to wait for a transmission to finish, its used to stop the LoRa device potentially locking up a sketch\program. TXpower is the transmit power in dBm to be used and WAIT_TX tells the function to wait for the transmit to complete before exiting. + +The matching receive function is; + +LT.receiveReliable(RXBUFFER, RXBUFFER\_SIZE, NetworkID, RXtimeout, WAIT\_RX); + +The receiver fills the defined RXBUFFER with the contents of the received packet if its valid. The size of this buffer\array specified in RXBUFFER\_SIZE needs to be large enough to accommodate the largest payload size intended to be received. Due to the 4 bytes used by the packet for the networkID and payload CRC, the largest payload that can be received is 251 bytes. + +The payload is copied on receive into the RXBUFFER array. If the transmitted payload array is say 16 bytes long, and the transmit function increases the packet size by 4 to accommodate the networkID and payload CRC the packet length will be 20 bytes. Note that any packets arriving with a length greater than the size defined for RXBUFFER plus 4 bytes will be rejected. + +The NetworkID specified in the receive function has to match that used by the transmitter if the packet is to be accepted. + +The RXtimeout parameter is the number of milli seconds the receive function will wait for a packet to be received, if the period is exceeded a time-out will be reported. To not use the time-out function set RXtimeout to 0. + +WAIT\_RX makes the receive function wait for the receipt of a packet to complete before exiting. + +The transmitter program prints to the Arduino IDE serial monitor the NetworkID and payloadCRC (the payload is "Hello World") and when the receiver picks up the packet this payload should also be printed out. If the networkID of the transmitter does not match the networkID of the receiver the receiver reports the error. If the payload CRC that the transmitter appends to the packet does not match the CRC that the receiver calculates on the payload, this is also reported as an error. + +Thus in the receive program '202\_Basic\_Reliable\_Receiver' the code in the 'packet\_is\_OK()' function is only executed it the received packet passes both the networkID and payload CRC checks. + +With these example programs if you change the NetworkID either in the transmit or receive program and you should see the receiver rejects them. + +The transmitter program output looks like this; + + Transmit Payload > Hello World + LocalNetworkID,0x3210,TransmittedPayloadCRC,0xBC69 +If the receiver reports a valid packet which passes networkID and CRC checks the output looks like; + + Payload received OK > Hello World + LocalNetworkID,0x3210,TransmitterNetworkID,0x3210,LocalPayloadCRC,0xBC69,RXPayloadCRC,0xBC69 +If the networkID check fails the output can look like this; + + Error LocalNetworkID,0x3210,TransmitterNetworkID,0x55AB,LocalPayloadCRC,0xBC69,RXPayloadCRC,0xBC69 + +If the payload CRC check fails the output looks like; + + Error LocalNetworkID,0x3210,TransmitterNetworkID,0x1234,LocalPayloadCRC,0xBC69,RXPayloadCRC,0xBC69,ReliableIDError +If you don't want to use the CRC checking of the payload it can be turned off with this command; + + LT.setReliableConfig(NoReliableCRC); //disable payload CRC check + +You need to use the command on both the transmitter and the receiver. + + +## Using reliable packets to control stuff + +Sending Hello World messages is a useful start but a more practical example is to use the payload to control something on the receiver. The reliable examples show how the sent payload can be a structure, writing variables direct to an array and how to write the payload direct into the LoRa devices internal buffer and thus bypassing the need for an memory array. + +### Using a structure + +The next example 203\_Reliable\_Transmitter\_Controller\_Structure uses a structure as the payload; + + struct controllerStructure + { + uint16\_t destinationNode; + uint8\_t outputNumber; + uint8\_t onoroff; + }; + +The destinationNode is a 16 bit number that the receiver reads to see if the received packet is destined for that receiver. You can set outputNumber to control a range of outputs and onoroff is set to 1 to turn on the output and to 0 to turn it on. + +With the 203 transmitter example running load the 204\_Reliable\_Receiver\_Controller\_Structure program onto the receiver Arduino and it should report the packets received and the LED connected to the pin defined by LED1 should flash on and off. + +These structure examples would be easy to expand to control more outputs such as servos and similar. + +**Note:** Structures on different Arduino types can use different formats. If the transmitter is one Arduino type and the receiver is another type the receiver may not be able to read the transferred structure correctly. + +## Writing variables direct to an array + +A SX12xx library file, arrayRW.h has routines that allow for variables to be directly written to an array. For example if the name of the array is 'controlarray' and then if you review the transmitter program, 205\_Reliable\_Transmitter\_Controller\_ArrayRW, you will see it fills the array with variables like this; + + beginarrayRW(controlarray, 0); //start writing to array at location 0 + arrayWriteUint16(destinationNode); + arrayWriteUint8(outputNumber); + arrayWriteUint8(onoroff); //0 for off, 1 for on + controlarraysize = endarrayRW(); //this function returns the length of the array to send + +When the 206\_Reliable\_Receive\_Controller\_ArrayRW program receives the packet its loaded into a defined array (RXBUFFER in the example) and the sketch then reads the variables from the the array like this; + + beginarrayRW(RXBUFFER, 0); //start reading from array at location 0 + destinationNode = arrayReadUint16(); + outputNumber = arrayReadUint8(); + onoroff = arrayReadUint8(); //0 for off, 1 for on + controlarraysize = endarrayRW(); //this function returns the length of the array to send + + +As with the previous examples the LED on the receiver should flash on and off. + +The full set of write variable functions provided by the arrayRW.h library file is; + + arrayWriteUint8(uint8_t buffdata) + arrayWriteInt8(int8_t buffdata) + arrayWriteChar(char buffdata) + arrayWriteUint16(uint16_t buffdata) + arrayWriteInt16(int16_t buffdata) + arrayWriteFloat(float tempf) + arrayWriteUint32(uint32_t tempdata) + arrayWriteInt32(int32_t tempdata) + arrayWriteCharArray(char *buff, uint8_t len) + arrayWriteByteArray(uint8_t *buff, uint8_t len) + +And the read functions are; + + uint8_t arrayReadUint8() + int8_t arrayReadInt8() + char arrayReadChar() + uint16_t arrayReadUint16() + int16_t arrayReadInt16() + float arrayReadFloat() + uint32_t arrayReadUint32() + int32_t arrayReadInt32() + arrayReadCharArray(char *buff, uint8_t len) + arrayReadByteArray(uint8_t *buff, uint8_t len) + + +## Low memory controller + +Programs 207\_Reliable\_SXTransmitter\_Controller and 208\_Reliable\_SXReceiver\_Controller demonstrate how use the SX12XX library to directly write and read variables to and from the LoRa devices internal buffer directly for transmit and receive functions. There is no memory array required to be defined filled or used by transmitter and receiver programs. If large packets are being sent and received this can save a significant amount of memory. + +The 207 and 208 examples follow the same controller example of programs 203 and 204 in that an LED on the receiver should flash on and off. + +The transmitter packet is loaded into the LoRa devices buffer like this; + + LT.startWriteSXBuffer(0); //start the write at SX12XX internal buffer location 0 + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + TXPayloadL = LT.endWriteSXBuffer(); //closes packet write and returns the length of the payload to send + +And the receiver loads the variables from the LoRa devices buffer like this; + + LT.startReadSXBuffer(0); //start buffer read at location 0 + destinationNode = LT.readUint16(); //load the destination node + outputNumber = LT.readUint8(); //load the output number + onoroff = LT.readUint8(); //0 for off, 1 for on + RXPayloadL = LT.endReadSXBuffer(); //this function returns the length of the array read + +Clearly with these direct accesses to the LoRa devices buffer the order and type of variables written has to match the order when read out by the receiver. + +The full set of library routines for writing direct to the LoRa devices buffer is; + + writeUint8(uint8_t x); + writeInt8(int8_t x); + writeChar(char x); + writeUint16(uint16_t x); + writeInt16(int16_t x); + writeUint32(uint32_t x); + writeInt32(int32_t x); + writeFloat(float x); + writeBuffer(uint8_t *txbuffer, uint8_t size); //uint8_t buffer + writeBufferChar(char *txbuffer, uint8_t size); //char buffer + +And for reading direct from the LoRa devices buffer; + + uint8_t readUint8(); + int8_t readInt8(); + char readChar(); + uint16_t readUint16(); + int16_t readInt16(); + uint32_t readUint32(); + int32_t readInt32(); + float readFloat(); + uint8_t readBuffer(uint8_t *rxbuffer); //reads buffer till a null 0x00 is reached + uint8_t readBuffer(uint8_t *rxbuffer, uint8_t size); + uint8_t readBufferChar(char *rxbuffer); + + +#### Getting the payload CRC into a sketch + +The CRC of the transmitted payload, that is appended to the packet and printed out in the examples, can be obtained by the sketch in several ways, first the simplest way is to use a SX12XX library function after the packet has been transmitted or received; + + TXpayloadCRC = LT.getTXPayloadCRC(TXPacketL); + RXpayloadCRC = LT.getRXPayloadCRC(RXPacketL); + +The payload CRC can be read direct from the end of the packet buffer of the LoRa device like this; + + PayloadCRC = LT.readUint16SXBuffer(PayloadL+2); + +Where PayloadL is the length in bytes of the payload, this is the method used in the 201 transmitter example. + +Or the payload CRC can be calculated locally by accessing the local payload array\buffer directly like this which uses another SX12xx library function; + + LocalPayloadCRC = LT.CRCCCITT(buff, sizeof(buff), 0xFFFF); + +Where buff is the name of the array containing the payload. +Where 0xFFFF is the CRC check start value, normally 0xFFFF. + +And finally there is a SX12xx library function that will return the CRC of an area of the LoRa devices internal buffer like this; + + LT.CRCCCITTReliable(0, PayloadL-1, 0xFFFF); + +Where PayloadL is the length of the payload array\buffer. + +Where 0 is the address in the buffer to start the CRC check. +Where 0xFFFF is the CRC check start value, normally 0xFFFF. + + +#### Getting the NetworkID into a sketch + +The NetworkID used for the transmission or reception of a packet, that is appended to the packet and printed out in the examples, can be obtained by the sketch using an SX12XX library function after the packet has been transmitted or received; + + TXNetworkID = LT.getTXNetworkID(TXPacketL); + RXNetworkID = LT.getRXNetworkID(RXPacketL); + + +## Sending and receiving reliable packets with an acknowledge + +Sometimes we may need to be sure the receiver has actually received the packet sent and we may want to keep transmitting the packet until it actually is received. Thus the receiver needs, when it has accepted a valid reliable packet, to send an acknowledge packet back that the transmitter will be listening for. + +Clearly we could now have the same problem as for a simple reliable packet, how does the transmitter know the received packet actually is an acknowledge from the specific receiver ? + +The simple way to be sure is to turn around the received NetworkID and payloadcrc (i.e. a total of 4 bytes) and send these bytes as an acknowledge. The transmitter program knows the NetworkID and payloadcrc used in the transmit function so can check to a high level of certainty that the received packet is a valid acknowledge, all four bytes of the acknowledge have to be correct. + + +## Examples using acknowledgements + +The previous examples, where there was no acknowledgement used were a very basic transmit and receive, a set of examples using a structure to control an remote output, doing the same with direct array read and write and then a low memory example writing and reading direct to the LoRa devices buffer. There are two types of acknowledgement possible with the library, the first is a simple Auto Acknowledge which is only 4 bytes, consisting of the the NetworkID and payload CRC. The second type of acknowledge allows the receiver to send back data to the transmitter. This form of acknowledge can be used when the transmitter wants to request some data or control information from the receiver. Since the original NetworkID and payload CRC used by the transmitter is sent back with the acknowledge the transmitter can be very confident that the data coming back is genuine. + +These two examples; **209\_Reliable\_Transmitter\_AutoACK** and **210\_Reliable\_Receiver\_AutoACK** are the basic 201 and 202 examples modified to use the auto acknowledge. + +The transmit function when acknowledge is configured is; + + transmitReliable(buff, sizeof(buff), NetworkID, ACKtimeout, TXtimeout, TXpower, WAIT_TX)) + +If AutoACK is used then a ACKtimeout needs to be specified. ACKtimeout is the milliseconds the transmit function would listen for a valid acknowledge before returning an error. How short this period is rather depends on the LoRa settings in use and the actual Arduinos being used. Remember the sending of the acknowledge will have a on-air time that the transmitter needs to account for. You can in some circumstances have an ACKtimeout as low as 25mS and that is still enough time for the receiver to turn around from receive to transmit and the transmitter to flip to receive mode and pick-up the packet. You need to experiment here, perhaps start at 1000mS and gradually reduce the time (with a working set-up) until the point is reached when the receipt of the acknowledge fails. + +With the transmit function if the returned byte is 0 this indicates to the sketch that there has been an error of some type, one such error could be no acknowledge received. + +The matching receive function is; + + receiveReliable(RXBUFFER, RXBUFFER_SIZE, NetworkID, ACKdelay, TXpower, RXtimeout, WAIT_RX); + +The ACKdelay parameter is in miliseconds and it's the time the receiver waits before sending an acknowledge. With some hardware a delay here of 0mS might be OK, but with faster hardware you may need to increase it. Maybe start with an ACKdelay of 50mS and an ACKtimeout of 1000mS in the transmit function and reduce the numbers in steps. You only need to do this if you want or need to maximise response time. + +If the returned byte from the receiveReliable() function is 0 then there was a problem during receive. + +The 209 transmitter will keep sending the payload until the transmitReliable() function returns a non zero value. The transmitReliable() function will return a zero value if no acknowledge is received within the ACKtimeout period. + +Note that in the case of a NetworkID mismatch the receiver will not transmit an acknowledge, so the transmitter reports it as an a NoReliableACK error. + +The auto acknowledge is a simple way of making the transmission of packets more reliable, but it might not be appropriate in all circumstances. For instance consider the **207\_Reliable\_Transmitter\_Controller\_LowMemory** and **208\_Reliable\_Receiver\_Controller\_LowMemory** examples where the payload contains the following; + + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + +Here the destinationNode number is directed to a particular node number, 2 in that example. If the packet is received by node number 5, then there should be no acknowledge sent. In these circumstances the receiver program needs to intervene directly on the received packet, read the payload and check for a matching destinationNode number. If there is a match then an acknowledge can be sent manually and the transmitter knows the packet has been received. + +## Low memory acknowledge + +Example programs **211\_Reliable\_SXTransmitter\_AutoACK** and **212\_Reliable\_SXReceiver\_AutoACK** demonstrate how to use the SX12xx library functions to bypass the need to use an intermediate array for the payload. Instead of filling an array with a structure or byte array full of variables, the variables to send are written directly to the LoRa device internal buffer on transmit and read from the buffer on receive. + +Filling the SX12XX LoRa devices buffer is done like so; + + LT.startWriteSXBuffer(0); //start the write at SX12XX internal buffer location 0 + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + TXPayloadL = LT.endWriteSXBuffer(); //closes packet write and returns the length of the payload to send + +And then the appropriate transmit function is used; + + TXPacketL = LT.transmitSXReliableAutoACK(0, TXPayloadL, NetworkID, ACKtimeout, TXtimeout, TXpower, WAIT_TX); + +On the receive side the receive is setup like so; + + PacketOK = LT.receiveSXReliableAutoACK(0, NetworkID, ACKdelay, TXpower, RXtimeout, WAIT_RX); + +And if on receive the packet passes the NetworkID and payloadCRC checks (PacketOK returns > 0) it is read like this; + + LT.startReadSXBuffer(0); //start buffer read at location 0 + destinationNode = LT.readUint16(); //load the destination node + outputNumber = LT.readUint8(); //load the output number + onoroff = LT.readUint8(); //0 for off, 1 for on + RXPayloadL = LT.endReadSXBuffer(); //this function returns the length of the array read + +Note that in this example the payload contains a 16 bit destinationNode variable, which can be used to direct the packet to one of many nodes. The sketch checks that the destinationNode matches the number given to that receiver and if there is a match actions the packet. + + +## Manual acknowledge + +The programs **213\_Reliable\_Transmitter\_Controller\_ManualACK** and **214\_Reliable\_Receiver\_Controller\_ManualACK** use a manual acknowledge set-up whereby the receive picks up the transmitted payload and reads the destinationNode parameter to decide if the packet is destined for that node. If it is the acknowledge is sent which contains the networkID and the CRC of the original payload, thus the transmitter knows the sent packet has been received correctly. + +The receive sketch can pause at the point the payload is being actioned, perhaps reading an external sensor waiting for conformation that the action as completed, a gate is confirmed opened\closed for instance, before sending the acknowledge. + +There is a further enhancement to the manual acknowledge set-up, the acknowledge can contain some data to be returned to the transmitter. + +## Manual acknowledge returning data + +The standard acknowledge is only 4 bytes, the NetworkID and payload CRC. However the acknowledge can be sent with an array of data included in the acknowledge. + +The format of this function is; + + transmitReliableACK(uint8\_t *txbuffer, uint8\_t size, uint16\_t networkID, uint16\_t payloadcrc, int8\_t txpower); + +Here the receiver sending the acknowledge can include an array txbuffer of a specified size. This returned array could be a structure, as per example 203 and 204 or an array filled directly with the arrayRW.h library file as used in examples 205 and 206. + +To demonstrate returning an array in the acknowledge examples 201 and 202 were modified so that the transmitted 'Hello World' example has 'Goodbye' returned from the receiver with the acknowledge and is then printed out on the transmitter. The modified programs are **215\_Reliable\_Transmitter\_ManualACK\_withData** and **216\_Reliable\_Receiver\_ManualACK\_withData** + + +## Requesting data + +We can use the acknowledge functions of reliable packets to send a request to a remote station to transfer data. The returned data will include the networkID and payloadCRC sent with the request and since the returned data\packet is also protected by the internal LoRa packet CRC we can be fairly confident we are getting valid data back. + +Take for instance a remote node that has a GPS attached and we want to know the current location of the node. Of course the node could transmit its location on a regular basis but this can be wasteful of power and there will be issues with conflicting transmissions if there are a number of nodes out there. + +Examples **217\_Reliable\_Transmitter\_Data\_Requestor** and **218\_Reliable\_Receiver\_Data\_Requestor** demonstrate such a requesting of data. + +Assume at the station wanting the remote nodes location we send this payload; + + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of packet + LT.writeUint8(RequestStation); //station to reply to request + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + +RequestGPSLocation is assigned a value of 1; + + const uint8_t RequestGPSLocation = 1; + +RequestStation is from 0 to 255 and is the station node\number that we want the location from, in this case RequestStation will have a number of 123. + +The packet is protected by the NetworkID and payload CRC so station 123 will see the request as valid and loads the location data into the LoRa device like this; + + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of request + LT.writeUint8(ThisStation); //who is the request reply from + LT.writeFloat(TestLatitude); //add latitude + LT.writeFloat(TestLongitude); //add longitude + LT.writeFloat(TestAltitude); //add altitude + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + +In this case the returned values are test values of Latitude, Longitude and Altitude. + +The original requesting node sees that the valid reply\Acknowledge is a GPS location request from station 123 and can then act on the data. + +Examples **219\_Reliable\_Transmitter\_Data\_RequestorIRQ** and **220\_Reliable\_Receiver\_Data\_RequestorIRQ** are versions of the above 217 and 218 examples that do not require access to the DIO1 pin on the LoRa device to detect RXdone and TXDone. These versions can be useful for situations where there are few microcontroller pins available to drive the LoRa device such as with the ESP32CAM for instance. + + +## Using program 221\_LoRa\_Packet\_Monitor or 222\_FLRC\_Packet\_Monitor + +When debugging what's going on in a send and acknowledge set-up its useful to be able to see what is happening in real time. This packet monitor example will display the bytes received in hexadecimal, in the example printout below you can see two packets. The 16 byte packet contains the text 'Hello World' and then the NetworkID, 0x3210, then the payload CRC, 0xBC69 at the end. + +The 4 byte packet that is seen around 130mS later is the acknowledge which contains the NetworkID, 0x3210, then the payload CRC, 0xBC69. + + + 125.103 RSSI,-99dBm,SNR,10dB 16 bytes > 48 65 6C 6C 6F 20 57 6F 72 6C 64 00 10 32 69 BC + 125.237 RSSI,-96dBm,SNR,8dB 4 bytes > 10 32 69 BC + +
+ + +## LoRa versus FLRC packets + +The SX128x devices support both LoRa and Fast Long Range Communication (FLRC) packets. The modem settings for the two types of packet are different. Both modulations are long range type modulations but the FLRC packets are capable of a faster air rate than LoRa, 1300Kbps for FLRC versus 200kbps for LoRa. LoRa allows a packet length of up to 255 bytes whilst FLRC is retricted to 127 bytes. + +
+ +### Stuart Robinson +### March 2024 \ No newline at end of file diff --git a/examples/SX128x_examples/Reliable/201_Reliable_Transmitter/201_Reliable_Transmitter.ino b/examples/SX128x_examples/Reliable/201_Reliable_Transmitter/201_Reliable_Transmitter.ino new file mode 100644 index 0000000..427aa14 --- /dev/null +++ b/examples/SX128x_examples/Reliable/201_Reliable_Transmitter/201_Reliable_Transmitter.ino @@ -0,0 +1,160 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 25/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the transmission of a 'Reliable' packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + It is possible to use the 'NetworkID' to direct the packet to specific receivers. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 2 //LoRa transmit power in dBm +#define TXtimeout 5000 //transmit timeout in mS. If 0 return from transmit function after send. + +uint8_t buff[] = "Hello World"; //the payload to send +uint16_t PayloadCRC; +uint8_t TXPayloadL; //this is the payload length sent +uint8_t TXPacketL; + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in receiver + + +void loop() +{ + Serial.print(F("Transmit Payload > ")); + TXPayloadL = sizeof(buff); + LT.printASCIIArray(buff, TXPayloadL); //print the payload buffer as ASCII + Serial.println(); + + if (LT.getReliableConfig(NoReliableCRC)) + { + Serial.println(F("Payload CRC check disabled")); + } + Serial.flush(); + + //now transmit the packet + digitalWrite(LED1, HIGH); //LED on to indicate transmit + TXPacketL = LT.transmitReliable(buff, TXPayloadL, NetworkID, TXtimeout, TXpower, WAIT_TX); //will return packet length > 0 if sent OK, otherwise 0 if transmit error + + if (TXPacketL > 0) + { + //if transmitReliable() returns > 0 then transmit was OK + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); //read the actual transmitted CRC from the LoRa device buffer + packet_is_OK(); + Serial.println(); + } + else + { + //if transmitReliable() returns 0 there was an error + packet_is_Error(); + Serial.println(); + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(5000); //have a delay between packets +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted payload + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("201_Basic_Reliable_Transmitter Starting")); + + pinMode(LED1, OUTPUT); + led_Flash(2, 125); //two quick LED flashes to indicate program start + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + do + { + digitalWrite(LED1, HIGH); + delay(50); + digitalWrite(LED1, LOW); + delay(50); + } while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers, normally 0x900 to 0x9FF + Serial.println(); + Serial.println(F("Transmitter ready")); + Serial.println(); + + //enable the following line if you want to disable payload CRC checking + //LT.setReliableConfig(NoReliableCRC); //disable payload CRC check +} diff --git a/examples/SX128x_examples/Reliable/202_Reliable_Receiver/202_Reliable_Receiver.ino b/examples/SX128x_examples/Reliable/202_Reliable_Receiver/202_Reliable_Receiver.ino new file mode 100644 index 0000000..4b2eacf --- /dev/null +++ b/examples/SX128x_examples/Reliable/202_Reliable_Receiver/202_Reliable_Receiver.ino @@ -0,0 +1,195 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 25/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a simple demonstration of the receipt of a 'Reliable' packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. This receiver needs to have the same + NetworkID as configured for the transmitter. The receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. This receiver carries out its own CRC check on the received payload and can then verify + this against the CRC appended to the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + The received payload is copied on receive into the RXBUFFER array. This buffer can be set up to a maximum + size of 251 bytes. A LoRa packet can be up to 255 bytes, but the reliable packet uses 4 bytes to hold the + networkID and payload CRC. As an example if the the transmitted payload array is 16 bytes long, then the + transmit function will increase the packet size by 4 to 20 to accommodate the networkID and payload CRC. + Note that any packets arriving with a length greater than the length defined for RXBUFFER + 4 bytes will + be rejected. + + It is possible to use the 'NetworkID' to direct the packet to specific receivers. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +#define RXtimeout 10000 //receive timeout in mS. + +const uint8_t RXBUFFER_SIZE = 251; //RX buffer size, set to max payload length of 251, or maximum expected payload length +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t PacketOK; //set to > 0 if packetOK +int16_t PacketRSSI; //stores RSSI of received packet +uint16_t LocalPayloadCRC; //locally calculated CRC of payload +uint16_t RXPayloadCRC; //CRC of payload received in packet +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter + + +void loop() +{ + PacketOK = LT.receiveReliable(RXBUFFER, RXBUFFER_SIZE, NetworkID, RXtimeout, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + digitalWrite(LED1, HIGH); //LED on to indicate receive + + RXPacketL = LT.readRXPacketL(); //get the received packet length + RXPayloadL = RXPacketL - 4; //payload length is always 4 bytes less than packet length + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + + if (PacketOK > 0) + { + //if the LT.receiveReliable() returns a value > 0 for PacketOK then packet was received OK + packet_is_OK(); + } + else + { + //if the LT.receiveReliable() function detects an error PacketOK is 0 + packet_is_Error(); + } + + digitalWrite(LED1, LOW); + Serial.println(); +} + + +void packet_is_OK() +{ + Serial.print(F("Payload received OK > ")); + LT.printASCIIPacket(RXBUFFER, RXPayloadL); + Serial.println(); + if (LT.getReliableConfig(NoReliableCRC)) + { + Serial.println(F("Payload CRC check disabled")); + } + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void printPacketDetails() +{ + LocalPayloadCRC = LT.CRCCCITT(RXBUFFER, RXPayloadL, 0xFFFF); //calculate payload crc from the received RXBUFFER + + TransmitterNetworkID = LT.getRXNetworkID(RXPacketL); + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); + + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(TransmitterNetworkID, HEX); + Serial.print(F(",LocalPayloadCRC,0x")); + Serial.print(LocalPayloadCRC, HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(RXPayloadCRC, HEX); + LT.printReliableStatus(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("202_Basic_Reliable_Receiver Starting")); + + pinMode(LED1, OUTPUT); + led_Flash(2, 125); //two quick LED flashes to indicate program start + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + do + { + digitalWrite(LED1, HIGH); + delay(50); + digitalWrite(LED1, LOW); + delay(50); + } while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + Serial.println(); + LT.printRegisters(0x900, 0x9FF); //print contents of device registers, normally 0x00 to 0x4F + Serial.println(); + Serial.println(F("Receiver ready")); + Serial.println(); + + //enable the following line if you want to disable payload CRC checking + //LT.setReliableConfig(NoReliableCRC); //disable payload CRC check +} diff --git a/examples/SX128x_examples/Reliable/203_Reliable_Transmitter_Controller_Structure/203_Reliable_Transmitter_Controller_Structure.ino b/examples/SX128x_examples/Reliable/203_Reliable_Transmitter_Controller_Structure/203_Reliable_Transmitter_Controller_Structure.ino new file mode 100644 index 0000000..8e9f184 --- /dev/null +++ b/examples/SX128x_examples/Reliable/203_Reliable_Transmitter_Controller_Structure/203_Reliable_Transmitter_Controller_Structure.ino @@ -0,0 +1,155 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 21/07/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the transmission of a 'Reliable' packet used to + control outputs on a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. This receiver needs to have the same + NetworkID as configured for the transmitter. The receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended is a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended to the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + This example program and the matching receiver, 204_Reliable_Receiver_Controller_Structure, transmits a + data structure that contains variables which when read by the remote receiver flashes an LED attached to + the receiver on and off. + + When using these transmit and receive examples the Arduino types should be the same as the structure + formats can be different on some Arduinos. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 2 //LoRa transmit power in dBm +uint8_t TXPacketL; + +#define TXtimeout 1000 //transmit timeout in mS. If 0 return from transmit function after send. + +struct controllerStructure +{ + uint16_t destinationNode; + uint8_t outputNumber; + uint8_t onoroff; +} __attribute__((packed, aligned(1))); //remove structure padding so there is compatibility between 8bit and 32bit Arduinos + +struct controllerStructure controller1; //define an instance called controller1 of the structure controllerStructure +uint16_t PayloadCRC; +uint8_t TXPayloadL; +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in receiver + + +void loop() +{ + + controller1.destinationNode = 2; //node number we are controlling, 0 to 65535 + controller1.outputNumber = 1; //output number on node we are controlling + + if (controller1.onoroff == 0) //toggle the on/off status + { + controller1.onoroff = 1; + } + else + { + controller1.onoroff = 0; + } + + Serial.print(F("Transmit Structure > ")); + TXPayloadL = sizeof(controller1); + LT.printHEXPacket((uint8_t *) &controller1, sizeof(controller1)); //print the sent array as HEX + Serial.println(); + Serial.flush(); + + //now transmit the packet + digitalWrite(LED1, HIGH); //LED on to indicate transmit + + TXPacketL = LT.transmitReliable((uint8_t *) &controller1, sizeof(controller1), NetworkID, TXtimeout, TXpower, WAIT_TX); //will return packet length > 0 if sent OK, otherwise 0 if transmit error + + if (TXPacketL > 0) + { + //if transmitReliable() returns > 0 then transmit was OK + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); //read the actual transmitted CRC from the LoRa device buffer + packet_is_OK(); + Serial.println(); + } + else + { + //if transmitReliable() returns 0 there was an error + packet_is_Error(); + Serial.println(); + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(5000); //have a delay between packets +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("203_Reliable_Transmitter_Controller_Structure Starting")); + + pinMode(LED1, OUTPUT); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); + + Serial.println(F("Transmitter ready")); + Serial.println(); + + //enable the following line if you want to disable payload CRC checking + //LT.setReliableConfig(NoReliableCRC); //disable payload CRC check + +} diff --git a/examples/SX128x_examples/Reliable/204_Reliable_Receiver_Controller_Structure/204_Reliable_Receiver_Controller_Structure.ino b/examples/SX128x_examples/Reliable/204_Reliable_Receiver_Controller_Structure/204_Reliable_Receiver_Controller_Structure.ino new file mode 100644 index 0000000..222ae90 --- /dev/null +++ b/examples/SX128x_examples/Reliable/204_Reliable_Receiver_Controller_Structure/204_Reliable_Receiver_Controller_Structure.ino @@ -0,0 +1,197 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 21/07/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the receive of a 'Reliable' packet used to control + outputs on the receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. This receiver needs to have the same + NetworkID as configured for the transmitter. The receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended is a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended to the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + This example program and the matching transmitter, 203_Reliable_Transmitter_Controller_Structure, + receives a reliable packet from the transmitter, reads the variables from the sent data structure which + when read remotly flashes the LED attached to the receiver on and off. Note that for the LED switching + to be actioned the destinationNode and outputNumber received in the structure have to match. + + When using these transmit and receive examples the Arduino types should be the same as the structure + formats can be different on some Arduinos. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define LED1 8 //this is output 1 + +#define RXtimeout 60000 //receive timeout in mS. + +uint8_t RXPacketL; //stores length of packet received +uint8_t PacketOK; //set to > 0 if packetOK +int16_t PacketRSSI; //stores RSSI of received packet +uint16_t LocalPayloadCRC; //locally calculated CRC of payload +uint16_t RXPayloadCRC; //CRC of payload received in packet +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter +const uint16_t thisNode = 2; //node number for this node + +struct controllerStructure +{ + uint16_t destinationNode; + uint8_t outputNumber; + uint8_t onoroff; +} __attribute__((packed, aligned(1))); //remove structure padding so there is compatibility between 8bit and 32bit Arduinos + +struct controllerStructure controller1; //define an instance called controller1 of the structure controllerStructure + + +void loop() +{ + PacketOK = LT.receiveReliable((uint8_t *) &controller1, sizeof(controller1), NetworkID, RXtimeout, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + RXPacketL = LT.readRXPacketL(); //get the received packet length + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + + if (PacketOK > 0) + { + //if the LT.receiveReliable() returns a value > 0 for PacketOK then packet was received OK + //then only action payload if destinationNode = thisNode + + Serial.print(F("Packet received OK > ")); + LT.printHEXPacket((uint8_t *) &controller1, sizeof(controller1)); + Serial.println(); + + Serial.print(F("destinationNode ")); + Serial.println(controller1.destinationNode); + + if (controller1.destinationNode == thisNode) + { + Serial.print(F("outputNumber ")); + Serial.println(controller1.outputNumber); + Serial.print(F("onoroff ")); + Serial.println(controller1.onoroff); + packet_is_OK(); + actionpayload(); + } + } + else + { + //if the LT.receiveReliable() function detects an error PacketOK is 0 + packet_is_Error(); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void actionpayload() +{ + Serial.print(F("Action payload > ")); + if (controller1.outputNumber == 1) + { + Serial.print(F("outputNumber ")); + Serial.print(controller1.outputNumber); + if (controller1.onoroff) + { + Serial.println(F(" High")); + digitalWrite(LED1, HIGH); + } + else + { + Serial.println(F(" Low")); + digitalWrite(LED1, LOW); + } + } + else + { + Serial.println(F("Not valid outputNumber")); + } +} + + +void printPacketDetails() +{ + LocalPayloadCRC = LT.CRCCCITT((uint8_t *) &controller1, sizeof(controller1), 0xFFFF); //calculate payload crc from the received RXBUFFER + TransmitterNetworkID = LT.getRXNetworkID(RXPacketL); + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(TransmitterNetworkID, HEX); + Serial.print(F(",LocalPayloadCRC,0x")); + Serial.print(LocalPayloadCRC, HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(RXPayloadCRC, HEX); + LT.printReliableStatus(); +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("204_Reliable_Receiver_Controller_Structure Starting")); + pinMode(LED1, OUTPUT); //set high for on + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/205_Reliable_Transmitter_Controller_ArrayRW/205_Reliable_Transmitter_Controller_ArrayRW.ino b/examples/SX128x_examples/Reliable/205_Reliable_Transmitter_Controller_ArrayRW/205_Reliable_Transmitter_Controller_ArrayRW.ino new file mode 100644 index 0000000..fd8d535 --- /dev/null +++ b/examples/SX128x_examples/Reliable/205_Reliable_Transmitter_Controller_ArrayRW/205_Reliable_Transmitter_Controller_ArrayRW.ino @@ -0,0 +1,149 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 21/07/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the transmission of a 'Reliable' packet used to + control outputs on a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. This receiver needs to have the same + NetworkID as configured for the transmitter. The receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended is a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended to the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + This example program and the matching receiver, 206_Reliable_Receiver_Controller_ArrayRW, transmits a + array that contains variables which when read by the remote receiver flash an LED attached to + the receiver on and off. The variables are writen direct to the array using a library file arrayRW.h. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library +#include //routines for reading and writing varaibles to array + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //LED used to indicate transmission +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 2 //LoRa transmit power in dBm + +#define TXtimeout 1000 //transmit timeout in mS. + +uint8_t controlarray[4]; //the array payload to send +uint8_t controlarraysize; +uint16_t PayloadCRC; +uint8_t TXPacketL; + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in receiver + +uint16_t destinationNode = 2; //node number we are controlling, 0 to 65535 +uint8_t outputNumber = 1; //output number on node we are controlling +uint8_t onoroff = 0; //set to 0 to set remote output off, 1 to set it on + + +void loop() +{ + + beginarrayRW(controlarray, 0); //start writing to controlarray array at location 0 + arrayWriteUint16(destinationNode); + arrayWriteUint8(outputNumber); + arrayWriteUint8(onoroff); //0 for off, 1 for on + controlarraysize = endarrayRW() + 1; //this function returns the length of the array to send, i.e. the packet payload + + Serial.print(F("Transmit Array > ")); + LT.printHEXPacket(controlarray, controlarraysize); //print the sent array as HEX + Serial.println(); + Serial.flush(); + + //now transmit the packet + digitalWrite(LED1, HIGH); //LED on to indicate transmit + TXPacketL = LT.transmitReliable(controlarray, controlarraysize, NetworkID, TXtimeout, TXpower, WAIT_TX); + + if (TXPacketL) + { + //if transmitReliable() returns > 0 then transmit was OK + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); //read the actual transmitted CRC from the LoRa device + packet_is_OK(); + Serial.println(); + } + else + { + //if transmitReliable() returns 0 there was an error + packet_is_Error(); + Serial.println(); + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(5000); //have a delay between packets + + if (onoroff == 0) //toggle the on/off status + { + onoroff = 1; + } + else + { + onoroff = 0; + } + +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("205_Reliable_Transmitter_Controller_ArrayRW Starting")); + + pinMode(LED1, OUTPUT); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/206_Reliable_Receiver_Controller_ArrayRW/206_Reliable_Receiver_Controller_ArrayRW.ino b/examples/SX128x_examples/Reliable/206_Reliable_Receiver_Controller_ArrayRW/206_Reliable_Receiver_Controller_ArrayRW.ino new file mode 100644 index 0000000..0f4d2ca --- /dev/null +++ b/examples/SX128x_examples/Reliable/206_Reliable_Receiver_Controller_ArrayRW/206_Reliable_Receiver_Controller_ArrayRW.ino @@ -0,0 +1,202 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 21/07/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the receive of a 'Reliable' packet used to control + outputs on this receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. This receiver needs to have the same + NetworkID as configured for the transmitter. The receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended is a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended to the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + This example program and the matching transmitter, 205_Reliable_Transmitter_Controller_ArrayRW, receives + an array that contains variables which when read flash an LED attached to the receiver on and off. + The variables are writen direct to the array using a library file arrayRW.h. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library +#include //routines for reading and writing varaibles to array + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define LED1 8 //this is output 1 + +#define RXtimeout 60000 //receive timeout in mS. + +const uint8_t RXBUFFER_SIZE = 251; //RX buffer size, set to max payload length of 251, or maximum expected length +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received payloads are copied into + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t PacketOK; //set to > 0 if packetOK +int16_t PacketRSSI; //stores RSSI of received packet +uint16_t LocalPayloadCRC; //locally calculated CRC of payload +uint16_t RXPayloadCRC; //CRC of payload received in packet +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter + +uint16_t destinationNode = 2; //node number we are controlling, 0 to 65535 +const uint16_t thisNode = 2; //node number for this node +uint8_t outputNumber = 1; //output number on node we are controlling +uint8_t onoroff = 1; //set to 0 to set remote output off, 1 to set it on + + +void loop() +{ + PacketOK = LT.receiveReliable(RXBUFFER, RXBUFFER_SIZE, NetworkID, RXtimeout, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + RXPacketL = LT.readRXPacketL(); //get the received packet length + RXPayloadL = RXPacketL - 4; //payload length is always 4 bytes less than packet length + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + + //now read the received array into variables + beginarrayRW(RXBUFFER, 0); //start reading from array at location 0 + destinationNode = arrayReadUint16(); + outputNumber = arrayReadUint8(); + onoroff = arrayReadUint8(); //0 for off, 1 for on + RXPayloadL = endarrayRW(); //this function returns the length of the array decoded + + if (PacketOK > 0) + { + //if the LT.receiveReliable() returns a value > 0 for PacketOK then packet was received OK + //then only action payload if destinationNode = thisNode + + Serial.print(F("Payload received OK > ")); + LT.printHEXPacket(RXBUFFER, RXPayloadL); + Serial.println(); + + Serial.print(F("destinationNode ")); + Serial.println(destinationNode); + + if (destinationNode == thisNode) + { + Serial.print(F("outputNumber ")); + Serial.println(outputNumber); + Serial.print(F("onoroff ")); + Serial.println(onoroff); + packet_is_OK(); + actionpayload(); + } + } + else + { + //if the LT.receiveReliable() function detects an error PacketOK is 0 + packet_is_Error(); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void actionpayload() +{ + Serial.print(F("Action payload > ")); + if (outputNumber == 1) + { + Serial.print(F("outputNumber ")); + Serial.print(outputNumber); + if (onoroff) + { + Serial.println(F(" High")); + digitalWrite(LED1, HIGH); + } + else + { + Serial.println(F(" Low")); + digitalWrite(LED1, LOW); + } + } + else + { + Serial.println(F("Not valid outputNumber")); + } +} + + +void printPacketDetails() +{ + LocalPayloadCRC = LT.CRCCCITT(RXBUFFER, RXPayloadL, 0xFFFF); //calculate payload crc from the received RXBUFFER + TransmitterNetworkID = LT.getRXNetworkID(RXPacketL); + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); + + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(TransmitterNetworkID, HEX); + Serial.print(F(",LocalPayloadCRC,0x")); + Serial.print(LocalPayloadCRC, HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(RXPayloadCRC, HEX); + LT.printReliableStatus(); +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("206_Reliable_Receiver_Controller_ArrayRW Starting")); + pinMode(LED1, OUTPUT); //set high for on + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/207_Reliable_SXTransmitter_Controller/207_Reliable_SXTransmitter_Controller.ino b/examples/SX128x_examples/Reliable/207_Reliable_SXTransmitter_Controller/207_Reliable_SXTransmitter_Controller.ino new file mode 100644 index 0000000..87b7c37 --- /dev/null +++ b/examples/SX128x_examples/Reliable/207_Reliable_SXTransmitter_Controller/207_Reliable_SXTransmitter_Controller.ino @@ -0,0 +1,150 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 13/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the transmission of a 'Reliable' packet used to + control outputs on a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. This receiver needs to have the same + NetworkID as configured for the transmitter. The receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended to the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + This example program and the matching receiver, 208_Reliable_SXReceiver_Controller writes a series + of variables direct into the LoRa devices internal buffer, it is not necessary to write variables to a + memory array first. The receiver then reads the matching variables direct from the LoRa devices buffer and + these variables contain the control information to flash a LED attached to the receiver on and off. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //this is output 1 +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 2 //LoRa transmit power in dBm + +#define TXtimeout 1000 //transmit timeout in mS. If 0 return from transmit function after send. + +uint8_t TXPacketL; +uint8_t TXPayloadL; //this is the payload length sent +uint16_t PayloadCRC; + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in receiver + +uint16_t destinationNode = 2; //node number we are controlling, 0 to 65535 +uint8_t outputNumber = 1; //output number on node we are controlling +uint8_t onoroff = 0; //set to 0 to set remote output off, 1 to set it on +uint8_t startaddr = 0; //address in SX buffer to start packet + + +void loop() +{ + LT.startWriteSXBuffer(startaddr); //start the write at SX12XX internal buffer location startaddr + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + TXPayloadL = LT.endWriteSXBuffer(); //closes packet write and returns the length of the payload to send + + Serial.print(F("Transmit SX buffer > ")); + LT.printSXBufferHEX(startaddr, TXPayloadL + startaddr - 1); //print the sent SX array as HEX + Serial.println(); + Serial.flush(); + + //now transmit the packet + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXReliable(startaddr, TXPayloadL, NetworkID, TXtimeout, TXpower, WAIT_TX); //will return packet length > 0 if sent OK, otherwise 0 if transmit error + + if (TXPacketL > 0) + { + //if transmitReliable() returns > 0 then transmit was OK + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); //read the actual transmitted CRC from the LoRa device buffer + packet_is_OK(); + Serial.println(); + } + else + { + //if transmitReliable() returns 0 there was an error + packet_is_Error(); + Serial.println(); + } + + digitalWrite(LED1, LOW); + Serial.println(); + delay(5000); //have a delay between packets + + if (onoroff == 0) //toggle the on/off status + { + onoroff = 1; + } + else + { + onoroff = 0; + } + +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("207_Reliable_SXTransmitter_Controller Starting")); + pinMode(LED1, OUTPUT); //set high for on + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); + + Serial.println(F("Transmitter ready")); + Serial.println(); + + //enable the following line if you want to disable payload CRC checking + //LT.setReliableConfig(NoReliableCRC); //disable payload CRC check + +} diff --git a/examples/SX128x_examples/Reliable/208_Reliable_SXReceiver_Controller/208_Reliable_SXReceiver_Controller.ino b/examples/SX128x_examples/Reliable/208_Reliable_SXReceiver_Controller/208_Reliable_SXReceiver_Controller.ino new file mode 100644 index 0000000..3e72bc3 --- /dev/null +++ b/examples/SX128x_examples/Reliable/208_Reliable_SXReceiver_Controller/208_Reliable_SXReceiver_Controller.ino @@ -0,0 +1,200 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a simple demonstration of the receipt of a 'Reliable' packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. This receiver needs to have the same + NetworkID as configured for the transmitter. The receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended to the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + This example program and the matching transmitter, 207_Reliable_SXTransmitter_Controller reads a + series of variables direct from the packet received in the LoRa devices internal buffer, it is not necessary + to read variables to a memory array first. The receiver then uses these variables as the control information + to flash a LED attached to the receiver on and off. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define LED1 8 //this is output 1 + +#define RXtimeout 60000 //receive timeout in mS. + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t PacketOK; //set to > 0 if packetOK +int16_t PacketRSSI; //stores RSSI of received packet +uint16_t LocalPayloadCRC; //locally calculated CRC of payload +uint16_t RXPayloadCRC; //CRC of payload received in packet +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter + +uint16_t destinationNode; //node number we are controlling, 0 to 65535 +const uint16_t thisNode = 2; //node number for this node +uint8_t outputNumber = 1; //output number on node we are controlling +uint8_t onoroff = 1; //set to 0 to set remote output off, 1 to set it on +uint8_t startaddr = 0; //address in SX buffer to start packet + +void loop() +{ + PacketOK = LT.receiveSXReliable(startaddr, NetworkID, RXtimeout, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + RXPacketL = LT.readRXPacketL(); //get the received packet length, get this before reading packet + RXPayloadL = RXPacketL - 4; + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + + LT.startReadSXBuffer(startaddr); //start buffer read at location 0 + destinationNode = LT.readUint16(); //load the destination node + outputNumber = LT.readUint8(); //load the output number + onoroff = LT.readUint8(); //0 for off, 1 for on + RXPayloadL = LT.endReadSXBuffer(); //this function returns the length of the array read + + if (PacketOK > 0) + { + //if the LT.receiveReliable() returns a value > 0 for PacketOK then packet was received OK + //then only action payload if destinationNode = thisNode + + Serial.print(F("Payload received OK > ")); + LT.printSXBufferHEX(startaddr, startaddr + RXPayloadL - 1); + Serial.println(); + + Serial.print(F("destinationNode ")); + Serial.println(destinationNode); + + if (destinationNode == thisNode) + { + Serial.print(F("outputNumber ")); + Serial.println(outputNumber); + Serial.print(F("onoroff ")); + Serial.println(onoroff); + packet_is_OK(); + actionpayload(); + } + } + else + { + //if the LT.receiveReliable() function detects an error PacketOK is 0 + packet_is_Error(); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void actionpayload() +{ + Serial.print(F("Action payload > ")); + if (outputNumber == 1) + { + Serial.print(F("outputNumber ")); + Serial.print(outputNumber); + if (onoroff) + { + Serial.println(F(" High")); + digitalWrite(LED1, HIGH); + } + else + { + Serial.println(F(" Low")); + digitalWrite(LED1, LOW); + } + } + else + { + Serial.println(F("Not valid outputNumber")); + } +} + + +void printPacketDetails() +{ + LocalPayloadCRC = LT.CRCCCITTSX(startaddr, startaddr + RXPayloadL - 1, 0xFFFF); //calculate payload crc from the received payload in LoRa device buffer + TransmitterNetworkID = LT.getRXNetworkID(RXPacketL); + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); //used the saved packet value to retrieve the payload CRC + + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(TransmitterNetworkID, HEX); + Serial.print(F(",LocalPayloadCRC,0x")); + Serial.print(LocalPayloadCRC, HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(RXPayloadCRC, HEX); + LT.printReliableStatus(); +} + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("208_Reliable_SXReceiver_Controller Starting")); + pinMode(LED1, OUTPUT); //set high for on + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); + + Serial.println(F("Receiver ready")); + Serial.println(); + + //enable the following line if you want to disable payload CRC checking + //LT.setReliableConfig(NoReliableCRC); //disable payload CRC check +} diff --git a/examples/SX128x_examples/Reliable/209_Reliable_Transmitter_AutoACK/209_Reliable_Transmitter_AutoACK.ino b/examples/SX128x_examples/Reliable/209_Reliable_Transmitter_AutoACK/209_Reliable_Transmitter_AutoACK.ino new file mode 100644 index 0000000..02ad5b3 --- /dev/null +++ b/examples/SX128x_examples/Reliable/209_Reliable_Transmitter_AutoACK/209_Reliable_Transmitter_AutoACK.ino @@ -0,0 +1,153 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the transmission and acknowledgement of a 'Reliable' + packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + If the received packet is valid then the networkID and payload CRC are returned in a 4 byte packet as an + acknowledgement that the transmitter listens for. If the transmitter does not receive the acknowledgement + within the ACKtimeout period, the original packet is re-transmitted until a valid acknowledgement is + received. This program should be used with the matching receiver program, 210_Reliable_Receiver_AutoACK. + + The program will attempt to transmit the packet and have it acknowledged by the receiver a number of times + as defined by constant TXattempts. If there is no acknowledge withing this time it will be reported. + + It is possible to use the 'NetworkID' to direct the packet to specific receivers. + + Serial monitor baud rate should be set at 115200. + + *******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 2 //LoRa transmit power in dBm + +#define ACKtimeout 1000 //Acknowledge timeout in mS +#define TXtimeout 1000 //transmit timeout in mS. If 0 return from transmit function after send. +#define TXattempts 10 //number of times to attempt to TX and get an Ack before failing + +uint8_t buff[] = "Hello World"; //the payload to send +uint16_t PayloadCRC; +uint8_t TXPacketL; + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in receiver + + +void loop() +{ + + //keep transmitting the packet until an ACK is received + uint8_t attempts = TXattempts; + + do + { + Serial.print(F("Transmit Payload > ")); + LT.printASCIIArray(buff, sizeof(buff)); //print the payload buffer as ASCII + Serial.println(); + Serial.flush(); + + Serial.print(F("Send attempt ")); + Serial.println(TXattempts - attempts + 1); + + TXPacketL = LT.transmitReliableAutoACK(buff, sizeof(buff), NetworkID, ACKtimeout, TXtimeout, TXpower, WAIT_TX); + attempts--; + + if (TXPacketL > 0) + { + //if transmitReliable() returns > 0 then transmit and ack was OK + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); //read the actual transmitted CRC from the LoRa device buffer + packet_is_OK(); + Serial.println(); + } + else + { + //if transmitReliableAutoACK() returns 0 there was an error, timeout etc + packet_is_Error(); + Serial.println(); + } + delay(500); //small delay between tranmission attampts + } + while ((TXPacketL == 0) && (attempts != 0)); + + if (TXPacketL > 0) + { + Serial.println(F("Packet acknowledged")); + } + + if (attempts == 0) + { + Serial.print(F("No acknowledge after ")); + Serial.print(TXattempts); + Serial.print(F(" attempts")); + } + + Serial.println(); + delay(5000); //have a delay between packets +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("No Packet acknowledge")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("209_Reliable_Transmitter_AutoACK Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF5, LORA_BW_1600, LORA_CR_4_5); + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/210_Reliable_Receiver_AutoACK/210_Reliable_Receiver_AutoACK.ino b/examples/SX128x_examples/Reliable/210_Reliable_Receiver_AutoACK/210_Reliable_Receiver_AutoACK.ino new file mode 100644 index 0000000..17bcc7f --- /dev/null +++ b/examples/SX128x_examples/Reliable/210_Reliable_Receiver_AutoACK/210_Reliable_Receiver_AutoACK.ino @@ -0,0 +1,153 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the transmission and acknowledgement of a 'Reliable' + packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + If the received packet is valid then the networkID and payload CRC are returned in a 4 byte packet as an + acknowledgement that the transmitter listens for. If the transmitter does not receive the acknowledgement + of the networkID and payloadCRC within the ACKtimeout period, the original packet is re-transmitted until + a valid acknowledgement is received. This program should be used with the matching transmitter program, + 209_Reliable_Transmitter_AutoACK. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +#define ACKdelay 100 //delay in mS before sending acknowledge +#define RXtimeout 60000 //receive timeout in mS. +#define TXpower 2 //dBm power to use for ACK + +const uint8_t RXBUFFER_SIZE = 251; //RX buffer size, set to max payload length of 251, or maximum expected length +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t PacketOK; //set to > 0 if packetOK +int16_t PacketRSSI; //stores RSSI of received packet +uint16_t LocalPayloadCRC; //locally calculated CRC of payload +uint16_t RXPayloadCRC; //CRC of payload received in packet +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter + + +void loop() +{ + PacketOK = LT.receiveReliableAutoACK(RXBUFFER, RXBUFFER_SIZE, NetworkID, ACKdelay, TXpower, RXtimeout, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + RXPacketL = LT.readRXPacketL(); //get the received packet length + RXPayloadL = RXPacketL - 4; //payload length is always 4 bytes less than packet length + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + + if (PacketOK > 0) + { + //if the LT.receiveReliable() returns a value > 0 for PacketOK then packet was received OK + packet_is_OK(); + } + else + { + //if the LT.receiveReliable() function detects an error PacketOK is 0 + packet_is_Error(); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + Serial.print(F("Payload received OK > ")); + LT.printASCIIPacket(RXBUFFER, RXPayloadL); + Serial.println(); + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void printPacketDetails() +{ + LocalPayloadCRC = LT.CRCCCITT(RXBUFFER, RXPayloadL, 0xFFFF); //calculate payload crc from the received RXBUFFER + TransmitterNetworkID = LT.getRXNetworkID(RXPacketL); + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); + + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(TransmitterNetworkID, HEX); + Serial.print(F(",LocalPayloadCRC,0x")); + Serial.print(LocalPayloadCRC, HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(RXPayloadCRC, HEX); + LT.printReliableStatus(); +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("210_Reliable_Receiver_AutoACK Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF5, LORA_BW_1600, LORA_CR_4_5); + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/211_Reliable_SXTransmitter_AutoACK/211_Reliable_SXTransmitter_AutoACK.ino b/examples/SX128x_examples/Reliable/211_Reliable_SXTransmitter_AutoACK/211_Reliable_SXTransmitter_AutoACK.ino new file mode 100644 index 0000000..0406a0d --- /dev/null +++ b/examples/SX128x_examples/Reliable/211_Reliable_SXTransmitter_AutoACK/211_Reliable_SXTransmitter_AutoACK.ino @@ -0,0 +1,157 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 13/09/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the transmission and acknowledgement of a 'Reliable' + packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + If the received packet is valid then the networkID and payload CRC are returned in a 4 byte packet as an + acknowledgement that the transmitter listens for. If the transmitter does not receive the acknowledgement + within the ACKtimeout period, the original packet is re-transmitted until a valid acknowledgement is + received. This program should be used with the matching receiver program, 212_Reliable_SXReceiver_AutoACK. + + This example writes the payload direct to the LoRa SX devices internal packet buffer, no intermediate array + or memory is needed. The payload is a simpel remote control packet directed to a specific receiver, + + The packet is destined for a specific receiver (destinationNode). A particular output (outputNumber) is turned + on and off. + + Serial monitor baud rate should be set at 115200. + + *******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 2 //LoRa transmit power in dBm + +#define ACKtimeout 1000 //Acknowledge timeout in mS +#define TXtimeout 1000 //transmit timeout in mS. If 0 return from transmit function after send. + +uint16_t PayloadCRC; //CRC of the payload +uint8_t TXPacketL; //length of transmitted packet +uint8_t TXPayloadL; //this is the payload length sent + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in receiver + +uint16_t destinationNode = 2; //node number we are controlling, 0 to 65535 +uint8_t outputNumber = 1; //output number on node we are controlling +uint8_t onoroff = 0; //set to 0 to set remote output off, 1 to set it on +uint8_t startaddr = 0; //address in SX buffer to start packet + +void loop() +{ + + //keep transmitting the packet until an ACK is received + do + { + LT.startWriteSXBuffer(startaddr); //start the write at SX12XX internal buffer location 0 + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + TXPayloadL = LT.endWriteSXBuffer(); //closes packet write and returns the length of the payload to send + + Serial.print(F("Transmit SX buffer > ")); + LT.printSXBufferHEX(startaddr, startaddr + TXPayloadL - 1); //print the sent SX array as HEX + Serial.println(); + Serial.flush(); + + TXPacketL = LT.transmitSXReliableAutoACK(startaddr, TXPayloadL, NetworkID, ACKtimeout, TXtimeout, TXpower, WAIT_TX); + + if (TXPacketL > 0) + { + //if transmitSXReliableAutoACK() returns > 0 then transmit and ack was OK + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); //read the actual transmitted CRC from the LoRa device buffer + packet_is_OK(); + Serial.println(); + } + else + { + //if transmitReliable() returns 0 there was an error + packet_is_Error(); + Serial.println(); + } + delay(500); //small delay between tranmission attampts + } + while (TXPacketL == 0); + + Serial.println(); + delay(1000); //have a delay between packets + + if (onoroff == 0) //toggle the on/off status + { + onoroff = 1; + } + else + { + onoroff = 0; + } + +} + + +void packet_is_OK() +{ + Serial.println(F("Packet acknowledged")); + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("No Packet acknowledge")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("211_Reliable_SXTransmitter_AutoACK Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF5, LORA_BW_1600, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/212_Reliable_SXReceiver_AutoACK/212_Reliable_SXReceiver_AutoACK.ino b/examples/SX128x_examples/Reliable/212_Reliable_SXReceiver_AutoACK/212_Reliable_SXReceiver_AutoACK.ino new file mode 100644 index 0000000..a8530fd --- /dev/null +++ b/examples/SX128x_examples/Reliable/212_Reliable_SXReceiver_AutoACK/212_Reliable_SXReceiver_AutoACK.ino @@ -0,0 +1,212 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a basic demonstration of the transmission and acknowledgement of a 'Reliable' + packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + If the received packet is valid then the networkID and payload CRC are returned in a 4 byte packet as an + acknowledgement that the transmitter listens for. If the transmitter does not receive the acknowledgement + of the networkID and payloadCRC within the ACKtimeout period, the original packet is re-transmitted until + a valid acknowledgement is received. This program should be used with the matching transmitter program, + 211_Reliable_SXTransmitter_AutoACK. + + The packet is destined for a specific receiver (destinationNode). A particular output (outputNumber) is + turned on and off. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define LED1 8 //this is output 1 + +#define ACKdelay 100 //delay in mS before sending acknowledge +#define RXtimeout 60000 //receive timeout in mS. +#define TXpower 2 //dBm power to use for ACK + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t PacketOK; //set to > 0 if packetOK +int16_t PacketRSSI; //stores RSSI of received packet +uint16_t LocalPayloadCRC; //locally calculated CRC of payload +uint16_t RXPayloadCRC; //CRC of payload received in packet +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter + +uint16_t destinationNode; //node number we are controlling, 0 to 65535 +const uint16_t thisNode = 2; //node number for this node +uint8_t outputNumber = 1; //output number on node we are controlling +uint8_t onoroff = 1; //set to 0 to set remote output off, 1 to set it on +uint8_t startaddr = 0; //address in SX buffer to start packet + +void loop() +{ + PacketOK = LT.receiveSXReliableAutoACK(startaddr, NetworkID, ACKdelay, TXpower, RXtimeout, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + RXPacketL = LT.readRXPacketL(); //get the received packet length + RXPayloadL = RXPacketL - 4; + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + + LT.startReadSXBuffer(startaddr); //start buffer read at location addr + destinationNode = LT.readUint16(); //load the destination node + outputNumber = LT.readUint8(); //load the output number + onoroff = LT.readUint8(); //0 for off, 1 for on + RXPayloadL = LT.endReadSXBuffer(); //this function returns the length of the array read + + if (PacketOK > 0) + { + //if the LT.receiveReliable() returns a value > 0 for PacketOK then packet and ack was received OK + //then only action payload if destinationNode = thisNode + Serial.print(F("Payload received OK > ")); + LT.printSXBufferHEX(startaddr, startaddr + RXPayloadL - 1); + Serial.println(); + + Serial.print(F("DestinationNode ")); + Serial.println(destinationNode); + + if (destinationNode == thisNode) + { + Serial.print(F("outputNumber ")); + Serial.println(outputNumber); + Serial.print(F("onoroff ")); + Serial.println(onoroff); + packet_is_OK(); + actionpayload(); + } + else + { + Serial.println(F("Not for this node")); + } + } + else + { + //if the LT.receiveReliable() function detects an error PacketOK is 0 + packet_is_Error(); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + //Serial.print(F("Payload received OK > ")); + //LT.printASCIIPacket(RXBUFFER, RXPayloadL); + //Serial.println(); + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void actionpayload() +{ + Serial.print(F("Action payload > ")); + if (outputNumber == 1) + { + Serial.print(F("outputNumber ")); + Serial.print(outputNumber); + if (onoroff) + { + Serial.println(F(" High")); + digitalWrite(LED1, HIGH); + } + else + { + Serial.println(F(" Low")); + digitalWrite(LED1, LOW); + } + } + else + { + Serial.println(F("Not valid outputNumber")); + } +} + + +void printPacketDetails() +{ + LocalPayloadCRC = LT.CRCCCITTReliable(startaddr, (startaddr + RXPayloadL - 1), 0xFFFF); //calculate payload crc from the received RXBUFFER + TransmitterNetworkID = LT.getRXNetworkID(RXPacketL); + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); + + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(TransmitterNetworkID, HEX); + Serial.print(F(",LocalPayloadCRC,0x")); + Serial.print(LocalPayloadCRC, HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(RXPayloadCRC, HEX); + LT.printReliableStatus(); +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("212_Reliable_SXReceiver_AutoACK Starting")); + + pinMode(LED1, OUTPUT); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF5, LORA_BW_1600, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/213_Reliable_Transmitter_Controller_ManualACK/213_Reliable_Transmitter_Controller_ManualACK.ino b/examples/SX128x_examples/Reliable/213_Reliable_Transmitter_Controller_ManualACK/213_Reliable_Transmitter_Controller_ManualACK.ino new file mode 100644 index 0000000..cce634b --- /dev/null +++ b/examples/SX128x_examples/Reliable/213_Reliable_Transmitter_Controller_ManualACK/213_Reliable_Transmitter_Controller_ManualACK.ino @@ -0,0 +1,163 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of a 'Reliable' + packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + If the received packet is valid then the networkID and payload CRC are returned in a 4 byte packet as an + acknowledgement that the transmitter listens for. If the transmitter does not receive the acknowledgement + within the ACKtimeout period, the original packet is re-transmitted until a valid acknowledgement is + received. + + With this example and the matching receiver program, 214_Reliable_Receiver_Controller_ManualACK + the generation of the acknowledge by the receiver is manual. This allows the received packet to be read + and decisions made as to whether to reply with an acknowledge. In this example an acknowledge is only + sent if the destinationNode variable in the transmitted packet matches the number for this node. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 2 //LoRa transmit power in dBm + +#define ACKtimeout 1000 //Acknowledge timeout in mS, set to 0 if ACK not used. +#define TXtimeout 1000 //transmit timeout in mS. If 0 return from transmit function after send. + +uint8_t TXPacketL; //length of transmitted packet +uint8_t TXPayloadL; //this is the payload length sent +uint8_t RXPacketL; //length of received acknowledge +uint16_t PayloadCRC; + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in receiver + +uint16_t destinationNode = 2; //node number we are controlling, 0 to 65535 +uint8_t outputNumber = 1; //output number on node we are controlling +uint8_t onoroff = 0; //set to 0 to set remote output off, 1 to set it on +uint8_t startaddr = 0; //address in SX buffer to start packet + + +void loop() +{ + + //now transmit the packet and keep transmitting until the acknowledge is received + + do + { + LT.startWriteSXBuffer(startaddr); //start the write at SX12XX internal buffer location 0 + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + TXPayloadL = LT.endWriteSXBuffer(); //closes packet write and returns the length of the payload to send + + Serial.print(F("Transmit payload > ")); + LT.printSXBufferHEX(startaddr, startaddr + TXPayloadL - 1); //print the sent SX array as HEX + Serial.println(); + Serial.flush(); + + TXPacketL = LT.transmitSXReliable(startaddr, TXPayloadL, NetworkID, TXtimeout, TXpower, WAIT_TX); //will return packet length > 0 if sent OK, otherwise 0 if transmit error + + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); //read the payload CRC sent + + RXPacketL = LT.waitReliableACK(NetworkID, PayloadCRC, ACKtimeout); + + if (RXPacketL > 0) + { + //if waitReliableACK() returns > 0 then valid ack was received + packet_is_OK(); + Serial.println(); + Serial.println(F("Ack Received")); + } + else + { + //if transmitReliable() returns 0 there is an error + packet_is_Error(); + Serial.println(); + Serial.println(F("No Ack Received")); + } + + + delay(200); //small delay between tranmission attampts + } + while (RXPacketL == 0); + + Serial.println(); + delay(1000); //have a delay between packets + + if (onoroff == 0) //toggle the on/off status + { + onoroff = 1; + } + else + { + onoroff = 0; + } + +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("213_Reliable_Transmitter_Controller_ManualACK Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/214_Reliable_Receiver_Controller_ManualACK/214_Reliable_Receiver_Controller_ManualACK.ino b/examples/SX128x_examples/Reliable/214_Reliable_Receiver_Controller_ManualACK/214_Reliable_Receiver_Controller_ManualACK.ino new file mode 100644 index 0000000..11c1c39 --- /dev/null +++ b/examples/SX128x_examples/Reliable/214_Reliable_Receiver_Controller_ManualACK/214_Reliable_Receiver_Controller_ManualACK.ino @@ -0,0 +1,215 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a demonstration of the receiving and acknowledgement of a 'Reliable' + packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + If the received packet is valid then the networkID and payload CRC are returned in a 4 byte packet as an + acknowledgement that the transmitter listens for. If the transmitter does not receive the acknowledgement + within the ACKtimeout period, the original packet is re-transmitted until a valid acknowledgement is + received. + + With this example and the matching receiver program, 213_Reliable_Transmitter_Controller_ManualACK + the generation of the acknowledge by the receiver is manual. This allows the received packet to be read + and decisions made as to whether to reply with an acknowledge. In this example an acknowledge is only + sent if the destinationNode variable in the transmitted packet matches the number for this node. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define LED1 8 //this is output 1 +#define ACKdelay 100 //delay in mS before sending acknowledge, not used here so set to 0 +#define RXtimeout 60000 //receive timeout in mS. +#define TXpower 2 //dBm power to use for ACK + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t PacketOK; //set to > 0 if packetOK +int16_t PacketRSSI; //stores RSSI of received packet +uint16_t LocalPayloadCRC; //locally calculated CRC of payload +uint16_t RXPayloadCRC; //CRC of payload received in packet +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter + +uint16_t destinationNode; //node number we are controlling, 0 to 65535 +const uint16_t thisNode = 2; //node number for this node +uint8_t outputNumber = 1; //output number on node we are controlling +uint8_t onoroff = 1; //set to 0 to set remote output off, 1 to set it on +uint8_t startaddr = 0; //address in SX buffer to start packet + + +void loop() +{ + PacketOK = LT.receiveSXReliable(startaddr, NetworkID, RXtimeout, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + RXPacketL = LT.readRXPacketL(); //get the received packet length + RXPayloadL = RXPacketL - 4; + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + + LT.startReadSXBuffer(startaddr); //start buffer read at location 0 + destinationNode = LT.readUint16(); //load the destination node + outputNumber = LT.readUint8(); //load the output number + onoroff = LT.readUint8(); //0 for off, 1 for on + RXPayloadL = LT.endReadSXBuffer(); //this function returns the length of the array read + + if (PacketOK > 0) + { + //if the LT.receiveReliable() returns a value > 0 for PacketOK then packet was received OK + //only action packet if destinationNode = thisNode + + Serial.print(F("Payload received OK > ")); + LT.printSXBufferHEX(startaddr, startaddr + RXPayloadL - 1); Serial.println(); + + Serial.print(F("destinationNode ")); + Serial.println(destinationNode); + + if (destinationNode == thisNode) + { + Serial.print(F("outputNumber ")); + Serial.println(outputNumber); + Serial.print(F("onoroff ")); + Serial.println(onoroff); + packet_is_OK(); + actionpayload(); + sendACK(); + } + } + else + { + //if the LT.receiveReliable() function detects an error PacketOK is 0 + packet_is_Error(); + } + + Serial.println(); + Serial.println(); +} + + +void packet_is_OK() +{ + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void actionpayload() +{ + Serial.print(F("Action payload > ")); + if (outputNumber == 1) + { + Serial.print(F("outputNumber ")); + Serial.print(outputNumber); + if (onoroff) + { + Serial.println(F(" High")); + digitalWrite(LED1, HIGH); + } + else + { + Serial.println(F(" Low")); + digitalWrite(LED1, LOW); + } + } + else + { + Serial.println(F("Not valid outputNumber")); + } +} + + +void printPacketDetails() +{ + LocalPayloadCRC = LT.CRCCCITTReliable(0, RXPayloadL - 1, 0xFFFF); //calculate payload crc + TransmitterNetworkID = LT.getRXNetworkID(RXPacketL); + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); + + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(TransmitterNetworkID, HEX); + Serial.print(F(",LocalPayloadCRC,0x")); + Serial.print(LocalPayloadCRC, HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(RXPayloadCRC, HEX); + LT.printReliableStatus(); +} + + +void sendACK() +{ + LocalPayloadCRC = LT.CRCCCITTReliable(0, RXPayloadL - 1, 0xFFFF); //calculate payload crc + delay(ACKdelay); + LT.sendReliableACK(NetworkID, LocalPayloadCRC, TXpower); + Serial.print(F("Ack sent")); +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("214_Reliable_Receiver_Controller_ManualACK Starting")); + pinMode(LED1, OUTPUT); //set high for on + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF7, LORA_BW_0400, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/215_Reliable_Transmitter_ACK_withData/215_Reliable_Transmitter_ACK_withData.ino b/examples/SX128x_examples/Reliable/215_Reliable_Transmitter_ACK_withData/215_Reliable_Transmitter_ACK_withData.ino new file mode 100644 index 0000000..b4707d5 --- /dev/null +++ b/examples/SX128x_examples/Reliable/215_Reliable_Transmitter_ACK_withData/215_Reliable_Transmitter_ACK_withData.ino @@ -0,0 +1,148 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of a 'Reliable' + packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + If the received packet is valid then a data payload together with the networkID and payload CRC are + returned in a packet as an acknowledgement that the transmitter listens for. If the transmitter does not + receive the acknowledgement within the ACKtimeout period, the original packet is re-transmitted until a + valid acknowledgement is received. + + With this example and the matching receiver program, 216_Reliable_Receiver_ACK_withData, the + generation of the acknowledge by the receiver is manual and also returns data to the transmitter. This + allows the transmitter to send a request to the receiver for it to return data. Since the acknowledge + returns the networkID and payload CRC used in the original transmitted request, when the transmitter + receives the acknowledge it can be very confident the data is geniune. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 2 //LoRa transmit power in dBm + +#define ACKtimeout 1000 //Acknowledge timeout in mS, set to 0 if ACK not used. +#define TXtimeout 1000 //transmit timeout in mS. If 0 return from transmit function after send. + +uint8_t TXPacketL; //length of transmitted packet +uint8_t RXPacketL; //length of received acknowledge +uint16_t PayloadCRC; + +const uint8_t RXBUFFER_SIZE = 16; //RX buffer size, set to max payload length of 251, or maximum expected length +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into +uint8_t buff[] = "Hello World"; //the payload to send + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in receiver + + +void loop() +{ + + do + { + Serial.print(F("Transmit Payload > ")); + LT.printASCIIArray(buff, sizeof(buff)); //print the payload buffer as ASCII + Serial.println(); + Serial.flush(); + + PayloadCRC = LT.CRCCCITT(buff, sizeof(buff), 0xFFFF); + + //now transmit the packet + TXPacketL = LT.transmitReliable(buff, sizeof(buff), NetworkID, TXtimeout, TXpower, WAIT_TX); //will return packet length > 0 if sent OK, otherwise 0 if transmit error + + RXPacketL = LT.waitReliableACK(RXBUFFER, sizeof(RXBUFFER), NetworkID, PayloadCRC, ACKtimeout); + + if (RXPacketL > 0) + { + //if waitReliableACK() returns > 0 then ack was received + packet_is_OK(); + Serial.println(); + Serial.print(F("Ack Received > ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL - 5); + Serial.println(); + } + else + { + //if transmitReliable() returns 0 there was an error + packet_is_Error(); + Serial.println(); + Serial.println(F("No Ack Received")); + Serial.println(); + } + + delay(200); //small delay between tranmission attempts + } + while (RXPacketL == 0); + + Serial.println(); + delay(5000); //have a delay between packets +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("215_Reliable_Transmitter_ACK_withData Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF5, LORA_BW_1600, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/216_Reliable_Receiver_ACK_withData/216_Reliable_Receiver_ACK_withData.ino b/examples/SX128x_examples/Reliable/216_Reliable_Receiver_ACK_withData/216_Reliable_Receiver_ACK_withData.ino new file mode 100644 index 0000000..634c923 --- /dev/null +++ b/examples/SX128x_examples/Reliable/216_Reliable_Receiver_ACK_withData/216_Reliable_Receiver_ACK_withData.ino @@ -0,0 +1,172 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 05/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of a 'Reliable' + packet. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + For a packet to be accepted by the receiver, the networkID and payload CRC appended to the packet by the + transmitter need to match those from the receiver which gives a high level of assurance that the packet + is valid. + + If the received packet is valid then a data payload together with the networkID and payload CRC are + returned in a packet as an acknowledgement that the transmitter listens for. If the transmitter does not + receive the acknowledgement within the ACKtimeout period, the original packet is re-transmitted until a + valid acknowledgement is received. + + With this example and the matching transmitter program, 215_Reliable_Transmitter_ACK_withData, the + generation of the acknowledge by the receiver is manual and also returns data to the transmitter. This + allows the transmitter to send a request to the receiver for it to return data. Since the acknowledge + returns the networkID and payload CRC used in the original transmitted request, when the transmitter + receives the acknowledge it can be very confident the data is geniune. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LT; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +#define ACKdelay 200 //delay in mS before sending acknowledge, not used here so set to 0 +#define RXtimeout 60000 //receive timeout in mS. +#define TXpower 2 //dBm power to use for ACK + +const uint8_t RXBUFFER_SIZE = 251; //RX buffer size, set to max payload length of 251, or maximum expected length +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create the buffer that received packets are copied into + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t PacketOK; //set to > 0 if packetOK +int16_t PacketRSSI; //stores RSSI of received packet +uint16_t LocalPayloadCRC; //locally calculated CRC of payload +uint16_t RXPayloadCRC; //CRC of payload received in packet +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter + + +void loop() +{ + PacketOK = LT.receiveReliable(RXBUFFER, RXBUFFER_SIZE, NetworkID, RXtimeout, WAIT_RX); //wait for a packet to arrive with 60seconds (60000mS) timeout + + RXPacketL = LT.readRXPacketL(); //get the received packet length + RXPayloadL = RXPacketL - 4; //payload length is always 4 bytes less than packet length + PacketRSSI = LT.readPacketRSSI(); //read the received packets RSSI value + + if (PacketOK > 0) + { + //if the LT.receiveReliable() returns a value > 0 for PacketOK then packet was received OK + Serial.print(F("Payload received OK > ")); + LT.printASCIIPacket(RXBUFFER, RXPayloadL); + Serial.println(); + packet_is_OK(); + sendACK(); + Serial.println(); + } + else + { + //if the LT.receiveReliable() function detects an error PacketOK is 0 + packet_is_Error(); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void printPacketDetails() +{ + LocalPayloadCRC = LT.CRCCCITT(RXBUFFER, RXPayloadL, 0xFFFF); //calculate payload crc from the received RXBUFFER + TransmitterNetworkID = LT.getRXNetworkID(RXPacketL); + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); + + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(TransmitterNetworkID, HEX); + Serial.print(F(",LocalPayloadCRC,0x")); + Serial.print(LocalPayloadCRC, HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(RXPayloadCRC, HEX); + LT.printReliableStatus(); +} + + +void sendACK() +{ + uint8_t buff[] = "Goodbye"; //the ack payload to send + LocalPayloadCRC = LT.CRCCCITTReliable(0, RXPayloadL - 1, 0xFFFF); //calculate received payload crc + delay(ACKdelay); + LT.sendReliableACK(buff, sizeof(buff), NetworkID, LocalPayloadCRC, TXpower); + Serial.print(F("Ack sent > ")); + LT.printASCIIPacket(buff, sizeof(buff)); +} + + +void setup() +{ + Serial.begin(115200); + Serial.println(); + Serial.println(F("216_Reliable_Receiver_ACK_withData Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1); + } + + LT.setupLoRa(2445000000, 0, LORA_SF5, LORA_BW_1600, LORA_CR_4_5); //configure frequency and LoRa settings + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/217_Reliable_Transmitter_Data_Requestor/217_Reliable_Transmitter_Data_Requestor.ino b/examples/SX128x_examples/Reliable/217_Reliable_Transmitter_Data_Requestor/217_Reliable_Transmitter_Data_Requestor.ino new file mode 100644 index 0000000..4916137 --- /dev/null +++ b/examples/SX128x_examples/Reliable/217_Reliable_Transmitter_Data_Requestor/217_Reliable_Transmitter_Data_Requestor.ino @@ -0,0 +1,238 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of 'Reliable' + packets to request information or operations from a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + The transmitter sends a request for the receiver which will respond with an acknowledge whach can include + any data requested. The transmitted request is a total of 6 or more bytes, with one byte for the payload + type and another byte for station the request is directed to. + + The request is for the receiver to return a set of GPS co-ordinates. This information is included in the + acknowledge the receiver sends if there is a match for request type and station number. The orginal network + ID and payload CRC are also returned with the acknowledge so the transmitter can verify if the packet it + receives in reply is geniune. + + The matching receiver program is 218_Reliable_Receiver_Data_Requestor. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" + +SX128XLT LT; //create a library class instance called LT + +uint8_t TXPacketL; //length of transmitted packet +uint8_t TXPayloadL; +uint8_t RXPayloadL; +uint8_t RXPacketL; //length of received acknowledge +uint16_t PayloadCRC; + +uint8_t RXBUFFER[16]; //create the buffer that received packets are copied into +uint8_t RequestType; //type of request to send +uint8_t RequestStation; //station request is aimed at + +float Latitude; //variables for GPS location +float Longitude; +float Altitude; +uint8_t TrackerStatus; + +//#define DEBUG + + +void loop() +{ + + //***************************************************************************************** + //Request GPS location + //***************************************************************************************** + + RequestStation = 123; + + if (sendRequestGPSLocation(RequestStation, 5)) + { + Serial.println(F("Valid ACK received")); +#ifdef DEBUG + packet_is_OK(); +#endif + actionRequestGPSLocation(); + } + else + { + Serial.println(F("No valid ACK received")); + } + + //***************************************************************************************** + + Serial.println(); + delay(5000); +} + + +uint8_t sendRequestGPSLocation(uint8_t station, uint8_t sendcount) +{ + uint8_t ackrequesttype; + uint8_t ackstation; + uint8_t startcount; + + startcount = sendcount; + + do + { + Serial.print(startcount - sendcount + 1); + Serial.print(F(" Transmit request")); + printRequestType(RequestGPSLocation); + Serial.print(F(" to station ")); + Serial.println(station); + Serial.flush(); + + //build the request payload + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of request + LT.writeUint8(station); //station to reply to request + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + + //now transmit the request + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXReliable(0, TXPayloadL, NetworkID, TXtimeout, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); + RXPacketL = LT.waitSXReliableACK(0, NetworkID, PayloadCRC, ACKtimeout); + + if (RXPacketL > 0) + { + //if waitSXReliableACK() returns > 0 then valid ack was received + ackrequesttype = LT.getByteSXBuffer(0); + ackstation = LT.getByteSXBuffer(1); + + if ((ackrequesttype == RequestGPSLocation) && (ackstation == station)) + { + return RXPacketL; + } + delay(200); //small delay between tranmission attampts + } + sendcount--; + } + while ((RXPacketL == 0) && (sendcount > 0)); + + Serial.println(F("ERROR send request failed")); + return 0; +} + + +void printRequestType(uint8_t type) +{ + switch (type) + { + case 1: + Serial.print(F(" RequestGPSLocation")); + break; + + default: + Serial.print(F(" not recognised")); + break; + } +} + + +void actionRequestGPSLocation() +{ + LT.startReadSXBuffer(0); //initialise SX buffer write at address 0 + RequestType = LT.readUint8(); //read type of request + RequestStation = LT.readUint8(); //who is the request reply from + Latitude = LT.readFloat(); //read latitude + Longitude = LT.readFloat(); //read longitude + Altitude = LT.readFloat(); //read altitude + TrackerStatus = LT.readUint8(); //read status byte + RXPayloadL = LT.endReadSXBuffer(); //close SX buffer read + + Serial.print(F("RequestGPSLocation reply > ")); + Serial.print(RequestStation); + Serial.print(F(",")); + Serial.print(Latitude, 6); + Serial.print(F(",")); + Serial.print(Longitude, 6); + Serial.print(F(",")); + Serial.print(Altitude, 1); + Serial.print(F(",")); + Serial.print(TrackerStatus); + Serial.println(); +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(115200); + Serial.println(); + Serial.println(__FILE__); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + } + else + { + Serial.println(F("ERROR No LoRa device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/217_Reliable_Transmitter_Data_Requestor/Settings.h b/examples/SX128x_examples/Reliable/217_Reliable_Transmitter_Data_Requestor/Settings.h new file mode 100644 index 0000000..f76a530 --- /dev/null +++ b/examples/SX128x_examples/Reliable/217_Reliable_Transmitter_Data_Requestor/Settings.h @@ -0,0 +1,29 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +const uint8_t NSS = 10; //select pin on LoRa device +const uint8_t NRESET = 9; //reset pin on LoRa device +const uint8_t RFBUSY = 7; //busy pin on LoRa device +const uint8_t DIO1 = 3; //DIO1 pin on LoRa device, used for sensing RX and TX done +const uint8_t LORA_DEVICE = DEVICE_SX1280; //we need to define the device we are using +const uint8_t LED1 = 8; + +//******* Setup LoRa modem parameters here ! *************** +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 2; //LoRa transmit power +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter +const uint8_t RequestGPSLocation = 1; //request type number for GPS location, so receiver knows what information to supply + +const uint32_t ACKtimeout = 1000; //Acknowledge timeout in mS, set to 0 if ACK not used. +const uint32_t TXtimeout = 1000; //transmit timeout in mS. If 0 return from transmit function after send. diff --git a/examples/SX128x_examples/Reliable/218_Reliable_Receiver_Data_Requestor/218_Reliable_Receiver_Data_Requestor.ino b/examples/SX128x_examples/Reliable/218_Reliable_Receiver_Data_Requestor/218_Reliable_Receiver_Data_Requestor.ino new file mode 100644 index 0000000..a9905d1 --- /dev/null +++ b/examples/SX128x_examples/Reliable/218_Reliable_Receiver_Data_Requestor/218_Reliable_Receiver_Data_Requestor.ino @@ -0,0 +1,249 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 17/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of 'Reliable' + packets to request information or operations from a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + The transmitter sends a request for the receiver which will respond with an acknowledge whach can include + any data requested. The transmitted request is a total of 6 or more bytes, with one byte for the payload + type and another byte for station the request is directed to. + + The request is for the receiver to return a set of GPS co-ordinates. This information is included in the + acknowledge the receiver sends if there is a match for request type and station number. The orginal network + ID and payload CRC are also returned with the acknowledge so the transmitter can verify if the packet it + receives in reply is geniune. + + The matching transmitter program is 217_Reliable_Transmitter_Data_Requestor. + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" + +SX128XLT LT; //create a library class instance called LT + +uint8_t RXBUFFER[251]; //create the buffer that received packets are copied into +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t TXPayloadL; //stores length of payload sent +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet +uint16_t StationNumber; +uint8_t RequestType; + +//#define DEBUG + + +void loop() +{ + uint8_t requesttype; + + requesttype = waitRequest(ThisStation, 5000); //listen for 5000mS, will return 0 if there is no request + + switch (requesttype) + { + case 0: + break; + + case RequestGPSLocation: + actionRequestGPSLocation(); + break; + + default: + Serial.println(F("Request not recognised")); + break; + } + Serial.println(); +} + + +uint8_t waitRequest(uint8_t station, uint32_t timeout) +{ + //wait for an incoming request, returns 0 if no request in timeout period + + Serial.println(F("Wait request")); + + RXPacketL = LT.receiveSXReliable(0, NetworkID, timeout, WAIT_RX); + + if (RXPacketL) + { +#ifdef DEBUG + Serial.print(F("Reliable packet received > ")); + packet_is_OK(); +#endif + RequestType = LT.getByteSXBuffer(0); + StationNumber = LT.getByteSXBuffer(1); + + Serial.print(F("Received ")); + printRequestType(RequestType); + Serial.print(F(" for station ")); + Serial.println(StationNumber); + + if (StationNumber == station) + { + return RequestType; + } + else + { + Serial.println(F("ERROR Request not for this station")); + } + } + else + { + if (LT.readIrqStatus() & IRQ_RX_TIMEOUT) + { + Serial.println(F("ERROR Timeout waiting for valid request")); + } + else + { + packet_is_Error(); + } + } + return 0; +} + + +bool actionRequestGPSLocation() +{ + uint16_t RXPayloadCRC; + + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); //fetch received payload crc to return with ack + + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of request + LT.writeUint8(ThisStation); //who is the request reply from + LT.writeFloat(TestLatitude); //add latitude + LT.writeFloat(TestLongitude); //add longitude + LT.writeFloat(TestAltitude); //add altitude + LT.writeUint8(TrackerStatus); //add status byte + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + + delay(ACKdelay); + + digitalWrite(LED1, HIGH); + LT.sendSXReliableACK(0, TXPayloadL, NetworkID, RXPayloadCRC, TXpower); + Serial.print(F("RequestGPSLocation Replied > ")); + Serial.print(ThisStation); + Serial.print(F(",")); + Serial.print(TestLatitude, 6); + Serial.print(F(",")); + Serial.print(TestLongitude, 6); + Serial.print(F(",")); + Serial.print(TestAltitude, 1); + Serial.print(F(",")); + Serial.print(TrackerStatus); + Serial.println(); + Serial.flush(); + digitalWrite(LED1, LOW); + return true; +} + + +void printRequestType(uint8_t type) +{ + switch (type) + { + case RequestGPSLocation: + Serial.print(F(" RequestGPSLocation")); + break; + + default: + Serial.print(F(" Request type not recognised")); + break; + } +} + + +void packet_is_OK() +{ + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void printPacketDetails() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(LT.getRXNetworkID(RXPacketL), HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(LT.getRXPayloadCRC(RXPacketL), HEX); + LT.printReliableStatus(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(115200); + Serial.println(); + Serial.println(__FILE__); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + } + else + { + Serial.println(F("ERROR No LoRa device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/218_Reliable_Receiver_Data_Requestor/Settings.h b/examples/SX128x_examples/Reliable/218_Reliable_Receiver_Data_Requestor/Settings.h new file mode 100644 index 0000000..5941e7f --- /dev/null +++ b/examples/SX128x_examples/Reliable/218_Reliable_Receiver_Data_Requestor/Settings.h @@ -0,0 +1,37 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +const uint8_t NSS = 10; //select pin on LoRa device +const uint8_t NRESET = 9; //reset pin on LoRa device +const uint8_t RFBUSY = 7; //busy pin on LoRa device +const uint8_t DIO1 = 3; //DIO1 pin on LoRa device, used for sensing RX and TX done +const uint8_t LED1 = 8; +const uint8_t LORA_DEVICE = DEVICE_SX1280; //we need to define the device we are using + +//******* Setup LoRa modem parameters here ! *************** +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power +const int8_t RangingTXPower = 10; //Transmit power used for ranging +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint32_t ACKdelay = 200; //delay in mS before sending reply +const uint32_t RXtimeout = 10000; //receive timeout in mS. + +const uint8_t RequestGPSLocation = 1; //request type for GPS location + +//GPS co-ordinates to use for the GPS location request +const float TestLatitude = 51.48230; //GPS co-ordinates to use for test +const float TestLongitude = -3.18136; //Cardiff castle keep, used for testing purposes +const float TestAltitude = 25.5; +const uint8_t TrackerStatus = 1; //set status bit to represent tracker GPS has fix + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter +const uint8_t ThisStation = 123; //the number of this station for requests and ranging diff --git a/examples/SX128x_examples/Reliable/219_Reliable_Transmitter_Data_RequestorIRQ/219_Reliable_Transmitter_Data_RequestorIRQ.ino b/examples/SX128x_examples/Reliable/219_Reliable_Transmitter_Data_RequestorIRQ/219_Reliable_Transmitter_Data_RequestorIRQ.ino new file mode 100644 index 0000000..2d5ce12 --- /dev/null +++ b/examples/SX128x_examples/Reliable/219_Reliable_Transmitter_Data_RequestorIRQ/219_Reliable_Transmitter_Data_RequestorIRQ.ino @@ -0,0 +1,240 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of 'Reliable' + packets to request information or operations from a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + The transmitter sends a request for the receiver which will respond with an acknowledge whach can include + any data requested. The transmitted request is a total of 6 or more bytes, with one byte for the payload + type and another byte for station the request is directed to. + + The request is for the receiver to return a set of GPS co-ordinates. This information is included in the + acknowledge the receiver sends if there is a match for request type and station number. The orginal network + ID and payload CRC are also returned with the acknowledge so the transmitter can verify if the packet it + receives in reply is geniune. + + No DIO1 pin needs to be connected to the LoRa device for this program. + + The matching receiver program is 220_Reliable_Receiver_Data_RequestorIRQ. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" + +SX128XLT LT; //create a library class instance called LT + +uint8_t TXPacketL; //length of transmitted packet +uint8_t TXPayloadL; +uint8_t RXPayloadL; +uint8_t RXPacketL; //length of received acknowledge +uint16_t PayloadCRC; + +uint8_t RXBUFFER[16]; //create the buffer that received packets are copied into +uint8_t RequestType; //type of request to send +uint8_t RequestStation; //station request is aimed at + +float Latitude; //variables for GPS location +float Longitude; +float Altitude; +uint8_t TrackerStatus; + +//#define DEBUG + + +void loop() +{ + + //***************************************************************************************** + //Request GPS location + //***************************************************************************************** + + RequestStation = 123; + + if (sendRequestGPSLocation(RequestStation, 5)) + { + Serial.println(F("Valid ACK received")); +#ifdef DEBUG + packet_is_OK(); +#endif + actionRequestGPSLocation(); + } + else + { + Serial.println(F("No valid ACK received")); + } + + //***************************************************************************************** + + Serial.println(); + delay(5000); +} + + +uint8_t sendRequestGPSLocation(uint8_t station, uint8_t sendcount) +{ + uint8_t ackrequesttype; + uint8_t ackstation; + uint8_t startcount; + + startcount = sendcount; + + do + { + Serial.print(startcount - sendcount + 1); + Serial.print(F(" Transmit request")); + printRequestType(RequestGPSLocation); + Serial.print(F(" to station ")); + Serial.println(station); + Serial.flush(); + + //build the request payload + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of request + LT.writeUint8(station); //station to reply to request + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + + //now transmit the request + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXReliableIRQ(0, TXPayloadL, NetworkID, TXtimeout, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); + PayloadCRC = LT.getTXPayloadCRC(TXPacketL); + RXPacketL = LT.waitSXReliableACKIRQ(0, NetworkID, PayloadCRC, ACKtimeout); + + if (RXPacketL > 0) + { + //if waitSXReliableACK() returns > 0 then valid ack was received + ackrequesttype = LT.getByteSXBuffer(0); + ackstation = LT.getByteSXBuffer(1); + + if ((ackrequesttype == RequestGPSLocation) && (ackstation == station)) + { + return RXPacketL; + } + delay(200); //small delay between tranmission attampts + } + sendcount--; + } + while ((RXPacketL == 0) && (sendcount > 0)); + + Serial.println(F("ERROR send request failed")); + return 0; +} + + +void printRequestType(uint8_t type) +{ + switch (type) + { + case 1: + Serial.print(F(" RequestGPSLocation")); + break; + + default: + Serial.print(F(" not recognised")); + break; + } +} + + +void actionRequestGPSLocation() +{ + LT.startReadSXBuffer(0); //initialise SX buffer write at address 0 + RequestType = LT.readUint8(); //read type of request + RequestStation = LT.readUint8(); //who is the request reply from + Latitude = LT.readFloat(); //read latitude + Longitude = LT.readFloat(); //read longitude + Altitude = LT.readFloat(); //read altitude + TrackerStatus = LT.readUint8(); //read status byte + RXPayloadL = LT.endReadSXBuffer(); //close SX buffer read + + Serial.print(F("RequestGPSLocation reply > ")); + Serial.print(RequestStation); + Serial.print(F(",")); + Serial.print(Latitude, 6); + Serial.print(F(",")); + Serial.print(Longitude, 6); + Serial.print(F(",")); + Serial.print(Altitude, 1); + Serial.print(F(",")); + Serial.print(TrackerStatus); + Serial.println(); +} + + +void packet_is_OK() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmittedPayloadCRC,0x")); //print CRC of transmitted packet + Serial.print(PayloadCRC, HEX); +} + + +void packet_is_Error() +{ + Serial.print(F("SendError")); + LT.printIrqStatus(); //prints the text of which IRQs set + LT.printReliableStatus(); //print the reliable status +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(115200); + Serial.println(); + Serial.println(__FILE__); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + } + else + { + Serial.println(F("ERROR No LoRa device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Transmitter ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/219_Reliable_Transmitter_Data_RequestorIRQ/Settings.h b/examples/SX128x_examples/Reliable/219_Reliable_Transmitter_Data_RequestorIRQ/Settings.h new file mode 100644 index 0000000..6c14936 --- /dev/null +++ b/examples/SX128x_examples/Reliable/219_Reliable_Transmitter_Data_RequestorIRQ/Settings.h @@ -0,0 +1,28 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +const uint8_t NSS = 10; //select pin on LoRa device +const uint8_t NRESET = 9; //reset pin on LoRa device +const uint8_t RFBUSY = 7; //busy pin on LoRa device +const uint8_t LORA_DEVICE = DEVICE_SX1280; //we need to define the device we are using +const uint8_t LED1 = 8; + +//******* Setup LoRa modem parameters here ! *************** +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 2; //LoRa transmit power +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter +const uint8_t RequestGPSLocation = 1; //request type number for GPS location, so receiver knows what information to supply + +const uint32_t ACKtimeout = 1000; //Acknowledge timeout in mS, set to 0 if ACK not used. +const uint32_t TXtimeout = 1000; //transmit timeout in mS. If 0 return from transmit function after send. diff --git a/examples/SX128x_examples/Reliable/220_Reliable_Receiver_Data_RequestorIRQ/220_Reliable_Receiver_Data_RequestorIRQ.ino b/examples/SX128x_examples/Reliable/220_Reliable_Receiver_Data_RequestorIRQ/220_Reliable_Receiver_Data_RequestorIRQ.ino new file mode 100644 index 0000000..ac65709 --- /dev/null +++ b/examples/SX128x_examples/Reliable/220_Reliable_Receiver_Data_RequestorIRQ/220_Reliable_Receiver_Data_RequestorIRQ.ino @@ -0,0 +1,252 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 17/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a demonstration of the transmission and acknowledgement of 'Reliable' + packets to request information or operations from a remote receiver. + + A reliable packet has 4 bytes automatically appended to the end of the buffer\array that is the data + payload. The first two bytes appended are a 16bit 'NetworkID'. The receiver needs to have the same + NetworkID as configured for the transmitter since the receiver program uses the NetworkID to check that + the received packet is from a known source. The third and fourth bytes appended are a 16 bit CRC of + the payload. The receiver will carry out its own CRC check on the received payload and can then verify + this against the CRC appended in the packet. The receiver is thus able to check if the payload is valid. + + The transmitter sends a request for the receiver which will respond with an acknowledge whach can include + any data requested. The transmitted request is a total of 6 or more bytes, with one byte for the payload + type and another byte for station the request is directed to. + + The request is for the receiver to return a set of GPS co-ordinates. This information is included in the + acknowledge the receiver sends if there is a match for request type and station number. The orginal network + ID and payload CRC are also returned with the acknowledge so the transmitter can verify if the packet it + receives in reply is geniune. + + No DIO1 pin needs to be connected to the LoRa device for this program. + + The matching transmitter program is 219_Reliable_Transmitter_Data_RequestorIRQ. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library +#include "Settings.h" + +SX128XLT LT; //create a library class instance called LT + +uint8_t RXBUFFER[251]; //create the buffer that received packets are copied into +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint8_t TXPayloadL; //stores length of payload sent +uint16_t TransmitterNetworkID; //the NetworkID from the transmitted and received packet +uint16_t StationNumber; +uint8_t RequestType; + +//#define DEBUG + + +void loop() +{ + uint8_t requesttype; + + requesttype = waitRequest(ThisStation, 5000); //listen for 5000mS, will return 0 if there is no request + + switch (requesttype) + { + case 0: + break; + + case RequestGPSLocation: + actionRequestGPSLocation(); + break; + + default: + Serial.println(F("Request not recognised")); + break; + } + Serial.println(); +} + + +uint8_t waitRequest(uint8_t station, uint32_t timeout) +{ + //wait for an incoming request, returns 0 if no request in timeout period + + Serial.println(F("Wait request")); + + RXPacketL = LT.receiveSXReliableIRQ(0, NetworkID, timeout, WAIT_RX); + + if (RXPacketL) + { +#ifdef DEBUG + Serial.print(F("Reliable packet received > ")); + packet_is_OK(); +#endif + RequestType = LT.getByteSXBuffer(0); + StationNumber = LT.getByteSXBuffer(1); + + Serial.print(F("Received ")); + printRequestType(RequestType); + Serial.print(F(" for station ")); + Serial.println(StationNumber); + + if (StationNumber == station) + { + return RequestType; + } + else + { + Serial.println(F("ERROR Request not for this station")); + } + } + else + { + if (LT.readIrqStatus() & IRQ_RX_TIMEOUT) + { + Serial.println(F("ERROR Timeout waiting for valid request")); + } + else + { + packet_is_Error(); + } + } + return 0; +} + + +bool actionRequestGPSLocation() +{ + uint16_t RXPayloadCRC; + + RXPayloadCRC = LT.getRXPayloadCRC(RXPacketL); //fetch received payload crc to return with ack + + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of request + LT.writeUint8(ThisStation); //who is the request reply from + LT.writeFloat(TestLatitude); //add latitude + LT.writeFloat(TestLongitude); //add longitude + LT.writeFloat(TestAltitude); //add altitude + LT.writeUint8(TrackerStatus); //add status byte + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + + delay(ACKdelay); + + digitalWrite(LED1, HIGH); + LT.sendSXReliableACKIRQ(0, TXPayloadL, NetworkID, RXPayloadCRC, TXpower); + Serial.print(F("RequestGPSLocation Replied > ")); + Serial.print(ThisStation); + Serial.print(F(",")); + Serial.print(TestLatitude, 6); + Serial.print(F(",")); + Serial.print(TestLongitude, 6); + Serial.print(F(",")); + Serial.print(TestAltitude, 1); + Serial.print(F(",")); + Serial.print(TrackerStatus); + Serial.println(); + Serial.flush(); + digitalWrite(LED1, LOW); + return true; +} + + +void printRequestType(uint8_t type) +{ + switch (type) + { + case RequestGPSLocation: + Serial.print(F(" RequestGPSLocation")); + break; + + default: + Serial.print(F(" Request type not recognised")); + break; + } +} + + +void packet_is_OK() +{ + printPacketDetails(); + Serial.println(); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + IRQStatus = LT.readIrqStatus(); //read the LoRa device IRQ status register + Serial.print(F("Error ")); + + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + Serial.print(F(" RXTimeout ")); + } + else + { + printPacketDetails(); + } +} + + +void printPacketDetails() +{ + Serial.print(F("LocalNetworkID,0x")); + Serial.print(NetworkID, HEX); + Serial.print(F(",TransmitterNetworkID,0x")); + Serial.print(LT.getRXNetworkID(RXPacketL), HEX); + Serial.print(F(",RXPayloadCRC,0x")); + Serial.print(LT.getRXPayloadCRC(RXPacketL), HEX); + LT.printReliableStatus(); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(115200); + Serial.println(); + Serial.println(__FILE__); + Serial.println(); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); //two further quick LED flashes to indicate device found + } + else + { + Serial.println(F("ERROR No LoRa device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed LED flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); //need to be in LoRa mode to receive requests + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/220_Reliable_Receiver_Data_RequestorIRQ/Settings.h b/examples/SX128x_examples/Reliable/220_Reliable_Receiver_Data_RequestorIRQ/Settings.h new file mode 100644 index 0000000..4481bde --- /dev/null +++ b/examples/SX128x_examples/Reliable/220_Reliable_Receiver_Data_RequestorIRQ/Settings.h @@ -0,0 +1,36 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 18/04/22 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +const uint8_t NSS = 10; //select pin on LoRa device +const uint8_t NRESET = 9; //reset pin on LoRa device +const uint8_t RFBUSY = 7; //busy pin on LoRa device +const uint8_t LED1 = 8; +const uint8_t LORA_DEVICE = DEVICE_SX1280; //we need to define the device we are using + +//******* Setup LoRa modem parameters here ! *************** +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const int8_t TXpower = 10; //LoRa transmit power +const int8_t RangingTXPower = 10; //Transmit power used for ranging +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint32_t ACKdelay = 200; //delay in mS before sending reply +const uint32_t RXtimeout = 5000; //receive timeout in mS. + +const uint8_t RequestGPSLocation = 1; //request type for GPS location + +//GPS co-ordinates to use for the GPS location request +const float TestLatitude = 51.48230; //GPS co-ordinates to use for test +const float TestLongitude = -3.18136; //Cardiff castle keep, used for testing purposes +const float TestAltitude = 25.5; +const uint8_t TrackerStatus = 1; //set status bit to represent tracker GPS has fix + +const uint16_t NetworkID = 0x3210; //NetworkID identifies this connection, needs to match value in transmitter +const uint8_t ThisStation = 123; //the number of this station for requests and ranging diff --git a/examples/SX128x_examples/Reliable/221_LoRa_Packet_Monitor/221_LoRa_Packet_Monitor.ino b/examples/SX128x_examples/Reliable/221_LoRa_Packet_Monitor/221_LoRa_Packet_Monitor.ino new file mode 100644 index 0000000..943c559 --- /dev/null +++ b/examples/SX128x_examples/Reliable/221_LoRa_Packet_Monitor/221_LoRa_Packet_Monitor.ino @@ -0,0 +1,157 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program listens for incoming packets using the LoRa settings in the 'Settings.h' + file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + There is a printout of the valid packets received in HEX format. Thus the program can be used to receive + and record non-ASCII packets. The LED will flash for each packet received. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + + +void loop() +{ + RXPacketL = LT.receiveSXBuffer(0, 60000, WAIT_RX); //returns 0 if packet error of some sort, timeout set at 60secs\60000mS + + digitalWrite(LED1, HIGH); //something has happened + + printSeconds(); + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + Serial.println(); +} + + +void packet_is_OK() +{ + printReceptionDetails(); + Serial.print(RXPacketL); + Serial.print(F(" bytes > ")); + LT.printSXBufferHEX(0, (RXPacketL - 1)); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //get the IRQ status + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F(" RXTimeout")); + } + else + { + Serial.print(F(" PacketError")); + printReceptionDetails(); + Serial.print(F(" Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + + +void printReceptionDetails() +{ + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB ")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void printSeconds() +{ + float secs; + + secs = ( (float) millis() / 1000); + Serial.print(secs, 3); +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(115200); + + Serial.println(F("221_LoRa_Packet_Monitor Starting")); + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1) + { + led_Flash(50, 50); + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); + Serial.println(); + LT.printOperatingSettings(); + Serial.println(); + Serial.print(F("Packet monitor ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/221_LoRa_Packet_Monitor/Settings.h b/examples/SX128x_examples/Reliable/221_LoRa_Packet_Monitor/Settings.h new file mode 100644 index 0000000..ee1e30c --- /dev/null +++ b/examples/SX128x_examples/Reliable/221_LoRa_Packet_Monitor/Settings.h @@ -0,0 +1,29 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done +#define LED1 8 //On board LED, high for on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes + +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate diff --git a/examples/SX128x_examples/Reliable/222_FLRC_Packet_Monitor/222_FLRC_Packet_Monitor.ino b/examples/SX128x_examples/Reliable/222_FLRC_Packet_Monitor/222_FLRC_Packet_Monitor.ino new file mode 100644 index 0000000..509e678 --- /dev/null +++ b/examples/SX128x_examples/Reliable/222_FLRC_Packet_Monitor/222_FLRC_Packet_Monitor.ino @@ -0,0 +1,157 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program listens for incoming packets using the FLRC settings in the 'Settings.h' + file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + There is a printout of the valid packets received in HEX format. Thus the program can be used to receive + and record non-ASCII packets. The LED will flash for each packet received. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + + +void loop() +{ + RXPacketL = LT.receiveSXBuffer(0, 60000, WAIT_RX); //returns 0 if packet error of some sort, timeout set at 60secs\60000mS + + digitalWrite(LED1, HIGH); //something has happened + + printSeconds(); + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + Serial.println(); +} + + +void packet_is_OK() +{ + printReceptionDetails(); + Serial.print(RXPacketL); + Serial.print(F(" bytes > ")); + LT.printSXBufferHEX(0, (RXPacketL - 1)); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //get the IRQ status + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F(" RXTimeout")); + } + else + { + Serial.print(F(" PacketError")); + printReceptionDetails(); + Serial.print(F(" Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + + +void printReceptionDetails() +{ + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB ")); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void printSeconds() +{ + float secs; + + secs = ( (float) millis() / 1000); + Serial.print(secs, 3); +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(115200); + + Serial.println(F("221_LoRa_Packet_Monitor Starting")); + + SPI.begin(); + + //SPI beginTranscation is normally part of library routines, but if it is disabled in library + //a single instance is needed here, so uncomment the program line below + //SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + while (1) + { + led_Flash(50, 50); + } + } + + LT.setupFLRC(Frequency, Offset, BandwidthBitRate, CodingRate, BT, Syncword); + + Serial.println(); + LT.printModemSettings(); + Serial.println(); + LT.printOperatingSettings(); + Serial.println(); + Serial.print(F("Packet monitor ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Reliable/222_FLRC_Packet_Monitor/Settings.h b/examples/SX128x_examples/Reliable/222_FLRC_Packet_Monitor/Settings.h new file mode 100644 index 0000000..1a1a09c --- /dev/null +++ b/examples/SX128x_examples/Reliable/222_FLRC_Packet_Monitor/Settings.h @@ -0,0 +1,31 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 06/11/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //RFBUSY pin on LoRa device +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done +#define LED1 8 //On board LED, high for on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes + +const uint8_t BandwidthBitRate = FLRC_BR_1_300_BW_1_2; //FLRC bandwidth and bit rate, 1.3Mbs +//const uint8_t BandwidthBitRate = FLRC_BR_0_260_BW_0_3; //FLRC 260kbps +const uint8_t CodingRate = FLRC_CR_1_0; //FLRC coding rate +const uint8_t BT = RADIO_MOD_SHAPING_BT_1_0; //FLRC BT +const uint32_t Syncword = 0x01234567; //FLRC uses syncword diff --git a/examples/SX128x_examples/Reliable/ReadMe.md b/examples/SX128x_examples/Reliable/ReadMe.md new file mode 100644 index 0000000..ecdc1fd --- /dev/null +++ b/examples/SX128x_examples/Reliable/ReadMe.md @@ -0,0 +1,389 @@ +--- +layout: post +title: "Reliable Packets with LoRa" +--- + +## Reliable Packets + +It would be helpful, and in some cases essential, if you could be really sure that a received LoRa packet was actually destined for a particular receiver and that it also contained valid sensor or control data for the application the receiver is running. + +It is inevitable that at some stage a LoRa receiver will get a packet from a foreign or unwanted source, so some thought needs to be done to be sure these rogue packets are rejected. + +### Sending and receiving reliable packets with cyclic redundancy checks + +The examples in this folder use a series of SX12xx library routines that append to the transmitted packets the CRC of the data payload. The receiver can then read this CRC from the packet and then do a local CRC check on the received payload to check there is a match. If there is no match, the packet is automatically rejected. + +Additional protection is added to the packet by the use of a 16 bit NetworkID. The NetworkID number (0-65535) is defined for a particular application so the transmitter and receiver need to use the same NetworkID. The receiver will then reject packets that do not match its own defined NetworkID. The NetworkID can also be used to direct packets to particular receivers. + +The CRC and NetworkID checking operates within the library transmit and receive functions in the background and although there are library routines to extract the payload CRC and NetworkID, there in no compelling need for the Arduino sketch itself to do so. + +The packet structure used for a reliable packet is; + + + +The use of the payload CRC and NetworkID adds a high level of certainty that the packet received is valid and safe to act on. If you so wish the CRC checking can be disabled and the NetworkID alone used to check packet validity. + + +## Reliable packet demonstration examples + +There are a number of examples of using reliable packets in this folder. The simplest examples are 201\_Reliable\_Transmitter and 201\_Reliable\_Receiver which transmit and receive a short payload 'Hello World'. + +### Examples '201\_Reliable\_Transmitter' and '202\_Reliable\_Receiver'. + +The basic reliable transmit function used is; + +LT.transmitReliable(buff, sizeof(buff), NetworkID, TXtimeout, TXpower, WAIT\_TX)) + +The buff defined in the transmit function is the name of a byte array that has been loaded with the payload, this can either be a sequence of characters, numbers or a structure. The receiver function also needs to know the size of this byte array. NetworkID is the ID for a particular application and needs to match the ID in the receiver. TXtimeout is the time in mS to wait for a transmission to finish, its used to stop the LoRa device potentially locking up a sketch\program. TXpower is the transmit power in dBm to be used and WAIT_TX tells the function to wait for the transmit to complete before exiting. + +The matching receive function is; + +LT.receiveReliable(RXBUFFER, RXBUFFER\_SIZE, NetworkID, RXtimeout, WAIT\_RX); + +The receiver fills the defined RXBUFFER with the contents of the received packet if its valid. The size of this buffer\array specified in RXBUFFER\_SIZE needs to be large enough to accommodate the largest payload size intended to be received. Due to the 4 bytes used by the packet for the networkID and payload CRC, the largest payload that can be received is 251 bytes. + +The payload is copied on receive into the RXBUFFER array. If the transmitted payload array is say 16 bytes long, and the transmit function increases the packet size by 4 to accommodate the networkID and payload CRC the packet length will be 20 bytes. Note that any packets arriving with a length greater than the size defined for RXBUFFER plus 4 bytes will be rejected. + +The NetworkID specified in the receive function has to match that used by the transmitter if the packet is to be accepted. + +The RXtimeout parameter is the number of milli seconds the receive function will wait for a packet to be received, if the period is exceeded a time-out will be reported. To not use the time-out function set RXtimeout to 0. + +WAIT\_RX makes the receive function wait for the receipt of a packet to complete before exiting. + +The transmitter program prints to the Arduino IDE serial monitor the NetworkID and payloadCRC (the payload is "Hello World") and when the receiver picks up the packet this payload should also be printed out. If the networkID of the transmitter does not match the networkID of the receiver the receiver reports the error. If the payload CRC that the transmitter appends to the packet does not match the CRC that the receiver calculates on the payload, this is also reported as an error. + +Thus in the receive program '202\_Basic\_Reliable\_Receiver' the code in the 'packet\_is\_OK()' function is only executed it the received packet passes both the networkID and payload CRC checks. + +With these example programs if you change the NetworkID either in the transmit or receive program and you should see the receiver rejects them. + +The transmitter program output looks like this; + + Transmit Payload > Hello World + LocalNetworkID,0x3210,TransmittedPayloadCRC,0xBC69 +If the receiver reports a valid packet which passes networkID and CRC checks the output looks like; + + Payload received OK > Hello World + LocalNetworkID,0x3210,TransmitterNetworkID,0x3210,LocalPayloadCRC,0xBC69,RXPayloadCRC,0xBC69 +If the networkID check fails the output can look like this; + + Error LocalNetworkID,0x3210,TransmitterNetworkID,0x55AB,LocalPayloadCRC,0xBC69,RXPayloadCRC,0xBC69 + +If the payload CRC check fails the output looks like; + + Error LocalNetworkID,0x3210,TransmitterNetworkID,0x1234,LocalPayloadCRC,0xBC69,RXPayloadCRC,0xBC69,ReliableIDError +If you don't want to use the CRC checking of the payload it can be turned off with this command; + + LT.setReliableConfig(NoReliableCRC); //disable payload CRC check + +You need to use the command on both the transmitter and the receiver. + + +## Using reliable packets to control stuff + +Sending Hello World messages is a useful start but a more practical example is to use the payload to control something on the receiver. The reliable examples show how the sent payload can be a structure, writing variables direct to an array and how to write the payload direct into the LoRa devices internal buffer and thus bypassing the need for an memory array. + +### Using a structure + +The next example 203\_Reliable\_Transmitter\_Controller\_Structure uses a structure as the payload; + + struct controllerStructure + { + uint16\_t destinationNode; + uint8\_t outputNumber; + uint8\_t onoroff; + }__attribute__((packed, aligned(1))); //remove structure padding so there is compatibility between 8bit and 32bit Arduinos + +The destinationNode is a 16 bit number that the receiver reads to see if the received packet is destined for that receiver. You can set outputNumber to control a range of outputs and onoroff is set to 1 to turn on the output and to 0 to turn it on. + +With the 203 transmitter example running load the 204\_Reliable\_Receiver\_Controller\_Structure program onto the receiver Arduino and it should report the packets received and the LED connected to the pin defined by LED1 should flash on and off. + +These structure examples would be easy to expand to control more outputs such as servos and similar. + +**Note:** Structures on different Arduino types can use different formats. If the transmitter is one Arduino type and the receiver is another type the receiver may not be able to read the transferred structure correctly. + +### Writing variables direct to an array + +A SX12xx library file, arrayRW.h has routines that allow for variables to be directly written to an array. For example if the name of the array is 'controlarray' and then if you review the transmitter program, 205\_Reliable\_Transmitter\_Controller\_ArrayRW, you will see it fills the array with variables like this; + + beginarrayRW(controlarray, 0); //start writing to array at location 0 + arrayWriteUint16(destinationNode); + arrayWriteUint8(outputNumber); + arrayWriteUint8(onoroff); //0 for off, 1 for on + controlarraysize = endarrayRW(); //this function returns the length of the array to send + +When the 206\_Reliable\_Receive\_Controller\_ArrayRW program receives the packet its loaded into a defined array (RXBUFFER in the example) and the sketch then reads the variables from the the array like this; + + beginarrayRW(RXBUFFER, 0); //start reading from array at location 0 + destinationNode = arrayReadUint16(); + outputNumber = arrayReadUint8(); + onoroff = arrayReadUint8(); //0 for off, 1 for on + controlarraysize = endarrayRW(); //this function returns the length of the array to send + + +As with the previous examples the LED on the receiver should flash on and off. + +The full set of write variable functions provided by the arrayRW.h library file is; + + arrayWriteUint8(uint8_t buffdata) + arrayWriteInt8(int8_t buffdata) + arrayWriteChar(char buffdata) + arrayWriteUint16(uint16_t buffdata) + arrayWriteInt16(int16_t buffdata) + arrayWriteFloat(float tempf) + arrayWriteUint32(uint32_t tempdata) + arrayWriteInt32(int32_t tempdata) + arrayWriteCharArray(char *buff, uint8_t len) + arrayWriteByteArray(uint8_t *buff, uint8_t len) + +And the read functions are; + + uint8_t arrayReadUint8() + int8_t arrayReadInt8() + char arrayReadChar() + uint16_t arrayReadUint16() + int16_t arrayReadInt16() + float arrayReadFloat() + uint32_t arrayReadUint32() + int32_t arrayReadInt32() + arrayReadCharArray(char *buff, uint8_t len) + arrayReadByteArray(uint8_t *buff, uint8_t len) + + +### Low memory controller + +Programs 207\_Reliable\_SXTransmitter\_Controller and 208\_Reliable\_SXReceiver\_Controller demonstrate how use the SX12XX library to directly write and read variables to and from the LoRa devices internal buffer directly for transmit and receive functions. There is no memory array required to be defined filled or used by transmitter and receiver programs. If large packets are being sent and received this can save a significant amount of memory. + +The 207 and 208 examples follow the same controller example of programs 203 and 204 in that an LED on the receiver should flash on and off. + +The transmitter packet is loaded into the LoRa devices buffer like this; + + LT.startWriteSXBuffer(0); //start the write at SX12XX internal buffer location 0 + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + TXPayloadL = LT.endWriteSXBuffer(); //closes packet write and returns the length of the payload to send + +And the receiver loads the variables from the LoRa devices buffer like this; + + LT.startReadSXBuffer(0); //start buffer read at location 0 + destinationNode = LT.readUint16(); //load the destination node + outputNumber = LT.readUint8(); //load the output number + onoroff = LT.readUint8(); //0 for off, 1 for on + RXPayloadL = LT.endReadSXBuffer(); //this function returns the length of the array read + +Clearly with these direct accesses to the LoRa devices buffer the order and type of variables written has to match the order when read out by the receiver. + +The full set of library routines for writing direct to the LoRa devices buffer is; + + writeUint8(uint8_t x); + writeInt8(int8_t x); + writeChar(char x); + writeUint16(uint16_t x); + writeInt16(int16_t x); + writeUint32(uint32_t x); + writeInt32(int32_t x); + writeFloat(float x); + writeBuffer(uint8_t *txbuffer, uint8_t size); //uint8_t buffer + writeBufferChar(char *txbuffer, uint8_t size); //char buffer + +And for reading direct from the LoRa devices buffer; + + uint8_t readUint8(); + int8_t readInt8(); + char readChar(); + uint16_t readUint16(); + int16_t readInt16(); + uint32_t readUint32(); + int32_t readInt32(); + float readFloat(); + uint8_t readBuffer(uint8_t *rxbuffer); //reads buffer till a null 0x00 is reached + uint8_t readBuffer(uint8_t *rxbuffer, uint8_t size); + uint8_t readBufferChar(char *rxbuffer); + + +#### Getting the payload CRC into a sketch + +The CRC of the transmitted payload, that is appended to the packet and printed out in the examples, can be obtained by the sketch in several ways, first the simplest way is to use a SX12XX library function after the packet has been transmitted or received; + + TXpayloadCRC = LT.getTXPayloadCRC(TXPacketL); + RXpayloadCRC = LT.getRXPayloadCRC(RXPacketL); + +The payload CRC can be read direct from the end of the packet buffer of the LoRa device like this; + + PayloadCRC = LT.readUint16SXBuffer(PayloadL+2); + +Where PayloadL is the length in bytes of the payload, this is the method used in the 201 transmitter example. + +Or the payload CRC can be calculated locally by accessing the local payload array\buffer directly like this which uses another SX12xx library function; + + LocalPayloadCRC = LT.CRCCCITT(buff, sizeof(buff), 0xFFFF); + +Where buff is the name of the array containing the payload. +Where 0xFFFF is the CRC check start value, normally 0xFFFF. + +And finally there is a SX12xx library function that will return the CRC of an area of the LoRa devices internal buffer like this; + + LT.CRCCCITTReliable(0, PayloadL-1, 0xFFFF); + +Where PayloadL is the length of the payload array\buffer. + +Where 0 is the address in the buffer to start the CRC check. +Where 0xFFFF is the CRC check start value, normally 0xFFFF. + + +#### Getting the NetworkID into a sketch + +The NetworkID used for the transmission or reception of a packet, that is appended to the packet and printed out in the examples, can be obtained by the sketch using an SX12XX library function after the packet has been transmitted or received; + + TXNetworkID = LT.getTXNetworkID(TXPacketL); + RXNetworkID = LT.getRXNetworkID(RXPacketL); + + +## Sending and receiving reliable packets with an acknowledge + +Sometimes we may need to be sure the receiver has actually received the packet sent and we may want to keep transmitting the packet until it actually is received. Thus the receiver needs, when it has accepted a valid reliable packet, to send an acknowledge packet back that the transmitter will be listening for. + +Clearly we could now have the same problem as for a simple reliable packet, how does the transmitter know the received packet actually is an acknowledge from the specific receiver ? + +The simple way to be sure is to turn around the received NetworkID and payloadcrc (i.e. a total of 4 bytes) and send these bytes as an acknowledge. The transmitter program knows the NetworkID and payloadcrc used in the transmit function so can check to a high level of certainty that the received packet is a valid acknowledge, all four bytes of the acknowledge have to be correct. + + +## Examples using acknowledgements + +The previous examples, where there was no acknowledgement used were a very basic transmit and receive, a set of examples using a structure to control an remote output, doing the same with direct array read and write and then a low memory example writing and reading direct to the LoRa devices buffer. There are two types of acknowledgement possible with the library, the first is a simple Auto Acknowledge which is only 4 bytes, consisting of the the NetworkID and payload CRC. The second type of acknowledge allows the receiver to send back data to the transmitter. This form of acknowledge can be used when the transmitter wants to request some data or control information from the receiver. Since the original NetworkID and payload CRC used by the transmitter is sent back with the acknowledge the transmitter can be very confident that the data coming back is genuine. + +These two examples; **209\_Reliable\_Transmitter\_AutoACK** and **210\_Reliable\_Receiver\_AutoACK** are the basic 201 and 202 examples modified to use the auto acknowledge. + +The transmit function when acknowledge is configured is; + + transmitReliable(buff, sizeof(buff), NetworkID, ACKtimeout, TXtimeout, TXpower, WAIT_TX)) + +If AutoACK is used then a ACKtimeout needs to be specified. ACKtimeout is the milliseconds the transmit function would listen for a valid acknowledge before returning an error. How short this period is rather depends on the LoRa settings in use and the actual Arduinos being used. Remember the sending of the acknowledge will have a on-air time that the transmitter needs to account for. You can in some circumstances have an ACKtimeout as low as 25mS and that is still enough time for the receiver to turn around from receive to transmit and the transmitter to flip to receive mode and pick-up the packet. You need to experiment here, perhaps start at 1000mS and gradually reduce the time (with a working set-up) until the point is reached when the receipt of the acknowledge fails. + +With the transmit function if the returned byte is 0 this indicates to the sketch that there has been an error of some type, one such error could be no acknowledge received. + +The matching receive function is; + + receiveReliable(RXBUFFER, RXBUFFER_SIZE, NetworkID, ACKdelay, TXpower, RXtimeout, WAIT_RX); + +The ACKdelay parameter is in miliseconds and it's the time the receiver waits before sending an acknowledge. With some hardware a delay here of 0mS might be OK, but with faster hardware you may need to increase it. Maybe start with an ACKdelay of 50mS and an ACKtimeout of 1000mS in the transmit function and reduce the numbers in steps. You only need to do this if you want or need to maximise response time. + +If the returned byte from the receiveReliable() function is 0 then there was a problem during receive. + +The 209 transmitter will keep sending the payload until the transmitReliable() function returns a non zero value. The transmitReliable() function will return a zero value if no acknowledge is received within the ACKtimeout period. + +Note that in the case of a NetworkID mismatch the receiver will not transmit an acknowledge, so the transmitter reports it as an a NoReliableACK error. + +The auto acknowledge is a simple way of making the transmission of packets more reliable, but it might not be appropriate in all circumstances. For instance consider the **207\_Reliable\_Transmitter\_Controller\_LowMemory** and **208\_Reliable\_Receiver\_Controller\_LowMemory** examples where the payload contains the following; + + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + +Here the destinationNode number is directed to a particular node number, 2 in that example. If the packet is received by node number 5, then there should be no acknowledge sent. In these circumstances the receiver program needs to intervene directly on the received packet, read the payload and check for a matching destinationNode number. If there is a match then an acknowledge can be sent manually and the transmitter knows the packet has been received. + +### Low memory acknowledge + +Example programs **211\_Reliable\_SXTransmitter\_AutoACK** and **212\_Reliable\_SXReceiver\_AutoACK** demonstrate how to use the SX12xx library functions to bypass the need to use an intermediate array for the payload. Instead of filling an array with a structure or byte array full of variables, the variables to send are written directly to the LoRa device internal buffer on transmit and read from the buffer on receive. + +Filling the SX12XX LoRa devices buffer is done like so; + + LT.startWriteSXBuffer(0); //start the write at SX12XX internal buffer location 0 + LT.writeUint16(destinationNode); //destination node for packet + LT.writeUint8(outputNumber); //output number on receiver + LT.writeUint8(onoroff); //0 for off, 1 for on + TXPayloadL = LT.endWriteSXBuffer(); //closes packet write and returns the length of the payload to send + +And then the appropriate transmit function is used; + + TXPacketL = LT.transmitSXReliableAutoACK(0, TXPayloadL, NetworkID, ACKtimeout, TXtimeout, TXpower, WAIT_TX); + +On the receive side the receive is setup like so; + + PacketOK = LT.receiveSXReliableAutoACK(0, NetworkID, ACKdelay, TXpower, RXtimeout, WAIT_RX); + +And if on receive the packet passes the NetworkID and payloadCRC checks (PacketOK returns > 0) it is read like this; + + LT.startReadSXBuffer(0); //start buffer read at location 0 + destinationNode = LT.readUint16(); //load the destination node + outputNumber = LT.readUint8(); //load the output number + onoroff = LT.readUint8(); //0 for off, 1 for on + RXPayloadL = LT.endReadSXBuffer(); //this function returns the length of the array read + +Note that in this example the payload contains a 16 bit destinationNode variable, which can be used to direct the packet to one of many nodes. The sketch checks that the destinationNode matches the number given to that receiver and if there is a match actions the packet. + + +### Manual acknowledge + +The programs **213\_Reliable\_Transmitter\_Controller\_ManualACK** and **214\_Reliable\_Receiver\_Controller\_ManualACK** use a manual acknowledge set-up whereby the receive picks up the transmitted payload and reads the destinationNode parameter to decide if the packet is destined for that node. If it is the acknowledge is sent which contains the networkID and the CRC of the original payload, thus the transmitter knows the sent packet has been received correctly. + +The receive sketch can pause at the point the payload is being actioned, perhaps reading an external sensor waiting for conformation that the action as completed, a gate is confirmed opened\closed for instance, before sending the acknowledge. + +There is a further enhancement to the manual acknowledge set-up, the acknowledge can contain some data to be returned to the transmitter. + +### Manual acknowledge returning data + +The standard acknowledge is only 4 bytes, the NetworkID and payload CRC. However the acknowledge can be sent with an array of data included in the acknowledge. + +The format of this function is; + + transmitReliableACK(uint8\_t *txbuffer, uint8\_t size, uint16\_t networkID, uint16\_t payloadcrc, int8\_t txpower); + +Here the receiver sending the acknowledge can include an array txbuffer of a specified size. This returned array could be a structure, as per example 203 and 204 or an array filled directly with the arrayRW.h library file as used in examples 205 and 206. + +To demonstrate returning an array in the acknowledge examples 201 and 202 were modified so that the transmitted 'Hello World' example has 'Goodbye' returned from the receiver with the acknowledge and is then printed out on the transmitter. The modified programs are **215\_Reliable\_Transmitter\_ManualACK\_withData** and **216\_Reliable\_Receiver\_ManualACK\_withData** + + +### Requesting data + +We can use the acknowledge functions of reliable packets to send a request to a remote station to transfer data. The returned data will include the networkID and payloadCRC sent with the request and since the returned data\packet is also protected by the internal LoRa packet CRC we can be fairly confident we are getting valid data back. + +Take for instance a remote node that has a GPS attached and we want to know the current location of the node. Of course the node could transmit its location on a regular basis but this can be wasteful of power and there will be issues with conflicting transmissions if there are a number of nodes out there. + +Examples **217\_Reliable\_Transmitter\_Data\_Requestor** and **218\_Reliable\_Receiver\_Data\_Requestor** demonstrate such a requesting of data. + +Assume at the station wanting the remote nodes location we send this payload; + + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of packet + LT.writeUint8(RequestStation); //station to reply to request + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + +RequestGPSLocation is assigned a value of 1; + + const uint8_t RequestGPSLocation = 1; + +RequestStation is from 0 to 255 and is the station node\number that we want the location from, in this case RequestStation will have a number of 123. + +The packet is protected by the NetworkID and payload CRC so station 123 will see the request as valid and loads the location data into the LoRa device like this; + + LT.startWriteSXBuffer(0); //initialise SX buffer write at address 0 + LT.writeUint8(RequestGPSLocation); //identify type of request + LT.writeUint8(ThisStation); //who is the request reply from + LT.writeFloat(TestLatitude); //add latitude + LT.writeFloat(TestLongitude); //add longitude + LT.writeFloat(TestAltitude); //add altitude + TXPayloadL = LT.endWriteSXBuffer(); //close SX buffer write + +In this case the returned values are test values of Latitude, Longitude and Altitude. + +The original requesting node sees that the valid reply\Acknowledge is a GPS location request from station 123 and can then act on the data. + +Examples **219\_Reliable\_Transmitter\_Data\_RequestorIRQ** and **220\_Reliable\_Receiver\_Data\_RequestorIRQ** are versions of the above 217 and 218 examples that do not require access to the DIO1 pin on the LoRa device to detect RXdone and TXDone. These versions can be useful for situations where there are few microcontroller pins available to drive the LoRa device such as with the ESP32CAM for instance. + + +### Using program 221\_LoRa\_Packet\_Monitor or 222\_FLRC\_Packet\_Monitor + +When debugging what's going on in a send and acknowledge set-up its useful to be able to see what is happening in real time. This packet monitor example will display the bytes received in hexadecimal, in the example printout below you can see two packets. The 16 byte packet contains the text 'Hello World' and then the NetworkID, 0x3210, then the payload CRC, 0xBC69 at the end. + +The 4 byte packet that is seen around 130mS later is the acknowledge which contains the NetworkID, 0x3210, then the payload CRC, 0xBC69. + + + 125.103 RSSI,-99dBm,SNR,10dB 16 bytes > 48 65 6C 6C 6F 20 57 6F 72 6C 64 00 10 32 69 BC + 125.237 RSSI,-96dBm,SNR,8dB 4 bytes > 10 32 69 BC + +
+
+ +### Stuart Robinson +### November 2021 \ No newline at end of file diff --git a/examples/SX128x_examples/RemoteControl/21_On_Off_Transmitter/21_On_Off_Transmitter.ino b/examples/SX128x_examples/RemoteControl/21_On_Off_Transmitter/21_On_Off_Transmitter.ino new file mode 100644 index 0000000..da2250c --- /dev/null +++ b/examples/SX128x_examples/RemoteControl/21_On_Off_Transmitter/21_On_Off_Transmitter.ino @@ -0,0 +1,306 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program is a remote control transmitter. When one of four switches are made + (shorted to ground) a packet is transmitted with single byte indicating the state of Switch0 as bit 0, + Switch1 as bit 1 and Switch2 as bit 2. To prevent false triggering at the receiver the packet contains a + 32 bit number called the TXIdentity which in this example is set to 1234554321. The receiver will only + act on, change the state of the outputs, if the identity set in the receiver matches that of the + transmitter. The chance of a false trigger is fairly remote. + + Between switch presses the LoRa device and Atmel microcontroller are put to sleep. A switch press wakes + up the processor from sleep, the switches are read and a packet sent. On a 'bare bones' Arduino setup + the transmitter has a sleep current of approx 2.2uA, so it's ideal for a battery powered remote control + with a potential range of many kilometres. + + The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. These settings + are not necessarily optimised for long range. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" +#include + +#include //watchdog timer library, integral to Arduino IDE +#include //watchdog timer library, integral to Arduino IDE +#include "PinChangeInterrupt.h" //get the library here; https://github.com/NicoHood/PinChangeInterrupt + +SX128XLT LT; + +uint32_t TXpacketCount; +uint8_t TXPacketL; + +volatile bool switch0flag = false; +volatile bool switch1flag = false; +volatile bool switch2flag = false; +volatile bool switch3flag = false; + +void loop() +{ + uint8_t switches; + + digitalWrite(LED1, LOW); //turn off indicator LED + Serial.print(F("Sleeping zzzz")); + Serial.flush(); //make sure all serial output has gone + + LT.setSleep(CONFIGURATION_RETENTION); //sleep LoRa device, keeping register settings in sleep. + sleep_permanent(); //sleep Atmel processor permanently for switch wakeup only + LT.wake(); //wake up the lora device - nicely + + digitalWrite(LED1, HIGH); + + Serial.println(F(" - Awake !!")); //the processor has woken up + switches = readSwitches(); //read the state of the switches + + TXpacketCount++; + Serial.print(TXpacketCount); //print the numbers of sends + Serial.print(F(" Sending > ")); + + Serial.print(switches, BIN); + + if (sendSwitchPacket(switches)) + { + Serial.println(F(" SentOK")); + } + else + { + Serial.print(F("Send Error - IRQreg,")); + Serial.print(LT.readIrqStatus(), HEX); + } + + Serial.println(); + delay(500); +} + + +uint8_t sendSwitchPacket(uint8_t switches) +{ + //The SX12XX buffer is filled with variables of a known type and in a known sequence. Make sure the + //receiver uses the same variable types and sequence to read variables out of the receive buffer. + uint8_t len; + + LT.startWriteSXBuffer(0); //start the write packet to buffer process + LT.writeUint8(RControl1); //this byte identifies the type of packet + LT.writeUint32(TXIdentity); //this 32bit integer defines the Identity of the transmiter + LT.writeUint8(switches); //this byte contains the 8 switch values to be sent + len = LT.endWriteSXBuffer(); //close the packet, get the length of data to be sent + + //now transmit the packet, 10 second timeout, and wait for it to complete sending + TXPacketL = LT.transmitSXBuffer(0, len, 10000, TXpower, WAIT_TX); + + return TXPacketL; //TXPacketL will be 0 if there was an error sending +} + + +void sleep_permanent() +{ + attachInterrupts(); + + ADCSRA = 0; //disable ADC + set_sleep_mode (SLEEP_MODE_PWR_DOWN); + noInterrupts (); //timed sequence follows + sleep_enable(); + + // turn off brown-out enable in software + MCUCR = bit (BODS) | bit (BODSE); //turn on brown-out enable select + MCUCR = bit (BODS); //this must be done within 4 clock cycles of above + interrupts (); //guarantees next instruction executed + + sleep_cpu (); //sleep within 3 clock cycles of above + + /* wake up here */ + + sleep_disable(); + + detachInterrupts(); +} + + +void attachInterrupts() +{ + if (SWITCH0 >= 0) + { + attachPCINT(digitalPinToPCINT(SWITCH0), wake0, FALLING); + switch0flag = false; + } + + if (SWITCH1 >= 0) + { + attachPCINT(digitalPinToPCINT(SWITCH1), wake1, FALLING); + switch1flag = false; + } + + if (SWITCH2 >= 0) + { + attachPCINT(digitalPinToPCINT(SWITCH2), wake2, FALLING); + switch2flag = false; + } + + if (SWITCH3 >= 0) + { + attachPCINT(digitalPinToPCINT(SWITCH3), wake3, FALLING); + switch3flag = false; + } + +} + + +void detachInterrupts() +{ + if (SWITCH0 >= 0) + { + detachPCINT(digitalPinToPCINT(SWITCH0)); + } + + if (SWITCH1 >= 0) + { + detachPCINT(digitalPinToPCINT(SWITCH1)); + } + + if (SWITCH2 >= 0) + { + detachPCINT(digitalPinToPCINT(SWITCH2)); + } + + if (SWITCH3 >= 0) + { + detachPCINT(digitalPinToPCINT(SWITCH3)); + } + +} + + + + +void wake0() +{ + switch0flag = true; +} + + +void wake1() +{ + switch1flag = true; +} + + +void wake2() +{ + switch2flag = true; +} + + +void wake3() +{ + switch3flag = true; +} + + +uint8_t readSwitches() +{ + uint8_t switchByte = 0xFF; //start assuming all switches off + + if (switch0flag) + { + bitClear(switchByte, 0); //if the flag is set clear the bit + Serial.println(F("SWITCH0 pressed")); + } + + if (switch1flag) + { + bitClear(switchByte, 1); //if the flag is set clear the bit + Serial.println(F("SWITCH1 pressed")); + } + + if (switch2flag) + { + bitClear(switchByte, 2); //if the flag is set clear the bit + Serial.println(F("SWITCH2 pressed")); + } + + + if (switch3flag) + { + bitClear(switchByte, 3); //if the flag is set clear the bit + Serial.println(F("SWITCH3 pressed")); + } + + return switchByte; +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setupSwitches() +{ + if (SWITCH0 >= 0) + { + pinMode(SWITCH0, INPUT_PULLUP); + } + + if (SWITCH1 >= 0) + { + pinMode(SWITCH1, INPUT_PULLUP); + } + + if (SWITCH2 >= 0) + { + pinMode(SWITCH2, INPUT_PULLUP); + } + + if (SWITCH3 >= 0) + { + pinMode(SWITCH3, INPUT_PULLUP); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + setupSwitches(); + + Serial.begin(9600); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates LoRa device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Transmitter ready")); + Serial.println(); + +} diff --git a/examples/SX128x_examples/RemoteControl/21_On_Off_Transmitter/Settings.h b/examples/SX128x_examples/RemoteControl/21_On_Off_Transmitter/Settings.h new file mode 100644 index 0000000..95fbd88 --- /dev/null +++ b/examples/SX128x_examples/RemoteControl/21_On_Off_Transmitter/Settings.h @@ -0,0 +1,42 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +const int8_t NSS = 10; //select on LoRa device +const int8_t NRESET = 9; //reset on LoRa device +const int8_t RFBUSY = 7; //RF busy on LoRa device +const int8_t DIO1 = 3; //DIO1 on LoRa device, used for RX and TX done + +const int8_t LED1 = 8; //On board LED, logic high is on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +const int8_t SWITCH0 = 2; +const int8_t SWITCH1 = 4; +const int8_t SWITCH2 = A3; +const int8_t SWITCH3 = A2; + +const uint32_t TXIdentity = 1234554321; //define an identity number, the receiver must use the same number +//range is 0 to 4294967296 + + + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +#define TXpower 10 //power for transmissions in dBm diff --git a/examples/SX128x_examples/RemoteControl/22_On_Off_Receiver/22_On_Off_Receiver.ino b/examples/SX128x_examples/RemoteControl/22_On_Off_Receiver/22_On_Off_Receiver.ino new file mode 100644 index 0000000..5179ed3 --- /dev/null +++ b/examples/SX128x_examples/RemoteControl/22_On_Off_Receiver/22_On_Off_Receiver.ino @@ -0,0 +1,272 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program is a remote control receiver. When a packet is received an 8 bit byte + (SwitchByte) is read and the four outputs (defined in Settings.h) are toggled according to the bits + set in this byte. If the Switch1 byte has bit 0 cleared, then OUTPUT0 is toggled. If the Switch1 byte + has bit 1 cleared, then OUTPUT1 is toggled. If the Switch1 byte has bit 2 cleared, then OUTPUT2 is toggled. + + To prevent false triggering at the receiver the packet contains also contains a 32 bit number called the + TXIdentity which in this example is set to 1234554321. The receiver will only act on, change the state + of the outputs, if the identity set in the receiver matches that of the transmitter. The chance of a + false trigger is fairly remote. + + The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" +#include + +SX128XLT LT; + +uint32_t RXpacketCount; +uint16_t errors; + +uint8_t RXPacketL; //length of received packet +uint8_t RXPacketType; //type of received packet +int16_t PacketRSSI; //RSSI of received packet +int8_t PacketSNR; //signal to noise ratio of received packet + +uint8_t SwitchByte = 0xFF; //this is the transmitted switch values, bit 0 = Switch0 etc + +void loop() +{ + + RXPacketL = LT.receiveSXBuffer(0, 0, WAIT_RX); //returns 0 if packet error of some sort, no timeout + + digitalWrite(LED1, HIGH); //something has happened + + PacketRSSI = LT.readPacketRSSI(); //read the signal strength of the received packet + PacketSNR = LT.readPacketSNR(); //read the signal to noise ratio of the received packet + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + Serial.println(); +} + + +uint8_t packet_is_OK() +{ + //packet has been received, now read from the SX12xx Buffer using the same variable type and + //order as the transmit side used. + uint32_t TXIdentity; + + RXpacketCount++; + Serial.print(RXpacketCount); + Serial.print(F(" Packet Received")); + + LT.startReadSXBuffer(0); //start buffer read at location 0 + RXPacketType = LT.readUint8(); //read in the packet type + TXIdentity = LT.readUint32(); //read in the identity of transmitter + SwitchByte = LT.readUint8(); //read in the Switch values + RXPacketL = LT.endReadSXBuffer(); //finish buffer read + + printpacketDetails(); + + if (RXPacketType != RControl1) + { + Serial.print(F(" Wrong packet type")); + led_Flash(5, 25); //short fast speed flash indicates wrong packet type + return 0; + } + + if (TXIdentity != RXIdentity) + { + Serial.print(F(" Transmitter ")); + Serial.print(TXIdentity); + Serial.print(F(" not recognised")); + led_Flash(5, 25); //short fast speed flash indicates transmitter not recognised + return 0; + } + + if (LT.readRXPacketL() != 6) + { + Serial.print(F(" Wrong Packet Length")); + led_Flash(5, 25); //short fast speed flash indicates transmitter not recognised + return 0; + } + + //if we get to here, then the packet is valid so switch outputs accordingly + + Serial.print(F(",SwitchByte Received ")); + Serial.print(SwitchByte, BIN); //print switch values in binary, if a bit is 0, that switch is active + actionOutputs(SwitchByte); + + return RXPacketL; +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F("RXTimeout")); + } + else + { + errors++; + Serial.print(F("PacketError")); + printpacketDetails(); + Serial.print(F("IRQreg,")); + Serial.print(IRQStatus, HEX); + } +} + + +void printpacketDetails() +{ + Serial.print(F(" RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void actionOutputs(uint8_t switches) +{ + //read the recreived switch byte and toggle outputs as required + + if (!bitRead(switches, 0)) + { + //toggle Output state + digitalWrite(OUTPUT0, !digitalRead(OUTPUT0)); //toggle Output state + } + + if (!bitRead(switches, 1)) + { + digitalWrite(OUTPUT1, !digitalRead(OUTPUT1)); //toggle Output state + } + + if (!bitRead(switches, 2)) + { + digitalWrite(OUTPUT2, !digitalRead(OUTPUT2)); //toggle Output state + } + + if (!bitRead(switches, 3)) + { + digitalWrite(OUTPUT3, !digitalRead(OUTPUT3)); //toggle Output state + } +} + + +void setupOutputs() +{ + //configure the output pins, if a pin is defiend in 'Settings.h' as -1, its not configured, so stays as input + + if (OUTPUT0 >= 0) + { + pinMode(OUTPUT0, OUTPUT); + } + + if (OUTPUT1 >= 0) + { + pinMode(OUTPUT1, OUTPUT); + } + + if (OUTPUT2 >= 0) + { + pinMode(OUTPUT2, OUTPUT); + } + + if (OUTPUT3 >= 0) + { + pinMode(OUTPUT3, OUTPUT); + } + +} + + +void outputCheck(uint8_t number, uint32_t ondelaymS, uint32_t offdelaymS) +{ + uint8_t index; + + Serial.println(F("Toggling outputs")); + + for (index = 1; index <= number; index++) + { + digitalWrite(OUTPUT0, HIGH); + delay(ondelaymS); + digitalWrite(OUTPUT0, LOW); + delay(offdelaymS); + digitalWrite(OUTPUT1, HIGH); + delay(ondelaymS); + digitalWrite(OUTPUT1, LOW); + delay(offdelaymS); + digitalWrite(OUTPUT2, HIGH); + delay(ondelaymS); + digitalWrite(OUTPUT2, LOW); + delay(offdelaymS); + digitalWrite(OUTPUT3, HIGH); + delay(offdelaymS); + digitalWrite(OUTPUT3, LOW); + delay(offdelaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(9600); + + setupOutputs(); + + outputCheck(3, 500, 100); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Receiver ready")); +} diff --git a/examples/SX128x_examples/RemoteControl/22_On_Off_Receiver/Settings.h b/examples/SX128x_examples/RemoteControl/22_On_Off_Receiver/Settings.h new file mode 100644 index 0000000..b9190de --- /dev/null +++ b/examples/SX128x_examples/RemoteControl/22_On_Off_Receiver/Settings.h @@ -0,0 +1,41 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +const int8_t NSS = 10; //select on LoRa device +const int8_t NRESET = 9; //reset on LoRa device +const int8_t RFBUSY = 7; //RF busy on LoRa device +const int8_t DIO1 = 3; //DIO1 on LoRa device, used for RX and TX done +const int8_t LED1 = 8; //On board LED, logic high is on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +const int8_t OUTPUT0 = 2; +const int8_t OUTPUT1 = 4; +const int8_t OUTPUT2 = A3; +const int8_t OUTPUT3 = A2; + +const uint32_t RXIdentity = 1234554321; //define an identity number, the receiver must use the same number +//range is 0 to 4294967296 + + + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +#define TXpower 10 //power for transmissions in dBm diff --git a/examples/SX128x_examples/RemoteControl/35_Remote_Control_Servo_Transmitter/35_Remote_Control_Servo_Transmitter.ino b/examples/SX128x_examples/RemoteControl/35_Remote_Control_Servo_Transmitter/35_Remote_Control_Servo_Transmitter.ino new file mode 100644 index 0000000..bdc681f --- /dev/null +++ b/examples/SX128x_examples/RemoteControl/35_Remote_Control_Servo_Transmitter/35_Remote_Control_Servo_Transmitter.ino @@ -0,0 +1,189 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a remote control transmitter that uses a LoRa link to transmit the positions + from a simple joystick to a remote receiver. The receiver uses the sent joystick positions to adjust the + positions of servos. The postions of the joysticks potentiometers on the transmitter are read with the + analogueRead() function. + + If the joystick has a switch, often made by pressing on the joystick, then this can be used to remote + control an output on the receiver. The switch is read by an interrupt, the interrupt routine sets a flag + byte which is read in loop(). + + The program is intended as a proof of concept demonstration of how to remote control servos, the program + is not designed as a practical remote control device for RC model cars for instance. + + To have the transmitter program print out the values read from the joystick, comment in the line; + + //#define DEBUG + + Which is just above the loop() function. With the DEBUG enabled the transmission rate, the rate at which + the control packets are transmitted will be slowed down. + + To reduce the risk of the receiver picking up LoRa packets from other sources, the packet sent contains a + 'TXidentity' number, valid values are 0 - 65535. The receiver must be setup with the matching identity + number or the received packets will be ignored. + + The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. These settings + are not necessarily optimised for long range. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" +#include + +SX128XLT LT; + +#include "PinChangeInterrupt.h" //get the library here; https://github.com/NicoHood/PinChangeInterrupt + +uint32_t TXpacketCount; +uint8_t TXPacketL; + +uint8_t joystickX1value; //variable to read the value from the analog pin +uint8_t joystickY1value; //variable to read the value from the analog pin + +volatile bool switch1flag = false; + +//#define DEBUG //comment in thie line (remove the two // at the beggining) for debug output + + +void loop() +{ + uint8_t switchByte = 0xFF; + + joystickX1value = (uint8_t) (analogRead(joystickX1) / 4) ; //read the joystick X1 pot, turn 0-1023 into 0 to 255 + joystickY1value = (uint8_t) (analogRead(joystickY1) / 4); //read the joystick Y1 pot + + if (switch1flag) + { + bitClear(switchByte, 1); //if the switch is down clear the bit + digitalWrite(LED1, HIGH); //turn on LED as switch indicator + switch1flag = false; + } + + if (!sendJoystickPacket(joystickX1value, joystickY1value, switchByte)) + { + Serial.print(F("Send Error - IRQreg,")); + Serial.print(LT.readIrqStatus(), HEX); + } +} + + +uint8_t sendJoystickPacket(uint16_t X1value, uint16_t Y1value, uint8_t switches) +{ + //The SX12XX buffer is filled with variables of a known type and in a known sequence. Make sure the + //receiver uses the same variable types and sequence to read variables out of the receive buffer. + + LT.startWriteSXBuffer(0); //start the write packet to buffer process + LT.writeUint8(RControl1); //this is the packet type + LT.writeUint8(TXIdentity); //this value represents the transmitter number + LT.writeUint8(X1value); //this byte contains joystick pot AD X1 value to be sent + LT.writeUint8(Y1value); //this byte contains joystick pot AD Y1 value to be sent + LT.writeUint8(switches); //switches value + LT.endWriteSXBuffer(); //close the packet, thee are 5 bytes to send + + //now transmit the packet, 10 second timeout, and wait for it to complete sending + TXPacketL = LT.transmitSXBuffer(0, PacketLength, 10000, TXpower, WAIT_TX); + +#ifdef DEBUG + Serial.print(TXIdentity); + Serial.print(F(",X1,")); + Serial.print(joystickX1value); + Serial.print(F(",Y1,")); + Serial.print(joystickY1value); + Serial.print(F(",")); + Serial.print(switches, BIN); + Serial.println(); +#endif + + digitalWrite(LED1, LOW); //LED off, may have been on due to switch press + + return TXPacketL; //TXPacketL will be 0 if there was an error sending +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void attachInterrupts() +{ + if (SWITCH1 >= 0) + { + attachPCINT(digitalPinToPCINT(SWITCH1), wake1, FALLING); + switch1flag = false; + } +} + + +void detachInterrupts() +{ + if (SWITCH1 >= 0) + { + detachPCINT(digitalPinToPCINT(SWITCH1)); + } +} + + +void wake1() +{ + switch1flag = true; +} + + +void setupSwitches() +{ + if (SWITCH1 >= 0) + { + pinMode(SWITCH1, INPUT_PULLUP); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + setupSwitches(); + + Serial.begin(115200); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + attachInterrupts(); + + Serial.println(F("35_Remote_Control_Servo_Transmitter ready")); +} diff --git a/examples/SX128x_examples/RemoteControl/35_Remote_Control_Servo_Transmitter/Settings.h b/examples/SX128x_examples/RemoteControl/35_Remote_Control_Servo_Transmitter/Settings.h new file mode 100644 index 0000000..e75a97b --- /dev/null +++ b/examples/SX128x_examples/RemoteControl/35_Remote_Control_Servo_Transmitter/Settings.h @@ -0,0 +1,41 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +const int8_t NSS = 10; //select on LoRa device +const int8_t NRESET = 9; //reset on LoRa device +const int8_t RFBUSY = 7; //RF busy on LoRa device +const int8_t DIO1 = 3; //DIO1 on LoRa device, used for RX and TX done +const int8_t LED1 = 8; //On board LED, logic high is on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +const int8_t joystickX1 = A2; //analog pin for the joystick 1 X pot +const int8_t joystickY1 = A3; //analog pin for the joystick 1 Y pot +const int8_t SWITCH1 = 2; //switch on joystick, set to -1 if not used + +const uint32_t TXIdentity = 123 ; //define a transmitter number, the receiver must use the same number +//range is 0 to 255 + + + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const uint8_t PacketLength = 5; //packet length is fixed +const int8_t TXpower = 10; //LoRa transmit power in dBm diff --git a/examples/SX128x_examples/RemoteControl/36_Remote_Control_Servo_Receiver/36_Remote_Control_Servo_Receiver.ino b/examples/SX128x_examples/RemoteControl/36_Remote_Control_Servo_Receiver/36_Remote_Control_Servo_Receiver.ino new file mode 100644 index 0000000..c533552 --- /dev/null +++ b/examples/SX128x_examples/RemoteControl/36_Remote_Control_Servo_Receiver/36_Remote_Control_Servo_Receiver.ino @@ -0,0 +1,244 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a remote control receiver that uses a LoRa link to control the positions of + servos sent from a remote transmitter. + + If the transmitter joystick has a switch, often made by pressing on the joystick, then this can be used + to remote control an output on the receiver. + + The program is intended as a proof of concept demonstration of how to remote control servos, the program + is not designed as a practical remote control device for RC model cars for instance. + + To have the receiver program print out the joystick values (0-255) read from the received packet, comment + in the line; + + //#define DEBUG + + Which is just above the loop() function. With the DEBUG enabled then there is a possibility that some + transmitted packets will be missed. With the DEBUG line enabled to servos should also sweep to and fro 3 + times at program start-up. + + To reduce the risk of the receiver picking up LoRa packets from other sources, the packet sent contains a + 'TXidentity' number, valid values are 0 - 255. The receiver must be setup with the matching RXIdentity + number in Settings.h or the received packets will be ignored. + + The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. These settings + are not necessarily optimised for long range. + + Serial monitor baud rate is set at 115200. +*******************************************************************************************************/ + +#define programversion "V1.0" + +#include +#include +#include "Settings.h" +#include + +SX128XLT LT; + +#include +Servo ServoX1; //create the servo object +Servo ServoY1; //create the servo object + +uint8_t joystickX1value; //variable to read the value from the analog pin +uint8_t joystickY1value; //variable to read the value from the analog pin +uint8_t RXPacketL; //length of received packet +uint8_t RXPacketType; //type of received packet + +//#define DEBUG + + +void loop() +{ + uint16_t IRQStatus; + + RXPacketL = LT.receiveSXBuffer(0, 0, WAIT_RX); //returns 0 if packet error of some sort + + while (!digitalRead(DIO1)); //wait for DIO1 to go high + + if (LT.readIrqStatus() == (IRQ_RX_DONE + IRQ_HEADER_VALID + IRQ_PREAMBLE_DETECTED) ) + { + packet_is_OK(); + } + else + { + packet_is_Error(); + } + +} + + +uint8_t packet_is_OK() +{ + //packet has been received, now read from the SX12xx Buffer using the same variable type and + //order as the transmit side used. + uint8_t TXIdentity; + uint16_t pulseX1, pulseY1; + uint8_t switchByte = 0xFF; //this is the transmitted switch values, bit 0 = Switch0 etc + + LT.startReadSXBuffer(0); //start buffer read at location 0 + RXPacketType = LT.readUint8(); //read in the packet type + TXIdentity = LT.readUint8(); //read in the transmitter number + joystickX1value = LT.readUint8(); //this byte contains joystick pot AD X1 value sent + joystickY1value = LT.readUint8(); //this byte contains joystick pot AD Y1 value sent + switchByte = LT.readUint8(); //read in the Switch values + RXPacketL = LT.endReadSXBuffer(); //end buffer read + +#ifdef DEBUG + Serial.print(TXIdentity); + Serial.print(F(",X1,")); + Serial.print(joystickX1value); + Serial.print(F(",Y1,")); + Serial.print(joystickY1value); + Serial.print(F(",")); + Serial.print(switchByte, BIN); + Serial.println(); +#endif + + + if (RXPacketType != RControl1) + { + Serial.print(F("Packet type ")); + Serial.println(RXPacketType); + led_Flash(5, 25); //short fast speed flash indicates wrong packet type + return 0; + } + + + if (TXIdentity != RXIdentity) + { + Serial.print(F("TX")); + Serial.print(TXIdentity); + Serial.println(F("?")); + return 0; + } + + //actionServos + pulseX1 = map(joystickX1value, 0, 255, 1000, 2000); //scale the numbers from the joystick + ServoX1.writeMicroseconds(pulseX1); + pulseY1 = map(joystickY1value, 0, 255, 1000, 2000); //scale the numbers from the joystick + ServoY1.writeMicroseconds(pulseY1); //move the servo to position + + //actionOutputs + if (!bitRead(switchByte, 1)) + { + digitalWrite(OUTPUT1, !digitalRead(OUTPUT1)); //Toggle Output state + } + + return RXPacketL; +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + int16_t PacketRSSI; + IRQStatus = LT.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F("RXTimeout")); + } + else + { + PacketRSSI = LT.readPacketRSSI(); //read the signal strength of the received packet + Serial.print(F("Err,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm")); + } + Serial.println(); +} + + +void setupOutputs() +{ + //configure the output pins, if a pin is defiend in 'Settings.h' as -1, its not configured, so stays as input + if (OUTPUT1 >= 0) + { + pinMode(OUTPUT1, OUTPUT); + } +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void sweepTest(uint8_t num) +{ + uint16_t index1, index2; + for (index1 = 1; index1 <= num; index1++) + { + for (index2 = 900; index2 <= 2100; index2++) + { + ServoX1.writeMicroseconds(index2); + ServoY1.writeMicroseconds(index2); + } + + delay(1000); + + for (index2 = 2100; index2 >= 900; index2--) + { + ServoX1.writeMicroseconds(index2); + ServoY1.writeMicroseconds(index2); + } + + delay(1000); + } +} + + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + setupOutputs(); + + Serial.begin(115200); + + ServoX1.attach(pinservoX1); //connect pin pinservoX1 to ServoX1 object + ServoY1.attach(pinservoY1); //connect pin pinservoY1 to ServoY1 object + +#ifdef DEBUG + Serial.println(F("Servo sweep test")); + sweepTest(3); +#endif + + SPI.begin(); + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("36_Remote_Control_Servo_Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/RemoteControl/36_Remote_Control_Servo_Receiver/Settings.h b/examples/SX128x_examples/RemoteControl/36_Remote_Control_Servo_Receiver/Settings.h new file mode 100644 index 0000000..1b393f8 --- /dev/null +++ b/examples/SX128x_examples/RemoteControl/36_Remote_Control_Servo_Receiver/Settings.h @@ -0,0 +1,39 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//******* Setup hardware pin definitions here ! *************** + + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +const int8_t NSS = 10; //select on LoRa device +const int8_t NRESET = 9; //reset on LoRa device +const int8_t RFBUSY = 7; //RF busy on LoRa device +const int8_t DIO1 = 3; //DIO1 on LoRa device, used for RX and TX done +const int8_t LED1 = 8; //On board LED, logic high is on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +const int8_t pinservoX1 = 2; //pin for controlling servo X1 +const int8_t pinservoY1 = 4; //pin for controlling servo Y1 +const int8_t OUTPUT1 = 8; //this output toggles when joystick switch is pressed on receiver + +const uint16_t RXIdentity = 123; //define a receiver number, the transmitter must use the same number +//range is 0 to 255 + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const uint8_t PacketLength = 5; //packet length is fixed diff --git a/examples/SX128x_examples/Sensor/17_Sensor_Transmitter/17_Sensor_Transmitter.ino b/examples/SX128x_examples/Sensor/17_Sensor_Transmitter/17_Sensor_Transmitter.ino new file mode 100644 index 0000000..1bfe7ac --- /dev/null +++ b/examples/SX128x_examples/Sensor/17_Sensor_Transmitter/17_Sensor_Transmitter.ino @@ -0,0 +1,314 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 20/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program transmits a LoRa packet without using a processor buffer, the LoRa + devices internal buffer is filled directly with variables. The program is for Atmel Arduinos. + + The sensor used is a BME280. The pressure, humidity, and temperature are read and transmitted. There + is also a 16bit value of battery mV (simulated) and and a 8 bit status value at the packet end. + + Although the LoRa packet transmitted and received has its own internal CRC error checking, you could + still receive packets of the same length from another source. If this valid packet were to be used + to recover the sensor values, you could be reading rubbish. To reduce the risk of this, when the packet + is transmitted the CRC value of the actual sensor data is calculated and sent out with the packet. + This CRC value is read by the receiver and used to check that the received CRC matches the supposed + sensor data in the packet. As an additional check there is some addressing information at the beginning + of the packet which is also checked for validity. Thus we can be relatively confident when reading the + received packet that its genuine and from this transmitter. The packet is built and sent in the + sendSensorPacket() function, there is a 'highlighted section' where the actual sensor data is added to + the packet. + + Between readings the LoRa device, BME280 sensor, and Atmel microcontroller are put to sleep in units of + 8 seconds using the Atmel processor internal watchdog. + + The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. + + There is also an option of using a logic pin to turn the resistor divider used to read battery voltage on + and off. This reduces current used in sleep mode. To use the feature set the define for pin BATVREADON + in 'Settings.h' to the pin used. If not using the feature set the pin number to -1. + + The Atmel watchdog timer is a viable option for a very low current sensor node. A 'bare bones' ATmega328P + with regulator and LoRa device has a sleep current of 6.6uA, add the LoRa devices and BME280 sensor + module and the average sleep current only rises to 6.8uA. + + One of these transmitter programs is running on a long term test with a 150mAh battery, to see how long + the battery actually lasts. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" +#include + +#include //watchdog timer library for Atmel processors, integral to Arduino IDE +#include //get the library here; https://github.com/rocketscream/Low-Power + +SX128XLT LT; + +#include //get library here; https://github.com/Seeed-Studio/Grove_BME280 +BME280 bme280; //create an instance of the BME280 senosor +#include + +uint32_t TXpacketCount; +uint8_t TXPacketL; + +float temperature; //the BME280 temperature value +float pressure; //the BME280 pressure value +uint16_t humidity; //the BME280 humididty value +uint16_t voltage; //the battery voltage value +uint8_t statusbyte; //a status byte, not currently used +uint16_t CRCvalue; //the CRC value of the packet data up to this point +uint8_t packetlength; //the packet length that was sent, checked against length received + + +void loop() +{ + TXpacketCount++; + Serial.print(TXpacketCount); //print the numbers of sends + Serial.print(F(" Sending > ")); + + readSensors(); //read the sensor values + printSensorValues(); //print the sensor values + + if (sendSensorPacket()) + { + Serial.println(F("SentOK")); + } + else + { + Serial.print(F("Send Error - IRQreg,")); + Serial.println(LT.readIrqStatus(), HEX); + } + + Serial.print(F("Sleeping zzzz")); + Serial.flush(); //make sure all serial output has gone + + //now put the sensor, LoRa device and processor to sleep + sleepBME280(); //sleep the BME280 + LT.setSleep(CONFIGURATION_RETENTION); //sleep LoRa device, keeping register settings in sleep. + sleep8seconds(sleeps); //sleep Atmel processor in units of approx 8 seconds + + //wait a bit ................ + Serial.println(F(" - Awake !!")); //the processor has woken up + Serial.println(); + + LT.wake(); + normalBME280(); //BME280 sensor to normal mode +} + + +uint8_t sendSensorPacket() +{ + //The SX12XX buffer is filled with variables of a known type and in a known sequence. Make sure the + //receiver uses the same variable types and sequence to read variables out of the receive buffer. + uint8_t len; + + LT.startWriteSXBuffer(0); //start the write packet to buffer process + + LT.writeUint8(Sensor1); //this byte defines the packet type + LT.writeUint8('B'); //this byte identifies the destination node of the packet + LT.writeUint8(1); //this byte identifies the source node of the packet + + /************************************************************************ + Highlighted section - this is where the actual sensor data is added to the packet + ************************************************************************/ + LT.writeFloat(temperature); //add the BME280 temperature + LT.writeFloat(pressure); //add the BME280 pressure + LT.writeUint16(humidity); //add the BME280 humididty + LT.writeUint16(voltage); //add the battery voltage + LT.writeUint8(statusbyte); //add the status byte + /************************************************************************/ + + len = LT.endWriteSXBuffer(); //close the packet, get the length of data to be sent + + addPacketErrorCheck(len); //add the additional CRC error checking to the packet end + + //now transmit the packet, set a timeout of 5000mS, wait for it to complete sending + digitalWrite(LED1, HIGH); //turn on LED as an indicator + TXPacketL = LT.transmitSXBuffer(0, (len + 2), 5000, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); //turn off indicator LED + + return TXPacketL; //TXPacketL will be 0 if there was an error sending +} + + +void addPacketErrorCheck(uint8_t len) +{ + //calculate the CRC of packet sensor data + CRCvalue = LT.CRCCCITTSX(3, (len - 1), 0xFFFF); + + Serial.print(F("Calculated CRC value ")); + Serial.println(CRCvalue, HEX); + + LT.startWriteSXBuffer(len); //start the write packet again at location of CRC, past end of sensor data + LT.writeUint16(CRCvalue); //add the actual CRC value + LT.endWriteSXBuffer(); //close the packet +} + + +void readSensors() +{ + //read the sensor values into the global variables + temperature = bme280.getTemperature(); + pressure = bme280.getPressure(); + humidity = bme280.getHumidity(); + + if (BATVREADON >= 0) + { + voltage = readBatteryVoltage(); //read resistor divider across battery + } + else + { + voltage = 9999; //set a default value + } + statusbyte = 0x55; //manually set this for now, its a test +} + + +void printSensorValues() +{ + Serial.print(F("Temperature,")); + Serial.print(temperature, 1); + Serial.print(F("c,Pressure,")); + Serial.print(pressure, 0); + Serial.print(F("Pa,Humidity,")); + Serial.print(humidity); + Serial.print(F("%,Voltage,")); + Serial.print(voltage); + Serial.print(F("mV,Status,")); + Serial.print(statusbyte, HEX); + Serial.print(F(" ")); + Serial.flush(); +} + + +void sleep8seconds(uint32_t sleeps) +{ + //uses the lowpower library + uint32_t index; + + for (index = 1; index <= sleeps; index++) + { + //sleep 8 seconds + LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); + } +} + + +void sleepBME280() +{ + //write this register value to BME280 to put it to sleep + writeBME280reg(BME280_REGISTER_CONTROL, B01111100); +} + + +void normalBME280() +{ + //write this register value to BME280 to put it to read mode + writeBME280reg(BME280_REGISTER_CONTROL, B01111111); +} + + +void writeBME280reg(byte reg, byte regvalue) +{ + //write a register value to the BME280 + Wire.beginTransmission((uint8_t) BME280_ADDRESS); + Wire.write((uint8_t)reg); + Wire.write((uint8_t)regvalue); + Wire.endTransmission(); +} + + +uint16_t readBatteryVoltage() +{ + //relies on 1V1 internal reference and 91K & 11K resistor divider + //returns supply in mV @ 10mV per AD bit read + uint16_t temp; + uint16_t volts = 0; + byte index; + + if (BATVREADON >= 0) + { + digitalWrite(BATVREADON, HIGH); //turn on MOSFET connecting resitor divider in circuit + } + + analogReference(INTERNAL1V1); + temp = analogRead(BATTERYAD); + + for (index = 0; index <= 4; index++) //sample AD 5 times + { + temp = analogRead(BATTERYAD); + volts = volts + temp; + } + volts = ((volts / 5) * ADMultiplier) + DIODEMV; + + if (BATVREADON >= 0) + { + digitalWrite(BATVREADON, LOW); //turn off MOSFET connecting resitor divider in circuit + } + + return volts; +} + + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + if (BATVREADON >= 0) + { + pinMode(BATVREADON, OUTPUT); + } + + Serial.begin(9600); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates LoRa device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + if (!bme280.init()) + { + Serial.println("BME280 Device error!"); + led_Flash(100, 15); //long very fast speed flash indicates BME280 device error + } + + Serial.println(F("Transmitter ready")); + Serial.println(); + + readSensors(); //do an initial sensor read +} diff --git a/examples/SX128x_examples/Sensor/17_Sensor_Transmitter/Settings.h b/examples/SX128x_examples/Sensor/17_Sensor_Transmitter/Settings.h new file mode 100644 index 0000000..dd6241e --- /dev/null +++ b/examples/SX128x_examples/Sensor/17_Sensor_Transmitter/Settings.h @@ -0,0 +1,38 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define BATVREADON 8 //when high turns on the resistor divider to measure voltage, -1 if not used +#define BATTERYAD A7 //Resitor divider for battery connected here, -1 if not used +#define ADMultiplier 10.00 //adjustment to convert AD value read into mV of battery voltage +#define DIODEMV 98 //mV voltage drop accross diode @ low idle current + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm + +#define BME280_ADDRESS 0x76 //I2C bus address of BME280 +#define BME280_REGISTER_CONTROL 0xF4 //BME280 register number for power control + +const uint8_t sleeps = 2; //number of 8 second sleeps, gap between transmissions diff --git a/examples/SX128x_examples/Sensor/18_Sensor_Receiver/18_Sensor_Receiver.ino b/examples/SX128x_examples/Sensor/18_Sensor_Receiver/18_Sensor_Receiver.ino new file mode 100644 index 0000000..a27b227 --- /dev/null +++ b/examples/SX128x_examples/Sensor/18_Sensor_Receiver/18_Sensor_Receiver.ino @@ -0,0 +1,357 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program receives a LoRa packet without using a processor buffer, the LoRa devices + internal buffer is read direct for the received sensor data. + + The sensor used in the matching '17_Sensor_Transmiter' program is a BME280 and the pressure, humidity, + and temperature are being and received. There is also a 16bit value of battery mV and and a 8 bit status + value at the end of the packet. + + When the program starts, the LoRa device is setup to set the DIO1 pin high when a packet is received. When + a packet is received, its printed and assuming the packet is validated, the sensor results are printed to + the serial monitor and screen. + + For the sensor data to be accepted as valid the folowing need to match; + + The 16bit CRC on the received sensor data must match the CRC value transmitted with the packet. + The packet must start with a byte that matches the packet type sent, 'Sensor1' + The RXdestination byte in the packet must match this node ID of this receiver node, defined by 'This_Node' + + In total thats 16 + 8 + 8 = 32bits of checking, so a 1:4294967296 chance (approx) that an invalid + packet is acted on and erroneous values displayed. + + The pin definitions, LoRa frequency and LoRa modem settings are in the Settings.h file. + + With a standard Arduino Pro Mini and SSD1306 display the current consumption was 20.25mA with the + display and 16.6mA without the display. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" +#include + +SX128XLT LT; + +#include //get library here > https://github.com/olikraus/u8g2 +U8X8_SSD1306_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //use this line for standard 0.96" SSD1306 +//U8X8_SH1106_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //use this line for 1.3" OLED often sold as 1.3" SSD1306 + +uint32_t RXpacketCount; //count of all packets received +uint32_t ValidPackets; //count of packets received with valid data +uint32_t RXpacketErrors; //count of all packets with errors received +bool packetisgood; + +uint8_t RXPacketL; //length of received packet +int16_t PacketRSSI; //RSSI of received packet +int8_t PacketSNR; //signal to noise ratio of received packet + +uint8_t RXPacketType; +uint8_t RXDestination; +uint8_t RXSource; +float temperature; //the BME280 temperature value +float pressure; //the BME280 pressure value +uint16_t humidity; //the BME280 humididty value +uint16_t voltage; //the battery voltage value +uint8_t statusbyte; //a status byte, not currently used +uint16_t TXCRCvalue; //the CRC value of the packet data as transmitted + + +void loop() +{ + RXPacketL = LT.receiveSXBuffer(0, 0, WAIT_RX); //returns 0 if packet error of some sort, no timeout set + + digitalWrite(LED1, HIGH); //something has happened + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_Received_OK(); //its a valid packet LoRa wise, but it might not be a packet we want + } + + digitalWrite(LED1, LOW); + Serial.println(); +} + + +void packet_Received_OK() +{ + //a LoRa packet has been received, which has passed the internal LoRa checks, including CRC, but it could be from + //an unknown source, so we need to check that its actually a sensor packet we are expecting, and has valid sensor data + + uint8_t len; + uint8_t contenterrors; //keep a count of errors found in packet + + RXpacketCount++; + Serial.print(RXpacketCount); + Serial.print(F(",PacketsReceived,")); + + LT.startReadSXBuffer(0); + RXPacketType = LT.readUint8(); + RXDestination = LT.readUint8(); + RXSource = LT.readUint8(); + + /************************************************************************ + Highlighted section - this is where the actual sensor data is read from + the packet + ************************************************************************/ + temperature = LT.readFloat(); //the BME280 temperature value + pressure = LT.readFloat(); //the BME280 pressure value + humidity = LT.readUint16(); //the BME280 humididty value + voltage = LT.readUint16(); //the battery voltage value + statusbyte = LT.readUint8(); //a status byte, not currently used + /************************************************************************/ + + len = LT.endReadSXBuffer(); + + printreceptionDetails(); //print details of reception, RSSI etc + Serial.println(); + + contenterrors = checkPacketValid(len); //pass length of packet to check routine + + if (contenterrors == 0) + { + Serial.println(F(" Packet is good")); + ValidPackets++; + printSensorValues(); //print the sensor values + Serial.println(); + printPacketCounts(); //print count of valid packets and errors + displayscreen1(); + Serial.println(); + } + else + { + Serial.println(F(" Packet is not valid")); + RXpacketErrors++; + disp.clearLine(7); + disp.setCursor(0, 7); + disp.print(F("Errors ")); + disp.print(RXpacketErrors); + } +} + + +uint8_t checkPacketValid(uint8_t len) +{ + //this function checks if the packet is valid and will be displayed + + uint8_t errors = 0; + + if (RXPacketType != Sensor1) //is it a Sensor1 type packet + { + errors++; + } + + if (RXDestination != This_Node) //was the packet sent to this receiver node ? + { + errors++; + } + + if (!checkCRCvalue(len)) //is the sent CRC value of sensor data valid ? + { + errors++; + } + + Serial.println(); + Serial.print(F("Error Check Count = ")); + Serial.print(errors); + return errors; +} + + +bool checkCRCvalue(uint8_t len) +{ + uint16_t CRCSensorData; + + Serial.print(F("len = ")); + Serial.println(len); + + CRCSensorData = LT.CRCCCITTSX(3, (len - 1), 0xFFFF); //calculate the CRC of packet sensor data + + Serial.print(F("(CRC of Received sensor data ")); + Serial.print(CRCSensorData, HEX); + Serial.print(F(")" )); + + TXCRCvalue = ((LT.getByteSXBuffer(len + 1) << 8) + (LT.getByteSXBuffer(len))); + + Serial.print(F("(CRC transmitted ")); + Serial.print(TXCRCvalue, HEX); + Serial.print(F(")" )); + + if (TXCRCvalue != CRCSensorData) + { + Serial.print(F(" Sensor Data Not Valid")); + return false; + } + else + { + Serial.print(F(" Sensor Data is Valid")); + return true; + } + +} + + +void printSensorValues() +{ + Serial.print(F("Temp,")); + Serial.print(temperature, 1); + Serial.print(F("c,Press,")); + Serial.print(pressure, 0); + Serial.print(F("Pa,Humidity,")); + Serial.print(humidity); + Serial.print(F("%,Voltage,")); + Serial.print(voltage); + Serial.print(F("mV,Status,")); + Serial.print(statusbyte, HEX); + Serial.print(F(",CRC,")); + Serial.print(TXCRCvalue, HEX); + Serial.flush(); +} + + +void printreceptionDetails() +{ + Serial.print(F("RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); +} + + +void printPacketCounts() +{ + Serial.print(F("ValidPackets,")); + Serial.print(ValidPackets); + Serial.print(F(",Errors,")); + Serial.print(RXpacketErrors); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + RXpacketErrors++; + IRQStatus = LT.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F("RXTimeout ")); + } + else + { + Serial.print(F("PacketError ")); + printreceptionDetails(); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); + Serial.println(); + disp.clearLine(7); + disp.setCursor(0, 7); + disp.print(F("Errors ")); + disp.print(RXpacketErrors); + } +} + + +void displayscreen1() +{ + //show sensor data on display + disp.clearLine(0); + disp.setCursor(0, 0); + disp.print(F("Sensor ")); + disp.print(RXSource); + disp.clearLine(1); + disp.setCursor(0, 1); + disp.print(temperature, 1); + disp.print(F("c")); + disp.clearLine(2); + disp.setCursor(0, 2); + disp.print(pressure, 0); + disp.print(F("Pa")); + disp.clearLine(3); + disp.setCursor(0, 3); + disp.print(humidity); + disp.print(F("%")); + disp.clearLine(4); + disp.setCursor(0, 4); + disp.print(voltage); + disp.print(F("mV")); + disp.clearLine(6); + disp.setCursor(0, 6); + disp.print(F("ValidPkts ")); + disp.print(ValidPackets); + disp.setCursor(0, 7); + disp.print(F("Errors ")); + disp.print(RXpacketErrors); +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(9600); + + disp.begin(); + disp.setFont(u8x8_font_chroma48medium8_r); + + disp.clear(); + disp.setCursor(0, 0); + disp.print(F("Check LoRa")); + disp.setCursor(0, 1); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + disp.print(F("LoRa OK")); + led_Flash(2, 125); + } + else + { + disp.print(F("Device error")); + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Sensor/18_Sensor_Receiver/Settings.h b/examples/SX128x_examples/Sensor/18_Sensor_Receiver/Settings.h new file mode 100644 index 0000000..fe27be6 --- /dev/null +++ b/examples/SX128x_examples/Sensor/18_Sensor_Receiver/Settings.h @@ -0,0 +1,38 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define BATVREADON 8 //when high turns on the resistor divider to measure voltage, -1 if not used +#define BATTERYAD A7 //Resistor divider for battery connected here, -1 if not used +#define ADMultiplier 10.00 //adjustment to convert AD value read into mV of battery voltage +#define DIODEMV 98 //mV voltage drop accross diode @ low idle current + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//*************** Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm + +#define packet_delay 1000 //mS delay between packets +#define This_Node 'B' //this is the node that the remote sensors send data to diff --git a/examples/SX128x_examples/SerialBridge/105_LoRa_Serial_Bridge_Transmitter/105_LoRa_Serial_Bridge_Transmitter.ino b/examples/SX128x_examples/SerialBridge/105_LoRa_Serial_Bridge_Transmitter/105_LoRa_Serial_Bridge_Transmitter.ino new file mode 100644 index 0000000..c32dcbc --- /dev/null +++ b/examples/SX128x_examples/SerialBridge/105_LoRa_Serial_Bridge_Transmitter/105_LoRa_Serial_Bridge_Transmitter.ino @@ -0,0 +1,189 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 28/12/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This the transmitter part of a Serial bridge. This tranmitter receives data on + the defined serial port and puts that serial data into a LoRa packet which is then transmitted. A + matching reciever picks up the packet and displays it on the remote Arduinos serial port. + + The purpose of the bridge is to allow the serial output of a device, anothor Arduino or sensor for + instance, to be remotely monitored, without the need for a long serial cable. + + Serial monitor baud rate should be set at 9600. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LoRa; //create a library class instance called LoRa + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //indicator LED +#define LORA_DEVICE DEVICE_SX1280 //we need to define the LoRa device we are using +#define TXpower 10 //LoRa transmit power in dBm + +uint8_t TXPacketL; //length of transmitted packet +uint8_t RXPacketL; //length of received acknowledge +uint16_t PayloadCRC; +uint16_t MessageID; //MessageID identifies each message, 2 bytes at front of packet +uint8_t receivedBytes; //count of serial bytes received +uint8_t Attempts; //number of times to send packet waiting for acknowledge +uint32_t startuS; //used for timeout for serial input +const uint8_t MaxMessageSize = 251; //max size of array to send with received serial +uint8_t Message[MaxMessageSize]; //array for received serial data + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint16_t NetworkID = 0x3210; //a unique identifier to go out with packet = 0x3210; + +uint32_t SerialTimeoutuS; //Timeout in uS before assuming message to send is complete +const uint32_t TimeoutCharacters = 10; //number of characters at specified baud rate that are a serial timeout +const uint8_t MaxAttempts = 4; //Maximum attempts to send message and receive acknowledge +const uint32_t ACKtimeoutmS = 50; //Acknowledge timeout in mS +const uint32_t TXtimeoutmS = 1000; //transmit timeout in mS. + +#define SerialInput Serial //assign serial port for reading data +#define DebugSerial Serial //assign serial port for debug output +#define SerialInputBaud 9600 //baud rate for the serial + +//#define DEBUG //enable define to see debug information +#define SENDREADY //enable define to see Ready control message at start + + +void loop() +{ +#ifdef DEBUG + DebugSerial.println(F("> ")); +#endif + + Message[0] = lowByte(MessageID); + Message[1] = highByte(MessageID); + MessageID++; //ready for next message + + receivedBytes = 2; + + while (SerialInput.available() == 0); //wait for serial data to be available + + digitalWrite(LED1, HIGH); + startuS = micros(); + + while (((uint32_t) (micros() - startuS) < SerialTimeoutuS)) + { + if (SerialInput.available() > 0) + { + if (receivedBytes >= MaxMessageSize) + { + break; + } + Message[receivedBytes] = SerialInput.read(); + startuS = micros(); //restart timeout + receivedBytes++; + } + }; + + //the only exits from the serial collection loop above are if there is a timeout, or the maximum + //message size is reached. + + Attempts = 0; + + do + { +#ifdef DEBUG + uint8_t index; + DebugSerial.print(receivedBytes); + DebugSerial.print(F(" ")); + DebugSerial.print(MessageID); + DebugSerial.print(F(" > ")); //flag on monitor for transmitting + + for (index = 2; index < receivedBytes; index++) + { + DebugSerial.write(Message[index]); + } + DebugSerial.println(); +#endif + + TXPacketL = LoRa.transmitReliable(Message, receivedBytes, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + PayloadCRC = LoRa.readUint16SXBuffer(TXPacketL - 2); //need the payload CRC to check for valid ACK + RXPacketL = LoRa.waitReliableACK(NetworkID, PayloadCRC, ACKtimeoutmS); + Attempts++; + } + while ((RXPacketL == 0) && (Attempts <= MaxAttempts)); + +#ifdef DEBUG + if (RXPacketL == 0) + { + Serial.println(F("NA>")); + } +#endif +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + + DebugSerial.begin(9600); + SerialInput.begin(SerialInputBaud); + +#ifdef DEBUG + DebugSerial.println(); + Serial.println(F(__FILE__)); + DebugSerial.println(); +#endif + + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { +#ifdef DEBUG + DebugSerial.println(F("LoRa Device found")); +#endif + } + else + { + DebugSerial.println(F("No LoRa device responding")); + while (1); + } + + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + +#ifdef SENDREADY + //Send a ready packet so remorte can check its working, send control message 0x0001 + Message[0] = 1; + Message[1] = 0; + Message[2] = 'R'; + TXPacketL = LoRa.transmitReliable(Message, 3, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + delay(1000); +#endif + + //now calculate timeout in microseconds based on baud rate and number of characters, assuming a 11bit byte + + SerialTimeoutuS = ((1000000 / SerialInputBaud) * 11) * TimeoutCharacters; + +#ifdef DEBUG + DebugSerial.print(F("SerialTimeoutuS ")); + DebugSerial.println(SerialTimeoutuS); + DebugSerial.println(F("Clear serial buffer")); +#endif + + while (SerialInput.available() > 0) //clear serial input + { + SerialInput.read(); + } + +#ifdef DEBUG + DebugSerial.println(F("Waiting start of serial input")); +#endif + + MessageID = 257; //first message to send is 0x0101 +} diff --git a/examples/SX128x_examples/SerialBridge/106_LoRa_Serial_Bridge_Receiver/106_LoRa_Serial_Bridge_Receiver.ino b/examples/SX128x_examples/SerialBridge/106_LoRa_Serial_Bridge_Receiver/106_LoRa_Serial_Bridge_Receiver.ino new file mode 100644 index 0000000..8d02949 --- /dev/null +++ b/examples/SX128x_examples/SerialBridge/106_LoRa_Serial_Bridge_Receiver/106_LoRa_Serial_Bridge_Receiver.ino @@ -0,0 +1,218 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 28/12/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This a the receiver part of a Serial bridge. The tranmitter receives data on + the defined serial port and puts that serial data into a LoRa packet which is then transmitted. This + matching reciever picks up the packet and displays it on the receivers serial port. + + The purpose of the bridge is to allow the serial output of a device, anothor Arduino or sensor for + instance, to be remotely monitored, without the need for a long serial cable. + + Serial monitor baud rate should be set at 9600. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LoRa; //create a library class instance called LT + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //this is output 1 +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using +#define TXpower 10 //dBm power to use for ACK + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint16_t RXPayloadCRC; //CRC of payload included in received packet +uint16_t MessageID; //MessageID identifies each message, 2 bytes at front of packet +uint16_t LastMessageID; //keep track of last received message, display messages start at 0x0100 + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint16_t NetworkID = 0x3210; //Message NetworkID identifies each message, automatically appended to packet + +const uint32_t ACKdelay1 = 10; //delay in mS before sending acknowledge +const uint32_t ACKdelay2 = 20; //delay in mS before sending acknowledge for message already received +const uint32_t RXtimeout = 60000 ; //receive timeout in mS. set to 0 for no timeout +const uint32_t TXtimeout = 1000; //transmit timeout in mS. +const uint8_t MaxMessageSize = 251 ; //maximum size of serial maeesage to send, 4 bytes needed for NetworkID and payload CRC +uint8_t Message[MaxMessageSize]; //array for receiving message + +#define SerialOutput Serial //assign serial port for outputing data +#define DebugSerial Serial //assign serial port for outputing debug data + +//#define DEBUG //enable this define to see debug information + + +void loop() +{ + +#ifdef DEBUG + DebugSerial.print(F("> ")); +#endif + + do + { + RXPacketL = LoRa.receiveReliable(Message, MaxMessageSize, NetworkID, RXtimeout, WAIT_RX); + + MessageID = Message[0] + (Message[1] * 256); //message number is first two bytes of Message array + + if (RXPacketL == 0) //check for a valid packet + { +#ifdef DEBUG + DebugSerial.print(MessageID); + DebugSerial.print(F("E>")); + packet_is_Error(); +#endif + break; + } + + + //if here there is a valid reliable packet + + if (MessageID > LastMessageID) //new message should be greater than last message + { + processMessage(); + delay(ACKdelay1); + sendACK(); + break; + } + + if (MessageID <= 256) //is it a control message + { + processControlMessage(); + break; + } + + if (LastMessageID == MessageID) //have we had this message before, remember TX may miss the sent ACK ? + { + delay(ACKdelay2); + sendACK(); //dont process message, but send ACK again + break; //and go back to receive + } + + if (MessageID == 257) //this appears to be a first message, transmitter may have restarted + { + LastMessageID = 257; //reset last message number + processMessage(); + delay(ACKdelay1); + sendACK(); //dont process message, but send ACK again + break; //and go back to receive + } + + break; //and go back to receive + } + while (1); +} + + +void processMessage() +{ + uint8_t index, endpayload; + + endpayload = RXPacketL - 4; + + for (index = 2; index < endpayload; index++) + { + SerialOutput.write(Message[index]); + } + +#ifdef DEBUG + DebugSerial.print(RXPacketL - 6); //print number of characters in message + DebugSerial.print(F(" ")); + DebugSerial.print(MessageID); //print message ID + DebugSerial.print(F(" > ")); + DebugSerial.println(); //so each message is on new line +#endif + + LastMessageID = MessageID; +} + + +void processControlMessage() +{ + if (MessageID == 1) + { + DebugSerial.println(F("Ready")); + sendACK(); + } +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LoRa.readIrqStatus(); //read the LoRa device IRQ status register + DebugSerial.print(F("RXPacketL,")); + DebugSerial.print(RXPacketL); + DebugSerial.print(F(",IRQStatus,0x")); + DebugSerial.println(IRQStatus, HEX); + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + DebugSerial.println(F("RXTimeout")); + } +} + + +void sendACK() +{ + RXPayloadCRC = LoRa.readUint16SXBuffer(RXPacketL - 2); //get sent payload CRC, needed for ACK + digitalWrite(LED1, HIGH); + LoRa.writeUint16SXBuffer(0, MessageID); + LoRa.writeUint16SXBuffer(2, NetworkID); + LoRa.writeUint16SXBuffer(4, RXPayloadCRC); + LoRa.transmitSXBuffer(0, 6, TXtimeout, TXpower, WAIT_TX); +#ifdef DEBUG + DebugSerial.print(MessageID); + DebugSerial.println(F(" A >")); +#endif + digitalWrite(LED1, LOW); +} + + +void setup() +{ + SerialOutput.begin(9600); + DebugSerial.begin(9600); + +#ifdef DEBUG + DebugSerial.println(); + DebugSerial.println(F(__FILE__)); + DebugSerial.println(); +#endif + + pinMode(LED1, OUTPUT); //set high for on + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { +#ifdef DEBUG + DebugSerial.println(F("LoRa Device found")); + delay(1000); +#endif + } + else + { + DebugSerial.println(F("No LoRa device responding")); + while (1); + } + + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + +#ifdef DEBUG + DebugSerial.println(F("Receiver ready")); +#endif + + LastMessageID = 0x0100; //start value +} diff --git a/examples/SX128x_examples/SerialBridge/107_LoRa_Serial_Bridge_GPS_Receiver/107_LoRa_Serial_Bridge_GPS_Receiver.ino b/examples/SX128x_examples/SerialBridge/107_LoRa_Serial_Bridge_GPS_Receiver/107_LoRa_Serial_Bridge_GPS_Receiver.ino new file mode 100644 index 0000000..50c79ac --- /dev/null +++ b/examples/SX128x_examples/SerialBridge/107_LoRa_Serial_Bridge_GPS_Receiver/107_LoRa_Serial_Bridge_GPS_Receiver.ino @@ -0,0 +1,256 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 28/12/21 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is the receiver part of a Serial bridge being used to monitor a remote GPS. + The tranmitter receives data from the GPS on the defined serial port and puts that serial data into a + LoRa packet which is then transmitted. This matching reciever picks up the packet and feeds it into the + TinyGPSplus library and which decodes the data into a position fix, which is printed to the serial + monitor. The number of NMEA sentences passing and failing on checksum is also printed to serial monitor. + + Serial monitor baud rate should be set at 115200. +*******************************************************************************************************/ + +#include //the LoRa device is SPI based so load the SPI library +#include //include the appropriate library + +SX128XLT LoRa; //create a library class instance called LoRa + +#define NSS 10 //select pin on LoRa device +#define NRESET 9 //reset pin on LoRa device +#define RFBUSY 7 //busy pin on LoRa device +#define DIO1 3 //DIO1 pin on LoRa device, used for sensing RX and TX done +#define LED1 8 //this is output 1 +#define LORA_DEVICE DEVICE_SX1280 //we need to define the LoRa device we are using +#define TXpower 10 //dBm power to use for ACK + +uint8_t RXPacketL; //stores length of packet received +uint8_t RXPayloadL; //stores length of payload received +uint16_t RXPayloadCRC; //CRC of payload included in received packet +uint16_t MessageID; //MessageID identifies each message, 2 bytes at front of packet +uint16_t LastMessageID; //keep track of last received message, display messages start at 0x0100 + +const uint32_t Frequency = 2445000000; //frequency of transmissions +const uint32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_1600; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF5; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate +const uint16_t NetworkID = 0x3210; //Message NetworkID identifies each message, automatically appended to packet + +const uint32_t ACKdelay1 = 10; //delay in mS before sending acknowledge +const uint32_t ACKdelay2 = 20; //delay in mS before sending acknowledge for message already received +const uint32_t RXtimeout = 60000 ; //receive timeout in mS. set to 0 for no timeout +const uint32_t TXtimeout = 1000; //transmit timeout in mS. +const uint8_t MaxMessageSize = 251 ; //maximum size of serial maeesage to send, 4 bytes needed for NetworkID and payload CRC +uint8_t Message[MaxMessageSize]; //array for receiving message + +float GPSLat; //Latitude from GPS +float GPSLon; //Longitude from GPS +float GPSAlt; //Altitude from GPS +uint8_t GPSSats; //number of GPS satellites in use + +#include //get library here > http://arduiniana.org/libraries/tinygpsplus/ +TinyGPSPlus gps; //create the TinyGPS++ object + +#define SerialOutput Serial //assign serial port for outputing data +#define DebugSerial Serial //assign serial port for outputing debug data + +//#define DEBUG //enable this define to see debug information + + +void loop() +{ + +#ifdef DEBUG + DebugSerial.print(F("> ")); +#endif + + do + { + RXPacketL = LoRa.receiveReliable(Message, MaxMessageSize, NetworkID, RXtimeout, WAIT_RX); + + MessageID = Message[0] + (Message[1] * 256); //message number is first two bytes of Message array + + if (RXPacketL == 0) //check for a valid packet + { +#ifdef DEBUG + DebugSerial.print(MessageID); + DebugSerial.print(F("E>")); + packet_is_Error(); +#endif + break; + } + + + //if here there is a valid reliable packet + + if (MessageID > LastMessageID) //new message should be greater than last message + { + processMessage(); + delay(ACKdelay1); + sendACK(); + break; + } + + if (MessageID <= 256) //is it a control message + { + processControlMessage(); + break; + } + + if (LastMessageID == MessageID) //have we had this message before, remember TX may miss the sent ACK ? + { + delay(ACKdelay2); + sendACK(); //dont process message, but send ACK again + break; //and go back to receive + } + + if (MessageID == 257) //this appears to be a first message, transmitter may have restarted + { + LastMessageID = 257; //reset last message number + processMessage(); + delay(ACKdelay1); + sendACK(); //dont process message, but send ACK again + break; //and go back to receive + } + + break; //and go back to receive + } + while (1); +} + + +void processMessage() +{ + uint8_t index, endpayload; + + endpayload = RXPacketL - 4; + + for (index = 2; index < endpayload; index++) + { + //SerialOutput.write(Message[index]); + gps.encode(Message[index]); + } + + if (gps.location.isUpdated() && gps.altitude.isUpdated() && gps.date.isUpdated()) + { + GPSLat = gps.location.lat(); + GPSLon = gps.location.lng(); + GPSAlt = gps.altitude.meters(); + GPSSats = gps.satellites.value(); + printGPSfix(); + } + +#ifdef DEBUG + DebugSerial.print(RXPacketL - 6); //print number of characters in message + DebugSerial.print(F(" ")); + DebugSerial.print(MessageID); //print message ID + DebugSerial.print(F(" > ")); + DebugSerial.println(); //so each message is on new line +#endif + + Serial.println(); + Serial.print(F("NMEA ")); + Serial.print(gps.passedChecksum()); //print number of valid NMEA sentences + Serial.print(F(" ")); + Serial.println(gps.failedChecksum()); //print number of failed NMEA sentences + + LastMessageID = MessageID; +} + + +void printGPSfix() +{ + Serial.print(F("GPS Fix > ")); + Serial.print(F("Lat,")); + Serial.print(GPSLat, 6); + Serial.print(F(",Lon,")); + Serial.print(GPSLon, 6); + Serial.print(F(",Alt,")); + Serial.print(GPSAlt, 1); + Serial.print(F("m,Sats,")); + Serial.println(GPSSats); + Serial.println(); +} + + +void processControlMessage() +{ + if (MessageID == 1) + { + DebugSerial.println(F("Ready")); + sendACK(); + } +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LoRa.readIrqStatus(); //read the LoRa device IRQ status register + DebugSerial.print(F("RXPacketL,")); + DebugSerial.print(RXPacketL); + DebugSerial.print(F(",IRQStatus,0x")); + DebugSerial.println(IRQStatus, HEX); + if (IRQStatus & IRQ_RX_TIMEOUT) //check for an RX timeout + { + DebugSerial.println(F("RXTimeout")); + } +} + + +void sendACK() +{ + RXPayloadCRC = LoRa.readUint16SXBuffer(RXPacketL - 2); //get sent payload CRC, needed for ACK + digitalWrite(LED1, HIGH); + LoRa.writeUint16SXBuffer(0, MessageID); + LoRa.writeUint16SXBuffer(2, NetworkID); + LoRa.writeUint16SXBuffer(4, RXPayloadCRC); + LoRa.transmitSXBuffer(0, 6, TXtimeout, TXpower, WAIT_TX); +#ifdef DEBUG + DebugSerial.print(MessageID); + DebugSerial.println(F(" A >")); +#endif + digitalWrite(LED1, LOW); +} + + +void setup() +{ + SerialOutput.begin(115200); + DebugSerial.begin(115200); + +#ifdef DEBUG + DebugSerial.println(); + DebugSerial.println(F(__FILE__)); + DebugSerial.println(); +#endif + + pinMode(LED1, OUTPUT); //set high for on + SPI.begin(); + + if (LoRa.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { +#ifdef DEBUG + DebugSerial.println(F("LoRa Device found")); + delay(1000); +#endif + } + else + { + DebugSerial.println(F("No LoRa device responding")); + while (1); + } + + LoRa.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + +#ifdef DEBUG + DebugSerial.println(F("Receiver ready")); +#endif + + LastMessageID = 256; //start value +} diff --git a/examples/SX128x_examples/Sleep/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel.ino b/examples/SX128x_examples/Sleep/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel.ino new file mode 100644 index 0000000..81e47b0 --- /dev/null +++ b/examples/SX128x_examples/Sleep/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel.ino @@ -0,0 +1,233 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program tests the sleep mode and register retention of the lora device in sleep + mode, it assumes an Atmel ATMega328P processor is in use. The LoRa settings to use are specified in the + 'Settings.h' file. + + A packet is sent, containing the text 'Before Device Sleep' and the LoRa device and Atmel processor are put + to sleep. The processor watchdog timer should wakeup the processor in 15 seconds (approx) and register + values should be retained. The device then attempts to transmit another packet 'After Device Sleep' + without re-loading all the LoRa settings. The receiver should see 'After Device Sleep' for the first + packet and 'After Device Sleep' for the second. + + Tested on a 'bare bones' ATmega328P board, the current in sleep mode was 12.2uA. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include //watchdog timer library, integral to Arduino IDE +#include +#include //get the library here; https://github.com/rocketscream/Low-Power + +#include +#include "Settings.h" + +SX128XLT LT; + +bool SendOK; +int8_t TestPower; +uint8_t TXPacketL; + + +void loop() +{ + digitalWrite(LED1, HIGH); + Serial.print(TXpower); + Serial.print(F("dBm ")); + Serial.print(F("TestPacket1> ")); + Serial.flush(); + + if (Send_Test_Packet1()) + { + packet_is_OK(); + } + else + { + packet_is_Error(); + } + Serial.println(); + delay(packet_delay); + + LT.setSleep(CONFIGURATION_RETENTION); //preserve register settings in sleep. + Serial.println(F("Sleeping zzzzz....")); + Serial.println(); + Serial.flush(); + digitalWrite(LED1, LOW); + + sleep1second(15); //goto sleep for 15 seconds + + Serial.println(F("Awake !")); + Serial.flush(); + digitalWrite(LED1, HIGH); + LT.wake(); + + Serial.print(TXpower); + Serial.print(F("dBm ")); + Serial.print(F("TestPacket2> ")); + Serial.flush(); + + if (Send_Test_Packet2()) + { + packet_is_OK(); + } + else + { + packet_is_Error(); + } + Serial.println(); + delay(packet_delay); +} + + +void sleep1second(uint32_t sleeps) +{ + //uses the lowpower library + uint32_t index; + + for (index = 1; index <= sleeps; index++) + { + LowPower.powerDown(SLEEP_1S, ADC_OFF, BOD_OFF); //sleep in 1 second steps + } +} + + +void packet_is_OK() +{ + Serial.print(F(" ")); + Serial.print(TXPacketL); + Serial.print(F(" Bytes SentOK")); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //get the IRQ status + Serial.print(F("SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); + digitalWrite(LED1, LOW); //this leaves the LED on slightly longer for a packet error +} + + +bool Send_Test_Packet1() +{ + uint8_t bufffersize; + + uint8_t buff[] = "Before Device Sleep"; + TXPacketL = sizeof(buff); + buff[TXPacketL - 1] = '*'; + + if (sizeof(buff) > TXBUFFER_SIZE) //check that defined buffer is not larger than TX_BUFFER + { + bufffersize = TXBUFFER_SIZE; + } + else + { + bufffersize = sizeof(buff); + } + + TXPacketL = bufffersize; + + LT.printASCIIPacket( (uint8_t*) buff, bufffersize); + digitalWrite(LED1, HIGH); + + if (LT.transmit( (uint8_t*) buff, TXPacketL, 10000, TXpower, WAIT_TX)) + { + digitalWrite(LED1, LOW); + return true; + } + else + { + return false; + } +} + + +bool Send_Test_Packet2() +{ + uint8_t bufffersize; + + uint8_t buff[] = "After Device Sleep"; + TXPacketL = sizeof(buff); + buff[TXPacketL - 1] = '*'; + + if (sizeof(buff) > TXBUFFER_SIZE) //check that defined buffer is not larger than TX_BUFFER + { + bufffersize = TXBUFFER_SIZE; + } + else + { + bufffersize = sizeof(buff); + } + + TXPacketL = bufffersize; + + LT.printASCIIPacket( (uint8_t*) buff, bufffersize); + digitalWrite(LED1, HIGH); + + if (LT.transmit( (uint8_t*) buff, TXPacketL, 10000, TXpower, WAIT_TX)) + { + digitalWrite(LED1, LOW); + return true; + } + else + { + return false; + } +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(F("5_LoRa_TX_Sleep_Timed_Wakeup_Atmel Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.print(F("Transmitter ready - TXBUFFER_SIZE ")); + Serial.println(TXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/Sleep/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel/Settings.h b/examples/SX128x_examples/Sleep/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel/Settings.h new file mode 100644 index 0000000..cc64d63 --- /dev/null +++ b/examples/SX128x_examples/Sleep/5_LoRa_TX_Sleep_Timed_Wakeup_Atmel/Settings.h @@ -0,0 +1,31 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 29/02/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 +#define RFBUSY 7 +#define NRESET 9 +#define LED1 8 +#define DIO1 3 + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm +const uint16_t packet_delay = 1000; //mS delay between packets + +#define TXBUFFER_SIZE 32 //RX buffer size diff --git a/examples/SX128x_examples/Sleep/62_LoRa_Wake_on_RX_Atmel/62_LoRa_Wake_on_RX_Atmel.ino b/examples/SX128x_examples/Sleep/62_LoRa_Wake_on_RX_Atmel/62_LoRa_Wake_on_RX_Atmel.ino new file mode 100644 index 0000000..b420d2c --- /dev/null +++ b/examples/SX128x_examples/Sleep/62_LoRa_Wake_on_RX_Atmel/62_LoRa_Wake_on_RX_Atmel.ino @@ -0,0 +1,215 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/06/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - The program listens for incoming packets using the LoRa settings in the 'Settings.h' + file. The pins to access the lora device need to be defined in the 'Settings.h' file also. + + When the program starts the LoRa device is setup to recieve packets with pin DIO1 set to go high when a + packet arrives. The receiver remains powered (it cannot receive otherwise) and the processor + (Atmel ATMega328P or 1284P) is put to sleep. When pin DIO1 does go high, indicating a packet is received, + the processor wakes up and prints the packet. It then goes back to sleep. + + There is a printout of the valid packets received, these are assumed to be in ASCII printable text. + The LED will flash for each packet received and the buzzer will sound,if fitted. + + Tested on a 'bare bones' ATmega328P board, the current in sleep mode was 11.32mA. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +#include +#include +#include "PinChangeInterrupt.h" //get the library here; https://github.com/NicoHood/PinChangeInterrupt + +#include + +SX128XLT LT; + +uint32_t RXpacketCount; +uint32_t errors; + +uint8_t RXBUFFER[RXBUFFER_SIZE]; //create a buffer for the received packet + +uint8_t RXPacketL; //stores length of packet received +int16_t PacketRSSI; //stores RSSI of received packet +int8_t PacketSNR; //stores signal to noise ratio of received packet + + +void loop() +{ + RXPacketL = LT.receive(RXBUFFER, RXBUFFER_SIZE, 0, NO_WAIT); //setup LoRa device for receive with no timeout + + Serial.println(F("Waiting for RX - Sleeping")); + Serial.flush(); + + attachInterrupt(digitalPinToInterrupt(DIO1), wakeUp, HIGH); + + atmelSleepPermanent(); //sleep the processor + + detachInterrupt(digitalPinToInterrupt(DIO1)); + + //something has happened ? + Serial.println(F("Awake")); + digitalWrite(LED1, HIGH); + + if (BUZZER > 0) + { + digitalWrite(BUZZER, HIGH); + } + + RXPacketL = LT.readPacket(RXBUFFER, RXBUFFER_SIZE); //now read in the received packet to the RX buffer + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + if (BUZZER > 0) + { + + digitalWrite(BUZZER, LOW); + } + Serial.println(); +} + + +void wakeUp() +{ + //handler for the interrupt +} + + +void packet_is_OK() +{ + uint16_t IRQStatus, localCRC; + + IRQStatus = LT.readIrqStatus(); + RXpacketCount++; + + RXPacketL = LT.readPacket(RXBUFFER, RXBUFFER_SIZE); //now read in the received packet to the RX buffer + + Serial.print(F("Packet> ")); + LT.printASCIIPacket(RXBUFFER, RXPacketL); + + localCRC = LT.CRCCCITT(RXBUFFER, RXPacketL, 0xFFFF); + Serial.print(F(" CRC,")); + Serial.print(localCRC, HEX); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(RXPacketL); + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + Serial.println(); + led_Flash(2, 125); //LED flash for approx 10 seconds +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + IRQStatus = LT.readIrqStatus(); //get the IRQ status + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.println(F("RXTimeout")); + } + else + { + errors++; + Serial.print(F("PacketError")); + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",Packets,")); + Serial.print(RXpacketCount); + Serial.print(F(",Errors,")); + Serial.print(errors); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); + Serial.println(); + } +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(F("62_LoRa_Wake_on_RX_Atmel Starting")); + + if (BUZZER > 0) + { + pinMode(BUZZER, OUTPUT); + digitalWrite(BUZZER, HIGH); + delay(50); + digitalWrite(BUZZER, LOW); + } + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.print(F("Receiver ready - RXBUFFER_SIZE ")); + Serial.println(RXBUFFER_SIZE); + Serial.println(); +} diff --git a/examples/SX128x_examples/Sleep/62_LoRa_Wake_on_RX_Atmel/Settings.h b/examples/SX128x_examples/Sleep/62_LoRa_Wake_on_RX_Atmel/Settings.h new file mode 100644 index 0000000..b143068 --- /dev/null +++ b/examples/SX128x_examples/Sleep/62_LoRa_Wake_on_RX_Atmel/Settings.h @@ -0,0 +1,40 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/06/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. BUZZER may not be used +//by this sketch so they do not need to be connected and should be set to -1. + +#define NSS 10 //select pin on lora device +#define NRESET 9 //reset pin on lora device +#define RFBUSY 7 //busy pin on lora device +#define DIO1 3 //DIO1 pin on lora device, used for RX and TX done + +#define LED1 8 //on board LED, high for on +#define BUZZER -1 //pin for buzzer, on when logic high + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +#define Frequency 2445000000 //frequency of transmissions +#define Offset 0 //offset frequency for calibration purposes +#define Bandwidth LORA_BW_0400 //LoRa bandwidth +#define SpreadingFactor LORA_SF7 //LoRa spreading factor +#define CodeRate LORA_CR_4_5 //LoRa coding rate + +const int8_t TXpower = 10; //LoRa transmit power in dBm + +const uint16_t packet_delay = 1000; //mS delay between packets + +#define RXBUFFER_SIZE 32 //RX buffer size +const uint16_t packetCRCcheck = 0x3F83; //CRC to check RX packet for +const uint8_t packetCRClengthcheck = 23; //packet length to check for diff --git a/examples/SX128x_examples/Tracker/23_GPS_Tracker_Transmitter/23_GPS_Tracker_Transmitter.ino b/examples/SX128x_examples/Tracker/23_GPS_Tracker_Transmitter/23_GPS_Tracker_Transmitter.ino new file mode 100644 index 0000000..47bad2d --- /dev/null +++ b/examples/SX128x_examples/Tracker/23_GPS_Tracker_Transmitter/23_GPS_Tracker_Transmitter.ino @@ -0,0 +1,399 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 21/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program is an example of a basic GPS tracker. The program reads the GPS, + waits for an updated fix and transmits location and altitude, number of satellites in view, the HDOP + value, the fix time of the GPS and the battery voltage. This transmitter can be also be used to + investigate GPS performance. At startup there should be a couple of seconds of recognisable text from + the GPS printed to the serial monitor. If you see garbage or funny characters its likley the GPS baud + rate is wrong. If the transmitter is turned on from cold, the receiver will pick up the cold fix time, + which is an indication of GPS performance. The GPS will be powered on for around 4 seconds before the + timing of the fix starts. Outside with a good view of the sky most GPSs should produce a fix in around + 45 seconds. The number of satellites and HDOP are good indications to how well a GPS is working. + + The program writes direct to the LoRa devices internal buffer, no memory buffer is used. + + The LoRa settings are configured in the Settings.h file. + + The program has the option of using a pin to control the power to the GPS (GPSPOWER), if the GPS module + or board being used has this feature. To not use this feature set the define for GPSPOWER in the + Settings.h file to '#define GPSPOWER -1'. Also set the GPSONSTATE and GPSOFFSTATE to the appropriate logic + levels. + + There is also an option of using a logic pin to turn the resistor divider used to read battery voltage on + and off. This reduces current used in sleep mode. To use the feature set the define for pin BATVREADON + in 'Settings.h' to the pin used. If not using the feature set the pin number to -1. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include + +#include "Settings.h" +#include + +SX128XLT LT; + +#include //get library here > http://arduiniana.org/libraries/tinygpsplus/ +TinyGPSPlus gps; //create the TinyGPS++ object + + +#ifdef USE_SOFTSERIAL_GPS +#include +SoftwareSerial GPSserial(RXpin, TXpin); +#else +#define GPSserial HardwareSerialPort //hardware serial port (eg Serial1) is configured in the Settings.h file +#endif + + +uint8_t TXStatus = 0; //used to store current status flag bits of Tracker transmitter (TX) +uint8_t TXPacketL; //length of LoRa packet (TX) +float TXLat; //Latitude from GPS on Tracker transmitter (TX) +float TXLon; //Longitude from GPS on Tracker transmitter (TX) +float TXAlt; //Altitude from GPS on Tracker transmitter (TX) +uint8_t TXSats; //number of GPS satellites seen (TX) +uint32_t TXHdop; //HDOP from GPS on Tracker transmitter (TX) +uint16_t TXVolts; //Volts (battery) level on Tracker transmitter (TX) +uint32_t TXGPSFixTime; //GPS fix time in hot fix mode of GPS on Tracker transmitter (TX) +uint32_t TXPacketCount, TXErrorsCount; //keep count of OK packets and send errors + + +void loop() +{ + + if (gpsWaitFix(WaitGPSFixSeconds)) + { + sendLocation(TXLat, TXLon, TXAlt, TXHdop, TXGPSFixTime); + Serial.println(); + Serial.print(F("Waiting ")); + Serial.print(Sleepsecs); + Serial.println(F("s")); + delay(Sleepsecs * 1000); //this sleep is used to set overall transmission cycle time + } + else + { + send_Command(NoFix); //send notification of no GPS fix. + } +} + + +bool gpsWaitFix(uint32_t waitSecs) +{ + //waits a specified number of seconds for a fix, returns true for good fix + uint32_t endwaitmS, GPSonTime; + bool GPSfix = false; + float tempfloat; + uint8_t GPSchar; + + GPSonTime = millis(); + GPSserial.begin(9600); //start GPSserial + + Serial.print(F("Wait GPS Fix ")); + Serial.print(waitSecs); + Serial.println(F("s")); + + endwaitmS = millis() + (waitSecs * 1000); + + while (millis() < endwaitmS) + { + if (GPSserial.available() > 0) + { + GPSchar = GPSserial.read(); + gps.encode(GPSchar); + } + + if (gps.location.isUpdated() && gps.altitude.isUpdated()) + { + GPSfix = true; + Serial.print(F("Have GPS Fix ")); + TXGPSFixTime = millis() - GPSonTime; + Serial.print(TXGPSFixTime); + Serial.println(F("mS")); + + TXLat = gps.location.lat(); + TXLon = gps.location.lng(); + TXAlt = gps.altitude.meters(); + TXSats = gps.satellites.value(); + TXHdop = gps.hdop.value(); + tempfloat = ( (float) TXHdop / 100); + + Serial.print(TXLat, 5); + Serial.print(F(",")); + Serial.print(TXLon, 5); + Serial.print(F(",")); + Serial.print(TXAlt, 1); + Serial.print(F(",")); + Serial.print(TXSats); + Serial.print(F(",")); + Serial.print(tempfloat, 2); + Serial.println(); + + break; //exit while loop reading GPS + } + } + + //if here then there has either been a fix or no fix and a timeout + + if (GPSfix) + { + setStatusByte(GPSFix, 1); //set status bit to flag a GPS fix + } + else + { + setStatusByte(GPSFix, 0); //set status bit to flag no fix + Serial.println(); + Serial.println(F("Timeout - No GPSFix")); + Serial.println(); + GPSfix = false; + } + + GPSserial.end(); //serial RX interrupts interfere with SPI, so stop GPSserial + return GPSfix; +} + + +void sendLocation(float Lat, float Lon, float Alt, uint32_t Hdop, uint32_t fixtime) +{ + uint8_t len; + uint16_t IRQStatus; + + Serial.print(F("Send Location")); + + TXVolts = readSupplyVoltage(); //get the latest supply\battery volts + + LT.startWriteSXBuffer(0); //initialise buffer write at address 0 + LT.writeUint8(LocationPacket); //indentify type of packet + LT.writeUint8(Broadcast); //who is the packet sent too + LT.writeUint8(ThisNode); //tells receiver where is packet from + LT.writeFloat(Lat); //add latitude + LT.writeFloat(Lon); //add longitude + LT.writeFloat(Alt); //add altitude + LT.writeUint8(TXSats); //add number of satellites + LT.writeUint32(Hdop); //add hdop + LT.writeUint8(TXStatus); //add tracker status + LT.writeUint32(fixtime); //add GPS fix time in mS + LT.writeUint16(TXVolts); //add tracker supply volts + LT.writeUint32(millis()); //add uptime in mS + len = LT.endWriteSXBuffer(); //close buffer write + + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXBuffer(0, len, 10000, TXpower, WAIT_TX); + digitalWrite(LED1, LOW); + + if (TXPacketL) + { + TXPacketCount++; + Serial.println(F(" - Done ")); + Serial.print(F("SentOK,")); + Serial.print(TXPacketCount); + Serial.print(F(",Errors,")); + Serial.println(TXErrorsCount); + } + else + { + //if here there was an error transmitting packet + TXErrorsCount++; + IRQStatus = LT.readIrqStatus(); //read the the interrupt register + Serial.print(F(" SendError,")); + Serial.print(F("Length,")); + Serial.print(TXPacketL); //print transmitted packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); //print IRQ status + LT.printIrqStatus(); //prints the text of which IRQs set + Serial.println(); + } +} + + +void setStatusByte(uint8_t bitnum, uint8_t bitval) +{ + //program the status byte + + if (bitval == 0) + { + bitClear(TXStatus, bitnum); + } + else + { + bitSet(TXStatus, bitnum); + } +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + //flash LED to show tracker is alive + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void send_Command(char cmd) +{ + bool SendOK; + uint8_t len; + + Serial.print(F("Send Cmd ")); + Serial.write(cmd); + + LT.startWriteSXBuffer(0); + LT.writeUint8(cmd); //packet addressing used indentify type of packet + LT.writeUint8(Broadcast); //who is the packet sent to + LT.writeUint8(ThisNode); //where is packet from + LT.writeUint16(TXVolts); + len = LT.endWriteSXBuffer(); + + digitalWrite(LED1, HIGH); + SendOK = LT.transmitSXBuffer(0, len, 10000, TXpower, WAIT_TX); //timeout set at 10 seconds + digitalWrite(LED1, LOW); + + if (SendOK) + { + Serial.println(F(" - Done")); + } + else + { + Serial.println(F(" - Error")); + } +} + + +uint16_t readSupplyVoltage() +{ + //relies on 1V internal reference and 91K & 11K resistor divider + //returns supply in mV @ 10mV per AD bit read + uint16_t temp; + uint16_t voltage = 0; + uint8_t index; + + if (BATVREADON >= 0) + { + digitalWrite(BATVREADON, HIGH); //turn on MOSFET connecting resitor divider in circuit + } + + analogReference(INTERNAL); + temp = analogRead(SupplyAD); + + for (index = 0; index <= 4; index++) //sample AD 5 times + { + temp = analogRead(SupplyAD); + voltage = voltage + temp; + } + + if (BATVREADON >= 0) + { + digitalWrite(BATVREADON, LOW); //turn off MOSFET connecting resitor divider in circuit + } + + + voltage = ((voltage / 5) * ADMultiplier) + DIODEMV; + return voltage; +} + + +void GPSON() +{ + if (GPSPOWER >= 0) + { + digitalWrite(GPSPOWER, GPSONSTATE); //power up GPS + } +} + + +void GPSOFF() +{ + if (GPSPOWER) + { + digitalWrite(GPSPOWER, GPSOFFSTATE); //power off GPS + } +} + + +void setup() +{ + uint32_t endmS; + + if (GPSPOWER >= 0) + { + pinMode(GPSPOWER, OUTPUT); + GPSON(); + } + + if (BATVREADON >= 0) + { + pinMode(BATVREADON, OUTPUT); + } + + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + + Serial.println(F("23_GPS_Tracker_Transmitter Starting")); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa Device found")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + LT.printOperatingSettings(); //reads and prints the configured operating settings, useful check + Serial.println(); + + TXVolts = readSupplyVoltage(); + + Serial.print(F("Supply ")); + Serial.print(TXVolts); + Serial.println(F("mV")); + + send_Command(PowerUp); //send power up command, includes supply mV + + Serial.println(F("Startup GPS check")); + + GPSserial.begin(9600); + + endmS = millis() + echomS; + + while (millis() < endmS) + { + while (GPSserial.available() > 0) + Serial.write(GPSserial.read()); + } + Serial.println(); + Serial.println(); + + Serial.println(F("Wait for first GPS fix")); + gpsWaitFix(WaitFirstGPSFixSeconds); + + sendLocation(TXLat, TXLon, TXAlt, TXHdop, TXGPSFixTime); +} diff --git a/examples/SX128x_examples/Tracker/23_GPS_Tracker_Transmitter/Settings.h b/examples/SX128x_examples/Tracker/23_GPS_Tracker_Transmitter/Settings.h new file mode 100644 index 0000000..15679f4 --- /dev/null +++ b/examples/SX128x_examples/Tracker/23_GPS_Tracker_Transmitter/Settings.h @@ -0,0 +1,63 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 16/12/19 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //SX128X busy pin +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done + +#define GPSPOWER 4 //Pin that controls power to GPS, set to -1 if not used +#define GPSONSTATE HIGH //logic level to turn GPS on via pin GPSPOWER +#define GPSOFFSTATE LOW //logic level to turn GPS off via pin GPSPOWER + +#define RXpin A3 //pin number for GPS RX input into Arduino - TX from GPS +#define TXpin A2 //pin number for GPS TX output from Arduino- RX into GPS + +#define LED1 8 //On board LED, high for on +#define SupplyAD A7 //pin for reading supply\battery voltage +#define BATVREADON 8 //turns on battery resistor divider, high for on, -1 if not used + +const float ADMultiplier = 10.0; //multiplier for supply volts calculation +#define DIODEMV 98 //mV voltage drop accross diode at approx 8mA + + +#define LORA_DEVICE DEVICE_SX1280 //we need to define the device we are using + + + +//******* Setup LoRa Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0200; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF12; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +const int8_t TXpower = 10; //Power for transmissions in dBm + +#define ThisNode '2' //a character that identifies this tracker + +//************************************************************************************************** +// GPS Settings +//************************************************************************************************** + +#define USE_SOFTSERIAL_GPS //need to include this if we are using softserial for GPS +//#define HardwareSerialPort Serial1 //if using hardware serial enable this define for hardware serial port + +#define GPSBaud 9600 //GPS Baud rate + +#define WaitGPSFixSeconds 30 //time in seconds to wait for a new GPS fix +#define WaitFirstGPSFixSeconds 1800 //time to seconds to wait for the first GPS fix at startup +#define Sleepsecs 5 //seconds between transmissions, this delay is used to set overall transmission cycle time + +#define echomS 2000 //number of mS to run GPS echo at startup diff --git a/examples/SX128x_examples/Tracker/24_GPS_Tracker_Receiver/24_GPS_Tracker_Receiver.ino b/examples/SX128x_examples/Tracker/24_GPS_Tracker_Receiver/24_GPS_Tracker_Receiver.ino new file mode 100644 index 0000000..6620906 --- /dev/null +++ b/examples/SX128x_examples/Tracker/24_GPS_Tracker_Receiver/24_GPS_Tracker_Receiver.ino @@ -0,0 +1,313 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 22/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program is an basic receiver for the '23_Simple_GPS_Tracker_Transmitter' program. + The program reads the received packet from the tracker transmitter and displays the results on + the serial monitor. The LoRa and frequency settings provided in the Settings.h file must + match those used by the transmitter. + + The program receives direct from the LoRa devices internal buffer. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include + +SX128XLT LT; + +#include "Settings.h" +#include + + +uint32_t RXpacketCount; //count of received packets + +uint8_t RXPacketL; //length of received packet +int16_t PacketRSSI; //RSSI of received packet +int8_t PacketSNR; //signal to noise ratio of received packet +uint8_t PacketType; //for packet addressing, identifies packet type +uint8_t Destination; //for packet addressing, identifies the destination (receiving) node +uint8_t Source; //for packet addressing, identifies the source (transmiting) node +uint8_t TXStatus; //A status byte +float TXLat; //latitude +float TXLon; //longitude +float TXAlt; //altitude +uint32_t TXHdop; //HDOP, indication of fix quality, horizontal dilution of precision, low is good +uint32_t TXGPSFixTime; //time in mS for fix +uint16_t TXVolts; //supply\battery voltage +uint8_t TXSats; //number of sattelites in use +uint32_t TXupTimemS; //up time of TX in mS + + +void loop() +{ + RXPacketL = LT.receiveSXBuffer(0, 0, WAIT_RX); //returns 0 if packet error of some sort + + digitalWrite(LED1, HIGH); + + if (BUZZER > 0) + { + digitalWrite(BUZZER, HIGH); + } + + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + if (BUZZER > 0) + { + digitalWrite(BUZZER, LOW); + } + + Serial.println(); +} + + +void readPacketAddressing() +{ + //the transmitter is using packet addressing, so read in the details + LT.startReadSXBuffer(0); + PacketType = LT.readUint8(); + Destination = LT.readUint8(); + Source = LT.readUint8(); + LT.endReadSXBuffer(); +} + + +void packet_is_OK() +{ + float tempHdop; + + RXpacketCount++; + Serial.print(F("Packet OK > ")); + + readPacketAddressing(); + + if (PacketType == PowerUp) + { + LT.startReadSXBuffer(0); + LT.readUint8(); //read byte from FIFO, not used + LT.readUint8(); //read byte from FIFO, not used + LT.readUint8(); //read byte from FIFO, not used + TXVolts = LT.readUint16(); + LT.endReadSXBuffer(); + Serial.print(F("Tracker transmitter powerup - battery ")); + Serial.print(TXVolts); + Serial.print(F("mV")); + } + + + if (PacketType == LocationPacket) + { + //packet has been received, now read from the SX12XX FIFO in the correct order. + Serial.print(F("LocationPacket ")); + LT.startReadSXBuffer(0); + PacketType = LT.readUint8(); + Destination = LT.readUint8(); + Source = LT.readUint8(); + TXLat = LT.readFloat(); + TXLon = LT.readFloat(); + TXAlt = LT.readFloat(); + TXSats = LT.readUint8(); + TXHdop = LT.readUint32(); + TXStatus = LT.readUint8(); + TXGPSFixTime = LT.readUint32(); + TXVolts = LT.readUint16(); + TXupTimemS = LT.readUint32(); + RXPacketL = LT.endReadSXBuffer(); + + tempHdop = ( (float) TXHdop / 100); //need to convert Hdop read from GPS as uint32_t to a float for display + + Serial.write(PacketType); + Serial.write(Destination); + Serial.write(Source); + Serial.print(F(",")); + Serial.print(TXLat, 5); + Serial.print(F(",")); + Serial.print(TXLon, 5); + Serial.print(F(",")); + Serial.print(TXAlt, 1); + Serial.print(F("m,")); + Serial.print(TXSats); + Serial.print(F(",")); + Serial.print(tempHdop, 2); + Serial.print(F(",")); + Serial.print(TXStatus); + Serial.print(F(",")); + Serial.print(TXGPSFixTime); + Serial.print(F("mS,")); + Serial.print(TXVolts); + Serial.print(F("mV,")); + Serial.print((TXupTimemS / 1000)); + Serial.print(F("s,")); + printpacketDetails(); + return; + } + + if (PacketType == LocationBinaryPacket) + { + //packet from locator has been received, now read from the SX12XX FIFO in the correct order. + Serial.print(F("LocationBinaryPacket ")); + LT.startReadSXBuffer(0); + PacketType = LT.readUint8(); + Destination = LT.readUint8(); + Source = LT.readUint8(); + TXLat = LT.readFloat(); + TXLon = LT.readFloat(); + TXAlt = LT.readInt16(); + TXStatus = LT.readUint8(); + RXPacketL = LT.endReadSXBuffer(); + + tempHdop = ( (float) TXHdop / 100); //need to convert Hdop read from GPS as uint32_t to a float for display + + Serial.write(PacketType); + Serial.write(Destination); + Serial.write(Source); + Serial.print(F(",")); + Serial.print(TXLat, 5); + Serial.print(F(",")); + Serial.print(TXLon, 5); + Serial.print(F(",")); + Serial.print(TXAlt, 0); + Serial.print(F("m,")); + Serial.print(TXStatus); + printpacketDetails(); + return; + } + + if (PacketType == NoFix) + { + Serial.print(F("No Tracker GPS fix ")); + printpacketDetails(); + return; + } + +} + + +void printpacketDetails() +{ + uint16_t IRQStatus; + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Packets,")); + Serial.print(RXpacketCount); + + Serial.print(F(",Length,")); + Serial.print(RXPacketL); + IRQStatus = LT.readIrqStatus(); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + if (BUZZER > 0) + { + digitalWrite(BUZZER, LOW); + delay(100); + digitalWrite(BUZZER, HIGH); + } + + IRQStatus = LT.readIrqStatus(); //get the IRQ status + Serial.print(F("PacketError,RSSI")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); + digitalWrite(LED1, LOW); + + if (BUZZER > 0) + { + digitalWrite(BUZZER, LOW); + delay(100); + digitalWrite(BUZZER, HIGH); + } +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + + Serial.println(F("24_GPS_Tracker_Receiver Starting")); + + if (BUZZER >= 0) + { + pinMode(BUZZER, OUTPUT); + Serial.println(F("BUZZER Enabled")); + } + else + { + Serial.println(F("BUZZER Not Enabled")); + } + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("LoRa device found")); + led_Flash(2, 125); + } + else + { + Serial.println(F("No device responding")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + LT.printModemSettings(); //reads and prints the configured LoRa settings, useful check + Serial.println(); + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Tracker/24_GPS_Tracker_Receiver/Settings.h b/examples/SX128x_examples/Tracker/24_GPS_Tracker_Receiver/Settings.h new file mode 100644 index 0000000..4b62df5 --- /dev/null +++ b/examples/SX128x_examples/Tracker/24_GPS_Tracker_Receiver/Settings.h @@ -0,0 +1,37 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 22/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //SX128X busy pin +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done + +#define LED1 8 //On board LED, high for on + +#define BUZZER -1 //Buzzer if fitted, high for on. Set to -1 if not used + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0200; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF12; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate diff --git a/examples/SX128x_examples/Tracker/25_GPS_Tracker_Receiver_With_Display_and_GPS/25_GPS_Tracker_Receiver_With_Display_and_GPS.ino b/examples/SX128x_examples/Tracker/25_GPS_Tracker_Receiver_With_Display_and_GPS/25_GPS_Tracker_Receiver_With_Display_and_GPS.ino new file mode 100644 index 0000000..b1a4fc5 --- /dev/null +++ b/examples/SX128x_examples/Tracker/25_GPS_Tracker_Receiver_With_Display_and_GPS/25_GPS_Tracker_Receiver_With_Display_and_GPS.ino @@ -0,0 +1,613 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 21/03/20 + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program is an example of a functional GPS tracker receiver using lora. + It is capable of picking up the trackers location packets from many kilometres away with only basic antennas. + + The program receives the location packets from the remote tracker transmitter and writes them on an OLED + display and also prints the information to the Arduino IDE serial monitor. The program can read a locally + attached GPS and when that has a fix, will display the distance and direction to the remote tracker. + + The program writes direct to the lora devices internal buffer, no memory buffer is used. The lora settings + are configured in the Settings.h file. + + The receiver recognises two types of tracker packet, the one from the matching program '23_GPS_Tracker_Transmitter' + (LocationPacket, 27 bytes) which causes these fields to be printed to the serial monitor; + + Latitude, Longitude, Altitude, Satellites, HDOP, TrackerStatusByte, GPS Fixtime, Battery mV, Distance, Direction, + Distance, Direction, PacketRSSI, PacketSNR, NumberPackets, PacketLength, IRQRegister. + + This is a long packet which at the long range LoRa settings takes just over 3 seconds to transmit. + + The receiver also recognises a much shorter location only packet (LocationBinaryPacket, 11 bytes) and when + received this is printed to the serial monitor; + + Latitude, Longitude, Altitude, TrackerStatusByte, Distance, Direction, PacketRSSI, PacketSNR, NumberPackets, + PacketLength, IRQRegister. + + Most of the tracker information (for both types of packet) is shown on the OLED display. If there has been a + tracker transmitter GPS fix the number\identifier of that tracker is shown on row 0 right of screen and if there + is a recent local (receiver) GPS fix an 'R' is displayed row 1 right of screen. + + When the tracker transmitter starts up or is reset its sends a power up message containing the battery voltage + which is shown on the OLED and printer to the serial monitor. + + The program has the option of using a pin to control the power to the GPS, if the GPS module being used has this + feature. To use the option change the define in Settings.h; + + '#define GPSPOWER -1' from -1 to the pin number being used. Also set the GPSONSTATE and GPSOFFSTATE defines to + the appropriate logic levels. + + The program by default uses software serial to read the GPS, you can use hardware serial by commenting out this + line in the Settings.h file; + + #define USE_SOFTSERIAL_GPS + + And then defining the hardware serial port you are using, which defaults to Serial1. + + Serial monitor baud rate is set at 9600. +*******************************************************************************************************/ + +#include +#include +SX128XLT LT; + +#include "Settings.h" +#include + +#include //https://github.com/olikraus/u8g2 +U8X8_SSD1306_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //standard 0.96" SSD1306 +//U8X8_SH1106_128X64_NONAME_HW_I2C disp(U8X8_PIN_NONE); //1.3" OLED often sold as 1.3" SSD1306 + + +#include //http://arduiniana.org/libraries/tinygpsplus/ +TinyGPSPlus gps; //create the TinyGPS++ object + +#ifdef USE_SOFTSERIAL_GPS +#include +SoftwareSerial GPSserial(RXpin, TXpin); +#else +#define GPSserial HardwareSerialPort //hardware serial port (eg Serial1) is configured in the Settings.h file +#endif + +uint32_t RXpacketCount; //count of received packets +uint8_t RXPacketL; //length of received packet +int16_t PacketRSSI; //signal strength (RSSI) dBm of received packet +int8_t PacketSNR; //signal to noise ratio (SNR) dB of received packet +uint8_t PacketType; //for packet addressing, identifies packet type +uint8_t Destination; //for packet addressing, identifies the destination (receiving) node +uint8_t Source; //for packet addressing, identifies the source (transmiting) node +uint8_t TXStatus; //status byte from tracker transmitter +uint8_t TXSats; //number of sattelites in use +float TXLat; //latitude +float TXLon; //longitude +float TXAlt; //altitude +float RXLat; //latitude +float RXLon; //longitude +float RXAlt; //altitude +uint32_t TXHdop; //HDOP, indication of fix quality, horizontal dilution of precision, low is good +uint32_t TXGPSFixTime; //time in mS for fix +uint16_t TXVolts; //supply\battery voltage +uint16_t RXVolts; //supply\battery voltage +float TXdistance; //calculated distance to tracker +uint16_t TXdirection; //calculated direction to tracker +uint16_t RXerrors; +uint32_t TXupTimemS; //up time of TX in mS + +uint32_t LastRXGPSfixCheck; //used to record the time of the last GPS fix + +bool TXLocation = false; //set to true when at least one tracker location packet has been received +bool RXGPSfix = false; //set to true if the local GPS has a recent fix + +uint8_t FixCount = DisplayRate; //used to keep track of number of GPS fixes before display updated + + +void loop() +{ + RXPacketL = LT.receiveSXBuffer(0, 0, NO_WAIT); //returns 0 if packet error of some sort + + while (!digitalRead(DIO1)) + { + readGPS(); //If the DIO pin is low, no packet arrived, so read the GPS + } + + //something has happened in receiver + digitalWrite(LED1, HIGH); + + if (BUZZER > 0) + { + digitalWrite(BUZZER, HIGH); + } + + RXPacketL = LT.readRXPacketL(); + PacketRSSI = LT.readPacketRSSI(); + PacketSNR = LT.readPacketSNR(); + + + if (RXPacketL == 0) + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + digitalWrite(LED1, LOW); + + if (BUZZER > 0) + { + digitalWrite(BUZZER, LOW); + } + Serial.println(); +} + + +void readGPS() +{ + if (GPSserial.available() > 0) + { + gps.encode(GPSserial.read()); + } + + + if ( millis() > (LastRXGPSfixCheck + NoRXGPSfixms)) + { + RXGPSfix = false; + LastRXGPSfixCheck = millis(); + dispscreen1(); + } + + + if (gps.location.isUpdated() && gps.altitude.isUpdated()) + { + RXGPSfix = true; + RXLat = gps.location.lat(); + RXLon = gps.location.lng(); + RXAlt = gps.altitude.meters(); + printRXLocation(); + LastRXGPSfixCheck = millis(); + + if ( FixCount == 1) //update screen when FIXcoount counts down from DisplayRate to 1 + { + FixCount = DisplayRate; + dispscreen1(); + } + FixCount--; + } +} + + +bool readTXStatus(byte bitnum) +{ + return bitRead(TXStatus, bitnum); +} + + +void printRXLocation() +{ + Serial.print(F("LocalGPS ")); + Serial.print(RXLat, 5); + Serial.print(F(",")); + Serial.print(RXLon, 5); + Serial.print(F(",")); + Serial.print(RXAlt, 1); + Serial.println(); +} + + +void readPacketAddressing() +{ + LT.startReadSXBuffer(0); + PacketType = LT.readUint8(); + Destination = LT.readUint8(); + Source = LT.readUint8(); + LT.endReadSXBuffer(); +} + + +void packet_is_OK() +{ + //uint16_t IRQStatus; + float tempfloat; + + RXpacketCount++; + + readPacketAddressing(); + + if (PacketType == PowerUp) + { + LT.startReadSXBuffer(0); + LT.readUint8(); //read byte from SXBuffer, not used + LT.readUint8(); //read byte from SXBuffer, not used + LT.readUint8(); //read byte from SXBuffer, not used + TXVolts = LT.readUint16(); //read tracker transmitter voltage + LT.endReadSXBuffer(); + Serial.print(F("Tracker Powerup - Battery ")); + Serial.print(TXVolts); + Serial.println(F("mV")); + dispscreen2(); + } + + if (PacketType == LocationPacket) + { + //packet has been received, now read from the SX12XX FIFO in the correct order. + Serial.print(F("LocationPacket ")); + TXLocation = true; + LT.startReadSXBuffer(0); //start the read of received packet + PacketType = LT.readUint8(); //read in the PacketType + Destination = LT.readUint8(); //read in the Packet destination address + Source = LT.readUint8(); //read in the Packet source address + TXLat = LT.readFloat(); //read in the tracker latitude + TXLon = LT.readFloat(); //read in the tracker longitude + TXAlt = LT.readFloat(); //read in the tracker altitude + TXSats = LT.readUint8(); //read in the satellites in use by tracker GPS + TXHdop = LT.readUint32(); //read in the HDOP of tracker GPS + TXStatus = LT.readUint8(); //read in the tracker status byte + TXGPSFixTime = LT.readUint32(); //read in the last fix time of tracker GPS + TXVolts = LT.readUint16(); //read in the tracker supply\battery volts + TXupTimemS = LT.readUint32(); //read in the TX uptime in mS + RXPacketL = LT.endReadSXBuffer(); //end the read of received packet + + + if (RXGPSfix) //if there has been a local GPS fix do the distance and direction calculation + { + TXdirection = (int16_t) TinyGPSPlus::courseTo(RXLat, RXLon, TXLat, TXLon); + TXdistance = TinyGPSPlus::distanceBetween(RXLat, RXLon, TXLat, TXLon); + } + else + { + TXdistance = 0; + TXdirection = 0; + } + + Serial.write(PacketType); + Serial.write(Destination); + Serial.write(Source); + Serial.print(F(",")); + Serial.print(TXLat, 5); + Serial.print(F(",")); + Serial.print(TXLon, 5); + Serial.print(F(",")); + Serial.print(TXAlt, 1); + Serial.print(F(",")); + Serial.print(TXSats); + Serial.print(F(",")); + + tempfloat = ( (float) TXHdop / 100); //need to convert Hdop read from GPS as uint32_t to a float for display + Serial.print(tempfloat, 2); + + Serial.print(F(",")); + Serial.print(TXStatus); + Serial.print(F(",")); + + Serial.print(TXGPSFixTime); + Serial.print(F("mS,")); + Serial.print(TXVolts); + Serial.print(F("mV,")); + Serial.print((TXupTimemS / 1000)); + Serial.print(F("s,")); + + Serial.print(TXdistance, 0); + Serial.print(F("m,")); + Serial.print(TXdirection); + Serial.print(F("d")); + printpacketDetails(); + dispscreen1(); //and show the packet detail it on screen + return; + } + + + if (PacketType == LocationBinaryPacket) + { + //packet from locator has been received, now read from the SX12XX FIFO in the correct order. + TXLocation = true; + Serial.print(F("LocationBinaryPacket ")); + LT.startReadSXBuffer(0); + PacketType = LT.readUint8(); + Destination = LT.readUint8(); + Source = LT.readUint8(); + TXLat = LT.readFloat(); + TXLon = LT.readFloat(); + TXAlt = LT.readInt16(); + TXStatus = LT.readUint8(); + RXPacketL = LT.endReadSXBuffer(); + + if (RXGPSfix) //if there has been a local GPS fix do the distance and direction calculation + { + TXdirection = (int16_t) TinyGPSPlus::courseTo(RXLat, RXLon, TXLat, TXLon); + TXdistance = TinyGPSPlus::distanceBetween(RXLat, RXLon, TXLat, TXLon); + } + else + { + TXdistance = 0; + TXdirection = 0; + } + + Serial.write(PacketType); + Serial.write(Destination); + Serial.write(Source); + Serial.print(F(",")); + Serial.print(TXLat, 5); + Serial.print(F(",")); + Serial.print(TXLon, 5); + Serial.print(F(",")); + Serial.print(TXAlt, 0); + Serial.print(F("m,")); + Serial.print(TXStatus); + Serial.print(F(",")); + Serial.print(TXdistance, 0); + Serial.print(F("m,")); + Serial.print(TXdirection); + Serial.print(F("d")); + printpacketDetails(); + dispscreen1(); + return; + } +} + + +void printpacketDetails() +{ + uint16_t IRQStatus; + Serial.print(F(",RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Packets,")); + Serial.print(RXpacketCount); + + Serial.print(F(",Length,")); + Serial.print(RXPacketL); + IRQStatus = LT.readIrqStatus(); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + if (BUZZER >= 0) + { + digitalWrite(BUZZER, LOW); + delay(100); + digitalWrite(BUZZER, HIGH); + } + + IRQStatus = LT.readIrqStatus(); //get the IRQ status + RXerrors++; + Serial.print(F("PacketError,RSSI")); + + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); //get the real packet length + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); + digitalWrite(LED1, LOW); + + if (BUZZER >= 0) + { + digitalWrite(BUZZER, LOW); + delay(100); + digitalWrite(BUZZER, HIGH); + } +} + + +void led_Flash(uint16_t flashes, uint16_t delaymS) +{ + unsigned int index; + + for (index = 1; index <= flashes; index++) + { + digitalWrite(LED1, HIGH); + delay(delaymS); + digitalWrite(LED1, LOW); + delay(delaymS); + } +} + + +void dispscreen1() +{ + //show received packet data on display + float tempfloat; + disp.clearLine(0); + disp.setCursor(0, 0); + disp.print(TXLat, 5); + disp.clearLine(1); + disp.setCursor(0, 1); + disp.print(TXLon, 5); + disp.clearLine(2); + disp.setCursor(0, 2); + disp.print(TXAlt, 0); + disp.print(F("m")); + disp.clearLine(3); + disp.setCursor(0, 3); + + disp.print(F("RSSI ")); + disp.print(PacketRSSI); + disp.print(F("dBm")); + disp.clearLine(4); + disp.setCursor(0, 4); + disp.print(F("SNR ")); + + if (PacketSNR > 0) + { + disp.print(F("+")); + } + + if (PacketSNR == 0) + { + disp.print(F(" ")); + } + + if (PacketSNR < 0) + { + disp.print(F("-")); + } + + disp.print(PacketSNR); + disp.print(F("dB")); + + if (PacketType == LocationPacket) + { + disp.clearLine(5); + disp.setCursor(0, 5); + tempfloat = ((float) TXVolts / 1000); + disp.print(F("Batt ")); + disp.print(tempfloat, 2); + disp.print(F("v")); + } + + disp.clearLine(6); + disp.setCursor(0, 6); + disp.print(F("Packets ")); + disp.print(RXpacketCount); + + disp.clearLine(7); + + if (RXGPSfix) + { + disp.setCursor(15, 1); + disp.print(F("R")); + } + else + { + disp.setCursor(15, 1); + disp.print(F(" ")); + disp.setCursor(0, 7); + disp.print(F("No Local Fix")); + } + + if (RXGPSfix && TXLocation) //only display distance and direction if have received tracker packet and have local GPS fix + { + disp.clearLine(7); + disp.setCursor(0, 7); + disp.print(TXdistance, 0); + disp.print(F("m ")); + disp.print(TXdirection); + disp.print(F("d")); + } + + if (readTXStatus(GPSFix)) + { + disp.setCursor(15, 0); + disp.write(Source); + } + +} + + +void dispscreen2() +{ + //show tracker powerup data on display + float tempfloat; + disp.clear(); + disp.setCursor(0, 0); + disp.print(F("Tracker Powerup")); + disp.setCursor(0, 1); + disp.print(F("Battery ")); + tempfloat = ((float) TXVolts / 1000); + disp.print(tempfloat, 2); + disp.print(F("v")); +} + + +void GPSON() +{ + if (GPSPOWER >= 0) + { + digitalWrite(GPSPOWER, GPSONSTATE); //power up GPS + } +} + + +void GPSOFF() +{ + if (GPSPOWER >= 0) + { + digitalWrite(GPSPOWER, GPSOFFSTATE); //power off GPS + } +} + + +void setup() +{ + uint32_t endmS; + + pinMode(LED1, OUTPUT); //setup pin as output for indicator LED + led_Flash(2, 125); //two quick LED flashes to indicate program start + + Serial.begin(9600); + Serial.println(); + + Serial.println(F("25_GPS_Tracker_Receiver_With_Display_and_GPS Starting")); + + if (BUZZER >= 0) + { + pinMode(BUZZER, OUTPUT); + } + + SPI.begin(); + + disp.begin(); + disp.setFont(u8x8_font_chroma48medium8_r); + + Serial.print(F("Checking LoRa device - ")); //Initialize LoRa + disp.setCursor(0, 0); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + Serial.println(F("Receiver ready")); + disp.print(F("Receiver ready")); + led_Flash(2, 125); + delay(1000); + } + else + { + Serial.println(F("No LoRa device responding")); + disp.print(F("No LoRa device")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + Serial.println(); + Serial.println(F("Startup GPS check")); + + endmS = millis() + echomS; + + //now startup GPS + if (GPSPOWER >= 0) + { + pinMode(GPSPOWER, OUTPUT); + } + + GPSON(); + GPSserial.begin(GPSBaud); + + while (millis() < endmS) + { + while (GPSserial.available() > 0) + Serial.write(GPSserial.read()); + } + Serial.println(); + Serial.println(); + + Serial.println(F("Receiver ready")); + Serial.println(); +} diff --git a/examples/SX128x_examples/Tracker/25_GPS_Tracker_Receiver_With_Display_and_GPS/Settings.h b/examples/SX128x_examples/Tracker/25_GPS_Tracker_Receiver_With_Display_and_GPS/Settings.h new file mode 100644 index 0000000..baf2c7f --- /dev/null +++ b/examples/SX128x_examples/Tracker/25_GPS_Tracker_Receiver_With_Display_and_GPS/Settings.h @@ -0,0 +1,54 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 16/12/19 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //SX128X busy pin +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done +#define LED1 8 //On board LED, high for on + +#define BUZZER -1 //Buzzer if fitted, high for on. Set to -1 if not used + +#define RXpin A3 //pin number for GPS RX input into Arduino - TX from GPS +#define TXpin A2 //pin number for GPS TX output from Arduino- RX into GPS + +#define GPSPOWER 4 //Pin that controls power to GPS, set to -1 if not used +#define GPSONSTATE HIGH //logic level to turn GPS on via pin GPSPOWER +#define GPSOFFSTATE LOW //logic level to turn GPS off via pin GPSPOWER + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0200; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF12; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//************************************************************************************************** +// GPS Settings +//************************************************************************************************** + +#define USE_SOFTSERIAL_GPS //need to include this if we are using softserial for GPS +//#define HardwareSerialPort Serial1 //if using hardware serial enable this define for hardware serial port + +#define GPSBaud 9600 //GPS Baud rate +#define WaitGPSFixSeconds 30 //time to wait for a new GPS fix +#define echomS 2000 //number of mS to run GPS echo for at startup + +#define NoRXGPSfixms 15000 //max number of mS to allow before no local fix flagged +#define DisplayRate 7 //when working OK the GPS will get a new fix every second or so +//this rate defines how often the display should be updated diff --git a/examples/SX128x_examples/Tracker/38_lora_Relay/38_lora_Relay.ino b/examples/SX128x_examples/Tracker/38_lora_Relay/38_lora_Relay.ino new file mode 100644 index 0000000..a55394d --- /dev/null +++ b/examples/SX128x_examples/Tracker/38_lora_Relay/38_lora_Relay.ino @@ -0,0 +1,156 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This program will receive a LoRa packet and relay (re-transmit) it. The receiving + and transmitting can use different frequencies and lora settings, although in this example they are + the same. The receiving and transmitting settings are in the 'Settings.h' file. If the relay is located + in an advantageous position, for instance on top of a tall tree, building or in an radio controlled model + then the range at which trackers or nodes on the ground can be received is considerably increased. + In these circumstances the relay may listen at a long range setting using SF12 for example and then + re-transmit back to the ground at SF7. + + Serial monitor baud rate is set at 9600. + +*******************************************************************************************************/ + +#include +#include +#include "Settings.h" + +SX128XLT LT; + +uint8_t RXPacketL, TXPacketL; +int16_t PacketRSSI, PacketSNR; +uint16_t RXPacketErrors; + + +void loop() +{ + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + + RXPacketL = LT.receiveSXBuffer(0, 0, WAIT_RX); //returns 0 if packet error of some sort, no timeout set + + digitalWrite(LED1, HIGH); //something has happened + + PacketRSSI = LT.readPacketRSSI(); //read the recived RSSI value + PacketSNR = LT.readPacketSNR(); //read the received SNR value + + if (RXPacketL == 0) //if the LT.receive() function detects an error, RXpacketL == 0 + { + packet_is_Error(); + } + else + { + packet_is_OK(); + } + + Serial.println(); +} + + +void packet_is_OK() +{ + //a packet has been received, so change to relay settings and transmit buffer + + Serial.print(F("PacketOK ")); + printreceptionDetails(); + delay(packet_delay / 2); + digitalWrite(LED1, LOW); + delay(packet_delay / 2); + + Serial.print(F(" Retransmit")); + LT.setupLoRa(RelayFrequency, RelayOffset, RelaySpreadingFactor, RelayBandwidth, RelayCodeRate); + digitalWrite(LED1, HIGH); + TXPacketL = LT.transmitSXBuffer(0, RXPacketL, 10000, TXpower, WAIT_TX); + Serial.print(F(" - Done")); + digitalWrite(LED1, LOW); +} + + +void packet_is_Error() +{ + uint16_t IRQStatus; + + RXPacketErrors++; + IRQStatus = LT.readIrqStatus(); + + led_Flash(5, 50); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Serial.print(F("RXTimeout ")); + } + else + { + Serial.print(F("PacketError ")); + printreceptionDetails(); + Serial.print(F(",IRQreg,")); + Serial.print(IRQStatus, HEX); + LT.printIrqStatus(); + } +} + + +void printreceptionDetails() +{ + Serial.print(F("RSSI,")); + Serial.print(PacketRSSI); + Serial.print(F("dBm,SNR,")); + Serial.print(PacketSNR); + Serial.print(F("dB,Length,")); + Serial.print(LT.readRXPacketL()); +} + + +void led_Flash(uint16_t flashdelay, uint16_t flashes) +{ + uint16_t index; + + for (index = 1; index <= flashes; index++) + { + + delay(flashdelay); + digitalWrite(LED1, HIGH); + delay(flashdelay); + digitalWrite(LED1, LOW); + } +} + + +void setup() +{ + pinMode(LED1, OUTPUT); + led_Flash(2, 125); + + Serial.begin(9600); + + SPI.begin(); + + if (LT.begin(NSS, NRESET, RFBUSY, DIO1, LORA_DEVICE)) + { + led_Flash(2, 125); + } + else + { + Serial.println(F("Device error")); + while (1) + { + led_Flash(50, 50); //long fast speed flash indicates device error + } + } + + LT.setupLoRa(Frequency, Offset, SpreadingFactor, Bandwidth, CodeRate); + Serial.print("ListenSettings,"); + LT.printModemSettings(); + Serial.println(); + LT.setupLoRa(RelayFrequency, RelayOffset, RelaySpreadingFactor, RelayBandwidth, RelayCodeRate); + Serial.print("RelaySettings,"); + LT.printModemSettings(); + Serial.println(); + Serial.println("Relay Ready"); +} diff --git a/examples/SX128x_examples/Tracker/38_lora_Relay/Settings.h b/examples/SX128x_examples/Tracker/38_lora_Relay/Settings.h new file mode 100644 index 0000000..52f61af --- /dev/null +++ b/examples/SX128x_examples/Tracker/38_lora_Relay/Settings.h @@ -0,0 +1,43 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 19/03/20 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + + +//******* Setup hardware pin definitions here ! *************** + +//These are the pin definitions for one of my own boards, the Easy Pro Mini, +//be sure to change the definitions to match your own setup. + +#define NSS 10 //select on LoRa device +#define NRESET 9 //reset on LoRa device +#define RFBUSY 7 //SX128X busy pin +#define DIO1 3 //DIO1 on LoRa device, used for RX and TX done +#define LED1 8 //On board LED, high for on + +#define LORA_DEVICE DEVICE_SX1280 //this is the device we are using + + +//******* Setup LoRa Test Parameters Here ! *************** + +//LoRa Modem Parameters - relay listens on these parameters +const uint32_t Frequency = 2445000000; //frequency of transmissions +const int32_t Offset = 0; //offset frequency for calibration purposes +const uint8_t Bandwidth = LORA_BW_0200; //LoRa bandwidth +const uint8_t SpreadingFactor = LORA_SF12; //LoRa spreading factor +const uint8_t CodeRate = LORA_CR_4_5; //LoRa coding rate + +//LoRa relay re-transmitts on these LoRa Modem Parameters +const uint32_t RelayFrequency = 2445000000; //frequency of transmissions +const uint32_t RelayOffset = 0; //offset frequency for calibration purposes + +const uint8_t RelayBandwidth = LORA_BW_0400; //LoRa bandwidth +const uint8_t RelaySpreadingFactor = LORA_SF7; //LoRa spreading factor +const uint8_t RelayCodeRate = LORA_CR_4_5; //LoRa coding rate + + +const int8_t TXpower = 10; //LoRa TX power in dBm + +#define packet_delay 1000 //mS delay before received packet transmitted diff --git a/keywords.txt b/keywords.txt new file mode 100644 index 0000000..15eed8b --- /dev/null +++ b/keywords.txt @@ -0,0 +1,283 @@ +####################################### +# Syntax Coloring Map For SX128xXLT +####################################### + +####################################### +# Datatypes (KEYWORD1) +####################################### + +SX128XLT KEYWORD1 +ProgramLT_Definitions KEYWORD1 +SX128XLT_Definitions KEYWORD1 + +####################################### +# Methods and Functions (KEYWORD2) +####################################### + +begin KEYWORD2 +pinInit KEYWORD2 +spiInit KEYWORD2 +setupLoRa KEYWORD2 +resetDevice KEYWORD2 +setMode KEYWORD2 +checkBusy KEYWORD2 +setSleep KEYWORD2 +setRegulatorMode KEYWORD2 +checkDevice KEYWORD2 +setDevice KEYWORD2 +config KEYWORD2 +readsavedRegulatorMode KEYWORD2 +setDIO2AsRfSwitchCtrl KEYWORD2 +setDIO3AsTCXOCtrl KEYWORD2 +clearDeviceErrors KEYWORD2 +calibrateDevice KEYWORD2 +printDeviceErrors KEYWORD2 +wake KEYWORD2 +calibrateImage KEYWORD2 +CRCCCITTSX KEYWORD2 +CRCCCITT KEYWORD2 +printSXBufferASCII KEYWORD2 +receiveAddressed KEYWORD2 +getByteSXBuffer KEYWORD2 +startWriteSXBuffer KEYWORD2 +transmitAddressed KEYWORD2 +startReadSXBuffer KEYWORD2 +endReadSXBuffer KEYWORD2 +readBuffer KEYWORD2 +receiveSXBuffer KEYWORD2 +transmitSXBuffer KEYWORD2 +endWriteSXBuffer KEYWORD2 +writeBuffer KEYWORD2 +readPacket KEYWORD2 +setSleep KEYBOARD2 +getOperatingMode KEYWORD2 +writeCommand KEYWORD2 +readCommand KEYWORD2 +writeRegisters KEYWORD2 +writeRegister KEYWORD2 +readRegisters KEYWORD2 +readRegister KEYWORD2 +printRegisters KEYWORD2 +setPacketParams KEYWORD2 +setModulationParams KEYWORD2 +printSavedModulationParams KEYWORD2 +setRfFrequency KEYWORD2 +getFreqInt KEYWORD2 +getFrequencyErrorRegValue KEYWORD2 +getFrequencyErrorHz KEYWORD2 +setTxParams KEYWORD2 +setTx KEYWORD2 +readTXDone KEYWORD2 +setRx KEYWORD2 +setLowPowerRX KEYWORD2 +setHighSensitivity KEYWORD2 +setRXGain KEYWORD2 +readRXDone KEYWORD2 +readRXBufferStatus KEYWORD2 +readRXPacketL KEYWORD2 +readPacketRSSI KEYWORD2 +readPacketSNR KEYWORD2 +setPaConfig KEYWORD2 +setRx2 KEYWORD2 +setRxDutyCycle KEYWORD2 +readRXBufferPointer KEYWORD2 +readsavedFrequency KEYWORD2 +readsavedOffset KEYWORD2 +setBufferBaseAddress KEYWORD2 +setPacketType KEYWORD2 +readsavedPacketType KEYWORD2 +clearIrqStatus KEYWORD2 +readIrqStatus KEYWORD2 +setDioIrqParams KEYWORD2 +printIrqStatus KEYWORD2 +readPacketCRCError KEYWORD2 +readPacketHeaderValid KEYWORD2 +readPacketHeaderError KEYWORD2 +readsavedPower KEYWORD2 +printASCIIorHEX KEYWORD2 +printHEXByte KEYWORD2 +printHEXByte0x KEYWORD2 +printASCIIPacket KEYWORD2 +printHEXPacket KEYWORD2 +readsavedModParam1 KEYWORD2 +readsavedModParam2 KEYWORD2 +readsavedModParam3 KEYWORD2 +readsavedModParam4 KEYWORD2 +readRXPacketType KEYWORD2 +readRXDestination KEYWORD2 +readRXSource KEYWORD2 +printAddressInfo KEYWORD2 +returnSF KEYWORD2 +returnbandwidth KEYWORD2 +readPacketAddressedLoRa KEYWORD2 +getOptimisation KEYWORD2 +calcSymbolTime KEYWORD2 +printModemSettings KEYWORD2 +printOperatingSettings KEYWORD2 +setSyncWord KEYWORD2 +startWriteFIFO KEYWORD2 +endWriteFIFO KEYWORD2 +startReadFIFO KEYWORD2 +endReadFIFO KEYWORD2 +writeUint8 KEYWORD2 +readUint8 KEYWORD2 +writeInt8 KEYWORD2 +readInt8 KEYWORD2 +writeInt16 KEYWORD2 +readInt16 KEYWORD2 +writeUint16 KEYWORD2 +readUint16 KEYWORD2 +writeInt32 KEYWORD2 +readInt32 KEYWORD2 +writeUint32 KEYWORD2 +readUint32 KEYWORD2 +writeFloat KEYWORD2 +readFloat KEYWORD2 +rxtxInit KEYWORD2 +rxEnable KEYWORD2 +txEnable KEYWORD2 +getLoRaBandwidth KEYWORD2 +setRangingRangingAddress KEYWORD2 +setRangingRequestAddress KEYWORD2 +setRangingRole KEYWORD2 +setRangingCalibration KEYWORD2 +getRangingResult KEYWORD2 +complement2 KEYWORD2 +getRangingResultRegValue KEYWORD2 +setupRanging KEYWORD2 +transmitRanging KEYWORD2 +receiveRanging KEYWORD2 +getRangingDistance KEYWORD2 +setRangingMasterAddress KEYWORD2 +setRangingSlaveAddress KEYWORD2 +getRangingCalibrationValue KEYWORD2 +toneFM KEYWORD2 +setupDirect KEYWORD2 +printDevice KEYWORD2 +getLoRaSF KEYWORD2 +getLoRaCodingRate KEYWORD2 +getSyncWord KEYWORD2 +getInvertIQ KEYWORD2 +getPreamble KEYWORD2 +printOperatingMode KEYWORD2 +getVersion KEYWORD2 +getPacketMode KEYWORD2 +getHeaderMode KEYWORD2 +getCRCMode KEYWORD2 +getAGC KEYWORD2 +getLNAgain KEYWORD2 +getLNAboostHF KEYWORD2 +getLNAboostLF KEYWORD2 +getOpmode KEYWORD2 +fillSXBuffer KEYWORD2 +writeByteSXBuffer KEYWORD2 +doAFC KEYWORD2 +getOffset KEYWORD2 +startFSKRTTY KEYWORD2 +transmitFSKRTTY KEYWORD2 +printRTTYregisters KEYWORD2 +endFSKRTTY KEYWORD2 +printSXBufferHEX KEYWORD2 +readBufferChar KEYWORD2 +writeBufferChar KEYWORD2 +setLowPowerReceive KEYWORD2 +doAFCPPM KEYWORD2 +setTXDirect KEYWORD2 +transmitReliable KEYWORD2 +receiveReliable KEYWORD2 +transmitReliableAutoACK KEYWORD2 +receiveReliableAutoACK KEYWORD2 +transmitSXReliable KEYWORD2 +transmitSXReliableAutoACK KEYWORD2 +receiveSXReliable KEYWORD2 +receiveSXReliableAutoACK KEYWORD2 +sendReliableACK KEYWORD2 +sendReliableACK KEYWORD2 +sendSXReliableACK KEYWORD2 +waitReliableACK KEYWORD2 +waitReliableACK KEYWORD2 +waitSXReliableACK KEYWORD2 +writeUint16SXBuffer KEYWORD2 +readUint16SXBuffer KEYWORD2 +readReliableErrors KEYWORD2 +readReliableFlags KEYWORD2 +readReliableConfig KEYWORD2 +setReliableConfig KEYWORD2 +clearReliableConfig KEYWORD2 +getReliableConfig KEYWORD2 +printReliableConfig KEYWORD2 +printReliableStatus KEYWORD2 +setReliableRX KEYWORD2 +CRCCCITTReliable KEYWORD2 +writeArray KEYWORD2 +printASCIIArray KEYWORD2 +getRXPayloadCRC KEYWORD2 +getTXPayloadCRC KEYWORD2 +getRXNetworkID KEYWORD2 +getTXNetworkID KEYWORD2 +isReceiveDone KEYWORD2 +isTransmitDone KEYWORD2 +printRegister KEYWORD2 +readTXIRQ KEYWORD2 +readRXIRQ KEYWORD2 +readTXPacketL KEYWORD2 +readCurrentRSSI KEYWORD2 +isRXdone KEYWORD2 +isTXdone KEYWORD2 +isRXdoneIRQ KEYWORD2 +isTXdoneIRQ KEYWORD2 +setTXDonePin KEYWORD2 +setRXDonePin KEYWORD2 +receiveIRQ KEYWORD2 +readPacketAddressed KEYWORD2 +transmitIRQ KEYWORD2 +returnOptimisation KEYWORD2 +getDeviceTemperature KEYWORD2 +fskCarrierOn KEYWORD2 +fskCarrierOff KEYWORD2 +setRfFrequencyDirect KEYWORD2 +getRfFrequencyRegisters KEYWORD2 +getPPM KEYWORD2 +printOCPTRIM KEYWORD2 +readBufferbytes KEYWORD2 +writeBufferbytes KEYWORD2 +setDevicePABOOST KEYWORD2 +setDeviceRFO KEYWORD2 +receiveSXBufferIRQ KEYWORD2 +transmitSXBufferIRQ KEYWORD2 +writeChar KEYWORD2 +readChar KEYWORD2 +transmitSXReliableIRQ KEYWORD2 +receiveSXReliableIRQ KEYWORD2 +sendSXReliableACKIRQ KEYWORD2 +waitSXReliableACKIRQ KEYWORD2 +transmitDT KEYWORD2 +receiveDT KEYWORD2 +sendACKDT KEYWORD2 +waitACKDT KEYWORD2 +transmitDTIRQ KEYWORD2 +sendACKDTIRQ KEYWORD2 +waitACKDTIRQ KEYWORD2 +receiveDTIRQ KEYWORD2 +set_REGTXMODULATION KEYWORD2 +readsavedPacketParam1 KEYWORD2 +readsavedPacketParam2 KEYWORD2 +readsavedPacketParam3 KEYWORD2 +readsavedPacketParam4 KEYWORD2 +readsavedPacketParam5 KEYWORD2 +setPayloadLength KEYWORD2 +setSyncWord2 KEYWORD2 +setSyncWord3 KEYWORD2 +setSyncWordErrors KEYWORD2 +setPeriodBase KEYWORD2 +getPeriodBase KEYWORD2 +lookupCalibrationValue KEYWORD2 +getSetCalibrationValue KEYWORD2 +getRangingRSSI KEYWORD2 +printArrayHEX KEYWORD2 +printArrayHEX KEYWORD2 +setupFLRC KEYWORD2 +setFLRCPayloadLengthReg KEYWORD2 +setLoRaPayloadLengthReg KEYWORD2 +getPacketType KEYWORD2 + diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..d860b03 --- /dev/null +++ b/library.properties @@ -0,0 +1,9 @@ +name=SX128xLT LoRa library +version=1.0.0 +author=Stuart Robinson +maintainer=sorry@no.spam +sentence=Arduino library for SX12xx LoRa device +paragraph=Supports SX1280,SX1281 devices +category=Communication +url=https://gitlab.kaust.edu.sa/hasanh/sx128x-lora +architectures=* diff --git a/src/AFSKRTTY.h b/src/AFSKRTTY.h new file mode 100644 index 0000000..e8cb1c8 --- /dev/null +++ b/src/AFSKRTTY.h @@ -0,0 +1,75 @@ +/* + Copyright 2020 - Stuart Robinson + Licensed under a MIT license displayed at the bottom of this document. + Original published 12/05/20 +*/ + +void startAFSKRTTY(int8_t audiopin, uint16_t freq, uint32_t afskleadinmS) +{ + tone(audiopin, freq); //lead in is high tone + delay(afskleadinmS); +} + + +void endAFSKRTTY(int8_t audiopin) +{ + delay(500); //500mS seconds of high tone to finish + noTone(audiopin); +} + + +void sendAFSKRTTY(uint8_t chartosend, int8_t audiopin, int8_t checkpin, uint16_t tonelowHz, uint16_t tonehighHz, uint32_t perioduS) +//send the byte in chartosend as AFSK RTTY, assumes mark condition (idle) is already present +//Format is 7 bits, no parity and 2 stop bits +{ + uint8_t numbits; + uint32_t startuS; + + startuS = micros(); + digitalWrite(checkpin, LOW); + tone(audiopin, tonelowHz); + + while ( (uint32_t) (micros() - startuS) < perioduS); //wait for start bit end + + for (numbits = 1; numbits <= 7; numbits++) //send 7 bits, LSB first + { + startuS = micros(); + if ((chartosend & 0x01) != 0) + { + digitalWrite(checkpin, HIGH); + tone(audiopin, tonehighHz); + } + else + { + digitalWrite(checkpin, LOW); + tone(audiopin, tonelowHz); //start 0 bit low tone + } + chartosend = (chartosend / 2); //get the next bit + while ( (uint32_t) (micros() - startuS) < perioduS); //wait bit period uS + } + perioduS = perioduS * 2; + startuS = micros(); + digitalWrite(checkpin, HIGH); //start mark condition + tone(audiopin, tonehighHz); //start high tone + while ( (uint32_t) (micros() - startuS) < perioduS); +} + + +/* + MIT license + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + diff --git a/src/AFSKRTTY2.h b/src/AFSKRTTY2.h new file mode 100644 index 0000000..98e1b1e --- /dev/null +++ b/src/AFSKRTTY2.h @@ -0,0 +1,180 @@ +/* + Copyright 2020 - Stuart Robinson + Licensed under a MIT license displayed at the bottom of this document. + Original published 18/12/20 +*/ + + +/* + This is the AFSK RTTY send library specifically for microcontrollers that do not have a tone() function + such as Arduino DUE and ESP32. Tones are generated here by manually toggling the selected tone pin with + a suitable delay between toggles. + + The baud rate and tone frequecies used can be adjusted by usin the startAFSKRTTY() command. + + The library has been tested and will work on an Arduion Pro Mini 8Mhz, Arduino DUE and ESP32 and when + used with this startup command; + + startAFSKRTTY(AUDIOOUTpin, CHECKpin, 5, 1000, 7, 714, 0, 1000); + + This outputs AFSKRTTY at 100baud, 7bit, no parity, 1stop bit, low tone 1000hz, hightone 1400hz. + + The audio comes out of the pin passed via AUDIOOUTpin and the bit timing can be checked by looking at + the CHECKpin on a scope or analyser. + + A low pass filter consiting of a 47K resistor and 470nF capacitor was used to redice the output for + feeding into a PC soundcard. + +*/ + + + +#define AFSKRTTY_UNUSED(v) (void) (v) //add AFSKRTTY_UNUSED(variable); in functions to avoid compiler warnings + +int8_t _audiopin, _checkpin, _adjust; +uint8_t _lowcycles, _highcycles; +uint32_t _lowperioduS, _highperioduS; + +void startAFSKRTTY(int8_t audiopin, int8_t checkpin, uint8_t lowcycles, uint16_t lowperioduS, uint8_t highcycles, uint16_t highperioduS, int16_t adjust, uint16_t leadinmS); +void sendAFSKRTTY(uint8_t chartosend); +void toneHigh(); +void toneLow(); + + +void startAFSKRTTY(int8_t audiopin, int8_t checkpin, uint8_t lowcycles, uint16_t lowperioduS, uint8_t highcycles, uint16_t highperioduS, int16_t adjust, uint16_t leadinmS) +{ + uint32_t startmS; + _audiopin = audiopin; + _checkpin = checkpin; + _lowperioduS = (lowperioduS / 2) + adjust; //period passed is equal to frequency, but delays are in two halfs + _highperioduS = (highperioduS / 2) + adjust; + _lowcycles = lowcycles; + _highcycles = highcycles; + _adjust = adjust; + + if (audiopin > 0) + { + pinMode(audiopin, OUTPUT); + } + + if (checkpin > 0) + { + pinMode(checkpin, OUTPUT); + } + + startmS = millis(); + + while ( (uint32_t) (millis() - startmS) < leadinmS) //allows for millis() overflow + { + digitalWrite(audiopin, HIGH); + delayMicroseconds(_highperioduS); + digitalWrite(audiopin, LOW); + delayMicroseconds(_highperioduS); + } +} + + +void endAFSKRTTY(int8_t audiopin, int8_t checkpin, uint16_t leadoutmS) +{ + uint32_t startmS; + + startmS = millis(); + + while ( (uint32_t) (millis() - startmS) < leadoutmS) //allows for millis() overflow + { + digitalWrite(audiopin, HIGH); + delayMicroseconds(_highperioduS); + digitalWrite(audiopin, LOW); + delayMicroseconds(_highperioduS); + } + + if (audiopin > 0) + { + pinMode(audiopin, INPUT); + } + + if (checkpin > 0) + { + pinMode(checkpin, INPUT); + } +} + + +void toneHigh() +{ + uint8_t index; + + for (index = 1; index <= _highcycles; index++) + { + digitalWrite(_audiopin, HIGH); + delayMicroseconds(_highperioduS); + digitalWrite(_audiopin, LOW); + delayMicroseconds(_highperioduS); + } + +} + + +void toneLow() +{ + uint8_t index; + + for (index = 1; index <= _lowcycles; index++) + { + digitalWrite(_audiopin, HIGH); + delayMicroseconds(_lowperioduS); + digitalWrite(_audiopin, LOW); + delayMicroseconds(_lowperioduS); + } + +} + + + +void sendAFSKRTTY(uint8_t chartosend) +//send the byte in chartosend as AFSK RTTY, assumes mark condition (idle) is already present +//Format is 7 bits, no parity and 2 stop bits +{ + + uint8_t numbits; + digitalWrite(_checkpin, LOW); + toneLow(); + + for (numbits = 1; numbits <= 7; numbits++) //send 7 bits, LSB first + { + if ((chartosend & 0x01) != 0) + { + digitalWrite(_checkpin, HIGH); + toneHigh(); + } + else + { + digitalWrite(_checkpin, LOW); + toneLow(); + } + chartosend = (chartosend / 2); //get the next bit + } + digitalWrite(_checkpin, HIGH); //start mark condition + toneHigh(); + toneHigh(); +} + + +/* + MIT license + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ + diff --git a/src/ARtransfer.h b/src/ARtransfer.h new file mode 100644 index 0000000..c1f8407 --- /dev/null +++ b/src/ARtransfer.h @@ -0,0 +1,1570 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 10/03/24 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a version of SX12xx library data tranfer functions that can transmit and + receive the contents of a memory array via LoRa. The receiver may receive the array directly into + memory or store the received data segments to an SD card. These array transfer functions assume that + ultimatly the array will be saved as a file somewhere, so a filename needs to be provided, even when + it may not be used. + + These functions expect the calling sketch to create an SX12XX library instance called LoRa, so that + SX12XX library functions are called like this; LoRa.getARTXNetworkID(). + + An example of a receiver program is 241_StuartCAM_ESP32CAM_LoRa_Receiver. + + Note that the receiver of the array sets up the memory array to recieve the transfer of a size that is + defined by the #define MAXarraysize. Clearly the receiver microcontroller has to have this amount of + memory available to use. + + Updated 18/01/22 to allow the port for Monitorport debug prints to be changed, all prints now goto + Monitorport. The actual port for prints is defined like this; + + #define Monitorport Serialx + + Updated 10/10/23 to correct issues with transfer of images > 65535 bytes long. + +*******************************************************************************************************/ + +//so that Monitorport prints default to the primary Monitorport port of Monitorport +#ifndef Monitorport +#define Monitorport Serial +#endif + +#define ARUNUSED(v) (void) (v) //add ARUNUSED(variable); to avoid compiler warnings + +//#define DEBUG //enable this define to show data transfer debug info +#include //part of SX12XX library + +//Variables used on transmitter and receiver +uint8_t ARRXPacketL; //length of received packet +uint8_t ARRXPacketType; //type of received packet, segment write, ACK, NACK etc +uint8_t ARRXHeaderL; //length of header +int16_t ARPacketRSSI; //stores RSSI of received packet +int8_t ARPacketSNR; //stores signal to noise ratio of received packet +uint16_t ARAckCount; //keep a count of acks that are received within timeout period +uint16_t ARNoAckCount; //keep a count of acks not received within timeout period +uint16_t ARDTSourceArrayCRC; //CRC returned of the remote received array +uint32_t ARDTSourceArrayLength; //length of file at source\transmitter +uint16_t ARDTDestinationArrayCRC; //CRC of complete array received +uint32_t ARDTDestinationArrayLength; //length of file\array written on the destination\receiver +uint32_t MAXarraysize; //maximum size or array that can be handled +uint32_t ARDTStartmS; //used for timeing transfers +uint16_t ARDTSegment = 0; //current segment number +char ARDTfilenamebuff[ARDTfilenamesize]; //global buffer to store filename +uint8_t ARDTheader[16]; //header array +uint8_t ARDTdata[245]; //data/segment array +uint8_t ARDTflags = 0; //Flags byte used to pass status information between nodes +int8_t ARDTLED = -1; //pin number for indicator LED, if -1 then not used +uint16_t ARDTErrors; //used for tracking errors in the transfer process +bool ARDTArrayTransferComplete; //bool to flag array transfer complete +uint32_t ARDTSendmS; //used for timing transfers +float ARDTsendSecs; //seconds to transfer a file +uint16_t ARDTNumberSegments; //number of segments for a file transfer +uint8_t ARDTLastSegmentSize; //size of the last segment +uint16_t ARLocalPayloadCRC; //for calculating the local data array CRC +uint8_t ARTXPacketL; //length of transmitted packet +uint16_t ARTXNetworkID; //this is used to store the 'network' number, receiver must have the same +uint16_t ARTXArrayCRC; //should contain CRC of data array transmitted +uint16_t ARDTSentSegments; //count of segments sent + +//Receive mode only variables +uint16_t ARRXErrors; //count of packets received with error +uint8_t ARRXFlags; //ARDTflags byte in header, could be used to control actions in TX and RX +uint8_t ARRXDataarrayL; //length of data array\segment +bool ARDTArrayStarted; //bool to flag when array write has started +bool ARDTArrayEnded; //bool to flag when array write has finished +bool ARDTArrayTimeout; //set true when there is a timeout waiting for transfer +uint16_t ARDTSegmentNext; //next segment expected +uint16_t ARDTReceivedSegments; //count of segments received +uint16_t ARDTSegmentLast; //last segment processed + +//A global pointer to the array and a variable for its length and current location are used so that all routines +//have access to the array to send without constantly passing the array pointer and variables between functions. +uint8_t *ptrARsendArray; //create a global pointer to the array to send, so all functions have access +uint8_t *ptrARreceivearray; //create a global pointer to the array to receive into, so all functions have access +uint32_t ARArrayLength; //length of array to send or receive +uint32_t ARarraylocation; //a global variable giving the location in the array last used + +//Transmitter mode functions +bool ARsendArray(uint8_t *ptrarray, uint32_t arraylength, char *filename, uint8_t namelength); +bool ARstartArrayTransfer(char *buff, uint8_t filenamesize); +bool ARsendSegments(); +bool ARsendArraySegment(uint16_t segnum, uint8_t segmentsize); +bool ARendArrayTransfer(char *buff, uint8_t filenamesize); +void ARbuild_DTArrayStartHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t arraylength, uint16_t arraycrc, uint8_t segsize); +void ARbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum); +void ARbuild_DTArrayEndHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t arraylength, uint16_t arraycrc, uint8_t segsize); +void ARprintLocalArrayDetails(); +void ARprintSeconds(); +void ARprintAckBrief(); +void ARprintAckReception(); +void ARprintACKdetail(); +void ARprintdata(uint8_t *dataarray, uint8_t arraysize); +uint16_t ARgetNumberSegments(uint32_t arraysize, uint8_t segmentsize); +uint8_t ARgetLastSegmentSize(uint32_t arraysize, uint8_t segmentsize); +bool ARsendDTInfo(); +void ARbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen); + +//Receiver mode functions +uint32_t ARreceiveArray(uint8_t *arraychar, uint32_t length, uint32_t receivetimeout); +bool ARreceivePacketDT(); +void ARreadHeaderDT(); +bool ARprocessPacket(uint8_t packettype); +bool ARprocessSegmentWrite(); +bool ARprocessArrayStart(uint8_t *buff, uint8_t filenamesize); +bool ARprocessArrayEnd(); +void ARprintSourceArrayDetails(); +void ARprintDestinationArrayDetails(); +uint16_t ARarrayCRC(uint8_t *buffer, uint32_t size, uint16_t startvalue); + +//Common functions +void ARsetDTLED(int8_t pinnumber); +void ARprintheader(uint8_t *hdr, uint8_t hdrsize); +void ARprintArrayHEX(uint8_t *buff, uint32_t len); +void ARprintReliableStatus(); +void ARprintPacketDetails(); + +//bit numbers used by ATDTErrors (16bits) and RXErrors (first 8bits) +const uint8_t ARNoFileSave = 0; //bit number of ATDTErrors to set when no file save, to SD for example +const uint8_t ARNothingToSend = 1; //bit number of ATDTErrors to set when nothing to send or unable to send image\file +const uint8_t ARNoCamera = 1; //bit number of ATDTErrors to set when camera fails +const uint8_t ARSendFile = 2; //bit number of ATDTErrors to set when file SD file image\file send fail +const uint8_t ARSendArray = 2; //bit number of ATDTErrors to set when file array image\file send fail +const uint8_t ARNoACKlimit = 3; //bit number of ATDTErrors to set when NoACK limit reached +const uint8_t ARSendPacket = 4; //bit number of ATDTErrors to set when sending a packet fails or there is no ack + +const uint8_t ARStartTransfer = 11; //bit number of ATDTErrors to set when StartTransfer fails +const uint8_t ARSendSegments = 12; //bit number of ATDTErrors to set when SendSegments function fails +const uint8_t ARSendSegment = 13; //bit number of ATDTErrors to set when sending a single Segment send fails +const uint8_t AROpeningFile = 14; //bit number of ATDTErrors to set when opening file fails +const uint8_t ARendTransfer = 15; //bit number of ATDTErrors to set when end transfer fails + + +//************************************************ +//Transmit mode functions +//************************************************ + +bool ARsendArray(uint8_t *ptrarray, uint32_t arraylength, char *filename, uint8_t namelength) +{ + //This routine allows the array transfer to be run with a function call of ARsendArray(). + + uint8_t localattempts = 0; + memcpy(ARDTfilenamebuff, filename, namelength); //copy the name of destination file into global filename array for use outside this function + ptrARsendArray = ptrarray; //set global pointer to array pointer passed + ARArrayLength = arraylength; // the length of array to send + ARDTSourceArrayCRC = 0; + ARDTSourceArrayLength = 0; + ARDTDestinationArrayCRC = 0; + ARDTDestinationArrayLength = 0; + + do + { + localattempts++; + ARNoAckCount = 0; + ARDTStartmS = millis(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Send array attempt ")); + Monitorport.println(localattempts); +#endif + + if (ARstartArrayTransfer(filename, namelength)) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Array write started OK on remote")); + ARprintLocalArrayDetails(); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("*************************")); + Monitorport.println(F("ERROR writing to remote array")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("*************************")); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + + delay(FunctionDelaymS); + + if (!ARsendSegments()) + { +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************")); + Monitorport.println(F("ERROR in sendSegments()")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("***********************")); + Monitorport.println(); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + + delay(FunctionDelaymS); + + if (ARendArrayTransfer(filename, namelength)) //send command to end remote array write + { + ARDTSendmS = millis() - ARDTStartmS; //record time taken for transfer + beginarrayRW(ARDTheader, 4); + ARDTDestinationArrayLength = arrayReadUint32(); + +#ifdef ENABLEMONITOR + Monitorport.println(F("Array write ended OK on remote")); + Monitorport.print(F("Acknowledged remote destination file length ")); + Monitorport.println(ARDTDestinationArrayLength); +#endif + + if (ARDTDestinationArrayLength != ARDTSourceArrayLength) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("************************************************")); + Monitorport.println(F("ERROR destination array and local array lengths do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("************************************************")); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + else + { + +#ifdef ENABLEMONITOR + Monitorport.println(F("Destination array and local array lengths match")); +#endif + } +#ifdef ENABLEARRAYCRC + ARDTDestinationArrayCRC = arrayReadUint16(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Acknowledged destination array CRC 0x")); + Monitorport.println(ARDTDestinationArrayCRC, HEX); +#endif + +#endif + if (ARDTDestinationArrayCRC != ARDTSourceArrayCRC) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("*********************************************")); + Monitorport.println(F("ERROR destination array and local array CRCs do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("*********************************************")); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Destination array and local array CRCs match")); +#endif + } + ARDTArrayTransferComplete = true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("******************************")); + Monitorport.println(F("ERROR ending destination array write")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("******************************")); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + } + while ((!ARDTArrayTransferComplete) && (localattempts < StartAttempts)); + + if (localattempts == StartAttempts) + { + bitSet(ARDTErrors, ARSendArray); + return false; + } + + ARDTsendSecs = (float) ARDTSendmS / 1000; + +#ifdef ENABLEMONITOR + Monitorport.print(F("ARNoAckCount ")); + Monitorport.println(ARNoAckCount); + Monitorport.print(F("Transmit time ")); + Monitorport.print(ARDTsendSecs, 3); + Monitorport.println(F("secs")); + Monitorport.print(F("Transmit rate ")); + Monitorport.print( (ARDTDestinationArrayLength * 8) / (ARDTsendSecs), 0 ); + Monitorport.println(F("bps")); + Monitorport.println(("Transfer finished")); +#endif + + return true; +} + + +bool ARstartArrayTransfer(char *buff, uint8_t filenamesize) +{ + //Start transfer of array to remote array or file + uint8_t ValidACK; + uint8_t localattempts = 0; + +#ifdef ENABLEMONITOR + Monitorport.print(F("Start array transfer for ")); + Monitorport.println(buff); +#endif + + ARDTSourceArrayLength = ARArrayLength; + + if (ARDTSourceArrayLength == 0) + { +#ifdef ENABLEMONITOR + Monitorport.print(F("Error - array 0 bytes ")); + Monitorport.println(buff); +#endif + + return false; + } + +#ifdef ENABLEARRAYCRC + ARDTSourceArrayCRC = ARarrayCRC((uint8_t *) ptrARsendArray, ARArrayLength, 0xFFFF); //get array CRC from position 0 to end +#endif + + ARDTNumberSegments = ARgetNumberSegments(ARDTSourceArrayLength, SegmentSize); + ARDTLastSegmentSize = ARgetLastSegmentSize(ARDTSourceArrayLength, SegmentSize); + ARbuild_DTArrayStartHeader(ARDTheader, DTArrayStartHeaderL, filenamesize, ARDTSourceArrayLength, ARDTSourceArrayCRC, SegmentSize); + ARLocalPayloadCRC = ARarrayCRC((uint8_t *) buff, filenamesize, 0xFFFF); + + do + { + localattempts++; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Send open remote file request")); +#endif + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + ARTXPacketL = LoRa.transmitDT(ARDTheader, DTArrayStartHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + + ARTXNetworkID = LoRa.getTXNetworkID(ARTXPacketL); //get the networkID appended to packet + ARTXArrayCRC = LoRa.getTXPayloadCRC(ARTXPacketL); //get the payload CRC thats was appended to packet + +#ifdef ENABLEMONITOR + Monitorport.print(F("Send attempt ")); + Monitorport.println(localattempts); + + if (ARTXPacketL == 0) //if there has been a send and ack error, ARTXPacketL returns as 0 + { + Monitorport.println(F("Transmit error")); + } +#endif + + ValidACK = LoRa.waitACKDT(ARDTheader, DTArrayStartHeaderL, ACKopentimeoutmS); + ARRXPacketType = ARDTheader[0]; + + if ((ValidACK > 0) && (ARRXPacketType == DTArrayStartACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("Valid ACK > ")); + ARprintArrayHEX(ARDTheader, ValidACK); //ValidACK is packet length +#endif +#endif + } + else + { + ARNoAckCount++; + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("NoACK")); +#endif +#endif + + if (ARNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); + Monitorport.println(); +#endif + + bitSet(ARDTErrors, ARNoACKlimit); + return false; + } + + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(ARDTErrors, ARStartTransfer); + return false; + } + return true; +} + + +bool ARsendSegments() +{ + //Start the array transfer at segment 0 + ARDTSegment = 0; + ARDTSentSegments = 0; + + ARarraylocation = 0; //start at first position in array + + while (ARDTSegment < (ARDTNumberSegments - 1)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + ARprintSeconds(); +#endif +#endif + + if (ARsendArraySegment(ARDTSegment, SegmentSize)) + { + ARDTSentSegments++; + } + else + { + return false; + } + delay(FunctionDelaymS); + }; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Last segment")); +#endif + + if (!ARsendArraySegment(ARDTSegment, ARDTLastSegmentSize)) + { + return false; + } + return true; +} + + +bool ARsendArraySegment(uint16_t segnum, uint8_t segmentsize) +{ + //Send array segment as payload in a data transfer packet + + uint8_t ValidACK; + uint8_t index; + uint8_t tempdata; + uint8_t localattempts = 0; + + for (index = 0; index < segmentsize; index++) + { + tempdata = ptrARsendArray[ARarraylocation]; + ARDTdata[index] = tempdata; + ARarraylocation++; + } + + ARbuild_DTSegmentHeader(ARDTheader, DTSegmentWriteHeaderL, segmentsize, segnum); + +#ifdef ENABLEMONITOR +#ifdef PRINTSEGMENTNUM + Monitorport.println(segnum); +#endif +#ifdef DEBUG + Monitorport.print(F(" ")); + ARprintheader(ARDTheader, DTSegmentWriteHeaderL); + Monitorport.print(F(" ")); + ARprintdata(ARDTdata, segmentsize); //print segment size of data array only + Monitorport.println(); +#endif +#endif + + do + { + localattempts++; + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + ARTXPacketL = LoRa.transmitDT(ARDTheader, DTSegmentWriteHeaderL, (uint8_t *) ARDTdata, segmentsize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + + if (ARTXPacketL == 0) //if there has been an error ARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDT(ARDTheader, DTSegmentWriteHeaderL, ACKsegtimeoutmS); + ARRXPacketType = ARDTheader[0]; + + if (ValidACK > 0) + { + if (ARRXPacketType == DTSegmentWriteNACK) + { + ARDTSegment = ARDTheader[4] + (ARDTheader[5] << 8); //load what the segment number should be + ARRXHeaderL = ARDTheader[2]; + ARarraylocation = ARDTSegment * SegmentSize; + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************************")); + Monitorport.print(F("Received restart request at segment ")); + Monitorport.println(ARDTSegment); +#ifdef DEBUG + ARprintheader(ARDTheader, ARRXHeaderL); +#endif + Monitorport.println(); + Monitorport.print(F("Seek to array location ")); + Monitorport.println(ARDTSegment * SegmentSize); + Monitorport.println(F("************************************")); + Monitorport.println(); + //Monitorport.flush(); +#endif + } + + if (ARRXPacketType == DTStartNACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Received restart request")); +#endif + + return false; + } + + //ack is valid, segment was acknowledged if here + + if (ARRXPacketType == DTSegmentWriteACK) + { + ARAckCount++; + ARDTSegment++; //increase value for next segment + return true; + } + } + else + { + ARNoAckCount++; + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("NoACK")); +#endif +#endif + + if (ARNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + + return false; + } + } + } while ((ValidACK == 0) && (localattempts < SendAttempts)) ; + + if (localattempts == SendAttempts) + { + bitSet(ARDTErrors, ARSendSegment); + return false; + } + + return true; +} + + +bool ARendArrayTransfer(char *buff, uint8_t filenamesize) +{ + //End array transfer + + uint8_t ValidACK; + uint8_t localattempts = 0; + + ARbuild_DTArrayEndHeader(ARDTheader, DTArrayEndHeaderL, filenamesize, ARDTSourceArrayLength, ARDTSourceArrayCRC, SegmentSize); + + do + { + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.println(F("Send end array write")); +#endif + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + ARTXPacketL = LoRa.transmitDT(ARDTheader, DTArrayEndHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + + ARTXNetworkID = LoRa.getTXNetworkID(ARTXPacketL); + ARTXArrayCRC = LoRa.getTXPayloadCRC(ARTXPacketL); + + if (ARTXPacketL == 0) //if there has been a send and ack error, ARARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDT(ARDTheader, DTArrayEndHeaderL, ACKclosetimeoutmS); + ARRXPacketType = ARDTheader[0]; + + if ((ValidACK > 0) && (ARRXPacketType == DTArrayEndACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("ACK header ")); + ARprintArrayHEX(ARDTheader, ValidACK); //ValidACK is packet length + Monitorport.println(); +#endif +#endif + } + else + { + ARNoAckCount++; + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("NoACK")); +#endif +#endif + + if (ARNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + return false; + } + +#ifdef ENABLEMONITOR +#ifdef DEBUG + ARprintArrayHEX(ARDTheader, ValidACK); //ValidACK is packet length +#endif + Monitorport.println(); +#endif + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)) ; + + + if (localattempts == SendAttempts) + { + bitSet(ARDTErrors, ARSendSegment); + return false; + } + return true; +} + + +void ARbuild_DTArrayStartHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t arraylength, uint16_t arraycrc, uint8_t segsize) +{ + //This builds the header buffer for the filename receiver might save the array as + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTArrayStart); //byte 0, write the packet type + arrayWriteUint8(ARDTflags); //byte 1, ARDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(arraylength); //byte 4,5,6,7, write the array length + arrayWriteUint16(arraycrc); //byte 8, 9, write array CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void ARbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum) +{ + //This builds the header buffer for a segment transmit + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTSegmentWrite); //write the packet type + arrayWriteUint8(ARDTflags); //initial ARDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint16(segnum); //write the DTsegment number + endarrayRW(); +} + + +void ARbuild_DTArrayEndHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t arraylength, uint16_t arraycrc, uint8_t segsize) +{ + //This builds the header buffer for the end remote array write + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTArrayEnd); //byte 0, write the packet type + arrayWriteUint8(ARDTflags); //byte 1, initial ARDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(arraylength); //byte 4,5,6,7, write the array length + arrayWriteUint16(arraycrc); //byte 8, 9, write array CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void ARprintLocalArrayDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Source array length ")); + Monitorport.print(ARDTSourceArrayLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEARRAYCRC + Monitorport.print(F("1 Source array CRC is 0x")); + Monitorport.println(ARDTSourceArrayCRC, HEX); +#endif + Monitorport.print(F("Segment Size ")); + Monitorport.println(SegmentSize); + Monitorport.print(F("Number segments ")); + Monitorport.println(ARDTNumberSegments); + Monitorport.print(F("Last segment size ")); + Monitorport.println(ARDTLastSegmentSize); +#endif +} + + +void ARprintSeconds() +{ +#ifdef ENABLEMONITOR + float secs; + secs = ( (float) millis() / 1000); + Monitorport.print(secs, 2); + Monitorport.print(F(" ")); +#endif +} + + +void ARprintAckBrief() +{ +#ifdef ENABLEMONITOR + ARPacketRSSI = LoRa.readPacketRSSI(); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(ARPacketRSSI); + Monitorport.print(F("dBm")); +#endif +} + + +void ARprintAckReception() +{ +#ifdef ENABLEMONITOR + ARPacketRSSI = LoRa.readPacketRSSI(); + ARPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F("ARAckCount,")); + Monitorport.print(ARAckCount); + Monitorport.print(F(",ARNoAckCount,")); + Monitorport.print(ARNoAckCount); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(ARPacketRSSI); + Monitorport.print(F("dBm,AckSNR,")); + Monitorport.print(ARPacketSNR); + Monitorport.print(F("dB")); + Monitorport.println(); +#endif +} + + +void ARprintACKdetail() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("ACKDetail")); + Monitorport.print(F(",RXNetworkID,0x")); + Monitorport.print(LoRa.getRXNetworkID(ARRXPacketL), HEX); + Monitorport.print(F(",RXPayloadCRC,0x")); + Monitorport.print(LoRa.getRXPayloadCRC(ARRXPacketL), HEX); + Monitorport.print(F(",ARRXPacketL,")); + Monitorport.print(ARRXPacketL); + Monitorport.print(F(" ")); + ARprintReliableStatus(); + Monitorport.println(); +#endif +} + + +void ARprintdata(uint8_t *dataarray, uint8_t arraysize) +{ + ARUNUSED(dataarray); + ARUNUSED(arraysize); + +#ifdef ENABLEMONITOR + Monitorport.print(F("DataBytes,")); + Monitorport.print(arraysize); + Monitorport.print(F(" ")); + ARprintArrayHEX((uint8_t *) dataarray, 16); //There is a lot of data to print so only print first 16 bytes +#endif +} + + +uint16_t ARgetNumberSegments(uint32_t arraysize, uint8_t segmentsize) +{ + uint16_t segments; + segments = arraysize / segmentsize; + + if ((arraysize % segmentsize) > 0) + { + segments++; + } + return segments; +} + + +uint8_t ARgetLastSegmentSize(uint32_t arraysize, uint8_t segmentsize) +{ + uint8_t lastsize; + + lastsize = arraysize % segmentsize; + if (lastsize == 0) + { + lastsize = segmentsize; + } + return lastsize; +} + + +bool ARsendDTInfo() +{ + //Send array info packet, for this implmentation its really only the flags in ARDTflags that is sent + + uint8_t ValidACK = 0; + uint8_t localattempts = 0; + + ARbuild_DTInfoHeader(ARDTheader, DTInfoHeaderL, 0); + + do + { + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.print(F("Send DTInfo packet attempt ")); + Monitorport.println(localattempts); +#endif + ARTXPacketL = LoRa.transmitDT(ARDTheader, DTInfoHeaderL, (uint8_t *) ARDTdata, 0, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (ARTXPacketL == 0) //if there has been an error ARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + continue; + } + + ValidACK = LoRa.waitACKDT(ARDTheader, DTInfoHeaderL, ACKsegtimeoutmS); + + if (ValidACK > 0) + { + //ack is a valid relaible packet + ARRXPacketType = ARDTheader[0]; +#ifdef ENABLEMONITOR + Monitorport.print(F("ACK Packet type 0x")); + Monitorport.println(ARRXPacketType, HEX); +#endif + + if (ARRXPacketType == DTInfoACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK received")); +#endif + ARAckCount++; + ARRXPacketType = ARDTheader[0]; + return true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK not received")); +#endif + return false; + } + + } + else + { + ARNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("No valid ACK received ")); +#endif + + if (ARNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + return false; + } + } + delay(PacketDelaymS); + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(ARDTErrors, ARSendPacket); + return false; + } + return true; +} + + +void ARbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen) +{ + //This builds the header buffer for a info only header, faults problems etc + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTInfo); //write the packet type + arrayWriteUint8(ARDTflags); //ARDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + endarrayRW(); +} + + +//************************************************ +//Receiver mode functions +//************************************************ + +uint32_t ARreceiveArray(uint8_t *ptrarray, uint32_t length, uint32_t receivetimeout) +{ + //returns 0 if no ARDTArrayEnded set, returns length of array if received + uint32_t startmS = millis(); + + ptrARreceivearray = ptrarray; //set global pointer to array pointer passed + MAXarraysize = length; + ARDTArrayTimeout = false; + ARDTArrayEnded = false; + ARDTDestinationArrayLength = 0; + + do + { + if (ARreceivePacketDT()) + { + startmS = millis(); + } + + if (ARDTArrayEnded) //has the end array transfer been received ? + { + return ARDTDestinationArrayLength; + } + } + while (((uint32_t) (millis() - startmS) < receivetimeout )); + + + if (ARDTArrayEnded) //has the end array transfer been received ? + { + return ARDTDestinationArrayLength; + } + else + { + ARDTArrayTimeout = true; + return 0; + } +} + + +bool ARreceivePacketDT() +{ + //Receive data transfer packets + + ARRXPacketType = 0; + ARRXPacketL = LoRa.receiveDT(ARDTheader, HeaderSizeMax, (uint8_t *) ARDTdata, DataSizeMax, NetworkID, RXtimeoutmS, WAIT_RX); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + +#ifdef ENABLEMONITOR +#ifdef DEBUG + ARprintSeconds(); +#endif +#endif + if (ARRXPacketL > 0) + { + //if the LoRa.receiveDT() returns a value > 0 for ARRXPacketL then packet was received OK + //then only action payload if destinationNode = thisNode + ARreadHeaderDT(); //get the basic header details into global variables ARRXPacketType etc + ARprocessPacket(ARRXPacketType); //process and act on the packet + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return true; + } + else + { + //if the LoRa.receiveDT() function detects an error RXOK is 0 + + uint16_t IRQStatus = LoRa.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Monitorport.println(F("RX Timeout")); + } + else + { + ARRXErrors++; + +#ifdef ENABLEMONITOR + Monitorport.print(F("PacketError")); + ARprintPacketDetails(); + ARprintReliableStatus(); + Monitorport.print(F(",IRQreg,0x")); + Monitorport.println(LoRa.readIrqStatus(), HEX); + //Monitorport.println(); +#endif + } + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return false; + } +} + + +void ARreadHeaderDT() +{ + //The first 6 bytes of the header contain the important stuff, so load it up + //so we can decide what to do next. + beginarrayRW(ARDTheader, 0); //start buffer read at location 0 + ARRXPacketType = arrayReadUint8(); //load the packet type + ARRXFlags = arrayReadUint8(); //ARDTflags byte + ARRXHeaderL = arrayReadUint8(); //load the header length + ARRXDataarrayL = arrayReadUint8(); //load the datalength + ARDTSegment = arrayReadUint16(); //load the segment number +} + + +bool ARprocessPacket(uint8_t packettype) +{ + //Decide what to do with an incoming packet + + if (packettype == DTSegmentWrite) + { + ARprocessSegmentWrite(); + return true; + } + + if (packettype == DTArrayStart) + { + ARprocessArrayStart(ARDTdata, ARRXDataarrayL); //ARDTdata contains the filename + return true; + } + + if (packettype == DTArrayEnd) + { + ARprocessArrayEnd(); + return true; + } + + return true; +} + + +bool ARprocessSegmentWrite() +{ + //There is a request to write a segment to array on receiver + //checks that the sequence of segment writes is correct + + uint8_t index, byteswritten = 0; + + if (!ARDTArrayStarted) + { + //something is wrong, have received a request to write a segment but there is no array + //write in progresss so need to reject the segment write with a restart NACK + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("*************************************************************")); + Monitorport.println(F("Error - Segment write with no array write started - send NACK")); + Monitorport.println(F("*************************************************************")); + Monitorport.println(); +#endif + + ARDTheader[0] = DTStartNACK; + delay(ACKdelaymS); + delay(DuplicatedelaymS); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + LoRa.sendACKDT(ARDTheader, DTStartHeaderL, TXpower); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return false; + } + + if (ARDTSegment == ARDTSegmentNext) + { + //segment to write is as expected + + for (index = 0; index < ARRXDataarrayL; index++) + { + ptrARreceivearray[ARarraylocation] = ARDTdata[index]; + ARarraylocation++; + byteswritten++; + } + +#ifdef ENABLEMONITOR +#ifdef PRINTSEGMENTNUM + Monitorport.println(ARDTSegment); +#endif +#endif + + ARDTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + LoRa.sendACKDT(ARDTheader, DTSegmentWriteHeaderL, TXpower); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + ARDTReceivedSegments++; + ARDTSegmentLast = ARDTSegment; //so we can tell if sequece has been received twice + ARDTSegmentNext = ARDTSegment + 1; + return true; + } + + if (ARDTSegment == ARDTSegmentLast) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("ERROR segment ")); + Monitorport.print(ARDTSegment); + Monitorport.println(F(" already received ")); +#endif +#endif + + ARDTheader[0] = DTSegmentWriteACK; + delay(DuplicatedelaymS); + delay(ACKdelaymS); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + LoRa.sendACKDT(ARDTheader, DTSegmentWriteHeaderL, TXpower); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return true; + } + + if (ARDTSegment != ARDTSegmentNext ) + { + ARDTheader[0] = DTSegmentWriteNACK; + ARDTheader[4] = lowByte(ARDTSegmentNext); + ARDTheader[5] = highByte(ARDTSegmentNext); + delay(ACKdelaymS); + delay(DuplicatedelaymS); //add an extra delay here to stop repeated segment sends + +#ifdef ENABLEMONITOR + Monitorport.print(F(" ERROR Received Segment ")); + Monitorport.print(ARDTSegment); + Monitorport.print(F(" expected ")); + Monitorport.print(ARDTSegmentNext); + Monitorport.print(F(" ")); + Monitorport.print(F(" Send NACK for segment ")); + Monitorport.print(ARDTSegmentNext); + Monitorport.println(); + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.print(F("Transmit restart request for segment ")); + Monitorport.println(ARDTSegmentNext); +#ifdef DEBUG + ARprintheader(ARDTheader, ARRXHeaderL); +#endif + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.println(); + //Monitorport.flush(); +#endif + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + LoRa.sendACKDT(ARDTheader, DTSegmentWriteHeaderL, TXpower); + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return false; + } + + return true; +} + + +bool ARprocessArrayStart(uint8_t *buff, uint8_t filenamesize) +{ + //There is a request to start writing to a local array on receiver + //The transmitter will have passed a filename to be used when saving the array to SD + //or for use when transferring the array elsewhere + + ARDTArrayStarted = false; //until this function completes the Array transfer has not started + ARDTArrayEnded = false; //and it cannot hve completed either ...... + ARarraylocation = 0; + beginarrayRW(ARDTheader, 4); //start buffer read at location 4 + ARDTSourceArrayLength = arrayReadUint32(); //load the array length being sent + + Monitorport.println(F("Start array transfer")); + + if (ARDTSourceArrayLength > MAXarraysize) + { + Monitorport.print(F("Array length to transfer ")); + Monitorport.print(ARDTSourceArrayLength); + Monitorport.println(F(" bytes")); + Monitorport.println(F("ERROR - Not enough memory allocated")); + Monitorport.print(F("MAXarraysize is defined as ")); + Monitorport.print(MAXarraysize); + Monitorport.println(F(" bytes")); + Monitorport.println(); + return false; + } + + + ARDTSourceArrayCRC = arrayReadUint16(); //load the CRC of the array being sent + memset(ARDTfilenamebuff, 0, ARDTfilenamesize); //clear ARDTfilenamebuff to all 0s + memcpy(ARDTfilenamebuff, buff, filenamesize); //copy received ARDTdata into ARDTfilenamebuff, the array should have a destination filename + +#ifdef ENABLEMONITOR + Monitorport.print((char*) ARDTfilenamebuff); + Monitorport.println(F(" Array start write request")); +#ifdef DEBUG + Monitorport.print(F("Header > ")); + ARprintArrayHEX(ARDTheader, HeaderSizeMax); +#endif + Monitorport.println(); + ARprintSourceArrayDetails(); + + if bitRead(ARRXFlags, ARNoFileSave) + { + Monitorport.println(F("Remote did not save file to SD")); + } +#endif + ARDTStartmS = millis(); + delay(ACKdelaystartendmS); //there needs to be a dealy here, to wait for receiver to be ready + + ARDTheader[0] = DTArrayStartACK; //set the ACK packet type + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + LoRa.sendACKDT(ARDTheader, DTArrayStartHeaderL, TXpower); + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + ARDTSegmentNext = 0; //after a\rray write start open, segment 0 is next + + ARDTArrayStarted = true; + + return true; +} + + +bool ARprocessArrayEnd() +{ + // There is a request to end writing to an array on receiver +#ifdef ENABLEMONITOR + Monitorport.print((char*) ARDTfilenamebuff); + Monitorport.println(F(" end array write request")); +#endif + + if (ARDTArrayStarted) //check if array write had been started, end it if it is + { + ARDTArrayStarted = false; + ARDTDestinationArrayLength = ARarraylocation; + +#ifdef ENABLEMONITOR + Monitorport.print(F("ARDTDestinationArrayLength ")); + Monitorport.println(ARDTDestinationArrayLength); +#ifdef ENABLEARRAYCRC + ARDTDestinationArrayCRC = ARarrayCRC(ptrARreceivearray, ARDTDestinationArrayLength, 0xFFFF); + Monitorport.print(F("Destination arrayCRC 0x")); + Monitorport.println(ARDTDestinationArrayCRC, HEX); +#endif +#endif + + beginarrayRW(ARDTheader, 4); //start writing to array at location 12 + arrayWriteUint32(ARDTDestinationArrayLength); //write array length of array just written just written to ACK header + arrayWriteUint16(ARDTDestinationArrayCRC); //write CRC of array just written to ACK header + +#ifdef ENABLEMONITOR + Monitorport.println(F("Array write ended")); + Monitorport.print(F("Transfer time ")); + Monitorport.print(millis() - ARDTStartmS); + Monitorport.print(F("mS")); + Monitorport.println(); + ARprintDestinationArrayDetails(); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Array write already ended")); +#endif + delay(DuplicatedelaymS); + } + + delay(ACKdelaystartendmS); + ARDTheader[0] = DTArrayEndACK; + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + LoRa.sendACKDT(ARDTheader, DTArrayEndHeaderL, TXpower); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + + ARDTArrayEnded = true; + return true; +} + + +uint16_t ARarrayCRC(uint8_t *buffer, uint32_t size, uint16_t startvalue) +{ + uint32_t index; + uint16_t libraryCRC; + uint8_t j; + + libraryCRC = startvalue; //start value for CRC16 + + for (index = 0; index < size; index++) + { + libraryCRC ^= (((uint16_t)buffer[index]) << 8); + for (j = 0; j < 8; j++) + { + if (libraryCRC & 0x8000) + libraryCRC = (libraryCRC << 1) ^ 0x1021; + else + libraryCRC <<= 1; + } + } + + return libraryCRC; +} + + +void ARprintSourceArrayDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(ARDTfilenamebuff); + Monitorport.print(F(" Source array length is ")); + Monitorport.print(ARDTSourceArrayLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEARRAYCRC + Monitorport.print(F("2 Source array CRC is 0x")); + Monitorport.println(ARDTSourceArrayCRC, HEX); +#endif +#endif +} + + +void ARprintDestinationArrayDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Destination array length ")); + Monitorport.print(ARDTDestinationArrayLength); + Monitorport.println(F(" bytes")); + Monitorport.print(F("Source array length ")); + Monitorport.print(ARDTSourceArrayLength); + Monitorport.println(F(" bytes")); + + if (ARDTDestinationArrayLength != ARDTSourceArrayLength) + { + Monitorport.println(F("ERROR - array lengths do not match")); + } + else + { + Monitorport.println(F("Array lengths match")); + } + +#ifdef ENABLEARRAYCRC + Monitorport.print(F("Destination array CRC is 0x")); + Monitorport.println(ARDTDestinationArrayCRC, HEX); + Monitorport.print(F("3 Source array CRC is 0x")); + Monitorport.println(ARDTSourceArrayCRC, HEX); + + if (ARDTDestinationArrayCRC != ARDTSourceArrayCRC) + { + Monitorport.println(F("ERROR - array CRCs do not match")); + } + else + { + Monitorport.println(F("Array CRCs match")); + } +#endif +#endif +} + + +//************************************************ +//Common functions +//************************************************ + +void ARprintArrayHEX(uint8_t *buff, uint32_t len) +{ + ARUNUSED(buff); + ARUNUSED(len); + +#ifdef ENABLEMONITOR + uint32_t index, buffdata; + + for (index = 0; index < len; index++) + { + buffdata = buff[index]; + if (buffdata < 16) + { + Monitorport.print(F("0")); + } + Monitorport.print(buffdata, HEX); + Monitorport.print(F(" ")); + } +#endif +} + + +void ARsetDTLED(int8_t pinnumber) +{ + //give the data transfer routines an LED to flash + if (pinnumber >= 0) + { + ARDTLED = pinnumber; + pinMode(pinnumber, OUTPUT); + } +} + + +void ARprintheader(uint8_t *hdr, uint8_t hdrsize) +{ + ARUNUSED(hdr); + ARUNUSED(hdrsize); + +#ifdef ENABLEMONITOR + Monitorport.print(F("HeaderBytes,")); + Monitorport.print(hdrsize); + Monitorport.print(F(" ")); + ARprintArrayHEX(hdr, hdrsize); +#endif +} + + +void ARprintPacketDetails() +{ +#ifdef ENABLEMONITOR +#ifdef DEBUG + ARPacketRSSI = LoRa.readPacketRSSI(); + ARPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F(" RSSI,")); + Monitorport.print(ARPacketRSSI); + Monitorport.print(F("dBm")); + Monitorport.print(F(",SNR,")); + Monitorport.print(ARPacketSNR); + Monitorport.print(F("dBm,RXOKCount,")); + Monitorport.print(ARDTReceivedSegments); + Monitorport.print(F(",RXErrs,")); + Monitorport.print(ARRXErrors); + Monitorport.print(F(" RX")); + ARprintheader(ARDTheader, ARRXHeaderL); +#endif +#endif +} + + +void ARprintReliableStatus() +{ + +#ifdef ENABLEMONITOR + + uint8_t reliableErrors = LoRa.readReliableErrors(); + uint8_t reliableFlags = LoRa.readReliableFlags(); + + if (bitRead(reliableErrors, ReliableCRCError)) + { + Monitorport.print(F(",ReliableCRCError")); + } + + if (bitRead(reliableErrors, ReliableIDError)) + { + Monitorport.print(F(",ReliableIDError")); + } + + if (bitRead(reliableErrors, ReliableSizeError)) + { + Monitorport.print(F(",ReliableSizeError")); + } + + if (bitRead(reliableErrors, ReliableACKError)) + { + Monitorport.print(F(",NoReliableACK")); + } + + if (bitRead(reliableErrors, ReliableTimeout)) + { + Monitorport.print(F(",ReliableTimeout")); + } + + if (bitRead(reliableFlags, ReliableACKSent)) + { + Monitorport.print(F(",ACKsent")); + } + + if (bitRead(reliableFlags, ReliableACKReceived)) + { + Monitorport.print(F(",ACKreceived")); + } +#endif +} diff --git a/src/ARtransferIRQ.h b/src/ARtransferIRQ.h new file mode 100644 index 0000000..3a0fde0 --- /dev/null +++ b/src/ARtransferIRQ.h @@ -0,0 +1,1571 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 10/03/24 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/******************************************************************************************************* + Program Operation - This is a version of SX12xx library data tranfer functions that can transmit and + receive the contents of a memory array via LoRa. The receiver may receive the array directly into + memory or store the received data segments to an SD card. These array transfer functions assume that + ultimatly the array will be saved as a file somewhere, so a filename needs to be provided, even when + it may not be used. This ARtransferIRQ.h library file is for the SX12XX library when the LoRa device + is used without the DIOO pin (SX127x) or DIO1 (SX126X and SX128X) parts. + + These functions expect the calling sketch to create an SX12XX library instance called LoRa, so that + SX12XX library functions are called like this; LoRa.getARTXNetworkID(). + + An example of a receiver program is 241_StuartCAM_ESP32CAM_LoRa_Receiver. + + Note that the receiver of the array sets up the memory array to recieve the transfer of a size that is + defined by the #define MAXarraysize. Clearly the receiver microcontroller has to have this amount of + memory available to use. + + Updated 18/01/22 to allow the port for Monitorport debug prints to be changed, all prints now goto + Monitorport. The actual port for prints is defined like this; + + #define Monitorport Serialx + + Updated 10/10/23 to correct issues with transfer of images > 65535 bytes long. + +*******************************************************************************************************/ + +//so that Monitorport prints default to the primary Monitorport port of Monitorport +#ifndef Monitorport +#define Monitorport Serial +#endif + +#define ARUNUSED(v) (void) (v) //add ARUNUSED(variable); to avoid compiler warnings + +//#define DEBUG //enable this define to show data transfer debug info +#include //part of SX12XX library + +//Variables used on transmitter and receiver +uint8_t ARRXPacketL; //length of received packet +uint8_t ARRXPacketType; //type of received packet, segment write, ACK, NACK etc +uint8_t ARRXHeaderL; //length of header +int16_t ARPacketRSSI; //stores RSSI of received packet +int8_t ARPacketSNR; //stores signal to noise ratio of received packet +uint16_t ARAckCount; //keep a count of acks that are received within timeout period +uint16_t ARNoAckCount; //keep a count of acks not received within timeout period +uint16_t ARDTSourceArrayCRC; //CRC returned of the remote received array +uint32_t ARDTSourceArrayLength; //length of file at source\transmitter +uint16_t ARDTDestinationArrayCRC; //CRC of complete array received +uint32_t ARDTDestinationArrayLength; //length of file\array written on the destination\receiver +uint32_t MAXarraysize; //maximum size or array that can be handled +uint32_t ARDTStartmS; //used for timeing transfers +uint16_t ARDTSegment = 0; //current segment number +char ARDTfilenamebuff[ARDTfilenamesize]; //global buffer to store filename +uint8_t ARDTheader[16]; //header array +uint8_t ARDTdata[245]; //data/segment array +uint8_t ARDTflags = 0; //Flags byte used to pass status information between nodes +int8_t ARDTLED = -1; //pin number for indicator LED, if -1 then not used +uint16_t ARDTErrors; //used for tracking errors in the transfer process +bool ARDTArrayTransferComplete; //bool to flag array transfer complete +uint32_t ARDTSendmS; //used for timing transfers +float ARDTsendSecs; //seconds to transfer a file +uint16_t ARDTNumberSegments; //number of segments for a file transfer +uint8_t ARDTLastSegmentSize; //size of the last segment +uint16_t ARLocalPayloadCRC; //for calculating the local data array CRC +uint8_t ARTXPacketL; //length of transmitted packet +uint16_t ARTXNetworkID; //this is used to store the 'network' number, receiver must have the same +uint16_t ARTXArrayCRC; //should contain CRC of data array transmitted +uint16_t ARDTSentSegments; //count of segments sent + +//Receive mode only variables +uint16_t ARRXErrors; //count of packets received with error +uint8_t ARRXFlags; //ARDTflags byte in header, could be used to control actions in TX and RX +uint8_t ARRXDataarrayL; //length of data array\segment +bool ARDTArrayStarted; //bool to flag when array write has started +bool ARDTArrayEnded; //bool to flag when array write has finished +bool ARDTArrayTimeout; //set true when there is a timeout waiting for transfer +uint16_t ARDTSegmentNext; //next segment expected +uint16_t ARDTReceivedSegments; //count of segments received +uint16_t ARDTSegmentLast; //last segment processed + +//A global pointer to the array and a variable for its length and current location are used so that all routines +//have access to the array to send without constantly passing the array pointer and variables between functions. +uint8_t *ptrARsendArray; //create a global pointer to the array to send, so all functions have access +uint8_t *ptrARreceivearray; //create a global pointer to the array to receive into, so all functions have access +uint32_t ARArrayLength; //length of array to send or receive +uint32_t ARarraylocation; //a global variable giving the location in the array last used + +//Transmitter mode functions +bool ARsendArray(uint8_t *ptrarray, uint32_t arraylength, char *filename, uint8_t namelength); +bool ARstartArrayTransfer(char *buff, uint8_t filenamesize); +bool ARsendSegments(); +bool ARsendArraySegment(uint16_t segnum, uint8_t segmentsize); +bool ARendArrayTransfer(char *buff, uint8_t filenamesize); +void ARbuild_DTArrayStartHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t arraylength, uint16_t arraycrc, uint8_t segsize); +void ARbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum); +void ARbuild_DTArrayEndHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t arraylength, uint16_t arraycrc, uint8_t segsize); +void ARprintLocalArrayDetails(); +void ARprintSeconds(); +void ARprintAckBrief(); +void ARprintAckReception(); +void ARprintACKdetail(); +void ARprintdata(uint8_t *dataarray, uint8_t arraysize); +uint16_t ARgetNumberSegments(uint32_t arraysize, uint8_t segmentsize); +uint8_t ARgetLastSegmentSize(uint32_t arraysize, uint8_t segmentsize); +bool ARsendDTInfo(); +void ARbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen); + +//Receiver mode functions +uint32_t ARreceiveArray(uint8_t *arraychar, uint32_t length, uint32_t receivetimeout); +bool ARreceivePacketDT(); +void ARreadHeaderDT(); +bool ARprocessPacket(uint8_t packettype); +bool ARprocessSegmentWrite(); +bool ARprocessArrayStart(uint8_t *buff, uint8_t filenamesize); +bool ARprocessArrayEnd(); +void ARprintSourceArrayDetails(); +void ARprintDestinationArrayDetails(); +uint16_t ARarrayCRC(uint8_t *buffer, uint32_t size, uint16_t startvalue); + +//Common functions +void ARsetDTLED(int8_t pinnumber); +void ARprintheader(uint8_t *hdr, uint8_t hdrsize); +void ARprintArrayHEX(uint8_t *buff, uint32_t len); +void ARprintReliableStatus(); +void ARprintPacketDetails(); + +//bit numbers used by ATDTErrors (16bits) and RXErrors (first 8bits) +const uint8_t ARNoFileSave = 0; //bit number of ATDTErrors to set when no file save, to SD for example +const uint8_t ARNothingToSend = 1; //bit number of ATDTErrors to set when nothing to send or unable to send image\file +const uint8_t ARNoCamera = 1; //bit number of ATDTErrors to set when camera fails +const uint8_t ARSendFile = 2; //bit number of ATDTErrors to set when file SD file image\file send fail +const uint8_t ARSendArray = 2; //bit number of ATDTErrors to set when file array image\file send fail +const uint8_t ARNoACKlimit = 3; //bit number of ATDTErrors to set when NoACK limit reached +const uint8_t ARSendPacket = 4; //bit number of ATDTErrors to set when sending a packet fails or there is no ack + +const uint8_t ARStartTransfer = 11; //bit number of ATDTErrors to set when StartTransfer fails +const uint8_t ARSendSegments = 12; //bit number of ATDTErrors to set when SendSegments function fails +const uint8_t ARSendSegment = 13; //bit number of ATDTErrors to set when sending a single Segment send fails +const uint8_t AROpeningFile = 14; //bit number of ATDTErrors to set when opening file fails +const uint8_t ARendTransfer = 15; //bit number of ATDTErrors to set when end transfer fails + + +//************************************************ +//Transmit mode functions +//************************************************ + +bool ARsendArray(uint8_t *ptrarray, uint32_t arraylength, char *filename, uint8_t namelength) +{ + //This routine allows the array transfer to be run with a function call of ARsendArray(). + + uint8_t localattempts = 0; + memcpy(ARDTfilenamebuff, filename, namelength); //copy the name of destination file into global filename array for use outside this function + ptrARsendArray = ptrarray; //set global pointer to array pointer passed + ARArrayLength = arraylength; // the length of array to send + ARDTSourceArrayCRC = 0; + ARDTSourceArrayLength = 0; + ARDTDestinationArrayCRC = 0; + ARDTDestinationArrayLength = 0; + + do + { + localattempts++; + ARNoAckCount = 0; + ARDTStartmS = millis(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Send array attempt ")); + Monitorport.println(localattempts); +#endif + + if (ARstartArrayTransfer(filename, namelength)) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Array write started OK on remote")); + ARprintLocalArrayDetails(); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("*************************")); + Monitorport.println(F("ERROR writing to remote array")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("*************************")); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + + delay(FunctionDelaymS); + + if (!ARsendSegments()) + { +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************")); + Monitorport.println(F("ERROR in sendSegments()")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("***********************")); + Monitorport.println(); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + + delay(FunctionDelaymS); + + if (ARendArrayTransfer(filename, namelength)) //send command to end remote array write + { + ARDTSendmS = millis() - ARDTStartmS; //record time taken for transfer + beginarrayRW(ARDTheader, 4); + ARDTDestinationArrayLength = arrayReadUint32(); + +#ifdef ENABLEMONITOR + Monitorport.println(F("Array write ended OK on remote")); + Monitorport.print(F("Acknowledged remote destination file length ")); + Monitorport.println(ARDTDestinationArrayLength); +#endif + + if (ARDTDestinationArrayLength != ARDTSourceArrayLength) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("************************************************")); + Monitorport.println(F("ERROR destination array and local array lengths do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("************************************************")); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + else + { + +#ifdef ENABLEMONITOR + Monitorport.println(F("Destination array and local array lengths match")); +#endif + } +#ifdef ENABLEARRAYCRC + ARDTDestinationArrayCRC = arrayReadUint16(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Acknowledged destination array CRC 0x")); + Monitorport.println(ARDTDestinationArrayCRC, HEX); +#endif + +#endif + if (ARDTDestinationArrayCRC != ARDTSourceArrayCRC) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("*********************************************")); + Monitorport.println(F("ERROR destination array and local array CRCs do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("*********************************************")); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Destination array and local array CRCs match")); +#endif + } + ARDTArrayTransferComplete = true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("******************************")); + Monitorport.println(F("ERROR ending destination array write")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("******************************")); +#endif + + ARDTArrayTransferComplete = false; + continue; + } + } + while ((!ARDTArrayTransferComplete) && (localattempts < StartAttempts)); + + if (localattempts == StartAttempts) + { + bitSet(ARDTErrors, ARSendArray); + return false; + } + + ARDTsendSecs = (float) ARDTSendmS / 1000; + +#ifdef ENABLEMONITOR + Monitorport.print(F("ARNoAckCount ")); + Monitorport.println(ARNoAckCount); + Monitorport.print(F("Transmit time ")); + Monitorport.print(ARDTsendSecs, 3); + Monitorport.println(F("secs")); + Monitorport.print(F("Transmit rate ")); + Monitorport.print( (ARDTDestinationArrayLength * 8) / (ARDTsendSecs), 0 ); + Monitorport.println(F("bps")); + Monitorport.println(("Transfer finished")); +#endif + + return true; +} + + +bool ARstartArrayTransfer(char *buff, uint8_t filenamesize) +{ + //Start transfer of array to remote array or file + uint8_t ValidACK; + uint8_t localattempts = 0; + +#ifdef ENABLEMONITOR + Monitorport.print(F("Start array transfer for ")); + Monitorport.println(buff); +#endif + + ARDTSourceArrayLength = ARArrayLength; + + if (ARDTSourceArrayLength == 0) + { +#ifdef ENABLEMONITOR + Monitorport.print(F("Error - array 0 bytes ")); + Monitorport.println(buff); +#endif + + return false; + } + +#ifdef ENABLEARRAYCRC + ARDTSourceArrayCRC = ARarrayCRC((uint8_t *) ptrARsendArray, ARArrayLength, 0xFFFF); //get array CRC from position 0 to end +#endif + + ARDTNumberSegments = ARgetNumberSegments(ARDTSourceArrayLength, SegmentSize); + ARDTLastSegmentSize = ARgetLastSegmentSize(ARDTSourceArrayLength, SegmentSize); + ARbuild_DTArrayStartHeader(ARDTheader, DTArrayStartHeaderL, filenamesize, ARDTSourceArrayLength, ARDTSourceArrayCRC, SegmentSize); + ARLocalPayloadCRC = ARarrayCRC((uint8_t *) buff, filenamesize, 0xFFFF); + + do + { + localattempts++; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Send open remote file request")); +#endif + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + ARTXPacketL = LoRa.transmitDTIRQ(ARDTheader, DTArrayStartHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + + ARTXNetworkID = LoRa.getTXNetworkID(ARTXPacketL); //get the networkID appended to packet + ARTXArrayCRC = LoRa.getTXPayloadCRC(ARTXPacketL); //get the payload CRC thats was appended to packet + +#ifdef ENABLEMONITOR + Monitorport.print(F("Send attempt ")); + Monitorport.println(localattempts); + + if (ARTXPacketL == 0) //if there has been a send and ack error, ARTXPacketL returns as 0 + { + Monitorport.println(F("Transmit error")); + } +#endif + + ValidACK = LoRa.waitACKDTIRQ(ARDTheader, DTArrayStartHeaderL, ACKopentimeoutmS); + ARRXPacketType = ARDTheader[0]; + + if ((ValidACK > 0) && (ARRXPacketType == DTArrayStartACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("Valid ACK > ")); + ARprintArrayHEX(ARDTheader, ValidACK); //ValidACK is packet length +#endif +#endif + } + else + { + ARNoAckCount++; + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("NoACK")); +#endif +#endif + + if (ARNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); + Monitorport.println(); +#endif + + bitSet(ARDTErrors, ARNoACKlimit); + return false; + } + + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(ARDTErrors, ARStartTransfer); + return false; + } + return true; +} + + +bool ARsendSegments() +{ + //Start the array transfer at segment 0 + ARDTSegment = 0; + ARDTSentSegments = 0; + + ARarraylocation = 0; //start at first position in array + + while (ARDTSegment < (ARDTNumberSegments - 1)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + ARprintSeconds(); +#endif +#endif + + if (ARsendArraySegment(ARDTSegment, SegmentSize)) + { + ARDTSentSegments++; + } + else + { + return false; + } + delay(FunctionDelaymS); + }; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Last segment")); +#endif + + if (!ARsendArraySegment(ARDTSegment, ARDTLastSegmentSize)) + { + return false; + } + return true; +} + + +bool ARsendArraySegment(uint16_t segnum, uint8_t segmentsize) +{ + //Send array segment as payload in a data transfer packet + + uint8_t ValidACK; + uint8_t index; + uint8_t tempdata; + uint8_t localattempts = 0; + + for (index = 0; index < segmentsize; index++) + { + tempdata = ptrARsendArray[ARarraylocation]; + ARDTdata[index] = tempdata; + ARarraylocation++; + } + + ARbuild_DTSegmentHeader(ARDTheader, DTSegmentWriteHeaderL, segmentsize, segnum); + +#ifdef ENABLEMONITOR +#ifdef PRINTSEGMENTNUM + Monitorport.println(segnum); +#endif +#ifdef DEBUG + Monitorport.print(F(" ")); + ARprintheader(ARDTheader, DTSegmentWriteHeaderL); + Monitorport.print(F(" ")); + ARprintdata(ARDTdata, segmentsize); //print segment size of data array only + Monitorport.println(); +#endif +#endif + + do + { + localattempts++; + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + ARTXPacketL = LoRa.transmitDTIRQ(ARDTheader, DTSegmentWriteHeaderL, (uint8_t *) ARDTdata, segmentsize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + + if (ARTXPacketL == 0) //if there has been an error ARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDTIRQ(ARDTheader, DTSegmentWriteHeaderL, ACKsegtimeoutmS); + ARRXPacketType = ARDTheader[0]; + + if (ValidACK > 0) + { + if (ARRXPacketType == DTSegmentWriteNACK) + { + ARDTSegment = ARDTheader[4] + (ARDTheader[5] << 8); //load what the segment number should be + ARRXHeaderL = ARDTheader[2]; + ARarraylocation = ARDTSegment * SegmentSize; + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************************")); + Monitorport.print(F("Received restart request at segment ")); + Monitorport.println(ARDTSegment); +#ifdef DEBUG + ARprintheader(ARDTheader, ARRXHeaderL); +#endif + Monitorport.println(); + Monitorport.print(F("Seek to array location ")); + Monitorport.println(ARDTSegment * SegmentSize); + Monitorport.println(F("************************************")); + Monitorport.println(); + //Monitorport.flush(); +#endif + } + + if (ARRXPacketType == DTStartNACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Received restart request")); +#endif + + return false; + } + + //ack is valid, segment was acknowledged if here + + if (ARRXPacketType == DTSegmentWriteACK) + { + ARAckCount++; + ARDTSegment++; //increase value for next segment + return true; + } + } + else + { + ARNoAckCount++; + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("NoACK")); +#endif +#endif + + if (ARNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + + return false; + } + } + } while ((ValidACK == 0) && (localattempts < SendAttempts)) ; + + if (localattempts == SendAttempts) + { + bitSet(ARDTErrors, ARSendSegment); + return false; + } + + return true; +} + + +bool ARendArrayTransfer(char *buff, uint8_t filenamesize) +{ + //End array transfer + + uint8_t ValidACK; + uint8_t localattempts = 0; + + ARbuild_DTArrayEndHeader(ARDTheader, DTArrayEndHeaderL, filenamesize, ARDTSourceArrayLength, ARDTSourceArrayCRC, SegmentSize); + + do + { + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.println(F("Send end array write")); +#endif + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + ARTXPacketL = LoRa.transmitDTIRQ(ARDTheader, DTArrayEndHeaderL, (uint8_t *) buff, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + + ARTXNetworkID = LoRa.getTXNetworkID(ARTXPacketL); + ARTXArrayCRC = LoRa.getTXPayloadCRC(ARTXPacketL); + + if (ARTXPacketL == 0) //if there has been a send and ack error, ARARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDTIRQ(ARDTheader, DTArrayEndHeaderL, ACKclosetimeoutmS); + ARRXPacketType = ARDTheader[0]; + + if ((ValidACK > 0) && (ARRXPacketType == DTArrayEndACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("ACK header ")); + ARprintArrayHEX(ARDTheader, ValidACK); //ValidACK is packet length + Monitorport.println(); +#endif +#endif + } + else + { + ARNoAckCount++; + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("NoACK")); +#endif +#endif + + if (ARNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + return false; + } + +#ifdef ENABLEMONITOR +#ifdef DEBUG + ARprintArrayHEX(ARDTheader, ValidACK); //ValidACK is packet length +#endif + Monitorport.println(); +#endif + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)) ; + + + if (localattempts == SendAttempts) + { + bitSet(ARDTErrors, ARSendSegment); + return false; + } + return true; +} + + +void ARbuild_DTArrayStartHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t arraylength, uint16_t arraycrc, uint8_t segsize) +{ + //This builds the header buffer for the filename receiver might save the array as + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTArrayStart); //byte 0, write the packet type + arrayWriteUint8(ARDTflags); //byte 1, ARDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(arraylength); //byte 4,5,6,7, write the array length + arrayWriteUint16(arraycrc); //byte 8, 9, write array CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void ARbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum) +{ + //This builds the header buffer for a segment transmit + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTSegmentWrite); //write the packet type + arrayWriteUint8(ARDTflags); //initial ARDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint16(segnum); //write the DTsegment number + endarrayRW(); +} + + +void ARbuild_DTArrayEndHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t arraylength, uint16_t arraycrc, uint8_t segsize) +{ + //This builds the header buffer for the end remote array write + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTArrayEnd); //byte 0, write the packet type + arrayWriteUint8(ARDTflags); //byte 1, initial ARDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(arraylength); //byte 4,5,6,7, write the array length + arrayWriteUint16(arraycrc); //byte 8, 9, write array CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void ARprintLocalArrayDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Source array length ")); + Monitorport.print(ARDTSourceArrayLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEARRAYCRC + Monitorport.print(F("1 Source array CRC is 0x")); + Monitorport.println(ARDTSourceArrayCRC, HEX); +#endif + Monitorport.print(F("Segment Size ")); + Monitorport.println(SegmentSize); + Monitorport.print(F("Number segments ")); + Monitorport.println(ARDTNumberSegments); + Monitorport.print(F("Last segment size ")); + Monitorport.println(ARDTLastSegmentSize); +#endif +} + + +void ARprintSeconds() +{ +#ifdef ENABLEMONITOR + float secs; + secs = ( (float) millis() / 1000); + Monitorport.print(secs, 2); + Monitorport.print(F(" ")); +#endif +} + + +void ARprintAckBrief() +{ +#ifdef ENABLEMONITOR + ARPacketRSSI = LoRa.readPacketRSSI(); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(ARPacketRSSI); + Monitorport.print(F("dBm")); +#endif +} + + +void ARprintAckReception() +{ +#ifdef ENABLEMONITOR + ARPacketRSSI = LoRa.readPacketRSSI(); + ARPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F("ARAckCount,")); + Monitorport.print(ARAckCount); + Monitorport.print(F(",ARNoAckCount,")); + Monitorport.print(ARNoAckCount); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(ARPacketRSSI); + Monitorport.print(F("dBm,AckSNR,")); + Monitorport.print(ARPacketSNR); + Monitorport.print(F("dB")); + Monitorport.println(); +#endif +} + + +void ARprintACKdetail() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("ACKDetail")); + Monitorport.print(F(",RXNetworkID,0x")); + Monitorport.print(LoRa.getRXNetworkID(ARRXPacketL), HEX); + Monitorport.print(F(",RXPayloadCRC,0x")); + Monitorport.print(LoRa.getRXPayloadCRC(ARRXPacketL), HEX); + Monitorport.print(F(",ARRXPacketL,")); + Monitorport.print(ARRXPacketL); + Monitorport.print(F(" ")); + ARprintReliableStatus(); + Monitorport.println(); +#endif +} + + +void ARprintdata(uint8_t *dataarray, uint8_t arraysize) +{ + ARUNUSED(dataarray); + ARUNUSED(arraysize); + +#ifdef ENABLEMONITOR + Monitorport.print(F("DataBytes,")); + Monitorport.print(arraysize); + Monitorport.print(F(" ")); + ARprintArrayHEX((uint8_t *) dataarray, 16); //There is a lot of data to print so only print first 16 bytes +#endif +} + + +uint16_t ARgetNumberSegments(uint32_t arraysize, uint8_t segmentsize) +{ + uint16_t segments; + segments = arraysize / segmentsize; + + if ((arraysize % segmentsize) > 0) + { + segments++; + } + return segments; +} + + +uint8_t ARgetLastSegmentSize(uint32_t arraysize, uint8_t segmentsize) +{ + uint8_t lastsize; + + lastsize = arraysize % segmentsize; + if (lastsize == 0) + { + lastsize = segmentsize; + } + return lastsize; +} + + +bool ARsendDTInfo() +{ + //Send array info packet, for this implmentation its really only the flags in ARDTflags that is sent + + uint8_t ValidACK = 0; + uint8_t localattempts = 0; + + ARbuild_DTInfoHeader(ARDTheader, DTInfoHeaderL, 0); + + do + { + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.print(F("Send DTInfo packet attempt ")); + Monitorport.println(localattempts); +#endif + ARTXPacketL = LoRa.transmitDTIRQ(ARDTheader, DTInfoHeaderL, (uint8_t *) ARDTdata, 0, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (ARTXPacketL == 0) //if there has been an error ARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + continue; + } + + ValidACK = LoRa.waitACKDTIRQ(ARDTheader, DTInfoHeaderL, ACKsegtimeoutmS); + + if (ValidACK > 0) + { + //ack is a valid relaible packet + ARRXPacketType = ARDTheader[0]; +#ifdef ENABLEMONITOR + Monitorport.print(F("ACK Packet type 0x")); + Monitorport.println(ARRXPacketType, HEX); +#endif + + if (ARRXPacketType == DTInfoACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK received")); +#endif + ARAckCount++; + ARRXPacketType = ARDTheader[0]; + return true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK not received")); +#endif + return false; + } + + } + else + { + ARNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("No valid ACK received ")); +#endif + + if (ARNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + return false; + } + } + delay(PacketDelaymS); + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(ARDTErrors, ARSendPacket); + return false; + } + return true; +} + + +void ARbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen) +{ + //This builds the header buffer for a info only header, faults problems etc + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTInfo); //write the packet type + arrayWriteUint8(ARDTflags); //ARDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + endarrayRW(); +} + + +//************************************************ +//Receiver mode functions +//************************************************ + +uint32_t ARreceiveArray(uint8_t *ptrarray, uint32_t length, uint32_t receivetimeout) +{ + //returns 0 if no ARDTArrayEnded set, returns length of array if received + uint32_t startmS = millis(); + + ptrARreceivearray = ptrarray; //set global pointer to array pointer passed + MAXarraysize = length; + ARDTArrayTimeout = false; + ARDTArrayEnded = false; + ARDTDestinationArrayLength = 0; + + do + { + if (ARreceivePacketDT()) + { + startmS = millis(); + } + + if (ARDTArrayEnded) //has the end array transfer been received ? + { + return ARDTDestinationArrayLength; + } + } + while (((uint32_t) (millis() - startmS) < receivetimeout )); + + + if (ARDTArrayEnded) //has the end array transfer been received ? + { + return ARDTDestinationArrayLength; + } + else + { + ARDTArrayTimeout = true; + return 0; + } +} + + +bool ARreceivePacketDT() +{ + //Receive data transfer packets + + ARRXPacketType = 0; + ARRXPacketL = LoRa.receiveDTIRQ(ARDTheader, HeaderSizeMax, (uint8_t *) ARDTdata, DataSizeMax, NetworkID, RXtimeoutmS, WAIT_RX); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + +#ifdef ENABLEMONITOR +#ifdef DEBUG + ARprintSeconds(); +#endif +#endif + if (ARRXPacketL > 0) + { + //if the LoRa.receiveDTIRQ() returns a value > 0 for ARRXPacketL then packet was received OK + //then only action payload if destinationNode = thisNode + ARreadHeaderDT(); //get the basic header details into global variables ARRXPacketType etc + ARprocessPacket(ARRXPacketType); //process and act on the packet + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return true; + } + else + { + //if the LoRa.receiveDT() function detects an error RXOK is 0 + + uint16_t IRQStatus = LoRa.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { + Monitorport.println(F("RX Timeout")); + } + else + { + ARRXErrors++; + +#ifdef ENABLEMONITOR + Monitorport.print(F("PacketError")); + ARprintPacketDetails(); + ARprintReliableStatus(); + Monitorport.print(F(",IRQreg,0x")); + Monitorport.println(LoRa.readIrqStatus(), HEX); + //Monitorport.println(); +#endif + } + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return false; + } +} + + +void ARreadHeaderDT() +{ + //The first 6 bytes of the header contain the important stuff, so load it up + //so we can decide what to do next. + beginarrayRW(ARDTheader, 0); //start buffer read at location 0 + ARRXPacketType = arrayReadUint8(); //load the packet type + ARRXFlags = arrayReadUint8(); //ARDTflags byte + ARRXHeaderL = arrayReadUint8(); //load the header length + ARRXDataarrayL = arrayReadUint8(); //load the datalength + ARDTSegment = arrayReadUint16(); //load the segment number +} + + +bool ARprocessPacket(uint8_t packettype) +{ + //Decide what to do with an incoming packet + + if (packettype == DTSegmentWrite) + { + ARprocessSegmentWrite(); + return true; + } + + if (packettype == DTArrayStart) + { + ARprocessArrayStart(ARDTdata, ARRXDataarrayL); //ARDTdata contains the filename + return true; + } + + if (packettype == DTArrayEnd) + { + ARprocessArrayEnd(); + return true; + } + + return true; +} + + +bool ARprocessSegmentWrite() +{ + //There is a request to write a segment to array on receiver + //checks that the sequence of segment writes is correct + + uint8_t index, byteswritten = 0; + + if (!ARDTArrayStarted) + { + //something is wrong, have received a request to write a segment but there is no array + //write in progresss so need to reject the segment write with a restart NACK + +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("*************************************************************")); + Monitorport.println(F("Error - Segment write with no array write started - send NACK")); + Monitorport.println(F("*************************************************************")); + Monitorport.println(); +#endif + + ARDTheader[0] = DTStartNACK; + delay(ACKdelaymS); + delay(DuplicatedelaymS); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + LoRa.sendACKDTIRQ(ARDTheader, DTStartHeaderL, TXpower); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return false; + } + + if (ARDTSegment == ARDTSegmentNext) + { + //segment to write is as expected + + for (index = 0; index < ARRXDataarrayL; index++) + { + ptrARreceivearray[ARarraylocation] = ARDTdata[index]; + ARarraylocation++; + byteswritten++; + } + +#ifdef ENABLEMONITOR +#ifdef PRINTSEGMENTNUM + Monitorport.println(ARDTSegment); +#endif +#endif + + ARDTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + LoRa.sendACKDTIRQ(ARDTheader, DTSegmentWriteHeaderL, TXpower); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + ARDTReceivedSegments++; + ARDTSegmentLast = ARDTSegment; //so we can tell if sequece has been received twice + ARDTSegmentNext = ARDTSegment + 1; + return true; + } + + if (ARDTSegment == ARDTSegmentLast) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("ERROR segment ")); + Monitorport.print(ARDTSegment); + Monitorport.println(F(" already received ")); +#endif +#endif + + ARDTheader[0] = DTSegmentWriteACK; + delay(DuplicatedelaymS); + delay(ACKdelaymS); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + LoRa.sendACKDTIRQ(ARDTheader, DTSegmentWriteHeaderL, TXpower); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return true; + } + + if (ARDTSegment != ARDTSegmentNext ) + { + ARDTheader[0] = DTSegmentWriteNACK; + ARDTheader[4] = lowByte(ARDTSegmentNext); + ARDTheader[5] = highByte(ARDTSegmentNext); + delay(ACKdelaymS); + delay(DuplicatedelaymS); //add an extra delay here to stop repeated segment sends + +#ifdef ENABLEMONITOR + Monitorport.print(F(" ERROR Received Segment ")); + Monitorport.print(ARDTSegment); + Monitorport.print(F(" expected ")); + Monitorport.print(ARDTSegmentNext); + Monitorport.print(F(" ")); + Monitorport.print(F(" Send NACK for segment ")); + Monitorport.print(ARDTSegmentNext); + Monitorport.println(); + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.print(F("Transmit restart request for segment ")); + Monitorport.println(ARDTSegmentNext); +#ifdef DEBUG + ARprintheader(ARDTheader, ARRXHeaderL); +#endif + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.println(); + //Monitorport.flush(); +#endif + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + LoRa.sendACKDTIRQ(ARDTheader, DTSegmentWriteHeaderL, TXpower); + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + return false; + } + + return true; +} + + +bool ARprocessArrayStart(uint8_t *buff, uint8_t filenamesize) +{ + //There is a request to start writing to a local array on receiver + //The transmitter will have passed a filename to be used when saving the array to SD + //or for use when transferring the array elsewhere + + ARDTArrayStarted = false; //until this function completes the Array transfer has not started + ARDTArrayEnded = false; //and it cannot hve completed either ...... + ARarraylocation = 0; + beginarrayRW(ARDTheader, 4); //start buffer read at location 4 + ARDTSourceArrayLength = arrayReadUint32(); //load the array length being sent + + Monitorport.println(F("Start array transfer")); + + if (ARDTSourceArrayLength > MAXarraysize) + { + Monitorport.print(F("Array length to transfer ")); + Monitorport.print(ARDTSourceArrayLength); + Monitorport.println(F(" bytes")); + Monitorport.println(F("ERROR - Not enough memory allocated")); + Monitorport.print(F("MAXarraysize is defined as ")); + Monitorport.print(MAXarraysize); + Monitorport.println(F(" bytes")); + Monitorport.println(); + return false; + } + + + ARDTSourceArrayCRC = arrayReadUint16(); //load the CRC of the array being sent + memset(ARDTfilenamebuff, 0, ARDTfilenamesize); //clear ARDTfilenamebuff to all 0s + memcpy(ARDTfilenamebuff, buff, filenamesize); //copy received ARDTdata into ARDTfilenamebuff, the array should have a destination filename + +#ifdef ENABLEMONITOR + Monitorport.print((char*) ARDTfilenamebuff); + Monitorport.println(F(" Array start write request")); +#ifdef DEBUG + Monitorport.print(F("Header > ")); + ARprintArrayHEX(ARDTheader, HeaderSizeMax); +#endif + Monitorport.println(); + ARprintSourceArrayDetails(); + + if bitRead(ARRXFlags, ARNoFileSave) + { + Monitorport.println(F("Remote did not save file to SD")); + } +#endif + ARDTStartmS = millis(); + delay(ACKdelaystartendmS); //there needs to be a dealy here, to wait for receiver to be ready + + ARDTheader[0] = DTArrayStartACK; //set the ACK packet type + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + LoRa.sendACKDTIRQ(ARDTheader, DTArrayStartHeaderL, TXpower); + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + ARDTSegmentNext = 0; //after a\rray write start open, segment 0 is next + + ARDTArrayStarted = true; + + return true; +} + + +bool ARprocessArrayEnd() +{ + // There is a request to end writing to an array on receiver +#ifdef ENABLEMONITOR + Monitorport.print((char*) ARDTfilenamebuff); + Monitorport.println(F(" end array write request")); +#endif + + if (ARDTArrayStarted) //check if array write had been started, end it if it is + { + ARDTArrayStarted = false; + ARDTDestinationArrayLength = ARarraylocation; + +#ifdef ENABLEMONITOR + Monitorport.print(F("ARDTDestinationArrayLength ")); + Monitorport.println(ARDTDestinationArrayLength); +#ifdef ENABLEARRAYCRC + ARDTDestinationArrayCRC = ARarrayCRC(ptrARreceivearray, ARDTDestinationArrayLength, 0xFFFF); + Monitorport.print(F("Destination arrayCRC 0x")); + Monitorport.println(ARDTDestinationArrayCRC, HEX); +#endif +#endif + + beginarrayRW(ARDTheader, 4); //start writing to array at location 12 + arrayWriteUint32(ARDTDestinationArrayLength); //write array length of array just written just written to ACK header + arrayWriteUint16(ARDTDestinationArrayCRC); //write CRC of array just written to ACK header + +#ifdef ENABLEMONITOR + Monitorport.println(F("Array write ended")); + Monitorport.print(F("Transfer time ")); + Monitorport.print(millis() - ARDTStartmS); + Monitorport.print(F("mS")); + Monitorport.println(); + ARprintDestinationArrayDetails(); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Array write already ended")); +#endif + delay(DuplicatedelaymS); + } + + delay(ACKdelaystartendmS); + ARDTheader[0] = DTArrayEndACK; + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, HIGH); + } + + LoRa.sendACKDTIRQ(ARDTheader, DTArrayEndHeaderL, TXpower); + + if (ARDTLED >= 0) + { + digitalWrite(ARDTLED, LOW); + } + + ARDTArrayEnded = true; + return true; +} + + +uint16_t ARarrayCRC(uint8_t *buffer, uint32_t size, uint16_t startvalue) +{ + uint32_t index; + uint16_t libraryCRC; + uint8_t j; + + libraryCRC = startvalue; //start value for CRC16 + + for (index = 0; index < size; index++) + { + libraryCRC ^= (((uint16_t)buffer[index]) << 8); + for (j = 0; j < 8; j++) + { + if (libraryCRC & 0x8000) + libraryCRC = (libraryCRC << 1) ^ 0x1021; + else + libraryCRC <<= 1; + } + } + + return libraryCRC; +} + + +void ARprintSourceArrayDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(ARDTfilenamebuff); + Monitorport.print(F(" Source array length is ")); + Monitorport.print(ARDTSourceArrayLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEARRAYCRC + Monitorport.print(F("2 Source array CRC is 0x")); + Monitorport.println(ARDTSourceArrayCRC, HEX); +#endif +#endif +} + + +void ARprintDestinationArrayDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Destination array length ")); + Monitorport.print(ARDTDestinationArrayLength); + Monitorport.println(F(" bytes")); + Monitorport.print(F("Source array length ")); + Monitorport.print(ARDTSourceArrayLength); + Monitorport.println(F(" bytes")); + + if (ARDTDestinationArrayLength != ARDTSourceArrayLength) + { + Monitorport.println(F("ERROR - array lengths do not match")); + } + else + { + Monitorport.println(F("Array lengths match")); + } + +#ifdef ENABLEARRAYCRC + Monitorport.print(F("Destination array CRC is 0x")); + Monitorport.println(ARDTDestinationArrayCRC, HEX); + Monitorport.print(F("3 Source array CRC is 0x")); + Monitorport.println(ARDTSourceArrayCRC, HEX); + + if (ARDTDestinationArrayCRC != ARDTSourceArrayCRC) + { + Monitorport.println(F("ERROR - array CRCs do not match")); + } + else + { + Monitorport.println(F("Array CRCs match")); + } +#endif +#endif +} + + +//************************************************ +//Common functions +//************************************************ + +void ARprintArrayHEX(uint8_t *buff, uint32_t len) +{ + ARUNUSED(buff); + ARUNUSED(len); + +#ifdef ENABLEMONITOR + uint32_t index, buffdata; + + for (index = 0; index < len; index++) + { + buffdata = buff[index]; + if (buffdata < 16) + { + Monitorport.print(F("0")); + } + Monitorport.print(buffdata, HEX); + Monitorport.print(F(" ")); + } +#endif +} + + +void ARsetDTLED(int8_t pinnumber) +{ + //give the data transfer routines an LED to flash + if (pinnumber >= 0) + { + ARDTLED = pinnumber; + pinMode(pinnumber, OUTPUT); + } +} + + +void ARprintheader(uint8_t *hdr, uint8_t hdrsize) +{ + ARUNUSED(hdr); + ARUNUSED(hdrsize); + +#ifdef ENABLEMONITOR + Monitorport.print(F("HeaderBytes,")); + Monitorport.print(hdrsize); + Monitorport.print(F(" ")); + ARprintArrayHEX(hdr, hdrsize); +#endif +} + + +void ARprintPacketDetails() +{ +#ifdef ENABLEMONITOR +#ifdef DEBUG + ARPacketRSSI = LoRa.readPacketRSSI(); + ARPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F(" RSSI,")); + Monitorport.print(ARPacketRSSI); + Monitorport.print(F("dBm")); + Monitorport.print(F(",SNR,")); + Monitorport.print(ARPacketSNR); + Monitorport.print(F("dBm,RXOKCount,")); + Monitorport.print(ARDTReceivedSegments); + Monitorport.print(F(",RXErrs,")); + Monitorport.print(ARRXErrors); + Monitorport.print(F(" RX")); + ARprintheader(ARDTheader, ARRXHeaderL); +#endif +#endif +} + + +void ARprintReliableStatus() +{ + +#ifdef ENABLEMONITOR + + uint8_t reliableErrors = LoRa.readReliableErrors(); + uint8_t reliableFlags = LoRa.readReliableFlags(); + + if (bitRead(reliableErrors, ReliableCRCError)) + { + Monitorport.print(F(",ReliableCRCError")); + } + + if (bitRead(reliableErrors, ReliableIDError)) + { + Monitorport.print(F(",ReliableIDError")); + } + + if (bitRead(reliableErrors, ReliableSizeError)) + { + Monitorport.print(F(",ReliableSizeError")); + } + + if (bitRead(reliableErrors, ReliableACKError)) + { + Monitorport.print(F(",NoReliableACK")); + } + + if (bitRead(reliableErrors, ReliableTimeout)) + { + Monitorport.print(F(",ReliableTimeout")); + } + + if (bitRead(reliableFlags, ReliableACKSent)) + { + Monitorport.print(F(",ACKsent")); + } + + if (bitRead(reliableFlags, ReliableACKReceived)) + { + Monitorport.print(F(",ACKreceived")); + } +#endif +} diff --git a/src/DTSDlibrary.h b/src/DTSDlibrary.h new file mode 100644 index 0000000..488faff --- /dev/null +++ b/src/DTSDlibrary.h @@ -0,0 +1,461 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 08/11/21 + + This code is supplied as is, it is up to the user of the program to decide if the program is suitable + for the intended purpose and free from errors. +*******************************************************************************************************/ + +#ifdef SDFATLIB +#include +SdFat SD; +File dataFile; //name the file instance needed for SD library routines +#endif + +#ifdef SDLIB +#include +File dataFile; //name the file instance needed for SD library routines +#endif + +#ifndef Monitorport +#define Monitorport Serial //output to Serial if no other port defined +#endif + + +bool DTSD_dumpFileASCII(char *buff); +bool DTSD_dumpFileHEX(char *buff); +bool DTSD_dumpSegmentHEX(uint8_t segmentsize); +bool DTSD_initSD(uint8_t CSpin); +uint32_t DTSD_getFileSize(char *buff); +void DTSD_printDirectory(); +uint32_t DTSD_openFileRead(char *buff); +uint16_t DTSD_getNumberSegments(uint32_t filesize, uint8_t segmentsize); +uint8_t DTSD_getLastSegmentSize(uint32_t filesize, uint8_t segmentsize); +bool DTSD_openNewFileWrite(char *buff); +bool DTSD_openFileWrite(char *buff, uint32_t position); +uint8_t DTSD_readFileSegment(uint8_t *buff, uint8_t segmentsize); +uint8_t DTSD_writeSegmentFile(uint8_t *buff, uint8_t segmentsize); +void DTSD_seekFileLocation(uint32_t position); +uint16_t DTSD_createFile(char *buff); +uint16_t DTSD_fileCRCCCITT(); +void DTSD_fileFlush(); +void DTSD_closeFile(); +void printDirectorySD(File dir, int numTabs); + + +bool DTSD_dumpFileASCII(char *buff) +{ + + File dataFile = SD.open(buff); //open the test file note that only one file can be open at a time, + + if (dataFile) //if the file is available, read from it + { + while (dataFile.available()) + { + Monitorport.write(dataFile.read()); + } + dataFile.close(); + return true; + } + else + { + return false; + } +} + + +bool DTSD_dumpFileHEX(char *buff) +{ + //Note, this function will return true if the SD card is removed. + uint16_t Loopv1, Loopv2; + uint8_t fileData; + uint32_t filesize; + + if (!SD.exists(buff)) + { + return false; + } + + dataFile = SD.open(buff); + filesize = dataFile.size(); + filesize--; //file data locations are from 0 to (filesize -1); + Monitorport.print(F("Lcn 0 1 2 3 4 5 6 7 8 9 A B C D E F")); + Monitorport.println(); + + if (dataFile) //if the file is available, read from it + { + while (dataFile.available()) + { + for (Loopv1 = 0; Loopv1 <= filesize;) + { + Monitorport.print(F("0x")); + if (Loopv1 < 0x10) + { + Monitorport.print(F("0")); + } + Monitorport.print((Loopv1), HEX); + Monitorport.print(F(" ")); + for (Loopv2 = 0; Loopv2 <= 15; Loopv2++) + { + fileData = dataFile.read(); + if (fileData < 0x10) + { + Monitorport.print(F("0")); + } + Monitorport.print(fileData, HEX); + Monitorport.print(F(" ")); + Loopv1++; + } + Monitorport.println(); + } + } + dataFile.close(); + return true; + } + else + { + Monitorport.println(F("File not available")); + return false; + } +} + + +bool DTSD_initSD(uint8_t CSpin) +{ + if (SD.begin(CSpin)) + { + return true; + } + else + { + return false; + } +} + + +uint32_t DTSD_getFileSize(char *buff) +{ + uint32_t filesize; + + if (!SD.exists(buff)) + { + return 0; + } + + dataFile = SD.open(buff); + filesize = dataFile.size(); + dataFile.close(); + return filesize; +} + + +#ifdef SDFATLIB +void DTSD_printDirectory() +{ + dataFile = SD.open("/"); + Monitorport.println(F("Card directory")); + SD.ls("/", LS_R); +} +#endif + + +#ifdef SDLIB +void DTSD_printDirectory() +{ + dataFile = SD.open("/"); + + printDirectorySD(dataFile, 0); + + Monitorport.println(); +} + + +void printDirectorySD(File dir, int numTabs) +{ + + while (true) + { + File entry = dir.openNextFile(); + + if (! entry) + { + //no more files + break; + } + + for (uint8_t i = 0; i < numTabs; i++) + { + Monitorport.print('\t'); + } + + Monitorport.print(entry.name()); + + if (entry.isDirectory()) + { + Monitorport.println("/"); + printDirectorySD(entry, numTabs + 1); + } + else + { + // files have sizes, directories do not + Monitorport.print("\t\t"); + Monitorport.println(entry.size(), DEC); + } + entry.close(); + } +} +#endif + + +bool DTSD_dumpSegmentHEX(uint8_t segmentsize) +{ + uint16_t Loopv1, Loopv2; + uint8_t fileData; + + Monitorport.print(F("Print segment of ")); + Monitorport.print(segmentsize); + Monitorport.println(F(" bytes")); + Monitorport.print(F("Lcn 0 1 2 3 4 5 6 7 8 9 A B C D E F")); + Monitorport.println(); + + if (dataFile) //if the file is available, read from it + { + for (Loopv1 = 0; Loopv1 < segmentsize;) + { + Monitorport.print(F("0x")); + if (Loopv1 < 0x10) + { + Monitorport.print(F("0")); + } + Monitorport.print((Loopv1), HEX); + Monitorport.print(F(" ")); + for (Loopv2 = 0; Loopv2 <= 15; Loopv2++) + { + //stop printing if all of segment has been printed + if (Loopv1 < segmentsize) + { + fileData = dataFile.read(); + if (fileData < 0x10) + { + Monitorport.print(F("0")); + } + Monitorport.print(fileData, HEX); + Monitorport.print(F(" ")); + Loopv1++; + } + } + Monitorport.println(); + } + return true; + } + else + { + return false; + } +} + + +uint32_t DTSD_openFileRead2(char *buff) +{ + uint32_t filesize; + + dataFile = SD.open(buff); + filesize = dataFile.size(); + dataFile.seek(0); + return filesize; +} + + +uint32_t DTSD_openFileRead(char *buff) +{ + uint32_t filesize; + + if (SD.exists(buff)) + { + //Monitorport.println(F("File exists")); + dataFile = SD.open(buff); + filesize = dataFile.size(); + dataFile.seek(0); + return filesize; + } + else + { + //Monitorport.println(F("File does not exist")); + return 0; + } +} + + +uint16_t DTSD_getNumberSegments(uint32_t filesize, uint8_t segmentsize) +{ + uint16_t segments; + segments = filesize / segmentsize; + + if ((filesize % segmentsize) > 0) + { + segments++; + } + return segments; +} + + +uint8_t DTSD_getLastSegmentSize(uint32_t filesize, uint8_t segmentsize) +{ + uint8_t lastsize; + + lastsize = filesize % segmentsize; + if (lastsize == 0) + { + lastsize = segmentsize; + } + return lastsize; +} + + +bool DTSD_openNewFileWrite(char *buff) +{ + if (SD.exists(buff)) + { + //Monitorport.print(buff); + //Monitorport.println(F(" File exists - deleting")); + SD.remove(buff); + } + + if (dataFile = SD.open(buff, FILE_WRITE)) + { + //Monitorport.print(buff); + //Monitorport.println(F(" SD File opened")); + return true; + } + else + { + //Monitorport.print(buff); + //Monitorport.println(F(" ERROR opening file")); + return false; + } +} + + +bool DTSD_openFileWrite(char *buff, uint32_t position) +{ + dataFile = SD.open(buff, FILE_WRITE); //seems to operate as append + dataFile.seek(position); //seek to first position in file + + if (dataFile) + { + return true; + } + else + { + return false; + } +} + + +uint8_t DTSD_readFileSegment(uint8_t *buff, uint8_t segmentsize) +{ + uint8_t index = 0; + uint8_t fileData; + + while (index < segmentsize) + { + fileData = (uint8_t) dataFile.read(); + buff[index] = fileData; + index++; + }; + + if (index == segmentsize) + { + return segmentsize; //if all written return segment size + } + else + { + return index - 1; //if not all written return number bytes read + } +} + + +uint8_t DTSD_writeSegmentFile(uint8_t *buff, uint8_t segmentsize) +{ + uint8_t index, byteswritten = 0; + + for (index = 0; index < segmentsize; index++) + { + dataFile.write(buff[index]); + byteswritten++; + } + return byteswritten; +} + + +void DTSD_seekFileLocation(uint32_t position) +{ + dataFile.seek(position); //seek to position in file + return; +} + + +uint16_t DTSD_createFile(char *buff) +{ + //creats a new filename use this definition as the base; + //char filename[] = "/SD0000.txt"; //filename used as base for creating logfile, 0000 replaced with numbers + //the 0000 in the filename is replaced with the next number avaialable + + uint16_t index; + + for (index = 1; index <= 9999; index++) { + buff[3] = index / 1000 + '0'; + buff[4] = ((index % 1000) / 100) + '0'; + buff[5] = ((index % 100) / 10) + '0'; + buff[6] = index % 10 + '0' ; + if (! SD.exists(buff)) + { + // only open a new file if it doesn't exist + dataFile = SD.open(buff, FILE_WRITE); + break; + } + } + + if (!dataFile) + { + return 0; + } + + return index; //return number of logfile created +} + + +uint16_t DTSD_fileCRCCCITT(uint32_t fsize) +{ + uint32_t index; + uint16_t CRCcalc; + uint8_t j, filedata; + + CRCcalc = 0xFFFF; //start value for CRC16 + + for (index = 0; index < fsize; index++) + { + filedata = dataFile.read(); + CRCcalc ^= (((uint16_t) filedata ) << 8); + for (j = 0; j < 8; j++) + { + if (CRCcalc & 0x8000) + CRCcalc = (CRCcalc << 1) ^ 0x1021; + else + CRCcalc <<= 1; + } + } + + return CRCcalc; +} + + +void DTSD_fileFlush() +{ + dataFile.flush(); +} + + +void DTSD_closeFile() +{ + dataFile.close(); //close local file +} diff --git a/src/EEPROM_Memory.h b/src/EEPROM_Memory.h new file mode 100644 index 0000000..e61f89f --- /dev/null +++ b/src/EEPROM_Memory.h @@ -0,0 +1,88 @@ +/* + Copyright 2020 - Stuart Robinson + 24/04/20 +*/ + +/******************************************************************************************************* + Library Operation - This library is for reading from and writing to the EEPROM. These routines provide + a funtional match to the libraries for FRAM. + +*******************************************************************************************************/ + +#include +#define LTUNUSED(v) (void) (v) //add LTUNUSED(variable); in functions to avoid compiler warnings + +void memoryStart(uint16_t addr) //yes I know the EEPROM does not have an address, but the other memory types do +{ + //left empty for future use + LTUNUSED(addr); //avoids compliler warning +} + + +void memoryEnd() +{ + //left empty for future use +} + + +void writeMemoryUint8(uint16_t addr, uint8_t x) +{ + //write a byte to the EEPROM + EEPROM.put(addr, x); +} + + +uint16_t readMemoryUint16(uint16_t addr) +{ + uint16_t x; + EEPROM.get(addr, x); + return x; +} + + +uint32_t readMemoryUint32(uint16_t addr) +{ + uint32_t x; + EEPROM.get(addr, x); + return x; +} + + +void writeMemoryUint32(uint16_t addr, uint32_t x) +{ + EEPROM.put(addr, x); +} + + +void writeMemoryUint16(uint16_t addr, uint16_t x) +{ + EEPROM.put(addr, x); +} + + +float readMemoryFloat(uint16_t addr) +{ + float x; + EEPROM.get(addr, x); + return x; +} + + +void writeMemoryFloat(uint16_t addr, float x) +{ + EEPROM.put(addr, x); +} + + +void fillMemory(uint16_t startaddr, uint16_t endaddr, uint8_t lval) +{ + uint32_t i; + for (i = startaddr; i <= endaddr; i++) + { + writeMemoryUint8(i, lval); + } +} + + + + diff --git a/src/LICENSE.txt b/src/LICENSE.txt new file mode 100644 index 0000000..d6dfb1b --- /dev/null +++ b/src/LICENSE.txt @@ -0,0 +1,25 @@ +--- Revised BSD License --- +Copyright (c) 2013, SEMTECH S.A. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Semtech corporation nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL SEMTECH S.A. BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/src/ProgramLT_Definitions.h b/src/ProgramLT_Definitions.h new file mode 100644 index 0000000..e92f8e3 --- /dev/null +++ b/src/ProgramLT_Definitions.h @@ -0,0 +1,155 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 17/12/19 + + This program is supplied as is, it is up to the user of the program to decide if the program is + suitable for the intended purpose and free from errors. +*******************************************************************************************************/ + +/* +****************************************************************************************************** + Definitions for packet types +****************************************************************************************************** +*/ + +const char Sensor1 = '!'; //Sensor packet1 +const char HABPacket = '$'; //HAB style CSV ASCII packet +const char Broadcast = '*'; //Broadcast destination address +const char RControl1 = 'D'; //Remote Control packet +const char TestMode1 = '1'; //used to switch to Testmode1 settings +const char TestPacket = 'T'; //Test packet +const char TXError = 't'; //Transmitter error +const char PowerUp = 'P'; //sent on tracker start +const char LocationPacket = 'L'; //LT library tracker location packet +const char ShortLocationPacket = 'S'; //LT library tracker short version location packet +const char LocationBinaryPacket = 's'; //Short search packet +const char NoFix = 'F'; //GPS no fix +const char NoGPS = 'G'; //No GPS found, or GPS error. +const char ACK = 'A'; //Acknowledge +const char NACK = 'N'; //Not Acknowledge, error +const char AFC = 'a'; //Packet sent for AFC purposes + + +//file transfer packetype definitions + +#define DTSegmentWrite 0xA0 //packet type for segment write +#define DTSegmentWriteACK 0xA1 //packet type for segment write ACK +#define DTSegmentWriteNACK 0xA2 //packet type for segment write NACK +#define DTSegmentWriteHeaderL 6 + +#define DTFileOpen 0xA4 //packet type for file open, filename +#define DTFileOpenACK 0xA5 //packet type for file open, filename ACK +#define DTFileOpenNACK 0xA6 //packet type for file open, filename NACK +#define DTFileOpenHeaderL 12 + +#define DTFileClose 0xA8 //packet type for file close, filename +#define DTFileCloseACK 0xA9 //packet type for file close, filename ACK +#define DTFileCloseNACK 0xAA //packet type for file close, filename NACK +#define DTFileCloseHeaderL 12 + +#define DTDataSeek 0xAC //packet type for seek to date or file position +#define DTDataSeekACK 0xAD //packet type for seek to date or file position ACK +#define DTDataSeekNACK 0xAE //packet type for seek to date or file position NACK +#define DTDataSeekHeaderL 9 + +#define DTStart 0xB0 //packet type for start (or restart) file transfer +#define DTStartACK 0xB1 //packet type for start (or restart) ACK +#define DTStartNACK 0xB2 //packet type for start (or restart) NACK +#define DTStartHeaderL 6 //header length for start (or restart) file transfer + +#define DTWake 0xB4 //packet type for wake of node +#define DTWakeACK 0xB5 //packet type for wake of node ACK +#define DTWakeNACK 0xB6 //packet type for wake of node ACK +#define DTWakeHeaderL 6 //header length for for wake of node + +#define DTInfo 0xB8 //packet type for wake of node +#define DTInfoACK 0xB9 //packet type for wake of node ACK +#define DTInfoNACK 0xBA //packet type for wake of node ACK +#define DTInfoHeaderL 8 //header length for for wake of node + +#define DTArrayStart 0xA4 //packet type for array start +#define DTArrayStartACK 0xA5 //packet type for array start +#define DTArrayStartNACK 0xA6 //packet type for array start +#define DTArrayStartHeaderL 12 + +#define DTArrayEnd 0xA8 //packet type for file end +#define DTArrayEndACK 0xA9 //packet type for file end +#define DTArrayEndNACK 0xAA //packet type for file end +#define DTArrayEndHeaderL 12 + + + +//GPS Tracker Status byte settings +const uint8_t GPSFix = 0; //flag bit set when GPS has a current fix +const uint8_t GPSConfigError = 1; //flag bit set to indicate cannot configure GPS or wrong configuration +const uint8_t CameraError = 2; //flag bit indicating a camera device error +const uint8_t GPSError = 3; //flag bit set to indicate GPS error, response timeout for instance +const uint8_t LORAError = 4; //flag bit indication a lora device error +const uint8_t SDError = 5; //flag bit indication a SD card device error +const uint8_t TrackerLost = 6; //flag bit indication that tracker in lost mode +const uint8_t NoGPSTestMode = 7; //flag bit number to indicate tracker in no GPS test mode + +/********************************************************************* + START Stored Program data +**********************************************************************/ +const uint16_t addr_StartMemory = 0x00; //the start of memory +const uint16_t addr_StartProgramData = 0x100; //the start of program data in memory +const uint16_t addr_ResetCount = 0x100; //unsigned long int 4 bytes +const uint16_t addr_SequenceNum = 0x104; //unsigned long int 4 bytes +const uint16_t addr_TXErrors = 0x108; //uint16_t 2 bytes +const uint16_t addr_EndMemory = 0x3FF; + + +/********************************************************************* + START GPS CoordinateData +**********************************************************************/ +//for storing last received GPS co-ordinates from local and remote tracker GPS +const uint16_t addr_StartCoordinateData = 0x300; +const uint16_t addr_RemoteLat = 0x300; //float 4 bytes +const uint16_t addr_RemoteLon = 0x304; //float 4 bytes +const uint16_t addr_RemoteAlt = 0x308; //uint16_t 2 bytes +const uint16_t addr_RemoteHour = 0x30C; //byte 1 byte; Note times for last tracker co-ordinates come from local GPS time +const uint16_t addr_RemoteMin = 0x310; //byte 1 byte +const uint16_t addr_RemoteSec = 0x311; //byte 1 byte +const uint16_t addr_RemoteDay = 0x312; //byte 1 byte +const uint16_t addr_RemoteMonth = 0x313; //byte 1 byte +const uint16_t addr_RemoteYear = 0x314; //byte 1 byte +const uint16_t addr_LocalLat = 0x318; //float 4 bytes +const uint16_t addr_LocalLon = 0x31C; //float 4 bytes +const uint16_t addr_LocalAlt = 0x320; //uint16_t 2 bytes +const uint16_t addr_LocalHour = 0x322; //byte 1 byte +const uint16_t addr_LocalMin = 0x323; //byte 1 byte +const uint16_t addr_LocalSec = 0x324; //byte 1 byte +const uint16_t addr_LocalDay = 0x325; //byte 1 byte +const uint16_t addr_LocalMonth = 0x326; //byte 1 byte +const uint16_t addr_LocalYear = 0x327; //byte 1 byte +const uint16_t addr_EndCoordinateData = 0x327; + +const uint16_t addr_RemotelocationCRC = 0x340; //the 16 bit CRC of the last tracker location data is saved here +const uint16_t addr_LocallocationCRC = 0x342; //the 16 bit CRC of the last local location data is saved here + +const uint16_t addr_TestLocation_page3 = 0x3FF; //used as a location for read\write tests + +/********************************************************************* + END GPS CoordinateData +**********************************************************************/ + + +/* +****************************************************************************************************** + Bit numbers for current_config byte settings end definitions for packet types +****************************************************************************************************** +*/ + +//Bit numbers for current_config byte settings in transmitter (addr_Default_config1) +const uint8_t SearchEnable = 0; //bit num to set in config byte to enable search mode packet +const uint8_t TXEnable = 1; //bit num to set in config byte to enable transmissions +const uint8_t FSKRTTYEnable = 2; //bit num to set in config byte to enable FSK RTTY +const uint8_t DozeEnable = 4; //bit num to set in config byte to put tracker in Doze mode +const uint8_t GPSHotFix = 7; //bit when set enables GPS Hot Fix mode. + + +#define CRC_ON 1 +#define CRC_OFF 0 + +#define ACK_ON 1 +#define ACK_OFF 0 diff --git a/src/SDtransfer.h b/src/SDtransfer.h new file mode 100644 index 0000000..4e8250d --- /dev/null +++ b/src/SDtransfer.h @@ -0,0 +1,1522 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 14/03/22 + + The functions expect the calling sketch to create an instance called LoRa, so that functions + are called like this; LoRa.getSDTXNetworkID(). + + This code is supplied as is, it is up to the user of the program to decide if the program is suitable + for the intended purpose and free from errors. + + There is a copy of this file in the SX12XX-LoRa library \src folder, but the file can be copied to the + sketch folder and used locally. In this way its possible to carry out custom modifications. + + Extensive use is made of #defines to allow the monotoring and debug prints to serial monitor to be + turned off, this is to allow for the circumstance where the primary serial port is in use for serial + file transfers to a PC or similar. To see the serial prints you need to have this set of #defines; + + #define Monitorport Serial + #define ENABLEMONITOR + +*******************************************************************************************************/ +/* + ToDo: + +*/ + +//110122 added local function printArrayHEX(uint8_t *buff, uint32_t len) +//130122 Made variable and function names unique so that the array transfer routines can be used in the same program +//130122 Converted all Serial prints to Monitorport.print() format +//140322 added #ifdef ENABLEMONITOR to serial prints + + +#define SDUNUSED(v) (void) (v) //add SDUNUSED(variable); to avoid compiler warnings + + +#ifndef Monitorport +#define Monitorport Serial +#endif + +#include //part of SX12xx library +//#define DEBUG //enable this define to print additional debug info for segment transfers + +uint8_t SDRXPacketL; //length of received packet +uint8_t SDRXPacketType; //type of received packet, segment write, ACK, NACK etc +uint8_t SDRXHeaderL; //length of header +int16_t SDPacketRSSI; //stores RSSI of received packet +int8_t SDPacketSNR; //stores signal to noise ratio of received packet +uint16_t SDAckCount; //keep a count of acks that are received within timeout period +uint16_t SDNoAckCount; //keep a count of acks not received within timeout period +uint16_t SDDTDestinationFileCRC; //CRC of complete file received +uint32_t SDDTDestinationFileLength; //length of file written on the destination\receiver +uint16_t SDDTSourceFileCRC; //CRC returned of the remote saved file +uint32_t SDDTSourceFileLength; //length of file at source\transmitter +uint32_t SDDTStartmS; //used for timeing transfers +uint16_t SDDTSegment = 0; //current segment number +char SDDTfilenamebuff[Maxfilenamesize]; //global buffer to store current filename +uint8_t SDDTheader[16]; //header array +uint8_t SDDTdata[245]; //data/segment array +uint8_t SDDTflags = 0; //Flags byte used to pass status information between nodes +int SDDTLED = -1; //pin number for indicator LED, if -1 then not used +uint16_t SDDTErrors; //used for tracking errors in the transfer process + +uint16_t SDTXNetworkID; //this is used to store the 'network' number from packet received, receiver must have the same networkID +uint16_t SDTXArrayCRC; //should contain CRC of data array transmitted +uint8_t SDTXPacketL; //length of transmitted packet +uint16_t SDLocalPayloadCRC; //for calculating the local data array CRC +uint8_t SDDTLastSegmentSize; //size of the last segment +uint16_t SDDTNumberSegments; //number of segments for a file transfer +uint16_t SDDTSentSegments; //count of segments sent +bool SDDTFileTransferComplete = false; //bool to flag file transfer complete +uint32_t SDDTSendmS; //used for timing transfers +float SDDTsendSecs; //seconds to transfer a file + +uint16_t SDRXErrors; //count of packets received with error +uint8_t SDRXFlags; //SDDTflags byte in header, could be used to control actions in TX and RX +uint8_t SDRXDataarrayL; //length of data array\segment +bool SDDTFileOpened; //bool to flag when file has been opened +bool SDDTFileClosed; //bool to flag when file has been saved to SD +uint16_t SDDTSegmentNext; //next segment expected +uint16_t SDDTReceivedSegments; //count of segments received +uint16_t SDDTSegmentLast; //last segment processed + +//Transmitter mode functions +uint32_t SDsendFile(char *filename, uint8_t namelength); +bool SDstartFileTransfer(char *filename, uint8_t filenamesize); +bool SDsendSegments(); +bool SDsendFileSegment(uint16_t segnum, uint8_t segmentsize); +bool SDendFileTransfer(char *filename, uint8_t filenamesize); +void SDbuild_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void SDbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum); +void SDbuild_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void SDprintLocalFileDetails(); +void SDprintSeconds(); +void SDprintAckBrief(); +void SDprintAckReception(); +void SDprintACKdetail(); +void SDprintdata(uint8_t *dataarray, uint8_t arraysize); +void SDprintPacketHex(); +bool SDsendDTInfo(); +void SDbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen); + +//Receiver mode functions +bool SDreceiveaPacketDT(); +void SDreadHeaderDT(); +bool SDprocessPacket(uint8_t packettype); +void SDprintPacketDetails(); +bool SDprocessSegmentWrite(); +bool SDprocessFileOpen(uint8_t *filename, uint8_t filenamesize); +bool SDprocessFileClose(); +void SDprintPacketRSSI(); +void SDprintSourceFileDetails(); +void SDprintDestinationFileDetails(); + +//Common functions +void SDsetLED(int8_t pinnumber); +void SDprintheader(uint8_t *header, uint8_t headersize); +void SDprintReliableStatus(); + +//bit numbers used by SDDTErrors (16bits) and RXErrors (first 8bits) +const uint8_t SDNoFileSave = 0; //bit number of SDDTErrors to set when no file save, to SD for example +const uint8_t SDNothingToSend = 1; //bit number of SDDTErrors to set when nothing to send or unable to send image\file +const uint8_t SDNoCamera = 1; //bit number of SDDTErrors to set when camera fails +const uint8_t SDSendFile = 2; //bit number of SDDTErrors to set when file SD file image\file send fail +const uint8_t SDSendArray = 2; //bit number of SDDTErrors to set when file array image\file send fail +const uint8_t SDNoACKlimit = 3; //bit number of SDDTErrors to set when NoACK limit reached +const uint8_t SDSendPacket = 4; //bit number of SDDTErrors to set when sending a packet fails or there is no ack + +const uint8_t SDStartTransfer = 11; //bit number of SDDTErrors to set when StartTransfer fails +const uint8_t SDSendSegments = 12; //bit number of SDDTErrors to set when SendSegments function fails +const uint8_t SDSendSegment = 13; //bit number of SDDTErrors to set when sending a single Segment send fails +const uint8_t SDOpeningFile = 14; //bit number of SDDTErrors to set when opening file fails +const uint8_t SDendTransfer = 15; //bit number of SDDTErrors to set when end transfer fails + + +//************************************************ +//Transmit mode functions +//************************************************ + +uint32_t SDsendFile(char *filename, uint8_t namelength) +{ + //This routine allows the file transfer to be run with a function call of sendFile(filename, sizeof(filename)); + memcpy(SDDTfilenamebuff, filename, namelength); //copy the name of file into global filename array for use outside this function + + uint8_t localattempts = 0; + + SDDTErrors = 0; //clear all error flags + SDDTDestinationFileCRC = 0; + SDDTSourceFileCRC = 0; + SDDTDestinationFileLength = 0; + SDDTSourceFileLength = 0; + + do + { + localattempts++; + SDNoAckCount = 0; + SDDTStartmS = millis(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Send file attempt ")); + Monitorport.println(localattempts); +#endif + + //opens the local file to send and sets up transfer parameters + if (SDstartFileTransfer(filename, namelength)) + { +#ifdef ENABLEMONITOR + Monitorport.print(filename); + Monitorport.println(F(" opened OK on remote")); + SDprintLocalFileDetails(); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("********************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR opening file")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("********************")); +#endif + SDDTFileTransferComplete = false; + delay(2000); + continue; + } + + delay(FunctionDelaymS); + + if (!SDsendSegments()) + { +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR in SDsendSegments()")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("***********************")); + Monitorport.println(); +#endif + SDDTFileTransferComplete = false; + continue; + } + + delay(FunctionDelaymS); + + if (SDendFileTransfer(filename, namelength)) //send command to close remote file + { + SDDTSendmS = millis() - SDDTStartmS; //record time taken for transfer + beginarrayRW(SDDTheader, 4); + SDDTDestinationFileLength = arrayReadUint32(); +#ifdef ENABLEMONITOR + Monitorport.print(filename); + Monitorport.println(F(" closed OK on remote")); + Monitorport.print(F("Acknowledged remote destination file length ")); + Monitorport.println(SDDTDestinationFileLength); +#endif + + if (SDDTDestinationFileLength != SDDTSourceFileLength) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("*******************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR file lengths do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("*******************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File lengths match")); +#endif + } +#ifdef ENABLEFILECRC + + SDDTDestinationFileCRC = arrayReadUint16(); +#ifdef ENABLEMONITOR + Monitorport.print(F("Acknowledged remote destination file CRC 0x")); + Monitorport.println(SDDTDestinationFileCRC, HEX); +#endif + + if (SDDTDestinationFileCRC != SDDTSourceFileCRC) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("****************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR file CRCs do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("****************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File CRCs match")); +#endif + } +#endif + //end of ENABLEFILECRC + SDDTFileTransferComplete = true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("******************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR close remote file failed")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("******************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + } + while ((!SDDTFileTransferComplete) && (localattempts < StartAttempts)); + + SDDTFileTransferComplete = true; + + SDDTsendSecs = (float) SDDTSendmS / 1000; + +#ifdef ENABLEMONITOR + Monitorport.print(F("StartAttempts ")); + Monitorport.println(localattempts); + Monitorport.print(F("SDNoAckCount ")); + Monitorport.println(SDNoAckCount); + Monitorport.print(F("Transmit time ")); + Monitorport.print(SDDTsendSecs, 3); + Monitorport.println(F("secs")); + Monitorport.print(F("Transmit rate ")); + Monitorport.print( (SDDTDestinationFileLength * 8) / (SDDTsendSecs), 0 ); + Monitorport.println(F("bps")); +#endif + + if (localattempts == StartAttempts) + { + bitSet(SDDTErrors, SDSendFile); + return 0; + } + + return SDDTDestinationFileLength; +} + + +bool SDstartFileTransfer(char *filename, uint8_t filenamesize) +{ + //Start file transfer, open local file first then remote file. + + uint8_t ValidACK; + uint8_t localattempts = 0; + +#ifdef ENABLEMONITOR + Monitorport.print(F("Start file transfer for ")); + Monitorport.println(filename); +#endif + SDDTSourceFileLength = DTSD_openFileRead(filename); //get the file length + + if (SDDTSourceFileLength == 0) + { +#ifdef ENABLEMONITOR + Monitorport.print(F("Error - opening file")); + Monitorport.println(filename); +#endif + bitSet(SDDTErrors, SDOpeningFile); + return false; + } + +#ifdef ENABLEFILECRC + SDDTSourceFileCRC = DTSD_fileCRCCCITT(SDDTSourceFileLength); //get file CRC from position 0 to end +#endif + + SDDTNumberSegments = DTSD_getNumberSegments(SDDTSourceFileLength, SegmentSize); + SDDTLastSegmentSize = DTSD_getLastSegmentSize(SDDTSourceFileLength, SegmentSize); + SDbuild_DTFileOpenHeader(SDDTheader, DTFileOpenHeaderL, filenamesize, SDDTSourceFileLength, SDDTSourceFileCRC, SegmentSize); + SDLocalPayloadCRC = LoRa.CRCCCITT((uint8_t *) filename, filenamesize, 0xFFFF); + + do + { + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.println(F("Send open remote file request")); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + SDTXPacketL = LoRa.transmitDT(SDDTheader, DTFileOpenHeaderL, (uint8_t *) filename, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDTXNetworkID = LoRa.getTXNetworkID(SDTXPacketL); //get the networkID appended to packet + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDTXPacketL); //get the payload CRC appended to packet + Monitorport.print(F("Send attempt ")); + Monitorport.println(localattempts); + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.println(SDTXNetworkID, HEX); + Monitorport.print(F("SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); +#endif +#endif + + if (SDTXPacketL == 0) //if there has been a send and ack error, SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDT(SDDTheader, DTFileOpenHeaderL, ACKopentimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if ((ValidACK > 0) && (SDRXPacketType == DTFileOpenACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F(" Valid ACK ")); +#endif +#endif + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#ifdef DEBUG + SDprintACKdetail(); + Monitorport.print(F(" ACKPacket ")); + SDprintPacketHex(); +#endif +#endif + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDStartTransfer); + return false; + } + + return true; +} + + +bool SDsendSegments() +{ + //Start the file transfer at segment 0 + SDDTSegment = 0; + SDDTSentSegments = 0; + + dataFile.seek(0); //ensure at first position in file + + while (SDDTSegment < (SDDTNumberSegments - 1)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintSeconds(); +#endif +#endif + + if (SDsendFileSegment(SDDTSegment, SegmentSize)) + { + SDDTSentSegments++; + } + else + { + bitSet(SDDTErrors, SDSendSegment); + return false; + } + delay(FunctionDelaymS); + }; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Last segment")); +#endif + + if (!SDsendFileSegment(SDDTSegment, SDDTLastSegmentSize)) + { + bitSet(SDDTErrors, SDSendSegment); + return false; + } + + return true; +} + + +bool SDsendFileSegment(uint16_t segnum, uint8_t segmentsize) +{ + //Send file segment as payload in a DT packet + + uint8_t ValidACK; + uint8_t localattempts = 0; + + DTSD_readFileSegment(SDDTdata, segmentsize); + SDbuild_DTSegmentHeader(SDDTheader, DTSegmentWriteHeaderL, segmentsize, segnum); + +#ifdef ENABLEMONITOR + +#ifdef PRINTSEGMENTNUM + Monitorport.println(segnum); +#endif + +#ifdef DEBUG + SDprintheader(SDDTheader, DTSegmentWriteHeaderL); + Monitorport.print(F(" ")); + SDprintdata(SDDTdata, 16); //print first 16 bytes data array +#endif + +#endif + + do + { + localattempts++; + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + SDTXPacketL = LoRa.transmitDT(SDDTheader, DTSegmentWriteHeaderL, (uint8_t *) SDDTdata, segmentsize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + if (SDTXPacketL == 0) //if there has been an error SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDT(SDDTheader, DTSegmentWriteHeaderL, ACKsegtimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if (ValidACK > 0) + { + if (SDRXPacketType == DTSegmentWriteNACK) + { + SDDTSegment = SDDTheader[4] + (SDDTheader[5] << 8); //load what the segment number should be + SDRXHeaderL = SDDTheader[2]; + DTSD_seekFileLocation(SDDTSegment * SegmentSize); +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************************")); + Monitorport.print(F("Received restart request at segment ")); + Monitorport.println(SDDTSegment); + SDprintheader(SDDTheader, SDRXHeaderL); + Monitorport.println(); + Monitorport.print(F("Seek to file location ")); + Monitorport.println(SDDTSegment * SegmentSize); + Monitorport.println(F("************************************")); + Monitorport.println(); + Monitorport.flush(); +#endif + + } + + //ack is valid, segment was acknowledged if here + + if (SDRXPacketType == DTStartNACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Received restart request")); +#endif + return false; + } + + if (SDRXPacketType == DTSegmentWriteACK) + { + SDAckCount++; + SDDTSegment++; //increase value for next segment +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintAckBrief(); +#endif +#endif + return true; + } + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#endif + + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } + } + } while ((ValidACK == 0) && (localattempts < SendAttempts)) ; + + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDSendSegment); + return 0; + } + + return true; +} + + +bool SDendFileTransfer(char *filename, uint8_t filenamesize) +{ + //End file transfer, close local file first then remote file + + uint8_t ValidACK; + uint8_t localattempts = 0; + + DTSD_closeFile(); + SDbuild_DTFileCloseHeader(SDDTheader, DTFileCloseHeaderL, filenamesize, SDDTSourceFileLength, SDDTSourceFileCRC, SegmentSize); + + do + { + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.println(F("Send close remote file")); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + SDTXPacketL = LoRa.transmitDT(SDDTheader, DTFileCloseHeaderL, (uint8_t *) filename, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + SDTXNetworkID = LoRa.getTXNetworkID(SDTXPacketL); + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDTXPacketL); + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.print(SDTXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Monitorport.print(F(",SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Monitorport.println(); +#endif +#endif + + if (SDTXPacketL == 0) //if there has been a send and ack error, SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDT(SDDTheader, DTFileCloseHeaderL, ACKclosetimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if ((ValidACK > 0) && (SDRXPacketType == DTFileCloseACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintPacketHex(); +#endif +#endif + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#endif + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(); + Monitorport.print(F(" ACKPacket ")); + SDprintPacketHex(); + Monitorport.println(); +#endif +#endif + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDendTransfer); + return 0; + } + + return true; +} + + +void SDbuild_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //This builds the header buffer for the filename to send + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileOpen); //byte 0, write the packet type + arrayWriteUint8(SDDTflags); //byte 1, SDDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void SDbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum) +{ + //This builds the header buffer for a segment transmit + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTSegmentWrite); //write the packet type + arrayWriteUint8(SDDTflags); //SDDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint16(segnum); //write the DTsegment number + endarrayRW(); +} + + +void SDbuild_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //This builds the header buffer for the filename passed + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileClose); //byte 0, write the packet type + arrayWriteUint8(SDDTflags); //byte 1, SDDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void SDprintLocalFileDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Source file length ")); + Monitorport.print(SDDTSourceFileLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEFILECRC + Monitorport.print(F("Source file CRC is 0x")); + Monitorport.println(SDDTSourceFileCRC, HEX); +#endif + Monitorport.print(F("Segment Size ")); + Monitorport.println(SegmentSize); + Monitorport.print(F("Number segments ")); + Monitorport.println(SDDTNumberSegments); + Monitorport.print(F("Last segment size ")); + Monitorport.println(SDDTLastSegmentSize); +#endif +} + + +void SDprintSeconds() +{ +#ifdef ENABLEMONITOR + float secs; + secs = ( (float) millis() / 1000); + Monitorport.print(secs, 2); + Monitorport.print(F(" ")); +#endif +} + + +void SDprintAckBrief() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.println(F("dBm")); +#endif +} + + +void SDprintAckReception() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + SDPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F("SDAckCount,")); + Monitorport.print(SDAckCount); + Monitorport.print(F(",SDNoAckCount,")); + Monitorport.print(SDNoAckCount); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm,AckSNR,")); + Monitorport.print(SDPacketSNR); + Monitorport.print(F("dB")); + Monitorport.println(); +#endif +} + + +void SDprintACKdetail() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("ACKDetail")); + Monitorport.print(F(",RXNetworkID,0x")); + Monitorport.print(LoRa.getRXNetworkID(SDRXPacketL), HEX); + Monitorport.print(F(",RXPayloadCRC,0x")); + Monitorport.print(LoRa.getRXPayloadCRC(SDRXPacketL), HEX); + Monitorport.print(F(",SDRXPacketL,")); + Monitorport.print(SDRXPacketL); + Monitorport.print(F(" ")); + SDprintReliableStatus(); + Monitorport.println(); +#endif +} + + +void SDprintdata(uint8_t *dataarray, uint8_t arraysize) +{ + SDUNUSED(dataarray); //to prevent a compiler warning + SDUNUSED(arraysize); //to prevent a compiler warning +#ifdef ENABLEMONITOR + Monitorport.print(F("DataBytes,")); + Monitorport.print(arraysize); + Monitorport.print(F(" ")); + printarrayHEX((uint8_t *) dataarray, 16); //There is a lot of data to print so only print first 16 bytes +#endif +} + + +bool SDsendDTInfo() +{ + //Send array info packet, for this implmentation its really only the flags in ARDTflags that is sent + + uint8_t ValidACK = 0; + uint8_t localattempts = 0; + + SDbuild_DTInfoHeader(SDDTheader, DTInfoHeaderL, 0); + + do + { + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.print(F("Send DTInfo packet attempt ")); + Monitorport.println(localattempts); +#endif + SDTXPacketL = LoRa.transmitDT(SDDTheader, DTInfoHeaderL, (uint8_t *) SDDTdata, 0, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (SDTXPacketL == 0) //if there has been an error ARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + continue; + } + + ValidACK = LoRa.waitACKDT(SDDTheader, DTInfoHeaderL, ACKsegtimeoutmS); + + if (ValidACK > 0) + { + //ack is a valid reliable packet + SDRXPacketType = SDDTheader[0]; +#ifdef ENABLEMONITOR + Monitorport.print(F("ACK Packet type 0x")); + Monitorport.println(SDRXPacketType, HEX); +#endif + + if (SDRXPacketType == DTInfoACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK received")); +#endif + SDAckCount++; + SDRXPacketType = SDDTheader[0]; + return true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK not received")); +#endif + return false; + } + + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("No valid ACK received ")); +#endif + + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + return false; + } + } + delay(PacketDelaymS); + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDSendPacket); + return false; + } + return true; +} + + +void SDbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen) +{ + //This builds the header buffer for a info only header, faults problems etc + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTInfo); //write the packet type + arrayWriteUint8(SDDTflags); //ARDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + endarrayRW(); +} + + +//************************************************ +//Receiver mode functions +//************************************************ + + +bool SDreceiveaPacketDT() +{ + //Receive Data transfer packets + + SDRXPacketType = 0; + SDRXPacketL = LoRa.receiveDT(SDDTheader, HeaderSizeMax, (uint8_t *) SDDTdata, DataSizeMax, NetworkID, RXtimeoutmS, WAIT_RX); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + +#ifdef DEBUG + SDprintSeconds(); +#endif + + if (SDRXPacketL > 0) + { + //if the LT.receiveDT() returns a value > 0 for SDRXPacketL then packet was received OK + SDreadHeaderDT(); //get the basic header details into global variables SDRXPacketType etc + SDprocessPacket(SDRXPacketType); //process and act on the packet + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return true; + } + else + { + //if the LoRa.receiveDT() function detects an error RXOK is 0 + uint16_t IRQStatus = LoRa.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("RX Timeout")); +#endif + } + else + { + SDRXErrors++; +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("PacketError")); + SDprintPacketDetails(); + SDprintReliableStatus(); + Monitorport.println(); +#endif +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + } + } + return false; +} + + +void SDreadHeaderDT() +{ + //The first 6 bytes of the header contain the important stuff, so load it up + //so we can decide what to do next. + beginarrayRW(SDDTheader, 0); //start buffer read at location 0 + SDRXPacketType = arrayReadUint8(); //load the packet type + SDRXFlags = arrayReadUint8(); //SDDTflags byte + SDRXHeaderL = arrayReadUint8(); //load the header length + SDRXDataarrayL = arrayReadUint8(); //load the datalength + SDDTSegment = arrayReadUint16(); //load the segment number +} + + +bool SDprocessPacket(uint8_t packettype) +{ + //Decide what to do with an incoming packet + + if (packettype == DTSegmentWrite) + { + SDprocessSegmentWrite(); + return true; + } + + if (packettype == DTFileOpen) + { + SDprocessFileOpen(SDDTdata, SDRXDataarrayL); + return true; + } + + if (packettype == DTFileClose) + { + SDprocessFileClose(); + return true; + } + + return true; +} + + +void SDprintPacketDetails() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + SDPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F(" RSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm")); + +#ifdef DEBUG + Monitorport.print(F(",SNR,")); + Monitorport.print(SDPacketSNR); + Monitorport.print(F("dBm,RXOKCount,")); + Monitorport.print(SDDTReceivedSegments); + Monitorport.print(F(",RXErrs,")); + Monitorport.print(SDRXErrors); + Monitorport.print(F(" RX")); + SDprintheader(SDDTheader, SDRXHeaderL); +#endif +#endif +} + + +bool SDprocessSegmentWrite() +{ + //There is a request to write a segment to file on receiver + //checks that the sequence of segment writes is correct + + if (!SDDTFileOpened) + { + //something is wrong, have received a request to write a segment but there is no file opened + //need to reject the segment write with a restart NACK +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("***************************************************")); + Monitorport.println(F("Error - Segment write with no file open - send NACK")); + Monitorport.println(F("***************************************************")); + Monitorport.println(); +#endif + SDDTheader[0] = DTStartNACK; + delay(ACKdelaymS); + delay(DuplicatedelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTStartHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return false; + } + + if (SDDTSegment == SDDTSegmentNext) + { + DTSD_writeSegmentFile(SDDTdata, SDRXDataarrayL); + +#ifdef ENABLEMONITOR +#ifdef PRINTSEGMENTNUM + //Monitorport.print(F("Segment,")); + Monitorport.println(SDDTSegment); +#endif + +#ifdef DEBUG + Monitorport.print(F("Bytes,")); + Monitorport.print(SDRXDataarrayL); + SDprintPacketRSSI(); + Monitorport.println(F(" SendACK")); +#endif +#endif + SDDTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTSegmentWriteHeaderL, TXpower); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + SDDTReceivedSegments++; + SDDTSegmentLast = SDDTSegment; //so we can tell if sequece has been received twice + SDDTSegmentNext = SDDTSegment + 1; + return true; + } + + if (SDDTSegment == SDDTSegmentLast) + { +#ifdef ENABLEMONITOR + Monitorport.print(F("ERROR segment ")); + Monitorport.print(SDDTSegment); + Monitorport.println(F(" already received ")); + delay(DuplicatedelaymS); +#ifdef DEBUG + SDprintPacketDetails(); + SDprintPacketRSSI(); +#endif +#endif + SDDTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTSegmentWriteHeaderL, TXpower); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return true; + } + + if (SDDTSegment != SDDTSegmentNext ) + { + SDDTheader[0] = DTSegmentWriteNACK; + SDDTheader[4] = lowByte(SDDTSegmentNext); + SDDTheader[5] = highByte(SDDTSegmentNext); + delay(ACKdelaymS); + delay(DuplicatedelaymS); //add an extra delay here to stop repeated segment sends + +#ifdef ENABLEMONITOR + Monitorport.print(F(" ERROR Received Segment ")); + Monitorport.print(SDDTSegment); + Monitorport.print(F(" expected ")); + Monitorport.print(SDDTSegmentNext); + Monitorport.print(F(" ")); + +#ifdef DEBUG + SDprintPacketDetails(); + SDprintPacketRSSI(); +#endif + + Monitorport.print(F(" Send NACK for segment ")); + Monitorport.print(SDDTSegmentNext); + Monitorport.println(); + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.print(F("Transmit restart request for segment ")); + Monitorport.println(SDDTSegmentNext); + SDprintheader(SDDTheader, SDRXHeaderL); + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.println(); + Monitorport.flush(); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTSegmentWriteHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return false; + } + + return true; +} + + +bool SDprocessFileOpen(uint8_t *filename, uint8_t filenamesize) +{ + //There is a request to open local file on receiver + + SDDTDestinationFileCRC = 0; //CRC of complete file received + SDDTDestinationFileLength = 0; //length of file written on the destination\receiver + + beginarrayRW(SDDTheader, 4); //start buffer read at location 4 + SDDTSourceFileLength = arrayReadUint32(); //load the file length of the remote file being sent + SDDTSourceFileCRC = arrayReadUint16(); //load the CRC of the source file being sent + memset(SDDTfilenamebuff, 0, Maxfilenamesize); //clear SDDTfilenamebuff to all 0s + memcpy(SDDTfilenamebuff, filename, filenamesize); //copy received SDDTdata into SDDTfilenamebuff + +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.print(F(" SD File Open request")); + Monitorport.println(); + +#ifdef DEBUG + SDTXNetworkID = LoRa.getTXNetworkID(SDRXPacketL); //get the networkID appended to packet + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDRXPacketL); //get the payload CRC appended to packet + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.println(SDTXNetworkID, HEX); + Monitorport.print(F("SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); +#endif + + SDprintSourceFileDetails(); + + if bitRead(SDRXFlags, SDNoFileSave) + { + Monitorport.println(F("Remote did not save file to SD")); + } +#endif + + if (DTSD_openNewFileWrite(SDDTfilenamebuff)) //open file for write at beginning, delete if it exists + { +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" DT File Opened OK")); + Monitorport.println(F("Waiting transfer")); +#endif + SDDTSegmentNext = 0; //since file is opened the next sequence should be the first + SDDTFileOpened = true; + SDDTStartmS = millis(); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" File Open fail")); +#endif + SDDTFileOpened = false; + return false; + } + + SDDTStartmS = millis(); + delay(ACKdelaymS); + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("Sending ACK")); +#endif +#endif + + SDDTheader[0] = DTFileOpenACK; //set the ACK packet type + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTFileOpenHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + SDDTSegmentNext = 0; //after file open, segment 0 is next + + return true; +} + + +bool SDprocessFileClose() +{ + // There is a request to close a file on SD of receiver + +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" File close request")); +#endif + + SDDTFileClosed = false; + + if (SDDTFileOpened) //check if file has been opened, close it if it is + { + if (SD.exists(SDDTfilenamebuff)) //check if file exists + { + DTSD_closeFile(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Transfer time ")); + Monitorport.print(millis() - SDDTStartmS); + Monitorport.print(F("mS")); + Monitorport.println(); + Monitorport.println(F("File closed")); +#endif + + SDDTFileOpened = false; + SDDTDestinationFileLength = DTSD_openFileRead(SDDTfilenamebuff); + +#ifdef ENABLEFILECRC + SDDTDestinationFileCRC = DTSD_fileCRCCCITT(SDDTDestinationFileLength); +#endif + + beginarrayRW(SDDTheader, 4); //start writing to array at location 12 + arrayWriteUint32(SDDTDestinationFileLength); //write file length of file just written just written to ACK header + arrayWriteUint16(SDDTDestinationFileCRC); //write CRC of file just written to ACK header + +#ifdef ENABLEMONITOR + SDprintDestinationFileDetails(); +#endif + } + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File already closed")); +#endif + delay(DuplicatedelaymS); + } + + delay(ACKdelaymS); +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("Sending ACK")); +#endif +#endif + SDDTheader[0] = DTFileCloseACK; + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDT(SDDTheader, DTFileCloseHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + + SDDTFileClosed = true; + return true; +} + + +void SDprintPacketRSSI() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + Monitorport.print(F(" RSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm")); +#endif +} + + +void SDprintSourceFileDetails() +{ + +#ifdef ENABLEMONITOR + Monitorport.print(F("Source file length is ")); + Monitorport.print(SDDTSourceFileLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEFILECRC + Monitorport.print(F("Source file CRC is 0x")); + Monitorport.println(SDDTSourceFileCRC, HEX); +#endif +#endif +} + + +void SDprintDestinationFileDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Destination file length ")); + Monitorport.print(SDDTDestinationFileLength); + Monitorport.println(F(" bytes")); + if (SDDTDestinationFileLength != SDDTSourceFileLength) + { + Monitorport.println(F("ERROR - file lengths do not match")); + } + else + { + Monitorport.println(F("File lengths match")); + } + +#ifdef ENABLEFILECRC + Monitorport.print(F("Destination file CRC is 0x")); + Monitorport.println(SDDTDestinationFileCRC, HEX); + if (SDDTDestinationFileCRC != SDDTSourceFileCRC) + { + Monitorport.println(F("ERROR - file CRCs do not match")); + } + else + { + Monitorport.println(F("File CRCs match")); + } +#endif + +#endif +} + + +//************************************************ +//Common functions +//************************************************ + +void SDsetLED(int8_t pinnumber) +{ + if (pinnumber >= 0) + { + SDDTLED = pinnumber; + pinMode(pinnumber, OUTPUT); + } +} + + +void SDprintheader(uint8_t *header, uint8_t headersize) +{ + SDUNUSED(header); //to prevent a compiler warning + SDUNUSED(headersize); //to prevent a compiler warning + +#ifdef ENABLEMONITOR + Monitorport.print(F("HeaderBytes,")); + Monitorport.print(headersize); + Monitorport.print(F(" ")); + printarrayHEX(header, headersize); +#endif +} + + +void printArrayHEX(uint8_t *buff, uint32_t len) +{ + SDUNUSED(buff); //to prevent a compiler warning + SDUNUSED(len); //to prevent a compiler warning + +#ifdef ENABLEMONITOR + uint8_t index, buffdata; + for (index = 0; index < len; index++) + { + buffdata = buff[index]; + if (buffdata < 16) + { + Monitorport.print(F("0")); + } + Monitorport.print(buffdata, HEX); + Monitorport.print(F(" ")); + } +#endif +} + + +void SDprintReliableStatus() +{ + +#ifdef ENABLEMONITOR + + uint8_t reliableErrors = LoRa.readReliableErrors(); + uint8_t reliableFlags = LoRa.readReliableFlags(); + + if (bitRead(reliableErrors, ReliableCRCError)) + { + Monitorport.print(F(",ReliableCRCError")); + } + + if (bitRead(reliableErrors, ReliableIDError)) + { + Monitorport.print(F(",ReliableIDError")); + } + + if (bitRead(reliableErrors, ReliableSizeError)) + { + Monitorport.print(F(",ReliableSizeError")); + } + + if (bitRead(reliableErrors, ReliableACKError)) + { + Monitorport.print(F(",NoReliableACK")); + } + + if (bitRead(reliableErrors, ReliableTimeout)) + { + Monitorport.print(F(",ReliableTimeout")); + } + + if (bitRead(reliableFlags, ReliableACKSent)) + { + Monitorport.print(F(",ACKsent")); + } + + if (bitRead(reliableFlags, ReliableACKReceived)) + { + Monitorport.print(F(",ACKreceived")); + } +#endif +} + + +void SDprintPacketHex() +{ +#ifdef ENABLEMONITOR + uint8_t packetlen = LoRa.readRXPacketL(); + Monitorport.print(packetlen); + Monitorport.print(F(" bytes > ")); + if (packetlen > 0) + { + LoRa.printSXBufferHEX(0, packetlen - 1); + } +#endif +} diff --git a/src/SDtransferIRQ.h b/src/SDtransferIRQ.h new file mode 100644 index 0000000..a10ec18 --- /dev/null +++ b/src/SDtransferIRQ.h @@ -0,0 +1,1518 @@ +/******************************************************************************************************* + Programs for Arduino - Copyright of the author Stuart Robinson - 14/03/22 + + The functions expect the calling sketch to create an instance called LoRa, so that functions + are called like this; LoRa.getSDTXNetworkID(). + + This code is supplied as is, it is up to the user of the program to decide if the program is suitable + for the intended purpose and free from errors. + + There is a copy of this file in the SX12XX-LoRa library \src folder, but the file can be copied to the + sketch folder and used locally. In this way its possible to carry out custom modifications. + + Extensive use is made of #defines to allow the monotoring and debug prints to serial monitor to be + turned off, this is to allow for the circumstance where the primary serial port is in use for serial + file transfers to a PC or similar. To see the serial prints you need to have this set of #defines; + + #define Monitorport Serial + #define ENABLEMONITOR + + This instance of the SDtransfer liubrary routines allows for the LoRa device to be used without a RX + or TX done pin. + +*******************************************************************************************************/ +/* + ToDo: + +*/ + +//110122 added local function printArrayHEX(uint8_t *buff, uint32_t len) +//130122 Made variable and function names unique so that the array transfer routines can be used in the same program +//130122 Converted all Serial prints to Monitorport.print() format +//140322 added #ifdef ENABLEMONITOR to serial prints + + +#define SDUNUSED(v) (void) (v) //add SDUNUSED(variable); to avoid compiler warnings + + +#ifndef Monitorport +#define Monitorport Serial +#endif + +#include +//#define DEBUG //enable this define to print additional debug info for segment transfers + +uint8_t SDRXPacketL; //length of received packet +uint8_t SDRXPacketType; //type of received packet, segment write, ACK, NACK etc +uint8_t SDRXHeaderL; //length of header +int16_t SDPacketRSSI; //stores RSSI of received packet +int8_t SDPacketSNR; //stores signal to noise ratio of received packet +uint16_t SDAckCount; //keep a count of acks that are received within timeout period +uint16_t SDNoAckCount; //keep a count of acks not received within timeout period +uint16_t SDDTDestinationFileCRC; //CRC of complete file received +uint32_t SDDTDestinationFileLength; //length of file written on the destination\receiver +uint16_t SDDTSourceFileCRC; //CRC returned of the remote saved file +uint32_t SDDTSourceFileLength; //length of file at source\transmitter +uint32_t SDDTStartmS; //used for timeing transfers +uint16_t SDDTSegment = 0; //current segment number +char SDDTfilenamebuff[Maxfilenamesize]; //global buffer to store current filename +uint8_t SDDTheader[16]; //header array +uint8_t SDDTdata[245]; //data/segment array +uint8_t SDDTflags = 0; //Flags byte used to pass status information between nodes +int SDDTLED = -1; //pin number for indicator LED, if -1 then not used +uint16_t SDDTErrors; //used for tracking errors in the transfer process + +uint16_t SDTXNetworkID; //this is used to store the 'network' number from packet received, receiver must have the same networkID +uint16_t SDTXArrayCRC; //should contain CRC of data array transmitted +uint8_t SDTXPacketL; //length of transmitted packet +uint16_t SDLocalPayloadCRC; //for calculating the local data array CRC +uint8_t SDDTLastSegmentSize; //size of the last segment +uint16_t SDDTNumberSegments; //number of segments for a file transfer +uint16_t SDDTSentSegments; //count of segments sent +bool SDDTFileTransferComplete; //bool to flag file transfer complete +uint32_t SDDTSendmS; //used for timing transfers +float SDDTsendSecs; //seconds to transfer a file + +uint16_t SDRXErrors; //count of packets received with error +uint8_t SDRXFlags; //SDDTflags byte in header, could be used to control actions in TX and RX +uint8_t SDRXDataarrayL; //length of data array\segment +bool SDDTFileOpened; //bool to flag when file has been opened +uint16_t SDDTSegmentNext; //next segment expected +uint16_t SDDTReceivedSegments; //count of segments received +uint16_t SDDTSegmentLast; //last segment processed + +//Transmitter mode functions +uint32_t SDsendFile(char *filename, uint8_t namelength); +bool SDstartFileTransfer(char *filename, uint8_t filenamesize); +bool SDsendSegments(); +bool SDsendFileSegment(uint16_t segnum, uint8_t segmentsize); +bool SDendFileTransfer(char *filename, uint8_t filenamesize); +void SDbuild_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void SDbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum); +void SDbuild_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize); +void SDprintLocalFileDetails(); +void SDprintSeconds(); +void SDprintAckBrief(); +void SDprintAckReception(); +void SDprintACKdetail(); +void SDprintdata(uint8_t *dataarray, uint8_t arraysize); +void SDprintPacketHex(); +bool SDsendDTInfo(); +void SDbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen); + +//Receiver mode functions +bool SDreceiveaPacketDT(); +void SDreadHeaderDT(); +bool SDprocessPacket(uint8_t packettype); +void SDprintPacketDetails(); +bool SDprocessSegmentWrite(); +bool SDprocessFileOpen(uint8_t *filename, uint8_t filenamesize); +bool SDprocessFileClose(); +void SDprintPacketRSSI(); +void SDprintSourceFileDetails(); +void SDprintDestinationFileDetails(); + +//Common functions +void SDsetLED(int8_t pinnumber); +void SDprintheader(uint8_t *header, uint8_t headersize); +void SDprintReliableStatus(); + +//bit numbers used by SDDTErrors (16bits) and RXErrors (first 8bits) +const uint8_t SDNoFileSave = 0; //bit number of SDDTErrors to set when no file save, to SD for example +const uint8_t SDNothingToSend = 1; //bit number of SDDTErrors to set when nothing to send or unable to send image\file +const uint8_t SDNoCamera = 1; //bit number of SDDTErrors to set when camera fails +const uint8_t SDSendFile = 2; //bit number of SDDTErrors to set when file SD file image\file send fail +const uint8_t SDSendArray = 2; //bit number of SDDTErrors to set when file array image\file send fail +const uint8_t SDNoACKlimit = 3; //bit number of SDDTErrors to set when NoACK limit reached +const uint8_t SDSendPacket = 4; //bit number of SDDTErrors to set when sending a packet fails or there is no ack + +const uint8_t SDStartTransfer = 11; //bit number of SDDTErrors to set when StartTransfer fails +const uint8_t SDSendSegments = 12; //bit number of SDDTErrors to set when SendSegments function fails +const uint8_t SDSendSegment = 13; //bit number of SDDTErrors to set when sending a single Segment send fails +const uint8_t SDOpeningFile = 14; //bit number of SDDTErrors to set when opening file fails +const uint8_t SDendTransfer = 15; //bit number of SDDTErrors to set when end transfer fails + + +//************************************************ +//Transmit mode functions +//************************************************ + +uint32_t SDsendFile(char *filename, uint8_t namelength) +{ + //This routine allows the file transfer to be run with a function call of sendFile(filename, sizeof(filename)); + memcpy(SDDTfilenamebuff, filename, namelength); //copy the name of file into global filename array for use outside this function + + uint8_t localattempts = 0; + + SDDTErrors = 0; //clear all error flags + SDDTDestinationFileCRC = 0; + SDDTSourceFileCRC = 0; + SDDTDestinationFileLength = 0; + SDDTSourceFileLength = 0; + + do + { + localattempts++; + SDNoAckCount = 0; + SDDTStartmS = millis(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Send file attempt ")); + Monitorport.println(localattempts); +#endif + + //opens the local file to send and sets up transfer parameters + if (SDstartFileTransfer(filename, namelength)) + { +#ifdef ENABLEMONITOR + Monitorport.print(filename); + Monitorport.println(F(" opened OK on remote")); + SDprintLocalFileDetails(); +#endif + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("********************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR opening file")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("********************")); +#endif + SDDTFileTransferComplete = false; + delay(2000); + continue; + } + + delay(FunctionDelaymS); + + if (!SDsendSegments()) + { +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR in SDsendSegments()")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("***********************")); + Monitorport.println(); +#endif + SDDTFileTransferComplete = false; + continue; + } + + delay(FunctionDelaymS); + + if (SDendFileTransfer(filename, namelength)) //send command to close remote file + { + SDDTSendmS = millis() - SDDTStartmS; //record time taken for transfer + beginarrayRW(SDDTheader, 4); + SDDTDestinationFileLength = arrayReadUint32(); +#ifdef ENABLEMONITOR + Monitorport.print(filename); + Monitorport.println(F(" closed OK on remote")); + Monitorport.print(F("Acknowledged remote destination file length ")); + Monitorport.println(SDDTDestinationFileLength); +#endif + + if (SDDTDestinationFileLength != SDDTSourceFileLength) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("*******************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR file lengths do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("*******************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File lengths match")); +#endif + } +#ifdef ENABLEFILECRC + + SDDTDestinationFileCRC = arrayReadUint16(); +#ifdef ENABLEMONITOR + Monitorport.print(F("Acknowledged remote destination file CRC 0x")); + Monitorport.println(SDDTDestinationFileCRC, HEX); +#endif + + if (SDDTDestinationFileCRC != SDDTSourceFileCRC) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("****************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR file CRCs do not match")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("****************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File CRCs match")); +#endif + } +#endif + //end of ENABLEFILECRC + SDDTFileTransferComplete = true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("******************************")); + Monitorport.println(filename); + Monitorport.println(F("ERROR close remote file failed")); + Monitorport.println(F("Restarting transfer")); + Monitorport.println(F("******************************")); +#endif + SDDTFileTransferComplete = false; + continue; + } + } + while ((!SDDTFileTransferComplete) && (localattempts < StartAttempts)); + + SDDTsendSecs = (float) SDDTSendmS / 1000; + +#ifdef ENABLEMONITOR + Monitorport.print(F("StartAttempts ")); + Monitorport.println(localattempts); + Monitorport.print(F("SDNoAckCount ")); + Monitorport.println(SDNoAckCount); + Monitorport.print(F("Transmit time ")); + Monitorport.print(SDDTsendSecs, 3); + Monitorport.println(F("secs")); + Monitorport.print(F("Transmit rate ")); + Monitorport.print( (SDDTDestinationFileLength * 8) / (SDDTsendSecs), 0 ); + Monitorport.println(F("bps")); +#endif + + if (localattempts == StartAttempts) + { + bitSet(SDDTErrors, SDSendFile); + return 0; + } + + return SDDTDestinationFileLength; +} + + +bool SDstartFileTransfer(char *filename, uint8_t filenamesize) +{ + //Start file transfer, open local file first then remote file. + + uint8_t ValidACK; + uint8_t localattempts = 0; + +#ifdef ENABLEMONITOR + Monitorport.print(F("Start file transfer for ")); + Monitorport.println(filename); +#endif + SDDTSourceFileLength = DTSD_openFileRead(filename); //get the file length + + if (SDDTSourceFileLength == 0) + { +#ifdef ENABLEMONITOR + Monitorport.print(F("Error - opening file")); + Monitorport.println(filename); +#endif + bitSet(SDDTErrors, SDOpeningFile); + return false; + } + +#ifdef ENABLEFILECRC + SDDTSourceFileCRC = DTSD_fileCRCCCITT(SDDTSourceFileLength); //get file CRC from position 0 to end +#endif + + SDDTNumberSegments = DTSD_getNumberSegments(SDDTSourceFileLength, SegmentSize); + SDDTLastSegmentSize = DTSD_getLastSegmentSize(SDDTSourceFileLength, SegmentSize); + SDbuild_DTFileOpenHeader(SDDTheader, DTFileOpenHeaderL, filenamesize, SDDTSourceFileLength, SDDTSourceFileCRC, SegmentSize); + SDLocalPayloadCRC = LoRa.CRCCCITT((uint8_t *) filename, filenamesize, 0xFFFF); + + do + { + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.println(F("Send open remote file request")); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + SDTXPacketL = LoRa.transmitDTIRQ(SDDTheader, DTFileOpenHeaderL, (uint8_t *) filename, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDTXNetworkID = LoRa.getTXNetworkID(SDTXPacketL); //get the networkID appended to packet + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDTXPacketL); //get the payload CRC appended to packet + Monitorport.print(F("Send attempt ")); + Monitorport.println(localattempts); + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.println(SDTXNetworkID, HEX); + Monitorport.print(F("SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); +#endif +#endif + + if (SDTXPacketL == 0) //if there has been a send and ack error, SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDTIRQ(SDDTheader, DTFileOpenHeaderL, ACKopentimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if ((ValidACK > 0) && (SDRXPacketType == DTFileOpenACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F(" Valid ACK ")); +#endif +#endif + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#ifdef DEBUG + SDprintACKdetail(); + Monitorport.print(F(" ACKPacket ")); + SDprintPacketHex(); +#endif +#endif + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDStartTransfer); + return false; + } + + return true; +} + + +bool SDsendSegments() +{ + //Start the file transfer at segment 0 + SDDTSegment = 0; + SDDTSentSegments = 0; + + dataFile.seek(0); //ensure at first position in file + + while (SDDTSegment < (SDDTNumberSegments - 1)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintSeconds(); +#endif +#endif + + if (SDsendFileSegment(SDDTSegment, SegmentSize)) + { + SDDTSentSegments++; + } + else + { + bitSet(SDDTErrors, SDSendSegment); + return false; + } + delay(FunctionDelaymS); + }; + +#ifdef ENABLEMONITOR + Monitorport.println(F("Last segment")); +#endif + + if (!SDsendFileSegment(SDDTSegment, SDDTLastSegmentSize)) + { + bitSet(SDDTErrors, SDSendSegment); + return false; + } + + return true; +} + + +bool SDsendFileSegment(uint16_t segnum, uint8_t segmentsize) +{ + //Send file segment as payload in a DT packet + + uint8_t ValidACK; + uint8_t localattempts = 0; + + DTSD_readFileSegment(SDDTdata, segmentsize); + SDbuild_DTSegmentHeader(SDDTheader, DTSegmentWriteHeaderL, segmentsize, segnum); + +#ifdef ENABLEMONITOR + +#ifdef PRINTSEGMENTNUM + Monitorport.println(segnum); +#endif + +#ifdef DEBUG + SDprintheader(SDDTheader, DTSegmentWriteHeaderL); + Monitorport.print(F(" ")); + SDprintdata(SDDTdata, segmentsize); //print segment size of data array only +#endif + +#endif + + do + { + localattempts++; + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + SDTXPacketL = LoRa.transmitDTIRQ(SDDTheader, DTSegmentWriteHeaderL, (uint8_t *) SDDTdata, segmentsize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + if (SDTXPacketL == 0) //if there has been an error SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDTIRQ(SDDTheader, DTSegmentWriteHeaderL, ACKsegtimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if (ValidACK > 0) + { + if (SDRXPacketType == DTSegmentWriteNACK) + { + SDDTSegment = SDDTheader[4] + (SDDTheader[5] << 8); //load what the segment number should be + SDRXHeaderL = SDDTheader[2]; + DTSD_seekFileLocation(SDDTSegment * SegmentSize); +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("************************************")); + Monitorport.print(F("Received restart request at segment ")); + Monitorport.println(SDDTSegment); + SDprintheader(SDDTheader, SDRXHeaderL); + Monitorport.println(); + Monitorport.print(F("Seek to file location ")); + Monitorport.println(SDDTSegment * SegmentSize); + Monitorport.println(F("************************************")); + Monitorport.println(); + Monitorport.flush(); +#endif + + } + + //ack is valid, segment was acknowledged if here + + if (SDRXPacketType == DTStartNACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Received restart request")); +#endif + return false; + } + + if (SDRXPacketType == DTSegmentWriteACK) + { + SDAckCount++; + SDDTSegment++; //increase value for next segment +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintAckBrief(); +#endif +#endif + return true; + } + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#endif + + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } + } + } while ((ValidACK == 0) && (localattempts < SendAttempts)) ; + + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDSendSegment); + return 0; + } + + return true; +} + + +bool SDendFileTransfer(char *filename, uint8_t filenamesize) +{ + //End file transfer, close local file first then remote file + + uint8_t ValidACK; + uint8_t localattempts = 0; + + DTSD_closeFile(); + SDbuild_DTFileCloseHeader(SDDTheader, DTFileCloseHeaderL, filenamesize, SDDTSourceFileLength, SDDTSourceFileCRC, SegmentSize); + + do + { + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.println(F("Send close remote file")); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + SDTXPacketL = LoRa.transmitDTIRQ(SDDTheader, DTFileCloseHeaderL, (uint8_t *) filename, filenamesize, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + SDTXNetworkID = LoRa.getTXNetworkID(SDTXPacketL); + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDTXPacketL); + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.print(SDTXNetworkID, HEX); //get the NetworkID of the packet just sent, its placed at the packet end + Monitorport.print(F(",SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); //get the CRC of the data array just sent, its placed at the packet end + Monitorport.println(); +#endif +#endif + + if (SDTXPacketL == 0) //if there has been a send and ack error, SDTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + } + + ValidACK = LoRa.waitACKDTIRQ(SDDTheader, DTFileCloseHeaderL, ACKclosetimeoutmS); + SDRXPacketType = SDDTheader[0]; + + if ((ValidACK > 0) && (SDRXPacketType == DTFileCloseACK)) + { +#ifdef ENABLEMONITOR +#ifdef DEBUG + SDprintPacketHex(); +#endif +#endif + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("NoACK")); +#endif + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + bitSet(SDDTErrors, SDNoACKlimit); + return false; + } +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(); + Monitorport.print(F(" ACKPacket ")); + SDprintPacketHex(); + Monitorport.println(); +#endif +#endif + } + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDendTransfer); + return 0; + } + + return true; +} + + +void SDbuild_DTFileOpenHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //This builds the header buffer for the filename to send + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileOpen); //byte 0, write the packet type + arrayWriteUint8(SDDTflags); //byte 1, SDDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void SDbuild_DTSegmentHeader(uint8_t *header, uint8_t headersize, uint8_t datalen, uint16_t segnum) +{ + //This builds the header buffer for a segment transmit + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTSegmentWrite); //write the packet type + arrayWriteUint8(SDDTflags); //SDDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint16(segnum); //write the DTsegment number + endarrayRW(); +} + + +void SDbuild_DTFileCloseHeader(uint8_t *header, uint8_t headersize, uint8_t datalength, uint32_t filelength, uint16_t filecrc, uint8_t segsize) +{ + //This builds the header buffer for the filename passed + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTFileClose); //byte 0, write the packet type + arrayWriteUint8(SDDTflags); //byte 1, SDDTflags byte + arrayWriteUint8(headersize); //byte 2, write length of header + arrayWriteUint8(datalength); //byte 3, write length of dataarray + arrayWriteUint32(filelength); //byte 4,5,6,7, write the file length + arrayWriteUint16(filecrc); //byte 8, 9, write file CRC + arrayWriteUint8(segsize); //byte 10, segment size + arrayWriteUint8(0); //byte 11, unused byte + endarrayRW(); +} + + +void SDprintLocalFileDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Source file length ")); + Monitorport.print(SDDTSourceFileLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEFILECRC + Monitorport.print(F("Source file CRC is 0x")); + Monitorport.println(SDDTSourceFileCRC, HEX); +#endif + Monitorport.print(F("Segment Size ")); + Monitorport.println(SegmentSize); + Monitorport.print(F("Number segments ")); + Monitorport.println(SDDTNumberSegments); + Monitorport.print(F("Last segment size ")); + Monitorport.println(SDDTLastSegmentSize); +#endif +} + + +void SDprintSeconds() +{ +#ifdef ENABLEMONITOR + float secs; + secs = ( (float) millis() / 1000); + Monitorport.print(secs, 2); + Monitorport.print(F(" ")); +#endif +} + + +void SDprintAckBrief() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.println(F("dBm")); +#endif +} + + +void SDprintAckReception() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + SDPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F("SDAckCount,")); + Monitorport.print(SDAckCount); + Monitorport.print(F(",SDNoAckCount,")); + Monitorport.print(SDNoAckCount); + Monitorport.print(F(",AckRSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm,AckSNR,")); + Monitorport.print(SDPacketSNR); + Monitorport.print(F("dB")); + Monitorport.println(); +#endif +} + + +void SDprintACKdetail() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("ACKDetail")); + Monitorport.print(F(",RXNetworkID,0x")); + Monitorport.print(LoRa.getRXNetworkID(SDRXPacketL), HEX); + Monitorport.print(F(",RXPayloadCRC,0x")); + Monitorport.print(LoRa.getRXPayloadCRC(SDRXPacketL), HEX); + Monitorport.print(F(",SDRXPacketL,")); + Monitorport.print(SDRXPacketL); + Monitorport.print(F(" ")); + SDprintReliableStatus(); + Monitorport.println(); +#endif +} + + +void SDprintdata(uint8_t *dataarray, uint8_t arraysize) +{ + SDUNUSED(dataarray); //to prevent a compiler warning + SDUNUSED(arraysize); //to prevent a compiler warning +#ifdef ENABLEMONITOR + Monitorport.print(F("DataBytes,")); + Monitorport.print(arraysize); + Monitorport.print(F(" ")); + printarrayHEX((uint8_t *) dataarray, 16); //There is a lot of data to print so only print first 16 bytes +#endif +} + + +bool SDsendDTInfo() +{ + //Send array info packet, for this implmentation its really only the flags in ARDTflags that is sent + + uint8_t ValidACK = 0; + uint8_t localattempts = 0; + + SDbuild_DTInfoHeader(SDDTheader, DTInfoHeaderL, 0); + + do + { + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + + localattempts++; +#ifdef ENABLEMONITOR + Monitorport.print(F("Send DTInfo packet attempt ")); + Monitorport.println(localattempts); +#endif + SDTXPacketL = LoRa.transmitDTIRQ(SDDTheader, DTInfoHeaderL, (uint8_t *) SDDTdata, 0, NetworkID, TXtimeoutmS, TXpower, WAIT_TX); + + if (SDTXPacketL == 0) //if there has been an error ARTXPacketL returns as 0 + { +#ifdef ENABLEMONITOR + Monitorport.println(F("Transmit error")); +#endif + continue; + } + + ValidACK = LoRa.waitACKDTIRQ(SDDTheader, DTInfoHeaderL, ACKsegtimeoutmS); + + if (ValidACK > 0) + { + //ack is a valid reliable packet + SDRXPacketType = SDDTheader[0]; +#ifdef ENABLEMONITOR + Monitorport.print(F("ACK Packet type 0x")); + Monitorport.println(SDRXPacketType, HEX); +#endif + + if (SDRXPacketType == DTInfoACK) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK received")); +#endif + SDAckCount++; + SDRXPacketType = SDDTheader[0]; + return true; + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("DTInfoACK not received")); +#endif + return false; + } + + } + else + { + SDNoAckCount++; +#ifdef ENABLEMONITOR + Monitorport.println(F("No valid ACK received ")); +#endif + + if (SDNoAckCount > NoAckCountLimit) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("ERROR NoACK limit reached")); +#endif + return false; + } + } + delay(PacketDelaymS); + } + while ((ValidACK == 0) && (localattempts < SendAttempts)); + + if (localattempts == SendAttempts) + { + bitSet(SDDTErrors, SDSendPacket); + return false; + } + return true; +} + + +void SDbuild_DTInfoHeader(uint8_t *header, uint8_t headersize, uint8_t datalen) +{ + //This builds the header buffer for a info only header, faults problems etc + + beginarrayRW(header, 0); //start writing to array at location 0 + arrayWriteUint8(DTInfo); //write the packet type + arrayWriteUint8(SDDTflags); //ARDTflags byte + arrayWriteUint8(headersize); //write length of header + arrayWriteUint8(datalen); //write length of data array + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + arrayWriteUint8(0); //unused + endarrayRW(); +} + + +//************************************************ +//Receiver mode functions +//************************************************ + + +bool SDreceiveaPacketDT() +{ + //Receive Data transfer packets + + SDRXPacketType = 0; + SDRXPacketL = LoRa.receiveDTIRQ(SDDTheader, HeaderSizeMax, (uint8_t *) SDDTdata, DataSizeMax, NetworkID, RXtimeoutmS, WAIT_RX); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + +#ifdef DEBUG + SDprintSeconds(); +#endif + + if (SDRXPacketL > 0) + { + //if the LT.receiveDT() returns a value > 0 for SDRXPacketL then packet was received OK + SDreadHeaderDT(); //get the basic header details into global variables SDRXPacketType etc + SDprocessPacket(SDRXPacketType); //process and act on the packet + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return true; + } + else + { + //if the LoRa.receiveDTIRQ() function detects an error RXOK is 0 + uint16_t IRQStatus = LoRa.readIrqStatus(); + + if (IRQStatus & IRQ_RX_TIMEOUT) + { +#ifdef ENABLEMONITOR + Monitorport.println(F("RX Timeout")); +#endif + } + else + { + SDRXErrors++; +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.print(F("PacketError")); + SDprintPacketDetails(); + SDprintReliableStatus(); + Monitorport.println(); +#endif +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + + } + } + return false; +} + + +void SDreadHeaderDT() +{ + //The first 6 bytes of the header contain the important stuff, so load it up + //so we can decide what to do next. + beginarrayRW(SDDTheader, 0); //start buffer read at location 0 + SDRXPacketType = arrayReadUint8(); //load the packet type + SDRXFlags = arrayReadUint8(); //SDDTflags byte + SDRXHeaderL = arrayReadUint8(); //load the header length + SDRXDataarrayL = arrayReadUint8(); //load the datalength + SDDTSegment = arrayReadUint16(); //load the segment number +} + + +bool SDprocessPacket(uint8_t packettype) +{ + //Decide what to do with an incoming packet + + if (packettype == DTSegmentWrite) + { + SDprocessSegmentWrite(); + return true; + } + + if (packettype == DTFileOpen) + { + SDprocessFileOpen(SDDTdata, SDRXDataarrayL); + return true; + } + + if (packettype == DTFileClose) + { + SDprocessFileClose(); + return true; + } + + return true; +} + + +void SDprintPacketDetails() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + SDPacketSNR = LoRa.readPacketSNR(); + Monitorport.print(F(" RSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm")); + +#ifdef DEBUG + Monitorport.print(F(",SNR,")); + Monitorport.print(SDPacketSNR); + Monitorport.print(F("dBm,RXOKCount,")); + Monitorport.print(SDDTReceivedSegments); + Monitorport.print(F(",RXErrs,")); + Monitorport.print(SDRXErrors); + Monitorport.print(F(" RX")); + SDprintheader(SDDTheader, SDRXHeaderL); +#endif +#endif +} + + +bool SDprocessSegmentWrite() +{ + //There is a request to write a segment to file on receiver + //checks that the sequence of segment writes is correct + + if (!SDDTFileOpened) + { + //something is wrong, have received a request to write a segment but there is no file opened + //need to reject the segment write with a restart NACK +#ifdef ENABLEMONITOR + Monitorport.println(); + Monitorport.println(F("***************************************************")); + Monitorport.println(F("Error - Segment write with no file open - send NACK")); + Monitorport.println(F("***************************************************")); + Monitorport.println(); +#endif + SDDTheader[0] = DTStartNACK; + delay(ACKdelaymS); + delay(DuplicatedelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDTIRQ(SDDTheader, DTStartHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return false; + } + + if (SDDTSegment == SDDTSegmentNext) + { + DTSD_writeSegmentFile(SDDTdata, SDRXDataarrayL); + +#ifdef ENABLEMONITOR +#ifdef PRINTSEGMENTNUM + //Monitorport.print(F("Segment,")); + Monitorport.println(SDDTSegment); +#endif + +#ifdef DEBUG + Monitorport.print(F("Bytes,")); + Monitorport.print(SDRXDataarrayL); + SDprintPacketRSSI(); + Monitorport.println(F(" SendACK")); +#endif +#endif + SDDTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDTIRQ(SDDTheader, DTSegmentWriteHeaderL, TXpower); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + SDDTReceivedSegments++; + SDDTSegmentLast = SDDTSegment; //so we can tell if sequece has been received twice + SDDTSegmentNext = SDDTSegment + 1; + return true; + } + + if (SDDTSegment == SDDTSegmentLast) + { +#ifdef ENABLEMONITOR + Monitorport.print(F("ERROR segment ")); + Monitorport.print(SDDTSegment); + Monitorport.println(F(" already received ")); + delay(DuplicatedelaymS); +#ifdef DEBUG + SDprintPacketDetails(); + SDprintPacketRSSI(); +#endif +#endif + SDDTheader[0] = DTSegmentWriteACK; + delay(ACKdelaymS); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDTIRQ(SDDTheader, DTSegmentWriteHeaderL, TXpower); + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return true; + } + + if (SDDTSegment != SDDTSegmentNext ) + { + SDDTheader[0] = DTSegmentWriteNACK; + SDDTheader[4] = lowByte(SDDTSegmentNext); + SDDTheader[5] = highByte(SDDTSegmentNext); + delay(ACKdelaymS); + delay(DuplicatedelaymS); //add an extra delay here to stop repeated segment sends + +#ifdef ENABLEMONITOR + Monitorport.print(F(" ERROR Received Segment ")); + Monitorport.print(SDDTSegment); + Monitorport.print(F(" expected ")); + Monitorport.print(SDDTSegmentNext); + Monitorport.print(F(" ")); + +#ifdef DEBUG + SDprintPacketDetails(); + SDprintPacketRSSI(); +#endif + + Monitorport.print(F(" Send NACK for segment ")); + Monitorport.print(SDDTSegmentNext); + Monitorport.println(); + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.print(F("Transmit restart request for segment ")); + Monitorport.println(SDDTSegmentNext); + SDprintheader(SDDTheader, SDRXHeaderL); + Monitorport.println(); + Monitorport.println(F("*****************************************")); + Monitorport.println(); + Monitorport.flush(); +#endif + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDTIRQ(SDDTheader, DTSegmentWriteHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + return false; + } + + return true; +} + + +bool SDprocessFileOpen(uint8_t *filename, uint8_t filenamesize) +{ + //There is a request to open local file on receiver + + SDDTDestinationFileCRC = 0; //CRC of complete file received + SDDTDestinationFileLength = 0; //length of file written on the destination\receiver + + beginarrayRW(SDDTheader, 4); //start buffer read at location 4 + SDDTSourceFileLength = arrayReadUint32(); //load the file length of the remote file being sent + SDDTSourceFileCRC = arrayReadUint16(); //load the CRC of the source file being sent + memset(SDDTfilenamebuff, 0, Maxfilenamesize); //clear SDDTfilenamebuff to all 0s + memcpy(SDDTfilenamebuff, filename, filenamesize); //copy received SDDTdata into SDDTfilenamebuff + +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.print(F(" SD File Open request")); + Monitorport.println(); + +#ifdef DEBUG + SDTXNetworkID = LoRa.getTXNetworkID(SDRXPacketL); //get the networkID appended to packet + SDTXArrayCRC = LoRa.getTXPayloadCRC(SDRXPacketL); //get the payload CRC appended to packet + Monitorport.print(F("SDTXNetworkID,0x")); + Monitorport.println(SDTXNetworkID, HEX); + Monitorport.print(F("SDTXArrayCRC,0x")); + Monitorport.println(SDTXArrayCRC, HEX); +#endif + + SDprintSourceFileDetails(); + + if bitRead(SDRXFlags, SDNoFileSave) + { + Monitorport.println(F("Remote did not save file to SD")); + } +#endif + + if (DTSD_openNewFileWrite(SDDTfilenamebuff)) //open file for write at beginning, delete if it exists + { +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" DT File Opened OK")); + Monitorport.println(F("Waiting transfer")); +#endif + SDDTSegmentNext = 0; //since file is opened the next sequence should be the first + SDDTFileOpened = true; + SDDTStartmS = millis(); + } + else + { +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" File Open fail")); +#endif + SDDTFileOpened = false; + return false; + } + + SDDTStartmS = millis(); + delay(ACKdelaymS); + +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("Sending ACK")); +#endif +#endif + + SDDTheader[0] = DTFileOpenACK; //set the ACK packet type + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDTIRQ(SDDTheader, DTFileOpenHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } + SDDTSegmentNext = 0; //after file open, segment 0 is next + + return true; +} + + +bool SDprocessFileClose() +{ + // There is a request to close a file on SD of receiver + +#ifdef ENABLEMONITOR + Monitorport.print((char*) SDDTfilenamebuff); + Monitorport.println(F(" File close request")); +#endif + + if (SDDTFileOpened) //check if file has been opened, close it if it is + { + if (SD.exists(SDDTfilenamebuff)) //check if file exists + { + DTSD_closeFile(); + +#ifdef ENABLEMONITOR + Monitorport.print(F("Transfer time ")); + Monitorport.print(millis() - SDDTStartmS); + Monitorport.print(F("mS")); + Monitorport.println(); + Monitorport.println(F("File closed")); +#endif + + SDDTFileOpened = false; + SDDTDestinationFileLength = DTSD_openFileRead(SDDTfilenamebuff); + +#ifdef ENABLEFILECRC + SDDTDestinationFileCRC = DTSD_fileCRCCCITT(SDDTDestinationFileLength); +#endif + + beginarrayRW(SDDTheader, 4); //start writing to array at location 12 + arrayWriteUint32(SDDTDestinationFileLength); //write file length of file just written just written to ACK header + arrayWriteUint16(SDDTDestinationFileCRC); //write CRC of file just written to ACK header + +#ifdef ENABLEMONITOR + SDprintDestinationFileDetails(); +#endif + } + } + else + { +#ifdef ENABLEMONITOR + Monitorport.println(F("File already closed")); +#endif + delay(DuplicatedelaymS); + } + + delay(ACKdelaymS); +#ifdef ENABLEMONITOR +#ifdef DEBUG + Monitorport.println(F("Sending ACK")); +#endif +#endif + SDDTheader[0] = DTFileCloseACK; + + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, HIGH); + } + LoRa.sendACKDTIRQ(SDDTheader, DTFileCloseHeaderL, TXpower); + if (SDDTLED >= 0) + { + digitalWrite(SDDTLED, LOW); + } +#ifdef ENABLEMONITOR + Monitorport.println(); +#endif + + return true; +} + + +void SDprintPacketRSSI() +{ +#ifdef ENABLEMONITOR + SDPacketRSSI = LoRa.readPacketRSSI(); + Monitorport.print(F(" RSSI,")); + Monitorport.print(SDPacketRSSI); + Monitorport.print(F("dBm")); +#endif +} + + +void SDprintSourceFileDetails() +{ + +#ifdef ENABLEMONITOR + Monitorport.print(F("Source file length is ")); + Monitorport.print(SDDTSourceFileLength); + Monitorport.println(F(" bytes")); +#ifdef ENABLEFILECRC + Monitorport.print(F("Source file CRC is 0x")); + Monitorport.println(SDDTSourceFileCRC, HEX); +#endif +#endif +} + + +void SDprintDestinationFileDetails() +{ +#ifdef ENABLEMONITOR + Monitorport.print(F("Destination file length ")); + Monitorport.print(SDDTDestinationFileLength); + Monitorport.println(F(" bytes")); + if (SDDTDestinationFileLength != SDDTSourceFileLength) + { + Monitorport.println(F("ERROR - file lengths do not match")); + } + else + { + Monitorport.println(F("File lengths match")); + } + +#ifdef ENABLEFILECRC + Monitorport.print(F("Destination file CRC is 0x")); + Monitorport.println(SDDTDestinationFileCRC, HEX); + if (SDDTDestinationFileCRC != SDDTSourceFileCRC) + { + Monitorport.println(F("ERROR - file CRCs do not match")); + } + else + { + Monitorport.println(F("File CRCs match")); + } +#endif + +#endif +} + +//************************************************ +//Common functions +//************************************************ + +void SDsetLED(int8_t pinnumber) +{ + if (pinnumber >= 0) + { + SDDTLED = pinnumber; + pinMode(pinnumber, OUTPUT); + } +} + + +void SDprintheader(uint8_t *header, uint8_t headersize) +{ + SDUNUSED(header); //to prevent a compiler warning + SDUNUSED(headersize); //to prevent a compiler warning + +#ifdef ENABLEMONITOR + Monitorport.print(F("HeaderBytes,")); + Monitorport.print(headersize); + Monitorport.print(F(" ")); + printarrayHEX(header, headersize); +#endif +} + + +void printArrayHEX(uint8_t *buff, uint32_t len) +{ + SDUNUSED(buff); //to prevent a compiler warning + SDUNUSED(len); //to prevent a compiler warning + +#ifdef ENABLEMONITOR + uint8_t index, buffdata; + for (index = 0; index < len; index++) + { + buffdata = buff[index]; + if (buffdata < 16) + { + Monitorport.print(F("0")); + } + Monitorport.print(buffdata, HEX); + Monitorport.print(F(" ")); + } +#endif +} + + +void SDprintReliableStatus() +{ + +#ifdef ENABLEMONITOR + + uint8_t reliableErrors = LoRa.readReliableErrors(); + uint8_t reliableFlags = LoRa.readReliableFlags(); + + if (bitRead(reliableErrors, ReliableCRCError)) + { + Monitorport.print(F(",ReliableCRCError")); + } + + if (bitRead(reliableErrors, ReliableIDError)) + { + Monitorport.print(F(",ReliableIDError")); + } + + if (bitRead(reliableErrors, ReliableSizeError)) + { + Monitorport.print(F(",ReliableSizeError")); + } + + if (bitRead(reliableErrors, ReliableACKError)) + { + Monitorport.print(F(",NoReliableACK")); + } + + if (bitRead(reliableErrors, ReliableTimeout)) + { + Monitorport.print(F(",ReliableTimeout")); + } + + if (bitRead(reliableFlags, ReliableACKSent)) + { + Monitorport.print(F(",ACKsent")); + } + + if (bitRead(reliableFlags, ReliableACKReceived)) + { + Monitorport.print(F(",ACKreceived")); + } +#endif +} + + +void SDprintPacketHex() +{ +#ifdef ENABLEMONITOR + uint8_t packetlen = LoRa.readRXPacketL(); + Monitorport.print(packetlen); + Monitorport.print(F(" bytes > ")); + if (packetlen > 0) + { + LoRa.printSXBufferHEX(0, packetlen - 1); + } +#endif +} diff --git a/src/SX128XLT.cpp b/src/SX128XLT.cpp new file mode 100644 index 0000000..8043e31 --- /dev/null +++ b/src/SX128XLT.cpp @@ -0,0 +1,5969 @@ +/* + Copyright 2019 - Stuart Robinson + Licensed under a MIT license displayed at the bottom of this document. + 06/02/20 +*/ + +/* + Parts of code Copyright (c) 2013, SEMTECH S.A. + See LICENSE.TXT file included in the library +*/ + +#include +#include + +#define LTUNUSED(v) (void) (v) //add LTUNUSED(variable); to avoid compiler warnings +#define USE_SPI_TRANSACTION + +//#define SX128XDEBUG //enable debug messages +//#define RANGINGDEBUG //enable debug messages for ranging +//#define SX128XDEBUGRXTX //enable debug messages for RX TX switching +//#define SX128XDEBUGPINS //enable pin allocation debug messages +//#define SX128XDEBUGRELIABLE //enable for debugging reliable and data transfer (DT) packets +//#define USEPAYLOADLENGTHREGISTER //enable autoamtic setting of Payload length with register write +//#define DETECTRELIABLERRORS //enable to improve error detect reliable errors such as incorrect packet size etc +#define REVISEDCHECKBUSY //change to checkBusy() made November 2021 to improve speed, comment this #define out to use previous code. + +//Changes November 2021 +//Revised packet RSSI +//Added ranging RSSI +//Revised checkBusy() to improve performance +//Revised receive functions to preven lock up in receive mode on header error +//Revised receive functions to include failure on syncword fail, needed for relaible FLRC error detection +//Added begin() functions to allow for limited pin use +//Added setPayloadLength() to more easily use LoRa or FLRC packets +//Added option, by enabling #define USEPAYLOADLENGTHREGISTER, to set payload length via a discovered register write +//Added reliable packet functions +//Added data transfer functions + + + +SX128XLT::SX128XLT() +{ +} + +/* Formats for :begin + 1 All pins > begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, int8_t pinDIO2, int8_t pinDIO3, int8_t pinRXEN, int8_t pinTXEN, uint8_t device) + 2 NiceRF > begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, uint8_t device) + 3 Ebyte > begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, int8_t pinRXEN, int8_t pinTXEN, uint8_t device); + 4 IRQ > begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, uint8_t device) + 5 IRQ > begin(int8_t pinNSS, int8_t pinRFBUSY, uint8_t device) +*/ + + +bool SX128XLT::begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, int8_t pinDIO2, int8_t pinDIO3, int8_t pinRXEN, int8_t pinTXEN, uint8_t device) +{ + + //format 1 pins, assign all available pins + _NSS = pinNSS; + _NRESET = pinNRESET; + _RFBUSY = pinRFBUSY; + _DIO1 = pinDIO1; + _DIO2 = pinDIO2; + _DIO3 = pinDIO3; + _RXEN = pinRXEN; + _TXEN = pinTXEN; + _Device = device; + _TXDonePin = pinDIO1; //this is defalt pin for sensing TX done + _RXDonePin = pinDIO1; //this is defalt pin for sensing RX done + + digitalWrite(_NSS, HIGH); + pinMode(_NSS, OUTPUT); + digitalWrite(_NSS, HIGH); + pinMode(_NRESET, OUTPUT); + digitalWrite(_NRESET, LOW); + pinMode(_RFBUSY, INPUT); + + +#ifdef SX128XDEBUGPINS + Serial.println(F("1 begin()")); + Serial.println(F("SX128XLT constructor instantiated successfully")); + Serial.print(F("NSS ")); + Serial.println(_NSS); + Serial.print(F("NRESET ")); + Serial.println(_NRESET); + Serial.print(F("RFBUSY ")); + Serial.println(_RFBUSY); + Serial.print(F("DIO1 ")); + Serial.println(_DIO1); + Serial.print(F("DIO2 ")); + Serial.println(_DIO2); + Serial.print(F("DIO3 ")); + Serial.println(_DIO3); + Serial.print(F("RX_EN ")); + Serial.println(_RXEN); + Serial.print(F("TX_EN ")); + Serial.println(_TXEN); +#endif + + if (_DIO1 >= 0) + { + pinMode( _DIO1, INPUT); + } + + if (_DIO2 >= 0) + { + pinMode( _DIO2, INPUT); + } + + if (_DIO3 >= 0) + { + pinMode( _DIO3, INPUT); + } + + if ((_RXEN >= 0) && (_TXEN >= 0)) + { +#ifdef SX128XDEBUGPINS + Serial.println(F("RX_EN & TX_EN switching enabled")); +#endif + pinMode(_RXEN, OUTPUT); + pinMode(_TXEN, OUTPUT); + _rxtxpinmode = true; + } + else + { +#ifdef SX128XDEBUGPINS + Serial.println(F("RX_EN & TX_EN switching disabled")); +#endif + _rxtxpinmode = false; + } + + resetDevice(); + + if (checkDevice()) + { + return true; + } + + return false; +} + + +bool SX128XLT::begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, uint8_t device) +{ + //format 2 pins for NiceRF, NSS, NRESET, RFBUSY, DIO1 + _NSS = pinNSS; + _NRESET = pinNRESET; + _RFBUSY = pinRFBUSY; + _DIO1 = pinDIO1; + _DIO2 = -1; //not defined, so mark as unused + _DIO3 = -1; //not defined, so mark as unused + _RXEN = -1; //not defined, so mark as unused + _TXEN = -1; //not defined, so mark as unused + _Device = device; + _TXDonePin = pinDIO1; //this is defalt pin for sensing TX done + _RXDonePin = pinDIO1; //this is defalt pin for sensing RX done + + digitalWrite(_NSS, HIGH); + pinMode(_NSS, OUTPUT); + digitalWrite(_NSS, HIGH); + pinMode(_NRESET, OUTPUT); + digitalWrite(_NRESET, LOW); + pinMode(_RFBUSY, INPUT); + +#ifdef SX128XDEBUGPINS + Serial.println(F("2 begin()")); + Serial.println(F("SX128XLT constructor instantiated successfully")); + Serial.print(F("NSS ")); + Serial.println(_NSS); + Serial.print(F("NRESET ")); + Serial.println(_NRESET); + Serial.print(F("RFBUSY ")); + Serial.println(_RFBUSY); + Serial.print(F("DIO1 ")); + Serial.println(_DIO1); + Serial.print(F("DIO2 ")); + Serial.println(_DIO2); + Serial.print(F("DIO3 ")); + Serial.println(_DIO3); + Serial.print(F("RX_EN ")); + Serial.println(_RXEN); + Serial.print(F("TX_EN ")); + Serial.println(_TXEN); +#endif + + if (_DIO1 >= 0) + { + pinMode( _DIO1, INPUT); + } + +#ifdef SX128XDEBUGPINS + Serial.println(F("RX_EN & TX_EN switching disabled")); +#endif + + _rxtxpinmode = false; + + resetDevice(); + + if (checkDevice()) + { + return true; + } + + return false; +} + + +bool SX128XLT::begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, int8_t pinRXEN, int8_t pinTXEN, uint8_t device) +{ + //format 3 pins for Ebyte, NSS, NRESET, RFBUSY, DIO1, RX_EN, TX_EN + _NSS = pinNSS; + _NRESET = pinNRESET; + _RFBUSY = pinRFBUSY; + _DIO1 = pinDIO1; + _DIO2 = -1; //not defined, so mark as unused + _DIO3 = -1; //not defined, so mark as unused + _RXEN = pinRXEN; + _TXEN = pinTXEN; + _Device = device; + _TXDonePin = pinDIO1; //this is defalt pin for sensing TX done + _RXDonePin = pinDIO1; //this is defalt pin for sensing RX done + + digitalWrite(_NSS, HIGH); + pinMode(_NSS, OUTPUT); + digitalWrite(_NSS, HIGH); + pinMode(_NRESET, OUTPUT); + digitalWrite(_NRESET, LOW); + pinMode(_RFBUSY, INPUT); + +#ifdef SX128XDEBUGPINS + Serial.println(F("3 begin()")); + Serial.println(F("SX128XLT constructor instantiated successfully")); + Serial.print(F("NSS ")); + Serial.println(_NSS); + Serial.print(F("NRESET ")); + Serial.println(_NRESET); + Serial.print(F("RFBUSY ")); + Serial.println(_RFBUSY); + Serial.print(F("DIO1 ")); + Serial.println(_DIO1); + Serial.print(F("DIO2 ")); + Serial.println(_DIO2); + Serial.print(F("DIO3 ")); + Serial.println(_DIO3); + Serial.print(F("RX_EN ")); + Serial.println(_RXEN); + Serial.print(F("TX_EN ")); + Serial.println(_TXEN); +#endif + + if (_DIO1 >= 0) + { + pinMode( _DIO1, INPUT); + } + + if ((_RXEN >= 0) && (_TXEN >= 0)) + { +#ifdef SX128XDEBUGPINS + Serial.println(F("RX_EN & TX_EN switching enabled")); +#endif + pinMode(_RXEN, OUTPUT); + pinMode(_TXEN, OUTPUT); + _rxtxpinmode = true; + } + else + { +#ifdef SX128XDEBUGPINS + Serial.println(F("RX_EN & TX_EN switching disabled")); +#endif + _rxtxpinmode = false; + } + + resetDevice(); + + if (checkDevice()) + { + return true; + } + + return false; +} + + +bool SX128XLT::begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, uint8_t device) +{ + //format 4 pins for IRQ use, NSS, NRESET, RFBUSY + _NSS = pinNSS; + _NRESET = pinNRESET; + _RFBUSY = pinRFBUSY; + _DIO1 = -1; //not defined, so mark as unused + _DIO2 = -1; //not defined, so mark as unused + _DIO3 = -1; //not defined, so mark as unused + _RXEN = -1; //not defined, so mark as unused + _TXEN = -1; //not defined, so mark as unused + _Device = device; + _TXDonePin = -1; //not defined, so mark as unused + _RXDonePin = -1; //not defined, so mark as unused + + digitalWrite(_NSS, HIGH); + pinMode(_NSS, OUTPUT); + digitalWrite(_NSS, HIGH); + pinMode(_NRESET, OUTPUT); + digitalWrite(_NRESET, LOW); + pinMode(_RFBUSY, INPUT); + +#ifdef SX128XDEBUGPINS + Serial.println(F("4 IRQ begin()")); + Serial.println(F("SX128XLT constructor instantiated successfully")); + Serial.print(F("NSS ")); + Serial.println(_NSS); + Serial.print(F("NRESET ")); + Serial.println(_NRESET); + Serial.print(F("RFBUSY ")); + Serial.println(_RFBUSY); +#endif + + _rxtxpinmode = false; + resetDevice(); + + if (checkDevice()) + { + return true; + } + + return false; +} + + +bool SX128XLT::begin(int8_t pinNSS, int8_t pinRFBUSY, uint8_t device) +{ + //Format 5 pins for IRQ use, NSS, RFBUSY. Some form of additional external reset is needed for + //the device, since this routine does not use resetDevice(); + //A pull up on NSS is recommended, a low to high transitions on NSS at startup can put the device + //into a busy state, with no reset available to clear it. + + _NSS = pinNSS; + _NRESET = -1; //not defined, so mark as unused + _RFBUSY = pinRFBUSY; + _DIO1 = -1; //not defined, so mark as unused + _DIO2 = -1; //not defined, so mark as unused + _DIO3 = -1; //not defined, so mark as unused + _RXEN = -1; //not defined, so mark as unused + _TXEN = -1; //not defined, so mark as unused + _Device = device; + _TXDonePin = -1; //not defined, so mark as unused + _RXDonePin = -1; //not defined, so mark as unused + + digitalWrite(_NSS, HIGH); //make sure NSS state is high, before turning on output, a low to high transision here causes issues + pinMode(_NSS, OUTPUT); + digitalWrite(_NSS, HIGH); + pinMode(_RFBUSY, INPUT); + +#ifdef SX128XDEBUGPINS + Serial.println(F("5 IRQ begin()")); + Serial.println(F("SX128XLT constructor instantiated successfully")); + Serial.print(F("NSS ")); + Serial.println(_NSS); + Serial.print(F("NRESET ")); + Serial.println(_NRESET); + Serial.print(F("RFBUSY ")); + Serial.println(_RFBUSY); +#endif + + _rxtxpinmode = false; + + if (checkDevice()) + { + return true; + } + + return false; +} + + +void SX128XLT::rxEnable() +{ + //Enable RX mode on device such as Ebyte E28-2G4M20S which have RX and TX enable pins +#ifdef SX128XDEBUGRXTX + Serial.println(F("rxEnable()")); +#endif + + digitalWrite(_RXEN, HIGH); + digitalWrite(_TXEN, LOW); +} + + +void SX128XLT::txEnable() +{ + //Enable RX mode on device such as Ebyte E28-2G4M20S which have RX and TX enable pins +#ifdef SX128XDEBUGRXTX + Serial.println(F("txEnable()")); +#endif + + digitalWrite(_RXEN, LOW); + digitalWrite(_TXEN, HIGH); +} + + +//Changes made November 2021 to improve performance of checkBusy(); +//As published the revised check busy function will used +//To use the previous version of code, remove the #define REVISEDCHECKBUSY at the top of this file +#ifdef REVISEDCHECKBUSY +void SX128XLT::checkBusy() +{ +#ifdef SX128XDEBUG + Serial.println(F("checkBusy() Revised")); +#endif + + uint32_t startmS = millis(); + + do + { + if ( ((uint32_t) (millis() - startmS) > 9)) //wait 10mS for busy to complete + { + Serial.println(F("ERROR - Busy Timeout!")); + resetDevice(); + setMode(MODE_STDBY_RC); + config(); //re-run saved config + break; + } + } while (digitalRead(_RFBUSY)); + +} +#endif + + +#ifndef REVISEDCHECKBUSY +void SX128XLT::checkBusy() +{ +#ifdef SX128XDEBUG + Serial.println(F("checkBusy() Original")); +#endif + + uint8_t busy_timeout_cnt; + busy_timeout_cnt = 0; + + while (digitalRead(_RFBUSY)) + { + delay(1); + busy_timeout_cnt++; + + if (busy_timeout_cnt > 10) //wait 10mS for busy to complete + { + Serial.println(F("ERROR - Busy Timeout!")); + resetDevice(); + setMode(MODE_STDBY_RC); + config(); //re-run saved config + break; + } + } + +} +#endif + + +bool SX128XLT::config() +{ +#ifdef SX128XDEBUG + Serial.println(F("config()")); +#endif + + resetDevice(); + setMode(MODE_STDBY_RC); + setRegulatorMode(savedRegulatorMode); + setPacketType(savedPacketType); + setRfFrequency(savedFrequency, savedOffset); + setModulationParams(savedModParam1, savedModParam2, savedModParam3); + setPacketParams(savedPacketParam1, savedPacketParam2, savedPacketParam3, savedPacketParam4, savedPacketParam5, savedPacketParam6, savedPacketParam7); + setDioIrqParams(savedIrqMask, savedDio1Mask, savedDio2Mask, savedDio3Mask); //set for IRQ on RX done on DIO1 + setHighSensitivity(); + return true; +} + + +void SX128XLT::readRegisters(uint16_t address, uint8_t *buffer, uint16_t size) +{ + uint16_t index; + uint8_t addr_l, addr_h; + + addr_h = address >> 8; + addr_l = address & 0x00FF; + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_READ_REGISTER); + SPI.transfer(addr_h); //MSB + SPI.transfer(addr_l); //LSB + SPI.transfer(0xFF); + for (index = 0; index < size; index++) + { + *(buffer + index) = SPI.transfer(0xFF); + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + +} + + +uint8_t SX128XLT::readRegister(uint16_t address) +{ + uint8_t data; + + readRegisters(address, &data, 1); + return data; +} + + +void SX128XLT::writeRegisters(uint16_t address, uint8_t *buffer, uint16_t size) +{ + uint8_t addr_l, addr_h; + uint8_t i; + + addr_l = address & 0xff; + addr_h = address >> 8; + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_REGISTER); + SPI.transfer(addr_h); //MSB + SPI.transfer(addr_l); //LSB + + for (i = 0; i < size; i++) + { + SPI.transfer(buffer[i]); + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + +} + + +void SX128XLT::writeRegister(uint16_t address, uint8_t value) +{ + writeRegisters( address, &value, 1 ); +} + + +void SX128XLT::writeCommand(uint8_t Opcode, uint8_t *buffer, uint16_t size) +{ +#ifdef SX128XDEBUG + //Serial.print(F("writeCommand() ")); + //Serial.println(Opcode, HEX); +#endif + + uint8_t index; + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(Opcode); + + for (index = 0; index < size; index++) + { + SPI.transfer(buffer[index]); + } + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + if (Opcode != RADIO_SET_SLEEP) + { + checkBusy(); + } +} + + +void SX128XLT::readCommand(uint8_t Opcode, uint8_t *buffer, uint16_t size) +{ +#ifdef SX128XDEBUG + //Serial.print(F("readCommand() ")); + //Serial.println(Opcode, HEX); +#endif + + uint8_t i; + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(Opcode); + SPI.transfer(0xFF); + + for ( i = 0; i < size; i++ ) + { + *(buffer + i) = SPI.transfer(0xFF); + } + digitalWrite(_NSS, HIGH); + + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif +} + + +void SX128XLT::resetDevice() +{ +#ifdef SX128XDEBUG + Serial.println(F("resetDevice()")); +#endif + + //Note: in the IRQ TX and RX examples _NRESET is set to -1, if so dont attempt to toggle pin + + if (_NRESET >= 0) + { + delay(20); + digitalWrite(_NRESET, LOW); + delay(50); + digitalWrite(_NRESET, HIGH); + delay(20); + } +} + + +bool SX128XLT::checkDevice() +{ + //check there is a device out there, writes a register and reads back +#ifdef SX128XDEBUG + Serial.println(F("checkDevice()")); +#endif + + uint8_t Regdata1, Regdata2; + Regdata1 = readRegister(0x0908); //low byte of frequency setting + writeRegister(0x0908, (Regdata1 + 1)); + Regdata2 = readRegister(0x0908); //read changed value back + writeRegister(0x0908, Regdata1); //restore register to original value + + if (Regdata2 == (Regdata1 + 1)) + { + return true; + } + else + { + return false; + } +} + + +void SX128XLT::setupLoRa(uint32_t frequency, int32_t offset, uint8_t modParam1, uint8_t modParam2, uint8_t modParam3) +{ +#ifdef SX128XDEBUG + Serial.println(F("setupLoRa()")); +#endif + + setMode(MODE_STDBY_RC); + setRegulatorMode(USE_LDO); + setPacketType(PACKET_TYPE_LORA); + setRfFrequency(frequency, offset); + setBufferBaseAddress(0, 0); + setModulationParams(modParam1, modParam2, modParam3); + setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 255, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); + setHighSensitivity(); +} + + +void SX128XLT::setupFLRC(uint32_t frequency, int32_t offset, uint8_t modParam1, uint8_t modParam2, uint8_t modParam3, uint32_t syncword) +{ + //see data sheet SX1280-1_V3.2 16.4 FLRC Modem: Increased PER in FLRC Packets with Synch + //some patterns of syncword can create increased error rates in FLRC + +#ifdef SX128XDEBUG + Serial.println(F("setupFLRC()")); +#endif + setMode(MODE_STDBY_RC); + setRegulatorMode(USE_LDO); + setPacketType(PACKET_TYPE_FLRC); + setRfFrequency(frequency, offset); + setBufferBaseAddress(0, 0); + setModulationParams(modParam1, modParam2, modParam3); + setPacketParams(PREAMBLE_LENGTH_32_BITS, FLRC_SYNC_WORD_LEN_P32S, RADIO_RX_MATCH_SYNCWORD_1, RADIO_PACKET_VARIABLE_LENGTH, 127, RADIO_CRC_3_BYTES, RADIO_WHITENING_OFF); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); + setSyncWord1(syncword); + setHighSensitivity(); +} + + +void SX128XLT::setMode(uint8_t modeconfig) +{ +#ifdef SX128XDEBUG + Serial.println(F("setMode()")); +#endif + + uint8_t Opcode = 0x80; + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(Opcode); + SPI.transfer(modeconfig); + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + _OperatingMode = modeconfig; + +} + + +void SX128XLT::setRegulatorMode(uint8_t mode) +{ +#ifdef SX128XDEBUG + Serial.println(F("setRegulatorMode()")); +#endif + + savedRegulatorMode = mode; + + writeCommand(RADIO_SET_REGULATORMODE, &mode, 1); +} + + +void SX128XLT::setPacketType(uint8_t packettype ) +{ +#ifdef SX128XDEBUG + Serial.println(F("setPacketType()")); +#endif + savedPacketType = packettype; + + writeCommand(RADIO_SET_PACKETTYPE, &packettype, 1); +} + + +void SX128XLT::setRfFrequency(uint32_t frequency, int32_t offset) +{ +#ifdef SX128XDEBUG + Serial.println(F("setRfFrequency()")); +#endif + + savedFrequency = frequency; + savedOffset = offset; + + frequency = frequency + offset; + uint8_t buffer[3]; + uint32_t freqtemp = 0; + freqtemp = ( uint32_t )( (double) frequency / (double)FREQ_STEP); + buffer[0] = ( uint8_t )( ( freqtemp >> 16 ) & 0xFF ); + buffer[1] = ( uint8_t )( ( freqtemp >> 8 ) & 0xFF ); + buffer[2] = ( uint8_t )( freqtemp & 0xFF ); + writeCommand(RADIO_SET_RFFREQUENCY, buffer, 3); +} + + +void SX128XLT::setBufferBaseAddress(uint8_t txBaseAddress, uint8_t rxBaseAddress) +{ +#ifdef SX128XDEBUG + Serial.println(F("setBufferBaseAddress()")); +#endif + + uint8_t buffer[2]; + + buffer[0] = txBaseAddress; + buffer[1] = rxBaseAddress; + writeCommand(RADIO_SET_BUFFERBASEADDRESS, buffer, 2); +} + + +void SX128XLT::setModulationParams(uint8_t modParam1, uint8_t modParam2, uint8_t modParam3) +{ + //sequence is spreading factor, bandwidth, coding rate. + +#ifdef SX128XDEBUG + Serial.println(F("setModulationParams()")); +#endif + + uint8_t buffer[3]; + + savedModParam1 = modParam1; + savedModParam2 = modParam2; + savedModParam3 = modParam3; + + buffer[0] = modParam1; + buffer[1] = modParam2; + buffer[2] = modParam3; + + writeCommand(RADIO_SET_MODULATIONPARAMS, buffer, 3); + + //implement data sheet additions, datasheet SX1280-1_V3.2section 14.47 + + writeRegister( 0x93C, 0x1 ); + + switch (modParam1) + { + case LORA_SF5: + writeRegister( 0x925, 0x1E ); + break; + case LORA_SF6: + writeRegister( 0x925, 0x1E ); + break; + + case LORA_SF7: + writeRegister( 0x925, 0x37 ); + break; + + case LORA_SF8: + writeRegister( 0x925, 0x37 ); + break; + + case LORA_SF9: + writeRegister( 0x925, 0x32 ); + break; + + case LORA_SF10: + writeRegister( 0x925, 0x32 ); + break; + + case LORA_SF11: + writeRegister( 0x925, 0x32 ); + break; + + case LORA_SF12: + writeRegister( 0x925, 0x32 ); + break; + + default: + break; + } +} + + +void SX128XLT::setPacketParams(uint8_t packetParam1, uint8_t packetParam2, uint8_t packetParam3, uint8_t packetParam4, uint8_t packetParam5, uint8_t packetParam6, uint8_t packetParam7) +{ + //This function is for FLRC mode + //For FLRC: order is PreambleLength, SyncWordLength, SyncWordMatch, HeaderType, PayloadLength, CrcLength, Whitening + +#ifdef SX128XDEBUG + Serial.println(F("SetPacketParams(7)")); +#endif + + savedPacketParam1 = packetParam1; + savedPacketParam2 = packetParam2; + savedPacketParam3 = packetParam3; + savedPacketParam4 = packetParam4; + savedPacketParam5 = packetParam5; + savedPacketParam6 = packetParam6; + savedPacketParam7 = packetParam7; + + uint8_t buffer[7]; + buffer[0] = packetParam1; + buffer[1] = packetParam2; + buffer[2] = packetParam3; + buffer[3] = packetParam4; + buffer[4] = packetParam5; + buffer[5] = packetParam6; + buffer[6] = packetParam7; + writeCommand(RADIO_SET_PACKETPARAMS, buffer, 7); +} + + +void SX128XLT::setPacketParams(uint8_t packetParam1, uint8_t packetParam2, uint8_t packetParam3, uint8_t packetParam4, uint8_t packetParam5) +{ + //This function is for LORa mode + //For LoRa: order is PreambleLength, HeaderType, PayloadLength, CRC, InvertIQ/chirp invert + +#ifdef SX128XDEBUG + Serial.println(F("SetPacketParams(5)")); +#endif + + savedPacketParam1 = packetParam1; + savedPacketParam2 = packetParam2; + savedPacketParam3 = packetParam3; + savedPacketParam4 = packetParam4; + savedPacketParam5 = packetParam5; + + uint8_t buffer[7]; + buffer[0] = packetParam1; + buffer[1] = packetParam2; + buffer[2] = packetParam3; + buffer[3] = packetParam4; + buffer[4] = packetParam5; + writeCommand(RADIO_SET_PACKETPARAMS, buffer, 5); +} + + +void SX128XLT::setDioIrqParams(uint16_t irqMask, uint16_t dio1Mask, uint16_t dio2Mask, uint16_t dio3Mask ) +{ +#ifdef SX128XDEBUG + Serial.println(F("setDioIrqParams()")); +#endif + + savedIrqMask = irqMask; + savedDio1Mask = dio1Mask; + savedDio2Mask = dio2Mask; + savedDio3Mask = dio3Mask; + + uint8_t buffer[8]; + + buffer[0] = (uint8_t) (irqMask >> 8); + buffer[1] = (uint8_t) (irqMask & 0xFF); + buffer[2] = (uint8_t) (dio1Mask >> 8); + buffer[3] = (uint8_t) (dio1Mask & 0xFF); + buffer[4] = (uint8_t) (dio2Mask >> 8); + buffer[5] = (uint8_t) (dio2Mask & 0xFF); + buffer[6] = (uint8_t) (dio3Mask >> 8); + buffer[7] = (uint8_t) (dio3Mask & 0xFF); + writeCommand(RADIO_SET_DIOIRQPARAMS, buffer, 8); +} + + +void SX128XLT::setHighSensitivity() +{ + //set bits 7,6 of REG_LNA_REGIME +#ifdef SX128XDEBUG + Serial.println(F("setHighSensitivity()")); +#endif + + writeRegister(REG_LNA_REGIME, (readRegister(REG_LNA_REGIME) | 0xC0)); +} + + +void SX128XLT::setLowPowerRX() +{ + //clear bits 7,6 of REG_LNA_REGIME +#ifdef SX128XDEBUG + Serial.println(F("setLowPowerRX()")); +#endif + + writeRegister(REG_LNA_REGIME, (readRegister(REG_LNA_REGIME) & 0x3F)); +} + + +void SX128XLT::printModemSettings() +{ + //modified November 2021 to provide more details on FLRC +#ifdef SX128XDEBUG + Serial.println(F("printModemSettings()")); +#endif + + printDevice(); + + Serial.print(F(",PACKET_TYPE_")); + + switch (savedPacketType) + { + case PACKET_TYPE_GFSK: + Serial.print(F("GFSK")); + break; + + case PACKET_TYPE_LORA: + Serial.print(F("LORA")); + break; + + case PACKET_TYPE_RANGING: + Serial.print(F("RANGING")); + break; + + case PACKET_TYPE_FLRC: + Serial.print(F("FLRC")); + break; + + case PACKET_TYPE_BLE: + Serial.print(F("BLE")); + break; + + default: + Serial.print(F("Unknown")); + } + + Serial.print(F(",")); + Serial.print(getFreqInt()); + Serial.print(F("hz")); + + if ((savedPacketType == PACKET_TYPE_LORA) || (savedPacketType == PACKET_TYPE_RANGING)) + { + Serial.print(F(",SF")); + Serial.print(getLoRaSF()); + Serial.print(F(",BW")); + Serial.print(returnBandwidth(savedModParam2)); + Serial.print(F(",CR4:")); + Serial.print((getLoRaCodingRate() + 4)); + } + + if (savedPacketType == PACKET_TYPE_FLRC) + { + Serial.print(F(",BandwidthBitRate_")); + switch (savedModParam1) + { + case FLRC_BR_1_300_BW_1_2: + Serial.print(F("FLRC_BR_1_300_BW_1_2")); + break; + + case FLRC_BR_1_000_BW_1_2: + Serial.print(F("FLRC_BR_1_000_BW_1_2")); + break; + + case FLRC_BR_0_650_BW_0_6: + Serial.print(F("FLRC_BR_0_650_BW_0_6")); + break; + + case FLRC_BR_0_520_BW_0_6: + Serial.print(F("FLRC_BR_0_520_BW_0_6")); + break; + + case FLRC_BR_0_325_BW_0_3: + Serial.print(F("FLRC_BR_0_325_BW_0_3")); + break; + + case FLRC_BR_0_260_BW_0_3: + Serial.print(F("FLRC_BR_0_260_BW_0_3")); + break; + + default: + Serial.print(F("Unknown")); + } + + Serial.print(F(",CodingRate_")); + switch (savedModParam2) + { + case FLRC_CR_1_2: + Serial.print(F("CR_1_2")); + break; + + case FLRC_CR_3_4: + Serial.print(F("CR_3_4")); + break; + + case FLRC_CR_1_0: + Serial.print(F("CR_1_0")); + break; + + default: + Serial.print(F("Unknown")); + } + + Serial.print(F(",BT_")); + switch (savedModParam3) + { + case BT_DIS: + Serial.print(F("NoFiltering")); + break; + + case BT_1: + Serial.print(F("1")); + break; + + case BT_0_5: + Serial.print(F("0_5")); + break; + + default: + Serial.print(F("Unknown")); + } + } +} + + +void SX128XLT::printDevice() +{ +#ifdef SX128XDEBUG + Serial.println(F("printDevice()")); +#endif + + switch (_Device) + { + case DEVICE_SX1280: + Serial.print(F("SX1280")); + break; + + case DEVICE_SX1281: + Serial.print(F("SX1281")); + break; + + default: + Serial.print(F("Unknown Device")); + } +} + + +uint32_t SX128XLT::getFreqInt() +{ + +#ifdef SX128XDEBUG + Serial.println(F("getFreqInt")); +#endif + + //get the current set device frequency, return as long integer + uint8_t Msb = 0; + uint8_t Mid = 0; + uint8_t Lsb = 0; + + uint32_t uinttemp; + float floattemp; + + LTUNUSED(Msb); //to prevent a compiler warning + LTUNUSED(Mid); //to prevent a compiler warning + LTUNUSED(Lsb); //to prevent a compiler warning + + if (savedPacketType == PACKET_TYPE_LORA) + { + Msb = readRegister(REG_RFFrequency23_16); + Mid = readRegister(REG_RFFrequency15_8); + Lsb = readRegister(REG_RFFrequency7_0); + } + + if (savedPacketType == PACKET_TYPE_RANGING) + { + Msb = readRegister(REG_RFFrequency23_16); + Mid = readRegister(REG_RFFrequency15_8); + Lsb = readRegister(REG_RFFrequency7_0); + } + + if (savedPacketType == PACKET_TYPE_FLRC) + { + Msb = readRegister(REG_FLRC_RFFrequency23_16); + Mid = readRegister(REG_FLRC_RFFrequency15_8); + Lsb = readRegister(REG_FLRC_RFFrequency7_0); + } + + floattemp = ((Msb * 0x10000ul) + (Mid * 0x100ul) + Lsb); + floattemp = ((floattemp * FREQ_STEP) / 1000000ul); + uinttemp = (uint32_t)(floattemp * 1000000); + return uinttemp; +} + + +uint8_t SX128XLT::getLoRaSF() +{ +#ifdef SX128XDEBUG + Serial.println(F("getLoRaSF()")); +#endif + return (savedModParam1 >> 4); +} + + +uint32_t SX128XLT::returnBandwidth(uint8_t data) +{ +#ifdef SX128XDEBUG + Serial.println(F("returnBandwidth()")); +#endif + + switch (data) + { + case LORA_BW_0200: + return 203125; + + case LORA_BW_0400: + return 406250; + + case LORA_BW_0800: + return 812500; + + case LORA_BW_1600: + return 1625000; + + default: + break; + } + + return 0x0; //so that a bandwidth not set can be identified +} + + +uint8_t SX128XLT::getLoRaCodingRate() +{ +#ifdef SX128XDEBUG + Serial.println(F("getLoRaCodingRate")); +#endif + + return savedModParam3; +} + + +uint8_t SX128XLT::getInvertIQ() +{ + //IQ mode reg 0x33 +#ifdef SX128XDEBUG + Serial.println(F("getInvertIQ")); +#endif + + return savedPacketParam5; +} + + +uint16_t SX128XLT::getPreamble() +{ +#ifdef SX128XDEBUG + Serial.println(F("getPreamble")); +#endif + + return savedPacketParam1; +} + + +void SX128XLT::printOperatingSettings() +{ + //modified November 2021 to provide more details on FLRC +#ifdef SX128XDEBUG + Serial.println(F("printOperatingSettings()")); +#endif + + printDevice(); + + Serial.print(F(",PACKET_TYPE_")); + + switch (savedPacketType) + { + case PACKET_TYPE_GFSK: + Serial.print(F("GFSK")); + break; + + case PACKET_TYPE_LORA: + Serial.print(F("LORA")); + break; + + case PACKET_TYPE_RANGING: + Serial.print(F("RANGING")); + break; + + case PACKET_TYPE_FLRC: + Serial.print(F("FLRC")); + break; + + + case PACKET_TYPE_BLE: + Serial.print(F("BLE")); + break; + + default: + Serial.print(F("Unknown")); + } + + if ((savedPacketType == PACKET_TYPE_LORA) || (savedPacketType == PACKET_TYPE_RANGING)) + { + Serial.print(F(",Preamble_")); + Serial.print(savedPacketParam1); + + switch (savedPacketParam2) + { + case LORA_PACKET_VARIABLE_LENGTH: + Serial.print(F(",Explicit")); + break; + + case LORA_PACKET_FIXED_LENGTH: + Serial.print(F(",Implicit")); + break; + + default: + Serial.print(F(",Unknown")); + } + + Serial.print(F(",PayloadL_")); + Serial.print(savedPacketParam3); //savedPacketParam3 is payload length + Serial.print(F(",CRC_")); + + switch (savedPacketParam4) + { + case LORA_CRC_ON: + Serial.print(F("ON")); + break; + + case LORA_CRC_OFF: + Serial.print(F("OFF")); + break; + + default: + Serial.print(F("Unknown")); + } + + Serial.print(F(",IQ_")); + + switch (savedPacketParam5) + { + case LORA_IQ_INVERTED: + Serial.print(F("INVERTED")); + break; + + case LORA_IQ_NORMAL: + Serial.print(F("NORMAL")); + break; + + default: + Serial.print(F("Unknown")); + } + + Serial.print(F(",LNAgain_")); + + if (getLNAgain() == 0xC0) + { + Serial.print(F("HighSensitivity")); + } + else + { + Serial.print(F("LowPowerRX")); + } + } + + + if (savedPacketType == PACKET_TYPE_FLRC) + { + Serial.print(F(",Preamble_")); + Serial.print(((savedPacketParam1 >> 4) * 4) + 4); + Serial.print(F("_BITS")); + Serial.print(F(",SyncWordLength_")); + Serial.print(savedPacketParam2); + Serial.print(F(",SyncWordMatch_")); + + switch (savedPacketParam3) + { + case RADIO_RX_MATCH_SYNCWORD_1: + Serial.print(F("1")); + break; + + case RADIO_RX_MATCH_SYNCWORD_2: + Serial.print(F("2")); + break; + + case RADIO_RX_MATCH_SYNCWORD_1_2: + Serial.print(F("1_2")); + break; + + case RADIO_RX_MATCH_SYNCWORD_3: + Serial.print(F("3")); + break; + + case RADIO_RX_MATCH_SYNCWORD_1_3: + Serial.print(F("1_3")); + break; + + case RADIO_RX_MATCH_SYNCWORD_2_3: + Serial.print(F("2_3")); + break; + + case RADIO_RX_MATCH_SYNCWORD_1_2_3: + Serial.print(F("1_2_3")); + break; + + default: + Serial.print(F("Unknown")); + } + + switch (savedPacketParam4) + { + case RADIO_PACKET_VARIABLE_LENGTH: + Serial.print(F(",VariableLengthPacket")); + break; + + case RADIO_PACKET_FIXED_LENGTH: + Serial.print(F(",FixedLengthPacket")); + break; + + default: + Serial.print(F("Unknown")); + } + + Serial.print(F(",PayloadLength_")); + Serial.print(savedPacketParam5); + Serial.print(F(",CRC_")); + Serial.print(savedPacketParam6 >> 4); + Serial.print(F("_Bytes")); + Serial.print(F(",Whitening_")); + + switch (savedPacketParam7) + { + case RADIO_WHITENING_OFF: + Serial.print(F("OFF")); + break; + + case RADIO_WHITENING_ON: + Serial.print(F("ON")); + break; + + default: + Serial.print(F("Unknown")); + } + + Serial.print(F(",LNAgain_")); + + if (getLNAgain() == 0xC0) + { + Serial.print(F("HighSensitivity")); + } + else + { + Serial.print(F("LowPowerRX")); + } + } +} + + +uint8_t SX128XLT::getLNAgain() +{ +#ifdef SX128XDEBUG + Serial.println(F("getLNAgain")); +#endif + + return (readRegister(REG_LNA_REGIME) & 0xC0); +} + + +void SX128XLT::printRegisters(uint16_t Start, uint16_t End) +{ + //prints the contents of SX1280 registers to serial monitor + +#ifdef SX128XDEBUG + Serial.println(F("printRegisters()")); +#endif + + uint16_t Loopv1, Loopv2, RegData; + + Serial.print(F("Reg 0 1 2 3 4 5 6 7 8 9 A B C D E F")); + Serial.println(); + + for (Loopv1 = Start; Loopv1 <= End;) //32 lines + { + Serial.print(F("0x")); + Serial.print((Loopv1), HEX); //print the register number + Serial.print(F(" ")); + for (Loopv2 = 0; Loopv2 <= 15; Loopv2++) + { + RegData = readRegister(Loopv1); + if (RegData < 0x10) + { + Serial.print(F("0")); + } + Serial.print(RegData, HEX); //print the register number + Serial.print(F(" ")); + Loopv1++; + } + Serial.println(); + } +} + + +void SX128XLT::printASCIIPacket(uint8_t *buffer, uint8_t size) +{ +#ifdef SX128XDEBUG + Serial.println(F("printASCIIPacket()")); +#endif + + uint8_t index; + + for (index = 0; index < size; index++) + { + Serial.write(buffer[index]); + } + +} + + +uint8_t SX128XLT::transmit(uint8_t *txbuffer, uint8_t size, uint16_t timeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("transmit()")); +#endif + uint8_t index; + uint8_t bufferdata; + + if (size == 0) + { + return false; + } + + setMode(MODE_STDBY_RC); + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + for (index = 0; index < size; index++) + { + bufferdata = txbuffer[index]; + SPI.transfer(bufferdata); + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + _TXPacketL = size; + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(timeout); //this starts the TX + + if (!wait) + { + return _TXPacketL; + } + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } +} + + +uint8_t SX128XLT::transmitIRQ(uint8_t *txbuffer, uint8_t size, uint16_t timeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("transmitIRQ()")); +#endif + uint8_t index; + uint8_t bufferdata; + + if (size == 0) + { + return false; + } + + setMode(MODE_STDBY_RC); + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + for (index = 0; index < size; index++) + { + bufferdata = txbuffer[index]; + SPI.transfer(bufferdata); + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + _TXPacketL = size; + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setTx(timeout); //this starts the TX + + if (!wait) + { + return _TXPacketL; + } + + //0x4001 = IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT + while (!(readIrqStatus() & 0x4001 )); //wait for IRQs going active + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } +} + + +void SX128XLT::setTxParams(int8_t TXpower, uint8_t RampTime) +{ +#ifdef SX128XDEBUG + Serial.println(F("setTxParams()")); +#endif + + uint8_t buffer[2]; + + savedTXPower = TXpower; + + //power register is set to 0 to 31 which is -18dBm to +12dBm + buffer[0] = (TXpower + 18); + buffer[1] = (uint8_t)RampTime; + writeCommand(RADIO_SET_TXPARAMS, buffer, 2); +} + + +void SX128XLT::setTx(uint16_t timeout) +{ + +#ifdef SX128XDEBUG + Serial.println(F("setTx()")); +#endif + + if (_rxtxpinmode) + { + txEnable(); + } + + uint8_t buffer[3]; + + clearIrqStatus(IRQ_RADIO_ALL); //clear all interrupt flags + buffer[0] = _PERIODBASE; + buffer[1] = ( uint8_t )( ( timeout >> 8 ) & 0x00FF ); + buffer[2] = ( uint8_t )( timeout & 0x00FF ); + writeCommand(RADIO_SET_TX, buffer, 3 ); +} + + +void SX128XLT::clearIrqStatus(uint16_t irqMask) +{ +#ifdef SX128XDEBUG + Serial.println(F("clearIrqStatus()")); +#endif + + uint8_t buffer[2]; + + buffer[0] = (uint8_t) (irqMask >> 8); + buffer[1] = (uint8_t) (irqMask & 0xFF); + writeCommand(RADIO_CLR_IRQSTATUS, buffer, 2); +} + + +uint16_t SX128XLT::readIrqStatus() +{ +#ifdef SX128XDEBUG + Serial.print(F("readIrqStatus()")); +#endif + + uint16_t temp; + uint8_t buffer[2]; + + readCommand(RADIO_GET_IRQSTATUS, buffer, 2); + temp = ((buffer[0] << 8) + buffer[1]); + return temp; +} + + +void SX128XLT::printIrqStatus() +{ +#ifdef SX128XDEBUG + Serial.println(F("printIrqStatus()")); +#endif + + uint16_t _IrqStatus; + _IrqStatus = readIrqStatus(); + + //0x0001 + if (_IrqStatus & IRQ_TX_DONE) + { + Serial.print(F(",IRQ_TX_DONE")); + } + + //0x0002 + if (_IrqStatus & IRQ_RX_DONE) + { + Serial.print(F(",IRQ_RX_DONE")); + } + + //0x0004 + if (_IrqStatus & IRQ_SYNCWORD_VALID) + { + Serial.print(F(",IRQ_SYNCWORD_VALID")); + } + + //0x0008 + if (_IrqStatus & IRQ_SYNCWORD_ERROR) + { + Serial.print(F(",IRQ_SYNCWORD_ERROR")); + } + + //0x0010 + if (_IrqStatus & IRQ_HEADER_VALID) + { + Serial.print(F(",IRQ_HEADER_VALID")); + } + + //0x0020 + if (_IrqStatus & IRQ_HEADER_ERROR) + { + Serial.print(F(",IRQ_HEADER_ERROR")); + } + + //0x0040 + if (_IrqStatus & IRQ_CRC_ERROR) + { + Serial.print(F(",IRQ_CRC_ERROR")); + } + + //0x0080 + if (_IrqStatus & IRQ_RANGING_SLAVE_RESPONSE_DONE) + { + Serial.print(F(",IRQ_RANGING_SLAVE_RESPONSE_DONE")); + } + + //0x0100 + if (_IrqStatus & IRQ_RANGING_SLAVE_REQUEST_DISCARDED) + { + Serial.print(F(",IRQ_RANGING_SLAVE_REQUEST_DISCARDED")); + } + + //0x0200 + if (_IrqStatus & IRQ_RANGING_MASTER_RESULT_VALID) + { + Serial.print(F(",IRQ_RANGING_MASTER_RESULT_VALID")); + } + + //0x0400 + if (_IrqStatus & IRQ_RANGING_MASTER_RESULT_TIMEOUT) + { + Serial.print(F(",IRQ_RANGING_MASTER_RESULT_TIMEOUT")); + } + + //0x0800 + if (_IrqStatus & IRQ_RANGING_SLAVE_REQUEST_VALID) + { + Serial.print(F(",IRQ_RANGING_SLAVE_REQUEST_VALID")); + } + + //0x1000 + if (_IrqStatus & IRQ_CAD_DONE) + { + Serial.print(F(",IRQ_CAD_DONE")); + } + + //0x2000 + if (_IrqStatus & IRQ_CAD_ACTIVITY_DETECTED) + { + Serial.print(F(",IRQ_CAD_ACTIVITY_DETECTED")); + } + + //0x4000 + if (_IrqStatus & IRQ_RX_TX_TIMEOUT) + { + Serial.print(F(",IRQ_RX_TX_TIMEOUT")); + } + + //0x8000 + if (_IrqStatus & IRQ_PREAMBLE_DETECTED) + { + Serial.print(F(",IRQ_PREAMBLE_DETECTED")); + } +} + + +uint16_t SX128XLT::CRCCCITT(uint8_t *buffer, uint32_t size, uint16_t start) +{ +#ifdef SX128XDEBUG + Serial.println(F("CRCCCITT()")); +#endif + + uint32_t index; + uint16_t libraryCRC; + uint8_t j; + + libraryCRC = start; //start value for CRC16 + + for (index = 0; index < size; index++) + { + libraryCRC ^= (((uint16_t)buffer[index]) << 8); + for (j = 0; j < 8; j++) + { + if (libraryCRC & 0x8000) + libraryCRC = (libraryCRC << 1) ^ 0x1021; + else + libraryCRC <<= 1; + } + } + + return libraryCRC; +} + + +uint8_t SX128XLT::receive(uint8_t *rxbuffer, uint8_t size, uint16_t timeout, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("receive()")); +#endif + + uint8_t index, RXstart, RXend; + uint16_t regdata; + uint8_t buffer[2]; + + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); //set for IRQ on RX done or timeout + setRx(timeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + regdata = readIrqStatus(); + + if ( (regdata & IRQ_HEADER_ERROR) | (regdata & IRQ_CRC_ERROR) | (regdata & IRQ_RX_TX_TIMEOUT ) | (regdata & IRQ_SYNCWORD_ERROR )) //check if any of the preceding IRQs is set + { + return 0; //packet is errored somewhere so return 0 + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + if (_RXPacketL > size) //check passed buffer is big enough for packet + { + _RXPacketL = size; //truncate packet if not enough space + } + + RXstart = buffer[1]; + RXend = RXstart + _RXPacketL; + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(RXstart); + SPI.transfer(0xFF); + + for (index = RXstart; index < RXend; index++) + { + regdata = SPI.transfer(0); + rxbuffer[index] = regdata; + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _RXPacketL; +} + + +uint8_t SX128XLT::receiveIRQ(uint8_t *rxbuffer, uint8_t size, uint16_t timeout, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("receiveIRQ()")); +#endif + + uint8_t index, RXstart, RXend; + uint16_t regdata; + uint8_t buffer[2]; + + setRx(timeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + //0x4022 = IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR + while (!(readIrqStatus() & 0x4022 )); //wait for IRQs going active + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + return 0; //packet is errored somewhere so return 0 + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + if (_RXPacketL > size) //check passed buffer is big enough for packet + { + _RXPacketL = size; //truncate packet if not enough space + } + + RXstart = buffer[1]; + RXend = RXstart + _RXPacketL; + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(RXstart); + SPI.transfer(0xFF); + + for (index = RXstart; index < RXend; index++) + { + regdata = SPI.transfer(0); + rxbuffer[index] = regdata; + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _RXPacketL; +} + + +int16_t SX128XLT::readPacketRSSI2() +{ +#ifdef SX128XDEBUG + Serial.println(F("readPacketRSSI2()")); +#endif + + uint8_t status[5]; + int16_t rssi; + + readCommand(RADIO_GET_PACKETSTATUS, status, 5) ; + rssi = -status[0] / 2; + + return rssi; +} + + +int16_t SX128XLT::readPacketRSSI() +{ +#ifdef SX128XDEBUG + Serial.println(F("readPacketRSSI()")); +#endif + + int8_t snr; + int16_t rssi = 0; //so routine returns 0 if packet not LoRa or FLRC + uint8_t status[5]; + + snr = readPacketSNR(); + readCommand(RADIO_GET_PACKETSTATUS, status, 5) ; + + if (savedPacketType == PACKET_TYPE_LORA) + { + rssi = -status[0] / 2; + if (snr < 0) + { + rssi = rssi + snr; + } + } + + if (savedPacketType == PACKET_TYPE_FLRC) + { + rssi = -status[1] / 2; + } + + return rssi; +} + + +int8_t SX128XLT::readPacketSNR() +{ +#ifdef SX128XDEBUG + Serial.println(F("readPacketSNR()")); +#endif + + uint8_t status[5]; + int8_t snr; + + readCommand(RADIO_GET_PACKETSTATUS, status, 5) ; + + if ( status[1] < 128 ) + { + snr = status[1] / 4 ; + } + else + { + snr = (( status[1] - 256 ) / 4); + } + + return snr; +} + + +uint8_t SX128XLT::readRXPacketL() +{ +#ifdef SX128XDEBUG + Serial.println(F("readRXPacketL()")); +#endif + + uint8_t buffer[2]; + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + return _RXPacketL; +} + + +void SX128XLT::setRx(uint16_t timeout) +{ +#ifdef SX128XDEBUG + Serial.println(F("setRx()")); +#endif + + uint8_t buffer[3]; + + if (_rxtxpinmode) + { + rxEnable(); + } + + clearIrqStatus(IRQ_RADIO_ALL); //clear all interrupt flags + buffer[0] = _PERIODBASE; //use pre determined period base setting + buffer[1] = ( uint8_t ) ((timeout >> 8 ) & 0x00FF); + buffer[2] = ( uint8_t ) (timeout & 0x00FF); + writeCommand(RADIO_SET_RX, buffer, 3); +} + + +void SX128XLT::setPeriodBase(uint8_t value) +{ +#ifdef SX128XDEBUG + Serial.println(F("setPeriodBase")); +#endif + + _PERIODBASE = value; +} + + +uint8_t SX128XLT::getPeriodBase() +{ +#ifdef SX128XDEBUG + Serial.println(F("getPeriodBase")); +#endif + + return _PERIODBASE; +} + + +void SX128XLT::setSyncWord1(uint32_t syncword) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("setSyncWord1()")); +#endif + + //For FLRC packet type. The SyncWord is one byte shorter and + //the base address is shifted by one byte versus GFSK packets + writeRegister( REG_FLRCSYNCWORD1_BASEADDR, ( syncword >> 24 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD1_BASEADDR + 1, ( syncword >> 16 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD1_BASEADDR + 2, ( syncword >> 8 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD1_BASEADDR + 3, syncword & 0x000000FF ); +} + + +void SX128XLT::setSyncWord2(uint32_t syncword) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("setSyncWord2()")); +#endif + + //For FLRC packet type. The SyncWord is one byte shorter and + //the base address is shifted by one byte versus GFSK packets + writeRegister( REG_FLRCSYNCWORD2_BASEADDR, ( syncword >> 24 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD2_BASEADDR + 1, ( syncword >> 16 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD2_BASEADDR + 2, ( syncword >> 8 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD2_BASEADDR + 3, syncword & 0x000000FF ); +} + + +void SX128XLT::setSyncWord3(uint32_t syncword) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("setSyncWord3()")); +#endif + + //For FLRC packet type. The SyncWord is one byte shorter and + //the base address is shifted by one byte versus GFSK packets + writeRegister( REG_FLRCSYNCWORD3_BASEADDR, ( syncword >> 24 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD3_BASEADDR + 1, ( syncword >> 16 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD3_BASEADDR + 2, ( syncword >> 8 ) & 0x000000FF ); + writeRegister( REG_FLRCSYNCWORD3_BASEADDR + 3, syncword & 0x000000FF ); +} + + +void SX128XLT::setSyncWordErrors(uint8_t errors) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("setSyncWordErrors()")); +#endif + + writeRegister( REG_LR_SYNCWORDTOLERANCE, errors ); +} + + +void SX128XLT::setSleep(uint8_t sleepconfig) +{ +//Note: Its been reported that the TXpower setting is not retained in sleep mode + +#ifdef SX128XDEBUG + Serial.println(F("setSleep()")); +#endif + + setMode(MODE_STDBY_RC); + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + //need to save registers to device RAM first + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_SET_SAVECONTEXT); + digitalWrite(_NSS, HIGH); + + checkBusy(); + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_SET_SLEEP); + SPI.transfer(sleepconfig); + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + delay(1); //allow time for shutdown +} + + +void SX128XLT::printHEXByte(uint8_t temp) +{ + if (temp < 0x10) + { + Serial.print(F("0")); + } + Serial.print(temp, HEX); +} + + +void SX128XLT::wake() +{ +#ifdef SX128XDEBUG + Serial.println(F("wake()")); +#endif + + digitalWrite(_NSS, LOW); + delay(1); + digitalWrite(_NSS, HIGH); + delay(1); +} + + +int32_t SX128XLT::getFrequencyErrorRegValue() +{ +#ifdef SX128XDEBUG + Serial.println(F("getFrequencyErrorRegValue()")); +#endif + + int32_t FrequencyError; + uint32_t regmsb, regmid, reglsb, allreg; + + setMode(MODE_STDBY_XOSC); + + regmsb = readRegister( REG_LR_ESTIMATED_FREQUENCY_ERROR_MSB ); + regmsb = regmsb & 0x0F; //clear bit 20 which is always set + regmid = readRegister( REG_LR_ESTIMATED_FREQUENCY_ERROR_MSB + 1 ); + reglsb = readRegister( REG_LR_ESTIMATED_FREQUENCY_ERROR_MSB + 2 ); + + setMode(MODE_STDBY_RC); + +#ifdef LORADEBUG + Serial.println(); + Serial.print(F("Registers ")); + Serial.print(regmsb, HEX); + Serial.print(F(" ")); + Serial.print(regmid, HEX); + Serial.print(F(" ")); + Serial.println(reglsb, HEX); +#endif + + allreg = (uint32_t) ( regmsb << 16 ) | ( regmid << 8 ) | reglsb; + + if (allreg & 0x80000) + { + FrequencyError = (0xFFFFF - allreg) * -1; + } + else + { + FrequencyError = allreg; + } + + return FrequencyError; +} + + +int32_t SX128XLT::getFrequencyErrorHz() +{ +#ifdef SX128XDEBUG + Serial.println(F("getFrequencyErrorHz()")); +#endif + + int32_t error, regvalue; + uint32_t bandwidth; + float divider; + + bandwidth = returnBandwidth(savedModParam2); //gets the last configured bandwidth + divider = (float) 1625000 / bandwidth; //data sheet says 1600000, but bandwidth is 1625000 + regvalue = getFrequencyErrorRegValue(); + error = (FREQ_ERROR_CORRECTION * regvalue) / divider; + + return error; +} + + +uint8_t SX128XLT::transmitAddressed(uint8_t *txbuffer, uint8_t size, char txpackettype, char txdestination, char txsource, uint32_t timeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("transmitAddressed()")); +#endif + uint8_t index; + uint8_t bufferdata; + + if (size == 0) + { + return false; + } + + setMode(MODE_STDBY_RC); + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + SPI.transfer(txpackettype); //Write the packet type + SPI.transfer(txdestination); //Destination node + SPI.transfer(txsource); //Source node + _TXPacketL = 3 + size; //we have added 3 header bytes to size + + for (index = 0; index < size; index++) + { + bufferdata = txbuffer[index]; + SPI.transfer(bufferdata); + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + setPayloadLength(_TXPacketL); + + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(timeout); //this starts the TX + + if (!wait) + { + return _TXPacketL; + } + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } +} + + +uint8_t SX128XLT::receiveAddressed(uint8_t *rxbuffer, uint8_t size, uint16_t timeout, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("receiveAddressed()")); +#endif + + uint8_t index, RXstart, RXend; + uint16_t regdata; + uint8_t buffer[2]; + + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); //set for IRQ on RX done or timeout + setRx(timeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + regdata = readIrqStatus(); + + if ( (regdata & IRQ_HEADER_ERROR) | (regdata & IRQ_CRC_ERROR) | (regdata & IRQ_RX_TX_TIMEOUT ) | (regdata & IRQ_SYNCWORD_ERROR )) //check if any of the preceding IRQs is set + { + return 0; //packet is errored somewhere so return 0 + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + if (_RXPacketL > size) //check passed buffer is big enough for packet + { + _RXPacketL = size; //truncate packet if not enough space + } + + RXstart = buffer[1]; + RXend = RXstart + _RXPacketL; + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(RXstart); + SPI.transfer(0xFF); + + + + for (index = RXstart; index < RXend; index++) + { + regdata = SPI.transfer(0); + rxbuffer[index] = regdata; + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _RXPacketL; //so we can check for packet having enough buffer space +} + + +uint8_t SX128XLT::readRXPacketType() +{ +#ifdef SX128XDEBUG + Serial.println(F("readRXPacketType()")); +#endif + + return _RXPacketType; +} + + +uint8_t SX128XLT::readPacket(uint8_t *rxbuffer, uint8_t size) +{ +#ifdef SX128XDEBUG + Serial.println(F("readPacket()")); +#endif + + uint8_t index, regdata, RXstart, RXend; + uint8_t buffer[2]; + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + if (_RXPacketL > size) //check passed buffer is big enough for packet + { + _RXPacketL = size; //truncate packet if not enough space + } + + RXstart = buffer[1]; + RXend = RXstart + _RXPacketL; + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(RXstart); + SPI.transfer(0xFF); + + for (index = RXstart; index < RXend; index++) + { + regdata = SPI.transfer(0); + rxbuffer[index] = regdata; + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _RXPacketL; //so we can check for packet having enough buffer space +} + + +void SX128XLT::printHEXPacket(uint8_t *buffer, uint8_t size) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("printHEXPacket() ")); +#endif + + uint8_t index; + + for (index = 0; index < size; index++) + { + printHEXByte(buffer[index]); + Serial.print(F(" ")); + } +} + + +void SX128XLT::printHEXPacket(char *buffer, uint8_t size) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("printHEXPacket() ")); +#endif + + uint8_t index; + + for (index = 0; index < size; index++) + { + printHEXByte(buffer[index]); + Serial.print(F(" ")); + } +} + + +void SX128XLT::printArrayHEX(uint8_t *buffer, uint8_t size) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("printArrayHEX() ")); +#endif + + uint8_t index; + + for (index = 0; index < size; index++) + { + printHEXByte(buffer[index]); + Serial.print(F(" ")); + } +} + + +void SX128XLT::printArrayHEX(char *buffer, uint8_t size) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("printArrayHEX() ")); +#endif + + uint8_t index; + + for (index = 0; index < size; index++) + { + printHEXByte(buffer[index]); + Serial.print(F(" ")); + } +} + + +//*********************************************************************************** +//direct access SX buffer routines +//these routines read\write variables\data direct to the LoRa devices internal buffer +//*********************************************************************************** + +void SX128XLT::startWriteSXBuffer(uint8_t ptr) +{ +#ifdef SX128XDEBUG + Serial.println(F("startWriteSXBuffer()")); +#endif + + _TXPacketL = 0; //this variable used to keep track of bytes written + setMode(MODE_STDBY_RC); + setBufferBaseAddress(ptr, 0); //TX,RX + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(ptr); //address in SX buffer to write to + //SPI interface ready for byte to write to buffer +} + + +uint8_t SX128XLT::endWriteSXBuffer() +{ +#ifdef SX128XDEBUG + Serial.println(F("endWriteSXBuffer()")); +#endif + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _TXPacketL; +} + + +void SX128XLT::startReadSXBuffer(uint8_t ptr) +{ +#ifdef SX128XDEBUG + Serial.println(F("startReadSXBuffer")); +#endif + + _RXPacketL = 0; + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(ptr); + SPI.transfer(0xFF); + + //next line would be data = SPI.transfer(0); + //SPI interface ready for byte to read from +} + + +uint8_t SX128XLT::endReadSXBuffer() +{ +#ifdef SX128XDEBUG + Serial.println(F("endReadSXBuffer()")); +#endif + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _RXPacketL; +} + + +void SX128XLT::writeUint8(uint8_t x) +{ +#ifdef SX128XDEBUG + Serial.println(F("writeUint8()")); +#endif + + SPI.transfer(x); + _TXPacketL++; //increment count of bytes written +} + +uint8_t SX128XLT::readUint8() +{ +#ifdef SX128XDEBUG + Serial.println(F("readUint8()")); +#endif + uint8_t x; + + x = SPI.transfer(0); + _RXPacketL++; //increment count of bytes read + return (x); +} + + +void SX128XLT::writeInt8(int8_t x) +{ +#ifdef SX128XDEBUG + Serial.println(F("writeInt8()")); +#endif + + SPI.transfer(x); + + _TXPacketL++; //increment count of bytes written +} + + +int8_t SX128XLT::readInt8() +{ +#ifdef SX128XDEBUG + Serial.println(F("readInt8()")); +#endif + int8_t x; + + x = SPI.transfer(0); + + _RXPacketL++; //increment count of bytes read + return (x); +} + + +void SX128XLT::writeInt16(int16_t x) +{ +#ifdef SX128XDEBUG + Serial.println(F("writeInt16()")); +#endif + + SPI.transfer(lowByte(x)); + SPI.transfer(highByte(x)); + + _TXPacketL = _TXPacketL + 2; //increment count of bytes written +} + + +int16_t SX128XLT::readInt16() +{ +#ifdef SX128XDEBUG + Serial.println(F("readInt16()")); +#endif + uint8_t lowbyte, highbyte; + + lowbyte = SPI.transfer(0); + highbyte = SPI.transfer(0); + + _RXPacketL = _RXPacketL + 2; //increment count of bytes read + return ((highbyte << 8) + lowbyte); +} + + +void SX128XLT::writeUint16(uint16_t x) +{ +#ifdef SX128XDEBUG + Serial.println(F("writeUint16()")); +#endif + + SPI.transfer(lowByte(x)); + SPI.transfer(highByte(x)); + + _TXPacketL = _TXPacketL + 2; //increment count of bytes written +} + + +uint16_t SX128XLT::readUint16() +{ +#ifdef SX128XDEBUG + Serial.println(F("writeUint16()")); +#endif + uint8_t lowbyte, highbyte; + + lowbyte = SPI.transfer(0); + highbyte = SPI.transfer(0); + + _RXPacketL = _RXPacketL + 2; //increment count of bytes read + return ((highbyte << 8) + lowbyte); +} + + +void SX128XLT::writeInt32(int32_t x) +{ +#ifdef SX128XDEBUG + Serial.println(F("writeInt32()")); +#endif + + uint8_t i, j; + + union + { + uint8_t b[4]; + int32_t f; + } data; + data.f = x; + + for (i = 0; i < 4; i++) + { + j = data.b[i]; + SPI.transfer(j); + } + + _TXPacketL = _TXPacketL + 4; //increment count of bytes written +} + + +int32_t SX128XLT::readInt32() +{ +#ifdef SX128XDEBUG + Serial.println(F("readInt32()")); +#endif + + uint8_t i, j; + + union + { + uint8_t b[4]; + int32_t f; + } readdata; + + for (i = 0; i < 4; i++) + { + j = SPI.transfer(0); + readdata.b[i] = j; + } + _RXPacketL = _RXPacketL + 4; //increment count of bytes read + return readdata.f; +} + + +void SX128XLT::writeUint32(uint32_t x) +{ +#ifdef SX128XDEBUG + Serial.println(F("writeUint32()")); +#endif + + uint8_t i, j; + + union + { + uint8_t b[4]; + uint32_t f; + } data; + data.f = x; + + for (i = 0; i < 4; i++) + { + j = data.b[i]; + SPI.transfer(j); + } + + _TXPacketL = _TXPacketL + 4; //increment count of bytes written +} + + +uint32_t SX128XLT::readUint32() +{ +#ifdef SX128XDEBUG + Serial.println(F("readUint32()")); +#endif + + uint8_t i, j; + + union + { + uint8_t b[4]; + uint32_t f; + } readdata; + + for (i = 0; i < 4; i++) + { + j = SPI.transfer(0); + readdata.b[i] = j; + } + _RXPacketL = _RXPacketL + 4; //increment count of bytes read + return readdata.f; +} + + +void SX128XLT::writeFloat(float x) +{ +#ifdef SX128XDEBUG + Serial.println(F("writeFloat()")); +#endif + + uint8_t i, j; + + union + { + uint8_t b[4]; + float f; + } data; + data.f = x; + + for (i = 0; i < 4; i++) + { + j = data.b[i]; + SPI.transfer(j); + } + + _TXPacketL = _TXPacketL + 4; //increment count of bytes written +} + + +float SX128XLT::readFloat() +{ +#ifdef SX128XDEBUG + Serial.println(F("readFloat()")); +#endif + + uint8_t i, j; + + union + { + uint8_t b[4]; + float f; + } readdata; + + for (i = 0; i < 4; i++) + { + j = SPI.transfer(0); + readdata.b[i] = j; + } + _RXPacketL = _RXPacketL + 4; //increment count of bytes read + return readdata.f; +} + + +uint8_t SX128XLT::transmitSXBuffer(uint8_t startaddr, uint8_t length, uint16_t timeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("transmitSXBuffer()")); +#endif + + setBufferBaseAddress(startaddr, 0); //TX, RX + setPayloadLength(length); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(timeout); //this starts the TX + + if (!wait) + { + return _TXPacketL; + } + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } +} + + +uint8_t SX128XLT::transmitSXBufferIRQ(uint8_t startaddr, uint8_t length, uint16_t timeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("transmitSXBuffer()")); +#endif + + setBufferBaseAddress(startaddr, 0); //TX, RX + setPayloadLength(length); + setTxParams(txpower, RAMP_TIME); + setTx(timeout); //this starts the TX + + if (!wait) + { + return _TXPacketL; + } + + //0x4001 = IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT + while (!(readIrqStatus() & 0x4001 )); //wait for IRQs going active + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } +} + + +void SX128XLT::writeBuffer(uint8_t *txbuffer, uint8_t size) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("writeBuffer()")); +#endif + + uint8_t index, regdata; + + _TXPacketL = _TXPacketL + size; //these are the number of bytes that will be added + size--; //loose one byte from size, the last byte written MUST be a 0 + + for (index = 0; index < size; index++) + { + regdata = txbuffer[index]; + SPI.transfer(regdata); + } + + SPI.transfer(0); //this ensures last byte of buffer written really is a null (0) + +} + + +uint8_t SX128XLT::receiveSXBuffer(uint8_t startaddr, uint16_t timeout, uint8_t wait ) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("receiveSXBuffer()")); +#endif + + uint16_t regdata; + uint8_t buffer[2]; + + setMode(MODE_STDBY_RC); + setBufferBaseAddress(0, startaddr); //order is TX RX + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); //set for IRQ on RX done or timeout + setRx(timeout); + + if (!wait) + { + return 0; + } + + while (!digitalRead(_RXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + regdata = readIrqStatus(); + + if ( (regdata & IRQ_HEADER_ERROR) | (regdata & IRQ_CRC_ERROR) | (regdata & IRQ_RX_TX_TIMEOUT ) | (regdata & IRQ_SYNCWORD_ERROR )) + { + return 0; //no RX done and header valid only, could be CRC error + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + return _RXPacketL; +} + + +uint8_t SX128XLT::receiveSXBufferIRQ(uint8_t startaddr, uint16_t timeout, uint8_t wait ) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("receiveSXBufferIRQ()")); +#endif + + uint8_t buffer[2]; + + setMode(MODE_STDBY_RC); + setBufferBaseAddress(0, startaddr); //order is TX RX + setRx(timeout); + + if (!wait) + { + return 0; + } + + //0x4022 = IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR + while (!(readIrqStatus() & 0x4022 )); //wait for IRQs going active + + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + return 0; //no RX done and header valid only, could be CRC error + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + return _RXPacketL; +} + + +uint8_t SX128XLT::readBuffer(uint8_t *rxbuffer) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("readBuffer()")); +#endif + + uint8_t index = 0, regdata; + + do //need to find the size of the buffer first + { + regdata = SPI.transfer(0); + rxbuffer[index] = regdata; //fill the buffer. + index++; + } while (regdata != 0); //keep reading until we have reached the null (0) at the buffer end + //or exceeded size of buffer allowed + _RXPacketL = _RXPacketL + index; //increment count of bytes read + return index; //return the actual size of the buffer, till the null (0) detected + +} + + +uint16_t SX128XLT::CRCCCITTSX(uint8_t startadd, uint8_t endadd, uint16_t startvalue) +{ + //genrates a CRC of an area of the internal SX buffer + +#ifdef SX126XDEBUG1 + Serial.println(F("CRCCCITTSX()")); +#endif + + uint16_t index, libraryCRC; + uint8_t j; + + libraryCRC = startvalue; //start value for CRC16 + startReadSXBuffer(startadd); //begin the buffer read + + for (index = startadd; index <= endadd; index++) + { + libraryCRC ^= (((uint16_t) readUint8() ) << 8); + for (j = 0; j < 8; j++) + { + if (libraryCRC & 0x8000) + libraryCRC = (libraryCRC << 1) ^ 0x1021; + else + libraryCRC <<= 1; + } + } + + endReadSXBuffer(); //end the buffer read + return libraryCRC; +} + + +uint8_t SX128XLT::getByteSXBuffer(uint8_t addr) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("getByteSXBuffer()")); +#endif + + uint8_t regdata; + + setMode(MODE_STDBY_RC); //this is needed to ensure we can read from buffer OK. + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(addr); + SPI.transfer(0xFF); + regdata = SPI.transfer(0); + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return regdata; +} + + +void SX128XLT::printSXBufferHEX(uint8_t start, uint8_t end) +{ +#ifdef SX128XDEBUG + Serial.println(F("printSXBufferHEX()")); +#endif + + uint8_t index, regdata; + + + setMode(MODE_STDBY_RC); + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION //to use SPI_TRANSACTION enable define at beginning of CPP file + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(start); + SPI.transfer(0xFF); + + for (index = start; index <= end; index++) + { + regdata = SPI.transfer(0); + printHEXByte(regdata); + Serial.print(F(" ")); + + } + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + +} + + +void SX128XLT::writeBufferChar(char *txbuffer, uint8_t size) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("writeBuffer()")); +#endif + + uint8_t index, regdata; + + _TXPacketL = _TXPacketL + size; //these are the number of bytes that will be added + size--; //loose one byte from size, the last byte written MUST be a 0 + + for (index = 0; index < size; index++) + { + regdata = txbuffer[index]; + SPI.transfer(regdata); + } + + SPI.transfer(0); //this ensures last byte of buffer writen really is a null (0) +} + + +uint8_t SX128XLT::readBufferChar(char *rxbuffer) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("readBufferChar()")); +#endif + + uint8_t index = 0, regdata; + + do //need to find the size of the buffer first + { + regdata = SPI.transfer(0); + rxbuffer[index] = regdata; //fill the buffer. + index++; + } while (regdata != 0); //keep reading until we have reached the null (0) at the buffer end + //or exceeded size of buffer allowed + + _RXPacketL = _RXPacketL + index; //increment count of bytes read + + return index; //return the actual size of the buffer, till the null (0) detected + +} + + +//There are two ways of setting the payload length, enabled by default is the appropriate function setPacketParams() +//To set payload length with a discovered direct register length enable the #define USEPAYLOADLENGTHREGISTER + +void SX128XLT::setPayloadLength(uint8_t length) +{ +#ifdef SX128XDEBUG + Serial.println(F("setPayloadLength()")); +#endif + if (savedPacketType == PACKET_TYPE_LORA) + { +#ifdef USEPAYLOADLENGTHREGISTER + //Serial.println(F(" USEPAYLOADLENGTHREGISTER ")); + writeRegister(REG_LR_PAYLOADLENGTH, length); +#else + //Serial.println(F(" USE setPacketParams() ")); + setPacketParams(savedPacketParam1, savedPacketParam2, length, savedPacketParam4, savedPacketParam5); +#endif + } + + if (savedPacketType == PACKET_TYPE_FLRC) + { +#ifdef USEPAYLOADLENGTHREGISTER + //Serial.println(F(" USEPAYLOADLENGTHREGISTER FLRC ")); + writeRegister(REG_LR_FLRCPAYLOADLENGTH, length); +#else + //Serial.println(F(" USE setPacketParams() FLRC ")); + setPacketParams(savedPacketParam1, savedPacketParam2, savedPacketParam3, savedPacketParam4, length, savedPacketParam6, savedPacketParam7); +#endif + } + +} + + +void SX128XLT::setFLRCPayloadLengthReg(uint8_t length) +{ + //uses a discovered register that is the FLRC payload length for TX and RX filtering +#ifdef SX128XDEBUG + Serial.println(F("setFLRCPayloadLengthReg()")); +#endif + + writeRegister(REG_LR_FLRCPAYLOADLENGTH, length); +} + + +void SX128XLT::setLoRaPayloadLengthReg(uint8_t length) +{ + //uses a discovered register that is the LoRa payload length for TX +#ifdef SX128XDEBUG + Serial.println(F("setLoRaPayloadLengthReg()")); +#endif + + writeRegister(REG_LR_PAYLOADLENGTH, length); +} + + +uint8_t SX128XLT::getPacketType() +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} getPacketType() ")); +#endif + + return savedPacketType; +} + +//******************************************************************************* +//Ranging routines +//******************************************************************************* + +void SX128XLT::setRangingSlaveAddress(uint32_t address) +{ + //sets address of ranging slave +#ifdef SX128XDEBUG + Serial.println(F("SetRangingSlaveAddress()")); +#endif + + uint8_t buffer[4]; + + buffer[0] = (address >> 24u ) & 0xFFu; + buffer[1] = (address >> 16u) & 0xFFu; + buffer[2] = (address >> 8u) & 0xFFu; + buffer[3] = (address & 0xFFu); + writeRegisters(0x916, buffer, 4 ); +} + + +void SX128XLT::setRangingSlaveAddress(uint32_t address, uint8_t bits) +{ + //sets address of ranging slave +#ifdef SX128XDEBUG + Serial.println(F("SetRangingSlaveAddress()")); +#endif + + uint8_t buffer[4]; + + buffer[0] = (address >> 24u ) & 0xFFu; + buffer[1] = (address >> 16u) & 0xFFu; + buffer[2] = (address >> 8u) & 0xFFu; + buffer[3] = (address & 0xFFu); + writeRegisters(0x916, buffer, 4 ); + writeRegister(REG_LR_RANGINGIDCHECKLENGTH, bits); //set slave to check all 32 bits of address +} + + + +void SX128XLT::setRangingMasterAddress(uint32_t address) +{ + //sets address of ranging master +#ifdef SX128XDEBUG + Serial.println(F("SetRangingMasterAddress()")); +#endif + + uint8_t buffer[4]; + + buffer[0] = (address >> 24u ) & 0xFFu; + buffer[1] = (address >> 16u) & 0xFFu; + buffer[2] = (address >> 8u) & 0xFFu; + buffer[3] = (address & 0xFFu); + writeRegisters(0x912, buffer, 4 ); +} + + +void SX128XLT::setRangingCalibration(uint16_t cal) +{ +#ifdef SX128XDEBUG + Serial.println(F("setRangingCalibration()")); +#endif + + savedCalibration = cal; + writeRegister( REG_LR_RANGINGRERXTXDELAYCAL, ( uint8_t )( ( cal >> 8 ) & 0xFF ) ); + writeRegister( REG_LR_RANGINGRERXTXDELAYCAL + 1, ( uint8_t )( ( cal ) & 0xFF ) ); +} + + +void SX128XLT::setRangingRole(uint8_t role) +{ +#ifdef SX128XDEBUG + Serial.println(F("setRangingRole()")); +#endif + + uint8_t buffer[1]; + + buffer[0] = role; + writeCommand(RADIO_SET_RANGING_ROLE, buffer, 1 ); +} + + +uint32_t SX128XLT::getRangingResultRegValue(uint8_t resultType) +{ + uint32_t valLsb = 0; + + setMode(MODE_STDBY_XOSC); + writeRegister( 0x97F, readRegister( 0x97F ) | ( 1 << 1 ) ); // enable LORA modem clock + writeRegister( REG_LR_RANGINGRESULTCONFIG, ( readRegister( REG_LR_RANGINGRESULTCONFIG ) & MASK_RANGINGMUXSEL ) | ( ( ( ( uint8_t )resultType ) & 0x03 ) << 4 ) ); + valLsb = ( ( (uint32_t) readRegister( REG_LR_RANGINGRESULTBASEADDR ) << 16 ) | ( (uint32_t) readRegister( REG_LR_RANGINGRESULTBASEADDR + 1 ) << 8 ) | ( readRegister( REG_LR_RANGINGRESULTBASEADDR + 2 ) ) ); + setMode(MODE_STDBY_RC); + return valLsb; +} + + +double SX128XLT::getRangingDistance(uint8_t resultType, int32_t regval, float adjust) +{ + float val = 0.0; + + if (regval >= 0x800000) //raw reg value at low distance can goto 0x800000 which is negative, set distance to zero if this happens + { + regval = 0; + } + + // Conversion from LSB to distance. For explanation on the formula, refer to Datasheet of SX1280 + + switch (resultType) + { + case RANGING_RESULT_RAW: + // Convert the ranging LSB to distance in meter. The theoretical conversion from register value to distance [m] is given by: + // distance [m] = ( complement2( register ) * 150 ) / ( 2^12 * bandwidth[MHz] ) ). The API provide BW in [Hz] so the implemented + // formula is complement2( register ) / bandwidth[Hz] * A, where A = 150 / (2^12 / 1e6) = 36621.09 + val = ( double ) regval / ( double ) returnBandwidth(savedModParam2) * 36621.09375; + break; + + case RANGING_RESULT_AVERAGED: + case RANGING_RESULT_DEBIASED: + case RANGING_RESULT_FILTERED: + Serial.print(F("??")); + val = ( double )regval * 20.0 / 100.0; + break; + default: + val = 0.0; + break; + } + + val = val * adjust; + return val; +} + + +bool SX128XLT::setupRanging(uint32_t frequency, int32_t offset, uint8_t modParam1, uint8_t modParam2, uint8_t modParam3, uint32_t address, uint8_t role) +{ + //sequence is frequency, offset, spreading factor, bandwidth, coding rate, calibration, role. +#ifdef SX128XDEBUG + Serial.println(F("setupRanging()")); +#endif + + setMode(MODE_STDBY_RC); + setPacketType(PACKET_TYPE_RANGING); + setModulationParams(modParam1, modParam2, modParam3); + setPacketParams(12, LORA_PACKET_VARIABLE_LENGTH, 0, LORA_CRC_ON, LORA_IQ_NORMAL, 0, 0); + setRfFrequency(frequency, offset); + setRangingSlaveAddress(address); + setRangingMasterAddress(address); + setRangingCalibration(lookupCalibrationValue(modParam1, modParam2)); + setRangingRole(role); + setHighSensitivity(); + return true; +} + + +bool SX128XLT::transmitRanging(uint32_t address, uint16_t timeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("transmitRanging()")); +#endif + + if ((_RXEN >= 0) || (_TXEN >= 0)) + { + return false; + } + + setMode(MODE_STDBY_RC); + setRangingMasterAddress(address); + setTxParams(txpower, RADIO_RAMP_02_US); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RANGING_MASTER_RESULT_VALID + IRQ_RANGING_MASTER_RESULT_TIMEOUT), 0, 0); + setTx(timeout); //this sends the ranging packet + + if (!wait) + { + return true; + } + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RANGING_MASTER_RESULT_VALID ) //check for timeout + { + return true; + } + else + { + return false; + } +} + + +uint8_t SX128XLT::receiveRanging(uint32_t address, uint16_t timeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("receiveRanging()")); +#endif + + setTxParams(txpower, RADIO_RAMP_02_US); + setRangingSlaveAddress(address); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RANGING_SLAVE_RESPONSE_DONE + IRQ_RANGING_SLAVE_REQUEST_DISCARDED + IRQ_HEADER_ERROR), 0, 0); + setRx(timeout); + + if (!wait) + { + return NO_WAIT; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if (readIrqStatus() & IRQ_RANGING_SLAVE_REQUEST_VALID) + { + return true; + } + else + { + return false; //so we can check for packet having enough buffer space + } +} + + +uint8_t SX128XLT::receiveRanging(uint32_t address, uint8_t bits, uint16_t timeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUG + Serial.println(F("receiveRanging()")); +#endif + + setTxParams(txpower, RADIO_RAMP_02_US); + setRangingSlaveAddress(address, bits); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RANGING_SLAVE_RESPONSE_DONE + IRQ_RANGING_SLAVE_REQUEST_DISCARDED + IRQ_HEADER_ERROR), 0, 0); + setRx(timeout); + + if (!wait) + { + return NO_WAIT; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if (readIrqStatus() & IRQ_RANGING_SLAVE_REQUEST_VALID) + { + return true; + } + else + { + return false; //so we can check for packet having enough buffer space + } +} + + + + +uint16_t SX128XLT::lookupCalibrationValue(uint8_t spreadingfactor, uint8_t bandwidth) +{ + //this looks up the calibration value from the table in SX128XLT_Definitions.hifdef SX128XDEBUG +#ifdef SX128XDEBUG + Serial.println(F("lookupCalibrationValue()")); +#endif + + switch (bandwidth) + { + case LORA_BW_0400: + savedCalibration = RNG_CALIB_0400[(spreadingfactor >> 4) - 5]; + return savedCalibration; + + case LORA_BW_0800: + savedCalibration = RNG_CALIB_0800[(spreadingfactor >> 4) - 5]; + return savedCalibration; + + + case LORA_BW_1600: + savedCalibration = RNG_CALIB_1600[(spreadingfactor >> 4) - 5]; + return savedCalibration; + + default: + return 0xFFFF; + } +} + + +uint16_t SX128XLT::getSetCalibrationValue() +{ +#ifdef SX128XDEBUG + Serial.println(F("getCalibrationValue()")); +#endif + + return savedCalibration; +} + + +int16_t SX128XLT::getRangingRSSI() +{ + //Added November 2021 - see datasheet SX1280-1_V3.2, 14.5.3 Ranging RSSI + int16_t regdata; + regdata = readRegister(REG_RANGING_RSSI); + regdata = regdata - 150; + return regdata; +} + +//********************************************************************************************** +// Reliable packet routines - added November 2021 +// Routines assume that RX and TX buffer base addresses are set to 0 by setupLoRa(), setupFLRC() +//********************************************************************************************** + +void SX128XLT::printASCIIArray(uint8_t *buffer, uint8_t size) +{ +#ifdef SX128XDEBUG1 + Serial.println(F("printASCIIArray() ")); +#endif + + uint8_t index; + + for (index = 0; index < size; index++) + { + Serial.write(buffer[index]); + } +} + + +uint8_t SX128XLT::getReliableConfig(uint8_t bitread) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} getReliableConfig() ")); + Serial.println(_ReliableConfig); +#endif + + return bitRead(_ReliableConfig, bitread); +} + + +uint8_t SX128XLT::transmitReliable(uint8_t *txbuffer, uint8_t size, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} transmitRELIABLE() ")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); + Serial.print(F(" {RELIABLE} Payload length ")); + Serial.println(size); +#endif + + uint8_t index, tempdata; + uint16_t payloadcrc; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + if (size > 251) + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + setMode(MODE_STDBY_RC); + _TXPacketL = size + 4; + + if (bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = 0; + } + else + { + payloadcrc = CRCCCITT(txbuffer, size, 0xFFFF); + //payloadcrc = CRCCCITT(txbuffer, size, 0xFFFF) + 1; + } + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + for (index = 0; index < size; index++) + { + tempdata = txbuffer[index]; + SPI.transfer(tempdata); + } + + SPI.transfer(lowByte(networkID)); + SPI.transfer(highByte(networkID)); + SPI.transfer(lowByte(payloadcrc)); + SPI.transfer(highByte(payloadcrc)); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + setPayloadLength(_TXPacketL); + + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); + + if (!wait) + { + return _TXPacketL; + } + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + + return _TXPacketL; +} + + +uint16_t SX128XLT::getTXPayloadCRC(uint8_t length) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} getTXPayloadCRC() ")); +#endif + + return readUint16SXBuffer(length - 2); +} + + +uint16_t SX128XLT::getRXPayloadCRC(uint8_t length) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} getRXPayloadCRC() ")); +#endif + + return readUint16SXBuffer(length - 2); +} + + +uint16_t SX128XLT::getRXNetworkID(uint8_t length) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} getRXnetworkID() ")); +#endif + + return readUint16SXBuffer(length - 4); +} + + +uint16_t SX128XLT::getTXNetworkID(uint8_t length) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} getTXnetworkID() ")); +#endif + + return readUint16SXBuffer(length - 4); +} + + +void SX128XLT::printReliableStatus() +{ +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} printReliableStatus() ")); +#endif + + //0x00 + if (bitRead(_ReliableErrors, ReliableCRCError)) + { + Serial.print(F(",ReliableCRCError")); + } + + //0x01 + if (bitRead(_ReliableErrors, ReliableIDError)) + { + Serial.print(F(",ReliableIDError")); + } + + //0x02 + if (bitRead(_ReliableErrors, ReliableSizeError)) + { + Serial.print(F(",ReliableSizeError")); + } + + //0x03 + if (bitRead(_ReliableErrors, ReliableACKError)) + { + Serial.print(F(",NoReliableACK")); + } + + //0x04 + if (bitRead(_ReliableErrors, ReliableTimeout)) + { + Serial.print(F(",ReliableTimeout")); + } + + //0x00 + if (bitRead(_ReliableFlags, ReliableACKSent)) + { + Serial.print(F(",ACKsent")); + } + + //0x01 + if (bitRead(_ReliableFlags, ReliableACKReceived)) + { + Serial.print(F(",ACKreceived")); + } + +} + + +void SX128XLT::writeUint16SXBuffer(uint8_t addr, uint16_t regdata) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} writeUint16SXBuffer() 0x")); + Serial.print(addr, HEX); + Serial.print(F(" 0x")); + Serial.println(regdata, HEX); +#endif + + setMode(MODE_STDBY_RC); //this is needed to ensure we can write to buffer OK. + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(addr); + SPI.transfer(lowByte(regdata)); + SPI.transfer(highByte(regdata)); + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif +} + + +uint16_t SX128XLT::readUint16SXBuffer(uint8_t addr) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} readUint16SXBuffer() 0x")); + Serial.println(addr, HEX); +#endif + + uint8_t regdatalow, regdatahigh; + setMode(MODE_STDBY_RC); //this is needed to ensure we can read from buffer OK. + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(addr); + SPI.transfer(0xFF); + regdatalow = SPI.transfer(0); + regdatahigh = SPI.transfer(0); + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return (regdatalow + (regdatahigh << 8)); +} + +//****************************************************************************************** +// Reliable packet routines - added November 2021 +//****************************************************************************************** + + +uint8_t SX128XLT::receiveReliable(uint8_t *rxbuffer, uint8_t size, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ) +{ + //Maximum total packet size is 255 bytes, so allowing for the 4 bytes appended to the end of a reliable + //packet, the maximum payload size for LORa is 251 bytes and 117 bytes for FLRC. So to avoid overwriting + //memory, we do need to check if the passed array is big enough to take the payload received in the packet. + //The assumed payload length will always be 4 bytes less than the received packet length. + +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} receiveReliable()")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t payloadcrc = 0, RXcrc, RXnetworkID = 0; + uint8_t regdataL, regdataH; + uint8_t index; + uint8_t buffer[2]; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + if (size > 251 ) + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + setMode(MODE_STDBY_RC); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); //set for IRQ on RX done or timeout + setRx(rxtimeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); //Wait for DIO1 to go high + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + return 0; //packet is errored somewhere so return 0 + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + + if (_RXPacketL < 4) //check received packet is 4 or more bytes long + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + if ((_RXPacketL - 4) > size ) //check if calculated payload size (_RXPacketL -4) fits in array + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(0); + SPI.transfer(0xFF); + + for (index = 0; index < (_RXPacketL - 4); index++) + { + regdataL = SPI.transfer(0); + rxbuffer[index] = regdataL; + } + + regdataL = SPI.transfer(0); + regdataH = SPI.transfer(0); + RXnetworkID = ((uint16_t) regdataH << 8) + regdataL; + regdataL = SPI.transfer(0); + regdataH = SPI.transfer(0); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = CRCCCITT(rxbuffer, (_RXPacketL - 4), 0xFFFF); + RXcrc = ((uint16_t) regdataH << 8) + regdataL; + + if (payloadcrc != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); + } + } + + if (RXnetworkID != networkID) + { + bitSet(_ReliableErrors, ReliableIDError); + } + + if (_ReliableErrors) //if there has been a reliable error return a RX fail + { + return 0; + } + + return _RXPacketL; //return and indicate RX OK. +} + + +void SX128XLT::setReliableRX(uint16_t timeout) +{ + //existing setRx() does not setup LoRa device as a receiver completly, just turns on receiver mode + //this routine does all the required setup for receive mode +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} setReliableRX()")); +#endif + + setMode(MODE_STDBY_RC); //stops receiver + clearIrqStatus(IRQ_RADIO_ALL); //clear current interrupt flags + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); + setRx(timeout); +} + + +uint8_t SX128XLT::transmitReliableAutoACK(uint8_t *txbuffer, uint8_t size, uint16_t networkID, uint32_t acktimeout, uint32_t txtimeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} transmitReliableAutoACK() ")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); + Serial.print(F(" {RELIABLE} Payload length ")); + Serial.println(size); +#endif + + uint8_t index, tempdata, RXPacketL; + uint16_t payloadcrc; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + if (size > 251) + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + setMode(MODE_STDBY_RC); + checkBusy(); + _TXPacketL = size + 4; + + if (bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = 0; + } + else + { + payloadcrc = CRCCCITT(txbuffer, size, 0xFFFF); + //payloadcrc = CRCCCITT(txbuffer, size, 0xFFFF) + 1; + } + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + for (index = 0; index < size; index++) + { + tempdata = txbuffer[index]; + SPI.transfer(tempdata); + } + + SPI.transfer(lowByte(networkID)); + SPI.transfer(highByte(networkID)); + SPI.transfer(lowByte(payloadcrc)); + SPI.transfer(highByte(payloadcrc)); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); + + if (!wait) + { + return _TXPacketL; + } + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + + RXPacketL = waitReliableACK(networkID, payloadcrc, acktimeout); + + if (RXPacketL != 4) + { + return 0; + } + + return _TXPacketL; +} + + +uint8_t SX128XLT::waitReliableACK(uint16_t networkID, uint16_t payloadcrc, uint32_t acktimeout) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} waitReliableACK()")); +#endif + + uint16_t RXnetworkID, RXcrc; + uint32_t startmS; + uint8_t buffer[2]; + + setReliableRX(0); + startmS = millis(); + + do + { + if (digitalRead(_RXDonePin)) //has a packet arrived ? + { + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + setReliableRX(0); + continue; + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + RXnetworkID = readUint16SXBuffer(_RXPacketL - 4); + RXcrc = readUint16SXBuffer(_RXPacketL - 2); + + if ( (RXnetworkID == networkID) && (RXcrc == payloadcrc)) + { + bitSet(_ReliableFlags, ReliableACKReceived); + return 4; //return value of 4 indicates valid ack + } + else + { + setReliableRX(0); + continue; + } + } + + } while ( ((uint32_t) (millis() - startmS) < acktimeout)); + + bitSet(_ReliableErrors, ReliableACKError); + return 0; +} + + +uint8_t SX128XLT::waitReliableACK(uint8_t *rxbuffer, uint8_t size, uint16_t networkID, uint16_t payloadcrc, uint32_t acktimeout) +{ + //overloaded version of waitReliableACK() for use when ack contains payload data +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} waitReliableACK()")); +#endif + + uint16_t RXnetworkID, RXcrc; + uint32_t startmS; + uint8_t buffer[2]; + uint8_t regdata, index; + + + if (size > 251 ) + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + setReliableRX(0); + startmS = millis(); + + do + { + if (digitalRead(_RXDonePin)) //has a packet arrived ? + { + regdata = readIrqStatus(); + + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + setReliableRX(0); + continue; + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + RXnetworkID = readUint16SXBuffer(_RXPacketL - 4); + RXcrc = readUint16SXBuffer(_RXPacketL - 2); + + if ( (RXnetworkID == networkID) && (RXcrc == payloadcrc)) + { + if ((_RXPacketL - 4) > size ) //check passed buffer is big enough for payload + { + bitSet(_ReliableErrors, ReliableACKError); + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + bitSet(_ReliableFlags, ReliableACKReceived); + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(0); + SPI.transfer(0xFF); + + for (index = 0; index < (_RXPacketL - 4); index++) //read packet into rxbuffer + { + regdata = SPI.transfer(0); + rxbuffer[index] = regdata; + } + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _RXPacketL; //_RXPacketL should be payload length + 4 + } + else + { + setReliableRX(0); + continue; + } + } + } while ( ((uint32_t) (millis() - startmS) < acktimeout) ); + + bitSet(_ReliableErrors, ReliableACKError); + return 0; +} + + + +uint8_t SX128XLT::receiveReliableAutoACK(uint8_t *rxbuffer, uint8_t size, uint16_t networkID, uint32_t ackdelay, int8_t txpower, uint32_t rxtimeout, uint8_t wait ) +{ + //Maximum total packet size is 255 bytes, so allowing for the 4 bytes appended to the end of a reliable + //packet, the maximum payload size for LORa is 251 bytes and 117 bytes for FLRC. So to avoid overwriting + //memory, we do need to check if the passed array is big enough to take the payload received in the packet. + //The assumed payload length will always be 4 bytes less than the received packet length. + + +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} receiveReliableAutoACK()")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t payloadcrc = 0, RXcrc, RXnetworkID = 0; + uint8_t regdataL, regdataH, index; + uint8_t buffer[2]; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + if (size > 251 ) + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + setMode(MODE_STDBY_RC); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); //set for IRQ on RX done or timeout + setRx(rxtimeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); //Wait for DIO1 to go high + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + return 0; //packet is errored somewhere so return 0 + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + if (_RXPacketL < 4) //check received packet is 4 or more bytes long + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + if ((_RXPacketL - 4) > size ) //check if calculated payload size (_RXPacketL -4) fits in array + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(0); + SPI.transfer(0xFF); + + for (index = 0; index < (_RXPacketL - 4); index++) + { + regdataL = SPI.transfer(0); + rxbuffer[index] = regdataL; + } + + regdataL = SPI.transfer(0); + regdataH = SPI.transfer(0); + RXnetworkID = ((uint16_t) regdataH << 8) + regdataL; + regdataL = SPI.transfer(0); + regdataH = SPI.transfer(0); + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = CRCCCITT(rxbuffer, (_RXPacketL - 4), 0xFFFF); + //payloadcrc = CRCCCITT(rxbuffer, (_RXPacketL - 4), 0xFFFF) + 1; + RXcrc = ((uint16_t) regdataH << 8) + regdataL; + + if (payloadcrc != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); + } + } + + if (RXnetworkID != networkID) + { + bitSet(_ReliableErrors, ReliableIDError); + } + + if (_ReliableErrors) //if there has been a reliable error return a RX fail + { + return 0; + } + + delay(ackdelay); + _TXPacketL = sendReliableACK(RXnetworkID, payloadcrc, txpower); + + if (_TXPacketL != 4) + { + return 0; + } + + return _RXPacketL; //return and indicate RX OK. +} + + +uint8_t SX128XLT::sendReliableACK(uint16_t networkID, uint16_t payloadcrc, int8_t txpower) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} sendReliableACK()")); +#endif + + uint32_t txtimeout = 12000; //set TX timeout to 12 seconds, longest packet is 8.7secs + _TXPacketL = 4; //packet is networkId (2 bytes) + payloadCRC (2 bytes) + setMode(MODE_STDBY_RC); + + //payloadcrc++; + + writeUint16SXBuffer(0, networkID); + writeUint16SXBuffer(2, payloadcrc); + + checkBusy(); + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); + + while (!digitalRead(_TXDonePin)); + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + + bitSet(_ReliableFlags, ReliableACKSent); + return 4; //TX OK so return TXpacket length +} + + +uint8_t SX128XLT::sendReliableACK(uint8_t *txbuffer, uint8_t size, uint16_t networkID, uint16_t payloadcrc, int8_t txpower) +{ + //overloaded version of sendReliableACK() for use when ack contains payload data +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} sendReliableACK() ")); + Serial.print(F("buffer size ")); + Serial.println(size); +#endif + + uint32_t txtimeout = 12000; //set TX timeout to 12 seconds, longest packet is 8.7secs + uint8_t bufferdata, index; + + //payloadcrc++; + + setMode(MODE_STDBY_RC); + _TXPacketL = size + 4; + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + for (index = 0; index < size; index++) + { + bufferdata = txbuffer[index]; + SPI.transfer(bufferdata); + } + + SPI.transfer(lowByte(networkID)); + SPI.transfer(highByte(networkID)); + SPI.transfer(lowByte(payloadcrc)); + SPI.transfer(highByte(payloadcrc)); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); //this starts the TX + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } + + bitSet(_ReliableFlags, ReliableACKSent); + return _TXPacketL; //TX OK so return TXpacket length +} + + +uint16_t SX128XLT::CRCCCITTReliable(uint8_t startadd, uint8_t endadd, uint16_t startvalue) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} CRCCCITTReliable()")); +#endif + + //generates a CRC of bytes from the internal SX buffer, _RXPackletL and _TXPackletL are not affected + +#ifdef SX128DEBUGRELIABLE + Serial.println(F(" {RELIABLE} CRCCCITTReliable() ")); +#endif + + uint16_t index, libraryCRC; + uint8_t j, readSX; + + libraryCRC = startvalue; //start value for CRC16 + setMode(MODE_STDBY_RC); + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(startadd); + SPI.transfer(0xFF); + + for (index = startadd; index <= endadd; index++) + { + readSX = SPI.transfer(0); + libraryCRC ^= (((uint16_t) readSX ) << 8); + for (j = 0; j < 8; j++) + { + if (libraryCRC & 0x8000) + libraryCRC = (libraryCRC << 1) ^ 0x1021; + else + libraryCRC <<= 1; + } + } + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return libraryCRC; +} + + +void SX128XLT::setReliableConfig(uint8_t bitset) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} setReliableConfig() bit ")); + Serial.println(bitset); +#endif + + bitSet(_ReliableConfig, bitset); + return; +} + + +void SX128XLT::clearReliableConfig(uint8_t bitset) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} clearReliableConfig() bit ")); + Serial.println(bitset); +#endif + + bitClear(_ReliableConfig, bitset); + return; +} + + + +uint8_t SX128XLT::readReliableErrors() +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} readReliableErrors()")); +#endif + + return _ReliableErrors; +} + +uint8_t SX128XLT::readReliableFlags() +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} readReliableFlags()")); +#endif + + return _ReliableFlags; +} + + +//****************************************************************************************** +// Reliable SX packet routines - added November 2021 +// SX Packet routines directly read\write data\bvariables to the LoRa device internal buffer +//****************************************************************************************** + + +uint8_t SX128XLT::transmitSXReliable(uint8_t startaddr, uint8_t length, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} transmitSXReliable() ")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t payloadcrc; + + setMode(MODE_STDBY_RC); + checkBusy(); + _ReliableErrors = 0; + _ReliableFlags = 0; + + if (startaddr + length > 251) + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + _TXPacketL = length + 4; + + if (bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = 0; + } + else + { + payloadcrc = CRCCCITTReliable(startaddr, startaddr + length - 1, 0xFFFF); + //payloadcrc = CRCCCITTReliable(startaddr, startaddr + length - 1, 0xFFFF) + 1; + } + + writeUint16SXBuffer(startaddr + _TXPacketL - 4, networkID); + writeUint16SXBuffer(startaddr + _TXPacketL - 2, payloadcrc); + + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); //this starts the TX + + if (!wait) + { + return _TXPacketL; + } + + while (!digitalRead(_TXDonePin)); //Wait for pin to go high, TX finished + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + + return _TXPacketL; +} + + +uint8_t SX128XLT::transmitSXReliableIRQ(uint8_t startaddr, uint8_t length, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} transmitSXReliableIRQ() ")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t payloadcrc; + + setMode(MODE_STDBY_RC); + checkBusy(); + _ReliableErrors = 0; + _ReliableFlags = 0; + + if (startaddr + length > 251) + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + _TXPacketL = length + 4; + + if (bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = 0; + } + else + { + payloadcrc = CRCCCITTReliable(startaddr, startaddr + length - 1, 0xFFFF); + //payloadcrc = CRCCCITTReliable(startaddr, startaddr + length - 1, 0xFFFF) + 1; + } + + writeUint16SXBuffer(startaddr + _TXPacketL - 4, networkID); + writeUint16SXBuffer(startaddr + _TXPacketL - 2, payloadcrc); + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setTx(txtimeout); //this starts the TX + + if (!wait) + { + return _TXPacketL; + } + + //0x4001 = IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT + while (!(readIrqStatus() & 0x4001 )); //wait for IRQs going active + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + + return _TXPacketL; +} + + +uint8_t SX128XLT::receiveSXReliable(uint8_t startaddr, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} receiveSXReliable()")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t payloadcrc = 0, RXcrc, RXnetworkID = 0; + uint8_t buffer[2]; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + setMode(MODE_STDBY_RC); + checkBusy(); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); //set for IRQ on RX done or timeout + setRx(rxtimeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); //Wait for DIO1 to go high, no timeout, RX DONE + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + return 0; //no RX done and header valid only, could be CRC error + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + if (_RXPacketL < 4) //check received packet is 4 or more bytes long + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + RXnetworkID = readUint16SXBuffer(startaddr + _RXPacketL - 4); + + if (RXnetworkID != networkID) + { + bitSet(_ReliableErrors, ReliableIDError); + } + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = CRCCCITTReliable(startaddr, (startaddr + _RXPacketL - 5), 0xFFFF); + //payloadcrc = CRCCCITTReliable(startaddr, (startaddr + _RXPacketL - 5), 0xFFFF) + 1; + RXcrc = readUint16SXBuffer(startaddr + _RXPacketL - 2); + + if (payloadcrc != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); + } + } + + if (_ReliableErrors) //if there has been a reliable error return a RX fail + { + return 0; + } + return _RXPacketL; //return and RX OK. +} + + +uint8_t SX128XLT::receiveSXReliableIRQ(uint8_t startaddr, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} receiveSXReliable()")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t payloadcrc = 0, RXcrc, RXnetworkID = 0; + uint8_t buffer[2]; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + setMode(MODE_STDBY_RC); + checkBusy(); + setRx(rxtimeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + //0x4022 = IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR + while (!(readIrqStatus() & 0x4022 )); //wait for IRQs going active + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + return 0; //no RX done and header valid only, could be CRC error + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + if (_RXPacketL < 4) //check received packet is 4 or more bytes long + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + RXnetworkID = readUint16SXBuffer(startaddr + _RXPacketL - 4); + + if (RXnetworkID != networkID) + { + bitSet(_ReliableErrors, ReliableIDError); + } + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = CRCCCITTReliable(startaddr, (startaddr + _RXPacketL - 5), 0xFFFF); + //payloadcrc = CRCCCITTReliable(startaddr, (startaddr + _RXPacketL - 5), 0xFFFF) + 1; + RXcrc = readUint16SXBuffer(startaddr + _RXPacketL - 2); + + if (payloadcrc != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); + } + } + + if (_ReliableErrors) //if there has been a reliable error return a RX fail + { + return 0; + } + + return _RXPacketL; //return and RX OK. +} + + +uint8_t SX128XLT::transmitSXReliableAutoACK(uint8_t startaddr, uint8_t length, uint16_t networkID, uint32_t acktimeout, uint32_t txtimeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} transmitSXReliableAutoACK() ")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint8_t RXPacketL; + uint16_t payloadcrc; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + if (startaddr + length > 251) + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + setMode(MODE_STDBY_RC); + checkBusy(); + _TXPacketL = length + 4; + + if (bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = 0; + } + else + { + payloadcrc = CRCCCITTReliable(startaddr, startaddr + length - 1, 0xFFFF); + //payloadcrc = CRCCCITTReliable(startaddr, startaddr + length - 1, 0xFFFF)+1; + } + + writeUint16SXBuffer(startaddr + _TXPacketL - 4, networkID); + writeUint16SXBuffer(startaddr + _TXPacketL - 2, payloadcrc); + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); + + if (!wait) + { + return _TXPacketL; + } + + if (!wait) + { + return _TXPacketL; + } + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + + RXPacketL = waitReliableACK(networkID, payloadcrc, acktimeout); + + if (RXPacketL != 4) + { + + return 0; + } + + return _TXPacketL; +} + + +uint8_t SX128XLT::receiveSXReliableAutoACK(uint8_t startaddr, uint16_t networkID, uint32_t ackdelay, int8_t txpower, uint32_t rxtimeout, uint8_t wait ) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} receiveSXReliableAutoACK()")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t payloadcrc = 0, RXcrc, RXnetworkID = 0; + uint16_t temp1, temp2; + uint8_t buffer[2]; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + setMode(MODE_STDBY_RC); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); //set for IRQ on RX done or timeout + setRx(rxtimeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); //Wait for DIO1 to go high + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + return 0; //packet is errored somewhere so return 0 + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + + if (_RXPacketL < 4) //check received packet is 4 or more bytes long + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + RXnetworkID = readUint16SXBuffer(startaddr + _RXPacketL - 4); + + if (RXnetworkID != networkID) + { + bitSet(_ReliableErrors, ReliableIDError); + } + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = CRCCCITTReliable(startaddr, (startaddr + _RXPacketL - 5), 0xFFFF); + //payloadcrc = CRCCCITTReliable(startaddr, (startaddr + _RXPacketL - 5), 0xFFFF)+1; + RXcrc = readUint16SXBuffer(startaddr + _RXPacketL - 2); + + if (payloadcrc != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); + } + } + + if (_ReliableErrors) //if there has been a reliable error return a RX fail + { + return 0; + } + + delay(ackdelay); + temp1 = readUint16SXBuffer(startaddr); //save bytes that would be overwritten by ack + temp2 = readUint16SXBuffer(startaddr + 2); //save bytes that would be overwritten by ack + _TXPacketL = sendReliableACK(RXnetworkID, payloadcrc, txpower); + writeUint16SXBuffer(startaddr, temp1); //restore bytes that would be overwritten by ack + writeUint16SXBuffer(startaddr + 2, temp2); //restore bytes that would be overwritten by ack + + if (_TXPacketL != 4) + { + bitSet(_ReliableErrors, ReliableACKError); + return 0; + } + + return _RXPacketL; //return indicating RX ack sent OK. +} + + +uint8_t SX128XLT::waitSXReliableACK(uint8_t startaddr, uint16_t networkID, uint16_t payloadcrc, uint32_t acktimeout) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} waitSXReliableACK()")); +#endif + + uint16_t RXnetworkID, RXcrc; + uint32_t startmS; + uint8_t buffer[2]; + + setReliableRX(0); + + startmS = millis(); + + do + { + if (digitalRead(_RXDonePin)) //has a packet arrived ? + { + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + setReliableRX(0); + continue; + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + RXnetworkID = readUint16SXBuffer(startaddr + _RXPacketL - 4); + RXcrc = readUint16SXBuffer(startaddr + _RXPacketL - 2); + + if ( (RXnetworkID == networkID) && (RXcrc == payloadcrc)) + { + bitSet(_ReliableFlags, ReliableACKReceived); + return _RXPacketL; //_RXPacketL should be payload length + 4 + } + else + { + setReliableRX(0); + continue; + } + } + } while ( ((uint32_t) (millis() - startmS) < acktimeout) ); + + bitSet(_ReliableErrors, ReliableACKError); + return 0; +} + + +uint8_t SX128XLT::waitSXReliableACKIRQ(uint8_t startaddr, uint16_t networkID, uint16_t payloadcrc, uint32_t acktimeout) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} waitSXReliableACKIRQ()")); +#endif + + uint16_t RXnetworkID, RXcrc; + uint32_t startmS; + uint8_t buffer[2]; + + setReliableRX(0); + + startmS = millis(); + + do + { + //0x4022 = IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR + if (readIrqStatus() & 0x4022 ) + { + if ( readIrqStatus() & (IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR)) + { + setReliableRX(0); + continue; + } + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + RXnetworkID = readUint16SXBuffer(startaddr + _RXPacketL - 4); + RXcrc = readUint16SXBuffer(startaddr + _RXPacketL - 2); + + if ( (RXnetworkID == networkID) && (RXcrc == payloadcrc)) + { + bitSet(_ReliableFlags, ReliableACKReceived); + return _RXPacketL; //_RXPacketL should be payload length + 4 + } + else + { + setReliableRX(0); + continue; + } + } + } while ( ((uint32_t) (millis() - startmS) < acktimeout) ); + + bitSet(_ReliableErrors, ReliableACKError); + return 0; +} + + +uint8_t SX128XLT::sendSXReliableACK(uint8_t startaddr, uint8_t length, uint16_t networkID, uint16_t payloadcrc, int8_t txpower) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} sendSXReliableACK() ")); +#endif + + uint32_t txtimeout = 12000; //set TX timeout to 12 seconds, longest packet is 8.7secs + + _TXPacketL = length + 4; //packet is networkId (2 bytes) + payloadCRC (2 bytes) + setMode(MODE_STDBY_RC); + + writeUint16SXBuffer((length + startaddr), networkID); + writeUint16SXBuffer((length + startaddr + 2), payloadcrc); + checkBusy(); + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); //start transmission + + while (!digitalRead(_TXDonePin)); + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + + bitSet(_ReliableFlags, ReliableACKSent); + return _TXPacketL; //TX OK so return TXpacket length +} + + +uint8_t SX128XLT::sendSXReliableACKIRQ(uint8_t startaddr, uint8_t length, uint16_t networkID, uint16_t payloadcrc, int8_t txpower) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} sendSXReliableACK() ")); +#endif + + uint32_t txtimeout = 12000; //set TX timeout to 12 seconds, longest packet is 8.7secs + + _TXPacketL = length + 4; //packet is networkId (2 bytes) + payloadCRC (2 bytes) + setMode(MODE_STDBY_RC); + + writeUint16SXBuffer((length + startaddr), networkID); + writeUint16SXBuffer((length + startaddr + 2), payloadcrc); + + checkBusy(); + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setTx(txtimeout); //start transmission + + //0x4001 = IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT + while (!(readIrqStatus() & 0x4001 )); //wait for IRQs going active + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + + bitSet(_ReliableFlags, ReliableACKSent); + return _TXPacketL; //TX OK so return TXpacket length +} + +//*********************************************************************************** +//Data Transfer functions - Added November 2021 +//TX and RX base addresses assumed to be 0 +//*********************************************************************************** + +uint8_t SX128XLT::transmitDT(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t datasize, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} transmitDT() ")); +#endif + + uint8_t index, bufferdata; + uint16_t payloadcrc; + + _ReliableErrors = 0; + _ReliableFlags = 0; + +#ifdef DETECTRELIABLERRORS + if (datasize > (251 - headersize)) //its 251 because of 4 bytes appended to packet + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } +#endif + setMode(MODE_STDBY_RC); + _TXPacketL = headersize + datasize + 4; + + if (bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = 0; + } + else + { + payloadcrc = CRCCCITT(dataarray, datasize, 0xFFFF); + } + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + //load up the header + for (index = 0; index < headersize; index++) + { + bufferdata = header[index]; + SPI.transfer(bufferdata); + } + + //load up the data array + for (index = 0; index < datasize; index++) + { + bufferdata = dataarray[index]; + SPI.transfer(bufferdata); + } + + //append the network ID and payload CRC at end + SPI.transfer(lowByte(networkID)); + SPI.transfer(highByte(networkID)); + SPI.transfer(lowByte(payloadcrc)); + SPI.transfer(highByte(payloadcrc)); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); + + if (!wait) + { + return _TXPacketL; + } + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } +} + + +uint8_t SX128XLT::waitACKDT(uint8_t *header, uint8_t headersize, uint32_t acktimeout) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} waitACKDT()")); +#endif + + uint16_t RXnetworkID, RXcrc; + uint32_t startmS; + uint8_t regdata, index; + uint16_t networkID; + uint16_t payloadCRC; + uint8_t buffer[2]; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + networkID = readUint16SXBuffer(_TXPacketL - 4); //get networkID used to transmit previous packet, before next RX + payloadCRC = readUint16SXBuffer(_TXPacketL - 2); //get payloadCRC used to transmit previous packet, before next RX + + setReliableRX(0); + startmS = millis(); //setReliableRX has a timeount, but here we want an overall timeout waiting for ACK + + do + { + if (digitalRead(_RXDonePin)) //has a packet arrived ? + { + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + //IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR = 0x4068 + if (readIrqStatus() & 0x4068) + { + setReliableRX(0); + continue; + } + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + RXnetworkID = readUint16SXBuffer(_RXPacketL - 4); + RXcrc = readUint16SXBuffer(_RXPacketL - 2); + + if ((_RXPacketL - 4) > headersize ) //check passed buffer is big enough for header + { + setReliableRX(0); + continue; + } + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + if (payloadCRC != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); + setReliableRX(0); + continue; + } + } + + if ( (RXnetworkID == networkID)) + { + bitSet(_ReliableFlags, ReliableACKReceived); + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(0); + SPI.transfer(0xFF); + + for (index = 0; index < (_RXPacketL - 4); index++) //read packet into rxbuffer + { + regdata = SPI.transfer(0); + header[index] = regdata; + } + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _RXPacketL; //_RXPacketL should be payload length + 4 + } + else + { + setReliableRX(0); + continue; + } + } + } while ( ((uint32_t) (millis() - startmS) < acktimeout)); + + bitSet(_ReliableErrors, ReliableACKError); + bitSet(_ReliableErrors, ReliableTimeout); + + return 0; +} + + +uint8_t SX128XLT::receiveDT(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t datasize, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} receiveDT()")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t index, payloadcrc = 0, RXcrc, RXnetworkID = 0; + uint8_t regdataL, regdataH; + uint8_t RXHeaderL; + uint8_t RXDataL; + uint8_t buffer[2]; + uint8_t RXstart; + + _ReliableErrors = 0; + _ReliableFlags = 0; + setMode(MODE_STDBY_RC); + setPayloadLength(127); //set for maximum packet length in FLRC mode, packets might be filtered otherwise + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); + setRx(rxtimeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + while (!digitalRead(_RXDonePin)); //Wait for DIO1 to go high, no timeout, RX DONE + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + //IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR = 0x4068 + if (readIrqStatus() & 0x4068) + { +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} Packet error")); +#endif + return 0; //packet is errored somewhere so return 0 + } + + RXHeaderL = getByteSXBuffer(2); + RXDataL = getByteSXBuffer(3); + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + RXstart = buffer[1]; + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} Header ")); + printSXBufferHEX(0, (RXHeaderL - 1)); + Serial.println(); + Serial.print(F(" {RELIABLE} Received data payload size ")); + Serial.println(RXDataL); + Serial.print(F(" {RELIABLE} Data payload ")); + printSXBufferHEX(RXHeaderL, RXHeaderL + RXDataL - 1); + Serial.println(); +#endif + + if (RXHeaderL > headersize ) + { +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} Header size error ")); + Serial.println(headersize); +#endif + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + if (RXDataL > datasize ) + { +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} Data size error ")); + Serial.println(datasize); +#endif + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} _RXPacketL ")); + Serial.println(_RXPacketL); + Serial.print(F(" {RELIABLE} Header ")); + printSXBufferHEX(0, (RXHeaderL - 1)); + Serial.println(); + Serial.print(F(" {RELIABLE} Received data payload size ")); + Serial.println(RXDataL); + Serial.print(F(" {RELIABLE} Data payload ")); + printSXBufferHEX(RXHeaderL, RXHeaderL + RXDataL - 1); + Serial.println(); +#endif + + if (_RXPacketL < 10) //check received packet is 10 or more bytes long + { + //Serial.println(F(" {RELIABLE} _RXPacketL < 10")); + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(RXstart); + SPI.transfer(0xFF); + + for (index = 0; index < RXHeaderL; index++) + { + regdataL = SPI.transfer(0); + header[index] = regdataL; + } + + for (index = 0; index < RXDataL; index++) + { + regdataL = SPI.transfer(0); + dataarray[index] = regdataL; + } + + regdataL = SPI.transfer(0); + regdataH = SPI.transfer(0); + RXnetworkID = ((uint16_t) regdataH << 8) + regdataL; + regdataL = SPI.transfer(0); + regdataH = SPI.transfer(0); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = CRCCCITT(dataarray, RXDataL, 0xFFFF); + RXcrc = ((uint16_t) regdataH << 8) + regdataL; + +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} Payload CRC check enabled")); + Serial.print(F(" {RELIABLE} payloadcrc 0x")); + Serial.println(payloadcrc, HEX); + Serial.print(F(" {RELIABLE} RXcrc 0x")); + Serial.println(RXcrc, HEX); +#endif + + if (payloadcrc != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} CRCmissmatch, received 0x")); + Serial.println(RXcrc, HEX); +#endif + } + } + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} RXnetworkID 0x")); + Serial.println(RXnetworkID, HEX); +#endif + + if (RXnetworkID != networkID) + { +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} NetworkID missmatch received 0x")); + Serial.print(RXnetworkID, HEX); + Serial.print(F(" LocalID 0x")); + Serial.println(networkID, HEX); +#endif + bitSet(_ReliableErrors, ReliableIDError); + } + + if (_ReliableErrors) //if there has been a reliable error return a RX fail + { +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} Reliable errors")); +#endif + + return 0; + } + + return _RXPacketL; //return and indicate RX OK. +} + + +uint8_t SX128XLT::sendACKDT(uint8_t *header, uint8_t headersize, int8_t txpower) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} sendACKDT() ")); +#endif + + uint32_t txtimeout = 12000; //set TX timeout to 12 seconds, longest packet is 8.7secs + uint8_t bufferdata, index; + uint16_t networkID; + uint16_t payloadCRC; + + setMode(MODE_STDBY_RC); + _TXPacketL = headersize + 4; + networkID = readUint16SXBuffer(_RXPacketL - 4); + payloadCRC = readUint16SXBuffer(_RXPacketL - 2); + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + for (index = 0; index < headersize; index++) + { + bufferdata = header[index]; + SPI.transfer(bufferdata); + } + + SPI.transfer(lowByte(networkID)); + SPI.transfer(highByte(networkID)); + SPI.transfer(lowByte(payloadCRC)); + SPI.transfer(highByte(payloadCRC)); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); + + while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + bitSet(_ReliableFlags, ReliableTimeout); + return 0; + } + + bitSet(_ReliableFlags, ReliableACKSent); + return _TXPacketL; //TX OK so return TXpacket length +} + + +//*********************************************************************************** +//IRQ Data Transfer functions - Added November 2021 +//TX and RX base addresses assumed to be 0 +//*********************************************************************************** + + + +uint8_t SX128XLT::transmitDTIRQ(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t datasize, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} transmitDT() ")); +#endif + + uint8_t index, bufferdata; + uint16_t payloadcrc; + + _ReliableErrors = 0; + _ReliableFlags = 0; + +#ifdef DETECTRELIABLERRORS + if (datasize > (251 - headersize)) //its 251 because of 4 bytes appended to packet + { + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } +#endif + setMode(MODE_STDBY_RC); + _TXPacketL = headersize + datasize + 4; + + if (bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = 0; + } + else + { + payloadcrc = CRCCCITT(dataarray, datasize, 0xFFFF); + } + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + //load up the header + for (index = 0; index < headersize; index++) + { + bufferdata = header[index]; + SPI.transfer(bufferdata); + } + + //load up the data array + for (index = 0; index < datasize; index++) + { + bufferdata = dataarray[index]; + SPI.transfer(bufferdata); + } + + //append the network ID and payload CRC at end + SPI.transfer(lowByte(networkID)); + SPI.transfer(highByte(networkID)); + SPI.transfer(lowByte(payloadcrc)); + SPI.transfer(highByte(payloadcrc)); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); + + if (!wait) + { + return _TXPacketL; + } + + //while (!digitalRead(_TXDonePin)); //Wait for DIO1 to go high + + //0x4001 = IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT + while (!(readIrqStatus() & 0x4001 )); //wait for IRQs going active + + + setMode(MODE_STDBY_RC); //ensure we leave function with TX off + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + return 0; + } + else + { + return _TXPacketL; + } +} + + +uint8_t SX128XLT::waitACKDTIRQ(uint8_t *header, uint8_t headersize, uint32_t acktimeout) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} waitACKDT()")); +#endif + + uint16_t RXnetworkID, RXcrc; + uint32_t startmS; + uint8_t regdata, index; + uint16_t networkID; + uint16_t payloadCRC; + uint8_t buffer[2]; + + _ReliableErrors = 0; + _ReliableFlags = 0; + + networkID = readUint16SXBuffer(_TXPacketL - 4); //get networkID used to transmit previous packet, before next RX + payloadCRC = readUint16SXBuffer(_TXPacketL - 2); //get payloadCRC used to transmit previous packet, before next RX + + setReliableRX(0); + startmS = millis(); //setReliableRX has a timeount, but here we want an overall timeout waiting for ACK + + do + { + //0x4022 = IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR + if (readIrqStatus() & 0x4022 ) + { + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + //IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR = 0x4068 + if (readIrqStatus() & 0x4068) + { + setReliableRX(0); + continue; + } + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + RXnetworkID = readUint16SXBuffer(_RXPacketL - 4); + RXcrc = readUint16SXBuffer(_RXPacketL - 2); + + if ((_RXPacketL - 4) > headersize ) //check passed buffer is big enough for header + { + setReliableRX(0); + continue; + } + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + if (payloadCRC != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); + setReliableRX(0); + continue; + } + } + + if ( (RXnetworkID == networkID)) + { + bitSet(_ReliableFlags, ReliableACKReceived); + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(0); + SPI.transfer(0xFF); + + for (index = 0; index < (_RXPacketL - 4); index++) //read packet into rxbuffer + { + regdata = SPI.transfer(0); + header[index] = regdata; + } + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + return _RXPacketL; //_RXPacketL should be payload length + 4 + } + else + { + setReliableRX(0); + continue; + } + } + } while ( ((uint32_t) (millis() - startmS) < acktimeout)); + + bitSet(_ReliableErrors, ReliableACKError); + bitSet(_ReliableErrors, ReliableTimeout); + return 0; +} + + +uint8_t SX128XLT::receiveDTIRQ(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t datasize, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ) +{ +#ifdef SX128XDEBUGRELIABLE + Serial.println(); + Serial.println(F(" {RELIABLE} receiveDT()")); + Serial.print(F(" {RELIABLE} _ReliableConfig ")); + Serial.println(_ReliableConfig, HEX); +#endif + + uint16_t index, payloadcrc = 0, RXcrc, RXnetworkID = 0; + uint8_t regdataL, regdataH; + uint8_t RXHeaderL; + uint8_t RXDataL; + uint8_t buffer[2]; + uint8_t RXstart; + + _ReliableErrors = 0; + _ReliableFlags = 0; + setMode(MODE_STDBY_RC); + setPayloadLength(127); //set for maximum packet length in FLRC mode, packets might be filtered otherwise + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR), 0, 0); + setRx(rxtimeout); + + if (!wait) + { + return 0; //not wait requested so no packet length to pass + } + + //0x4022 = IRQ_RX_DONE + IRQ_RX_TX_TIMEOUT + IRQ_HEADER_ERROR + while (!(readIrqStatus() & 0x4022 )); //wait for IRQs going active + + setMode(MODE_STDBY_RC); //ensure to stop further packet reception + + //IRQ_HEADER_ERROR + IRQ_CRC_ERROR + IRQ_RX_TX_TIMEOUT + IRQ_SYNCWORD_ERROR = 0x4068 + if (readIrqStatus() & 0x4068) + { +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} Packet error")); +#endif + return 0; //packet is errored somewhere so return 0 + } + + RXHeaderL = getByteSXBuffer(2); + RXDataL = getByteSXBuffer(3); + + readCommand(RADIO_GET_RXBUFFERSTATUS, buffer, 2); + _RXPacketL = buffer[0]; + RXstart = buffer[1]; + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} Header ")); + printSXBufferHEX(0, (RXHeaderL - 1)); + Serial.println(); + Serial.print(F(" {RELIABLE} Received data payload size ")); + Serial.println(RXDataL); + Serial.print(F(" {RELIABLE} Data payload ")); + printSXBufferHEX(RXHeaderL, RXHeaderL + RXDataL - 1); + Serial.println(); +#endif + + if (RXHeaderL > headersize ) + { +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} Header size error ")); + Serial.println(headersize); +#endif + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + if (RXDataL > datasize ) + { +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} Data size error ")); + Serial.println(datasize); +#endif + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} _RXPacketL ")); + Serial.println(_RXPacketL); + Serial.print(F(" {RELIABLE} Header ")); + printSXBufferHEX(0, (RXHeaderL - 1)); + Serial.println(); + Serial.print(F(" {RELIABLE} Received data payload size ")); + Serial.println(RXDataL); + Serial.print(F(" {RELIABLE} Data payload ")); + printSXBufferHEX(RXHeaderL, RXHeaderL + RXDataL - 1); + Serial.println(); +#endif + + if (_RXPacketL < 10) //check received packet is 10 or more bytes long + { + //Serial.println(F(" {RELIABLE} _RXPacketL < 10")); + bitSet(_ReliableErrors, ReliableSizeError); + return 0; + } + + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); //start the burst read + SPI.transfer(RADIO_READ_BUFFER); + SPI.transfer(RXstart); + SPI.transfer(0xFF); + + for (index = 0; index < RXHeaderL; index++) + { + regdataL = SPI.transfer(0); + header[index] = regdataL; + } + + for (index = 0; index < RXDataL; index++) + { + regdataL = SPI.transfer(0); + dataarray[index] = regdataL; + } + + regdataL = SPI.transfer(0); + regdataH = SPI.transfer(0); + RXnetworkID = ((uint16_t) regdataH << 8) + regdataL; + regdataL = SPI.transfer(0); + regdataH = SPI.transfer(0); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + if (!bitRead(_ReliableConfig, NoReliableCRC)) + { + payloadcrc = CRCCCITT(dataarray, RXDataL, 0xFFFF); + RXcrc = ((uint16_t) regdataH << 8) + regdataL; + +#ifdef SX128XDEBUGRELIABLE + Serial.println(F(" {RELIABLE} Payload CRC check enabled")); + Serial.print(F(" {RELIABLE} payloadcrc 0x")); + Serial.println(payloadcrc, HEX); + Serial.print(F(" {RELIABLE} RXcrc 0x")); + Serial.println(RXcrc, HEX); +#endif + + if (payloadcrc != RXcrc) + { + bitSet(_ReliableErrors, ReliableCRCError); +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} CRCmissmatch, received 0x")); + Serial.println(RXcrc, HEX); +#endif + } + } + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} RXnetworkID 0x")); + Serial.println(RXnetworkID, HEX); +#endif + + if (RXnetworkID != networkID) + { +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} NetworkID missmatch received 0x")); + Serial.print(RXnetworkID, HEX); + Serial.print(F(" LocalID 0x")); + Serial.println(networkID, HEX); +#endif + bitSet(_ReliableErrors, ReliableIDError); + } + + if (_ReliableErrors) //if there has been a reliable error return a RX fail + { +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} Reliable errors")); +#endif + + return 0; + } + + return _RXPacketL; //return and indicate RX OK. +} + + +uint8_t SX128XLT::sendACKDTIRQ(uint8_t *header, uint8_t headersize, int8_t txpower) +{ + +#ifdef SX128XDEBUGRELIABLE + Serial.print(F(" {RELIABLE} sendACKDT() ")); +#endif + + uint32_t txtimeout = 12000; //set TX timeout to 12 seconds, longest packet is 8.7secs + uint8_t bufferdata, index; + uint16_t networkID; + uint16_t payloadCRC; + + setMode(MODE_STDBY_RC); + _TXPacketL = headersize + 4; + networkID = readUint16SXBuffer(_RXPacketL - 4); + payloadCRC = readUint16SXBuffer(_RXPacketL - 2); + checkBusy(); + +#ifdef USE_SPI_TRANSACTION + SPI.beginTransaction(SPISettings(LTspeedMaximum, LTdataOrder, LTdataMode)); +#endif + + digitalWrite(_NSS, LOW); + SPI.transfer(RADIO_WRITE_BUFFER); + SPI.transfer(0); + + for (index = 0; index < headersize; index++) + { + bufferdata = header[index]; + SPI.transfer(bufferdata); + } + + SPI.transfer(lowByte(networkID)); + SPI.transfer(highByte(networkID)); + SPI.transfer(lowByte(payloadCRC)); + SPI.transfer(highByte(payloadCRC)); + + digitalWrite(_NSS, HIGH); + +#ifdef USE_SPI_TRANSACTION + SPI.endTransaction(); +#endif + + setPayloadLength(_TXPacketL); + setTxParams(txpower, RAMP_TIME); + setDioIrqParams(IRQ_RADIO_ALL, (IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT), 0, 0); //set for IRQ on TX done and timeout on DIO1 + setTx(txtimeout); + + //0x4001 = IRQ_TX_DONE + IRQ_RX_TX_TIMEOUT + while (!(readIrqStatus() & 0x4001 )); //wait for IRQs going active + + + + if (readIrqStatus() & IRQ_RX_TX_TIMEOUT ) //check for timeout + { + bitSet(_ReliableFlags, ReliableTimeout); + return 0; + } + + bitSet(_ReliableFlags, ReliableACKSent); + return _TXPacketL; //TX OK so return TXpacket length +} + + + + +/* + MIT license + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ diff --git a/src/SX128XLT.h b/src/SX128XLT.h new file mode 100644 index 0000000..9ddb694 --- /dev/null +++ b/src/SX128XLT.h @@ -0,0 +1,259 @@ +#ifndef SX128XLT_h +#define SX128XLT_h + +#include "Arduino.h" +#include + + +class SX128XLT { + + public: + + SX128XLT(); + + bool begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, int8_t pinDIO2, int8_t pinDIO3, int8_t pinRXEN, int8_t pinTXEN, uint8_t device); + bool begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, uint8_t device); + bool begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, int8_t pinDIO1, int8_t pinRXEN, int8_t pinTXEN, uint8_t device); + bool begin(int8_t pinNSS, int8_t pinNRESET, int8_t pinRFBUSY, uint8_t device); + bool begin(int8_t pinNSS, int8_t pinRFBUSY, uint8_t device); + + void rxEnable(); + void txEnable(); + + + void checkBusy(); + bool config(); + void readRegisters( uint16_t address, uint8_t *buffer, uint16_t size ); + uint8_t readRegister( uint16_t address ); + void writeRegisters( uint16_t address, uint8_t *buffer, uint16_t size ); + void writeRegister( uint16_t address, uint8_t value ); + void writeCommand(uint8_t Opcode, uint8_t *buffer, uint16_t size ); + void readCommand( uint8_t Opcode, uint8_t *buffer, uint16_t size ); + void resetDevice(); + bool checkDevice(); + void setupLoRa(uint32_t frequency, int32_t offset, uint8_t modParam1, uint8_t modParam2, uint8_t modParam3); + void setMode(uint8_t modeconfig); + void setRegulatorMode(uint8_t mode); + void setPacketType(uint8_t PacketType); + void setRfFrequency( uint32_t frequency, int32_t offset ); + void setBufferBaseAddress(uint8_t txBaseAddress, uint8_t rxBaseAddress); + void setModulationParams(uint8_t modParam1, uint8_t modParam2, uint8_t modParam3); + void setPacketParams(uint8_t packetParam1, uint8_t packetParam2, uint8_t packetParam3, uint8_t packetParam4, uint8_t packetParam5, uint8_t packetParam6, uint8_t packetParam7); + void setPacketParams(uint8_t packetParam1, uint8_t packetParam2, uint8_t packetParam3, uint8_t packetParam4, uint8_t packetParam5); + void setDioIrqParams(uint16_t irqMask, uint16_t dio1Mask, uint16_t dio2Mask, uint16_t dio3Mask ); + void setHighSensitivity(); + void setLowPowerRX(); + void printModemSettings(); + void printDevice(); + uint32_t getFreqInt(); + uint8_t getLoRaSF(); + uint32_t returnBandwidth(uint8_t data); + uint8_t getLoRaCodingRate(); + uint8_t getInvertIQ(); + uint16_t getPreamble(); + void printOperatingSettings(); + uint8_t getLNAgain(); + void printRegisters(uint16_t Start, uint16_t End); + void printASCIIPacket(uint8_t *buff, uint8_t tsize); + uint8_t transmit(uint8_t *txbuffer, uint8_t size, uint16_t timeout, int8_t txpower, uint8_t wait); + uint8_t transmitIRQ(uint8_t *txbuffer, uint8_t size, uint16_t timeout, int8_t txpower, uint8_t wait); + void setTxParams(int8_t TXpower, uint8_t RampTime); + void setTx(uint16_t timeout); + void clearIrqStatus( uint16_t irq ); + uint16_t readIrqStatus(); + void printIrqStatus(); + uint16_t CRCCCITT(uint8_t *buffer, uint32_t size, uint16_t start); + uint8_t receive(uint8_t *rxbuffer, uint8_t size, uint16_t timeout, uint8_t wait); + uint8_t receiveIRQ(uint8_t *rxbuffer, uint8_t size, uint16_t timeout, uint8_t wait); + int16_t readPacketRSSI2(); + int16_t readPacketRSSI(); + int8_t readPacketSNR(); + uint8_t readRXPacketL(); + void setRx(uint16_t timeout); + void setSyncWord1(uint32_t syncword); + void setSyncWord2(uint32_t syncword); + void setSyncWord3(uint32_t syncword); + void setSyncWordErrors(uint8_t errors); + void setSleep(uint8_t sleepconfig); + uint16_t CRCCCITTSX(uint8_t startadd, uint8_t endadd, uint16_t startvalue); + uint8_t getByteSXBuffer(uint8_t addr); + int32_t getFrequencyErrorRegValue(); + int32_t getFrequencyErrorHz(); + void printHEXByte(uint8_t temp); + void wake(); + uint8_t transmitAddressed(uint8_t *txbuffer, uint8_t size, char txpackettype, char txdestination, char txsource, uint32_t timeout, int8_t txpower, uint8_t wait); + uint8_t receiveAddressed(uint8_t *rxbuffer, uint8_t size, uint16_t timeout, uint8_t wait); + uint8_t readRXPacketType(); + uint8_t readPacket(uint8_t *rxbuffer, uint8_t size); + void setPeriodBase(uint8_t value); + uint8_t getPeriodBase(); + + + //*************************************************************************** + //Start direct access SX buffer routines + //*************************************************************************** + void startWriteSXBuffer(uint8_t ptr); + uint8_t endWriteSXBuffer(); + void startReadSXBuffer(uint8_t ptr); + uint8_t endReadSXBuffer(); + + void writeUint8(uint8_t x); + uint8_t readUint8(); + + void writeInt8(int8_t x); + int8_t readInt8(); + + void writeInt16(int16_t x); + int16_t readInt16(); + + void writeUint16(uint16_t x); + uint16_t readUint16(); + + void writeInt32(int32_t x); + int32_t readInt32(); + + void writeUint32(uint32_t x); + uint32_t readUint32(); + + void writeFloat(float x); + float readFloat(); + + uint8_t transmitSXBuffer(uint8_t startaddr, uint8_t length, uint16_t timeout, int8_t txpower, uint8_t wait); + uint8_t transmitSXBufferIRQ(uint8_t startaddr, uint8_t length, uint16_t timeout, int8_t txpower, uint8_t wait); + void writeBuffer(uint8_t *txbuffer, uint8_t size); + //void writeBuffer(uint8_t *txbuffer, uint8_t startaddr, uint8_t size); + uint8_t receiveSXBuffer(uint8_t startaddr, uint16_t timeout, uint8_t wait); + uint8_t receiveSXBufferIRQ(uint8_t startaddr, uint16_t timeout, uint8_t wait ); + uint8_t readBuffer(uint8_t *rxbuffer); + //uint8_t readBuffer(uint8_t *rxbuffer, uint8_t startaddr, uint8_t len); + void printSXBufferHEX(uint8_t start, uint8_t end); + void writeBufferChar(char *txbuffer, uint8_t size); + uint8_t readBufferChar(char *rxbuffer); + + + //*************************************************************************** + //Start ranging routines + //*************************************************************************** + void setRangingSlaveAddress(uint32_t address); + void setRangingSlaveAddress(uint32_t address, uint8_t bits); + void setRangingMasterAddress(uint32_t address); + void setRangingCalibration(uint16_t cal); + void setRangingRole(uint8_t role); + double getRangingDistance(uint8_t resultType, int32_t regval, float adjust); + uint32_t getRangingResultRegValue(uint8_t resultType); + int32_t complement2( uint32_t num, uint8_t bitCnt ); + bool setupRanging(uint32_t frequency, int32_t offset, uint8_t modParam1, uint8_t modParam2, uint8_t modParam3, uint32_t address, uint8_t role); + bool transmitRanging(uint32_t address, uint16_t timeout, int8_t txpower, uint8_t wait); + uint8_t receiveRanging(uint32_t address, uint16_t timeout, int8_t txpower, uint8_t wait); + uint8_t receiveRanging(uint32_t address, uint8_t bits, uint16_t timeout, int8_t txpower, uint8_t wait); + uint16_t lookupCalibrationValue(uint8_t spreadingfactor, uint8_t bandwidth); + uint16_t getSetCalibrationValue(); + int16_t getRangingRSSI(); + + + //******************************************************************************* + //Reliable RX\TX routines + //******************************************************************************* + uint8_t transmitReliable(uint8_t *txbuffer, uint8_t size, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait); + uint8_t receiveReliable(uint8_t *rxbuffer, uint8_t size, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ); + + uint8_t transmitReliableAutoACK(uint8_t *txbuffer, uint8_t size, uint16_t networkID, uint32_t acktimeout, uint32_t txtimeout, int8_t txpower, uint8_t wait); + uint8_t receiveReliableAutoACK(uint8_t *rxbuffer, uint8_t size, uint16_t networkID, uint32_t ackdelay, int8_t txpower, uint32_t rxtimeout, uint8_t wait ); + + uint8_t sendReliableACK(uint16_t networkID, uint16_t payloadcrc, int8_t txpower); + uint8_t sendReliableACK(uint8_t *txbuffer, uint8_t size, uint16_t networkID, uint16_t payloadcrc, int8_t txpower); + uint8_t waitReliableACK(uint16_t networkID, uint16_t payloadcrc, uint32_t acktimeout); + uint8_t waitReliableACK(uint8_t *rxbuffer, uint8_t size, uint16_t networkID, uint16_t payloadcrc, uint32_t acktimeout); + + void printASCIIArray(uint8_t *buffer, uint8_t size); + uint8_t getReliableConfig(uint8_t bitread); + + uint16_t getTXPayloadCRC(uint8_t length); + void printReliableStatus(); + uint16_t getRXPayloadCRC(uint8_t length); + uint16_t getRXNetworkID(uint8_t length); + uint16_t getTXNetworkID(uint8_t length); + void writeUint16SXBuffer(uint8_t addr, uint16_t regdata); + uint16_t readUint16SXBuffer(uint8_t addr); + + void printHEXPacket(uint8_t *buffer, uint8_t size); + void printHEXPacket(char *buffer, uint8_t size); + void printArrayHEX(uint8_t *buffer, uint8_t size); + void printArrayHEX(char *buffer, uint8_t size); + void setReliableConfig(uint8_t bitset); + void clearReliableConfig(uint8_t bitset); + + uint8_t getPacketType(); + void setReliableRX(uint16_t timeout); + + void setupFLRC(uint32_t frequency, int32_t offset, uint8_t modParam1, uint8_t modParam2, uint8_t modParam3, uint32_t syncword); + void setFLRCPayloadLengthReg(uint8_t length); + void setLoRaPayloadLengthReg(uint8_t length); + void setPayloadLength(uint8_t length); + uint16_t CRCCCITTReliable(uint8_t startadd, uint8_t endadd, uint16_t startvalue); + + uint8_t transmitSXReliable(uint8_t startaddr, uint8_t length, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait); + uint8_t transmitSXReliableIRQ(uint8_t startaddr, uint8_t length, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait); + uint8_t receiveSXReliable(uint8_t startaddr, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ); + uint8_t receiveSXReliableIRQ(uint8_t startaddr, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ); + uint8_t transmitSXReliableAutoACK(uint8_t startaddr, uint8_t length, uint16_t networkID, uint32_t acktimeout, uint32_t txtimeout, int8_t txpower, uint8_t wait); + uint8_t receiveSXReliableAutoACK(uint8_t startaddr, uint16_t networkID, uint32_t ackdelay, int8_t txpower, uint32_t rxtimeout, uint8_t wait ); + uint8_t waitSXReliableACK(uint8_t startaddr, uint16_t networkID, uint16_t payloadcrc, uint32_t acktimeout); + uint8_t waitSXReliableACKIRQ(uint8_t startaddr, uint16_t networkID, uint16_t payloadcrc, uint32_t acktimeout); + uint8_t sendSXReliableACK(uint8_t startaddr, uint8_t length, uint16_t networkID, uint16_t payloadcrc, int8_t txpower); + uint8_t sendSXReliableACKIRQ(uint8_t startaddr, uint8_t length, uint16_t networkID, uint16_t payloadcrc, int8_t txpower); + uint8_t readReliableErrors(); + uint8_t readReliableFlags(); + + //******************************************************************************* + //Data Transfer functions + //******************************************************************************* + uint8_t transmitDT(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t datasize, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait); + uint8_t waitACKDT(uint8_t *header, uint8_t headersize, uint32_t acktimeout); + uint8_t receiveDT(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t datasize, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ); + uint8_t sendACKDT(uint8_t *header, uint8_t headersize, int8_t txpower); + + uint8_t transmitDTIRQ(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t datasize, uint16_t networkID, uint32_t txtimeout, int8_t txpower, uint8_t wait); + uint8_t waitACKDTIRQ(uint8_t *header, uint8_t headersize, uint32_t acktimeout); + uint8_t receiveDTIRQ(uint8_t *header, uint8_t headersize, uint8_t *dataarray, uint8_t datasize, uint16_t networkID, uint32_t rxtimeout, uint8_t wait ); + uint8_t sendACKDTIRQ(uint8_t *header, uint8_t headersize, int8_t txpower); + + + private: + + int8_t _NSS, _NRESET, _RFBUSY, _DIO1, _DIO2, _DIO3; + int8_t _RXEN, _TXEN; + uint8_t _RXPacketL; //length of packet received + uint8_t _RXPacketType; //type number of received packet + uint8_t _RXDestination; //destination address of received packet + uint8_t _RXSource; //source address of received packet + //int8_t _PacketRSSI; //RSSI of received packet - removed November 2021, not used + //int8_t _PacketSNR; //signal to noise ratio of received packet - removed November 2021, not used + int8_t _TXPacketL; //transmitted packet length + uint8_t _RXcount; //used to keep track of the bytes read from SX1280 buffer during readFloat() etc + uint8_t _TXcount; //used to keep track of the bytes written to SX1280 buffer during writeFloat() etc + uint8_t _OperatingMode; //current operating mode + bool _rxtxpinmode = false; //set to true if RX and TX pin mode is used. + + uint8_t _Device; //saved device type + uint8_t _TXDonePin; //the pin that will indicate TX done + uint8_t _RXDonePin; //the pin that will indicate RX done + uint8_t _PERIODBASE = PERIODBASE_01_MS; + + uint8_t savedRegulatorMode; + uint8_t savedPacketType; + uint32_t savedFrequency; + int32_t savedOffset; + uint8_t savedModParam1, savedModParam2, savedModParam3; //sequence is spreading factor, bandwidth, coding rate + uint8_t savedPacketParam1, savedPacketParam2, savedPacketParam3, savedPacketParam4, savedPacketParam5, savedPacketParam6, savedPacketParam7; + uint16_t savedIrqMask, savedDio1Mask, savedDio2Mask, savedDio3Mask; + int8_t savedTXPower; + uint16_t savedCalibration; + uint32_t savedFrequencyReg; + + uint8_t _ReliableErrors; //Reliable status byte + uint8_t _ReliableFlags; //Reliable flags byte + uint8_t _ReliableConfig; //Reliable config byte + +}; +#endif diff --git a/src/SX128XLT_Definitions.h b/src/SX128XLT_Definitions.h new file mode 100644 index 0000000..887cd12 --- /dev/null +++ b/src/SX128XLT_Definitions.h @@ -0,0 +1,439 @@ +// SX1280LT Includes + +//************************************************************* +// LoRa Modem Settings +//************************************************************* +// +// LoRa spreading factors +#define LORA_SF5 0x50 +#define LORA_SF6 0x60 +#define LORA_SF7 0x70 +#define LORA_SF8 0x80 +#define LORA_SF9 0x90 +#define LORA_SF10 0xA0 +#define LORA_SF11 0xB0 +#define LORA_SF12 0xC0 + +// LoRa bandwidths +#define LORA_BW_0200 0x34 // actually 203125hz +#define LORA_BW_0400 0x26 // actually 406250hz +#define LORA_BW_0800 0x18 // actually 812500hz +#define LORA_BW_1600 0x0A // actually 1625000hz + +// LoRa coding rates +#define LORA_CR_4_5 0x01 +#define LORA_CR_4_6 0x02 +#define LORA_CR_4_7 0x03 +#define LORA_CR_4_8 0x04 + +// March 2020 A new interleaving scheme has been implemented to increase robustness to +// burst interference and/or strong Doppler events. +// Note: There is a limitation on maximum payload length for LORA_CR_LI_4_8. Payload length should +// not exceed 253 bytes if CRC is enabled. +#define LORA_CR_LI_4_5 0x05 +#define LORA_CR_LI_4_6 0x06 +#define LORA_CR_LI_4_8 0x07 + +// LoRa CAD settings +#define LORA_CAD_01_SYMBOL 0x00 +#define LORA_CAD_02_SYMBOL 0x20 +#define LORA_CAD_04_SYMBOL 0x40 +#define LORA_CAD_08_SYMBOL 0x60 +#define LORA_CAD_16_SYMBOL 0x80 + +// LoRa Header Types +#define LORA_PACKET_VARIABLE_LENGTH 0x00 +#define LORA_PACKET_FIXED_LENGTH 0x80 +#define LORA_PACKET_EXPLICIT LORA_PACKET_VARIABLE_LENGTH +#define LORA_PACKET_IMPLICIT LORA_PACKET_FIXED_LENGTH + +// LoRa packet CRC settings +#define LORA_CRC_ON 0x20 +#define LORA_CRC_OFF 0x00 + +// LoRa IQ Setings +#define LORA_IQ_NORMAL 0x40 +#define LORA_IQ_INVERTED 0x00 + +#define FREQ_STEP 198.364 +#define FREQ_ERROR_CORRECTION 1.55 + +//************************************************************* +// SX1280 Interrupt flags +//************************************************************* + +#define IRQ_RADIO_NONE 0x0000 +#define IRQ_TX_DONE 0x0001 +#define IRQ_RX_DONE 0x0002 +#define IRQ_SYNCWORD_VALID 0x0004 +#define IRQ_SYNCWORD_ERROR 0x0008 +#define IRQ_HEADER_VALID 0x0010 +#define IRQ_HEADER_ERROR 0x0020 +#define IRQ_CRC_ERROR 0x0040 +#define IRQ_RANGING_SLAVE_RESPONSE_DONE 0x0080 + +#define IRQ_RANGING_SLAVE_REQUEST_DISCARDED 0x0100 +#define IRQ_RANGING_MASTER_RESULT_VALID 0x0200 +#define IRQ_RANGING_MASTER_RESULT_TIMEOUT 0x0400 +#define IRQ_RANGING_SLAVE_REQUEST_VALID 0x0800 +#define IRQ_CAD_DONE 0x1000 +#define IRQ_CAD_ACTIVITY_DETECTED 0x2000 +#define IRQ_RX_TX_TIMEOUT 0x4000 +#define IRQ_TX_TIMEOUT 0x4000 +#define IRQ_RX_TIMEOUT 0x4000 +#define IRQ_PREAMBLE_DETECTED 0x8000 +#define IRQ_RADIO_ALL 0xFFFF + +//************************************************************* +// SX1280 Commands +//************************************************************* + +#define RADIO_GET_PACKETTYPE 0x03 +#define RADIO_GET_IRQSTATUS 0x15 +#define RADIO_GET_RXBUFFERSTATUS 0x17 +#define RADIO_WRITE_REGISTER 0x18 +#define RADIO_READ_REGISTER 0x19 +#define RADIO_WRITE_BUFFER 0x1A +#define RADIO_READ_BUFFER 0x1B +#define RADIO_GET_PACKETSTATUS 0x1D +#define RADIO_GET_RSSIINST 0x1F +#define RADIO_SET_STANDBY 0x80 +#define RADIO_SET_RX 0x82 +#define RADIO_SET_TX 0x83 +#define RADIO_SET_SLEEP 0x84 +#define RADIO_SET_RFFREQUENCY 0x86 +#define RADIO_SET_CADPARAMS 0x88 +#define RADIO_CALIBRATE 0x89 +#define RADIO_SET_PACKETTYPE 0x8A +#define RADIO_SET_MODULATIONPARAMS 0x8B +#define RADIO_SET_PACKETPARAMS 0x8C +#define RADIO_SET_DIOIRQPARAMS 0x8D +#define RADIO_SET_TXPARAMS 0x8E +#define RADIO_SET_BUFFERBASEADDRESS 0x8F +#define RADIO_SET_RXDUTYCYCLE 0x94 +#define RADIO_SET_REGULATORMODE 0x96 +#define RADIO_CLR_IRQSTATUS 0x97 +#define RADIO_SET_AUTOTX 0x98 +#define RADIO_SET_LONGPREAMBLE 0x9B +#define RADIO_SET_UARTSPEED 0x9D +#define RADIO_SET_AUTOFS 0x9E +#define RADIO_SET_RANGING_ROLE 0xA3 +#define RADIO_GET_STATUS 0xC0 +#define RADIO_SET_FS 0xC1 +#define RADIO_SET_CAD 0xC5 +#define RADIO_SET_TXCONTINUOUSWAVE 0xD1 +#define RADIO_SET_TXCONTINUOUSPREAMBLE 0xD2 +#define RADIO_SET_SAVECONTEXT 0xD5 + +//************************************************************* +// SX1280 Registers +//************************************************************* + +#define REG_LNA_REGIME 0x0891 +#define REG_LR_PAYLOADLENGTH 0x901 +#define REG_LR_PACKETPARAMS 0x903 + +#define REG_RFFrequency23_16 0x906 +#define REG_RFFrequency15_8 0x907 +#define REG_RFFrequency7_0 0x908 + +#define REG_FLRC_RFFrequency23_16 0x9A3 // found by experiment +#define REG_FLRC_RFFrequency15_8 0x9A4 +#define REG_FLRC_RFFrequency7_0 0x9A5 + +#define REG_RANGING_FILTER_WINDOW_SIZE 0x091E +#define REG_LR_DEVICERANGINGADDR 0x0916 +#define REG_LR_DEVICERANGINGADDR 0x0916 +#define REG_LR_RANGINGRESULTCONFIG 0x0924 +#define REG_LR_RANGINGRERXTXDELAYCAL 0x092C +#define REG_LR_RANGINGIDCHECKLENGTH 0x0931 +#define REG_LR_ESTIMATED_FREQUENCY_ERROR_MSB 0x954 +#define REG_LR_ESTIMATED_FREQUENCY_ERROR_MID 0x955 +#define REG_LR_ESTIMATED_FREQUENCY_ERROR_LSB 0x956 +#define REG_LR_RANGINGRESULTBASEADDR 0x0961 +#define REG_RANGING_RSSI 0x0964 +#define REG_LR_FLRCPAYLOADLENGTH 0x09C3 // found by experiment +#define REG_LR_SYNCWORDTOLERANCE 0x09CD +#define REG_LR_SYNCWORDBASEADDRESS1 0x09CE +#define REG_FLRCSYNCWORD1_BASEADDR 0x09CF +#define REG_LR_SYNCWORDBASEADDRESS2 0x09D3 +#define REG_FLRCSYNCWORD2_BASEADDR 0x09D4 +#define REG_LR_SYNCWORDBASEADDRESS3 0x09D8 +#define REG_FLRCSYNCWORD3_BASEADDR 0x09D9 + +#define REG_RANGING_RSSI 0x0964 + +#define REG_LR_ESTIMATED_FREQUENCY_ERROR_MASK 0x0FFFFF + +// SX1280 Packet Types +#define PACKET_TYPE_GFSK 0x00 +#define PACKET_TYPE_LORA 0x01 +#define PACKET_TYPE_RANGING 0x02 +#define PACKET_TYPE_FLRC 0x03 +#define PACKET_TYPE_BLE 0x04 + +// SX1280 Standby modes +#define MODE_STDBY_RC 0x00 +#define MODE_STDBY_XOSC 0x01 + +// TX and RX timeout based periods +#define PERIODBASE_15_US 0x00 +#define PERIODBASE_62_US 0x01 +#define PERIODBASE_01_MS 0x02 +#define PERIODBASE_04_MS 0x03 + +// TX ramp periods +#define RADIO_RAMP_02_US 0x00 +#define RADIO_RAMP_04_US 0x20 +#define RADIO_RAMP_06_US 0x40 +#define RADIO_RAMP_08_US 0x60 +#define RADIO_RAMP_10_US 0x80 +#define RADIO_RAMP_12_US 0xA0 +#define RADIO_RAMP_16_US 0xC0 +#define RADIO_RAMP_20_US 0xE0 + +// SX1280 Power settings +#define USE_LDO 0x00 +#define USE_DCDC 0x01 + +//************************************************************* +// SX1280 Ranging settings +//************************************************************* + +#define RANGING_IDCHECK_LENGTH_08_BITS 0x00 +#define RANGING_IDCHECK_LENGTH_16_BITS 0x01 +#define RANGING_IDCHECK_LENGTH_24_BITS 0x02 +#define RANGING_IDCHECK_LENGTH_32_BITS 0x03 + +#define RANGING_RESULT_RAW 0x00 +#define RANGING_RESULT_AVERAGED 0x01 +#define RANGING_RESULT_DEBIASED 0x02 +#define RANGING_RESULT_FILTERED 0x03 + +#define MASK_RANGINGMUXSEL 0xCF + +#define RANGING_SLAVE 0x00 +#define RANGING_MASTER 0x01 + +//************************************************************* +// GFSK modem settings +//************************************************************* + +#define GFS_BLE_BR_2_000_BW_2_4 0x04 +#define GFS_BLE_BR_1_600_BW_2_4 0x28 +#define GFS_BLE_BR_1_000_BW_2_4 0x4C +#define GFS_BLE_BR_1_000_BW_1_2 0x45 +#define GFS_BLE_BR_0_800_BW_2_4 0x70 +#define GFS_BLE_BR_0_800_BW_1_2 0x69 +#define GFS_BLE_BR_0_500_BW_1_2 0x8D +#define GFS_BLE_BR_0_500_BW_0_6 0x86 +#define GFS_BLE_BR_0_400_BW_1_2 0xB1 +#define GFS_BLE_BR_0_400_BW_0_6 0xAA +#define GFS_BLE_BR_0_250_BW_0_6 0xCE +#define GFS_BLE_BR_0_250_BW_0_3 0xC7 +#define GFS_BLE_BR_0_125_BW_0_3 0xEF + +#define GFS_BLE_MOD_IND_0_35 0 +#define GFS_BLE_MOD_IND_0_50 1 +#define GFS_BLE_MOD_IND_0_75 2 +#define GFS_BLE_MOD_IND_1_00 3 +#define GFS_BLE_MOD_IND_1_25 4 +#define GFS_BLE_MOD_IND_1_50 5 +#define GFS_BLE_MOD_IND_1_75 6 +#define GFS_BLE_MOD_IND_2_00 7 +#define GFS_BLE_MOD_IND_2_25 8 +#define GFS_BLE_MOD_IND_2_50 9 +#define GFS_BLE_MOD_IND_2_75 10 +#define GFS_BLE_MOD_IND_3_00 11 +#define GFS_BLE_MOD_IND_3_25 12 +#define GFS_BLE_MOD_IND_3_50 13 +#define GFS_BLE_MOD_IND_3_75 14 +#define GFS_BLE_MOD_IND_4_00 15 + +#define PREAMBLE_LENGTH_04_BITS 0x00 // 4 bits +#define PREAMBLE_LENGTH_08_BITS 0x10 // 8 bits +#define PREAMBLE_LENGTH_12_BITS 0x20 // 12 bits +#define PREAMBLE_LENGTH_16_BITS 0x30 // 16 bits +#define PREAMBLE_LENGTH_20_BITS 0x40 // 20 bits +#define PREAMBLE_LENGTH_24_BITS 0x50 // 24 bits +#define PREAMBLE_LENGTH_28_BITS 0x60 // 28 bits +#define PREAMBLE_LENGTH_32_BITS 0x70 // 32 bits + +#define GFS_SYNCWORD_LENGTH_1_BYTE 0x00 // Sync word length 1 byte +#define GFS_SYNCWORD_LENGTH_2_BYTE 0x02 // Sync word length 2 bytes +#define GFS_SYNCWORD_LENGTH_3_BYTE 0x04 // Sync word length 3 bytes +#define GFS_SYNCWORD_LENGTH_4_BYTE 0x06 // Sync word length 4 bytes +#define GFS_SYNCWORD_LENGTH_5_BYTE 0x08 // Sync word length 5 bytes + +#define RADIO_RX_MATCH_SYNCWORD_OFF 0x00 // no search for SyncWord +#define RADIO_RX_MATCH_SYNCWORD_1 0x10 +#define RADIO_RX_MATCH_SYNCWORD_2 0x20 +#define RADIO_RX_MATCH_SYNCWORD_1_2 0x30 +#define RADIO_RX_MATCH_SYNCWORD_3 0x40 +#define RADIO_RX_MATCH_SYNCWORD_1_3 0x50 +#define RADIO_RX_MATCH_SYNCWORD_2_3 0x60 +#define RADIO_RX_MATCH_SYNCWORD_1_2_3 0x70 + +#define RADIO_PACKET_FIXED_LENGTH 0x00 // The packet is fixed length, klnown on both RX and TX, no header +#define RADIO_PACKET_VARIABLE_LENGTH 0x20 // The packet is variable size, header included + +#define RADIO_CRC_OFF 0x00 +#define RADIO_CRC_1_BYTES 0x10 +#define RADIO_CRC_2_BYTES 0x20 +#define RADIO_CRC_3_BYTES 0x30 + +#define RADIO_WHITENING_ON 0x00 +#define RADIO_WHITENING_OFF 0x08 + +// End GFSK **************************************************** + +//************************************************************* +// FLRC modem settings +//************************************************************* + +#define FLRC_SYNC_NOSYNC 0x00 +#define FLRC_SYNC_WORD_LEN_P32S 0x04 + +#define RX_DISABLE_SYNC_WORD 0x00 +#define RX_MATCH_SYNC_WORD_1 0x10 +#define RX_MATCH_SYNC_WORD_2 0x20 +#define RX_MATCH_SYNC_WORD_1_2 0x30 +#define RX_MATCH_SYNC_WORD_3 0x40 +#define RX_MATCH_SYNC_WORD_1_3 0x50 +#define RX_MATCH_SYNC_WORD_2_3 0x60 +#define RX_MATCH_SYNC_WORD_1_2_3 0x70 + +#define FLRC_BR_1_300_BW_1_2 0x45 // 1.3Mbs +#define FLRC_BR_1_000_BW_1_2 0x69 // 1.04Mbs +#define FLRC_BR_0_650_BW_0_6 0x86 // 0.65Mbs +#define FLRC_BR_0_520_BW_0_6 0xAA // 0.52Mbs +#define FLRC_BR_0_325_BW_0_3 0xC7 // 0.325Mbs +#define FLRC_BR_0_260_BW_0_3 0xEB // 0.26Mbs + +#define FLRC_CR_1_2 0x00 // coding rate 1:2 +#define FLRC_CR_3_4 0x02 // coding rate 3:4 +#define FLRC_CR_1_0 0x04 // coding rate 1 + +#define BT_DIS 0x00 // No filtering +#define BT_1 0x10 // 1 +#define BT_0_5 0x20 // 0.5 + +#define RADIO_MOD_SHAPING_BT_OFF 0x00 +#define RADIO_MOD_SHAPING_BT_1_0 0x10 +#define RADIO_MOD_SHAPING_BT_0_5 0x20 + +// Table 13-45: PacketStatus2 in FLRC Packet +#define PacketCtrlBusy 0x01 +#define PacketReceived 0x02 +#define HeaderReceived 0x04 +#define AbortError 0x08 +#define CrcError 0x10 +#define LengthError 0x20 +#define SyncError 0x40 +#define Reserved 0x80 + +// Table 13-46: PacketStatus3 in FLRC Packet +#define PktSent 0x01 +#define rxpiderr 0x08 +#define rx_no_ack 0x10 + +// FLRC default packetparamns +#define FLRC_Default_AGCPreambleLength PREAMBLE_LENGTH_32_BITS // packetParam1 +#define FLRC_Default_SyncWordLength FLRC_SYNC_WORD_LEN_P32S // packetParam2 +#define FLRC_Default_SyncWordMatch RADIO_RX_MATCH_SYNCWORD_1 // packetParam3 +#define FLRC_Default_PacketType RADIO_PACKET_VARIABLE_LENGTH // packetParam4 +#define FLRC_Default_PayloadLength BUFFER_SIZE_DEFAULT // packetParam5 +#define FLRC_Default_CrcLength RADIO_CRC_3_BYTES // packetParam6 +#define FLRC_Default_Whitening RADIO_WHITENING_OFF // packetParam7 + +// Table 11-15 Sleep modes +#define RETAIN_INSTRUCTION_RAM 0x04 +#define RETAIN_DATABUFFER 0x02 +#define RETAIN_DATA_RAM 0x01 +#define CONFIGURATION_RETENTION 0x01 // included for libray compatibility +#define RETAIN_None 0x00 + +#ifndef RAMP_TIME +#define RAMP_TIME RADIO_RAMP_02_US +#endif + +#ifndef PERIODBASE +#define PERIODBASE PERIOBASE_01_MS +#endif + +#ifndef PERIODBASE_COUNT_15_8 +#define PERIODBASE_COUNT_15_8 0 +#endif + +#ifndef PERIODBASE_COUNT_7_0 +#define PERIODBASE_COUNT_7_0 0 +#endif + +#define DEVICE_SX1280 0x20 +#define DEVICE_SX1281 0x21 + +// SPI settings +#define LTspeedMaximum 8000000 +#define LTdataOrder MSBFIRST +#define LTdataMode SPI_MODE0 + +#define RANGING_VALID 0x03 +#define RANGING_TIMEOUT 0x02 +#define WAIT_RX 0x01 +#define WAIT_TX 0x01 +#define NO_WAIT 0x00 + +#define CalibrationSF10BW400 10180 // calibration value for ranging, SF10, BW400 +#define CalibrationSF5BW1600 13100 // calibration value for ranging, SF5, BW1600 + +const uint16_t RNG_CALIB_0400[] = {10260, 10244, 10228, 10212, 10196, 10180}; // SF5 to SF10 +const uint16_t RNG_CALIB_0800[] = {11380, 11370, 11360, 11350, 11340, 11330}; +const uint16_t RNG_CALIB_1600[] = {13100, 13160, 13220, 13280, 13340, 13400}; + +// Channel of LoRa 2.4 G (there is no channel zero., just dummy value) +const double CH_freq[] = {0E6, + 2450E6, 2402E6, 2476E6, 2436E6, 2430E6, 2468E6, 2458E6, 2416E6, 2424E6, 2478E6, + 2456E6, 2448E6, 2462E6, 2472E6, 2432E6, 2446E6, 2422E6, 2442E6, 2460E6, 2474E6, + 2414E6, 2464E6, 2454E6, 2444E6, 2404E6, 2434E6, 2410E6, 2408E6, 2440E6, 2452E6, + 2480E6, 2426E6, 2428E6, 2466E6, 2418E6, 2412E6, 2406E6, 2470E6, 2438E6, 2420E6}; + +// These are the bit numbers which when set indicate reliable errors, variable _ReliableErrors +#define ReliableCRCError 0x00 // bit number set in _ReliableErrors when there is a reliable CRC missmatch +#define ReliableIDError 0x01 // bit number set in _ReliableErrors when there is a NetworkID missmatch +#define ReliableSizeError 0x02 // bit number set in _ReliableErrors when there is a size error for packet +#define ReliableACKError 0x03 // bit number set in _ReliableErrors when there is a ACK error +#define ReliableTimeout 0x04 // bit number set in _ReliableErrors when there is a timeout error +#define SegmentSequenceError 0x05 // bit number set in _ReliableErrors when there is a segment sequence error +#define FileError 0x06 // bit number set in _ReliableErrors when there ia a file (SD) error + +// These are the bit numbers which when set indicate reliable status flags, variable _ReliableFlags +#define ReliableACKSent 0x00 // bit number set in _ReliableFlags when there is a ACK sent +#define ReliableACKReceived 0x01 // bit number set in _ReliableFlags when there is a ACK received + +// These are the bit numbers which when set indicate reliable configuration, variable _ReliableConfig +#define NoReliableCRC 0x00 // bit number set in _ReliableConfig when reliable CRC is not used +#define NoAutoACK 0x01 // bit number set in _ReliableConfig when ACK is not used + +// number of bits to use to match ranging slave address +#define bits8 0x00 +#define bits16 0x40 +#define bits24 0x80 +#define bits32 0xC0 + +/* + MIT license + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/ diff --git a/src/arrayRW.h b/src/arrayRW.h new file mode 100644 index 0000000..d522f3e --- /dev/null +++ b/src/arrayRW.h @@ -0,0 +1,270 @@ +/* + Copyright 2021 - Stuart Robinson + Licensed under a MIT license displayed at the bottom of this document. + Original published 06/05/21 +*/ + +//Changes +//February 2022, endarrayRW() returns last location written in array + +uint32_t _arraylcn; +uint8_t *_arrayaddress; + +void printArrayLocation(uint8_t num); + + +void beginarrayRW(uint8_t *buff, uint32_t lcn) +{ + _arrayaddress = buff; + _arraylcn = lcn; //sets the current location in the array +} + + +uint8_t endarrayRW() +{ + //_arraylcn will have incremented by 1 at every write to the array, so to return the last location + //written we need to subtract 1 from _arraylcn + return _arraylcn - 1; +} + + +void printarrayHEX(uint8_t *buff, uint32_t len) +{ + uint8_t index, buffdata; + + for (index = 0; index < len; index++) + { + buffdata = buff[index]; + if (buffdata < 16) + { + Serial.print(F("0")); + } + Serial.print(buffdata, HEX); + Serial.print(F(" ")); + } + +} + +void printArrayLocation(uint8_t num) +{ + Serial.print(num); + Serial.print(F(",_arraylcn,")); + Serial.println(_arraylcn); +} + + +//RW uint8_t ************************************** +void arrayWriteUint8(uint8_t buffdata) +{ + _arrayaddress[_arraylcn++] = buffdata; +} + +uint8_t arrayReadUint8() +{ + return _arrayaddress[_arraylcn++]; +} +//************************************************** + + +//RW int8_t **************************************** +void arrayWriteInt8(int8_t buffdata) +{ + _arrayaddress[_arraylcn++] = buffdata; +} + +int8_t arrayReadInt8() +{ + return _arrayaddress[_arraylcn++]; +} +//************************************************** + + +//RW char ****************************************** +void arrayWriteChar(char buffdata) +{ + _arrayaddress[_arraylcn++] = buffdata; +} + +char arrayReadChar() +{ + return _arrayaddress[_arraylcn++]; +} +//************************************************** + + +//RW uint16_t ************************************* +void arrayWriteUint16(uint16_t buffdata) +{ + _arrayaddress[_arraylcn++] = lowByte(buffdata); + _arrayaddress[_arraylcn++] = highByte(buffdata); +} + +uint16_t arrayReadUint16() +{ + uint16_t buffdata; + buffdata = _arrayaddress[_arraylcn++]; + buffdata = buffdata + (_arrayaddress[_arraylcn++] << 8); + return buffdata; +} +//************************************************** + + +//RW int16_t *************************************** +void arrayWriteInt16(int16_t buffdata) +{ + _arrayaddress[_arraylcn++] = lowByte(buffdata); + _arrayaddress[_arraylcn++] = highByte(buffdata); +} + +int16_t arrayReadInt16() +{ + uint16_t buffdata; + buffdata = _arrayaddress[_arraylcn++]; + buffdata = buffdata + (_arrayaddress[_arraylcn++] << 8); + return buffdata; +} +//************************************************** + + +//RW float ***************************************** +void arrayWriteFloat(float tempf) +{ + memcpy ((_arrayaddress + _arraylcn), &tempf, 4); + _arraylcn = _arraylcn + 4; +} + + +float arrayReadFloat() +{ + float tempf; + memcpy (&tempf, (_arrayaddress + _arraylcn), 4); + _arraylcn = _arraylcn + 4; + return tempf; +} +//************************************************** + + +/* + commented out, not supported by some platforms and produces a warning + //RW double ***************************************** + void arrayWriteDouble(double tempf) + { + memcpy ((_arrayaddress + _arraylcn), &tempf, 8); + _arraylcn = _arraylcn + 8; + } + + + float arrayReadDouble() + { + double tempd; + memcpy (&tempd, (_arrayaddress + _arraylcn), 8); + _arraylcn = _arraylcn + 8; + return tempd; + } +*/ + + +//RW uint32_t ************************************** +void arrayWriteUint32(uint32_t tempdata) +{ + memcpy ((_arrayaddress + _arraylcn), &tempdata, 4); + _arraylcn = _arraylcn + 4; +} + +uint32_t arrayReadUint32() +{ + uint32_t tempdata; + memcpy (&tempdata, (_arrayaddress + _arraylcn), 4); + _arraylcn = _arraylcn + 4; + return tempdata; +} +//************************************************** + + +//RW int32_t ************************************** +void arrayWriteInt32(int32_t tempdata) +{ + memcpy ((_arrayaddress + _arraylcn), &tempdata, 4); + _arraylcn = _arraylcn + 4; +} + +int32_t arrayReadInt32() +{ + int32_t tempdata; + memcpy (&tempdata, (_arrayaddress + _arraylcn), 4); + _arraylcn = _arraylcn + 4; + return tempdata; +} +//************************************************* + + +//RW character arrays ************************************** +void arrayWriteCharArray(char *buff, uint8_t len) +{ + uint8_t index = 0; + + for (index = 0; index < len; index++) + { + _arrayaddress[_arraylcn] = buff[index]; + _arraylcn++; + } +} + + +void arrayReadCharArray(char *buff, uint8_t len) +{ + uint8_t index = 0; + + for (index = 0; index < len; index++) + { + buff[index] = _arrayaddress[_arraylcn]; + _arraylcn++; + } +} +//************************************************* + + +//RW byte (uint8_t) arrays ************************************** +void arrayWriteByteArray(uint8_t *buff, uint8_t len) +{ + uint8_t index = 0; + + for (index = 0; index < len; index++) + { + _arrayaddress[_arraylcn] = buff[index]; + _arraylcn++; + } +} + + +void arrayReadByteArray(uint8_t *buff, uint8_t len) +{ + uint8_t index = 0; + + for (index = 0; index < len; index++) + { + buff[index] = _arrayaddress[_arraylcn]; + _arraylcn++; + } +} +//************************************************* + + + +/* + MIT license + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or substantial portions + of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED + TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. +*/