From 0c6f05bfa0c5df7f58a3b93f040dc3a9f7d37922 Mon Sep 17 00:00:00 2001 From: 3djc <3djc@gh.com> Date: Sat, 5 Sep 2026 10:40:43 +0200 Subject: [PATCH 1/5] feat: auto set and adjust rtc clock compensation --- radio/src/cli.cpp | 4 + radio/src/rtc.cpp | 102 +++++++++++++++++- radio/src/rtc.h | 17 +++ .../common/arm/stm32/f4/CMakeLists.txt | 1 + .../common/arm/stm32/h7/CMakeLists.txt | 2 + .../targets/common/arm/stm32/rtc_driver.cpp | 52 ++++++++- radio/src/targets/simu/simulib.cpp | 7 +- 7 files changed, 179 insertions(+), 6 deletions(-) diff --git a/radio/src/cli.cpp b/radio/src/cli.cpp index 372348ae8db..d3f52ca64cf 100644 --- a/radio/src/cli.cpp +++ b/radio/src/cli.cpp @@ -1496,6 +1496,10 @@ int cliDisplay(const char ** argv) struct gtm utm; gettime(&utm); cliSerialPrint("rtc = %4d-%02d-%02d %02d:%02d:%02d.%02d0", utm.tm_year+TM_YEAR_BASE, utm.tm_mon+1, utm.tm_mday, utm.tm_hour, utm.tm_min, utm.tm_sec, g_ms100); + int32_t units = rtcGetCalibration(); + cliSerialPrint("rtc calibration = %d units (%d ppm x10), reference = %u", + (int)units, (int)rtcCalibrationPpm10(units), + (unsigned)rtcGetCalibrationRef()); } #if defined(VOLUME_I2C_ADDRESS) else if (!strcmp(argv[1], "volume")) { diff --git a/radio/src/rtc.cpp b/radio/src/rtc.cpp index 96a33a3f042..e7dc25028b0 100644 --- a/radio/src/rtc.cpp +++ b/radio/src/rtc.cpp @@ -22,8 +22,6 @@ #include #include "edgetx.h" -extern void rtcdriver_settime(struct gtm * t); - #define LEAP_SECONDS_POSSIBLE 0 /* Shift A right by B bits portably, by dividing A by 2**B and @@ -445,8 +443,6 @@ void gettime(struct gtm * tm) filltm(&g_rtcTime, tm); // create a struct tm date/time structure from global unix time stamp } -void rtcGetTime(struct gtm * t); - #define RTC_ADJUST_PERIOD 60 // how often RTC is checked for accuracy [seconds] #define RTC_ADJUST_TRESHOLD 20 // how much clock must differ before adjustment is made [seconds] /* @@ -494,6 +490,104 @@ uint8_t rtcAdjust(uint16_t year, uint8_t mon, uint8_t day, uint8_t hour, uint8_t return 0; } +// Crystal drift measured between two clock settings, compensated by the RTC +// smooth calibration hardware. Reference kept in the RTC backup domain. + +#define RTC_CALIB_MIN_ELAPSED SECS_PER_DAY // no reliable measurement below that +#define RTC_CALIB_MAX_ELAPSED (10 * 365 * SECS_PER_DAY) // nonsense reference +#define RTC_CALIB_MIN_ERROR 4 // [s] below that it is input noise +#define RTC_CALIB_SET_ACCURACY 2 // [s] hand setting accuracy +#define RTC_CALIB_MAX_PPM 200 // beyond any crystal: clock was set +#define RTC_CALIB_TZ_STEP 900 // [s] time zone granularity +#define RTC_CALIB_TZ_BAND 60 // [s] tolerance around it +#define RTC_CALIB_MIN_YEAR 101 // 2001, RTC resets to 2000 +#define RTC_CALIB_SESSION_GAP (5 * 60 * 100) // [10ms] same edit session + +// Sampled before the clock got changed +static bool rtcCalibSession = false; +static tmr10ms_t rtcCalibLastSet = 0; +static gtime_t rtcCalibSessionRef = 0; +static gtime_t rtcCalibSessionRtc = 0; +static int32_t rtcCalibSessionUnits = 0; +static bool rtcCalibSessionValid = false; + +int32_t rtcCalibrationPpm10(int32_t units) +{ + return (int32_t)(((int64_t)units * 10000000) / RTC_CALIB_UNITS_PER_SECOND); +} + +static void rtcCalibrationStart() +{ + struct gtm before = {}; + rtcGetTime(&before); + + rtcCalibSessionValid = (before.tm_year >= RTC_CALIB_MIN_YEAR); + rtcCalibSessionRtc = rtcCalibSessionValid ? gmktime(&before) : 0; + rtcCalibSessionRef = rtcGetCalibrationRef(); + rtcCalibSessionUnits = rtcGetCalibration(); + rtcCalibSession = true; +} + +// Recomputed from the calibration in use at session start, so that successive +// edits converge instead of piling up +static void rtcCalibrationUpdate(gtime_t newTime) +{ + // Initial setting, or clock that lost power + if (rtcCalibSessionRef == 0 || !rtcCalibSessionValid) return; + + gtime_t elapsed = newTime - rtcCalibSessionRef; + if (elapsed < (gtime_t)RTC_CALIB_MIN_ELAPSED || elapsed > (gtime_t)RTC_CALIB_MAX_ELAPSED) return; + + gtime_t error = rtcCalibSessionRtc - newTime; // > 0 when the clock runs fast + int64_t absError = (error < 0) ? -(int64_t)error : (int64_t)error; + + // Too small to tell from input noise + if (absError < RTC_CALIB_MIN_ERROR) return; + + // Faster than any crystal: the clock was moved + if (absError * 1000000 > (int64_t)elapsed * RTC_CALIB_MAX_PPM) return; + + // A whole time zone step is a time zone or DST change + if (absError >= RTC_CALIB_TZ_STEP - RTC_CALIB_TZ_BAND) { + int64_t rem = absError % RTC_CALIB_TZ_STEP; + if (rem <= RTC_CALIB_TZ_BAND || rem >= RTC_CALIB_TZ_STEP - RTC_CALIB_TZ_BAND) return; + } + + // Do not over correct on a short measurement + absError -= RTC_CALIB_SET_ACCURACY; + + int32_t delta = (int32_t)((absError * RTC_CALIB_UNITS_PER_SECOND + elapsed / 2) / elapsed); + if (error < 0) delta = -delta; + + int32_t units = rtcCalibSessionUnits - delta; + if (units > RTC_CALIB_UNIT_MAX) units = RTC_CALIB_UNIT_MAX; + if (units < RTC_CALIB_UNIT_MIN) units = RTC_CALIB_UNIT_MIN; + + rtcSetCalibration(units); + + TRACE("RTC drift %d s over %d s, calibration %d -> %d units (%d -> %d ppm x10)", + (int)error, (int)elapsed, (int)rtcCalibSessionUnits, (int)units, + (int)rtcCalibrationPpm10(rtcCalibSessionUnits), + (int)rtcCalibrationPpm10(units)); +} + +void rtcSetTime(const struct gtm * t) +{ + struct gtm tm = *t; + gtime_t newTime = gmktime(&tm); + + tmr10ms_t now = get_tmr10ms(); + if (!rtcCalibSession || (now - rtcCalibLastSet) > RTC_CALIB_SESSION_GAP) { + rtcCalibrationStart(); + } + rtcCalibLastSet = now; + + rtcDriverSetTime(t); + + rtcCalibrationUpdate(newTime); + rtcSetCalibrationRef(newTime); +} + bool rtcIsValid() { struct gtm t; diff --git a/radio/src/rtc.h b/radio/src/rtc.h index 40a985ef0dd..dc810c6d206 100644 --- a/radio/src/rtc.h +++ b/radio/src/rtc.h @@ -55,6 +55,23 @@ void rtcSetTime(const struct gtm * tm); gtime_t gmktime (struct gtm *tm); uint8_t rtcAdjust(uint16_t year, uint8_t mon, uint8_t day, uint8_t hour, uint8_t min, uint8_t sec); +// Driver interface, rtcSetTime() wraps rtcDriverSetTime() +void rtcDriverSetTime(const struct gtm * tm); +void rtcGetTime(struct gtm * tm); + +// Smooth calibration, one unit is one clock pulse out of 2^20 (~0.954 ppm) +#define RTC_CALIB_UNITS_PER_SECOND 1048576 +#define RTC_CALIB_UNIT_MAX 512 +#define RTC_CALIB_UNIT_MIN (-511) + +int32_t rtcGetCalibration(); +void rtcSetCalibration(int32_t units); +int32_t rtcCalibrationPpm10(int32_t units); + +// Time of the last known good setting, 0 when unknown +gtime_t rtcGetCalibrationRef(); +void rtcSetCalibrationRef(gtime_t t); + #if defined(__cplusplus) && !defined(SIMU) extern "C" { #endif diff --git a/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt b/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt index 72477d740bd..3872d8df029 100644 --- a/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt +++ b/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt @@ -41,6 +41,7 @@ if(NOT NATIVE_BUILD) ${STM32CUBE_DIR}/Src/stm32f4xx_hal_rcc.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_rcc_ex.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_rtc.c + ${STM32CUBE_DIR}/Src/stm32f4xx_hal_rtc_ex.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_i2c.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_i2c_ex.c ${STM32CUBE_DIR}/Src/stm32f4xx_hal_sd.c diff --git a/radio/src/targets/common/arm/stm32/h7/CMakeLists.txt b/radio/src/targets/common/arm/stm32/h7/CMakeLists.txt index 8455b8335fc..bc1751a2d7e 100644 --- a/radio/src/targets/common/arm/stm32/h7/CMakeLists.txt +++ b/radio/src/targets/common/arm/stm32/h7/CMakeLists.txt @@ -44,6 +44,7 @@ if(NOT NATIVE_BUILD) ${STM32CUBE_SRC_PREFIX}_hal_rcc.c ${STM32CUBE_SRC_PREFIX}_hal_rcc_ex.c ${STM32CUBE_SRC_PREFIX}_hal_rtc.c + ${STM32CUBE_SRC_PREFIX}_hal_rtc_ex.c ${STM32CUBE_SRC_PREFIX}_hal_i2c.c ${STM32CUBE_SRC_PREFIX}_hal_i2c_ex.c ${STM32CUBE_SRC_PREFIX}_hal_i2s.c @@ -85,6 +86,7 @@ if(NOT NATIVE_BUILD) ${STM32CUBE_SRC_PREFIX}_hal_rcc.c ${STM32CUBE_SRC_PREFIX}_hal_rcc_ex.c ${STM32CUBE_SRC_PREFIX}_hal_rtc.c + ${STM32CUBE_SRC_PREFIX}_hal_rtc_ex.c ${STM32CUBE_SRC_PREFIX}_hal_i2c.c ${STM32CUBE_SRC_PREFIX}_hal_i2c_ex.c ${STM32CUBE_SRC_PREFIX}_hal_i2s.c diff --git a/radio/src/targets/common/arm/stm32/rtc_driver.cpp b/radio/src/targets/common/arm/stm32/rtc_driver.cpp index ae9141c7512..60795b05ae3 100644 --- a/radio/src/targets/common/arm/stm32/rtc_driver.cpp +++ b/radio/src/targets/common/arm/stm32/rtc_driver.cpp @@ -24,7 +24,7 @@ RTC_HandleTypeDef rtc = {}; -void rtcSetTime(const struct gtm * t) +void rtcDriverSetTime(const struct gtm * t) { g_ms100 = 0; // start of next second begins now @@ -57,6 +57,56 @@ void rtcGetTime(struct gtm * t) t->tm_mday = RTC_DateStruct.Date; } +#if defined(RTC_CALR_CALM) && !defined(BOOT) + +// DR0 is left alone, legacy code uses it for shutdown/soft reset requests +#define RTC_CALIB_BKP_MAGIC_REG RTC_BKP_DR1 +#define RTC_CALIB_BKP_REF_REG RTC_BKP_DR2 +#define RTC_CALIB_BKP_MAGIC 0x52544301 + +int32_t rtcGetCalibration() +{ + uint32_t calr = READ_REG(rtc.Instance->CALR); + int32_t units = -(int32_t)(calr & RTC_CALR_CALM); + if (calr & RTC_CALR_CALP) units += 512; + return units; +} + +void rtcSetCalibration(int32_t units) +{ + if (units > RTC_CALIB_UNIT_MAX) units = RTC_CALIB_UNIT_MAX; + if (units < RTC_CALIB_UNIT_MIN) units = RTC_CALIB_UNIT_MIN; + + // CALP adds 512 pulses, CALM removes up to 511 + uint32_t plus = (units > 0) ? RTC_SMOOTHCALIB_PLUSPULSES_SET + : RTC_SMOOTHCALIB_PLUSPULSES_RESET; + uint32_t minus = (units > 0) ? (512 - units) : -units; + + HAL_RTCEx_SetSmoothCalib(&rtc, RTC_SMOOTHCALIB_PERIOD_32SEC, plus, minus); +} + +gtime_t rtcGetCalibrationRef() +{ + if (HAL_RTCEx_BKUPRead(&rtc, RTC_CALIB_BKP_MAGIC_REG) != RTC_CALIB_BKP_MAGIC) + return 0; + return (gtime_t)HAL_RTCEx_BKUPRead(&rtc, RTC_CALIB_BKP_REF_REG); +} + +void rtcSetCalibrationRef(gtime_t t) +{ + HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_REF_REG, (uint32_t)t); + HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_MAGIC_REG, RTC_CALIB_BKP_MAGIC); +} + +#else // no smooth calibration hardware + +int32_t rtcGetCalibration() { return 0; } +void rtcSetCalibration(int32_t units) { (void)units; } +gtime_t rtcGetCalibrationRef() { return 0; } +void rtcSetCalibrationRef(gtime_t t) { (void)t; } + +#endif + void rtcInit() { rtc.Instance = RTC; diff --git a/radio/src/targets/simu/simulib.cpp b/radio/src/targets/simu/simulib.cpp index 3a7aa0bc742..4191fd06c02 100644 --- a/radio/src/targets/simu/simulib.cpp +++ b/radio/src/targets/simu/simulib.cpp @@ -447,10 +447,15 @@ void rtcGetTime(struct gtm * t) { } -void rtcSetTime(const struct gtm * t) +void rtcDriverSetTime(const struct gtm * t) { } +int32_t rtcGetCalibration() { return 0; } +void rtcSetCalibration(int32_t units) { (void)units; } +gtime_t rtcGetCalibrationRef() { return 0; } +void rtcSetCalibrationRef(gtime_t t) { (void)t; } + #if defined(PCBTARANIS) void sdPoll10ms() {} #endif From 3c8caeba52d54f268fd426e76889697092d9c558 Mon Sep 17 00:00:00 2001 From: 3djc <3djc@gh.com> Date: Tue, 8 Sep 2026 10:08:11 +0200 Subject: [PATCH 2/5] add debug tools --- radio/src/cli.cpp | 17 +++++++++++++++++ radio/src/rtc.cpp | 17 +++++++++++++++++ radio/src/rtc.h | 5 +++++ .../src/targets/common/arm/stm32/rtc_driver.cpp | 7 +++++++ radio/src/targets/simu/simulib.cpp | 1 + 5 files changed, 47 insertions(+) diff --git a/radio/src/cli.cpp b/radio/src/cli.cpp index d3f52ca64cf..2fedff05be1 100644 --- a/radio/src/cli.cpp +++ b/radio/src/cli.cpp @@ -1102,6 +1102,23 @@ int cliSet(const char **argv) return -1; } } +#if defined(DEBUG) + else if (!strcmp(argv[1], "rtccal")) { + int ppm = 0; + if (!strcmp(argv[2], "reset")) { + rtcResetCalibration(); + } else if (toInt(argv, 2, &ppm) > 0) { + rtcSetCalibration(rtcCalibrationUnits(ppm)); + } else { + cliSerialPrint("%s: expected \"reset\" or a ppm value", argv[0]); + return -1; + } + int32_t units = rtcGetCalibration(); + cliSerialPrint("rtc calibration = %d units (%d ppm x10), reference = %u", + (int)units, (int)rtcCalibrationPpm10(units), + (unsigned)rtcGetCalibrationRef()); + } +#endif #if !defined(SOFTWARE_VOLUME) && defined(AUDIO) else if (!strcmp(argv[1], "volume")) { int level = 0; diff --git a/radio/src/rtc.cpp b/radio/src/rtc.cpp index e7dc25028b0..3aecd5c3c17 100644 --- a/radio/src/rtc.cpp +++ b/radio/src/rtc.cpp @@ -516,6 +516,13 @@ int32_t rtcCalibrationPpm10(int32_t units) return (int32_t)(((int64_t)units * 10000000) / RTC_CALIB_UNITS_PER_SECOND); } +int32_t rtcCalibrationUnits(int32_t ppm) +{ + int64_t v = (int64_t)ppm * RTC_CALIB_UNITS_PER_SECOND; + v += (v < 0) ? -500000 : 500000; + return (int32_t)(v / 1000000); +} + static void rtcCalibrationStart() { struct gtm before = {}; @@ -571,6 +578,16 @@ static void rtcCalibrationUpdate(gtime_t newTime) (int)rtcCalibrationPpm10(units)); } +// Clears the hardware trim and the stored reference, so the next two settings +// start a fresh measurement +void rtcResetCalibration() +{ + rtcSetCalibration(0); + rtcClearCalibrationRef(); + rtcCalibSession = false; + rtcCalibLastSet = 0; +} + void rtcSetTime(const struct gtm * t) { struct gtm tm = *t; diff --git a/radio/src/rtc.h b/radio/src/rtc.h index dc810c6d206..c3dad5ba640 100644 --- a/radio/src/rtc.h +++ b/radio/src/rtc.h @@ -67,10 +67,15 @@ void rtcGetTime(struct gtm * tm); int32_t rtcGetCalibration(); void rtcSetCalibration(int32_t units); int32_t rtcCalibrationPpm10(int32_t units); +int32_t rtcCalibrationUnits(int32_t ppm); // Time of the last known good setting, 0 when unknown gtime_t rtcGetCalibrationRef(); void rtcSetCalibrationRef(gtime_t t); +void rtcClearCalibrationRef(); + +// Back to a factory-fresh state, as if the backup domain had been lost +void rtcResetCalibration(); #if defined(__cplusplus) && !defined(SIMU) extern "C" { diff --git a/radio/src/targets/common/arm/stm32/rtc_driver.cpp b/radio/src/targets/common/arm/stm32/rtc_driver.cpp index 60795b05ae3..7eaf5ab5ba1 100644 --- a/radio/src/targets/common/arm/stm32/rtc_driver.cpp +++ b/radio/src/targets/common/arm/stm32/rtc_driver.cpp @@ -98,12 +98,19 @@ void rtcSetCalibrationRef(gtime_t t) HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_MAGIC_REG, RTC_CALIB_BKP_MAGIC); } +void rtcClearCalibrationRef() +{ + HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_MAGIC_REG, 0); + HAL_RTCEx_BKUPWrite(&rtc, RTC_CALIB_BKP_REF_REG, 0); +} + #else // no smooth calibration hardware int32_t rtcGetCalibration() { return 0; } void rtcSetCalibration(int32_t units) { (void)units; } gtime_t rtcGetCalibrationRef() { return 0; } void rtcSetCalibrationRef(gtime_t t) { (void)t; } +void rtcClearCalibrationRef() {} #endif diff --git a/radio/src/targets/simu/simulib.cpp b/radio/src/targets/simu/simulib.cpp index 4191fd06c02..c615ce47b0e 100644 --- a/radio/src/targets/simu/simulib.cpp +++ b/radio/src/targets/simu/simulib.cpp @@ -455,6 +455,7 @@ int32_t rtcGetCalibration() { return 0; } void rtcSetCalibration(int32_t units) { (void)units; } gtime_t rtcGetCalibrationRef() { return 0; } void rtcSetCalibrationRef(gtime_t t) { (void)t; } +void rtcClearCalibrationRef() {} #if defined(PCBTARANIS) void sdPoll10ms() {} From 82de0f78d5e0912b350a3adf30106243520a4ca1 Mon Sep 17 00:00:00 2001 From: 3djc <3djc@gh.com> Date: Thu, 10 Sep 2026 10:20:55 +0200 Subject: [PATCH 3/5] Add more debug reporting --- radio/src/cli.cpp | 14 +++++++++++ radio/src/rtc.cpp | 60 +++++++++++++++++++++++++++++++++++++++++------ radio/src/rtc.h | 24 +++++++++++++++++++ 3 files changed, 91 insertions(+), 7 deletions(-) diff --git a/radio/src/cli.cpp b/radio/src/cli.cpp index 2fedff05be1..e81482db550 100644 --- a/radio/src/cli.cpp +++ b/radio/src/cli.cpp @@ -1096,6 +1096,20 @@ int cliSet(const char **argv) // update local timestamp and get wday calculated g_rtcTime = gmktime(&t); rtcSetTime(&t); +#if defined(DEBUG) + const struct RtcCalibReport * rep = rtcGetCalibrationReport(); + if (rep->elapsed != 0) { + int32_t ppm10 = 0; + if (rep->elapsed > 0) + ppm10 = (int32_t)(((int64_t)rep->error * 10000000) / rep->elapsed); + cliSerialPrint("rtc drift = %d s over %d s (%d ppm x10)", (int)rep->error, + (int)rep->elapsed, (int)ppm10); + } + int32_t units = rtcGetCalibration(); + cliSerialPrint("rtc calibration: %s (now %d units, %d ppm x10)", + rtcCalibrationResultText(rep->result), (int)units, + (int)rtcCalibrationPpm10(units)); +#endif } else { cliSerialPrint("%s: Invalid arguments \"%s\" \"%s\"", argv[0], argv[1], argv[2]); diff --git a/radio/src/rtc.cpp b/radio/src/rtc.cpp index 3aecd5c3c17..a7844fe138d 100644 --- a/radio/src/rtc.cpp +++ b/radio/src/rtc.cpp @@ -535,29 +535,69 @@ static void rtcCalibrationStart() rtcCalibSession = true; } +#if defined(DEBUG) +static RtcCalibReport rtcCalibReport = {}; + +const struct RtcCalibReport * rtcGetCalibrationReport() +{ + return &rtcCalibReport; +} + +const char * rtcCalibrationResultText(uint8_t result) +{ + switch (result) { + case RTC_CALIB_APPLIED: return "applied"; + case RTC_CALIB_NO_REF: return "no reference yet, this setting becomes one"; + case RTC_CALIB_CLOCK_INVALID: return "previous clock invalid, RTC had lost power"; + case RTC_CALIB_REF_AHEAD: return "reference is in the future"; + case RTC_CALIB_TOO_SOON: return "less than a day since the reference"; + case RTC_CALIB_REF_TOO_OLD: return "reference older than 10 years"; + case RTC_CALIB_TOO_SMALL: return "drift under 4 s, indistinguishable from input noise"; + case RTC_CALIB_TOO_LARGE: return "over 200 ppm, clock assumed moved on purpose"; + case RTC_CALIB_TIME_ZONE: return "near a 15 min step, assumed time zone or DST change"; + default: return "unknown"; + } +} +#endif + // Recomputed from the calibration in use at session start, so that successive // edits converge instead of piling up -static void rtcCalibrationUpdate(gtime_t newTime) +static uint8_t rtcCalibrationUpdate(gtime_t newTime) { +#if defined(DEBUG) + rtcCalibReport.elapsed = 0; + rtcCalibReport.error = 0; +#endif + // Initial setting, or clock that lost power - if (rtcCalibSessionRef == 0 || !rtcCalibSessionValid) return; + if (rtcCalibSessionRef == 0) return RTC_CALIB_NO_REF; + if (!rtcCalibSessionValid) return RTC_CALIB_CLOCK_INVALID; gtime_t elapsed = newTime - rtcCalibSessionRef; - if (elapsed < (gtime_t)RTC_CALIB_MIN_ELAPSED || elapsed > (gtime_t)RTC_CALIB_MAX_ELAPSED) return; - gtime_t error = rtcCalibSessionRtc - newTime; // > 0 when the clock runs fast + +#if defined(DEBUG) + rtcCalibReport.elapsed = elapsed; + rtcCalibReport.error = error; +#endif + + if (elapsed < 0) return RTC_CALIB_REF_AHEAD; + if (elapsed < (gtime_t)RTC_CALIB_MIN_ELAPSED) return RTC_CALIB_TOO_SOON; + if (elapsed > (gtime_t)RTC_CALIB_MAX_ELAPSED) return RTC_CALIB_REF_TOO_OLD; + int64_t absError = (error < 0) ? -(int64_t)error : (int64_t)error; // Too small to tell from input noise - if (absError < RTC_CALIB_MIN_ERROR) return; + if (absError < RTC_CALIB_MIN_ERROR) return RTC_CALIB_TOO_SMALL; // Faster than any crystal: the clock was moved - if (absError * 1000000 > (int64_t)elapsed * RTC_CALIB_MAX_PPM) return; + if (absError * 1000000 > (int64_t)elapsed * RTC_CALIB_MAX_PPM) return RTC_CALIB_TOO_LARGE; // A whole time zone step is a time zone or DST change if (absError >= RTC_CALIB_TZ_STEP - RTC_CALIB_TZ_BAND) { int64_t rem = absError % RTC_CALIB_TZ_STEP; - if (rem <= RTC_CALIB_TZ_BAND || rem >= RTC_CALIB_TZ_STEP - RTC_CALIB_TZ_BAND) return; + if (rem <= RTC_CALIB_TZ_BAND || rem >= RTC_CALIB_TZ_STEP - RTC_CALIB_TZ_BAND) + return RTC_CALIB_TIME_ZONE; } // Do not over correct on a short measurement @@ -576,6 +616,8 @@ static void rtcCalibrationUpdate(gtime_t newTime) (int)error, (int)elapsed, (int)rtcCalibSessionUnits, (int)units, (int)rtcCalibrationPpm10(rtcCalibSessionUnits), (int)rtcCalibrationPpm10(units)); + + return RTC_CALIB_APPLIED; } // Clears the hardware trim and the stored reference, so the next two settings @@ -601,7 +643,11 @@ void rtcSetTime(const struct gtm * t) rtcDriverSetTime(t); +#if defined(DEBUG) + rtcCalibReport.result = rtcCalibrationUpdate(newTime); +#else rtcCalibrationUpdate(newTime); +#endif rtcSetCalibrationRef(newTime); } diff --git a/radio/src/rtc.h b/radio/src/rtc.h index c3dad5ba640..661284c5aaf 100644 --- a/radio/src/rtc.h +++ b/radio/src/rtc.h @@ -69,6 +69,30 @@ void rtcSetCalibration(int32_t units); int32_t rtcCalibrationPpm10(int32_t units); int32_t rtcCalibrationUnits(int32_t ppm); +// Why the last clock setting did or did not move the calibration +enum { + RTC_CALIB_APPLIED, + RTC_CALIB_NO_REF, + RTC_CALIB_CLOCK_INVALID, + RTC_CALIB_REF_AHEAD, + RTC_CALIB_TOO_SOON, + RTC_CALIB_REF_TOO_OLD, + RTC_CALIB_TOO_SMALL, + RTC_CALIB_TOO_LARGE, + RTC_CALIB_TIME_ZONE, +}; + +#if defined(DEBUG) +struct RtcCalibReport { + uint8_t result; + gtime_t elapsed; + gtime_t error; +}; + +const struct RtcCalibReport * rtcGetCalibrationReport(); +const char * rtcCalibrationResultText(uint8_t result); +#endif + // Time of the last known good setting, 0 when unknown gtime_t rtcGetCalibrationRef(); void rtcSetCalibrationRef(gtime_t t); From 638c4dc5400e34b1adb82757a4c72deb93e52375 Mon Sep 17 00:00:00 2001 From: 3djc <3djc@gh.com> Date: Thu, 10 Sep 2026 11:08:46 +0200 Subject: [PATCH 4/5] Set cli time handling precision to milliseconds level --- radio/src/cli.cpp | 58 +++++++++++++++-- radio/src/rtc.cpp | 62 ++++++++++++------- radio/src/rtc.h | 5 +- .../targets/common/arm/stm32/rtc_driver.cpp | 14 ++++- radio/src/targets/simu/simulib.cpp | 6 ++ 5 files changed, 115 insertions(+), 30 deletions(-) diff --git a/radio/src/cli.cpp b/radio/src/cli.cpp index e81482db550..f009b4a7e6b 100644 --- a/radio/src/cli.cpp +++ b/radio/src/cli.cpp @@ -248,6 +248,40 @@ int toInt(const char ** argv, int index, int * val) return result; } +// "30" -> 30 s, "30.52" -> 30 s 520 ms +static int toSeconds(const char ** argv, int index, int * sec, int * ms) +{ + const char * s = argv[index]; + if (s == nullptr || *s == '\0') return 0; + + char * endptr = nullptr; + long v = strtol(s, &endptr, 10); + if (endptr == s || v < 0 || v > 59) { + cliSerialPrint("%s: Invalid argument \"%s\"", argv[0], s); + return -1; + } + *sec = (int)v; + *ms = 0; + if (*endptr == '\0') return 1; + if (*endptr != '.') { + cliSerialPrint("%s: Invalid argument \"%s\"", argv[0], s); + return -1; + } + + int scale = 100; + for (const char * p = endptr + 1; *p; p++) { + if (*p < '0' || *p > '9') { + cliSerialPrint("%s: Invalid argument \"%s\"", argv[0], s); + return -1; + } + if (scale) { + *ms += (*p - '0') * scale; + scale /= 10; + } + } + return 2; +} + int cliBeep(const char ** argv) { int freq = BEEP_DEFAULT_FREQ; @@ -1083,10 +1117,11 @@ int cliSet(const char **argv) { if (!strcmp(argv[1], "rtc")) { struct gtm t; - int year, month, day, hour, minute, second; + int year, month, day, hour, minute, second, ms = 0; + int secOk = toSeconds(argv, 7, &second, &ms); if (toInt(argv, 2, &year) > 0 && toInt(argv, 3, &month) > 0 && toInt(argv, 4, &day) > 0 && toInt(argv, 5, &hour) > 0 && - toInt(argv, 6, &minute) > 0 && toInt(argv, 7, &second) > 0) { + toInt(argv, 6, &minute) > 0 && secOk > 0) { t.tm_year = year - TM_YEAR_BASE; t.tm_mon = month - 1; t.tm_mday = day; @@ -1095,14 +1130,15 @@ int cliSet(const char **argv) t.tm_sec = second; // update local timestamp and get wday calculated g_rtcTime = gmktime(&t); - rtcSetTime(&t); + // the CLI is driven by a host, the menu is where someone sets it by hand + rtcSetTimeAt(&t, (uint16_t)ms); #if defined(DEBUG) const struct RtcCalibReport * rep = rtcGetCalibrationReport(); if (rep->elapsed != 0) { int32_t ppm10 = 0; if (rep->elapsed > 0) - ppm10 = (int32_t)(((int64_t)rep->error * 10000000) / rep->elapsed); - cliSerialPrint("rtc drift = %d s over %d s (%d ppm x10)", (int)rep->error, + ppm10 = (int32_t)(((int64_t)rep->errorMs * 10000) / rep->elapsed); + cliSerialPrint("rtc drift = %d ms over %d s (%d ppm x10)", (int)rep->errorMs, (int)rep->elapsed, (int)ppm10); } int32_t units = rtcGetCalibration(); @@ -1525,8 +1561,18 @@ int cliDisplay(const char ** argv) } else if (!strcmp(argv[1], "rtc")) { struct gtm utm; + uint8_t sw100 = g_ms100; gettime(&utm); - cliSerialPrint("rtc = %4d-%02d-%02d %02d:%02d:%02d.%02d0", utm.tm_year+TM_YEAR_BASE, utm.tm_mon+1, utm.tm_mday, utm.tm_hour, utm.tm_min, utm.tm_sec, g_ms100); + cliSerialPrint("rtc = %4d-%02d-%02d %02d:%02d:%02d.%02d0", utm.tm_year+TM_YEAR_BASE, utm.tm_mon+1, utm.tm_mday, utm.tm_hour, utm.tm_min, utm.tm_sec, sw100); + + // gettime() reports the software clock, which free runs on the system tick + struct gtm htm = {}; + uint16_t hwMs = rtcGetTimeMs(&htm); + cliSerialPrint("rtc hw = %4d-%02d-%02d %02d:%02d:%02d.%03d", htm.tm_year+TM_YEAR_BASE, + htm.tm_mon+1, htm.tm_mday, htm.tm_hour, htm.tm_min, htm.tm_sec, (int)hwMs); + int32_t skew = (int32_t)(((int64_t)gmktime(&utm) - gmktime(&htm)) * 1000 + + (int32_t)sw100 * 10 - hwMs); + cliSerialPrint("rtc software clock offset = %d ms", (int)skew); int32_t units = rtcGetCalibration(); cliSerialPrint("rtc calibration = %d units (%d ppm x10), reference = %u", (int)units, (int)rtcCalibrationPpm10(units), diff --git a/radio/src/rtc.cpp b/radio/src/rtc.cpp index a7844fe138d..0640739f5ac 100644 --- a/radio/src/rtc.cpp +++ b/radio/src/rtc.cpp @@ -495,11 +495,12 @@ uint8_t rtcAdjust(uint16_t year, uint8_t mon, uint8_t day, uint8_t hour, uint8_t #define RTC_CALIB_MIN_ELAPSED SECS_PER_DAY // no reliable measurement below that #define RTC_CALIB_MAX_ELAPSED (10 * 365 * SECS_PER_DAY) // nonsense reference -#define RTC_CALIB_MIN_ERROR 4 // [s] below that it is input noise -#define RTC_CALIB_SET_ACCURACY 2 // [s] hand setting accuracy +#define RTC_CALIB_MIN_ERROR 4000 // [ms] below that it is input noise +#define RTC_CALIB_MIN_ERROR_TIMED 500 // [ms] host timed: link latency only +#define RTC_CALIB_SET_ACCURACY 2000 // [ms] hand setting accuracy #define RTC_CALIB_MAX_PPM 200 // beyond any crystal: clock was set -#define RTC_CALIB_TZ_STEP 900 // [s] time zone granularity -#define RTC_CALIB_TZ_BAND 60 // [s] tolerance around it +#define RTC_CALIB_TZ_STEP 900000 // [ms] time zone granularity +#define RTC_CALIB_TZ_BAND 60000 // [ms] tolerance around it #define RTC_CALIB_MIN_YEAR 101 // 2001, RTC resets to 2000 #define RTC_CALIB_SESSION_GAP (5 * 60 * 100) // [10ms] same edit session @@ -508,6 +509,7 @@ static bool rtcCalibSession = false; static tmr10ms_t rtcCalibLastSet = 0; static gtime_t rtcCalibSessionRef = 0; static gtime_t rtcCalibSessionRtc = 0; +static uint16_t rtcCalibSessionMs = 0; static int32_t rtcCalibSessionUnits = 0; static bool rtcCalibSessionValid = false; @@ -526,10 +528,11 @@ int32_t rtcCalibrationUnits(int32_t ppm) static void rtcCalibrationStart() { struct gtm before = {}; - rtcGetTime(&before); + uint16_t ms = rtcGetTimeMs(&before); rtcCalibSessionValid = (before.tm_year >= RTC_CALIB_MIN_YEAR); rtcCalibSessionRtc = rtcCalibSessionValid ? gmktime(&before) : 0; + rtcCalibSessionMs = rtcCalibSessionValid ? ms : 0; rtcCalibSessionRef = rtcGetCalibrationRef(); rtcCalibSessionUnits = rtcGetCalibration(); rtcCalibSession = true; @@ -562,11 +565,11 @@ const char * rtcCalibrationResultText(uint8_t result) // Recomputed from the calibration in use at session start, so that successive // edits converge instead of piling up -static uint8_t rtcCalibrationUpdate(gtime_t newTime) +static uint8_t rtcCalibrationUpdate(gtime_t newTime, uint16_t newMs, bool timed) { #if defined(DEBUG) rtcCalibReport.elapsed = 0; - rtcCalibReport.error = 0; + rtcCalibReport.errorMs = 0; #endif // Initial setting, or clock that lost power @@ -574,24 +577,27 @@ static uint8_t rtcCalibrationUpdate(gtime_t newTime) if (!rtcCalibSessionValid) return RTC_CALIB_CLOCK_INVALID; gtime_t elapsed = newTime - rtcCalibSessionRef; - gtime_t error = rtcCalibSessionRtc - newTime; // > 0 when the clock runs fast + // > 0 when the clock runs fast + int64_t errorMs = ((int64_t)rtcCalibSessionRtc - newTime) * 1000 + + rtcCalibSessionMs - newMs; #if defined(DEBUG) rtcCalibReport.elapsed = elapsed; - rtcCalibReport.error = error; + rtcCalibReport.errorMs = (int32_t)limit(INT32_MIN, errorMs, INT32_MAX); #endif if (elapsed < 0) return RTC_CALIB_REF_AHEAD; if (elapsed < (gtime_t)RTC_CALIB_MIN_ELAPSED) return RTC_CALIB_TOO_SOON; if (elapsed > (gtime_t)RTC_CALIB_MAX_ELAPSED) return RTC_CALIB_REF_TOO_OLD; - int64_t absError = (error < 0) ? -(int64_t)error : (int64_t)error; + int64_t absError = (errorMs < 0) ? -errorMs : errorMs; // Too small to tell from input noise - if (absError < RTC_CALIB_MIN_ERROR) return RTC_CALIB_TOO_SMALL; + if (absError < (timed ? RTC_CALIB_MIN_ERROR_TIMED : RTC_CALIB_MIN_ERROR)) + return RTC_CALIB_TOO_SMALL; // Faster than any crystal: the clock was moved - if (absError * 1000000 > (int64_t)elapsed * RTC_CALIB_MAX_PPM) return RTC_CALIB_TOO_LARGE; + if (absError * 1000 > (int64_t)elapsed * RTC_CALIB_MAX_PPM) return RTC_CALIB_TOO_LARGE; // A whole time zone step is a time zone or DST change if (absError >= RTC_CALIB_TZ_STEP - RTC_CALIB_TZ_BAND) { @@ -600,11 +606,12 @@ static uint8_t rtcCalibrationUpdate(gtime_t newTime) return RTC_CALIB_TIME_ZONE; } - // Do not over correct on a short measurement - absError -= RTC_CALIB_SET_ACCURACY; + // Only a hand set needs that margin, a host timed one is good to the link latency + if (!timed) absError -= RTC_CALIB_SET_ACCURACY; - int32_t delta = (int32_t)((absError * RTC_CALIB_UNITS_PER_SECOND + elapsed / 2) / elapsed); - if (error < 0) delta = -delta; + int64_t denom = (int64_t)elapsed * 1000; + int32_t delta = (int32_t)((absError * RTC_CALIB_UNITS_PER_SECOND + denom / 2) / denom); + if (errorMs < 0) delta = -delta; int32_t units = rtcCalibSessionUnits - delta; if (units > RTC_CALIB_UNIT_MAX) units = RTC_CALIB_UNIT_MAX; @@ -612,8 +619,8 @@ static uint8_t rtcCalibrationUpdate(gtime_t newTime) rtcSetCalibration(units); - TRACE("RTC drift %d s over %d s, calibration %d -> %d units (%d -> %d ppm x10)", - (int)error, (int)elapsed, (int)rtcCalibSessionUnits, (int)units, + TRACE("RTC drift %d ms over %d s, calibration %d -> %d units (%d -> %d ppm x10)", + (int)errorMs, (int)elapsed, (int)rtcCalibSessionUnits, (int)units, (int)rtcCalibrationPpm10(rtcCalibSessionUnits), (int)rtcCalibrationPpm10(units)); @@ -630,13 +637,14 @@ void rtcResetCalibration() rtcCalibLastSet = 0; } -void rtcSetTime(const struct gtm * t) +// A host timed set is a one shot, never part of a menu edit session +static void rtcSetTimeInternal(const struct gtm * t, uint16_t ms, bool timed) { struct gtm tm = *t; gtime_t newTime = gmktime(&tm); tmr10ms_t now = get_tmr10ms(); - if (!rtcCalibSession || (now - rtcCalibLastSet) > RTC_CALIB_SESSION_GAP) { + if (timed || !rtcCalibSession || (now - rtcCalibLastSet) > RTC_CALIB_SESSION_GAP) { rtcCalibrationStart(); } rtcCalibLastSet = now; @@ -644,13 +652,23 @@ void rtcSetTime(const struct gtm * t) rtcDriverSetTime(t); #if defined(DEBUG) - rtcCalibReport.result = rtcCalibrationUpdate(newTime); + rtcCalibReport.result = rtcCalibrationUpdate(newTime, ms, timed); #else - rtcCalibrationUpdate(newTime); + rtcCalibrationUpdate(newTime, ms, timed); #endif rtcSetCalibrationRef(newTime); } +void rtcSetTime(const struct gtm * t) +{ + rtcSetTimeInternal(t, 0, false); +} + +void rtcSetTimeAt(const struct gtm * t, uint16_t ms) +{ + rtcSetTimeInternal(t, ms, true); +} + bool rtcIsValid() { struct gtm t; diff --git a/radio/src/rtc.h b/radio/src/rtc.h index 661284c5aaf..d4bc6925ee9 100644 --- a/radio/src/rtc.h +++ b/radio/src/rtc.h @@ -52,12 +52,15 @@ extern uint8_t g_ms100; // global to allow time set function to reset to zero bool rtcIsValid(); void rtcInit(); void rtcSetTime(const struct gtm * tm); +// Host timed the second boundary itself, ms is how far into it the true time was +void rtcSetTimeAt(const struct gtm * tm, uint16_t ms); gtime_t gmktime (struct gtm *tm); uint8_t rtcAdjust(uint16_t year, uint8_t mon, uint8_t day, uint8_t hour, uint8_t min, uint8_t sec); // Driver interface, rtcSetTime() wraps rtcDriverSetTime() void rtcDriverSetTime(const struct gtm * tm); void rtcGetTime(struct gtm * tm); +uint16_t rtcGetTimeMs(struct gtm * tm); // fills tm, returns 0..999 within that second // Smooth calibration, one unit is one clock pulse out of 2^20 (~0.954 ppm) #define RTC_CALIB_UNITS_PER_SECOND 1048576 @@ -86,7 +89,7 @@ enum { struct RtcCalibReport { uint8_t result; gtime_t elapsed; - gtime_t error; + int32_t errorMs; // clamped, > 0 when the clock runs fast }; const struct RtcCalibReport * rtcGetCalibrationReport(); diff --git a/radio/src/targets/common/arm/stm32/rtc_driver.cpp b/radio/src/targets/common/arm/stm32/rtc_driver.cpp index 7eaf5ab5ba1..85d2c65e6f4 100644 --- a/radio/src/targets/common/arm/stm32/rtc_driver.cpp +++ b/radio/src/targets/common/arm/stm32/rtc_driver.cpp @@ -41,7 +41,9 @@ void rtcDriverSetTime(const struct gtm * t) HAL_RTC_SetDate(&rtc, &RTC_DateStruct, RTC_FORMAT_BIN); } -void rtcGetTime(struct gtm * t) +// SSR must be read before TR, and TR before DR, or the shadow registers stay +// frozen. HAL does that within one call, so seconds and sub-seconds match. +uint16_t rtcGetTimeMs(struct gtm * t) { RTC_TimeTypeDef RTC_TimeStruct; RTC_DateTypeDef RTC_DateStruct; @@ -55,6 +57,16 @@ void rtcGetTime(struct gtm * t) t->tm_year = RTC_DateStruct.Year + 100; // STM32 year is two decimals only (so base is currently 2000), gtm is based on number of years since 1900 t->tm_mon = RTC_DateStruct.Month - 1; t->tm_mday = RTC_DateStruct.Date; + + // SSR counts down over PREDIV_S+1 steps, 1/256 s each with the usual setup + uint32_t fraction = RTC_TimeStruct.SecondFraction; + if (fraction == 0 || RTC_TimeStruct.SubSeconds > fraction) return 0; + return (uint16_t)(((fraction - RTC_TimeStruct.SubSeconds) * 1000) / (fraction + 1)); +} + +void rtcGetTime(struct gtm * t) +{ + rtcGetTimeMs(t); } #if defined(RTC_CALR_CALM) && !defined(BOOT) diff --git a/radio/src/targets/simu/simulib.cpp b/radio/src/targets/simu/simulib.cpp index c615ce47b0e..1db8b815d5a 100644 --- a/radio/src/targets/simu/simulib.cpp +++ b/radio/src/targets/simu/simulib.cpp @@ -447,6 +447,12 @@ void rtcGetTime(struct gtm * t) { } +uint16_t rtcGetTimeMs(struct gtm * t) +{ + rtcGetTime(t); + return 0; +} + void rtcDriverSetTime(const struct gtm * t) { } From 444fbbd7c9cee3f4d5ae6272860402f410379e84 Mon Sep 17 00:00:00 2001 From: 3djc <3djc@gh.com> Date: Thu, 10 Sep 2026 11:15:33 +0200 Subject: [PATCH 5/5] add platform independent script to adjust time --- tools/set-radio-time.py | 164 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100755 tools/set-radio-time.py diff --git a/tools/set-radio-time.py b/tools/set-radio-time.py new file mode 100755 index 00000000000..4238abb22ce --- /dev/null +++ b/tools/set-radio-time.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Set an EdgeTX radio's clock from the host, over the CLI serial port. + +The radio must be in USB Serial (VCP) mode, running a build with CLI enabled. + +The command body is sent ahead of time and only the closing newline is timed, so +what has to land on the second boundary is a single byte rather than a whole +line. The radio treats a CLI set as host timed and skips the margin it allows +for someone turning the dials by hand, which leaves the drift measurement good +to the USB round trip, a few milliseconds. + +That accuracy is in the measurement, not in the resulting clock: the RTC is +written in whole seconds and keeps whatever sub-second phase it already had, so +the radio can still read up to a second away from the host afterwards. + +Every setting rewrites the drift reference, and two settings less than a day +apart are discarded, so do not put this on a short schedule or the radio never +learns its crystal error. Every day or two is enough for it to converge. +""" + +import argparse +import sys +import time + +try: + import serial + from serial.tools import list_ports +except ImportError: + sys.exit("pyserial is required: python3 -m pip install pyserial") + + +# radio/src/targets/common/arm/stm32/usbd_desc.c, fixed for the Windows ST driver +USB_CDC_VID = 0x0483 +USB_CDC_PID = 0x5740 + + +def _device(port): + """macOS lists both tty and cu for one device; only cu opens without DCD.""" + dev = port.device + if sys.platform == "darwin" and dev.startswith("/dev/tty."): + return "/dev/cu." + dev[len("/dev/tty."):] + return dev + + +def _looks_like_cdc(dev): + name = dev.upper() + return name.startswith("COM") or "TTYACM" in name or "USBMODEM" in name + + +def list_candidates(): + """Best first: exact USB ids, then self-named, then any plausible CDC port.""" + exact, named, generic = [], [], [] + for port in list_ports.comports(): + dev = _device(port) + if port.vid == USB_CDC_VID and port.pid == USB_CDC_PID: + bucket = exact + else: + blob = " ".join(x for x in (port.description, port.manufacturer, + port.product) if x).lower() + if "edgetx" in blob or "opentx" in blob: + bucket = named + elif _looks_like_cdc(dev): + bucket = generic + else: + continue + if dev not in bucket: + bucket.append(dev) + return exact, named, generic + + +def find_port(): + exact, named, generic = list_candidates() + if len(exact) > 1: + sys.exit("several radios found, pick one with --port:\n " + + "\n ".join(exact)) + for bucket in (exact, named, generic): + if bucket: + return bucket[0] + return None + + +def drain(ser, seconds): + out = bytearray() + end = time.time() + seconds + while time.time() < end: + chunk = ser.read(256) + if chunk: + out += chunk + else: + time.sleep(0.01) + return out.decode("ascii", "replace") + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("-p", "--port", + help="serial port, e.g. /dev/cu.usbmodem1234 on macOS, " + "/dev/ttyACM0 on Linux, COM4 on Windows " + "(default: autodetect)") + ap.add_argument("--list", action="store_true", + help="list the ports autodetection can see, then exit") + ap.add_argument("-u", "--utc", action="store_true", + help="send UTC instead of host local time") + ap.add_argument("-l", "--lead", type=float, default=0.004, + help="seconds to send the newline early, to cover USB latency " + "(default: 0.004)") + ap.add_argument("-n", "--dry-run", action="store_true", + help="show the command without opening the port") + ap.add_argument("-c", "--check", action="store_true", + help="run 'p rtc' afterwards and show the result") + args = ap.parse_args() + + if args.list: + for label, bucket in zip(("usb id match", "named", "possible"), + list_candidates()): + for dev in bucket: + print("%-12s %s" % (label, dev)) + return 0 + + port = args.port or find_port() + if not port and not args.dry_run: + sys.exit("no radio found; is it plugged in and set to USB Serial (VCP)?") + + # aim at the start of a whole second, far enough out to get the body there + target = int(time.time()) + 2 + tm = time.gmtime(target) if args.utc else time.localtime(target) + # the fractional second says the newline is meant to land on the boundary, + # which lets the radio skip the margin it allows for a hand set + body = "set rtc %04d %02d %02d %02d %02d %02d.000" % ( + tm.tm_year, tm.tm_mon, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec) + + if args.dry_run: + print("port: %s" % (port or "none found")) + print("would send: %s" % body) + return 0 + + with serial.Serial(port, 115200, timeout=0.05) as ser: + ser.write(b"\n") # wake the prompt + time.sleep(0.25) + ser.reset_input_buffer() + + ser.write(body.encode()) # everything except the newline + ser.flush() + + while time.time() < target - args.lead: + time.sleep(0.0005) + ser.write(b"\n") + ser.flush() + + print("%s (%s)" % (body, "UTC" if args.utc else "local")) + print(drain(ser, 1.0).strip()) + + if args.check: + ser.reset_input_buffer() + ser.write(b"p rtc\n") + ser.flush() + print(drain(ser, 1.0).strip()) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())