From 0855a9567a94f53ca0d137ae98a3c6b32ccc4981 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Fri, 16 Oct 2026 11:22:49 +0000 Subject: [PATCH 1/2] Initialize portable UTC library with generic RTC access and 64-bit time conversion --- .github/workflows/ci.yml | 29 ++ .github/workflows/release.yml | 279 ++++++++++++++++++ .gitignore | 32 +- .vscode/settings.json | 8 + .vscode/tasks.json | 55 ++++ CMakeLists.txt | 78 +++++ README.md | 61 +++- dmutc.dmr | 33 +++ docs/README.md | 4 + docs/api-reference.md | 43 +++ docs/testing.md | 33 +++ .../stm32f746g-disco-2026-10-16.txt | 111 +++++++ include/dmutc.h | 60 ++++ manifest.dmm | 5 + scripts/sync-claude.sh | 117 ++++++++ src/dmutc.c | 78 +++++ src/rtc.c | 84 ++++++ tests/CMakeLists.txt | 56 ++++ tests/dmutc_test.c | 121 ++++++++ tests/rtc/rtc_test.c | 145 +++++++++ tests/rtc/stubs/dmod.h | 11 + tests/rtc/stubs/dmutc_defs.h | 5 + tests/test_dmutc.dmr | 6 + 23 files changed, 1435 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CMakeLists.txt create mode 100644 dmutc.dmr create mode 100644 docs/README.md create mode 100644 docs/api-reference.md create mode 100644 docs/testing.md create mode 100644 docs/validation/stm32f746g-disco-2026-10-16.txt create mode 100644 include/dmutc.h create mode 100644 manifest.dmm create mode 100755 scripts/sync-claude.sh create mode 100644 src/dmutc.c create mode 100644 src/rtc.c create mode 100644 tests/CMakeLists.txt create mode 100644 tests/dmutc_test.c create mode 100644 tests/rtc/rtc_test.c create mode 100644 tests/rtc/stubs/dmod.h create mode 100644 tests/rtc/stubs/dmutc_defs.h create mode 100644 tests/test_dmutc.dmr diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..703743f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + push: + branches: [main, master, develop] + pull_request: + branches: [main, master, develop] +jobs: + build: + name: Build and test (${{ matrix.tools }}) + runs-on: ubuntu-latest + permissions: + contents: read + strategy: + fail-fast: false + matrix: + tools: [arch/x86_64, arch/armv7/cortex-m4, arch/armv7/cortex-m7] + container: + image: chocotechnologies/dmod:1.0.4 + steps: + - uses: actions/checkout@v4 + - name: Build + run: | + cmake -S . -B build -DDMOD_TOOLS_NAME=${{ matrix.tools }} + cmake --build build --parallel 4 + test -f build/dmf/dmutc.dmf + test -f build/dmf/test_dmutc.dmf + - name: Native tests + if: matrix.tools == 'arch/x86_64' + run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6e294af --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,279 @@ +name: Release + +on: + release: + types: [created] + +jobs: + discover-architectures: + name: Discover Architectures + runs-on: ubuntu-latest + permissions: + contents: read + container: + image: chocotechnologies/dmod:1.0.4 + outputs: + architectures: ${{ steps.list-archs.outputs.architectures }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Fetch dmod to discover architectures + run: | + # A plain `git clone` here (rather than configuring dmutc's + # own CMakeLists.txt via FetchContent, as before) so this step only + # ever depends on dmod itself - not on dmutc's + # dmod_link_modules() fetches of other modules, which this step has + # no business needing just to list dmod's supported architectures, + # and which previously made this discovery job (and therefore the + # whole release) fail outright whenever any of those unrelated + # external fetches hiccuped. + git clone --depth 1 https://github.com/choco-technologies/dmod.git dmod_discovery + + - name: List available architectures + id: list-archs + run: | + # create JSON array + ARCHS=$(find dmod_discovery/configs/arch -name "tools-cfg.cmake" | \ + sed "s|dmod_discovery/configs/arch/||g" | \ + sed 's|/tools-cfg.cmake||g' | \ + jq -R -s -c 'split("\n") | map(select(length > 0))') + + echo "Found architectures: $ARCHS" + echo "architectures=$ARCHS" >> $GITHUB_OUTPUT + + build-release: + name: Build Release for ${{ matrix.arch_name }} + needs: discover-architectures + runs-on: ubuntu-latest + permissions: + contents: write + strategy: + matrix: + arch_name: ${{ fromJson(needs.discover-architectures.outputs.architectures) }} + + container: + image: chocotechnologies/dmod:1.0.4 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract version from tag + id: get_version + run: | + # Extract version from tag (e.g., v1.2 -> 1.2) + VERSION="${{ github.event.release.tag_name }}" + VERSION="${VERSION#v}" # Remove 'v' prefix if present + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Extracted version: $VERSION" + + - name: Build dmutc for ${{ matrix.arch_name }} + run: | + set -e + ARCH_DIR_NAME=$(echo "${{ matrix.arch_name }}" | sed 's|/|-|') + echo "ARCH_DIR_NAME=$ARCH_DIR_NAME" >> $GITHUB_ENV + echo "ARTIFACT_NAME=release-$ARCH_DIR_NAME" >> $GITHUB_ENV + mkdir -p build_$ARCH_DIR_NAME + cd build_$ARCH_DIR_NAME + + # DMOD_DMR_PATH is already set in CMakeLists.txt, so this build also + # assembles build_$ARCH_DIR_NAME/packages// for every module + # defined in this repo's CMake config - nothing left to do here but + # discover whatever got packaged, add release notes, and zip it. + cmake .. -DDMOD_TOOLS_NAME=arch/${{ matrix.arch_name }} -DDMOD_MODULE_VERSION="${{ steps.get_version.outputs.version }}" + cmake --build . + + echo "Build completed for ${{ matrix.arch_name }}" + ls -la packages/ + + - name: Add release notes and create release archives + run: | + set -e + BUILD_DIR="build_$ARCH_DIR_NAME" + TAG="${{ github.event.release.tag_name }}" + PACKAGES_DIR="$BUILD_DIR/packages" + + if [ ! -d "$PACKAGES_DIR" ] || [ -z "$(ls -A "$PACKAGES_DIR")" ]; then + echo "Error: no packages found in $PACKAGES_DIR" + exit 1 + fi + + # A repo may define more than one module (e.g. a core + port split, + # or several independent modules) - package every subdirectory + # found under packages/, not just dmutc. + : > package-names.txt + for pkg_dir in "$PACKAGES_DIR"/*/; do + pkg=$(basename "$pkg_dir") + echo "$pkg" >> package-names.txt + + echo "${{ github.event.release.body }}" > "${pkg_dir}RELEASE_NOTES.txt" + (cd "$pkg_dir" && zip -r "${OLDPWD}/${pkg}-${TAG}-${ARCH_DIR_NAME}.zip" .) + done + + echo "Created archives:" + ls -lh *-"${TAG}"-"${ARCH_DIR_NAME}".zip + echo "Packages found:" + cat package-names.txt + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ARTIFACT_NAME }} + path: | + *.zip + package-names.txt + retention-days: 1 + + generate-versions-manifest: + name: Generate versions.dmm + needs: [discover-architectures, build-release] + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history to get all tags + + - name: Download release artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Generate versions.dmm + run: | + set -e + echo "# List of available versions for dmutc modules" > versions.dmm + echo "# Generated automatically by CI" >> versions.dmm + echo "" >> versions.dmm + + # Get all version tags (starting with 'v') and extract version numbers + VERSIONS=$(git tag -l 'v*' | sed 's/^v//' | sort -V | tr '\n' ' ' | sed 's/ $//') + + # Remove vlatest from the list if present + VERSIONS=$(echo $VERSIONS | sed 's/\blatest\b//g' | xargs) + + if [ -z "$VERSIONS" ]; then + echo "Warning: No version tags found" + VERSIONS="${{ github.event.release.tag_name }}" + VERSIONS="${VERSIONS#v}" + fi + + echo "Found versions: $VERSIONS" + + # Union of package names discovered across every architecture build + # (the same set is expected everywhere, but union is safe even if + # some packages only exist for a subset of architectures). + PACKAGES=$(cat artifacts/release-*/package-names.txt | sort -u) + echo "Found packages: $PACKAGES" + + for pkg in $PACKAGES; do + echo "\$version-available $pkg $VERSIONS" >> versions.dmm + done + + echo "Generated versions.dmm:" + cat versions.dmm + + - name: Upload versions.dmm as artifact + uses: actions/upload-artifact@v4 + with: + name: versions-manifest + path: versions.dmm + retention-days: 1 + + upload-release-assets: + name: Upload Release Assets + needs: [build-release, generate-versions-manifest] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display artifact structure + run: | + echo "Downloaded artifacts:" + ls -lR artifacts/ + + - name: Upload release assets to versioned tag + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + shopt -s nullglob + zip_files=(artifacts/release-*/*.zip) + + if [ ${#zip_files[@]} -eq 0 ]; then + echo "Error: No artifacts found to upload" + exit 1 + fi + + for zip_file in "${zip_files[@]}"; do + echo "Uploading $zip_file to ${{ github.event.release.tag_name }}..." + gh release upload ${{ github.event.release.tag_name }} \ + "$zip_file" \ + --repo ${{ github.repository }} \ + --clobber + done + + if [ -f artifacts/versions-manifest/versions.dmm ]; then + echo "Uploading versions.dmm to ${{ github.event.release.tag_name }}..." + gh release upload ${{ github.event.release.tag_name }} \ + artifacts/versions-manifest/versions.dmm \ + --repo ${{ github.repository }} \ + --clobber + fi + + echo "Successfully uploaded ${#zip_files[@]} artifact(s) to ${{ github.event.release.tag_name }}" + + - name: Create or update latest release + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + if gh release view vlatest --repo ${{ github.repository }} >/dev/null 2>&1; then + echo "Release vlatest exists, deleting it..." + gh release delete vlatest --repo ${{ github.repository }} --yes + fi + + echo "Creating vlatest release..." + gh release create vlatest \ + --repo ${{ github.repository }} \ + --title "Latest Release (based on ${{ github.event.release.tag_name }})" \ + --notes "This release always points to the latest stable version. Currently based on ${{ github.event.release.tag_name }}." + + - name: Upload release assets to latest tag + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -e + shopt -s nullglob + zip_files=(artifacts/release-*/*.zip) + + for zip_file in "${zip_files[@]}"; do + echo "Uploading $zip_file to vlatest..." + gh release upload vlatest \ + "$zip_file" \ + --repo ${{ github.repository }} \ + --clobber + done + + if [ -f artifacts/versions-manifest/versions.dmm ]; then + echo "Uploading versions.dmm to vlatest..." + gh release upload vlatest \ + artifacts/versions-manifest/versions.dmm \ + --repo ${{ github.repository }} \ + --clobber + fi + + echo "Successfully uploaded ${#zip_files[@]} artifact(s) to vlatest" diff --git a/.gitignore b/.gitignore index 1f99f9d..c2af1f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,15 @@ -CMakeLists.txt.user -CMakeCache.txt -CMakeFiles -CMakeScripts -Testing -Makefile -cmake_install.cmake -install_manifest.txt -compile_commands.json -CTestTestfile.cmake -_deps -CMakeUserPresets.json +# Build directories +build/ +*.dmf +*.dmfc -# CLion -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#cmake-build-* +# IDE +# .vscode/tasks.json and .vscode/settings.json are committed (see every real +# module repo) so the build tasks are shared with the team - don't ignore +# .vscode/ wholesale. +.idea/ + +# Temporary files +*.tmp +*.swp +*~ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5f0465c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.associations": { + "dmutc.h": "c" + }, + "C_Cpp.default.compileCommands": "${workspaceFolder}/build/compile_commands.json", + "C_Cpp.intelliSenseEngine": "default", + "C_Cpp.errorSquiggles": "enabled" +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2a66b75 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,55 @@ +{ + // See https://code.visualstudio.com/docs/editor/tasks for more information + "version": "2.0.0", + "tasks": [ + { + "label": "CMake: Configure", + "type": "shell", + "command": "cmake", + "args": [ + "-S", + "${workspaceFolder}", + "-B", + "${workspaceFolder}/build", + "-DDMOD_DIR=${input:dmodDir}" + ], + "group": "build", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + }, + { + "label": "CMake: Build", + "type": "shell", + "command": "cmake", + "args": [ + "--build", + "${workspaceFolder}/build" + ], + "group": "build", + "problemMatcher": [] + }, + { + "label": "CMake: Clean", + "type": "shell", + "command": "cmake", + "args": [ + "--build", + "${workspaceFolder}/build", + "--target", + "clean" + ], + "group": "build", + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "dmodDir", + "description": "Path to a local dmod checkout (leave blank to fetch dmod's develop branch from GitHub instead)", + "default": "${workspaceFolder}/../dmod", + "type": "promptString" + } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..051c620 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,78 @@ +cmake_minimum_required(VERSION 3.18) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(NOT DEFINED DMOD_MODULE_VERSION) + set(DMOD_MODULE_VERSION "0.1" CACHE STRING "DMOD module version") +endif() + +# If a local dmod checkout is provided, use it instead of fetching from git +if(DEFINED DMOD_DIR AND NOT "${DMOD_DIR}" STREQUAL "") + set(FETCHCONTENT_SOURCE_DIR_DMOD "${DMOD_DIR}" CACHE PATH "" FORCE) +endif() + +include(FetchContent) +FetchContent_Declare( + dmod + GIT_REPOSITORY https://github.com/choco-technologies/dmod.git + GIT_TAG develop +) + +set(DMOD_MODE "DMOD_MODULE" CACHE STRING "DMOD build mode") +set(DMOD_BUILD_TESTS OFF CACHE BOOL "Build tests") +set(DMOD_BUILD_EXAMPLES OFF CACHE BOOL "Build examples") +set(DMOD_BUILD_TOOLS OFF CACHE BOOL "Build tools") +set(DMOD_BUILD_TEMPLATES OFF CACHE BOOL "Build templates") + +FetchContent_MakeAvailable(dmod) + +project(dmutc VERSION ${DMOD_MODULE_VERSION} + DESCRIPTION "dmutc DMOD library module" + LANGUAGES C CXX) + +set(DMOD_DIR ${dmod_SOURCE_DIR} CACHE PATH "DMOD source directory") +set(DMOD_SCRIPTS_DIR ${DMOD_DIR}/scripts CACHE PATH "DMOD scripts directory") +include(${DMOD_DIR}/paths.cmake) +dmod_setup_external_module() + +# Name of the module +set(DMOD_MODULE_NAME dmutc) + +# Author (should be string) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") + +# Stack size for the module (should be integer) +set(DMOD_STACK_SIZE 1024) + +# Path to the .dmr file - enables release package creation in build/packages/ +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dmutc.dmr) + +# List of MAL interfaces implemented by this module (leave empty if none) +set(DMOD_MAL_IMPLS ) + +# List of DIF interfaces implemented by this module (leave empty if none) +set(DMOD_DIF_IMPLS ) + +# +# dmod_add_library - create a library module +# it has the same signature as add_library +# and can be used in the same way after the creation +# (for example, to link libraries) +# +dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + # List of source files - can include C and C++ files + src/dmutc.c + src/rtc.c +) + +target_include_directories(${DMOD_MODULE_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +dmod_link_modules(${DMOD_MODULE_NAME} dmdrvi) +enable_testing() +add_subdirectory(tests) diff --git a/README.md b/README.md index 833597a..47d162a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,61 @@ # dmutc -UTC library + +[![CI](https://github.com/choco-technologies/dmutc/actions/workflows/ci.yml/badge.svg)](https://github.com/choco-technologies/dmutc/actions/workflows/ci.yml) + +Portable UTC library for DMOD. It converts Gregorian UTC calendars to signed +64-bit Unix time and reads/sets a configured RTC through VFS and the generic +`DMDRVI_IOCTL_RTC_*` interface. It is a library, with no driver DIF, hardware +port, MCU registers or dependency on dmrtc. Other RTC implementations work +through the same contract. + +```c +#include "dmutc.h" + +dmutc_time_t now; +int rc = dmutc_get(NULL, &now); /* /dev/rtc; seconds and nanoseconds */ +if (rc == 0) { + dmutc_calendar_t calendar; + rc = dmutc_to_calendar(&now, &calendar); +} + +/* Another RTC can be selected per call. Its calendar must store UTC. */ +rc = dmutc_get("/dev/external-rtc", &now); +``` + +The library is stateless and allocates no persistent context, task, timer or +mutex. Each RTC operation opens and closes its own VFS handle. Pure conversion +functions require neither VFS nor an RTC. No libc `time_t`, timezone setting, +locale or `mktime()` is involved. + +Time uses the POSIX convention: seconds since 1970-01-01T00:00:00Z, excluding +leap seconds, with nanoseconds in 0..999999999. Years 1..9999 are supported; +negative epochs and dates after 2038 work on 32-bit targets. For example, +`{-1, 500000000, 0}` is 1969-12-31T23:59:59.500000000Z. RTC hardware may have +narrower years/precision; restrictions and errors are preserved, never rounded. + +`dmutc_get()` rejects stopped, unset and detected-lost clocks. Without a +hardware loss-detection capability, an unset loss flag does not establish +continuity. The chosen RTC must store UTC rather than local civil time. +Successful reads do **not** authenticate the clock or establish its accuracy. +A TLS caller must obtain its trust policy from the application; this library +does not turn a retained RTC marker or unauthenticated time into trusted UTC. +Network synchronization is a separate source of time, which can use +`dmutc_set()` after validating its sample. + +Build and test using CMake: + +```sh +cmake -S . -B build/host -DDMOD_TOOLS_NAME=arch/x86_64 +cmake --build build/host --parallel +ctest --test-dir build/host --output-on-failure + +cmake -S . -B build/stm32f7 -DDMOD_TOOLS_NAME=arch/armv7/cortex-m7 +cmake --build build/stm32f7 --parallel +``` + +For local development, `-DDMOD_DIR=/path/to/dmod` selects an existing checkout; +no development-machine path or dependency version is pinned in source. +The module is scaffolded using DMOD's `new-module.sh --type library --github`. +Packages contain the public/generated headers, documentation, license and DMR. + +See [API reference](docs/api-reference.md) and [validation](docs/testing.md). diff --git a/dmutc.dmr b/dmutc.dmr new file mode 100644 index 0000000..f69f1ad --- /dev/null +++ b/dmutc.dmr @@ -0,0 +1,33 @@ +# DMOD Resource File for dmutc +# This file specifies where resources should be installed + +# === Core Module === +# Main module file - always installed +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] + +# Compressed module +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] + +# Dependencies file (if exists) +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] + +# Version information +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] + +# === Documentation === +# Module documentation in markdown format for dmf-man tool +docs=./docs => ${destination}/${module}/docs [origin=${repo_dir}/docs] + +# README file +readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/README.md] + +# === Header Files === +# Include directory with all headers (public header + generated _defs.h) +inc=./include => ${destination}/${module}/include [origin=${repo_dir}/include] [origin=${build_dir}/${module}_defs.h] + +# === License === +# License file +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] + +# Adding itself +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/${module}.dmr] diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..210ba8e --- /dev/null +++ b/docs/README.md @@ -0,0 +1,4 @@ +# UTC library + +[dmutc](../README.md) provides Gregorian/POSIX conversion and generic VFS RTC +access. See [API reference](api-reference.md) and [validation](testing.md). diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..ebe56d5 --- /dev/null +++ b/docs/api-reference.md @@ -0,0 +1,43 @@ +# API reference + +Include `dmutc.h` and declare `dmutc` using `dmod_link_modules()`. +All operations return zero on success and a negative errno-compatible code on +failure. Required output pointers must be non-NULL; outputs are cleared on +failure. Inputs/outputs of conversion functions must not overlap. + +| Function | Behavior | +|---|---| +| `dmutc_from_calendar(calendar, time)` | Convert UTC Gregorian calendar to Unix seconds/nanoseconds. Invalid fields return EINVAL. | +| `dmutc_to_calendar(time, calendar)` | Convert normalized Unix time to UTC; timestamps outside years 1..9999 return ERANGE. | +| `dmutc_get(device, time)` | Read an initialized running RTC, interpreting its civil calendar as UTC. | +| `dmutc_set(device, time)` | Validate and convert Unix time, then write RTC through its generic ioctl. | +| `dmutc_status(device, status)` | Report device years, quantum, running/set/write/loss capabilities and state. Works when unset. | + +`device == NULL` selects `/dev/rtc`. An empty path returns EINVAL; failed VFS +open returns ENODEV (the SAL open API does not expose a portable errno). +Driver errors such as EIO, ENODATA, ERANGE, ETIMEDOUT, EACCES and ENOTSUP +propagate. GET_INFO and GET_STATUS are required by the RTC device contract. +Malformed device metadata/calendar returns EIO. Loss is interpreted only when +the driver advertises loss detection. No status bit means authenticated UTC. + +`dmutc_time_t` contains signed `int64_t seconds`, normalized `uint32_t +nanosecond` and a zero `reserved` field (16 bytes). `dmutc_calendar_t` contains +full year, month, day, hour, minute, second, weekday, zero reserved field and +nanoseconds (16 bytes). Weekday is Monday=1..Sunday=7 and is ignored on input. +Gregorian leap-century rules apply; the API does not encode leap seconds. + +`dmutc_status_t` reports `resolution_ns`, inclusive `min_year`/`max_year`, flags +and zero reserved field (24 bytes). The quantum reports representable precision, +not oscillator accuracy. Flags are RUNNING, TIME_SET, LOSS_DETECTION, TIME_LOST +and WRITABLE, prefixed with `DMUTC_CLOCK_`. + +Every RTC call uses its own file handle (rb for reads, r+b for writes) and +closes it on every exit after opening. Validation of input precedes opening +for writes. A device-range violation is rejected before SET_TIME; finer +precision is passed intact for the driver to accept or reject without rounding. +Metadata and status are observations, not an atomic promise about a later call: +an intervening driver change is handled by propagating GET_TIME/SET_TIME errors. + +No global UTC cache, background worker, polling, clock discipline or hardware +port is introduced. Network/GNSS sources may call SET after validating samples; +the application decides which source to trust and when to accept it for TLS. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..0bd9800 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,33 @@ +# Validation — 2026-10-16 + +- Native ctest: 2/2 targets. The actual DMOD diagnostic passes 4/4 steps for + known epochs, negative fractional times, leap centuries, invalid input, + overflow and month transitions throughout years 1..9999. +- The actual RTC provider compiled against an independent VFS/RTC model passes + 4/4 scenarios: clocks outside STM32 limits with nanoseconds, unset/stopped/ + loss-detected states, read-only/range/precision behavior, errors, malformed + calendar/metadata, output clearing and handle closure on all failure paths. +- An independent Python datetime oracle agrees with 10,000 seeded random + calendar/epoch round trips across years 1..9999, including weekday and ns. +- Cortex-M4 and Cortex-M7 builds passed. No hardware execution on M4 is claimed. +- STM32F746G-DISCO: test_dmutc 5/5, RTC regression 12/12, RNG regression 6/6. + UTC read/write traverses the real VFS/RTC interface; fractional writes beyond + RTC precision return ERANGE. The test restores its saved clock. +- The board initially had an unset calendar; GET returned ENODATA. The board + round-trip diagnostic requires an initialized RTC to preserve/restore time, + so the harness sets PC UTC before running it and restores PC UTC afterwards. + This does not claim authenticated UTC or measured oscillator accuracy. +- Flashed via OpenOCD with Verified OK; cat /dev/rtc and DHCP checked afterwards. + +[Board console evidence](validation/stm32f746g-disco-2026-10-16.txt). + +The firmware validation image stages dmutc and its diagnostic locally; no boot +configuration change or development-machine package path is committed. Public +package headers resolve dmdrvi 2.8 after refreshing a stale dmf-get environment +cache. Source dependencies remain unversioned in dmod_link_modules. + +STM32F7 library size: 5104 bytes DMF, 4054 bytes compressed DMFC. It has no +non-system runtime module dependencies, hardware port or driver registration. + +- `dmutc.dmfc` SHA-256: `8f535b6495c99365c2ba8133cc5086b27e87177faff02f2c6912eb7fe046ba67` +- `test_dmutc.dmfc` SHA-256: `7ddab5049ac8b5894a577415d3ccf41975c15ee7b01b321b40c64db869d8e74f` diff --git a/docs/validation/stm32f746g-disco-2026-10-16.txt b/docs/validation/stm32f746g-disco-2026-10-16.txt new file mode 100644 index 0000000..fc637d7 --- /dev/null +++ b/docs/validation/stm32f746g-disco-2026-10-16.txt @@ -0,0 +1,111 @@ +rtc set 2026-10-16T11:21:24 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> 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-16T11:21:25 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T11:21:26 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T11:21:26 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> test_dmutc +=== DMOD Test Runner === +[ RUN ] vfs_rtc_utc_round_trip_and_restore +UTC_SAMPLE seconds=1792149686 nanosecond=0 resolution_ns=1000000000 +[ OK ] vfs_rtc_utc_round_trip_and_restore +[ RUN ] month_boundaries_round_trip_across_the_supported_era +[ OK ] month_boundaries_round_trip_across_the_supported_era +[ RUN ] floor_normalization_and_overflow_rejection +[ OK ] floor_normalization_and_overflow_rejection +[ RUN ] gregorian_centuries_and_invalid_fields +[ OK ] gregorian_centuries_and_invalid_fields +[ RUN ] known_unix_values_before_epoch_after_2038_and_at_limits +[ OK ] known_unix_values_before_epoch_after_2038_and_at_limits + +=== Results: 5/5 passed === +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_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=0fc0889a +[ 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-16T11:21:32 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc get +2026-10-16T11:21:32 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T11:21:32 +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/include/dmutc.h b/include/dmutc.h new file mode 100644 index 0000000..8d35f00 --- /dev/null +++ b/include/dmutc.h @@ -0,0 +1,60 @@ +#ifndef DMUTC_H +#define DMUTC_H +#include +#include "dmutc_defs.h" +#ifdef __cplusplus +extern "C" { +#endif + +/* POSIX UTC: seconds since 1970-01-01T00:00:00Z, excluding leap seconds. + * Fractions are normalized: 0 <= nanosecond < 1e9, even before the epoch. + * Example: -0.5 seconds is { -1, 500000000, 0 }. reserved must be zero. */ +typedef struct { + int64_t seconds; + uint32_t nanosecond; + uint32_t reserved; +} dmutc_time_t; + +/* Proleptic Gregorian UTC calendar, years 1..9999. weekday is Monday=1 to + * Sunday=7 on output and ignored on input. All reserved fields are zero. */ +typedef struct { + int32_t year; + uint8_t month, day, hour, minute, second, weekday; + uint16_t reserved; + uint32_t nanosecond; +} dmutc_calendar_t; + +#define DMUTC_CLOCK_RUNNING (1u << 0) +#define DMUTC_CLOCK_TIME_SET (1u << 1) +#define DMUTC_CLOCK_LOSS_DETECTION (1u << 2) +#define DMUTC_CLOCK_TIME_LOST (1u << 3) +#define DMUTC_CLOCK_WRITABLE (1u << 4) +/* TIME_LOST is meaningful only with LOSS_DETECTION. No flag asserts UTC + * accuracy, authentication or uninterrupted power on other devices. */ +typedef struct { + uint64_t resolution_ns; + int32_t min_year, max_year; + uint32_t flags; + uint32_t reserved; +} dmutc_status_t; + +/* Conversion is independent of libc time_t, timezone, VFS and hardware. + * -EINVAL: malformed fields; -ERANGE: time outside years 1..9999. + * Output is cleared on failure. Input and output must not overlap. */ +dmod_dmutc_api(1.0, int, _from_calendar, (const dmutc_calendar_t* calendar, dmutc_time_t* time)); +dmod_dmutc_api(1.0, int, _to_calendar, (const dmutc_time_t* time, dmutc_calendar_t* calendar)); + +/* Stateless VFS operations: NULL device selects /dev/rtc. The chosen RTC + * must store UTC, never local civil time. Each call opens/closes its own + * handle; reads use rb, writes r+b. No dmrtc or port dependency. + * -ENODEV: opening the device failed; -ENODATA: unset/lost/stopped clock; + * other negative driver errors propagate. Outputs are cleared on failure. + * A successful read provides configured UTC, not authenticated time. TLS + * users must establish their trust policy independently of these APIs. */ +dmod_dmutc_api(1.0, int, _get, (const char* device, dmutc_time_t* time)); +dmod_dmutc_api(1.0, int, _set, (const char* device, const dmutc_time_t* time)); +dmod_dmutc_api(1.0, int, _status, (const char* device, dmutc_status_t* status)); +#ifdef __cplusplus +} +#endif +#endif diff --git a/manifest.dmm b/manifest.dmm new file mode 100644 index 0000000..5451328 --- /dev/null +++ b/manifest.dmm @@ -0,0 +1,5 @@ +# Include dynamically generated versions list from latest release +$include https://github.com/choco-technologies/dmutc/releases/download/vlatest/versions.dmm + +# Module entry with version placeholder - expanded by $version-available +dmutc https://github.com/choco-technologies/dmutc/releases/download/v/dmutc-v-.zip diff --git a/scripts/sync-claude.sh b/scripts/sync-claude.sh new file mode 100755 index 0000000..8ce1006 --- /dev/null +++ b/scripts/sync-claude.sh @@ -0,0 +1,117 @@ +#!/bin/bash + +# ############################################################################## +# +# Syncs Claude Code skills (.claude/skills/) from the dmod repository into the +# current module repository. Run this explicitly whenever you want the latest +# ecosystem knowledge available to Claude Code - it is not run automatically +# by CMake or by any build step, on purpose: the result is meant to be +# reviewed and committed like any other file, not regenerated silently. +# +# Only .claude/skills/ is touched. Local .claude/settings.json, hooks, or +# other skills you added under a different name are left alone. +# +# Usage: +# ./scripts/sync-claude.sh [--dmod-dir PATH] [--ref REF] +# +# Options: +# --dmod-dir PATH Use a local dmod checkout instead of cloning from GitHub +# --ref REF Git branch/tag to clone when no --dmod-dir is given +# (default: develop) +# --help Show this help message +# +# ############################################################################## + +set -e + +DMOD_DIR="" +GIT_REF="develop" +DMOD_REPO_URL="https://github.com/choco-technologies/dmod.git" + +RED='\033[0;31m' +GREEN='\033[0;32m' +NC='\033[0m' + +print_error() { + echo -e "${RED}Error: $1${NC}" >&2 +} + +print_success() { + echo -e "${GREEN}$1${NC}" +} + +print_help() { + cat << EOF +Sync Claude Code skills from dmod + +Usage: + ./scripts/sync-claude.sh [--dmod-dir PATH] [--ref REF] + +Options: + --dmod-dir PATH Use a local dmod checkout instead of cloning from GitHub + --ref REF Git branch/tag to clone when no --dmod-dir is given (default: develop) + --help Show this help message +EOF +} + +while [[ $# -gt 0 ]]; do + case $1 in + --dmod-dir) + DMOD_DIR="$2" + shift 2 + ;; + --ref) + GIT_REF="$2" + shift 2 + ;; + --help) + print_help + exit 0 + ;; + *) + print_error "Unknown parameter: $1" + print_help + exit 1 + ;; + esac +done + +TMP_CLONE_DIR="" + +cleanup() { + if [[ -n "${TMP_CLONE_DIR}" && -d "${TMP_CLONE_DIR}" ]]; then + rm -rf "${TMP_CLONE_DIR}" + fi +} +trap cleanup EXIT + +if [[ -n "${DMOD_DIR}" ]]; then + if [[ ! -d "${DMOD_DIR}" ]]; then + print_error "--dmod-dir does not exist: ${DMOD_DIR}" + exit 1 + fi + SOURCE_SKILLS_DIR="${DMOD_DIR}/.claude/skills" + echo "Using local dmod checkout: ${DMOD_DIR}" +else + TMP_CLONE_DIR="$(mktemp -d)" + echo "Cloning dmod (branch: ${GIT_REF}) ..." + git clone --depth 1 --branch "${GIT_REF}" "${DMOD_REPO_URL}" "${TMP_CLONE_DIR}" > /dev/null 2>&1 + SOURCE_SKILLS_DIR="${TMP_CLONE_DIR}/.claude/skills" +fi + +if [[ ! -d "${SOURCE_SKILLS_DIR}" ]]; then + print_error "No .claude/skills directory found at ${SOURCE_SKILLS_DIR}" + exit 1 +fi + +mkdir -p .claude/skills +cp -r "${SOURCE_SKILLS_DIR}/." .claude/skills/ + +echo "" +print_success "Synced Claude Code skills:" +for skill_dir in .claude/skills/*/; do + [[ -d "${skill_dir}" ]] || continue + echo " - $(basename "${skill_dir}")" +done +echo "" +echo "Review the changes with 'git status'/'git diff' and commit them like any other file." diff --git a/src/dmutc.c b/src/dmutc.c new file mode 100644 index 0000000..e34edc6 --- /dev/null +++ b/src/dmutc.c @@ -0,0 +1,78 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod.h" +#include "dmutc.h" +#include +#include + +static int64_t year_days(int year) +{ + int64_t y = year - 1; + return 365 * y + y / 4 - y / 100 + y / 400; +} +static int leap(int year) +{ + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} +static unsigned month_days(int year, unsigned month) +{ + static const uint8_t days[] = {31,28,31,30,31,30,31,31,30,31,30,31}; + return days[month - 1] + (month == 2 && leap(year)); +} + +dmod_dmutc_api_declaration(1.0, int, _from_calendar, + (const dmutc_calendar_t* c, dmutc_time_t* out)) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + if (c == NULL || c->reserved != 0 || c->year < 1 || c->year > 9999 || + c->month < 1 || c->month > 12 || c->day < 1 || + c->day > month_days(c->year, c->month) || c->hour > 23 || + c->minute > 59 || c->second > 59 || c->nanosecond >= 1000000000u) return -EINVAL; + int64_t days = year_days(c->year) - 719162; + for (unsigned m = 1; m < c->month; ++m) days += month_days(c->year, m); + days += c->day - 1; + out->seconds = days * 86400 + c->hour * 3600 + c->minute * 60 + c->second; + out->nanosecond = c->nanosecond; + return 0; +} + +dmod_dmutc_api_declaration(1.0, int, _to_calendar, + (const dmutc_time_t* time, dmutc_calendar_t* out)) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + if (time == NULL || time->reserved != 0 || time->nanosecond >= 1000000000u) return -EINVAL; + if (time->seconds < -62135596800LL || time->seconds > 253402300799LL) return -ERANGE; + int64_t days = time->seconds / 86400; + int64_t second = time->seconds % 86400; + if (second < 0) { --days; second += 86400; } + int64_t absolute = days + 719162; + int low = 1, high = 10000; + while (low + 1 < high) { + int middle = low + (high - low) / 2; + if (year_days(middle) <= absolute) low = middle; + else high = middle; + } + unsigned remaining = (unsigned)(absolute - year_days(low)); + unsigned month = 1; + while (remaining >= month_days(low, month)) { + remaining -= month_days(low, month++); + } + out->year = low; out->month = (uint8_t)month; out->day = (uint8_t)(remaining + 1); + out->hour = (uint8_t)(second / 3600); + out->minute = (uint8_t)(second / 60 % 60); out->second = (uint8_t)(second % 60); + int weekday = (int)((days + 3) % 7); + if (weekday < 0) weekday += 7; + out->weekday = (uint8_t)(weekday + 1); out->nanosecond = time->nanosecond; + return 0; +} + +int dmod_init(const Dmod_Config_t* config) +{ + (void)config; + return 0; +} +int dmod_deinit(void) +{ + return 0; +} diff --git a/src/rtc.c b/src/rtc.c new file mode 100644 index 0000000..b148f4d --- /dev/null +++ b/src/rtc.c @@ -0,0 +1,84 @@ +#include "dmod.h" +#include "dmutc.h" +#include "dmdrvi_ioctl.h" +#include +#include + +static const char* rtc_path(const char* device) +{ + return device == NULL ? "/dev/rtc" : device; +} +static int read_status(void* file, dmutc_status_t* out) +{ + dmdrvi_rtc_info_t info = {0}; dmdrvi_rtc_status_t status = {0}; + int rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_GET_INFO, &info); + if (rc == 0) rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_GET_STATUS, &status); + if (rc != 0) return rc; + if (info.reserved != 0 || status.reserved != 0 || info.resolution_ns == 0 || + info.min_year < 1 || info.max_year > 9999 || info.min_year > info.max_year) return -EIO; + out->resolution_ns = info.resolution_ns; + out->min_year = info.min_year; out->max_year = info.max_year; + if (info.capabilities & DMDRVI_RTC_CAP_SET_TIME) out->flags |= DMUTC_CLOCK_WRITABLE; + if (info.capabilities & DMDRVI_RTC_CAP_LOSS_DETECTION) { + out->flags |= DMUTC_CLOCK_LOSS_DETECTION; + if (status.flags & DMDRVI_RTC_STATUS_TIME_LOST) out->flags |= DMUTC_CLOCK_TIME_LOST; + } + if (status.flags & DMDRVI_RTC_STATUS_RUNNING) out->flags |= DMUTC_CLOCK_RUNNING; + if (status.flags & DMDRVI_RTC_STATUS_TIME_SET) out->flags |= DMUTC_CLOCK_TIME_SET; + return 0; +} + +dmod_dmutc_api_declaration(1.0, int, _status, (const char* device, dmutc_status_t* out)) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + if (*rtc_path(device) == '\0') return -EINVAL; + void* file = Dmod_FileOpen(rtc_path(device), "rb"); + if (file == NULL) return -ENODEV; + int rc = read_status(file, out); + Dmod_FileClose(file); + return rc; +} + +dmod_dmutc_api_declaration(1.0, int, _get, (const char* device, dmutc_time_t* out)) +{ + if (out == NULL) return -EINVAL; + memset(out, 0, sizeof(*out)); + if (*rtc_path(device) == '\0') return -EINVAL; + void* file = Dmod_FileOpen(rtc_path(device), "rb"); + if (file == NULL) return -ENODEV; + dmutc_status_t status = {0}; + int rc = read_status(file, &status); + if (rc == 0 && ((status.flags & (DMUTC_CLOCK_RUNNING | DMUTC_CLOCK_TIME_SET)) != + (DMUTC_CLOCK_RUNNING | DMUTC_CLOCK_TIME_SET) || (status.flags & DMUTC_CLOCK_TIME_LOST))) rc = -ENODATA; + dmdrvi_rtc_time_t time = {0}; + if (rc == 0) rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_GET_TIME, &time); + Dmod_FileClose(file); + if (rc != 0) return rc; + if (time.year < status.min_year || time.year > status.max_year) return -EIO; + dmutc_calendar_t c = {time.year, time.month, time.day, time.hour, time.minute, + time.second, 0, time.reserved, time.nanosecond}; + rc = dmutc_from_calendar(&c, out); + return rc == 0 ? 0 : -EIO; +} + +dmod_dmutc_api_declaration(1.0, int, _set, (const char* device, const dmutc_time_t* time)) +{ + dmutc_calendar_t c; + int rc = dmutc_to_calendar(time, &c); + if (rc != 0) return rc; + if (*rtc_path(device) == '\0') return -EINVAL; + void* file = Dmod_FileOpen(rtc_path(device), "r+b"); + if (file == NULL) return -ENODEV; + dmutc_status_t status = {0}; + rc = read_status(file, &status); + if (rc == 0 && !(status.flags & DMUTC_CLOCK_WRITABLE)) rc = -ENOTSUP; + if (rc == 0 && (c.year < status.min_year || c.year > status.max_year)) rc = -ERANGE; + if (rc == 0) { + const dmdrvi_rtc_time_t value = {c.year, c.month, c.day, c.hour, c.minute, + c.second, c.weekday, 0, c.nanosecond}; + rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_SET_TIME, (void*)&value); + } + Dmod_FileClose(file); + return rc; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..5cb9d02 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,56 @@ +set(DMOD_MODULE_NAME test_dmutc) +set(DMOD_MODULE_VERSION ${PROJECT_VERSION}) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") +set(DMOD_STACK_SIZE 2048) +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/test_dmutc.dmr) +set(DMOD_PRIORITY 0) + +dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + dmutc_test.c +) + +target_include_directories(${DMOD_MODULE_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../include +) + +target_link_libraries(${DMOD_MODULE_NAME} dmutc_if) + +# Register the test module with ctest - this replays the same two steps CI +# runs (see .github/templates/ci.yml.template): install the test module's +# dependencies with dmf-get, then execute it through dmod_loader. The test's +# exit code equals its number of failed steps (see dmod_add_test). +find_program(DMF_GET_EXECUTABLE dmf-get HINTS ${DMOD_TOOLS_BIN_DIR} PATHS ENV PATH) +find_program(DMOD_LOADER_EXECUTABLE dmod_loader HINTS ${DMOD_TOOLS_BIN_DIR} PATHS ENV PATH) + +if(DMF_GET_EXECUTABLE AND DMOD_LOADER_EXECUTABLE) + add_test( + NAME ${DMOD_MODULE_NAME} + COMMAND ${CMAKE_COMMAND} -E env DMOD_DMF_DIR=${DMOD_DMF_DIR} + bash -c "${DMF_GET_EXECUTABLE} install -d ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}-local.dmd -y && ${DMOD_LOADER_EXECUTABLE} ${DMOD_DMF_DIR}/${DMOD_MODULE_NAME}.dmf" + ) +else() + message(WARNING "dmf-get/dmod_loader not found - 'ctest' will not run ${DMOD_MODULE_NAME}. Build the tools first with -DDMOD_BUILD_TOOLS=ON") +endif() + +if(DMOD_ARCH MATCHES "armv7") + target_compile_definitions(${DMOD_MODULE_NAME} PRIVATE DMUTC_BOARD_TEST) +endif() + +# Exercise the actual provider with an independent VFS/RTC model on the host. +if(DMOD_ARCH STREQUAL "x86_64" OR NOT DEFINED DMOD_ARCH) + find_program(HOST_CC NAMES cc gcc REQUIRED) + set(PROVIDER_TEST ${CMAKE_CURRENT_SOURCE_DIR}/rtc) + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_provider + COMMAND ${HOST_CC} -std=c11 -Wall -Wextra -Werror + -I${PROVIDER_TEST}/stubs -I${CMAKE_CURRENT_SOURCE_DIR}/../include + -I${DMOD_DMF_DIR}/inc/dmdrvi/include + ${PROVIDER_TEST}/rtc_test.c ${CMAKE_CURRENT_SOURCE_DIR}/../src/dmutc.c + ${CMAKE_CURRENT_SOURCE_DIR}/../src/rtc.c + -o ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_provider + DEPENDS ${PROVIDER_TEST}/rtc_test.c ${PROVIDER_TEST}/stubs/dmod.h + ${PROVIDER_TEST}/stubs/dmutc_defs.h ${CMAKE_CURRENT_SOURCE_DIR}/../include/dmutc.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/dmutc.c ${CMAKE_CURRENT_SOURCE_DIR}/../src/rtc.c + ${DMOD_DMF_DIR}/inc/dmdrvi/include/dmdrvi_ioctl.h) + add_custom_target(rtc_provider_test_build ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_provider) + add_test(NAME rtc_provider COMMAND ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_provider) +endif() diff --git a/tests/dmutc_test.c b/tests/dmutc_test.c new file mode 100644 index 0000000..47d76db --- /dev/null +++ b/tests/dmutc_test.c @@ -0,0 +1,121 @@ +#define DMOD_ENABLE_REGISTRATION ON +#include "dmod_test.h" +#include "dmutc.h" +#include +#include +#include +#include + +_Static_assert(sizeof(dmutc_time_t) == 16, "time ABI"); +_Static_assert(offsetof(dmutc_time_t, nanosecond) == 8, "fraction ABI"); +_Static_assert(sizeof(dmutc_calendar_t) == 16, "calendar ABI"); +_Static_assert(sizeof(dmutc_status_t) == 24, "status ABI"); +DMOD_TEST_STEP(known_unix_values_before_epoch_after_2038_and_at_limits) +{ + static const struct { int y,m,d,h,min,s,weekday; int64_t seconds; } vectors[] = { + {1,1,1,0,0,0,1,-62135596800LL}, + {1969,12,31,23,59,59,3,-1LL}, + {1970,1,1,0,0,0,4,0LL}, + {2000,2,29,0,0,0,2,951782400LL}, + {2038,1,19,3,14,8,2,2147483648LL}, + {2100,3,1,0,0,0,1,4107542400LL}, + {2400,2,29,12,0,0,2,13574606400LL}, + {9999,12,31,23,59,59,5,253402300799LL}, + }; + for (unsigned i = 0; i < sizeof(vectors)/sizeof(vectors[0]); ++i) { + dmutc_calendar_t c = {vectors[i].y,vectors[i].m,vectors[i].d,vectors[i].h, + vectors[i].min,vectors[i].s,99,0,123456789}, out; + dmutc_time_t time; + DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c, &time), 0); + DMOD_TEST_EXPECT_EQ(time.seconds, vectors[i].seconds); + DMOD_TEST_EXPECT_EQ(time.nanosecond, 123456789); + DMOD_TEST_EXPECT_EQ(time.reserved, 0); + DMOD_TEST_EXPECT_EQ(dmutc_to_calendar(&time, &out), 0); + DMOD_TEST_EXPECT_EQ(out.year,c.year); DMOD_TEST_EXPECT_EQ(out.month,c.month); + DMOD_TEST_EXPECT_EQ(out.day,c.day); DMOD_TEST_EXPECT_EQ(out.hour,c.hour); + DMOD_TEST_EXPECT_EQ(out.minute,c.minute); DMOD_TEST_EXPECT_EQ(out.second,c.second); + DMOD_TEST_EXPECT_EQ(out.weekday,vectors[i].weekday); + DMOD_TEST_EXPECT_EQ(out.nanosecond,c.nanosecond); DMOD_TEST_EXPECT_EQ(out.reserved,0); + } +} +DMOD_TEST_STEP(gregorian_centuries_and_invalid_fields) +{ + dmutc_calendar_t c = {1900,2,29,0,0,0,0,0,0}; dmutc_time_t out; + DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.year=2000; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),0); + c.year=2100; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.year=2400; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),0); + c.year=0; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.year=10000; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.year=2026; c.month=4; c.day=31; + DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.day=30; c.month=0; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.month=13; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.month=4; c.hour=24; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.hour=0; c.minute=60; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.minute=0; c.second=60; DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.second=0; c.nanosecond=1000000000u; + DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + c.nanosecond=0; c.reserved=1; + memset(&out,0xa5,sizeof(out)); DMOD_TEST_EXPECT_EQ(dmutc_from_calendar(&c,&out),-EINVAL); + const dmutc_time_t zero={0}; DMOD_TEST_EXPECT_EQ(memcmp(&out,&zero,sizeof(out)),0); +} +DMOD_TEST_STEP(floor_normalization_and_overflow_rejection) +{ + dmutc_time_t t={-1,500000000,0}; dmutc_calendar_t c; + DMOD_TEST_EXPECT_EQ(dmutc_to_calendar(&t,&c),0); + DMOD_TEST_EXPECT_EQ(c.year,1969); DMOD_TEST_EXPECT_EQ(c.second,59); + DMOD_TEST_EXPECT_EQ(c.nanosecond,500000000); + const int64_t invalid[]={INT64_MIN,-62135596801LL,253402300800LL,INT64_MAX}; + for (unsigned i=0;i=seed.seconds && now.seconds<=seed.seconds+1); + dmutc_time_t fraction=seed; fraction.nanosecond=1; + DMOD_TEST_EXPECT_EQ(dmutc_set(NULL,&fraction),-ERANGE); + saved.seconds+=(int64_t)((Dmod_GetUptime()-started)/1000); + DMOD_TEST_EXPECT_EQ(dmutc_set(NULL,&saved),0); + DMOD_TEST_EXPECT_EQ(dmutc_get(NULL,&now),0); + DMOD_TEST_EXPECT_TRUE(now.seconds>=saved.seconds && now.seconds<=saved.seconds+1); + Dmod_Printf("UTC_SAMPLE seconds=%lld nanosecond=%u resolution_ns=%llu\n", + (long long)now.seconds,(unsigned)now.nanosecond,(unsigned long long)status.resolution_ns); +} +#endif diff --git a/tests/rtc/rtc_test.c b/tests/rtc/rtc_test.c new file mode 100644 index 0000000..39169fa --- /dev/null +++ b/tests/rtc/rtc_test.c @@ -0,0 +1,145 @@ +#include "dmod.h" +#include "dmutc.h" +#include "dmdrvi_ioctl.h" +#include +#include +#include +#include + +static dmdrvi_rtc_time_t clock_time; +static dmdrvi_rtc_status_t clock_status; +static dmdrvi_rtc_info_t clock_info; +static unsigned opens, closes, writes, reads; +static bool open_error, write_handle; +static int failing_command, command_error; +static char last_path[64]; +static void reset(void) +{ + clock_time=(dmdrvi_rtc_time_t){2040,2,29,12,34,56,3,0,123456789}; + clock_status=(dmdrvi_rtc_status_t){DMDRVI_RTC_STATUS_RUNNING | DMDRVI_RTC_STATUS_TIME_SET,0}; + clock_info=(dmdrvi_rtc_info_t){1,DMDRVI_RTC_CAP_SET_TIME,1900,2199,0}; + opens=closes=writes=reads=0; open_error=false; + failing_command=command_error=0; +} +void* Dmod_FileOpen(const char* path, const char* mode) +{ + snprintf(last_path,sizeof(last_path),"%s",path); + if(open_error)return NULL; + ++opens; + write_handle=strcmp(mode,"r+b")==0; + assert(write_handle || strcmp(mode,"rb")==0); + return &clock_time; +} +void Dmod_FileClose(void* file) +{ + assert(file==&clock_time); ++closes; +} +int Dmod_Ioctl(void* file,int command,void* arg) +{ + assert(file==&clock_time); + if(command==failing_command)return command_error; + switch(command) { + case DMDRVI_IOCTL_RTC_GET_INFO: *(dmdrvi_rtc_info_t*)arg=clock_info; return 0; + case DMDRVI_IOCTL_RTC_GET_STATUS: *(dmdrvi_rtc_status_t*)arg=clock_status; return 0; + case DMDRVI_IOCTL_RTC_GET_TIME: ++reads; *(dmdrvi_rtc_time_t*)arg=clock_time; return 0; + case DMDRVI_IOCTL_RTC_SET_TIME: { + assert(write_handle); ++writes; + dmdrvi_rtc_time_t* time=arg; + if(time->nanosecond%clock_info.resolution_ns!=0)return -ERANGE; + clock_time=*time; return 0; + } + default:return -ENOTTY; + } +} +static void assert_zero(const dmutc_time_t* t) +{ + const dmutc_time_t zero={0}; assert(memcmp(t,&zero,sizeof(*t))==0); +} +static void reads_utc_without_stm32_range_or_precision_assumptions(void) +{ + reset(); dmutc_time_t time; dmutc_calendar_t calendar; + assert(dmutc_get("/dev/other-rtc",&time)==0); + assert(strcmp(last_path,"/dev/other-rtc")==0); + assert(time.seconds>INT32_MAX && time.nanosecond==123456789); + assert(dmutc_to_calendar(&time,&calendar)==0); + assert(calendar.year==2040 && calendar.month==2 && calendar.day==29); + assert(calendar.hour==12 && calendar.minute==34 && calendar.second==56); + assert(opens==closes && reads==1); +} +static void lost_unset_and_stopped_clocks_do_not_supply_utc(void) +{ + reset(); dmutc_time_t time; + clock_status.flags=DMDRVI_RTC_STATUS_RUNNING; + assert(dmutc_get(NULL,&time)==-ENODATA); assert_zero(&time); + clock_status.flags=DMDRVI_RTC_STATUS_TIME_SET; + assert(dmutc_get(NULL,&time)==-ENODATA); assert_zero(&time); + clock_status.flags=DMDRVI_RTC_STATUS_RUNNING|DMDRVI_RTC_STATUS_TIME_SET|DMDRVI_RTC_STATUS_TIME_LOST; + // A non-advertised loss flag is not continuity detection. + dmutc_status_t status; + assert(dmutc_status(NULL,&status)==0); + assert(!(status.flags&(DMUTC_CLOCK_LOSS_DETECTION|DMUTC_CLOCK_TIME_LOST))); + assert(dmutc_get(NULL,&time)==0); + clock_info.capabilities|=DMDRVI_RTC_CAP_LOSS_DETECTION; + assert(dmutc_get(NULL,&time)==-ENODATA); assert_zero(&time); + assert(dmutc_status(NULL,&status)==0); + assert(status.flags&DMUTC_CLOCK_TIME_LOST); + assert(opens==closes); +} +static void write_validation_and_driver_precision_errors(void) +{ + reset(); dmutc_calendar_t calendar={2000,2,29,1,2,3,0,0,250000000}; dmutc_time_t time; + assert(dmutc_from_calendar(&calendar,&time)==0); + assert(dmutc_set(NULL,&time)==0); + assert(strcmp(last_path,"/dev/rtc")==0 && clock_time.nanosecond==250000000 && writes==1); + clock_info.resolution_ns=1000000000; + assert(dmutc_set(NULL,&time)==-ERANGE && clock_time.nanosecond==250000000); + clock_info.capabilities=0; + assert(dmutc_set(NULL,&time)==-ENOTSUP); + clock_info.capabilities=DMDRVI_RTC_CAP_SET_TIME; + calendar.year=1800; calendar.day=28; assert(dmutc_from_calendar(&calendar,&time)==0); + unsigned before=writes; + assert(dmutc_set(NULL,&time)==-ERANGE && writes==before); + time.nanosecond=1000000000u; + before=opens; + assert(dmutc_set(NULL,&time)==-EINVAL && opens==before); + assert(opens==closes); +} +static void errors_close_handles_and_clear_outputs(void) +{ + reset(); dmutc_time_t time; dmutc_status_t status; + const int commands[]={DMDRVI_IOCTL_RTC_GET_INFO,DMDRVI_IOCTL_RTC_GET_STATUS,DMDRVI_IOCTL_RTC_GET_TIME}; + for(unsigned i=0;i +#include +#include +#include "dmutc_defs.h" +typedef struct Dmod_Config Dmod_Config_t; +void* Dmod_FileOpen(const char* path, const char* mode); +void Dmod_FileClose(void* file); +int Dmod_Ioctl(void* file, int command, void* arg); +#endif diff --git a/tests/rtc/stubs/dmutc_defs.h b/tests/rtc/stubs/dmutc_defs.h new file mode 100644 index 0000000..58a8ce9 --- /dev/null +++ b/tests/rtc/stubs/dmutc_defs.h @@ -0,0 +1,5 @@ +#ifndef TEST_DMUTC_DEFS_H +#define TEST_DMUTC_DEFS_H +#define dmod_dmutc_api(v, ret, name, args) ret dmutc##name args +#define dmod_dmutc_api_declaration(v, ret, name, args) ret dmutc##name args +#endif diff --git a/tests/test_dmutc.dmr b/tests/test_dmutc.dmr new file mode 100644 index 0000000..f8cab27 --- /dev/null +++ b/tests/test_dmutc.dmr @@ -0,0 +1,6 @@ +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/tests/${module}.dmr] +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] From 807a975f1339352c8c67f4256d5d03eeb166dc78 Mon Sep 17 00:00:00 2001 From: Jax Calder Date: Fri, 16 Oct 2026 12:20:50 +0000 Subject: [PATCH 2/2] Bind UTC sources through RTC device rules and library services --- CMakeLists.txt | 5 +- README.md | 86 ++++++---- configs/dmutc.rules | 2 + configs/dmutc@.ini | 4 + dmutc.dmr | 3 + docs/api-reference.md | 82 +++++----- docs/testing.md | 43 ++++- .../stm32f746g-disco-service-2026-10-16.txt | 126 +++++++++++++++ include/dmutc.h | 24 +-- src/dmutc.c | 31 +++- src/rtc.c | 56 ++++--- src/service.c | 90 +++++++++++ src/service.h | 10 ++ tests/CMakeLists.txt | 10 +- tests/dmutc_test.c | 52 +++++- tests/rtc/rtc_test.c | 151 +++++++++++++++--- tests/rtc/stubs/dmod.h | 2 + tests/rtc/stubs/dmosi.h | 14 ++ tests/rtc/stubs/libsystemd_service.h | 4 + 19 files changed, 649 insertions(+), 146 deletions(-) create mode 100644 configs/dmutc.rules create mode 100644 configs/dmutc@.ini create mode 100644 docs/validation/stm32f746g-disco-service-2026-10-16.txt create mode 100644 src/service.c create mode 100644 src/service.h create mode 100644 tests/rtc/stubs/dmosi.h create mode 100644 tests/rtc/stubs/libsystemd_service.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 051c620..61b8d91 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,7 +51,7 @@ set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/dmutc.dmr) set(DMOD_MAL_IMPLS ) # List of DIF interfaces implemented by this module (leave empty if none) -set(DMOD_DIF_IMPLS ) +set(DMOD_DIF_IMPLS libsystemd) # # dmod_add_library - create a library module @@ -63,6 +63,7 @@ dmod_add_library(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} # List of source files - can include C and C++ files src/dmutc.c src/rtc.c + src/service.c ) target_include_directories(${DMOD_MODULE_NAME} PRIVATE @@ -73,6 +74,6 @@ target_include_directories(${DMOD_MODULE_NAME}_if INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include ) -dmod_link_modules(${DMOD_MODULE_NAME} dmdrvi) +dmod_link_modules(${DMOD_MODULE_NAME} dmdrvi libsystemd dmosi) enable_testing() add_subdirectory(tests) diff --git a/README.md b/README.md index 47d162a..e2105ba 100644 --- a/README.md +++ b/README.md @@ -2,60 +2,78 @@ [![CI](https://github.com/choco-technologies/dmutc/actions/workflows/ci.yml/badge.svg)](https://github.com/choco-technologies/dmutc/actions/workflows/ci.yml) -Portable UTC library for DMOD. It converts Gregorian UTC calendars to signed -64-bit Unix time and reads/sets a configured RTC through VFS and the generic -`DMDRVI_IOCTL_RTC_*` interface. It is a library, with no driver DIF, hardware -port, MCU registers or dependency on dmrtc. Other RTC implementations work -through the same contract. +UTC library and device-driven library service for DMOD. It converts Gregorian +UTC calendars to signed 64-bit Unix time and accesses the RTC selected by the +system's device/service rules. It has no driver DIF, hardware port, MCU code +or dependency on a particular RTC implementation. + +Device discovery owns the binding: + +1. dmdevfs probes `DMDRVI_IOCTL_RTC_GET_INFO` and reports class `rtc`, even if + its calendar is unset. The device event carries the actual VFS path. +2. `dmutc.rules` starts `dmutc@%name` for that device. +3. `dmutc@.ini` is `type=library`, `exec=dmutc`, `args=%v`. libsystemd enables + the shared library and calls its service START DIF with the expanded path. +4. Device removal calls the matching service STOP DIF and withdraws that source. + +Applications consume UTC without selecting a driver or path: ```c #include "dmutc.h" dmutc_time_t now; -int rc = dmutc_get(NULL, &now); /* /dev/rtc; seconds and nanoseconds */ +int rc = dmutc_get(&now); if (rc == 0) { dmutc_calendar_t calendar; rc = dmutc_to_calendar(&now, &calendar); } - -/* Another RTC can be selected per call. Its calendar must store UTC. */ -rc = dmutc_get("/dev/external-rtc", &now); ``` -The library is stateless and allocates no persistent context, task, timer or -mutex. Each RTC operation opens and closes its own VFS handle. Pure conversion -functions require neither VFS nor an RTC. No libc `time_t`, timezone setting, -locale or `mktime()` is involved. +`dmutc_get`, `dmutc_set` and `dmutc_status` return ENODEV when no service has +bound a source. There is no default path, fallback to `/dev/rtc`, directory +scan or application-side bind API. `dmutc_source()` copies the selected path +for diagnostics only. An API consumer may enable the module before the RTC +appears; later service START still binds it. Stopping one unit neither unloads +code used by consumers nor removes other units' sources. + +Multiple active source units are ordered lexicographically by unit name. +Removing the selected one selects the next; removing the last makes UTC +unavailable. Rules can restrict which devices start a UTC service. The chosen +RTC must store UTC rather than local civil time. Stopped, unset and detected-lost +clocks are rejected; lack of loss detection never proves continuity. A failed +read clears its output. Successful UTC reads do not authenticate their source. -Time uses the POSIX convention: seconds since 1970-01-01T00:00:00Z, excluding -leap seconds, with nanoseconds in 0..999999999. Years 1..9999 are supported; -negative epochs and dates after 2038 work on 32-bit targets. For example, -`{-1, 500000000, 0}` is 1969-12-31T23:59:59.500000000Z. RTC hardware may have -narrower years/precision; restrictions and errors are preserved, never rounded. +Time uses POSIX seconds since 1970-01-01T00:00:00Z, excluding leap seconds, +with normalized nanoseconds. Years 1..9999, negative epochs and post-2038 dates +work on 32-bit targets without libc time_t, timezone or locale state. Device +range and precision errors propagate without rounding. Conversion functions +work independently of whether any RTC service is active. -`dmutc_get()` rejects stopped, unset and detected-lost clocks. Without a -hardware loss-detection capability, an unset loss flag does not establish -continuity. The chosen RTC must store UTC rather than local civil time. -Successful reads do **not** authenticate the clock or establish its accuracy. -A TLS caller must obtain its trust policy from the application; this library -does not turn a retained RTC marker or unauthenticated time into trusted UTC. -Network synchronization is a separate source of time, which can use -`dmutc_set()` after validating its sample. +The library keeps a mutex-protected source registry. RTC operations own their +VFS handles and finish before a concurrent STOP can release the chosen path. +There is no background thread, timer or polling loop. -Build and test using CMake: +Install the packaged configs with the standard service/rules tags: + +```text +dmutc service=dmutc@.ini +dmutc rules=dmutc.rules +``` + +The library uses the paired library-service lifecycle DIF in libsystemd; release +that interface and RTC reporting in dmdevfs before integrating this version. + +Build with CMake: ```sh cmake -S . -B build/host -DDMOD_TOOLS_NAME=arch/x86_64 cmake --build build/host --parallel ctest --test-dir build/host --output-on-failure - -cmake -S . -B build/stm32f7 -DDMOD_TOOLS_NAME=arch/armv7/cortex-m7 -cmake --build build/stm32f7 --parallel ``` -For local development, `-DDMOD_DIR=/path/to/dmod` selects an existing checkout; -no development-machine path or dependency version is pinned in source. -The module is scaffolded using DMOD's `new-module.sh --type library --github`. -Packages contain the public/generated headers, documentation, license and DMR. +Use `-DDMOD_TOOLS_NAME=arch/armv7/cortex-m7` for M7, or cortex-m4 for M4. +`-DDMOD_DIR=/path/to/dmod` selects a local checkout. No dependency version or +development-machine path is pinned in source. The initial library was generated +using dmod's new-module.sh with --type library and --github. See [API reference](docs/api-reference.md) and [validation](docs/testing.md). diff --git a/configs/dmutc.rules b/configs/dmutc.rules new file mode 100644 index 0000000..d7d9f9c --- /dev/null +++ b/configs/dmutc.rules @@ -0,0 +1,2 @@ +[class=rtc] +start=dmutc@%name diff --git a/configs/dmutc@.ini b/configs/dmutc@.ini new file mode 100644 index 0000000..10e138a --- /dev/null +++ b/configs/dmutc@.ini @@ -0,0 +1,4 @@ +description=UTC source %i +type=library +exec=dmutc +args=%v diff --git a/dmutc.dmr b/dmutc.dmr index f69f1ad..bc82a10 100644 --- a/dmutc.dmr +++ b/dmutc.dmr @@ -31,3 +31,6 @@ license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENS # Adding itself dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/${module}.dmr] + +# Library service unit and its device rule. +configs=./configs => ${destination}/${module}/configs [origin=${repo_dir}/configs] diff --git a/docs/api-reference.md b/docs/api-reference.md index ebe56d5..1c0e13a 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -1,43 +1,49 @@ # API reference -Include `dmutc.h` and declare `dmutc` using `dmod_link_modules()`. -All operations return zero on success and a negative errno-compatible code on -failure. Required output pointers must be non-NULL; outputs are cleared on -failure. Inputs/outputs of conversion functions must not overlap. +Include `dmutc.h`; declare the module through `dmod_link_modules()`. +All calls return zero or a negative errno-compatible code. Required outputs +are cleared on failure; conversion input/output objects must not overlap. | Function | Behavior | |---|---| -| `dmutc_from_calendar(calendar, time)` | Convert UTC Gregorian calendar to Unix seconds/nanoseconds. Invalid fields return EINVAL. | -| `dmutc_to_calendar(time, calendar)` | Convert normalized Unix time to UTC; timestamps outside years 1..9999 return ERANGE. | -| `dmutc_get(device, time)` | Read an initialized running RTC, interpreting its civil calendar as UTC. | -| `dmutc_set(device, time)` | Validate and convert Unix time, then write RTC through its generic ioctl. | -| `dmutc_status(device, status)` | Report device years, quantum, running/set/write/loss capabilities and state. Works when unset. | - -`device == NULL` selects `/dev/rtc`. An empty path returns EINVAL; failed VFS -open returns ENODEV (the SAL open API does not expose a portable errno). -Driver errors such as EIO, ENODATA, ERANGE, ETIMEDOUT, EACCES and ENOTSUP -propagate. GET_INFO and GET_STATUS are required by the RTC device contract. -Malformed device metadata/calendar returns EIO. Loss is interpreted only when -the driver advertises loss detection. No status bit means authenticated UTC. - -`dmutc_time_t` contains signed `int64_t seconds`, normalized `uint32_t -nanosecond` and a zero `reserved` field (16 bytes). `dmutc_calendar_t` contains -full year, month, day, hour, minute, second, weekday, zero reserved field and -nanoseconds (16 bytes). Weekday is Monday=1..Sunday=7 and is ignored on input. -Gregorian leap-century rules apply; the API does not encode leap seconds. - -`dmutc_status_t` reports `resolution_ns`, inclusive `min_year`/`max_year`, flags -and zero reserved field (24 bytes). The quantum reports representable precision, -not oscillator accuracy. Flags are RUNNING, TIME_SET, LOSS_DETECTION, TIME_LOST -and WRITABLE, prefixed with `DMUTC_CLOCK_`. - -Every RTC call uses its own file handle (rb for reads, r+b for writes) and -closes it on every exit after opening. Validation of input precedes opening -for writes. A device-range violation is rejected before SET_TIME; finer -precision is passed intact for the driver to accept or reject without rounding. -Metadata and status are observations, not an atomic promise about a later call: -an intervening driver change is handled by propagating GET_TIME/SET_TIME errors. - -No global UTC cache, background worker, polling, clock discipline or hardware -port is introduced. Network/GNSS sources may call SET after validating samples; -the application decides which source to trust and when to accept it for TLS. +| `dmutc_from_calendar(calendar, time)` | Gregorian UTC to normalized Unix seconds/nanoseconds. | +| `dmutc_to_calendar(time, calendar)` | Unix time to UTC; years outside 1..9999 fail with ERANGE. | +| `dmutc_get(time)` | Read the source bound by the active library service. | +| `dmutc_set(time)` | Validate, convert and write the bound RTC. | +| `dmutc_status(status)` | Observe the bound source's range, quantum and state/capabilities. | +| `dmutc_source(buffer, capacity)` | Copy the selected source path, including NUL; ENOSPC if too small. | + +No source is implicitly selected. With no active source unit, device operations +return ENODEV; pure conversion remains available. Applications never pass a +path into UTC read/write/status calls. The service lifecycle accepts the path +from libsystemd's expanded `args=%v`, as supplied by the `rtc` device event. + +`dmutc_time_t` is 16 bytes: signed int64_t seconds, uint32_t nanoseconds and +zero reserved field. Fractions are 0..999999999 even for negative epochs: +{-1,500000000,0} represents 1969-12-31T23:59:59.500000000Z. Calendars are 16 +bytes with full Gregorian year, month/day/hour/minute/second, weekday +(Monday=1..Sunday=7, ignored on input), zero reserved field and nanoseconds. +Leap-century rules apply; POSIX time excludes leap seconds. + +Status is 24 bytes: resolution_ns, inclusive min_year/max_year, flags and zero +reserved field. DMUTC_CLOCK_ flags are RUNNING, TIME_SET, LOSS_DETECTION, +TIME_LOST and WRITABLE. Quantum is representable granularity, not accuracy. +TIME_LOST is only meaningful when loss detection is advertised. No flag +attests UTC authenticity, accuracy or continuity on other devices. + +SET rejects malformed time before accessing hardware. Range restrictions are +checked before SET_TIME; subsecond precision reaches the driver intact for it +to accept or reject. Driver errors propagate. Failed opens return ENODEV; +malformed metadata/calendar returns EIO; stopped/unset/detected-lost clocks +return ENODATA. Metadata is an observation, not an atomic promise about a later +ioctl; intervening device changes are handled by the driver error path. + +Each source is owned by a libsystemd unit instance. Multiple units are sorted +by unit name, independent of arrival order; the first is selected. STOP waits +on the source mutex for active I/O, removes only its binding, and leaves shared +library code available to API consumers. No source is reconstructed from a +fixed path after STOP. Module deinitialization releases the registry and mutex. + +Calls use VFS/mutexes and belong in thread context. There is no periodic worker, +network synchronization or implicit authentication of time. A TLS application +must establish which time sources it accepts through its system/trust policy. diff --git a/docs/testing.md b/docs/testing.md index 0bd9800..9a40309 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -1,4 +1,41 @@ -# Validation — 2026-10-16 +# Service-driven RTC binding — 2026-10-16 + +The current implementation receives its source through dmdevfs RTC events, +libsystemd rules and a `type=library` unit. It has no default device path. + +- Native ctest: 2/2 targets; DMOD conversion diagnostic 4/4 and provider model + 6/6, including multiple sources and STOP waiting for an in-flight read. +- libsystemd diagnostic: 62/62, including cold load, prior API enable, + independent unit instances, failed START and refreshed event paths. + systemd/service command diagnostics: 1/1 each. +- dmdevfs native suite: 10/10 targets, including RTC reporting with an unset + calendar and report-mask filtering. +- Cortex-M4 and Cortex-M7 dmutc builds passed. libsystemd and dmdevfs M7 builds + passed. No execution on M4 is claimed. +- STM32F746G-DISCO after flashing/reset: `dmutc@rtc` automatically running, + type=library, pid=0. dmutc 6/6, RTC 12/12, RNG 6/6. +- Hardware checks stop the UTC unit while the RTC still exists, confirm + ENODEV, restart it, remove its device notification, reject a re-add with a + nonexistent path, then bind the actual path from a fresh event. The logged + open error for `/no-such-utc-source` is an intentional negative test. +- The harness sets PC UTC before the round-trip tests and restores it after + all diagnostics. RTC text reads and DHCP were checked afterwards. These + checks do not establish authenticated time or oscillator accuracy. + +[Current board console](validation/stm32f746g-disco-service-2026-10-16.txt). + +Validation uses local build manifests for the new libsystemd lifecycle header +and unreleased modules. They are build-environment files, not source dependency +pins. Public CI needs the libsystemd change released before dmutc can resolve +that header from the package registry. Boot installs the packaged rule and +service through the normal service/rules tags. + +Current Cortex-M7 artifacts: + +- `dmutc.dmfc`: 5076 bytes; SHA-256 `2e0878df76e38b3c76955551e8ea6acc45cb44f4f0635eead3df6c4cdb66b2d4`. +- `test_dmutc.dmfc`: 6980 bytes; SHA-256 `2d5a4493e3e8071b145723d01ed9c0b67b6d2abb0c32ae19d90dabc426fbb5c6`. + +# Initial conversion and explicit-path implementation — 2026-10-16 - Native ctest: 2/2 targets. The actual DMOD diagnostic passes 4/4 steps for known epochs, negative fractional times, leap centuries, invalid input, @@ -26,8 +63,8 @@ configuration change or development-machine package path is committed. Public package headers resolve dmdrvi 2.8 after refreshing a stale dmf-get environment cache. Source dependencies remain unversioned in dmod_link_modules. -STM32F7 library size: 5104 bytes DMF, 4054 bytes compressed DMFC. It has no -non-system runtime module dependencies, hardware port or driver registration. +STM32F7 library size: 5104 bytes DMF, 4054 bytes compressed DMFC. At that stage it had no non-system runtime module dependencies, hardware port +or driver registration. The service integration below supersedes path selection. - `dmutc.dmfc` SHA-256: `8f535b6495c99365c2ba8133cc5086b27e87177faff02f2c6912eb7fe046ba67` - `test_dmutc.dmfc` SHA-256: `7ddab5049ac8b5894a577415d3ccf41975c15ee7b01b321b40c64db869d8e74f` diff --git a/docs/validation/stm32f746g-disco-service-2026-10-16.txt b/docs/validation/stm32f746g-disco-service-2026-10-16.txt new file mode 100644 index 0000000..1defa39 --- /dev/null +++ b/docs/validation/stm32f746g-disco-service-2026-10-16.txt @@ -0,0 +1,126 @@ +rtc set 2026-10-16T12:17:42 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> service status dmutc@rtc +dmutc@rtc running pid=0 UTC source rtc + type=library restart=no +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> 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-16T12:17:44 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T12:17:44 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T12:17:45 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> test_dmutc +=== DMOD Test Runner === +[ RUN ] vfs_rtc_utc_round_trip_and_restore +UTC_SAMPLE seconds=1792153065 nanosecond=0 resolution_ns=1000000000 +[ OK ] vfs_rtc_utc_round_trip_and_restore +[ RUN ] library_service_controls_binding_without_a_path_fallback +UTC_SOURCE unit=dmutc@rtc path=/dev/rtc +dmramfs: [ERROR] dmramfs: File not found and cannot be created: '/no-such-utc-source' +[ERROR] Failed to open file '/no-such-utc-source' +[ OK ] library_service_controls_binding_without_a_path_fallback +[ RUN ] month_boundaries_round_trip_across_the_supported_era +[ OK ] month_boundaries_round_trip_across_the_supported_era +[ RUN ] floor_normalization_and_overflow_rejection +[ OK ] floor_normalization_and_overflow_rejection +[ RUN ] gregorian_centuries_and_invalid_fields +[ OK ] gregorian_centuries_and_invalid_fields +[ RUN ] known_unix_values_before_epoch_after_2038_and_at_limits +[ OK ] known_unix_values_before_epoch_after_2038_and_at_limits + +=== Results: 6/6 passed === +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_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=86eee190 +[ 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-16T12:17:51 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> rtc get +2026-10-16T12:17:51 weekday=5 +stm32f746xg@/> echo RTC_COMMAND_DONE_473 +RTC_COMMAND_DONE_473 +stm32f746xg@/> cat /dev/rtc +2026-10-16T12:17:51 +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@/> service status dmutc@rtc +dmutc@rtc running pid=0 UTC source rtc + type=library restart=no +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/include/dmutc.h b/include/dmutc.h index 8d35f00..ac876ec 100644 --- a/include/dmutc.h +++ b/include/dmutc.h @@ -1,6 +1,7 @@ #ifndef DMUTC_H #define DMUTC_H #include +#include #include "dmutc_defs.h" #ifdef __cplusplus extern "C" { @@ -44,16 +45,19 @@ typedef struct { dmod_dmutc_api(1.0, int, _from_calendar, (const dmutc_calendar_t* calendar, dmutc_time_t* time)); dmod_dmutc_api(1.0, int, _to_calendar, (const dmutc_time_t* time, dmutc_calendar_t* calendar)); -/* Stateless VFS operations: NULL device selects /dev/rtc. The chosen RTC - * must store UTC, never local civil time. Each call opens/closes its own - * handle; reads use rb, writes r+b. No dmrtc or port dependency. - * -ENODEV: opening the device failed; -ENODATA: unset/lost/stopped clock; - * other negative driver errors propagate. Outputs are cleared on failure. - * A successful read provides configured UTC, not authenticated time. TLS - * users must establish their trust policy independently of these APIs. */ -dmod_dmutc_api(1.0, int, _get, (const char* device, dmutc_time_t* time)); -dmod_dmutc_api(1.0, int, _set, (const char* device, const dmutc_time_t* time)); -dmod_dmutc_api(1.0, int, _status, (const char* device, dmutc_status_t* status)); +/* The RTC source is supplied by a libsystemd library-service instance. + * get/set/status return -ENODEV while no source is bound. There is no default + * path or API-side device lookup. The chosen RTC must store UTC. Each operation + * owns its VFS handle; stop waits for in-flight operations before unbinding. + * Multiple units are ordered lexicographically by unit name; removing the + * selected unit selects the next active source. No flag authenticates UTC. + * Outputs are cleared on failure; negative driver errors propagate. */ +dmod_dmutc_api(1.0, int, _get, (dmutc_time_t* time)); +dmod_dmutc_api(1.0, int, _set, (const dmutc_time_t* time)); +dmod_dmutc_api(1.0, int, _status, (dmutc_status_t* status)); +/* Copy the selected source path for diagnostics; -ENOSPC if too small. + * Includes the NUL terminator; buffer is cleared on failure. */ +dmod_dmutc_api(1.0, int, _source, (char* buffer, size_t capacity)); #ifdef __cplusplus } #endif diff --git a/src/dmutc.c b/src/dmutc.c index e34edc6..899f315 100644 --- a/src/dmutc.c +++ b/src/dmutc.c @@ -1,6 +1,8 @@ #define DMOD_ENABLE_REGISTRATION ON #include "dmod.h" #include "dmutc.h" +#include "libsystemd_service.h" +#include "service.h" #include #include @@ -67,12 +69,31 @@ dmod_dmutc_api_declaration(1.0, int, _to_calendar, return 0; } -int dmod_init(const Dmod_Config_t* config) +dmod_dmutc_api_declaration(1.0, int, _source, (char* buffer, size_t capacity)) { - (void)config; - return 0; + if (buffer == NULL || capacity == 0) return -EINVAL; + buffer[0] = '\0'; + const char* path; + int rc = dmutc_lock_source(&path); + if (rc != 0) return rc; + size_t length = strlen(path) + 1; + if (length > capacity) rc = -ENOSPC; + else memcpy(buffer, path, length); + dmutc_unlock_source(); + return rc; } -int dmod_deinit(void) +dmod_libsystemd_dif_api_declaration(1.0, dmutc, int, _service_start, + (const char* unit, int argc, const char* const argv[])) { - return 0; + return dmutc_start_source(unit, argc, argv); +} +dmod_libsystemd_dif_api_declaration(1.0, dmutc, int, _service_stop, (const char* unit)) +{ + return dmutc_stop_source(unit); +} +int dmod_init(const Dmod_Config_t* config) +{ + (void)config; + return dmutc_sources_init(); } +int dmod_deinit(void) { return dmutc_sources_deinit(); } diff --git a/src/rtc.c b/src/rtc.c index b148f4d..b47f580 100644 --- a/src/rtc.c +++ b/src/rtc.c @@ -1,12 +1,23 @@ #include "dmod.h" #include "dmutc.h" +#include "service.h" #include "dmdrvi_ioctl.h" #include #include -static const char* rtc_path(const char* device) +static void* open_source(const char* mode, int* rc) { - return device == NULL ? "/dev/rtc" : device; + const char* path; + *rc = dmutc_lock_source(&path); + if (*rc != 0) return NULL; + void* file = Dmod_FileOpen(path, mode); + if (file == NULL) { *rc = -ENODEV; dmutc_unlock_source(); } + return file; +} +static void close_source(void* file) +{ + Dmod_FileClose(file); + dmutc_unlock_source(); } static int read_status(void* file, dmutc_status_t* out) { @@ -28,32 +39,32 @@ static int read_status(void* file, dmutc_status_t* out) return 0; } -dmod_dmutc_api_declaration(1.0, int, _status, (const char* device, dmutc_status_t* out)) +dmod_dmutc_api_declaration(1.0, int, _status, (dmutc_status_t* out)) { if (out == NULL) return -EINVAL; memset(out, 0, sizeof(*out)); - if (*rtc_path(device) == '\0') return -EINVAL; - void* file = Dmod_FileOpen(rtc_path(device), "rb"); - if (file == NULL) return -ENODEV; - int rc = read_status(file, out); - Dmod_FileClose(file); + int rc; + void* file = open_source("rb", &rc); + if (file == NULL) return rc; + rc = read_status(file, out); + close_source(file); return rc; } -dmod_dmutc_api_declaration(1.0, int, _get, (const char* device, dmutc_time_t* out)) +dmod_dmutc_api_declaration(1.0, int, _get, (dmutc_time_t* out)) { if (out == NULL) return -EINVAL; memset(out, 0, sizeof(*out)); - if (*rtc_path(device) == '\0') return -EINVAL; - void* file = Dmod_FileOpen(rtc_path(device), "rb"); - if (file == NULL) return -ENODEV; + int rc; + void* file = open_source("rb", &rc); + if (file == NULL) return rc; dmutc_status_t status = {0}; - int rc = read_status(file, &status); + rc = read_status(file, &status); if (rc == 0 && ((status.flags & (DMUTC_CLOCK_RUNNING | DMUTC_CLOCK_TIME_SET)) != (DMUTC_CLOCK_RUNNING | DMUTC_CLOCK_TIME_SET) || (status.flags & DMUTC_CLOCK_TIME_LOST))) rc = -ENODATA; dmdrvi_rtc_time_t time = {0}; if (rc == 0) rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_GET_TIME, &time); - Dmod_FileClose(file); + close_source(file); if (rc != 0) return rc; if (time.year < status.min_year || time.year > status.max_year) return -EIO; dmutc_calendar_t c = {time.year, time.month, time.day, time.hour, time.minute, @@ -62,14 +73,13 @@ dmod_dmutc_api_declaration(1.0, int, _get, (const char* device, dmutc_time_t* ou return rc == 0 ? 0 : -EIO; } -dmod_dmutc_api_declaration(1.0, int, _set, (const char* device, const dmutc_time_t* time)) +dmod_dmutc_api_declaration(1.0, int, _set, (const dmutc_time_t* time)) { dmutc_calendar_t c; int rc = dmutc_to_calendar(time, &c); if (rc != 0) return rc; - if (*rtc_path(device) == '\0') return -EINVAL; - void* file = Dmod_FileOpen(rtc_path(device), "r+b"); - if (file == NULL) return -ENODEV; + void* file = open_source("r+b", &rc); + if (file == NULL) return rc; dmutc_status_t status = {0}; rc = read_status(file, &status); if (rc == 0 && !(status.flags & DMUTC_CLOCK_WRITABLE)) rc = -ENOTSUP; @@ -79,6 +89,16 @@ dmod_dmutc_api_declaration(1.0, int, _set, (const char* device, const dmutc_time c.second, c.weekday, 0, c.nanosecond}; rc = Dmod_Ioctl(file, DMDRVI_IOCTL_RTC_SET_TIME, (void*)&value); } + close_source(file); + return rc; +} + +int dmutc_probe_source(const char* path) +{ + void* file = Dmod_FileOpen(path, "rb"); + if (file == NULL) return -ENODEV; + dmutc_status_t status = {0}; + int rc = read_status(file, &status); Dmod_FileClose(file); return rc; } diff --git a/src/service.c b/src/service.c new file mode 100644 index 0000000..5f3ecc5 --- /dev/null +++ b/src/service.c @@ -0,0 +1,90 @@ +#include "dmod.h" +#include "dmosi.h" +#include "service.h" +#include +#include + +struct source { + struct source* next; + char* unit; + char* path; +}; +static struct source* sources; +static dmosi_mutex_t lock; +static char* copy(const char* value) +{ + size_t length = strlen(value) + 1; + char* out = Dmod_Malloc(length); + if (out != NULL) memcpy(out, value, length); + return out; +} +static void free_source(struct source* source) +{ + Dmod_Free(source->unit); Dmod_Free(source->path); Dmod_Free(source); +} +int dmutc_sources_init(void) +{ + if (lock != NULL) return 0; + lock = dmosi_mutex_create(false); + return lock == NULL ? -ENOMEM : 0; +} +int dmutc_sources_deinit(void) +{ + if (lock == NULL) return 0; + int rc = dmosi_mutex_lock(lock); + if (rc != 0) return rc; + while (sources != NULL) { + struct source* old = sources; sources = old->next; free_source(old); + } + dmosi_mutex_unlock(lock); dmosi_mutex_destroy(lock); lock = NULL; + return 0; +} +int dmutc_start_source(const char* unit, int argc, const char* const argv[]) +{ + if (unit == NULL || *unit == '\0' || argc != 2 || argv == NULL || + argv[0] == NULL || argv[1] == NULL || argv[1][0] != '/') return -EINVAL; + if (lock == NULL) return -ENODEV; + struct source* candidate = Dmod_Malloc(sizeof(*candidate)); + if (candidate == NULL) return -ENOMEM; + candidate->unit = copy(unit); candidate->path = copy(argv[1]); candidate->next = NULL; + if (candidate->unit == NULL || candidate->path == NULL) { + free_source(candidate); return -ENOMEM; + } + int rc = dmosi_mutex_lock(lock); + if (rc != 0) { free_source(candidate); return rc; } + rc = dmutc_probe_source(candidate->path); /* An unset RTC is still a valid source. */ + if (rc == 0) { + struct source** slot = &sources; + while (*slot != NULL && strcmp((*slot)->unit, unit) < 0) slot = &(*slot)->next; + if (*slot != NULL && strcmp((*slot)->unit, unit) == 0) { + struct source* old = *slot; + candidate->next = old->next; *slot = candidate; free_source(old); + } else { candidate->next = *slot; *slot = candidate; } + } + dmosi_mutex_unlock(lock); + if (rc != 0) free_source(candidate); + return rc; +} +int dmutc_stop_source(const char* unit) +{ + if (unit == NULL) return -EINVAL; + if (lock == NULL) return -ENODEV; + int rc = dmosi_mutex_lock(lock); + if (rc != 0) return rc; + struct source** slot = &sources; + while (*slot != NULL && strcmp((*slot)->unit, unit) != 0) slot = &(*slot)->next; + if (*slot == NULL) rc = -ENOENT; + else { struct source* old = *slot; *slot = old->next; free_source(old); } + dmosi_mutex_unlock(lock); + return rc; +} +int dmutc_lock_source(const char** path) +{ + if (lock == NULL) return -ENODEV; + int rc = dmosi_mutex_lock(lock); + if (rc != 0) return rc; + if (sources == NULL) { dmosi_mutex_unlock(lock); return -ENODEV; } + *path = sources->path; + return 0; +} +void dmutc_unlock_source(void) { dmosi_mutex_unlock(lock); } diff --git a/src/service.h b/src/service.h new file mode 100644 index 0000000..b675546 --- /dev/null +++ b/src/service.h @@ -0,0 +1,10 @@ +#ifndef DMUTC_SERVICE_H +#define DMUTC_SERVICE_H +int dmutc_sources_init(void); +int dmutc_sources_deinit(void); +int dmutc_start_source(const char* unit, int argc, const char* const argv[]); +int dmutc_stop_source(const char* unit); +int dmutc_lock_source(const char** path); +void dmutc_unlock_source(void); +int dmutc_probe_source(const char* path); +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5cb9d02..08e8eef 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -33,6 +33,7 @@ else() endif() if(DMOD_ARCH MATCHES "armv7") + dmod_link_modules(${DMOD_MODULE_NAME} libsystemd dmosi) target_compile_definitions(${DMOD_MODULE_NAME} PRIVATE DMUTC_BOARD_TEST) endif() @@ -41,14 +42,17 @@ if(DMOD_ARCH STREQUAL "x86_64" OR NOT DEFINED DMOD_ARCH) find_program(HOST_CC NAMES cc gcc REQUIRED) set(PROVIDER_TEST ${CMAKE_CURRENT_SOURCE_DIR}/rtc) add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_provider - COMMAND ${HOST_CC} -std=c11 -Wall -Wextra -Werror + COMMAND ${HOST_CC} -std=c11 -Wall -Wextra -Werror -pthread -I${PROVIDER_TEST}/stubs -I${CMAKE_CURRENT_SOURCE_DIR}/../include + -I${CMAKE_CURRENT_SOURCE_DIR}/../src -I${DMOD_DMF_DIR}/inc/dmdrvi/include ${PROVIDER_TEST}/rtc_test.c ${CMAKE_CURRENT_SOURCE_DIR}/../src/dmutc.c - ${CMAKE_CURRENT_SOURCE_DIR}/../src/rtc.c + ${CMAKE_CURRENT_SOURCE_DIR}/../src/rtc.c ${CMAKE_CURRENT_SOURCE_DIR}/../src/service.c -o ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_provider DEPENDS ${PROVIDER_TEST}/rtc_test.c ${PROVIDER_TEST}/stubs/dmod.h - ${PROVIDER_TEST}/stubs/dmutc_defs.h ${CMAKE_CURRENT_SOURCE_DIR}/../include/dmutc.h + ${PROVIDER_TEST}/stubs/dmutc_defs.h ${PROVIDER_TEST}/stubs/dmosi.h + ${PROVIDER_TEST}/stubs/libsystemd_service.h + ${CMAKE_CURRENT_SOURCE_DIR}/../src/service.c ${CMAKE_CURRENT_SOURCE_DIR}/../src/service.h ${CMAKE_CURRENT_SOURCE_DIR}/../include/dmutc.h ${CMAKE_CURRENT_SOURCE_DIR}/../src/dmutc.c ${CMAKE_CURRENT_SOURCE_DIR}/../src/rtc.c ${DMOD_DMF_DIR}/inc/dmdrvi/include/dmdrvi_ioctl.h) add_custom_target(rtc_provider_test_build ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/test_rtc_provider) diff --git a/tests/dmutc_test.c b/tests/dmutc_test.c index 47d76db..adb653b 100644 --- a/tests/dmutc_test.c +++ b/tests/dmutc_test.c @@ -5,6 +5,9 @@ #include #include #include +#ifdef DMUTC_BOARD_TEST +#include "libsystemd.h" +#endif _Static_assert(sizeof(dmutc_time_t) == 16, "time ABI"); _Static_assert(offsetof(dmutc_time_t, nanosecond) == 8, "fraction ABI"); @@ -96,24 +99,59 @@ DMOD_TEST_STEP(month_boundaries_round_trip_across_the_supported_era) } } #ifdef DMUTC_BOARD_TEST +struct selected_unit { char name[96]; unsigned count; }; +static bool find_utc_unit(const libsystemd_service_info_t* info, void* user) +{ + struct selected_unit* found=user; + if(strncmp(info->unit_name,"dmutc@",6)==0 && info->status.state==DMOSI_PROCESS_STATE_RUNNING) { + ++found->count; + size_t n=strlen(info->unit_name); + if(nname))memcpy(found->name,info->unit_name,n+1); + } + return true; +} +DMOD_TEST_STEP(library_service_controls_binding_without_a_path_fallback) +{ + struct selected_unit unit={{0},0}; + DMOD_TEST_EXPECT_EQ(libsystemd_list(find_utc_unit,&unit),0); + DMOD_TEST_EXPECT_EQ(unit.count,1); + if(unit.count!=1 || unit.name[0]=='\0')return; + char source[128]; dmutc_time_t now; + DMOD_TEST_EXPECT_EQ(dmutc_source(source,sizeof(source)),0); + Dmod_Printf("UTC_SOURCE unit=%s path=%s\n",unit.name,source); + DMOD_TEST_EXPECT_EQ(libsystemd_stop_service(unit.name),0); + DMOD_TEST_EXPECT_TRUE(Dmod_IsModuleEnabled("dmutc")); /* API consumer is still connected. */ + DMOD_TEST_EXPECT_TRUE(Dmod_FileAvailable(source)); /* RTC still exists. */ + DMOD_TEST_EXPECT_EQ(dmutc_get(&now),-ENODEV); + DMOD_TEST_EXPECT_EQ(libsystemd_start_service(unit.name,NULL),0); + DMOD_TEST_EXPECT_EQ(dmutc_get(&now),0); + /* Replace the device event payload while keeping its identity: a stale + * cached %v must not silently bind the old, still-existing RTC path. */ + const char* name=unit.name+6; + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_removed("rtc",name),0); + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_added("rtc",name,"/no-such-utc-source"),-ENODEV); + DMOD_TEST_EXPECT_EQ(dmutc_get(&now),-ENODEV); + DMOD_TEST_EXPECT_EQ(libsystemd_notify_device_added("rtc",name,source),0); + DMOD_TEST_EXPECT_EQ(dmutc_get(&now),0); +} DMOD_TEST_STEP(vfs_rtc_utc_round_trip_and_restore) { dmutc_time_t saved, now; dmutc_status_t status; - int rc=dmutc_get(NULL,&saved); + int rc=dmutc_get(&saved); DMOD_TEST_EXPECT_EQ(rc,0); if(rc!=0)return; uint64_t started=Dmod_GetUptime(); - DMOD_TEST_EXPECT_EQ(dmutc_status(NULL,&status),0); + DMOD_TEST_EXPECT_EQ(dmutc_status(&status),0); DMOD_TEST_EXPECT_TRUE(status.flags & DMUTC_CLOCK_RUNNING); DMOD_TEST_EXPECT_TRUE(status.flags & DMUTC_CLOCK_WRITABLE); const dmutc_time_t seed={951827696,0,0}; - DMOD_TEST_EXPECT_EQ(dmutc_set(NULL,&seed),0); - DMOD_TEST_EXPECT_EQ(dmutc_get(NULL,&now),0); + DMOD_TEST_EXPECT_EQ(dmutc_set(&seed),0); + DMOD_TEST_EXPECT_EQ(dmutc_get(&now),0); DMOD_TEST_EXPECT_TRUE(now.seconds>=seed.seconds && now.seconds<=seed.seconds+1); dmutc_time_t fraction=seed; fraction.nanosecond=1; - DMOD_TEST_EXPECT_EQ(dmutc_set(NULL,&fraction),-ERANGE); + DMOD_TEST_EXPECT_EQ(dmutc_set(&fraction),-ERANGE); saved.seconds+=(int64_t)((Dmod_GetUptime()-started)/1000); - DMOD_TEST_EXPECT_EQ(dmutc_set(NULL,&saved),0); - DMOD_TEST_EXPECT_EQ(dmutc_get(NULL,&now),0); + DMOD_TEST_EXPECT_EQ(dmutc_set(&saved),0); + DMOD_TEST_EXPECT_EQ(dmutc_get(&now),0); DMOD_TEST_EXPECT_TRUE(now.seconds>=saved.seconds && now.seconds<=saved.seconds+1); Dmod_Printf("UTC_SAMPLE seconds=%lld nanosecond=%u resolution_ns=%llu\n", (long long)now.seconds,(unsigned)now.nanosecond,(unsigned long long)status.resolution_ns); diff --git a/tests/rtc/rtc_test.c b/tests/rtc/rtc_test.c index 39169fa..4955ef9 100644 --- a/tests/rtc/rtc_test.c +++ b/tests/rtc/rtc_test.c @@ -1,11 +1,23 @@ +#define _POSIX_C_SOURCE 200809L #include "dmod.h" #include "dmutc.h" #include "dmdrvi_ioctl.h" #include #include #include +#include +#include +#include +#include "service.h" +int dmod_init(const Dmod_Config_t* config); +int dmod_deinit(void); +void* Dmod_Malloc(size_t size) { return malloc(size); } +void Dmod_Free(void* p) { free(p); } #include +static pthread_mutex_t gate_lock=PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t gate_changed=PTHREAD_COND_INITIALIZER; +static bool block_read, read_entered, release_read, stop_started, stop_done; static dmdrvi_rtc_time_t clock_time; static dmdrvi_rtc_status_t clock_status; static dmdrvi_rtc_info_t clock_info; @@ -15,11 +27,14 @@ static int failing_command, command_error; static char last_path[64]; static void reset(void) { + dmutc_stop_source("dmutc@model"); clock_time=(dmdrvi_rtc_time_t){2040,2,29,12,34,56,3,0,123456789}; clock_status=(dmdrvi_rtc_status_t){DMDRVI_RTC_STATUS_RUNNING | DMDRVI_RTC_STATUS_TIME_SET,0}; clock_info=(dmdrvi_rtc_info_t){1,DMDRVI_RTC_CAP_SET_TIME,1900,2199,0}; opens=closes=writes=reads=0; open_error=false; failing_command=command_error=0; + const char* argv[]={"dmutc","/devices/other-rtc"}; + assert(dmutc_start_source("dmutc@model",2,argv)==0); } void* Dmod_FileOpen(const char* path, const char* mode) { @@ -41,7 +56,14 @@ int Dmod_Ioctl(void* file,int command,void* arg) switch(command) { case DMDRVI_IOCTL_RTC_GET_INFO: *(dmdrvi_rtc_info_t*)arg=clock_info; return 0; case DMDRVI_IOCTL_RTC_GET_STATUS: *(dmdrvi_rtc_status_t*)arg=clock_status; return 0; - case DMDRVI_IOCTL_RTC_GET_TIME: ++reads; *(dmdrvi_rtc_time_t*)arg=clock_time; return 0; + case DMDRVI_IOCTL_RTC_GET_TIME: + if(block_read) { + pthread_mutex_lock(&gate_lock); + read_entered=true; pthread_cond_broadcast(&gate_changed); + while(!release_read)pthread_cond_wait(&gate_changed,&gate_lock); + pthread_mutex_unlock(&gate_lock); + } + ++reads; *(dmdrvi_rtc_time_t*)arg=clock_time; return 0; case DMDRVI_IOCTL_RTC_SET_TIME: { assert(write_handle); ++writes; dmdrvi_rtc_time_t* time=arg; @@ -58,8 +80,8 @@ static void assert_zero(const dmutc_time_t* t) static void reads_utc_without_stm32_range_or_precision_assumptions(void) { reset(); dmutc_time_t time; dmutc_calendar_t calendar; - assert(dmutc_get("/dev/other-rtc",&time)==0); - assert(strcmp(last_path,"/dev/other-rtc")==0); + assert(dmutc_get(&time)==0); + assert(strcmp(last_path,"/devices/other-rtc")==0); assert(time.seconds>INT32_MAX && time.nanosecond==123456789); assert(dmutc_to_calendar(&time,&calendar)==0); assert(calendar.year==2040 && calendar.month==2 && calendar.day==29); @@ -70,18 +92,18 @@ static void lost_unset_and_stopped_clocks_do_not_supply_utc(void) { reset(); dmutc_time_t time; clock_status.flags=DMDRVI_RTC_STATUS_RUNNING; - assert(dmutc_get(NULL,&time)==-ENODATA); assert_zero(&time); + assert(dmutc_get(&time)==-ENODATA); assert_zero(&time); clock_status.flags=DMDRVI_RTC_STATUS_TIME_SET; - assert(dmutc_get(NULL,&time)==-ENODATA); assert_zero(&time); + assert(dmutc_get(&time)==-ENODATA); assert_zero(&time); clock_status.flags=DMDRVI_RTC_STATUS_RUNNING|DMDRVI_RTC_STATUS_TIME_SET|DMDRVI_RTC_STATUS_TIME_LOST; // A non-advertised loss flag is not continuity detection. dmutc_status_t status; - assert(dmutc_status(NULL,&status)==0); + assert(dmutc_status(&status)==0); assert(!(status.flags&(DMUTC_CLOCK_LOSS_DETECTION|DMUTC_CLOCK_TIME_LOST))); - assert(dmutc_get(NULL,&time)==0); + assert(dmutc_get(&time)==0); clock_info.capabilities|=DMDRVI_RTC_CAP_LOSS_DETECTION; - assert(dmutc_get(NULL,&time)==-ENODATA); assert_zero(&time); - assert(dmutc_status(NULL,&status)==0); + assert(dmutc_get(&time)==-ENODATA); assert_zero(&time); + assert(dmutc_status(&status)==0); assert(status.flags&DMUTC_CLOCK_TIME_LOST); assert(opens==closes); } @@ -89,19 +111,19 @@ static void write_validation_and_driver_precision_errors(void) { reset(); dmutc_calendar_t calendar={2000,2,29,1,2,3,0,0,250000000}; dmutc_time_t time; assert(dmutc_from_calendar(&calendar,&time)==0); - assert(dmutc_set(NULL,&time)==0); - assert(strcmp(last_path,"/dev/rtc")==0 && clock_time.nanosecond==250000000 && writes==1); + assert(dmutc_set(&time)==0); + assert(strcmp(last_path,"/devices/other-rtc")==0 && clock_time.nanosecond==250000000 && writes==1); clock_info.resolution_ns=1000000000; - assert(dmutc_set(NULL,&time)==-ERANGE && clock_time.nanosecond==250000000); + assert(dmutc_set(&time)==-ERANGE && clock_time.nanosecond==250000000); clock_info.capabilities=0; - assert(dmutc_set(NULL,&time)==-ENOTSUP); + assert(dmutc_set(&time)==-ENOTSUP); clock_info.capabilities=DMDRVI_RTC_CAP_SET_TIME; calendar.year=1800; calendar.day=28; assert(dmutc_from_calendar(&calendar,&time)==0); unsigned before=writes; - assert(dmutc_set(NULL,&time)==-ERANGE && writes==before); + assert(dmutc_set(&time)==-ERANGE && writes==before); time.nanosecond=1000000000u; before=opens; - assert(dmutc_set(NULL,&time)==-EINVAL && opens==before); + assert(dmutc_set(&time)==-EINVAL && opens==before); assert(opens==closes); } static void errors_close_handles_and_clear_outputs(void) @@ -111,35 +133,112 @@ static void errors_close_handles_and_clear_outputs(void) for(unsigned i=0;i=1000000000){++deadline.tv_sec;deadline.tv_nsec-=1000000000;} + int rc=0; + while(!stop_done && rc==0)rc=pthread_cond_timedwait(&gate_changed,&gate_lock,&deadline); + assert(rc==ETIMEDOUT && !stop_done); + release_read=true; pthread_cond_broadcast(&gate_changed); + pthread_mutex_unlock(&gate_lock); + assert(pthread_join(reader,NULL)==0 && pthread_join(stopper,NULL)==0); + assert(reader_result==0 && stopper_result==0); + block_read=false; + dmutc_time_t now; assert(dmutc_get(&now)==-ENODEV); assert_zero(&now); assert(opens==closes); } int main(void) { + assert(dmod_init(NULL)==0); reads_utc_without_stm32_range_or_precision_assumptions(); lost_unset_and_stopped_clocks_do_not_supply_utc(); write_validation_and_driver_precision_errors(); errors_close_handles_and_clear_outputs(); - puts("UTC RTC provider: 4/4 scenarios passed"); + service_instances_select_and_remove_sources(); + stop_waits_for_io_and_does_not_restore_a_default_source(); + assert(dmod_deinit()==0); + puts("UTC RTC provider/service: 6/6 scenarios passed"); return 0; } diff --git a/tests/rtc/stubs/dmod.h b/tests/rtc/stubs/dmod.h index c4f48c0..c29846a 100644 --- a/tests/rtc/stubs/dmod.h +++ b/tests/rtc/stubs/dmod.h @@ -5,6 +5,8 @@ #include #include "dmutc_defs.h" typedef struct Dmod_Config Dmod_Config_t; +void* Dmod_Malloc(size_t size); +void Dmod_Free(void* p); void* Dmod_FileOpen(const char* path, const char* mode); void Dmod_FileClose(void* file); int Dmod_Ioctl(void* file, int command, void* arg); diff --git a/tests/rtc/stubs/dmosi.h b/tests/rtc/stubs/dmosi.h new file mode 100644 index 0000000..4d2b756 --- /dev/null +++ b/tests/rtc/stubs/dmosi.h @@ -0,0 +1,14 @@ +#ifndef TEST_DMOSI_H +#define TEST_DMOSI_H +#include +#include +#include +typedef pthread_mutex_t* dmosi_mutex_t; +static inline dmosi_mutex_t dmosi_mutex_create(bool recursive) { + (void)recursive; dmosi_mutex_t m=malloc(sizeof(*m)); + if(m!=NULL && pthread_mutex_init(m,NULL)!=0) {free(m);m=NULL;} return m; +} +static inline int dmosi_mutex_lock(dmosi_mutex_t m) {return -pthread_mutex_lock(m);} +static inline int dmosi_mutex_unlock(dmosi_mutex_t m) {return -pthread_mutex_unlock(m);} +static inline void dmosi_mutex_destroy(dmosi_mutex_t m) {pthread_mutex_destroy(m);free(m);} +#endif diff --git a/tests/rtc/stubs/libsystemd_service.h b/tests/rtc/stubs/libsystemd_service.h new file mode 100644 index 0000000..e3939ce --- /dev/null +++ b/tests/rtc/stubs/libsystemd_service.h @@ -0,0 +1,4 @@ +#ifndef TEST_LIBRARY_SERVICE_H +#define TEST_LIBRARY_SERVICE_H +#define dmod_libsystemd_dif_api_declaration(v,impl,ret,name,args) ret impl##name args +#endif