├── Adafruit_SSD1306.cpp ├── Adafruit_SSD1306.h ├── Lora-TTNMapper-T-Beam-v10.ino ├── README.md ├── TTN-decoder.script ├── config.h ├── gps.cpp ├── gps.h ├── gpsicon.h ├── ttnmapper_t-beam_v10_01.jpg └── ttnmapper_t-beam_v10_02.jpg /Adafruit_SSD1306.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Modified Adafruilt SSD1306 library: Custom I²C pins and ability to set I²C Clock to 800kHz. 3 | * 4 | * Originally written by Limor Fried/Ladyada for Adafruit Industries. 5 | * BSD license, check license.txt for more information 6 | * All text above, and the splash screen below must be included in any redistribution 7 | */ 8 | 9 | #ifdef __AVR__ 10 | #include 11 | #elif defined(ESP8266) || defined(ESP32) 12 | #include 13 | #else 14 | #define pgm_read_byte(addr) (*(const unsigned char *)(addr)) 15 | #endif 16 | 17 | #if !defined(__ARM_ARCH) && !defined(ENERGIA) && !defined(ESP8266) && !defined(ESP32) && !defined(__arc__) 18 | #include 19 | #endif 20 | 21 | #include 22 | 23 | #include 24 | #include 25 | #include "Adafruit_GFX.h" 26 | #include "Adafruit_SSD1306.h" 27 | 28 | // the memory buffer for the LCD 29 | 30 | static uint8_t buffer[SSD1306_LCDHEIGHT * SSD1306_LCDWIDTH / 8] = { 31 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 32 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 33 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 34 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 35 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 36 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 37 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 38 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 39 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 40 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 41 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 42 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 43 | #if (SSD1306_LCDHEIGHT * SSD1306_LCDWIDTH > 96*16) 44 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 45 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 51 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 52 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 53 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 54 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 56 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 57 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 58 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 60 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 61 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 62 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 63 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 64 | #if (SSD1306_LCDHEIGHT == 64) 65 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 66 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 67 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 68 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 69 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 70 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 71 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 72 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 73 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 74 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 75 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 76 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 77 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 78 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 79 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 80 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 81 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 82 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 83 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 84 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 85 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 86 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 89 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 90 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 91 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 92 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 93 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 94 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 95 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 96 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 97 | #endif 98 | #endif 99 | }; 100 | 101 | #define ssd1306_swap(a, b) { int16_t t = a; a = b; b = t; } 102 | 103 | // the most basic function, set a single pixel 104 | void Adafruit_SSD1306::drawPixel(int16_t x, int16_t y, uint16_t color) { 105 | if ((x < 0) || (x >= width()) || (y < 0) || (y >= height())) 106 | return; 107 | 108 | // check rotation, move pixel around if necessary 109 | switch (getRotation()) { 110 | case 1: 111 | ssd1306_swap(x, y); 112 | x = WIDTH - x - 1; 113 | break; 114 | case 2: 115 | x = WIDTH - x - 1; 116 | y = HEIGHT - y - 1; 117 | break; 118 | case 3: 119 | ssd1306_swap(x, y); 120 | y = HEIGHT - y - 1; 121 | break; 122 | } 123 | 124 | // x is which column 125 | switch (color) 126 | { 127 | case WHITE: buffer[x+ (y/8)*SSD1306_LCDWIDTH] |= (1 << (y&7)); break; 128 | case BLACK: buffer[x+ (y/8)*SSD1306_LCDWIDTH] &= ~(1 << (y&7)); break; 129 | case INVERSE: buffer[x+ (y/8)*SSD1306_LCDWIDTH] ^= (1 << (y&7)); break; 130 | } 131 | 132 | } 133 | 134 | Adafruit_SSD1306::Adafruit_SSD1306(int8_t SID, int8_t SCLK, int8_t DC, int8_t RST, int8_t CS) : Adafruit_GFX(SSD1306_LCDWIDTH, SSD1306_LCDHEIGHT) { 135 | cs = CS; 136 | rst = RST; 137 | dc = DC; 138 | sclk = SCLK; 139 | sid = SID; 140 | hwSPI = false; 141 | } 142 | 143 | // constructor for hardware SPI - we indicate DataCommand, ChipSelect, Reset 144 | Adafruit_SSD1306::Adafruit_SSD1306(int8_t DC, int8_t RST, int8_t CS) : Adafruit_GFX(SSD1306_LCDWIDTH, SSD1306_LCDHEIGHT) { 145 | dc = DC; 146 | rst = RST; 147 | cs = CS; 148 | hwSPI = true; 149 | } 150 | 151 | #ifdef ESP32 152 | // ESP32 - constructor for hardware SPI - we indicate MISO, MOSI, SCLK, DataCommand, ChipSelect, Reset 153 | Adafruit_SSD1306::Adafruit_SSD1306(int8_t mmiso, int8_t mmosi, int8_t msclk, int8_t DC, int8_t RST, int8_t CS, unsigned long clockspeed) : Adafruit_GFX(SSD1306_LCDWIDTH, SSD1306_LCDHEIGHT) { 154 | dc = DC; 155 | rst = RST; 156 | cs = CS; 157 | mymiso = mmiso; 158 | sid = mmosi; 159 | mysclk = msclk; 160 | hwSPI = true; 161 | _clockspeed = clockspeed; 162 | } 163 | #endif 164 | 165 | // initializer for I2C - we only indicate the reset pin! 166 | Adafruit_SSD1306::Adafruit_SSD1306(int8_t reset) : 167 | Adafruit_GFX(SSD1306_LCDWIDTH, SSD1306_LCDHEIGHT) { 168 | sclk = dc = cs = sid = -1; 169 | rst = reset; 170 | } 171 | 172 | 173 | #if defined(ESP8266) || defined(ESP32) 174 | void Adafruit_SSD1306::begin(uint8_t vccstate, uint8_t i2caddr, bool reset, uint8_t sdapin, uint8_t sclpin, unsigned long clockspeed) { 175 | #else 176 | void Adafruit_SSD1306::begin(uint8_t vccstate, uint8_t i2caddr, bool reset) { 177 | #endif 178 | 179 | _vccstate = vccstate; 180 | _i2caddr = i2caddr; 181 | #if defined(ESP8266) || defined(ESP32) 182 | _sdapin=sdapin; 183 | _sclpin=sclpin; 184 | _clockspeed = clockspeed; 185 | #endif 186 | 187 | // set pin directions 188 | if (sid != -1){ 189 | pinMode(dc, OUTPUT); 190 | pinMode(cs, OUTPUT); 191 | #ifdef HAVE_PORTREG 192 | csport = portOutputRegister(digitalPinToPort(cs)); 193 | cspinmask = digitalPinToBitMask(cs); 194 | dcport = portOutputRegister(digitalPinToPort(dc)); 195 | dcpinmask = digitalPinToBitMask(dc); 196 | #endif 197 | if (!hwSPI){ 198 | // set pins for software-SPI 199 | pinMode(sid, OUTPUT); 200 | pinMode(sclk, OUTPUT); 201 | #ifdef HAVE_PORTREG 202 | clkport = portOutputRegister(digitalPinToPort(sclk)); 203 | clkpinmask = digitalPinToBitMask(sclk); 204 | mosiport = portOutputRegister(digitalPinToPort(sid)); 205 | mosipinmask = digitalPinToBitMask(sid); 206 | #endif 207 | } 208 | if (hwSPI){ 209 | #ifdef ESP32 210 | pinMode(dc, OUTPUT); 211 | pinMode(cs, OUTPUT); 212 | SPI.begin(mysclk, mymiso, sid, cs); 213 | SPI.setFrequency(_clockspeed); 214 | #else 215 | SPI.begin(); 216 | #ifdef SPI_HAS_TRANSACTION 217 | SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0)); 218 | #else 219 | SPI.setClockDivider (4); 220 | #endif 221 | #endif 222 | } 223 | } 224 | else 225 | { 226 | // I2C Init 227 | #if defined(ESP8266) || defined(ESP32) 228 | if (_sdapin != -1) { 229 | Wire.begin(_sdapin,_sclpin); 230 | } else { 231 | Wire.begin(); 232 | } 233 | Wire.setClock(_clockspeed); 234 | #else 235 | Wire.begin(); 236 | #endif 237 | #ifdef __SAM3X8E__ 238 | // Force 400 KHz I2C, rawr! (Uses pins 20, 21 for SDA, SCL) 239 | TWI1->TWI_CWGR = 0; 240 | TWI1->TWI_CWGR = ((VARIANT_MCK / (2 * 400000)) - 4) * 0x101; 241 | #endif 242 | } 243 | if ((reset) && (rst >= 0)) { 244 | // Setup reset pin direction (used by both SPI and I2C) 245 | pinMode(rst, OUTPUT); 246 | digitalWrite(rst, HIGH); 247 | // VDD (3.3V) goes high at start, lets just chill for a ms 248 | delay(1); 249 | // bring reset low 250 | digitalWrite(rst, LOW); 251 | // wait 10ms 252 | delay(10); 253 | // bring out of reset 254 | digitalWrite(rst, HIGH); 255 | // turn on VCC (9V?) 256 | } 257 | 258 | // Init sequence 259 | ssd1306_command(SSD1306_DISPLAYOFF); // 0xAE 260 | ssd1306_command(SSD1306_SETDISPLAYCLOCKDIV); // 0xD5 261 | ssd1306_command(0x80); // the suggested ratio 0x80 262 | 263 | ssd1306_command(SSD1306_SETMULTIPLEX); // 0xA8 264 | ssd1306_command(SSD1306_LCDHEIGHT - 1); 265 | 266 | ssd1306_command(SSD1306_SETDISPLAYOFFSET); // 0xD3 267 | ssd1306_command(0x0); // no offset 268 | ssd1306_command(SSD1306_SETSTARTLINE | 0x0); // line #0 269 | ssd1306_command(SSD1306_CHARGEPUMP); // 0x8D 270 | if (vccstate == SSD1306_EXTERNALVCC) 271 | { ssd1306_command(0x10); } 272 | else 273 | { ssd1306_command(0x14); } 274 | ssd1306_command(SSD1306_MEMORYMODE); // 0x20 275 | ssd1306_command(0x00); // 0x0 act like ks0108 276 | ssd1306_command(SSD1306_SEGREMAP | 0x1); 277 | ssd1306_command(SSD1306_COMSCANDEC); 278 | 279 | #if defined SSD1306_128_32 280 | ssd1306_command(SSD1306_SETCOMPINS); // 0xDA 281 | ssd1306_command(0x02); 282 | ssd1306_command(SSD1306_SETCONTRAST); // 0x81 283 | ssd1306_command(0x8F); 284 | 285 | #elif defined SSD1306_128_64 286 | ssd1306_command(SSD1306_SETCOMPINS); // 0xDA 287 | ssd1306_command(0x12); 288 | ssd1306_command(SSD1306_SETCONTRAST); // 0x81 289 | if (vccstate == SSD1306_EXTERNALVCC) 290 | { ssd1306_command(0x9F); } 291 | else 292 | { ssd1306_command(0xCF); } 293 | 294 | #elif defined SSD1306_96_16 295 | ssd1306_command(SSD1306_SETCOMPINS); // 0xDA 296 | ssd1306_command(0x2); //ada x12 297 | ssd1306_command(SSD1306_SETCONTRAST); // 0x81 298 | if (vccstate == SSD1306_EXTERNALVCC) 299 | { ssd1306_command(0x10); } 300 | else 301 | { ssd1306_command(0xAF); } 302 | 303 | #endif 304 | 305 | ssd1306_command(SSD1306_SETPRECHARGE); // 0xd9 306 | if (vccstate == SSD1306_EXTERNALVCC) 307 | { ssd1306_command(0x22); } 308 | else 309 | { ssd1306_command(0xF1); } 310 | ssd1306_command(SSD1306_SETVCOMDETECT); // 0xDB 311 | ssd1306_command(0x40); 312 | ssd1306_command(SSD1306_DISPLAYALLON_RESUME); // 0xA4 313 | ssd1306_command(SSD1306_NORMALDISPLAY); // 0xA6 314 | 315 | ssd1306_command(SSD1306_DEACTIVATE_SCROLL); 316 | 317 | ssd1306_command(SSD1306_DISPLAYON);//--turn on oled panel 318 | } 319 | 320 | 321 | void Adafruit_SSD1306::invertDisplay(uint8_t i) { 322 | if (i) { 323 | ssd1306_command(SSD1306_INVERTDISPLAY); 324 | } else { 325 | ssd1306_command(SSD1306_NORMALDISPLAY); 326 | } 327 | } 328 | 329 | void Adafruit_SSD1306::ssd1306_command(uint8_t c) { 330 | if (sid != -1) 331 | { 332 | // SPI 333 | #ifdef HAVE_PORTREG 334 | *csport |= cspinmask; 335 | *dcport &= ~dcpinmask; 336 | *csport &= ~cspinmask; 337 | #else 338 | digitalWrite(cs, HIGH); 339 | digitalWrite(dc, LOW); 340 | digitalWrite(cs, LOW); 341 | #endif 342 | fastSPIwrite(c); 343 | #ifdef HAVE_PORTREG 344 | *csport |= cspinmask; 345 | #else 346 | digitalWrite(cs, HIGH); 347 | #endif 348 | } 349 | else 350 | { 351 | // I2C 352 | uint8_t control = 0x00; // Co = 0, D/C = 0 353 | Wire.beginTransmission(_i2caddr); 354 | Wire.write(control); 355 | Wire.write(c); 356 | Wire.endTransmission(); 357 | } 358 | } 359 | 360 | // startscrollright 361 | // Activate a right handed scroll for rows start through stop 362 | // Hint, the display is 16 rows tall. To scroll the whole display, run: 363 | // display.scrollright(0x00, 0x0F) 364 | void Adafruit_SSD1306::startscrollright(uint8_t start, uint8_t stop){ 365 | ssd1306_command(SSD1306_RIGHT_HORIZONTAL_SCROLL); 366 | ssd1306_command(0X00); 367 | ssd1306_command(start); 368 | ssd1306_command(0X00); 369 | ssd1306_command(stop); 370 | ssd1306_command(0X00); 371 | ssd1306_command(0XFF); 372 | ssd1306_command(SSD1306_ACTIVATE_SCROLL); 373 | } 374 | 375 | // startscrollleft 376 | // Activate a right handed scroll for rows start through stop 377 | // Hint, the display is 16 rows tall. To scroll the whole display, run: 378 | // display.scrollright(0x00, 0x0F) 379 | void Adafruit_SSD1306::startscrollleft(uint8_t start, uint8_t stop){ 380 | ssd1306_command(SSD1306_LEFT_HORIZONTAL_SCROLL); 381 | ssd1306_command(0X00); 382 | ssd1306_command(start); 383 | ssd1306_command(0X00); 384 | ssd1306_command(stop); 385 | ssd1306_command(0X00); 386 | ssd1306_command(0XFF); 387 | ssd1306_command(SSD1306_ACTIVATE_SCROLL); 388 | } 389 | 390 | // startscrolldiagright 391 | // Activate a diagonal scroll for rows start through stop 392 | // Hint, the display is 16 rows tall. To scroll the whole display, run: 393 | // display.scrollright(0x00, 0x0F) 394 | void Adafruit_SSD1306::startscrolldiagright(uint8_t start, uint8_t stop){ 395 | ssd1306_command(SSD1306_SET_VERTICAL_SCROLL_AREA); 396 | ssd1306_command(0X00); 397 | ssd1306_command(SSD1306_LCDHEIGHT); 398 | ssd1306_command(SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL); 399 | ssd1306_command(0X00); 400 | ssd1306_command(start); 401 | ssd1306_command(0X00); 402 | ssd1306_command(stop); 403 | ssd1306_command(0X01); 404 | ssd1306_command(SSD1306_ACTIVATE_SCROLL); 405 | } 406 | 407 | // startscrolldiagleft 408 | // Activate a diagonal scroll for rows start through stop 409 | // Hint, the display is 16 rows tall. To scroll the whole display, run: 410 | // display.scrollright(0x00, 0x0F) 411 | void Adafruit_SSD1306::startscrolldiagleft(uint8_t start, uint8_t stop){ 412 | ssd1306_command(SSD1306_SET_VERTICAL_SCROLL_AREA); 413 | ssd1306_command(0X00); 414 | ssd1306_command(SSD1306_LCDHEIGHT); 415 | ssd1306_command(SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL); 416 | ssd1306_command(0X00); 417 | ssd1306_command(start); 418 | ssd1306_command(0X00); 419 | ssd1306_command(stop); 420 | ssd1306_command(0X01); 421 | ssd1306_command(SSD1306_ACTIVATE_SCROLL); 422 | } 423 | 424 | void Adafruit_SSD1306::stopscroll(void){ 425 | ssd1306_command(SSD1306_DEACTIVATE_SCROLL); 426 | } 427 | 428 | // Dim the display 429 | // dim = true: display is dimmed 430 | // dim = false: display is normal 431 | void Adafruit_SSD1306::dim(boolean dim) { 432 | uint8_t contrast; 433 | 434 | if (dim) { 435 | contrast = 0; // Dimmed display 436 | } else { 437 | if (_vccstate == SSD1306_EXTERNALVCC) { 438 | contrast = 0x9F; 439 | } else { 440 | contrast = 0xCF; 441 | } 442 | } 443 | // the range of contrast to too small to be really useful 444 | // it is useful to dim the display 445 | ssd1306_command(SSD1306_SETCONTRAST); 446 | ssd1306_command(contrast); 447 | } 448 | 449 | void Adafruit_SSD1306::display(void) { 450 | ssd1306_command(SSD1306_COLUMNADDR); 451 | ssd1306_command(0); // Column start address (0 = reset) 452 | ssd1306_command(SSD1306_LCDWIDTH-1); // Column end address (127 = reset) 453 | 454 | ssd1306_command(SSD1306_PAGEADDR); 455 | ssd1306_command(0); // Page start address (0 = reset) 456 | #if SSD1306_LCDHEIGHT == 64 457 | ssd1306_command(7); // Page end address 458 | #endif 459 | #if SSD1306_LCDHEIGHT == 32 460 | ssd1306_command(3); // Page end address 461 | #endif 462 | #if SSD1306_LCDHEIGHT == 16 463 | ssd1306_command(1); // Page end address 464 | #endif 465 | 466 | if (sid != -1) 467 | { 468 | // SPI 469 | #ifdef HAVE_PORTREG 470 | *csport |= cspinmask; 471 | *dcport |= dcpinmask; 472 | *csport &= ~cspinmask; 473 | #else 474 | digitalWrite(cs, HIGH); 475 | digitalWrite(dc, HIGH); 476 | digitalWrite(cs, LOW); 477 | #endif 478 | 479 | for (uint16_t i=0; i<(SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8); i++) { 480 | fastSPIwrite(buffer[i]); 481 | } 482 | #ifdef HAVE_PORTREG 483 | *csport |= cspinmask; 484 | #else 485 | digitalWrite(cs, HIGH); 486 | #endif 487 | } 488 | else 489 | { 490 | // save I2C bitrate 491 | #ifdef TWBR 492 | uint8_t twbrbackup = TWBR; 493 | TWBR = 12; // upgrade to 400KHz! 494 | #endif 495 | 496 | //Serial.println(TWBR, DEC); 497 | //Serial.println(TWSR & 0x3, DEC); 498 | 499 | // I2C 500 | for (uint16_t i=0; i<(SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8); i++) { 501 | // send a bunch of data in one xmission 502 | Wire.beginTransmission(_i2caddr); 503 | WIRE_WRITE(0x40); 504 | for (uint8_t x=0; x<16; x++) { 505 | WIRE_WRITE(buffer[i]); 506 | i++; 507 | } 508 | i--; 509 | Wire.endTransmission(); 510 | } 511 | #ifdef TWBR 512 | TWBR = twbrbackup; 513 | #endif 514 | } 515 | } 516 | 517 | // clear everything 518 | void Adafruit_SSD1306::clearDisplay(void) { 519 | memset(buffer, 0, (SSD1306_LCDWIDTH*SSD1306_LCDHEIGHT/8)); 520 | } 521 | 522 | 523 | inline void Adafruit_SSD1306::fastSPIwrite(uint8_t d) { 524 | 525 | if(hwSPI) { 526 | (void)SPI.transfer(d); 527 | } else { 528 | for(uint8_t bit = 0x80; bit; bit >>= 1) { 529 | #ifdef HAVE_PORTREG 530 | *clkport &= ~clkpinmask; 531 | if(d & bit) *mosiport |= mosipinmask; 532 | else *mosiport &= ~mosipinmask; 533 | *clkport |= clkpinmask; 534 | #else 535 | digitalWrite(sclk, LOW); 536 | if(d & bit) digitalWrite(sid, HIGH); 537 | else digitalWrite(sid, LOW); 538 | digitalWrite(sclk, HIGH); 539 | #endif 540 | } 541 | } 542 | } 543 | 544 | void Adafruit_SSD1306::drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color) { 545 | boolean bSwap = false; 546 | switch(rotation) { 547 | case 0: 548 | // 0 degree rotation, do nothing 549 | break; 550 | case 1: 551 | // 90 degree rotation, swap x & y for rotation, then invert x 552 | bSwap = true; 553 | ssd1306_swap(x, y); 554 | x = WIDTH - x - 1; 555 | break; 556 | case 2: 557 | // 180 degree rotation, invert x and y - then shift y around for height. 558 | x = WIDTH - x - 1; 559 | y = HEIGHT - y - 1; 560 | x -= (w-1); 561 | break; 562 | case 3: 563 | // 270 degree rotation, swap x & y for rotation, then invert y and adjust y for w (not to become h) 564 | bSwap = true; 565 | ssd1306_swap(x, y); 566 | y = HEIGHT - y - 1; 567 | y -= (w-1); 568 | break; 569 | } 570 | 571 | if(bSwap) { 572 | drawFastVLineInternal(x, y, w, color); 573 | } else { 574 | drawFastHLineInternal(x, y, w, color); 575 | } 576 | } 577 | 578 | void Adafruit_SSD1306::drawFastHLineInternal(int16_t x, int16_t y, int16_t w, uint16_t color) { 579 | // Do bounds/limit checks 580 | if(y < 0 || y >= HEIGHT) { return; } 581 | 582 | // make sure we don't try to draw below 0 583 | if(x < 0) { 584 | w += x; 585 | x = 0; 586 | } 587 | 588 | // make sure we don't go off the edge of the display 589 | if( (x + w) > WIDTH) { 590 | w = (WIDTH - x); 591 | } 592 | 593 | // if our width is now negative, punt 594 | if(w <= 0) { return; } 595 | 596 | // set up the pointer for movement through the buffer 597 | register uint8_t *pBuf = buffer; 598 | // adjust the buffer pointer for the current row 599 | pBuf += ((y/8) * SSD1306_LCDWIDTH); 600 | // and offset x columns in 601 | pBuf += x; 602 | 603 | register uint8_t mask = 1 << (y&7); 604 | 605 | switch (color) 606 | { 607 | case WHITE: while(w--) { *pBuf++ |= mask; }; break; 608 | case BLACK: mask = ~mask; while(w--) { *pBuf++ &= mask; }; break; 609 | case INVERSE: while(w--) { *pBuf++ ^= mask; }; break; 610 | } 611 | } 612 | 613 | void Adafruit_SSD1306::drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color) { 614 | bool bSwap = false; 615 | switch(rotation) { 616 | case 0: 617 | break; 618 | case 1: 619 | // 90 degree rotation, swap x & y for rotation, then invert x and adjust x for h (now to become w) 620 | bSwap = true; 621 | ssd1306_swap(x, y); 622 | x = WIDTH - x - 1; 623 | x -= (h-1); 624 | break; 625 | case 2: 626 | // 180 degree rotation, invert x and y - then shift y around for height. 627 | x = WIDTH - x - 1; 628 | y = HEIGHT - y - 1; 629 | y -= (h-1); 630 | break; 631 | case 3: 632 | // 270 degree rotation, swap x & y for rotation, then invert y 633 | bSwap = true; 634 | ssd1306_swap(x, y); 635 | y = HEIGHT - y - 1; 636 | break; 637 | } 638 | 639 | if(bSwap) { 640 | drawFastHLineInternal(x, y, h, color); 641 | } else { 642 | drawFastVLineInternal(x, y, h, color); 643 | } 644 | } 645 | 646 | 647 | void Adafruit_SSD1306::drawFastVLineInternal(int16_t x, int16_t __y, int16_t __h, uint16_t color) { 648 | 649 | // do nothing if we're off the left or right side of the screen 650 | if(x < 0 || x >= WIDTH) { return; } 651 | 652 | // make sure we don't try to draw below 0 653 | if(__y < 0) { 654 | // __y is negative, this will subtract enough from __h to account for __y being 0 655 | __h += __y; 656 | __y = 0; 657 | 658 | } 659 | 660 | // make sure we don't go past the height of the display 661 | if( (__y + __h) > HEIGHT) { 662 | __h = (HEIGHT - __y); 663 | } 664 | 665 | // if our height is now negative, punt 666 | if(__h <= 0) { 667 | return; 668 | } 669 | 670 | // this display doesn't need ints for coordinates, use local byte registers for faster juggling 671 | register uint8_t y = __y; 672 | register uint8_t h = __h; 673 | 674 | 675 | // set up the pointer for fast movement through the buffer 676 | register uint8_t *pBuf = buffer; 677 | // adjust the buffer pointer for the current row 678 | pBuf += ((y/8) * SSD1306_LCDWIDTH); 679 | // and offset x columns in 680 | pBuf += x; 681 | 682 | // do the first partial byte, if necessary - this requires some masking 683 | register uint8_t mod = (y&7); 684 | if(mod) { 685 | // mask off the high n bits we want to set 686 | mod = 8-mod; 687 | 688 | // note - lookup table results in a nearly 10% performance improvement in fill* functions 689 | // register uint8_t mask = ~(0xFF >> (mod)); 690 | static uint8_t premask[8] = {0x00, 0x80, 0xC0, 0xE0, 0xF0, 0xF8, 0xFC, 0xFE }; 691 | register uint8_t mask = premask[mod]; 692 | 693 | // adjust the mask if we're not going to reach the end of this byte 694 | if( h < mod) { 695 | mask &= (0XFF >> (mod-h)); 696 | } 697 | 698 | switch (color) 699 | { 700 | case WHITE: *pBuf |= mask; break; 701 | case BLACK: *pBuf &= ~mask; break; 702 | case INVERSE: *pBuf ^= mask; break; 703 | } 704 | 705 | // fast exit if we're done here! 706 | if(h= 8) { 716 | if (color == INVERSE) { // separate copy of the code so we don't impact performance of the black/white write version with an extra comparison per loop 717 | do { 718 | *pBuf=~(*pBuf); 719 | 720 | // adjust the buffer forward 8 rows worth of data 721 | pBuf += SSD1306_LCDWIDTH; 722 | 723 | // adjust h & y (there's got to be a faster way for me to do this, but this should still help a fair bit for now) 724 | h -= 8; 725 | } while(h >= 8); 726 | } 727 | else { 728 | // store a local value to work with 729 | register uint8_t val = (color == WHITE) ? 255 : 0; 730 | 731 | do { 732 | // write our value in 733 | *pBuf = val; 734 | 735 | // adjust the buffer forward 8 rows worth of data 736 | pBuf += SSD1306_LCDWIDTH; 737 | 738 | // adjust h & y (there's got to be a faster way for me to do this, but this should still help a fair bit for now) 739 | h -= 8; 740 | } while(h >= 8); 741 | } 742 | } 743 | 744 | // now do the final partial byte, if necessary 745 | if(h) { 746 | mod = h & 7; 747 | // this time we want to mask the low bits of the byte, vs the high bits we did above 748 | // register uint8_t mask = (1 << mod) - 1; 749 | // note - lookup table results in a nearly 10% performance improvement in fill* functions 750 | static uint8_t postmask[8] = {0x00, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F }; 751 | register uint8_t mask = postmask[mod]; 752 | switch (color) 753 | { 754 | case WHITE: *pBuf |= mask; break; 755 | case BLACK: *pBuf &= ~mask; break; 756 | case INVERSE: *pBuf ^= mask; break; 757 | } 758 | } 759 | } 760 | -------------------------------------------------------------------------------- /Adafruit_SSD1306.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Modified Adafruilt SSD1306 library: Custom I²C pins and ability to set I²C Clock to 800kHz. 3 | * 4 | * Originally written by Limor Fried/Ladyada for Adafruit Industries. 5 | * BSD license, check license.txt for more information 6 | * All text above, and the splash screen below must be included in any redistribution 7 | */ 8 | 9 | #ifndef _Adafruit_SSD1306_H_ 10 | #define _Adafruit_SSD1306_H_ 11 | 12 | #if ARDUINO >= 100 13 | #include "Arduino.h" 14 | #define WIRE_WRITE Wire.write 15 | #else 16 | #include "WProgram.h" 17 | #define WIRE_WRITE Wire.send 18 | #endif 19 | 20 | #if defined(__SAM3X8E__) 21 | typedef volatile RwReg PortReg; 22 | typedef uint32_t PortMask; 23 | #define HAVE_PORTREG 24 | #elif defined(ARDUINO_ARCH_SAMD) 25 | // not supported 26 | #elif defined(ESP8266) || defined(ESP32) || defined(ARDUINO_STM32_FEATHER) || defined(__arc__) 27 | typedef volatile uint32_t PortReg; 28 | typedef uint32_t PortMask; 29 | #elif defined(__AVR__) 30 | typedef volatile uint8_t PortReg; 31 | typedef uint8_t PortMask; 32 | #define HAVE_PORTREG 33 | #else 34 | // chances are its 32 bit so assume that 35 | typedef volatile uint32_t PortReg; 36 | typedef uint32_t PortMask; 37 | #endif 38 | 39 | #include 40 | #include 41 | 42 | #define BLACK 0 43 | #define WHITE 1 44 | #define INVERSE 2 45 | 46 | #define SSD1306_I2C_ADDRESS 0x3C // 011110+SA0+RW - 0x3C or 0x3D 47 | // Address for 128x32 is 0x3C 48 | // Address for 128x64 is 0x3D (default) or 0x3C (if SA0 is grounded) 49 | 50 | /*========================================================================= 51 | SSD1306 Displays 52 | ----------------------------------------------------------------------- 53 | The driver is used in multiple displays (128x64, 128x32, etc.). 54 | Select the appropriate display below to create an appropriately 55 | sized framebuffer, etc. 56 | 57 | SSD1306_128_64 128x64 pixel display 58 | 59 | SSD1306_128_32 128x32 pixel display 60 | 61 | SSD1306_96_16 62 | 63 | -----------------------------------------------------------------------*/ 64 | #define SSD1306_128_64 65 | // #define SSD1306_128_32 66 | // #define SSD1306_96_16 67 | /*=========================================================================*/ 68 | 69 | #if defined SSD1306_128_64 && defined SSD1306_128_32 70 | #error "Only one SSD1306 display can be specified at once in SSD1306.h" 71 | #endif 72 | #if !defined SSD1306_128_64 && !defined SSD1306_128_32 && !defined SSD1306_96_16 73 | #error "At least one SSD1306 display must be specified in SSD1306.h" 74 | #endif 75 | 76 | #if defined SSD1306_128_64 77 | #define SSD1306_LCDWIDTH 128 78 | #define SSD1306_LCDHEIGHT 64 79 | #endif 80 | #if defined SSD1306_128_32 81 | #define SSD1306_LCDWIDTH 128 82 | #define SSD1306_LCDHEIGHT 32 83 | #endif 84 | #if defined SSD1306_96_16 85 | #define SSD1306_LCDWIDTH 96 86 | #define SSD1306_LCDHEIGHT 16 87 | #endif 88 | 89 | #define SSD1306_SETCONTRAST 0x81 90 | #define SSD1306_DISPLAYALLON_RESUME 0xA4 91 | #define SSD1306_DISPLAYALLON 0xA5 92 | #define SSD1306_NORMALDISPLAY 0xA6 93 | #define SSD1306_INVERTDISPLAY 0xA7 94 | #define SSD1306_DISPLAYOFF 0xAE 95 | #define SSD1306_DISPLAYON 0xAF 96 | 97 | #define SSD1306_SETDISPLAYOFFSET 0xD3 98 | #define SSD1306_SETCOMPINS 0xDA 99 | 100 | #define SSD1306_SETVCOMDETECT 0xDB 101 | 102 | #define SSD1306_SETDISPLAYCLOCKDIV 0xD5 103 | #define SSD1306_SETPRECHARGE 0xD9 104 | 105 | #define SSD1306_SETMULTIPLEX 0xA8 106 | 107 | #define SSD1306_SETLOWCOLUMN 0x00 108 | #define SSD1306_SETHIGHCOLUMN 0x10 109 | 110 | #define SSD1306_SETSTARTLINE 0x40 111 | 112 | #define SSD1306_MEMORYMODE 0x20 113 | #define SSD1306_COLUMNADDR 0x21 114 | #define SSD1306_PAGEADDR 0x22 115 | 116 | #define SSD1306_COMSCANINC 0xC0 117 | #define SSD1306_COMSCANDEC 0xC8 118 | 119 | #define SSD1306_SEGREMAP 0xA0 120 | 121 | #define SSD1306_CHARGEPUMP 0x8D 122 | 123 | #define SSD1306_EXTERNALVCC 0x1 124 | #define SSD1306_SWITCHCAPVCC 0x2 125 | 126 | // Scrolling #defines 127 | #define SSD1306_ACTIVATE_SCROLL 0x2F 128 | #define SSD1306_DEACTIVATE_SCROLL 0x2E 129 | #define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3 130 | #define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26 131 | #define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27 132 | #define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29 133 | #define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A 134 | 135 | class Adafruit_SSD1306 : public Adafruit_GFX { 136 | public: 137 | Adafruit_SSD1306(int8_t SID, int8_t SCLK, int8_t DC, int8_t RST, int8_t CS); 138 | Adafruit_SSD1306(int8_t DC, int8_t RST, int8_t CS); 139 | Adafruit_SSD1306(int8_t RST = -1); 140 | #ifdef ESP32 //esp32 hardware spi with custom pins (MISO is unused, but needs some (unused) GPIO Number) 141 | Adafruit_SSD1306(int8_t mmiso, int8_t mmosi, int8_t msclk, int8_t DC, int8_t RST, int8_t CS, unsigned long clockspeed); 142 | #endif 143 | 144 | #if defined(ESP8266) || defined(ESP32) 145 | void begin(uint8_t switchvcc = SSD1306_SWITCHCAPVCC, uint8_t i2caddr = SSD1306_I2C_ADDRESS, bool reset=true, uint8_t sdapin=-1,uint8_t sclpin=-1, unsigned long clockspeed=400000UL); 146 | #else 147 | void begin(uint8_t switchvcc = SSD1306_SWITCHCAPVCC, uint8_t i2caddr = SSD1306_I2C_ADDRESS, bool reset=true); 148 | #endif 149 | 150 | void ssd1306_command(uint8_t c); 151 | 152 | void clearDisplay(void); 153 | void invertDisplay(uint8_t i); 154 | void display(); 155 | 156 | void startscrollright(uint8_t start, uint8_t stop); 157 | void startscrollleft(uint8_t start, uint8_t stop); 158 | 159 | void startscrolldiagright(uint8_t start, uint8_t stop); 160 | void startscrolldiagleft(uint8_t start, uint8_t stop); 161 | void stopscroll(void); 162 | 163 | void dim(boolean dim); 164 | 165 | void drawPixel(int16_t x, int16_t y, uint16_t color); 166 | 167 | virtual void drawFastVLine(int16_t x, int16_t y, int16_t h, uint16_t color); 168 | virtual void drawFastHLine(int16_t x, int16_t y, int16_t w, uint16_t color); 169 | 170 | private: 171 | int8_t _i2caddr, _vccstate, sid, sclk, dc, rst, cs; 172 | void fastSPIwrite(uint8_t c); 173 | #ifdef ESP32 174 | int8_t mymiso, mysclk; 175 | #endif 176 | #if defined(ESP8266) || defined(ESP32) 177 | unsigned long _clockspeed; 178 | uint8_t _sdapin; 179 | uint8_t _sclpin; 180 | #endif 181 | 182 | 183 | boolean hwSPI; 184 | #ifdef HAVE_PORTREG 185 | PortReg *mosiport, *clkport, *csport, *dcport; 186 | PortMask mosipinmask, clkpinmask, cspinmask, dcpinmask; 187 | #endif 188 | 189 | inline void drawFastVLineInternal(int16_t x, int16_t y, int16_t h, uint16_t color) __attribute__((always_inline)); 190 | inline void drawFastHLineInternal(int16_t x, int16_t y, int16_t w, uint16_t color) __attribute__((always_inline)); 191 | 192 | }; 193 | 194 | #endif /* _Adafruit_SSD1306_H_ */ 195 | -------------------------------------------------------------------------------- /Lora-TTNMapper-T-Beam-v10.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 sbiermann - https://github.com/sbiermann/Lora-TTNMapper-ESP32 3 | * Copyright 2019 hottimuc - https://github.com/hottimuc/Lora-TTNMapper-T-Beam 4 | * Copyright 2020 noppingen - https://www.onderka.com 5 | * 6 | * UPDATE the gps.h file in the same folder with your gateway GPS coordinates. 7 | * UPDATE the config.h file in the same folder with your TTN keys and device address. 8 | * 9 | * Node: TTGO T-Beam "T22_v1.0, 20190612" 10 | * --------------------------------------------------------------------------------------------- 11 | * LoRa: Reset GPIO 23 12 | * GPS: RX GPIO 34 and TX GPIO 12. 13 | * AXP192: I2C is at SDA GPIO 21 and SCK GPIO 22 14 | * Button PWR: 2s: AXP192 on / 6s: AXP192 off (Charging via USB stays active) 15 | * LED red: GPS fix, also on GPIO37 16 | 17 | * SF-CHANGE < 2 SEC. 18 | * INTERVAL-CHANGE = BUTTON PRESS FOR MIN. 2 / MAX 4 SEC. 19 | * ADR = BUTTON PRESS FOR MIN. 4 / MAX 6 SEC. 20 | * PORT-CHANGE = BUTTON PRESS FOR MIN. 6 / MAX 8 SEC. 21 | * NOTHING > 8 SEC. 22 | * 23 | */ 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include "config.h" 32 | #include "gps.h" 33 | #include "gpsicon.h" 34 | #include "Adafruit_GFX.h" 35 | #include "Adafruit_SSD1306.h" 36 | 37 | // T-Beam specific hardware, middle button 38 | #define SELECT_BTN 38 39 | 40 | // OLED reset pin 41 | #define OLED_RESET 4 42 | Adafruit_SSD1306 display(OLED_RESET); 43 | 44 | // Power management chip AXP192 45 | AXP20X_Class axp; 46 | bool axpIrq = 0; 47 | #define AXP192_SLAVE_ADDRESS 0x34 48 | const uint8_t axp_irq_pin = 35; 49 | 50 | String LoraStatus; 51 | 52 | /* Spread factor and TX interval */ 53 | int TX_Mode = 6; // Default: SF7-14 54 | int TX_Interval_Mode = 1; // Default: 30 second interval 55 | 56 | char s[32]=""; // used to sprintf for Serial output 57 | char sd[10]=""; // used to sprintf for Display sf-txpow output 58 | char iv[10]=""; // used to sprintf for Display tx-interval output 59 | 60 | // Misc 61 | unsigned int blinkGps = 0; 62 | boolean noFix = true; 63 | boolean redraw = false; 64 | boolean isDimmed = false; 65 | boolean GPSonceFixed = false; 66 | byte adr = 0; 67 | byte port = 1; 68 | 69 | // Set LED mode when packet is queued 70 | // AXP20X_LED_OFF - LED off 71 | // AXP20X_LED_LOW_LEVEL - LED on 72 | // AXP20X_LED_BLINK_1HZ - LED 1 blink/s 73 | // AXP20X_LED_BLINK_4HZ - LED 4 blink/s 74 | axp_chgled_mode_t ledMode = AXP20X_LED_LOW_LEVEL; 75 | 76 | uint8_t txBuffer[9]; 77 | uint16_t txBuffer2[5]; 78 | gps gps; 79 | Preferences prefs; 80 | 81 | #ifndef OTAA 82 | // These callbacks are only used in over-the-air activation, so they are 83 | // left empty here (we cannot leave them out completely unless 84 | // DISABLE_JOIN is set in config.h, otherwise the linker will complain). 85 | void os_getArtEui (u1_t* buf) { } 86 | void os_getDevEui (u1_t* buf) { } 87 | void os_getDevKey (u1_t* buf) { } 88 | #endif 89 | 90 | static osjob_t sendjob; 91 | // Schedule TX every this many seconds (might become longer due to duty cycle limitations). 92 | unsigned TX_INTERVAL = 15; 93 | unsigned long lastMillis = 0; 94 | unsigned long lastMillis2 = 0; 95 | 96 | // For battery mesurement 97 | float VBAT; // battery voltage from ESP32 ADC read 98 | 99 | const lmic_pinmap lmic_pins = { 100 | .nss = 18, 101 | .rxtx = LMIC_UNUSED_PIN, 102 | .rst = 23, // was "14," 103 | .dio = {26, 33, 32}, 104 | }; 105 | 106 | void do_send(osjob_t* j) { 107 | // Check if there is not a current TX/RX job running 108 | if (LMIC.opmode & OP_TXRXPEND) { 109 | Serial.println(F("OP_TXRXPEND, not sending")); 110 | LoraStatus = "OP_TXRXPEND, not sending"; 111 | } else { 112 | if (gps.checkGpsFix()) { 113 | // Prepare upstream data transmission at the next possible time. 114 | gps.buildPacket(txBuffer); 115 | LMIC_setTxData2(port, txBuffer, sizeof(txBuffer), 0); 116 | Serial.println(F("EV_PKG_QUEUED [LED on]")); 117 | axp.setChgLEDMode(ledMode); 118 | LoraStatus = "PKG_QUEUED"; 119 | } else { 120 | // Try again in 3 seconds 121 | os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(3), do_send); 122 | } 123 | } 124 | // Next TX: Scheduled after TX_COMPLETE event. 125 | } 126 | 127 | void sf_set() { 128 | if (TX_Mode==0) { 129 | LMIC_setDrTxpow(DR_SF7,17); 130 | sprintf(sd,"SF7-17"); 131 | } else if (TX_Mode==1) { 132 | LMIC_setDrTxpow(DR_SF8,17); 133 | sprintf(sd,"SF8-17"); 134 | } else if (TX_Mode==2) { 135 | LMIC_setDrTxpow(DR_SF9,17); 136 | sprintf(sd,"SF9-17"); 137 | } else if (TX_Mode==3) { 138 | LMIC_setDrTxpow(DR_SF10,17); 139 | sprintf(sd,"SF10-17"); 140 | } else if (TX_Mode==4) { 141 | LMIC_setDrTxpow(DR_SF11,17); 142 | sprintf(sd,"SF11-17"); 143 | } else if (TX_Mode==5) { 144 | LMIC_setDrTxpow(DR_SF12,17); 145 | sprintf(sd,"SF12-17"); 146 | } else if (TX_Mode==6) { 147 | LMIC_setDrTxpow(DR_SF7,14); 148 | sprintf(sd,"SF7-14"); 149 | } else if (TX_Mode==7) { 150 | LMIC_setDrTxpow(DR_SF8,14); 151 | sprintf(sd,"SF8-14"); 152 | } else if (TX_Mode==8) { 153 | LMIC_setDrTxpow(DR_SF9,14); 154 | sprintf(sd,"SF9-14"); 155 | } else if (TX_Mode==9) { 156 | LMIC_setDrTxpow(DR_SF10,14); 157 | sprintf(sd,"SF10-14"); 158 | } else if (TX_Mode==10) { 159 | LMIC_setDrTxpow(DR_SF11,14); 160 | sprintf(sd,"SF11-14"); 161 | } else if (TX_Mode==11) { 162 | LMIC_setDrTxpow(DR_SF12,14); 163 | sprintf(sd,"SF12-14"); 164 | } 165 | } 166 | 167 | void iv_set() { 168 | if (TX_Interval_Mode==0) { 169 | TX_INTERVAL = 15; 170 | sprintf(iv,"15s"); 171 | } else if (TX_Interval_Mode==1) { 172 | TX_INTERVAL = 30; 173 | sprintf(iv,"30s"); 174 | } else if (TX_Interval_Mode==2) { 175 | TX_INTERVAL = 60; 176 | sprintf(iv,"60s"); 177 | } else if (TX_Interval_Mode==3) { 178 | TX_INTERVAL = 120; 179 | sprintf(iv,"2m"); 180 | } else if (TX_Interval_Mode==4) { 181 | TX_INTERVAL = 300; 182 | sprintf(iv,"5m"); 183 | } else if (TX_Interval_Mode==5) { 184 | TX_INTERVAL = 600; 185 | sprintf(iv,"10m"); 186 | } else if (TX_Interval_Mode==6) { 187 | TX_INTERVAL = 1800; 188 | sprintf(iv,"30m"); 189 | } else if (TX_Interval_Mode==7) { 190 | TX_INTERVAL = 3600; 191 | sprintf(iv,"60m"); 192 | } 193 | } 194 | 195 | void sf_select() { 196 | if (digitalRead(SELECT_BTN) == LOW) { 197 | display.invertDisplay(true); 198 | delay(50); 199 | display.invertDisplay(false); 200 | ostime_t startTime = os_getTime(); 201 | int selection = 0; 202 | 203 | while (digitalRead(SELECT_BTN) == LOW) { 204 | if (((os_getTime()-startTime) > sec2osticks(2)) && (os_getTime()-startTime) <= sec2osticks(4)) { 205 | // INTERVAL-CHANGE = BUTTON PRESS FOR MIN. 2 / MAX 4 SEC. 206 | display.clearDisplay(); 207 | display.setTextColor(WHITE); 208 | display.setTextSize(2); 209 | TX_Interval_Mode++; 210 | if (TX_Interval_Mode > 7) { 211 | TX_Interval_Mode = 0; 212 | } 213 | iv_set(); 214 | display.setCursor(20,32); 215 | display.print("IV->"); 216 | display.print(iv); 217 | display.display(); 218 | TX_Interval_Mode--; 219 | if (TX_Interval_Mode < 0) { 220 | TX_Interval_Mode = 7; 221 | } 222 | iv_set(); 223 | delay(20); 224 | selection = 1; 225 | } 226 | 227 | if (((os_getTime()-startTime) >= sec2osticks(4)) && (os_getTime()-startTime) <= sec2osticks(6)) { 228 | // ADR = BUTTON PRESS FOR MIN. 4 / MAX 6 SEC. 229 | display.clearDisplay(); 230 | display.setTextColor(WHITE); 231 | display.setTextSize(2); 232 | display.setCursor(8,32); 233 | if (adr == 0) { 234 | display.print("ADR -> ON"); 235 | } else { 236 | display.print("ADR -> OFF"); 237 | } 238 | display.display(); 239 | delay(20); 240 | selection = 2; 241 | } 242 | 243 | if (((os_getTime()-startTime) > sec2osticks(6)) && (os_getTime()-startTime) <= sec2osticks(8)) { 244 | // PORT-CHANGE = BUTTON PRESS FOR MIN. 6 / MAX 8 SEC. 245 | display.clearDisplay(); 246 | display.setTextColor(WHITE); 247 | display.setTextSize(2); 248 | display.setCursor(10,32); 249 | display.print("PORT -> " + String(3 - port)); 250 | display.display(); 251 | delay(20); 252 | selection = 3; 253 | } 254 | if ((os_getTime()-startTime) > sec2osticks(8)) { 255 | // > 8 SEC. = NOTHING... 256 | display.clearDisplay(); 257 | display.display(); 258 | delay(20); 259 | selection = 4; 260 | } 261 | } 262 | 263 | // Long-Press 264 | 265 | if (selection == 1) { 266 | // Change Interval 267 | TX_Interval_Mode++; 268 | if (TX_Interval_Mode > 7) { 269 | TX_Interval_Mode = 0; 270 | } 271 | iv_set(); 272 | prefs.begin("nvs", false); 273 | prefs.putString("IV_MODE", String(TX_Interval_Mode)); 274 | prefs.end(); 275 | } 276 | 277 | if (selection == 2) { 278 | // ADR 279 | adr = 1 - adr; 280 | prefs.begin("nvs", false); 281 | prefs.putString("ADR", String(adr)); 282 | prefs.end(); 283 | LMIC_setAdrMode(adr); 284 | sf_set(); 285 | } 286 | 287 | if (selection == 3) { 288 | // Change Port 289 | port = 3 - port; // 1 -> 2, 2 -> 1 290 | prefs.begin("nvs", false); 291 | prefs.putString("PORT", String(port)); 292 | prefs.end(); 293 | } 294 | 295 | // Short-Press (SF&POWER) 296 | if (selection == 0) { 297 | TX_Mode++; 298 | if (TX_Mode >= 12) { 299 | TX_Mode = 0; 300 | } 301 | sf_set(); 302 | os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(2), do_send); 303 | prefs.begin("nvs", false); 304 | prefs.putString("SF_MODE", String(TX_Mode)); 305 | prefs.end(); 306 | } 307 | } 308 | } 309 | 310 | void onEvent (ev_t ev) { 311 | switch (ev) { 312 | case EV_SCAN_TIMEOUT: 313 | Serial.println(F("EV_SCAN_TIMEOUT")); 314 | LoraStatus = "EV_SCAN_TIMEOUT"; 315 | break; 316 | case EV_BEACON_FOUND: 317 | Serial.println(F("EV_BEACON_FOUND")); 318 | LoraStatus = "EV_BEACON_FOUND"; 319 | break; 320 | case EV_BEACON_MISSED: 321 | Serial.println(F("EV_BEACON_MISSED")); 322 | LoraStatus = "EV_BEACON_MISSED"; 323 | break; 324 | case EV_BEACON_TRACKED: 325 | Serial.println(F("EV_BEACON_TRACKED")); 326 | LoraStatus = "EV_BEACON_TRACKED"; 327 | break; 328 | case EV_JOINING: 329 | Serial.println(F("EV_JOINING")); 330 | LoraStatus = "EV_JOINING"; 331 | break; 332 | case EV_JOINED: 333 | Serial.println(F("EV_JOINED")); 334 | LoraStatus = "EV_JOINED"; 335 | // Don't ask for ACKs: Disable link check validation (automatically enabled during join, but not supported by TTN at this time). 336 | LMIC_setLinkCheckMode(0); 337 | break; 338 | case EV_RFU1: 339 | Serial.println(F("EV_RFU1")); 340 | LoraStatus = "EV_RFU1"; 341 | break; 342 | case EV_JOIN_FAILED: 343 | Serial.println(F("EV_JOIN_FAILED")); 344 | LoraStatus = "EV_JOIN_FAILED"; 345 | break; 346 | case EV_REJOIN_FAILED: 347 | Serial.println(F("EV_REJOIN_FAILED")); 348 | LoraStatus = "EV_REJOIN_FAILED"; 349 | break; 350 | case EV_TXCOMPLETE: 351 | Serial.println(F("EV_TXCOMPLETE [LED off]")); 352 | LoraStatus = "EV_TXCOMPLETE"; 353 | axp.setChgLEDMode(AXP20X_LED_OFF); 354 | if (LMIC.txrxFlags & TXRX_ACK) { 355 | Serial.println(F("Received Ack")); 356 | LoraStatus = "Received Ack"; 357 | } 358 | if (LMIC.dataLen) { 359 | sprintf(s, "Received %i bytes of payload", LMIC.dataLen); 360 | Serial.println(s); 361 | sprintf(s, "RSSI %d / SNR %.1d", LMIC.rssi, LMIC.snr); 362 | Serial.println(s); 363 | } 364 | // Schedule next transmission 365 | os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send); 366 | // do_send(&sendjob); 367 | break; 368 | case EV_LOST_TSYNC: 369 | Serial.println(F("EV_LOST_TSYNC")); 370 | LoraStatus = "EV_LOST_TSYNC"; 371 | break; 372 | case EV_RESET: 373 | Serial.println(F("EV_RESET")); 374 | LoraStatus = "EV_RESET"; 375 | break; 376 | case EV_RXCOMPLETE: 377 | // data received in ping slot 378 | Serial.println(F("EV_RXCOMPLETE")); 379 | LoraStatus = "EV_RXCOMPLETE"; 380 | break; 381 | case EV_LINK_DEAD: 382 | Serial.println(F("EV_LINK_DEAD")); 383 | LoraStatus = "EV_LINK_DEAD"; 384 | break; 385 | case EV_LINK_ALIVE: 386 | Serial.println(F("EV_LINK_ALIVE")); 387 | LoraStatus = "EV_LINK_ALIVE"; 388 | break; 389 | default: 390 | Serial.println(F("Unknown event")); 391 | LoraStatus = "Unknown event"; 392 | break; 393 | } 394 | } 395 | 396 | void setup() { 397 | Serial.begin(115200); 398 | Serial.println(); 399 | Serial.println(F("T-Beam TTNMapper")); 400 | Serial.println(F("----------------------------------------------")); 401 | 402 | /* Init I2C, SDA=21 / SCL=22 */ 403 | Wire.begin(21, 22); 404 | if (!axp.begin(Wire, AXP192_SLAVE_ADDRESS)) { 405 | Serial.println("AXP192 power controller init: Pass"); 406 | } else { 407 | Serial.println("AXP192 power controller init: Fail"); 408 | } 409 | 410 | // enable all irq channel 411 | axp.enableIRQ(0xFFFFFFFF, true); 412 | 413 | // attachInterrupt to gpio 35, HI -> LO (falling) 414 | pinMode(axp_irq_pin, INPUT_PULLUP); 415 | attachInterrupt(axp_irq_pin, [] { 416 | axpIrq = 1; 417 | Serial.println("AXP IRQ R!"); 418 | }, FALLING); 419 | axp.clearIRQ(); 420 | 421 | // Setup AXP192, OLED display at 3.3v 422 | axp.setDCDC1Voltage(3300); 423 | 424 | // Activate ADC 425 | axp.adc1Enable(AXP202_BATT_VOL_ADC1, true); 426 | axp.adc1Enable(AXP202_BATT_CUR_ADC1, true); 427 | axp.adc1Enable(AXP202_VBUS_VOL_ADC1, true); 428 | axp.adc1Enable(AXP202_VBUS_CUR_ADC1, true); 429 | 430 | // Activate power rails 431 | axp.setPowerOutPut(AXP192_LDO2, AXP202_ON); 432 | axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); 433 | axp.setPowerOutPut(AXP192_DCDC2, AXP202_ON); 434 | axp.setPowerOutPut(AXP192_EXTEN, AXP202_ON); 435 | axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); 436 | 437 | // Read preferences 438 | prefs.begin("nvs", false); 439 | TX_Mode = prefs.getString("SF_MODE", "0").toInt(); 440 | TX_Interval_Mode = prefs.getString("IV_MODE", "0").toInt(); 441 | adr = prefs.getString("ADR", "0").toInt(); 442 | port = prefs.getString("PORT", "1").toInt(); 443 | TX_Interval_Mode = prefs.getString("IV_MODE", "0").toInt(); 444 | prefs.end(); 445 | iv_set(); 446 | 447 | // UI Button 448 | pinMode(SELECT_BTN, INPUT); 449 | display.begin(SSD1306_SWITCHCAPVCC, 0x3C, 0, 21, 22, 800000); 450 | display.clearDisplay(); 451 | 452 | // Set text color 453 | display.setTextColor(WHITE); 454 | 455 | // Set font size 456 | display.setTextSize(2); 457 | 458 | // Set cursor position 459 | display.setCursor(1,16); 460 | 461 | // Show text 462 | display.print("TTN Mapper"); 463 | display.setCursor(0,32); 464 | display.print("----------"); 465 | display.setCursor(0,48); 466 | 467 | // TTN device ID 468 | display.print("0x26011EBC"); 469 | display.drawLine(0, 9, display.width(), 9, WHITE); 470 | display.setTextSize(1); 471 | display.setCursor(86,0); 472 | if (adr == 0) { 473 | display.print("ADR:OFF"); 474 | } else { 475 | display.print("ADR:ON"); 476 | } 477 | display.setCursor(42,00); 478 | display.print("IV:"); 479 | display.print(iv); 480 | display.setCursor(0,0); 481 | display.print("PORT:"+String(port)); 482 | display.display(); 483 | 484 | // Turn off WiFi and Bluetooth 485 | WiFi.mode(WIFI_OFF); 486 | btStop(); 487 | gps.init(); 488 | 489 | // LMIC init 490 | os_init(); 491 | // Reset the MAC state. Session and pending data transfers will be discarded. 492 | LMIC_reset(); 493 | 494 | // Set LMC clock error 495 | LMIC_setClockError(MAX_CLOCK_ERROR * 10 / 100); 496 | #ifndef OTAA 497 | // Init session 498 | LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY); 499 | #endif 500 | 501 | #ifdef CFG_eu868 502 | // 868 MHz SRD channels 503 | LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 504 | LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI); // g-band 505 | LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 506 | LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 507 | LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 508 | LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 509 | LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 510 | LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band 511 | LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK, DR_FSK), BAND_MILLI); // g2-band 512 | #endif 513 | 514 | #ifdef CFG_us915 515 | // 915 MHz band 516 | LMIC_selectSubBand(1); 517 | // Disable channels 16-72 518 | for (int i = 16; i < 73; i++) { 519 | if (i != 10) 520 | LMIC_disableChannel(i); 521 | } 522 | #endif 523 | 524 | // Don't ask for ACKs: Disable link check validation 525 | LMIC_setLinkCheckMode(0); 526 | 527 | // Disable/Enable ADR 528 | LMIC_setAdrMode(adr); 529 | 530 | // TheThingsNetwork uses SF9 for its RX2 window. 531 | LMIC.dn2Dr = DR_SF9; 532 | 533 | // Set data rate and transmit power for uplink (note: Txpow seems to be ignored by the library) 534 | // LMIC_setDrTxpow(DR_SF10,17); 535 | sf_set(); 536 | 537 | // Set Interval-Text corresponding to current setting 538 | iv_set(); 539 | 540 | do_send(&sendjob); 541 | axp.setChgLEDMode(AXP20X_LED_OFF); 542 | display.clearDisplay(); 543 | lastMillis2 = -1000; 544 | delay(2000); 545 | } 546 | 547 | void loop() { 548 | // AXP power management IRQ-handling 549 | if (axpIrq) { 550 | Serial.println("AXP IRQ!"); 551 | axpIrq = 0; 552 | axp.readIRQ(); 553 | if (axp.isPEKLongtPressIRQ()) { 554 | // switch LED off 555 | axp.setChgLEDMode(AXP20X_LED_OFF); 556 | delay(5000); 557 | } 558 | if (axp.isPEKShortPressIRQ()) { 559 | Serial.printf("AXP202 PEK key Short press\n"); 560 | // Reduce display-voltage to 1.7V 561 | if (isDimmed) { 562 | // End dimming OLED 563 | isDimmed = false; 564 | axp.setDCDC1Voltage(3300); 565 | ledMode = AXP20X_LED_LOW_LEVEL; 566 | } else { 567 | // Start dimming OLED 568 | isDimmed = true; 569 | axp.setDCDC1Voltage(1700); 570 | ledMode = AXP20X_LED_OFF; 571 | } 572 | 573 | } 574 | axp.clearIRQ(); 575 | } 576 | 577 | // Where are we? 578 | gps.encode(); 579 | sf_select(); 580 | if (lastMillis + 1000 < millis()) { 581 | lastMillis = millis(); 582 | VBAT = axp.getBattVoltage()/1000; 583 | 584 | os_runloop_once(); 585 | if (gps.checkGpsFix()) { 586 | // We had at least one GPS fix 587 | GPSonceFixed = true; 588 | noFix = false; 589 | gps.gdisplay(txBuffer2); 590 | float hdop = txBuffer2[4] / 10.0; 591 | display.clearDisplay(); 592 | display.setTextColor(WHITE); 593 | display.setTextSize(1); 594 | display.setCursor(0,0); 595 | display.print("SAT: " + String(txBuffer2[0])); 596 | display.setCursor(104,0); 597 | display.print(VBAT,1); 598 | display.setCursor(122,0); 599 | // Display battery charging state 600 | if (axp.isChargeing()) { 601 | // Charging: Uppercase 602 | display.print("V"); 603 | } else { 604 | // Discharging: Lowercase 605 | display.print("v"); 606 | } 607 | display.setCursor(0,10); 608 | display.print("Speed: " + String(txBuffer2[1])+ " km/h"); 609 | display.setCursor(0,20); 610 | display.print("Course: " + String(txBuffer2[2])+(char)247); 611 | display.setCursor(0,30); 612 | display.print("Alt: " + String(txBuffer2[3])+ "m"); 613 | display.setCursor(0,40); 614 | display.print("HDOP: "); 615 | display.setCursor(35,40); 616 | display.print(hdop,1); 617 | display.setCursor(80,20); // SF and TXpow 618 | display.print(sd); 619 | display.setCursor(80,30); // Sending interval 620 | display.print("Iv: "); 621 | display.print(iv); 622 | display.setCursor(80,40); // up packet number 623 | display.print("Up: " + String(LMIC.seqnoUp-1)); 624 | display.drawLine(0, 48, display.width(), 48, WHITE); 625 | display.setCursor(0,54); 626 | display.print("LoRa: "); 627 | display.setCursor(35,54); 628 | display.print(LoraStatus); 629 | 630 | if (gps.tGps.time.isValid()) { 631 | // GPS-Zeit vorhanden 632 | display.setCursor(48,0); 633 | if (gps.tGps.time.hour() < 10) display.print("0"); 634 | display.print(gps.tGps.time.hour()); 635 | display.print(":"); 636 | if (gps.tGps.time.minute() < 10) display.print("0"); 637 | display.print(gps.tGps.time.minute()); 638 | display.print(":"); 639 | if (gps.tGps.time.second() < 10) display.print("0"); 640 | display.print(gps.tGps.time.second()); 641 | } 642 | } else { 643 | // No GPS fix yet 644 | noFix = true; 645 | display.clearDisplay(); 646 | display.setTextColor(WHITE); 647 | display.setTextSize(2); 648 | display.setCursor(0,16); 649 | // Change wording after first successful fix 650 | if (GPSonceFixed) { 651 | display.print("Lost"); 652 | } else { 653 | display.print("Missing"); 654 | } 655 | display.setCursor(0,32); 656 | display.print("GPS fix"); 657 | display.setCursor(0,48); // SF and TXpow 658 | display.print(sd); 659 | display.setTextSize(1); 660 | display.setCursor(104,0); 661 | display.print(VBAT,1); 662 | display.setCursor(122,0); 663 | // Display battery charging state 664 | if (axp.isChargeing()) { 665 | // Charging: Uppercase 666 | display.print("V"); 667 | } else { 668 | // Discharging: Lowercase 669 | display.print("v"); 670 | } 671 | } 672 | redraw = true; 673 | } 674 | 675 | if ((lastMillis2 + 500 < millis()) || redraw) { 676 | lastMillis2 = millis(); 677 | if (noFix) 678 | { 679 | /* Disable ugly satellite icon ;) */ 680 | /* 681 | blinkGps = 1-blinkGps; 682 | if (blinkGps == 1) { 683 | display.fillRect((display.width() - imageWidthGpsIcon ), (display.height() - imageHeightGpsIcon), imageWidthGpsIcon, imageHeightGpsIcon, 0); 684 | display.drawBitmap( 685 | (display.width() - imageWidthGpsIcon ), 686 | (display.height() - imageHeightGpsIcon), 687 | gpsIcon, imageWidthGpsIcon, imageHeightGpsIcon, 1); 688 | } else { 689 | display.fillRect((display.width() - imageWidthGpsIcon ), (display.height() - imageHeightGpsIcon), imageWidthGpsIcon, imageHeightGpsIcon, 0); 690 | display.drawBitmap( 691 | (display.width() - imageWidthGpsIcon ), 692 | (display.height() - imageHeightGpsIcon), 693 | gpsIcon2, imageWidthGpsIcon, imageHeightGpsIcon, 1); 694 | } 695 | */ 696 | } 697 | redraw = true; 698 | } 699 | 700 | if (redraw) { 701 | redraw = false; 702 | display.display(); 703 | } 704 | } 705 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lora-TTNMapper-T-Beam-v10 2 | 3 | ## Summary 4 | 5 | Code for a TTNMapper node with GPS running on a [TTGO "T-Beam **v10**"](https://www.amazon.de/DollaTek-T-Beam-Drahtloses-Bluetooth-Batteriehalter/dp/B07RWY36ZY) node, based on [Lora-TTNMapper-T-Beam by DeuxVis](https://github.com/DeuxVis/Lora-TTNMapper-T-Beam) 6 | 7 | ## Copyright and sources 8 | 9 | * Copyright sbiermann - https://github.com/sbiermann/Lora-TTNMapper-ESP32 10 | * Copyright cyberman54 - https://github.com/cyberman54/ESP32-Paxcounter 11 | * Copyright Edzelf - https://github.com/Edzelf/LoRa 12 | * Copyright DeuxVis - https://github.com/DeuxVis/Lora-TTNMapper-T-Beam 13 | * Copyright 2020 noppingen - https://github.com/noppingen/Lora-TTNMapper-T-Beam-v10 14 | 15 | ## Modifications 16 | 17 | Hacked a quick & dirty fix for revision v1.0 (v10) boards marked **T22_v1.0, 20190612** 18 | 19 | 20 | * Change TX/RX pins in `gps.h` 21 | * Add support for the AXP20X power controller 22 | * Init / switch on the power controller 23 | * More diag output on serial interface 24 | * Display of distance to your mapped "home" gateway 25 | 26 | ## Get it running 27 | 28 | * Add required libraries 29 | * Set TTN `NWKSKEY`, `APPKSKEY` and `DEVADDR` in `config.h` 30 | * Set gateway GPS coordinates to get the distance displayed on your way in `gps.h` 31 | * Change bands if you are not in the EU868 region 32 | * Print my case: [Thingiverse](https://www.thingiverse.com/thing:4147272) 33 | 34 | ## TTN 35 | 36 | * Activation method is set to ABP 37 | * TTN decoder script is [here](TTN-decoder.script) 38 | 39 | ## Images 40 | 41 | ![Mapper Node #1](ttnmapper_t-beam_v10_01.jpg) 42 | 43 | ![Mapper Node #2](ttnmapper_t-beam_v10_02.jpg) 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | // Comment the next line to use ABP authentication on TTN. 2 | // Leave it as it is to use recommended OTAA 3 | //#define OTAA 4 | 5 | #ifndef LORA_TTNMAPPER_TBEAM_CONFIG_INCLUDED 6 | #define LORA_TTNMAPPER_TBEAM_CONFIG_INCLUDED 7 | 8 | #ifndef OTAA 9 | // Settings for ABP 10 | static PROGMEM u1_t NWKSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // LoRaWAN NwkSKey, network session key 11 | static u1_t PROGMEM APPSKEY[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // LoRaWAN AppSKey, application session key 12 | static const u4_t DEVADDR = 0xffffffff ; // LoRaWAN end-device address (DevAddr) 13 | #else 14 | // Settings from OTAA device 15 | static const u1_t PROGMEM DEVEUI[8]={ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11 }; // Device EUI, hex, lsb 16 | static const u1_t PROGMEM APPEUI[8]={ 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11 }; // Application EUI, hex, lsb 17 | static const u1_t PROGMEM APPKEY[16] = { 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11 }; // App Key, hex, msb 18 | 19 | void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8); } 20 | void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8); } 21 | void os_getDevKey (u1_t* buf) { memcpy_P(buf, APPKEY, 16); } 22 | #endif 23 | 24 | #endif //LORA_TTNMAPPER_TBEAM_CONFIG_INCLUDED 25 | -------------------------------------------------------------------------------- /gps.cpp: -------------------------------------------------------------------------------- 1 | #include "gps.h" 2 | 3 | HardwareSerial GPSSerial(1); 4 | 5 | void gps::init() { 6 | GPSSerial.begin(9600, SERIAL_8N1, GPS_TX, GPS_RX); 7 | GPSSerial.setTimeout(2); 8 | } 9 | 10 | void gps::encode() { 11 | int data; 12 | unsigned long previousMillis = millis(); 13 | 14 | while((previousMillis + 100) > millis()) { 15 | while (GPSSerial.available() ) { 16 | char data = GPSSerial.read(); 17 | tGps.encode(data); 18 | //Serial.print(data); 19 | } 20 | } 21 | //Serial.println(""); 22 | } 23 | 24 | void gps::buildPacket(uint8_t txBuffer[9]) { 25 | LatitudeBinary = ((tGps.location.lat() + 90) / 180.0) * 16777215; 26 | LongitudeBinary = ((tGps.location.lng() + 180) / 360.0) * 16777215; 27 | double gw_distance_m = tGps.distanceBetween(tGps.location.lat(), tGps.location.lng(), HOME_LAT, HOME_LNG); 28 | float gw_distance_km = gw_distance_m/1000; 29 | double gw_course = tGps.courseTo(tGps.location.lat(), tGps.location.lng(), HOME_LAT, HOME_LNG); 30 | int hour = tGps.time.hour(); 31 | int minute = tGps.time.minute(); 32 | int second = tGps.time.second(); 33 | 34 | Serial.println(); 35 | Serial.println(F("GPS Data")); 36 | Serial.println(F("----------------------------------------------")); 37 | 38 | sprintf(t, "Latitude: %f", tGps.location.lat()); 39 | Serial.println(t); 40 | 41 | sprintf(t, "Longitude: %f", tGps.location.lng()); 42 | Serial.println(t); 43 | 44 | sprintf(t, "Altitude: %.0f m", tGps.altitude.meters()); 45 | Serial.println(t); 46 | 47 | sprintf(t, "Speed: %.2f km/h", tGps.speed.kmph()); 48 | Serial.println(t); 49 | 50 | sprintf(t, "HDOP: %.2lf", tGps.hdop.value()/100.0); 51 | Serial.println(t); 52 | 53 | sprintf(t, "Satellites: %i", tGps.satellites.value()); 54 | Serial.println(t); 55 | 56 | Serial.println(F("----------------------------------------------")); 57 | 58 | Serial.print("Dst to Gateway: "); 59 | Serial.print(gw_distance_m); 60 | Serial.print(" m to "); 61 | Serial.println(tGps.cardinal(gw_course)); 62 | 63 | Serial.println(F("----------------------------------------------")); 64 | Serial.println(); 65 | 66 | // Build LoRa payload 67 | txBuffer[0] = ( LatitudeBinary >> 16 ) & 0xFF; 68 | txBuffer[1] = ( LatitudeBinary >> 8 ) & 0xFF; 69 | txBuffer[2] = LatitudeBinary & 0xFF; 70 | 71 | txBuffer[3] = ( LongitudeBinary >> 16 ) & 0xFF; 72 | txBuffer[4] = ( LongitudeBinary >> 8 ) & 0xFF; 73 | txBuffer[5] = LongitudeBinary & 0xFF; 74 | 75 | altitudeGps = tGps.altitude.meters(); 76 | txBuffer[6] = ( altitudeGps >> 8 ) & 0xFF; 77 | txBuffer[7] = altitudeGps & 0xFF; 78 | 79 | hdopGps = tGps.hdop.value()/10; 80 | txBuffer[8] = hdopGps & 0xFF; 81 | } 82 | 83 | void gps::gdisplay(uint16_t txBuffer2[5]) { 84 | txBuffer2[0] = tGps.satellites.value(); 85 | txBuffer2[1] = tGps.speed.kmph(); 86 | txBuffer2[2] = tGps.course.deg(); 87 | txBuffer2[3] = tGps.altitude.meters(); 88 | txBuffer2[4] = tGps.hdop.value()/10; 89 | } 90 | 91 | bool gps::checkGpsFix() { 92 | encode(); 93 | if (tGps.location.isValid() && 94 | tGps.location.age() < 4000 && 95 | tGps.hdop.isValid() && 96 | tGps.hdop.value() <= 600 && 97 | tGps.hdop.age() < 4000 && 98 | tGps.altitude.isValid() && 99 | tGps.altitude.age() < 4000 ) 100 | { 101 | // Serial.println("Valid GPS fix"); 102 | return true; 103 | } else { 104 | Serial.println("No GPS fix"); 105 | // sprintf(t, "location valid: %i" , tGps.location.isValid()); 106 | // Serial.println(t); 107 | // sprintf(t, "location age: %i" , tGps.location.age()); 108 | // Serial.println(t); 109 | // sprintf(t, "hdop valid: %i" , tGps.hdop.isValid()); 110 | // Serial.println(t); 111 | // sprintf(t, "hdop age: %i" , tGps.hdop.age()); 112 | // Serial.println(t); 113 | // sprintf(t, "hdop: %i" , tGps.hdop.value()); 114 | // Serial.println(t); 115 | // sprintf(t, "altitude valid: %i" , tGps.altitude.isValid()); 116 | // Serial.println(t); 117 | // sprintf(t, "altitude age: %i" , tGps.altitude.age()); 118 | // Serial.println(t); 119 | return false; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /gps.h: -------------------------------------------------------------------------------- 1 | #ifndef __GPS_H__ 2 | #define __GPS_H__ 3 | 4 | #include 5 | #include 6 | 7 | /* GPS serial pins */ 8 | #define GPS_TX 34 9 | #define GPS_RX 12 10 | 11 | /* GPS coordinates of mapped gateway for calculating the distance */ 12 | const double HOME_LAT = 49.000000; 13 | const double HOME_LNG = 11.000000; 14 | 15 | class gps { 16 | public: 17 | void init(); 18 | bool checkGpsFix(); 19 | void buildPacket(uint8_t txBuffer[9]); 20 | void gdisplay(uint16_t txBuffer2[5]); 21 | void encode(); 22 | TinyGPSPlus tGps; 23 | 24 | private: 25 | uint32_t LatitudeBinary, LongitudeBinary; 26 | uint16_t altitudeGps; 27 | uint8_t hdopGps; 28 | char t[32]; // used to sprintf for Serial output 29 | }; 30 | #endif 31 | -------------------------------------------------------------------------------- /gpsicon.h: -------------------------------------------------------------------------------- 1 | // Filename: gps.png 2 | // Filesize: 258 Bytes 3 | 4 | // Size bitmap: 128 bytes 5 | 6 | #define imageWidthGpsIcon 32 7 | #define imageHeightGpsIcon 32 8 | 9 | static const uint8_t gpsIcon PROGMEM [] = 10 | { 11 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x80, 0xFC, 12 | 0x00, 0x07, 0xCF, 0xC6, 0x00, 0x0F, 0xFC, 0x02, 0x00, 0x0F, 0xF0, 0x02, 0x00, 0x7F, 0xF0, 0x03, 13 | 0x07, 0xDF, 0xF0, 0x1F, 0x7C, 0x0F, 0xF1, 0xF8, 0xE0, 0x0F, 0xFF, 0x80, 0x40, 0x07, 0xF0, 0x00, 14 | 0x40, 0x0F, 0xF0, 0x00, 0x60, 0xFB, 0xF0, 0x00, 0x6F, 0xC3, 0xC0, 0x00, 0x3C, 0x00, 0x00, 0x00, 15 | 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x1C, 0x40, 0x00, 0x01, 0xF0, 0xC0, 16 | 0x00, 0x00, 0x00, 0x88, 0x00, 0x08, 0x01, 0x88, 0x00, 0x0E, 0x07, 0x18, 0x00, 0x07, 0xFC, 0x30, 17 | 0x00, 0x00, 0xF0, 0x20, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x1C, 0x01, 0x80, 0x00, 0x07, 0xDF, 0x00, 18 | 0x00, 0x01, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 19 | }; 20 | 21 | static const uint8_t gpsIcon2 PROGMEM [] = 22 | { 23 | 0x40, 0x40, 0x00, 0x00, 0xA4, 0xA0, 0x00, 0x00, 0x2A, 0x20, 0x00, 0x0C, 0x42, 0x40, 0x80, 0xFC, 24 | 0x04, 0x07, 0xCF, 0xC6, 0x40, 0x4F, 0xFC, 0x02, 0x04, 0x0F, 0xF0, 0x02, 0x00, 0x7F, 0xF0, 0x03, 25 | 0x07, 0xDF, 0xF0, 0x1F, 0x7C, 0x0F, 0xF1, 0xF8, 0xE0, 0x0F, 0xFF, 0x80, 0x40, 0x07, 0xF0, 0x00, 26 | 0x40, 0x0F, 0xF0, 0x00, 0x60, 0xFB, 0xF0, 0x00, 0x6F, 0xC3, 0xC0, 0x00, 0x3C, 0x00, 0x00, 0x00, 27 | 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x03, 0x1C, 0x40, 0x00, 0x01, 0xF0, 0xC0, 28 | 0x00, 0x00, 0x00, 0x88, 0x00, 0x08, 0x01, 0x88, 0x00, 0x0E, 0x07, 0x18, 0x00, 0x07, 0xFC, 0x30, 29 | 0x00, 0x00, 0xF0, 0x20, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x1C, 0x01, 0x80, 0x00, 0x07, 0xDF, 0x00, 30 | 0x00, 0x01, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 31 | }; 32 | -------------------------------------------------------------------------------- /ttnmapper_t-beam_v10_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppingen/Lora-TTNMapper-T-Beam-v10/78f407a49dffad3fc520b8e372174ae9c404e353/ttnmapper_t-beam_v10_01.jpg -------------------------------------------------------------------------------- /ttnmapper_t-beam_v10_02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppingen/Lora-TTNMapper-T-Beam-v10/78f407a49dffad3fc520b8e372174ae9c404e353/ttnmapper_t-beam_v10_02.jpg --------------------------------------------------------------------------------