diff --git a/Filter_clock_sync.py b/Filter_clock_sync.py new file mode 100644 index 0000000..12f0ba5 --- /dev/null +++ b/Filter_clock_sync.py @@ -0,0 +1,64 @@ +# PlatformIO monitor filter: automatic clock sync for Meck devices +# +# When a Meck device boots with no valid RTC time, it prints "MECK_CLOCK_REQ" +# over serial. This filter watches for that line and responds immediately +# with "clock sync \r\n", setting the device's real-time clock to +# the host computer's current time. +# +# The sync is completely transparent — the user just sees it happen in the +# boot log. If the RTC already has valid time, the device never sends the +# request and this filter does nothing. +# +# Install: place this file in /monitor/filter_clock_sync.py +# Enable: add "clock_sync" to monitor_filters in platformio.ini +# +# Works with: PlatformIO Core >= 6.0 + +import time + +from platformio.device.monitor.filters.base import DeviceFilter + + +class ClockSync(DeviceFilter): + NAME = "clock_sync" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._buf = bytearray() + self._synced = False + + def rx(self, text): + """Called with each chunk of data received from the device.""" + if self._synced: + return text + + # Accumulate into a line buffer to detect MECK_CLOCK_REQ + if isinstance(text, str): + self._buf.extend(text.encode("utf-8", errors="replace")) + else: + self._buf.extend(text) + + if b"MECK_CLOCK_REQ" in self._buf: + epoch = int(time.time()) + response = "clock sync {}\r\n".format(epoch) + try: + # Write directly to the serial port + self.miniterm.serial.write(response.encode("utf-8")) + except Exception as e: + # Fallback: shouldn't happen, but don't crash the monitor + import sys + print( + "\n[clock_sync] Failed to auto-sync: {}".format(e), + file=sys.stderr, + ) + self._synced = True + self._buf = bytearray() + elif len(self._buf) > 2048: + # Prevent unbounded growth — keep tail only + self._buf = self._buf[-256:] + + return text + + def tx(self, text): + """Called with each chunk of data sent from terminal to device.""" + return text \ No newline at end of file diff --git a/Serial Settings Guide.md b/Serial Settings Guide.md index b1250ae..7983aa7 100644 --- a/Serial Settings Guide.md +++ b/Serial Settings Guide.md @@ -69,6 +69,7 @@ All commands follow a simple pattern: `get` to read, `set` to write. | `get presets` | List all radio presets with parameters | | `get pubkey` | Device public key (hex) | | `get firmware` | Firmware version string | +| `clock` | Current RTC time (UTC + epoch) | **4G variant only:** @@ -294,6 +295,68 @@ To clear a custom APN and revert to auto-detection on next boot: set apn ``` +### Clock Sync + +Set the device's real-time clock from a Unix timestamp. This is especially important for the T5S3 E-Paper Pro which has no GPS to auto-set the clock. These are standalone commands (not `get`/`set` prefixed) — matching the same `clock sync` command used on MeshCore repeaters. + +#### View Current Time + +``` +clock +``` + +Output: + +``` + > 2026-03-13 04:22:15 UTC (epoch: 1773554535) +``` + +If the clock has never been set: + +``` + > not set (epoch: 0) +``` + +#### Sync Clock from Serial + +``` +clock sync 1773554535 +``` + +The value must be a Unix epoch timestamp in the 2024–2036 range. + +**Quick one-liner from your terminal (macOS / Linux / WSL):** + +``` +echo "clock sync $(date +%s)" > /dev/ttyACM0 +``` + +Or paste directly into the Arduino IDE Serial Monitor: + +``` +clock sync 1773554535 +``` + +**Tip:** On macOS/Linux, run `date +%s` to get the current epoch. On Windows PowerShell: `[int](Get-Date -UFormat %s)`. + +#### Boot-Time Auto-Sync (T5S3) + +When the T5S3 boots with no valid RTC time and detects a USB serial host is connected, it sends a `MECK_CLOCK_REQ` handshake over serial. If you're using PlatformIO's serial monitor (`pio device monitor`), the built-in `clock_sync` monitor filter responds automatically with the host computer's current time — no user action required. The sync appears transparently in the boot log: + +``` +MECK_CLOCK_REQ + (Waiting 3s for clock sync from host...) + > Clock synced to 1773554535 +``` + +If no USB host is connected (e.g. running on battery), the sync window is skipped entirely with no boot delay. + +**Manual fallback:** If you're using a serial terminal that doesn't have the filter (e.g. `screen`, PuTTY), you can paste a `clock sync` command during the 3-second window, or any time after boot: + +``` +clock sync $(date +%s) +``` + ### System Commands | Command | Description | diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2dbb91f..4388fd1 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -2290,6 +2290,14 @@ void MyMesh::checkCLIRescueCmd() { char hex[PUB_KEY_SIZE * 2 + 1]; mesh::Utils::toHex(hex, self_id.pub_key, PUB_KEY_SIZE); Serial.printf(" pubkey: %s\n", hex); + { + uint32_t clk = getRTCClock()->getCurrentTime(); + if (clk > 1704067200UL) { + Serial.printf(" clock: %lu (valid)\n", (unsigned long)clk); + } else { + Serial.printf(" clock: not set\n"); + } + } // List channels Serial.println(" channels:"); bool chFound = false; @@ -2694,6 +2702,45 @@ void MyMesh::checkCLIRescueCmd() { Serial.printf(" Error: unknown setting '%s' (try 'help')\n", config); } + // ===================================================================== + // CLOCK commands (standalone — matches repeater admin convention) + // ===================================================================== + } else if (memcmp(cli_command, "clock sync ", 11) == 0) { + uint32_t epoch = (uint32_t)strtoul(&cli_command[11], nullptr, 10); + if (epoch > 1704067200UL && epoch < 2082758400UL) { + getRTCClock()->setCurrentTime(epoch); + Serial.printf(" > clock synced to %lu\n", (unsigned long)epoch); + } else { + Serial.println(" Error: invalid epoch (must be 2024-2036 range)"); + Serial.println(" Hint: on macOS/Linux run: date +%s"); + } + } else if (strcmp(cli_command, "clock sync") == 0) { + // Bare "clock sync" without a value — show usage + Serial.println(" Usage: clock sync "); + Serial.println(" Hint: clock sync $(date +%s)"); + } else if (strcmp(cli_command, "clock") == 0) { + uint32_t t = getRTCClock()->getCurrentTime(); + if (t > 1704067200UL) { + // Break epoch into human-readable UTC + uint32_t ep = t; + int s = ep % 60; ep /= 60; + int mi = ep % 60; ep /= 60; + int h = ep % 24; ep /= 24; + int yr = 1970; + while (true) { int d = ((yr%4==0&&yr%100!=0)||yr%400==0)?366:365; if(ep<(uint32_t)d) break; ep-=d; yr++; } + int mo = 1; + while (true) { + static const uint8_t dm[]={31,28,31,30,31,30,31,31,30,31,30,31}; + int d = (mo==2&&((yr%4==0&&yr%100!=0)||yr%400==0))?29:dm[mo-1]; + if(ep<(uint32_t)d) break; ep-=d; mo++; + } + int dy = ep + 1; + Serial.printf(" > %04d-%02d-%02d %02d:%02d:%02d UTC (epoch: %lu)\n", + yr, mo, dy, h, mi, s, (unsigned long)t); + } else { + Serial.printf(" > not set (epoch: %lu)\n", (unsigned long)t); + } + // ===================================================================== // HELP command // ===================================================================== @@ -2713,6 +2760,11 @@ void MyMesh::checkCLIRescueCmd() { Serial.println(" int.thresh <0|14+> Interference threshold dB (0=off, 14=typical)"); Serial.println(" gps.baud GPS baud (0=default, reboot to apply)"); Serial.println(""); + Serial.println(" Clock:"); + Serial.println(" clock Show current RTC time (UTC)"); + Serial.println(" clock sync Set RTC from Unix timestamp"); + Serial.println(" Hint: clock sync $(date +%s)"); + Serial.println(""); Serial.println(" Compound commands:"); Serial.println(" get all Dump all settings"); Serial.println(" get radio Show all radio params"); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index d230d05..260ca53 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -1189,14 +1189,57 @@ void setup() { } #endif - // RTC diagnostic — verify the auto-discovered RTC is working + // RTC diagnostic + boot-time serial clock sync (T5S3 has no GPS) #if defined(LilyGo_T5S3_EPaper_Pro) { uint32_t rtcTime = rtc_clock.getCurrentTime(); Serial.printf("setup() - RTC time: %lu (valid=%s)\n", rtcTime, rtcTime > 1700000000 ? "YES" : "NO"); if (rtcTime < 1700000000) { - Serial.println("setup() - RTC has no valid time (will be set by companion app)"); + // No valid time. If a USB host has the serial port open (Serial + // evaluates true on ESP32-S3 native CDC), request an automatic + // clock sync. The PlatformIO monitor filter "clock_sync" watches + // for MECK_CLOCK_REQ and responds immediately with the host time. + // Manual sync is also accepted: type "clock sync " in any + // serial terminal. + if (Serial) { + Serial.println("MECK_CLOCK_REQ"); + Serial.println(" (Waiting 3s for clock sync from host...)"); + + char syncBuf[64]; + int syncPos = 0; + unsigned long syncDeadline = millis() + 3000; + bool synced = false; + + while (millis() < syncDeadline && !synced) { + while (Serial.available() && syncPos < (int)sizeof(syncBuf) - 1) { + char c = Serial.read(); + if (c == '\r' || c == '\n') { + if (syncPos > 0) { + syncBuf[syncPos] = '\0'; + if (memcmp(syncBuf, "clock sync ", 11) == 0) { + uint32_t epoch = (uint32_t)strtoul(&syncBuf[11], nullptr, 10); + if (epoch > 1704067200UL && epoch < 2082758400UL) { + rtc_clock.setCurrentTime(epoch); + Serial.printf(" > Clock synced to %lu\n", (unsigned long)epoch); + synced = true; + } + } + syncPos = 0; + } + break; + } + syncBuf[syncPos++] = c; + } + if (!synced) delay(10); + } + if (!synced) { + Serial.println(" > No clock sync received, continuing boot"); + Serial.println(" > Use 'clock sync ' any time to sync later"); + } + } else { + Serial.println("setup() - RTC not set, no serial host detected (skipping sync window)"); + } } } #endif diff --git a/platformio.ini b/platformio.ini index 57de8a3..eff84da 100644 --- a/platformio.ini +++ b/platformio.ini @@ -56,7 +56,7 @@ build_src_filter = [esp32_base] extends = arduino_base platform = platformio/espressif32@6.11.0 -monitor_filters = esp32_exception_decoder +monitor_filters = esp32_exception_decoder, clock_sync extra_scripts = merge-bin.py build_flags = ${arduino_base.build_flags} -D ESP32_PLATFORM diff --git a/src/helpers/BaseChatMesh.cpp b/src/helpers/BaseChatMesh.cpp index fb820a2..ff15d6b 100644 --- a/src/helpers/BaseChatMesh.cpp +++ b/src/helpers/BaseChatMesh.cpp @@ -56,6 +56,14 @@ void BaseChatMesh::sendAckTo(const ContactInfo& dest, uint32_t ack_hash) { } void BaseChatMesh::bootstrapRTCfromContacts() { + // If the RTC already has a sane time (e.g. hardware RTC like PCF8563, or + // GPS-synced), don't overwrite it with a potentially stale contact lastmod. + // This bootstrap is only useful for boards with no hardware RTC at all. + uint32_t current = getRTCClock()->getCurrentTime(); + if (current > 1704067200UL) { // Jan 1 2024 — matches EPOCH_MIN_SANE + return; + } + uint32_t latest = 0; for (int i = 0; i < num_contacts; i++) { if (contacts[i].lastmod > latest) {