├── .gitignore
├── LICENSE
├── README.md
├── esp32-ttnmapper-gps
├── esp32-ttnmapper-gps.ino
├── gps.ino
├── lmic_Payload.ino
├── oled.ino
└── pbutton.ino
└── img
├── hardware-gps.jpg
├── hardware-ttgo.jpg
├── hardware-ttn-box.jpg
├── hardware-ttn.jpg
├── ttn-integration.png
├── ttn-node-abp.png
├── ttn-node.png
├── ttn-payload.png
└── ttn.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | # Prerequisites
2 | *.d
3 |
4 | # Compiled Object files
5 | *.slo
6 | *.lo
7 | *.o
8 | *.obj
9 |
10 | # Precompiled Headers
11 | *.gch
12 | *.pch
13 |
14 | # Compiled Dynamic libraries
15 | *.so
16 | *.dylib
17 | *.dll
18 |
19 | # Fortran module files
20 | *.mod
21 | *.smod
22 |
23 | # Compiled Static libraries
24 | *.lai
25 | *.la
26 | *.a
27 | *.lib
28 |
29 | # Executables
30 | *.exe
31 | *.out
32 | *.app
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Luiz Henrique Cassettari
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # esp32-ttnmapper-gps
2 |
3 | Program to make a LoRaWAN node based on the TTGO LoRa 915MHz plus GPS (ATGM336H). The code is configured to connect to [The Things Network](https://www.thethingsnetwork.org/) using US frequency and send the gps values to the [ttnmapper](http://ttnmapper.org/)
4 |
5 | [](https://travis-ci.org/ricaun/esp32-ttnmapper-gps)
6 | [](LICENSE)
7 |
8 |
9 |
10 | ## Hardware
11 |
12 | * TTGO LORA32 915Mhz (Heltec Wifi LoRa 32)
13 | * GPS ATGM336H
14 |
15 | ### Images
16 |
17 | 
18 | 
19 |
20 | ### Schematic
21 |
22 | | ESP32 | GPS |
23 | | :----: | :-----: |
24 | | 5V | VCC |
25 | | GND | GND |
26 | | 34 | TX |
27 | | 35 | RX |
28 |
29 | ## Librarys
30 |
31 | * [LMIC](https://github.com/mcci-catena/arduino-lmic)
32 | * [ThingPulse OLED SSD1306](https://github.com/ThingPulse/esp8266-oled-ssd1306)
33 | * [TinyGPSPlus](https://github.com/mikalhart/TinyGPSPlus)
34 |
35 | ## The Things Network
36 |
37 | ### Application Configuration
38 |
39 | First it's a good idea to create a new application, them go on Payload Formats and put the code below.
40 |
41 |
42 |
43 | ```java
44 | function Decoder(bytes, port) {
45 | // Decode an uplink message from a buffer
46 | // (array) of bytes to an object of fields.
47 | var decoded = {};
48 | // if (port === 1) decoded.led = bytes[0];
49 | decoded.lat = ((bytes[0]<<16)>>>0) + ((bytes[1]<<8)>>>0) + bytes[2];
50 | decoded.lat = (decoded.lat / 16777215.0 * 180) - 90;
51 | decoded.lon = ((bytes[3]<<16)>>>0) + ((bytes[4]<<8)>>>0) + bytes[5];
52 | decoded.lon = (decoded.lon / 16777215.0 * 360) - 180;
53 | var altValue = ((bytes[6]<<8)>>>0) + bytes[7];
54 | var sign = bytes[6] & (1 << 7);
55 | if(sign)
56 | {
57 | decoded.alt = 0xFFFF0000 | altValue;
58 | }
59 | else
60 | {
61 | decoded.alt = altValue;
62 | }
63 | decoded.hdop = bytes[8] / 10.0;
64 | return decoded;
65 | }
66 | ```
67 |
68 | Next go on integration and add a integration TNN Mapper. (It's a good idea to add a experiment name)
69 |
70 |
71 |
72 | With the experiment name is possible to check only the experiment [criciuma](https://ttnmapper.org/experiments/map.php?name=criciuma).
73 | More information on the [TTN Mapper Documentation](https://www.thethingsnetwork.org/docs/applications/ttnmapper/).
74 |
75 | ### Node
76 |
77 | Create a new device and goes to setting, change the Activation Method to ABP and disable the Frame Counter Check, them save.
78 |
79 |
80 |
81 | Next goes to Overview on the bottow Example Code.
82 |
83 |
84 |
85 | Copy the code and replace on the sample code (esp32-ttnmapper-gps.ino).
86 |
87 | If your board is diferent from the `Heltec Wifi LoRa 32` you should check the pins connection on `lmic_pins`.
88 |
89 | ----
90 |
91 | Do you like this? Please [star this project on GitHub](https://github.com/ricaun/esp32-ttnmapper-gps/stargazers)!
92 |
93 |
--------------------------------------------------------------------------------
/esp32-ttnmapper-gps/esp32-ttnmapper-gps.ino:
--------------------------------------------------------------------------------
1 | //----------------------------------------//
2 | // esp32-ttnmapper-gps.ino
3 | //
4 | // created 01/06/2019
5 | // by Luiz H. Cassettari
6 | //----------------------------------------//
7 | // Designed to work with esp32 lora 915MHz
8 | // Create a device with ABP on ttn
9 | // Create a integration with ttnmapper
10 | //----------------------------------------//
11 |
12 | const char *devAddr = "00000000";
13 | const char *nwkSKey = "00000000000000000000000000000000";
14 | const char *appSKey = "00000000000000000000000000000000";
15 |
16 | //----------------------------------------//
17 |
18 | #include
19 | #include
20 | #include
21 |
22 | #define SEND_TIMER 10
23 |
24 | #define LORA_HTOI(c) ((c<='9')?(c-'0'):((c<='F')?(c-'A'+10):((c<='f')?(c-'a'+10):(0))))
25 | #define LORA_TWO_HTOI(h, l) ((LORA_HTOI(h) << 4) + LORA_HTOI(l))
26 | #define LORA_HEX_TO_BYTE(a, h, n) { for (int i = 0; i < n; i++) (a)[i] = LORA_TWO_HTOI(h[2*i], h[2*i + 1]); }
27 | #define LORA_DEVADDR(a) (uint32_t) ((uint32_t) (a)[3] | (uint32_t) (a)[2] << 8 | (uint32_t) (a)[1] << 16 | (uint32_t) (a)[0] << 24)
28 |
29 | static uint8_t DEVADDR[4];
30 | static uint8_t NWKSKEY[16];
31 | static uint8_t APPSKEY[16];
32 |
33 | // Pin mapping
34 | const lmic_pinmap lmic_pins = {
35 | .nss = 18,
36 | .rxtx = LMIC_UNUSED_PIN,
37 | .rst = 14,
38 | .dio = {/*dio0*/ 26, /*dio1*/ 33, /*dio2*/ 32}
39 | };
40 |
41 | void os_getArtEui (u1_t* buf) { }
42 | void os_getDevEui (u1_t* buf) { }
43 | void os_getDevKey (u1_t* buf) { }
44 |
45 | static osjob_t sendjob;
46 |
47 | void onEvent (ev_t ev) {
48 | Serial.print(os_getTime());
49 | Serial.print(": ");
50 | switch (ev) {
51 | case EV_TXCOMPLETE:
52 | oled_status(" --- TXCOMPLETE --- ");
53 | Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
54 | if (LMIC.txrxFlags & TXRX_ACK)
55 | {
56 | Serial.println(F("Received ack"));
57 | }
58 | if (LMIC.dataLen != 0 || LMIC.dataBeg != 0) {
59 | uint8_t port = 0;
60 | if (LMIC.txrxFlags & TXRX_PORT)
61 | {
62 | port = LMIC.frame[LMIC.dataBeg - 1];
63 | }
64 | message(LMIC.frame + LMIC.dataBeg, LMIC.dataLen , port);
65 | }
66 | os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(SEND_TIMER), do_send);
67 | break;
68 | case EV_TXSTART:
69 | oled_status(" --- TXSTART --- ");
70 | Serial.println(F("EV_TXSTART"));
71 | break;
72 | }
73 | }
74 |
75 |
76 |
77 | void do_send(osjob_t* j) {
78 | // Check if there is not a current TX/RX job running
79 | if (LMIC.opmode & OP_TXRXPEND) {
80 | Serial.println(F("OP_TXRXPEND, not sending"));
81 | } else {
82 | PayloadNow();
83 | }
84 | }
85 |
86 | void setup() {
87 | Serial.begin(115200);
88 | Serial.println(F("Starting"));
89 |
90 | oled_setup();
91 | gps_setup();
92 | button_setup();
93 |
94 | LORA_HEX_TO_BYTE(DEVADDR, devAddr, 4);
95 | LORA_HEX_TO_BYTE(NWKSKEY, nwkSKey, 16);
96 | LORA_HEX_TO_BYTE(APPSKEY, appSKey, 16);
97 |
98 | if (LORA_DEVADDR(DEVADDR) == 0) while(true);
99 |
100 | os_init();
101 |
102 | LMIC_reset();
103 | LMIC_setSession (0x13, LORA_DEVADDR(DEVADDR), NWKSKEY, APPSKEY);
104 | LMIC_setAdrMode(0);
105 | LMIC_setClockError(MAX_CLOCK_ERROR * 10 / 100);
106 | LMIC_selectSubBand(1);
107 | LMIC_setLinkCheckMode(0);
108 |
109 | do_send(&sendjob);
110 | }
111 |
112 | void loop() {
113 | os_runloop_once();
114 | gps_loop();
115 | oled_loop();
116 | if (button_loop())
117 | {
118 | oled_mode(button_mode());
119 |
120 | if (button_count() == 0)
121 | do_send(&sendjob);
122 | else if (button_count() == 2)
123 | {
124 | os_clearCallback(&sendjob);
125 | os_radio(RADIO_RST);
126 | }
127 | }
128 | }
129 |
130 | void message(const uint8_t *payload, size_t size, uint8_t port)
131 | {
132 | Serial.println("-- MESSAGE");
133 | Serial.println("Received " + String(size) + " bytes on port " + String(port) + ":");
134 | if (port == 0)
135 | {
136 | oled_status(" --- TX_CONFIRMED --- ");
137 | return;
138 | }
139 | if (size == 0) return;
140 | switch (port) {
141 | case 1:
142 | break;
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/esp32-ttnmapper-gps/gps.ino:
--------------------------------------------------------------------------------
1 | //----------------------------------------//
2 | // gps.ino
3 | //
4 | // created 06/09/2018
5 | // update to esp32 - 05/06/2019
6 | // by Luiz H. Cassettari
7 | //----------------------------------------//
8 |
9 |
10 | #include
11 | #include
12 |
13 | #define RXPin 34
14 | #define TXPin 35
15 | #define GPSBaud 9600
16 |
17 | TinyGPSPlus gps;
18 | HardwareSerial ss(1);
19 |
20 | void gps_setup(){
21 | ss.begin(GPSBaud, SERIAL_8N1, RXPin, TXPin);
22 | Serial.print(F("TinyGPS++ library v. ")); Serial.println(TinyGPSPlus::libraryVersion());
23 | Serial.println();
24 | }
25 |
26 | void gps_loop(){
27 | while (ss.available() > 0) {
28 | gps.encode(ss.read());
29 | }
30 | if (runEvery_gps(5000))
31 | {
32 | Serial.println(gps_time() + " " + gps_date());
33 | }
34 | }
35 |
36 | boolean gps_read(){
37 | return (gps.location.isValid());
38 | }
39 |
40 | float gps_latitude(){
41 | return gps.location.lat();
42 | }
43 | float gps_longitude(){
44 | return gps.location.lng();
45 | }
46 |
47 | float gps_meters() {
48 | return gps.altitude.meters();
49 | }
50 |
51 | float gps_HDOP(){
52 | if (gps.hdop.isValid())
53 | return gps.hdop.hdop();
54 | else
55 | return 40.0;
56 | }
57 |
58 | String gps_location()
59 | {
60 | if (gps.location.isValid())
61 | {
62 | String str = "";
63 | str += gps_latitude();
64 | str += " ";
65 | str += gps_longitude();
66 | return str;
67 | }
68 | else
69 | {
70 | return "#### ####";
71 | }
72 | }
73 |
74 | String gps_date()
75 | {
76 | if (gps.date.isValid())
77 | {
78 | int d = gps.date.day();
79 | int m = gps.date.month();
80 | int y = gps.date.year();
81 | String str = "";
82 | if (d < 10) str += "0";
83 | str += d;
84 | str += "/";
85 | if (m < 10) str += "0";
86 | str += m;
87 | str += "/";
88 | str += y;
89 | return str;
90 | }
91 | else
92 | {
93 | return "xx/xx/xxxx";
94 | }
95 | }
96 |
97 | String gps_time()
98 | {
99 | if (gps.time.isValid())
100 | {
101 | int s = gps.time.second();
102 | int m = gps.time.minute();
103 | int h = gps.time.hour();
104 | String str = "";
105 | if (h < 10) str += "0";
106 | str += h;
107 | str += ":";
108 | if (m < 10) str += "0";
109 | str += m;
110 | str += ":";
111 | if (s < 10) str += "0";
112 | str += s;
113 | return str;
114 | }
115 | else
116 | {
117 | return "xx:xx:xx";
118 | }
119 | }
120 |
121 | boolean runEvery_gps(unsigned long interval)
122 | {
123 | static unsigned long previousMillis = 0;
124 | unsigned long currentMillis = millis();
125 | if (currentMillis - previousMillis >= interval)
126 | {
127 | previousMillis = currentMillis;
128 | return true;
129 | }
130 | return false;
131 | }
132 |
133 |
134 | void displayInfo()
135 | {
136 | Serial.print(F("Location: "));
137 | if (gps.location.isValid())
138 | {
139 | Serial.print(gps.location.lat(), 6);
140 | Serial.print(F(","));
141 | Serial.print(gps.location.lng(), 6);
142 | }
143 | else
144 | {
145 | Serial.print(F("INVALID"));
146 | }
147 |
148 | Serial.print(F(" Date/Time: "));
149 | if (gps.date.isValid())
150 | {
151 | Serial.print(gps.date.month());
152 | Serial.print(F("/"));
153 | Serial.print(gps.date.day());
154 | Serial.print(F("/"));
155 | Serial.print(gps.date.year());
156 | }
157 | else
158 | {
159 | Serial.print(F("INVALID"));
160 | }
161 |
162 | Serial.print(F(" "));
163 | if (gps.time.isValid())
164 | {
165 | if (gps.time.hour() < 10) Serial.print(F("0"));
166 | Serial.print(gps.time.hour());
167 | Serial.print(F(":"));
168 | if (gps.time.minute() < 10) Serial.print(F("0"));
169 | Serial.print(gps.time.minute());
170 | Serial.print(F(":"));
171 | if (gps.time.second() < 10) Serial.print(F("0"));
172 | Serial.print(gps.time.second());
173 | Serial.print(F("."));
174 | if (gps.time.centisecond() < 10) Serial.print(F("0"));
175 | Serial.print(gps.time.centisecond());
176 | }
177 | else
178 | {
179 | Serial.print(F("INVALID"));
180 | }
181 |
182 |
183 | Serial.println();
184 | }
185 |
--------------------------------------------------------------------------------
/esp32-ttnmapper-gps/lmic_Payload.ino:
--------------------------------------------------------------------------------
1 | //----------------------------------------//
2 | // lmic_payload.ino
3 | //
4 | // created 03/06/2019
5 | // by Luiz Henrique Cassettari
6 | //----------------------------------------//
7 |
8 | uint8_t txBuffer[9];
9 | uint32_t LatitudeBinary, LongitudeBinary;
10 | uint16_t altitudeGps;
11 | uint8_t hdopGps;
12 |
13 | void PayloadNow()
14 | {
15 | boolean confirmed = false;
16 |
17 | if (button_count() == 1) confirmed = true;
18 |
19 | if (gps_read()) {
20 |
21 | LatitudeBinary = ((gps_latitude() + 90) / 180) * 16777215;
22 | LongitudeBinary = ((gps_longitude() + 180) / 360) * 16777215;
23 |
24 | txBuffer[0] = ( LatitudeBinary >> 16 ) & 0xFF;
25 | txBuffer[1] = ( LatitudeBinary >> 8 ) & 0xFF;
26 | txBuffer[2] = LatitudeBinary & 0xFF;
27 |
28 | txBuffer[3] = ( LongitudeBinary >> 16 ) & 0xFF;
29 | txBuffer[4] = ( LongitudeBinary >> 8 ) & 0xFF;
30 | txBuffer[5] = LongitudeBinary & 0xFF;
31 |
32 | altitudeGps = gps_meters();
33 | txBuffer[6] = ( altitudeGps >> 8 ) & 0xFF;
34 | txBuffer[7] = altitudeGps & 0xFF;
35 |
36 | hdopGps = gps_HDOP() * 10;
37 | txBuffer[8] = hdopGps & 0xFF;
38 |
39 | LMIC_setTxData2(1, txBuffer, sizeof(txBuffer), confirmed);
40 | }
41 | else
42 | {
43 | LMIC_setTxData2(1, txBuffer, 0, confirmed);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/esp32-ttnmapper-gps/oled.ino:
--------------------------------------------------------------------------------
1 | //----------------------------------------//
2 | // oled.ino
3 | //
4 | // created 23/06/2018
5 | // by Luiz Henrique Cassettari
6 | //----------------------------------------//
7 |
8 | #include
9 | #include "SSD1306.h"
10 |
11 | #define OLED_RUNEVERY 500
12 | #define OLED_TIMEOUT 4
13 |
14 | #define OLED_SDA 4
15 | #define OLED_SCL 15
16 | #define OLED_ADDR 0x3C
17 | #define OLED_RST 16
18 |
19 | SSD1306 display(OLED_ADDR, OLED_SDA, OLED_SCL);// i2c ADDR & SDA, SCL on wemos
20 |
21 |
22 | void oled_setup() {
23 | pinMode(OLED_RST, OUTPUT);
24 | digitalWrite(OLED_RST, HIGH);
25 |
26 | display.init();
27 | display.resetDisplay();
28 | display.displayOn();
29 | display.flipScreenVertically();
30 | display.setFont(ArialMT_Plain_10);
31 |
32 | display.setContrast(255);
33 |
34 | oled_display("...");
35 | }
36 |
37 | void oled_display(String s) {
38 | display.clear();
39 | display.setTextAlignment(TEXT_ALIGN_CENTER);
40 | display.drawString(64, 0, s);
41 | display.display();
42 | }
43 |
44 | void oled_turnoff() {
45 | display.clear();
46 | display.displayOff();
47 | display.end();
48 | //digitalWrite(OLED_RST, LOW);
49 | }
50 |
51 | static int oled_i;
52 | static String oled_mode_string;
53 | static int oled_mode_timeout;
54 | static String oled_status_string;
55 | static int oled_status_timeout;
56 |
57 | boolean oled_loop() {
58 | if (oled_runEvery(OLED_RUNEVERY)) {
59 | display.clear();
60 | display.setTextAlignment(TEXT_ALIGN_CENTER);
61 | String str = "";
62 | if (oled_mode_string == "")
63 | {
64 | str += timeOn(0);
65 | }
66 | else
67 | {
68 | str += oled_mode_string;
69 | }
70 | str += "\n";
71 | str += gps_date();
72 | str += "\n";
73 | str += gps_time();
74 | str += "\n";
75 | str += gps_location();
76 | str += "\n";
77 | str += oled_status_string;
78 | display.drawString(64,0,str);
79 | display.display();
80 |
81 | if (--oled_status_timeout == 0)
82 | {
83 | oled_status_timeout = 0;
84 | oled_status_string = "";
85 | }
86 |
87 | if (--oled_mode_timeout == 0)
88 | {
89 | oled_mode_timeout = 0;
90 | oled_mode_string = "";
91 | }
92 |
93 | return true;
94 | }
95 | return false;
96 | }
97 |
98 | void oled_status(String status) {
99 | oled_status_string = status;
100 | oled_status_timeout = OLED_TIMEOUT;
101 | }
102 |
103 | void oled_mode(String status) {
104 | oled_mode_string = status;
105 | oled_mode_timeout = OLED_TIMEOUT;
106 | }
107 |
108 | boolean oled_runEvery(unsigned long interval)
109 | {
110 | static unsigned long previousMillis = 0;
111 | unsigned long currentMillis = millis();
112 | if (currentMillis - previousMillis >= interval)
113 | {
114 | previousMillis = currentMillis;
115 | return true;
116 | }
117 | return false;
118 | }
119 |
120 | String timeOn(unsigned long diff)
121 | {
122 | String str = "";
123 | unsigned long t = millis() / 1000;
124 | int s = t % 60;
125 | int m = (t / 60) % 60;
126 | int h = (t / 3600);
127 | str += h;
128 | str += ":";
129 | if (m < 10)
130 | str += "0";
131 | str += m;
132 | str += ":";
133 | if (s < 10)
134 | str += "0";
135 | str += s;
136 | return str;
137 | }
138 |
--------------------------------------------------------------------------------
/esp32-ttnmapper-gps/pbutton.ino:
--------------------------------------------------------------------------------
1 | //----------------------------------------//
2 | // pbutton.ino
3 | //
4 | // created 03/06/2019
5 | // by Luiz Henrique Cassettari
6 | //----------------------------------------//
7 |
8 | #define BUTTON 0
9 | #define BUTTON_MODE_MAX 3
10 |
11 | static int button_i;
12 |
13 | void button_setup()
14 | {
15 | pinMode(BUTTON, INPUT_PULLUP);
16 | }
17 |
18 | boolean button_press()
19 | {
20 | static boolean last;
21 | boolean now = digitalRead(BUTTON);
22 | boolean ret = (now == false & last == true);
23 | last = now;
24 | return ret;
25 | }
26 |
27 | boolean button_loop()
28 | {
29 | if (button_press())
30 | {
31 | button_i++;
32 | return true;
33 | }
34 | return false;
35 | }
36 |
37 | int button_count()
38 | {
39 | return button_i;
40 | }
41 |
42 | String button_mode()
43 | {
44 | button_i = button_i % BUTTON_MODE_MAX;
45 | switch (button_i) {
46 | case 0:
47 | return "MODE UNCONFIRMED";
48 | case 1:
49 | return "MODE CONFIRMED";
50 | case 2:
51 | return "MODE SEND OFF";
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/img/hardware-gps.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/hardware-gps.jpg
--------------------------------------------------------------------------------
/img/hardware-ttgo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/hardware-ttgo.jpg
--------------------------------------------------------------------------------
/img/hardware-ttn-box.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/hardware-ttn-box.jpg
--------------------------------------------------------------------------------
/img/hardware-ttn.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/hardware-ttn.jpg
--------------------------------------------------------------------------------
/img/ttn-integration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/ttn-integration.png
--------------------------------------------------------------------------------
/img/ttn-node-abp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/ttn-node-abp.png
--------------------------------------------------------------------------------
/img/ttn-node.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/ttn-node.png
--------------------------------------------------------------------------------
/img/ttn-payload.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/ttn-payload.png
--------------------------------------------------------------------------------
/img/ttn.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ricaun/esp32-ttnmapper-gps/8d37aa60e96707303ae07ca30366d2982e15b286/img/ttn.jpg
--------------------------------------------------------------------------------