Refine gat562 mono UI and persist UI settings

This commit is contained in:
liu weikai
2026-03-23 14:22:25 +08:00
parent 25381c5c22
commit 8ccb1732f2
25 changed files with 2188 additions and 83 deletions
+2
View File
@@ -24,6 +24,8 @@ jobs:
git ls-files -z '*.c' '*.cpp' '*.h' '*.hpp' ':(exclude)modules/core_chat/generated/**' \
':(exclude)modules/ui_shared/include/ui/assets/**' \
':(exclude)modules/ui_shared/src/ui/assets/**' \
':(exclude)platform/nrf52/arduino_common/include/ui/fonts/**' \
':(exclude)platform/nrf52/arduino_common/src/ui/fonts/**' \
':(exclude)third_party/codec2/**' \
| xargs -0 clang-format-14 --dry-run --Werror
@@ -2,6 +2,7 @@
#include "chat/domain/chat_types.h"
#include "chat/runtime/self_identity_provider.h"
#include "chat/usecase/contact_service.h"
#include "platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h"
#include <memory>
@@ -16,6 +17,7 @@ namespace apps::gat562_mesh_evb_pro
std::unique_ptr<chat::IMeshAdapter> createProtocolAdapter(chat::MeshProtocol protocol,
const chat::runtime::SelfIdentityProvider* identity_provider,
platform::nrf52::arduino_common::chat::meshtastic::NodeStore* meshtastic_node_store = nullptr);
platform::nrf52::arduino_common::chat::meshtastic::NodeStore* meshtastic_node_store = nullptr,
chat::contacts::ContactService* contact_service = nullptr);
} // namespace apps::gat562_mesh_evb_pro
@@ -185,7 +185,8 @@ void AppFacadeRuntime::initializeChatRuntime()
(void)installMeshBackend(chat::MeshProtocol::Meshtastic,
createProtocolAdapter(chat::MeshProtocol::Meshtastic,
identityProvider(),
static_cast<platform::nrf52::arduino_common::chat::meshtastic::NodeStore*>(node_store_.get())));
static_cast<platform::nrf52::arduino_common::chat::meshtastic::NodeStore*>(node_store_.get()),
contact_service_.get()));
(void)installMeshBackend(chat::MeshProtocol::MeshCore,
createProtocolAdapter(chat::MeshProtocol::MeshCore, identityProvider()));
@@ -8,7 +8,8 @@ namespace apps::gat562_mesh_evb_pro
std::unique_ptr<chat::IMeshAdapter> createProtocolAdapter(chat::MeshProtocol protocol,
const chat::runtime::SelfIdentityProvider* identity_provider,
platform::nrf52::arduino_common::chat::meshtastic::NodeStore* meshtastic_node_store)
platform::nrf52::arduino_common::chat::meshtastic::NodeStore* meshtastic_node_store,
chat::contacts::ContactService* contact_service)
{
switch (protocol)
{
@@ -19,7 +20,8 @@ std::unique_ptr<chat::IMeshAdapter> createProtocolAdapter(chat::MeshProtocol pro
default:
return std::unique_ptr<chat::IMeshAdapter>(
new platform::nrf52::arduino_common::chat::meshtastic::MeshtasticRadioAdapter(identity_provider,
meshtastic_node_store));
meshtastic_node_store,
contact_service));
}
}
@@ -11,6 +11,7 @@
#include "ui/mono_128x64/runtime.h"
#include <Arduino.h>
#include <InternalFileSystem.h>
#include <ctime>
namespace apps::gat562_mesh_evb_pro::ui_runtime
@@ -19,11 +20,16 @@ namespace
{
using boards::gat562_mesh_evb_pro::BoardInputEvent;
using boards::gat562_mesh_evb_pro::BoardInputKey;
using Adafruit_LittleFS_Namespace::File;
constexpr uint32_t kProbeHoldMs = 900;
constexpr uint32_t kGat562TotalRamBytes = 248832U;
constexpr uint32_t kGat562FsTotalBytes = 7U * 4096U;
const char kProbeAscii[] = "ABC123";
const char kProbeCjk[] = "\xE4\xB8\xAD\xE6\x96\x87";
const char kProbeSymbols[] = "\xE2\x94\x80\xE2\x96\x88\xE2\x96\xA0";
extern "C" char* sbrk(int incr);
uint32_t now_ms() { return millis(); }
time_t utc_now() { return static_cast<time_t>(sys::epoch_seconds_now()); }
@@ -37,6 +43,73 @@ bool format_freq(uint32_t freq_hz, char* out, size_t out_len)
return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().formatLoraFrequencyMHz(freq_hz, out, out_len);
}
ui::mono_128x64::HostCallbacks::ResourceUsage ram_usage()
{
ui::mono_128x64::HostCallbacks::ResourceUsage usage{};
usage.available = true;
usage.total_bytes = kGat562TotalRamBytes;
char stack_marker = 0;
const uintptr_t stack_ptr = reinterpret_cast<uintptr_t>(&stack_marker);
const uintptr_t heap_ptr = reinterpret_cast<uintptr_t>(sbrk(0));
if (stack_ptr > heap_ptr)
{
const uint32_t free_bytes = static_cast<uint32_t>(stack_ptr - heap_ptr);
usage.used_bytes = usage.total_bytes > free_bytes ? (usage.total_bytes - free_bytes) : 0U;
}
return usage;
}
uint32_t accumulateFsBytes(File dir)
{
uint32_t total = 0;
if (!dir)
{
return total;
}
dir.rewindDirectory();
while (true)
{
File entry = dir.openNextFile();
if (!entry)
{
break;
}
if (entry.isDirectory())
{
total += accumulateFsBytes(entry);
}
else
{
total += entry.size();
}
entry.close();
}
return total;
}
ui::mono_128x64::HostCallbacks::ResourceUsage flash_usage()
{
ui::mono_128x64::HostCallbacks::ResourceUsage usage{};
if (!InternalFS.begin())
{
return usage;
}
File root = InternalFS.open("/");
if (!root)
{
return usage;
}
usage.available = true;
usage.used_bytes = accumulateFsBytes(root);
usage.total_bytes = kGat562FsTotalBytes;
root.close();
return usage;
}
ui::mono_128x64::InputAction to_input_action(
const BoardInputEvent* event)
{
@@ -126,6 +199,8 @@ bool initialize()
callbacks.gps_data_fn = platform::ui::gps::get_data;
callbacks.gps_enabled_fn = platform::ui::gps::is_enabled;
callbacks.gps_powered_fn = platform::ui::gps::is_powered;
callbacks.ram_usage_fn = ram_usage;
callbacks.flash_usage_fn = flash_usage;
static ui::mono_128x64::Runtime runtime(::boards::gat562_mesh_evb_pro::Gat562Board::instance().monoDisplay(),
callbacks);
@@ -81,6 +81,7 @@ class Gat562Board final : public BoardBase
void wakeUp() override;
void handlePowerButton() override;
void softwareShutdown() override;
int getPowerTier() const override;
void setBrightness(uint8_t level) override;
uint8_t getBrightness() override;
@@ -7,10 +7,30 @@
namespace boards::gat562_mesh_evb_pro::settings_store
{
enum class StoreStatus : uint8_t
{
Ok = 0,
NotFound,
FsInitFailed,
OpenFailed,
ReadFailed,
WriteFailed,
FlushFailed,
HeaderInvalid,
VersionMismatch,
PayloadSizeMismatch,
CrcMismatch,
RenameFailed,
BackupFailed,
};
void normalizeConfig(app::AppConfig& config);
bool loadAppConfig(app::AppConfig& config);
bool saveAppConfig(const app::AppConfig& config);
uint8_t loadMessageToneVolume();
bool saveMessageToneVolume(uint8_t volume);
StoreStatus lastLoadStatus();
StoreStatus lastSaveStatus();
const char* statusLabel(StoreStatus status);
} // namespace boards::gat562_mesh_evb_pro::settings_store
+3 -1
View File
@@ -27,7 +27,9 @@
"-I../../.pio/libdeps/gat562_mesh_evb_pro/TinyGPSPlus/src",
"-I../../.pio/libdeps/gat562_mesh_evb_pro/Adafruit\\ GFX\\ Library",
"-I../../.pio/libdeps/gat562_mesh_evb_pro/Adafruit\\ SSD1306",
"-I../../.pio/libdeps/gat562_mesh_evb_pro/Adafruit\\ BusIO"
"-I../../.pio/libdeps/gat562_mesh_evb_pro/Adafruit\\ BusIO",
"-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/Adafruit_LittleFS/src",
"-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/InternalFileSytem/src"
]
}
}
@@ -447,10 +447,13 @@ void Gat562Board::enablePeripheralRail()
void Gat562Board::wakeUp()
{
enablePeripheralRail();
setStatusLed(false);
}
void Gat562Board::handlePowerButton()
{
pulseNotificationLed(40);
}
void Gat562Board::softwareShutdown()
@@ -458,6 +461,24 @@ void Gat562Board::softwareShutdown()
NVIC_SystemReset();
}
int Gat562Board::getPowerTier() const
{
const int level = const_cast<Gat562Board*>(this)->readBatteryPercent();
if (level < 0)
{
return 0;
}
if (level <= 10)
{
return 2;
}
if (level <= 20)
{
return 1;
}
return 0;
}
void Gat562Board::setBrightness(uint8_t level)
{
brightness_ = static_cast<uint8_t>(
@@ -544,6 +565,9 @@ void Gat562Board::vibrator()
void Gat562Board::stopVibrator()
{
const auto& leds = kBoardProfile.leds;
const int pin = leds.notification_shares_status ? leds.status : leds.notification;
writeLed(pin, leds.active_high, false);
}
void Gat562Board::playMessageTone()
@@ -709,21 +733,23 @@ bool Gat562Board::pollInputEvent(BoardInputEvent* out_event)
namespace
{
constexpr int kMonoScreenWidth = 128;
constexpr int kMonoScreenHeight = 64;
class Ssd1306MonoDisplay final : public ::ui::mono_128x64::MonoDisplay
{
public:
Ssd1306MonoDisplay()
: display_(SCREEN_WIDTH,
SCREEN_HEIGHT,
: display_(kMonoScreenWidth,
kMonoScreenHeight,
&::boards::gat562_mesh_evb_pro::Gat562Board::instance().i2cWire(),
-1)
{
}
bool begin() override;
int width() const override { return SCREEN_WIDTH; }
int height() const override { return SCREEN_HEIGHT; }
int width() const override { return kMonoScreenWidth; }
int height() const override { return kMonoScreenHeight; }
void clear() override
{
if (online_)
@@ -1001,19 +1027,19 @@ bool Gat562Board::gpsGnssSnapshot(::gps::GnssSatInfo* out,
{
*status = s_gps.status;
}
if (out && max > 0 && s_gps.data.valid)
if (out && max > 0 && s_gps.data.satellites > 0)
{
out[0].id = 0;
out[0].sys = ::gps::GnssSystem::GPS;
out[0].snr = -1;
out[0].used = true;
out[0].used = s_gps.data.valid;
if (out_count)
{
*out_count = 1;
}
return true;
}
return s_gps.data.valid;
return s_gps.data.valid || s_gps.data.satellites > 0;
}
void Gat562Board::setGpsCollectionInterval(uint32_t interval_ms) { s_gps.collection_interval_ms = interval_ms; }
@@ -10,6 +10,84 @@
namespace platform::ui::device
{
namespace
{
struct BatteryUiFilterState
{
bool initialized = false;
uint32_t last_sample_ms = 0;
int stable_level = -1;
};
BatteryUiFilterState s_battery_filter{};
int applyBatteryUiFilter(int raw_level)
{
if (raw_level < 0)
{
return raw_level;
}
constexpr uint32_t kSampleIntervalMs = 1500;
constexpr int kDisplayHysteresis = 2;
const uint32_t now_ms = millis();
if (!s_battery_filter.initialized)
{
s_battery_filter.initialized = true;
s_battery_filter.last_sample_ms = now_ms;
s_battery_filter.stable_level = raw_level;
return raw_level;
}
if ((now_ms - s_battery_filter.last_sample_ms) < kSampleIntervalMs)
{
return s_battery_filter.stable_level;
}
s_battery_filter.last_sample_ms = now_ms;
const int delta = raw_level - s_battery_filter.stable_level;
const int abs_delta = delta >= 0 ? delta : -delta;
if (abs_delta >= kDisplayHysteresis)
{
s_battery_filter.stable_level = raw_level;
}
else
{
s_battery_filter.stable_level = (s_battery_filter.stable_level + raw_level) / 2;
}
return s_battery_filter.stable_level;
}
int readFilteredBatteryLevel(::boards::gat562_mesh_evb_pro::Gat562Board& board)
{
constexpr int kBatterySamples = 6;
int valid_sum = 0;
int valid_count = 0;
for (int i = 0; i < kBatterySamples; ++i)
{
const int sample = board.getBatteryLevel();
if (sample >= 0)
{
valid_sum += sample;
++valid_count;
}
delay(2);
}
if (valid_count == 0)
{
return -1;
}
const int averaged = (valid_sum + (valid_count / 2)) / valid_count;
return applyBatteryUiFilter(averaged);
}
} // namespace
void delay_ms(uint32_t ms)
{
delay(ms);
@@ -31,7 +109,7 @@ BatteryInfo battery_info()
BatteryInfo info{};
info.available = true;
info.charging = board.isCharging();
info.level = board.getBatteryLevel();
info.level = readFilteredBatteryLevel(board);
return info;
}
@@ -72,7 +150,7 @@ bool gps_ready()
int power_tier()
{
return 0;
return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().getPowerTier();
}
} // namespace platform::ui::device
+376 -18
View File
@@ -4,14 +4,53 @@
#include "chat/infra/meshcore/mc_region_presets.h"
#include "chat/infra/meshtastic/mt_region.h"
#include <Arduino.h>
#include <InternalFileSystem.h>
#include <algorithm>
#include <cstring>
namespace boards::gat562_mesh_evb_pro::settings_store
{
namespace
{
using Adafruit_LittleFS_Namespace::FILE_O_READ;
using Adafruit_LittleFS_Namespace::FILE_O_WRITE;
bool s_has_config = false;
app::AppConfig s_config{};
uint8_t s_tone_volume = 45;
constexpr const char* kSettingsPath = "/gat562_settings.bin";
constexpr const char* kSettingsTempPath = "/gat562_settings.bin.tmp";
constexpr const char* kSettingsBackupPath = "/gat562_settings.bin.bak";
constexpr const char* kSettingsCorruptPath = "/gat562_settings.bin.corrupt";
constexpr uint32_t kSettingsMagic = 0x53415447UL; // GTAS
constexpr uint16_t kSettingsVersion = 1;
constexpr uint8_t kDefaultToneVolume = 45;
struct PersistedPayload
{
app::AppConfig config;
uint8_t tone_volume = kDefaultToneVolume;
uint8_t reserved[3] = {};
};
struct FileHeader
{
uint32_t magic = 0;
uint16_t version = 0;
uint16_t reserved = 0;
uint32_t payload_size = 0;
uint32_t crc32 = 0;
} __attribute__((packed));
struct CachedSettings
{
app::AppConfig config;
uint8_t tone_volume = kDefaultToneVolume;
};
bool s_cache_loaded = false;
CachedSettings s_cache{};
StoreStatus s_last_load_status = StoreStatus::NotFound;
StoreStatus s_last_save_status = StoreStatus::NotFound;
int8_t clampTxPower(int8_t value)
{
@@ -26,6 +65,307 @@ int8_t clampTxPower(int8_t value)
return value;
}
uint8_t clampToneVolume(uint8_t volume)
{
return static_cast<uint8_t>(std::min<unsigned>(volume, 100U));
}
const char* statusText(StoreStatus status)
{
switch (status)
{
case StoreStatus::Ok:
return "ok";
case StoreStatus::NotFound:
return "not_found";
case StoreStatus::FsInitFailed:
return "fs_init_failed";
case StoreStatus::OpenFailed:
return "open_failed";
case StoreStatus::ReadFailed:
return "read_failed";
case StoreStatus::WriteFailed:
return "write_failed";
case StoreStatus::FlushFailed:
return "flush_failed";
case StoreStatus::HeaderInvalid:
return "header_invalid";
case StoreStatus::VersionMismatch:
return "version_mismatch";
case StoreStatus::PayloadSizeMismatch:
return "payload_size_mismatch";
case StoreStatus::CrcMismatch:
return "crc_mismatch";
case StoreStatus::RenameFailed:
return "rename_failed";
case StoreStatus::BackupFailed:
return "backup_failed";
default:
return "unknown";
}
}
bool ensureFs()
{
if (InternalFS.begin())
{
return true;
}
Serial.printf("[gat562][settings] fs init failed\n");
return false;
}
void removeIfExists(const char* path)
{
if (path && InternalFS.exists(path))
{
InternalFS.remove(path);
}
}
uint32_t crc32(const uint8_t* data, size_t len)
{
uint32_t crc = 0xFFFFFFFFU;
for (size_t i = 0; i < len; ++i)
{
crc ^= data[i];
for (int bit = 0; bit < 8; ++bit)
{
crc = (crc & 1U) ? ((crc >> 1) ^ 0xEDB88320U) : (crc >> 1);
}
}
return ~crc;
}
bool quarantineCorruptFile(StoreStatus status)
{
if (!InternalFS.exists(kSettingsPath))
{
return true;
}
removeIfExists(kSettingsCorruptPath);
if (InternalFS.rename(kSettingsPath, kSettingsCorruptPath))
{
Serial.printf("[gat562][settings] quarantined corrupt store status=%s path=%s\n",
statusText(status),
kSettingsCorruptPath);
return true;
}
Serial.printf("[gat562][settings] failed to quarantine corrupt store status=%s\n",
statusText(status));
return false;
}
void resetCacheToDefaults()
{
s_cache = CachedSettings{};
s_cache.tone_volume = kDefaultToneVolume;
normalizeConfig(s_cache.config);
}
bool loadFromFs()
{
if (!ensureFs())
{
s_last_load_status = StoreStatus::FsInitFailed;
return false;
}
if (!InternalFS.exists(kSettingsPath))
{
s_last_load_status = StoreStatus::NotFound;
return false;
}
auto file = InternalFS.open(kSettingsPath, FILE_O_READ);
if (!file)
{
s_last_load_status = StoreStatus::OpenFailed;
Serial.printf("[gat562][settings] open failed path=%s\n", kSettingsPath);
return false;
}
const uint32_t expected_size = static_cast<uint32_t>(sizeof(FileHeader) + sizeof(PersistedPayload));
const uint32_t actual_size = file.size();
if (actual_size != expected_size)
{
file.close();
s_last_load_status = StoreStatus::PayloadSizeMismatch;
(void)quarantineCorruptFile(s_last_load_status);
Serial.printf("[gat562][settings] size mismatch actual=%lu expected=%lu\n",
static_cast<unsigned long>(actual_size),
static_cast<unsigned long>(expected_size));
return false;
}
FileHeader header{};
if (file.read(&header, sizeof(header)) != sizeof(header))
{
file.close();
s_last_load_status = StoreStatus::ReadFailed;
Serial.printf("[gat562][settings] header read failed\n");
return false;
}
if (header.magic != kSettingsMagic)
{
file.close();
s_last_load_status = StoreStatus::HeaderInvalid;
(void)quarantineCorruptFile(s_last_load_status);
Serial.printf("[gat562][settings] magic mismatch got=0x%08lX expected=0x%08lX\n",
static_cast<unsigned long>(header.magic),
static_cast<unsigned long>(kSettingsMagic));
return false;
}
if (header.version != kSettingsVersion)
{
file.close();
s_last_load_status = StoreStatus::VersionMismatch;
(void)quarantineCorruptFile(s_last_load_status);
Serial.printf("[gat562][settings] version mismatch got=%u expected=%u\n",
static_cast<unsigned>(header.version),
static_cast<unsigned>(kSettingsVersion));
return false;
}
if (header.payload_size != sizeof(PersistedPayload))
{
file.close();
s_last_load_status = StoreStatus::PayloadSizeMismatch;
(void)quarantineCorruptFile(s_last_load_status);
Serial.printf("[gat562][settings] payload size mismatch got=%lu expected=%lu\n",
static_cast<unsigned long>(header.payload_size),
static_cast<unsigned long>(sizeof(PersistedPayload)));
return false;
}
PersistedPayload payload{};
if (file.read(&payload, sizeof(payload)) != sizeof(payload))
{
file.close();
s_last_load_status = StoreStatus::ReadFailed;
Serial.printf("[gat562][settings] payload read failed\n");
return false;
}
file.close();
const uint32_t actual_crc = crc32(reinterpret_cast<const uint8_t*>(&payload), sizeof(payload));
if (actual_crc != header.crc32)
{
s_last_load_status = StoreStatus::CrcMismatch;
(void)quarantineCorruptFile(s_last_load_status);
Serial.printf("[gat562][settings] crc mismatch got=0x%08lX expected=0x%08lX\n",
static_cast<unsigned long>(actual_crc),
static_cast<unsigned long>(header.crc32));
return false;
}
s_cache.config = payload.config;
normalizeConfig(s_cache.config);
s_cache.tone_volume = clampToneVolume(payload.tone_volume);
s_last_load_status = StoreStatus::Ok;
Serial.printf("[gat562][settings] load ok tone=%u ble=%u proto=%u\n",
static_cast<unsigned>(s_cache.tone_volume),
static_cast<unsigned>(s_cache.config.ble_enabled ? 1 : 0),
static_cast<unsigned>(s_cache.config.mesh_protocol));
return true;
}
bool saveToFs()
{
if (!ensureFs())
{
s_last_save_status = StoreStatus::FsInitFailed;
return false;
}
removeIfExists(kSettingsTempPath);
auto file = InternalFS.open(kSettingsTempPath, FILE_O_WRITE);
if (!file)
{
s_last_save_status = StoreStatus::OpenFailed;
Serial.printf("[gat562][settings] temp open failed path=%s\n", kSettingsTempPath);
return false;
}
PersistedPayload payload{};
payload.config = s_cache.config;
payload.tone_volume = clampToneVolume(s_cache.tone_volume);
FileHeader header{};
header.magic = kSettingsMagic;
header.version = kSettingsVersion;
header.payload_size = sizeof(payload);
header.crc32 = crc32(reinterpret_cast<const uint8_t*>(&payload), sizeof(payload));
const size_t header_written = file.write(reinterpret_cast<const uint8_t*>(&header), sizeof(header));
const size_t payload_written = header_written == sizeof(header)
? file.write(reinterpret_cast<const uint8_t*>(&payload), sizeof(payload))
: 0U;
file.flush();
const bool write_ok = (header_written == sizeof(header) && payload_written == sizeof(payload));
file.close();
if (!write_ok)
{
removeIfExists(kSettingsTempPath);
s_last_save_status = StoreStatus::WriteFailed;
Serial.printf("[gat562][settings] write failed header=%lu payload=%lu\n",
static_cast<unsigned long>(header_written),
static_cast<unsigned long>(payload_written));
return false;
}
removeIfExists(kSettingsBackupPath);
if (InternalFS.exists(kSettingsPath) && !InternalFS.rename(kSettingsPath, kSettingsBackupPath))
{
removeIfExists(kSettingsTempPath);
s_last_save_status = StoreStatus::BackupFailed;
Serial.printf("[gat562][settings] backup rename failed main=%s backup=%s\n",
kSettingsPath,
kSettingsBackupPath);
return false;
}
if (!InternalFS.rename(kSettingsTempPath, kSettingsPath))
{
if (InternalFS.exists(kSettingsBackupPath))
{
(void)InternalFS.rename(kSettingsBackupPath, kSettingsPath);
}
removeIfExists(kSettingsTempPath);
s_last_save_status = StoreStatus::RenameFailed;
Serial.printf("[gat562][settings] commit rename failed temp=%s main=%s\n",
kSettingsTempPath,
kSettingsPath);
return false;
}
removeIfExists(kSettingsBackupPath);
s_last_save_status = StoreStatus::Ok;
Serial.printf("[gat562][settings] save ok tone=%u ble=%u proto=%u\n",
static_cast<unsigned>(payload.tone_volume),
static_cast<unsigned>(payload.config.ble_enabled ? 1 : 0),
static_cast<unsigned>(payload.config.mesh_protocol));
return true;
}
void ensureCacheLoaded()
{
if (s_cache_loaded)
{
return;
}
resetCacheToDefaults();
(void)loadFromFs();
s_cache_loaded = true;
}
} // namespace
void normalizeConfig(app::AppConfig& config)
@@ -60,38 +400,56 @@ void normalizeConfig(app::AppConfig& config)
{
config.meshcore_config.meshcore_region_preset = 0;
}
if (config.gps_interval_ms == 0)
{
config.gps_interval_ms = 60000UL;
}
}
bool loadAppConfig(app::AppConfig& config)
{
if (s_has_config)
{
config = s_config;
normalizeConfig(config);
return true;
}
ensureCacheLoaded();
config = s_cache.config;
normalizeConfig(config);
return false;
s_cache.config = config;
return s_last_load_status == StoreStatus::Ok;
}
bool saveAppConfig(const app::AppConfig& config)
{
s_config = config;
normalizeConfig(s_config);
s_has_config = true;
return true;
ensureCacheLoaded();
s_cache.config = config;
normalizeConfig(s_cache.config);
return saveToFs();
}
uint8_t loadMessageToneVolume()
{
return s_tone_volume;
ensureCacheLoaded();
return s_cache.tone_volume;
}
bool saveMessageToneVolume(uint8_t volume)
{
s_tone_volume = volume;
return true;
ensureCacheLoaded();
s_cache.tone_volume = clampToneVolume(volume);
return saveToFs();
}
StoreStatus lastLoadStatus()
{
return s_last_load_status;
}
StoreStatus lastSaveStatus()
{
return s_last_save_status;
}
const char* statusLabel(StoreStatus status)
{
return statusText(status);
}
} // namespace boards::gat562_mesh_evb_pro::settings_store
@@ -1,6 +1,7 @@
#pragma once
#include "app/app_facades.h"
#include "app/app_config.h"
#include "chat/domain/contact_types.h"
#include "chat/usecase/chat_service.h"
#include "platform/ui/device_runtime.h"
@@ -44,10 +45,19 @@ enum class InputAction : uint8_t
struct HostCallbacks
{
struct ResourceUsage
{
bool available = false;
uint32_t used_bytes = 0;
uint32_t total_bytes = 0;
};
app::IAppFacade* app = nullptr;
// Platform-owned mono UI font. NRF is expected to provide Fusion Pixel 8px
// here so ASCII and CJK share one renderer and one asset boundary.
// Platform-owned mono UI font. NRF is expected to provide the selected
// Fusion Pixel mono asset here so the renderer stays decoupled from a
// specific platform font size.
const MonoFont* ui_font = nullptr;
const MonoFont* accent_font = nullptr;
uint32_t (*millis_fn)() = nullptr;
time_t (*utc_now_fn)() = nullptr;
int (*timezone_offset_min_fn)() = nullptr;
@@ -58,6 +68,8 @@ struct HostCallbacks
platform::ui::gps::GpsState (*gps_data_fn)() = nullptr;
bool (*gps_enabled_fn)() = nullptr;
bool (*gps_powered_fn)() = nullptr;
ResourceUsage (*ram_usage_fn)() = nullptr;
ResourceUsage (*flash_usage_fn)() = nullptr;
void (*debug_log_fn)(const char* text) = nullptr;
};
@@ -145,6 +157,7 @@ class Runtime : public chat::ChatService::IncomingTextObserver
void renderInfoPage();
void renderGnssPage();
void renderActionPage();
void renderSettingPopup();
void enterPage(Page page);
void openCompose(EditTarget target, const char* seed_text = nullptr);
@@ -160,6 +173,11 @@ class Runtime : public chat::ChatService::IncomingTextObserver
void ensureSleepTimeout(InputAction action);
void adjustRadioSetting(int delta);
void adjustDeviceSetting(int delta);
void beginSettingPopup(Page owner, size_t index);
void cancelSettingPopup();
void confirmSettingPopup();
void adjustSettingPopup(int delta);
void formatSettingPopupValue(char* out, size_t out_len) const;
void adjustComposeSelection(int delta);
void adjustComposeCandidate(int delta);
void adjustComposeAction(int delta);
@@ -191,6 +209,7 @@ class Runtime : public chat::ChatService::IncomingTextObserver
MonoDisplay& display_;
TextRenderer text_renderer_;
TextRenderer accent_text_renderer_;
HostCallbacks host_{};
bool initialized_ = false;
Page page_ = Page::BootLog;
@@ -208,6 +227,12 @@ class Runtime : public chat::ChatService::IncomingTextObserver
size_t settings_menu_index_ = 0;
size_t radio_index_ = 0;
size_t device_index_ = 0;
bool setting_popup_active_ = false;
Page setting_popup_owner_ = Page::SettingsMenu;
size_t setting_popup_index_ = 0;
app::AppConfig setting_popup_config_{};
bool setting_popup_ble_enabled_ = false;
int setting_popup_timezone_min_ = 0;
size_t info_scroll_ = 0;
size_t action_index_ = 0;
size_t chat_list_index_ = 0;
+820 -32
View File
@@ -429,6 +429,304 @@ const char* bearingCardinal(double bearing_deg)
return kDirs[index];
}
void drawFrame(MonoDisplay& display, int x, int y, int w, int h, bool filled = false)
{
if (w <= 0 || h <= 0)
{
return;
}
if (filled)
{
display.fillRect(x, y, w, h, true);
return;
}
display.fillRect(x, y, w, 1, true);
display.fillRect(x, y + h - 1, w, 1, true);
display.fillRect(x, y, 1, h, true);
display.fillRect(x + w - 1, y, 1, h, true);
}
void drawBatteryIcon(MonoDisplay& display, int x, int y, int level, bool charging)
{
drawFrame(display, x, y, 14, 7);
display.fillRect(x + 14, y + 2, 2, 3, true);
const int fill_w = clampValue((level < 0 ? 0 : level), 0, 100) / 25;
for (int i = 0; i < fill_w; ++i)
{
display.fillRect(x + 2 + i * 3, y + 2, 2, 3, true);
}
if (charging)
{
display.fillRect(x + 6, y + 1, 1, 5, true);
display.drawPixel(x + 7, y + 2, true);
display.drawPixel(x + 5, y + 4, true);
}
}
void drawMessageIcon(MonoDisplay& display, int x, int y, bool active)
{
drawFrame(display, x, y + 1, 10, 7);
display.drawPixel(x + 1, y + 2, true);
display.drawPixel(x + 2, y + 3, true);
display.drawPixel(x + 3, y + 4, true);
display.drawPixel(x + 4, y + 5, true);
display.drawPixel(x + 8, y + 2, true);
display.drawPixel(x + 7, y + 3, true);
display.drawPixel(x + 6, y + 4, true);
display.drawPixel(x + 5, y + 5, true);
if (active)
{
display.fillRect(x + 11, y, 2, 2, true);
}
}
void formatToggleLabel(char* out, size_t out_len, const char* name, bool enabled)
{
if (!out || out_len == 0)
{
return;
}
std::snprintf(out, out_len, "%s %s", name ? name : "--", enabled ? "ON" : "OFF");
}
void drawClockSegmentH(MonoDisplay& display, int x, int y, int w)
{
if (w < 6)
{
display.fillRect(x, y, w, 2, true);
return;
}
display.fillRect(x + 1, y, w - 2, 1, true);
display.fillRect(x, y + 1, w, 1, true);
display.drawPixel(x + 1, y + 2, true);
display.drawPixel(x + w - 2, y + 2, true);
}
void drawClockSegmentV(MonoDisplay& display, int x, int y, int h)
{
if (h < 6)
{
display.fillRect(x, y, 2, h, true);
return;
}
display.drawPixel(x + 1, y, true);
display.fillRect(x, y + 1, 2, h - 2, true);
display.drawPixel(x, y + h - 1, true);
}
void drawClockDigit(MonoDisplay& display, int x, int y, char ch)
{
if (ch == ':')
{
display.fillRect(x + 1, y + 5, 2, 2, true);
display.fillRect(x + 1, y + 13, 2, 2, true);
return;
}
if (ch < '0' || ch > '9')
{
return;
}
static constexpr uint8_t kDigitSegments[] = {
0b1111110, // 0
0b0110000, // 1
0b1101101, // 2
0b1111001, // 3
0b0110011, // 4
0b1011011, // 5
0b1011111, // 6
0b1110000, // 7
0b1111111, // 8
0b1111011, // 9
};
const uint8_t seg = kDigitSegments[ch - '0'];
constexpr int kDigitW = 8;
constexpr int kDigitH = 15;
constexpr int kMidY = 6;
constexpr int kBottomY = kDigitH - 3;
constexpr int kLeftX = 0;
constexpr int kRightX = kDigitW - 2;
constexpr int kSegW = 6;
constexpr int kSegH = 4;
if (seg & 0b1000000)
{
drawClockSegmentH(display, x + 1, y, kSegW);
}
if (seg & 0b0100000)
{
drawClockSegmentV(display, x + kRightX, y + 1, kSegH);
}
if (seg & 0b0010000)
{
drawClockSegmentV(display, x + kRightX, y + 8, kSegH);
}
if (seg & 0b0001000)
{
drawClockSegmentH(display, x + 1, y + kBottomY, kSegW);
}
if (seg & 0b0000100)
{
drawClockSegmentV(display, x + kLeftX, y + 8, kSegH);
}
if (seg & 0b0000010)
{
drawClockSegmentV(display, x + kLeftX, y + 1, kSegH);
}
if (seg & 0b0000001)
{
drawClockSegmentH(display, x + 1, y + kMidY, kSegW);
}
}
void splitClockText(const char* time_text, char* main_out, size_t main_len, char* sec_out, size_t sec_len)
{
if (main_out && main_len > 0)
{
main_out[0] = '\0';
}
if (sec_out && sec_len > 0)
{
sec_out[0] = '\0';
}
if (!time_text || !main_out || main_len == 0)
{
return;
}
if (std::strlen(time_text) >= 5)
{
std::snprintf(main_out, main_len, "%.5s", time_text);
}
else
{
std::snprintf(main_out, main_len, "%s", time_text);
}
if (sec_out && sec_len > 0 && std::strlen(time_text) >= 8)
{
std::snprintf(sec_out, sec_len, "%.2s", time_text + 6);
}
}
int clockGlyphWidth(char ch)
{
return ch == ':' ? 2 : 8;
}
int measureClockText(const char* text)
{
if (!text || *text == '\0')
{
return 0;
}
int width = 0;
for (const char* p = text; *p != '\0'; ++p)
{
if (p != text)
{
width += 2;
}
width += clockGlyphWidth(*p);
}
return width;
}
void drawClockText(MonoDisplay& display, int x, int y, const char* text)
{
if (!text)
{
return;
}
int cursor_x = x;
for (const char* p = text; *p != '\0'; ++p)
{
drawClockDigit(display, cursor_x, y, *p);
cursor_x += clockGlyphWidth(*p) + 2;
}
}
const char* signalRatingLabel(float snr, float rssi)
{
if (snr >= 9.0f || rssi >= -90.0f)
{
return "STR";
}
if (snr >= 4.0f || rssi >= -102.0f)
{
return "OK";
}
if (snr > -2.0f || rssi >= -112.0f)
{
return "WEAK";
}
return "POOR";
}
void formatElapsedShort(time_t now_s, uint32_t then_s, char* out, size_t out_len)
{
if (!out || out_len == 0)
{
return;
}
out[0] = '\0';
if (then_s == 0 || now_s < static_cast<time_t>(1700000000) || now_s < static_cast<time_t>(then_s))
{
std::snprintf(out, out_len, "-");
return;
}
uint32_t delta = static_cast<uint32_t>(now_s - static_cast<time_t>(then_s));
if (delta < 60U)
{
std::snprintf(out, out_len, "%lus", static_cast<unsigned long>(delta));
}
else if (delta < 3600U)
{
std::snprintf(out, out_len, "%lum", static_cast<unsigned long>(delta / 60U));
}
else if (delta < 86400U)
{
std::snprintf(out, out_len, "%luh", static_cast<unsigned long>(delta / 3600U));
}
else
{
std::snprintf(out, out_len, "%lud", static_cast<unsigned long>(delta / 86400U));
}
}
void formatDistanceShort(double meters, char* out, size_t out_len)
{
if (!out || out_len == 0)
{
return;
}
out[0] = '\0';
if (!(meters >= 0.0))
{
std::snprintf(out, out_len, "-");
return;
}
if (meters < 1000.0)
{
std::snprintf(out, out_len, "%.0fm", meters);
}
else if (meters < 10000.0)
{
std::snprintf(out, out_len, "%.1fk", meters / 1000.0);
}
else
{
std::snprintf(out, out_len, "%.0fk", meters / 1000.0);
}
}
const char* gnssFixLabel(::gps::GnssFix fix)
{
switch (fix)
@@ -647,6 +945,8 @@ const char* blePairingModeLabel(const ble::BlePairingStatus& status)
Runtime::Runtime(MonoDisplay& display, const HostCallbacks& host)
: display_(display),
text_renderer_(host.ui_font ? *host.ui_font : builtin_ui_font()),
accent_text_renderer_(host.accent_font ? *host.accent_font
: (host.ui_font ? *host.ui_font : builtin_ui_font())),
host_(host)
{
}
@@ -787,6 +1087,27 @@ void Runtime::handleInput(InputAction action)
last_interaction_ms_ = nowMs();
}
if (setting_popup_active_)
{
if (action == InputAction::Back)
{
cancelSettingPopup();
}
else if (action == InputAction::Select || action == InputAction::Primary)
{
confirmSettingPopup();
}
else if (action == InputAction::Up || action == InputAction::Right)
{
adjustSettingPopup(1);
}
else if (action == InputAction::Down || action == InputAction::Left)
{
adjustSettingPopup(-1);
}
return;
}
switch (page_)
{
case Page::MainMenu:
@@ -1185,19 +1506,11 @@ void Runtime::handleInput(InputAction action)
{
++radio_index_;
}
else if (action == InputAction::Left)
{
adjustRadioSetting(-1);
}
else if (action == InputAction::Right)
{
adjustRadioSetting(1);
}
else if (action == InputAction::Back)
else if (action == InputAction::Left || action == InputAction::Back)
{
enterPage(Page::SettingsMenu);
}
else if (action == InputAction::Select || action == InputAction::Primary)
else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary)
{
if (app()->getConfig().mesh_protocol == chat::MeshProtocol::MeshCore &&
radio_index_ + 1 == radioItemCount(app()->getConfig().mesh_protocol))
@@ -1206,7 +1519,7 @@ void Runtime::handleInput(InputAction action)
}
else
{
adjustRadioSetting(1);
beginSettingPopup(Page::RadioSettings, radio_index_);
}
}
break;
@@ -1220,17 +1533,13 @@ void Runtime::handleInput(InputAction action)
{
++device_index_;
}
else if (action == InputAction::Left)
else if (action == InputAction::Left || action == InputAction::Back)
{
adjustDeviceSetting(-1);
enterPage(Page::SettingsMenu);
}
else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary)
{
adjustDeviceSetting(1);
}
else if (action == InputAction::Back)
{
enterPage(Page::SettingsMenu);
beginSettingPopup(Page::DeviceSettings, device_index_);
}
break;
@@ -1357,6 +1666,11 @@ void Runtime::render()
break;
}
if (setting_popup_active_)
{
renderSettingPopup();
}
ble::BlePairingStatus ble_status{};
if (loadBlePairingStatus(app(), &ble_status) &&
ble_status.requires_passkey &&
@@ -1392,31 +1706,114 @@ void Runtime::renderScreensaver()
char protocol[8] = {};
char freq[20] = {};
char time_buf[16] = {};
char time_main_buf[8] = {};
char time_sec_buf[4] = {};
char date_buf[24] = {};
char status_buf[16] = {};
char node_buf[12] = {};
char tz_buf[16] = {};
char unread_buf[8] = {};
char bat_pct_buf[8] = {};
char sat_buf[16] = {};
char top_left_buf[32] = {};
char top_right_buf[32] = {};
char left_toggle_buf[12] = {};
char right_toggle_buf[12] = {};
formatProtocol(protocol, sizeof(protocol));
formatNodeLabel(node_buf, sizeof(node_buf));
formatTime(time_buf, sizeof(time_buf), date_buf, sizeof(date_buf));
splitClockText(time_buf, time_main_buf, sizeof(time_main_buf), time_sec_buf, sizeof(time_sec_buf));
formatTimezoneLabel(host_.timezone_offset_min_fn ? host_.timezone_offset_min_fn() : 0, tz_buf, sizeof(tz_buf));
if (host_.format_frequency_fn)
{
host_.format_frequency_fn(host_.active_lora_frequency_hz_fn ? host_.active_lora_frequency_hz_fn() : 0U,
freq,
sizeof(freq));
}
formatTime(time_buf, sizeof(time_buf), date_buf, sizeof(date_buf));
drawTitleBar(protocol, freq[0] != '\0' ? freq : nullptr);
const auto battery = host_.battery_info_fn ? host_.battery_info_fn() : platform::ui::device::BatteryInfo{};
const auto gps = host_.gps_data_fn ? host_.gps_data_fn() : platform::ui::gps::GpsState{};
const int unread = app() ? app()->getChatService().getTotalUnread() : 0;
const bool gps_enabled = host_.gps_enabled_fn && host_.gps_enabled_fn();
const bool ble_enabled = app() && app()->isBleEnabled();
std::snprintf(unread_buf, sizeof(unread_buf), unread > 99 ? "99+" : "%d", unread);
if (battery.available && battery.level >= 0)
{
std::snprintf(bat_pct_buf, sizeof(bat_pct_buf), "BAT:%d%%", battery.level);
}
else
{
std::snprintf(bat_pct_buf, sizeof(bat_pct_buf), "BAT:--");
}
if (gps.satellites > 0)
{
std::snprintf(sat_buf, sizeof(sat_buf), "SAT %u", static_cast<unsigned>(gps.satellites));
}
else if (gps_enabled)
{
std::snprintf(sat_buf, sizeof(sat_buf), "SAT --");
}
else
{
std::snprintf(sat_buf, sizeof(sat_buf), "SAT OFF");
}
const bool clock_unsynced = std::strcmp(date_buf, "TIME UNSYNC") == 0;
if (clock_unsynced)
{
std::snprintf(status_buf, sizeof(status_buf), "CLOCK UNSYNC");
}
else
{
std::snprintf(status_buf, sizeof(status_buf), "%s", date_buf);
}
std::snprintf(top_left_buf, sizeof(top_left_buf), "%s %s", protocol[0] ? protocol : "--", bat_pct_buf);
std::snprintf(top_right_buf, sizeof(top_right_buf), "%s", freq[0] ? freq : "--");
formatToggleLabel(left_toggle_buf, sizeof(left_toggle_buf), "GPS", gps_enabled);
formatToggleLabel(right_toggle_buf, sizeof(right_toggle_buf), "BLE", ble_enabled);
const int time_w = text_renderer_.measureTextWidth(time_buf);
constexpr int kTopY = 1;
constexpr int kTopDetailY = 9;
constexpr int kTimeY = 22;
constexpr int kSideToggleY = 26;
constexpr int kSecY = 28;
constexpr int kDateY = 42;
constexpr int kFooterY = 55;
drawTextClipped(2, kTopY, 60, top_left_buf);
const int top_right_w = text_renderer_.measureTextWidth(top_right_buf);
text_renderer_.drawText(display_, std::max(66, display_.width() - top_right_w - 2), kTopY, top_right_buf);
drawTextClipped(2, kTopDetailY, 42, sat_buf);
char top_detail_right[16] = {};
std::snprintf(top_detail_right, sizeof(top_detail_right), "MSG %s", unread_buf);
const int top_detail_right_w = text_renderer_.measureTextWidth(top_detail_right);
text_renderer_.drawText(display_, std::max(70, display_.width() - top_detail_right_w - 2), kTopDetailY, top_detail_right);
const int time_w = measureClockText(time_main_buf);
const int time_x = std::max(0, (display_.width() - time_w) / 2);
text_renderer_.drawText(display_, time_x, 18, time_buf);
drawClockText(display_, time_x, kTimeY, time_main_buf);
if (time_sec_buf[0] != '\0')
{
const int sec_x = std::min(display_.width() - 12, time_x + time_w + 4);
text_renderer_.drawText(display_, sec_x, kSecY, time_sec_buf);
}
drawTextClipped(0, kSideToggleY, 28, left_toggle_buf);
const int right_toggle_w = text_renderer_.measureTextWidth(right_toggle_buf);
text_renderer_.drawText(display_, std::max(96, display_.width() - right_toggle_w), kSideToggleY, right_toggle_buf);
const int status_w = text_renderer_.measureTextWidth(status_buf);
text_renderer_.drawText(display_, std::max(0, (display_.width() - status_w) / 2), kDateY, status_buf);
const int date_w = text_renderer_.measureTextWidth(date_buf);
const int date_x = std::max(0, (display_.width() - date_w) / 2);
text_renderer_.drawText(display_, date_x, 34, date_buf);
char footer_left[24] = {};
char footer_right[24] = {};
std::snprintf(footer_left, sizeof(footer_left), "ID %s", node_buf[0] ? node_buf : "--");
std::snprintf(footer_right, sizeof(footer_right), "%s", tz_buf[0] ? tz_buf : "UTC+0");
drawTextClipped(3, kFooterY, 76, footer_left);
const int right_w = text_renderer_.measureTextWidth(footer_right);
text_renderer_.drawText(display_, std::max(80, display_.width() - right_w - 3), kFooterY, footer_right);
const int node_w = text_renderer_.measureTextWidth(node_buf);
const int node_x = std::max(0, (display_.width() - node_w) / 2);
text_renderer_.drawText(display_, node_x, 50, node_buf);
if (battery.available && battery.level >= 0 && battery.level <= 20)
{
display_.fillRect(display_.width() - 10, kFooterY + text_renderer_.lineHeight() - 2, 7, 2, true);
}
}
void Runtime::renderSleep()
@@ -1462,10 +1859,9 @@ void Runtime::renderNodeList()
return;
}
const int line_h = text_renderer_.lineHeight();
constexpr size_t kNodeAliasMax = 8;
const size_t selected = std::min(node_list_index_, node_count_ - 1U);
const size_t visible = std::min(node_count_, static_cast<size_t>(6));
const size_t visible = std::min(node_count_, static_cast<size_t>(3));
size_t start = 0;
if (node_count_ > visible)
{
@@ -1480,6 +1876,7 @@ void Runtime::renderNodeList()
{
const size_t node_index = start + i;
const auto& node = nodes_[node_index];
const int card_y = 10 + static_cast<int>(i * 18);
char node_id[8] = {};
std::snprintf(node_id, sizeof(node_id), "%04lX",
static_cast<unsigned long>(node.node_id & 0xFFFFUL));
@@ -1490,7 +1887,7 @@ void Runtime::renderNodeList()
alias[kNodeAliasMax] = '\0';
}
char line[40] = {};
char line[24] = {};
if (alias[0] == '\0' || equalsIgnoreCase(alias, node_id))
{
std::snprintf(line, sizeof(line), "%s", node_id);
@@ -1499,7 +1896,54 @@ void Runtime::renderNodeList()
{
std::snprintf(line, sizeof(line), "%s %s", node_id, alias);
}
drawTextClipped(0, 10 + static_cast<int>(i * line_h), display_.width(), line, node_index == selected);
if (node_index == selected)
{
drawFrame(display_, 0, card_y - 1, display_.width(), 17);
}
drawTextClipped(2, card_y, 90, line, false);
char age_buf[8] = {};
const time_t now_s = host_.utc_now_fn ? host_.utc_now_fn() : 0;
formatElapsedShort(now_s, node.last_seen, age_buf, sizeof(age_buf));
char dist_buf[12] = {};
const auto gps = host_.gps_data_fn ? host_.gps_data_fn() : platform::ui::gps::GpsState{};
if (gps.valid && node.position.valid)
{
const double node_lat = static_cast<double>(node.position.latitude_i) / 1e7;
const double node_lon = static_cast<double>(node.position.longitude_i) / 1e7;
formatDistanceShort(haversineMeters(gps.lat, gps.lng, node_lat, node_lon), dist_buf, sizeof(dist_buf));
}
else
{
std::snprintf(dist_buf, sizeof(dist_buf), "-");
}
char subline[32] = {};
std::snprintf(subline, sizeof(subline), "%s %s %s",
age_buf,
dist_buf,
signalRatingLabel(node.snr, node.rssi));
drawTextClipped(6, card_y + 8, 78, subline, false);
const char* sig = signalRatingLabel(node.snr, node.rssi);
const int bars = std::strcmp(sig, "STR") == 0 ? 4 : std::strcmp(sig, "OK") == 0 ? 3 : std::strcmp(sig, "WEAK") == 0 ? 2
: 1;
for (int bar = 0; bar < 4; ++bar)
{
const int bar_h = 2 + bar * 2;
const int bar_x = 108 + bar * 4;
const int bar_y = card_y + 14 - bar_h;
if (bar < bars)
{
display_.fillRect(bar_x, bar_y, 3, bar_h, true);
}
else
{
drawFrame(display_, bar_x, bar_y, 3, bar_h);
}
}
}
}
@@ -1908,6 +2352,52 @@ void Runtime::renderDeviceSettings()
}
}
void Runtime::renderSettingPopup()
{
char title[24] = {};
char value[32] = {};
const bool is_radio = setting_popup_owner_ == Page::RadioSettings;
const bool is_device = setting_popup_owner_ == Page::DeviceSettings;
if (!is_radio && !is_device)
{
return;
}
if (is_radio)
{
const auto protocol = setting_popup_config_.mesh_protocol;
const auto* items = (protocol == chat::MeshProtocol::Meshtastic) ? kMeshtasticRadioItems : kMeshCoreRadioItems;
const size_t count = radioItemCount(protocol);
if (setting_popup_index_ >= count)
{
return;
}
std::snprintf(title, sizeof(title), "%s", items[setting_popup_index_]);
}
else
{
if (setting_popup_index_ >= arrayCount(kDeviceItems))
{
return;
}
std::snprintf(title, sizeof(title), "%s", kDeviceItems[setting_popup_index_]);
}
formatSettingPopupValue(value, sizeof(value));
constexpr int kBoxX = 8;
constexpr int kBoxY = 18;
constexpr int kBoxW = 112;
constexpr int kBoxH = 28;
display_.fillRect(kBoxX, kBoxY, kBoxW, kBoxH, false);
drawFrame(display_, kBoxX, kBoxY, kBoxW, kBoxH);
const int title_w = text_renderer_.measureTextWidth(title);
text_renderer_.drawText(display_, kBoxX + std::max(2, (kBoxW - title_w) / 2), kBoxY + 3, title);
const int value_w = text_renderer_.measureTextWidth(value);
text_renderer_.drawText(display_, kBoxX + std::max(2, (kBoxW - value_w) / 2), kBoxY + 13, value);
drawTextClipped(kBoxX + 2, kBoxY + 21, kBoxW - 4, "ADJ ARROWS SEL OK BACK ESC");
}
void Runtime::renderInfoPage()
{
drawTitleBar("INFO", nullptr);
@@ -1937,6 +2427,8 @@ void Runtime::renderInfoPage()
const auto& cfg = app()->getConfig();
const auto battery = host_.battery_info_fn ? host_.battery_info_fn() : platform::ui::device::BatteryInfo{};
const auto gps = host_.gps_data_fn ? host_.gps_data_fn() : platform::ui::gps::GpsState{};
const auto ram = host_.ram_usage_fn ? host_.ram_usage_fn() : HostCallbacks::ResourceUsage{};
const auto flash = host_.flash_usage_fn ? host_.flash_usage_fn() : HostCallbacks::ResourceUsage{};
ble::BlePairingStatus ble_status{};
const bool has_ble_status = loadBlePairingStatus(app(), &ble_status);
@@ -2023,6 +2515,20 @@ void Runtime::renderInfoPage()
formatTime(time_buf, sizeof(time_buf), date_buf, sizeof(date_buf));
push_kv("TIME", time_buf[0] ? time_buf : "-");
push_kv("DATE", date_buf[0] ? date_buf : "-");
if (ram.available && ram.total_bytes > 0)
{
std::snprintf(value, sizeof(value), "%lu/%luK",
static_cast<unsigned long>(ram.used_bytes / 1024U),
static_cast<unsigned long>(ram.total_bytes / 1024U));
push_kv("SRAM", value);
}
if (flash.available && flash.total_bytes > 0)
{
std::snprintf(value, sizeof(value), "%lu/%luK",
static_cast<unsigned long>(flash.used_bytes / 1024U),
static_cast<unsigned long>(flash.total_bytes / 1024U));
push_kv("FLASH", value);
}
push_line("[GPS]");
push_kv("GPS", cfg.gps_mode != 0 ? "ON" : "OFF");
@@ -2434,6 +2940,9 @@ void Runtime::buildNodeInfo()
push_kv("RS", value);
formatTimestamp(value, sizeof(value), node.last_seen);
push_kv("SEEN", value[0] != '\0' ? value : "-");
formatElapsedShort(host_.utc_now_fn ? host_.utc_now_fn() : 0, node.last_seen, value, sizeof(value));
push_kv("AGO", value);
push_kv("SIG", signalRatingLabel(node.snr, node.rssi));
push_section("POS");
if (node.position.valid)
@@ -2928,6 +3437,285 @@ void Runtime::adjustDeviceSetting(int delta)
}
}
void Runtime::beginSettingPopup(Page owner, size_t index)
{
if (!app())
{
return;
}
setting_popup_active_ = true;
setting_popup_owner_ = owner;
setting_popup_index_ = index;
setting_popup_config_ = app()->getConfig();
setting_popup_ble_enabled_ = app()->isBleEnabled();
setting_popup_timezone_min_ = host_.timezone_offset_min_fn ? host_.timezone_offset_min_fn() : 0;
}
void Runtime::cancelSettingPopup()
{
setting_popup_active_ = false;
}
void Runtime::confirmSettingPopup()
{
if (!setting_popup_active_ || !app())
{
return;
}
auto& cfg = app()->getConfig();
cfg = setting_popup_config_;
cfg.ble_enabled = setting_popup_ble_enabled_;
if (host_.set_timezone_offset_min_fn)
{
host_.set_timezone_offset_min_fn(setting_popup_timezone_min_);
}
app()->setBleEnabled(setting_popup_ble_enabled_);
app()->saveConfig();
char value[32] = {};
formatSettingPopupValue(value, sizeof(value));
appendStatus(this, "%s", value);
setting_popup_active_ = false;
}
void Runtime::adjustSettingPopup(int delta)
{
if (!setting_popup_active_ || !app() || delta == 0)
{
return;
}
if (setting_popup_owner_ == Page::RadioSettings)
{
auto& cfg = setting_popup_config_;
switch (setting_popup_index_)
{
case 0:
cfg.mesh_protocol = (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic)
? chat::MeshProtocol::MeshCore
: chat::MeshProtocol::Meshtastic;
break;
case 1:
if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic)
{
size_t count = 0;
const auto* table = chat::meshtastic::getRegionTable(&count);
if (count > 0)
{
size_t index = 0;
for (size_t i = 0; i < count; ++i)
{
if (table[i].code ==
static_cast<meshtastic_Config_LoRaConfig_RegionCode>(cfg.meshtastic_config.region))
{
index = i;
break;
}
}
index = static_cast<size_t>(
clampValue<int>(static_cast<int>(index) + delta, 0, static_cast<int>(count) - 1));
cfg.meshtastic_config.region = static_cast<uint8_t>(table[index].code);
}
}
else
{
size_t count = 0;
const auto* table = chat::meshcore::getRegionPresetTable(&count);
if (count > 0)
{
int index = -1;
for (size_t i = 0; i < count; ++i)
{
if (table[i].id == cfg.meshcore_config.meshcore_region_preset)
{
index = static_cast<int>(i);
break;
}
}
index = clampValue(index + delta, 0, static_cast<int>(count) - 1);
cfg.meshcore_config.meshcore_region_preset = table[index].id;
}
}
break;
case 2:
cfg.activeMeshConfig().tx_power = static_cast<int8_t>(clampValue<int>(
static_cast<int>(cfg.activeMeshConfig().tx_power) + delta,
static_cast<int>(app::AppConfig::kTxPowerMinDbm),
static_cast<int>(app::AppConfig::kTxPowerMaxDbm)));
break;
case 3:
if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic)
{
constexpr int kPresetMin = static_cast<int>(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST);
constexpr int kPresetMax = static_cast<int>(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO);
cfg.meshtastic_config.modem_preset = static_cast<uint8_t>(clampValue(
static_cast<int>(cfg.meshtastic_config.modem_preset) + delta, kPresetMin, kPresetMax));
cfg.meshtastic_config.use_preset = true;
}
else
{
size_t count = 0;
const auto* table = chat::meshcore::getRegionPresetTable(&count);
if (count > 0)
{
int index = -1;
for (size_t i = 0; i < count; ++i)
{
if (table[i].id == cfg.meshcore_config.meshcore_region_preset)
{
index = static_cast<int>(i);
break;
}
}
index = clampValue(index + delta, 0, static_cast<int>(count) - 1);
cfg.meshcore_config.meshcore_region_preset = table[index].id;
cfg.meshcore_config.meshcore_freq_mhz = table[index].freq_mhz;
cfg.meshcore_config.meshcore_bw_khz = table[index].bw_khz;
cfg.meshcore_config.meshcore_sf = table[index].sf;
cfg.meshcore_config.meshcore_cr = table[index].cr;
}
}
break;
case 4:
if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic)
{
cfg.meshtastic_config.channel_num = static_cast<uint16_t>(clampValue<int>(
static_cast<int>(cfg.meshtastic_config.channel_num) + delta, 0, 255));
}
else
{
cfg.meshcore_config.meshcore_channel_slot = static_cast<uint8_t>(clampValue<int>(
static_cast<int>(cfg.meshcore_config.meshcore_channel_slot) + delta, 0, 15));
}
break;
case 5:
setEncryptEnabled(cfg, !encryptEnabled(cfg));
break;
default:
break;
}
return;
}
if (setting_popup_owner_ == Page::DeviceSettings)
{
switch (setting_popup_index_)
{
case 0:
setting_popup_ble_enabled_ = !setting_popup_ble_enabled_;
break;
case 1:
setting_popup_config_.gps_mode = (setting_popup_config_.gps_mode == 0) ? 1 : 0;
break;
case 2:
setting_popup_config_.gps_sat_mask = nextGpsSatMask(setting_popup_config_.gps_sat_mask, delta);
break;
case 3:
{
static constexpr uint32_t kGpsIntervals[] = {15000UL, 30000UL, 60000UL, 300000UL, 600000UL};
size_t index = 0;
while (index + 1 < arrayCount(kGpsIntervals) &&
kGpsIntervals[index] < setting_popup_config_.gps_interval_ms)
{
++index;
}
const int next = clampValue<int>(static_cast<int>(index) + delta, 0, static_cast<int>(arrayCount(kGpsIntervals)) - 1);
setting_popup_config_.gps_interval_ms = kGpsIntervals[next];
break;
}
case 4:
setting_popup_timezone_min_ = clampValue(setting_popup_timezone_min_ + delta * kTimezoneStep,
kTimezoneMin,
kTimezoneMax);
break;
default:
break;
}
}
}
void Runtime::formatSettingPopupValue(char* out, size_t out_len) const
{
if (!out || out_len == 0)
{
return;
}
out[0] = '\0';
if (setting_popup_owner_ == Page::RadioSettings)
{
const auto& cfg = setting_popup_config_;
switch (setting_popup_index_)
{
case 0:
std::snprintf(out, out_len, "%s", protocolShortLabel(cfg.mesh_protocol));
return;
case 1:
std::snprintf(out, out_len, "%s", radioRegionLabel(cfg));
return;
case 2:
std::snprintf(out, out_len, "%ddBm", static_cast<int>(cfg.activeMeshConfig().tx_power));
return;
case 3:
if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic)
{
std::snprintf(out, out_len, "%s",
chat::meshtastic::presetDisplayName(
static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(cfg.meshtastic_config.modem_preset)));
}
else
{
std::snprintf(out, out_len, "%s", radioRegionLabel(cfg));
}
return;
case 4:
if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic)
{
std::snprintf(out, out_len, "CH %u", static_cast<unsigned>(cfg.meshtastic_config.channel_num));
}
else
{
std::snprintf(out, out_len, "SLOT %u", static_cast<unsigned>(cfg.meshcore_config.meshcore_channel_slot));
}
return;
case 5:
std::snprintf(out, out_len, "%s", encryptEnabled(cfg) ? "ON" : "OFF");
return;
default:
break;
}
}
if (setting_popup_owner_ == Page::DeviceSettings)
{
switch (setting_popup_index_)
{
case 0:
std::snprintf(out, out_len, "%s", setting_popup_ble_enabled_ ? "ON" : "OFF");
return;
case 1:
std::snprintf(out, out_len, "%s", setting_popup_config_.gps_mode != 0 ? "ON" : "OFF");
return;
case 2:
std::snprintf(out, out_len, "%s", gpsSatMaskLabel(setting_popup_config_.gps_sat_mask));
return;
case 3:
std::snprintf(out, out_len, "%lus", static_cast<unsigned long>(setting_popup_config_.gps_interval_ms / 1000UL));
return;
case 4:
{
char tz_label[16] = {};
formatTimezoneLabel(setting_popup_timezone_min_, tz_label, sizeof(tz_label));
std::snprintf(out, out_len, "%s", tz_label);
return;
}
default:
break;
}
}
}
void Runtime::adjustComposeSelection(int delta)
{
if (usesSmartCompose())
+12 -3
View File
@@ -37,6 +37,11 @@ struct GlyphView
uint8_t advance;
};
uint8_t glyphRowBytes(uint8_t width)
{
return static_cast<uint8_t>((width + 7U) / 8U);
}
size_t utf8SequenceLength(unsigned char lead)
{
if ((lead & 0x80U) == 0)
@@ -177,7 +182,9 @@ GlyphView resolveGlyph(const MonoFont& font, uint16_t glyph_index)
const bool is_compact16 = (font.codepoints16 != nullptr) && (font.glyphs == nullptr);
if (is_compact16)
{
const uint32_t offset = static_cast<uint32_t>(glyph_index) * static_cast<uint32_t>(font.glyph_height);
const uint32_t offset = static_cast<uint32_t>(glyph_index) *
static_cast<uint32_t>(font.glyph_height) *
static_cast<uint32_t>(glyphRowBytes(font.glyph_width));
const uint8_t advance = font.advances ? font.advances[glyph_index] : font.fixed_advance;
return GlyphView{
font.bitmap + offset,
@@ -219,10 +226,12 @@ void drawGlyphBitmap(MonoDisplay& display, const MonoFont& font, int x, int y, c
for (int row = 0; row < glyph.height; ++row)
{
const uint8_t bits = glyph.bitmap[row];
const uint8_t row_bytes = glyphRowBytes(glyph.width);
const uint8_t* row_ptr = glyph.bitmap + static_cast<size_t>(row) * row_bytes;
for (int col = 0; col < glyph.width; ++col)
{
const bool on = ((bits >> (7 - col)) & 0x01U) != 0;
const uint8_t byte_value = row_ptr[col / 8];
const bool on = ((byte_value >> (7 - (col % 8))) & 0x01U) != 0;
if (!on)
{
continue;
@@ -4,6 +4,7 @@
#include "chat/ports/i_mesh_adapter.h"
#include "chat/runtime/self_identity_policy.h"
#include "chat/runtime/self_identity_provider.h"
#include "chat/usecase/contact_service.h"
#include "meshtastic/mesh.pb.h"
#include "platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h"
@@ -35,7 +36,8 @@ class MeshtasticRadioAdapter final : public ::chat::IMeshAdapter
};
explicit MeshtasticRadioAdapter(const ::chat::runtime::SelfIdentityProvider* identity_provider = nullptr,
NodeStore* node_store = nullptr);
NodeStore* node_store = nullptr,
::chat::contacts::ContactService* contact_service = nullptr);
::chat::MeshCapabilities getCapabilities() const override;
bool sendText(::chat::ChannelId channel, const std::string& text,
@@ -178,6 +180,7 @@ class MeshtasticRadioAdapter final : public ::chat::IMeshAdapter
std::string short_name_;
const ::chat::runtime::SelfIdentityProvider* identity_provider_ = nullptr;
NodeStore* node_store_ = nullptr;
::chat::contacts::ContactService* contact_service_ = nullptr;
float last_rx_rssi_ = 0.0f;
float last_rx_snr_ = 0.0f;
std::queue<::chat::MeshIncomingText> text_queue_;
@@ -0,0 +1,10 @@
#pragma once
#include "ui/mono_128x64/font/mono_font.h"
namespace platform::nrf52::ui::fonts
{
const ::ui::mono_128x64::MonoFont& fusion_pixel_10_font();
} // namespace platform::nrf52::ui::fonts
@@ -0,0 +1,10 @@
#pragma once
#include "ui/mono_128x64/font/mono_font.h"
namespace ui::mono_128x64
{
extern const MonoFont kFusionPixel10Font;
} // namespace ui::mono_128x64
@@ -10,6 +10,7 @@
#include "meshtastic/mqtt.pb.h"
#include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h"
#include "platform/nrf52/arduino_common/device_identity.h"
#include "sys/clock.h"
#include <Arduino.h>
@@ -193,13 +194,20 @@ const uint8_t* selectKeyByHash(const ::chat::MeshConfig& config,
return nullptr;
}
uint32_t nowSeconds()
{
return sys::epoch_seconds_now();
}
} // namespace
MeshtasticRadioAdapter::MeshtasticRadioAdapter(const ::chat::runtime::SelfIdentityProvider* identity_provider,
NodeStore* node_store)
NodeStore* node_store,
::chat::contacts::ContactService* contact_service)
: node_id_(device_identity::getSelfNodeId()),
identity_provider_(identity_provider),
node_store_(node_store)
node_store_(node_store),
contact_service_(contact_service)
{
randomSeed(static_cast<unsigned long>(NRF_FICR->DEVICEADDR[0] ^ NRF_FICR->DEVICEADDR[1] ^ micros()));
next_packet_id_ = static_cast<::chat::MessageId>(random(1, 0x7FFFFFFF));
@@ -572,7 +580,7 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size)
{
::chat::RxMeta implicit_rx{};
implicit_rx.rx_timestamp_ms = millis();
implicit_rx.rx_timestamp_s = static_cast<uint32_t>(std::time(nullptr));
implicit_rx.rx_timestamp_s = nowSeconds();
implicit_rx.time_source = ::chat::RxTimeSource::DeviceUtc;
implicit_rx.origin = ::chat::RxOrigin::Mesh;
implicit_rx.channel_hash = header.channel;
@@ -601,7 +609,7 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size)
::chat::RxMeta rx_meta{};
rx_meta.rx_timestamp_ms = millis();
rx_meta.rx_timestamp_s = static_cast<uint32_t>(std::time(nullptr));
rx_meta.rx_timestamp_s = nowSeconds();
rx_meta.time_source = ::chat::RxTimeSource::DeviceUtc;
rx_meta.origin = ::chat::RxOrigin::Mesh;
rx_meta.direct = (::chat::meshtastic::computeHopsAway(header.flags) == 0);
@@ -669,7 +677,7 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size)
const uint8_t hops_away =
node.has_hops_away ? node.hops_away : ::chat::meshtastic::computeHopsAway(header.flags);
node_store_->upsert(node_id, short_name, long_name,
static_cast<uint32_t>(std::time(nullptr)),
nowSeconds(),
snr, last_rx_rssi_,
static_cast<uint8_t>(::chat::contacts::NodeProtocolType::Meshtastic),
role, hops_away,
@@ -688,7 +696,7 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size)
role = static_cast<uint8_t>(user.role);
}
node_store_->upsert(header.from, user.short_name, user.long_name,
static_cast<uint32_t>(std::time(nullptr)),
nowSeconds(),
last_rx_snr_, last_rx_rssi_,
static_cast<uint8_t>(::chat::contacts::NodeProtocolType::Meshtastic),
role, ::chat::meshtastic::computeHopsAway(header.flags),
@@ -698,6 +706,32 @@ void MeshtasticRadioAdapter::handleRawPacket(const uint8_t* data, size_t size)
}
}
if (decoded_ok &&
decoded.portnum == meshtastic_PortNum_POSITION_APP &&
decoded.payload.size > 0 &&
contact_service_)
{
meshtastic_Position position_pb = meshtastic_Position_init_zero;
pb_istream_t pstream = pb_istream_from_buffer(decoded.payload.bytes, decoded.payload.size);
if (pb_decode(&pstream, meshtastic_Position_fields, &position_pb) &&
::chat::meshtastic::hasValidPosition(position_pb))
{
::chat::contacts::NodePosition pos{};
pos.valid = true;
pos.latitude_i = position_pb.latitude_i;
pos.longitude_i = position_pb.longitude_i;
pos.has_altitude = position_pb.has_altitude;
pos.altitude = position_pb.altitude;
pos.timestamp = position_pb.timestamp != 0 ? position_pb.timestamp : position_pb.time;
pos.precision_bits = position_pb.precision_bits;
pos.pdop = position_pb.PDOP;
pos.hdop = position_pb.HDOP;
pos.vdop = position_pb.VDOP;
pos.gps_accuracy_mm = position_pb.gps_accuracy;
contact_service_->updateNodePosition(header.from, pos);
}
}
if (want_ack_flag && to_us && decoded_ok)
{
(void)sendRoutingAck(header.from, header.id, header.channel, channel, key, key_len, 0);
@@ -807,7 +841,7 @@ void MeshtasticRadioAdapter::processSendQueue()
}
if (node_store_ && pending.dest != 0 && pending.dest != kBroadcastNode)
{
node_store_->setNextHop(pending.dest, 0, static_cast<uint32_t>(std::time(nullptr)));
node_store_->setNextHop(pending.dest, 0, nowSeconds());
}
it = pending_retransmits_.erase(it);
continue;
@@ -819,7 +853,7 @@ void MeshtasticRadioAdapter::processSendQueue()
pending.fallback_sent = true;
if (node_store_ && pending.dest != 0 && pending.dest != kBroadcastNode)
{
node_store_->setNextHop(pending.dest, 0, static_cast<uint32_t>(std::time(nullptr)));
node_store_->setNextHop(pending.dest, 0, nowSeconds());
}
}
@@ -1141,7 +1175,7 @@ void MeshtasticRadioAdapter::emitRoutingResult(uint32_t request_id, meshtastic_R
else
{
incoming.rx_meta.rx_timestamp_ms = millis();
incoming.rx_meta.rx_timestamp_s = static_cast<uint32_t>(std::time(nullptr));
incoming.rx_meta.rx_timestamp_s = nowSeconds();
incoming.rx_meta.time_source = ::chat::RxTimeSource::DeviceUtc;
incoming.rx_meta.origin = ::chat::RxOrigin::Mesh;
incoming.rx_meta.channel_hash = channel_hash;
@@ -1175,7 +1209,7 @@ void MeshtasticRadioAdapter::learnNextHop(::chat::NodeId dest, uint8_t next_hop)
{
return;
}
(void)node_store_->setNextHop(dest, next_hop, static_cast<uint32_t>(std::time(nullptr)));
(void)node_store_->setNextHop(dest, next_hop, nowSeconds());
}
MeshtasticRadioAdapter::PacketHistoryEntry* MeshtasticRadioAdapter::findHistory(::chat::NodeId sender, ::chat::MessageId packet_id)
@@ -1384,7 +1418,7 @@ void MeshtasticRadioAdapter::updateNodeLastSeen(::chat::NodeId node_id, uint8_t
{
return;
}
node_store_->upsert(node_id, "", "", static_cast<uint32_t>(std::time(nullptr)),
node_store_->upsert(node_id, "", "", nowSeconds(),
last_rx_snr_, last_rx_rssi_,
static_cast<uint8_t>(::chat::contacts::NodeProtocolType::Meshtastic),
::chat::contacts::kNodeRoleUnknown,
@@ -1439,7 +1473,7 @@ void MeshtasticRadioAdapter::handleRoutingPacket(const ::chat::meshtastic::Packe
{
if (node_store_)
{
node_store_->setNextHop(header.from, 0, static_cast<uint32_t>(std::time(nullptr)));
node_store_->setNextHop(header.from, 0, nowSeconds());
}
}
@@ -1,5 +1,8 @@
#include "platform/ui/settings_store.h"
#include <InternalFileSystem.h>
#include <cstdint>
#include <cstring>
#include <map>
#include <string>
@@ -8,6 +11,38 @@
namespace
{
using Adafruit_LittleFS_Namespace::FILE_O_READ;
using Adafruit_LittleFS_Namespace::FILE_O_WRITE;
constexpr const char* kSettingsPath = "/ui_settings.bin";
constexpr const char* kSettingsTempPath = "/ui_settings.bin.tmp";
constexpr uint32_t kMagic = 0x55535447UL; // USTG
constexpr uint16_t kVersion = 1;
enum class ValueType : uint8_t
{
Int = 1,
Bool = 2,
Uint = 3,
Blob = 4,
};
struct FileHeader
{
uint32_t magic = kMagic;
uint16_t version = kVersion;
uint16_t reserved = 0;
uint32_t record_count = 0;
} __attribute__((packed));
struct RecordHeader
{
uint8_t type = 0;
uint8_t reserved = 0;
uint16_t key_len = 0;
uint32_t value_len = 0;
} __attribute__((packed));
std::string makeScopedKey(const char* ns, const char* key)
{
const char* scope = ns ? ns : "";
@@ -39,6 +74,224 @@ std::map<std::string, std::vector<uint8_t>>& blobStore()
return store;
}
bool& loadedFlag()
{
static bool loaded = false;
return loaded;
}
void clearAllStores()
{
intStore().clear();
boolStore().clear();
uintStore().clear();
blobStore().clear();
}
bool ensureFs()
{
return InternalFS.begin();
}
template <typename T>
bool writePod(Adafruit_LittleFS_Namespace::File& file, const T& value)
{
return file.write(reinterpret_cast<const uint8_t*>(&value), sizeof(T)) == sizeof(T);
}
template <typename T>
bool readPod(Adafruit_LittleFS_Namespace::File& file, T* value)
{
return value && file.read(value, sizeof(T)) == sizeof(T);
}
bool writeRecordHeader(Adafruit_LittleFS_Namespace::File& file,
ValueType type,
const std::string& key,
uint32_t value_len)
{
RecordHeader header{};
header.type = static_cast<uint8_t>(type);
header.key_len = static_cast<uint16_t>(key.size());
header.value_len = value_len;
return writePod(file, header) &&
(key.empty() || file.write(key.data(), key.size()) == key.size());
}
bool saveToFs()
{
if (!ensureFs())
{
return false;
}
auto file = InternalFS.open(kSettingsTempPath, FILE_O_WRITE);
if (!file)
{
return false;
}
const uint32_t record_count = static_cast<uint32_t>(
intStore().size() + boolStore().size() + uintStore().size() + blobStore().size());
FileHeader header{};
header.record_count = record_count;
if (!writePod(file, header))
{
file.close();
return false;
}
for (const auto& entry : intStore())
{
const int32_t value = entry.second;
if (!writeRecordHeader(file, ValueType::Int, entry.first, sizeof(value)) ||
file.write(reinterpret_cast<const uint8_t*>(&value), sizeof(value)) != sizeof(value))
{
file.close();
return false;
}
}
for (const auto& entry : boolStore())
{
const uint8_t value = entry.second ? 1U : 0U;
if (!writeRecordHeader(file, ValueType::Bool, entry.first, sizeof(value)) ||
file.write(&value, sizeof(value)) != sizeof(value))
{
file.close();
return false;
}
}
for (const auto& entry : uintStore())
{
const uint32_t value = entry.second;
if (!writeRecordHeader(file, ValueType::Uint, entry.first, sizeof(value)) ||
file.write(reinterpret_cast<const uint8_t*>(&value), sizeof(value)) != sizeof(value))
{
file.close();
return false;
}
}
for (const auto& entry : blobStore())
{
if (!writeRecordHeader(file, ValueType::Blob, entry.first, static_cast<uint32_t>(entry.second.size())) ||
(!entry.second.empty() && file.write(entry.second.data(), entry.second.size()) != entry.second.size()))
{
file.close();
return false;
}
}
file.flush();
file.close();
if (InternalFS.exists(kSettingsPath))
{
InternalFS.remove(kSettingsPath);
}
return InternalFS.rename(kSettingsTempPath, kSettingsPath);
}
void ensureLoaded()
{
if (loadedFlag())
{
return;
}
loadedFlag() = true;
clearAllStores();
if (!ensureFs() || !InternalFS.exists(kSettingsPath))
{
return;
}
auto file = InternalFS.open(kSettingsPath, FILE_O_READ);
if (!file)
{
return;
}
FileHeader header{};
if (!readPod(file, &header) || header.magic != kMagic || header.version != kVersion)
{
file.close();
return;
}
for (uint32_t i = 0; i < header.record_count; ++i)
{
RecordHeader rec{};
if (!readPod(file, &rec))
{
break;
}
std::string key(rec.key_len, '\0');
if (rec.key_len > 0 && file.read(&key[0], rec.key_len) != rec.key_len)
{
break;
}
switch (static_cast<ValueType>(rec.type))
{
case ValueType::Int:
{
int32_t value = 0;
if (rec.value_len != sizeof(value) || !readPod(file, &value))
{
return;
}
intStore()[key] = value;
break;
}
case ValueType::Bool:
{
uint8_t value = 0;
if (rec.value_len != sizeof(value) || !readPod(file, &value))
{
return;
}
boolStore()[key] = (value != 0);
break;
}
case ValueType::Uint:
{
uint32_t value = 0;
if (rec.value_len != sizeof(value) || !readPod(file, &value))
{
return;
}
uintStore()[key] = value;
break;
}
case ValueType::Blob:
{
std::vector<uint8_t> value(rec.value_len, 0);
if (rec.value_len > 0 && file.read(value.data(), rec.value_len) != rec.value_len)
{
return;
}
blobStore()[key] = value;
break;
}
default:
{
std::vector<uint8_t> skip(rec.value_len, 0);
if (rec.value_len > 0 && file.read(skip.data(), rec.value_len) != rec.value_len)
{
return;
}
break;
}
}
}
file.close();
}
} // namespace
namespace platform::ui::settings_store
@@ -50,7 +303,9 @@ void put_int(const char* ns, const char* key, int value)
{
return;
}
ensureLoaded();
intStore()[makeScopedKey(ns, key)] = value;
(void)saveToFs();
}
void put_bool(const char* ns, const char* key, bool value)
@@ -59,7 +314,9 @@ void put_bool(const char* ns, const char* key, bool value)
{
return;
}
ensureLoaded();
boolStore()[makeScopedKey(ns, key)] = value;
(void)saveToFs();
}
void put_uint(const char* ns, const char* key, uint32_t value)
@@ -68,7 +325,9 @@ void put_uint(const char* ns, const char* key, uint32_t value)
{
return;
}
ensureLoaded();
uintStore()[makeScopedKey(ns, key)] = value;
(void)saveToFs();
}
bool put_blob(const char* ns, const char* key, const void* data, std::size_t len)
@@ -77,9 +336,10 @@ bool put_blob(const char* ns, const char* key, const void* data, std::size_t len
{
return false;
}
ensureLoaded();
auto& blob = blobStore()[makeScopedKey(ns, key)];
blob.assign(static_cast<const uint8_t*>(data), static_cast<const uint8_t*>(data) + len);
return true;
return saveToFs();
}
int get_int(const char* ns, const char* key, int default_value)
@@ -88,6 +348,7 @@ int get_int(const char* ns, const char* key, int default_value)
{
return default_value;
}
ensureLoaded();
const auto it = intStore().find(makeScopedKey(ns, key));
return it == intStore().end() ? default_value : it->second;
}
@@ -98,6 +359,7 @@ bool get_bool(const char* ns, const char* key, bool default_value)
{
return default_value;
}
ensureLoaded();
const auto it = boolStore().find(makeScopedKey(ns, key));
return it == boolStore().end() ? default_value : it->second;
}
@@ -108,6 +370,7 @@ uint32_t get_uint(const char* ns, const char* key, uint32_t default_value)
{
return default_value;
}
ensureLoaded();
const auto it = uintStore().find(makeScopedKey(ns, key));
return it == uintStore().end() ? default_value : it->second;
}
@@ -119,6 +382,7 @@ bool get_blob(const char* ns, const char* key, std::vector<uint8_t>& out)
{
return false;
}
ensureLoaded();
const auto it = blobStore().find(makeScopedKey(ns, key));
if (it == blobStore().end())
{
@@ -134,6 +398,7 @@ void remove_keys(const char* ns, const char* const* keys, std::size_t key_count)
{
return;
}
ensureLoaded();
for (std::size_t index = 0; index < key_count; ++index)
{
if (!keys[index])
@@ -146,10 +411,12 @@ void remove_keys(const char* ns, const char* const* keys, std::size_t key_count)
uintStore().erase(scoped);
blobStore().erase(scoped);
}
(void)saveToFs();
}
void clear_namespace(const char* ns)
{
ensureLoaded();
const std::string prefix = std::string(ns ? ns : "") + ":";
for (auto it = intStore().begin(); it != intStore().end();)
@@ -168,6 +435,7 @@ void clear_namespace(const char* ns)
{
it = (it->first.rfind(prefix, 0) == 0) ? blobStore().erase(it) : std::next(it);
}
(void)saveToFs();
}
} // namespace platform::ui::settings_store
@@ -0,0 +1,26 @@
#ifndef TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED
#define TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED 0
#endif
#if TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED
#include "ui/fonts/fusion_pixel_10_font.h"
namespace ui::mono_128x64
{
extern const MonoFont kFusionPixel10Font;
} // namespace ui::mono_128x64
namespace platform::nrf52::ui::fonts
{
const ::ui::mono_128x64::MonoFont& fusion_pixel_10_font()
{
return ::ui::mono_128x64::kFusionPixel10Font;
}
} // namespace platform::nrf52::ui::fonts
#endif
@@ -0,0 +1,173 @@
#ifndef TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED
#define TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED 0
#endif
#if TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED
#include "ui/fonts/fusion_pixel_10_font_generated.h"
namespace ui::mono_128x64
{
static const uint8_t kFusionPixel10Bitmap[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xD0, 0x00, 0xF0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xF0, 0x00, 0xD0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xF0, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00,
0xE0, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0x10, 0x00, 0x20, 0x00,
0x20, 0x00, 0xC0, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xD0, 0x00,
0x20, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x20, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00,
0xF0, 0x00, 0x20, 0x00, 0x20, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00,
0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x10, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x00, 0xC0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x20, 0x00,
0x20, 0x00, 0x20, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x10, 0x00,
0x10, 0x00, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xE0, 0x00, 0x10, 0x00, 0x20, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x30, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xF0, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0x10, 0x00,
0x10, 0x00, 0x10, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00,
0xF0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xF0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xD0, 0x00, 0x20, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xF0, 0x00,
0xF0, 0x00, 0x10, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xC0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x20, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0x00,
0x00, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00,
0x20, 0x00, 0x10, 0x00, 0x10, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0xD0, 0x00, 0x10, 0x00, 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xD0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xC0, 0x00,
0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xF0, 0x00,
0xF0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xD0, 0x00,
0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x30, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x00, 0xF0, 0x00, 0xC0, 0x00,
0xC0, 0x00, 0xC0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0xC0, 0x00,
0xF0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x30, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x30, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xF0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x20, 0x00, 0x20, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00,
0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0xD0, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00,
0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x20, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xC0, 0x00,
0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0x20, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x30, 0x00, 0xC0, 0x00, 0x20, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0xD0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x20, 0x00,
0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x10, 0x00,
0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x30, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x30, 0x00,
0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x10, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xD0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00,
0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x30, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x30, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xE0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x20, 0x00,
0xF0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x30, 0x00, 0x10, 0x00, 0xE0, 0x00,
0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x20, 0x00,
0x20, 0x00, 0x20, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
0xF0, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0x10, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00,
0xC0, 0x00, 0xC0, 0x00, 0xD0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xF0, 0x00,
0xF0, 0x00, 0xF0, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0x20, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0x30, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xD0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0x30, 0x00, 0xE0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0xF0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xD0, 0x00, 0xF0, 0x00, 0xF0, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0x20, 0x00, 0x20, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD0, 0x00, 0xD0, 0x00,
0xD0, 0x00, 0x30, 0x00, 0x10, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xF0, 0x00, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x00, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x10, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00,
0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x20, 0x00, 0x20, 0x00, 0x10, 0x00,
0x10, 0x00, 0x20, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xE0, 0x00, 0x30, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static const uint16_t kFusionPixel10Codepoints[] = {
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E,
};
static const uint8_t kFusionPixel10Advances[] = {
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05,
};
const MonoFont kFusionPixel10Font = MonoFont::makeCompact16(
kFusionPixel10Bitmap,
kFusionPixel10Codepoints,
kFusionPixel10Advances,
static_cast<uint16_t>(95),
10,
9,
10,
static_cast<uint16_t>(31),
10,
10,
10);
} // namespace ui::mono_128x64
#endif
@@ -1,3 +1,9 @@
#ifndef TRAIL_MATE_NRF_MONO_FUSION_PIXEL_8_ENABLED
#define TRAIL_MATE_NRF_MONO_FUSION_PIXEL_8_ENABLED 1
#endif
#if TRAIL_MATE_NRF_MONO_FUSION_PIXEL_8_ENABLED
#include "ui/fonts/fusion_pixel_8_font.h"
namespace ui::mono_128x64
@@ -18,3 +24,5 @@ const ::ui::mono_128x64::MonoFont& fusion_pixel_8_font()
}
} // namespace platform::nrf52::ui::fonts
#endif
@@ -1,3 +1,9 @@
#ifndef TRAIL_MATE_NRF_MONO_FUSION_PIXEL_8_ENABLED
#define TRAIL_MATE_NRF_MONO_FUSION_PIXEL_8_ENABLED 1
#endif
#if TRAIL_MATE_NRF_MONO_FUSION_PIXEL_8_ENABLED
#include "ui/fonts/fusion_pixel_8_font_generated.h"
namespace ui::mono_128x64
@@ -71356,3 +71362,5 @@ const MonoFont kFusionPixel8Font = MonoFont::makeCompact16(
8);
} // namespace ui::mono_128x64
#endif
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import math
import re
from pathlib import Path
ARRAY_RE = re.compile(
r"static const uint(8|16)_t (kFusionPixel8(?:Bitmap|Codepoints|Advances))\[\] = \{\s*(.*?)\s*\};",
re.S,
)
COUNT_RE = re.compile(r"static_cast<uint16_t>\((\d+)\)")
def parse_array(body: str) -> list[int]:
return [int(token, 0) for token in re.findall(r"0x[0-9A-Fa-f]+|\d+", body)]
def parse_source(path: Path) -> tuple[list[int], list[int], list[int], int]:
text = path.read_text(encoding="utf-8")
found: dict[str, list[int]] = {}
for _bits, name, body in ARRAY_RE.findall(text):
found[name] = parse_array(body)
counts = [int(v) for v in COUNT_RE.findall(text)]
if len(counts) < 2:
raise RuntimeError("unable to parse glyph count/fallback index from source")
glyph_count = counts[0]
bitmap = found["kFusionPixel8Bitmap"]
codepoints = found["kFusionPixel8Codepoints"]
advances = found["kFusionPixel8Advances"]
return bitmap, codepoints, advances, glyph_count
def ascii_charset() -> list[int]:
return list(range(0x20, 0x7F))
def filter_charset(bitmap: list[int],
codepoints: list[int],
advances: list[int],
glyph_count: int,
allowed_codepoints: set[int]) -> tuple[list[int], list[int], list[int], int]:
filtered_bitmap: list[int] = []
filtered_codepoints: list[int] = []
filtered_advances: list[int] = []
for glyph_index in range(glyph_count):
codepoint = codepoints[glyph_index]
if codepoint not in allowed_codepoints:
continue
start = glyph_index * 8
filtered_bitmap.extend(bitmap[start:start + 8])
filtered_codepoints.append(codepoint)
filtered_advances.append(advances[glyph_index])
return filtered_bitmap, filtered_codepoints, filtered_advances, len(filtered_codepoints)
def upscale_glyph(rows8: list[int], dest_size: int = 10) -> list[int]:
rows10: list[int] = []
for y in range(dest_size):
src_y = min(7, (y * 8) // dest_size)
src_row = rows8[src_y]
dest_row = 0
for x in range(dest_size):
src_x = min(7, (x * 8) // dest_size)
bit = (src_row >> (7 - src_x)) & 0x01
if bit:
dest_row |= 1 << (15 - x)
rows10.append(dest_row)
return rows10
def scale_advance(adv: int, dest_size: int = 10) -> int:
return max(1, min(dest_size, int(math.floor((adv * dest_size / 8.0) + 0.5))))
def format_bytes(data: list[int], per_line: int = 16) -> str:
toks = [f"0x{value:02X}" for value in data]
lines = []
for idx in range(0, len(toks), per_line):
lines.append(" " + ", ".join(toks[idx:idx + per_line]) + ",")
return "\n".join(lines)
def format_words(data: list[int], per_line: int = 8) -> str:
toks = [f"0x{value:04X}" for value in data]
lines = []
for idx in range(0, len(toks), per_line):
lines.append(" " + ", ".join(toks[idx:idx + per_line]) + ",")
return "\n".join(lines)
def write_source(path: Path, bitmap8: list[int], codepoints: list[int], advances8: list[int], glyph_count: int) -> None:
bitmap10: list[int] = []
advances10 = [scale_advance(value) for value in advances8]
for glyph_index in range(glyph_count):
rows8 = bitmap8[glyph_index * 8:(glyph_index + 1) * 8]
rows10 = upscale_glyph(rows8)
for row in rows10:
bitmap10.append((row >> 8) & 0xFF)
bitmap10.append(row & 0xFF)
fallback_index = codepoints.index(ord("?")) if ord("?") in codepoints else 0
text = f"""#ifndef TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED
#define TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED 0
#endif
#if TRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED
#include "ui/fonts/fusion_pixel_10_font_generated.h"
namespace ui::mono_128x64
{{
static const uint8_t kFusionPixel10Bitmap[] = {{
{format_bytes(bitmap10)}
}};
static const uint16_t kFusionPixel10Codepoints[] = {{
{format_words(codepoints)}
}};
static const uint8_t kFusionPixel10Advances[] = {{
{format_bytes(advances10)}
}};
const MonoFont kFusionPixel10Font = MonoFont::makeCompact16(
kFusionPixel10Bitmap,
kFusionPixel10Codepoints,
kFusionPixel10Advances,
static_cast<uint16_t>({glyph_count}),
10,
9,
10,
static_cast<uint16_t>({fallback_index}),
10,
10,
10);
}} // namespace ui::mono_128x64
#endif
"""
path.write_text(text, encoding="utf-8")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--source8", required=True)
ap.add_argument("--out-source", required=True)
ap.add_argument("--ascii-only", action="store_true")
args = ap.parse_args()
bitmap, codepoints, advances, glyph_count = parse_source(Path(args.source8))
if args.ascii_only:
bitmap, codepoints, advances, glyph_count = filter_charset(bitmap,
codepoints,
advances,
glyph_count,
set(ascii_charset()))
if ord("?") not in codepoints:
raise RuntimeError("ASCII subset must include '?' fallback glyph")
write_source(Path(args.out_source), bitmap, codepoints, advances, glyph_count)
if __name__ == "__main__":
main()
@@ -15,6 +15,8 @@ build_flags =
-DGAT562_NO_SD=1
-DGAT562_NO_CJK=1
-DGAT562_NO_PINYIN_IME=1
-DTRAIL_MATE_NRF_MONO_FUSION_PIXEL_8_ENABLED=1
-DTRAIL_MATE_NRF_MONO_FUSION_PIXEL_10_ENABLED=0
-DCONFIG_NFCT_PINS_AS_GPIOS=1
-DUI_SHARED_TOUCH_IME_ENABLED=0
-DSCREEN_WIDTH=128