Screen cleanups and refactoring

Work towards separating out how Screen interacts with other stuff.
* `Screen` should now be thread-safe. All commands to it are put in a
  queue and handled in `doTask` from the `loop()` task.
* Break dependency from `BluetoothUtil` to `Screen` by changing the
  pairing request into a callback.
* All accesses to screen now happen through the class.
* Fix `drawRows` so that the text scrolls along with frame animations.
* Remove example code that wasn't used.
This commit is contained in:
Girts Folkmanis 2020-03-15 16:47:38 -07:00
parent 5b54fd6359
commit daf8594b99
9 changed files with 356 additions and 288 deletions

View File

@ -5,7 +5,6 @@
#include <Arduino.h> #include <Arduino.h>
#include <Update.h> #include <Update.h>
#include "configuration.h" #include "configuration.h"
#include "screen.h"
SimpleAllocator btPool; SimpleAllocator btPool;
@ -173,7 +172,7 @@ uint32_t getValue32(BLECharacteristic *c, uint32_t defaultValue)
class MySecurity : public BLESecurityCallbacks class MySecurity : public BLESecurityCallbacks
{ {
protected:
bool onConfirmPIN(uint32_t pin) bool onConfirmPIN(uint32_t pin)
{ {
Serial.printf("onConfirmPIN %u\n", pin); Serial.printf("onConfirmPIN %u\n", pin);
@ -189,7 +188,7 @@ class MySecurity : public BLESecurityCallbacks
void onPassKeyNotify(uint32_t pass_key) void onPassKeyNotify(uint32_t pass_key)
{ {
Serial.printf("onPassKeyNotify %u\n", pass_key); Serial.printf("onPassKeyNotify %u\n", pass_key);
screen_start_bluetooth(pass_key); startCb(pass_key);
} }
bool onSecurityRequest() bool onSecurityRequest()
@ -211,9 +210,13 @@ class MySecurity : public BLESecurityCallbacks
Serial.printf("onAuthenticationComplete -> fail %d\n", cmpl.fail_reason); Serial.printf("onAuthenticationComplete -> fail %d\n", cmpl.fail_reason);
} }
// Remove our custom screen // Remove our custom PIN request screen.
screen.setFrames(); stopCb();
} }
public:
StartBluetoothPinScreenCallback startCb;
StopBluetoothPinScreenCallback stopCb;
}; };
BLEServer *pServer; BLEServer *pServer;
@ -255,7 +258,10 @@ void deinitBLE()
btPool.reset(); 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::init(deviceName);
BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT); 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 * Required in authentication process to provide displaying and/or input passkey or yes/no butttons confirmation
*/ */
static MySecurity mySecurity; static MySecurity mySecurity;
mySecurity.startCb = startBtPinScreen;
mySecurity.stopCb = stopBtPinScreen;
BLEDevice::setSecurityCallbacks(&mySecurity); BLEDevice::setSecurityCallbacks(&mySecurity);
// Create the BLE Server // Create the BLE Server

View File

@ -1,5 +1,7 @@
#pragma once #pragma once
#include <functional>
#include <Arduino.h> #include <Arduino.h>
#include <BLEDevice.h> #include <BLEDevice.h>
#include <BLEServer.h> #include <BLEServer.h>
@ -17,8 +19,14 @@ void dumpCharacteristic(BLECharacteristic *c);
/** converting endianness pull out a 32 bit value */ /** converting endianness pull out a 32 bit value */
uint32_t getValue32(BLECharacteristic *c, uint32_t defaultValue); uint32_t getValue32(BLECharacteristic *c, uint32_t defaultValue);
// TODO(girts): create a class for the bluetooth utils helpers?
using StartBluetoothPinScreenCallback = std::function<void(uint32_t pass_key)>;
using StopBluetoothPinScreenCallback = std::function<void(void)>;
void loopBLE(); 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(); void deinitBLE();
/// Add a characteristic that we will delete when we restart /// Add a characteristic that we will delete when we restart

View File

