From 975b0d94600a876f4dde38c9a1280bfc24961dd1 Mon Sep 17 00:00:00 2001 From: w1ne <14119286+w1ne@users.noreply.github.com> Date: Sun, 5 Jul 2026 01:43:53 +0200 Subject: [PATCH] chore(iolink): bump device+master pins to V1.1.5 spec-conformant wire Bump third_party/iolinki submodule to cac724a and re-vendor the iolinki-master stack at 3d54109, both carrying the V1.1.5-conformant wire (startup probe MC 0xA2; OPERATE via MasterCommand 0x99 page-channel write; ISDU I-Service nibbles 0xB read / 0x3 write). Firmware wrappers drive the C stacks via their public APIs, so the station model needs no octet changes. Rebuilt all five STM32L476 ELFs and reran the multichip, iolink-dido, and service-coverage harnesses: startup->OPERATE, cyclic PD, ISDU, EVENT and data-storage all green. --- third_party/iolinki | 2 +- third_party/iolinki-master/.clang-format | 34 ++ third_party/iolinki-master/.editorconfig | 16 + .../iolinki-master/.github/workflows/ci.yml | 340 ++++++++++++++++++ .../.github/workflows/release.yml | 116 ++++++ third_party/iolinki-master/.gitignore | 17 + .../iolinki-master/.pre-commit-config.yaml | 47 +++ third_party/iolinki-master/CHANGELOG.md | 98 +++++ third_party/iolinki-master/CMakeLists.txt | 35 +- third_party/iolinki-master/Doxyfile | 32 ++ third_party/iolinki-master/README.md | 55 ++- third_party/iolinki-master/SECURITY.md | 66 ++++ third_party/iolinki-master/SOURCE_COMMIT | 2 +- third_party/iolinki-master/check_quality.sh | 123 +++++++ third_party/iolinki-master/docs/API.md | 233 ++++++++++++ .../iolinki-master/docs/ARCHITECTURE.md | 132 +++++++ .../iolinki-master/docs/CONTRIBUTING.md | 94 +++++ .../docs/IMPLEMENTATION_STATUS.md | 43 ++- .../iolinki-master/docs/MISRA_DEVIATIONS.md | 34 ++ third_party/iolinki-master/docs/PORTING.md | 129 +++++++ .../iolinki-master/docs/RELEASE_STRATEGY.md | 85 +++++ .../iolinki-master/docs/security/CRA.md | 67 ++++ .../docs/security/THREAT_MODEL.md | 239 ++++++++++++ .../plans/2026-06-17-iolink-master-isdu.md | 53 --- .../plans/2026-06-17-opaque-master-api.md | 52 --- .../2026-06-17-opaque-master-api-design.md | 31 -- .../examples/master_loopback_demo.c | 10 +- .../include/iolinki_master/master.h | 122 ++++++- .../samples/iolink_master/CMakeLists.txt | 9 + .../samples/iolink_master/prj.conf | 12 + .../samples/iolink_master/sample.yaml | 19 + .../samples/iolink_master/src/main.c | 208 +++++++++++ .../iolinki-master/src/master_controller.c | 2 +- .../iolinki-master/src/master_internal.h | 87 ++++- third_party/iolinki-master/src/master_isdu.c | 82 +++-- .../iolinki-master/src/master_parameters.c | 157 ++++++-- third_party/iolinki-master/src/master_port.c | 102 ++++-- .../iolinki-master/tests/fake_iolink_device.c | 54 ++- .../tests/test_master_fake_device.c | 94 +++++ .../iolinki-master/tests/test_master_isdu.c | 54 ++- .../tests/test_master_parameters.c | 224 ++++++++++++ .../tests/test_master_public_flow.c | 5 +- .../tests/test_master_startup.c | 71 +++- .../iolinki-master/tests/test_master_tick.c | 14 +- .../tools/generate_release_notes.py | 134 +++++++ .../iolinki-master/tools/generate_sbom.py | 182 ++++++++++ .../iolinki-master/tools/run-cppcheck.sh | 20 ++ .../tools/test_generate_sbom.py | 102 ++++++ third_party/iolinki-master/west.yml | 46 +++ .../iolinki-master/zephyr/CMakeLists.txt | 50 +++ third_party/iolinki-master/zephyr/Kconfig | 26 ++ third_party/iolinki-master/zephyr/module.yml | 6 + 52 files changed, 3804 insertions(+), 263 deletions(-) create mode 100644 third_party/iolinki-master/.clang-format create mode 100644 third_party/iolinki-master/.editorconfig create mode 100644 third_party/iolinki-master/.github/workflows/ci.yml create mode 100644 third_party/iolinki-master/.github/workflows/release.yml create mode 100644 third_party/iolinki-master/.pre-commit-config.yaml create mode 100644 third_party/iolinki-master/CHANGELOG.md create mode 100644 third_party/iolinki-master/Doxyfile create mode 100644 third_party/iolinki-master/SECURITY.md create mode 100755 third_party/iolinki-master/check_quality.sh create mode 100644 third_party/iolinki-master/docs/API.md create mode 100644 third_party/iolinki-master/docs/ARCHITECTURE.md create mode 100644 third_party/iolinki-master/docs/CONTRIBUTING.md create mode 100644 third_party/iolinki-master/docs/MISRA_DEVIATIONS.md create mode 100644 third_party/iolinki-master/docs/PORTING.md create mode 100644 third_party/iolinki-master/docs/RELEASE_STRATEGY.md create mode 100644 third_party/iolinki-master/docs/security/CRA.md create mode 100644 third_party/iolinki-master/docs/security/THREAT_MODEL.md delete mode 100644 third_party/iolinki-master/docs/superpowers/plans/2026-06-17-iolink-master-isdu.md delete mode 100644 third_party/iolinki-master/docs/superpowers/plans/2026-06-17-opaque-master-api.md delete mode 100644 third_party/iolinki-master/docs/superpowers/specs/2026-06-17-opaque-master-api-design.md create mode 100644 third_party/iolinki-master/samples/iolink_master/CMakeLists.txt create mode 100644 third_party/iolinki-master/samples/iolink_master/prj.conf create mode 100644 third_party/iolinki-master/samples/iolink_master/sample.yaml create mode 100644 third_party/iolinki-master/samples/iolink_master/src/main.c create mode 100755 third_party/iolinki-master/tools/generate_release_notes.py create mode 100755 third_party/iolinki-master/tools/generate_sbom.py create mode 100755 third_party/iolinki-master/tools/run-cppcheck.sh create mode 100755 third_party/iolinki-master/tools/test_generate_sbom.py create mode 100644 third_party/iolinki-master/west.yml create mode 100644 third_party/iolinki-master/zephyr/CMakeLists.txt create mode 100644 third_party/iolinki-master/zephyr/Kconfig create mode 100644 third_party/iolinki-master/zephyr/module.yml diff --git a/third_party/iolinki b/third_party/iolinki index 3712db1d7..cac724ad0 160000 --- a/third_party/iolinki +++ b/third_party/iolinki @@ -1 +1 @@ -Subproject commit 3712db1d7a9eed857a3701d467ef576892a2ae44 +Subproject commit cac724ad00de91b4516102b1de977aed32559b36 diff --git a/third_party/iolinki-master/.clang-format b/third_party/iolinki-master/.clang-format new file mode 100644 index 000000000..94873247a --- /dev/null +++ b/third_party/iolinki-master/.clang-format @@ -0,0 +1,34 @@ +# iolinki-master .clang-format +# Based on Google, customized for iolinki coding standards (matching the iolinki device stack) + +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 100 +AllowShortFunctionsOnASingleLine: None +KeepEmptyLinesAtTheStartOfBlocks: false + +# Bracing Style +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: Never + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: true + AfterStruct: true + AfterUnion: true + BeforeCatch: true + BeforeElse: true + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true + +# Spacing +SpaceAfterCStyleCast: true +SpaceBeforeRangeBasedForLoopColon: true +SpaceAroundPointerQualifiers: Both + +# Sorting +SortIncludes: Never diff --git a/third_party/iolinki-master/.editorconfig b/third_party/iolinki-master/.editorconfig new file mode 100644 index 000000000..362b50e1e --- /dev/null +++ b/third_party/iolinki-master/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{yml,yaml,json}] +indent_size = 2 + +[Makefile] +indent_style = tab +indent_size = 4 diff --git a/third_party/iolinki-master/.github/workflows/ci.yml b/third_party/iolinki-master/.github/workflows/ci.yml new file mode 100644 index 000000000..d439f53e6 --- /dev/null +++ b/third_party/iolinki-master/.github/workflows/ci.yml @@ -0,0 +1,340 @@ +name: iolinki-master CI + +on: + push: + branches: + - master + - main + pull_request: + branches: + - master + - main + workflow_dispatch: + +jobs: + ctest: + name: cmake-ctest + # Pluto's repo runner is named `pluto-labwired-ci-runner` and exposes these labels. + runs-on: [self-hosted, Linux, X64, ci] + steps: + - name: Checkout iolinki-master + uses: actions/checkout@v4 + with: + path: iolinki-master + + - name: Checkout iolinki device stack + uses: actions/checkout@v4 + with: + repository: w1ne/iolinki + ref: develop + path: iolinki-device + + - name: Configure device stack path + run: | + set -euo pipefail + dir="$GITHUB_WORKSPACE/iolinki-device" + test -d "$dir/include/iolinki" + test -f "$dir/src/frame.c" + test -f "$dir/src/device.c" + echo "IOLINKI_DEVICE_DIR=$dir" >> "$GITHUB_ENV" + echo "Using iolinki device stack at $dir" + + - name: Ensure test dependencies + timeout-minutes: 10 + run: | + set -euo pipefail + + toolbin="$RUNNER_TEMP/toolbin" + mkdir -p "$toolbin" + + if ! command -v cc >/dev/null 2>&1; then + if command -v clang >/dev/null 2>&1; then + printf '#!/usr/bin/env bash\nexec clang "$@"\n' > "$toolbin/cc" + chmod +x "$toolbin/cc" + export PATH="$toolbin:$PATH" + echo "$toolbin" >> "$GITHUB_PATH" + echo "Created cc shim to clang." + else + zig_bin="$HOME/.local/zig/zig" + + if [ -x "$zig_bin" ]; then + echo "Reusing existing Zig toolchain at $zig_bin." + else + for tool in curl tar sha256sum; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "::error::$tool is required to bootstrap a C compiler on the Pluto runner." + exit 1 + fi + done + + case "$(uname -m)" in + x86_64) + zig_pkg="zig-linux-x86_64-0.13.0" + zig_sha="d45312e61ebcc48032b77bc4cf7fd6915c11fa16e4aad116b66c9468211230ea" + ;; + aarch64|arm64) + zig_pkg="zig-linux-aarch64-0.13.0" + zig_sha="041ac42323837eb5624068acd8b00cd5777dac4cf91179e8dad7a7e90dd0c556" + ;; + *) + echo "::error::No C compiler is installed and Zig bootstrap is unsupported for $(uname -m)." + exit 1 + ;; + esac + + zig_dir="$RUNNER_TEMP/zig" + zig_archive="$RUNNER_TEMP/${zig_pkg}.tar.xz" + rm -rf "$zig_dir" "$zig_archive" + mkdir -p "$zig_dir" + curl --connect-timeout 20 --max-time 180 -fsSL \ + "https://ziglang.org/download/0.13.0/${zig_pkg}.tar.xz" \ + -o "$zig_archive" + echo "${zig_sha} ${zig_archive}" | sha256sum --check --strict + tar -xf "$zig_archive" -C "$zig_dir" --strip-components=1 + zig_bin="$zig_dir/zig" + echo "Downloaded Zig toolchain to $zig_dir." + fi + + printf '#!/usr/bin/env bash\nexec %q cc "$@"\n' "$zig_bin" > "$toolbin/cc" + printf '#!/usr/bin/env bash\nexec %q ar "$@"\n' "$zig_bin" > "$toolbin/ar" + printf '#!/usr/bin/env bash\nexec %q ranlib "$@"\n' "$zig_bin" > "$toolbin/ranlib" + chmod +x "$toolbin/cc" "$toolbin/ar" "$toolbin/ranlib" + export PATH="$toolbin:$PATH" + echo "$toolbin" >> "$GITHUB_PATH" + echo "Created cc/ar/ranlib shims to Zig." + fi + fi + + if ! command -v cmake >/dev/null 2>&1; then + for tool in curl tar; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "::error::$tool is required to bootstrap CMake on the Pluto runner." + exit 1 + fi + done + + case "$(uname -m)" in + x86_64) + cmake_pkg="cmake-3.30.9-linux-x86_64" + ;; + *) + echo "::error::CMake is not installed and bootstrap is unsupported for $(uname -m)." + exit 1 + ;; + esac + + cmake_dir="$RUNNER_TEMP/cmake" + cmake_archive="$RUNNER_TEMP/${cmake_pkg}.tar.gz" + rm -rf "$cmake_dir" "$cmake_archive" + mkdir -p "$cmake_dir" + curl --connect-timeout 20 --max-time 180 -fsSL \ + "https://cmake.org/files/v3.30/${cmake_pkg}.tar.gz" \ + -o "$cmake_archive" + tar -xf "$cmake_archive" -C "$cmake_dir" --strip-components=1 + export PATH="$cmake_dir/bin:$PATH" + echo "$cmake_dir/bin" >> "$GITHUB_PATH" + cmake --version + fi + + if ! command -v ninja >/dev/null 2>&1; then + ninja_archive="$RUNNER_TEMP/ninja-linux.zip" + rm -f "$ninja_archive" "$toolbin/ninja" + curl --connect-timeout 20 --max-time 180 -fL \ + https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-linux.zip \ + -o "$ninja_archive" + cmake -E tar xf "$ninja_archive" --format=zip + mv ninja "$toolbin/ninja" + chmod +x "$toolbin/ninja" + export PATH="$toolbin:$PATH" + echo "$toolbin" >> "$GITHUB_PATH" + ninja --version + fi + + export CMAKE_GENERATOR=Ninja + echo "CMAKE_GENERATOR=Ninja" >> "$GITHUB_ENV" + + if command -v cc >/dev/null 2>&1 \ + && command -v cmake >/dev/null 2>&1 \ + && command -v pkg-config >/dev/null 2>&1 \ + && pkg-config --exists cmocka; then + echo "CMake, C compiler, pkg-config, and CMocka are already available." + exit 0 + fi + + if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y build-essential cmake pkg-config libcmocka-dev + exit 0 + fi + + for tool in cc cmake ninja curl tar; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "::error::$tool is required on the Pluto runner." + exit 1 + fi + done + + prefix="$RUNNER_TEMP/cmocka" + src="$RUNNER_TEMP/cmocka-1.1.7" + archive="$RUNNER_TEMP/cmocka-1.1.7.tar.xz" + + if ! pkg-config --exists cmocka; then + rm -rf "$prefix" "$src" "$archive" + curl --connect-timeout 20 --max-time 180 -fsSL \ + https://cmocka.org/files/1.1/cmocka-1.1.7.tar.xz \ + -o "$archive" + tar -xf "$archive" -C "$RUNNER_TEMP" + cmake -S "$src" -B "$src/build" \ + -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DWITH_STATIC_LIB=ON \ + -DWITH_EXAMPLES=OFF \ + -DUNIT_TESTING=OFF + cmake --build "$src/build" --target install + fi + + echo "PKG_CONFIG_PATH=$prefix/lib/pkgconfig:$prefix/lib64/pkgconfig:${PKG_CONFIG_PATH:-}" >> "$GITHUB_ENV" + echo "CMAKE_PREFIX_PATH=$prefix:${CMAKE_PREFIX_PATH:-}" >> "$GITHUB_ENV" + + - name: Configure + run: | + cmake -S iolinki-master -B iolinki-master/build \ + -DIOLINKI_DEVICE_DIR="$IOLINKI_DEVICE_DIR" + + - name: Build + run: cmake --build iolinki-master/build + + - name: Test + run: ctest --test-dir iolinki-master/build --output-on-failure + + # Non-blocking: the Zephyr simulation build uses a multi-GB toolchain image and + # west-pulls Zephyr + the iolinki device stack, which can exhaust a free + # runner. It is intentionally NOT a required check and is allowed to fail + # without blocking merges (mirrors the iolinki device repo's zephyr-validation + # job). It builds the native_sim iolink_master sample to prove the Zephyr + # module integration. + zephyr-validation: + name: zephyr-validation + runs-on: ubuntu-latest + continue-on-error: true + container: + image: zephyrprojectrtos/zephyr-build:latest + options: --user root + env: + ZEPHYR_TOOLCHAIN_VARIANT: host + steps: + - name: Free up disk space + run: | + rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc \ + /usr/local/.ghcup /usr/local/share/boost \ + /usr/local/lib/node_modules /usr/lib/google-cloud-sdk || true + df -h + + - name: Checkout iolinki-master + uses: actions/checkout@v4 + with: + path: iolinki-master + + - name: Initialize west workspace + shell: bash + run: | + set -euo pipefail + # Use the master's own west.yml (self: iolinki-master) as the manifest, + # pulling Zephyr and the iolinki device stack alongside it. + west init -l iolinki-master + west update --narrow -o=--depth=1 + west zephyr-export + + - name: Build native_sim sample + shell: bash + run: | + set -euo pipefail + west build -p auto -b native_sim iolinki-master/samples/iolink_master + + sbom-tools: + name: sbom-tools + runs-on: ubuntu-latest + steps: + - name: Checkout iolinki-master + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + + - name: Run SBOM generator self-test + run: python3 tools/test_generate_sbom.py + + labwired-model: + name: labwired-real-firmware-model + runs-on: ubuntu-latest + steps: + - name: Checkout iolinki-master + uses: actions/checkout@v4 + with: + path: iolinki-master + + - name: Checkout LabWired core + uses: actions/checkout@v4 + with: + repository: w1ne/labwired-core + ref: main + path: labwired-core + submodules: recursive + + - name: Overlay current master stack into LabWired + run: | + set -euo pipefail + rm -rf labwired-core/third_party/iolinki-master + mkdir -p labwired-core/third_party/iolinki-master + tar -C iolinki-master \ + --exclude .git \ + --exclude build \ + -cf - . | tar -C labwired-core/third_party/iolinki-master -xf - + git -C labwired-core status --short -- third_party/iolinki-master + + - name: Install Rust + uses: dtolnay/rust-toolchain@1.95.0 + + - name: Cache LabWired dependencies + uses: Swatinem/rust-cache@v2 + with: + shared-key: iolinki-master-labwired-model + workspaces: | + labwired-core -> target + + - name: Install firmware toolchain + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends gcc-arm-none-eabi libnewlib-arm-none-eabi + + - name: Fetch STM32CubeL4 pack + uses: ./labwired-core/.github/actions/fetch-pack + with: + pack: stm32cubel4@v1.18.2 + + - name: Build real IO-Link station firmware + working-directory: labwired-core + run: examples/iolink-station/ci/build.sh + + - name: Run LabWired multiport model + working-directory: labwired-core + env: + CARGO_TERM_COLOR: never + run: | + set -o pipefail + examples/iolink-station/ci/test.sh 2>&1 | tee multiport-report.txt + + # The report is the tee'd test log (per-node prefixed console output) + # plus the exact firmware ELFs that ran in the model. + - name: Upload LabWired IO-Link artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: iolinki-master-labwired-model + path: | + labwired-core/multiport-report.txt + labwired-core/examples/iolink-station/master-fw/master.elf + labwired-core/examples/iolink-station/master-fw-4port/master.elf + labwired-core/examples/iolink-dido/firmware/iolink_dido.elf + if-no-files-found: warn diff --git a/third_party/iolinki-master/.github/workflows/release.yml b/third_party/iolinki-master/.github/workflows/release.yml new file mode 100644 index 000000000..cb9854b46 --- /dev/null +++ b/third_party/iolinki-master/.github/workflows/release.yml @@ -0,0 +1,116 @@ +name: Create Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + create-release: + runs-on: ubuntu-latest + + steps: + - name: Checkout iolinki-master + uses: actions/checkout@v4 + with: + fetch-depth: 0 + path: iolinki-master + + - name: Checkout iolinki device stack + uses: actions/checkout@v4 + with: + repository: w1ne/iolinki + ref: develop + path: iolinki-device + + - name: Set up build environment + run: | + sudo apt-get update + sudo apt-get install -y cmake build-essential libcmocka-dev lcov python3 + + - name: Extract version info + id: version + working-directory: iolinki-master + run: | + VERSION=${GITHUB_REF#refs/tags/v} + TAG=${GITHUB_REF#refs/tags/} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "tag=$TAG" >> $GITHUB_OUTPUT + + PREV_TAG=$(git describe --tags --abbrev=0 ${GITHUB_REF}^ 2>/dev/null || echo "") + if [ -z "$PREV_TAG" ]; then + PREV_TAG=$(git rev-list --max-parents=0 HEAD) + fi + echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT + + - name: Build with Coverage + run: | + cmake -S iolinki-master -B iolinki-master/build \ + -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON \ + -DIOLINKI_DEVICE_DIR="$GITHUB_WORKSPACE/iolinki-device" + cmake --build iolinki-master/build + + - name: Run Tests + working-directory: iolinki-master + run: | + cd build + ctest --output-on-failure | tee test_output.log + + PASSED=$(grep -oE '[0-9]+% tests passed' test_output.log | grep -oE '[0-9]+' || echo "0") + TOTAL=$(grep -oE 'out of [0-9]+' test_output.log | awk '{print $3}' | head -n1 || echo "0") + COUNT_PASSED=$(echo "$TOTAL * $PASSED / 100" | bc) + echo "{\"total\": $TOTAL, \"passed\": $COUNT_PASSED}" > ../test_stats.json + + - name: Generate Coverage Report + working-directory: iolinki-master + run: | + cd build + lcov --capture --directory . --output-file coverage.info --ignore-errors mismatch,gcov || true + lcov --remove coverage.info '/usr/*' '*/tests/*' '*/examples/*' '*/iolinki-device/*' \ + --output-file coverage.filtered.info --ignore-errors unused || true + lcov --list coverage.filtered.info > ../coverage_summary.txt 2>/dev/null \ + || echo "Coverage: N/A" > ../coverage_summary.txt + grep "lines\.\.\.\.\.\.:" ../coverage_summary.txt | awk '{print $2}' > ../coverage_percent.txt \ + || echo "N/A" > ../coverage_percent.txt + + - name: Generate Release Notes + working-directory: iolinki-master + run: | + python3 tools/generate_release_notes.py \ + "${{ steps.version.outputs.version }}" \ + "${{ steps.version.outputs.prev_tag }}" \ + "test_stats.json" \ + "coverage_percent.txt" > release_notes.md + cat release_notes.md + + - name: Generate SBOMs + working-directory: iolinki-master + run: | + export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) + python3 tools/test_generate_sbom.py + python3 tools/generate_sbom.py \ + --version "${{ steps.version.outputs.version }}" \ + --format cyclonedx \ + --output "iolinki-master-${{ steps.version.outputs.version }}.cdx.json" + python3 tools/generate_sbom.py \ + --version "${{ steps.version.outputs.version }}" \ + --format spdx \ + --output "iolinki-master-${{ steps.version.outputs.version }}.spdx.json" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ steps.version.outputs.tag }} + name: iolinki-master ${{ steps.version.outputs.version }} + body_path: iolinki-master/release_notes.md + draft: false + prerelease: false + files: | + iolinki-master/build/master_loopback_demo + iolinki-master/iolinki-master-${{ steps.version.outputs.version }}.cdx.json + iolinki-master/iolinki-master-${{ steps.version.outputs.version }}.spdx.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/third_party/iolinki-master/.gitignore b/third_party/iolinki-master/.gitignore index 4534dc81b..19d485036 100644 --- a/third_party/iolinki-master/.gitignore +++ b/third_party/iolinki-master/.gitignore @@ -6,3 +6,20 @@ CTestTestfile.cmake Makefile *.a *.o + +# Doxygen output +docs/api/ +doxygen.log + +# Release artifacts +*.cdx.json +*.spdx.json +coverage*.info +coverage*.txt + +# Python +__pycache__/ +*.pyc + +# Agent worktrees / scratch +.claude/ diff --git a/third_party/iolinki-master/.pre-commit-config.yaml b/third_party/iolinki-master/.pre-commit-config.yaml new file mode 100644 index 000000000..5e2f72e48 --- /dev/null +++ b/third_party/iolinki-master/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: no-commit-to-branch + args: [--branch, master, --branch, main] + + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v21.1.8 + hooks: + - id: clang-format + types_or: [c, header] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.0 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format + + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: v0.11.0.1 + hooks: + - id: shellcheck + + # detect-secrets is added once a `.secrets.baseline` is generated + # (`detect-secrets scan > .secrets.baseline`); see the device repo for the + # reference configuration. + + - repo: local + hooks: + - id: unit-tests + name: Run Unit Tests + entry: bash -c 'if command -v cmake >/dev/null 2>&1; then cmake -B build_test -DCMAKE_BUILD_TYPE=Debug >/dev/null 2>&1 && cmake --build build_test -j$(nproc) >/dev/null 2>&1 && (cd build_test && ctest --output-on-failure); else echo "CMake not found, skipping tests"; fi' + language: system + pass_filenames: false + always_run: false + files: '\.(c|h)$' + - id: cppcheck + name: cppcheck + entry: tools/run-cppcheck.sh + language: script + types_or: [c, header] diff --git a/third_party/iolinki-master/CHANGELOG.md b/third_party/iolinki-master/CHANGELOG.md new file mode 100644 index 000000000..2f517ef5d --- /dev/null +++ b/third_party/iolinki-master/CHANGELOG.md @@ -0,0 +1,98 @@ +# Changelog + +All notable changes to the `iolinki-master` project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed +- **Magic numbers extracted to named constants** (`src/master_internal.h`): the + retry budget, wake-up byte, frame buffer size, Direct Parameter Page 1 field + offsets, MinCycleTime / M-sequenceCapability / ProcessData-descriptor bit + fields, ISDU framing lengths, Data Storage record and event-entry sizes, and + the startup micro-sequence steps are now named rather than inline literals. +- **MISRA C:2012 pass**: fixed Rule 17.7 (ignored `memcpy`/`memset` returns now + `(void)`-cast), Rule 13.4 (assignment-in-`while` → explicit read + `break`), + and Rule 15.7 (all `if … else if` chains terminated). Remaining accepted + deviations are recorded in [`docs/MISRA_DEVIATIONS.md`](docs/MISRA_DEVIATIONS.md); + `check_quality.sh` now also finds the Debian x86_64 cppcheck MISRA addon path. + +### Fixed +- **ProcessData descriptor decode** (`master_parameters.c`): isolate the Length + field to bits 0-4 per Table B.6 so a device that sets the (legal) SIO bit, or + reports a sub-byte bit length, is decoded to the correct octet count. + +### Documented +- **Spec conformance audit** against Interface & System Spec V1.1.5 in + `docs/IMPLEMENTATION_STATUS.md`: field encodings are conformant; the + startup/OPERATE-transition octets and the ISDU I-Service nibble are known + co-designed deviations pending a coordinated master+device change. + +## [0.2.0] - 2026-07-04 + +Release-engineering and documentation parity with the sibling `iolinki` device +stack. No protocol behavior changes beyond one guarded cast. + +### Added +- **Security package**: STRIDE [threat model](docs/security/THREAT_MODEL.md) + aligned to the IO-Link Security Design and Development Guideline (Order No. + 10.512) and an EU Cyber Resilience Act overview ([CRA.md](docs/security/CRA.md)). +- **Per-release SBOMs**: `tools/generate_sbom.py` emits reproducible CycloneDX 1.6 + and SPDX 2.3 documents (self-tested by `tools/test_generate_sbom.py`, gated in + CI via the `sbom-tools` job) and attaches them to each tagged release. +- **Release automation**: `.github/workflows/release.yml` builds with coverage, + runs tests, generates notes, and publishes a GitHub Release with SBOMs on `v*`. +- **Local quality gate**: `check_quality.sh` (strict `-Werror -Wpedantic + -Wconversion -Wshadow` compile, cppcheck, opt-in MISRA C:2012, clang-format, + Doxygen zero-warning) and `tools/run-cppcheck.sh`. +- **Developer tooling**: `.pre-commit-config.yaml`, `ENABLE_COVERAGE` and + `IOLINK_MASTER_ENABLE_DOCS` CMake options, and a `Doxyfile`. +- **Documentation**: `docs/ARCHITECTURE.md`, `docs/API.md`, `docs/PORTING.md`, + `docs/CONTRIBUTING.md`, and `docs/RELEASE_STRATEGY.md`. + +### Fixed +- Explicit cast on the ISDU read out-length (`master_isdu.c`) to satisfy + `-Wconversion`; the value is already bounds-checked, so behavior is unchanged. + +## [0.1.0] - 2026-07-04 + +First tagged release. `iolinki-master` is a portable, heap-free IO-Link **master** +protocol library with a caller-owned public API, validated against a co-designed +simulated device and an on-wire firmware model — **not yet a conformant master for +real hardware** (see [`docs/IMPLEMENTATION_STATUS.md`](docs/IMPLEMENTATION_STATUS.md) +for the honest gap list). + +### Added +- **Port lifecycle and startup**: inactive/startup/preoperate/operate/error state + machine, wake-up + Type 0 idle + operate transition, fixed and auto-baudrate + (COM3→COM2→COM1) scan, and configurable per-baud wake-up retry. +- **Cyclic process data**: configured PD in/out for M-sequence Types 0, 1_1/1_2/1_V, + and 2_1/2_2/2_V, with RX accumulation, checksum handling, and retry tracking. +- **ISDU services**: read/write with segmentation, Data Storage read/write/restore + with readback verification, block parameterization (download/upload/store), and + ISDU readback verification. +- **Device identity**: Direct Parameter Page 1 parse/apply/validate, capability-driven + config selection, and `NO_CHECK`/`TYPE_COMP`/`IDENTICAL` inspection levels enforcing + VendorID/DeviceID at startup. +- **MasterCycleTime decoding**: the time-base/multiplier octet is decoded to 100us + units for validation and cycle pacing. +- **Master Command helpers**: R/W + communication-channel + address encode/decode. +- **Events**: event-code/detail read and ack, plus optional dispatch callbacks + (rising-edge event-pending notify and per-event handler). +- **Scheduler/timing**: monotonic 100us cycle pacing, response-deadline scheduling, + and cycle-slip / jitter / link-quality diagnostics. +- **Multi-port controller**: init/tick fan-out with per-port pacing and diagnostics. +- **SIO DI/DQ** modes and dynamic mode transitions. +- **PHY boundary**: hardware-independent protocol core with a documented PHY contract + and adapter hooks (checked mode/baudrate, RX flush, half-duplex TX/RX prepare). +- **Tooling and tests**: CMake build against the sibling `iolinki` frame/CRC helpers, + 16 CTest targets (CMocka), a fake-device harness, runnable 1-port and 4-port + examples, and CI (`cmake-ctest` + the on-wire `labwired-real-firmware-model`). +- **Project baseline**: dual-license model (GPLv3 + commercial), coordinated-disclosure + `SECURITY.md`, and `.clang-format` / `.editorconfig`. + +[Unreleased]: https://github.com/w1ne/iolinki-master/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/w1ne/iolinki-master/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/w1ne/iolinki-master/releases/tag/v0.1.0 diff --git a/third_party/iolinki-master/CMakeLists.txt b/third_party/iolinki-master/CMakeLists.txt index 33b3a37d3..ecffa3a12 100644 --- a/third_party/iolinki-master/CMakeLists.txt +++ b/third_party/iolinki-master/CMakeLists.txt @@ -1,15 +1,31 @@ cmake_minimum_required(VERSION 3.10) -project(iolinki_master VERSION 0.1.0 LANGUAGES C) +project(iolinki_master VERSION 0.2.0 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra") +# Code coverage instrumentation (used by the release workflow). +option(ENABLE_COVERAGE "Enable code coverage instrumentation" OFF) +if(ENABLE_COVERAGE) + if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang") + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} --coverage -fprofile-arcs -ftest-coverage") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage") + endif() +endif() + set(IOLINKI_DEVICE_DIR - "/home/andrii/projects/labwired/core/third_party/iolinki" + "${CMAKE_CURRENT_SOURCE_DIR}/../iolinki" CACHE PATH "Path to the local iolinki device repository") +if(NOT EXISTS "${IOLINKI_DEVICE_DIR}/src/frame.c") + message(FATAL_ERROR + "iolinki device stack not found at '${IOLINKI_DEVICE_DIR}'. " + "Clone it (git clone -b develop git@github.com:w1ne/iolinki.git) " + "next to this repository, or pass -DIOLINKI_DEVICE_DIR=/path/to/iolinki.") +endif() + option(BUILD_TESTING "Build unit tests" ON) option(BUILD_EXAMPLES "Build runnable examples" ON) @@ -37,6 +53,21 @@ if(BUILD_EXAMPLES) target_link_libraries(master_4port_controller_demo iolinki_master) endif() +# Optional Doxygen API documentation target (`cmake --build build --target doc`). +option(IOLINK_MASTER_ENABLE_DOCS "Enable Doxygen documentation target" OFF) +if(IOLINK_MASTER_ENABLE_DOCS) + find_package(Doxygen) + if(DOXYGEN_FOUND) + add_custom_target(doc + COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Generating API documentation with Doxygen" + VERBATIM) + else() + message(WARNING "Doxygen not found; documentation target disabled.") + endif() +endif() + if(BUILD_TESTING) enable_testing() add_subdirectory(tests) diff --git a/third_party/iolinki-master/Doxyfile b/third_party/iolinki-master/Doxyfile new file mode 100644 index 000000000..3f0fbbfcc --- /dev/null +++ b/third_party/iolinki-master/Doxyfile @@ -0,0 +1,32 @@ +# Doxygen Configuration for iolinki-master +PROJECT_NAME = "iolinki-master" +PROJECT_NUMBER = "0.2.0" +PROJECT_BRIEF = "Portable IO-Link Master Stack" +OUTPUT_DIRECTORY = docs/api +INPUT = include/iolinki_master src +RECURSIVE = YES +OPTIMIZE_OUTPUT_FOR_C = YES +EXTRACT_ALL = YES +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = NO +GENERATE_LATEX = NO +GENERATE_MAN = NO +GENERATE_RTF = NO + +# Graphviz Support +HAVE_DOT = NO +COLLABORATION_GRAPH = NO +INCLUDE_GRAPH = NO +INCLUDED_BY_GRAPH = NO +GRAPHICAL_HIERARCHY = NO + +# Warning settings +WARN_IF_UNDOCUMENTED = YES +WARN_AS_ERROR = NO +QUIET = NO + +# HTML Settings +GENERATE_HTML = YES +HTML_OUTPUT = html +HTML_DYNAMIC_SECTIONS = YES +DISABLE_INDEX = NO diff --git a/third_party/iolinki-master/README.md b/third_party/iolinki-master/README.md index 65f382fcb..ac41f4b74 100644 --- a/third_party/iolinki-master/README.md +++ b/third_party/iolinki-master/README.md @@ -1,23 +1,37 @@ # iolinki-master `iolinki-master` is a standalone IO-Link master stack. It is intentionally split -from the device-oriented `iolinki` repository and reuses only the narrow shared -pieces needed for CRC, frame handling, PHY contracts, and IO-Link constants. +from the device-oriented [`iolinki`](https://github.com/w1ne/iolinki) repository +and reuses only the narrow shared pieces needed for CRC, frame handling, PHY +contracts, and IO-Link constants. The master API is built around caller-owned opaque storage. Public users allocate `iolink_master_port_t` or `iolink_master_controller_t`; private state lives in `src/` and is not exposed through the public header. -Track implementation status and next work here: +> **Status: `v0.1.0` — protocol-core, simulation-validated.** The stack is +> exercised against a co-designed simulated device and an on-wire firmware model, +> not yet against real IO-Link silicon. It is **not** a conformant hardware master +> yet (open: wake-response baud detection, physical wake-pulse timing, official +> conformance). See [`docs/IMPLEMENTATION_STATUS.md`](docs/IMPLEMENTATION_STATUS.md) +> and [`CHANGELOG.md`](CHANGELOG.md). -- [`docs/IMPLEMENTATION_STATUS.md`](docs/IMPLEMENTATION_STATUS.md) -- [`docs/ROADMAP.md`](docs/ROADMAP.md) -- [`docs/TESTING.md`](docs/TESTING.md) +Documentation: -The default local dependency path is: +- [`docs/API.md`](docs/API.md) — public API tour and a compiling example +- [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) — layered design +- [`docs/PORTING.md`](docs/PORTING.md) — implement the PHY contract for a board +- [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) — build/test loop and quality gate +- [`docs/IMPLEMENTATION_STATUS.md`](docs/IMPLEMENTATION_STATUS.md) — honest status ledger +- [`docs/ROADMAP.md`](docs/ROADMAP.md), [`docs/TESTING.md`](docs/TESTING.md), + [`docs/RELEASE_STRATEGY.md`](docs/RELEASE_STRATEGY.md) + +The build needs a local checkout of the `iolinki` device repository for the +shared CRC/frame helpers (and, for tests, the real device stack). By default it +is expected as a sibling of this repository: ```sh -/home/andrii/projects/labwired/core/third_party/iolinki +git clone -b develop git@github.com:w1ne/iolinki.git ../iolinki ``` Build and test: @@ -48,9 +62,26 @@ To point at another local `iolinki` checkout: cmake -S . -B build -DIOLINKI_DEVICE_DIR=/path/to/iolinki ``` +## Related Projects + +- **[iolinki](https://github.com/w1ne/iolinki)** — the companion IO-Link + **device** stack. This repository builds against it for the shared + CRC/frame/PHY helpers, and CI runs both stacks against each other over a + simulated wire (real firmware, multi-port station) in LabWired. + +## Security + +This repository has its own coordinated-disclosure policy in +[`SECURITY.md`](SECURITY.md) — use GitHub private vulnerability reporting. It +ships a master-specific STRIDE [threat model](docs/security/THREAT_MODEL.md), a +[CRA overview](docs/security/CRA.md), and per-release SBOMs (CycloneDX 1.6 + +SPDX 2.3) attached to each tagged release. + ## License -`iolinki-master` follows the same licensing model as `iolinki`: GPLv3-or-later -for GPL-compatible use, with a separate commercial license available for -closed-source/proprietary products. See [`LICENSE`](LICENSE) and -[`LICENSE.COMMERCIAL`](LICENSE.COMMERCIAL). +`iolinki-master` follows the same licensing model as +[`iolinki`](https://github.com/w1ne/iolinki): dual-licensed under the **GPLv3** +(free, for open-source/GPLv3 use) and a **commercial license** (for +closed-source / proprietary products that cannot accept the GPLv3 copyleft). +Shipping a proprietary product? Email **andrii@shylenko.com**. See +[`LICENSE`](LICENSE) and [`LICENSE.COMMERCIAL`](LICENSE.COMMERCIAL). diff --git a/third_party/iolinki-master/SECURITY.md b/third_party/iolinki-master/SECURITY.md new file mode 100644 index 000000000..897f34d42 --- /dev/null +++ b/third_party/iolinki-master/SECURITY.md @@ -0,0 +1,66 @@ +# Security Policy + +`iolinki-master` is a dual-licensed IO-Link **master** stack intended for +integration into industrial products. Security is handled as an engineering +discipline here, not a checkbox: the stack ships with a public +[threat model](docs/security/THREAT_MODEL.md) (STRIDE, aligned to the IO-Link +Security Design and Development Guideline, Order No. 10.512), per-release +machine-readable SBOMs (from `0.2`), a [CRA overview](docs/security/CRA.md), and +the coordinated-disclosure process below. + +## Supported Versions + +| Version | Security fixes | +| ------- | -------------- | +| Latest tagged release + `master` | ✅ free of charge | +| Older releases | Under a commercial support agreement only | + +Security fixes are delivered as tagged releases with a changelog entry and, for +confirmed vulnerabilities, a GitHub security advisory. Commercial licensees can +contractually fix a support period per release — see the commercial +security-update terms. + +## Reporting a Vulnerability + +Please do **not** open a public issue for suspected vulnerabilities. + +1. **Preferred:** GitHub private vulnerability reporting — use *Report a + vulnerability* under the repository's **Security** tab. +2. Alternatively, contact the maintainer privately (contact details on the GitHub + profile). + +What to include: affected version/commit, the IO-Link frame, M-sequence, or ISDU +sequence (or code path) that triggers the issue, and impact as you understand it. +A proof of concept against the fake-device harness (`tests/fake_iolink_device.c`) +or the LabWired on-wire firmware model is ideal but not required. + +**Response targets:** + +- Acknowledgement within **72 hours**. +- Triage verdict (accepted / rejected / needs info) within **14 days**. +- Fix timeline agreed with the reporter at triage; critical issues in the frame, + M-sequence, or ISDU parsing paths are prioritized ahead of all other work. + +## Coordinated Disclosure + +We ask reporters to withhold public disclosure until a fixed release is available. +In return we commit to: keeping the reporter informed, crediting them in the +advisory and changelog (unless they prefer otherwise), and not taking legal action +against good-faith research performed against your own or simulated hardware. + +## CRA Readiness + +For products placed on the EU market, Regulation (EU) 2024/2847 (Cyber Resilience +Act) applies — its vulnerability-reporting obligations from September 2026, its +full obligations from December 2027. As the supplier of a commercially licensed +stack we maintain the corresponding internal process: confirmed actively exploited +vulnerabilities in the stack are handled under the CRA notification regime (early +warning within 24 hours, notification within 72 hours) and communicated to +commercial licensees so they can meet their own Article 14 duties. + +The stack has **zero third-party runtime dependencies** (it reuses only narrow +frame/CRC helper sources from the sibling `iolinki` checkout at build time). The +per-release SBOMs (CycloneDX 1.6 + SPDX 2.3) state this explicitly rather than by +omission. Integrating the stack does not transfer manufacturer obligations: device +makers remain responsible for their own conformity assessment, CE marking, and +reporting — see [`docs/security/CRA.md`](docs/security/CRA.md). diff --git a/third_party/iolinki-master/SOURCE_COMMIT b/third_party/iolinki-master/SOURCE_COMMIT index 56c6ea529..71a67b28f 100644 --- a/third_party/iolinki-master/SOURCE_COMMIT +++ b/third_party/iolinki-master/SOURCE_COMMIT @@ -1 +1 @@ -b90ddd6b3b7bd3efb6e560c53319daa6cc1180e1 +3d541094588d32ff63bda843295fee084a5cb6ab diff --git a/third_party/iolinki-master/check_quality.sh b/third_party/iolinki-master/check_quality.sh new file mode 100755 index 000000000..df875ef4c --- /dev/null +++ b/third_party/iolinki-master/check_quality.sh @@ -0,0 +1,123 @@ +#!/bin/bash +set -e + +echo "============================================" +echo "🔍 iolinki-master Code Quality & Safety Check" +echo "============================================" + +# 1. Compiler Warnings (Strict) +echo -e "\n[1/5] 🛡️ Verifying Compilation Warnings..." +BUILD_DIR="${IOLINKI_MASTER_BUILD_DIR:-build_quality}" +if [ -f "${BUILD_DIR}/CMakeCache.txt" ]; then + cached_src=$(grep -E '^CMAKE_HOME_DIRECTORY:INTERNAL=' "${BUILD_DIR}/CMakeCache.txt" | cut -d= -f2-) + if [ -n "${cached_src}" ] && [ "${cached_src}" != "$(pwd)" ]; then + rm -rf "${BUILD_DIR}" + fi +fi +mkdir -p "${BUILD_DIR}" +cd "${BUILD_DIR}" +cmake .. -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_FLAGS="-Wall -Wextra -Werror -Wpedantic -Wconversion -Wshadow" +if make -j"$(nproc)"; then + echo " ✅ Strict Compilation Passed" +else + echo " ❌ Strict Compilation FAILED" + exit 1 +fi +cd .. + +# 2. Static Analysis (Cppcheck) +echo -e "\n[2/5] 🧹 Running Static Analysis (Cppcheck)..." +if command -v cppcheck &> /dev/null; then + # variableScope suppressed: this stack declares locals at function top by + # convention; all other warning/style/performance/portability checks stay on. + if cppcheck --enable=warning,style,performance,portability \ + --error-exitcode=1 \ + --suppress=missingIncludeSystem \ + --suppress=unusedFunction \ + --suppress=variableScope \ + --inline-suppr \ + --quiet \ + -I include \ + src/ examples/; then + echo " ✅ Static Analysis Passed" + else + echo " ❌ Static Analysis FAILED" + exit 1 + fi +else + echo " ⚠️ Cppcheck not installed. Skipping static analysis." + echo " Install with: sudo apt-get install cppcheck" +fi + +# 3. MISRA C:2012 Check (Cppcheck addon) +echo -e "\n[3/5] 📏 Running MISRA C:2012 Check..." +if command -v cppcheck &> /dev/null; then + MISRA_ADDON="" + for path in /usr/share/cppcheck/addons/misra.py \ + /usr/lib/cppcheck/addons/misra.py \ + /usr/lib/x86_64-linux-gnu/cppcheck/addons/misra.py; do + if [ -f "$path" ]; then + MISRA_ADDON="$path" + break + fi + done + + if [ -n "$MISRA_ADDON" ]; then + cppcheck --addon=misra \ + --enable=warning,style,performance,portability \ + --error-exitcode=1 \ + --suppress=missingIncludeSystem \ + --suppress=unusedFunction \ + --suppress=variableScope \ + --inline-suppr \ + --quiet \ + -I include \ + src/ examples/ + echo " ✅ MISRA Check Passed" + else + if [ "${IOLINKI_MASTER_MISRA_ENFORCE}" = "1" ]; then + echo " ❌ MISRA addon not available in cppcheck. Enforced mode fails." + exit 1 + else + echo " ⚠️ Cppcheck MISRA addon not available. Skipping MISRA checks." + fi + fi +else + echo " ⚠️ Cppcheck not installed. Skipping MISRA checks." +fi + +# 4. Code Formatting Check +echo -e "\n[4/5] 🎨 Checking Code Formatting..." +if command -v clang-format &> /dev/null; then + if find src include tests examples -type f \( -name "*.c" -o -name "*.h" \) -print0 \ + | xargs -0 clang-format --dry-run --Werror; then + echo " ✅ Code Formatting Passed" + else + echo " ❌ Code Formatting FAILED" + exit 1 + fi +else + echo " ⚠️ clang-format not installed. Skipping check." +fi + +# 5. Doxygen Warning Check +echo -e "\n[5/5] 📚 Checking Doxygen Warnings..." +if command -v doxygen &> /dev/null; then + doxygen Doxyfile > /dev/null 2> doxygen.log + if grep -q "warning:" doxygen.log; then + echo " ❌ Doxygen warnings found:" + grep "warning:" doxygen.log + exit 1 + else + echo " ✅ Doxygen Check Passed" + rm -f doxygen.log + fi +else + echo " ⚠️ Doxygen not installed. Skipping check." +fi + +echo -e "\n============================================" +echo "✅ Code Quality Checks Completed" +echo "============================================" +exit 0 diff --git a/third_party/iolinki-master/docs/API.md b/third_party/iolinki-master/docs/API.md new file mode 100644 index 000000000..289cd9604 --- /dev/null +++ b/third_party/iolinki-master/docs/API.md @@ -0,0 +1,233 @@ +# iolinki-master API Reference + +A guided tour of the public API in +[`include/iolinki_master/master.h`](../include/iolinki_master/master.h). Every +function returns a named result constant — `IOLINK_MASTER_STATUS_OK` (0), +`IOLINK_MASTER_STATUS_PENDING` (1), or a negative `IOLINK_MASTER_ERR_*` / +`..._ISDU_ERR_*` / `..._SIO_ERR_*` / `..._PARAM_ERR_*` code. Check every return. + +All state is caller-owned: you allocate `iolink_master_port_t` (a fixed-size opaque +union, 1280 B) or `iolink_master_controller_t` (32 B). There is no heap. + +## 1. Configuration and init + +Fill an `iolink_master_config_t`, then init a port against a PHY: + +```c +int iolink_master_init(iolink_master_port_t* port, + const iolink_phy_api_t* phy, + const iolink_master_config_t* config); +``` + +Key config fields (`iolink_master_config_t`): `port_mode` +(`IOLINK_MASTER_PORT_MODE_IOLINK` / `_DI` / `_DQ` / `_DEACTIVATED`), `m_seq_type`, +`baudrate`, `min_cycle_time`, `pd_in_len` / `pd_out_len`, `auto_baudrate`, +`response_timeout_100us`, `wake_retry_limit`, the identity fields +(`validate_device_info`, `inspection_level`, `expected_vendor_id`, +`expected_device_id`), the event callbacks (§6), and the PHY adapter hooks +(`set_mode_checked`, `set_baudrate_checked`, `flush_rx`, `prepare_tx`, +`prepare_rx`, `read_cq_line` / `read_cq_line_checked`, `wake_up` — see +[`PORTING.md`](PORTING.md)). + +> **Lifetime:** the config is copied into the port, but the PHY is retained **by +> pointer**. The `iolink_phy_api_t` must outlive the port — never pass a stack +> temporary. + +Related: `iolink_master_validate_phy_contract(phy, config)` checks the PHY/config +pair is complete for real hardware; `iolink_master_restart(port)` restarts startup; +`iolink_master_get_state(port)` returns the current `iolink_master_state_t`. + +## 2. The tick / scheduler model + +The core owns no clock. You drive it and supply time. + +- `iolink_master_process(port)` — send one pending startup/preoperate/operate action. +- `iolink_master_poll_rx(port)` — decode available RX bytes; returns the decoded + frame count, `OK` when no byte is available, or `INVALID_ARG`/`ERR_FRAME`/`ERR_CHECKSUM`. +- `iolink_master_tick(port, response_timeout)` — bool-flag tick. +- `iolink_master_tick_event(port, event)` — explicit event tick + (`IOLINK_MASTER_TICK_NONE` / `_CYCLE_DUE` / `_RESPONSE_TIMEOUT`). +- `iolink_master_tick_at(port, event, now_100us)` — as above, applying monotonic + 100µs `min_cycle_time` pacing. +- `iolink_master_get_next_tick_time(port, now_100us, &out_next_100us)` — when the + port is next due, for your hardware timer. +- `iolink_master_on_timeout(port)` — advance the retry policy on a response timeout; + returns `OK`, `PENDING` while retrying, or `ERR_RETRY_LIMIT`. +- `iolink_master_get_timing(port, &timing)` — read-only scheduler snapshot. + +`response_timeout_100us` controls the response deadline; `min_cycle_time` controls +cycle spacing (a zero response timeout falls back to `min_cycle_time`). + +## 3. Process data + +```c +int iolink_master_set_pd_out(iolink_master_port_t* port, const uint8_t* data, uint8_t len); +int iolink_master_get_pd_in(const iolink_master_port_t* port, + uint8_t* buffer, uint8_t buffer_len, uint8_t* out_len); +int iolink_master_get_od_status(const iolink_master_port_t* port, uint8_t* status); +``` + +`set_pd_out` returns `ERR_BUFFER_TOO_SMALL` if `len` does not match the configured +PD-out size. `get_pd_in` returns `PENDING` until valid PD has arrived. + +## 4. Device identity and Direct Parameter Page 1 + +- `iolink_master_parse_direct_parameter_page1(page, len, &info)` — decode a raw + page-1 buffer into `iolink_master_device_info_t`. +- `iolink_master_apply_direct_parameter_page1(port, page, len)` — parse and store. +- `iolink_master_get_device_info(port, &info)` / `iolink_master_read_device_info(port)`. +- `iolink_master_validate_device_info(port)` — VendorID/DeviceID vs configured + expectations at the selected `inspection_level` (`NO_CHECK` / `TYPE_COMP` / + `IDENTICAL`; the SerialNumber leg of `IDENTICAL` is not yet wired). +- `iolink_master_select_config_from_device_info(&info, &config)` and + `iolink_master_validate_config_against_device_info(&info, &config)`. +- `iolink_master_decode_min_cycle_time_100us(octet)` — MasterCycleTime octet →100µs. + +## 5. ISDU and services + +All service calls are non-blocking state machines: they return `OK` when complete, +`PENDING` while active, `INVALID_ARG`, or a domain error. + +```c +int iolink_master_read_isdu (port, index, subindex, data, &len); /* len: in=cap, out=actual */ +int iolink_master_write_isdu(port, index, subindex, data, len); +int iolink_master_verify_isdu(port, index, subindex, expected, len); +``` + +Data Storage: `iolink_master_read_data_storage`, `..._write_data_storage`, +`..._restore_data_storage`, `..._verify_data_storage`. Block parameterization: +`iolink_master_begin_parameter_download` / `..._end_parameter_download` / +`..._begin_parameter_upload` / `..._end_parameter_upload` / +`..._store_parameter_download` / `..._write_parameter_block`. Status: +`iolink_master_read_detailed_device_status`. ISDU errors use +`IOLINK_MASTER_ISDU_ERR_*` (`BUFFER_TOO_SMALL`, `BUSY`, `DEVICE`, `INVALID_STATE`, +`VERIFY_FAILED`). + +## 6. Events + +Poll model: read `iolink_master_diagnostics_t.event_pending`, then: + +```c +int iolink_master_read_event_code(port, &event_code); +int iolink_master_ack_event(port, &event_code); /* read == explicit ack */ +int iolink_master_read_event_details(port, events, max_events, &out_count); +``` + +`read_event_details` writes at most `max_events` `iolink_master_event_t` +(`{qualifier, type, code}`) and returns `BUFFER_TOO_SMALL` rather than overrunning. + +Dispatch model (optional): set `event_pending_handler` and/or `event_handler` in the +config (with `event_user` passed through). `event_pending_handler` fires on the +rising edge of the OD Event flag during a cyclic response; `event_handler` fires +once per decoded event. Both NULL keeps poll-only behavior. + +## 7. Diagnostics + +```c +int iolink_master_get_diagnostics(const iolink_master_port_t* port, + iolink_master_diagnostics_t* diagnostics); +uint8_t iolink_master_get_device_status(const iolink_master_port_t* port); +``` + +`iolink_master_diagnostics_t` carries `od_status`, `event_pending`, +`rx_retry_count`, `checksum_errors`, `send_errors`, `response_timeouts`, +`cycle_slips`, last/max cycle jitter (100µs), `supply_voltage_mv`, `short_circuit` +(sampled from PHY hooks when present), `link_quality_percent`, `last_service_result`, +`last_event_count`/`last_event_code`, and `last_isdu_error`. + +## 8. SIO DI/DQ + +```c +int iolink_master_set_dq(iolink_master_port_t* port, bool level); /* DQ mode */ +int iolink_master_get_di(const iolink_master_port_t* port, bool* level); /* DI mode */ +int iolink_master_set_port_mode(iolink_master_port_t* port, iolink_master_port_mode_t mode); +``` + +Wrong-mode / unsupported-PHY calls return `IOLINK_MASTER_SIO_ERR_WRONG_MODE` / +`..._UNSUPPORTED_PHY`. Switching to IO-Link mode restarts startup on the port. + +## 9. Master Command helpers + +`iolink_master_encode_master_command(read, channel, address)` composes a Master +Command octet; `iolink_master_mc_is_read` / `..._mc_channel` / `..._mc_address` +decode one. Channels: `IOLINK_MASTER_MC_CHANNEL_PROCESS` / `_PAGE` / `_DIAGNOSIS` / +`_ISDU`. + +## 10. Multi-port controller + +```c +int iolink_master_controller_init(controller, ports, port_count, phys, configs); +int iolink_master_controller_tick(controller, response_timeouts); /* bool[] */ +int iolink_master_controller_tick_events(controller, events); /* tick_event[] */ +int iolink_master_controller_tick_at(controller, now_100us); +int iolink_master_controller_get_port_count(controller, &out_count); +int iolink_master_controller_get_port(controller, index, &out_port); +int iolink_master_controller_get_next_tick_time(controller, now_100us, &out_next_100us); +``` + +Same lifetime contract as `iolink_master_init`: the `phys` array (and the PHYs it +points to) and the `ports` array must outlive the controller. A failing port +returns the first negative result without corrupting its siblings. + +## Minimal example + +Drives one port from startup into OPERATE and reads back process data. Compiles +against the real API (mirrors `examples/master_loopback_demo.c`; the PHY here is a +trivial stub — a real PHY talks to a transceiver, see [`PORTING.md`](PORTING.md)). + +```c +#include +#include "iolinki/phy.h" +#include "iolinki_master/master.h" + +/* A real PHY drives a transceiver; these stubs just satisfy the contract. */ +static int phy_init(void* u) { (void)u; return 0; } +static void phy_set_mode(void* u, iolink_phy_mode_t m) { (void)u; (void)m; } +static void phy_set_baud(void* u, iolink_baudrate_t b) { (void)u; (void)b; } +static int phy_send(void* u, const uint8_t* d, size_t n){ (void)u; (void)d; return (int)n; } +static int phy_recv(void* u, uint8_t* b) { (void)u; (void)b; return 0; } + +static const iolink_phy_api_t phy = { + .init = phy_init, .set_mode = phy_set_mode, + .set_baudrate = phy_set_baud, .send = phy_send, .recv_byte = phy_recv, +}; + +int main(void) +{ + iolink_master_port_t port; /* caller-owned, no heap */ + iolink_master_config_t config = { + .port_mode = IOLINK_MASTER_PORT_MODE_IOLINK, + .m_seq_type = IOLINK_MASTER_M_SEQ_TYPE_2_1, + .baudrate = IOLINK_BAUDRATE_COM3, + .min_cycle_time = 20U, + .pd_in_len = 1U, + .pd_out_len = 1U, + .auto_baudrate = false, + }; + uint8_t pd_out[1] = { 0x11U }; + uint8_t pd_in[1] = { 0U }; + uint8_t pd_in_len = sizeof(pd_in); + + if (iolink_master_init(&port, &phy, &config) != IOLINK_MASTER_STATUS_OK) { + return 1; + } + if (iolink_master_set_pd_out(&port, pd_out, sizeof(pd_out)) != IOLINK_MASTER_STATUS_OK) { + return 1; + } + + /* Drive the port: process() sends, poll_rx() decodes responses. A real + * integration paces these from a 100us timer via iolink_master_tick_at(). */ + iolink_master_process(&port); + (void)iolink_master_poll_rx(&port); + + if (iolink_master_get_pd_in(&port, pd_in, sizeof(pd_in), &pd_in_len) + == IOLINK_MASTER_STATUS_OK) { + printf("PD in: 0x%02X\n", pd_in[0]); + } + return 0; +} +``` + +Build it against the master library (which links the sibling `iolinki` frame/CRC +helpers) — see [`CONTRIBUTING.md`](CONTRIBUTING.md) and the runnable +`examples/master_loopback_demo.c` / `examples/master_4port_controller_demo.c`. diff --git a/third_party/iolinki-master/docs/ARCHITECTURE.md b/third_party/iolinki-master/docs/ARCHITECTURE.md new file mode 100644 index 000000000..bf03e9b75 --- /dev/null +++ b/third_party/iolinki-master/docs/ARCHITECTURE.md @@ -0,0 +1,132 @@ +# iolinki-master Architecture + +`iolinki-master` is a standalone, hardware-independent IO-Link **master** protocol +library. The design goal is a portable embedded master core with clean boundaries: +no heap, caller-owned storage, a public API that hides private state, and no +coupling to the device-stack singleton. This document describes the layering. It +does **not** restate the build order — that is [`ROADMAP.md`](ROADMAP.md) — and it +cross-references the PHY contract in [`PHY_BOUNDARY.md`](PHY_BOUNDARY.md) rather +than duplicating it. + +## 1. Repository and shared-helper boundary + +The master is a *sibling* of the device stack, not a fork of it. It compiles only +the narrow shared helper sources it needs from a local `iolinki` checkout — `crc.c` +and `frame.c` (CRC6/checksum and frame encode/decode) — into its own build via +`CMakeLists.txt`. It must never `add_subdirectory()` the full device stack or call +device singleton entry points (`iolink_init()`, `iolink_process()`). The device +checkout is located through `-DIOLINKI_DEVICE_DIR` and defaults to a sibling +`../iolinki`. + +## 2. Public API vs private state + +The central architectural move: **callers own opaque storage; private state lives +under `src/`.** + +- Public users allocate `iolink_master_port_t` (per port) or + `iolink_master_controller_t` (per multi-port controller). These are unions sized + by audited budgets — `IOLINK_MASTER_PORT_STORAGE_SIZE` (1280 B) and + `IOLINK_MASTER_CONTROLLER_STORAGE_SIZE` (32 B) — giving embedded integrators a + fixed, auditable RAM ceiling and keeping the ABI heap-free and caller-owned. +- The real layout lives in `src/master_internal.h` and is reached only through + `iolink_master_port_state()` inside `src/master_*.c`; the public header + (`include/iolinki_master/master.h`) exposes none of it. +- Every public function has a documented return contract expressed as named result + codes (`IOLINK_MASTER_STATUS_OK`, `IOLINK_MASTER_STATUS_PENDING`, + `IOLINK_MASTER_ERR_*`, and the per-domain `..._ISDU_ERR_*` / `..._SIO_ERR_*` / + `..._PARAM_ERR_*` enums), never bare magic integers. + +This boundary matters because every later feature becomes harder to change once +external users compile against the storage sizes and result codes. + +## 3. Protocol core + +`src/master_port.c` is the core engine. It owns: + +- **Port lifecycle**: `INACTIVE → STARTUP → PREOPERATE → OPERATE`, plus `ERROR` + (`iolink_master_state_t`), driven by `iolink_master_process` / + `iolink_master_poll_rx`. +- **Startup**: wake-up request, Type-0 idle exchange, operate transition command, + and OPERATE entry; fixed baudrate or auto-baud scan across COM3→COM2→COM1 with a + configurable per-baud `wake_retry_limit`. +- **Cyclic process data**: configured PD in/out for M-sequence Types 0, 1_1/1_2/1_V, + 2_1/2_2/2_V (`iolink_master_m_seq_type_t`), exposed via `iolink_master_set_pd_out` + / `iolink_master_get_pd_in` / `iolink_master_get_od_status`. +- **RX/retry**: byte accumulation, checksum/CRC verification, bounded retry, and the + diagnostics counters (`checksum_errors`, `response_timeouts`, `cycle_slips`, + jitter, derived link quality). + +Frame encode/decode and CRC come from the shared `../iolinki` helpers; the core adds +master-side sequencing on top. + +## 4. Timing and scheduler + +The core does **not** own a clock — the caller does. Timing is expressed as explicit +monotonic 100µs inputs so it is testable without wall-clock sleeps: + +- `iolink_master_tick` / `iolink_master_tick_event` take an explicit tick event + (`IOLINK_MASTER_TICK_CYCLE_DUE`, `..._RESPONSE_TIMEOUT`, `..._NONE`). +- `iolink_master_tick_at(port, event, now_100us)` applies `min_cycle_time` pacing + and response-deadline scheduling against caller-supplied time. +- `iolink_master_get_next_tick_time(port, now_100us, out_next_100us)` tells the + caller-owned hardware timer when the port is next due. +- `iolink_master_get_timing` exposes a read-only snapshot of scheduler-visible state + (`iolink_master_timing_t`). + +Response timeout (`response_timeout_100us`) is kept distinct from cycle spacing +(`min_cycle_time`); a zero response timeout falls back to `min_cycle_time`. The +MasterCycleTime octet (time-base + multiplier) is decoded to 100µs units by +`iolink_master_decode_min_cycle_time_100us` for both validation and pacing. This is +the current architecture-priority layer — see [`ROADMAP.md`](ROADMAP.md) §4. + +## 5. PHY adapter boundary + +The core is board-agnostic. Board support enters only through `iolink_phy_api_t` +(shared PHY struct: `init`/`set_mode`/`set_baudrate`/`send`/`recv_byte`, plus +optional `set_cq_line`/`detect_wakeup`/`get_voltage_mv`/`is_short_circuit`) and the +fallible adapter hooks carried in `iolink_master_config_t` +(`set_mode_checked`, `set_baudrate_checked`, `flush_rx`, `prepare_tx`, `prepare_rx`, +`read_cq_line` / `read_cq_line_checked`, `wake_up`). The PHY is retained **by +pointer** and must outlive the port. `iolink_master_validate_phy_contract` checks +that a PHY/config pair is complete enough for real hardware. The physical 80µs WURQ +pulse and `t_WU`/`t_REN`/`TDMT` startup timing are the adapter's responsibility and +are unverified on silicon. Full contract: [`PHY_BOUNDARY.md`](PHY_BOUNDARY.md); +implementing an adapter: [`PORTING.md`](PORTING.md). + +## 6. Controller / multi-port runtime + +`src/master_controller.c` initializes and drives an array of ports. It fans one +controller timestamp out to per-port cycle pacing +(`iolink_master_controller_tick_at`), applies per-port response deadlines, supports +independent per-port modes/timings, and exposes port count and per-port access +(`iolink_master_controller_get_port_count` / `..._get_port`). A failing port +returns its result without corrupting siblings. The controller computes the earliest +next due time across ports (`iolink_master_controller_get_next_tick_time`). It is a +fan-out helper, not yet a full port-policy scheduler. + +## 7. Services layer + +Services sit **above** cyclic transport in `src/master_isdu.c` (with identity in +`src/master_parameters.c`), kept independent of startup policy: + +- **ISDU** read/write with segmentation into fixed buffers. +- **Direct Parameter Page 1** parse/apply/get/validate and capability-driven config + selection (`src/master_parameters.c`), plus VendorID/DeviceID inspection. +- **Events**: code/detail read and ack, with optional rising-edge event-pending and + per-event dispatch callbacks. +- **Data Storage**: read/write/restore with readback verification. +- **Block parameterization**: download/upload/store system commands and a + write-with-readback sequence. +- **SIO DI/DQ** and dynamic mode transitions in `src/master_sio.c`. + +## 8. Design principles + +- **No dynamic memory** anywhere in `src/` or `include/` — all state is in + caller-owned fixed storage. +- **No board headers in `src/master_*.c`**; no sleeping inside core calls. +- **Fixed-width types** (``) throughout for portability. +- **Named result codes**, checked at every call site. + +Note on maturity: the core is protocol- and simulation-validated only. It is not +timing-certified, hardware-validated, or IO-Link-conformance validated — see +[`IMPLEMENTATION_STATUS.md`](IMPLEMENTATION_STATUS.md). diff --git a/third_party/iolinki-master/docs/CONTRIBUTING.md b/third_party/iolinki-master/docs/CONTRIBUTING.md new file mode 100644 index 000000000..27480901a --- /dev/null +++ b/third_party/iolinki-master/docs/CONTRIBUTING.md @@ -0,0 +1,94 @@ +# Contributing to iolinki-master + +## Getting started + +`iolinki-master` builds against a **sibling checkout** of the `iolinki` device +repository for the shared CRC/frame helpers (and, for the real-device test, the +device stack sources). Clone it next to this repo: + +```bash +git clone git@github.com:w1ne/iolinki-master.git +git clone -b develop git@github.com:w1ne/iolinki.git # sibling ../iolinki +cd iolinki-master +``` + +Point elsewhere with `-DIOLINKI_DEVICE_DIR=/path/to/iolinki` if it is not a sibling. + +You need CMake (≥ 3.10), a C99 compiler, and CMocka for the tests. + +## Build / test loop + +The canonical loop — run it before every commit: + +```bash +cmake -S . -B build +cmake --build build +ctest --test-dir build --output-on-failure +git diff --check +``` + +`ctest` exercises the CMocka unit/protocol suites, the fake-device harness, the +real-device in-memory harness, and the runnable examples (see +[`TESTING.md`](TESTING.md) for the target list). Passing local tests means the +behavior is *locally* verified — not hardware-tested or conformance-validated. Keep +that distinction in [`IMPLEMENTATION_STATUS.md`](IMPLEMENTATION_STATUS.md) honest. + +## Quality gate + +Run the strict gate before opening a PR: + +```bash +./check_quality.sh +``` + +It builds with warnings-as-errors (`-Wall -Wextra -Werror` and friends) and runs +static analysis on top of the test suite. CI runs the equivalent on every push and +PR: the `cmake-ctest` job (which fetches the `iolinki` device stack and builds + +tests the master) and the `labwired-real-firmware-model` job (which runs this master +against the real device firmware over an on-wire model). Both must be green. + +Install the pre-commit hooks so formatting and hygiene are checked locally: + +```bash +pre-commit install +``` + +The hooks cover trailing whitespace / EOF, YAML and large-file checks, and +`clang-format`, and block direct commits to protected branches. + +## Style + +- **Portable Standard C (C99).** No OS-specific calls in `src/`; no board headers in + `src/master_*.c` (board code goes through the PHY boundary — see + [`PHY_BOUNDARY.md`](PHY_BOUNDARY.md)). +- **No dynamic memory.** No `malloc`/`calloc`/`free` anywhere in `src/` or + `include/`; all state is caller-owned opaque storage. +- **Fixed-width types** from ``. **No globals.** **Checked inputs.** +- **No compiler warnings.** Formatting is enforced by `.clang-format`; do not + hand-format against it. +- **Named result codes.** Return the `IOLINK_MASTER_*` constants, never bare magic + integers, and document the return contract of every public function in the header. + +## Branch and PR conventions + +- Single long-lived branch: `master`. Do commit to it directly for release-ready + work; land features through a short-lived feature branch → PR → merge to `master`. +- Conventional-commit subjects (`feat`, `fix`, `docs`, `refactor`, `test`, `chore`, + `style`), scoped where useful, e.g.: + + ```text + feat(isdu): add Data Storage restore readback verification + fix(port): reset rx_retry_count after a good frame + docs(porting): document t_REN half-duplex settling + ``` + +- Update `CHANGELOG.md` under `[Unreleased]` in the same PR as a user-visible change, + and update `IMPLEMENTATION_STATUS.md` when a feature's status changes. +- Prefer merge over rebase for integrating branches. +- **No AI-authored attribution** in commits, PR descriptions, or code comments. + +## Reporting issues + +File issues on GitHub with a description, reproduction steps, and expected vs actual +behavior. For suspected vulnerabilities, follow [`SECURITY.md`](../SECURITY.md) +(private reporting) instead of opening a public issue. diff --git a/third_party/iolinki-master/docs/IMPLEMENTATION_STATUS.md b/third_party/iolinki-master/docs/IMPLEMENTATION_STATUS.md index 70dce1ffb..3afd17b94 100644 --- a/third_party/iolinki-master/docs/IMPLEMENTATION_STATUS.md +++ b/third_party/iolinki-master/docs/IMPLEMENTATION_STATUS.md @@ -19,18 +19,20 @@ Status definitions: | Public API shape | Partial | [`include/iolinki_master/master.h`](../include/iolinki_master/master.h), [`tests/test_master_public_header.c`](../tests/test_master_public_header.c), [`tests/test_master_isdu_public.c`](../tests/test_master_isdu_public.c), [`tests/test_master_sio_public.c`](../tests/test_master_sio_public.c) | Add more black-box tests for future service APIs. | | Opaque storage/private state | Implemented | [`include/iolinki_master/master.h`](../include/iolinki_master/master.h), [`src/master_internal.h`](../src/master_internal.h) | Tune the public storage sizes once the private state stops moving quickly. | | Port lifecycle | Implemented | [`src/master_port.c`](../src/master_port.c), [`tests/test_master_startup.c`](../tests/test_master_startup.c) | Add a public lifecycle example for downstream users. | -| Startup and baudrate scan | Implemented | [`src/master_port.c`](../src/master_port.c), [`tests/test_master_startup.c`](../tests/test_master_startup.c) | Validate timing and retries against real hardware. | +| Startup and baudrate scan | Implemented | [`src/master_port.c`](../src/master_port.c), [`tests/test_master_startup.c`](../tests/test_master_startup.c) | Per-baud wake-up retry (`wake_retry_limit`) added; the physical 80us WURQ pulse and t_WU/t_REN/TDMT timing still live in the PHY adapter and remain unverified on silicon. | | M-sequence handling | Implemented | [`src/master_port.c`](../src/master_port.c), [`src/master_parameters.c`](../src/master_parameters.c), [`tests/test_master_pd.c`](../tests/test_master_pd.c), [`tests/test_master_startup.c`](../tests/test_master_startup.c), [`tests/test_master_parameters.c`](../tests/test_master_parameters.c) | Real-device validation remains open. | | Cyclic process data | Implemented | [`src/master_port.c`](../src/master_port.c), [`tests/test_master_pd.c`](../tests/test_master_pd.c), [`tests/test_master_public_flow.c`](../tests/test_master_public_flow.c) | Add more black-box coverage for configured PD sizes and invalid user buffers. | | RX path and retries | Implemented | [`src/master_port.c`](../src/master_port.c), [`tests/test_master_startup.c`](../tests/test_master_startup.c), [`tests/test_master_tick.c`](../tests/test_master_tick.c) | Add line-noise and long-running soak tests with a real PHY. | | ISDU read/write | Partial | [`src/master_isdu.c`](../src/master_isdu.c), [`tests/test_master_isdu.c`](../tests/test_master_isdu.c), [`tests/test_master_isdu_public.c`](../tests/test_master_isdu_public.c), [`tests/test_master_fake_device.c`](../tests/test_master_fake_device.c) | Verify behavior against real devices. | | Direct Parameter Page 1 | Implemented | [`src/master_parameters.c`](../src/master_parameters.c), [`tests/test_master_parameters.c`](../tests/test_master_parameters.c), [`tests/test_master_isdu.c`](../tests/test_master_isdu.c) | Real-device validation remains open. | | Startup device validation | Implemented | [`src/master_parameters.c`](../src/master_parameters.c), [`src/master_port.c`](../src/master_port.c), [`tests/test_master_startup.c`](../tests/test_master_startup.c) | Expand validation once automatic negotiation exists. | +| Device identity / inspection level | Partial | [`src/master_parameters.c`](../src/master_parameters.c), [`src/master_port.c`](../src/master_port.c), [`tests/test_master_parameters.c`](../tests/test_master_parameters.c) | VendorID/DeviceID checked under `TYPE_COMP`/`IDENTICAL`; the SerialNumber leg that distinguishes `IDENTICAL` (ISDU index 0x0015) is not yet wired. | | Diagnostics | Partial | [`include/iolinki_master/master.h`](../include/iolinki_master/master.h), [`src/master_port.c`](../src/master_port.c), [`src/master_isdu.c`](../src/master_isdu.c), [`tests/test_master_pd.c`](../tests/test_master_pd.c), [`tests/test_master_isdu.c`](../tests/test_master_isdu.c) | Add event detail and link-quality metrics. | | Multi-port controller | Partial | [`src/master_controller.c`](../src/master_controller.c), [`tests/test_master_controller.c`](../tests/test_master_controller.c), [`examples/master_4port_controller_demo.c`](../examples/master_4port_controller_demo.c) | Define scheduler ownership and port-level runtime policy. | | SIO DI/DQ | Partial | [`src/master_sio.c`](../src/master_sio.c), [`tests/test_master_startup.c`](../tests/test_master_startup.c), [`tests/test_master_sio_public.c`](../tests/test_master_sio_public.c) | Validate SIO and mode transitions against real adapters. | -| Scheduler/timing | Partial | [`src/master_port.c`](../src/master_port.c), [`src/master_controller.c`](../src/master_controller.c), [`tests/test_master_tick.c`](../tests/test_master_tick.c), [`tests/test_master_controller.c`](../tests/test_master_controller.c), [`tests/test_master_public_flow.c`](../tests/test_master_public_flow.c) | Validate timing against hardware captures. | -| Events | Partial | [`include/iolinki_master/master.h`](../include/iolinki_master/master.h), [`src/master_isdu.c`](../src/master_isdu.c), [`tests/test_master_isdu_public.c`](../tests/test_master_isdu_public.c) | Validate event flows against real devices. | +| Scheduler/timing | Partial | [`src/master_port.c`](../src/master_port.c), [`src/master_parameters.c`](../src/master_parameters.c), [`src/master_controller.c`](../src/master_controller.c), [`tests/test_master_tick.c`](../tests/test_master_tick.c), [`tests/test_master_controller.c`](../tests/test_master_controller.c), [`tests/test_master_parameters.c`](../tests/test_master_parameters.c) | MasterCycleTime octet (time-base + multiplier) now decoded to 100us for validation and pacing. Validate timing against hardware captures. | +| Master Command channel/addressing | Implemented | [`src/master_parameters.c`](../src/master_parameters.c), [`src/master_port.c`](../src/master_port.c), [`tests/test_master_parameters.c`](../tests/test_master_parameters.c) | R/W + communication-channel + address encode/decode helpers; the operate transition is composed through them. Page/diagnosis channel services build on this next. | +| Events | Partial | [`include/iolinki_master/master.h`](../include/iolinki_master/master.h), [`src/master_isdu.c`](../src/master_isdu.c), [`src/master_port.c`](../src/master_port.c), [`tests/test_master_isdu_public.c`](../tests/test_master_isdu_public.c), [`tests/test_master_fake_device.c`](../tests/test_master_fake_device.c) | Optional dispatch callbacks (rising-edge event-pending notify + per-event handler) added; fully autonomous async event servicing and real-device validation remain. | | Data Storage | Implemented | [`src/master_isdu.c`](../src/master_isdu.c), [`tests/test_master_isdu_public.c`](../tests/test_master_isdu_public.c), [`tests/test_master_fake_device.c`](../tests/test_master_fake_device.c) | Validate Data Storage restore flows against real devices. | | Block parameterization | Implemented | [`src/master_isdu.c`](../src/master_isdu.c), [`tests/test_master_isdu_public.c`](../tests/test_master_isdu_public.c) | Validate block flows against real devices. | | Hardware PHY adapters | Open | [`include/iolinki_master/master.h`](../include/iolinki_master/master.h) consumes the dependency PHY contract | Add real master-port hardware adapters outside the protocol core. | @@ -57,6 +59,11 @@ and gap detail. - [x] Startup wake-up, Type 0 idle, transition command, and operate entry. - [x] Fixed-baudrate startup. - [x] Auto-baudrate scan across COM3/COM2/COM1. +- [x] Configurable per-baud wake-up retry before scan advance / error. +- [x] MasterCycleTime octet (time-base + multiplier) decode to 100us units. +- [x] Master Command R/W + communication-channel + address encode/decode helpers. +- [x] Rising-edge event-pending dispatch callback. +- [x] Per-event dispatch callback from event details. - [x] Configured cyclic PD input/output. - [x] RX accumulation, checksum handling, and retry tracking. - [x] ISDU read/write transfer in local tests. @@ -75,6 +82,7 @@ and gap detail. - [x] Fixed Type 2 capability selection for code-0 Direct Parameter profiles. - [x] Public requested-config validation against Direct Parameter Page 1. - [x] Optional startup device-info validation. +- [x] Device identity (VendorID/DeviceID) check with `NO_CHECK`/`TYPE_COMP`/`IDENTICAL` inspection levels. - [x] Basic diagnostics API. - [x] Response timeout counter in public diagnostics. - [x] Cycle-slip counter in public diagnostics. @@ -169,6 +177,35 @@ Update this file in the same commit as implementation changes when the status of a feature changes. Keep the gap column honest: passing local tests does not mean hardware or conformance coverage exists. +## Spec Conformance Audit (Interface & System Spec V1.1.5) + +Verified against the V1.1.5 spec text on 2026-07-04. Bit-level field encodings +are conformant: MinCycleTime octet (Table B.3), Direct Parameter Page 1 layout +(Table B.1), M-sequenceCapability bits (Figure B.3), RevisionID (Figure B.4), and +the M-sequence control octet — R/W, communication channel, address (Figure A.1, +Tables A.1/A.2). + +**Fixed:** ProcessData descriptor decode now isolates Length to bits 0-4 per +Table B.6 (previously the SIO bit corrupted the length, and sub-byte bit lengths +were truncated). + +**Known deviations (co-designed with the `iolinki` device stack; a third-party +conformant device would reject them). Fixing requires a coordinated master+device +change and will break the on-wire model until both land:** + +- **Startup / OPERATE transition.** The spec's startup state machine requires the + first message to be `MC = 0xA2` (read MinCycleTime at address 0x02 on the page + channel) and the OPERATE transition to be MasterCommand `0x99` "DeviceOperate" + (Table B.2) written to address 0x00 on the page channel. The stack instead + sends a bare `0x00` probe and a bare `0x0F` transition octet. +- **ISDU I-Service nibble.** Table A.12 defines Read = `0x9/0xA/0xB` and + Write = `0x1/0x2/0x3`. The shared `IOLINK_ISDU_SERVICE_READ 0x08` / + `_WRITE 0x09` constants emit `0x8` (reserved) for reads and `0x9` (a *read* + code) for writes. + +The on-wire `labwired-real-firmware-model` CI proves master↔`iolinki`-device +interop, not spec conformance: the device mirrors these same conventions. + ## Architecture Priority Do not treat all open rows as equal. The scheduler/timing row is the current diff --git a/third_party/iolinki-master/docs/MISRA_DEVIATIONS.md b/third_party/iolinki-master/docs/MISRA_DEVIATIONS.md new file mode 100644 index 000000000..2f5f771f3 --- /dev/null +++ b/third_party/iolinki-master/docs/MISRA_DEVIATIONS.md @@ -0,0 +1,34 @@ +# MISRA C:2012 Deviations + +The `iolinki-master` protocol core (`src/master_*.c`, `include/iolinki_master/master.h`) +targets MISRA C:2012. It is checked with `cppcheck --addon=misra` (run by +`check_quality.sh`, opt-in). This file records the deviations that are +**accepted** rather than fixed, with rationale. Rules not listed here are either +clean or fixed in code. + +Cleared in code: Rule 17.7 (ignored `memcpy`/`memset` returns now `(void)`-cast), +Rule 13.4 (assignment-in-`while` replaced with an explicit read + `break`), +Rule 15.7 (all `if … else if` chains terminated with an `else`). + +## Accepted deviations + +| Rule | Type | Where | Rationale | +| --- | --- | --- | --- | +| **11.5** | Required | `master_internal.h` opaque-storage accessors (×4) | `void*` → private-state pointer. The public ABI is caller-owned, heap-free, and opaque; the `_storage_must_fit` static asserts guarantee size and the `iolink_master_*_t` union alignment members guarantee alignment. Annotated at the source. | +| **19.2** | Advisory | `master.h` `iolink_master_port_t` / `_controller_t` (×N) | The `union` keyword. Used only for the opaque caller-owned storage types, which need a fixed size and worst-case alignment. No other unions exist. | +| **10.4** | Required | status/error comparisons (×22) | The public API returns `int` for status/error codes (a deliberate, forward-compatible ABI choice) and compares against named `IOLINK_MASTER_*` enum constants. The comparisons are value-correct; unifying the essential type would change the public return type. | +| **15.5** | Advisory | throughout (×~200) | Multiple `return` statements (guard-clause early exits). The style is the established idiom in this stack and is clearer than deep nesting; single-exit restructuring would reduce readability. | +| **13.3** | Advisory | buffer index post-increments (×9) | `buf[i++]` within a larger expression. Local, idiomatic, and clear; no sequencing ambiguity. | +| **10.8** | Advisory | `master_parameters.c`, `master_port.c` (×2) | Composite expression cast to a narrower type / enum. Each operand is masked to range before the cast, so the conversion is value-safe. | +| **9.3** | Advisory | `{0U}` array initializers (×2) | A partial initializer that zero-initializes the whole array per C. Intentional. | +| **2.3 / 2.4 / 2.5** | Advisory | public header (×N) | Typedefs / tags / macros declared in the installed public header but unused by the single translation unit under analysis. They are part of the API surface, not dead code. | +| **8.7** | Advisory | public API functions (×3) | Flagged as "could be static", but these are the public API declared in `include/iolinki_master/master.h` and have external callers. False positive. | + +## Re-running the check + +```sh +IOLINKI_MASTER_MISRA_ENFORCE=1 ./check_quality.sh +``` + +The MISRA stage is skipped when the cppcheck MISRA addon is not installed, unless +`IOLINKI_MASTER_MISRA_ENFORCE=1` is set (then a missing addon fails the gate). diff --git a/third_party/iolinki-master/docs/PORTING.md b/third_party/iolinki-master/docs/PORTING.md new file mode 100644 index 000000000..aaecc5cd2 --- /dev/null +++ b/third_party/iolinki-master/docs/PORTING.md @@ -0,0 +1,129 @@ +# Porting Guide — implementing a master PHY adapter + +`iolinki-master` is board-agnostic. To run it on real hardware you implement the +PHY contract and the fallible adapter hooks; the protocol core never includes board +headers and never sleeps. This guide is the how-to; the normative contract and +adapter rules live in [`PHY_BOUNDARY.md`](PHY_BOUNDARY.md). Nothing here has been +run on silicon yet — treat the timing responsibilities below as the spec you must +satisfy, not as validated behavior. + +## Two surfaces to implement + +Board support enters the core through **two** structures, not one: + +1. **`iolink_phy_api_t`** (shared from `iolinki/phy.h`) — the transceiver/UART + driver. +2. **Adapter hooks in `iolink_master_config_t`** (in + `include/iolinki_master/master.h`) — the fallible, master-specific operations + the core calls around startup and each frame. + +### 1. The PHY struct (`iolink_phy_api_t`) + +```c +typedef struct { + void* user; /* passed to every call */ + int (*init)(void* user); /* 0 ok, <0 hw failure */ + void (*set_mode)(void* user, iolink_phy_mode_t mode); /* INACTIVE/SIO/SDCI */ + void (*set_baudrate)(void* user, iolink_baudrate_t b); /* COM1/COM2/COM3 */ + int (*send)(void* user, const uint8_t* data, size_t len); /* exact len, or <0 */ + int (*recv_byte)(void* user, uint8_t* byte); /* 1 got, 0 none, <0 err */ + int (*detect_wakeup)(void* user); /* optional */ + void (*set_cq_line)(void* user, uint8_t state); /* optional, DQ mode */ + int (*get_voltage_mv)(void* user); /* optional, L+ diag */ + bool (*is_short_circuit)(void* user); /* optional, fault diag */ +} iolink_phy_api_t; +``` + +Rules that matter for the master core: + +- `send` must return the exact length or a negative/short result — never a partial + "success" — so the core can enter error handling. +- `recv_byte` must be non-blocking and must surface UART framing errors as a + negative return, not hide them. +- `set_cq_line` is required for DQ output; `get_voltage_mv` / `is_short_circuit` + feed `iolink_master_get_diagnostics` when present. +- The PHY is retained **by pointer** and must outlive the port. Never pass a stack + temporary to `iolink_master_init`. + +### 2. The config adapter hooks + +These live in `iolink_master_config_t` and are what +`iolink_master_validate_phy_contract()` requires for strict hardware use: + +| Hook | When the core calls it | Your job | +|---|---|---| +| `set_mode_checked(mode)` | mode transitions | Switch the transceiver into SDCI / SIO / inactive and **return non-zero on failure** | +| `set_baudrate_checked(baud)` | fixed and auto-baud startup | Apply COM1/COM2/COM3 and report failure | +| `flush_rx()` | before startup and before each retry / baud change | Clear the UART/adapter RX FIFO so stale bytes cannot bleed across attempts | +| `prepare_tx()` | before each core-driven `send` | Switch the half-duplex driver to transmit | +| `prepare_rx()` | after each `send` | Switch back to receive; return non-zero if you cannot, so the core stops instead of listening in the wrong direction | +| `wake_up()` | startup, per `wake_retry_limit` | Generate the master wake-up request (WURQ) — see timing below | +| `read_cq_line_checked()` | DI mode | Read the C/Q line, report adapter failure | +| `read_cq_line()` | DI mode (permissive fallback) | Legacy reader for tests/partial fakes | + +`iolink_master_init()` stays permissive (it accepts partial fake PHYs for unit +tests); real adapters should pass `iolink_master_validate_phy_contract()` before a +hardware run. + +## Timing that lives in the adapter — not the core + +The core supplies monotonic 100µs pacing (`iolink_master_tick_at`), but the physical +line timing is **entirely the adapter's responsibility** and is currently +unverified on hardware: + +- **The 80µs WURQ wake pulse.** `wake_up()` must generate the master wake-up + request (a defined wake pulse on C/Q). The core only decides *when* to call it and + how many times (`wake_retry_limit`); it does not shape the pulse. +- **`t_WU`** — the wake-up recovery / device-ready window after the pulse before the + first master message. +- **`t_REN`** — the driver-enable / receiver-enable settling around half-duplex + direction changes, which is why `prepare_tx` / `prepare_rx` exist as explicit + hooks. +- **`TDMT`** — the master's inter-frame idle time before it starts a new message. + +If your transceiver or MCU UART cannot meet these windows, that is a hardware/timing +limitation the core cannot paper over. Validate them with a logic analyzer per +[`HARDWARE_VALIDATION.md`](HARDWARE_VALIDATION.md). + +## Wiring it up + +```c +static const iolink_phy_api_t my_phy = { + .user = &my_board_ctx, + .init = my_init, .set_mode = my_set_mode, .set_baudrate = my_set_baud, + .send = my_send, .recv_byte = my_recv, .set_cq_line = my_set_cq, + .get_voltage_mv = my_vmon, .is_short_circuit = my_fault, +}; + +static iolink_master_config_t cfg = { + .port_mode = IOLINK_MASTER_PORT_MODE_IOLINK, + .m_seq_type = IOLINK_MASTER_M_SEQ_TYPE_2_1, + .baudrate = IOLINK_BAUDRATE_COM3, + .min_cycle_time = 20U, .pd_in_len = 1U, .pd_out_len = 1U, + .response_timeout_100us = 30U, .wake_retry_limit = 3U, + .set_mode_checked = my_set_mode_checked, + .set_baudrate_checked = my_set_baud_checked, + .flush_rx = my_flush_rx, + .prepare_tx = my_prepare_tx, .prepare_rx = my_prepare_rx, + .wake_up = my_wake_up, + .read_cq_line_checked = my_read_cq_checked, +}; + +iolink_master_port_t port; /* caller-owned, no heap */ +if (iolink_master_validate_phy_contract(&my_phy, &cfg) != IOLINK_MASTER_STATUS_OK) { /* fix adapter */ } +if (iolink_master_init(&port, &my_phy, &cfg) != IOLINK_MASTER_STATUS_OK) { /* handle */ } +``` + +Then run it from your timer loop: compute the next due time with +`iolink_master_get_next_tick_time`, and on each due tick call +`iolink_master_tick_at(&port, event, now_100us)` followed by +`iolink_master_poll_rx(&port)`. See [`API.md`](API.md) for the tick model. + +## Adapter don'ts + +- Do not include board headers from `src/master_*.c` — adapter code lives outside + the core. +- Do not sleep inside core calls; schedule the next call with the next-due-time + helpers. +- Do not mutate core state from a fault callback; surface faults through PHY + callbacks / diagnostics only. diff --git a/third_party/iolinki-master/docs/RELEASE_STRATEGY.md b/third_party/iolinki-master/docs/RELEASE_STRATEGY.md new file mode 100644 index 000000000..6f08e180b --- /dev/null +++ b/third_party/iolinki-master/docs/RELEASE_STRATEGY.md @@ -0,0 +1,85 @@ +# iolinki-master Release Strategy + +The master stack uses a **single-branch** model — simpler than the device stack's +Gitflow. There is no `develop`. All release-ready work lives on `master`; features +land through short-lived branches and PRs (see [`CONTRIBUTING.md`](CONTRIBUTING.md)). +A release is a tag on `master`. + +## 1. Branches + +- **`master`** — the one long-lived branch. Stable, releasable, protected. Version + tags (`vX.Y.Z`) are pushed from here. +- **feature / fix branches** — short-lived, created from `master`, merged back via + PR after CI is green. + +## 2. Versioning + +**Semantic Versioning 2.0.0**, `MAJOR.MINOR.PATCH`: + +- **MAJOR** — incompatible public-API changes (the `include/iolinki_master/master.h` + contract, opaque storage sizes, or result-code meanings). +- **MINOR** — backward-compatible new functionality (new services, new diagnostics). +- **PATCH** — backward-compatible bug fixes. + +Pre-1.0, the API is still moving; minor versions may tighten contracts as the +timing/scheduler layer stabilizes. Keep the project version in `CMakeLists.txt` +(`project(... VERSION ...)`) in step with the tag. + +## 3. What gates a release + +A tag is only cut when, on the exact `master` SHA being tagged: + +- The full local gate is green: `cmake -S . -B build && cmake --build build && + ctest --test-dir build --output-on-failure && git diff --check`. +- Both CI jobs are green on that SHA: `cmake-ctest` and + `labwired-real-firmware-model`. +- `CHANGELOG.md` has the release's entry moved out of `[Unreleased]` into a dated + `vX.Y.Z` section, and `IMPLEMENTATION_STATUS.md` reflects reality — no feature is + claimed as more mature than its evidence. In particular, do **not** claim + hardware, timing, or IO-Link-conformance validation that has not happened. + +## 4. Release process + +```bash +git checkout master +git pull origin master +# bump project() VERSION in CMakeLists.txt, finalize CHANGELOG.md, commit via PR +git tag -a v0.2.0 -m "Release v0.2.0" +git push origin v0.2.0 +``` + +Pushing a `v*` tag triggers `release.yml`, which: + +- builds and tests the stack with **code coverage**, +- generates **SBOMs** in both **CycloneDX and SPDX** (shipping from `0.2`), +- generates **release notes** from the conventional-commit history, +- creates the **GitHub Release** with the notes, SBOMs, and coverage attached. + +The SBOMs record the two source origins — this repository and the pinned `iolinki` +frame/CRC helper sources — making explicit that the stack has zero third-party +runtime dependencies (see [`security/CRA.md`](security/CRA.md)). + +### Failed release + +If CI fails on a tag, delete the tag locally and on the remote, fix on `master`, and +re-tag: + +```bash +git tag -d v0.2.0 +git push origin :v0.2.0 +# fix on master, then re-tag +``` + +## 5. Release artifacts + +Each GitHub Release includes: the auto-generated source archive, the CycloneDX + +SPDX SBOMs, the coverage summary, and the generated release notes. Security fixes +ship as tagged releases with a `CHANGELOG.md` entry and, for confirmed +vulnerabilities, a GitHub security advisory (see [`SECURITY.md`](../SECURITY.md)). + +## 6. Security and CRA + +Supported versions and the coordinated-disclosure process are defined in +[`SECURITY.md`](../SECURITY.md); the CRA division of labor and the SBOM commitment +are in [`security/CRA.md`](security/CRA.md). The per-release SBOMs are the +machine-readable half of that commitment and begin with the `0.2` release. diff --git a/third_party/iolinki-master/docs/security/CRA.md b/third_party/iolinki-master/docs/security/CRA.md new file mode 100644 index 000000000..3a41d5806 --- /dev/null +++ b/third_party/iolinki-master/docs/security/CRA.md @@ -0,0 +1,67 @@ +# iolinki-master and the EU Cyber Resilience Act + +*This page is orientation for master-product makers evaluating the stack. It is not +legal advice; your conformity assessment is yours.* + +## You remain the manufacturer + +Integrating `iolinki-master` — or any third-party stack — does not change your +status under Regulation (EU) 2024/2847 (the Cyber Resilience Act). For the IO-Link +master product you place on the EU market, CE marking, the EU Declaration of +Conformity, the ten-year technical-documentation retention, and the Article 14 +reporting obligations stay entirely with you. What a stack supplier owes you is the +*foundation* your technical documentation builds on. That is what this package is. + +Key dates: the CRA's vulnerability-reporting obligations apply from +**11 September 2026**; the full obligations from **11 December 2027**. + +## What iolinki-master provides + +| Deliverable | Where | Terms | +|---|---|---| +| SBOM per release (CycloneDX 1.6 + SPDX 2.3) | attached to every [tagged release](https://github.com/w1ne/iolinki-master/releases), **from `0.2`** | free, public | +| STRIDE threat model aligned to IO-Link guideline 10.512 (Master surface) | [`THREAT_MODEL.md`](THREAT_MODEL.md) | free, public | +| Coordinated disclosure + advisory process | [`SECURITY.md`](../../SECURITY.md) | free, public | +| CRA compliance statement mapping the stack to Regulation (EU) 2024/2847 Annex I, per stack release for your product context | commercial license package | commercial | +| Contractually agreed security updates over a defined support period | commercial license package | commercial | + +The public artifacts let you verify our engineering rigor before you talk to us. +The commercial artifacts are the contract-grade documents your CRA technical file +and supplier-management process need. + +## Zero third-party runtime dependencies + +The master stack has **no third-party runtime dependencies**. It reuses only the +narrow `crc.c` / `frame.c` helper sources from the sibling `iolinki` device +repository, compiled directly into the master build (see `CMakeLists.txt` and +[`README.md`](../../README.md)); it does **not** link the device stack, a fieldbus +runtime, or any external library. The per-release SBOMs shipping from `0.2` state +this explicitly rather than by omission, so both source origins — this repository +and the pinned `iolinki` helper sources — appear in your bill of materials. + +## Why a protocol stack is in scope at all + +Commercially licensed software placed on the EU market is a "product with digital +elements" under the CRA. As the stack's supplier we carry manufacturer obligations +for the stack itself — which is why the SBOM, the disclosure process, and the +support-period commitment exist as maintained artifacts rather than sales material. +The stack is in the CRA's *default* (non-critical) class: conformity is +self-assessed, no notified body involved. + +## The division of labor, concretely + +**We cover (for the stack):** risk analysis of the master's attack surface — the +untrusted C/Q wire from a rogue Device, PHY adapter boundary, and ISDU/event/Data +Storage parsing (the [threat model](THREAT_MODEL.md)); input-validation and +integrity mechanisms with code anchors; SBOM; vulnerability handling and +advisories; security fixes over the support period. + +**You cover (for your master product):** your product risk assessment; the +fieldbus/network uplink and its security; the PHY adapter and its wake-pulse/timing +integrity (the stack ships no hardware adapter — see the threat model's gaps and +[`PHY_BOUNDARY.md`](../PHY_BOUNDARY.md)); firmware update authenticity and boot +integrity of the master; physical-protection guidance in your user documentation; +your DoC, CE marking, and Article 14 reporting. + +For commercial-package inquiries, email **andrii@shylenko.com** or open a +(non-security) discussion on the repository. diff --git a/third_party/iolinki-master/docs/security/THREAT_MODEL.md b/third_party/iolinki-master/docs/security/THREAT_MODEL.md new file mode 100644 index 000000000..253a5d1df --- /dev/null +++ b/third_party/iolinki-master/docs/security/THREAT_MODEL.md @@ -0,0 +1,239 @@ +# iolinki-master Threat Model + +**Aligned to:** IO-Link Security Design and Development Guideline, Order No. 10.512 +(D1.0.0-01, October 2025) and IO-Link Secure Deployment Guideline, Order No. 10.502 +(V1.0.0, June 2025), both published by the IO-Link Community. This document +paraphrases and cites those guidelines; it does not reproduce their text. Obtain +them from [io-link.com/downloads](https://io-link.com/downloads). + +**Scope:** the `iolinki-master` stack as a software component integrated into an +IO-Link Master port controller. This is the *Master* side of the link: the peer on +the wire is a potentially rogue or faulty **Device**, and the primary job of the +stack is to ensure that no byte sequence a Device puts on the C/Q line can corrupt +master state or the integrating application's memory. The protocol in scope is the +wired point-to-point SDCI interface only — no networking, no TCP/IP, no wireless, +no fieldbus uplink (that uplink is the master product's domain, not this stack's). + +Every stack claim below carries a code anchor. A claim without an anchor belongs in +[§5 Gaps](#5-gaps-and-integrator-duties), not here. Because `iolinki-master` is +simulation-validated and **not yet validated on real IO-Link silicon**, this model +is explicit about which guarantees are protocol-logic guarantees (locally tested) +and which depend on unverified PHY/timing behavior. + +## 1. System model and trust boundaries + +``` + Fieldbus / application host (trusted: the master product's own firmware) + │ caller API (iolink_master_* calls, caller-owned storage) + Public API include/iolinki_master/master.h + │ + Protocol core src/master_port.c (startup, cyclic PD, RX/retry) + ├─ Direct Parameters src/master_parameters.c (page 1 parse, identity) + ├─ ISDU / services src/master_isdu.c (segmentation, DS, events, blocks) + ├─ SIO DI/DQ src/master_sio.c + └─ Multi-port controller src/master_controller.c + │ encoded frames / decoded bytes + Shared frame + CRC ../iolinki: src/frame.c, src/crc.c (build-time reuse) + │ bytes + PHY adapter iolink_phy_api_t + config adapter hooks (board code) + │ C/Q wire ═══════════════════════════════════ (untrusted input boundary) + IO-Link Device (rogue / faulty / attacker-controlled) +``` + +Trust boundaries: + +- **The wire and the Device are untrusted.** Every byte arriving through + `recv_byte` / `iolink_master_on_rx` — wake response, Direct Parameter Page 1, + cyclic PD, OD/ISDU responses, event details, Data Storage records — is + Device-controlled and, in the threat scenarios of 10.512 clause 6, attacker + controlled. A rogue Device is the master's primary adversary. +- **The PHY adapter is a boundary, not a defender.** Board code owns transceiver + registers, half-duplex direction, and the physical wake pulse/timing. The core + treats adapter returns as data: a negative `send`/`recv_byte`/checked-hook result + drives error handling rather than being trusted. +- **The caller/application is trusted.** The stack runs in the master firmware's + trust domain and does not defend against its own host. Caller-owned opaque + storage (`iolink_master_port_t`, `iolink_master_controller_t`) means the host, + not the stack, controls all allocation. +- **Build/supply chain** is outside the runtime model and covered by the SBOM (see + `SECURITY.md` / [`CRA.md`](CRA.md)): zero third-party runtime dependencies — the + only external source reuse is the sibling `iolinki` `frame.c`/`crc.c` at build + time — so the supply-chain surface is these two repositories plus the integrator's + toolchain. + +## 2. Assets + +| Asset | Where it lives | +|---|---| +| Integrity of decoded process data returned to the application | `src/master_port.c`, `iolink_master_get_pd_in` | +| Master port state machine (inactive→startup→preoperate→operate→error) | `src/master_port.c`, `iolink_master_get_state` | +| Device parameterization images (ISDU, Data Storage, block params) | `src/master_isdu.c` | +| Device identity / configuration match | `src/master_parameters.c` | +| Availability of the master port function | whole stack, fixed-resource design | + +## 3. STRIDE analysis + +10.512 clause 6 (Table 1) identifies spoofing of either peer, tampering/replay on +the wire, information disclosure on the wire, and denial of service as the relevant +threats, with **physical protection of cable and device as the guideline's +countermeasure at SL-C 1**. The SDCI protocol carries no cryptographic +authentication, integrity, or confidentiality (10.512 §7.4.2, §7.5.2). The stack +therefore cannot — and does not claim to — defend against a physically present +attacker; what it guarantees is that malformed or hostile Device traffic is +*rejected safely* rather than corrupting the master. + +### S — Spoofing (a rogue Device impersonating the expected one) + +- Protocol reality (10.512 §6): SDCI has no cryptographic peer authentication; + physical protection is the countermeasure. Integrator duty. +- Stack guarantees — identity is checked at the *inspection* level the port + configuration model defines, not cryptographically: + - Direct Parameter Page 1 is parsed into a typed device-info record and the + connected Device's VendorID/DeviceID are compared against the configured + expected values whenever `inspection_level != NO_CHECK` + (`src/master_parameters.c`, `iolink_master_validate_config_against_device_info`; + grep anchors `vendor_id`/`device_id` compare at the identity check). A mismatch + is rejected with `PARAM_ERR_VENDOR_ID` / `PARAM_ERR_DEVICE_ID` before the port + is allowed into OPERATE with that Device. + - **Honest limit:** `IDENTICAL` inspection is meant to additionally bind the + Device SerialNumber (ISDU index 0x0015). That leg is **not yet wired**; today + `IDENTICAL` enforces the same VendorID/DeviceID check as `TYPE_COMP` + (documented at the `iolink_master_inspection_level_t` enum in `master.h`, and in + [`IMPLEMENTATION_STATUS.md`](../IMPLEMENTATION_STATUS.md) "Device identity"). A + Device that clones a VendorID/DeviceID pair is not distinguished from the + genuine unit. This is an identity gap, not a memory-safety gap. + +### T — Tampering (modified, replayed, or forged Device frames) + +- Protocol reality: the per-frame CRC6/checksum detects *accidental* corruption + only; intentional modification with a valid checksum is undetectable at the + protocol level (10.512 §7.4.2). Integrator duty: physical protection. +- Stack guarantees (CR 3.1, CR 3.5 — 10.512 §7.4.2, §7.4.5), all locally tested: + - Every received response is checksum-verified before use: Type-0 replies via + `iolink_checksum_ck` and multi-octet OPERATE frames via `iolink_crc6` / + `resp.checksum_ok` (`src/master_port.c`, RX path around the `checksum_errors` + counter). A failed check increments `checksum_errors`, triggers bounded retry, + and otherwise returns `IOLINK_MASTER_ERR_CHECKSUM` — it never forwards + corrupted PD to the caller. + - Frame decode is length/bounds-checked before parsing; NULL and size violations + return `INVALID_ARG` / `ERR_FRAME` (`iolink_master_on_rx`, `iolink_master_poll_rx`). + - ISDU responses accumulate into a fixed buffer with an explicit ceiling: the + response length is clamped at `IOLINK_ISDU_BUFFER_SIZE` before each byte is + stored (`src/master_isdu.c`, `iolink_master_isdu_on_od`), and the read-out path + refuses to overflow the caller buffer — when `*len < result_len` it reports the + required size and returns `ISDU_ERR_BUFFER_TOO_SMALL` instead of copying + (`iolink_master_isdu_finish_read`). ISDU writes bound the request against + `IOLINK_ISDU_BUFFER_SIZE - 5` before staging (`iolink_master_write_isdu`). + - Event-detail decode is caller-bounded: `iolink_master_read_event_details` takes + `max_events` and writes no more than that, returning `BUFFER_TOO_SMALL` rather + than overrunning the caller's `events[]` array. + - Data Storage restore verifies by readback: `iolink_master_restore_data_storage` + and `iolink_master_verify_data_storage` compare the written image against a + read-back copy and return `VERIFY_FAILED` on mismatch, so a Device that + silently mangles a stored record is detected rather than trusted. + +### R — Repudiation + +- Assessed not relevant at SL-C 1 (10.512 §7.3.9): machine-to-machine, no human + users, no accounts. No stack claims. Diagnostics counters + (`iolink_master_get_diagnostics`) are operational, not an audit log. + +### I — Information disclosure (wire eavesdropping) + +- Protocol reality: no encryption exists (10.512 §7.5.2); confidentiality in + transit is achieved by restricting physical access. Integrator duty, to be stated + in the master product's user documentation. +- Stack guarantee (CR 3.7 — 10.512 §7.4.7): the stack originates no data of its own + onto the wire beyond protocol-required master frames and the services the caller + invokes. Error paths return the named result codes in `master.h` + (`IOLINK_MASTER_ERR_*`, `IOLINK_MASTER_ISDU_ERR_*`); no internal pointers or + private `src/master_internal.h` state are exposed through the public API. + +### D — Denial of service (a Device that floods, drops, stalls, or truncates) + +- Protocol reality: a point-to-point peer can always stop or corrupt + communication; DoS by the peer is not fully defendable at the link (10.512 §7.8.2). +- Stack guarantees: + - **No dynamic memory anywhere** (`grep` for `malloc`/`calloc`/`free` in `src/` + and `include/` returns nothing). All port/controller state lives in + caller-owned opaque storage with audited fixed budgets + (`IOLINK_MASTER_PORT_STORAGE_SIZE`, `IOLINK_MASTER_CONTROLLER_STORAGE_SIZE` in + `master.h`). A flooding Device cannot exhaust a heap because there is none. + - **Bounded RX retry.** Checksum/short-frame failures retry at most twice + (`rx_retry_count < 2U`, `src/master_port.c`) before surfacing an error and a + counter increment; a Device injecting persistent bad checksums degrades the port + to an observable error, not an unbounded loop. + - **Response timeouts are explicit and scheduler-visible.** A dropped or truncated + response is modelled as `IOLINK_MASTER_TICK_RESPONSE_TIMEOUT` / + `iolink_master_on_timeout`, increments `response_timeouts`, and lets the + caller-owned scheduler decide recovery — the core never blocks or sleeps + (see [`PHY_BOUNDARY.md`](../PHY_BOUNDARY.md)). Truncated-frame and + dropped-response recovery are covered by the fake-device harness + (`tests/test_master_fake_device.c`). + - **One bad port does not corrupt its siblings.** The controller isolates + per-port state and returns the first negative per-port result without letting a + failing port mutate others (`src/master_controller.c`, + `iolink_master_controller_tick*`). + +## 4. IEC 62443-4-2 requirement mapping (stack view) + +Restating the *relevant* 10.512 clause 7 (Table 2) requirements as master-stack +claims. Rows Table 2 assesses "not relevant" (FR 1 identification, most of FR 2, +host/network-device requirements) are omitted for the reasons the guideline gives. + +| Requirement (10.512 ref) | Stack claim | Anchor | +|---|---|---| +| CR 3.1 communication integrity (§7.4.2) | CRC6/checksum verified on every Device response before PD is exposed | `src/master_port.c` RX path, `../iolinki/src/crc.c` | +| CR 3.4 software/information integrity (§7.4.4) | Data Storage / block-param images verified by readback before being trusted | `iolink_master_verify_data_storage`, `iolink_master_write_parameter_block` | +| CR 3.5 input validation (§7.4.5) | Length/bounds/segment legality of every received frame, ISDU, event, and DS record enforced before use | `src/master_isdu.c`, `src/master_port.c`, `src/master_parameters.c` | +| CR 3.6 deterministic output (§7.4.6) | Response timeout / comm loss is an observable state and counter, not a hang | `iolink_master_on_timeout`, `iolink_master_get_diagnostics` | +| CR 3.7 error handling (§7.4.7) | Errors answered with the named result codes only; no internal state leaks through the public API | `include/iolinki_master/master.h` result enums | +| CR 5.1 network segmentation (§7.6.2) | Point-to-point by construction | protocol property | +| CR 7.3/7.4 backup and recovery (§7.8.4-5) | Data Storage upload/restore/verify sequencing with readback | `src/master_isdu.c` DS/block services | +| CR 7.6/7.7 least functionality (§7.8.6-7) | Services layer sits above cyclic transport; unused service calls are simply not invoked, and PD/ISDU sizes are caller-configured | `iolink_master_config_t`, services in `master.h` | + +## 5. Gaps and integrator duties + +Stated plainly, because a threat model that hides gaps is marketing: + +1. **Not validated on real silicon.** All guarantees above are verified by local + CTest and the fake-device / on-wire firmware-model harness, **not** against real + IO-Link Devices. The physical 80µs WURQ wake pulse and the `t_WU`/`t_REN`/`TDMT` + startup timing live in the PHY adapter and are **unverified on hardware** (see + [`PHY_BOUNDARY.md`](../PHY_BOUNDARY.md), [`HARDWARE_VALIDATION.md`](../HARDWARE_VALIDATION.md)). + A timing or adapter defect could admit or misclassify traffic this model assumes + is rejected. Official IO-Link master conformance has **not** been run. +2. **Identity is inspection-level, not cryptographic, and `IDENTICAL` is partial.** + The SerialNumber leg (ISDU 0x0015) that distinguishes `IDENTICAL` from + `TYPE_COMP` is not wired. A VendorID/DeviceID-cloning Device is not detected. +3. **Physical protection is the countermeasure.** Per 10.512 §6 and 10.502, + spoofing/tampering/disclosure on the C/Q wire are mitigated physically at + SL-C 1. The master product's user documentation should carry that + security-assessment recommendation. +4. **Master-product duties the stack cannot see:** the fieldbus/network uplink and + its segmentation, firmware update and boot integrity of the master, secrets at + rest, and the product's CRA risk assessment. No BLOB Transfer & Firmware Update + profile is implemented here. +5. **10.512 D1.0.0-01 is a draft.** Claims cite the draft; the mapping is + re-verified against the final release. + +## 6. Verification + +Claims here are regression-checked by the local CMocka/CTest suite and the +fake-device harness (see [`TESTING.md`](../TESTING.md)): + +- Frame/checksum validation and retry: `tests/test_master_startup.c`, + `tests/test_master_pd.c`, `tests/test_master_tick.c` +- ISDU parsing, segmentation, buffer bounds, write/readback: + `tests/test_master_isdu.c`, `tests/test_master_isdu_public.c` +- Direct Parameter Page 1 and identity/inspection: + `tests/test_master_parameters.c` +- Data Storage / block-param verify: `tests/test_master_isdu_public.c`, + `tests/test_master_fake_device.c` +- Bad-checksum, dropped-response, and truncated-frame handling: + `tests/test_master_fake_device.c` +- End-to-end against the real device stack over in-memory queues: + `tests/test_master_real_iolinki_device.c` + +*Maintenance rule:* any PR that changes a file cited as an anchor here must +re-verify the corresponding claim or update this document. diff --git a/third_party/iolinki-master/docs/superpowers/plans/2026-06-17-iolink-master-isdu.md b/third_party/iolinki-master/docs/superpowers/plans/2026-06-17-iolink-master-isdu.md deleted file mode 100644 index e5d6697a0..000000000 --- a/third_party/iolinki-master/docs/superpowers/plans/2026-06-17-iolink-master-isdu.md +++ /dev/null @@ -1,53 +0,0 @@ -# IO-Link Master ISDU Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Turn the current ISDU skeleton into a real nonblocking master-side ISDU client layered over cyclic OD bytes. - -**Architecture:** `iolink_master_process()` emits process data plus OD bytes from the active ISDU transaction. `iolink_master_on_rx()` keeps latching PD input and feeds OD response bytes back into that transaction. Public read/write calls start a transaction, poll pending completion, and return copied data or protocol errors. - -**Tech Stack:** C99, CMake, cmocka, local `iolinki` frame/protocol helpers. - ---- - -### Task 1: Pin Down ISDU Wire Behavior - -**Files:** -- Modify: `tests/test_master_isdu.c` - -- [ ] Add tests proving read request OD emission, read response completion, write request payload emission, busy rejection, buffer-too-small handling, and device ISDU error handling. -- [ ] Run `cmake --build build --target test_master_isdu && ./build/tests/test_master_isdu`. -- [ ] Confirm the new tests fail because the skeleton does not emit or complete ISDU transactions. - -### Task 2: Add Master Transaction State - -**Files:** -- Modify: `include/iolinki_master/master.h` -- Modify: `src/master_internal.h` - -- [ ] Add small enums and fields for active operation, phase, request buffer, response buffer, sequence number, caller length, and error code. -- [ ] Keep public APIs unchanged. -- [ ] Run `cmake --build build --target test_master_public_header`. - -### Task 3: Integrate ISDU With Cyclic OD - -**Files:** -- Modify: `src/master_port.c` -- Modify: `src/master_isdu.c` - -- [ ] Let `master_port.c` ask `master_isdu.c` for the next OD bytes before encoding cyclic frames. -- [ ] Let `master_port.c` pass decoded response OD bytes to `master_isdu.c`. -- [ ] Implement nonblocking `read_isdu` and `write_isdu` start/poll semantics. -- [ ] Run `cmake --build build --target test_master_isdu && ./build/tests/test_master_isdu`. - -### Task 4: Verify Master Library - -**Files:** -- Modify if needed: `README.md` - -- [ ] Run `cmake --build build`. -- [ ] Run `./build/tests/test_master_startup`. -- [ ] Run `./build/tests/test_master_pd`. -- [ ] Run `./build/tests/test_master_isdu`. -- [ ] Run `./build/tests/test_master_public_header`. -- [ ] Note that full `ctest` may still include unbuilt dependency tests unless CTest registration is cleaned up separately. diff --git a/third_party/iolinki-master/docs/superpowers/plans/2026-06-17-opaque-master-api.md b/third_party/iolinki-master/docs/superpowers/plans/2026-06-17-opaque-master-api.md deleted file mode 100644 index 93f9c01a2..000000000 --- a/third_party/iolinki-master/docs/superpowers/plans/2026-06-17-opaque-master-api.md +++ /dev/null @@ -1,52 +0,0 @@ -# Opaque Master API Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Hide IO-Link master port/controller internals behind caller-owned opaque storage. - -**Architecture:** Public `master.h` keeps embedded-friendly storage objects and public APIs. Private `master_internal.h` owns the real state structs plus cast helpers. Implementation files access state through helpers; behavior tests either use public accessors or include the private header when they intentionally inspect state-machine internals. - -**Tech Stack:** C99, CMake, CMocka, local `iolinki` CRC/frame helpers. - ---- - -### Task 1: Introduce Opaque Storage Types - -**Files:** -- Modify: `include/iolinki_master/master.h` -- Modify: `src/master_internal.h` -- Modify: `src/master_port.c` -- Modify: `src/master_isdu.c` -- Modify: `tests/test_master_public_header.c` - -- [ ] Add failing public-header checks that `iolink_master_port_t` has no visible fields and remains stack allocatable. -- [ ] Move the current port/controller field layouts into private state structs in `src/master_internal.h`. -- [ ] Replace public structs with fixed-size aligned storage unions. -- [ ] Add compile-time checks that private state fits public storage. -- [ ] Update implementation files to convert public pointers to private state pointers before field access. -- [ ] Run `cmake --build build --target test_master_public_header`. - -### Task 2: Update Behavior Tests For The Private Boundary - -**Files:** -- Modify: `tests/test_master_startup.c` -- Modify: `tests/test_master_pd.c` -- Modify: `tests/test_master_isdu.c` -- Modify: `tests/test_master_controller.c` -- Modify: `tests/test_master_tick.c` -- Modify: `tests/test_master_parameters.c` - -- [ ] Replace public field reads with public getters where practical. -- [ ] Include `master_internal.h` only in tests that need deliberate internal state inspection. -- [ ] Keep existing behavior expectations unchanged. -- [ ] Run each focused test executable. - -### Task 3: Verify And Commit - -**Files:** -- Modify only files required by Tasks 1 and 2. - -- [ ] Run `cmake --build build`. -- [ ] Run `ctest --test-dir build --output-on-failure`. -- [ ] Run `git diff --check`. -- [ ] Commit with `refactor: hide master internals behind opaque storage`. diff --git a/third_party/iolinki-master/docs/superpowers/specs/2026-06-17-opaque-master-api-design.md b/third_party/iolinki-master/docs/superpowers/specs/2026-06-17-opaque-master-api-design.md deleted file mode 100644 index e8718dc7e..000000000 --- a/third_party/iolinki-master/docs/superpowers/specs/2026-06-17-opaque-master-api-design.md +++ /dev/null @@ -1,31 +0,0 @@ -# Opaque Master API Design - -## Goal - -Make the IO-Link master public API better architected by hiding port and controller internals while keeping embedded-friendly caller-owned storage. - -## Decision - -Use caller-owned opaque storage, not heap allocation. Public consumers keep declaring `iolink_master_port_t` and `iolink_master_controller_t`, but those types become fixed-size aligned storage unions. The real field layouts move to internal state structs visible only through `master_internal.h`. - -## Public API Shape - -The existing public functions remain stable. Callers still pass `iolink_master_port_t*` and `iolink_master_controller_t*` to init, tick, process, ISDU, PD, diagnostics, SIO, and controller APIs. - -Public code may no longer access fields such as `port.state`, `port.diagnostics`, `port.startup`, or `port.isdu`. Legitimate state is read through existing getters or through new narrow getters only when required by public behavior. - -## Internal Architecture - -`include/iolinki_master/master.h` owns public enums, public result structs, config structs, API prototypes, and opaque storage types. - -`src/master_internal.h` owns the private state structs and conversion helpers from public storage to internal state. Implementation files use internal state pointers for all field access. - -`src/master_port.c` remains responsible for lifecycle, startup, RX/TX, ticking, PD, diagnostics, DQ, parameters, and controller behavior in this slice. Later behavior splits can move parameters or SIO to separate files once the public/private boundary is clean. - -## Compatibility - -This is an API source break only for consumers that directly access public fields. It preserves stack/static allocation and avoids `malloc`, which keeps the API appropriate for MCU firmware. - -## Verification - -The public-header test must prove the public header no longer exposes struct fields. Existing behavior tests must keep passing after being updated to use public getters or private test access where they intentionally inspect internals. diff --git a/third_party/iolinki-master/examples/master_loopback_demo.c b/third_party/iolinki-master/examples/master_loopback_demo.c index b4eb9ed2e..8b22846f2 100644 --- a/third_party/iolinki-master/examples/master_loopback_demo.c +++ b/third_party/iolinki-master/examples/master_loopback_demo.c @@ -73,7 +73,10 @@ static int demo_phy_send(void* user, const uint8_t* data, size_t len) return (int)len; } - if((len == IOLINK_M_SEQ_TYPE0_LEN) && (data[0] == 0x00U)) + /* Startup probe (spec T1): Type-0 READ of a Direct Parameter octet on the + page channel (MC 0xA2). Answer with a valid 2-octet Type-0 frame. */ + if((len == IOLINK_M_SEQ_TYPE0_LEN) && ((data[0] & IOLINK_MC_RW_MASK) != 0U) && + ((data[0] & IOLINK_MC_COMM_CHANNEL_MASK) == 0x20U)) { response[0] = 0x00U; response[1] = iolink_checksum_ck(response[0], 0U); @@ -81,7 +84,10 @@ static int demo_phy_send(void* user, const uint8_t* data, size_t len) return (int)len; } - if((len == IOLINK_M_SEQ_TYPE0_LEN) && (data[0] == IOLINK_MC_TRANSITION_COMMAND)) + /* Transition to OPERATE: Type-0 DeviceOperate write (MC 0x20, OD 0x99); no + response per spec. */ + if((len == IOLINK_M_SEQ_MIN_LEN) && (data[0] == 0x20U) && + (data[1] == IOLINK_CMD_DEVICE_OPERATE)) { return (int)len; } diff --git a/third_party/iolinki-master/include/iolinki_master/master.h b/third_party/iolinki-master/include/iolinki_master/master.h index 34729dd85..f14984d79 100644 --- a/third_party/iolinki-master/include/iolinki_master/master.h +++ b/third_party/iolinki-master/include/iolinki_master/master.h @@ -78,9 +78,27 @@ typedef enum IOLINK_MASTER_PARAM_ERR_REVISION = -2, IOLINK_MASTER_PARAM_ERR_CYCLE_TIME = -3, IOLINK_MASTER_PARAM_ERR_PD_SIZE = -4, - IOLINK_MASTER_PARAM_ERR_M_SEQUENCE = -5 + IOLINK_MASTER_PARAM_ERR_M_SEQUENCE = -5, + IOLINK_MASTER_PARAM_ERR_VENDOR_ID = -6, + IOLINK_MASTER_PARAM_ERR_DEVICE_ID = -7 } iolink_master_parameter_result_t; +/* + * Device-identity inspection level, per the IO-Link port configuration model. + * NO_CHECK establishes communication without comparing the device identity. + * TYPE_COMP requires the connected device's VendorID and DeviceID to match the + * configured expected values (a type-compatible device). IDENTICAL additionally + * requires the device SerialNumber to match; the SerialNumber leg is not yet + * wired here (it lives in ISDU index 0x0015, not Direct Parameter Page 1), so + * IDENTICAL currently enforces the same VendorID/DeviceID check as TYPE_COMP. + */ +typedef enum +{ + IOLINK_MASTER_INSPECTION_NO_CHECK = 0, + IOLINK_MASTER_INSPECTION_TYPE_COMP = 1, + IOLINK_MASTER_INSPECTION_IDENTICAL = 2 +} iolink_master_inspection_level_t; + typedef enum { IOLINK_MASTER_ISDU_ERR_BUFFER_TOO_SMALL = -2, @@ -96,6 +114,41 @@ typedef enum IOLINK_MASTER_SIO_ERR_UNSUPPORTED_PHY = -3 } iolink_master_sio_result_t; +/* + * Master Command communication channel, per the IO-Link Master Command octet + * layout (`IOLINK_MC_COMM_CHANNEL_MASK`). The channel selects which logical + * page/diagnosis/ISDU register a Master Command addresses. + */ +typedef enum +{ + IOLINK_MASTER_MC_CHANNEL_PROCESS = 0U, + IOLINK_MASTER_MC_CHANNEL_PAGE = 1U, + IOLINK_MASTER_MC_CHANNEL_DIAGNOSIS = 2U, + IOLINK_MASTER_MC_CHANNEL_ISDU = 3U +} iolink_master_mc_channel_t; + +/* Address of the MasterCommand transition register (operate transition = 0x0F). */ +#define IOLINK_MASTER_MC_TRANSITION_ADDR 0x0FU + +typedef struct +{ + uint8_t qualifier; + iolink_master_event_type_t type; + uint16_t code; +} iolink_master_event_t; + +/* + * Optional event-dispatch callbacks. When set, they turn event handling from a + * poll-only model (read `diagnostics.event_pending` yourself) into a dispatch: + * `event_pending_handler` fires on the rising edge of the OD Event flag during a + * cyclic response, prompting the application to read event details; + * `event_handler` fires once per decoded event from + * `iolink_master_read_event_details`. Both may be NULL to keep poll-only + * behavior. `event_user` is passed through unchanged. + */ +typedef void (*iolink_master_event_pending_cb_t)(void* user); +typedef void (*iolink_master_event_cb_t)(void* user, const iolink_master_event_t* event); + typedef struct { iolink_master_port_mode_t port_mode; @@ -106,7 +159,22 @@ typedef struct uint8_t pd_out_len; bool auto_baudrate; bool validate_device_info; + iolink_master_inspection_level_t inspection_level; + uint16_t expected_vendor_id; + uint32_t expected_device_id; uint8_t response_timeout_100us; + /* + * Number of extra wake-up requests to issue at the current baudrate before + * giving up (auto-baud: advancing to the next COM rate; fixed baud: erroring). + * 0 preserves the historical "one attempt then advance/error" behavior; real + * hardware bring-up should set this to a small count (the spec allows the + * master to retry the wake-up sequence) so a device that misses the first + * WURQ still links up. + */ + uint8_t wake_retry_limit; + void* event_user; + iolink_master_event_pending_cb_t event_pending_handler; + iolink_master_event_cb_t event_handler; int (*set_mode_checked)(iolink_phy_mode_t mode); int (*set_baudrate_checked)(iolink_baudrate_t baudrate); int (*read_cq_line_checked)(void); @@ -150,6 +218,7 @@ typedef struct { bool valid; uint8_t min_cycle_time; + uint16_t min_cycle_time_100us; uint8_t mseq_capability; bool isdu_supported; uint8_t operate_mseq_code; @@ -163,22 +232,15 @@ typedef struct uint32_t device_id; } iolink_master_device_info_t; -typedef struct -{ - uint8_t qualifier; - iolink_master_event_type_t type; - uint16_t code; -} iolink_master_event_t; - /* * Opaque storage budgets keep the public ABI caller-owned and heap-free while * giving embedded users a fixed RAM ceiling to audit. Port storage carries the * protocol buffers and service state; controller storage only tracks a port * array reference plus port count. */ -#define IOLINK_MASTER_PORT_STORAGE_BUDGET_SIZE 1216U +#define IOLINK_MASTER_PORT_STORAGE_BUDGET_SIZE 1280U #define IOLINK_MASTER_CONTROLLER_STORAGE_BUDGET_SIZE 32U -#define IOLINK_MASTER_PORT_STORAGE_SIZE 1216U +#define IOLINK_MASTER_PORT_STORAGE_SIZE 1280U #define IOLINK_MASTER_CONTROLLER_STORAGE_SIZE 32U typedef union @@ -195,7 +257,18 @@ typedef union uint8_t storage[IOLINK_MASTER_CONTROLLER_STORAGE_SIZE]; } iolink_master_controller_t; -/* Returns OK, INVALID_ARG, or a nonzero PHY init error forwarded from phy->init. */ +/* + * Initializes a port for communication. + * + * Lifetime contract: the config is copied into the port, but the PHY is + * retained BY POINTER (the port stores `phy`, not a copy). The + * `iolink_phy_api_t` must therefore outlive the port — pass a pointer to + * storage with at least the port's lifetime, never an automatic/stack + * temporary. (Passing a stack-local PHY compiles fine but dangles on the next + * tick.) + * + * Returns OK, INVALID_ARG, or a nonzero PHY init error forwarded from phy->init. + */ int iolink_master_init(iolink_master_port_t* port, const iolink_phy_api_t* phy, const iolink_master_config_t* config); @@ -243,6 +316,24 @@ int iolink_master_set_dq(iolink_master_port_t* port, bool level); int iolink_master_get_di(const iolink_master_port_t* port, bool* level); /* Returns OK or INVALID_ARG; switching to IO-Link restarts startup on the port. */ int iolink_master_set_port_mode(iolink_master_port_t* port, iolink_master_port_mode_t mode); +/* + * Decodes a MinCycleTime/MasterCycleTime octet (Direct Parameter Page 1 byte + * 0x02) into 100us units per the IO-Link time-base encoding: bits 7-6 select the + * time base (00 = 0.1ms, 01 = 0.4ms, 10 = 1.6ms) and bits 5-0 the multiplier. + * Base 00 maps octet value directly to 100us units; base 01 = 6.4ms + n*0.4ms; + * base 10 = 32.0ms + n*1.6ms. The reserved base 11 falls back to the raw octet. + */ +uint16_t iolink_master_decode_min_cycle_time_100us(uint8_t octet); +/* Composes a Master Command octet from R/W direction, communication channel, and 5-bit address. */ +uint8_t iolink_master_encode_master_command(bool read, + iolink_master_mc_channel_t channel, + uint8_t address); +/* Returns true when the Master Command octet is a read (R/W bit set). */ +bool iolink_master_mc_is_read(uint8_t mc); +/* Returns the communication channel encoded in a Master Command octet. */ +iolink_master_mc_channel_t iolink_master_mc_channel(uint8_t mc); +/* Returns the 5-bit address encoded in a Master Command octet. */ +uint8_t iolink_master_mc_address(uint8_t mc); /* Returns OK, INVALID_ARG, or PARAM_TOO_SHORT. */ int iolink_master_parse_direct_parameter_page1(const uint8_t* page, uint8_t len, @@ -329,7 +420,14 @@ int iolink_master_write_parameter_block(iolink_master_port_t* port, uint8_t subindex, const uint8_t* data, uint8_t len); -/* Returns OK, INVALID_ARG, or the first per-port init error. */ +/* + * Initializes a multi-port controller by init-ing each port from the matching + * `phys[i]`/`configs[i]`. Same lifetime contract as iolink_master_init: every + * PHY is retained by pointer, so the `phys` array (and the PHYs it points to) + * must outlive the controller. The `ports` array must too. + * + * Returns OK, INVALID_ARG, or the first per-port init error. + */ int iolink_master_controller_init(iolink_master_controller_t* controller, iolink_master_port_t* ports, uint8_t port_count, diff --git a/third_party/iolinki-master/samples/iolink_master/CMakeLists.txt b/third_party/iolinki-master/samples/iolink_master/CMakeLists.txt new file mode 100644 index 000000000..a16ad9ef9 --- /dev/null +++ b/third_party/iolinki-master/samples/iolink_master/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright (C) 2026 Andrii Shylenko +# SPDX-License-Identifier: GPL-3.0-or-later + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(iolink_master) + +target_sources(app PRIVATE src/main.c) diff --git a/third_party/iolinki-master/samples/iolink_master/prj.conf b/third_party/iolinki-master/samples/iolink_master/prj.conf new file mode 100644 index 000000000..c255c6423 --- /dev/null +++ b/third_party/iolinki-master/samples/iolink_master/prj.conf @@ -0,0 +1,12 @@ +# Copyright (C) 2026 Andrii Shylenko +# SPDX-License-Identifier: GPL-3.0-or-later + +CONFIG_IOLINKI_MASTER=y + +# The master drives a trivial in-process fake PHY (see src/main.c), so the +# device-side stack is not needed here. Disabling it also keeps the shared +# frame/CRC helpers compiled by the master module only (no duplicate symbols). +CONFIG_IOLINKI=n + +CONFIG_LOG=y +CONFIG_MAIN_STACK_SIZE=4096 diff --git a/third_party/iolinki-master/samples/iolink_master/sample.yaml b/third_party/iolinki-master/samples/iolink_master/sample.yaml new file mode 100644 index 000000000..2a1387a25 --- /dev/null +++ b/third_party/iolinki-master/samples/iolink_master/sample.yaml @@ -0,0 +1,19 @@ +sample: + name: IO-Link Master + description: > + Minimal IO-Link master built on the iolinki-master stack. Initializes a + port, drives it through startup into OPERATE against a trivial in-process + fake PHY, and reads back one process-data cycle. +common: + tags: iolink fieldbus industrial + harness: console +tests: + sample.iolink.master.loopback: + platform_allow: + - native_sim + integration_platforms: + - native_sim + harness_config: + type: one_line + regex: + - "Starting iolinki IO-Link master sample" diff --git a/third_party/iolinki-master/samples/iolink_master/src/main.c b/third_party/iolinki-master/samples/iolink_master/src/main.c new file mode 100644 index 000000000..c9b6a82ce --- /dev/null +++ b/third_party/iolinki-master/samples/iolink_master/src/main.c @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2026 Andrii Shylenko + * SPDX-License-Identifier: GPL-3.0-or-later + * + * This file is part of iolinki-master. + * See LICENSE for details. + * + * Minimal IO-Link master sample for native_sim. + * + * The master: + * - drives a trivial in-process fake PHY (no hardware/devicetree needed), + * - initializes a port with a small fixed-PD configuration, + * - advances the link through startup into OPERATE, + * - reads back one process-data cycle and the OD status. + * + * The fake PHY answers the master's wake-up, Type-0 identification and cyclic + * requests just enough to reach OPERATE, mirroring examples/master_loopback_demo.c + * from the host build. Being pure C with no Zephyr APIs, it runs unchanged on + * native_sim. + */ + +#include +#include +#include + +#include "iolinki/crc.h" +#include "iolinki/frame.h" +#include "iolinki/protocol.h" +#include "iolinki_master/master.h" + +LOG_MODULE_REGISTER(iolink_master_sample, LOG_LEVEL_INF); + +static uint8_t g_rx_queue[64]; +static uint8_t g_rx_len; +static uint8_t g_rx_pos; +static iolink_baudrate_t g_baudrate; +static iolink_phy_mode_t g_mode; + +static void queue_bytes(const uint8_t* data, uint8_t len) +{ + memcpy(g_rx_queue, data, len); + g_rx_len = len; + g_rx_pos = 0U; +} + +static int demo_phy_init(void* user) +{ + (void)user; + g_rx_len = 0U; + g_rx_pos = 0U; + g_baudrate = IOLINK_BAUDRATE_COM3; + g_mode = IOLINK_PHY_MODE_INACTIVE; + return 0; +} + +static void demo_phy_set_mode(void* user, iolink_phy_mode_t mode) +{ + (void)user; + g_mode = mode; +} + +static void demo_phy_set_baudrate(void* user, iolink_baudrate_t baudrate) +{ + (void)user; + g_baudrate = baudrate; +} + +static int demo_phy_recv_byte(void* user, uint8_t* byte) +{ + (void)user; + if(byte == NULL) + { + return -1; + } + + if(g_rx_pos >= g_rx_len) + { + return 0; + } + + *byte = g_rx_queue[g_rx_pos++]; + return 1; +} + +static int demo_phy_send(void* user, const uint8_t* data, size_t len) +{ + uint8_t response[8] = {0U}; + + (void)user; + if((data == NULL) || (len == 0U)) + { + return -1; + } + + if((len == 1U) && (data[0] == 0x55U)) + { + return (int)len; + } + + if((len == IOLINK_M_SEQ_TYPE0_LEN) && (data[0] == 0x00U)) + { + response[0] = 0x00U; + response[1] = iolink_checksum_ck(response[0], 0U); + queue_bytes(response, 2U); + return (int)len; + } + + if((len == IOLINK_M_SEQ_TYPE0_LEN) && (data[0] == IOLINK_MC_TRANSITION_COMMAND)) + { + return (int)len; + } + + if(len == 6U) + { + response[0] = IOLINK_OD_STATUS_PD_VALID; + response[1] = 0x5AU; + response[2] = 0x00U; + response[3] = 0x00U; + response[4] = iolink_crc6(response, 4U); + queue_bytes(response, 5U); + return (int)len; + } + + return -1; +} + +static const iolink_phy_api_t g_demo_phy = { + .init = demo_phy_init, + .set_mode = demo_phy_set_mode, + .set_baudrate = demo_phy_set_baudrate, + .send = demo_phy_send, + .recv_byte = demo_phy_recv_byte, +}; + +int main(void) +{ + LOG_INF("Starting iolinki IO-Link master sample"); + + iolink_master_port_t port; + uint8_t pd_out[1] = {0x11U}; + uint8_t pd_in[1] = {0U}; + uint8_t pd_in_len = sizeof(pd_in); + uint8_t od_status = 0U; + iolink_master_config_t config = { + .port_mode = IOLINK_MASTER_PORT_MODE_IOLINK, + .m_seq_type = IOLINK_MASTER_M_SEQ_TYPE_2_1, + .baudrate = IOLINK_BAUDRATE_COM3, + .min_cycle_time = 20U, + .pd_in_len = sizeof(pd_in), + .pd_out_len = sizeof(pd_out), + .auto_baudrate = false, + }; + + if(iolink_master_init(&port, &g_demo_phy, &config) != 0) + { + LOG_ERR("iolink_master_init failed"); + return -1; + } + + if((g_mode != IOLINK_PHY_MODE_SDCI) || (g_baudrate != IOLINK_BAUDRATE_COM3)) + { + LOG_ERR("PHY not brought up into SDCI/COM3"); + return -1; + } + + if(iolink_master_set_pd_out(&port, pd_out, sizeof(pd_out)) != 0) + { + LOG_ERR("iolink_master_set_pd_out failed"); + return -1; + } + + /* Advance through startup identification into PREOPERATE. */ + iolink_master_process(&port); + iolink_master_process(&port); + (void)iolink_master_poll_rx(&port); + + if(iolink_master_get_state(&port) != IOLINK_MASTER_STATE_PREOPERATE) + { + LOG_ERR("port did not reach PREOPERATE"); + return -1; + } + + /* Transition into OPERATE and run one cyclic exchange. */ + iolink_master_process(&port); + if(iolink_master_get_state(&port) != IOLINK_MASTER_STATE_OPERATE) + { + LOG_ERR("port did not reach OPERATE"); + return -1; + } + + iolink_master_process(&port); + (void)iolink_master_poll_rx(&port); + + if(iolink_master_get_pd_in(&port, pd_in, sizeof(pd_in), &pd_in_len) != 0) + { + LOG_ERR("iolink_master_get_pd_in failed"); + return -1; + } + + (void)iolink_master_get_od_status(&port, &od_status); + + LOG_INF("Master reached state=%u pd_in=0x%02X od_status=0x%02X", + (unsigned)iolink_master_get_state(&port), + (unsigned)pd_in[0], + (unsigned)od_status); + + return 0; +} diff --git a/third_party/iolinki-master/src/master_controller.c b/third_party/iolinki-master/src/master_controller.c index d455322ae..ed6f9038a 100644 --- a/third_party/iolinki-master/src/master_controller.c +++ b/third_party/iolinki-master/src/master_controller.c @@ -18,7 +18,7 @@ int iolink_master_controller_init(iolink_master_controller_t* controller, return IOLINK_MASTER_ERR_INVALID_ARG; } - memset(controller, 0, sizeof(*controller)); + (void)memset(controller, 0, sizeof(*controller)); state = iolink_master_controller_state(controller); state->ports = ports; state->port_count = port_count; diff --git a/third_party/iolinki-master/src/master_internal.h b/third_party/iolinki-master/src/master_internal.h index 783ecd475..ab8d25806 100644 --- a/third_party/iolinki-master/src/master_internal.h +++ b/third_party/iolinki-master/src/master_internal.h @@ -3,10 +3,85 @@ #include "iolinki_master/master.h" +/* + * Named constants for the master stack. These are master-owned (they intentionally + * do not modify the shared device-stack protocol.h); values that already have a + * name in iolinki/protocol.h are reused rather than redefined here. + */ + +/* RX/TX scratch buffer size; must hold the worst-case operate frame. */ +#define IOLINK_MASTER_FRAME_BUF_SIZE 64U +/* Checksum/response retry budget before entering the error state. */ +#define IOLINK_MASTER_RX_RETRY_LIMIT 2U +/* Wake-up request pattern (alternating bits) emitted when no wake_up hook is set. */ +#define IOLINK_MASTER_WAKEUP_BYTE 0x55U + +/* Direct Parameter Page 1 wire layout (see IO-Link spec Table B.1). */ +#define IOLINK_MASTER_DPP1_LEN 16U +#define IOLINK_MASTER_DPP1_OFF_MASTER_COMMAND 0x00U +#define IOLINK_MASTER_DPP1_OFF_MIN_CYCLE_TIME 0x02U +#define IOLINK_MASTER_DPP1_OFF_MSEQ_CAPABILITY 0x03U +#define IOLINK_MASTER_DPP1_OFF_REVISION_ID 0x04U +#define IOLINK_MASTER_DPP1_OFF_PD_IN_DESC 0x05U +#define IOLINK_MASTER_DPP1_OFF_PD_OUT_DESC 0x06U +#define IOLINK_MASTER_DPP1_OFF_VENDOR_ID_HI 0x07U +#define IOLINK_MASTER_DPP1_OFF_VENDOR_ID_LO 0x08U +#define IOLINK_MASTER_DPP1_OFF_DEVICE_ID_HI 0x09U +#define IOLINK_MASTER_DPP1_OFF_DEVICE_ID_MID 0x0AU +#define IOLINK_MASTER_DPP1_OFF_DEVICE_ID_LO 0x0BU + +/* IO-Link protocol revision IDs (RevisionID octet, Figure B.4). */ +#define IOLINK_MASTER_REVISION_1_0 0x10U +#define IOLINK_MASTER_REVISION_1_1 0x11U + +/* MinCycleTime octet fields (Figure B.2 / Table B.3). */ +#define IOLINK_MASTER_MIN_CYCLE_BASE_SHIFT 6U +#define IOLINK_MASTER_MIN_CYCLE_BASE_MASK 0x03U +#define IOLINK_MASTER_MIN_CYCLE_MULT_MASK 0x3FU + +/* M-sequenceCapability octet bit fields (Figure B.3). */ +#define IOLINK_MASTER_MSEQ_CAP_ISDU_BIT 0x01U +#define IOLINK_MASTER_MSEQ_CAP_OPERATE_SHIFT 1U +#define IOLINK_MASTER_MSEQ_CAP_OPERATE_MASK 0x07U +#define IOLINK_MASTER_MSEQ_CAP_PREOP_SHIFT 4U +#define IOLINK_MASTER_MSEQ_CAP_PREOP_MASK 0x03U + +/* ProcessData descriptor octet fields (Figure B.5 / Table B.6). */ +#define IOLINK_MASTER_PD_DESC_BYTE_BIT 0x80U +#define IOLINK_MASTER_PD_DESC_LENGTH_MASK 0x1FU +#define IOLINK_MASTER_PD_DESC_BITS_PER_OCTET 8U + +/* Master Command comm-channel field position (pairs with IOLINK_MC_COMM_CHANNEL_MASK). */ +#define IOLINK_MASTER_MC_COMM_CHANNEL_SHIFT 5U + +/* ISDU framing. */ +#define IOLINK_MASTER_ISDU_SERVICE_SHIFT 4U +#define IOLINK_MASTER_ISDU_RESPONSE_ERROR 0x80U +#define IOLINK_MASTER_ISDU_LENGTH_NIBBLE_MAX 15U +#define IOLINK_MASTER_ISDU_LENGTH_EXTENDED 0x0FU +#define IOLINK_MASTER_ISDU_WRITE_HEADER_MAX 5U +#define IOLINK_MASTER_ISDU_READ_HEADER_LEN 4U + +/* Data Storage record header + event-entry framing. */ +#define IOLINK_MASTER_DS_RECORD_HEADER_LEN 4U +#define IOLINK_MASTER_EVENT_ENTRY_LEN 3U +#define IOLINK_MASTER_MAX_EVENTS 8U +#define IOLINK_MASTER_EVENT_QUALIFIER_MODE_SHIFT 4U +#define IOLINK_MASTER_EVENT_QUALIFIER_MODE_MASK 0x03U +#define IOLINK_MASTER_EVENT_MODE_NOTIFICATION 1U +#define IOLINK_MASTER_EVENT_MODE_WARNING 2U +#define IOLINK_MASTER_EVENT_MODE_ERROR 3U + +/* Startup micro-sequence steps (state of iolink_master_startup_state_t.step). */ +#define IOLINK_MASTER_STARTUP_STEP_WAKE 0U +#define IOLINK_MASTER_STARTUP_STEP_SEND_TYPE0 1U +#define IOLINK_MASTER_STARTUP_STEP_AWAIT_RESPONSE 2U + typedef struct { uint8_t step; uint8_t baudrate_index; + uint8_t wake_attempts; } iolink_master_startup_state_t; typedef struct @@ -31,7 +106,7 @@ typedef struct typedef struct { - uint8_t buf[64]; + uint8_t buf[IOLINK_MASTER_FRAME_BUF_SIZE]; uint8_t len; } iolink_master_rx_state_t; @@ -59,7 +134,7 @@ typedef struct iolink_master_config_t config; iolink_master_state_t state; uint8_t od_len; - uint8_t tx_buf[64]; + uint8_t tx_buf[IOLINK_MASTER_FRAME_BUF_SIZE]; uint8_t pd_in[IOLINK_PD_IN_MAX_SIZE]; uint8_t pd_in_len; uint8_t pd_out[IOLINK_PD_OUT_MAX_SIZE]; @@ -89,6 +164,14 @@ typedef char iolink_master_port_storage_must_fit typedef char iolink_master_controller_storage_must_fit [(sizeof(iolink_master_controller_state_t) <= IOLINK_MASTER_CONTROLLER_STORAGE_SIZE) ? 1 : -1]; +/* + * These accessors reinterpret the caller-owned opaque storage as the private + * state struct. The `void*` cast is a deliberate, documented deviation from + * MISRA C:2012 Rule 11.5: the public ABI keeps the state opaque and heap-free, + * and the `_storage_must_fit` static asserts above guarantee the storage is + * large enough (and the union alignment members in master.h guarantee + * alignment). See docs/MISRA_DEVIATIONS.md. + */ static inline iolink_master_port_state_t* iolink_master_port_state(iolink_master_port_t* port) { return (iolink_master_port_state_t*)(void*)port->storage; diff --git a/third_party/iolinki-master/src/master_isdu.c b/third_party/iolinki-master/src/master_isdu.c index 808c61884..f0859410e 100644 --- a/third_party/iolinki-master/src/master_isdu.c +++ b/third_party/iolinki-master/src/master_isdu.c @@ -84,7 +84,7 @@ static int iolink_master_isdu_finish_read(iolink_master_port_t* port, return iolink_master_service_result(port, IOLINK_MASTER_ISDU_ERR_DEVICE); } - if((result_len >= 2U) && (iolink_master_port_state(port)->isdu.response[0] == 0x80U)) + if((result_len >= 2U) && (iolink_master_port_state(port)->isdu.response[0] == IOLINK_MASTER_ISDU_RESPONSE_ERROR)) { iolink_master_port_state(port)->isdu.error = iolink_master_port_state(port)->isdu.response[1]; iolink_master_port_state(port)->diagnostics.last_isdu_error = @@ -101,9 +101,10 @@ static int iolink_master_isdu_finish_read(iolink_master_port_t* port, if(result_len > 0U) { - memcpy(data, iolink_master_port_state(port)->isdu.response, result_len); + (void)memcpy(data, iolink_master_port_state(port)->isdu.response, result_len); } - *len = result_len; + /* Guarded above by `*len < result_len`, so result_len fits in the uint8 out-length. */ + *len = (uint8_t)result_len; iolink_master_isdu_clear(port); return iolink_master_service_result(port, IOLINK_MASTER_STATUS_OK); } @@ -118,7 +119,7 @@ static int iolink_master_isdu_finish_write(iolink_master_port_t* port) return iolink_master_service_result(port, IOLINK_MASTER_ISDU_ERR_DEVICE); } - if((iolink_master_port_state(port)->isdu.response_len >= 2U) && (iolink_master_port_state(port)->isdu.response[0] == 0x80U)) + if((iolink_master_port_state(port)->isdu.response_len >= 2U) && (iolink_master_port_state(port)->isdu.response[0] == IOLINK_MASTER_ISDU_RESPONSE_ERROR)) { iolink_master_port_state(port)->isdu.error = iolink_master_port_state(port)->isdu.response[1]; iolink_master_port_state(port)->diagnostics.last_isdu_error = @@ -141,7 +142,7 @@ void iolink_master_isdu_fill_od(iolink_master_port_t* port, uint8_t* od, uint8_t return; } - memset(od, 0, od_len); + (void)memset(od, 0, od_len); if((iolink_master_port_state(port)->isdu.op == IOLINK_MASTER_ISDU_OP_NONE) || iolink_master_port_state(port)->isdu.request_sent) { @@ -289,18 +290,18 @@ int iolink_master_read_isdu(iolink_master_port_t* port, } iolink_master_isdu_start(port, IOLINK_MASTER_ISDU_OP_READ, index, subindex); - iolink_master_port_state(port)->isdu.request[0] = (uint8_t)(IOLINK_ISDU_SERVICE_READ << 4); + iolink_master_port_state(port)->isdu.request[0] = (uint8_t)(IOLINK_ISDU_SERVICE_READ << IOLINK_MASTER_ISDU_SERVICE_SHIFT); iolink_master_port_state(port)->isdu.request[1] = (uint8_t)(index >> 8); iolink_master_port_state(port)->isdu.request[2] = (uint8_t)(index & 0xFFU); iolink_master_port_state(port)->isdu.request[3] = subindex; - iolink_master_port_state(port)->isdu.request_len = 4U; + iolink_master_port_state(port)->isdu.request_len = IOLINK_MASTER_ISDU_READ_HEADER_LEN; return IOLINK_MASTER_STATUS_PENDING; } int iolink_master_read_device_info(iolink_master_port_t* port) { - uint8_t page[16]; + uint8_t page[IOLINK_MASTER_DPP1_LEN]; uint8_t len = sizeof(page); int ret; @@ -361,21 +362,21 @@ int iolink_master_write_isdu(iolink_master_port_t* port, return iolink_master_isdu_finish_write(port); } - if(len > (uint8_t)(IOLINK_ISDU_BUFFER_SIZE - 5U)) + if(len > (uint8_t)(IOLINK_ISDU_BUFFER_SIZE - IOLINK_MASTER_ISDU_WRITE_HEADER_MAX)) { return IOLINK_MASTER_ISDU_ERR_BUFFER_TOO_SMALL; } iolink_master_isdu_start(port, IOLINK_MASTER_ISDU_OP_WRITE, index, subindex); - if(len >= 15U) + if(len >= IOLINK_MASTER_ISDU_LENGTH_NIBBLE_MAX) { - iolink_master_port_state(port)->isdu.request[pos++] = (uint8_t)((IOLINK_ISDU_SERVICE_WRITE << 4) | 0x0FU); + iolink_master_port_state(port)->isdu.request[pos++] = (uint8_t)((IOLINK_ISDU_SERVICE_WRITE << IOLINK_MASTER_ISDU_SERVICE_SHIFT) | IOLINK_MASTER_ISDU_LENGTH_EXTENDED); iolink_master_port_state(port)->isdu.request[pos++] = len; } else { - iolink_master_port_state(port)->isdu.request[pos++] = (uint8_t)((IOLINK_ISDU_SERVICE_WRITE << 4) | len); + iolink_master_port_state(port)->isdu.request[pos++] = (uint8_t)((IOLINK_ISDU_SERVICE_WRITE << IOLINK_MASTER_ISDU_SERVICE_SHIFT) | len); } iolink_master_port_state(port)->isdu.request[pos++] = (uint8_t)(index >> 8); @@ -384,7 +385,7 @@ int iolink_master_write_isdu(iolink_master_port_t* port, if(len > 0U) { - memcpy(&iolink_master_port_state(port)->isdu.request[pos], data, len); + (void)memcpy(&iolink_master_port_state(port)->isdu.request[pos], data, len); pos = (uint8_t)(pos + len); } @@ -450,19 +451,19 @@ static bool iolink_master_ds_next_record(const uint8_t* data, uint8_t value_len; if((data == NULL) || (pos == NULL) || (record == NULL) || (record_len == NULL) || - (*pos > len) || ((uint8_t)(len - *pos) < 4U)) + (*pos > len) || ((uint8_t)(len - *pos) < IOLINK_MASTER_DS_RECORD_HEADER_LEN)) { return false; } - value_len = data[(uint8_t)(*pos + 3U)]; - if(value_len > (uint8_t)(len - *pos - 4U)) + value_len = data[(uint8_t)(*pos + (IOLINK_MASTER_DS_RECORD_HEADER_LEN - 1U))]; + if(value_len > (uint8_t)(len - *pos - IOLINK_MASTER_DS_RECORD_HEADER_LEN)) { return false; } *record = &data[*pos]; - *record_len = (uint8_t)(4U + value_len); + *record_len = (uint8_t)(IOLINK_MASTER_DS_RECORD_HEADER_LEN + value_len); *pos = (uint8_t)(*pos + *record_len); return true; } @@ -582,6 +583,10 @@ int iolink_master_verify_data_storage(iolink_master_port_t* port, { return iolink_master_service_result(port, IOLINK_MASTER_ISDU_ERR_VERIFY_FAILED); } + else + { + /* Raw image matched: fall through to the success result. */ + } return iolink_master_service_result(port, IOLINK_MASTER_STATUS_OK); } @@ -627,13 +632,13 @@ int iolink_master_ack_event(iolink_master_port_t* port, uint16_t* event_code) static iolink_master_event_type_t iolink_master_event_type_from_qualifier(uint8_t qualifier) { - switch((uint8_t)((qualifier >> 4U) & 0x03U)) + switch((uint8_t)((qualifier >> IOLINK_MASTER_EVENT_QUALIFIER_MODE_SHIFT) & IOLINK_MASTER_EVENT_QUALIFIER_MODE_MASK)) { - case 1U: + case IOLINK_MASTER_EVENT_MODE_NOTIFICATION: return IOLINK_MASTER_EVENT_TYPE_NOTIFICATION; - case 2U: + case IOLINK_MASTER_EVENT_MODE_WARNING: return IOLINK_MASTER_EVENT_TYPE_WARNING; - case 3U: + case IOLINK_MASTER_EVENT_MODE_ERROR: return IOLINK_MASTER_EVENT_TYPE_ERROR; default: return IOLINK_MASTER_EVENT_TYPE_UNKNOWN; @@ -645,7 +650,7 @@ int iolink_master_read_event_details(iolink_master_port_t* port, uint8_t max_events, uint8_t* out_count) { - uint8_t data[24] = {0U}; + uint8_t data[IOLINK_MASTER_MAX_EVENTS * IOLINK_MASTER_EVENT_ENTRY_LEN] = {0U}; uint8_t len = sizeof(data); uint8_t count; uint8_t i; @@ -662,12 +667,12 @@ int iolink_master_read_event_details(iolink_master_port_t* port, return ret; } - if((len % 3U) != 0U) + if((len % IOLINK_MASTER_EVENT_ENTRY_LEN) != 0U) { return IOLINK_MASTER_ISDU_ERR_DEVICE; } - count = (uint8_t)(len / 3U); + count = (uint8_t)(len / IOLINK_MASTER_EVENT_ENTRY_LEN); *out_count = count; iolink_master_port_state(port)->diagnostics.last_event_count = count; iolink_master_port_state(port)->diagnostics.last_event_code = 0U; @@ -678,16 +683,25 @@ int iolink_master_read_event_details(iolink_master_port_t* port, for(i = 0U; i < count; i++) { - events[i].qualifier = data[i * 3U]; + events[i].qualifier = data[i * IOLINK_MASTER_EVENT_ENTRY_LEN]; events[i].type = iolink_master_event_type_from_qualifier(events[i].qualifier); - events[i].code = (uint16_t)(((uint16_t)data[(i * 3U) + 1U] << 8U) | - data[(i * 3U) + 2U]); + events[i].code = (uint16_t)(((uint16_t)data[(i * IOLINK_MASTER_EVENT_ENTRY_LEN) + 1U] << 8U) | + data[(i * IOLINK_MASTER_EVENT_ENTRY_LEN) + 2U]); } if(count > 0U) { iolink_master_port_state(port)->diagnostics.last_event_code = events[count - 1U].code; } + if(iolink_master_port_state(port)->config.event_handler != NULL) + { + for(i = 0U; i < count; i++) + { + iolink_master_port_state(port)->config.event_handler( + iolink_master_port_state(port)->config.event_user, &events[i]); + } + } + return IOLINK_MASTER_STATUS_OK; } @@ -735,7 +749,7 @@ static bool iolink_master_block_matches(const iolink_master_port_t* port, static void iolink_master_block_clear(iolink_master_port_t* port) { - memset(&iolink_master_port_state(port)->block, 0, sizeof(iolink_master_port_state(port)->block)); + (void)memset(&iolink_master_port_state(port)->block, 0, sizeof(iolink_master_port_state(port)->block)); } int iolink_master_write_parameter_block(iolink_master_port_t* port, @@ -752,7 +766,7 @@ int iolink_master_write_parameter_block(iolink_master_port_t* port, return IOLINK_MASTER_ERR_INVALID_ARG; } - if(len > (uint8_t)(IOLINK_ISDU_BUFFER_SIZE - 5U)) + if(len > (uint8_t)(IOLINK_ISDU_BUFFER_SIZE - IOLINK_MASTER_ISDU_WRITE_HEADER_MAX)) { return IOLINK_MASTER_ISDU_ERR_BUFFER_TOO_SMALL; } @@ -766,13 +780,17 @@ int iolink_master_write_parameter_block(iolink_master_port_t* port, block->len = len; if(len > 0U) { - memcpy(block->data, data, len); + (void)memcpy(block->data, data, len); } } else if(!iolink_master_block_matches(port, index, subindex, data, len)) { return iolink_master_service_result(port, IOLINK_MASTER_ISDU_ERR_BUSY); } + else + { + /* Resuming the same block transfer: keep the latched state. */ + } if(block->step == IOLINK_MASTER_BLOCK_STEP_BEGIN_DOWNLOAD) { @@ -820,6 +838,10 @@ int iolink_master_write_parameter_block(iolink_master_port_t* port, { iolink_master_block_clear(port); } + else + { + /* Still pending: keep the block state for the next call. */ + } return iolink_master_service_result(port, ret); } diff --git a/third_party/iolinki-master/src/master_parameters.c b/third_party/iolinki-master/src/master_parameters.c index 7032706b4..eaab194d4 100644 --- a/third_party/iolinki-master/src/master_parameters.c +++ b/third_party/iolinki-master/src/master_parameters.c @@ -1,20 +1,86 @@ #include "master_internal.h" +#include "iolinki/protocol.h" + #include -static uint8_t iolink_master_decode_pd_descriptor(uint8_t descriptor) +uint16_t iolink_master_decode_min_cycle_time_100us(uint8_t octet) { - if(descriptor == 0U) + uint8_t time_base = + (uint8_t)((octet >> IOLINK_MASTER_MIN_CYCLE_BASE_SHIFT) & IOLINK_MASTER_MIN_CYCLE_BASE_MASK); + uint8_t multiplier = (uint8_t)(octet & IOLINK_MASTER_MIN_CYCLE_MULT_MASK); + + switch(time_base) { - return 0U; + case 0U: + /* 0.1 ms base: octet value is already the 100us count (0..6.3 ms). */ + return multiplier; + case 1U: + /* 0.4 ms base, offset 6.4 ms: 64 + n*4 (100us units), 6.4..31.6 ms. */ + return (uint16_t)(64U + ((uint16_t)multiplier * 4U)); + case 2U: + /* 1.6 ms base, offset 32.0 ms: 320 + n*16 (100us units), 32.0..132.8 ms. */ + return (uint16_t)(320U + ((uint16_t)multiplier * 16U)); + default: + /* Reserved base 11: fall back to the raw octet rather than inventing timing. */ + return octet; } +} - if((descriptor & 0x80U) != 0U) +uint8_t iolink_master_encode_master_command(bool read, + iolink_master_mc_channel_t channel, + uint8_t address) +{ + uint8_t mc = (uint8_t)(address & IOLINK_MC_ADDR_MASK); + + mc = (uint8_t)(mc | ((uint8_t)((uint8_t)channel << IOLINK_MASTER_MC_COMM_CHANNEL_SHIFT) & + IOLINK_MC_COMM_CHANNEL_MASK)); + if(read) { - return (uint8_t)((descriptor & 0x7FU) + 1U); + mc = (uint8_t)(mc | IOLINK_MC_RW_MASK); } - return (uint8_t)(descriptor / 8U); + return mc; +} + +bool iolink_master_mc_is_read(uint8_t mc) +{ + return (mc & IOLINK_MC_RW_MASK) != 0U; +} + +iolink_master_mc_channel_t iolink_master_mc_channel(uint8_t mc) +{ + return (iolink_master_mc_channel_t)((mc & IOLINK_MC_COMM_CHANNEL_MASK) >> + IOLINK_MASTER_MC_COMM_CHANNEL_SHIFT); +} + +uint8_t iolink_master_mc_address(uint8_t mc) +{ + return (uint8_t)(mc & IOLINK_MC_ADDR_MASK); +} + +static uint8_t iolink_master_decode_pd_descriptor(uint8_t descriptor) +{ + /* + * ProcessData descriptor (Direct Parameter Page 1, Figure B.5 / Table B.6): + * bit 7 = BYTE (length unit: 1 = octets, 0 = bits) + * bit 6 = SIO (switching signal available in SIO mode) + * bit 5 = reserved + * bits 0-4 = Length + * Only bits 0-4 carry the length, so the SIO/reserved bits must be masked + * off before decoding (a SIO-capable device sets bit 6 legally). + */ + uint8_t length = (uint8_t)(descriptor & IOLINK_MASTER_PD_DESC_LENGTH_MASK); + + if((descriptor & IOLINK_MASTER_PD_DESC_BYTE_BIT) != 0U) + { + /* BYTE = 1: octets. Table B.6 maps Length code n to (n + 1) octets. */ + return (uint8_t)(length + 1U); + } + + /* BYTE = 0: Length is in bits (0..16); round up to whole octets. */ + return (uint8_t)((length + (IOLINK_MASTER_PD_DESC_BITS_PER_OCTET - 1U)) / + IOLINK_MASTER_PD_DESC_BITS_PER_OCTET); } static uint8_t iolink_master_mseq_capability_code(iolink_master_m_seq_type_t type) @@ -81,26 +147,36 @@ int iolink_master_parse_direct_parameter_page1(const uint8_t* page, return IOLINK_MASTER_ERR_INVALID_ARG; } - if(len < 16U) + if(len < IOLINK_MASTER_DPP1_LEN) { return IOLINK_MASTER_PARAM_ERR_TOO_SHORT; } - memset(info, 0, sizeof(*info)); + (void)memset(info, 0, sizeof(*info)); info->valid = true; - info->min_cycle_time = page[0x02]; - info->mseq_capability = page[0x03]; - info->isdu_supported = ((page[0x03] & 0x01U) != 0U); - info->operate_mseq_code = (uint8_t)((page[0x03] >> 1U) & 0x07U); - info->preoperate_mseq_code = (uint8_t)((page[0x03] >> 4U) & 0x03U); - info->revision_id = page[0x04]; - info->pd_in_descriptor = page[0x05]; - info->pd_out_descriptor = page[0x06]; - info->pd_in_len = iolink_master_decode_pd_descriptor(page[0x05]); - info->pd_out_len = iolink_master_decode_pd_descriptor(page[0x06]); - info->vendor_id = (uint16_t)(((uint16_t)page[0x07] << 8U) | page[0x08]); - info->device_id = ((uint32_t)page[0x09] << 16U) | ((uint32_t)page[0x0A] << 8U) | - (uint32_t)page[0x0B]; + info->min_cycle_time = page[IOLINK_MASTER_DPP1_OFF_MIN_CYCLE_TIME]; + info->min_cycle_time_100us = + iolink_master_decode_min_cycle_time_100us(page[IOLINK_MASTER_DPP1_OFF_MIN_CYCLE_TIME]); + info->mseq_capability = page[IOLINK_MASTER_DPP1_OFF_MSEQ_CAPABILITY]; + info->isdu_supported = + ((page[IOLINK_MASTER_DPP1_OFF_MSEQ_CAPABILITY] & IOLINK_MASTER_MSEQ_CAP_ISDU_BIT) != 0U); + info->operate_mseq_code = + (uint8_t)((page[IOLINK_MASTER_DPP1_OFF_MSEQ_CAPABILITY] >> + IOLINK_MASTER_MSEQ_CAP_OPERATE_SHIFT) & IOLINK_MASTER_MSEQ_CAP_OPERATE_MASK); + info->preoperate_mseq_code = + (uint8_t)((page[IOLINK_MASTER_DPP1_OFF_MSEQ_CAPABILITY] >> + IOLINK_MASTER_MSEQ_CAP_PREOP_SHIFT) & IOLINK_MASTER_MSEQ_CAP_PREOP_MASK); + info->revision_id = page[IOLINK_MASTER_DPP1_OFF_REVISION_ID]; + info->pd_in_descriptor = page[IOLINK_MASTER_DPP1_OFF_PD_IN_DESC]; + info->pd_out_descriptor = page[IOLINK_MASTER_DPP1_OFF_PD_OUT_DESC]; + info->pd_in_len = iolink_master_decode_pd_descriptor(page[IOLINK_MASTER_DPP1_OFF_PD_IN_DESC]); + info->pd_out_len = iolink_master_decode_pd_descriptor(page[IOLINK_MASTER_DPP1_OFF_PD_OUT_DESC]); + info->vendor_id = + (uint16_t)(((uint16_t)page[IOLINK_MASTER_DPP1_OFF_VENDOR_ID_HI] << 8U) | + page[IOLINK_MASTER_DPP1_OFF_VENDOR_ID_LO]); + info->device_id = ((uint32_t)page[IOLINK_MASTER_DPP1_OFF_DEVICE_ID_HI] << 16U) | + ((uint32_t)page[IOLINK_MASTER_DPP1_OFF_DEVICE_ID_MID] << 8U) | + (uint32_t)page[IOLINK_MASTER_DPP1_OFF_DEVICE_ID_LO]; return IOLINK_MASTER_STATUS_OK; } @@ -165,12 +241,19 @@ int iolink_master_validate_config_against_device_info(const iolink_master_device return IOLINK_MASTER_STATUS_PENDING; } - if((info->revision_id != 0x10U) && (info->revision_id != 0x11U)) + if((info->revision_id != IOLINK_MASTER_REVISION_1_0) && + (info->revision_id != IOLINK_MASTER_REVISION_1_1)) { return IOLINK_MASTER_PARAM_ERR_REVISION; } - if(config->min_cycle_time < info->min_cycle_time) + /* + * The master's configured cycle time (raw 100us count) must be at least the + * device's decoded MinCycleTime. Comparing decoded 100us values is what makes + * devices that report a non-zero time base (0.4 / 1.6 ms) time correctly; + * for the common 0.1 ms base the decoded value equals the raw octet. + */ + if((uint16_t)config->min_cycle_time < info->min_cycle_time_100us) { return IOLINK_MASTER_PARAM_ERR_CYCLE_TIME; } @@ -185,6 +268,24 @@ int iolink_master_validate_config_against_device_info(const iolink_master_device return IOLINK_MASTER_PARAM_ERR_M_SEQUENCE; } + /* + * Device identity check. Any inspection level other than NO_CHECK rejects a + * device whose VendorID/DeviceID differ from the configured expected values. + * The SerialNumber leg that distinguishes IDENTICAL from TYPE_COMP is a + * documented follow-up (it is not carried in Direct Parameter Page 1). + */ + if(config->inspection_level != IOLINK_MASTER_INSPECTION_NO_CHECK) + { + if(info->vendor_id != config->expected_vendor_id) + { + return IOLINK_MASTER_PARAM_ERR_VENDOR_ID; + } + if(info->device_id != config->expected_device_id) + { + return IOLINK_MASTER_PARAM_ERR_DEVICE_ID; + } + } + return IOLINK_MASTER_STATUS_OK; } @@ -213,7 +314,15 @@ int iolink_master_select_config_from_device_info(const iolink_master_device_info } config->m_seq_type = m_seq_type; - config->min_cycle_time = info->min_cycle_time; + /* + * Adopt the device's decoded MinCycleTime as the port cycle time (100us + * units). Clamp to the uint8 config field: a device whose minimum exceeds + * 25.5 ms cannot be paced by this field and will subsequently fail + * validation, which is the honest outcome rather than silently wrapping. + */ + config->min_cycle_time = (info->min_cycle_time_100us > (uint16_t)UINT8_MAX) + ? UINT8_MAX + : (uint8_t)info->min_cycle_time_100us; config->pd_in_len = info->pd_in_len; config->pd_out_len = info->pd_out_len; diff --git a/third_party/iolinki-master/src/master_port.c b/third_party/iolinki-master/src/master_port.c index 86e6d3763..8410d684f 100644 --- a/third_party/iolinki-master/src/master_port.c +++ b/third_party/iolinki-master/src/master_port.c @@ -12,6 +12,12 @@ static const iolink_baudrate_t g_iolink_master_baudrate_scan[] = { IOLINK_BAUDRATE_COM1, }; +static bool iolink_master_startup_validation_required(const iolink_master_config_t* config) +{ + return config->validate_device_info || + (config->inspection_level != IOLINK_MASTER_INSPECTION_NO_CHECK); +} + static iolink_baudrate_t iolink_master_startup_baudrate(const iolink_master_port_t* port) { const iolink_master_port_state_t* state = iolink_master_port_const_state(port); @@ -128,7 +134,7 @@ static bool iolink_master_wake_up(iolink_master_port_t* port) if(state->config.wake_up == NULL) { - state->tx_buf[0] = 0x55U; + state->tx_buf[0] = IOLINK_MASTER_WAKEUP_BYTE; return iolink_master_send_full(port, state->tx_buf, 1U); } @@ -283,6 +289,9 @@ static int iolink_master_tick_common(iolink_master_port_t* port, cycle_count_before = state->cycle_count; iolink_master_process(port); + /* iolink_master_process() increments cycle_count through the port pointer on a + successful operate send; cppcheck does not model that side effect. */ + /* cppcheck-suppress knownConditionTrueFalse */ if(pace_cycles && (state->cycle_count != cycle_count_before)) { uint32_t jitter_100us = iolink_master_cycle_jitter_at(port, now_100us); @@ -325,7 +334,7 @@ int iolink_master_init(iolink_master_port_t* port, return IOLINK_MASTER_ERR_INVALID_ARG; } - memset(port, 0, sizeof(*port)); + (void)memset(port, 0, sizeof(*port)); iolink_master_port_state(port)->phy = phy; iolink_master_port_state(port)->config = *config; iolink_master_port_state(port)->od_len = iolink_master_od_len_for_type(config->m_seq_type); @@ -474,7 +483,7 @@ int iolink_master_on_timeout(iolink_master_port_t* port) if(iolink_master_port_state(port)->state == IOLINK_MASTER_STATE_OPERATE) { iolink_master_port_state(port)->diagnostics.response_timeouts++; - if(iolink_master_port_state(port)->diagnostics.rx_retry_count < 2U) + if(iolink_master_port_state(port)->diagnostics.rx_retry_count < IOLINK_MASTER_RX_RETRY_LIMIT) { iolink_master_port_state(port)->diagnostics.rx_retry_count++; return IOLINK_MASTER_STATUS_PENDING; @@ -492,6 +501,20 @@ int iolink_master_on_timeout(iolink_master_port_t* port) return IOLINK_MASTER_ERR_INVALID_ARG; } + /* + * Re-issue the wake-up request at the current baudrate before giving up on + * it. A device can miss the first WURQ pulse; retrying the wake sequence is + * spec-permitted and lets a slow-to-wake device still link up. Only advance + * the baud scan (or error) once the per-baud wake budget is exhausted. + */ + if(iolink_master_port_state(port)->startup.wake_attempts < + iolink_master_port_state(port)->config.wake_retry_limit) + { + iolink_master_port_state(port)->startup.wake_attempts++; + iolink_master_port_state(port)->startup.step = 0U; + return IOLINK_MASTER_STATUS_PENDING; + } + if(iolink_master_port_state(port)->config.auto_baudrate && (iolink_master_port_state(port)->startup.baudrate_index < (uint8_t)((sizeof(g_iolink_master_baudrate_scan) / @@ -500,6 +523,7 @@ int iolink_master_on_timeout(iolink_master_port_t* port) { iolink_master_port_state(port)->startup.baudrate_index++; iolink_master_port_state(port)->startup.step = 0U; + iolink_master_port_state(port)->startup.wake_attempts = 0U; ret = iolink_master_set_baudrate(port, iolink_master_startup_baudrate(port)); if(ret != IOLINK_MASTER_STATUS_OK) { @@ -545,7 +569,7 @@ void iolink_master_process(iolink_master_port_t* port) if(iolink_master_port_state(port)->state == IOLINK_MASTER_STATE_STARTUP) { - if(iolink_master_port_state(port)->startup.step == 0U) + if(iolink_master_port_state(port)->startup.step == IOLINK_MASTER_STARTUP_STEP_WAKE) { if(iolink_master_wake_up(port)) { @@ -554,9 +578,17 @@ void iolink_master_process(iolink_master_port_t* port) return; } - if(iolink_master_port_state(port)->startup.step == 1U) + if(iolink_master_port_state(port)->startup.step == IOLINK_MASTER_STARTUP_STEP_SEND_TYPE0) { - frame_len = iolink_frame_encode_type0(0x00U, iolink_master_port_state(port)->tx_buf, sizeof(iolink_master_port_state(port)->tx_buf)); + /* Spec startup transition T1: first message is a Type-0 READ of the + Direct Parameter page MinCycleTime octet (MC = 0xA2) on the page + communication channel. */ + frame_len = iolink_frame_encode_type0( + iolink_master_encode_master_command(true, + IOLINK_MASTER_MC_CHANNEL_PAGE, + IOLINK_MASTER_DPP1_OFF_MIN_CYCLE_TIME), + iolink_master_port_state(port)->tx_buf, + sizeof(iolink_master_port_state(port)->tx_buf)); if(frame_len > 0) { if(iolink_master_send_full(port, iolink_master_port_state(port)->tx_buf, (size_t)frame_len)) @@ -584,7 +616,7 @@ void iolink_master_process(iolink_master_port_t* port) return; } - if(iolink_master_port_state(port)->config.validate_device_info && !iolink_master_port_state(port)->device_info.valid) + if(iolink_master_startup_validation_required(&iolink_master_port_state(port)->config) && !iolink_master_port_state(port)->device_info.valid) { ret = iolink_master_read_device_info(port); if(ret == IOLINK_MASTER_STATUS_PENDING) @@ -598,15 +630,22 @@ void iolink_master_process(iolink_master_port_t* port) } } - if(iolink_master_port_state(port)->config.validate_device_info && (iolink_master_validate_device_info(port) != 0)) + if(iolink_master_startup_validation_required(&iolink_master_port_state(port)->config) && (iolink_master_validate_device_info(port) != 0)) { iolink_master_port_state(port)->state = IOLINK_MASTER_STATE_ERROR; return; } - frame_len = iolink_frame_encode_type0(IOLINK_MC_TRANSITION_COMMAND, - iolink_master_port_state(port)->tx_buf, - sizeof(iolink_master_port_state(port)->tx_buf)); + /* Spec transition to OPERATE: write MasterCommand DeviceOperate (0x99, + Table B.2) to Direct Parameter page address 0x00 on the page channel, + i.e. a Type-0 WRITE frame (MC = 0x20, one OD data octet 0x99). */ + frame_len = iolink_frame_encode_type0_write( + iolink_master_encode_master_command(false, + IOLINK_MASTER_MC_CHANNEL_PAGE, + IOLINK_MASTER_DPP1_OFF_MASTER_COMMAND), + IOLINK_CMD_DEVICE_OPERATE, + iolink_master_port_state(port)->tx_buf, + sizeof(iolink_master_port_state(port)->tx_buf)); if(frame_len > 0) { if(iolink_master_send_full(port, iolink_master_port_state(port)->tx_buf, (size_t)frame_len)) @@ -673,7 +712,7 @@ int iolink_master_poll_rx(iolink_master_port_t* port) return IOLINK_MASTER_STATUS_OK; } - if((iolink_master_port_state(port)->state == IOLINK_MASTER_STATE_STARTUP) && (iolink_master_port_state(port)->startup.step >= 2U)) + if((iolink_master_port_state(port)->state == IOLINK_MASTER_STATE_STARTUP) && (iolink_master_port_state(port)->startup.step >= IOLINK_MASTER_STARTUP_STEP_AWAIT_RESPONSE)) { expected_len = IOLINK_M_SEQ_TYPE0_LEN; } @@ -690,10 +729,15 @@ int iolink_master_poll_rx(iolink_master_port_t* port) return IOLINK_MASTER_STATUS_OK; } - while((recv_ret = iolink_master_port_state(port)->phy->recv_byte( - iolink_master_port_state(port)->phy->user, - &byte)) > 0) + for(;;) { + recv_ret = iolink_master_port_state(port)->phy->recv_byte( + iolink_master_port_state(port)->phy->user, &byte); + if(recv_ret <= 0) + { + break; + } + if(iolink_master_port_state(port)->rx.len >= sizeof(iolink_master_port_state(port)->rx.buf)) { iolink_master_port_state(port)->state = IOLINK_MASTER_STATE_ERROR; @@ -746,7 +790,7 @@ int iolink_master_on_rx(iolink_master_port_t* port, const uint8_t* data, uint8_t if(iolink_checksum_ck(data[0], 0U) != data[1]) { iolink_master_port_state(port)->diagnostics.checksum_errors++; - if(iolink_master_port_state(port)->diagnostics.rx_retry_count < 2U) + if(iolink_master_port_state(port)->diagnostics.rx_retry_count < IOLINK_MASTER_RX_RETRY_LIMIT) { iolink_master_port_state(port)->diagnostics.rx_retry_count++; } @@ -758,6 +802,7 @@ int iolink_master_on_rx(iolink_master_port_t* port, const uint8_t* data, uint8_t } iolink_master_port_state(port)->diagnostics.rx_retry_count = 0U; + iolink_master_port_state(port)->startup.wake_attempts = 0U; iolink_master_port_state(port)->awaiting_response = false; iolink_master_port_state(port)->state = IOLINK_MASTER_STATE_PREOPERATE; return IOLINK_MASTER_STATUS_OK; @@ -773,7 +818,7 @@ int iolink_master_on_rx(iolink_master_port_t* port, const uint8_t* data, uint8_t if(iolink_checksum_ck(data[0], 0U) != data[1]) { iolink_master_port_state(port)->diagnostics.checksum_errors++; - if(iolink_master_port_state(port)->diagnostics.rx_retry_count < 2U) + if(iolink_master_port_state(port)->diagnostics.rx_retry_count < IOLINK_MASTER_RX_RETRY_LIMIT) { iolink_master_port_state(port)->diagnostics.rx_retry_count++; } @@ -802,7 +847,7 @@ int iolink_master_on_rx(iolink_master_port_t* port, const uint8_t* data, uint8_t if(iolink_checksum_ck(data[0], 0U) != data[1]) { iolink_master_port_state(port)->diagnostics.checksum_errors++; - if(iolink_master_port_state(port)->diagnostics.rx_retry_count < 2U) + if(iolink_master_port_state(port)->diagnostics.rx_retry_count < IOLINK_MASTER_RX_RETRY_LIMIT) { iolink_master_port_state(port)->diagnostics.rx_retry_count++; } @@ -831,7 +876,7 @@ int iolink_master_on_rx(iolink_master_port_t* port, const uint8_t* data, uint8_t if(!resp.checksum_ok) { iolink_master_port_state(port)->diagnostics.checksum_errors++; - if(iolink_master_port_state(port)->diagnostics.rx_retry_count < 2U) + if(iolink_master_port_state(port)->diagnostics.rx_retry_count < IOLINK_MASTER_RX_RETRY_LIMIT) { iolink_master_port_state(port)->diagnostics.rx_retry_count++; } @@ -845,11 +890,24 @@ int iolink_master_on_rx(iolink_master_port_t* port, const uint8_t* data, uint8_t iolink_master_port_state(port)->diagnostics.rx_retry_count = 0U; iolink_master_port_state(port)->awaiting_response = false; iolink_master_port_state(port)->diagnostics.od_status = resp.status; + + /* + * Dispatch on the rising edge of the OD Event flag. This turns event + * handling from "poll diagnostics.event_pending yourself" into a + * notification: the application reacts by reading event details (which then + * dispatches each decoded event through config.event_handler). + */ + if(resp.event_pending && !iolink_master_port_state(port)->diagnostics.event_pending && + (iolink_master_port_state(port)->config.event_pending_handler != NULL)) + { + iolink_master_port_state(port)->config.event_pending_handler( + iolink_master_port_state(port)->config.event_user); + } iolink_master_port_state(port)->diagnostics.event_pending = resp.event_pending; if(resp.pd_valid) { - memcpy(iolink_master_port_state(port)->pd_in, resp.pd, resp.pd_len); + (void)memcpy(iolink_master_port_state(port)->pd_in, resp.pd, resp.pd_len); iolink_master_port_state(port)->pd_in_len = resp.pd_len; iolink_master_port_state(port)->pd_valid = true; } @@ -899,7 +957,7 @@ int iolink_master_get_pd_in(const iolink_master_port_t* port, return IOLINK_MASTER_STATUS_PENDING; } - memcpy(buffer, state->pd_in, state->pd_in_len); + (void)memcpy(buffer, state->pd_in, state->pd_in_len); return IOLINK_MASTER_STATUS_OK; } @@ -995,7 +1053,7 @@ int iolink_master_set_pd_out(iolink_master_port_t* port, const uint8_t* data, ui if(len > 0U) { - memcpy(iolink_master_port_state(port)->pd_out, data, len); + (void)memcpy(iolink_master_port_state(port)->pd_out, data, len); } iolink_master_port_state(port)->pd_out_len = len; return IOLINK_MASTER_STATUS_OK; diff --git a/third_party/iolinki-master/tests/fake_iolink_device.c b/third_party/iolinki-master/tests/fake_iolink_device.c index 9e1fc77a5..bbb6e37e8 100644 --- a/third_party/iolinki-master/tests/fake_iolink_device.c +++ b/third_party/iolinki-master/tests/fake_iolink_device.c @@ -12,6 +12,13 @@ #define FAKE_IOLINK_DEVICE_OBJECT_MAX_COUNT 4U #define FAKE_IOLINK_DEVICE_ISDU_REQUEST_MAX_LEN 8U +/* Coarse link state, used to disambiguate the startup probe (a page-channel + Type-0 read seen only once, right after wake-up) from later PREOPERATE ISDU + traffic that can carry the same MC bit pattern. */ +#define FAKE_LINK_STARTUP 0U +#define FAKE_LINK_PREOPERATE 1U +#define FAKE_LINK_OPERATE 2U + typedef struct { uint16_t index; @@ -30,6 +37,7 @@ typedef struct uint8_t rx_queue[16]; uint8_t rx_len; uint8_t rx_pos; + uint8_t link_state; uint32_t wakeup_count; uint32_t transition_count; uint32_t operate_cycle_count; @@ -116,7 +124,8 @@ static void fake_iolink_device_prepare_isdu_response(void) index = (uint16_t)(((uint16_t)g_device.isdu_request[1] << 8) | g_device.isdu_request[2]); subindex = g_device.isdu_request[3]; - if(service == IOLINK_ISDU_SERVICE_READ) + /* Accept the spec Table A.12 read I-Service codes (0x9/0xA/0xB). */ + if((service == 0x09U) || (service == 0x0AU) || (service == 0x0BU)) { object = fake_iolink_device_find_object(index, subindex); if(object == NULL) @@ -132,7 +141,8 @@ static void fake_iolink_device_prepare_isdu_response(void) return; } - if(service == IOLINK_ISDU_SERVICE_WRITE) + /* Accept the spec Table A.12 write I-Service codes (0x1/0x2/0x3). */ + if((service == 0x01U) || (service == 0x02U) || (service == 0x03U)) { len = (uint8_t)(g_device.isdu_request[0] & 0x0FU); if((len == 0x0FU) || (g_device.isdu_request_len < (uint8_t)(4U + len)) || @@ -296,6 +306,17 @@ static void fake_iolink_device_queue_operate_response(void) g_device.rx_pos = 0U; } +static uint8_t fake_iolink_device_direct_param_octet(uint8_t addr) +{ + fake_iolink_device_object_t* page = + fake_iolink_device_find_object(IOLINK_IDX_DIRECT_PARAMETERS_1, 0U); + if((page != NULL) && (addr < page->len)) + { + return page->data[addr]; + } + return 0U; +} + static int fake_iolink_device_send(void* user, const uint8_t* data, size_t len) { (void)user; @@ -310,20 +331,49 @@ static int fake_iolink_device_send(void* user, const uint8_t* data, size_t len) return (int)len; } + /* Spec DeviceOperate: Type-0 WRITE of MasterCommand 0x99 to Direct Parameter + address 0x00 on the page channel (MC 0x20). Establishes communication. */ + if((len == IOLINK_M_SEQ_MIN_LEN) && (data[0] == 0x20U) && + (data[1] == IOLINK_CMD_DEVICE_OPERATE)) + { + g_device.transition_count++; + g_device.link_state = FAKE_LINK_OPERATE; + return (int)len; + } + if(len == IOLINK_M_SEQ_TYPE0_LEN) { + /* Startup probe (spec T1): the first Type-0 frame after wake-up is a page- + channel READ of a Direct Parameter octet. Answer it from the Direct + Parameter page rather than treating it as ISDU traffic. */ + if((g_device.link_state == FAKE_LINK_STARTUP) && + ((data[0] & IOLINK_MC_RW_MASK) != 0U) && + ((data[0] & IOLINK_MC_COMM_CHANNEL_MASK) == 0x20U)) + { + g_device.link_state = FAKE_LINK_PREOPERATE; + fake_iolink_device_queue_type0( + fake_iolink_device_direct_param_octet((uint8_t)(data[0] & IOLINK_MC_ADDR_MASK))); + return (int)len; + } + if(data[0] == IOLINK_MC_TRANSITION_COMMAND) { g_device.transition_count++; + g_device.link_state = FAKE_LINK_OPERATE; return (int)len; } + if(g_device.link_state == FAKE_LINK_STARTUP) + { + g_device.link_state = FAKE_LINK_PREOPERATE; + } fake_iolink_device_on_master_od(data[0]); fake_iolink_device_queue_type0(fake_iolink_device_next_response_od()); return (int)len; } g_device.operate_cycle_count++; + g_device.link_state = FAKE_LINK_OPERATE; if(len > IOLINK_M_SEQ_HEADER_LEN) { fake_iolink_device_on_master_od(data[IOLINK_M_SEQ_HEADER_LEN]); diff --git a/third_party/iolinki-master/tests/test_master_fake_device.c b/third_party/iolinki-master/tests/test_master_fake_device.c index 6cd611dc9..c4b9c2a82 100644 --- a/third_party/iolinki-master/tests/test_master_fake_device.c +++ b/third_party/iolinki-master/tests/test_master_fake_device.c @@ -20,10 +20,30 @@ static const iolink_master_config_t g_config = { .auto_baudrate = false, }; +static uint32_t g_event_pending_calls; +static uint32_t g_event_dispatch_calls; +static iolink_master_event_t g_last_dispatched_event; + +static void on_event_pending(void* user) +{ + (void)user; + g_event_pending_calls++; +} + +static void on_event(void* user, const iolink_master_event_t* event) +{ + (void)user; + g_event_dispatch_calls++; + g_last_dispatched_event = *event; +} + static int reset_fixture(void** state) { (void)state; fake_iolink_device_reset(0xA5U, 1U, 1U); + g_event_pending_calls = 0U; + g_event_dispatch_calls = 0U; + memset(&g_last_dispatched_event, 0, sizeof(g_last_dispatched_event)); return 0; } @@ -302,6 +322,76 @@ static void test_fake_device_ack_event_reads_event_code(void** state) assert_int_equal(diagnostics.last_event_code, 0x1803U); } +static void test_fake_device_dispatches_event_pending_on_rising_edge(void** state) +{ + iolink_master_port_t port; + iolink_master_config_t config = g_config; + + (void)state; + + config.event_pending_handler = on_event_pending; + fake_iolink_device_set_event_pending(true); + + assert_int_equal(iolink_master_init(&port, fake_iolink_device_phy(), &config), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_NONE), 1); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_OPERATE); + + /* The first operate response carrying the OD Event flag dispatches once. */ + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_NONE), 1); + assert_int_equal(g_event_pending_calls, 1U); + + /* The flag stays set on later cycles: dispatch is edge-triggered, not level. */ + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_NONE), 1); + assert_int_equal(g_event_pending_calls, 1U); +} + +static void test_fake_device_dispatches_decoded_events_to_handler(void** state) +{ + iolink_master_port_t port; + iolink_master_config_t config = g_config; + iolink_master_event_t events[1]; + uint8_t count = 0U; + const uint8_t details[] = {0xE2U, 0x42U, 0x10U}; + uint8_t i; + + (void)state; + + memset(events, 0, sizeof(events)); + config.event_handler = on_event; + fake_iolink_device_set_event_pending(true); + fake_iolink_device_set_isdu_object(IOLINK_IDX_DETAILED_DEVICE_STATUS, 0U, details, + sizeof(details)); + + assert_int_equal(iolink_master_init(&port, fake_iolink_device_phy(), &config), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_NONE), 1); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_OPERATE); + + assert_int_equal(iolink_master_read_event_details(&port, events, 1U, &count), + IOLINK_MASTER_STATUS_PENDING); + + for(i = 0U; i < 13U; i++) + { + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 0); + assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_NONE), 1); + } + + assert_int_equal(iolink_master_read_event_details(&port, events, 1U, &count), + IOLINK_MASTER_STATUS_OK); + assert_int_equal(count, 1U); + assert_int_equal(g_event_dispatch_calls, 1U); + assert_int_equal(g_last_dispatched_event.qualifier, 0xE2U); + assert_int_equal(g_last_dispatched_event.type, IOLINK_MASTER_EVENT_TYPE_WARNING); + assert_int_equal(g_last_dispatched_event.code, 0x4210U); +} + static void test_fake_device_serves_isdu_object_dictionary_read(void** state) { iolink_master_port_t port; @@ -638,6 +728,10 @@ int main(void) reset_fixture), cmocka_unit_test_setup(test_fake_device_ack_event_reads_event_code, reset_fixture), + cmocka_unit_test_setup(test_fake_device_dispatches_event_pending_on_rising_edge, + reset_fixture), + cmocka_unit_test_setup(test_fake_device_dispatches_decoded_events_to_handler, + reset_fixture), cmocka_unit_test_setup(test_fake_device_serves_isdu_object_dictionary_read, reset_fixture), cmocka_unit_test_setup(test_fake_device_accepts_isdu_object_dictionary_write, diff --git a/third_party/iolinki-master/tests/test_master_isdu.c b/third_party/iolinki-master/tests/test_master_isdu.c index 201aa7067..48e19e686 100644 --- a/third_party/iolinki-master/tests/test_master_isdu.c +++ b/third_party/iolinki-master/tests/test_master_isdu.c @@ -300,6 +300,42 @@ static void test_read_isdu_completes_after_response_bytes(void** state) assert_int_equal(data[1], 0x4BU); } +static void test_read_isdu_skips_pre_response_filler_after_request_sent(void** state) +{ + iolink_master_port_t port; + uint8_t data[8] = {0U}; + uint8_t len = sizeof(data); + + (void)state; + + enter_operate(&port); + assert_int_equal(iolink_master_read_isdu(&port, 0x0010U, 0U, data, &len), + IOLINK_MASTER_STATUS_PENDING); + + /* + * Regression (found by the LabWired full-flow model): once the request is + * fully transmitted, a real device needs one or more idle cycles to compute + * its response and emits filler 0x00 OD bytes in the meantime. The response + * collector must skip those, not consume the first 0x00 as the response + * START control byte — otherwise the control/data phase desyncs and every + * following sequence number mismatches (SEGMENTATION error). The skip gate + * must not depend on request_sent. + */ + iolink_master_port_state(&port)->isdu.request_sent = true; + + feed_response_od(&port, 0x00U, 0x00U); + feed_response_od(&port, 0x00U, 0x00U); + + /* The real single-segment response then completes the read cleanly. */ + feed_response_od(&port, (uint8_t)(IOLINK_ISDU_CTRL_START | IOLINK_ISDU_CTRL_LAST), 0x4FU); + + len = sizeof(data); + assert_int_equal(iolink_master_read_isdu(&port, 0x0010U, 0U, data, &len), + IOLINK_MASTER_STATUS_OK); + assert_int_equal(len, 1U); + assert_int_equal(data[0], 0x4FU); +} + static void test_read_device_info_reads_direct_parameter_page1(void** state) { static const uint8_t page1[] = { @@ -376,7 +412,8 @@ static void test_preoperate_read_device_info_uses_type0_parameter_frames(void** iolink_master_port_t port; iolink_master_device_info_t info; uint8_t startup_resp[2] = {0U}; - uint8_t frame[2]; + uint8_t transition[8] = {0U}; + int expected_len; (void)state; @@ -398,10 +435,15 @@ static void test_preoperate_read_device_info_uses_type0_parameter_frames(void** assert_int_equal(info.vendor_id, 0x1234U); iolink_master_process(&port); - assert_int_equal(g_sent_len[g_send_calls - 1], 2U); - frame[0] = IOLINK_MC_TRANSITION_COMMAND; - frame[1] = iolink_checksum_ck(frame[0], 0U); - assert_memory_equal(g_sent[g_send_calls - 1], frame, sizeof(frame)); + /* Transition to OPERATE is a Type-0 WRITE of MasterCommand DeviceOperate + (0x99) to Direct Parameter address 0x00 on the page channel (MC 0x20). */ + expected_len = iolink_frame_encode_type0_write( + iolink_master_encode_master_command(false, IOLINK_MASTER_MC_CHANNEL_PAGE, + IOLINK_MASTER_DPP1_OFF_MASTER_COMMAND), + IOLINK_CMD_DEVICE_OPERATE, transition, sizeof(transition)); + assert_int_equal(expected_len, 3); + assert_int_equal(g_sent_len[g_send_calls - 1], (size_t)expected_len); + assert_memory_equal(g_sent[g_send_calls - 1], transition, (size_t)expected_len); } static void test_read_isdu_reports_small_result_buffer(void** state) @@ -579,6 +621,8 @@ int main(void) cmocka_unit_test_setup(test_type0_read_isdu_completes_from_type0_response_bytes, reset_fake_phy), cmocka_unit_test_setup(test_read_isdu_completes_after_response_bytes, reset_fake_phy), + cmocka_unit_test_setup(test_read_isdu_skips_pre_response_filler_after_request_sent, + reset_fake_phy), cmocka_unit_test_setup(test_read_device_info_reads_direct_parameter_page1, reset_fake_phy), cmocka_unit_test_setup(test_read_device_info_rejects_incompatible_device_page, reset_fake_phy), diff --git a/third_party/iolinki-master/tests/test_master_parameters.c b/third_party/iolinki-master/tests/test_master_parameters.c index 4869c450a..a842e7369 100644 --- a/third_party/iolinki-master/tests/test_master_parameters.c +++ b/third_party/iolinki-master/tests/test_master_parameters.c @@ -6,6 +6,7 @@ #include +#include "iolinki/protocol.h" #include "../src/master_internal.h" static const uint8_t g_page1[] = { @@ -85,6 +86,34 @@ static void test_parse_direct_parameter_page1_decodes_zero_and_small_bit_lengths assert_int_equal(info.pd_out_len, 0U); } +static void test_parse_direct_parameter_page1_decodes_pd_descriptor_per_table_b6(void** state) +{ + uint8_t page[16] = {0U}; + iolink_master_device_info_t info; + + (void)state; + + /* + * ProcessData descriptor per Table B.6, isolating Length to bits 0-4 so the + * legal SIO bit (6) and sub-byte bit lengths decode correctly. + * ProcessDataIn (0x05): BYTE=1, SIO=1, Length=3 -> 4 octets. + * ProcessDataOut (0x06): BYTE=0, SIO=1, Length=4 bits -> 1 octet (round up). + */ + page[0x05] = 0xC3U; /* 1_1_0_00011 */ + page[0x06] = 0x44U; /* 0_1_0_00100 */ + assert_int_equal(iolink_master_parse_direct_parameter_page1(page, sizeof(page), &info), 0); + assert_int_equal(info.pd_in_len, 4U); + assert_int_equal(info.pd_out_len, 1U); + + /* SIO-only descriptor with no Process Data (bit 6 set, Length 0) -> 0 octets. */ + page[0x05] = 0x40U; + /* A 12-bit (BYTE=0) descriptor rounds up to 2 octets. */ + page[0x06] = 0x0CU; + assert_int_equal(iolink_master_parse_direct_parameter_page1(page, sizeof(page), &info), 0); + assert_int_equal(info.pd_in_len, 0U); + assert_int_equal(info.pd_out_len, 2U); +} + static void test_parse_direct_parameter_page1_rejects_invalid_args(void** state) { iolink_master_device_info_t info; @@ -350,10 +379,198 @@ static void test_select_config_from_device_info_rejects_invalid_inputs(void** st IOLINK_MASTER_PARAM_ERR_M_SEQUENCE); } +static void test_validate_config_against_device_info_enforces_device_identity(void** state) +{ + iolink_master_device_info_t info; + iolink_master_config_t config = g_config; + + (void)state; + + assert_int_equal(iolink_master_parse_direct_parameter_page1(g_page1, sizeof(g_page1), &info), + IOLINK_MASTER_STATUS_OK); + + /* g_page1 advertises operate M-sequence code 5, so use the matching type to + get past the compatibility checks and reach the identity check. */ + config.m_seq_type = IOLINK_MASTER_M_SEQ_TYPE_2_V; + + /* NO_CHECK (the default) ignores a mismatched identity. */ + config.expected_vendor_id = 0xBEEFU; + config.expected_device_id = 0x010203U; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_STATUS_OK); + + /* TYPE_COMP accepts the matching VendorID/DeviceID carried in g_page1. */ + config.inspection_level = IOLINK_MASTER_INSPECTION_TYPE_COMP; + config.expected_vendor_id = 0x1234U; + config.expected_device_id = 0x56789AU; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_STATUS_OK); + + /* A wrong VendorID is rejected with a distinct code. */ + config.expected_vendor_id = 0x1235U; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_PARAM_ERR_VENDOR_ID); + + /* A wrong DeviceID is rejected with a distinct code. */ + config.expected_vendor_id = 0x1234U; + config.expected_device_id = 0x56789BU; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_PARAM_ERR_DEVICE_ID); + + /* IDENTICAL enforces the same VendorID/DeviceID match today. */ + config.inspection_level = IOLINK_MASTER_INSPECTION_IDENTICAL; + config.expected_device_id = 0x56789AU; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_STATUS_OK); + config.expected_vendor_id = 0x0000U; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_PARAM_ERR_VENDOR_ID); +} + +static void test_validate_device_info_enforces_configured_identity_on_port(void** state) +{ + iolink_master_config_t config = g_config; + iolink_master_port_t port; + + (void)state; + + config.m_seq_type = IOLINK_MASTER_M_SEQ_TYPE_2_V; + config.inspection_level = IOLINK_MASTER_INSPECTION_TYPE_COMP; + config.expected_vendor_id = 0x1234U; + config.expected_device_id = 0x56789AU; + + assert_int_equal(iolink_master_init(&port, &g_phy, &config), 0); + assert_int_equal(iolink_master_apply_direct_parameter_page1(&port, g_page1, sizeof(g_page1)), 0); + assert_int_equal(iolink_master_validate_device_info(&port), IOLINK_MASTER_STATUS_OK); + + config.expected_device_id = 0x000001U; + assert_int_equal(iolink_master_init(&port, &g_phy, &config), 0); + assert_int_equal(iolink_master_apply_direct_parameter_page1(&port, g_page1, sizeof(g_page1)), 0); + assert_int_equal(iolink_master_validate_device_info(&port), IOLINK_MASTER_PARAM_ERR_DEVICE_ID); +} + +static void test_decode_min_cycle_time_octet_covers_all_time_bases(void** state) +{ + (void)state; + + /* Base 00 (0.1 ms): the octet value is already the 100us count. */ + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0x00U), 0U); + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0x0AU), 10U); + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0x3FU), 63U); + + /* Base 01 (0.4 ms, offset 6.4 ms): 64 + n*4 in 100us units. */ + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0x40U), 64U); + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0x41U), 68U); + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0x7FU), 316U); + + /* Base 10 (1.6 ms, offset 32.0 ms): 320 + n*16 in 100us units. */ + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0x80U), 320U); + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0x81U), 336U); + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0xBFU), 1328U); + + /* Reserved base 11 falls back to the raw octet rather than inventing timing. */ + assert_int_equal(iolink_master_decode_min_cycle_time_100us(0xC5U), 0xC5U); +} + +static void test_parse_direct_parameter_page1_decodes_cycle_time_time_base(void** state) +{ + uint8_t page[16]; + iolink_master_device_info_t info; + + (void)state; + + memcpy(page, g_page1, sizeof(page)); + page[0x02] = 0x41U; /* 0.4 ms base, multiplier 1 -> 6.8 ms. */ + + assert_int_equal(iolink_master_parse_direct_parameter_page1(page, sizeof(page), &info), 0); + assert_int_equal(info.min_cycle_time, 0x41U); + assert_int_equal(info.min_cycle_time_100us, 68U); +} + +static void test_validate_config_compares_decoded_cycle_time(void** state) +{ + uint8_t page[16]; + iolink_master_device_info_t info; + iolink_master_config_t config = g_config; + + (void)state; + + memcpy(page, g_page1, sizeof(page)); + page[0x02] = 0x41U; /* device minimum 6.8 ms = 68 (100us). */ + page[0x03] = 0x01U; /* ISDU supported, operate M-sequence code 0 (matches type 2_1). */ + + assert_int_equal(iolink_master_parse_direct_parameter_page1(page, sizeof(page), &info), 0); + + /* 2.0 ms configured cycle is below the 6.8 ms device minimum: rejected. */ + config.min_cycle_time = 20U; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_PARAM_ERR_CYCLE_TIME); + + /* One 100us tick short still fails; matching the decoded minimum passes. */ + config.min_cycle_time = 67U; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_PARAM_ERR_CYCLE_TIME); + config.min_cycle_time = 68U; + assert_int_equal(iolink_master_validate_config_against_device_info(&info, &config), + IOLINK_MASTER_STATUS_OK); +} + +static void test_select_config_adopts_decoded_cycle_time_and_clamps(void** state) +{ + uint8_t page[16]; + iolink_master_device_info_t info; + iolink_master_config_t config = { + .port_mode = IOLINK_MASTER_PORT_MODE_IOLINK, + .baudrate = IOLINK_BAUDRATE_COM3, + }; + + (void)state; + + memcpy(page, g_page1, sizeof(page)); + page[0x02] = 0x41U; /* decoded 68 (100us). */ + assert_int_equal(iolink_master_parse_direct_parameter_page1(page, sizeof(page), &info), 0); + assert_int_equal(iolink_master_select_config_from_device_info(&info, &config), + IOLINK_MASTER_STATUS_OK); + assert_int_equal(config.min_cycle_time, 68U); + + /* A device minimum above 25.5 ms clamps to the uint8 config ceiling. */ + page[0x02] = 0xBFU; /* decoded 1328 (100us) > 255. */ + assert_int_equal(iolink_master_parse_direct_parameter_page1(page, sizeof(page), &info), 0); + assert_int_equal(iolink_master_select_config_from_device_info(&info, &config), + IOLINK_MASTER_STATUS_OK); + assert_int_equal(config.min_cycle_time, 0xFFU); +} + +static void test_master_command_encode_decode_round_trips(void** state) +{ + uint8_t mc; + + (void)state; + + mc = iolink_master_encode_master_command(true, IOLINK_MASTER_MC_CHANNEL_ISDU, 0x12U); + assert_int_equal(mc, (uint8_t)(IOLINK_MC_RW_MASK | IOLINK_MC_COMM_CHANNEL_MASK | 0x12U)); + assert_true(iolink_master_mc_is_read(mc)); + assert_int_equal(iolink_master_mc_channel(mc), IOLINK_MASTER_MC_CHANNEL_ISDU); + assert_int_equal(iolink_master_mc_address(mc), 0x12U); + + /* The operate-transition command composes from parts to the legacy 0x0F octet. */ + assert_int_equal(iolink_master_encode_master_command(false, + IOLINK_MASTER_MC_CHANNEL_PROCESS, + IOLINK_MASTER_MC_TRANSITION_ADDR), + IOLINK_MC_TRANSITION_COMMAND); + + /* Address masks to 5 bits and a write clears the R/W bit. */ + mc = iolink_master_encode_master_command(false, IOLINK_MASTER_MC_CHANNEL_PAGE, 0xFFU); + assert_false(iolink_master_mc_is_read(mc)); + assert_int_equal(iolink_master_mc_channel(mc), IOLINK_MASTER_MC_CHANNEL_PAGE); + assert_int_equal(iolink_master_mc_address(mc), 0x1FU); +} + int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_parse_direct_parameter_page1_decodes_standard_fields), + cmocka_unit_test(test_parse_direct_parameter_page1_decodes_pd_descriptor_per_table_b6), cmocka_unit_test(test_parse_direct_parameter_page1_decodes_zero_and_small_bit_lengths), cmocka_unit_test(test_parse_direct_parameter_page1_rejects_invalid_args), cmocka_unit_test(test_apply_direct_parameter_page1_latches_info_on_port), @@ -365,7 +582,14 @@ int main(void) cmocka_unit_test(test_select_config_from_device_info_maps_fixed_type2_profiles), cmocka_unit_test(test_select_config_from_device_info_maps_all_public_mseq_profiles), cmocka_unit_test(test_validate_config_against_device_info_rejects_incompatible_request), + cmocka_unit_test(test_validate_config_against_device_info_enforces_device_identity), + cmocka_unit_test(test_validate_device_info_enforces_configured_identity_on_port), cmocka_unit_test(test_select_config_from_device_info_rejects_invalid_inputs), + cmocka_unit_test(test_decode_min_cycle_time_octet_covers_all_time_bases), + cmocka_unit_test(test_parse_direct_parameter_page1_decodes_cycle_time_time_base), + cmocka_unit_test(test_validate_config_compares_decoded_cycle_time), + cmocka_unit_test(test_select_config_adopts_decoded_cycle_time_and_clamps), + cmocka_unit_test(test_master_command_encode_decode_round_trips), }; return cmocka_run_group_tests(tests, NULL, NULL); diff --git a/third_party/iolinki-master/tests/test_master_public_flow.c b/third_party/iolinki-master/tests/test_master_public_flow.c index 80664f04d..35e8486ae 100644 --- a/third_party/iolinki-master/tests/test_master_public_flow.c +++ b/third_party/iolinki-master/tests/test_master_public_flow.c @@ -101,7 +101,10 @@ static void test_public_api_drives_startup_and_latches_process_data(void** state queue_bytes(startup_resp, sizeof(startup_resp)); assert_int_equal(iolink_master_tick(&port, false), 1); assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_OPERATE); - assert_int_equal(g_sent[g_send_calls - 1][0], IOLINK_MC_TRANSITION_COMMAND); + /* Transition to OPERATE is the Type-0 DeviceOperate write (MC 0x20, OD 0x99). */ + assert_int_equal(g_sent[g_send_calls - 1][0], + iolink_master_encode_master_command(false, IOLINK_MASTER_MC_CHANNEL_PAGE, 0x00U)); + assert_int_equal(g_sent[g_send_calls - 1][1], IOLINK_CMD_DEVICE_OPERATE); queue_bytes(operate_resp, sizeof(operate_resp)); assert_int_equal(iolink_master_tick(&port, false), 1); diff --git a/third_party/iolinki-master/tests/test_master_startup.c b/third_party/iolinki-master/tests/test_master_startup.c index aa7033683..9147833a8 100644 --- a/third_party/iolinki-master/tests/test_master_startup.c +++ b/third_party/iolinki-master/tests/test_master_startup.c @@ -592,6 +592,58 @@ static void test_fixed_baudrate_startup_timeout_enters_error(void** state) assert_int_equal(g_set_baudrate_calls, 1); } +static void test_wake_retry_limit_reissues_wake_before_error_on_fixed_baud(void** state) +{ + iolink_master_port_t port; + iolink_master_config_t config = g_config; + + (void)state; + + config.wake_retry_limit = 2U; + + assert_int_equal(iolink_master_init(&port, &g_fake_phy, &config), 0); + + /* Each timeout re-arms the wake-up (step back to 0) at the same baudrate. */ + assert_int_equal(iolink_master_on_timeout(&port), IOLINK_MASTER_STATUS_PENDING); + assert_int_equal(iolink_master_port_state(&port)->startup.step, 0U); + assert_int_equal(iolink_master_port_state(&port)->startup.wake_attempts, 1U); + assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_STARTUP); + + assert_int_equal(iolink_master_on_timeout(&port), IOLINK_MASTER_STATUS_PENDING); + assert_int_equal(iolink_master_port_state(&port)->startup.wake_attempts, 2U); + assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_STARTUP); + + /* Wake budget exhausted on a fixed baudrate: enter error, never re-baud. */ + assert_int_equal(iolink_master_on_timeout(&port), -2); + assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_ERROR); + assert_int_equal(g_set_baudrate_calls, 1); +} + +static void test_wake_retry_exhausts_per_baud_then_advances_scan(void** state) +{ + iolink_master_port_t port; + iolink_master_config_t config = g_config; + + (void)state; + + config.auto_baudrate = true; + config.wake_retry_limit = 1U; + + assert_int_equal(iolink_master_init(&port, &g_fake_phy, &config), 0); + assert_int_equal(g_baudrate_history[0], IOLINK_BAUDRATE_COM3); + + /* One wake retry at COM3 before the scan is allowed to advance. */ + assert_int_equal(iolink_master_on_timeout(&port), IOLINK_MASTER_STATUS_PENDING); + assert_int_equal(iolink_master_port_state(&port)->startup.wake_attempts, 1U); + assert_int_equal(g_set_baudrate_calls, 1); + + /* Budget spent: advance to COM2 and reset the per-baud wake counter. */ + assert_int_equal(iolink_master_on_timeout(&port), IOLINK_MASTER_STATUS_PENDING); + assert_int_equal(g_set_baudrate_calls, 2); + assert_int_equal(g_baudrate_history[1], IOLINK_BAUDRATE_COM2); + assert_int_equal(iolink_master_port_state(&port)->startup.wake_attempts, 0U); +} + static void test_restart_reenters_startup_and_clears_runtime_state(void** state) { iolink_master_port_t port; @@ -783,7 +835,11 @@ static void test_process_startup_waits_for_type0_response_before_preoperate(void assert_int_equal(g_sent[0][0], 0x55U); iolink_master_process(&port); - expected_len = iolink_frame_encode_type0(0x00U, expected, sizeof(expected)); + /* Startup probe: Type-0 READ of MinCycleTime on the page channel (MC 0xA2). */ + expected_len = iolink_frame_encode_type0( + iolink_master_encode_master_command(true, IOLINK_MASTER_MC_CHANNEL_PAGE, + IOLINK_MASTER_DPP1_OFF_MIN_CYCLE_TIME), + expected, sizeof(expected)); assert_int_equal(expected_len, 2); assert_int_equal(g_send_calls, 2); assert_int_equal(g_sent_len[1], (size_t)expected_len); @@ -797,8 +853,13 @@ static void test_process_startup_waits_for_type0_response_before_preoperate(void assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_PREOPERATE); iolink_master_process(&port); - expected_len = iolink_frame_encode_type0(IOLINK_MC_TRANSITION_COMMAND, expected, sizeof(expected)); - assert_int_equal(expected_len, 2); + /* Transition to OPERATE: Type-0 WRITE of MasterCommand DeviceOperate (0x99) + to Direct Parameter address 0x00 on the page channel (MC 0x20). */ + expected_len = iolink_frame_encode_type0_write( + iolink_master_encode_master_command(false, IOLINK_MASTER_MC_CHANNEL_PAGE, + IOLINK_MASTER_DPP1_OFF_MASTER_COMMAND), + IOLINK_CMD_DEVICE_OPERATE, expected, sizeof(expected)); + assert_int_equal(expected_len, 3); assert_int_equal(g_send_calls, 3); assert_int_equal(g_sent_len[2], (size_t)expected_len); assert_memory_equal(g_sent[2], expected, (size_t)expected_len); @@ -1125,6 +1186,10 @@ int main(void) reset_fake_phy), cmocka_unit_test_setup(test_fixed_baudrate_startup_timeout_enters_error, reset_fake_phy), + cmocka_unit_test_setup(test_wake_retry_limit_reissues_wake_before_error_on_fixed_baud, + reset_fake_phy), + cmocka_unit_test_setup(test_wake_retry_exhausts_per_baud_then_advances_scan, + reset_fake_phy), cmocka_unit_test_setup(test_restart_reenters_startup_and_clears_runtime_state, reset_fake_phy), cmocka_unit_test_setup(test_get_diagnostics_samples_hardware_fault_hooks, diff --git a/third_party/iolinki-master/tests/test_master_tick.c b/third_party/iolinki-master/tests/test_master_tick.c index ed1c2d7b1..7bbc3659e 100644 --- a/third_party/iolinki-master/tests/test_master_tick.c +++ b/third_party/iolinki-master/tests/test_master_tick.c @@ -111,8 +111,11 @@ static void test_tick_drains_rx_before_sending_next_frame(void** state) assert_int_equal(iolink_master_tick(&port, false), 1); assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_OPERATE); assert_int_equal(g_send_calls, 3); - assert_int_equal(g_sent_len[2], 2U); - assert_int_equal(g_sent[2][0], IOLINK_MC_TRANSITION_COMMAND); + assert_int_equal(g_sent_len[2], 3U); + assert_int_equal(g_sent[2][0], + iolink_master_encode_master_command(false, IOLINK_MASTER_MC_CHANNEL_PAGE, + IOLINK_MASTER_DPP1_OFF_MASTER_COMMAND)); + assert_int_equal(g_sent[2][1], IOLINK_CMD_DEVICE_OPERATE); } static void test_tick_applies_timeout_before_transmit(void** state) @@ -170,8 +173,11 @@ static void test_tick_event_cycle_due_transmits_after_rx(void** state) assert_int_equal(iolink_master_tick_event(&port, IOLINK_MASTER_TICK_CYCLE_DUE), 1); assert_int_equal(iolink_master_get_state(&port), IOLINK_MASTER_STATE_OPERATE); assert_int_equal(g_send_calls, 3); - assert_int_equal(g_sent_len[2], 2U); - assert_int_equal(g_sent[2][0], IOLINK_MC_TRANSITION_COMMAND); + assert_int_equal(g_sent_len[2], 3U); + assert_int_equal(g_sent[2][0], + iolink_master_encode_master_command(false, IOLINK_MASTER_MC_CHANNEL_PAGE, + IOLINK_MASTER_DPP1_OFF_MASTER_COMMAND)); + assert_int_equal(g_sent[2][1], IOLINK_CMD_DEVICE_OPERATE); } static void test_tick_event_response_timeout_applies_before_transmit(void** state) diff --git a/third_party/iolinki-master/tools/generate_release_notes.py b/third_party/iolinki-master/tools/generate_release_notes.py new file mode 100755 index 000000000..ac4dd0538 --- /dev/null +++ b/third_party/iolinki-master/tools/generate_release_notes.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +import sys +import subprocess +import json + + +def get_git_log(from_tag, to_tag): + """Get git log messages between two tags.""" + cmd = ["git", "log", f"{from_tag}..{to_tag}", "--pretty=format:%s"] + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + return result.stdout.split("\n") + except subprocess.CalledProcessError: + return [] + + +def categorize_commits(log_lines): + """Categorize commits based on conventional commits.""" + categories = { + "Features": [], + "Bug Fixes": [], + "Documentation": [], + "Refactoring": [], + "Testing": [], + "Maintenance": [], + } + + for line in log_lines: + line = line.strip() + if not line: + continue + + if line.startswith("feat"): + categories["Features"].append(line) + elif line.startswith("fix"): + categories["Bug Fixes"].append(line) + elif line.startswith("docs"): + categories["Documentation"].append(line) + elif line.startswith("refactor") or line.startswith("style"): + categories["Refactoring"].append(line) + elif line.startswith("test"): + categories["Testing"].append(line) + else: + categories["Maintenance"].append(line) + + return categories + + +def generate_markdown(version, date, categories, test_stats, coverage_stats): + lines = [] + lines.append(f"# Release {version}") + lines.append(f"**Date:** {date}") + lines.append("") + + lines.append("## 📊 Quality Report") + lines.append("| Metric | Status |") + lines.append("| :--- | :--- |") + lines.append( + f"| **Tests** | ✅ {test_stats['passed']} Passed / {test_stats['total']} Total |" + ) + lines.append(f"| **Coverage** | 📈 {coverage_stats} |") + lines.append("") + + lines.append("## 🚀 New Features") + if categories["Features"]: + for item in categories["Features"]: + lines.append(f"- {item}") + else: + lines.append("- _No major features in this release_") + lines.append("") + + lines.append("## 🐛 Bug Fixes") + if categories["Bug Fixes"]: + for item in categories["Bug Fixes"]: + lines.append(f"- {item}") + else: + lines.append("- _No bug fixes in this release_") + lines.append("") + + lines.append("
") + lines.append("Other Changes (Docs, Refactor, Maint)") + lines.append("") + for cat in ["Documentation", "Refactoring", "Testing", "Maintenance"]: + if categories[cat]: + lines.append(f"### {cat}") + for item in categories[cat]: + lines.append(f"- {item}") + lines.append("") + lines.append("
") + + return "\n".join(lines) + + +if __name__ == "__main__": + if len(sys.argv) < 5: + print( + "Usage: generate_release_notes.py " + " " + ) + sys.exit(1) + + version = sys.argv[1] + prev_tag = sys.argv[2] + test_json = sys.argv[3] + cov_file = sys.argv[4] + + log = get_git_log(prev_tag, "HEAD") + cats = categorize_commits(log) + + t_passed = 0 + t_total = 0 + try: + with open(test_json, "r") as f: + d = json.load(f) + t_passed = d.get("passed", 0) + t_total = d.get("total", 0) + except Exception: + pass + + cov_str = "N/A" + try: + with open(cov_file, "r") as f: + cov_str = f.read().strip() + except Exception: + pass + + import datetime + + date_str = datetime.date.today().strftime("%Y-%m-%d") + + md = generate_markdown( + version, date_str, cats, {"passed": t_passed, "total": t_total}, cov_str + ) + print(md) diff --git a/third_party/iolinki-master/tools/generate_sbom.py b/third_party/iolinki-master/tools/generate_sbom.py new file mode 100755 index 000000000..ac0636438 --- /dev/null +++ b/third_party/iolinki-master/tools/generate_sbom.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Generate a per-release SBOM for the iolinki-master stack. + +Emits CycloneDX 1.6 JSON or SPDX 2.3 JSON describing the iolinki-master library: +a self-contained C master stack with zero third-party runtime dependencies. The +narrow frame/CRC helper sources reused from the sibling ``iolinki`` device stack +are compiled in from source, and build-/test-only tooling (CMake, cmocka) is +listed explicitly with a non-runtime scope, so the zero-dependency claim is +auditable rather than implied. + +Usage: + python3 tools/generate_sbom.py --version 0.2.0 --format cyclonedx \ + --output iolinki-master-0.2.0.cdx.json + +Set SOURCE_DATE_EPOCH for a reproducible timestamp. +""" + +import argparse +import json +import os +import sys +import uuid +from datetime import datetime, timezone + +NAME = "iolinki-master" +SUPPLIER = "Andrii Shylenko" +REPO_URL = "https://github.com/w1ne/iolinki-master" +DESCRIPTION = ( + "IO-Link master stack in portable C (IO-Link Interface and System " + "Specification): startup state machine, cyclic process data, ISDU, Data " + "Storage, events, and multi-port control; no third-party runtime dependencies." +) +LICENSE_EXPRESSION = "GPL-3.0-only OR LicenseRef-iolinki-Commercial" + +# Build/test-time tooling only; never part of the shipped library. +BUILD_TOOLS = ( + ("cmake", "build system"), + ("cmocka", "unit-test framework"), + ("iolinki", "sibling device stack (frame/CRC helper sources, compiled in)"), +) + + +def _purl(version): + return f"pkg:github/w1ne/iolinki-master@v{version}" + + +def _timestamp(): + epoch = os.environ.get("SOURCE_DATE_EPOCH") + when = ( + datetime.fromtimestamp(int(epoch), tz=timezone.utc) + if epoch + else datetime.now(tz=timezone.utc) + ) + return when.strftime("%Y-%m-%dT%H:%M:%SZ") + + +def build_cyclonedx(version): + root_ref = f"{NAME}@{version}" + tool_components = [ + { + "type": "application", + "bom-ref": f"buildtool-{name}", + "name": name, + "description": f"{role} (build/test only, not a runtime dependency)", + "scope": "excluded", + } + for name, role in BUILD_TOOLS + ] + return { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": f"urn:uuid:{uuid.uuid4()}", + "version": 1, + "metadata": { + "timestamp": _timestamp(), + "supplier": {"name": SUPPLIER}, + "component": { + "type": "library", + "bom-ref": root_ref, + "name": NAME, + "version": version, + "description": DESCRIPTION, + "supplier": {"name": SUPPLIER}, + "licenses": [{"expression": LICENSE_EXPRESSION}], + "purl": _purl(version), + "externalReferences": [{"type": "vcs", "url": REPO_URL}], + }, + }, + "components": tool_components, + # The root component depends on nothing at runtime: the stack is self-contained C. + "dependencies": [{"ref": root_ref, "dependsOn": []}], + } + + +def build_spdx(version): + root_id = "SPDXRef-Package-iolinki-master" + tool_packages = [] + tool_relationships = [] + for name, role in BUILD_TOOLS: + pkg_id = f"SPDXRef-Package-{name}" + tool_packages.append( + { + "SPDXID": pkg_id, + "name": name, + "downloadLocation": "NOASSERTION", + "filesAnalyzed": False, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "comment": f"{role} (build/test only, not a runtime dependency)", + } + ) + tool_relationships.append( + { + "spdxElementId": pkg_id, + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": root_id, + } + ) + return { + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": f"{NAME}-{version}", + "documentNamespace": f"{REPO_URL}/spdx/{version}-{uuid.uuid4()}", + "creationInfo": { + "created": _timestamp(), + "creators": [ + f"Person: {SUPPLIER}", + "Tool: iolinki-master-generate-sbom", + ], + }, + "packages": [ + { + "SPDXID": root_id, + "name": NAME, + "versionInfo": version, + "supplier": f"Person: {SUPPLIER}", + "downloadLocation": f"git+{REPO_URL}@v{version}", + "filesAnalyzed": False, + "licenseConcluded": LICENSE_EXPRESSION, + "licenseDeclared": LICENSE_EXPRESSION, + "description": DESCRIPTION, + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": _purl(version), + } + ], + }, + *tool_packages, + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": root_id, + }, + *tool_relationships, + ], + } + + +BUILDERS = {"cyclonedx": build_cyclonedx, "spdx": build_spdx} + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--version", required=True, help="release version, no v prefix") + parser.add_argument("--format", required=True, choices=sorted(BUILDERS)) + parser.add_argument("--output", required=True, help="output JSON path") + args = parser.parse_args(argv) + + doc = BUILDERS[args.format](args.version) + with open(args.output, "w", encoding="utf-8") as fh: + json.dump(doc, fh, indent=2, sort_keys=False) + fh.write("\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/third_party/iolinki-master/tools/run-cppcheck.sh b/third_party/iolinki-master/tools/run-cppcheck.sh new file mode 100755 index 000000000..3dab389ac --- /dev/null +++ b/third_party/iolinki-master/tools/run-cppcheck.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Wrapper for cppcheck to be used with pre-commit and check_quality.sh. +# It ensures correct include paths and suppression settings. +# +# Pre-commit passes the list of files as arguments. + +set -e + +# variableScope is suppressed on purpose: this stack declares locals at function +# top (a deliberate, consistent convention), which cppcheck's style check would +# otherwise flag. All other warning/style/performance/portability checks stay on. +cppcheck --enable=warning,style,performance,portability \ + --error-exitcode=1 \ + --suppress=missingIncludeSystem \ + --suppress=unusedFunction \ + --suppress=variableScope \ + --inline-suppr \ + --quiet \ + -I include \ + "$@" diff --git a/third_party/iolinki-master/tools/test_generate_sbom.py b/third_party/iolinki-master/tools/test_generate_sbom.py new file mode 100755 index 000000000..08774b0ee --- /dev/null +++ b/third_party/iolinki-master/tools/test_generate_sbom.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Tests for generate_sbom.py — per-release CycloneDX/SPDX SBOM generator.""" + +import json +import os +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from generate_sbom import LICENSE_EXPRESSION, build_cyclonedx, build_spdx # noqa: E402 + +TOOL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "generate_sbom.py") +VERSION = "0.2.0" + + +class TestCycloneDX(unittest.TestCase): + def setUp(self): + self.doc = build_cyclonedx(VERSION) + + def test_format_and_spec_version(self): + self.assertEqual(self.doc["bomFormat"], "CycloneDX") + self.assertEqual(self.doc["specVersion"], "1.6") + + def test_root_component_identity(self): + comp = self.doc["metadata"]["component"] + self.assertEqual(comp["name"], "iolinki-master") + self.assertEqual(comp["version"], VERSION) + self.assertEqual(comp["purl"], f"pkg:github/w1ne/iolinki-master@v{VERSION}") + self.assertEqual(comp["licenses"][0]["expression"], LICENSE_EXPRESSION) + + def test_zero_runtime_dependencies_is_explicit(self): + root_ref = self.doc["metadata"]["component"]["bom-ref"] + deps = {d["ref"]: d.get("dependsOn", []) for d in self.doc["dependencies"]} + self.assertIn(root_ref, deps) + self.assertEqual(deps[root_ref], []) + + def test_build_tools_are_excluded_scope(self): + names = {c["name"]: c for c in self.doc.get("components", [])} + for tool in ("cmake", "cmocka", "iolinki"): + self.assertIn(tool, names) + self.assertEqual(names[tool]["scope"], "excluded") + + +class TestSPDX(unittest.TestCase): + def setUp(self): + self.doc = build_spdx(VERSION) + + def test_document_identity(self): + self.assertEqual(self.doc["spdxVersion"], "SPDX-2.3") + self.assertEqual(self.doc["SPDXID"], "SPDXRef-DOCUMENT") + self.assertEqual(self.doc["dataLicense"], "CC0-1.0") + + def test_root_package(self): + pkgs = {p["name"]: p for p in self.doc["packages"]} + self.assertIn("iolinki-master", pkgs) + pkg = pkgs["iolinki-master"] + self.assertEqual(pkg["versionInfo"], VERSION) + self.assertEqual(pkg["licenseDeclared"], LICENSE_EXPRESSION) + purls = [ + r["referenceLocator"] + for r in pkg.get("externalRefs", []) + if r["referenceType"] == "purl" + ] + self.assertEqual(purls, [f"pkg:github/w1ne/iolinki-master@v{VERSION}"]) + + def test_describes_relationship(self): + rels = [ + r + for r in self.doc["relationships"] + if r["relationshipType"] == "DESCRIBES" + and r["spdxElementId"] == "SPDXRef-DOCUMENT" + ] + self.assertEqual(len(rels), 1) + + +class TestCLI(unittest.TestCase): + def run_tool(self, *args): + return subprocess.run( + [sys.executable, TOOL, *args], capture_output=True, text=True + ) + + def test_writes_valid_json_for_both_formats(self): + for fmt in ("cyclonedx", "spdx"): + with tempfile.TemporaryDirectory() as tmp: + out = os.path.join(tmp, "sbom.json") + res = self.run_tool( + "--version", VERSION, "--format", fmt, "--output", out + ) + self.assertEqual(res.returncode, 0, res.stderr) + with open(out, encoding="utf-8") as fh: + json.load(fh) + + def test_unknown_format_fails(self): + res = self.run_tool("--version", VERSION, "--format", "xml", "--output", "x") + self.assertNotEqual(res.returncode, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/third_party/iolinki-master/west.yml b/third_party/iolinki-master/west.yml new file mode 100644 index 000000000..0311ed556 --- /dev/null +++ b/third_party/iolinki-master/west.yml @@ -0,0 +1,46 @@ +# Copyright (C) 2026 Andrii Shylenko +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Standalone west manifest for iolinki-master. +# +# This manifest lets you use iolinki-master as the manifest repository of a +# fresh west workspace, pulling in Zephyr and the iolinki device stack (which +# supplies the shared frame/CRC helpers and public headers) alongside it: +# +# west init -m https://github.com/w1ne/iolinki-master --mr main iolinki-ws +# cd iolinki-ws +# west update +# west build -b native_sim iolinki-master/samples/iolink_master +# +# The pinned Zephyr revision below tracks Zephyr 'main'; the master core uses +# only portable C99 and no Zephyr APIs directly, so it builds across Zephyr 3.7 +# LTS through main. The native_sim sample is verified to compile and link green +# in CI (see the zephyr-validation job in .github/workflows/ci.yml). +# +# For production, do NOT use this file: add iolinki-master and iolinki as +# projects entries in your OWN west.yml pinned to your chosen Zephyr/SDK. +manifest: + self: + # iolinki-master is the manifest repo; it is also a Zephyr module via + # zephyr/module.yml. + path: iolinki-master + west-commands: "" + + remotes: + - name: zephyrproject-rtos + url-base: https://github.com/zephyrproject-rtos + - name: w1ne + url-base: https://github.com/w1ne + + projects: + # The iolinki device stack is a Zephyr module too; the master reuses its + # frame/CRC helper sources and headers. + - name: iolinki + remote: w1ne + revision: develop + path: iolinki + + - name: zephyr + remote: zephyrproject-rtos + revision: main + import: true diff --git a/third_party/iolinki-master/zephyr/CMakeLists.txt b/third_party/iolinki-master/zephyr/CMakeLists.txt new file mode 100644 index 000000000..939a78cf3 --- /dev/null +++ b/third_party/iolinki-master/zephyr/CMakeLists.txt @@ -0,0 +1,50 @@ +# Copyright (C) 2026 Andrii Shylenko +# SPDX-License-Identifier: GPL-3.0-or-later + +if(CONFIG_IOLINKI_MASTER) + +# The master builds against the iolinki device stack, which provides the shared +# IO-Link frame/CRC helper sources (src/crc.c, src/frame.c) and the public +# headers. In a west workspace the iolinki repo is a sibling +# Zephyr module, so Zephyr exports its location as ZEPHYR_IOLINKI_MODULE_DIR. +# Fall back to a sibling checkout for non-west builds (ZEPHYR_EXTRA_MODULES). +if(DEFINED ZEPHYR_IOLINKI_MODULE_DIR) + set(IOLINKI_DEVICE_DIR "${ZEPHYR_IOLINKI_MODULE_DIR}") +else() + set(IOLINKI_DEVICE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../iolinki") +endif() + +if(NOT EXISTS "${IOLINKI_DEVICE_DIR}/src/frame.c") + message(FATAL_ERROR + "iolinki device stack not found at '${IOLINKI_DEVICE_DIR}'. Add the " + "iolinki repository to your west workspace (see the top-level west.yml) " + "or point ZEPHYR_EXTRA_MODULES at a sibling checkout.") +endif() + +zephyr_library() + +# Public master API (iolinki_master/*) and the device stack's public headers +# (iolinki/*) are exported globally so the library and the application can both +# include them, e.g. and . +zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../include) +zephyr_include_directories(${IOLINKI_DEVICE_DIR}/include) + +zephyr_library_sources( + ../src/master_controller.c + ../src/master_isdu.c + ../src/master_parameters.c + ../src/master_port.c + ../src/master_sio.c +) + +# Shared IO-Link frame/CRC helpers from the device stack. When the device +# module is also enabled (CONFIG_IOLINKI=y) it compiles these itself, so build +# them here only when the device library is absent, to avoid duplicate symbols. +if(NOT CONFIG_IOLINKI) + zephyr_library_sources( + ${IOLINKI_DEVICE_DIR}/src/crc.c + ${IOLINKI_DEVICE_DIR}/src/frame.c + ) +endif() + +endif() # CONFIG_IOLINKI_MASTER diff --git a/third_party/iolinki-master/zephyr/Kconfig b/third_party/iolinki-master/zephyr/Kconfig new file mode 100644 index 000000000..14460326c --- /dev/null +++ b/third_party/iolinki-master/zephyr/Kconfig @@ -0,0 +1,26 @@ +# Copyright (C) 2026 Andrii Shylenko +# SPDX-License-Identifier: GPL-3.0-or-later + +menuconfig IOLINKI_MASTER + bool "IO-Link Master Stack" + default y + help + Enable the iolinki IO-Link Master Stack. This module implements the + master-side IO-Link Data Link Layer, ISDU engine, parameter/data-storage + handling and SIO fallback, driving a device through a hardware-agnostic + PHY abstraction (iolink_phy_api_t). It reuses the shared frame/CRC helper + sources and public headers from the iolinki device stack, + which must be present in the same west workspace (see west.yml). + +if IOLINKI_MASTER + +config IOLINKI_MASTER_CONTROLLER + bool "Multi-port master controller helpers" + default y + help + Build the multi-port controller layer (iolink_master_controller_*), which + manages an array of ports from a single tick/scheduling entry point. The + controller sources are always compiled with the master library; this + symbol is provided so applications can gate controller-only code. + +endif # IOLINKI_MASTER diff --git a/third_party/iolinki-master/zephyr/module.yml b/third_party/iolinki-master/zephyr/module.yml new file mode 100644 index 000000000..d453f24ce --- /dev/null +++ b/third_party/iolinki-master/zephyr/module.yml @@ -0,0 +1,6 @@ +name: iolinki_master +build: + kconfig: zephyr/Kconfig + cmake: zephyr +samples: + - samples/iolink_master