diff --git a/README.md b/README.md index cbe76494d..c5d7b1bbf 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This software is 100% open source and developed by a group of hobbyist experimen We currently support three models of radios. - TTGO T-Beam - + - [T-Beam V0.7 w/ NEO-6M](https://www.aliexpress.com/item/4000574335430.html) - [T-Beam V1.0 w/ NEO-6M - special Meshtastic version](https://www.aliexpress.com/item/4001178678568.html) (Includes built-in OLED display and they have **preinstalled** the meshtastic software) - [T-Beam V1.0 w/ NEO-M8N](https://www.aliexpress.com/item/33047631119.html) (slightly better GPS) - 3D printable cases @@ -40,9 +40,10 @@ We currently support three models of radios. **Make sure to get the frequency for your country** -- US/JP/AU/NZ - 915MHz +- US/JP/AU/NZ/CA - 915MHz - CN - 470MHz - EU - 868MHz, 433MHz +- full list of LoRa frequencies per region is available [here](https://www.thethingsnetwork.org/docs/lorawan/frequencies-by-country.html) Getting a version that includes a screen is optional, but highly recommended. diff --git a/bin/version.sh b/bin/version.sh index 58db3c516..11037db32 100644 --- a/bin/version.sh +++ b/bin/version.sh @@ -1,3 +1,3 @@ -export VERSION=0.7.9 \ No newline at end of file +export VERSION=0.7.10 \ No newline at end of file diff --git a/docs/software/TODO.md b/docs/software/TODO.md index 60d0e5b0a..e8355bcf1 100644 --- a/docs/software/TODO.md +++ b/docs/software/TODO.md @@ -2,7 +2,7 @@ You probably don't care about this section - skip to the next one. -- implement first cut of router mode: preferentially handle flooding, and change sleep and GPS behaviors +- implement first cut of router mode: preferentially handle flooding, and change sleep and GPS behaviors (plan for geofence mode and battery save mode) - NRF52 BLE support # Medium priority @@ -48,7 +48,6 @@ Items after the first final candidate release. - split out the software update utility so other projects can use it. Have the appload specify the URL for downloads. - read the PMU battery fault indicators and blink/led/warn user on screen - discard very old nodedb records (> 1wk) -- add a watchdog timer - handle millis() rollover in GPS.getTime - otherwise we will break after 50 days - report esp32 device code bugs back to the mothership via android - change BLE bonding to something more secure. see comment by pSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND) diff --git a/docs/software/mesh-alg.md b/docs/software/mesh-alg.md index 2a2e4dd26..f9427cd21 100644 --- a/docs/software/mesh-alg.md +++ b/docs/software/mesh-alg.md @@ -32,8 +32,8 @@ optimizations / low priority: - read this [this](http://pages.cs.wisc.edu/~suman/pubs/nadv-mobihoc05.pdf) paper and others and make our naive flood routing less naive - read @cyclomies long email with good ideas on optimizations and reply -- Remove NodeNum assignment algorithm (now that we use 4 byte node nums) -- make android app warn if firmware is too old or too new to talk to +- DONE Remove NodeNum assignment algorithm (now that we use 4 byte node nums) +- DONE make android app warn if firmware is too old or too new to talk to - change nodenums and packetids in protobuf to be fixed32 - low priority: think more careful about reliable retransmit intervals - make ReliableRouter.pending threadsafe diff --git a/platformio.ini b/platformio.ini index bad647bfa..9f73a1aa1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -23,6 +23,8 @@ default_envs = tbeam ; Note: the github actions CI test build can't yet build NR [env] +framework = arduino + ; customize the partition table ; http://docs.platformio.org/en/latest/platforms/espressif32.html#partition-tables board_build.partitions = partition-table.csv @@ -72,13 +74,12 @@ lib_deps = Wire ; explicitly needed here because the AXP202 library forgets to add it https://github.com/meshtastic/arduino-fsm.git https://github.com/meshtastic/SparkFun_Ublox_Arduino_Library.git - https://github.com/meshtastic/RadioLib.git + https://github.com/meshtastic/RadioLib.git#6aa38a85856012c99c4e9b4e7cee35e37671a4bc https://github.com/meshtastic/TinyGPSPlus.git ; Common settings for ESP targes, mixin with extends = esp32_base [esp32_base] platform = espressif32 -framework = arduino src_filter = ${env.src_filter} - upload_speed = 921600 @@ -89,7 +90,7 @@ build_flags = # board_build.ldscript = linker/esp32.extram.bss.ld lib_ignore = segger_rtt platform_packages = - framework-arduinoespressif32 @ https://github.com/meshtastic/arduino-esp32.git#f26c4f96fefd13ed0ed042e27954f8aba6328f6b + framework-arduinoespressif32 @ https://github.com/meshtastic/arduino-esp32.git#71ed4002c953d8c87f44ed27e34fe0735f99013e ; The 1.0 release of the TBEAM board [env:tbeam] @@ -98,17 +99,16 @@ board = ttgo-t-beam lib_deps = ${env.lib_deps} https://github.com/meshtastic/AXP202X_Library.git - build_flags = ${esp32_base.build_flags} -D TBEAM_V10 ; The original TBEAM board without the AXP power chip and a few other changes ; Note: I've heard reports this didn't work. Disabled until someone with a 0.7 can test and debug. -;[env:tbeam0.7] -;extends = esp32_base -;board = ttgo-t-beam -;build_flags = -; ${esp32_base.build_flags} -D TBEAM_V07 +[env:tbeam0.7] +extends = esp32_base +board = ttgo-t-beam +build_flags = + ${esp32_base.build_flags} -D TBEAM_V07 [env:heltec] ;build_type = debug ; to make it possible to step through our jtag debugger @@ -133,7 +133,6 @@ build_flags = ; For more details see my post in the forum. [env:cubecellplus] platform = https://github.com/HelTecAutomation/platform-asrmicro650x.git ; we use top-of-tree because stable version has too many bugs - asrmicro650x -framework = arduino board = cubecell_board_plus ; FIXME, bug in cubecell arduino - they are supposed to set ARDUINO build_flags = ${env.build_flags} -DARDUINO=100 -Isrc/cubecell @@ -143,7 +142,6 @@ src_filter = ; Common settings for NRF52 based targets [nrf52_base] platform = nordicnrf52 -framework = arduino debug_tool = jlink build_type = debug ; I'm debugging with ICE a lot now ; note: liboberon provides the AES256 implementation for NRF52 (though not using the hardware acceleration of the NRF52840 - FIXME) diff --git a/src/GPSStatus.h b/src/GPSStatus.h new file mode 100644 index 000000000..ed9e0fbc6 --- /dev/null +++ b/src/GPSStatus.h @@ -0,0 +1,126 @@ +#pragma once +#include +#include "Status.h" +#include "configuration.h" + +namespace meshtastic { + + /// Describes the state of the GPS system. + class GPSStatus : public Status + { + + private: + CallbackObserver statusObserver = CallbackObserver(this, &GPSStatus::updateStatus); + + bool hasLock = false; // default to false, until we complete our first read + bool isConnected = false; // Do we have a GPS we are talking to + int32_t latitude = 0, longitude = 0; // as an int mult by 1e-7 to get value as double + int32_t altitude = 0; + uint32_t dop = 0; // Diminution of position; PDOP where possible (UBlox), HDOP otherwise (TinyGPS) in 10^2 units (needs scaling before use) + uint32_t heading = 0; + uint32_t numSatellites = 0; + + public: + + GPSStatus() { + statusType = STATUS_TYPE_GPS; + } + GPSStatus( bool hasLock, bool isConnected, int32_t latitude, int32_t longitude, int32_t altitude, uint32_t dop, uint32_t heading, uint32_t numSatellites ) : Status() + { + this->hasLock = hasLock; + this->isConnected = isConnected; + this->latitude = latitude; + this->longitude = longitude; + this->altitude = altitude; + this->dop = dop; + this->heading = heading; + this->numSatellites = numSatellites; + } + GPSStatus(const GPSStatus &); + GPSStatus &operator=(const GPSStatus &); + + void observe(Observable *source) + { + statusObserver.observe(source); + } + + bool getHasLock() const + { + return hasLock; + } + + bool getIsConnected() const + { + return isConnected; + } + + int32_t getLatitude() const + { + return latitude; + } + + int32_t getLongitude() const + { + return longitude; + } + + int32_t getAltitude() const + { + return altitude; + } + + uint32_t getDOP() const + { + return dop; + } + + uint32_t getHeading() const + { + return heading; + } + + uint32_t getNumSatellites() const + { + return numSatellites; + } + + bool matches(const GPSStatus *newStatus) const + { + return ( + newStatus->hasLock != hasLock || + newStatus->isConnected != isConnected || + newStatus->latitude != latitude || + newStatus->longitude != longitude || + newStatus->altitude != altitude || + newStatus->dop != dop || + newStatus->heading != heading || + newStatus->numSatellites != numSatellites + ); + } + int updateStatus(const GPSStatus *newStatus) { + // Only update the status if values have actually changed + bool isDirty; + { + isDirty = matches(newStatus); + initialized = true; + hasLock = newStatus->hasLock; + isConnected = newStatus->isConnected; + latitude = newStatus->latitude; + longitude = newStatus->longitude; + altitude = newStatus->altitude; + dop = newStatus->dop; + heading = newStatus->heading; + numSatellites = newStatus->numSatellites; + } + if(isDirty) { + DEBUG_MSG("New GPS pos lat=%f, lon=%f, alt=%d, pdop=%f, heading=%f, sats=%d\n", latitude * 1e-7, longitude * 1e-7, altitude, dop * 1e-2, heading * 1e-5, numSatellites); + onNewStatus.notifyObservers(this); + } + return 0; + } + + }; + +} + +extern meshtastic::GPSStatus *gpsStatus; \ No newline at end of file diff --git a/src/NodeStatus.h b/src/NodeStatus.h new file mode 100644 index 000000000..dc567fd2f --- /dev/null +++ b/src/NodeStatus.h @@ -0,0 +1,83 @@ +#pragma once +#include +#include "Status.h" +#include "configuration.h" + +namespace meshtastic { + + /// Describes the state of the NodeDB system. + class NodeStatus : public Status + { + + private: + CallbackObserver statusObserver = CallbackObserver(this, &NodeStatus::updateStatus); + + uint8_t numOnline = 0; + uint8_t numTotal = 0; + + uint8_t lastNumTotal = 0; + + public: + bool forceUpdate = false; + + NodeStatus() { + statusType = STATUS_TYPE_NODE; + } + NodeStatus( uint8_t numOnline, uint8_t numTotal, bool forceUpdate = false ) : Status() + { + this->forceUpdate = forceUpdate; + this->numOnline = numOnline; + this->numTotal = numTotal; + } + NodeStatus(const NodeStatus &); + NodeStatus &operator=(const NodeStatus &); + + void observe(Observable *source) + { + statusObserver.observe(source); + } + + uint8_t getNumOnline() const + { + return numOnline; + } + + uint8_t getNumTotal() const + { + return numTotal; + } + + uint8_t getLastNumTotal() const + { + return lastNumTotal; + } + + bool matches(const NodeStatus *newStatus) const + { + return ( + newStatus->getNumOnline() != numOnline || + newStatus->getNumTotal() != numTotal + ); + } + int updateStatus(const NodeStatus *newStatus) { + // Only update the status if values have actually changed + lastNumTotal = numTotal; + bool isDirty; + { + isDirty = matches(newStatus); + initialized = true; + numOnline = newStatus->getNumOnline(); + numTotal = newStatus->getNumTotal(); + } + if(isDirty || newStatus->forceUpdate) { + DEBUG_MSG("Node status update: %d online, %d total\n", numOnline, numTotal); + onNewStatus.notifyObservers(this); + } + return 0; + } + + }; + +} + +extern meshtastic::NodeStatus *nodeStatus; \ No newline at end of file diff --git a/src/Power.cpp b/src/Power.cpp new file mode 100644 index 000000000..23332698b --- /dev/null +++ b/src/Power.cpp @@ -0,0 +1,179 @@ +#include "power.h" +#include "PowerFSM.h" +#include "main.h" +#include "utils.h" +#include "sleep.h" + +#ifdef TBEAM_V10 + +// FIXME. nasty hack cleanup how we load axp192 +#undef AXP192_SLAVE_ADDRESS +#include "axp20x.h" +AXP20X_Class axp; +bool pmu_irq = false; + +Power *power; + +bool Power::setup() +{ + + axp192Init(); + concurrency::PeriodicTask::setup(); // We don't start our periodic task unless we actually found the device + setPeriod(1); + + return axp192_found; +} + +/// Reads power status to powerStatus singleton. +// +// TODO(girts): move this and other axp stuff to power.h/power.cpp. +void Power::readPowerStatus() +{ + bool hasBattery = axp.isBatteryConnect(); + int batteryVoltageMv = 0; + uint8_t batteryChargePercent = 0; + if (hasBattery) { + batteryVoltageMv = axp.getBattVoltage(); + // If the AXP192 returns a valid battery percentage, use it + if (axp.getBattPercentage() >= 0) { + batteryChargePercent = axp.getBattPercentage(); + } else { + // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error + // In that case, we compute an estimate of the charge percent based on maximum and minimum voltages defined in power.h + batteryChargePercent = clamp((int)(((batteryVoltageMv - BAT_MILLIVOLTS_EMPTY) * 1e2) / (BAT_MILLIVOLTS_FULL - BAT_MILLIVOLTS_EMPTY)), 0, 100); + } + } + + // Notify any status instances that are observing us + const meshtastic::PowerStatus powerStatus = meshtastic::PowerStatus(hasBattery, axp.isVBUSPlug(), axp.isChargeing(), batteryVoltageMv, batteryChargePercent); + newStatus.notifyObservers(&powerStatus); + + // If we have a battery at all and it is less than 10% full, force deep sleep + if (powerStatus.getHasBattery() && !powerStatus.getHasUSB() && + axp.getBattVoltage() < MIN_BAT_MILLIVOLTS) + powerFSM.trigger(EVENT_LOW_BATTERY); +} + +void Power::doTask() +{ + readPowerStatus(); + + // Only read once every 20 seconds once the power status for the app has been initialized + if(statusHandler && statusHandler->isInitialized()) + setPeriod(1000 * 20); +} +#endif // TBEAM_V10 + +#ifdef AXP192_SLAVE_ADDRESS +/** + * Init the power manager chip + * + * axp192 power + DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the axp192 + share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this on!) LDO1 + 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can + not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS + */ +void Power::axp192Init() +{ + if (axp192_found) { + if (!axp.begin(Wire, AXP192_SLAVE_ADDRESS)) { + DEBUG_MSG("AXP192 Begin PASS\n"); + + // axp.setChgLEDMode(LED_BLINK_4HZ); + DEBUG_MSG("DCDC1: %s\n", axp.isDCDC1Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("DCDC2: %s\n", axp.isDCDC2Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("LDO2: %s\n", axp.isLDO2Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("LDO3: %s\n", axp.isLDO3Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("DCDC3: %s\n", axp.isDCDC3Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("Exten: %s\n", axp.isExtenEnable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("----------------------------------------\n"); + + axp.setPowerOutPut(AXP192_LDO2, AXP202_ON); // LORA radio + axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); // GPS main power + axp.setPowerOutPut(AXP192_DCDC2, AXP202_ON); + axp.setPowerOutPut(AXP192_EXTEN, AXP202_ON); + axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); + axp.setDCDC1Voltage(3300); // for the OLED power + + DEBUG_MSG("DCDC1: %s\n", axp.isDCDC1Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("DCDC2: %s\n", axp.isDCDC2Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("LDO2: %s\n", axp.isLDO2Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("LDO3: %s\n", axp.isLDO3Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("DCDC3: %s\n", axp.isDCDC3Enable() ? "ENABLE" : "DISABLE"); + DEBUG_MSG("Exten: %s\n", axp.isExtenEnable() ? "ENABLE" : "DISABLE"); + + axp.setChargeControlCur(AXP1XX_CHARGE_CUR_1320MA); // actual limit (in HW) on the tbeam is 450mA +#if 0 + + // Not connected + //val = 0xfc; + //axp._writeByte(AXP202_VHTF_CHGSET, 1, &val); // Set temperature protection + + //not used + //val = 0x46; + //axp._writeByte(AXP202_OFF_CTL, 1, &val); // enable bat detection +#endif + axp.debugCharging(); + +#ifdef PMU_IRQ + pinMode(PMU_IRQ, INPUT); + attachInterrupt( + PMU_IRQ, [] { pmu_irq = true; }, FALLING); + + axp.adc1Enable(AXP202_BATT_CUR_ADC1, 1); + axp.enableIRQ(AXP202_BATT_REMOVED_IRQ | AXP202_BATT_CONNECT_IRQ | AXP202_CHARGING_FINISHED_IRQ | AXP202_CHARGING_IRQ | + AXP202_VBUS_REMOVED_IRQ | AXP202_VBUS_CONNECT_IRQ | AXP202_PEK_SHORTPRESS_IRQ, + 1); + + axp.clearIRQ(); +#endif + readPowerStatus(); + } else { + DEBUG_MSG("AXP192 Begin FAIL\n"); + } + } else { + DEBUG_MSG("AXP192 not found\n"); + } +} +#endif + +void Power::loop() +{ + +#ifdef PMU_IRQ + if (pmu_irq) { + pmu_irq = false; + axp.readIRQ(); + + DEBUG_MSG("pmu irq!\n"); + + if (axp.isChargingIRQ()) { + DEBUG_MSG("Battery start charging\n"); + } + if (axp.isChargingDoneIRQ()) { + DEBUG_MSG("Battery fully charged\n"); + } + if (axp.isVbusRemoveIRQ()) { + DEBUG_MSG("USB unplugged\n"); + } + if (axp.isVbusPlugInIRQ()) { + DEBUG_MSG("USB plugged In\n"); + } + if (axp.isBattPlugInIRQ()) { + DEBUG_MSG("Battery inserted\n"); + } + if (axp.isBattRemoveIRQ()) { + DEBUG_MSG("Battery removed\n"); + } + if (axp.isPEKShortPressIRQ()) { + DEBUG_MSG("PEK short button press\n"); + } + + readPowerStatus(); + axp.clearIRQ(); + } + +#endif // T_BEAM_V10 + +} diff --git a/src/PowerStatus.h b/src/PowerStatus.h new file mode 100644 index 000000000..f40d9445c --- /dev/null +++ b/src/PowerStatus.h @@ -0,0 +1,103 @@ +#pragma once +#include +#include "Status.h" +#include "configuration.h" + +namespace meshtastic { + + /// Describes the state of the GPS system. + class PowerStatus : public Status + { + + private: + CallbackObserver statusObserver = CallbackObserver(this, &PowerStatus::updateStatus); + + /// Whether we have a battery connected + bool hasBattery; + /// Battery voltage in mV, valid if haveBattery is true + int batteryVoltageMv; + /// Battery charge percentage, either read directly or estimated + uint8_t batteryChargePercent; + /// Whether USB is connected + bool hasUSB; + /// Whether we are charging the battery + bool isCharging; + + public: + + PowerStatus() { + statusType = STATUS_TYPE_POWER; + } + PowerStatus( bool hasBattery, bool hasUSB, bool isCharging, int batteryVoltageMv, uint8_t batteryChargePercent ) : Status() + { + this->hasBattery = hasBattery; + this->hasUSB = hasUSB; + this->isCharging = isCharging; + this->batteryVoltageMv = batteryVoltageMv; + this->batteryChargePercent = batteryChargePercent; + } + PowerStatus(const PowerStatus &); + PowerStatus &operator=(const PowerStatus &); + + void observe(Observable *source) + { + statusObserver.observe(source); + } + + bool getHasBattery() const + { + return hasBattery; + } + + bool getHasUSB() const + { + return hasUSB; + } + + bool getIsCharging() const + { + return isCharging; + } + + int getBatteryVoltageMv() const + { + return batteryVoltageMv; + } + + uint8_t getBatteryChargePercent() const + { + return batteryChargePercent; + } + + bool matches(const PowerStatus *newStatus) const + { + return ( + newStatus->getHasBattery() != hasBattery || + newStatus->getHasUSB() != hasUSB || + newStatus->getBatteryVoltageMv() != batteryVoltageMv + ); + } + int updateStatus(const PowerStatus *newStatus) { + // Only update the status if values have actually changed + bool isDirty; + { + isDirty = matches(newStatus); + initialized = true; + hasBattery = newStatus->getHasBattery(); + batteryVoltageMv = newStatus->getBatteryVoltageMv(); + batteryChargePercent = newStatus->getBatteryChargePercent(); + hasUSB = newStatus->getHasUSB(); + isCharging = newStatus->getIsCharging(); + } + if(isDirty) { + DEBUG_MSG("Battery %dmV %d%%\n", batteryVoltageMv, batteryChargePercent); + onNewStatus.notifyObservers(this); + } + return 0; + } + + }; + +} + +extern meshtastic::PowerStatus *powerStatus; diff --git a/src/Status.h b/src/Status.h new file mode 100644 index 000000000..eb41b60ce --- /dev/null +++ b/src/Status.h @@ -0,0 +1,72 @@ +#pragma once + +#include "Observer.h" + +// Constants for the various status types, so we can tell subclass instances apart +#define STATUS_TYPE_BASE 0 +#define STATUS_TYPE_POWER 1 +#define STATUS_TYPE_GPS 2 +#define STATUS_TYPE_NODE 3 + + +namespace meshtastic +{ + + // A base class for observable status + class Status + { + protected: + // Allows us to observe an Observable + CallbackObserver statusObserver = CallbackObserver(this, &Status::updateStatus); + bool initialized = false; + // Workaround for no typeid support + int statusType; + + public: + // Allows us to generate observable events + Observable onNewStatus; + + // Enable polymorphism ? + virtual ~Status() = default; + + Status() { + if (!statusType) + { + statusType = STATUS_TYPE_BASE; + } + } + + // Prevent object copy/move + Status(const Status &) = delete; + Status &operator=(const Status &) = delete; + + // Start observing a source of data + void observe(Observable *source) + { + statusObserver.observe(source); + } + + // Determines whether or not existing data matches the data in another Status instance + bool matches(const Status *otherStatus) const + { + return true; + } + + bool isInitialized() const + { + return initialized; + } + + int getStatusType() const + { + return statusType; + } + + // Called when the Observable we're observing generates a new notification + int updateStatus(const Status *newStatus) + { + return 0; + } + + }; +}; diff --git a/src/StatusHandler.h b/src/StatusHandler.h new file mode 100644 index 000000000..e69de29bb diff --git a/src/concurrency/Thread.cpp b/src/concurrency/Thread.cpp index 39fd8f6ed..99dd16300 100644 --- a/src/concurrency/Thread.cpp +++ b/src/concurrency/Thread.cpp @@ -1,4 +1,5 @@ #include "Thread.h" +#include "timing.h" namespace concurrency { diff --git a/src/concurrency/Thread.h b/src/concurrency/Thread.h index b297e40d1..bc8fe3951 100644 --- a/src/concurrency/Thread.h +++ b/src/concurrency/Thread.h @@ -1,6 +1,7 @@ #pragma once #include "freertosinc.h" +#include "esp_task_wdt.h" namespace concurrency { @@ -30,8 +31,26 @@ class Thread */ virtual void doRun() = 0; + /** + * All thread run methods must periodically call serviceWatchdog, or the system will declare them hung and panic. + * + * this only applies after startWatchdog() has been called. If you need to sleep for a long time call stopWatchdog() + */ + void serviceWatchdog() { esp_task_wdt_reset(); } + void startWatchdog() + { + auto r = esp_task_wdt_add(taskHandle); + assert(r == ESP_OK); + } + void stopWatchdog() + { + auto r = esp_task_wdt_delete(taskHandle); + assert(r == ESP_OK); + } + private: static void callRun(void *_this); }; + } // namespace concurrency diff --git a/src/concurrency/WorkerThread.cpp b/src/concurrency/WorkerThread.cpp index 8650b7b82..8ea1e6a85 100644 --- a/src/concurrency/WorkerThread.cpp +++ b/src/concurrency/WorkerThread.cpp @@ -5,21 +5,28 @@ namespace concurrency { void WorkerThread::doRun() { + startWatchdog(); + while (!wantExit) { + stopWatchdog(); block(); + startWatchdog(); + + // no need - startWatchdog is guaranteed to give us one full watchdog interval + // serviceWatchdog(); // Let our loop worker have one full watchdog interval (at least) to run #ifdef DEBUG_STACK static uint32_t lastPrint = 0; if (timing::millis() - lastPrint > 10 * 1000L) { lastPrint = timing::millis(); - uint32_t taskHandle = reinterpret_cast(xTaskGetCurrentTaskHandle()); - DEBUG_MSG("printThreadInfo(%s) task: %" PRIx32 " core id: %u min free stack: %u\n", "thread", taskHandle, xPortGetCoreID(), - uxTaskGetStackHighWaterMark(nullptr)); + meshtastic::printThreadInfo("net"); } #endif loop(); } + + stopWatchdog(); } } // namespace concurrency diff --git a/src/esp32/main-esp32.cpp b/src/esp32/main-esp32.cpp index e0d53eab1..dcc80aa66 100644 --- a/src/esp32/main-esp32.cpp +++ b/src/esp32/main-esp32.cpp @@ -2,11 +2,11 @@ #include "MeshBluetoothService.h" #include "PowerFSM.h" #include "configuration.h" +#include "esp_task_wdt.h" #include "main.h" -#include "power.h" #include "sleep.h" -#include "utils.h" #include "target_specific.h" +#include "utils.h" bool bluetoothOn; @@ -60,111 +60,6 @@ void getMacAddr(uint8_t *dmac) assert(esp_efuse_mac_get_default(dmac) == ESP_OK); } -#ifdef TBEAM_V10 - -// FIXME. nasty hack cleanup how we load axp192 -#undef AXP192_SLAVE_ADDRESS -#include "axp20x.h" -AXP20X_Class axp; -bool pmu_irq = false; - -/// Reads power status to powerStatus singleton. -// -// TODO(girts): move this and other axp stuff to power.h/power.cpp. -void readPowerStatus() -{ - powerStatus.haveBattery = axp.isBatteryConnect(); - if (powerStatus.haveBattery) { - powerStatus.batteryVoltageMv = axp.getBattVoltage(); - // If the AXP192 returns a valid battery percentage, use it - if (axp.getBattPercentage() >= 0) { - powerStatus.batteryChargePercent = axp.getBattPercentage(); - } else { - // If the AXP192 returns a percentage less than 0, the feature is either not supported or there is an error - // In that case, we compute an estimate of the charge percent based on maximum and minimum voltages defined in power.h - powerStatus.batteryChargePercent = clamp((int)(((powerStatus.batteryVoltageMv - BAT_MILLIVOLTS_EMPTY) * 1e2) / (BAT_MILLIVOLTS_FULL - BAT_MILLIVOLTS_EMPTY)), 0, 100); - } - DEBUG_MSG("Battery %dmV %d%%\n", powerStatus.batteryVoltageMv, powerStatus.batteryChargePercent); - } - powerStatus.usb = axp.isVBUSPlug(); - powerStatus.charging = axp.isChargeing(); -} -#endif // TBEAM_V10 - -#ifdef AXP192_SLAVE_ADDRESS -/** - * Init the power manager chip - * - * axp192 power - DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms to the axp192 because the OLED and the axp192 - share the same i2c bus, instead use ssd1306 sleep mode DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this on!) LDO1 - 30mA -> charges GPS backup battery // charges the tiny J13 battery by the GPS to power the GPS ram (for a couple of days), can - not be turned off LDO2 200mA -> LORA LDO3 200mA -> GPS - */ -void axp192Init() -{ - if (axp192_found) { - if (!axp.begin(Wire, AXP192_SLAVE_ADDRESS)) { - DEBUG_MSG("AXP192 Begin PASS\n"); - - // axp.setChgLEDMode(LED_BLINK_4HZ); - DEBUG_MSG("DCDC1: %s\n", axp.isDCDC1Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("DCDC2: %s\n", axp.isDCDC2Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("LDO2: %s\n", axp.isLDO2Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("LDO3: %s\n", axp.isLDO3Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("DCDC3: %s\n", axp.isDCDC3Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("Exten: %s\n", axp.isExtenEnable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("----------------------------------------\n"); - - axp.setPowerOutPut(AXP192_LDO2, AXP202_ON); // LORA radio - axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); // GPS main power - axp.setPowerOutPut(AXP192_DCDC2, AXP202_ON); - axp.setPowerOutPut(AXP192_EXTEN, AXP202_ON); - axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); - axp.setDCDC1Voltage(3300); // for the OLED power - - DEBUG_MSG("DCDC1: %s\n", axp.isDCDC1Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("DCDC2: %s\n", axp.isDCDC2Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("LDO2: %s\n", axp.isLDO2Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("LDO3: %s\n", axp.isLDO3Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("DCDC3: %s\n", axp.isDCDC3Enable() ? "ENABLE" : "DISABLE"); - DEBUG_MSG("Exten: %s\n", axp.isExtenEnable() ? "ENABLE" : "DISABLE"); - - axp.setChargeControlCur(AXP1XX_CHARGE_CUR_1320MA); // actual limit (in HW) on the tbeam is 450mA -#if 0 - - // Not connected - //val = 0xfc; - //axp._writeByte(AXP202_VHTF_CHGSET, 1, &val); // Set temperature protection - - //not used - //val = 0x46; - //axp._writeByte(AXP202_OFF_CTL, 1, &val); // enable bat detection -#endif - axp.debugCharging(); - -#ifdef PMU_IRQ - pinMode(PMU_IRQ, INPUT); - attachInterrupt( - PMU_IRQ, [] { pmu_irq = true; }, FALLING); - - axp.adc1Enable(AXP202_BATT_CUR_ADC1, 1); - axp.enableIRQ(AXP202_BATT_REMOVED_IRQ | AXP202_BATT_CONNECT_IRQ | AXP202_CHARGING_FINISHED_IRQ | AXP202_CHARGING_IRQ | - AXP202_VBUS_REMOVED_IRQ | AXP202_VBUS_CONNECT_IRQ | AXP202_PEK_SHORTPRESS_IRQ, - 1); - - axp.clearIRQ(); -#endif - readPowerStatus(); - } else { - DEBUG_MSG("AXP192 Begin FAIL\n"); - } - } else { - DEBUG_MSG("AXP192 not found\n"); - } -} -#endif - /* static void printBLEinfo() { int dev_num = esp_ble_get_bond_device_num(); @@ -190,9 +85,15 @@ void esp32Setup() // enableModemSleep(); -#ifdef AXP192_SLAVE_ADDRESS - axp192Init(); -#endif +// Since we are turning on watchdogs rather late in the release schedule, we really don't want to catch any +// false positives. The wait-to-sleep timeout for shutting down radios is 30 secs, so pick 45 for now. +#define APP_WATCHDOG_SECS 45 + + auto res = esp_task_wdt_init(APP_WATCHDOG_SECS, true); + assert(res == ESP_OK); + + res = esp_task_wdt_add(NULL); + assert(res == ESP_OK); } #if 0 @@ -215,51 +116,12 @@ uint32_t axpDebugRead() Periodic axpDebugOutput(axpDebugRead); #endif - /// loop code specific to ESP32 targets void esp32Loop() { + esp_task_wdt_reset(); // service our app level watchdog loopBLE(); // for debug printing // radio.radioIf.canSleep(); - -#ifdef PMU_IRQ - if (pmu_irq) { - pmu_irq = false; - axp.readIRQ(); - - DEBUG_MSG("pmu irq!\n"); - - if (axp.isChargingIRQ()) { - DEBUG_MSG("Battery start charging\n"); - } - if (axp.isChargingDoneIRQ()) { - DEBUG_MSG("Battery fully charged\n"); - } - if (axp.isVbusRemoveIRQ()) { - DEBUG_MSG("USB unplugged\n"); - } - if (axp.isVbusPlugInIRQ()) { - DEBUG_MSG("USB plugged In\n"); - } - if (axp.isBattPlugInIRQ()) { - DEBUG_MSG("Battery inserted\n"); - } - if (axp.isBattRemoveIRQ()) { - DEBUG_MSG("Battery removed\n"); - } - if (axp.isPEKShortPressIRQ()) { - DEBUG_MSG("PEK short button press\n"); - } - - readPowerStatus(); - axp.clearIRQ(); - } - - if (powerStatus.haveBattery && !powerStatus.usb && - axp.getBattVoltage() < MIN_BAT_MILLIVOLTS) // If we have a battery at all and it is less than 10% full, force deep sleep - powerFSM.trigger(EVENT_LOW_BATTERY); - -#endif // T_BEAM_V10 } \ No newline at end of file diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 97d696986..14e6c9dfe 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -1,6 +1,8 @@ #pragma once #include "Observer.h" +#include "GPSStatus.h" +#include "../concurrency/PeriodicTask.h" #include "sys/time.h" /// If we haven't yet set our RTC this boot, set it from a GPS derived time @@ -34,11 +36,15 @@ class GPS : public Observable int32_t latitude = 0, longitude = 0; // as an int mult by 1e-7 to get value as double int32_t altitude = 0; uint32_t dop = 0; // Diminution of position; PDOP where possible (UBlox), HDOP otherwise (TinyGPS) in 10^2 units (needs scaling before use) + uint32_t heading = 0; // Heading of motion, in degrees * 10^-5 + uint32_t numSatellites = 0; bool isConnected = false; // Do we have a GPS we are talking to virtual ~GPS() {} + Observable newStatus; + /** * Returns true if we succeeded */ diff --git a/src/gps/NEMAGPS.cpp b/src/gps/NEMAGPS.cpp index c4453c910..144b5e6b5 100644 --- a/src/gps/NEMAGPS.cpp +++ b/src/gps/NEMAGPS.cpp @@ -55,16 +55,26 @@ void NEMAGPS::loop() longitude = toDegInt(loc.lng); } // Diminution of precision (an accuracy metric) is reported in 10^2 units, so we need to scale down when we use it - if(reader.hdop.isValid()) { + if (reader.hdop.isValid()) { dop = reader.hdop.value(); } + if (reader.course.isValid()) { + heading = reader.course.value() * 1e3; //Scale the heading (in degrees * 10^-2) to match the expected degrees * 10^-5 + } + if (reader.satellites.isValid()) { + numSatellites = reader.satellites.value(); + } // expect gps pos lat=37.520825, lon=-122.309162, alt=158 - DEBUG_MSG("new NEMA GPS pos lat=%f, lon=%f, alt=%d, hdop=%f\n", latitude * 1e-7, longitude * 1e-7, altitude, dop * 1e-2); + DEBUG_MSG("new NEMA GPS pos lat=%f, lon=%f, alt=%d, hdop=%f, heading=%f\n", latitude * 1e-7, longitude * 1e-7, altitude, dop * 1e-2, heading * 1e-5); hasValidLocation = (latitude != 0) || (longitude != 0); // bogus lat lon is reported as 0,0 if (hasValidLocation) notifyObservers(NULL); } + + // Notify any status instances that are observing us + const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasLock(), isConnected, latitude, longitude, altitude, dop, heading, numSatellites); + newStatus.notifyObservers(&status); } } \ No newline at end of file diff --git a/src/gps/UBloxGPS.cpp b/src/gps/UBloxGPS.cpp index 5ba7da307..8c3bb9bac 100644 --- a/src/gps/UBloxGPS.cpp +++ b/src/gps/UBloxGPS.cpp @@ -116,7 +116,8 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s longitude = ublox.getLongitude(0); altitude = ublox.getAltitude(0) / 1000; // in mm convert to meters dop = ublox.getPDOP(0); // PDOP (an accuracy metric) is reported in 10^2 units so we have to scale down when we use it - DEBUG_MSG("new gps pos lat=%f, lon=%f, alt=%d, pdop=%f\n", latitude * 1e-7, longitude * 1e-7, altitude, dop * 1e-2); + heading = ublox.getHeading(0); + numSatellites = ublox.getSIV(0); // bogus lat lon is reported as 0 or 0 (can be bogus just for one) // Also: apparently when the GPS is initially reporting lock it can output a bogus latitude > 90 deg! @@ -129,6 +130,10 @@ The Unix epoch (or Unix time or POSIX time or Unix timestamp) is the number of s } else // we didn't get a location update, go back to sleep and hope the characters show up wantNewLocation = true; + // Notify any status instances that are observing us + const meshtastic::GPSStatus status = meshtastic::GPSStatus(hasLock(), isConnected, latitude, longitude, altitude, dop, heading, numSatellites); + newStatus.notifyObservers(&status); + // Once we have sent a location once we only poll the GPS rarely, otherwise check back every 1s until we have something over // the serial setPeriod(hasValidLocation && !wantNewLocation ? 30 * 1000 : 10 * 1000); diff --git a/src/main.cpp b/src/main.cpp index 8ef075953..19bd4740f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -38,8 +38,8 @@ #include "screen.h" #include "sleep.h" #include "timing.h" -#include #include +#include // #include #ifndef NO_ESP32 @@ -57,8 +57,14 @@ // We always create a screen object, but we only init it if we find the hardware meshtastic::Screen screen(SSD1306_ADDRESS); -// Global power status singleton -meshtastic::PowerStatus powerStatus; +// Global power status +meshtastic::PowerStatus *powerStatus = new meshtastic::PowerStatus(); + +// Global GPS status +meshtastic::GPSStatus *gpsStatus = new meshtastic::GPSStatus(); + +// Global Node status +meshtastic::NodeStatus *nodeStatus = new meshtastic::NodeStatus(); bool ssd1306_found; bool axp192_found; @@ -122,22 +128,24 @@ static uint32_t ledBlinker() setLed(ledOn); // have a very sparse duty cycle of LED being on, unless charging, then blink 0.5Hz square wave rate to indicate that - return powerStatus.charging ? 1000 : (ledOn ? 2 : 1000); + return powerStatus->getIsCharging() ? 1000 : (ledOn ? 2 : 1000); } concurrency::Periodic ledPeriodic(ledBlinker); // Prepare for button presses #ifdef BUTTON_PIN - OneButton userButton; +OneButton userButton; #endif #ifdef BUTTON_PIN_ALT - OneButton userButtonAlt; +OneButton userButtonAlt; #endif -void userButtonPressed() { +void userButtonPressed() +{ powerFSM.trigger(EVENT_PRESS); } -void userButtonPressedLong(){ +void userButtonPressedLong() +{ screen.adjustBrightness(); } @@ -226,6 +234,14 @@ void setup() esp32Setup(); #endif +#ifdef TBEAM_V10 + // Currently only the tbeam has a PMU + power = new Power(); + power->setup(); + power->setStatusHandler(powerStatus); + powerStatus->observe(&power->newStatus); +#endif + #ifdef NRF52_SERIES nrf52Setup(); #endif @@ -254,9 +270,10 @@ void setup() gps = new NEMAGPS(); gps->setup(); #endif + gpsStatus->observe(&gps->newStatus); + nodeStatus->observe(&nodeDB.newStatus); service.init(); - #ifndef NO_ESP32 // Must be after we init the service, because the wifi settings are loaded by NodeDB (oops) initWifi(); @@ -342,6 +359,9 @@ void loop() #ifndef NO_ESP32 esp32Loop(); #endif +#ifdef TBEAM_V10 + power->loop(); +#endif #ifdef BUTTON_PIN userButton.tick(); @@ -366,9 +386,8 @@ void loop() #endif // Update the screen last, after we've figured out what to show. - screen.debug_info()->setNodeNumbersStatus(nodeDB.getNumOnlineNodes(), nodeDB.getNumNodes()); screen.debug_info()->setChannelNameStatus(channelSettings.name); - screen.debug_info()->setPowerStatus(powerStatus); + // screen.debug()->setPowerStatus(powerStatus); // No GPS lock yet, let the OS put the main CPU in low power mode for 100ms (or until another interrupt comes in) // i.e. don't just keep spinning in loop as fast as we can. diff --git a/src/main.h b/src/main.h index 9d0cde8db..471ba2e85 100644 --- a/src/main.h +++ b/src/main.h @@ -1,6 +1,9 @@ #pragma once #include "screen.h" +#include "PowerStatus.h" +#include "GPSStatus.h" +#include "NodeStatus.h" extern bool axp192_found; extern bool ssd1306_found; @@ -9,6 +12,11 @@ extern bool isUSBPowered; // Global Screen singleton. extern meshtastic::Screen screen; +//extern Observable newPowerStatus; //TODO: move this to main-esp32.cpp somehow or a helper class + +//extern meshtastic::PowerStatus *powerStatus; +//extern meshtastic::GPSStatus *gpsStatus; +//extern meshtastic::NodeStatusHandler *nodeStatusHandler; // Return a human readable string of the form "Meshtastic_ab13" const char *getDeviceName(); diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 21d30bcd0..645cefc38 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -304,7 +304,7 @@ int MeshService::onGPSChanged(void *unused) } // Include our current battery voltage in our position announcement - pos.battery_level = powerStatus.batteryChargePercent; + pos.battery_level = powerStatus->getBatteryChargePercent(); updateBatteryLevel(pos.battery_level); // We limit our GPS broadcasts to a max rate diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index ca28c17a5..fe816a202 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -339,12 +339,8 @@ void NodeDB::updateFrom(const MeshPacket &mp) const SubPacket &p = mp.decoded; DEBUG_MSG("Update DB node 0x%x, rx_time=%u\n", mp.from, mp.rx_time); - int oldNumNodes = *numNodes; NodeInfo *info = getOrCreateNode(mp.from); - if (oldNumNodes != *numNodes) - updateGUI = true; // we just created a nodeinfo - if (mp.rx_time) { // if the packet has a valid timestamp use it to update our last_seen info->has_position = true; // at least the time is valid info->position.time = mp.rx_time; @@ -360,6 +356,7 @@ void NodeDB::updateFrom(const MeshPacket &mp) info->position.time = oldtime; info->has_position = true; updateGUIforNode = info; + notifyObservers(true); //Force an update whether or not our node counts have changed break; } @@ -374,6 +371,7 @@ void NodeDB::updateFrom(const MeshPacket &mp) devicestate.has_rx_text_message = true; updateTextMessage = true; powerFSM.trigger(EVENT_RECEIVED_TEXT_MSG); + notifyObservers(true); //Force an update whether or not our node counts have changed } } break; @@ -392,6 +390,7 @@ void NodeDB::updateFrom(const MeshPacket &mp) if (changed) { updateGUIforNode = info; powerFSM.trigger(EVENT_NODEDB_UPDATED); + notifyObservers(true); //Force an update whether or not our node counts have changed // Not really needed - we will save anyways when we go to sleep // We just changed something important about the user, store our DB @@ -399,6 +398,10 @@ void NodeDB::updateFrom(const MeshPacket &mp) } break; } + + default: { + notifyObservers(); //If the node counts have changed, notify observers + } } } } diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 611024e22..2465c2021 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -2,9 +2,11 @@ #include #include +#include "Observer.h" #include "MeshTypes.h" #include "mesh-pb-constants.h" +#include "NodeStatus.h" extern DeviceState devicestate; extern MyNodeInfo &myNodeInfo; @@ -32,6 +34,7 @@ class NodeDB bool updateGUI = false; // we think the gui should definitely be redrawn, screen will clear this once handled NodeInfo *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI bool updateTextMessage = false; // if true, the GUI should show a new text message + Observable newStatus; /// don't do mesh based algoritm for node id assignment (initially) /// instead just store in flash - possibly even in the initial alpha release do this hack @@ -91,6 +94,13 @@ class NodeDB /// Find a node in our DB, create an empty NodeInfo if missing NodeInfo *getOrCreateNode(NodeNum n); + /// Notify observers of changes to the DB + void notifyObservers(bool forceUpdate = false) { + // Notify observers of the current node state + const meshtastic::NodeStatus status = meshtastic::NodeStatus(getNumOnlineNodes(), getNumNodes(), forceUpdate); + newStatus.notifyObservers(&status); + } + /// read our db from flash void loadFromDisk(); diff --git a/src/power.h b/src/power.h index 21c913d50..dd6b8ce87 100644 --- a/src/power.h +++ b/src/power.h @@ -1,4 +1,6 @@ #pragma once +#include "concurrency/PeriodicTask.h" +#include "PowerStatus.h" /** * Per @spattinson @@ -13,23 +15,26 @@ #define BAT_MILLIVOLTS_FULL 4100 #define BAT_MILLIVOLTS_EMPTY 3500 -namespace meshtastic +class Power : public concurrency::PeriodicTask { -/// Describes the state of the power system. -struct PowerStatus { - /// Whether we have a battery connected - bool haveBattery; - /// Battery voltage in mV, valid if haveBattery is true - int batteryVoltageMv; - /// Battery charge percentage, either read directly or estimated - int batteryChargePercent; - /// Whether USB is connected - bool usb; - /// Whether we are charging the battery - bool charging; + public: + + Observable newStatus; + + void readPowerStatus(); + void loop(); + virtual bool setup(); + virtual void doTask(); + void setStatusHandler(meshtastic::PowerStatus *handler) + { + statusHandler = handler; + } + + protected: + meshtastic::PowerStatus *statusHandler; + virtual void axp192Init(); + }; -} // namespace meshtastic - -extern meshtastic::PowerStatus powerStatus; +extern Power *power; \ No newline at end of file diff --git a/src/screen.cpp b/src/screen.cpp index 6a210beb5..60bfaadb0 100644 --- a/src/screen.cpp +++ b/src/screen.cpp @@ -35,14 +35,11 @@ along with this program. If not, see . #define FONT_HEIGHT 14 // actually 13 for "ariel 10" but want a little extra space #define FONT_HEIGHT_16 (ArialMT_Plain_16[1] + 1) -#ifdef USE_SH1106 -#define SCREEN_WIDTH 132 -#else +// This means the *visible* area (sh1106 can address 132, but shows 128 for example) #define SCREEN_WIDTH 128 -#endif #define SCREEN_HEIGHT 64 #define TRANSITION_FRAMERATE 30 // fps -#define IDLE_FRAMERATE 10 // in fps +#define IDLE_FRAMERATE 1 // in fps #define COMPASS_DIAM 44 #define NUM_EXTRA_FRAMES 2 // text message and debug frame @@ -55,7 +52,16 @@ static FrameCallback normalFrames[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; static uint32_t targetFramerate = IDLE_FRAMERATE; static char btPIN[16] = "888888"; -uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C}; +uint8_t imgBattery[16] = { 0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C }; +uint8_t imgSatellite[8] = { 0x70, 0x71, 0x22, 0xFA, 0xFA, 0x22, 0x71, 0x70 }; + +uint32_t dopThresholds[5] = { 2000, 1000, 500, 200, 100 }; + +// if defined a pixel will blink to show redraws +// #define SHOW_REDRAWS +#ifdef SHOW_REDRAWS +static bool heartbeat = false; +#endif static void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { @@ -143,38 +149,40 @@ static void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char * } } -/// Draw a series of fields in a row, wrapping to multiple rows if needed -/// @return the max y we ended up printing to -static uint32_t drawRows(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) -{ - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); +#if 0 + /// Draw a series of fields in a row, wrapping to multiple rows if needed + /// @return the max y we ended up printing to + static uint32_t drawRows(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) + { + // The coordinates define the left starting point of the text + display->setTextAlignment(TEXT_ALIGN_LEFT); - const char **f = fields; - int xo = x, yo = y; - const int COLUMNS = 2; // hardwired for two columns per row.... - int col = 0; // track which column we are on - while (*f) { - display->drawString(xo, yo, *f); - xo += SCREEN_WIDTH / COLUMNS; - // Wrap to next row, if needed. - if (++col >= COLUMNS) { - xo = x; - yo += FONT_HEIGHT; - col = 0; + const char **f = fields; + int xo = x, yo = y; + const int COLUMNS = 2; // hardwired for two columns per row.... + int col = 0; // track which column we are on + while (*f) { + display->drawString(xo, yo, *f); + xo += SCREEN_WIDTH / COLUMNS; + // Wrap to next row, if needed. + if (++col >= COLUMNS) { + xo = x; + yo += FONT_HEIGHT; + col = 0; + } + f++; + } + if (col != 0) { + // Include last incomplete line in our total. + yo += FONT_HEIGHT; } - f++; - } - if (col != 0) { - // Include last incomplete line in our total. - yo += FONT_HEIGHT; - } - return yo; -} + return yo; + } +#endif // Draw power bars or a charging indicator on an image of a battery, determined by battery charge voltage or percentage. -static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, PowerStatus *powerStatus) +static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer, const PowerStatus *powerStatus) { static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD}; static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85}; @@ -183,12 +191,12 @@ static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *img imgBuffer[i] = 0x81; } // If charging, draw a charging indicator - if (powerStatus->charging) { + if (powerStatus->getIsCharging()) { memcpy(imgBuffer + 3, lightning, 8); // If not charging, Draw power bars } else { for (int i = 0; i < 4; i++) { - if (powerStatus->batteryChargePercent >= 25 * i) + if (powerStatus->getBatteryChargePercent() >= 25 * i) memcpy(imgBuffer + 1 + (i * 3), powerBar, 3); } } @@ -196,49 +204,50 @@ static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *img } // Draw nodes status -static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, int nodesOnline, int nodesTotal) +static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, NodeStatus *nodeStatus) { char usersString[20]; - sprintf(usersString, "%d/%d", nodesOnline, nodesTotal); + sprintf(usersString, "%d/%d", nodeStatus->getNumOnline(), nodeStatus->getNumTotal()); display->drawFastImage(x, y, 8, 8, imgUser); display->drawString(x + 10, y - 2, usersString); } // Draw GPS status summary -static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, GPS *gps) +static void drawGPS(OLEDDisplay *display, int16_t x, int16_t y, const GPSStatus *gps) { - if (!gps->isConnected) { + if (!gps->getIsConnected()) + { display->drawString(x, y - 2, "No GPS"); return; } - display->drawFastImage(x, y, 6, 8, gps->hasLock() ? imgPositionSolid : imgPositionEmpty); - if (!gps->hasLock()) { + display->drawFastImage(x, y, 6, 8, gps->getHasLock() ? imgPositionSolid : imgPositionEmpty); + if (!gps->getHasLock()) + { display->drawString(x + 8, y - 2, "No sats"); return; - } - if (gps->dop <= 100) { - display->drawString(x + 8, y - 2, "Ideal"); - return; - } - if (gps->dop <= 200) { - display->drawString(x + 8, y - 2, "Exc."); - return; - } - if (gps->dop <= 500) { - display->drawString(x + 8, y - 2, "Good"); - return; - } - if (gps->dop <= 1000) { - display->drawString(x + 8, y - 2, "Mod."); - return; - } - if (gps->dop <= 2000) { - display->drawString(x + 8, y - 2, "Fair"); - return; - } - if (gps->dop > 0) { - display->drawString(x + 8, y - 2, "Poor"); - return; + } + else + { + char satsString[3]; + uint8_t bar[2] = { 0 }; + + //Draw DOP signal bars + for(int i = 0; i < 5; i++) + { + if (gps->getDOP() <= dopThresholds[i]) + bar[0] = ~((1 << (5 - i)) - 1); + else + bar[0] = 0b10000000; + //bar[1] = bar[0]; + display->drawFastImage(x + 9 + (i * 2), y, 2, 8, bar); + } + + //Draw satellite image + display->drawFastImage(x + 24, y, 8, 8, imgSatellite); + + //Draw the number of satellites + sprintf(satsString, "%d", gps->getNumSatellites()); + display->drawString(x + 34, y - 2, satsString); } } @@ -381,28 +390,41 @@ static bool hasPosition(NodeInfo *n) static size_t nodeIndex; static int8_t prevFrame = -1; -// Draw the compass and arrow pointing to location -static void drawCompass(OLEDDisplay *display, int16_t compassX, int16_t compassY, float headingRadian) +// Draw the arrow pointing to a node's location +static void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, float headingRadian) { - // display->drawXbm(compassX, compassY, compass_width, compass_height, - // (const uint8_t *)compass_bits); - Point tip(0.0f, 0.5f), tail(0.0f, -0.5f); // pointing up initially float arrowOffsetX = 0.2f, arrowOffsetY = 0.2f; Point leftArrow(tip.x - arrowOffsetX, tip.y - arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y - arrowOffsetY); - Point *points[] = {&tip, &tail, &leftArrow, &rightArrow}; + + Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow}; for (int i = 0; i < 4; i++) { - points[i]->rotate(headingRadian); - points[i]->scale(COMPASS_DIAM * 0.6); - points[i]->translate(compassX, compassY); + arrowPoints[i]->rotate(headingRadian); + arrowPoints[i]->scale(COMPASS_DIAM * 0.6); + arrowPoints[i]->translate(compassX, compassY); } drawLine(display, tip, tail); drawLine(display, leftArrow, tip); drawLine(display, rightArrow, tip); +} - display->drawCircle(compassX, compassY, COMPASS_DIAM / 2); +// Draw the compass heading +static void drawCompassHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading) +{ + Point N1(-0.04f, -0.65f), N2( 0.04f, -0.65f); + Point N3(-0.04f, -0.55f), N4( 0.04f, -0.55f); + Point *rosePoints[] = {&N1, &N2, &N3, &N4}; + + for (int i = 0; i < 4; i++) { + rosePoints[i]->rotate(myHeading); + rosePoints[i]->scale(COMPASS_DIAM); + rosePoints[i]->translate(compassX, compassY); + } + drawLine(display, N1, N3); + drawLine(display, N2, N4); + drawLine(display, N1, N4); } /// Convert an integer GPS coords to a floating point @@ -422,10 +444,13 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ nodeIndex = (nodeIndex + 1) % nodeDB.getNumNodes(); n = nodeDB.getNodeByIndex(nodeIndex); } - - // We just changed to a new node screen, ask that node for updated state displayedNodeNum = n->num; - service.sendNetworkPing(displayedNodeNum, true); + + // We just changed to a new node screen, ask that node for updated state if it's older than 2 minutes + if(sinceLastSeen(n) > 120) + { + service.sendNetworkPing(displayedNodeNum, true); + } } NodeInfo *node = nodeDB.getNodeByIndex(nodeIndex); @@ -456,29 +481,40 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ const char *fields[] = {username, distStr, signalStr, lastStr, NULL}; // coordinates for the center of the compass/circle - int16_t compassX = x + SCREEN_WIDTH - COMPASS_DIAM / 2 - 1, compassY = y + SCREEN_HEIGHT / 2; + int16_t compassX = x + SCREEN_WIDTH - COMPASS_DIAM / 2 - 5, compassY = y + SCREEN_HEIGHT / 2; + bool hasNodeHeading = false; - if (ourNode && hasPosition(ourNode) && hasPosition(node)) { // display direction toward node - Position &op = ourNode->position, &p = node->position; - float d = latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - if (d < 2000) - snprintf(distStr, sizeof(distStr), "%.0f m", d); - else - snprintf(distStr, sizeof(distStr), "%.1f km", d / 1000); + if(ourNode && hasPosition(ourNode)) + { + Position &op = ourNode->position; + float myHeading = estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i)); + drawCompassHeading(display, compassX, compassY, myHeading); - // FIXME, also keep the guess at the operators heading and add/substract - // it. currently we don't do this and instead draw north up only. - float bearingToOther = bearing(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); - float myHeading = estimatedHeading(DegD(p.latitude_i), DegD(p.longitude_i)); - headingRadian = bearingToOther - myHeading; - drawCompass(display, compassX, compassY, headingRadian); - } else { // direction to node is unknown so display question mark + if(hasPosition(node)) + { + // display direction toward node + hasNodeHeading = true; + Position &p = node->position; + float d = latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + if (d < 2000) + snprintf(distStr, sizeof(distStr), "%.0f m", d); + else + snprintf(distStr, sizeof(distStr), "%.1f km", d / 1000); + + // FIXME, also keep the guess at the operators heading and add/substract + // it. currently we don't do this and instead draw north up only. + float bearingToOther = bearing(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i)); + headingRadian = bearingToOther - myHeading; + drawNodeHeading(display, compassX, compassY, headingRadian); + } + } + if(!hasNodeHeading) + // direction to node is unknown so display question mark // Debug info for gps lock errors // DEBUG_MSG("ourNode %d, ourPos %d, theirPos %d\n", !!ourNode, ourNode && hasPosition(ourNode), hasPosition(node)); - display->drawString(compassX - FONT_HEIGHT / 4, compassY - FONT_HEIGHT / 2, "?"); - display->drawCircle(compassX, compassY, COMPASS_DIAM / 2); - } + display->drawCircle(compassX, compassY, COMPASS_DIAM / 2); + // Must be after distStr is populated drawColumns(display, x, y, fields); @@ -576,6 +612,11 @@ void Screen::setup() // twice initially. ui.update(); ui.update(); + + // Subscribe to status updates + powerStatusObserver.observe(&powerStatus->onNewStatus); + gpsStatusObserver.observe(&gpsStatus->onNewStatus); + nodeStatusObserver.observe(&nodeStatus->onNewStatus); } void Screen::doTask() @@ -637,14 +678,7 @@ void Screen::doTask() // While showing the bootscreen or Bluetooth pair screen all of our // standard screen switching is stopped. if (showingNormalScreen) { - // TODO(girts): decouple nodeDB from screen. - // standard screen loop handling ehre - // If the # nodes changes, we need to regen our list of screens - if (nodeDB.updateGUI || nodeDB.updateTextMessage) { - setFrames(); - nodeDB.updateGUI = false; - nodeDB.updateTextMessage = false; - } + // standard screen loop handling here } ui.update(); @@ -669,8 +703,8 @@ void Screen::setFrames() DEBUG_MSG("showing standard frames\n"); showingNormalScreen = true; - size_t numnodes = nodeDB.getNumNodes(); // We don't show the node info our our node (if we have it yet - we should) + size_t numnodes = nodeStatus->getNumTotal(); if (numnodes > 0) numnodes--; @@ -750,20 +784,26 @@ void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 snprintf(channelStr, sizeof(channelStr), "#%s", channelName.c_str()); // Display power status - if (powerStatus.haveBattery) - drawBattery(display, x, y + 2, imgBattery, &powerStatus); + if (powerStatus->getHasBattery()) + drawBattery(display, x, y + 2, imgBattery, powerStatus); else - display->drawFastImage(x, y + 2, 16, 8, powerStatus.usb ? imgUSB : imgPower); + display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower); // Display nodes status - drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodesOnline, nodesTotal); + drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus); // Display GPS status - drawGPS(display, x + (SCREEN_WIDTH * 0.66), y + 2, gps); + drawGPS(display, x + (SCREEN_WIDTH * 0.63), y + 2, gpsStatus); } - const char *fields[] = {channelStr, nullptr}; - uint32_t yo = drawRows(display, x, y + FONT_HEIGHT, fields); + display->drawString(x, y + FONT_HEIGHT, channelStr); - display->drawLogBuffer(x, yo); + display->drawLogBuffer(x, y + (FONT_HEIGHT * 2)); + + /* Display a heartbeat pixel that blinks every time the frame is redrawn */ +#ifdef SHOW_REDRAWS + if (heartbeat) + display->setPixel(0, 0); + heartbeat = !heartbeat; +#endif } // adjust Brightness cycle trough 1 to 254 as long as attachDuringLongPress is true @@ -781,4 +821,20 @@ void Screen::adjustBrightness() dispdev.setBrightness(brightness); } +int Screen::handleStatusUpdate(const Status *arg) +{ + //DEBUG_MSG("Screen got status update %d\n", arg->getStatusType()); + switch(arg->getStatusType()) + { + case STATUS_TYPE_NODE: + if (nodeDB.updateTextMessage || nodeStatus->getLastNumTotal() != nodeStatus->getNumTotal()) + setFrames(); + prevFrame = -1; + nodeDB.updateGUI = false; + nodeDB.updateTextMessage = false; + break; + } + setPeriod(1); // Update the screen right away + return 0; +} } // namespace meshtastic diff --git a/src/screen.h b/src/screen.h index fe09d68f0..4f24aaef0 100644 --- a/src/screen.h +++ b/src/screen.h @@ -29,14 +29,6 @@ class DebugInfo DebugInfo(const DebugInfo &) = delete; DebugInfo &operator=(const DebugInfo &) = delete; - /// Sets user statistics. - void setNodeNumbersStatus(int online, int total) - { - concurrency::LockGuard guard(&lock); - nodesOnline = online; - nodesTotal = total; - } - /// Sets the name of the channel. void setChannelNameStatus(const char *name) { @@ -44,25 +36,6 @@ class DebugInfo channelName = name; } - /// Sets battery/charging/etc status. - // - void setPowerStatus(const PowerStatus &status) - { - concurrency::LockGuard guard(&lock); - powerStatus = status; - } - - /// Sets GPS status. - // - // If this function never gets called, we assume GPS does not exist on this - // device. - // TODO(girts): figure out what the format should be. - void setGPSStatus(const char *status) - { - concurrency::LockGuard guard(&lock); - gpsStatus = status; - } - private: friend Screen; @@ -71,15 +44,8 @@ class DebugInfo /// Renders the debug screen. void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); - int nodesOnline = 0; - int nodesTotal = 0; - - PowerStatus powerStatus; - std::string channelName; - std::string gpsStatus; - /// Protects all of internal state. concurrency::Lock lock; }; @@ -93,6 +59,10 @@ class DebugInfo // simultaneously). class Screen : public concurrency::PeriodicTask { + CallbackObserver powerStatusObserver = CallbackObserver(this, &Screen::handleStatusUpdate); + CallbackObserver gpsStatusObserver = CallbackObserver(this, &Screen::handleStatusUpdate); + CallbackObserver nodeStatusObserver = CallbackObserver(this, &Screen::handleStatusUpdate); + public: Screen(uint8_t address, int sda = -1, int scl = -1); @@ -119,7 +89,7 @@ class Screen : public concurrency::PeriodicTask // Implementation to Adjust Brightness void adjustBrightness(); - int brightness = 150; + uint8_t brightness = 150; /// Starts showing the Bluetooth PIN screen. // @@ -189,6 +159,8 @@ class Screen : public concurrency::PeriodicTask // Use this handle to set things like battery status, user count, GPS status, etc. DebugInfo* debug_info() { return &debugInfo; } + int handleStatusUpdate(const meshtastic::Status *arg); + protected: /// Updates the UI. //