Set ClientNotification.type on every firmware notification (#12280)

* Set ClientNotification.type on every firmware notification

Every ClientNotification the firmware sends now carries the
NotificationType for its message, so clients can localize or act on it
without matching the text. `message` is unchanged.

The shared senders take the type as a required argument so a new call
site cannot omit it: PhoneAPI::sendNotification, AdminModule::sendWarning
/ sendWarningAndLog / queueChannelWarning, and RadioInterface's
sendErrorNotification. The channel-warning queue keeps the type of a
lone warning and uses CHANNEL_*_ISSUES for a coalesced one.
checkConfigRegion() gains an optional errType out-parameter so
validateConfigRegion() can send the reason's type.

NodeDB::notifyRefusedIdentity() (added in #12191) has no type yet and
stays UNSET until the protobufs add one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZVEy3QxYqj916trqLEsrb

* Fix AdminModule.h formatting

Move pendingWarningType below the aligned member block so clang-format
leaves the existing trailing-comment alignment alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZVEy3QxYqj916trqLEsrb

* Honor level in PhoneAPI::sendNotification; test catch-all types

sendNotification ignored its level argument and always sent WARNING.
Every caller passes WARNING today, so behavior is unchanged.

Add test_admin_radio cases for the CHANNEL_PSK_ISSUES and
CHANNEL_NAME_AND_PSK_ISSUES catch-all types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AZVEy3QxYqj916trqLEsrb

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jonathan BennettandClaude Opus 5.5 authored and GitHub committed 2026-11-04 00:34:20 +00:00
1 parent 24c6e139dc
commit 51ac6ea7b7
18 files changed
+260 -61

No files matched your search

+1
View File
@@ -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);
+2
View File
@@ -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);
+10 -5
View File
@@ -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;
+2 -1
View File
@@ -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
+20 -10
View File
@@ -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
+3 -2
View File
@@ -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.
+1
View File
@@ -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);
+63 -38
View File
@@ -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);
+9 -4
View File
@@ -139,8 +139,10 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, 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<meshtastic_AdminMessage>, 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<meshtastic_AdminMessage>, 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 =
+5
View File
@@ -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;
+1
View File
@@ -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) /
+1
View File
@@ -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);
@@ -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,
@@ -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,
+3
View File
@@ -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
+133
View File
@@ -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<std::string> capturedWarnings;
static std::vector<meshtastic_ClientNotification_NotificationType> 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);
@@ -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());
+2 -1
View File
@@ -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);