From 13c96b9424f21f273321fdf3e510a198c827632e Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 11:06:10 -0500 Subject: [PATCH 1/6] firmware: a serial command sets the real-time clock SetRTCToken (0x6474, 'dt') takes UTC seconds since 1970, big endian, and sets the RTC, which a TR+ with a battery keeps through power-off. Until now the clock could only be set from NTP or stepped an hour, minute or second at a time from the menu. tools/bench/setclock.py sends this computer's clock. --- .../Teensy/MinimalBoot/Common/Common_Defs.h | 1 + Source/Teensy/RemoteControl.ino | 17 +++++++++++++++ Source/Teensy/SerUSBIO.ino | 4 ++++ docs/ControlComms.md | 15 +++++++++++++ tools/bench/README.md | 1 + tools/bench/protocol.py | 2 ++ tools/bench/setclock.py | 21 +++++++++++++++++++ 7 files changed, 61 insertions(+) create mode 100755 tools/bench/setclock.py diff --git a/Source/Teensy/MinimalBoot/Common/Common_Defs.h b/Source/Teensy/MinimalBoot/Common/Common_Defs.h index 7feae537..76ed8058 100644 --- a/Source/Teensy/MinimalBoot/Common/Common_Defs.h +++ b/Source/Teensy/MinimalBoot/Common/Common_Defs.h @@ -51,6 +51,7 @@ //synch with win app: //all commands must start with 0x64 #define SetColorToken 0x6422 +#define SetRTCToken 0x6474 //dt #define LaunchFileToken 0x6444 #define PingToken 0x6455 #define PauseSIDToken 0x6466 //df diff --git a/Source/Teensy/RemoteControl.ino b/Source/Teensy/RemoteControl.ino index e3076195..f24e5926 100644 --- a/Source/Teensy/RemoteControl.ino +++ b/Source/Teensy/RemoteControl.ino @@ -111,6 +111,23 @@ FLASHMEM bool SetColorRef() return true; } +// Command: +// Set the TR's real-time clock, which the TR+'s battery keeps through power-off +// +// Workflow: +// Receive <-- SetRTCToken Token 0x6474 +// Receive <-- UTC time, seconds since 1970 (4 bytes, big endian) +// Send --> AckToken 0x64CC or FailToken 0x9B7F +FLASHMEM bool SetRTC() +{ + uint32_t SecsSince1970; + if (!GetUInt(&SecsSince1970, 4)) return false; + + Teensy3Clock.set(SecsSince1970); + C64TODfromRTC(); + return true; +} + // Command: // Set sub-song number of currently loaded SID // diff --git a/Source/Teensy/SerUSBIO.ino b/Source/Teensy/SerUSBIO.ino index 92480e73..da5e77d0 100644 --- a/Source/Teensy/SerUSBIO.ino +++ b/Source/Teensy/SerUSBIO.ino @@ -661,6 +661,10 @@ FLASHMEM void ProcessCommand() if(SetColorRef()) SendU16(AckToken); else SendU16(FailToken); break; + case SetRTCToken: //Set the real-time clock + if(SetRTC()) SendU16(AckToken); + else SendU16(FailToken); + break; case DebugToken: //'dg'Test/debug //for (int a=0; a<256; a++) CmdChannel->printf("\n%3d, // %3d '%c'", ToPETSCII(a), a, a); //Printf_dbg("isFab2x: %d\n", isFab2x()); diff --git a/docs/ControlComms.md b/docs/ControlComms.md index 5e9d3d5a..99ac19ca 100644 --- a/docs/ControlComms.md +++ b/docs/ControlComms.md @@ -118,6 +118,7 @@ A C64 reset the TeensyROM doesn't see (reset detect disabled in Settings) doesn' |GetDirectoryToken | 0x64DD | List directory contents (JSON format - deprecated) | |GetDirNDJSONToken | 0x64DE | List directory contents (NDJSON format) | |SetColorToken | 0x6422 | Set a TR UI color value | +|SetRTCToken | 0x6474 | Set the real-time clock | |PauseSIDToken | 0x6466 | Pause SID playback | |SetSIDSongToken | 0x6488 | Set sub-song number of currently loaded SID | |SIDSpeedLinToken | 0x6499 | Set SID playback speed (linear equation) | @@ -348,6 +349,20 @@ Sets one of the colors in the TR UI. Color is stored in EEPROM and IO space; the --- +### Set Real-Time Clock +Sets the TR's real-time clock. The time is UTC; the TR applies its time zone setting when it shows or passes on the time. On a TR+ with a battery, the clock keeps the time through power-off. + +**Workflow:** +| Direction | Data | +|---|---| +| Receive | `SetRTCToken` — `0x6474` | +| Receive | UTC time in seconds since 1970-01-01 (4 bytes, big endian) | +| Send | `AckToken 0x64CC` or `FailToken 0x9B7F` | + +**Handler:** `SetRTC()` + +--- + ### Pause SID Pauses/Resumes SID playback by sending an interrupt request to the C64. diff --git a/tools/bench/README.md b/tools/bench/README.md index 2e5844ec..b3734be8 100644 --- a/tools/bench/README.md +++ b/tools/bench/README.md @@ -56,6 +56,7 @@ whatever node appeared beside it. | `probe.py` | Which image is running -- main, minimal or silent -- and its build banner. | | `ls.py [path] [drive]` | List a directory, to see that a push landed where it was aimed (first 1000 entries). | | `reset.py` | Reset the C64 to the menu, and the board out of the minimal image. | +| `setclock.py [UTC time]` | Set the TR's real-time clock, to this computer's clock or an ISO 8601 UTC time such as `2026-10-16T17:00:00`. Only at the TR menu. | | `hostops.py` | Installing and removing a host, and `run_step` -- driving anything that ends in a reboot and reading which of the two shapes came back. Also the phrases a reboot is judged by: `INSTALLED`, `REMOVED` and `FINISHED_NORMALLY`. Shared by the `host*.py` scripts and by `exttest.py`, whose exit status is `finished_normally()`'s answer, so a phrase edited here moves that verdict; and by `test_bounds.py`, which patches its timeouts to test them, and `test_protocol.py`, which pins the phrases against the firmware's own `VmFail::describe()`. | | `trlink.py` | The shared library the above are built on. | | `protocol.py` | Every value that goes on the wire, named once. | diff --git a/tools/bench/protocol.py b/tools/bench/protocol.py index a8f38414..81032a05 100644 --- a/tools/bench/protocol.py +++ b/tools/bench/protocol.py @@ -21,6 +21,7 @@ POST_FILE = 0x64BB DELETE_FILE = 0x64CF GET_DIR_NDJSON = 0x64DE +SET_RTC = 0x6474 # Replies. ACK = 0x64CC @@ -49,6 +50,7 @@ 'PostFileToken': POST_FILE, 'DeleteFileToken': DELETE_FILE, 'GetDirNDJSONToken': GET_DIR_NDJSON, + 'SetRTCToken': SET_RTC, 'AckToken': ACK, 'FailToken': FAIL, 'FWFullToken': FW_FULL, diff --git a/tools/bench/setclock.py b/tools/bench/setclock.py new file mode 100755 index 00000000..cf232167 --- /dev/null +++ b/tools/bench/setclock.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Set the TR's real-time clock. setclock.py [UTC time] + +The time is UTC, to the second: this computer's clock, or an ISO 8601 time +such as 2026-10-16T17:00:00. The board refuses it with "Busy!" unless it is +sitting at its own menu. +""" +import datetime, sys +from protocol import SET_RTC, to_board +from trlink import Link + +if len(sys.argv) > 1: + when = datetime.datetime.fromisoformat(sys.argv[1]).replace(tzinfo=datetime.timezone.utc) +else: + when = datetime.datetime.now(datetime.timezone.utc) +with Link() as tr: + tr.drain(0.3) + tr.wr(to_board(SET_RTC) + to_board(int(when.timestamp()), 4)) + tr.ack('set clock') +print('clock set to', when.strftime('%Y-%m-%d %H:%M:%S UTC')) From 2e6c1d37dc01b10bd791629e42419cb6497a3666 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 11:08:16 -0500 Subject: [PATCH 2/6] firmware: setclock fixes from review: honor a UTC offset, refuse times four bytes can't hold SetRTC no longer refreshes the IO1 TOD registers: every C64 reader asks for them first, which refreshes them. --- Source/Teensy/RemoteControl.ino | 1 - tools/bench/setclock.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Source/Teensy/RemoteControl.ino b/Source/Teensy/RemoteControl.ino index f24e5926..f6e281d3 100644 --- a/Source/Teensy/RemoteControl.ino +++ b/Source/Teensy/RemoteControl.ino @@ -124,7 +124,6 @@ FLASHMEM bool SetRTC() if (!GetUInt(&SecsSince1970, 4)) return false; Teensy3Clock.set(SecsSince1970); - C64TODfromRTC(); return true; } diff --git a/tools/bench/setclock.py b/tools/bench/setclock.py index cf232167..1018374c 100755 --- a/tools/bench/setclock.py +++ b/tools/bench/setclock.py @@ -11,11 +11,17 @@ from trlink import Link if len(sys.argv) > 1: - when = datetime.datetime.fromisoformat(sys.argv[1]).replace(tzinfo=datetime.timezone.utc) + when = datetime.datetime.fromisoformat(sys.argv[1]) + if when.tzinfo is None: + when = when.replace(tzinfo=datetime.timezone.utc) + when = when.astimezone(datetime.timezone.utc) else: when = datetime.datetime.now(datetime.timezone.utc) +secs = int(when.timestamp()) +if not 0 <= secs < 1 << 32: + raise SystemExit(f'{when:%Y-%m-%d %H:%M:%S} UTC is outside 1970-2106, the range four bytes of seconds hold') with Link() as tr: tr.drain(0.3) - tr.wr(to_board(SET_RTC) + to_board(int(when.timestamp()), 4)) + tr.wr(to_board(SET_RTC) + to_board(secs, 4)) tr.ack('set clock') print('clock set to', when.strftime('%Y-%m-%d %H:%M:%S UTC')) From 1353029fcff791ada7d67c2abba7dc1be02bf198 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 11:11:21 -0500 Subject: [PATCH 3/6] bench: setclock refuses a time in the last second of 1969 int() truncates toward zero, so 1969-12-31T23:59:59.5 became 0 and passed the range check. --- tools/bench/setclock.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/bench/setclock.py b/tools/bench/setclock.py index 1018374c..4e91ee62 100755 --- a/tools/bench/setclock.py +++ b/tools/bench/setclock.py @@ -6,7 +6,7 @@ such as 2026-10-16T17:00:00. The board refuses it with "Busy!" unless it is sitting at its own menu. """ -import datetime, sys +import datetime, math, sys from protocol import SET_RTC, to_board from trlink import Link @@ -17,7 +17,7 @@ when = when.astimezone(datetime.timezone.utc) else: when = datetime.datetime.now(datetime.timezone.utc) -secs = int(when.timestamp()) +secs = math.floor(when.timestamp()) if not 0 <= secs < 1 << 32: raise SystemExit(f'{when:%Y-%m-%d %H:%M:%S} UTC is outside 1970-2106, the range four bytes of seconds hold') with Link() as tr: From 8b4bf72de9cdde1683ed1985990cc29828834daa Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Fri, 16 Oct 2026 11:12:28 -0500 Subject: [PATCH 4/6] docs: the menu shows a newly set clock after its next reset --- docs/ControlComms.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ControlComms.md b/docs/ControlComms.md index 99ac19ca..1d5c2abd 100644 --- a/docs/ControlComms.md +++ b/docs/ControlComms.md @@ -350,7 +350,7 @@ Sets one of the colors in the TR UI. Color is stored in EEPROM and IO space; the --- ### Set Real-Time Clock -Sets the TR's real-time clock. The time is UTC; the TR applies its time zone setting when it shows or passes on the time. On a TR+ with a battery, the clock keeps the time through power-off. +Sets the TR's real-time clock. The time is UTC; the TR applies its time zone setting when it shows or passes on the time. On a TR+ with a battery, the clock keeps the time through power-off. The menu's clock shows the new time after its next reset. **Workflow:** | Direction | Data | From ab5006b38b29dfaeb9e72cab29d81a8ffafd2452 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:16:41 -0500 Subject: [PATCH 5/6] firmware: a refused setclock no longer leaves its time bytes to run as commands --- Source/Teensy/SerUSBIO.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Teensy/SerUSBIO.ino b/Source/Teensy/SerUSBIO.ino index da5e77d0..eff5e2d4 100644 --- a/Source/Teensy/SerUSBIO.ino +++ b/Source/Teensy/SerUSBIO.ino @@ -612,6 +612,9 @@ FLASHMEM void ProcessCommand() if (CurrentIOHandler != IOH_TeensyROM && !ASIDAllowed) { + //left unread, the time bytes would each run as a command of their own: 'e' resets EEPROM + uint32_t RefusedTime; + if (TokenVal == SetRTCToken) GetUInt(&RefusedTime, 4); SendU16(FailToken); CmdChannel->print("Busy!\n"); return; From e110008f875dba47797b8f023cbf695f4bf35e34 Mon Sep 17 00:00:00 2001 From: Kelly Fox Date: Thu, 22 Oct 2026 19:27:54 -0500 Subject: [PATCH 6/6] firmware: the minimal image's refused setclock drains its time bytes too --- Source/Teensy/MinimalBoot/Min_SerUSBIO.ino | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Teensy/MinimalBoot/Min_SerUSBIO.ino b/Source/Teensy/MinimalBoot/Min_SerUSBIO.ino index 74563626..a73531c9 100644 --- a/Source/Teensy/MinimalBoot/Min_SerUSBIO.ino +++ b/Source/Teensy/MinimalBoot/Min_SerUSBIO.ino @@ -52,6 +52,9 @@ FLASHMEM void ServiceSerial(Stream *ThisCmdChannel) SendU16(FWMinimalToken); return; } + //left unread, the time bytes would each run as a command of their own: a 0x64 can reset or launch + uint32_t RefusedTime; + if (inVal == SetRTCToken) GetUInt(&RefusedTime, 4); SendU16(FailToken); CmdChannel->print("Busy!\n"); return;