├── README.md ├── TBTracker-RX.ino ├── apx.ino ├── base64.ino ├── gps.ino ├── json.ino ├── logger.ino ├── lora-aprs.ino ├── parser.ino ├── radio.ino ├── settings.h ├── ssd1306.ino ├── ssdv.ino ├── utils.ino ├── webserver.ino └── wifi.ino /README.md: -------------------------------------------------------------------------------- 1 | # TBTracker-RX 2 | A cheap, mobile LoRa High Altitude Balloon receiver for Arduino based on esp32 and sx1278 with support for GPS, a web interface and an OLED display. As of v0.0.9 it supports ssdv. 3 | 4 | TBTracker-RX is a sketch for receiving LoRa transmissions from high altitude balloons. It will receive, decode and upload those transmissions. 5 | It is designed to upload telemetry data in the correct format to https://amateur.sondehub.org 6 | 7 | # Notes about v0.0.12 (latest release) 8 | - Fixed a bug that caused your position wrongly uploaded to Sondehub 9 | - Added a button to the webinterface for (re)uploading your position manually to improve chasing a balloon. 10 | 11 | # Hardware needed 12 | The sketch is designed to compile in the Arduino IDE and work with a TTGO T-Beam board but it will also work with seperate hardware modules. 13 | 14 | You will need at least: 15 | - esp32 based board (T-Beam recommended, but it should work with most esp32 based board) 16 | - LoRa radio module (SX127x or RFM9x module) 17 | 18 | Optional components: 19 | - GPS module (Ublox, ATGM or any compatible module) 20 | - SSD1306 LCD display (or compatible) 21 | 22 | # Libraries needed 23 | The sketch uses several libraries. Some will probably already be installed in your Arduino IDE but if not, follow the directions below: 24 | 25 | - ArduinoJson library (install from the library manager) 26 | - Adafruit_SSD1306 library (install from the library manager) 27 | - Adafruit_GFX library (install from the library manager) 28 | - Adafruit_BusIO library (install from the library manager) 29 | - Radiolib library (install from the library manager) 30 | - TinyGPSPlus library ((install from the library manager) 31 | - XPowersLib (needed as of release V0.0.11. Install from the library manager) 32 | - ESPAsyncWebServer library (download: https://github.com/me-no-dev/ESPAsyncWebServer/archive/master.zip ) 33 | - AsyncTCP library (download: https://github.com/me-no-dev/AsyncTCP/archive/master.zip ) 34 | 35 | Install the last two libraries from the IDE menu: Sketch -> Include Library -> Add .ZIP library 36 | 37 | # settings.h 38 | You will need to change the values in settings.h before you can use the sketch. 39 | Some important settings: 40 | 41 | - DEVFLAG: Set this to true if you want to test uploading to Sondehub but don't want to store the data in the Sondehub database. The telemetry will not appear on the map. You can still check the Serial Monitor to check the upload status of your data. Recommended for testing. 42 | - WiFi settings: You can add up to three wifi networks to the sketch. The software will automatically select the strongest network. Good choices are probalby your home network and the hotspot network from you mobile device. 43 | - CALLSIGN: Change your callsign. It will be shown on sondehub 44 | - UPL_LAT,UPL_LON,UPL_ALT: change these to your current location. It will be used when the software cannot get a valid GPS position 45 | - UPLOAD_YOUR_POSITION: if set to true, the software will upload your position periodically to Sondehub, so it will be show on the map. 46 | - UPLOAD_PAYLOAD_PACKET: if set to true, the software will upload received telemetry to sondehub 47 | - LORA SETTINGS: Change the frequency and LoRa mode (currently I only tested mode 2) 48 | 49 | # Compile and run 50 | >> Before you hit the compile button, be sure to select an ESP32 board in the Arduino IDE. Otherwise you will get compile errors! << 51 | >> https://docs.espressif.com/projects/arduino-esp32/en/latest/installing.html#installing-using-arduino-ide << 52 | 53 | Compile, upload and run the sketch. Use the Serial Monitor to monitor the software. It will try to connect to one of your specified WiFi networks and show the IP-number it got from DHCP. The IP-number will also show on the OLED display (if you have one of those installed on your board). 54 | 55 | # Using the web interface 56 | Just enter the ip-number in a browser and the web interface will show. From the web interface you can change the RX frequency and toggle the option to upload telemetry to Sondehub. The main webpage will autoload every 20 seconds and will show you which direction you need to go if you want to chase your balloon. 57 | 58 | # Versions 59 | v0.0.12 60 | - 14-MAY-2024: Solved a bug in uploading your position to sondehub every 30 minutes 61 | - 15-MAY-2024: Added a button in the webinterface to manually upload your position to sondehub 62 | 63 | v0.0.11 64 | - 28-JUN-2023: Added GPS debugging option in settings file 65 | - 28-JUN-2023: Solved: Packet is uploaded with the wrong LoRa mode text. 66 | - 29-JUN-2023: Compatibility with Radiolib 6.0.1 checked 67 | - 28-JUL-2023: Added support for the APX power management chip which is found in the v1.1 and v1.2 T-BEAMs 68 | 69 | v0.0.10: 70 | - 22-MAY-2023: Architecture changes to minimise the time taken to get the radio listening for the next packet 71 | - 22-MAY-2023: Updated for RadioLib 6.0.0 - https://github.com/jgromes/RadioLib/releases/tag/6.0.0 72 | - 24-MAY-2023: Re-enabled OLED Flash and Flash Pin on Packet Receive 73 | 74 | v0.0.9: 75 | - 03-MAR-2023: Serial port baudrate to 115200 76 | - 15-MAR-2023: Added support for SSDV 77 | - 20-MAR-2023: changed uploading part of the code. uploading will now take place from a queue and in a seperate thread 78 | - 07-APR-2023: disabled temporary OLED flashing, time since last packet on the OLED display and flashing a pin when a new packets is reveived 79 | 80 | v0.0.8: 81 | - 23-FEB-2023: Added support for different visual modes for the OLED (default, all, chase) 82 | - 24-FEB-2023: Added support for a "FLASH PIN" which will set HIGH for 300ms when a packet is received (new entry in settings file!) 83 | 84 | Many thanks to Star Holden, Luc Bodson and Eelco de Graaff for testing and suggesting improvements 85 | 86 | - Example of the three OLED modes 87 | 88 | ![IMG-6557](https://user-images.githubusercontent.com/58561387/222697528-747ce37b-25bc-49ef-a2cb-feca527335bf.JPG) 89 | ![IMG-6559](https://user-images.githubusercontent.com/58561387/222697536-9b1211c6-e4a5-4414-aa3b-89f98305b998.JPG) 90 | ![IMG-6555](https://user-images.githubusercontent.com/58561387/222697542-d7b8b98e-4abe-41ea-9730-17eccb419490.JPG) 91 | 92 | v0.0.7: 93 | - 03-FEB-2023: The link to Sondehub in the web interface now opens in a new window 94 | - 03-FEB-2023: Software now works also without WiFi (data on Serial output or OLED display) 95 | 96 | v0.0.6 97 | - 29-JAN-2023: Added a parser for the APRS packets to display on the Serial interface, webinterface and SSD1306 display 98 | 99 | v0.0.5 100 | - 21-JAN-2023: Print length of received packet in the Serial monitor 101 | - 21-JAN-2023: Check if the received packet is indeed a HAB telemetry packet 102 | - 21-JAN-2023: Removed the RAW telemetry string from the Serial monitor to avoid double info and unreadable characters 103 | - 21-JAN-2023: Added support for LoRa-APRS packets 104 | - 21-JAN-2023: Added a packet Log trail in the web interface 105 | 106 | v0.0.4: 107 | - 14-JAN-2023: Added support for LoRa Mode 5 (Explicit mode, Error coding 4:8, Bandwidth 41.7kHz, SF 11, Low data rate optimize off) 108 | - 14-JAN-2023: Added support for LoRa Mode 3 (Explicit mode, Error coding 4:6, Bandwidth 250kHz, SF 7, Low data rate optimize off) 109 | - 20-JAN-2023: Added support for LoRa Mode 0 (Explicit mode, Error coding 4:8, Bandwidth 20.8kHz, SF 11, Low data rate optimize on) 110 | - 20-JAN-2023: Added support for LoRa Mode 1 (Implicit mode, Error coding 4:5, Bandwidth 20.8kHz, SF 6, Low data rate optimize off) 111 | - 20-JAN-2023: Added support for showing and changing the LoRa Mode in the webinterface 112 | - 20-JAN-2023: Solved several bugs 113 | - 20-JAN-2023: Added autotune to the radio (based on the frquency error calculated by the radio) 114 | 115 | v0.0.3: 116 | - 06-JAN-2023: Added SNR, RSSI to the web interface 117 | - 06-JAN-2023: Added time since latest packet to the web interface 118 | - 06-JAN-2023: Made the Google Maps links open in a new window 119 | - 06-JAN-2023: Solved several reported bugs / unexpected behaviour 120 | - 07-JAN-2023: Changed some UI language. (I apologize, English is not my native language) 121 | 122 | v0.0.2: 123 | - 23-DEC-2022: Added upload result to the web interface 124 | - 23-DEC-2022: "comment" string is now disabled by default 125 | - 23-DEC-2022: Try to determine if the packet received is an actual HAB packet or an invalid or unknown packet 126 | - 23-DEC-2022: Changed the wording of "Test mode" in the webinterface 127 | 128 | v0.0.1: 129 | - 19-DEC-2022: Changed to x.y.z version numbering 130 | - 19-DEC-2022: Moved version number to TBTracker-rx.ino from settings.h 131 | - 19-DEC-2022: The OLED display will now show frequency updates 132 | - 19-DEC-2022: Added a way to change the DEVFLAG in the webinterface 133 | 134 | v0.1ß: 135 | - 18-DEC-2022: Initial version, released in the Facebook HAB-NL group 136 | 137 | # Work in progress 138 | This software is a work in progress. I made it to track my own balloons. Use it at your own risk, there are no guarantees. Let me know if you find it useful or not and as always don't forget to have fun. 139 | 140 | Roel. 141 | 142 | ![TBTracker-RX-001](https://user-images.githubusercontent.com/58561387/208243067-bfdd5e9e-8f6b-4190-9626-1636de4a8068.png) 143 | ![TBTracker-RX-002](https://user-images.githubusercontent.com/58561387/208243079-900dfd50-ce42-46ea-b731-e743e1de91d0.png) 144 | ![TBTracker-RX-003](https://user-images.githubusercontent.com/58561387/208243093-8fb9749e-5dee-47d9-8347-a0649a3a4bc4.png) 145 | ![tb-tracker_v003](https://user-images.githubusercontent.com/58561387/211216791-2b8a34f7-5c6b-442a-bf04-dfc4209e8bcd.png) 146 | 147 | -------------------------------------------------------------------------------- /TBTracker-RX.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * TBTracker-RX - roel@kroes.com 3 | * 4 | * A mobile software platform for receiving LoRa transmissions and uploading those 5 | * to amateur.sondehub.org. The software is designed to run on the esp32 platform. 6 | * A TTGO T-Beam would be ideal. It has WiFi connectivity, a simple web interface and support for a OLED display 7 | * 8 | * First adjust the settings in the settings file <<<<<<<<<<<<<<<<<<<< 9 | * 10 | * Be sure you run the latest version of the Arduino IDE. 11 | * 12 | * V0.0.12 13 | * 14-MAY-2024: Solved a bug in uploading your position to sondehub every 30 minutes 14 | * 15-MAY-2024: Added a button in the webinterface to manually upload your position to sondehub 15 | * 16 | * V0.0.11 17 | * WARNING: THIS VERSION REQUIRES A CHANGE IN YOUR SETTINGS.H file 18 | * Add this to your settings.h of use the file in the code base: 19 | * #define I2C_SDA 21 20 | * #define I2C_SCL 22 21 | * #define PMU_IRQ_BTN 38 22 | * 23 | * 28-JUN-2023: Added GPS debugging option in settings file 24 | * 28-JUN-2023: Solved: Packet is uploaded with the wrong LoRa mode text. 25 | * 29-JUN-2023: Compatibility with Radiolib 6.0.1 checked 26 | * 28-JUL-2023: Added support for the APX power management chip which is found in the v1.1 and v1.2 T-BEAMs 27 | * 28 | * V0.0.10 29 | * 22-May-2023: Architecture changes to minimise the time taken to get the radio listening for the next packet. 30 | * 22-MAY-2023: Updated for RadioLib 6.0.0 - https://github.com/jgromes/RadioLib/releases/tag/6.0.0 31 | * 24-MAY-2023: Re-enabled OLED Flash and Flash Pin on Packet Receive 32 | * 33 | * v0.0.9 34 | * 03-MAR-2023: Serial port baudrate to 115200 35 | * 15-MAR-2023: Added support for SSDV 36 | * 20-MAR-2023: changed uploading part of the code. uploading will now take place from a queue and in a seperate thread 37 | * 07-APR-2023: disabled temporary timed OLED update and OLED Flash and PIN flash 38 | * 39 | * v0.0.8 40 | * 23-FEB-2023: Added support for different visual modes for the OLED (default, all, chase) 41 | * 24-FEB-2023: Added support for a "FLASH PIN" which will set HIGH for 300ms when a packet is received (new entry in settings file!) 42 | * 43 | * Thanks to Star Holden, Luc Bodson and Eelco de Graaff for testing and suggesting improvements 44 | * 45 | * v0.0.7 46 | * 03-FEB-2023: The link to Sondehub in the web interface now opens in a new window 47 | * 03-FEB-2023: Software now works also without WiFi (data on Serial output or OLED display) 48 | * 49 | * v0.0.6 50 | * 29-JAN-2023: Added a parser for the APRS packets to display on the Serial interface, webinterface and SSD1306 display 51 | * 52 | * v0.0.5 53 | * 21-JAN-2023: Print length of received packet in the Serial monitor 54 | * 21-JAN-2023: Check if the received packet is indeed a HAB telemetry packet 55 | * 21-JAN-2023: Removed the RAW telemetry string from the Serial monitor to avoid double info and unreadable characters 56 | * 21-JAN-2023: Added support for LoRa-APRS packets 57 | * 21-JAN-2023: Added a packet Log trail in the web interface 58 | * 59 | * v0.0.4 60 | * 14-JAN-2023: Added support for LoRa Mode 5 (Explicit mode, Error coding 4:8, Bandwidth 41.7kHz, SF 11, Low data rate optimize off) 61 | * 14-JAN-2023: Added support for LoRa Mode 3 (Explicit mode, Error coding 4:6, Bandwidth 250kHz, SF 7, Low data rate optimize off) 62 | * 20-JAN-2023: Added support for LoRa Mode 0 (Explicit mode, Error coding 4:8, Bandwidth 20.8kHz, SF 11, Low data rate optimize on) 63 | * 20-JAN-2023: Added support for LoRa Mode 1 (Implicit mode, Error coding 4:5, Bandwidth 20.8kHz, SF 6, Low data rate optimize off) 64 | * 20-JAN-2023: Added support for showing and changing the LoRa Mode in the webinterface 65 | * 20-JAN-2023: Solved several bugs 66 | * 20-JAN-2023: Added autotune to the radio (based on the frquency error calculated by the radio) 67 | * 68 | * v0.0.3 69 | * 06-JAN-2023: Added SNR, RSSI to the web interface 70 | * 06-JAN-2023: Added time since latest packet to the web interface 71 | * 06-JAN-2023: Made the Google Maps open in a new window 72 | * 06-JAN-2023: Solved several reported bugs / unexpected behaviour 73 | * 07-JAN-2023: Changed some UI language. (I apologize, English is not my native language) 74 | * 75 | * v0.0.2 76 | * 23-DEC-2022: Added upload result to the web interface 77 | * 23-DEC-2022: "comment" string is now disabled by default 78 | * 23-DEC-2022: Try to determine if the packet received is an actual HAB packet or an invalid or unknown packet 79 | * 23-DEC-2022: Changed the wording of "Test mode" in the webinterface 80 | * 81 | * v0.0.1: 82 | * 19-DEC-2022: Changed to x.y.z version numbering 83 | * 19-DEC-2022: Moved version number to TBTracker-rx.ino from settings.h 84 | * 19-DEV-2022: The OLED display will now show frequency updates 85 | * 19-DEC-2022: Added a way to change the DEVFLAG in the webinterface 86 | * 87 | * v0.1ß: 88 | * 18-DEC-2022: Initial version, released in the Facebook HAB-NL group 89 | ************************************************************************************/ 90 | #include 91 | #include 92 | #include 93 | #include "settings.h" 94 | 95 | // TBTracker-RX version number 96 | #define TBTRACKER_VERSION "V0.0.12" 97 | // MAX possible length for a packet 98 | #define PACKETLEN 255 99 | 100 | // Struct to hold LoRA settings 101 | struct TLoRaSettings 102 | { 103 | uint8_t LoRaMode = LORA_MODE; 104 | float Frequency = LORA_FREQUENCY; 105 | float Bandwidth; 106 | uint8_t SpreadFactor; 107 | uint8_t CodeRate; 108 | uint8_t SyncWord; 109 | uint8_t Power; 110 | uint16_t PreambleLength; 111 | uint8_t Gain; 112 | size_t implicitHeader = 255; 113 | uint8_t packetType; 114 | String ModeString = "LoRa"; 115 | } LoRaSettings; 116 | 117 | struct TTBScanner 118 | { 119 | float scanFreq[5] = {LORA_FREQUENCY,0,0,0,0}; 120 | int currentNr = 0; 121 | int maxNr = 4; 122 | } TBScanner; 123 | 124 | // Struct to hold Time information 125 | struct tm timeinfo; 126 | 127 | // Keeps track of uploading your position to Sondehub 128 | bool uploader_position_sent = false; 129 | 130 | // Is the local GPS position (of this reciever) valid 131 | bool gps_valid = false; 132 | 133 | // Just a variable to calculate simple time difference 134 | unsigned long timeCounter = 0; 135 | 136 | // global counter for the number of valid packets we receive 137 | unsigned long packetCounter = 0; 138 | 139 | // Holder for the dev flag. If dev flag is true than data sent to Sondehub is not added to the database. Can be set in settings.h 140 | bool devflag; 141 | 142 | // flag to indicate that a packet was received 143 | volatile bool receivedFlag = false; 144 | 145 | // Variable to hold the time that the OLED display was turned inverted / Flash LED was turned on 146 | unsigned long flashMillis = 0; 147 | unsigned long pinMillis = 0; 148 | 149 | // Variable to hold the time that the OLED was updated 150 | unsigned long oledLastUpdated=0; 151 | bool oledUpdateNeeded = false; 152 | 153 | TaskHandle_t task_UploadSSDV; // Uploading the queue with SSDV packets to the SSDV server on core 0. 154 | TaskHandle_t task_UploadTelemetry; // Uploading the queue with Telemetry packets to the Sondehub server on core 0. 155 | TaskHandle_t task_updateDisplay; // Task for updating the oled display in the background in Core 0 156 | QueueHandle_t ssdv_Queue; // queue which will hold the SSDV records to send to the server 157 | QueueHandle_t telemetry_Queue; // queue which will hold the JSON docs to upload to the Sondehub server 158 | 159 | volatile unsigned long start; // various timing measurements 160 | 161 | 162 | /************************************************************************************ 163 | * Struct and variable which contains the latest telemetry 164 | ************************************************************************************/ 165 | struct TTelemetry 166 | { 167 | String raw; // Raw received telemetry 168 | float uploader_position[3]; // position of yourself in GPS coordinates and altitude in meters [ -34.0, 138.0, 0 ] 169 | float snr; // Receiver metadata - SNR 170 | float rssi; // Receiver metadata - RSSI 171 | float frequency; // Receiver Metadata - RX Frequency 172 | float frequency_error; // Measured by Radio. Based on this value the radio will be retuned. 173 | size_t rxPacketLen=0; // Length of received packet 174 | String modulation; // Modulation type 175 | unsigned long atmillis=0; // Reported millis when packet was received by the radio 176 | char time_received[30]; // Date/Time the packet was received on the network (example: "2022-04-18T04:36:59.899304Z") 177 | char datetime[30]; // Date/time reported by the payload itself. Use todays UTC date if no date available. (example: "2022-04-18T04:36:58.000000Z") 178 | String payload_callsign; // Callsign of the payload 179 | long frame; // Optional - Frame number as reported by the payload 180 | float lat; // Position latitude as reported by the payload 181 | float lon; // Position longitude as reported by the payload 182 | float alt; // Altitude in meters as reported by the payload 183 | unsigned int sats; // Number of satellites as reported by the payload 184 | float temp; // Measured temperature by the payload 185 | float batt; // Battery voltage measured by the payload 186 | float heading; // compass heading as reported by payload 187 | float pressure; // Pressure (hPa) 188 | float humidity; // humidity (%) 189 | float distance; // Distance in km to payload 190 | float bearing; // Bearing to payload 191 | String comment; // Optional comment for upload to Sondehub 192 | String compass; // Compass direction in terms of "N", "SW", ... 193 | String lastField; // Contains info about what data is in the fields after the location and altitude 194 | bool extraFields; // is true when there are custom fields at the end of the payload data 195 | bool uploadSondehub; // is true when the telemetry should be uploaded to Sondehub 196 | String uploadResult; // holds the latest upload result to Sondehub 197 | } Telemetry; 198 | 199 | 200 | /************************************************************************************ 201 | * Parallel task that runs on core 0 and handles uploading of the SSDV packets 202 | ************************************************************************************/ 203 | void uploadSSDV(void * parameter) 204 | { 205 | for (;;) // Run forever 206 | { 207 | // Processing the queue 208 | postSSDVToServer(); 209 | } 210 | } 211 | 212 | 213 | /************************************************************************************ 214 | * Parallel task that runs on core 0 and handles uploading of the Telemetry packets 215 | ************************************************************************************/ 216 | void uploadTelemetry(void * parameter) 217 | { 218 | for (;;) // Run forever 219 | { 220 | // Processing the queue 221 | postTelemetryToServer(); 222 | } 223 | } 224 | 225 | /************************************************************************************ 226 | * Setting up all parts of the program 227 | ************************************************************************************/ 228 | void setup() 229 | { 230 | // disable brownout 231 | WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); 232 | 233 | Serial.begin(115200); 234 | 235 | // Switching off bluetooth 236 | btStop(); 237 | 238 | devflag = DEVFLAG; 239 | if (devflag) 240 | { 241 | Serial.println(F("SOFTWARE IS IN DEVELOPMENT MODE, Data will not be shown on Sondehub. Change DEVFLAG in settings.h")); 242 | } 243 | 244 | // Initialize the Power Management chip if present 245 | XPowerInit(); 246 | 247 | // Create the Telemetry queue with 3 slots of 10124 bytes 248 | telemetry_Queue = xQueueCreate(3, 1024); 249 | if (telemetry_Queue == NULL) 250 | { 251 | Serial.println("Error creating the telemetry queue"); 252 | } 253 | else 254 | { 255 | // Start the SSDV queue uploader task on core 0 256 | xTaskCreatePinnedToCore 257 | ( 258 | uploadTelemetry, /* Function to implement the task */ 259 | "task_UploadTelemetry", /* Name of the task */ 260 | 10000, /* Stack size in words */ 261 | NULL, /* Task input parameter */ 262 | 0, /* Priority of the task */ 263 | &task_UploadTelemetry, /* Task handle. */ 264 | 0 /* Core where the task should run */ 265 | ); 266 | } 267 | 268 | // Create the SSDV queue with 10 slots of 256 bytes 269 | ssdv_Queue = xQueueCreate(10, 256); 270 | if (ssdv_Queue == NULL) 271 | { 272 | Serial.println("Error creating the SSDV queue"); 273 | } 274 | else 275 | { 276 | // Start the SSDV queue uploader task on core 0 277 | xTaskCreatePinnedToCore 278 | ( 279 | uploadSSDV, /* Function to implement the task */ 280 | "task_UploadSSDV", /* Name of the task */ 281 | 10000, /* Stack size in words */ 282 | NULL, /* Task input parameter */ 283 | 0, /* Priority of the task */ 284 | &task_UploadSSDV, /* Task handle. */ 285 | 0 /* Core where the task should run */ 286 | ); 287 | } 288 | 289 | #if defined(USE_SSD1306) 290 | // Setup the SSD1306 display if there is any 291 | setupSSD1306(); 292 | #endif 293 | 294 | setupLoRa(); 295 | setupWifi(); 296 | updateTime(); 297 | setupWebserver(); 298 | 299 | #if defined(FLASH_PIN) 300 | // Setup the pin that flashes when a packet is received 301 | setupFlashPin(); 302 | #endif 303 | 304 | // Sync the time keeper 305 | timeCounter = millis(); 306 | 307 | #if defined(USE_SSD1306) 308 | // Init the OLED display 309 | updateOLEDforFrequency(); 310 | #endif 311 | 312 | // When there is no valid GPS postion, we will take the GPS coordinates from the settings file 313 | Telemetry.uploader_position[0] = UPL_LAT; 314 | Telemetry.uploader_position[1] = UPL_LON; 315 | Telemetry.uploader_position[2] = UPL_ALT; 316 | 317 | // Upload telemetry? 318 | if (UPLOAD_PAYLOAD_PACKET) 319 | Telemetry.uploadSondehub = true; 320 | else 321 | Telemetry.uploadSondehub = false; 322 | 323 | #if defined(USE_GPS) 324 | // Setup the GPS if there is any 325 | Serial2.begin(GPS_BAUD, SERIAL_8N1, GPS_RX, GPS_TX); 326 | delay(500); 327 | smartDelay(5000); 328 | #endif 329 | 330 | } 331 | 332 | 333 | /************************************************************************************ 334 | * As most events are interrupt driven, there is only a small loop 335 | ************************************************************************************/ 336 | void loop() 337 | { 338 | // Process received LoRa packets 339 | if (receivedFlag) { 340 | receiveLoRa(); 341 | } 342 | 343 | #if defined(USE_GPS) 344 | // Poll the GPS, drops back here early if packet recieved 345 | smartDelay(20); 346 | 347 | // Process received LoRa packets 348 | if (receivedFlag) { 349 | receiveLoRa(); 350 | } 351 | #endif 352 | 353 | #if defined(FLASH_PIN) 354 | // disable the LED after Xms 355 | // Takes less than 1ms, so dont bother checking if it is on first 356 | if (millis() > (pinMillis + 300) ) 357 | { 358 | disablePin(); 359 | } 360 | 361 | // Process received LoRa packets 362 | if (receivedFlag) { 363 | receiveLoRa(); 364 | } 365 | #endif 366 | 367 | #if defined(USE_SSD1306) 368 | // disable the inverted display after Xms 369 | // Takes less than 1ms, so dont bother checking if it is on first 370 | if (millis() > (flashMillis + 100) ) 371 | { 372 | disableFlash(); 373 | } 374 | 375 | // Process received LoRa packets 376 | if (receivedFlag) { 377 | receiveLoRa(); 378 | } 379 | 380 | // Update the OLED 381 | // Includes if (oledUpdateNeeded) check to ensure only completed just after we have received a packet 382 | displayUpdate(); // 24/05/23 Measured as 27ms on a T-Beam when a display update is needed in OLED_DEFAULT mode 383 | 384 | // Blocks for around 25ms, so only safe to call just after we have received a packet, at which point it would always 385 | // show 1s since last packet received - so gives no value, unless we can move to a seperate thread? 386 | // if (millis() > (oledLastUpdated + 1000) ) 387 | // { 388 | // timedOledUpdate(); // Takes around 25ms, so only safe to call just after we have received a packet, 389 | // oledLastUpdated = millis(); 390 | // } 391 | 392 | // Process received LoRa packets 393 | if (receivedFlag) { 394 | receiveLoRa(); 395 | } 396 | #endif 397 | 398 | // Keep track of the time for re-uploading your position 399 | // Update every 30 minutes. 400 | if (millis()-timeCounter > 1800000ul) 401 | { 402 | uploader_position_sent = false; 403 | timeCounter = millis(); 404 | } 405 | 406 | // Send your position to sondehub if enabled 407 | // TODO Move to a parallel task otherwise we will loose packets here 408 | if (UPLOAD_YOUR_POSITION && !uploader_position_sent) 409 | { 410 | start = millis(); 411 | postStationToServer(); // 24/05/23 Measured as 2 to 5 seconds! 412 | uploader_position_sent = true; 413 | //Serial.print(F("\nTIME spent in postStationToServer():\t\t")); 414 | //Serial.println(millis()- start); 415 | } 416 | 417 | } 418 | -------------------------------------------------------------------------------- /apx.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * AXP power management related stuff 3 | ************************************************************************************/ 4 | 5 | #include "XPowersLib.h" 6 | 7 | #ifndef I2C_SDA 8 | #define I2C_SDA 21 9 | #endif 10 | 11 | #ifndef I2C_SCL 12 | #define I2C_SCL 22 13 | #endif 14 | 15 | // The middle button of your T-BEAM v1.1 or v1.2 16 | #ifndef PMU_IRQ_BTN 17 | #define PMU_IRQ_BTN 38 18 | #endif 19 | 20 | 21 | // Use the XPowersLibInterface standard to use the xpowers API 22 | XPowersLibInterface *PMU = NULL; 23 | 24 | bool pmu_flag = 0; 25 | 26 | const uint8_t i2c_sda = I2C_SDA; 27 | const uint8_t i2c_scl = I2C_SCL; 28 | const uint8_t pmu_irq_pin = PMU_IRQ_BTN; 29 | 30 | void set_pmuFlag(void) 31 | { 32 | pmu_flag = true; 33 | } 34 | 35 | /************************************************************************************ 36 | * Determine if a PMU that we support is present 37 | ************************************************************************************/ 38 | void XPowerInit() 39 | { 40 | // Check for the AXP2101 (most common in the T-Beam's v1.1 and v1.2) 41 | if (!PMU) 42 | { 43 | PMU = new XPowersAXP2101(Wire, i2c_sda, i2c_scl); 44 | if (!PMU->init()) 45 | { 46 | Serial.printf("Did not find AXP2101 power management\n"); 47 | delete PMU; 48 | PMU = NULL; 49 | } else 50 | { 51 | Serial.printf("AXP2101 PMU init succeeded, using AXP2101 PMU\n\n"); 52 | } 53 | } 54 | 55 | // check for the AXP192 56 | if (!PMU) 57 | { 58 | PMU = new XPowersAXP192(Wire, i2c_sda, i2c_scl); 59 | if (!PMU->init()) 60 | { 61 | Serial.printf("Did not find AXP192 power management\n"); 62 | delete PMU; 63 | PMU = NULL; 64 | } else 65 | { 66 | Serial.printf("AXP192 PMU init succeeded, using AXP192 PMU\n\n"); 67 | } 68 | } 69 | 70 | // check for the axp202 71 | if (!PMU) 72 | { 73 | PMU = new XPowersAXP202(Wire, i2c_sda, i2c_scl); 74 | if (!PMU->init()) 75 | { 76 | Serial.printf("Did not find AXP202 power management\n"); 77 | delete PMU; 78 | PMU = NULL; 79 | } else 80 | { 81 | Serial.printf("AXP202 PMU init succeeded, using AXP202 PMU\n\n"); 82 | } 83 | } 84 | 85 | // No supported PMU detected 86 | if (!PMU) 87 | { 88 | Serial.println("Continuing without APX support, which is no problem...\n\n"); 89 | } 90 | else 91 | { 92 | // Set the correct power levels for the differents power rails in the detected PMU 93 | // The following AXP192 power supply setting voltage is based on esp32 T-beam 94 | if (PMU->getChipModel() == XPOWERS_AXP192) 95 | { 96 | // lora radio power channel to 3.3V 97 | PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300); 98 | PMU->enablePowerOutput(XPOWERS_LDO2); 99 | 100 | // oled module power channel to 3.3V 101 | // disable it will cause abnormal communication between boot and AXP power supply, 102 | // do not turn it off 103 | PMU->setPowerChannelVoltage(XPOWERS_DCDC1, 3300); 104 | // enable oled power 105 | PMU->enablePowerOutput(XPOWERS_DCDC1); 106 | 107 | // GPS module power channel to 3.3V 108 | PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300); 109 | //enable GPS power 110 | PMU->enablePowerOutput(XPOWERS_LDO3); 111 | 112 | //protected oled power source 113 | PMU->setProtectedChannel(XPOWERS_DCDC1); 114 | //protected esp32 power source 115 | PMU->setProtectedChannel(XPOWERS_DCDC3); 116 | 117 | //disable not use channel 118 | PMU->disablePowerOutput(XPOWERS_DCDC2); 119 | 120 | //disable all axp chip interrupt 121 | PMU->disableIRQ(XPOWERS_AXP192_ALL_IRQ); 122 | 123 | // 124 | /* Set the constant current charging current of AXP192 125 | opt: 126 | XPOWERS_AXP192_CHG_CUR_100MA, 127 | XPOWERS_AXP192_CHG_CUR_190MA, 128 | XPOWERS_AXP192_CHG_CUR_280MA, 129 | XPOWERS_AXP192_CHG_CUR_360MA, 130 | XPOWERS_AXP192_CHG_CUR_450MA, 131 | XPOWERS_AXP192_CHG_CUR_550MA, 132 | XPOWERS_AXP192_CHG_CUR_630MA, 133 | XPOWERS_AXP192_CHG_CUR_700MA, 134 | XPOWERS_AXP192_CHG_CUR_780MA, 135 | XPOWERS_AXP192_CHG_CUR_880MA, 136 | XPOWERS_AXP192_CHG_CUR_960MA, 137 | XPOWERS_AXP192_CHG_CUR_1000MA, 138 | XPOWERS_AXP192_CHG_CUR_1080MA, 139 | XPOWERS_AXP192_CHG_CUR_1160MA, 140 | XPOWERS_AXP192_CHG_CUR_1240MA, 141 | XPOWERS_AXP192_CHG_CUR_1320MA, 142 | */ 143 | // Set the CC charge to 550mA 144 | PMU->setChargerConstantCurr(XPOWERS_AXP192_CHG_CUR_550MA); 145 | } 146 | // Set the correct power levels for the differents power rails in the detected AXP202 PMU 147 | // This chip is usually usid in esp32 watch 148 | else if (PMU->getChipModel() == XPOWERS_AXP202) 149 | { 150 | PMU->disablePowerOutput(XPOWERS_DCDC2); //not elicited 151 | 152 | PMU->setPowerChannelVoltage(XPOWERS_LDO2, 3300); 153 | PMU->enablePowerOutput(XPOWERS_LDO2); 154 | 155 | PMU->setPowerChannelVoltage(XPOWERS_LDO3, 3300); 156 | PMU->enablePowerOutput(XPOWERS_LDO3); 157 | 158 | PMU->setPowerChannelVoltage(XPOWERS_LDO4, 3300); 159 | PMU->enablePowerOutput(XPOWERS_LDO4); 160 | 161 | // 162 | /* Set the constant current charging current of AXP202 163 | opt: 164 | XPOWERS_AXP202_CHG_CUR_100MA, 165 | XPOWERS_AXP202_CHG_CUR_190MA, 166 | XPOWERS_AXP202_CHG_CUR_280MA, 167 | XPOWERS_AXP202_CHG_CUR_360MA, 168 | XPOWERS_AXP202_CHG_CUR_450MA, 169 | XPOWERS_AXP202_CHG_CUR_550MA, 170 | XPOWERS_AXP202_CHG_CUR_630MA, 171 | XPOWERS_AXP202_CHG_CUR_700MA, 172 | XPOWERS_AXP202_CHG_CUR_780MA, 173 | XPOWERS_AXP202_CHG_CUR_880MA, 174 | XPOWERS_AXP202_CHG_CUR_960MA, 175 | XPOWERS_AXP202_CHG_CUR_1000MA, 176 | XPOWERS_AXP202_CHG_CUR_1080MA, 177 | XPOWERS_AXP202_CHG_CUR_1160MA, 178 | XPOWERS_AXP202_CHG_CUR_1240MA, 179 | XPOWERS_AXP202_CHG_CUR_1320MA, 180 | */ 181 | 182 | PMU->setChargerConstantCurr(XPOWERS_AXP202_CHG_CUR_550MA); 183 | } 184 | // The following AXP192 power supply voltage setting is based on esp32s3 T-beam 185 | else if (PMU->getChipModel() == XPOWERS_AXP2101) 186 | { 187 | // lora radio power channel 188 | PMU->setPowerChannelVoltage(XPOWERS_ALDO2, 3300); 189 | PMU->enablePowerOutput(XPOWERS_ALDO2); 190 | 191 | // gps module power channel 192 | PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); 193 | PMU->enablePowerOutput(XPOWERS_ALDO3); 194 | 195 | // m.2 interface 196 | PMU->setPowerChannelVoltage(XPOWERS_DCDC3, 3300); 197 | PMU->enablePowerOutput(XPOWERS_DCDC3); 198 | 199 | // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300); 200 | // PMU->enablePowerOutput(XPOWERS_DCDC4); 201 | 202 | //not use channel 203 | PMU->disablePowerOutput(XPOWERS_DCDC2); //not elicited 204 | PMU->disablePowerOutput(XPOWERS_DCDC5); //not elicited 205 | PMU->disablePowerOutput(XPOWERS_DLDO1); //Invalid power channel, it does not exist 206 | PMU->disablePowerOutput(XPOWERS_DLDO2); //Invalid power channel, it does not exist 207 | PMU->disablePowerOutput(XPOWERS_VBACKUP); 208 | 209 | //disable all axp chip interrupt 210 | PMU->disableIRQ(XPOWERS_AXP2101_ALL_IRQ); 211 | 212 | /* Set the constant current charging current of AXP2101 213 | opt: 214 | XPOWERS_AXP2101_CHG_CUR_100MA, 215 | XPOWERS_AXP2101_CHG_CUR_125MA, 216 | XPOWERS_AXP2101_CHG_CUR_150MA, 217 | XPOWERS_AXP2101_CHG_CUR_175MA, 218 | XPOWERS_AXP2101_CHG_CUR_200MA, 219 | XPOWERS_AXP2101_CHG_CUR_300MA, 220 | XPOWERS_AXP2101_CHG_CUR_400MA, 221 | XPOWERS_AXP2101_CHG_CUR_500MA, 222 | XPOWERS_AXP2101_CHG_CUR_600MA, 223 | XPOWERS_AXP2101_CHG_CUR_700MA, 224 | XPOWERS_AXP2101_CHG_CUR_800MA, 225 | XPOWERS_AXP2101_CHG_CUR_900MA, 226 | XPOWERS_AXP2101_CHG_CUR_1000MA, 227 | */ 228 | PMU->setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_500MA); 229 | } 230 | 231 | //Set up the charging voltage, AXP2101/AXP192 4.2V gear is the same 232 | // XPOWERS_AXP192_CHG_VOL_4V2 = XPOWERS_AXP2101_CHG_VOL_4V2 233 | PMU->setChargeTargetVoltage(XPOWERS_AXP192_CHG_VOL_4V2); 234 | 235 | // Set VSY off voltage as 2600mV , Adjustment range 2600mV ~ 3300mV 236 | PMU->setSysPowerDownVoltage(2600); 237 | 238 | // Set the time of pressing the button to turn off 239 | PMU->setPowerKeyPressOffTime(XPOWERS_POWEROFF_4S); 240 | uint8_t opt = PMU->getPowerKeyPressOffTime(); 241 | Serial.print("PowerKeyPressOffTime:"); 242 | switch (opt) 243 | { 244 | case XPOWERS_POWEROFF_4S: Serial.println("4 Second"); 245 | break; 246 | case XPOWERS_POWEROFF_6S: Serial.println("6 Second"); 247 | break; 248 | case XPOWERS_POWEROFF_8S: Serial.println("8 Second"); 249 | break; 250 | case XPOWERS_POWEROFF_10S: Serial.println("10 Second"); 251 | break; 252 | default: 253 | break; 254 | } 255 | 256 | // Set the button power-on press time 257 | PMU->setPowerKeyPressOnTime(XPOWERS_POWERON_128MS); 258 | opt = PMU->getPowerKeyPressOnTime(); 259 | Serial.print("PowerKeyPressOnTime:"); 260 | switch (opt) 261 | { 262 | case XPOWERS_POWERON_128MS: Serial.println("128 Ms"); 263 | break; 264 | case XPOWERS_POWERON_512MS: Serial.println("512 Ms"); 265 | break; 266 | case XPOWERS_POWERON_1S: Serial.println("1 Second"); 267 | break; 268 | case XPOWERS_POWERON_2S: Serial.println("2 Second"); 269 | break; 270 | default: 271 | break; 272 | } 273 | 274 | Serial.println("==========================================================================="); 275 | 276 | // It is necessary to disable the detection function of the TS pin on the board 277 | // without the battery temperature detection function, otherwise it will cause abnormal charging 278 | PMU->disableTSPinMeasure(); 279 | 280 | // Enable internal ADC detection 281 | PMU->enableBattDetection(); 282 | PMU->enableVbusVoltageMeasure(); 283 | PMU->enableBattVoltageMeasure(); 284 | PMU->enableSystemVoltageMeasure(); 285 | 286 | /* 287 | The default setting is CHGLED is automatically controlled by the PMU. 288 | - XPOWERS_CHG_LED_OFF, 289 | - XPOWERS_CHG_LED_BLINK_1HZ, 290 | - XPOWERS_CHG_LED_BLINK_4HZ, 291 | - XPOWERS_CHG_LED_ON, 292 | - XPOWERS_CHG_LED_CTRL_CHG, 293 | */ 294 | PMU->setChargingLedMode(XPOWERS_CHG_LED_CTRL_CHG); 295 | 296 | 297 | // Not used yet in TBTracker but can be used for showing battery voltage for example. 298 | pinMode(pmu_irq_pin, INPUT); 299 | attachInterrupt(pmu_irq_pin, set_pmuFlag, FALLING); 300 | 301 | // Clear all interrupt flags 302 | PMU->clearIrqStatus(); 303 | 304 | /* 305 | // call specific interrupt request 306 | 307 | uint64_t pmuIrqMask = 0; 308 | 309 | if (PMU->getChipModel() == XPOWERS_AXP192) 310 | { 311 | 312 | pmuIrqMask = XPOWERS_AXP192_VBUS_INSERT_IRQ | XPOWERS_AXP192_VBUS_REMOVE_IRQ | //BATTERY 313 | XPOWERS_AXP192_BAT_INSERT_IRQ | XPOWERS_AXP192_BAT_REMOVE_IRQ | //VBUS 314 | XPOWERS_AXP192_PKEY_SHORT_IRQ | XPOWERS_AXP192_PKEY_LONG_IRQ | //POWER KEY 315 | XPOWERS_AXP192_BAT_CHG_START_IRQ | XPOWERS_AXP192_BAT_CHG_DONE_IRQ ; //CHARGE 316 | } else 317 | if (PMU->getChipModel() == XPOWERS_AXP2101) 318 | { 319 | pmuIrqMask = XPOWERS_AXP2101_BAT_INSERT_IRQ | XPOWERS_AXP2101_BAT_REMOVE_IRQ | //BATTERY 320 | XPOWERS_AXP2101_VBUS_INSERT_IRQ | XPOWERS_AXP2101_VBUS_REMOVE_IRQ | //VBUS 321 | XPOWERS_AXP2101_PKEY_SHORT_IRQ | XPOWERS_AXP2101_PKEY_LONG_IRQ | //POWER KEY 322 | XPOWERS_AXP2101_BAT_CHG_DONE_IRQ | XPOWERS_AXP2101_BAT_CHG_START_IRQ; //CHARGE 323 | } 324 | // Enable the required interrupt function 325 | PMU->enableIRQ(pmuIrqMask); 326 | 327 | */ 328 | 329 | // Call the interrupt request through the interface class 330 | PMU->disableInterrupt(XPOWERS_ALL_INT); 331 | 332 | PMU->enableInterrupt(XPOWERS_USB_INSERT_INT | 333 | XPOWERS_USB_REMOVE_INT | 334 | XPOWERS_BATTERY_INSERT_INT | 335 | XPOWERS_BATTERY_REMOVE_INT | 336 | XPOWERS_PWR_BTN_CLICK_INT | 337 | XPOWERS_CHARGE_START_INT | 338 | XPOWERS_CHARGE_DONE_INT); 339 | } 340 | } 341 | 342 | -------------------------------------------------------------------------------- /base64.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * base64 related stuff 3 | ************************************************************************************/ 4 | static char encoding_table[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 5 | 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 6 | 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 7 | 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 8 | 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 9 | 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 10 | 'w', 'x', 'y', 'z', '0', '1', '2', '3', 11 | '4', '5', '6', '7', '8', '9', '+', '/' 12 | }; 13 | 14 | 15 | static int mod_table[] = { 0, 2, 1 }; 16 | 17 | 18 | char * base64_encode( const char *data, size_t input_length, size_t * output_length, char *encoded_data ) 19 | { 20 | 21 | int i, j; 22 | 23 | *output_length = 4 * ( ( input_length + 2 ) / 3 ); 24 | 25 | for ( i = 0, j = 0; i < input_length; ) 26 | { 27 | uint32_t octet_a = i < input_length ? ( unsigned char ) data[i++] : 0; 28 | uint32_t octet_b = i < input_length ? ( unsigned char ) data[i++] : 0; 29 | uint32_t octet_c = i < input_length ? ( unsigned char ) data[i++] : 0; 30 | 31 | uint32_t triple = ( octet_a << 0x10 ) + ( octet_b << 0x08 ) + octet_c; 32 | 33 | encoded_data[j++] = encoding_table[( triple >> 3 * 6 ) & 0x3F]; 34 | encoded_data[j++] = encoding_table[( triple >> 2 * 6 ) & 0x3F]; 35 | encoded_data[j++] = encoding_table[( triple >> 1 * 6 ) & 0x3F]; 36 | encoded_data[j++] = encoding_table[( triple >> 0 * 6 ) & 0x3F]; 37 | } 38 | 39 | for ( i = 0; i < mod_table[input_length % 3]; i++ ) 40 | encoded_data[*output_length - 1 - i] = '='; 41 | 42 | return encoded_data; 43 | } -------------------------------------------------------------------------------- /gps.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * GPS related stuff 3 | ************************************************************************************/ 4 | 5 | #if defined(USE_GPS) 6 | 7 | #include 8 | 9 | // Global object which contains GPS data 10 | TinyGPSPlus gps; 11 | 12 | /************************************************************************************ 13 | * Get data from the GPS 14 | ************************************************************************************/ 15 | static void smartDelay(unsigned long ms) 16 | { 17 | unsigned long start = millis(); 18 | do 19 | { 20 | while (Serial2.available() && !receivedFlag) 21 | gps.encode(Serial2.read()); 22 | } while ((millis() - start < ms) && !receivedFlag); 23 | 24 | // Set gps_valid flag, used to display GPS status on OLED display 25 | if (gps.satellites.value() > 3) 26 | gps_valid = true; 27 | else 28 | gps_valid = false; 29 | 30 | #if defined(GPS_DEBUG) 31 | Serial.println(gps.charsProcessed() + (String)" GPS chars read, " + gps.passedChecksum() + " valid GPS sentances, " + gps.satellites.value() + " GPS sats."); 32 | #endif 33 | } 34 | 35 | /************************************************************************************ 36 | * Get data from the GPS 37 | ************************************************************************************/ 38 | static void processGPSData() 39 | { 40 | // Location 41 | if (gps.location.isValid()) 42 | { 43 | Telemetry.uploader_position[0] = gps.location.lat(); 44 | Telemetry.uploader_position[1] = gps.location.lng(); 45 | } 46 | else 47 | { 48 | // Invalid GPS position, use data from settings.h 49 | Telemetry.uploader_position[0] = UPL_LAT; 50 | Telemetry.uploader_position[1] = UPL_LON; 51 | } 52 | 53 | // Altitude 54 | if (gps.altitude.isValid()) 55 | Telemetry.uploader_position[2] = gps.altitude.meters(); 56 | else 57 | // Invalid altitude, use data from settings.h 58 | Telemetry.uploader_position[2] = UPL_ALT; 59 | } 60 | 61 | #endif 62 | 63 | 64 | /************************************************************************************ 65 | * Compute great-circle distance in km, using haversine formula 66 | * 67 | * The haversine formula 'remains particularly well-conditioned for numerical 68 | * computation even at small distances' 69 | * 70 | * It was published by R W Sinnott in Sky and Telescope, 1984, though known about 71 | * for much longer by navigators. (For the curious, c is the angular distance in 72 | * radians, and a is the square of half the chord length between the points). 73 | * 74 | * We don't adjust for altitude. Maybe in a future version. 75 | ************************************************************************************/ 76 | float GPSDistance(float lat1, float lon1, float lat2, float lon2) 77 | { 78 | float ToRad = PI / 180.0; 79 | float R = 6371; // radius earth in Km 80 | 81 | float dLat = (lat2-lat1) * ToRad; 82 | float dLon = (lon2-lon1) * ToRad; 83 | 84 | float a = sin(dLat/2) * sin(dLat/2) + 85 | cos(lat1 * ToRad) * cos(lat2 * ToRad) * 86 | sin(dLon/2) * sin(dLon/2); 87 | 88 | float c = 2 * atan2(sqrt(a), sqrt(1-a)); 89 | 90 | float d = R * c; 91 | return d; 92 | } 93 | 94 | /************************************************************************************ 95 | * Compute bearing from current location to the location of the payload 96 | * Can be used for chasing a payload. 97 | * 98 | * lat = current latitude 99 | * lon = current longitude 100 | * lat2 = payload latitude 101 | * lon2 = payload longitude 102 | ************************************************************************************/ 103 | float GPSBearing(float lat,float lon,float lat2,float lon2){ 104 | 105 | float teta1 = radians(lat); 106 | float teta2 = radians(lat2); 107 | // float delta1 = radians(lat2-lat); not used 108 | float delta2 = radians(lon2-lon); 109 | float y = sin(delta2) * cos(teta2); 110 | float x = cos(teta1)*sin(teta2) - sin(teta1)*cos(teta2)*cos(delta2); 111 | float brng = atan2(y,x); 112 | brng = degrees(brng);// radians to degrees 113 | brng = ( ((int)brng + 360) % 360 ); 114 | return brng; 115 | } 116 | 117 | 118 | /************************************************************************************ 119 | * Convert the bearing in degrees to a bearing in compass wind directions 120 | ************************************************************************************/ 121 | String degToCompass(int num) 122 | { 123 | int val=int((num/22.5)+.5); 124 | const char *arr[] = {"N","NNE","NE","ENE","E","ESE", "SE", "SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"}; 125 | return String(arr[(val % 16)]); 126 | } 127 | -------------------------------------------------------------------------------- /json.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * JSON related stuff 3 | ************************************************************************************/ 4 | 5 | #include 6 | #include 7 | #include 8 | #include "WiFi.h" 9 | 10 | /************************************************************************************ 11 | * JSON structure setup for sondehub. 12 | * 13 | * Example: 14 | * { 15 | * "software_name": "ttnhabbridge", # Receiving software name 16 | * "software_version": "0.0.1", # Receiving software version 17 | * "uploader_callsign": "foobar", # Mandatory - TTN station name? 18 | * "uploader_position": [ -34.0, 138.0, 0 ], # Optional - TTN station location, if available 19 | * "uploader_radio": "???", # Optional - Any other details 20 | * "uploader_antenna": "???", # Optional - other rx details 21 | * "snr": 11.79, # Optional - Receiver metadata - SNR 22 | * "frequency": 434.201003, # Optional - Receiver Metadata - RX Frequency 23 | * "modulation": "LoRaWAN - TTNv3", # Optional, but recommended - Modulation type 24 | * "time_received": "2022-04-18T04:36:59.899304Z", # Time the packet was received on the TTN network 25 | * "datetime": "2022-04-18T04:36:58.000000Z", # Date/time reported by the payload itself. Use todays UTC date if no date available. 26 | * "payload_callsign": "CALLSIGN_HERE", # Callsign of the payload 27 | * "frame": 6, # Optional - Frame number 28 | * "lat": -34.1, # Mandatory - Position 29 | * "lon": 138.1, 30 | * "alt": 100.0, 31 | * "temp": 30, # Some examples of optional fields 32 | * "sats": 0, 33 | * "batt": 3.15, 34 | * } 35 | * 36 | * 37 | * Station info JSON 38 | *{ 39 | "software_name": "string", 40 | "software_version": "string", 41 | "uploader_callsign": "string", 42 | "uploader_position": [ 43 | 0, 44 | 0, 45 | 0 46 | ], 47 | "uploader_antenna": "string", 48 | "uploader_contact_email": "string", 49 | "mobile": true 50 | } 51 | * 52 | ************************************************************************************/ 53 | 54 | /************************************************************************************ 55 | * Upload your current position to the Sondehub server 56 | ************************************************************************************/ 57 | void postStationToServer() 58 | { 59 | 60 | if (WiFi.status() == WL_CONNECTED) 61 | { 62 | HTTPClient https; 63 | #if defined(USE_GPS) 64 | processGPSData(); 65 | #endif 66 | 67 | https.begin(JSON_URL_LISTENERS); 68 | https.addHeader("Content-Type", "application/json"); 69 | https.addHeader("accept", "text/plain"); 70 | 71 | DynamicJsonDocument doc(350); 72 | // Add values in the document 73 | doc["software_name"] = "TBTracker-RX"; 74 | doc["software_version"] = TBTRACKER_VERSION; 75 | doc["uploader_callsign"] = CALLSIGN; 76 | doc["uploader_antenna"] = ANTENNA_USED; 77 | doc["uploader_radio"] = RADIO_USED; 78 | doc["uploader_contact_email"] = UPLOADER_EMAIL; 79 | doc["mobile"] = I_AM_MOBILE; 80 | JsonArray uploader_position = doc.createNestedArray("uploader_position"); 81 | // Add the uploader position to the JSON by populating the nested array 82 | uploader_position.add(Telemetry.uploader_position[0]); 83 | uploader_position.add(Telemetry.uploader_position[1]); 84 | uploader_position.add(Telemetry.uploader_position[2]); 85 | 86 | String json; 87 | serializeJson(doc, json); 88 | 89 | // Use the lines below for debugging. Be sure that the total length of the JSON 90 | // doen NOT exceed 350 91 | // Serial.println(); Serial.print("JSON length: "); Serial.println(json.length()); 92 | // Serial.println(json); 93 | 94 | Serial.println(); 95 | Serial.println(); 96 | Serial.println(F("Uploading your position to Sondehub:")); 97 | int httpResponseCode = https.PUT(json); 98 | 99 | // Display the result of the JSON upload 100 | if(httpResponseCode>0) 101 | { 102 | String response = https.getString(); 103 | Serial.print(httpResponseCode); Serial.print(F(" - ")); Serial.println(response); 104 | } 105 | else 106 | { 107 | Serial.printf("Error code: %d\n",httpResponseCode); 108 | Serial.printf("Error occurred while sending HTTP POST: %s\n", https.errorToString(httpResponseCode).c_str()); 109 | } 110 | 111 | https.end(); 112 | } 113 | 114 | } 115 | 116 | 117 | /************************************************************************************ 118 | // Get SSDV records from the upload queue and post the SSDV data to the SondeHub server 119 | ************************************************************************************/ 120 | void postSSDVToServer() 121 | { 122 | 123 | if (WiFi.status() == WL_CONNECTED) 124 | { 125 | char packetBuf[256]; // Contains the hex encoded packet 126 | char base64_data[512]; 127 | size_t base64_length; 128 | 129 | // Get a SSDV packet from the queue 130 | if( xQueueReceive( ssdv_Queue, 131 | packetBuf, 132 | ( TickType_t ) 1) == pdPASS ) 133 | { 134 | HTTPClient https; 135 | // packetBuf now contains a copy of the first item in the queue 136 | // Add the http headers 137 | https.begin("http://ssdv.habhub.org/api/v0/packets"); 138 | https.addHeader("Content-Type", "application/json"); 139 | https.addHeader("Accept", "application/json"); 140 | https.addHeader("charsets", "utf-8"); 141 | 142 | // code the packet into base64 143 | base64_encode(packetBuf, 256, &base64_length, base64_data); 144 | base64_data[base64_length] = '\0'; 145 | 146 | DynamicJsonDocument doc(1024); 147 | // Add values in the document 148 | doc["type"] = "packet"; 149 | doc["packet"] = base64_data; 150 | doc["encoding"] = "base64"; 151 | doc["received"] = "2023-03-17"; 152 | doc["receiver"] = CALLSIGN; 153 | 154 | String json; 155 | serializeJson(doc, json); 156 | Serial.println(); 157 | Serial.print(F("JSON length SSDV: ")); Serial.println(json.length()); 158 | 159 | int httpResponseCode = https.POST(json); 160 | 161 | // Print the results to the Serial console 162 | if(httpResponseCode <= 0) 163 | { 164 | Serial.printf("Error code: %d\n",httpResponseCode); 165 | Serial.printf("Error occurred while sending HTTP POST: %s\n", https.errorToString(httpResponseCode).c_str()); 166 | } 167 | // cleanup 168 | https.end(); 169 | } 170 | } 171 | } 172 | 173 | 174 | /************************************************************************************ 175 | // Post the telemetry data to the SondeHub upload queue 176 | ************************************************************************************/ 177 | void putTelemetryinQueue() 178 | { 179 | DynamicJsonDocument doc(1024); 180 | // Add values in the document 181 | 182 | // Add the "dev" flag to the JSON if you only want to test the validity of the 183 | if (devflag) doc["dev"] = "true"; 184 | doc["software_name"] = "TBTracker-RX"; 185 | doc["software_version"] = TBTRACKER_VERSION; 186 | doc["uploader_callsign"] = CALLSIGN; 187 | doc["time_received"] = Telemetry.time_received; 188 | doc["payload_callsign"] = Telemetry.payload_callsign; 189 | doc["datetime"] = Telemetry.datetime; 190 | doc["lat"] = Telemetry.lat; 191 | doc["lon"] = Telemetry.lon; 192 | doc["alt"] = Telemetry.alt; 193 | doc["frequency"] = Telemetry.frequency; 194 | doc["rssi"] = Telemetry.rssi; 195 | doc["snr"] = Telemetry.snr; 196 | doc["modulation"] = LoRaSettings.ModeString; 197 | doc["raw"] = Telemetry.raw; 198 | doc["uploader_antenna"] = ANTENNA_USED; 199 | doc["uploader_radio"] = RADIO_USED; 200 | 201 | if (UPLOAD_YOUR_POSITION) 202 | { 203 | // Add the uploader position to the JSON 204 | JsonArray uploader_position = doc.createNestedArray("uploader_position"); 205 | uploader_position.add(Telemetry.uploader_position[0]); 206 | uploader_position.add(Telemetry.uploader_position[1]); 207 | uploader_position.add(Telemetry.uploader_position[2]); 208 | } 209 | 210 | // Add non standard data from the payload to the JSON 211 | if (Telemetry.extraFields) 212 | { 213 | // We need to check for 6, 8, 9, A, B, R, S 214 | if (Telemetry.lastField.indexOf("6") >= 0) doc["sats"] = Telemetry.sats; 215 | if (Telemetry.lastField.indexOf("8") >= 0) doc["heading"] = Telemetry.heading; 216 | if (Telemetry.lastField.indexOf("9") >= 0) doc["batt"] = Telemetry.batt; 217 | if ( (Telemetry.lastField.indexOf("A") >= 0) || (Telemetry.lastField.indexOf("B") >= 0) ) doc["temp"] = Telemetry.temp; 218 | if (Telemetry.lastField.indexOf("R") >= 0) doc["pressure"] = Telemetry.pressure; 219 | if (Telemetry.lastField.indexOf("S") >= 0) doc["humidity"] = Telemetry.humidity; 220 | #if defined(PAYLOAD_COMMENT) 221 | doc["comment"] = Telemetry.comment; 222 | #endif 223 | } 224 | 225 | String json; 226 | serializeJson(doc, json); 227 | json = "[" + json + "]"; 228 | 229 | // JSON is ready here 230 | // Put it in the Telemetry queue 231 | char jbuf[1024]; 232 | json.toCharArray(jbuf,json.length()+1); 233 | // Add the packet to the queue. do not wait if thge queue is full 234 | if (telemetry_Queue != NULL) 235 | { 236 | if (xQueueSend(telemetry_Queue, jbuf, 0) == pdPASS) 237 | { 238 | Telemetry.uploadResult = "Telemetry packet added to upload queue."; 239 | } 240 | else 241 | { 242 | Telemetry.uploadResult = "Could not upload telemetry. Queue is full."; 243 | } 244 | } 245 | } 246 | 247 | 248 | /************************************************************************************ 249 | // Retrieve a record from the telemetry queue and upload to Sondehub 250 | ************************************************************************************/ 251 | void postTelemetryToServer() 252 | { 253 | 254 | if (WiFi.status() == WL_CONNECTED) 255 | { 256 | String json; 257 | 258 | // Get a record from the telemetry queue 259 | char jbuf[1024]; 260 | // Get a SSDV packet from the queue 261 | if( xQueueReceive( telemetry_Queue, 262 | jbuf, 263 | ( TickType_t ) 1) == pdPASS ) 264 | { 265 | HTTPClient https; 266 | 267 | https.begin(JSON_URL); 268 | https.addHeader("Content-Type", "application/json"); 269 | https.addHeader("accept", "text/plain"); 270 | 271 | json = jbuf; 272 | Serial.println(); 273 | Serial.print(F("JSON length: ")); Serial.println(json.length()); 274 | 275 | int httpResponseCode = https.PUT(json); 276 | 277 | // Print the results to the Serial console 278 | if(httpResponseCode>0) 279 | { 280 | 281 | String response = https.getString(); 282 | Serial.print("\nUpload result: "); 283 | Serial.print(httpResponseCode); Serial.print(" - "); 284 | Serial.println(response); 285 | // Telemetry.uploadResult = response; 286 | } 287 | else 288 | { 289 | Serial.printf("Error code: %d\n",httpResponseCode); 290 | Serial.printf("Error occurred while sending HTTP POST: %s\n", https.errorToString(httpResponseCode).c_str()); 291 | // Telemetry.uploadResult = https.errorToString(httpResponseCode); 292 | } 293 | https.end(); 294 | } 295 | } 296 | } 297 | 298 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /logger.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * Functions to log the packets in a FIFO queue 3 | ************************************************************************************/ 4 | 5 | // change this if you want more packets logged in the web interface 6 | #define MAXLOGS 10 7 | 8 | String logger[MAXLOGS]; 9 | unsigned long millisLogger[MAXLOGS]; 10 | 11 | /************************************************************************************ 12 | * Add the received packet to the queue for display on the web interface 13 | ************************************************************************************/ 14 | void addToLog() 15 | { 16 | // Bubble the log to most recent packet first. 17 | // purge the oldest record when the log is full 18 | for(int i=MAXLOGS-1; i > 0; i--) 19 | { 20 | logger[i]=logger[i-1]; 21 | millisLogger[i]=millisLogger[i-1]; 22 | } 23 | logger[0]=Telemetry.raw; 24 | millisLogger[0]=Telemetry.atmillis; 25 | } 26 | 27 | 28 | /************************************************************************************ 29 | * Deliver the whole queue in HTML format so it can be displayed on the web interface 30 | ************************************************************************************/ 31 | String getLogs() 32 | { 33 | String res=""; 34 | 35 | for(int i=0; i"; 44 | res += getDuration(millisLogger[i],false); 45 | res += ""; 46 | // Telemetry string 47 | res +=""; 48 | res += logger[i]; 49 | res += ""; 50 | 51 | // End of row 52 | res += ""; 53 | } 54 | } 55 | return res; 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /lora-aprs.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * Experimental functions to decode LoRa-APRS 3 | ************************************************************************************/ 4 | 5 | void parseAPRSPacket(byte *buf) 6 | { 7 | String gps_Lat; 8 | String gps_Long; 9 | String gps_Alt; 10 | String message((char *) buf); 11 | 12 | packetCounter++; 13 | Telemetry.raw = "LoRa-APRS packet"; 14 | Serial.println(message); 15 | 16 | // Get the source of the APRS packet 17 | int pos_Src = message.indexOf('>'); 18 | if (pos_Src >= 0) 19 | { 20 | Telemetry.payload_callsign = message.substring(3, pos_Src); 21 | Serial.print(F("APRS source:\t")); Serial.println(Telemetry.payload_callsign); 22 | } 23 | 24 | // Get the location of the APRS payload 25 | int pos_Loc = message.indexOf(':'); 26 | if (pos_Loc > 0) 27 | { 28 | // Get the latitude 29 | gps_Lat = message.substring(pos_Loc+2,pos_Loc+10); 30 | // Convert the latitude to decimal 31 | Telemetry.lat = gps_Lat.substring(0,2).toFloat()+(gps_Lat.substring(2,7).toFloat() / 60); 32 | if (gps_Lat[7]== 'S') 33 | { 34 | Telemetry.lat = Telemetry.lat*-1; 35 | } 36 | Serial.print("APRS latitude:\t"); Serial.println(Telemetry.lat,5); 37 | 38 | // Get the longitude 39 | gps_Long = message.substring(pos_Loc+11,pos_Loc+20); 40 | Telemetry.lon = gps_Long.substring(0,3).toFloat()+(gps_Long.substring(3,8).toFloat() / 60); 41 | Serial.print("APRS longitude:\t"); Serial.println(Telemetry.lon,5); 42 | if (gps_Long[8] == 'W') 43 | { 44 | Telemetry.lon = Telemetry.lon*-1; 45 | } 46 | } 47 | 48 | // Get the altitude of the APRS payload 49 | int pos_Alt = message.indexOf("/A="); 50 | if (pos_Alt > 0) 51 | { 52 | // altitude is in feet 53 | gps_Alt = message.substring(pos_Alt+3,pos_Alt+9); 54 | // Convert to meters 55 | Telemetry.alt = gps_Alt.toFloat() / 3.2808; 56 | Serial.print(F("APRS altitude:\t")); Serial.print(Telemetry.alt,0); Serial.println(F(" meter")); 57 | } 58 | else 59 | { 60 | Telemetry.alt=0; 61 | } 62 | 63 | // Determine your location 64 | setUploaderPosition(); 65 | 66 | // Determine the distance and bearing of the payload 67 | setDistanceAndBearing(); 68 | 69 | // Create a visual end-of-packet on the Serial console 70 | closePacket(); 71 | 72 | // Update the SSD1306 display 73 | #if defined(USE_SSD1306) 74 | // displayUpdate(); 75 | oledUpdateNeeded = true; 76 | #endif 77 | 78 | } 79 | -------------------------------------------------------------------------------- /parser.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * Telemetry parser related stuff. 3 | ************************************************************************************/ 4 | 5 | /************************************************************************************ 6 | * Get the last field from the RAW packet and determine if it contains metadata 7 | *************************************************************************************/ 8 | void getMetafromRaw(char *Buf) 9 | { 10 | char Str[PACKETLEN]; 11 | char *token; 12 | int counter = 0; 13 | const char *delimiter = ","; 14 | String lastField; 15 | 16 | // Create a copy of the original string received from the radio 17 | memcpy(Str,Buf,PACKETLEN) ; 18 | 19 | // Get the CRC, which is the string after the asterisk 20 | char *dataStr = strtok(Str,"*"); 21 | char *crcStr = strtok(NULL,"*"); 22 | 23 | // Remove the prefix of $ characters 24 | while (dataStr[0] == '$') 25 | dataStr = &dataStr[1]; 26 | 27 | // dataStr now contains all fields in a string separated by the delimiter 28 | // Parse all the fields and remember the last one 29 | token = strtok(dataStr, delimiter); 30 | while (token != NULL) 31 | { 32 | if (token != NULL) 33 | { 34 | lastField = token; 35 | } 36 | token = strtok(NULL, delimiter); 37 | } 38 | 39 | if (lastField.startsWith("012345")) 40 | { 41 | // There is metadata 42 | Telemetry.lastField = lastField; 43 | Telemetry.extraFields = true; 44 | Telemetry.lastField.toUpperCase(); 45 | } 46 | else 47 | { 48 | Telemetry.extraFields = false; 49 | } 50 | 51 | if (Telemetry.extraFields) 52 | { 53 | Serial.print("Meta data detected:\t"); 54 | Serial.println(Telemetry.lastField); 55 | } 56 | } 57 | 58 | 59 | /*********************************************************************************** 60 | * SONDEHUB EXTRA FIELDS SETTINGS 61 | * 62 | * For displaying extra fields at sondehub, we need to define which fields are 63 | * in the telemetry after the lat, lon, alt fields 64 | * This can be done by adding a specific string after the last telemetry field 65 | * This is supported by the various receivers made by Dave Akerman, and we just copy it. 66 | * See: https://www.daveakerman.com/?page_id=2410 67 | * 68 | * 0 PayloadID 69 | * 1 Counter 70 | * 2 Time 71 | * 3 Latitude 72 | * 4 Longitude 73 | * 5 Altitude 74 | * 6 Satellites 75 | * 7 Speed 76 | * 8 Heading 77 | * 9 Battery Voltage 78 | * A InternalTemperature 79 | * B ExternalTemperature 80 | * C PredictedLatitude 81 | * D PredictedLongitude 82 | * E CutdownStatus 83 | * F LastPacketSNR 84 | * G LastPacketRSSI 85 | * H ReceivedCommandCount 86 | * I-N ExtraFields 87 | * O MaximumAltitude 88 | * P Battery Current 89 | * Q External Temperature 2 90 | * R Pressure 91 | * S Humidity 92 | * T CDA 93 | * U Predicted Landing Speed 94 | * V Time Till Landing 95 | * W Last Command Received 96 | * 97 | 98 | * Parse the RAW data received by the radio 99 | ************************************************************************************/ 100 | void parseRawData(char *Buf) 101 | { 102 | char Str[PACKETLEN]; 103 | char *token; 104 | int counter = 0; 105 | const char *delimiter = ","; 106 | 107 | // Create a copy of the original string received from the radio 108 | memcpy(Str,Buf,PACKETLEN) ; 109 | 110 | // Get the CRC, which is the string after the asterisk 111 | char *dataStr = strtok(Str,"*"); 112 | Telemetry.raw = dataStr; 113 | char *crcStr = strtok(NULL,"*"); 114 | 115 | // Remove the prefix of $ characters 116 | while (dataStr[0] == '$') 117 | dataStr = &dataStr[1]; 118 | 119 | 120 | // 1. Check the internal checksum 121 | if (!CheckCRC(dataStr,crcStr)) 122 | return; // Error 123 | 124 | // 2. parse the fields 125 | // The software assumes that some data files are in the following fixed order: 126 | // 0. payload_callsign 127 | // 1. frame number 128 | // 2. Time 129 | // 3. Latitude 130 | // 4. Longitude 131 | // 5. Altitude 132 | 133 | 134 | // Parse all the fields 135 | token = strtok(dataStr, delimiter); 136 | while (token != NULL) 137 | { 138 | if (counter == 0) // payload_callsign 139 | { 140 | Telemetry.payload_callsign = token; 141 | Serial.println(); Serial.print(F("Payload callsign:\t")); Serial.println(Telemetry.payload_callsign); 142 | } 143 | 144 | if (counter == 1) // frame 145 | { 146 | Telemetry.frame = atol(token); 147 | Serial.print(F("Payload frame:\t\t")); Serial.println(Telemetry.frame); 148 | } 149 | 150 | if (counter == 2) // Time 151 | { 152 | strftime(Telemetry.datetime,sizeof(Telemetry.datetime),"%Y-%m-%dT",&timeinfo); 153 | strcat(Telemetry.datetime,token); strcat(Telemetry.datetime,".000000Z"); 154 | Serial.print(F("Payload time:\t\t")); Serial.println(Telemetry.datetime); 155 | } 156 | 157 | if (counter == 3) // Latitude 158 | { 159 | Telemetry.lat = atof(token); 160 | Serial.print(F("Latitude:\t\t")); Serial.println(Telemetry.lat,5); 161 | } 162 | 163 | if (counter == 4) // Longitude 164 | { 165 | Telemetry.lon = atof(token); 166 | Serial.print(F("Longitude:\t\t")); Serial.println(Telemetry.lon,5); 167 | } 168 | 169 | if (counter == 5) // Altitude 170 | { 171 | Telemetry.alt = atof(token); 172 | Serial.print(F("Altitude:\t\t")); Serial.println(Telemetry.alt,0); 173 | } 174 | 175 | // All fields below here are considered metaData are and only sent when defined in the payloaddata 176 | if ((Telemetry.extraFields) && (counter > 5) && (counter < Telemetry.lastField.length())) 177 | { 178 | 179 | switch (Telemetry.lastField[counter]) 180 | { 181 | case '6': Telemetry.sats = atoi(token); 182 | Serial.print(F("Satellites:\t\t")); 183 | Serial.println(Telemetry.sats); 184 | break; 185 | case '8': Telemetry.heading = atof(token); 186 | Serial.print(F("Heading:\t\t")); 187 | Serial.println(Telemetry.heading,0); 188 | break; 189 | case '9': Telemetry.batt = atof(token); 190 | Serial.print(F("Battery voltage:\t")); 191 | Serial.println(Telemetry.batt,2); 192 | break; 193 | case 'A': 194 | case 'B': Telemetry.temp = atoi(token); 195 | Serial.print(F("Temperature:\t\t")); 196 | Serial.println(Telemetry.temp); 197 | break; 198 | case 'R': Telemetry.pressure = atof(token); 199 | Serial.print(F("Pressure (hPa):\t\t")); 200 | Serial.println(Telemetry.pressure,1); 201 | break; 202 | case 'S': Telemetry.humidity = atof(token); 203 | Serial.print(F("Humidity (%):\t\t")); 204 | Serial.println(Telemetry.humidity,0); 205 | break; 206 | default: Serial.print(F("Extra field: ")); 207 | Serial.print(token); 208 | Serial.print(" "); 209 | Serial.println(Telemetry.lastField[counter]); 210 | break; 211 | } 212 | } 213 | token=strtok(NULL, delimiter); 214 | counter++; 215 | } 216 | 217 | #if defined(PAYLOAD_COMMENT) 218 | Telemetry.comment = PAYLOAD_COMMENT; 219 | Serial.print(F("Comment:\t\t")); 220 | Serial.println(Telemetry.comment); 221 | #endif 222 | 223 | // 3. Parse the GPS data or use the position from the Settings file 224 | Serial.println(); 225 | setUploaderPosition(); 226 | setDistanceAndBearing(); 227 | 228 | // 4. Update the OLED display 229 | #if defined(USE_SSD1306) 230 | // flash the screen 231 | // displayFlash(); // Moved to receiveLoRa so we do it for every packet type 232 | //displayUpdate(); // Replaced with timed update trigered by "oledUpdateNeeded" 233 | oledUpdateNeeded = true; 234 | #endif 235 | 236 | // 5. Flash the flash pin if defined 237 | #if defined(FLASH_PIN) 238 | // flashPin(); // Moved to receiveLoRa so we do it for every packet type 239 | #endif 240 | 241 | // 6. Upload to the Sondehub upload queue 242 | if (Telemetry.uploadSondehub) putTelemetryinQueue(); 243 | 244 | // 7. Close the packet with some dashes 245 | closePacket(); 246 | } 247 | 248 | 249 | /************************************************************************************ 250 | * Create a visual end-of-packet on the Serial console 251 | ************************************************************************************/ 252 | void closePacket() 253 | { 254 | Serial.println(F("----------------------------")); 255 | Serial.println(); 256 | Serial.println(); 257 | } 258 | 259 | /************************************************************************************ 260 | * Determine your position 261 | ************************************************************************************/ 262 | void setUploaderPosition() 263 | { 264 | #if defined(USE_GPS) 265 | processGPSData(); 266 | 267 | Serial.println(F("Your position from GPS:")); 268 | Serial.print(F("Your GPS latitude:\t")); Serial.println(Telemetry.uploader_position[0],5); 269 | Serial.print(F("Your GPS longitude:\t")); Serial.println(Telemetry.uploader_position[1],5); 270 | Serial.print(F("Your GPS Altitude:\t")); Serial.println(Telemetry.uploader_position[2],0); 271 | #else 272 | Telemetry.uploader_position[0] = UPL_LAT; 273 | Telemetry.uploader_position[1] = UPL_LON; 274 | Telemetry.uploader_position[2] = UPL_ALT; 275 | Serial.println(F("Your position from settings:")); 276 | Serial.print(F("Your latitude:\t")); Serial.println(Telemetry.uploader_position[0],5); 277 | Serial.print(F("Your longitude:\t")); Serial.println(Telemetry.uploader_position[1],5); 278 | Serial.print(F("Your Altitude:\t")); Serial.println(Telemetry.uploader_position[2],0); 279 | #endif 280 | } 281 | 282 | /************************************************************************************ 283 | * Determine the distance and bearing from your position to the tracker 284 | ************************************************************************************/ 285 | void setDistanceAndBearing() 286 | { 287 | // Show the distance between the receiver and the balloon 288 | Telemetry.distance = GPSDistance(Telemetry.uploader_position[0],Telemetry.uploader_position[1],Telemetry.lat,Telemetry.lon); 289 | Serial.print(F("Distance (km):\t\t")); Serial.println(Telemetry.distance,2); 290 | 291 | // Show the bearing between the receiver and the balloon 292 | Telemetry.bearing = GPSBearing(Telemetry.uploader_position[0],Telemetry.uploader_position[1],Telemetry.lat,Telemetry.lon); 293 | 294 | // Get the bearing in compass direction 295 | Telemetry.compass = degToCompass(Telemetry.bearing); 296 | Serial.print(F("Bearing (degr.):\t")); Serial.print(Telemetry.bearing,0); Serial.print(" - travel "); Serial.print(Telemetry.compass); Serial.println(" to chase"); 297 | } 298 | 299 | /************************************************************************************ 300 | * Check the CRC from the received data 301 | ************************************************************************************/ 302 | bool CheckCRC(char *dataStr, char *crcStr) 303 | { 304 | unsigned int CRC; 305 | int i, j, Count; 306 | char crcCalculated[6]; 307 | 308 | // Calculate the CRC from the received rawData 309 | CRC = 0xffff; // Seed 310 | for (i=0; dataStr[i] != '\0'; i++) 311 | { 312 | CRC ^= (((unsigned int)dataStr[i]) << 8); 313 | for (j=0; j<8; j++) 314 | { 315 | if (CRC & 0x8000) 316 | CRC = (CRC << 1) ^ 0x1021; 317 | else 318 | CRC <<= 1; 319 | } 320 | } 321 | 322 | Count = 0; 323 | crcCalculated[Count++] = Hex((CRC >> 12) & 15); 324 | crcCalculated[Count++] = Hex((CRC >> 8) & 15); 325 | crcCalculated[Count++] = Hex((CRC >> 4) & 15); 326 | crcCalculated[Count++] = Hex(CRC & 15); 327 | crcCalculated[Count++] = '\0'; 328 | 329 | Serial.print("Telemetry:\t\t"); Serial.println(dataStr); 330 | Serial.print("CRC received:\t\t"); 331 | for (int n=0; n<=3; n++) 332 | { 333 | Serial.print(crcStr[n]); 334 | } 335 | Serial.println(); 336 | Serial.print("CRC calculated:\t\t"); Serial.print(crcCalculated); 337 | 338 | if ((crcCalculated[0] == crcStr[0]) && (crcCalculated[1] == crcStr[1]) && (crcCalculated[2] == crcStr[2]) && (crcCalculated[3] == crcStr[3]) ) 339 | { 340 | Serial.println(" << CRC OK"); 341 | return true; 342 | } 343 | else 344 | { 345 | Serial.println(F(" << CRC NOT CORRECT!")); 346 | return false; 347 | } 348 | } 349 | 350 | 351 | /************************************************************************************ 352 | * Lookup table for HEX values, used in the CRC calculation 353 | ************************************************************************************/ 354 | char Hex(char Character) 355 | { 356 | char HexTable[] = "0123456789ABCDEF"; 357 | 358 | return HexTable[Character]; 359 | } 360 | -------------------------------------------------------------------------------- /radio.ino: -------------------------------------------------------------------------------- 1 | /*********************************************************************************** 2 | * Radio related stuff 3 | ***********************************************************************************/ 4 | // include the library 5 | #include // https://github.com/jgromes/RadioLib 6 | 7 | /************************************************************************************ 8 | * Globals 9 | ************************************************************************************/ 10 | // Pin numbers are defined in the settings file 11 | SX1278 radio = new Module(PIN_NSS, PIN_DIO0, PIN_RESET, PIN_DIO1); 12 | 13 | #if defined(ESP8266) || defined(ESP32) 14 | ICACHE_RAM_ATTR 15 | #endif 16 | 17 | uint8_t rawChar[PACKETLEN]; 18 | 19 | // Define the different packettypes 20 | #define PACKETTYPE_SSDV 1 21 | #define PACKETTYPE_TELEMETRY 2 22 | #define PACKETTYPE_APRS 3 23 | #define PACKETTYPE_UNKNOWN 99 24 | 25 | 26 | /************************************************************************************ 27 | * Setup the Radio 28 | ************************************************************************************/ 29 | void setupLoRa() 30 | { 31 | // Initialize the SX1278 32 | Serial.print(F("[LoRa] Initializing ... ")); 33 | 34 | // First setup the mode 35 | // 0 = (normal for telemetry) Explicit mode, Error coding 4:8, Bandwidth 20.8kHz, SF 11, Low data rate optimize on 36 | // 1 = (normal for SSDV) Implicit mode, Error coding 4:5, Bandwidth 20.8kHz, SF 6, Low data rate optimize off 37 | // 2 = (normal for repeater) Explicit mode, Error coding 4:8, Bandwidth 62.5kHz, SF 8, Low data rate optimize off 38 | // 3 = (normal for fast SSDV) Explicit mode, Error coding 4:6, Bandwidth 250kHz, SF 7, Low data rate optimize off 39 | // 5 = (normal for calling mode) Explicit mode, Error coding 4:8, Bandwidth 41.7kHz, SF 11, Low data rate optimize off 40 | // 99 = (LoRa APRS) Explicit mode, Error coding 4:5, Bandwidth 125KHz, SF 12 (experimental) - Only receiving, no igating, no uploading, experimental 41 | 42 | int16_t state = radio.begin(); 43 | 44 | switch (LoRaSettings.LoRaMode) 45 | { 46 | case 0: 47 | LoRaSettings.CodeRate = 8; 48 | LoRaSettings.Bandwidth = 20.8; 49 | LoRaSettings.SpreadFactor = 11; 50 | LoRaSettings.SyncWord = 0x12; 51 | LoRaSettings.ModeString = "LoRa Mode 0"; 52 | break; 53 | 54 | case 1: 55 | LoRaSettings.CodeRate = 5; 56 | LoRaSettings.Bandwidth = 20.8; 57 | LoRaSettings.SpreadFactor = 6; 58 | LoRaSettings.SyncWord = 0x12; 59 | LoRaSettings.implicitHeader = 255; 60 | LoRaSettings.ModeString = "LoRa Mode 1"; 61 | break; 62 | 63 | case 2: 64 | LoRaSettings.CodeRate = 8; 65 | LoRaSettings.Bandwidth = 62.5; 66 | LoRaSettings.SpreadFactor = 8; 67 | LoRaSettings.SyncWord = 0x12; 68 | LoRaSettings.ModeString = "LoRa Mode 2"; 69 | break; 70 | 71 | case 3: 72 | LoRaSettings.CodeRate = 6; 73 | LoRaSettings.Bandwidth = 250; 74 | LoRaSettings.SpreadFactor = 7; 75 | LoRaSettings.SyncWord = 0x12; 76 | LoRaSettings.ModeString = "LoRa Mode 3"; 77 | break; 78 | 79 | case 5: 80 | LoRaSettings.CodeRate = 8; 81 | LoRaSettings.Bandwidth = 41.7; 82 | LoRaSettings.SpreadFactor = 11; 83 | LoRaSettings.SyncWord = 0x12; 84 | LoRaSettings.ModeString = "LoRa Mode 5"; 85 | break; 86 | 87 | case 99: 88 | // Experimental 89 | // Frequency should be set to 433.775 in settings.h 90 | LoRaSettings.CodeRate = 5; 91 | LoRaSettings.Bandwidth = 125; 92 | LoRaSettings.SpreadFactor = 12; 93 | LoRaSettings.SyncWord = 0x12; 94 | LoRaSettings.ModeString = "LoRa-APRS"; 95 | break; 96 | } 97 | 98 | // Set the radio to the correct settings 99 | radio.setFrequency(LoRaSettings.Frequency); 100 | radio.setBandwidth(LoRaSettings.Bandwidth); 101 | radio.setSpreadingFactor(LoRaSettings.SpreadFactor); 102 | radio.setCodingRate(LoRaSettings.CodeRate); 103 | radio.setSyncWord(LoRaSettings.SyncWord); 104 | 105 | // Set the radio to LoRa mode specific settings 106 | // Add some extra radio parameters 107 | switch (LoRaSettings.LoRaMode) 108 | { 109 | case 0: 110 | // Low Data Rate Optimization 111 | radio.forceLDRO(true); 112 | radio.explicitHeader(); 113 | radio.setDio0Action(setFlag, RISING); // As of RadioLib 6.0.0 all methods to attach interrupts no longer have a default level change direction 114 | state = radio.startReceive(); 115 | break; 116 | case 1: 117 | // Mode 1 needs an implicit header with data length defined in advance 118 | radio.implicitHeader(LoRaSettings.implicitHeader); 119 | radio.setCRC(true); 120 | radio.autoLDRO(); 121 | radio.setDio0Action(setFlag, RISING); 122 | state = radio.startReceive(LoRaSettings.implicitHeader); 123 | break; 124 | default: 125 | radio.explicitHeader(); 126 | radio.autoLDRO(); 127 | radio.setDio0Action(setFlag, RISING); 128 | state = radio.startReceive(); 129 | break; 130 | } 131 | 132 | if (state == RADIOLIB_ERR_NONE) 133 | { 134 | Serial.println(F("success!")); 135 | Serial.print(F("[LoRa] Waiting for packets on: ")); Serial.print(LoRaSettings.Frequency,3); Serial.println(F(" MHz")); 136 | Serial.println(F("----------------------------")); 137 | } 138 | else 139 | { 140 | Serial.print(F("failed, code ")); 141 | Serial.println(state); 142 | while (true); 143 | } 144 | } 145 | 146 | /************************************************************************************ 147 | * this function is called when a complete packet is received by the radio module 148 | * IMPORTANT: this function MUST be 'void' type and MUST NOT have any arguments! 149 | ************************************************************************************/ 150 | void setFlag(void) 151 | { 152 | // we got a packet, set the flag 153 | receivedFlag = true; 154 | start = millis(); 155 | } 156 | 157 | /************************************************************************************ 158 | * Start receiving next packet 159 | ************************************************************************************/ 160 | void startReceive() 161 | { 162 | if (LoRaSettings.LoRaMode == 1) { 163 | radio.startReceive(LoRaSettings.implicitHeader); 164 | } else { 165 | radio.startReceive(); 166 | } 167 | //Serial.print(F("\nTIME spent not listening:\t\t")); 168 | //Serial.println(millis()- start); 169 | } 170 | 171 | /************************************************************************************ 172 | * Process a received LoRa packet. 173 | ************************************************************************************/ 174 | void receiveLoRa() 175 | { 176 | // reset the data received flag 177 | receivedFlag = false; 178 | 179 | // Read data from the radio 180 | int state; 181 | 182 | // Buffer to hold the received data from the radio 183 | byte buf[PACKETLEN]; 184 | // Init the buffer to zeros 185 | // memset(buf,0x00,sizeof(buf)); 186 | 187 | // Grab the data from the radio module 188 | switch(LoRaSettings.LoRaMode) 189 | { 190 | case 1: // Implicit header, so tell the radio how many bytes to read 191 | state = radio.readData(buf,LoRaSettings.implicitHeader); 192 | break; 193 | default: 194 | state = radio.readData(buf,0); 195 | break; 196 | } 197 | 198 | if (state == RADIOLIB_ERR_NONE) 199 | { 200 | // A LoRa packet was successfully received 201 | // Now process it. 202 | // 1. Get as much metadata from the radio as possible 203 | Telemetry.rxPacketLen = radio.getPacketLength(); 204 | Telemetry.rssi = radio.getRSSI(); 205 | Telemetry.snr = radio.getSNR(); 206 | Telemetry.frequency_error = radio.getFrequencyError(); 207 | 208 | // Get the frequency error and retune 209 | LoRaSettings.Frequency = LoRaSettings.Frequency - (Telemetry.frequency_error / 1000000); 210 | radio.setFrequency(LoRaSettings.Frequency); 211 | 212 | // Then get back to receiving 213 | startReceive(); 214 | 215 | // Flash the OLED display 216 | #if defined(USE_SSD1306) 217 | displayFlash(); 218 | #endif 219 | 220 | // Flash the LED 221 | #if defined(FLASH_PIN) 222 | flashPin(); 223 | #endif 224 | 225 | // Print lots of data 226 | Serial.println(); 227 | 228 | // Print the first 10 hex chars of the packet 229 | Serial.print("[RADIO] first 10 hex chars:\t"); 230 | for (int i = 0; i < 10; i++) 231 | { 232 | Serial.print(buf[i],HEX); 233 | Serial.print(" "); 234 | } 235 | Serial.println(); 236 | 237 | // Get the time from the ESP, so we have a timestamp 238 | formatLocalTime(); 239 | Telemetry.atmillis = millis(); 240 | 241 | // Process datapacket from the radio, print it to the serial port and store it in the telemetry struct 242 | Serial.print(F("[RADIO] Received packet:\t")); 243 | Serial.println(Telemetry.time_received); 244 | 245 | // Length of the latest packet that was received 246 | Serial.print(F("[RADIO] Packet length:\t\t")); 247 | Serial.println(Telemetry.rxPacketLen); 248 | 249 | // print RSSI (Received Signal Strength Indicator) 250 | Serial.print(F("[RADIO] RSSI:\t\t\t")); 251 | Serial.print(Telemetry.rssi); 252 | Serial.println(F(" dBm")); 253 | 254 | // print SNR (Signal-to-Noise Ratio) 255 | Serial.print(F("[RADIO] SNR:\t\t\t")); 256 | Serial.print(Telemetry.snr); 257 | Serial.println(F(" dB")); 258 | 259 | // print frequency error 260 | Serial.print(F("[RADIO] Frequency error:\t")); 261 | Serial.print(Telemetry.frequency_error); 262 | Serial.println(F(" Hz (Radio has been retuned)")); 263 | Telemetry.frequency = LoRaSettings.Frequency; 264 | 265 | // 2. Check the type of packet 266 | // SSDV ? 267 | if ( ((buf[0] & 0x7F) == 0x66) || ((buf[0] & 0x7F) == 0x67) || // SSDV 268 | ((buf[0] & 0x7F) == 0x68) || ((buf[0] & 0x7F) == 0x69) 269 | ) 270 | { 271 | LoRaSettings.packetType = PACKETTYPE_SSDV ; 272 | } 273 | else if (buf[0] == '$' && buf[1]=='$') // Telemetry 274 | { 275 | LoRaSettings.packetType = PACKETTYPE_TELEMETRY ; 276 | } 277 | else if (buf[0] == '<' && buf[1] == 0xff && buf[2] == 0x01) // APRS 278 | { 279 | LoRaSettings.packetType = PACKETTYPE_APRS ; 280 | } 281 | else 282 | { 283 | LoRaSettings.packetType = PACKETTYPE_UNKNOWN ; // UNKNOWN OR corrupted 284 | } 285 | 286 | // Print the packet type 287 | Serial.print(F("[RADIO] Packet type:\t\t")); 288 | switch (LoRaSettings.packetType) 289 | { 290 | case PACKETTYPE_SSDV: 291 | Serial.println("SSDV"); 292 | processSSDVPacket(buf); 293 | // Put the SSDV packet into the upload queue 294 | if (Telemetry.uploadSondehub) postSSDVinQueue(buf); 295 | addToLog(); 296 | break; 297 | case PACKETTYPE_TELEMETRY: 298 | Serial.println("TELEMETRY"); 299 | processTelemetryPacket(buf); 300 | addToLog(); 301 | break; 302 | case PACKETTYPE_APRS: 303 | Serial.println("LORA-APRS"); 304 | // Valid APRS packet 305 | parseAPRSPacket(buf); 306 | addToLog(); 307 | break; 308 | 309 | default: Serial.println("UNKNOWN"); break; 310 | } 311 | Serial.println(); 312 | } 313 | else if (state == RADIOLIB_ERR_CRC_MISMATCH) 314 | { 315 | startReceive(); // This packet is a dud, so start listening for the next one 316 | // packet was received, but is malformed 317 | Serial.println(F("[RADIO] CRC error - maybe adjust frequency a bit?")); 318 | } 319 | else 320 | { 321 | startReceive(); // This packet is a dud, so start listening for the next one 322 | // some other error occurred 323 | Serial.print(F("[RADIO] Failed, code ")); 324 | Serial.println(state); 325 | Telemetry.raw = "Invalid Packet"; 326 | } 327 | 328 | //Serial.print(F("\nTIME spent in receiveLoRa():\t\t")); 329 | //Serial.println(millis()- start); 330 | } 331 | 332 | 333 | /************************************************************************************ 334 | * Process a telemetry packet 335 | ************************************************************************************/ 336 | void processTelemetryPacket(byte *buf) 337 | { 338 | bool validPacket = true; 339 | int i =0; 340 | // Check if it is telemetry whenever the LoRaMode is not APRS 341 | if (LoRaSettings.LoRaMode < 99) 342 | { 343 | while (i < Telemetry.rxPacketLen-2 && buf[i] != '\n') 344 | { 345 | if (buf[i] < ' ' || buf[i] > '~') 346 | { 347 | validPacket = false; 348 | Telemetry.raw = "Received a telemetry packet but it is corrupted."; 349 | Serial.println(Telemetry.raw); 350 | } 351 | i++; 352 | } 353 | if (validPacket) 354 | { 355 | packetCounter++; 356 | getMetafromRaw((char *) buf); 357 | parseRawData((char *) buf); 358 | } 359 | } 360 | else 361 | { 362 | // APRS telemetry packet 363 | // Check if it is a valid LoRa-APRS packet 364 | if (buf[0] != '<' || buf[1] != 0xff || buf[2] != 0x01) 365 | { 366 | // not a LoRa-APRS packet 367 | Telemetry.raw = "Received a LoRa-APRS packet but it is not valid"; 368 | } 369 | // Extra check for validity of package: Read until end of package. It should all be printable ASCII 370 | for (i=3 ; i < Telemetry.rxPacketLen-1; i++) 371 | { 372 | if (buf[i] < ' ' || buf[i] > '~' ) 373 | { 374 | Telemetry.raw = "Received a LoRa-APRS packet but it is corrupted."; 375 | Serial.println(Telemetry.raw); 376 | } 377 | } 378 | } 379 | } 380 | 381 | 382 | /************************************************************************************ 383 | * Check if received packet is a LoRa HAB packet. 384 | * Should be updated later for all the different HAB packets 385 | ************************************************************************************/ 386 | bool checkIfHABPacket() 387 | { 388 | int i; 389 | int j; 390 | // Check if it is telemetry whenever the LoRaMode is not APRS 391 | if (LoRaSettings.LoRaMode < 99) 392 | { 393 | if (Telemetry.raw.indexOf("$$") != 0) 394 | { 395 | // not a HAB package 396 | Telemetry.raw = "Received a packet but it is not a HAB telemetry packet."; 397 | return false; 398 | } 399 | i = 0; 400 | j = 2; 401 | // Extra check for validity of package: Read until end of package. It should all be printable ASCII 402 | while (i < Telemetry.rxPacketLen-j && Telemetry.raw[i] != '\n') 403 | { 404 | if (Telemetry.raw[i] < ' ' || Telemetry.raw[i] > '~') 405 | { 406 | Serial.println(Telemetry.raw); 407 | Telemetry.raw = "Received a packet but it is corrupted."; 408 | return false; 409 | } 410 | i++; 411 | } 412 | } 413 | else 414 | { 415 | // Check if it is a LoRa-APRS packet 416 | if (Telemetry.raw.substring(0, 3) != "<\xff\x01") 417 | { 418 | // not a LoRa-APRS packet 419 | Telemetry.raw = "Received a LoRa-APRS packet but it is not valid"; 420 | return false; 421 | } 422 | i=3; 423 | j=1; 424 | // Extra check for validity of package: Read until end of package. It should all be printable ASCII 425 | for (i ; i < Telemetry.rxPacketLen-j; i++) 426 | { 427 | if (Telemetry.raw[i] < ' ' || Telemetry.raw[i] > '~' ) 428 | { 429 | Serial.println(Telemetry.raw); 430 | Telemetry.raw = "Received a packet but it is corrupted."; 431 | return false; 432 | } 433 | } 434 | } 435 | 436 | return true; 437 | } 438 | 439 | /************************************************************************************ 440 | * Change the RX frequency or one of the scanner frequencies 441 | ************************************************************************************/ 442 | bool changeFrequency(String newFrequency, int nr) 443 | { 444 | int str_len = newFrequency.length() + 1; 445 | char char_array[str_len]; 446 | newFrequency.toCharArray(char_array, str_len); 447 | TBScanner.scanFreq[nr] = atof(char_array); 448 | if (nr == 0) 449 | { 450 | LoRaSettings.Frequency = TBScanner.scanFreq[0]; 451 | } 452 | setupLoRa(); 453 | #if defined(USE_SSD1306) 454 | updateOLEDforFrequency(); 455 | #endif 456 | return true; 457 | } 458 | 459 | /************************************************************************************ 460 | * Change the frequency to the next scan frequencies 461 | ************************************************************************************/ 462 | void nextScanFrequency() 463 | { 464 | // find the next available frequency to scan 465 | TBScanner.currentNr++; 466 | while (TBScanner.scanFreq[TBScanner.currentNr] == 0.0) 467 | { 468 | TBScanner.currentNr++; 469 | if (TBScanner.currentNr > TBScanner.maxNr) 470 | { 471 | TBScanner.currentNr = 0; 472 | } 473 | } 474 | // Change the frequency 475 | LoRaSettings.Frequency = TBScanner.scanFreq[TBScanner.currentNr]; 476 | setupLoRa(); 477 | } 478 | 479 | 480 | /************************************************************************************ 481 | * Change LoRa Mode 482 | ************************************************************************************/ 483 | bool changeLoRaMode(int newMode) 484 | { 485 | if (newMode >= 0 && newMode != LoRaSettings.LoRaMode) 486 | { 487 | LoRaSettings.LoRaMode = newMode; 488 | setupLoRa(); 489 | return true; 490 | } 491 | else 492 | { 493 | return false; 494 | } 495 | } 496 | 497 | 498 | -------------------------------------------------------------------------------- /settings.h: -------------------------------------------------------------------------------- 1 | /*********************************************************************************** 2 | * SETTINGS 3 | * 4 | * It is important that you work through this file and and change the parameters 5 | * when needed so. 6 | * 7 | * Download latest version and instructions: https://github.com/RoelKroes/TBTracker-RX 8 | ************************************************************************************/ 9 | // Development flag 10 | // Set to false if you want the software to upload and store your telemetry in the sondehub database 11 | // Set to true if you only want to upload the telemetry but not store it in the database (recommended when testing) 12 | // If DEVFLAG is set to true, the Serial output will still let you know if an upload to Sondehub succeeded 13 | #define DEVFLAG false 14 | 15 | /*********************************************************************************** 16 | * WIFI SETTINGS 17 | * 18 | * you can define three SSID's with password here. 19 | * For example: your home network, you phone's mobile hotspot and maybe another network 20 | * You need to specify at least 1 SSID and password 21 | ************************************************************************************/ 22 | #define WIFI_SSID_1 "MySSID" 23 | #define WIFI_PASSWORD_1 "MyPassword" 24 | 25 | #define WIFI_SSID_2 "MyMobileHotspot" 26 | #define WIFI_PASSWORD_2 "MyOtherPassword" 27 | 28 | #define WIFI_SSID_3 "" 29 | #define WIFI_PASSWORD_3 "" 30 | 31 | /************************************************************************ 32 | * UPLOADER INFO 33 | * 34 | * Change this! 35 | * This is what is shown on Sondehub 36 | ************************************************************************************/ 37 | // Callsign to show up on Sondehub as the "Receiver" 38 | #define CALLSIGN "MYCALL" 39 | // Contact e-mail (optional) 40 | #define UPLOADER_EMAIL "somebody@somedomain.org" 41 | // Antenna used (optional) 42 | #define ANTENNA_USED "GP" 43 | // Radio used (optional) 44 | #define RADIO_USED "T-Beam" 45 | // Display the mobile flag (car icon) on the map 46 | #define I_AM_MOBILE false 47 | // Display a custom comment on sondehub. Just comment it out if you do not want a custom comment 48 | // #define PAYLOAD_COMMENT "TBTracker-RX test" 49 | // Latidude, longitude and altitude in meters of the uploader 50 | // Fill this in if you are not using a GPS 51 | #define UPL_LAT 52.0000 52 | #define UPL_LON 5.0000 53 | #define UPL_ALT 10 54 | // Set this to false if you don't want your postion uploaded to the sondenhub map 55 | #define UPLOAD_YOUR_POSITION true 56 | // Set this to false if you don't want the packet you received from the payload to upload to Sondehub 57 | #define UPLOAD_PAYLOAD_PACKET false 58 | 59 | /************************************************************************ 60 | * PIN NUMBERS for SX1278 61 | * 62 | * Change if needed 63 | ************************************************************************************/ 64 | // Below are the settings for a TTGO T-BEAM v1. Yours might be different! 65 | #define PIN_NSS 18 66 | #define PIN_DIO0 26 67 | #define PIN_RESET 23 68 | #define PIN_DIO1 33 69 | 70 | /************************************************************************ 71 | * PIN NUMBERS for optional GPS 72 | * 73 | * The GPS will use the Serial2 of the ESP32. 74 | * 75 | * Change the pins if needed 76 | ************************************************************************************/ 77 | // Remove or comment out the next line if your ESP32 does NOT has a GPS chip 78 | // Leave the define when you have a GPS connected to your esp32 79 | #define USE_GPS 80 | // Pin numbers for the optional GPS chip. 81 | // Older verions of the T-Beams use 12,15 82 | // Newer versions of the T-Beam use 12,34. 83 | // yours might be different! 84 | #define GPS_RX 34 85 | #define GPS_TX 12 86 | #define GPS_BAUD 9600 87 | // Uncomment to print GPS Debuging info 88 | //#define GPS_DEBUG 89 | // GPS on a T-beam can get turned off, use this: https://github.com/eriktheV-king/TTGO_T-beam_GPS-reset/tree/master to reset if no output 90 | 91 | /************************************************************************ 92 | * Parameters for the optional SSD1306 OLED panel which can be mounted 93 | * to a TTGO T-Beam or any other ESP32 94 | * 95 | * Change the if needed 96 | ************************************************************************************/ 97 | // Comment these lines out if you do not use a SSD1306 panel 98 | #define USE_SSD1306 99 | #define SCREEN_WIDTH 128 // OLED display width, in pixels 100 | #define SCREEN_HEIGHT 64 // OLED display height, in pixels 101 | #define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin) 102 | #define SCREEN_ADDRESS 0x3C // See datasheet for Address; Usually 0x3D or 0x3C 103 | /*********************************************************************************** 104 | * LORA SETTINGS 105 | * 106 | * Change when needed 107 | ************************************************************************************/ 108 | #define LORA_FREQUENCY 434.126 // Frequency the radio chip is listening 109 | #define LORA_BANDWIDTH 125.0 // Do not change, change LORA_MODE instead 110 | #define LORA_SPREADFACTOR 9 // Do not change, change LORA_MODE instead 111 | #define LORA_CODERATE 7 // Do not change, change LORA_MODE instead 112 | #define LORA_SYNCWORD 0x12 // for sx1278 113 | #define LORA_POWER 10 // in dBm between 2 and 17. 10 = 10mW (recommended), currently not used 114 | #define LORA_PREAMBLELENGTH 8 115 | #define LORA_GAIN 0 116 | 117 | 118 | // HAB modes 119 | // 0 = (normal for telemetry) Explicit mode, Error coding 4:8, Bandwidth 20.8kHz, SF 11, Low data rate optimize on 120 | // 1 = (normal for SSDV) Implicit mode, Error coding 4:5, Bandwidth 20.8kHz, SF 6, Low data rate optimize off 121 | // 2 = (normal for repeater) Explicit mode, Error coding 4:8, Bandwidth 62.5kHz, SF 8, Low data rate optimize off 122 | // 3 = (normal for fast SSDV) Explicit mode, Error coding 4:6, Bandwidth 250kHz, SF 7, Low data rate optimize off 123 | // 4 = Test mode not for normal use. - NUT SUPPORTED 124 | // 5 = (normal for calling mode) Explicit mode, Error coding 4:8, Bandwidth 41.7kHz, SF 11, Low data rate optimize off 125 | // 99 = (LoRa APRS) Explicit mode, Error coding 4:5, Bandwidth 125KHz, SF 12 experimental - Only receiving, no i-gating, or uploading. Requires Frequency set to 433.775 126 | 127 | // Default tracker mode = 2 128 | // If you set the radio to mode 99, be sure to set the frequency to 433.775, which is the standard for LoRa-APRS 129 | #define LORA_MODE 2 // Mode 2 is usually used for simple telemetry data for pico balloons 130 | #define LORA_MODULATION "LoRa Mode 2" // This string will be visible in Sondehub 131 | 132 | /*********************************************************************************** 133 | * FLASH PIN settings 134 | * 135 | * This pin will be set HIGH for 300ms when a new packet is received 136 | * You can for example attach a LED to it. 137 | * It can be any pin you find suitable on your board 138 | * 139 | * Uncomment and change the pin number when you do need a flash pin 140 | ************************************************************************************/ 141 | // #define FLASH_PIN 14 142 | 143 | /*********************************************************************************** 144 | * THIS SECTION IS NEW AS OF V0.0.11 145 | * i2c PIN and PMU settings 146 | * These are used if you have a Power Management chip present. 147 | * The PMU chips are usually built in the new T-BEAM v1.1 and v1.2 148 | * 149 | * These pins normally need no change 150 | ************************************************************************************/ 151 | //#define I2C_SDA 21 152 | //#define I2C_SCL 22 153 | // The middle button of your T-BEAM v1.1 or v1.2 154 | // #define PMU_IRQ_BTN 38 155 | 156 | /*********************************************************************************** 157 | * TIME SETTINGS 158 | * 159 | * Change when needed 160 | ************************************************************************************/ 161 | const char* ntpServer = "pool.ntp.org"; // URL for NTP server(s). We need an accurate date and time. 162 | 163 | /*********************************************************************************** 164 | * JSON SETTINGS 165 | * 166 | * This will probably need no change. 167 | * These are the URL's where the JSONS are uploaded to. 168 | ************************************************************************************/ 169 | String JSON_URL = "https://api.v2.sondehub.org/amateur/telemetry"; 170 | String JSON_URL_LISTENERS = "https://api.v2.sondehub.org/amateur/listeners"; 171 | -------------------------------------------------------------------------------- /ssd1306.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * All OLED SSD1306 related stuff 3 | ************************************************************************************/ 4 | #if defined(USE_SSD1306) 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | #define OLED_DEFAULT 0 11 | #define OLED_CHASE 1 12 | #define OLED_GOD 2 13 | 14 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 15 | int oledMode = OLED_DEFAULT; 16 | 17 | /************************************************************************************ 18 | * Initialize the SSD1306 19 | ************************************************************************************/ 20 | void setupSSD1306() 21 | { 22 | // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally 23 | if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) 24 | { 25 | Serial.println(F("SSD1306 allocation failed")); 26 | for(;;); // Don't proceed, loop forever 27 | } 28 | 29 | // Clear the buffer 30 | display.clearDisplay(); 31 | 32 | // Show the display buffer on the screen. You MUST call display() after 33 | // drawing commands to make them visible on screen! 34 | display.display(); 35 | } 36 | 37 | /************************************************************************************ 38 | * This function is called about once every 10 secs 39 | ************************************************************************************/ 40 | void timedOledUpdate() 41 | { 42 | // If time since last packet is one the screen, update the timing 43 | switch(oledMode) 44 | { 45 | case OLED_GOD: 46 | case OLED_CHASE: 47 | // clear half of the first line 48 | for (int y=0; y<=6; y++) 49 | { 50 | for (int x=0; x<64; x++) 51 | { 52 | display.drawPixel(x, y, BLACK); 53 | } 54 | } 55 | display.setTextSize(1); 56 | display.setTextColor(WHITE); 57 | display.setCursor(0, 0); 58 | display.print(getDuration(Telemetry.atmillis,true)); 59 | display.setCursor(97, 0); 60 | display.print("#"); display.print(packetCounter); 61 | display.display(); // Takes around 25ms 62 | break; 63 | } 64 | } 65 | 66 | 67 | /************************************************************************************ 68 | * display a Text message on the OLED SSD1306 69 | ************************************************************************************/ 70 | void displayOled(int X, int Y, const char* str) 71 | { 72 | display.setTextSize(1); 73 | display.setTextColor(WHITE); 74 | display.setCursor(X, Y); 75 | // Display static text 76 | display.print(str); 77 | display.display(); 78 | } 79 | 80 | /************************************************************************************ 81 | * clear the OLED display 82 | ************************************************************************************/ 83 | void displayClear() 84 | { 85 | // Clear the buffer 86 | display.clearDisplay(); 87 | display.display(); 88 | } 89 | 90 | /************************************************************************************ 91 | * clear the OLED mode 92 | ************************************************************************************/ 93 | bool changeOLEDMode(int aMode) 94 | { 95 | oledMode = aMode; 96 | // displayUpdate(); 97 | oledUpdateNeeded = true; 98 | return true; 99 | } 100 | 101 | /************************************************************************************ 102 | * display packet data on the OLED display 103 | ************************************************************************************/ 104 | void displayUpdate() 105 | { 106 | if (oledUpdateNeeded) 107 | { 108 | start = millis(); 109 | switch (oledMode) 110 | { 111 | case OLED_DEFAULT: 112 | display.clearDisplay(); 113 | display.setTextSize(1); 114 | display.setTextColor(WHITE); 115 | display.setCursor(0, 0); 116 | display.print("IP: "); 117 | display.println(WiFi.localIP().toString().c_str()); 118 | display.print("#"); display.println(packetCounter); 119 | display.print(" ID: "); display.println(Telemetry.payload_callsign); 120 | display.print("Frequency: "); display.println(Telemetry.frequency,3); 121 | display.print(" Altitude: "); display.println(Telemetry.alt,0); 122 | display.print(" Distance: "); display.println(Telemetry.distance,1); 123 | display.print(" Chase: "); display.println(Telemetry.compass); 124 | display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text 125 | display.print("TBTacker-RX "); display.print(TBTRACKER_VERSION); 126 | 127 | #if defined(USE_GPS) 128 | display.setTextColor(SSD1306_WHITE, SSD1306_BLACK); // Not inverse text 129 | display.setCursor(122, 0); 130 | if (gps_valid) 131 | display.print("G"); 132 | else 133 | display.print("X"); 134 | #endif 135 | 136 | display.display(); 137 | break; 138 | case OLED_CHASE: 139 | drawCompass(); 140 | break; 141 | case OLED_GOD: 142 | display.clearDisplay(); 143 | display.setTextSize(1); 144 | display.setTextColor(WHITE); 145 | display.setCursor(0, 0); 146 | display.print(getDuration(Telemetry.atmillis,true)); 147 | display.setCursor(97, 0); 148 | display.print("#"); display.print(packetCounter); 149 | display.drawLine(0, 10, 128, 10, WHITE); 150 | display.setCursor(0, 13); 151 | display.print(Telemetry.payload_callsign); 152 | display.setCursor(64, 13); 153 | display.print(Telemetry.frequency, 3); display.print("MHz"); 154 | display.setCursor(0, 23); 155 | display.print(Telemetry.alt, 0); display.print("m"); 156 | display.setCursor(64, 23); 157 | if (Telemetry.distance < 10) 158 | display.print(Telemetry.distance, 1); 159 | else 160 | display.print(Telemetry.distance, 0); 161 | display.print("km"); 162 | display.setCursor(105, 23); 163 | display.print(Telemetry.compass); 164 | display.setCursor(0, 33); 165 | display.print(Telemetry.batt, 2); display.print("V"); 166 | display.setCursor(64, 33); 167 | display.print(Telemetry.snr); display.print("dB"); 168 | display.setCursor(0, 43); 169 | display.print(Telemetry.temp, 1); display.print((char)247); display.print("C"); 170 | display.setCursor(64, 43); 171 | display.print(Telemetry.sats); display.print(" sats"); 172 | display.setCursor(0, 53); 173 | display.print(Telemetry.lat, 6); 174 | display.setCursor(64, 53); 175 | display.print(Telemetry.lon, 6); 176 | display.display(); 177 | break; 178 | } 179 | oledUpdateNeeded = false; 180 | //Serial.print(F("\nTIME spent in displayUpdate():\t\t")); 181 | //Serial.println(millis()- start); 182 | } 183 | } 184 | 185 | /************************************************************************************ 186 | * flash the OLED screen to show packet was received 187 | ************************************************************************************/ 188 | void displayFlash() 189 | { 190 | //display.clearDisplay(); 191 | //display.setTextColor(WHITE); 192 | //display.setTextSize(2); 193 | //display.setCursor(10, 27); 194 | //display.print("PACKET RX"); 195 | //display.display(); 196 | display.invertDisplay(true); 197 | flashMillis = millis(); 198 | } 199 | 200 | void disableFlash() 201 | { 202 | display.invertDisplay(false);; 203 | } 204 | 205 | 206 | /************************************************************************************ 207 | // Update the OLED with the correct frequency 208 | ************************************************************************************/ 209 | void updateOLEDforFrequency(void) 210 | { 211 | // Clear the display 212 | display.clearDisplay(); 213 | // Set the Text size 214 | display.setTextSize(1); 215 | // Set the text color 216 | display.setTextColor(WHITE); 217 | // Set the cursor 218 | display.setCursor(0, 0); 219 | 220 | // Line 1 221 | display.print("IP: "); display.println(WiFi.localIP().toString().c_str()); 222 | // Line 2 223 | display.println(); 224 | // Line 3 225 | display.println(); 226 | // Line 4 227 | display.println("Waiting for packets"); 228 | // Line 5 229 | display.print("on: "); 230 | display.print(String(LoRaSettings.Frequency,3).c_str()); 231 | display.println(" MHz"); 232 | // Line 6 233 | display.println(); 234 | // Line 7 235 | display.println(); 236 | // Invert the screen color 237 | display.setTextColor(SSD1306_BLACK, SSD1306_WHITE); // Draw 'inverse' text 238 | // Line 8 239 | display.print("TBTacker-RX "); display.println(TBTRACKER_VERSION); 240 | // Display everything 241 | display.display(); 242 | } 243 | 244 | /************************************************************************************ 245 | // Draw a compass on the OLED 246 | ************************************************************************************/ 247 | void drawCompass() 248 | { 249 | static int armLength = 22; 250 | static int arrowLength = 15; 251 | static int cx = 86; 252 | static int cy = 32; 253 | int armX, armY, arrow1X, arrow1Y, arrow2X, arrow2Y; 254 | int bearing = Telemetry.bearing; 255 | 256 | //convert degree to radian 257 | float bearingRad = bearing/57.2957795; // 1 Radian is 57.2957 degrees 258 | float arm1Rad = (bearing-15)/57.2957795; // calculate for little arrow arms +/- a few degrees. 259 | float arm2Rad = (bearing+15)/57.2957795; 260 | 261 | armX = armLength*cos(bearingRad); // use trig to get x and y values. x=hypotenuse*cos(angle in Rads) 262 | armY = -armLength*sin(bearingRad); // y = hypotenuse*sin(angle in Rads) 263 | 264 | arrow1X = arrowLength*cos(arm1Rad); // x and y offsets to draw the arrow bits 265 | arrow1Y = -arrowLength*sin(arm1Rad); 266 | arrow2X = arrowLength*cos(arm2Rad); // x and y offsets to draw the rest of the arrow bits 267 | arrow2Y = -arrowLength*sin(arm2Rad); 268 | 269 | display.clearDisplay(); 270 | // draw line, circle, and arrows 271 | display.drawLine(cx, cy, cx-armY, cy-armX, WHITE); // for some reason have to invert x and y to get correct compass heading 272 | //u8g2.drawLine(cx-armY, cy-armX, cx-arrow1Y, cy-arrow1X); // draw 1/2 of arrowhead 273 | //u8g2.drawLine(cx-armY, cy-armX, cx-arrow2Y, cy-arrow2X); 274 | display.drawTriangle(cx-armY, cy-armX, cx-arrow1Y, cy-arrow1X, cx-arrow2Y, cy-arrow2X, WHITE); 275 | //display.drawCircle(cx, cy, armLength, U8G2_DRAW_ALL, WHITE); 276 | display.drawCircle(cx, cy, armLength, WHITE); 277 | //display.drawCircle(cx, cy, 2, U8G2_DRAW_ALL); 278 | display.drawCircle(cx, cy, 2, WHITE); 279 | 280 | // Draw tick marks at each Compass point 281 | display.drawLine(cx, cy-(armLength-2), cx, cy-(armLength +2),WHITE); // North tick mark 282 | display.drawLine(cx, cy+(armLength-2), cx, cy+(armLength +2),WHITE); // South tick mark 283 | display.drawLine(cx-(armLength-2), cy, cx-(armLength+2), cy, WHITE); // West tick mark 284 | display.drawLine(cx+(armLength-2), cy, cx+(armLength+2), cy, WHITE); // East tick mark 285 | //u8g2.setFont(u8g_font_unifont); 286 | // display.setFont(u8g2_font_profont12_tf); //8 pixel font 287 | display.setTextColor(WHITE); 288 | 289 | // Label the Compass Directions 290 | display.setTextSize(1); 291 | display.setCursor(cx-2, cy-(armLength+9)); 292 | display.print("N"); 293 | display.setCursor(cx-2, cy+(armLength+3)); 294 | display.print("S"); 295 | display.setCursor(cx+(armLength+6), cy-3); 296 | display.print("E"); 297 | display.setCursor(cx-(armLength+9), cy-3); 298 | display.print("W"); 299 | 300 | // display time since last packet 301 | display.setTextSize(1); 302 | display.setTextColor(WHITE); 303 | display.setCursor(0, 0); 304 | display.print(getDuration(Telemetry.atmillis,true)); 305 | display.setCursor(97, 0); 306 | display.print("#"); display.print(packetCounter); 307 | display.display(); 308 | 309 | // Display altitude 310 | display.setCursor(0, 34); 311 | display.print(Telemetry.alt,0); display.print("m"); 312 | 313 | // Display the actual bearing in a larger font 314 | display.setTextSize(2); 315 | display.setCursor(0, 15); 316 | display.print(bearing,0); display.print((char)247); 317 | 318 | // display the distance 319 | display.setCursor(0,48); 320 | // Add a decimal if the distance is < 10km 321 | if (Telemetry.distance < 10) 322 | display.print(Telemetry.distance, 1); 323 | else 324 | display.print(Telemetry.distance, 0); 325 | display.print("km"); 326 | display.display(); 327 | } 328 | 329 | #endif 330 | -------------------------------------------------------------------------------- /ssdv.ino: -------------------------------------------------------------------------------- 1 | /*********************************************************************************** 2 | * SSDV related stuff 3 | ***********************************************************************************/ 4 | #define SSDV_MAX_CALLSIGN (6) /* Maximum number of characters in a callsign */ 5 | 6 | // Struct to hold LoRA settings 7 | struct TSSDVSettings 8 | { 9 | char callsign[SSDV_MAX_CALLSIGN+1]; 10 | uint8_t imageID; 11 | uint16_t packetID; 12 | } SSDVSettings; 13 | 14 | 15 | /*********************************************************************************** 16 | * Process the received SSDV packet 17 | ***********************************************************************************/ 18 | void processSSDVPacket(byte *buf) 19 | { 20 | uint32_t callsign_code; 21 | 22 | // Decode the callsign, which should be bytes 1, 2, 3 and 4 in BASE40 format. 23 | callsign_code = (buf[1] * 256 * 256 * 256) + (buf[2] * 256 * 256) + (buf[3] * 256) + buf[4]; 24 | decode_callsign(SSDVSettings.callsign, callsign_code); 25 | Telemetry.payload_callsign = SSDVSettings.callsign; 26 | // Decode the image ID 27 | SSDVSettings.imageID = buf[5]; 28 | SSDVSettings.packetID = buf[6]*256 + buf[7]; 29 | 30 | Telemetry.raw = "SSDV packet - callsign: "; 31 | Telemetry.raw += SSDVSettings.callsign; 32 | Telemetry.raw += " image ID: "; 33 | Telemetry.raw += SSDVSettings.imageID; 34 | Telemetry.raw += " packet ID: "; 35 | Telemetry.raw += SSDVSettings.packetID; 36 | 37 | Serial.println(Telemetry.raw); 38 | } 39 | 40 | /*********************************************************************************** 41 | * Decodes the callsign in the packet. BASE40 decode 42 | ***********************************************************************************/ 43 | static char *decode_callsign(char *callsign, uint32_t code) 44 | { 45 | char *c, s; 46 | 47 | *callsign = '\0'; 48 | 49 | /* Is callsign valid? */ 50 | if(code > 0xF423FFFF) return(callsign); 51 | 52 | for(c = callsign; code; c++) 53 | { 54 | s = code % 40; 55 | if(s == 0) *c = '-'; 56 | else if(s < 11) *c = '0' + s - 1; 57 | else if(s < 14) *c = '-'; 58 | else *c = 'A' + s - 14; 59 | code /= 40; 60 | } 61 | *c = '\0'; 62 | 63 | return(callsign); 64 | } 65 | 66 | 67 | /************************************************************************************ 68 | // Put the SSDV packet into the SSDV queue 69 | ************************************************************************************/ 70 | void postSSDVinQueue( byte* buf) 71 | { 72 | char packetBuf[256]; // Contains the hex encoded packet 73 | 74 | // Put the received SSDV packet into a temporary buffer and add a sync byte 75 | packetBuf[0] = 0x55; // Sync Byte; 76 | for (int i = 1; i < 256; i++) 77 | { 78 | packetBuf[i] = buf[i-1]; 79 | } 80 | // Add the packet to the queue. do not wait if thge queue is full 81 | if (ssdv_Queue != NULL) 82 | { 83 | if (xQueueSend(ssdv_Queue, packetBuf, 0) == pdPASS) 84 | { 85 | Telemetry.uploadResult = "Packet added to upload queue."; 86 | } 87 | else 88 | { 89 | Telemetry.uploadResult = "Could not upload. Queue is full."; 90 | } 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /utils.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * some useful utilities 3 | ************************************************************************************/ 4 | 5 | /************************************************************************************ 6 | * Will return a string that is used to display the time since the last reception 7 | * of a valid packet. 8 | ************************************************************************************/ 9 | String getDuration(unsigned long lastReceived, bool shortStr) 10 | { 11 | String res=""; 12 | unsigned long durationMillis = millis() - lastReceived; 13 | unsigned long seconds = durationMillis / 1000; 14 | unsigned long minutes = seconds / 60; 15 | unsigned long hours = minutes / 60; 16 | unsigned long days = hours / 24; 17 | res.concat("("); 18 | if (days > 0) 19 | { 20 | res += days; 21 | if (shortStr) 22 | res.concat(" days)"); 23 | else 24 | res.concat(" days ago)"); 25 | return res; 26 | } 27 | 28 | if (hours > 0) 29 | { 30 | res += hours; 31 | if (shortStr) 32 | res.concat("hr ago)"); 33 | else 34 | res.concat(" hours ago)"); 35 | return res; 36 | } 37 | 38 | if (minutes > 0) 39 | { 40 | res += minutes; 41 | if (shortStr) 42 | res.concat("m ago)"); 43 | else 44 | res.concat(" minutes ago)"); 45 | return res; 46 | } 47 | 48 | if (seconds > 0) 49 | { 50 | res += seconds; 51 | if (shortStr) 52 | res.concat("s ago)"); 53 | else 54 | res.concat(" seconds ago)"); 55 | return res; 56 | } 57 | 58 | return ""; 59 | } 60 | 61 | #if defined(FLASH_PIN) 62 | /************************************************************************************ 63 | * Setup the flash pin if it was defined in the settings file 64 | ************************************************************************************/ 65 | void setupFlashPin() 66 | { 67 | pinMode(FLASH_PIN, OUTPUT); 68 | digitalWrite(FLASH_PIN,LOW); 69 | } 70 | 71 | /************************************************************************************ 72 | * enable the FLASH_PIN for 300ms 73 | ************************************************************************************/ 74 | void flashPin() 75 | { 76 | digitalWrite(FLASH_PIN, HIGH); 77 | pinMillis = millis(); 78 | } 79 | 80 | /************************************************************************************ 81 | * disable the FLASH_PIN 82 | ************************************************************************************/ 83 | void disablePin() 84 | { 85 | digitalWrite(FLASH_PIN, LOW); 86 | } 87 | 88 | #endif -------------------------------------------------------------------------------- /webserver.ino: -------------------------------------------------------------------------------- 1 | /************************************************************************************ 2 | * All webserver related stuff 3 | ************************************************************************************/ 4 | #include 5 | 6 | // webserver on standard port 80. But can be any port. 7 | AsyncWebServer server(80); 8 | 9 | void notFound(AsyncWebServerRequest *request); 10 | 11 | /************************************************************************************ 12 | * This is the code for the HTML page as a RAW literal. You can change this if you want 13 | * Basically it is a template that contains variables which will be replaced by actual 14 | * values when sent to the client. 15 | * 16 | * The variables in the templates look like %VARIABLE% and will be replaced with actual 17 | * values in the function "processor" 18 | ************************************************************************************/ 19 | const char index_html[] PROGMEM = R"rawliteral( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 33 | 34 | TBTracker-RX 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
%TBTRACKERRX%
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 97 | 98 | 99 |
Current frequency:%FREQUENCY%
LoRa Mode:%LORAMODE% 55 |
56 | 65 | 66 |
67 |
Your callsign:%CALLSIGN% 
Your location:Google Maps
Upload to Sondehub:%SONDEHUB%
Test mode:%DEVFLAG%
OLED Mode:%OLEDMODE% 87 |
88 | 94 | 95 |
96 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
LATEST PACKET %TIMESINCE%
108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 |
Payload ID:%PAYLOADID% (SNR: %SNR% dB, RSSI: %RSSI% dBm, FREQ.ERR.: %FERROR% Hz)
Telemetry:%TELEMETRY%
Location:Google Maps
Altitude (m):%ALTITUDE%
Distance (km):%DISTANCE%
Bearing (degr.):%BEARING% - travel %COMPASS% to chase
Upload result:%UPLOADRESULT%
134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
LAST PACKETS
142 | 143 | 144 | 145 | %LOGHISTORY% 146 | 147 |
148 | 149 |

