#include "CommonRadioPrefs.h" #include "TxtDataHelpers.h" #include "target.h" #include #include #include namespace { constexpr size_t RADIO_ARGS_CAPACITY = 132; size_t boundedLength(const char* text, size_t capacity) { if (text == nullptr) return capacity; size_t length = 0; while (length < capacity && text[length] != 0) length++; return length; } bool startsWith(const char* text, const char* prefix) { if (text == nullptr || prefix == nullptr) return false; const size_t prefix_len = strlen(prefix); return strncmp(text, prefix, prefix_len) == 0; } bool parseIntStrict(const char* text, int32_t min_value, int32_t max_value, int32_t& result) { if (text == nullptr || *text == 0 || min_value > max_value) return false; bool negative = false; if (*text == '-') { negative = true; text++; } if (*text == 0) return false; int64_t magnitude = 0; while (*text != 0) { if (*text < '0' || *text > '9') return false; magnitude = magnitude * 10 + (*text++ - '0'); if (magnitude > 2147483648LL) return false; } const int64_t parsed = negative ? -magnitude : magnitude; if (parsed < min_value || parsed > max_value) return false; result = static_cast(parsed); return true; } // Accept only a plain fixed-point decimal. Exponents, NaN/Inf, whitespace, // and trailing characters are rejected so malformed CLI values never become // zero through atoi()/atof() fallback behavior. bool parseDecimalStrict(const char* text, float& result) { if (text == nullptr || *text == 0) return false; const char* cursor = text; bool negative = false; if (*cursor == '-') { negative = true; cursor++; } bool saw_digit = false; double value = 0.0; while (*cursor >= '0' && *cursor <= '9') { value = value * 10.0 + (*cursor++ - '0'); if (!isfinite(value)) return false; saw_digit = true; } if (*cursor == '.') { cursor++; double place = 0.1; while (*cursor >= '0' && *cursor <= '9') { value += (*cursor++ - '0') * place; place *= 0.1; saw_digit = true; } } if (!saw_digit || *cursor != 0) return false; value = negative ? -value : value; if (!isfinite(value) || value > 3.402823466e+38 || value < -3.402823466e+38) { return false; } result = static_cast(value); return isfinite(result); } bool splitRadioArgs(char* text, const char* parts[4]) { if (text == nullptr || *text == 0) return false; size_t count = 0; char* field = text; while (true) { if (*field == 0 || count >= 4) return false; parts[count++] = field; char* comma = strchr(field, ','); if (comma == nullptr) break; *comma = 0; field = comma + 1; } return count == 4; } bool bwMatches(float bw, float allowed) { return fabsf(bw - allowed) <= 0.001f; } bool isValidLoRaBandwidth(float bw) { #if defined(USE_LR1110) return bwMatches(bw, 62.5f) || bwMatches(bw, 125.0f) || bwMatches(bw, 250.0f) || bwMatches(bw, 500.0f); #elif defined(USE_LLCC68) || defined(USE_SX1272) return bwMatches(bw, 125.0f) || bwMatches(bw, 250.0f) || bwMatches(bw, 500.0f); #else return bwMatches(bw, 7.8f) || bwMatches(bw, 10.4f) || bwMatches(bw, 15.6f) || bwMatches(bw, 20.8f) || bwMatches(bw, 31.25f) || bwMatches(bw, 41.7f) || bwMatches(bw, 62.5f) || bwMatches(bw, 125.0f) || bwMatches(bw, 250.0f) || bwMatches(bw, 500.0f); #endif } bool copyValue(char* destination, size_t capacity, const char* value) { if (destination == nullptr || capacity == 0 || value == nullptr) return false; const size_t value_len = strlen(value); if (value_len >= capacity) { destination[0] = 0; return false; } memcpy(destination, value, value_len + 1); return true; } } // namespace bool CommonRadioPrefs::getByKey(const char* key, char* value, size_t max_len) { if (key == nullptr || value == nullptr || max_len == 0) return false; if (strcmp(key, "fem_rxgain") == 0) { return copyValue(value, max_len, getFEMRxGain() == 0 ? "0" : "1"); } if (strcmp(key, "fem_txgain") == 0) { return copyValue(value, max_len, getFEMTxGain() == 0 ? "0" : "1"); } return false; } bool CommonRadioPrefs::setByKey(const char* key, const char* value) { if (key == nullptr || value == nullptr || (strcmp(value, "0") != 0 && strcmp(value, "1") != 0)) { return false; } const uint8_t enabled = strcmp(value, "1") == 0 ? 1 : 0; if (strcmp(key, "fem_rxgain") == 0) { setFEMRxGain(enabled); markDirty(); return true; } if (strcmp(key, "fem_txgain") == 0) { setFEMTxGain(enabled); markDirty(); return true; } return false; } bool CommonRadioPrefs::handleCommand(const char* command, uint32_t sender_timestamp, char* reply) { (void)sender_timestamp; if (command == nullptr || reply == nullptr) return false; if (strcmp(command, "get radio") == 0) { char freq[16], bw[16]; snprintf(freq, sizeof(freq), "%s", StrHelper::ftoa(getFreq())); snprintf(bw, sizeof(bw), "%s", StrHelper::ftoa3(getBandwidth())); sprintf(reply, "> %s,%s,%d,%d", freq, bw, (uint32_t)getSpreadFactor(), (uint32_t)getCodingRate()); return true; } if (startsWith(command, "set radio ")) { const char* args = command + strlen("set radio "); const size_t args_len = boundedLength(args, RADIO_ARGS_CAPACITY); if (args_len >= RADIO_ARGS_CAPACITY) { strcpy(reply, "Error, invalid radio params"); return true; } char tmp[RADIO_ARGS_CAPACITY]; memcpy(tmp, args, args_len + 1); const char *parts[4]; float freq = 0.0f; float bw = 0.0f; int32_t sf = 0; int32_t cr = 0; if (splitRadioArgs(tmp, parts) && parseDecimalStrict(parts[0], freq) && parseDecimalStrict(parts[1], bw) && parseIntStrict(parts[2], 5, 12, sf) && parseIntStrict(parts[3], 5, 8, cr) && freq >= 150.0f && freq <= 2500.0f && isValidLoRaBandwidth(bw)) { setSpreadFactor(static_cast(sf)); setCodingRate(static_cast(cr)); setFreq(freq); setBandwidth(bw); strcpy(reply, "OK - reboot to apply"); } else { strcpy(reply, "Error, invalid radio params"); } return true; } if (strcmp(command, "get freq") == 0) { sprintf(reply, "> %s", StrHelper::ftoa(getFreq())); return true; } if (strcmp(command, "get af") == 0) { sprintf(reply, "> %s", StrHelper::ftoa(getAirtimeFactor())); return true; } if (startsWith(command, "set af ")) { float factor = 0.0f; if (parseDecimalStrict(command + strlen("set af "), factor) && factor >= 0.0f && factor <= 9.0f) { setAirtimeFactor(factor); strcpy(reply, "OK"); } else { strcpy(reply, "Error, invalid airtime factor"); } return true; } if (strcmp(command, "get dutycycle") == 0) { const float factor = getAirtimeFactor(); if (!isfinite(factor) || factor < 0.0f) { strcpy(reply, "Error: invalid airtime factor"); } else { const int tenths = static_cast(1000.0f / (factor + 1.0f) + 0.5f); sprintf(reply, "> %d.%d%%", tenths / 10, tenths % 10); } return true; } if (startsWith(command, "set dutycycle ")) { float dc = 0.0f; if (!parseDecimalStrict(command + strlen("set dutycycle "), dc) || dc < 1.0f || dc > 100.0f) { strcpy(reply, "ERROR: dutycycle must be 1-100"); } else { setAirtimeFactor((100.0f / dc) - 1.0f); const float actual = 100.0f / (getAirtimeFactor() + 1.0f); const int tenths = static_cast(actual * 10.0f + 0.5f); sprintf(reply, "OK - %d.%d%%", tenths / 10, tenths % 10); } return true; } if (strcmp(command, "get int.thresh") == 0) { sprintf(reply, "> %d", (uint32_t) getIntThresh()); return true; } if (startsWith(command, "set int.thresh ")) { int32_t threshold = 0; if (parseIntStrict(command + strlen("set int.thresh "), 0, 255, threshold)) { setIntThresh(static_cast(threshold)); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-255"); } return true; } if (strcmp(command, "get cad") == 0) { sprintf(reply, "> %s", isCadEnabled() ? "on" : "off"); return true; } if (startsWith(command, "set cad ")) { const char* value = command + strlen("set cad "); if (strcmp(value, "on") == 0 || strcmp(value, "off") == 0) { setCadEnabled(strcmp(value, "on") == 0); strcpy(reply, "OK"); } else { strcpy(reply, "Error: use set cad on|off"); } return true; } if (strcmp(command, "get radio.rxgain") == 0) { sprintf(reply, "> %s", getRxGain() != 0 ? "on" : "off"); return true; } if (startsWith(command, "set radio.rxgain ")) { const char* value = command + strlen("set radio.rxgain "); if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) { strcpy(reply, "Error: use set radio.rxgain on|off"); return true; } const bool enabled = strcmp(value, "on") == 0; if (radio_driver.setRxBoostedGainMode(enabled)) { setRxGain(enabled ? 1 : 0); strcpy(reply, "OK"); } else { strcpy(reply, "Error: unsupported"); } return true; } if (strcmp(command, "get tx") == 0) { sprintf(reply, "> %d", static_cast(getTxPower())); return true; } if (startsWith(command, "set tx ")) { int32_t dbm = 0; if (!parseIntStrict(command + strlen("set tx "), -128, 127, dbm)) { strcpy(reply, "Error: invalid TX power"); } else if (!radio_driver.setTxPower(static_cast(dbm))) { strcpy(reply, "Error: TX power rejected"); } else { setTxPower(static_cast(dbm)); strcpy(reply, "OK"); } return true; } if (strcmp(command, "get rxdelay") == 0) { sprintf(reply, "> %s", StrHelper::ftoa(getRxDelay())); return true; } if (startsWith(command, "set rxdelay ")) { float db = 0.0f; if (parseDecimalStrict(command + strlen("set rxdelay "), db) && db >= 0.0f && db <= 20.0f) { setRxDelay(db); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-20"); } return true; } if (strcmp(command, "get agc.reset.interval") == 0) { sprintf(reply, "> %d", (uint32_t) getAgcResetInt()); return true; } if (startsWith(command, "set agc.reset.interval ")) { int32_t seconds = 0; if (parseIntStrict(command + strlen("set agc.reset.interval "), 0, 255, seconds)) { setAgcResetInt(static_cast(seconds)); sprintf(reply, "OK - interval rounded to %d", (uint32_t) getAgcResetInt()); } else { strcpy(reply, "Error, must be 0-255"); } return true; } if (strcmp(command, "get path.hash.mode") == 0) { sprintf(reply, "> %d", (uint32_t)getHashMode()); return true; } if (startsWith(command, "set path.hash.mode ")) { int32_t mode = 0; if (parseIntStrict(command + strlen("set path.hash.mode "), 0, 2, mode)) { setHashMode(static_cast(mode)); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0,1, or 2"); } return true; } if (strcmp(command, "get multi.acks") == 0) { sprintf(reply, "> %d", (uint32_t) getMultiAcks()); return true; } if (startsWith(command, "set multi.acks ")) { int32_t enabled = 0; if (parseIntStrict(command + strlen("set multi.acks "), 0, 1, enabled)) { setMultiAcks(static_cast(enabled)); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0 or 1"); } return true; } if (strcmp(command, "get txdelay") == 0) { sprintf(reply, "> %s", StrHelper::ftoa(getFloodTxDelay())); return true; } if (startsWith(command, "set txdelay ")) { float f = 0.0f; if (parseDecimalStrict(command + strlen("set txdelay "), f) && f >= 0.0f && f <= 2.0f) { setFloodTxDelay(f); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-2"); } return true; } if (strcmp(command, "get direct.txdelay") == 0) { sprintf(reply, "> %s", StrHelper::ftoa(getDirectTxDelay())); return true; } if (startsWith(command, "set direct.txdelay ")) { float f = 0.0f; if (parseDecimalStrict(command + strlen("set direct.txdelay "), f) && f >= 0.0f && f <= 2.0f) { setDirectTxDelay(f); strcpy(reply, "OK"); } else { strcpy(reply, "Error, must be 0-2"); } return true; } return false; // not handled }