@ -2,12 +2,12 @@
#include <Arduino.h> #include <Arduino.h>
#include <assert.h> #include <assert.h>
#include "main.h"
#include "mesh-pb-constants.h" #include "mesh-pb-constants.h"
#include "MeshService.h" #include "MeshService.h"
#include "MeshBluetoothService.h" #include "MeshBluetoothService.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "GPS.h" #include "GPS.h"
#include "screen.h"
#include "Periodic.h" #include "Periodic.h"
#include "PowerFSM.h" #include "PowerFSM.h"
@ -118,7 +118,7 @@ MeshPacket *MeshService::handleFromRadioUser(MeshPacket *mp)
sendOurOwner(mp->from); sendOurOwner(mp->from);
String lcd = String("Joined: ") + mp->payload.variant.user.long_name + "\n"; String lcd = String("Joined: ") + mp->payload.variant.user.long_name + "\n";
screen_print(lcd.c_str()); screen.print(lcd.c_str());
} }
return mp; return mp;
@ -360,4 +360,4 @@ void MeshService::onNotify(Observable *o)
{ {
DEBUG_MSG("got gps notify\n"); DEBUG_MSG("got gps notify\n");
onGPSChanged(); onGPSChanged();
} }

View File

@ -179,4 +179,4 @@ void PowerFSM_setup()
// timeout"); // timeout");
powerFSM.run_machine(); // run one interation of the state machine, so we run our on enter tasks for the initial DARK state powerFSM.run_machine(); // run one interation of the state machine, so we run our on enter tasks for the initial DARK state
} }

View File

@ -44,6 +44,14 @@ AXP20X_Class axp;
bool pmu_irq = false; bool pmu_irq = false;
#endif #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 // these flags are all in bss so they default false
bool isCharging; bool isCharging;
bool isUSBPowered; bool isUSBPowered;
@ -221,8 +229,6 @@ void setup()
scanI2Cdevice(); scanI2Cdevice();
#endif #endif
axp192Init();
// Buttons & LED // Buttons & LED
#ifdef BUTTON_PIN #ifdef BUTTON_PIN
pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(BUTTON_PIN, INPUT_PULLUP);
@ -240,13 +246,16 @@ void setup()
if (wakeCause == ESP_SLEEP_WAKEUP_TIMER) if (wakeCause == ESP_SLEEP_WAKEUP_TIMER)
ssd1306_found = false; // forget we even have the hardware 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) if (ssd1306_found)
screen.setup(); screen.setup();
axp192Init();
// Init GPS // Init GPS
gps.setup(); gps.setup();
screen_print("Started...\n"); screen.print("Started...\n");
service.init(); service.init();
@ -264,7 +273,14 @@ void initBluetooth()
// FIXME - we are leaking like crazy // FIXME - we are leaking like crazy
// AllocatorScope scope(btPool); // 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); createMeshBluetoothService(serve);
// Start advertising - this must be done _after_ creating all services // Start advertising - this must be done _after_ creating all services
@ -392,6 +408,14 @@ void loop()
} }
#endif #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) // 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. // i.e. don't just keep spinning in loop as fast as we can.
//DEBUG_MSG("msecs %d\n", msecstosleep); //DEBUG_MSG("msecs %d\n", msecstosleep);

View File

@ -1,6 +1,11 @@
#pragma once #pragma once
#include "screen.h"
extern bool axp192_found; extern bool axp192_found;
extern bool ssd1306_found; extern bool ssd1306_found;
extern bool isCharging; extern bool isCharging;
extern bool isUSBPowered; extern bool isUSBPowered;
// Global Screen singleton.
extern meshtastic::Screen screen;

View File