150 | (Page will autoload every 20 seconds. If not, press the button) 151 |

152 | 153 | 154 | )rawliteral"; 155 | 156 | /************************************************************************************ 157 | * The function that replaces the variables from the template with real values 158 | ************************************************************************************/ 159 | String processor(const String& var) 160 | { 161 | if (var == "TBTRACKERRX") 162 | return "TBTRACKER-RX " + String(TBTRACKER_VERSION); 163 | else if (var == "FREQUENCY") 164 | return String(LoRaSettings.Frequency,3); 165 | else if (var == "DEVFLAG") 166 | if (devflag) return String("Yes"); 167 | else return("No"); 168 | else if (var == "CALLSIGN") 169 | return String(CALLSIGN); 170 | else if (var == "TELEMETRY") 171 | return Telemetry.raw; 172 | else if (var == "PAYLOADID") 173 | return Telemetry.payload_callsign; 174 | else if (var == "PAYLOADLOCATION") 175 | return String(Telemetry.lat,5) + "," + String(Telemetry.lon,5); 176 | else if (var == "ALTITUDE") 177 | return String(Telemetry.alt,0); 178 | else if (var == "DISTANCE") 179 | return String(Telemetry.distance,2); 180 | else if (var == "BEARING") 181 | return String(Telemetry.bearing,0); 182 | else if (var == "SNR") 183 | return String(Telemetry.snr); 184 | else if (var == "LOGHISTORY") 185 | return getLogs(); 186 | else if (var == "RSSI") 187 | return String(Telemetry.rssi); 188 | else if (var == "FERROR") 189 | return String(Telemetry.frequency_error); 190 | else if (var == "LORAMODE") 191 | return String(LoRaSettings.LoRaMode); 192 | else if (var == "SCAN1") 193 | return String(TBScanner.scanFreq[1],3); 194 | else if (var == "SCAN2") 195 | return String(TBScanner.scanFreq[2],3); 196 | else if (var == "SCAN3") 197 | return String(TBScanner.scanFreq[3],3); 198 | else if (var == "SCAN4") 199 | return String(TBScanner.scanFreq[4],3); 200 | 201 | #if defined(USE_SSD1306) 202 | else if (var == "OLEDMODE") 203 | { 204 | switch (oledMode) 205 | { 206 | case OLED_CHASE: 207 | return String("Chase"); 208 | break; 209 | case OLED_GOD: 210 | return String("See it all"); 211 | break; 212 | default: 213 | return String("Default"); 214 | break; 215 | } 216 | } 217 | #endif 218 | else if (var == "TIMESINCE") 219 | { 220 | if (Telemetry.atmillis > 0) 221 | return getDuration(Telemetry.atmillis,false); 222 | else 223 | return ""; 224 | } 225 | else if (var == "UPLOADRESULT") 226 | { 227 | if (Telemetry.uploadSondehub) 228 | return Telemetry.uploadResult; 229 | } 230 | else if (var == "COMPASS") 231 | return String(Telemetry.compass); 232 | else if (var == "LOCATION") 233 | { 234 | #if defined(USE_GPS) 235 | processGPSData(); 236 | return String(Telemetry.uploader_position[0],5) + "," + String(Telemetry.uploader_position[1],5); 237 | #else 238 | return String(UPL_LAT) + "," + String(UPL_LON); 239 | #endif 240 | } 241 | else if (var == "SONDEHUB") 242 | if (Telemetry.uploadSondehub) 243 | return String("Yes"); 244 | else 245 | return String("No"); 246 | 247 | return String(); 248 | 249 | } 250 | 251 | /************************************************************************************ 252 | * HTML template for when the user requests a non existing page 253 | ************************************************************************************/ 254 | void notFound(AsyncWebServerRequest *request) 255 | { 256 | request->send(404, "text/plain", "Not found"); 257 | } 258 | 259 | /************************************************************************************ 260 | * Setting up the webserver with logic when a specific page is requested. 261 | ************************************************************************************/ 262 | void setupWebserver() 263 | { 264 | 265 | server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) 266 | { 267 | request->send_P(200, "text/html", index_html, processor); 268 | }); 269 | 270 | server.on("/get2", HTTP_GET, [](AsyncWebServerRequest *request) 271 | { 272 | if (Telemetry.uploadSondehub) 273 | Telemetry.uploadSondehub = false; 274 | else 275 | Telemetry.uploadSondehub = true; 276 | request->send(200, "text/html", "Upload to Sondehub was changed.
Return to Home Page"); 277 | }); 278 | 279 | server.on("/get6", HTTP_GET, [](AsyncWebServerRequest *request) 280 | { 281 | postStationToServer(); 282 | request->send(200, "text/html", "Your position was uploaded to Sondehub.
Return to Home Page"); 283 | }); 284 | 285 | 286 | server.on("/get3", HTTP_GET, [](AsyncWebServerRequest *request) 287 | { 288 | if (devflag) 289 | devflag = false; 290 | else 291 | devflag = true; 292 | request->send(200, "text/html", "Development flag was changed.
Return to Home Page"); 293 | }); 294 | 295 | server.on("/getscan", HTTP_GET, [](AsyncWebServerRequest *request) 296 | { 297 | changeFrequency(request->getParam("scan1")->value(),1); 298 | changeFrequency(request->getParam("scan2")->value(),2); 299 | changeFrequency(request->getParam("scan3")->value(),3); 300 | changeFrequency(request->getParam("scan4")->value(),4); 301 | 302 | request->send(200, "text/html", "Scanning frequencies were changed.
Return to Home Page"); 303 | }); 304 | 305 | server.on("/get4", HTTP_GET, [](AsyncWebServerRequest *request) 306 | { 307 | String lMode; 308 | if (request->hasParam("modes")) 309 | { 310 | lMode = request->getParam("modes")->value(); 311 | if (changeLoRaMode(lMode.toInt())) 312 | { 313 | request->send(200, "text/html", "LoRa mode was changed.
Return to Home Page"); 314 | } 315 | else 316 | { 317 | request->send(200, "text/html", "LoRa mode was NOT changed.
Return to Home Page"); 318 | } 319 | 320 | } 321 | request->send(200, "text/html", "LoRa mode was changed.
Return to Home Page"); 322 | }); 323 | 324 | 325 | #if defined(USE_SSD1306) 326 | server.on("/get5", HTTP_GET, [](AsyncWebServerRequest *request) 327 | { 328 | String oledMode; 329 | if (request->hasParam("oledmodes")) 330 | { 331 | oledMode = request->getParam("oledmodes")->value(); 332 | if (changeOLEDMode(oledMode.toInt())) 333 | { 334 | request->send(200, "text/html", "OLED mode was changed.
Return to Home Page"); 335 | } 336 | else 337 | { 338 | request->send(200, "text/html", "OLED mode was NOT changed.
Return to Home Page"); 339 | } 340 | 341 | } 342 | request->send(200, "text/html", "OLED mode was changed.
Return to Home Page"); 343 | }); 344 | #endif 345 | 346 | server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) 347 | { 348 | String inputMessage; 349 | String inputParam; 350 | 351 | // GET input1 value on /get?input1= 352 | if (request->hasParam("frequency")) 353 | { 354 | inputMessage = request->getParam("frequency")->value(); 355 | inputParam = "frequency"; 356 | } 357 | Serial.print("Frequency changed to: "); 358 | Serial.println(inputMessage); 359 | // Try to change the frequency 360 | if ( changeFrequency(inputMessage,0) ) 361 | { 362 | request->send(200, "text/html", "Frequency changed to " + inputMessage + "
Return to Home Page"); 363 | } 364 | else 365 | { 366 | request->send(200, "text/html", "ERROR changing frequency to " + inputMessage + "
Return to Home Page"); 367 | } 368 | }); 369 | 370 | server.onNotFound(notFound); 371 | server.begin(); 372 | } 373 | -------------------------------------------------------------------------------- /wifi.ino: -------------------------------------------------------------------------------- 1 | /*********************************************************************************** 2 | * TBTracker-RX uses Wifi to send JSON data to Sondehub 3 | * 4 | * You can add multiple WiFi networks here. 5 | * TBTracker-RX will choose the strongest one 6 | ***********************************************************************************/ 7 | #include "time.h" 8 | #include 9 | 10 | WiFiMulti WiFiMulti; 11 | 12 | /************************************************************************************ 13 | * WIFI setup 14 | ************************************************************************************/ 15 | void setupWifi() 16 | { 17 | Serial.println(); Serial.println(); 18 | 19 | #if defined(USE_SSD1306) 20 | displayClear(); 21 | displayOled(0,0,"Searching for WiFi..."); 22 | #endif 23 | 24 | // Set WiFi in Station mode 25 | WiFi.mode(WIFI_STA); 26 | 27 | // Add the WiFi networks as defined in the settings file 28 | if (WIFI_SSID_1 != "") WiFiMulti.addAP(WIFI_SSID_1, WIFI_PASSWORD_1); 29 | if (WIFI_SSID_2 != "") WiFiMulti.addAP(WIFI_SSID_2, WIFI_PASSWORD_2); 30 | if (WIFI_SSID_3 != "") WiFiMulti.addAP(WIFI_SSID_3, WIFI_PASSWORD_3); 31 | 32 | // WiFi network Scan 33 | Serial.print("[WiFi] Scanning WiFi networks..."); 34 | // WiFi.scanNetworks will return the number of networks found 35 | int n = WiFi.scanNetworks(); 36 | Serial.println("scan done."); 37 | if (n == 0) 38 | { 39 | Serial.println("no networks found"); 40 | } 41 | else 42 | { 43 | Serial.print(n); Serial.println(" networks found"); 44 | for (int i = 0; i < n; ++i) 45 | { 46 | // Print SSID and RSSI for each network found 47 | Serial.print(i + 1); 48 | Serial.print(": "); 49 | Serial.print(WiFi.SSID(i)); 50 | Serial.print(" ("); 51 | Serial.print(WiFi.RSSI(i)); 52 | Serial.print(")"); 53 | Serial.println((WiFi.encryptionType(i) == WIFI_AUTH_OPEN)?" ":"*"); 54 | delay(10); 55 | } 56 | } 57 | 58 | // Try to connect to the stongest know access point 59 | Serial.print(F("[WiFi} Connecting to WiFi, please wait.")); 60 | #if defined(USE_SSD1306) 61 | displayClear(); 62 | displayOled(0,0,"Connecting to WiFi..."); 63 | #endif 64 | int loopCounter = 0; 65 | while (WiFiMulti.run() != WL_CONNECTED && loopCounter < 25) 66 | { 67 | delay(500); 68 | Serial.print("."); 69 | loopCounter++; 70 | } 71 | 72 | if (WiFi.status() == WL_CONNECTED) 73 | { 74 | Serial.println(""); 75 | Serial.print(F("[WiFi} WiFi connected to: ")); Serial.println(WiFi.SSID()); 76 | Serial.print(F("[WiFi} IP address: ")); Serial.println(WiFi.localIP()); 77 | WiFi.setAutoReconnect(true); 78 | WiFi.persistent(true); 79 | } 80 | else 81 | { 82 | Serial.println(); 83 | Serial.println(F("Could not connect to WiFi. No uploading possible.")); 84 | } 85 | } 86 | 87 | 88 | /************************************************************************************ 89 | * Format the local time in UTC 90 | ************************************************************************************/ 91 | void formatLocalTime() 92 | { 93 | 94 | if(!getLocalTime(&timeinfo)) 95 | { 96 | Serial.println(F("Failed to obtain time")); 97 | return; 98 | } 99 | strftime(Telemetry.time_received,sizeof(Telemetry.time_received),"%Y-%m-%dT%T.000000Z",&timeinfo); 100 | } 101 | 102 | 103 | /************************************************************************************ 104 | * Get time information from the NTP server 105 | ************************************************************************************/ 106 | void updateTime() 107 | { 108 | if (WiFi.status() == WL_CONNECTED) 109 | { 110 | //init and get the time. Use UTC 111 | configTime(0, 0, ntpServer); 112 | } 113 | } 114 | --------------------------------------------------------------------------------