mirror of
https://github.com/meshtastic/firmware.git
synced 2026-03-29 04:23:08 -04:00
* Add ESP32 Power Management lessons learned document Documents our experimentation with ESP-IDF DFS and why it doesn't work well for Meshtastic (RTOS locks, BLE locks, USB issues). Proposes simpler alternative: manual setCpuFrequencyMhz() control with explicit triggers for when to go fast vs slow. * Addition of traffic management module * Fixing compile issues, but may still need to update protobufs. * Fixing log2Floor in cuckoo hash function * Adding support for traffic management in PhoneAPI. * Making router_preserve_hops work without checking if the previous hop was a router. Also works for CLIENT_BASE. * Adding station-g2 and portduino varients to be able to use this module. * Spoofing from address for nodeinfo cache * Changing name and behavior for zero_hop_telemetry / zero_hop_position * Name change for exhausting telemetry packets and setting hop_limit to 1 so it will be 0 when sent. * Updated hop logic, including exhaustRequested flag to bypass some checks later in the code. * Reducing memory on nrf52 nodes further to 12 bytes per entry, 12KB total using 8 bit hashes with 0.4% collision. Probably ok. Adding portduino to the platforms that don't need to worry about memory as much. * Fixing hopsAway for nodeinfo responses. * traffic_management.nodeinfo_direct_response_min_hops -> traffic_management.nodeinfo_direct_response_max_hops * Removing dry run mode * Updates to UnifiedCacheEntry to use a common cache, created defaults for some values, reduced a couple bytes per entry by using a resolution-scale time selection based on configuration value. * Enhance traffic management logging and configuration. Updated log messages in NextHopRouter and Router to include more context. Adjusted traffic management configuration checks in AdminModule and improved cache handling in TrafficManagementModule. Ensured consistent enabling of traffic management across various variants. * Implement destructor for TrafficManagementModule and improve cache allocation handling. The destructor ensures proper deallocation of cache memory based on its allocation source (PSRAM or heap). Additionally, updated cache allocation logic to log warnings only when PSRAM allocation fails. * Update TrafficManagementModule with enhanced comments for clarity and improve cache handling logic. Update protobuf submodule to latest commit. * Creating consistent log messages * Remove docs/ESP32_Power_Management.md from traffic_module * Add unit tests for Traffic Management Module functionality * Fixing compile issues, but may still need to update protobufs. * Adding support for traffic management in PhoneAPI. * Making router_preserve_hops work without checking if the previous hop was a router. Also works for CLIENT_BASE. * Enhance traffic management logging and configuration. Updated log messages in NextHopRouter and Router to include more context. Adjusted traffic management configuration checks in AdminModule and improved cache handling in TrafficManagementModule. Ensured consistent enabling of traffic management across various variants. * Implement destructor for TrafficManagementModule and improve cache allocation handling. The destructor ensures proper deallocation of cache memory based on its allocation source (PSRAM or heap). Additionally, updated cache allocation logic to log warnings only when PSRAM allocation fails. * Update TrafficManagementModule with enhanced comments for clarity and improve cache handling logic. Update protobuf submodule to latest commit. * Add mock classes and unit tests for Traffic Management Module functionality. * Refactor setup and loop functions in test_main.cpp to include extern "C" linkage * Update comment to include reduced memory requirements Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Re-arranging comments for programmers with the attention span of less than 5 lines of code. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update comments in TrafficManagementModule to reflect changes in timestamp epoch handling and memory optimization details. * bug: Use node-wide config_ok_to_mqtt setting for cached NodeInfo replies. * Better way to handle clearing the ok_to_mqtt bit * Add bucketing to cuckoo hashing, allowing for 95% occupied rate before major eviction problems. * Extend nodeinfo cache for psram devices. * Refactor traffic management to make hop exhaustion packet-scoped. Nice catch. * Implement better position precision sanitization in TrafficManagementModule. * Added logic in TrafficManagementModule to invalidate stale traffic state. Also, added some tests to avoid future me from creating a regression here. * Fixing tests for native * Enhance TrafficManagementModule to improve NodeInfo response handling and position deduplication logic. Added tests to ensure local packets bypass transit filters and that NodeInfo requests correctly update the requester information in the cache. Updated deduplication checks to prevent dropping valid position packets under certain conditions. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1571 lines
68 KiB
C++
1571 lines
68 KiB
C++
#include "AdminModule.h"
|
|
#include "Channels.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "PowerFSM.h"
|
|
#include "RTC.h"
|
|
#include "SPILock.h"
|
|
#include "input/InputBroker.h"
|
|
#include "meshUtils.h"
|
|
#include <FSCommon.h>
|
|
#include <ctype.h> // for better whitespace handling
|
|
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
|
#include "MeshtasticOTA.h"
|
|
#endif
|
|
#include "Router.h"
|
|
#include "configuration.h"
|
|
#include "main.h"
|
|
#ifdef ARCH_NRF52
|
|
#include "main.h"
|
|
#endif
|
|
#ifdef ARCH_PORTDUINO
|
|
#include "unistd.h"
|
|
#endif
|
|
|
|
#include "Default.h"
|
|
#include "MeshRadio.h"
|
|
#include "TypeConversions.h"
|
|
|
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
|
#include "mqtt/MQTT.h"
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#endif
|
|
|
|
#if MESHTASTIC_EXCLUDE_GPS
|
|
#include "modules/PositionModule.h"
|
|
#endif
|
|
|
|
#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C
|
|
#include "motion/AccelerometerThread.h"
|
|
#endif
|
|
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
|
|
!defined(CONFIG_IDF_TARGET_ESP32C3)
|
|
#include "SerialModule.h"
|
|
#endif
|
|
|
|
AdminModule *adminModule;
|
|
bool hasOpenEditTransaction;
|
|
|
|
/// A special reserved string to indicate strings we can not share with external nodes. We will use this 'reserved' word instead.
|
|
/// Also, to make setting work correctly, if someone tries to set a string to this reserved value we assume they don't really want
|
|
/// a change.
|
|
static const char *secretReserved = "sekrit";
|
|
|
|
/// If buf is the reserved secret word, replace the buffer with currentVal
|
|
static void writeSecret(char *buf, size_t bufsz, const char *currentVal)
|
|
{
|
|
if (strcmp(buf, secretReserved) == 0) {
|
|
strncpy(buf, currentVal, bufsz);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Handle received protobuf message
|
|
*
|
|
* @param mp Received MeshPacket
|
|
* @param r Decoded AdminMessage
|
|
* @return bool
|
|
*/
|
|
bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r)
|
|
{
|
|
// if handled == false, then let others look at this message also if they want
|
|
bool handled = false;
|
|
assert(r);
|
|
bool fromOthers = !isFromUs(&mp);
|
|
if (mp.which_payload_variant != meshtastic_MeshPacket_decoded_tag) {
|
|
return handled;
|
|
}
|
|
meshtastic_Channel *ch = &channels.getByIndex(mp.channel);
|
|
// Could tighten this up further by tracking the last public_key we went an AdminMessage request to
|
|
// and only allowing responses from that remote.
|
|
if (messageIsResponse(r)) {
|
|
LOG_DEBUG("Allow admin response message");
|
|
} else if (mp.from == 0) {
|
|
if (config.security.is_managed) {
|
|
LOG_INFO("Ignore local admin payload because is_managed");
|
|
return handled;
|
|
}
|
|
} else if (strcasecmp(ch->settings.name, Channels::adminChannel) == 0) {
|
|
if (!config.security.admin_channel_enabled) {
|
|
LOG_INFO("Ignore admin channel, legacy admin is disabled");
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp);
|
|
return handled;
|
|
}
|
|
} else if (mp.pki_encrypted) {
|
|
if ((config.security.admin_key[0].size == 32 &&
|
|
memcmp(mp.public_key.bytes, config.security.admin_key[0].bytes, 32) == 0) ||
|
|
(config.security.admin_key[1].size == 32 &&
|
|
memcmp(mp.public_key.bytes, config.security.admin_key[1].bytes, 32) == 0) ||
|
|
(config.security.admin_key[2].size == 32 &&
|
|
memcmp(mp.public_key.bytes, config.security.admin_key[2].bytes, 32) == 0)) {
|
|
LOG_INFO("PKC admin payload with authorized sender key");
|
|
|
|
// Automatically favorite the node that is using the admin key
|
|
auto remoteNode = nodeDB->getMeshNode(mp.from);
|
|
if (remoteNode && !remoteNode->is_favorite) {
|
|
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
|
|
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
|
|
// without the user doing so deliberately.
|
|
LOG_INFO("PKC admin valid, but not auto-favoriting node %x because role==CLIENT_BASE", mp.from);
|
|
} else {
|
|
LOG_INFO("PKC admin valid. Auto-favoriting node %x", mp.from);
|
|
remoteNode->is_favorite = true;
|
|
}
|
|
}
|
|
} else {
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_PUBLIC_KEY_UNAUTHORIZED, &mp);
|
|
LOG_INFO("Received PKC admin payload, but the sender public key does not match the admin authorized key!");
|
|
return handled;
|
|
}
|
|
} else {
|
|
LOG_INFO("Ignore unauthorized admin payload %i", r->which_payload_variant);
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_NOT_AUTHORIZED, &mp);
|
|
return handled;
|
|
}
|
|
|
|
LOG_INFO("Handle admin payload %i", r->which_payload_variant);
|
|
|
|
// all of the get and set messages, including those for other modules, flow through here first.
|
|
// any message that changes state, we want to check the passkey for
|
|
if (mp.from != 0 && !messageIsRequest(r) && !messageIsResponse(r)) {
|
|
if (!checkPassKey(r)) {
|
|
LOG_WARN("Admin message without session_key!");
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_ADMIN_BAD_SESSION_KEY, &mp);
|
|
return handled;
|
|
}
|
|
}
|
|
switch (r->which_payload_variant) {
|
|
|
|
/**
|
|
* Getters
|
|
*/
|
|
case meshtastic_AdminMessage_get_owner_request_tag:
|
|
LOG_DEBUG("Client got owner");
|
|
handleGetOwner(mp);
|
|
break;
|
|
|
|
case meshtastic_AdminMessage_get_config_request_tag:
|
|
LOG_DEBUG("Client got config");
|
|
handleGetConfig(mp, r->get_config_request);
|
|
break;
|
|
|
|
case meshtastic_AdminMessage_get_module_config_request_tag:
|
|
LOG_DEBUG("Client got module config");
|
|
handleGetModuleConfig(mp, r->get_module_config_request);
|
|
break;
|
|
|
|
case meshtastic_AdminMessage_get_channel_request_tag: {
|
|
uint32_t i = r->get_channel_request - 1;
|
|
LOG_DEBUG("Client got channel %u", i);
|
|
if (i >= MAX_NUM_CHANNELS)
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
|
|
else
|
|
handleGetChannel(mp, i);
|
|
break;
|
|
}
|
|
|
|
/**
|
|
* Setters
|
|
*/
|
|
case meshtastic_AdminMessage_set_owner_tag:
|
|
LOG_DEBUG("Client set owner");
|
|
// Validate names
|
|
if (*r->set_owner.long_name) {
|
|
const char *start = r->set_owner.long_name;
|
|
// Skip all whitespace (space, tab, newline, etc)
|
|
while (*start && isspace((unsigned char)*start))
|
|
start++;
|
|
if (*start == '\0') {
|
|
LOG_WARN("Rejected long_name: must contain at least 1 non-whitespace character");
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
|
|
break;
|
|
}
|
|
}
|
|
if (*r->set_owner.short_name) {
|
|
const char *start = r->set_owner.short_name;
|
|
while (*start && isspace((unsigned char)*start))
|
|
start++;
|
|
if (*start == '\0') {
|
|
LOG_WARN("Rejected short_name: must contain at least 1 non-whitespace character");
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
|
|
break;
|
|
}
|
|
}
|
|
handleSetOwner(r->set_owner);
|
|
break;
|
|
|
|
case meshtastic_AdminMessage_set_config_tag:
|
|
LOG_DEBUG("Client set config");
|
|
handleSetConfig(r->set_config);
|
|
break;
|
|
|
|
case meshtastic_AdminMessage_set_module_config_tag:
|
|
LOG_DEBUG("Client set module config");
|
|
if (!handleSetModuleConfig(r->set_module_config)) {
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
|
|
}
|
|
break;
|
|
|
|
case meshtastic_AdminMessage_set_channel_tag:
|
|
LOG_DEBUG("Client set channel %d", r->set_channel.index);
|
|
if (r->set_channel.index < 0 || r->set_channel.index >= (int)MAX_NUM_CHANNELS)
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
|
|
else
|
|
handleSetChannel(r->set_channel);
|
|
break;
|
|
case meshtastic_AdminMessage_set_ham_mode_tag:
|
|
LOG_DEBUG("Client set ham mode");
|
|
handleSetHamMode(r->set_ham_mode);
|
|
break;
|
|
case meshtastic_AdminMessage_get_ui_config_request_tag: {
|
|
LOG_DEBUG("Client is getting device-ui config");
|
|
handleGetDeviceUIConfig(mp);
|
|
handled = true;
|
|
break;
|
|
}
|
|
|
|
/**
|
|
* Other
|
|
*/
|
|
case meshtastic_AdminMessage_reboot_seconds_tag: {
|
|
reboot(r->reboot_seconds);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_ota_request_tag: {
|
|
#if defined(ARCH_ESP32)
|
|
LOG_INFO("OTA Requested");
|
|
|
|
if (r->ota_request.ota_hash.size != 32) {
|
|
suppressRebootBanner = true;
|
|
sendWarningAndLog("Cannot start OTA: Invalid `ota_hash` provided.");
|
|
break;
|
|
}
|
|
|
|
meshtastic_OTAMode mode = r->ota_request.reboot_ota_mode;
|
|
const char *mode_name = (mode == METHOD_OTA_BLE ? "BLE" : "WiFi");
|
|
|
|
// Check that we have an OTA partition
|
|
const esp_partition_t *part = MeshtasticOTA::getAppPartition();
|
|
if (part == NULL) {
|
|
suppressRebootBanner = true;
|
|
sendWarningAndLog("Cannot start OTA: Cannot find OTA Loader partition.");
|
|
break;
|
|
}
|
|
|
|
static esp_app_desc_t app_desc;
|
|
if (!MeshtasticOTA::getAppDesc(part, &app_desc)) {
|
|
suppressRebootBanner = true;
|
|
sendWarningAndLog("Cannot start OTA: Device does have a valid OTA Loader.");
|
|
break;
|
|
}
|
|
|
|
if (!MeshtasticOTA::checkOTACapability(&app_desc, mode)) {
|
|
suppressRebootBanner = true;
|
|
sendWarningAndLog("OTA Loader does not support %s", mode_name);
|
|
break;
|
|
}
|
|
|
|
if (MeshtasticOTA::trySwitchToOTA()) {
|
|
suppressRebootBanner = true;
|
|
if (screen)
|
|
screen->startFirmwareUpdateScreen();
|
|
MeshtasticOTA::saveConfig(&config.network, mode, r->ota_request.ota_hash.bytes);
|
|
sendWarningAndLog("Rebooting to %s OTA", mode_name);
|
|
} else {
|
|
sendWarningAndLog("Unable to switch to the OTA partition.");
|
|
}
|
|
#endif
|
|
int s = 1; // Reboot in 1 second, hard coded
|
|
LOG_INFO("Reboot in %d seconds", s);
|
|
rebootAtMsec = (s < 0) ? 0 : (millis() + s * 1000);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_shutdown_seconds_tag: {
|
|
int32_t s = r->shutdown_seconds;
|
|
LOG_INFO("Shutdown in %d seconds", s);
|
|
shutdownAtMsec = (s < 0) ? 0 : (millis() + s * 1000);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_get_device_metadata_request_tag: {
|
|
LOG_INFO("Client got device metadata");
|
|
handleGetDeviceMetadata(mp);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_factory_reset_config_tag: {
|
|
disableBluetooth();
|
|
LOG_INFO("Initiate factory config reset");
|
|
nodeDB->factoryReset();
|
|
LOG_INFO("Factory config reset finished, rebooting soon");
|
|
reboot(DEFAULT_REBOOT_SECONDS);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_factory_reset_device_tag: {
|
|
disableBluetooth();
|
|
LOG_INFO("Initiate full factory reset");
|
|
nodeDB->factoryReset(true);
|
|
reboot(DEFAULT_REBOOT_SECONDS);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_nodedb_reset_tag: {
|
|
disableBluetooth();
|
|
LOG_INFO("Initiate node-db reset");
|
|
// CLIENT_BASE, ROUTER and ROUTER_LATE are able to preserve the remaining hop count when relaying a packet via a
|
|
// favorited node, so ensure that their favorites are kept on reset
|
|
bool rolePreference =
|
|
isOneOf(config.device.role, meshtastic_Config_DeviceConfig_Role_CLIENT_BASE,
|
|
meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE);
|
|
nodeDB->resetNodes(rolePreference ? rolePreference : r->nodedb_reset);
|
|
reboot(DEFAULT_REBOOT_SECONDS);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_store_ui_config_tag: {
|
|
LOG_INFO("Storing device-ui config");
|
|
handleStoreDeviceUIConfig(r->store_ui_config);
|
|
handled = true;
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_begin_edit_settings_tag: {
|
|
LOG_INFO("Begin transaction for editing settings");
|
|
hasOpenEditTransaction = true;
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_commit_edit_settings_tag: {
|
|
disableBluetooth();
|
|
LOG_INFO("Commit transaction for edited settings");
|
|
hasOpenEditTransaction = false;
|
|
saveChanges(SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS | SEGMENT_NODEDATABASE);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_get_device_connection_status_request_tag: {
|
|
LOG_INFO("Client got device connection status");
|
|
handleGetDeviceConnectionStatus(mp);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_get_module_config_response_tag: {
|
|
LOG_INFO("Client received a get_module_config response");
|
|
if (fromOthers && r->get_module_config_response.which_payload_variant ==
|
|
meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG) {
|
|
handleGetModuleConfigResponse(mp, r);
|
|
}
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_remove_by_nodenum_tag: {
|
|
LOG_INFO("Client received remove_nodenum command");
|
|
nodeDB->removeNodeByNum(r->remove_by_nodenum);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_add_contact_tag: {
|
|
LOG_INFO("Client received add_contact command");
|
|
nodeDB->addFromContact(r->add_contact);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_set_favorite_node_tag: {
|
|
LOG_INFO("Client received set_favorite_node command");
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_favorite_node);
|
|
if (node != NULL) {
|
|
node->is_favorite = true;
|
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
|
if (screen)
|
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
|
}
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_remove_favorite_node_tag: {
|
|
LOG_INFO("Client received remove_favorite_node command");
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_favorite_node);
|
|
if (node != NULL) {
|
|
node->is_favorite = false;
|
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
|
if (screen)
|
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
|
}
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_set_ignored_node_tag: {
|
|
LOG_INFO("Client received set_ignored_node command");
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->set_ignored_node);
|
|
if (node != NULL) {
|
|
node->is_ignored = true;
|
|
node->has_device_metrics = false;
|
|
node->has_position = false;
|
|
node->user.public_key.size = 0;
|
|
memset(node->user.public_key.bytes, 0, sizeof(node->user.public_key.bytes));
|
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
|
}
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_remove_ignored_node_tag: {
|
|
LOG_INFO("Client received remove_ignored_node command");
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->remove_ignored_node);
|
|
if (node != NULL) {
|
|
node->is_ignored = false;
|
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
|
}
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_toggle_muted_node_tag: {
|
|
LOG_INFO("Client received toggle_muted_node command");
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(r->toggle_muted_node);
|
|
if (node != NULL) {
|
|
node->bitfield ^= (1 << NODEINFO_BITFIELD_IS_MUTED_SHIFT);
|
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case meshtastic_AdminMessage_set_fixed_position_tag: {
|
|
LOG_INFO("Client received set_fixed_position command");
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
|
node->has_position = true;
|
|
node->position = TypeConversions::ConvertToPositionLite(r->set_fixed_position);
|
|
nodeDB->setLocalPosition(r->set_fixed_position);
|
|
config.position.fixed_position = true;
|
|
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
if (gps != nullptr)
|
|
gps->enable();
|
|
// Send our new fixed position to the mesh for good measure
|
|
positionModule->sendOurPosition();
|
|
#endif
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_remove_fixed_position_tag: {
|
|
LOG_INFO("Client received remove_fixed_position command");
|
|
nodeDB->clearLocalPosition();
|
|
config.position.fixed_position = false;
|
|
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_set_time_only_tag: {
|
|
LOG_INFO("Client received set_time_only command");
|
|
struct timeval tv;
|
|
tv.tv_sec = r->set_time_only;
|
|
tv.tv_usec = 0;
|
|
|
|
perhapsSetRTC(RTCQualityNTP, &tv, false);
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_enter_dfu_mode_request_tag: {
|
|
LOG_INFO("Client requesting to enter DFU mode");
|
|
#if HAS_SCREEN
|
|
IF_SCREEN(screen->showSimpleBanner("Device is rebooting\ninto DFU mode.", 0));
|
|
#endif
|
|
#if defined(ARCH_NRF52) || defined(ARCH_RP2040)
|
|
enterDfuMode();
|
|
#endif
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_delete_file_request_tag: {
|
|
LOG_DEBUG("Client requesting to delete file: %s", r->delete_file_request);
|
|
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
if (FSCom.remove(r->delete_file_request)) {
|
|
LOG_DEBUG("Successfully deleted file");
|
|
} else {
|
|
LOG_DEBUG("Failed to delete file");
|
|
}
|
|
spiLock->unlock();
|
|
#endif
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_backup_preferences_tag: {
|
|
LOG_INFO("Client requesting to backup preferences");
|
|
if (nodeDB->backupPreferences(r->backup_preferences)) {
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
|
|
} else {
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
|
|
}
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_restore_preferences_tag: {
|
|
LOG_INFO("Client requesting to restore preferences");
|
|
if (nodeDB->restorePreferences(r->backup_preferences,
|
|
SEGMENT_DEVICESTATE | SEGMENT_CONFIG | SEGMENT_MODULECONFIG | SEGMENT_CHANNELS)) {
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
|
|
LOG_DEBUG("Rebooting after successful restore of preferences");
|
|
reboot(1000);
|
|
disableBluetooth();
|
|
} else {
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_BAD_REQUEST, &mp);
|
|
}
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_remove_backup_preferences_tag: {
|
|
LOG_INFO("Client requesting to remove backup preferences");
|
|
#ifdef FSCom
|
|
if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_FLASH) {
|
|
spiLock->lock();
|
|
FSCom.remove(backupFileName);
|
|
spiLock->unlock();
|
|
} else if (r->remove_backup_preferences == meshtastic_AdminMessage_BackupLocation_SD) {
|
|
// TODO: After more mainline SD card support
|
|
LOG_ERROR("SD backup removal not implemented yet");
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
case meshtastic_AdminMessage_send_input_event_tag: {
|
|
LOG_INFO("Client requesting to send input event");
|
|
handleSendInputEvent(r->send_input_event);
|
|
break;
|
|
}
|
|
#ifdef ARCH_PORTDUINO
|
|
case meshtastic_AdminMessage_exit_simulator_tag:
|
|
LOG_INFO("Exiting simulator");
|
|
exit(0);
|
|
break;
|
|
#endif
|
|
|
|
default:
|
|
meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default;
|
|
AdminMessageHandleResult handleResult = MeshModule::handleAdminMessageForAllModules(mp, r, &res);
|
|
|
|
if (handleResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) {
|
|
setPassKey(&res);
|
|
myReply = allocDataProtobuf(res);
|
|
} else if (mp.decoded.want_response) {
|
|
LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant);
|
|
} else if (handleResult != AdminMessageHandleResult::HANDLED) {
|
|
// Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages
|
|
LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// Allow any observers (e.g. the UI) to handle/respond
|
|
AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED;
|
|
meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default;
|
|
AdminModule_ObserverData observerData = {
|
|
.request = r,
|
|
.response = &observerResponse,
|
|
.result = &observerResult,
|
|
};
|
|
|
|
notifyObservers(&observerData);
|
|
|
|
if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) {
|
|
setPassKey(&observerResponse);
|
|
myReply = allocDataProtobuf(observerResponse);
|
|
LOG_DEBUG("Observer responded to admin message");
|
|
} else if (observerResult == AdminMessageHandleResult::HANDLED) {
|
|
LOG_DEBUG("Observer handled admin message");
|
|
}
|
|
|
|
// If asked for a response and it is not yet set, generate an 'ACK' response
|
|
if (mp.decoded.want_response && !myReply) {
|
|
myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
|
|
}
|
|
if (mp.pki_encrypted && myReply) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
return handled;
|
|
}
|
|
|
|
void AdminModule::handleGetModuleConfigResponse(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *r)
|
|
{
|
|
// Skip if it's disabled or no pins are exposed
|
|
if (!r->get_module_config_response.payload_variant.remote_hardware.enabled ||
|
|
r->get_module_config_response.payload_variant.remote_hardware.available_pins_count == 0) {
|
|
LOG_DEBUG("Remote hardware module disabled or no available_pins. Skip");
|
|
return;
|
|
}
|
|
for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) {
|
|
if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) {
|
|
continue;
|
|
}
|
|
for (uint8_t j = 0; j < r->get_module_config_response.payload_variant.remote_hardware.available_pins_count; j++) {
|
|
auto availablePin = r->get_module_config_response.payload_variant.remote_hardware.available_pins[j];
|
|
if (i < devicestate.node_remote_hardware_pins_count) {
|
|
devicestate.node_remote_hardware_pins[i].node_num = mp.from;
|
|
devicestate.node_remote_hardware_pins[i].pin = availablePin;
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setter methods
|
|
*/
|
|
|
|
void AdminModule::handleSetOwner(const meshtastic_User &o)
|
|
{
|
|
int changed = 0;
|
|
|
|
if (*o.long_name) {
|
|
changed |= strcmp(owner.long_name, o.long_name);
|
|
strncpy(owner.long_name, o.long_name, sizeof(owner.long_name));
|
|
}
|
|
if (*o.short_name) {
|
|
changed |= strcmp(owner.short_name, o.short_name);
|
|
strncpy(owner.short_name, o.short_name, sizeof(owner.short_name));
|
|
}
|
|
snprintf(owner.id, sizeof(owner.id), "!%08x", nodeDB->getNodeNum());
|
|
|
|
if (owner.is_licensed != o.is_licensed) {
|
|
changed = 1;
|
|
owner.is_licensed = o.is_licensed;
|
|
if (channels.ensureLicensedOperation()) {
|
|
sendWarning(licensedModeMessage);
|
|
}
|
|
}
|
|
if (owner.has_is_unmessagable != o.has_is_unmessagable ||
|
|
(o.has_is_unmessagable && owner.is_unmessagable != o.is_unmessagable)) {
|
|
changed = 1;
|
|
owner.has_is_unmessagable = owner.has_is_unmessagable || o.has_is_unmessagable;
|
|
owner.is_unmessagable = o.is_unmessagable;
|
|
}
|
|
|
|
if (changed) { // If nothing really changed, don't broadcast on the network or write to flash
|
|
service->reloadOwner(!hasOpenEditTransaction);
|
|
saveChanges(SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE);
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleSetConfig(const meshtastic_Config &c)
|
|
{
|
|
auto changes = SEGMENT_CONFIG;
|
|
auto existingRole = config.device.role;
|
|
bool isRegionUnset = (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET);
|
|
bool requiresReboot = true;
|
|
|
|
switch (c.which_payload_variant) {
|
|
case meshtastic_Config_device_tag:
|
|
LOG_INFO("Set config: Device");
|
|
config.has_device = true;
|
|
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
|
if (config.device.double_tap_as_button_press == false && c.payload_variant.device.double_tap_as_button_press == true &&
|
|
accelerometerThread->enabled == false) {
|
|
config.device.double_tap_as_button_press = c.payload_variant.device.double_tap_as_button_press;
|
|
accelerometerThread->enabled = true;
|
|
accelerometerThread->start();
|
|
}
|
|
#endif
|
|
if (config.device.button_gpio == c.payload_variant.device.button_gpio &&
|
|
config.device.buzzer_gpio == c.payload_variant.device.buzzer_gpio &&
|
|
config.device.role == c.payload_variant.device.role &&
|
|
config.device.rebroadcast_mode == c.payload_variant.device.rebroadcast_mode) {
|
|
requiresReboot = false;
|
|
}
|
|
config.device = c.payload_variant.device;
|
|
if (config.device.rebroadcast_mode == meshtastic_Config_DeviceConfig_RebroadcastMode_NONE &&
|
|
(config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
|
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)) {
|
|
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL;
|
|
const char *warning = "Rebroadcast mode can't be set to NONE for a router role";
|
|
LOG_WARN(warning);
|
|
sendWarning(warning);
|
|
}
|
|
// If we're setting router role for the first time, install its intervals
|
|
if (existingRole != c.payload_variant.device.role) {
|
|
nodeDB->installRoleDefaults(c.payload_variant.device.role);
|
|
changes |= SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE; // Some role defaults affect owner
|
|
}
|
|
if (config.device.node_info_broadcast_secs < min_node_info_broadcast_secs) {
|
|
LOG_DEBUG("Tried to set node_info_broadcast_secs too low, setting to %d", min_node_info_broadcast_secs);
|
|
config.device.node_info_broadcast_secs = min_node_info_broadcast_secs;
|
|
}
|
|
// Router Client and Repeater deprecated; Set it to client
|
|
if (IS_ONE_OF(c.payload_variant.device.role, meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT,
|
|
meshtastic_Config_DeviceConfig_Role_REPEATER)) {
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
if (moduleConfig.store_forward.enabled && !moduleConfig.store_forward.is_server) {
|
|
moduleConfig.store_forward.is_server = true;
|
|
changes |= SEGMENT_MODULECONFIG;
|
|
requiresReboot = true;
|
|
}
|
|
}
|
|
#if USERPREFS_EVENT_MODE
|
|
// If we're in event mode, nobody is a Router or Router Late
|
|
if (config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER ||
|
|
config.device.role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
}
|
|
#endif
|
|
break;
|
|
case meshtastic_Config_position_tag:
|
|
LOG_INFO("Set config: Position");
|
|
config.has_position = true;
|
|
// If we have turned off the GPS (disabled or not present) and we're not using fixed position,
|
|
// clear the stored position since it may not get updated
|
|
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
|
|
c.payload_variant.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED &&
|
|
config.position.fixed_position == false && c.payload_variant.position.fixed_position == false) {
|
|
nodeDB->clearLocalPosition();
|
|
saveChanges(SEGMENT_NODEDATABASE | SEGMENT_CONFIG, false);
|
|
}
|
|
config.position = c.payload_variant.position;
|
|
|
|
// Save nodedb as well in case we got a fixed position packet
|
|
break;
|
|
case meshtastic_Config_power_tag:
|
|
LOG_INFO("Set config: Power");
|
|
config.has_power = true;
|
|
// Really just the adc override is the only thing that can change without a reboot
|
|
if (config.power.device_battery_ina_address == c.payload_variant.power.device_battery_ina_address &&
|
|
config.power.is_power_saving == c.payload_variant.power.is_power_saving &&
|
|
config.power.ls_secs == c.payload_variant.power.ls_secs &&
|
|
config.power.min_wake_secs == c.payload_variant.power.min_wake_secs &&
|
|
config.power.on_battery_shutdown_after_secs == c.payload_variant.power.on_battery_shutdown_after_secs &&
|
|
config.power.sds_secs == c.payload_variant.power.sds_secs &&
|
|
config.power.wait_bluetooth_secs == c.payload_variant.power.wait_bluetooth_secs) {
|
|
requiresReboot = false;
|
|
}
|
|
config.power = c.payload_variant.power;
|
|
if (c.payload_variant.power.on_battery_shutdown_after_secs > 0 &&
|
|
c.payload_variant.power.on_battery_shutdown_after_secs < 30) {
|
|
LOG_WARN("Tried to set on_battery_shutdown_after_secs too low, set to min 30 seconds");
|
|
config.power.on_battery_shutdown_after_secs = 30;
|
|
}
|
|
break;
|
|
case meshtastic_Config_network_tag:
|
|
LOG_INFO("Set config: WiFi");
|
|
config.has_network = true;
|
|
config.network = c.payload_variant.network;
|
|
break;
|
|
case meshtastic_Config_display_tag:
|
|
LOG_INFO("Set config: Display");
|
|
config.has_display = true;
|
|
if (config.display.screen_on_secs == c.payload_variant.display.screen_on_secs &&
|
|
config.display.flip_screen == c.payload_variant.display.flip_screen &&
|
|
config.display.oled == c.payload_variant.display.oled &&
|
|
config.display.displaymode == c.payload_variant.display.displaymode) {
|
|
requiresReboot = false;
|
|
} else if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR &&
|
|
c.payload_variant.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
|
config.bluetooth.enabled = false;
|
|
}
|
|
#if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR
|
|
if (config.display.wake_on_tap_or_motion == false && c.payload_variant.display.wake_on_tap_or_motion == true &&
|
|
accelerometerThread->enabled == false) {
|
|
config.display.wake_on_tap_or_motion = c.payload_variant.display.wake_on_tap_or_motion;
|
|
accelerometerThread->enabled = true;
|
|
accelerometerThread->start();
|
|
}
|
|
#endif
|
|
config.display = c.payload_variant.display;
|
|
break;
|
|
|
|
case meshtastic_Config_lora_tag: {
|
|
// Wrap the entire case in a block to scope variables and avoid crossing initialization
|
|
auto oldLoraConfig = config.lora;
|
|
auto validatedLora = c.payload_variant.lora;
|
|
|
|
LOG_INFO("Set config: LoRa");
|
|
config.has_lora = true;
|
|
|
|
if (validatedLora.coding_rate != clampCodingRate(validatedLora.coding_rate)) {
|
|
LOG_WARN("Invalid coding_rate %d, setting to %d", validatedLora.coding_rate, LORA_CR_DEFAULT);
|
|
validatedLora.coding_rate = LORA_CR_DEFAULT;
|
|
}
|
|
|
|
if (validatedLora.spread_factor != clampSpreadFactor(validatedLora.spread_factor)) {
|
|
LOG_WARN("Invalid spread_factor %d, setting to %d", validatedLora.spread_factor, LORA_SF_DEFAULT);
|
|
validatedLora.spread_factor = LORA_SF_DEFAULT;
|
|
}
|
|
|
|
// If no lora radio parameters change, don't need to reboot
|
|
if (oldLoraConfig.use_preset == validatedLora.use_preset && oldLoraConfig.region == validatedLora.region &&
|
|
oldLoraConfig.modem_preset == validatedLora.modem_preset && oldLoraConfig.bandwidth == validatedLora.bandwidth &&
|
|
oldLoraConfig.spread_factor == validatedLora.spread_factor &&
|
|
oldLoraConfig.coding_rate == validatedLora.coding_rate && oldLoraConfig.tx_power == validatedLora.tx_power &&
|
|
oldLoraConfig.frequency_offset == validatedLora.frequency_offset &&
|
|
oldLoraConfig.override_frequency == validatedLora.override_frequency &&
|
|
oldLoraConfig.channel_num == validatedLora.channel_num &&
|
|
oldLoraConfig.sx126x_rx_boosted_gain == validatedLora.sx126x_rx_boosted_gain) {
|
|
requiresReboot = false;
|
|
}
|
|
|
|
#if defined(ARCH_PORTDUINO)
|
|
// If running on portduino and using SimRadio, do not require reboot
|
|
if (SimRadio::instance) {
|
|
requiresReboot = false;
|
|
}
|
|
#endif
|
|
|
|
#ifdef RF95_FAN_EN
|
|
// Turn PA off if disabled by config
|
|
if (c.payload_variant.lora.pa_fan_disabled) {
|
|
digitalWrite(RF95_FAN_EN, LOW ^ 0);
|
|
} else {
|
|
digitalWrite(RF95_FAN_EN, HIGH ^ 0);
|
|
}
|
|
#endif
|
|
config.lora = validatedLora;
|
|
|
|
#if HAS_LORA_FEM
|
|
// Apply FEM LNA mode from config (only meaningful on hardware that supports it)
|
|
if (loraFEMInterface.isLnaCanControl()) {
|
|
loraFEMInterface.setLNAEnable(config.lora.fem_lna_mode == meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED);
|
|
} else if (config.lora.fem_lna_mode != meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT) {
|
|
// Hardware FEM does not support LNA control; normalize stored config to match actual capability
|
|
LOG_WARN("FEM LNA mode configured but current FEM does not support LNA control; normalizing to NOT_PRESENT");
|
|
config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT;
|
|
}
|
|
#endif
|
|
// If we're setting region for the first time, init the region and regenerate the keys
|
|
if (isRegionUnset && config.lora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
|
if (!owner.is_licensed) {
|
|
bool keygenSuccess = false;
|
|
if (config.security.private_key.size == 32) {
|
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
|
keygenSuccess = true;
|
|
}
|
|
} else {
|
|
LOG_INFO("Generate new PKI keys");
|
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
|
keygenSuccess = true;
|
|
}
|
|
if (keygenSuccess) {
|
|
config.security.public_key.size = 32;
|
|
config.security.private_key.size = 32;
|
|
owner.public_key.size = 32;
|
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
|
}
|
|
}
|
|
#endif
|
|
config.lora.tx_enabled = true;
|
|
initRegion();
|
|
if (myRegion->dutyCycle < 100) {
|
|
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
|
}
|
|
// Compare the entire string, we are sure of the length as a topic has never been set
|
|
if (strcmp(moduleConfig.mqtt.root, default_mqtt_root) == 0) {
|
|
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
|
changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG;
|
|
}
|
|
}
|
|
if (config.lora.region != myRegion->code) {
|
|
// Region has changed so check whether there is a regulatory one we should be using instead.
|
|
// Additionally as a side-effect, assume a new value under myRegion
|
|
initRegion();
|
|
|
|
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
|
|
// Default root is in use, so subscribe to the appropriate MQTT topic for this region
|
|
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
|
}
|
|
|
|
changes = SEGMENT_CONFIG | SEGMENT_MODULECONFIG;
|
|
}
|
|
break;
|
|
}
|
|
case meshtastic_Config_bluetooth_tag:
|
|
LOG_INFO("Set config: Bluetooth");
|
|
config.has_bluetooth = true;
|
|
config.bluetooth = c.payload_variant.bluetooth;
|
|
break;
|
|
case meshtastic_Config_security_tag:
|
|
LOG_INFO("Set config: Security");
|
|
config.security = c.payload_variant.security;
|
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN) && !(MESHTASTIC_EXCLUDE_PKI)
|
|
// If the client set the key to blank, go ahead and regenerate so long as we're not in ham mode
|
|
if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
if (config.security.private_key.size != 32) {
|
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
|
|
|
} else {
|
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
|
config.security.public_key.size = 32;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
owner.public_key.size = config.security.public_key.size;
|
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size);
|
|
#if !MESHTASTIC_EXCLUDE_PKI
|
|
crypto->setDHPrivateKey(config.security.private_key.bytes);
|
|
#endif
|
|
if (config.security.is_managed && !(config.security.admin_key[0].size == 32 || config.security.admin_key[1].size == 32 ||
|
|
config.security.admin_key[2].size == 32)) {
|
|
config.security.is_managed = false;
|
|
const char *warning = "You must provide at least one admin public key to enable managed mode";
|
|
LOG_WARN(warning);
|
|
sendWarning(warning);
|
|
}
|
|
|
|
if (config.security.debug_log_api_enabled == c.payload_variant.security.debug_log_api_enabled &&
|
|
config.security.serial_enabled == c.payload_variant.security.serial_enabled)
|
|
requiresReboot = false;
|
|
|
|
break;
|
|
case meshtastic_Config_device_ui_tag:
|
|
// NOOP! This is handled by handleStoreDeviceUIConfig
|
|
break;
|
|
}
|
|
if (requiresReboot && !hasOpenEditTransaction) {
|
|
disableBluetooth();
|
|
}
|
|
|
|
saveChanges(changes, requiresReboot);
|
|
}
|
|
|
|
bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
|
{
|
|
bool shouldReboot = true;
|
|
// If we are in an open transaction or configuring MQTT or Serial (which have validation), defer disabling Bluetooth
|
|
// Otherwise, disable Bluetooth to prevent the phone from interfering with the config
|
|
if (!hasOpenEditTransaction && !IS_ONE_OF(c.which_payload_variant, meshtastic_ModuleConfig_mqtt_tag,
|
|
meshtastic_ModuleConfig_serial_tag, meshtastic_ModuleConfig_statusmessage_tag)) {
|
|
disableBluetooth();
|
|
}
|
|
|
|
switch (c.which_payload_variant) {
|
|
case meshtastic_ModuleConfig_mqtt_tag:
|
|
#if MESHTASTIC_EXCLUDE_MQTT
|
|
LOG_WARN("Set module config: MESHTASTIC_EXCLUDE_MQTT is defined. Not setting MQTT config");
|
|
return false;
|
|
#else
|
|
LOG_INFO("Set module config: MQTT");
|
|
if (!MQTT::isValidConfig(c.payload_variant.mqtt)) {
|
|
return false;
|
|
}
|
|
// Disable Bluetooth to prevent interference during MQTT configuration
|
|
disableBluetooth();
|
|
moduleConfig.has_mqtt = true;
|
|
moduleConfig.mqtt = c.payload_variant.mqtt;
|
|
#endif
|
|
break;
|
|
case meshtastic_ModuleConfig_serial_tag:
|
|
LOG_INFO("Set module config: Serial");
|
|
#if (defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040)) && !defined(CONFIG_IDF_TARGET_ESP32S2) && \
|
|
!defined(CONFIG_IDF_TARGET_ESP32C3)
|
|
if (!SerialModule::isValidConfig(c.payload_variant.serial)) {
|
|
LOG_ERROR("Invalid serial config");
|
|
return false;
|
|
}
|
|
disableBluetooth(); // Disable Bluetooth to prevent interference during Serial configuration
|
|
#endif
|
|
moduleConfig.has_serial = true;
|
|
moduleConfig.serial = c.payload_variant.serial;
|
|
break;
|
|
case meshtastic_ModuleConfig_external_notification_tag:
|
|
LOG_INFO("Set module config: External Notification");
|
|
moduleConfig.has_external_notification = true;
|
|
moduleConfig.external_notification = c.payload_variant.external_notification;
|
|
break;
|
|
case meshtastic_ModuleConfig_store_forward_tag:
|
|
LOG_INFO("Set module config: Store & Forward");
|
|
moduleConfig.has_store_forward = true;
|
|
moduleConfig.store_forward = c.payload_variant.store_forward;
|
|
break;
|
|
case meshtastic_ModuleConfig_range_test_tag:
|
|
LOG_INFO("Set module config: Range Test");
|
|
moduleConfig.has_range_test = true;
|
|
moduleConfig.range_test = c.payload_variant.range_test;
|
|
break;
|
|
case meshtastic_ModuleConfig_telemetry_tag:
|
|
LOG_INFO("Set module config: Telemetry");
|
|
moduleConfig.has_telemetry = true;
|
|
moduleConfig.telemetry = c.payload_variant.telemetry;
|
|
break;
|
|
case meshtastic_ModuleConfig_canned_message_tag:
|
|
LOG_INFO("Set module config: Canned Message");
|
|
moduleConfig.has_canned_message = true;
|
|
moduleConfig.canned_message = c.payload_variant.canned_message;
|
|
break;
|
|
case meshtastic_ModuleConfig_audio_tag:
|
|
LOG_INFO("Set module config: Audio");
|
|
moduleConfig.has_audio = true;
|
|
moduleConfig.audio = c.payload_variant.audio;
|
|
break;
|
|
case meshtastic_ModuleConfig_remote_hardware_tag:
|
|
LOG_INFO("Set module config: Remote Hardware");
|
|
moduleConfig.has_remote_hardware = true;
|
|
moduleConfig.remote_hardware = c.payload_variant.remote_hardware;
|
|
break;
|
|
case meshtastic_ModuleConfig_neighbor_info_tag:
|
|
LOG_INFO("Set module config: Neighbor Info");
|
|
moduleConfig.has_neighbor_info = true;
|
|
if (moduleConfig.neighbor_info.update_interval < min_neighbor_info_broadcast_secs) {
|
|
LOG_DEBUG("Tried to set update_interval too low, setting to %d", default_neighbor_info_broadcast_secs);
|
|
moduleConfig.neighbor_info.update_interval = default_neighbor_info_broadcast_secs;
|
|
}
|
|
moduleConfig.neighbor_info = c.payload_variant.neighbor_info;
|
|
break;
|
|
case meshtastic_ModuleConfig_detection_sensor_tag:
|
|
LOG_INFO("Set module config: Detection Sensor");
|
|
moduleConfig.has_detection_sensor = true;
|
|
moduleConfig.detection_sensor = c.payload_variant.detection_sensor;
|
|
break;
|
|
case meshtastic_ModuleConfig_ambient_lighting_tag:
|
|
LOG_INFO("Set module config: Ambient Lighting");
|
|
moduleConfig.has_ambient_lighting = true;
|
|
moduleConfig.ambient_lighting = c.payload_variant.ambient_lighting;
|
|
break;
|
|
case meshtastic_ModuleConfig_paxcounter_tag:
|
|
LOG_INFO("Set module config: Paxcounter");
|
|
moduleConfig.has_paxcounter = true;
|
|
moduleConfig.paxcounter = c.payload_variant.paxcounter;
|
|
break;
|
|
case meshtastic_ModuleConfig_statusmessage_tag:
|
|
LOG_INFO("Set module config: StatusMessage");
|
|
moduleConfig.has_statusmessage = true;
|
|
moduleConfig.statusmessage = c.payload_variant.statusmessage;
|
|
shouldReboot = false;
|
|
break;
|
|
case meshtastic_ModuleConfig_traffic_management_tag:
|
|
LOG_INFO("Set module config: Traffic Management");
|
|
moduleConfig.has_traffic_management = true;
|
|
moduleConfig.traffic_management = c.payload_variant.traffic_management;
|
|
break;
|
|
}
|
|
saveChanges(SEGMENT_MODULECONFIG, shouldReboot);
|
|
return true;
|
|
}
|
|
|
|
void AdminModule::handleSetChannel(const meshtastic_Channel &cc)
|
|
{
|
|
channels.setChannel(cc);
|
|
if (channels.ensureLicensedOperation()) {
|
|
sendWarning(licensedModeMessage);
|
|
}
|
|
channels.onConfigChanged(); // tell the radios about this change
|
|
saveChanges(SEGMENT_CHANNELS, false);
|
|
}
|
|
|
|
/**
|
|
* Getters
|
|
*/
|
|
|
|
void AdminModule::handleGetOwner(const meshtastic_MeshPacket &req)
|
|
{
|
|
if (req.decoded.want_response) {
|
|
// We create the reply here
|
|
meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default;
|
|
res.get_owner_response = owner;
|
|
|
|
res.which_payload_variant = meshtastic_AdminMessage_get_owner_response_tag;
|
|
setPassKey(&res);
|
|
myReply = allocDataProtobuf(res);
|
|
if (req.pki_encrypted) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleGetConfig(const meshtastic_MeshPacket &req, const uint32_t configType)
|
|
{
|
|
meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default;
|
|
|
|
if (req.decoded.want_response) {
|
|
switch (configType) {
|
|
case meshtastic_AdminMessage_ConfigType_DEVICE_CONFIG:
|
|
LOG_INFO("Get config: Device");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_device_tag;
|
|
res.get_config_response.payload_variant.device = config.device;
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_POSITION_CONFIG:
|
|
LOG_INFO("Get config: Position");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_position_tag;
|
|
res.get_config_response.payload_variant.position = config.position;
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_POWER_CONFIG:
|
|
LOG_INFO("Get config: Power");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_power_tag;
|
|
res.get_config_response.payload_variant.power = config.power;
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_NETWORK_CONFIG:
|
|
LOG_INFO("Get config: Network");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_network_tag;
|
|
res.get_config_response.payload_variant.network = config.network;
|
|
writeSecret(res.get_config_response.payload_variant.network.wifi_psk,
|
|
sizeof(res.get_config_response.payload_variant.network.wifi_psk), config.network.wifi_psk);
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_DISPLAY_CONFIG:
|
|
LOG_INFO("Get config: Display");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_display_tag;
|
|
res.get_config_response.payload_variant.display = config.display;
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_LORA_CONFIG:
|
|
LOG_INFO("Get config: LoRa");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_lora_tag;
|
|
res.get_config_response.payload_variant.lora = config.lora;
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_BLUETOOTH_CONFIG:
|
|
LOG_INFO("Get config: Bluetooth");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_bluetooth_tag;
|
|
res.get_config_response.payload_variant.bluetooth = config.bluetooth;
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_SECURITY_CONFIG:
|
|
LOG_INFO("Get config: Security");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_security_tag;
|
|
res.get_config_response.payload_variant.security = config.security;
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_SESSIONKEY_CONFIG:
|
|
LOG_INFO("Get config: Sessionkey");
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_sessionkey_tag;
|
|
break;
|
|
case meshtastic_AdminMessage_ConfigType_DEVICEUI_CONFIG:
|
|
// NOOP! This is handled by handleGetDeviceUIConfig
|
|
res.get_config_response.which_payload_variant = meshtastic_Config_device_ui_tag;
|
|
break;
|
|
}
|
|
// NOTE: The phone app needs to know the ls_secs value so it can properly expect sleep behavior.
|
|
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
|
|
// using to the app (so that even old phone apps work with new device loads).
|
|
// r.get_radio_response.preferences.ls_secs = getPref_ls_secs();
|
|
// hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally
|
|
// private and useful for users to know current provisioning)
|
|
// hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant =
|
|
// Config_ModuleConfig_telemetry_tag;
|
|
res.which_payload_variant = meshtastic_AdminMessage_get_config_response_tag;
|
|
setPassKey(&res);
|
|
myReply = allocDataProtobuf(res);
|
|
if (req.pki_encrypted) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleGetModuleConfig(const meshtastic_MeshPacket &req, const uint32_t configType)
|
|
{
|
|
meshtastic_AdminMessage res = meshtastic_AdminMessage_init_default;
|
|
|
|
if (req.decoded.want_response) {
|
|
switch (configType) {
|
|
case meshtastic_AdminMessage_ModuleConfigType_MQTT_CONFIG:
|
|
LOG_INFO("Get module config: MQTT");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_mqtt_tag;
|
|
res.get_module_config_response.payload_variant.mqtt = moduleConfig.mqtt;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_SERIAL_CONFIG:
|
|
LOG_INFO("Get module config: Serial");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_serial_tag;
|
|
res.get_module_config_response.payload_variant.serial = moduleConfig.serial;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_EXTNOTIF_CONFIG:
|
|
LOG_INFO("Get module config: External Notification");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_external_notification_tag;
|
|
res.get_module_config_response.payload_variant.external_notification = moduleConfig.external_notification;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_STOREFORWARD_CONFIG:
|
|
LOG_INFO("Get module config: Store & Forward");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_store_forward_tag;
|
|
res.get_module_config_response.payload_variant.store_forward = moduleConfig.store_forward;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_RANGETEST_CONFIG:
|
|
LOG_INFO("Get module config: Range Test");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_range_test_tag;
|
|
res.get_module_config_response.payload_variant.range_test = moduleConfig.range_test;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_TELEMETRY_CONFIG:
|
|
LOG_INFO("Get module config: Telemetry");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_telemetry_tag;
|
|
res.get_module_config_response.payload_variant.telemetry = moduleConfig.telemetry;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_CANNEDMSG_CONFIG:
|
|
LOG_INFO("Get module config: Canned Message");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_canned_message_tag;
|
|
res.get_module_config_response.payload_variant.canned_message = moduleConfig.canned_message;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_AUDIO_CONFIG:
|
|
LOG_INFO("Get module config: Audio");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_audio_tag;
|
|
res.get_module_config_response.payload_variant.audio = moduleConfig.audio;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_REMOTEHARDWARE_CONFIG:
|
|
LOG_INFO("Get module config: Remote Hardware");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_remote_hardware_tag;
|
|
res.get_module_config_response.payload_variant.remote_hardware = moduleConfig.remote_hardware;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_NEIGHBORINFO_CONFIG:
|
|
LOG_INFO("Get module config: Neighbor Info");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_neighbor_info_tag;
|
|
res.get_module_config_response.payload_variant.neighbor_info = moduleConfig.neighbor_info;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_DETECTIONSENSOR_CONFIG:
|
|
LOG_INFO("Get module config: Detection Sensor");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_detection_sensor_tag;
|
|
res.get_module_config_response.payload_variant.detection_sensor = moduleConfig.detection_sensor;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_AMBIENTLIGHTING_CONFIG:
|
|
LOG_INFO("Get module config: Ambient Lighting");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_ambient_lighting_tag;
|
|
res.get_module_config_response.payload_variant.ambient_lighting = moduleConfig.ambient_lighting;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_PAXCOUNTER_CONFIG:
|
|
LOG_INFO("Get module config: Paxcounter");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_paxcounter_tag;
|
|
res.get_module_config_response.payload_variant.paxcounter = moduleConfig.paxcounter;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_STATUSMESSAGE_CONFIG:
|
|
LOG_INFO("Get module config: StatusMessage");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_statusmessage_tag;
|
|
res.get_module_config_response.payload_variant.statusmessage = moduleConfig.statusmessage;
|
|
break;
|
|
case meshtastic_AdminMessage_ModuleConfigType_TRAFFICMANAGEMENT_CONFIG:
|
|
LOG_INFO("Get module config: Traffic Management");
|
|
res.get_module_config_response.which_payload_variant = meshtastic_ModuleConfig_traffic_management_tag;
|
|
res.get_module_config_response.payload_variant.traffic_management = moduleConfig.traffic_management;
|
|
break;
|
|
}
|
|
|
|
// NOTE: The phone app needs to know the ls_secsvalue so it can properly expect sleep behavior.
|
|
// So even if we internally use 0 to represent 'use default' we still need to send the value we are
|
|
// using to the app (so that even old phone apps work with new device loads).
|
|
// r.get_radio_response.preferences.ls_secs = getPref_ls_secs();
|
|
// hideSecret(r.get_radio_response.preferences.wifi_ssid); // hmm - leave public for now, because only minimally
|
|
// private and useful for users to know current provisioning)
|
|
// hideSecret(r.get_radio_response.preferences.wifi_password); r.get_config_response.which_payloadVariant =
|
|
// Config_ModuleConfig_telemetry_tag;
|
|
res.which_payload_variant = meshtastic_AdminMessage_get_module_config_response_tag;
|
|
setPassKey(&res);
|
|
myReply = allocDataProtobuf(res);
|
|
if (req.pki_encrypted) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleGetNodeRemoteHardwarePins(const meshtastic_MeshPacket &req)
|
|
{
|
|
meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default;
|
|
r.which_payload_variant = meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag;
|
|
for (uint8_t i = 0; i < devicestate.node_remote_hardware_pins_count; i++) {
|
|
if (devicestate.node_remote_hardware_pins[i].node_num == 0 || !devicestate.node_remote_hardware_pins[i].has_pin) {
|
|
continue;
|
|
}
|
|
r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i] = devicestate.node_remote_hardware_pins[i];
|
|
}
|
|
for (uint8_t i = 0; i < moduleConfig.remote_hardware.available_pins_count; i++) {
|
|
if (!moduleConfig.remote_hardware.available_pins[i].gpio_pin) {
|
|
continue;
|
|
}
|
|
meshtastic_NodeRemoteHardwarePin nodePin = meshtastic_NodeRemoteHardwarePin_init_default;
|
|
nodePin.node_num = nodeDB->getNodeNum();
|
|
nodePin.pin = moduleConfig.remote_hardware.available_pins[i];
|
|
r.get_node_remote_hardware_pins_response.node_remote_hardware_pins[i + 12] = nodePin;
|
|
}
|
|
setPassKey(&r);
|
|
myReply = allocDataProtobuf(r);
|
|
if (req.pki_encrypted) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleGetDeviceMetadata(const meshtastic_MeshPacket &req)
|
|
{
|
|
meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default;
|
|
r.get_device_metadata_response = getDeviceMetadata();
|
|
r.which_payload_variant = meshtastic_AdminMessage_get_device_metadata_response_tag;
|
|
setPassKey(&r);
|
|
myReply = allocDataProtobuf(r);
|
|
if (req.pki_encrypted) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &req)
|
|
{
|
|
meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default;
|
|
|
|
meshtastic_DeviceConnectionStatus conn = meshtastic_DeviceConnectionStatus_init_zero;
|
|
|
|
#if HAS_WIFI
|
|
conn.has_wifi = true;
|
|
conn.wifi.has_status = true;
|
|
#ifdef ARCH_PORTDUINO
|
|
conn.wifi.status.is_connected = true;
|
|
#else
|
|
conn.wifi.status.is_connected = WiFi.status() == WL_CONNECTED;
|
|
#endif
|
|
strncpy(conn.wifi.ssid, config.network.wifi_ssid, 33);
|
|
if (conn.wifi.status.is_connected) {
|
|
conn.wifi.rssi = WiFi.RSSI();
|
|
conn.wifi.status.ip_address = WiFi.localIP();
|
|
#ifndef MESHTASTIC_EXCLUDE_MQTT
|
|
conn.wifi.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly();
|
|
#endif
|
|
conn.wifi.status.is_syslog_connected = false; // FIXME wire this up
|
|
}
|
|
#endif
|
|
|
|
#if HAS_ETHERNET && !defined(USE_WS5500)
|
|
conn.has_ethernet = true;
|
|
conn.ethernet.has_status = true;
|
|
if (Ethernet.linkStatus() == LinkON) {
|
|
conn.ethernet.status.is_connected = true;
|
|
conn.ethernet.status.ip_address = Ethernet.localIP();
|
|
#if !MESHTASTIC_EXCLUDE_MQTT
|
|
conn.ethernet.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly();
|
|
#endif
|
|
conn.ethernet.status.is_syslog_connected = false; // FIXME wire this up
|
|
} else {
|
|
conn.ethernet.status.is_connected = false;
|
|
}
|
|
#endif
|
|
|
|
#if HAS_BLUETOOTH
|
|
conn.has_bluetooth = true;
|
|
conn.bluetooth.pin = config.bluetooth.fixed_pin;
|
|
#ifdef ARCH_ESP32
|
|
if (config.bluetooth.enabled && nimbleBluetooth) {
|
|
conn.bluetooth.is_connected = nimbleBluetooth->isConnected();
|
|
conn.bluetooth.rssi = nimbleBluetooth->getRssi();
|
|
}
|
|
#elif defined(ARCH_NRF52)
|
|
if (config.bluetooth.enabled && nrf52Bluetooth) {
|
|
conn.bluetooth.is_connected = nrf52Bluetooth->isConnected();
|
|
}
|
|
#endif
|
|
#endif
|
|
conn.has_serial = true; // No serial-less devices
|
|
#if !MESHTASTIC_EXCLUDE_POWER_FSM
|
|
conn.serial.is_connected = powerFSM.getState() == &stateSERIAL;
|
|
#else
|
|
conn.serial.is_connected = powerFSM.getState();
|
|
#endif
|
|
conn.serial.baud = SERIAL_BAUD;
|
|
|
|
r.get_device_connection_status_response = conn;
|
|
r.which_payload_variant = meshtastic_AdminMessage_get_device_connection_status_response_tag;
|
|
setPassKey(&r);
|
|
myReply = allocDataProtobuf(r);
|
|
if (req.pki_encrypted) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleGetChannel(const meshtastic_MeshPacket &req, uint32_t channelIndex)
|
|
{
|
|
if (req.decoded.want_response) {
|
|
// We create the reply here
|
|
meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default;
|
|
r.get_channel_response = channels.getByIndex(channelIndex);
|
|
r.which_payload_variant = meshtastic_AdminMessage_get_channel_response_tag;
|
|
setPassKey(&r);
|
|
myReply = allocDataProtobuf(r);
|
|
if (req.pki_encrypted) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleGetDeviceUIConfig(const meshtastic_MeshPacket &req)
|
|
{
|
|
meshtastic_AdminMessage r = meshtastic_AdminMessage_init_default;
|
|
r.which_payload_variant = meshtastic_AdminMessage_get_ui_config_response_tag;
|
|
r.get_ui_config_response = uiconfig;
|
|
myReply = allocDataProtobuf(r);
|
|
if (req.pki_encrypted) {
|
|
myReply->pki_encrypted = true;
|
|
}
|
|
}
|
|
|
|
void AdminModule::reboot(int32_t seconds)
|
|
{
|
|
LOG_INFO("Reboot in %d seconds", seconds);
|
|
if (screen)
|
|
screen->showSimpleBanner("Rebooting...", 0); // stays on screen
|
|
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
|
|
}
|
|
|
|
void AdminModule::saveChanges(int saveWhat, bool shouldReboot)
|
|
{
|
|
if (!hasOpenEditTransaction) {
|
|
LOG_INFO("Save changes to disk");
|
|
service->reloadConfig(saveWhat); // Calls saveToDisk among other things
|
|
} else {
|
|
LOG_INFO("Delay save of changes to disk until the open transaction is committed");
|
|
}
|
|
if (shouldReboot && !hasOpenEditTransaction) {
|
|
reboot(DEFAULT_REBOOT_SECONDS);
|
|
}
|
|
}
|
|
|
|
void AdminModule::handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg)
|
|
{
|
|
nodeDB->saveProto("/prefs/uiconfig.proto", meshtastic_DeviceUIConfig_size, &meshtastic_DeviceUIConfig_msg, &uicfg);
|
|
}
|
|
|
|
void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
|
{
|
|
// Validate ham parameters before setting since this would bypass validation in the owner struct
|
|
if (*p.call_sign) {
|
|
const char *start = p.call_sign;
|
|
// Skip all whitespace
|
|
while (*start && isspace((unsigned char)*start))
|
|
start++;
|
|
if (*start == '\0') {
|
|
LOG_WARN("Rejected ham call_sign: must contain at least 1 non-whitespace character");
|
|
return;
|
|
}
|
|
}
|
|
if (*p.short_name) {
|
|
const char *start = p.short_name;
|
|
while (*start && isspace((unsigned char)*start))
|
|
start++;
|
|
if (*start == '\0') {
|
|
LOG_WARN("Rejected ham short_name: must contain at least 1 non-whitespace character");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Set call sign and override lora limitations for licensed use
|
|
strncpy(owner.long_name, p.call_sign, sizeof(owner.long_name));
|
|
strncpy(owner.short_name, p.short_name, sizeof(owner.short_name));
|
|
owner.is_licensed = true;
|
|
config.lora.override_duty_cycle = true;
|
|
config.lora.tx_power = p.tx_power;
|
|
config.lora.override_frequency = p.frequency;
|
|
// Set node info broadcast interval to 10 minutes
|
|
// For FCC minimum call-sign announcement
|
|
config.device.node_info_broadcast_secs = 600;
|
|
|
|
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY;
|
|
// Remove PSK of primary channel for plaintext amateur usage
|
|
|
|
if (channels.ensureLicensedOperation()) {
|
|
sendWarning(licensedModeMessage);
|
|
}
|
|
channels.onConfigChanged();
|
|
|
|
service->reloadOwner(false);
|
|
saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS);
|
|
}
|
|
|
|
AdminModule::AdminModule() : ProtobufModule("Admin", meshtastic_PortNum_ADMIN_APP, &meshtastic_AdminMessage_msg)
|
|
{
|
|
// restrict to the admin channel for rx
|
|
// boundChannel = Channels::adminChannel;
|
|
}
|
|
|
|
void AdminModule::setPassKey(meshtastic_AdminMessage *res)
|
|
{
|
|
if (session_time == 0 || millis() / 1000 > session_time + 150) {
|
|
for (int i = 0; i < 8; i++) {
|
|
session_passkey[i] = random();
|
|
}
|
|
session_time = millis() / 1000;
|
|
}
|
|
memcpy(res->session_passkey.bytes, session_passkey, 8);
|
|
res->session_passkey.size = 8;
|
|
printBytes("Set admin key to ", res->session_passkey.bytes, 8);
|
|
// if halfway to session_expire, regenerate session_passkey, reset the timeout
|
|
// set the key in the packet
|
|
}
|
|
|
|
bool AdminModule::checkPassKey(meshtastic_AdminMessage *res)
|
|
{ // check that the key in the packet is still valid
|
|
printBytes("Incoming session key: ", res->session_passkey.bytes, 8);
|
|
printBytes("Expected session key: ", session_passkey, 8);
|
|
return (session_time + 300 > millis() / 1000 && res->session_passkey.size == 8 &&
|
|
memcmp(res->session_passkey.bytes, session_passkey, 8) == 0);
|
|
}
|
|
|
|
bool AdminModule::messageIsResponse(const meshtastic_AdminMessage *r)
|
|
{
|
|
if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_owner_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_config_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_module_config_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_response_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_response_tag)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
bool AdminModule::messageIsRequest(const meshtastic_AdminMessage *r)
|
|
{
|
|
if (r->which_payload_variant == meshtastic_AdminMessage_get_channel_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_owner_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_config_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_module_config_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_canned_message_module_messages_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_device_metadata_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_ringtone_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_device_connection_status_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_node_remote_hardware_pins_request_tag ||
|
|
r->which_payload_variant == meshtastic_AdminMessage_get_ui_config_request_tag)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent)
|
|
{
|
|
LOG_DEBUG("Processing input event: event_code=%u, kb_char=%u, touch_x=%u, touch_y=%u", inputEvent.event_code,
|
|
inputEvent.kb_char, inputEvent.touch_x, inputEvent.touch_y);
|
|
|
|
// Create InputEvent for injection
|
|
InputEvent event = {.inputEvent = (input_broker_event)inputEvent.event_code,
|
|
.kbchar = (unsigned char)inputEvent.kb_char,
|
|
.touchX = inputEvent.touch_x,
|
|
.touchY = inputEvent.touch_y};
|
|
|
|
// Log the event being injected
|
|
LOG_INFO("Injecting input event from admin: source=%s, event=%u, char=%c(%u), touch=(%u,%u)", event.source, event.inputEvent,
|
|
(event.kbchar >= 32 && event.kbchar <= 126) ? event.kbchar : '?', event.kbchar, event.touchX, event.touchY);
|
|
|
|
// Wake the device if asleep
|
|
powerFSM.trigger(EVENT_INPUT);
|
|
#if !defined(MESHTASTIC_EXCLUDE_INPUTBROKER)
|
|
// Inject the event through InputBroker
|
|
if (inputBroker) {
|
|
inputBroker->injectInputEvent(&event);
|
|
} else {
|
|
LOG_ERROR("InputBroker not available for event injection");
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void AdminModule::sendWarning(const char *format, ...)
|
|
{
|
|
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
|
if (!cn)
|
|
return;
|
|
|
|
cn->level = meshtastic_LogRecord_Level_WARNING;
|
|
cn->time = getValidTime(RTCQualityFromNet);
|
|
|
|
va_list args;
|
|
va_start(args, format);
|
|
// Format the arguments directly into the notification object
|
|
vsnprintf(cn->message, sizeof(cn->message), format, args);
|
|
va_end(args);
|
|
|
|
service->sendClientNotification(cn);
|
|
}
|
|
|
|
void AdminModule::sendWarningAndLog(const char *format, ...)
|
|
{
|
|
// We need a temporary buffer to hold the formatted text so we can log it
|
|
// Using 250 bytes as a safe upper limit for typical text notifications
|
|
char buf[250];
|
|
|
|
va_list args;
|
|
va_start(args, format);
|
|
vsnprintf(buf, sizeof(buf), format, args);
|
|
va_end(args);
|
|
|
|
LOG_WARN(buf);
|
|
// 2. Call sendWarning
|
|
// SECURITY NOTE: We pass "%s", buf instead of just 'buf'.
|
|
// If 'buf' contained a % symbol (e.g. "Battery 50%"), passing it directly
|
|
// would crash sendWarning. "%s" treats it purely as text.
|
|
sendWarning("%s", buf);
|
|
}
|
|
|
|
void disableBluetooth()
|
|
{
|
|
#if HAS_BLUETOOTH
|
|
#ifdef ARCH_ESP32
|
|
if (nimbleBluetooth)
|
|
nimbleBluetooth->deinit();
|
|
#elif defined(ARCH_NRF52)
|
|
if (nrf52Bluetooth)
|
|
nrf52Bluetooth->shutdown();
|
|
#endif
|
|
#endif
|
|
}
|