mirror of
https://github.com/ratspeak/ratdeck.git
synced 2026-10-22 17:31:14 +00:00
GPS time sync, status bar clock, configurable UTC offset
- Enable UBlox MIA-M10Q GPS on T-Deck Plus (HAS_GPS true) - Custom zero-dependency NMEA parser (NMEAParser.h) with XOR checksum - GPSManager: baud auto-detect (115200/38400/9600, 10s timeout), settimeofday() sync every 60s, NVS time/position persistence - GPS Time enabled by default, GPS Location disabled by default (user must explicitly opt in via Settings) - Replace status bar connectivity bars with clock display (top-left) - Default 12h format without AM/PM, configurable 24h in Settings - Add GPS/Time settings category: GPS Time, GPS Location, UTC Offset (-12 to +14), 24h Time toggle - Fix hardcoded UTC-5 NTP timezone to use configurable utcOffset - Change status bar brand text to "Ratspeak.org"
This commit is contained in:
1 parent
31cbab7fc2
commit
311c5ad3df
11 files changed
+728
-76
No files matched your search
+47
-60
@@ -1,6 +1,7 @@
|
||||
#include "LvStatusBar.h"
|
||||
#include "Theme.h"
|
||||
#include <Arduino.h>
|
||||
#include <time.h>
|
||||
|
||||
void LvStatusBar::create(lv_obj_t* parent) {
|
||||
_bar = lv_obj_create(parent);
|
||||
@@ -17,29 +18,27 @@ void LvStatusBar::create(lv_obj_t* parent) {
|
||||
|
||||
const lv_font_t* font = &lv_font_montserrat_12;
|
||||
|
||||
// Left side: Signal bars (3 bars, increasing height)
|
||||
static const int barW = 4;
|
||||
static const int barH[] = {6, 10, 14};
|
||||
static const int barGap = 2;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
_bars[i] = lv_obj_create(_bar);
|
||||
lv_obj_set_size(_bars[i], barW, barH[i]);
|
||||
lv_obj_set_style_radius(_bars[i], 1, 0);
|
||||
lv_obj_set_style_bg_opa(_bars[i], LV_OPA_COVER, 0);
|
||||
lv_obj_set_style_border_width(_bars[i], 0, 0);
|
||||
lv_obj_set_style_pad_all(_bars[i], 0, 0);
|
||||
int x = 4 + i * (barW + barGap);
|
||||
int y = Theme::STATUS_BAR_H - barH[i] - 3; // bottom-aligned
|
||||
lv_obj_set_pos(_bars[i], x, y);
|
||||
}
|
||||
// Left: Time display (hidden until valid time is available)
|
||||
_lblTime = lv_label_create(_bar);
|
||||
lv_obj_set_style_text_font(_lblTime, font, 0);
|
||||
lv_obj_set_style_text_color(_lblTime, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_label_set_text(_lblTime, "");
|
||||
lv_obj_align(_lblTime, LV_ALIGN_LEFT_MID, 4, 0);
|
||||
|
||||
// Center: "Ratspeak"
|
||||
// Center: "Ratspeak.org"
|
||||
_lblBrand = lv_label_create(_bar);
|
||||
lv_obj_set_style_text_font(_lblBrand, font, 0);
|
||||
lv_obj_set_style_text_color(_lblBrand, lv_color_hex(Theme::ACCENT), 0);
|
||||
lv_label_set_text(_lblBrand, "Ratspeak");
|
||||
lv_label_set_text(_lblBrand, "Ratspeak.org");
|
||||
lv_obj_align(_lblBrand, LV_ALIGN_CENTER, 0, 0);
|
||||
|
||||
// Right: GPS indicator (left of battery, hidden until fix)
|
||||
_lblGPS = lv_label_create(_bar);
|
||||
lv_obj_set_style_text_font(_lblGPS, font, 0);
|
||||
lv_obj_set_style_text_color(_lblGPS, lv_color_hex(Theme::PRIMARY), 0);
|
||||
lv_label_set_text(_lblGPS, "");
|
||||
lv_obj_align(_lblGPS, LV_ALIGN_RIGHT_MID, -42, 0);
|
||||
|
||||
// Right: Battery %
|
||||
_lblBatt = lv_label_create(_bar);
|
||||
lv_obj_set_style_text_font(_lblBatt, font, 0);
|
||||
@@ -62,16 +61,12 @@ void LvStatusBar::create(lv_obj_t* parent) {
|
||||
lv_obj_set_style_text_color(_lblToast, lv_color_hex(Theme::BG), 0);
|
||||
lv_obj_center(_lblToast);
|
||||
lv_label_set_text(_lblToast, "");
|
||||
|
||||
// Set initial indicator colors
|
||||
refreshIndicators();
|
||||
}
|
||||
|
||||
void LvStatusBar::update() {
|
||||
// Handle announce flash timeout
|
||||
if (_announceFlashEnd > 0 && millis() >= _announceFlashEnd) {
|
||||
_announceFlashEnd = 0;
|
||||
refreshIndicators();
|
||||
}
|
||||
|
||||
// Handle toast timeout
|
||||
@@ -81,24 +76,39 @@ void LvStatusBar::update() {
|
||||
}
|
||||
}
|
||||
|
||||
void LvStatusBar::setLoRaOnline(bool online) {
|
||||
_loraOnline = online;
|
||||
refreshIndicators();
|
||||
void LvStatusBar::updateTime() {
|
||||
if (!_lblTime) return;
|
||||
|
||||
time_t now = time(nullptr);
|
||||
if (now <= 1700000000) {
|
||||
// No valid time yet — show nothing
|
||||
lv_label_set_text(_lblTime, "");
|
||||
return;
|
||||
}
|
||||
|
||||
struct tm* local = localtime(&now);
|
||||
if (!local) {
|
||||
lv_label_set_text(_lblTime, "");
|
||||
return;
|
||||
}
|
||||
|
||||
char buf[8];
|
||||
if (_use24h) {
|
||||
snprintf(buf, sizeof(buf), "%02d:%02d", local->tm_hour, local->tm_min);
|
||||
} else {
|
||||
int h = local->tm_hour % 12;
|
||||
if (h == 0) h = 12;
|
||||
snprintf(buf, sizeof(buf), "%d:%02d", h, local->tm_min);
|
||||
}
|
||||
lv_label_set_text(_lblTime, buf);
|
||||
}
|
||||
|
||||
void LvStatusBar::setBLEActive(bool active) {
|
||||
_bleActive = active;
|
||||
refreshIndicators();
|
||||
}
|
||||
|
||||
void LvStatusBar::setWiFiActive(bool active) {
|
||||
_wifiActive = active;
|
||||
refreshIndicators();
|
||||
}
|
||||
|
||||
void LvStatusBar::setTCPConnected(bool connected) {
|
||||
_tcpConnected = connected;
|
||||
refreshIndicators();
|
||||
void LvStatusBar::setGPSFix(bool hasFix) {
|
||||
if (_gpsFix == hasFix) return;
|
||||
_gpsFix = hasFix;
|
||||
if (_lblGPS) {
|
||||
lv_label_set_text(_lblGPS, hasFix ? "GPS" : "");
|
||||
}
|
||||
}
|
||||
|
||||
void LvStatusBar::setBatteryPercent(int pct) {
|
||||
@@ -121,7 +131,6 @@ void LvStatusBar::setTransportMode(const char* mode) {
|
||||
|
||||
void LvStatusBar::flashAnnounce() {
|
||||
_announceFlashEnd = millis() + 1000;
|
||||
refreshIndicators();
|
||||
}
|
||||
|
||||
void LvStatusBar::showToast(const char* msg, uint32_t durationMs) {
|
||||
@@ -129,25 +138,3 @@ void LvStatusBar::showToast(const char* msg, uint32_t durationMs) {
|
||||
_toastEnd = millis() + durationMs;
|
||||
lv_obj_clear_flag(_toast, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void LvStatusBar::refreshIndicators() {
|
||||
// Bar 0 (short): LoRa — green=online, red=offline
|
||||
if (_bars[0]) {
|
||||
uint32_t col = _loraOnline ? Theme::PRIMARY : Theme::ERROR_CLR;
|
||||
lv_obj_set_style_bg_color(_bars[0], lv_color_hex(col), 0);
|
||||
}
|
||||
// Bar 1 (medium): WiFi — green=connected, yellow=enabled but not connected, red=off
|
||||
if (_bars[1]) {
|
||||
uint32_t col = Theme::ERROR_CLR;
|
||||
if (_wifiActive) col = Theme::PRIMARY;
|
||||
else if (_wifiEnabled) col = Theme::WARNING_CLR;
|
||||
lv_obj_set_style_bg_color(_bars[1], lv_color_hex(col), 0);
|
||||
}
|
||||
// Bar 2 (tall): TCP — green=connected, yellow=WiFi up but TCP down, red=off
|
||||
if (_bars[2]) {
|
||||
uint32_t col = Theme::ERROR_CLR;
|
||||
if (_tcpConnected) col = Theme::PRIMARY;
|
||||
else if (_wifiActive) col = Theme::WARNING_CLR;
|
||||
lv_obj_set_style_bg_color(_bars[2], lv_color_hex(col), 0);
|
||||
}
|
||||
}
|
||||
+18
-12
@@ -8,27 +8,31 @@ public:
|
||||
void create(lv_obj_t* parent);
|
||||
void update();
|
||||
|
||||
// Status indicators: green=active, yellow=enabled-but-unconfigured, red=off
|
||||
void setLoRaOnline(bool online);
|
||||
void setBLEActive(bool active);
|
||||
void setBLEEnabled(bool enabled) { _bleEnabled = enabled; refreshIndicators(); }
|
||||
void setWiFiActive(bool active);
|
||||
void setWiFiEnabled(bool enabled) { _wifiEnabled = enabled; refreshIndicators(); }
|
||||
void setTCPConnected(bool connected);
|
||||
// Status setters (kept for API compatibility, no bars drawn)
|
||||
void setLoRaOnline(bool online) { _loraOnline = online; }
|
||||
void setBLEActive(bool active) { _bleActive = active; }
|
||||
void setBLEEnabled(bool enabled) { _bleEnabled = enabled; }
|
||||
void setWiFiActive(bool active) { _wifiActive = active; }
|
||||
void setWiFiEnabled(bool enabled) { _wifiEnabled = enabled; }
|
||||
void setTCPConnected(bool connected) { _tcpConnected = connected; }
|
||||
void setGPSFix(bool hasFix);
|
||||
void setBatteryPercent(int pct);
|
||||
void setTransportMode(const char* mode);
|
||||
void flashAnnounce();
|
||||
void showToast(const char* msg, uint32_t durationMs = 1500);
|
||||
|
||||
// Time display
|
||||
void setUse24Hour(bool use24h) { _use24h = use24h; }
|
||||
void updateTime(); // Call at 1 Hz to refresh clock
|
||||
|
||||
lv_obj_t* obj() { return _bar; }
|
||||
|
||||
private:
|
||||
void refreshIndicators();
|
||||
|
||||
lv_obj_t* _bar = nullptr;
|
||||
lv_obj_t* _bars[3] = {};
|
||||
lv_obj_t* _lblBrand = nullptr;
|
||||
lv_obj_t* _lblBatt = nullptr;
|
||||
lv_obj_t* _lblTime = nullptr; // Top-left: current time
|
||||
lv_obj_t* _lblBrand = nullptr; // Center: "Ratspeak.org"
|
||||
lv_obj_t* _lblGPS = nullptr; // Right: GPS indicator
|
||||
lv_obj_t* _lblBatt = nullptr; // Right: battery %
|
||||
lv_obj_t* _toast = nullptr;
|
||||
lv_obj_t* _lblToast = nullptr;
|
||||
|
||||
@@ -38,6 +42,8 @@ private:
|
||||
bool _wifiActive = false;
|
||||
bool _wifiEnabled = false;
|
||||
bool _tcpConnected = false;
|
||||
bool _gpsFix = false;
|
||||
bool _use24h = false;
|
||||
int _battPct = -1;
|
||||
unsigned long _announceFlashEnd = 0;
|
||||
unsigned long _toastEnd = 0;
|
||||
|
||||
@@ -368,6 +368,37 @@ void LvSettingsScreen::buildItems() {
|
||||
return String(modes[constrain((int)s.wifiMode, 0, 2)]);
|
||||
}});
|
||||
|
||||
// GPS & Time
|
||||
int gpsStart = idx;
|
||||
#if HAS_GPS
|
||||
_items.push_back({"GPS Time", SettingType::TOGGLE,
|
||||
[&s]() { return s.gpsTimeEnabled ? 1 : 0; },
|
||||
[&s](int v) { s.gpsTimeEnabled = (v != 0); },
|
||||
[](int v) { return v ? String("ON") : String("OFF"); }});
|
||||
idx++;
|
||||
_items.push_back({"GPS Location", SettingType::TOGGLE,
|
||||
[&s]() { return s.gpsLocationEnabled ? 1 : 0; },
|
||||
[&s](int v) { s.gpsLocationEnabled = (v != 0); },
|
||||
[](int v) { return v ? String("ON") : String("OFF"); }});
|
||||
idx++;
|
||||
#endif
|
||||
_items.push_back({"UTC Offset", SettingType::INTEGER,
|
||||
[&s]() { return (int)s.utcOffset; }, [&s](int v) { s.utcOffset = (int8_t)v; },
|
||||
[](int v) { char buf[8]; snprintf(buf, sizeof(buf), "UTC%+d", v); return String(buf); },
|
||||
-12, 14, 1});
|
||||
idx++;
|
||||
_items.push_back({"24h Time", SettingType::TOGGLE,
|
||||
[&s]() { return s.use24HourTime ? 1 : 0; },
|
||||
[&s](int v) { s.use24HourTime = (v != 0); },
|
||||
[](int v) { return v ? String("ON") : String("OFF"); }});
|
||||
idx++;
|
||||
_categories.push_back({"GPS/Time", gpsStart, idx - gpsStart,
|
||||
[&s]() {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "UTC%+d", s.utcOffset);
|
||||
return String(buf);
|
||||
}});
|
||||
|
||||
// Audio
|
||||
int audioStart = idx;
|
||||
_items.push_back({"Audio", SettingType::TOGGLE,
|
||||
@@ -1133,6 +1164,7 @@ void LvSettingsScreen::snapshotRebootSettings() {
|
||||
_rebootSnap.wifiSTASSID = s.wifiSTASSID;
|
||||
_rebootSnap.wifiSTAPassword = s.wifiSTAPassword;
|
||||
_rebootSnap.bleEnabled = s.bleEnabled;
|
||||
_gpsSnapEnabled = s.gpsTimeEnabled;
|
||||
}
|
||||
|
||||
bool LvSettingsScreen::rebootSettingsChanged() const {
|
||||
@@ -1195,6 +1227,12 @@ void LvSettingsScreen::applyAndSave() {
|
||||
if (_tcpChangeCb) _tcpChangeCb();
|
||||
}
|
||||
|
||||
// Apply GPS toggle live (start/stop GPS UART)
|
||||
if (s.gpsTimeEnabled != _gpsSnapEnabled) {
|
||||
_gpsSnapEnabled = s.gpsTimeEnabled;
|
||||
if (_gpsChangeCb) _gpsChangeCb(s.gpsTimeEnabled);
|
||||
}
|
||||
|
||||
// Check if reboot-required settings changed
|
||||
if (rebootSettingsChanged()) {
|
||||
_rebootNeeded = true;
|
||||
|
||||
@@ -75,6 +75,7 @@ public:
|
||||
void setDestinationHash(const String& hash) { _destinationHash = hash; }
|
||||
void setSaveCallback(std::function<bool()> cb) { _saveCallback = cb; }
|
||||
void setTCPChangeCallback(std::function<void()> cb) { _tcpChangeCb = cb; }
|
||||
void setGPSChangeCallback(std::function<void(bool enabled)> cb) { _gpsChangeCb = cb; }
|
||||
|
||||
const char* title() const override { return "Settings"; }
|
||||
|
||||
@@ -114,6 +115,8 @@ private:
|
||||
String _destinationHash;
|
||||
std::function<bool()> _saveCallback;
|
||||
std::function<void()> _tcpChangeCb;
|
||||
std::function<void(bool)> _gpsChangeCb;
|
||||
bool _gpsSnapEnabled = true;
|
||||
|
||||
SettingsView _view = SettingsView::CATEGORY_LIST;
|
||||
std::vector<SettingsCategory> _categories;
|
||||
|
||||
Reference in new issue
Block a user