mirror of https://github.com/meshcore-dev/MeshCore
committed by
GitHub
12 changed files with 1804 additions and 0 deletions
@ -0,0 +1,137 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include <helpers/ui/DisplayDriver.h> |
||||
|
|
||||
|
|
||||
|
#ifndef STATUS_BAR_SCROLL_MS |
||||
|
#define STATUS_BAR_SCROLL_MS 80 |
||||
|
#endif |
||||
|
|
||||
|
#ifndef STATUS_BAR_SEPARATOR |
||||
|
#define STATUS_BAR_SEPARATOR " | " |
||||
|
#endif |
||||
|
|
||||
|
#ifndef STATUS_BAR_UPDATE_MS |
||||
|
#define STATUS_BAR_UPDATE_MS 2000 // rebuild status string every 2s
|
||||
|
#endif |
||||
|
|
||||
|
class ScrollingStatusBar { |
||||
|
char _status[160]; |
||||
|
int _text_width; |
||||
|
int _scroll_x; |
||||
|
int _display_width; |
||||
|
unsigned long _next_scroll; |
||||
|
unsigned long _next_update; |
||||
|
bool _needs_redraw; |
||||
|
|
||||
|
// cached state for change detection
|
||||
|
char _last_name[32]; |
||||
|
uint16_t _last_batt_mv; |
||||
|
bool _last_buzzer_quiet; |
||||
|
bool _last_gps_on; |
||||
|
bool _last_ble_on; |
||||
|
|
||||
|
public: |
||||
|
ScrollingStatusBar() : _text_width(0), _scroll_x(0), _display_width(72), |
||||
|
_next_scroll(0), _next_update(0), _needs_redraw(true), |
||||
|
_last_batt_mv(0), _last_buzzer_quiet(false), |
||||
|
_last_gps_on(false), _last_ble_on(false) { |
||||
|
_status[0] = 0; |
||||
|
_last_name[0] = 0; |
||||
|
} |
||||
|
|
||||
|
void begin(int display_width) { |
||||
|
_display_width = display_width; |
||||
|
_scroll_x = 0; |
||||
|
_next_scroll = 0; |
||||
|
_next_update = 0; |
||||
|
} |
||||
|
|
||||
|
// Call periodically to update the status string content.
|
||||
|
// Only rebuilds if values have changed or update interval has elapsed.
|
||||
|
void update(DisplayDriver& display, const char* node_name, uint16_t batt_millivolts, |
||||
|
bool buzzer_quiet, bool gps_on, bool ble_on) { |
||||
|
|
||||
|
bool changed = (batt_millivolts != _last_batt_mv) |
||||
|
|| (buzzer_quiet != _last_buzzer_quiet) |
||||
|
|| (gps_on != _last_gps_on) |
||||
|
|| (ble_on != _last_ble_on) |
||||
|
|| (strcmp(node_name, _last_name) != 0); |
||||
|
|
||||
|
if (!changed) return; |
||||
|
|
||||
|
// cache current values
|
||||
|
strncpy(_last_name, node_name, sizeof(_last_name) - 1); |
||||
|
_last_name[sizeof(_last_name) - 1] = 0; |
||||
|
_last_batt_mv = batt_millivolts; |
||||
|
_last_buzzer_quiet = buzzer_quiet; |
||||
|
_last_gps_on = gps_on; |
||||
|
_last_ble_on = ble_on; |
||||
|
|
||||
|
float volts = batt_millivolts / 1000.0f; |
||||
|
|
||||
|
snprintf(_status, sizeof(_status), |
||||
|
"%s" STATUS_BAR_SEPARATOR |
||||
|
"%.2fV" STATUS_BAR_SEPARATOR |
||||
|
"BUZ:%s" STATUS_BAR_SEPARATOR |
||||
|
"GPS:%s" STATUS_BAR_SEPARATOR |
||||
|
"BLE:%s" |
||||
|
" - ", // trailing gap before the text loops
|
||||
|
node_name, |
||||
|
volts, |
||||
|
buzzer_quiet ? "OFF" : "ON", |
||||
|
gps_on ? "ON" : "OFF", |
||||
|
ble_on ? "ON" : "OFF" |
||||
|
); |
||||
|
|
||||
|
display.setTextSize(1); |
||||
|
_text_width = display.getTextWidth(_status); |
||||
|
_next_update = millis() + STATUS_BAR_UPDATE_MS; |
||||
|
_needs_redraw = true; |
||||
|
} |
||||
|
|
||||
|
// Returns true if the status bar needs a redraw this frame.
|
||||
|
bool needsRedraw() { |
||||
|
if (_text_width <= _display_width) return _needs_redraw; // static, no scrolling
|
||||
|
return millis() >= _next_scroll; |
||||
|
} |
||||
|
|
||||
|
// Render the status bar via DisplayDriver.
|
||||
|
// U8g2 full-buffer mode clips to display bounds automatically,
|
||||
|
// and the font height stays within STATUS_BAR_HEIGHT, so no
|
||||
|
// explicit clip window is needed.
|
||||
|
void render(DisplayDriver& display) { |
||||
|
if (_status[0] == 0) return; |
||||
|
|
||||
|
display.setTextSize(1); |
||||
|
display.setColor(DisplayDriver::GREEN); |
||||
|
|
||||
|
// if (_needs_redraw) {
|
||||
|
// _text_width = display.getTextWidth(_status);
|
||||
|
// }
|
||||
|
|
||||
|
// static text: no scrolling needed
|
||||
|
if (_text_width <= _display_width) { |
||||
|
display.setCursor(0, 0); |
||||
|
display.print(_status); |
||||
|
_needs_redraw = false; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
int x = _scroll_x; |
||||
|
do { |
||||
|
display.setCursor(x, 0); |
||||
|
display.print(_status); |
||||
|
x += _text_width; |
||||
|
} while (x < _display_width); |
||||
|
|
||||
|
|
||||
|
// advance scroll position
|
||||
|
_scroll_x--; |
||||
|
if (_scroll_x <= -_text_width) _scroll_x = 0; |
||||
|
|
||||
|
_next_scroll = millis() + STATUS_BAR_SCROLL_MS; |
||||
|
_needs_redraw = false; |
||||
|
} |
||||
|
|
||||
|
}; |
||||
@ -0,0 +1,819 @@ |
|||||
|
#include "UITask.h" |
||||
|
#include <helpers/TxtDataHelpers.h> |
||||
|
#include "../MyMesh.h" |
||||
|
#include "target.h" |
||||
|
#include "u8g2_icons.h" |
||||
|
|
||||
|
#ifdef WIFI_SSID |
||||
|
#include <WiFi.h> |
||||
|
#endif |
||||
|
|
||||
|
#ifndef AUTO_OFF_MILLIS |
||||
|
#define AUTO_OFF_MILLIS 15000 // 15 seconds
|
||||
|
#endif |
||||
|
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
||||
|
|
||||
|
#ifdef PIN_STATUS_LED |
||||
|
#define LED_ON_MILLIS 20 |
||||
|
#define LED_ON_MSG_MILLIS 200 |
||||
|
#define LED_CYCLE_MILLIS 4000 |
||||
|
#endif |
||||
|
|
||||
|
#define LONG_PRESS_MILLIS 1200 |
||||
|
|
||||
|
#ifndef UI_RECENT_LIST_SIZE |
||||
|
#define UI_RECENT_LIST_SIZE 4 |
||||
|
#endif |
||||
|
|
||||
|
#if UI_HAS_JOYSTICK |
||||
|
#define PRESS_LABEL "press Enter" |
||||
|
#else |
||||
|
#define PRESS_LABEL "long press" |
||||
|
#endif |
||||
|
|
||||
|
class SplashScreen : public UIScreen { |
||||
|
UITask* _task; |
||||
|
unsigned long dismiss_after; |
||||
|
unsigned long version_after; |
||||
|
char _version_info[12]; |
||||
|
|
||||
|
public: |
||||
|
SplashScreen(UITask* task) : _task(task) { |
||||
|
// strip off dash and commit hash by changing dash to null terminator
|
||||
|
// e.g: v1.2.3-abcdef -> v1.2.3
|
||||
|
const char *ver = FIRMWARE_VERSION; |
||||
|
const char *dash = strchr(ver, '-'); |
||||
|
|
||||
|
int len = dash ? dash - ver : strlen(ver); |
||||
|
if (len >= sizeof(_version_info)) len = sizeof(_version_info) - 1; |
||||
|
memcpy(_version_info, ver, len); |
||||
|
_version_info[len] = 0; |
||||
|
|
||||
|
version_after = millis() + BOOT_SCREEN_MILLIS / 2; |
||||
|
dismiss_after = millis() + BOOT_SCREEN_MILLIS; |
||||
|
} |
||||
|
|
||||
|
int render(DisplayDriver& display) override { |
||||
|
if (millis() < version_after) { |
||||
|
// meshcore logo
|
||||
|
display.setColor(DisplayDriver::BLUE); |
||||
|
int logoWidth = 72; |
||||
|
display.drawXbm(0, 0, meshcore_logo, 72, 36); |
||||
|
} else { |
||||
|
|
||||
|
// meshcore website
|
||||
|
const char* website = "meshcore.io"; |
||||
|
display.setColor(DisplayDriver::LIGHT); |
||||
|
display.setTextSize(1); |
||||
|
uint16_t websiteWidth = display.getTextWidth(website); |
||||
|
display.setCursor((display.width() - websiteWidth) / 2, 9); |
||||
|
display.print(website); |
||||
|
|
||||
|
// version info
|
||||
|
display.setColor(DisplayDriver::LIGHT); |
||||
|
display.setTextSize(1); |
||||
|
display.drawTextCentered(display.width()/2, 18, _version_info); |
||||
|
|
||||
|
display.setTextSize(1); |
||||
|
display.drawTextCentered(display.width()/2, 27, FIRMWARE_BUILD_DATE); |
||||
|
} |
||||
|
return 1000; |
||||
|
} |
||||
|
|
||||
|
void poll() override { |
||||
|
if (millis() >= dismiss_after) { |
||||
|
_task->gotoHomeScreen(); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
class HomeScreen : public UIScreen { |
||||
|
enum HomePage { |
||||
|
FIRST, |
||||
|
RECENT, |
||||
|
RADIO, |
||||
|
BLUETOOTH, |
||||
|
ADVERT, |
||||
|
#if ENV_INCLUDE_GPS == 1 |
||||
|
GPS, |
||||
|
#endif |
||||
|
#if UI_SENSORS_PAGE == 1 |
||||
|
SENSORS, |
||||
|
#endif |
||||
|
SHUTDOWN, |
||||
|
Count // keep as last
|
||||
|
}; |
||||
|
|
||||
|
UITask* _task; |
||||
|
mesh::RTCClock* _rtc; |
||||
|
SensorManager* _sensors; |
||||
|
NodePrefs* _node_prefs; |
||||
|
uint8_t _page; |
||||
|
bool _shutdown_init; |
||||
|
AdvertPath recent[UI_RECENT_LIST_SIZE]; |
||||
|
|
||||
|
CayenneLPP sensors_lpp; |
||||
|
int sensors_nb = 0; |
||||
|
bool sensors_scroll = false; |
||||
|
int sensors_scroll_offset = 0; |
||||
|
int next_sensors_refresh = 0; |
||||
|
|
||||
|
void refresh_sensors() { |
||||
|
if (millis() > next_sensors_refresh) { |
||||
|
sensors_lpp.reset(); |
||||
|
sensors_nb = 0; |
||||
|
sensors_lpp.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); |
||||
|
sensors.querySensors(0xFF, sensors_lpp); |
||||
|
LPPReader reader (sensors_lpp.getBuffer(), sensors_lpp.getSize()); |
||||
|
uint8_t channel, type; |
||||
|
while(reader.readHeader(channel, type)) { |
||||
|
reader.skipData(type); |
||||
|
sensors_nb ++; |
||||
|
} |
||||
|
sensors_scroll = sensors_nb > UI_RECENT_LIST_SIZE; |
||||
|
#if AUTO_OFF_MILLIS > 0 |
||||
|
next_sensors_refresh = millis() + 5000; // refresh sensor values every 5 sec
|
||||
|
#else |
||||
|
next_sensors_refresh = millis() + 60000; // refresh sensor values every 1 min
|
||||
|
#endif |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public: |
||||
|
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs) |
||||
|
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), |
||||
|
_shutdown_init(false), sensors_lpp(200) { } |
||||
|
|
||||
|
void poll() override { |
||||
|
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
|
||||
|
_task->shutdown(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
int render(DisplayDriver& display) override { |
||||
|
char tmp[80]; |
||||
|
|
||||
|
if (_page == HomePage::FIRST) { |
||||
|
// // node name
|
||||
|
// display.setTextSize(1);
|
||||
|
// display.setColor(DisplayDriver::GREEN);
|
||||
|
// char filtered_name[sizeof(_node_prefs->node_name)];
|
||||
|
// display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name));
|
||||
|
// display.setCursor(0, 0);
|
||||
|
// display.print(filtered_name);
|
||||
|
|
||||
|
|
||||
|
display.setColor(DisplayDriver::YELLOW); |
||||
|
display.setTextSize(2); |
||||
|
sprintf(tmp, "MSG: %d", _task->getMsgCount()); |
||||
|
display.setCursor(0, 10); |
||||
|
display.print(tmp); |
||||
|
|
||||
|
sprintf(tmp, "BATT: %.2fV", _task->getCachedBattMV() / 1000.0f); |
||||
|
display.setCursor(0, 19); |
||||
|
display.print(tmp); |
||||
|
|
||||
|
#ifdef WIFI_SSID |
||||
|
IPAddress ip = WiFi.localIP(); |
||||
|
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); |
||||
|
display.setTextSize(1); |
||||
|
display.drawTextCentered(display.width() / 2, 54, tmp); |
||||
|
#endif |
||||
|
if (_task->hasConnection()) { |
||||
|
display.setColor(DisplayDriver::GREEN); |
||||
|
display.setTextSize(1); |
||||
|
display.drawTextCentered(display.width() / 2, display.height()-8, "< Connected >"); |
||||
|
|
||||
|
} else if (the_mesh.getBLEPin() != 0) { // BT pin
|
||||
|
display.setColor(DisplayDriver::RED); |
||||
|
display.setTextSize(2); |
||||
|
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); |
||||
|
display.drawTextCentered(display.width() / 2, display.height()-8, tmp); |
||||
|
} |
||||
|
} else if (_page == HomePage::RECENT) { |
||||
|
the_mesh.getRecentlyHeard(recent, UI_RECENT_LIST_SIZE); |
||||
|
display.setColor(DisplayDriver::GREEN); |
||||
|
int y = 8; |
||||
|
for (int i = 0; i < UI_RECENT_LIST_SIZE; i++, y += 11) { |
||||
|
auto a = &recent[i]; |
||||
|
if (a->name[0] == 0) continue; // empty slot
|
||||
|
int secs = _rtc->getCurrentTime() - a->recv_timestamp; |
||||
|
if (secs < 60) { |
||||
|
sprintf(tmp, "%ds", secs); |
||||
|
} else if (secs < 60*60) { |
||||
|
sprintf(tmp, "%dm", secs / 60); |
||||
|
} else { |
||||
|
sprintf(tmp, "%dh", secs / (60*60)); |
||||
|
} |
||||
|
|
||||
|
int timestamp_width = display.getTextWidth(tmp); |
||||
|
int max_name_width = display.width() - timestamp_width - 1; |
||||
|
|
||||
|
char filtered_recent_name[sizeof(a->name)]; |
||||
|
display.translateUTF8ToBlocks(filtered_recent_name, a->name, sizeof(filtered_recent_name)); |
||||
|
display.drawTextEllipsized(0, y, max_name_width, filtered_recent_name); |
||||
|
display.setCursor(display.width() - timestamp_width - 1, y); |
||||
|
display.print(tmp); |
||||
|
} |
||||
|
} else if (_page == HomePage::RADIO) { |
||||
|
display.setColor(DisplayDriver::YELLOW); |
||||
|
display.setTextSize(1); |
||||
|
// frequency and spreading factor
|
||||
|
display.setCursor(0, 8); |
||||
|
sprintf(tmp, "FQ %06.3f", _node_prefs->freq); |
||||
|
display.print(tmp); |
||||
|
sprintf(tmp, "SF%d", _node_prefs->sf); |
||||
|
display.drawTextRightAlign(display.width(), 8, tmp); |
||||
|
// bandwidth and coding rate
|
||||
|
display.setCursor(0, 17); |
||||
|
sprintf(tmp, "BW %03.2f", _node_prefs->bw); |
||||
|
display.print(tmp); |
||||
|
sprintf(tmp, "CR%d", _node_prefs->cr); |
||||
|
display.drawTextRightAlign(display.width(), 17, tmp); |
||||
|
// tx power and noise floor
|
||||
|
display.setCursor(0, 26); |
||||
|
sprintf(tmp, "NF %ddB", radio_driver.getNoiseFloor()); |
||||
|
display.print(tmp); |
||||
|
sprintf(tmp, "TX%d", _node_prefs->tx_power_dbm); |
||||
|
display.drawTextRightAlign(display.width(), 26, tmp); |
||||
|
|
||||
|
} else if (_page == HomePage::BLUETOOTH) { |
||||
|
display.setColor(DisplayDriver::GREEN); |
||||
|
display.drawXbm((display.width() - 32) / 2, 8, |
||||
|
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off, |
||||
|
32, 32); |
||||
|
display.setTextSize(1); |
||||
|
// display.drawTextCentered(display.width() / 2, 40 - 11, "toggle: " PRESS_LABEL);
|
||||
|
} else if (_page == HomePage::ADVERT) { |
||||
|
display.setColor(DisplayDriver::GREEN); |
||||
|
display.drawXbm((display.width() - 32) / 2, 8, advert_icon, 32, 32); |
||||
|
// display.drawTextCentered(display.width() / 2, 40 - 11, "advert: " PRESS_LABEL);
|
||||
|
#if ENV_INCLUDE_GPS == 1 |
||||
|
} else if (_page == HomePage::GPS) { |
||||
|
LocationProvider* nmea = sensors.getLocationProvider(); |
||||
|
char buf[50]; |
||||
|
int y = 8; |
||||
|
bool gps_state = _task->getGPSState(); |
||||
|
#ifdef PIN_GPS_SWITCH |
||||
|
bool hw_gps_state = digitalRead(PIN_GPS_SWITCH); |
||||
|
if (gps_state != hw_gps_state) { |
||||
|
strcpy(buf, gps_state ? "gps off(hw)" : "gps off(sw)"); |
||||
|
} else { |
||||
|
strcpy(buf, gps_state ? "gps on" : "gps off"); |
||||
|
} |
||||
|
#else |
||||
|
strcpy(buf, gps_state ? "gps on" : "gps off"); |
||||
|
#endif |
||||
|
display.drawTextLeftAlign(0, y, buf); |
||||
|
if (nmea == NULL) { |
||||
|
// y = y + 8;
|
||||
|
display.drawTextLeftAlign(0, y, "Can't access GPS"); |
||||
|
} else { |
||||
|
if (!gps_state || !nmea->isValid()) { |
||||
|
strcpy(buf, "no fix"); |
||||
|
} else { |
||||
|
sprintf(buf, "%d sat", nmea->satellitesCount()); |
||||
|
} |
||||
|
display.drawTextRightAlign(display.width()-1, y, buf); |
||||
|
y = y + 8; |
||||
|
sprintf(buf, "lat %.4f", |
||||
|
nmea->getLatitude()/1000000.); |
||||
|
display.drawTextLeftAlign(0, y, buf); |
||||
|
y = y + 8; |
||||
|
sprintf(buf, "lon %.4f", |
||||
|
nmea->getLongitude()/1000000.); |
||||
|
display.drawTextLeftAlign(0, y, buf); |
||||
|
y = y + 8; |
||||
|
sprintf(buf, "alt %.1f", nmea->getAltitude()/1000.); |
||||
|
display.drawTextLeftAlign(0, y, buf); |
||||
|
} |
||||
|
#endif |
||||
|
#if UI_SENSORS_PAGE == 1 |
||||
|
} else if (_page == HomePage::SENSORS) { |
||||
|
int y = 8; |
||||
|
refresh_sensors(); |
||||
|
char buf[30]; |
||||
|
char name[30]; |
||||
|
LPPReader r(sensors_lpp.getBuffer(), sensors_lpp.getSize()); |
||||
|
|
||||
|
for (int i = 0; i < sensors_scroll_offset; i++) { |
||||
|
uint8_t channel, type; |
||||
|
r.readHeader(channel, type); |
||||
|
r.skipData(type); |
||||
|
} |
||||
|
|
||||
|
for (int i = 0; i < (sensors_scroll?UI_RECENT_LIST_SIZE:sensors_nb); i++) { |
||||
|
uint8_t channel, type; |
||||
|
if (!r.readHeader(channel, type)) { // reached end, reset
|
||||
|
r.reset(); |
||||
|
r.readHeader(channel, type); |
||||
|
} |
||||
|
|
||||
|
display.setCursor(0, y); |
||||
|
float v; |
||||
|
switch (type) { |
||||
|
case LPP_GPS: // GPS
|
||||
|
float lat, lon, alt; |
||||
|
r.readGPS(lat, lon, alt); |
||||
|
strcpy(name, "gps"); sprintf(buf, "%.4f %.4f", lat, lon); |
||||
|
break; |
||||
|
case LPP_VOLTAGE: |
||||
|
r.readVoltage(v); |
||||
|
strcpy(name, "voltage"); sprintf(buf, "%6.2f", v); |
||||
|
break; |
||||
|
case LPP_CURRENT: |
||||
|
r.readCurrent(v); |
||||
|
strcpy(name, "current"); sprintf(buf, "%.3f", v); |
||||
|
break; |
||||
|
case LPP_TEMPERATURE: |
||||
|
r.readTemperature(v); |
||||
|
strcpy(name, "temperature"); sprintf(buf, "%.2f", v); |
||||
|
break; |
||||
|
case LPP_RELATIVE_HUMIDITY: |
||||
|
r.readRelativeHumidity(v); |
||||
|
strcpy(name, "humidity"); sprintf(buf, "%.2f", v); |
||||
|
break; |
||||
|
case LPP_BAROMETRIC_PRESSURE: |
||||
|
r.readPressure(v); |
||||
|
strcpy(name, "pressure"); sprintf(buf, "%.2f", v); |
||||
|
break; |
||||
|
case LPP_ALTITUDE: |
||||
|
r.readAltitude(v); |
||||
|
strcpy(name, "altitude"); sprintf(buf, "%.0f", v); |
||||
|
break; |
||||
|
case LPP_POWER: |
||||
|
r.readPower(v); |
||||
|
strcpy(name, "power"); sprintf(buf, "%6.2f", v); |
||||
|
break; |
||||
|
default: |
||||
|
r.skipData(type); |
||||
|
strcpy(name, "unk"); sprintf(buf, ""); |
||||
|
} |
||||
|
display.setCursor(0, y); |
||||
|
display.print(name); |
||||
|
display.setCursor( |
||||
|
display.width()-display.getTextWidth(buf)-1, y |
||||
|
); |
||||
|
display.print(buf); |
||||
|
y = y + 12; |
||||
|
} |
||||
|
if (sensors_scroll) sensors_scroll_offset = (sensors_scroll_offset+1)%sensors_nb; |
||||
|
else sensors_scroll_offset = 0; |
||||
|
#endif |
||||
|
} else if (_page == HomePage::SHUTDOWN) { |
||||
|
display.setColor(DisplayDriver::GREEN); |
||||
|
display.setTextSize(1); |
||||
|
if (_shutdown_init) { |
||||
|
display.drawTextCentered(display.width() / 2, 20, "hibernating..."); |
||||
|
} else { |
||||
|
display.drawXbm((display.width() - 32) / 2, 8, power_icon, 32, 32); |
||||
|
// display.drawTextCentered(display.width() / 2, 40 - 11, "hibernate:" PRESS_LABEL);
|
||||
|
} |
||||
|
} |
||||
|
return 5000; // next render after 5000 ms
|
||||
|
} |
||||
|
|
||||
|
bool handleInput(char c) override { |
||||
|
if (c == KEY_LEFT || c == KEY_PREV) { |
||||
|
_page = (_page + HomePage::Count - 1) % HomePage::Count; |
||||
|
return true; |
||||
|
} |
||||
|
if (c == KEY_NEXT || c == KEY_RIGHT) { |
||||
|
_page = (_page + 1) % HomePage::Count; |
||||
|
if (_page == HomePage::RECENT) { |
||||
|
_task->showAlert("Recent adverts", 800); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { |
||||
|
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
|
||||
|
_task->disableSerial(); |
||||
|
} else { |
||||
|
_task->enableSerial(); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
if (c == KEY_ENTER && _page == HomePage::ADVERT) { |
||||
|
_task->notify(UIEventType::ack); |
||||
|
if (the_mesh.advert()) { |
||||
|
_task->showAlert("Advert sent!", 1000); |
||||
|
} else { |
||||
|
_task->showAlert("Advert failed..", 1000); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
#if ENV_INCLUDE_GPS == 1 |
||||
|
if (c == KEY_ENTER && _page == HomePage::GPS) { |
||||
|
_task->toggleGPS(); |
||||
|
return true; |
||||
|
} |
||||
|
#endif |
||||
|
#if UI_SENSORS_PAGE == 1 |
||||
|
if (c == KEY_ENTER && _page == HomePage::SENSORS) { |
||||
|
_task->toggleGPS(); |
||||
|
next_sensors_refresh=0; |
||||
|
return true; |
||||
|
} |
||||
|
#endif |
||||
|
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { |
||||
|
_shutdown_init = true; // need to wait for button to be released
|
||||
|
return true; |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) { |
||||
|
_display = display; |
||||
|
_sensors = sensors; |
||||
|
_auto_off = millis() + AUTO_OFF_MILLIS; |
||||
|
_cached_batt_mv = getBattMilliVolts(); |
||||
|
|
||||
|
#if defined(PIN_USER_BTN) |
||||
|
user_btn.begin(); |
||||
|
#endif |
||||
|
#if defined(PIN_USER_BTN_ANA) |
||||
|
analog_btn.begin(); |
||||
|
#endif |
||||
|
|
||||
|
_node_prefs = node_prefs; |
||||
|
|
||||
|
if (_display != NULL) { |
||||
|
_display->turnOn(); |
||||
|
} |
||||
|
_statusBar.begin(_display->width()); |
||||
|
|
||||
|
|
||||
|
#ifdef PIN_BUZZER |
||||
|
buzzer.begin(); |
||||
|
buzzer.quiet(_node_prefs->buzzer_quiet); |
||||
|
buzzer.startup(); |
||||
|
#endif |
||||
|
|
||||
|
#ifdef PIN_VIBRATION |
||||
|
vibration.begin(); |
||||
|
#endif |
||||
|
|
||||
|
ui_started_at = millis(); |
||||
|
_alert_expiry = 0; |
||||
|
|
||||
|
splash = new SplashScreen(this); |
||||
|
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); |
||||
|
setCurrScreen(splash); |
||||
|
} |
||||
|
|
||||
|
void UITask::showAlert(const char* text, int duration_millis) { |
||||
|
strcpy(_alert, text); |
||||
|
_alert_expiry = millis() + duration_millis; |
||||
|
} |
||||
|
|
||||
|
void UITask::notify(UIEventType t) { |
||||
|
#if defined(PIN_BUZZER) |
||||
|
switch(t){ |
||||
|
case UIEventType::contactMessage: |
||||
|
// gemini's pick
|
||||
|
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); |
||||
|
break; |
||||
|
case UIEventType::channelMessage: |
||||
|
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#"); |
||||
|
break; |
||||
|
case UIEventType::ack: |
||||
|
buzzer.play("ack:d=32,o=8,b=120:c"); |
||||
|
break; |
||||
|
case UIEventType::roomMessage: |
||||
|
case UIEventType::newContactMessage: |
||||
|
case UIEventType::none: |
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
#ifdef PIN_VIBRATION |
||||
|
// Trigger vibration for all UI events except none
|
||||
|
if (t != UIEventType::none) { |
||||
|
vibration.trigger(); |
||||
|
} |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
|
||||
|
void UITask::msgRead(int msgcount) { |
||||
|
_msgcount = msgcount; |
||||
|
if (msgcount == 0) { |
||||
|
gotoHomeScreen(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { |
||||
|
_msgcount = msgcount; |
||||
|
|
||||
|
if (_display != NULL) { |
||||
|
if (!_display->isOn() && !hasConnection()) { |
||||
|
_display->turnOn(); |
||||
|
} |
||||
|
if (_display->isOn()) { |
||||
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
||||
|
_next_refresh = 100; // trigger refresh
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void UITask::userLedHandler() { |
||||
|
#ifdef PIN_STATUS_LED |
||||
|
int cur_time = millis(); |
||||
|
if (cur_time > next_led_change) { |
||||
|
if (led_state == 0) { |
||||
|
led_state = 1; |
||||
|
if (_msgcount > 0) { |
||||
|
last_led_increment = LED_ON_MSG_MILLIS; |
||||
|
} else { |
||||
|
last_led_increment = LED_ON_MILLIS; |
||||
|
} |
||||
|
next_led_change = cur_time + last_led_increment; |
||||
|
} else { |
||||
|
led_state = 0; |
||||
|
next_led_change = cur_time + LED_CYCLE_MILLIS - last_led_increment; |
||||
|
} |
||||
|
digitalWrite(PIN_STATUS_LED, led_state == LED_STATE_ON); |
||||
|
} |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
void UITask::setCurrScreen(UIScreen* c) { |
||||
|
curr = c; |
||||
|
_next_refresh = 100; |
||||
|
} |
||||
|
|
||||
|
/*
|
||||
|
hardware-agnostic pre-shutdown activity should be done here |
||||
|
*/ |
||||
|
void UITask::shutdown(bool restart){ |
||||
|
|
||||
|
#ifdef PIN_BUZZER |
||||
|
/* note: we have a choice here -
|
||||
|
we can do a blocking buzzer.loop() with non-deterministic consequences |
||||
|
or we can set a flag and delay the shutdown for a couple of seconds |
||||
|
while a non-blocking buzzer.loop() plays out in UITask::loop() |
||||
|
*/ |
||||
|
buzzer.shutdown(); |
||||
|
uint32_t buzzer_timer = millis(); // fail-safe shutdown
|
||||
|
while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer) |
||||
|
buzzer.loop(); |
||||
|
|
||||
|
#endif // PIN_BUZZER
|
||||
|
|
||||
|
if (restart) { |
||||
|
_board->reboot(); |
||||
|
} else { |
||||
|
_display->turnOff(); |
||||
|
radio_driver.powerOff(); |
||||
|
_board->powerOff(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bool UITask::isButtonPressed() const { |
||||
|
#ifdef PIN_USER_BTN |
||||
|
return user_btn.isPressed(); |
||||
|
#else |
||||
|
return false; |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
void UITask::loop() { |
||||
|
char c = 0; |
||||
|
#if UI_HAS_JOYSTICK |
||||
|
int ev = user_btn.check(); |
||||
|
if (ev == BUTTON_EVENT_CLICK) { |
||||
|
c = checkDisplayOn(KEY_ENTER); |
||||
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) { |
||||
|
c = handleLongPress(KEY_ENTER); // REVISIT: could be mapped to different key code
|
||||
|
} |
||||
|
ev = joystick_left.check(); |
||||
|
if (ev == BUTTON_EVENT_CLICK) { |
||||
|
c = checkDisplayOn(KEY_LEFT); |
||||
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) { |
||||
|
c = handleLongPress(KEY_LEFT); |
||||
|
} |
||||
|
ev = joystick_right.check(); |
||||
|
if (ev == BUTTON_EVENT_CLICK) { |
||||
|
c = checkDisplayOn(KEY_RIGHT); |
||||
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) { |
||||
|
c = handleLongPress(KEY_RIGHT); |
||||
|
} |
||||
|
ev = back_btn.check(); |
||||
|
if (ev == BUTTON_EVENT_TRIPLE_CLICK) { |
||||
|
c = handleTripleClick(KEY_SELECT); |
||||
|
} |
||||
|
#elif defined(PIN_USER_BTN) |
||||
|
int ev = user_btn.check(); |
||||
|
if (ev == BUTTON_EVENT_CLICK) { |
||||
|
c = checkDisplayOn(KEY_NEXT); |
||||
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) { |
||||
|
c = handleLongPress(KEY_ENTER); |
||||
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { |
||||
|
c = handleDoubleClick(KEY_PREV); |
||||
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { |
||||
|
c = handleTripleClick(KEY_SELECT); |
||||
|
} |
||||
|
#endif |
||||
|
#if defined(PIN_USER_BTN_ANA) |
||||
|
if (abs(millis() - _analogue_pin_read_millis) > 10) { |
||||
|
int ev = analog_btn.check(); |
||||
|
if (ev == BUTTON_EVENT_CLICK) { |
||||
|
c = checkDisplayOn(KEY_NEXT); |
||||
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) { |
||||
|
c = handleLongPress(KEY_ENTER); |
||||
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { |
||||
|
c = handleDoubleClick(KEY_PREV); |
||||
|
} else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { |
||||
|
c = handleTripleClick(KEY_SELECT); |
||||
|
} |
||||
|
_analogue_pin_read_millis = millis(); |
||||
|
} |
||||
|
#endif |
||||
|
#if defined(BACKLIGHT_BTN) |
||||
|
if (millis() > next_backlight_btn_check) { |
||||
|
bool touch_state = digitalRead(PIN_BUTTON2); |
||||
|
#if defined(DISP_BACKLIGHT) |
||||
|
digitalWrite(DISP_BACKLIGHT, !touch_state); |
||||
|
#elif defined(EXP_PIN_BACKLIGHT) |
||||
|
expander.digitalWrite(EXP_PIN_BACKLIGHT, !touch_state); |
||||
|
#endif |
||||
|
next_backlight_btn_check = millis() + 300; |
||||
|
} |
||||
|
#endif |
||||
|
#if defined(HAS_TORCH) |
||||
|
ev = back_btn.check(); |
||||
|
if (ev == BUTTON_EVENT_CLICK && c == 0) { |
||||
|
c = checkDisplayOn(KEY_PREV); |
||||
|
} else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { |
||||
|
board.toggleTorch(); |
||||
|
c = 0; |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
if (c != 0 && curr) { |
||||
|
curr->handleInput(c); |
||||
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
|
_next_refresh = 100; // trigger refresh
|
||||
|
} |
||||
|
|
||||
|
userLedHandler(); |
||||
|
|
||||
|
#ifdef PIN_BUZZER |
||||
|
if (buzzer.isPlaying()) buzzer.loop(); |
||||
|
#endif |
||||
|
|
||||
|
if (curr) curr->poll(); |
||||
|
|
||||
|
if (_display != NULL && _display->isOn()) { |
||||
|
_statusBar.update(*_display, |
||||
|
_node_prefs->node_name, |
||||
|
_cached_batt_mv, |
||||
|
isBuzzerQuiet(), |
||||
|
getGPSState(), |
||||
|
isSerialEnabled()); |
||||
|
|
||||
|
bool status_dirty = _statusBar.needsRedraw(); |
||||
|
bool content_dirty = (millis() >= _next_refresh && curr); |
||||
|
|
||||
|
if (status_dirty || content_dirty) { |
||||
|
_display->startFrame(); |
||||
|
_statusBar.render(*_display); |
||||
|
|
||||
|
if (curr) { |
||||
|
int delay_millis = curr->render(*_display); |
||||
|
if (content_dirty) { |
||||
|
_next_refresh = millis() + delay_millis; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (millis() < _alert_expiry) { // render alert popup
|
||||
|
_display->setTextSize(1); |
||||
|
int y = _display->height() / 3; |
||||
|
int p = _display->height() / 32; |
||||
|
_display->setColor(DisplayDriver::DARK); |
||||
|
_display->fillRect(p, y, _display->width() - p*2, y); |
||||
|
_display->setColor(DisplayDriver::LIGHT); // draw box border
|
||||
|
_display->drawRect(p, y, _display->width() - p*2, y); |
||||
|
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert); |
||||
|
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed
|
||||
|
} |
||||
|
|
||||
|
_display->endFrame(); |
||||
|
} |
||||
|
#if AUTO_OFF_MILLIS > 0 |
||||
|
if (millis() > _auto_off) { |
||||
|
_display->turnOff(); |
||||
|
} |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
#ifdef PIN_VIBRATION |
||||
|
vibration.loop(); |
||||
|
#endif |
||||
|
|
||||
|
#ifdef AUTO_SHUTDOWN_MILLIVOLTS |
||||
|
if (millis() > next_batt_chck) { |
||||
|
_cached_batt_mv = getBattMilliVolts(); |
||||
|
if (_cached_batt_mv > 0 && _cached_batt_mv < AUTO_SHUTDOWN_MILLIVOLTS) { |
||||
|
|
||||
|
shutdown(); |
||||
|
|
||||
|
} |
||||
|
next_batt_chck = millis() + 8000; |
||||
|
} |
||||
|
#else |
||||
|
if (_display != NULL && _display->isOn() && millis >= next_batt_chck) { |
||||
|
_cached_batt_mv = getBattMilliVolts(); |
||||
|
next_batt_chck = millis() + 8000; |
||||
|
} |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
char UITask::checkDisplayOn(char c) { |
||||
|
if (_display != NULL) { |
||||
|
if (!_display->isOn()) { |
||||
|
_display->turnOn(); // turn display on and consume event
|
||||
|
c = 0; |
||||
|
} |
||||
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
||||
|
_next_refresh = 0; // trigger refresh
|
||||
|
} |
||||
|
return c; |
||||
|
} |
||||
|
|
||||
|
char UITask::handleLongPress(char c) { |
||||
|
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
|
||||
|
the_mesh.enterCLIRescue(); |
||||
|
c = 0; // consume event
|
||||
|
} |
||||
|
return c; |
||||
|
} |
||||
|
|
||||
|
char UITask::handleDoubleClick(char c) { |
||||
|
MESH_DEBUG_PRINTLN("UITask: double click triggered"); |
||||
|
checkDisplayOn(c); |
||||
|
return c; |
||||
|
} |
||||
|
|
||||
|
char UITask::handleTripleClick(char c) { |
||||
|
MESH_DEBUG_PRINTLN("UITask: triple click triggered"); |
||||
|
checkDisplayOn(c); |
||||
|
toggleBuzzer(); |
||||
|
c = 0; |
||||
|
return c; |
||||
|
} |
||||
|
|
||||
|
bool UITask::getGPSState() { |
||||
|
if (_sensors != NULL) { |
||||
|
int num = _sensors->getNumSettings(); |
||||
|
for (int i = 0; i < num; i++) { |
||||
|
if (strcmp(_sensors->getSettingName(i), "gps") == 0) { |
||||
|
return !strcmp(_sensors->getSettingValue(i), "1"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
void UITask::toggleGPS() { |
||||
|
if (_sensors != NULL) { |
||||
|
// toggle GPS on/off
|
||||
|
int num = _sensors->getNumSettings(); |
||||
|
for (int i = 0; i < num; i++) { |
||||
|
if (strcmp(_sensors->getSettingName(i), "gps") == 0) { |
||||
|
if (strcmp(_sensors->getSettingValue(i), "1") == 0) { |
||||
|
_sensors->setSettingValue("gps", "0"); |
||||
|
_node_prefs->gps_enabled = 0; |
||||
|
notify(UIEventType::ack); |
||||
|
} else { |
||||
|
_sensors->setSettingValue("gps", "1"); |
||||
|
_node_prefs->gps_enabled = 1; |
||||
|
notify(UIEventType::ack); |
||||
|
} |
||||
|
the_mesh.savePrefs(); |
||||
|
showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800); |
||||
|
_next_refresh = 0; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void UITask::toggleBuzzer() { |
||||
|
// Toggle buzzer quiet mode
|
||||
|
#ifdef PIN_BUZZER |
||||
|
if (buzzer.isQuiet()) { |
||||
|
buzzer.quiet(false); |
||||
|
notify(UIEventType::ack); |
||||
|
} else { |
||||
|
buzzer.quiet(true); |
||||
|
} |
||||
|
_node_prefs->buzzer_quiet = buzzer.isQuiet(); |
||||
|
the_mesh.savePrefs(); |
||||
|
showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800); |
||||
|
_next_refresh = 0; // trigger refresh
|
||||
|
#endif |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include <MeshCore.h> |
||||
|
#include <helpers/ui/DisplayDriver.h> |
||||
|
#include <helpers/ui/UIScreen.h> |
||||
|
#include <helpers/SensorManager.h> |
||||
|
#include <helpers/BaseSerialInterface.h> |
||||
|
#include <Arduino.h> |
||||
|
#include <helpers/sensors/LPPDataHelpers.h> |
||||
|
|
||||
|
#include "ScrollingStatusBar.h" |
||||
|
|
||||
|
|
||||
|
#ifndef LED_STATE_ON |
||||
|
#define LED_STATE_ON 1 |
||||
|
#endif |
||||
|
|
||||
|
#ifdef PIN_BUZZER |
||||
|
#include <helpers/ui/buzzer.h> |
||||
|
#endif |
||||
|
#ifdef PIN_VIBRATION |
||||
|
#include <helpers/ui/GenericVibration.h> |
||||
|
#endif |
||||
|
|
||||
|
#include "../AbstractUITask.h" |
||||
|
#include "../NodePrefs.h" |
||||
|
|
||||
|
class UITask : public AbstractUITask { |
||||
|
DisplayDriver* _display; |
||||
|
SensorManager* _sensors; |
||||
|
ScrollingStatusBar _statusBar; |
||||
|
#ifdef PIN_BUZZER |
||||
|
genericBuzzer buzzer; |
||||
|
#endif |
||||
|
#ifdef PIN_VIBRATION |
||||
|
GenericVibration vibration; |
||||
|
#endif |
||||
|
unsigned long _next_refresh, _auto_off; |
||||
|
NodePrefs* _node_prefs; |
||||
|
char _alert[80]; |
||||
|
unsigned long _alert_expiry; |
||||
|
int _msgcount; |
||||
|
unsigned long ui_started_at, next_batt_chck; |
||||
|
int next_backlight_btn_check = 0; |
||||
|
uint16_t _cached_batt_mv; |
||||
|
#ifdef PIN_STATUS_LED |
||||
|
int led_state = 0; |
||||
|
int next_led_change = 0; |
||||
|
int last_led_increment = 0; |
||||
|
#endif |
||||
|
|
||||
|
#ifdef PIN_USER_BTN_ANA |
||||
|
unsigned long _analogue_pin_read_millis = millis(); |
||||
|
#endif |
||||
|
|
||||
|
UIScreen* splash; |
||||
|
UIScreen* home; |
||||
|
// UIScreen* msg_preview;
|
||||
|
UIScreen* curr; |
||||
|
|
||||
|
|
||||
|
void userLedHandler(); |
||||
|
|
||||
|
// Button action handlers
|
||||
|
char checkDisplayOn(char c); |
||||
|
char handleLongPress(char c); |
||||
|
char handleDoubleClick(char c); |
||||
|
char handleTripleClick(char c); |
||||
|
|
||||
|
void setCurrScreen(UIScreen* c); |
||||
|
|
||||
|
public: |
||||
|
|
||||
|
UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { |
||||
|
next_batt_chck = _next_refresh = 0; |
||||
|
_cached_batt_mv = 0; |
||||
|
ui_started_at = 0; |
||||
|
curr = NULL; |
||||
|
} |
||||
|
void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); |
||||
|
|
||||
|
void gotoHomeScreen() { setCurrScreen(home); } |
||||
|
void showAlert(const char* text, int duration_millis); |
||||
|
int getMsgCount() const { return _msgcount; } |
||||
|
uint16_t getCachedBattMV() const { return _cached_batt_mv; } |
||||
|
bool hasDisplay() const { return _display != NULL; } |
||||
|
bool isButtonPressed() const; |
||||
|
|
||||
|
bool isBuzzerQuiet() { |
||||
|
#ifdef PIN_BUZZER |
||||
|
return buzzer.isQuiet(); |
||||
|
#else |
||||
|
return true; |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
void toggleBuzzer(); |
||||
|
bool getGPSState(); |
||||
|
void toggleGPS(); |
||||
|
|
||||
|
|
||||
|
// from AbstractUITask
|
||||
|
void msgRead(int msgcount) override; |
||||
|
void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; |
||||
|
void notify(UIEventType t = UIEventType::none) override; |
||||
|
void loop() override; |
||||
|
|
||||
|
void shutdown(bool restart = false); |
||||
|
}; |
||||
@ -0,0 +1,104 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include <stdint.h> |
||||
|
// icons converted for use with U8g2 which needs a different format of xbm data.
|
||||
|
|
||||
|
// 'meshcore', 72x36px
|
||||
|
static const uint8_t meshcore_logo [] = { |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
|
0x00, 0x00, 0x00, 0xf0, 0x00, 0x3e, 0xfe, 0x3f, 0xfe, 0x3f, 0x1e, 0x78, |
||||
|
0xf8, 0x00, 0x1f, 0xff, 0x3f, 0xff, 0x3f, 0x1e, 0x78, 0xf8, 0x01, 0x1f, |
||||
|
0xff, 0x9f, 0xff, 0x1f, 0x0e, 0x78, 0xf8, 0x81, 0x1f, 0x0f, 0x80, 0x07, |
||||
|
0x00, 0x0f, 0x38, 0xf8, 0xc1, 0x1f, 0x0f, 0x80, 0x07, 0x00, 0x0f, 0x3c, |
||||
|
0xf8, 0xc3, 0x1f, 0xff, 0x87, 0xff, 0x07, 0xff, 0x3f, 0xf8, 0xe3, 0x1f, |
||||
|
0xff, 0x87, 0xff, 0x0f, 0xff, 0x3f, 0xfc, 0xf3, 0x8f, 0xff, 0x07, 0xff, |
||||
|
0x1f, 0xff, 0x3f, 0xfc, 0xf3, 0x8f, 0x07, 0x00, 0x00, 0x9e, 0x0f, 0x1e, |
||||
|
0xbc, 0x7f, 0x8f, 0x07, 0x00, 0x00, 0x9e, 0x07, 0x1e, 0x9c, 0x3f, 0x8f, |
||||
|
0x07, 0x00, 0x00, 0x9f, 0x07, 0x1e, 0x9c, 0x3f, 0x8f, 0xff, 0xcf, 0xff, |
||||
|
0x8f, 0x07, 0x1e, 0x1e, 0x1f, 0xc7, 0xff, 0xcf, 0xff, 0x87, 0x07, 0x1e, |
||||
|
0x1e, 0x0f, 0xc7, 0xff, 0xc7, 0xff, 0x83, 0x03, 0x0e, 0x00, 0x00, 0x00, |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
|
0xc0, 0xff, 0xf0, 0xff, 0xe0, 0xff, 0xc1, 0xff, 0x07, 0xf0, 0xff, 0xf8, |
||||
|
0xff, 0xf1, 0xff, 0xc3, 0xff, 0x07, 0xf0, 0xff, 0xfc, 0xff, 0xf1, 0xff, |
||||
|
0xc7, 0xff, 0x07, 0x78, 0x00, 0x3c, 0xe0, 0xf1, 0xc0, 0xe7, 0x01, 0x00, |
||||
|
0x78, 0x00, 0x1e, 0xe0, 0xf1, 0x80, 0xe7, 0x01, 0x00, 0x78, 0x00, 0x1e, |
||||
|
0xe0, 0xf1, 0xc0, 0xe3, 0x01, 0x00, 0x78, 0x00, 0x1e, 0xe0, 0x71, 0xc0, |
||||
|
0xe3, 0xff, 0x00, 0x3c, 0x00, 0x1e, 0xe0, 0xf9, 0xff, 0xe3, 0xff, 0x00, |
||||
|
0x3c, 0x00, 0x1e, 0xe0, 0xf8, 0xff, 0xf1, 0xff, 0x00, 0x3c, 0x00, 0x0e, |
||||
|
0xf0, 0xf8, 0xff, 0xf0, 0x00, 0x00, 0x3c, 0x00, 0x1f, 0xf0, 0x78, 0x7c, |
||||
|
0xf0, 0x00, 0x00, 0xfc, 0x3f, 0xff, 0xff, 0x38, 0xf8, 0xf0, 0xff, 0x01, |
||||
|
0xfc, 0x3f, 0xfe, 0x7f, 0x3c, 0xf0, 0xf0, 0xff, 0x01, 0xf8, 0x3f, 0xfe, |
||||
|
0x3f, 0x3c, 0xf0, 0xf1, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 |
||||
|
|
||||
|
}; |
||||
|
|
||||
|
|
||||
|
// bluetooth on icon, 32x32px, horizontal
|
||||
|
static const uint8_t bluetooth_on[] = { |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
|
0x00, 0x0c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, 0x00, 0x7c, 0x00, 0x00, |
||||
|
0x00, 0xfc, 0x01, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0xdc, 0x07, 0x00, |
||||
|
0x0c, 0x1c, 0x1f, 0x00, 0x3c, 0x1c, 0x3e, 0x00, 0x7c, 0x1c, 0x3e, 0x00, |
||||
|
0xf8, 0x1d, 0x1f, 0x0e, 0xe0, 0x9f, 0x0f, 0x1e, 0xc0, 0xff, 0x03, 0x1e, |
||||
|
0x00, 0xff, 0x01, 0x3c, 0x00, 0xfe, 0xe0, 0x38, 0x00, 0x7e, 0xe0, 0x38, |
||||
|
0xc0, 0xff, 0x41, 0x38, 0xc0, 0xff, 0x03, 0x1e, 0xe0, 0xdf, 0x07, 0x1e, |
||||
|
0xf0, 0x1d, 0x1f, 0x0e, 0x7c, 0x1c, 0x3e, 0x00, 0x3c, 0x1c, 0x3e, 0x00, |
||||
|
0x1c, 0x1c, 0x1f, 0x00, 0x00, 0x9c, 0x0f, 0x00, 0x00, 0xfc, 0x03, 0x00, |
||||
|
0x00, 0xfc, 0x01, 0x00, 0x00, 0x7c, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x00, |
||||
|
0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; |
||||
|
|
||||
|
|
||||
|
// bluetooth off icon, 32x32px, horizontal
|
||||
|
static const uint8_t bluetooth_off[] = { |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x01, 0x00, |
||||
|
0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x1c, 0xc0, 0x1f, 0x00, |
||||
|
0x3c, 0xc0, 0x3f, 0x00, 0x7c, 0xc0, 0xfd, 0x00, 0xf0, 0xc1, 0xf1, 0x01, |
||||
|
0xe0, 0xc3, 0xe1, 0x03, 0xc0, 0x0f, 0xc0, 0x03, 0x00, 0x1f, 0xf0, 0x01, |
||||
|
0x00, 0x3e, 0xf0, 0x00, 0x00, 0xf8, 0x70, 0x00, 0x00, 0xf0, 0x01, 0x00, |
||||
|
0x00, 0xe0, 0x07, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0xf0, 0x1f, 0x00, |
||||
|
0x00, 0xfc, 0x7d, 0x00, 0x00, 0xfe, 0xf9, 0x00, 0x00, 0xdf, 0xf1, 0x03, |
||||
|
0xc0, 0xc7, 0xc1, 0x07, 0xc0, 0xc3, 0xe1, 0x0f, 0xc0, 0xc1, 0xf1, 0x3f, |
||||
|
0x00, 0xc0, 0xfd, 0x3c, 0x00, 0xc0, 0x3f, 0x38, 0x00, 0xc0, 0x1f, 0x00, |
||||
|
0x00, 0xc0, 0x07, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x01, 0x00, |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; |
||||
|
|
||||
|
|
||||
|
// power icon, 32x32px, horizontal
|
||||
|
static const uint8_t power_icon[] = { |
||||
|
0x00, 0x80, 0x01, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0xc0, 0x03, 0x00, |
||||
|
0x00, 0xcc, 0x33, 0x00, 0x00, 0xcf, 0xf3, 0x00, 0x80, 0xcf, 0xf3, 0x01, |
||||
|
0xc0, 0xcf, 0xf3, 0x03, 0xe0, 0xcf, 0xf3, 0x07, 0xf0, 0xc7, 0xe3, 0x0f, |
||||
|
0xf8, 0xc3, 0xc3, 0x1f, 0xf8, 0xc1, 0x83, 0x1f, 0xfc, 0xc0, 0x03, 0x3f, |
||||
|
0x7c, 0xc0, 0x03, 0x3e, 0x7c, 0xc0, 0x03, 0x3e, 0x7e, 0x80, 0x01, 0x7e, |
||||
|
0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c, |
||||
|
0x3e, 0x00, 0x00, 0x7c, 0x3e, 0x00, 0x00, 0x7c, 0x7c, 0x00, 0x00, 0x3e, |
||||
|
0x7c, 0x00, 0x00, 0x3e, 0xfc, 0x00, 0x00, 0x3f, 0xf8, 0x01, 0x80, 0x1f, |
||||
|
0xf8, 0x03, 0xc0, 0x1f, 0xf0, 0x07, 0xe0, 0x0f, 0xf0, 0x1f, 0xf8, 0x0f, |
||||
|
0xe0, 0xff, 0xff, 0x07, 0xc0, 0xff, 0xff, 0x03, 0x00, 0xff, 0xff, 0x00, |
||||
|
0x00, 0xfc, 0x3f, 0x00, 0x00, 0xf0, 0x0f, 0x00 }; |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
// 'advert', 32x32px, horizontal
|
||||
|
static const uint8_t advert_icon[] = { |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x0c, |
||||
|
0x38, 0x00, 0x00, 0x1c, 0x18, 0x00, 0x00, 0x18, 0x0c, 0x00, 0x00, 0x30, |
||||
|
0x0c, 0x06, 0x60, 0x30, 0x06, 0x07, 0xe0, 0x60, 0x86, 0x03, 0xc0, 0x61, |
||||
|
0x87, 0x81, 0x81, 0xe1, 0xc3, 0xe0, 0x07, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3, |
||||
|
0xc3, 0xf0, 0x0f, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3, 0xc3, 0xf0, 0x0f, 0xc3, |
||||
|
0xc3, 0xe0, 0x07, 0xc3, 0x83, 0xc1, 0x83, 0xc1, 0x86, 0x01, 0x80, 0x61, |
||||
|
0x06, 0x03, 0xc0, 0x60, 0x0e, 0x07, 0xe0, 0x70, 0x0c, 0x02, 0x40, 0x30, |
||||
|
0x1c, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0c, |
||||
|
0x20, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; |
||||
|
|
||||
|
|
||||
|
|
||||
|
// 'muted, 8x8px, horizontal
|
||||
|
static const uint8_t muted_icon[] = { |
||||
|
0x20, 0x6a, 0xea, 0xe4, 0xe4, 0xea, 0x6a, 0x20 }; |
||||
@ -0,0 +1,127 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include "DisplayDriver.h" |
||||
|
#include <U8g2lib.h> |
||||
|
#include <Wire.h> |
||||
|
|
||||
|
#ifndef DISPLAY_ADDRESS |
||||
|
#define DISPLAY_ADDRESS 0x3C |
||||
|
#endif |
||||
|
|
||||
|
#ifndef OLED_WIDTH |
||||
|
#define OLED_WIDTH 72 |
||||
|
#endif |
||||
|
|
||||
|
#ifndef OLED_HEIGHT |
||||
|
#define OLED_HEIGHT 40 |
||||
|
#endif |
||||
|
|
||||
|
class U8g2Display : public DisplayDriver { |
||||
|
// U8g2 constructor for SSD1306/SSD1315 72×40 panel — handles all
|
||||
|
// GDDRAM column/page offsets, SETMULTIPLEX, SETDISPLAYOFFSET internally
|
||||
|
U8G2_SSD1306_72X40_ER_F_HW_I2C _u8g2; |
||||
|
bool _isOn; |
||||
|
uint8_t _drawColor; |
||||
|
|
||||
|
// Font metrics for current font (cached on setTextSize)
|
||||
|
uint8_t _fontAscent; |
||||
|
uint8_t _fontHeight; |
||||
|
|
||||
|
void applyFont(int sz) { |
||||
|
if (sz >= 2) { |
||||
|
_u8g2.setFont(u8g2_font_6x10_mr); // slightly larger font for better readability. TODO: more font sizes?
|
||||
|
} else { |
||||
|
_u8g2.setFont(u8g2_font_5x7_mr); |
||||
|
} |
||||
|
_fontAscent = _u8g2.getAscent(); |
||||
|
_fontHeight = _u8g2.getAscent() - _u8g2.getDescent(); |
||||
|
} |
||||
|
|
||||
|
public: |
||||
|
U8g2Display() : DisplayDriver(OLED_WIDTH, OLED_HEIGHT), |
||||
|
_u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE), |
||||
|
_isOn(false), _drawColor(1), _fontAscent(5), _fontHeight(6) {} |
||||
|
|
||||
|
bool begin() { |
||||
|
// Wire must already be initialised by board.begin() before this is called
|
||||
|
bool ok = _u8g2.begin(); |
||||
|
if (ok) { |
||||
|
_u8g2.setI2CAddress(DISPLAY_ADDRESS * 2); // U8g2 uses 8-bit address
|
||||
|
_u8g2.setFontPosTop(); // y coordinate = top of text, not baseline
|
||||
|
_u8g2.setFontMode(1); // transparent background
|
||||
|
applyFont(1); // default to compact font
|
||||
|
_isOn = true; |
||||
|
} |
||||
|
return ok; |
||||
|
} |
||||
|
|
||||
|
bool isOn() override { return _isOn; } |
||||
|
|
||||
|
void turnOn() override { |
||||
|
_u8g2.setPowerSave(0); |
||||
|
_isOn = true; |
||||
|
} |
||||
|
|
||||
|
void turnOff() override { |
||||
|
_u8g2.setPowerSave(1); |
||||
|
_isOn = false; |
||||
|
} |
||||
|
|
||||
|
void clear() override { |
||||
|
_u8g2.clearBuffer(); |
||||
|
_u8g2.sendBuffer(); |
||||
|
} |
||||
|
|
||||
|
void startFrame(Color bkg = DARK) override { |
||||
|
_u8g2.clearBuffer(); |
||||
|
_drawColor = 1; |
||||
|
_u8g2.setDrawColor(1); |
||||
|
applyFont(1); |
||||
|
} |
||||
|
|
||||
|
void setTextSize(int sz) override { |
||||
|
applyFont(sz); |
||||
|
} |
||||
|
|
||||
|
void setColor(Color c) override { |
||||
|
_drawColor = (c != DARK) ? 1 : 0; |
||||
|
_u8g2.setDrawColor(_drawColor); |
||||
|
} |
||||
|
|
||||
|
void setCursor(int x, int y) override { |
||||
|
_cursorX = x; |
||||
|
_cursorY = y; |
||||
|
} |
||||
|
|
||||
|
void print(const char* str) override { |
||||
|
_u8g2.setDrawColor(_drawColor); |
||||
|
_u8g2.drawStr(_cursorX, _cursorY, str); |
||||
|
} |
||||
|
|
||||
|
void fillRect(int x, int y, int w, int h) override { |
||||
|
_u8g2.setDrawColor(_drawColor); |
||||
|
_u8g2.drawBox(x, y, w, h); |
||||
|
} |
||||
|
|
||||
|
void drawRect(int x, int y, int w, int h) override { |
||||
|
_u8g2.setDrawColor(_drawColor); |
||||
|
_u8g2.drawFrame(x, y, w, h); |
||||
|
} |
||||
|
|
||||
|
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { |
||||
|
_u8g2.setDrawColor(1); |
||||
|
_u8g2.drawXBM(x, y, w, h, bits); |
||||
|
} |
||||
|
|
||||
|
uint16_t getTextWidth(const char* str) override { |
||||
|
return _u8g2.getStrWidth(str); |
||||
|
} |
||||
|
|
||||
|
void endFrame() override { |
||||
|
_u8g2.sendBuffer(); |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
int _cursorX = 0; |
||||
|
int _cursorY = 0; |
||||
|
}; |
||||
@ -0,0 +1,97 @@ |
|||||
|
#include <Arduino.h> |
||||
|
#include <Wire.h> |
||||
|
|
||||
|
|
||||
|
#include "TechoCardBoard.h" |
||||
|
|
||||
|
#ifdef LILYGO_TECHO_CARD |
||||
|
|
||||
|
Adafruit_NeoPixel Led_A(1, WS2812_DATA_2, NEO_GRB + NEO_KHZ800); |
||||
|
Adafruit_NeoPixel Led_B(1, WS2812_DATA_3, NEO_GRB + NEO_KHZ800); |
||||
|
Adafruit_NeoPixel Led_C(1, WS2812_DATA_1, NEO_GRB + NEO_KHZ800); |
||||
|
|
||||
|
Adafruit_NeoPixel *Led[] = |
||||
|
{ |
||||
|
&Led_A, |
||||
|
&Led_B, |
||||
|
&Led_C, |
||||
|
}; |
||||
|
|
||||
|
|
||||
|
void TechoCardBoard::begin() { |
||||
|
NRF52BoardDCDC::begin(); |
||||
|
Wire.begin(); |
||||
|
|
||||
|
for (uint8_t i = 0; i < sizeof(Led) / sizeof(Led[0]); i++) |
||||
|
{ |
||||
|
Led[i]->begin(); |
||||
|
delay(3); // allow the LEDs to initialise, otherwise they can get stuck
|
||||
|
Led[i]->setPixelColor(0, Led[i]->Color(0, 0, 0)); |
||||
|
Led[i]->show(); |
||||
|
} |
||||
|
|
||||
|
// put IMU20948 to sleep
|
||||
|
// see https://product.tdk.com/system/files/dam/doc/product/sensor/mortion-inertial/imu/data_sheet/ds-000189-icm-20948-v1.5.pdf
|
||||
|
Wire.beginTransmission(0x68); |
||||
|
Wire.write(0x06); // PWR_MGMT_1 register
|
||||
|
Wire.write(0x40); // set SLEEP bit
|
||||
|
Wire.endTransmission(); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
uint16_t TechoCardBoard::getBattMilliVolts() { |
||||
|
int adcvalue = 0; |
||||
|
|
||||
|
analogReference(AR_INTERNAL_3_0); |
||||
|
analogReadResolution(12); |
||||
|
|
||||
|
digitalWrite(PIN_BAT_CTL, HIGH); // enable vbat vdiv
|
||||
|
delay(10); |
||||
|
|
||||
|
// ADC range is 0..3000mV and resolution is 12-bit (0..4095)
|
||||
|
adcvalue = analogRead(PIN_VBAT_READ); |
||||
|
digitalWrite(PIN_BAT_CTL, LOW); |
||||
|
|
||||
|
// Convert the raw value to compensated mv, taking the resistor-
|
||||
|
// divider into account (providing the actual LIPO voltage)
|
||||
|
return (uint16_t)((float)adcvalue * REAL_VBAT_MV_PER_LSB); |
||||
|
} |
||||
|
|
||||
|
void TechoCardBoard::onBeforeTransmit() { |
||||
|
Led_A.setPixelColor(0, 20, 20, 20); // turn TX LED on
|
||||
|
Led_A.show(); |
||||
|
} |
||||
|
|
||||
|
void TechoCardBoard::onAfterTransmit() { |
||||
|
Led_A.setPixelColor(0, 0, 0, 0); // turn TX LED off
|
||||
|
Led_A.show(); |
||||
|
} |
||||
|
|
||||
|
void TechoCardBoard::toggleTorch() { |
||||
|
if (!_torchStatus) { |
||||
|
Led_C.setPixelColor(0, 255, 255, 255); |
||||
|
Led_C.show(); |
||||
|
_torchStatus = true; |
||||
|
} else { |
||||
|
Led_C.setPixelColor(0, 0, 0, 0); |
||||
|
Led_C.show(); |
||||
|
_torchStatus = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void TechoCardBoard::turnOffLeds() { |
||||
|
for (uint8_t i = 0; i < sizeof(Led) / sizeof(*Led); i++) |
||||
|
{ |
||||
|
Led[i]->setPixelColor(0, 0, 0, 0); |
||||
|
Led[i]->show(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
void TechoCardBoard::powerOff() { |
||||
|
nrf_gpio_cfg_sense_input(BUTTON_PIN, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); |
||||
|
turnOffLeds(); |
||||
|
digitalWrite(PIN_PWR_EN, LOW); |
||||
|
sd_power_system_off(); |
||||
|
} |
||||
|
|
||||
|
#endif |
||||
@ -0,0 +1,36 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#include <MeshCore.h> |
||||
|
#include <Arduino.h> |
||||
|
#include <helpers/NRF52Board.h> |
||||
|
#include <Adafruit_Neopixel.h> |
||||
|
|
||||
|
// built-ins
|
||||
|
#define VBAT_MV_PER_LSB (0.73242188F) // 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096
|
||||
|
|
||||
|
#define VBAT_DIVIDER (0.5F) // Even voltage divider on VBAT
|
||||
|
#define VBAT_DIVIDER_COMP (2.0F) // Compensation factor for the VBAT divider
|
||||
|
|
||||
|
#define PIN_VBAT_READ (2) |
||||
|
#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) |
||||
|
|
||||
|
class TechoCardBoard : public NRF52BoardDCDC { |
||||
|
bool _torchStatus = false; |
||||
|
public: |
||||
|
TechoCardBoard() : NRF52Board("TECHO_OTA") {} |
||||
|
void begin(); |
||||
|
uint16_t getBattMilliVolts() override; |
||||
|
void onBeforeTransmit(void) override; |
||||
|
void onAfterTransmit(void) override; |
||||
|
|
||||
|
|
||||
|
const char* getManufacturerName() const override { |
||||
|
return "LilyGo T-Echo Card"; |
||||
|
} |
||||
|
|
||||
|
void powerOff() override; |
||||
|
|
||||
|
void toggleTorch(); |
||||
|
void turnOffLeds(); |
||||
|
|
||||
|
}; |
||||
@ -0,0 +1,119 @@ |
|||||
|
[LilyGo_T-Echo_Card] |
||||
|
extends = nrf52_base |
||||
|
board = t-echo |
||||
|
board_build.ldscript = boards/nrf52840_s140_v6.ld |
||||
|
build_flags = ${nrf52_base.build_flags} |
||||
|
-I variants/lilygo_techo_card |
||||
|
-I src/helpers/nrf52 |
||||
|
-I lib/nrf52/s140_nrf52_6.1.1_API/include |
||||
|
-I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 |
||||
|
-D LILYGO_TECHO_CARD |
||||
|
-D RADIO_CLASS=CustomSX1262 |
||||
|
-D WRAPPER_CLASS=CustomSX1262Wrapper |
||||
|
-D LORA_TX_POWER=22 |
||||
|
-D SX126X_CURRENT_LIMIT=140 |
||||
|
-D SX126X_RX_BOOSTED_GAIN=1 |
||||
|
-D HAS_NEOPIXEL=1 |
||||
|
-D HAS_TORCH=1 |
||||
|
-D DISABLE_DIAGNOSTIC_OUTPUT |
||||
|
-D ENV_INCLUDE_GPS=1 |
||||
|
-D DISPLAY_CLASS=U8g2Display |
||||
|
-D PIN_OLED_RESET=-1 |
||||
|
build_src_filter = ${nrf52_base.build_src_filter} |
||||
|
+<helpers/*.cpp> |
||||
|
+<TechoCardBoard.cpp> |
||||
|
+<helpers/sensors/EnvironmentSensorManager.cpp> |
||||
|
+<helpers/ui/U8g2Display.h> |
||||
|
+<helpers/ui/MomentaryButton.cpp> |
||||
|
+<../variants/lilygo_techo_card> |
||||
|
lib_deps = |
||||
|
${nrf52_base.lib_deps} |
||||
|
stevemarple/MicroNMEA @ ^2.0.6 |
||||
|
olikraus/U8g2 @ ^2.35.19 |
||||
|
adafruit/Adafruit NeoPixel@^1.10.0 |
||||
|
bakercp/CRC32 @ ^2.0.0 |
||||
|
debug_tool = jlink |
||||
|
upload_protocol = nrfutil |
||||
|
|
||||
|
[env:LilyGo_T-Echo_Card_repeater] |
||||
|
extends = LilyGo_T-Echo_Card |
||||
|
build_src_filter = ${LilyGo_T-Echo_Card.build_src_filter} |
||||
|
+<../examples/simple_repeater> |
||||
|
build_flags = |
||||
|
${LilyGo_T-Echo_Card.build_flags} |
||||
|
-D ADVERT_NAME='"T-Echo Card Repeater"' |
||||
|
-D ADVERT_LAT=0.0 |
||||
|
-D ADVERT_LON=0.0 |
||||
|
-D ADMIN_PASSWORD='"password"' |
||||
|
-D MAX_NEIGHBOURS=50 |
||||
|
; -D MESH_PACKET_LOGGING=1 |
||||
|
; -D MESH_DEBUG=1 |
||||
|
|
||||
|
[env:LilyGo_T-Echo_Card_room_server] |
||||
|
extends = LilyGo_T-Echo_Card |
||||
|
build_src_filter = ${LilyGo_T-Echo_Card.build_src_filter} |
||||
|
+<../examples/simple_room_server> |
||||
|
build_flags = |
||||
|
${LilyGo_T-Echo_Card.build_flags} |
||||
|
-D ADVERT_NAME='"T-Echo Card Room"' |
||||
|
-D ADVERT_LAT=0.0 |
||||
|
-D ADVERT_LON=0.0 |
||||
|
-D ADMIN_PASSWORD='"password"' |
||||
|
; -D MESH_PACKET_LOGGING=1 |
||||
|
; -D MESH_DEBUG=1 |
||||
|
|
||||
|
[env:LilyGo_T-Echo_Card_companion_radio_ble] |
||||
|
extends = LilyGo_T-Echo_Card |
||||
|
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld |
||||
|
board_upload.maximum_size = 712704 |
||||
|
build_flags = |
||||
|
${LilyGo_T-Echo_Card.build_flags} |
||||
|
-I src/helpers/ui |
||||
|
-I examples/companion_radio/ui-tiny |
||||
|
-D PIN_BUZZER=38 |
||||
|
-D QSPIFLASH=1 |
||||
|
-D MAX_CONTACTS=350 |
||||
|
-D MAX_GROUP_CHANNELS=40 |
||||
|
-D BLE_PIN_CODE=123456 |
||||
|
; -D BLE_DEBUG_LOGGING=1 |
||||
|
-D OFFLINE_QUEUE_SIZE=256 |
||||
|
-D UI_RECENT_LIST_SIZE=3 |
||||
|
-D UI_GPS_PAGE=1 |
||||
|
; -D MESH_PACKET_LOGGING=1 |
||||
|
; -D MESH_DEBUG=1 |
||||
|
-D AUTO_SHUTDOWN_MILLIVOLTS=3300 |
||||
|
build_src_filter = ${LilyGo_T-Echo_Card.build_src_filter} |
||||
|
+<helpers/nrf52/SerialBLEInterface.cpp> |
||||
|
+<../examples/companion_radio/*.cpp> |
||||
|
+<../examples/companion_radio/ui-tiny/*.cpp> |
||||
|
+<helpers/ui/buzzer.cpp> |
||||
|
lib_deps = |
||||
|
${LilyGo_T-Echo_Card.lib_deps} |
||||
|
end2endzone/NonBlockingRTTTL@^1.3.0 |
||||
|
densaugeo/base64 @ ~1.4.0 |
||||
|
|
||||
|
[env:LilyGo_T-Echo_Card_companion_radio_usb] |
||||
|
extends = LilyGo_T-Echo_Card |
||||
|
board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld |
||||
|
board_upload.maximum_size = 712704 |
||||
|
build_flags = |
||||
|
${LilyGo_T-Echo_Card.build_flags} |
||||
|
-I src/helpers/ui |
||||
|
-I examples/companion_radio/ui-tiny |
||||
|
-D PIN_BUZZER=38 |
||||
|
-D QSPIFLASH=1 |
||||
|
-D MAX_CONTACTS=350 |
||||
|
-D MAX_GROUP_CHANNELS=40 |
||||
|
-D OFFLINE_QUEUE_SIZE=256 |
||||
|
-D UI_RECENT_LIST_SIZE=3 |
||||
|
-D UI_GPS_PAGE=1 |
||||
|
; -D MESH_PACKET_LOGGING=1 |
||||
|
; -D MESH_DEBUG=1 |
||||
|
-D AUTO_SHUTDOWN_MILLIVOLTS=3300 |
||||
|
build_src_filter = ${LilyGo_T-Echo_Card.build_src_filter} |
||||
|
+<../examples/companion_radio/*.cpp> |
||||
|
+<../examples/companion_radio/ui-tiny/*.cpp> |
||||
|
lib_deps = |
||||
|
${LilyGo_T-Echo_Card.lib_deps} |
||||
|
end2endzone/NonBlockingRTTTL@^1.3.0 |
||||
|
densaugeo/base64 @ ~1.4.0 |
||||
@ -0,0 +1,38 @@ |
|||||
|
#include <Arduino.h> |
||||
|
#include "target.h" |
||||
|
#include <helpers/ArduinoHelpers.h> |
||||
|
#include <helpers/sensors/MicroNMEALocationProvider.h> |
||||
|
|
||||
|
TechoCardBoard board; |
||||
|
|
||||
|
RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); |
||||
|
|
||||
|
WRAPPER_CLASS radio_driver(radio, board); |
||||
|
|
||||
|
VolatileRTCClock fallback_clock; |
||||
|
AutoDiscoverRTCClock rtc_clock(fallback_clock); |
||||
|
|
||||
|
#ifdef ENV_INCLUDE_GPS |
||||
|
MicroNMEALocationProvider nmea = MicroNMEALocationProvider(Serial1, &rtc_clock); |
||||
|
EnvironmentSensorManager sensors = EnvironmentSensorManager(nmea); |
||||
|
#else |
||||
|
EnvironmentSensorManager sensors = EnvironmentSensorManager(); |
||||
|
#endif |
||||
|
|
||||
|
|
||||
|
#ifdef DISPLAY_CLASS |
||||
|
DISPLAY_CLASS display; |
||||
|
MomentaryButton user_btn(PIN_USER_BTN, 1000, true); |
||||
|
MomentaryButton back_btn(PIN_BUTTON2, 1000, true); |
||||
|
#endif |
||||
|
|
||||
|
bool radio_init() { |
||||
|
rtc_clock.begin(Wire); |
||||
|
|
||||
|
return radio.std_init(&SPI); |
||||
|
} |
||||
|
|
||||
|
mesh::LocalIdentity radio_new_identity() { |
||||
|
RadioNoiseListener rng(radio); |
||||
|
return mesh::LocalIdentity(&rng); // create new random identity
|
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
#pragma once |
||||
|
|
||||
|
#define RADIOLIB_STATIC_ONLY 1 |
||||
|
#include <RadioLib.h> |
||||
|
#include <helpers/radiolib/RadioLibWrappers.h> |
||||
|
#include <TechoCardBoard.h> |
||||
|
#include <helpers/radiolib/CustomSX1262Wrapper.h> |
||||
|
#include <helpers/AutoDiscoverRTCClock.h> |
||||
|
#include <helpers/SensorManager.h> |
||||
|
#include <helpers/sensors/EnvironmentSensorManager.h> |
||||
|
#include <helpers/sensors/LocationProvider.h> |
||||
|
#ifdef DISPLAY_CLASS |
||||
|
#include <helpers/ui/U8g2Display.h> |
||||
|
#include <helpers/ui/MomentaryButton.h> |
||||
|
#endif |
||||
|
|
||||
|
extern TechoCardBoard board; |
||||
|
extern WRAPPER_CLASS radio_driver; |
||||
|
extern AutoDiscoverRTCClock rtc_clock; |
||||
|
extern EnvironmentSensorManager sensors; |
||||
|
|
||||
|
#ifdef DISPLAY_CLASS |
||||
|
extern DISPLAY_CLASS display; |
||||
|
extern MomentaryButton user_btn; |
||||
|
extern MomentaryButton back_btn; |
||||
|
#endif |
||||
|
|
||||
|
bool radio_init(); |
||||
|
mesh::LocalIdentity radio_new_identity(); |
||||
@ -0,0 +1,45 @@ |
|||||
|
#include "variant.h" |
||||
|
#include "wiring_constants.h" |
||||
|
#include "wiring_digital.h" |
||||
|
#include "Adafruit_NeoPixel.h" |
||||
|
|
||||
|
const int MISO = PIN_SPI_MISO; |
||||
|
const int MOSI = PIN_SPI_MOSI; |
||||
|
const int SCK = PIN_SPI_SCK; |
||||
|
|
||||
|
|
||||
|
|
||||
|
const uint32_t g_ADigitalPinMap[] = { |
||||
|
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, |
||||
|
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, |
||||
|
27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, |
||||
|
40, 41, 42, 43, 44, 45, 46, 47 |
||||
|
}; |
||||
|
|
||||
|
void initVariant() { |
||||
|
// turn on 3v3 rail
|
||||
|
pinMode(PIN_PWR_EN, OUTPUT); |
||||
|
digitalWrite(PIN_PWR_EN, HIGH); |
||||
|
|
||||
|
// VDIV enable
|
||||
|
pinMode(PIN_BAT_CTL, OUTPUT); |
||||
|
|
||||
|
// buttons
|
||||
|
pinMode(PIN_BUTTON1, INPUT_PULLUP); |
||||
|
pinMode(PIN_BUTTON2, INPUT_PULLUP); |
||||
|
|
||||
|
// speaker
|
||||
|
pinMode(SPEAKER_EN, OUTPUT); |
||||
|
digitalWrite(SPEAKER_EN, LOW); |
||||
|
pinMode(SPEAKER_EN_2, OUTPUT); |
||||
|
digitalWrite(SPEAKER_EN_2, LOW); |
||||
|
|
||||
|
// gps
|
||||
|
pinMode(PIN_GPS_STANDBY, OUTPUT); |
||||
|
digitalWrite(PIN_GPS_STANDBY, HIGH); |
||||
|
pinMode(PIN_GPS_EN, OUTPUT); |
||||
|
digitalWrite(PIN_GPS_EN, HIGH); |
||||
|
pinMode(PIN_GPS_RESET, OUTPUT); |
||||
|
digitalWrite(PIN_GPS_RESET, HIGH); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,144 @@ |
|||||
|
/*
|
||||
|
* variant.h |
||||
|
* |
||||
|
* MIT License |
||||
|
*/ |
||||
|
|
||||
|
#pragma once |
||||
|
|
||||
|
#include "WVariant.h" |
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// Low frequency clock source
|
||||
|
|
||||
|
#define USE_LFXO // 32.768 kHz crystal oscillator
|
||||
|
#define VARIANT_MCK (64000000ul) |
||||
|
|
||||
|
#define WIRE_INTERFACES_COUNT (1) |
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// Power
|
||||
|
|
||||
|
#define PIN_PWR_EN (30) // RT9080 LDO enable pin for 3v3 rail
|
||||
|
|
||||
|
#define PIN_BAT_CTL (31) // vdiv enable
|
||||
|
#define PIN_VBAT_READ (2) |
||||
|
#define ADC_MULTIPLIER (4.90F) |
||||
|
|
||||
|
#define ADC_RESOLUTION (14) |
||||
|
#define BATTERY_SENSE_RES (12) |
||||
|
#define AREF_VOLTAGE (3.0) |
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// Number of pins
|
||||
|
|
||||
|
#define PINS_COUNT (48) |
||||
|
#define NUM_DIGITAL_PINS (48) |
||||
|
#define NUM_ANALOG_INPUTS (1) |
||||
|
#define NUM_ANALOG_OUTPUTS (0) |
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// UART pin definition
|
||||
|
|
||||
|
#define PIN_SERIAL1_RX PIN_GPS_TX |
||||
|
#define PIN_SERIAL1_TX PIN_GPS_RX |
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// I2C pin definition
|
||||
|
|
||||
|
#define PIN_WIRE_SDA (36) // P1.04
|
||||
|
#define PIN_WIRE_SCL (34) // P1.02
|
||||
|
#define I2C_NO_RESCAN |
||||
|
#define DISABLE_DS3231_PROBE // DS3231 lives at 0x68 but this board has ICM20948 at that address, causing broken clock.
|
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// SPI pin definition
|
||||
|
|
||||
|
#define SPI_INTERFACES_COUNT (1) |
||||
|
|
||||
|
#define PIN_SPI_MISO (17) |
||||
|
#define PIN_SPI_MOSI (15) |
||||
|
#define PIN_SPI_SCK (13) |
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// QSPI FLASH
|
||||
|
|
||||
|
#define PIN_QSPI_SCK (4) |
||||
|
#define PIN_QSPI_CS (12) |
||||
|
#define PIN_QSPI_IO0 (6) |
||||
|
#define PIN_QSPI_IO1 (8) |
||||
|
#define PIN_QSPI_IO2 (41) // P1.09
|
||||
|
#define PIN_QSPI_IO3 (26) |
||||
|
|
||||
|
#define EXTERNAL_FLASH_DEVICES ZD25WQ32CEIGR |
||||
|
#define EXTERNAL_FLASH_USE_QSPI |
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// Builtin LEDs (only WS2812, no traditional LEDs available)
|
||||
|
|
||||
|
// WS1812 data lines
|
||||
|
#define WS2812_DATA_1 (39) // P1.07
|
||||
|
#define WS2812_DATA_2 (44) // P1.12
|
||||
|
#define WS2812_DATA_3 (28) // P0.28
|
||||
|
|
||||
|
#define LED_BLUE (-1) |
||||
|
#define LED_BUILTIN (-1) |
||||
|
#define LED_PIN LED_BUILTIN |
||||
|
#define LED_STATE_ON LOW |
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// Builtin buttons
|
||||
|
|
||||
|
#define PIN_BUTTON1 (42) // P1.10
|
||||
|
#define BUTTON_PIN PIN_BUTTON1 // BUTTON A
|
||||
|
#define PIN_USER_BTN BUTTON_PIN |
||||
|
|
||||
|
#define PIN_BUTTON2 (24) |
||||
|
#define BUTTON_PIN2 PIN_BUTTON2 // BUTTON C
|
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// Lora (Acsip S62F)
|
||||
|
|
||||
|
#define USE_SX1262 |
||||
|
#define P_LORA_SCLK PIN_SPI_SCK |
||||
|
#define P_LORA_MISO PIN_SPI_MISO |
||||
|
#define P_LORA_MOSI PIN_SPI_MOSI |
||||
|
#define P_LORA_DIO_1 (40) // P1.08
|
||||
|
#define P_LORA_RESET (7) // P0.07
|
||||
|
#define P_LORA_BUSY (14) // P0.14
|
||||
|
#define P_LORA_NSS (11) // P0.11
|
||||
|
#define SX126X_RXEN (33) // P1.01
|
||||
|
#define SX126X_TXEN (27) // P0.27
|
||||
|
#define SX126X_DIO3_TCXO_VOLTAGE (1.8f) |
||||
|
|
||||
|
|
||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||
|
// GPS
|
||||
|
|
||||
|
// NOTE: these pins are defined differently to how lilygo does them but they
|
||||
|
// seem to work properly for how EnvironmentSensorManager operates.
|
||||
|
// TODO: MAYBE? migrate to board based sensor manager / add GPS_WAKE_UP to ESM
|
||||
|
|
||||
|
#define PIN_GPS_RX (21) // GPS_UART_RX in lilygo pin defs
|
||||
|
#define PIN_GPS_TX (19) // GPS_UART_TX in lilygo pin defs
|
||||
|
#define PIN_GPS_EN (25) // GPS_WAKE_UP in lilygo pin defs
|
||||
|
#define PIN_GPS_RESET (47) // GPS_EN in lilygo pin defs
|
||||
|
#define PIN_GPS_STANDBY (29) // GPS_RF_EN in lilygo pin defs
|
||||
|
#define PIN_GPS_PPS (23) // GPS_1PPS in lilygo pin defs
|
||||
|
|
||||
|
// buzzer - enabled in platformio.ini so it can be easily turned off if not wanted.
|
||||
|
// #define PIN_BUZZER (38) // P1.06
|
||||
|
|
||||
|
// microphone
|
||||
|
#define MICROPHONE_SCLK (35) // P1.03
|
||||
|
#define MICROPHONE_DATA (37) // P1.05
|
||||
|
|
||||
|
// speaker
|
||||
|
#define SPEAKER_EN (43) // P1.11
|
||||
|
#define SPEAKER_EN_2 (3) // P0.03
|
||||
|
#define SPEAKER_BCLK (16) // P0.16
|
||||
|
#define SPEAKER_DATA (20) // P0.20
|
||||
|
#define SPEAKER_WS_LRCK (22) // P0.22
|
||||
|
|
||||
|
// ICM20948 9dof motion sensor (accelerometer and magnetometer)
|
||||
|
#define ICM20948_SDA PIN_WIRE_SDA // P1.4
|
||||
|
#define ICM20948_SCL PIN_WIRE_SCL // P1.2
|
||||
|
#define ICM20948_ADDRESS 0x68 |
||||
Loading…
Reference in new issue