From 1b8b36abb35a14c3a417d23adc016b17d1f9ff72 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Fri, 16 Oct 2026 07:54:03 +0000 Subject: [PATCH] Expose a bounded calendar snapshot through RTC device reads --- README.md | 7 +- docs/testing.md | 20 +++++ docs/validation/text-read-2026-10-16.txt | 109 +++++++++++++++++++++++ src/dmrtc.c | 47 +++++++++- tests/dmrtc_test.c | 70 ++++++++++++++- 5 files changed, 247 insertions(+), 6 deletions(-) create mode 100644 docs/validation/text-read-2026-10-16.txt diff --git a/README.md b/README.md index 9b603ec..024e6e7 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,12 @@ Device files implement `DMDRVI_IOCTL_RTC_GET_TIME`, `SET_TIME`, `GET_STATUS`, `GET_INFO`, and optional `INVALIDATE`. The shared calendar, capabilities and status structures belong to dmdrvi. Read commands require a readable handle; SET_TIME/INVALIDATE require write access. Private ioctl numbers are removed. -There is no byte-stream encoding. See the [shared contract](https://github.com/choco-technologies/dmdrvi/blob/main/docs/rtc.md). +`cat /dev/rtc` reads a 20-byte diagnostic line, `YYYY-MM-DDTHH:MM:SS\n`, +with no NUL terminator or implicit timezone. The first nonempty read captures a +calendar snapshot for that open handle; partial reads and seeks reuse it, and +byte offset 20 or later returns EOF. Reopen the device for fresh time. An unset +calendar returns `-ENODATA` without caching a failed read. Writes remain +unsupported; use the standard ioctl contract to set time. See the [shared contract](https://github.com/choco-technologies/dmdrvi/blob/main/docs/rtc.md). GET_INFO reports 2000–2099, one-second resolution, SET_TIME and INVALIDATE. This driver does not advertise hardware continuity-loss detection; the console diff --git a/docs/testing.md b/docs/testing.md index 4959d2c..02d8f34 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -96,3 +96,23 @@ Validated STM32F7 compressed packages: - `dmrtc_port.dmfc`: `6f8f416592d6a7a8eccfe794d909536824fc5ff02bad47dd434a9ae84a26390c` - `rtc.dmfc`: `d5c120a682f5fab69631444fc6edc45d2df42b8eeea38302acc21afb59962810` - `test_dmrtc.dmfc`: `2106b08b510d912fa778fd67eb44a9903808cac87eb42214d0295dcae43ab10a` + +## Diagnostic text reads — 2026-10-16 + +`cat /dev/rtc` now prints a bounded 20-byte civil calendar line and reaches EOF. +The first nonempty read snapshots time per open handle. Partial reads and seeks +use that snapshot, even if the calendar changes; a new open obtains fresh time. +An unset calendar returns ENODATA and can be retried after setting time. + +- Native ctest: 3/3 targets, including 14/14 driver steps. New checks cover + partial reads across a calendar change, buffer guards, rewind, EOF, fresh + opens, invalid offset/buffer/handle, write-only access and unset-clock retry. +- STM32F4/F7 builds passed. +- STM32F746G-DISCO: RTC 12/12 through VFS, dmdrvi 6/6 and RNG 6/6; actual + `cat /dev/rtc` checked before and after restoring PC time. +- Firmware flashed with OpenOCD Verified OK. + +[Console evidence](validation/text-read-2026-10-16.txt). + +- `dmrtc.dmfc`: `ec3ff4189437e1967a158d74aba065379513608321e08e66b48694e6ee08ea39` +- `test_dmrtc.dmfc`: `224de0720fb9239b9a0b4d7aed0833abac163ec29cfd0b889656abc019bc6957` diff --git a/docs/validation/text-read-2026-10-16.txt b/docs/validation/text-read-2026-10-16.txt new file mode 100644 index 0000000..a535903 --- /dev/null +++ b/docs/validation/text-read-2026-10-16.txt @@ -0,0 +1,109 @@ +rtc info +years=2000..2099 resolution_ns=1000000000 writable=1 invalidatable=1 loss_detection=0 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc status +running=1 calendar_set=1 time_lost=unknown +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc get +2026-10-16T07:53:29 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T07:53:30 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T07:53:30 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> test_dmrtc +=== DMOD Test Runner === +[ RUN ] year_rollover +[ OK ] year_rollover +[ RUN ] leap_day_rollover +[ OK ] leap_day_rollover +[ RUN ] vfs_ioctl_and_access_permissions +[ OK ] vfs_ioctl_and_access_permissions +[ RUN ] single_owner_and_bad_configuration +[ OK ] single_owner_and_bad_configuration +[ RUN ] year_2000_is_not_an_uninitialized_calendar +[ OK ] year_2000_is_not_an_uninitialized_calendar +[ RUN ] set_read_and_reject_invalid +[ OK ] set_read_and_reject_invalid +[ RUN ] unset_calendar +[ OK ] unset_calendar +[ RUN ] calendar_validation +[ OK ] calendar_validation +[ RUN ] private_rtc_command_numbers_are_not_supported +[ OK ] private_rtc_command_numbers_are_not_supported +[ RUN ] standard_rtc_range_and_precision_are_enforced +[ OK ] standard_rtc_range_and_precision_are_enforced +[ RUN ] standard_rtc_capabilities_and_unset_state +[ OK ] standard_rtc_capabilities_and_unset_state +[ RUN ] text_read_is_a_bounded_per_open_calendar_snapshot +[ OK ] text_read_is_a_bounded_per_open_calendar_snapshot + +=== Results: 12/12 passed === +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> test_dmdrvi +=== DMOD Test Runner === +[ RUN ] dmdrvi_input_states_compare_every_field +[ OK ] dmdrvi_input_states_compare_every_field +[ RUN ] dmdrvi_monitor_policy_holds_longest_handler_name +[ OK ] dmdrvi_monitor_policy_holds_longest_handler_name +[ RUN ] dmdrvi_monitor_commands_are_distinct +[ OK ] dmdrvi_monitor_commands_are_distinct +[ RUN ] dmdrvi_errors_are_distinct_from_eof +[ OK ] dmdrvi_errors_are_distinct_from_eof +[ RUN ] dmdrvi_sizes_round_trip_above_four_gib +[ OK ] dmdrvi_sizes_round_trip_above_four_gib +[ RUN ] dmdrvi_v2_signatures_are_registered +[ OK ] dmdrvi_v2_signatures_are_registered + +=== Results: 6/6 passed === +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> test_dmrng +=== DMOD Test Runner === +[ RUN ] vfs_device +dmdevfs: [ERROR] Driver failed to open device: /rng +[ERROR] Failed to open file '/dev/rng' +[ OK ] vfs_device +[ RUN ] sustained_generation +RNG_SAMPLE bytes=65536 checksum=b6acd4c9 +[ OK ] sustained_generation +[ RUN ] single_owner_and_configuration_validation +[ OK ] single_owner_and_configuration_validation +[ RUN ] independent_blocks +[ OK ] independent_blocks +[ RUN ] unaligned_and_guarded_buffer +[ OK ] unaligned_and_guarded_buffer +[ RUN ] zero_and_invalid_reads +[ OK ] zero_and_invalid_reads + +=== Results: 6/6 passed === +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc set 2026-10-16T07:53:35 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc get +2026-10-16T07:53:35 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T07:53:35 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc status +running=1 calendar_set=1 time_lost=unknown +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> ip addr +eth0: 192.168.50.184/24 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> \ No newline at end of file diff --git a/src/dmrtc.c b/src/dmrtc.c index d59f506..99ecdc7 100644 --- a/src/dmrtc.c +++ b/src/dmrtc.c @@ -180,7 +180,18 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void, _free, (dmdrvi_context_t ctx)) int rc = dmrtc_destroy((dmrtc_t)ctx); if (rc != 0) DMOD_LOG_ERROR("dmrtc: release failed (%d)\n", rc); } -struct rtc_file { dmrtc_t owner; int flags; }; +enum { RTC_TEXT_LENGTH = 20 }; +struct rtc_file { + dmrtc_t owner; + int flags; + bool text_ready; + char text[RTC_TEXT_LENGTH]; +}; + +static void text_number(char* out, unsigned value, unsigned width) +{ + while (width != 0) { out[--width] = (char)('0' + value % 10); value /= 10; } +} dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void*, _open, (dmdrvi_context_t context, int flags, const dmdrvi_dev_num_t* number)) { @@ -191,7 +202,7 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void*, _open, return NULL; if (dmosi_mutex_lock(ctx->lock) != 0) return NULL; struct rtc_file* file = ctx->opens == UINT32_MAX ? NULL : Dmod_Malloc(sizeof(*file)); - if (file != NULL) { file->owner = ctx; file->flags = access; ++ctx->opens; } + if (file != NULL) { file->owner = ctx; file->flags = access; file->text_ready = false; ++ctx->opens; } dmosi_mutex_unlock(ctx->lock); return file; } @@ -208,8 +219,36 @@ dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, void, _close, dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, dmdrvi_ssize_t, _read, (dmdrvi_context_t ctx, void* handle, void* buffer, size_t size, dmdrvi_offset_t offset)) { - (void)ctx; (void)handle; (void)buffer; (void)size; (void)offset; - return -ENOTSUP; + dmrtc_t owner = (dmrtc_t)ctx; + struct rtc_file* file = handle; + if (!dmrtc_is_valid(owner) || file == NULL || file->owner != owner) return -EBADF; + if (!(file->flags & (DMDRVI_O_RDONLY | DMDRVI_O_RDWR))) return -EACCES; + if (offset < 0) return -EINVAL; + if (size == 0) return 0; + if (buffer == NULL) return -EINVAL; + if (offset >= RTC_TEXT_LENGTH) return 0; + int rc = dmosi_mutex_lock(owner->lock); + if (rc != 0) return rc; + if (!file->text_ready) { + dmrtc_calendar_t c; + rc = dmrtc_port_get(&c); + if (rc == 0 && dmrtc_normalize(&c) != 0) rc = -EIO; + if (rc == 0) { + memcpy(file->text, "0000-00-00T00:00:00\n", RTC_TEXT_LENGTH); + text_number(file->text, c.year, 4); + text_number(file->text + 5, c.month, 2); + text_number(file->text + 8, c.day, 2); + text_number(file->text + 11, c.hour, 2); + text_number(file->text + 14, c.minute, 2); + text_number(file->text + 17, c.second, 2); + file->text_ready = true; + } + } + size_t count = RTC_TEXT_LENGTH - (size_t)offset; + if (count > size) count = size; + if (rc == 0) memcpy(buffer, file->text + (size_t)offset, count); + dmosi_mutex_unlock(owner->lock); + return rc == 0 ? (dmdrvi_ssize_t)count : rc; } dmod_dmdrvi_dif_api_declaration(2.0, dmrtc, dmdrvi_ssize_t, _write, (dmdrvi_context_t ctx, void* handle, const void* buffer, size_t size, dmdrvi_offset_t offset)) diff --git a/tests/dmrtc_test.c b/tests/dmrtc_test.c index 4446283..f140bc8 100644 --- a/tests/dmrtc_test.c +++ b/tests/dmrtc_test.c @@ -47,6 +47,7 @@ void dmod_test_teardown(void) owned = NULL; ctx = NULL; } #ifdef DMRTC_MOCK +static dmod_dmdrvi_read_t device_read; static dmod_dmdrvi_ioctl_t device_ioctl; static dmod_dmdrvi_close_t device_close; #endif @@ -56,9 +57,10 @@ static void* open_device(const char* mode) Dmod_Context_t* module = Dmod_LoadModuleByName("dmrtc"); if (module == NULL) return NULL; dmod_dmdrvi_open_t open = (dmod_dmdrvi_open_t)Dmod_GetDifFunction(module, dmod_dmdrvi_open_sig); + device_read = (dmod_dmdrvi_read_t)Dmod_GetDifFunction(module, dmod_dmdrvi_read_sig); device_ioctl = (dmod_dmdrvi_ioctl_t)Dmod_GetDifFunction(module, dmod_dmdrvi_ioctl_sig); device_close = (dmod_dmdrvi_close_t)Dmod_GetDifFunction(module, dmod_dmdrvi_close_sig); - if (open == NULL || device_ioctl == NULL || device_close == NULL) return NULL; + if (open == NULL || device_read == NULL || device_ioctl == NULL || device_close == NULL) return NULL; dmdrvi_dev_num_t num = {0}; int flags = strcmp(mode,"rb") == 0 ? DMDRVI_O_RDONLY : strcmp(mode,"wb") == 0 ? DMDRVI_O_WRONLY : DMDRVI_O_RDWR; @@ -83,6 +85,72 @@ static void close_device(void* file) Dmod_FileClose(file); #endif } +/* Host exercises the DIF directly; hardware exercises the same read via VFS. */ +static dmdrvi_ssize_t read_device(void* file, void* buffer, size_t size, dmdrvi_offset_t offset) +{ +#ifdef DMRTC_MOCK + return device_read((dmdrvi_context_t)ctx, file, buffer, size, offset); +#else + if (Dmod_FileSeek(file, offset, 0) != 0) return -EIO; + return (dmdrvi_ssize_t)Dmod_FileRead(buffer, 1, size, file); +#endif +} +DMOD_TEST_STEP(text_read_is_a_bounded_per_open_calendar_snapshot) +{ + dmrtc_calendar_t c = {2024, 2, 29, 12, 34, 56, 0}; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + void* file = open_device("rb"); DMOD_TEST_EXPECT_NOT_NULL(file); + if (file == NULL) return; + char text[22]; memset(text, '#', sizeof(text)); + DMOD_TEST_EXPECT_EQ(read_device(file, text + 1, 17, 0), 17); + DMOD_TEST_EXPECT_EQ(memcmp(text + 1, "2024-02-29T12:34:", 17), 0); + c = (dmrtc_calendar_t){2000, 1, 1, 0, 0, 0, 0}; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + for (int offset = 17; offset < 20; ++offset) + DMOD_TEST_EXPECT_EQ(read_device(file, text + 1 + offset, 1, offset), 1); + DMOD_TEST_EXPECT_EQ(text[0], '#'); DMOD_TEST_EXPECT_EQ(text[21], '#'); + DMOD_TEST_EXPECT_EQ(text[20], '\n'); + DMOD_TEST_EXPECT_TRUE(text[18] >= '0' && text[18] <= '5'); + DMOD_TEST_EXPECT_TRUE(text[19] >= '0' && text[19] <= '9'); + char again[32]; memset(again, '#', sizeof(again)); + DMOD_TEST_EXPECT_EQ(read_device(file, again, sizeof(again), 0), 20); + DMOD_TEST_EXPECT_EQ(memcmp(again, text + 1, 20), 0); + DMOD_TEST_EXPECT_EQ(again[20], '#'); + DMOD_TEST_EXPECT_EQ(read_device(file, again, sizeof(again), 20), 0); + DMOD_TEST_EXPECT_EQ(read_device(file, again, sizeof(again), 1000), 0); + close_device(file); + file = open_device("rb"); DMOD_TEST_EXPECT_NOT_NULL(file); + if (file != NULL) { + DMOD_TEST_EXPECT_EQ(read_device(file, again, sizeof(again), 0), 20); + DMOD_TEST_EXPECT_EQ(memcmp(again, "2000-01-01T00:00:", 17), 0); + close_device(file); + } +} +#ifdef DMRTC_MOCK +DMOD_TEST_STEP(text_read_validates_access_and_retries_unset_clock) +{ + void* file = open_device("rb"); DMOD_TEST_EXPECT_NOT_NULL(file); + if (file == NULL) return; + char out[24]; memset(out, '#', sizeof(out)); + DMOD_TEST_EXPECT_EQ(dmrtc_invalidate(ctx), 0); + DMOD_TEST_EXPECT_EQ(read_device(file, NULL, 0, 0), 0); + DMOD_TEST_EXPECT_EQ(read_device(file, NULL, 1, 0), -EINVAL); + DMOD_TEST_EXPECT_EQ(read_device(file, out, sizeof(out), -1), -EINVAL); + DMOD_TEST_EXPECT_EQ(read_device(file, out, sizeof(out), 0), -ENODATA); + DMOD_TEST_EXPECT_EQ(out[0], '#'); + dmrtc_calendar_t c = {2026, 10, 16, 1, 2, 3, 0}; + DMOD_TEST_EXPECT_EQ(dmrtc_set(ctx, &c), 0); + DMOD_TEST_EXPECT_EQ(read_device(file, out, sizeof(out), 0), 20); + DMOD_TEST_EXPECT_EQ(memcmp(out, "2026-10-16T01:02:03\n", 20), 0); + DMOD_TEST_EXPECT_EQ(device_read((dmdrvi_context_t)ctx, NULL, out, 1, 0), -EBADF); + close_device(file); + file = open_device("wb"); DMOD_TEST_EXPECT_NOT_NULL(file); + if (file != NULL) { + DMOD_TEST_EXPECT_EQ(read_device(file, out, 1, 0), -EACCES); + close_device(file); + } +} +#endif DMOD_TEST_STEP(standard_rtc_capabilities_and_unset_state) { void* file = open_device("r+b"); DMOD_TEST_EXPECT_NOT_NULL(file);