@ -21,8 +21,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <OLEDDisplay.h> #include <OLEDDisplay.h>
#include <OLEDDisplayUi.h>
#include <SSD1306Wire.h>
#include <Wire.h> #include <Wire.h>
#include "GPS.h" #include "GPS.h"
@ -38,41 +36,21 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#define FONT_HEIGHT_16 (ArialMT_Plain_16[1] + 1) #define FONT_HEIGHT_16 (ArialMT_Plain_16[1] + 1)
#define SCREEN_WIDTH 128 #define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64 #define SCREEN_HEIGHT 64
#define TRANSITION_FRAMERATE 30 // fps
#ifdef I2C_SDA #define IDLE_FRAMERATE 10 // in fps
SSD1306Wire dispdev(SSD1306_ADDRESS, I2C_SDA, I2C_SCL); #define COMPASS_DIAM 44
#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 NUM_EXTRA_FRAMES 2 // text message and debug frame #define NUM_EXTRA_FRAMES 2 // text message and debug frame
namespace meshtastic
{
// A text message frame + debug frame + all the node infos // A text message frame + debug frame + all the node infos
FrameCallback nonBootFrames[MAX_NUM_NODES + NUM_EXTRA_FRAMES]; static FrameCallback normalFrames[MAX_NUM_NODES + NUM_EXTRA_FRAMES];
static uint32_t targetFramerate = IDLE_FRAMERATE;
static char btPIN[16] = "888888";
Screen screen; static void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
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)
{
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)
{ {
// draw an xbm image. // draw an xbm image.
// Please note that everything that should be transitioned // Please note that everything that should be transitioned
@ -83,16 +61,10 @@ void drawBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
display->setFont(ArialMT_Plain_16); display->setFont(ArialMT_Plain_16);
display->setTextAlignment(TEXT_ALIGN_CENTER); display->setTextAlignment(TEXT_ALIGN_CENTER);
display->drawString(64 + x, SCREEN_HEIGHT - FONT_HEIGHT_16, "meshtastic.org"); display->drawString(64 + x, SCREEN_HEIGHT - FONT_HEIGHT_16, "meshtastic.org");
ui.disableIndicator();
} }
static char btPIN[16] = "888888"; static void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
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->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(ArialMT_Plain_16); display->setFont(ArialMT_Plain_16);
display->drawString(64 + x, 2 + y, "Bluetooth"); display->drawString(64 + x, 2 + y, "Bluetooth");
@ -103,53 +75,19 @@ void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
display->setTextAlignment(TEXT_ALIGN_CENTER); display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(ArialMT_Plain_24); display->setFont(ArialMT_Plain_24);
display->drawString(64 + x, 22 + y, btPIN); 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 /// 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; MeshPacket &mp = devicestate.rx_text_message;
NodeInfo *node = nodeDB.getNode(mp.from); 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: // Demo for drawStringMaxWidth:
// with the third parameter you can define the width after which words will be wrapped. // with the third parameter you can define the width after which words will
// Currently only spaces and "-" are allowed for wrapping // be wrapped. Currently only spaces and "-" are allowed for wrapping
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
display->setFont(ArialMT_Plain_16); display->setFont(ArialMT_Plain_16);
String sender = (node && node->has_user) ? node->user.short_name : "???"; String sender = (node && node->has_user) ? node->user.short_name : "???";
@ -162,12 +100,10 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
mp.payload.variant.data.payload.bytes); mp.payload.variant.data.payload.bytes);
display->drawStringMaxWidth(4 + x, 10 + y, 128, tempBuf); display->drawStringMaxWidth(4 + x, 10 + y, 128, tempBuf);
// ui.disableIndicator();
} }
/// Draw a series of fields in a column, wrapping to multiple colums if needed /// 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 // The coordinates define the left starting point of the text
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
@ -187,19 +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 /// Draw a series of fields in a row, wrapping to multiple rows if needed
/// @return the max y we ended up printing to /// @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 // The coordinates define the left starting point of the text
display->setTextAlignment(TEXT_ALIGN_LEFT); display->setTextAlignment(TEXT_ALIGN_LEFT);
const char **f = fields; const char **f = fields;
int xo = x, yo = y; 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) { while (*f) {
display->drawString(xo, yo, *f); display->drawString(xo, yo, *f);
xo += SCREEN_WIDTH / 2; // hardwired for two columns per row.... xo += SCREEN_WIDTH / COLUMNS;
if (xo >= SCREEN_WIDTH) { // Wrap to next row, if needed.
if (++col > COLUMNS) {
xo = x;
yo += FONT_HEIGHT; yo += FONT_HEIGHT;
xo = 0; col = 0;
} }
f++; f++;
} }
@ -209,8 +149,9 @@ uint32_t drawRows(OLEDDisplay *display, int16_t x, int16_t y, const char **field
return yo; return yo;
} }
/// Ported from my old java code, returns distance in meters along the globe surface (by magic?) /// Ported from my old java code, returns distance in meters along the globe
float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b) /// surface (by magic?)
static float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b)
{ {
double pk = (180 / 3.14169); double pk = (180 / 3.14169);
double a1 = lat_a / pk; double a1 = lat_a / pk;
@ -229,18 +170,19 @@ float latLongToMeter(double lat_a, double lng_a, double lat_b, double lng_b)
return (float)(6366000 * tt); return (float)(6366000 * tt);
} }
inline double toRadians(double deg) static inline double toRadians(double deg)
{ {
return deg * PI / 180; return deg * PI / 180;
} }
inline double toDegrees(double r) static inline double toDegrees(double r)
{ {
return r * 180 / PI; return r * 180 / PI;
} }
/** /**
* Computes the bearing in degrees between two points on Earth. Ported from my old Gaggle android app. * Computes the bearing in degrees between two points on Earth. Ported from my
* old Gaggle android app.
* *
* @param lat1 * @param lat1
* Latitude of the first point * Latitude of the first point
@ -253,7 +195,7 @@ inline double toDegrees(double r)
* @return Bearing between the two points in radians. A value of 0 means due * @return Bearing between the two points in radians. A value of 0 means due
* north. * north.
*/ */
float bearing(double lat1, double lon1, double lat2, double lon2) static float bearing(double lat1, double lon1, double lat2, double lon2)
{ {
double lat1Rad = toRadians(lat1); double lat1Rad = toRadians(lat1);
double lat2Rad = toRadians(lat2); double lat2Rad = toRadians(lat2);
@ -263,6 +205,9 @@ float bearing(double lat1, double lon1, double lat2, double lon2)
return atan2(y, x); return atan2(y, x);
} }
namespace
{
/// A basic 2D point class for drawing /// A basic 2D point class for drawing
class Point class Point
{ {
@ -294,7 +239,9 @@ class Point
} }
}; };
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); d->drawLine(p1.x, p1.y, p2.x, p2.y);
} }
@ -302,9 +249,10 @@ void drawLine(OLEDDisplay *d, const Point &p1, const Point &p2)
/** /**
* Given a recent lat/lon return a guess of the heading the user is walking on. * 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 double oldLat, oldLon;
static float b; static float b;
@ -328,21 +276,22 @@ float estimatedHeading(double lat, double lon)
return b; return b;
} }
/// Sometimes we will have Position objects that only have a time, so check for valid lat/lon /// Sometimes we will have Position objects that only have a time, so check for
bool hasPosition(NodeInfo *n) /// valid lat/lon
static bool hasPosition(NodeInfo *n)
{ {
return n->has_position && (n->position.latitude != 0 || n->position.longitude != 0); 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 size_t nodeIndex;
static int8_t prevFrame = -1; 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 // We only advance our nodeIndex if the frame # has changed - because
// is shown // drawNodeInfo will be called repeatedly while the frame is shown
if (state->currentFrame != prevFrame) { if (state->currentFrame != prevFrame) {
prevFrame = state->currentFrame; prevFrame = state->currentFrame;
@ -376,7 +325,8 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in
snprintf(lastStr, sizeof(lastStr), "%d hours ago", agoSecs / 60 / 60); snprintf(lastStr, sizeof(lastStr), "%d hours ago", agoSecs / 60 / 60);
static float simRadian; 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]; static char distStr[20];
*distStr = 0; // might not have location data *distStr = 0; // might not have location data
@ -390,8 +340,8 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in
else else
snprintf(distStr, sizeof(distStr), "%.1f km", d / 1000); 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 // FIXME, also keep the guess at the operators heading and add/substract
// north up only. // 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 bearingToOther = bearing(p.latitude, p.longitude, op.latitude, op.longitude);
float myHeading = estimatedHeading(p.latitude, p.longitude); float myHeading = estimatedHeading(p.latitude, p.longitude);
headingRadian = bearingToOther - myHeading; headingRadian = bearingToOther - myHeading;
@ -402,7 +352,8 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in
// coordinates for the center of the compass // coordinates for the center of the compass
int16_t compassX = x + SCREEN_WIDTH - COMPASS_DIAM / 2 - 1, compassY = y + SCREEN_HEIGHT / 2; 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 Point tip(0.0f, 0.5f), tail(0.0f, -0.5f); // pointing up initially
float arrowOffsetX = 0.2f, arrowOffsetY = 0.2f; float arrowOffsetX = 0.2f, arrowOffsetY = 0.2f;
@ -422,7 +373,7 @@ void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, in
display->drawCircle(compassX, compassY, COMPASS_DIAM / 2); 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); display->setFont(ArialMT_Plain_10);
@ -441,7 +392,8 @@ void drawDebugInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i
static char gpsStr[20]; static char gpsStr[20];
if (myNodeInfo.has_gps) 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 else
gpsStr[0] = '\0'; // Just show emptystring gpsStr[0] = '\0'; // Just show emptystring
@ -451,17 +403,6 @@ void drawDebugInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, i
display->drawLogBuffer(x, yo); 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 #if 0
void _screen_header() void _screen_header()
{ {
@ -486,16 +427,20 @@ void _screen_header()
} }
#endif #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; return;
if (on != screenOn) { if (on != screenOn) {
if (on) { if (on) {
DEBUG_MSG("Turning on screen\n"); DEBUG_MSG("Turning on screen\n");
dispdev.displayOn(); dispdev.displayOn();
setPeriod(1); // redraw ASAP
} else { } else {
DEBUG_MSG("Turning off screen\n"); DEBUG_MSG("Turning off screen\n");
dispdev.displayOff(); dispdev.displayOff();
@ -504,102 +449,99 @@ 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() void Screen::setup()
{ {
#ifdef I2C_SDA if (!useDisplay)
// Display instance return;
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
// 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 #ifdef BICOLOR_DISPLAY
dispdev.flipScreenVertically(); // looks better without this on lora32 dispdev.flipScreenVertically(); // looks better without this on lora32
#endif #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 // Add frames.
ui.update(); // force an immediate draw of the bootscreen, because on some ssd1306 clones, the first draw command is discarded static FrameCallback bootFrames[] = {drawBootScreen};
#endif 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() 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); setPeriod(0);
return; 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); setPeriod(0);
return; return;
} }
// Switch to a low framerate (to save CPU) when we are not in transition // 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 // but we should only call setTargetFPS when framestate changes, because
// animations. // otherwise that breaks animations.
if (targetFramerate != IDLE_FRAMERATE && ui.getUiState()->frameState == FIXED) { if (targetFramerate != IDLE_FRAMERATE && ui.getUiState()->frameState == FIXED) {
// oldFrameState = ui.getUiState()->frameState; // oldFrameState = ui.getUiState()->frameState;
DEBUG_MSG("Setting idle framerate\n"); DEBUG_MSG("Setting idle framerate\n");
@ -607,58 +549,34 @@ void Screen::doTask()
ui.setTargetFPS(targetFramerate); ui.setTargetFPS(targetFramerate);
} }
// While showing the bluetooth pair screen all of our standard screen switching is stopped // While showing the bootscreen or Bluetooth pair screen all of our
if (!showingBluetooth) { // standard screen switching is stopped.
// Once we finish showing the bootscreen, remove it from the loop if (showingNormalScreen) {
if (showingBootScreen) { // TODO(girts): decouple nodeDB from screen.
if (millis() > 3 * 1000) // we show the boot screen for a few seconds only // standard screen loop handling ehre
{ // If the # nodes changes, we need to regen our list of screens
showingBootScreen = false; if (nodeDB.updateGUI || nodeDB.updateTextMessage) {
setFrames(); setFrames();
} nodeDB.updateGUI = false;
} else // standard screen loop handling ehre nodeDB.updateTextMessage = false;
{
// 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(); ui.update();
// DEBUG_MSG("want fps %d, fixed=%d\n", targetFramerate, ui.getUiState()->frameState); // DEBUG_MSG("want fps %d, fixed=%d\n", targetFramerate,
// If we are scrolling we need to be called soon, otherwise just 1 fps (to save CPU) // ui.getUiState()->frameState); If we are scrolling we need to be called
// We also ask to be called twice as fast as we really need so that any rounding errors still result // soon, otherwise just 1 fps (to save CPU) We also ask to be called twice
// with the correct framerate // as fast as we really need so that any rounding errors still result with
// the correct framerate
setPeriod(1000 / targetFramerate); 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 // restore our regular frame list
void Screen::setFrames() void Screen::setFrames()
{ {
DEBUG_MSG("showing standard frames\n"); DEBUG_MSG("showing standard frames\n");
showingNormalScreen = true;
size_t numnodes = nodeDB.getNumNodes(); size_t numnodes = nodeDB.getNumNodes();
// We don't show the node info our our node (if we have it yet - we should) // We don't show the node info our our node (if we have it yet - we should)
@ -669,23 +587,45 @@ void Screen::setFrames()
// If we have a text message - show it first // If we have a text message - show it first
if (devicestate.has_rx_text_message) if (devicestate.has_rx_text_message)
nonBootFrames[numframes++] = drawTextMessageFrame; normalFrames[numframes++] = drawTextMessageFrame;
// then all the nodes // then all the nodes
for (size_t i = 0; i < numnodes; i++) for (size_t i = 0; i < numnodes; i++)
nonBootFrames[numframes++] = drawNodeInfo; normalFrames[numframes++] = drawNodeInfo;
// then the debug info // then the debug info
nonBootFrames[numframes++] = drawDebugInfo; normalFrames[numframes++] = drawDebugInfo;
ui.setFrames(nonBootFrames, numframes); ui.setFrames(normalFrames, numframes);
showingBluetooth = false; 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::handleStartBluetoothPinScreen(uint32_t pin)
void Screen::onPress() {
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 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 we are in a transition, the press must have bounced, drop it.
@ -700,3 +640,5 @@ void Screen::onPress()
ui.setTargetFPS(targetFramerate); ui.setTargetFPS(targetFramerate);
} }
} }
} // namespace meshtastic

View File

@ -1,44 +1,126 @@
#pragma once #pragma once
#include <cstring>
#include <OLEDDisplayUi.h>
#include <SSD1306Wire.h>
#include "PeriodicTask.h" #include "PeriodicTask.h"
#include "TypedQueue.h"
void screen_print(const char * text); namespace meshtastic
void screen_print(const char * text, uint8_t x, uint8_t y, uint8_t alignment); {
/// Deals with showing things on the screen of the device.
// Show the bluetooth PIN screen //
void screen_start_bluetooth(uint32_t pin); // Other than setup(), this class is thread-safe. All state-changing calls are
// queued and executed when the main loop calls us.
// restore our regular frame list
void screen_set_frames();
/**
* Slowly I'm moving screen crap into this class
*/
class Screen : public PeriodicTask 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(); 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 /// Handles a button press.
void doWakeScreen(); void onPress() { enqueueCmd(CmdItem{.cmd = Cmd::ON_PRESS}); }
/// Is the screen currently on /// Starts showing the Bluetooth PIN screen.
bool isOn(); //
// 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 /// Stops showing the bluetooth PIN screen.
void setOn(bool on); void stopBluetoothPinScreen() { enqueueCmd(CmdItem{.cmd = Cmd::STOP_BLUETOOTH_PIN_SCREEN}); }
/// Handle a button press /// Stops showing the boot screen.
void onPress(); 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(); void setFrames();
private:
/// Queue of commands to execute in doTask.
TypedQueue<CmdItem> 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

View File

@ -6,7 +6,6 @@
#include "MeshBluetoothService.h" #include "MeshBluetoothService.h"
#include "MeshService.h" #include "MeshService.h"
#include "GPS.h" #include "GPS.h"
#include "screen.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "Periodic.h" #include "Periodic.h"
#include "esp32/pm.h" #include "esp32/pm.h"