diff --git a/apps/esp_pio/src/app_context.cpp b/apps/esp_pio/src/app_context.cpp index 55929cd6..a6aa240c 100644 --- a/apps/esp_pio/src/app_context.cpp +++ b/apps/esp_pio/src/app_context.cpp @@ -10,6 +10,7 @@ #include "board/LoraBoard.h" #include "board/MotionBoard.h" #include "chat/infra/mesh_protocol_utils.h" +#include "chat/runtime/self_identity_policy.h" #include "sys/event_bus.h" #ifdef USING_ST25R3916 #endif @@ -99,9 +100,18 @@ void AppContext::initChatRuntime(bool use_mock_adapter) void AppContext::initTeamServices() { - if (!mesh_router_ || !platform_bindings_.create_team_services) + if (!mesh_router_) { - Serial.printf("[Team] platform bindings missing\n"); + Serial.printf("[Team] mesh router unavailable, skip team services\n"); + return; + } + + if (!platform_bindings_.create_team_services) + { + if (platform_bindings_.set_team_mode_active) + { + platform_bindings_.set_team_mode_active(false); + } return; } @@ -219,7 +229,7 @@ void AppContext::applyPrivacyConfig() bool AppContext::isBleEnabled() const { - return ble_manager_ && ble_manager_->isEnabled(); + return config_.ble_enabled; } bool AppContext::init(BoardBase& board, LoraBoard* lora_board, GpsBoard* gps_board, MotionBoard* motion_board, @@ -320,41 +330,21 @@ void AppContext::getEffectiveUserInfo(char* out_long, size_t long_len, return; } - out_long[0] = '\0'; - out_short[0] = '\0'; + chat::runtime::SelfIdentityInput input{}; + input.node_id = getSelfNodeId(); + input.configured_long_name = config_.node_name; + input.configured_short_name = config_.short_name; + input.fallback_long_prefix = "lilygo"; + input.fallback_ble_prefix = "TrailMate"; + input.allow_short_hex_fallback = true; - const char* cfg_long = config_.node_name; - const char* cfg_short = config_.short_name; - uint16_t suffix = static_cast(getSelfNodeId() & 0x0ffff); + chat::runtime::EffectiveSelfIdentity identity{}; + (void)chat::runtime::resolveEffectiveSelfIdentity(input, &identity); - if (cfg_long && cfg_long[0] != '\0') - { - strncpy(out_long, cfg_long, long_len - 1); - out_long[long_len - 1] = '\0'; - } - else - { - snprintf(out_long, long_len, "lilygo-%04X", suffix); - } - - if (cfg_short && cfg_short[0] != '\0') - { - size_t copy_len = strlen(cfg_short); - if (copy_len > 4) - { - copy_len = 4; - } - if (copy_len > short_len - 1) - { - copy_len = short_len - 1; - } - memcpy(out_short, cfg_short, copy_len); - out_short[copy_len] = '\0'; - } - else - { - snprintf(out_short, short_len, "%04X", suffix); - } + strncpy(out_long, identity.long_name, long_len - 1); + out_long[long_len - 1] = '\0'; + strncpy(out_short, identity.short_name, short_len - 1); + out_short[short_len - 1] = '\0'; } void AppContext::updateCoreServices() @@ -411,10 +401,12 @@ void AppContext::attachBleManager(std::unique_ptr ble_manager) void AppContext::setBleEnabled(bool enabled) { + config_.ble_enabled = enabled; if (ble_manager_) { ble_manager_->setEnabled(enabled); } + saveConfig(); } chat::NodeId AppContext::getSelfNodeId() const diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/app_facade_runtime.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/app_facade_runtime.h new file mode 100644 index 00000000..9a42ee2a --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/app_facade_runtime.h @@ -0,0 +1,122 @@ +#pragma once + +#include "app/app_config.h" +#include "app/app_facades.h" +#include "chat/runtime/self_identity_policy.h" +#include "chat/runtime/self_identity_provider.h" + +#include + +namespace chat +{ +class ChatModel; +class ChatService; +class IChatStore; +class IMeshAdapter; +namespace contacts +{ +class INodeStore; +class IContactStore; +class ContactService; +} // namespace contacts +} + +namespace platform::nrf52::arduino_common +{ +class SelfIdentityBridge; +} + +namespace boards::gat562_mesh_evb_pro +{ +class Gat562Board; +} + +namespace apps::gat562_mesh_evb_pro +{ + +class AppFacadeRuntime final : public app::IAppBleFacade +{ + public: + static AppFacadeRuntime& instance(); + ~AppFacadeRuntime(); + + bool initialize(); + bool isInitialized() const; + + bool installMeshBackend(chat::MeshProtocol protocol, + std::unique_ptr backend); + + app::AppConfig& getConfig() override; + const app::AppConfig& getConfig() const override; + void saveConfig() override; + void applyMeshConfig() override; + void applyUserInfo() override; + void applyPositionConfig() override; + void applyNetworkLimits() override; + void applyPrivacyConfig() override; + void applyChatDefaults() override; + void getEffectiveUserInfo(char* out_long, std::size_t long_len, + char* out_short, std::size_t short_len) const override; + bool switchMeshProtocol(chat::MeshProtocol protocol, bool persist = true) override; + + chat::ChatService& getChatService() override; + chat::contacts::ContactService& getContactService() override; + chat::IMeshAdapter* getMeshAdapter() override; + const chat::IMeshAdapter* getMeshAdapter() const override; + chat::NodeId getSelfNodeId() const override; + + team::TeamController* getTeamController() override; + team::TeamPairingService* getTeamPairing() override; + team::TeamService* getTeamService() override; + const team::TeamService* getTeamService() const override; + team::TeamTrackSampler* getTeamTrackSampler() override; + void setTeamModeActive(bool active) override; + + void broadcastNodeInfo() override; + void clearNodeDb() override; + void clearMessageDb() override; + + ble::BleManager* getBleManager() override; + const ble::BleManager* getBleManager() const override; + bool isBleEnabled() const override; + void setBleEnabled(bool enabled) override; + chat::contacts::INodeStore* getNodeStore() override; + const chat::contacts::INodeStore* getNodeStore() const override; + void resetMeshConfig() override; + chat::ui::IChatUiRuntime* getChatUiRuntime() override; + void setChatUiRuntime(chat::ui::IChatUiRuntime* runtime) override; + BoardBase* getBoard() override; + const BoardBase* getBoard() const override; + + void updateCoreServices() override; + void tickEventRuntime() override; + void dispatchPendingEvents(std::size_t max_events = 32) override; + + const chat::runtime::EffectiveSelfIdentity& effectiveIdentity() const; + + private: + AppFacadeRuntime(); + + void initializeStores(); + void initializeChatRuntime(); + void refreshEffectiveIdentity(); + const chat::runtime::SelfIdentityProvider* identityProvider() const; + + bool initialized_ = false; + app::AppConfig config_{}; + std::unique_ptr identity_bridge_; + mutable chat::runtime::EffectiveSelfIdentity effective_identity_{}; + + std::unique_ptr node_store_; + std::unique_ptr contact_store_; + std::unique_ptr contact_service_; + std::unique_ptr chat_model_; + std::unique_ptr chat_store_; + std::unique_ptr mesh_router_; + std::unique_ptr chat_service_; + std::unique_ptr ble_manager_; + boards::gat562_mesh_evb_pro::Gat562Board* board_ = nullptr; + chat::ui::IChatUiRuntime* chat_ui_runtime_ = nullptr; +}; + +} // namespace apps::gat562_mesh_evb_pro diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/app_runtime_access.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/app_runtime_access.h new file mode 100644 index 00000000..e8159540 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/app_runtime_access.h @@ -0,0 +1,16 @@ +#pragma once + +namespace apps::gat562_mesh_evb_pro::app_runtime_access +{ + +struct Status +{ + bool initialized = false; + bool app_facade_bound = false; +}; + +bool initialize(); +void tick(); +const Status& status(); + +} // namespace apps::gat562_mesh_evb_pro::app_runtime_access diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/arduino_entry.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/arduino_entry.h new file mode 100644 index 00000000..d0030dad --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/arduino_entry.h @@ -0,0 +1,9 @@ +#pragma once + +namespace apps::gat562_mesh_evb_pro::arduino_entry +{ + +void setup(); +void loop(); + +} // namespace apps::gat562_mesh_evb_pro::arduino_entry diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/debug_console.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/debug_console.h new file mode 100644 index 00000000..cfd8f389 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/debug_console.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace apps::gat562_mesh_evb_pro::debug_console +{ + +void begin(); +void print(const char* text); +void println(); +void println(const char* text); +void printf(const char* format, ...); + +} // namespace apps::gat562_mesh_evb_pro::debug_console diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/loop_runtime.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/loop_runtime.h new file mode 100644 index 00000000..c87f3650 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/loop_runtime.h @@ -0,0 +1,8 @@ +#pragma once + +namespace apps::gat562_mesh_evb_pro::loop_runtime +{ + +void tick(); + +} // namespace apps::gat562_mesh_evb_pro::loop_runtime diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/protocol_factory.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/protocol_factory.h new file mode 100644 index 00000000..4d093344 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/protocol_factory.h @@ -0,0 +1,19 @@ +#pragma once + +#include "chat/domain/chat_types.h" +#include "chat/runtime/self_identity_provider.h" + +#include + +namespace chat +{ +class IMeshAdapter; +} + +namespace apps::gat562_mesh_evb_pro +{ + +std::unique_ptr createProtocolAdapter(chat::MeshProtocol protocol, + const chat::runtime::SelfIdentityProvider* identity_provider); + +} // namespace apps::gat562_mesh_evb_pro diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/self_identity_provider.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/self_identity_provider.h new file mode 100644 index 00000000..dc88d858 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/self_identity_provider.h @@ -0,0 +1,10 @@ +#pragma once + +#include "platform/nrf52/arduino_common/self_identity_bridge.h" + +namespace apps::gat562_mesh_evb_pro +{ + +using SelfIdentityProvider = platform::nrf52::arduino_common::SelfIdentityBridge; + +} // namespace apps::gat562_mesh_evb_pro diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/startup_runtime.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/startup_runtime.h new file mode 100644 index 00000000..1e3cc9b2 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/startup_runtime.h @@ -0,0 +1,8 @@ +#pragma once + +namespace apps::gat562_mesh_evb_pro::startup_runtime +{ + +void run(); + +} // namespace apps::gat562_mesh_evb_pro::startup_runtime diff --git a/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/ui_runtime.h b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/ui_runtime.h new file mode 100644 index 00000000..2d98b53c --- /dev/null +++ b/apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/ui_runtime.h @@ -0,0 +1,12 @@ +#pragma once + +#include "boards/gat562_mesh_evb_pro/gat562_board.h" + +namespace apps::gat562_mesh_evb_pro::ui_runtime +{ + +bool initialize(); +void appendBootLog(const char* line); +void tick(const boards::gat562_mesh_evb_pro::BoardInputEvent* event); + +} // namespace apps::gat562_mesh_evb_pro::ui_runtime diff --git a/apps/gat562_mesh_evb_pro/library.json b/apps/gat562_mesh_evb_pro/library.json new file mode 100644 index 00000000..adc8668b --- /dev/null +++ b/apps/gat562_mesh_evb_pro/library.json @@ -0,0 +1,27 @@ +{ + "name": "gat562_mesh_evb_pro", + "version": "0.1.0", + "frameworks": [ + "arduino" + ], + "platforms": [ + "nordicnrf52" + ], + "build": { + "includeDir": "include", + "srcDir": "src", + "flags": [ + "-Iinclude", + "-I../../boards/gat562_mesh_evb_pro/include", + "-I../../platform/esp/boards/include", + "-I../../modules/core_sys/include", + "-I../../modules/core_chat/include", + "-I../../modules/core_chat/generated", + "-I../../modules/core_gps/include", + "-I../../modules/ui_mono_128x64/include", + "-I../../modules/ui_shared/include", + "-I../../platform/nrf52/arduino_common/include", + "-I../.." + ] + } +} diff --git a/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp b/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp new file mode 100644 index 00000000..ffdfa318 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp @@ -0,0 +1,516 @@ +#include "apps/gat562_mesh_evb_pro/app_facade_runtime.h" + +#include "app/app_facade_access.h" +#include "apps/gat562_mesh_evb_pro/debug_console.h" +#include "apps/gat562_mesh_evb_pro/protocol_factory.h" +#include "boards/gat562_mesh_evb_pro/gat562_board.h" +#include "boards/gat562_mesh_evb_pro/settings_store.h" +#include "ble/ble_manager.h" +#include "chat/domain/chat_model.h" +#include "chat/infra/mesh_adapter_router_core.h" +#include "chat/infra/mesh_protocol_utils.h" +#include "chat/infra/store/ram_store.h" +#include "chat/runtime/self_identity_provider.h" +#include "chat/usecase/chat_service.h" +#include "chat/usecase/contact_service.h" +#include "platform/nrf52/arduino_common/chat/infra/contact_store.h" +#include "platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h" +#include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h" +#include "platform/nrf52/arduino_common/self_identity_bridge.h" + +#include + +#include +#include +#include + +namespace apps::gat562_mesh_evb_pro +{ +namespace +{ + +template +void copyString(const char* src, T* dst, size_t dst_len) +{ + if (!dst || dst_len == 0) + { + return; + } + + if (!src) + { + dst[0] = '\0'; + return; + } + + const size_t copy_len = std::min(std::strlen(src), dst_len - 1); + std::memcpy(dst, src, copy_len); + dst[copy_len] = '\0'; +} + +} // namespace + +AppFacadeRuntime& AppFacadeRuntime::instance() +{ + static AppFacadeRuntime runtime; + return runtime; +} + +AppFacadeRuntime::AppFacadeRuntime() = default; + +AppFacadeRuntime::~AppFacadeRuntime() = default; + +bool AppFacadeRuntime::initialize() +{ + if (initialized_) + { + return true; + } + + board_ = &::boards::gat562_mesh_evb_pro::Gat562Board::instance(); + if (board_) + { + (void)board_->begin(); + } + + (void)::boards::gat562_mesh_evb_pro::settings_store::loadAppConfig(config_); + ::boards::gat562_mesh_evb_pro::settings_store::normalizeConfig(config_); + identity_bridge_ = std::unique_ptr( + new platform::nrf52::arduino_common::SelfIdentityBridge(config_, + NRF_FICR->DEVICEADDR[0], + NRF_FICR->DEVICEADDR[1], + board_->defaultLongName(), + board_->defaultShortName())); + refreshEffectiveIdentity(); + initializeStores(); + initializeChatRuntime(); + + app::bindAppFacade(*this); + ble_manager_ = std::unique_ptr(new ble::BleManager(*this)); + if (config_.ble_enabled && ble_manager_) + { + ble_manager_->begin(); + } + initialized_ = true; + debug_console::printf("[gat562] app facade ready node=%08lX\n", + static_cast(effective_identity_.node_id)); + return true; +} + +bool AppFacadeRuntime::isInitialized() const +{ + return initialized_; +} + +bool AppFacadeRuntime::installMeshBackend(chat::MeshProtocol protocol, + std::unique_ptr backend) +{ + if (!mesh_router_ || !backend) + { + return false; + } + + if (!mesh_router_->installBackend(protocol, std::move(backend))) + { + return false; + } + + if (protocol == config_.mesh_protocol) + { + applyMeshConfig(); + applyUserInfo(); + applyNetworkLimits(); + applyPrivacyConfig(); + } + return true; +} + +void AppFacadeRuntime::initializeStores() +{ + node_store_ = std::unique_ptr( + new platform::nrf52::arduino_common::chat::meshtastic::NodeStore()); + contact_store_ = std::unique_ptr( + new platform::nrf52::arduino_common::chat::infra::ContactStore()); + contact_service_ = std::unique_ptr( + new chat::contacts::ContactService(*node_store_, *contact_store_)); + + if (node_store_) + { + node_store_->begin(); + } + if (contact_store_) + { + contact_store_->begin(); + } + if (contact_service_) + { + contact_service_->begin(); + } +} + +void AppFacadeRuntime::initializeChatRuntime() +{ + chat_model_ = std::unique_ptr(new chat::ChatModel()); + chat_store_ = std::unique_ptr(new chat::RamStore()); + mesh_router_ = std::unique_ptr(new chat::MeshAdapterRouterCore()); + chat_service_ = std::unique_ptr( + new chat::ChatService(*chat_model_, *mesh_router_, *chat_store_, config_.mesh_protocol)); + + if (chat_model_) + { + chat_model_->setPolicy(config_.chat_policy); + } + + (void)installMeshBackend(chat::MeshProtocol::Meshtastic, + createProtocolAdapter(chat::MeshProtocol::Meshtastic, identityProvider())); + (void)installMeshBackend(chat::MeshProtocol::MeshCore, + createProtocolAdapter(chat::MeshProtocol::MeshCore, identityProvider())); + + applyMeshConfig(); + applyUserInfo(); + applyNetworkLimits(); + applyPrivacyConfig(); + applyChatDefaults(); +} + +void AppFacadeRuntime::refreshEffectiveIdentity() +{ + effective_identity_ = chat::runtime::EffectiveSelfIdentity{}; + if (!identity_bridge_) + { + return; + } + + chat::runtime::SelfIdentityInput input{}; + if (!identity_bridge_->readSelfIdentityInput(&input)) + { + return; + } + + (void)chat::runtime::resolveEffectiveSelfIdentity(input, &effective_identity_); +} + +const chat::runtime::SelfIdentityProvider* AppFacadeRuntime::identityProvider() const +{ + return identity_bridge_.get(); +} + +app::AppConfig& AppFacadeRuntime::getConfig() +{ + return config_; +} + +const app::AppConfig& AppFacadeRuntime::getConfig() const +{ + return config_; +} + +void AppFacadeRuntime::saveConfig() +{ + ::boards::gat562_mesh_evb_pro::settings_store::normalizeConfig(config_); + (void)::boards::gat562_mesh_evb_pro::settings_store::saveAppConfig(config_); + refreshEffectiveIdentity(); + applyMeshConfig(); + applyUserInfo(); + applyPositionConfig(); + applyNetworkLimits(); + applyPrivacyConfig(); + applyChatDefaults(); +} + +void AppFacadeRuntime::applyMeshConfig() +{ + ::boards::gat562_mesh_evb_pro::settings_store::normalizeConfig(config_); + if (mesh_router_) + { + mesh_router_->applyConfig(config_.activeMeshConfig()); + } + if (board_) + { + board_->applyRadioConfig(config_.mesh_protocol, config_.activeMeshConfig()); + } + if (chat_service_) + { + chat_service_->setActiveProtocol(config_.mesh_protocol); + } + if (ble_manager_) + { + ble_manager_->applyProtocol(config_.mesh_protocol); + } +} + +void AppFacadeRuntime::applyUserInfo() +{ + refreshEffectiveIdentity(); + if (mesh_router_) + { + mesh_router_->setUserInfo(effective_identity_.long_name, + effective_identity_.short_name); + } + if (ble_manager_ && ble_manager_->isEnabled()) + { + ble_manager_->setEnabled(false); + ble_manager_->setEnabled(true); + } +} + +void AppFacadeRuntime::applyPositionConfig() +{ + if (board_) + { + board_->applyGpsConfig(config_); + } +} + +void AppFacadeRuntime::applyNetworkLimits() +{ + if (mesh_router_) + { + mesh_router_->setNetworkLimits(config_.net_duty_cycle, config_.net_channel_util); + } +} + +void AppFacadeRuntime::applyPrivacyConfig() +{ + if (mesh_router_) + { + mesh_router_->setPrivacyConfig(config_.privacy_encrypt_mode, config_.privacy_pki); + } +} + +void AppFacadeRuntime::applyChatDefaults() +{ + if (!chat_service_) + { + return; + } + + const chat::ChannelId channel = (config_.chat_channel == 1) + ? chat::ChannelId::SECONDARY + : chat::ChannelId::PRIMARY; + chat_service_->switchChannel(channel); +} + +void AppFacadeRuntime::getEffectiveUserInfo(char* out_long, std::size_t long_len, + char* out_short, std::size_t short_len) const +{ + const char* long_name = effective_identity_.long_name[0] != '\0' + ? effective_identity_.long_name + : (board_ ? board_->defaultLongName() : ""); + const char* short_name = effective_identity_.short_name[0] != '\0' + ? effective_identity_.short_name + : (board_ ? board_->defaultShortName() : ""); + copyString(long_name, out_long, long_len); + copyString(short_name, out_short, short_len); +} + +bool AppFacadeRuntime::switchMeshProtocol(chat::MeshProtocol protocol, bool persist) +{ + if (!chat::infra::isValidMeshProtocol(protocol)) + { + return false; + } + + config_.mesh_protocol = protocol; + applyMeshConfig(); + applyUserInfo(); + applyNetworkLimits(); + applyPrivacyConfig(); + + if (persist) + { + saveConfig(); + } + return true; +} + +chat::ChatService& AppFacadeRuntime::getChatService() +{ + return *chat_service_; +} + +chat::contacts::ContactService& AppFacadeRuntime::getContactService() +{ + return *contact_service_; +} + +chat::IMeshAdapter* AppFacadeRuntime::getMeshAdapter() +{ + return mesh_router_.get(); +} + +const chat::IMeshAdapter* AppFacadeRuntime::getMeshAdapter() const +{ + return mesh_router_.get(); +} + +chat::NodeId AppFacadeRuntime::getSelfNodeId() const +{ + return effective_identity_.node_id; +} + +team::TeamController* AppFacadeRuntime::getTeamController() +{ + return nullptr; +} + +team::TeamPairingService* AppFacadeRuntime::getTeamPairing() +{ + return nullptr; +} + +team::TeamService* AppFacadeRuntime::getTeamService() +{ + return nullptr; +} + +const team::TeamService* AppFacadeRuntime::getTeamService() const +{ + return nullptr; +} + +team::TeamTrackSampler* AppFacadeRuntime::getTeamTrackSampler() +{ + return nullptr; +} + +void AppFacadeRuntime::setTeamModeActive(bool active) +{ + (void)active; +} + +void AppFacadeRuntime::broadcastNodeInfo() +{ + if (mesh_router_) + { + (void)mesh_router_->requestNodeInfo(0xFFFFFFFFUL, false); + } +} + +void AppFacadeRuntime::clearNodeDb() +{ + if (node_store_) + { + node_store_->clear(); + } + if (contact_service_) + { + contact_service_->clearCache(); + } +} + +void AppFacadeRuntime::clearMessageDb() +{ + if (chat_service_) + { + chat_service_->clearAllMessages(); + } +} + +ble::BleManager* AppFacadeRuntime::getBleManager() +{ + return ble_manager_.get(); +} + +const ble::BleManager* AppFacadeRuntime::getBleManager() const +{ + return ble_manager_.get(); +} + +bool AppFacadeRuntime::isBleEnabled() const +{ + return config_.ble_enabled; +} + +void AppFacadeRuntime::setBleEnabled(bool enabled) +{ + if (config_.ble_enabled == enabled) + { + if (ble_manager_) + { + ble_manager_->setEnabled(enabled); + } + return; + } + + config_.ble_enabled = enabled; + if (ble_manager_) + { + ble_manager_->setEnabled(enabled); + } + (void)::boards::gat562_mesh_evb_pro::settings_store::saveAppConfig(config_); +} + +chat::contacts::INodeStore* AppFacadeRuntime::getNodeStore() +{ + return node_store_.get(); +} + +const chat::contacts::INodeStore* AppFacadeRuntime::getNodeStore() const +{ + return node_store_.get(); +} + +void AppFacadeRuntime::resetMeshConfig() +{ + if (config_.mesh_protocol == chat::MeshProtocol::MeshCore) + { + config_.meshcore_config = chat::MeshConfig(); + config_.applyMeshCoreFactoryDefaults(); + } + else + { + config_.meshtastic_config = chat::MeshConfig(); + config_.meshtastic_config.region = app::AppConfig::kDefaultRegionCode; + } + saveConfig(); + applyMeshConfig(); +} + +chat::ui::IChatUiRuntime* AppFacadeRuntime::getChatUiRuntime() +{ + return chat_ui_runtime_; +} + +void AppFacadeRuntime::setChatUiRuntime(chat::ui::IChatUiRuntime* runtime) +{ + chat_ui_runtime_ = runtime; +} + +BoardBase* AppFacadeRuntime::getBoard() +{ + return board_; +} + +const BoardBase* AppFacadeRuntime::getBoard() const +{ + return board_; +} + +void AppFacadeRuntime::updateCoreServices() +{ + if (chat_service_) + { + chat_service_->processIncoming(); + } + if (ble_manager_) + { + ble_manager_->update(); + } +} + +void AppFacadeRuntime::tickEventRuntime() +{ +} + +void AppFacadeRuntime::dispatchPendingEvents(std::size_t max_events) +{ + (void)max_events; +} + +const chat::runtime::EffectiveSelfIdentity& AppFacadeRuntime::effectiveIdentity() const +{ + return effective_identity_; +} + +} // namespace apps::gat562_mesh_evb_pro diff --git a/apps/gat562_mesh_evb_pro/src/app_runtime_access.cpp b/apps/gat562_mesh_evb_pro/src/app_runtime_access.cpp new file mode 100644 index 00000000..e42aafb2 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/src/app_runtime_access.cpp @@ -0,0 +1,73 @@ +#include "apps/gat562_mesh_evb_pro/app_runtime_access.h" + +#include + +#include "app/app_facade_access.h" +#include "apps/gat562_mesh_evb_pro/app_facade_runtime.h" +#include "apps/gat562_mesh_evb_pro/debug_console.h" +#include "apps/gat562_mesh_evb_pro/ui_runtime.h" +#include "boards/gat562_mesh_evb_pro/gat562_board.h" +#include "chat/ports/i_mesh_adapter.h" +#include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h" + +namespace apps::gat562_mesh_evb_pro::app_runtime_access +{ +namespace +{ + +Status s_status{}; + +} // namespace + +bool initialize() +{ + if (s_status.initialized) + { + return s_status.app_facade_bound; + } + + s_status = Status{}; + s_status.initialized = true; + + AppFacadeRuntime& runtime = AppFacadeRuntime::instance(); + s_status.app_facade_bound = runtime.initialize() && app::hasAppFacade(); + if (!s_status.app_facade_bound) + { + debug_console::println("[gat562] app runtime init failed"); + } + return s_status.app_facade_bound; +} + +void tick() +{ + auto& board = ::boards::gat562_mesh_evb_pro::Gat562Board::instance(); + board.tickGps(); + ::boards::gat562_mesh_evb_pro::BoardInputEvent input_event{}; + (void)board.pollInputEvent(&input_event); + AppFacadeRuntime& runtime = AppFacadeRuntime::instance(); + if (chat::IMeshAdapter* adapter = runtime.getMeshAdapter()) + { + adapter->processSendQueue(); + + platform::nrf52::arduino_common::chat::infra::RadioPacket packet{}; + auto* io = platform::nrf52::arduino_common::chat::infra::radioPacketIo(); + while (io && io->pollReceive(&packet)) + { + adapter->setLastRxStats(packet.rx_meta.rssi_dbm_x10 / 10.0f, + packet.rx_meta.snr_db_x10 / 10.0f); + adapter->handleRawPacket(packet.data, packet.size); + } + } + + runtime.updateCoreServices(); + runtime.tickEventRuntime(); + runtime.dispatchPendingEvents(); + ui_runtime::tick(&input_event); +} + +const Status& status() +{ + return s_status; +} + +} // namespace apps::gat562_mesh_evb_pro::app_runtime_access diff --git a/apps/gat562_mesh_evb_pro/src/arduino_entry.cpp b/apps/gat562_mesh_evb_pro/src/arduino_entry.cpp new file mode 100644 index 00000000..20403290 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/src/arduino_entry.cpp @@ -0,0 +1,19 @@ +#include "apps/gat562_mesh_evb_pro/arduino_entry.h" + +#include "apps/gat562_mesh_evb_pro/loop_runtime.h" +#include "apps/gat562_mesh_evb_pro/startup_runtime.h" + +namespace apps::gat562_mesh_evb_pro::arduino_entry +{ + +void setup() +{ + startup_runtime::run(); +} + +void loop() +{ + loop_runtime::tick(); +} + +} // namespace apps::gat562_mesh_evb_pro::arduino_entry diff --git a/apps/gat562_mesh_evb_pro/src/debug_console.cpp b/apps/gat562_mesh_evb_pro/src/debug_console.cpp new file mode 100644 index 00000000..08a8b412 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/src/debug_console.cpp @@ -0,0 +1,62 @@ +#include "apps/gat562_mesh_evb_pro/debug_console.h" + +#include +#include +#include + +namespace apps::gat562_mesh_evb_pro::debug_console +{ +namespace +{ + +constexpr unsigned long kBaudRate = 115200UL; + +void writeToUart(const char* text) +{ + if (!text) + { + return; + } + Serial2.print(text); +} + +} // namespace + +void begin() +{ + Serial2.begin(kBaudRate); + delay(40); +} + +void print(const char* text) +{ + writeToUart(text); +} + +void println() +{ + Serial2.println(); +} + +void println(const char* text) +{ + writeToUart(text); + Serial2.println(); +} + +void printf(const char* format, ...) +{ + if (!format) + { + return; + } + + char buffer[192] = {}; + va_list args; + va_start(args, format); + vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + print(buffer); +} + +} // namespace apps::gat562_mesh_evb_pro::debug_console diff --git a/apps/gat562_mesh_evb_pro/src/loop_runtime.cpp b/apps/gat562_mesh_evb_pro/src/loop_runtime.cpp new file mode 100644 index 00000000..85074351 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/src/loop_runtime.cpp @@ -0,0 +1,16 @@ +#include "apps/gat562_mesh_evb_pro/loop_runtime.h" + +#include + +#include "apps/gat562_mesh_evb_pro/app_runtime_access.h" + +namespace apps::gat562_mesh_evb_pro::loop_runtime +{ + +void tick() +{ + app_runtime_access::tick(); + delay(2); +} + +} // namespace apps::gat562_mesh_evb_pro::loop_runtime diff --git a/apps/gat562_mesh_evb_pro/src/protocol_factory.cpp b/apps/gat562_mesh_evb_pro/src/protocol_factory.cpp new file mode 100644 index 00000000..90bba6de --- /dev/null +++ b/apps/gat562_mesh_evb_pro/src/protocol_factory.cpp @@ -0,0 +1,24 @@ +#include "apps/gat562_mesh_evb_pro/protocol_factory.h" + +#include "platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_adapter_lite.h" +#include "platform/nrf52/arduino_common/chat/infra/meshtastic/mt_adapter_lite.h" + +namespace apps::gat562_mesh_evb_pro +{ + +std::unique_ptr createProtocolAdapter(chat::MeshProtocol protocol, + const chat::runtime::SelfIdentityProvider* identity_provider) +{ + switch (protocol) + { + case chat::MeshProtocol::MeshCore: + return std::unique_ptr( + new platform::nrf52::arduino_common::chat::meshcore::MeshCoreAdapterLite(identity_provider)); + case chat::MeshProtocol::Meshtastic: + default: + return std::unique_ptr( + new platform::nrf52::arduino_common::chat::meshtastic::MtAdapterLite(identity_provider)); + } +} + +} // namespace apps::gat562_mesh_evb_pro diff --git a/apps/gat562_mesh_evb_pro/src/startup_runtime.cpp b/apps/gat562_mesh_evb_pro/src/startup_runtime.cpp new file mode 100644 index 00000000..b9cd9626 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/src/startup_runtime.cpp @@ -0,0 +1,49 @@ +#include "apps/gat562_mesh_evb_pro/startup_runtime.h" + +#include + +#include "apps/gat562_mesh_evb_pro/app_facade_runtime.h" +#include "apps/gat562_mesh_evb_pro/app_runtime_access.h" +#include "apps/gat562_mesh_evb_pro/debug_console.h" +#include "apps/gat562_mesh_evb_pro/ui_runtime.h" +#include "boards/gat562_mesh_evb_pro/gat562_board.h" +#include "sys/clock.h" + +namespace apps::gat562_mesh_evb_pro::startup_runtime +{ + +void run() +{ + debug_console::begin(); + debug_console::println(); + debug_console::println("[gat562] startup begin"); + auto& board = ::boards::gat562_mesh_evb_pro::Gat562Board::instance(); + (void)board.begin(); + sys::set_millis_provider([]() -> uint32_t { return millis(); }); + sys::set_epoch_seconds_provider([]() -> uint32_t { + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().currentEpochSeconds(); + }); + ui_runtime::initialize(); + ui_runtime::appendBootLog("startup begin"); + ui_runtime::appendBootLog("board/input ok"); + + (void)board.bindRadioIo(); + const bool lora_ok = board.beginRadioIo(); + debug_console::println(lora_ok ? "[gat562] startup lora io ok" : "[gat562] startup lora io failed"); + ui_runtime::appendBootLog(lora_ok ? "lora io ok" : "lora io fail"); + + if (app_runtime_access::initialize()) + { + auto& cfg = AppFacadeRuntime::instance().getConfig(); + (void)board.startGpsRuntime(cfg); + debug_console::println("[gat562] startup app facade ok"); + ui_runtime::appendBootLog("app/gps ok"); + } + else + { + debug_console::println("[gat562] startup app facade failed"); + ui_runtime::appendBootLog("app init fail"); + } +} + +} // namespace apps::gat562_mesh_evb_pro::startup_runtime diff --git a/apps/gat562_mesh_evb_pro/src/ui_runtime.cpp b/apps/gat562_mesh_evb_pro/src/ui_runtime.cpp new file mode 100644 index 00000000..17358b93 --- /dev/null +++ b/apps/gat562_mesh_evb_pro/src/ui_runtime.cpp @@ -0,0 +1,104 @@ +#include "apps/gat562_mesh_evb_pro/ui_runtime.h" + +#include "apps/gat562_mesh_evb_pro/app_facade_runtime.h" +#include "boards/gat562_mesh_evb_pro/gat562_board.h" +#include "platform/ui/device_runtime.h" +#include "platform/ui/gps_runtime.h" +#include "platform/ui/time_runtime.h" +#include "sys/clock.h" +#include "ui/mono_128x64/runtime.h" + +#include +#include + +namespace apps::gat562_mesh_evb_pro::ui_runtime +{ +namespace +{ +using boards::gat562_mesh_evb_pro::BoardInputEvent; +using boards::gat562_mesh_evb_pro::BoardInputKey; + +uint32_t now_ms() { return millis(); } +time_t utc_now() { return static_cast(sys::epoch_seconds_now()); } + +uint32_t active_lora_frequency_hz() +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().activeLoraFrequencyHz(); +} + +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::InputAction to_input_action( + const BoardInputEvent* event) +{ + if (!event || !event->pressed) + { + return ui::mono_128x64::InputAction::None; + } + + switch (event->key) + { + case BoardInputKey::JoystickUp: return ui::mono_128x64::InputAction::Up; + case BoardInputKey::JoystickDown: return ui::mono_128x64::InputAction::Down; + case BoardInputKey::JoystickLeft: return ui::mono_128x64::InputAction::Left; + case BoardInputKey::JoystickRight: return ui::mono_128x64::InputAction::Right; + case BoardInputKey::JoystickPress: return ui::mono_128x64::InputAction::Select; + case BoardInputKey::PrimaryButton: return ui::mono_128x64::InputAction::Primary; + case BoardInputKey::SecondaryButton: return ui::mono_128x64::InputAction::Secondary; + default: return ui::mono_128x64::InputAction::None; + } +} + +bool s_initialized = false; +ui::mono_128x64::Runtime* s_runtime = nullptr; + +} // namespace + +bool initialize() +{ + if (s_initialized) + { + return s_runtime != nullptr; + } + s_initialized = true; + + static ui::mono_128x64::HostCallbacks callbacks{}; + callbacks.app = &AppFacadeRuntime::instance(); + callbacks.millis_fn = now_ms; + callbacks.utc_now_fn = utc_now; + callbacks.timezone_offset_min_fn = platform::ui::time::timezone_offset_min; + callbacks.set_timezone_offset_min_fn = platform::ui::time::set_timezone_offset_min; + callbacks.active_lora_frequency_hz_fn = active_lora_frequency_hz; + callbacks.format_frequency_fn = format_freq; + callbacks.battery_info_fn = platform::ui::device::battery_info; + 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; + + static ui::mono_128x64::Runtime runtime(::boards::gat562_mesh_evb_pro::Gat562Board::instance().monoDisplay(), + callbacks); + s_runtime = &runtime; + return s_runtime->begin(); +} + +void appendBootLog(const char* line) +{ + if (initialize() && s_runtime) + { + s_runtime->appendBootLog(line); + s_runtime->tick(ui::mono_128x64::InputAction::None); + } +} + +void tick(const BoardInputEvent* event) +{ + if (initialize() && s_runtime) + { + s_runtime->tick(to_input_action(event)); + } +} + +} // namespace apps::gat562_mesh_evb_pro::ui_runtime diff --git a/boards/gat562_mesh_evb_pro.json b/boards/gat562_mesh_evb_pro.json new file mode 100644 index 00000000..ec09e173 --- /dev/null +++ b/boards/gat562_mesh_evb_pro.json @@ -0,0 +1,53 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "GAT562 Mesh EVB Pro", + "mcu": "nrf52840", + "variant": "gat562_mesh_evb_pro", + "variants_dir": "variants", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "6.1.1", + "sd_fwid": "0x00B6" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd", + "openocd_target": "nrf52840-mdk-rs" + }, + "frameworks": ["arduino", "freertos"], + "name": "GAT562 Mesh EVB Pro", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "http://www.gat-iot.com/", + "vendor": "GAT-IOT" +} diff --git a/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/board_profile.h b/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/board_profile.h new file mode 100644 index 00000000..9b9f9977 --- /dev/null +++ b/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/board_profile.h @@ -0,0 +1,139 @@ +#pragma once + +#include + +namespace boards::gat562_mesh_evb_pro +{ + +struct BoardProfile +{ + struct LedPins + { + int status = -1; + int notification = -1; + bool active_high = true; + bool notification_shares_status = false; + }; + + struct InputPins + { + int button_primary = -1; + int button_secondary = -1; + int joystick_up = -1; + int joystick_down = -1; + int joystick_left = -1; + int joystick_right = -1; + int joystick_press = -1; + bool buttons_need_pullup = true; + bool joystick_need_pullup = true; + bool joystick_is_two_way = false; + uint16_t debounce_ms = 50; + }; + + struct I2cPins + { + int sda = -1; + int scl = -1; + uint8_t address = 0x3C; + }; + + struct UartPins + { + int rx = -1; + int tx = -1; + int aux = -1; + }; + + struct SpiPins + { + int sck = -1; + int miso = -1; + int mosi = -1; + int cs = -1; + }; + + struct LoraPins + { + SpiPins spi{}; + int dio1 = -1; + int busy = -1; + int reset = -1; + int power_en = -1; + bool dio2_controls_rf_switch = true; + float dio3_tcxo_voltage = 1.8f; + }; + + struct GpsProfile + { + UartPins uart{}; + int pps = -1; + uint32_t baud_rate = 9600; + }; + + struct BatteryProfile + { + int adc_pin = -1; + uint8_t adc_resolution_bits = 12; + float aref_voltage = 3.0f; + float adc_multiplier = 1.73f; + }; + + struct ProductBoundary + { + bool supports_meshtastic = true; + bool supports_meshcore = true; + bool supports_ble = true; + bool supports_lora = true; + bool supports_gnss = true; + bool supports_team = false; + bool supports_hostlink = false; + bool supports_sdcard = false; + bool supports_cjk_input = false; + bool supports_pinyin_ime = false; + bool supports_touch = false; + bool supports_keyboard = false; + }; + + struct ProductIdentity + { + const char* long_name = "GAT562"; + const char* short_name = "GAT562"; + const char* ble_name = "GAT562"; + }; + + LedPins leds{}; + InputPins inputs{}; + I2cPins oled_i2c{}; + UartPins jlink_cdc{}; + LoraPins lora{}; + GpsProfile gps{}; + BatteryProfile battery{}; + int peripheral_3v3_enable = -1; + bool has_screen = true; + bool use_ssd1306 = true; + uint32_t max_flash_size = 815104; + uint32_t max_ram_size = 248832; + uint32_t bootloader_settings_addr = 0xFF000; + ProductIdentity identity{}; + ProductBoundary boundary{}; +}; + +inline constexpr BoardProfile kBoardProfile{ + {35, 36, true, false}, + {9, 12, 28, 4, 30, 31, 10, true, true, false, 50}, + {13, 14, 0x3C}, + {8, 6, -1}, + {{43, 45, 44, 42}, 47, 46, 38, 37, true, 1.8f}, + {{15, 16, -1}, 17, 9600}, + {5, 12, 3.0f, 1.73f}, + 34, + true, + true, + 815104, + 248832, + 0xFF000, + {"GAT562", "GAT562", "GAT562"}, + {} +}; + +} // namespace boards::gat562_mesh_evb_pro diff --git a/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/gat562_board.h b/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/gat562_board.h new file mode 100644 index 00000000..76a52314 --- /dev/null +++ b/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/gat562_board.h @@ -0,0 +1,168 @@ +#pragma once + +#include "board/BoardBase.h" +#include "app/app_config.h" +#include "chat/domain/chat_types.h" +#include "gps/domain/gnss_satellite.h" +#include "gps/domain/gps_state.h" + +#include +#include + +class TwoWire; + +namespace ui::mono_128x64 +{ +class MonoDisplay; +} + +namespace platform::nrf52::arduino_common::chat::infra +{ +class IRadioPacketIo; +} + +namespace boards::gat562_mesh_evb_pro +{ + +enum class BoardInputKey : uint8_t +{ + None = 0, + PrimaryButton, + SecondaryButton, + JoystickUp, + JoystickDown, + JoystickLeft, + JoystickRight, + JoystickPress, +}; + +struct BoardInputEvent +{ + BoardInputKey key = BoardInputKey::None; + bool pressed = false; + uint32_t timestamp_ms = 0; +}; + +struct BoardInputSnapshot +{ + bool button_primary = false; + bool button_secondary = false; + bool joystick_up = false; + bool joystick_down = false; + bool joystick_left = false; + bool joystick_right = false; + bool joystick_press = false; + bool any_activity = false; +}; + +class Gat562Board final : public BoardBase +{ + public: + class I2cGuard + { + public: + explicit I2cGuard(Gat562Board& board, uint32_t timeout_ms = 100); + ~I2cGuard(); + + I2cGuard(const I2cGuard&) = delete; + I2cGuard& operator=(const I2cGuard&) = delete; + + bool locked() const; + explicit operator bool() const; + + private: + Gat562Board* board_ = nullptr; + bool locked_ = false; + }; + + static Gat562Board& instance(); + + uint32_t begin(uint32_t disable_hw_init = 0) override; + void wakeUp() override; + void handlePowerButton() override; + void softwareShutdown() override; + + void setBrightness(uint8_t level) override; + uint8_t getBrightness() override; + + bool hasKeyboard() override; + void keyboardSetBrightness(uint8_t level) override; + uint8_t keyboardGetBrightness() override; + + bool isRTCReady() const override; + bool isCharging() override; + int getBatteryLevel() override; + + bool isSDReady() const override; + bool isCardReady() override; + bool isGPSReady() const override; + + void vibrator() override; + void stopVibrator() override; + void playMessageTone() override; + + void setMessageToneVolume(uint8_t volume_percent) override; + uint8_t getMessageToneVolume() const override; + + void setStatusLed(bool on); + void pulseNotificationLed(uint32_t pulse_ms = 25); + bool pollInputSnapshot(BoardInputSnapshot* out_snapshot) const; + bool pollInputEvent(BoardInputEvent* out_event); + bool formatLoraFrequencyMHz(uint32_t freq_hz, char* out, std::size_t out_len) const; + uint16_t inputDebounceMs() const; + ::ui::mono_128x64::MonoDisplay& monoDisplay(); + bool ensureI2cReady(); + bool lockI2c(uint32_t timeout_ms = 100); + void unlockI2c(); + TwoWire& i2cWire(); + const char* defaultLongName() const; + const char* defaultShortName() const; + const char* defaultBleName() const; + bool prepareRadioHardware(); + bool beginRadioIo(); + platform::nrf52::arduino_common::chat::infra::IRadioPacketIo* bindRadioIo(); + void applyRadioConfig(chat::MeshProtocol protocol, const chat::MeshConfig& config); + uint32_t activeLoraFrequencyHz() const; + + bool startGpsRuntime(const app::AppConfig& config); + bool beginGps(const app::AppConfig& config); + void applyGpsConfig(const app::AppConfig& config); + void tickGps(); + bool isGpsRuntimeReady() const; + ::gps::GpsState gpsData() const; + bool gpsEnabled() const; + bool gpsPowered() const; + uint32_t gpsLastMotionMs() const; + bool gpsGnssSnapshot(::gps::GnssSatInfo* out, + std::size_t max, + std::size_t* out_count, + ::gps::GnssStatus* status) const; + void setGpsCollectionInterval(uint32_t interval_ms); + void setGpsPowerStrategy(uint8_t strategy); + void setGpsConfig(uint8_t mode, uint8_t sat_mask); + void setGpsNmeaConfig(uint8_t output_hz, uint8_t sentence_mask); + void setGpsMotionIdleTimeout(uint32_t timeout_ms); + void setGpsMotionSensorId(uint8_t sensor_id); + void suspendGps(); + void resumeGps(); + void setCurrentEpochSeconds(uint32_t epoch_s); + uint32_t currentEpochSeconds() const; + + private: + Gat562Board() = default; + + void initializeBoardHardware(); + void enablePeripheralRail(); + int readBatteryPercent() const; + + bool initialized_ = false; + bool i2c_initialized_ = false; + bool i2c_locked_ = false; + bool peripheral_rail_enabled_ = false; + bool radio_hw_ready_ = false; + uint8_t brightness_ = DEVICE_MAX_BRIGHTNESS_LEVEL; + uint8_t keyboard_brightness_ = 0; + uint8_t message_tone_volume_ = 45; +}; + +} // namespace boards::gat562_mesh_evb_pro diff --git a/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/settings_store.h b/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/settings_store.h new file mode 100644 index 00000000..dd114e7c --- /dev/null +++ b/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/settings_store.h @@ -0,0 +1,16 @@ +#pragma once + +#include "app/app_config.h" + +#include + +namespace boards::gat562_mesh_evb_pro::settings_store +{ + +void normalizeConfig(app::AppConfig& config); +bool loadAppConfig(app::AppConfig& config); +bool saveAppConfig(const app::AppConfig& config); +uint8_t loadMessageToneVolume(); +bool saveMessageToneVolume(uint8_t volume); + +} // namespace boards::gat562_mesh_evb_pro::settings_store diff --git a/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/sx1262_radio_packet_io.h b/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/sx1262_radio_packet_io.h new file mode 100644 index 00000000..50fe4fa8 --- /dev/null +++ b/boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/sx1262_radio_packet_io.h @@ -0,0 +1,58 @@ +#pragma once + +#include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h" + +#include + +class Module; +class SX1262; + +namespace boards::gat562_mesh_evb_pro +{ + +class Sx1262RadioPacketIo final : public platform::nrf52::arduino_common::chat::infra::IRadioPacketIo +{ + public: + struct AppliedRadioConfig + { + float freq_mhz = 0.0f; + float bw_khz = 0.0f; + uint8_t sf = 0; + uint8_t cr = 0; + int8_t tx_power = 0; + uint16_t preamble_len = 0; + uint8_t sync_word = 0; + uint8_t crc_len = 0; + }; + + Sx1262RadioPacketIo(); + ~Sx1262RadioPacketIo() override; + + bool begin() override; + void applyConfig(::chat::MeshProtocol protocol, const ::chat::MeshConfig& config) override; + bool transmit(const uint8_t* data, size_t size) override; + bool pollReceive(platform::nrf52::arduino_common::chat::infra::RadioPacket* out_packet) override; + uint32_t appliedFrequencyHz() const { return applied_freq_hz_; } + uint32_t appliedBandwidthHz() const { return applied_bw_hz_; } + + private: + bool initializeRadioChip(); + bool enterReceiveMode(); + bool applyRadioConfig(const AppliedRadioConfig& config); + AppliedRadioConfig deriveRadioConfig(::chat::MeshProtocol protocol, const ::chat::MeshConfig& config) const; + + std::unique_ptr module_; + std::unique_ptr radio_; + bool initialized_ = false; + bool receiving_ = false; + bool radio_online_ = false; + ::chat::MeshProtocol active_protocol_ = ::chat::MeshProtocol::Meshtastic; + ::chat::MeshConfig active_config_{}; + AppliedRadioConfig applied_config_{}; + uint32_t applied_freq_hz_ = 0; + uint32_t applied_bw_hz_ = 0; +}; + +Sx1262RadioPacketIo& sx1262RadioPacketIo(); + +} // namespace boards::gat562_mesh_evb_pro diff --git a/boards/gat562_mesh_evb_pro/library.json b/boards/gat562_mesh_evb_pro/library.json new file mode 100644 index 00000000..cc3c9fd3 --- /dev/null +++ b/boards/gat562_mesh_evb_pro/library.json @@ -0,0 +1,33 @@ +{ + "name": "boards_gat562_mesh_evb_pro", + "version": "0.1.0", + "frameworks": [ + "arduino" + ], + "platforms": [ + "nordicnrf52" + ], + "build": { + "includeDir": "include", + "srcDir": "src", + "flags": [ + "-std=gnu++17", + "-Iinclude", + "-I../..", + "-I../../platform/esp/boards/include", + "-I../../modules/core_sys/include", + "-I../../modules/core_chat/include", + "-I../../modules/core_chat/generated", + "-I../../modules/core_gps/include", + "-I../../modules/ui_mono_128x64/include", + "-I../../modules/ui_shared/include", + "-I../../platform/nrf52/arduino_common/include", + "-I../../variants/gat562_mesh_evb_pro", + "-I../../.pio/libdeps/gat562_mesh_evb_pro/RadioLib/src", + "-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" + ] + } +} diff --git a/boards/gat562_mesh_evb_pro/src/gat562_board.cpp b/boards/gat562_mesh_evb_pro/src/gat562_board.cpp new file mode 100644 index 00000000..17bccc6c --- /dev/null +++ b/boards/gat562_mesh_evb_pro/src/gat562_board.cpp @@ -0,0 +1,955 @@ +#include "boards/gat562_mesh_evb_pro/gat562_board.h" + +#include "boards/gat562_mesh_evb_pro/board_profile.h" +#include "boards/gat562_mesh_evb_pro/settings_store.h" +#include "boards/gat562_mesh_evb_pro/sx1262_radio_packet_io.h" +#include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h" +#include "ui/mono_128x64/runtime.h" + +#include ".pio/libdeps/gat562_mesh_evb_pro/Adafruit SSD1306/Adafruit_SSD1306.h" +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace boards::gat562_mesh_evb_pro +{ +namespace +{ + +bool readActiveLowPin(int pin, bool use_pullup) +{ + if (pin < 0) + { + return false; + } + pinMode(pin, use_pullup ? INPUT_PULLUP : INPUT); + return digitalRead(pin) == LOW; +} + +void writeLed(int pin, bool active_high, bool on) +{ + if (pin < 0) + { + return; + } + pinMode(pin, OUTPUT); + digitalWrite(pin, (on == active_high) ? HIGH : LOW); +} + +struct DebounceState +{ + bool stable = false; + bool sampled = false; + uint32_t changed_at_ms = 0; +}; + +struct InputRuntimeState +{ + uint32_t last_activity_ms = 0; + BoardInputSnapshot snapshot{}; + DebounceState button_primary{}; + DebounceState button_secondary{}; + DebounceState joystick_up{}; + DebounceState joystick_down{}; + DebounceState joystick_left{}; + DebounceState joystick_right{}; + DebounceState joystick_press{}; +} s_input; + +struct GpsRuntimeState +{ + TinyGPSPlus parser{}; + ::gps::GpsState data{}; + ::gps::GnssStatus status{}; + uint32_t last_motion_ms = 0; + uint32_t collection_interval_ms = 60000; + uint32_t motion_idle_timeout_ms = 0; + uint8_t power_strategy = 0; + uint8_t gnss_mode = 0; + uint8_t sat_mask = 0; + uint8_t nmea_output_hz = 0; + uint8_t nmea_sentence_mask = 0; + uint8_t motion_sensor_id = 0; + uint32_t epoch_base_s = 0; + uint32_t epoch_base_ms = 0; + uint32_t last_nmea_ms = 0; + bool enabled = true; + bool powered = false; + bool initialized = false; + bool time_synced = false; + bool nmea_seen = false; +} s_gps; + +constexpr uint32_t kMinValidEpochSeconds = 1700000000UL; + +uint32_t readSystemEpochSeconds() +{ + const time_t now = ::time(nullptr); + if (now < static_cast(kMinValidEpochSeconds)) + { + return 0; + } + return static_cast(now); +} + +void syncSystemClockFromEpoch(uint32_t epoch_s) +{ + (void)epoch_s; +} + +uint8_t daysInMonth(int year, uint8_t month) +{ + static constexpr uint8_t kDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month == 0 || month > 12) + { + return 31; + } + if (month != 2) + { + return kDays[month - 1]; + } + const bool leap = ((year % 4) == 0 && (year % 100) != 0) || ((year % 400) == 0); + return leap ? 29 : 28; +} + +bool gpsDateTimeValid(int year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) +{ + if (year < 2020 || year > 2100) + { + return false; + } + if (month < 1 || month > 12) + { + return false; + } + const uint8_t max_day = daysInMonth(year, month); + if (day < 1 || day > max_day) + { + return false; + } + if (hour >= 24 || minute >= 60 || second >= 60) + { + return false; + } + return true; +} + +int64_t daysFromCivil(int year, unsigned month, unsigned day) +{ + year -= month <= 2 ? 1 : 0; + const int era = (year >= 0 ? year : year - 399) / 400; + const unsigned yoe = static_cast(year - era * 400); + const unsigned doy = (153 * (month + (month > 2 ? static_cast(-3) : 9)) + 2) / 5 + day - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return static_cast(era) * 146097 + static_cast(doe) - 719468; +} + +time_t gpsDateTimeToEpochUtc(int year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second) +{ + const int64_t days = daysFromCivil(year, month, day); + const int64_t sec_of_day = + static_cast(hour) * 3600 + static_cast(minute) * 60 + static_cast(second); + const int64_t epoch64 = days * 86400 + sec_of_day; + if (epoch64 < 0 || epoch64 > static_cast(std::numeric_limits::max())) + { + return static_cast(-1); + } + return static_cast(epoch64); +} + +bool updateDebounced(bool sampled, + DebounceState& state, + uint16_t debounce_ms, + BoardInputKey key, + BoardInputEvent* out_event, + uint32_t now_ms) +{ + if (sampled != state.sampled) + { + state.sampled = sampled; + state.changed_at_ms = now_ms; + } + + if (state.stable == state.sampled) + { + return false; + } + + if ((now_ms - state.changed_at_ms) < debounce_ms) + { + return false; + } + + state.stable = state.sampled; + if (out_event) + { + out_event->key = key; + out_event->pressed = state.stable; + out_event->timestamp_ms = now_ms; + } + if (state.stable) + { + s_input.last_activity_ms = now_ms; + } + return true; +} + +void applyGpsTimeIfValid() +{ + if (!s_gps.parser.time.isValid() || !s_gps.parser.date.isValid()) + { + return; + } + + const uint16_t year = s_gps.parser.date.year(); + const uint8_t month = s_gps.parser.date.month(); + const uint8_t day = s_gps.parser.date.day(); + const uint8_t hour = s_gps.parser.time.hour(); + const uint8_t minute = s_gps.parser.time.minute(); + const uint8_t second = s_gps.parser.time.second(); + + if (!gpsDateTimeValid(year, month, day, hour, minute, second)) + { + return; + } + + const time_t utc = gpsDateTimeToEpochUtc(year, month, day, hour, minute, second); + if (utc < static_cast(kMinValidEpochSeconds)) + { + return; + } + + const uint32_t utc_s = static_cast(utc); + if (s_gps.epoch_base_s == utc_s) + { + return; + } + s_gps.epoch_base_s = utc_s; + s_gps.epoch_base_ms = millis(); + s_gps.time_synced = true; + syncSystemClockFromEpoch(utc_s); +} + +void refreshGpsFix() +{ + s_gps.data.valid = s_gps.parser.location.isValid(); + s_gps.data.lat = s_gps.parser.location.lat(); + s_gps.data.lng = s_gps.parser.location.lng(); + s_gps.data.has_alt = s_gps.parser.altitude.isValid(); + s_gps.data.alt_m = s_gps.data.has_alt ? s_gps.parser.altitude.meters() : 0.0; + s_gps.data.has_speed = s_gps.parser.speed.isValid(); + s_gps.data.speed_mps = s_gps.data.has_speed ? (s_gps.parser.speed.kmph() / 3.6) : 0.0; + s_gps.data.has_course = s_gps.parser.course.isValid(); + s_gps.data.course_deg = s_gps.data.has_course ? s_gps.parser.course.deg() : 0.0; + s_gps.data.satellites = static_cast( + std::min(s_gps.parser.satellites.isValid() ? s_gps.parser.satellites.value() : 0U, 255U)); + s_gps.data.age = s_gps.parser.location.isValid() + ? static_cast(s_gps.parser.location.age()) + : 0xFFFFFFFFUL; + + s_gps.status.sats_in_use = s_gps.data.satellites; + s_gps.status.sats_in_view = s_gps.data.satellites; + s_gps.status.hdop = s_gps.parser.hdop.isValid() + ? static_cast(s_gps.parser.hdop.hdop()) + : 0.0f; + s_gps.status.fix = s_gps.data.valid + ? (s_gps.data.has_alt ? ::gps::GnssFix::FIX3D : ::gps::GnssFix::FIX2D) + : ::gps::GnssFix::NOFIX; + + if (s_gps.data.valid) + { + s_gps.last_motion_ms = millis(); + } +} + +} // namespace + +Gat562Board& Gat562Board::instance() +{ + static Gat562Board board_instance; + return board_instance; +} + +uint32_t Gat562Board::begin(uint32_t disable_hw_init) +{ + (void)disable_hw_init; + if (initialized_) + { + return 1U; + } + + initializeBoardHardware(); + ensureI2cReady(); + message_tone_volume_ = ::boards::gat562_mesh_evb_pro::settings_store::loadMessageToneVolume(); + initialized_ = true; + return 1U; +} + +void Gat562Board::initializeBoardHardware() +{ + const auto& profile = kBoardProfile; + enablePeripheralRail(); + + writeLed(profile.leds.status, profile.leds.active_high, false); + if (!profile.leds.notification_shares_status) + { + writeLed(profile.leds.notification, profile.leds.active_high, false); + } + + const auto setup_input = [](int pin, bool pullup) + { + if (pin >= 0) + { + pinMode(pin, pullup ? INPUT_PULLUP : INPUT); + } + }; + + setup_input(profile.inputs.button_primary, profile.inputs.buttons_need_pullup); + setup_input(profile.inputs.button_secondary, profile.inputs.buttons_need_pullup); + setup_input(profile.inputs.joystick_up, profile.inputs.joystick_need_pullup); + setup_input(profile.inputs.joystick_down, profile.inputs.joystick_need_pullup); + setup_input(profile.inputs.joystick_left, profile.inputs.joystick_need_pullup); + setup_input(profile.inputs.joystick_right, profile.inputs.joystick_need_pullup); + setup_input(profile.inputs.joystick_press, profile.inputs.joystick_need_pullup); +} + +void Gat562Board::enablePeripheralRail() +{ + if (peripheral_rail_enabled_) + { + return; + } + + const auto& profile = kBoardProfile; + if (profile.peripheral_3v3_enable >= 0) + { + pinMode(profile.peripheral_3v3_enable, OUTPUT); + digitalWrite(profile.peripheral_3v3_enable, HIGH); + } + peripheral_rail_enabled_ = true; +} + +void Gat562Board::wakeUp() +{ +} + +void Gat562Board::handlePowerButton() +{ +} + +void Gat562Board::softwareShutdown() +{ + NVIC_SystemReset(); +} + +void Gat562Board::setBrightness(uint8_t level) +{ + brightness_ = static_cast( + std::clamp(level, DEVICE_MIN_BRIGHTNESS_LEVEL, DEVICE_MAX_BRIGHTNESS_LEVEL)); +} + +uint8_t Gat562Board::getBrightness() +{ + return brightness_; +} + +bool Gat562Board::hasKeyboard() +{ + return false; +} + +void Gat562Board::keyboardSetBrightness(uint8_t level) +{ + keyboard_brightness_ = level; +} + +uint8_t Gat562Board::keyboardGetBrightness() +{ + return keyboard_brightness_; +} + +bool Gat562Board::isRTCReady() const +{ + return s_gps.time_synced && currentEpochSeconds() >= kMinValidEpochSeconds; +} + +bool Gat562Board::isCharging() +{ + return false; +} + +int Gat562Board::readBatteryPercent() const +{ + const auto& battery = kBoardProfile.battery; + if (battery.adc_pin < 0) + { + return -1; + } + + analogReference(AR_INTERNAL_3_0); + analogReadResolution(battery.adc_resolution_bits); + const int raw = analogRead(battery.adc_pin); + if (raw <= 0) + { + return -1; + } + + const float max_raw = static_cast((1UL << battery.adc_resolution_bits) - 1UL); + const float voltage = (static_cast(raw) / max_raw) * battery.aref_voltage * battery.adc_multiplier; + const float ratio = (voltage - 3.30f) / (4.20f - 3.30f); + const float clamped = ratio < 0.0f ? 0.0f : (ratio > 1.0f ? 1.0f : ratio); + return static_cast(clamped * 100.0f + 0.5f); +} + +int Gat562Board::getBatteryLevel() +{ + return readBatteryPercent(); +} + +bool Gat562Board::isSDReady() const +{ + return false; +} + +bool Gat562Board::isCardReady() +{ + return false; +} + +bool Gat562Board::isGPSReady() const +{ + return isGpsRuntimeReady(); +} + +void Gat562Board::vibrator() +{ + pulseNotificationLed(20); +} + +void Gat562Board::stopVibrator() +{ +} + +void Gat562Board::playMessageTone() +{ + pulseNotificationLed(25); +} + +void Gat562Board::setMessageToneVolume(uint8_t volume_percent) +{ + message_tone_volume_ = volume_percent; + (void)::boards::gat562_mesh_evb_pro::settings_store::saveMessageToneVolume(volume_percent); +} + +uint8_t Gat562Board::getMessageToneVolume() const +{ + return message_tone_volume_; +} + +void Gat562Board::setStatusLed(bool on) +{ + const auto& leds = kBoardProfile.leds; + writeLed(leds.status, leds.active_high, on); +} + +void Gat562Board::pulseNotificationLed(uint32_t pulse_ms) +{ + const auto& leds = kBoardProfile.leds; + const int pin = leds.notification_shares_status ? leds.status : leds.notification; + if (pin < 0) + { + return; + } + + writeLed(pin, leds.active_high, true); + delay(pulse_ms); + writeLed(pin, leds.active_high, false); +} + +bool Gat562Board::pollInputSnapshot(BoardInputSnapshot* out_snapshot) const +{ + if (!out_snapshot) + { + return false; + } + + const auto& inputs = kBoardProfile.inputs; + BoardInputSnapshot snapshot{}; + snapshot.button_primary = readActiveLowPin(inputs.button_primary, inputs.buttons_need_pullup); + snapshot.button_secondary = readActiveLowPin(inputs.button_secondary, inputs.buttons_need_pullup); + snapshot.joystick_up = readActiveLowPin(inputs.joystick_up, inputs.joystick_need_pullup); + snapshot.joystick_down = readActiveLowPin(inputs.joystick_down, inputs.joystick_need_pullup); + snapshot.joystick_left = readActiveLowPin(inputs.joystick_left, inputs.joystick_need_pullup); + snapshot.joystick_right = readActiveLowPin(inputs.joystick_right, inputs.joystick_need_pullup); + snapshot.joystick_press = readActiveLowPin(inputs.joystick_press, inputs.joystick_need_pullup); + snapshot.any_activity = snapshot.button_primary || snapshot.button_secondary || + snapshot.joystick_up || snapshot.joystick_down || + snapshot.joystick_left || snapshot.joystick_right || + snapshot.joystick_press; + + *out_snapshot = snapshot; + return snapshot.any_activity; +} + +bool Gat562Board::formatLoraFrequencyMHz(uint32_t freq_hz, char* out, std::size_t out_len) const +{ + if (!out || out_len == 0 || freq_hz == 0) + { + return false; + } + + const uint32_t mhz = freq_hz / 1000000UL; + const uint32_t khz = (freq_hz % 1000000UL) / 1000UL; + std::snprintf(out, out_len, "%lu.%03luMHz", + static_cast(mhz), + static_cast(khz)); + return true; +} + +uint16_t Gat562Board::inputDebounceMs() const +{ + return kBoardProfile.inputs.debounce_ms; +} + +bool Gat562Board::ensureI2cReady() +{ + if (i2c_initialized_) + { + return true; + } + + const auto& profile = kBoardProfile; + Wire.setPins(profile.oled_i2c.sda, profile.oled_i2c.scl); + Wire.begin(); + Wire.setClock(400000); + i2c_initialized_ = true; + return true; +} + +bool Gat562Board::lockI2c(uint32_t timeout_ms) +{ + const uint32_t start_ms = millis(); + while (true) + { + noInterrupts(); + if (!i2c_locked_) + { + i2c_locked_ = true; + interrupts(); + return true; + } + interrupts(); + + if ((millis() - start_ms) >= timeout_ms) + { + return false; + } + delay(1); + } +} + +void Gat562Board::unlockI2c() +{ + noInterrupts(); + i2c_locked_ = false; + interrupts(); +} + +TwoWire& Gat562Board::i2cWire() +{ + (void)ensureI2cReady(); + return Wire; +} + +bool Gat562Board::pollInputEvent(BoardInputEvent* out_event) +{ + if (out_event) + { + *out_event = BoardInputEvent{}; + } + + BoardInputSnapshot current{}; + (void)pollInputSnapshot(¤t); + s_input.snapshot = current; + + const uint32_t now_ms = millis(); + const uint16_t debounce_ms = inputDebounceMs(); + + return updateDebounced(current.button_primary, s_input.button_primary, debounce_ms, + BoardInputKey::PrimaryButton, out_event, now_ms) || + updateDebounced(current.button_secondary, s_input.button_secondary, debounce_ms, + BoardInputKey::SecondaryButton, out_event, now_ms) || + updateDebounced(current.joystick_up, s_input.joystick_up, debounce_ms, + BoardInputKey::JoystickUp, out_event, now_ms) || + updateDebounced(current.joystick_down, s_input.joystick_down, debounce_ms, + BoardInputKey::JoystickDown, out_event, now_ms) || + updateDebounced(current.joystick_left, s_input.joystick_left, debounce_ms, + BoardInputKey::JoystickLeft, out_event, now_ms) || + updateDebounced(current.joystick_right, s_input.joystick_right, debounce_ms, + BoardInputKey::JoystickRight, out_event, now_ms) || + updateDebounced(current.joystick_press, s_input.joystick_press, debounce_ms, + BoardInputKey::JoystickPress, out_event, now_ms); +} + +namespace +{ + +class Ssd1306MonoDisplay final : public ::ui::mono_128x64::MonoDisplay +{ + public: + Ssd1306MonoDisplay() + : display_(SCREEN_WIDTH, + SCREEN_HEIGHT, + &::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 charWidth(::ui::mono_128x64::FontSize size) const override + { + return size == ::ui::mono_128x64::FontSize::Large ? 12 : 6; + } + int lineHeight(::ui::mono_128x64::FontSize size) const override + { + return size == ::ui::mono_128x64::FontSize::Large ? 16 : 8; + } + void clear() override + { + if (online_) + { + display_.clearDisplay(); + } + } + void drawText(int x, int y, const char* text, ::ui::mono_128x64::FontSize size, bool inverse = false) override + { + if (!online_ || !text) + { + return; + } + display_.setTextSize(size == ::ui::mono_128x64::FontSize::Large ? 2 : 1); + display_.setTextColor(inverse ? SSD1306_BLACK : SSD1306_WHITE, + inverse ? SSD1306_WHITE : SSD1306_BLACK); + display_.setCursor(x, y); + display_.print(text); + display_.setTextColor(SSD1306_WHITE, SSD1306_BLACK); + } + void drawHLine(int x, int y, int w) override + { + if (online_) + { + display_.drawFastHLine(x, y, w, SSD1306_WHITE); + } + } + void fillRect(int x, int y, int w, int h, bool on) override + { + if (online_) + { + display_.fillRect(x, y, w, h, on ? SSD1306_WHITE : SSD1306_BLACK); + } + } + void present() override + { + if (!online_) + { + return; + } + auto& board = ::boards::gat562_mesh_evb_pro::Gat562Board::instance(); + Gat562Board::I2cGuard guard(board, 100); + if (guard) + { + display_.display(); + } + } + + private: + Adafruit_SSD1306 display_; + bool initialized_ = false; + bool online_ = false; +}; + +bool Ssd1306MonoDisplay::begin() +{ + if (initialized_) + { + return online_; + } + initialized_ = true; + + const auto& profile = ::boards::gat562_mesh_evb_pro::kBoardProfile; + auto& board = ::boards::gat562_mesh_evb_pro::Gat562Board::instance(); + Gat562Board::I2cGuard guard(board, 200); + if (!guard) + { + return false; + } + online_ = display_.begin(SSD1306_SWITCHCAPVCC, profile.oled_i2c.address, true, false); + if (online_) + { + display_.clearDisplay(); + display_.setTextWrap(false); + display_.display(); + } + return online_; +} + +} // namespace + +::ui::mono_128x64::MonoDisplay& Gat562Board::monoDisplay() +{ + static Ssd1306MonoDisplay display; + return display; +} + +Gat562Board::I2cGuard::I2cGuard(Gat562Board& board, uint32_t timeout_ms) + : board_(&board), + locked_(board.ensureI2cReady() && board.lockI2c(timeout_ms)) +{ +} + +Gat562Board::I2cGuard::~I2cGuard() +{ + if (board_ && locked_) + { + board_->unlockI2c(); + } +} + +bool Gat562Board::I2cGuard::locked() const +{ + return locked_; +} + +Gat562Board::I2cGuard::operator bool() const +{ + return locked_; +} + +const char* Gat562Board::defaultLongName() const +{ + return kBoardProfile.identity.long_name; +} + +const char* Gat562Board::defaultShortName() const +{ + return kBoardProfile.identity.short_name; +} + +const char* Gat562Board::defaultBleName() const +{ + return kBoardProfile.identity.ble_name; +} + +bool Gat562Board::prepareRadioHardware() +{ + if (radio_hw_ready_) + { + return true; + } + + (void)begin(); + enablePeripheralRail(); + + const auto& profile = kBoardProfile; + if (profile.lora.power_en >= 0) + { + pinMode(profile.lora.power_en, OUTPUT); + digitalWrite(profile.lora.power_en, HIGH); + delay(5); + } + + SPI.begin(); + radio_hw_ready_ = true; + return true; +} + +bool Gat562Board::beginRadioIo() +{ + return ::boards::gat562_mesh_evb_pro::sx1262RadioPacketIo().begin(); +} + +platform::nrf52::arduino_common::chat::infra::IRadioPacketIo* Gat562Board::bindRadioIo() +{ + auto& io = ::boards::gat562_mesh_evb_pro::sx1262RadioPacketIo(); + ::platform::nrf52::arduino_common::chat::infra::bindRadioPacketIo(&io); + return &io; +} + +void Gat562Board::applyRadioConfig(chat::MeshProtocol protocol, const chat::MeshConfig& config) +{ + ::boards::gat562_mesh_evb_pro::sx1262RadioPacketIo().applyConfig(protocol, config); +} + +uint32_t Gat562Board::activeLoraFrequencyHz() const +{ + return ::boards::gat562_mesh_evb_pro::sx1262RadioPacketIo().appliedFrequencyHz(); +} + +bool Gat562Board::startGpsRuntime(const app::AppConfig& config) +{ + if (!beginGps(config)) + { + return false; + } + applyGpsConfig(config); + return true; +} + +bool Gat562Board::beginGps(const app::AppConfig& config) +{ + (void)config; + if (!s_gps.initialized) + { + const auto& profile = kBoardProfile; + if (profile.gps.uart.aux >= 0) + { + pinMode(profile.gps.uart.aux, OUTPUT); + digitalWrite(profile.gps.uart.aux, HIGH); + } + Serial1.setPins(profile.gps.uart.rx, profile.gps.uart.tx); + Serial1.begin(profile.gps.baud_rate); + s_gps.initialized = true; + s_gps.powered = true; + } + return true; +} + +void Gat562Board::applyGpsConfig(const app::AppConfig& config) +{ + s_gps.collection_interval_ms = config.gps_interval_ms; + s_gps.power_strategy = config.gps_strategy; + s_gps.gnss_mode = config.gps_mode; + s_gps.sat_mask = config.gps_sat_mask; + s_gps.nmea_output_hz = config.privacy_nmea_output; + s_gps.nmea_sentence_mask = config.privacy_nmea_sentence; + s_gps.motion_idle_timeout_ms = config.motion_config.idle_timeout_ms; + s_gps.motion_sensor_id = config.motion_config.sensor_id; + s_gps.enabled = true; +} + +void Gat562Board::tickGps() +{ + if (!s_gps.initialized || !s_gps.enabled) + { + return; + } + + while (Serial1.available() > 0) + { + s_gps.nmea_seen = true; + s_gps.last_nmea_ms = millis(); + s_gps.parser.encode(static_cast(Serial1.read())); + } + applyGpsTimeIfValid(); + refreshGpsFix(); +} + +bool Gat562Board::isGpsRuntimeReady() const +{ + return s_gps.initialized && s_gps.powered; +} + +::gps::GpsState Gat562Board::gpsData() const +{ + return s_gps.data; +} + +bool Gat562Board::gpsEnabled() const +{ + return s_gps.enabled; +} + +bool Gat562Board::gpsPowered() const +{ + return s_gps.powered; +} + +uint32_t Gat562Board::gpsLastMotionMs() const +{ + return s_gps.last_motion_ms; +} + +bool Gat562Board::gpsGnssSnapshot(::gps::GnssSatInfo* out, + std::size_t max, + std::size_t* out_count, + ::gps::GnssStatus* status) const +{ + if (out_count) + { + *out_count = 0; + } + if (status) + { + *status = s_gps.status; + } + if (out && max > 0 && s_gps.data.valid) + { + out[0].id = 0; + out[0].sys = ::gps::GnssSystem::GPS; + out[0].snr = -1; + out[0].used = true; + if (out_count) + { + *out_count = 1; + } + return true; + } + return s_gps.data.valid; +} + +void Gat562Board::setGpsCollectionInterval(uint32_t interval_ms) { s_gps.collection_interval_ms = interval_ms; } +void Gat562Board::setGpsPowerStrategy(uint8_t strategy) { s_gps.power_strategy = strategy; } +void Gat562Board::setGpsConfig(uint8_t mode, uint8_t sat_mask) +{ + s_gps.gnss_mode = mode; + s_gps.sat_mask = sat_mask; +} +void Gat562Board::setGpsNmeaConfig(uint8_t output_hz, uint8_t sentence_mask) +{ + s_gps.nmea_output_hz = output_hz; + s_gps.nmea_sentence_mask = sentence_mask; +} +void Gat562Board::setGpsMotionIdleTimeout(uint32_t timeout_ms) { s_gps.motion_idle_timeout_ms = timeout_ms; } +void Gat562Board::setGpsMotionSensorId(uint8_t sensor_id) { s_gps.motion_sensor_id = sensor_id; } +void Gat562Board::suspendGps() { s_gps.enabled = false; } +void Gat562Board::resumeGps() { s_gps.enabled = true; } +void Gat562Board::setCurrentEpochSeconds(uint32_t epoch_s) +{ + if (epoch_s < kMinValidEpochSeconds) + { + return; + } + + s_gps.epoch_base_s = epoch_s; + s_gps.epoch_base_ms = millis(); + s_gps.time_synced = true; + syncSystemClockFromEpoch(epoch_s); +} + +uint32_t Gat562Board::currentEpochSeconds() const +{ + const uint32_t system_epoch_s = readSystemEpochSeconds(); + if (system_epoch_s >= kMinValidEpochSeconds) + { + return system_epoch_s; + } + + if (!s_gps.time_synced || s_gps.epoch_base_s == 0) + { + return 0; + } + const uint32_t elapsed_s = (millis() - s_gps.epoch_base_ms) / 1000U; + return s_gps.epoch_base_s + elapsed_s; +} + +} // namespace boards::gat562_mesh_evb_pro + +BoardBase& board = ::boards::gat562_mesh_evb_pro::Gat562Board::instance(); diff --git a/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp b/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp new file mode 100644 index 00000000..a38c5f7a --- /dev/null +++ b/boards/gat562_mesh_evb_pro/src/platform_ui_bindings.cpp @@ -0,0 +1,155 @@ +#include "boards/gat562_mesh_evb_pro/gat562_board.h" + +#include "platform/ui/device_runtime.h" +#include "platform/ui/gps_runtime.h" + +#include + +#include + +namespace platform::ui::device +{ + +void delay_ms(uint32_t ms) +{ + delay(ms); +} + +void restart() +{ + NVIC_SystemReset(); +} + +bool rtc_ready() +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().isRTCReady(); +} + +BatteryInfo battery_info() +{ + auto& board = ::boards::gat562_mesh_evb_pro::Gat562Board::instance(); + BatteryInfo info{}; + info.available = true; + info.charging = board.isCharging(); + info.level = board.getBatteryLevel(); + return info; +} + +void handle_low_battery(const BatteryInfo& info) +{ + (void)info; +} + +uint8_t default_message_tone_volume() +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().getMessageToneVolume(); +} + +void set_message_tone_volume(uint8_t volume_percent) +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setMessageToneVolume(volume_percent); +} + +void play_message_tone() +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().playMessageTone(); +} + +bool sd_ready() +{ + return false; +} + +bool card_ready() +{ + return false; +} + +bool gps_ready() +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().isGPSReady(); +} + +int power_tier() +{ + return 0; +} + +} // namespace platform::ui::device + +namespace platform::ui::gps +{ + +GpsState get_data() +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().gpsData(); +} + +bool get_gnss_snapshot(GnssSatInfo* out, std::size_t max, std::size_t* out_count, GnssStatus* status) +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().gpsGnssSnapshot(out, max, out_count, status); +} + +uint32_t last_motion_ms() +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().gpsLastMotionMs(); +} + +bool is_enabled() +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().gpsEnabled(); +} + +bool is_powered() +{ + return ::boards::gat562_mesh_evb_pro::Gat562Board::instance().gpsPowered(); +} + +void set_collection_interval(uint32_t interval_ms) +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setGpsCollectionInterval(interval_ms); +} + +void set_power_strategy(uint8_t strategy) +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setGpsPowerStrategy(strategy); +} + +void set_gnss_config(uint8_t mode, uint8_t sat_mask) +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setGpsConfig(mode, sat_mask); +} + +void set_nmea_config(uint8_t output_hz, uint8_t sentence_mask) +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setGpsNmeaConfig(output_hz, sentence_mask); +} + +void set_motion_idle_timeout(uint32_t timeout_ms) +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setGpsMotionIdleTimeout(timeout_ms); +} + +void set_motion_sensor_id(uint8_t sensor_id) +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setGpsMotionSensorId(sensor_id); +} + +void suspend_runtime() +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().suspendGps(); +} + +void resume_runtime() +{ + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().resumeGps(); +} + +double calculate_map_resolution(int zoom, double lat) +{ + constexpr double kEarthCircumferenceM = 40075016.686; + return (kEarthCircumferenceM * std::cos(lat * 3.14159265358979323846 / 180.0)) / + (256.0 * static_cast(1 << zoom)); +} + +} // namespace platform::ui::gps diff --git a/boards/gat562_mesh_evb_pro/src/settings_store.cpp b/boards/gat562_mesh_evb_pro/src/settings_store.cpp new file mode 100644 index 00000000..5d591322 --- /dev/null +++ b/boards/gat562_mesh_evb_pro/src/settings_store.cpp @@ -0,0 +1,97 @@ +#include "boards/gat562_mesh_evb_pro/settings_store.h" + +#include "chat/infra/mesh_protocol_utils.h" +#include "chat/infra/meshcore/mc_region_presets.h" +#include "chat/infra/meshtastic/mt_region.h" + +namespace boards::gat562_mesh_evb_pro::settings_store +{ +namespace +{ + +bool s_has_config = false; +app::AppConfig s_config{}; +uint8_t s_tone_volume = 45; + +int8_t clampTxPower(int8_t value) +{ + if (value < app::AppConfig::kTxPowerMinDbm) + { + return app::AppConfig::kTxPowerMinDbm; + } + if (value > app::AppConfig::kTxPowerMaxDbm) + { + return app::AppConfig::kTxPowerMaxDbm; + } + return value; +} + +} // namespace + +void normalizeConfig(app::AppConfig& config) +{ + if (!chat::infra::isValidMeshProtocol(config.mesh_protocol)) + { + config.mesh_protocol = chat::MeshProtocol::Meshtastic; + } + + if (chat::meshtastic::findRegion( + static_cast(config.meshtastic_config.region)) == nullptr) + { + config.meshtastic_config.region = app::AppConfig::kDefaultRegionCode; + } + + config.meshtastic_config.tx_power = clampTxPower(config.meshtastic_config.tx_power); + config.meshcore_config.tx_power = clampTxPower(config.meshcore_config.tx_power); + + if (chat::meshcore::isValidRegionPresetId(config.meshcore_config.meshcore_region_preset) && + config.meshcore_config.meshcore_region_preset > 0) + { + if (const chat::meshcore::RegionPreset* preset = + chat::meshcore::findRegionPresetById(config.meshcore_config.meshcore_region_preset)) + { + config.meshcore_config.meshcore_freq_mhz = preset->freq_mhz; + config.meshcore_config.meshcore_bw_khz = preset->bw_khz; + config.meshcore_config.meshcore_sf = preset->sf; + config.meshcore_config.meshcore_cr = preset->cr; + } + } + else + { + config.meshcore_config.meshcore_region_preset = 0; + } +} + +bool loadAppConfig(app::AppConfig& config) +{ + if (s_has_config) + { + config = s_config; + normalizeConfig(config); + return true; + } + + normalizeConfig(config); + return false; +} + +bool saveAppConfig(const app::AppConfig& config) +{ + s_config = config; + normalizeConfig(s_config); + s_has_config = true; + return true; +} + +uint8_t loadMessageToneVolume() +{ + return s_tone_volume; +} + +bool saveMessageToneVolume(uint8_t volume) +{ + s_tone_volume = volume; + return true; +} + +} // namespace boards::gat562_mesh_evb_pro::settings_store diff --git a/boards/gat562_mesh_evb_pro/src/sx1262_radio_packet_io.cpp b/boards/gat562_mesh_evb_pro/src/sx1262_radio_packet_io.cpp new file mode 100644 index 00000000..12f2eff3 --- /dev/null +++ b/boards/gat562_mesh_evb_pro/src/sx1262_radio_packet_io.cpp @@ -0,0 +1,344 @@ +#include "boards/gat562_mesh_evb_pro/sx1262_radio_packet_io.h" + +#include "boards/gat562_mesh_evb_pro/gat562_board.h" +#include "boards/gat562_mesh_evb_pro/board_profile.h" +#include "chat/infra/meshcore/mc_region_presets.h" +#include "chat/infra/meshtastic/mt_region.h" + +#include +#include +#include + +#include +#include + +namespace boards::gat562_mesh_evb_pro +{ +namespace +{ + +constexpr uint8_t kMeshtasticSyncWord = 0x2B; +constexpr uint8_t kMeshCoreSyncWord = 0x12; +constexpr uint16_t kDefaultPreambleLen = 16; +constexpr uint8_t kDefaultCrcLen = 2; + +float normalizeBandwidthKhz(float bw_khz) +{ + if (bw_khz == 31.0f) return 31.25f; + if (bw_khz == 62.0f) return 62.5f; + if (bw_khz == 200.0f) return 203.125f; + if (bw_khz == 400.0f) return 406.25f; + if (bw_khz == 800.0f) return 812.5f; + if (bw_khz == 1600.0f) return 1625.0f; + return bw_khz; +} + +void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset preset, + bool wide_lora, + float& bw_khz, + uint8_t& sf, + uint8_t& cr_denom) +{ + switch (preset) + { + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_SLOW: + bw_khz = 125.0f; + sf = 12; + cr_denom = 8; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST: + bw_khz = 250.0f; + sf = 11; + cr_denom = 5; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_SLOW: + bw_khz = 250.0f; + sf = 10; + cr_denom = 8; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST: + bw_khz = 250.0f; + sf = 9; + cr_denom = 5; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW: + bw_khz = 250.0f; + sf = 8; + cr_denom = 8; + break; + case meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST: + default: + bw_khz = wide_lora ? 500.0f : 250.0f; + sf = wide_lora ? 7 : 8; + cr_denom = 5; + break; + } +} + +Sx1262RadioPacketIo::AppliedRadioConfig deriveMeshtasticRadioConfig(const ::chat::MeshConfig& config) +{ + Sx1262RadioPacketIo::AppliedRadioConfig out{}; + auto region_code = static_cast(config.region); + if (region_code == meshtastic_Config_LoRaConfig_RegionCode_UNSET) + { + region_code = meshtastic_Config_LoRaConfig_RegionCode_CN; + } + + const ::chat::meshtastic::RegionInfo* region = ::chat::meshtastic::findRegion(region_code); + if (!region) + { + region = ::chat::meshtastic::findRegion(meshtastic_Config_LoRaConfig_RegionCode_CN); + } + + float bw_khz = 250.0f; + uint8_t sf = 11; + uint8_t cr_denom = 5; + if (config.use_preset && region) + { + modemPresetToParams(static_cast(config.modem_preset), + region->wide_lora, + bw_khz, + sf, + cr_denom); + } + else + { + bw_khz = normalizeBandwidthKhz(config.bandwidth_khz); + sf = std::clamp(config.spread_factor, 5, 12); + cr_denom = std::clamp(config.coding_rate, 5, 8); + if (region) + { + if (bw_khz < 7.8f) bw_khz = 7.8f; + if (!region->wide_lora && bw_khz > 500.0f) bw_khz = 500.0f; + if (region->wide_lora && bw_khz > 1625.0f) bw_khz = 1625.0f; + } + } + + float freq_mhz = ::chat::meshtastic::estimateFrequencyMhz(config.region, config.modem_preset); + if (config.override_frequency_mhz > 0.0f) + { + freq_mhz = config.override_frequency_mhz; + } + freq_mhz += config.frequency_offset_mhz; + + out.freq_mhz = freq_mhz; + out.bw_khz = bw_khz; + out.sf = sf; + out.cr = cr_denom; + out.tx_power = std::clamp(config.tx_power == 0 ? 17 : config.tx_power, -9, 20); + if (region && region->power_limit_dbm > 0 && out.tx_power > static_cast(region->power_limit_dbm)) + { + out.tx_power = static_cast(region->power_limit_dbm); + } + out.preamble_len = kDefaultPreambleLen; + out.sync_word = kMeshtasticSyncWord; + out.crc_len = kDefaultCrcLen; + return out; +} + +Sx1262RadioPacketIo::AppliedRadioConfig deriveMeshCoreRadioConfig(const ::chat::MeshConfig& config) +{ + Sx1262RadioPacketIo::AppliedRadioConfig out{}; + float freq_mhz = config.meshcore_freq_mhz; + float bw_khz = config.meshcore_bw_khz; + uint8_t sf = config.meshcore_sf; + uint8_t cr = config.meshcore_cr; + + if (config.meshcore_region_preset > 0) + { + if (const auto* preset = ::chat::meshcore::findRegionPresetById(config.meshcore_region_preset)) + { + freq_mhz = preset->freq_mhz; + bw_khz = preset->bw_khz; + sf = preset->sf; + cr = preset->cr; + } + } + + out.freq_mhz = std::clamp(freq_mhz, 400.0f, 2500.0f); + out.bw_khz = std::clamp(normalizeBandwidthKhz(bw_khz), 7.8f, 500.0f); + out.sf = std::clamp(sf, 5, 12); + out.cr = std::clamp(cr, 5, 8); + out.tx_power = std::clamp(config.tx_power, -9, 20); + out.preamble_len = kDefaultPreambleLen; + out.sync_word = kMeshCoreSyncWord; + out.crc_len = kDefaultCrcLen; + return out; +} + +} // namespace + +Sx1262RadioPacketIo::Sx1262RadioPacketIo() = default; +Sx1262RadioPacketIo::~Sx1262RadioPacketIo() = default; + +bool Sx1262RadioPacketIo::begin() +{ + if (initialized_) + { + return radio_online_; + } + + initialized_ = true; + const auto& profile = ::boards::gat562_mesh_evb_pro::kBoardProfile; + auto& board = ::boards::gat562_mesh_evb_pro::Gat562Board::instance(); + (void)board.begin(); + delay(10); + (void)board.prepareRadioHardware(); + module_.reset(new Module(profile.lora.spi.cs, + profile.lora.dio1, + profile.lora.reset, + profile.lora.busy)); + radio_.reset(new SX1262(module_.get())); + + radio_online_ = initializeRadioChip(); + if (radio_online_) + { + applyConfig(active_protocol_, active_config_); + } + return radio_online_; +} + +void Sx1262RadioPacketIo::applyConfig(::chat::MeshProtocol protocol, const ::chat::MeshConfig& config) +{ + active_protocol_ = protocol; + active_config_ = config; + if (!radio_online_ || !radio_) + { + return; + } + + (void)applyRadioConfig(deriveRadioConfig(protocol, config)); +} + +bool Sx1262RadioPacketIo::transmit(const uint8_t* data, size_t size) +{ + if (!radio_online_ || !radio_ || !data || size == 0) + { + return false; + } + + receiving_ = false; + const int state = radio_->transmit(data, size); + if (state != RADIOLIB_ERR_NONE) + { + return false; + } + return enterReceiveMode(); +} + +bool Sx1262RadioPacketIo::pollReceive(platform::nrf52::arduino_common::chat::infra::RadioPacket* out_packet) +{ + if (!radio_online_ || !radio_ || !out_packet) + { + return false; + } + + if (!receiving_) + { + (void)enterReceiveMode(); + return false; + } + + const auto& profile = ::boards::gat562_mesh_evb_pro::kBoardProfile; + if (profile.lora.dio1 < 0 || digitalRead(profile.lora.dio1) == LOW) + { + return false; + } + + const uint32_t irq = radio_->getIrqFlags(); + if ((irq & RADIOLIB_SX126X_IRQ_RX_DONE) == 0) + { + if (irq & RADIOLIB_SX126X_IRQ_TIMEOUT) + { + (void)radio_->finishReceive(); + (void)enterReceiveMode(); + } + return false; + } + + const size_t packet_len = radio_->getPacketLength(); + if (packet_len <= 0 || packet_len > static_cast(sizeof(out_packet->data))) + { + (void)radio_->finishReceive(); + (void)enterReceiveMode(); + return false; + } + + const int state = radio_->readData(out_packet->data, packet_len); + out_packet->size = ((state == RADIOLIB_ERR_NONE || state == RADIOLIB_ERR_CRC_MISMATCH) && packet_len > 0) + ? packet_len + : 0; + out_packet->rx_meta.rssi_dbm_x10 = static_cast(radio_->getRSSI() * 10.0f); + out_packet->rx_meta.snr_db_x10 = static_cast(radio_->getSNR() * 10.0f); + + (void)radio_->finishReceive(); + (void)enterReceiveMode(); + return out_packet->size > 0; +} + +bool Sx1262RadioPacketIo::initializeRadioChip() +{ + if (!radio_) + { + return false; + } + + const int state = radio_->begin(); + if (state != RADIOLIB_ERR_NONE) + { + return false; + } + + radio_->setDio2AsRfSwitch(true); + radio_->setTCXO(1.8f); + radio_->setCurrentLimit(140.0f); + return true; +} + +bool Sx1262RadioPacketIo::enterReceiveMode() +{ + if (!radio_online_ || !radio_) + { + return false; + } + receiving_ = (radio_->startReceive() == RADIOLIB_ERR_NONE); + return receiving_; +} + +bool Sx1262RadioPacketIo::applyRadioConfig(const AppliedRadioConfig& config) +{ + if (!radio_) + { + return false; + } + + if (radio_->setFrequency(config.freq_mhz) != RADIOLIB_ERR_NONE) return false; + if (radio_->setBandwidth(config.bw_khz) != RADIOLIB_ERR_NONE) return false; + if (radio_->setSpreadingFactor(config.sf) != RADIOLIB_ERR_NONE) return false; + if (radio_->setCodingRate(config.cr) != RADIOLIB_ERR_NONE) return false; + if (radio_->setOutputPower(config.tx_power) != RADIOLIB_ERR_NONE) return false; + if (radio_->setPreambleLength(config.preamble_len) != RADIOLIB_ERR_NONE) return false; + if (radio_->setSyncWord(config.sync_word) != RADIOLIB_ERR_NONE) return false; + if (radio_->setCRC(config.crc_len) != RADIOLIB_ERR_NONE) return false; + + applied_config_ = config; + applied_freq_hz_ = static_cast(config.freq_mhz * 1000000.0f + 0.5f); + applied_bw_hz_ = static_cast(config.bw_khz * 1000.0f + 0.5f); + return enterReceiveMode(); +} + +Sx1262RadioPacketIo::AppliedRadioConfig Sx1262RadioPacketIo::deriveRadioConfig(::chat::MeshProtocol protocol, + const ::chat::MeshConfig& config) const +{ + return protocol == ::chat::MeshProtocol::MeshCore + ? deriveMeshCoreRadioConfig(config) + : deriveMeshtasticRadioConfig(config); +} + +Sx1262RadioPacketIo& sx1262RadioPacketIo() +{ + static Sx1262RadioPacketIo io; + return io; +} + +} // namespace boards::gat562_mesh_evb_pro diff --git a/docs/Best Practices/GAT562_IMPLEMENTATION_MAP.md b/docs/Best Practices/GAT562_IMPLEMENTATION_MAP.md new file mode 100644 index 00000000..ada6d067 --- /dev/null +++ b/docs/Best Practices/GAT562_IMPLEMENTATION_MAP.md @@ -0,0 +1,314 @@ +# GAT562 实现映射 + +本文件用于把 `gat562_mesh_evb_pro` 的实现è½ç‚¹ã€åˆ†å±‚归属和当å‰å®Œæˆçжæ€å†™æ¸…楚。 + +æƒå¨çº¦æŸæ¥æºï¼š +- `.tmp/meshtastic-firmware` 中 GAT562 硬件å‚考 +- `docs/Best Practices/GAT562_REQUIREMENTS.md` +- `docs/Best Practices/new_hardware_adaptation_prompt.md` + +--- + +## 1. æ¿çº§äº‹å®ž + +è½ç‚¹ï¼š +- `boards/gat562_mesh_evb_pro.json` +- `boards/gat562_mesh_evb_pro/include/boards/gat562_mesh_evb_pro/board_profile.h` +- `variants/gat562_mesh_evb_pro/variant.h` +- `variants/gat562_mesh_evb_pro/variant.cpp` + +承载内容: +- nRF52840 / S140 / Flash / RAM / bootloader çº¦æŸ +- OLED / LoRa / GNSS / LED / 按键 / 电池 / 3V3 供电引脚 +- 产å“è¾¹ç•Œï¼šæ”¯æŒ `Meshtastic / MeshCore / BLE / LoRa / GNSS` +- 产å“è¾¹ç•Œï¼šä¸æ”¯æŒ `Team / HostLink / SD / CJK / 拼音 IME` + +原则: +- æ¿çº§å‚æ•°åªæ”¾åœ¨ `boards/` å’Œ `variants/` +- 业务层ä¸ç›´æŽ¥å†™æ­»å¼•脚和硬件事实 + +--- + +## 2. 环境定义 + +è½ç‚¹ï¼š +- `variants/gat562_mesh_evb_pro/envs/gat562_mesh_evb_pro.ini` + +承载内容: +- GAT562 独立编译环境 +- nRF52 include path / generated protobuf include path +- `RadioLib / TinyGPSPlus / nanopb` ä¾èµ– +- GAT562 边界è£å‰ªï¼šæŽ’除 `Team / HostLink / USB/PC Link / CJK 字库 / 拼音 IME` + +原则: +- GAT562 的编译è£å‰ªåœ¨çŽ¯å¢ƒå±‚å®Œæˆ +- ä¸ç”¨åœ¨å…±äº«æ¨¡å—里到处写æ¿çº§ ifdef 兜底 + +--- + +## 3. 共享身份与自宣告 + +è½ç‚¹ï¼š +- `modules/core_chat/include/chat/runtime/self_identity_provider.h` +- `modules/core_chat/include/chat/runtime/self_identity_policy.h` +- `modules/core_chat/src/runtime/self_identity_policy.cpp` +- `modules/core_chat/include/chat/runtime/self_announcement_core.h` +- `modules/core_chat/include/chat/runtime/meshtastic_self_announcement_core.h` +- `modules/core_chat/src/runtime/meshtastic_self_announcement_core.cpp` +- `modules/core_chat/include/chat/runtime/meshcore_self_announcement_core.h` +- `modules/core_chat/src/runtime/meshcore_self_announcement_core.cpp` + +承载内容: +- `long name / short name / node id / BLE åç§°` 的统一派生规则 +- Meshtastic NodeInfo 组包 +- MeshCore identity advert 组包 + +原则: +- 身份派生规则属于共享业务,ä¸å±žäºŽ app +- 空å£è‡ªå®£å‘Šè§„则属于共享业务,ä¸å±žäºŽæ¿çº§ runtime + +--- + +## 4. Shared app 容器去 Team 强ä¾èµ– + +è½ç‚¹ï¼š +- `modules/core_sys/include/app/app_context_platform_bindings.h` +- `apps/esp_pio/src/app_context.cpp` + +承载内容: +- `create_team_services` ä¸å†æ˜¯ app context æœ‰æ•ˆæ€§çš„ç¡¬è¦æ±‚ +- GAT562 å¯ä»¥ä½œä¸ºçœŸå®žçš„ “无 Team è®¾å¤‡â€ æŽ¥å…¥ï¼Œè€Œä¸æ˜¯ä¼ªé€  Team 壳 + +--- + +## 5. nRF52 平尿¡¥æŽ¥ä¸ŽæŒä¹…化 + +è½ç‚¹ï¼š +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/device_identity.h` +- `platform/nrf52/arduino_common/src/device_identity.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/settings_runtime.h` +- `platform/nrf52/arduino_common/src/settings_runtime.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/self_identity_bridge.h` +- `platform/nrf52/arduino_common/src/self_identity_bridge.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/app_config_store.h` +- `platform/nrf52/arduino_common/src/app_config_store.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/blob_file_store.h` +- `platform/nrf52/arduino_common/src/chat/infra/blob_file_store.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/contact_store.h` +- `platform/nrf52/arduino_common/src/chat/infra/contact_store.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h` +- `platform/nrf52/arduino_common/src/chat/infra/meshtastic/node_store.cpp` + +承载内容: +- 从 `NRF_FICR->DEVICEADDR` 派生 node id / MAC +- 统一规范化 Meshtastic / MeshCore é…ç½® +- `InternalFS` 上的 app config / 节点 / è”系人存储 +- æ¶ˆæ¯æç¤ºéŸ³éŸ³é‡æŒä¹…åŒ–å…¥å£ +- ä¿å­˜é…ç½®åŽç›´æŽ¥å›žæŽ¨ radio / BLE / GNSS / identity è¿è¡Œæ€ + +原则: +- å¹³å°å±‚负责“读真值â€å’Œâ€œè½ç£ç›˜â€ +- å…±äº«å±‚åªæ¶ˆè´¹æŠ½è±¡ç»“æžœ + +--- + +## 6. nRF52 LoRa 传输层 + +è½ç‚¹ï¼š +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/radio_packet_io.h` +- `platform/nrf52/arduino_common/src/chat/infra/radio_packet_io.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/sx1262_radio_packet_io.h` +- `platform/nrf52/arduino_common/src/chat/infra/sx1262_radio_packet_io.cpp` + +承载内容: +- `IRadioPacketIo` 抽象 +- GAT562 SX1262 + RadioLib 实现 +- 3V3 / radio power rail / SPI / Radio chip åˆå§‹åŒ– +- Meshtastic / MeshCore 两套 radio 傿•°åº”用 +- TX åŽå›žåˆ° RX,RX æ—¶å¡«å…… `RSSI / SNR / freq / bw / sf / cr` +- 频率字符串格å¼åŒ–能力在æ¿çº§ runtime æä¾›ï¼ŒåŽç»­å±ä¿é¡µç›´æŽ¥æ¶ˆè´¹ + +原则: +- radio çœŸå®žå‚æ•°ç”Ÿæ•ˆå±žäºŽå¹³å°å±‚ +- app åªè´Ÿè´£æŠŠå½“å‰åè®®é…置下å‘给平å°å±‚ + +--- + +## 7. nRF52 å议适é…器 + +è½ç‚¹ï¼š +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/protocol_factory.h` +- `platform/nrf52/arduino_common/src/chat/infra/protocol_factory.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/mt_adapter_lite.h` +- `platform/nrf52/arduino_common/src/chat/infra/meshtastic/mt_adapter_lite.cpp` +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_adapter_lite.h` +- `platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_adapter_lite.cpp` + +承载内容: +- Meshtastic Lite Adapter:文本ã€AppDataã€NodeInfoã€è‡ªå®£å‘Š +- MeshCore Lite Adapter:文本/AppData 转å‘ã€identity advertã€è‡ªå®£å‘Š +- Meshtastic 收包按 `channel_hash -> key` 解密,ä¸å†åªæ”¯æŒæ—  PSK + +原则: +- å议逻辑尽é‡å¤ç”¨ `modules/core_chat` +- å¹³å°é€‚é…器åªè¡¥ transport / runtime glue + +--- + +## 8. nRF52 GNSS / Device runtime + +è½ç‚¹ï¼š +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/gps_runtime.h` +- `platform/nrf52/arduino_common/src/platform_ui_gps_runtime.cpp` +- `platform/nrf52/arduino_common/src/platform_ui_device_runtime.cpp` +- `platform/nrf52/arduino_common/src/platform_ui_time_runtime.cpp` + +承载内容: +- `platform::ui::gps::*` çš„ nRF52 实现 +- `platform::ui::device::*` çš„ nRF52 实现 +- UART GNSS è¾“å…¥è§£æž +- åŸºç¡€ç”µæ± ç™¾åˆ†æ¯”è¯»å– +- GPS æ ¡æ—¶å…¥å£ +- 本地时区åç§»æŒä¹…化与本地时间æ¢ç®— +- 统一走 `board_runtime` å®Œæˆ 3V3 rail / LED / 输入引脚åˆå§‹åŒ– + +原则: +- UI è¯»çš„æ˜¯å…±äº«å¹³å° runtime æŽ¥å£ +- 具体串å£ã€ADCã€RTC 判定都归平å°å±‚ + +--- + +## 9. nRF52 BLE runtime + +è½ç‚¹ï¼š +- `platform/nrf52/arduino_common/include/ble/ble_manager.h` +- `platform/nrf52/arduino_common/src/ble/ble_manager.cpp` + +承载内容: +- Bluefruit 基础广播/è¿žæŽ¥å…¥å£ +- æ ¹æ®å½“å‰å议切æ¢å¹¿æ’­æœåŠ¡ UUID +- BLE å称统一使用 `modules/core_chat` 的共享身份派生规则 + +当å‰çжæ€ï¼š +- 已建立 nRF52 ä¾§ BLE manager 边界和基础广播能力 +- 还需è¦ç»§ç»­è¡¥é½ Meshtastic / MeshCore 的完整手机侧åè®®æœåŠ¡ + +--- + +## 9.5 nRF52 æ¿çº§ runtime + +è½ç‚¹ï¼š +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/board_runtime.h` +- `platform/nrf52/arduino_common/src/board_runtime.cpp` + +承载内容: +- 3V3 rail åˆå§‹åŒ– +- çŠ¶æ€ LED / æç¤º LED 控制 +- GAT562 摇æ†ä¸ŽæŒ‰é”®åŽŸå§‹è¾“å…¥å¿«ç…§ +- LoRa 频率字符串格å¼åŒ– + +原则: +- ç”µæº rail / LED / GPIO è¾“å…¥äº‹å®žå½’å¹³å°æ¿çº§ runtime +- LoRa / GPS / UI ä¸å†å„自散è½åˆå§‹åŒ–相åŒçš„æ¿çº§å¼•è„š + +--- + +## 9.6 nRF52 输入 runtime + +è½ç‚¹ï¼š +- `platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/input_runtime.h` +- `platform/nrf52/arduino_common/src/input_runtime.cpp` + +承载内容: +- æ‘‡æ† / 按键的去抖事件 +- 最近活动时间戳 +- 当å‰åŽŸå§‹è¾“å…¥å¿«ç…§ + +原则: +- 原始 GPIO -> 坿¶ˆè´¹è¾“入事件的转æ¢å½’å¹³å° runtime +- UI ä¸ç›´æŽ¥è½®è¯¢æ•£è½ GPIO + +--- + +## 10. GAT562 app 装é…层 + +è½ç‚¹ï¼š +- `apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/app_facade_runtime.h` +- `apps/gat562_mesh_evb_pro/src/app_facade_runtime.cpp` +- `apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/app_runtime_access.h` +- `apps/gat562_mesh_evb_pro/src/app_runtime_access.cpp` +- `apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/startup_runtime.h` +- `apps/gat562_mesh_evb_pro/src/startup_runtime.cpp` +- `apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/loop_runtime.h` +- `apps/gat562_mesh_evb_pro/src/loop_runtime.cpp` +- `apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/arduino_entry.h` +- `apps/gat562_mesh_evb_pro/src/arduino_entry.cpp` +- `apps/gat562_mesh_evb_pro/include/apps/gat562_mesh_evb_pro/ui_runtime.h` +- `apps/gat562_mesh_evb_pro/src/ui_runtime.cpp` +- `src/main.cpp` + +承载内容: +- GAT562 独立 app facade,ä¸å†å¤ç”¨ ESP å置的 `AppContext` +- å¯åŠ¨æ—¶è£…é…: + - app config + - identity bridge + - node/contact store + - protocol adapters + - BLE manager + - SX1262 radio packet io + - GPS runtime +- loop 中驱动: + - GPS tick + - Mono OLED UI tick + - 输入事件 -> UI 动作映射 + - raw packet -> active adapter + - chat core update + - BLE update + - event dispatch + +原则: +- `apps/` åªè´Ÿè´£ assemble,ä¸è´Ÿè´£å‘明共享业务 + +--- + +## 11. 与需求文档的对应关系 + +å·²ç»è¦†ç›–的主线: +- `User Name / Short Name` -> 本机身份 / BLE åç§° / LoRa 自宣告è”动 +- GAT562 真实 `no-Team / no-HostLink / no-SD / no-CJK` +- LoRa 傿•°ç”± settings 真正驱动 radio é…ç½® +- GNSS / battery / device runtime ä¸å†æ˜¯å‡å…¥å£ +- GAT562 å•色 UI 已有独立 shared module 与 nRF52 backend +- å¯åŠ¨æ—¥å¿— / å±ä¿ / 主èœå• / ä¼šè¯ / 文本输入 / 身份/无线/设备/GNSS/动作页 已有明确è½ç‚¹ +- MeshCore Lite ä¸å†åªè®¤ advert,已能把 direct/group app payload å…¥ç«™æˆ `AppData` +- Meshtastic Lite ä¸å†åªè®¤æ–‡æœ¬ï¼Œå·²èƒ½æŠŠéžæ–‡æœ¬ `AppData` å…¥ç«™æˆ `AppData` + +ä»éœ€ç»§ç»­è¡¥é½çš„主线: +- Meshtastic / MeshCore 完整 BLE 手机åè®®æœåŠ¡ +- æ›´å®Œæ•´çš„æ¶ˆæ¯æŽ¥æ”¶/ACK/控制类业务 +- å•色 UI 的编译与实机è”调闭环 + +--- + +## 12. 分层结论 + +本轮确定下æ¥çš„长期结构: +- `modules/`:共享业务ã€åè®®ã€è‡ªå®£å‘Šã€èº«ä»½ç­–ç•¥ +- `platform/nrf52/`:nRF52 transport / BLE / GNSS / FS / device runtime +- `boards/gat562_mesh_evb_pro/`:æ¿çº§äº‹å®žä¸Žè¾¹ç•Œ +- `apps/gat562_mesh_evb_pro/`:设备å¯åŠ¨ä¸Žè£…é… +- `variants/gat562_mesh_evb_pro/`:编译环境与能力è£å‰ª + +这也是åŽç»­ç»§ç»­è¡¥é½ GAT562ã€ä»¥åŠé€‚é…æ›´å¤šæ–°ç¡¬ä»¶æ—¶çš„æ ‡å‡†è½ç‚¹ã€‚ + +## Board Ownership Update + +- `boards/gat562_mesh_evb_pro/src/gat562_board.cpp` is now the single board owner for GAT562 board-specific hardware coordination. +- Board-owned concerns include: 3V3 power rail, GPIO input mapping, OLED/I2C access, GPS UART bring-up, board default identity, and LoRa radio binding/config application. +- `apps/gat562_mesh_evb_pro/*` remains composition-only: it wires board, shared platform BLE, shared protocol adapters, and shared UI together. +- GAT562 app code must not keep a second board-specific LoRa/GPIO/bus/power owner outside `Gat562Board`. +- Shared BLE logic should stay in `platform/nrf52/arduino_common`, with board-specific prerequisites absorbed by `Gat562Board`. +- BLE ownership rule update: `apps/gat562_mesh_evb_pro` no longer keeps a board-specific `ble_owner` layer. The app composes the shared `platform/nrf52` `BleManager` directly, while board-specific defaults/prerequisites stay in `Gat562Board`. +- GPS startup rule update: app startup should call a single board-facing GPS runtime entry, rather than splitting UART bring-up and config application across multiple app/runtime locations. +- Radio hardware ownership update: GAT562 LoRa rail enable and SPI bus bring-up are board-owned concerns and now live behind `Gat562Board::prepareRadioHardware()`. `sx1262_radio_packet_io` should only perform chip-level RadioLib initialization and packet I/O. +- I2C ownership update: the former standalone `i2c_bus` helper is now folded into `Gat562Board`. Shared OLED/I2C access and the RAII lock live behind `Gat562Board::i2cWire()` and `Gat562Board::I2cGuard`, keeping board bus ownership at a single entry point. diff --git a/docs/Best Practices/GAT562_REQUIREMENTS.md b/docs/Best Practices/GAT562_REQUIREMENTS.md new file mode 100644 index 00000000..345ada31 --- /dev/null +++ b/docs/Best Practices/GAT562_REQUIREMENTS.md @@ -0,0 +1,643 @@ +# GAT562 需求文档 + +## 1. 文档目的 + +本文档定义 `gat562_mesh_evb_pro` 固件的产å“目标ã€èƒ½åŠ›è¾¹ç•Œã€åŠŸèƒ½éœ€æ±‚ã€éžåŠŸèƒ½è¦æ±‚与验收标准。 + +ç›®æ ‡ä¸æ˜¯åšâ€œèƒ½ç¼–译的演示固件â€ï¼Œè€Œæ˜¯åšä¸€ä¸ªï¼š + +- 能稳定å¯åЍ +- 能稳定交互 +- 能真实承接 LoRa / BLE / GNSS / 本地设置 +- 能作为åŽç»­æ›´å¤šç¡¬ä»¶é€‚é…å‚è€ƒæ¨¡æ¿ + +的正å¼ç¡¬ä»¶çŽ¯å¢ƒã€‚ + +--- + +## 2. 产å“å®šä½ + +`GAT562` 是一个: + +- å°å±è®¾å¤‡ +- è¾“å…¥èƒ½åŠ›æœ‰é™ +- 无触摸 +- 无全键盘 +- 无中文输入 +- 无大å±å¤æ‚ UI +- 但具备真实无线通信与身份能力 + +的手æŒèŠ‚ç‚¹ã€‚ + +å®ƒçš„æ ¸å¿ƒä»·å€¼ä¸æ˜¯å¤æ‚交互,而是: + +- 作为真实 Meshtastic / MeshCore åŒå议节点 +- 作为 BLE 手机é…对与é…ç½®å…¥å£ +- 作为 LoRa 空å£èŠ‚ç‚¹ +- 作为具备基础本地 UI 的独立设备 + +--- + +## 3. 总体目标 + +`gat562_mesh_evb_pro` 固件必须满足以下总体目标: + +1. å¯åŠ¨ç¨³å®šï¼Œä¸å¾—éšæœºå¡æ­»ã€å¡å±ã€å¡åœ¨æŸä¸ªåˆå§‹åŒ–阶段 +2. 主循环稳定,ä¸å¾—å›  UI / LoRa / BLE / GNSS / 文件系统任务互相抢å è€Œå¤±æ´» +3. UI 䏿˜¯ç©ºå£³ï¼Œé¡µé¢å…¥å£å¿…须对应真实能力 +4. Settings ä¸­çš„å¯æ”¹é¡¹å¿…é¡»çœŸå®žç”Ÿæ•ˆï¼Œä¸”å¯æŒä¹…化è½ç›˜ +5. LoRaã€BLEã€GNSSã€èº«ä»½å¹¿æ’­å¿…é¡»æ˜¯å®žé™…ä¸šåŠ¡èƒ½åŠ›ï¼Œä¸æ˜¯ mock / no-op +6. ä¿æŒ `GAT562` 自身边界清晰,ä¸å¼•å…¥ä¸é€‚åˆè¯¥ç¡¬ä»¶å½¢æ€çš„功能 + +--- + +## 4. 产å“边界 + +### 4.1 必须支æŒçš„能力 + +- Meshtastic 真实能力 +- MeshCore 真实能力 +- BLE 手机连接与基本é…置能力 +- LoRa æ”¶å‘能力 +- GNSS 基础能力 +- 本机身份显示 +- æœ¬æœºè®¾ç½®ç®¡ç† +- å±ä¿ / 主èœå• / å…³é”®ä¸šåŠ¡é¡µé¢ + +### 4.2 æ˜Žç¡®ä¸æ”¯æŒçš„能力 + +以下能力ä¸å±žäºŽ `GAT562` 范围,åŽç»­å®žçŽ°æ—¶å¿…é¡»æŽ’é™¤ï¼š + +- Team 相关全部能力 +- HostLink / PC Link +- SD / Card 能力 +- 中文输入法 +- 拼音输入 +- 中文字库 / CJK 字库路径 +- 夿‚大å±é¡µé¢ +- ä¸é€‚åˆæœ‰é™è¾“入设备的é‡äº¤äº’æµç¨‹ + +### 4.3 输入边界 + +`GAT562` åªéœ€è¦æ”¯æŒï¼š + +- 英文 +- æ•°å­— +- ç¬¦å· + +ä¸éœ€è¦æ”¯æŒï¼š + +- 中文输入 +- 拼音候选 +- 手写 / 触摸输入 + +--- + +## 5. 架构原则 + +### 5.1 基本原则 + +åŽç»­æ‰€æœ‰å®žçŽ°å¿…é¡»éµå®ˆä»¥ä¸‹åŽŸåˆ™ï¼š + +- 行为优先,抽象第二 +- 稳定环境优先,共享模å—第二 +- å…ˆä¿è¯æ¿çº§è¿è¡Œé“¾å¯é ï¼Œå†åšè·¨å¹³å°æŠ½è±¡ +- ä¸å…è®¸ä¸€æ¬¡åŒæ—¶æ”¹ owner / lifecycle / startup order / shared boundary + +### 5.2 模å—边界原则 + +å…±äº«æ¨¡å— `modules/*` åº”åªæ‰¿è½½ï¼š + +- å议逻辑 +- 纯业务逻辑 +- 与具体硬件无关的 runtime core + +æ¿çº§æˆ–å¹³å°å±‚应承载: + +- 引脚 +- ä¸Šç”µé¡ºåº +- 总线åˆå§‹åŒ– +- 芯片åè°ƒ +- 设备生命周期 +- ä¸­æ–­ä¸Žè½®è¯¢èŠ‚å¥ +- 与具体外设绑定的 host/context/runtime + +### 5.3 硬件适é…原则 + +新增或改造硬件环境时: + +- ä¸å…许把一个ä¸ç¨³å®šæ¿çº§çŽ¯å¢ƒå½“æˆå…±äº«æŠ½è±¡éªŒè¯åŸºå‡† +- 必须先有一个稳定å‚考环境 +- 对 `GAT562` 的改动ä¸å¾—顺手破å ESP 环境 +- 对 shared modules 的改动必须先验è¯è‡³å°‘一个 ESP 环境å¯ç¼–译通过 + +--- + +## 6. å¯åŠ¨ä¸Žè¿è¡Œæ—¶éœ€æ±‚ + +### 6.1 å¯åŠ¨éœ€æ±‚ + +设备上电åŽåº”完æˆä»¥ä¸‹æµç¨‹ï¼š + +1. 基础硬件上电 +2. 显示å¯å·¥ä½œ +3. å¯åŠ¨æ—¥å¿—å¯è§ +4. æ¿çº§åˆå§‹åŒ–便¬¡æŽ¨è¿› +5. æˆåŠŸè¿›å…¥å±ä¿é¡µæˆ–ä¸»ç•Œé¢ + +ä¸å¾—出现: + +- å¡åœ¨ `lora ok` +- å¡åœ¨ `gnss ok` +- å¡åœ¨å±ä¿é¡µä½†æ—¶é—´ä¸æ›´æ–° +- å¡åœ¨å±ä¿é¡µä¸”æ‘‡æ†æ— å“应 +- 进入æŸé¡µåŽä¸»å¾ªçŽ¯åœæ‘† + +### 6.2 å¯åŠ¨æ—¥å¿—éœ€æ±‚ + +å¯åŠ¨æ—¥å¿—å¿…é¡»ï¼š + +- èƒ½åæ˜ å½“å‰åˆå§‹åŒ–阶段 +- 能帮助判断å¡åœ¨å“ªä¸ªæ¨¡å— +- ä¸ä¾èµ– logo æ‰å¯è§‚察 + +用户åå¥½è¦æ±‚: + +- 开机åªä¿ç•™æ»šåŠ¨æ—¥å¿— +- ä¸è¦æ˜¾ç¤ºä¸ç›¸å…³ logo + +### 6.3 主循环需求 + +主循环必须æŒç»­è¿è¡Œï¼Œä¸”èƒ½åŒæ—¶æ”¯æ’‘: + +- board runtime +- input poll +- UI 刷新 +- LoRa poll +- BLE poll +- GNSS poll +- 设置 / 状æ€ç»´æŠ¤ + +主循环ä¸å¾—因为任一模å—: + +- 长时间阻塞 +- æ­»é” +- 饥饿 +- å æ»¡æ€»çº¿ + +而导致全局失活。 + +### 6.4 IC å调需求 + +必须显å¼è€ƒè™‘以下芯片与总线å调: + +- OLED / I2C +- GNSS / UART +- LoRa / SPI +- BLE / SoftDevice / Bluefruit +- InternalFS / Flash +- 输入 GPIO +- LED GPIO + +è¦æ±‚: + +- 引脚用途明确,ä¸å…è®¸å†²çª +- åˆå§‹åŒ–é¡ºåºæ˜Žç¡® +- 轮询频率åˆç† +- é¿å… I2C / SPI / Flash 与 UI 渲染形æˆé•¿æœŸé˜»å¡ž + +--- + +## 7. UI 需求 + +### 7.1 UI æ€»ä½“è¦æ±‚ + +UI 必须是“有é™ä½†çœŸå®žâ€çš„ UI: + +- 页颿•°é‡å¯å°‘ +- 页é¢èƒ½åŠ›ä¸èƒ½ç©º +- å…¥å£å¿…须能走通 +- 页é¢èƒŒåŽå¿…须有真实能力承接 + +### 7.2 å¯åЍ页 + +è¦æ±‚: + +- æ”¯æŒæ»šåŠ¨æ—¥å¿— +- 䏿˜¾ç¤ºå¤šä½™ logo +- å¯åŠ¨å®ŒæˆåŽè‡ªåŠ¨è½¬å…¥å±ä¿ + +### 7.3 å±ä¿é¡µ + +å±ä¿é¡µå¿…须支æŒï¼š + +- 当剿—¶é—´æ˜¾ç¤º +- æ—¶é—´æŒç»­æ›´æ–° +- 年月日与星期显示 +- é¡¶æ å·¦ä¾§æ˜¾ç¤ºå议简写:`mt` / `mc` +- é¡¶æ å³ä¾§æ˜¾ç¤ºå½“å‰ LoRa 频率,带å•ä½ `MHz` +- 频率显示必须正确处ç†å°æ•°ï¼Œä¾‹å¦‚ `478.875MHz` +- é¡¶éƒ¨æ¨ªçº¿é€šæ æ˜¾ç¤º + +äº¤äº’è¦æ±‚: + +- 从å±ä¿é¡µå¯é€šè¿‡æ‘‡æ†è¿›å…¥ä¸»èœå• +- ä¸å¾—塿­» +- ä¸å¾—æ—¶é—´åœä½ + +### 7.4 主èœå• + +è¦æ±‚: + +- å¯ä»Žå±ä¿é¡µè¿›å…¥ +- å„èœå•入壿œ‰æ•ˆ +- æ— æ­»å…¥å£ +- ä¸å¾—进入 no-op 页é¢åŽæ²¡æœ‰å馈 + +### 7.5 Settings é¡µé¢ + +è¦æ±‚: + +- 从主èœå•å¯è¿›å…¥ +- 分类清晰 +- 所有展示项è¦ä¹ˆçœŸå®žç”Ÿæ•ˆï¼Œè¦ä¹ˆæ˜Žç¡®ä¸åœ¨æœ¬ç¡¬ä»¶èŒƒå›´å†…å¹¶éšè— +- ä¸å…许放大é‡ç©ºå£³é¡¹ + +### 7.6 字体与排版 + +用户åå¥½è¦æ±‚: + +- 页é¢å¤´éƒ¨æ ‡é¢˜å­—体尽é‡å° +- 行与行之间间隔尽é‡å° +- å°å±ä¿¡æ¯å¯†åº¦ä¼˜å…ˆ + +--- + +## 8. Settings 功能需求 + +## 8.1 System ç±» + +System 中æ¯ä¸ªè®¾ç½®é¡¹éƒ½å¿…须满足: + +- æœ‰æ˜Žç¡®æ¥æº +- 有真实当å‰å€¼ +- 改动åŽå¯ç”Ÿæ•ˆ +- å¯è½ç›˜ + +ä¸å…è®¸åªæ˜¯å±•ç¤ºé™æ€å ä½æ–‡æ¡ˆã€‚ + +## 8.2 Chat ç±» + +Chat 设置必须按“真实接入,ä¸åšç©ºå£³â€çš„è¦æ±‚实现。 + +è¦æ±‚覆盖以下能力: + +1. Protocol +2. TX +3. Region +4. Preset +5. Channel +6. User Name +7. Short Name +8. PSK +9. Encrypt + +### 8.2.1 Protocol + +è¦æ±‚: + +- å¯åˆ‡æ¢å½“å‰èŠå¤©åè®® +- 至少覆盖 Meshtastic / MeshCore +- 切æ¢åŽ UI / runtime / LoRa 使用的å议一致 + +### 8.2.2 TX + +è¦æ±‚: + +- å¯é…ç½®å‘å°„ç›¸å…³å‚æ•° +- é…置项必须真实作用于 LoRa é…ç½® + +### 8.2.3 Region / Preset / Channel + +è¦æ±‚: + +- 必须真实驱动无线é…ç½® +- 改动åŽå½±å“当å‰åè®®æ— çº¿å‚æ•° +- 显示值与实际生效值一致 + +### 8.2.4 User Name / Short Name + +è¦æ±‚: + +- 䏿˜¯åªæ”¹ BLE åç§° +- å¿…é¡»è”动本机显示文案 +- å¿…é¡»è”动 Meshtastic nodeinfo +- å¿…é¡»è”动 MeshCore 身份相关展示与空å£å¹¿æ’­ + +å³ï¼š + +- 本机 UI 看è§çš„身份 +- BLE 广播/设备åç§° +- LoRa 空å£ä¸­çš„èº«ä»½ä¿¡æ¯ + +必须一致或按明确规则派生。 + +### 8.2.5 PSK / Encrypt + +è¦æ±‚: + +- ä¸èƒ½åªæ˜¯ UI 勾选项 +- 必须真实影å“åè®®æ”¶å‘ +- å¿…é¡»å¯æŒä¹…化 + +--- + +## 9. 通信需求 + +### 9.1 Meshtastic + +必须支æŒçœŸå®ž Meshtastic 业务能力,包括但ä¸é™äºŽï¼š + +- 基本 LoRa æ”¶å‘ +- 节点身份广播 +- Channel 相关é…ç½® +- 与手机的 BLE æ•°æ®é€šè·¯ +- NodeInfo åŒæ­¥ + +### 9.2 MeshCore + +必须支æŒçœŸå®ž MeshCore 业务能力,包括但ä¸é™äºŽï¼š + +- 身份广播 +- 本机身份展示 +- 空å£èº«ä»½è”动 +- LoRa æ•°æ®é€šè·¯ + +### 9.3 åŒå议边界 + +è¦æ±‚: + +- `GAT562` æ”¯æŒ Meshtastic / MeshCore +- å±ä¿é¡µåªéœ€æ˜¾ç¤ºå½“å‰å议简写 +- Team 相关能力ä¸åœ¨èŒƒå›´å†… + +--- + +## 10. LoRa 需求 + +### 10.1 åŸºç¡€æ”¶å‘ + +必须支æŒï¼š + +- LoRa åˆå§‹åŒ– +- æŽ¥æ”¶æ•°æ® +- å‘逿•°æ® +- æ ¹æ®åè®®é€‰æ‹©å‚æ•° + +### 10.2 æ— çº¿å‚æ•°å±•示 + +必须支æŒï¼š + +- 当å‰é¢‘率显示 +- 与实际é…置一致 +- å°æ•°ä½ç²¾ç¡®æ˜¾ç¤ºï¼Œä¸å¾—ç²—æš´å››èˆäº”å…¥æˆé”™è¯¯å€¼ + +### 10.3 åŽå°å¤„ç† + +必须支æŒï¼š + +- 接收轮询 +- 消æ¯å…¥ç«™ +- 对应åè®®å¤„ç† +- å‘å°„æ¢å¤æŽ¥æ”¶ + +--- + +## 11. BLE 需求 + +### 11.1 基础能力 + +必须支æŒï¼š + +- 手机连接 +- 正常广播 +- è®¾å¤‡åæ­£ç¡® +- 基本收å‘链路å¯ç”¨ + +### 11.2 身份è”动 + +BLE åç§°ä¸èƒ½å­¤ç«‹ç®¡ç†ï¼Œå¿…é¡»æœä»Žæœ¬æœºèº«ä»½ç­–略。 + +### 11.3 稳定性 + +BLE ä¸å¾—: + +- 抢å ä¸»å¾ªçŽ¯å¯¼è‡´ UI 塿­» +- 导致å¯åŠ¨æ­»é” +- 导致 LoRa / GNSS 失活 + +--- + +## 12. GNSS 需求 + +必须支æŒï¼š + +- GNSS åˆå§‹åŒ– +- åŸºæœ¬å®šä½æ•°æ®è¯»å– +- 定时任务è¿è¡Œ +- 时间校准相关能力 + +ä¸å¾—: + +- åªåˆå§‹åŒ–ä¸€æ¬¡ç„¶åŽæ— åŽç»­è°ƒåº¦ +- 导致主循环被 GNSS ä»»åŠ¡æ‹–ä½ + +--- + +## 13. 身份与广播需求 + +### 13.1 本机身份 + +æœ¬æœºå¿…é¡»å­˜åœ¨ç»Ÿä¸€èº«ä»½æ¥æºï¼Œè‡³å°‘包括: + +- long name +- short name +- node id + +### 13.2 å±ä¿æ˜¾ç¤º + +用户åå¥½è¦æ±‚: + +- å±ä¿ / 展示页åªéœ€è¦æ˜¾ç¤ºçŸ­çš„ `node id` + +### 13.3 空å£è”动 + +必须支æŒï¼š + +- Meshtastic 身份广播 +- MeshCore 身份广播 +- 本机显示文案è”动 + +è¦æ±‚: + +- 设置修改åŽï¼Œèº«ä»½å¹¿æ’­ä¸Žæœ¬æœºæ˜¾ç¤ºä¿æŒä¸€è‡´ + +--- + +## 14. 输入需求 + +### 14.1 æ‘‡æ† / 按键 + +必须支æŒï¼š + +- 上下左å³ä¸­é”® +- 输入消抖 +- 活动检测 +- 从å±ä¿å”¤é†’ +- èœå•导航 + +### 14.2 è¾“å…¥è°ƒè¯•è¦æ±‚ + +在问题排查阶段,输入层应å¯è¾“出: + +- åŽŸå§‹å¼•è„šçŠ¶æ€ +- 消抖åŽçš„æ–¹å‘事件 +- 活动时间戳 + +但正å¼ç‰ˆæœ¬ä¸åº”长期ä¿ç•™é«˜é¢‘噪声日志。 + +--- + +## 15. 文件系统与æŒä¹…化需求 + +必须支æŒï¼š + +- 设置æŒä¹…化 +- æ¶ˆæ¯æŒä¹…化 +- Peer ä¿¡æ¯æŒä¹…化 + +è¦æ±‚: + +- 文件系统æŸå时有æ¢å¤ç­–ç•¥ +- æ¢å¤ç­–ç•¥ä¸èƒ½å¯¼è‡´æ— ç©·é‡å¯æˆ–é•¿æœŸå¡æ­» +- æŒä¹…化路径ä¸èƒ½æŠŠä¸»å¾ªçŽ¯æ‹–åœ + +--- + +## 16. 体积与资æºè¦æ±‚ + +### 16.1 Flash + +è¦æ±‚: + +- 固件必须能稳定烧录 +- ä¸ä»…仅是“编译未满 100%†+- 实际 UF2 / bootloader 兼容性也必须满足 + +### 16.2 RAM + +è¦æ±‚: + +- ä¸å…许因为 UI / åè®® / 文件系统组åˆåŽè€—å°½ RAM å¯¼è‡´å¡æ­» +- 必须关注è¿è¡Œæ—¶å³°å€¼ï¼Œè€Œä¸åªæ˜¯é™æ€ç¼–è¯‘æ•°æ® + +### 16.3 ç²¾ç®€è¦æ±‚ + +为了控制体积,必须移除: + +- 拼音输入 +- 中文输入法 +- 中文字库 / CJK 路径 + +--- + +## 17. 验收标准 + +`GAT562` 环境至少满足以下标准,æ‰ç®—进入“å¯ç»§ç»­æ¼”è¿›â€çš„状æ€ã€‚ + +### 17.1 å¯åŠ¨éªŒæ”¶ + +- 坿ˆåŠŸçƒ§å½• +- 上电å¯è§æ»šåŠ¨æ—¥å¿— +- ä¸ä¼šå¡åœ¨å¯åŠ¨ä¸­é—´çŠ¶æ€ +- 能进入å±ä¿é¡µ + +### 17.2 UI 验收 + +- å±ä¿æ—¶é—´æŒç»­æ›´æ–° +- 日期 / 星期显示正常 +- å议简写显示正常 +- 频率显示准确 +- å¯ä»Žå±ä¿è¿›å…¥ä¸»èœå• +- èœå•坿­£å¸¸å¯¼èˆª + +### 17.3 Settings 验收 + +- è®¾ç½®é¡¹ä¸æ˜¯ç©ºå£³ +- 坿”¹é¡¹å¯çœŸå®žç”Ÿæ•ˆ +- 坿Œä¹…化 +- é‡å¯åŽä¿æŒ + +### 17.4 通信验收 + +- BLE å¯è¿žæŽ¥ +- LoRa 坿”¶å‘ +- Meshtastic å¯å¹¿æ’­èº«ä»½ +- MeshCore å¯å¹¿æ’­èº«ä»½ +- User Name / Short Name 对本机显示与空å£èº«ä»½è”动生效 + +### 17.5 稳定性验收 + +- 连续è¿è¡Œä¸å› å•个模å—å¯¼è‡´ä¸»å¾ªçŽ¯åœæ‘† +- UI ä¸ä¼šè¿›å…¥é™æ­¢å‡æ­»çŠ¶æ€ +- 摇æ†ä¸ä¼šå¤±æ´» +- 日志ä¸ä¼šåœåœ¨å›ºå®šé˜¶æ®µè€Œç³»ç»Ÿæ— å“应 + +--- + +## 18. å¼€å‘顺åºå»ºè®® + +åŽç»­æ¢å¤å’Œå¼€å‘ï¼Œå»ºè®®ä¸¥æ ¼æŒ‰ä»¥ä¸‹é¡ºåºæŽ¨è¿›ï¼š + +1. 稳定å¯åЍ链 +2. 稳定输入链 +3. 稳定 UI 基础页 +4. 稳定 LoRa æ”¶å‘ +5. 稳定 BLE 连接 +6. 稳定 GNSS 定时任务 +7. 接入真实 Settings +8. 接入真实身份è”动 +9. å®Œæˆ Meshtastic / MeshCore åŒå议业务闭环 + +ç¦æ­¢åå‘顺åºï¼Œä¾‹å¦‚: + +- 在å¯åЍ链ä¸ç¨³å®šæ—¶å…ˆæŠ½ shared modules +- åœ¨è¾“å…¥å¤±æ´»æ—¶å…ˆå †é¡µé¢ +- 在 LoRa / BLE 还没跑稳时先åšé«˜å±‚抽象 + +--- + +## 19. 结论 + +`GAT562` çš„ç›®æ ‡ä¸æ˜¯åšæˆâ€œå¤§è€Œå…¨â€çš„å¤šåª’ä½“ç»ˆç«¯ï¼Œè€Œæ˜¯åšæˆä¸€ä¸ªï¼š + +- å°å± +- 有é™è¾“å…¥ +- æ—  Team +- 无中文输入 +- æ—  SD +- æ—  HostLink + +但具备真实: + +- Meshtastic +- MeshCore +- BLE +- LoRa +- GNSS +- 本机设置 +- 身份è”动 + +能力的稳定设备环境。 + +åŽç»­æ‰€æœ‰å®žçްã€é‡æž„ã€å…±äº«æ¨¡å—抽å–,都必须æœä»Žè¿™ä¸ªç›®æ ‡ä¸Žè¾¹ç•Œã€‚ diff --git a/docs/Best Practices/GAT562_REQUIREMENT_COVERAGE.md b/docs/Best Practices/GAT562_REQUIREMENT_COVERAGE.md new file mode 100644 index 00000000..9efb981d --- /dev/null +++ b/docs/Best Practices/GAT562_REQUIREMENT_COVERAGE.md @@ -0,0 +1,78 @@ +# GAT562 éœ€æ±‚è¦†ç›–æ¸…å• + +本文件用于对照 `GAT562_REQUIREMENTS.md` 跟踪当å‰å®žçŽ°çŠ¶æ€ã€‚ + +## å·²è½åœ° + +- 身份å•ä¸€äº‹å®žæ¥æºï¼š`long name / short name / node id / BLE åç§°` 统一走 `modules/core_chat` çš„ `self_identity_policy` +- LoRa 真é…置:`region / preset / channel / tx_power / MeshCore radio params` 已能下推到 `SX1262` +- Meshtastic 空å£è‡ªå®£å‘Šï¼šå…±äº« `MeshtasticSelfAnnouncementCore` +- MeshCore 空å£è‡ªå®£å‘Šï¼šå…±äº« `MeshCoreSelfAnnouncementCore` +- Meshtastic Lite 入站:文本 + éžæ–‡æœ¬ `AppData` +- MeshCore Lite 入站:advert + direct/group `AppData` +- GNSS å¹³å° runtime:UART 读æµã€fix 状æ€ã€åŸºç¡€æ ¡æ—¶ +- Device runtimeï¼šç”µæ± ç™¾åˆ†æ¯”ã€æç¤ºéŸ³éŸ³é‡æŒä¹…åŒ–ã€æç¤º LED +- å•色 128x64 shared UI 模å—:已新增 `modules/ui_mono_128x64` +- GAT562 å•色 UI 装é…:已接入å¯åŠ¨æ—¥å¿—ã€å±ä¿ã€ä¸»èœå•ã€èŠå¤©åˆ—表ã€ä¼šè¯ã€è‹±æ–‡/æ•°å­—/符å·è¾“å…¥ã€èº«ä»½/无线/设备/GNSS/动作页 +- å±ä¿ä¿¡æ¯ï¼šå·²æŒ‰éœ€æ±‚æä¾› `mt/mc`ã€`MHz` é¢‘çŽ‡ã€æ—¶é—´ã€æ—¥æœŸã€æ˜ŸæœŸã€çŸ­ node id +- GAT562 timezone runtimeï¼šå·²æ–°å¢žç‹¬ç«‹å¹³å°æ—¶é—´åç§»æŒä¹…åŒ–æŽ¥å£ +- no-Team / no-HostLink / no-SD / no-CJK / no-Pinyin:已在 env 与 shared UI 边界è£å‰ª +- `saveConfig()`:已改为“è½ç›˜åŽç«‹å³å›žæŽ¨è¿è¡Œæ€â€ + +## å·²æ­éª¨æž¶ä½†æœªé—­çޝ + +- nRF52 BLE manager:已有å议切æ¢ã€å¹¿æ’­åè”动ã€åŸºç¡€ connectable service +- nRF52 board runtime:已有 3V3 rail / LED / 输入快照 / 频率格å¼åŒ– +- GAT562 app facadeï¼šå·²å…·å¤‡ç‹¬ç«‹è£…é…æ ¹ï¼Œä¸å†ä¾é™„ ESP app context +- å•色 UI 还未ç»è¿‡ç¼–译与实机回归,当å‰å±žäºŽâ€œç»“构已è½åœ°ã€å¾…è”调验è¯â€ + +## ä»å¾…å®Œæˆ + +- Meshtastic 手机侧 BLE 完整åè®® +- MeshCore 手机侧 BLE 完整åè®® +- è®¾ç½®é¡µä¸Žæ‰€æœ‰çœŸå®žèƒ½åŠ›çš„æœ€ç»ˆé¡µé¢æ˜ å°„æ”¶å°¾ +- å¯åЍ链 / 主循环 / IC åè°ƒçš„æœ€ç»ˆå®žæœºéªŒè¯ + +## 结论 + +当å‰ä»£ç å·²ç»æŠŠâ€œå…±äº«èº«ä»½ / 自宣告 / LoRa é…ç½® / GNSS / æ¿çº§ runtime / no-Team 边界â€è¿™å‡ æ¡åŸºç¡€ä¸»çº¿é‡æ–°åŽ‹å›žäº†æ­£ç¡®å±‚çº§ï¼Œ +但 **GAT562 还没有达到需求文档里的“全部完æˆâ€çжæ€**。åŽç»­æœ€é«˜ä¼˜å…ˆçº§ä»ç„¶æ˜¯ï¼š + +1. å•色 UI 闭环 +2. BLE 手机å议闭环 +3. 实机稳定性闭环 + +--- + +## 2026-03-18 Incremental Coverage Notes + +- nRF52 `Meshtastic BLE` now covers: + - `get_device_connection_status_request` + - `get_module_config_request` + - `set_module_config` + - `FromRadio.moduleConfig` snapshot emission during `want_config_id` +- nRF52 `MeshCore BLE` now additionally covers: + - `CMD_GET_BATT_AND_STORAGE` + - `CMD_SEND_LOGIN` + - `CMD_SEND_STATUS_REQ` + - `CMD_SEND_BINARY_REQ` + - `CMD_SEND_PATH_DISCOVERY_REQ` + - `CMD_SEND_RAW_DATA` + - `CMD_SEND_TRACE_PATH` + - `CMD_SEND_CONTROL_DATA` + - `CMD_SET_FLOOD_SCOPE` + - `CMD_HAS_CONNECTION` + - `CMD_LOGOUT` + - `CMD_RESET_PATH` +- These additions stay inside `platform/nrf52/arduino_common`, which keeps the board/app/module boundary consistent with `new_hardware_adaptation_prompt.md`. +- No build or device validation is claimed in this note. +- nRF52 `MeshCore BLE` coverage in this round also now includes: + - `CMD_SEND_TELEMETRY_REQ` + - `CMD_GET_ADVERT_PATH` + - `CMD_GET_BATT_AND_STORAGE` +- nRF52 `Meshtastic BLE` in this round also now handles lightweight local admin helpers: + - canned-message get/set + - ringtone get/set + - `set_time_only` + - `store_ui_config` + - `remove_by_nodenum` request path diff --git a/docs/Best Practices/GAT562_STATUS_SNAPSHOT.md b/docs/Best Practices/GAT562_STATUS_SNAPSHOT.md new file mode 100644 index 00000000..a764f9ef --- /dev/null +++ b/docs/Best Practices/GAT562_STATUS_SNAPSHOT.md @@ -0,0 +1,122 @@ +# GAT562 Status Snapshot + +This snapshot records what the current `gat562_mesh_evb_pro` branch has already aligned with the GAT562 requirements, and what is still intentionally unfinished. + +## Aligned in this round + +- Shared self identity stays in `modules/core_chat`, including: + - effective `long_name / short_name / node_id` + - screen node label formatting + - BLE visible name derivation +- GAT562 app assembly stays in `apps/gat562_mesh_evb_pro`: + - startup + - loop + - app facade + - mono UI runtime binding +- nRF52 platform ownership stays in `platform/nrf52/arduino_common`: + - board runtime + - input runtime + - SX1262 packet transport + - GNSS runtime + - time persistence + - BLE manager and protocol service split +- GAT562 env keeps hard product boundaries in `variants/gat562_mesh_evb_pro/envs/gat562_mesh_evb_pro.ini`: + - no Team + - no HostLink + - no SD + - no CJK + - no Pinyin IME + - no `modules/ui_shared/src/*` +- GAT562 app facade still implements Team-related interface slots only as compatibility stubs returning `nullptr`, so Team capability stays structurally excluded instead of half-enabled. + +## Newly aligned details + +- Mono UI settings now cover real persisted/runtime-backed items: + - identity + - protocol + - tx power + - region + - preset + - channel + - encrypt + - PSK or MeshCore channel name + - BLE + - time zone + - GPS on/off + - GPS interval + - active chat channel +- BLE enabled state is now treated as configuration state instead of only transient runtime state. +- ESP-side effective identity and BLE visible name now consume the same shared identity policy as GAT. +- nRF52 BLE manager no longer owns protocol-specific service construction logic directly; Meshtastic and MeshCore BLE services now have their own files. +- nRF52 Meshtastic BLE service now moves beyond advertising shell and includes: + - `ToRadio.packet` text/app-data ingress + - `want_config_id / heartbeat / disconnect` + - `MyInfo / self NodeInfo / node-store NodeInfo / Metadata / config-complete` + - radio-to-phone forwarding for text plus polled app-data +- nRF52 MeshCore BLE service now moves beyond advertising shell and includes: + - `CMD_DEVICE_QEURY` + - `CMD_APP_START` + - `CMD_SEND_TXT_MSG` + - `CMD_SEND_CHANNEL_TXT_MSG` + - `CMD_GET_BATT_AND_STORAGE` + - `CMD_GET_DEVICE_TIME / CMD_SET_DEVICE_TIME` + - `CMD_GET_CONTACTS` + - `CMD_GET_CONTACT_BY_KEY` + - `CMD_SET_ADVERT_NAME` + - `CMD_SEND_SELF_ADVERT` + - `CMD_SET_RADIO_PARAMS / CMD_SET_RADIO_TX_POWER` + - `CMD_GET_CHANNEL / CMD_SET_CHANNEL` + - `CMD_GET_STATS` + - `CMD_EXPORT_PRIVATE_KEY / CMD_IMPORT_PRIVATE_KEY` + - `CMD_SIGN_START / CMD_SIGN_DATA / CMD_SIGN_FINISH` + - `CMD_REBOOT / CMD_FACTORY_RESET` + - `CMD_SEND_LOGIN` + - `CMD_SEND_STATUS_REQ` + - `CMD_HAS_CONNECTION` + - `CMD_SEND_BINARY_REQ` + - `CMD_SEND_PATH_DISCOVERY_REQ` + - `CMD_SEND_RAW_DATA` + - `CMD_SEND_TRACE_PATH` + - `CMD_SEND_TELEMETRY_REQ` + - `CMD_GET_ADVERT_PATH` + - `CMD_SET_FLOOD_SCOPE` + - `CMD_SEND_CONTROL_DATA` + - `CMD_GET_BATT_AND_STORAGE` + - `CMD_LOGOUT / CMD_RESET_PATH` + - incoming text/app-data forwarding on the BLE TX path +- nRF52 `MeshCoreAdapterLite` now exports its own public key so BLE self-info can stay protocol-backed instead of inventing board-local identity logic. +- nRF52 `MeshCoreAdapterLite` now exposes self-advert triggering for BLE command routing. +- nRF52 `MeshCoreAdapterLite` now also owns lightweight MeshCore request/control packet building for: + - peer request types + - anonymous login-style requests + - binary request payloads + - trace path packets + - control channel packets + - flood-scope key storage +- nRF52 Meshtastic BLE admin flow now also answers: + - `get_device_connection_status_request` + - correct config echo type for `display` and `device_ui` +- nRF52 Meshtastic BLE now also covers module-config admin/snapshot flow: + - `get_module_config_request` + - `set_module_config` + - `FromRadio.moduleConfig` snapshot stream during `want_config_id` +- nRF52 Meshtastic BLE also now covers lightweight local admin helpers: + - canned-message get/set + - ringtone get/set + - `set_time_only` + - `store_ui_config` + - `remove_by_nodenum` as a handled request path + +## Still not closed + +- nRF52 Meshtastic phone BLE still lacks the fuller official module-config/admin mutation stream. +- nRF52 MeshCore phone BLE still uses lite peer resolution and does not yet claim parity with the richer ESP route/session model. +- The new mono UI and the new nRF52 BLE service split are not build-verified in this round. +- No runtime validation is claimed here. + +## Next priority + +1. Finish nRF52 Meshtastic BLE protocol behavior on top of the new service split. +2. Finish nRF52 MeshCore BLE protocol behavior on top of the new service split. +3. Build-verify `tdeck` and `gat562_mesh_evb_pro`. +4. Only after build passes, move to device flashing and runtime validation. diff --git a/docs/Best Practices/new_hardware_adaptation_prompt.md b/docs/Best Practices/new_hardware_adaptation_prompt.md index 541f279d..af2e8a26 100644 --- a/docs/Best Practices/new_hardware_adaptation_prompt.md +++ b/docs/Best Practices/new_hardware_adaptation_prompt.md @@ -1,192 +1,653 @@ -# æ–°ç¡¬ä»¶é€‚é… Prompt(Trail-Mate) +# 新硬件适é…å¼€å‘规范(Trail-Mate) -你是本项目(Trail-Mate)的嵌入å¼é€‚é…工程师。你的任务是为“一个新硬件æ¿å¡â€å¢žåŠ æ”¯æŒï¼ŒåŒæ—¶**ä¸ç ´å已有环境(尤其是 tlora_pager_*)**,并严格éµå®ˆâ€œé¢å‘能力接å£è§£è€¦â€çš„设计原则。 +æœ¬æ–‡æ¡£ä¸æ˜¯ä¸€æ¬¡æ€§çš„ Prompt 备忘录,而是 Trail-Mate åŽç»­é€‚é…æ–°ç¡¬ä»¶æ—¶å¿…é¡»éµå®ˆçš„工程规范。 -## 0. æ ¸å¿ƒç›®æ ‡ï¼ˆå¿…é¡»åŒæ—¶æ»¡è¶³ï¼‰ +ç›®æ ‡ä¸æ˜¯â€œå…ˆè·‘èµ·æ¥â€ï¼Œè€Œæ˜¯ï¼š -1) æ—§æ¿é›¶æ”¹åŠ¨æˆ–æœ€å°æ”¹åŠ¨ï¼ˆä¸æ”¹è¡Œä¸ºï¼ŒåªåšæŠ½è±¡æ”¶å£/兼容性修å¤ï¼‰ã€‚ -2) æ–°æ¿é€šè¿‡æ–°å¢žçŽ¯å¢ƒ + æ–°æ¿å®žçŽ°æŽ¥å…¥ï¼ˆOpen/Closed:对扩展开放,对修改关闭)。 -3) ç¦æ­¢åœ¨ä¸Šå±‚(app/chat/ui/gps/hal)引入具体æ¿ç±»åž‹ä¾èµ–。 -4) ç¦æ­¢ä½¿ç”¨ dynamic_cast 作为主路径(嵌入å¼ä¸æŽ¨è)。 - -如果你å‘现实现路径会è¿å上述目标,必须åœä¸‹å¹¶æ”¹èµ°â€œèƒ½åŠ›æŽ¥å£ + 环境隔离â€çš„路径。 +1. 新硬件å¯ä»¥æŒç»­æŽ¥å…¥ï¼› +2. 旧硬件ä¸è¢«ç ´åï¼› +3. 能力边界长期清晰; +4. 业务逻辑ä¸å› ä¸ºæ¢æ¿å­è€Œå››å¤„å¤åˆ¶ï¼› +5. åŽç»­ä»»ä½•人接手,都能快速判断代ç è¯¥å†™åœ¨å“ªä¸€å±‚。 --- -## 1. å…ˆåšâ€œç»“构审视â€ï¼Œå†åŠ¨æ‰‹æ”¹ä»£ç  +## 1. 核心设计原则 -在修改å‰å…ˆå¿«é€Ÿå®¡è§†ä»¥ä¸‹å†…容: +åŽç»­æ‰€æœ‰æ–°ç¡¬ä»¶æŽ¥å…¥ï¼Œéƒ½å¿…é¡»åŒæ—¶æ»¡è¶³ä¸‹é¢å‡ ä¸ªåŽŸåˆ™ã€‚ -- æ¿çº§æŠ½è±¡ä¸Žèƒ½åŠ›æŽ¥å£ï¼š - - `platform/esp/boards/include/board/BoardBase.h` - - `platform/esp/boards/include/board/LoraBoard.h` - - `platform/esp/boards/include/board/GpsBoard.h` - - `platform/esp/boards/include/board/MotionBoard.h` -- 现有æ¿å®žçŽ°ï¼š - - `platform/esp/boards/include/board/TLoRaPagerBoard.h` - - `platform/esp/boards/src/board/TLoRaPagerBoard.cpp` -- 环境与å˜ä½“: - - `variants/lilygo_tlora_pager/envs/tlora_pager.ini` - - `variants/tdeck/envs/tdeck.ini` - - `variants/*/pins_arduino.h` -- å…¥å£ä¸Žè£…é…点: - - `apps/esp_pio/src/arduino_entry.cpp` - - `src/app/app_context.cpp` - - `platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/protocol_factory.h / platform/esp/arduino_common/src/chat/infra/protocol_factory.cpp` +### 1.1 å•一èŒè´£ -审视åŽç”¨ 3-6 æ¡è¦ç‚¹è¯´æ˜Žï¼š -- 哪些地方已ç»è§£è€¦è‰¯å¥½ï¼› -- 哪些地方ä»å¯èƒ½æ³„æ¼å…·ä½“æ¿ç±»åž‹ï¼› -- 你的接入策略(必须是“新增实现 + èƒ½åŠ›æŽ¥å£æ¶ˆè´¹ + 环境隔离â€ï¼‰ã€‚ +- `modules/` è´Ÿè´£å¯å¤ç”¨ä¸šåŠ¡èƒ½åŠ›ã€å…±äº«ç­–ç•¥ã€å…±äº«æŽ§åˆ¶å™¨ã€å…±äº«å议核心。 +- `platform/` 负责芯片/å¹³å°ç›¸å…³èƒ½åЛ适é…,例如 ESPã€nRF52ã€BLEã€LoRaã€æ–‡ä»¶ç³»ç»Ÿã€ç³»ç»Ÿæ—¶é’Ÿã€å¹³å°æ¡¥æŽ¥ã€‚ +- `boards/` 负责具体硬件æ¿å¡å·®å¼‚ã€æ¿çº§å¼•è„šã€æ¿çº§è®¾å¤‡ç»„åˆã€æ¿çº§ profile。 +- `apps/` 负责具体产å“/设备è¿è¡Œæ—¶è£…é…,åªåš startup / loop / assembleï¼Œä¸æŒæœ‰å¯å¤ç”¨ä¸šåŠ¡èµ„äº§æœ¬ä½“ã€‚ +- `variants/` 负责编译环境ã€å®ã€å‚æ•°ã€`build_src_filter`ã€ç›®æ ‡æ¿çŽ¯å¢ƒéš”ç¦»ã€‚ + +### 1.2 开闭原则 + +- 新硬件接入优先通过“新增实现â€å®Œæˆï¼Œè€Œä¸æ˜¯ä¿®æ”¹æ—§æ¿è¡Œä¸ºã€‚ +- 能通过新增 `board` / `env` / `bridge` / `provider` 解决的,ä¸è¦åŽ»æ±¡æŸ“çŽ°æœ‰ä¸šåŠ¡æ ¸å¿ƒã€‚ +- ä¿®æ”¹æ—§ä»£ç æ—¶ï¼Œåªå…许åšï¼š + - 抽象上æï¼› + - é‡å¤é€»è¾‘收敛; + - 兼容性修å¤ï¼› + - 命忾„清。 + +### 1.3 èƒ½åŠ›æ¶ˆè´¹ä¼˜å…ˆï¼Œç¦æ­¢ä¸Šå±‚直连æ¿çº§ç»†èŠ‚ + +- 上层åªèƒ½ä¾èµ–抽象能力,ä¸èƒ½ä¾èµ–具体æ¿ç±»ã€å…·ä½“驱动实例ã€å…·ä½“引脚。 +- ç¦æ­¢åœ¨ `apps/`ã€`modules/`ã€UI controllerã€chat service 中写æ¿å¡ç‰¹åˆ¤ã€‚ +- ç¦æ­¢ç”¨ `dynamic_cast<具体æ¿ç±»*>` 作为主路径。 + +### 1.4 é…ç½®å•ä¸€äº‹å®žæ¥æº + +- æ¿çº§å‚æ•°æ¥è‡ª `variants/*/envs/*.ini` 与 `boards/*/board_profile.*`。 +- è¿è¡Œæ—¶è®¾ç½®æ¥è‡ª settings runtime / app config / platform config owner。 +- ä¸å…许åŒä¸€ä»½é…置在多个层级å„自存一套“真值â€ã€‚ + +### 1.5 å…±äº«é€»è¾‘å¿…é¡»å‘ `modules/` æ±‡èš + +凡是满足下é¢ä»»ä¸€æ¡ä»¶ï¼Œå°±ä¸åº”该继续留在 app 或 platform 层: + +- ESP 与 nRF52 都会用到; +- åŒä¸€å议在多个设备上都会å¤ç”¨ï¼› +- åŒä¸€ UI 控制器会被多å—设备å¤ç”¨ï¼› +- åŒä¸€ identity / announcement / settings / protocol 规则会跨环境å¤ç”¨ã€‚ --- -## 2. ç»Ÿä¸€å‡†åˆ™ï¼šèƒ½åŠ›æŽ¥å£æ¶ˆè´¹ï¼Œä¸æš´éœ²å…·ä½“æ¿ç±»åž‹ +## 2. 目录分层规范 -### 2.1 你应该åšçš„(DO) +这是当å‰é¡¹ç›®åŽç»­é•¿æœŸéµå®ˆçš„目录语义。 -- åªåœ¨â€œæ¿çº§å®žçŽ°å±‚ï¼ˆsrc/boardï¼‰â€æŽ¥è§¦å…·ä½“ç¡¬ä»¶ç»†èŠ‚ä¸Žç¬¬ä¸‰æ–¹é©±åŠ¨ç±»åž‹ï¼ˆå¦‚ SX1262)。 -- 在上层通过能力接å£è®¿é—®ç¡¬ä»¶èƒ½åŠ›ï¼š - - LoRa:åªç”¨ `LoraBoard` 中的能力方法(例如 `transmitRadio` / `startRadioReceive` / `configureLoraRadio` 等)。 - - GPS:åªç”¨ `GpsBoard` 能力。 - - Motion:åªç”¨ `MotionBoard` 能力。 -- 使用编译期å®åœ¨å…¥å£å¤„选择æ¿å®žçŽ°ï¼š - - 例如在 `apps/esp_pio/src/arduino_entry.cpp`: - - `#if defined(ARDUINO_T_DECK)` -> `#include "board/TDeckBoard.h"` - - `#else` -> `#include "board/TLoRaPagerBoard.h"` -- 使用 PlatformIO 的环境隔离æ¿çº§æºæ–‡ä»¶ï¼š - - 在 env 中使用 `build_src_filter`,确ä¿ï¼š - - æ–°æ¿çŽ¯å¢ƒåªç¼–è¯‘æ–°æ¿æ–‡ä»¶ï¼› - - æ—§æ¿çŽ¯å¢ƒåªç¼–è¯‘æ—§æ¿æ–‡ä»¶ã€‚ +## 2.1 `modules/` -### 2.2 ä½ ä¸åº”该åšçš„(DON'T) +`modules/` 是共享业务与共享能力的归属地。 -- ä¸è¦åœ¨ `app/`ã€`chat/`ã€`ui/`ã€`gps/`ã€`hal/` 中包å«å…·ä½“æ¿å¤´æ–‡ä»¶ï¼ˆå¦‚ `TLoRaPagerBoard.h` / `TDeckBoard.h`)。 -- ä¸è¦åœ¨ä¸Šå±‚写: - - `dynamic_cast` - - `static_cast` - - 直接调用 `radio_` 或特定驱动 API。 -- ä¸è¦æŠŠâ€œçŽ¯å¢ƒé…ç½®å‚æ•°â€ï¼ˆå¦‚å±å¹•宽高)散è½åœ¨ä»£ç é‡Œä½œä¸ºå…œåº•默认值。 - - å±å¹•傿•°å¿…é¡»æ¥è‡ª env çš„ `build_flags`(例如 `-DSCREEN_WIDTH=...`)。 +é€‚åˆæ”¾å…¥ `modules/` 的内容: + +- å议核心; +- 共享 controllerï¼› +- 共享 page modelï¼› +- 共享 UI 渲染抽象; +- identity policyï¼› +- self-announcement policyï¼› +- team/chat/contact/core runtimeï¼› +- ä¸ä¾èµ–具体æ¿å¡çš„æ˜¾ç¤ºæŠ½è±¡ï¼› +- ä¸ä¾èµ–具体平å°çš„ä¸šåŠ¡çŠ¶æ€æœºã€‚ + +ä¸åº”该放入 `modules/` 的内容: + +- 具体引脚; +- 具体 SPI/I2C/UART 实例; +- æŸä¸ªæ¿å­çš„æŒ‰é”®ç¼–å·ï¼› +- æŸä¸ªæ¿å­çš„ OLED 地å€ï¼› +- æŸä¸ªæ¿å­çš„电池 ADC æ¢ç®—ï¼› +- æŸä¸ªæ¿å­çš„ RadioLib åˆå§‹åŒ–åºåˆ—。 + +### 当å‰å·²ç»ç¡®è®¤çš„å…±äº«æ¨¡å—æ–¹å‘ + +- `modules/core_chat` + - identity policy + - self identity provider 抽象 + - self announcement core + - Meshtastic / MeshCore 自宣核心 +- `modules/ui_mono_128x64` + - 128x64 å•色å±å…±äº« UI/controller/page/display 抽象 + +## 2.2 `platform/` + +`platform/` 䏿˜¯â€œå…·ä½“设备层â€ï¼Œè€Œæ˜¯â€œå¹³å°/芯片/系统能力适é…层â€ã€‚ + +例如: + +- `platform/esp/*` +- `platform/nrf52/*` + +é€‚åˆæ”¾å…¥ `platform/` 的内容: + +- BLE runtimeï¼› +- LoRa runtimeï¼› +- platform-specific identity bridgeï¼› +- 平尿¶ˆæ¯æ³µï¼› +- 平尿–‡ä»¶ç³»ç»Ÿå°è£…ï¼› +- å¹³å° radio contextï¼› +- å¹³å°åè®® backend 适é…ï¼› +- å¹³å°ä¾§å¯¹ shared modules 的桥接。 + +ä¸é€‚åˆæ”¾å…¥ `platform/` 的内容: + +- æŸå—具体æ¿å­çš„äº§å“ UI 资产本体; +- æŸå—具体设备专属业务èœå•结构; +- æŸå—æ¿å­çš„ startup 文案; +- 具体产å“装é…逻辑。 + +## 2.3 `boards/` + +`boards/` 是具体硬件æ¿å¡å±‚。 + +é€‚åˆæ”¾å…¥ `boards/` 的内容: + +- `board_profile.h` +- PinMap / BleProfile / LoraProfile / InputProfile / BatteryProfile +- æ¿çº§ä¸“属 binding +- æ¿çº§ capability ç»„åˆ +- æŸå—æ¿å­çš„设备 runtime bridge + +命åå¿…é¡»åæ˜ çœŸå®žæ¿å¡ï¼Œè€Œä¸æ˜¯æ¨¡ç³Šç¼©å†™ã€‚ + +推è: + +- `boards/gat562_mesh_evb_pro` + +䏿ލè: + +- `boards/gat562` + +原因: + +- `gat562` æ›´åƒç³»åˆ—å,ä¸åƒå…·ä½“æ¿åï¼› +- åŽç»­åŒç³»åˆ—多æ¿å¹¶å­˜æ—¶ä¼šæ··ä¹±ï¼› +- 目录å必须对应真实硬件适é…对象。 + +## 2.4 `apps/` + +`apps/` 是具体产å“/设备è¿è¡Œæ—¶çš„装é…层。 + +é€‚åˆæ”¾å…¥ `apps/` 的内容: + +- startup runtime +- loop runtime +- app context / runtime assembly +- provider / port çš„å®žä¾‹è£…é… +- ç”Ÿå‘½å‘¨æœŸè§¦å‘ +- 儿¨¡å—之间的最终 wiring + +ä¸é€‚åˆæ”¾å…¥ `apps/` 的内容: + +- å议包拼装规则; +- identity fallback 规则; +- 共享 UI 资产本体; +- Meshtastic / MeshCore 自宣业务核心; +- 通用 settings controller 逻辑; +- 通用å±ä¿é¡µé¢é€»è¾‘。 + +一å¥è¯ï¼š + +- `apps/` åªè´Ÿè´£â€œæŠŠå·²æœ‰æ¨¡å—拼起æ¥â€ï¼Œä¸è´Ÿè´£â€œé‡æ–°å‘明模å—â€ã€‚ + +## 2.5 `variants/` + +`variants/` åªè´Ÿè´£çŽ¯å¢ƒå’Œç¼–è¯‘éš”ç¦»ã€‚ + +必须放在这里的内容: + +- `build_flags` +- `build_src_filter` +- board å® +- å±å¹•尺寸 +- æ¿çº§ include path +- 目标环境åç§° + +ç¦æ­¢æŠŠè¿™äº›é…置散è½åˆ°ä»£ç é‡Œåšå…œåº•默认值。 + +例如å±å¹•尺寸: + +- 正确:在 env 用 `-DSCREEN_WIDTH=128 -DSCREEN_HEIGHT=64` +- 错误:代ç é‡Œå·å·é»˜è®¤ `128x64` --- -## 3. çŽ¯å¢ƒä¸Žå‚æ•°çš„å”¯ä¸€äº‹å®žæ¥æºï¼ˆSingle Source of Truth) +## 3. 推èè®¾è®¡æ¨¡å¼ -所有“æ¿çº§å‚æ•°â€ä¼˜å…ˆæ”¾åœ¨ env é…置层: +以下模å¼å·²ç»åœ¨æœ¬å·¥ç¨‹ä¸­éªŒè¯æœ‰æ•ˆï¼ŒåŽç»­ç¡¬ä»¶é€‚é…优先沿用。 -- å±å¹•尺寸: - - 在 env 中通过 `-DSCREEN_WIDTH=...` / `-DSCREEN_HEIGHT=...` æä¾›ã€‚ - - 代ç ä¸­å¦‚果缺失,应 `#error`ï¼Œè€Œä¸æ˜¯å…œåº•默认值。 -- æ¿çº§æºæ–‡ä»¶é€‰æ‹©ï¼š - - 在 env 中通过 `build_src_filter` 控制。 +## 3.1 Provider æ¨¡å¼ -如果你å‘现代ç å†…存在æ¿çº§å‚数兜底默认值: -- 优先删除默认值,改为 `#error` æç¤ºå¿…须由 env æä¾›ï¼› -- ç„¶åŽç¡®ä¿æ‰€æœ‰ env 都æä¾›è¯¥å‚数。 +适用场景: + +- æŸç±»å…±äº«é€»è¾‘需è¦è¯»å–“当å‰è¿è¡Œæ—¶çжæ€â€ï¼Œä½†ä¸åº”该ä¾èµ–æŸä¸ª app / board / platform 细节。 + +典型例å­ï¼š + +- `SelfIdentityProvider` + +èŒè´£ï¼š + +- åªæä¾›æ•°æ®ï¼› +- ä¸åšä¸šåŠ¡ï¼› +- ä¸åšæŒä¹…化; +- ä¸åš UIï¼› +- ä¸åšåè®®å‘包。 + +é€‚åˆ provider 的数æ®ï¼š + +- å½“å‰ node id +- å½“å‰ configured long/short name +- 当å‰é»˜è®¤å‰ç¼€ +- å½“å‰ BLE 默认å +- å½“å‰ active mesh config + +## 3.2 Port + Core æ¨¡å¼ + +适用场景: + +- 共享业务核心需è¦è°ƒç”¨å¹³å°èƒ½åŠ›ï¼Œä½†ä¸åº”该直接ä¾èµ–硬件。 + +典型例å­ï¼š + +- `MeshtasticSelfAnnouncementCore` + `MeshtasticSelfAnnouncementPort` +- `MeshCoreSelfAnnouncementCore` + `MeshCoreSelfAnnouncementPort` + +规范: + +- `Core` 放在 `modules/` +- `Port` æŽ¥å£æ”¾åœ¨ `modules/` +- `Port` 实现放在 `apps/` 或 `platform/` +- `Core` ä¸å¾— include 具体æ¿é©±åŠ¨å¤´æ–‡ä»¶ + +## 3.3 Bridge æ¨¡å¼ + +适用场景: + +- å¹³å°å±‚å†…éƒ¨ä»æœ‰å¤šä¸ª runtime 需è¦è¯»å–åŒä¸€ç±»å¹³å°çжæ€ï¼› +- 但åˆä¸å¸Œæœ›æ‰€æœ‰ runtime 直接ä¾èµ– settings/runtime 细节。 + +典型例å­ï¼š + +- `platform/nrf52/self_identity_bridge` + +èŒè´£ï¼š + +- 统一暴露平å°å†…éƒ¨èº«ä»½è¯»å–æŽ¥å£ï¼› +- 隔离 `settings_runtime` 细节; +- ç»™ BLEã€radio contextã€host config ç­‰å¹³å° runtime 使用。 + +## 3.4 Composition Root æ¨¡å¼ + +适用场景: + +- ä¸€ä¸ªçŽ¯å¢ƒéœ€è¦æœ€ç»ˆå†³å®šâ€œè£…什么实现ã€è¿žä»€ä¹ˆ providerã€èµ°ä»€ä¹ˆ controllerâ€ã€‚ + +典型ä½ç½®ï¼š + +- `apps/esp_pio` +- `apps/gat562_mesh_evb_pro` + +èŒè´£ï¼š + +- åªè£…é…,ä¸é€ è½®å­ã€‚ + +## 3.5 Adapter æ¨¡å¼ + +适用场景: + +- å¹³å°å·²æœ‰ runtime / backend / 驱动接å£ï¼Œä¸Žå…±äº«æ¨¡å—接å£ä¸ä¸€è‡´ã€‚ + +例如: + +- å¹³å° radio context 对 shared mesh adapter 的桥接; +- æ¿çº§ OLED 驱动对 `MonoDisplay` 的桥接。 --- -## 4. æ–°ç¡¬ä»¶æŽ¥å…¥çš„æŽ¨èæ­¥éª¤ï¼ˆæŒ‰é¡ºåºæ‰§è¡Œï¼‰ +## 4. 当å‰å·²ç»ç¡®ç«‹çš„æž¶æž„结论 -### Step A - æ–°å¢žçŽ¯å¢ƒï¼ˆä¸æ”¹æ—§çŽ¯å¢ƒè¡Œä¸ºï¼‰ +## 4.1 Identity / Self-Announcement -1) 在 `variants//envs/.ini` 新增环境: - - 设置 `board = ...` - - 设置 `build_flags`(包括å±å¹•å°ºå¯¸ã€æ¿å®ã€å˜ä½“ include path) - - 设置 `build_src_filter`: - - `+<*>` - - `-<../platform/esp/boards/src/board/TLoRaPagerBoard.cpp>`ï¼ˆæˆ–å…¶ä»–æ—§æ¿æ–‡ä»¶ï¼‰ - - `+<../platform/esp/boards/src/board/.cpp>` +现在这æ¡é“¾è·¯çš„æœ€ç»ˆå½’å±žå·²ç»æ˜Žç¡®ï¼š -2) 在旧环境(如 `variants/lilygo_tlora_pager/envs/tlora_pager.ini`)确ä¿ï¼š - - `build_src_filter` æŽ’é™¤æ–°æ¿æºæ–‡ä»¶ã€‚ +- `modules/core_chat` + - `SelfIdentityPolicy` + - `SelfIdentityProvider` + - `SelfAnnouncementCore` + - `MeshtasticSelfAnnouncementCore` + - `MeshCoreSelfAnnouncementCore` +- `platform/nrf52/settings_runtime` + - nRF52 本地 identity é…置真值 +- `platform/nrf52/self_identity_bridge` + - nRF52 å¹³å°ç»Ÿä¸€ identity è¯»å–æ¡¥ +- `apps/esp_pio` + - ESP provider è£…é… +- `apps/gat562_mesh_evb_pro` + - GAT562 provider 与自宣 port è£…é… -### Step B - 新增æ¿å®žçŽ°ï¼ˆè€Œä¸æ˜¯æ±¡æŸ“旧实现) +结论: -1) 新建: - - `platform/esp/boards/include/board/.h` - - `platform/esp/boards/src/board/.cpp` +- identity fallback 规则ä¸èƒ½å†™åœ¨ app 里; +- protocol self-announcement 规则ä¸èƒ½å†™åœ¨ app 里; +- app åªèƒ½æä¾› provider å’Œ portï¼› +- platform åªèƒ½æä¾› bridge å’Œ runtime 适é…。 -2) æ–°æ¿ç±»åº”: - - `public BoardBase` - - 视能力实现情况å†å®žçŽ°ï¼š`LoraBoard` / `GpsBoard` / `MotionBoard` / `LilyGo_Display` +## 4.2 UI -3) 在新æ¿å®žçŽ°ä¸­ï¼š - - å¯ä»¥æŽ¥è§¦å…·ä½“驱动类型(RadioLib / 传感器驱动等); - - ä½†å¯¹å¤–åªæš´éœ²èƒ½åŠ›æŽ¥å£å®šä¹‰çš„æ–¹æ³•。 +å•色 128x64 OLED UI 的归属规则: -### Step C - å…¥å£è£…é…(最å°èŒƒå›´å®åˆ†å‘) +- 显示抽象ã€controllerã€pageã€flow 在 `modules/ui_mono_128x64` +- `platform` åªä¿ç•™å±å¹• backend / runtime é€‚é… +- `boards` æä¾›æ¿çº§ display profile / pin / wiring +- `apps` åªè£…é… UI runtime -仅在“装é…点â€åšå®åˆ†å‘: +结论: -- `apps/esp_pio/src/arduino_entry.cpp` -- 如有必è¦ï¼š`src/app/app_context.cpp` +- “å±ä¿é¡µ / 主èœå•页 / settings page 业务控制器â€ä¸å±žäºŽ app +- app åªå†³å®šï¼š + - 是å¦å¯ç”¨è¿™ä¸ª UI + - 用什么显示 backend + - ç”¨ä»€ä¹ˆæ•°æ® provider -模å¼ï¼š +## 4.3 Meshtastic / MeshCore -- æ¡ä»¶ include 具体æ¿å¤´æ–‡ä»¶ï¼› -- 但上层调用尽é‡åªä½¿ç”¨ `BoardBase& board` å’Œèƒ½åŠ›æŽ¥å£æŒ‡é’ˆï¼ˆ`LoraBoard*` 等)。 +å议规则必须尽é‡å›žåˆ° shared modules: + +- åè®® packet shaping +- NodeInfo 组装 +- peer/event/payload å¤„ç† +- identity derivation + +å¹³å°å±‚åªåšï¼š + +- radio transport +- protocol backend runtime +- driver / filesystem / BLE / UART / crypto 接线 --- -## 5. 修改策略与边界控制 +## 5. 新硬件适é…的标准æµç¨‹ -### 5.1 å…许改动的范围 +åŽç»­ä»»ä½•æ–°æ¿æŽ¥å…¥ï¼Œéƒ½æŒ‰è¿™ä¸ªé¡ºåºåšã€‚ -- å…许改动: - - `variants/*/envs/*.ini` - - `variants/*/pins_arduino.h` - - `platform/esp/boards/include/board/*` 与 `platform/esp/boards/src/board/*` - - `apps/esp_pio/src/arduino_entry.cpp`(PlatformIO 装é…/å®åˆ†å‘层é¢ï¼‰ - - æžå°‘é‡ä¸Šå±‚代ç ï¼šä»…为了移除具体æ¿ä¾èµ–ã€æ”¹ä¸ºèƒ½åŠ›æŽ¥å£ +## Step 1:先åšç»“构审视 -### 5.2 高风险改动(尽é‡é¿å…) +在改代ç å‰å…ˆå›žç­” 4 个问题: -- 大规模é‡å†™ `TLoRaPagerBoard.*`。 -- 在多个上层模å—中加入新的å®åˆ†æ”¯ã€‚ -- 为了新æ¿é€‚é…而更改旧æ¿è¡Œä¸ºé€»è¾‘。 +1. 这是新æ¿å¡é—®é¢˜ï¼Œè¿˜æ˜¯å…±äº«æ¨¡å—边界问题? +2. 这段逻辑未æ¥ä¼šä¸ä¼šè¢«ç¬¬äºŒå—æ¿å¤ç”¨ï¼Ÿ +3. 这段逻辑属于 `modules`ã€`platform`ã€`boards` 还是 `apps`? +4. 这次修改会ä¸ä¼šè®©æ—§æ¿è¡Œä¸ºæ¼‚移? -如确需高风险改动,必须: -- 解释为什么ä¸èƒ½ç”¨æ›´å°æ”¹åŠ¨è¾¾æˆç›®æ ‡ï¼› -- 列出风险点和回归验è¯ç‚¹ã€‚ +如果 2 的答案是“会å¤ç”¨â€ï¼Œä¼˜å…ˆè¿›å…¥ `modules/`。 + +## Step 2ï¼šæ–°å¢žçŽ¯å¢ƒï¼Œä¸æ±¡æŸ“旧环境 + +必须先新增: + +- `variants//envs/.ini` + +必须显å¼é…置: + +- board +- `build_flags` +- `build_src_filter` +- variant include path + +å¿…é¡»ä¿è¯ï¼š + +- æ–°æ¿çŽ¯å¢ƒåªç¼–è¯‘æ–°æ¿æ‰€éœ€æºæ–‡ä»¶ï¼› +- æ—§æ¿çŽ¯å¢ƒä¸è¯¯ç¼–è¯‘æ–°æ¿æºæ–‡ä»¶ã€‚ + +## Step 3ï¼šè¡¥é½ board profile + +必须在 `boards//` è¡¥é½æ¿çº§ profile: + +- PinMap +- InputProfile +- BleProfile +- LoraProfile +- BatteryProfile +- AudioProfile + +规则: + +- æ¿çº§å‚æ•°ä¸è®¸æ•£è½åœ¨ runtime `.cpp` 里; +- runtime åªèƒ½å¼•用 board profileï¼› +- board profile ä¸åšä¸šåŠ¡å†³ç­–ã€‚ + +## Step 4:优先接 platform bridgeï¼Œå†æŽ¥ app + +如果æŸèƒ½åŠ›ä¼šè¢«å¤šä¸ª runtime 读å–: + +- å…ˆåš bridge +- å†è®© BLE / radio / settings / host config 共用 + +ä¸è¦ç›´æŽ¥åœ¨å¤šä¸ª runtime 中å„è‡ªè¯»å– settings / config 真值。 + +## Step 5:app åªåš assemble + +在 `apps//` 中åªå…许åšï¼š + +- provider 实现 +- port 实现 +- runtime startup / loop +- controller è£…é… + +ä¸å…许åšï¼š + +- 业务å议核心; +- 通用 controllerï¼› +- 通用 pageï¼› +- å¯å¤ç”¨çŠ¶æ€æœºã€‚ + +## Step 6:åŒçŽ¯å¢ƒå›žå½’ç¼–è¯‘ + +所有共享改造必须至少验è¯ï¼š + +- å—å½±å“çš„ ESP 环境; +- å—å½±å“çš„ nRF52 环境。 + +当剿œ€ä½Žå›žå½’标准: + +- `platformio run -e tdeck` +- `platformio run -e gat562_mesh_evb_pro` --- -## 6. 编译与回归验è¯ï¼ˆå¿…须执行) +## 6. 命å规范 -æ¯æ¬¡å…³é”®æ”¹åЍåŽè‡³å°‘验è¯ï¼š +## 6.1 目录命å -1) 旧环境: - - `C:\Users\VicLi\.platformio\penv\Scripts\platformio.exe run -e tlora_pager_sx1262` -2) 新环境: - - `C:\Users\VicLi\.platformio\penv\Scripts\platformio.exe run -e ` +- æ¿çº§ç›®å½•必须用真实æ¿å: + - `boards/gat562_mesh_evb_pro` +- ç¦æ­¢ç”¨æ¨¡ç³Šç³»åˆ—å充当具体æ¿ç›®å½•: + - 䏿ލè `boards/gat562` -如果失败: -- ä¼˜å…ˆåˆ¤æ–­æ˜¯ä¸æ˜¯â€œæºæ–‡ä»¶æœªéš”离 / 命å空间或å®ä½œç”¨åŸŸé”™è¯¯ / 傿•°æœªåœ¨ env 定义â€ã€‚ -- ä¸è¦ç”¨ä¸´æ—¶å…œåº•默认值掩盖é…置缺失。 +## 6.2 API 命å + +命å必须体现语义,ä¸è¦æ¨¡ç³Šã€‚ + +推è: + +- `applyUserIdentity` +- `fillSelfIdentityPolicyArgs` +- `getEffectiveUserInfo` +- `buildBleVisibleName` +- `broadcast(...)` + +䏿ލè: + +- `setUserInfo` 用æ¥è¡¨ç¤ºâ€œæŒä¹…化 + apply + runtime 生效†+ +说明: + +- `setXxx` æ›´åƒç®€å• setterï¼› +- 如果有æŒä¹…化ã€å¹¿æ’­ã€å‰¯ä½œç”¨ï¼Œåº”该用 `apply` / `persist` / `broadcast` / `refresh` 这类动è¯ã€‚ + +## 6.3 文件命å + +共享策略文件应直接体现èŒè´£ï¼š + +- `self_identity_policy.*` +- `self_identity_provider.*` +- `self_announcement_core.*` +- `meshtastic_self_announcement_core.*` +- `meshcore_self_announcement_core.*` + +ä¸è¦ç”¨ï¼š + +- `helper2` +- `runtime_misc` +- `temp_adapter` +- `stage_*` --- -## 7. 输出格å¼è¦æ±‚(给人看的结果) +## 7. 忍¡å¼æ¸…å• -请用中文输出,结构如下: +下é¢è¿™äº›å𿳕åŽç»­ä¸€å¾‹è§†ä¸ºéœ€è¦é‡æž„。 -1) 你的接入策略(3-6 æ¡è¦ç‚¹ï¼‰ -2) ä½ å®žé™…ä¿®æ”¹äº†å“ªäº›æ–‡ä»¶ï¼ˆé€æ¡åˆ—出路径) -3) 为什么这些改动符åˆè§£è€¦åŽŸåˆ™ï¼ˆç®€æ´è¯´æ˜Žï¼‰ -4) 编译验è¯ç»“果(旧环境 + 新环境) -5) 下一步建议(最多 3 æ¡ï¼Œå¿…须具体䏔坿‰§è¡Œï¼‰ +## 7.1 在 app 中手æ“共享业务逻辑 + +例如: + +- app 里自己组 Meshtastic `PhoneUserArgs` +- app 里自己拼 MeshCore NodeInfo 包 +- app 里自己写 identity fallback + +这些都应该回到 `modules/`。 + +## 7.2 在多个 runtime 里å„è‡ªè¯»å– settings 真值 + +例如: + +- BLE runtime 自己读 user_name +- radio context 自己读 short_name +- host config 自己读一é user_name + +æ­£ç¡®åšæ³•: + +- 统一ç»ç”± bridge / provider 暴露。 + +## 7.3 用“兼容临时代ç â€é•¿æœŸå ä½ + +å…许短期兼容包装,但必须满足: + +- åªä½œä¸ºè¿ç§»æ¡¥ï¼› +- 䏿–°å¢žé‡å¤é€»è¾‘ï¼› +- åŽç»­å¯æ¸…ç†ï¼› +- 命å能看出它是兼容层。 + +如果兼容层开始承载新逻辑,就说明边界错了。 + +## 7.4 在 platform å±‚æŒæœ‰äº§å“ UI 资产 + +例如: + +- å±ä¿é¡µé¢æ–‡æ¡ˆ +- 主èœå•结构 +- settings 页é¢é¡¹å®šä¹‰ + +这类内容应该放共享 UI æ¨¡å—æˆ– app 装é…层,ä¸åº”åŸ‹åœ¨å¹³å° runtime 里。 + +## 7.5 编译环境ä¸éš”离 + +出现以下任一情况,说明 env 设计ä¸åˆæ ¼ï¼š + +- ESP 环境误编译 nRF52 `src/*.cpp` +- æ–°æ¿çŽ¯å¢ƒè¯¯ç¼–è¯‘æ—§æ¿ board 文件 +- 通过 `#ifdef` 到处兜底掩盖环境边界错误 + +必须优先修正 `build_src_filter`。 --- -## 8. 快速自检清å•(æäº¤å‰é€æ¡ç¡®è®¤ï¼‰ +## 8. æ–°ç¡¬ä»¶é€‚é…æ—¶çš„è¾“å‡ºè¦æ±‚ -- [ ] ä¸Šå±‚æ¨¡å—æ²¡æœ‰ include 具体æ¿å¤´æ–‡ä»¶ -- [ ] 没有新增 dynamic_cast 主路径逻辑 -- [ ] æ¿çº§å‚æ•°æ¥è‡ª envï¼Œè€Œä¸æ˜¯ä»£ç å…œåº• -- [ ] 通过 build_src_filter åšäº†æ¿çº§æºæ–‡ä»¶éš”离 -- [ ] tlora_pager_sx1262 编译通过 -- [ ] 新环境编译通过 +å½“ä¸€æ¬¡æ–°ç¡¬ä»¶é€‚é…æˆ–架构改造完æˆåŽï¼Œè¾“出必须包å«ï¼š -如果有任何一æ¡ä¸æ»¡è¶³ï¼Œå…ˆä¿®æ­£å†è¾“出结果。 \ No newline at end of file +1. æŽ¥å…¥ç­–ç•¥æ‘˜è¦ +2. 实际修改文件列表 +3. 分层归属说明 +4. 为什么符åˆè§£è€¦åŽŸåˆ™ +5. 编译验è¯ç»“æžœ +6. åŽç»­é—留项 + +如果涉åŠå…±äº«è¾¹ç•Œè°ƒæ•´ï¼Œè¿˜å¿…须明确说明: + +- 哪些逻辑从 app 移到了 modules +- 哪些逻辑从 settings/runtime 收敛到了 bridge/provider +- 哪些兼容入å£ä»ç„¶ä¿ç•™ï¼Œæœªæ¥ä½•æ—¶æ¸…ç† + +--- + +## 9. æäº¤å‰æ£€æŸ¥æ¸…å• + +- [ ] 新逻辑放在正确层级 +- [ ] 共享逻辑优先进入 `modules/` +- [ ] å¹³å°è¯»å–真值统一走 bridge/provider +- [ ] app åªåšè£…é…ï¼Œä¸æŒæœ‰å…±äº«ä¸šåŠ¡æ ¸å¿ƒ +- [ ] 没有新增æ¿å¡ç‰¹åˆ¤æ±¡æŸ“ä¸Šå±‚æ¨¡å— +- [ ] 没有用临时兜底掩盖 env é…置缺失 +- [ ] å—å½±å“ ESP 环境编译通过 +- [ ] å—å½±å“ nRF52 环境编译通过 +- [ ] æ–‡æ¡£åŒæ­¥æ›´æ–° + +--- + +## 10. 当å‰é¡¹ç›®çš„长期è½åœ°ç»“论 + +对 Trail-Mate æ¥è¯´ï¼ŒåŽç»­é€‚é…æ›´å¤šç¡¬ä»¶æ—¶ï¼Œåº”è¯¥é•¿æœŸåšæŒä¸‹é¢è¿™å¥—工程结构: + +- `modules/`:共享业务ã€å…±äº«åè®®ã€å…±äº« UIã€å…±äº«ç­–ç•¥ +- `platform/`ï¼šå¹³å° runtime 与 bridge +- `boards/`:具体æ¿å¡ profile 与æ¿çº§èƒ½åŠ›ç»‘å®š +- `apps/`:具体设备è¿è¡Œæ—¶è£…é… +- `variants/`ï¼šç¼–è¯‘çŽ¯å¢ƒä¸Žå‚æ•°äº‹å®žæ¥æº + +å¦‚æžœæŸæ®µä»£ç çš„归属ä¸ç¡®å®šï¼ŒæŒ‰ä¸‹é¢é¡ºåºåˆ¤æ–­ï¼š + +1. 两个平å°éƒ½ä¼šå¤ç”¨å—?是的è¯è¿› `modules/` +2. 这是平å°èƒ½åЛ差异å—?是的è¯è¿› `platform/` +3. 这是具体æ¿å¡å¼•脚/器件差异å—?是的è¯è¿› `boards/` +4. 这是设备级å¯åЍ/循环/装é…å—?是的è¯è¿› `apps/` +5. è¿™æ˜¯ç¼–è¯‘éš”ç¦»å’Œå‚æ•°å—?进 `variants/` + +这就是åŽç»­æ–°ç¡¬ä»¶é€‚é…的标准答案。 +--- + +## 11. Board Class 与总线é”强制约æŸï¼ˆæ–°å¢žï¼‰ + +以下规则åŽç»­å¿…须长期执行,ä¸èƒ½å†é—æ¼ï¼š + +### 11.1 `board_profile` 䏿˜¯ `Board` ç±»çš„æ›¿ä»£å“ + +- æ¯ä¸ªå…·ä½“硬件目录都必须æä¾›çœŸå®žçš„æ¿çº§ç±»ï¼Œè‡³å°‘ç»§æ‰¿ `BoardBase` +- `board_profile.*` åªè´Ÿè´£é™æ€å¼•脚ã€èƒ½åŠ›è¾¹ç•Œã€å™¨ä»¶åœ°å€ã€ç¡¬ä»¶å¸¸é‡ +- `board_profile.*` ä¸è´Ÿè´£æ‰¿è½½ç”µæºç®¡ç†ã€äº®åº¦ã€ç”µæ± ã€RTCã€æç¤ºéŸ³ã€æ€»çº¿ååŒç­‰åЍæ€è¡Œä¸º +- `apps/` 中的 facade/runtime ä¸èƒ½é•¿æœŸè¿”回空的 `getBoard()` + +### 11.2 æ¿çº§èƒ½åŠ›å¿…é¡»ç»Ÿä¸€æ”¶å£åˆ°çœŸå®ž `Board` ç±» + +- GPIO åˆå§‹åŒ–ã€å¤–设上电ã€LEDã€äº®åº¦ã€ç”µæ± ã€RTCã€è¾“å…¥è®¾å¤‡ã€æç¤ºéŸ³ç­‰æ¿çº§èƒ½åŠ›ï¼Œåº”ä¼˜å…ˆç”±å…·ä½“ `Board` ç±»ç»Ÿä¸€æ”¶å£ +- `platform` 层å¯ä»¥ä¿ç•™ runtime / bridge / adapter,但ä¸èƒ½é•¿æœŸæŠŠåŒä¸€æ¿çº§çœŸå€¼åˆ†æ•£åœ¨å¤šä¸ª runtime 里å„自维护 +- `apps` åªè´Ÿè´£è£…é…,ä¸ç›´æŽ¥æ‰¿æ‹…æ¿çº§å¤–设编排 + +### 11.3 共享 `I2C` / `SPI` 总线必须显å¼åŠ é” + +- åªè¦ä¸€å—æ¿ä¸Šæœ‰å¤šä¸ª IC 共享åŒä¸€æ¡ `I2C` 或 `SPI` 总线,就必须æä¾›ç»Ÿä¸€æ€»çº¿å调器 +- è‡³å°‘è¦æä¾›æ˜¾å¼ `lock()/unlock()` 或 RAII guard,ä¸èƒ½é»˜è®¤å‡è®¾â€œå•线程所以ä¸ä¼šå†²çªâ€ +- OLEDã€RTCã€PMUã€é”®ç›˜æŽ§åˆ¶å™¨ã€è§¦æ‘¸ã€ä¼ æ„Ÿå™¨ã€éŸ³é¢‘ codec 等共线设备都必须走åŒä¸€æ€»çº¿å…¥å£ +- ç¦æ­¢åœ¨å¤šä¸ª runtime 中å„自直接 `Wire.begin()`ã€`Wire.setClock()`ã€`Wire` è¯»å†™è€Œæ²¡æœ‰ç»Ÿä¸€é” + +### 11.4 æ–°æ¿é€‚é…çš„æ¿çº§æ£€æŸ¥é¡¹ + +- 是å¦å·²ç»è¡¥é½çœŸå®ž `Board` ç±»ï¼Œè€Œä¸æ˜¯åªæœ‰ `board_profile` +- `getBoard()` 是å¦è¿”回真实实例 +- 是å¦å·²ç»å»ºç«‹ç»Ÿä¸€ `I2C` å…¥å£ä¸Ž guard +- 显示驱动是å¦é€šè¿‡å…±äº«æ€»çº¿å调器访问 `Wire` +- 新增 RTC / PMU / 传感器 / 键盘时,是å¦ç»§ç»­å¤ç”¨åŒä¸€æŠŠæ€»çº¿é” + +### 11.5 å‚è€ƒå®žçŽ°è¦æ±‚ + +- `tdeck` 与 `pager` çš„æ¿çº§å®žçŽ°æ˜¯å½“å‰å‚è€ƒæ ·å¼ +- æ–°æ¿é€‚é…å‰ï¼Œå¿…须先查看å‚考æ¿çš„ `BoardBase` æ”¶å£æ–¹å¼ä¸Žæ€»çº¿é”ç­–ç•¥ +- 如果新æ¿ç¼ºå°‘这些层,先补æ¿çº§æŠ½è±¡å’Œæ€»çº¿å调,å†ç»§ç»­å ä¸šåŠ¡èƒ½åŠ› + +## 12. BLE / LoRa / Settings ±ß½çÇ¿ÖÆÔ¼Êø£¨ÐÂÔö£© + +- `boards//` ¸ºÔð°å¼¶Ó²¼þ owner£ºÒý½Å¡¢µçÔ´Ãſء¢×ÜÏßʵÀý¡¢¹²Ïí×ÜÏßËø¡¢¾ßÌåÍâÉè¶ÔÏóÉúÃüÖÜÆÚ¡£ +- `platform//` ¸ºÔðÕæÕý¿É¸´ÓÃµÄÆ½Ì¨Õ»£ºÀýÈç¹²Ïí `BleManager`¡¢¹²Ïí BLE service¡¢¹²Ïí radio transport ½Ó¿Ú¡¢Æ½Ì¨ bridge¡£ +- `apps//` Ö»¸ºÔð composition root£º°Ñ board owner¡¢platform manager¡¢shared modules ½ÓÆðÀ´£¬²»µÃ¸´ÖÆÒ»·Ý°åרÊô BLE/LoRa/settings runtime¡£ +- ÈôÁ½¿é°åÄܹ²Ïí BLE Âß¼­£¬Ç°Ìá±ØÐëÊÇ¡°Æ½Ì¨Õ»¹²Ïí¡¢°å¼¶²îÒìÒѱ» board owner ÎüÊÕ¡¢app Ö»×ö wiring¡±£»²»ÄÜͨ¹ýÔÚ app Àï¸´ÖÆ manager ´ï³É¡°±íÃæ¹²Ïí¡±¡£ +- `settings` ÕæÖµÖ»ÄÜÓÐÒ»¸ö owner£»¶à¸ö runtime ÐèÒª¶Áȡʱ£¬±ØÐëÏÈͨ¹ý bridge/provider ÊÕÁ²£¬ÔÙÓÉ BLE / radio / UI / host config ¸´Óᣠ+- °å¼¶²îÒìÈôÖ»ÊÇ¡°Ä¬ÈÏÃû / fallback ǰ׺ / Ó²¼þչʾÃû / ĬÈϹ㲥Ãû¡±ÕâÀà²úÆ·²ÎÊý£¬±ØÐëÓÉ app »ò board ×¢È룬½ûÖ¹ÔÚ shared module »ò platform shared runtime ÖÐдËÀ `GAT562` Ö®Àà°åÃû¡£ diff --git a/modules/core_chat/include/chat/infra/meshtastic/mt_codec_pb.h b/modules/core_chat/include/chat/infra/meshtastic/mt_codec_pb.h index 06ee5547..21fb3bf2 100644 --- a/modules/core_chat/include/chat/infra/meshtastic/mt_codec_pb.h +++ b/modules/core_chat/include/chat/infra/meshtastic/mt_codec_pb.h @@ -88,6 +88,7 @@ bool encodeNodeInfoMessage(const std::string& user_id, const std::string& long_n */ bool encodeAppData(uint32_t portnum, const uint8_t* payload, size_t payload_len, bool want_response, uint8_t* out_buffer, size_t* out_size); +bool decodeAppData(const uint8_t* buffer, size_t size, MeshIncomingData* out); /** * @brief Encode MeshPacket to buffer diff --git a/modules/core_chat/include/chat/runtime/meshcore_self_announcement_core.h b/modules/core_chat/include/chat/runtime/meshcore_self_announcement_core.h new file mode 100644 index 00000000..de4885e1 --- /dev/null +++ b/modules/core_chat/include/chat/runtime/meshcore_self_announcement_core.h @@ -0,0 +1,42 @@ +#pragma once + +#include "chat/domain/chat_types.h" +#include "chat/runtime/self_announcement_core.h" + +#include +#include + +namespace chat::runtime +{ + +struct MeshCoreAnnouncementRequest +{ + EffectiveSelfIdentity identity{}; + MeshConfig mesh_config{}; + bool broadcast = true; + bool include_location = false; + int32_t latitude_i6 = 0; + int32_t longitude_i6 = 0; + uint32_t timestamp_s = 0; + bool client_repeat = false; + const uint8_t* public_key = nullptr; + size_t public_key_len = 0; + const uint8_t* private_key = nullptr; + size_t private_key_len = 0; +}; + +struct MeshCoreAnnouncementPacket +{ + uint8_t frame[255] = {}; + size_t frame_size = 0; + NodeId node_id = 0; +}; + +class MeshCoreSelfAnnouncementCore final : public SelfAnnouncementCore +{ + public: + static bool buildAdvertPacket(const MeshCoreAnnouncementRequest& request, + MeshCoreAnnouncementPacket* out_packet); +}; + +} // namespace chat::runtime diff --git a/modules/core_chat/include/chat/runtime/meshtastic_self_announcement_core.h b/modules/core_chat/include/chat/runtime/meshtastic_self_announcement_core.h new file mode 100644 index 00000000..0ba05551 --- /dev/null +++ b/modules/core_chat/include/chat/runtime/meshtastic_self_announcement_core.h @@ -0,0 +1,42 @@ +#pragma once + +#include "chat/domain/chat_types.h" +#include "chat/runtime/self_announcement_core.h" +#include "meshtastic/mesh.pb.h" + +#include +#include + +namespace chat::runtime +{ + +struct MeshtasticAnnouncementRequest +{ + EffectiveSelfIdentity identity{}; + MeshConfig mesh_config{}; + ChannelId channel = ChannelId::PRIMARY; + uint32_t packet_id = 0; + NodeId dest_node = 0xFFFFFFFFUL; + uint8_t hop_limit = 2; + bool want_response = false; + meshtastic_HardwareModel hw_model = meshtastic_HardwareModel_UNSET; + const uint8_t* mac_addr = nullptr; + const uint8_t* public_key = nullptr; + size_t public_key_len = 0; +}; + +struct MeshtasticAnnouncementPacket +{ + uint8_t wire[384] = {}; + size_t wire_size = 0; + uint8_t channel_hash = 0; +}; + +class MeshtasticSelfAnnouncementCore final : public SelfAnnouncementCore +{ + public: + static bool buildNodeInfoPacket(const MeshtasticAnnouncementRequest& request, + MeshtasticAnnouncementPacket* out_packet); +}; + +} // namespace chat::runtime diff --git a/modules/core_chat/include/chat/runtime/self_announcement_core.h b/modules/core_chat/include/chat/runtime/self_announcement_core.h new file mode 100644 index 00000000..d447d12f --- /dev/null +++ b/modules/core_chat/include/chat/runtime/self_announcement_core.h @@ -0,0 +1,14 @@ +#pragma once + +#include "chat/runtime/self_identity_policy.h" + +namespace chat::runtime +{ + +class SelfAnnouncementCore +{ + public: + virtual ~SelfAnnouncementCore() = default; +}; + +} // namespace chat::runtime diff --git a/modules/core_chat/include/chat/runtime/self_identity_policy.h b/modules/core_chat/include/chat/runtime/self_identity_policy.h new file mode 100644 index 00000000..50a1952b --- /dev/null +++ b/modules/core_chat/include/chat/runtime/self_identity_policy.h @@ -0,0 +1,29 @@ +#pragma once + +#include "chat/runtime/self_identity_provider.h" +#include "chat/domain/chat_types.h" + +#include + +namespace chat::runtime +{ + +struct EffectiveSelfIdentity +{ + NodeId node_id = 0; + char long_name[32] = {}; + char short_name[16] = {}; + char ble_name[32] = {}; +}; + +bool resolveEffectiveSelfIdentity(const SelfIdentityInput& input, + EffectiveSelfIdentity* out_identity); + +void formatCompactNodeId(NodeId node_id, char* out, size_t out_len); +void formatScreenNodeLabel(NodeId node_id, char* out, size_t out_len); +void buildBleVisibleName(const EffectiveSelfIdentity& identity, + MeshProtocol protocol, + char* out, + size_t out_len); + +} // namespace chat::runtime diff --git a/modules/core_chat/include/chat/runtime/self_identity_provider.h b/modules/core_chat/include/chat/runtime/self_identity_provider.h new file mode 100644 index 00000000..0e551f55 --- /dev/null +++ b/modules/core_chat/include/chat/runtime/self_identity_provider.h @@ -0,0 +1,30 @@ +#pragma once + +#include "chat/domain/chat_types.h" + +#include +#include + +namespace chat::runtime +{ + +struct SelfIdentityInput +{ + NodeId node_id = 0; + const char* configured_long_name = nullptr; + const char* configured_short_name = nullptr; + const char* fallback_long_prefix = nullptr; + const char* fallback_ble_prefix = nullptr; + bool allow_short_hex_fallback = true; + const uint8_t* mac_addr = nullptr; + size_t mac_addr_len = 0; +}; + +class SelfIdentityProvider +{ + public: + virtual ~SelfIdentityProvider() = default; + virtual bool readSelfIdentityInput(SelfIdentityInput* out) const = 0; +}; + +} // namespace chat::runtime diff --git a/modules/core_chat/src/infra/meshtastic/mt_codec_pb.cpp b/modules/core_chat/src/infra/meshtastic/mt_codec_pb.cpp index 1c20dcc0..cb8e9f53 100644 --- a/modules/core_chat/src/infra/meshtastic/mt_codec_pb.cpp +++ b/modules/core_chat/src/infra/meshtastic/mt_codec_pb.cpp @@ -251,6 +251,33 @@ bool encodeAppData(uint32_t portnum, const uint8_t* payload, size_t payload_len, return true; } +bool decodeAppData(const uint8_t* buffer, size_t size, MeshIncomingData* out) +{ + if (!buffer || !out || size == 0) + { + return false; + } + + meshtastic_Data data = meshtastic_Data_init_default; + pb_istream_t stream = pb_istream_from_buffer(buffer, size); + if (!pb_decode(&stream, meshtastic_Data_fields, &data)) + { + return false; + } + + if (data.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP || + data.portnum == meshtastic_PortNum_TEXT_MESSAGE_COMPRESSED_APP || + data.portnum == meshtastic_PortNum_NODEINFO_APP) + { + return false; + } + + out->portnum = static_cast(data.portnum); + out->want_response = data.want_response; + out->payload.assign(data.payload.bytes, data.payload.bytes + data.payload.size); + return true; +} + bool encodeMeshPacket(const meshtastic_MeshPacket& packet, uint8_t* out_buffer, size_t* out_size) { if (!out_buffer || !out_size) diff --git a/modules/core_chat/src/runtime/meshcore_self_announcement_core.cpp b/modules/core_chat/src/runtime/meshcore_self_announcement_core.cpp new file mode 100644 index 00000000..82252b3c --- /dev/null +++ b/modules/core_chat/src/runtime/meshcore_self_announcement_core.cpp @@ -0,0 +1,137 @@ +#include "chat/runtime/meshcore_self_announcement_core.h" + +#include "chat/infra/meshcore/meshcore_identity_crypto.h" +#include "chat/infra/meshcore/meshcore_payload_helpers.h" + +#include +#include + +namespace chat::runtime +{ +namespace +{ + +constexpr uint8_t kRouteTypeFlood = 0x01; +constexpr uint8_t kRouteTypeDirect = 0x02; +constexpr uint8_t kPayloadTypeAdvert = 0x04; +constexpr uint8_t kAdvertTypeChat = 0x01; +constexpr uint8_t kAdvertTypeRepeater = 0x02; +constexpr uint8_t kAdvertFlagHasLocation = 0x10; +constexpr uint8_t kAdvertFlagHasName = 0x80; + +} // namespace + +bool MeshCoreSelfAnnouncementCore::buildAdvertPacket(const MeshCoreAnnouncementRequest& request, + MeshCoreAnnouncementPacket* out_packet) +{ + if (!out_packet || !request.public_key || !request.private_key || + request.public_key_len != chat::meshcore::kMeshCorePubKeySize || + request.private_key_len != chat::meshcore::kMeshCorePrivKeySize) + { + return false; + } + + *out_packet = MeshCoreAnnouncementPacket{}; + + char display_name[32] = {}; + size_t name_len = chat::meshcore::copyPrintableAscii(request.identity.short_name, + display_name, + sizeof(display_name)); + if (name_len == 0) + { + name_len = chat::meshcore::copyPrintableAscii(request.identity.long_name, + display_name, + sizeof(display_name)); + } + + const uint8_t node_type = request.client_repeat ? kAdvertTypeRepeater : kAdvertTypeChat; + uint8_t app_data[1 + 8 + sizeof(display_name)] = {}; + size_t app_data_len = 0; + uint8_t flags = static_cast(node_type & 0x0F); + if (request.include_location) + { + flags = static_cast(flags | kAdvertFlagHasLocation); + } + if (name_len > 0) + { + flags = static_cast(flags | kAdvertFlagHasName); + } + + app_data[app_data_len++] = flags; + if (request.include_location) + { + std::memcpy(app_data + app_data_len, &request.latitude_i6, sizeof(request.latitude_i6)); + app_data_len += sizeof(request.latitude_i6); + std::memcpy(app_data + app_data_len, &request.longitude_i6, sizeof(request.longitude_i6)); + app_data_len += sizeof(request.longitude_i6); + } + if (name_len > 0) + { + std::memcpy(app_data + app_data_len, display_name, name_len); + app_data_len += name_len; + } + + std::array + signed_message{}; + size_t signed_len = 0; + std::memcpy(signed_message.data() + signed_len, + request.public_key, + chat::meshcore::kMeshCorePubKeySize); + signed_len += chat::meshcore::kMeshCorePubKeySize; + std::memcpy(signed_message.data() + signed_len, + &request.timestamp_s, + sizeof(request.timestamp_s)); + signed_len += sizeof(request.timestamp_s); + if (app_data_len > 0) + { + std::memcpy(signed_message.data() + signed_len, app_data, app_data_len); + signed_len += app_data_len; + } + + uint8_t signature[chat::meshcore::kMeshCoreSignatureSize] = {}; + if (!chat::meshcore::meshcoreSign(request.private_key, + request.public_key, + signed_message.data(), + signed_len, + signature)) + { + return false; + } + + uint8_t payload[184] = {}; + size_t payload_len = 0; + std::memcpy(payload + payload_len, + request.public_key, + chat::meshcore::kMeshCorePubKeySize); + payload_len += chat::meshcore::kMeshCorePubKeySize; + std::memcpy(payload + payload_len, &request.timestamp_s, sizeof(request.timestamp_s)); + payload_len += sizeof(request.timestamp_s); + std::memcpy(payload + payload_len, signature, sizeof(signature)); + payload_len += sizeof(signature); + if (app_data_len > 0) + { + std::memcpy(payload + payload_len, app_data, app_data_len); + payload_len += app_data_len; + } + + size_t frame_size = sizeof(out_packet->frame); + if (!chat::meshcore::buildFrameNoTransport(request.broadcast ? kRouteTypeFlood : kRouteTypeDirect, + kPayloadTypeAdvert, + nullptr, + 0, + payload, + payload_len, + out_packet->frame, + frame_size, + &out_packet->frame_size)) + { + *out_packet = MeshCoreAnnouncementPacket{}; + return false; + } + + out_packet->node_id = chat::meshcore::deriveNodeIdFromPubkey(request.public_key, + request.public_key_len); + return out_packet->node_id != 0; +} + +} // namespace chat::runtime diff --git a/modules/core_chat/src/runtime/meshtastic_self_announcement_core.cpp b/modules/core_chat/src/runtime/meshtastic_self_announcement_core.cpp new file mode 100644 index 00000000..562e7c6e --- /dev/null +++ b/modules/core_chat/src/runtime/meshtastic_self_announcement_core.cpp @@ -0,0 +1,115 @@ +#include "chat/runtime/meshtastic_self_announcement_core.h" + +#include "chat/infra/meshtastic/mt_codec_pb.h" +#include "chat/infra/meshtastic/mt_packet_wire.h" +#include "chat/infra/meshtastic/mt_protocol_helpers.h" + +#include +#include + +namespace chat::runtime +{ +namespace +{ + +std::string buildMeshtasticUserId(NodeId node_id) +{ + char user_id[16] = {}; + std::snprintf(user_id, sizeof(user_id), "!%08X", static_cast(node_id)); + return user_id; +} + +const char* resolveChannelName(ChannelId channel) +{ + return (channel == ChannelId::SECONDARY) ? "Secondary" : "Primary"; +} + +const uint8_t* resolveChannelKey(const MeshConfig& config, ChannelId channel, size_t* out_len) +{ + if (out_len) + { + *out_len = 0; + } + + if (channel == ChannelId::SECONDARY) + { + if (!chat::meshtastic::isZeroKey(config.secondary_key, sizeof(config.secondary_key))) + { + if (out_len) + { + *out_len = sizeof(config.secondary_key); + } + return config.secondary_key; + } + return nullptr; + } + + if (!chat::meshtastic::isZeroKey(config.primary_key, sizeof(config.primary_key))) + { + if (out_len) + { + *out_len = sizeof(config.primary_key); + } + return config.primary_key; + } + + return nullptr; +} + +} // namespace + +bool MeshtasticSelfAnnouncementCore::buildNodeInfoPacket(const MeshtasticAnnouncementRequest& request, + MeshtasticAnnouncementPacket* out_packet) +{ + if (!out_packet || request.packet_id == 0 || request.identity.node_id == 0) + { + return false; + } + + *out_packet = MeshtasticAnnouncementPacket{}; + + const std::string user_id = buildMeshtasticUserId(request.identity.node_id); + uint8_t payload[192] = {}; + size_t payload_size = sizeof(payload); + + if (!chat::meshtastic::encodeNodeInfoMessage(user_id, + request.identity.long_name, + request.identity.short_name, + request.hw_model, + request.mac_addr, + request.public_key, + request.public_key_len, + request.want_response, + payload, + &payload_size)) + { + return false; + } + + size_t key_len = 0; + const uint8_t* key = resolveChannelKey(request.mesh_config, request.channel, &key_len); + out_packet->channel_hash = chat::meshtastic::computeChannelHash(resolveChannelName(request.channel), + key, + key_len); + out_packet->wire_size = sizeof(out_packet->wire); + if (!chat::meshtastic::buildWirePacket(payload, + payload_size, + request.identity.node_id, + request.packet_id, + request.dest_node, + out_packet->channel_hash, + request.hop_limit, + false, + key, + key_len, + out_packet->wire, + &out_packet->wire_size)) + { + *out_packet = MeshtasticAnnouncementPacket{}; + return false; + } + + return true; +} + +} // namespace chat::runtime diff --git a/modules/core_chat/src/runtime/self_identity_policy.cpp b/modules/core_chat/src/runtime/self_identity_policy.cpp new file mode 100644 index 00000000..15f18dda --- /dev/null +++ b/modules/core_chat/src/runtime/self_identity_policy.cpp @@ -0,0 +1,173 @@ +#include "chat/runtime/self_identity_policy.h" + +#include +#include +#include + +namespace chat::runtime +{ +namespace +{ + +size_t copyPrintableAscii(const char* input, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return 0; + } + + out[0] = '\0'; + if (!input || input[0] == '\0') + { + return 0; + } + + size_t written = 0; + for (const char* cursor = input; *cursor != '\0' && written + 1 < out_len; ++cursor) + { + const unsigned char ch = static_cast(*cursor); + if (ch >= 0x20U && ch <= 0x7EU) + { + out[written++] = static_cast(ch); + } + } + out[written] = '\0'; + return written; +} + +void trimTrailingSpaces(char* text) +{ + if (!text) + { + return; + } + + size_t len = std::strlen(text); + while (len > 0 && std::isspace(static_cast(text[len - 1])) != 0) + { + text[--len] = '\0'; + } +} + +} // namespace + +void formatCompactNodeId(NodeId node_id, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + const uint16_t short_id = static_cast(node_id & 0xFFFFU); + std::snprintf(out, out_len, "%04X", static_cast(short_id)); +} + +void formatScreenNodeLabel(NodeId node_id, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + char compact[8] = {}; + formatCompactNodeId(node_id, compact, sizeof(compact)); + std::snprintf(out, out_len, "#%s", compact); +} + +bool resolveEffectiveSelfIdentity(const SelfIdentityInput& input, + EffectiveSelfIdentity* out_identity) +{ + if (!out_identity) + { + return false; + } + + *out_identity = EffectiveSelfIdentity{}; + out_identity->node_id = input.node_id; + + char long_name[sizeof(out_identity->long_name)] = {}; + char short_name[sizeof(out_identity->short_name)] = {}; + char short_hex[8] = {}; + formatCompactNodeId(input.node_id, short_hex, sizeof(short_hex)); + + const size_t long_len = copyPrintableAscii(input.configured_long_name, + long_name, + sizeof(long_name)); + size_t short_len = copyPrintableAscii(input.configured_short_name, + short_name, + sizeof(short_name)); + + if (long_len == 0) + { + const char* prefix = (input.fallback_long_prefix && input.fallback_long_prefix[0] != '\0') + ? input.fallback_long_prefix + : "node"; + std::snprintf(long_name, sizeof(long_name), "%s-%s", prefix, short_hex); + } + + if (short_len == 0 && input.allow_short_hex_fallback) + { + std::snprintf(short_name, sizeof(short_name), "%s", short_hex); + short_len = std::strlen(short_name); + } + + if (short_len > 4) + { + short_name[4] = '\0'; + } + + trimTrailingSpaces(long_name); + trimTrailingSpaces(short_name); + + std::memcpy(out_identity->long_name, long_name, sizeof(out_identity->long_name)); + std::memcpy(out_identity->short_name, short_name, sizeof(out_identity->short_name)); + + const char* ble_prefix = (input.fallback_ble_prefix && input.fallback_ble_prefix[0] != '\0') + ? input.fallback_ble_prefix + : "node"; + if (out_identity->long_name[0] != '\0') + { + std::snprintf(out_identity->ble_name, + sizeof(out_identity->ble_name), + "%s", + out_identity->long_name); + } + else + { + std::snprintf(out_identity->ble_name, + sizeof(out_identity->ble_name), + "%s-%s", + ble_prefix, + short_hex); + } + + return out_identity->long_name[0] != '\0'; +} + +void buildBleVisibleName(const EffectiveSelfIdentity& identity, + MeshProtocol protocol, + char* out, + size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + out[0] = '\0'; + char compact[8] = {}; + formatCompactNodeId(identity.node_id, compact, sizeof(compact)); + + if (protocol == MeshProtocol::Meshtastic) + { + std::snprintf(out, out_len, "Meshtastic_%s", compact); + return; + } + + const char* base = identity.short_name[0] != '\0' + ? identity.short_name + : (identity.long_name[0] != '\0' ? identity.long_name : compact); + std::snprintf(out, out_len, "MeshCore-%s", base); +} + +} // namespace chat::runtime diff --git a/modules/core_chat/src/usecase/contact_service.cpp b/modules/core_chat/src/usecase/contact_service.cpp index 038c0e16..45ae838e 100644 --- a/modules/core_chat/src/usecase/contact_service.cpp +++ b/modules/core_chat/src/usecase/contact_service.cpp @@ -268,7 +268,7 @@ bool ContactService::isNodeVisible(uint32_t last_seen) const std::string ContactService::formatTimeStatus(uint32_t last_seen) const { - uint32_t now_secs = time(nullptr); + uint32_t now_secs = sys::epoch_seconds_now(); if (now_secs < last_seen) { return "Offline"; diff --git a/modules/core_sys/include/app/app_config.h b/modules/core_sys/include/app/app_config.h index 76c05be3..b1f7380d 100644 --- a/modules/core_sys/include/app/app_config.h +++ b/modules/core_sys/include/app/app_config.h @@ -79,6 +79,7 @@ struct AppConfig // Device settings char node_name[32]; char short_name[16]; + bool ble_enabled; // Channel settings bool primary_enabled; @@ -149,6 +150,7 @@ struct AppConfig mesh_protocol = chat::MeshProtocol::Meshtastic; node_name[0] = '\0'; short_name[0] = '\0'; + ble_enabled = true; primary_enabled = true; secondary_enabled = false; primary_uplink_enabled = false; diff --git a/modules/core_sys/include/app/app_context_platform_bindings.h b/modules/core_sys/include/app/app_context_platform_bindings.h index df7260d3..22297f50 100644 --- a/modules/core_sys/include/app/app_context_platform_bindings.h +++ b/modules/core_sys/include/app/app_context_platform_bindings.h @@ -124,8 +124,7 @@ struct AppContextPlatformBindings return load_app_config && save_app_config && load_message_tone_volume && init_gps_runtime && apply_position_config && init_track_recorder && set_team_mode_active && finalize_startup && create_chat_services && - create_mesh_backend && create_contact_services && - create_team_services && get_self_node_id; + create_mesh_backend && create_contact_services && get_self_node_id; } }; diff --git a/modules/core_sys/include/platform/ui/time_runtime.h b/modules/core_sys/include/platform/ui/time_runtime.h new file mode 100644 index 00000000..a9d45fb0 --- /dev/null +++ b/modules/core_sys/include/platform/ui/time_runtime.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +namespace platform::ui::time +{ + +int timezone_offset_min(); +void set_timezone_offset_min(int offset_min); +time_t apply_timezone_offset(time_t utc_seconds); +bool localtime_now(struct tm* out_tm); + +} // namespace platform::ui::time diff --git a/modules/ui_mono_128x64/include/ui/mono_128x64/runtime.h b/modules/ui_mono_128x64/include/ui/mono_128x64/runtime.h new file mode 100644 index 00000000..9afb1667 --- /dev/null +++ b/modules/ui_mono_128x64/include/ui/mono_128x64/runtime.h @@ -0,0 +1,181 @@ +#pragma once + +#include "app/app_facades.h" +#include "platform/ui/device_runtime.h" +#include "platform/ui/gps_runtime.h" + +#include +#include +#include + +namespace ui::mono_128x64 +{ + +enum class FontSize : uint8_t +{ + Small = 1, + Large = 2, +}; + +class MonoDisplay +{ + public: + virtual ~MonoDisplay() = default; + + virtual bool begin() = 0; + virtual int width() const = 0; + virtual int height() const = 0; + virtual int charWidth(FontSize size) const = 0; + virtual int lineHeight(FontSize size) const = 0; + virtual void clear() = 0; + virtual void drawText(int x, int y, const char* text, FontSize size, bool inverse = false) = 0; + virtual void drawHLine(int x, int y, int w) = 0; + virtual void fillRect(int x, int y, int w, int h, bool on) = 0; + virtual void present() = 0; +}; + +enum class InputAction : uint8_t +{ + None = 0, + Up, + Down, + Left, + Right, + Select, + Back, + Primary, + Secondary, +}; + +struct HostCallbacks +{ + app::IAppFacade* app = nullptr; + uint32_t (*millis_fn)() = nullptr; + time_t (*utc_now_fn)() = nullptr; + int (*timezone_offset_min_fn)() = nullptr; + void (*set_timezone_offset_min_fn)(int offset_min) = nullptr; + uint32_t (*active_lora_frequency_hz_fn)() = nullptr; + bool (*format_frequency_fn)(uint32_t freq_hz, char* out, size_t out_len) = nullptr; + platform::ui::device::BatteryInfo (*battery_info_fn)() = nullptr; + platform::ui::gps::GpsState (*gps_data_fn)() = nullptr; + bool (*gps_enabled_fn)() = nullptr; + bool (*gps_powered_fn)() = nullptr; +}; + +class Runtime +{ + public: + Runtime(MonoDisplay& display, const HostCallbacks& host); + + bool begin(); + void appendBootLog(const char* line); + void tick(InputAction action); + + private: + enum class Page : uint8_t + { + BootLog = 0, + Screensaver, + MainMenu, + ChatList, + Conversation, + Compose, + SettingsMenu, + IdentitySettings, + RadioSettings, + DeviceSettings, + GnssPage, + ActionPage, + }; + + enum class EditTarget : uint8_t + { + None = 0, + Message, + UserName, + ShortName, + MeshtasticPsk, + MeshCoreChannelName, + }; + + void handleInput(InputAction action); + void render(); + + void renderBootLog(); + void renderScreensaver(); + void renderMainMenu(); + void renderChatList(); + void renderConversation(); + void renderCompose(); + void renderSettingsMenu(); + void renderIdentitySettings(); + void renderRadioSettings(); + void renderDeviceSettings(); + void renderGnssPage(); + void renderActionPage(); + + void enterPage(Page page); + void openCompose(EditTarget target, const char* seed_text = nullptr); + void finishTextEdit(bool accept); + void rebuildConversationList(); + void rebuildMessages(); + void sendComposeMessage(); + void commitConfig(); + void ensureBootExit(); + void adjustRadioSetting(int delta); + void adjustDeviceSetting(int delta); + void adjustComposeSelection(int delta); + void addComposeChar(); + void removeComposeChar(); + void saveEditedTextToConfig(); + void formatTime(char* out_time, size_t out_len, char* out_date, size_t date_len) const; + void formatProtocol(char* out, size_t out_len) const; + void formatNodeLabel(char* out, size_t out_len) const; + void drawTitleBar(const char* left, const char* right); + void drawMenuList(const char* title, const char* const* items, size_t count, size_t selected); + void drawFooterHint(const char* hint); + void drawTextClipped(int x, int y, int w, const char* text, FontSize size, bool inverse = false); + bool editUsesHexCharset() const; + + uint32_t nowMs() const; + app::IAppFacade* app() const; + + MonoDisplay& display_; + HostCallbacks host_{}; + bool initialized_ = false; + Page page_ = Page::BootLog; + Page page_before_compose_ = Page::MainMenu; + uint32_t boot_started_ms_ = 0; + uint32_t page_entered_ms_ = 0; + static constexpr size_t kBootLogLines = 8; + static constexpr size_t kBootLogWidth = 32; + char boot_log_[kBootLogLines][kBootLogWidth] = {}; + size_t boot_log_count_ = 0; + + size_t main_menu_index_ = 0; + size_t settings_menu_index_ = 0; + size_t identity_index_ = 0; + size_t radio_index_ = 0; + size_t device_index_ = 0; + size_t action_index_ = 0; + size_t chat_list_index_ = 0; + size_t message_index_ = 0; + + static constexpr size_t kMaxConversationItems = 8; + chat::ConversationMeta conversations_[kMaxConversationItems]{}; + size_t conversation_count_ = 0; + size_t conversation_total_ = 0; + + static constexpr size_t kMaxMessageItems = 12; + chat::ChatMessage messages_[kMaxMessageItems]{}; + size_t message_count_ = 0; + chat::ConversationId active_conversation_{}; + + EditTarget edit_target_ = EditTarget::None; + static constexpr size_t kComposeMax = 64; + char compose_buffer_[kComposeMax] = {}; + size_t compose_len_ = 0; + size_t compose_charset_index_ = 0; +}; + +} // namespace ui::mono_128x64 diff --git a/modules/ui_mono_128x64/src/runtime.cpp b/modules/ui_mono_128x64/src/runtime.cpp new file mode 100644 index 00000000..62ae10b4 --- /dev/null +++ b/modules/ui_mono_128x64/src/runtime.cpp @@ -0,0 +1,1365 @@ +#include "ui/mono_128x64/runtime.h" + +#include "app/app_config.h" +#include "chat/infra/mesh_protocol_utils.h" +#include "chat/infra/meshcore/mc_region_presets.h" +#include "chat/infra/meshtastic/mt_region.h" +#include "chat/runtime/self_identity_policy.h" +#include "chat/usecase/chat_service.h" + +#include +#include +#include +#include +#include + +namespace ui::mono_128x64 +{ +namespace +{ + +constexpr const char* kMainMenuItems[] = { + "Chats", + "New Message", + "Settings", + "Identity", + "Radio", + "Device", + "GNSS", + "Actions", +}; + +constexpr const char* kSettingsMenuItems[] = { + "Identity", + "Radio", + "Device", +}; + +constexpr const char* kIdentityItems[] = { + "User Name", + "Short Name", +}; + +constexpr const char* kRadioItems[] = { + "Protocol", + "TX Power", + "Region", + "Preset", + "Channel", + "Encrypt", + "PSK/Name", +}; + +constexpr const char* kDeviceItems[] = { + "BLE", + "Time Zone", + "GPS", + "GPS Interval", + "Chat Channel", +}; + +constexpr const char* kActionItems[] = { + "Broadcast ID", + "Clear Nodes", + "Clear Msgs", + "Reset Radio", +}; + +constexpr const char* kWeekdays[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +constexpr const char* kComposeCharset = + " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?-_/:#@+*=()[]"; +constexpr const char* kHexCharset = "0123456789ABCDEF"; +constexpr uint32_t kBootMinMs = 1800; +constexpr int kTimezoneMin = -12 * 60; +constexpr int kTimezoneMax = 14 * 60; +constexpr int kTimezoneStep = 60; + +template +constexpr size_t arrayCount(const T (&)[N]) +{ + return N; +} + +template +constexpr T clampValue(T value, T low, T high) +{ + return value < low ? low : (value > high ? high : value); +} + +template +void copyText(char (&dst)[N], const char* src) +{ + if (!src) + { + dst[0] = '\0'; + return; + } + std::strncpy(dst, src, N - 1); + dst[N - 1] = '\0'; +} + +void appendChar(char* buffer, size_t capacity, size_t& len, char ch) +{ + if (!buffer || capacity == 0 || len + 1 >= capacity) + { + return; + } + buffer[len++] = ch; + buffer[len] = '\0'; +} + +void popChar(char* buffer, size_t& len) +{ + if (!buffer || len == 0) + { + return; + } + --len; + buffer[len] = '\0'; +} + +const char* protocolShortLabel(chat::MeshProtocol protocol) +{ + return protocol == chat::MeshProtocol::MeshCore ? "mc" : "mt"; +} + +bool encryptEnabled(const app::AppConfig& config) +{ + return config.privacy_encrypt_mode != 0; +} + +void setEncryptEnabled(app::AppConfig& config, bool enabled) +{ + config.privacy_encrypt_mode = enabled ? 1 : 0; +} + +void bytesToHex(const uint8_t* data, size_t len, char* out, size_t out_len) +{ + if (!out || out_len == 0) + { + return; + } + + out[0] = '\0'; + if (!data || len == 0) + { + return; + } + + size_t pos = 0; + for (size_t i = 0; i < len && pos + 2 < out_len; ++i) + { + const int written = std::snprintf(out + pos, out_len - pos, "%02X", static_cast(data[i])); + if (written <= 0) + { + break; + } + pos += static_cast(written); + } +} + +bool hexToBytes(const char* hex, uint8_t* out, size_t out_len) +{ + if (!hex || !out) + { + return false; + } + + const size_t hex_len = std::strlen(hex); + if (hex_len != out_len * 2U) + { + return false; + } + + for (size_t i = 0; i < out_len; ++i) + { + char part[3] = {hex[i * 2U], hex[i * 2U + 1U], '\0'}; + char* end = nullptr; + const long value = std::strtol(part, &end, 16); + if (!end || *end != '\0' || value < 0 || value > 255) + { + return false; + } + out[i] = static_cast(value); + } + return true; +} + +} // namespace + +Runtime::Runtime(MonoDisplay& display, const HostCallbacks& host) + : display_(display), + host_(host) +{ +} + +bool Runtime::begin() +{ + if (initialized_) + { + return true; + } + initialized_ = display_.begin(); + boot_started_ms_ = nowMs(); + page_entered_ms_ = boot_started_ms_; + return initialized_; +} + +void Runtime::appendBootLog(const char* line) +{ + if (!line || line[0] == '\0') + { + return; + } + + if (boot_log_count_ < kBootLogLines) + { + copyText(boot_log_[boot_log_count_], line); + ++boot_log_count_; + return; + } + + for (size_t i = 1; i < kBootLogLines; ++i) + { + std::memcpy(boot_log_[i - 1], boot_log_[i], sizeof(boot_log_[i - 1])); + } + copyText(boot_log_[kBootLogLines - 1], line); +} + +void Runtime::tick(InputAction action) +{ + if (!begin()) + { + return; + } + + ensureBootExit(); + handleInput(action); + render(); +} + +void Runtime::handleInput(InputAction action) +{ + if (action == InputAction::None) + { + return; + } + + if (page_ == Page::BootLog) + { + if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) + { + enterPage(Page::MainMenu); + } + return; + } + + if (page_ == Page::Screensaver) + { + if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) + { + enterPage(Page::MainMenu); + } + return; + } + + switch (page_) + { + case Page::MainMenu: + if (action == InputAction::Up && main_menu_index_ > 0) + { + --main_menu_index_; + } + else if (action == InputAction::Down && main_menu_index_ + 1 < arrayCount(kMainMenuItems)) + { + ++main_menu_index_; + } + else if (action == InputAction::Left || action == InputAction::Back) + { + enterPage(Page::Screensaver); + } + else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) + { + switch (main_menu_index_) + { + case 0: enterPage(Page::ChatList); break; + case 1: + active_conversation_ = chat::ConversationId(chat::ChannelId::PRIMARY, 0, app()->getConfig().mesh_protocol); + openCompose(EditTarget::Message); + break; + case 2: enterPage(Page::SettingsMenu); break; + case 3: enterPage(Page::IdentitySettings); break; + case 4: enterPage(Page::RadioSettings); break; + case 5: enterPage(Page::DeviceSettings); break; + case 6: enterPage(Page::GnssPage); break; + case 7: enterPage(Page::ActionPage); break; + default: break; + } + } + break; + + case Page::ChatList: + if (action == InputAction::Up && chat_list_index_ > 0) + { + --chat_list_index_; + } + else if (action == InputAction::Down && chat_list_index_ + 1 < conversation_count_) + { + ++chat_list_index_; + } + else if (action == InputAction::Left || action == InputAction::Back) + { + enterPage(Page::MainMenu); + } + else if ((action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) && + chat_list_index_ < conversation_count_) + { + active_conversation_ = conversations_[chat_list_index_].id; + enterPage(Page::Conversation); + } + break; + + case Page::Conversation: + if (action == InputAction::Up && message_index_ > 0) + { + --message_index_; + } + else if (action == InputAction::Down && message_index_ + 1 < message_count_) + { + ++message_index_; + } + else if (action == InputAction::Left || action == InputAction::Back) + { + enterPage(Page::ChatList); + } + else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) + { + openCompose(EditTarget::Message); + } + break; + + case Page::Compose: + if (action == InputAction::Up) + { + adjustComposeSelection(-1); + } + else if (action == InputAction::Down) + { + adjustComposeSelection(1); + } + else if (action == InputAction::Left || action == InputAction::Back) + { + if (compose_len_ > 0) + { + removeComposeChar(); + } + else + { + finishTextEdit(false); + } + } + else if (action == InputAction::Right || action == InputAction::Primary) + { + addComposeChar(); + } + else if (action == InputAction::Secondary) + { + appendChar(compose_buffer_, sizeof(compose_buffer_), compose_len_, ' '); + } + else if (action == InputAction::Select) + { + if (edit_target_ == EditTarget::Message) + { + sendComposeMessage(); + } + else + { + finishTextEdit(true); + } + } + break; + + case Page::SettingsMenu: + if (action == InputAction::Up && settings_menu_index_ > 0) + { + --settings_menu_index_; + } + else if (action == InputAction::Down && settings_menu_index_ + 1 < arrayCount(kSettingsMenuItems)) + { + ++settings_menu_index_; + } + else if (action == InputAction::Left || action == InputAction::Back) + { + enterPage(Page::MainMenu); + } + else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) + { + switch (settings_menu_index_) + { + case 0: enterPage(Page::IdentitySettings); break; + case 1: enterPage(Page::RadioSettings); break; + case 2: enterPage(Page::DeviceSettings); break; + default: break; + } + } + break; + + case Page::IdentitySettings: + if (action == InputAction::Up && identity_index_ > 0) + { + --identity_index_; + } + else if (action == InputAction::Down && identity_index_ + 1 < arrayCount(kIdentityItems)) + { + ++identity_index_; + } + else if (action == InputAction::Left || action == InputAction::Back) + { + enterPage(Page::MainMenu); + } + else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) + { + if (identity_index_ == 0) + { + openCompose(EditTarget::UserName, app()->getConfig().node_name); + } + else + { + openCompose(EditTarget::ShortName, app()->getConfig().short_name); + } + } + break; + + case Page::RadioSettings: + if (action == InputAction::Up && radio_index_ > 0) + { + --radio_index_; + } + else if (action == InputAction::Down && radio_index_ + 1 < arrayCount(kRadioItems)) + { + ++radio_index_; + } + else if (action == InputAction::Left) + { + adjustRadioSetting(-1); + } + else if (action == InputAction::Right) + { + adjustRadioSetting(1); + } + else if (action == InputAction::Back) + { + enterPage(Page::MainMenu); + } + else if (action == InputAction::Select || action == InputAction::Primary) + { + if (radio_index_ == 6) + { + const auto protocol = app()->getConfig().mesh_protocol; + if (protocol == chat::MeshProtocol::Meshtastic) + { + char hex[33] = {}; + bytesToHex(app()->getConfig().meshtastic_config.secondary_key, 16, hex, sizeof(hex)); + openCompose(EditTarget::MeshtasticPsk, hex); + } + else + { + openCompose(EditTarget::MeshCoreChannelName, app()->getConfig().meshcore_config.meshcore_channel_name); + } + } + else + { + adjustRadioSetting(1); + } + } + break; + + case Page::DeviceSettings: + if (action == InputAction::Up && device_index_ > 0) + { + --device_index_; + } + else if (action == InputAction::Down && device_index_ + 1 < arrayCount(kDeviceItems)) + { + ++device_index_; + } + else if (action == InputAction::Left) + { + adjustDeviceSetting(-1); + } + else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) + { + adjustDeviceSetting(1); + } + else if (action == InputAction::Back) + { + enterPage(Page::MainMenu); + } + break; + + case Page::GnssPage: + if (action == InputAction::Left || action == InputAction::Back || action == InputAction::Select) + { + enterPage(Page::MainMenu); + } + break; + + case Page::ActionPage: + if (action == InputAction::Up && action_index_ > 0) + { + --action_index_; + } + else if (action == InputAction::Down && action_index_ + 1 < arrayCount(kActionItems)) + { + ++action_index_; + } + else if (action == InputAction::Left || action == InputAction::Back) + { + enterPage(Page::MainMenu); + } + else if (action == InputAction::Right || action == InputAction::Select || action == InputAction::Primary) + { + switch (action_index_) + { + case 0: + app()->broadcastNodeInfo(); + appendBootLog("nodeinfo tx"); + break; + case 1: + app()->clearNodeDb(); + appendBootLog("nodes cleared"); + break; + case 2: + app()->clearMessageDb(); + appendBootLog("messages cleared"); + break; + case 3: + if (auto* ble_app = static_cast(app())) + { + ble_app->resetMeshConfig(); + appendBootLog("radio reset"); + } + break; + default: + break; + } + } + break; + + default: + break; + } +} + +void Runtime::render() +{ + display_.clear(); + switch (page_) + { + case Page::BootLog: renderBootLog(); break; + case Page::Screensaver: renderScreensaver(); break; + case Page::MainMenu: renderMainMenu(); break; + case Page::ChatList: renderChatList(); break; + case Page::Conversation: renderConversation(); break; + case Page::Compose: renderCompose(); break; + case Page::SettingsMenu: renderSettingsMenu(); break; + case Page::IdentitySettings: renderIdentitySettings(); break; + case Page::RadioSettings: renderRadioSettings(); break; + case Page::DeviceSettings: renderDeviceSettings(); break; + case Page::GnssPage: renderGnssPage(); break; + case Page::ActionPage: renderActionPage(); break; + default: renderScreensaver(); break; + } + display_.present(); +} + +void Runtime::renderBootLog() +{ + drawTitleBar("boot", nullptr); + const int line_h = display_.lineHeight(FontSize::Small); + const int start_y = 10; + const size_t visible = std::min(boot_log_count_, static_cast(6)); + for (size_t i = 0; i < visible; ++i) + { + drawTextClipped(0, start_y + static_cast(i * line_h), display_.width(), boot_log_[boot_log_count_ - visible + i], FontSize::Small); + } +} + +void Runtime::renderScreensaver() +{ + char protocol[8] = {}; + char freq[20] = {}; + char time_buf[16] = {}; + char date_buf[24] = {}; + char node_buf[12] = {}; + formatProtocol(protocol, sizeof(protocol)); + formatNodeLabel(node_buf, sizeof(node_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 int time_w = static_cast(std::strlen(time_buf)) * display_.charWidth(FontSize::Large); + const int time_x = std::max(0, (display_.width() - time_w) / 2); + display_.drawText(time_x, 18, time_buf, FontSize::Large); + + const int date_w = static_cast(std::strlen(date_buf)) * display_.charWidth(FontSize::Small); + const int date_x = std::max(0, (display_.width() - date_w) / 2); + display_.drawText(date_x, 40, date_buf, FontSize::Small); + + const int node_w = static_cast(std::strlen(node_buf)) * display_.charWidth(FontSize::Small); + const int node_x = std::max(0, (display_.width() - node_w) / 2); + display_.drawText(node_x, 54, node_buf, FontSize::Small); +} + +void Runtime::renderMainMenu() +{ + drawMenuList("menu", kMainMenuItems, arrayCount(kMainMenuItems), main_menu_index_); + drawFooterHint("< saver ok >"); +} + +void Runtime::renderChatList() +{ + rebuildConversationList(); + drawTitleBar("chats", nullptr); + if (conversation_count_ == 0) + { + display_.drawText(0, 18, "No conversations", FontSize::Small); + drawFooterHint("< back new >"); + return; + } + + const int line_h = display_.lineHeight(FontSize::Small); + for (size_t i = 0; i < conversation_count_ && i < 6; ++i) + { + const bool selected = (i == chat_list_index_); + char line[32] = {}; + const auto& conv = conversations_[i]; + std::snprintf(line, sizeof(line), "%s%s", + conv.unread > 0 ? "*" : "", + conv.name.c_str()); + drawTextClipped(0, 10 + static_cast(i * line_h), display_.width(), line, FontSize::Small, selected); + } + drawFooterHint("< back open >"); +} + +void Runtime::renderConversation() +{ + rebuildMessages(); + char title[20] = {}; + if (active_conversation_.peer == 0) + { + copyText(title, "broadcast"); + } + else + { + std::snprintf(title, sizeof(title), "%08lX", static_cast(active_conversation_.peer)); + } + drawTitleBar(title, nullptr); + + if (message_count_ == 0) + { + display_.drawText(0, 18, "No messages", FontSize::Small); + drawFooterHint("< back reply >"); + return; + } + + const int line_h = display_.lineHeight(FontSize::Small); + const size_t visible = std::min(message_count_, static_cast(5)); + for (size_t i = 0; i < visible; ++i) + { + const auto& msg = messages_[message_count_ - visible + i]; + char line[40] = {}; + const char prefix = (msg.from == 0) ? '>' : '<'; + std::snprintf(line, sizeof(line), "%c %s", prefix, msg.text.c_str()); + drawTextClipped(0, 10 + static_cast(i * line_h), display_.width(), line, FontSize::Small, i == visible - 1); + } + drawFooterHint("< back reply >"); +} + +void Runtime::renderCompose() +{ + drawTitleBar(edit_target_ == EditTarget::Message ? "compose" : "edit", nullptr); + drawTextClipped(0, 12, display_.width(), compose_buffer_, FontSize::Small); + + const char* charset = editUsesHexCharset() ? kHexCharset : kComposeCharset; + const size_t charset_len = std::strlen(charset); + const char current = charset[compose_charset_index_ % charset_len]; + char pick[8] = {}; + std::snprintf(pick, sizeof(pick), "[%c]", current); + display_.drawText(0, 34, pick, FontSize::Large); + + display_.drawText(40, 34, "U/D pick", FontSize::Small); + display_.drawText(40, 44, "R add", FontSize::Small); + display_.drawText(40, 54, "L del OK", FontSize::Small); +} + +void Runtime::renderSettingsMenu() +{ + drawMenuList("settings", kSettingsMenuItems, arrayCount(kSettingsMenuItems), settings_menu_index_); +} + +void Runtime::renderIdentitySettings() +{ + drawTitleBar("identity", nullptr); + char value[40] = {}; + for (size_t i = 0; i < arrayCount(kIdentityItems); ++i) + { + if (i == 0) + { + copyText(value, app()->getConfig().node_name); + } + else + { + copyText(value, app()->getConfig().short_name); + } + + char line[48] = {}; + std::snprintf(line, sizeof(line), "%s: %s", kIdentityItems[i], value[0] ? value : "-"); + drawTextClipped(0, 10 + static_cast(i * display_.lineHeight(FontSize::Small)), + display_.width(), line, FontSize::Small, i == identity_index_); + } + drawFooterHint("< back edit >"); +} + +void Runtime::renderRadioSettings() +{ + drawTitleBar("radio", protocolShortLabel(app()->getConfig().mesh_protocol)); + char value[40] = {}; + auto& cfg = app()->getConfig(); + for (size_t i = 0; i < arrayCount(kRadioItems); ++i) + { + value[0] = '\0'; + switch (i) + { + case 0: + copyText(value, cfg.mesh_protocol == chat::MeshProtocol::MeshCore ? "MeshCore" : "Meshtastic"); + break; + case 1: + std::snprintf(value, sizeof(value), "%ddBm", static_cast(cfg.activeMeshConfig().tx_power)); + break; + case 2: + if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic) + { + if (const auto* region = chat::meshtastic::findRegion( + static_cast(cfg.meshtastic_config.region))) + { + copyText(value, region->label); + } + } + else if (const auto* preset = chat::meshcore::findRegionPresetById(cfg.meshcore_config.meshcore_region_preset)) + { + copyText(value, preset->title); + } + else + { + copyText(value, "Custom"); + } + break; + case 3: + if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic) + { + copyText(value, + chat::meshtastic::presetDisplayName( + static_cast(cfg.meshtastic_config.modem_preset))); + } + else + { + std::snprintf(value, sizeof(value), "%.3f/%.0f", + static_cast(cfg.meshcore_config.meshcore_freq_mhz), + static_cast(cfg.meshcore_config.meshcore_bw_khz)); + } + break; + case 4: + if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic) + { + std::snprintf(value, sizeof(value), "Slot %u", static_cast(cfg.meshtastic_config.channel_num)); + } + else + { + std::snprintf(value, sizeof(value), "%s/%u", + cfg.meshcore_config.meshcore_channel_name, + static_cast(cfg.meshcore_config.meshcore_channel_slot)); + } + break; + case 5: + copyText(value, encryptEnabled(cfg) ? "On" : "Off"); + break; + case 6: + if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic) + { + char hex[33] = {}; + bytesToHex(cfg.meshtastic_config.secondary_key, 16, hex, sizeof(hex)); + copyText(value, hex[0] ? hex : "0000..."); + } + else + { + copyText(value, cfg.meshcore_config.meshcore_channel_name); + } + break; + } + + char line[48] = {}; + std::snprintf(line, sizeof(line), "%s: %s", kRadioItems[i], value); + drawTextClipped(0, 10 + static_cast(i * display_.lineHeight(FontSize::Small)), + display_.width(), line, FontSize::Small, i == radio_index_); + } + drawFooterHint("L/R adj OK edit"); +} + +void Runtime::renderDeviceSettings() +{ + drawTitleBar("device", nullptr); + char line[48] = {}; + for (size_t i = 0; i < arrayCount(kDeviceItems); ++i) + { + if (i == 0) + { + std::snprintf(line, sizeof(line), "BLE: %s", app()->isBleEnabled() ? "On" : "Off"); + } + else if (i == 1) + { + const int tz = host_.timezone_offset_min_fn ? host_.timezone_offset_min_fn() : 0; + std::snprintf(line, sizeof(line), "Time Zone: UTC%+d", tz / 60); + } + else if (i == 2) + { + std::snprintf(line, sizeof(line), "GPS: %s", app()->getConfig().gps_mode != 0 ? "On" : "Off"); + } + else if (i == 3) + { + std::snprintf(line, sizeof(line), "GPS Int: %lus", + static_cast(app()->getConfig().gps_interval_ms / 1000UL)); + } + else + { + std::snprintf(line, sizeof(line), "Chat Ch: %s", + app()->getConfig().chat_channel == 0 ? "Primary" : "Secondary"); + } + drawTextClipped(0, 10 + static_cast(i * display_.lineHeight(FontSize::Small)), + display_.width(), line, FontSize::Small, i == device_index_); + } + drawFooterHint("L/R toggle"); +} + +void Runtime::renderGnssPage() +{ + drawTitleBar("gnss", nullptr); + const auto state = host_.gps_data_fn ? host_.gps_data_fn() : platform::ui::gps::GpsState{}; + char line[40] = {}; + std::snprintf(line, sizeof(line), "Enabled: %s", (host_.gps_enabled_fn && host_.gps_enabled_fn()) ? "yes" : "no"); + display_.drawText(0, 12, line, FontSize::Small); + std::snprintf(line, sizeof(line), "Powered: %s", (host_.gps_powered_fn && host_.gps_powered_fn()) ? "yes" : "no"); + display_.drawText(0, 22, line, FontSize::Small); + std::snprintf(line, sizeof(line), "Fix: %s", state.valid ? "yes" : "no"); + display_.drawText(0, 32, line, FontSize::Small); + if (state.valid) + { + std::snprintf(line, sizeof(line), "Lat %.4f", state.lat); + display_.drawText(0, 42, line, FontSize::Small); + std::snprintf(line, sizeof(line), "Lng %.4f", state.lng); + display_.drawText(0, 52, line, FontSize::Small); + } +} + +void Runtime::renderActionPage() +{ + drawMenuList("actions", kActionItems, arrayCount(kActionItems), action_index_); +} + +void Runtime::enterPage(Page page) +{ + page_ = page; + page_entered_ms_ = nowMs(); + if (page == Page::ChatList) + { + rebuildConversationList(); + chat_list_index_ = std::min(chat_list_index_, conversation_count_ == 0 ? 0U : conversation_count_ - 1U); + } + else if (page == Page::Conversation) + { + if (app()) + { + app()->getChatService().markConversationRead(active_conversation_); + } + rebuildMessages(); + } +} + +void Runtime::openCompose(EditTarget target, const char* seed_text) +{ + edit_target_ = target; + page_before_compose_ = page_; + compose_buffer_[0] = '\0'; + compose_len_ = 0; + compose_charset_index_ = 0; + if (seed_text) + { + copyText(compose_buffer_, seed_text); + compose_len_ = std::strlen(compose_buffer_); + } + page_ = Page::Compose; + page_entered_ms_ = nowMs(); +} + +void Runtime::finishTextEdit(bool accept) +{ + if (accept) + { + saveEditedTextToConfig(); + } + edit_target_ = EditTarget::None; + enterPage(page_before_compose_); +} + +void Runtime::rebuildConversationList() +{ + conversation_count_ = 0; + conversation_total_ = 0; + if (!app()) + { + return; + } + + size_t total = 0; + const auto list = app()->getChatService().getConversations(0, kMaxConversationItems, &total); + conversation_total_ = total; + conversation_count_ = std::min(list.size(), static_cast(kMaxConversationItems)); + for (size_t i = 0; i < conversation_count_; ++i) + { + conversations_[i] = list[i]; + } +} + +void Runtime::rebuildMessages() +{ + message_count_ = 0; + if (!app()) + { + return; + } + + const auto list = app()->getChatService().getRecentMessages(active_conversation_, kMaxMessageItems); + message_count_ = std::min(list.size(), static_cast(kMaxMessageItems)); + for (size_t i = 0; i < message_count_; ++i) + { + messages_[i] = list[i]; + } +} + +void Runtime::sendComposeMessage() +{ + if (!app() || compose_len_ == 0) + { + finishTextEdit(false); + return; + } + + app()->getChatService().sendText(active_conversation_.channel, compose_buffer_, active_conversation_.peer); + finishTextEdit(false); + enterPage(Page::Conversation); +} + +void Runtime::commitConfig() +{ + if (!app()) + { + return; + } + app()->saveConfig(); +} + +void Runtime::ensureBootExit() +{ + if (page_ == Page::BootLog && (nowMs() - boot_started_ms_) >= kBootMinMs) + { + enterPage(Page::Screensaver); + } +} + +void Runtime::adjustRadioSetting(int delta) +{ + if (!app()) + { + return; + } + + auto& cfg = app()->getConfig(); + switch (radio_index_) + { + case 0: + app()->switchMeshProtocol(cfg.mesh_protocol == chat::MeshProtocol::Meshtastic + ? chat::MeshProtocol::MeshCore + : chat::MeshProtocol::Meshtastic, + false); + break; + case 1: + cfg.activeMeshConfig().tx_power = static_cast(clampValue( + static_cast(cfg.activeMeshConfig().tx_power) + delta, + static_cast(app::AppConfig::kTxPowerMinDbm), + static_cast(app::AppConfig::kTxPowerMaxDbm))); + break; + case 2: + 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(cfg.meshtastic_config.region)) + { + index = i; + break; + } + } + index = static_cast(clampValue(static_cast(index) + delta, 0, static_cast(count) - 1)); + cfg.meshtastic_config.region = static_cast(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(i); + break; + } + } + index = clampValue(index + delta, 0, static_cast(count) - 1); + cfg.meshcore_config.meshcore_region_preset = table[index].id; + } + } + break; + case 3: + if (cfg.mesh_protocol == chat::MeshProtocol::Meshtastic) + { + constexpr int kPresetMin = static_cast(meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST); + constexpr int kPresetMax = static_cast(meshtastic_Config_LoRaConfig_ModemPreset_LONG_TURBO); + cfg.meshtastic_config.modem_preset = static_cast(clampValue( + static_cast(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(i); + break; + } + } + index = clampValue(index + delta, 0, static_cast(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(clampValue( + static_cast(cfg.meshtastic_config.channel_num) + delta, 0, 255)); + } + else + { + cfg.meshcore_config.meshcore_channel_slot = static_cast(clampValue( + static_cast(cfg.meshcore_config.meshcore_channel_slot) + delta, 0, 15)); + } + break; + case 5: + setEncryptEnabled(cfg, !encryptEnabled(cfg)); + break; + default: + break; + } + + commitConfig(); +} + +void Runtime::adjustDeviceSetting(int delta) +{ + if (!app()) + { + return; + } + + if (device_index_ == 0) + { + app()->setBleEnabled(!app()->isBleEnabled()); + } + else if (device_index_ == 1 && host_.timezone_offset_min_fn && host_.set_timezone_offset_min_fn) + { + const int current = host_.timezone_offset_min_fn(); + const int next = clampValue(current + delta * kTimezoneStep, kTimezoneMin, kTimezoneMax); + host_.set_timezone_offset_min_fn(next); + } + else if (device_index_ == 2) + { + app()->getConfig().gps_mode = (app()->getConfig().gps_mode == 0) ? 1 : 0; + commitConfig(); + } + else if (device_index_ == 3) + { + static constexpr uint32_t kGpsIntervals[] = {15000UL, 30000UL, 60000UL, 300000UL, 600000UL}; + size_t index = 0; + while (index + 1 < arrayCount(kGpsIntervals) && + kGpsIntervals[index] < app()->getConfig().gps_interval_ms) + { + ++index; + } + const int next = clampValue(static_cast(index) + delta, 0, static_cast(arrayCount(kGpsIntervals)) - 1); + app()->getConfig().gps_interval_ms = kGpsIntervals[next]; + commitConfig(); + } + else if (device_index_ == 4) + { + app()->getConfig().chat_channel = app()->getConfig().chat_channel == 0 ? 1 : 0; + commitConfig(); + } +} + +void Runtime::adjustComposeSelection(int delta) +{ + const char* charset = editUsesHexCharset() ? kHexCharset : kComposeCharset; + const size_t len = std::strlen(charset); + if (len == 0) + { + compose_charset_index_ = 0; + return; + } + const int next = static_cast(compose_charset_index_) + delta; + if (next < 0) + { + compose_charset_index_ = len - 1; + } + else + { + compose_charset_index_ = static_cast(next) % len; + } +} + +void Runtime::addComposeChar() +{ + const char* charset = editUsesHexCharset() ? kHexCharset : kComposeCharset; + const size_t len = std::strlen(charset); + if (len == 0) + { + return; + } + appendChar(compose_buffer_, sizeof(compose_buffer_), compose_len_, charset[compose_charset_index_ % len]); +} + +void Runtime::removeComposeChar() +{ + popChar(compose_buffer_, compose_len_); +} + +void Runtime::saveEditedTextToConfig() +{ + if (!app()) + { + return; + } + + auto& cfg = app()->getConfig(); + switch (edit_target_) + { + case EditTarget::UserName: + copyText(cfg.node_name, compose_buffer_); + break; + case EditTarget::ShortName: + copyText(cfg.short_name, compose_buffer_); + break; + case EditTarget::MeshtasticPsk: + (void)hexToBytes(compose_buffer_, cfg.meshtastic_config.secondary_key, 16); + break; + case EditTarget::MeshCoreChannelName: + copyText(cfg.meshcore_config.meshcore_channel_name, compose_buffer_); + break; + default: + break; + } + commitConfig(); +} + +void Runtime::formatTime(char* out_time, size_t out_len, char* out_date, size_t date_len) const +{ + if (out_time && out_len > 0) + { + out_time[0] = '\0'; + } + if (out_date && date_len > 0) + { + out_date[0] = '\0'; + } + + if (!host_.utc_now_fn && !host_.millis_fn) + { + return; + } + + time_t now = host_.utc_now_fn ? host_.utc_now_fn() : 0; + const bool has_valid_wall_clock = now >= static_cast(1700000000); + + if (has_valid_wall_clock && host_.timezone_offset_min_fn) + { + now += static_cast(host_.timezone_offset_min_fn()) * 60; + } + + if (!has_valid_wall_clock) + { + const uint32_t uptime_s = host_.millis_fn ? (host_.millis_fn() / 1000U) : 0U; + const uint32_t hours = uptime_s / 3600U; + const uint32_t minutes = (uptime_s / 60U) % 60U; + const uint32_t seconds = uptime_s % 60U; + + if (out_time && out_len > 0) + { + std::snprintf(out_time, out_len, "%02lu:%02lu:%02lu", + static_cast(hours), + static_cast(minutes), + static_cast(seconds)); + } + if (out_date && date_len > 0) + { + std::snprintf(out_date, date_len, "TIME UNSYNC"); + } + return; + } + + const tm* local = gmtime(&now); + if (!local) + { + return; + } + + if (out_time && out_len > 0) + { + std::snprintf(out_time, out_len, "%02d:%02d:%02d", local->tm_hour, local->tm_min, local->tm_sec); + } + if (out_date && date_len > 0) + { + const char* weekday = (local->tm_wday >= 0 && local->tm_wday < 7) ? kWeekdays[local->tm_wday] : "---"; + std::snprintf(out_date, date_len, "%04d-%02d-%02d %s", + local->tm_year + 1900, local->tm_mon + 1, local->tm_mday, weekday); + } +} + +void Runtime::formatProtocol(char* out, size_t out_len) const +{ + if (!out || out_len == 0 || !app()) + { + return; + } + std::snprintf(out, out_len, "%s", protocolShortLabel(app()->getConfig().mesh_protocol)); +} + +void Runtime::formatNodeLabel(char* out, size_t out_len) const +{ + if (!out || out_len == 0 || !app()) + { + return; + } + chat::runtime::formatScreenNodeLabel(app()->getSelfNodeId(), out, out_len); +} + +void Runtime::drawTitleBar(const char* left, const char* right) +{ + if (left && left[0] != '\0') + { + display_.drawText(0, 0, left, FontSize::Small); + } + if (right && right[0] != '\0') + { + const int w = static_cast(std::strlen(right)) * display_.charWidth(FontSize::Small); + display_.drawText(std::max(0, display_.width() - w), 0, right, FontSize::Small); + } + display_.drawHLine(0, 8, display_.width()); +} + +void Runtime::drawMenuList(const char* title, const char* const* items, size_t count, size_t selected) +{ + drawTitleBar(title, nullptr); + const int line_h = display_.lineHeight(FontSize::Small); + for (size_t i = 0; i < count && i < 5; ++i) + { + drawTextClipped(0, 10 + static_cast(i * line_h), display_.width(), items[i], FontSize::Small, i == selected); + } +} + +void Runtime::drawFooterHint(const char* hint) +{ + if (!hint) + { + return; + } + drawTextClipped(0, 56, display_.width(), hint, FontSize::Small); +} + +void Runtime::drawTextClipped(int x, int y, int w, const char* text, FontSize size, bool inverse) +{ + if (!text || w <= 0) + { + return; + } + + const int cw = std::max(1, display_.charWidth(size)); + const size_t max_chars = static_cast(std::max(1, w / cw)); + char clipped[48] = {}; + if (std::strlen(text) <= max_chars) + { + copyText(clipped, text); + } + else if (max_chars > 3) + { + std::strncpy(clipped, text, max_chars - 3); + std::strcpy(clipped + (max_chars - 3), "..."); + } + else + { + std::strncpy(clipped, text, max_chars); + clipped[max_chars] = '\0'; + } + display_.drawText(x, y, clipped, size, inverse); +} + +bool Runtime::editUsesHexCharset() const +{ + return edit_target_ == EditTarget::MeshtasticPsk; +} + +uint32_t Runtime::nowMs() const +{ + return host_.millis_fn ? host_.millis_fn() : 0U; +} + +app::IAppFacade* Runtime::app() const +{ + return host_.app; +} + +} // namespace ui::mono_128x64 diff --git a/modules/ui_shared/include/ui/assets/fonts/font_utils.h b/modules/ui_shared/include/ui/assets/fonts/font_utils.h index e432c16a..232eef05 100644 --- a/modules/ui_shared/include/ui/assets/fonts/font_utils.h +++ b/modules/ui_shared/include/ui/assets/fonts/font_utils.h @@ -42,7 +42,12 @@ inline const lv_font_t* ui_chrome_font() inline const lv_font_t* chat_content_font(const char* text) { +#if defined(GAT562_NO_CJK) && GAT562_NO_CJK + (void)text; + return ui_chrome_font(); +#else return utf8_has_non_ascii(text) ? &lv_font_noto_cjk_16_2bpp : ui_chrome_font(); +#endif } inline void apply_font(lv_obj_t* label, const lv_font_t* font) diff --git a/modules/ui_shared/include/ui/assets/fonts/fonts.h b/modules/ui_shared/include/ui/assets/fonts/fonts.h index 609becca..ddc2cdf7 100644 --- a/modules/ui_shared/include/ui/assets/fonts/fonts.h +++ b/modules/ui_shared/include/ui/assets/fonts/fonts.h @@ -7,4 +7,8 @@ #include "lvgl.h" +#if defined(GAT562_NO_CJK) && GAT562_NO_CJK +#define lv_font_noto_cjk_16_2bpp lv_font_montserrat_14 +#else LV_FONT_DECLARE(lv_font_noto_cjk_16_2bpp); +#endif diff --git a/modules/ui_shared/include/ui/widgets/ime/pinyin_ime.h b/modules/ui_shared/include/ui/widgets/ime/pinyin_ime.h index ffa08bb9..d18767f9 100644 --- a/modules/ui_shared/include/ui/widgets/ime/pinyin_ime.h +++ b/modules/ui_shared/include/ui/widgets/ime/pinyin_ime.h @@ -16,6 +16,45 @@ namespace widgets class PinyinIme { public: +#if defined(GAT562_NO_PINYIN_IME) && GAT562_NO_PINYIN_IME + void setEnabled(bool enabled) { enabled_ = enabled; } + bool isEnabled() const { return false; } + + void reset() + { + buffer_.clear(); + candidates_.clear(); + candidate_index_ = 0; + } + bool hasBuffer() const { return false; } + const std::string& buffer() const { return buffer_; } + const std::vector& candidates() const { return candidates_; } + int candidateIndex() const { return 0; } + + bool appendLetter(char c) + { + (void)c; + return false; + } + bool backspace() { return false; } + bool moveCandidate(int delta) + { + (void)delta; + return false; + } + + bool commitCandidate(int index, std::string& out) + { + (void)index; + out.clear(); + return false; + } + bool commitActive(std::string& out) + { + out.clear(); + return false; + } +#else void setEnabled(bool enabled); bool isEnabled() const; @@ -31,10 +70,13 @@ class PinyinIme bool commitCandidate(int index, std::string& out); bool commitActive(std::string& out); +#endif private: +#if !(defined(GAT562_NO_PINYIN_IME) && GAT562_NO_PINYIN_IME) void updateCandidates(); void updateCandidatesFromBuiltin(); +#endif bool enabled_ = false; std::string buffer_; diff --git a/modules/ui_shared/src/ui/app_catalog_builder.cpp b/modules/ui_shared/src/ui/app_catalog_builder.cpp index 86605cb7..53aa0b70 100644 --- a/modules/ui_shared/src/ui/app_catalog_builder.cpp +++ b/modules/ui_shared/src/ui/app_catalog_builder.cpp @@ -9,12 +9,18 @@ #include "ui/screens/energy_sweep/energy_sweep_page_shell.h" #include "ui/screens/gnss/gnss_skyplot_page_shell.h" #include "ui/screens/gps/gps_page_shell.h" +#if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK #include "ui/screens/pc_link/pc_link_page_shell.h" +#endif #include "ui/screens/settings/settings_page_shell.h" #include "ui/screens/sstv/sstv_page_shell.h" +#if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM #include "ui/screens/team/team_page_shell.h" +#endif #include "ui/screens/tracker/tracker_page_shell.h" +#if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK #include "ui/screens/usb/usb_page_shell.h" +#endif #include "ui/screens/walkie_talkie/walkie_talkie_page_shell.h" namespace @@ -29,12 +35,18 @@ extern "C" extern const lv_image_dsc_t Satellite; extern const lv_image_dsc_t contact; extern const lv_image_dsc_t Spectrum; +#if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM extern const lv_image_dsc_t team_icon; +#endif extern const lv_image_dsc_t tracker_icon; +#if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK extern const lv_image_dsc_t rf; +#endif extern const lv_image_dsc_t sstv; extern const lv_image_dsc_t Setting; +#if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK extern const lv_image_dsc_t img_usb; +#endif extern const lv_image_dsc_t walkie_talkie; } @@ -72,26 +84,32 @@ ui::CallbackAppScreen s_energy_sweep_app("Energy Sweep", &Spectrum, energy_sweep::ui::shell::enter, energy_sweep::ui::shell::exit, &s_menu_host); +#if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM ui::CallbackAppScreen s_team_app("Team", &team_icon, team::ui::shell::enter, team::ui::shell::exit, &s_menu_host); +#endif ui::CallbackAppScreen s_tracker_app("Tracker", &tracker_icon, tracker::ui::shell::enter, tracker::ui::shell::exit, &s_menu_host); +#if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK ui::CallbackAppScreen s_pc_link_app("Data Exchange", &rf, pc_link::ui::shell::enter, pc_link::ui::shell::exit, &s_menu_host); +#endif ui::CallbackAppScreen s_sstv_app("SSTV", &sstv, sstv_page::ui::shell::enter, sstv_page::ui::shell::exit, &s_menu_host); +#if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK ui::CallbackAppScreen s_usb_app("USB Mass Storage", &img_usb, usb_storage::ui::shell::enter, usb_storage::ui::shell::exit, &s_menu_host); +#endif ui::CallbackAppScreen s_setting_app("Setting", &Setting, settings::ui::shell::enter, settings::ui::shell::exit, @@ -129,7 +147,9 @@ AppCatalog build(const FeatureFlags& flags) } if (flags.include_team) { +#if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM add(&s_team_app); +#endif } if (flags.profile == CatalogProfile::IdfDefault && flags.include_tracker) { @@ -137,7 +157,9 @@ AppCatalog build(const FeatureFlags& flags) } if (flags.include_pc_link) { +#if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK add(&s_pc_link_app); +#endif } if (flags.profile == CatalogProfile::PioDefault && flags.include_sstv) { @@ -153,7 +175,9 @@ AppCatalog build(const FeatureFlags& flags) } if (flags.include_usb) { +#if !defined(GAT562_NO_HOSTLINK) || !GAT562_NO_HOSTLINK add(&s_usb_app); +#endif } if (flags.profile == CatalogProfile::IdfDefault && flags.include_sstv) { diff --git a/modules/ui_shared/src/ui/menu/menu_runtime.cpp b/modules/ui_shared/src/ui/menu/menu_runtime.cpp index 5a3c9b7a..1de22014 100644 --- a/modules/ui_shared/src/ui/menu/menu_runtime.cpp +++ b/modules/ui_shared/src/ui/menu/menu_runtime.cpp @@ -6,6 +6,7 @@ #include "app/app_facade_access.h" #include "platform/ui/device_runtime.h" +#include "platform/ui/time_runtime.h" #include "ui/menu/menu_layout.h" #include "ui/menu/menu_profile.h" #include "ui/ui_common.h" @@ -92,24 +93,16 @@ void updateWatchFaceTime() return; } - const time_t now = time(nullptr); - if (now <= 0) - { - watchFaceSetTime(-1, -1, -1, -1, nullptr, battery); - return; - } - - const time_t local = ui_apply_timezone_offset(now); - struct tm* info = gmtime(&local); - if (!info) + struct tm info{}; + if (!::platform::ui::time::localtime_now(&info)) { watchFaceSetTime(-1, -1, -1, -1, nullptr, battery); return; } char weekday[8] = "---"; - strftime(weekday, sizeof(weekday), "%a", info); - watchFaceSetTime(info->tm_hour, info->tm_min, info->tm_mon + 1, info->tm_mday, weekday, battery); + strftime(weekday, sizeof(weekday), "%a", &info); + watchFaceSetTime(info.tm_hour, info.tm_min, info.tm_mon + 1, info.tm_mday, weekday, battery); } void hideWatchFaceInternal() diff --git a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp index 8bd2dc1c..1ee6dc2d 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp @@ -100,7 +100,7 @@ static void format_message_time(char* out, size_t out_len, uint32_t ts) return; } - uint32_t now_epoch = static_cast(time(nullptr)); + uint32_t now_epoch = sys::epoch_seconds_now(); bool ts_is_epoch = is_valid_epoch_ts(ts); bool now_is_epoch = is_valid_epoch_ts(now_epoch); uint32_t now_secs = now_is_epoch ? now_epoch : static_cast(sys::millis_now() / 1000U); diff --git a/modules/ui_shared/src/ui/screens/chat/chat_message_list_components.cpp b/modules/ui_shared/src/ui/screens/chat/chat_message_list_components.cpp index 325b3880..4507bd73 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_message_list_components.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_message_list_components.cpp @@ -59,7 +59,7 @@ static void format_time_hhmm(char out[16], uint32_t ts) } if (!is_valid_epoch_ts(ts)) { - uint32_t now_epoch = static_cast(time(nullptr)); + uint32_t now_epoch = sys::epoch_seconds_now(); uint32_t now_secs = is_valid_epoch_ts(now_epoch) ? now_epoch : static_cast(sys::millis_now() / 1000U); if (now_secs < ts) { diff --git a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp index 64da6abe..36ac0407 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_ui_controller.cpp @@ -1175,7 +1175,7 @@ bool UiController::sendTeamLocationWithIcon(uint8_t icon_id) return false; } - uint32_t ts = static_cast(time(nullptr)); + uint32_t ts = sys::epoch_seconds_now(); if (ts < kMinValidEpochSeconds) { ts = static_cast(sys::millis_now() / 1000U); @@ -1307,7 +1307,7 @@ void UiController::handleComposeAction(ChatComposeScreen::ActionIntent intent) return; } - uint32_t ts = static_cast(time(nullptr)); + uint32_t ts = sys::epoch_seconds_now(); if (ts < kMinValidEpochSeconds) { ts = static_cast(sys::millis_now() / 1000U); diff --git a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp index 8903da7f..2f00c42f 100644 --- a/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/contacts/contacts_page_components.cpp @@ -233,7 +233,7 @@ void refresh_contacts_data() static std::string format_time_status(uint32_t last_seen) { - uint32_t now_secs = time(nullptr); + uint32_t now_secs = sys::epoch_seconds_now(); if (now_secs < last_seen) { return "Offline"; @@ -588,7 +588,7 @@ static bool is_team_available() static uint32_t current_timestamp_seconds() { - uint32_t ts = static_cast(time(nullptr)); + uint32_t ts = sys::epoch_seconds_now(); if (ts < 1577836800U) { ts = sys::millis_now() / 1000U; @@ -1500,7 +1500,7 @@ static void on_compose_action(chat::ui::ChatComposeScreen::ActionIntent intent, if (g_contacts_state.chat_service) { s_last_sent_text = text; - s_last_sent_ts = static_cast(time(nullptr)); + s_last_sent_ts = sys::epoch_seconds_now(); chat::MessageId msg_id = g_contacts_state.chat_service->sendText( s_compose_channel, text, s_compose_peer_id); g_contacts_state.compose_screen->beginSend( @@ -1529,7 +1529,7 @@ static void on_compose_send_done(bool ok, bool /*timeout*/, void* /*user_data*/) uint32_t ts = s_last_sent_ts; if (ts == 0) { - ts = static_cast(time(nullptr)); + ts = sys::epoch_seconds_now(); } team::ui::team_ui_chatlog_append(team::ui::g_team_state.team_id, 0, diff --git a/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp b/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp index 31430b0b..cb6d6427 100644 --- a/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/node_info/node_info_page_components.cpp @@ -7,6 +7,7 @@ #include "app/app_config.h" #include "app/app_facade_access.h" #include "chat/infra/meshtastic/mt_region.h" +#include "sys/clock.h" #include "ui/screens/node_info/node_info_page_layout.h" #include "ui/ui_common.h" #include "ui/widgets/top_bar.h" @@ -210,7 +211,7 @@ void format_age(const char* prefix, uint32_t ts, char* out, size_t out_len) snprintf(out, out_len, "%s -", prefix); return; } - uint32_t now = time(nullptr); + uint32_t now = sys::epoch_seconds_now(); if (now < ts) { snprintf(out, out_len, "%s 0s", prefix); diff --git a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp index cf476251..9c871031 100644 --- a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp @@ -17,6 +17,7 @@ #include "platform/ui/gps_runtime.h" #include "platform/ui/screen_runtime.h" #include "platform/ui/settings_store.h" +#include "platform/ui/time_runtime.h" #include "platform/ui/tracker_runtime.h" #include "ui/page/page_profile.h" #include "ui/screens/settings/settings_page_components.h" @@ -563,7 +564,7 @@ static void settings_load() g_settings.privacy_nmea_sentence = cfg.privacy_nmea_sentence; g_settings.screen_timeout_ms = prefs_get_int("screen_timeout", static_cast(screen_runtime::timeout_ms())); - g_settings.timezone_offset_min = prefs_get_int("timezone_offset", 0); + g_settings.timezone_offset_min = ::platform::ui::time::timezone_offset_min(); g_settings.speaker_volume = prefs_get_int("speaker_volume", static_cast(get_message_tone_volume_default())); if (g_settings.speaker_volume < 0) @@ -1323,7 +1324,7 @@ static void on_option_clicked(lv_event_t* e) } if (payload->item->pref_key && strcmp(payload->item->pref_key, "timezone_offset") == 0) { - ui_set_timezone_offset_min(payload->value); + ::platform::ui::time::set_timezone_offset_min(payload->value); (void)previous_value; restart_now = true; } diff --git a/modules/ui_shared/src/ui/startup_shell.cpp b/modules/ui_shared/src/ui/startup_shell.cpp index c5cb2073..5a488a42 100644 --- a/modules/ui_shared/src/ui/startup_shell.cpp +++ b/modules/ui_shared/src/ui/startup_shell.cpp @@ -2,6 +2,7 @@ #include +#include "platform/ui/time_runtime.h" #include "platform/ui/screen_runtime.h" #include "ui/app_runtime.h" #include "ui/menu/menu_layout.h" @@ -22,20 +23,12 @@ bool format_menu_time(char* out, size_t out_len) return false; } - const time_t now = time(nullptr); - if (now <= 0) + struct tm info{}; + if (!::platform::ui::time::localtime_now(&info)) { return false; } - - const time_t local = ui_apply_timezone_offset(now); - struct tm* info = gmtime(&local); - if (!info) - { - return false; - } - - strftime(out, out_len, "%H:%M", info); + strftime(out, out_len, "%H:%M", &info); return true; } diff --git a/modules/ui_shared/src/ui/ui_status.cpp b/modules/ui_shared/src/ui/ui_status.cpp index 36e0896a..4cb2e850 100644 --- a/modules/ui_shared/src/ui/ui_status.cpp +++ b/modules/ui_shared/src/ui/ui_status.cpp @@ -11,7 +11,9 @@ #include "platform/ui/gps_runtime.h" #include "platform/ui/tracker_runtime.h" #include "sys/clock.h" +#if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM #include "ui/screens/team/team_ui_store.h" +#endif #include @@ -20,7 +22,9 @@ extern "C" extern const lv_image_dsc_t gps_topbar; extern const lv_image_dsc_t message_topbar; extern const lv_image_dsc_t route_topbar; +#if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM extern const lv_image_dsc_t team_topbar; +#endif extern const lv_image_dsc_t tracker_topbar; extern const lv_image_dsc_t ble_topbar; } @@ -70,6 +74,14 @@ bool obj_valid(lv_obj_t* obj) void refresh_team_cache(bool force = false) { +#if defined(GAT562_NO_TEAM) && GAT562_NO_TEAM + (void)force; + s_team_cache.team_active = false; + s_team_cache.team_unread = 0; + s_team_cache.valid = true; + s_team_cache.last_refresh_ms = sys::millis_now(); + return; +#else const uint32_t now = sys::millis_now(); if (!force && s_team_cache.valid && (now - s_team_cache.last_refresh_ms) < kTeamSnapshotRefreshMs) { @@ -89,6 +101,7 @@ void refresh_team_cache(bool force = false) } s_team_cache.valid = true; s_team_cache.last_refresh_ms = now; +#endif } StatusSnapshot collect_status() @@ -138,7 +151,11 @@ void apply_menu_icons(const StatusSnapshot& snap) apply_icon(s_menu_route_icon, &route_topbar, snap.route_active); apply_icon(s_menu_tracker_icon, &tracker_topbar, snap.track_recording); apply_icon(s_menu_gps_icon, &gps_topbar, snap.gps_enabled); +#if !defined(GAT562_NO_TEAM) || !GAT562_NO_TEAM apply_icon(s_menu_team_icon, &team_topbar, snap.team_active); +#else + apply_icon(s_menu_team_icon, nullptr, false); +#endif apply_icon(s_menu_msg_icon, &message_topbar, snap.unread > 0); apply_icon(s_menu_ble_icon, &ble_topbar, snap.ble_enabled); diff --git a/platform/esp/arduino_common/library.json b/platform/esp/arduino_common/library.json index 7aab6cdc..a2fe915f 100644 --- a/platform/esp/arduino_common/library.json +++ b/platform/esp/arduino_common/library.json @@ -22,9 +22,9 @@ "dependencies": [ { "name": "esp_boards" - }, - { - "name": "trail_mate_codec2" - } + }, + { + "name": "trail_mate_codec2" + } ] } diff --git a/platform/esp/arduino_common/src/app_config_store.cpp b/platform/esp/arduino_common/src/app_config_store.cpp index e6493536..d9f93904 100644 --- a/platform/esp/arduino_common/src/app_config_store.cpp +++ b/platform/esp/arduino_common/src/app_config_store.cpp @@ -107,6 +107,7 @@ bool loadAppConfigFromPreferences(AppConfig& config, Preferences& prefs) auto& map_track_enabled = config.map_track_enabled; auto& map_track_interval = config.map_track_interval; auto& map_track_format = config.map_track_format; + auto& ble_enabled = config.ble_enabled; auto& chat_channel = config.chat_channel; auto& net_duty_cycle = config.net_duty_cycle; auto& net_channel_util = config.net_channel_util; @@ -239,6 +240,7 @@ bool loadAppConfigFromPreferences(AppConfig& config, Preferences& prefs) map_track_enabled = prefs.getBool("map_track", map_track_enabled); map_track_interval = prefs.getUChar("map_track_interval", map_track_interval); map_track_format = prefs.getUChar("map_track_format", map_track_format); + ble_enabled = prefs.getBool("ble_enabled", ble_enabled); chat_channel = prefs.getUChar("chat_channel", chat_channel); net_duty_cycle = prefs.getBool("net_duty_cycle", net_duty_cycle); net_channel_util = prefs.getUChar("net_util", net_channel_util); @@ -330,6 +332,7 @@ bool saveAppConfigToPreferences(AppConfig& config, Preferences& prefs) auto& map_track_enabled = config.map_track_enabled; auto& map_track_interval = config.map_track_interval; auto& map_track_format = config.map_track_format; + auto& ble_enabled = config.ble_enabled; auto& chat_channel = config.chat_channel; auto& net_duty_cycle = config.net_duty_cycle; auto& net_channel_util = config.net_channel_util; @@ -422,6 +425,7 @@ bool saveAppConfigToPreferences(AppConfig& config, Preferences& prefs) prefs.putBool("map_track", map_track_enabled); prefs.putUChar("map_track_interval", map_track_interval); prefs.putUChar("map_track_format", map_track_format); + prefs.putBool("ble_enabled", ble_enabled); prefs.putUChar("chat_channel", chat_channel); prefs.putBool("net_duty_cycle", net_duty_cycle); prefs.putUChar("net_util", net_channel_util); diff --git a/platform/esp/arduino_common/src/app_runtime_support.cpp b/platform/esp/arduino_common/src/app_runtime_support.cpp index 1700b552..860d9ab3 100644 --- a/platform/esp/arduino_common/src/app_runtime_support.cpp +++ b/platform/esp/arduino_common/src/app_runtime_support.cpp @@ -1,5 +1,6 @@ #include "platform/esp/arduino_common/app_runtime_support.h" +#include "app/app_config.h" #include "app/app_facade_access.h" #include "app/app_facades.h" #include "ble/ble_manager.h" @@ -146,7 +147,7 @@ bool dispatchEvent(app::IAppFacade& app_context, sys::Event* event) std::unique_ptr createBleManager(app::IAppBleFacade& app_facade) { std::unique_ptr ble_manager(new ble::BleManager(app_facade)); - if (platform::esp::arduino_common::device_identity::loadBleEnabledPreference()) + if (app_facade.getConfig().ble_enabled) { ble_manager->setEnabled(true); } diff --git a/platform/esp/arduino_common/src/ble/ble_manager.cpp b/platform/esp/arduino_common/src/ble/ble_manager.cpp index d9f0662a..7e8508e5 100644 --- a/platform/esp/arduino_common/src/ble/ble_manager.cpp +++ b/platform/esp/arduino_common/src/ble/ble_manager.cpp @@ -5,8 +5,10 @@ #include "ble/meshcore_ble.h" #include "ble/meshtastic_ble.h" #include "chat/infra/mesh_protocol_utils.h" +#include "chat/runtime/self_identity_policy.h" #include #include +#include namespace ble { @@ -130,52 +132,20 @@ void BleManager::shutdownNimble() std::string BleManager::buildDeviceName(chat::MeshProtocol protocol) const { - const auto& cfg = ctx_.getConfig(); - std::string name; - if (cfg.node_name[0] != '\0') - { - name = std::string(cfg.node_name); - } - else - { - char buf[32]; - uint32_t suffix = static_cast(ctx_.getSelfNodeId() & 0xFFFF); - snprintf(buf, sizeof(buf), "lilygo-%04X", static_cast(suffix)); - name = std::string(buf); - } + char long_name[32] = {}; + char short_name[16] = {}; + ctx_.getEffectiveUserInfo(long_name, sizeof(long_name), short_name, sizeof(short_name)); - // BLE advertised name should not include TrailMate branding prefix. - // Keep the suffix part so the user can still identify the node. - static const std::string kTrailMatePrefix = "TrailMate-"; - if (name.rfind(kTrailMatePrefix, 0) == 0) - { - name.erase(0, kTrailMatePrefix.size()); - if (name.empty()) - { - char buf[16]; - uint32_t suffix = static_cast(ctx_.getSelfNodeId() & 0xFFFF); - snprintf(buf, sizeof(buf), "%04X", static_cast(suffix)); - name = std::string(buf); - } - } + chat::runtime::EffectiveSelfIdentity identity{}; + identity.node_id = ctx_.getSelfNodeId(); + std::strncpy(identity.long_name, long_name, sizeof(identity.long_name) - 1); + identity.long_name[sizeof(identity.long_name) - 1] = '\0'; + std::strncpy(identity.short_name, short_name, sizeof(identity.short_name) - 1); + identity.short_name[sizeof(identity.short_name) - 1] = '\0'; - if (protocol == chat::MeshProtocol::MeshCore) - { - static const std::string kMeshCorePrefix = "MeshCore-"; - if (name.rfind(kMeshCorePrefix, 0) != 0) - { - name = kMeshCorePrefix + name; - } - } - else if (protocol == chat::MeshProtocol::Meshtastic) - { - char meshtastic_name[32]; - snprintf(meshtastic_name, sizeof(meshtastic_name), "Meshtastic_%04X", - static_cast(ctx_.getSelfNodeId() & 0xFFFF)); - name = meshtastic_name; - } - - return name; + char visible_name[32] = {}; + chat::runtime::buildBleVisibleName(identity, protocol, visible_name, sizeof(visible_name)); + return visible_name; } } // namespace ble diff --git a/platform/esp/arduino_common/src/platform_ui_time_runtime.cpp b/platform/esp/arduino_common/src/platform_ui_time_runtime.cpp new file mode 100644 index 00000000..ca7eb813 --- /dev/null +++ b/platform/esp/arduino_common/src/platform_ui_time_runtime.cpp @@ -0,0 +1,39 @@ +#include "platform/ui/time_runtime.h" + +#include "ui/ui_common.h" + +namespace platform::ui::time +{ + +int timezone_offset_min() +{ + return ui_get_timezone_offset_min(); +} + +void set_timezone_offset_min(int offset_min) +{ + ui_set_timezone_offset_min(offset_min); +} + +::time_t apply_timezone_offset(::time_t utc_seconds) +{ + return ui_apply_timezone_offset(utc_seconds); +} + +bool localtime_now(struct tm* out_tm) +{ + if (!out_tm) + { + return false; + } + const ::time_t local = apply_timezone_offset(::time(nullptr)); + const ::tm* tmp = ::gmtime(&local); + if (!tmp) + { + return false; + } + *out_tm = *tmp; + return true; +} + +} // namespace platform::ui::time diff --git a/platform/esp/idf_common/src/platform_ui_time_runtime.cpp b/platform/esp/idf_common/src/platform_ui_time_runtime.cpp new file mode 100644 index 00000000..ba23941c --- /dev/null +++ b/platform/esp/idf_common/src/platform_ui_time_runtime.cpp @@ -0,0 +1,39 @@ +#include "platform/ui/time_runtime.h" + +#include "ui/ui_common.h" + +namespace platform::ui::time +{ + +int timezone_offset_min() +{ + return ui_get_timezone_offset_min(); +} + +void set_timezone_offset_min(int offset_min) +{ + ui_set_timezone_offset_min(offset_min); +} + +time_t apply_timezone_offset(time_t utc_seconds) +{ + return ui_apply_timezone_offset(utc_seconds); +} + +bool localtime_now(struct tm* out_tm) +{ + if (!out_tm) + { + return false; + } + const time_t local = apply_timezone_offset(time(nullptr)); + const tm* tmp = gmtime(&local); + if (!tmp) + { + return false; + } + *out_tm = *tmp; + return true; +} + +} // namespace platform::ui::time diff --git a/platform/nrf52/arduino_common/include/ble/ble_manager.h b/platform/nrf52/arduino_common/include/ble/ble_manager.h new file mode 100644 index 00000000..8b3b4303 --- /dev/null +++ b/platform/nrf52/arduino_common/include/ble/ble_manager.h @@ -0,0 +1,42 @@ +#pragma once + +#include "app/app_facades.h" +#include "chat/domain/chat_types.h" + +#include +#include + +namespace ble +{ + +class BleService +{ + public: + virtual ~BleService() = default; + virtual void start() = 0; + virtual void stop() = 0; + virtual void update() = 0; +}; + +class BleManager +{ + public: + explicit BleManager(app::IAppBleFacade& ctx); + ~BleManager(); + + void begin(); + void setEnabled(bool enabled); + bool isEnabled() const; + void update(); + void applyProtocol(chat::MeshProtocol protocol); + + private: + void restartService(chat::MeshProtocol protocol); + std::string buildDeviceName(chat::MeshProtocol protocol) const; + + app::IAppBleFacade& ctx_; + chat::MeshProtocol active_protocol_; + std::unique_ptr service_; +}; + +} // namespace ble diff --git a/platform/nrf52/arduino_common/include/ble/ble_uuids.h b/platform/nrf52/arduino_common/include/ble/ble_uuids.h new file mode 100644 index 00000000..4e6ad663 --- /dev/null +++ b/platform/nrf52/arduino_common/include/ble/ble_uuids.h @@ -0,0 +1,12 @@ +#pragma once + +#define MESH_SERVICE_UUID "6ba1b218-15a8-461f-9fa8-5dcae273eafd" +#define TORADIO_UUID "f75c76d2-129e-4dad-a1dd-7866124401e7" +#define FROMRADIO_UUID "2c55e69e-4993-11ed-b878-0242ac120002" +#define FROMNUM_UUID "ed9da18c-a800-4f66-a670-aa7547e34453" +#define LOGRADIO_UUID "5a3d6e49-06e6-4423-9944-e9de8cdf9547" +#define FROMRADIOSYNC_UUID "888a50c3-982d-45db-9963-c7923769165d" + +#define NUS_SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" +#define NUS_CHAR_RX_UUID "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" +#define NUS_CHAR_TX_UUID "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" diff --git a/platform/nrf52/arduino_common/include/ble/meshcore_ble.h b/platform/nrf52/arduino_common/include/ble/meshcore_ble.h new file mode 100644 index 00000000..fe0bbe7e --- /dev/null +++ b/platform/nrf52/arduino_common/include/ble/meshcore_ble.h @@ -0,0 +1,67 @@ +#pragma once + +#include "app/app_facades.h" +#include "ble_manager.h" +#include "chat/domain/chat_types.h" +#include "chat/ports/i_node_store.h" +#include "chat/usecase/chat_service.h" + +#include +#include +#include +#include +#include + +namespace ble +{ + +class MeshCoreBleService final : public BleService, + public chat::ChatService::IncomingTextObserver +{ + public: + MeshCoreBleService(app::IAppBleFacade& ctx, const std::string& device_name); + ~MeshCoreBleService() override; + + void start() override; + void stop() override; + void update() override; + void onIncomingText(const chat::MeshIncomingText& msg) override; + + bool handleRxFrame(const uint8_t* data, size_t len); + bool popTxFrame(uint8_t* out, size_t* out_len); + + private: + struct Frame + { + size_t len = 0; + std::array buf{}; + }; + + void pumpIncomingAppData(); + void handleCmdFrame(const uint8_t* data, size_t len); + void enqueueFrame(const uint8_t* data, size_t len); + void enqueueSentOk(); + void enqueueErr(uint8_t err); + void sendPendingNotifications(); + void enqueueRawDataPush(const chat::MeshIncomingData& msg); + uint32_t resolveNodeIdFromPrefix(const uint8_t* prefix, size_t len) const; + bool buildContactFromNode(const chat::contacts::NodeEntry& entry, uint8_t code, Frame& out) const; + + app::IAppBleFacade& ctx_; + std::string device_name_; + ::BLEService service_; + ::BLECharacteristic rx_char_; + ::BLECharacteristic tx_char_; + bool active_ = false; + std::deque tx_queue_; + std::vector sign_data_; + bool sign_active_ = false; + uint32_t stats_rx_packets_ = 0; + uint32_t stats_tx_packets_ = 0; + uint32_t stats_tx_flood_ = 0; + uint32_t stats_tx_direct_ = 0; + uint32_t stats_rx_flood_ = 0; + uint32_t stats_rx_direct_ = 0; +}; + +} // namespace ble diff --git a/platform/nrf52/arduino_common/include/ble/meshtastic_ble.h b/platform/nrf52/arduino_common/include/ble/meshtastic_ble.h new file mode 100644 index 00000000..4722187b --- /dev/null +++ b/platform/nrf52/arduino_common/include/ble/meshtastic_ble.h @@ -0,0 +1,86 @@ +#pragma once + +#include "app/app_facades.h" +#include "ble_manager.h" +#include "chat/domain/chat_types.h" +#include "chat/ports/i_node_store.h" +#include "chat/usecase/chat_service.h" +#include "meshtastic/admin.pb.h" +#include "meshtastic/channel.pb.h" +#include "meshtastic/config.pb.h" +#include "meshtastic/device_ui.pb.h" +#include "meshtastic/localonly.pb.h" +#include "meshtastic/mesh.pb.h" +#include "meshtastic/module_config.pb.h" +#include "meshtastic/telemetry.pb.h" + +#include +#include +#include + +namespace ble +{ + +class MeshtasticBleService final : public BleService, + public chat::ChatService::IncomingTextObserver +{ + public: + struct Frame + { + size_t len = 0; + uint32_t from_num = 0; + uint8_t buf[meshtastic_FromRadio_size] = {}; + }; + + MeshtasticBleService(app::IAppBleFacade& ctx, const std::string& device_name); + ~MeshtasticBleService() override; + + void start() override; + void stop() override; + void update() override; + void onIncomingText(const chat::MeshIncomingText& msg) override; + bool handleToRadio(const uint8_t* data, size_t len); + bool popToPhone(Frame* out); + + private: + bool handleToRadioPacket(meshtastic_MeshPacket& packet); + bool handleAdmin(meshtastic_MeshPacket& packet); + bool handleLocalSelfPacket(meshtastic_MeshPacket& packet); + void pumpIncomingAppData(); + bool encodeFromRadio(const meshtastic_FromRadio& from, uint32_t from_num, Frame* out) const; + void enqueueQueueStatus(uint32_t packet_id, bool ok); + void enqueueConfigSnapshot(uint32_t config_nonce); + void enqueueFromRadio(const meshtastic_FromRadio& from, uint32_t from_num); + void notifyFromNum(uint32_t from_num); + meshtastic_MyNodeInfo buildMyInfo() const; + meshtastic_NodeInfo buildSelfNodeInfo() const; + meshtastic_NodeInfo buildNodeInfoFromEntry(const chat::contacts::NodeEntry& entry) const; + meshtastic_DeviceMetadata buildMetadata() const; + meshtastic_DeviceMetrics buildDeviceMetrics() const; + meshtastic_LocalStats buildLocalStats() const; + meshtastic_DeviceUIConfig buildDeviceUi() const; + meshtastic_Channel buildChannel(uint8_t idx) const; + meshtastic_Config buildConfig(meshtastic_AdminMessage_ConfigType type) const; + meshtastic_ModuleConfig buildModuleConfig(meshtastic_AdminMessage_ModuleConfigType type) const; + meshtastic_MeshPacket buildPacketFromText(const chat::MeshIncomingText& msg) const; + meshtastic_MeshPacket buildPacketFromData(const chat::MeshIncomingData& msg) const; + + app::IAppBleFacade& ctx_; + std::string device_name_; + ::BLEService service_; + ::BLECharacteristic to_radio_; + ::BLECharacteristic from_radio_; + ::BLECharacteristic from_num_; + ::BLECharacteristic log_radio_; + bool active_ = false; + uint8_t last_to_radio_[meshtastic_ToRadio_size] = {}; + size_t last_to_radio_len_ = 0; + std::deque frame_queue_; + std::deque queue_status_queue_; + std::deque packet_queue_; + meshtastic_LocalModuleConfig module_config_ = meshtastic_LocalModuleConfig_init_zero; + char admin_canned_messages_[160] = {}; + char admin_ringtone_[96] = {}; +}; + +} // namespace ble diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/blob_file_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/blob_file_store.h new file mode 100644 index 00000000..507012cd --- /dev/null +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/blob_file_store.h @@ -0,0 +1,59 @@ +#pragma once + +#include "chat/ports/i_contact_blob_store.h" +#include "chat/ports/i_node_blob_store.h" + +#include +#include +#include + +namespace platform::nrf52::arduino_common::chat::infra +{ + +class BlobFileStore +{ + public: + explicit BlobFileStore(const char* path); + + bool loadBlob(std::vector& out); + bool saveBlob(const uint8_t* data, size_t len); + void clearBlob(); + + private: + bool ensureFs() const; + + const char* path_ = nullptr; +}; + +class NodeBlobFileStore final : public ::chat::contacts::INodeBlobStore +{ + public: + explicit NodeBlobFileStore(const char* path) + : store_(path) + { + } + + bool loadBlob(std::vector& out) override { return store_.loadBlob(out); } + bool saveBlob(const uint8_t* data, size_t len) override { return store_.saveBlob(data, len); } + void clearBlob() override { store_.clearBlob(); } + + private: + BlobFileStore store_; +}; + +class ContactBlobFileStore final : public ::chat::IContactBlobStore +{ + public: + explicit ContactBlobFileStore(const char* path) + : store_(path) + { + } + + bool loadBlob(std::vector& out) override { return store_.loadBlob(out); } + bool saveBlob(const uint8_t* data, size_t len) override { return store_.saveBlob(data, len); } + + private: + BlobFileStore store_; +}; + +} // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/contact_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/contact_store.h new file mode 100644 index 00000000..89059dd2 --- /dev/null +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/contact_store.h @@ -0,0 +1,27 @@ +#pragma once + +#include "chat/infra/contact_store_core.h" +#include "platform/nrf52/arduino_common/chat/infra/blob_file_store.h" + +namespace platform::nrf52::arduino_common::chat::infra +{ + +class ContactStore final : public ::chat::contacts::IContactStore +{ + public: + ContactStore(); + + void begin() override; + std::string getNickname(uint32_t node_id) const override; + bool setNickname(uint32_t node_id, const char* nickname) override; + bool removeNickname(uint32_t node_id) override; + bool hasNickname(const char* nickname) const override; + std::vector getAllContactIds() const override; + size_t getCount() const override; + + private: + ContactBlobFileStore blob_store_; + ::chat::contacts::ContactStoreCore core_; +}; + +} // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_adapter_lite.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_adapter_lite.h new file mode 100644 index 00000000..d94d5b7c --- /dev/null +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_adapter_lite.h @@ -0,0 +1,87 @@ +#pragma once + +#include "chat/infra/meshcore/meshcore_identity_crypto.h" +#include "chat/ports/i_mesh_adapter.h" +#include "chat/runtime/self_identity_policy.h" +#include "chat/runtime/self_identity_provider.h" + +#include +#include +#include + +namespace platform::nrf52::arduino_common::chat::meshcore +{ + +class MeshCoreAdapterLite final : public ::chat::IMeshAdapter +{ + public: + explicit MeshCoreAdapterLite(const ::chat::runtime::SelfIdentityProvider* identity_provider = nullptr); + + ::chat::MeshCapabilities getCapabilities() const override; + bool sendText(::chat::ChannelId channel, const std::string& text, + ::chat::MessageId* out_msg_id, ::chat::NodeId peer = 0) override; + bool pollIncomingText(::chat::MeshIncomingText* out) override; + bool sendAppData(::chat::ChannelId channel, uint32_t portnum, + const uint8_t* payload, size_t len, + ::chat::NodeId dest = 0, bool want_ack = false, + ::chat::MessageId packet_id = 0, + bool want_response = false) override; + bool pollIncomingData(::chat::MeshIncomingData* out) override; + bool requestNodeInfo(::chat::NodeId dest, bool want_response) override; + bool triggerDiscoveryAction(::chat::MeshDiscoveryAction action) override; + void applyConfig(const ::chat::MeshConfig& config) override; + void setUserInfo(const char* long_name, const char* short_name) override; + void setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) override; + void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) override; + bool isReady() const override; + ::chat::NodeId getNodeId() const override; + bool pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len) override; + void handleRawPacket(const uint8_t* data, size_t size) override; + void setLastRxStats(float rssi, float snr) override; + void processSendQueue() override; + bool exportIdentityPublicKey(uint8_t* out_key, size_t out_len); + bool exportIdentityPrivateKey(uint8_t* out_key, size_t out_len); + bool importIdentityPrivateKey(const uint8_t* key, size_t len); + bool signPayload(const uint8_t* payload, size_t len, uint8_t* out_signature, size_t out_len); + bool sendSelfAdvert(bool broadcast); + bool sendPeerRequestType(const uint8_t* pubkey, size_t len, uint8_t req_type, + uint32_t* out_tag, uint32_t* out_est_timeout, + bool* out_sent_flood); + bool sendPeerRequestPayload(const uint8_t* pubkey, size_t len, + const uint8_t* payload, size_t payload_len, + bool force_flood, + uint32_t* out_tag, uint32_t* out_est_timeout, + bool* out_sent_flood); + bool sendAnonRequestPayload(const uint8_t* pubkey, size_t len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout, + bool* out_sent_flood); + bool sendTracePath(const uint8_t* path, size_t path_len, + uint32_t tag, uint32_t auth, uint8_t flags, + uint32_t* out_est_timeout); + bool sendControlData(const uint8_t* payload, size_t payload_len); + bool sendRawData(const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout); + void setFloodScopeKey(const uint8_t* key, size_t len); + + private: + ::chat::runtime::EffectiveSelfIdentity buildEffectiveIdentity() const; + void ensureIdentityKeys(); + bool transmitFrame(const uint8_t* data, size_t size); + bool sendAdvert(bool broadcast); + + ::chat::MeshConfig config_{}; + ::chat::NodeId node_id_ = 0; + std::string long_name_; + std::string short_name_; + const ::chat::runtime::SelfIdentityProvider* identity_provider_ = nullptr; + bool keys_ready_ = false; + uint8_t public_key_[::chat::meshcore::kMeshCorePubKeySize] = {}; + uint8_t private_key_[::chat::meshcore::kMeshCorePrivKeySize] = {}; + std::array flood_scope_key_{}; + std::queue<::chat::MeshIncomingText> text_queue_; + std::queue<::chat::MeshIncomingData> data_queue_; +}; + +} // namespace platform::nrf52::arduino_common::chat::meshcore diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/mt_adapter_lite.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/mt_adapter_lite.h new file mode 100644 index 00000000..c2cebe80 --- /dev/null +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/mt_adapter_lite.h @@ -0,0 +1,57 @@ +#pragma once + +#include "chat/ports/i_mesh_adapter.h" +#include "chat/runtime/self_identity_policy.h" +#include "chat/runtime/self_identity_provider.h" + +#include +#include + +namespace platform::nrf52::arduino_common::chat::meshtastic +{ + +class MtAdapterLite final : public ::chat::IMeshAdapter +{ + public: + explicit MtAdapterLite(const ::chat::runtime::SelfIdentityProvider* identity_provider = nullptr); + + ::chat::MeshCapabilities getCapabilities() const override; + bool sendText(::chat::ChannelId channel, const std::string& text, + ::chat::MessageId* out_msg_id, ::chat::NodeId peer = 0) override; + bool pollIncomingText(::chat::MeshIncomingText* out) override; + bool sendAppData(::chat::ChannelId channel, uint32_t portnum, + const uint8_t* payload, size_t len, + ::chat::NodeId dest = 0, bool want_ack = false, + ::chat::MessageId packet_id = 0, + bool want_response = false) override; + bool pollIncomingData(::chat::MeshIncomingData* out) override; + bool requestNodeInfo(::chat::NodeId dest, bool want_response) override; + void applyConfig(const ::chat::MeshConfig& config) override; + void setUserInfo(const char* long_name, const char* short_name) override; + void setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) override; + void setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) override; + bool isReady() const override; + ::chat::NodeId getNodeId() const override; + bool pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len) override; + void handleRawPacket(const uint8_t* data, size_t size) override; + void setLastRxStats(float rssi, float snr) override; + void processSendQueue() override; + + private: + ::chat::runtime::EffectiveSelfIdentity buildEffectiveIdentity() const; + bool transmitWire(const uint8_t* data, size_t size); + bool buildAndQueueNodeInfo(::chat::NodeId dest, bool want_response); + + ::chat::MeshConfig config_{}; + ::chat::NodeId node_id_ = 0; + ::chat::MessageId next_packet_id_ = 1; + std::string long_name_; + std::string short_name_; + const ::chat::runtime::SelfIdentityProvider* identity_provider_ = nullptr; + float last_rx_rssi_ = 0.0f; + float last_rx_snr_ = 0.0f; + std::queue<::chat::MeshIncomingText> text_queue_; + std::queue<::chat::MeshIncomingData> data_queue_; +}; + +} // namespace platform::nrf52::arduino_common::chat::meshtastic diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h new file mode 100644 index 00000000..6d56989d --- /dev/null +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h @@ -0,0 +1,29 @@ +#pragma once + +#include "chat/infra/node_store_core.h" +#include "platform/nrf52/arduino_common/chat/infra/blob_file_store.h" + +namespace platform::nrf52::arduino_common::chat::meshtastic +{ + +class NodeStore final : public ::chat::contacts::INodeStore +{ + public: + NodeStore(); + + void begin() override; + void upsert(uint32_t node_id, const char* short_name, const char* long_name, + uint32_t now_secs, float snr = 0.0f, float rssi = 0.0f, uint8_t protocol = 0, + uint8_t role = ::chat::contacts::kNodeRoleUnknown, uint8_t hops_away = 0xFF, + uint8_t hw_model = 0, uint8_t channel = 0xFF) override; + void updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) override; + bool remove(uint32_t node_id) override; + const std::vector<::chat::contacts::NodeEntry>& getEntries() const override; + void clear() override; + + private: + ::platform::nrf52::arduino_common::chat::infra::NodeBlobFileStore blob_store_; + ::chat::contacts::NodeStoreCore core_; +}; + +} // namespace platform::nrf52::arduino_common::chat::meshtastic diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/radio_packet_io.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/radio_packet_io.h new file mode 100644 index 00000000..f9547259 --- /dev/null +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/radio_packet_io.h @@ -0,0 +1,31 @@ +#pragma once + +#include "chat/domain/chat_types.h" + +#include +#include + +namespace platform::nrf52::arduino_common::chat::infra +{ + +struct RadioPacket +{ + uint8_t data[256] = {}; + size_t size = 0; + ::chat::RxMeta rx_meta{}; +}; + +class IRadioPacketIo +{ + public: + virtual ~IRadioPacketIo() = default; + virtual bool begin() = 0; + virtual void applyConfig(::chat::MeshProtocol protocol, const ::chat::MeshConfig& config) = 0; + virtual bool transmit(const uint8_t* data, size_t size) = 0; + virtual bool pollReceive(RadioPacket* out_packet) = 0; +}; + +void bindRadioPacketIo(IRadioPacketIo* io); +IRadioPacketIo* radioPacketIo(); + +} // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/device_identity.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/device_identity.h new file mode 100644 index 00000000..9105047a --- /dev/null +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/device_identity.h @@ -0,0 +1,16 @@ +#pragma once + +#include "chat/domain/chat_types.h" + +#include +#include + +namespace platform::nrf52::arduino_common::device_identity +{ + +::chat::NodeId deriveNodeIdFromDeviceAddress(uint32_t deviceaddr0, uint32_t deviceaddr1); +std::array deriveMacAddressFromDeviceAddress(uint32_t deviceaddr0, uint32_t deviceaddr1); +::chat::NodeId getSelfNodeId(); +std::array getSelfMacAddress(); + +} // namespace platform::nrf52::arduino_common::device_identity diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/self_identity_bridge.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/self_identity_bridge.h new file mode 100644 index 00000000..e599facb --- /dev/null +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/self_identity_bridge.h @@ -0,0 +1,35 @@ +#pragma once + +#include "app/app_config.h" +#include "chat/runtime/self_identity_provider.h" + +#include + +namespace platform::nrf52::arduino_common +{ + +class SelfIdentityBridge final : public ::chat::runtime::SelfIdentityProvider +{ + public: + SelfIdentityBridge(const ::app::AppConfig& config, + uint32_t deviceaddr0, + uint32_t deviceaddr1, + const char* fallback_long_prefix, + const char* fallback_ble_prefix); + + bool readSelfIdentityInput(::chat::runtime::SelfIdentityInput* out) const override; + + const std::array& macAddress() const + { + return mac_addr_; + } + + private: + const ::app::AppConfig& config_; + ::chat::NodeId node_id_ = 0; + std::array mac_addr_{}; + const char* fallback_long_prefix_ = nullptr; + const char* fallback_ble_prefix_ = nullptr; +}; + +} // namespace platform::nrf52::arduino_common diff --git a/platform/nrf52/arduino_common/library.json b/platform/nrf52/arduino_common/library.json new file mode 100644 index 00000000..e4dc54c9 --- /dev/null +++ b/platform/nrf52/arduino_common/library.json @@ -0,0 +1,35 @@ +{ + "name": "platform_nrf52_arduino_common", + "version": "0.1.0", + "build": { + "includeDir": "include", + "srcDir": "src", + "srcFilter": [ + "+<*>" + ], + "flags": [ + "-std=gnu++17", + "-Iinclude", + "-I../../..", + "-I../../../boards/gat562_mesh_evb_pro/include", + "-I../../../platform/esp/boards/include", + "-I../../../modules/core_sys/include", + "-I../../../modules/core_chat/include", + "-I../../../modules/core_chat/generated", + "-I../../../modules/core_gps/include", + "-I../../../modules/ui_mono_128x64/include", + "-I../../../modules/ui_shared/include", + "-I../../../variants/gat562_mesh_evb_pro", + "-I../../../.pio/libdeps/gat562_mesh_evb_pro/RadioLib/src", + "-I../../../.pio/libdeps/gat562_mesh_evb_pro/TinyGPSPlus/src", + "-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/SPI", + "-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/Wire", + "-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/Bluefruit52Lib/src", + "-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/Bluefruit52Lib/src/services", + "-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/Adafruit_TinyUSB_Arduino/src", + "-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/Adafruit_nRFCrypto/src", + "-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/Adafruit_LittleFS/src", + "-IC:/Users/VicLi/.platformio/packages/framework-arduinoadafruitnrf52/libraries/InternalFileSytem/src" + ] + } +} diff --git a/platform/nrf52/arduino_common/src/ble/ble_manager.cpp b/platform/nrf52/arduino_common/src/ble/ble_manager.cpp new file mode 100644 index 00000000..29499e79 --- /dev/null +++ b/platform/nrf52/arduino_common/src/ble/ble_manager.cpp @@ -0,0 +1,138 @@ +#include "../../include/ble/ble_manager.h" + +#include "app/app_config.h" +#include "../../include/ble/meshcore_ble.h" +#include "../../include/ble/meshtastic_ble.h" +#include "chat/infra/mesh_protocol_utils.h" +#include "chat/runtime/self_identity_policy.h" + +#include +#include +#include + +namespace ble +{ + +BleManager::BleManager(app::IAppBleFacade& ctx) + : ctx_(ctx), + active_protocol_(ctx.getConfig().mesh_protocol) +{ +} + +BleManager::~BleManager() +{ + if (service_) + { + service_->stop(); + service_.reset(); + } +} + +void BleManager::begin() +{ + setEnabled(true); +} + +void BleManager::setEnabled(bool enabled) +{ + if (enabled) + { + if (!service_) + { + restartService(ctx_.getConfig().mesh_protocol); + } + } + else + { + if (service_) + { + service_->stop(); + service_.reset(); + } + } +} + +bool BleManager::isEnabled() const +{ + return ctx_.isBleEnabled(); +} + +void BleManager::update() +{ + const auto current_protocol = ctx_.getConfig().mesh_protocol; + if (current_protocol != active_protocol_) + { + applyProtocol(current_protocol); + } + + if (service_) + { + service_->update(); + } +} + +void BleManager::applyProtocol(chat::MeshProtocol protocol) +{ + if (protocol != active_protocol_) + { + restartService(protocol); + } +} + +void BleManager::restartService(chat::MeshProtocol protocol) +{ + if (service_) + { + service_->stop(); + service_.reset(); + } + + active_protocol_ = protocol; + const std::string device_name = buildDeviceName(protocol); + const char* protocol_name = chat::infra::meshProtocolSlug(active_protocol_); + + switch (active_protocol_) + { + case chat::MeshProtocol::MeshCore: + service_ = std::unique_ptr(new MeshCoreBleService(ctx_, device_name)); + break; + case chat::MeshProtocol::Meshtastic: + default: + service_ = std::unique_ptr(new MeshtasticBleService(ctx_, device_name)); + break; + } + + if (service_) + { + service_->start(); + Serial2.printf("[BLE][nrf52] protocol=%s name=%s service=started\n", + protocol_name, + device_name.c_str()); + } + else + { + Serial2.printf("[BLE][nrf52] protocol=%s name=%s service=create_failed\n", + protocol_name, + device_name.c_str()); + } +} + +std::string BleManager::buildDeviceName(chat::MeshProtocol protocol) const +{ + char long_name[32] = {}; + char short_name[16] = {}; + ctx_.getEffectiveUserInfo(long_name, sizeof(long_name), short_name, sizeof(short_name)); + + chat::runtime::EffectiveSelfIdentity identity{}; + identity.node_id = ctx_.getSelfNodeId(); + std::strncpy(identity.long_name, long_name, sizeof(identity.long_name) - 1); + identity.long_name[sizeof(identity.long_name) - 1] = '\0'; + std::strncpy(identity.short_name, short_name, sizeof(identity.short_name) - 1); + identity.short_name[sizeof(identity.short_name) - 1] = '\0'; + + char visible_name[32] = {}; + chat::runtime::buildBleVisibleName(identity, protocol, visible_name, sizeof(visible_name)); + return visible_name; +} + +} // namespace ble diff --git a/platform/nrf52/arduino_common/src/ble/meshcore_ble.cpp b/platform/nrf52/arduino_common/src/ble/meshcore_ble.cpp new file mode 100644 index 00000000..ad750eb8 --- /dev/null +++ b/platform/nrf52/arduino_common/src/ble/meshcore_ble.cpp @@ -0,0 +1,1504 @@ +#include "../../include/ble/meshcore_ble.h" + +#include "app/app_config.h" +#include "boards/gat562_mesh_evb_pro/gat562_board.h" +#include "ble/ble_uuids.h" +#include "chat/ports/i_node_store.h" +#include "platform/ui/device_runtime.h" +#include "platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_adapter_lite.h" +#include "sys/clock.h" + +#include +#include +#include +#include +#include + +namespace ble +{ +namespace +{ + +constexpr uint8_t CMD_APP_START = 1; +constexpr uint8_t CMD_SEND_TXT_MSG = 2; +constexpr uint8_t CMD_SEND_CHANNEL_TXT_MSG = 3; +constexpr uint8_t CMD_GET_CONTACTS = 4; +constexpr uint8_t CMD_GET_DEVICE_TIME = 5; +constexpr uint8_t CMD_SET_DEVICE_TIME = 6; +constexpr uint8_t CMD_SEND_SELF_ADVERT = 7; +constexpr uint8_t CMD_SET_ADVERT_NAME = 8; +constexpr uint8_t CMD_SET_RADIO_PARAMS = 11; +constexpr uint8_t CMD_SET_RADIO_TX_POWER = 12; +constexpr uint8_t CMD_RESET_PATH = 13; +constexpr uint8_t CMD_GET_BATT_AND_STORAGE = 20; +constexpr uint8_t CMD_SEND_RAW_DATA = 25; +constexpr uint8_t CMD_SEND_LOGIN = 26; +constexpr uint8_t CMD_SEND_STATUS_REQ = 27; +constexpr uint8_t CMD_HAS_CONNECTION = 28; +constexpr uint8_t CMD_LOGOUT = 29; +constexpr uint8_t CMD_GET_CONTACT_BY_KEY = 30; +constexpr uint8_t CMD_GET_CHANNEL = 31; +constexpr uint8_t CMD_SET_CHANNEL = 32; +constexpr uint8_t CMD_SIGN_START = 33; +constexpr uint8_t CMD_SIGN_DATA = 34; +constexpr uint8_t CMD_SIGN_FINISH = 35; +constexpr uint8_t CMD_SEND_TRACE_PATH = 36; +constexpr uint8_t CMD_SEND_BINARY_REQ = 50; +constexpr uint8_t CMD_EXPORT_PRIVATE_KEY = 23; +constexpr uint8_t CMD_IMPORT_PRIVATE_KEY = 24; +constexpr uint8_t CMD_REBOOT = 19; +constexpr uint8_t CMD_FACTORY_RESET = 51; +constexpr uint8_t CMD_SEND_PATH_DISCOVERY_REQ = 52; +constexpr uint8_t CMD_SET_FLOOD_SCOPE = 54; +constexpr uint8_t CMD_SEND_CONTROL_DATA = 55; +constexpr uint8_t CMD_GET_STATS = 56; +constexpr uint8_t CMD_SEND_TELEMETRY_REQ = 39; +constexpr uint8_t CMD_GET_ADVERT_PATH = 42; +constexpr uint8_t CMD_DEVICE_QEURY = 22; + +constexpr uint8_t RESP_CODE_OK = 0; +constexpr uint8_t RESP_CODE_ERR = 1; +constexpr uint8_t RESP_CODE_CONTACTS_START = 2; +constexpr uint8_t RESP_CODE_CONTACT = 3; +constexpr uint8_t RESP_CODE_END_OF_CONTACTS = 4; +constexpr uint8_t RESP_CODE_SELF_INFO = 5; +constexpr uint8_t RESP_CODE_SENT = 6; +constexpr uint8_t RESP_CODE_CONTACT_MSG_RECV = 7; +constexpr uint8_t RESP_CODE_CHANNEL_MSG_RECV = 8; +constexpr uint8_t RESP_CODE_CURR_TIME = 9; +constexpr uint8_t RESP_CODE_DEVICE_INFO = 13; +constexpr uint8_t RESP_CODE_PRIVATE_KEY = 14; +constexpr uint8_t RESP_CODE_CHANNEL_INFO = 18; +constexpr uint8_t RESP_CODE_SIGN_START = 19; +constexpr uint8_t RESP_CODE_SIGNATURE = 20; +constexpr uint8_t RESP_CODE_ADVERT_PATH = 22; +constexpr uint8_t RESP_CODE_STATS = 24; +constexpr uint8_t RESP_CODE_BATT_AND_STORAGE = 25; + +constexpr uint8_t PUSH_CODE_RAW_DATA = 0x84; +constexpr uint8_t PUSH_CODE_TELEMETRY_RESPONSE = 0x8B; + +constexpr uint8_t ERR_CODE_UNSUPPORTED_CMD = 1; +constexpr uint8_t ERR_CODE_NOT_FOUND = 2; +constexpr uint8_t ERR_CODE_TABLE_FULL = 3; +constexpr uint8_t ERR_CODE_BAD_STATE = 4; +constexpr uint8_t ERR_CODE_ILLEGAL_ARG = 6; + +constexpr uint8_t ADV_TYPE_CHAT = 1; +constexpr uint8_t TXT_TYPE_PLAIN = 0; +constexpr uint8_t TXT_TYPE_CLI_DATA = 1; +constexpr uint8_t kCompatFirmwareVerCode = 8; +constexpr uint8_t kCompatMaxContactsDiv2 = 50; +constexpr uint8_t kCompatMaxGroupChannels = 1; +constexpr const char* kCompatFirmwareVersion = "v1.11.0"; +constexpr size_t kPrefixSize = 6; +constexpr size_t kMaxFrameSize = 172; +constexpr size_t kPubKeySize = chat::meshcore::kMeshCorePubKeySize; +constexpr size_t kMaxPathSize = 64; +constexpr uint8_t STATS_TYPE_CORE = 0; +constexpr uint8_t STATS_TYPE_RADIO = 1; +constexpr uint8_t STATS_TYPE_PACKETS = 2; + +void copyBounded(char* dst, size_t dst_len, const char* src) +{ + if (!dst || dst_len == 0) + { + return; + } + if (!src) + { + dst[0] = '\0'; + return; + } + std::strncpy(dst, src, dst_len - 1); + dst[dst_len - 1] = '\0'; +} + +MeshCoreBleService* s_active_service = nullptr; + +uint32_t nowSeconds() +{ + return sys::epoch_seconds_now(); +} + +void prepareBluefruit(const std::string& device_name) +{ + Bluefruit.autoConnLed(false); + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.begin(); + Bluefruit.setName(device_name.c_str()); +} + +void startAdvertising(::BLEService& service) +{ + Bluefruit.Advertising.stop(); + Bluefruit.Advertising.clearData(); + Bluefruit.ScanResponse.clearData(); + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addService(service); + Bluefruit.ScanResponse.addName(); + Bluefruit.ScanResponse.addTxPower(); + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 668); + Bluefruit.Advertising.setFastTimeout(30); + Bluefruit.Advertising.start(0); +} + +void authorizeRead(uint16_t conn_handle) +{ + ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_READ}; + reply.params.read.gatt_status = BLE_GATT_STATUS_SUCCESS; + sd_ble_gatts_rw_authorize_reply(conn_handle, &reply); +} + +void onRxWrite(uint16_t, BLECharacteristic*, uint8_t* data, uint16_t len) +{ + if (!s_active_service || !data || len == 0) + { + return; + } + (void)s_active_service->handleRxFrame(data, len); +} + +void onTxAuthorize(uint16_t conn_handle, BLECharacteristic* chr, ble_gatts_evt_read_t* request) +{ + if (!chr || !request) + { + authorizeRead(conn_handle); + return; + } + + if (request->offset == 0) + { + uint8_t out[kMaxFrameSize] = {}; + size_t out_len = 0; + if (s_active_service && s_active_service->popTxFrame(out, &out_len)) + { + chr->write(out, out_len); + } + else + { + uint8_t empty = 0; + chr->write(&empty, 0); + } + } + + authorizeRead(conn_handle); +} + +platform::nrf52::arduino_common::chat::meshcore::MeshCoreAdapterLite* resolveMeshCoreAdapter( + app::IAppBleFacade& ctx) +{ + auto* adapter = ctx.getMeshAdapter(); + if (!adapter || ctx.getConfig().mesh_protocol != chat::MeshProtocol::MeshCore) + { + return nullptr; + } + + if (auto* backend = adapter->backendForProtocol(chat::MeshProtocol::MeshCore)) + { + return static_cast(backend); + } + + return static_cast(adapter); +} + +} // namespace + +MeshCoreBleService::MeshCoreBleService(app::IAppBleFacade& ctx, const std::string& device_name) + : ctx_(ctx), + device_name_(device_name), + service_(::BLEUuid(NUS_SERVICE_UUID)), + rx_char_(::BLEUuid(NUS_CHAR_RX_UUID)), + tx_char_(::BLEUuid(NUS_CHAR_TX_UUID)) +{ +} + +MeshCoreBleService::~MeshCoreBleService() +{ + stop(); +} + +void MeshCoreBleService::start() +{ + s_active_service = this; + prepareBluefruit(device_name_); + + service_.begin(); + + rx_char_.setProperties(CHR_PROPS_WRITE); + rx_char_.setPermission(SECMODE_OPEN, SECMODE_OPEN); + rx_char_.setFixedLen(0); + rx_char_.setMaxLen(kMaxFrameSize); + rx_char_.setWriteCallback(onRxWrite, false); + rx_char_.begin(); + + tx_char_.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); + tx_char_.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); + tx_char_.setFixedLen(0); + tx_char_.setMaxLen(kMaxFrameSize); + tx_char_.setReadAuthorizeCallback(onTxAuthorize, false); + tx_char_.begin(); + + ctx_.getChatService().addIncomingTextObserver(this); + startAdvertising(service_); + active_ = true; +} + +void MeshCoreBleService::stop() +{ + ctx_.getChatService().removeIncomingTextObserver(this); + Bluefruit.Advertising.stop(); + tx_queue_.clear(); + active_ = false; + if (s_active_service == this) + { + s_active_service = nullptr; + } +} + +void MeshCoreBleService::update() +{ + if (!active_) + { + return; + } + + pumpIncomingAppData(); + sendPendingNotifications(); + + if (!Bluefruit.connected() && !Bluefruit.Advertising.isRunning()) + { + Bluefruit.Advertising.start(0); + } +} + +void MeshCoreBleService::onIncomingText(const chat::MeshIncomingText& msg) +{ + ++stats_rx_packets_; + if (msg.rx_meta.direct) + { + ++stats_rx_direct_; + } + else + { + ++stats_rx_flood_; + } + + Frame frame{}; + size_t index = 0; + if (msg.to == 0xFFFFFFFFUL || msg.to == 0) + { + frame.buf[index++] = RESP_CODE_CHANNEL_MSG_RECV; + frame.buf[index++] = static_cast(msg.channel); + frame.buf[index++] = msg.rx_meta.direct ? 0xFFU : msg.rx_meta.hop_count; + } + else + { + frame.buf[index++] = RESP_CODE_CONTACT_MSG_RECV; + std::memcpy(&frame.buf[index], &msg.from, std::min(sizeof(msg.from), kPrefixSize)); + index += kPrefixSize; + frame.buf[index++] = msg.rx_meta.direct ? 0xFFU : msg.rx_meta.hop_count; + } + + frame.buf[index++] = TXT_TYPE_PLAIN; + std::memcpy(&frame.buf[index], &msg.timestamp, sizeof(msg.timestamp)); + index += sizeof(msg.timestamp); + + const size_t text_len = std::min(msg.text.size(), frame.buf.size() - index); + if (text_len > 0) + { + std::memcpy(&frame.buf[index], msg.text.data(), text_len); + index += text_len; + } + frame.len = index; + tx_queue_.push_back(frame); +} + +bool MeshCoreBleService::handleRxFrame(const uint8_t* data, size_t len) +{ + if (!data || len == 0 || len > kMaxFrameSize) + { + return false; + } + handleCmdFrame(data, len); + return true; +} + +bool MeshCoreBleService::popTxFrame(uint8_t* out, size_t* out_len) +{ + if (!out || !out_len || tx_queue_.empty()) + { + return false; + } + const Frame frame = tx_queue_.front(); + tx_queue_.pop_front(); + std::memcpy(out, frame.buf.data(), frame.len); + *out_len = frame.len; + return true; +} + +void MeshCoreBleService::pumpIncomingAppData() +{ + chat::IMeshAdapter* adapter = ctx_.getMeshAdapter(); + if (!adapter) + { + return; + } + + for (uint8_t count = 0; count < 4; ++count) + { + chat::MeshIncomingData msg{}; + if (!adapter->pollIncomingData(&msg)) + { + break; + } + enqueueRawDataPush(msg); + } +} + +void MeshCoreBleService::handleCmdFrame(const uint8_t* data, size_t len) +{ + const uint8_t cmd = data[0]; + chat::IMeshAdapter* adapter = ctx_.getMeshAdapter(); + auto& cfg = ctx_.getConfig(); + auto* lite = resolveMeshCoreAdapter(ctx_); + + if (cmd == CMD_DEVICE_QEURY && len >= 2) + { + uint8_t out[kMaxFrameSize] = {}; + size_t index = 0; + out[index++] = RESP_CODE_DEVICE_INFO; + out[index++] = kCompatFirmwareVerCode; + out[index++] = kCompatMaxContactsDiv2; + out[index++] = kCompatMaxGroupChannels; + const uint32_t ble_pin = 0; + std::memcpy(&out[index], &ble_pin, sizeof(ble_pin)); + index += sizeof(ble_pin); + std::memset(&out[index], 0, 12); + std::strncpy(reinterpret_cast(&out[index]), __DATE__, 11); + index += 12; + std::strncpy(reinterpret_cast(&out[index]), device_name_.c_str(), 40); + index += 40; + std::strncpy(reinterpret_cast(&out[index]), kCompatFirmwareVersion, 20); + index += 20; + enqueueFrame(out, index); + return; + } + + if (cmd == CMD_APP_START && len >= 8) + { + uint8_t out[kMaxFrameSize] = {}; + size_t index = 0; + out[index++] = RESP_CODE_SELF_INFO; + out[index++] = ADV_TYPE_CHAT; + out[index++] = static_cast(cfg.meshcore_config.tx_power); + out[index++] = static_cast(app::AppConfig::kTxPowerMaxDbm); + + uint8_t pubkey[chat::meshcore::kMeshCorePubKeySize] = {}; + if (lite) + { + lite->exportIdentityPublicKey(pubkey, sizeof(pubkey)); + } + std::memcpy(&out[index], pubkey, sizeof(pubkey)); + index += sizeof(pubkey); + + const int32_t lat = 0; + const int32_t lon = 0; + std::memcpy(&out[index], &lat, sizeof(lat)); + index += sizeof(lat); + std::memcpy(&out[index], &lon, sizeof(lon)); + index += sizeof(lon); + + out[index++] = cfg.meshcore_config.meshcore_multi_acks ? 1U : 0U; + out[index++] = 0; + out[index++] = 0; + + const uint32_t freq = static_cast(cfg.meshcore_config.meshcore_freq_mhz * 1000.0f); + const uint32_t bw = static_cast(cfg.meshcore_config.meshcore_bw_khz * 1000.0f); + std::memcpy(&out[index], &freq, sizeof(freq)); + index += sizeof(freq); + std::memcpy(&out[index], &bw, sizeof(bw)); + index += sizeof(bw); + out[index++] = cfg.meshcore_config.meshcore_sf; + out[index++] = cfg.meshcore_config.meshcore_cr; + + const size_t name_len = strnlen(cfg.node_name, sizeof(cfg.node_name)); + const size_t copy_len = std::min(name_len, static_cast(kMaxFrameSize - index)); + if (copy_len > 0) + { + std::memcpy(&out[index], cfg.node_name, copy_len); + index += copy_len; + } + enqueueFrame(out, index); + return; + } + + if (cmd == CMD_GET_CONTACTS) + { + uint32_t filter_since = 0; + if (len >= 5) + { + std::memcpy(&filter_since, &data[1], sizeof(filter_since)); + } + + uint32_t total = 0; + if (const auto* store = ctx_.getNodeStore()) + { + for (const auto& entry : store->getEntries()) + { + if (entry.node_id == 0) + { + continue; + } + if (filter_since != 0 && entry.last_seen <= filter_since) + { + continue; + } + ++total; + } + } + + uint8_t start_buf[5] = {RESP_CODE_CONTACTS_START, 0, 0, 0, 0}; + std::memcpy(&start_buf[1], &total, sizeof(total)); + enqueueFrame(start_buf, sizeof(start_buf)); + + uint32_t most_recent = 0; + if (const auto* store = ctx_.getNodeStore()) + { + for (const auto& entry : store->getEntries()) + { + if (entry.node_id == 0) + { + continue; + } + if (filter_since != 0 && entry.last_seen <= filter_since) + { + continue; + } + Frame frame{}; + if (buildContactFromNode(entry, RESP_CODE_CONTACT, frame)) + { + enqueueFrame(frame.buf.data(), frame.len); + most_recent = std::max(most_recent, entry.last_seen); + } + } + } + + uint8_t end_buf[5] = {RESP_CODE_END_OF_CONTACTS, 0, 0, 0, 0}; + std::memcpy(&end_buf[1], &most_recent, sizeof(most_recent)); + enqueueFrame(end_buf, sizeof(end_buf)); + return; + } + + if (cmd == CMD_SEND_TXT_MSG && len >= 14) + { + if (!adapter) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + size_t index = 1; + const uint8_t txt_type = data[index++]; + index++; + index += 4; + const uint8_t* prefix = &data[index]; + index += kPrefixSize; + if (txt_type != TXT_TYPE_PLAIN && txt_type != TXT_TYPE_CLI_DATA) + { + enqueueErr(ERR_CODE_UNSUPPORTED_CMD); + return; + } + + const uint32_t dest = resolveNodeIdFromPrefix(prefix, kPrefixSize); + if (dest == 0) + { + enqueueErr(ERR_CODE_NOT_FOUND); + return; + } + + const std::string text(reinterpret_cast(&data[index]), len - index); + chat::MessageId msg_id = 0; + if (!adapter->sendText(chat::ChannelId::PRIMARY, text, &msg_id, dest)) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + + ++stats_tx_packets_; + ++stats_tx_direct_; + uint8_t out[10] = {}; + out[0] = RESP_CODE_SENT; + out[1] = 0; + const uint32_t ack = msg_id; + const uint32_t timeout = 0; + std::memcpy(&out[2], &ack, sizeof(ack)); + std::memcpy(&out[6], &timeout, sizeof(timeout)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_SEND_CHANNEL_TXT_MSG && len >= 7) + { + if (!adapter) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + size_t index = 1; + const uint8_t txt_type = data[index++]; + const uint8_t channel_idx = data[index++]; + index += 4; + if (txt_type != TXT_TYPE_PLAIN) + { + enqueueErr(ERR_CODE_UNSUPPORTED_CMD); + return; + } + const std::string text(reinterpret_cast(&data[index]), len - index); + chat::MessageId msg_id = 0; + const chat::ChannelId channel = (channel_idx == 1U) ? chat::ChannelId::SECONDARY : chat::ChannelId::PRIMARY; + if (!adapter->sendText(channel, text, &msg_id, 0)) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + ++stats_tx_packets_; + ++stats_tx_flood_; + enqueueSentOk(); + return; + } + + if (cmd == CMD_SEND_LOGIN && len >= 1 + kPubKeySize) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + const uint8_t* pubkey = &data[1]; + const size_t pass_len = (len > (1 + kPubKeySize)) ? (len - (1 + kPubKeySize)) : 0; + uint8_t payload[24] = {}; + size_t payload_len = 0; + const uint32_t now_ts = nowSeconds(); + std::memcpy(&payload[payload_len], &now_ts, sizeof(now_ts)); + payload_len += sizeof(now_ts); + if (pass_len > 0) + { + const size_t copy_len = std::min(pass_len, sizeof(payload) - payload_len); + std::memcpy(&payload[payload_len], &data[1 + kPubKeySize], copy_len); + payload_len += copy_len; + } + + uint32_t est_timeout = 0; + bool sent_flood = false; + if (!lite->sendAnonRequestPayload(pubkey, + kPubKeySize, + payload, + payload_len, + &est_timeout, + &sent_flood)) + { + enqueueErr(ERR_CODE_TABLE_FULL); + return; + } + + ++stats_tx_packets_; + ++stats_tx_flood_; + uint8_t out[10] = {}; + out[0] = RESP_CODE_SENT; + out[1] = sent_flood ? 1U : 0U; + std::memcpy(&out[2], pubkey, sizeof(uint32_t)); + std::memcpy(&out[6], &est_timeout, sizeof(est_timeout)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_SEND_STATUS_REQ && len >= 1 + kPubKeySize) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + const uint8_t* pubkey = &data[1]; + uint32_t tag = 0; + uint32_t est_timeout = 0; + bool sent_flood = false; + if (!lite->sendPeerRequestType(pubkey, + kPubKeySize, + 0x01, + &tag, + &est_timeout, + &sent_flood)) + { + enqueueErr(ERR_CODE_TABLE_FULL); + return; + } + + ++stats_tx_packets_; + ++stats_tx_flood_; + uint8_t out[10] = {}; + out[0] = RESP_CODE_SENT; + out[1] = sent_flood ? 1U : 0U; + std::memcpy(&out[2], &tag, sizeof(tag)); + std::memcpy(&out[6], &est_timeout, sizeof(est_timeout)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_SET_ADVERT_NAME && len >= 2) + { + const size_t nlen = std::min(len - 1, sizeof(cfg.node_name) - 1); + std::memcpy(cfg.node_name, &data[1], nlen); + cfg.node_name[nlen] = '\0'; + ctx_.saveConfig(); + ctx_.applyUserInfo(); + enqueueSentOk(); + return; + } + + if (cmd == CMD_SEND_SELF_ADVERT) + { + const bool broadcast = (len >= 2 && data[1] == 1); + if (lite && lite->sendSelfAdvert(broadcast)) + { + ++stats_tx_packets_; + if (broadcast) + { + ++stats_tx_flood_; + } + else + { + ++stats_tx_direct_; + } + enqueueSentOk(); + } + else + { + enqueueErr(ERR_CODE_BAD_STATE); + } + return; + } + + if (cmd == CMD_GET_DEVICE_TIME) + { + uint8_t out[5] = {}; + out[0] = RESP_CODE_CURR_TIME; + const uint32_t now = nowSeconds(); + std::memcpy(&out[1], &now, sizeof(now)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_SET_DEVICE_TIME && len >= 5) + { + uint32_t epoch = 0; + std::memcpy(&epoch, &data[1], sizeof(epoch)); + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setCurrentEpochSeconds(epoch); + enqueueSentOk(); + return; + } + + if (cmd == CMD_GET_BATT_AND_STORAGE) + { + uint8_t out[11] = {}; + out[0] = RESP_CODE_BATT_AND_STORAGE; + const auto battery = platform::ui::device::battery_info(); + const uint8_t level = (battery.level >= 0) ? static_cast(battery.level) : 0; + const uint16_t mv = static_cast(3000U + static_cast(level) * 12U); + const uint32_t used = 0; + const uint32_t total = 0; + std::memcpy(&out[1], &mv, sizeof(mv)); + std::memcpy(&out[3], &used, sizeof(used)); + std::memcpy(&out[7], &total, sizeof(total)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_SET_RADIO_PARAMS && len >= 11) + { + uint32_t freq = 0; + uint32_t bw = 0; + std::memcpy(&freq, &data[1], sizeof(freq)); + std::memcpy(&bw, &data[5], sizeof(bw)); + const uint8_t sf = data[9]; + const uint8_t cr = data[10]; + if (freq < 300000U || freq > 2500000U || bw < 7000U || bw > 500000U || sf < 5U || sf > 12U || cr < 5U || + cr > 8U) + { + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + cfg.meshcore_config.meshcore_freq_mhz = static_cast(freq) / 1000.0f; + cfg.meshcore_config.meshcore_bw_khz = static_cast(bw) / 1000.0f; + cfg.meshcore_config.meshcore_sf = sf; + cfg.meshcore_config.meshcore_cr = cr; + ctx_.saveConfig(); + ctx_.applyMeshConfig(); + enqueueSentOk(); + return; + } + + if (cmd == CMD_SET_RADIO_TX_POWER && len >= 2) + { + const int8_t tx = static_cast(data[1]); + if (tx < app::AppConfig::kTxPowerMinDbm || tx > app::AppConfig::kTxPowerMaxDbm) + { + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + cfg.meshcore_config.tx_power = tx; + ctx_.saveConfig(); + ctx_.applyMeshConfig(); + enqueueSentOk(); + return; + } + + if (cmd == CMD_GET_CHANNEL && len >= 2) + { + const uint8_t channel_idx = data[1]; + if (channel_idx > 1U) + { + enqueueErr(ERR_CODE_NOT_FOUND); + return; + } + uint8_t out[kMaxFrameSize] = {}; + size_t index = 0; + out[index++] = RESP_CODE_CHANNEL_INFO; + out[index++] = channel_idx; + if (channel_idx == 0U) + { + copyBounded(reinterpret_cast(&out[index]), 32, cfg.meshcore_config.meshcore_channel_name); + index += 32; + std::memcpy(&out[index], cfg.meshcore_config.primary_key, 16); + index += 16; + } + else + { + copyBounded(reinterpret_cast(&out[index]), 32, "Secondary"); + index += 32; + std::memcpy(&out[index], cfg.meshcore_config.secondary_key, 16); + index += 16; + } + enqueueFrame(out, index); + return; + } + + if (cmd == CMD_EXPORT_PRIVATE_KEY) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + uint8_t priv[chat::meshcore::kMeshCorePrivKeySize] = {}; + if (!lite->exportIdentityPrivateKey(priv, sizeof(priv))) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + uint8_t out[1 + chat::meshcore::kMeshCorePrivKeySize] = {}; + out[0] = RESP_CODE_PRIVATE_KEY; + std::memcpy(&out[1], priv, sizeof(priv)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_IMPORT_PRIVATE_KEY && len >= 1 + chat::meshcore::kMeshCorePrivKeySize) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + if (!lite->importIdentityPrivateKey(&data[1], chat::meshcore::kMeshCorePrivKeySize)) + { + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + enqueueSentOk(); + return; + } + + if (cmd == CMD_SEND_PATH_DISCOVERY_REQ) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + if (len < 2 + kPubKeySize || data[1] != 0) + { + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + const uint8_t* pubkey = &data[2]; + uint8_t req_data[9] = {}; + req_data[0] = 0x03; + req_data[1] = static_cast(~0x01U); + const uint32_t nonce = static_cast(millis()); + std::memcpy(&req_data[5], &nonce, sizeof(nonce)); + + uint32_t tag = 0; + uint32_t est_timeout = 0; + bool sent_flood = false; + if (!lite->sendPeerRequestPayload(pubkey, + kPubKeySize, + req_data, + sizeof(req_data), + true, + &tag, + &est_timeout, + &sent_flood)) + { + enqueueErr(ERR_CODE_TABLE_FULL); + return; + } + + ++stats_tx_packets_; + ++stats_tx_flood_; + uint8_t out[10] = {}; + out[0] = RESP_CODE_SENT; + out[1] = sent_flood ? 1U : 0U; + std::memcpy(&out[2], &tag, sizeof(tag)); + std::memcpy(&out[6], &est_timeout, sizeof(est_timeout)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_SEND_RAW_DATA && len >= 6) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + size_t index = 1; + const int8_t path_len = static_cast(data[index++]); + if (path_len < 0 || (index + static_cast(path_len) + 4U) > len) + { + enqueueErr(ERR_CODE_UNSUPPORTED_CMD); + return; + } + const uint8_t* path = &data[index]; + index += static_cast(path_len); + const uint8_t* payload = &data[index]; + const size_t payload_len = len - index; + uint32_t est_timeout = 0; + if (!lite->sendRawData(path, static_cast(path_len), payload, payload_len, &est_timeout)) + { + enqueueErr(ERR_CODE_TABLE_FULL); + return; + } + ++stats_tx_packets_; + ++stats_tx_direct_; + enqueueSentOk(); + return; + } + + if (cmd == CMD_SEND_BINARY_REQ && len >= 1 + kPubKeySize + 1) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + const uint8_t* pubkey = &data[1]; + const uint8_t* payload = &data[1 + kPubKeySize]; + const size_t payload_len = len - (1 + kPubKeySize); + uint32_t tag = 0; + uint32_t est_timeout = 0; + bool sent_flood = false; + if (!lite->sendPeerRequestPayload(pubkey, + kPubKeySize, + payload, + payload_len, + false, + &tag, + &est_timeout, + &sent_flood)) + { + enqueueErr(ERR_CODE_TABLE_FULL); + return; + } + + ++stats_tx_packets_; + ++stats_tx_flood_; + uint8_t out[10] = {}; + out[0] = RESP_CODE_SENT; + out[1] = sent_flood ? 1U : 0U; + std::memcpy(&out[2], &tag, sizeof(tag)); + std::memcpy(&out[6], &est_timeout, sizeof(est_timeout)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_SEND_TELEMETRY_REQ) + { + if (len == 4) + { + uint8_t out[kMaxFrameSize] = {}; + size_t index = 0; + out[index++] = PUSH_CODE_TELEMETRY_RESPONSE; + out[index++] = 0; + uint8_t prefix[kPrefixSize] = {}; + if (lite) + { + uint8_t pubkey[kPubKeySize] = {}; + if (lite->exportIdentityPublicKey(pubkey, sizeof(pubkey))) + { + std::memcpy(prefix, pubkey, kPrefixSize); + } + } + if (prefix[0] == 0) + { + const uint32_t self_id = ctx_.getSelfNodeId(); + std::memcpy(prefix, &self_id, std::min(sizeof(self_id), sizeof(prefix))); + } + std::memcpy(&out[index], prefix, sizeof(prefix)); + index += sizeof(prefix); + const auto battery = platform::ui::device::battery_info(); + const uint8_t level = (battery.level >= 0) ? static_cast(battery.level) : 0; + const uint16_t mv = static_cast(3000U + static_cast(level) * 12U); + const uint16_t lpp_val = static_cast(mv / 10U); + out[index++] = 1; + out[index++] = 116; + std::memcpy(&out[index], &lpp_val, sizeof(lpp_val)); + index += sizeof(lpp_val); + enqueueFrame(out, index); + return; + } + if (len >= 4 + kPubKeySize) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + const uint8_t* pubkey = &data[4]; + uint32_t tag = 0; + uint32_t est_timeout = 0; + bool sent_flood = false; + if (!lite->sendPeerRequestType(pubkey, kPubKeySize, 0x03, &tag, &est_timeout, &sent_flood)) + { + enqueueErr(ERR_CODE_TABLE_FULL); + return; + } + ++stats_tx_packets_; + ++stats_tx_flood_; + uint8_t out[10] = {}; + out[0] = RESP_CODE_SENT; + out[1] = sent_flood ? 1U : 0U; + std::memcpy(&out[2], &tag, sizeof(tag)); + std::memcpy(&out[6], &est_timeout, sizeof(est_timeout)); + enqueueFrame(out, sizeof(out)); + return; + } + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + + if (cmd == CMD_SEND_TRACE_PATH && len > 10) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + uint32_t tag = 0; + uint32_t auth = 0; + std::memcpy(&tag, &data[1], sizeof(tag)); + std::memcpy(&auth, &data[5], sizeof(auth)); + const uint8_t flags = data[9]; + const size_t path_len = len - 10; + const uint8_t path_sz_bits = flags & 0x03U; + const size_t path_stride = static_cast(1U << path_sz_bits); + if (path_stride == 0 || (path_len >> path_sz_bits) > kMaxPathSize || (path_len % path_stride) != 0) + { + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + uint32_t est_timeout = 0; + if (!lite->sendTracePath(&data[10], path_len, tag, auth, flags, &est_timeout)) + { + enqueueErr(ERR_CODE_TABLE_FULL); + return; + } + ++stats_tx_packets_; + ++stats_tx_direct_; + uint8_t out[10] = {}; + out[0] = RESP_CODE_SENT; + out[1] = 0; + std::memcpy(&out[2], &tag, sizeof(tag)); + std::memcpy(&out[6], &est_timeout, sizeof(est_timeout)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_GET_ADVERT_PATH && len >= 2 + kPubKeySize) + { + const uint8_t* pubkey = &data[2]; + uint32_t ts = 0; + uint8_t path_len = 0; + uint8_t path[kMaxPathSize] = {}; + bool found = false; + if (const auto* store = ctx_.getNodeStore()) + { + for (const auto& entry : store->getEntries()) + { + if (entry.node_id == 0) + { + continue; + } + uint32_t entry_id = 0; + std::memcpy(&entry_id, pubkey, std::min(sizeof(entry_id), kPubKeySize)); + if (entry.node_id == entry_id) + { + ts = entry.last_seen; + found = true; + break; + } + } + } + if (!found) + { + enqueueErr(ERR_CODE_NOT_FOUND); + return; + } + uint8_t out[kMaxFrameSize] = {}; + size_t index = 0; + out[index++] = RESP_CODE_ADVERT_PATH; + std::memcpy(&out[index], &ts, sizeof(ts)); + index += sizeof(ts); + out[index++] = path_len; + if (path_len > 0) + { + std::memcpy(&out[index], path, path_len); + index += path_len; + } + enqueueFrame(out, index); + return; + } + + if (cmd == CMD_REBOOT && len >= 7) + { + if (std::memcmp(&data[1], "reboot", 6) != 0) + { + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + enqueueSentOk(); + delay(100); + NVIC_SystemReset(); + return; + } + + if (cmd == CMD_GET_CONTACT_BY_KEY && len >= 2) + { + const uint8_t* key = &data[1]; + const size_t key_len = len - 1; + if (const auto* store = ctx_.getNodeStore()) + { + for (const auto& entry : store->getEntries()) + { + if (entry.node_id == 0) + { + continue; + } + if (std::memcmp(&entry.node_id, key, std::min(key_len, sizeof(entry.node_id))) == 0) + { + Frame frame{}; + if (buildContactFromNode(entry, RESP_CODE_CONTACT, frame)) + { + enqueueFrame(frame.buf.data(), frame.len); + return; + } + } + } + } + enqueueErr(ERR_CODE_NOT_FOUND); + return; + } + + if (cmd == CMD_RESET_PATH && len >= 1 + kPubKeySize) + { + if (lite) + { + lite->setFloodScopeKey(nullptr, 0); + } + enqueueSentOk(); + return; + } + + if (cmd == CMD_SIGN_START) + { + sign_active_ = true; + sign_data_.clear(); + sign_data_.reserve(8 * 1024); + uint8_t out[6] = {}; + out[0] = RESP_CODE_SIGN_START; + out[1] = 0; + const uint32_t max_len = 8 * 1024; + std::memcpy(&out[2], &max_len, sizeof(max_len)); + enqueueFrame(out, sizeof(out)); + return; + } + + if (cmd == CMD_SIGN_DATA && len > 1) + { + if (!sign_active_) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + if (sign_data_.size() + (len - 1) > (8 * 1024)) + { + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + sign_data_.insert(sign_data_.end(), &data[1], &data[len]); + enqueueSentOk(); + return; + } + + if (cmd == CMD_SIGN_FINISH) + { + if (!sign_active_ || !lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + sign_active_ = false; + uint8_t sig[chat::meshcore::kMeshCoreSignatureSize] = {}; + if (!lite->signPayload(sign_data_.data(), sign_data_.size(), sig, sizeof(sig))) + { + enqueueErr(ERR_CODE_BAD_STATE); + sign_data_.clear(); + return; + } + uint8_t out[1 + chat::meshcore::kMeshCoreSignatureSize] = {}; + out[0] = RESP_CODE_SIGNATURE; + std::memcpy(&out[1], sig, sizeof(sig)); + enqueueFrame(out, sizeof(out)); + sign_data_.clear(); + return; + } + + if (cmd == CMD_SET_CHANNEL && len >= 2 + 32 + 16) + { + const uint8_t channel_idx = data[1]; + if (channel_idx > 1U) + { + enqueueErr(ERR_CODE_NOT_FOUND); + return; + } + if (channel_idx == 0U) + { + copyBounded(cfg.meshcore_config.meshcore_channel_name, + sizeof(cfg.meshcore_config.meshcore_channel_name), + reinterpret_cast(&data[2])); + std::memcpy(cfg.meshcore_config.primary_key, &data[34], 16); + } + else + { + std::memcpy(cfg.meshcore_config.secondary_key, &data[34], 16); + } + ctx_.saveConfig(); + ctx_.applyMeshConfig(); + enqueueSentOk(); + return; + } + + if (cmd == CMD_SEND_CONTROL_DATA && len >= 2 && (data[1] & 0x80U) != 0) + { + if (!lite) + { + enqueueErr(ERR_CODE_BAD_STATE); + return; + } + if (!lite->sendControlData(&data[1], len - 1)) + { + enqueueErr(ERR_CODE_TABLE_FULL); + return; + } + ++stats_tx_packets_; + ++stats_tx_direct_; + enqueueSentOk(); + return; + } + + if (cmd == CMD_SET_FLOOD_SCOPE && len >= 2 && data[1] == 0) + { + if (lite) + { + if (len >= 18) + { + lite->setFloodScopeKey(&data[2], 16); + } + else + { + lite->setFloodScopeKey(nullptr, 0); + } + } + enqueueSentOk(); + return; + } + + if (cmd == CMD_HAS_CONNECTION && len >= 1 + kPubKeySize) + { + const uint32_t node_id = resolveNodeIdFromPrefix(&data[1], sizeof(uint32_t)); + if (node_id != 0) + { + enqueueSentOk(); + } + else + { + enqueueErr(ERR_CODE_NOT_FOUND); + } + return; + } + + if (cmd == CMD_LOGOUT && len >= 1 + kPubKeySize) + { + enqueueSentOk(); + return; + } + + if (cmd == CMD_GET_STATS && len >= 2) + { + const uint8_t stats_type = data[1]; + if (stats_type == STATS_TYPE_CORE) + { + uint8_t out[16] = {}; + size_t index = 0; + out[index++] = RESP_CODE_STATS; + out[index++] = STATS_TYPE_CORE; + const uint16_t mv = 0; + const uint32_t uptime = millis() / 1000U; + const uint16_t err_flags = 0; + const uint8_t queue_len = static_cast(tx_queue_.size()); + std::memcpy(&out[index], &mv, sizeof(mv)); + index += sizeof(mv); + std::memcpy(&out[index], &uptime, sizeof(uptime)); + index += sizeof(uptime); + std::memcpy(&out[index], &err_flags, sizeof(err_flags)); + index += sizeof(err_flags); + out[index++] = queue_len; + enqueueFrame(out, index); + return; + } + + if (stats_type == STATS_TYPE_RADIO) + { + uint8_t out[16] = {}; + size_t index = 0; + out[index++] = RESP_CODE_STATS; + out[index++] = STATS_TYPE_RADIO; + const int16_t noise_floor = 0; + const int8_t last_rssi = 0; + const int8_t last_snr = 0; + const uint32_t tx_air = 0; + const uint32_t rx_air = 0; + std::memcpy(&out[index], &noise_floor, sizeof(noise_floor)); + index += sizeof(noise_floor); + out[index++] = static_cast(last_rssi); + out[index++] = static_cast(last_snr); + std::memcpy(&out[index], &tx_air, sizeof(tx_air)); + index += sizeof(tx_air); + std::memcpy(&out[index], &rx_air, sizeof(rx_air)); + index += sizeof(rx_air); + enqueueFrame(out, index); + return; + } + + if (stats_type == STATS_TYPE_PACKETS) + { + uint8_t out[32] = {}; + size_t index = 0; + out[index++] = RESP_CODE_STATS; + out[index++] = STATS_TYPE_PACKETS; + uint32_t counts[6] = { + stats_rx_packets_, + stats_tx_packets_, + stats_tx_flood_, + stats_tx_direct_, + stats_rx_flood_, + stats_rx_direct_, + }; + std::memcpy(&out[index], counts, sizeof(counts)); + index += sizeof(counts); + enqueueFrame(out, index); + return; + } + + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + + if (cmd == CMD_FACTORY_RESET && len >= 6) + { + if (std::memcmp(&data[1], "reset", 5) != 0) + { + enqueueErr(ERR_CODE_ILLEGAL_ARG); + return; + } + ctx_.resetMeshConfig(); + ctx_.clearNodeDb(); + ctx_.clearMessageDb(); + ctx_.setBleEnabled(true); + tx_queue_.clear(); + sign_data_.clear(); + sign_active_ = false; + enqueueSentOk(); + delay(100); + NVIC_SystemReset(); + return; + } + + enqueueErr(ERR_CODE_ILLEGAL_ARG); +} + +void MeshCoreBleService::enqueueFrame(const uint8_t* data, size_t len) +{ + if (!data || len == 0) + { + return; + } + Frame frame{}; + frame.len = std::min(len, frame.buf.size()); + std::memcpy(frame.buf.data(), data, frame.len); + tx_queue_.push_back(frame); +} + +void MeshCoreBleService::enqueueSentOk() +{ + const uint8_t code = RESP_CODE_OK; + enqueueFrame(&code, 1); +} + +void MeshCoreBleService::enqueueErr(uint8_t err) +{ + const uint8_t buf[2] = {RESP_CODE_ERR, err}; + enqueueFrame(buf, sizeof(buf)); +} + +void MeshCoreBleService::sendPendingNotifications() +{ + if (!active_ || !Bluefruit.connected()) + { + return; + } + + while (!tx_queue_.empty()) + { + const Frame& frame = tx_queue_.front(); + if (!tx_char_.notify(frame.buf.data(), static_cast(frame.len))) + { + break; + } + tx_queue_.pop_front(); + } +} + +void MeshCoreBleService::enqueueRawDataPush(const chat::MeshIncomingData& msg) +{ + ++stats_rx_packets_; + if (msg.rx_meta.direct) + { + ++stats_rx_direct_; + } + else + { + ++stats_rx_flood_; + } + + Frame frame{}; + size_t index = 0; + frame.buf[index++] = PUSH_CODE_RAW_DATA; + const size_t payload_len = std::min(msg.payload.size(), frame.buf.size() - index); + if (payload_len > 0) + { + std::memcpy(&frame.buf[index], msg.payload.data(), payload_len); + index += payload_len; + } + frame.len = index; + tx_queue_.push_back(frame); +} + +uint32_t MeshCoreBleService::resolveNodeIdFromPrefix(const uint8_t* prefix, size_t len) const +{ + if (!prefix || len == 0) + { + return 0; + } + + if (len >= sizeof(uint32_t)) + { + uint32_t node_id = 0; + std::memcpy(&node_id, prefix, sizeof(node_id)); + if (node_id != 0) + { + return node_id; + } + } + + const auto* store = ctx_.getNodeStore(); + if (!store) + { + return 0; + } + + for (const auto& entry : store->getEntries()) + { + if (entry.node_id == 0) + { + continue; + } + if (std::memcmp(&entry.node_id, prefix, std::min(len, sizeof(entry.node_id))) == 0) + { + return entry.node_id; + } + } + return 0; +} + +bool MeshCoreBleService::buildContactFromNode(const chat::contacts::NodeEntry& entry, uint8_t code, Frame& out) const +{ + size_t index = 0; + out.buf[index++] = code; + uint8_t pubkey[kPubKeySize] = {}; + std::memcpy(pubkey, &entry.node_id, std::min(sizeof(entry.node_id), sizeof(pubkey))); + std::memcpy(&out.buf[index], pubkey, sizeof(pubkey)); + index += sizeof(pubkey); + out.buf[index++] = ADV_TYPE_CHAT; + out.buf[index++] = 0; + out.buf[index++] = 0; + std::memset(&out.buf[index], 0, kMaxPathSize); + index += kMaxPathSize; + + char name[32] = {}; + copyBounded(name, sizeof(name), entry.long_name[0] != '\0' ? entry.long_name : entry.short_name); + if (name[0] == '\0') + { + std::snprintf(name, sizeof(name), "%08lX", static_cast(entry.node_id)); + } + copyBounded(reinterpret_cast(&out.buf[index]), 32, name); + index += 32; + + const uint32_t last_adv = entry.last_seen; + std::memcpy(&out.buf[index], &last_adv, sizeof(last_adv)); + index += sizeof(last_adv); + const int32_t lat = 0; + const int32_t lon = 0; + std::memcpy(&out.buf[index], &lat, sizeof(lat)); + index += sizeof(lat); + std::memcpy(&out.buf[index], &lon, sizeof(lon)); + index += sizeof(lon); + std::memcpy(&out.buf[index], &last_adv, sizeof(last_adv)); + index += sizeof(last_adv); + + out.len = index; + return true; +} + +} // namespace ble diff --git a/platform/nrf52/arduino_common/src/ble/meshtastic_ble.cpp b/platform/nrf52/arduino_common/src/ble/meshtastic_ble.cpp new file mode 100644 index 00000000..3f702220 --- /dev/null +++ b/platform/nrf52/arduino_common/src/ble/meshtastic_ble.cpp @@ -0,0 +1,1511 @@ +#include "../../include/ble/meshtastic_ble.h" + +#include "app/app_config.h" +#include "boards/gat562_mesh_evb_pro/gat562_board.h" +#include "ble/ble_uuids.h" +#include "chat/ports/i_node_store.h" +#include "chat/runtime/self_identity_policy.h" +#include "pb_decode.h" +#include "pb_encode.h" +#include "platform/nrf52/arduino_common/device_identity.h" +#include "sys/clock.h" + +#include +#include +#include +#include +#include +#include + +namespace ble +{ +namespace +{ + +constexpr uint32_t kOfficialMinAppVersion = 30200; +constexpr uint32_t kOfficialDeviceStateVersion = 24; +constexpr const char* kCompatFirmwareVersion = "2.7.4.0"; +constexpr uint8_t kQueueDepthHint = 4; +constexpr uint8_t kMaxMeshtasticChannels = 8; +constexpr uint32_t kModuleConfigVersion = 1; + +MeshtasticBleService* s_active_service = nullptr; + +void copyBounded(char* dst, size_t dst_len, const char* src) +{ + if (!dst || dst_len == 0) + { + return; + } + if (!src) + { + dst[0] = '\0'; + return; + } + std::strncpy(dst, src, dst_len - 1); + dst[dst_len - 1] = '\0'; +} + +uint32_t nowSeconds() +{ + return sys::epoch_seconds_now(); +} + +uint8_t channelIndexFromId(chat::ChannelId channel) +{ + return (channel == chat::ChannelId::SECONDARY) ? 1U : 0U; +} + +meshtastic_Config_DeviceConfig_Role roleFromEntry(uint8_t role) +{ + switch (role) + { + case 0: + return meshtastic_Config_DeviceConfig_Role_CLIENT; + case 1: + return meshtastic_Config_DeviceConfig_Role_CLIENT_MUTE; + case 2: + return meshtastic_Config_DeviceConfig_Role_ROUTER; + case 3: + return meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT; + case 4: + return meshtastic_Config_DeviceConfig_Role_REPEATER; + case 5: + return meshtastic_Config_DeviceConfig_Role_TRACKER; + case 6: + return meshtastic_Config_DeviceConfig_Role_SENSOR; + case 7: + return meshtastic_Config_DeviceConfig_Role_TAK; + case 8: + return meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN; + case 9: + return meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND; + case 10: + return meshtastic_Config_DeviceConfig_Role_TAK_TRACKER; + case 11: + return meshtastic_Config_DeviceConfig_Role_ROUTER_LATE; + case 12: + return meshtastic_Config_DeviceConfig_Role_CLIENT_BASE; + default: + return meshtastic_Config_DeviceConfig_Role_CLIENT; + } +} + +void prepareBluefruit(const std::string& device_name) +{ + Bluefruit.autoConnLed(false); + Bluefruit.configPrphBandwidth(BANDWIDTH_MAX); + Bluefruit.begin(); + Bluefruit.setName(device_name.c_str()); +} + +void startAdvertising(::BLEService& service) +{ + Bluefruit.Advertising.stop(); + Bluefruit.Advertising.clearData(); + Bluefruit.ScanResponse.clearData(); + Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE); + Bluefruit.Advertising.addService(service); + Bluefruit.ScanResponse.addName(); + Bluefruit.ScanResponse.addTxPower(); + Bluefruit.Advertising.restartOnDisconnect(true); + Bluefruit.Advertising.setInterval(32, 668); + Bluefruit.Advertising.setFastTimeout(30); + Bluefruit.Advertising.start(0); +} + +void disconnectAll() +{ + for (uint8_t index = 0; index < BLE_MAX_CONNECTION; ++index) + { + if (Bluefruit.connected(index)) + { + Bluefruit.disconnect(index); + } + } +} + +void authorizeRead(uint16_t conn_handle) +{ + ble_gatts_rw_authorize_reply_params_t reply = {.type = BLE_GATTS_AUTHORIZE_TYPE_READ}; + reply.params.read.gatt_status = BLE_GATT_STATUS_SUCCESS; + sd_ble_gatts_rw_authorize_reply(conn_handle, &reply); +} + +void onToRadioWrite(uint16_t, BLECharacteristic*, uint8_t* data, uint16_t len) +{ + if (!s_active_service || !data || len == 0) + { + return; + } + (void)s_active_service->handleToRadio(data, len); +} + +void onFromRadioAuthorize(uint16_t conn_handle, BLECharacteristic* chr, ble_gatts_evt_read_t* request) +{ + if (!chr || !request) + { + authorizeRead(conn_handle); + return; + } + + if (request->offset == 0) + { + MeshtasticBleService::Frame frame{}; + if (s_active_service && s_active_service->popToPhone(&frame)) + { + chr->write(frame.buf, frame.len); + } + else + { + uint8_t empty = 0; + chr->write(&empty, 0); + } + } + + authorizeRead(conn_handle); +} + +void initDefaultModuleConfig(meshtastic_LocalModuleConfig* out, uint32_t self_node) +{ + if (!out) + { + return; + } + meshtastic_LocalModuleConfig zero = meshtastic_LocalModuleConfig_init_zero; + *out = zero; + out->version = kModuleConfigVersion; + out->has_mqtt = true; + out->has_serial = true; + out->has_external_notification = true; + out->has_store_forward = true; + out->has_range_test = true; + out->has_telemetry = true; + out->has_canned_message = true; + out->has_audio = true; + out->has_remote_hardware = true; + out->has_neighbor_info = true; + out->has_ambient_lighting = true; + out->has_detection_sensor = true; + out->has_paxcounter = true; + + out->telemetry.device_update_interval = 3600; + out->telemetry.device_telemetry_enabled = true; + out->telemetry.environment_update_interval = 0; + out->telemetry.environment_measurement_enabled = false; + out->telemetry.power_update_interval = 0; + out->telemetry.health_update_interval = 0; + out->telemetry.air_quality_interval = 0; + + out->neighbor_info.enabled = false; + out->neighbor_info.update_interval = 0; + out->neighbor_info.transmit_over_lora = false; + + out->detection_sensor.enabled = false; + out->ambient_lighting.current = 8; + out->ambient_lighting.red = (self_node >> 16) & 0xFFU; + out->ambient_lighting.green = (self_node >> 8) & 0xFFU; + out->ambient_lighting.blue = self_node & 0xFFU; +} + +bool moduleConfigTypeFromVariant(pb_size_t variant_tag, meshtastic_AdminMessage_ModuleConfigType* out) +{ + if (!out) + { + return false; + } + switch (variant_tag) + { + case meshtastic_ModuleConfig_mqtt_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG; + return true; + case meshtastic_ModuleConfig_serial_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG; + return true; + case meshtastic_ModuleConfig_external_notification_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG; + return true; + case meshtastic_ModuleConfig_store_forward_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG; + return true; + case meshtastic_ModuleConfig_range_test_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG; + return true; + case meshtastic_ModuleConfig_telemetry_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG; + return true; + case meshtastic_ModuleConfig_canned_message_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG; + return true; + case meshtastic_ModuleConfig_audio_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG; + return true; + case meshtastic_ModuleConfig_remote_hardware_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG; + return true; + case meshtastic_ModuleConfig_neighbor_info_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG; + return true; + case meshtastic_ModuleConfig_ambient_lighting_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG; + return true; + case meshtastic_ModuleConfig_detection_sensor_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG; + return true; + case meshtastic_ModuleConfig_paxcounter_tag: + *out = meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG; + return true; + default: + return false; + } +} + +} // namespace + +MeshtasticBleService::MeshtasticBleService(app::IAppBleFacade& ctx, const std::string& device_name) + : ctx_(ctx), + device_name_(device_name), + service_(::BLEUuid(MESH_SERVICE_UUID)), + to_radio_(::BLEUuid(TORADIO_UUID)), + from_radio_(::BLEUuid(FROMRADIO_UUID)), + from_num_(::BLEUuid(FROMNUM_UUID)), + log_radio_(::BLEUuid(LOGRADIO_UUID)) +{ + initDefaultModuleConfig(&module_config_, ctx_.getSelfNodeId()); +} + +MeshtasticBleService::~MeshtasticBleService() +{ + stop(); +} + +void MeshtasticBleService::start() +{ + s_active_service = this; + prepareBluefruit(device_name_); + + service_.begin(); + + to_radio_.setProperties(CHR_PROPS_WRITE); + to_radio_.setPermission(SECMODE_OPEN, SECMODE_OPEN); + to_radio_.setFixedLen(0); + to_radio_.setMaxLen(meshtastic_ToRadio_size); + to_radio_.setWriteCallback(onToRadioWrite, false); + to_radio_.begin(); + + from_radio_.setProperties(CHR_PROPS_READ); + from_radio_.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); + from_radio_.setFixedLen(0); + from_radio_.setMaxLen(meshtastic_FromRadio_size); + from_radio_.setReadAuthorizeCallback(onFromRadioAuthorize, false); + from_radio_.begin(); + + from_num_.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); + from_num_.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); + from_num_.setFixedLen(4); + from_num_.write32(0); + from_num_.begin(); + + log_radio_.setProperties(CHR_PROPS_NOTIFY | CHR_PROPS_READ); + log_radio_.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS); + log_radio_.setFixedLen(0); + log_radio_.setMaxLen(96); + log_radio_.begin(); + + ctx_.getChatService().addIncomingTextObserver(this); + startAdvertising(service_); + active_ = true; +} + +void MeshtasticBleService::stop() +{ + ctx_.getChatService().removeIncomingTextObserver(this); + Bluefruit.Advertising.stop(); + frame_queue_.clear(); + queue_status_queue_.clear(); + packet_queue_.clear(); + active_ = false; + if (s_active_service == this) + { + s_active_service = nullptr; + } +} + +void MeshtasticBleService::update() +{ + if (!active_) + { + return; + } + + pumpIncomingAppData(); + + if (!Bluefruit.connected() && !Bluefruit.Advertising.isRunning()) + { + Bluefruit.Advertising.start(0); + } +} + +void MeshtasticBleService::onIncomingText(const chat::MeshIncomingText& msg) +{ + packet_queue_.push_back(buildPacketFromText(msg)); + notifyFromNum(packet_queue_.back().id); +} + +bool MeshtasticBleService::handleToRadio(const uint8_t* data, size_t len) +{ + if (!data || len == 0 || len > sizeof(last_to_radio_)) + { + return false; + } + + if (last_to_radio_len_ == len && std::memcmp(last_to_radio_, data, len) == 0) + { + return true; + } + std::memcpy(last_to_radio_, data, len); + last_to_radio_len_ = len; + + meshtastic_ToRadio to_radio = meshtastic_ToRadio_init_zero; + pb_istream_t stream = pb_istream_from_buffer(data, len); + if (!pb_decode(&stream, meshtastic_ToRadio_fields, &to_radio)) + { + return false; + } + + switch (to_radio.which_payload_variant) + { + case meshtastic_ToRadio_packet_tag: + return handleToRadioPacket(to_radio.packet); + case meshtastic_ToRadio_want_config_id_tag: + enqueueConfigSnapshot(to_radio.want_config_id); + return true; + case meshtastic_ToRadio_heartbeat_tag: + enqueueQueueStatus(to_radio.heartbeat.nonce, true); + return true; + case meshtastic_ToRadio_disconnect_tag: + disconnectAll(); + return true; + default: + return false; + } +} + +bool MeshtasticBleService::handleToRadioPacket(meshtastic_MeshPacket& packet) +{ + if (packet.which_payload_variant != meshtastic_MeshPacket_decoded_tag) + { + enqueueQueueStatus(packet.id, false); + return false; + } + + if (packet.id == 0) + { + packet.id = millis(); + } + + packet.from = ctx_.getSelfNodeId(); + packet.rx_time = nowSeconds(); + + const bool admin_for_self = + (packet.decoded.portnum == meshtastic_PortNum_ADMIN_APP) && + (packet.to == 0 || packet.to == ctx_.getSelfNodeId()); + if (admin_for_self) + { + const bool ok = handleAdmin(packet); + enqueueQueueStatus(packet.id, ok); + return ok; + } + + if (handleLocalSelfPacket(packet)) + { + enqueueQueueStatus(packet.id, true); + return true; + } + + const chat::ChannelId channel = (packet.channel == 1) ? chat::ChannelId::SECONDARY : chat::ChannelId::PRIMARY; + + if (packet.decoded.portnum == meshtastic_PortNum_TEXT_MESSAGE_APP) + { + std::string text(reinterpret_cast(packet.decoded.payload.bytes), packet.decoded.payload.size); + const chat::MessageId msg_id = ctx_.getChatService().sendText(channel, text, packet.to); + enqueueQueueStatus(packet.id, msg_id != 0); + return msg_id != 0; + } + + chat::IMeshAdapter* adapter = ctx_.getMeshAdapter(); + if (!adapter) + { + enqueueQueueStatus(packet.id, false); + return false; + } + + const bool ok = adapter->sendAppData(channel, + static_cast(packet.decoded.portnum), + packet.decoded.payload.bytes, + packet.decoded.payload.size, + packet.to, + packet.want_ack, + packet.id, + packet.decoded.want_response); + enqueueQueueStatus(packet.id, ok); + return ok; +} + +bool MeshtasticBleService::handleAdmin(meshtastic_MeshPacket& packet) +{ + meshtastic_AdminMessage req = meshtastic_AdminMessage_init_zero; + pb_istream_t stream = pb_istream_from_buffer(packet.decoded.payload.bytes, packet.decoded.payload.size); + if (!pb_decode(&stream, meshtastic_AdminMessage_fields, &req)) + { + return false; + } + + meshtastic_AdminMessage resp = meshtastic_AdminMessage_init_zero; + bool has_resp = false; + auto& cfg = ctx_.getConfig(); + + switch (req.which_payload_variant) + { + case meshtastic_AdminMessage_get_owner_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; + resp.get_owner_response = buildSelfNodeInfo().user; + has_resp = true; + break; + case meshtastic_AdminMessage_get_channel_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; + resp.get_channel_response = buildChannel(static_cast(req.get_channel_request > 0 ? (req.get_channel_request - 1) : 0)); + has_resp = true; + break; + case meshtastic_AdminMessage_get_config_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; + resp.get_config_response = buildConfig(req.get_config_request); + has_resp = true; + break; + case meshtastic_AdminMessage_get_module_config_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; + resp.get_module_config_response = buildModuleConfig(req.get_module_config_request); + has_resp = true; + break; + case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + copyBounded(resp.get_canned_message_module_messages_response, + sizeof(resp.get_canned_message_module_messages_response), + admin_canned_messages_); + has_resp = true; + break; + case meshtastic_AdminMessage_get_device_metadata_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag; + resp.get_device_metadata_response = buildMetadata(); + has_resp = true; + break; + case meshtastic_AdminMessage_get_ringtone_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_ringtone_response_tag; + copyBounded(resp.get_ringtone_response, + sizeof(resp.get_ringtone_response), + admin_ringtone_); + has_resp = true; + break; + case meshtastic_AdminMessage_get_device_connection_status_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag; + { + meshtastic_DeviceConnectionStatus status = meshtastic_DeviceConnectionStatus_init_zero; + resp.get_device_connection_status_response = status; + } + resp.get_device_connection_status_response.has_bluetooth = true; + resp.get_device_connection_status_response.bluetooth.pin = 0; + resp.get_device_connection_status_response.bluetooth.rssi = 0; + resp.get_device_connection_status_response.bluetooth.is_connected = Bluefruit.connected(); + has_resp = true; + break; + case meshtastic_AdminMessage_get_ui_config_request_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; + resp.get_ui_config_response = buildDeviceUi(); + has_resp = true; + break; + case meshtastic_AdminMessage_set_owner_tag: + copyBounded(cfg.node_name, sizeof(cfg.node_name), req.set_owner.long_name); + copyBounded(cfg.short_name, sizeof(cfg.short_name), req.set_owner.short_name); + ctx_.saveConfig(); + ctx_.applyUserInfo(); + resp.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag; + resp.get_owner_response = buildSelfNodeInfo().user; + has_resp = true; + break; + case meshtastic_AdminMessage_set_channel_tag: + if (req.set_channel.index == 0) + { + cfg.primary_enabled = (req.set_channel.role != meshtastic_Channel_Role_DISABLED); + cfg.primary_uplink_enabled = req.set_channel.settings.uplink_enabled; + cfg.primary_downlink_enabled = req.set_channel.settings.downlink_enabled; + if (req.set_channel.settings.psk.size == 16) + { + std::memcpy(cfg.meshtastic_config.primary_key, req.set_channel.settings.psk.bytes, 16); + } + else if (req.set_channel.settings.psk.size == 0) + { + std::memset(cfg.meshtastic_config.primary_key, 0, sizeof(cfg.meshtastic_config.primary_key)); + } + } + else if (req.set_channel.index == 1) + { + cfg.secondary_enabled = (req.set_channel.role != meshtastic_Channel_Role_DISABLED); + cfg.secondary_uplink_enabled = req.set_channel.settings.uplink_enabled; + cfg.secondary_downlink_enabled = req.set_channel.settings.downlink_enabled; + if (req.set_channel.settings.psk.size == 16) + { + std::memcpy(cfg.meshtastic_config.secondary_key, req.set_channel.settings.psk.bytes, 16); + } + else if (req.set_channel.settings.psk.size == 0) + { + std::memset(cfg.meshtastic_config.secondary_key, 0, sizeof(cfg.meshtastic_config.secondary_key)); + } + } + ctx_.saveConfig(); + ctx_.applyMeshConfig(); + resp.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag; + resp.get_channel_response = buildChannel(req.set_channel.index); + has_resp = true; + break; + case meshtastic_AdminMessage_set_config_tag: + switch (req.set_config.which_payload_variant) + { + case meshtastic_Config_lora_tag: + cfg.meshtastic_config.use_preset = req.set_config.payload_variant.lora.use_preset; + cfg.meshtastic_config.modem_preset = static_cast(req.set_config.payload_variant.lora.modem_preset); + cfg.meshtastic_config.bandwidth_khz = req.set_config.payload_variant.lora.bandwidth; + cfg.meshtastic_config.spread_factor = static_cast(req.set_config.payload_variant.lora.spread_factor); + cfg.meshtastic_config.coding_rate = req.set_config.payload_variant.lora.coding_rate; + cfg.meshtastic_config.frequency_offset_mhz = req.set_config.payload_variant.lora.frequency_offset; + cfg.meshtastic_config.region = static_cast(req.set_config.payload_variant.lora.region); + cfg.meshtastic_config.hop_limit = static_cast(req.set_config.payload_variant.lora.hop_limit); + cfg.meshtastic_config.tx_enabled = req.set_config.payload_variant.lora.tx_enabled; + cfg.meshtastic_config.tx_power = req.set_config.payload_variant.lora.tx_power; + cfg.meshtastic_config.channel_num = req.set_config.payload_variant.lora.channel_num; + cfg.meshtastic_config.override_duty_cycle = req.set_config.payload_variant.lora.override_duty_cycle; + cfg.meshtastic_config.override_frequency_mhz = req.set_config.payload_variant.lora.override_frequency; + cfg.meshtastic_config.ignore_mqtt = req.set_config.payload_variant.lora.ignore_mqtt; + cfg.meshtastic_config.config_ok_to_mqtt = req.set_config.payload_variant.lora.config_ok_to_mqtt; + ctx_.saveConfig(); + ctx_.applyMeshConfig(); + break; + case meshtastic_Config_position_tag: + cfg.gps_mode = req.set_config.payload_variant.position.gps_enabled ? 1 : 0; + cfg.gps_interval_ms = req.set_config.payload_variant.position.gps_update_interval * 1000U; + ctx_.saveConfig(); + ctx_.applyPositionConfig(); + break; + case meshtastic_Config_bluetooth_tag: + ctx_.setBleEnabled(req.set_config.payload_variant.bluetooth.enabled); + break; + case meshtastic_Config_device_ui_tag: + break; + case meshtastic_Config_display_tag: + break; + default: + break; + } + resp.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag; + if (req.set_config.which_payload_variant == meshtastic_Config_position_tag) + { + resp.get_config_response = buildConfig(meshtastic_AdminMessage_ConfigType_POSITION_CONFIG); + } + else if (req.set_config.which_payload_variant == meshtastic_Config_bluetooth_tag) + { + resp.get_config_response = buildConfig(meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG); + } + else if (req.set_config.which_payload_variant == meshtastic_Config_display_tag) + { + resp.get_config_response = buildConfig(meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG); + } + else if (req.set_config.which_payload_variant == meshtastic_Config_device_ui_tag) + { + resp.get_config_response = buildConfig(meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG); + } + else + { + resp.get_config_response = buildConfig(meshtastic_AdminMessage_ConfigType_LORA_CONFIG); + } + has_resp = true; + break; + case meshtastic_AdminMessage_set_module_config_tag: + { + switch (req.set_module_config.which_payload_variant) + { + case meshtastic_ModuleConfig_mqtt_tag: + module_config_.has_mqtt = true; + module_config_.mqtt = req.set_module_config.payload_variant.mqtt; + break; + case meshtastic_ModuleConfig_serial_tag: + module_config_.has_serial = true; + module_config_.serial = req.set_module_config.payload_variant.serial; + break; + case meshtastic_ModuleConfig_external_notification_tag: + module_config_.has_external_notification = true; + module_config_.external_notification = req.set_module_config.payload_variant.external_notification; + break; + case meshtastic_ModuleConfig_store_forward_tag: + module_config_.has_store_forward = true; + module_config_.store_forward = req.set_module_config.payload_variant.store_forward; + break; + case meshtastic_ModuleConfig_range_test_tag: + module_config_.has_range_test = true; + module_config_.range_test = req.set_module_config.payload_variant.range_test; + break; + case meshtastic_ModuleConfig_telemetry_tag: + module_config_.has_telemetry = true; + module_config_.telemetry = req.set_module_config.payload_variant.telemetry; + break; + case meshtastic_ModuleConfig_canned_message_tag: + module_config_.has_canned_message = true; + module_config_.canned_message = req.set_module_config.payload_variant.canned_message; + break; + case meshtastic_ModuleConfig_audio_tag: + module_config_.has_audio = true; + module_config_.audio = req.set_module_config.payload_variant.audio; + break; + case meshtastic_ModuleConfig_remote_hardware_tag: + module_config_.has_remote_hardware = true; + module_config_.remote_hardware = req.set_module_config.payload_variant.remote_hardware; + break; + case meshtastic_ModuleConfig_neighbor_info_tag: + module_config_.has_neighbor_info = true; + module_config_.neighbor_info = req.set_module_config.payload_variant.neighbor_info; + break; + case meshtastic_ModuleConfig_ambient_lighting_tag: + module_config_.has_ambient_lighting = true; + module_config_.ambient_lighting = req.set_module_config.payload_variant.ambient_lighting; + break; + case meshtastic_ModuleConfig_detection_sensor_tag: + module_config_.has_detection_sensor = true; + module_config_.detection_sensor = req.set_module_config.payload_variant.detection_sensor; + break; + case meshtastic_ModuleConfig_paxcounter_tag: + module_config_.has_paxcounter = true; + module_config_.paxcounter = req.set_module_config.payload_variant.paxcounter; + break; + default: + break; + } + + resp.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag; + meshtastic_AdminMessage_ModuleConfigType module_type = meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG; + if (moduleConfigTypeFromVariant(req.set_module_config.which_payload_variant, &module_type)) + { + resp.get_module_config_response = buildModuleConfig(module_type); + } + else + { + resp.get_module_config_response = req.set_module_config; + } + has_resp = true; + break; + } + case meshtastic_AdminMessage_set_canned_message_module_messages_tag: + copyBounded(admin_canned_messages_, + sizeof(admin_canned_messages_), + req.set_canned_message_module_messages); + resp.which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag; + copyBounded(resp.get_canned_message_module_messages_response, + sizeof(resp.get_canned_message_module_messages_response), + admin_canned_messages_); + has_resp = true; + break; + case meshtastic_AdminMessage_set_ringtone_message_tag: + copyBounded(admin_ringtone_, + sizeof(admin_ringtone_), + req.set_ringtone_message); + resp.which_payload_variant = meshtastic_AdminMessage_get_ringtone_response_tag; + copyBounded(resp.get_ringtone_response, + sizeof(resp.get_ringtone_response), + admin_ringtone_); + has_resp = true; + break; + case meshtastic_AdminMessage_store_ui_config_tag: + resp.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag; + resp.get_ui_config_response = buildDeviceUi(); + has_resp = true; + break; + case meshtastic_AdminMessage_set_time_only_tag: + { + ::boards::gat562_mesh_evb_pro::Gat562Board::instance().setCurrentEpochSeconds( + static_cast(req.set_time_only)); + return true; + } + case meshtastic_AdminMessage_remove_by_nodenum_tag: + return true; + case meshtastic_AdminMessage_factory_reset_config_tag: + ctx_.resetMeshConfig(); + ctx_.clearNodeDb(); + ctx_.clearMessageDb(); + return true; + default: + return false; + } + + if (!has_resp) + { + return true; + } + + meshtastic_MeshPacket reply = meshtastic_MeshPacket_init_zero; + reply.from = ctx_.getSelfNodeId(); + reply.to = ctx_.getSelfNodeId(); + reply.channel = packet.channel; + reply.id = static_cast(millis()); + reply.rx_time = nowSeconds(); + reply.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + reply.decoded = meshtastic_Data_init_zero; + reply.decoded.portnum = meshtastic_PortNum_ADMIN_APP; + reply.decoded.dest = reply.to; + reply.decoded.source = reply.from; + reply.decoded.request_id = packet.id; + reply.decoded.want_response = false; + reply.decoded.has_bitfield = true; + reply.decoded.bitfield = 0; + + pb_ostream_t out_stream = pb_ostream_from_buffer(reply.decoded.payload.bytes, sizeof(reply.decoded.payload.bytes)); + if (!pb_encode(&out_stream, meshtastic_AdminMessage_fields, &resp)) + { + return false; + } + reply.decoded.payload.size = static_cast(out_stream.bytes_written); + packet_queue_.push_back(reply); + notifyFromNum(reply.id); + return true; +} + +bool MeshtasticBleService::handleLocalSelfPacket(meshtastic_MeshPacket& packet) +{ + const uint32_t self = ctx_.getSelfNodeId(); + if (self == 0 || packet.to != self || packet.which_payload_variant != meshtastic_MeshPacket_decoded_tag) + { + return false; + } + + if (packet.decoded.portnum == meshtastic_PortNum_TELEMETRY_APP && packet.decoded.want_response && + packet.decoded.payload.size > 0) + { + meshtastic_Telemetry req = meshtastic_Telemetry_init_zero; + pb_istream_t req_stream = pb_istream_from_buffer(packet.decoded.payload.bytes, packet.decoded.payload.size); + if (!pb_decode(&req_stream, meshtastic_Telemetry_fields, &req)) + { + return false; + } + + meshtastic_Telemetry resp = meshtastic_Telemetry_init_zero; + resp.time = nowSeconds(); + switch (req.which_variant) + { + case meshtastic_Telemetry_device_metrics_tag: + resp.which_variant = meshtastic_Telemetry_device_metrics_tag; + resp.variant.device_metrics = buildDeviceMetrics(); + break; + case meshtastic_Telemetry_local_stats_tag: + resp.which_variant = meshtastic_Telemetry_local_stats_tag; + resp.variant.local_stats = buildLocalStats(); + break; + default: + return false; + } + + meshtastic_MeshPacket reply = meshtastic_MeshPacket_init_zero; + reply.from = self; + reply.to = self; + reply.channel = packet.channel; + reply.id = static_cast(millis()); + reply.rx_time = nowSeconds(); + reply.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + reply.decoded = meshtastic_Data_init_zero; + reply.decoded.portnum = meshtastic_PortNum_TELEMETRY_APP; + reply.decoded.dest = self; + reply.decoded.source = self; + reply.decoded.request_id = packet.id; + reply.decoded.want_response = false; + reply.decoded.has_bitfield = true; + reply.decoded.bitfield = 0; + pb_ostream_t out_stream = pb_ostream_from_buffer(reply.decoded.payload.bytes, sizeof(reply.decoded.payload.bytes)); + if (!pb_encode(&out_stream, meshtastic_Telemetry_fields, &resp)) + { + return false; + } + reply.decoded.payload.size = static_cast(out_stream.bytes_written); + packet_queue_.push_back(reply); + notifyFromNum(reply.id); + return true; + } + + if (packet.decoded.portnum == meshtastic_PortNum_NODEINFO_APP && packet.decoded.want_response) + { + meshtastic_NodeInfo self_info = buildSelfNodeInfo(); + meshtastic_MeshPacket reply = meshtastic_MeshPacket_init_zero; + reply.from = self; + reply.to = self; + reply.channel = packet.channel; + reply.id = static_cast(millis()); + reply.rx_time = nowSeconds(); + reply.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + reply.decoded = meshtastic_Data_init_zero; + reply.decoded.portnum = meshtastic_PortNum_NODEINFO_APP; + reply.decoded.dest = self; + reply.decoded.source = self; + reply.decoded.request_id = packet.id; + reply.decoded.want_response = false; + reply.decoded.has_bitfield = true; + reply.decoded.bitfield = 0; + pb_ostream_t out_stream = pb_ostream_from_buffer(reply.decoded.payload.bytes, sizeof(reply.decoded.payload.bytes)); + if (!pb_encode(&out_stream, meshtastic_User_fields, &self_info.user)) + { + return false; + } + reply.decoded.payload.size = static_cast(out_stream.bytes_written); + packet_queue_.push_back(reply); + notifyFromNum(reply.id); + return true; + } + + return false; +} + +void MeshtasticBleService::pumpIncomingAppData() +{ + chat::IMeshAdapter* adapter = ctx_.getMeshAdapter(); + if (!adapter) + { + return; + } + + for (uint8_t count = 0; count < kQueueDepthHint; ++count) + { + chat::MeshIncomingData incoming{}; + if (!adapter->pollIncomingData(&incoming)) + { + break; + } + packet_queue_.push_back(buildPacketFromData(incoming)); + notifyFromNum(packet_queue_.back().id); + } +} + +bool MeshtasticBleService::popToPhone(Frame* out) +{ + if (!out) + { + return false; + } + + if (!frame_queue_.empty()) + { + *out = frame_queue_.front(); + frame_queue_.pop_front(); + return true; + } + + meshtastic_FromRadio from = meshtastic_FromRadio_init_zero; + if (!queue_status_queue_.empty()) + { + from.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; + from.queueStatus = queue_status_queue_.front(); + queue_status_queue_.pop_front(); + return encodeFromRadio(from, from.queueStatus.mesh_packet_id, out); + } + + if (!packet_queue_.empty()) + { + from.which_payload_variant = meshtastic_FromRadio_packet_tag; + from.packet = packet_queue_.front(); + packet_queue_.pop_front(); + return encodeFromRadio(from, from.packet.id, out); + } + + return false; +} + +bool MeshtasticBleService::encodeFromRadio(const meshtastic_FromRadio& from, uint32_t from_num, Frame* out) const +{ + if (!out) + { + return false; + } + + meshtastic_FromRadio msg = from; + pb_ostream_t ostream = pb_ostream_from_buffer(out->buf, sizeof(out->buf)); + if (!pb_encode(&ostream, meshtastic_FromRadio_fields, &msg)) + { + return false; + } + + out->len = ostream.bytes_written; + out->from_num = from_num; + return true; +} + +void MeshtasticBleService::enqueueQueueStatus(uint32_t packet_id, bool ok) +{ + meshtastic_QueueStatus status = meshtastic_QueueStatus_init_zero; + status.res = ok ? 0 : 1; + status.free = kQueueDepthHint; + status.maxlen = kQueueDepthHint; + status.mesh_packet_id = packet_id; + queue_status_queue_.push_back(status); + notifyFromNum(packet_id); +} + +void MeshtasticBleService::enqueueConfigSnapshot(uint32_t config_nonce) +{ + meshtastic_FromRadio from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_my_info_tag; + from.my_info = buildMyInfo(); + enqueueFromRadio(from, config_nonce); + + from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_deviceuiConfig_tag; + from.deviceuiConfig = buildDeviceUi(); + enqueueFromRadio(from, config_nonce); + + from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_node_info_tag; + from.node_info = buildSelfNodeInfo(); + enqueueFromRadio(from, config_nonce); + + if (const auto* store = ctx_.getNodeStore()) + { + const auto& entries = store->getEntries(); + for (const auto& entry : entries) + { + if (entry.node_id == 0 || entry.node_id == ctx_.getSelfNodeId()) + { + continue; + } + from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_node_info_tag; + from.node_info = buildNodeInfoFromEntry(entry); + enqueueFromRadio(from, entry.node_id); + } + } + + from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_metadata_tag; + from.metadata = buildMetadata(); + enqueueFromRadio(from, config_nonce); + + for (uint8_t channel_idx = 0; channel_idx < kMaxMeshtasticChannels; ++channel_idx) + { + from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_channel_tag; + from.channel = buildChannel(channel_idx); + enqueueFromRadio(from, config_nonce); + } + + const meshtastic_AdminMessage_ConfigType config_types[] = { + meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG, + meshtastic_AdminMessage_ConfigType_POSITION_CONFIG, + meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG, + meshtastic_AdminMessage_ConfigType_LORA_CONFIG, + meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG, + meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG, + meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG, + }; + for (const auto config_type : config_types) + { + from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_config_tag; + from.config = buildConfig(config_type); + enqueueFromRadio(from, config_nonce); + } + + const meshtastic_AdminMessage_ModuleConfigType module_types[] = { + meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG, + meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG, + }; + for (const auto module_type : module_types) + { + from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_moduleConfig_tag; + from.moduleConfig = buildModuleConfig(module_type); + enqueueFromRadio(from, config_nonce); + } + + from = meshtastic_FromRadio_init_zero; + from.which_payload_variant = meshtastic_FromRadio_config_complete_id_tag; + from.config_complete_id = config_nonce; + enqueueFromRadio(from, config_nonce); + + notifyFromNum(config_nonce); +} + +void MeshtasticBleService::enqueueFromRadio(const meshtastic_FromRadio& from, uint32_t from_num) +{ + Frame frame{}; + if (encodeFromRadio(from, from_num, &frame)) + { + frame_queue_.push_back(frame); + } +} + +void MeshtasticBleService::notifyFromNum(uint32_t from_num) +{ + if (active_ && Bluefruit.connected()) + { + from_num_.notify32(from_num); + } +} + +meshtastic_MyNodeInfo MeshtasticBleService::buildMyInfo() const +{ + meshtastic_MyNodeInfo info = meshtastic_MyNodeInfo_init_zero; + info.my_node_num = ctx_.getSelfNodeId(); + info.reboot_count = 0; + info.min_app_version = kOfficialMinAppVersion; + + size_t nodedb_count = 1; + if (const auto* store = ctx_.getNodeStore()) + { + nodedb_count += store->getEntries().size(); + } + if (nodedb_count > 0xFFFFU) + { + nodedb_count = 0xFFFFU; + } + info.nodedb_count = static_cast(nodedb_count); + + const uint32_t addr0 = NRF_FICR->DEVICEADDR[0]; + const uint32_t addr1 = NRF_FICR->DEVICEADDR[1]; + const std::array mac = + platform::nrf52::arduino_common::device_identity::deriveMacAddressFromDeviceAddress(addr0, addr1); + std::memcpy(info.device_id.bytes, mac.data(), mac.size()); + std::memcpy(info.device_id.bytes + mac.size(), &info.my_node_num, sizeof(info.my_node_num)); + info.device_id.size = static_cast(mac.size() + sizeof(info.my_node_num)); + + copyBounded(info.pio_env, sizeof(info.pio_env), "Trail Mate"); + info.firmware_edition = meshtastic_FirmwareEdition_VANILLA; + return info; +} + +meshtastic_NodeInfo MeshtasticBleService::buildSelfNodeInfo() const +{ + meshtastic_NodeInfo info = meshtastic_NodeInfo_init_zero; + info.num = ctx_.getSelfNodeId(); + info.has_user = true; + + char long_name[32] = {}; + char short_name[16] = {}; + ctx_.getEffectiveUserInfo(long_name, sizeof(long_name), short_name, sizeof(short_name)); + + char user_id[16] = {}; + std::snprintf(user_id, sizeof(user_id), "!%08lX", static_cast(ctx_.getSelfNodeId())); + copyBounded(info.user.id, sizeof(info.user.id), user_id); + copyBounded(info.user.long_name, sizeof(info.user.long_name), long_name); + copyBounded(info.user.short_name, sizeof(info.user.short_name), short_name); + info.user.hw_model = meshtastic_HardwareModel_UNSET; + info.user.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + info.channel = 0; + info.last_heard = nowSeconds(); + info.has_hops_away = true; + info.hops_away = 0; + return info; +} + +meshtastic_NodeInfo MeshtasticBleService::buildNodeInfoFromEntry(const chat::contacts::NodeEntry& entry) const +{ + meshtastic_NodeInfo info = meshtastic_NodeInfo_init_zero; + info.num = entry.node_id; + info.has_user = true; + + char user_id[16] = {}; + std::snprintf(user_id, sizeof(user_id), "!%08lX", static_cast(entry.node_id)); + copyBounded(info.user.id, sizeof(info.user.id), user_id); + copyBounded(info.user.long_name, sizeof(info.user.long_name), entry.long_name); + copyBounded(info.user.short_name, sizeof(info.user.short_name), entry.short_name); + info.user.hw_model = static_cast(entry.hw_model); + info.user.role = roleFromEntry(entry.role); + info.channel = entry.channel; + info.last_heard = entry.last_seen; + info.snr = entry.snr; + info.has_hops_away = (entry.hops_away != 0xFFU); + info.hops_away = entry.hops_away; + return info; +} + +meshtastic_DeviceMetadata MeshtasticBleService::buildMetadata() const +{ + meshtastic_DeviceMetadata metadata = meshtastic_DeviceMetadata_init_zero; + copyBounded(metadata.firmware_version, sizeof(metadata.firmware_version), kCompatFirmwareVersion); + metadata.device_state_version = kOfficialDeviceStateVersion; + metadata.canShutdown = true; + metadata.hasBluetooth = true; + metadata.hasWifi = false; + metadata.hasEthernet = false; + metadata.hasRemoteHardware = false; + metadata.hasPKC = ctx_.getMeshAdapter() ? ctx_.getMeshAdapter()->isPkiReady() : false; + metadata.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + metadata.position_flags = 0; + metadata.hw_model = meshtastic_HardwareModel_UNSET; + metadata.excluded_modules = 0; + return metadata; +} + +meshtastic_DeviceMetrics MeshtasticBleService::buildDeviceMetrics() const +{ + meshtastic_DeviceMetrics metrics = meshtastic_DeviceMetrics_init_zero; + metrics.has_uptime_seconds = true; + metrics.uptime_seconds = static_cast(millis() / 1000U); + return metrics; +} + +meshtastic_LocalStats MeshtasticBleService::buildLocalStats() const +{ + meshtastic_LocalStats stats = meshtastic_LocalStats_init_zero; + stats.uptime_seconds = static_cast(millis() / 1000U); + stats.channel_utilization = 0.0f; + stats.air_util_tx = 0.0f; + stats.num_packets_tx = 0; + stats.num_packets_rx = 0; + stats.num_packets_rx_bad = 0; + stats.num_rx_dupe = 0; + stats.num_tx_relay = 0; + stats.num_tx_relay_canceled = 0; + stats.num_tx_dropped = 0; + stats.heap_total_bytes = 0; + stats.heap_free_bytes = 0; + if (const auto* store = ctx_.getNodeStore()) + { + const size_t total = store->getEntries().size(); + stats.num_total_nodes = static_cast(std::min(total, 0xFFFFU)); + stats.num_online_nodes = stats.num_total_nodes; + } + return stats; +} + +meshtastic_DeviceUIConfig MeshtasticBleService::buildDeviceUi() const +{ + meshtastic_DeviceUIConfig ui = meshtastic_DeviceUIConfig_init_zero; + ui.version = 1; + ui.screen_brightness = 255; + ui.screen_timeout = 30; + ui.screen_lock = false; + ui.settings_lock = false; + ui.pin_code = 0; + ui.theme = meshtastic_Theme_LIGHT; + ui.alert_enabled = false; + ui.banner_enabled = true; + ui.ring_tone_id = 0; + ui.language = meshtastic_Language_ENGLISH; + ui.has_node_filter = false; + ui.has_node_highlight = false; + ui.has_map_data = false; + ui.compass_mode = meshtastic_CompassMode_DYNAMIC; + ui.screen_rgb_color = 0; + ui.is_clockface_analog = false; + ui.gps_format = meshtastic_DeviceUIConfig_GpsCoordinateFormat_DEC; + return ui; +} + +meshtastic_Channel MeshtasticBleService::buildChannel(uint8_t idx) const +{ + meshtastic_Channel channel = meshtastic_Channel_init_zero; + channel.index = idx; + + const auto& cfg = ctx_.getConfig(); + bool enabled = false; + if (idx == 0 && cfg.primary_enabled) + { + channel.role = meshtastic_Channel_Role_PRIMARY; + enabled = true; + } + else if (idx == 1 && cfg.secondary_enabled) + { + channel.role = meshtastic_Channel_Role_SECONDARY; + enabled = true; + } + else + { + channel.role = meshtastic_Channel_Role_DISABLED; + } + + if (!enabled) + { + channel.has_settings = false; + return channel; + } + + channel.has_settings = true; + { + meshtastic_ChannelSettings settings = meshtastic_ChannelSettings_init_zero; + channel.settings = settings; + } + channel.settings.channel_num = idx; + channel.settings.id = 0; + channel.settings.uplink_enabled = (idx == 0) ? cfg.primary_uplink_enabled : cfg.secondary_uplink_enabled; + channel.settings.downlink_enabled = (idx == 0) ? cfg.primary_downlink_enabled : cfg.secondary_downlink_enabled; + channel.settings.has_module_settings = false; + + if (idx == 0) + { + copyBounded(channel.settings.name, sizeof(channel.settings.name), "Primary"); + if (cfg.meshtastic_config.primary_key[0] != 0) + { + channel.settings.psk.size = sizeof(cfg.meshtastic_config.primary_key); + std::memcpy(channel.settings.psk.bytes, + cfg.meshtastic_config.primary_key, + sizeof(cfg.meshtastic_config.primary_key)); + } + else + { + channel.settings.psk.size = 1; + channel.settings.psk.bytes[0] = 1; + } + } + else + { + copyBounded(channel.settings.name, sizeof(channel.settings.name), "Secondary"); + if (cfg.meshtastic_config.secondary_key[0] != 0) + { + channel.settings.psk.size = sizeof(cfg.meshtastic_config.secondary_key); + std::memcpy(channel.settings.psk.bytes, + cfg.meshtastic_config.secondary_key, + sizeof(cfg.meshtastic_config.secondary_key)); + } + } + return channel; +} + +meshtastic_Config MeshtasticBleService::buildConfig(meshtastic_AdminMessage_ConfigType type) const +{ + const auto& cfg = ctx_.getConfig(); + meshtastic_Config out = meshtastic_Config_init_zero; + switch (type) + { + case meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG: + out.which_payload_variant = meshtastic_Config_device_tag; + { + meshtastic_Config_DeviceConfig device = meshtastic_Config_DeviceConfig_init_zero; + out.payload_variant.device = device; + } + out.payload_variant.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; + out.payload_variant.device.rebroadcast_mode = + cfg.chat_policy.enable_relay ? meshtastic_Config_DeviceConfig_RebroadcastMode_ALL + : meshtastic_Config_DeviceConfig_RebroadcastMode_NONE; + out.payload_variant.device.node_info_broadcast_secs = 900; + out.payload_variant.device.serial_enabled = false; + out.payload_variant.device.is_managed = false; + out.payload_variant.device.led_heartbeat_disabled = false; + out.payload_variant.device.buzzer_mode = meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED; + break; + case meshtastic_AdminMessage_ConfigType_POSITION_CONFIG: + out.which_payload_variant = meshtastic_Config_position_tag; + { + meshtastic_Config_PositionConfig position = meshtastic_Config_PositionConfig_init_zero; + out.payload_variant.position = position; + } + out.payload_variant.position.position_broadcast_secs = 900; + out.payload_variant.position.gps_enabled = (cfg.gps_mode != 0); + out.payload_variant.position.gps_update_interval = cfg.gps_interval_ms / 1000U; + out.payload_variant.position.gps_mode = (cfg.gps_mode != 0) + ? meshtastic_Config_PositionConfig_GpsMode_ENABLED + : meshtastic_Config_PositionConfig_GpsMode_DISABLED; + break; + case meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG: + out.which_payload_variant = meshtastic_Config_display_tag; + { + meshtastic_Config_DisplayConfig display = meshtastic_Config_DisplayConfig_init_zero; + out.payload_variant.display = display; + } + out.payload_variant.display.screen_on_secs = 30; + out.payload_variant.display.units = meshtastic_Config_DisplayConfig_DisplayUnits_METRIC; + out.payload_variant.display.oled = meshtastic_Config_DisplayConfig_OledType_OLED_AUTO; + out.payload_variant.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT; + out.payload_variant.display.compass_orientation = meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_0; + break; + case meshtastic_AdminMessage_ConfigType_LORA_CONFIG: + out.which_payload_variant = meshtastic_Config_lora_tag; + { + meshtastic_Config_LoRaConfig lora = meshtastic_Config_LoRaConfig_init_zero; + out.payload_variant.lora = lora; + } + out.payload_variant.lora.use_preset = cfg.meshtastic_config.use_preset; + out.payload_variant.lora.modem_preset = + static_cast(cfg.meshtastic_config.modem_preset); + out.payload_variant.lora.bandwidth = static_cast(cfg.meshtastic_config.bandwidth_khz); + out.payload_variant.lora.spread_factor = cfg.meshtastic_config.spread_factor; + out.payload_variant.lora.coding_rate = cfg.meshtastic_config.coding_rate; + out.payload_variant.lora.frequency_offset = cfg.meshtastic_config.frequency_offset_mhz; + out.payload_variant.lora.region = static_cast(cfg.meshtastic_config.region); + out.payload_variant.lora.hop_limit = cfg.meshtastic_config.hop_limit; + out.payload_variant.lora.tx_enabled = cfg.meshtastic_config.tx_enabled; + out.payload_variant.lora.tx_power = cfg.meshtastic_config.tx_power; + out.payload_variant.lora.channel_num = cfg.meshtastic_config.channel_num; + out.payload_variant.lora.override_duty_cycle = cfg.meshtastic_config.override_duty_cycle; + out.payload_variant.lora.override_frequency = cfg.meshtastic_config.override_frequency_mhz; + out.payload_variant.lora.ignore_mqtt = cfg.meshtastic_config.ignore_mqtt; + out.payload_variant.lora.config_ok_to_mqtt = cfg.meshtastic_config.config_ok_to_mqtt; + break; + case meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG: + out.which_payload_variant = meshtastic_Config_bluetooth_tag; + { + meshtastic_Config_BluetoothConfig bluetooth = meshtastic_Config_BluetoothConfig_init_zero; + out.payload_variant.bluetooth = bluetooth; + } + out.payload_variant.bluetooth.enabled = ctx_.isBleEnabled(); + out.payload_variant.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN; + out.payload_variant.bluetooth.fixed_pin = 0; + break; + case meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG: + out.which_payload_variant = meshtastic_Config_security_tag; + { + meshtastic_Config_SecurityConfig security = meshtastic_Config_SecurityConfig_init_zero; + out.payload_variant.security = security; + } + out.payload_variant.security.is_managed = false; + out.payload_variant.security.serial_enabled = false; + out.payload_variant.security.debug_log_api_enabled = false; + out.payload_variant.security.admin_channel_enabled = false; + break; + case meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG: + out.which_payload_variant = meshtastic_Config_device_ui_tag; + out.payload_variant.device_ui = buildDeviceUi(); + break; + default: + out.which_payload_variant = meshtastic_Config_device_tag; + { + meshtastic_Config_DeviceConfig device = meshtastic_Config_DeviceConfig_init_zero; + out.payload_variant.device = device; + } + break; + } + return out; +} + +meshtastic_ModuleConfig MeshtasticBleService::buildModuleConfig(meshtastic_AdminMessage_ModuleConfigType type) const +{ + meshtastic_ModuleConfig out = meshtastic_ModuleConfig_init_zero; + switch (type) + { + case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag; + out.payload_variant.mqtt = module_config_.mqtt; + break; + case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_serial_tag; + out.payload_variant.serial = module_config_.serial; + break; + case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag; + out.payload_variant.external_notification = module_config_.external_notification; + break; + case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag; + out.payload_variant.store_forward = module_config_.store_forward; + break; + case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_range_test_tag; + out.payload_variant.range_test = module_config_.range_test; + break; + case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag; + out.payload_variant.telemetry = module_config_.telemetry; + break; + case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag; + out.payload_variant.canned_message = module_config_.canned_message; + break; + case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_audio_tag; + out.payload_variant.audio = module_config_.audio; + break; + case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag; + out.payload_variant.remote_hardware = module_config_.remote_hardware; + break; + case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag; + out.payload_variant.neighbor_info = module_config_.neighbor_info; + break; + case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag; + out.payload_variant.ambient_lighting = module_config_.ambient_lighting; + break; + case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag; + out.payload_variant.detection_sensor = module_config_.detection_sensor; + break; + case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG: + out.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag; + out.payload_variant.paxcounter = module_config_.paxcounter; + break; + default: + break; + } + return out; +} + +meshtastic_MeshPacket MeshtasticBleService::buildPacketFromText(const chat::MeshIncomingText& msg) const +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + packet.from = msg.from; + packet.to = msg.to; + packet.channel = channelIndexFromId(msg.channel); + packet.id = (msg.msg_id == 0) ? static_cast(millis()) : msg.msg_id; + packet.rx_time = (msg.rx_meta.rx_timestamp_s != 0) ? msg.rx_meta.rx_timestamp_s : msg.timestamp; + packet.rx_snr = msg.rx_meta.snr_db_x10 / 10.0f; + packet.rx_rssi = msg.rx_meta.rssi_dbm_x10 / 10; + packet.hop_limit = msg.hop_limit; + packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + packet.decoded = meshtastic_Data_init_zero; + packet.decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP; + packet.decoded.source = msg.from; + packet.decoded.dest = msg.to; + packet.decoded.want_response = false; + packet.decoded.has_bitfield = true; + packet.decoded.bitfield = 0; + packet.decoded.payload.size = static_cast( + std::min(msg.text.size(), sizeof(packet.decoded.payload.bytes))); + if (packet.decoded.payload.size > 0) + { + std::memcpy(packet.decoded.payload.bytes, msg.text.data(), packet.decoded.payload.size); + } + return packet; +} + +meshtastic_MeshPacket MeshtasticBleService::buildPacketFromData(const chat::MeshIncomingData& msg) const +{ + meshtastic_MeshPacket packet = meshtastic_MeshPacket_init_zero; + packet.from = msg.from; + packet.to = msg.to; + packet.channel = channelIndexFromId(msg.channel); + packet.id = (msg.packet_id == 0) ? static_cast(millis()) : msg.packet_id; + packet.rx_time = (msg.rx_meta.rx_timestamp_s != 0) ? msg.rx_meta.rx_timestamp_s : nowSeconds(); + packet.rx_snr = msg.rx_meta.snr_db_x10 / 10.0f; + packet.rx_rssi = msg.rx_meta.rssi_dbm_x10 / 10; + packet.hop_limit = msg.hop_limit; + packet.relay_node = msg.rx_meta.relay_node; + packet.which_payload_variant = meshtastic_MeshPacket_decoded_tag; + packet.decoded = meshtastic_Data_init_zero; + packet.decoded.portnum = static_cast(msg.portnum); + packet.decoded.source = msg.from; + packet.decoded.dest = msg.to; + packet.decoded.request_id = msg.request_id; + packet.decoded.want_response = msg.want_response; + packet.decoded.has_bitfield = true; + packet.decoded.bitfield = 0; + packet.decoded.payload.size = static_cast( + std::min(msg.payload.size(), sizeof(packet.decoded.payload.bytes))); + if (packet.decoded.payload.size > 0) + { + std::memcpy(packet.decoded.payload.bytes, msg.payload.data(), packet.decoded.payload.size); + } + return packet; +} + +} // namespace ble diff --git a/platform/nrf52/arduino_common/src/chat/infra/blob_file_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/blob_file_store.cpp new file mode 100644 index 00000000..cfa5efd9 --- /dev/null +++ b/platform/nrf52/arduino_common/src/chat/infra/blob_file_store.cpp @@ -0,0 +1,73 @@ +#include "platform/nrf52/arduino_common/chat/infra/blob_file_store.h" + +#include +#include + +namespace platform::nrf52::arduino_common::chat::infra +{ +namespace +{ + +std::map>& blobStore() +{ + static std::map> store; + return store; +} + +} // namespace + +BlobFileStore::BlobFileStore(const char* path) + : path_(path) +{ +} + +bool BlobFileStore::ensureFs() const +{ + return path_ != nullptr; +} + +bool BlobFileStore::loadBlob(std::vector& out) +{ + out.clear(); + if (!path_ || !ensureFs()) + { + return false; + } + + auto it = blobStore().find(path_); + if (it == blobStore().end() || it->second.empty()) + { + return false; + } + + out = it->second; + return true; +} + +bool BlobFileStore::saveBlob(const uint8_t* data, size_t len) +{ + if (!path_ || !ensureFs()) + { + return false; + } + + if (!data || len == 0) + { + clearBlob(); + return true; + } + + blobStore()[path_] = std::vector(data, data + len); + return true; +} + +void BlobFileStore::clearBlob() +{ + if (!path_ || !ensureFs()) + { + return; + } + blobStore().erase(path_); +} + +} // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/src/chat/infra/contact_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/contact_store.cpp new file mode 100644 index 00000000..fab28c22 --- /dev/null +++ b/platform/nrf52/arduino_common/src/chat/infra/contact_store.cpp @@ -0,0 +1,47 @@ +#include "platform/nrf52/arduino_common/chat/infra/contact_store.h" + +namespace platform::nrf52::arduino_common::chat::infra +{ + +ContactStore::ContactStore() + : blob_store_("/chat_contacts.bin"), + core_(blob_store_) +{ +} + +void ContactStore::begin() +{ + core_.begin(); +} + +std::string ContactStore::getNickname(uint32_t node_id) const +{ + return core_.getNickname(node_id); +} + +bool ContactStore::setNickname(uint32_t node_id, const char* nickname) +{ + return core_.setNickname(node_id, nickname); +} + +bool ContactStore::removeNickname(uint32_t node_id) +{ + return core_.removeNickname(node_id); +} + +bool ContactStore::hasNickname(const char* nickname) const +{ + return core_.hasNickname(nickname); +} + +std::vector ContactStore::getAllContactIds() const +{ + return core_.getAllContactIds(); +} + +size_t ContactStore::getCount() const +{ + return core_.getCount(); +} + +} // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_adapter_lite.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_adapter_lite.cpp new file mode 100644 index 00000000..5d74927c --- /dev/null +++ b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_adapter_lite.cpp @@ -0,0 +1,786 @@ +#include "platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_adapter_lite.h" + +#include "chat/infra/meshcore/meshcore_identity_crypto.h" +#include "chat/infra/meshcore/meshcore_payload_helpers.h" +#include "chat/infra/meshcore/meshcore_protocol_helpers.h" +#include "chat/runtime/meshcore_self_announcement_core.h" +#include "chat/runtime/self_identity_policy.h" +#include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h" +#include "platform/nrf52/arduino_common/device_identity.h" + +#include + +#include +#include +#include + +namespace platform::nrf52::arduino_common::chat::meshcore +{ +namespace +{ +constexpr uint8_t kRouteTypeFlood = 0x01; +constexpr uint8_t kRouteTypeDirect = 0x02; +constexpr uint8_t kPayloadTypeReq = 0x00; +constexpr uint8_t kPayloadTypeDirectData = 0x07; +constexpr uint8_t kPayloadTypeGrpData = 0x06; +constexpr uint8_t kPayloadTypeTrace = 0x09; +constexpr uint8_t kPayloadTypeControl = 0x0B; +constexpr uint8_t kPayloadTypeRawCustom = 0x0F; +constexpr uint8_t kDirectAppMagic0 = 0xDA; +constexpr uint8_t kDirectAppMagic1 = 0x7A; +constexpr uint8_t kDirectAppFlagWantAck = 0x01; +constexpr uint8_t kGroupDataMagic0 = 0x47; +constexpr uint8_t kGroupDataMagic1 = 0x44; +constexpr size_t kMeshcoreMaxFrameSize = 255; +constexpr size_t kMeshcoreMaxPayloadSize = 220; + +uint32_t estimateTimeoutMs(const ::chat::MeshConfig& cfg, size_t frame_len, size_t path_len, bool flood) +{ + return ::chat::meshcore::estimateSendTimeoutMs(frame_len, + path_len, + flood, + cfg.meshcore_bw_khz, + cfg.meshcore_sf, + cfg.meshcore_cr); +} + +bool isPrintableTextPayload(const uint8_t* data, size_t len) +{ + if (!data || len == 0) + { + return false; + } + for (size_t index = 0; index < len; ++index) + { + const uint8_t ch = data[index]; + if (ch == '\n' || ch == '\r' || ch == '\t') + { + continue; + } + if (ch < 0x20 || ch > 0x7EU) + { + return false; + } + } + return true; +} + +} // namespace + +MeshCoreAdapterLite::MeshCoreAdapterLite(const ::chat::runtime::SelfIdentityProvider* identity_provider) + : node_id_(device_identity::getSelfNodeId()), + identity_provider_(identity_provider) +{ +} + +::chat::MeshCapabilities MeshCoreAdapterLite::getCapabilities() const +{ + ::chat::MeshCapabilities caps{}; + caps.supports_unicast_appdata = true; + caps.supports_discovery_actions = true; + return caps; +} + +bool MeshCoreAdapterLite::sendText(::chat::ChannelId channel, const std::string& text, + ::chat::MessageId* out_msg_id, ::chat::NodeId peer) +{ + if (!out_msg_id) + { + static ::chat::MessageId sink = 0; + out_msg_id = &sink; + } + *out_msg_id = millis(); + return sendAppData(channel, + 0x1001, + reinterpret_cast(text.data()), + text.size(), + peer, + false, + *out_msg_id, + false); +} + +bool MeshCoreAdapterLite::pollIncomingText(::chat::MeshIncomingText* out) +{ + if (!out || text_queue_.empty()) + { + return false; + } + *out = text_queue_.front(); + text_queue_.pop(); + return true; +} + +bool MeshCoreAdapterLite::sendAppData(::chat::ChannelId channel, uint32_t portnum, + const uint8_t* payload, size_t len, + ::chat::NodeId dest, bool want_ack, + ::chat::MessageId packet_id, + bool want_response) +{ + (void)channel; + (void)packet_id; + (void)want_response; + if (!payload || len == 0 || !config_.tx_enabled) + { + return false; + } + + uint8_t frame[255] = {}; + size_t frame_len = 0; + + if (dest != 0) + { + uint8_t plain[220] = {}; + size_t plain_len = 0; + plain[plain_len++] = kDirectAppMagic0; + plain[plain_len++] = kDirectAppMagic1; + plain[plain_len++] = want_ack ? kDirectAppFlagWantAck : 0x00; + std::memcpy(&plain[plain_len], &portnum, sizeof(portnum)); + plain_len += sizeof(portnum); + const size_t body_len = std::min(len, sizeof(plain) - plain_len); + std::memcpy(&plain[plain_len], payload, body_len); + plain_len += body_len; + + if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeFlood, + kPayloadTypeDirectData, + nullptr, + 0, + plain, + plain_len, + frame, + sizeof(frame), + &frame_len)) + { + return false; + } + return transmitFrame(frame, frame_len); + } + + uint8_t plain[220] = {}; + size_t plain_len = 0; + plain[plain_len++] = kGroupDataMagic0; + plain[plain_len++] = kGroupDataMagic1; + std::memcpy(&plain[plain_len], &node_id_, sizeof(node_id_)); + plain_len += sizeof(node_id_); + std::memcpy(&plain[plain_len], &portnum, sizeof(portnum)); + plain_len += sizeof(portnum); + const size_t body_len = std::min(len, sizeof(plain) - plain_len); + std::memcpy(&plain[plain_len], payload, body_len); + plain_len += body_len; + + if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeFlood, + kPayloadTypeGrpData, + nullptr, + 0, + plain, + plain_len, + frame, + sizeof(frame), + &frame_len)) + { + return false; + } + return transmitFrame(frame, frame_len); +} + +bool MeshCoreAdapterLite::pollIncomingData(::chat::MeshIncomingData* out) +{ + if (!out || data_queue_.empty()) + { + return false; + } + *out = data_queue_.front(); + data_queue_.pop(); + return true; +} + +bool MeshCoreAdapterLite::requestNodeInfo(::chat::NodeId dest, bool want_response) +{ + (void)dest; + (void)want_response; + return sendAdvert(true); +} + +bool MeshCoreAdapterLite::triggerDiscoveryAction(::chat::MeshDiscoveryAction action) +{ + switch (action) + { + case ::chat::MeshDiscoveryAction::SendIdLocal: + return sendAdvert(false); + case ::chat::MeshDiscoveryAction::SendIdBroadcast: + case ::chat::MeshDiscoveryAction::ScanLocal: + default: + return sendAdvert(true); + } +} + +void MeshCoreAdapterLite::applyConfig(const ::chat::MeshConfig& config) +{ + config_ = config; +} + +void MeshCoreAdapterLite::setUserInfo(const char* long_name, const char* short_name) +{ + long_name_ = long_name ? long_name : ""; + short_name_ = short_name ? short_name : ""; +} + +void MeshCoreAdapterLite::setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) +{ + (void)duty_cycle_enabled; + (void)util_percent; +} + +void MeshCoreAdapterLite::setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) +{ + (void)encrypt_mode; + (void)pki_enabled; +} + +bool MeshCoreAdapterLite::isReady() const +{ + return ::platform::nrf52::arduino_common::chat::infra::radioPacketIo() != nullptr; +} + +::chat::NodeId MeshCoreAdapterLite::getNodeId() const +{ + return node_id_; +} + +bool MeshCoreAdapterLite::pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len) +{ + (void)out_data; + (void)max_len; + out_len = 0; + return false; +} + +void MeshCoreAdapterLite::handleRawPacket(const uint8_t* data, size_t size) +{ + if (!data || size == 0) + { + return; + } + + ::chat::meshcore::ParsedPacket parsed{}; + if (!::chat::meshcore::parsePacket(data, size, &parsed)) + { + return; + } + + if (parsed.payload_type == 0x04) + { + ::chat::meshcore::DecodedAdvertAppData advert{}; + if (::chat::meshcore::decodeAdvertAppData(parsed.payload, parsed.payload_len, &advert) && + advert.valid && advert.has_name) + { + ::chat::MeshIncomingText incoming{}; + incoming.text = advert.name; + text_queue_.push(std::move(incoming)); + } + return; + } + + ::chat::meshcore::DecodedDirectAppPayload direct_payload{}; + if (::chat::meshcore::decodeDirectAppPayload(parsed.payload, parsed.payload_len, &direct_payload) && + direct_payload.payload && direct_payload.payload_len > 0) + { + ::chat::MeshIncomingData incoming{}; + incoming.from = node_id_; + incoming.to = 0; + incoming.portnum = direct_payload.portnum; + incoming.payload.assign(direct_payload.payload, + direct_payload.payload + direct_payload.payload_len); + data_queue_.push(incoming); + + if (direct_payload.portnum == 0x1001 && + isPrintableTextPayload(direct_payload.payload, direct_payload.payload_len)) + { + ::chat::MeshIncomingText text{}; + text.from = incoming.from; + text.to = incoming.to; + text.channel = ::chat::ChannelId::PRIMARY; + text.text.assign(reinterpret_cast(direct_payload.payload), + direct_payload.payload_len); + text_queue_.push(std::move(text)); + } + return; + } + + ::chat::meshcore::DecodedGroupAppPayload group_payload{}; + if (::chat::meshcore::decodeGroupAppPayload(parsed.payload, parsed.payload_len, &group_payload) && + group_payload.payload && group_payload.payload_len > 0) + { + ::chat::MeshIncomingData incoming{}; + incoming.from = group_payload.sender; + incoming.to = 0xFFFFFFFFUL; + incoming.portnum = group_payload.portnum; + incoming.payload.assign(group_payload.payload, + group_payload.payload + group_payload.payload_len); + data_queue_.push(incoming); + + if (group_payload.portnum == 0x1001 && + isPrintableTextPayload(group_payload.payload, group_payload.payload_len)) + { + ::chat::MeshIncomingText text{}; + text.from = incoming.from; + text.to = incoming.to; + text.channel = ::chat::ChannelId::PRIMARY; + text.text.assign(reinterpret_cast(group_payload.payload), + group_payload.payload_len); + text_queue_.push(std::move(text)); + } + } +} + +void MeshCoreAdapterLite::setLastRxStats(float rssi, float snr) +{ + (void)rssi; + (void)snr; +} + +void MeshCoreAdapterLite::processSendQueue() +{ +} + +bool MeshCoreAdapterLite::exportIdentityPublicKey(uint8_t* out_key, size_t out_len) +{ + if (!out_key || out_len < sizeof(public_key_)) + { + return false; + } + ensureIdentityKeys(); + if (!keys_ready_) + { + return false; + } + std::memcpy(out_key, public_key_, sizeof(public_key_)); + return true; +} + +bool MeshCoreAdapterLite::exportIdentityPrivateKey(uint8_t* out_key, size_t out_len) +{ + if (!out_key || out_len < sizeof(private_key_)) + { + return false; + } + ensureIdentityKeys(); + if (!keys_ready_) + { + return false; + } + std::memcpy(out_key, private_key_, sizeof(private_key_)); + return true; +} + +bool MeshCoreAdapterLite::importIdentityPrivateKey(const uint8_t* key, size_t len) +{ + if (!key || len < sizeof(private_key_)) + { + return false; + } + std::memcpy(private_key_, key, sizeof(private_key_)); + keys_ready_ = ::chat::meshcore::meshcoreDerivePublicKey(private_key_, public_key_); + return keys_ready_; +} + +bool MeshCoreAdapterLite::signPayload(const uint8_t* payload, size_t len, uint8_t* out_signature, size_t out_len) +{ + if (!payload || len == 0 || !out_signature || out_len < ::chat::meshcore::kMeshCoreSignatureSize) + { + return false; + } + ensureIdentityKeys(); + if (!keys_ready_) + { + return false; + } + return ::chat::meshcore::meshcoreSign(private_key_, public_key_, payload, len, out_signature); +} + +bool MeshCoreAdapterLite::sendSelfAdvert(bool broadcast) +{ + return sendAdvert(broadcast); +} + +bool MeshCoreAdapterLite::sendPeerRequestType(const uint8_t* pubkey, size_t len, uint8_t req_type, + uint32_t* out_tag, uint32_t* out_est_timeout, + bool* out_sent_flood) +{ + uint8_t payload[9] = {}; + payload[0] = req_type; + const uint32_t nonce = static_cast(millis()); + std::memcpy(payload + 5, &nonce, sizeof(nonce)); + return sendPeerRequestPayload(pubkey, + len, + payload, + sizeof(payload), + false, + out_tag, + out_est_timeout, + out_sent_flood); +} + +bool MeshCoreAdapterLite::sendPeerRequestPayload(const uint8_t* pubkey, size_t len, + const uint8_t* payload, size_t payload_len, + bool force_flood, + uint32_t* out_tag, uint32_t* out_est_timeout, + bool* out_sent_flood) +{ + if (!pubkey || len != sizeof(public_key_) || !payload || payload_len == 0) + { + return false; + } + + ensureIdentityKeys(); + if (!keys_ready_) + { + return false; + } + + uint8_t shared_secret[::chat::meshcore::kMeshCorePubKeySize] = {}; + if (!::chat::meshcore::meshcoreDeriveSharedSecret(private_key_, pubkey, shared_secret)) + { + return false; + } + + uint8_t key16[16] = {}; + uint8_t key32[32] = {}; + ::chat::meshcore::sharedSecretToKeys(shared_secret, key16, key32); + + uint8_t plain[kMeshcoreMaxPayloadSize] = {}; + size_t plain_len = 0; + const uint32_t tag = static_cast(millis()); + std::memcpy(plain + plain_len, &tag, sizeof(tag)); + plain_len += sizeof(tag); + if (plain_len + payload_len > sizeof(plain)) + { + return false; + } + std::memcpy(plain + plain_len, payload, payload_len); + plain_len += payload_len; + + uint8_t datagram[kMeshcoreMaxPayloadSize] = {}; + size_t datagram_len = 0; + if (!::chat::meshcore::buildPeerDatagramPayload(pubkey[0], + public_key_[0], + key16, + key32, + plain, + plain_len, + datagram, + sizeof(datagram), + &datagram_len)) + { + return false; + } + + uint8_t frame[kMeshcoreMaxFrameSize] = {}; + size_t frame_len = 0; + const uint8_t route_type = force_flood ? kRouteTypeFlood : kRouteTypeFlood; + if (!::chat::meshcore::buildFrameNoTransport(route_type, + kPayloadTypeReq, + nullptr, + 0, + datagram, + datagram_len, + frame, + sizeof(frame), + &frame_len)) + { + return false; + } + + if (!transmitFrame(frame, frame_len)) + { + return false; + } + + if (out_tag) + { + *out_tag = tag; + } + if (out_est_timeout) + { + *out_est_timeout = estimateTimeoutMs(config_, frame_len, 0, true); + } + if (out_sent_flood) + { + *out_sent_flood = true; + } + return true; +} + +bool MeshCoreAdapterLite::sendAnonRequestPayload(const uint8_t* pubkey, size_t len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout, + bool* out_sent_flood) +{ + if (!pubkey || len != sizeof(public_key_) || !payload || payload_len == 0) + { + return false; + } + + ensureIdentityKeys(); + if (!keys_ready_) + { + return false; + } + + uint8_t shared_secret[::chat::meshcore::kMeshCorePubKeySize] = {}; + if (!::chat::meshcore::meshcoreDeriveSharedSecret(private_key_, pubkey, shared_secret)) + { + return false; + } + + uint8_t key16[16] = {}; + uint8_t key32[32] = {}; + ::chat::meshcore::sharedSecretToKeys(shared_secret, key16, key32); + + uint8_t cipher[kMeshcoreMaxPayloadSize] = {}; + const size_t cipher_len = ::chat::meshcore::encryptThenMac(key16, + key32, + cipher, + sizeof(cipher), + payload, + payload_len); + if (cipher_len == 0) + { + return false; + } + + uint8_t datagram[kMeshcoreMaxPayloadSize] = {}; + size_t datagram_len = 0; + datagram[datagram_len++] = pubkey[0]; + std::memcpy(datagram + datagram_len, public_key_, sizeof(public_key_)); + datagram_len += sizeof(public_key_); + if (datagram_len + cipher_len > sizeof(datagram)) + { + return false; + } + std::memcpy(datagram + datagram_len, cipher, cipher_len); + datagram_len += cipher_len; + + uint8_t frame[kMeshcoreMaxFrameSize] = {}; + size_t frame_len = 0; + if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeFlood, + kPayloadTypeDirectData, + nullptr, + 0, + datagram, + datagram_len, + frame, + sizeof(frame), + &frame_len)) + { + return false; + } + + if (!transmitFrame(frame, frame_len)) + { + return false; + } + + if (out_est_timeout) + { + *out_est_timeout = estimateTimeoutMs(config_, frame_len, 0, true); + } + if (out_sent_flood) + { + *out_sent_flood = true; + } + return true; +} + +bool MeshCoreAdapterLite::sendTracePath(const uint8_t* path, size_t path_len, + uint32_t tag, uint32_t auth, uint8_t flags, + uint32_t* out_est_timeout) +{ + if (!path || path_len == 0 || path_len > 64) + { + return false; + } + + uint8_t payload[9] = {}; + std::memcpy(payload, &tag, sizeof(tag)); + std::memcpy(payload + 4, &auth, sizeof(auth)); + payload[8] = flags; + + uint8_t frame[kMeshcoreMaxFrameSize] = {}; + size_t frame_len = 0; + if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeDirect, + kPayloadTypeTrace, + path, + path_len, + payload, + sizeof(payload), + frame, + sizeof(frame), + &frame_len)) + { + return false; + } + + if (!transmitFrame(frame, frame_len)) + { + return false; + } + if (out_est_timeout) + { + *out_est_timeout = estimateTimeoutMs(config_, frame_len, path_len, false); + } + return true; +} + +bool MeshCoreAdapterLite::sendControlData(const uint8_t* payload, size_t payload_len) +{ + if (!payload || payload_len == 0 || payload_len > kMeshcoreMaxPayloadSize) + { + return false; + } + + uint8_t frame[kMeshcoreMaxFrameSize] = {}; + size_t frame_len = 0; + if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeDirect, + kPayloadTypeControl, + nullptr, + 0, + payload, + payload_len, + frame, + sizeof(frame), + &frame_len)) + { + return false; + } + return transmitFrame(frame, frame_len); +} + +bool MeshCoreAdapterLite::sendRawData(const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout) +{ + if (!payload || payload_len == 0 || path_len > 64 || (path_len > 0 && !path)) + { + return false; + } + + uint8_t frame[kMeshcoreMaxFrameSize] = {}; + size_t frame_len = 0; + if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeDirect, + kPayloadTypeRawCustom, + path, + path_len, + payload, + payload_len, + frame, + sizeof(frame), + &frame_len)) + { + return false; + } + if (!transmitFrame(frame, frame_len)) + { + return false; + } + if (out_est_timeout) + { + *out_est_timeout = estimateTimeoutMs(config_, frame_len, path_len, false); + } + return true; +} + +void MeshCoreAdapterLite::setFloodScopeKey(const uint8_t* key, size_t len) +{ + flood_scope_key_.fill(0); + if (!key || len == 0) + { + return; + } + std::memcpy(flood_scope_key_.data(), key, std::min(len, flood_scope_key_.size())); +} + +::chat::runtime::EffectiveSelfIdentity MeshCoreAdapterLite::buildEffectiveIdentity() const +{ + ::chat::runtime::EffectiveSelfIdentity identity{}; + + if (identity_provider_) + { + ::chat::runtime::SelfIdentityInput input{}; + if (identity_provider_->readSelfIdentityInput(&input)) + { + if (!long_name_.empty()) + { + input.configured_long_name = long_name_.c_str(); + } + if (!short_name_.empty()) + { + input.configured_short_name = short_name_.c_str(); + } + (void)::chat::runtime::resolveEffectiveSelfIdentity(input, &identity); + return identity; + } + } + + ::chat::runtime::SelfIdentityInput input{}; + input.node_id = node_id_; + input.configured_long_name = long_name_.c_str(); + input.configured_short_name = short_name_.c_str(); + input.fallback_long_prefix = "node"; + input.fallback_ble_prefix = "node"; + input.allow_short_hex_fallback = true; + (void)::chat::runtime::resolveEffectiveSelfIdentity(input, &identity); + return identity; +} + +void MeshCoreAdapterLite::ensureIdentityKeys() +{ + if (keys_ready_) + { + return; + } + + uint8_t seed[::chat::meshcore::kMeshCoreSeedSize] = {}; + const auto mac = device_identity::getSelfMacAddress(); + for (size_t i = 0; i < sizeof(seed); ++i) + { + seed[i] = static_cast(mac[i % mac.size()] ^ + ((node_id_ >> ((i & 0x3U) * 8U)) & 0xFFU) ^ + i); + } + + keys_ready_ = ::chat::meshcore::meshcoreCreateKeypair(seed, public_key_, private_key_); +} + +bool MeshCoreAdapterLite::transmitFrame(const uint8_t* data, size_t size) +{ + auto* io = ::platform::nrf52::arduino_common::chat::infra::radioPacketIo(); + return io && io->transmit(data, size); +} + +bool MeshCoreAdapterLite::sendAdvert(bool broadcast) +{ + ensureIdentityKeys(); + if (!keys_ready_) + { + return false; + } + + ::chat::runtime::MeshCoreAnnouncementRequest request{}; + request.identity = buildEffectiveIdentity(); + request.mesh_config = config_; + request.broadcast = broadcast; + request.include_location = false; + request.timestamp_s = millis() / 1000U; + request.client_repeat = config_.meshcore_client_repeat; + request.public_key = public_key_; + request.public_key_len = sizeof(public_key_); + request.private_key = private_key_; + request.private_key_len = sizeof(private_key_); + + ::chat::runtime::MeshCoreAnnouncementPacket packet{}; + return ::chat::runtime::MeshCoreSelfAnnouncementCore::buildAdvertPacket(request, &packet) && + transmitFrame(packet.frame, packet.frame_size); +} + +} // namespace platform::nrf52::arduino_common::chat::meshcore diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/mt_adapter_lite.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/mt_adapter_lite.cpp new file mode 100644 index 00000000..dd4e6fa1 --- /dev/null +++ b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/mt_adapter_lite.cpp @@ -0,0 +1,430 @@ +#include "platform/nrf52/arduino_common/chat/infra/meshtastic/mt_adapter_lite.h" + +#include "chat/infra/meshtastic/mt_codec_pb.h" +#include "chat/infra/meshtastic/mt_packet_wire.h" +#include "chat/infra/meshtastic/mt_protocol_helpers.h" +#include "chat/runtime/meshtastic_self_announcement_core.h" +#include "chat/runtime/self_identity_policy.h" +#include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h" +#include "platform/nrf52/arduino_common/device_identity.h" + +#include + +#include +#include + +namespace platform::nrf52::arduino_common::chat::meshtastic +{ +namespace +{ + +std::array readMac() +{ + return device_identity::deriveMacAddressFromDeviceAddress(NRF_FICR->DEVICEADDR[0], + NRF_FICR->DEVICEADDR[1]); +} + +const uint8_t* selectKey(const ::chat::MeshConfig& config, + ::chat::ChannelId channel, + size_t* out_len) +{ + if (out_len) + { + *out_len = 0; + } + + if (channel == ::chat::ChannelId::SECONDARY) + { + if (config.secondary_key[0] != 0) + { + if (out_len) + { + *out_len = sizeof(config.secondary_key); + } + return config.secondary_key; + } + return nullptr; + } + + if (config.primary_key[0] != 0) + { + if (out_len) + { + *out_len = sizeof(config.primary_key); + } + return config.primary_key; + } + return nullptr; +} + +const uint8_t* selectKeyByHash(const ::chat::MeshConfig& config, + uint8_t channel_hash, + size_t* out_len, + ::chat::ChannelId* out_channel) +{ + if (out_len) + { + *out_len = 0; + } + if (out_channel) + { + *out_channel = ::chat::ChannelId::PRIMARY; + } + + size_t key_len = 0; + const uint8_t* key = selectKey(config, ::chat::ChannelId::PRIMARY, &key_len); + if (::chat::meshtastic::computeChannelHash("Primary", key, key_len) == channel_hash) + { + if (out_len) + { + *out_len = key_len; + } + return key; + } + + key = selectKey(config, ::chat::ChannelId::SECONDARY, &key_len); + if (::chat::meshtastic::computeChannelHash("Secondary", key, key_len) == channel_hash) + { + if (out_len) + { + *out_len = key_len; + } + if (out_channel) + { + *out_channel = ::chat::ChannelId::SECONDARY; + } + return key; + } + + if (::chat::meshtastic::computeChannelHash("Primary", nullptr, 0) == channel_hash) + { + return nullptr; + } + return nullptr; +} + +} // namespace + +MtAdapterLite::MtAdapterLite(const ::chat::runtime::SelfIdentityProvider* identity_provider) + : node_id_(device_identity::getSelfNodeId()), + identity_provider_(identity_provider) +{ +} + +::chat::MeshCapabilities MtAdapterLite::getCapabilities() const +{ + ::chat::MeshCapabilities caps{}; + caps.supports_unicast_text = true; + caps.supports_unicast_appdata = true; + caps.supports_node_info = true; + return caps; +} + +bool MtAdapterLite::sendText(::chat::ChannelId channel, const std::string& text, + ::chat::MessageId* out_msg_id, ::chat::NodeId peer) +{ + if (text.empty() || !config_.tx_enabled) + { + return false; + } + + uint8_t payload[256] = {}; + size_t payload_size = sizeof(payload); + const ::chat::MessageId packet_id = next_packet_id_++; + if (!::chat::meshtastic::encodeTextMessage(channel, + text, + node_id_, + packet_id, + peer == 0 ? 0xFFFFFFFFUL : peer, + payload, + &payload_size)) + { + return false; + } + + size_t key_len = 0; + const uint8_t* key = selectKey(config_, channel, &key_len); + const uint8_t channel_hash = ::chat::meshtastic::computeChannelHash( + channel == ::chat::ChannelId::SECONDARY ? "Secondary" : "Primary", + key, + key_len); + + uint8_t wire[384] = {}; + size_t wire_size = sizeof(wire); + if (!::chat::meshtastic::buildWirePacket(payload, + payload_size, + node_id_, + packet_id, + peer == 0 ? 0xFFFFFFFFUL : peer, + channel_hash, + config_.hop_limit, + false, + key, + key_len, + wire, + &wire_size)) + { + return false; + } + + if (!transmitWire(wire, wire_size)) + { + return false; + } + + if (out_msg_id) + { + *out_msg_id = packet_id; + } + return true; +} + +bool MtAdapterLite::pollIncomingText(::chat::MeshIncomingText* out) +{ + if (!out || text_queue_.empty()) + { + return false; + } + *out = text_queue_.front(); + text_queue_.pop(); + return true; +} + +bool MtAdapterLite::sendAppData(::chat::ChannelId channel, uint32_t portnum, + const uint8_t* payload, size_t len, + ::chat::NodeId dest, bool want_ack, + ::chat::MessageId packet_id, + bool want_response) +{ + (void)want_ack; + if (!payload || len == 0 || !config_.tx_enabled) + { + return false; + } + + uint8_t data_pb[256] = {}; + size_t data_pb_size = sizeof(data_pb); + if (!::chat::meshtastic::encodeAppData(portnum, payload, len, want_response, data_pb, &data_pb_size)) + { + return false; + } + + if (packet_id == 0) + { + packet_id = next_packet_id_++; + } + + size_t key_len = 0; + const uint8_t* key = selectKey(config_, channel, &key_len); + const uint8_t channel_hash = ::chat::meshtastic::computeChannelHash( + channel == ::chat::ChannelId::SECONDARY ? "Secondary" : "Primary", + key, + key_len); + + uint8_t wire[384] = {}; + size_t wire_size = sizeof(wire); + return ::chat::meshtastic::buildWirePacket(data_pb, + data_pb_size, + node_id_, + packet_id, + dest == 0 ? 0xFFFFFFFFUL : dest, + channel_hash, + config_.hop_limit, + false, + key, + key_len, + wire, + &wire_size) && + transmitWire(wire, wire_size); +} + +bool MtAdapterLite::pollIncomingData(::chat::MeshIncomingData* out) +{ + if (!out || data_queue_.empty()) + { + return false; + } + *out = data_queue_.front(); + data_queue_.pop(); + return true; +} + +bool MtAdapterLite::requestNodeInfo(::chat::NodeId dest, bool want_response) +{ + return buildAndQueueNodeInfo(dest == 0 ? 0xFFFFFFFFUL : dest, want_response); +} + +void MtAdapterLite::applyConfig(const ::chat::MeshConfig& config) +{ + config_ = config; +} + +void MtAdapterLite::setUserInfo(const char* long_name, const char* short_name) +{ + long_name_ = long_name ? long_name : ""; + short_name_ = short_name ? short_name : ""; +} + +void MtAdapterLite::setNetworkLimits(bool duty_cycle_enabled, uint8_t util_percent) +{ + (void)duty_cycle_enabled; + (void)util_percent; +} + +void MtAdapterLite::setPrivacyConfig(uint8_t encrypt_mode, bool pki_enabled) +{ + (void)encrypt_mode; + (void)pki_enabled; +} + +bool MtAdapterLite::isReady() const +{ + return ::platform::nrf52::arduino_common::chat::infra::radioPacketIo() != nullptr; +} + +::chat::NodeId MtAdapterLite::getNodeId() const +{ + return node_id_; +} + +bool MtAdapterLite::pollIncomingRawPacket(uint8_t* out_data, size_t& out_len, size_t max_len) +{ + (void)out_data; + (void)max_len; + out_len = 0; + return false; +} + +void MtAdapterLite::handleRawPacket(const uint8_t* data, size_t size) +{ + if (!data || size == 0) + { + return; + } + + ::chat::meshtastic::PacketHeaderWire header{}; + uint8_t payload[256] = {}; + size_t payload_size = sizeof(payload); + if (!::chat::meshtastic::parseWirePacket(data, size, &header, payload, &payload_size)) + { + return; + } + + uint8_t plain[256] = {}; + size_t plain_len = sizeof(plain); + ::chat::ChannelId channel = ::chat::ChannelId::PRIMARY; + size_t key_len = 0; + const uint8_t* key = selectKeyByHash(config_, header.channel, &key_len, &channel); + if (!::chat::meshtastic::decryptPayload(header, payload, payload_size, + key, key_len, plain, &plain_len)) + { + return; + } + + ::chat::MeshIncomingText incoming{}; + if (::chat::meshtastic::decodeTextMessage(plain, plain_len, &incoming)) + { + incoming.from = header.from; + incoming.to = header.to; + incoming.msg_id = header.id; + incoming.channel = channel; + incoming.encrypted = key_len > 0; + incoming.rx_meta.rssi_dbm_x10 = static_cast(last_rx_rssi_ * 10.0f); + incoming.rx_meta.snr_db_x10 = static_cast(last_rx_snr_ * 10.0f); + incoming.rx_meta.channel_hash = header.channel; + text_queue_.push(std::move(incoming)); + return; + } + + ::chat::MeshIncomingData app_data{}; + if (::chat::meshtastic::decodeAppData(plain, plain_len, &app_data)) + { + app_data.from = header.from; + app_data.to = header.to; + app_data.packet_id = header.id; + app_data.channel = channel; + app_data.channel_hash = header.channel; + app_data.rx_meta.rssi_dbm_x10 = static_cast(last_rx_rssi_ * 10.0f); + app_data.rx_meta.snr_db_x10 = static_cast(last_rx_snr_ * 10.0f); + app_data.rx_meta.channel_hash = header.channel; + data_queue_.push(std::move(app_data)); + } +} + +void MtAdapterLite::setLastRxStats(float rssi, float snr) +{ + last_rx_rssi_ = rssi; + last_rx_snr_ = snr; +} + +void MtAdapterLite::processSendQueue() +{ +} + +::chat::runtime::EffectiveSelfIdentity MtAdapterLite::buildEffectiveIdentity() const +{ + ::chat::runtime::EffectiveSelfIdentity identity{}; + + if (identity_provider_) + { + ::chat::runtime::SelfIdentityInput input{}; + if (identity_provider_->readSelfIdentityInput(&input)) + { + if (!long_name_.empty()) + { + input.configured_long_name = long_name_.c_str(); + } + if (!short_name_.empty()) + { + input.configured_short_name = short_name_.c_str(); + } + (void)::chat::runtime::resolveEffectiveSelfIdentity(input, &identity); + return identity; + } + } + + ::chat::runtime::SelfIdentityInput input{}; + input.node_id = node_id_; + input.configured_long_name = long_name_.c_str(); + input.configured_short_name = short_name_.c_str(); + input.fallback_long_prefix = "node"; + input.fallback_ble_prefix = "node"; + input.allow_short_hex_fallback = true; + (void)::chat::runtime::resolveEffectiveSelfIdentity(input, &identity); + return identity; +} + +bool MtAdapterLite::transmitWire(const uint8_t* data, size_t size) +{ + auto* io = ::platform::nrf52::arduino_common::chat::infra::radioPacketIo(); + return io && io->transmit(data, size); +} + +bool MtAdapterLite::buildAndQueueNodeInfo(::chat::NodeId dest, bool want_response) +{ + auto mac = readMac(); + if (identity_provider_) + { + ::chat::runtime::SelfIdentityInput input{}; + if (identity_provider_->readSelfIdentityInput(&input) && input.mac_addr && input.mac_addr_len >= mac.size()) + { + std::memcpy(mac.data(), input.mac_addr, mac.size()); + } + } + + ::chat::runtime::MeshtasticAnnouncementRequest request{}; + request.identity = buildEffectiveIdentity(); + request.mesh_config = config_; + request.channel = ::chat::ChannelId::PRIMARY; + request.packet_id = next_packet_id_++; + request.dest_node = dest; + request.hop_limit = config_.hop_limit; + request.want_response = want_response; + request.mac_addr = mac.data(); + + ::chat::runtime::MeshtasticAnnouncementPacket packet{}; + return ::chat::runtime::MeshtasticSelfAnnouncementCore::buildNodeInfoPacket(request, &packet) && + transmitWire(packet.wire, packet.wire_size); +} + +} // namespace platform::nrf52::arduino_common::chat::meshtastic diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshtastic/node_store.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/node_store.cpp new file mode 100644 index 00000000..bc1629a1 --- /dev/null +++ b/platform/nrf52/arduino_common/src/chat/infra/meshtastic/node_store.cpp @@ -0,0 +1,45 @@ +#include "platform/nrf52/arduino_common/chat/infra/meshtastic/node_store.h" + +namespace platform::nrf52::arduino_common::chat::meshtastic +{ + +NodeStore::NodeStore() + : blob_store_("/chat_nodes.bin"), + core_(blob_store_) +{ +} + +void NodeStore::begin() +{ + core_.begin(); +} + +void NodeStore::upsert(uint32_t node_id, const char* short_name, const char* long_name, + uint32_t now_secs, float snr, float rssi, uint8_t protocol, + uint8_t role, uint8_t hops_away, uint8_t hw_model, uint8_t channel) +{ + core_.upsert(node_id, short_name, long_name, now_secs, snr, rssi, + protocol, role, hops_away, hw_model, channel); +} + +void NodeStore::updateProtocol(uint32_t node_id, uint8_t protocol, uint32_t now_secs) +{ + core_.updateProtocol(node_id, protocol, now_secs); +} + +bool NodeStore::remove(uint32_t node_id) +{ + return core_.remove(node_id); +} + +const std::vector<::chat::contacts::NodeEntry>& NodeStore::getEntries() const +{ + return core_.getEntries(); +} + +void NodeStore::clear() +{ + core_.clear(); +} + +} // namespace platform::nrf52::arduino_common::chat::meshtastic diff --git a/platform/nrf52/arduino_common/src/chat/infra/radio_packet_io.cpp b/platform/nrf52/arduino_common/src/chat/infra/radio_packet_io.cpp new file mode 100644 index 00000000..9e39e1b8 --- /dev/null +++ b/platform/nrf52/arduino_common/src/chat/infra/radio_packet_io.cpp @@ -0,0 +1,20 @@ +#include "platform/nrf52/arduino_common/chat/infra/radio_packet_io.h" + +namespace platform::nrf52::arduino_common::chat::infra +{ +namespace +{ +IRadioPacketIo* g_radio_packet_io = nullptr; +} + +void bindRadioPacketIo(IRadioPacketIo* io) +{ + g_radio_packet_io = io; +} + +IRadioPacketIo* radioPacketIo() +{ + return g_radio_packet_io; +} + +} // namespace platform::nrf52::arduino_common::chat::infra diff --git a/platform/nrf52/arduino_common/src/device_identity.cpp b/platform/nrf52/arduino_common/src/device_identity.cpp new file mode 100644 index 00000000..ff133479 --- /dev/null +++ b/platform/nrf52/arduino_common/src/device_identity.cpp @@ -0,0 +1,42 @@ +#include "platform/nrf52/arduino_common/device_identity.h" + +#include + +namespace platform::nrf52::arduino_common::device_identity +{ + +::chat::NodeId deriveNodeIdFromDeviceAddress(uint32_t deviceaddr0, uint32_t deviceaddr1) +{ + ::chat::NodeId node_id = (static_cast<::chat::NodeId>((deviceaddr0 >> 16) & 0xFFU) << 24) | + (static_cast<::chat::NodeId>((deviceaddr0 >> 24) & 0xFFU) << 16) | + (static_cast<::chat::NodeId>(deviceaddr1 & 0xFFU) << 8) | + static_cast<::chat::NodeId>((deviceaddr1 >> 8) & 0xFFU); + if (node_id == 0) + { + node_id = 1; + } + return node_id; +} + +std::array deriveMacAddressFromDeviceAddress(uint32_t deviceaddr0, uint32_t deviceaddr1) +{ + return { + static_cast(deviceaddr0 & 0xFFU), + static_cast((deviceaddr0 >> 8) & 0xFFU), + static_cast((deviceaddr0 >> 16) & 0xFFU), + static_cast((deviceaddr0 >> 24) & 0xFFU), + static_cast(deviceaddr1 & 0xFFU), + static_cast((deviceaddr1 >> 8) & 0xFFU)}; +} + +::chat::NodeId getSelfNodeId() +{ + return deriveNodeIdFromDeviceAddress(NRF_FICR->DEVICEADDR[0], NRF_FICR->DEVICEADDR[1]); +} + +std::array getSelfMacAddress() +{ + return deriveMacAddressFromDeviceAddress(NRF_FICR->DEVICEADDR[0], NRF_FICR->DEVICEADDR[1]); +} + +} // namespace platform::nrf52::arduino_common::device_identity diff --git a/platform/nrf52/arduino_common/src/platform_ui_settings_store.cpp b/platform/nrf52/arduino_common/src/platform_ui_settings_store.cpp new file mode 100644 index 00000000..630094b6 --- /dev/null +++ b/platform/nrf52/arduino_common/src/platform_ui_settings_store.cpp @@ -0,0 +1,173 @@ +#include "platform/ui/settings_store.h" + +#include +#include +#include +#include + +namespace +{ + +std::string makeScopedKey(const char* ns, const char* key) +{ + const char* scope = ns ? ns : ""; + const char* name = key ? key : ""; + return std::string(scope) + ":" + name; +} + +std::map& intStore() +{ + static std::map store; + return store; +} + +std::map& boolStore() +{ + static std::map store; + return store; +} + +std::map& uintStore() +{ + static std::map store; + return store; +} + +std::map>& blobStore() +{ + static std::map> store; + return store; +} + +} // namespace + +namespace platform::ui::settings_store +{ + +void put_int(const char* ns, const char* key, int value) +{ + if (!key) + { + return; + } + intStore()[makeScopedKey(ns, key)] = value; +} + +void put_bool(const char* ns, const char* key, bool value) +{ + if (!key) + { + return; + } + boolStore()[makeScopedKey(ns, key)] = value; +} + +void put_uint(const char* ns, const char* key, uint32_t value) +{ + if (!key) + { + return; + } + uintStore()[makeScopedKey(ns, key)] = value; +} + +bool put_blob(const char* ns, const char* key, const void* data, std::size_t len) +{ + if (!key || (!data && len != 0)) + { + return false; + } + auto& blob = blobStore()[makeScopedKey(ns, key)]; + blob.assign(static_cast(data), static_cast(data) + len); + return true; +} + +int get_int(const char* ns, const char* key, int default_value) +{ + if (!key) + { + return default_value; + } + const auto it = intStore().find(makeScopedKey(ns, key)); + return it == intStore().end() ? default_value : it->second; +} + +bool get_bool(const char* ns, const char* key, bool default_value) +{ + if (!key) + { + return default_value; + } + const auto it = boolStore().find(makeScopedKey(ns, key)); + return it == boolStore().end() ? default_value : it->second; +} + +uint32_t get_uint(const char* ns, const char* key, uint32_t default_value) +{ + if (!key) + { + return default_value; + } + const auto it = uintStore().find(makeScopedKey(ns, key)); + return it == uintStore().end() ? default_value : it->second; +} + +bool get_blob(const char* ns, const char* key, std::vector& out) +{ + out.clear(); + if (!key) + { + return false; + } + const auto it = blobStore().find(makeScopedKey(ns, key)); + if (it == blobStore().end()) + { + return false; + } + out = it->second; + return true; +} + +void remove_keys(const char* ns, const char* const* keys, std::size_t key_count) +{ + if (!keys) + { + return; + } + for (std::size_t index = 0; index < key_count; ++index) + { + if (!keys[index]) + { + continue; + } + const std::string scoped = makeScopedKey(ns, keys[index]); + intStore().erase(scoped); + boolStore().erase(scoped); + uintStore().erase(scoped); + blobStore().erase(scoped); + } +} + +void clear_namespace(const char* ns) +{ + const std::string prefix = std::string(ns ? ns : "") + ":"; + + for (auto it = intStore().begin(); it != intStore().end();) + { + it = (it->first.rfind(prefix, 0) == 0) ? intStore().erase(it) : std::next(it); + } + for (auto it = boolStore().begin(); it != boolStore().end();) + { + it = (it->first.rfind(prefix, 0) == 0) ? boolStore().erase(it) : std::next(it); + } + for (auto it = uintStore().begin(); it != uintStore().end();) + { + it = (it->first.rfind(prefix, 0) == 0) ? uintStore().erase(it) : std::next(it); + } + for (auto it = blobStore().begin(); it != blobStore().end();) + { + it = (it->first.rfind(prefix, 0) == 0) ? blobStore().erase(it) : std::next(it); + } +} + +} // namespace platform::ui::settings_store diff --git a/platform/nrf52/arduino_common/src/platform_ui_time_runtime.cpp b/platform/nrf52/arduino_common/src/platform_ui_time_runtime.cpp new file mode 100644 index 00000000..d9e1746c --- /dev/null +++ b/platform/nrf52/arduino_common/src/platform_ui_time_runtime.cpp @@ -0,0 +1,59 @@ +#include "platform/ui/time_runtime.h" +#include "platform/ui/settings_store.h" +#include "sys/clock.h" + +#include + +namespace platform::ui::time +{ +namespace +{ + +constexpr const char* kSettingsNs = "settings"; +constexpr const char* kTimezoneKey = "timezone_offset"; + +int& timezoneOffsetStorage() +{ + static int s_offset_min = ::platform::ui::settings_store::get_int(kSettingsNs, kTimezoneKey, 0); + return s_offset_min; +} + +} // namespace + +int timezone_offset_min() +{ + return timezoneOffsetStorage(); +} + +void set_timezone_offset_min(int offset_min) +{ + timezoneOffsetStorage() = offset_min; + ::platform::ui::settings_store::put_int(kSettingsNs, kTimezoneKey, offset_min); +} + +time_t apply_timezone_offset(time_t utc_seconds) +{ + if (utc_seconds <= 0) + { + return utc_seconds; + } + return utc_seconds + static_cast(timezone_offset_min()) * 60; +} + +bool localtime_now(struct tm* out_tm) +{ + if (!out_tm) + { + return false; + } + const time_t now = apply_timezone_offset(static_cast(sys::epoch_seconds_now())); + const tm* tmp = gmtime(&now); + if (!tmp) + { + return false; + } + *out_tm = *tmp; + return true; +} + +} // namespace platform::ui::time diff --git a/platform/nrf52/arduino_common/src/self_identity_bridge.cpp b/platform/nrf52/arduino_common/src/self_identity_bridge.cpp new file mode 100644 index 00000000..c964257c --- /dev/null +++ b/platform/nrf52/arduino_common/src/self_identity_bridge.cpp @@ -0,0 +1,40 @@ +#include "platform/nrf52/arduino_common/self_identity_bridge.h" + +#include "platform/nrf52/arduino_common/device_identity.h" + +namespace platform::nrf52::arduino_common +{ + +SelfIdentityBridge::SelfIdentityBridge(const app::AppConfig& config, + uint32_t deviceaddr0, + uint32_t deviceaddr1, + const char* fallback_long_prefix, + const char* fallback_ble_prefix) + : config_(config), + node_id_(device_identity::deriveNodeIdFromDeviceAddress(deviceaddr0, deviceaddr1)), + mac_addr_(device_identity::deriveMacAddressFromDeviceAddress(deviceaddr0, deviceaddr1)), + fallback_long_prefix_(fallback_long_prefix), + fallback_ble_prefix_(fallback_ble_prefix) +{ +} + +bool SelfIdentityBridge::readSelfIdentityInput(chat::runtime::SelfIdentityInput* out) const +{ + if (!out) + { + return false; + } + + *out = chat::runtime::SelfIdentityInput{}; + out->node_id = node_id_; + out->configured_long_name = config_.node_name; + out->configured_short_name = config_.short_name; + out->fallback_long_prefix = fallback_long_prefix_; + out->fallback_ble_prefix = fallback_ble_prefix_; + out->allow_short_hex_fallback = true; + out->mac_addr = mac_addr_.data(); + out->mac_addr_len = mac_addr_.size(); + return true; +} + +} // namespace platform::nrf52::arduino_common diff --git a/platformio.ini b/platformio.ini index 53c3a2c7..525ebf61 100644 --- a/platformio.ini +++ b/platformio.ini @@ -71,7 +71,11 @@ lib_extra_dirs = ${PROJECT_DIR}/apps ${PROJECT_DIR}/platform/esp ${PROJECT_DIR}/platform/esp/boards - ${PROJECT_DIR}/third_party + ${PROJECT_DIR}/third_party +lib_ignore = + gat562_mesh_evb_pro + boards_gat562_mesh_evb_pro + platform_nrf52_arduino_common lib_deps = lvgl/lvgl @ 9.4.0 jgromes/RadioLib @ 7.4.0 diff --git a/src/main.cpp b/src/main.cpp index 9ba495e2..a8ce2d92 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,13 +1,25 @@ #include +#if defined(GAT562_MESH_EVB_PRO) +#include "apps/gat562_mesh_evb_pro/arduino_entry.h" +#else #include "apps/esp_pio/arduino_entry.h" +#endif void setup() { +#if defined(GAT562_MESH_EVB_PRO) + apps::gat562_mesh_evb_pro::arduino_entry::setup(); +#else apps::esp_pio::arduino_entry::setup(); +#endif } void loop() { +#if defined(GAT562_MESH_EVB_PRO) + apps::gat562_mesh_evb_pro::arduino_entry::loop(); +#else apps::esp_pio::arduino_entry::loop(); +#endif } diff --git a/variants/gat562_mesh_evb_pro/envs/gat562_mesh_evb_pro.ini b/variants/gat562_mesh_evb_pro/envs/gat562_mesh_evb_pro.ini new file mode 100644 index 00000000..bc9c00d2 --- /dev/null +++ b/variants/gat562_mesh_evb_pro/envs/gat562_mesh_evb_pro.ini @@ -0,0 +1,67 @@ +[env:gat562_mesh_evb_pro] +extends = env +framework = arduino +platform = nordicnrf52 +board = gat562_mesh_evb_pro +monitor_speed = 115200 +build_flags = + -std=gnu++17 + -DARDUINO_NRF52840_FEATHER + -DNRF52840_XXAA + -DS140 + -DGAT562_MESH_EVB_PRO + -DGAT562_NO_TEAM=1 + -DGAT562_NO_HOSTLINK=1 + -DGAT562_NO_SD=1 + -DGAT562_NO_CJK=1 + -DGAT562_NO_PINYIN_IME=1 + -DUI_SHARED_TOUCH_IME_ENABLED=0 + -DSCREEN_WIDTH=128 + -DSCREEN_HEIGHT=64 + -I${PROJECT_DIR} + -I${PROJECT_DIR}/boards/gat562_mesh_evb_pro/include + -I${PROJECT_DIR}/platform/esp/boards/include + -I${PROJECT_DIR}/variants/gat562_mesh_evb_pro + -I${PROJECT_DIR}/modules/core_sys/include + -I${PROJECT_DIR}/modules/core_chat/include + -I${PROJECT_DIR}/modules/core_chat/generated + -I${PROJECT_DIR}/modules/core_chat/third_party/nanopb + -I${PROJECT_DIR}/modules/core_gps/include + -I${PROJECT_DIR}/modules/ui_mono_128x64/include + -I${PROJECT_DIR}/modules/ui_shared/include + -I${PROJECT_DIR}/platform/nrf52/arduino_common/include + -I${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries/Adafruit_TinyUSB_Arduino/src + -I${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries/SPI + -I${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries/Wire + -I${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries/Bluefruit52Lib/src + -I${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries/Bluefruit52Lib/src/services + -I${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries/Adafruit_nRFCrypto/src + -I${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries/Adafruit_LittleFS/src + -I${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries/InternalFileSytem/src +build_src_filter = + +<*> + - + - + - + - +lib_extra_dirs = + ${PROJECT_DIR}/modules + ${PROJECT_DIR}/apps + ${PROJECT_DIR}/boards + ${PROJECT_DIR}/platform/nrf52 + ${PROJECT_DIR}/third_party + ${platformio.packages_dir}/framework-arduinoadafruitnrf52/libraries +lib_deps = + jgromes/RadioLib @ 7.4.0 + mikalhart/TinyGPSPlus @ 1.0.3 + nanopb/nanopb @ ^0.4.8 + rweather/Crypto @ 0.4.0 + adafruit/Adafruit GFX Library @ ^1.12.0 + adafruit/Adafruit SSD1306 @ ^2.5.13 + Adafruit Bluefruit nRF52 Libraries + Adafruit TinyUSB Library + Adafruit nRFCrypto +lib_ignore = + apps_esp_pio + core_team + core_hostlink diff --git a/variants/gat562_mesh_evb_pro/variant.cpp b/variants/gat562_mesh_evb_pro/variant.cpp new file mode 100644 index 00000000..977c3132 --- /dev/null +++ b/variants/gat562_mesh_evb_pro/variant.cpp @@ -0,0 +1,18 @@ +#include "variant.h" + +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_3V3_EN, OUTPUT); + digitalWrite(PIN_3V3_EN, HIGH); +} diff --git a/variants/gat562_mesh_evb_pro/variant.h b/variants/gat562_mesh_evb_pro/variant.h new file mode 100644 index 00000000..1ed16fc8 --- /dev/null +++ b/variants/gat562_mesh_evb_pro/variant.h @@ -0,0 +1,118 @@ +#ifndef _VARIANT_GAT562_MESH_EVB_PRO_ +#define _VARIANT_GAT562_MESH_EVB_PRO_ + +#define RAK4630 +#define VARIANT_MCK (64000000ul) +#define USE_LFXO + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (6) +#define NUM_ANALOG_OUTPUTS (0) + +#define PIN_LED1 (35) +#define LED_BUILTIN PIN_LED1 +#define LED_BLUE (36) +#define LED_GREEN PIN_LED1 +#define LED_NOTIFICATION LED_BLUE +#define LED_STATE_ON 1 + +#define PIN_BUTTON1 (9) +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 (12) + +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; + +#define ADC_RESOLUTION 14 +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) +static const uint8_t AREF = PIN_AREF; + +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +#define SPI_INTERFACES_COUNT 2 +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) +#define PIN_SPI1_MISO (29) +#define PIN_SPI1_MOSI (30) +#define PIN_SPI1_SCK (3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +#define HAS_SCREEN 1 +#define USE_SSD1306 + +#define WIRE_INTERFACES_COUNT 1 +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +#define SX126X_POWER_EN (37) +#define SX126X_DIO2_AS_RF_SWITCH +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define NRF_APM +#define PIN_3V3_EN (34) + +#define PIN_GPS_PPS (17) +#define GPS_BAUDRATE 9600 +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +#define BATTERY_PIN PIN_A0 +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER 1.73 + +#ifdef __cplusplus +} +#endif + +#endif