#ifdef HAS_DISPLAY /* Display-Mask (128 x 64 pixel): | | | | 11111111112 |012345678901234567890 Font ----------------------- --------- 0|PAX:aabbccdd LARGE 1|PAX:aabbccdd LARGE 2| 3|WIFI:abcde BLTH:abcde SMALL 4|Batt:abc% chan:ab SMALL 5|RLIM:abcd Mem:abcdeKB SMALL 6|27.Feb 2019 20:27:00* SMALL 7|yyyyyyyyyyyyy xx SFab SMALL * = char {L|G|R|?} indicates time source, inverse = clock controller is active, pulsed = pps input signal is active y = LMIC event message xx = payload sendqueue length ab = LMIC spread factor MY_FONT_SMALL: 6x8px = 21 chars / line @ 8 lines MY_FONT_NORMAL: 8x8px = 16 chars / line @ 8 lines MY_FONT_STRETCHED: 12x16px = 10 chars / line @ 4 lines MY_FONT_LARGE: 16x32px = 8 chars / line @ 2 lines */ // Basic Config #include // needed for reading ESP32 chip attributes #include "globals.h" #include "display.h" static uint8_t plotbuf[PLOTBUFFERSIZE] = {0}; uint8_t DisplayIsOn = 0; hw_timer_t *displayIRQ = NULL; static QRCode qrcode; // select display driver #ifdef HAS_DISPLAY #if (HAS_DISPLAY) == 1 ONE_BIT_DISPLAY *dp = NULL; #elif (HAS_DISPLAY) > 1 BB_SPI_LCD *dp = NULL; #else #error Unknown display type specified in hal file #endif #endif void dp_setup(int contrast) { #if (HAS_DISPLAY) == 1 // I2C OLED dp = new ONE_BIT_DISPLAY; dp->setI2CPins(MY_DISPLAY_SDA, MY_DISPLAY_SCL, MY_DISPLAY_RST); dp->setBitBang(false); dp->I2Cbegin(OLED_TYPE, OLED_ADDR, OLED_FREQUENCY); dp->allocBuffer(); // render all outputs to lib internal backbuffer dp->setTextWrap(false); dp->setRotation( MY_DISPLAY_FLIP ? 2 : 0); // 0 = no rotation, 1 = 90°, 2 = 180°, 3 = 270° #elif (HAS_DISPLAY) > 1 // TFT LCD dp = new BB_SPI_LCD; dp->begin(TFT_TYPE); dp->allocBuffer(); // render all outputs to lib internal backbuffer dp->setRotation( MY_DISPLAY_FLIP ? 1 : 3); // 0 = no rotation, 1 = 90°, 2 = 180°, 3 = 270° dp->setTextColor(MY_DISPLAY_FGCOLOR, MY_DISPLAY_BGCOLOR); #endif dp_clear(); if (contrast) dp_contrast(contrast); } void dp_init(bool verbose) { dp_setup(DISPLAYCONTRAST); // show chip information if (verbose) { #if (VERBOSE) esp_chip_info_t chip_info; esp_chip_info(&chip_info); dp_setFont(MY_FONT_NORMAL); dp->printf("** PAXCOUNTER **\r\n"); dp->printf("Software v%s\r\n", PROGVERSION); dp->printf("ESP32 %d cores\r\n", chip_info.cores); dp->printf("Chip Rev.%d\r\n", chip_info.revision); dp->printf("WiFi%s%s\r\n", (chip_info.features & CHIP_FEATURE_BT) ? "/BT" : "", (chip_info.features & CHIP_FEATURE_BLE) ? "/BLE" : ""); dp->printf("%dMB %s Flash", spi_flash_get_chip_size() / (1024 * 1024), (chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "int." : "ext."); dp_dump(); delay(2000); dp_clear(); #endif // VERBOSE #if (HAS_LORA) // generate DEVEUI as QR code and text uint8_t buf[8], *p = buf; char deveui[17]; os_getDevEui((u1_t *)buf); snprintf(deveui, 17, "%016llX", (*(uint64_t *)(p))); // display DEVEUI as QR code on the left dp_contrast(30); dp_printqr(3, 3, deveui); // display DEVEUI as plain text on the right const int x_offset = QR_SCALEFACTOR * 29 + 14; dp_setFont(MY_FONT_NORMAL); dp->setCursor(x_offset, 0); dp->printf("DEVEUI:\r\n"); for (uint8_t i = 0; i <= 3; i++) { dp->setCursor(x_offset, i * 8 + 20); dp->printf("%4.4s", deveui + i * 4); } // give user some time to read or take picture dp_dump(); #if !(BOOTMENU) delay(8000); #endif #endif // HAS_LORA } // verbose dp_power(cfg.screenon); // set display off if disabled } // dp_init // write display content to display buffer // nextpage = true -> flip 1 page void dp_refresh(bool nextPage) { struct count_payload_t count; // libpax count storage static uint8_t DisplayPage = 0; char timeState, strftime_buf[64]; time_t now; struct tm timeinfo = {0}; #ifndef HAS_BUTTON static uint32_t framecounter = 0; const uint32_t flip_threshold = DISPLAYCYCLE * 1000 / DISPLAYREFRESH_MS; #endif // if display is switched off we don't refresh it to relax cpu if (!DisplayIsOn && (DisplayIsOn == cfg.screenon)) return; // set display on/off according to current device configuration if (DisplayIsOn != cfg.screenon) { DisplayIsOn = cfg.screenon; dp_power(cfg.screenon); } #ifndef HAS_BUTTON // auto flip page if we are in unattended mode if (++framecounter > flip_threshold) { framecounter = 0; nextPage = true; } #endif if (nextPage) { DisplayPage = (DisplayPage >= DISPLAY_PAGES - 1) ? 0 : (DisplayPage + 1); dp_clear(); } else dp->setCursor(0, 0); switch (DisplayPage) { // page 0: pax + parameters overview // page 1: pax + lorawan parameters // page 2: pax + GPS lat/lon // page 3: BME280/680 values // page 4: timeofday // page 5: pax graph // page 6: blank screen // ---------- page 0: parameters overview ---------- case 0: // show pax libpax_counter_count(&count); dp_setFont(MY_FONT_LARGE); dp->printf("%-8u", count.pax); dp_setFont(MY_FONT_SMALL); dp->setCursor(0, MY_DISPLAY_FIRSTLINE); // line 3: wifi + bluetooth counters // WIFI:abcde BLTH:abcde if (cfg.wifiscan) dp->printf("WIFI:%-5u", count.wifi_count); else dp->printf("WIFI:off "); if (cfg.blescan) dp->printf("BLTH:%-5u", count.ble_count); else dp->printf(" BLTH:off"); dp->printf("\r\n"); // line 4: Battery + GPS status + Wifi channel // B:a.bcV Sats:ab ch:ab #if (defined BAT_MEASURE_ADC || defined HAS_PMU || defined HAS_IP5306) if (batt_level > 0) dp->printf("Batt:%3u%% ", batt_level); else dp->printf("No batt "); #else dp->printf(" "); #endif dp->printf("chan:%02u\r\n", channel); // line 5: RSSI limiter + free memory // RLIM:abcd Mem:abcdKB dp->printf(!cfg.rssilimit ? "RLIM:off " : "RLIM:%-4d", cfg.rssilimit); dp->printf(" Mem:%uKB\r\n", getFreeRAM() / 1024); // line 6: time + date // Wed Jan 12 21:49:08 * #if (TIME_SYNC_INTERVAL) timeState = TimePulseTick ? ' ' : timeSetSymbols[timeSource]; time(&now); localtime_r(&now, &timeinfo); strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo); dp->printf("%.20s", strftime_buf); // display inverse timeState if clock controller is enabled #if (defined HAS_DCF77) || (defined HAS_IF482) dp_setFont(MY_FONT_SMALL, 1); dp->printf("%c\r\n", timeState); dp_setFont(MY_FONT_SMALL, 0); #else dp->printf("%c\r\n", timeState); #endif #endif // TIME_SYNC_INTERVAL // line 7: LMIC status // yyyyyyyyyyyyy xx SFab #if (HAS_LORA) // LMiC event display dp->printf("%-16s ", lmic_event_msg); // LORA datarate, display inverse if ADR disabled dp_setFont(MY_FONT_SMALL, !cfg.adrmode); dp->printf("%-4s", getSfName(updr2rps(LMIC.datarate))); dp_setFont(MY_FONT_SMALL, 0); #endif // HAS_LORA dp_dump(); break; // ---------- page 1: lorawan parameters ---------- case 1: #if (HAS_LORA) // 3|Net:000000 Pwr:aa // 4|Dev:00000000 DR:0 // 5|CHMsk:0000 Nonce:0000 // 6|fUp:000000 fDn:000000 // 7|SNR:-0000 RSSI:-0000 // show pax libpax_counter_count(&count); dp_setFont(MY_FONT_LARGE); dp->printf("%-8u", count.pax); dp_setFont(MY_FONT_SMALL); dp->setCursor(0, MY_DISPLAY_FIRSTLINE); dp->printf("Net:%06X Pwr:%2u\r\n", LMIC.netid & 0x001FFFFF, LMIC.radio_txpow); dp->printf("Dev:%08X DR:%1u\r\n", LMIC.devaddr, LMIC.datarate); dp->printf("ChMsk:%04X Nonce:%04X\r\n", LMIC.channelMap, LMIC.devNonce); dp->printf("fUp:%-6u fDn:%-6u\r\n", LMIC.seqnoUp ? LMIC.seqnoUp - 1 : 0, LMIC.seqnoDn ? LMIC.seqnoDn - 1 : 0); dp->printf("SNR:%-5d RSSI:%-5d", (LMIC.snr + 2) / 4, LMIC.rssi); dp_dump(); break; #else // skip this page DisplayPage++; break; #endif // HAS_LORA // ---------- page 2: GPS ---------- case 2: #if (HAS_GPS) // show pax libpax_counter_count(&count); dp_setFont(MY_FONT_LARGE); dp->printf("%-8u", count.pax); // show satellite status at bottom line dp_setFont(MY_FONT_SMALL); dp->setCursor(0, 56); dp->printf("%u Sats", gps.satellites.value()); dp->printf(gps_hasfix() ? " " : " - No fix"); // show latitude and longitude dp_setFont(MY_FONT_STRETCHED); dp->setCursor(0, MY_DISPLAY_FIRSTLINE); dp->printf("%c%09.6f\r\n", gps.location.rawLat().negative ? 'S' : 'N', gps.location.lat()); dp->printf("%c%09.6f", gps.location.rawLng().negative ? 'W' : 'E', gps.location.lng()); dp_dump(); break; #else // skip this page DisplayPage++; break; #endif // ---------- page 3: BME280/680 ---------- case 3: #if (HAS_BME) dp_setFont(MY_FONT_STRETCHED); dp->setCursor(0, 0); dp->printf("TMP %-6.1f\r\n", bme_status.temperature); dp->printf("HUM %-6.1f\r\n", bme_status.humidity); dp->printf("PRS %-6.1f\r\n", bme_status.pressure); #ifdef HAS_BME680 dp->printf("IAQ %-6.0f", bme_status.iaq); #endif dp_dump(); break; #else // skip this page DisplayPage++; break; #endif // HAS_BME // ---------- page 4: time ---------- case 4: time(&now); localtime_r(&now, &timeinfo); dp_setFont(MY_FONT_STRETCHED); dp->setCursor(0, 0); dp->printf("Timeofday:"); dp->setCursor(0, 26); dp_setFont(MY_FONT_LARGE); strftime(strftime_buf, sizeof(strftime_buf), "%T", &timeinfo); dp->printf("%.8s\r\n", strftime_buf); dp_setFont(MY_FONT_SMALL); dp->printf("%-12.1f", uptime() / 1000.0); dp_dump(); break; // ---------- page 5: pax graph ---------- case 5: // update and show histogram dp_plotCurve(count.pax, false); dp_dump(plotbuf); break; // ---------- page 6: blank screen ---------- case 6: #ifdef HAS_BUTTON dp_clear(); break; #else // skip this page DisplayPage++; break; #endif } // switch (page) } // dp_refresh // ------------- display helper functions ----------------- void dp_setFont(int font, int inv) { dp->setFont(font); if (inv) dp->setTextColor(MY_DISPLAY_BGCOLOR, MY_DISPLAY_FGCOLOR); else dp->setTextColor(MY_DISPLAY_FGCOLOR, MY_DISPLAY_BGCOLOR); } void dp_dump(uint8_t *pBuffer) { if (pBuffer) memcpy(dp->getBuffer(), pBuffer, PLOTBUFFERSIZE); dp->display(); } void dp_clear(void) { dp->fillScreen(MY_DISPLAY_BGCOLOR); dp->display(); dp->setCursor(0, 0); } void dp_contrast(uint8_t contrast) { #if (HAS_DISPLAY) == 1 dp->setContrast(contrast); #elif (HAS_DISPLAY) > 1 // to do: gamma correction for TFT #endif } void dp_power(uint8_t screenon) { #if (HAS_DISPLAY) == 1 dp->setPower(screenon); #elif (HAS_DISPLAY) > 1 // to come #endif } void dp_shutdown(void) { #if (HAS_DISPLAY) == 1 dp->setPower(false); delay(DISPLAYREFRESH_MS / 1000 * 1.1); #elif (HAS_DISPLAY) > 1 // to come #endif } // ------------- QR code plotter ----------------- void dp_printqr(uint16_t offset_x, uint16_t offset_y, const char *Message) { uint8_t qrcodeData[qrcode_getBufferSize(QR_VERSION)]; qrcode_initText(&qrcode, qrcodeData, QR_VERSION, ECC_HIGH, Message); // draw QR code for (uint8_t y = 0; y < qrcode.size; y++) for (uint8_t x = 0; x < qrcode.size; x++) if (!qrcode_getModule(&qrcode, x, y)) // "black" dp->fillRect(x * QR_SCALEFACTOR + offset_x, y * QR_SCALEFACTOR + offset_y, QR_SCALEFACTOR, QR_SCALEFACTOR, MY_DISPLAY_FGCOLOR); // draw horizontal frame lines dp->fillRect(0, 0, qrcode.size * QR_SCALEFACTOR + 2 * offset_x, offset_y, MY_DISPLAY_FGCOLOR); dp->fillRect(0, qrcode.size * QR_SCALEFACTOR + offset_y, qrcode.size * QR_SCALEFACTOR + 2 * offset_x, offset_y, MY_DISPLAY_FGCOLOR); // draw vertical frame lines dp->fillRect(0, 0, offset_x, qrcode.size * QR_SCALEFACTOR + 2 * offset_y, MY_DISPLAY_FGCOLOR); dp->fillRect(qrcode.size * QR_SCALEFACTOR + offset_x, 0, offset_x, qrcode.size * QR_SCALEFACTOR + 2 * offset_y, MY_DISPLAY_FGCOLOR); } // ------------- graphics primitives ----------------- int dp_drawPixel(uint8_t *buf, const uint16_t x, const uint16_t y, const uint8_t dot) { if (x > MY_DISPLAY_WIDTH || y > MY_DISPLAY_HEIGHT) return -1; uint8_t bit = y & 7; uint16_t idx = y / 8 * MY_DISPLAY_WIDTH + x; buf[idx] &= ~(1 << bit); // clear pixel if (dot) buf[idx] |= (1 << bit); // set pixel return 0; } // ------------- buffer scroll functions ----------------- void dp_scrollHorizontal(uint8_t *buf, const uint16_t width, const uint16_t height, bool left) { uint16_t col, page, idx = 0; for (page = 0; page < height / 8; page++) { if (left) { // scroll left for (col = 0; col < width - 1; col++) { idx = page * width + col; buf[idx] = buf[idx + 1]; } buf[idx + 1] = 0; } else // scroll right { for (col = width - 1; col > 0; col--) { idx = page * width + col; buf[idx] = buf[idx - 1]; } buf[idx - 1] = 0; } } } void dp_scrollVertical(uint8_t *buf, const uint16_t width, const uint16_t height, int offset) { uint64_t buf_col; if (!offset) return; // nothing to do for (uint16_t col = 0; col < MY_DISPLAY_WIDTH; col++) { // convert column bytes from display buffer to uint64_t buf_col = *(uint64_t *)&buf[col * MY_DISPLAY_HEIGHT / 8]; if (offset > 0) // scroll down buf_col <<= offset; else // scroll up buf_col >>= abs(offset); // write back uint64_t to uint8_t display buffer *(uint64_t *)&buf[col * MY_DISPLAY_HEIGHT / 8] = buf_col; } } // ------------- curve plotter ----------------- void dp_plotCurve(uint16_t count, bool reset) { static uint16_t last_count = 0, col = 0, row = 0; uint16_t v_scroll = 0; // nothing new to plot? -> then exit early if ((last_count == count) && !reset) return; if (reset) { // next count cycle? if (col < MY_DISPLAY_WIDTH - 1) // matrix not full -> increment column col++; else // matrix full -> scroll left 1 dot dp_scrollHorizontal(plotbuf, MY_DISPLAY_WIDTH, MY_DISPLAY_HEIGHT, true); } else // clear current dot dp_drawPixel(plotbuf, col, row, 0); // scroll down, if necessary while ((count - v_scroll) > MY_DISPLAY_HEIGHT - 1) v_scroll++; if (v_scroll) dp_scrollVertical(plotbuf, MY_DISPLAY_WIDTH, MY_DISPLAY_HEIGHT, v_scroll); // set new dot // row = MY_DISPLAY_HEIGHT - 1 - (count - v_scroll) % MY_DISPLAY_HEIGHT; row = MY_DISPLAY_HEIGHT - 1 - count - v_scroll; last_count = count; dp_drawPixel(plotbuf, col, row, 1); } #endif // HAS_DISPLAY