diff --git a/src/main.cpp b/src/main.cpp index 33aed0ca4d..fff0290bb2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1123,6 +1123,7 @@ void setup() meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_LOW_ENTROPY_KEY_REGENERATED; cn->time = getValidTime(RTCQualityFromNet); sprintf(cn->message, LOW_ENTROPY_WARNING); service->sendClientNotification(cn); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 06354ba443..654b3a6cf2 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -3995,6 +3995,7 @@ bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelInde meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_DUPLICATED_PUBLIC_KEY; cn->time = getValidTime(RTCQualityFromNet); snprintf(cn->message, sizeof(cn->message), warning, safeName); service->sendClientNotification(cn); @@ -4925,6 +4926,7 @@ bool NodeDB::notifyPendingLicensedIdentityMigration() if (!notification) return false; notification->level = meshtastic_LogRecord_Level_WARNING; + notification->type = meshtastic_ClientNotification_NotificationType_LICENSED_IDENTITY_CHANGED; notification->time = getValidTime(RTCQualityFromNet); snprintf(notification->message, sizeof(notification->message), "%s", LICENSED_IDENTITY_MIGRATION_WARNING); service->sendClientNotification(notification); diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index a632817875..19566b69fe 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -1750,14 +1750,16 @@ bool PhoneAPI::available() return false; } -void PhoneAPI::sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message) +void PhoneAPI::sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message, + meshtastic_ClientNotification_NotificationType type) { meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (!cn) return; cn->has_reply_id = true; cn->reply_id = replyId; - cn->level = meshtastic_LogRecord_Level_WARNING; + cn->level = level; + cn->type = type; cn->time = getValidTime(RTCQualityFromNet); strncpy(cn->message, message, sizeof(cn->message)); service->sendClientNotification(cn); @@ -1863,7 +1865,8 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) LOG_DEBUG("Suppress phone coordinate send on event (everyone) channel"); meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); - sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Location sharing is disabled on this channel"); + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Location sharing is disabled on this channel", + meshtastic_ClientNotification_NotificationType_POSITION_SHARING_DISABLED); return false; } @@ -1879,12 +1882,14 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p) if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && lastPortNumToRadio[p.decoded.portnum] && Throttle::isWithinTimespanMs(lastPortNumToRadio[p.decoded.portnum], THIRTY_SECONDS_MS)) { LOG_WARN("Rate limit portnum %d", p.decoded.portnum); - sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "TraceRoute can only be sent once every 30 seconds"); + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "TraceRoute can only be sent once every 30 seconds", + meshtastic_ClientNotification_NotificationType_TRACEROUTE_RATE_LIMITED); meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); return false; } else if (p.decoded.portnum == meshtastic_PortNum_TRACEROUTE_APP && isBroadcast(p.to) && p.hop_limit > 0) { - sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed"); + sendNotification(meshtastic_LogRecord_Level_WARNING, p.id, "Multi-hop traceroute to broadcast address is not allowed", + meshtastic_ClientNotification_NotificationType_TRACEROUTE_BROADCAST_REJECTED); meshtastic_QueueStatus qs = router->getQueueStatus(); service->sendQueueStatusToPhone(qs, 0, p.id); return false; diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 0e68fe9b3c..b4e76abffe 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -156,7 +156,8 @@ class PhoneAPI /** * Send a (client)notification to the phone */ - virtual void sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message); + virtual void sendNotification(meshtastic_LogRecord_Level level, uint32_t replyId, const char *message, + meshtastic_ClientNotification_NotificationType type); /** * Get the next packet we want to send to the phone diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index eaeaf576e3..7a84b89cd4 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -1033,7 +1033,8 @@ uint32_t RadioInterface::getChannelNum() /** * Send a client notification (error level unless specified). Safe to call when service is null (e.g. in tests). */ -static void sendErrorNotification(const char *msg, meshtastic_LogRecord_Level level = meshtastic_LogRecord_Level_ERROR) +static void sendErrorNotification(const char *msg, meshtastic_ClientNotification_NotificationType type, + meshtastic_LogRecord_Level level = meshtastic_LogRecord_Level_ERROR) { if (!service) return; @@ -1041,6 +1042,7 @@ static void sendErrorNotification(const char *msg, meshtastic_LogRecord_Level le if (!cn) return; cn->level = level; + cn->type = type; snprintf(cn->message, sizeof(cn->message), "%s", msg); service->sendClientNotification(cn); } @@ -1067,12 +1069,12 @@ const RegionInfo *RadioInterface::regionSwapForPreset(meshtastic_Config_LoRaConf /** * Checks if a region is valid for the current settings, with no side effects. - * Safe to call speculatively (e.g. from UI pickers). When errBuf is given, it - * receives the human-readable failure reason. + * Safe to call speculatively (e.g. from UI pickers). When errBuf/errType are given, + * they receive the human-readable failure reason and its notification type. * Returns false if not compatible. */ bool RadioInterface::checkConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig, char *errBuf, size_t errLen, - bool prospectiveLicensedOwner) + bool prospectiveLicensedOwner, meshtastic_ClientNotification_NotificationType *errType) { const RegionInfo *newRegion = getRegion(loraConfig.region); @@ -1080,6 +1082,8 @@ bool RadioInterface::checkConfigRegion(const meshtastic_Config_LoRaConfig &loraC if (newRegion->code != loraConfig.region) { if (errBuf) snprintf(errBuf, errLen, "Region code %d is not recognized", loraConfig.region); + if (errType) + *errType = meshtastic_ClientNotification_NotificationType_LORA_REGION_UNRECOGNIZED; return false; } @@ -1087,6 +1091,8 @@ bool RadioInterface::checkConfigRegion(const meshtastic_Config_LoRaConfig &loraC if (newRegion->profile->licensedOnly && !devicestate.owner.is_licensed && !prospectiveLicensedOwner) { if (errBuf) snprintf(errBuf, errLen, "Region %s requires licensed mode", newRegion->name); + if (errType) + *errType = meshtastic_ClientNotification_NotificationType_LORA_REGION_REQUIRES_LICENSE; return false; } @@ -1103,6 +1109,8 @@ bool RadioInterface::checkConfigRegion(const meshtastic_Config_LoRaConfig &loraC if (unsupported) { if (errBuf) snprintf(errBuf, errLen, "Region %s needs %s, which this radio does not support", newRegion->name, unsupported); + if (errType) + *errType = meshtastic_ClientNotification_NotificationType_LORA_REGION_UNSUPPORTED_BY_RADIO; return false; } } @@ -1118,12 +1126,13 @@ bool RadioInterface::checkConfigRegion(const meshtastic_Config_LoRaConfig &loraC bool RadioInterface::validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig) { char err_string[160]; - if (checkConfigRegion(loraConfig, err_string, sizeof(err_string))) + meshtastic_ClientNotification_NotificationType errType = meshtastic_ClientNotification_NotificationType_UNSET; + if (checkConfigRegion(loraConfig, err_string, sizeof(err_string), false, &errType)) return true; LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - sendErrorNotification(err_string); + sendErrorNotification(err_string, errType); return false; } @@ -1162,7 +1171,8 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo snprintf(err_string, sizeof(err_string), "Preset %s swaps region %s to %s", presetName, newRegion->name, swapRegion->name); LOG_INFO("%s", err_string); - sendErrorNotification(err_string, meshtastic_LogRecord_Level_INFO); + sendErrorNotification(err_string, meshtastic_ClientNotification_NotificationType_LORA_PRESET_REGION_SWAP, + meshtastic_LogRecord_Level_INFO); loraConfig.region = swapRegion->code; newRegion = swapRegion; @@ -1180,7 +1190,7 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo } LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - sendErrorNotification(err_string); + sendErrorNotification(err_string, meshtastic_ClientNotification_NotificationType_LORA_PRESET_INVALID_FOR_REGION); if (clamp) { loraConfig.modem_preset = newRegion->getDefaultPreset(); @@ -1206,7 +1216,7 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo snprintf(err_string, sizeof(err_string), "%s span %.0fkHz < requested %.0fkHz", newRegion->name, regionSpanKHz, check_bw); LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - sendErrorNotification(err_string); + sendErrorNotification(err_string, meshtastic_ClientNotification_NotificationType_LORA_BANDWIDTH_EXCEEDS_REGION); if (clamp) { loraConfig.bandwidth = bwKHzToCode(modemPresetToBwKHz(newRegion->getDefaultPreset(), newRegion->wideLora)); @@ -1248,7 +1258,7 @@ bool RadioInterface::checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraCo newRegion->name, numFreqSlots); LOG_ERROR("%s", err_string); RECORD_CRITICALERROR(meshtastic_CriticalErrorCode_INVALID_RADIO_SETTING); - sendErrorNotification(err_string); + sendErrorNotification(err_string, meshtastic_ClientNotification_NotificationType_LORA_CHANNEL_NUM_INVALID); if (clamp) { if (uses_custom_channel_name) { // clamp to channel name hash diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index 33aee9f3fc..124b750767 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -278,9 +278,10 @@ class RadioInterface // Check if a candidate region is compatible and valid, with no side effects (safe for // speculative UI checks). prospectiveLicensedOwner is for a UI flow that requires - // confirmation before it sets the owner licensed. errBuf, if given, receives the failure reason. + // confirmation before it sets the owner licensed. errBuf and errType, if given, receive the failure reason. static bool checkConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig, char *errBuf = nullptr, size_t errLen = 0, - bool prospectiveLicensedOwner = false); + bool prospectiveLicensedOwner = false, + meshtastic_ClientNotification_NotificationType *errType = nullptr); // Check if a candidate region is compatible and valid. On failure, logs at ERROR, // records a critical error, and sends a client notification. diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index a445df6df4..cc35ff8cc0 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -505,6 +505,7 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) cn->has_reply_id = true; cn->reply_id = p->id; cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_DUTY_CYCLE_LIMIT_EXCEEDED; cn->time = getValidTime(RTCQualityFromNet); snprintf(cn->message, sizeof(cn->message), "Duty cycle limit exceeded. You can send again in %d mins", silentMinutes); diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index 0e2ded0564..6b1a9091a5 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -390,7 +390,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (r->ota_request.ota_hash.size != 32) { suppressRebootBanner = true; - sendWarningAndLog("Cannot start OTA: Invalid `ota_hash` provided."); + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_OTA_INVALID_HASH, + "Cannot start OTA: Invalid `ota_hash` provided."); break; } @@ -401,20 +402,23 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta const esp_partition_t *part = MeshtasticOTA::getAppPartition(); if (part == NULL) { suppressRebootBanner = true; - sendWarningAndLog("Cannot start OTA: Cannot find OTA Loader partition."); + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_OTA_LOADER_PARTITION_MISSING, + "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."); + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_OTA_LOADER_INVALID, + "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); + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_OTA_MODE_UNSUPPORTED, + "OTA Loader does not support %s", mode_name); break; } @@ -425,9 +429,10 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta screen->startFirmwareUpdateScreen(); } MeshtasticOTA::saveConfig(&config.network, mode, r->ota_request.ota_hash.bytes); - sendWarningAndLog("Rebooting to %s OTA", mode_name); + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_OTA_REBOOTING, "Rebooting to %s OTA", mode_name); } else { - sendWarningAndLog("Unable to switch to the OTA partition."); + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_OTA_PARTITION_SWITCH_FAILED, + "Unable to switch to the OTA partition."); } #endif int s = 1; // Reboot in 1 second, hard coded @@ -528,7 +533,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens } else if (mp.from == 0) { // local request from the phone - tell the user why it didn't take - sendWarning(NodeDB::PROTECTED_CAP_WARN_FMT, "favorite", r->set_favorite_node, MAX_NUM_NODES - 2); + sendWarning(meshtastic_ClientNotification_NotificationType_PROTECTED_NODE_LIMIT_REACHED, + NodeDB::PROTECTED_CAP_WARN_FMT, "favorite", r->set_favorite_node, MAX_NUM_NODES - 2); } else { LOG_WARN("Remote set_favorite_node for 0x%08x refused: protected-node cap", r->set_favorite_node); } @@ -564,7 +570,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta screen->setFrames(graphics::Screen::FOCUS_PRESERVE); #endif } else if (mp.from == 0) { // local request from the phone - tell the user why it didn't take - sendWarning(NodeDB::PROTECTED_CAP_WARN_FMT, "ignore", r->set_ignored_node, MAX_NUM_NODES - 2); + sendWarning(meshtastic_ClientNotification_NotificationType_PROTECTED_NODE_LIMIT_REACHED, + NodeDB::PROTECTED_CAP_WARN_FMT, "ignore", r->set_ignored_node, MAX_NUM_NODES - 2); } else { LOG_WARN("Remote set_ignored_node for 0x%08x refused: protected-node cap", r->set_ignored_node); } @@ -813,7 +820,8 @@ void AdminModule::handleSetOwner(const meshtastic_User &o) const bool identityWillMigrate = o.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET && licensedIdentityWillMigrate(); if (identityWillMigrate) - sendWarning(licensedIdentityMigrationMessage); + sendWarning(meshtastic_ClientNotification_NotificationType_LICENSED_IDENTITY_WILL_CHANGE, + licensedIdentityMigrationMessage); #endif owner.is_licensed = o.is_licensed; if (channels.ensureLicensedOperation()) { @@ -912,7 +920,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) 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); + sendWarning(meshtastic_ClientNotification_NotificationType_ROUTER_REBROADCAST_NONE_REJECTED, warning); } // If we're setting router role for the first time, install its intervals if (existingRole != c.payload_variant.device.role) { @@ -1061,7 +1069,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) if (owner.is_licensed && isRegionUnset && validatedLora.region > meshtastic_Config_LoRaConfig_RegionCode_UNSET) { const bool identityWillMigrate = licensedIdentityWillMigrate(); if (identityWillMigrate) - sendWarning(licensedIdentityMigrationMessage); + sendWarning(meshtastic_ClientNotification_NotificationType_LICENSED_IDENTITY_WILL_CHANGE, + licensedIdentityMigrationMessage); nodeDB->generateCryptoKeyPair(); changes |= SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE; if (identityWillMigrate) @@ -1206,7 +1215,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) meshtastic_Config_SecurityConfig_PacketSignaturePolicy_PACKET_SIGNATURE_POLICY_COMPATIBLE; const char *warning = "Packet authenticity policy is unavailable on this firmware build"; LOG_WARN(warning); - sendWarning(warning); + sendWarning(meshtastic_ClientNotification_NotificationType_PACKET_SIGNATURE_POLICY_UNAVAILABLE, warning); } #endif config.security = incoming; @@ -1226,7 +1235,8 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) const bool keygenSucceeded = nodeDB->generateCryptoKeyPair(priorPrivateKey); const bool keyWasReplaced = memcmp(priorPrivateKey, config.security.private_key.bytes, 32) != 0; if (keygenSucceeded && keyWasReplaced && nodeDB->keyIsLowEntropy) { - sendWarning(LOW_ENTROPY_RESTORE_WARNING); + sendWarning(meshtastic_ClientNotification_NotificationType_LOW_ENTROPY_KEY_RESTORE_REJECTED, + LOW_ENTROPY_RESTORE_WARNING); } } #endif @@ -1234,7 +1244,7 @@ void AdminModule::handleSetConfig(const meshtastic_Config &c, bool fromOthers) 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); + sendWarning(meshtastic_ClientNotification_NotificationType_MANAGED_MODE_REQUIRES_ADMIN_KEY, warning); } changes = SEGMENT_CONFIG | SEGMENT_DEVICESTATE | SEGMENT_NODEDATABASE; @@ -1431,7 +1441,8 @@ void AdminModule::handleSetChannel(const meshtastic_Channel &cc) } } if (clamped) - sendWarning(publicChannelPrecisionMessage); + sendWarning(meshtastic_ClientNotification_NotificationType_PUBLIC_CHANNEL_POSITION_PRECISION_REDUCED, + publicChannelPrecisionMessage); saveChanges(SEGMENT_CHANNELS, false); warnOnChannelSet(channels.getByIndex(cc.index)); // passes the saved channel // Inside an edit transaction the queued warnings are flushed once at commit; otherwise emit now. @@ -1930,7 +1941,8 @@ bool AdminModule::handleSetHamMode(const meshtastic_HamParameters &p) if (config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) { const bool identityWillMigrate = licensedIdentityWillMigrate(); if (identityWillMigrate) - sendWarning(licensedIdentityMigrationMessage); + sendWarning(meshtastic_ClientNotification_NotificationType_LICENSED_IDENTITY_WILL_CHANGE, + licensedIdentityMigrationMessage); nodeDB->generateCryptoKeyPair(); if (identityWillMigrate) nodeDB->licensedIdentityMigrationPending = false; @@ -2174,13 +2186,14 @@ void AdminModule::handleSendInputEvent(const meshtastic_AdminMessage_InputEvent #endif } -void AdminModule::sendWarning(const char *format, ...) +void AdminModule::sendWarning(meshtastic_ClientNotification_NotificationType type, const char *format, ...) { meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (!cn) return; cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = type; cn->time = getValidTime(RTCQualityFromNet); va_list args; @@ -2192,7 +2205,7 @@ void AdminModule::sendWarning(const char *format, ...) service->sendClientNotification(cn); } -void AdminModule::sendWarningAndLog(const char *format, ...) +void AdminModule::sendWarningAndLog(meshtastic_ClientNotification_NotificationType type, 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 @@ -2208,7 +2221,7 @@ void AdminModule::sendWarningAndLog(const char *format, ...) // user-supplied channel name like "50%"), passing it as the format string would // read bogus varargs and could crash. "%s" treats it purely as text. LOG_WARN("%s", buf); - sendWarning("%s", buf); + sendWarning(type, "%s", buf); } // Strip spaces and fold to lowercase for loose preset-name comparison. @@ -2225,9 +2238,11 @@ static void normalizePresetName(const char *src, char *dst, size_t dstLen) // Record one channel-configuration warning. The first message is kept verbatim in case it // turns out to be the only one; nameIssue/pskIssue and the channel bitmask feed the catch-all // wording if more than one channel ends up flagged. flushChannelWarnings() emits the result. -void AdminModule::queueChannelWarning(uint8_t channelIndex, bool nameIssue, bool pskIssue, const char *format, ...) +void AdminModule::queueChannelWarning(uint8_t channelIndex, bool nameIssue, bool pskIssue, + meshtastic_ClientNotification_NotificationType type, const char *format, ...) { if (pendingWarningCount == 0) { + pendingWarningType = type; va_list args; va_start(args, format); vsnprintf(pendingWarningText, sizeof(pendingWarningText), format, args); @@ -2247,7 +2262,7 @@ void AdminModule::warnLicensedMode() if (hasOpenEditTransaction) pendingLicenseWarning = true; else - sendWarning(licensedModeMessage); + sendWarning(meshtastic_ClientNotification_NotificationType_LICENSED_MODE_ACTIVATED, licensedModeMessage); } // Emit the coalesced channel warning(s): nothing if none queued, the lone message verbatim if @@ -2256,10 +2271,10 @@ void AdminModule::warnLicensedMode() void AdminModule::flushChannelWarnings() { if (pendingLicenseWarning) - sendWarning(licensedModeMessage); + sendWarning(meshtastic_ClientNotification_NotificationType_LICENSED_MODE_ACTIVATED, licensedModeMessage); if (pendingWarningCount == 1) { - sendWarningAndLog("%s", pendingWarningText); + sendWarningAndLog(pendingWarningType, "%s", pendingWarningText); } else if (pendingWarningCount > 1) { char list[48] = {}; for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) { @@ -2270,13 +2285,17 @@ void AdminModule::flushChannelWarnings() strncat(list, num, sizeof(list) - strlen(list) - 1); } if (pendingWarningNameIssue && pendingWarningPskIssue) - sendWarningAndLog("There may be name and PSK issues on channels %s", list); // max 60 bytes + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_AND_PSK_ISSUES, + "There may be name and PSK issues on channels %s", list); // max 60 bytes else if (pendingWarningNameIssue) - sendWarningAndLog("There may be name issues on channels %s", list); // max 52 bytes + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_ISSUES, + "There may be name issues on channels %s", list); // max 52 bytes else - sendWarningAndLog("There may be PSK issues on channels %s", list); // max 51 bytes + sendWarningAndLog(meshtastic_ClientNotification_NotificationType_CHANNEL_PSK_ISSUES, + "There may be PSK issues on channels %s", list); // max 51 bytes } pendingWarningText[0] = '\0'; + pendingWarningType = meshtastic_ClientNotification_NotificationType_UNSET; pendingWarningChannels = 0; pendingWarningCount = 0; pendingWarningNameIssue = false; @@ -2331,14 +2350,15 @@ void AdminModule::warnOnChannelSet(const meshtastic_Channel &cc) } // At most one warning: collapse blank-PSK + mistype into a single catch-all. if (blankPsk && mistypePreset) - queueChannelWarning(cc.index, true, true, "There may be name and PSK issues on channel %d", cc.index); + queueChannelWarning(cc.index, true, true, meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_AND_PSK_ISSUES, + "There may be name and PSK issues on channel %d", cc.index); else if (mistypePreset) - queueChannelWarning(cc.index, true, false, + queueChannelWarning(cc.index, true, false, meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_PRESET_MISTYPE, "Channel %d name '%s' looks like a mistype of '%s' - " "make sure to type it exactly!", // max 90 bytes cc.index, cc.settings.name, mistypePreset); else if (blankPsk) - queueChannelWarning(cc.index, false, true, + queueChannelWarning(cc.index, false, true, meshtastic_ClientNotification_NotificationType_CHANNEL_BLANK_PSK, "Channel %d '%s' has a blank PSK (no encryption) instead of default key", // max 100 bytes cc.index, cc.settings.name); return; @@ -2353,11 +2373,12 @@ void AdminModule::warnOnChannelSet(const meshtastic_Channel &cc) if (!*cc.settings.name) { // Blank name resolves to the preset name - A and B are mutually exclusive (psk.size can't be both 0 and >0) if (blankPsk) - queueChannelWarning(cc.index, false, true, + queueChannelWarning(cc.index, false, true, meshtastic_ClientNotification_NotificationType_CHANNEL_BLANK_PSK, "Channel %d '%s' has a blank PSK (no encryption) instead of default key", // max 100 bytes cc.index, cc.settings.name); else if (!isDefaultKey && cc.settings.psk.size > 0) queueChannelWarning(cc.index, false, true, + meshtastic_ClientNotification_NotificationType_CHANNEL_PRESET_NON_DEFAULT_KEY, "Channel %d will resolve to preset '%s' but uses a non-default key - " "default-key nodes can't decode it.", // max 102 bytes cc.index, presetName); @@ -2366,7 +2387,7 @@ void AdminModule::warnOnChannelSet(const meshtastic_Channel &cc) if (strcmp(normChan, normPreset) != 0) { // name unrelated to preset if (blankPsk) - queueChannelWarning(cc.index, false, true, + queueChannelWarning(cc.index, false, true, meshtastic_ClientNotification_NotificationType_CHANNEL_BLANK_PSK, "Channel %d '%s' has a blank PSK (no encryption) instead of default key", // max 100 bytes cc.index, cc.settings.name); return; @@ -2380,25 +2401,29 @@ void AdminModule::warnOnChannelSet(const meshtastic_Channel &cc) bool hasNameIssue = variantName; bool hasPskIssue = blankPsk || keyMismatch; if (hasNameIssue && hasPskIssue) - queueChannelWarning(cc.index, true, true, "There may be name and PSK issues on channel %d", cc.index); + queueChannelWarning(cc.index, true, true, meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_AND_PSK_ISSUES, + "There may be name and PSK issues on channel %d", cc.index); else if (hasNameIssue) - queueChannelWarning(cc.index, true, false, "There may be name issues on channel %d", cc.index); + queueChannelWarning(cc.index, true, false, meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_ISSUES, + "There may be name issues on channel %d", cc.index); else - queueChannelWarning(cc.index, false, true, "There may be PSK issues on channel %d", cc.index); + queueChannelWarning(cc.index, false, true, meshtastic_ClientNotification_NotificationType_CHANNEL_PSK_ISSUES, + "There may be PSK issues on channel %d", cc.index); return; } if (blankPsk) - queueChannelWarning(cc.index, false, true, + queueChannelWarning(cc.index, false, true, meshtastic_ClientNotification_NotificationType_CHANNEL_BLANK_PSK, "Channel %d '%s' has a blank PSK (no encryption) instead of default key", // max 100 bytes cc.index, cc.settings.name); if (variantName) queueChannelWarning(cc.index, true, false, + meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_PRESET_MISTYPE_CLEAR_NAME, "Channel %d name '%s' looks like a mistype of '%s' - " "clear the name to use the preset name automatically.", // max 113 bytes cc.index, cc.settings.name, presetName); if (keyMismatch) - queueChannelWarning(cc.index, false, true, + queueChannelWarning(cc.index, false, true, meshtastic_ClientNotification_NotificationType_CHANNEL_PRESET_NON_DEFAULT_KEY, "Channel %d '%s' matches preset '%s' but uses a non-default key - " "default-key nodes can't decode it.", // max 108 bytes cc.index, cc.settings.name, presetName); @@ -2444,12 +2469,12 @@ void AdminModule::warnOnLoraPresetChange(const meshtastic_Config_LoRaConfig &old char normChan[32]; normalizePresetName(ch.settings.name, normChan, sizeof(normChan)); if (*normOld && strcmp(normChan, normOld) == 0) - queueChannelWarning(i, true, false, + queueChannelWarning(i, true, false, meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_MATCHES_OLD_PRESET, "Channel %d name '%s' matches the old preset. " "Rename it manually if it should track the new preset.", // max 98 bytes i, ch.settings.name); else if (strcmp(normChan, normNew) == 0) - queueChannelWarning(i, true, false, + queueChannelWarning(i, true, false, meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_WONT_AUTO_RESOLVE, "Channel %d '%s' looks like preset '%s' but won't auto-resolve - " "clear the name to fix it.", // max 98 bytes i, ch.settings.name, newName); diff --git a/src/modules/AdminModule.h b/src/modules/AdminModule.h index 0fbc8ac3b1..aa860d382e 100644 --- a/src/modules/AdminModule.h +++ b/src/modules/AdminModule.h @@ -139,8 +139,10 @@ class AdminModule : public ProtobufModule, public Obser bool messageIsResponse(const meshtastic_AdminMessage *r); bool messageIsRequest(const meshtastic_AdminMessage *r); - void sendWarning(const char *format, ...) __attribute__((format(printf, 2, 3))); - void sendWarningAndLog(const char *format, ...) __attribute__((format(printf, 2, 3))); + void sendWarning(meshtastic_ClientNotification_NotificationType type, const char *format, ...) + __attribute__((format(printf, 3, 4))); + void sendWarningAndLog(meshtastic_ClientNotification_NotificationType type, const char *format, ...) + __attribute__((format(printf, 3, 4))); void warnOnLoraPresetChange(const meshtastic_Config_LoRaConfig &oldLora, const meshtastic_Config_LoRaConfig &newLora); void warnOnChannelSet(const meshtastic_Channel &cc); @@ -149,8 +151,9 @@ class AdminModule : public ProtobufModule, public Obser // is open (begin/commit_edit_settings) the warnings accumulate and flushChannelWarnings() // emits one combined message at commit. Outside a transaction the caller flushes // immediately, so a single channel/preset edit still produces a single message. - void queueChannelWarning(uint8_t channelIndex, bool nameIssue, bool pskIssue, const char *format, ...) - __attribute__((format(printf, 5, 6))); + void queueChannelWarning(uint8_t channelIndex, bool nameIssue, bool pskIssue, + meshtastic_ClientNotification_NotificationType type, const char *format, ...) + __attribute__((format(printf, 6, 7))); // Emit the "licensed mode activated" notice, deferring to commit during an edit transaction // so repeated triggers (e.g. owner + several channels) produce a single message. void warnLicensedMode(); @@ -162,6 +165,8 @@ class AdminModule : public ProtobufModule, public Obser bool pendingWarningNameIssue = false; // any queued warning was about a channel name bool pendingWarningPskIssue = false; // any queued warning was about a PSK bool pendingLicenseWarning = false; // a licensed-mode notice is queued for this transaction + // Notification type of the lone queued message. + meshtastic_ClientNotification_NotificationType pendingWarningType = meshtastic_ClientNotification_NotificationType_UNSET; }; static constexpr const char *licensedModeMessage = diff --git a/src/modules/KeyVerificationModule.cpp b/src/modules/KeyVerificationModule.cpp index 75c316ec38..2a6c6bc189 100644 --- a/src/modules/KeyVerificationModule.cpp +++ b/src/modules/KeyVerificationModule.cpp @@ -108,6 +108,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_KEY_VERIFICATION_NUMBER_REQUEST; snprintf(cn->message, sizeof(cn->message), "Enter Security Number for Key Verification"); cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_request_tag; cn->payload_variant.key_verification_number_request.nonce = currentNonce; @@ -144,6 +145,7 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket & meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_KEY_VERIFICATION_FINAL_INCOMING; snprintf(cn->message, sizeof(cn->message), "Final confirmation for incoming manual key verification %s", message); cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->payload_variant.key_verification_final.nonce = currentNonce; @@ -169,6 +171,7 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_KEY_VERIFICATION_LICENSED_UNAVAILABLE; snprintf(cn->message, sizeof(cn->message), "Key verification is unavailable in licensed (ham) mode"); service->sendClientNotification(cn); } @@ -318,6 +321,7 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply() meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_KEY_VERIFICATION_NUMBER_INFORM; snprintf(cn->message, sizeof(cn->message), "Incoming Key Verification.\nSecurity Number\n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000); cn->which_payload_variant = meshtastic_ClientNotification_key_verification_number_inform_tag; @@ -390,6 +394,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_KEY_VERIFICATION_FINAL_OUTGOING; snprintf(cn->message, sizeof(cn->message), "Final confirmation for outgoing manual key verification %s", message); cn->which_payload_variant = meshtastic_ClientNotification_key_verification_final_tag; cn->payload_variant.key_verification_final.nonce = currentNonce; diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index ae271e2939..baccd5bb0d 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -486,6 +486,7 @@ bool PositionModule::sendOurPosition(NodeNum dest, bool wantReplies, uint8_t cha meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); if (notification) { notification->level = meshtastic_LogRecord_Level_INFO; + notification->type = meshtastic_ClientNotification_NotificationType_POSITION_SENT_SLEEPING; notification->time = getValidTime(RTCQualityFromNet); sprintf(notification->message, "Sending position and sleeping for %us interval in a moment", Default::getConfiguredOrDefaultMs(config.position.position_broadcast_secs, default_broadcast_interval_secs) / diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index 6828a53f44..00686bfaec 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -63,6 +63,7 @@ bool serialConfigIsValid(const meshtastic_ModuleConfig_SerialConfig &config) meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_ERROR; + cn->type = meshtastic_ClientNotification_NotificationType_SERIAL_CONSOLE_OVERRIDE_INVALID; cn->time = getValidTime(RTCQualityFromNet); snprintf(cn->message, sizeof(cn->message), "%s", warning); service->sendClientNotification(cn); diff --git a/src/modules/Telemetry/AirQualityTelemetry.cpp b/src/modules/Telemetry/AirQualityTelemetry.cpp index bfb0ab73bb..03ab58a449 100644 --- a/src/modules/Telemetry/AirQualityTelemetry.cpp +++ b/src/modules/Telemetry/AirQualityTelemetry.cpp @@ -543,6 +543,7 @@ bool AirQualityTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); if (notification) { notification->level = meshtastic_LogRecord_Level_INFO; + notification->type = meshtastic_ClientNotification_NotificationType_TELEMETRY_SENT_SLEEPING; notification->time = getValidTime(RTCQualityFromNet); sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.air_quality_interval, diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 18323036b4..4ba4728dbf 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -869,6 +869,7 @@ bool EnvironmentTelemetryModule::sendTelemetry(NodeNum dest, bool phoneOnly) meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed(); if (notification) { notification->level = meshtastic_LogRecord_Level_INFO; + notification->type = meshtastic_ClientNotification_NotificationType_TELEMETRY_SENT_SLEEPING; notification->time = getValidTime(RTCQualityFromNet); sprintf(notification->message, "Sending telemetry and sleeping for %us interval in a moment", Default::getConfiguredOrDefaultMs(moduleConfig.telemetry.environment_update_interval, diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 26e9da6243..ea1d5dd713 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -675,6 +675,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_WARNING; + cn->type = meshtastic_ClientNotification_NotificationType_MQTT_SERVER_UNREACHABLE; cn->time = getValidTime(RTCQualityFromNet); strncpy(cn->message, warning, sizeof(cn->message) - 1); cn->message[sizeof(cn->message) - 1] = '\0'; @@ -691,6 +692,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_ERROR; + cn->type = meshtastic_ClientNotification_NotificationType_MQTT_PROXY_TO_CLIENT_REQUIRED; cn->time = getValidTime(RTCQualityFromNet); strncpy(cn->message, warning, sizeof(cn->message) - 1); cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination @@ -709,6 +711,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed(); if (cn) { cn->level = meshtastic_LogRecord_Level_ERROR; + cn->type = meshtastic_ClientNotification_NotificationType_MQTT_DEFAULT_SERVER_PORT_INVALID; cn->time = getValidTime(RTCQualityFromNet); strncpy(cn->message, warning, sizeof(cn->message) - 1); cn->message[sizeof(cn->message) - 1] = '\0'; // Ensure null termination diff --git a/test/test_admin_radio/test_main.cpp b/test/test_admin_radio/test_main.cpp index d45db0c59b..d50ef9a41d 100644 --- a/test/test_admin_radio/test_main.cpp +++ b/test/test_admin_radio/test_main.cpp @@ -9,6 +9,11 @@ * 4. RadioInterface::clampConfigLora() * 5. RegionInfo preset lists (PRESETS_STD, PRESETS_EU_868, PRESETS_UNDEF) * 6. Channel spacing calculation (placeholder for future protobuf changes) + * 7. ClientNotification.type on the paths that do not set it as a literal at the send site: + * AdminModule's channel-warning queue (queueChannelWarning()/flushChannelWarnings()) and + * RadioInterface::checkConfigRegion(), which hands its failure type to validateConfigRegion(). + * Clients key off the type to localize or act on a notification instead of matching its + * text, so a path that drops it sends UNSET and the client falls back to text matching. */ #include "Channels.h" @@ -43,6 +48,7 @@ extern uint32_t hash(const char *str); // set of messages produced by a sequence of admin messages. This shadows test/support/MockMeshService.h's // release-only stub because these tests need to inspect the captured message text, not just avoid leaks. static std::vector capturedWarnings; +static std::vector capturedTypes; // parallel to capturedWarnings class MockMeshService : public MeshService { @@ -50,6 +56,7 @@ class MockMeshService : public MeshService void sendClientNotification(meshtastic_ClientNotification *n) override { capturedWarnings.push_back(n->message); + capturedTypes.push_back(n->type); releaseClientNotificationToPool(n); } }; @@ -2287,6 +2294,120 @@ static void test_warn_license_transaction_coalescedToSingleMessage() TEST_ASSERT_EQUAL_INT(1, (int)capturedWarnings.size()); } +// ----------------------------------------------------------------------- +// ClientNotification.type tests +// +// A lone queued channel warning must keep the type it was queued with, including when it is +// held across an edit transaction; several warnings coalesced into one catch-all message must +// carry the matching CHANNEL_*_ISSUES type instead. Losing either sends the client UNSET, or a +// type that names the wrong fix. +// ----------------------------------------------------------------------- + +static void test_notificationType_loneChannelWarning_keepsItsType() +{ + usePresetLongFast(); + sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1)); + TEST_ASSERT_EQUAL_INT(1, (int)capturedTypes.size()); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_PRESET_MISTYPE_CLEAR_NAME, capturedTypes[0]); +} + +static void test_notificationType_transaction_loneChannelWarning_keepsItsType() +{ + usePresetLongFast(); + sendBeginEdit(); + sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1)); + sendCommitEdit(); + TEST_ASSERT_EQUAL_INT(1, (int)capturedTypes.size()); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_PRESET_MISTYPE_CLEAR_NAME, capturedTypes[0]); +} + +static void test_notificationType_transaction_coalescedWarnings_useCatchAllType() +{ + usePresetLongFast(); + sendBeginEdit(); + sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1)); + sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "long fast", DEFAULT_KEY, 1)); + sendCommitEdit(); + TEST_ASSERT_EQUAL_INT(1, (int)capturedTypes.size()); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_ISSUES, capturedTypes[0]); +} + +static void test_notificationType_transaction_coalescedPskWarnings_usePskCatchAllType() +{ + usePresetLongFast(); + sendBeginEdit(); + // Blank names resolve to the preset; a non-default key on each is a PSK issue only. + sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "", CUSTOM_KEY, 2)); + sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "", CUSTOM_KEY, 2)); + sendCommitEdit(); + TEST_ASSERT_EQUAL_INT(1, (int)capturedTypes.size()); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_CHANNEL_PSK_ISSUES, capturedTypes[0]); +} + +static void test_notificationType_transaction_mixedWarnings_useNameAndPskCatchAllType() +{ + usePresetLongFast(); + sendBeginEdit(); + sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "long fast", DEFAULT_KEY, 1)); // name issue + sendSetChannel(makeChannel(1, meshtastic_Channel_Role_SECONDARY, "", CUSTOM_KEY, 2)); // PSK issue + sendCommitEdit(); + TEST_ASSERT_EQUAL_INT(1, (int)capturedTypes.size()); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_CHANNEL_NAME_AND_PSK_ISSUES, capturedTypes[0]); +} + +static void test_notificationType_licensedMode_deferredNoticeKeepsItsType() +{ + usePresetLongFast(); + owner.is_licensed = true; + sendBeginEdit(); + sendSetChannel(makeChannel(0, meshtastic_Channel_Role_PRIMARY, "", CUSTOM_KEY, 2)); + sendCommitEdit(); + TEST_ASSERT_EQUAL_INT(1, (int)capturedTypes.size()); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_LICENSED_MODE_ACTIVATED, capturedTypes[0]); +} + +// checkConfigRegion() is the only place that knows why a region failed; validateConfigRegion() +// sends whatever type it reports, so a missing assignment there reaches the client as UNSET. +static void test_notificationType_checkConfigRegion_reportsFailureType() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + devicestate.owner.is_licensed = false; + meshtastic_ClientNotification_NotificationType type = meshtastic_ClientNotification_NotificationType_UNSET; + + cfg.region = (meshtastic_Config_LoRaConfig_RegionCode)254; + TEST_ASSERT_FALSE(RadioInterface::checkConfigRegion(cfg, nullptr, 0, false, &type)); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_LORA_REGION_UNRECOGNIZED, type); + + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M; + TEST_ASSERT_FALSE(RadioInterface::checkConfigRegion(cfg, nullptr, 0, false, &type)); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_LORA_REGION_REQUIRES_LICENSE, type); +} + +static void test_notificationType_validateConfigRegion_sendsFailureType() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M; + devicestate.owner.is_licensed = false; + + TEST_ASSERT_FALSE(RadioInterface::validateConfigRegion(cfg)); + TEST_ASSERT_EQUAL_INT(1, (int)capturedTypes.size()); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_LORA_REGION_REQUIRES_LICENSE, capturedTypes[0]); +} + +static void test_notificationType_clampConfigLora_channelNumOutOfRange() +{ + meshtastic_Config_LoRaConfig cfg = meshtastic_Config_LoRaConfig_init_zero; + cfg.region = meshtastic_Config_LoRaConfig_RegionCode_US; + cfg.use_preset = true; + cfg.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST; + cfg.channel_num = 5000; // far beyond the US slot count + + RadioInterface::clampConfigLora(cfg); + + TEST_ASSERT_EQUAL_INT(1, (int)capturedTypes.size()); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_LORA_CHANNEL_NUM_INVALID, capturedTypes[0]); +} + // ----------------------------------------------------------------------- // Node-DB admin metadata: favorite / ignore / mute // ----------------------------------------------------------------------- @@ -2514,6 +2635,7 @@ void setUp(void) service = mockMeshService; testAdmin = new AdminModuleTestShim(); capturedWarnings.clear(); + capturedTypes.clear(); // Every test gets its own NodeDB and its own copy of the globals the admin handlers write. replaceAdminRadioGlobals(); } @@ -2664,6 +2786,17 @@ void setup() RUN_TEST(test_warn_license_noTransaction_emittedImmediately); RUN_TEST(test_warn_license_transaction_coalescedToSingleMessage); + // ClientNotification.type + RUN_TEST(test_notificationType_loneChannelWarning_keepsItsType); + RUN_TEST(test_notificationType_transaction_loneChannelWarning_keepsItsType); + RUN_TEST(test_notificationType_transaction_coalescedWarnings_useCatchAllType); + RUN_TEST(test_notificationType_transaction_coalescedPskWarnings_usePskCatchAllType); + RUN_TEST(test_notificationType_transaction_mixedWarnings_useNameAndPskCatchAllType); + RUN_TEST(test_notificationType_licensedMode_deferredNoticeKeepsItsType); + RUN_TEST(test_notificationType_checkConfigRegion_reportsFailureType); + RUN_TEST(test_notificationType_validateConfigRegion_sendsFailureType); + RUN_TEST(test_notificationType_clampConfigLora_channelNumOutOfRange); + // Node-DB metadata saves must not reconfigure the radio RUN_TEST(test_setFavoriteNode_skipsRadioReload_butPersists); RUN_TEST(test_setIgnoredNode_skipsRadioReload_butPersists); diff --git a/test/test_event_channel_phone_api/test_main.cpp b/test/test_event_channel_phone_api/test_main.cpp index 6b13c6fd2b..1a76978368 100644 --- a/test/test_event_channel_phone_api/test_main.cpp +++ b/test/test_event_channel_phone_api/test_main.cpp @@ -252,6 +252,8 @@ static void test_event_position_ingress_does_not_poison_retry_state() mockService->assertQueueStatus(BLOCKED_PACKET_ID); TEST_ASSERT_EQUAL(1, mockService->notifications.size()); TEST_ASSERT_EQUAL_UINT32(BLOCKED_PACKET_ID, mockService->notifications[0].reply_id); + TEST_ASSERT_EQUAL(meshtastic_ClientNotification_NotificationType_POSITION_SHARING_DISABLED, + mockService->notifications[0].type); TEST_ASSERT_TRUE(sendToRadio(sameIdPrivateRetry)); TEST_ASSERT_EQUAL(1, mockRouter->sentPackets.size()); diff --git a/test/test_stream_api/test_main.cpp b/test/test_stream_api/test_main.cpp index 55139588b2..1039bf1238 100644 --- a/test/test_stream_api/test_main.cpp +++ b/test/test_stream_api/test_main.cpp @@ -414,7 +414,8 @@ void test_stream_api_finishes_pending_before_advancing_phone_api() ScriptedStream stream; StreamAPITestShim api(&stream); api.sendConfigComplete(); - api.sendNotification(meshtastic_LogRecord_Level_WARNING, 42, "still queued"); + api.sendNotification(meshtastic_LogRecord_Level_WARNING, 42, "still queued", + meshtastic_ClientNotification_NotificationType_UNSET); api.finishReady = false; api.runOncePart(nullptr, 0);