diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 71857417d..4da8506ec 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -9544,7 +9544,8 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp, } } } - if (!strcmp(command, "get radio")) _radio_profiles.appendSavedPreamble(reply, reply_capacity, _prefs.sf, _prefs.bw); + // Keep get radio as the upstream four-field tuple. Apps parse this CSV + // directly; preamble and timing diagnostics have separate commands. if (!strncmp(command, "set radio ", 10) && !strncmp(reply, "OK", 2)) _radio_profiles.appendPrimaryChirpWarning(reply, reply_capacity, _prefs.sf, _prefs.bw, _radio_profiles.primaryPreamble()); diff --git a/scripts/check_nrf52_ble_reconnect.py b/scripts/check_nrf52_ble_reconnect.py index 0d07b7759..015a2d617 100644 --- a/scripts/check_nrf52_ble_reconnect.py +++ b/scripts/check_nrf52_ble_reconnect.py @@ -102,6 +102,12 @@ async def check_commands(client, args): raise RuntimeError("Unexpected firmware: " + reply) if "Unknown command" in reply or "Not Supported" in reply: raise RuntimeError(reply) + if cmd == "get radio": + fields = reply.removeprefix("> ").split(",") + if len(fields) != 4: + raise RuntimeError("Radio reply must have exactly four upstream fields: " + reply) + # Reject annotations as well as extra CSV fields. + float(fields[0]); float(fields[1]); int(fields[2]); int(fields[3]) report("app_cli", command=cmd, trailing_nul=True, reply=reply) diff --git a/src/helpers/TxtDataHelpers.cpp b/src/helpers/TxtDataHelpers.cpp index 955754601..5928cc01f 100644 --- a/src/helpers/TxtDataHelpers.cpp +++ b/src/helpers/TxtDataHelpers.cpp @@ -109,7 +109,8 @@ static void _ftoa(float f, char *p, int *status) else if (exp2 >= 0) { int_part = mantissa>>(23 - exp2); - frac_part = (mantissa<<(exp2 + 1))&0xFFFFFF; + // Only the low 24 bits are needed; shift unsigned to avoid signed overflow. + frac_part = (uint32_t(mantissa)<<(exp2 + 1))&0xFFFFFF; } else { diff --git a/test/test_companion_primary_radio_persistence.py b/test/test_companion_primary_radio_persistence.py index 35698c1ad..809668eb1 100644 --- a/test/test_companion_primary_radio_persistence.py +++ b/test/test_companion_primary_radio_persistence.py @@ -15,6 +15,7 @@ ROOT = Path(__file__).resolve().parents[1] HARNESS = r''' #include +#include #include #include #define constrain(value,low,high) ((value)<(low)?(low):((value)>(high)?(high):(value))) @@ -88,16 +89,32 @@ struct MyMesh { // CommonCLI's infrastructure roles share the same accepted duty-cycle range. // Extract their actual setter and boot sanitization, with only storage mocked. struct Infrastructure { - struct Prefs { float airtime_factor=1; } prefs; + struct Prefs { float airtime_factor=1,freq=909.5f,bw=62.5f;uint8_t sf=7,cr=5; } prefs; Prefs* _prefs=&prefs; uint8_t image[sizeof(float)]={}; void savePrefs() { memcpy(image,&prefs.airtime_factor,sizeof(float)); } void command(const char* config,char* reply) { @INFRA_SETTER@ } + void radioCommand(char* reply) { @INFRA_RADIO@ } void reboot() { memcpy(&prefs.airtime_factor,image,sizeof(float));@INFRA_SANITIZE@ } }; static bool error(const char* reply) { return strstr(reply,"Error") || strstr(reply,"ERROR"); } int main() { unsigned checks=0; + // App radio settings require exactly the upstream four-field CSV, even + // with an explicit preamble or an active temporary/secondary profile. + for(uint32_t timestamp:{0U,1700000000U})for(uint16_t preamble:{0U,48U}) { + MyMesh m;assert(m._radio_profiles.savePrimaryPreamble(preamble)); + m.radio.p.primary.freq=915;m.radio.p.primary.preamble=96; + m.radio.p.primary_temporary=true; + m.radio.p.secondary.params={916,125,64,8,6}; + m.radio.p.secondary.mode=mesh::RadioProfileMode::RxTx; + const char* reply=m.command("get radio",timestamp); + assert(!strcmp(reply,"> 909.5,62.5,7,5")); + assert(m._radio_profiles.primaryPreamble()==preamble); + Infrastructure infra;char infrastructure_reply[160]={}; + infra.radioCommand(infrastructure_reply); + assert(!strcmp(infrastructure_reply,reply));++checks; + } // The outer profile validator accepts 125.005, but CommonRadioPrefs rejects // it. Rejection must preserve both on-disk images, even with a dirty adapter. for(uint32_t timestamp:{0U,1700000000U})for(bool dirty:{false,true}) { @@ -199,6 +216,7 @@ class CompanionPrimaryRadioPersistenceTests(unittest.TestCase): '@COMPANION_SANITIZE@': boot_checks(companion, '_prefs.airtime_factor'), '@INFRA_SANITIZE@': boot_checks(infra, '_prefs->airtime_factor'), '@INFRA_SETTER@': extract_braced(infra, 'if (memcmp(config, "dutycycle ", 10) == 0)'), + '@INFRA_RADIO@': extract_braced(infra, 'if (configKeyEquals(config, "radio"))').split('{', 1)[1].rsplit('}', 1)[0], } harness = support + HARNESS for marker, value in replacements.items():