Display.h
   1  // Copyright (C) 2024, Mark Qvist
   2  
   3  // This program is free software: you can redistribute it and/or modify
   4  // it under the terms of the GNU General Public License as published by
   5  // the Free Software Foundation, either version 3 of the License, or
   6  // (at your option) any later version.
   7  
   8  // This program is distributed in the hope that it will be useful,
   9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11  // GNU General Public License for more details.
  12  
  13  // You should have received a copy of the GNU General Public License
  14  // along with this program.  If not, see <https://www.gnu.org/licenses/>.
  15  
  16  #include "Graphics.h"
  17  #include <Adafruit_GFX.h>
  18  
  19  #if BOARD_MODEL != BOARD_TECHO
  20    #if BOARD_MODEL == BOARD_TDECK
  21      #include <Adafruit_ST7789.h>
  22    #elif BOARD_MODEL == BOARD_HELTEC_T114
  23      #include "ST7789.h"
  24      #define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3))
  25    #elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_STATION_G2
  26      #include <Adafruit_SH110X.h>
  27    #else
  28      #include <Wire.h>
  29      #include <Adafruit_SSD1306.h>
  30    #endif
  31  
  32  #else
  33    void (*display_callback)();
  34    void display_add_callback(void (*callback)()) { display_callback = callback; }
  35    void busyCallback(const void* p) { display_callback(); }
  36    #define SSD1306_BLACK GxEPD_BLACK
  37    #define SSD1306_WHITE GxEPD_WHITE
  38    #include <GxEPD2_BW.h>
  39    #include <SPI.h>
  40  #endif
  41  
  42  #include "Fonts/Org_01.h"
  43  #define DISP_W 128
  44  #define DISP_H 64
  45  
  46  #if BOARD_MODEL == BOARD_RNODE_NG_20 || BOARD_MODEL == BOARD_LORA32_V2_0
  47    #define DISP_RST -1
  48    #define DISP_ADDR 0x3C
  49  #elif BOARD_MODEL == BOARD_TBEAM
  50    #define DISP_RST 13
  51    #define DISP_ADDR 0x3C
  52    #define DISP_CUSTOM_ADDR true
  53  #elif BOARD_MODEL == BOARD_HELTEC32_V2 || BOARD_MODEL == BOARD_LORA32_V1_0
  54    #define DISP_RST 16
  55    #define DISP_ADDR 0x3C
  56    #define SCL_OLED 15
  57    #define SDA_OLED 4
  58  #elif BOARD_MODEL == BOARD_HELTEC32_V3
  59    #define DISP_RST 21
  60    #define DISP_ADDR 0x3C
  61    #define SCL_OLED 18
  62    #define SDA_OLED 17
  63  #elif BOARD_MODEL == BOARD_HELTEC32_V4
  64    #define DISP_RST 21
  65    #define DISP_ADDR 0x3C
  66    #define SCL_OLED 18
  67    #define SDA_OLED 17
  68  #elif BOARD_MODEL == BOARD_RAK4631
  69    // RAK1921/SSD1306
  70    #define DISP_RST -1
  71    #define DISP_ADDR 0x3C
  72    #define SCL_OLED 14
  73    #define SDA_OLED 13
  74  #elif BOARD_MODEL == BOARD_RNODE_NG_21
  75    #define DISP_RST -1
  76    #define DISP_ADDR 0x3C
  77  #elif BOARD_MODEL == BOARD_T3S3
  78    #define DISP_RST 21
  79    #define DISP_ADDR 0x3C
  80    #define SCL_OLED 17
  81    #define SDA_OLED 18
  82  #elif BOARD_MODEL == BOARD_STATION_G2
  83    #define DISP_RST -1
  84    #define DISP_ADDR 0x3C
  85    #define SCL_OLED 6
  86    #define SDA_OLED 5
  87    #define DISP_CUSTOM_ADDR false
  88  #elif BOARD_MODEL == BOARD_TECHO
  89    SPIClass displaySPI = SPIClass(NRF_SPIM0, pin_disp_miso, pin_disp_sck, pin_disp_mosi);
  90    #define DISP_W 128
  91    #define DISP_H 64
  92    #define DISP_ADDR -1
  93  #elif BOARD_MODEL == BOARD_TBEAM_S_V1
  94    #define DISP_RST -1
  95    #define DISP_ADDR 0x3C
  96    #define SCL_OLED 18
  97    #define SDA_OLED 17
  98    #define DISP_CUSTOM_ADDR false
  99  #elif BOARD_MODEL == BOARD_XIAO_S3
 100    #define DISP_RST -1
 101    #define DISP_ADDR 0x3C
 102    #define SCL_OLED 6
 103    #define SDA_OLED 5
 104    #define DISP_CUSTOM_ADDR true
 105  #else
 106    #define DISP_RST -1
 107    #define DISP_ADDR 0x3C
 108    #define DISP_CUSTOM_ADDR true
 109  #endif
 110  
 111  #define SMALL_FONT &Org_01
 112  
 113  #if BOARD_MODEL == BOARD_TDECK
 114    Adafruit_ST7789 display = Adafruit_ST7789(DISPLAY_CS, DISPLAY_DC, -1);
 115    #define SSD1306_WHITE ST77XX_WHITE
 116    #define SSD1306_BLACK ST77XX_BLACK
 117  #elif BOARD_MODEL == BOARD_HELTEC_T114
 118    ST7789Spi display(&SPI1, DISPLAY_RST, DISPLAY_DC, DISPLAY_CS);
 119    #define SSD1306_WHITE ST77XX_WHITE
 120    #define SSD1306_BLACK ST77XX_BLACK
 121  #elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_STATION_G2
 122    Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);
 123    #define SSD1306_WHITE SH110X_WHITE
 124    #define SSD1306_BLACK SH110X_BLACK
 125  #elif BOARD_MODEL == BOARD_TECHO
 126    GxEPD2_BW<GxEPD2_154_D67, GxEPD2_154_D67::HEIGHT> display(GxEPD2_154_D67(pin_disp_cs, pin_disp_dc, pin_disp_reset, pin_disp_busy));
 127    uint32_t last_epd_refresh = 0;
 128    uint32_t last_epd_full_refresh = 0;
 129    #define REFRESH_PERIOD 300000
 130  #else
 131    Adafruit_SSD1306 display(DISP_W, DISP_H, &Wire, DISP_RST);
 132  #endif
 133  
 134  #if BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4
 135    float disp_target_fps = 5;
 136  #else
 137    float disp_target_fps = 7;
 138  #endif
 139  float epd_update_fps  = 0.5;
 140  
 141  #define DISP_MODE_UNKNOWN   0x00
 142  #define DISP_MODE_LANDSCAPE 0x01
 143  #define DISP_MODE_PORTRAIT  0x02
 144  #define DISP_PIN_SIZE   6
 145  #define DISPLAY_BLANKING_TIMEOUT 15*1000
 146  uint8_t disp_mode = DISP_MODE_UNKNOWN;
 147  uint8_t disp_ext_fb = false;
 148  unsigned char fb[512];
 149  uint32_t last_disp_update = 0;
 150  uint32_t last_unblank_event = 0;
 151  uint32_t display_blanking_timeout = DISPLAY_BLANKING_TIMEOUT;
 152  uint8_t display_unblank_intensity = display_intensity;
 153  bool display_blanked = false;
 154  bool display_tx = false;
 155  bool recondition_display = false;
 156  int disp_update_interval = 1000/disp_target_fps;
 157  int epd_update_interval = 1000/disp_target_fps;
 158  // While blanked, refresh far less often (OLED is off/dim); big idle save vs full FPS.
 159  #define DISP_BLANK_UPDATE_INTERVAL_MS 2000
 160  int disp_blank_update_interval = DISP_BLANK_UPDATE_INTERVAL_MS;
 161  uint32_t last_page_flip = 0;
 162  int page_interval = 4000;
 163  bool device_signatures_ok();
 164  bool device_firmware_ok();
 165  
 166  #define WATERFALL_SIZE 46
 167  int waterfall[WATERFALL_SIZE];
 168  int waterfall_head = 0;
 169  
 170  int p_ad_x = 0;
 171  int p_ad_y = 0;
 172  int p_as_x = 0;
 173  int p_as_y = 0;
 174  
 175  GFXcanvas1 stat_area(64, 64);
 176  GFXcanvas1 disp_area(64, 64);
 177  
 178  static const uint8_t one_counts[256] = {
 179    0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  1,  2,  1,  1,  1,  1,
 180    1,  1,  1,  1,  0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,
 181    0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0,  0,  0,  0,
 182    0,  0,  0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,
 183    0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0,  0,  0,  0,  0,  0,
 184    0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0,  0,
 185    0,  0,  0,  0,  1,  2,  1,  1,  1,  1,  1,  1,  1,  1,  2,  3,
 186    2,  2,  2,  2,  2,  2,  2,  2,  1,  2,  1,  1,  1,  1,  1,  1,
 187    1,  1,  1,  2,  1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  1,  1,
 188    1,  1,  1,  1,  1,  1,  1,  2,  1,  1,  1,  1,  1,  1,  1,  1,
 189    1,  2,  1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  1,  1,  1,  1,
 190    1,  1,  1,  1,  1,  2,  1,  1,  1,  1,  1,  1,  1,  1,  1,  2,
 191    1,  1,  1,  1,  1,  1,  1,  1,  0,  1,  0,  0,  0,  0,  0,  0,
 192    0,  0,  1,  2,  1,  1,  1,  1,  1,  1,  1,  1,  0,  1,  0,  0,
 193    0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0,  0,  0,  0,  0,  0,
 194    0,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0,  0
 195  };
 196  
 197  void fillRect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour);
 198  
 199  void update_area_positions() {
 200    #if BOARD_MODEL == BOARD_HELTEC_T114
 201      if (disp_mode == DISP_MODE_PORTRAIT) {
 202        p_ad_x = 16;
 203        p_ad_y = 64;
 204        p_as_x = 16;
 205        p_as_y = p_ad_y+126;
 206      } else if (disp_mode == DISP_MODE_LANDSCAPE) {
 207        p_ad_x = 0;
 208        p_ad_y = 96;
 209        p_as_x = 126;
 210        p_as_y = p_ad_y;
 211      }
 212    #elif BOARD_MODEL == BOARD_TECHO
 213      if (disp_mode == DISP_MODE_PORTRAIT) {
 214        p_ad_x = 61;
 215        p_ad_y = 36;
 216        p_as_x = 64;
 217        p_as_y = 64+36;
 218      } else if (disp_mode == DISP_MODE_LANDSCAPE) {
 219        p_ad_x = 0;
 220        p_ad_y = 0;
 221        p_as_x = 64;
 222        p_as_y = 0;
 223      }
 224    #else
 225      if (disp_mode == DISP_MODE_PORTRAIT) {
 226        p_ad_x = 0 * DISPLAY_SCALE;
 227        p_ad_y = 0 * DISPLAY_SCALE;
 228        p_as_x = 0 * DISPLAY_SCALE;
 229        p_as_y = 64 * DISPLAY_SCALE;
 230      } else if (disp_mode == DISP_MODE_LANDSCAPE) {
 231        p_ad_x = 0 * DISPLAY_SCALE;
 232        p_ad_y = 0 * DISPLAY_SCALE;
 233        p_as_x = 64 * DISPLAY_SCALE;
 234        p_as_y = 0 * DISPLAY_SCALE;
 235      }
 236    #endif
 237  }
 238  
 239  uint8_t display_contrast = 0x00;
 240  #if BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_STATION_G2
 241    void set_contrast(Adafruit_SH1106G *display, uint8_t value) {
 242    }
 243  #elif BOARD_MODEL == BOARD_HELTEC_T114
 244    void set_contrast(ST7789Spi *display, uint8_t value) { }
 245  #elif BOARD_MODEL == BOARD_TECHO
 246    void set_contrast(void *display, uint8_t value) {
 247      if (value == 0) { analogWrite(pin_backlight, 0); }
 248      else            { analogWrite(pin_backlight, value); }
 249    }
 250  #elif BOARD_MODEL == BOARD_TDECK
 251    void set_contrast(Adafruit_ST7789 *display, uint8_t value) {
 252      static uint8_t level = 0;
 253      static uint8_t steps = 16;
 254      if (value > 15) value = 15;
 255      if (value == 0) {
 256          digitalWrite(DISPLAY_BL_PIN, 0);
 257          delay(3);
 258          level = 0;
 259          return;
 260      }
 261      if (level == 0) {
 262          digitalWrite(DISPLAY_BL_PIN, 1);
 263          level = steps;
 264          delayMicroseconds(30);
 265      }
 266      int from = steps - level;
 267      int to = steps - value;
 268      int num = (steps + to - from) % steps;
 269      for (int i = 0; i < num; i++) {
 270          digitalWrite(DISPLAY_BL_PIN, 0);
 271          digitalWrite(DISPLAY_BL_PIN, 1);
 272      }
 273      level = value;
 274    }
 275  #else
 276    void set_contrast(Adafruit_SSD1306 *display, uint8_t contrast) {
 277      display->ssd1306_command(SSD1306_SETCONTRAST);
 278      display->ssd1306_command(contrast);
 279    }
 280  #endif
 281  
 282  bool display_init() {
 283    #if HAS_DISPLAY
 284      #if BOARD_MODEL == BOARD_RNODE_NG_20 || BOARD_MODEL == BOARD_LORA32_V2_0
 285        int pin_display_en = 16;
 286        digitalWrite(pin_display_en, LOW);
 287        delay(50);
 288        digitalWrite(pin_display_en, HIGH);
 289      #elif BOARD_MODEL == BOARD_T3S3
 290        Wire.begin(SDA_OLED, SCL_OLED);
 291      #elif BOARD_MODEL == BOARD_HELTEC32_V2
 292        Wire.begin(SDA_OLED, SCL_OLED);
 293      #elif BOARD_MODEL == BOARD_HELTEC32_V3
 294        // enable vext / pin 36
 295        pinMode(Vext, OUTPUT);
 296        digitalWrite(Vext, LOW);
 297        delay(50);
 298        int pin_display_en = 21;
 299        pinMode(pin_display_en, OUTPUT);
 300        digitalWrite(pin_display_en, LOW);
 301        delay(50);
 302        digitalWrite(pin_display_en, HIGH);
 303        delay(50);
 304        Wire.begin(SDA_OLED, SCL_OLED);
 305      #elif BOARD_MODEL == BOARD_HELTEC32_V4
 306        // enable vext / pin 36
 307        pinMode(Vext, OUTPUT);
 308        digitalWrite(Vext, LOW);
 309        delay(50);
 310        int pin_display_en = 21;
 311        pinMode(pin_display_en, OUTPUT);
 312        digitalWrite(pin_display_en, LOW);
 313        delay(50);
 314        digitalWrite(pin_display_en, HIGH);
 315        delay(50);
 316        Wire.begin(SDA_OLED, SCL_OLED);
 317      #elif BOARD_MODEL == BOARD_LORA32_V1_0
 318        int pin_display_en = 16;
 319        digitalWrite(pin_display_en, LOW);
 320        delay(50);
 321        digitalWrite(pin_display_en, HIGH);
 322        Wire.begin(SDA_OLED, SCL_OLED);
 323      #elif BOARD_MODEL == BOARD_HELTEC_T114
 324        pinMode(PIN_T114_TFT_EN, OUTPUT);
 325        digitalWrite(PIN_T114_TFT_EN, LOW);
 326      #elif BOARD_MODEL == BOARD_TECHO
 327        display.init(0, true, 10, false, displaySPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
 328        display.setPartialWindow(0, 0, DISP_W, DISP_H);
 329        display.epd2.setBusyCallback(busyCallback);
 330        #if HAS_BACKLIGHT
 331          pinMode(pin_backlight, OUTPUT);
 332          analogWrite(pin_backlight, 0);
 333        #endif
 334      #elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_STATION_G2
 335        Wire.begin(SDA_OLED, SCL_OLED);
 336      #elif BOARD_MODEL == BOARD_XIAO_S3
 337        Wire.begin(SDA_OLED, SCL_OLED);
 338      #endif
 339  
 340      #if HAS_EEPROM
 341        uint8_t display_rotation = EEPROM.read(eeprom_addr(ADDR_CONF_DROT));
 342      #elif MCU_VARIANT == MCU_NRF52
 343        uint8_t display_rotation = eeprom_read(eeprom_addr(ADDR_CONF_DROT));
 344      #endif
 345      if (display_rotation < 0 or display_rotation > 3) display_rotation = 0xFF;
 346  
 347      #if DISP_CUSTOM_ADDR == true
 348        #if HAS_EEPROM
 349          uint8_t display_address = EEPROM.read(eeprom_addr(ADDR_CONF_DADR));
 350        #elif MCU_VARIANT == MCU_NRF52
 351          uint8_t display_address = eeprom_read(eeprom_addr(ADDR_CONF_DADR));
 352        #endif
 353        if (display_address == 0xFF) display_address = DISP_ADDR;
 354      #else
 355        uint8_t display_address = DISP_ADDR;
 356      #endif
 357  
 358      #if HAS_EEPROM
 359        if (EEPROM.read(eeprom_addr(ADDR_CONF_BSET)) == CONF_OK_BYTE) {
 360          uint8_t db_timeout = EEPROM.read(eeprom_addr(ADDR_CONF_DBLK));
 361          if (db_timeout == 0x00) {
 362            display_blanking_enabled = false;
 363          } else {
 364            display_blanking_enabled = true;
 365            display_blanking_timeout = db_timeout*1000;
 366          }
 367        }
 368      #elif MCU_VARIANT == MCU_NRF52
 369        if (eeprom_read(eeprom_addr(ADDR_CONF_BSET)) == CONF_OK_BYTE) {
 370          uint8_t db_timeout = eeprom_read(eeprom_addr(ADDR_CONF_DBLK));
 371          if (db_timeout == 0x00) {
 372            display_blanking_enabled = false;
 373          } else {
 374            display_blanking_enabled = true;
 375            display_blanking_timeout = db_timeout*1000;
 376          }
 377        }
 378      #endif
 379  
 380      #if HAS_EEPROM && (BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4)
 381        // If blanking was never stored in EEPROM, default to 15s idle blank (see DISPLAY_BLANKING_TIMEOUT).
 382        if (EEPROM.read(eeprom_addr(ADDR_CONF_BSET)) != CONF_OK_BYTE) {
 383          display_blanking_enabled = true;
 384          display_blanking_timeout = DISPLAY_BLANKING_TIMEOUT;
 385        }
 386      #endif
 387      
 388      #if BOARD_MODEL == BOARD_TECHO
 389      // Don't check if display is actually connected
 390      if(false) {
 391      #elif BOARD_MODEL == BOARD_TDECK
 392      display.init(240, 320);
 393      display.setSPISpeed(80e6);
 394      #elif BOARD_MODEL == BOARD_HELTEC_T114
 395      display.init();
 396      // set white as default pixel colour for Heltec T114
 397      display.setRGB(COLOR565(0xFF, 0xFF, 0xFF));
 398      if (false) {
 399      #elif BOARD_MODEL == BOARD_TBEAM_S_V1 || BOARD_MODEL == BOARD_STATION_G2
 400      if (!display.begin(display_address, true)) {
 401      #else
 402      if (!display.begin(SSD1306_SWITCHCAPVCC, display_address)) {
 403      #endif
 404        return false;
 405      } else {
 406        set_contrast(&display, display_contrast);
 407        if (display_rotation != 0xFF) {
 408          if (display_rotation == 0 || display_rotation == 2) {
 409            disp_mode = DISP_MODE_LANDSCAPE;
 410          } else {
 411            disp_mode = DISP_MODE_PORTRAIT;
 412          }
 413          display.setRotation(display_rotation);
 414        } else {
 415          #if BOARD_MODEL == BOARD_RNODE_NG_20
 416            disp_mode = DISP_MODE_PORTRAIT;
 417            display.setRotation(3);
 418          #elif BOARD_MODEL == BOARD_RNODE_NG_21
 419            disp_mode = DISP_MODE_PORTRAIT;
 420            display.setRotation(3);
 421          #elif BOARD_MODEL == BOARD_LORA32_V1_0
 422            disp_mode = DISP_MODE_PORTRAIT;
 423            display.setRotation(3);
 424          #elif BOARD_MODEL == BOARD_LORA32_V2_0
 425            disp_mode = DISP_MODE_PORTRAIT;
 426            display.setRotation(3);
 427          #elif BOARD_MODEL == BOARD_LORA32_V2_1
 428            disp_mode = DISP_MODE_LANDSCAPE;
 429            display.setRotation(0);
 430          #elif BOARD_MODEL == BOARD_TBEAM
 431            disp_mode = DISP_MODE_LANDSCAPE;
 432            display.setRotation(0);
 433          #elif BOARD_MODEL == BOARD_TBEAM_S_V1
 434            disp_mode = DISP_MODE_PORTRAIT;
 435            display.setRotation(1);
 436          #elif BOARD_MODEL == BOARD_STATION_G2
 437            disp_mode = DISP_MODE_LANDSCAPE;
 438            display.setRotation(0);
 439          #elif BOARD_MODEL == BOARD_HELTEC32_V2
 440            disp_mode = DISP_MODE_PORTRAIT;
 441            display.setRotation(1);
 442          #elif BOARD_MODEL == BOARD_HELTEC32_V3
 443            disp_mode = DISP_MODE_PORTRAIT;
 444            display.setRotation(1);
 445          #elif BOARD_MODEL == BOARD_HELTEC32_V4
 446            disp_mode = DISP_MODE_PORTRAIT;
 447            display.setRotation(1);
 448          #elif BOARD_MODEL == BOARD_HELTEC_T114
 449            disp_mode = DISP_MODE_PORTRAIT;
 450            display.setRotation(1);
 451          #elif BOARD_MODEL == BOARD_RAK4631
 452            disp_mode = DISP_MODE_LANDSCAPE;
 453            display.setRotation(0);
 454          #elif BOARD_MODEL == BOARD_TDECK
 455            disp_mode = DISP_MODE_PORTRAIT;
 456            display.setRotation(3);
 457          #elif BOARD_MODEL == BOARD_TECHO
 458            disp_mode = DISP_MODE_PORTRAIT;
 459            display.setRotation(3);
 460          #else
 461            disp_mode = DISP_MODE_PORTRAIT;
 462            display.setRotation(3);
 463          #endif
 464        }
 465  
 466        update_area_positions();
 467  
 468        for (int i = 0; i < WATERFALL_SIZE; i++) { waterfall[i] = 0; }
 469  
 470        last_page_flip = millis();
 471  
 472        stat_area.cp437(true);
 473        disp_area.cp437(true);
 474  
 475        #if BOARD_MODEL != BOARD_HELTEC_T114
 476        display.cp437(true);
 477        #endif
 478  
 479        #if HAS_EEPROM
 480          display_intensity = EEPROM.read(eeprom_addr(ADDR_CONF_DINT));
 481        #elif MCU_VARIANT == MCU_NRF52
 482          display_intensity = eeprom_read(eeprom_addr(ADDR_CONF_DINT));
 483        #endif
 484        display_unblank_intensity = display_intensity;
 485  
 486        #if BOARD_MODEL == BOARD_TECHO
 487          #if HAS_BACKLIGHT
 488            if (display_intensity == 0) { analogWrite(pin_backlight, 0); }
 489            else                        { analogWrite(pin_backlight, display_intensity); }
 490          #endif
 491        #endif
 492  
 493        #if BOARD_MODEL == BOARD_TDECK
 494          display.fillScreen(SSD1306_BLACK);
 495        #endif
 496  
 497        #if BOARD_MODEL == BOARD_HELTEC_T114
 498          // Enable backlight led (display is always black without this)
 499          fillRect(p_ad_x, p_ad_y, 128, 128, SSD1306_BLACK);
 500          fillRect(p_as_x, p_as_y, 128, 128, SSD1306_BLACK);
 501          pinMode(PIN_T114_TFT_BLGT, OUTPUT);
 502          digitalWrite(PIN_T114_TFT_BLGT, LOW);
 503        #endif
 504  
 505        return true;
 506      }
 507    #else
 508      return false;
 509    #endif
 510  }
 511  
 512  // Draws a line on the screen
 513  void drawLine(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour) {
 514    #if BOARD_MODEL == BOARD_HELTEC_T114
 515    if(colour == SSD1306_WHITE){
 516      display.setColor(WHITE);
 517    } else if(colour == SSD1306_BLACK) {
 518      display.setColor(BLACK);
 519    }
 520    display.drawLine(x, y, width, height);
 521    #else
 522    display.drawLine(x, y, width, height, colour);
 523    #endif
 524  }
 525  
 526  // Draws a filled rectangle on the screen
 527  void fillRect(int16_t x, int16_t y, int16_t width, int16_t height, uint16_t colour) {
 528    #if BOARD_MODEL == BOARD_HELTEC_T114
 529    if(colour == SSD1306_WHITE){
 530      display.setColor(WHITE);
 531    } else if(colour == SSD1306_BLACK) {
 532      display.setColor(BLACK);
 533    }
 534    display.fillRect(x, y, width, height);
 535    #else
 536    display.fillRect(x, y, width, height, colour);
 537    #endif
 538  }
 539  
 540  // Draws a bitmap to the display and auto scales it based on the boards configured DISPLAY_SCALE
 541  void drawBitmap(int16_t startX, int16_t startY, const uint8_t* bitmap, int16_t bitmapWidth, int16_t bitmapHeight, uint16_t foregroundColour, uint16_t backgroundColour) {
 542    #if DISPLAY_SCALE == 1
 543      display.drawBitmap(startX, startY, bitmap, bitmapWidth, bitmapHeight, foregroundColour, backgroundColour);
 544    #else
 545      for(int16_t row = 0; row < bitmapHeight; row++){
 546          for(int16_t col = 0; col < bitmapWidth; col++){
 547  
 548              // determine index and bitmask
 549              int16_t index = row * ((bitmapWidth + 7) / 8) + (col / 8);
 550              uint8_t bitmask = 1 << (7 - (col % 8));
 551  
 552              // check if the current pixel is set in the bitmap
 553              if(bitmap[index] & bitmask){
 554                  // draw a scaled rectangle for the foreground pixel
 555                  fillRect(startX + col * DISPLAY_SCALE, startY + row * DISPLAY_SCALE, DISPLAY_SCALE, DISPLAY_SCALE, foregroundColour);
 556              } else {
 557                  // draw a scaled rectangle for the background pixel
 558                  fillRect(startX + col * DISPLAY_SCALE, startY + row * DISPLAY_SCALE, DISPLAY_SCALE, DISPLAY_SCALE, backgroundColour);
 559              }
 560  
 561          }
 562      }
 563    #endif
 564  }
 565  
 566  extern uint8_t wifi_mode;
 567  extern bool wifi_is_connected();
 568  extern bool wifi_host_is_connected();
 569  void draw_cable_icon(int px, int py) {
 570    #if HAS_WIFI
 571      if (wifi_mode == WR_WIFI_OFF) {
 572        if      (cable_state == CABLE_STATE_DISCONNECTED) { stat_area.drawBitmap(px, py, bm_cable+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 573        else if (cable_state == CABLE_STATE_CONNECTED)    { stat_area.drawBitmap(px, py, bm_cable+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 574      } else {
 575        if (wifi_mode == WR_WIFI_STA) {
 576          if (wifi_is_connected()) {
 577            stat_area.drawBitmap(px, py, bm_wifi+3*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 578            if (!wifi_host_is_connected()) { stat_area.fillRect(px+5, py+12, 6, 3, SSD1306_BLACK); }
 579          } else { stat_area.drawBitmap(px, py, bm_wifi+2*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 580        
 581        } else if (wifi_mode == WR_WIFI_AP) {
 582          if (wifi_host_is_connected()) { stat_area.drawBitmap(px, py, bm_wifi+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 583          else                          { stat_area.drawBitmap(px, py, bm_wifi+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 584        
 585        } else {
 586          if      (cable_state == CABLE_STATE_DISCONNECTED) { stat_area.drawBitmap(px, py, bm_cable+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 587          else if (cable_state == CABLE_STATE_CONNECTED)    { stat_area.drawBitmap(px, py, bm_cable+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 588        }
 589      }
 590  
 591    #else
 592    if      (cable_state == CABLE_STATE_DISCONNECTED) { stat_area.drawBitmap(px, py, bm_cable+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 593    else if (cable_state == CABLE_STATE_CONNECTED)    { stat_area.drawBitmap(px, py, bm_cable+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK); }
 594    #endif
 595  }
 596  
 597  void draw_bt_icon(int px, int py) {
 598    if (bt_state == BT_STATE_OFF) {
 599      stat_area.drawBitmap(px, py, bm_bt+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 600    } else if (bt_state == BT_STATE_ON) {
 601      stat_area.drawBitmap(px, py, bm_bt+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 602    } else if (bt_state == BT_STATE_PAIRING) {
 603      stat_area.drawBitmap(px, py, bm_bt+2*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 604    } else if (bt_state == BT_STATE_CONNECTED) {
 605      stat_area.drawBitmap(px, py, bm_bt+3*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 606    } else {
 607      stat_area.drawBitmap(px, py, bm_bt+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 608    }
 609  }
 610  
 611  void draw_lora_icon(int px, int py) {
 612    if (radio_online) {
 613      stat_area.drawBitmap(px, py, bm_rf+1*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 614    } else {
 615      stat_area.drawBitmap(px, py, bm_rf+0*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 616    }
 617  }
 618  
 619  void draw_mw_icon(int px, int py) {
 620    if (mw_radio_online) {
 621      stat_area.drawBitmap(px, py, bm_rf+3*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 622    } else {
 623      stat_area.drawBitmap(px, py, bm_rf+2*32, 16, 16, SSD1306_WHITE, SSD1306_BLACK);
 624    }
 625  }
 626  
 627  uint8_t charge_tick = 0;
 628  void draw_battery_bars(int px, int py) {
 629    if (pmu_ready) {
 630      if (battery_ready) {
 631        if (battery_installed) {
 632          float battery_value = battery_percent;
 633  
 634          // Disable charging state display for now, since
 635          // boards without dedicated PMU are completely
 636          // unreliable for determining actual charging state.
 637          bool disable_charge_status = false;
 638          if (battery_indeterminate && battery_state == BATTERY_STATE_CHARGING) {
 639            disable_charge_status = true;
 640          }
 641          
 642          if (battery_state == BATTERY_STATE_CHARGING && !disable_charge_status) {
 643            float battery_prog = battery_percent;
 644            if (battery_prog > 85) { battery_prog = 84; }
 645            if (charge_tick < battery_prog ) { charge_tick = battery_prog; }
 646            battery_value = charge_tick;
 647            charge_tick += 3;
 648            if (charge_tick > 100) charge_tick = 0;
 649          }
 650  
 651          if (battery_indeterminate && battery_state == BATTERY_STATE_CHARGING && !disable_charge_status) {
 652            stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
 653            stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK);
 654          } else {
 655            if (battery_state == BATTERY_STATE_CHARGED) {
 656              stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
 657              stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK);
 658            } else {
 659              // stat_area.fillRect(px, py, 14, 3, SSD1306_BLACK);
 660              stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
 661              stat_area.drawRect(px-2, py-2, 17, 7, SSD1306_WHITE);
 662              stat_area.drawLine(px+15, py, px+15, py+3, SSD1306_WHITE);
 663              if (battery_value > 7) stat_area.drawLine(px, py, px, py+2, SSD1306_WHITE);
 664              if (battery_value > 20) stat_area.drawLine(px+1*2, py, px+1*2, py+2, SSD1306_WHITE);
 665              if (battery_value > 33) stat_area.drawLine(px+2*2, py, px+2*2, py+2, SSD1306_WHITE);
 666              if (battery_value > 46) stat_area.drawLine(px+3*2, py, px+3*2, py+2, SSD1306_WHITE);
 667              if (battery_value > 59) stat_area.drawLine(px+4*2, py, px+4*2, py+2, SSD1306_WHITE);
 668              if (battery_value > 72) stat_area.drawLine(px+5*2, py, px+5*2, py+2, SSD1306_WHITE);
 669              if (battery_value > 85) stat_area.drawLine(px+6*2, py, px+6*2, py+2, SSD1306_WHITE);
 670            }
 671          }
 672        } else {
 673          stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
 674          stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK);
 675        }
 676      }
 677    } else {
 678      stat_area.fillRect(px-2, py-2, 18, 7, SSD1306_BLACK);
 679      stat_area.drawBitmap(px-2, py-2, bm_plug, 17, 7, SSD1306_WHITE, SSD1306_BLACK);
 680    }
 681  }
 682  
 683  #define Q_SNR_STEP 2.0
 684  #define Q_SNR_MIN_BASE -9.0
 685  #define Q_SNR_MAX 6.0
 686  void draw_quality_bars(int px, int py) {
 687    stat_area.fillRect(px, py, 13, 7, SSD1306_BLACK);
 688    if (radio_online) {
 689      signed char t_snr = (signed int)last_snr_raw;
 690      int snr_int = (int)t_snr;
 691      float snr_min = Q_SNR_MIN_BASE-(int)lora_sf*Q_SNR_STEP;
 692      float snr_span = (Q_SNR_MAX-snr_min);
 693      float snr = ((int)snr_int) * 0.25;
 694      float quality = ((snr-snr_min)/(snr_span))*100;
 695      if (quality > 100.0) quality = 100.0;
 696      if (quality < 0.0) quality = 0.0;
 697  
 698      // Serial.printf("Last SNR: %.2f\n, quality: %.2f\n", snr, quality);
 699      if (quality > 0)  stat_area.drawLine(px+0*2, py+7, px+0*2, py+6, SSD1306_WHITE);
 700      if (quality > 15) stat_area.drawLine(px+1*2, py+7, px+1*2, py+5, SSD1306_WHITE);
 701      if (quality > 30) stat_area.drawLine(px+2*2, py+7, px+2*2, py+4, SSD1306_WHITE);
 702      if (quality > 45) stat_area.drawLine(px+3*2, py+7, px+3*2, py+3, SSD1306_WHITE);
 703      if (quality > 60) stat_area.drawLine(px+4*2, py+7, px+4*2, py+2, SSD1306_WHITE);
 704      if (quality > 75) stat_area.drawLine(px+5*2, py+7, px+5*2, py+1, SSD1306_WHITE);
 705      if (quality > 90) stat_area.drawLine(px+6*2, py+7, px+6*2, py+0, SSD1306_WHITE);
 706    }
 707  }
 708  
 709  #if MODEM == SX1280
 710    #define S_RSSI_MIN -105.0
 711    #define S_RSSI_MAX -65.0
 712  #else
 713    #define S_RSSI_MIN -135.0
 714    #define S_RSSI_MAX -75.0
 715  #endif
 716  #define S_RSSI_SPAN (S_RSSI_MAX-S_RSSI_MIN)
 717  void draw_signal_bars(int px, int py) {
 718    stat_area.fillRect(px, py, 13, 7, SSD1306_BLACK);
 719  
 720    if (radio_online) {
 721      int rssi_val = last_rssi;
 722      if (rssi_val < S_RSSI_MIN) rssi_val = S_RSSI_MIN;
 723      if (rssi_val > S_RSSI_MAX) rssi_val = S_RSSI_MAX;
 724      int signal = ((rssi_val - S_RSSI_MIN)*(1.0/S_RSSI_SPAN))*100.0;
 725  
 726      if (signal > 100.0) signal = 100.0;
 727      if (signal < 0.0) signal = 0.0;
 728  
 729      // Serial.printf("Last SNR: %.2f\n, quality: %.2f\n", snr, quality);
 730      if (signal > 85) stat_area.drawLine(px+0*2, py+7, px+0*2, py+0, SSD1306_WHITE);
 731      if (signal > 72) stat_area.drawLine(px+1*2, py+7, px+1*2, py+1, SSD1306_WHITE);
 732      if (signal > 59) stat_area.drawLine(px+2*2, py+7, px+2*2, py+2, SSD1306_WHITE);
 733      if (signal > 46) stat_area.drawLine(px+3*2, py+7, px+3*2, py+3, SSD1306_WHITE);
 734      if (signal > 33) stat_area.drawLine(px+4*2, py+7, px+4*2, py+4, SSD1306_WHITE);
 735      if (signal > 20) stat_area.drawLine(px+5*2, py+7, px+5*2, py+5, SSD1306_WHITE);
 736      if (signal > 7)  stat_area.drawLine(px+6*2, py+7, px+6*2, py+6, SSD1306_WHITE);
 737    }
 738  }
 739  
 740  #if MODEM == SX1280
 741    #define WF_TX_SIZE 5
 742  #else
 743    #define WF_TX_SIZE 5
 744  #endif
 745  #define WF_RSSI_MAX -60
 746  #define WF_RSSI_MIN -135
 747  #define WF_RSSI_SPAN (WF_RSSI_MAX-WF_RSSI_MIN)
 748  #define WF_PIXEL_WIDTH 10
 749  void draw_waterfall(int px, int py) {
 750    int rssi_val = current_rssi;
 751    if (rssi_val < WF_RSSI_MIN) rssi_val = WF_RSSI_MIN;
 752    if (rssi_val > WF_RSSI_MAX) rssi_val = WF_RSSI_MAX;
 753    int rssi_normalised = ((rssi_val - WF_RSSI_MIN)*(1.0/WF_RSSI_SPAN))*WF_PIXEL_WIDTH;
 754    if (display_tx) {
 755      for (uint8_t i = 0; i < WF_TX_SIZE; i++) {
 756        waterfall[waterfall_head++] = -1;
 757        if (waterfall_head >= WATERFALL_SIZE) waterfall_head = 0;
 758      }
 759      display_tx = false;
 760    } else {
 761      waterfall[waterfall_head++] = rssi_normalised;
 762      if (waterfall_head >= WATERFALL_SIZE) waterfall_head = 0;
 763    }
 764  
 765    stat_area.fillRect(px,py,WF_PIXEL_WIDTH, WATERFALL_SIZE, SSD1306_BLACK);
 766    for (int i = 0; i < WATERFALL_SIZE; i++){
 767      int wi = (waterfall_head+i)%WATERFALL_SIZE;
 768      int ws = waterfall[wi];
 769      if (ws > 0) {
 770        stat_area.drawLine(px, py+i, px+ws-1, py+i, SSD1306_WHITE);
 771      } else if (ws == -1) {
 772        uint8_t o = i%2;
 773        for (uint8_t ti = 0; ti < WF_PIXEL_WIDTH/2; ti++) {
 774          stat_area.drawPixel(px+ti*2+o, py+i, SSD1306_WHITE);
 775        }
 776      }
 777    }
 778  }
 779  
 780  bool stat_area_intialised = false;
 781  void draw_stat_area() {
 782    if (device_init_done) {
 783      if (!stat_area_intialised) {
 784        stat_area.drawBitmap(0, 0, bm_frame, 64, 64, SSD1306_WHITE, SSD1306_BLACK);
 785        stat_area_intialised = true;
 786      }
 787  
 788      draw_cable_icon(3, 8);
 789      draw_bt_icon(3, 30);
 790      draw_lora_icon(45, 8);
 791      draw_mw_icon(45, 30);
 792      draw_battery_bars(4, 58);
 793      draw_quality_bars(28, 56);
 794      draw_signal_bars(44, 56);
 795      if (radio_online) {
 796        draw_waterfall(27, 4);
 797      }
 798    }
 799  }
 800  
 801  void update_stat_area() {
 802    if (eeprom_ok && !firmware_update_mode && !console_active) {
 803  
 804      draw_stat_area();
 805      if (disp_mode == DISP_MODE_PORTRAIT) {
 806        drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
 807      } else if (disp_mode == DISP_MODE_LANDSCAPE) {
 808        drawBitmap(p_as_x+2, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
 809        if (device_init_done && !disp_ext_fb) drawLine(p_as_x, 0, p_as_x, 64, SSD1306_WHITE);
 810      }
 811  
 812    } else {
 813      if (firmware_update_mode) {
 814        drawBitmap(p_as_x, p_as_y, bm_updating, stat_area.width(), stat_area.height(), SSD1306_BLACK, SSD1306_WHITE);
 815      } else if (console_active && device_init_done) {
 816        drawBitmap(p_as_x, p_as_y, bm_console, stat_area.width(), stat_area.height(), SSD1306_BLACK, SSD1306_WHITE);
 817        if (disp_mode == DISP_MODE_LANDSCAPE) {
 818          drawLine(p_as_x, 0, p_as_x, 64, SSD1306_WHITE);
 819        }
 820      }
 821    }
 822  }
 823  
 824  #define START_PAGE 0
 825  const uint8_t pages = 3;
 826  uint8_t disp_page = START_PAGE;
 827  extern char bt_devname[11];
 828  extern char bt_dh[16];
 829  #if HAS_WIFI
 830    extern IPAddress wr_device_ip;
 831  #endif
 832  void draw_disp_area() {
 833    if (!device_init_done || firmware_update_mode) {
 834      uint8_t p_by = 37;
 835      if (disp_mode == DISP_MODE_LANDSCAPE || firmware_update_mode) {
 836        p_by = 18;
 837        disp_area.fillRect(0, 0, disp_area.width(), disp_area.height(), SSD1306_BLACK);
 838      }
 839      if (!device_init_done) disp_area.drawBitmap(0, p_by, bm_boot, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 840      if (firmware_update_mode) disp_area.drawBitmap(0, p_by, bm_fw_update, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 841    } else {
 842      if (!disp_ext_fb or bt_ssp_pin != 0) {
 843        if (radio_online && display_diagnostics) {
 844          disp_area.fillRect(0,8,disp_area.width(),37, SSD1306_BLACK); disp_area.fillRect(0,37,disp_area.width(),27, SSD1306_WHITE);
 845          disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(1);
 846  
 847          disp_area.setCursor(2, 13);
 848          disp_area.print("On");
 849          disp_area.setCursor(14, 13);
 850          disp_area.print("@");
 851          disp_area.setCursor(21, 13);
 852          disp_area.printf("%.1fKbps", (float)lora_bitrate/1000.0);
 853  
 854          //disp_area.setCursor(31, 23-1);
 855          disp_area.setCursor(2, 23-1);
 856          disp_area.print("Airtime:");
 857          
 858          disp_area.setCursor(11, 33-1);
 859          if (total_channel_util < 0.099) {
 860            //disp_area.printf("%.1f%%", total_channel_util*100.0);
 861            disp_area.printf("%.1f%%", airtime*100.0);
 862          } else {
 863            //disp_area.printf("%.0f%%", total_channel_util*100.0);
 864            disp_area.printf("%.0f%%", airtime*100.0);
 865          }
 866          disp_area.drawBitmap(2, 26-1, bm_hg_low, 5, 9, SSD1306_WHITE, SSD1306_BLACK);
 867  
 868          disp_area.setCursor(32+11, 33-1);
 869          if (longterm_channel_util < 0.099) {
 870            //disp_area.printf("%.1f%%", longterm_channel_util*100.0);
 871            disp_area.printf("%.1f%%", longterm_airtime*100.0);
 872          } else {
 873            //disp_area.printf("%.0f%%", longterm_channel_util*100.0);
 874            disp_area.printf("%.0f%%", longterm_airtime*100.0);
 875          }
 876          disp_area.drawBitmap(32+2, 26-1, bm_hg_high, 5, 9, SSD1306_WHITE, SSD1306_BLACK);
 877  
 878  
 879          disp_area.setTextColor(SSD1306_BLACK);
 880          disp_area.setCursor(2, 46);
 881          disp_area.print("Channel");
 882          disp_area.setCursor(38, 46);
 883          disp_area.print("Load:");
 884          
 885          disp_area.setCursor(11, 57);
 886          if (total_channel_util < 0.099) {
 887            //disp_area.printf("%.1f%%", airtime*100.0);
 888            disp_area.printf("%.1f%%", total_channel_util*100.0);
 889          } else {
 890            //disp_area.printf("%.0f%%", airtime*100.0);
 891            disp_area.printf("%.0f%%", total_channel_util*100.0);
 892          }
 893          disp_area.drawBitmap(2, 50, bm_hg_low, 5, 9, SSD1306_BLACK, SSD1306_WHITE);
 894  
 895          disp_area.setCursor(32+11, 57);
 896          if (longterm_channel_util < 0.099) {
 897            //disp_area.printf("%.1f%%", longterm_airtime*100.0);
 898            disp_area.printf("%.1f%%", longterm_channel_util*100.0);
 899          } else {
 900            //disp_area.printf("%.0f%%", longterm_airtime*100.0);
 901            disp_area.printf("%.0f%%", longterm_channel_util*100.0);
 902          }
 903          disp_area.drawBitmap(32+2, 50, bm_hg_high, 5, 9, SSD1306_BLACK, SSD1306_WHITE);
 904  
 905        } else {
 906          if (device_signatures_ok()) { disp_area.drawBitmap(0, 0, bm_def_lc, disp_area.width(), 23, SSD1306_WHITE, SSD1306_BLACK); }
 907          else {                        disp_area.drawBitmap(0, 0, bm_def,    disp_area.width(), 23, SSD1306_WHITE, SSD1306_BLACK); }
 908  
 909          bool display_ip = false;
 910          #if HAS_WIFI
 911            if (wifi_is_connected() && disp_page%2 == 1) { display_ip = true; }
 912          #endif
 913          if (display_ip) {
 914            #if HAS_WIFI
 915              uint8_t ones = 3+one_counts[wr_device_ip[0]]+one_counts[wr_device_ip[1]]+one_counts[wr_device_ip[2]]+one_counts[wr_device_ip[3]];
 916              uint8_t chars = 7;
 917              for (uint8_t i = 0; i<4; i++) { if (wr_device_ip[i] > 9) { chars++; } if (wr_device_ip[i] > 99) { chars++; } }
 918              uint8_t width = chars*6-(ones*4);
 919              int alignment_offset = disp_area.width()-width;
 920              int ipxpos = alignment_offset;
 921              disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(1);
 922              disp_area.fillRect(0, 20, disp_area.width(), 17, SSD1306_BLACK);
 923              disp_area.setCursor(3, 34-8); disp_area.print("WiFi IP:");
 924              disp_area.setCursor(ipxpos, 34); disp_area.print(wr_device_ip);
 925            #endif
 926          } else {
 927            disp_area.setFont(SMALL_FONT); disp_area.setTextWrap(false); disp_area.setTextColor(SSD1306_WHITE); disp_area.setTextSize(2);
 928            disp_area.fillRect(0, 20, disp_area.width(), 17, SSD1306_BLACK); uint8_t ofsc = 0;
 929            if ((bt_dh[14] & 0b00001111) == 0x01) { ofsc += 8; }
 930            if ((bt_dh[14] >> 4)         == 0x01) { ofsc += 8; }
 931            if ((bt_dh[15] & 0b00001111) == 0x01) { ofsc += 8; }
 932            if ((bt_dh[15] >> 4)         == 0x01) { ofsc += 8; }
 933            disp_area.setCursor(17+ofsc, 32); disp_area.printf("%02X%02X", bt_dh[14], bt_dh[15]);
 934          }
 935        }
 936  
 937        if (!hw_ready || radio_error || !device_firmware_ok()) {
 938          if (!device_firmware_ok()) {
 939            disp_area.drawBitmap(0, 37, bm_fw_corrupt, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 940          } else {
 941            if (!modem_installed) {
 942              disp_area.drawBitmap(0, 37, bm_no_radio, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 943            } else {
 944              disp_area.drawBitmap(0, 37, bm_conf_missing, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 945            }
 946          }
 947        } else if (bt_state == BT_STATE_PAIRING and bt_ssp_pin != 0) {
 948          char *pin_str = (char*)malloc(DISP_PIN_SIZE+1);
 949          sprintf(pin_str, "%06d", bt_ssp_pin);
 950  
 951          disp_area.drawBitmap(0, 37, bm_pairing, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 952          for (int i = 0; i < DISP_PIN_SIZE; i++) {
 953            uint8_t numeric = pin_str[i]-48;
 954            uint8_t offset = numeric*5;
 955            disp_area.drawBitmap(7+9*i, 37+16, bm_n_uh+offset, 8, 5, SSD1306_WHITE, SSD1306_BLACK);
 956          }
 957          free(pin_str);
 958        } else {
 959          if (millis()-last_page_flip >= page_interval) {
 960            disp_page = (++disp_page%pages);
 961            last_page_flip = millis();
 962            if (not community_fw and disp_page == 0) disp_page = 1;
 963          }
 964  
 965          if (radio_online) {
 966            if (!display_diagnostics) {
 967              disp_area.drawBitmap(0, 37, bm_online, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 968            }
 969          } else {
 970            if (disp_page == 0) {
 971              if (true || device_signatures_ok()) {
 972                disp_area.drawBitmap(0, 37, bm_checks, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 973              } else {
 974                disp_area.drawBitmap(0, 37, bm_nfr, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 975              }
 976            } else if (disp_page == 1) {
 977              if (!console_active) {
 978                disp_area.drawBitmap(0, 37, bm_hwok, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 979              } else {
 980                disp_area.drawBitmap(0, 37, bm_console_active, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 981              }
 982            } else if (disp_page == 2) {
 983              disp_area.drawBitmap(0, 37, bm_version, disp_area.width(), 27, SSD1306_WHITE, SSD1306_BLACK);
 984              char *v_str = (char*)malloc(3+1);
 985              sprintf(v_str, "%01d%02d", MAJ_VERS, MIN_VERS);
 986              for (int i = 0; i < 3; i++) {
 987                uint8_t numeric = v_str[i]-48; uint8_t bm_offset = numeric*5;
 988                uint8_t dxp = 20;
 989                if (i == 1) dxp += 9*1+4;
 990                if (i == 2) dxp += 9*2+4;
 991                disp_area.drawBitmap(dxp, 37+16, bm_n_uh+bm_offset, 8, 5, SSD1306_WHITE, SSD1306_BLACK);
 992              }
 993              free(v_str);
 994              disp_area.drawLine(27, 37+19, 28, 37+19, SSD1306_BLACK);
 995              disp_area.drawLine(27, 37+20, 28, 37+20, SSD1306_BLACK);
 996            }
 997          }
 998        }
 999      } else {
1000        disp_area.drawBitmap(0, 0, fb, disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK);
1001      }
1002    }
1003  }
1004  
1005  void update_disp_area() {
1006    draw_disp_area();
1007  
1008    drawBitmap(p_ad_x, p_ad_y, disp_area.getBuffer(), disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK);
1009    if (disp_mode == DISP_MODE_LANDSCAPE) {
1010      if (device_init_done && !firmware_update_mode && !disp_ext_fb) {
1011        drawLine(0, 0, 0, 63, SSD1306_WHITE);
1012      }
1013    }
1014  }
1015  
1016  void display_recondition() {
1017    #if PLATFORM == PLATFORM_ESP32
1018      for (uint8_t iy = 0; iy < disp_area.height(); iy++) {
1019        unsigned char rand_seg [] = {random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF),random(0xFF)};
1020        stat_area.drawBitmap(0, iy, rand_seg, 64, 1, SSD1306_WHITE, SSD1306_BLACK);
1021        disp_area.drawBitmap(0, iy, rand_seg, 64, 1, SSD1306_WHITE, SSD1306_BLACK);
1022      }
1023  
1024      drawBitmap(p_ad_x, p_ad_y, disp_area.getBuffer(), disp_area.width(), disp_area.height(), SSD1306_WHITE, SSD1306_BLACK);
1025      if (disp_mode == DISP_MODE_PORTRAIT) {
1026        drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
1027      } else if (disp_mode == DISP_MODE_LANDSCAPE) {
1028        drawBitmap(p_as_x, p_as_y, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK);
1029      }
1030    #endif
1031  }
1032  
1033  bool epd_blanked = false;
1034  #if BOARD_MODEL == BOARD_TECHO
1035    void epd_blank(bool full_update = true) {
1036      display.setFullWindow();
1037      display.fillScreen(SSD1306_WHITE);
1038      display.display(full_update);
1039    }
1040  
1041    void epd_black(bool full_update = true) {
1042      display.setFullWindow();
1043      display.fillScreen(SSD1306_BLACK);
1044      display.display(full_update);
1045    }
1046  #endif
1047  
1048  void update_display(bool blank = false) {
1049    display_updating = true;
1050    if (blank == true) {
1051      last_disp_update = millis()-disp_update_interval-1;
1052    } else {
1053      if (display_blanking_enabled && millis()-last_unblank_event >= display_blanking_timeout) {
1054        blank = true;
1055        if (!display_blanked) {
1056          last_disp_update = 0;
1057        }
1058        display_blanked = true;
1059        if (display_intensity != 0) {
1060          display_unblank_intensity = display_intensity;
1061        }
1062        display_intensity = 0;
1063      } else {
1064        display_blanked = false;
1065        if (display_unblank_intensity != 0x00) {
1066          display_intensity = display_unblank_intensity;
1067          display_unblank_intensity = 0x00;
1068        }
1069      }
1070    }
1071  
1072    if (blank) {
1073      int blank_iv = display_blanked ? disp_blank_update_interval : disp_update_interval;
1074      if (millis()-last_disp_update >= (uint32_t)blank_iv) {
1075        if (display_contrast != display_intensity) {
1076          display_contrast = display_intensity;
1077          set_contrast(&display, display_contrast);
1078        }
1079  
1080        #if BOARD_MODEL == BOARD_TECHO
1081          if (!epd_blanked) {
1082            epd_blank();
1083            epd_blanked = true;
1084          }
1085        #endif
1086  
1087        #if BOARD_MODEL == BOARD_HELTEC_T114
1088          display.clear();
1089          display.display();
1090        #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO
1091          display.clearDisplay();
1092          display.display();
1093        #else
1094          // TODO: Clear screen
1095        #endif
1096  
1097        last_disp_update = millis();
1098      }
1099  
1100    } else {
1101      if (millis()-last_disp_update >= disp_update_interval) {
1102        uint32_t current = millis();
1103        if (display_contrast != display_intensity) {
1104          display_contrast = display_intensity;
1105          set_contrast(&display, display_contrast);
1106        }
1107  
1108        #if BOARD_MODEL == BOARD_HELTEC_T114
1109          display.clear();
1110        #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO
1111          display.clearDisplay();
1112        #endif
1113  
1114        if (recondition_display) {
1115          disp_target_fps = 30;
1116          disp_update_interval = 1000/disp_target_fps;
1117          display_recondition();
1118        } else {
1119          #if BOARD_MODEL == BOARD_TECHO
1120            display.setFullWindow();
1121            display.fillScreen(SSD1306_WHITE);
1122          #endif
1123  
1124          update_stat_area();
1125          update_disp_area();
1126        }
1127        
1128        #if BOARD_MODEL == BOARD_TECHO
1129          if (current-last_epd_refresh >= epd_update_interval) {
1130            if (current-last_epd_full_refresh >= REFRESH_PERIOD) { display.display(false); last_epd_full_refresh = millis(); }
1131            else { display.display(true); }
1132            last_epd_refresh = millis();
1133            epd_blanked = false;
1134          }
1135        #elif BOARD_MODEL != BOARD_TDECK
1136          display.display();
1137        #endif
1138  
1139        last_disp_update = millis();
1140      }
1141    }
1142    display_updating = false;
1143  }
1144  
1145  void display_unblank() {
1146    last_unblank_event = millis();
1147  }
1148  
1149  void ext_fb_enable() {
1150    disp_ext_fb = true;
1151  }
1152  
1153  void ext_fb_disable() {
1154    disp_ext_fb = false;
1155  }