feat(cli): support relative minutes in radio schedules

This commit is contained in:
mikecarper committed 2026-09-23 00:19:12 -07:00
1 parent 7c9cb59400
commit ff50589b69
8 files changed
+121 -16

No files matched your search

+17
View File
@@ -675,6 +675,23 @@ inline bool parseUnsignedIntegerStrict(const char* text, uint32_t& result) {
return true;
}
// Bare values are UTC seconds; +N is N minutes from one command-time snapshot.
// Resolve both endpoints against the same snapshot, never end against start.
inline bool parseRadioScheduleTime(const char* text, uint32_t now, uint32_t& result) {
if (text == nullptr) return false;
while (*text == ' ' || *text == '\t') ++text;
const bool relative = *text == '+';
if (relative && (text[1] < '0' || text[1] > '9')) return false;
uint32_t value;
if (!parseUnsignedIntegerStrict(relative ? text + 1 : text, value)) return false;
if (relative) {
if (!value || value > (UINT32_MAX - now) / 60U) return false;
value = now + value * 60U;
}
result = value;
return true;
}
// Zero clears a custom port override; a full broker URI supplies its default.
inline bool parseMqttPort(const char* text, uint16_t& result) {
uint32_t value;
+7 -7
View File
@@ -364,7 +364,7 @@ static int countSeparatedParts(const char* s, char separator) {
static bool parseScheduledRadioArgs(const char* args, bool temporary, float& freq, float& bw,
uint8_t& sf, uint8_t& cr, uint32_t& start_time,
uint32_t& end_time) {
uint32_t& end_time, uint32_t now) {
const int expected_parts = temporary ? 6 : 5;
args = skipSpacesConst(args);
if (countSeparatedParts(args, ',') != expected_parts) {
@@ -387,7 +387,7 @@ static bool parseScheduledRadioArgs(const char* args, bool temporary, float& fre
|| !mesh::cli::parseDecimalStrict(parts[1], bw)
|| !parseUint32Strict(parts[2], sf_u32)
|| !parseUint32Strict(parts[3], cr_u32)
|| !parseUint32Strict(parts[4], start_time)) {
|| !mesh::cli::parseRadioScheduleTime(parts[4], now, start_time)) {
return false;
}
if (sf_u32 > 255 || cr_u32 > 255) {
@@ -396,7 +396,7 @@ static bool parseScheduledRadioArgs(const char* args, bool temporary, float& fre
sf = (uint8_t)sf_u32;
cr = (uint8_t)cr_u32;
if (temporary && !parseUint32Strict(parts[5], end_time)) {
if (temporary && !mesh::cli::parseRadioScheduleTime(parts[5], now, end_time)) {
return false;
}
if (!temporary) {
@@ -3854,8 +3854,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
uint16_t preamble = 0;
char legacy[120];
if (!mesh::RadioProfileCLI::parseSuffix(&config[8], 5, legacy, sizeof(legacy), preamble)
|| !parseScheduledRadioArgs(legacy, false, freq, bw, sf, cr, start_time, end_time)) {
strcpy(reply, "Error, use: set radioat f,bw,sf,cr,start");
|| !parseScheduledRadioArgs(legacy, false, freq, bw, sf, cr, start_time, end_time, _rtc->getCurrentTime())) {
strcpy(reply, "Error, use: set radioat f,bw,sf,cr,start (UTC or +minutes)");
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)
|| !_radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
strcpy(reply, "Error, invalid radio params");
@@ -3873,8 +3873,8 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
uint16_t preamble = 0;
char legacy[120];
if (!mesh::RadioProfileCLI::parseSuffix(&config[12], 6, legacy, sizeof(legacy), preamble)
|| !parseScheduledRadioArgs(legacy, true, freq, bw, sf, cr, start_time, end_time)) {
strcpy(reply, "Error, use: set tempradioat f,bw,sf,cr,start,end");
|| !parseScheduledRadioArgs(legacy, true, freq, bw, sf, cr, start_time, end_time, _rtc->getCurrentTime())) {
strcpy(reply, "Error, use: set tempradioat f,bw,sf,cr,start,end (UTC or +minutes)");
} else if (freq < 150.0f || freq > 2500.0f || sf < 5 || sf > 12 || cr < 5 || cr > 8 || !isValidLoRaBandwidth(bw)
|| !_radio_profiles.acceptsPrimary(freq, bw, sf, cr, preamble)) {
strcpy(reply, "Error, invalid radio params");
+4 -4
View File
@@ -665,17 +665,17 @@ bool RadioProfileCLI::handle(const char* command, char* reply, size_t capacity,
&& cli::parseUnsignedIntegerStrict(parts[3], cr) && cr <= 255
&& parseMode(parts[4], config.mode);
config.params.sf = sf; config.params.cr = cr;
const uint32_t epoch = scheduled && rtc_ ? rtc_->getCurrentTime() : 0;
if (valid && count > expected) valid = parsePreamble(parts[expected], config.params.preamble);
if (valid && temporary) valid = cli::parseUnsignedIntegerStrict(parts[5], minutes) && minutes >= 1 && minutes <= 10080;
if (valid && scheduled) valid = cli::parseUnsignedIntegerStrict(parts[5], start);
if (valid && scheduled_temp) valid = cli::parseUnsignedIntegerStrict(parts[6], end);
if (valid && scheduled) valid = cli::parseRadioScheduleTime(parts[5], epoch, start);
if (valid && scheduled_temp) valid = cli::parseRadioScheduleTime(parts[6], epoch, end);
if (valid) valid = radio_->validateProfile(config.params);
RadioProfiles preview = *radio_->profiles(); preview.setSecondary(config, false);
if (valid) valid = preview.automaticPreambleFits();
if (!valid) { snprintf(reply, capacity, "Error: use %s f,bw,sf,cr,rx|rxtx%s[,preamble|auto]", key,
scheduled_temp ? ",start,end" : scheduled ? ",start" : temporary ? ",minutes" : ""); return true; }
scheduled_temp ? ",start,end (UTC or +minutes)" : scheduled ? ",start (UTC or +minutes)" : temporary ? ",minutes" : ""); return true; }
if (scheduled) {
const uint32_t epoch = rtc_ ? rtc_->getCurrentTime() : 0;
if (!epoch || start <= epoch || (uint64_t)(start-epoch) * 1000 > 0x7fffffffULL
|| (scheduled_temp && (end <= start || (uint64_t)(end-epoch) * 1000 > 0x7fffffffULL))) {
snprintf(reply, capacity, "Error: future UTC start/end required, within 24 days"); return true;