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