diff --git a/lib/BluetoothOTA/src/BluetoothUtil.cpp b/lib/BluetoothOTA/src/BluetoothUtil.cpp index 4cbb5755..682db0b9 100644 --- a/lib/BluetoothOTA/src/BluetoothUtil.cpp +++ b/lib/BluetoothOTA/src/BluetoothUtil.cpp @@ -5,7 +5,6 @@ #include #include #include "configuration.h" -#include "screen.h" SimpleAllocator btPool; @@ -173,7 +172,7 @@ uint32_t getValue32(BLECharacteristic *c, uint32_t defaultValue) class MySecurity : public BLESecurityCallbacks { - + protected: bool onConfirmPIN(uint32_t pin) { Serial.printf("onConfirmPIN %u\n", pin); @@ -189,7 +188,7 @@ class MySecurity : public BLESecurityCallbacks void onPassKeyNotify(uint32_t pass_key) { Serial.printf("onPassKeyNotify %u\n", pass_key); - screen_start_bluetooth(pass_key); + startCb(pass_key); } bool onSecurityRequest() @@ -211,9 +210,13 @@ class MySecurity : public BLESecurityCallbacks Serial.printf("onAuthenticationComplete -> fail %d\n", cmpl.fail_reason); } - // Remove our custom screen - screen.setFrames(); + // Remove our custom PIN request screen. + stopCb(); } + + public: + StartBluetoothPinScreenCallback startCb; + StopBluetoothPinScreenCallback stopCb; }; BLEServer *pServer; @@ -255,7 +258,10 @@ void deinitBLE() btPool.reset(); } -BLEServer *initBLE(std::string deviceName, std::string hwVendor, std::string swVersion, std::string hwVersion) +BLEServer *initBLE( + StartBluetoothPinScreenCallback startBtPinScreen, + StopBluetoothPinScreenCallback stopBtPinScreen, + std::string deviceName, std::string hwVendor, std::string swVersion, std::string hwVersion) { BLEDevice::init(deviceName); BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT); @@ -264,6 +270,8 @@ BLEServer *initBLE(std::string deviceName, std::string hwVendor, std::string swV * Required in authentication process to provide displaying and/or input passkey or yes/no butttons confirmation */ static MySecurity mySecurity; + mySecurity.startCb = startBtPinScreen; + mySecurity.stopCb = stopBtPinScreen; BLEDevice::setSecurityCallbacks(&mySecurity); // Create the BLE Server diff --git a/lib/BluetoothOTA/src/BluetoothUtil.h b/lib/BluetoothOTA/src/BluetoothUtil.h index 72920aec..b1aa77db 100644 --- a/lib/BluetoothOTA/src/BluetoothUtil.h +++ b/lib/BluetoothOTA/src/BluetoothUtil.h @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -17,8 +19,14 @@ void dumpCharacteristic(BLECharacteristic *c); /** converting endianness pull out a 32 bit value */ uint32_t getValue32(BLECharacteristic *c, uint32_t defaultValue); +// TODO(girts): create a class for the bluetooth utils helpers? +using StartBluetoothPinScreenCallback = std::function; +using StopBluetoothPinScreenCallback = std::function; + void loopBLE(); -BLEServer *initBLE(std::string devName, std::string hwVendor, std::string swVersion, std::string hwVersion = ""); +BLEServer *initBLE( + StartBluetoothPinScreenCallback startBtPinScreen, StopBluetoothPinScreenCallback stopBtPinScreen, + std::string devName, std::string hwVendor, std::string swVersion, std::string hwVersion = ""); void deinitBLE(); /// Add a characteristic that we will delete when we restart diff --git a/src/MeshService.cpp b/src/MeshService.cpp index 51c942d2..0834f518 100644 --- a/src/MeshService.cpp +++ b/src/MeshService.cpp @@ -2,12 +2,12 @@ #include #include +#include "main.h" #include "mesh-pb-constants.h" #include "MeshService.h" #include "MeshBluetoothService.h" #include "NodeDB.h" #include "GPS.h" -#include "screen.h" #include "Periodic.h" #include "PowerFSM.h" @@ -118,7 +118,7 @@ MeshPacket *MeshService::handleFromRadioUser(MeshPacket *mp) sendOurOwner(mp->from); String lcd = String("Joined: ") + mp->payload.variant.user.long_name + "\n"; - screen_print(lcd.c_str()); + screen.print(lcd.c_str()); } return mp; @@ -360,4 +360,4 @@ void MeshService::onNotify(Observable *o) { DEBUG_MSG("got gps notify\n"); onGPSChanged(); -} \ No newline at end of file +} diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 7080bb4a..5a02c08e 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -179,4 +179,4 @@ void PowerFSM_setup() // timeout"); powerFSM.run_machine(); // run one interation of the state machine, so we run our on enter tasks for the initial DARK state -} \ No newline at end of file +} diff --git a/src/main.cpp b/src/main.cpp index 28785228..30de8769 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -44,6 +44,14 @@ AXP20X_Class axp; bool pmu_irq = false; #endif +// Global Screen singleton +#ifdef I2C_SDA +meshtastic::Screen screen(SSD1306_ADDRESS, I2C_SDA, I2C_SCL); +#else +// Fake values for pins to keep build happy, we won't ever initialize it. +meshtastic::Screen screen(SSD1306_ADDRESS, 0, 0); +#endif + // these flags are all in bss so they default false bool isCharging; bool isUSBPowered; @@ -221,8 +229,6 @@ void setup() scanI2Cdevice(); #endif - axp192Init(); - // Buttons & LED #ifdef BUTTON_PIN pinMode(BUTTON_PIN, INPUT_PULLUP); @@ -240,13 +246,16 @@ void setup() if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) ssd1306_found = false; // forget we even have the hardware + // Initialize the screen first so we can show the logo while we start up everything else. if (ssd1306_found) screen.setup(); + axp192Init(); + // Init GPS gps.setup(); - screen_print("Started...\n"); + screen.print("Started...\n"); service.init(); @@ -264,7 +273,14 @@ void initBluetooth() // FIXME - we are leaking like crazy // AllocatorScope scope(btPool); - BLEServer *serve = initBLE(getDeviceName(), HW_VENDOR, xstr(APP_VERSION), xstr(HW_VERSION)); // FIXME, use a real name based on the macaddr + // Note: these callbacks might be coming in from a different thread. + BLEServer *serve = initBLE( + [](uint8_t pin) { + powerFSM.trigger(EVENT_BLUETOOTH_PAIR); + screen.startBluetoothPinScreen(pin); + }, + []() { screen.stopBluetoothPinScreen(); }, + getDeviceName(), HW_VENDOR, xstr(APP_VERSION), xstr(HW_VERSION)); // FIXME, use a real name based on the macaddr createMeshBluetoothService(serve); // Start advertising - this must be done _after_ creating all services @@ -392,6 +408,14 @@ void loop() } #endif + // Show boot screen for first 3 seconds, then switch to normal operation. + static bool showingBootScreen = true; + if (showingBootScreen && (millis() > 3000)) + { + screen.stopBootScreen(); + showingBootScreen = false; + } + // 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. //DEBUG_MSG("msecs %d\n", msecstosleep); diff --git a/src/main.h b/src/main.h index 5d2db07c..9842ea96 100644 --- a/src/main.h +++ b/src/main.h @@ -1,6 +1,11 @@ #pragma once +#include "screen.h" + extern bool axp192_found; extern bool ssd1306_found; extern bool isCharging; -extern bool isUSBPowered; \ No newline at end of file +extern bool isUSBPowered; + +// Global Screen singleton. +extern meshtastic::Screen screen; diff --git a/src/screen.cpp b/src/screen.cpp index 977ef2d8..d5390ad0 100644 --- a/src/screen.cpp +++ b/src/screen.cpp @@ -20,55 +20,37 @@ along with this program. If not, see . */ +#include #include -#include "SSD1306Wire.h" -#include "OLEDDisplay.h" -#include "images.h" -#include "fonts.h" + #include "GPS.h" -#include "OLEDDisplayUi.h" -#include "screen.h" -#include "mesh-pb-constants.h" #include "NodeDB.h" -#include "main.h" #include "configuration.h" +#include "fonts.h" +#include "images.h" +#include "main.h" +#include "mesh-pb-constants.h" +#include "screen.h" #define FONT_HEIGHT 14 // actually 13 for "ariel 10" but want a little extra space #define FONT_HEIGHT_16 (ArialMT_Plain_16[1] + 1) #define SCREEN_WIDTH 128 #define SCREEN_HEIGHT 64 - -#ifdef I2C_SDA -SSD1306Wire dispdev(SSD1306_ADDRESS, I2C_SDA, I2C_SCL); -#else -SSD1306Wire dispdev(SSD1306_ADDRESS, 0, 0); // fake values to keep build happy, we won't ever init -#endif - -bool disp; // true if we are using display -bool screenOn; // true if the display is currently powered - -OLEDDisplayUi ui(&dispdev); +#define TRANSITION_FRAMERATE 30 // fps +#define IDLE_FRAMERATE 10 // in fps +#define COMPASS_DIAM 44 #define NUM_EXTRA_FRAMES 2 // text message and debug frame -// A text message frame + debug frame + all the node infos -FrameCallback nonBootFrames[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; -Screen screen; -static bool showingBluetooth; - -/// If set to true (possibly from an ISR), we should turn on the screen the next time our idle loop runs. -static bool showingBootScreen = true; // start by showing the bootscreen - -bool Screen::isOn() { return screenOn; } - -void msOverlay(OLEDDisplay *display, OLEDDisplayUiState *state) +namespace meshtastic { - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->setFont(ArialMT_Plain_10); - display->drawString(128, 0, String(millis())); -} -void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +// A text message frame + debug frame + all the node infos +static FrameCallback normalFrames[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; +static uint32_t targetFramerate = IDLE_FRAMERATE; +static char btPIN[16] = "888888"; + +static void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { // draw an xbm image. // Please note that everything that should be transitioned @@ -79,16 +61,10 @@ void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, display->setFont(ArialMT_Plain_16); display->setTextAlignment(TEXT_ALIGN_CENTER); display->drawString(64 + x, SCREEN_HEIGHT - FONT_HEIGHT_16, "meshtastic.org"); - - ui.disableIndicator(); } -static char btPIN[16] = "888888"; - -void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +static void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // Demonstrates the 3 included default sizes. The fonts come from SSD1306Fonts.h file - // Besides the default fonts there will be a program to convert TrueType fonts into this format display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(ArialMT_Plain_16); display->drawString(64 + x, 2 + y, "Bluetooth"); @@ -99,81 +75,45 @@ void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(ArialMT_Plain_24); display->drawString(64 + x, 22 + y, btPIN); - - ui.disableIndicator(); -} - -void drawFrame2(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Demonstrates the 3 included default sizes. The fonts come from SSD1306Fonts.h file - // Besides the default fonts there will be a program to convert TrueType fonts into this format - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->setFont(ArialMT_Plain_10); - display->drawString(0 + x, 10 + y, "Arial 10"); - - display->setFont(ArialMT_Plain_16); - display->drawString(0 + x, 20 + y, "Arial 16"); - - display->setFont(ArialMT_Plain_24); - display->drawString(0 + x, 34 + y, "Arial 24"); -} - -void drawFrame3(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -{ - // Text alignment demo - display->setFont(ArialMT_Plain_10); - - // The coordinates define the left starting point of the text - display->setTextAlignment(TEXT_ALIGN_LEFT); - display->drawString(0 + x, 11 + y, "Left aligned (0,10)"); - - // The coordinates define the center of the text - display->setTextAlignment(TEXT_ALIGN_CENTER); - display->drawString(64 + x, 22 + y, "Center aligned (64,22)"); - - // The coordinates define the right end of the text - display->setTextAlignment(TEXT_ALIGN_RIGHT); - display->drawString(128 + x, 33 + y, "Right aligned (128,33)"); } /// Draw the last text message we received -void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +static void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { MeshPacket &mp = devicestate.rx_text_message; NodeInfo *node = nodeDB.getNode(mp.from); - // DEBUG_MSG("drawing text message from 0x%x: %s\n", mp.from, mp.payload.variant.data.payload.bytes); + // DEBUG_MSG("drawing text message from 0x%x: %s\n", mp.from, + // mp.payload.variant.data.payload.bytes); // Demo for drawStringMaxWidth: - // with the third parameter you can define the width after which words will be wrapped. - // Currently only spaces and "-" are allowed for wrapping + // with the third parameter you can define the width after which words will + // be wrapped. Currently only spaces and "-" are allowed for wrapping display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(ArialMT_Plain_16); String sender = (node && node->has_user) ? node->user.short_name : "???"; display->drawString(0 + x, 0 + y, sender); display->setFont(ArialMT_Plain_10); + // the max length of this buffer is much longer than we can possibly print static char tempBuf[96]; - snprintf(tempBuf, sizeof(tempBuf), " %s", mp.payload.variant.data.payload.bytes); // the max length of this buffer is much longer than we can possibly print + snprintf(tempBuf, sizeof(tempBuf), " %s", + mp.payload.variant.data.payload.bytes); display->drawStringMaxWidth(4 + x, 10 + y, 128, tempBuf); - - // ui.disableIndicator(); } /// Draw a series of fields in a column, wrapping to multiple colums if needed -void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) +static void drawColumns(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; - while (*f) - { + while (*f) { display->drawString(xo, yo, *f); yo += FONT_HEIGHT; - if (yo > SCREEN_HEIGHT - FONT_HEIGHT) - { + if (yo > SCREEN_HEIGHT - FONT_HEIGHT) { xo += SCREEN_WIDTH / 2; yo = 0; } @@ -183,21 +123,23 @@ void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields /// Draw a series of fields in a row, wrapping to multiple rows if needed /// @return the max y we ended up printing to -uint32_t drawRows(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) +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; - while (*f) - { + 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 / 2; // hardwired for two columns per row.... - if (xo >= SCREEN_WIDTH) - { + xo += SCREEN_WIDTH / COLUMNS; + // Wrap to next row, if needed. + if (++col > COLUMNS) { + xo = x; yo += FONT_HEIGHT; - xo = 0; + col = 0; } f++; } @@ -207,8 +149,9 @@ uint32_t drawRows(OLEDDisplay *display, int16_t x, int16_t y, const char **field return yo; } -/// Ported from my old java code, returns distance in meters along the globe surface (by magic?) -float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) +/// Ported from my old java code, returns distance in meters along the globe +/// surface (by magic?) +static float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) { double pk = (180 / 3.14169); double a1 = lat_a / pk; @@ -217,10 +160,8 @@ float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) double b2 = lng_b / pk; double cos_b1 = cos(b1); double cos_a1 = cos(a1); - double t1 = - cos_a1 * cos(a2) * cos_b1 * cos(b2); - double t2 = - cos_a1 * sin(a2) * cos_b1 * sin(b2); + double t1 = cos_a1 * cos(a2) * cos_b1 * cos(b2); + double t2 = cos_a1 * sin(a2) * cos_b1 * sin(b2); double t3 = sin(a1) * sin(b1); double tt = acos(t1 + t2 + t3); if (isnan(tt)) @@ -229,31 +170,32 @@ float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) return (float)(6366000 * tt); } -inline double toRadians(double deg) +static inline double toRadians(double deg) { return deg * PI / 180; } -inline double toDegrees(double r) +static inline double toDegrees(double r) { return r * 180 / PI; } /** - * Computes the bearing in degrees between two points on Earth. Ported from my old Gaggle android app. - * - * @param lat1 - * Latitude of the first point - * @param lon1 - * Longitude of the first point - * @param lat2 - * Latitude of the second point - * @param lon2 - * Longitude of the second point - * @return Bearing between the two points in radians. A value of 0 means due - * north. - */ -float bearing(double lat1, double lon1, double lat2, double lon2) + * Computes the bearing in degrees between two points on Earth. Ported from my + * old Gaggle android app. + * + * @param lat1 + * Latitude of the first point + * @param lon1 + * Longitude of the first point + * @param lat2 + * Latitude of the second point + * @param lon2 + * Longitude of the second point + * @return Bearing between the two points in radians. A value of 0 means due + * north. + */ +static float bearing(double lat1, double lon1, double lat2, double lon2) { double lat1Rad = toRadians(lat1); double lat2Rad = toRadians(lat2); @@ -263,10 +205,13 @@ float bearing(double lat1, double lon1, double lat2, double lon2) return atan2(y, x); } +namespace +{ + /// A basic 2D point class for drawing class Point { -public: + public: float x, y; Point(float _x, float _y) : x(_x), y(_y) {} @@ -274,10 +219,8 @@ public: /// Apply a rotation around zero (standard rotation matrix math) void rotate(float radian) { - float cos = cosf(radian), - sin = sinf(radian); - float rx = x * cos - y * sin, - ry = x * sin + y * cos; + float cos = cosf(radian), sin = sinf(radian); + float rx = x * cos - y * sin, ry = x * sin + y * cos; x = rx; y = ry; @@ -296,23 +239,25 @@ public: } }; -void drawLine(OLEDDisplay *d, const Point &p1, const Point &p2) +} // namespace + +static void drawLine(OLEDDisplay *d, const Point &p1, const Point &p2) { d->drawLine(p1.x, p1.y, p2.x, p2.y); } /** * Given a recent lat/lon return a guess of the heading the user is walking on. - * - * We keep a series of "after you've gone 10 meters, what is your heading since the last reference point?" + * + * We keep a series of "after you've gone 10 meters, what is your heading since + * the last reference point?" */ -float estimatedHeading(double lat, double lon) +static float estimatedHeading(double lat, double lon) { static double oldLat, oldLon; static float b; - if (oldLat == 0) - { + if (oldLat == 0) { // just prepare for next time oldLat = lat; oldLon = lon; @@ -331,28 +276,28 @@ float estimatedHeading(double lat, double lon) return b; } -/// Sometimes we will have Position objects that only have a time, so check for valid lat/lon -bool hasPosition(NodeInfo *n) +/// Sometimes we will have Position objects that only have a time, so check for +/// valid lat/lon +static bool hasPosition(NodeInfo *n) { return n->has_position && (n->position.latitude != 0 || n->position.longitude != 0); } -#define COMPASS_DIAM 44 -/// We will skip one node - the one for us, so we just blindly loop over all nodes +/// We will skip one node - the one for us, so we just blindly loop over all +/// nodes static size_t nodeIndex; static int8_t prevFrame = -1; -void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { - // We only advance our nodeIndex if the frame # has changed - because drawNodeInfo will be called repeatedly while the frame is shown - if (state->currentFrame != prevFrame) - { + // We only advance our nodeIndex if the frame # has changed - because + // drawNodeInfo will be called repeatedly while the frame is shown + if (state->currentFrame != prevFrame) { prevFrame = state->currentFrame; nodeIndex = (nodeIndex + 1) % nodeDB.getNumNodes(); NodeInfo *n = nodeDB.getNodeByIndex(nodeIndex); - if (n->num == nodeDB.getNodeNum()) - { + if (n->num == nodeDB.getNodeNum()) { // Don't show our node, just skip to next nodeIndex = (nodeIndex + 1) % nodeDB.getNumNodes(); } @@ -380,14 +325,14 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in snprintf(lastStr, sizeof(lastStr), "%d hours ago", agoSecs / 60 / 60); static float simRadian; - simRadian += 0.1; // For testing, have the compass spin unless both locations are valid + simRadian += 0.1; // For testing, have the compass spin unless both + // locations are valid static char distStr[20]; *distStr = 0; // might not have location data float headingRadian = simRadian; NodeInfo *ourNode = nodeDB.getNode(nodeDB.getNodeNum()); - if (ourNode && hasPosition(ourNode) && hasPosition(node)) - { + if (ourNode && hasPosition(ourNode) && hasPosition(node)) { Position &op = ourNode->position, &p = node->position; float d = latLongToMeter(p.latitude, p.longitude, op.latitude, op.longitude); if (d < 2000) @@ -395,23 +340,20 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in 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. + // 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(p.latitude, p.longitude, op.latitude, op.longitude); float myHeading = estimatedHeading(p.latitude, p.longitude); headingRadian = bearingToOther - myHeading; } - const char *fields[] = { - username, - distStr, - signalStr, - lastStr, - NULL}; + const char *fields[] = {username, distStr, signalStr, lastStr, NULL}; drawColumns(display, x, y, fields); // coordinates for the center of the compass int16_t compassX = x + SCREEN_WIDTH - COMPASS_DIAM / 2 - 1, compassY = y + SCREEN_HEIGHT / 2; - // display->drawXbm(compassX, compassY, compass_width, compass_height, (const uint8_t *)compass_bits); + // 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; @@ -419,8 +361,7 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in Point *points[] = {&tip, &tail, &leftArrow, &rightArrow}; - for (int i = 0; i < 4; i++) - { + for (int i = 0; i < 4; i++) { points[i]->rotate(headingRadian); points[i]->scale(COMPASS_DIAM * 0.6); points[i]->translate(compassX, compassY); @@ -432,7 +373,7 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in display->drawCircle(compassX, compassY, COMPASS_DIAM / 2); } -void drawDebugInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +static void drawDebugInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { display->setFont(ArialMT_Plain_10); @@ -451,32 +392,17 @@ void drawDebugInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i static char gpsStr[20]; if (myNodeInfo.has_gps) - snprintf(gpsStr, sizeof(gpsStr), "GPS %d%%", 75); // FIXME, use something based on hdop + snprintf(gpsStr, sizeof(gpsStr), "GPS %d%%", + 75); // FIXME, use something based on hdop else gpsStr[0] = '\0'; // Just show emptystring - const char *fields[] = { - batStr, - gpsStr, - usersStr, - channelStr, - NULL}; + const char *fields[] = {batStr, gpsStr, usersStr, channelStr, NULL}; uint32_t yo = drawRows(display, x, y, fields); display->drawLogBuffer(x, yo); } -// This array keeps function pointers to all frames -// frames are the single views that slide in -FrameCallback bootFrames[] = {drawBootScreen}; - -// Overlays are statically drawn on top of a frame eg. a clock -OverlayCallback overlays[] = {/* msOverlay */}; - -// how many frames are there? -const int bootFrameCount = sizeof(bootFrames) / sizeof(bootFrames[0]); -const int overlaysCount = sizeof(overlays) / sizeof(overlays[0]); - #if 0 void _screen_header() { @@ -501,20 +427,21 @@ void _screen_header() } #endif -void Screen::setOn(bool on) +Screen::Screen(uint8_t address, uint8_t sda, uint8_t scl) + : cmdQueue(32), useDisplay(sda || scl), dispdev(address, sda, scl), ui(&dispdev) { - if (!disp) +} + +void Screen::handleSetOn(bool on) +{ + if (!useDisplay) return; - if (on != screenOn) - { - if (on) - { + if (on != screenOn) { + if (on) { DEBUG_MSG("Turning on screen\n"); dispdev.displayOn(); - setPeriod(1); // redraw ASAP - } - else { + } else { DEBUG_MSG("Turning off screen\n"); dispdev.displayOff(); } @@ -522,167 +449,134 @@ void Screen::setOn(bool on) } } -void screen_print(const char *text, uint8_t x, uint8_t y, uint8_t alignment) -{ - DEBUG_MSG(text); - - if (!disp) - return; - - dispdev.setTextAlignment((OLEDDISPLAY_TEXT_ALIGNMENT)alignment); - dispdev.drawString(x, y, text); -} - -void screen_print(const char *text) -{ - DEBUG_MSG("Screen: %s", text); - if (!disp) - return; - - dispdev.print(text); - // ui.update(); -} - void Screen::setup() { -#ifdef I2C_SDA - // Display instance - disp = true; - - // The ESP is capable of rendering 60fps in 80Mhz mode - // but that won't give you much time for anything else - // run it in 160Mhz mode or just set it to 30 fps - // We do this now in loop() - // ui.setTargetFPS(30); - - // Customize the active and inactive symbol - //ui.setActiveSymbol(activeSymbol); - //ui.setInactiveSymbol(inactiveSymbol); - - ui.setTimePerTransition(300); // msecs - - // You can change this to - // TOP, LEFT, BOTTOM, RIGHT - ui.setIndicatorPosition(BOTTOM); - - // Defines where the first frame is located in the bar. - ui.setIndicatorDirection(LEFT_RIGHT); - - // You can change the transition that is used - // SLIDE_LEFT, SLIDE_RIGHT, SLIDE_UP, SLIDE_DOWN - ui.setFrameAnimation(SLIDE_LEFT); - - // Add frames - we subtract one from the framecount so there won't be a visual glitch when we take the boot screen out of the sequence. - ui.setFrames(bootFrames, bootFrameCount); - - // Add overlays - ui.setOverlays(overlays, overlaysCount); - - // Initialising the UI will init the display too. - ui.init(); - - // Scroll buffer - dispdev.setLogBuffer(3, 32); - - setOn(true); // update our screenOn bool + if (!useDisplay) + return; +// TODO(girts): how many of the devices come with the bicolor displays? With +// this enabled, the logo looklooks nice, but the regular screens look a bit +// wacky as the text crosses the color boundary and there's a 1px gap. #ifdef BICOLOR_DISPLAY dispdev.flipScreenVertically(); // looks better without this on lora32 #endif - // dispdev.setFont(Custom_ArialMT_Plain_10); + // Initialising the UI will init the display too. + ui.init(); + ui.setTimePerTransition(300); // msecs + ui.setIndicatorPosition(BOTTOM); + // Defines where the first frame is located in the bar. + ui.setIndicatorDirection(LEFT_RIGHT); + ui.setFrameAnimation(SLIDE_LEFT); + // Don't show the page swipe dots while in boot screen. + ui.disableAllIndicators(); - ui.disableAutoTransition(); // we now require presses - ui.update(); // force an immediate draw of the bootscreen, because on some ssd1306 clones, the first draw command is discarded -#endif + // Add frames. + static FrameCallback bootFrames[] = {drawBootScreen}; + static const int bootFrameCount = sizeof(bootFrames) / sizeof(bootFrames[0]); + ui.setFrames(bootFrames, bootFrameCount); + // No overlays. + ui.setOverlays(nullptr, 0); + + // Require presses to switch between frames. + ui.disableAutoTransition(); + + // Set up a log buffer with 3 lines, 32 chars each. + dispdev.setLogBuffer(3, 32); + + // Turn on the display hardware. + handleSetOn(true); + + // On some ssd1306 clones, the first draw command is discarded, so draw it + // twice initially. + ui.update(); + ui.update(); } -#define TRANSITION_FRAMERATE 30 // fps -#define IDLE_FRAMERATE 10 // in fps - -static uint32_t targetFramerate = IDLE_FRAMERATE; - void Screen::doTask() { - if (!disp) - { // If we don't have a screen, don't ever spend any CPU for us + // If we don't have a screen, don't ever spend any CPU for us. + if (!useDisplay) { setPeriod(0); return; } - if (!screenOn) - { // If we didn't just wake and the screen is still off, then stop updating until it is on again + // Process incoming commands. + for (;;) { + CmdItem cmd; + if (!cmdQueue.dequeue(&cmd, 0)) { + break; + } + switch (cmd.cmd) { + case Cmd::SET_ON: + handleSetOn(true); + break; + case Cmd::SET_OFF: + handleSetOn(false); + break; + case Cmd::ON_PRESS: + handleOnPress(); + break; + case Cmd::START_BLUETOOTH_PIN_SCREEN: + handleStartBluetoothPinScreen(cmd.bluetooth_pin); + break; + case Cmd::STOP_BLUETOOTH_PIN_SCREEN: + case Cmd::STOP_BOOT_SCREEN: + setFrames(); + break; + case Cmd::PRINT: + handlePrint(cmd.print_text); + free(cmd.print_text); + break; + default: + DEBUG_MSG("BUG: invalid cmd"); + } + } + + if (!screenOn) { // If we didn't just wake and the screen is still off, then + // stop updating until it is on again setPeriod(0); return; } // Switch to a low framerate (to save CPU) when we are not in transition - // but we should only call setTargetFPS when framestate changes, because otherwise that breaks - // animations. - if (targetFramerate != IDLE_FRAMERATE && ui.getUiState()->frameState == FIXED) - { + // but we should only call setTargetFPS when framestate changes, because + // otherwise that breaks animations. + if (targetFramerate != IDLE_FRAMERATE && ui.getUiState()->frameState == FIXED) { // oldFrameState = ui.getUiState()->frameState; DEBUG_MSG("Setting idle framerate\n"); targetFramerate = IDLE_FRAMERATE; ui.setTargetFPS(targetFramerate); } - // While showing the bluetooth pair screen all of our standard screen switching is stopped - if (!showingBluetooth) - { - // Once we finish showing the bootscreen, remove it from the loop - if (showingBootScreen) - { - if (millis() > 3 * 1000) // we show the boot screen for a few seconds only - { - showingBootScreen = false; - setFrames(); - } - } - else // 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; - } + // 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; } } - // This must be after we possibly do screen_set_frames() to ensure we draw the new data ui.update(); - // DEBUG_MSG("want fps %d, fixed=%d\n", targetFramerate, ui.getUiState()->frameState); - // If we are scrolling we need to be called soon, otherwise just 1 fps (to save CPU) - // We also ask to be called twice as fast as we really need so that any rounding errors still result - // with the correct framerate + // DEBUG_MSG("want fps %d, fixed=%d\n", targetFramerate, + // ui.getUiState()->frameState); If we are scrolling we need to be called + // soon, otherwise just 1 fps (to save CPU) We also ask to be called twice + // as fast as we really need so that any rounding errors still result with + // the correct framerate setPeriod(1000 / targetFramerate); } -#include "PowerFSM.h" - -// Show the bluetooth PIN screen -void screen_start_bluetooth(uint32_t pin) -{ - static FrameCallback btFrames[] = {drawFrameBluetooth}; - - snprintf(btPIN, sizeof(btPIN), "%06d", pin); - - DEBUG_MSG("showing bluetooth screen\n"); - showingBluetooth = true; - powerFSM.trigger(EVENT_BLUETOOTH_PAIR); - - ui.setFrames(btFrames, 1); // Just show the bluetooth frame - // we rely on our main loop to show this screen (because we are invoked deep inside of bluetooth callbacks) - // ui.update(); // manually draw once, because I'm not sure if loop is getting called -} - // restore our regular frame list 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) @@ -693,28 +587,49 @@ void Screen::setFrames() // If we have a text message - show it first if (devicestate.has_rx_text_message) - nonBootFrames[numframes++] = drawTextMessageFrame; + normalFrames[numframes++] = drawTextMessageFrame; // then all the nodes for (size_t i = 0; i < numnodes; i++) - nonBootFrames[numframes++] = drawNodeInfo; + normalFrames[numframes++] = drawNodeInfo; // then the debug info - nonBootFrames[numframes++] = drawDebugInfo; + normalFrames[numframes++] = drawDebugInfo; - ui.setFrames(nonBootFrames, numframes); - showingBluetooth = false; + ui.setFrames(normalFrames, numframes); + ui.enableAllIndicators(); - prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed) + prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list + // just changed) } -/// handle press of the button -void Screen::onPress() +void Screen::handleStartBluetoothPinScreen(uint32_t pin) +{ + DEBUG_MSG("showing bluetooth screen\n"); + showingNormalScreen = false; + + static FrameCallback btFrames[] = {drawFrameBluetooth}; + + snprintf(btPIN, sizeof(btPIN), "%06d", pin); + + ui.disableAllIndicators(); + ui.setFrames(btFrames, 1); +} + +void Screen::handlePrint(const char *text) +{ + DEBUG_MSG("Screen: %s", text); + if (!useDisplay) + return; + + dispdev.print(text); +} + +void Screen::handleOnPress() { // If screen was off, just wake it, otherwise advance to next frame // If we are in a transition, the press must have bounced, drop it. - if (ui.getUiState()->frameState == FIXED) - { + if (ui.getUiState()->frameState == FIXED) { setPeriod(1); // redraw ASAP ui.nextFrame(); @@ -725,3 +640,5 @@ void Screen::onPress() ui.setTargetFPS(targetFramerate); } } + +} // namespace meshtastic diff --git a/src/screen.h b/src/screen.h index 9637f928..17c21793 100644 --- a/src/screen.h +++ b/src/screen.h @@ -1,44 +1,126 @@ #pragma once +#include + +#include +#include + #include "PeriodicTask.h" +#include "TypedQueue.h" -void screen_print(const char * text); -void screen_print(const char * text, uint8_t x, uint8_t y, uint8_t alignment); +namespace meshtastic +{ - -// Show the bluetooth PIN screen -void screen_start_bluetooth(uint32_t pin); - -// restore our regular frame list -void screen_set_frames(); - - -/** - * Slowly I'm moving screen crap into this class - */ +/// Deals with showing things on the screen of the device. +// +// Other than setup(), this class is thread-safe. All state-changing calls are +// queued and executed when the main loop calls us. class Screen : public PeriodicTask { -public: + public: + Screen(uint8_t address, uint8_t sda, uint8_t scl); + Screen(const Screen &) = delete; + Screen &operator=(const Screen &) = delete; + + /// Initializes the UI, turns on the display, starts showing boot screen. + // + // Not thread safe - must be called before any other methods are called. void setup(); - virtual void doTask(); + /// Turns the screen on/off. + void setOn(bool on) { enqueueCmd(CmdItem{.cmd = on ? Cmd::SET_ON : Cmd::SET_OFF}); } - /// Turn on the screen asap - void doWakeScreen(); + /// Handles a button press. + void onPress() { enqueueCmd(CmdItem{.cmd = Cmd::ON_PRESS}); } - /// Is the screen currently on - bool isOn(); + /// Starts showing the Bluetooth PIN screen. + // + // Switches over to a static frame showing the Bluetooth pairing screen + // with the PIN. + void startBluetoothPinScreen(uint32_t pin) + { + CmdItem cmd; + cmd.cmd = Cmd::START_BLUETOOTH_PIN_SCREEN; + cmd.bluetooth_pin = pin; + enqueueCmd(cmd); + } - /// Turn the screen on/off - void setOn(bool on); + /// Stops showing the bluetooth PIN screen. + void stopBluetoothPinScreen() { enqueueCmd(CmdItem{.cmd = Cmd::STOP_BLUETOOTH_PIN_SCREEN}); } - /// Handle a button press - void onPress(); + /// Stops showing the boot screen. + void stopBootScreen() { enqueueCmd(CmdItem{.cmd = Cmd::STOP_BOOT_SCREEN}); } - /// Rebuilt our list of screens + /// Writes a string to the screen. + void print(const char *text) + { + CmdItem cmd; + cmd.cmd = Cmd::PRINT; + // TODO(girts): strdup() here is scary, but we can't use std::string as + // FreeRTOS queue is just dumbly copying memory contents. It would be + // nice if we had a queue that could copy objects by value. + cmd.print_text = strdup(text); + if (!enqueueCmd(cmd)) { + free(cmd.print_text); + } + } + + protected: + /// Updates the UI. + // + // Called periodically from the main loop. + void doTask() final; + + private: + enum class Cmd { + INVALID, + SET_ON, + SET_OFF, + ON_PRESS, + START_BLUETOOTH_PIN_SCREEN, + STOP_BLUETOOTH_PIN_SCREEN, + STOP_BOOT_SCREEN, + PRINT, + }; + struct CmdItem { + Cmd cmd; + union { + uint32_t bluetooth_pin; + char *print_text; + }; + }; + + /// Enques given command item to be processed by main loop(). + bool enqueueCmd(const CmdItem &cmd) + { + bool success = cmdQueue.enqueue(cmd, 0); + setPeriod(1); // handle ASAP + return success; + } + + // Implementations of various commands, called from doTask(). + void handleSetOn(bool on); + void handleOnPress(); + void handleStartBluetoothPinScreen(uint32_t pin); + void handlePrint(const char *text); + + /// Rebuilds our list of frames (screens) to default ones. void setFrames(); -private: + + /// Queue of commands to execute in doTask. + TypedQueue cmdQueue; + /// Whether we are using a display + bool useDisplay = false; + /// Whether the display is currently powered + bool screenOn = false; + // Whether we are showing the regular screen (as opposed to booth screen or + // Bluetooth PIN screen) + bool showingNormalScreen = false; + /// Display device + SSD1306Wire dispdev; + /// UI helper for rendering to frames and switching between them + OLEDDisplayUi ui; }; -extern Screen screen; +} // namespace meshtastic diff --git a/src/sleep.cpp b/src/sleep.cpp index 5d13dc35..5133e91c 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -6,7 +6,6 @@ #include "MeshBluetoothService.h" #include "MeshService.h" #include "GPS.h" -#include "screen.h" #include "NodeDB.h" #include "Periodic.h" #include "esp32/pm.h"