mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-07-11 14:18:52 +00:00
Refine gat562 mono UI and persist UI settings
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
-1
@@ -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
|
||||
+46
-12
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user