mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-15 16:01:19 +10:00
Merge branch 'pioarduino' into crowpanel-p4
This commit is contained in:
@@ -21,8 +21,15 @@
|
||||
// How many messages are stored (RAM + flash).
|
||||
// Define -DMESSAGE_HISTORY_LIMIT=N in build_flags to control memory usage.
|
||||
#ifndef MESSAGE_HISTORY_LIMIT
|
||||
#if defined(ARCH_ESP32) && \
|
||||
!(defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32S2))
|
||||
// Baseline ESP32 (non-PSRAM variants) has limited heap; reduce message history on resource-constrained builds.
|
||||
// Override with -DMESSAGE_HISTORY_LIMIT=N if needed.
|
||||
#define MESSAGE_HISTORY_LIMIT 10
|
||||
#else
|
||||
#define MESSAGE_HISTORY_LIMIT 20
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Internal alias used everywhere in code – do NOT redefine elsewhere.
|
||||
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
|
||||
|
||||
+32
-3
@@ -47,6 +47,22 @@
|
||||
#include "concurrency/LockGuard.h"
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_STM32WL) && defined(BATTERY_PIN)
|
||||
#include "stm32yyxx_ll_adc.h"
|
||||
|
||||
/* Analog read resolution */
|
||||
#if defined(LL_ADC_RESOLUTION_12B)
|
||||
#define LL_ADC_RESOLUTION LL_ADC_RESOLUTION_12B
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#elif defined(LL_ADC_DS_DATA_WIDTH_12_BIT)
|
||||
#define LL_ADC_RESOLUTION LL_ADC_DS_DATA_WIDTH_12_BIT
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 12
|
||||
#else
|
||||
#error "ADC resolution could not be defined!"
|
||||
#endif
|
||||
#define ADC_RANGE (1 << BATTERY_SENSE_RESOLUTION_BITS)
|
||||
#endif
|
||||
|
||||
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
|
||||
#include "mqtt/MQTT.h"
|
||||
#include "target_specific.h"
|
||||
@@ -399,7 +415,13 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
float scaled = 0;
|
||||
|
||||
battery_adcEnable();
|
||||
#ifdef ARCH_ESP32 // ADC block for espressif platforms
|
||||
#ifdef ARCH_STM32WL
|
||||
// STM32 ADC with VREFINT runtime calibration
|
||||
Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
|
||||
raw = analogRead(BATTERY_PIN);
|
||||
scaled = __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, raw, LL_ADC_RESOLUTION);
|
||||
scaled *= operativeAdcMultiplier;
|
||||
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
|
||||
raw = espAdcRead();
|
||||
int voltage_mv = 0;
|
||||
if (adc_calibrated && adc_cali_handle) {
|
||||
@@ -584,6 +606,11 @@ class AnalogBatteryLevel : public HasBatteryLevel
|
||||
bool initial_read_done = false;
|
||||
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
|
||||
uint32_t last_read_time_ms = 0;
|
||||
#ifdef ARCH_STM32WL
|
||||
// 3300mV placeholder for STM32 errata where VREFINT factory calibration may be missing
|
||||
// (e.g. STM32U0, see DS14756 Rev 3 §2.4.1 "VREFINT offset")
|
||||
uint32_t Vref = 3300;
|
||||
#endif
|
||||
|
||||
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
|
||||
|
||||
@@ -693,7 +720,9 @@ bool Power::analogInit()
|
||||
#define BATTERY_SENSE_RESOLUTION_BITS 10
|
||||
#endif
|
||||
|
||||
#ifdef ARCH_ESP32 // ESP32 needs special analog stuff
|
||||
#ifdef ARCH_STM32WL
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
|
||||
adc_oneshot_unit_init_cfg_t init_config = {
|
||||
.unit_id = unit,
|
||||
};
|
||||
@@ -722,7 +751,7 @@ bool Power::analogInit()
|
||||
|
||||
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
|
||||
|
||||
#ifndef ARCH_ESP32
|
||||
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32WL)
|
||||
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ SerialConsole *console;
|
||||
|
||||
void consoleInit()
|
||||
{
|
||||
if (console) {
|
||||
return;
|
||||
}
|
||||
auto sc = new SerialConsole(); // Must be dynamically allocated because we are now inheriting from thread
|
||||
|
||||
#if defined(SERIAL_HAS_ON_RECEIVE)
|
||||
|
||||
@@ -31,6 +31,10 @@ class ScanI2C
|
||||
INA3221,
|
||||
MAX17048,
|
||||
MCP9808,
|
||||
SHT31,
|
||||
SHT4X,
|
||||
SHTC3,
|
||||
SHTXX,
|
||||
LPS22HB,
|
||||
QMC6310U,
|
||||
QMC6310N,
|
||||
@@ -86,13 +90,13 @@ class ScanI2C
|
||||
DA217,
|
||||
CHSC6X,
|
||||
CST226SE,
|
||||
CST3530,
|
||||
BMI270,
|
||||
SEN5X,
|
||||
SFA30,
|
||||
CW2015,
|
||||
SCD30,
|
||||
ADS1115,
|
||||
SHTXX
|
||||
} DeviceType;
|
||||
|
||||
// typedef uint8_t DeviceAddress;
|
||||
|
||||
@@ -629,7 +629,31 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
|
||||
SCAN_SIMPLE_CASE(PCT2075_ADDR, PCT2075, "PCT2075", (uint8_t)addr.address);
|
||||
SCAN_SIMPLE_CASE(SCD30_ADDR, SCD30, "SCD30", (uint8_t)addr.address);
|
||||
case CST328_ADDR:
|
||||
// Do we have the CST328 or the CST226SE
|
||||
// Do we have the CST328 or the CST226SE,CST3530
|
||||
{
|
||||
// T-Deck pro V1.1 new touch panel use CST3530
|
||||
int retry = 5;
|
||||
while (retry--) {
|
||||
uint8_t buffer[7];
|
||||
uint8_t r_cmd[] = {0x0d0, 0x03, 0x00, 0x00};
|
||||
i2cBus->beginTransmission(addr.address);
|
||||
i2cBus->write(r_cmd, sizeof(r_cmd));
|
||||
if (i2cBus->endTransmission() == 0) {
|
||||
i2cBus->requestFrom((int)addr.address, 7);
|
||||
i2cBus->readBytes(buffer, 7);
|
||||
if (buffer[2] == 0xCA && buffer[3] == 0xCA) {
|
||||
logFoundDevice("CST3530", (uint8_t)addr.address);
|
||||
type = CST3530;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t cmd1[] = {0xD0, 0x00, 0x04, 0x00};
|
||||
i2cBus->beginTransmission(addr.address);
|
||||
i2cBus->write(cmd1, sizeof(cmd1));
|
||||
i2cBus->endTransmission();
|
||||
delay(50);
|
||||
}
|
||||
}
|
||||
registerValue = getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0xAB), 1);
|
||||
if (registerValue == 0xA9) {
|
||||
type = CST226SE;
|
||||
|
||||
@@ -104,8 +104,13 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit)
|
||||
// End the update process - virtual method, overridden in derived class
|
||||
void EInkDisplay::endUpdate()
|
||||
{
|
||||
// Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep)
|
||||
#ifndef EINK_NOT_HIBERNATE
|
||||
// By default, power off the E-Ink display hardware and enter hibernate().
|
||||
// Boards/panels that define EINK_NOT_HIBERNATE intentionally skip this step.
|
||||
// Skipping hibernate() can help avoid panel-specific wake/refresh or ghosting issues,
|
||||
// but it typically trades lower power savings for that compatibility.
|
||||
adafruitDisplay->hibernate();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Write the buffer to the display memory
|
||||
|
||||
+184
-7
@@ -60,6 +60,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "main.h"
|
||||
#include "mesh-pb-constants.h"
|
||||
#include "mesh/Channels.h"
|
||||
#include "mesh/Default.h"
|
||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
@@ -98,6 +99,7 @@ namespace graphics
|
||||
|
||||
// This means the *visible* area (sh1106 can address 132, but shows 128 for example)
|
||||
#define IDLE_FRAMERATE 1 // in fps
|
||||
#define COMPASS_ACTIVE_FRAMERATE 20
|
||||
|
||||
// DEBUG
|
||||
#define NUM_EXTRA_FRAMES 3 // text message and debug frame
|
||||
@@ -135,6 +137,60 @@ static bool heartbeat = false;
|
||||
|
||||
extern bool hasUnreadMessage;
|
||||
|
||||
static inline float wrapHeading360(float heading)
|
||||
{
|
||||
if (heading < 0.0f) {
|
||||
heading += 360.0f;
|
||||
} else if (heading >= 360.0f) {
|
||||
heading -= 360.0f;
|
||||
}
|
||||
return heading;
|
||||
}
|
||||
|
||||
void Screen::setHeading(float heading)
|
||||
{
|
||||
const float wrappedHeading = wrapHeading360(heading);
|
||||
|
||||
if (!hasCompass) {
|
||||
hasCompass = true;
|
||||
compassHeading = wrappedHeading;
|
||||
return;
|
||||
}
|
||||
|
||||
// Interpolate using shortest-path angular delta to avoid jumps around 0/360.
|
||||
float delta = wrappedHeading - compassHeading;
|
||||
if (delta > 180.0f) {
|
||||
delta -= 360.0f;
|
||||
} else if (delta < -180.0f) {
|
||||
delta += 360.0f;
|
||||
}
|
||||
|
||||
// Adaptive filtering:
|
||||
// - Strong damping for tiny deltas (jitter)
|
||||
// - Faster response for larger turns
|
||||
const float absDelta = (delta >= 0.0f) ? delta : -delta;
|
||||
if (absDelta < 1.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
float alpha = 0.35f;
|
||||
if (absDelta > 25.0f) {
|
||||
alpha = 0.85f;
|
||||
} else if (absDelta > 10.0f) {
|
||||
alpha = 0.65f;
|
||||
}
|
||||
|
||||
float step = delta * alpha;
|
||||
const float maxStep = 12.0f;
|
||||
if (step > maxStep) {
|
||||
step = maxStep;
|
||||
} else if (step < -maxStep) {
|
||||
step = -maxStep;
|
||||
}
|
||||
|
||||
compassHeading = wrapHeading360(compassHeading + step);
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// Overlay Alert Banner Renderer
|
||||
// ==============================
|
||||
@@ -272,10 +328,25 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
||||
float Screen::estimatedHeading(double lat, double lon)
|
||||
{
|
||||
static double oldLat, oldLon;
|
||||
static float b;
|
||||
static float b = -1.0f;
|
||||
static uint32_t lastHeadingAtMs = 0;
|
||||
const uint32_t now = millis();
|
||||
const uint32_t gpsUpdateIntervalSecs =
|
||||
Default::getConfiguredOrDefault(config.position.gps_update_interval, default_gps_update_interval);
|
||||
uint32_t effectiveUpdateIntervalSecs = gpsUpdateIntervalSecs;
|
||||
if (config.position.position_broadcast_smart_enabled) {
|
||||
const uint32_t smartMinIntervalSecs = Default::getConfiguredOrDefault(
|
||||
config.position.broadcast_smart_minimum_interval_secs, default_broadcast_smart_minimum_interval_secs);
|
||||
if (smartMinIntervalSecs > effectiveUpdateIntervalSecs) {
|
||||
effectiveUpdateIntervalSecs = smartMinIntervalSecs;
|
||||
}
|
||||
}
|
||||
// Two expected update windows; keep arithmetic 32-bit to avoid pulling in larger 64-bit helpers.
|
||||
const uint32_t headingStaleMs =
|
||||
(effectiveUpdateIntervalSecs > (UINT32_MAX / 2000U)) ? UINT32_MAX : (effectiveUpdateIntervalSecs * 2000U);
|
||||
|
||||
if (oldLat == 0) {
|
||||
// just prepare for next time
|
||||
// Need at least two position points before we can infer heading.
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
|
||||
@@ -283,12 +354,20 @@ float Screen::estimatedHeading(double lat, double lon)
|
||||
}
|
||||
|
||||
float d = GeoCoord::latLongToMeter(oldLat, oldLon, lat, lon);
|
||||
if (d < 10) // haven't moved enough, just keep current bearing
|
||||
if (d < 10) { // haven't moved enough, keep previous heading (invalid until first real movement)
|
||||
if (lastHeadingAtMs != 0 && (now - lastHeadingAtMs) >= headingStaleMs) {
|
||||
// Heading is stale after prolonged no-movement; force reacquire.
|
||||
b = -1.0f;
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
b = GeoCoord::bearing(oldLat, oldLon, lat, lon) * RAD_TO_DEG;
|
||||
oldLat = lat;
|
||||
oldLon = lon;
|
||||
lastHeadingAtMs = now;
|
||||
|
||||
return b;
|
||||
}
|
||||
@@ -923,9 +1002,22 @@ int32_t Screen::runOnce()
|
||||
// but we should only call setTargetFPS when framestate changes, because
|
||||
// otherwise that breaks animations.
|
||||
|
||||
if (targetFramerate != IDLE_FRAMERATE && ui->getUiState()->frameState == FIXED) {
|
||||
uint32_t desiredFramerate = IDLE_FRAMERATE;
|
||||
#if HAS_GPS && !defined(USE_EINK)
|
||||
if (showingNormalScreen && hasCompass) {
|
||||
const uint8_t currentFrame = ui->getUiState()->currentFrame;
|
||||
if ((framesetInfo.positions.gps != 255 && currentFrame == framesetInfo.positions.gps) ||
|
||||
(framesetInfo.positions.waypoint != 255 && currentFrame == framesetInfo.positions.waypoint) ||
|
||||
(framesetInfo.positions.firstFavorite != 255 && currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||
currentFrame <= framesetInfo.positions.lastFavorite)) {
|
||||
desiredFramerate = COMPASS_ACTIVE_FRAMERATE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (targetFramerate != desiredFramerate && ui->getUiState()->frameState == FIXED) {
|
||||
// oldFrameState = ui->getUiState()->frameState;
|
||||
targetFramerate = IDLE_FRAMERATE;
|
||||
targetFramerate = desiredFramerate;
|
||||
|
||||
ui->setTargetFPS(targetFramerate);
|
||||
forceDisplay();
|
||||
@@ -1197,7 +1289,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
const meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||
if (n && n->num != nodeDB->getNodeNum() && n->is_favorite) {
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawNodeInfo);
|
||||
favoriteFrames.push_back(graphics::UIRenderer::drawFavoriteNode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1226,7 +1318,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawBannercallback};
|
||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||
|
||||
prevFrame = -1; // Force drawNodeInfo to pick a new node (because our list just changed)
|
||||
prevFrame = -1; // Force drawFavoriteNode to pick a new node (because our list just changed)
|
||||
|
||||
// Focus on a specific frame, in the frame set we just created
|
||||
switch (focus) {
|
||||
@@ -1265,6 +1357,10 @@ void Screen::setFrames(FrameFocus focus)
|
||||
// Store the info about this frameset, for future setFrames calls
|
||||
this->framesetInfo = fsi;
|
||||
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
logFrameChange("rebuild", ui->getUiState()->currentFrame);
|
||||
#endif
|
||||
|
||||
setFastFramerate(); // Draw ASAP
|
||||
}
|
||||
|
||||
@@ -1419,11 +1515,77 @@ void Screen::handleOnPress()
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
void Screen::logFrameChange(const char *reason, uint8_t targetIdx)
|
||||
{
|
||||
// Reverse-map an index to a stable name string keyed off FramePositions
|
||||
// field names — so the pytest harness can assert `name=nodelist_nodes`
|
||||
// without caring about how the positions were ordered this boot.
|
||||
const auto &p = framesetInfo.positions;
|
||||
const char *name = "unknown";
|
||||
if (targetIdx == p.home)
|
||||
name = "home";
|
||||
else if (targetIdx == p.deviceFocused)
|
||||
name = "deviceFocused";
|
||||
else if (targetIdx == p.textMessage)
|
||||
name = "textMessage";
|
||||
else if (targetIdx == p.nodelist_nodes)
|
||||
name = "nodelist_nodes";
|
||||
else if (targetIdx == p.nodelist_location)
|
||||
name = "nodelist_location";
|
||||
else if (targetIdx == p.nodelist_lastheard)
|
||||
name = "nodelist_lastheard";
|
||||
else if (targetIdx == p.nodelist_hopsignal)
|
||||
name = "nodelist_hopsignal";
|
||||
else if (targetIdx == p.nodelist_distance)
|
||||
name = "nodelist_distance";
|
||||
else if (targetIdx == p.nodelist_bearings)
|
||||
name = "nodelist_bearings";
|
||||
else if (targetIdx == p.system)
|
||||
name = "system";
|
||||
else if (targetIdx == p.gps)
|
||||
name = "gps";
|
||||
else if (targetIdx == p.lora)
|
||||
name = "lora";
|
||||
else if (targetIdx == p.clock)
|
||||
name = "clock";
|
||||
else if (targetIdx == p.chirpy)
|
||||
name = "chirpy";
|
||||
else if (targetIdx == p.fault)
|
||||
name = "fault";
|
||||
else if (targetIdx == p.waypoint)
|
||||
name = "waypoint";
|
||||
else if (targetIdx == p.focusedModule)
|
||||
name = "focusedModule";
|
||||
else if (targetIdx == p.log)
|
||||
name = "log";
|
||||
else if (targetIdx == p.settings)
|
||||
name = "settings";
|
||||
else if (targetIdx == p.wifi)
|
||||
name = "wifi";
|
||||
else if (p.firstFavorite != 255 && p.lastFavorite != 255 && targetIdx >= p.firstFavorite && targetIdx <= p.lastFavorite)
|
||||
name = "favorite";
|
||||
LOG_INFO("Screen: frame %u/%u name=%s reason=%s", (unsigned)targetIdx, (unsigned)framesetInfo.frameCount, name, reason);
|
||||
}
|
||||
#endif
|
||||
|
||||
void Screen::showFrame(FrameDirection direction)
|
||||
{
|
||||
// Only advance frames when UI is stable
|
||||
if (ui->getUiState()->frameState == FIXED) {
|
||||
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
// Log the *intended* target before the (async) transition fires, so
|
||||
// tests see a deterministic record of what was requested.
|
||||
if (framesetInfo.frameCount > 0) {
|
||||
uint8_t curr = ui->getUiState()->currentFrame;
|
||||
uint8_t target = (direction == FrameDirection::NEXT)
|
||||
? (uint8_t)((curr + 1) % framesetInfo.frameCount)
|
||||
: (uint8_t)((curr + framesetInfo.frameCount - 1) % framesetInfo.frameCount);
|
||||
logFrameChange(direction == FrameDirection::NEXT ? "next" : "prev", target);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (direction == FrameDirection::NEXT) {
|
||||
ui->nextFrame();
|
||||
} else {
|
||||
@@ -1755,22 +1917,37 @@ int Screen::handleInputEvent(const InputEvent *event)
|
||||
showFrame(FrameDirection::NEXT);
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F1) {
|
||||
this->ui->switchToFrame(0);
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
logFrameChange("fn_f1", 0);
|
||||
#endif
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F2) {
|
||||
this->ui->switchToFrame(1);
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
logFrameChange("fn_f2", 1);
|
||||
#endif
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F3) {
|
||||
this->ui->switchToFrame(2);
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
logFrameChange("fn_f3", 2);
|
||||
#endif
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F4) {
|
||||
this->ui->switchToFrame(3);
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
logFrameChange("fn_f4", 3);
|
||||
#endif
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_FN_F5) {
|
||||
this->ui->switchToFrame(4);
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
logFrameChange("fn_f5", 4);
|
||||
#endif
|
||||
lastScreenTransition = millis();
|
||||
setFastFramerate();
|
||||
} else if (event->inputEvent == INPUT_BROKER_UP_LONG) {
|
||||
|
||||
+13
-7
@@ -330,15 +330,11 @@ class Screen : public concurrency::OSThread
|
||||
|
||||
// Function to allow the AccelerometerThread to set the heading if a sensor provides it
|
||||
// Mutex needed?
|
||||
void setHeading(long _heading)
|
||||
{
|
||||
hasCompass = true;
|
||||
compassHeading = fmod(_heading, 360);
|
||||
}
|
||||
void setHeading(float heading);
|
||||
|
||||
bool hasHeading() { return hasCompass; }
|
||||
|
||||
long getHeading() { return compassHeading; }
|
||||
float getHeading() { return compassHeading; }
|
||||
|
||||
void setEndCalibration(uint32_t _endCalibrationAt) { endCalibrationAt = _endCalibrationAt; }
|
||||
uint32_t getEndCalibration() { return endCalibrationAt; }
|
||||
@@ -673,6 +669,16 @@ class Screen : public concurrency::OSThread
|
||||
void handleOnPress();
|
||||
void handleStartFirmwareUpdateScreen();
|
||||
|
||||
#ifdef USERPREFS_UI_TEST_LOG
|
||||
// Test-only: emits one LOG_INFO line on every frame transition so the
|
||||
// pytest harness can assert which frame is shown. Gated behind a macro
|
||||
// so the chatty log doesn't ship in release builds. Enabled via
|
||||
// build_testing_profile(enable_ui_log=True) in mcp-server/userprefs.py.
|
||||
// Member function (not free) because FramesetInfo is a private nested
|
||||
// type — only methods of Screen can reach it.
|
||||
void logFrameChange(const char *reason, uint8_t targetIdx);
|
||||
#endif
|
||||
|
||||
// Info collected by setFrames method.
|
||||
// Index location of specific frames.
|
||||
// - Used to apply the FrameFocus parameter of setFrames
|
||||
@@ -782,4 +788,4 @@ extern std::vector<std::string> functionSymbol;
|
||||
extern std::string functionSymbolString;
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
#include "configuration.h"
|
||||
#if HAS_SCREEN
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "UIRenderer.h"
|
||||
#include "configuration.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include <cmath>
|
||||
@@ -21,8 +17,8 @@ struct Point {
|
||||
|
||||
void rotate(float angle)
|
||||
{
|
||||
float cos_a = cos(angle);
|
||||
float sin_a = sin(angle);
|
||||
float cos_a = cosf(angle);
|
||||
float sin_a = sinf(angle);
|
||||
float new_x = x * cos_a - y * sin_a;
|
||||
float new_y = x * sin_a + y * cos_a;
|
||||
x = new_x;
|
||||
@@ -51,21 +47,30 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
radius += 4;
|
||||
}
|
||||
Point north(0, -radius);
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
north.rotate(-myHeading);
|
||||
north.translate(compassX, compassY);
|
||||
float northX = 0.0f;
|
||||
float northY = -radius;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING) {
|
||||
const float c = cosf(-myHeading);
|
||||
const float s = sinf(-myHeading);
|
||||
const float rx = northX * c - northY * s;
|
||||
const float ry = northX * s + northY * c;
|
||||
northX = rx;
|
||||
northY = ry;
|
||||
}
|
||||
northX += compassX;
|
||||
northY += compassY;
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setColor(BLACK);
|
||||
const int16_t nLabelWidth = display->getStringWidth("N");
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||
display->fillRect(northX - 8, northY - 1, nLabelWidth + 3, FONT_HEIGHT_SMALL - 6);
|
||||
} else {
|
||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||
display->fillRect(northX - 4, northY - 1, nLabelWidth + 2, FONT_HEIGHT_SMALL - 6);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
display->drawString(north.x, north.y - 3, "N");
|
||||
display->drawString(northX, northY - 3, "N");
|
||||
}
|
||||
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||
@@ -113,11 +118,46 @@ void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, f
|
||||
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
||||
}
|
||||
|
||||
float estimatedHeading(double lat, double lon)
|
||||
bool getHeadingRadians(double lat, double lon, float &headingRadian)
|
||||
{
|
||||
// Simple magnetic declination estimation
|
||||
// This is a very basic implementation - the original might be more sophisticated
|
||||
return 0.0f; // Return 0 for now, indicating no heading available
|
||||
headingRadian = 0.0f;
|
||||
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING)
|
||||
return true;
|
||||
|
||||
if (!screen)
|
||||
return false;
|
||||
|
||||
if (screen->hasHeading()) {
|
||||
headingRadian = screen->getHeading() * DEG_TO_RAD;
|
||||
return true;
|
||||
}
|
||||
|
||||
const float estimatedHeadingDeg = screen->estimatedHeading(lat, lon);
|
||||
if (!(estimatedHeadingDeg >= 0.0f))
|
||||
return false;
|
||||
|
||||
headingRadian = estimatedHeadingDeg * DEG_TO_RAD;
|
||||
return true;
|
||||
}
|
||||
|
||||
float adjustBearingForCompassMode(float bearingRadian, float headingRadian)
|
||||
{
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
return bearingRadian - headingRadian;
|
||||
|
||||
return bearingRadian;
|
||||
}
|
||||
|
||||
float radiansToDegrees360(float angleRadian)
|
||||
{
|
||||
constexpr float fullTurnDeg = 360.0f;
|
||||
float degrees = angleRadian * RAD_TO_DEG;
|
||||
if (degrees < 0.0f)
|
||||
degrees += fullTurnDeg;
|
||||
else if (degrees >= fullTurnDeg)
|
||||
degrees -= fullTurnDeg;
|
||||
return degrees;
|
||||
}
|
||||
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
@@ -137,4 +177,4 @@ uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
|
||||
} // namespace CompassRenderer
|
||||
} // namespace graphics
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "graphics/Screen.h"
|
||||
#include "mesh/generated/meshtastic/mesh.pb.h"
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
@@ -25,7 +24,9 @@ void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, u
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing);
|
||||
|
||||
// Navigation and location functions
|
||||
float estimatedHeading(double lat, double lon);
|
||||
bool getHeadingRadians(double lat, double lon, float &headingRadian);
|
||||
float adjustBearingForCompassMode(float bearingRadian, float headingRadian);
|
||||
float radiansToDegrees360(float angleRadian);
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight);
|
||||
|
||||
} // namespace CompassRenderer
|
||||
|
||||
@@ -422,6 +422,17 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
std::vector<bool> isMine; // track alignment
|
||||
std::vector<bool> isHeader; // track header lines
|
||||
std::vector<AckStatus> ackForLine;
|
||||
// Hard limit on total cached lines to prevent unbounded growth from a single long message.
|
||||
// Reserve to the actual cache cap up front, because a single message can expand to many more
|
||||
// wrapped display lines than a small per-message estimate would predict. For a display
|
||||
// rendering only ~5-30 lines at a time, caching more than this limit wastes heap. Stop
|
||||
// appending once we reach MAX_CACHED_LINES to prevent a single message from blowing out the
|
||||
// heap.
|
||||
constexpr size_t MAX_CACHED_LINES = 100U; // ~5-6KB for std::string overhead on 32-bit (if each ~50-60 bytes avg)
|
||||
allLines.reserve(MAX_CACHED_LINES);
|
||||
isMine.reserve(MAX_CACHED_LINES);
|
||||
isHeader.reserve(MAX_CACHED_LINES);
|
||||
ackForLine.reserve(MAX_CACHED_LINES);
|
||||
|
||||
for (auto it = filtered.rbegin(); it != filtered.rend(); ++it) {
|
||||
const auto &m = *it;
|
||||
@@ -565,16 +576,23 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
|
||||
int wrapWidth = mine ? rightTextWidth : leftTextWidth;
|
||||
std::vector<std::string> wrapped = generateLines(display, "", msgText, wrapWidth);
|
||||
// Per-message wrap-line limit: even if wrapping produces many lines, cap them to prevent
|
||||
// a single long message from consuming most or all of the cache.
|
||||
constexpr size_t MAX_WRAPPED_LINES_PER_MSG = 20U;
|
||||
size_t wrappedCount = 0;
|
||||
for (auto &ln : wrapped) {
|
||||
allLines.push_back(ln);
|
||||
if (allLines.size() >= MAX_CACHED_LINES || wrappedCount >= MAX_WRAPPED_LINES_PER_MSG)
|
||||
break; // Cache limit or per-message limit reached; stop adding lines from this message
|
||||
allLines.emplace_back(std::move(ln));
|
||||
isMine.push_back(mine);
|
||||
isHeader.push_back(false);
|
||||
ackForLine.push_back(AckStatus::NONE);
|
||||
++wrappedCount;
|
||||
}
|
||||
}
|
||||
|
||||
// Cache lines and heights
|
||||
cachedLines = allLines;
|
||||
cachedLines.swap(allLines);
|
||||
cachedHeights = calculateLineHeights(cachedLines, emotes, isHeader);
|
||||
|
||||
std::vector<MessageBlock> blocks = buildMessageBlocks(isHeader, isMine);
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "CompassRenderer.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
#include "gps/RTC.h" // for getTime() function
|
||||
@@ -92,8 +95,41 @@ std::string getSafeNodeName(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
|
||||
// 1) Choose target candidate (long vs short) only if present
|
||||
const char *raw = nullptr;
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
// If long-name mode is enabled, and we have a recent status for this node,
|
||||
// prefer "(short_name) statusText" as the raw candidate.
|
||||
std::string composedFromStatus;
|
||||
if (config.display.use_long_node_name && node && node->has_user && statusMessageModule) {
|
||||
const auto &recent = statusMessageModule->getRecentReceived();
|
||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||
found = &(*it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
const char *shortName = node->user.short_name;
|
||||
composedFromStatus.reserve(4 + (shortName ? std::strlen(shortName) : 0) + 1 + found->statusText.size());
|
||||
composedFromStatus += "(";
|
||||
if (shortName && *shortName) {
|
||||
composedFromStatus += shortName;
|
||||
}
|
||||
composedFromStatus += ") ";
|
||||
composedFromStatus += found->statusText;
|
||||
|
||||
raw = composedFromStatus.c_str(); // safe for now; we'll sanitize immediately into std::string
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// If we didn't compose from status, use normal long/short selection
|
||||
if (!raw) {
|
||||
if (node && node->has_user) {
|
||||
raw = config.display.use_long_node_name ? node->user.long_name : node->user.short_name;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Preserve UTF-8 names so emotes can be detected and rendered.
|
||||
@@ -239,9 +275,12 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
|
||||
int nameMaxWidth = getNodeNameMaxWidth(columnWidth, columnWidth - 25);
|
||||
int barsOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||
int hopOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||
constexpr int kBarCount = 4;
|
||||
constexpr int kBarWidth = 2;
|
||||
constexpr int kBarGap = 1;
|
||||
|
||||
int barsXOffset = columnWidth - barsOffset;
|
||||
int barsRightEdge = x + barsXOffset + ((kBarCount - 1) * (kBarWidth + kBarGap)) + kBarWidth;
|
||||
|
||||
const int nameX = x + ((currentResolution == ScreenResolution::High) ? 6 : 3);
|
||||
char nodeName[96];
|
||||
@@ -268,28 +307,35 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
||||
}
|
||||
}
|
||||
|
||||
// Draw signal strength bars
|
||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||
int barWidth = 2;
|
||||
int barStartX = x + barsXOffset;
|
||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
|
||||
|
||||
for (int b = 0; b < 4; b++) {
|
||||
if (b < bars) {
|
||||
int height = (b * 2);
|
||||
display->fillRect(barStartX + (b * (barWidth + 1)), barStartY - height, barWidth, height);
|
||||
// Show signal only for direct neighbors (0 hops)
|
||||
if (isZeroHop) {
|
||||
int bars = (node->snr > 5) ? 4 : (node->snr > 0) ? 3 : (node->snr > -5) ? 2 : (node->snr > -10) ? 1 : 0;
|
||||
int barStartX = x + barsXOffset;
|
||||
int barStartY = y + 1 + (FONT_HEIGHT_SMALL / 2) + 2;
|
||||
|
||||
for (int b = 0; b < kBarCount; b++) {
|
||||
if (b < bars) {
|
||||
int height = (b * 2);
|
||||
display->fillRect(barStartX + (b * (kBarWidth + kBarGap)), barStartY - height, kBarWidth, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw hop count
|
||||
char hopStr[6] = "";
|
||||
if (node->has_hops_away && node->hops_away > 0)
|
||||
snprintf(hopStr, sizeof(hopStr), "[%d]", node->hops_away);
|
||||
// Draw hop count + hop icon
|
||||
if (node->has_hops_away && node->hops_away > 0) {
|
||||
char hopCount[6];
|
||||
snprintf(hopCount, sizeof(hopCount), "%d", node->hops_away);
|
||||
|
||||
if (hopStr[0] != '\0') {
|
||||
int rightEdge = x + columnWidth - hopOffset;
|
||||
int textWidth = display->getStringWidth(hopStr);
|
||||
display->drawString(rightEdge - textWidth, y, hopStr);
|
||||
const int hopCountWidth = display->getStringWidth(hopCount);
|
||||
const int gap = 1;
|
||||
const int totalWidth = hopCountWidth + gap + hop_width;
|
||||
const int hopX = barsRightEdge - totalWidth;
|
||||
const int iconY = y + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
||||
|
||||
display->drawString(hopX, y, hopCount);
|
||||
display->drawXbm(hopX + hopCountWidth + gap, iconY, hop_width, hop_height, hop);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,14 +419,13 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(distStr) > 0) {
|
||||
int offset = (currentResolution == ScreenResolution::High)
|
||||
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int rightEdge = x + columnWidth - offset;
|
||||
int textWidth = display->getStringWidth(distStr);
|
||||
display->drawString(rightEdge - textWidth, y, distStr);
|
||||
}
|
||||
const char *distanceLabel = (strlen(distStr) > 0) ? distStr : "?";
|
||||
int offset = (currentResolution == ScreenResolution::High)
|
||||
? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||
int rightEdge = x + columnWidth - offset;
|
||||
int textWidth = display->getStringWidth(distanceLabel);
|
||||
display->drawString(rightEdge - textWidth, y, distanceLabel);
|
||||
}
|
||||
|
||||
void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||
@@ -431,8 +476,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
}
|
||||
}
|
||||
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon)
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
||||
float myHeadingRadian, double userLat, double userLon)
|
||||
{
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
return;
|
||||
@@ -446,11 +491,11 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
double nodeLat = node->position.latitude_i * 1e-7;
|
||||
double nodeLon = node->position.longitude_i * 1e-7;
|
||||
float bearing = GeoCoord::bearing(userLat, userLon, nodeLat, nodeLon);
|
||||
float bearingToNode = RAD_TO_DEG * bearing;
|
||||
float relativeBearing = fmod((bearingToNode - myHeading + 360), 360);
|
||||
float relativeBearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeadingRadian);
|
||||
float relativeBearingDeg = CompassRenderer::radiansToDegrees360(relativeBearing);
|
||||
// Shrink size by 2px
|
||||
int size = FONT_HEIGHT_SMALL - 5;
|
||||
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearing);
|
||||
CompassRenderer::drawArrowToNode(display, centerX, centerY, size, relativeBearingDeg);
|
||||
/*
|
||||
float angle = relativeBearing * DEG_TO_RAD;
|
||||
float halfSize = size / 2.0;
|
||||
@@ -480,12 +525,27 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
||||
*/
|
||||
}
|
||||
|
||||
void drawCompassUnknown(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float, double,
|
||||
double)
|
||||
{
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
return;
|
||||
|
||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||
int arrowXOffset = (currentResolution == ScreenResolution::High) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||
int centerX = x + columnWidth - arrowXOffset;
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(centerX, y, "?");
|
||||
}
|
||||
|
||||
// =============================
|
||||
// Main Screen Functions
|
||||
// =============================
|
||||
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras, float heading, double lat, double lon)
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras, float headingRadian, double lat, double lon)
|
||||
{
|
||||
const int COMMON_HEADER_HEIGHT = FONT_HEIGHT_SMALL - 1;
|
||||
const int rowYOffset = FONT_HEIGHT_SMALL - 3;
|
||||
@@ -570,7 +630,7 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
renderer(display, node, xPos, yPos, columnWidth);
|
||||
|
||||
if (extras)
|
||||
extras(display, node, xPos, yPos, columnWidth, heading, lat, lon);
|
||||
extras(display, node, xPos, yPos, columnWidth, headingRadian, lat, lon);
|
||||
|
||||
lastNodeY = max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||
yOffset += rowYOffset;
|
||||
@@ -765,9 +825,13 @@ void drawDistanceScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
||||
#endif
|
||||
void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
float heading = 0;
|
||||
bool validHeading = false;
|
||||
float headingRadian = 0.0f;
|
||||
auto ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (!ourNode || !nodeDB->hasValidPosition(ourNode)) {
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, 0.0, 0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
double lat = DegD(ourNode->position.latitude_i);
|
||||
double lon = DegD(ourNode->position.longitude_i);
|
||||
|
||||
@@ -779,21 +843,12 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
lastSwitchTime = now;
|
||||
}
|
||||
#endif
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
#if HAS_GPS
|
||||
if (screen->hasHeading()) {
|
||||
heading = screen->getHeading(); // degrees
|
||||
validHeading = true;
|
||||
} else {
|
||||
heading = screen->estimatedHeading(lat, lon);
|
||||
validHeading = !isnan(heading);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!validHeading)
|
||||
return;
|
||||
if (!CompassRenderer::getHeadingRadians(lat, lon, headingRadian)) {
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassUnknown, headingRadian, lat, lon);
|
||||
return;
|
||||
}
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, heading, lat, lon);
|
||||
|
||||
drawNodeListScreen(display, state, x, y, "Bearings", drawEntryCompass, drawCompassArrow, headingRadian, lat, lon);
|
||||
}
|
||||
|
||||
/// Draw a series of fields in a column, wrapping to multiple columns if needed
|
||||
|
||||
@@ -32,7 +32,7 @@ enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATIO
|
||||
|
||||
// Main node list screen function
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float headingRadian = 0, double lat = 0,
|
||||
double lon = 0);
|
||||
|
||||
// Entry renderers
|
||||
@@ -43,8 +43,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
|
||||
// Extras renderers
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon);
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth,
|
||||
float myHeadingRadian, double userLat, double userLon);
|
||||
|
||||
// Screen frame functions
|
||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
+229
-180
@@ -5,6 +5,9 @@
|
||||
#include "MeshService.h"
|
||||
#include "NodeDB.h"
|
||||
#include "NodeListRenderer.h"
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "modules/StatusMessageModule.h"
|
||||
#endif
|
||||
#include "UIRenderer.h"
|
||||
#include "airtime.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
@@ -38,6 +41,15 @@ static inline void drawSatelliteIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
}
|
||||
}
|
||||
|
||||
static void drawCompassStatusText(OLEDDisplay *display, int16_t compassX, int16_t compassY, const char *statusLine1,
|
||||
const char *statusLine2)
|
||||
{
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1);
|
||||
display->drawString(compassX, compassY, statusLine2);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
}
|
||||
|
||||
void graphics::UIRenderer::rebuildFavoritedNodes()
|
||||
{
|
||||
favoritedNodes.clear();
|
||||
@@ -290,7 +302,7 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
||||
// * Favorite Node Info *
|
||||
// **********************
|
||||
// cppcheck-suppress constParameterPointer; signature must match FrameCallback typedef from OLEDDisplayUi library
|
||||
void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
void UIRenderer::drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (favoritedNodes.empty())
|
||||
return;
|
||||
@@ -342,10 +354,61 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
UIRenderer::drawStringWithEmotes(display, x, getTextPositions(display)[line++], username, FONT_HEIGHT_SMALL, 1, false);
|
||||
}
|
||||
|
||||
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||
char signalHopsStr[32] = "";
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
// === Optional: Last received StatusMessage line for this node ===
|
||||
// Display it directly under the username line (if we have one).
|
||||
if (statusMessageModule) {
|
||||
const auto &recent = statusMessageModule->getRecentReceived();
|
||||
const StatusMessageModule::RecentStatus *found = nullptr;
|
||||
|
||||
// Search newest-to-oldest
|
||||
for (auto it = recent.rbegin(); it != recent.rend(); ++it) {
|
||||
if (it->fromNodeId == node->num && !it->statusText.empty()) {
|
||||
found = &(*it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
std::string statusLine = std::string(" Status: ") + found->statusText;
|
||||
{
|
||||
const int screenW = display->getWidth();
|
||||
const int ellipseW = display->getStringWidth("...");
|
||||
int w = display->getStringWidth(statusLine.c_str());
|
||||
|
||||
// Only do work if it overflows
|
||||
if (w > screenW) {
|
||||
bool truncated = false;
|
||||
if (ellipseW > screenW) {
|
||||
statusLine.clear();
|
||||
} else {
|
||||
while (!statusLine.empty()) {
|
||||
// remove one char (byte) at a time
|
||||
statusLine.pop_back();
|
||||
truncated = true;
|
||||
|
||||
// Measure candidate with ellipsis appended
|
||||
std::string candidate = statusLine + "...";
|
||||
if (display->getStringWidth(candidate.c_str()) <= screenW) {
|
||||
statusLine = std::move(candidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (statusLine.empty() && ellipseW <= screenW) {
|
||||
statusLine = "...";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
display->drawString(x, getTextPositions(display)[line++], statusLine.c_str());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// === 2. Signal/Hops line (if available) ===
|
||||
bool haveSignal = false;
|
||||
int bars = 0;
|
||||
const char *qualityLabel = nullptr;
|
||||
|
||||
// Helper to get SNR limit based on modem preset
|
||||
auto getSnrLimit = [](meshtastic_Config_LoRaConfig_ModemPreset preset) -> float {
|
||||
@@ -366,80 +429,51 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate signal grade using modem preset and SNR only
|
||||
float snrLimit = getSnrLimit(config.lora.modem_preset);
|
||||
float snr = node->snr;
|
||||
|
||||
// Determine signal quality label and bars using SNR-only grading
|
||||
const char *qualityLabel = nullptr;
|
||||
|
||||
if (snr > snrLimit + 10) {
|
||||
qualityLabel = "Good";
|
||||
bars = 4;
|
||||
} else if (snr > snrLimit + 6) {
|
||||
qualityLabel = "Good";
|
||||
bars = 3;
|
||||
} else if (snr > snrLimit + 2) {
|
||||
qualityLabel = "Good";
|
||||
bars = 2;
|
||||
} else if (snr > snrLimit - 4) {
|
||||
qualityLabel = "Fair";
|
||||
bars = 1;
|
||||
} else {
|
||||
qualityLabel = "Bad";
|
||||
bars = 1;
|
||||
}
|
||||
|
||||
// Add extra spacing on the left if we have an API connection to account for the common footer icons
|
||||
const char *leftSideSpacing =
|
||||
graphics::isAPIConnected(service->api_state) ? (currentResolution == ScreenResolution::High ? " " : " ") : " ";
|
||||
const bool isZeroHop = node->has_hops_away && node->hops_away == 0;
|
||||
|
||||
// --- Build the Signal/Hops line ---
|
||||
// Only show signal if we have valid SNR
|
||||
if (snr > -100 && snr != 0) {
|
||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "%sSig:%s", leftSideSpacing, qualityLabel);
|
||||
haveSignal = true;
|
||||
}
|
||||
|
||||
if (node->hops_away > 0) {
|
||||
size_t len = strlen(signalHopsStr);
|
||||
if (haveSignal) {
|
||||
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [#]");
|
||||
} else {
|
||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "[#]");
|
||||
}
|
||||
}
|
||||
|
||||
if (signalHopsStr[0]) {
|
||||
int yPos = getTextPositions(display)[line++];
|
||||
int curX = x;
|
||||
|
||||
// Split combined string into signal text and hop suffix
|
||||
char sigPart[20] = "";
|
||||
const char *hopPart = nullptr;
|
||||
|
||||
char *bracket = strchr(signalHopsStr, '[');
|
||||
if (bracket) {
|
||||
size_t n = (size_t)(bracket - signalHopsStr);
|
||||
if (n >= sizeof(sigPart))
|
||||
n = sizeof(sigPart) - 1;
|
||||
memcpy(sigPart, signalHopsStr, n);
|
||||
sigPart[n] = '\0';
|
||||
|
||||
// Trim trailing spaces
|
||||
while (strlen(sigPart) && sigPart[strlen(sigPart) - 1] == ' ') {
|
||||
sigPart[strlen(sigPart) - 1] = '\0';
|
||||
// Signal text/bars are only for direct (zero-hop) nodes with valid SNR.
|
||||
if (isZeroHop) {
|
||||
float snr = node->snr;
|
||||
if (snr > -100 && snr != 0) {
|
||||
float snrLimit = getSnrLimit(config.lora.modem_preset);
|
||||
// Determine signal quality label and bars using SNR-only grading.
|
||||
if (snr > snrLimit + 10) {
|
||||
qualityLabel = "Good";
|
||||
bars = 4;
|
||||
} else if (snr > snrLimit + 6) {
|
||||
qualityLabel = "Good";
|
||||
bars = 3;
|
||||
} else if (snr > snrLimit + 2) {
|
||||
qualityLabel = "Good";
|
||||
bars = 2;
|
||||
} else if (snr > snrLimit - 4) {
|
||||
qualityLabel = "Fair";
|
||||
bars = 1;
|
||||
} else {
|
||||
qualityLabel = "Bad";
|
||||
bars = 1;
|
||||
}
|
||||
|
||||
hopPart = bracket; // "[n Hop(s)]"
|
||||
} else {
|
||||
strncpy(sigPart, signalHopsStr, sizeof(sigPart) - 1);
|
||||
sigPart[sizeof(sigPart) - 1] = '\0';
|
||||
haveSignal = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw signal quality text
|
||||
display->drawString(curX, yPos, sigPart);
|
||||
curX += display->getStringWidth(sigPart) + 4;
|
||||
const bool showHops = node->has_hops_away && node->hops_away > 0;
|
||||
|
||||
if (haveSignal || showHops) {
|
||||
int yPos = getTextPositions(display)[line++];
|
||||
int curX = x + display->getStringWidth(leftSideSpacing);
|
||||
|
||||
// Draw signal quality text for zero-hop nodes when present.
|
||||
if (haveSignal && qualityLabel) {
|
||||
char signalLabel[20];
|
||||
snprintf(signalLabel, sizeof(signalLabel), "Sig:%s", qualityLabel);
|
||||
display->drawString(curX, yPos, signalLabel);
|
||||
curX += display->getStringWidth(signalLabel) + 4;
|
||||
}
|
||||
|
||||
// Draw signal bars (skip on UltraLow, text only)
|
||||
if (currentResolution != ScreenResolution::UltraLow && haveSignal && bars > 0) {
|
||||
@@ -478,12 +512,12 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
curX += (kMaxBars * barWidth) + ((kMaxBars - 1) * barGap) + 2;
|
||||
}
|
||||
|
||||
// Draw hops AFTER the bars as: [ number + hop icon ]
|
||||
if (hopPart && node->hops_away > 0) {
|
||||
|
||||
// open bracket
|
||||
display->drawString(curX, yPos, "[");
|
||||
curX += display->getStringWidth("[") + 1;
|
||||
// Draw hops for non-zero-hop nodes as: number + hop icon.
|
||||
// This path is mutually exclusive with the zero-hop signal-bars path above.
|
||||
if (showHops) {
|
||||
// hop label
|
||||
display->drawString(curX, yPos, "Hop:");
|
||||
curX += display->getStringWidth("Hop:") + 2;
|
||||
|
||||
// hop count
|
||||
char hopCount[6];
|
||||
@@ -495,9 +529,6 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
const int iconY = yPos + (FONT_HEIGHT_SMALL - hop_height) / 2;
|
||||
display->drawXbm(curX, iconY, hop_width, hop_height, hop);
|
||||
curX += hop_width + 1;
|
||||
|
||||
// closing bracket
|
||||
display->drawString(curX, yPos, "]");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -638,51 +669,54 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
display->drawString(x, getTextPositions(display)[line++], batLine);
|
||||
}
|
||||
|
||||
bool showCompass = false;
|
||||
float myHeading = 0.0f;
|
||||
float bearing = 0.0f;
|
||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||
const bool hasNodePositionFix = nodeDB->hasValidPosition(node);
|
||||
const char *statusLine1 = nullptr;
|
||||
const char *statusLine2 = nullptr;
|
||||
if (hasOwnPositionFix && hasNodePositionFix) {
|
||||
const auto &op = ourNode->position;
|
||||
showCompass = CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
||||
if (showCompass) {
|
||||
const auto &p = node->position;
|
||||
bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||
bearing = CompassRenderer::adjustBearingForCompassMode(bearing, myHeading);
|
||||
} else {
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Heading";
|
||||
}
|
||||
} else if (!hasOwnPositionFix || !hasNodePositionFix) {
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Fix";
|
||||
}
|
||||
|
||||
// --- Compass Rendering: landscape (wide) screens use the original side-aligned logic ---
|
||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||
bool showCompass = false;
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) {
|
||||
showCompass = true;
|
||||
}
|
||||
if (showCompass) {
|
||||
if (showCompass || statusLine1) {
|
||||
const int16_t topY = getTextPositions(display)[1];
|
||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1);
|
||||
const int16_t usableHeight = bottomY - topY - 5;
|
||||
int16_t compassRadius = usableHeight / 2;
|
||||
if (compassRadius < 8)
|
||||
compassRadius = 8;
|
||||
const int16_t compassDiam = compassRadius * 2;
|
||||
const int16_t compassX = x + SCREEN_WIDTH - compassRadius - 8;
|
||||
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||
|
||||
const auto &op = ourNode->position;
|
||||
float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
||||
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
|
||||
const auto &p = node->position;
|
||||
/* unused
|
||||
float d =
|
||||
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
*/
|
||||
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
myHeading = 0;
|
||||
} else {
|
||||
bearing -= myHeading;
|
||||
}
|
||||
const int16_t compassDiam = compassRadius * 2;
|
||||
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
||||
if (showCompass) {
|
||||
CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
||||
} else {
|
||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||
}
|
||||
}
|
||||
// else show nothing
|
||||
} else {
|
||||
// Portrait or square: put compass at the bottom and centered, scaled to fit available space
|
||||
bool showCompass = false;
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading()) && nodeDB->hasValidPosition(node)) {
|
||||
showCompass = true;
|
||||
}
|
||||
if (showCompass) {
|
||||
if (showCompass || statusLine1) {
|
||||
int yBelowContent = (line > 0 && line <= 5) ? (getTextPositions(display)[line - 1] + FONT_HEIGHT_SMALL + 2)
|
||||
: getTextPositions(display)[1];
|
||||
const int margin = 4;
|
||||
@@ -693,8 +727,8 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
#else
|
||||
const int navBarHeight = 0;
|
||||
#endif
|
||||
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||
// --------- END PATCH FOR EINK NAV BAR -----------
|
||||
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||
|
||||
if (availableHeight < FONT_HEIGHT_SMALL * 2)
|
||||
return;
|
||||
@@ -708,25 +742,13 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
||||
int compassX = x + SCREEN_WIDTH / 2;
|
||||
int compassY = yBelowContent + availableHeight / 2;
|
||||
|
||||
const auto &op = ourNode->position;
|
||||
float myHeading = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
||||
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
}
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
|
||||
const auto &p = node->position;
|
||||
/* unused
|
||||
float d =
|
||||
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
*/
|
||||
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
|
||||
bearing -= myHeading;
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
||||
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
if (showCompass) {
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
||||
} else {
|
||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||
}
|
||||
}
|
||||
// else show nothing
|
||||
}
|
||||
@@ -1162,6 +1184,7 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
const int *textPos = getTextPositions(display);
|
||||
|
||||
// === First Row: My Location ===
|
||||
#if HAS_GPS
|
||||
@@ -1176,12 +1199,12 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
} else {
|
||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||
}
|
||||
drawSatelliteIcon(display, x, getTextPositions(display)[line]);
|
||||
drawSatelliteIcon(display, x, textPos[line]);
|
||||
int xOffset = (currentResolution == ScreenResolution::High) ? 6 : 0;
|
||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||
display->drawString(x + 11 + xOffset, textPos[line++], displayLine);
|
||||
} else {
|
||||
// Onboard GPS
|
||||
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
||||
UIRenderer::drawGps(display, 0, textPos[line++], gpsStatus);
|
||||
}
|
||||
|
||||
config.display.heading_bold = origBold;
|
||||
@@ -1190,18 +1213,36 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
geoCoord.updateCoords(int32_t(gpsStatus->getLatitude()), int32_t(gpsStatus->getLongitude()),
|
||||
int32_t(gpsStatus->getAltitude()));
|
||||
|
||||
// === Determine Compass Heading ===
|
||||
float heading = 0;
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||
const bool hasLiveGpsFix =
|
||||
(gpsStatus && gpsStatus->getHasLock() && (gpsStatus->getLatitude() != 0 || gpsStatus->getLongitude() != 0));
|
||||
const bool hasSensorHeading = screen->hasHeading();
|
||||
float heading = 0.0f;
|
||||
bool validHeading = false;
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
validHeading = true;
|
||||
} else {
|
||||
if (screen->hasHeading()) {
|
||||
heading = radians(screen->getHeading());
|
||||
validHeading = true;
|
||||
const char *statusLine1 = nullptr;
|
||||
const char *statusLine2 = nullptr;
|
||||
if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) {
|
||||
double headingLat = 0.0;
|
||||
double headingLon = 0.0;
|
||||
if (hasLiveGpsFix) {
|
||||
headingLat = DegD(gpsStatus->getLatitude());
|
||||
headingLon = DegD(gpsStatus->getLongitude());
|
||||
} else if (hasOwnPositionFix) {
|
||||
const auto &op = ourNode->position;
|
||||
headingLat = DegD(op.latitude_i);
|
||||
headingLon = DegD(op.longitude_i);
|
||||
}
|
||||
validHeading = CompassRenderer::getHeadingRadians(headingLat, headingLon, heading);
|
||||
}
|
||||
|
||||
if (!validHeading) {
|
||||
if (hasSensorHeading || hasLiveGpsFix || hasOwnPositionFix) {
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Heading";
|
||||
} else {
|
||||
heading = screen->estimatedHeading(geoCoord.getLatitude() * 1e-7, geoCoord.getLongitude() * 1e-7);
|
||||
validHeading = !isnan(heading);
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Fix";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1219,18 +1260,18 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
getUptimeStr(delta, "Last: ", uptimeStr, sizeof(uptimeStr), true);
|
||||
#endif
|
||||
|
||||
display->drawString(0, getTextPositions(display)[line++], uptimeStr);
|
||||
display->drawString(0, textPos[line++], uptimeStr);
|
||||
} else {
|
||||
display->drawString(0, getTextPositions(display)[line++], "Last: ?");
|
||||
display->drawString(0, textPos[line++], "Last: ?");
|
||||
}
|
||||
|
||||
// === Third Row: Line 1 GPS Info ===
|
||||
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line1");
|
||||
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line1");
|
||||
|
||||
if (uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_OLC &&
|
||||
uiconfig.gps_format != meshtastic_DeviceUIConfig_GpsCoordinateFormat_MLS) {
|
||||
// === Fourth Row: Line 2 GPS Info ===
|
||||
UIRenderer::drawGpsCoordinates(display, x, getTextPositions(display)[line++], gpsStatus, "line2");
|
||||
UIRenderer::drawGpsCoordinates(display, x, textPos[line++], gpsStatus, "line2");
|
||||
}
|
||||
|
||||
// === Final Row: Altitude ===
|
||||
@@ -1241,14 +1282,14 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
} else {
|
||||
snprintf(altitudeLine, sizeof(altitudeLine), "Alt: %.0im", alt);
|
||||
}
|
||||
display->drawString(x, getTextPositions(display)[line++], altitudeLine);
|
||||
display->drawString(x, textPos[line++], altitudeLine);
|
||||
}
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === Draw Compass if heading is valid ===
|
||||
if (validHeading) {
|
||||
// === Draw Compass ===
|
||||
if (validHeading || statusLine1) {
|
||||
// --- Compass Rendering: landscape (wide) screens use original side-aligned logic ---
|
||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||
const int16_t topY = getTextPositions(display)[1];
|
||||
const int16_t topY = textPos[1];
|
||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1); // nav row height
|
||||
const int16_t usableHeight = bottomY - topY - 5;
|
||||
|
||||
@@ -1261,29 +1302,33 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
// Center vertically and nudge down slightly to keep "N" clear of header
|
||||
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading);
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
if (validHeading) {
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, -heading);
|
||||
|
||||
// "N" label
|
||||
float northAngle = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
northAngle = -heading;
|
||||
float radius = compassRadius;
|
||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||
// "N" label
|
||||
float northAngle = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
northAngle = -heading;
|
||||
float radius = compassRadius;
|
||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||
} else {
|
||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||
}
|
||||
} else {
|
||||
// Portrait or square: put compass at the bottom and centered, scaled to fit available space
|
||||
// For E-Ink screens, account for navigation bar at the bottom!
|
||||
int yBelowContent = getTextPositions(display)[5] + FONT_HEIGHT_SMALL + 2;
|
||||
int yBelowContent = textPos[5] + FONT_HEIGHT_SMALL + 2;
|
||||
const int margin = 4;
|
||||
int availableHeight =
|
||||
#if defined(USE_EINK)
|
||||
@@ -1304,25 +1349,29 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
||||
int compassX = x + SCREEN_WIDTH / 2;
|
||||
int compassY = yBelowContent + availableHeight / 2;
|
||||
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading);
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
if (validHeading) {
|
||||
CompassRenderer::drawNodeHeading(display, compassX, compassY, compassRadius * 2, -heading);
|
||||
|
||||
// "N" label
|
||||
float northAngle = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
northAngle = -heading;
|
||||
float radius = compassRadius;
|
||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||
// "N" label
|
||||
float northAngle = 0;
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
northAngle = -heading;
|
||||
float radius = compassRadius;
|
||||
int16_t nX = compassX + (radius - 1) * sin(northAngle);
|
||||
int16_t nY = compassY - (radius - 1) * cos(northAngle);
|
||||
int16_t nLabelWidth = display->getStringWidth("N") + 2;
|
||||
int16_t nLabelHeightBox = FONT_HEIGHT_SMALL + 1;
|
||||
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(nX - nLabelWidth / 2, nY - nLabelHeightBox / 2, nLabelWidth, nLabelHeightBox);
|
||||
display->setColor(WHITE);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(nX, nY - FONT_HEIGHT_SMALL / 2, "N");
|
||||
} else {
|
||||
drawCompassStatusText(display, compassX, compassY, statusLine1, statusLine2);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -50,7 +50,7 @@ class UIRenderer
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawFavoriteNode(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
|
||||
NicheGraphics parallel E-Ink driver for the LilyGo T5-S3-ePaper-Pro (ED047TC1).
|
||||
|
||||
InkHUD buffer format : 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
|
||||
FastEPD buffer format: 1bpp, horizontal bytes, MSB = leftmost pixel, 1 = white
|
||||
|
||||
Both formats share the same pixel layout and polarity (1 = white, 0 = black).
|
||||
The InkHUD safe-area buffer (944×523) is copied into the centre of the physical
|
||||
960×540 FastEPD buffer so content clears the panel's inactive edge border.
|
||||
See ED047TC1.h for the H_OFFSET_BYTES / V_OFFSET_TOP / V_OFFSET_BOTTOM constants.
|
||||
|
||||
*/
|
||||
|
||||
// Ruler diagnostic — uncomment to draw calibration lines at each physical edge.
|
||||
// #define EINK_EDGE_LINES
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#ifdef T5_S3_EPAPER_PRO
|
||||
|
||||
#include "./ED047TC1.h"
|
||||
|
||||
#include "FastEPD.h"
|
||||
#include "configuration.h"
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||
{
|
||||
// Parallel display — SPI parameters are not used
|
||||
(void)spi;
|
||||
(void)pin_dc;
|
||||
(void)pin_cs;
|
||||
(void)pin_busy;
|
||||
(void)pin_rst;
|
||||
|
||||
epaper = new FASTEPD;
|
||||
|
||||
#if defined(T5_S3_EPAPER_PRO_V1)
|
||||
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
|
||||
#elif defined(T5_S3_EPAPER_PRO_V2)
|
||||
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
|
||||
// Initialize all PCA9535 port-0 pins as outputs / HIGH
|
||||
for (int i = 0; i < 8; i++) {
|
||||
epaper->ioPinMode(i, OUTPUT);
|
||||
epaper->ioWrite(i, HIGH);
|
||||
}
|
||||
#else
|
||||
#error "ED047TC1 driver: unsupported variant — define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2"
|
||||
#endif
|
||||
|
||||
epaper->setMode(BB_MODE_1BPP);
|
||||
epaper->clearWhite();
|
||||
epaper->fullUpdate(true); // Blocking initial clear
|
||||
}
|
||||
|
||||
void ED047TC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
{
|
||||
if (!epaper)
|
||||
return;
|
||||
|
||||
// InkHUD renders into a DISPLAY_WIDTH × DISPLAY_HEIGHT safe-area buffer.
|
||||
// We need to place that into the centre of the physical 960×540 FastEPD buffer,
|
||||
// leaving blank margins at every edge to avoid the panel's inactive border.
|
||||
const uint32_t srcRowBytes = (DISPLAY_WIDTH + 7) / 8; // bytes per row in InkHUD buffer (118)
|
||||
const uint32_t dstRowBytes = (960 + 7) / 8; // bytes per row in physical buffer (120)
|
||||
const uint32_t dstTotalRows = 540;
|
||||
|
||||
uint8_t *cur = epaper->currentBuffer();
|
||||
|
||||
// Fill physical buffer with white (0xFF = white in FastEPD 1bpp)
|
||||
memset(cur, 0xFF, dstRowBytes * dstTotalRows);
|
||||
|
||||
// Copy each InkHUD row into the physical buffer with horizontal + vertical offsets
|
||||
for (uint32_t row = 0; row < DISPLAY_HEIGHT; row++) {
|
||||
const uint8_t *srcRow = imageData + row * srcRowBytes;
|
||||
uint8_t *dstRow = cur + (row + V_OFFSET_TOP) * dstRowBytes + H_OFFSET_BYTES;
|
||||
memcpy(dstRow, srcRow, srcRowBytes);
|
||||
}
|
||||
|
||||
#ifdef EINK_EDGE_LINES
|
||||
// Draw a 1px black box at the exact boundary of the safe area within the
|
||||
// physical buffer. If the margins are correct, all 4 lines should be
|
||||
// fully visible and right at the edge of the usable display area.
|
||||
|
||||
auto setPixelBlack = [&](uint32_t col, uint32_t row) { cur[row * dstRowBytes + col / 8] &= ~(0x80 >> (col % 8)); };
|
||||
|
||||
const uint32_t safeX = H_OFFSET_BYTES * 8;
|
||||
const uint32_t safeY = V_OFFSET_TOP;
|
||||
const uint32_t safeW = DISPLAY_WIDTH;
|
||||
const uint32_t safeH = DISPLAY_HEIGHT;
|
||||
|
||||
// Top edge: horizontal line at safeY
|
||||
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||
setPixelBlack(col, safeY);
|
||||
|
||||
// Bottom edge: horizontal line at safeY + safeH - 1
|
||||
for (uint32_t col = safeX; col < safeX + safeW; col++)
|
||||
setPixelBlack(col, safeY + safeH - 1);
|
||||
|
||||
// Left edge: vertical line at safeX
|
||||
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||
setPixelBlack(safeX, row);
|
||||
|
||||
// Right edge: vertical line at safeX + safeW - 1
|
||||
for (uint32_t row = safeY; row < safeY + safeH; row++)
|
||||
setPixelBlack(safeX + safeW - 1, row);
|
||||
#endif
|
||||
|
||||
if (type == FULL) {
|
||||
epaper->fullUpdate(CLEAR_SLOW, false);
|
||||
epaper->backupPlane(); // Sync pPrevious so next partialUpdate has a correct baseline
|
||||
} else {
|
||||
// FAST: true partial update — compares pCurrent vs pPrevious and only applies
|
||||
// the update waveform to rows that actually changed. Unchanged rows get a neutral
|
||||
// signal (no visible effect). partialUpdate() updates pPrevious internally.
|
||||
epaper->partialUpdate(false, 0, dstTotalRows - 1);
|
||||
}
|
||||
}
|
||||
|
||||
#endif // T5_S3_EPAPER_PRO
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
|
||||
E-Ink display driver adapter
|
||||
- ED047TC1 (via FastEPD library)
|
||||
- Manufacturer: E Ink / used in LilyGo T5-E-Paper-S3-Pro
|
||||
- Size: 4.7 inch
|
||||
- Physical resolution: 960px x 540px
|
||||
- Interface: 8-bit parallel (NOT SPI)
|
||||
|
||||
Unlike the other NicheGraphics EInk drivers, this one drives a parallel e-paper
|
||||
panel via the FastEPD library. SPI parameters passed to begin() are ignored.
|
||||
|
||||
The ED047TC1 panel has an inactive pixel border on all four edges (~4–8 physical
|
||||
pixels). DISPLAY_WIDTH / DISPLAY_HEIGHT expose a reduced "safe area" to InkHUD so
|
||||
that content is never drawn into this dead zone. The update() method copies the
|
||||
InkHUD frame buffer into the centre of the larger physical 960×540 buffer, using
|
||||
H_OFFSET_BYTES (horizontal, whole bytes = 8 pixels per byte),
|
||||
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
|
||||
|
||||
Changing these constants shifts content inward from each physical edge:
|
||||
H_OFFSET_BYTES = 1 → 8px left margin, 8px right margin (960 – 8 – 8 = 944)
|
||||
V_OFFSET_TOP = 9 → 9px top margin (asymmetric: top ≠ bottom)
|
||||
V_OFFSET_BOTTOM = 8 → 8px bottom margin (540 – 9 – 8 = 523)
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./EInk.h"
|
||||
|
||||
// Forward declare to avoid pulling FastEPD into all translation units
|
||||
class FASTEPD;
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
|
||||
class ED047TC1 : public EInk
|
||||
{
|
||||
// Safe-area dimensions exposed to InkHUD (physical panel is 960×540).
|
||||
//
|
||||
// The ED047TC1 has an inactive pixel border on all physical edges.
|
||||
// The physical buffer coordinates do NOT directly match the visual orientation
|
||||
// due to FastEPD's portrait scan direction and InkHUD's rotation=3 (270° CW):
|
||||
//
|
||||
// Physical buffer Visual on device (rotation=3)
|
||||
// ───────────────── ──────────────────────────────
|
||||
// Physical LEFT cols → Visual TOP edge
|
||||
// Physical RIGHT cols → Visual BOTTOM edge
|
||||
// Physical TOP rows → Visual RIGHT edge
|
||||
// Physical BOTTOM rows → Visual LEFT edge
|
||||
//
|
||||
// Offset constants shift the InkHUD safe-area away from each physical dead zone:
|
||||
// H_OFFSET_BYTES : whole bytes from physical left (8px per byte, affects visual TOP)
|
||||
// Physical right margin = 960 − H_OFFSET_BYTES×8 − DISPLAY_WIDTH (affects visual BOTTOM)
|
||||
// V_OFFSET_TOP : pixel rows from physical top (affects visual RIGHT)
|
||||
// V_OFFSET_BOTTOM: pixel rows from physical bottom (affects visual LEFT)
|
||||
//
|
||||
// Calibrated by flashing a 1px border box and adjusting until all 4 sides are visible.
|
||||
|
||||
static constexpr uint16_t DISPLAY_WIDTH = 944; // 960 − H_OFFSET_BYTES×8 − right_margin (8+8 = 16px)
|
||||
static constexpr uint16_t DISPLAY_HEIGHT = 523; // 540 − V_OFFSET_TOP − V_OFFSET_BOTTOM (9+8 = 17px)
|
||||
|
||||
static constexpr uint8_t H_OFFSET_BYTES = 1; // visual TOP : 8px physical left margin
|
||||
// visual BOTTOM: 960−8−944=8px physical right margin
|
||||
static constexpr uint8_t V_OFFSET_TOP = 9; // visual RIGHT : CONFIRMED OK
|
||||
static constexpr uint8_t V_OFFSET_BOTTOM = 8; // visual LEFT : 8px physical bottom margin
|
||||
|
||||
static constexpr UpdateTypes supported = static_cast<UpdateTypes>(FULL | FAST);
|
||||
|
||||
public:
|
||||
ED047TC1() : EInk(DISPLAY_WIDTH, DISPLAY_HEIGHT, supported) {}
|
||||
|
||||
// EInk interface — SPI params are not used for this parallel display
|
||||
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = 0xFF) override;
|
||||
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
|
||||
protected:
|
||||
bool isUpdateDone() override { return true; } // FastEPD updates are blocking
|
||||
|
||||
private:
|
||||
FASTEPD *epaper = nullptr;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -88,8 +88,12 @@ class AppletFont
|
||||
|
||||
// Greek
|
||||
#include "graphics/niche/Fonts/FreeSans12pt_Win1253.h"
|
||||
#include "graphics/niche/Fonts/FreeSans18pt_Win1253.h"
|
||||
#include "graphics/niche/Fonts/FreeSans24pt_Win1253.h"
|
||||
#include "graphics/niche/Fonts/FreeSans6pt_Win1253.h"
|
||||
#include "graphics/niche/Fonts/FreeSans9pt_Win1253.h"
|
||||
#define FREESANS_24PT_WIN1253 InkHUD::AppletFont(FreeSans24pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -5, 3)
|
||||
#define FREESANS_18PT_WIN1253 InkHUD::AppletFont(FreeSans18pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -4, 2)
|
||||
#define FREESANS_12PT_WIN1253 InkHUD::AppletFont(FreeSans12pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -3, 1)
|
||||
#define FREESANS_9PT_WIN1253 InkHUD::AppletFont(FreeSans9pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -2, -1)
|
||||
#define FREESANS_6PT_WIN1253 InkHUD::AppletFont(FreeSans6pt_Win1253, InkHUD::AppletFont::WINDOWS_1253, -1, -2)
|
||||
|
||||
@@ -53,10 +53,11 @@ void InkHUD::MessageStore::saveToFlash()
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.timestamp), sizeof(m.timestamp)); // Write timestamp. 4 bytes
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.sender), sizeof(m.sender)); // Write sender NodeId. 4 Bytes
|
||||
f.write(reinterpret_cast<const uint8_t *>(&m.channelIndex), sizeof(m.channelIndex)); // Write channel index. 1 Byte
|
||||
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i), min(MAX_MESSAGE_SIZE, m.text.size()),
|
||||
m.text.c_str());
|
||||
f.write(reinterpret_cast<const uint8_t *>(m.text.c_str()),
|
||||
min((size_t)MAX_MESSAGE_SIZE, m.text.size())); // Write message text
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", static_cast<uint32_t>(i),
|
||||
min((size_t)MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
|
||||
}
|
||||
|
||||
// Release firmware's SPI lock, because SafeFile::close needs it
|
||||
|
||||
@@ -143,6 +143,10 @@ int32_t ButtonThread::runOnce()
|
||||
leadUpSequenceActive = false;
|
||||
resetLeadUpSequence();
|
||||
}
|
||||
#ifdef INPUT_DEBUG
|
||||
if (buttonCurrentlyPressed)
|
||||
LOG_WARN("Button held for %u ms", millis() - buttonPressStartTime);
|
||||
#endif
|
||||
|
||||
// Progressive lead-up sound system
|
||||
if (!_suppressLeadUp && buttonCurrentlyPressed && (millis() - buttonPressStartTime) >= BUTTON_LEADUP_MS) {
|
||||
@@ -311,7 +315,8 @@ int32_t ButtonThread::runOnce()
|
||||
void ButtonThread::attachButtonInterrupts()
|
||||
{
|
||||
// Interrupt for user button, during normal use. Improves responsiveness.
|
||||
attachInterrupt(_pinNum, _intRoutine, CHANGE);
|
||||
if (_intRoutine != nullptr)
|
||||
attachInterrupt(_pinNum, _intRoutine, CHANGE);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -320,7 +325,8 @@ void ButtonThread::attachButtonInterrupts()
|
||||
*/
|
||||
void ButtonThread::detachButtonInterrupts()
|
||||
{
|
||||
detachInterrupt(_pinNum);
|
||||
if (_intRoutine != nullptr)
|
||||
detachInterrupt(_pinNum);
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
@@ -121,7 +121,6 @@ void CardputerKeyboard::pressed(uint8_t key)
|
||||
modifierFlag = 0;
|
||||
}
|
||||
|
||||
uint8_t next_key = 0;
|
||||
int row = (key - 1) / 10;
|
||||
int col = (key - 1) % 10;
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ void TouchScreenImpl1::init()
|
||||
return;
|
||||
#else
|
||||
TouchScreenBase::init(true);
|
||||
inputBroker->registerSource(this);
|
||||
if (inputBroker)
|
||||
inputBroker->registerSource(this);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
+129
@@ -339,9 +339,138 @@ void setup()
|
||||
|
||||
#ifdef BLE_LED
|
||||
pinMode(BLE_LED, OUTPUT);
|
||||
#ifdef BLE_LED_INVERTED
|
||||
digitalWrite(BLE_LED, HIGH);
|
||||
#else
|
||||
digitalWrite(BLE_LED, LOW);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(T_DECK)
|
||||
// GPIO10 manages all peripheral power supplies
|
||||
// Turn on peripheral power immediately after MUC starts.
|
||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
||||
pinMode(KB_POWERON, OUTPUT);
|
||||
digitalWrite(KB_POWERON, HIGH);
|
||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
||||
// e.g. when detecting the SD card
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
delay(100);
|
||||
#elif defined(T_DECK_PRO)
|
||||
pinMode(LORA_EN, OUTPUT);
|
||||
digitalWrite(LORA_EN, HIGH);
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(PIN_EINK_CS, OUTPUT);
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
#if PIN_EINK_RES >= 0
|
||||
pinMode(PIN_EINK_RES, OUTPUT);
|
||||
digitalWrite(PIN_EINK_RES, HIGH);
|
||||
#endif
|
||||
pinMode(CST328_PIN_RST, OUTPUT);
|
||||
digitalWrite(CST328_PIN_RST, HIGH);
|
||||
#elif defined(T_LORA_PAGER)
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
pinMode(KB_INT, INPUT_PULLUP);
|
||||
// io expander
|
||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
pinMode(KB_INT, INPUT);
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
|
||||
#if defined(T_DECK)
|
||||
// GPIO10 manages all peripheral power supplies
|
||||
// Turn on peripheral power immediately after MUC starts.
|
||||
// If some boards are turned on late, ESP32 will reset due to low voltage.
|
||||
// ESP32-C3(Keyboard) , MAX98357A(Audio Power Amplifier) ,
|
||||
// TF Card , Display backlight(AW9364DNR) , AN48841B(Trackball) , ES7210(Decoder)
|
||||
pinMode(KB_POWERON, OUTPUT);
|
||||
digitalWrite(KB_POWERON, HIGH);
|
||||
// T-Deck has all three SPI peripherals (TFT, SD, LoRa) attached to the same SPI bus
|
||||
// We need to initialize all CS pins in advance otherwise there will be SPI communication issues
|
||||
// e.g. when detecting the SD card
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
delay(100);
|
||||
#elif defined(T_DECK_PRO)
|
||||
pinMode(LORA_EN, OUTPUT);
|
||||
digitalWrite(LORA_EN, HIGH);
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(PIN_EINK_CS, OUTPUT);
|
||||
digitalWrite(PIN_EINK_CS, HIGH);
|
||||
#if PIN_EINK_RES >= 0
|
||||
pinMode(PIN_EINK_RES, OUTPUT);
|
||||
digitalWrite(PIN_EINK_RES, HIGH);
|
||||
#endif
|
||||
pinMode(CST328_PIN_RST, OUTPUT);
|
||||
digitalWrite(CST328_PIN_RST, HIGH);
|
||||
#elif defined(T_LORA_PAGER)
|
||||
pinMode(LORA_CS, OUTPUT);
|
||||
digitalWrite(LORA_CS, HIGH);
|
||||
pinMode(SDCARD_CS, OUTPUT);
|
||||
digitalWrite(SDCARD_CS, HIGH);
|
||||
pinMode(TFT_CS, OUTPUT);
|
||||
digitalWrite(TFT_CS, HIGH);
|
||||
pinMode(KB_INT, INPUT_PULLUP);
|
||||
// io expander
|
||||
io.begin(Wire, XL9555_SLAVE_ADDRESS0, SDA, SCL);
|
||||
io.pinMode(EXPANDS_DRV_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_DRV_EN, HIGH);
|
||||
io.pinMode(EXPANDS_AMP_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_AMP_EN, LOW);
|
||||
io.pinMode(EXPANDS_LORA_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_LORA_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPS_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPS_EN, HIGH);
|
||||
io.pinMode(EXPANDS_KB_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_KB_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_SD_EN, HIGH);
|
||||
io.pinMode(EXPANDS_GPIO_EN, OUTPUT);
|
||||
io.digitalWrite(EXPANDS_GPIO_EN, HIGH);
|
||||
io.pinMode(EXPANDS_SD_PULLEN, INPUT);
|
||||
#elif defined(HACKADAY_COMMUNICATOR)
|
||||
pinMode(KB_INT, INPUT);
|
||||
#endif
|
||||
|
||||
concurrency::hasBeenSetup = true;
|
||||
#if HAS_SCREEN
|
||||
meshtastic_Config_DisplayConfig_OledType screen_model =
|
||||
|
||||
+23
-5
@@ -10,8 +10,26 @@
|
||||
#include "memGet.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef ARCH_STM32WL
|
||||
#if defined(MESHTASTIC_DYNAMIC_SBRK_HEAP)
|
||||
#include <malloc.h>
|
||||
#include <unistd.h> // sbrk
|
||||
|
||||
#ifdef ARCH_STM32WL
|
||||
// Returns the uncommitted sbrk headroom: addressable space between the current heap
|
||||
// break and the stack pointer that has not yet been committed to the arena.
|
||||
static uint32_t sbrkHeadroom()
|
||||
{
|
||||
// defined in STM32 linker script
|
||||
extern char _estack;
|
||||
extern char _Min_Stack_Size;
|
||||
|
||||
uint32_t max_sp = (uint32_t)(&_estack - &_Min_Stack_Size);
|
||||
uint32_t heap_end = (uint32_t)sbrk(0);
|
||||
return (max_sp > heap_end) ? (max_sp - heap_end) : 0;
|
||||
}
|
||||
#else
|
||||
#error Unsupported architecture!
|
||||
#endif
|
||||
#endif
|
||||
|
||||
MemGet memGet;
|
||||
@@ -28,9 +46,9 @@ uint32_t MemGet::getFreeHeap()
|
||||
return dbgHeapFree();
|
||||
#elif defined(ARCH_RP2040)
|
||||
return rp2040.getFreeHeap();
|
||||
#elif defined(ARCH_STM32WL)
|
||||
#elif defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) // Currently: ARCH_STM32WL
|
||||
struct mallinfo m = mallinfo();
|
||||
return m.fordblks; // Total free space (bytes)
|
||||
return m.fordblks + sbrkHeadroom(); // Free space within arena + uncommitted sbrk headroom
|
||||
#else
|
||||
// this platform does not have heap management function implemented
|
||||
return UINT32_MAX;
|
||||
@@ -49,9 +67,9 @@ uint32_t MemGet::getHeapSize()
|
||||
return dbgHeapTotal();
|
||||
#elif defined(ARCH_RP2040)
|
||||
return rp2040.getTotalHeap();
|
||||
#elif defined(ARCH_STM32WL)
|
||||
#elif defined(MESHTASTIC_DYNAMIC_SBRK_HEAP) // Currently: ARCH_STM32WL
|
||||
struct mallinfo m = mallinfo();
|
||||
return m.arena; // Non-mmapped space allocated (bytes)
|
||||
return m.arena + sbrkHeadroom(); // Non-mmapped space allocated + uncommitted sbrk headroom
|
||||
#else
|
||||
// this platform does not have heap management function implemented
|
||||
return UINT32_MAX;
|
||||
|
||||
@@ -48,7 +48,8 @@ bool mixWithLoRaEntropy(uint8_t *buffer, size_t length)
|
||||
// and return false so callers know no extra mixing occurred.
|
||||
RadioLibInterface *radio = RadioLibInterface::instance;
|
||||
if (!radio) {
|
||||
LOG_ERROR("No radio instance available to provide entropy");
|
||||
// Intentionally silent: this path runs during portduinoSetup() before the
|
||||
// console/SerialConsole is initialized, so LOG_* here would dereference a null pointer.
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "MeshTypes.h"
|
||||
#include "PointerQueue.h"
|
||||
#include "configuration.h"
|
||||
#include "detect/LoRaRadioType.h"
|
||||
|
||||
// Sentinel marking the end of a modem preset array
|
||||
static constexpr meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
|
||||
@@ -59,7 +60,7 @@ extern const RegionInfo *myRegion;
|
||||
extern void initRegion();
|
||||
|
||||
// Valid LoRa spread factor range and defaults
|
||||
constexpr uint8_t LORA_SF_MIN = 7;
|
||||
constexpr uint8_t LORA_SF_MIN = 5;
|
||||
constexpr uint8_t LORA_SF_MAX = 12;
|
||||
constexpr uint8_t LORA_SF_DEFAULT = 11; // LONG_FAST default
|
||||
|
||||
@@ -71,10 +72,14 @@ constexpr uint8_t LORA_CR_DEFAULT = 5; // LONG_FAST default
|
||||
// Default bandwidth in kHz (LONG_FAST)
|
||||
constexpr float LORA_BW_DEFAULT_KHZ = 250.0f;
|
||||
|
||||
/// Clamp spread factor to the valid LoRa range [7, 12].
|
||||
/// Clamp spread factor to the valid LoRa range [5, 12].
|
||||
/// Out-of-range values (including 0 from unset preset mode) return LORA_SF_DEFAULT.
|
||||
static inline uint8_t clampSpreadFactor(uint8_t sf)
|
||||
{
|
||||
// We check for RF95 radios that are incompatible with Spreading Factors 5 and 6.
|
||||
if (radioType == RF95_RADIO && (sf == 5 || sf == 6))
|
||||
return LORA_SF_DEFAULT;
|
||||
|
||||
if (sf < LORA_SF_MIN || sf > LORA_SF_MAX)
|
||||
return LORA_SF_DEFAULT;
|
||||
return sf;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Router.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "TransmitHistory.h"
|
||||
#include "TypeConversions.h"
|
||||
#include "error.h"
|
||||
#include "main.h"
|
||||
@@ -509,6 +510,12 @@ bool NodeDB::factoryReset(bool eraseBleBonds)
|
||||
}
|
||||
#endif
|
||||
spiLock->unlock();
|
||||
|
||||
// rmDir above nuked the .dat file, but TransmitHistory's in-memory
|
||||
// cache auto-flushes every 5 min and would resurrect it.
|
||||
if (transmitHistory) {
|
||||
transmitHistory->clear();
|
||||
}
|
||||
// second, install default state (this will deal with the duplicate mac address issue)
|
||||
installDefaultNodeDatabase();
|
||||
installDefaultDeviceState();
|
||||
|
||||
+18
-2
@@ -17,6 +17,7 @@
|
||||
#include "TypeConversions.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "main.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include "xmodem.h"
|
||||
|
||||
#if FromRadio_size > MAX_TO_FROM_RADIO_SIZE
|
||||
@@ -190,8 +191,23 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
break;
|
||||
#endif
|
||||
case meshtastic_ToRadio_heartbeat_tag:
|
||||
LOG_DEBUG("Got client heartbeat");
|
||||
heartbeatReceived = true;
|
||||
// nonce==1 is a special "nodeinfo ping" trigger: force a fresh
|
||||
// NodeInfo broadcast on the 60-second shorterTimeout path so
|
||||
// peers can re-learn our public key after a reboot or
|
||||
// factory_reset without waiting out the normal 10-minute
|
||||
// NodeInfo send cooldown. Mirrors the TCP/UDP path in
|
||||
// `src/mesh/api/PacketAPI.cpp:74-79` for serial clients.
|
||||
// Default nonce (0) remains a plain keepalive that triggers
|
||||
// a queue-status reply.
|
||||
if (toRadioScratch.heartbeat.nonce == 1) {
|
||||
if (nodeInfoModule) {
|
||||
LOG_INFO("Broadcasting nodeinfo ping (serial)");
|
||||
nodeInfoModule->sendOurNodeInfo(NODENUM_BROADCAST, true, 0, true);
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("Got client heartbeat");
|
||||
heartbeatReceived = true;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Ignore nop messages
|
||||
|
||||
@@ -455,6 +455,15 @@ template <typename T> void SX126xInterface<T>::resetAGC()
|
||||
// RX boosted gain mode
|
||||
lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
|
||||
// Re-apply the undocumented 0x8B5 RX sensitivity patch that was set in init().
|
||||
// The CALIBRATE_ALL (0x7F) command above clears bit 0 of register 0x8B5, which
|
||||
// silently removes the RX sensitivity improvement introduced in #9571 / #9777.
|
||||
// Without this re-apply, every SX1262 node loses its RX boost ~60s after boot
|
||||
// and never recovers until reboot. See empirical evidence in the PR description.
|
||||
if (module.SPIsetRegValue(0x8B5, 0x01, 0, 0) != RADIOLIB_ERR_NONE) {
|
||||
LOG_WARN("SX126x resetAGC: failed to re-apply 0x8B5 RX sensitivity patch");
|
||||
}
|
||||
|
||||
// 6. Resume receiving
|
||||
startReceive();
|
||||
}
|
||||
|
||||
+35
-10
@@ -2,6 +2,7 @@
|
||||
#include "PowerFSM.h"
|
||||
#include "RTC.h"
|
||||
#include "Throttle.h"
|
||||
#include "concurrency/LockGuard.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#define START1 0x94
|
||||
@@ -177,6 +178,9 @@ void StreamAPI::emitTxBuffer(size_t len)
|
||||
txBuf[3] = len & 0xff;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
// Serialize stream writes against `emitLogRecord` so a LOG_ firing
|
||||
// mid-packet-emission can't interleave bytes on the wire.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
stream->write(txBuf, totalLen);
|
||||
stream->flush();
|
||||
}
|
||||
@@ -195,21 +199,42 @@ void StreamAPI::emitRebooted()
|
||||
|
||||
void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg)
|
||||
{
|
||||
// In case we send a FromRadio packet
|
||||
memset(&fromRadioScratch, 0, sizeof(fromRadioScratch));
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_log_record_tag;
|
||||
fromRadioScratch.log_record.level = level;
|
||||
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here — those
|
||||
// belong to the main packet-emission path and a LOG_ firing during
|
||||
// `writeStream()` would corrupt an in-flight encode. We keep a
|
||||
// dedicated `fromRadioScratchLog` + `txBufLog` for log records and
|
||||
// only serialize the actual `stream->write` call via `streamLock` so
|
||||
// a concurrent packet emission doesn't interleave bytes on the wire.
|
||||
memset(&fromRadioScratchLog, 0, sizeof(fromRadioScratchLog));
|
||||
fromRadioScratchLog.which_payload_variant = meshtastic_FromRadio_log_record_tag;
|
||||
fromRadioScratchLog.log_record.level = level;
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
fromRadioScratch.log_record.time = rtc_sec;
|
||||
strncpy(fromRadioScratch.log_record.source, src, sizeof(fromRadioScratch.log_record.source) - 1);
|
||||
fromRadioScratchLog.log_record.time = rtc_sec;
|
||||
strncpy(fromRadioScratchLog.log_record.source, src, sizeof(fromRadioScratchLog.log_record.source) - 1);
|
||||
|
||||
auto num_printed =
|
||||
vsnprintf(fromRadioScratch.log_record.message, sizeof(fromRadioScratch.log_record.message) - 1, format, arg);
|
||||
if (num_printed > 0 && fromRadioScratch.log_record.message[num_printed - 1] ==
|
||||
vsnprintf(fromRadioScratchLog.log_record.message, sizeof(fromRadioScratchLog.log_record.message) - 1, format, arg);
|
||||
if (num_printed > 0 && fromRadioScratchLog.log_record.message[num_printed - 1] ==
|
||||
'\n') // Strip any ending newline, because we have records for framing instead.
|
||||
fromRadioScratch.log_record.message[num_printed - 1] = '\0';
|
||||
emitTxBuffer(pb_encode_to_bytes(txBuf + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratch));
|
||||
fromRadioScratchLog.log_record.message[num_printed - 1] = '\0';
|
||||
|
||||
size_t len =
|
||||
pb_encode_to_bytes(txBufLog + HEADER_LEN, meshtastic_FromRadio_size, &meshtastic_FromRadio_msg, &fromRadioScratchLog);
|
||||
if (len != 0) {
|
||||
txBufLog[0] = START1;
|
||||
txBufLog[1] = START2;
|
||||
txBufLog[2] = (len >> 8) & 0xff;
|
||||
txBufLog[3] = len & 0xff;
|
||||
|
||||
auto totalLen = len + HEADER_LEN;
|
||||
// Serialize stream writes against `emitTxBuffer` so a packet
|
||||
// emission in flight on another task doesn't interleave bytes
|
||||
// with this log record.
|
||||
concurrency::LockGuard guard(&streamLock);
|
||||
stream->write(txBufLog, totalLen);
|
||||
stream->flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// Hookable to find out when connection changes
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "PhoneAPI.h"
|
||||
#include "Stream.h"
|
||||
#include "concurrency/Lock.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <cstdarg>
|
||||
|
||||
@@ -89,4 +90,27 @@ class StreamAPI : public PhoneAPI
|
||||
|
||||
/// Low level function to emit a protobuf encapsulated log record
|
||||
void emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg);
|
||||
|
||||
private:
|
||||
/// Dedicated scratch + tx buffer for LogRecord emission.
|
||||
///
|
||||
/// The main packet emission path (`writeStream` -> `getFromRadio` ->
|
||||
/// `emitTxBuffer`) holds `fromRadioScratch` (from PhoneAPI) and `txBuf`
|
||||
/// from the moment `getFromRadio` starts encoding until `emitTxBuffer`
|
||||
/// finishes pushing bytes to the stream. If a `LOG_` macro fires during
|
||||
/// that window and we emit through the API, the old implementation
|
||||
/// re-used `fromRadioScratch` / `txBuf` and corrupted whatever the main
|
||||
/// path had already encoded. Symptoms on the host were
|
||||
/// `google.protobuf.message.DecodeError: Error parsing message with type
|
||||
/// 'meshtastic.protobuf.FromRadio'` — any tool with
|
||||
/// `config.security.debug_log_api_enabled=true` under traffic would see
|
||||
/// torn frames every few messages.
|
||||
///
|
||||
/// Giving the log path its own scratch + txBuf means the main path is
|
||||
/// never clobbered. We still need `streamLock` to serialize the actual
|
||||
/// `stream->write` call so a log emission and a packet emission don't
|
||||
/// interleave on the wire.
|
||||
meshtastic_FromRadio fromRadioScratchLog = {};
|
||||
uint8_t txBufLog[MAX_STREAM_BUF_SIZE] = {0};
|
||||
concurrency::Lock streamLock;
|
||||
};
|
||||
@@ -255,6 +255,21 @@ bool TransmitHistory::saveToDisk()
|
||||
return false;
|
||||
}
|
||||
|
||||
void TransmitHistory::clear()
|
||||
{
|
||||
history.clear();
|
||||
lastMillis.clear();
|
||||
dirty = false;
|
||||
lastDiskSave = 0; // so the next legit broadcast persists immediately
|
||||
|
||||
spiLock->lock();
|
||||
if (FSCom.exists(FILENAME)) {
|
||||
FSCom.remove(FILENAME);
|
||||
}
|
||||
spiLock->unlock();
|
||||
LOG_INFO("TransmitHistory: cleared in-memory state + on-disk file");
|
||||
}
|
||||
|
||||
#else
|
||||
// No filesystem available — provide stub with in-memory tracking
|
||||
TransmitHistory *transmitHistory = nullptr;
|
||||
@@ -290,4 +305,10 @@ bool TransmitHistory::saveToDisk()
|
||||
return true;
|
||||
}
|
||||
|
||||
void TransmitHistory::clear()
|
||||
{
|
||||
history.clear();
|
||||
lastMillis.clear();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -76,6 +76,13 @@ class TransmitHistory
|
||||
*/
|
||||
bool saveToDisk();
|
||||
|
||||
/**
|
||||
* Wipe in-memory throttle state + remove the on-disk file. Required
|
||||
* alongside rmDir("/prefs") in factoryReset — otherwise the 5-min
|
||||
* auto-flush resurrects the file from the still-populated maps.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
private:
|
||||
TransmitHistory() = default;
|
||||
|
||||
|
||||
@@ -81,7 +81,9 @@ meshtastic_UserLite TypeConversions::ConvertToUserLite(meshtastic_User user)
|
||||
meshtastic_UserLite lite = meshtastic_UserLite_init_default;
|
||||
|
||||
strncpy(lite.long_name, user.long_name, sizeof(lite.long_name));
|
||||
lite.long_name[sizeof(lite.long_name) - 1] = '\0';
|
||||
strncpy(lite.short_name, user.short_name, sizeof(lite.short_name));
|
||||
lite.short_name[sizeof(lite.short_name) - 1] = '\0';
|
||||
lite.hw_model = user.hw_model;
|
||||
lite.role = user.role;
|
||||
lite.is_licensed = user.is_licensed;
|
||||
@@ -99,7 +101,9 @@ meshtastic_User TypeConversions::ConvertToUser(uint32_t nodeNum, meshtastic_User
|
||||
|
||||
snprintf(user.id, sizeof(user.id), "!%08x", nodeNum);
|
||||
strncpy(user.long_name, lite.long_name, sizeof(user.long_name));
|
||||
user.long_name[sizeof(user.long_name) - 1] = '\0';
|
||||
strncpy(user.short_name, lite.short_name, sizeof(user.short_name));
|
||||
user.short_name[sizeof(user.short_name) - 1] = '\0';
|
||||
user.hw_model = lite.hw_model;
|
||||
user.role = lite.role;
|
||||
user.is_licensed = lite.is_licensed;
|
||||
|
||||
@@ -27,6 +27,42 @@ PB_BIND(meshtastic_PLI, meshtastic_PLI, AUTO)
|
||||
PB_BIND(meshtastic_AircraftTrack, meshtastic_AircraftTrack, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_CotGeoPoint, meshtastic_CotGeoPoint, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_DrawnShape, meshtastic_DrawnShape, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Marker, meshtastic_Marker, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_RangeAndBearing, meshtastic_RangeAndBearing, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Route, meshtastic_Route, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Route_Link, meshtastic_Route_Link, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_CasevacReport, meshtastic_CasevacReport, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_ZMistEntry, meshtastic_ZMistEntry, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_EmergencyAlert, meshtastic_EmergencyAlert, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_TaskRequest, meshtastic_TaskRequest, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_TAKEnvironment, meshtastic_TAKEnvironment, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_SensorFov, meshtastic_SensorFov, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_TAKPacketV2, meshtastic_TAKPacketV2, 2)
|
||||
|
||||
|
||||
@@ -41,3 +77,29 @@ PB_BIND(meshtastic_TAKPacketV2, meshtastic_TAKPacketV2, 2)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -285,7 +285,11 @@ typedef enum _meshtastic_Config_LoRaConfig_RegionCode {
|
||||
/* Nepal 865MHz */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_NP_865 = 25,
|
||||
/* Brazil 902MHz */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26
|
||||
meshtastic_Config_LoRaConfig_RegionCode_BR_902 = 26,
|
||||
/* ITU Region 1 Amateur Radio 2m band (144-146 MHz) */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M = 27,
|
||||
/* ITU Region 2 / 3 Amateur Radio 2m band (144-148 MHz) */
|
||||
meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M = 28
|
||||
} meshtastic_Config_LoRaConfig_RegionCode;
|
||||
|
||||
/* Standard predefined channel settings
|
||||
@@ -702,8 +706,8 @@ extern "C" {
|
||||
#define _meshtastic_Config_DisplayConfig_CompassOrientation_ARRAYSIZE ((meshtastic_Config_DisplayConfig_CompassOrientation)(meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_MIN meshtastic_Config_LoRaConfig_RegionCode_UNSET
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_BR_902
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_BR_902+1))
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_MAX meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M
|
||||
#define _meshtastic_Config_LoRaConfig_RegionCode_ARRAYSIZE ((meshtastic_Config_LoRaConfig_RegionCode)(meshtastic_Config_LoRaConfig_RegionCode_ITU23_2M+1))
|
||||
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MIN meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST
|
||||
#define _meshtastic_Config_LoRaConfig_ModemPreset_MAX meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO
|
||||
|
||||
@@ -27,6 +27,9 @@ PB_BIND(meshtastic_KeyVerification, meshtastic_KeyVerification, AUTO)
|
||||
PB_BIND(meshtastic_StoreForwardPlusPlus, meshtastic_StoreForwardPlusPlus, 2)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_RemoteShell, meshtastic_RemoteShell, AUTO)
|
||||
|
||||
|
||||
PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO)
|
||||
|
||||
|
||||
@@ -129,6 +132,8 @@ PB_BIND(meshtastic_ChunkedPayloadResponse, meshtastic_ChunkedPayloadResponse, AU
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -308,8 +308,13 @@ typedef enum _meshtastic_HardwareModel {
|
||||
meshtastic_HardwareModel_TDISPLAY_S3_PRO = 126,
|
||||
/* Heltec Mesh Node T096 board features an nRF52840 CPU and a TFT screen. */
|
||||
meshtastic_HardwareModel_HELTEC_MESH_NODE_T096 = 127,
|
||||
/* Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio, GPS, button, buzzer, and sensors. */
|
||||
/* Seeed studio T1000-E Pro tracker card. NRF52840 w/ LR2021 radio,
|
||||
GPS, button, buzzer, and sensors. */
|
||||
meshtastic_HardwareModel_TRACKER_T1000_E_PRO = 128,
|
||||
/* Elecrow ThinkNode M7, M8 and M9 */
|
||||
meshtastic_HardwareModel_THINKNODE_M7 = 129,
|
||||
meshtastic_HardwareModel_THINKNODE_M8 = 130,
|
||||
meshtastic_HardwareModel_THINKNODE_M9 = 131,
|
||||
/* ------------------------------------------------------------------------------------------------------------------------------------------
|
||||
Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits.
|
||||
------------------------------------------------------------------------------------------------------------------------------------------ */
|
||||
@@ -513,6 +518,27 @@ typedef enum _meshtastic_StoreForwardPlusPlus_SFPP_message_type {
|
||||
meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF = 6
|
||||
} meshtastic_StoreForwardPlusPlus_SFPP_message_type;
|
||||
|
||||
/* Frame op code for PTY session control and stream transport.
|
||||
|
||||
Values 1-63 are client->server requests.
|
||||
Values 64-127 are server->client responses/events. */
|
||||
typedef enum _meshtastic_RemoteShell_OpCode {
|
||||
meshtastic_RemoteShell_OpCode_OP_UNSET = 0,
|
||||
/* Client -> server */
|
||||
meshtastic_RemoteShell_OpCode_OPEN = 1,
|
||||
meshtastic_RemoteShell_OpCode_INPUT = 2,
|
||||
meshtastic_RemoteShell_OpCode_RESIZE = 3,
|
||||
meshtastic_RemoteShell_OpCode_CLOSE = 4,
|
||||
meshtastic_RemoteShell_OpCode_PING = 5,
|
||||
meshtastic_RemoteShell_OpCode_ACK = 6,
|
||||
/* Server -> client */
|
||||
meshtastic_RemoteShell_OpCode_OPEN_OK = 64,
|
||||
meshtastic_RemoteShell_OpCode_OUTPUT = 65,
|
||||
meshtastic_RemoteShell_OpCode_CLOSED = 66,
|
||||
meshtastic_RemoteShell_OpCode_ERROR = 67,
|
||||
meshtastic_RemoteShell_OpCode_PONG = 68
|
||||
} meshtastic_RemoteShell_OpCode;
|
||||
|
||||
/* The priority of this message for sending.
|
||||
Higher priorities are sent first (when managing the transmit queue).
|
||||
This field is never sent over the air, it is only used internally inside of a local device node.
|
||||
@@ -845,6 +871,31 @@ typedef struct _meshtastic_StoreForwardPlusPlus {
|
||||
uint32_t chain_count;
|
||||
} meshtastic_StoreForwardPlusPlus;
|
||||
|
||||
typedef PB_BYTES_ARRAY_T(200) meshtastic_RemoteShell_payload_t;
|
||||
/* The actual over-the-mesh message doing RemoteShell */
|
||||
typedef struct _meshtastic_RemoteShell {
|
||||
/* Structured frame operation. */
|
||||
meshtastic_RemoteShell_OpCode op;
|
||||
/* Logical PTY session identifier. */
|
||||
uint32_t session_id;
|
||||
/* Monotonic sequence number for this frame. */
|
||||
uint32_t seq;
|
||||
/* Cumulative ack sequence number. */
|
||||
uint32_t ack_seq;
|
||||
/* Opaque bytes payload for INPUT/OUTPUT/ERROR and other frame bodies. */
|
||||
meshtastic_RemoteShell_payload_t payload;
|
||||
/* Terminal size columns used for OPEN/RESIZE signaling. */
|
||||
uint32_t cols;
|
||||
/* Terminal size rows used for OPEN/RESIZE signaling. */
|
||||
uint32_t rows;
|
||||
/* Bit flags for protocol extensions. */
|
||||
uint32_t flags;
|
||||
/* The last sequence number TX'd. */
|
||||
uint32_t last_tx_seq;
|
||||
/* The last sequence number RX'd. */
|
||||
uint32_t last_rx_seq;
|
||||
} meshtastic_RemoteShell;
|
||||
|
||||
/* Waypoint message, used to share arbitrary locations across the mesh */
|
||||
typedef struct _meshtastic_Waypoint {
|
||||
/* Id of the waypoint */
|
||||
@@ -1385,6 +1436,10 @@ extern "C" {
|
||||
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_MAX meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF
|
||||
#define _meshtastic_StoreForwardPlusPlus_SFPP_message_type_ARRAYSIZE ((meshtastic_StoreForwardPlusPlus_SFPP_message_type)(meshtastic_StoreForwardPlusPlus_SFPP_message_type_LINK_PROVIDE_SECONDHALF+1))
|
||||
|
||||
#define _meshtastic_RemoteShell_OpCode_MIN meshtastic_RemoteShell_OpCode_OP_UNSET
|
||||
#define _meshtastic_RemoteShell_OpCode_MAX meshtastic_RemoteShell_OpCode_PONG
|
||||
#define _meshtastic_RemoteShell_OpCode_ARRAYSIZE ((meshtastic_RemoteShell_OpCode)(meshtastic_RemoteShell_OpCode_PONG+1))
|
||||
|
||||
#define _meshtastic_MeshPacket_Priority_MIN meshtastic_MeshPacket_Priority_UNSET
|
||||
#define _meshtastic_MeshPacket_Priority_MAX meshtastic_MeshPacket_Priority_MAX
|
||||
#define _meshtastic_MeshPacket_Priority_ARRAYSIZE ((meshtastic_MeshPacket_Priority)(meshtastic_MeshPacket_Priority_MAX+1))
|
||||
@@ -1415,6 +1470,8 @@ extern "C" {
|
||||
|
||||
#define meshtastic_StoreForwardPlusPlus_sfpp_message_type_ENUMTYPE meshtastic_StoreForwardPlusPlus_SFPP_message_type
|
||||
|
||||
#define meshtastic_RemoteShell_op_ENUMTYPE meshtastic_RemoteShell_OpCode
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1459,6 +1516,7 @@ extern "C" {
|
||||
#define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
|
||||
#define meshtastic_KeyVerification_init_default {0, {0, {0}}, {0, {0}}}
|
||||
#define meshtastic_StoreForwardPlusPlus_init_default {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_RemoteShell_init_default {_meshtastic_RemoteShell_OpCode_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_Waypoint_init_default {0, false, 0, false, 0, 0, 0, "", "", 0}
|
||||
#define meshtastic_StatusMessage_init_default {""}
|
||||
#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0}
|
||||
@@ -1492,6 +1550,7 @@ extern "C" {
|
||||
#define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0, false, 0}
|
||||
#define meshtastic_KeyVerification_init_zero {0, {0, {0}}, {0, {0}}}
|
||||
#define meshtastic_StoreForwardPlusPlus_init_zero {_meshtastic_StoreForwardPlusPlus_SFPP_message_type_MIN, {0, {0}}, {0, {0}}, {0, {0}}, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_RemoteShell_init_zero {_meshtastic_RemoteShell_OpCode_MIN, 0, 0, 0, {0, {0}}, 0, 0, 0, 0, 0}
|
||||
#define meshtastic_Waypoint_init_zero {0, false, 0, false, 0, 0, 0, "", "", 0}
|
||||
#define meshtastic_StatusMessage_init_zero {""}
|
||||
#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0}
|
||||
@@ -1581,6 +1640,16 @@ extern "C" {
|
||||
#define meshtastic_StoreForwardPlusPlus_encapsulated_from_tag 8
|
||||
#define meshtastic_StoreForwardPlusPlus_encapsulated_rxtime_tag 9
|
||||
#define meshtastic_StoreForwardPlusPlus_chain_count_tag 10
|
||||
#define meshtastic_RemoteShell_op_tag 1
|
||||
#define meshtastic_RemoteShell_session_id_tag 2
|
||||
#define meshtastic_RemoteShell_seq_tag 3
|
||||
#define meshtastic_RemoteShell_ack_seq_tag 4
|
||||
#define meshtastic_RemoteShell_payload_tag 5
|
||||
#define meshtastic_RemoteShell_cols_tag 6
|
||||
#define meshtastic_RemoteShell_rows_tag 7
|
||||
#define meshtastic_RemoteShell_flags_tag 8
|
||||
#define meshtastic_RemoteShell_last_tx_seq_tag 9
|
||||
#define meshtastic_RemoteShell_last_rx_seq_tag 10
|
||||
#define meshtastic_Waypoint_id_tag 1
|
||||
#define meshtastic_Waypoint_latitude_i_tag 2
|
||||
#define meshtastic_Waypoint_longitude_i_tag 3
|
||||
@@ -1813,6 +1882,20 @@ X(a, STATIC, SINGULAR, UINT32, chain_count, 10)
|
||||
#define meshtastic_StoreForwardPlusPlus_CALLBACK NULL
|
||||
#define meshtastic_StoreForwardPlusPlus_DEFAULT NULL
|
||||
|
||||
#define meshtastic_RemoteShell_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UENUM, op, 1) \
|
||||
X(a, STATIC, SINGULAR, UINT32, session_id, 2) \
|
||||
X(a, STATIC, SINGULAR, UINT32, seq, 3) \
|
||||
X(a, STATIC, SINGULAR, UINT32, ack_seq, 4) \
|
||||
X(a, STATIC, SINGULAR, BYTES, payload, 5) \
|
||||
X(a, STATIC, SINGULAR, UINT32, cols, 6) \
|
||||
X(a, STATIC, SINGULAR, UINT32, rows, 7) \
|
||||
X(a, STATIC, SINGULAR, UINT32, flags, 8) \
|
||||
X(a, STATIC, SINGULAR, UINT32, last_tx_seq, 9) \
|
||||
X(a, STATIC, SINGULAR, UINT32, last_rx_seq, 10)
|
||||
#define meshtastic_RemoteShell_CALLBACK NULL
|
||||
#define meshtastic_RemoteShell_DEFAULT NULL
|
||||
|
||||
#define meshtastic_Waypoint_FIELDLIST(X, a) \
|
||||
X(a, STATIC, SINGULAR, UINT32, id, 1) \
|
||||
X(a, STATIC, OPTIONAL, SFIXED32, latitude_i, 2) \
|
||||
@@ -2095,6 +2178,7 @@ extern const pb_msgdesc_t meshtastic_Routing_msg;
|
||||
extern const pb_msgdesc_t meshtastic_Data_msg;
|
||||
extern const pb_msgdesc_t meshtastic_KeyVerification_msg;
|
||||
extern const pb_msgdesc_t meshtastic_StoreForwardPlusPlus_msg;
|
||||
extern const pb_msgdesc_t meshtastic_RemoteShell_msg;
|
||||
extern const pb_msgdesc_t meshtastic_Waypoint_msg;
|
||||
extern const pb_msgdesc_t meshtastic_StatusMessage_msg;
|
||||
extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg;
|
||||
@@ -2130,6 +2214,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_Data_fields &meshtastic_Data_msg
|
||||
#define meshtastic_KeyVerification_fields &meshtastic_KeyVerification_msg
|
||||
#define meshtastic_StoreForwardPlusPlus_fields &meshtastic_StoreForwardPlusPlus_msg
|
||||
#define meshtastic_RemoteShell_fields &meshtastic_RemoteShell_msg
|
||||
#define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg
|
||||
#define meshtastic_StatusMessage_fields &meshtastic_StatusMessage_msg
|
||||
#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg
|
||||
@@ -2185,6 +2270,7 @@ extern const pb_msgdesc_t meshtastic_ChunkedPayloadResponse_msg;
|
||||
#define meshtastic_NodeRemoteHardwarePin_size 29
|
||||
#define meshtastic_Position_size 144
|
||||
#define meshtastic_QueueStatus_size 23
|
||||
#define meshtastic_RemoteShell_size 253
|
||||
#define meshtastic_RouteDiscovery_size 256
|
||||
#define meshtastic_Routing_size 259
|
||||
#define meshtastic_StatusMessage_size 81
|
||||
|
||||
@@ -76,6 +76,8 @@ typedef enum _meshtastic_PortNum {
|
||||
meshtastic_PortNum_ALERT_APP = 11,
|
||||
/* Module/port for handling key verification requests. */
|
||||
meshtastic_PortNum_KEY_VERIFICATION_APP = 12,
|
||||
/* Module/port for handling primitive remote shell access. */
|
||||
meshtastic_PortNum_REMOTE_SHELL_APP = 13,
|
||||
/* Provides a 'ping' service that replies to any packet it receives.
|
||||
Also serves as a small example module.
|
||||
ENCODING: ASCII Plaintext */
|
||||
|
||||
@@ -113,7 +113,9 @@ typedef enum _meshtastic_TelemetrySensorType {
|
||||
/* SCD30 CO2, humidity, temperature sensor */
|
||||
meshtastic_TelemetrySensorType_SCD30 = 49,
|
||||
/* SHT family of sensors for temperature and humidity */
|
||||
meshtastic_TelemetrySensorType_SHTXX = 50
|
||||
meshtastic_TelemetrySensorType_SHTXX = 50,
|
||||
/* DS248X Bridge for one-wire temperature sensors */
|
||||
meshtastic_TelemetrySensorType_DS248X = 51
|
||||
} meshtastic_TelemetrySensorType;
|
||||
|
||||
/* Struct definitions */
|
||||
@@ -206,6 +208,9 @@ typedef struct _meshtastic_EnvironmentMetrics {
|
||||
/* Soil temperature measured (*C) */
|
||||
bool has_soil_temperature;
|
||||
float soil_temperature;
|
||||
/* One-wire temperature (*C) */
|
||||
pb_size_t one_wire_temperature_count;
|
||||
float one_wire_temperature[8];
|
||||
} meshtastic_EnvironmentMetrics;
|
||||
|
||||
/* Power Metrics (voltage / current / etc) */
|
||||
@@ -491,8 +496,8 @@ extern "C" {
|
||||
|
||||
/* Helper constants for enums */
|
||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_SHTXX
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_SHTXX+1))
|
||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_DS248X
|
||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_DS248X+1))
|
||||
|
||||
|
||||
|
||||
@@ -508,7 +513,7 @@ extern "C" {
|
||||
|
||||
/* Initializer values for message structs */
|
||||
#define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}}
|
||||
#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
@@ -519,7 +524,7 @@ extern "C" {
|
||||
#define meshtastic_Nau7802Config_init_default {0, 0}
|
||||
#define meshtastic_SEN5XState_init_default {0, 0, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}}
|
||||
#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||
#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
@@ -558,6 +563,7 @@ extern "C" {
|
||||
#define meshtastic_EnvironmentMetrics_rainfall_24h_tag 20
|
||||
#define meshtastic_EnvironmentMetrics_soil_moisture_tag 21
|
||||
#define meshtastic_EnvironmentMetrics_soil_temperature_tag 22
|
||||
#define meshtastic_EnvironmentMetrics_one_wire_temperature_tag 23
|
||||
#define meshtastic_PowerMetrics_ch1_voltage_tag 1
|
||||
#define meshtastic_PowerMetrics_ch1_current_tag 2
|
||||
#define meshtastic_PowerMetrics_ch2_voltage_tag 3
|
||||
@@ -683,7 +689,8 @@ X(a, STATIC, OPTIONAL, FLOAT, radiation, 18) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, rainfall_1h, 19) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, rainfall_24h, 20) \
|
||||
X(a, STATIC, OPTIONAL, UINT32, soil_moisture, 21) \
|
||||
X(a, STATIC, OPTIONAL, FLOAT, soil_temperature, 22)
|
||||
X(a, STATIC, OPTIONAL, FLOAT, soil_temperature, 22) \
|
||||
X(a, STATIC, REPEATED, FLOAT, one_wire_temperature, 23)
|
||||
#define meshtastic_EnvironmentMetrics_CALLBACK NULL
|
||||
#define meshtastic_EnvironmentMetrics_DEFAULT NULL
|
||||
|
||||
@@ -852,7 +859,7 @@ extern const pb_msgdesc_t meshtastic_SEN5XState_msg;
|
||||
#define MESHTASTIC_MESHTASTIC_TELEMETRY_PB_H_MAX_SIZE meshtastic_Telemetry_size
|
||||
#define meshtastic_AirQualityMetrics_size 150
|
||||
#define meshtastic_DeviceMetrics_size 27
|
||||
#define meshtastic_EnvironmentMetrics_size 113
|
||||
#define meshtastic_EnvironmentMetrics_size 161
|
||||
#define meshtastic_HealthMetrics_size 11
|
||||
#define meshtastic_HostMetrics_size 264
|
||||
#define meshtastic_LocalStats_size 87
|
||||
|
||||
@@ -480,7 +480,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
#if HAS_SCREEN
|
||||
IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
|
||||
#endif
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_RP2040)
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_RP2040) || defined(ARCH_STM32WL)
|
||||
enterDfuMode();
|
||||
#endif
|
||||
break;
|
||||
@@ -1519,8 +1519,15 @@ void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent
|
||||
LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code,
|
||||
inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y);
|
||||
|
||||
// Create InputEvent for injection
|
||||
InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code,
|
||||
// Create InputEvent for injection.
|
||||
//
|
||||
// `.source` MUST be a non-null C string: the LOG_INFO below formats it
|
||||
// with %s, and passing NULL to the esp-log formatter crashes with
|
||||
// Guru Meditation LoadProhibited at strlen(NULL). Other InputBroker
|
||||
// sources (buttons, rotary) always set this; the admin path was the
|
||||
// only one leaving it default-null.
|
||||
InputEvent event = {.source = "admin",
|
||||
.inputEvent = (input_broker_event)inputEvent.event_code,
|
||||
.kbchar = (unsigned char)inputEvent.kb_char,
|
||||
.touchX = inputEvent.touch_x,
|
||||
.touchY = inputEvent.touch_y};
|
||||
|
||||
@@ -29,10 +29,23 @@ int32_t StatusMessageModule::runOnce()
|
||||
ProcessMessage StatusMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
|
||||
meshtastic_StatusMessage incomingMessage;
|
||||
meshtastic_StatusMessage incomingMessage = meshtastic_StatusMessage_init_zero;
|
||||
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_StatusMessage_fields,
|
||||
&incomingMessage)) {
|
||||
|
||||
LOG_INFO("Received a NodeStatus message %s", incomingMessage.status);
|
||||
|
||||
RecentStatus entry;
|
||||
entry.fromNodeId = mp.from;
|
||||
entry.statusText = incomingMessage.status;
|
||||
|
||||
recentReceived.push_back(std::move(entry));
|
||||
|
||||
// Keep only last MAX_RECENT_STATUSMESSAGES
|
||||
if (recentReceived.size() > MAX_RECENT_STATUSMESSAGES) {
|
||||
recentReceived.erase(recentReceived.begin()); // drop oldest
|
||||
}
|
||||
}
|
||||
}
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
#if !MESHTASTIC_EXCLUDE_STATUS
|
||||
#include "SinglePortModule.h"
|
||||
#include "configuration.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class StatusMessageModule : public SinglePortModule, private concurrency::OSThread
|
||||
{
|
||||
|
||||
public:
|
||||
/** Constructor
|
||||
* name is for debugging output
|
||||
@@ -19,16 +20,28 @@ class StatusMessageModule : public SinglePortModule, private concurrency::OSThre
|
||||
this->setInterval(1000 * 12 * 60 * 60);
|
||||
}
|
||||
// TODO: If we have a string, set the initial delay (15 minutes maybe)
|
||||
|
||||
// Keep vector from reallocating as we fill up to MAX_RECENT_STATUSMESSAGES
|
||||
recentReceived.reserve(MAX_RECENT_STATUSMESSAGES);
|
||||
}
|
||||
|
||||
virtual int32_t runOnce() override;
|
||||
|
||||
struct RecentStatus {
|
||||
uint32_t fromNodeId; // mp.from
|
||||
std::string statusText; // incomingMessage.status
|
||||
};
|
||||
|
||||
const std::vector<RecentStatus> &getRecentReceived() const { return recentReceived; }
|
||||
|
||||
protected:
|
||||
/** Called to handle a particular incoming message
|
||||
*/
|
||||
virtual ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
private:
|
||||
static constexpr size_t MAX_RECENT_STATUSMESSAGES = 5;
|
||||
std::vector<RecentStatus> recentReceived;
|
||||
};
|
||||
|
||||
extern StatusMessageModule *statusMessageModule;
|
||||
|
||||
@@ -15,15 +15,6 @@
|
||||
|
||||
WaypointModule *waypointModule;
|
||||
|
||||
static inline float degToRad(float deg)
|
||||
{
|
||||
return deg * PI / 180.0f;
|
||||
}
|
||||
static inline float radToDeg(float rad)
|
||||
{
|
||||
return rad * 180.0f / PI;
|
||||
}
|
||||
|
||||
ProcessMessage WaypointModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
#if defined(DEBUG_PORT) && !defined(DEBUG_MUTE)
|
||||
@@ -91,9 +82,7 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||
|
||||
const int w = display->getWidth();
|
||||
const int h = display->getHeight();
|
||||
const int *textPos = graphics::getTextPositions(display);
|
||||
|
||||
// Decode the waypoint
|
||||
const meshtastic_MeshPacket &mp = devicestate.rx_waypoint;
|
||||
@@ -108,71 +97,118 @@ void WaypointModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state,
|
||||
getTimeAgoStr(sinceReceived(&mp), lastStr, sizeof(lastStr));
|
||||
|
||||
// Will contain distance information, passed as a field to drawColumns
|
||||
char distStr[20];
|
||||
char distStr[20] = "";
|
||||
|
||||
// Get our node, to use our own position
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
|
||||
// Dimensions / co-ordinates for the compass/circle
|
||||
const uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(w, h);
|
||||
const int16_t compassX = x + w - (compassDiam / 2) - 5;
|
||||
const int16_t compassY = (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT)
|
||||
? y + h / 2
|
||||
: y + FONT_HEIGHT_SMALL + (h - FONT_HEIGHT_SMALL) / 2;
|
||||
// Match compass sizing/placement to favorite node screen logic.
|
||||
const int w = display->getWidth();
|
||||
int16_t compassRadius = 8;
|
||||
int16_t compassX = x + w - compassRadius - 8;
|
||||
int16_t compassY = y + display->getHeight() / 2;
|
||||
|
||||
// If our node has a position:
|
||||
if (ourNode && (nodeDB->hasValidPosition(ourNode) || screen->hasHeading())) {
|
||||
const meshtastic_PositionLite &op = ourNode->position;
|
||||
float myHeading;
|
||||
if (uiconfig.compass_mode == meshtastic_CompassMode_FREEZE_HEADING) {
|
||||
myHeading = 0;
|
||||
} else {
|
||||
if (screen->hasHeading())
|
||||
myHeading = degToRad(screen->getHeading());
|
||||
else
|
||||
myHeading = screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
if (SCREEN_WIDTH > SCREEN_HEIGHT) {
|
||||
const int16_t topY = textPos[1];
|
||||
const int16_t bottomY = SCREEN_HEIGHT - (FONT_HEIGHT_SMALL - 1);
|
||||
const int16_t usableHeight = bottomY - topY - 5;
|
||||
compassRadius = usableHeight / 2;
|
||||
if (compassRadius < 8)
|
||||
compassRadius = 8;
|
||||
compassX = x + SCREEN_WIDTH - compassRadius - 8;
|
||||
compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||
} else {
|
||||
// Waypoint content uses rows 1..4, so place the compass below that block.
|
||||
const int yBelowContent = textPos[4] + FONT_HEIGHT_SMALL + 2;
|
||||
const int margin = 4;
|
||||
#if defined(USE_EINK)
|
||||
const int iconSize = (graphics::currentResolution == graphics::ScreenResolution::High) ? 16 : 8;
|
||||
const int navBarHeight = iconSize + 6;
|
||||
#else
|
||||
const int navBarHeight = 0;
|
||||
#endif
|
||||
const int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||
if (availableHeight > 0) {
|
||||
compassRadius = availableHeight / 2;
|
||||
if (compassRadius < 8)
|
||||
compassRadius = 8;
|
||||
if (compassRadius * 2 > SCREEN_WIDTH - 16)
|
||||
compassRadius = (SCREEN_WIDTH - 16) / 2;
|
||||
if (compassRadius < 8)
|
||||
compassRadius = 8;
|
||||
compassX = x + SCREEN_WIDTH / 2;
|
||||
compassY = yBelowContent + availableHeight / 2;
|
||||
}
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, (compassDiam / 2));
|
||||
}
|
||||
const uint16_t compassDiam = compassRadius * 2;
|
||||
|
||||
// Compass bearing to waypoint
|
||||
float bearingToOther =
|
||||
GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i));
|
||||
// If the top of the compass is a static north then bearingToOther can be drawn on the compass directly
|
||||
// If the top of the compass is not a static north we need adjust bearingToOther based on heading
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FREEZE_HEADING)
|
||||
bearingToOther -= myHeading;
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
||||
const bool hasOwnPositionFix = (ourNode && nodeDB->hasValidPosition(ourNode));
|
||||
const char *statusLine1 = nullptr;
|
||||
const char *statusLine2 = nullptr;
|
||||
|
||||
float bearingToOtherDegrees = (bearingToOther < 0) ? bearingToOther + 2 * PI : bearingToOther;
|
||||
bearingToOtherDegrees = radToDeg(bearingToOtherDegrees);
|
||||
// Distance only needs our own position fix; compass/bearing additionally needs heading.
|
||||
if (hasOwnPositionFix) {
|
||||
const meshtastic_PositionLite &op = ourNode->position;
|
||||
const float d =
|
||||
GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
|
||||
// Distance to Waypoint
|
||||
float d = GeoCoord::latLongToMeter(DegD(wp.latitude_i), DegD(wp.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||
// Always show distance once we have an own-position fix, even without heading.
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
float feet = d * METERS_TO_FEET;
|
||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
|
||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
|
||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft" : "%.1fmi",
|
||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET);
|
||||
} else {
|
||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
|
||||
bearingToOtherDegrees);
|
||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm" : "%.1fkm", d < 2000 ? d : d / 1000);
|
||||
}
|
||||
|
||||
float myHeading = 0.0f;
|
||||
const bool hasHeading =
|
||||
graphics::CompassRenderer::getHeadingRadians(DegD(op.latitude_i), DegD(op.longitude_i), myHeading);
|
||||
if (hasHeading) {
|
||||
// Draw compass circle
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, myHeading, compassRadius);
|
||||
|
||||
// Compass bearing to waypoint
|
||||
float bearingToOther =
|
||||
GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(wp.latitude_i), DegD(wp.longitude_i));
|
||||
bearingToOther = graphics::CompassRenderer::adjustBearingForCompassMode(bearingToOther, myHeading);
|
||||
graphics::CompassRenderer::drawNodeHeading(display, compassX, compassY, compassDiam, bearingToOther);
|
||||
|
||||
const float bearingToOtherDegrees = graphics::CompassRenderer::radiansToDegrees360(bearingToOther);
|
||||
|
||||
// Distance to waypoint with relative bearing when heading is available.
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
float feet = d * METERS_TO_FEET;
|
||||
snprintf(distStr, sizeof(distStr), feet < (2 * MILES_TO_FEET) ? "%.0fft %.0f°" : "%.1fmi %.0f°",
|
||||
feet < (2 * MILES_TO_FEET) ? feet : feet / MILES_TO_FEET, bearingToOtherDegrees);
|
||||
} else {
|
||||
snprintf(distStr, sizeof(distStr), d < 2000 ? "%.0fm %.0f°" : "%.1fkm %.0f°", d < 2000 ? d : d / 1000,
|
||||
bearingToOtherDegrees);
|
||||
}
|
||||
|
||||
} else {
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Heading";
|
||||
}
|
||||
} else {
|
||||
// No own fix yet, so compass/bearing data would be misleading.
|
||||
statusLine1 = "No";
|
||||
statusLine2 = "Fix";
|
||||
}
|
||||
|
||||
else {
|
||||
display->drawString(compassX - FONT_HEIGHT_SMALL / 4, compassY - FONT_HEIGHT_SMALL / 2, "?");
|
||||
|
||||
// ? in the distance field
|
||||
snprintf(distStr, sizeof(distStr), "? %s ?°",
|
||||
(config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) ? "mi" : "km");
|
||||
if (statusLine1) {
|
||||
display->drawCircle(compassX, compassY, compassRadius);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(compassX, compassY - FONT_HEIGHT_SMALL, statusLine1);
|
||||
display->drawString(compassX, compassY, statusLine2);
|
||||
}
|
||||
|
||||
// Draw compass circle
|
||||
display->drawCircle(compassX, compassY, compassDiam / 2);
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // Something above me changes to a different alignment, forcing a fix here!
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], lastStr);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], wp.name);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], wp.description);
|
||||
display->drawString(0, graphics::getTextPositions(display)[line++], distStr);
|
||||
display->drawString(0, textPos[line++], lastStr);
|
||||
display->drawString(0, textPos[line++], wp.name);
|
||||
display->drawString(0, textPos[line++], wp.description);
|
||||
if (distStr[0])
|
||||
display->drawString(0, textPos[line++], distStr);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
extern graphics::Screen *screen;
|
||||
#endif
|
||||
|
||||
// Flag when an interrupt has been detected
|
||||
volatile static bool BMM150_IRQ = false;
|
||||
|
||||
BMM150Sensor::BMM150Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {}
|
||||
|
||||
bool BMM150Sensor::init()
|
||||
@@ -23,24 +20,7 @@ int32_t BMM150Sensor::runOnce()
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
float heading = sensor->getCompassDegree();
|
||||
|
||||
switch (config.display.compass_orientation) {
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||
heading += 90;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||
heading += 180;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||
heading += 270;
|
||||
break;
|
||||
}
|
||||
heading = applyCompassOrientation(heading);
|
||||
if (screen)
|
||||
screen->setHeading(heading);
|
||||
#endif
|
||||
@@ -90,4 +70,4 @@ bool BMM150Singleton::init(ScanI2C::FoundDevice device)
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -16,6 +16,7 @@ bool BMX160Sensor::init()
|
||||
if (sensor.begin()) {
|
||||
// set output data rate
|
||||
sensor.ODR_Config(BMX160_ACCEL_ODR_100HZ, BMX160_GYRO_ODR_100HZ);
|
||||
loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
LOG_DEBUG("BMX160 init ok");
|
||||
return true;
|
||||
}
|
||||
@@ -33,42 +34,12 @@ int32_t BMX160Sensor::runOnce()
|
||||
sensor.getAllData(&magAccel, NULL, &gAccel);
|
||||
|
||||
if (doCalibration) {
|
||||
|
||||
if (!showingScreen) {
|
||||
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
||||
showingScreen = true;
|
||||
if (screen)
|
||||
screen->startAlert((FrameCallback)drawFrameCalibration);
|
||||
}
|
||||
|
||||
if (magAccel.x > highestX)
|
||||
highestX = magAccel.x;
|
||||
if (magAccel.x < lowestX)
|
||||
lowestX = magAccel.x;
|
||||
if (magAccel.y > highestY)
|
||||
highestY = magAccel.y;
|
||||
if (magAccel.y < lowestY)
|
||||
lowestY = magAccel.y;
|
||||
if (magAccel.z > highestZ)
|
||||
highestZ = magAccel.z;
|
||||
if (magAccel.z < lowestZ)
|
||||
lowestZ = magAccel.z;
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now > endCalibrationAt) {
|
||||
doCalibration = false;
|
||||
endCalibrationAt = 0;
|
||||
showingScreen = false;
|
||||
if (screen)
|
||||
screen->endAlert();
|
||||
}
|
||||
|
||||
// LOG_DEBUG("BMX160 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX,
|
||||
// lowestY, highestY, lowestZ, highestZ);
|
||||
beginCalibrationDisplay(showingScreen);
|
||||
updateCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
|
||||
lowestZ);
|
||||
}
|
||||
|
||||
int highestRealX = highestX - (highestX + lowestX) / 2;
|
||||
|
||||
magAccel.x -= (highestX + lowestX) / 2;
|
||||
magAccel.y -= (highestY + lowestY) / 2;
|
||||
magAccel.z -= (highestZ + lowestZ) / 2;
|
||||
@@ -88,23 +59,7 @@ int32_t BMX160Sensor::runOnce()
|
||||
|
||||
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
|
||||
|
||||
switch (config.display.compass_orientation) {
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||
heading += 90;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||
heading += 180;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||
heading += 270;
|
||||
break;
|
||||
}
|
||||
heading = applyCompassOrientation(heading);
|
||||
if (screen)
|
||||
screen->setHeading(heading);
|
||||
#endif
|
||||
@@ -119,15 +74,8 @@ void BMX160Sensor::calibrate(uint16_t forSeconds)
|
||||
sBmx160SensorData_t gAccel;
|
||||
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
||||
sensor.getAllData(&magAccel, NULL, &gAccel);
|
||||
highestX = magAccel.x, lowestX = magAccel.x;
|
||||
highestY = magAccel.y, lowestY = magAccel.y;
|
||||
highestZ = magAccel.z, lowestZ = magAccel.z;
|
||||
|
||||
doCalibration = true;
|
||||
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
|
||||
endCalibrationAt = millis() + calibrateFor;
|
||||
if (screen)
|
||||
screen->setEndCalibration(endCalibrationAt);
|
||||
seedCalibrationExtrema(magAccel.x, magAccel.y, magAccel.z, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
startCalibrationWindow(forSeconds);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class BMX160Sensor : public MotionSensor
|
||||
private:
|
||||
RAK_BMX160 sensor;
|
||||
bool showingScreen = false;
|
||||
static constexpr const char *compassCalibrationFileName = "/prefs/compass_bmx160.dat";
|
||||
float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||
|
||||
public:
|
||||
@@ -39,4 +40,4 @@ class BMX160Sensor : public MotionSensor
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -26,7 +26,11 @@ bool ICM20948Sensor::init()
|
||||
return false;
|
||||
|
||||
// Enable simple Wake on Motion
|
||||
return sensor->setWakeOnMotion();
|
||||
bool wakeOnMotionOk = sensor->setWakeOnMotion();
|
||||
if (wakeOnMotionOk) {
|
||||
loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
}
|
||||
return wakeOnMotionOk;
|
||||
}
|
||||
|
||||
#ifdef ICM_20948_INT_PIN
|
||||
@@ -47,7 +51,8 @@ int32_t ICM20948Sensor::runOnce()
|
||||
int32_t ICM20948Sensor::runOnce()
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
if (screen && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion && !config.device.double_tap_as_button_press) {
|
||||
if (screen && !doCalibration && !screen->isScreenOn() && !config.display.wake_on_tap_or_motion &&
|
||||
!config.device.double_tap_as_button_press) {
|
||||
if (!isAsleep) {
|
||||
LOG_DEBUG("sleeping IMU");
|
||||
sensor->sleep(true);
|
||||
@@ -69,38 +74,10 @@ int32_t ICM20948Sensor::runOnce()
|
||||
}
|
||||
|
||||
if (doCalibration) {
|
||||
|
||||
if (!showingScreen) {
|
||||
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
||||
showingScreen = true;
|
||||
if (screen)
|
||||
screen->startAlert((FrameCallback)drawFrameCalibration);
|
||||
}
|
||||
|
||||
if (magX > highestX)
|
||||
highestX = magX;
|
||||
if (magX < lowestX)
|
||||
lowestX = magX;
|
||||
if (magY > highestY)
|
||||
highestY = magY;
|
||||
if (magY < lowestY)
|
||||
lowestY = magY;
|
||||
if (magZ > highestZ)
|
||||
highestZ = magZ;
|
||||
if (magZ < lowestZ)
|
||||
lowestZ = magZ;
|
||||
|
||||
uint32_t now = millis();
|
||||
if (now > endCalibrationAt) {
|
||||
doCalibration = false;
|
||||
endCalibrationAt = 0;
|
||||
showingScreen = false;
|
||||
if (screen)
|
||||
screen->endAlert();
|
||||
}
|
||||
|
||||
// LOG_DEBUG("ICM20948 min_x: %.4f, max_X: %.4f, min_Y: %.4f, max_Y: %.4f, min_Z: %.4f, max_Z: %.4f", lowestX, highestX,
|
||||
// lowestY, highestY, lowestZ, highestZ);
|
||||
beginCalibrationDisplay(showingScreen);
|
||||
updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ,
|
||||
lowestZ);
|
||||
}
|
||||
|
||||
magX -= (highestX + lowestX) / 2;
|
||||
@@ -122,23 +99,7 @@ int32_t ICM20948Sensor::runOnce()
|
||||
|
||||
float heading = FusionCompassCalculateHeading(FusionConventionNed, ga, ma);
|
||||
|
||||
switch (config.display.compass_orientation) {
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0_INVERTED:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0:
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||
heading += 90;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||
heading += 180;
|
||||
break;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||
heading += 270;
|
||||
break;
|
||||
}
|
||||
heading = applyCompassOrientation(heading);
|
||||
if (screen)
|
||||
screen->setHeading(heading);
|
||||
#endif
|
||||
@@ -169,26 +130,16 @@ int32_t ICM20948Sensor::runOnce()
|
||||
void ICM20948Sensor::calibrate(uint16_t forSeconds)
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
LOG_DEBUG("Old calibration data: highestX = %f, lowestX = %f, highestY = %f, lowestY = %f, highestZ = %f, lowestZ = %f",
|
||||
highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
LOG_DEBUG("BMX160 calibration started for %is", forSeconds);
|
||||
LOG_DEBUG("ICM20948 cal start %is", forSeconds);
|
||||
if (sensor->dataReady()) {
|
||||
sensor->getAGMT();
|
||||
highestX = sensor->agmt.mag.axes.x;
|
||||
lowestX = sensor->agmt.mag.axes.x;
|
||||
highestY = sensor->agmt.mag.axes.y;
|
||||
lowestY = sensor->agmt.mag.axes.y;
|
||||
highestZ = sensor->agmt.mag.axes.z;
|
||||
lowestZ = sensor->agmt.mag.axes.z;
|
||||
seedCalibrationExtrema(sensor->agmt.mag.axes.x, sensor->agmt.mag.axes.y, sensor->agmt.mag.axes.z, highestX, lowestX,
|
||||
highestY, lowestY, highestZ, lowestZ);
|
||||
} else {
|
||||
highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0;
|
||||
seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
}
|
||||
|
||||
doCalibration = true;
|
||||
uint16_t calibrateFor = forSeconds * 1000; // calibrate for seconds provided
|
||||
endCalibrationAt = millis() + calibrateFor;
|
||||
if (screen)
|
||||
screen->setEndCalibration(endCalibrationAt);
|
||||
startCalibrationWindow(forSeconds);
|
||||
#endif
|
||||
}
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -314,11 +265,6 @@ bool ICM20948Singleton::setWakeOnMotion()
|
||||
status = intEnableWOM(true);
|
||||
LOG_DEBUG("ICM20948 init set intEnableWOM - %s", statusString());
|
||||
return status == ICM_20948_Stat_Ok;
|
||||
|
||||
// Clear any current interrupts
|
||||
ICM20948_IRQ = false;
|
||||
clearInterrupts();
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -83,6 +83,7 @@ class ICM20948Sensor : public MotionSensor
|
||||
ICM20948Singleton *sensor = nullptr;
|
||||
bool showingScreen = false;
|
||||
bool isAsleep = false;
|
||||
static constexpr const char *compassCalibrationFileName = "/prefs/compass_icm20948.dat";
|
||||
#ifdef MUZI_BASE
|
||||
float highestX = 449.000000, lowestX = -140.000000, highestY = 422.000000, lowestY = -232.000000, highestZ = 749.000000,
|
||||
lowestZ = 98.000000;
|
||||
@@ -103,4 +104,4 @@ class ICM20948Sensor : public MotionSensor
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+245
-14
@@ -1,10 +1,37 @@
|
||||
#include "MotionSensor.h"
|
||||
#include "FSCommon.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "graphics/draw/CompassRenderer.h"
|
||||
|
||||
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
|
||||
|
||||
char timeRemainingBuffer[12];
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint32_t COMPASS_CALIBRATION_MAGIC = 0x4D43414CL; // "MCAL"
|
||||
constexpr uint16_t COMPASS_CALIBRATION_VERSION = 1;
|
||||
|
||||
struct CompassCalibrationRecord {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t reserved;
|
||||
float highestX;
|
||||
float lowestX;
|
||||
float highestY;
|
||||
float lowestY;
|
||||
float highestZ;
|
||||
float lowestZ;
|
||||
};
|
||||
|
||||
bool isRangeValid(float highest, float lowest)
|
||||
{
|
||||
// NaN/Inf guard without pulling in extra math helpers.
|
||||
return (highest == highest) && (lowest == lowest) && (highest > lowest);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// screen is defined in main.cpp
|
||||
extern graphics::Screen *screen;
|
||||
|
||||
@@ -32,33 +59,237 @@ ScanI2C::I2CPort MotionSensor::devicePort()
|
||||
return device.address.port;
|
||||
}
|
||||
|
||||
bool MotionSensor::saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY,
|
||||
float highestZ, float lowestZ)
|
||||
{
|
||||
#ifdef FSCom
|
||||
if (!isRangeValid(highestX, lowestX) || !isRangeValid(highestY, lowestY) || !isRangeValid(highestZ, lowestZ)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FSCom.mkdir("/prefs");
|
||||
CompassCalibrationRecord record = {
|
||||
COMPASS_CALIBRATION_MAGIC, COMPASS_CALIBRATION_VERSION, 0, highestX, lowestX, highestY, lowestY, highestZ, lowestZ};
|
||||
|
||||
auto file = SafeFile(filePath, true);
|
||||
const size_t written = file.write(reinterpret_cast<const uint8_t *>(&record), sizeof(record));
|
||||
return (written == sizeof(record)) && file.close();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool MotionSensor::loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ)
|
||||
{
|
||||
#ifdef FSCom
|
||||
CompassCalibrationRecord record = {};
|
||||
size_t bytesRead = 0;
|
||||
|
||||
spiLock->lock();
|
||||
auto file = FSCom.open(filePath, FILE_O_READ);
|
||||
if (!file) {
|
||||
spiLock->unlock();
|
||||
return false;
|
||||
}
|
||||
bytesRead = file.read(reinterpret_cast<uint8_t *>(&record), sizeof(record));
|
||||
file.close();
|
||||
spiLock->unlock();
|
||||
|
||||
const bool headerValid = (bytesRead == sizeof(record)) && (record.magic == COMPASS_CALIBRATION_MAGIC) &&
|
||||
(record.version == COMPASS_CALIBRATION_VERSION) && (record.reserved == 0U);
|
||||
const bool rangeValid = isRangeValid(record.highestX, record.lowestX) && isRangeValid(record.highestY, record.lowestY) &&
|
||||
isRangeValid(record.highestZ, record.lowestZ);
|
||||
if (!headerValid || !rangeValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
highestX = record.highestX;
|
||||
lowestX = record.lowestX;
|
||||
highestY = record.highestY;
|
||||
lowestY = record.lowestY;
|
||||
highestZ = record.highestZ;
|
||||
lowestZ = record.lowestZ;
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotionSensor::beginCalibrationDisplay(bool &showingScreen)
|
||||
{
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
if (!showingScreen) {
|
||||
powerFSM.trigger(EVENT_PRESS); // keep screen alive during calibration
|
||||
showingScreen = true;
|
||||
if (screen)
|
||||
screen->startAlert((FrameCallback)drawFrameCalibration);
|
||||
}
|
||||
#else
|
||||
(void)showingScreen;
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotionSensor::finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX,
|
||||
float highestY, float lowestY, float highestZ, float lowestZ)
|
||||
{
|
||||
const uint32_t now = millis();
|
||||
if ((int32_t)(now - endCalibrationAt) < 0)
|
||||
return;
|
||||
|
||||
doCalibration = false;
|
||||
endCalibrationAt = 0;
|
||||
showingScreen = false;
|
||||
saveMagnetometerCalibration(filePath, highestX, lowestX, highestY, lowestY, highestZ, lowestZ);
|
||||
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
if (screen) {
|
||||
screen->setEndCalibration(0);
|
||||
screen->endAlert();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotionSensor::startCalibrationWindow(uint16_t forSeconds)
|
||||
{
|
||||
doCalibration = true;
|
||||
const uint32_t calibrateFor = static_cast<uint32_t>(forSeconds) * 1000U;
|
||||
endCalibrationAt = millis() + calibrateFor;
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
if (screen)
|
||||
screen->setEndCalibration(endCalibrationAt);
|
||||
#endif
|
||||
}
|
||||
|
||||
void MotionSensor::seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ)
|
||||
{
|
||||
highestX = lowestX = x;
|
||||
highestY = lowestY = y;
|
||||
highestZ = lowestZ = z;
|
||||
}
|
||||
|
||||
void MotionSensor::updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ)
|
||||
{
|
||||
if (x > highestX)
|
||||
highestX = x;
|
||||
if (x < lowestX)
|
||||
lowestX = x;
|
||||
if (y > highestY)
|
||||
highestY = y;
|
||||
if (y < lowestY)
|
||||
lowestY = y;
|
||||
if (z > highestZ)
|
||||
highestZ = z;
|
||||
if (z < lowestZ)
|
||||
lowestZ = z;
|
||||
}
|
||||
|
||||
float MotionSensor::applyCompassOrientation(float heading)
|
||||
{
|
||||
switch (config.display.compass_orientation) {
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_90_INVERTED:
|
||||
return heading + 90;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_180_INVERTED:
|
||||
return heading + 180;
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270:
|
||||
case meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270_INVERTED:
|
||||
return heading + 270;
|
||||
default:
|
||||
return heading;
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN
|
||||
void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
if (screen == nullptr)
|
||||
return;
|
||||
// int x_offset = display->width() / 2;
|
||||
// int y_offset = display->height() <= 80 ? 0 : 32;
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x, y, "Calibrating\nCompass");
|
||||
|
||||
uint8_t timeRemaining = (screen->getEndCalibration() - millis()) / 1000;
|
||||
sprintf(timeRemainingBuffer, "( %02d )", timeRemaining);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->drawString(x, y + 40, timeRemainingBuffer);
|
||||
const int16_t width = display->getWidth();
|
||||
const int16_t height = display->getHeight();
|
||||
const bool compactLayout = (height <= 80);
|
||||
const int16_t margin = 4;
|
||||
|
||||
const uint32_t now = millis();
|
||||
const uint32_t endCalibrationAt = screen->getEndCalibration();
|
||||
uint32_t timeRemaining = 0;
|
||||
if (endCalibrationAt > now) {
|
||||
timeRemaining = (endCalibrationAt - now + 999) / 1000;
|
||||
}
|
||||
|
||||
int16_t compassX = 0, compassY = 0;
|
||||
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(display->getWidth(), display->getHeight());
|
||||
uint16_t compassDiam = graphics::CompassRenderer::getCompassDiam(width, height);
|
||||
const int16_t compassRadius = compassDiam / 2;
|
||||
|
||||
// coordinates for the center of the compass/circle
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||
compassY = y + display->getHeight() / 2;
|
||||
compassX = x + width - compassRadius - margin;
|
||||
compassY = y + height / 2;
|
||||
} else {
|
||||
compassX = x + display->getWidth() - compassDiam / 2 - 5;
|
||||
compassY = y + FONT_HEIGHT_SMALL + (display->getHeight() - FONT_HEIGHT_SMALL) / 2;
|
||||
compassX = x + width - compassRadius - margin;
|
||||
compassY = y + FONT_HEIGHT_SMALL + (height - FONT_HEIGHT_SMALL) / 2;
|
||||
}
|
||||
|
||||
const int16_t textLeft = x + 1;
|
||||
const int16_t textRight = compassX - compassRadius - margin;
|
||||
const int16_t textWidth = textRight - textLeft;
|
||||
int16_t lineY = y;
|
||||
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
if (textWidth > 12) {
|
||||
const char *title = "Cal";
|
||||
const char *line1 = "Figure-8";
|
||||
const char *line2 = "Rotate axes";
|
||||
const char *line3 = "Away from metal";
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
if (!compactLayout && display->getStringWidth("Compass Calibration") <= textWidth) {
|
||||
display->setFont(FONT_MEDIUM);
|
||||
title = "Compass Calibration";
|
||||
line1 = "Move in figure-8";
|
||||
line2 = "Rotate all axes";
|
||||
line3 = "Keep from metal";
|
||||
display->drawString(textLeft, lineY, title);
|
||||
lineY += FONT_HEIGHT_MEDIUM;
|
||||
display->setFont(FONT_SMALL);
|
||||
} else if (display->getStringWidth("Compass Cal") <= textWidth) {
|
||||
title = "Compass Cal";
|
||||
if (textWidth >= display->getStringWidth("Move in figure-8")) {
|
||||
line1 = "Move in figure-8";
|
||||
line2 = "Rotate all axes";
|
||||
line3 = "Keep from metal";
|
||||
}
|
||||
display->drawString(textLeft, lineY, title);
|
||||
lineY += FONT_HEIGHT_SMALL;
|
||||
} else {
|
||||
display->drawString(textLeft, lineY, title);
|
||||
lineY += FONT_HEIGHT_SMALL;
|
||||
}
|
||||
|
||||
display->drawString(textLeft, lineY, line1);
|
||||
lineY += FONT_HEIGHT_SMALL;
|
||||
display->drawString(textLeft, lineY, line2);
|
||||
lineY += FONT_HEIGHT_SMALL;
|
||||
if (!compactLayout || textWidth >= display->getStringWidth(line3)) {
|
||||
display->drawString(textLeft, lineY, line3);
|
||||
}
|
||||
}
|
||||
|
||||
if (textWidth >= display->getStringWidth("000s left")) {
|
||||
snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus left", (unsigned long)timeRemaining);
|
||||
} else {
|
||||
snprintf(timeRemainingBuffer, sizeof(timeRemainingBuffer), "%lus", (unsigned long)timeRemaining);
|
||||
}
|
||||
display->setFont(FONT_SMALL);
|
||||
if (textWidth > 12) {
|
||||
display->drawString(textLeft, y + height - FONT_HEIGHT_SMALL - 1, timeRemainingBuffer);
|
||||
}
|
||||
|
||||
display->drawCircle(compassX, compassY, compassDiam / 2);
|
||||
graphics::CompassRenderer::drawCompassNorth(display, compassX, compassY, screen->getHeading() * PI / 180, (compassDiam / 2));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#ifndef _MOTION_SENSOR_H_
|
||||
#define _MOTION_SENSOR_H_
|
||||
|
||||
#define MOTION_SENSOR_CHECK_INTERVAL_MS 100
|
||||
#define MOTION_SENSOR_CHECK_INTERVAL_MS 50
|
||||
#define MOTION_SENSOR_CLICK_THRESHOLD 40
|
||||
|
||||
#include "../configuration.h"
|
||||
@@ -54,6 +54,20 @@ class MotionSensor
|
||||
static void drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
#endif
|
||||
|
||||
bool saveMagnetometerCalibration(const char *filePath, float highestX, float lowestX, float highestY, float lowestY,
|
||||
float highestZ, float lowestZ);
|
||||
bool loadMagnetometerCalibration(const char *filePath, float &highestX, float &lowestX, float &highestY, float &lowestY,
|
||||
float &highestZ, float &lowestZ);
|
||||
void beginCalibrationDisplay(bool &showingScreen);
|
||||
void finishCalibrationIfExpired(bool &showingScreen, const char *filePath, float highestX, float lowestX, float highestY,
|
||||
float lowestY, float highestZ, float lowestZ);
|
||||
void startCalibrationWindow(uint16_t forSeconds);
|
||||
static void seedCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ);
|
||||
static void updateCalibrationExtrema(float x, float y, float z, float &highestX, float &lowestX, float &highestY,
|
||||
float &lowestY, float &highestZ, float &lowestZ);
|
||||
static float applyCompassOrientation(float heading);
|
||||
|
||||
ScanI2C::FoundDevice device;
|
||||
|
||||
// Do calibration if true
|
||||
@@ -63,4 +77,4 @@ class MotionSensor
|
||||
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+157
-241
@@ -10,23 +10,18 @@
|
||||
#include "mesh/PhoneAPI.h"
|
||||
#include "mesh/mesh-pb-constants.h"
|
||||
#include "sleep.h"
|
||||
#include <NimBLEDevice.h>
|
||||
#include <BLE2904.h>
|
||||
#include <BLEAdvertising.h>
|
||||
#include <BLEDevice.h>
|
||||
#include <BLESecurity.h>
|
||||
#include <BLEUtils.h>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
#include "NimBLEAdvertising.h"
|
||||
#include "NimBLEExtAdvertising.h"
|
||||
#include "PowerStatus.h"
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_NIMBLE_CPP_IDF)
|
||||
#include "host/ble_gap.h"
|
||||
#else
|
||||
#include "nimble/nimble/host/include/host/ble_gap.h"
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
||||
#include "host/ble_store.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -34,7 +29,6 @@ constexpr uint16_t kPreferredBleMtu = 517;
|
||||
constexpr uint16_t kPreferredBleTxOctets = 251;
|
||||
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
// Debugging options: careful, they slow things down quite a bit!
|
||||
// #define DEBUG_NIMBLE_ON_READ_TIMING // uncomment to time onRead duration
|
||||
@@ -44,10 +38,10 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
#define NIMBLE_BLUETOOTH_TO_PHONE_QUEUE_SIZE 3
|
||||
#define NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE 3
|
||||
|
||||
NimBLECharacteristic *fromNumCharacteristic;
|
||||
NimBLECharacteristic *BatteryCharacteristic;
|
||||
NimBLECharacteristic *logRadioCharacteristic;
|
||||
NimBLEServer *bleServer;
|
||||
BLECharacteristic *fromNumCharacteristic;
|
||||
BLECharacteristic *BatteryCharacteristic;
|
||||
BLECharacteristic *logRadioCharacteristic;
|
||||
BLEServer *bleServer;
|
||||
|
||||
static bool passkeyShowing;
|
||||
static std::atomic<uint16_t> nimbleBluetoothConnHandle{BLE_HS_CONN_HANDLE_NONE}; // BLE_HS_CONN_HANDLE_NONE means "no connection"
|
||||
@@ -118,7 +112,8 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
- Yes, we have to do some copy operations on pop because of this, but it's worth it to avoid cross-task memory management.
|
||||
|
||||
NOTIFY IS BROKEN:
|
||||
- Adding NIMBLE_PROPERTY::NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards compatible.
|
||||
- Adding BLECharacteristic::PROPERTY_NOTIFY to FromRadioCharacteristic appears to break things. It is NOT backwards
|
||||
compatible.
|
||||
|
||||
ZERO-SIZE READS:
|
||||
- Returning a zero-size read from onRead breaks some clients during the config phase. So we have to block onRead until we
|
||||
@@ -139,7 +134,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
std::mutex fromPhoneMutex;
|
||||
std::atomic<size_t> fromPhoneQueueSize{0};
|
||||
// We use array here (and pay the cost of memcpy) to avoid dynamic memory allocations and frees across FreeRTOS tasks.
|
||||
std::array<NimBLEAttValue, NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE> fromPhoneQueue{};
|
||||
std::array<BLEValue, NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE> fromPhoneQueue{};
|
||||
|
||||
/* Packets to phone (BLE onRead callback) */
|
||||
std::mutex toPhoneMutex;
|
||||
@@ -301,7 +296,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
LOG_DEBUG("NimbleBluetooth: handling ToRadio packet, fromPhoneQueueSize=%u", fromPhoneQueueSize.load());
|
||||
|
||||
// Pop the front of fromPhoneQueue, holding the mutex only briefly while we pop.
|
||||
NimBLEAttValue val;
|
||||
BLEValue val;
|
||||
{ // scope for fromPhoneMutex mutex
|
||||
std::lock_guard<std::mutex> guard(fromPhoneMutex);
|
||||
val = fromPhoneQueue[0];
|
||||
@@ -316,7 +311,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
fromPhoneQueueSize--;
|
||||
}
|
||||
|
||||
handleToRadio(val.data(), val.length());
|
||||
handleToRadio(val.getData(), val.getLength());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,9 +323,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
PhoneAPI::onNowHasData(fromRadioNum);
|
||||
|
||||
#ifdef DEBUG_NIMBLE_NOTIFY
|
||||
|
||||
int currentNotifyCount = notifyCount.fetch_add(1);
|
||||
|
||||
uint8_t cc = bleServer->getConnectedCount();
|
||||
// This logging slows things down when there are lots of packets going to the phone, like initial connection:
|
||||
LOG_DEBUG("BLE notify(%d) fromNum: %d connections: %d", currentNotifyCount, fromRadioNum, cc);
|
||||
@@ -340,13 +333,7 @@ class BluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
|
||||
put_le32(val, fromRadioNum);
|
||||
|
||||
fromNumCharacteristic->setValue(val, sizeof(val));
|
||||
#ifdef NIMBLE_TWO
|
||||
// NOTE: I don't have any NIMBLE_TWO devices, but this line makes me suspicious, and I suspect it needs to just be
|
||||
// notify().
|
||||
fromNumCharacteristic->notify(val, sizeof(val), BLE_HS_CONN_HANDLE_NONE);
|
||||
#else
|
||||
fromNumCharacteristic->notify();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Check the current underlying physical link to see if the client is currently connected
|
||||
@@ -409,14 +396,9 @@ static BluetoothPhoneAPI *bluetoothPhoneAPI;
|
||||
// Last ToRadio value received from the phone
|
||||
static uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE];
|
||||
|
||||
class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
class NimbleBluetoothToRadioCallback : public BLECharacteristicCallbacks
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
|
||||
#else
|
||||
virtual void onWrite(NimBLECharacteristic *pCharacteristic)
|
||||
|
||||
#endif
|
||||
void onWrite(BLECharacteristic *pCharacteristic) override
|
||||
{
|
||||
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
||||
// Assumption: onWrite is serialized by NimBLE, so we don't need to lock here against multiple concurrent onWrite calls.
|
||||
@@ -428,15 +410,17 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
LOG_DEBUG("BLE onWrite(%d): start millis=%d", currentWriteCount, startMillis);
|
||||
#endif
|
||||
|
||||
auto val = pCharacteristic->getValue();
|
||||
// Create a BLEValue and populate it with the received data
|
||||
BLEValue val;
|
||||
val.setValue(pCharacteristic->getData(), pCharacteristic->getLength());
|
||||
|
||||
if (memcmp(lastToRadio, val.data(), val.length()) != 0) {
|
||||
if (memcmp(lastToRadio, val.getData(), val.getLength()) != 0) {
|
||||
if (bluetoothPhoneAPI->fromPhoneQueueSize < NIMBLE_BLUETOOTH_FROM_PHONE_QUEUE_SIZE) {
|
||||
// Note: the comparison above is safe without a mutex because we are the only method that *increases*
|
||||
// fromPhoneQueueSize. (It's okay if fromPhoneQueueSize *decreases* in the main task meanwhile.)
|
||||
memcpy(lastToRadio, val.data(), val.length());
|
||||
memcpy(lastToRadio, val.getData(), val.getLength());
|
||||
|
||||
{ // scope for fromPhoneMutex mutex
|
||||
{ // scope for fromPhoneMutex mutexv, pCharacteristic->getLen
|
||||
// Append to fromPhoneQueue, protected by fromPhoneMutex. Hold the mutex as briefly as possible.
|
||||
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
|
||||
bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
|
||||
@@ -450,24 +434,21 @@ class NimbleBluetoothToRadioCallback : public NimBLECharacteristicCallbacks
|
||||
#ifdef DEBUG_NIMBLE_ON_WRITE_TIMING
|
||||
int finishMillis = millis();
|
||||
LOG_DEBUG("BLE onWrite(%d): append to fromPhoneQueue took %u ms. numBytes=%d", currentWriteCount,
|
||||
finishMillis - startMillis, val.length());
|
||||
finishMillis - startMillis, val.getLength());
|
||||
#endif
|
||||
} else {
|
||||
LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount, val.length());
|
||||
LOG_WARN("BLE onWrite(%d): Drop ToRadio packet, fromPhoneQueue full (%u bytes)", currentWriteCount,
|
||||
val.getLength());
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.length());
|
||||
LOG_DEBUG("BLE onWrite(%d): Drop duplicate ToRadio packet (%u bytes)", currentWriteCount, val.getLength());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
||||
class NimbleBluetoothFromRadioCallback : public BLECharacteristicCallbacks
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic, NimBLEConnInfo &connInfo)
|
||||
#else
|
||||
virtual void onRead(NimBLECharacteristic *pCharacteristic)
|
||||
#endif
|
||||
void onRead(BLECharacteristic *pCharacteristic) override
|
||||
{
|
||||
// CAUTION: This callback runs in the NimBLE task!!! Don't do anything except communicate with the main task's runOnce.
|
||||
|
||||
@@ -573,34 +554,15 @@ class NimbleBluetoothFromRadioCallback : public NimBLECharacteristicCallbacks
|
||||
}
|
||||
};
|
||||
|
||||
class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
class NimbleBluetoothSecurityCallback : public BLESecurityCallbacks
|
||||
{
|
||||
public:
|
||||
NimbleBluetoothServerCallback(NimbleBluetooth *ble) { this->ble = ble; }
|
||||
|
||||
private:
|
||||
NimbleBluetooth *ble;
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual uint32_t onPassKeyDisplay()
|
||||
#else
|
||||
virtual uint32_t onPassKeyRequest()
|
||||
#endif
|
||||
void onPassKeyNotify(uint32_t passkey) override
|
||||
{
|
||||
uint32_t passkey = config.bluetooth.fixed_pin;
|
||||
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
|
||||
LOG_INFO("Use random passkey");
|
||||
// This is the passkey to be entered on peer - we pick a number >100,000 to ensure 6 digits
|
||||
passkey = random(100000, 999999);
|
||||
}
|
||||
LOG_INFO("*** Enter passkey %d on the peer side ***", passkey);
|
||||
|
||||
LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey);
|
||||
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
|
||||
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
#if HAS_SCREEN // Todo: migrate this display code back into Screen class, and observe bluetoothStatus
|
||||
#if HAS_SCREEN
|
||||
if (screen) {
|
||||
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
char btPIN[16] = "888888";
|
||||
@@ -630,15 +592,8 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
}
|
||||
#endif
|
||||
passkeyShowing = true;
|
||||
|
||||
return passkey;
|
||||
}
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onAuthenticationComplete(NimBLEConnInfo &connInfo)
|
||||
#else
|
||||
virtual void onAuthenticationComplete(ble_gap_conn_desc *desc)
|
||||
#endif
|
||||
void onAuthenticationComplete(ble_gap_conn_desc *desc) override
|
||||
{
|
||||
LOG_INFO("BLE authentication complete");
|
||||
|
||||
@@ -646,52 +601,46 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
clearPairingDisplay();
|
||||
|
||||
// Store the connection handle for future use
|
||||
#ifdef NIMBLE_TWO
|
||||
nimbleBluetoothConnHandle = connInfo.getConnHandle();
|
||||
#else
|
||||
nimbleBluetoothConnHandle = desc->conn_handle;
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo)
|
||||
class NimbleBluetoothServerCallback : public BLEServerCallbacks
|
||||
{
|
||||
public:
|
||||
explicit NimbleBluetoothServerCallback(NimbleBluetooth *ble) : ble(ble) {}
|
||||
|
||||
private:
|
||||
NimbleBluetooth *ble;
|
||||
|
||||
void onConnect(BLEServer *pServer, struct ble_gap_conn_desc *desc)
|
||||
{
|
||||
LOG_INFO("BLE incoming connection %s", connInfo.getAddress().toString().c_str());
|
||||
BLEAddress peer_addr(desc->peer_id_addr);
|
||||
LOG_INFO("BLE incoming connection %s", peer_addr.toString().c_str());
|
||||
|
||||
const uint16_t connHandle = connInfo.getConnHandle();
|
||||
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
|
||||
int phyResult =
|
||||
ble_gap_set_prefered_le_phy(connHandle, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_CODED_ANY);
|
||||
if (phyResult == 0) {
|
||||
LOG_INFO("BLE conn %u requested 2M PHY", connHandle);
|
||||
} else {
|
||||
LOG_WARN("Failed to prefer 2M PHY for conn %u, rc=%d", connHandle, phyResult);
|
||||
}
|
||||
#endif
|
||||
const uint16_t connHandle = desc->conn_handle;
|
||||
|
||||
// With Google Pixel 8 Android devices, this causes ESP32 device crash
|
||||
// when phone reconnects. Disable this to make progress on the
|
||||
// Arduino v3 migration while we investigate the Android compatibility
|
||||
// issue.
|
||||
#if 0
|
||||
int dataLenResult = ble_gap_set_data_len(connHandle, kPreferredBleTxOctets, kPreferredBleTxTimeUs);
|
||||
if (dataLenResult == 0) {
|
||||
LOG_INFO("BLE conn %u requested data length %u bytes", connHandle, kPreferredBleTxOctets);
|
||||
} else {
|
||||
LOG_WARN("Failed to raise data length for conn %u, rc=%d", connHandle, dataLenResult);
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_INFO("BLE conn %u initial MTU %u (target %u)", connHandle, connInfo.getMTU(), kPreferredBleMtu);
|
||||
LOG_INFO("BLE conn %u peer MTU %u (target %u)", connHandle, pServer->getPeerMTU(connHandle), kPreferredBleMtu);
|
||||
pServer->updateConnParams(connHandle, 6, 12, 0, 200);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
virtual void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason)
|
||||
void onDisconnect(BLEServer *pServer, struct ble_gap_conn_desc *desc)
|
||||
{
|
||||
LOG_INFO("BLE disconnect reason: %d", reason);
|
||||
#else
|
||||
virtual void onDisconnect(NimBLEServer *pServer, ble_gap_conn_desc *desc)
|
||||
{
|
||||
LOG_INFO("BLE disconnect");
|
||||
#endif
|
||||
if (ble && ble->isDeInit)
|
||||
LOG_INFO("BLE disconnected");
|
||||
if (ble->isDeInit)
|
||||
return;
|
||||
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
|
||||
@@ -717,43 +666,51 @@ class NimbleBluetoothServerCallback : public NimBLEServerCallbacks
|
||||
bluetoothPhoneAPI->writeCount = 0;
|
||||
}
|
||||
|
||||
// Clear the last ToRadio packet buffer to avoid rejecting first packet from new connection
|
||||
memset(lastToRadio, 0, sizeof(lastToRadio));
|
||||
|
||||
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE; // BLE_HS_CONN_HANDLE_NONE means "no connection"
|
||||
nimbleBluetoothConnHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
|
||||
#ifdef NIMBLE_TWO
|
||||
// Restart Advertising
|
||||
ble->startAdvertising();
|
||||
#else
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
if (!pAdvertising->start(0)) {
|
||||
if (pAdvertising->isAdvertising()) {
|
||||
LOG_DEBUG("BLE advertising already running");
|
||||
} else {
|
||||
LOG_ERROR("BLE failed to restart advertising");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
static NimbleBluetoothToRadioCallback *toRadioCallbacks;
|
||||
static NimbleBluetoothFromRadioCallback *fromRadioCallbacks;
|
||||
|
||||
void NimbleBluetooth::startAdvertising()
|
||||
{
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
pAdvertising->stop();
|
||||
pAdvertising->reset();
|
||||
|
||||
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
|
||||
// if (powerStatus->getHasBattery() == 1) {
|
||||
// pAdvertising->addServiceUUID(BLEUUID((uint16_t)0x180f));
|
||||
// }
|
||||
|
||||
BLEAdvertisementData scan = BLEAdvertisementData();
|
||||
scan.setName(getDeviceName());
|
||||
pAdvertising->setScanResponseData(scan);
|
||||
pAdvertising->setMinPreferred(0x06); // functions that help with iPhone connections issue
|
||||
pAdvertising->setMaxPreferred(0x12);
|
||||
|
||||
if (!pAdvertising->start(0)) {
|
||||
LOG_ERROR("BLE failed to start advertising");
|
||||
} else {
|
||||
LOG_DEBUG("BLE Advertising started");
|
||||
}
|
||||
}
|
||||
|
||||
void NimbleBluetooth::shutdown()
|
||||
{
|
||||
// No measurable power saving for ESP32 during light-sleep(?)
|
||||
#ifndef ARCH_ESP32
|
||||
// Shutdown bluetooth for minimum power draw
|
||||
LOG_INFO("Disable bluetooth");
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
|
||||
pAdvertising->reset();
|
||||
pAdvertising->stop();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Proper shutdown for ESP32. Needs reboot to reverse.
|
||||
void NimbleBluetooth::deinit()
|
||||
{
|
||||
#ifdef ARCH_ESP32
|
||||
@@ -763,21 +720,17 @@ void NimbleBluetooth::deinit()
|
||||
#ifdef BLE_LED
|
||||
digitalWrite(BLE_LED, LED_STATE_OFF);
|
||||
#endif
|
||||
#ifndef NIMBLE_TWO
|
||||
NimBLEDevice::deinit();
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
// Has initial setup been completed
|
||||
bool NimbleBluetooth::isActive()
|
||||
{
|
||||
return bleServer;
|
||||
return bleServer != nullptr;
|
||||
}
|
||||
|
||||
bool NimbleBluetooth::isConnected()
|
||||
{
|
||||
return bleServer->getConnectedCount() > 0;
|
||||
return bleServer && bleServer->getConnectedCount() > 0;
|
||||
}
|
||||
|
||||
int NimbleBluetooth::getRssi()
|
||||
@@ -790,9 +743,9 @@ int NimbleBluetooth::getRssi()
|
||||
uint16_t connHandle = nimbleBluetoothConnHandle.load();
|
||||
|
||||
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
|
||||
const auto peers = bleServer->getPeerDevices();
|
||||
const auto peers = bleServer->getPeerDevices(true);
|
||||
if (!peers.empty()) {
|
||||
connHandle = peers.front();
|
||||
connHandle = peers.begin()->first;
|
||||
nimbleBluetoothConnHandle = connHandle;
|
||||
}
|
||||
}
|
||||
@@ -820,74 +773,84 @@ void NimbleBluetooth::setup()
|
||||
|
||||
LOG_INFO("Init the NimBLE bluetooth module");
|
||||
|
||||
NimBLEDevice::init(getDeviceName());
|
||||
NimBLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
BLEDevice::init(getDeviceName());
|
||||
BLEDevice::setPower(ESP_PWR_LVL_P9);
|
||||
|
||||
#if NIMBLE_ENABLE_2M_PHY && (defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6))
|
||||
int mtuResult = NimBLEDevice::setMTU(kPreferredBleMtu);
|
||||
int mtuResult = BLEDevice::setMTU(kPreferredBleMtu);
|
||||
if (mtuResult == 0) {
|
||||
LOG_INFO("BLE MTU request set to %u", kPreferredBleMtu);
|
||||
} else {
|
||||
LOG_WARN("Unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult);
|
||||
}
|
||||
|
||||
int phyResult = ble_gap_set_prefered_default_le_phy(BLE_GAP_LE_PHY_2M_MASK, BLE_GAP_LE_PHY_2M_MASK);
|
||||
if (phyResult == 0) {
|
||||
LOG_INFO("BLE default PHY preference set to 2M");
|
||||
} else {
|
||||
LOG_WARN("Failed to prefer 2M PHY by default, rc=%d", phyResult);
|
||||
}
|
||||
|
||||
int dataLenResult = ble_gap_write_sugg_def_data_len(kPreferredBleTxOctets, kPreferredBleTxTimeUs);
|
||||
if (dataLenResult == 0) {
|
||||
LOG_INFO("BLE suggested data length set to %u bytes", kPreferredBleTxOctets);
|
||||
} else {
|
||||
LOG_WARN("Failed to raise suggested data length (%u/%u), rc=%d", kPreferredBleTxOctets, kPreferredBleTxTimeUs,
|
||||
dataLenResult);
|
||||
}
|
||||
#endif
|
||||
|
||||
BLESecurity *pSecurity = new BLESecurity();
|
||||
pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
pSecurity->setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
|
||||
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
NimBLEDevice::setSecurityAuth(BLE_SM_PAIR_AUTHREQ_BOND | BLE_SM_PAIR_AUTHREQ_MITM | BLE_SM_PAIR_AUTHREQ_SC);
|
||||
NimBLEDevice::setSecurityInitKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
|
||||
NimBLEDevice::setSecurityRespKey(BLE_SM_PAIR_KEY_DIST_ENC | BLE_SM_PAIR_KEY_DIST_ID);
|
||||
NimBLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
|
||||
// Set IO capability to DisplayOnly for MITM authentication
|
||||
pSecurity->setCapability(ESP_IO_CAP_OUT);
|
||||
// Set the passkey
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
|
||||
LOG_INFO("Use random passkey");
|
||||
pSecurity->setPassKey(false); // generate a random passkey
|
||||
} else {
|
||||
LOG_INFO("Use fixed passkey");
|
||||
pSecurity->setPassKey(true, config.bluetooth.fixed_pin);
|
||||
}
|
||||
// Enable authorization requirements:
|
||||
// - bonding: true (for persistent storage of the keys)
|
||||
// - MITM: true (enables Man-In-The-Middle protection for password prompts)
|
||||
// - secure connection: true (enables secure connection for encryption)
|
||||
pSecurity->setAuthenticationMode(true, true, true);
|
||||
} else {
|
||||
// No IO capability for no PIN mode
|
||||
pSecurity->setCapability(ESP_IO_CAP_NONE);
|
||||
// No PIN mode: no MITM protection
|
||||
pSecurity->setAuthenticationMode(true, false, false);
|
||||
}
|
||||
bleServer = NimBLEDevice::createServer();
|
||||
#ifdef NIMBLE_TWO
|
||||
NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this);
|
||||
#else
|
||||
NimbleBluetoothServerCallback *serverCallbacks = new NimbleBluetoothServerCallback(this);
|
||||
#endif
|
||||
bleServer->setCallbacks(serverCallbacks, true);
|
||||
// Set the security callbacks
|
||||
BLEDevice::setSecurityCallbacks(new NimbleBluetoothSecurityCallback());
|
||||
bleServer = BLEDevice::createServer();
|
||||
|
||||
// BLEDevice::createServer calls ble_svc_gap_init, which resets the device
|
||||
// name to default, so set it again.
|
||||
int nameRc = ble_svc_gap_device_name_set(BLEDevice::getDeviceName().c_str());
|
||||
if (nameRc != 0) {
|
||||
LOG_ERROR("ble_svc_gap_device_name_set: rc=%d %s", nameRc, BLEUtils::returnCodeToString(nameRc));
|
||||
}
|
||||
|
||||
bleServer->setCallbacks(new NimbleBluetoothServerCallback(this));
|
||||
setupService();
|
||||
startAdvertising();
|
||||
}
|
||||
|
||||
void NimbleBluetooth::setupService()
|
||||
{
|
||||
NimBLEService *bleService = bleServer->createService(MESH_SERVICE_UUID);
|
||||
NimBLECharacteristic *ToRadioCharacteristic;
|
||||
NimBLECharacteristic *FromRadioCharacteristic;
|
||||
BLEService *bleService = bleServer->createService(MESH_SERVICE_UUID);
|
||||
BLECharacteristic *ToRadioCharacteristic;
|
||||
BLECharacteristic *FromRadioCharacteristic;
|
||||
// Define the characteristics that the app is looking for
|
||||
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, NIMBLE_PROPERTY::WRITE);
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE);
|
||||
// Allow notifications so phones can stream FromRadio without polling.
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, NIMBLE_PROPERTY::READ);
|
||||
fromNumCharacteristic = bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ);
|
||||
logRadioCharacteristic =
|
||||
bleService->createCharacteristic(LOGRADIO_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ, 512U);
|
||||
} else {
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(
|
||||
TORADIO_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN | NIMBLE_PROPERTY::WRITE_ENC);
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(
|
||||
FROMRADIO_UUID, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ);
|
||||
fromNumCharacteristic =
|
||||
bleService->createCharacteristic(FROMNUM_UUID, NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ |
|
||||
NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC);
|
||||
bleService->createCharacteristic(FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ);
|
||||
logRadioCharacteristic = bleService->createCharacteristic(LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY |
|
||||
BLECharacteristic::PROPERTY_READ);
|
||||
} else {
|
||||
ToRadioCharacteristic = bleService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE |
|
||||
BLECharacteristic::PROPERTY_WRITE_AUTHEN |
|
||||
BLECharacteristic::PROPERTY_WRITE_ENC);
|
||||
FromRadioCharacteristic = bleService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN |
|
||||
BLECharacteristic::PROPERTY_READ_ENC);
|
||||
fromNumCharacteristic = bleService->createCharacteristic(
|
||||
FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
|
||||
logRadioCharacteristic = bleService->createCharacteristic(
|
||||
LOGRADIO_UUID,
|
||||
NIMBLE_PROPERTY::NOTIFY | NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::READ_AUTHEN | NIMBLE_PROPERTY::READ_ENC, 512U);
|
||||
LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
|
||||
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
|
||||
}
|
||||
bluetoothPhoneAPI = new BluetoothPhoneAPI();
|
||||
|
||||
@@ -900,76 +863,31 @@ void NimbleBluetooth::setupService()
|
||||
bleService->start();
|
||||
|
||||
// Setup the battery service
|
||||
NimBLEService *batteryService = bleServer->createService(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
|
||||
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
|
||||
(uint16_t)0x2a19, NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY, 1);
|
||||
#ifdef NIMBLE_TWO
|
||||
NimBLE2904 *batteryLevelDescriptor = BatteryCharacteristic->create2904();
|
||||
#else
|
||||
NimBLE2904 *batteryLevelDescriptor = (NimBLE2904 *)BatteryCharacteristic->createDescriptor((uint16_t)0x2904);
|
||||
#endif
|
||||
batteryLevelDescriptor->setFormat(NimBLE2904::FORMAT_UINT8);
|
||||
BLEService *batteryService = bleServer->createService(BLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
|
||||
BLE2904 *batteryLevelDescriptor = new BLE2904();
|
||||
batteryLevelDescriptor->setFormat(BLE2904::FORMAT_UINT8);
|
||||
batteryLevelDescriptor->setNamespace(1);
|
||||
batteryLevelDescriptor->setUnit(0x27ad);
|
||||
|
||||
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
|
||||
(uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
BatteryCharacteristic->addDescriptor(batteryLevelDescriptor);
|
||||
batteryService->start();
|
||||
}
|
||||
|
||||
void NimbleBluetooth::startAdvertising()
|
||||
{
|
||||
#ifdef NIMBLE_TWO
|
||||
NimBLEExtAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
NimBLEExtAdvertisement legacyAdvertising;
|
||||
|
||||
legacyAdvertising.setLegacyAdvertising(true);
|
||||
legacyAdvertising.setScannable(true);
|
||||
legacyAdvertising.setConnectable(true);
|
||||
legacyAdvertising.setFlags(BLE_HS_ADV_F_DISC_GEN);
|
||||
if (powerStatus->getHasBattery() == 1) {
|
||||
legacyAdvertising.setCompleteServices(NimBLEUUID((uint16_t)0x180f));
|
||||
}
|
||||
legacyAdvertising.setCompleteServices(NimBLEUUID(MESH_SERVICE_UUID));
|
||||
legacyAdvertising.setMinInterval(500);
|
||||
legacyAdvertising.setMaxInterval(1000);
|
||||
|
||||
NimBLEExtAdvertisement legacyScanResponse;
|
||||
legacyScanResponse.setLegacyAdvertising(true);
|
||||
legacyScanResponse.setConnectable(true);
|
||||
legacyScanResponse.setName(getDeviceName());
|
||||
|
||||
if (!pAdvertising->setInstanceData(0, legacyAdvertising)) {
|
||||
LOG_ERROR("BLE failed to set legacyAdvertising");
|
||||
} else if (!pAdvertising->setScanResponseData(0, legacyScanResponse)) {
|
||||
LOG_ERROR("BLE failed to set legacyScanResponse");
|
||||
} else if (!pAdvertising->start(0, 0, 0)) {
|
||||
LOG_ERROR("BLE failed to start legacyAdvertising");
|
||||
}
|
||||
#else
|
||||
NimBLEAdvertising *pAdvertising = NimBLEDevice::getAdvertising();
|
||||
pAdvertising->reset();
|
||||
pAdvertising->addServiceUUID(MESH_SERVICE_UUID);
|
||||
pAdvertising->addServiceUUID(NimBLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service
|
||||
pAdvertising->start(0);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
if ((config.bluetooth.enabled == true) && bleServer && bluetoothApi && bluetoothApi->isConnected()) {
|
||||
BatteryCharacteristic->setValue(&level, 1);
|
||||
#ifdef NIMBLE_TWO
|
||||
BatteryCharacteristic->notify(&level, 1, BLE_HS_CONN_HANDLE_NONE);
|
||||
#else
|
||||
BatteryCharacteristic->notify();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
void NimbleBluetooth::clearBonds()
|
||||
{
|
||||
LOG_INFO("Clearing bluetooth bonds!");
|
||||
NimBLEDevice::deleteAllBonds();
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
|
||||
}
|
||||
|
||||
void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
@@ -977,16 +895,14 @@ void NimbleBluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
if (!bleServer || !isConnected() || length > 512) {
|
||||
return;
|
||||
}
|
||||
#ifdef NIMBLE_TWO
|
||||
logRadioCharacteristic->notify(logMessage, length, BLE_HS_CONN_HANDLE_NONE);
|
||||
#else
|
||||
logRadioCharacteristic->notify(logMessage, length, true);
|
||||
#endif
|
||||
logRadioCharacteristic->setValue(logMessage, length);
|
||||
logRadioCharacteristic->notify();
|
||||
}
|
||||
|
||||
void clearNVS()
|
||||
{
|
||||
NimBLEDevice::deleteAllBonds();
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
|
||||
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
|
||||
#ifdef ARCH_ESP32
|
||||
ESP.restart();
|
||||
#endif
|
||||
|
||||
@@ -13,9 +13,10 @@ class NimbleBluetooth : public BluetoothApi
|
||||
int getRssi() override;
|
||||
void sendLog(const uint8_t *logMessage, size_t length) override;
|
||||
void startAdvertising();
|
||||
bool isDeInit = false;
|
||||
virtual ~NimbleBluetooth() {}
|
||||
|
||||
bool isDeInit = false;
|
||||
|
||||
private:
|
||||
void setupService();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef M5STACK_CARDPUTER_ADV
|
||||
|
||||
#include "AudioBoard.h"
|
||||
|
||||
DriverPins PinsAudioBoardES8311;
|
||||
AudioBoard board(AudioDriverES8311, PinsAudioBoardES8311);
|
||||
|
||||
// M5stack Cardputer ADV specific init
|
||||
|
||||
void lateInitVariant()
|
||||
{
|
||||
// AudioDriverLogger.begin(Serial, AudioDriverLogLevel::Debug);
|
||||
// I2C: function, scl, sda
|
||||
PinsAudioBoardES8311.addI2C(PinFunction::CODEC, Wire);
|
||||
// I2S: function, mclk, bck, ws, data_out, data_in
|
||||
PinsAudioBoardES8311.addI2S(PinFunction::CODEC, DAC_I2S_MCLK, DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_DIN);
|
||||
|
||||
// configure codec
|
||||
CodecConfig cfg;
|
||||
cfg.input_device = ADC_INPUT_LINE1;
|
||||
cfg.output_device = DAC_OUTPUT_ALL;
|
||||
cfg.i2s.bits = BIT_LENGTH_16BITS;
|
||||
cfg.i2s.rate = RATE_44K;
|
||||
board.begin(cfg);
|
||||
|
||||
// extra ES8311 init
|
||||
auto es8311_write_reg = [](uint8_t reg, uint8_t val) {
|
||||
Wire.beginTransmission(0x18); // ES8311 i2c address
|
||||
Wire.write(reg);
|
||||
Wire.write(val);
|
||||
Wire.endTransmission();
|
||||
};
|
||||
es8311_write_reg(0x00, 0x80); // reset, power on
|
||||
es8311_write_reg(0x01, 0xB5); // MCLK = BCLK
|
||||
es8311_write_reg(0x02, 0x18); // CLOCK_MANAGER/ MULT_PRE=3
|
||||
es8311_write_reg(0x0D, 0x01); // analog power up
|
||||
es8311_write_reg(0x12, 0x00); // DAC power up
|
||||
es8311_write_reg(0x13, 0x10); // enable HP drive
|
||||
es8311_write_reg(0x32, 0xBF); // DAC volume (0dB)
|
||||
es8311_write_reg(0x37, 0x08); // EQ bypass
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,144 @@
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef T5_S3_EPAPER_PRO
|
||||
|
||||
#include "Observer.h"
|
||||
#include "TouchDrvGT911.hpp"
|
||||
#include "Wire.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "input/TouchScreenImpl1.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
// Bridges touch events from TouchScreenImpl1 directly into InkHUD,
|
||||
// bypassing the InputBroker (which is excluded in InkHUD builds).
|
||||
// Routing mirrors the mini-epaper-s3 two-way rocker pattern:
|
||||
// - Nav left/right: prevApplet/nextApplet when idle, navUp/Down when a system applet has focus (e.g. menu)
|
||||
// - Nav up/down: navUp/navDown always (menu scroll)
|
||||
// - Tap: shortpress (cycle applets / confirm in menu)
|
||||
// - Long press: longpress (open menu / back)
|
||||
class TouchInkHUDBridge : public Observer<const InputEvent *>
|
||||
{
|
||||
int onNotify(const InputEvent *e) override
|
||||
{
|
||||
auto *inkhud = NicheGraphics::InkHUD::InkHUD::getInstance();
|
||||
|
||||
// Keep alignment in sync with the current rotation so that visual-frame gestures
|
||||
// always pass through nav functions without remapping: (rotation + alignment) % 4 == 0.
|
||||
inkhud->persistence->settings.joystick.alignment = (4 - inkhud->persistence->settings.rotation) % 4;
|
||||
|
||||
// Check whether a system applet (e.g. menu) is currently handling input
|
||||
bool systemHandlingInput = false;
|
||||
for (NicheGraphics::InkHUD::SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
systemHandlingInput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch (e->inputEvent) {
|
||||
case INPUT_BROKER_USER_PRESS:
|
||||
inkhud->shortpress();
|
||||
break;
|
||||
case INPUT_BROKER_SELECT:
|
||||
inkhud->longpress();
|
||||
break;
|
||||
case INPUT_BROKER_LEFT:
|
||||
if (systemHandlingInput)
|
||||
inkhud->navUp();
|
||||
else
|
||||
inkhud->prevApplet();
|
||||
break;
|
||||
case INPUT_BROKER_RIGHT:
|
||||
if (systemHandlingInput)
|
||||
inkhud->navDown();
|
||||
else
|
||||
inkhud->nextApplet();
|
||||
break;
|
||||
case INPUT_BROKER_UP:
|
||||
inkhud->navUp();
|
||||
break;
|
||||
case INPUT_BROKER_DOWN:
|
||||
inkhud->navDown();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
static TouchInkHUDBridge touchBridge;
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
TouchDrvGT911 touch;
|
||||
|
||||
// Commands the GT911 into standby before the Wire bus is torn down.
|
||||
// notifyDeepSleep fires before Wire.end() in doDeepSleep(), so I2C is still available here.
|
||||
struct TouchDeepSleepObserver {
|
||||
int onDeepSleep(void *)
|
||||
{
|
||||
touch.sleep();
|
||||
return 0;
|
||||
}
|
||||
CallbackObserver<TouchDeepSleepObserver, void *> observer{this, &TouchDeepSleepObserver::onDeepSleep};
|
||||
} static touchDeepSleepObserver;
|
||||
|
||||
bool readTouch(int16_t *x, int16_t *y)
|
||||
{
|
||||
if (!digitalRead(GT911_PIN_INT)) {
|
||||
int16_t raw_x;
|
||||
int16_t raw_y;
|
||||
if (touch.getPoint(&raw_x, &raw_y)) {
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
// Transform raw GT911 axes to visual-frame coordinates for the current display rotation.
|
||||
// rotation=3 is the physical identity (device's default orientation).
|
||||
switch (NicheGraphics::InkHUD::InkHUD::getInstance()->persistence->settings.rotation) {
|
||||
default:
|
||||
case 3:
|
||||
*x = raw_x;
|
||||
*y = raw_y;
|
||||
break; // identity
|
||||
case 2:
|
||||
*x = (EPD_WIDTH - 1) - raw_y;
|
||||
*y = raw_x;
|
||||
break; // 90° CW tilt
|
||||
case 1:
|
||||
*x = (EPD_HEIGHT - 1) - raw_x;
|
||||
*y = (EPD_WIDTH - 1) - raw_y;
|
||||
break; // 180° flip
|
||||
case 0:
|
||||
*x = raw_y;
|
||||
*y = (EPD_HEIGHT - 1) - raw_x;
|
||||
break; // 90° CCW tilt
|
||||
}
|
||||
#else
|
||||
*x = raw_x;
|
||||
*y = raw_y;
|
||||
#endif
|
||||
LOG_DEBUG("touched(%d/%d)", *x, *y);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// T5-S3-ePaper Pro specific (late-) init
|
||||
void lateInitVariant(void)
|
||||
{
|
||||
touch.setPins(GT911_PIN_RST, GT911_PIN_INT);
|
||||
if (touch.begin(Wire, GT911_SLAVE_ADDRESS_H, GT911_PIN_SDA, GT911_PIN_SCL)) {
|
||||
touchDeepSleepObserver.observer.observe(¬ifyDeepSleep);
|
||||
touchScreenImpl1 = new TouchScreenImpl1(EPD_WIDTH, EPD_HEIGHT, readTouch);
|
||||
touchScreenImpl1->init();
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
touchBridge.observe(touchScreenImpl1);
|
||||
#endif
|
||||
} else {
|
||||
LOG_ERROR("Failed to find touch controller!");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -8,20 +8,127 @@
|
||||
|
||||
CSE_CST328 tsPanel = CSE_CST328(EINK_WIDTH, EINK_HEIGHT, &Wire, CST328_PIN_RST, CST328_PIN_INT);
|
||||
|
||||
static bool is_cst3530 = false;
|
||||
volatile bool touch_isr = false;
|
||||
#define CST3530_ADDR 0x1A
|
||||
|
||||
bool read_cst3530_touch(int16_t *x, int16_t *y)
|
||||
{
|
||||
uint8_t buffer[9] = {0};
|
||||
uint8_t r_cmd[] = {0xD0, 0x07, 0x00, 0x00};
|
||||
uint8_t clear_cmd[] = {0xD0, 0x00, 0x02, 0xAB};
|
||||
|
||||
Wire.beginTransmission(CST3530_ADDR);
|
||||
Wire.write(r_cmd, sizeof(r_cmd));
|
||||
if (Wire.endTransmission() != 0) {
|
||||
LOG_DEBUG("CST3530 I2C send addr failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
int read_len = Wire.requestFrom((int)CST3530_ADDR, sizeof(buffer));
|
||||
if (read_len != sizeof(buffer)) {
|
||||
LOG_DEBUG("CST3530 read len error: %d (expect 9)", read_len);
|
||||
return false;
|
||||
}
|
||||
int actual_read = Wire.readBytes(buffer, sizeof(buffer));
|
||||
if (actual_read != sizeof(buffer)) {
|
||||
LOG_DEBUG("CST3530 read bytes error: %d (expect 9)", actual_read);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t report_typ = buffer[2];
|
||||
if (report_typ != 0xFF) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t touch_points = buffer[3] & 0x0F;
|
||||
if (touch_points == 0 || touch_points > 1) {
|
||||
LOG_DEBUG("CST3530 touch points invalid: %d", touch_points);
|
||||
return false;
|
||||
}
|
||||
|
||||
*x = buffer[4] + ((uint16_t)(buffer[7] & 0x0F) << 8);
|
||||
*y = buffer[5] + ((uint16_t)(buffer[7] & 0xF0) << 4);
|
||||
|
||||
// LOG_DEBUG("CST3530 touch: num:%d x=%d,y=%d", touch_points, *x, *y);
|
||||
|
||||
Wire.beginTransmission(CST3530_ADDR);
|
||||
Wire.write(clear_cmd, sizeof(clear_cmd));
|
||||
if (Wire.endTransmission() != 0) {
|
||||
LOG_DEBUG("CST3530 clear cmd failed");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool readTouch(int16_t *x, int16_t *y)
|
||||
{
|
||||
if (tsPanel.getTouches()) {
|
||||
*x = tsPanel.getPoint(0).x;
|
||||
*y = tsPanel.getPoint(0).y;
|
||||
return true;
|
||||
|
||||
if (is_cst3530) {
|
||||
if (touch_isr) {
|
||||
touch_isr = false;
|
||||
return read_cst3530_touch(x, y);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
if (tsPanel.getTouches()) {
|
||||
*x = tsPanel.getPoint(0).x;
|
||||
*y = tsPanel.getPoint(0).y;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void IRAM_ATTR touchInterruptHandler()
|
||||
{
|
||||
touch_isr = true;
|
||||
}
|
||||
|
||||
// T-Deck Pro specific init
|
||||
void lateInitVariant()
|
||||
{
|
||||
tsPanel.begin();
|
||||
// Reset touch
|
||||
pinMode(CST328_PIN_RST, OUTPUT);
|
||||
digitalWrite(CST328_PIN_RST, HIGH);
|
||||
delay(20);
|
||||
digitalWrite(CST328_PIN_RST, LOW);
|
||||
delay(80);
|
||||
digitalWrite(CST328_PIN_RST, HIGH);
|
||||
delay(20);
|
||||
|
||||
int retry = 5;
|
||||
uint8_t buffer[7];
|
||||
uint8_t r_cmd[] = {0x0d0, 0x03, 0x00, 0x00};
|
||||
|
||||
// Probe touch chip
|
||||
while (retry--) {
|
||||
Wire.beginTransmission(CST3530_ADDR);
|
||||
Wire.write(r_cmd, sizeof(r_cmd));
|
||||
if (Wire.endTransmission() == 0) {
|
||||
Wire.requestFrom((int)CST3530_ADDR, 7);
|
||||
Wire.readBytes(buffer, 7);
|
||||
if (buffer[2] == 0xCA && buffer[3] == 0xCA) {
|
||||
LOG_DEBUG("CST3530 detected");
|
||||
is_cst3530 = true;
|
||||
|
||||
// The CST3530 will automatically enter sleep mode;
|
||||
// polling should not be used, but rather an interrupt method should be employed.
|
||||
pinMode(CST328_PIN_INT, INPUT);
|
||||
attachInterrupt(digitalPinToInterrupt(CST328_PIN_INT), touchInterruptHandler, FALLING);
|
||||
|
||||
break;
|
||||
} else {
|
||||
LOG_DEBUG("CST3530 not response ~!");
|
||||
}
|
||||
}
|
||||
uint8_t cmd1[] = {0xD0, 0x00, 0x04, 0x00};
|
||||
Wire.beginTransmission(CST3530_ADDR);
|
||||
Wire.write(cmd1, sizeof(cmd1));
|
||||
Wire.endTransmission();
|
||||
delay(50);
|
||||
}
|
||||
|
||||
touchScreenImpl1 = new TouchScreenImpl1(EINK_WIDTH, EINK_HEIGHT, readTouch);
|
||||
touchScreenImpl1->init();
|
||||
}
|
||||
|
||||
@@ -182,6 +182,10 @@ void portduinoSetup()
|
||||
// Force stdout to be line buffered
|
||||
setvbuf(stdout, stdoutBuffer, _IOLBF, sizeof(stdoutBuffer));
|
||||
|
||||
// We do this super early so that we can log from the rest of the init code
|
||||
concurrency::hasBeenSetup = true;
|
||||
consoleInit();
|
||||
|
||||
if (portduino_config.force_simradio == true) {
|
||||
portduino_config.lora_module = use_simradio;
|
||||
} else if (configPath != nullptr) {
|
||||
@@ -650,7 +654,9 @@ void portduinoSetup()
|
||||
if (verboseEnabled && portduino_config.logoutputlevel != level_trace) {
|
||||
portduino_config.logoutputlevel = level_debug;
|
||||
}
|
||||
|
||||
if (portduino_config.lora_spi_dev != "") {
|
||||
portduinoSetOptions({.realHardware = true});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,57 @@
|
||||
#include <stm32wle5xx.h>
|
||||
#include <stm32wlxx_hal.h>
|
||||
|
||||
// ─── Bootloader redirect ──────────────────────────────────────────────────────
|
||||
//
|
||||
// Why .noinit + constructor instead of TAMP backup registers:
|
||||
//
|
||||
// The STM32duino startup sequence initialises clocks which may call
|
||||
// __HAL_RCC_BACKUPRESET_FORCE/RELEASE when configuring the LSE oscillator,
|
||||
// wiping the entire backup domain (including TAMP->BKP0R) before setup()
|
||||
// ever runs. The backup-register approach therefore cannot reliably survive
|
||||
// a soft reset in this toolchain.
|
||||
//
|
||||
// Solution: store the magic in a .noinit SRAM variable.
|
||||
// - NVIC_SystemReset() does NOT clear SRAM.
|
||||
// - The linker script skips zero-init for .noinit sections.
|
||||
// - __attribute__((constructor)) fires before main()/HAL_Init(), so we can
|
||||
// intercept and jump before anything disturbs peripheral state.
|
||||
|
||||
#define BOOTLOADER_MAGIC 0xD00DB007UL
|
||||
#define SYS_MEM_BASE 0x1FFF0000UL
|
||||
|
||||
// Placed in .noinit — not zeroed at startup, survives NVIC_SystemReset().
|
||||
__attribute__((section(".noinit"), used)) volatile uint32_t g_bootloaderMagic;
|
||||
|
||||
// Fires before main() / HAL_Init(). Must use only core Cortex-M registers.
|
||||
__attribute__((constructor(101), used)) static void earlyBootCheck(void)
|
||||
{
|
||||
if (g_bootloaderMagic != BOOTLOADER_MAGIC)
|
||||
return;
|
||||
g_bootloaderMagic = 0;
|
||||
|
||||
SysTick->CTRL = 0;
|
||||
SysTick->LOAD = 0;
|
||||
SysTick->VAL = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
NVIC->ICER[i] = 0xFFFFFFFF;
|
||||
NVIC->ICPR[i] = 0xFFFFFFFF;
|
||||
}
|
||||
__DSB();
|
||||
__ISB();
|
||||
SCB->VTOR = SYS_MEM_BASE;
|
||||
__set_MSP(*(volatile uint32_t *)SYS_MEM_BASE);
|
||||
((void (*)(void))(*(volatile uint32_t *)(SYS_MEM_BASE + 4)))();
|
||||
while (1)
|
||||
;
|
||||
}
|
||||
|
||||
void enterDfuMode()
|
||||
{
|
||||
g_bootloaderMagic = BOOTLOADER_MAGIC;
|
||||
HAL_NVIC_SystemReset();
|
||||
}
|
||||
|
||||
void setBluetoothEnable(bool enable) {}
|
||||
|
||||
void playStartMelody() {}
|
||||
|
||||
@@ -17,8 +17,13 @@
|
||||
|
||||
// Device specific curves go in variant.h
|
||||
#ifndef OCV_ARRAY
|
||||
#if defined(ARCH_STM32WL) && BATTERY_PIN == AVBAT
|
||||
// STM32 VDD/VBAT absolute maximum is 4V so use an LFP curve
|
||||
#define OCV_ARRAY 3650, 3400, 3340, 3320, 3300, 3280, 3270, 3260, 3240, 3200, 2500
|
||||
#else
|
||||
#define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/
|
||||
#ifndef NUM_CELLS
|
||||
|
||||
Reference in New Issue
Block a user