├── Lora-TTNMapper-Heltec.ino ├── README.md ├── TTN-decoder.script ├── ttnmapper_esp32_01.jpg └── ttnmapper_esp32_02.jpg /Lora-TTNMapper-Heltec.ino: -------------------------------------------------------------------------------- 1 | /* 2 | ----------------------------------------------------------------------------------------------------------- 3 | TTNMapper node with GPS running on an Heltec "WiFi Lora32 v2": https://heltec.org/project/wifi-lora-32/ 4 | 5 | Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman: https://github.com/matthijskooijman/arduino-lmic/blob/master/examples/ttn-abp/ttn-abp.ino 6 | Copyright (c) 2018 sbiermann: https://github.com/sbiermann/Lora-TTNMapper-ESP32 7 | Copyright (c) 2019 sistemasorp: https://github.com/sistemasorp/Heltec-Wifi-Lora-32-TTN-Mapper 8 | Copyright (c) 2020 Stefan Onderka: https://www.onderka.com/computer-und-netzwerk/ttnmapper-node-mit-heltec-sx1276-lora-esp32-v2 9 | Case: https://www.thingiverse.com/thing:4145143 10 | 11 | This code expects a serial connected GPS module like an U-Blox NEO-6m, you may change baud rate and pins. 12 | Default is GPIO 2 Rx and GPIO 17 Tx (Closest to "end" of board), speed 9600 Baud. Use 3V3 or Vext for GPS power 13 | 14 | Set your gateway GPS coordinates to show distance and direction when mapping. 15 | 16 | The activation method of the TTN device has to be ABP. 17 | 18 | Libraries needed: U8x8 (From U8g2), TinyGPS++, IBM LMIC, SPI, Wifi 19 | ----------------------------------------------------------------------------------------------------------- 20 | */ 21 | 22 | /* Hardware serial */ 23 | #include 24 | /* GPS */ 25 | #include 26 | /* LoraMAC-in-C */ 27 | #include 28 | /* Hardware abstraction layer */ 29 | #include 30 | /* SPI */ 31 | #include 32 | /* OLED */ 33 | #include 34 | /* Wireless */ 35 | #include "WiFi.h" 36 | 37 | /* Integrated LED (white) */ 38 | #define BUILTIN_LED 25 39 | /* GPS Rx pin */ 40 | #define GPS_RX 17 41 | /* GPS Tx pin */ 42 | #define GPS_TX 2 43 | /* GPS baud rate */ 44 | #define GPS_BAUD 9600 45 | 46 | /* GPS coordinates of mapped gateway for calculating the distance */ 47 | const double HOME_LAT = 49.000000; 48 | const double HOME_LNG = 11.000000; 49 | 50 | /* Initial sending interval in seconds */ 51 | const unsigned TX_INTERVAL = 15; 52 | 53 | /* Upper TinyGPS++ HDOP value limit to send Pakets with */ 54 | int HDOP_MAX = 300; // Set to 200-500 (HDOP 2.00 - 5.00) 55 | 56 | /* Time to wait in seconds if HDOP is above limit */ 57 | const unsigned TX_WAIT_INTERVAL = 5; 58 | 59 | /* Define serial for GPS */ 60 | HardwareSerial GPSSerial(1); 61 | 62 | /* Init GPS */ 63 | TinyGPSPlus gps; 64 | 65 | /* Define OLED (RST, SCL, SDA - See pinout) */ 66 | U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(15, 4, 16); 67 | 68 | /* LoRaWAN network session key */ 69 | static const PROGMEM u1_t NWKSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 70 | /* LoRaWAN application session key */ 71 | static const u1_t PROGMEM APPSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 72 | /* LoRaWAN device address */ 73 | static const u4_t DEVADDR = 0xffffffff; 74 | 75 | /* These callbacks are only used in over-the-air activation, so they are left empty here. We cannot */ 76 | /* leave them out completely unless DISABLE_JOIN is set in config.h, otherwise the linker will complain */ 77 | void os_getArtEui (u1_t* buf) { } 78 | void os_getDevEui (u1_t* buf) { } 79 | void os_getDevKey (u1_t* buf) { } 80 | 81 | static osjob_t sendjob; 82 | 83 | /* Pin mapping */ 84 | const lmic_pinmap lmic_pins = { 85 | .nss = 18, 86 | .rxtx = LMIC_UNUSED_PIN, 87 | .rst = 14, 88 | .dio = {26, 34, 35}, /* Board V2 */ 89 | }; 90 | 91 | /* Buffer with data to send to TTNMapper */ 92 | uint8_t txBuffer[9]; 93 | 94 | /* Buffer for sending to serial line*/ 95 | char sbuf[16]; 96 | 97 | void build_packet() { 98 | /* For sprintf to OLED display */ 99 | char s[16]; 100 | String toLog; 101 | 102 | uint32_t LatitudeBinary, LongitudeBinary; 103 | uint16_t altitudeGps; 104 | uint8_t hdopGps; 105 | 106 | while (GPSSerial.available()) 107 | gps.encode(GPSSerial.read()); 108 | 109 | /* Get GPS data */ 110 | double latitude = gps.location.lat(); 111 | double longitude = gps.location.lng(); 112 | double altitude = gps.altitude.meters(); 113 | uint32_t hdop = gps.hdop.value(); 114 | 115 | /* For OLED display */ 116 | float hdopprint = (float)hdop / 100.0; 117 | uint32_t sats = gps.satellites.value(); 118 | uint32_t characters = gps.charsProcessed(); 119 | uint32_t fixes = gps.sentencesWithFix(); 120 | double failedchecksums = gps.failedChecksum(); 121 | double passedchecksums = gps.passedChecksum(); 122 | float speed = gps.speed.kmph(); 123 | 124 | /* Serial output */ 125 | Serial.println(); 126 | 127 | /* Distance to GW in Meters */ 128 | double fromhome = gps.distanceBetween(gps.location.lat(), gps.location.lng(), HOME_LAT, HOME_LNG); 129 | 130 | /* Course to GW */ 131 | double direction_home = gps.courseTo(gps.location.lat(), gps.location.lng(), HOME_LAT, HOME_LNG); 132 | 133 | Serial.print("Latitude: "); 134 | sprintf(s, "%f", latitude); 135 | Serial.println(s); 136 | //Serial.println(latitude); 137 | Serial.print("Longitude: "); 138 | sprintf (s, "%f", longitude); 139 | Serial.println(s); 140 | //Serial.println(longitude); 141 | Serial.print("Altitude (m): "); 142 | Serial.println(altitude); 143 | Serial.print("Speed (km/h): "); 144 | Serial.println(speed); 145 | Serial.print("HDOP: "); 146 | Serial.println(hdopprint); 147 | Serial.print("Satellite count: "); 148 | Serial.println(sats); 149 | if ( fromhome < 1000) { 150 | // Less than 1000 m 151 | Serial.print("Distance to GW (m): "); 152 | Serial.println(fromhome); 153 | } else { 154 | // More than 1 km 155 | Serial.print("Distance to GW (km): "); 156 | Serial.println(fromhome/1000); 157 | } 158 | Serial.print("Direction to GW: "); 159 | Serial.print((String)gps.cardinal(direction_home) + " (" + (String)direction_home + "°)"); 160 | 161 | /* 162 | Serial.print("Characters processed: "); 163 | Serial.println(characters); 164 | Serial.print("Sentences with fix: "); 165 | Serial.println(fixes); 166 | Serial.print("Checksum passed: "); 167 | Serial.println(passedchecksums); 168 | Serial.print("Checksum failed: "); 169 | Serial.println(failedchecksums); 170 | */ 171 | 172 | LatitudeBinary = ((latitude + 90) / 180.0) * 16777215; 173 | LongitudeBinary = ((longitude + 180) / 360.0) * 16777215; 174 | 175 | /* OLED output */ 176 | // Latitude 177 | u8x8.clearLine(1); 178 | u8x8.setCursor(0, 1); 179 | u8x8.print("Lati "); 180 | u8x8.setCursor(5, 1); 181 | sprintf(s, "%f", latitude); 182 | u8x8.print(s); 183 | 184 | // Longitude 185 | u8x8.clearLine(2); 186 | u8x8.setCursor(0, 2); 187 | u8x8.print("Long "); 188 | u8x8.setCursor(5, 2); 189 | sprintf(s, "%f", longitude); 190 | u8x8.print(s); 191 | 192 | // Altitude 193 | u8x8.clearLine(3); 194 | u8x8.setCursor(0, 3); 195 | u8x8.print("Alti "); 196 | u8x8.setCursor(5, 3); 197 | sprintf(s, "%f", altitude); 198 | u8x8.print(s); 199 | 200 | // Number of fixes 201 | /* 202 | u8x8.clearLine(4); 203 | u8x8.setCursor(0, 4); 204 | u8x8.print("Fixs "); 205 | u8x8.setCursor(5, 4); 206 | u8x8.print(fixes); 207 | */ 208 | 209 | // Distance from home GW 210 | u8x8.clearLine(4); 211 | u8x8.setCursor(0, 4); 212 | u8x8.print("Dist "); 213 | u8x8.setCursor(5, 4); 214 | if ( fromhome < 1000) { 215 | // Less than 1000 m 216 | sprintf(s, "%.0f m", fromhome); 217 | // More than 1 km 218 | } else { 219 | sprintf(s, "%.3f km", fromhome/1000); 220 | } 221 | u8x8.print(s); 222 | 223 | // HDOP 224 | u8x8.clearLine(5); 225 | u8x8.setCursor(0, 5); 226 | u8x8.print("HDOP "); 227 | u8x8.setCursor(5, 5); 228 | u8x8.print(hdopprint); 229 | 230 | // Number of satellites 231 | u8x8.clearLine(6); 232 | u8x8.setCursor(0, 6); 233 | u8x8.print("Sats "); 234 | u8x8.setCursor(5, 6); 235 | u8x8.print(sats); 236 | 237 | txBuffer[0] = ( LatitudeBinary >> 16 ) & 0xFF; 238 | txBuffer[1] = ( LatitudeBinary >> 8 ) & 0xFF; 239 | txBuffer[2] = LatitudeBinary & 0xFF; 240 | 241 | txBuffer[3] = ( LongitudeBinary >> 16 ) & 0xFF; 242 | txBuffer[4] = ( LongitudeBinary >> 8 ) & 0xFF; 243 | txBuffer[5] = LongitudeBinary & 0xFF; 244 | 245 | altitudeGps = altitude; 246 | txBuffer[6] = ( altitudeGps >> 8 ) & 0xFF; 247 | txBuffer[7] = altitudeGps & 0xFF; 248 | 249 | hdopGps = hdop/10; 250 | txBuffer[8] = hdopGps & 0xFF; 251 | 252 | toLog = ""; 253 | for(size_t i = 0; i EV_SCAN_TIMEOUT")); 271 | u8x8.drawString(0, 7, "EV_SCAN_TIMEOUT "); 272 | break; 273 | case EV_BEACON_FOUND: 274 | Serial.println(F("> EV_BEACON_FOUND")); 275 | u8x8.drawString(0, 7, "EV_BEACON_FOUND "); 276 | break; 277 | case EV_BEACON_MISSED: 278 | Serial.println(F("> EV_BEACON_MISSED")); 279 | u8x8.drawString(0, 7, "EV_BEACON_MISSED"); 280 | break; 281 | case EV_BEACON_TRACKED: 282 | Serial.println(F("> EV_BEACON_TRACKED")); 283 | u8x8.drawString(0, 7, "EV_BEACON_TRACKD"); 284 | break; 285 | case EV_JOINING: 286 | Serial.println(F("> EV_JOINING")); 287 | u8x8.drawString(0, 7, "EV_JOINING "); 288 | break; 289 | case EV_JOINED: 290 | Serial.println(F("> EV_JOINED")); 291 | u8x8.drawString(0, 7, "EV_JOINED "); 292 | /* Disable link check validation (automatically enabled during join, but not supported by TTN at this time). */ 293 | LMIC_setLinkCheckMode(0); 294 | break; 295 | case EV_RFU1: 296 | Serial.println(F("> EV_RFU1")); 297 | u8x8.drawString(0, 7, "EV_RFUI "); 298 | break; 299 | case EV_JOIN_FAILED: 300 | Serial.println(F("> EV_JOIN_FAILED")); 301 | u8x8.drawString(0, 7, "EV_JOIN_FAILED "); 302 | break; 303 | case EV_REJOIN_FAILED: 304 | Serial.println(F("> EV_REJOIN_FAILED")); 305 | u8x8.drawString(0, 7, "EV_REJOIN_FAILED"); 306 | //break; 307 | break; 308 | case EV_TXCOMPLETE: 309 | Serial.println(F("> EV_TXCOMPLETE (including wait for RX window)")); 310 | u8x8.drawString(0, 7, "EV_TXCOMPLETE "); 311 | digitalWrite(BUILTIN_LED, LOW); 312 | if (LMIC.txrxFlags & TXRX_ACK) { 313 | /* Received ACK */ 314 | Serial.println(F("Received ack")); 315 | u8x8.drawString(0, 7, "ACK RECEIVED "); 316 | } 317 | if (LMIC.dataLen) { 318 | /* Received data */ 319 | Serial.println(F("Received ")); 320 | Serial.println(LMIC.dataLen); 321 | Serial.println(F(" bytes payload")); 322 | 323 | u8x8.clearLine(6); 324 | u8x8.drawString(0, 6, "RX "); 325 | u8x8.setCursor(3, 6); 326 | u8x8.printf("%i bytes", LMIC.dataLen); 327 | 328 | u8x8.setCursor(0, 7); 329 | u8x8.printf("RSSI %d SNR %.1d", LMIC.rssi, LMIC.snr); 330 | } 331 | /* Schedule next transmission in TX_INTERVAL seconds */ 332 | os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send); 333 | break; 334 | case EV_LOST_TSYNC: 335 | Serial.println(F("> EV_LOST_TSYNC")); 336 | u8x8.drawString(0, 7, "EV_LOST_TSYNC "); 337 | break; 338 | case EV_RESET: 339 | Serial.println(F("> EV_RESET")); 340 | u8x8.drawString(0, 7, "EV_RESET "); 341 | break; 342 | case EV_RXCOMPLETE: 343 | Serial.println(F("> EV_RXCOMPLETE")); 344 | u8x8.drawString(0, 7, "EV_RXCOMPLETE "); 345 | break; 346 | case EV_LINK_DEAD: 347 | Serial.println(F("> EV_LINK_DEAD")); 348 | u8x8.drawString(0, 7, "EV_LINK_DEAD "); 349 | break; 350 | case EV_LINK_ALIVE: 351 | Serial.println(F("> EV_LINK_ALIVE")); 352 | u8x8.drawString(0, 7, "EV_LINK_ALIVE "); 353 | break; 354 | default: 355 | Serial.println(F("Unknown event")); 356 | u8x8.setCursor(0, 7); 357 | u8x8.printf("UNKNOWN EVT %d", ev); 358 | break; 359 | } 360 | } 361 | 362 | void do_send(osjob_t* j) { 363 | /* Check for running Tx/Rx job */ 364 | if (LMIC.opmode & OP_TXRXPEND) { 365 | /* Tx pending */ 366 | Serial.println(F("> OP_TXRXPEND, not sending")); 367 | u8x8.drawString(0, 7, "OP_TXRXPEND, not sent"); 368 | } else { 369 | /* No pending Tx, Go! */ 370 | build_packet(); 371 | if ( ((int)gps.hdop.value() < HDOP_MAX) && ((int)gps.hdop.value() != 0) ) { 372 | /* GPS OK, prepare upstream data transmission at the next possible time */ 373 | //build_packet(); 374 | LMIC_setTxData2(1, txBuffer, sizeof(txBuffer), 0); 375 | Serial.println(); 376 | Serial.println(F("> PACKET QUEUED")); 377 | u8x8.drawString(0, 7, "PACKET_QUEUED "); 378 | digitalWrite(BUILTIN_LED, HIGH); 379 | } else { 380 | /* GPS not ready */ 381 | //Serial.println(); 382 | Serial.println(F("> GPS NOT READY, not sending")); 383 | //Serial.println(gps.hdop.value()); 384 | u8x8.drawString(0, 7, "NO_GPS_WAIT "); 385 | /* Schedule next transmission in TX_WAIT_INTERVAL seconds */ 386 | os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_WAIT_INTERVAL), do_send); 387 | } 388 | } 389 | /* Next TX is scheduled after TX_COMPLETE event. */ 390 | } 391 | 392 | void setup() { 393 | /* Start serial */ 394 | Serial.begin(115200); 395 | 396 | /* Turn off WiFi and Bluetooth */ 397 | WiFi.mode(WIFI_OFF); 398 | btStop(); 399 | 400 | /* Turn on Vext pin fpr GPS power */ 401 | Serial.println("Turning on Vext pins for GPS ..."); 402 | pinMode(Vext, OUTPUT); 403 | digitalWrite(Vext, LOW); 404 | 405 | Serial.println("Waiting 2 seconds for GPS to power on ..."); 406 | delay(2000); 407 | 408 | /* Initialize GPS */ 409 | GPSSerial.begin(GPS_BAUD, SERIAL_8N1, GPS_RX, GPS_TX); 410 | 411 | /* Initialize OLED */ 412 | u8x8.begin(); 413 | u8x8.setFont(u8x8_font_chroma48medium8_r); 414 | 415 | /* Initialize SPI */ 416 | SPI.begin(5, 19, 27); 417 | 418 | /* Initialize LMIC */ 419 | os_init(); 420 | 421 | /* Reset the MAC state. Session and pending data transfers will be discarded. */ 422 | LMIC_reset(); 423 | 424 | uint8_t appskey[sizeof(APPSKEY)]; 425 | uint8_t nwkskey[sizeof(NWKSKEY)]; 426 | memcpy_P(appskey, APPSKEY, sizeof(APPSKEY)); 427 | memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY)); 428 | LMIC_setSession (0x1, DEVADDR, nwkskey, appskey); 429 | 430 | /* Define all usable channels and data rates (SF) on EU_868 */ 431 | LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 432 | LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI); // g-band 433 | LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 434 | LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 435 | LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 436 | LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 437 | LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 438 | LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 439 | LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK, DR_FSK), BAND_MILLI); // g2-band 440 | 441 | /* For testing: Define a fixed single channel and data rate (SF) to use */ 442 | // int channel = 0; 443 | 444 | /* For Testing: Disable all channels, except for the one defined above. */ 445 | /* 446 | for(int i=0; i<9; i++) { // "i<9" For EU; for US use "i<71" 447 | if(i != channel) { 448 | LMIC_disableChannel(i); 449 | } 450 | } 451 | */ 452 | 453 | /* Disable link check validation */ 454 | LMIC_setLinkCheckMode(0); 455 | 456 | /* TTN uses SF9 for its RX2 window. */ 457 | LMIC.dn2Dr = DR_SF9; 458 | 459 | /* Set data rate SF7 for mapping and transmit power for uplink (note: TXpow seems to be ignored by the library) */ 460 | LMIC_setDrTxpow(DR_SF7,14); 461 | 462 | /* Start sending job */ 463 | do_send(&sendjob); 464 | pinMode(BUILTIN_LED, OUTPUT); 465 | digitalWrite(BUILTIN_LED, LOW); 466 | } 467 | 468 | void loop() { 469 | /* The loop */ 470 | os_runloop_once(); 471 | } 472 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lora-TTNMapper-Heltec-v2 2 | 3 | ## Summary 4 | 5 | Code for a TTNMapper node with GPS running on a [Heltec "WiFi Lora32 **V2**"](https://heltec.org/project/wifi-lora-32/) node, based on [Heltec-Wifi-Lora-32-TTN-Mapper by sistemasorp](https://github.com/sistemasorp/Heltec-Wifi-Lora-32-TTN-Mapper) 6 | 7 | ## Copyright and sources 8 | 9 | * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman: https://github.com/matthijskooijman/arduino-lmic/blob/master/examples/ttn-abp/ttn-abp.ino 10 | * Copyright (c) 2018 sbiermann: https://github.com/sbiermann/Lora-TTNMapper-ESP32 11 | * Copyright (c) 2019 sistemasorp: https://github.com/sistemasorp/Heltec-Wifi-Lora-32-TTN-Mapper 12 | * Copyright (c) 2020 noppingen: https://github.com/noppingen/Lora-TTNMapper-Heltec-v2/ 13 | 14 | ## Modifications 15 | 16 | * More diag output on serial interface 17 | * Stay quiet, LoRa TX only if GPS `HDOP` is below a certain limit 18 | * Vext is initialized during start, you may connect the GPS to Vext 19 | * Display of distance to your mapped "home" gateway 20 | 21 | ## Get it running 22 | 23 | * Add required libraries 24 | * Set TTN `NWKSKEY`, `APPKSKEY` and `DEVADDR` 25 | * Set `GPS_RX` and `GPS_TX` 26 | * Set gateway GPS coordinates to get the distance displayed on your way 27 | * Set `TX_INTERVAL`, `HDOP_MAX` and `TX_WAIT_INTERVAL` 28 | * Change bands if you are not in the EU868 region 29 | * Print my case: [Thingiverse](https://www.thingiverse.com/thing:4145143) 30 | 31 | ## TTN 32 | 33 | * Activation method is set to ABP 34 | * TTN decoder script is [here](TTN-decoder.script) 35 | 36 | ## Images 37 | 38 | ![Mapper Node #1](ttnmapper_esp32_01.jpg) 39 | 40 | ![Mapper Node #2](ttnmapper_esp32_02.jpg) 41 | -------------------------------------------------------------------------------- /TTN-decoder.script: -------------------------------------------------------------------------------- 1 | function Decoder(bytes, port) { 2 | var decoded = {}; 3 | decoded.lat = ((bytes[0]<<16)>>>0) + ((bytes[1]<<8)>>>0) + bytes[2]; 4 | decoded.lat = (decoded.lat / 16777215.0 * 180) - 90; 5 | decoded.lon = ((bytes[3]<<16)>>>0) + ((bytes[4]<<8)>>>0) + bytes[5]; 6 | decoded.lon = (decoded.lon / 16777215.0 * 360) - 180; 7 | var altValue = ((bytes[6]<<8)>>>0) + bytes[7]; 8 | var sign = bytes[6] & (1 << 7); 9 | if(sign) 10 | { 11 | decoded.alt = 0xFFFF0000 | altValue; 12 | } 13 | else 14 | { 15 | decoded.alt = altValue; 16 | } 17 | decoded.hdop = bytes[8] / 10.0; 18 | return decoded; 19 | } 20 | -------------------------------------------------------------------------------- /ttnmapper_esp32_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppingen/Lora-TTNMapper-Heltec-v2/06c9da50f3566a035664c04de853e8dce3c10204/ttnmapper_esp32_01.jpg -------------------------------------------------------------------------------- /ttnmapper_esp32_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppingen/Lora-TTNMapper-Heltec-v2/06c9da50f3566a035664c04de853e8dce3c10204/ttnmapper_esp32_02.jpg --------------------------------------------------------------------------------