diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index d052837..90832fc 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -1,4 +1,5 @@ -# This workflow will build a .NET project +# Continuous integration: build and test on every push and pull request. +# Publishing to NuGet is handled separately by Release.yml (tag-gated). # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net name: Build @@ -16,69 +17,18 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # Required for GitVersion to access full commit history + fetch-depth: 0 # Nerdbank.GitVersioning needs full history to compute the version - name: Setup .NET uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - - name: Setup GitVersion - uses: GitTools/actions/gitversion/setup@v0.9.10 - with: - versionSpec: '5.x' - - - name: Run GitVersion - id: gitversion - uses: GitTools/actions/gitversion/execute@v0.9.10 - - name: Restore dependencies run: dotnet restore src/Z21.sln - - - name: Build for testing (Debug) - run: dotnet build src/Z21.sln --no-restore /p:Version=${{ steps.gitversion.outputs.semVer }} - - - name: Test - run: dotnet test src/Z21.sln --no-build --verbosity normal - - name: Build for packaging (Release) - run: dotnet build src/Z21.sln --no-restore --configuration Release /p:Version=${{ steps.gitversion.outputs.semVer }} - - - name: Tag commit with GitVersion - if: github.event_name == 'push' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - git tag v${{ steps.gitversion.outputs.semVer }} - git push origin v${{ steps.gitversion.outputs.semVer }} - - - name: Pack Z21.sln NuGet package - run: | - dotnet pack src/Z21.sln --no-build --configuration Release \ - /p:PackageVersion=${{ steps.gitversion.outputs.semVer }} \ - /p:Version=${{ steps.gitversion.outputs.semVer }} \ - --output ./nupkgs + - name: Build (Release) + run: dotnet build src/Z21.sln --no-restore --configuration Release - - name: Pack Z21.DependencyInjection NuGet package - run: | - dotnet pack src/Z21.DependencyInjection/Z21.DependencyInjection.csproj --no-build --configuration Release \ - /p:PackageVersion=${{ steps.gitversion.outputs.semVer }} \ - /p:Version=${{ steps.gitversion.outputs.semVer }} \ - --output ./nupkgs - - - name: Pack Z21.Autofac NuGet package - run: | - dotnet pack src/Z21.Autofac/Z21.Autofac.csproj --no-build --configuration Release \ - /p:PackageVersion=${{ steps.gitversion.outputs.semVer }} \ - /p:Version=${{ steps.gitversion.outputs.semVer }} \ - --output ./nupkgs - - - name: Push packages to NuGet - if: | - github.ref == 'refs/heads/main' - run: | - for pkg in ./nupkgs/*.nupkg; do - dotnet nuget push "$pkg" \ - --api-key ${{ secrets.NUGET_API_KEY }} \ - --source https://api.nuget.org/v3/index.json - done + - name: Test + run: dotnet test src/Z21.sln --no-build --configuration Release --verbosity normal diff --git a/.github/workflows/BuildAndDeployDoc.yml b/.github/workflows/BuildAndDeployDoc.yml index 53da436..4a73c94 100644 --- a/.github/workflows/BuildAndDeployDoc.yml +++ b/.github/workflows/BuildAndDeployDoc.yml @@ -1,37 +1,67 @@ name: Github Pages +# Builds the API documentation with DocFX (generated from the source XML doc comments) +# and publishes it with GitHub's native Pages deployment. Nothing is committed to the +# repository. Requires Settings -> Pages -> Source = "GitHub Actions". on: push: branches: [ "main" ] + paths: + - 'docfx/**' + - 'src/**' + - '.github/workflows/BuildAndDeployDoc.yml' pull_request: branches: [ "main" ] + paths: + - 'docfx/**' + - 'src/**' + - '.github/workflows/BuildAndDeployDoc.yml' workflow_dispatch: +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment; let an in-progress run finish rather than cancel it. +concurrency: + group: pages + cancel-in-progress: false + jobs: build: runs-on: ubuntu-latest - steps: - name: Checkout - uses: actions/checkout@v3 - - - name: Copy README as index.md - run: cp README.md src/index.md + uses: actions/checkout@v4 + with: + fetch-depth: 0 # DocFX builds the projects; Nerdbank.GitVersioning needs full history - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + dotnet-version: 8.0.x - name: Install DocFX run: dotnet tool install -g docfx - - name: Build Docs - run: docfx src/docfx.json + - name: Build docs (docfx -> docfx/_site) + run: docfx docfx/docfx.json - - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v3 + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./src/_site - destination_dir: . + path: docfx/_site + + deploy: + # PRs only build (to validate); deployment happens on main / manual dispatch. + if: github.event_name != 'pull_request' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/MutationTesting.yml b/.github/workflows/MutationTesting.yml index 2f47a0f..9bab18e 100644 --- a/.github/workflows/MutationTesting.yml +++ b/.github/workflows/MutationTesting.yml @@ -15,10 +15,12 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Nerdbank.GitVersioning needs full history to compute the version - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: dotnet-version: '8.0.x' diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml new file mode 100644 index 0000000..3edd8d9 --- /dev/null +++ b/.github/workflows/Release.yml @@ -0,0 +1,57 @@ +# Continuous delivery: pack and publish the NuGet packages on every merge to main. +# +# Versions are produced automatically by Nerdbank.GitVersioning (version.json + git +# height), so they increment on every commit — no tags or manual version bumps. A push +# to main (which is what merging a pull request produces) builds, tests, packs the whole +# solution once, and pushes all packages to NuGet.org with --skip-duplicate. + +name: Release + +on: + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-main + cancel-in-progress: false + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Nerdbank.GitVersioning needs full history to compute the version + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore src/Z21.sln + + - name: Build (Release) + run: dotnet build src/Z21.sln --no-restore --configuration Release + + - name: Test + run: dotnet test src/Z21.sln --no-build --configuration Release --verbosity normal + + - name: Pack + run: dotnet pack src/Z21.sln --no-build --configuration Release --output ./nupkgs + + - name: Upload packages artifact + uses: actions/upload-artifact@v4 + with: + name: nupkgs + path: ./nupkgs/*.nupkg + + - name: Push to NuGet + if: github.ref == 'refs/heads/main' # never publish from a manually-dispatched non-main branch + run: dotnet nuget push "./nupkgs/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.gitignore b/.gitignore index 4fbe19f..22a1650 100644 --- a/.gitignore +++ b/.gitignore @@ -396,4 +396,9 @@ _UpgradeReport_Files/ Thumbs.db Desktop.ini -.DS_Store \ No newline at end of file +.DS_Store + +# DocFX generated output (the site is published by CI via GitHub Pages, never committed) +docfx/_site/ +docfx/api/ +**/*.manifest \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..95315a8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,154 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Overview + +C# client library implementing the **ROCO/Fleischmann Z21 LAN protocol** (V1.13) over UDP. Platform-independent, event-driven, SOLID. The protocol spec PDFs are linked in `README.md`; the command/response support matrix (✅/❌) also lives there and should be kept in sync when commands or handlers are added. + +## Build, Test, Run + +All commands operate on the solution at `src/Z21.sln`. The library targets `net8.0;net8.0-windows` and only builds for the `x64` platform. + +```bash +dotnet restore src/Z21.sln +dotnet build src/Z21.sln +dotnet test src/Z21.sln # all tests +dotnet test src/Z21.sln --filter "FullyQualifiedName~SetLocoDriveCommandTest" # single test class +dotnet run --project src/Z21.Console # demo console app against a live/simulated Z21 +``` + +Mutation testing (Stryker.NET) is run from inside the test project and gates CI: + +```bash +dotnet tool install -g dotnet-stryker +cd src/Z21.Client.UnitTest +dotnet stryker --reporter html --reporter progress --mutation-level Complete --threshold-high 98 --threshold-low 90 --break-at 85 +``` + +These thresholds (and `coverage-analysis: perTest`) are pinned in `stryker-config.json` in each test +project, so a bare `dotnet stryker` uses them. Target line coverage is ~95%. `Z21.Client` (all protocol +logic) holds **break 85** and currently scores ~85%. `CommandStation.Transport.Udp` uses a lower +**break 60** (reports 90/98): its residual mutants are non-observable socket internals (`UdpClient` is +sealed/non-mockable — resource disposal, `AllowNatTraversal`, `GC.SuppressFinalize`, equivalent logical +ops), so ~66% is the accepted floor for that thin transport shell. + +Tests use **NUnit + Moq**. New protocol logic is expected to be both unit-tested and to survive mutation testing — the bar is high, so assert on exact datagram bytes. + +> **Architecture is mid-refactor to v7** (decoupling transport + protocol so other command +> stations can be added). The layering below is the current state. The neutral root namespace +> `CommandStation` is provisional (brand name TBD). See the plan and the `architecture-refactor-v7` +> memory for context. + +## Projects + +Two orthogonal axes are separated into assemblies: **transport** (how bytes move) and **protocol** +(the Z21 wire format). The abstractions assembly is protocol- and transport-neutral. + +- **CommandStation.Abstractions** (ns `CommandStation`) — neutral contracts, no Z21/UDP specifics: + `ITransport`, `IFrameReader` (+ event args) under `Transport`/`Framing`; the domain API + `ICommandStation` + capability interfaces (`ILocoControl`, `IAccessoryControl`, + `ITrackPowerControl`, `ISystemInfoProvider`, `IProgrammingControl`, `IFeedbackControl`, + `IFastClockControl`); and the domain vocabulary under `Model` + (enums like `DccSpeedMode`/`DrivingDirection`, data like `LocoInfoData`/`SystemState`/`FirmwareVersion`/ + `CvValue`/`FeedbackData`/`ModelTime`). Z21-only protocol features (LocoNet raw tunneling, CAN, RailCom, + zLink booster/decoder/adapter) have **no neutral capability**; reach them via the Z21 escape hatch + (`IZ21CommandStation.Commands` + the Z21 response-handler events). +- **CommandStation.Transport.Udp** — `UdpTransport : ITransport` + `UdpTransportOptions`. A future + serial transport would be a sibling assembly; nothing in `Z21.Client` references it directly. +- **Z21.Client** (NuGet id `Z21`, root ns `Z21`) — the Z21 protocol implementation: commands + + `IZ21CommandFactory`, response handlers/parsers, `Z21FrameReader`/`Z21FrameBuilder`, the + `IAddressCodec`/`ILocoSpeedCodec` codecs, and `Z21CommandStation : IZ21CommandStation`. References + only `CommandStation.Abstractions`. A `global using CommandStation.Model;` makes the domain + vocabulary available without per-file usings. +- **Z21.DependencyInjection** / **Z21.Autofac** — `AddZ21(...)` extensions; reference + `CommandStation.Transport.Udp` to wire the concrete UDP transport. +- **Z21.Console** — runnable demo / manual test harness. +- **\*.UnitTest(s)** — one test project per shippable project. + +## Architecture + +Four layers, bottom → top — transport and protocol are independent: + +- **Transport** (`ITransport`) — a raw byte pipe (`ConnectAsync`/`DisconnectAsync`/`SendAsync`, + `OnBytesReceived`, `OnConnectionChanged`). `UdpTransport` today; serial later. +- **Framing** (`IFrameReader`) — reassembles the byte stream into discrete frames. `Z21FrameReader` + buffers partial frames using the `DataLen` length-prefix, so it is correct for both message + (UDP) and stream (serial) transports. +- **Protocol** — Z21 encode/decode. `IZ21FrameBuilder` (+ `IAddressCodec`/`ILocoSpeedCodec`) builds + command bytes (`BuildXBus`/`BuildLan`); handlers (`IZ21ResponseHandler`, `CanHandle`/`Handle`) and + parsers (`IZ21ResponseParser`) decode frames and raise typed `On...Received` events. Commands and + these services are all **injected**, never static. +- **Domain** (`ICommandStation` + capabilities) — the protocol-agnostic public API. `Z21CommandStation` + implements it (+ a Z21 raw escape hatch `IZ21CommandStation` exposing `Commands` and + `SendCommandsAsync(params IZ21Command[])`). + +Data flow: + +1. `ICommandStation` op (e.g. `DriveAsync`) → `IZ21CommandFactory` builds an `IZ21Command` (bytes via + `IZ21FrameBuilder` + codecs) → `SendCommandsAsync` concatenates command `Data` into **one packet** + (so simultaneous actions like double-traction stay atomic), enforces `MaxUdpPayload` (1472), and + sends via `ITransport`. A `DelayedAction` keep-alive re-sends a firmware query after + `Z21Options.KeepAliveInterval` (default 45s). +2. `ITransport.OnBytesReceived` → `Z21FrameReader.Append` → `OnFrameReceived` per complete frame. +3. The dispatcher `Z21ResponseHandler` (distinct from individual handlers) offers each frame to every + `IZ21ResponseHandler` whose `CanHandle` returns true; handler exceptions are caught and logged. +4. Handlers raise typed events; `Z21CommandStation` re-raises them as neutral capability events + (`LocoInfoReceived`, `SystemStateReceived`, `TrackPowerChanged`, …). + +`Z21CommandStation.ConnectAsync` connects the transport then runs `LogOnAsync` (broadcast flags + +firmware query). There is **no ICMP watchdog** — liveness is the transport connection state plus the +protocol keep-alive (the old `Z21Watchdog` was removed as part of the transport decoupling). + +The dispatcher must be instantiated for inbound handling to work — both DI extensions register it as +an **activated/auto-activated singleton** so it wires up `ITransport.OnBytesReceived` eagerly. + +### DI registration + +Both `Z21DependencyInjectionExtension` and `Z21AutofacExtensions` discover all `IZ21ResponseHandler` / +`IZ21ResponseParser` implementations by reflection and register each concrete type plus all of its +handler/parser interfaces as singletons. **Adding a new handler or parser requires no registration +changes** — implement the interface and it is picked up automatically. `AddZ21(...)` takes optional +`Action` and `Action` configurators. Both containers must stay +behavior-equivalent. + +### Conventions + +- **Coding rules are strict** (see the `coding-rules` memory): no static methods/properties except + `const` fields; no empty catch blocks; a new subtype must require zero edits outside its own file; + TDD test-first with a quotable red run. Committing is allowed, but **never include AI + attribution in anything that touches git or GitHub** — not in commit messages, PR titles or + descriptions, issue/PR comments, tags, or release notes. Concretely: no `Co-Authored-By` + trailer, no "Generated with Claude Code" (or any similar "made/assisted by AI") line, and no + AI tool name anywhere in the history or on GitHub. This applies to every `git` and `gh`/GitHub + API action without exception. +- The library assumes a **little-endian** host (`Z21CommandStation` throws + `PlatformNotSupportedException` otherwise); protocol multi-byte fields are little-endian. +- Command construction goes through `IZ21CommandFactory` (the station exposes it as `Commands`); a new + command is one new file plus an optional factory method. +- Custom exceptions live in `Core/Exception/`; `MtuPayloadLengthExceededException.ThrowIfExceeded` + guards payload size against `Z21CommandStation.MaxUdpPayload`. +- Logging is via `ILogger?` (Microsoft.Extensions.Logging.Abstractions), always optional. + +## Versioning & CI + +- **Versioning — Nerdbank.GitVersioning (nbgv).** `version.json` at the repo root holds + the base version (`"version": "7.0"`) and `publicReleaseRefs` (`^refs/heads/main$`). nbgv + is referenced once in the root `Directory.Build.props` (`PrivateAssets="all"`), so it + stamps the assembly **and** NuGet package versions of every project automatically from + the base version + **git height** — the patch increments on every commit (`7.0.` + on `main`; off-`main` builds get a `-g` prerelease suffix). There is **no** + `-p:Version` in CI and no hand-bumping; bump `"version"` in `version.json` for the next + minor/major. **Consequence:** nbgv fails on shallow clones, so every workflow that builds + (`Build.yml`, `Release.yml`, `MutationTesting.yml`) checks out with `fetch-depth: 0`. +- **CI — `.github/workflows/Build.yml`:** builds (`Release`) and tests on every push and + pull request. It does **not** pack or publish. +- **CD — `.github/workflows/Release.yml`:** publishes on every **push to `main`** (i.e. + when a PR is merged) and on manual `workflow_dispatch`. It builds → tests → packs the + whole solution **once** (`dotnet pack src/Z21.sln`, producing all five packable packages + — `Z21`, `Z21.DependencyInjection`, `Z21.Autofac`, `CommandStation.Abstractions`, + `CommandStation.Transport.Udp`; Console + test projects are `IsPackable=false`) → pushes + with `--skip-duplicate`. The push step is guarded `if: github.ref == 'refs/heads/main'`, + so a manual dispatch from a non-`main` branch is a pack-only dry run. +- The `GeneratePackageOnBuild=true` properties in the five packable csproj are redundant + with the explicit Release pack and may be removed later. diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..2121924 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/GitVersion.yaml b/GitVersion.yaml deleted file mode 100644 index f35971e..0000000 --- a/GitVersion.yaml +++ /dev/null @@ -1,167 +0,0 @@ -assembly-versioning-scheme: MajorMinorPatch -assembly-file-versioning-scheme: MajorMinorPatch -tag-prefix: '[vV]?' -version-in-branch-pattern: (?[vV]?\d+(\.\d+)?(\.\d+)?).* -major-version-bump-message: \+semver:\s?(breaking|major) -minor-version-bump-message: \+semver:\s?(feature|minor) -patch-version-bump-message: \+semver:\s?(fix|patch) -no-bump-message: \+semver:\s?(none|skip) -tag-pre-release-weight: 60000 -commit-date-format: yyyy-MM-dd -merge-message-formats: {} -update-build-number: true -semantic-version-format: Strict -strategies: -- Fallback -- ConfiguredNextVersion -- MergeMessage -- TaggedCommit -- TrackReleaseBranches -- VersionInBranchName -branches: - develop: - mode: ContinuousDeployment - label: alpha - increment: Minor - prevent-increment: - when-current-commit-tagged: false - track-merge-target: true - track-merge-message: true - regex: ^dev(elop)?(ment)?$ - source-branches: - - main - is-source-branch-for: [] - tracks-release-branches: true - is-release-branch: false - is-main-branch: false - pre-release-weight: 0 - main: - label: '' - increment: Patch - prevent-increment: - of-merged-branch: true - track-merge-target: false - track-merge-message: true - regex: ^master$|^main$ - source-branches: [] - is-source-branch-for: [] - tracks-release-branches: false - is-release-branch: false - is-main-branch: true - pre-release-weight: 55000 - release: - mode: ContinuousDeployment - label: beta - increment: Minor - prevent-increment: - of-merged-branch: true - when-current-commit-tagged: false - track-merge-target: false - regex: ^releases?[\/-](?.+) - source-branches: - - main - - support - is-source-branch-for: [] - tracks-release-branches: false - is-release-branch: true - is-main-branch: false - pre-release-weight: 30000 - feature: - mode: ContinuousDeployment - label: '{BranchName}' - increment: Inherit - prevent-increment: - when-current-commit-tagged: false - track-merge-message: true - regex: ^features?[\/-](?.+) - source-branches: - - develop - - main - - release - - support - - hotfix - is-source-branch-for: [] - is-main-branch: false - pre-release-weight: 30000 - pull-request: - mode: ContinuousDeployment - label: PullRequest{Number} - increment: Inherit - prevent-increment: - of-merged-branch: true - when-current-commit-tagged: false - track-merge-message: true - regex: ^(pull-requests|pull|pr)[\/-](?\d*) - source-branches: - - develop - - main - - release - - feature - - support - - hotfix - is-source-branch-for: [] - pre-release-weight: 30000 - hotfix: - mode: ManualDeployment - label: beta - increment: Inherit - prevent-increment: - when-current-commit-tagged: false - regex: ^hotfix(es)?[\/-](?.+) - source-branches: - - main - - support - is-source-branch-for: [] - is-release-branch: true - is-main-branch: false - pre-release-weight: 30000 - support: - label: '' - increment: Patch - prevent-increment: - of-merged-branch: true - track-merge-target: false - regex: ^support[\/-](?.+) - source-branches: - - main - is-source-branch-for: [] - tracks-release-branches: false - is-release-branch: false - is-main-branch: true - pre-release-weight: 55000 - unknown: - mode: ManualDeployment - label: '{BranchName}' - increment: Inherit - prevent-increment: - when-current-commit-tagged: true - regex: (?.+) - source-branches: - - main - - develop - - release - - feature - - pull-request - - hotfix - - support - is-source-branch-for: [] - is-main-branch: false -ignore: - sha: [] - paths: [] -mode: ContinuousDeployment -label: '{BranchName}' -increment: Inherit -prevent-increment: - of-merged-branch: false - when-branch-merged: false - when-current-commit-tagged: true -track-merge-target: false -track-merge-message: true -commit-message-incrementing: Enabled -regex: '' -source-branches: [] -is-source-branch-for: [] -tracks-release-branches: false -is-release-branch: false -is-main-branch: false \ No newline at end of file diff --git a/README.md b/README.md index d272e5a..1bea464 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Z21 [![Build](https://github.com/Jakob-Eichberger/Z21/actions/workflows/Build.yml/badge.svg)](https://github.com/Jakob-Eichberger/Z21/actions/workflows/Build.yml) [![Github Pages](https://github.com/Jakob-Eichberger/Z21/actions/workflows/BuildAndDeployDoc.yml/badge.svg)](https://github.com/Jakob-Eichberger/Z21/actions/workflows/BuildAndDeployDoc.yml) [![Mutation Testing](https://github.com/Jakob-Eichberger/Z21/actions/workflows/MutationTesting.yml/badge.svg)](https://github.com/Jakob-Eichberger/Z21/actions/workflows/MutationTesting.yml) +# Z21 [![Build](https://github.com/jaak0b/Z21/actions/workflows/Build.yml/badge.svg)](https://github.com/jaak0b/Z21/actions/workflows/Build.yml) [![Github Pages](https://github.com/jaak0b/Z21/actions/workflows/BuildAndDeployDoc.yml/badge.svg)](https://github.com/jaak0b/Z21/actions/workflows/BuildAndDeployDoc.yml) [![Mutation Testing](https://github.com/jaak0b/Z21/actions/workflows/MutationTesting.yml/badge.svg)](https://github.com/jaak0b/Z21/actions/workflows/MutationTesting.yml) @@ -25,23 +25,52 @@ The official documentation of the protocol can be downloaded from the ROCO homep - System ✅ - Driving ✅ - Switching ✅ + - CV / POM programming ✅ + - R-BUS feedback ✅ + - RailCom ✅ + - LocoNet gateway ✅ + - CAN (detector & booster) ✅ + - Fast clock (model time) ✅ + - zLink booster / decoder / adapter ✅ +## Documentation + +The API reference, generated from the library's source XML documentation comments, plus a +short getting-started guide, is published at **[jaak0b.github.io/Z21](https://jaak0b.github.io/Z21/)**. +It is built with [DocFX](https://dotnet.github.io/docfx/) from the config in [`docfx/`](docfx); +to preview locally run `dotnet tool install -g docfx` then `docfx docfx/docfx.json --serve`. + ## Getting Started Get started by downloading the provided [Z21](https://www.nuget.org/packages/Z21/) nuget package. -### Commands -All Commands can be found in the Z21.Core.Command namespace. -#### Sending Commands -> [!WARNING] -> When sending multiple commands at once take note of the maximum payload length. If the commands exceeds that length an exception will be thrown. +### Using the command station +The headline API is the protocol-agnostic `ICommandStation` (implemented for Z21 by `Z21CommandStation`). +Resolve it from your container, connect, then drive locomotives, switch turnouts, control track power and +subscribe to status events through its capability interfaces (`ILocoControl`, `IAccessoryControl`, +`ITrackPowerControl`, `ISystemInfoProvider`). -Create a command instance and hand it to the Z21Client.SendCommandsAsync method. -Multiple commands can be send at the same time in the same UDP packet. -This is important if certain actions should happen at the same time (i.e. controlling locos in a double traction where it is critical that both locomotives change speed at the same time) +```csharp + var station = container.Resolve(); // or ICommandStation + await station.ConnectAsync(); + + station.LocoInfoReceived += (_, loco) => Console.WriteLine($"Loco {loco.LocoAddress} @ {loco.LocoSpeed}"); + + await station.DriveAsync(locoAddress: 13, DccSpeedMode.Steps128, DrivingDirection.Forward, speed: 40); + await station.TrackPowerOffAsync(); +``` + +#### Raw commands +For full control you can still build and send raw Z21 commands. Build them via `station.Commands` +(an `IZ21CommandFactory`) and hand them to `SendCommandsAsync`. +Multiple commands are sent in the same UDP packet — important when actions must happen simultaneously +(e.g. a double-traction where both locomotives must change speed at once). + +> [!WARNING] +> When sending multiple commands at once take note of the maximum payload length. If the commands exceed that length an exception will be thrown. ```csharp - await Z21Client.SendCommandsAsync(new GetFirmwareVersionCommand()); // Sends a single command - await Z21Client.SendCommandsAsync(new GetFirmwareVersionCommand(), new GetLocoInfoCommand(locoAddress: 13); // Send multiple commands in a single UDP packet + await station.SendCommandsAsync(station.Commands.Create()); // single command + await station.SendCommandsAsync(station.Commands.Create(), station.Commands.Create((ushort)13)); // one UDP packet ``` ### @@ -53,10 +82,14 @@ This is important if certain actions should happen at the same time (i.e. contro ```csharp var builder = new ContainerBuilder(); - builder.AddZ21(); - var container = builder.Build(); + builder.AddZ21(transport => transport.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.0.111"), 21105)); + var container = builder.Build(); + var station = container.Resolve(); ``` +`AddZ21` optionally takes an `Action` (transport/endpoint settings) and an +`Action` (protocol settings such as broadcast flags and keep-alive interval). + ### Dependency Injection Dependency Injection is supported natively via [Z21.DependencyInjection](https://www.nuget.org/packages/Z21.DependencyInjection/) and requires the use of hosted services. Z21 registers background components that must run inside the .NET Generic Host lifecycle. @@ -88,11 +121,11 @@ The host is responsible for starting all Z21‑related hosted services and manag | LAN_X_GET_STATUS | ✅ | | | LAN_X_SET_TRACK_POWER_OFF | ✅ | | | LAN_X_SET_TRACK_POWER_ON | ✅ | | -| LAN_X_DCC_READ_REGISTER | ❌ | | +| LAN_X_DCC_READ_REGISTER | ✅ | | | LAN_X_CV_READ | ✅ | | -| LAN_X_DCC_WRITE_REGISTER | ❌ | | -| LAN_X_CV_WRITE | ❌ | | -| LAN_X_MM_WRITE_BYTE | ❌ | | +| LAN_X_DCC_WRITE_REGISTER | ✅ | | +| LAN_X_CV_WRITE | ✅ | | +| LAN_X_MM_WRITE_BYTE | ✅ | | | LAN_X_GET_TURNOUT_INFO | ✅ | | | LAN_X_GET_EXT_ACCESSORY_INFO | ✅ | | | LAN_X_SET_TURNOUT | ✅ | | @@ -103,14 +136,14 @@ The host is responsible for starting all Z21‑related hosted services and manag | LAN_X_GET_LOCO_INFO | ✅ | | | LAN_X_SET_LOCO_DRIVE | ✅ | | | LAN_X_SET_LOCO_FUNCTION | ✅ | | -| LAN_X_SET_LOCO_FUNCTION_GROUP | ❌ | | -| LAN_X_SET_LOCO_BINARY_STATE | ❌ | | -| LAN_X_CV_POM_WRITE_BYTE | ❌ | | -| LAN_X_CV_POM_WRITE_BIT | ❌ | | -| LAN_X_CV_POM_READ_BYTE | ❌ | | -| LAN_X_CV_POM_ACCESSORY_WRITE_BYTE | ❌ | | -| LAN_X_CV_POM_ACCESSORY_WRITE_BIT | ❌ | | -| LAN_X_CV_POM_ACCESSORY_READ_BYTE | ❌ | | +| LAN_X_SET_LOCO_FUNCTION_GROUP | ✅ | | +| LAN_X_SET_LOCO_BINARY_STATE | ✅ | | +| LAN_X_CV_POM_WRITE_BYTE | ✅ | | +| LAN_X_CV_POM_WRITE_BIT | ✅ | | +| LAN_X_CV_POM_READ_BYTE | ✅ | | +| LAN_X_CV_POM_ACCESSORY_WRITE_BYTE | ✅ | | +| LAN_X_CV_POM_ACCESSORY_WRITE_BIT | ✅ | | +| LAN_X_CV_POM_ACCESSORY_READ_BYTE | ✅ | | | LAN_X_GET_FIRMWARE_VERSION | ✅ | | | LAN_SET_BROADCASTFLAGS | ✅ | | | LAN_GET_BROADCASTFLAGS | ✅ | | @@ -118,28 +151,28 @@ The host is responsible for starting all Z21‑related hosted services and manag | LAN_SET_LOCOMODE | ✅ | | | LAN_GET_TURNOUTMODE | ✅ | | | LAN_SET_TURNOUTMODE | ✅ | | -| LAN_RMBUS_GETDATA | ❌ | | -| LAN_RMBUS_PROGRAMMODULE | ❌ | | +| LAN_RMBUS_GETDATA | ✅ | | +| LAN_RMBUS_PROGRAMMODULE | ✅ | | | LAN_SYSTEMSTATE_GETDATA | ✅ | | -| LAN_RAILCOM_GETDATA | ❌ | | -| LAN_LOCONET_FROM_LAN | ❌ | | -| LAN_LOCONET_DISPATCH_ADDR | ❌ | | -| LAN_LOCONET_DETECTOR | ❌ | | -| LAN_CAN_DETECTOR | ❌ | | -| LAN_CAN_DEVICE_GET_DESCRIPTION | ❌ | | -| LAN_CAN_DEVICE_SET_DESCRIPTION | ❌ | | -| LAN_CAN_BOOSTER_SET_TRACKPOWER | ❌ | | -| LAN_FAST_CLOCK_CONTROL | ❌ | | -| LAN_FAST_CLOCK_SETTINGS_GET | ❌ | | -| LAN_FAST_CLOCK_SETTINGS_SET | ❌ | | -| LAN_BOOSTER_SET_POWER |❌ | | -| LAN_BOOSTER_GET_DESCRIPTION | ❌ | | -| LAN_BOOSTER_SET_DESCRIPTION | ❌ | | -| LAN_BOOSTER_SYSTEMSTATE_GETDATA | ❌ | | -| LAN_DECODER_GET_DESCRIPTION | ❌ | | -| LAN_DECODER_SET_DESCRIPTION | ❌ | | -| LAN_DECODER_SYSTEMSTATE_GETDATA | ❌ | | -| LAN_ZLINK_GET_HWINFO| ❌ | | +| LAN_RAILCOM_GETDATA | ✅ | | +| LAN_LOCONET_FROM_LAN | ✅ | | +| LAN_LOCONET_DISPATCH_ADDR | ✅ | | +| LAN_LOCONET_DETECTOR | ✅ | | +| LAN_CAN_DETECTOR | ✅ | | +| LAN_CAN_DEVICE_GET_DESCRIPTION | ✅ | | +| LAN_CAN_DEVICE_SET_DESCRIPTION | ✅ | | +| LAN_CAN_BOOSTER_SET_TRACKPOWER | ✅ | | +| LAN_FAST_CLOCK_CONTROL | ✅ | | +| LAN_FAST_CLOCK_SETTINGS_GET | ✅ | | +| LAN_FAST_CLOCK_SETTINGS_SET | ✅ | | +| LAN_BOOSTER_SET_POWER |✅ | | +| LAN_BOOSTER_GET_DESCRIPTION | ✅ | | +| LAN_BOOSTER_SET_DESCRIPTION | ✅ | | +| LAN_BOOSTER_SYSTEMSTATE_GETDATA | ✅ | | +| LAN_DECODER_GET_DESCRIPTION | ✅ | | +| LAN_DECODER_SET_DESCRIPTION | ✅ | | +| LAN_DECODER_SYSTEMSTATE_GETDATA | ✅ | | +| LAN_ZLINK_GET_HWINFO| ✅ | | ## Z21 Responses @@ -157,36 +190,36 @@ The host is responsible for starting all Z21‑related hosted services and manag | LAN_X_BC_TRACK_POWER_ON | ✅ | | | LAN_X_BC_PROGRAMMING_MODE | ✅ | | | LAN_X_BC_TRACK_SHORT_CIRCUIT | ✅ | | - | LAN_X_CV_NACK_SC | ❌ | | - | LAN_X_CV_NACK | ❌ | | + | LAN_X_CV_NACK_SC | ✅ | | + | LAN_X_CV_NACK | ✅ | | | LAN_X_UNKNOWN_COMMAND | ✅ | | | LAN_X_STATUS_CHANGED | ✅ | | | LAN_X_GET_VERSION | ✅ | | - | LAN_X_CV_RESULT | ❌ | | + | LAN_X_CV_RESULT | ✅ | | | LAN_X_BC_STOPPED | ✅ | | | LAN_X_LOCO_INFO | ✅ | | | LAN_X_GET_FIRMWARE_VERSION | ✅ | | | LAN_GET_BROADCASTFLAGS | ✅ | | | LAN_GET_LOCOMODE | ✅ | | | LAN_GET_TURNOUTMODE | ✅ | | - | LAN_RMBUS_DATACHANGED | ❌ | | + | LAN_RMBUS_DATACHANGED | ✅ | | | LAN_SYSTEMSTATE_DATACHANGED | ✅ | | - | LAN_RAILCOM_DATACHANGED | ❌ | | - | LAN_LOCONET_Z21_RX | ❌ | | - | LAN_LOCONET_Z21_TX | ❌ | | - | LAN_LOCONET_FROM_LAN | ❌ | | - | LAN_LOCONET_DISPATCH_ADDR | ❌ | | - | LAN_LOCONET_DETECTOR | ❌ | | - | LAN_CAN_DETECTOR | ❌ | | - | LAN_CAN_DEVICE_GET_DESCRIPTION | ❌ | | - | LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD | ❌ | | - | LAN_FAST_CLOCK_DATA | ❌ | | - | LAN_FAST_CLOCK_SETTINGS_GET | ❌ | | - | LAN_BOOSTER_GET_DESCRIPTION | ❌ | | - | LAN_BOOSTER_SYSTEMSTATE_DATACHANGED | ❌ | | - | LAN_DECODER_GET_DESCRIPTION | ❌ | | - | LAN_DECODER_SYSTEMSTATE_DATACHANGED | ❌ | | - | LAN_ZLINK_GET_HWINFO | ❌ | | + | LAN_RAILCOM_DATACHANGED | ✅ | | + | LAN_LOCONET_Z21_RX | ✅ | | + | LAN_LOCONET_Z21_TX | ✅ | | + | LAN_LOCONET_FROM_LAN | ✅ | | + | LAN_LOCONET_DISPATCH_ADDR | ✅ | | + | LAN_LOCONET_DETECTOR | ✅ | | + | LAN_CAN_DETECTOR | ✅ | | + | LAN_CAN_DEVICE_GET_DESCRIPTION | ✅ | | + | LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD | ✅ | | + | LAN_FAST_CLOCK_DATA | ✅ | | + | LAN_FAST_CLOCK_SETTINGS_GET | ✅ | | + | LAN_BOOSTER_GET_DESCRIPTION | ✅ | | + | LAN_BOOSTER_SYSTEMSTATE_DATACHANGED | ✅ | | + | LAN_DECODER_GET_DESCRIPTION | ✅ | | + | LAN_DECODER_SYSTEMSTATE_DATACHANGED | ✅ | | + | LAN_ZLINK_GET_HWINFO | ✅ | | ## Contributing diff --git a/docfx/articles/getting-started.md b/docfx/articles/getting-started.md new file mode 100644 index 0000000..eb4c21c --- /dev/null +++ b/docfx/articles/getting-started.md @@ -0,0 +1,118 @@ +# Getting Started + +This guide takes you from an empty project to driving a locomotive and reacting to +status events. + +## Prerequisites + +- **.NET 8 SDK** or later. +- A **Z21** (or compatible command station) reachable on your network, or a simulator. + The factory-default endpoint is `192.168.0.111:21105`. + +## 1. Install the packages + +The core library is the [`Z21`](https://www.nuget.org/packages/Z21/) package. Add one of +the integration packages for dependency injection: + +```bash +dotnet add package Z21 +dotnet add package Z21.DependencyInjection # Microsoft.Extensions.DependencyInjection +# or +dotnet add package Z21.Autofac # Autofac +``` + +`Z21` contains the full protocol implementation; the integration packages only add the +`AddZ21(...)` registration helper, which wires up the concrete UDP transport and +discovers all response handlers automatically. + +## 2. Register and resolve the command station + +# [.NET DI](#tab/net-di) + +```csharp +using System.Net; +using Microsoft.Extensions.DependencyInjection; +using Z21.Core; +using Z21.DependencyInjection; + +var services = new ServiceCollection(); +services.AddZ21(transport => + transport.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.0.111"), 21105)); + +await using var provider = services.BuildServiceProvider(); +var station = provider.GetRequiredService(); +``` + +# [Autofac](#tab/autofac) + +```csharp +using System.Net; +using Autofac; +using Z21.Autofac; +using Z21.Core; + +var builder = new ContainerBuilder(); +builder.AddZ21(transport => + transport.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.0.111"), 21105)); + +var container = builder.Build(); +var station = container.Resolve(); +``` + +*** + +Resolving `IZ21CommandStation` activates the inbound message dispatcher — if you never +resolve the station, incoming frames are never processed and no events fire. + +## 3. Connect + +`ConnectAsync` opens the UDP transport and logs on to the Z21 (broadcast flags + +firmware query); a protocol keep-alive then runs automatically. + +```csharp +await station.ConnectAsync(); +``` + +## 4. Subscribe to events and drive + +```csharp +station.LocoInfoReceived += (_, loco) => + Console.WriteLine($"Loco {loco.LocoAddress} @ {loco.LocoSpeed} ({loco.DrivingDirection})"); +station.TrackPowerChanged += (_, on) => + Console.WriteLine($"Track power: {(on ? "ON" : "OFF")}"); + +await station.TrackPowerOnAsync(); +await station.DriveAsync( + locoAddress: 13, + speedMode: DccSpeedMode.Steps128, + direction: DrivingDirection.Forward, + speed: 40); +``` + +## 5. Disconnect + +`Z21CommandStation` is `IDisposable`/`IAsyncDisposable`. Disposing (or +`await DisconnectAsync()`) stops the keep-alive and closes the transport. + +```csharp +await station.DisconnectAsync(); +``` + +## Raw commands and Z21-only features + +Features without a neutral capability (LocoNet, CAN, RailCom, zLink) are reachable through +the Z21 escape hatch on `IZ21CommandStation`: build commands with `station.Commands` +(an `IZ21CommandFactory`) and send them with `SendCommandsAsync`. + +```csharp +await station.SendCommandsAsync( + station.Commands.Create(), + station.Commands.Create((ushort)13)); // one UDP packet +``` + +> [!NOTE] +> `Create(...)` binds constructor arguments by exact type — cast integer +> literals to the parameter type (e.g. `(ushort)13`). + +Browse the full API reference — start at [`IZ21CommandStation`](xref:Z21.Core.IZ21CommandStation) — +for every command, handler, capability, and model. diff --git a/docfx/articles/toc.yml b/docfx/articles/toc.yml new file mode 100644 index 0000000..fc96f54 --- /dev/null +++ b/docfx/articles/toc.yml @@ -0,0 +1,2 @@ +- name: Getting Started + href: getting-started.md diff --git a/docfx/docfx.json b/docfx/docfx.json new file mode 100644 index 0000000..e5a38a4 --- /dev/null +++ b/docfx/docfx.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", + "metadata": [ + { + "src": [ + { + "src": "../src", + "files": [ + "Z21.Client/Z21.Client.csproj", + "Z21.DependencyInjection/Z21.DependencyInjection.csproj", + "Z21.Autofac/Z21.Autofac.csproj", + "CommandStation.Abstractions/CommandStation.Abstractions.csproj", + "CommandStation.Transport.Udp/CommandStation.Transport.Udp.csproj" + ] + } + ], + "dest": "api", + "properties": { + "TargetFramework": "net8.0" + } + } + ], + "build": { + "content": [ + { + "files": [ + "index.md", + "toc.yml" + ] + }, + { + "files": [ + "api/**.{yml,md}" + ] + }, + { + "files": [ + "articles/**.{md,yml}" + ] + } + ], + "globalMetadata": { + "_appName": "Z21", + "_appTitle": "Z21", + "_enableSearch": true, + "_appFooter": "Z21 — ROCO/Fleischmann Z21 LAN protocol client for .NET (GPL-3.0)" + }, + "template": [ + "default", + "modern" + ], + "dest": "_site" + } +} diff --git a/docfx/index.md b/docfx/index.md new file mode 100644 index 0000000..ebfc175 --- /dev/null +++ b/docfx/index.md @@ -0,0 +1,63 @@ +# Z21 Client Library + +A platform-independent, event-driven **C# client for the ROCO/Fleischmann Z21 LAN +protocol (V1.13)** over UDP. This site hosts the **API reference**, generated directly +from the library's source XML documentation comments, plus a short +[Getting Started](articles/getting-started.md) guide. + +## Highlights + +- **Protocol-agnostic public API** — the headline interface is `ICommandStation` with + small, focused capability interfaces (`ILocoControl`, `IAccessoryControl`, + `ITrackPowerControl`, `ISystemInfoProvider`, `IProgrammingControl`, `IFeedbackControl`, + `IFastClockControl`). Transport (UDP) and protocol (Z21) are cleanly decoupled. +- **Event-driven** — subscribe to typed events such as `LocoInfoReceived`, + `SystemStateReceived`, `TrackPowerChanged`. +- **Dependency injection out of the box** — `AddZ21(...)` for both + `Microsoft.Extensions.DependencyInjection` and Autofac. +- **Complete protocol coverage** — System, Driving, Switching, CV/POM programming, R-BUS + feedback, RailCom, LocoNet gateway, CAN, fast clock, and the zLink + booster/decoder/adapter messages. + +## Install + +```bash +dotnet add package Z21 +dotnet add package Z21.DependencyInjection # or Z21.Autofac +``` + +## Quick start + +```csharp +using System.Net; +using CommandStation; +using Microsoft.Extensions.DependencyInjection; +using Z21.Core; +using Z21.DependencyInjection; + +var services = new ServiceCollection(); +services.AddZ21(transport => + transport.RemoteEndPoint = new IPEndPoint(IPAddress.Parse("192.168.0.111"), 21105)); + +await using var provider = services.BuildServiceProvider(); +var station = provider.GetRequiredService(); + +station.LocoInfoReceived += (_, loco) => + Console.WriteLine($"Loco {loco.LocoAddress} @ {loco.LocoSpeed}"); + +await station.ConnectAsync(); +await station.TrackPowerOnAsync(); +await station.DriveAsync(13, DccSpeedMode.Steps128, DrivingDirection.Forward, 40); +``` + +See [Getting Started](articles/getting-started.md) for the full walkthrough, or browse +the API reference starting at [`ICommandStation`](xref:CommandStation.ICommandStation). + +## Protocol specification + +The official Z21 LAN protocol documentation is available from ROCO in +[English](https://www.z21.eu/media/Kwc_Basic_DownloadTag_Component/root-en-main_47-1652-959-downloadTag-download/default/d559b9cf/1628743384/z21-lan-protokoll-en.pdf) +and +[German](https://www.z21.eu/media/Kwc_Basic_DownloadTag_Component/47-1652-959-downloadTag/default/69bad87e/1699290251/z21-lan-protokoll.pdf). + +This project is licensed under **GPL-3.0**. diff --git a/docfx/toc.yml b/docfx/toc.yml new file mode 100644 index 0000000..d19422c --- /dev/null +++ b/docfx/toc.yml @@ -0,0 +1,6 @@ +- name: Home + href: index.md +- name: Getting Started + href: articles/getting-started.md +- name: API Reference + href: api/ diff --git a/src/CommandStation.Abstractions/CommandStation.Abstractions.csproj b/src/CommandStation.Abstractions/CommandStation.Abstractions.csproj new file mode 100644 index 0000000..28101b2 --- /dev/null +++ b/src/CommandStation.Abstractions/CommandStation.Abstractions.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + 12 + disable + true + true + $(NoWarn);CS1591;CS1573 + CommandStation.Abstractions + CommandStation.Abstractions + Jakob Eichberger + Protocol- and transport-agnostic abstractions for model-railway command stations (ICommandStation, ITransport, IFrameReader, domain model). + ModelRailway;CommandStation;DCC;Abstractions + https://github.com/jaak0b/Z21 + GPL-3.0-only + + + diff --git a/src/CommandStation.Abstractions/Framing/FrameReceivedEventArgs.cs b/src/CommandStation.Abstractions/Framing/FrameReceivedEventArgs.cs new file mode 100644 index 0000000..743337a --- /dev/null +++ b/src/CommandStation.Abstractions/Framing/FrameReceivedEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace CommandStation.Framing +{ + public class FrameReceivedEventArgs : EventArgs + { + public FrameReceivedEventArgs(byte[] frame) + { + ArgumentNullException.ThrowIfNull(frame); + Frame = frame; + } + + public byte[] Frame { get; } + } +} diff --git a/src/CommandStation.Abstractions/Framing/IFrameReader.cs b/src/CommandStation.Abstractions/Framing/IFrameReader.cs new file mode 100644 index 0000000..0270900 --- /dev/null +++ b/src/CommandStation.Abstractions/Framing/IFrameReader.cs @@ -0,0 +1,20 @@ +using System; + +namespace CommandStation.Framing +{ + /// + /// Reassembles a stream of transport bytes into discrete protocol frames. Implementations buffer + /// partial frames across calls, so they work over both message-oriented (UDP) and stream-oriented + /// (serial, TCP) transports. + /// + public interface IFrameReader + { + event EventHandler? OnFrameReceived; + + /// + /// Appends freshly received bytes and raises for every complete + /// frame that can now be extracted. + /// + void Append(byte[] data); + } +} diff --git a/src/CommandStation.Abstractions/IAccessoryControl.cs b/src/CommandStation.Abstractions/IAccessoryControl.cs new file mode 100644 index 0000000..c50a261 --- /dev/null +++ b/src/CommandStation.Abstractions/IAccessoryControl.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using CommandStation.Model; + +namespace CommandStation +{ + /// + /// Switching turnouts and extended accessory decoders, with their status notifications. + /// + public interface IAccessoryControl + { + Task SetTurnoutAsync(ushort accessoryAddress, AccessoryOutput output, AccessoryState state, bool executeImmediately); + + Task SetExtAccessoryAsync(ushort accessoryAddress, byte payload); + + Task RequestTurnoutInfoAsync(ushort accessoryAddress); + + Task RequestExtAccessoryInfoAsync(ushort accessoryAddress); + + event EventHandler? TurnoutInfoReceived; + + event EventHandler? ExtAccessoryInfoReceived; + } +} diff --git a/src/CommandStation.Abstractions/ICommandStation.cs b/src/CommandStation.Abstractions/ICommandStation.cs new file mode 100644 index 0000000..174e067 --- /dev/null +++ b/src/CommandStation.Abstractions/ICommandStation.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; +using CommandStation.Transport; + +namespace CommandStation +{ + /// + /// A protocol-agnostic connection to a model-railway command station. Feature operations live on + /// the capability interfaces (, , + /// , ); a station implements only + /// the capabilities it supports, so consumers test for them (e.g. station is ILocoControl). + /// + public interface ICommandStation + { + bool IsConnected { get; } + + event EventHandler? ConnectionChanged; + + Task ConnectAsync(); + + Task DisconnectAsync(); + } +} diff --git a/src/CommandStation.Abstractions/IFastClockControl.cs b/src/CommandStation.Abstractions/IFastClockControl.cs new file mode 100644 index 0000000..bd74ff2 --- /dev/null +++ b/src/CommandStation.Abstractions/IFastClockControl.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using CommandStation.Model; + +namespace CommandStation +{ + /// + /// Controlling the accelerated model railway clock (model time). + /// + public interface IFastClockControl + { + Task RequestModelTimeAsync(); + + Task SetModelTimeAsync(ModelTime time); + + Task StartModelTimeAsync(); + + Task StopModelTimeAsync(); + + event EventHandler? ModelTimeChanged; + } +} diff --git a/src/CommandStation.Abstractions/IFeedbackControl.cs b/src/CommandStation.Abstractions/IFeedbackControl.cs new file mode 100644 index 0000000..68d0607 --- /dev/null +++ b/src/CommandStation.Abstractions/IFeedbackControl.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using CommandStation.Model; + +namespace CommandStation +{ + /// + /// Reading occupancy/feedback modules. Group index 0 covers module addresses 1–10, group index 1 + /// covers 11–20. + /// + public interface IFeedbackControl + { + Task RequestFeedbackAsync(byte groupIndex); + + event EventHandler? FeedbackChanged; + } +} diff --git a/src/CommandStation.Abstractions/ILocoControl.cs b/src/CommandStation.Abstractions/ILocoControl.cs new file mode 100644 index 0000000..09d159c --- /dev/null +++ b/src/CommandStation.Abstractions/ILocoControl.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using CommandStation.Model; + +namespace CommandStation +{ + /// + /// Driving a locomotive: speed, direction, functions, and locomotive status notifications. + /// + public interface ILocoControl + { + Task DriveAsync(ushort locoAddress, DccSpeedMode speedMode, DrivingDirection direction, ushort speed); + + Task EmergencyStopAsync(ushort locoAddress); + + Task SetFunctionAsync(ushort locoAddress, ushort functionIndex, FunctionToggleType toggleType); + + Task PurgeAsync(ushort locoAddress); + + Task RequestLocoInfoAsync(ushort locoAddress); + + event EventHandler? LocoInfoReceived; + } +} diff --git a/src/CommandStation.Abstractions/IProgrammingControl.cs b/src/CommandStation.Abstractions/IProgrammingControl.cs new file mode 100644 index 0000000..9b85896 --- /dev/null +++ b/src/CommandStation.Abstractions/IProgrammingControl.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading.Tasks; +using CommandStation.Model; + +namespace CommandStation +{ + /// + /// Reading and writing decoder configuration variables (CVs) in direct mode on the programming track. + /// CV addresses are 0-based (0 = CV1). + /// + public interface IProgrammingControl + { + Task ReadCvAsync(ushort cvAddress); + + Task WriteCvAsync(ushort cvAddress, byte value); + + event EventHandler? CvReadCompleted; + + event EventHandler? CvProgrammingFailed; + } +} diff --git a/src/CommandStation.Abstractions/ISystemInfoProvider.cs b/src/CommandStation.Abstractions/ISystemInfoProvider.cs new file mode 100644 index 0000000..948a9ba --- /dev/null +++ b/src/CommandStation.Abstractions/ISystemInfoProvider.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; +using CommandStation.Model; + +namespace CommandStation +{ + /// + /// Querying command-station system information and receiving status notifications. + /// + public interface ISystemInfoProvider + { + Task RequestSystemStateAsync(); + + Task RequestFirmwareVersionAsync(); + + Task RequestStatusAsync(); + + event EventHandler? SystemStateReceived; + + event EventHandler? FirmwareVersionReceived; + + event EventHandler? StatusChanged; + } +} diff --git a/src/CommandStation.Abstractions/ITrackPowerControl.cs b/src/CommandStation.Abstractions/ITrackPowerControl.cs new file mode 100644 index 0000000..a208745 --- /dev/null +++ b/src/CommandStation.Abstractions/ITrackPowerControl.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; + +namespace CommandStation +{ + /// + /// Track power and global emergency stop. + /// + public interface ITrackPowerControl + { + Task TrackPowerOnAsync(); + + Task TrackPowerOffAsync(); + + /// + /// Stops all locomotives while leaving the track voltage on. + /// + Task EmergencyStopAllAsync(); + + /// + /// Raised when track power is switched on (true) or off (false). + /// + event EventHandler? TrackPowerChanged; + } +} diff --git a/src/Z21.Client/Core/Model/AccessoryOutput.cs b/src/CommandStation.Abstractions/Model/AccessoryOutput.cs similarity index 75% rename from src/Z21.Client/Core/Model/AccessoryOutput.cs rename to src/CommandStation.Abstractions/Model/AccessoryOutput.cs index efd7ced..0574fe6 100644 --- a/src/Z21.Client/Core/Model/AccessoryOutput.cs +++ b/src/CommandStation.Abstractions/Model/AccessoryOutput.cs @@ -1,6 +1,6 @@ using System; -namespace Z21.Core.Model +namespace CommandStation.Model { [Flags] public enum AccessoryOutput @@ -8,4 +8,4 @@ public enum AccessoryOutput Output1 = 0x0, Output2 = 0x1 } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Model/AccessoryState.cs b/src/CommandStation.Abstractions/Model/AccessoryState.cs similarity index 76% rename from src/Z21.Client/Core/Model/AccessoryState.cs rename to src/CommandStation.Abstractions/Model/AccessoryState.cs index e62a38e..bcebfef 100644 --- a/src/Z21.Client/Core/Model/AccessoryState.cs +++ b/src/CommandStation.Abstractions/Model/AccessoryState.cs @@ -1,12 +1,11 @@ using System; -namespace Z21.Core.Model +namespace CommandStation.Model { - [Flags] public enum AccessoryState { Deactivate = 0x0, Activate = 0x8 } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Model/Capabilities.cs b/src/CommandStation.Abstractions/Model/Capabilities.cs similarity index 92% rename from src/Z21.Client/Core/Model/Capabilities.cs rename to src/CommandStation.Abstractions/Model/Capabilities.cs index e3798ed..478b219 100644 --- a/src/Z21.Client/Core/Model/Capabilities.cs +++ b/src/CommandStation.Abstractions/Model/Capabilities.cs @@ -1,7 +1,7 @@ -namespace Z21.Core.Model +namespace CommandStation.Model { /// - /// Represents the supported capabilities of a Z21 device. + /// Represents the supported capabilities of a command station device. /// Each property corresponds to a specific protocol feature. /// public class Capabilities @@ -41,4 +41,4 @@ public class Capabilities /// public bool NeedsUnlockCode { get; init; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Model/CentralState.cs b/src/CommandStation.Abstractions/Model/CentralState.cs similarity index 90% rename from src/Z21.Client/Core/Model/CentralState.cs rename to src/CommandStation.Abstractions/Model/CentralState.cs index a434fc2..1cf6c55 100644 --- a/src/Z21.Client/Core/Model/CentralState.cs +++ b/src/CommandStation.Abstractions/Model/CentralState.cs @@ -1,7 +1,7 @@ -namespace Z21.Core.Model +namespace CommandStation.Model { /// - /// Represents the current operational state of the Z21 central unit. + /// Represents the current operational state of a command station. /// Each property reflects a specific system condition or mode. /// public class CentralState @@ -30,5 +30,4 @@ public class CentralState /// public bool ProgrammingModeActive { get; init; } } - -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Model/CentralStateEx.cs b/src/CommandStation.Abstractions/Model/CentralStateEx.cs similarity index 92% rename from src/Z21.Client/Core/Model/CentralStateEx.cs rename to src/CommandStation.Abstractions/Model/CentralStateEx.cs index 2d6adfc..abe7361 100644 --- a/src/Z21.Client/Core/Model/CentralStateEx.cs +++ b/src/CommandStation.Abstractions/Model/CentralStateEx.cs @@ -1,7 +1,7 @@ -namespace Z21.Core.Model +namespace CommandStation.Model { /// - /// Represents extended diagnostic states reported by the Z21 central unit. + /// Represents extended diagnostic states reported by a command station. /// These flags provide additional system-level status information beyond the basic central state. /// public class CentralStateEx @@ -36,5 +36,4 @@ public class CentralStateEx /// public bool Rcn213 { get; init; } } - -} \ No newline at end of file +} diff --git a/src/CommandStation.Abstractions/Model/CvProgrammingError.cs b/src/CommandStation.Abstractions/Model/CvProgrammingError.cs new file mode 100644 index 0000000..ad9a646 --- /dev/null +++ b/src/CommandStation.Abstractions/Model/CvProgrammingError.cs @@ -0,0 +1,14 @@ +namespace CommandStation.Model +{ + /// + /// Why a CV programming operation failed. + /// + public enum CvProgrammingError + { + /// No decoder acknowledgement was received. + NoAcknowledgement, + + /// Programming failed because of a short circuit on the track. + ShortCircuit + } +} diff --git a/src/CommandStation.Abstractions/Model/CvValue.cs b/src/CommandStation.Abstractions/Model/CvValue.cs new file mode 100644 index 0000000..1c6bb75 --- /dev/null +++ b/src/CommandStation.Abstractions/Model/CvValue.cs @@ -0,0 +1,8 @@ +namespace CommandStation.Model +{ + /// + /// The value of a decoder configuration variable read back from the command station. + /// is 0-based (0 = CV1). + /// + public record CvValue(ushort CvAddress, byte Value); +} diff --git a/src/Z21.Client/Core/Model/DccSpeedMode.cs b/src/CommandStation.Abstractions/Model/DccSpeedMode.cs similarity index 89% rename from src/Z21.Client/Core/Model/DccSpeedMode.cs rename to src/CommandStation.Abstractions/Model/DccSpeedMode.cs index f65309f..d3cbe8e 100644 --- a/src/Z21.Client/Core/Model/DccSpeedMode.cs +++ b/src/CommandStation.Abstractions/Model/DccSpeedMode.cs @@ -1,6 +1,4 @@ -using System; - -namespace Z21.Core.Model +namespace CommandStation.Model { public enum DccSpeedMode { @@ -19,4 +17,4 @@ public enum DccSpeedMode /// Steps128 = 128 } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Model/DecoderMode.cs b/src/CommandStation.Abstractions/Model/DecoderMode.cs similarity index 87% rename from src/Z21.Client/Core/Model/DecoderMode.cs rename to src/CommandStation.Abstractions/Model/DecoderMode.cs index b0a7302..b4e2c12 100644 --- a/src/Z21.Client/Core/Model/DecoderMode.cs +++ b/src/CommandStation.Abstractions/Model/DecoderMode.cs @@ -1,4 +1,4 @@ -namespace Z21.Core.Model +namespace CommandStation.Model { public enum DecoderMode { @@ -6,15 +6,15 @@ public enum DecoderMode /// DCC format /// DCC = 0, - + /// /// MM format /// MM = 1, - + /// /// Unknown format /// Unknown = int.MaxValue } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Model/DrivingDirection.cs b/src/CommandStation.Abstractions/Model/DrivingDirection.cs similarity index 76% rename from src/Z21.Client/Core/Model/DrivingDirection.cs rename to src/CommandStation.Abstractions/Model/DrivingDirection.cs index 7a0828b..86fc670 100644 --- a/src/Z21.Client/Core/Model/DrivingDirection.cs +++ b/src/CommandStation.Abstractions/Model/DrivingDirection.cs @@ -1,6 +1,6 @@ using System; -namespace Z21.Core.Model +namespace CommandStation.Model { [Flags] public enum DrivingDirection @@ -8,4 +8,4 @@ public enum DrivingDirection Backward = 0x0, Forward = 0x80, } -} \ No newline at end of file +} diff --git a/src/CommandStation.Abstractions/Model/ExtAccessoryInfo.cs b/src/CommandStation.Abstractions/Model/ExtAccessoryInfo.cs new file mode 100644 index 0000000..437e362 --- /dev/null +++ b/src/CommandStation.Abstractions/Model/ExtAccessoryInfo.cs @@ -0,0 +1,7 @@ +namespace CommandStation.Model +{ + /// + /// The reported state of an extended accessory decoder. + /// + public record ExtAccessoryInfo(ushort AccessoryAddress, byte EncodedState, bool DataValid); +} diff --git a/src/CommandStation.Abstractions/Model/FeedbackData.cs b/src/CommandStation.Abstractions/Model/FeedbackData.cs new file mode 100644 index 0000000..25b54f2 --- /dev/null +++ b/src/CommandStation.Abstractions/Model/FeedbackData.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace CommandStation.Model +{ + /// + /// A feedback bus status snapshot: the group index and one status byte per feedback module + /// (one bit per input). + /// + public record FeedbackData(byte GroupIndex, IReadOnlyList States); +} diff --git a/src/CommandStation.Abstractions/Model/FirmwareVersion.cs b/src/CommandStation.Abstractions/Model/FirmwareVersion.cs new file mode 100644 index 0000000..3963655 --- /dev/null +++ b/src/CommandStation.Abstractions/Model/FirmwareVersion.cs @@ -0,0 +1,41 @@ +using System; + +namespace CommandStation.Model +{ + public sealed class FirmwareVersion(int major, int minor) : IComparable, IEquatable + { + public int Major { get; } = major; + + public int Minor { get; } = minor; + + public string Firmware { get; } = major + "." + minor; + + override public string ToString() => Firmware; + + public bool Equals(FirmwareVersion? other) => Major == other?.Major && Minor == other.Minor; + + override public bool Equals(object? obj) => obj is FirmwareVersion other && Equals(other); + + override public int GetHashCode() => HashCode.Combine(Major, Minor); + + public int CompareTo(FirmwareVersion? other) + { + if (other is null) + return 1; + int majorCmp = Major.CompareTo(other.Major); + return majorCmp != 0 ? majorCmp : Minor.CompareTo(other.Minor); + } + + public static bool operator <(FirmwareVersion? left, FirmwareVersion? right) => left is null ? right is not null : left.CompareTo(right) < 0; + + public static bool operator >(FirmwareVersion? left, FirmwareVersion? right) => left is not null && left.CompareTo(right) > 0; + + public static bool operator <=(FirmwareVersion? left, FirmwareVersion? right) => left is null || left.CompareTo(right) <= 0; + + public static bool operator >=(FirmwareVersion? left, FirmwareVersion? right) => left is null ? right is null : left.CompareTo(right) >= 0; + + public static bool operator ==(FirmwareVersion? left, FirmwareVersion? right) => Equals(left, right); + + public static bool operator !=(FirmwareVersion? left, FirmwareVersion? right) => !Equals(left, right); + } +} diff --git a/src/Z21.Client/Core/Model/FunctionToggleType.cs b/src/CommandStation.Abstractions/Model/FunctionToggleType.cs similarity index 77% rename from src/Z21.Client/Core/Model/FunctionToggleType.cs rename to src/CommandStation.Abstractions/Model/FunctionToggleType.cs index 32dc7da..b4dfee9 100644 --- a/src/Z21.Client/Core/Model/FunctionToggleType.cs +++ b/src/CommandStation.Abstractions/Model/FunctionToggleType.cs @@ -1,6 +1,6 @@ using System; -namespace Z21.Core.Model +namespace CommandStation.Model { [Flags] public enum FunctionToggleType @@ -9,4 +9,4 @@ public enum FunctionToggleType On = 0x40, Toggle = 0x80 } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Model/LocoFunctionData.cs b/src/CommandStation.Abstractions/Model/LocoFunctionData.cs similarity index 94% rename from src/Z21.Client/Core/Model/LocoFunctionData.cs rename to src/CommandStation.Abstractions/Model/LocoFunctionData.cs index 10ba5eb..822ae40 100644 --- a/src/Z21.Client/Core/Model/LocoFunctionData.cs +++ b/src/CommandStation.Abstractions/Model/LocoFunctionData.cs @@ -1,6 +1,6 @@ using System; -namespace Z21.Core.Model +namespace CommandStation.Model { public class LocoFunctionData(short functionIndex, FunctionToggleType functionToggleType) : IEquatable { @@ -14,4 +14,4 @@ public class LocoFunctionData(short functionIndex, FunctionToggleType functionTo override public int GetHashCode() => HashCode.Combine(FunctionIndex); } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Model/LocoInfoData.cs b/src/CommandStation.Abstractions/Model/LocoInfoData.cs similarity index 94% rename from src/Z21.Client/Core/Model/LocoInfoData.cs rename to src/CommandStation.Abstractions/Model/LocoInfoData.cs index 023b317..13d2ddd 100644 --- a/src/Z21.Client/Core/Model/LocoInfoData.cs +++ b/src/CommandStation.Abstractions/Model/LocoInfoData.cs @@ -1,11 +1,11 @@ using System.Collections.Generic; -namespace Z21.Core.Model +namespace CommandStation.Model { public class LocoInfoData { public required ushort LocoAddress { get; init; } - + public required IReadOnlyCollection LocoFunctionsData { get; init; } public required DccSpeedMode DccSpeedMode { get; init; } @@ -22,4 +22,4 @@ public class LocoInfoData public required bool SmartSearch { get; init; } } -} \ No newline at end of file +} diff --git a/src/CommandStation.Abstractions/Model/ModelTime.cs b/src/CommandStation.Abstractions/Model/ModelTime.cs new file mode 100644 index 0000000..b8bd3f3 --- /dev/null +++ b/src/CommandStation.Abstractions/Model/ModelTime.cs @@ -0,0 +1,8 @@ +namespace CommandStation.Model +{ + /// + /// Accelerated model railway clock time. is 0 (Monday) to 6 (Sunday), + /// is the acceleration factor (0–63; 1 = real time). + /// + public record ModelTime(byte Day, byte Hour, byte Minute, byte Second, byte Rate); +} diff --git a/src/Z21.Client/Core/Model/SystemState.cs b/src/CommandStation.Abstractions/Model/SystemState.cs similarity index 70% rename from src/Z21.Client/Core/Model/SystemState.cs rename to src/CommandStation.Abstractions/Model/SystemState.cs index f2efc09..65637c1 100644 --- a/src/Z21.Client/Core/Model/SystemState.cs +++ b/src/CommandStation.Abstractions/Model/SystemState.cs @@ -1,7 +1,7 @@ -namespace Z21.Core.Model +namespace CommandStation.Model { /// - /// Represents the complete system status of the Z21 central unit, + /// Represents the complete system status of a command station, /// including electrical measurements, temperature, and operational flags. /// public class SystemState @@ -23,40 +23,37 @@ public class SystemState public int FilteredMainCurrent { get; init; } /// - /// The internal temperature of the Z21 unit (in degrees Celsius). + /// The internal temperature of the unit (in degrees Celsius). /// public int Temperature { get; init; } /// - /// The supply voltage (in millivolts) provided to the Z21 unit. + /// The supply voltage (in millivolts) provided to the unit. /// public int SupplyVoltage { get; init; } /// - /// The internal Vcc voltage (in millivolts) used by the Z21 logic circuits. + /// The internal Vcc voltage (in millivolts) used by the logic circuits. /// public int VccVoltage { get; init; } /// - /// The basic operational state of the Z21 central unit, + /// The basic operational state of the central unit, /// including emergency stop, voltage status, and programming mode. /// public required CentralState CentralState { get; init; } /// - /// Extended diagnostic flags from the Z21 central unit, + /// Extended diagnostic flags from the central unit, /// such as temperature warnings, power loss, and short circuit conditions. /// public required CentralStateEx CentralStateEx { get; init; } - // Reserved field for future use or protocol alignment. - // public int? Reserved { get; init; } - /// - /// The set of capabilities supported by the Z21 device, + /// The set of capabilities supported by the device, /// such as RailCom, LocoNet, and accessory command support. /// Will be null on older firmware versions. /// public Capabilities? Capabilities { get; init; } } -} \ No newline at end of file +} diff --git a/src/CommandStation.Abstractions/Model/TurnoutInfo.cs b/src/CommandStation.Abstractions/Model/TurnoutInfo.cs new file mode 100644 index 0000000..0df64e1 --- /dev/null +++ b/src/CommandStation.Abstractions/Model/TurnoutInfo.cs @@ -0,0 +1,8 @@ +namespace CommandStation.Model +{ + /// + /// The reported state of a turnout/accessory. is null when the turnout has + /// not yet been switched or was switched with an invalid combination. + /// + public record TurnoutInfo(ushort AccessoryAddress, AccessoryOutput? Output); +} diff --git a/src/CommandStation.Abstractions/Transport/BytesReceivedEventArgs.cs b/src/CommandStation.Abstractions/Transport/BytesReceivedEventArgs.cs new file mode 100644 index 0000000..4d377b2 --- /dev/null +++ b/src/CommandStation.Abstractions/Transport/BytesReceivedEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace CommandStation.Transport +{ + public class BytesReceivedEventArgs : EventArgs + { + public BytesReceivedEventArgs(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + Data = data; + } + + public byte[] Data { get; } + } +} diff --git a/src/CommandStation.Abstractions/Transport/ConnectionChangedEventArgs.cs b/src/CommandStation.Abstractions/Transport/ConnectionChangedEventArgs.cs new file mode 100644 index 0000000..81a8ad0 --- /dev/null +++ b/src/CommandStation.Abstractions/Transport/ConnectionChangedEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace CommandStation.Transport +{ + public class ConnectionChangedEventArgs : EventArgs + { + public ConnectionChangedEventArgs(bool isConnected) + { + IsConnected = isConnected; + } + + public bool IsConnected { get; } + } +} diff --git a/src/CommandStation.Abstractions/Transport/ITransport.cs b/src/CommandStation.Abstractions/Transport/ITransport.cs new file mode 100644 index 0000000..0277997 --- /dev/null +++ b/src/CommandStation.Abstractions/Transport/ITransport.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; + +namespace CommandStation.Transport +{ + /// + /// A protocol-agnostic byte pipe to a command station. Implementations may use any medium + /// (UDP, TCP, serial, …) and deliver received bytes in arbitrary chunk sizes. + /// + public interface ITransport + { + bool IsConnected { get; } + + event EventHandler? OnBytesReceived; + + event EventHandler? OnConnectionChanged; + + /// + /// Opens the underlying connection and begins receiving. + /// + Task ConnectAsync(); + + /// + /// Closes the underlying connection. + /// + Task DisconnectAsync(); + + /// + /// Sends the given bytes to the command station. + /// + Task SendAsync(ReadOnlyMemory data); + } +} diff --git a/src/CommandStation.Transport.Udp.UnitTest/CommandStation.Transport.Udp.UnitTest.csproj b/src/CommandStation.Transport.Udp.UnitTest/CommandStation.Transport.Udp.UnitTest.csproj new file mode 100644 index 0000000..0a929a3 --- /dev/null +++ b/src/CommandStation.Transport.Udp.UnitTest/CommandStation.Transport.Udp.UnitTest.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + CommandStation.Transport.Udp.UnitTest + + + + + + + + + + + + + + + + + + + diff --git a/src/CommandStation.Transport.Udp.UnitTest/UdpTransportTest.cs b/src/CommandStation.Transport.Udp.UnitTest/UdpTransportTest.cs new file mode 100644 index 0000000..d2f6274 --- /dev/null +++ b/src/CommandStation.Transport.Udp.UnitTest/UdpTransportTest.cs @@ -0,0 +1,284 @@ +using System.Net; +using System.Net.Sockets; +using CommandStation.Transport; + +namespace CommandStation.Transport.Udp.UnitTest +{ + [TestFixture] + public class UdpTransportTest + { + private UdpClient _station = null!; + private IPEndPoint _stationEndPoint = null!; + + [SetUp] + public void SetUp() + { + _station = new UdpClient(new IPEndPoint(IPAddress.Loopback, 0)); + _stationEndPoint = (IPEndPoint)_station.Client.LocalEndPoint!; + } + + [TearDown] + public void TearDown() + { + _station.Dispose(); + } + + [Test] + public async Task ConnectAsync_SetsIsConnected_AndRaisesOnConnectionChanged() + { + await using var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + bool? raised = null; + transport.OnConnectionChanged += (_, args) => raised = args.IsConnected; + + await transport.ConnectAsync(); + + Assert.That(transport.IsConnected, Is.True); + Assert.That(raised, Is.True); + } + + [Test] + public async Task SendAsync_TransmitsBytes_ToRemoteEndpoint() + { + await using var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + await transport.ConnectAsync(); + byte[] payload = [0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00]; + + await transport.SendAsync(payload); + + var received = await _station.ReceiveAsync().WaitAsync(TimeSpan.FromSeconds(2)); + Assert.That(received.Buffer, Is.EqualTo(payload)); + } + + [Test] + public async Task ReceiveLoop_OnSocketError_RaisesDisconnectedExactlyOnce() + { + int deadPort; + using (UdpClient dead = new(new IPEndPoint(IPAddress.Loopback, 0))) + deadPort = ((IPEndPoint)dead.Client.LocalEndPoint!).Port; + + var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = new IPEndPoint(IPAddress.Loopback, deadPort) }); + int disconnectedRaises = 0; + TaskCompletionSource disconnected = new(); + transport.OnConnectionChanged += (_, args) => + { + if (!args.IsConnected) + { + System.Threading.Interlocked.Increment(ref disconnectedRaises); + disconnected.TrySetResult(true); + } + }; + try + { + await transport.ConnectAsync(); + + await transport.SendAsync(new byte[] { 0x01 }); + + Assert.That(await disconnected.Task.WaitAsync(TimeSpan.FromSeconds(5)), Is.True); + Assert.Multiple(() => + { + Assert.That(transport.IsConnected, Is.False); + Assert.That(disconnectedRaises, Is.EqualTo(1), "the lost connection must raise disconnected exactly once"); + }); + } + finally + { + await transport.DisposeAsync(); + } + } + + [Test] + public void Ctor_NullOptions_Throws() + { + Assert.Throws(() => _ = new UdpTransport(null!)); + } + + [Test] + public void SendAsync_WhenNotConnected_ThrowsWithMessage() + { + var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + var exception = Assert.ThrowsAsync(async () => await transport.SendAsync(new byte[] { 0x01 }))!; + Assert.That(exception.Message, Does.Contain("not connected")); + } + + [Test] + public async Task ConnectAsync_WhenAlreadyConnected_IsIdempotent() + { + await using var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + int connectedRaises = 0; + transport.OnConnectionChanged += (_, args) => + { + if (args.IsConnected) + connectedRaises++; + }; + + await transport.ConnectAsync(); + await transport.ConnectAsync(); + + Assert.Multiple(() => + { + Assert.That(transport.IsConnected, Is.True); + Assert.That(connectedRaises, Is.EqualTo(1), "second connect must be a no-op"); + }); + } + + [Test] + public async Task DisconnectAsync_SetsDisconnected_AndRaisesOnce() + { + await using var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + int disconnectedRaises = 0; + transport.OnConnectionChanged += (_, args) => + { + if (!args.IsConnected) + disconnectedRaises++; + }; + await transport.ConnectAsync(); + + await transport.DisconnectAsync(); + await transport.DisconnectAsync(); + + Assert.Multiple(() => + { + Assert.That(transport.IsConnected, Is.False); + Assert.That(disconnectedRaises, Is.EqualTo(1), "second disconnect must be a no-op"); + }); + } + + [Test] + public async Task DisconnectAsync_WhenNeverConnected_DoesNotRaise() + { + await using var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + bool raised = false; + transport.OnConnectionChanged += (_, _) => raised = true; + + await transport.DisconnectAsync(); + + Assert.Multiple(() => + { + Assert.That(raised, Is.False); + Assert.That(transport.IsConnected, Is.False); + }); + } + + [Test] + public async Task DisposeAsync_DisconnectsActiveTransport() + { + var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + bool disconnected = false; + transport.OnConnectionChanged += (_, args) => + { + if (!args.IsConnected) + disconnected = true; + }; + await transport.ConnectAsync(); + + await transport.DisposeAsync(); + + Assert.Multiple(() => + { + Assert.That(transport.IsConnected, Is.False); + Assert.That(disconnected, Is.True); + }); + } + + [Test] + public async Task Dispose_DisconnectsActiveTransport_AndRaisesOnce() + { + var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + int disconnectedRaises = 0; + transport.OnConnectionChanged += (_, args) => + { + if (!args.IsConnected) + disconnectedRaises++; + }; + await transport.ConnectAsync(); + + transport.Dispose(); + + Assert.Multiple(() => + { + Assert.That(transport.IsConnected, Is.False); + Assert.That(disconnectedRaises, Is.EqualTo(1)); + }); + } + + [Test] + public void Dispose_WhenNeverConnected_DoesNotRaise() + { + var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + bool raised = false; + transport.OnConnectionChanged += (_, _) => raised = true; + + transport.Dispose(); + + Assert.Multiple(() => + { + Assert.That(raised, Is.False); + Assert.That(transport.IsConnected, Is.False); + }); + } + + [Test] + public async Task Dispose_CalledTwice_RaisesDisconnectedOnce() + { + var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + int disconnectedRaises = 0; + transport.OnConnectionChanged += (_, args) => + { + if (!args.IsConnected) + disconnectedRaises++; + }; + await transport.ConnectAsync(); + + transport.Dispose(); + transport.Dispose(); + + Assert.That(disconnectedRaises, Is.EqualTo(1), "second dispose must be a no-op"); + } + + [Test] + public async Task IncomingBytes_RaiseOnBytesReceived() + { + await using var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + var tcs = new TaskCompletionSource(); + transport.OnBytesReceived += (_, args) => tcs.TrySetResult(args.Data); + await transport.ConnectAsync(); + + // Provoke the station to learn the transport's source endpoint, then reply to it. + await transport.SendAsync(new byte[] { 0x01 }); + var probe = await _station.ReceiveAsync().WaitAsync(TimeSpan.FromSeconds(2)); + byte[] reply = [0x07, 0x00, 0x40, 0x00, 0x61, 0x01, 0x60]; + await _station.SendAsync(reply, reply.Length, probe.RemoteEndPoint); + + byte[] got = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(2)); + Assert.That(got, Is.EqualTo(reply)); + } + + [Test] + public async Task ReceiveLoop_SubscriberThrows_LoopSurvivesAndKeepsDelivering() + { + await using var transport = new UdpTransport(new UdpTransportOptions { RemoteEndPoint = _stationEndPoint }); + int received = 0; + var secondReceived = new TaskCompletionSource(); + transport.OnBytesReceived += (_, _) => + { + if (System.Threading.Interlocked.Increment(ref received) == 1) + throw new System.InvalidOperationException("boom in subscriber"); + secondReceived.TrySetResult(true); + }; + await transport.ConnectAsync(); + + await transport.SendAsync(new byte[] { 0x01 }); + var probe = await _station.ReceiveAsync().WaitAsync(TimeSpan.FromSeconds(2)); + byte[] reply = [0x07, 0x00, 0x40, 0x00, 0x61, 0x01, 0x60]; + await _station.SendAsync(reply, reply.Length, probe.RemoteEndPoint); + await _station.SendAsync(reply, reply.Length, probe.RemoteEndPoint); + + Assert.Multiple(() => + { + Assert.That(secondReceived.Task.WaitAsync(TimeSpan.FromSeconds(2)).Result, Is.True, + "a throwing OnBytesReceived subscriber must not kill the receive loop"); + Assert.That(transport.IsConnected, Is.True); + }); + } + } +} diff --git a/src/CommandStation.Transport.Udp.UnitTest/stryker-config.json b/src/CommandStation.Transport.Udp.UnitTest/stryker-config.json new file mode 100644 index 0000000..eba8138 --- /dev/null +++ b/src/CommandStation.Transport.Udp.UnitTest/stryker-config.json @@ -0,0 +1,16 @@ +{ + "stryker-config": { + "project": "CommandStation.Transport.Udp.csproj", + "mutation-level": "Complete", + "coverage-analysis": "perTest", + "thresholds": { + "high": 98, + "low": 90, + "break": 60 + }, + "reporters": [ + "html", + "progress" + ] + } +} diff --git a/src/CommandStation.Transport.Udp/CommandStation.Transport.Udp.csproj b/src/CommandStation.Transport.Udp/CommandStation.Transport.Udp.csproj new file mode 100644 index 0000000..876e598 --- /dev/null +++ b/src/CommandStation.Transport.Udp/CommandStation.Transport.Udp.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + 12 + disable + CommandStation.Transport.Udp + true + true + $(NoWarn);CS1591;CS1573 + CommandStation.Transport.Udp + CommandStation.Transport.Udp + Jakob Eichberger + UDP transport (ITransport) for model-railway command stations. + ModelRailway;CommandStation;UDP;Transport + https://github.com/jaak0b/Z21 + GPL-3.0-only + + + + + + + + + + + diff --git a/src/CommandStation.Transport.Udp/UdpTransport.cs b/src/CommandStation.Transport.Udp/UdpTransport.cs new file mode 100644 index 0000000..821d541 --- /dev/null +++ b/src/CommandStation.Transport.Udp/UdpTransport.cs @@ -0,0 +1,169 @@ +using System; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace CommandStation.Transport.Udp +{ + public class UdpTransport : ITransport, IDisposable, IAsyncDisposable + { + private readonly UdpTransportOptions _options; + private readonly ILogger? _logger; + private readonly object _sync = new(); + private UdpClient? _udpClient; + private CancellationTokenSource? _receiveCancellation; + + public UdpTransport(UdpTransportOptions options, ILogger? logger = null) + { + ArgumentNullException.ThrowIfNull(options); + _options = options; + _logger = logger; + } + + public bool IsConnected { get; private set; } + + public event EventHandler? OnBytesReceived; + + public event EventHandler? OnConnectionChanged; + + public Task ConnectAsync() + { + UdpClient udpClient; + CancellationToken token; + lock (_sync) + { + if (IsConnected) + return Task.CompletedTask; + + udpClient = new UdpClient(); + if (OperatingSystem.IsWindows()) + udpClient.AllowNatTraversal(_options.AllowNatTraversal); + udpClient.Connect(_options.RemoteEndPoint); + + _udpClient = udpClient; + _receiveCancellation = new CancellationTokenSource(); + token = _receiveCancellation.Token; + IsConnected = true; + } + + _ = ReceiveLoopAsync(udpClient, token); + + OnConnectionChanged?.Invoke(this, new ConnectionChangedEventArgs(true)); + return Task.CompletedTask; + } + + public Task DisconnectAsync() + { + Disconnect(); + return Task.CompletedTask; + } + + private void Disconnect() + { + CancellationTokenSource? cancellation; + UdpClient? udpClient; + lock (_sync) + { + if (!IsConnected) + return; + + IsConnected = false; + cancellation = _receiveCancellation; + _receiveCancellation = null; + udpClient = _udpClient; + _udpClient = null; + } + + cancellation?.Cancel(); + cancellation?.Dispose(); + udpClient?.Dispose(); + + OnConnectionChanged?.Invoke(this, new ConnectionChangedEventArgs(false)); + } + + public async Task SendAsync(ReadOnlyMemory data) + { + UdpClient udpClient; + lock (_sync) + { + if (_udpClient is null || !IsConnected) + throw new InvalidOperationException("UdpTransport is not connected."); + udpClient = _udpClient; + } + + await udpClient.SendAsync(data); + } + + public void Dispose() + { + Disconnect(); + GC.SuppressFinalize(this); + } + + public ValueTask DisposeAsync() + { + Disconnect(); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + + private async Task ReceiveLoopAsync(UdpClient udpClient, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + UdpReceiveResult result; + try + { + result = await udpClient.ReceiveAsync(cancellationToken); + } + catch (OperationCanceledException) + { + return; + } + catch (ObjectDisposedException) + { + return; + } + catch (SocketException socketException) + { + _logger?.LogError(socketException, "UdpTransport receive loop terminated due to a socket error."); + SignalConnectionLost(udpClient); + return; + } + + try + { + OnBytesReceived?.Invoke(this, new BytesReceivedEventArgs(result.Buffer)); + } + catch (System.Exception exception) + { + _logger?.LogError(exception, "UdpTransport receive loop swallowed an exception thrown by an OnBytesReceived subscriber."); + } + } + } + + private void SignalConnectionLost(UdpClient faultedClient) + { + CancellationTokenSource? cancellation = null; + bool raise = false; + lock (_sync) + { + if (IsConnected && ReferenceEquals(_udpClient, faultedClient)) + { + IsConnected = false; + cancellation = _receiveCancellation; + _receiveCancellation = null; + _udpClient = null; + raise = true; + } + } + + cancellation?.Dispose(); + faultedClient.Dispose(); + + if (raise) + OnConnectionChanged?.Invoke(this, new ConnectionChangedEventArgs(false)); + } + } +} diff --git a/src/CommandStation.Transport.Udp/UdpTransportOptions.cs b/src/CommandStation.Transport.Udp/UdpTransportOptions.cs new file mode 100644 index 0000000..d460fd1 --- /dev/null +++ b/src/CommandStation.Transport.Udp/UdpTransportOptions.cs @@ -0,0 +1,21 @@ +using System.Net; + +namespace CommandStation.Transport.Udp +{ + public class UdpTransportOptions + { + /// + /// The remote endpoint of the command station. + /// + public IPEndPoint RemoteEndPoint { get; set; } = new(IPAddress.Parse(DefaultAddress), DefaultPort); + + /// + /// Enables or disables NAT traversal on the underlying socket (Windows only). + /// + public bool AllowNatTraversal { get; set; } = true; + + public const string DefaultAddress = "192.168.0.111"; + + public const int DefaultPort = 21105; + } +} diff --git a/src/Z21.Autofac.UnitTests/Z21AutofacExtensions.cs b/src/Z21.Autofac.UnitTests/Z21AutofacExtensions.cs index 03c9b57..d2f20f8 100644 --- a/src/Z21.Autofac.UnitTests/Z21AutofacExtensions.cs +++ b/src/Z21.Autofac.UnitTests/Z21AutofacExtensions.cs @@ -1,10 +1,10 @@ -using System.Net; using Autofac; +using CommandStation; +using CommandStation.Transport; using Z21.Core; -using Z21.Core.Model; +using Z21.Core.ResponseHandler.Settings; using Z21.Core.ResponseHandler.SystemState; using Z21.Core.ResponseParser; -using Z21.Transport; namespace Z21.Autofac.UnitTests { @@ -25,17 +25,26 @@ public void AddZ21ResponseHandler_RegistersTypesCorrectly() var handler = container.Resolve(); Assert.That(handler, Is.InstanceOf()); - + var handlerType = container.Resolve(); Assert.That(handlerType, Is.InstanceOf()); Assert.That(handlerType, Is.EqualTo(handler)); } + [Test] + public void AddZ21ResponseHandler_DiscoversAccessoryModeHandler() + { + using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21()); + + var handler = container.Resolve(); + Assert.That(handler, Is.InstanceOf()); + } + [Test] public void AddZ21ResponseParser_Registers_All_Parser_Types() { using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21()); - + var baseInterface = typeof(IZ21ResponseParser); var parserTypes = baseInterface.Assembly .GetTypes() @@ -60,40 +69,38 @@ public void AddZ21ResponseParser_Registers_All_Parser_Types() } [Test] - public void AddZ21Transport_Registers_Transport_As_Singleton() + public void AddZ21_Registers_Transport_As_Singleton() { using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21()); - - var t1 = container.Resolve(); - var t2 = container.Resolve(); + + var t1 = container.Resolve(); + var t2 = container.Resolve(); Assert.That(t1, Is.Not.Null); - Assert.That(t2, Is.Not.Null); Assert.That(t2, Is.SameAs(t1), "Transport should be singleton"); } [Test] - public void AddZ21Client_Registers_Client_As_Singleton() + public void AddZ21_Registers_CommandStation_As_Singleton() { using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21()); - var c1 = container.Resolve(); - var c2 = container.Resolve(); + var s1 = container.Resolve(); + var s2 = container.Resolve(); - Assert.NotNull(c1); - Assert.NotNull(c2); - Assert.That(c2, Is.SameAs(c1), "Client should be singleton"); + Assert.That(s1, Is.Not.Null); + Assert.That(s2, Is.SameAs(s1), "ICommandStation and IZ21CommandStation should resolve to the same singleton"); } [Test] - public void ConfigureZ21Client_Registers_Configuration_Instance() + public void AddZ21_Registers_Options_Instance() { - using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21(cfg => cfg.ResponseTime = TimeSpan.FromSeconds(5))); + using var container = BuildContainer(containerBuilder => containerBuilder.AddZ21(optionsConfiguration: options => options.KeepAliveInterval = TimeSpan.FromSeconds(5))); - var config = container.Resolve(); + var options = container.Resolve(); - Assert.NotNull(config); - Assert.That(config.ResponseTime, Is.EqualTo(TimeSpan.FromSeconds(5))); + Assert.That(options, Is.Not.Null); + Assert.That(options.KeepAliveInterval, Is.EqualTo(TimeSpan.FromSeconds(5))); } } -} \ No newline at end of file +} diff --git a/src/Z21.Autofac/Z21.Autofac.csproj b/src/Z21.Autofac/Z21.Autofac.csproj index 0dde02b..e8ab2d0 100644 --- a/src/Z21.Autofac/Z21.Autofac.csproj +++ b/src/Z21.Autofac/Z21.Autofac.csproj @@ -3,11 +3,12 @@ net8.0;net8.0-windows true + true + $(NoWarn);CS1591;CS1573 enable enable Jakob Eichberger - https://github.com/Jakob-Eichberger/z21Client - 6.0.0 + https://github.com/jaak0b/Z21 LICENSE true @@ -18,6 +19,7 @@ + diff --git a/src/Z21.Autofac/Z21AutofacExtensions.cs b/src/Z21.Autofac/Z21AutofacExtensions.cs index 0ccf002..7d0a9d9 100644 --- a/src/Z21.Autofac/Z21AutofacExtensions.cs +++ b/src/Z21.Autofac/Z21AutofacExtensions.cs @@ -1,22 +1,37 @@ -using Autofac; +using Autofac; +using CommandStation; +using CommandStation.Framing; +using CommandStation.Transport; +using CommandStation.Transport.Udp; using Z21.Core; -using Z21.Core.Model; +using Z21.Core.Codecs; +using Z21.Core.Command; +using Z21.Core.Framing; +using Z21.Core.Reflection; using Z21.Core.ResponseHandler; using Z21.Core.ResponseParser; -using Z21.Transport; namespace Z21.Autofac { public static class Z21AutofacExtensions { - public static ContainerBuilder AddZ21(this ContainerBuilder builder, Action? configurationAction = null) + public static ContainerBuilder AddZ21(this ContainerBuilder builder, Action? transportConfiguration = null, Action? optionsConfiguration = null) { - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().SingleInstance(); - builder.RegisterType().AsSelf().SingleInstance().AutoActivate(); - - builder.ConfigureZ21Client(configurationAction); + UdpTransportOptions transportOptions = new(); + transportConfiguration?.Invoke(transportOptions); + builder.RegisterInstance(transportOptions).AsSelf().SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().As().SingleInstance(); + builder.RegisterType().AsSelf().SingleInstance(); + + builder.ConfigureZ21Options(optionsConfiguration); builder.AddZ21ResponseParser(); builder.AddZ21ResponseHandler(); return builder; @@ -25,64 +40,27 @@ public static ContainerBuilder AddZ21(this ContainerBuilder builder, Action /// Discovers all Z21 response handlers and registers them in the container. /// - private static ContainerBuilder AddZ21ResponseHandler(this ContainerBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - Type baseInterface = typeof(IZ21ResponseHandler); - - IEnumerable handlerTypes = - baseInterface.Assembly - .GetTypes() - .Where(t => t is { IsClass: true, IsAbstract: false } && baseInterface.IsAssignableFrom(t)); - - foreach (Type handlerType in handlerTypes) - { - builder.RegisterType(handlerType) - .AsSelf() - .SingleInstance(); - - List interfacesToRegister = - handlerType.GetInterfaces() - .Where(baseInterface.IsAssignableFrom) - .ToList(); + private static ContainerBuilder AddZ21ResponseHandler(this ContainerBuilder builder) => + builder.AddDiscovered(typeof(IZ21ResponseHandler), includeBaseInterface: true); - foreach (Type serviceType in interfacesToRegister) - { - builder.Register(ctx => ctx.Resolve(handlerType)) - .As(serviceType) - .SingleInstance(); - } - } + private static ContainerBuilder AddZ21ResponseParser(this ContainerBuilder builder) => + builder.AddDiscovered(typeof(IZ21ResponseParser), includeBaseInterface: false); - return builder; - } - - private static ContainerBuilder AddZ21ResponseParser(this ContainerBuilder builder) + private static ContainerBuilder AddDiscovered(this ContainerBuilder builder, Type baseInterface, bool includeBaseInterface) { ArgumentNullException.ThrowIfNull(builder); - Type baseInterface = typeof(IZ21ResponseParser); + Z21ServiceDiscovery discovery = new(); - IEnumerable parserTypes = - baseInterface.Assembly - .GetTypes() - .Where(t => t is { IsClass: true, IsAbstract: false } && baseInterface.IsAssignableFrom(t)); - - foreach (Type parserType in parserTypes) + foreach (Type implementationType in discovery.GetImplementations(baseInterface)) { - builder.RegisterType(parserType) + builder.RegisterType(implementationType) .AsSelf() .SingleInstance(); - List interfacesToRegister = - parserType.GetInterfaces() - .Where(i => baseInterface.IsAssignableFrom(i) && i != baseInterface) - .ToList(); - - foreach (Type serviceType in interfacesToRegister) + foreach (Type serviceType in discovery.GetServiceInterfaces(implementationType, baseInterface, includeBaseInterface)) { - builder.Register(ctx => ctx.Resolve(parserType)) + builder.Register(ctx => ctx.Resolve(implementationType)) .As(serviceType) .SingleInstance(); } @@ -91,18 +69,18 @@ private static ContainerBuilder AddZ21ResponseParser(this ContainerBuilder build return builder; } - private static ContainerBuilder ConfigureZ21Client(this ContainerBuilder builder, Action? configurationAction = null) + private static ContainerBuilder ConfigureZ21Options(this ContainerBuilder builder, Action? optionsConfiguration = null) { ArgumentNullException.ThrowIfNull(builder); - var config = new Z21Configuration(); - configurationAction?.Invoke(config); + Z21Options options = new(); + optionsConfiguration?.Invoke(options); - builder.RegisterInstance(config) - .As() + builder.RegisterInstance(options) + .As() .SingleInstance(); return builder; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Codecs/AddressCodecTest.cs b/src/Z21.Client.UnitTest/Core/Codecs/AddressCodecTest.cs new file mode 100644 index 0000000..5457435 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Codecs/AddressCodecTest.cs @@ -0,0 +1,208 @@ +using Z21.Core.Codecs; + +namespace Z21.UnitTest.Core.Codecs +{ + public class AddressCodecTest + { + private AddressCodec _codec = null!; + + [SetUp] + public void SetUp() => _codec = new AddressCodec(); + + [Test] + [TestCase((ushort)24, 0x00, 0x18)] + [TestCase((ushort)0, 0x00, 0x00)] + [TestCase((ushort)255, 0x00, 0xFF)] + [TestCase((ushort)256, 0x01, 0x00)] + [TestCase((ushort)300, 0x01, 0x2C)] + public void SplitAddressBigEndian_ReturnsMsbThenLsb(ushort input, byte expectedMsb, byte expectedLsb) + { + (byte msb, byte lsb) = _codec.SplitAddressBigEndian(input); + + Assert.Multiple(() => + { + Assert.That(msb, Is.EqualTo(expectedMsb)); + Assert.That(lsb, Is.EqualTo(expectedLsb)); + }); + } + + [Test] + [TestCase((ushort)0, 0x00, 0x00)] + [TestCase((ushort)1, 0x01, 0x00)] + [TestCase((ushort)127, 0x7F, 0x00)] + [TestCase((ushort)128, 0x80, 0x00)] + [TestCase((ushort)255, 0xFF, 0x00)] + [TestCase((ushort)256, 0x00, 0x01)] + [TestCase((ushort)512, 0x00, 0x02)] + [TestCase((ushort)1023, 0xFF, 0x03)] + [TestCase((ushort)1234, 0xD2, 0x04)] + [TestCase((ushort)16383, 0xFF, 0x3F)] + public void SplitLocoAddress_ReturnsCorrectLSBAndMSB(ushort input, byte expectedLsb, byte expectedMsb) + { + (byte lsb, byte msb) = _codec.SplitLocoAddress(input); + + if (input >= 128) + expectedMsb |= 0xC0; + + Assert.Multiple(() => + { + Assert.That(lsb, Is.EqualTo(expectedLsb), "LSB is incorrect"); + Assert.That(msb, Is.EqualTo(expectedMsb), "MSB is incorrect"); + }); + } + + [Test] + public void SplitAccessoryAddress_ReturnsCorrectLSBAndMSB() + { + (byte lsb, byte msb) = _codec.SplitAccessoryAddress(48); + Assert.Multiple(() => + { + Assert.That((msb << 8) + lsb, Is.EqualTo(47)); + Assert.That(msb, Is.EqualTo(0x00)); + Assert.That(lsb, Is.EqualTo(0x2F)); + }); + } + + [Test] + public void SplitAccessoryAddress_LargeAddress_FillsMsb() + { + (byte lsb, byte msb) = _codec.SplitAccessoryAddress(300); + Assert.Multiple(() => + { + Assert.That(msb, Is.EqualTo(0x01), "MSB must carry the high byte of (address - 1)"); + Assert.That(lsb, Is.EqualTo(0x2B)); + }); + } + + [Test] + public void SplitAccessoryAddress_AddressIs1_DoesNotThrow() + { + Assert.DoesNotThrow(() => _codec.SplitAccessoryAddress(1)); + } + + [Test] + public void SplitAccessoryAddress_AddressIs0_ThrowsWithMessage() + { + ArgumentOutOfRangeException exception = Assert.Throws(() => _codec.SplitAccessoryAddress(0))!; + Assert.That(exception.Message, Does.Contain("Smallest address is 1")); + } + + [Test] + public void SplitExtAccessoryAddress_AddressIs0_ThrowsWithMessage() + { + ArgumentOutOfRangeException exception = Assert.Throws(() => _codec.SplitExtAccessoryAddress(0))!; + Assert.That(exception.Message, Does.Contain("Smallest address is 1")); + } + + [Test] + public void CombineAccessoryAddress_ReturnsCorrectAddress() + { + const byte msb = 0x00; + const byte lsb = 0x2f; + ushort address = _codec.CombineAccessoryAddress(lsb, msb); + Assert.That(address, Is.EqualTo(48)); + } + + [Test] + public void CombineAccessoryAddress_WithMsb_ShiftsHighByte() + { + Assert.That(_codec.CombineAccessoryAddress(0x00, 0x01), Is.EqualTo(257)); + } + + [Test] + public void CombineExtAccessoryAddress_WithMsb_ShiftsHighByte() + { + Assert.That(_codec.CombineExtAccessoryAddress(0x00, 0x01), Is.EqualTo(253)); + } + + [Test] + [TestCase((ushort)1, 0x04, 0x00)] + [TestCase((ushort)2, 0x05, 0x00)] + [TestCase((ushort)253, 0x00, 0x01)] + public void SplitExtAccessoryAddress_MapsUserAddressToRawAddress(ushort userAddress, byte expectedLsb, byte expectedMsb) + { + (byte lsb, byte msb) = _codec.SplitExtAccessoryAddress(userAddress); + Assert.Multiple(() => + { + Assert.That(lsb, Is.EqualTo(expectedLsb), "LSB is incorrect"); + Assert.That(msb, Is.EqualTo(expectedMsb), "MSB is incorrect"); + }); + } + + [Test] + public void SplitExtAccessoryAddress_AddressIs0_ThrowsArgumentOutOfRangeException() + { + Assert.Throws(() => _codec.SplitExtAccessoryAddress(0)); + } + + [Test] + public void CombineExtAccessoryAddress_IsInverseOfSplit() + { + (byte lsb, byte msb) = _codec.SplitExtAccessoryAddress(1); + Assert.Multiple(() => + { + Assert.That((msb << 8) + lsb, Is.EqualTo(4), "RawAddress for user address 1 must be 4"); + Assert.That(_codec.CombineExtAccessoryAddress(lsb, msb), Is.EqualTo((ushort)1)); + }); + } + + [Test] + [TestCase((ushort)0, 0x00, 0x00)] + [TestCase((ushort)1, 0x00, 0x01)] + [TestCase((ushort)255, 0x00, 0xFF)] + [TestCase((ushort)256, 0x01, 0x00)] + [TestCase((ushort)1021, 0x03, 0xFD)] + public void SplitCvAddress_ReturnsCorrectMsbAndLsb(ushort cvAddress, byte expectedMsb, byte expectedLsb) + { + (byte msb, byte lsb) = _codec.SplitCvAddress(cvAddress); + Assert.Multiple(() => + { + Assert.That(msb, Is.EqualTo(expectedMsb), "MSB is incorrect"); + Assert.That(lsb, Is.EqualTo(expectedLsb), "LSB is incorrect"); + }); + } + + [Test] + [TestCase((ushort)0)] + [TestCase((ushort)255)] + [TestCase((ushort)1021)] + public void CombineCvAddress_IsInverseOfSplit(ushort cvAddress) + { + (byte msb, byte lsb) = _codec.SplitCvAddress(cvAddress); + Assert.That(_codec.CombineCvAddress(msb, lsb), Is.EqualTo(cvAddress)); + } + + [Test] + public void EncodeAccessoryPomAddress_WholeDecoder_SetsCddNibbleToZero() + { + (byte db1, byte db2) = _codec.EncodeAccessoryPomAddress(1, wholeDecoder: true, output: 0); + Assert.Multiple(() => + { + Assert.That(db1, Is.EqualTo(0x00), "DB1 (aaaaa) is incorrect"); + Assert.That(db2, Is.EqualTo(0x10), "DB2 (AAAACDDD) is incorrect"); + }); + } + + [Test] + public void EncodeAccessoryPomAddress_SingleOutput_SetsCbitAndOutput() + { + (byte db1, byte db2) = _codec.EncodeAccessoryPomAddress(1, wholeDecoder: false, output: 3); + Assert.Multiple(() => + { + Assert.That(db1, Is.EqualTo(0x00), "DB1 (aaaaa) is incorrect"); + Assert.That(db2, Is.EqualTo(0x1B), "DB2 (AAAACDDD) is incorrect"); + }); + } + + [Test] + public void EncodeAccessoryPomAddress_LargeAddress_FillsHighByte() + { + (byte db1, byte db2) = _codec.EncodeAccessoryPomAddress(0x1FF, wholeDecoder: true, output: 0); + Assert.Multiple(() => + { + Assert.That(db1, Is.EqualTo(0x1F), "DB1 (aaaaa) is incorrect"); + Assert.That(db2, Is.EqualTo(0xF0), "DB2 (AAAACDDD) is incorrect"); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Helper/LocoSpeedHelperTest.cs b/src/Z21.Client.UnitTest/Core/Codecs/LocoSpeedCodecTest.cs similarity index 70% rename from src/Z21.Client.UnitTest/Core/Helper/LocoSpeedHelperTest.cs rename to src/Z21.Client.UnitTest/Core/Codecs/LocoSpeedCodecTest.cs index 24d5f0d..ba13508 100644 --- a/src/Z21.Client.UnitTest/Core/Helper/LocoSpeedHelperTest.cs +++ b/src/Z21.Client.UnitTest/Core/Codecs/LocoSpeedCodecTest.cs @@ -1,10 +1,14 @@ -using Z21.Core.Helper; +using Z21.Core.Codecs; using Z21.Core.Model; -namespace Z21.UnitTest.Core.Helper +namespace Z21.UnitTest.Core.Codecs { - public class LocoSpeedHelperTest + public class LocoSpeedCodecTest { + private LocoSpeedCodec _codec = null!; + + [SetUp] + public void SetUp() => _codec = new LocoSpeedCodec(); [Test] [TestCase((ushort)0, (ushort)0)] @@ -13,7 +17,7 @@ public class LocoSpeedHelperTest [TestCase((ushort)13, (ushort)14)] public void CalculateDccSpeed_Dcc14(ushort speedStep, ushort dccSpeed) { - Assert.That(LocoSpeedHelper.CalculateDccSpeed(DccSpeedMode.Steps14, speedStep), Is.EqualTo(dccSpeed)); + Assert.That(_codec.CalculateDccSpeed(DccSpeedMode.Steps14, speedStep), Is.EqualTo(dccSpeed)); } [Test] @@ -23,7 +27,7 @@ public void CalculateDccSpeed_Dcc14(ushort speedStep, ushort dccSpeed) [TestCase((ushort)127, (ushort)128)] public void CalculateDccSpeed_Dcc128(ushort speedStep, ushort dccSpeed) { - Assert.That(LocoSpeedHelper.CalculateDccSpeed(DccSpeedMode.Steps128, speedStep), Is.EqualTo(dccSpeed)); + Assert.That(_codec.CalculateDccSpeed(DccSpeedMode.Steps128, speedStep), Is.EqualTo(dccSpeed)); } [Test] @@ -58,17 +62,16 @@ public void CalculateDccSpeed_Dcc128(ushort speedStep, ushort dccSpeed) [TestCase((ushort)28, (ushort)31)] public void CalculateDccSpeed_Dcc28(ushort speedStep, ushort dccSpeed) { - Assert.That(LocoSpeedHelper.CalculateDccSpeed(DccSpeedMode.Steps28, speedStep), Is.EqualTo(dccSpeed)); + Assert.That(_codec.CalculateDccSpeed(DccSpeedMode.Steps28, speedStep), Is.EqualTo(dccSpeed)); } - [TestCase((ushort)0, (ushort)0)] [TestCase((ushort)1, (ushort)0)] [TestCase((ushort)2, (ushort)1)] [TestCase((ushort)13, (ushort)12)] public void CalculateSpeedStep_Dcc14(ushort dccSpeed, ushort speedStep) { - Assert.That(LocoSpeedHelper.CalculateSpeedStep(DccSpeedMode.Steps14, dccSpeed), Is.EqualTo(speedStep)); + Assert.That(_codec.CalculateSpeedStep(DccSpeedMode.Steps14, dccSpeed), Is.EqualTo(speedStep)); } [TestCase((ushort)0, (ushort)0)] @@ -77,10 +80,9 @@ public void CalculateSpeedStep_Dcc14(ushort dccSpeed, ushort speedStep) [TestCase((ushort)129, (ushort)128)] public void CalculateSpeedStep_Dcc128(ushort dccSpeed, ushort speedStep) { - Assert.That(LocoSpeedHelper.CalculateSpeedStep(DccSpeedMode.Steps128, dccSpeed), Is.EqualTo(speedStep)); + Assert.That(_codec.CalculateSpeedStep(DccSpeedMode.Steps128, dccSpeed), Is.EqualTo(speedStep)); } - [Test] [TestCase(0, 0)] [TestCase(16, 0)] @@ -116,7 +118,17 @@ public void CalculateSpeedStep_Dcc128(ushort dccSpeed, ushort speedStep) [TestCase(31, 28)] public void CalculateSpeedStep_Dcc28(short dccSpeed, short speedStep) { - Assert.That(LocoSpeedHelper.CalculateSpeedStep(DccSpeedMode.Steps28, (ushort)dccSpeed), Is.EqualTo((ushort)speedStep), $"Dcc Speed: {dccSpeed}. Expected speed step: {speedStep}"); + Assert.That(_codec.CalculateSpeedStep(DccSpeedMode.Steps28, (ushort)dccSpeed), Is.EqualTo((ushort)speedStep), $"Dcc Speed: {dccSpeed}. Expected speed step: {speedStep}"); + } + + [Test] + [TestCase((ushort)32)] + [TestCase((ushort)37)] + [TestCase((ushort)127)] + public void CalculateSpeedStep_Dcc28_OutOfRangeValue_ReturnsZeroAndDoesNotThrow(ushort dccSpeed) + { + Assert.That(() => _codec.CalculateSpeedStep(DccSpeedMode.Steps28, dccSpeed), Throws.Nothing); + Assert.That(_codec.CalculateSpeedStep(DccSpeedMode.Steps28, dccSpeed), Is.EqualTo((ushort)0)); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Booster/BoosterCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Booster/BoosterCommandTest.cs new file mode 100644 index 0000000..75a66fa --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Booster/BoosterCommandTest.cs @@ -0,0 +1,58 @@ +using System; +using Z21.Core.Command.Booster; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Booster +{ + public class BoosterCommandTest : CommandTestFixture + { + [Test] + public void GetDescription_BuildsRequest() + { + GetBoosterDescriptionCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0xB8, 0x00 })); + } + + [Test] + public void SetDescription_PadsNameToThirtyTwoBytes() + { + SetBoosterDescriptionCommand command = Factory.Create("AB"); + byte[] expected = new byte[36]; + expected[0] = 0x24; + expected[2] = 0xB9; + expected[4] = 0x41; + expected[5] = 0x42; + Assert.That(command.Data, Is.EqualTo(expected)); + } + + [Test] + [TestCase("a\"b")] + [TestCase("a\\b")] + public void SetDescription_RejectsForbiddenCharacters(string name) + { + ArgumentException exception = Assert.Throws(() => Factory.Create(name))!; + Assert.That(exception.Message, Does.Contain("not allowed")); + } + + [Test] + public void SetDescription_LongName_IsTruncatedToThirtyTwoBytes() + { + SetBoosterDescriptionCommand command = Factory.Create(new string('X', 40)); + Assert.That(command.Data, Has.Length.EqualTo(36), "frame stays 4 header + 32 name bytes even for an over-long name"); + } + + [Test] + public void SetPower_WritesPortAndState() + { + SetBoosterPowerCommand command = Factory.Create((byte)0x03, (byte)0x01); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x06, 0x00, 0xB2, 0x00, 0x03, 0x01 })); + } + + [Test] + public void GetSystemState_BuildsRequest() + { + GetBoosterSystemStateCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0xBB, 0x00 })); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Can/CanCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Can/CanCommandTest.cs new file mode 100644 index 0000000..ad9c683 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Can/CanCommandTest.cs @@ -0,0 +1,49 @@ +using System; +using Z21.Core.Command.Can; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Can +{ + public class CanCommandTest : CommandTestFixture + { + [Test] + public void GetCanDetector_MatchesSpecExample() + { + GetCanDetectorCommand command = Factory.Create((ushort)0xD000); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0xC4, 0x00, 0x00, 0x00, 0xD0 })); + } + + [Test] + public void GetCanDeviceDescription_WritesLittleEndianNetworkId() + { + GetCanDeviceDescriptionCommand command = Factory.Create((ushort)0xC101); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x06, 0x00, 0xC8, 0x00, 0x01, 0xC1 })); + } + + [Test] + public void SetCanDeviceDescription_PadsNameToSixteenBytes() + { + SetCanDeviceDescriptionCommand command = Factory.Create((ushort)0xC101, "AB"); + Assert.That(command.Data, Is.EqualTo(new byte[] + { + 0x16, 0x00, 0xC9, 0x00, 0x01, 0xC1, + 0x41, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + })); + } + + [Test] + [TestCase("a\"b")] + [TestCase("a\\b")] + public void SetCanDeviceDescription_RejectsForbiddenCharacters(string name) + { + Assert.Throws(() => Factory.Create((ushort)0xC101, name)); + } + + [Test] + public void SetCanBoosterTrackPower_WritesNetworkIdAndPower() + { + SetCanBoosterTrackPowerCommand command = Factory.Create((ushort)0xC101, (byte)0xFF); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0xCB, 0x00, 0x01, 0xC1, 0xFF })); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/CommandTestFixture.cs b/src/Z21.Client.UnitTest/Core/Command/CommandTestFixture.cs new file mode 100644 index 0000000..5623eae --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/CommandTestFixture.cs @@ -0,0 +1,12 @@ +using Z21.Core.Codecs; +using Z21.Core.Command; +using Z21.Core.Framing; + +namespace Z21.UnitTest.Core.Command +{ + public abstract class CommandTestFixture + { + protected IZ21CommandFactory Factory { get; } = + new Z21CommandFactory(new Z21FrameBuilder(), new AddressCodec(), new LocoSpeedCodec()); + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Decoder/DecoderCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Decoder/DecoderCommandTest.cs new file mode 100644 index 0000000..a353a64 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Decoder/DecoderCommandTest.cs @@ -0,0 +1,43 @@ +using System; +using Z21.Core.Command.Decoder; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Decoder +{ + public class DecoderCommandTest : CommandTestFixture + { + [Test] + public void GetDescription_BuildsRequest() + { + GetDecoderDescriptionCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0xD8, 0x00 })); + } + + [Test] + public void SetDescription_PadsNameToThirtyTwoBytes() + { + SetDecoderDescriptionCommand command = Factory.Create("AB"); + byte[] expected = new byte[36]; + expected[0] = 0x24; + expected[2] = 0xD9; + expected[4] = 0x41; + expected[5] = 0x42; + Assert.That(command.Data, Is.EqualTo(expected)); + } + + [Test] + [TestCase("a\"b")] + [TestCase("a\\b")] + public void SetDescription_RejectsForbiddenCharacters(string name) + { + Assert.Throws(() => Factory.Create(name)); + } + + [Test] + public void GetSystemState_BuildsRequest() + { + GetDecoderSystemStateCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0xDB, 0x00 })); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Driving/GetLocoInfoCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Driving/GetLocoInfoCommandTest.cs index 7dfad7c..5d28d3b 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Driving/GetLocoInfoCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Driving/GetLocoInfoCommandTest.cs @@ -1,13 +1,14 @@ using Z21.Core.Command.Driving; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Driving { - public class GetLocoInfoCommandTest + public class GetLocoInfoCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetLocoInfoCommand command = new(3); + GetLocoInfoCommand command = Factory.Create((ushort)3); Assert.That(command.Data, Is.EqualTo(new byte[] { @@ -21,4 +22,4 @@ public void Ctor_SetsCorrectDataBits() })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Driving/PurgeLocoCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Driving/PurgeLocoCommandTest.cs index 7d46fb0..3170d9a 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Driving/PurgeLocoCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Driving/PurgeLocoCommandTest.cs @@ -1,18 +1,19 @@ using Z21.Core.Command.Driving; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Driving { - public class PurgeLocoCommandTest + public class PurgeLocoCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - PurgeLocoCommand getSerialNumberCommand = new(3); - Assert.That(getSerialNumberCommand.Data, + PurgeLocoCommand command = Factory.Create((ushort)3); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x09, 0x00, 0x40, 0x00, 0xE3, 0x44, 0x00, 0x03, 0xA4, })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoBinaryStateCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoBinaryStateCommandTest.cs new file mode 100644 index 0000000..f9d10d8 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoBinaryStateCommandTest.cs @@ -0,0 +1,28 @@ +using System; +using Z21.Core.Command.Driving; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Driving +{ + public class SetLocoBinaryStateCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)3, (ushort)29, true, new byte[] { 0x0B, 0x00, 0x40, 0x00, 0xE5, 0x5F, 0x00, 0x03, 0x9D, 0x00, 0x24 })] + [TestCase((ushort)1000, (ushort)32767, false, new byte[] { 0x0B, 0x00, 0x40, 0x00, 0xE5, 0x5F, 0xC3, 0xE8, 0x7F, 0xFF, 0x11 })] + public void Ctor_SetsCorrectDataBits(ushort locoAddress, ushort binaryStateAddress, bool enabled, byte[] expected) + { + SetLocoBinaryStateCommand command = Factory.Create(locoAddress, binaryStateAddress, enabled); + Assert.That(command.Data, Is.EqualTo(expected)); + } + + [Test] + [TestCase((ushort)28)] + [TestCase((ushort)0)] + [TestCase((ushort)32768)] + public void Ctor_BinaryStateAddressOutOfRange_ThrowsWithMessage(ushort binaryStateAddress) + { + ArgumentOutOfRangeException exception = Assert.Throws(() => Factory.Create((ushort)3, binaryStateAddress, true))!; + Assert.That(exception.Message, Does.Contain("between 29 and 32767")); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoDriveCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoDriveCommandTest.cs index 1e281ed..ac6b244 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoDriveCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoDriveCommandTest.cs @@ -1,10 +1,11 @@ using Z21.Core.Command.Driving; using Z21.Core.Exception; using Z21.Core.Model; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Driving { - public class SetLocoDriveCommandTest + public class SetLocoDriveCommandTest : CommandTestFixture { [Test] [TestCase(DccSpeedMode.Steps14, (ushort)15)] @@ -12,7 +13,7 @@ public class SetLocoDriveCommandTest [TestCase(DccSpeedMode.Steps128, (ushort)127)] public void Ctor_SpeedOutOfRange_ThrowsLocoSpeedOutOfRangeException(DccSpeedMode dccSpeedMode, ushort locoSpeed) { - Assert.Throws(() => _ = new SetLocoDriveCommand(dccSpeedMode, 0, DrivingDirection.Forward, locoSpeed)); + Assert.Throws(() => _ = Factory.Create(dccSpeedMode, (ushort)0, DrivingDirection.Forward, locoSpeed)); } [Test] @@ -22,8 +23,8 @@ public void Ctor_SpeedOutOfRange_ThrowsLocoSpeedOutOfRangeException(DccSpeedMode [TestCase(DccSpeedMode.Steps14, (ushort)130, DrivingDirection.Backward, (ushort)1, new byte[] { 0x0A, 0x00, 0x40, 0x00, 0xE4, 0x10, 0xC0, 0x82, 0x2, 0xB4 })] public void Ctor_SetsCorrectDataBits(DccSpeedMode dccSpeedMode, ushort locoAddress, DrivingDirection drivingDirection, ushort locoSpeed, byte[] data) { - SetLocoDriveCommand command = new(dccSpeedMode, locoAddress, drivingDirection, locoSpeed); + SetLocoDriveCommand command = Factory.Create(dccSpeedMode, locoAddress, drivingDirection, locoSpeed); Assert.That(command.Data, Is.EqualTo(data)); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoEStopCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoEStopCommandTest.cs index 42ce1dc..21fdb56 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoEStopCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoEStopCommandTest.cs @@ -1,18 +1,19 @@ using Z21.Core.Command.Driving; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Driving { - public class SetLocoEStopCommandTest + public class SetLocoEStopCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - SetLocoEStopCommand getSerialNumberCommand = new(3); - Assert.That(getSerialNumberCommand.Data, + SetLocoEStopCommand command = Factory.Create((ushort)3); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x08, 0x00, 0x40, 0x00, 0x92, 0x00, 0x03, 0x91 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoFunctionCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoFunctionCommandTest.cs index 60d475b..247e747 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoFunctionCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoFunctionCommandTest.cs @@ -1,9 +1,12 @@ +using Z21.Core.Codecs; using Z21.Core.Command.Driving; +using Z21.Core.Framing; using Z21.Core.Model; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Driving { - public class SetLocoFunctionCommandTest + public class SetLocoFunctionCommandTest : CommandTestFixture { [Test] [TestCase((ushort)20, (ushort)0, FunctionToggleType.Off, new byte[] { 0x0A, 0x00, 0x40, 0x00, 0xE4, 0xF8, 0x00, 0x14, 0x00, 0x08 })] @@ -11,8 +14,19 @@ public class SetLocoFunctionCommandTest [TestCase((ushort)16, (ushort)5, FunctionToggleType.Toggle, new byte[] { 0x0A, 0x00, 0x40, 0x00, 0xE4, 0xF8, 0x00, 0x10, 0x85, 0x89 })] public void Ctor_SetsCorrectDataBits(ushort locoAddress, ushort functionIndex, FunctionToggleType toggleType, byte[] data) { - SetLocoFunctionCommand command = new(locoAddress, functionIndex, toggleType); + SetLocoFunctionCommand command = Factory.Create(locoAddress, functionIndex, toggleType); Assert.That(command.Data, Is.EqualTo(data)); } + + [Test] + [TestCase((ushort)64)] + [TestCase((ushort)255)] + public void Ctor_FunctionIndexAboveSixBitField_ThrowsArgumentOutOfRange(ushort functionIndex) + { + // Spec §4.3.1: DB3 is TTNNNNNN, so the index occupies only the low 6 bits (0..63). + // A larger value would overflow into the TT toggle-type bits and must be rejected. + Assert.Throws( + () => new SetLocoFunctionCommand(new Z21FrameBuilder(), new AddressCodec(), 3, functionIndex, FunctionToggleType.Off)); + } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoFunctionGroupCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoFunctionGroupCommandTest.cs new file mode 100644 index 0000000..ace36c5 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Driving/SetLocoFunctionGroupCommandTest.cs @@ -0,0 +1,18 @@ +using Z21.Core.Command.Driving; +using Z21.Core.Model; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Driving +{ + public class SetLocoFunctionGroupCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)3, LocoFunctionGroup.Group1, (byte)0x10, new byte[] { 0x0A, 0x00, 0x40, 0x00, 0xE4, 0x20, 0x00, 0x03, 0x10, 0xD7 })] + [TestCase((ushort)200, LocoFunctionGroup.Group4, (byte)0x05, new byte[] { 0x0A, 0x00, 0x40, 0x00, 0xE4, 0x23, 0xC0, 0xC8, 0x05, 0xCA })] + public void Ctor_SetsCorrectDataBits(ushort locoAddress, LocoFunctionGroup group, byte functions, byte[] expected) + { + SetLocoFunctionGroupCommand command = Factory.Create(locoAddress, group, functions); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/FastClock/FastClockCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/FastClock/FastClockCommandTest.cs new file mode 100644 index 0000000..b9a1bc1 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/FastClock/FastClockCommandTest.cs @@ -0,0 +1,62 @@ +using CommandStation.Model; +using Z21.Core.Command.FastClock; +using Z21.Core.Model; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.FastClock +{ + public class FastClockCommandTest : CommandTestFixture + { + [Test] + [TestCase(FastClockAction.Read, new byte[] { 0x07, 0x00, 0xCC, 0x00, 0x21, 0x2A, 0x0B })] + [TestCase(FastClockAction.Start, new byte[] { 0x07, 0x00, 0xCC, 0x00, 0x21, 0x2C, 0x0D })] + [TestCase(FastClockAction.Stop, new byte[] { 0x07, 0x00, 0xCC, 0x00, 0x21, 0x2D, 0x0C })] + public void Control_Action_BuildsChecksummedFrame(FastClockAction action, byte[] expected) + { + FastClockControlCommand command = Factory.Create(action); + Assert.That(command.Data, Is.EqualTo(expected)); + } + + [Test] + public void Control_SetModelTime_EncodesDayHourMinuteRate() + { + FastClockControlCommand command = Factory.Create(new ModelTime(0, 12, 30, 0, 8)); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x0A, 0x00, 0xCC, 0x00, 0x24, 0x2B, 0x0C, 0x1E, 0x08, 0x15 })); + } + + [Test] + public void Control_SetModelTime_EncodesDayInHighBits() + { + FastClockControlCommand command = Factory.Create(new ModelTime(2, 12, 30, 0, 8)); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x0A, 0x00, 0xCC, 0x00, 0x24, 0x2B, 0x4C, 0x1E, 0x08, 0x55 })); + } + + [Test] + public void GetSettings_BuildsRequest() + { + GetFastClockSettingsCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x05, 0x00, 0xCE, 0x00, 0x04 })); + } + + [Test] + public void SetSettings_SettingsOnly() + { + SetFastClockSettingsCommand command = Factory.Create((byte)0x4F); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x05, 0x00, 0xCF, 0x00, 0x4F })); + } + + [Test] + public void SetSettings_SettingsAndRate() + { + SetFastClockSettingsWithRateCommand command = Factory.Create((byte)0x4F, (byte)0x01); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x06, 0x00, 0xCF, 0x00, 0x4F, 0x01 })); + } + + [Test] + public void SetSettings_SettingsRateAndStart() + { + SetFastClockSettingsWithStartTimeCommand command = Factory.Create((byte)0x4F, (byte)0x01, (byte)0x0C, (byte)0x1E); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x08, 0x00, 0xCF, 0x00, 0x4F, 0x01, 0x0C, 0x1E })); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Feedback/GetRmBusDataCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Feedback/GetRmBusDataCommandTest.cs new file mode 100644 index 0000000..c1683f6 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Feedback/GetRmBusDataCommandTest.cs @@ -0,0 +1,17 @@ +using Z21.Core.Command.Feedback; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Feedback +{ + public class GetRmBusDataCommandTest : CommandTestFixture + { + [Test] + [TestCase((byte)0, new byte[] { 0x05, 0x00, 0x81, 0x00, 0x00 })] + [TestCase((byte)1, new byte[] { 0x05, 0x00, 0x81, 0x00, 0x01 })] + public void Ctor_SetsCorrectDataBits(byte groupIndex, byte[] expected) + { + GetRmBusDataCommand command = Factory.Create(groupIndex); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Feedback/ProgramRmBusModuleCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Feedback/ProgramRmBusModuleCommandTest.cs new file mode 100644 index 0000000..21208e0 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Feedback/ProgramRmBusModuleCommandTest.cs @@ -0,0 +1,17 @@ +using Z21.Core.Command.Feedback; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Feedback +{ + public class ProgramRmBusModuleCommandTest : CommandTestFixture + { + [Test] + [TestCase((byte)5, new byte[] { 0x05, 0x00, 0x82, 0x00, 0x05 })] + [TestCase((byte)0, new byte[] { 0x05, 0x00, 0x82, 0x00, 0x00 })] + public void Ctor_SetsCorrectDataBits(byte address, byte[] expected) + { + ProgramRmBusModuleCommand command = Factory.Create(address); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/LocoNet/LocoNetCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/LocoNet/LocoNetCommandTest.cs new file mode 100644 index 0000000..357d73d --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/LocoNet/LocoNetCommandTest.cs @@ -0,0 +1,31 @@ +using Z21.Core.Command.LocoNet; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.LocoNet +{ + public class LocoNetCommandTest : CommandTestFixture + { + [Test] + public void LocoNetFromLan_WrapsRawMessage() + { + LocoNetFromLanCommand command = Factory.Create(new byte[] { 0xB0, 0x01, 0x02, 0x03 }); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x08, 0x00, 0xA2, 0x00, 0xB0, 0x01, 0x02, 0x03 })); + } + + [Test] + [TestCase((ushort)3, new byte[] { 0x06, 0x00, 0xA3, 0x00, 0x03, 0x00 })] + [TestCase((ushort)1000, new byte[] { 0x06, 0x00, 0xA3, 0x00, 0xE8, 0x03 })] + public void LocoNetDispatchAddress_WritesLittleEndianAddress(ushort locoAddress, byte[] expected) + { + LocoNetDispatchAddressCommand command = Factory.Create(locoAddress); + Assert.That(command.Data, Is.EqualTo(expected)); + } + + [Test] + public void LocoNetDetector_MatchesSpecExample() + { + LocoNetDetectorCommand command = Factory.Create((byte)0x81, (ushort)1016); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0xA4, 0x00, 0x81, 0xF8, 0x03 })); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/NewCommandNameTest.cs b/src/Z21.Client.UnitTest/Core/Command/NewCommandNameTest.cs new file mode 100644 index 0000000..3178ce9 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/NewCommandNameTest.cs @@ -0,0 +1,62 @@ +using CommandStation.Model; +using Z21.Core.Command.Booster; +using Z21.Core.Command.Can; +using Z21.Core.Command.Decoder; +using Z21.Core.Command.Driving; +using Z21.Core.Command.FastClock; +using Z21.Core.Command.Feedback; +using Z21.Core.Command.LocoNet; +using Z21.Core.Command.Programming; +using Z21.Core.Command.RailCom; +using Z21.Core.Command.ZLink; +using Z21.Core.Model; + +namespace Z21.UnitTest.Core.Command +{ + public class NewCommandNameTest : CommandTestFixture + { + [Test] + public void Commands_ExposeTheirProtocolName() + { + Assert.Multiple(() => + { + Assert.That(Factory.Create((ushort)3, LocoFunctionGroup.Group1, (byte)0x10).Name, Is.EqualTo("LAN_X_SET_LOCO_FUNCTION_GROUP")); + Assert.That(Factory.Create((ushort)3, (ushort)29, true).Name, Is.EqualTo("LAN_X_SET_LOCO_BINARY_STATE")); + Assert.That(Factory.Create((ushort)0).Name, Is.EqualTo("LAN_X_CV_READ")); + Assert.That(Factory.Create((ushort)0, (byte)0).Name, Is.EqualTo("LAN_X_CV_WRITE")); + Assert.That(Factory.Create((byte)1).Name, Is.EqualTo("LAN_X_DCC_READ_REGISTER")); + Assert.That(Factory.Create((byte)1, (byte)1).Name, Is.EqualTo("LAN_X_DCC_WRITE_REGISTER")); + Assert.That(Factory.Create((byte)0, (byte)0).Name, Is.EqualTo("LAN_X_MM_WRITE_BYTE")); + Assert.That(Factory.Create((ushort)3, (ushort)0, (byte)0).Name, Is.EqualTo("LAN_X_CV_POM_WRITE_BYTE")); + Assert.That(Factory.Create((ushort)3, (ushort)0, (byte)0, true).Name, Is.EqualTo("LAN_X_CV_POM_WRITE_BIT")); + Assert.That(Factory.Create((ushort)3, (ushort)0).Name, Is.EqualTo("LAN_X_CV_POM_READ_BYTE")); + Assert.That(Factory.Create((ushort)1, true, (byte)0, (ushort)0, (byte)0).Name, Is.EqualTo("LAN_X_CV_POM_ACCESSORY_WRITE_BYTE")); + Assert.That(Factory.Create((ushort)1, true, (byte)0, (ushort)0, (byte)0, true).Name, Is.EqualTo("LAN_X_CV_POM_ACCESSORY_WRITE_BIT")); + Assert.That(Factory.Create((ushort)1, true, (byte)0, (ushort)0).Name, Is.EqualTo("LAN_X_CV_POM_ACCESSORY_READ_BYTE")); + Assert.That(Factory.Create((byte)0).Name, Is.EqualTo("LAN_RMBUS_GETDATA")); + Assert.That(Factory.Create((byte)0).Name, Is.EqualTo("LAN_RMBUS_PROGRAMMODULE")); + Assert.That(Factory.Create((ushort)3).Name, Is.EqualTo("LAN_RAILCOM_GETDATA")); + Assert.That(Factory.Create(new byte[] { 0xB0 }).Name, Is.EqualTo("LAN_LOCONET_FROM_LAN")); + Assert.That(Factory.Create((ushort)3).Name, Is.EqualTo("LAN_LOCONET_DISPATCH_ADDR")); + Assert.That(Factory.Create((byte)0x81, (ushort)1016).Name, Is.EqualTo("LAN_LOCONET_DETECTOR")); + Assert.That(Factory.Create((ushort)0xD000).Name, Is.EqualTo("LAN_CAN_DETECTOR")); + Assert.That(Factory.Create((ushort)0xC101).Name, Is.EqualTo("LAN_CAN_DEVICE_GET_DESCRIPTION")); + Assert.That(Factory.Create((ushort)0xC101, "AB").Name, Is.EqualTo("LAN_CAN_DEVICE_SET_DESCRIPTION")); + Assert.That(Factory.Create((ushort)0xC101, (byte)0xFF).Name, Is.EqualTo("LAN_CAN_BOOSTER_SET_TRACKPOWER")); + Assert.That(Factory.Create(FastClockAction.Read).Name, Is.EqualTo("LAN_FAST_CLOCK_CONTROL")); + Assert.That(Factory.Create().Name, Is.EqualTo("LAN_FAST_CLOCK_SETTINGS_GET")); + Assert.That(Factory.Create((byte)0x4F).Name, Is.EqualTo("LAN_FAST_CLOCK_SETTINGS_SET")); + Assert.That(Factory.Create((byte)0x4F, (byte)1).Name, Is.EqualTo("LAN_FAST_CLOCK_SETTINGS_SET")); + Assert.That(Factory.Create((byte)0x4F, (byte)1, (byte)0, (byte)0).Name, Is.EqualTo("LAN_FAST_CLOCK_SETTINGS_SET")); + Assert.That(Factory.Create().Name, Is.EqualTo("LAN_BOOSTER_GET_DESCRIPTION")); + Assert.That(Factory.Create("AB").Name, Is.EqualTo("LAN_BOOSTER_SET_DESCRIPTION")); + Assert.That(Factory.Create((byte)0x03, (byte)0x01).Name, Is.EqualTo("LAN_BOOSTER_SET_POWER")); + Assert.That(Factory.Create().Name, Is.EqualTo("LAN_BOOSTER_SYSTEMSTATE_GETDATA")); + Assert.That(Factory.Create().Name, Is.EqualTo("LAN_DECODER_GET_DESCRIPTION")); + Assert.That(Factory.Create("AB").Name, Is.EqualTo("LAN_DECODER_SET_DESCRIPTION")); + Assert.That(Factory.Create().Name, Is.EqualTo("LAN_DECODER_SYSTEMSTATE_GETDATA")); + Assert.That(Factory.Create().Name, Is.EqualTo("LAN_ZLINK_GET_HWINFO")); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryReadByteCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryReadByteCommandTest.cs new file mode 100644 index 0000000..2d22fc6 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryReadByteCommandTest.cs @@ -0,0 +1,17 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class CvPomAccessoryReadByteCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)1, true, (byte)0, (ushort)0, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE4, 0x00, 0x00, 0x23 })] + [TestCase((ushort)1, true, (byte)0, (ushort)256, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE5, 0x00, 0x00, 0x22 })] + public void Ctor_SetsCorrectDataBits(ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress, byte[] expected) + { + CvPomAccessoryReadByteCommand command = Factory.Create(decoderAddress, wholeDecoder, output, cvAddress); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteBitCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteBitCommandTest.cs new file mode 100644 index 0000000..71d6572 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteBitCommandTest.cs @@ -0,0 +1,18 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class CvPomAccessoryWriteBitCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)1, true, (byte)0, (ushort)0, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE8, 0x00, 0x0A, 0x25 })] + [TestCase((ushort)1, true, (byte)0, (ushort)0, (byte)2, false, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE8, 0x00, 0x02, 0x2D })] + [TestCase((ushort)1, true, (byte)0, (ushort)256, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xE9, 0x00, 0x0A, 0x24 })] + public void Ctor_SetsCorrectDataBits(ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress, byte bitPosition, bool bitValue, byte[] expected) + { + CvPomAccessoryWriteBitCommand command = Factory.Create(decoderAddress, wholeDecoder, output, cvAddress, bitPosition, bitValue); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteByteCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteByteCommandTest.cs new file mode 100644 index 0000000..7bdbbff --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomAccessoryWriteByteCommandTest.cs @@ -0,0 +1,16 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class CvPomAccessoryWriteByteCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)1, true, (byte)0, (ushort)0, (byte)0x05, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x31, 0x00, 0x10, 0xEC, 0x00, 0x05, 0x2E })] + public void Ctor_SetsCorrectDataBits(ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress, byte value, byte[] expected) + { + CvPomAccessoryWriteByteCommand command = Factory.Create(decoderAddress, wholeDecoder, output, cvAddress, value); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomReadByteCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomReadByteCommandTest.cs new file mode 100644 index 0000000..962a072 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomReadByteCommandTest.cs @@ -0,0 +1,16 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class CvPomReadByteCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)3, (ushort)0, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE4, 0x00, 0x00, 0x31 })] + public void Ctor_SetsCorrectDataBits(ushort locoAddress, ushort cvAddress, byte[] expected) + { + CvPomReadByteCommand command = Factory.Create(locoAddress, cvAddress); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteBitCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteBitCommandTest.cs new file mode 100644 index 0000000..50a5e4f --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteBitCommandTest.cs @@ -0,0 +1,18 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class CvPomWriteBitCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)3, (ushort)0, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE8, 0x00, 0x0A, 0x37 })] + [TestCase((ushort)3, (ushort)0, (byte)2, false, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE8, 0x00, 0x02, 0x3F })] + [TestCase((ushort)3, (ushort)256, (byte)2, true, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xE9, 0x00, 0x0A, 0x36 })] + public void Ctor_SetsCorrectDataBits(ushort locoAddress, ushort cvAddress, byte bitPosition, bool bitValue, byte[] expected) + { + CvPomWriteBitCommand command = Factory.Create(locoAddress, cvAddress, bitPosition, bitValue); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteByteCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteByteCommandTest.cs new file mode 100644 index 0000000..4f70f41 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvPomWriteByteCommandTest.cs @@ -0,0 +1,17 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class CvPomWriteByteCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)3, (ushort)0, (byte)0x05, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xEC, 0x00, 0x05, 0x3C })] + [TestCase((ushort)3, (ushort)256, (byte)0x05, new byte[] { 0x0C, 0x00, 0x40, 0x00, 0xE6, 0x30, 0x00, 0x03, 0xED, 0x00, 0x05, 0x3D })] + public void Ctor_SetsCorrectDataBits(ushort locoAddress, ushort cvAddress, byte value, byte[] expected) + { + CvPomWriteByteCommand command = Factory.Create(locoAddress, cvAddress, value); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvReadCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvReadCommandTest.cs new file mode 100644 index 0000000..ad0464a --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvReadCommandTest.cs @@ -0,0 +1,17 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class CvReadCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)0, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x23, 0x11, 0x00, 0x00, 0x32 })] + [TestCase((ushort)28, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x23, 0x11, 0x00, 0x1C, 0x2E })] + public void Ctor_SetsCorrectDataBits(ushort cvAddress, byte[] expected) + { + CvReadCommand command = Factory.Create(cvAddress); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/CvWriteCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/CvWriteCommandTest.cs new file mode 100644 index 0000000..cc5d7fb --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/CvWriteCommandTest.cs @@ -0,0 +1,16 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class CvWriteCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)0, (byte)0x03, new byte[] { 0x0A, 0x00, 0x40, 0x00, 0x24, 0x12, 0x00, 0x00, 0x03, 0x35 })] + public void Ctor_SetsCorrectDataBits(ushort cvAddress, byte value, byte[] expected) + { + CvWriteCommand command = Factory.Create(cvAddress, value); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/DccReadRegisterCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/DccReadRegisterCommandTest.cs new file mode 100644 index 0000000..18f2407 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/DccReadRegisterCommandTest.cs @@ -0,0 +1,16 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class DccReadRegisterCommandTest : CommandTestFixture + { + [Test] + [TestCase((byte)0x01, new byte[] { 0x08, 0x00, 0x40, 0x00, 0x22, 0x11, 0x01, 0x32 })] + public void Ctor_SetsCorrectDataBits(byte register, byte[] expected) + { + DccReadRegisterCommand command = Factory.Create(register); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/DccWriteRegisterCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/DccWriteRegisterCommandTest.cs new file mode 100644 index 0000000..80a4c18 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/DccWriteRegisterCommandTest.cs @@ -0,0 +1,16 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class DccWriteRegisterCommandTest : CommandTestFixture + { + [Test] + [TestCase((byte)0x01, (byte)0x05, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x23, 0x12, 0x01, 0x05, 0x35 })] + public void Ctor_SetsCorrectDataBits(byte register, byte value, byte[] expected) + { + DccWriteRegisterCommand command = Factory.Create(register, value); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Programming/MmWriteByteCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Programming/MmWriteByteCommandTest.cs new file mode 100644 index 0000000..753716f --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Programming/MmWriteByteCommandTest.cs @@ -0,0 +1,16 @@ +using Z21.Core.Command.Programming; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.Programming +{ + public class MmWriteByteCommandTest : CommandTestFixture + { + [Test] + [TestCase((byte)0x00, (byte)0x05, new byte[] { 0x0A, 0x00, 0x40, 0x00, 0x24, 0xFF, 0x00, 0x00, 0x05, 0xDE })] + public void Ctor_MatchesSpecExample(byte register, byte value, byte[] expected) + { + MmWriteByteCommand command = Factory.Create(register, value); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/RailCom/GetRailComDataCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/RailCom/GetRailComDataCommandTest.cs new file mode 100644 index 0000000..c626be8 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/RailCom/GetRailComDataCommandTest.cs @@ -0,0 +1,18 @@ +using Z21.Core.Command.RailCom; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.RailCom +{ + public class GetRailComDataCommandTest : CommandTestFixture + { + [Test] + [TestCase((ushort)3, new byte[] { 0x07, 0x00, 0x89, 0x00, 0x01, 0x03, 0x00 })] + [TestCase((ushort)1000, new byte[] { 0x07, 0x00, 0x89, 0x00, 0x01, 0xE8, 0x03 })] + [TestCase((ushort)0, new byte[] { 0x07, 0x00, 0x89, 0x00, 0x01, 0x00, 0x00 })] + public void Ctor_SetsCorrectDataBits(ushort locoAddress, byte[] expected) + { + GetRailComDataCommand command = Factory.Create(locoAddress); + Assert.That(command.Data, Is.EqualTo(expected)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Settings/GetAccessoryModeCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Settings/GetAccessoryModeCommandTest.cs index 86bddfd..245cafb 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Settings/GetAccessoryModeCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Settings/GetAccessoryModeCommandTest.cs @@ -1,13 +1,14 @@ using Z21.Core.Command.Settings; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Settings { - public class GetAccessoryModeCommandTest + public class GetAccessoryModeCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetAccessoryModeCommand command = new(24); + GetAccessoryModeCommand command = Factory.Create((short)24); Assert.That(command.Data, Is.EqualTo(new byte[] { @@ -20,4 +21,4 @@ public void Ctor_SetsCorrectDataBits() })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Settings/GetLocoModeCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Settings/GetLocoModeCommandTest.cs index bb78b7a..9c51d9d 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Settings/GetLocoModeCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Settings/GetLocoModeCommandTest.cs @@ -1,13 +1,14 @@ using Z21.Core.Command.Settings; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Settings { - public class GetLocoModeCommandTest + public class GetLocoModeCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetLocoModeCommand command = new(24); + GetLocoModeCommand command = Factory.Create((short)24); Assert.That( command.Data, Is.EqualTo( new byte[] @@ -21,4 +22,4 @@ public void Ctor_SetsCorrectDataBits() })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Settings/SetAccessoryModeCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Settings/SetAccessoryModeCommandTest.cs index 9ec317a..ed6f6d3 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Settings/SetAccessoryModeCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Settings/SetAccessoryModeCommandTest.cs @@ -1,14 +1,15 @@ using Z21.Core.Command.Settings; using Z21.Core.Model; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Settings { - public class SetAccessoryModeCommandTest + public class SetAccessoryModeCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits([Values(DecoderMode.DCC, DecoderMode.MM)] DecoderMode decoderMode) { - SetAccessoryModeCommand command = new(12, decoderMode); + SetAccessoryModeCommand command = Factory.Create((short)12, decoderMode); Assert.That(command.Data, Is.EqualTo(new byte[] { @@ -25,9 +26,9 @@ public void Ctor_SetsCorrectDataBits([Values(DecoderMode.DCC, DecoderMode.MM)] D [Test] public void Ctor_LocoModeUnknown_ThrowsArgumentException() { - ArgumentException? exception = Assert.Throws(() => _ = new SetAccessoryModeCommand(12, DecoderMode.Unknown)); + ArgumentException? exception = Assert.Throws(() => _ = Factory.Create((short)12, DecoderMode.Unknown)); Assert.That(exception, Is.Not.Null); Assert.That(exception.Message, Is.EqualTo($"{DecoderMode.Unknown} is not a valid DecoderMode. (Parameter 'decoderMode')")); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Settings/SetLocoModeCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Settings/SetLocoModeCommandTest.cs index 9a1a967..04e7d4d 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Settings/SetLocoModeCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Settings/SetLocoModeCommandTest.cs @@ -1,14 +1,15 @@ using Z21.Core.Command.Settings; using Z21.Core.Model; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Settings { - public class SetLocoModeCommandTest + public class SetLocoModeCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits([Values(DecoderMode.DCC, DecoderMode.MM)] DecoderMode decoderMode) { - SetLocoModeCommand command = new(12, decoderMode); + SetLocoModeCommand command = Factory.Create((short)12, decoderMode); Assert.That(command.Data, Is.EqualTo(new byte[] { @@ -25,9 +26,9 @@ public void Ctor_SetsCorrectDataBits([Values(DecoderMode.DCC, DecoderMode.MM)] D [Test] public void Ctor_LocoModeUnknown_ThrowsArgumentException() { - ArgumentException? exception = Assert.Throws(() => _ = new SetLocoModeCommand(12, DecoderMode.Unknown)); + ArgumentException? exception = Assert.Throws(() => _ = Factory.Create((short)12, DecoderMode.Unknown)); Assert.That(exception, Is.Not.Null); Assert.That(exception.Message, Is.EqualTo($"{DecoderMode.Unknown} is not a valid DecoderMode. (Parameter 'decoderMode')")); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Switching/GetExtAccessoryInfoCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Switching/GetExtAccessoryInfoCommandTest.cs index 87f80cc..88cbdb3 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Switching/GetExtAccessoryInfoCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Switching/GetExtAccessoryInfoCommandTest.cs @@ -1,13 +1,14 @@ using Z21.Core.Command.Switching; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Switching { - public class GetExtAccessoryInfoCommandTest + public class GetExtAccessoryInfoCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetExtAccessoryInfoCommand command = new(15); + GetExtAccessoryInfoCommand command = Factory.Create((ushort)1); Assert.That(command.Data, Is.EqualTo(new byte[] { @@ -15,10 +16,10 @@ public void Ctor_SetsCorrectDataBits() 0x40, 0x0, 0x44, 0x0, - 0xE, + 0x4, 0x0, - 0x4A + 0x40 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Switching/GetTurnoutInfoCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Switching/GetTurnoutInfoCommandTest.cs index 7e72dc7..804a4bd 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Switching/GetTurnoutInfoCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Switching/GetTurnoutInfoCommandTest.cs @@ -1,13 +1,14 @@ using Z21.Core.Command.Switching; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Switching { - public class GetTurnoutInfoCommandTest + public class GetTurnoutInfoCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetTurnoutInfoCommand command = new(15); + GetTurnoutInfoCommand command = Factory.Create((ushort)15); Assert.That(command.Data, Is.EqualTo(new byte[] { @@ -18,7 +19,7 @@ public void Ctor_SetsCorrectDataBits() [Test] public void Ctor_AccessoryAddressIs0_ThrowsArgumentOutOfRangeException() { - Assert.Throws(() => _ = new GetTurnoutInfoCommand(0)); + Assert.Throws(() => _ = Factory.Create((ushort)0)); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Switching/SetExtAccessoryCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Switching/SetExtAccessoryCommandTest.cs index 6c478f2..4eb03ac 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Switching/SetExtAccessoryCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Switching/SetExtAccessoryCommandTest.cs @@ -1,9 +1,10 @@ using Z21.Core.Command.Switching; using Z21.Core.Model.ExcAccessoryPayload; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Switching { - public class SetExtAccessoryCommandTest + public class SetExtAccessoryCommandTest : CommandTestFixture { public class ExcAccessoryPayloadDummy(byte payload) : IExcAccessoryPayload { @@ -13,7 +14,7 @@ public class ExcAccessoryPayloadDummy(byte payload) : IExcAccessoryPayload [Test] public void Ctor_WithPayload_SetsCorrectDataBits() { - SetExtAccessoryCommand command = new(15, new ExcAccessoryPayloadDummy(0x52)); + SetExtAccessoryCommand command = Factory.Create((ushort)1, new ExcAccessoryPayloadDummy(0x52)); Assert.That(command.Data, Is.EqualTo(new byte[] { @@ -21,17 +22,17 @@ public void Ctor_WithPayload_SetsCorrectDataBits() 0x40, 0x0, 0x54, 0x0, - 0xE, + 0x4, 0x52, 0x0, - 0x8 + 0x2 })); } [Test] public void Ctor_SetsCorrectDataBits() { - SetExtAccessoryCommand command = new(15, 0x62); + SetExtAccessoryCommand command = Factory.Create((ushort)1, (byte)0x05); Assert.That(command.Data, Is.EqualTo(new byte[] { @@ -39,11 +40,11 @@ public void Ctor_SetsCorrectDataBits() 0x40, 0x0, 0x54, 0x0, - 0xE, - 0x62, + 0x4, + 0x5, 0x0, - 0x38 + 0x55 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Switching/SetTurnoutCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/Switching/SetTurnoutCommandTest.cs index 2d6c3af..86c5238 100644 --- a/src/Z21.Client.UnitTest/Core/Command/Switching/SetTurnoutCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/Switching/SetTurnoutCommandTest.cs @@ -1,25 +1,26 @@ using Z21.Core.Command.Switching; using Z21.Core.Model; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.Switching { - public class SetTurnoutCommandTest + public class SetTurnoutCommandTest : CommandTestFixture { [Test] - [TestCase((ushort)16, AccessoryOutput.Output1, AccessoryState.Activate, true, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x53, 0x00, 0x0F, 0xA8, 0xF4 })] - [TestCase((ushort)8, AccessoryOutput.Output2, AccessoryState.Activate, true, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x53, 0x00, 0x07, 0xA9, 0xFD })] - [TestCase((ushort)8, AccessoryOutput.Output2, AccessoryState.Deactivate, true, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x53, 0x00, 0x07, 0xA1, 0xF5 })] - [TestCase((ushort)8, AccessoryOutput.Output2, AccessoryState.Deactivate, false, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x53, 0x00, 0x07, 0x81, 0xD5 })] + [TestCase((ushort)16, AccessoryOutput.Output1, AccessoryState.Activate, true, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x53, 0x00, 0x0F, 0x88, 0xD4 })] + [TestCase((ushort)8, AccessoryOutput.Output2, AccessoryState.Activate, true, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x53, 0x00, 0x07, 0x89, 0xDD })] + [TestCase((ushort)8, AccessoryOutput.Output2, AccessoryState.Deactivate, true, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x53, 0x00, 0x07, 0x81, 0xD5 })] + [TestCase((ushort)8, AccessoryOutput.Output2, AccessoryState.Deactivate, false, new byte[] { 0x09, 0x00, 0x40, 0x00, 0x53, 0x00, 0x07, 0xA1, 0xF5 })] public void Ctor_SetsCorrectDataBits(ushort accessoryAddress, AccessoryOutput accessoryOutput, AccessoryState accessoryState, bool executeImmediately, byte[] data) { - SetTurnoutCommand command = new(accessoryAddress, accessoryOutput, accessoryState, executeImmediately); + SetTurnoutCommand command = Factory.Create(accessoryAddress, accessoryOutput, accessoryState, executeImmediately); Assert.That(command.Data, Is.EqualTo(data)); } [Test] public void Ctor_AccessoryAddressIs0_ThrowsArgumentOutOfRangeException() { - Assert.Throws(() => _ = new SetTurnoutCommand(0, AccessoryOutput.Output1, AccessoryState.Activate, false)); + Assert.Throws(() => _ = Factory.Create((ushort)0, AccessoryOutput.Output1, AccessoryState.Activate, false)); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetBroadcastFlagsCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetBroadcastFlagsCommandTest.cs index 844f84b..c588106 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetBroadcastFlagsCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetBroadcastFlagsCommandTest.cs @@ -1,22 +1,15 @@ -using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class GetBroadcastFlagsCommandTest + public class GetBroadcastFlagsCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetBroadcastFlagsCommand getSerialNumberCommand = new(); - Assert.That( - getSerialNumberCommand.Data, Is.EqualTo( - new byte[] - { - 0x04, - 0x00, - 0x51, - 0x00 - })); + GetBroadcastFlagsCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0x51, 0x00 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetCodeCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetCodeCommandTest.cs index bf96598..fe62383 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetCodeCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetCodeCommandTest.cs @@ -1,22 +1,15 @@ using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class GetSoftwareLockCommandTest + public class GetSoftwareLockCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetSoftwareLockCommand command = new(); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x04, - 0x00, - 0x18, - 0x00 - })); + GetSoftwareLockCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0x18, 0x00 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetFirmwareVersionCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetFirmwareVersionCommandTest.cs index 0021cc3..3550e35 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetFirmwareVersionCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetFirmwareVersionCommandTest.cs @@ -1,25 +1,15 @@ -using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class GetFirmwareVersionCommandTest + public class GetFirmwareVersionCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetFirmwareVersionCommand getSerialNumberCommand = new(); - Assert.That( - getSerialNumberCommand.Data, Is.EqualTo( - new byte[] - { - 0x07, - 0x00, - 0x40, - 0x00, - 0xF1, - 0x0A, - 0xFB - })); + GetFirmwareVersionCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0xF1, 0x0A, 0xFB })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetHardwareInfoCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetHardwareInfoCommandTest.cs index 639e781..efb9e23 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetHardwareInfoCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetHardwareInfoCommandTest.cs @@ -1,22 +1,15 @@ using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class GetHardwareInfoCommandTest + public class GetHardwareInfoCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetHardwareInfoCommand command = new(); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x04, - 0x00, - 0x1A, - 0x00 - })); + GetHardwareInfoCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0x1A, 0x00 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetSerialNumberCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetSerialNumberCommandTest.cs index e72f9f1..9c5d0f9 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetSerialNumberCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetSerialNumberCommandTest.cs @@ -1,22 +1,15 @@ -using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class GetSerialNumberCommandTest + public class GetSerialNumberCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetSerialNumberCommand getSerialNumberCommand = new(); - Assert.That( - getSerialNumberCommand.Data, Is.EqualTo( - new byte[] - { - 0x04, - 0x00, - 0x10, - 0x00 - })); + GetSerialNumberCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0x10, 0x00 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetStatusCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetStatusCommandTest.cs index 68dfdb6..4da8612 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetStatusCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetStatusCommandTest.cs @@ -1,23 +1,15 @@ -using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class GetStatusCommandTest + public class GetStatusCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - GetStatusCommand command = new(); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x07, 0x00, - 0x40, 0x00, - 0x21, - 0x24, - 0x05 - })); + GetStatusCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x21, 0x24, 0x05 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetSystemStateDataCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetSystemStateDataCommandTest.cs index a9e4679..53d668e 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetSystemStateDataCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetSystemStateDataCommandTest.cs @@ -1,14 +1,15 @@ using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class GetSystemStateDataCommandTest + public class GetSystemStateDataCommandTest : CommandTestFixture { [Test] public void Ctor_SetsDataCorrectly() { - GetSystemStateDataCommand command = new(); + GetSystemStateDataCommand command = Factory.Create(); Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0x85, 0x00 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetVersionCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetVersionCommandTest.cs index c229eb0..f2bb773 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/GetVersionCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/GetVersionCommandTest.cs @@ -1,23 +1,15 @@ -using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class GetVersionCommandTest + public class GetVersionCommandTest : CommandTestFixture { [Test] public void Ctor_SetsDataCorrectly() { - GetVersionCommand command = new(); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x07, 0x00, - 0x40, 0x00, - 0x21, - 0x21, - 0x00 - })); + GetVersionCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/LogOffCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/LogOffCommandTest.cs index 089af1c..d72b1b9 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/LogOffCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/LogOffCommandTest.cs @@ -1,22 +1,15 @@ -using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class LogOffCommandTest + public class LogOffCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - LogOffCommand command = new(); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x04, - 0x00, - 0x30, - 0x00 - })); + LogOffCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x04, 0x00, 0x30, 0x00 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/SetBroadcastFlagsCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/SetBroadcastFlagsCommandTest.cs index 5eecfa8..09ecd6c 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/SetBroadcastFlagsCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/SetBroadcastFlagsCommandTest.cs @@ -1,29 +1,35 @@ -using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState; using Z21.Core.Model; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState { - public class SetBroadcastFlagsCommandTest + public class SetBroadcastFlagsCommandTest : CommandTestFixture { [Test] public void Ctor_SetsCorrectDataBits() { - SetBroadcastFlagsCommand command = new( - Z21BroadcastFlags.DriveAndSwitchingMessages, - Z21BroadcastFlags.RailComDataChangedMessages); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x08, - 0x0, - 0x50, - 0x0, - 0x5, - 0x0, - 0x0, - 0x0 - })); + SetBroadcastFlagsCommand command = Factory.Create( + new[] + { + Z21BroadcastFlags.DriveAndSwitchingMessages, + Z21BroadcastFlags.RailComDataChangedMessages + }); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x08, 0x0, 0x50, 0x0, 0x5, 0x0, 0x0, 0x0 })); + } + + [Test] + public void Ctor_NoFlags_EncodesZero() + { + SetBroadcastFlagsCommand command = Factory.Create(new uint[0]); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x08, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00 })); + } + + [Test] + public void Name_IsLanSetBroadcastFlags() + { + SetBroadcastFlagsCommand command = Factory.Create(new[] { Z21BroadcastFlags.DriveAndSwitchingMessages }); + Assert.That(command.Name, Is.EqualTo("LAN_SET_BROADCASTFLAGS")); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetStopCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetStopCommandTest.cs index 88cbd22..9acd5b1 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetStopCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetStopCommandTest.cs @@ -1,22 +1,15 @@ -using Z21.Core.Command.SystemState.TrackPower; +using Z21.Core.Command.SystemState.TrackPower; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState.TrackPower { - public class SetStopCommandTest + public class SetStopCommandTest : CommandTestFixture { [Test] public void Ctor_SetsDataCorrectly() { - SetStopCommand command = new(); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x06, 0x00, - 0x40, 0x00, - 0x80, - (0 ^ 0x80) - })); + SetStopCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x06, 0x00, 0x40, 0x00, 0x80, 0x80 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetTrackPowerOffCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetTrackPowerOffCommandTest.cs index 270d62a..f850d18 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetTrackPowerOffCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetTrackPowerOffCommandTest.cs @@ -1,23 +1,15 @@ -using Z21.Core.Command.SystemState.TrackPower; +using Z21.Core.Command.SystemState.TrackPower; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState.TrackPower { - public class SetTrackPowerOffCommandTest + public class SetTrackPowerOffCommandTest : CommandTestFixture { [Test] public void Ctor_SetsDataCorrectly() { - SetTrackPowerOffCommand command = new(); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x07, 0x00, - 0x40, 0x00, - 0x21, - 0x80, - 0xa1 - })); + SetTrackPowerOffCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x21, 0x80, 0xa1 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetTrackPowerOnCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetTrackPowerOnCommandTest.cs index 8e0bf50..0c0ce4d 100644 --- a/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetTrackPowerOnCommandTest.cs +++ b/src/Z21.Client.UnitTest/Core/Command/SystemState/TrackPower/SetTrackPowerOnCommandTest.cs @@ -1,23 +1,15 @@ -using Z21.Core.Command.SystemState.TrackPower; +using Z21.Core.Command.SystemState.TrackPower; +using Z21.UnitTest.Core.Command; namespace Z21.UnitTest.Core.Command.SystemState.TrackPower { - public class SetTrackPowerOnCommandTest + public class SetTrackPowerOnCommandTest : CommandTestFixture { [Test] public void Ctor_SetDataCorrectly() { - SetTrackPowerOnCommand command = new(); - Assert.That( - command.Data, Is.EqualTo( - new byte[] - { - 0x07, 0x00, - 0x40, 0x00, - 0x21, - 0x81, - 0xa0 - })); + SetTrackPowerOnCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x21, 0x81, 0xa0 })); } } -} \ No newline at end of file +} diff --git a/src/Z21.Client.UnitTest/Core/Command/Z21CommandFactoryTest.cs b/src/Z21.Client.UnitTest/Core/Command/Z21CommandFactoryTest.cs new file mode 100644 index 0000000..f6b3ccd --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/Z21CommandFactoryTest.cs @@ -0,0 +1,58 @@ +using Z21.Core.Command; +using Z21.Core.Command.SystemState; +using Z21.Core.Framing; + +namespace Z21.UnitTest.Core.Command +{ + public class Z21CommandFactoryTest : CommandTestFixture + { + /// + /// A command defined entirely outside the factory: proves a new command needs zero factory edits (open/closed). + /// + private sealed class CustomTestCommand : IZ21Command + { + public CustomTestCommand(IZ21FrameBuilder frameBuilder, byte header, byte payload) + { + Data = frameBuilder.BuildXBus(header, payload); + } + + public string Name => "CUSTOM_TEST_COMMAND"; + + public byte[] Data { get; } + } + + [Test] + public void Create_BuildsCommandDefinedOutsideTheFactory() + { + CustomTestCommand command = Factory.Create((byte)0x21, (byte)0x80); + + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x21, 0x80, 0xA1 })); + } + + [Test] + public void Create_ResolvesEncodingServicesForParameterlessCommand() + { + GetFirmwareVersionCommand command = Factory.Create(); + + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0xF1, 0x0A, 0xFB })); + } + + [Test] + public void Ctor_NullFrameBuilder_Throws() + { + Assert.Throws(() => new Z21CommandFactory(null!, new Z21.Core.Codecs.AddressCodec(), new Z21.Core.Codecs.LocoSpeedCodec())); + } + + [Test] + public void Ctor_NullAddressCodec_Throws() + { + Assert.Throws(() => new Z21CommandFactory(new Z21.Core.Framing.Z21FrameBuilder(), null!, new Z21.Core.Codecs.LocoSpeedCodec())); + } + + [Test] + public void Ctor_NullLocoSpeedCodec_Throws() + { + Assert.Throws(() => new Z21CommandFactory(new Z21.Core.Framing.Z21FrameBuilder(), new Z21.Core.Codecs.AddressCodec(), null!)); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Command/ZLink/GetZLinkHardwareInfoCommandTest.cs b/src/Z21.Client.UnitTest/Core/Command/ZLink/GetZLinkHardwareInfoCommandTest.cs new file mode 100644 index 0000000..3a39b5d --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Command/ZLink/GetZLinkHardwareInfoCommandTest.cs @@ -0,0 +1,15 @@ +using Z21.Core.Command.ZLink; +using Z21.UnitTest.Core.Command; + +namespace Z21.UnitTest.Core.Command.ZLink +{ + public class GetZLinkHardwareInfoCommandTest : CommandTestFixture + { + [Test] + public void Ctor_BuildsRequest() + { + GetZLinkHardwareInfoCommand command = Factory.Create(); + Assert.That(command.Data, Is.EqualTo(new byte[] { 0x05, 0x00, 0xE8, 0x00, 0x06 })); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Exception/LocoSpeedOutOfRangeExceptionTest.cs b/src/Z21.Client.UnitTest/Core/Exception/LocoSpeedOutOfRangeExceptionTest.cs index d9ecc84..95312e0 100644 --- a/src/Z21.Client.UnitTest/Core/Exception/LocoSpeedOutOfRangeExceptionTest.cs +++ b/src/Z21.Client.UnitTest/Core/Exception/LocoSpeedOutOfRangeExceptionTest.cs @@ -15,12 +15,18 @@ public void ThrowIfOutOfRange_ValuesInRange_DoesNothing(DccSpeedMode dccSpeedMod } [Test] - [TestCase(DccSpeedMode.Steps14, (ushort)15)] - [TestCase(DccSpeedMode.Steps28, (ushort)29)] - [TestCase(DccSpeedMode.Steps128, (ushort)127)] - public void ThrowIfOutOfRange_ValuesOutOfRange_ThrowsLocoSpeedOutOfRangeException(DccSpeedMode dccSpeedMode, ushort locoSpeed) + [TestCase(DccSpeedMode.Steps14, (ushort)15, "Steps14", "14")] + [TestCase(DccSpeedMode.Steps28, (ushort)29, "Steps28", "28")] + [TestCase(DccSpeedMode.Steps128, (ushort)127, "Steps128", "126")] + public void ThrowIfOutOfRange_ValuesOutOfRange_ThrowsWithDescriptiveMessage(DccSpeedMode dccSpeedMode, ushort locoSpeed, string expectedFragment, string expectedMax) { - Assert.Throws(() => LocoSpeedOutOfRangeException.ThrowIfOutOfRange(dccSpeedMode, locoSpeed)); + LocoSpeedOutOfRangeException exception = Assert.Throws(() => LocoSpeedOutOfRangeException.ThrowIfOutOfRange(dccSpeedMode, locoSpeed))!; + Assert.Multiple(() => + { + Assert.That(exception.Message, Does.Contain(expectedFragment)); + Assert.That(exception.Message, Does.Contain($"maximum speed of {expectedMax} steps"), + "the message must state the actual maximum that the guard enforces"); + }); } } } \ No newline at end of file diff --git a/src/Z21.Client.UnitTest/Core/Exception/MtuPayloadLengthExceededExceptionTest.cs b/src/Z21.Client.UnitTest/Core/Exception/MtuPayloadLengthExceededExceptionTest.cs index 4d6a0c9..e62af8a 100644 --- a/src/Z21.Client.UnitTest/Core/Exception/MtuPayloadLengthExceededExceptionTest.cs +++ b/src/Z21.Client.UnitTest/Core/Exception/MtuPayloadLengthExceededExceptionTest.cs @@ -8,23 +8,23 @@ public class MtuPayloadLengthExceededExceptionTest [Test] public void ThrowIfExceeded_DatagramSmallerThenMaxUdpPayload_DoNothing() { - byte[] datagram = new byte [Z21Client.MaxUdpPayload - 1]; + byte[] datagram = new byte [Z21CommandStation.MaxUdpPayload - 1]; Assert.DoesNotThrow(() => MtuPayloadLengthExceededException.ThrowIfExceeded(datagram)); } [Test] public void ThrowIfExceeded_DatagramEqualMaxUdpPayload_DoNothing() { - byte[] datagram = new byte [Z21Client.MaxUdpPayload]; + byte[] datagram = new byte [Z21CommandStation.MaxUdpPayload]; Assert.DoesNotThrow(() => MtuPayloadLengthExceededException.ThrowIfExceeded(datagram)); } [Test] public void ThrowIfExceeded_DatagramBiggerThenMaxUdpPayload_ThrowMtuPayloadLengthExceededException() { - byte[] datagram = new byte [Z21Client.MaxUdpPayload + 1]; + byte[] datagram = new byte [Z21CommandStation.MaxUdpPayload + 1]; MtuPayloadLengthExceededException exception = Assert.Throws(() => MtuPayloadLengthExceededException.ThrowIfExceeded(datagram)); - Assert.That(exception.Message, Is.EqualTo($"Combined UDP payload length '{datagram.Length}' exceeds MTU size '{Z21Client.MaxUdpPayload}'.")); + Assert.That(exception.Message, Is.EqualTo($"Combined UDP payload length '{datagram.Length}' exceeds MTU size '{Z21CommandStation.MaxUdpPayload}'.")); } } } \ No newline at end of file diff --git a/src/Z21.Client.UnitTest/Core/FakeTransport.cs b/src/Z21.Client.UnitTest/Core/FakeTransport.cs new file mode 100644 index 0000000..cff3664 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/FakeTransport.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CommandStation.Transport; + +namespace Z21.UnitTest.Core +{ + public class FakeTransport : ITransport + { + public List Sent { get; } = []; + + public bool IsConnected { get; private set; } + + public event EventHandler? OnBytesReceived; + + public event EventHandler? OnConnectionChanged; + + public Task ConnectAsync() + { + IsConnected = true; + OnConnectionChanged?.Invoke(this, new ConnectionChangedEventArgs(true)); + return Task.CompletedTask; + } + + public Task DisconnectAsync() + { + IsConnected = false; + OnConnectionChanged?.Invoke(this, new ConnectionChangedEventArgs(false)); + return Task.CompletedTask; + } + + public Task SendAsync(ReadOnlyMemory data) + { + Sent.Add(data.ToArray()); + return Task.CompletedTask; + } + + public void RaiseBytes(byte[] data) => OnBytesReceived?.Invoke(this, new BytesReceivedEventArgs(data)); + + /// Simulates a transport-level connection loss (e.g. socket error), independent of . + public void RaiseConnectionLost() + { + IsConnected = false; + OnConnectionChanged?.Invoke(this, new ConnectionChangedEventArgs(false)); + } + + /// Forces the connected flag without raising events, to model the transport reconnecting on its own. + public void SetConnected(bool value) => IsConnected = value; + } +} diff --git a/src/Z21.Client.UnitTest/Core/Framing/Z21FrameBuilderTest.cs b/src/Z21.Client.UnitTest/Core/Framing/Z21FrameBuilderTest.cs new file mode 100644 index 0000000..954ba8e --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Framing/Z21FrameBuilderTest.cs @@ -0,0 +1,99 @@ +using Z21.Core.Framing; + +namespace Z21.UnitTest.Core.Framing +{ + public class Z21FrameBuilderTest + { + private Z21FrameBuilder _builder = null!; + + [SetUp] + public void SetUp() => _builder = new Z21FrameBuilder(); + + [Test] + public void BuildLan_NoPayload_WritesLengthAndHeaderOnly() + { + Assert.That(_builder.BuildLan(0x0010), Is.EqualTo(new byte[] { 0x04, 0x00, 0x10, 0x00 })); + } + + [Test] + public void BuildLan_WithPayload_AppendsPayloadAndSetsLength() + { + Assert.That(_builder.BuildLan(0x0050, 0x01, 0x02, 0x03, 0x04), + Is.EqualTo(new byte[] { 0x08, 0x00, 0x50, 0x00, 0x01, 0x02, 0x03, 0x04 })); + } + + [Test] + public void BuildLan_LowHeaderByteVaries_IsWrittenLittleEndian() + { + Assert.That(_builder.BuildLan(0x0061, 0xAA, 0xBB, 0x02), + Is.EqualTo(new byte[] { 0x07, 0x00, 0x61, 0x00, 0xAA, 0xBB, 0x02 })); + } + + [Test] + public void BuildXBus_AppendsXorOverXHeaderAndData() + { + Assert.That(_builder.BuildXBus(0xF1, 0x0A), + Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0xF1, 0x0A, 0xFB })); + } + + [Test] + public void BuildXBus_NoData_XorIsXHeaderItself() + { + Assert.That(_builder.BuildXBus(0x80), + Is.EqualTo(new byte[] { 0x06, 0x00, 0x40, 0x00, 0x80, 0x80 })); + } + + [Test] + public void BuildXBus_MatchesSetLocoDriveFrame() + { + Assert.That(_builder.BuildXBus(0xE4, 0x13, 0x00, 0x14, 0x82), + Is.EqualTo(new byte[] { 0x0A, 0x00, 0x40, 0x00, 0xE4, 0x13, 0x00, 0x14, 0x82, 0x61 })); + } + + [Test] + public void BuildLanChecksummed_AppendsXorOverDataBytes() + { + Assert.That(_builder.BuildLanChecksummed(0x00CC, 0x21, 0x2A), + Is.EqualTo(new byte[] { 0x07, 0x00, 0xCC, 0x00, 0x21, 0x2A, 0x0B })); + } + + [Test] + public void BuildLanChecksummed_NoData_AppendsZeroChecksum() + { + Assert.That(_builder.BuildLanChecksummed(0x00CC), + Is.EqualTo(new byte[] { 0x05, 0x00, 0xCC, 0x00, 0x00 })); + } + + [Test] + public void BuildLan_NullPayload_Throws() + { + Assert.Throws(() => _builder.BuildLan(0x0010, (byte[])null!)); + } + + [Test] + public void BuildXBus_NullData_Throws() + { + Assert.Throws(() => _builder.BuildXBus(0x80, (byte[])null!)); + } + + [Test] + public void BuildLanChecksummed_NullData_Throws() + { + Assert.Throws(() => _builder.BuildLanChecksummed(0x00CC, (byte[])null!)); + } + + [Test] + public void BuildLan_PayloadOver255_WritesHighLengthByte() + { + byte[] payload = new byte[252]; // total length = 4 + 252 = 256 = 0x0100 + byte[] frame = _builder.BuildLan(0x0010, payload); + + Assert.Multiple(() => + { + Assert.That(frame, Has.Length.EqualTo(256)); + Assert.That(frame[0], Is.EqualTo(0x00), "low length byte"); + Assert.That(frame[1], Is.EqualTo(0x01), "high length byte must be set"); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Framing/Z21FrameReaderTest.cs b/src/Z21.Client.UnitTest/Core/Framing/Z21FrameReaderTest.cs new file mode 100644 index 0000000..264cfdc --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Framing/Z21FrameReaderTest.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using CommandStation.Framing; +using Z21.Core.Framing; + +namespace Z21.UnitTest.Core.Framing +{ + [TestFixture] + public class Z21FrameReaderTest + { + private Z21FrameReader _reader = null!; + private List _frames = null!; + + [SetUp] + public void SetUp() + { + _reader = new Z21FrameReader(); + _frames = []; + _reader.OnFrameReceived += (_, args) => _frames.Add(args.Frame); + } + + [Test] + public void Append_SingleCompleteFrame_EmitsThatFrame() + { + byte[] frame = [0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00]; + + _reader.Append(frame); + + Assert.That(_frames, Has.Count.EqualTo(1)); + Assert.That(_frames[0], Is.EqualTo(frame)); + } + + [Test] + public void Append_TwoFramesInOneChunk_EmitsBothInOrder() + { + byte[] first = [0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00]; + byte[] second = [0x04, 0x00, 0x12, 0x34]; + byte[] combined = [.. first, .. second]; + + _reader.Append(combined); + + Assert.That(_frames, Has.Count.EqualTo(2)); + Assert.That(_frames[0], Is.EqualTo(first)); + Assert.That(_frames[1], Is.EqualTo(second)); + } + + [Test] + public void Append_FrameSplitAcrossChunks_EmitsOnceComplete() + { + _reader.Append(new byte[] { 0x07, 0x00, 0x40, 0x00 }); + Assert.That(_frames, Is.Empty); + + _reader.Append(new byte[] { 0x21, 0x21, 0x00 }); + + Assert.That(_frames, Has.Count.EqualTo(1)); + Assert.That(_frames[0], Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00 })); + } + + [Test] + public void Append_LengthPrefixSplitAcrossChunks_StillReassembles() + { + _reader.Append(new byte[] { 0x07 }); + Assert.That(_frames, Is.Empty); + + _reader.Append(new byte[] { 0x00, 0x40, 0x00, 0x21, 0x21, 0x00 }); + + Assert.That(_frames, Has.Count.EqualTo(1)); + Assert.That(_frames[0], Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00 })); + } + + [Test] + public void Append_Null_ThrowsArgumentNullException() + { + Assert.Throws(() => _reader.Append(null!)); + } + + [Test] + public void Append_ZeroLengthFrame_DiscardsBufferAndEmitsNothing() + { + _reader.Append(new byte[] { 0x00, 0x00, 0xAA, 0xBB }); + + Assert.That(_frames, Is.Empty); + + // A subsequent valid frame is processed (buffer was cleared, not stuck). + _reader.Append(new byte[] { 0x04, 0x00, 0x12, 0x34 }); + Assert.That(_frames, Has.Count.EqualTo(1)); + Assert.That(_frames[0], Is.EqualTo(new byte[] { 0x04, 0x00, 0x12, 0x34 })); + } + + [Test] + public void Append_LengthPrefixExceedsMaxFrame_DiscardsAndResyncs() + { + // DataLen = 0x2000 (8192) is far beyond the 1472-byte IPv4 payload limit: a corrupt prefix + // must not make the reader buffer indefinitely waiting for bytes that will never arrive. + _reader.Append(new byte[] { 0x00, 0x20, 0xAA, 0xBB }); + Assert.That(_frames, Is.Empty); + + // A subsequent valid frame is still processed (buffer was cleared, not stuck). + _reader.Append(new byte[] { 0x04, 0x00, 0x12, 0x34 }); + Assert.That(_frames, Has.Count.EqualTo(1)); + Assert.That(_frames[0], Is.EqualTo(new byte[] { 0x04, 0x00, 0x12, 0x34 })); + } + + [Test] + public void Append_FrameWithHighByteLength_UsesBothLengthBytes() + { + byte[] frame = new byte[256]; + frame[0] = 0x00; // low byte of length + frame[1] = 0x01; // high byte of length => 0x0100 = 256 + frame[2] = 0x40; + + _reader.Append(frame); + + Assert.That(_frames, Has.Count.EqualTo(1)); + Assert.That(_frames[0], Has.Length.EqualTo(256)); + } + + [Test] + public void Append_TrailingPartialFrame_RetainedUntilCompleted() + { + byte[] complete = [0x04, 0x00, 0x12, 0x34]; + byte[] partial = [0x05, 0x00, 0xAA]; + _reader.Append([.. complete, .. partial]); + + Assert.That(_frames, Has.Count.EqualTo(1)); + Assert.That(_frames[0], Is.EqualTo(complete)); + + _reader.Append(new byte[] { 0xBB, 0xCC }); + + Assert.That(_frames, Has.Count.EqualTo(2)); + Assert.That(_frames[1], Is.EqualTo(new byte[] { 0x05, 0x00, 0xAA, 0xBB, 0xCC })); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Helper/AddressHelperTest.cs b/src/Z21.Client.UnitTest/Core/Helper/AddressHelperTest.cs deleted file mode 100644 index 0fa495e..0000000 --- a/src/Z21.Client.UnitTest/Core/Helper/AddressHelperTest.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Z21.Core.Helper; - -namespace Z21.UnitTest.Core.Helper -{ - public class AddressHelperTest - { - [Test] - [TestCase((ushort)0, 0x00, 0x00)] - [TestCase((ushort)1, 0x01, 0x00)] - [TestCase((ushort)255, 0xFF, 0x00)] - [TestCase((ushort)256, 0x00, 0x01)] - [TestCase((ushort)512, 0x00, 0x02)] - [TestCase((ushort)1023, 0xFF, 0x03)] - [TestCase((ushort)1234, 0xD2, 0x04)] - [TestCase((ushort)16383, 0xFF, 0x3F)] - public void SplitLocoAddress_ReturnsCorrectLSBAndMSB(ushort input, byte expectedLsb, byte expectedMsb) - { - (byte lsb, byte msb) = AddressHelper.SplitLocoAddress(input); - - if (input >= 128) - expectedMsb |= 0xC0; - - Assert.Multiple(() => - { - Assert.That(lsb, Is.EqualTo(expectedLsb), "LSB is incorrect"); - Assert.That(msb, Is.EqualTo(expectedMsb), "MSB is incorrect"); - }); - } - - [Test] - public void SplitAccessoryAddress_ReturnsCorrectLSBAndMSB() - { - (byte lsb, byte msb) = AddressHelper.SplitAccessoryAddress(48); - Assert.Multiple(() => - { - Assert.That((msb << 8) + lsb, Is.EqualTo(47)); - Assert.That(msb, Is.EqualTo(0x00)); - Assert.That(lsb, Is.EqualTo(0x2F)); - }); - } - - [Test] - public void SplitAccessoryAddress_AddressIs0_ThrowsArgumentOutOfRangeException() - { - Assert.Throws(() => AddressHelper.SplitAccessoryAddress(0)); - } - - [Test] - public void CombineAccessoryAddress_ReturnsCorrectAddress() - { - const byte msb = 0x00; - const byte lsb = 0x2f; - ushort address = AddressHelper.CombineAccessoryAddress(lsb, msb); - Assert.That(address, Is.EqualTo(48)); - } - } -} \ No newline at end of file diff --git a/src/Z21.Client.UnitTest/Core/Helper/DelayedActionTest.cs b/src/Z21.Client.UnitTest/Core/Helper/DelayedActionTest.cs new file mode 100644 index 0000000..de807b0 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Helper/DelayedActionTest.cs @@ -0,0 +1,47 @@ +using System.Threading; +using Z21.Core.Helper; + +namespace Z21.UnitTest.Core.Helper +{ + public class DelayedActionTest + { + [Test] + public async Task Delay_FiresActionAfterInterval() + { + int count = 0; + using DelayedAction action = new(TimeSpan.FromMilliseconds(40), () => + { + Interlocked.Increment(ref count); + return Task.CompletedTask; + }); + + action.Delay(); + await Task.Delay(150); + + Assert.That(Volatile.Read(ref count), Is.GreaterThanOrEqualTo(1)); + } + + [Test] + public async Task Stop_PreventsFurtherFiring() + { + int count = 0; + using DelayedAction action = new(TimeSpan.FromMilliseconds(40), () => + { + Interlocked.Increment(ref count); + return Task.CompletedTask; + }); + + action.Delay(); + await Task.Delay(150); + action.Stop(); + int snapshot = Volatile.Read(ref count); + await Task.Delay(150); + + Assert.Multiple(() => + { + Assert.That(snapshot, Is.GreaterThanOrEqualTo(1), "timer should fire while running"); + Assert.That(Volatile.Read(ref count), Is.EqualTo(snapshot), "no further firing after Stop"); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Model/ExcAccessoryPayload/SwitchDecoderPayloadTest.cs b/src/Z21.Client.UnitTest/Core/Model/ExcAccessoryPayload/SwitchDecoderPayloadTest.cs index c688c55..d86d6ef 100644 --- a/src/Z21.Client.UnitTest/Core/Model/ExcAccessoryPayload/SwitchDecoderPayloadTest.cs +++ b/src/Z21.Client.UnitTest/Core/Model/ExcAccessoryPayload/SwitchDecoderPayloadTest.cs @@ -13,9 +13,17 @@ public void Ctor_CalculatesPayloadCorrectly() } [Test] - public void Ctor_SwitchTimeBiggerThen127_ThrowsArgumentOutOfRangeException() + public void Ctor_SwitchTime127_IsPermanentlyOn() { - Assert.Throws(() => _ = new SwitchDecoderPayload(AccessoryOutput.Output1, 128)); + SwitchDecoderPayload payload = new(AccessoryOutput.Output2, 127); + Assert.That(payload.Payload, Is.EqualTo(0x7F | (int)AccessoryOutput.Output2)); + } + + [Test] + public void Ctor_SwitchTimeBiggerThen127_ThrowsWithMessage() + { + ArgumentOutOfRangeException exception = Assert.Throws(() => _ = new SwitchDecoderPayload(AccessoryOutput.Output1, 128))!; + Assert.That(exception.Message, Does.Contain("Maximum switch time is 127")); } } } \ No newline at end of file diff --git a/src/Z21.Client.UnitTest/Core/Model/FirmwareVersionTest.cs b/src/Z21.Client.UnitTest/Core/Model/FirmwareVersionTest.cs index f9307f2..fff452e 100644 --- a/src/Z21.Client.UnitTest/Core/Model/FirmwareVersionTest.cs +++ b/src/Z21.Client.UnitTest/Core/Model/FirmwareVersionTest.cs @@ -78,5 +78,37 @@ public void Equals_Null_ReturnsFalse() FirmwareVersion version = new(3, 6); Assert.That(version, Is.Not.EqualTo(null)); } + + [Test] + public void EqualityOperators_WithNull_DoNotThrowAndCompareByReference() + { + FirmwareVersion version = new(3, 6); + FirmwareVersion? @null = null; + + Assert.Multiple(() => + { + Assert.That(@null == null, Is.True, "null == null"); + Assert.That(version == @null, Is.False, "version == null"); + Assert.That(@null == version, Is.False, "null == version"); + Assert.That(version != @null, Is.True, "version != null"); + }); + } + + [Test] + public void RelationalOperators_WithNull_TreatNullAsLowest() + { + FirmwareVersion version = new(1, 0); + FirmwareVersion? @null = null; + FirmwareVersion? otherNull = null; + + Assert.Multiple(() => + { + Assert.That(@null < version, Is.True, "null < version"); + Assert.That(version > @null, Is.True, "version > null"); + Assert.That(version < @null, Is.False, "version < null"); + Assert.That(@null <= otherNull, Is.True, "null <= null"); + Assert.That(version >= @null, Is.True, "version >= null"); + }); + } } } \ No newline at end of file diff --git a/src/Z21.Client.UnitTest/Core/Reflection/Z21ServiceDiscoveryTest.cs b/src/Z21.Client.UnitTest/Core/Reflection/Z21ServiceDiscoveryTest.cs new file mode 100644 index 0000000..aef210b --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Reflection/Z21ServiceDiscoveryTest.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Z21.Core.Reflection; +using Z21.Core.ResponseHandler; +using Z21.Core.ResponseHandler.Driving; +using Z21.Core.ResponseParser; + +namespace Z21.UnitTest.Core.Reflection +{ + public class Z21ServiceDiscoveryTest + { + private Z21ServiceDiscovery _discovery = null!; + + [SetUp] + public void SetUp() => _discovery = new Z21ServiceDiscovery(); + + [Test] + public void GetImplementations_ReturnsConcreteHandlersOnly() + { + Type[] implementations = _discovery.GetImplementations(typeof(IZ21ResponseHandler)).ToArray(); + + Assert.Multiple(() => + { + Assert.That(implementations, Has.Member(typeof(LocoInfoResponseHandler)), "concrete handler must be discovered"); + Assert.That(implementations, Has.None.Matches(t => t.IsAbstract), "abstract types must be excluded"); + Assert.That(implementations, Has.None.Matches(t => t.IsInterface), "interfaces must be excluded"); + Assert.That(implementations, Has.None.Matches(t => !typeof(IZ21ResponseHandler).IsAssignableFrom(t)), "all must implement the base interface"); + }); + } + + [Test] + public void GetServiceInterfaces_IncludeBaseTrue_ContainsBaseInterface() + { + Type[] interfaces = _discovery.GetServiceInterfaces(typeof(LocoInfoResponseHandler), typeof(IZ21ResponseHandler), includeBaseInterface: true).ToArray(); + + Assert.Multiple(() => + { + Assert.That(interfaces, Has.Member(typeof(IZ21ResponseHandler)), "base interface included when flag is true"); + Assert.That(interfaces, Has.Member(typeof(ILocoInfoResponseHandler)), "specific interface always included"); + }); + } + + [Test] + public void GetServiceInterfaces_IncludeBaseFalse_ExcludesBaseInterface() + { + Type[] interfaces = _discovery.GetServiceInterfaces(typeof(SystemStateResponseParser), typeof(IZ21ResponseParser), includeBaseInterface: false).ToArray(); + + Assert.Multiple(() => + { + Assert.That(interfaces, Has.None.EqualTo(typeof(IZ21ResponseParser)), "base interface excluded when flag is false"); + Assert.That(interfaces, Has.Member(typeof(ISystemStateResponseParser)), "specific interface still included"); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Booster/BoosterResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Booster/BoosterResponseHandlerTest.cs new file mode 100644 index 0000000..4a54626 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Booster/BoosterResponseHandlerTest.cs @@ -0,0 +1,79 @@ +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.Booster; + +namespace Z21.UnitTest.Core.ResponseHandler.Booster +{ + public class BoosterResponseHandlerTest + { + [Test] + public void Description_DecodesName() + { + BoosterDescriptionResponseHandler handler = new(); + byte[] response = new byte[36]; + response[0] = 0x24; + response[2] = 0xB8; + response[4] = 0x41; + response[5] = 0x42; + + Assert.That(handler.CanHandle(response), Is.True); + + BoosterDescriptionReceivedEventArgs? received = null; + handler.OnBoosterDescriptionReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received!.Name, Is.EqualTo("AB")); + } + + [Test] + public void Description_NeverSet_IsEmptyString() + { + BoosterDescriptionResponseHandler handler = new(); + byte[] response = new byte[36]; + response[0] = 0x24; + response[2] = 0xB8; + response[4] = 0xFF; + + BoosterDescriptionReceivedEventArgs? received = null; + handler.OnBoosterDescriptionReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received!.Name, Is.EqualTo(string.Empty)); + } + + [Test] + public void SystemState_DecodesAllFields() + { + BoosterSystemStateResponseHandler handler = new(); + byte[] response = + [ + 0x1C, 0x00, 0xBA, 0x00, + 0x64, 0x00, 0xFF, 0xFF, 0xC8, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00, + 0x98, 0x3A, 0x88, 0x13, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 + ]; + + Assert.That(handler.CanHandle(response), Is.True); + + BoosterSystemStateReceivedEventArgs? received = null; + handler.OnBoosterSystemStateReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.Multiple(() => + { + Assert.That(received!.State.Booster1MainCurrent, Is.EqualTo(100)); + Assert.That(received.State.Booster2MainCurrent, Is.EqualTo(-1)); + Assert.That(received.State.Booster1FilteredMainCurrent, Is.EqualTo(200)); + Assert.That(received.State.Booster1Temperature, Is.EqualTo(25)); + Assert.That(received.State.SupplyVoltage, Is.EqualTo(15000)); + Assert.That(received.State.Booster1VccVoltage, Is.EqualTo(5000)); + Assert.That(received.State.CentralState, Is.EqualTo(0x02)); + }); + } + + [Test] + public void SystemState_RejectsOtherHeaders() + { + BoosterSystemStateResponseHandler handler = new(); + Assert.That(handler.CanHandle([0x00]), Is.False); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Can/CanResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Can/CanResponseHandlerTest.cs new file mode 100644 index 0000000..d610f45 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Can/CanResponseHandlerTest.cs @@ -0,0 +1,112 @@ +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.Can; + +namespace Z21.UnitTest.Core.ResponseHandler.Can +{ + public class CanResponseHandlerTest + { + [Test] + public void Detector_DecodesAllFields() + { + CanDetectorResponseHandler handler = new(); + byte[] response = [0x0E, 0x00, 0xC4, 0x00, 0x01, 0xC1, 0x05, 0x00, 0x03, 0x01, 0x00, 0x11, 0x00, 0x00]; + + Assert.That(handler.CanHandle(response), Is.True); + + CanDetectorReceivedEventArgs? received = null; + handler.OnCanDetectorReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.Data.NetworkId, Is.EqualTo(0xC101)); + Assert.That(received.Data.ModuleAddress, Is.EqualTo(5)); + Assert.That(received.Data.Port, Is.EqualTo(3)); + Assert.That(received.Data.Type, Is.EqualTo(0x01)); + Assert.That(received.Data.Value1, Is.EqualTo(0x1100)); + Assert.That(received.Data.Value2, Is.EqualTo(0)); + }); + } + + [Test] + public void Detector_RejectsOtherHeaders() + { + CanDetectorResponseHandler handler = new(); + Assert.That(handler.CanHandle([0x0E, 0x00, 0xC8, 0x00, 0x01, 0xC1, 0x05, 0x00, 0x03, 0x01, 0x00, 0x11, 0x00, 0x00]), Is.False); + Assert.That(handler.CanHandle([0x00]), Is.False); + } + + [Test] + public void DeviceDescription_DecodesNetworkIdAndName() + { + CanDeviceDescriptionResponseHandler handler = new(); + byte[] response = [0x16, 0x00, 0xC8, 0x00, 0x01, 0xC1, 0x41, 0x42, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + + Assert.That(handler.CanHandle(response), Is.True); + + CanDeviceDescriptionReceivedEventArgs? received = null; + handler.OnCanDeviceDescriptionReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.Multiple(() => + { + Assert.That(received!.NetworkId, Is.EqualTo(0xC101)); + Assert.That(received.Name, Is.EqualTo("AB")); + }); + } + + [Test] + public void DeviceDescription_NameWithoutTerminator_KeepsFullLength() + { + CanDeviceDescriptionResponseHandler handler = new(); + byte[] response = new byte[22]; + response[2] = 0xC8; + for (int i = 6; i < 22; i++) + response[i] = (byte)'X'; + + CanDeviceDescriptionReceivedEventArgs? received = null; + handler.OnCanDeviceDescriptionReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received!.Name, Has.Length.EqualTo(16)); + } + + [Test] + public void DeviceDescription_NameStartingWithTerminator_IsEmpty() + { + CanDeviceDescriptionResponseHandler handler = new(); + byte[] response = new byte[22]; + response[2] = 0xC8; + + CanDeviceDescriptionReceivedEventArgs? received = null; + handler.OnCanDeviceDescriptionReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received!.Name, Is.Empty); + } + + [Test] + public void BoosterSystemState_DecodesAllFields() + { + CanBoosterSystemStateResponseHandler handler = new(); + byte[] response = [0x0E, 0x00, 0xCA, 0x00, 0x01, 0xC1, 0x01, 0x00, 0x80, 0x00, 0x10, 0x27, 0xE8, 0x03]; + + Assert.That(handler.CanHandle(response), Is.True); + + CanBoosterSystemStateReceivedEventArgs? received = null; + handler.OnCanBoosterSystemStateReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.Multiple(() => + { + Assert.That(received!.State.NetworkId, Is.EqualTo(0xC101)); + Assert.That(received.State.OutputPort, Is.EqualTo(1)); + Assert.That(received.State.State, Is.EqualTo(CanBoosterState.TrackVoltageOff)); + Assert.That(received.State.VccVoltage, Is.EqualTo(10000)); + Assert.That(received.State.Current, Is.EqualTo(1000)); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Decoder/DecoderDescriptionResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Decoder/DecoderDescriptionResponseHandlerTest.cs new file mode 100644 index 0000000..238ef4d --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Decoder/DecoderDescriptionResponseHandlerTest.cs @@ -0,0 +1,68 @@ +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.Decoder; + +namespace Z21.UnitTest.Core.ResponseHandler.Decoder +{ + public class DecoderDescriptionResponseHandlerTest + { + private DecoderDescriptionResponseHandler _handler = null!; + + [SetUp] + public void Setup() => _handler = new(); + + [Test] + public void Handle_DecodesName() + { + byte[] response = new byte[36]; + response[0] = 0x24; + response[2] = 0xD8; + response[4] = 0x41; + response[5] = 0x42; + + Assert.That(_handler.CanHandle(response), Is.True); + + DecoderDescriptionReceivedEventArgs? received = null; + _handler.OnDecoderDescriptionReceived += (_, args) => received = args; + _handler.Handle(response); + + Assert.That(received!.Name, Is.EqualTo("AB")); + } + + [Test] + public void Handle_NameWithoutTerminator_KeepsFullLength() + { + byte[] response = new byte[36]; + response[2] = 0xD8; + for (int i = 4; i < 36; i++) + response[i] = (byte)'X'; + + DecoderDescriptionReceivedEventArgs? received = null; + _handler.OnDecoderDescriptionReceived += (_, args) => received = args; + _handler.Handle(response); + + Assert.That(received!.Name, Has.Length.EqualTo(32)); + } + + [Test] + public void Handle_NameStartingWithTerminator_IsEmpty() + { + byte[] response = new byte[36]; + response[2] = 0xD8; + + DecoderDescriptionReceivedEventArgs? received = null; + _handler.OnDecoderDescriptionReceived += (_, args) => received = args; + _handler.Handle(response); + + Assert.That(received!.Name, Is.Empty); + } + + [Test] + public void CanHandle_RejectsOtherHeaders() + { + Assert.That(_handler.CanHandle([0x00]), Is.False); + byte[] wrong = new byte[36]; + wrong[2] = 0xDA; + Assert.That(_handler.CanHandle(wrong), Is.False); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Decoder/DecoderSystemStateResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Decoder/DecoderSystemStateResponseHandlerTest.cs new file mode 100644 index 0000000..ea9ed7a --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Decoder/DecoderSystemStateResponseHandlerTest.cs @@ -0,0 +1,101 @@ +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.Decoder; +using Z21.Core.ResponseParser; + +namespace Z21.UnitTest.Core.ResponseHandler.Decoder +{ + public class DecoderSystemStateResponseHandlerTest + { + private DecoderSystemStateResponseHandler _handler = null!; + + [SetUp] + public void Setup() => _handler = new(new SwitchDecoderSystemStateParser(), new SignalDecoderSystemStateParser()); + + private static byte[] SwitchFrame() + { + byte[] frame = new byte[48]; + frame[0] = 0x30; + frame[2] = 0xDA; + // payload begins at index 4 + frame[4] = 0x64; // Current = 100 + frame[8] = 0xE4; frame[9] = 0x0C; // Voltage = 3300 + frame[10] = 0x02; // CentralState + frame[11] = 0x20; // CentralStateEx + frame[12] = 0x11; // OutputStates[0] + frame[36] = 0x01; // Address = 1 + frame[38] = 0x02; // Address2 = 2 + frame[46] = 0x03; // Dimmed + return frame; + } + + private static byte[] SignalFrame() + { + byte[] frame = new byte[46]; + frame[0] = 0x2E; + frame[2] = 0xDA; + frame[8] = 0xE0; frame[9] = 0x2E; // Voltage = 12000 + frame[10] = 0x01; // CentralState + frame[12] = 0xAB; // OutputStates[0] + frame[16] = 0x10; // SignalDccExt[0] + frame[27] = 0x02; // SignalCount + frame[28] = 0x05; // SignalConfig[0] + frame[36] = 0x10; // Address = 16 + return frame; + } + + [Test] + public void Handle_SwitchDecoderFrame_RaisesSwitchState() + { + byte[] frame = SwitchFrame(); + Assert.That(_handler.CanHandle(frame), Is.True); + + SwitchDecoderSystemStateReceivedEventArgs? received = null; + _handler.OnSwitchDecoderSystemStateReceived += (_, args) => received = args; + _handler.Handle(frame); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.State.Current, Is.EqualTo(100)); + Assert.That(received.State.Voltage, Is.EqualTo(3300)); + Assert.That(received.State.CentralState, Is.EqualTo(0x02)); + Assert.That(received.State.OutputStates[0], Is.EqualTo(0x11)); + Assert.That(received.State.Address, Is.EqualTo(1)); + Assert.That(received.State.Address2, Is.EqualTo(2)); + Assert.That(received.State.Dimmed, Is.EqualTo(0x03)); + }); + } + + [Test] + public void Handle_SignalDecoderFrame_RaisesSignalState() + { + byte[] frame = SignalFrame(); + Assert.That(_handler.CanHandle(frame), Is.True); + + SignalDecoderSystemStateReceivedEventArgs? received = null; + _handler.OnSignalDecoderSystemStateReceived += (_, args) => received = args; + _handler.Handle(frame); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.State.Voltage, Is.EqualTo(12000)); + Assert.That(received.State.CentralState, Is.EqualTo(0x01)); + Assert.That(received.State.OutputStates[0], Is.EqualTo(0xAB)); + Assert.That(received.State.SignalDccExt[0], Is.EqualTo(0x10)); + Assert.That(received.State.SignalCount, Is.EqualTo(2)); + Assert.That(received.State.SignalConfig[0], Is.EqualTo(0x05)); + Assert.That(received.State.Address, Is.EqualTo(16)); + }); + } + + [Test] + public void CanHandle_RejectsOtherHeaders() + { + Assert.That(_handler.CanHandle([0x00]), Is.False); + byte[] wrongHeader = new byte[48]; + wrongHeader[2] = 0xDB; + Assert.That(_handler.CanHandle(wrongHeader), Is.False); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Driving/LocoInfoResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Driving/LocoInfoResponseHandlerTest.cs index 1b99441..a5f1691 100644 --- a/src/Z21.Client.UnitTest/Core/ResponseHandler/Driving/LocoInfoResponseHandlerTest.cs +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Driving/LocoInfoResponseHandlerTest.cs @@ -1,3 +1,4 @@ +using Z21.Core.Codecs; using Z21.Core.Model; using Z21.Core.Model.EventArgs; using Z21.Core.ResponseHandler.Driving; @@ -11,7 +12,7 @@ public class LocoInfoResponseHandlerTest [SetUp] public void Setup() { - _handler = new(); + _handler = new(new LocoSpeedCodec()); } [Test] @@ -34,7 +35,6 @@ public void CanHandle_InvalidResponse_ReturnsFalse(byte[] response) Assert.That(result, Is.False); } - //TODO add even more tests. [Test] [TestCase(3, DccSpeedMode.Steps28, @@ -45,6 +45,31 @@ public void CanHandle_InvalidResponse_ReturnsFalse(byte[] response) false, false, new byte[] { 0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x02, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69 })] + [TestCase(3, + DccSpeedMode.Steps14, + DecoderMode.DCC, + DrivingDirection.Forward, + (ushort)6, + false, + false, + false, + new byte[] { 0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x00, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69 }, + TestName = "14 speed steps (KKK=000)")] + [TestCase(3, DccSpeedMode.Steps14, DecoderMode.MM, DrivingDirection.Forward, (ushort)0, false, false, false, + new byte[] { 0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x10, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69 }, + TestName = "MM decoder (M bit set)")] + [TestCase(3, DccSpeedMode.Steps14, DecoderMode.DCC, DrivingDirection.Forward, (ushort)0, true, false, false, + new byte[] { 0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x08, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69 }, + TestName = "Busy (B bit set)")] + [TestCase(3, DccSpeedMode.Steps128, DecoderMode.DCC, DrivingDirection.Forward, (ushort)4, false, false, false, + new byte[] { 0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x04, 0x85, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69 }, + TestName = "128 speed steps (KKK=100)")] + [TestCase(3, DccSpeedMode.Steps28, DecoderMode.DCC, DrivingDirection.Backward, (ushort)0, false, false, false, + new byte[] { 0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69 }, + TestName = "Backward (R bit clear)")] + [TestCase(3, DccSpeedMode.Steps28, DecoderMode.DCC, DrivingDirection.Forward, (ushort)11, false, true, true, + new byte[] { 0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x02, 0x87, 0x60, 0x00, 0x00, 0x00, 0x00, 0x69 }, + TestName = "Double traction + smart search")] public void Handle_ValidResponse_RaisesEventWithCorrectArgs(short locoAddress, DccSpeedMode dccSpeedMode, DecoderMode decoderMode, DrivingDirection drivingDirection, ushort locoSpeed, bool locoIsBusy, bool locoContainedInDoubleTraction, bool smartSearch, byte[] response) { @@ -73,9 +98,41 @@ public void Handle_ValidResponse_RaisesEventWithCorrectArgs(short locoAddress, D Assert.That(receivedArgs.Data.LocoFunctionsData.All(data => data.FunctionToggleType == FunctionToggleType.Off), Is.True); List index = receivedArgs.Data.LocoFunctionsData.Select(data => data.FunctionIndex).Distinct().ToList(); - Assert.That(index, Has.Count.EqualTo(29)); + Assert.That(index, Has.Count.EqualTo(37)); Assert.That(index.Min(), Is.EqualTo(0)); - Assert.That(index.Max(), Is.EqualTo(28)); + Assert.That(index.Max(), Is.EqualTo(36)); + } + + [Test] + public void Handle_Db8FunctionBitsSet_ReportsF29ToF31On() + { + // 15-byte frame (DataLen 0x0F): DB8 (the byte immediately before the XOR) = 0x07 => F29, F30, F31 on. + byte[] response = [0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x02, 0x87, 0x00, 0x00, 0x00, 0x00, 0x07, 0x69]; + + LocoInfoReceivedEventArgs? receivedArgs = null; + _handler.OnLocoInfoReceived += (_, args) => receivedArgs = args; + + _handler.Handle(response); + + Assert.That(receivedArgs, Is.Not.Null); + var on = receivedArgs!.Data.LocoFunctionsData.Where(d => d.FunctionToggleType == FunctionToggleType.On).Select(d => (int)d.FunctionIndex).ToList(); + Assert.That(on, Is.EquivalentTo(new[] { 29, 30, 31 })); + } + + [Test] + public void Handle_FunctionBitsSet_ReportsThoseFunctionsOn() + { + // db4 = 0x1F => F0(L), F4, F3, F2, F1 all on; db5 = 0x01 => F5 on. + byte[] response = [0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x02, 0x87, 0x1F, 0x01, 0x00, 0x00, 0x00, 0x69]; + + LocoInfoReceivedEventArgs? receivedArgs = null; + _handler.OnLocoInfoReceived += (_, args) => receivedArgs = args; + + _handler.Handle(response); + + Assert.That(receivedArgs, Is.Not.Null); + var on = receivedArgs!.Data.LocoFunctionsData.Where(d => d.FunctionToggleType == FunctionToggleType.On).Select(d => (int)d.FunctionIndex).ToList(); + Assert.That(on, Is.EquivalentTo(new[] { 0, 1, 2, 3, 4, 5 })); } } } \ No newline at end of file diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/FastClock/FastClockResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/FastClock/FastClockResponseHandlerTest.cs new file mode 100644 index 0000000..d88fece --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/FastClock/FastClockResponseHandlerTest.cs @@ -0,0 +1,101 @@ +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.FastClock; + +namespace Z21.UnitTest.Core.ResponseHandler.FastClock +{ + public class FastClockResponseHandlerTest + { + [Test] + public void Data_DecodesTimeAndFlags() + { + FastClockDataResponseHandler handler = new(); + // day=0, hour=12 (0x0C), minute=30 (0x1E), second=45 (0x2D), rate=8, settings=0x80 + byte[] response = [0x0C, 0x00, 0xCD, 0x00, 0x66, 0x25, 0x0C, 0x1E, 0x2D, 0x08, 0x80, 0x00]; + + Assert.That(handler.CanHandle(response), Is.True); + + FastClockDataReceivedEventArgs? received = null; + handler.OnFastClockDataReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.Data.Day, Is.EqualTo(0)); + Assert.That(received.Data.Hour, Is.EqualTo(12)); + Assert.That(received.Data.Minute, Is.EqualTo(30)); + Assert.That(received.Data.Second, Is.EqualTo(45)); + Assert.That(received.Data.Rate, Is.EqualTo(8)); + Assert.That(received.Data.IsStopped, Is.False); + Assert.That(received.Data.IsHalted, Is.False); + Assert.That(received.Data.Settings, Is.EqualTo(FastClockSettings.Enabled)); + }); + } + + [Test] + public void Data_DayInHighBits_IsDecoded() + { + FastClockDataResponseHandler handler = new(); + // dayHour = 0x4C => day 2, hour 12 + byte[] response = [0x0C, 0x00, 0xCD, 0x00, 0x66, 0x25, 0x4C, 0x1E, 0x2D, 0x08, 0x80, 0x00]; + + FastClockDataReceivedEventArgs? received = null; + handler.OnFastClockDataReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.Multiple(() => + { + Assert.That(received!.Data.Day, Is.EqualTo(2)); + Assert.That(received.Data.Hour, Is.EqualTo(12)); + }); + } + + [Test] + public void Data_StopAndHaltFlags_AreDecoded() + { + FastClockDataResponseHandler handler = new(); + byte[] response = [0x0C, 0x00, 0xCD, 0x00, 0x66, 0x25, 0x0C, 0x1E, 0xC5, 0x08, 0x80, 0x00]; + + FastClockDataReceivedEventArgs? received = null; + handler.OnFastClockDataReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.Multiple(() => + { + Assert.That(received!.Data.Second, Is.EqualTo(5)); + Assert.That(received.Data.IsStopped, Is.True); + Assert.That(received.Data.IsHalted, Is.True); + }); + } + + [Test] + public void Data_RejectsOtherHeaders() + { + FastClockDataResponseHandler handler = new(); + Assert.That(handler.CanHandle([0x0C, 0x00, 0xCE, 0x00, 0x66, 0x25, 0x0C, 0x1E, 0x2D, 0x08, 0x80, 0x00]), Is.False); + Assert.That(handler.CanHandle([0x00]), Is.False); + } + + [Test] + public void Settings_DecodesFields() + { + FastClockSettingsResponseHandler handler = new(); + byte[] response = [0x08, 0x00, 0xCE, 0x00, 0x4F, 0x01, 0x0C, 0x1E]; + + Assert.That(handler.CanHandle(response), Is.True); + + FastClockSettingsReceivedEventArgs? received = null; + handler.OnFastClockSettingsReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.Multiple(() => + { + Assert.That(received!.Settings.Settings, Is.EqualTo((FastClockSettings)0x4F)); + Assert.That(received.Settings.Rate, Is.EqualTo(1)); + Assert.That(received.Settings.StartDayHour, Is.EqualTo(0x0C)); + Assert.That(received.Settings.StartMinute, Is.EqualTo(0x1E)); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Feedback/RmBusDataChangedResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Feedback/RmBusDataChangedResponseHandlerTest.cs new file mode 100644 index 0000000..66dc538 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Feedback/RmBusDataChangedResponseHandlerTest.cs @@ -0,0 +1,46 @@ +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.Feedback; + +namespace Z21.UnitTest.Core.ResponseHandler.Feedback +{ + public class RmBusDataChangedResponseHandlerTest + { + private RmBusDataChangedResponseHandler _handler = null!; + + [SetUp] + public void Setup() => _handler = new(); + + [Test] + public void CanHandle_ValidResponse_ReturnsTrue() + { + byte[] validResponse = [0x0F, 0x00, 0x80, 0x00, 0x01, 0x01, 0x00, 0xC5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + Assert.That(_handler.CanHandle(validResponse), Is.True); + } + + [Test] + [TestCase(new byte[] { 0x0F, 0x00, 0x81, 0x00, 0x01, 0x01, 0x00, 0xC5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }, TestName = "Wrong header")] + [TestCase(new byte[] { 0x00 }, TestName = "Response too small")] + public void CanHandle_InvalidResponse_ReturnsFalse(byte[] response) + { + Assert.That(_handler.CanHandle(response), Is.False); + } + + [Test] + public void Handle_ValidResponse_RaisesGroupIndexAndStates() + { + byte[] response = [0x0F, 0x00, 0x80, 0x00, 0x01, 0x01, 0x00, 0xC5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]; + + RmBusDataReceivedEventArgs? received = null; + _handler.OnRmBusDataReceived += (_, args) => received = args; + + _handler.Handle(response); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.GroupIndex, Is.EqualTo(1)); + Assert.That(received.FeedbackStates, Is.EqualTo(new byte[] { 0x01, 0x00, 0xC5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 })); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/LocoNet/LocoNetResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/LocoNet/LocoNetResponseHandlerTest.cs new file mode 100644 index 0000000..85ee440 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/LocoNet/LocoNetResponseHandlerTest.cs @@ -0,0 +1,101 @@ +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.LocoNet; + +namespace Z21.UnitTest.Core.ResponseHandler.LocoNet +{ + public class LocoNetResponseHandlerTest + { + [Test] + public void Receive_CanHandleAndExtractsMessage() + { + LocoNetReceiveResponseHandler handler = new(); + byte[] response = [0x07, 0x00, 0xA0, 0x00, 0xB0, 0x01, 0x60]; + + Assert.That(handler.CanHandle(response), Is.True); + + LocoNetMessageReceivedEventArgs? received = null; + handler.OnLocoNetMessageReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received, Is.Not.Null); + Assert.That(received!.Message, Is.EqualTo(new byte[] { 0xB0, 0x01, 0x60 })); + } + + [Test] + public void Receive_RejectsOtherHeaders() + { + LocoNetReceiveResponseHandler handler = new(); + Assert.That(handler.CanHandle([0x07, 0x00, 0xA1, 0x00, 0xB0, 0x01, 0x60]), Is.False); + Assert.That(handler.CanHandle([0x00]), Is.False); + } + + [Test] + public void Transmit_CanHandleAndExtractsMessage() + { + LocoNetTransmitResponseHandler handler = new(); + byte[] response = [0x07, 0x00, 0xA1, 0x00, 0xB0, 0x01, 0x60]; + + Assert.That(handler.CanHandle(response), Is.True); + + LocoNetMessageReceivedEventArgs? received = null; + handler.OnLocoNetMessageReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received!.Message, Is.EqualTo(new byte[] { 0xB0, 0x01, 0x60 })); + } + + [Test] + public void FromLan_CanHandleAndExtractsMessage() + { + LocoNetFromLanResponseHandler handler = new(); + byte[] response = [0x07, 0x00, 0xA2, 0x00, 0xB0, 0x01, 0x60]; + + Assert.That(handler.CanHandle(response), Is.True); + + LocoNetMessageReceivedEventArgs? received = null; + handler.OnLocoNetMessageReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.That(received!.Message, Is.EqualTo(new byte[] { 0xB0, 0x01, 0x60 })); + } + + [Test] + public void DispatchAddress_DecodesAddressAndSlot() + { + LocoNetDispatchAddressResponseHandler handler = new(); + byte[] response = [0x07, 0x00, 0xA3, 0x00, 0x03, 0x00, 0x0B]; + + Assert.That(handler.CanHandle(response), Is.True); + + LocoNetDispatchAddressReceivedEventArgs? received = null; + handler.OnLocoNetDispatchAddressReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.Multiple(() => + { + Assert.That(received!.LocoAddress, Is.EqualTo(3)); + Assert.That(received.Slot, Is.EqualTo(11)); + }); + } + + [Test] + public void Detector_DecodesTypeAddressAndInfo() + { + LocoNetDetectorResponseHandler handler = new(); + byte[] response = [0x08, 0x00, 0xA4, 0x00, 0x01, 0xF8, 0x03, 0x01]; + + Assert.That(handler.CanHandle(response), Is.True); + + LocoNetDetectorReceivedEventArgs? received = null; + handler.OnLocoNetDetectorReceived += (_, args) => received = args; + handler.Handle(response); + + Assert.Multiple(() => + { + Assert.That(received!.Type, Is.EqualTo(0x01)); + Assert.That(received.ReportAddress, Is.EqualTo(1016)); + Assert.That(received.Info, Is.EqualTo(new byte[] { 0x01 })); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/NewHandlerContractTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/NewHandlerContractTest.cs new file mode 100644 index 0000000..38eb3bf --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/NewHandlerContractTest.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using Z21.Core.Codecs; +using Z21.Core.ResponseHandler; +using Z21.Core.ResponseHandler.Booster; +using Z21.Core.ResponseHandler.Can; +using Z21.Core.ResponseHandler.Decoder; +using Z21.Core.ResponseHandler.FastClock; +using Z21.Core.ResponseHandler.Feedback; +using Z21.Core.ResponseHandler.LocoNet; +using Z21.Core.ResponseHandler.Programming; +using Z21.Core.ResponseHandler.RailCom; +using Z21.Core.ResponseHandler.ZLink; +using Z21.Core.ResponseParser; + +namespace Z21.UnitTest.Core.ResponseHandler +{ + public class NewHandlerContractTest + { + private static byte[] Frame(int length, byte header) + { + byte[] frame = new byte[length]; + frame[2] = header; + frame[3] = 0x00; + return frame; + } + + private static IEnumerable Handlers() + { + TestCaseData Case(string name, IZ21ResponseHandler handler, byte[] valid) => new TestCaseData(handler, valid).SetName(name); + + yield return Case("CvResult", new CvResultResponseHandler(new AddressCodec()), Set(Frame(6, 0x40), (4, 0x64), (5, 0x14))); + yield return Case("CvNack", new CvNackResponseHandler(), Set(Frame(6, 0x40), (4, 0x61), (5, 0x13))); + yield return Case("CvNackSc", new CvNackShortCircuitResponseHandler(), Set(Frame(6, 0x40), (4, 0x61), (5, 0x12))); + yield return Case("RmBus", new RmBusDataChangedResponseHandler(), Frame(15, 0x80)); + yield return Case("RailCom", new RailComDataChangedResponseHandler(new RailComDataParser()), Frame(17, 0x88)); + yield return Case("LocoNetRx", new LocoNetReceiveResponseHandler(), Frame(4, 0xA0)); + yield return Case("LocoNetTx", new LocoNetTransmitResponseHandler(), Frame(4, 0xA1)); + yield return Case("LocoNetFromLan", new LocoNetFromLanResponseHandler(), Frame(4, 0xA2)); + yield return Case("LocoNetDispatch", new LocoNetDispatchAddressResponseHandler(), Frame(7, 0xA3)); + yield return Case("LocoNetDetector", new LocoNetDetectorResponseHandler(), Frame(7, 0xA4)); + yield return Case("CanDetector", new CanDetectorResponseHandler(), Frame(14, 0xC4)); + yield return Case("CanDeviceDescription", new CanDeviceDescriptionResponseHandler(), Frame(22, 0xC8)); + yield return Case("CanBoosterState", new CanBoosterSystemStateResponseHandler(), Frame(14, 0xCA)); + yield return Case("FastClockData", new FastClockDataResponseHandler(), Frame(12, 0xCD)); + yield return Case("FastClockSettings", new FastClockSettingsResponseHandler(), Frame(8, 0xCE)); + yield return Case("BoosterDescription", new BoosterDescriptionResponseHandler(), Frame(36, 0xB8)); + yield return Case("BoosterState", new BoosterSystemStateResponseHandler(), Frame(28, 0xBA)); + yield return Case("DecoderDescription", new DecoderDescriptionResponseHandler(), Frame(36, 0xD8)); + yield return Case("DecoderState", new DecoderSystemStateResponseHandler(new SwitchDecoderSystemStateParser(), new SignalDecoderSystemStateParser()), Frame(48, 0xDA)); + yield return Case("ZLinkHwInfo", new ZLinkHardwareInfoResponseHandler(new ZLinkHardwareInfoParser()), Set(Frame(63, 0xE8), (4, 0x06))); + } + + private static byte[] Set(byte[] frame, params (int index, byte value)[] overrides) + { + foreach ((int index, byte value) in overrides) + frame[index] = value; + return frame; + } + + [TestCaseSource(nameof(Handlers))] + public void Handler_NameIsNotEmpty(IZ21ResponseHandler handler, byte[] valid) + { + Assert.That(handler.Name, Is.Not.Empty); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_ValidMinLengthFrame_ReturnsTrue(IZ21ResponseHandler handler, byte[] valid) + { + Assert.That(handler.CanHandle(valid), Is.True); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_TooShortFrame_ReturnsFalse(IZ21ResponseHandler handler, byte[] valid) + { + Assert.That(handler.CanHandle([0x00, 0x00]), Is.False); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_WrongHeaderByte_ReturnsFalse(IZ21ResponseHandler handler, byte[] valid) + { + byte[] wrong = (byte[])valid.Clone(); + wrong[2] ^= 0xFF; + Assert.That(handler.CanHandle(wrong), Is.False); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_WrongHeaderHighByte_ReturnsFalse(IZ21ResponseHandler handler, byte[] valid) + { + byte[] wrong = (byte[])valid.Clone(); + wrong[3] = 0x01; + Assert.That(handler.CanHandle(wrong), Is.False); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/PreExistingHandlerContractTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/PreExistingHandlerContractTest.cs new file mode 100644 index 0000000..a2436c6 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/PreExistingHandlerContractTest.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; +using Z21.Core.ResponseParser; +using Z21.Core.ResponseHandler; +using Z21.Core.ResponseHandler.SystemState; +using Z21.Core.ResponseHandler.SystemState.TrackPower; + +namespace Z21.UnitTest.Core.ResponseHandler +{ + public class PreExistingHandlerContractTest + { + private static IEnumerable Handlers() + { + TestCaseData Case(string name, IZ21ResponseHandler handler, byte[] valid, int xHeaderIndex) => + new TestCaseData(handler, valid, xHeaderIndex).SetName(name); + + yield return Case("TrackShort", new TrackShortResponseHandler(), new byte[] { 0x00, 0x00, 0x40, 0x00, 0x61, 0x08 }, 4); + yield return Case("TrackPowerOn", new TrackPowerOnResponseHandler(), new byte[] { 0x00, 0x00, 0x40, 0x00, 0x61, 0x01 }, 4); + yield return Case("TrackPowerOff", new TrackPowerOffResponseHandler(), new byte[] { 0x00, 0x00, 0x40, 0x00, 0x61, 0x00 }, 4); + yield return Case("ProgrammingMode", new ProgrammingModeResponseHandler(), new byte[] { 0x00, 0x00, 0x40, 0x00, 0x61, 0x02 }, 4); + yield return Case("Stopped", new StoppedResponseHandler(), new byte[] { 0x00, 0x00, 0x40, 0x00, 0x81, 0x00 }, 4); + yield return Case("UnknownCommand", new UnknownCommandResponseHandler(), new byte[] { 0x00, 0x00, 0x40, 0x00, 0x61, 0x82 }, 4); + yield return Case("Version", new VersionResponseHandler(), new byte[] { 0x00, 0x00, 0x40, 0x00, 0x63, 0x21 }, 4); + yield return Case("Firmware", new FirmwareVersionResponseHandler(), new byte[] { 0x00, 0x00, 0x40, 0x00, 0xF3, 0x0A, 0x00, 0x00, 0xF9 }, 4); + yield return Case("StatusChanged", new StatusChangedResponseHandler(new CentralStateResponseParser()), new byte[] { 0x00, 0x00, 0x40, 0x00, 0x62, 0x22, 0x00, 0x40 }, 4); + } + + [TestCaseSource(nameof(Handlers))] + public void Handler_NameIsNotEmpty(IZ21ResponseHandler handler, byte[] valid, int xHeaderIndex) + { + Assert.That(handler.Name, Is.Not.Empty); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_ValidFrame_ReturnsTrue(IZ21ResponseHandler handler, byte[] valid, int xHeaderIndex) + { + Assert.That(handler.CanHandle(valid), Is.True); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_TooShortFrame_ReturnsFalse(IZ21ResponseHandler handler, byte[] valid, int xHeaderIndex) + { + Assert.That(handler.CanHandle([0x00, 0x00]), Is.False); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_WrongLanHeader_ReturnsFalse(IZ21ResponseHandler handler, byte[] valid, int xHeaderIndex) + { + byte[] wrong = (byte[])valid.Clone(); + wrong[2] = 0x41; + Assert.That(handler.CanHandle(wrong), Is.False); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_WrongXHeader_ReturnsFalse(IZ21ResponseHandler handler, byte[] valid, int xHeaderIndex) + { + byte[] wrong = (byte[])valid.Clone(); + wrong[xHeaderIndex] ^= 0xFF; + Assert.That(handler.CanHandle(wrong), Is.False); + } + + [TestCaseSource(nameof(Handlers))] + public void CanHandle_WrongDb0_ReturnsFalse(IZ21ResponseHandler handler, byte[] valid, int xHeaderIndex) + { + byte[] wrong = (byte[])valid.Clone(); + wrong[xHeaderIndex + 1] ^= 0xFF; + Assert.That(handler.CanHandle(wrong), Is.False); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvNackResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvNackResponseHandlerTest.cs new file mode 100644 index 0000000..45a14c1 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvNackResponseHandlerTest.cs @@ -0,0 +1,40 @@ +using Z21.Core.ResponseHandler.Programming; + +namespace Z21.UnitTest.Core.ResponseHandler.Programming +{ + public class CvNackResponseHandlerTest + { + private CvNackResponseHandler _handler = null!; + + [SetUp] + public void Setup() => _handler = new(); + + [Test] + public void CanHandle_ValidResponse_ReturnsTrue() + { + byte[] validResponse = [0x07, 0x00, 0x40, 0x00, 0x61, 0x13, 0x72]; + Assert.That(_handler.CanHandle(validResponse), Is.True); + } + + [Test] + [TestCase(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x61, 0x12, 0x73 }, TestName = "Short circuit nack")] + [TestCase(new byte[] { 0x00 }, TestName = "Response too small")] + public void CanHandle_InvalidResponse_ReturnsFalse(byte[] response) + { + Assert.That(_handler.CanHandle(response), Is.False); + } + + [Test] + public void Handle_ValidResponse_RaisesEvent() + { + byte[] response = [0x07, 0x00, 0x40, 0x00, 0x61, 0x13, 0x72]; + + bool raised = false; + _handler.OnCvNackReceived += (_, _) => raised = true; + + _handler.Handle(response); + + Assert.That(raised, Is.True); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvNackShortCircuitResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvNackShortCircuitResponseHandlerTest.cs new file mode 100644 index 0000000..fdf6f17 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvNackShortCircuitResponseHandlerTest.cs @@ -0,0 +1,40 @@ +using Z21.Core.ResponseHandler.Programming; + +namespace Z21.UnitTest.Core.ResponseHandler.Programming +{ + public class CvNackShortCircuitResponseHandlerTest + { + private CvNackShortCircuitResponseHandler _handler = null!; + + [SetUp] + public void Setup() => _handler = new(); + + [Test] + public void CanHandle_ValidResponse_ReturnsTrue() + { + byte[] validResponse = [0x07, 0x00, 0x40, 0x00, 0x61, 0x12, 0x73]; + Assert.That(_handler.CanHandle(validResponse), Is.True); + } + + [Test] + [TestCase(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x61, 0x13, 0x72 }, TestName = "Plain nack")] + [TestCase(new byte[] { 0x00 }, TestName = "Response too small")] + public void CanHandle_InvalidResponse_ReturnsFalse(byte[] response) + { + Assert.That(_handler.CanHandle(response), Is.False); + } + + [Test] + public void Handle_ValidResponse_RaisesEvent() + { + byte[] response = [0x07, 0x00, 0x40, 0x00, 0x61, 0x12, 0x73]; + + bool raised = false; + _handler.OnCvNackShortCircuitReceived += (_, _) => raised = true; + + _handler.Handle(response); + + Assert.That(raised, Is.True); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvResultResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvResultResponseHandlerTest.cs new file mode 100644 index 0000000..c04bede --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Programming/CvResultResponseHandlerTest.cs @@ -0,0 +1,47 @@ +using Z21.Core.Codecs; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.Programming; + +namespace Z21.UnitTest.Core.ResponseHandler.Programming +{ + public class CvResultResponseHandlerTest + { + private CvResultResponseHandler _handler = null!; + + [SetUp] + public void Setup() => _handler = new(new AddressCodec()); + + [Test] + public void CanHandle_ValidResponse_ReturnsTrue() + { + byte[] validResponse = [0x0A, 0x00, 0x40, 0x00, 0x64, 0x14, 0x00, 0x1C, 0x05, 0x00]; + Assert.That(_handler.CanHandle(validResponse), Is.True); + } + + [Test] + [TestCase(new byte[] { 0x0A, 0x00, 0x40, 0x00, 0x64, 0x13, 0x00, 0x1C, 0x05, 0x00 }, TestName = "Wrong DB0")] + [TestCase(new byte[] { 0x00 }, TestName = "Response too small")] + public void CanHandle_InvalidResponse_ReturnsFalse(byte[] response) + { + Assert.That(_handler.CanHandle(response), Is.False); + } + + [Test] + public void Handle_ValidResponse_RaisesEventWithCvAndValue() + { + byte[] response = [0x0A, 0x00, 0x40, 0x00, 0x64, 0x14, 0x00, 0x1C, 0x05, 0x00]; + + CvResultReceivedEventArgs? received = null; + _handler.OnCvResultReceived += (_, args) => received = args; + + _handler.Handle(response); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.CvAddress, Is.EqualTo(28)); + Assert.That(received.Value, Is.EqualTo(5)); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/RailCom/RailComDataChangedResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/RailCom/RailComDataChangedResponseHandlerTest.cs new file mode 100644 index 0000000..959f334 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/RailCom/RailComDataChangedResponseHandlerTest.cs @@ -0,0 +1,48 @@ +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.RailCom; +using Z21.Core.ResponseParser; + +namespace Z21.UnitTest.Core.ResponseHandler.RailCom +{ + public class RailComDataChangedResponseHandlerTest + { + private RailComDataChangedResponseHandler _handler = null!; + + [SetUp] + public void Setup() => _handler = new(new RailComDataParser()); + + [Test] + public void CanHandle_ValidResponse_ReturnsTrue() + { + byte[] valid = [0x11, 0x00, 0x88, 0x00, 0x03, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x50, 0x0A, 0x00]; + Assert.That(_handler.CanHandle(valid), Is.True); + } + + [Test] + [TestCase(new byte[] { 0x11, 0x00, 0x80, 0x00, 0x03, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x50, 0x0A, 0x00 }, TestName = "Wrong header")] + [TestCase(new byte[] { 0x00 }, TestName = "Response too small")] + public void CanHandle_InvalidResponse_ReturnsFalse(byte[] response) + { + Assert.That(_handler.CanHandle(response), Is.False); + } + + [Test] + public void Handle_ValidResponse_RaisesParsedData() + { + byte[] response = [0x11, 0x00, 0x88, 0x00, 0x03, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x50, 0x0A, 0x00]; + + RailComDataReceivedEventArgs? received = null; + _handler.OnRailComDataReceived += (_, args) => received = args; + + _handler.Handle(response); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.Data.LocoAddress, Is.EqualTo(3)); + Assert.That(received.Data.Speed, Is.EqualTo(80)); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Switching/ExtAccessoryInfoResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Switching/ExtAccessoryInfoResponseHandlerTest.cs index b22fdb6..5ab8470 100644 --- a/src/Z21.Client.UnitTest/Core/ResponseHandler/Switching/ExtAccessoryInfoResponseHandlerTest.cs +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Switching/ExtAccessoryInfoResponseHandlerTest.cs @@ -1,3 +1,4 @@ +using Z21.Core.Codecs; using Z21.Core.Model; using Z21.Core.Model.EventArgs; using Z21.Core.ResponseHandler.Switching; @@ -11,7 +12,7 @@ public class ExtAccessoryInfoResponseHandlerTest [SetUp] public void Setup() { - _handler = new(); + _handler = new(new AddressCodec()); } [Test] @@ -53,7 +54,7 @@ public void Handle_ValidResponse_RaisesEventWithCorrectArgs(byte db2, byte db3, Assert.That(handler, Is.EqualTo(_handler)); Assert.That(receivedArgs, Is.Not.Null); - Assert.That(receivedArgs.AccessoryAddress, Is.EqualTo(48)); + Assert.That(receivedArgs.AccessoryAddress, Is.EqualTo(44)); Assert.That(receivedArgs.EncodedState, Is.EqualTo(db2)); Assert.That(receivedArgs.DataValid, Is.EqualTo(validData)); } diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/Switching/TurnoutInfoResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/Switching/TurnoutInfoResponseHandlerTest.cs index 547d5c4..83c0f45 100644 --- a/src/Z21.Client.UnitTest/Core/ResponseHandler/Switching/TurnoutInfoResponseHandlerTest.cs +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/Switching/TurnoutInfoResponseHandlerTest.cs @@ -1,3 +1,4 @@ +using Z21.Core.Codecs; using Z21.Core.Model; using Z21.Core.Model.EventArgs; using Z21.Core.ResponseHandler.Switching; @@ -11,7 +12,7 @@ public class TurnoutInfoResponseHandlerTest [SetUp] public void Setup() { - _handler = new(); + _handler = new(new AddressCodec()); } [Test] diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/SystemState/HardwareInfoResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/SystemState/HardwareInfoResponseHandlerTest.cs index 0593acf..8f9e852 100644 --- a/src/Z21.Client.UnitTest/Core/ResponseHandler/SystemState/HardwareInfoResponseHandlerTest.cs +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/SystemState/HardwareInfoResponseHandlerTest.cs @@ -55,6 +55,8 @@ public void Handle_ValidResponse_RaisesEventWithCorrectArgs() Assert.That(handler, Is.EqualTo(_handler)); Assert.That(receivedArgs, Is.Not.Null); Assert.That(receivedArgs.Z21HardwareType, Is.EqualTo(Z21HardwareType.z21Start)); + // FW Version bytes 43 01 00 00 => 0x0143 (BCD "1.43") per spec §2.20. + Assert.That(receivedArgs.FirmwareVersion, Is.EqualTo(0x0143)); } } } \ No newline at end of file diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/SystemState/SystemStateDataChangedResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/SystemState/SystemStateDataChangedResponseHandlerTest.cs index 7a11c21..9fabe99 100644 --- a/src/Z21.Client.UnitTest/Core/ResponseHandler/SystemState/SystemStateDataChangedResponseHandlerTest.cs +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/SystemState/SystemStateDataChangedResponseHandlerTest.cs @@ -51,7 +51,7 @@ public void Handle_ValidResponse_RaisesEventWithCorrectArgs() handler = sender as SystemStateDataChangedResponseHandler; }; - Z21.Core.Model.SystemState systemState = new() { CentralState = null!, CentralStateEx = null! }; + CommandStation.Model.SystemState systemState = new() { CentralState = null!, CentralStateEx = null! }; _systemStateResponseParserMock.Setup(parser => parser.Parse(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x00, 0x83, 0x45, 0x83, 0x45, 0x00, 0x00, 0x00, 0x7B })) .Returns(systemState) diff --git a/src/Z21.Client.UnitTest/Core/ResponseHandler/ZLink/ZLinkHardwareInfoResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/ResponseHandler/ZLink/ZLinkHardwareInfoResponseHandlerTest.cs new file mode 100644 index 0000000..18420b4 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseHandler/ZLink/ZLinkHardwareInfoResponseHandlerTest.cs @@ -0,0 +1,94 @@ +using System.Text; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.ZLink; +using Z21.Core.ResponseParser; + +namespace Z21.UnitTest.Core.ResponseHandler.ZLink +{ + public class ZLinkHardwareInfoResponseHandlerTest + { + private ZLinkHardwareInfoResponseHandler _handler = null!; + + [SetUp] + public void Setup() => _handler = new(new ZLinkHardwareInfoParser()); + + private static byte[] BuildFrame() + { + byte[] frame = new byte[63]; + frame[0] = 0x3F; + frame[2] = 0xE8; + frame[4] = 0x06; + frame[5] = 0x91; frame[6] = 0x01; // HwID 401 + frame[7] = 0x01; // major + frame[8] = 0x01; // minor + frame[9] = 0x91; frame[10] = 0x0C; // build 3217 + Encoding.Latin1.GetBytes("EC FA BC", 0, 8, frame, 11); // MAC + Encoding.Latin1.GetBytes("device", 0, 6, frame, 29); // Name + return frame; + } + + [Test] + public void CanHandle_ValidResponse_ReturnsTrue() + { + Assert.That(_handler.CanHandle(BuildFrame()), Is.True); + } + + [Test] + public void CanHandle_RejectsOtherFrames() + { + Assert.That(_handler.CanHandle([0x00]), Is.False); + byte[] wrong = new byte[63]; + wrong[2] = 0xE8; + wrong[4] = 0x07; + Assert.That(_handler.CanHandle(wrong), Is.False); + } + + [Test] + public void Handle_NameWithoutTerminator_KeepsFullLength() + { + byte[] frame = BuildFrame(); + for (int i = 29; i < 62; i++) + frame[i] = (byte)'X'; + + ZLinkHardwareInfoReceivedEventArgs? received = null; + _handler.OnZLinkHardwareInfoReceived += (_, args) => received = args; + _handler.Handle(frame); + + Assert.That(received!.Info.Name, Has.Length.EqualTo(33)); + } + + [Test] + public void Handle_NameStartingWithTerminator_IsEmpty() + { + byte[] frame = BuildFrame(); + for (int i = 29; i < 62; i++) + frame[i] = 0x00; + + ZLinkHardwareInfoReceivedEventArgs? received = null; + _handler.OnZLinkHardwareInfoReceived += (_, args) => received = args; + _handler.Handle(frame); + + Assert.That(received!.Info.Name, Is.Empty); + } + + [Test] + public void Handle_DecodesAllFields() + { + ZLinkHardwareInfoReceivedEventArgs? received = null; + _handler.OnZLinkHardwareInfoReceived += (_, args) => received = args; + + _handler.Handle(BuildFrame()); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.Info.HardwareId, Is.EqualTo(401)); + Assert.That(received.Info.FirmwareMajor, Is.EqualTo(1)); + Assert.That(received.Info.FirmwareMinor, Is.EqualTo(1)); + Assert.That(received.Info.FirmwareBuild, Is.EqualTo(3217)); + Assert.That(received.Info.MacAddress, Is.EqualTo("EC FA BC")); + Assert.That(received.Info.Name, Is.EqualTo("device")); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/ResponseParser/RailComDataParserTest.cs b/src/Z21.Client.UnitTest/Core/ResponseParser/RailComDataParserTest.cs new file mode 100644 index 0000000..a568fdc --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/ResponseParser/RailComDataParserTest.cs @@ -0,0 +1,31 @@ +using Z21.Core.Model; +using Z21.Core.ResponseParser; + +namespace Z21.UnitTest.Core.ResponseParser +{ + public class RailComDataParserTest + { + private RailComDataParser _parser = null!; + + [SetUp] + public void SetUp() => _parser = new(); + + [Test] + public void Parse_ReadsAllFields() + { + byte[] data = [0x03, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x05, 0x50, 0x0A, 0x00]; + + RailComData result = _parser.Parse(data); + + Assert.Multiple(() => + { + Assert.That(result.LocoAddress, Is.EqualTo(3)); + Assert.That(result.ReceiveCounter, Is.EqualTo(255u)); + Assert.That(result.ErrorCounter, Is.EqualTo(2)); + Assert.That(result.Options, Is.EqualTo(RailComOptions.Speed1 | RailComOptions.QoS)); + Assert.That(result.Speed, Is.EqualTo(80)); + Assert.That(result.QualityOfService, Is.EqualTo(10)); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Z21CommandStationTest.cs b/src/Z21.Client.UnitTest/Core/Z21CommandStationTest.cs new file mode 100644 index 0000000..e1f2f30 --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Z21CommandStationTest.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using CommandStation.Model; +using Moq; +using Z21.Core; +using Z21.Core.Codecs; +using Z21.Core.Command; +using Z21.Core.Command.Driving; +using Z21.Core.Command.FastClock; +using Z21.Core.Command.Feedback; +using Z21.Core.Command.Programming; +using Z21.Core.Command.Switching; +using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState.TrackPower; +using Z21.Core.Exception; +using Z21.Core.Framing; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler; +using Z21.Core.ResponseHandler.Driving; +using Z21.Core.ResponseHandler.FastClock; +using Z21.Core.ResponseHandler.Feedback; +using Z21.Core.ResponseHandler.Programming; +using Z21.Core.ResponseHandler.Switching; +using Z21.Core.ResponseHandler.SystemState; +using Z21.Core.ResponseHandler.SystemState.TrackPower; +using FastClockActionEnum = Z21.Core.Model.FastClockAction; + +namespace Z21.UnitTest.Core +{ + public class Z21CommandStationTest + { + private FakeTransport _transport = null!; + private IZ21CommandFactory _factory = null!; + private Mock _locoInfo = null!; + private Mock _turnoutInfo = null!; + private Mock _extAccessory = null!; + private Mock _systemState = null!; + private Mock _firmware = null!; + private Mock _statusChanged = null!; + private Mock _trackPowerOn = null!; + private Mock _trackPowerOff = null!; + private Mock _cvResult = null!; + private Mock _cvNack = null!; + private Mock _cvNackSc = null!; + private Mock _rmBus = null!; + private Mock _fastClock = null!; + private Z21CommandStation _station = null!; + + [SetUp] + public void SetUp() + { + _transport = new FakeTransport(); + _factory = new Z21CommandFactory(new Z21FrameBuilder(), new AddressCodec(), new LocoSpeedCodec()); + _locoInfo = new Mock(); + _turnoutInfo = new Mock(); + _extAccessory = new Mock(); + _systemState = new Mock(); + _firmware = new Mock(); + _statusChanged = new Mock(); + _trackPowerOn = new Mock(); + _trackPowerOff = new Mock(); + _cvResult = new Mock(); + _cvNack = new Mock(); + _cvNackSc = new Mock(); + _rmBus = new Mock(); + _fastClock = new Mock(); + + Z21ResponseHandler dispatcher = new(_transport, new Z21FrameReader(), new List()); + + _station = new Z21CommandStation( + _transport, + dispatcher, + _factory, + new Z21Options(), + _locoInfo.Object, + _turnoutInfo.Object, + _extAccessory.Object, + _systemState.Object, + _firmware.Object, + _statusChanged.Object, + _trackPowerOn.Object, + _trackPowerOff.Object, + _cvResult.Object, + _cvNack.Object, + _cvNackSc.Object, + _rmBus.Object, + _fastClock.Object); + } + + [TearDown] + public void TearDown() => _station.Dispose(); + + private static IEnumerable SendCases() + { + TestCaseData Case(string name, Func invoke, Func expected) => + new TestCaseData(invoke, expected).SetName(name); + + yield return Case("Drive", s => s.DriveAsync(3, DccSpeedMode.Steps128, DrivingDirection.Forward, 1), + f => f.Create(DccSpeedMode.Steps128, (ushort)3, DrivingDirection.Forward, (ushort)1)); + yield return Case("EmergencyStop", s => s.EmergencyStopAsync(3), f => f.Create((ushort)3)); + yield return Case("SetFunction", s => s.SetFunctionAsync(3, 1, FunctionToggleType.On), + f => f.Create((ushort)3, (ushort)1, FunctionToggleType.On)); + yield return Case("Purge", s => s.PurgeAsync(3), f => f.Create((ushort)3)); + yield return Case("RequestLocoInfo", s => s.RequestLocoInfoAsync(3), f => f.Create((ushort)3)); + yield return Case("SetTurnout", s => s.SetTurnoutAsync(3, AccessoryOutput.Output1, AccessoryState.Activate, true), + f => f.Create((ushort)3, AccessoryOutput.Output1, AccessoryState.Activate, true)); + yield return Case("SetExtAccessory", s => s.SetExtAccessoryAsync(1, 5), f => f.Create((ushort)1, (byte)5)); + yield return Case("RequestTurnoutInfo", s => s.RequestTurnoutInfoAsync(3), f => f.Create((ushort)3)); + yield return Case("RequestExtAccessoryInfo", s => s.RequestExtAccessoryInfoAsync(1), f => f.Create((ushort)1)); + yield return Case("TrackPowerOn", s => s.TrackPowerOnAsync(), f => f.Create()); + yield return Case("TrackPowerOff", s => s.TrackPowerOffAsync(), f => f.Create()); + yield return Case("EmergencyStopAll", s => s.EmergencyStopAllAsync(), f => f.Create()); + yield return Case("RequestSystemState", s => s.RequestSystemStateAsync(), f => f.Create()); + yield return Case("RequestFirmwareVersion", s => s.RequestFirmwareVersionAsync(), f => f.Create()); + yield return Case("RequestStatus", s => s.RequestStatusAsync(), f => f.Create()); + yield return Case("ReadCv", s => s.ReadCvAsync(28), f => f.Create((ushort)28)); + yield return Case("WriteCv", s => s.WriteCvAsync(28, 5), f => f.Create((ushort)28, (byte)5)); + yield return Case("RequestFeedback", s => s.RequestFeedbackAsync(1), f => f.Create((byte)1)); + yield return Case("RequestModelTime", s => s.RequestModelTimeAsync(), f => f.Create(FastClockActionEnum.Read)); + yield return Case("SetModelTime", s => s.SetModelTimeAsync(new ModelTime(0, 12, 30, 0, 8)), + f => f.Create(new ModelTime(0, 12, 30, 0, 8))); + yield return Case("StartModelTime", s => s.StartModelTimeAsync(), f => f.Create(FastClockActionEnum.Start)); + yield return Case("StopModelTime", s => s.StopModelTimeAsync(), f => f.Create(FastClockActionEnum.Stop)); + } + + [TestCaseSource(nameof(SendCases))] + public async Task SendMethods_SendExpectedDatagram(Func invoke, Func expected) + { + await _station.ConnectAsync(); + _transport.Sent.Clear(); + + await invoke(_station); + + Assert.That(_transport.Sent.Single(), Is.EqualTo(expected(_factory).Data)); + } + + [Test] + public void SendCommandsAsync_WhenNotConnected_ThrowsNotConnectedException() + { + Assert.ThrowsAsync(() => _station.DriveAsync(3, DccSpeedMode.Steps128, DrivingDirection.Forward, 1)); + Assert.That(_transport.IsConnected, Is.False, "send must not implicitly connect"); + } + + [Test] + public async Task ConnectionLost_StopsKeepAlive_NoSpuriousSends() + { + // Build a station with a fast keep-alive so the timer would fire well within the test window. + Z21ResponseHandler dispatcher = new(_transport, new Z21FrameReader(), new List()); + using Z21CommandStation station = new( + _transport, + dispatcher, + _factory, + new Z21Options { KeepAliveInterval = TimeSpan.FromMilliseconds(100) }, + _locoInfo.Object, _turnoutInfo.Object, _extAccessory.Object, _systemState.Object, + _firmware.Object, _statusChanged.Object, _trackPowerOn.Object, _trackPowerOff.Object, + _cvResult.Object, _cvNack.Object, _cvNackSc.Object, _rmBus.Object, _fastClock.Object); + + await station.ConnectAsync(); // arms the keep-alive timer + _transport.RaiseConnectionLost(); // socket-level loss must stop the keep-alive + _transport.SetConnected(true); // transport reconnects on its own; the station did not re-arm anything + _transport.Sent.Clear(); + + await Task.Delay(TimeSpan.FromMilliseconds(500)); // > 4 keep-alive intervals + + Assert.That(_transport.Sent, Is.Empty, "a lost connection must stop the keep-alive timer (no spurious keep-alive sends)"); + } + + [Test] + public async Task SendAfterDisconnect_ThrowsAndDoesNotReconnect() + { + await _station.ConnectAsync(); + await _station.DisconnectAsync(); + + Assert.ThrowsAsync(() => _station.TrackPowerOnAsync()); + Assert.That(_transport.IsConnected, Is.False, "disconnect must be authoritative"); + } + + [Test] + public async Task ConnectAsync_SendsLogonAndSetsConnected() + { + await _station.ConnectAsync(); + + Assert.That(_station.IsConnected, Is.True); + byte[] expected = _factory.Create(new Z21Options().BroadcastFlags).Data + .Concat(_factory.Create().Data).ToArray(); + Assert.That(_transport.Sent.Single(), Is.EqualTo(expected), "logon should send broadcast flags + firmware query in one packet"); + } + + [Test] + public void LocoInfoReceived_FromHandler_IsReRaisedWithSameData() + { + LocoInfoData data = new() + { + LocoAddress = 3, + LocoFunctionsData = new List(), + DccSpeedMode = DccSpeedMode.Steps128, + DecoderMode = DecoderMode.DCC, + DrivingDirection = DrivingDirection.Forward, + LocoSpeed = 1, + LocoIsBusy = false, + LocoContainedInDoubleTraction = false, + SmartSearch = false + }; + LocoInfoData? received = null; + _station.LocoInfoReceived += (_, d) => received = d; + + _locoInfo.Raise(handler => handler.OnLocoInfoReceived += null, new LocoInfoReceivedEventArgs(data)); + + Assert.That(received, Is.SameAs(data)); + } + + [Test] + public void TurnoutInfoReceived_FromHandler_IsReRaised() + { + TurnoutInfo? received = null; + _station.TurnoutInfoReceived += (_, info) => received = info; + + _turnoutInfo.Raise(h => h.OnTurnoutInfoReceived += null, new TurnoutInfoReceivedEventArgs(65, AccessoryOutput.Output1)); + + Assert.That(received, Is.EqualTo(new TurnoutInfo(65, AccessoryOutput.Output1))); + } + + [Test] + public void ExtAccessoryInfoReceived_FromHandler_IsReRaised() + { + ExtAccessoryInfo? received = null; + _station.ExtAccessoryInfoReceived += (_, info) => received = info; + + _extAccessory.Raise(h => h.OnExtAccessoryInfoReceived += null, new ExtAccessoryInfoReceivedEventArgs(1, 5, true)); + + Assert.That(received, Is.EqualTo(new ExtAccessoryInfo(1, 5, true))); + } + + [Test] + public void SystemStateReceived_FromHandler_IsReRaised() + { + SystemState state = new() { CentralState = new CentralState(), CentralStateEx = new CentralStateEx() }; + SystemState? received = null; + _station.SystemStateReceived += (_, s) => received = s; + + _systemState.Raise(h => h.OnSystemStateDataChangedReceived += null, _systemState.Object, new SystemStatusChangedReceivedEventArgs(state)); + + Assert.That(received, Is.SameAs(state)); + } + + [Test] + public void FirmwareVersionReceived_FromHandler_IsReRaised() + { + FirmwareVersion version = new(1, 42); + FirmwareVersion? received = null; + _station.FirmwareVersionReceived += (_, v) => received = v; + + _firmware.Raise(h => h.OnFirmwareVersionReceived += null, _firmware.Object, new FirmwareVersionReceivedEventArgs(version)); + + Assert.That(received, Is.SameAs(version)); + } + + [Test] + public void StatusChanged_FromHandler_IsReRaised() + { + CentralState? received = null; + _station.StatusChanged += (_, s) => received = s; + + _statusChanged.Raise(h => h.OnStatusChangedReceived += null, _statusChanged.Object, new StatusChangedReceivedEventArgs(new CentralState())); + + Assert.That(received, Is.Not.Null); + } + + [Test] + public void TrackPowerChanged_OnTrackPowerOn_RaisesTrue() + { + bool? state = null; + _station.TrackPowerChanged += (_, on) => state = on; + + _trackPowerOn.Raise(handler => handler.OnTrackPowerOnReceived += null, System.EventArgs.Empty); + + Assert.That(state, Is.True); + } + + [Test] + public void TrackPowerChanged_OnTrackPowerOff_RaisesFalse() + { + bool? state = null; + _station.TrackPowerChanged += (_, on) => state = on; + + _trackPowerOff.Raise(handler => handler.OnTrackPowerOffReceived += null, System.EventArgs.Empty); + + Assert.That(state, Is.False); + } + + [Test] + public async Task ReadCvAsync_SendsCvReadDatagram() + { + await _station.ConnectAsync(); + _transport.Sent.Clear(); + + await ((CommandStation.IProgrammingControl)_station).ReadCvAsync(28); + + byte[] expected = _factory.Create((ushort)28).Data; + Assert.That(_transport.Sent.Single(), Is.EqualTo(expected)); + } + + [Test] + public void CvReadCompleted_FromHandler_IsReRaisedAsCvValue() + { + CvValue? received = null; + ((CommandStation.IProgrammingControl)_station).CvReadCompleted += (_, value) => received = value; + + _cvResult.Raise(handler => handler.OnCvResultReceived += null, new CvResultReceivedEventArgs(28, 5)); + + Assert.That(received, Is.EqualTo(new CvValue(28, 5))); + } + + [Test] + public void CvProgrammingFailed_OnNack_RaisesNoAcknowledgement() + { + CvProgrammingError? error = null; + ((CommandStation.IProgrammingControl)_station).CvProgrammingFailed += (_, e) => error = e; + + _cvNack.Raise(handler => handler.OnCvNackReceived += null, System.EventArgs.Empty); + + Assert.That(error, Is.EqualTo(CvProgrammingError.NoAcknowledgement)); + } + + [Test] + public void CvProgrammingFailed_OnShortCircuit_RaisesShortCircuitError() + { + CvProgrammingError? error = null; + ((CommandStation.IProgrammingControl)_station).CvProgrammingFailed += (_, e) => error = e; + + _cvNackSc.Raise(handler => handler.OnCvNackShortCircuitReceived += null, System.EventArgs.Empty); + + Assert.That(error, Is.EqualTo(CvProgrammingError.ShortCircuit)); + } + + [Test] + public void FeedbackChanged_FromHandler_IsReRaised() + { + FeedbackData? received = null; + ((CommandStation.IFeedbackControl)_station).FeedbackChanged += (_, data) => received = data; + + _rmBus.Raise(h => h.OnRmBusDataReceived += null, new RmBusDataReceivedEventArgs(1, new byte[] { 0x05 })); + + Assert.Multiple(() => + { + Assert.That(received!.GroupIndex, Is.EqualTo(1)); + Assert.That(received.States, Is.EqualTo(new byte[] { 0x05 })); + }); + } + + [Test] + public void ModelTimeChanged_FromHandler_IsReRaised() + { + ModelTime? received = null; + ((CommandStation.IFastClockControl)_station).ModelTimeChanged += (_, time) => received = time; + + FastClockData data = new(0, 12, 30, 45, 8, false, false, FastClockSettings.Enabled); + _fastClock.Raise(h => h.OnFastClockDataReceived += null, new FastClockDataReceivedEventArgs(data)); + + Assert.Multiple(() => + { + Assert.That(received!.Hour, Is.EqualTo(12)); + Assert.That(received.Minute, Is.EqualTo(30)); + Assert.That(received.Second, Is.EqualTo(45)); + Assert.That(received.Rate, Is.EqualTo(8)); + }); + } + } +} diff --git a/src/Z21.Client.UnitTest/Core/Z21ResponseHandlerTest.cs b/src/Z21.Client.UnitTest/Core/Z21ResponseHandlerTest.cs new file mode 100644 index 0000000..4ebf40f --- /dev/null +++ b/src/Z21.Client.UnitTest/Core/Z21ResponseHandlerTest.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using Z21.Core; +using Z21.Core.Framing; +using Z21.Core.ResponseHandler; + +namespace Z21.UnitTest.Core +{ + public class Z21ResponseHandlerTest + { + private sealed class RecordingHandler : IZ21ResponseHandler + { + private readonly bool _canHandle; + private readonly bool _throwOnHandle; + private readonly bool _throwOnCanHandle; + + public RecordingHandler(bool canHandle, bool throwOnHandle = false, bool throwOnCanHandle = false) + { + _canHandle = canHandle; + _throwOnHandle = throwOnHandle; + _throwOnCanHandle = throwOnCanHandle; + } + + public List Handled { get; } = []; + + public string Name => "RECORDING"; + + public bool CanHandle(byte[] response) + { + if (_throwOnCanHandle) + throw new System.InvalidOperationException("boom in CanHandle"); + return _canHandle; + } + + public void Handle(byte[] response) + { + Handled.Add(response); + if (_throwOnHandle) + throw new System.InvalidOperationException("boom"); + } + } + + [Test] + public void IncomingBytes_AreFramedAndDispatchedToCapableHandlers() + { + FakeTransport transport = new(); + RecordingHandler capable = new(canHandle: true); + RecordingHandler incapable = new(canHandle: false); + _ = new Z21ResponseHandler(transport, new Z21FrameReader(), new List { capable, incapable }); + + transport.RaiseBytes([0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00]); + + Assert.Multiple(() => + { + Assert.That(capable.Handled, Has.Count.EqualTo(1), "capable handler must receive the frame"); + Assert.That(capable.Handled[0], Is.EqualTo(new byte[] { 0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00 })); + Assert.That(incapable.Handled, Is.Empty, "incapable handler must be skipped"); + }); + } + + [Test] + public void HandlerThrowing_DoesNotPropagateAndOtherHandlersStillRun() + { + FakeTransport transport = new(); + RecordingHandler throwing = new(canHandle: true, throwOnHandle: true); + RecordingHandler second = new(canHandle: true); + _ = new Z21ResponseHandler(transport, new Z21FrameReader(), new List { throwing, second }); + + Assert.DoesNotThrow(() => transport.RaiseBytes([0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00])); + Assert.That(second.Handled, Has.Count.EqualTo(1), "a throwing handler must not stop the others"); + } + + [Test] + public void CanHandleThrowing_DoesNotPropagateAndOtherHandlersStillRun() + { + FakeTransport transport = new(); + RecordingHandler throwing = new(canHandle: true, throwOnCanHandle: true); + RecordingHandler second = new(canHandle: true); + _ = new Z21ResponseHandler(transport, new Z21FrameReader(), new List { throwing, second }); + + Assert.DoesNotThrow(() => transport.RaiseBytes([0x07, 0x00, 0x40, 0x00, 0x21, 0x21, 0x00])); + Assert.That(second.Handled, Has.Count.EqualTo(1), "a handler whose CanHandle throws must not stop the others"); + } + } +} diff --git a/src/Z21.Client.UnitTest/GlobalUsings.cs b/src/Z21.Client.UnitTest/GlobalUsings.cs new file mode 100644 index 0000000..bffbd65 --- /dev/null +++ b/src/Z21.Client.UnitTest/GlobalUsings.cs @@ -0,0 +1 @@ +global using CommandStation.Model; diff --git a/src/Z21.Client.UnitTest/stryker-config.json b/src/Z21.Client.UnitTest/stryker-config.json new file mode 100644 index 0000000..ab726e9 --- /dev/null +++ b/src/Z21.Client.UnitTest/stryker-config.json @@ -0,0 +1,16 @@ +{ + "stryker-config": { + "project": "Z21.Client.csproj", + "mutation-level": "Complete", + "coverage-analysis": "perTest", + "thresholds": { + "high": 98, + "low": 90, + "break": 85 + }, + "reporters": [ + "html", + "progress" + ] + } +} diff --git a/src/Z21.Client/Core/Codecs/AddressCodec.cs b/src/Z21.Client/Core/Codecs/AddressCodec.cs new file mode 100644 index 0000000..f9e6524 --- /dev/null +++ b/src/Z21.Client/Core/Codecs/AddressCodec.cs @@ -0,0 +1,78 @@ +using System; + +namespace Z21.Core.Codecs +{ + public class AddressCodec : IAddressCodec + { + public (byte lsb, byte msb) SplitLocoAddress(ushort address) + { + byte lsb = (byte)(address & 0xFF); + byte msb = (byte)((address >> 8) & 0xFF); + + if (address >= 128) + msb |= 0xC0; + + return (lsb, msb); + } + + public (byte msb, byte lsb) SplitAddressBigEndian(ushort address) + { + byte msb = (byte)((address >> 8) & 0xFF); + byte lsb = (byte)(address & 0xFF); + return (msb, lsb); + } + + public (byte lsb, byte msb) SplitAccessoryAddress(ushort address) + { + if (address < 1) + throw new ArgumentOutOfRangeException(nameof(address), address, "Smallest address is 1"); + + ushort dccAddress = (ushort)(address - 1); + byte msb = (byte)((dccAddress >> 8) & 0xFF); + byte lsb = (byte)(dccAddress & 0xFF); + return (lsb, msb); + } + + public ushort CombineAccessoryAddress(byte lsb, byte msb) + { + return (ushort)((msb << 8) + lsb + 1); + } + + public (byte lsb, byte msb) SplitExtAccessoryAddress(ushort address) + { + if (address < 1) + throw new ArgumentOutOfRangeException(nameof(address), address, "Smallest address is 1"); + + ushort rawAddress = (ushort)(address + 3); + byte msb = (byte)((rawAddress >> 8) & 0xFF); + byte lsb = (byte)(rawAddress & 0xFF); + return (lsb, msb); + } + + public ushort CombineExtAccessoryAddress(byte lsb, byte msb) + { + return (ushort)((msb << 8) + lsb - 3); + } + + public (byte msb, byte lsb) SplitCvAddress(ushort cvAddress) + { + byte msb = (byte)((cvAddress >> 8) & 0xFF); + byte lsb = (byte)(cvAddress & 0xFF); + return (msb, lsb); + } + + public ushort CombineCvAddress(byte msb, byte lsb) + { + return (ushort)((msb << 8) + lsb); + } + + public (byte db1, byte db2) EncodeAccessoryPomAddress(ushort decoderAddress, bool wholeDecoder, byte output) + { + int cddd = wholeDecoder ? 0x00 : (0x08 | (output & 0x07)); + int value = ((decoderAddress & 0x1FF) << 4) | cddd; + byte db1 = (byte)((value >> 8) & 0xFF); + byte db2 = (byte)(value & 0xFF); + return (db1, db2); + } + } +} diff --git a/src/Z21.Client/Core/Codecs/IAddressCodec.cs b/src/Z21.Client/Core/Codecs/IAddressCodec.cs new file mode 100644 index 0000000..35918d7 --- /dev/null +++ b/src/Z21.Client/Core/Codecs/IAddressCodec.cs @@ -0,0 +1,51 @@ +using System; + +namespace Z21.Core.Codecs +{ + /// + /// Encodes and decodes locomotive and accessory addresses in the Z21 wire representation. + /// + public interface IAddressCodec + { + (byte lsb, byte msb) SplitLocoAddress(ushort address); + + /// + /// Splits an address into its big-endian wire bytes (most-significant byte first), as used by the + /// LAN_GET/SET_LOCOMODE and LAN_GET/SET_TURNOUTMODE settings commands. + /// + (byte msb, byte lsb) SplitAddressBigEndian(ushort address); + + /// Thrown when is smaller than 1. + (byte lsb, byte msb) SplitAccessoryAddress(ushort address); + + ushort CombineAccessoryAddress(byte lsb, byte msb); + + /// + /// Maps a user-facing extended accessory address (1-based) to its RCN-213 RawAddress wire bytes (user address 1 = RawAddress 4). + /// + /// Thrown when is smaller than 1. + (byte lsb, byte msb) SplitExtAccessoryAddress(ushort address); + + /// + /// Maps the RCN-213 RawAddress wire bytes of an extended accessory decoder back to the user-facing address (RawAddress 4 = user address 1). + /// + ushort CombineExtAccessoryAddress(byte lsb, byte msb); + + /// + /// Splits a CV address (0 = CV1) into its high and low wire bytes (no offset applied). + /// + (byte msb, byte lsb) SplitCvAddress(ushort cvAddress); + + /// + /// Combines the high and low wire bytes of a CV address back into a CV address (0 = CV1). + /// + ushort CombineCvAddress(byte msb, byte lsb); + + /// + /// Encodes an accessory decoder address for POM commands into the two wire bytes + /// aaaaa / AAAACDDD. When is true the CV refers to the + /// whole decoder (CDDD = 0000); otherwise C = 1 and DDD = . + /// + (byte db1, byte db2) EncodeAccessoryPomAddress(ushort decoderAddress, bool wholeDecoder, byte output); + } +} diff --git a/src/Z21.Client/Core/Codecs/ILocoSpeedCodec.cs b/src/Z21.Client/Core/Codecs/ILocoSpeedCodec.cs new file mode 100644 index 0000000..a2d2f26 --- /dev/null +++ b/src/Z21.Client/Core/Codecs/ILocoSpeedCodec.cs @@ -0,0 +1,14 @@ +using Z21.Core.Model; + +namespace Z21.Core.Codecs +{ + /// + /// Converts between user-facing DCC speed steps and the speed bytes used on the Z21 wire. + /// + public interface ILocoSpeedCodec + { + ushort CalculateDccSpeed(DccSpeedMode dccSpeedMode, ushort speedStep); + + ushort CalculateSpeedStep(DccSpeedMode dccSpeedMode, ushort dccSpeed); + } +} diff --git a/src/Z21.Client/Core/Codecs/LocoSpeedCodec.cs b/src/Z21.Client/Core/Codecs/LocoSpeedCodec.cs new file mode 100644 index 0000000..f4828c8 --- /dev/null +++ b/src/Z21.Client/Core/Codecs/LocoSpeedCodec.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using Z21.Core.Model; + +namespace Z21.Core.Codecs +{ + public class LocoSpeedCodec : ILocoSpeedCodec + { + private readonly Dictionary _dcc28SpeedStepLookup = new() + { + { 0, 0 }, { 16, 0 }, + { 1, 0 }, { 17, 0 }, + { 2, 1 }, { 18, 2 }, + { 3, 3 }, { 19, 4 }, + { 4, 5 }, { 20, 6 }, + { 5, 7 }, { 21, 8 }, + { 6, 9 }, { 22, 10 }, + { 7, 11 }, { 23, 12 }, + { 8, 13 }, { 24, 14 }, + { 9, 15 }, { 25, 16 }, + { 10, 17 }, { 26, 18 }, + { 11, 19 }, { 27, 20 }, + { 12, 21 }, { 28, 22 }, + { 13, 23 }, { 29, 24 }, + { 14, 25 }, { 30, 26 }, + { 15, 27 }, { 31, 28 } + }; + + public ushort CalculateDccSpeed(DccSpeedMode dccSpeedMode, ushort speedStep) => dccSpeedMode switch + { + DccSpeedMode.Steps14 when speedStep > 0 => (ushort)(speedStep + 1), + DccSpeedMode.Steps28 when speedStep > 0 => CalculateDcc28DccSpeed(speedStep + 3), + DccSpeedMode.Steps128 when speedStep > 0 => (ushort)(speedStep + 1), + _ => speedStep + }; + + public ushort CalculateSpeedStep(DccSpeedMode dccSpeedMode, ushort dccSpeed) => dccSpeedMode switch + { + DccSpeedMode.Steps14 when dccSpeed > 1 => (ushort)(dccSpeed - 1), + DccSpeedMode.Steps28 when dccSpeed > 0 => DecodeDcc28SpeedStep(dccSpeed), + DccSpeedMode.Steps128 when dccSpeed > 1 => (ushort)(dccSpeed - 1), + _ => 0 + }; + + private ushort DecodeDcc28SpeedStep(ushort dccSpeed) => + _dcc28SpeedStepLookup.TryGetValue(dccSpeed, out ushort speedStep) ? speedStep : (ushort)0; + + private ushort CalculateDcc28DccSpeed(int speedStep) + { + double dcc14Speed = speedStep / 2.0; + int dccSpeed = (int)Math.Floor(dcc14Speed); + + if (dcc14Speed % 1 != 0) + dccSpeed |= 0x10; + return (ushort)dccSpeed; + } + } +} diff --git a/src/Z21.Client/Core/Command/Booster/GetBoosterDescriptionCommand.cs b/src/Z21.Client/Core/Command/Booster/GetBoosterDescriptionCommand.cs new file mode 100644 index 0000000..9a4d3cf --- /dev/null +++ b/src/Z21.Client/Core/Command/Booster/GetBoosterDescriptionCommand.cs @@ -0,0 +1,19 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Booster +{ + /// + /// Reads the description of a zLink booster (LAN_BOOSTER_GET_DESCRIPTION, protocol §11.2.1). + /// + public class GetBoosterDescriptionCommand : IZ21Command + { + public GetBoosterDescriptionCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x00B8); + } + + public string Name => "LAN_BOOSTER_GET_DESCRIPTION"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Booster/GetBoosterSystemStateCommand.cs b/src/Z21.Client/Core/Command/Booster/GetBoosterSystemStateCommand.cs new file mode 100644 index 0000000..ecdb043 --- /dev/null +++ b/src/Z21.Client/Core/Command/Booster/GetBoosterSystemStateCommand.cs @@ -0,0 +1,19 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Booster +{ + /// + /// Requests the system state of a zLink booster (LAN_BOOSTER_SYSTEMSTATE_GETDATA, protocol §11.2.3). + /// + public class GetBoosterSystemStateCommand : IZ21Command + { + public GetBoosterSystemStateCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x00BB); + } + + public string Name => "LAN_BOOSTER_SYSTEMSTATE_GETDATA"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Booster/SetBoosterDescriptionCommand.cs b/src/Z21.Client/Core/Command/Booster/SetBoosterDescriptionCommand.cs new file mode 100644 index 0000000..1909696 --- /dev/null +++ b/src/Z21.Client/Core/Command/Booster/SetBoosterDescriptionCommand.cs @@ -0,0 +1,33 @@ +using System; +using System.Text; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Booster +{ + /// + /// Overwrites the description of a zLink booster (LAN_BOOSTER_SET_DESCRIPTION, protocol §11.2.2). + /// The name is ISO 8859-1, truncated/padded to 32 bytes; the characters " and \ are not allowed. + /// + public class SetBoosterDescriptionCommand : IZ21Command + { + private const int NameLength = 32; + + /// Thrown when contains a forbidden character. + public SetBoosterDescriptionCommand(IZ21FrameBuilder frameBuilder, string name) + { + ArgumentNullException.ThrowIfNull(name); + if (name.Contains('"') || name.Contains('\\')) + throw new ArgumentException("The characters '\"' and '\\' are not allowed in a booster description.", nameof(name)); + + byte[] nameBuffer = new byte[NameLength]; + byte[] encoded = Encoding.Latin1.GetBytes(name); + Array.Copy(encoded, 0, nameBuffer, 0, Math.Min(encoded.Length, NameLength)); + + Data = frameBuilder.BuildLan(0x00B9, nameBuffer); + } + + public string Name => "LAN_BOOSTER_SET_DESCRIPTION"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Booster/SetBoosterPowerCommand.cs b/src/Z21.Client/Core/Command/Booster/SetBoosterPowerCommand.cs new file mode 100644 index 0000000..c987ed8 --- /dev/null +++ b/src/Z21.Client/Core/Command/Booster/SetBoosterPowerCommand.cs @@ -0,0 +1,21 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Booster +{ + /// + /// From booster FW V1.11, disables/re-enables a zLink booster output (LAN_BOOSTER_SET_POWER, + /// protocol §11.2.5). Port 0x01 = first output, 0x02 = second (dual only), 0x03 = all; state 0x00 = off, + /// 0x01 = on. + /// + public class SetBoosterPowerCommand : IZ21Command + { + public SetBoosterPowerCommand(IZ21FrameBuilder frameBuilder, byte port, byte state) + { + Data = frameBuilder.BuildLan(0x00B2, port, state); + } + + public string Name => "LAN_BOOSTER_SET_POWER"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Can/GetCanDetectorCommand.cs b/src/Z21.Client/Core/Command/Can/GetCanDetectorCommand.cs new file mode 100644 index 0000000..63ce29e --- /dev/null +++ b/src/Z21.Client/Core/Command/Can/GetCanDetectorCommand.cs @@ -0,0 +1,22 @@ +using System; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Can +{ + /// + /// From Z21 FW version 1.30, queries a CAN occupancy detector by its CAN network id + /// (LAN_CAN_DETECTOR, protocol §10.1). Network id 0xD000 queries all CAN detectors. + /// + public class GetCanDetectorCommand : IZ21Command + { + public GetCanDetectorCommand(IZ21FrameBuilder frameBuilder, ushort networkId) + { + byte[] nid = BitConverter.GetBytes(networkId); + Data = frameBuilder.BuildLan(0x00C4, 0x00, nid[0], nid[1]); + } + + public string Name => "LAN_CAN_DETECTOR"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Can/GetCanDeviceDescriptionCommand.cs b/src/Z21.Client/Core/Command/Can/GetCanDeviceDescriptionCommand.cs new file mode 100644 index 0000000..a536d95 --- /dev/null +++ b/src/Z21.Client/Core/Command/Can/GetCanDeviceDescriptionCommand.cs @@ -0,0 +1,22 @@ +using System; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Can +{ + /// + /// From Z21 FW version 1.41, reads the free-text description of a CAN booster + /// (LAN_CAN_DEVICE_GET_DESCRIPTION, protocol §10.2.1). + /// + public class GetCanDeviceDescriptionCommand : IZ21Command + { + public GetCanDeviceDescriptionCommand(IZ21FrameBuilder frameBuilder, ushort networkId) + { + byte[] nid = BitConverter.GetBytes(networkId); + Data = frameBuilder.BuildLan(0x00C8, nid[0], nid[1]); + } + + public string Name => "LAN_CAN_DEVICE_GET_DESCRIPTION"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Can/SetCanBoosterTrackPowerCommand.cs b/src/Z21.Client/Core/Command/Can/SetCanBoosterTrackPowerCommand.cs new file mode 100644 index 0000000..7d187dc --- /dev/null +++ b/src/Z21.Client/Core/Command/Can/SetCanBoosterTrackPowerCommand.cs @@ -0,0 +1,22 @@ +using System; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Can +{ + /// + /// From Z21 FW version 1.41, disables/re-enables the track outputs of a CAN booster + /// (LAN_CAN_BOOSTER_SET_TRACKPOWER, protocol §10.2.4). 0x00 disables all outputs, 0xFF re-enables. + /// + public class SetCanBoosterTrackPowerCommand : IZ21Command + { + public SetCanBoosterTrackPowerCommand(IZ21FrameBuilder frameBuilder, ushort networkId, byte power) + { + byte[] nid = BitConverter.GetBytes(networkId); + Data = frameBuilder.BuildLan(0x00CB, nid[0], nid[1], power); + } + + public string Name => "LAN_CAN_BOOSTER_SET_TRACKPOWER"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Can/SetCanDeviceDescriptionCommand.cs b/src/Z21.Client/Core/Command/Can/SetCanDeviceDescriptionCommand.cs new file mode 100644 index 0000000..7a50c54 --- /dev/null +++ b/src/Z21.Client/Core/Command/Can/SetCanDeviceDescriptionCommand.cs @@ -0,0 +1,40 @@ +using System; +using System.Text; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Can +{ + /// + /// From Z21 FW version 1.41, overwrites the free-text description of a CAN booster + /// (LAN_CAN_DEVICE_SET_DESCRIPTION, protocol §10.2.2). The name is ISO 8859-1, truncated to and + /// padded to 16 bytes; the characters " and \ are not allowed. + /// + public class SetCanDeviceDescriptionCommand : IZ21Command + { + private const int NameLength = 16; + + /// Thrown when contains a forbidden character. + public SetCanDeviceDescriptionCommand(IZ21FrameBuilder frameBuilder, ushort networkId, string name) + { + ArgumentNullException.ThrowIfNull(name); + if (name.Contains('"') || name.Contains('\\')) + throw new ArgumentException("The characters '\"' and '\\' are not allowed in a device description.", nameof(name)); + + byte[] nameBuffer = new byte[NameLength]; + byte[] encoded = Encoding.Latin1.GetBytes(name); + Array.Copy(encoded, 0, nameBuffer, 0, Math.Min(encoded.Length, NameLength)); + + byte[] nid = BitConverter.GetBytes(networkId); + byte[] payload = new byte[2 + NameLength]; + payload[0] = nid[0]; + payload[1] = nid[1]; + Array.Copy(nameBuffer, 0, payload, 2, NameLength); + + Data = frameBuilder.BuildLan(0x00C9, payload); + } + + public string Name => "LAN_CAN_DEVICE_SET_DESCRIPTION"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Decoder/GetDecoderDescriptionCommand.cs b/src/Z21.Client/Core/Command/Decoder/GetDecoderDescriptionCommand.cs new file mode 100644 index 0000000..9265695 --- /dev/null +++ b/src/Z21.Client/Core/Command/Decoder/GetDecoderDescriptionCommand.cs @@ -0,0 +1,19 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Decoder +{ + /// + /// Reads the description of a zLink decoder (LAN_DECODER_GET_DESCRIPTION, protocol §11.3.1). + /// + public class GetDecoderDescriptionCommand : IZ21Command + { + public GetDecoderDescriptionCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x00D8); + } + + public string Name => "LAN_DECODER_GET_DESCRIPTION"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Decoder/GetDecoderSystemStateCommand.cs b/src/Z21.Client/Core/Command/Decoder/GetDecoderSystemStateCommand.cs new file mode 100644 index 0000000..81a71bd --- /dev/null +++ b/src/Z21.Client/Core/Command/Decoder/GetDecoderSystemStateCommand.cs @@ -0,0 +1,19 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Decoder +{ + /// + /// Requests the system state of a zLink decoder (LAN_DECODER_SYSTEMSTATE_GETDATA, protocol §11.3.3). + /// + public class GetDecoderSystemStateCommand : IZ21Command + { + public GetDecoderSystemStateCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x00DB); + } + + public string Name => "LAN_DECODER_SYSTEMSTATE_GETDATA"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Decoder/SetDecoderDescriptionCommand.cs b/src/Z21.Client/Core/Command/Decoder/SetDecoderDescriptionCommand.cs new file mode 100644 index 0000000..1fa1e91 --- /dev/null +++ b/src/Z21.Client/Core/Command/Decoder/SetDecoderDescriptionCommand.cs @@ -0,0 +1,33 @@ +using System; +using System.Text; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Decoder +{ + /// + /// Overwrites the description of a zLink decoder (LAN_DECODER_SET_DESCRIPTION, protocol §11.3.2). + /// The name is ISO 8859-1, truncated/padded to 32 bytes; the characters " and \ are not allowed. + /// + public class SetDecoderDescriptionCommand : IZ21Command + { + private const int NameLength = 32; + + /// Thrown when contains a forbidden character. + public SetDecoderDescriptionCommand(IZ21FrameBuilder frameBuilder, string name) + { + ArgumentNullException.ThrowIfNull(name); + if (name.Contains('"') || name.Contains('\\')) + throw new ArgumentException("The characters '\"' and '\\' are not allowed in a decoder description.", nameof(name)); + + byte[] nameBuffer = new byte[NameLength]; + byte[] encoded = Encoding.Latin1.GetBytes(name); + Array.Copy(encoded, 0, nameBuffer, 0, Math.Min(encoded.Length, NameLength)); + + Data = frameBuilder.BuildLan(0x00D9, nameBuffer); + } + + public string Name => "LAN_DECODER_SET_DESCRIPTION"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Driving/GetLocoInfoCommand.cs b/src/Z21.Client/Core/Command/Driving/GetLocoInfoCommand.cs index e5ad71c..f328b02 100644 --- a/src/Z21.Client/Core/Command/Driving/GetLocoInfoCommand.cs +++ b/src/Z21.Client/Core/Command/Driving/GetLocoInfoCommand.cs @@ -1,5 +1,6 @@ +using Z21.Core.Codecs; using Z21.Core.Command.SystemState; -using Z21.Core.Helper; +using Z21.Core.Framing; using Z21.Core.Model; namespace Z21.Core.Command.Driving @@ -9,27 +10,14 @@ namespace Z21.Core.Command.Driving /// public class GetLocoInfoCommand : IZ21Command { - public GetLocoInfoCommand(ushort locoAddress) + public GetLocoInfoCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress) { - const byte xHeader = 0xE3; - const byte db0 = 0xF0; - - (byte lsb, byte msb) = AddressHelper.SplitLocoAddress(locoAddress); - byte xor = (byte)(xHeader ^ db0 ^ lsb ^ msb); - Data = - [ - 0x09, 0x00, - 0x40, 0x00, - xHeader, - db0, - msb, - lsb, - xor - ]; + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + Data = frameBuilder.BuildXBus(0xE3, 0xF0, msb, lsb); } public string Name => "LAN_X_GET_LOCO_INFO"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Driving/PurgeLocoCommand.cs b/src/Z21.Client/Core/Command/Driving/PurgeLocoCommand.cs index 4e3a28f..f3dcd6a 100644 --- a/src/Z21.Client/Core/Command/Driving/PurgeLocoCommand.cs +++ b/src/Z21.Client/Core/Command/Driving/PurgeLocoCommand.cs @@ -1,4 +1,5 @@ -using Z21.Core.Helper; +using Z21.Core.Codecs; +using Z21.Core.Framing; namespace Z21.Core.Command.Driving { @@ -9,23 +10,14 @@ namespace Z21.Core.Command.Driving /// public class PurgeLocoCommand : IZ21Command { - public PurgeLocoCommand(ushort locoAddress) + public PurgeLocoCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress) { - (byte lsb, byte msb) = AddressHelper.SplitLocoAddress(locoAddress); - Data = - [ - 0x09, 0x00, - 0x40, 0x00, - 0xE3, - 0x44, - msb, - lsb, - (byte)(0xE3 ^ 0x44 ^ msb ^ lsb) - ]; + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + Data = frameBuilder.BuildXBus(0xE3, 0x44, msb, lsb); } public string Name => "LAN_X_PURGE_LOCO"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Driving/SetLocoBinaryStateCommand.cs b/src/Z21.Client/Core/Command/Driving/SetLocoBinaryStateCommand.cs new file mode 100644 index 0000000..d3c170e --- /dev/null +++ b/src/Z21.Client/Core/Command/Driving/SetLocoBinaryStateCommand.cs @@ -0,0 +1,29 @@ +using System; +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Driving +{ + /// + /// From Z21 FW version 1.42, sends a DCC "binary state" command to a locomotive decoder (protocol §4.3.3). + /// Allowed binary state addresses are 29 to 32767. + /// + public class SetLocoBinaryStateCommand : IZ21Command + { + /// Thrown when is outside 29..32767. + public SetLocoBinaryStateCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, ushort binaryStateAddress, bool enabled) + { + if (binaryStateAddress is < 29 or > 32767) + throw new ArgumentOutOfRangeException(nameof(binaryStateAddress), binaryStateAddress, "Binary state address must be between 29 and 32767."); + + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + byte db3 = (byte)((enabled ? 0x80 : 0x00) | (binaryStateAddress & 0x7F)); + byte db4 = (byte)((binaryStateAddress >> 7) & 0xFF); + Data = frameBuilder.BuildXBus(0xE5, 0x5F, msb, lsb, db3, db4); + } + + public string Name => "LAN_X_SET_LOCO_BINARY_STATE"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Driving/SetLocoDriveCommand.cs b/src/Z21.Client/Core/Command/Driving/SetLocoDriveCommand.cs index fbb9dc8..d2062c9 100644 --- a/src/Z21.Client/Core/Command/Driving/SetLocoDriveCommand.cs +++ b/src/Z21.Client/Core/Command/Driving/SetLocoDriveCommand.cs @@ -1,6 +1,7 @@ using System; +using Z21.Core.Codecs; using Z21.Core.Exception; -using Z21.Core.Helper; +using Z21.Core.Framing; using Z21.Core.Model; namespace Z21.Core.Command.Driving @@ -10,26 +11,15 @@ namespace Z21.Core.Command.Driving /// public class SetLocoDriveCommand : IZ21Command { - public SetLocoDriveCommand(DccSpeedMode dccSpeedMode, ushort locoAddress, DrivingDirection drivingDirection, ushort locoSpeed) + public SetLocoDriveCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ILocoSpeedCodec locoSpeedCodec, DccSpeedMode dccSpeedMode, ushort locoAddress, DrivingDirection drivingDirection, ushort locoSpeed) { LocoSpeedOutOfRangeException.ThrowIfOutOfRange(dccSpeedMode, locoSpeed); - ushort dccSpeed = LocoSpeedHelper.CalculateDccSpeed(dccSpeedMode, locoSpeed); + ushort dccSpeed = locoSpeedCodec.CalculateDccSpeed(dccSpeedMode, locoSpeed); - const byte xHeader = 0xE4; byte db0 = (byte)(0x10 | GetByte(dccSpeedMode)); - (byte lsb, byte msb) = AddressHelper.SplitLocoAddress(locoAddress); + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); byte db3 = (byte)((byte)drivingDirection | dccSpeed); - Data = - [ - 0x0A, 0x00, - 0x40, 0x00, - xHeader, - db0, - msb, - lsb, - db3, - (byte)(xHeader ^ db0 ^ msb ^ lsb ^ db3) - ]; + Data = frameBuilder.BuildXBus(0xE4, db0, msb, lsb, db3); } public string Name => "LAN_X_SET_LOCO_DRIVE"; @@ -44,4 +34,4 @@ public SetLocoDriveCommand(DccSpeedMode dccSpeedMode, ushort locoAddress, Drivin _ => throw new ArgumentOutOfRangeException(nameof(dccSpeedMode), dccSpeedMode, null) }; } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Driving/SetLocoEStopCommand.cs b/src/Z21.Client/Core/Command/Driving/SetLocoEStopCommand.cs index 933ddb2..4c95ecc 100644 --- a/src/Z21.Client/Core/Command/Driving/SetLocoEStopCommand.cs +++ b/src/Z21.Client/Core/Command/Driving/SetLocoEStopCommand.cs @@ -1,4 +1,5 @@ -using Z21.Core.Helper; +using Z21.Core.Codecs; +using Z21.Core.Framing; namespace Z21.Core.Command.Driving { @@ -9,24 +10,14 @@ namespace Z21.Core.Command.Driving /// public class SetLocoEStopCommand : IZ21Command { - public SetLocoEStopCommand(ushort locoAddress) + public SetLocoEStopCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress) { - (byte lsb, byte msb) = AddressHelper.SplitLocoAddress(locoAddress); - Data = - [ - 0x08, - 0x00, - 0x40, - 0x00, - 0x92, - msb, - lsb, - (byte)(0x92 ^ msb ^ lsb) - ]; + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + Data = frameBuilder.BuildXBus(0x92, msb, lsb); } public string Name => "LAN_X_SET_LOCO_E_STOP"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Driving/SetLocoFunctionCommand.cs b/src/Z21.Client/Core/Command/Driving/SetLocoFunctionCommand.cs index c9fc1cc..5552c83 100644 --- a/src/Z21.Client/Core/Command/Driving/SetLocoFunctionCommand.cs +++ b/src/Z21.Client/Core/Command/Driving/SetLocoFunctionCommand.cs @@ -1,4 +1,6 @@ -using Z21.Core.Helper; +using System; +using Z21.Core.Codecs; +using Z21.Core.Framing; using Z21.Core.Model; namespace Z21.Core.Command.Driving @@ -8,27 +10,19 @@ namespace Z21.Core.Command.Driving /// public class SetLocoFunctionCommand : IZ21Command { - public SetLocoFunctionCommand(ushort locoAddress, ushort functionIndex, FunctionToggleType toggleType) + /// Thrown when exceeds the 6-bit field (0..63). + public SetLocoFunctionCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, ushort functionIndex, FunctionToggleType toggleType) { - const byte xHeader = 0xE4; - const byte db0 = 0xF8; + if (functionIndex > 0x3F) + throw new ArgumentOutOfRangeException(nameof(functionIndex), functionIndex, "Function index must be between 0 and 63 (the 6-bit NNNNNN field of LAN_X_SET_LOCO_FUNCTION)."); + byte db3 = (byte)((byte)toggleType | functionIndex); - (byte lsb, byte msb) = AddressHelper.SplitLocoAddress(locoAddress); - Data = - [ - 0x0A, 0x00, - 0x40, 0x00, - xHeader, - db0, - msb, - lsb, - db3, - (byte)(xHeader ^ db0 ^ msb ^ lsb ^ db3) - ]; + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + Data = frameBuilder.BuildXBus(0xE4, 0xF8, msb, lsb, db3); } public string Name => "LAN_X_SET_LOCO_FUNCTION"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Driving/SetLocoFunctionGroupCommand.cs b/src/Z21.Client/Core/Command/Driving/SetLocoFunctionGroupCommand.cs new file mode 100644 index 0000000..4745bea --- /dev/null +++ b/src/Z21.Client/Core/Command/Driving/SetLocoFunctionGroupCommand.cs @@ -0,0 +1,22 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; +using Z21.Core.Model; + +namespace Z21.Core.Command.Driving +{ + /// + /// Switches a whole locomotive function group (up to 8 functions) with a single command (protocol §4.3.2). + /// + public class SetLocoFunctionGroupCommand : IZ21Command + { + public SetLocoFunctionGroupCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, LocoFunctionGroup group, byte functions) + { + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + Data = frameBuilder.BuildXBus(0xE4, (byte)group, msb, lsb, functions); + } + + public string Name => "LAN_X_SET_LOCO_FUNCTION_GROUP"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/FastClock/FastClockControlCommand.cs b/src/Z21.Client/Core/Command/FastClock/FastClockControlCommand.cs new file mode 100644 index 0000000..e59d856 --- /dev/null +++ b/src/Z21.Client/Core/Command/FastClock/FastClockControlCommand.cs @@ -0,0 +1,38 @@ +using System; +using CommandStation.Model; +using Z21.Core.Framing; +using Z21.Core.Model; + +namespace Z21.Core.Command.FastClock +{ + /// + /// From Z21 FW version 1.43, reads, sets, starts or stops the model time + /// (LAN_FAST_CLOCK_CONTROL, protocol §12.1). + /// + public class FastClockControlCommand : IZ21Command + { + public FastClockControlCommand(IZ21FrameBuilder frameBuilder, FastClockAction action) + { + byte selector = action switch + { + FastClockAction.Start => 0x2C, + FastClockAction.Stop => 0x2D, + _ => 0x2A + }; + Data = frameBuilder.BuildLanChecksummed(0x00CC, 0x21, selector); + } + + public FastClockControlCommand(IZ21FrameBuilder frameBuilder, ModelTime time) + { + ArgumentNullException.ThrowIfNull(time); + byte dayHour = (byte)(((time.Day & 0x07) << 5) | (time.Hour & 0x1F)); + byte minute = (byte)(time.Minute & 0x3F); + byte rate = (byte)(time.Rate & 0x3F); + Data = frameBuilder.BuildLanChecksummed(0x00CC, 0x24, 0x2B, dayHour, minute, rate); + } + + public string Name => "LAN_FAST_CLOCK_CONTROL"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/FastClock/GetFastClockSettingsCommand.cs b/src/Z21.Client/Core/Command/FastClock/GetFastClockSettingsCommand.cs new file mode 100644 index 0000000..b5ad488 --- /dev/null +++ b/src/Z21.Client/Core/Command/FastClock/GetFastClockSettingsCommand.cs @@ -0,0 +1,19 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.FastClock +{ + /// + /// Reads the persistent fast-clock settings (LAN_FAST_CLOCK_SETTINGS_GET, protocol §12.3). + /// + public class GetFastClockSettingsCommand : IZ21Command + { + public GetFastClockSettingsCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x00CE, 0x04); + } + + public string Name => "LAN_FAST_CLOCK_SETTINGS_GET"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsCommand.cs b/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsCommand.cs new file mode 100644 index 0000000..a7aa561 --- /dev/null +++ b/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.FastClock +{ + /// + /// Overwrites only the persistent fast-clock setting flags + /// (LAN_FAST_CLOCK_SETTINGS_SET, protocol §12.4). + /// + public class SetFastClockSettingsCommand : IZ21Command + { + public SetFastClockSettingsCommand(IZ21FrameBuilder frameBuilder, byte settings) + { + Data = frameBuilder.BuildLan(0x00CF, settings); + } + + public string Name => "LAN_FAST_CLOCK_SETTINGS_SET"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsWithRateCommand.cs b/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsWithRateCommand.cs new file mode 100644 index 0000000..d7d7db1 --- /dev/null +++ b/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsWithRateCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.FastClock +{ + /// + /// Overwrites the persistent fast-clock setting flags and the rate + /// (LAN_FAST_CLOCK_SETTINGS_SET, protocol §12.4). + /// + public class SetFastClockSettingsWithRateCommand : IZ21Command + { + public SetFastClockSettingsWithRateCommand(IZ21FrameBuilder frameBuilder, byte settings, byte rate) + { + Data = frameBuilder.BuildLan(0x00CF, settings, rate); + } + + public string Name => "LAN_FAST_CLOCK_SETTINGS_SET"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsWithStartTimeCommand.cs b/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsWithStartTimeCommand.cs new file mode 100644 index 0000000..1071aeb --- /dev/null +++ b/src/Z21.Client/Core/Command/FastClock/SetFastClockSettingsWithStartTimeCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.FastClock +{ + /// + /// Overwrites the persistent fast-clock setting flags, the rate and the default start time + /// (LAN_FAST_CLOCK_SETTINGS_SET, protocol §12.4). + /// + public class SetFastClockSettingsWithStartTimeCommand : IZ21Command + { + public SetFastClockSettingsWithStartTimeCommand(IZ21FrameBuilder frameBuilder, byte settings, byte rate, byte startDayHour, byte startMinute) + { + Data = frameBuilder.BuildLan(0x00CF, settings, rate, startDayHour, startMinute); + } + + public string Name => "LAN_FAST_CLOCK_SETTINGS_SET"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Feedback/GetRmBusDataCommand.cs b/src/Z21.Client/Core/Command/Feedback/GetRmBusDataCommand.cs new file mode 100644 index 0000000..fd6fc9f --- /dev/null +++ b/src/Z21.Client/Core/Command/Feedback/GetRmBusDataCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Feedback +{ + /// + /// Requests the current status of the R-BUS feedback modules (protocol §7.2). Group index 0 covers + /// modules with addresses 1–10, group index 1 covers addresses 11–20. + /// + public class GetRmBusDataCommand : IZ21Command + { + public GetRmBusDataCommand(IZ21FrameBuilder frameBuilder, byte groupIndex) + { + Data = frameBuilder.BuildLan(0x0081, groupIndex); + } + + public string Name => "LAN_RMBUS_GETDATA"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Feedback/ProgramRmBusModuleCommand.cs b/src/Z21.Client/Core/Command/Feedback/ProgramRmBusModuleCommand.cs new file mode 100644 index 0000000..57a3920 --- /dev/null +++ b/src/Z21.Client/Core/Command/Feedback/ProgramRmBusModuleCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Feedback +{ + /// + /// Programs the address of an R-BUS feedback module (protocol §7.3). The programming command is issued + /// on the R-BUS until it is sent again with address 0. Range: 0 and 1–20. + /// + public class ProgramRmBusModuleCommand : IZ21Command + { + public ProgramRmBusModuleCommand(IZ21FrameBuilder frameBuilder, byte address) + { + Data = frameBuilder.BuildLan(0x0082, address); + } + + public string Name => "LAN_RMBUS_PROGRAMMODULE"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/IZ21CommandFactory.cs b/src/Z21.Client/Core/Command/IZ21CommandFactory.cs new file mode 100644 index 0000000..8d0dc0f --- /dev/null +++ b/src/Z21.Client/Core/Command/IZ21CommandFactory.cs @@ -0,0 +1,14 @@ +namespace Z21.Core.Command +{ + /// + /// Constructs instances, supplying their required encoding services and + /// binding any remaining constructor arguments. Adding a new command requires no change here. + /// + public interface IZ21CommandFactory + { + /// + /// Creates a command of type ; encoding services are supplied automatically and fills the remaining constructor parameters. + /// + TCommand Create(params object[] args) where TCommand : IZ21Command; + } +} diff --git a/src/Z21.Client/Core/Command/LocoNet/LocoNetDetectorCommand.cs b/src/Z21.Client/Core/Command/LocoNet/LocoNetDetectorCommand.cs new file mode 100644 index 0000000..332ff3c --- /dev/null +++ b/src/Z21.Client/Core/Command/LocoNet/LocoNetDetectorCommand.cs @@ -0,0 +1,22 @@ +using System; +using Z21.Core.Framing; + +namespace Z21.Core.Command.LocoNet +{ + /// + /// From Z21 FW version 1.22, queries the occupancy status of LocoNet track occupancy detectors + /// (LAN_LOCONET_DETECTOR, protocol §9.5). + /// + public class LocoNetDetectorCommand : IZ21Command + { + public LocoNetDetectorCommand(IZ21FrameBuilder frameBuilder, byte type, ushort reportAddress) + { + byte[] address = BitConverter.GetBytes(reportAddress); + Data = frameBuilder.BuildLan(0x00A4, type, address[0], address[1]); + } + + public string Name => "LAN_LOCONET_DETECTOR"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/LocoNet/LocoNetDispatchAddressCommand.cs b/src/Z21.Client/Core/Command/LocoNet/LocoNetDispatchAddressCommand.cs new file mode 100644 index 0000000..cd98623 --- /dev/null +++ b/src/Z21.Client/Core/Command/LocoNet/LocoNetDispatchAddressCommand.cs @@ -0,0 +1,22 @@ +using System; +using Z21.Core.Framing; + +namespace Z21.Core.Command.LocoNet +{ + /// + /// From Z21 FW version 1.20, prepares a locomotive address for LocoNet dispatch ("DISPATCH_PUT", + /// LAN_LOCONET_DISPATCH_ADDR, protocol §9.4). + /// + public class LocoNetDispatchAddressCommand : IZ21Command + { + public LocoNetDispatchAddressCommand(IZ21FrameBuilder frameBuilder, ushort locoAddress) + { + byte[] address = BitConverter.GetBytes(locoAddress); + Data = frameBuilder.BuildLan(0x00A3, address[0], address[1]); + } + + public string Name => "LAN_LOCONET_DISPATCH_ADDR"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/LocoNet/LocoNetFromLanCommand.cs b/src/Z21.Client/Core/Command/LocoNet/LocoNetFromLanCommand.cs new file mode 100644 index 0000000..4bfe304 --- /dev/null +++ b/src/Z21.Client/Core/Command/LocoNet/LocoNetFromLanCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.LocoNet +{ + /// + /// From Z21 FW version 1.20, writes a raw LocoNet message (including its checksum) onto the LocoNet bus + /// (LAN_LOCONET_FROM_LAN, protocol §9.3). + /// + public class LocoNetFromLanCommand : IZ21Command + { + public LocoNetFromLanCommand(IZ21FrameBuilder frameBuilder, byte[] message) + { + Data = frameBuilder.BuildLan(0x00A2, message); + } + + public string Name => "LAN_LOCONET_FROM_LAN"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryReadByteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryReadByteCommand.cs new file mode 100644 index 0000000..ab480de --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryReadByteCommand.cs @@ -0,0 +1,24 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// From Z21 FW version 1.22, reads a CV of an accessory decoder on the main track (POM, protocol §6.11). + /// Requires RailCom enabled. + /// + public class CvPomAccessoryReadByteCommand : IZ21Command + { + public CvPomAccessoryReadByteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress) + { + (byte db1, byte db2) = addressCodec.EncodeAccessoryPomAddress(decoderAddress, wholeDecoder, output); + byte db3 = (byte)(0xE4 | ((cvAddress >> 8) & 0x03)); + byte cvLsb = (byte)(cvAddress & 0xFF); + Data = frameBuilder.BuildXBus(0xE6, 0x31, db1, db2, db3, cvLsb, 0x00); + } + + public string Name => "LAN_X_CV_POM_ACCESSORY_READ_BYTE"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteBitCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteBitCommand.cs new file mode 100644 index 0000000..f264d30 --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteBitCommand.cs @@ -0,0 +1,25 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// From Z21 FW version 1.22, writes a single bit of a CV of an accessory decoder on the main track + /// (POM, protocol §6.10). + /// + public class CvPomAccessoryWriteBitCommand : IZ21Command + { + public CvPomAccessoryWriteBitCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress, byte bitPosition, bool bitValue) + { + (byte db1, byte db2) = addressCodec.EncodeAccessoryPomAddress(decoderAddress, wholeDecoder, output); + byte db3 = (byte)(0xE8 | ((cvAddress >> 8) & 0x03)); + byte cvLsb = (byte)(cvAddress & 0xFF); + byte db5 = (byte)((bitValue ? 0x08 : 0x00) | (bitPosition & 0x07)); + Data = frameBuilder.BuildXBus(0xE6, 0x31, db1, db2, db3, cvLsb, db5); + } + + public string Name => "LAN_X_CV_POM_ACCESSORY_WRITE_BIT"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteByteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteByteCommand.cs new file mode 100644 index 0000000..276d6e7 --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/CvPomAccessoryWriteByteCommand.cs @@ -0,0 +1,24 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// From Z21 FW version 1.22, writes a CV of an accessory decoder on the main track (POM, protocol §6.9). + /// When wholeDecoder is true the CV refers to the whole decoder; otherwise to a single output. + /// + public class CvPomAccessoryWriteByteCommand : IZ21Command + { + public CvPomAccessoryWriteByteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort decoderAddress, bool wholeDecoder, byte output, ushort cvAddress, byte value) + { + (byte db1, byte db2) = addressCodec.EncodeAccessoryPomAddress(decoderAddress, wholeDecoder, output); + byte db3 = (byte)(0xEC | ((cvAddress >> 8) & 0x03)); + byte cvLsb = (byte)(cvAddress & 0xFF); + Data = frameBuilder.BuildXBus(0xE6, 0x31, db1, db2, db3, cvLsb, value); + } + + public string Name => "LAN_X_CV_POM_ACCESSORY_WRITE_BYTE"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/CvPomReadByteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomReadByteCommand.cs new file mode 100644 index 0000000..e079118 --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/CvPomReadByteCommand.cs @@ -0,0 +1,24 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// From Z21 FW version 1.22, reads a CV of a locomotive decoder on the main track (POM, protocol §6.8). + /// Requires RailCom enabled. CV address 0 = CV1. + /// + public class CvPomReadByteCommand : IZ21Command + { + public CvPomReadByteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, ushort cvAddress) + { + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + byte db3 = (byte)(0xE4 | ((cvAddress >> 8) & 0x03)); + byte cvLsb = (byte)(cvAddress & 0xFF); + Data = frameBuilder.BuildXBus(0xE6, 0x30, msb, lsb, db3, cvLsb, 0x00); + } + + public string Name => "LAN_X_CV_POM_READ_BYTE"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/CvPomWriteBitCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomWriteBitCommand.cs new file mode 100644 index 0000000..9f4848b --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/CvPomWriteBitCommand.cs @@ -0,0 +1,24 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// Writes a single bit of a CV of a locomotive decoder on the main track (POM, protocol §6.7). + /// + public class CvPomWriteBitCommand : IZ21Command + { + public CvPomWriteBitCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, ushort cvAddress, byte bitPosition, bool bitValue) + { + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + byte db3 = (byte)(0xE8 | ((cvAddress >> 8) & 0x03)); + byte cvLsb = (byte)(cvAddress & 0xFF); + byte db5 = (byte)((bitValue ? 0x08 : 0x00) | (bitPosition & 0x07)); + Data = frameBuilder.BuildXBus(0xE6, 0x30, msb, lsb, db3, cvLsb, db5); + } + + public string Name => "LAN_X_CV_POM_WRITE_BIT"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/CvPomWriteByteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvPomWriteByteCommand.cs new file mode 100644 index 0000000..ad18223 --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/CvPomWriteByteCommand.cs @@ -0,0 +1,23 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// Writes a CV of a locomotive decoder on the main track (POM, protocol §6.6). CV address 0 = CV1. + /// + public class CvPomWriteByteCommand : IZ21Command + { + public CvPomWriteByteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort locoAddress, ushort cvAddress, byte value) + { + (byte lsb, byte msb) = addressCodec.SplitLocoAddress(locoAddress); + byte db3 = (byte)(0xEC | ((cvAddress >> 8) & 0x03)); + byte cvLsb = (byte)(cvAddress & 0xFF); + Data = frameBuilder.BuildXBus(0xE6, 0x30, msb, lsb, db3, cvLsb, value); + } + + public string Name => "LAN_X_CV_POM_WRITE_BYTE"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/CvReadCommand.cs b/src/Z21.Client/Core/Command/Programming/CvReadCommand.cs new file mode 100644 index 0000000..96aff35 --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/CvReadCommand.cs @@ -0,0 +1,21 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// Reads a CV in direct mode on the programming track (protocol §6.1). CV address 0 = CV1. + /// + public class CvReadCommand : IZ21Command + { + public CvReadCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort cvAddress) + { + (byte msb, byte lsb) = addressCodec.SplitCvAddress(cvAddress); + Data = frameBuilder.BuildXBus(0x23, 0x11, msb, lsb); + } + + public string Name => "LAN_X_CV_READ"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/CvWriteCommand.cs b/src/Z21.Client/Core/Command/Programming/CvWriteCommand.cs new file mode 100644 index 0000000..b28f97e --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/CvWriteCommand.cs @@ -0,0 +1,21 @@ +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// Overwrites a CV in direct mode on the programming track (protocol §6.2). CV address 0 = CV1. + /// + public class CvWriteCommand : IZ21Command + { + public CvWriteCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort cvAddress, byte value) + { + (byte msb, byte lsb) = addressCodec.SplitCvAddress(cvAddress); + Data = frameBuilder.BuildXBus(0x24, 0x12, msb, lsb, value); + } + + public string Name => "LAN_X_CV_WRITE"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/DccReadRegisterCommand.cs b/src/Z21.Client/Core/Command/Programming/DccReadRegisterCommand.cs new file mode 100644 index 0000000..c83b180 --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/DccReadRegisterCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// From Z21 FW version 1.25, reads a register of a DCC decoder in register mode on the programming + /// track (protocol §6.13). Register range 0x01–0x08. + /// + public class DccReadRegisterCommand : IZ21Command + { + public DccReadRegisterCommand(IZ21FrameBuilder frameBuilder, byte register) + { + Data = frameBuilder.BuildXBus(0x22, 0x11, register); + } + + public string Name => "LAN_X_DCC_READ_REGISTER"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/DccWriteRegisterCommand.cs b/src/Z21.Client/Core/Command/Programming/DccWriteRegisterCommand.cs new file mode 100644 index 0000000..27748c3 --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/DccWriteRegisterCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// From Z21 FW version 1.25, overwrites a register of a DCC decoder in register mode on the + /// programming track (protocol §6.14). Register range 0x01–0x08. + /// + public class DccWriteRegisterCommand : IZ21Command + { + public DccWriteRegisterCommand(IZ21FrameBuilder frameBuilder, byte register, byte value) + { + Data = frameBuilder.BuildXBus(0x23, 0x12, register, value); + } + + public string Name => "LAN_X_DCC_WRITE_REGISTER"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Programming/MmWriteByteCommand.cs b/src/Z21.Client/Core/Command/Programming/MmWriteByteCommand.cs new file mode 100644 index 0000000..bbfe90f --- /dev/null +++ b/src/Z21.Client/Core/Command/Programming/MmWriteByteCommand.cs @@ -0,0 +1,20 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.Programming +{ + /// + /// From Z21 FW version 1.23, overwrites a register of a Motorola decoder on the programming track + /// (protocol §6.12). Register range 0–78. + /// + public class MmWriteByteCommand : IZ21Command + { + public MmWriteByteCommand(IZ21FrameBuilder frameBuilder, byte register, byte value) + { + Data = frameBuilder.BuildXBus(0x24, 0xFF, 0x00, register, value); + } + + public string Name => "LAN_X_MM_WRITE_BYTE"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/RailCom/GetRailComDataCommand.cs b/src/Z21.Client/Core/Command/RailCom/GetRailComDataCommand.cs new file mode 100644 index 0000000..f5f5429 --- /dev/null +++ b/src/Z21.Client/Core/Command/RailCom/GetRailComDataCommand.cs @@ -0,0 +1,22 @@ +using System; +using Z21.Core.Framing; + +namespace Z21.Core.Command.RailCom +{ + /// + /// From Z21 FW version 1.29, requests RailCom data for a given locomotive address (protocol §8.2). + /// Locomotive address 0 returns the next locomotive in the ring buffer. + /// + public class GetRailComDataCommand : IZ21Command + { + public GetRailComDataCommand(IZ21FrameBuilder frameBuilder, ushort locoAddress) + { + byte[] address = BitConverter.GetBytes(locoAddress); + Data = frameBuilder.BuildLan(0x0089, 0x01, address[0], address[1]); + } + + public string Name => "LAN_RAILCOM_GETDATA"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Command/Settings/GetAccessoryModeCommand.cs b/src/Z21.Client/Core/Command/Settings/GetAccessoryModeCommand.cs index c0a47eb..5d40744 100644 --- a/src/Z21.Client/Core/Command/Settings/GetAccessoryModeCommand.cs +++ b/src/Z21.Client/Core/Command/Settings/GetAccessoryModeCommand.cs @@ -1,9 +1,10 @@ -using System; +using Z21.Core.Codecs; +using Z21.Core.Framing; namespace Z21.Core.Command.Settings { /// - /// Read the settings for a given accessory decoder address ("Accessory Decoder" RP-9.2.1). + /// Read the settings for a given accessory decoder address ("Accessory Decoder" RP-9.2.1). /// /// /// In the Z21, the output format (DCC, MM) is persistently stored for each accessory decoder address. @@ -11,24 +12,14 @@ namespace Z21.Core.Command.Settings /// public class GetAccessoryModeCommand : IZ21Command { - public GetAccessoryModeCommand(short locoAddress) + public GetAccessoryModeCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, short accessoryAddress) { - byte[] addressBytes = BitConverter.GetBytes(locoAddress); - Array.Reverse(addressBytes); - - Data = - [ - 0x06, - 0x00, - 0x70, - 0x00, - addressBytes[0], - addressBytes[1] - ]; + (byte msb, byte lsb) = addressCodec.SplitAddressBigEndian((ushort)accessoryAddress); + Data = frameBuilder.BuildLan(0x0070, msb, lsb); } public string Name => "LAN_GET_TURNOUTMODE"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Settings/GetLocoModeCommand.cs b/src/Z21.Client/Core/Command/Settings/GetLocoModeCommand.cs index 790b5b1..85a2cd3 100644 --- a/src/Z21.Client/Core/Command/Settings/GetLocoModeCommand.cs +++ b/src/Z21.Client/Core/Command/Settings/GetLocoModeCommand.cs @@ -1,4 +1,5 @@ -using System; +using Z21.Core.Codecs; +using Z21.Core.Framing; namespace Z21.Core.Command.Settings { @@ -10,25 +11,14 @@ namespace Z21.Core.Command.Settings /// public class GetLocoModeCommand : IZ21Command { - - public GetLocoModeCommand(short locoAddress) + public GetLocoModeCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, short locoAddress) { - byte[] addressBytes = BitConverter.GetBytes(locoAddress); - Array.Reverse(addressBytes); - - Data = - [ - 0x06, - 0x00, - 0x60, - 0x00, - addressBytes[0], - addressBytes[1] - ]; + (byte msb, byte lsb) = addressCodec.SplitAddressBigEndian((ushort)locoAddress); + Data = frameBuilder.BuildLan(0x0060, msb, lsb); } public string Name => "LAN_GET_LOCOMODE"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Settings/SetAccessoryModeCommand.cs b/src/Z21.Client/Core/Command/Settings/SetAccessoryModeCommand.cs index 5288edd..d1c5dcc 100644 --- a/src/Z21.Client/Core/Command/Settings/SetAccessoryModeCommand.cs +++ b/src/Z21.Client/Core/Command/Settings/SetAccessoryModeCommand.cs @@ -1,4 +1,6 @@ using System; +using Z21.Core.Codecs; +using Z21.Core.Framing; using Z21.Core.Model; namespace Z21.Core.Command.Settings @@ -9,28 +11,17 @@ namespace Z21.Core.Command.Settings public class SetAccessoryModeCommand : IZ21Command { /// Is thrown when is - public SetAccessoryModeCommand(short accessoryAddress, DecoderMode decoderMode) + public SetAccessoryModeCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, short accessoryAddress, DecoderMode decoderMode) { if (decoderMode is DecoderMode.Unknown) throw new ArgumentException($"{DecoderMode.Unknown} is not a valid DecoderMode.", nameof(decoderMode)); - byte[] addressBytes = BitConverter.GetBytes(accessoryAddress); - Array.Reverse(addressBytes); - - Data = - [ - 0x07, - 0x00, - 0x71, - 0x00, - addressBytes[0], - addressBytes[1], - (byte)decoderMode - ]; + (byte msb, byte lsb) = addressCodec.SplitAddressBigEndian((ushort)accessoryAddress); + Data = frameBuilder.BuildLan(0x0071, msb, lsb, (byte)decoderMode); } public string Name => "LAN_SET_TURNOUTMODE"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Settings/SetLocoModeCommand.cs b/src/Z21.Client/Core/Command/Settings/SetLocoModeCommand.cs index 3a8de9c..c29d77e 100644 --- a/src/Z21.Client/Core/Command/Settings/SetLocoModeCommand.cs +++ b/src/Z21.Client/Core/Command/Settings/SetLocoModeCommand.cs @@ -1,4 +1,6 @@ using System; +using Z21.Core.Codecs; +using Z21.Core.Framing; using Z21.Core.Model; namespace Z21.Core.Command.Settings @@ -9,28 +11,17 @@ namespace Z21.Core.Command.Settings public class SetLocoModeCommand : IZ21Command { /// Is thrown when is - public SetLocoModeCommand(short locoAddress, DecoderMode decoderMode) + public SetLocoModeCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, short locoAddress, DecoderMode decoderMode) { if (decoderMode is DecoderMode.Unknown) throw new ArgumentException($"{DecoderMode.Unknown} is not a valid DecoderMode.", nameof(decoderMode)); - byte[] addressBytes = BitConverter.GetBytes(locoAddress); - Array.Reverse(addressBytes); - - Data = - [ - 0x07, - 0x00, - 0x61, - 0x00, - addressBytes[0], - addressBytes[1], - (byte)decoderMode - ]; + (byte msb, byte lsb) = addressCodec.SplitAddressBigEndian((ushort)locoAddress); + Data = frameBuilder.BuildLan(0x0061, msb, lsb, (byte)decoderMode); } public string Name => "LAN_SET_LOCOMODE"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Switching/GetExtAccessoryInfoCommand.cs b/src/Z21.Client/Core/Command/Switching/GetExtAccessoryInfoCommand.cs index 19ff68f..c16b2e6 100644 --- a/src/Z21.Client/Core/Command/Switching/GetExtAccessoryInfoCommand.cs +++ b/src/Z21.Client/Core/Command/Switching/GetExtAccessoryInfoCommand.cs @@ -1,4 +1,5 @@ -using Z21.Core.Helper; +using Z21.Core.Codecs; +using Z21.Core.Framing; namespace Z21.Core.Command.Switching { @@ -7,23 +8,14 @@ namespace Z21.Core.Command.Switching /// public class GetExtAccessoryInfoCommand : IZ21Command { - public GetExtAccessoryInfoCommand(ushort accessoryAddress) + public GetExtAccessoryInfoCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort accessoryAddress) { - (byte lsb, byte msb) = AddressHelper.SplitAccessoryAddress(accessoryAddress); - Data = - [ - 0x09, 0x00, - 0x40, 0x00, - 0x44, - msb, - lsb, - 0x00, - (byte)(0x44 ^ msb ^ lsb ^ 0x00) - ]; + (byte lsb, byte msb) = addressCodec.SplitExtAccessoryAddress(accessoryAddress); + Data = frameBuilder.BuildXBus(0x44, msb, lsb, 0x00); } public string Name => "LAN_X_GET_EXT_ACCESSORY_INFO"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Switching/GetTurnoutInfoCommand.cs b/src/Z21.Client/Core/Command/Switching/GetTurnoutInfoCommand.cs index ef7b061..b3734de 100644 --- a/src/Z21.Client/Core/Command/Switching/GetTurnoutInfoCommand.cs +++ b/src/Z21.Client/Core/Command/Switching/GetTurnoutInfoCommand.cs @@ -1,5 +1,6 @@ using System; -using Z21.Core.Helper; +using Z21.Core.Codecs; +using Z21.Core.Framing; namespace Z21.Core.Command.Switching { @@ -9,22 +10,14 @@ namespace Z21.Core.Command.Switching public class GetTurnoutInfoCommand : IZ21Command { /// Thrown when is smaller than 1. - public GetTurnoutInfoCommand(ushort accessoryAddress) + public GetTurnoutInfoCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort accessoryAddress) { - (byte lsb, byte msb) = AddressHelper.SplitAccessoryAddress(accessoryAddress); - Data = - [ - 0x08, 0x00, - 0x40, 0x00, - 0x43, - msb, - lsb, - (byte)(0x43 ^ msb ^ lsb) - ]; + (byte lsb, byte msb) = addressCodec.SplitAccessoryAddress(accessoryAddress); + Data = frameBuilder.BuildXBus(0x43, msb, lsb); } public string Name => "LAN_X_GET_TURNOUT_INFO"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Switching/SetExtAccessoryCommand.cs b/src/Z21.Client/Core/Command/Switching/SetExtAccessoryCommand.cs index 32121d4..b6e11d8 100644 --- a/src/Z21.Client/Core/Command/Switching/SetExtAccessoryCommand.cs +++ b/src/Z21.Client/Core/Command/Switching/SetExtAccessoryCommand.cs @@ -1,4 +1,5 @@ -using Z21.Core.Helper; +using Z21.Core.Codecs; +using Z21.Core.Framing; using Z21.Core.Model.ExcAccessoryPayload; namespace Z21.Core.Command.Switching @@ -13,7 +14,7 @@ public class SetExtAccessoryCommand : IZ21Command /// /// The 10837 Z21 signaldecoder interprets as one of 256 theoretically possible signal aspects. The actual value range depends to a large extent on the signal type set in the signal decoder. See for all possible values. /// The 10836 Z21 switch DECODER interprets the payload as "switch decoder with reception of switching time". Use to generate payload. - public SetExtAccessoryCommand(ushort accessoryAddress, IExcAccessoryPayload payload) : this(accessoryAddress, (byte)payload.Payload) + public SetExtAccessoryCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort accessoryAddress, IExcAccessoryPayload payload) : this(frameBuilder, addressCodec, accessoryAddress, (byte)payload.Payload) { } @@ -22,25 +23,14 @@ public SetExtAccessoryCommand(ushort accessoryAddress, IExcAccessoryPayload payl /// /// The 10837 Z21 signaldecoder interprets as one of 256 theoretically possible signal aspects. The actual value range depends to a large extent on the signal type set in the signal decoder. See for all possible values. /// The 10836 Z21 switch DECODER interprets the payload as "switch decoder with reception of switching time". Use to generate payload. - public SetExtAccessoryCommand(ushort accessoryAddress, byte payload) + public SetExtAccessoryCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort accessoryAddress, byte payload) { - (byte lsb, byte msb) = AddressHelper.SplitAccessoryAddress(accessoryAddress); - - Data = - [ - 0x0A, 0x00, - 0x40, 0x00, - 0x54, - msb, - lsb, - payload, - 0x00, - (byte)(0x54 ^ msb ^ lsb ^ payload ^ 0x00) - ]; + (byte lsb, byte msb) = addressCodec.SplitExtAccessoryAddress(accessoryAddress); + Data = frameBuilder.BuildXBus(0x54, msb, lsb, payload, 0x00); } public string Name => "LAN_X_SET_EXT_ACCESSORY"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Switching/SetTurnoutCommand.cs b/src/Z21.Client/Core/Command/Switching/SetTurnoutCommand.cs index c9c8de2..95862da 100644 --- a/src/Z21.Client/Core/Command/Switching/SetTurnoutCommand.cs +++ b/src/Z21.Client/Core/Command/Switching/SetTurnoutCommand.cs @@ -1,5 +1,6 @@ using System; -using Z21.Core.Helper; +using Z21.Core.Codecs; +using Z21.Core.Framing; using Z21.Core.Model; namespace Z21.Core.Command.Switching @@ -16,25 +17,15 @@ namespace Z21.Core.Command.Switching public class SetTurnoutCommand : IZ21Command { /// Thrown when is smaller than 1. - public SetTurnoutCommand(ushort accessoryAddress, AccessoryOutput accessoryOutput, AccessoryState accessoryState, bool executeImmediately) + public SetTurnoutCommand(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ushort accessoryAddress, AccessoryOutput accessoryOutput, AccessoryState accessoryState, bool executeImmediately) { - (byte lsb, byte msb) = AddressHelper.SplitAccessoryAddress(accessoryAddress); - byte db2 = (byte)(0x80 | (int)accessoryOutput | (int)accessoryState | (executeImmediately ? 0x20 : 0x00)); - - Data = - [ - 0x09, 0x00, - 0x40, 0x00, - 0x53, - msb, - lsb, - db2, - (byte)(0x53 ^ msb ^ lsb ^ db2) - ]; + (byte lsb, byte msb) = addressCodec.SplitAccessoryAddress(accessoryAddress); + byte db2 = (byte)(0x80 | (int)accessoryOutput | (int)accessoryState | (executeImmediately ? 0x00 : 0x20)); + Data = frameBuilder.BuildXBus(0x53, msb, lsb, db2); } public string Name => "LAN_X_SET_TURNOUT"; public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/GetBroadcastFlagsCommand.cs b/src/Z21.Client/Core/Command/SystemState/GetBroadcastFlagsCommand.cs index a1ee498..02179e8 100644 --- a/src/Z21.Client/Core/Command/SystemState/GetBroadcastFlagsCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/GetBroadcastFlagsCommand.cs @@ -1,18 +1,19 @@ -namespace Z21.Core.Command.SystemState +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState { /// /// Reading the broadcast flags in the Z21. /// public class GetBroadcastFlagsCommand : IZ21Command { + public GetBroadcastFlagsCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x0051); + } + public string Name => "LAN_GET_BROADCASTFLAGS"; - public byte[] Data { get; } = - [ - 0x04, - 0x00, - 0x51, - 0x00 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/GetFirmwareVersionCommand.cs b/src/Z21.Client/Core/Command/SystemState/GetFirmwareVersionCommand.cs index 09ceeb4..b09a642 100644 --- a/src/Z21.Client/Core/Command/SystemState/GetFirmwareVersionCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/GetFirmwareVersionCommand.cs @@ -1,21 +1,19 @@ -namespace Z21.Core.Command.SystemState +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState { /// /// The firmware version of the Z21 can be read with this command. /// public class GetFirmwareVersionCommand : IZ21Command { + public GetFirmwareVersionCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildXBus(0xF1, 0x0A); + } + public string Name => "LAN_X_GET_FIRMWARE_VERSION"; - public byte[] Data { get; } = - [ - 0x07, - 0x00, - 0x40, - 0x00, - 0xF1, - 0x0A, - 0xF1 ^ 0x0A - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/GetHardwareInfoCommand.cs b/src/Z21.Client/Core/Command/SystemState/GetHardwareInfoCommand.cs index d6c92cf..b576fad 100644 --- a/src/Z21.Client/Core/Command/SystemState/GetHardwareInfoCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/GetHardwareInfoCommand.cs @@ -1,3 +1,5 @@ +using Z21.Core.Framing; + namespace Z21.Core.Command.SystemState { /// @@ -5,14 +7,13 @@ namespace Z21.Core.Command.SystemState /// public class GetHardwareInfoCommand : IZ21Command { + public GetHardwareInfoCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x001A); + } + public string Name => "LAN_GET_HWINFO"; - public byte[] Data { get; } = - [ - 0x04, - 0x00, - 0x1A, - 0x00 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/GetSerialNumberCommand.cs b/src/Z21.Client/Core/Command/SystemState/GetSerialNumberCommand.cs index 81dc090..64bf2fe 100644 --- a/src/Z21.Client/Core/Command/SystemState/GetSerialNumberCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/GetSerialNumberCommand.cs @@ -1,18 +1,19 @@ -namespace Z21.Core.Command.SystemState +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState { /// /// Reading the serial number of the Z21. /// public class GetSerialNumberCommand : IZ21Command { + public GetSerialNumberCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x0010); + } + public string Name => "LAN_GET_SERIAL_NUMBER"; - public byte[] Data { get; } = - [ - 0x04, - 0x00, - 0x10, - 0x00 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/GetSoftwareLockCommand.cs b/src/Z21.Client/Core/Command/SystemState/GetSoftwareLockCommand.cs index 8fb141c..3f280e4 100644 --- a/src/Z21.Client/Core/Command/SystemState/GetSoftwareLockCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/GetSoftwareLockCommand.cs @@ -1,3 +1,5 @@ +using Z21.Core.Framing; + namespace Z21.Core.Command.SystemState { /// @@ -6,14 +8,13 @@ namespace Z21.Core.Command.SystemState /// This command is of particular interest for the hardware variant "z21 start", in order to be able to check whether driving and switching via LAN is blocked or permitted. public class GetSoftwareLockCommand : IZ21Command { + public GetSoftwareLockCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x0018); + } + public string Name => "LAN_GET_CODE"; - public byte[] Data { get; } = - [ - 0x04, - 0x00, - 0x18, - 0x00 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/GetStatusCommand.cs b/src/Z21.Client/Core/Command/SystemState/GetStatusCommand.cs index 629960d..a6cadec 100644 --- a/src/Z21.Client/Core/Command/SystemState/GetStatusCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/GetStatusCommand.cs @@ -1,19 +1,19 @@ -namespace Z21.Core.Command.SystemState +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState { /// /// This command can be used to request the Z21 status. /// public class GetStatusCommand : IZ21Command { + public GetStatusCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildXBus(0x21, 0x24); + } + public string Name => "LAN_X_GET_STATUS"; - public byte[] Data { get; } = - [ - 0x07, 0x00, //DataLen - 0x40, 0x00, //Header - 0x21, // X-Header - 0x24, // DB0 - 0x21 ^ 0x24 // XOR-Byte - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/GetSystemStateDataCommand.cs b/src/Z21.Client/Core/Command/SystemState/GetSystemStateDataCommand.cs index 76ec424..59c270d 100644 --- a/src/Z21.Client/Core/Command/SystemState/GetSystemStateDataCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/GetSystemStateDataCommand.cs @@ -1,3 +1,5 @@ +using Z21.Core.Framing; + namespace Z21.Core.Command.SystemState { /// @@ -5,14 +7,13 @@ namespace Z21.Core.Command.SystemState /// public class GetSystemStateDataCommand : IZ21Command { + public GetSystemStateDataCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x0085); + } + public string Name => "LAN_SYSTEMSTATE_GETDATA"; - public byte[] Data { get; } = - [ - 0x04, - 0x00, - 0x85, - 0x00 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/GetVersionCommand.cs b/src/Z21.Client/Core/Command/SystemState/GetVersionCommand.cs index 336a770..f9eef0d 100644 --- a/src/Z21.Client/Core/Command/SystemState/GetVersionCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/GetVersionCommand.cs @@ -1,19 +1,19 @@ -namespace Z21.Core.Command.SystemState +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState { /// /// The X-Bus version of the Z21 can be read out with the following command. /// public class GetVersionCommand : IZ21Command { + public GetVersionCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildXBus(0x21, 0x21); + } + public string Name => "LAN_X_GET_VERSION"; - public byte[] Data { get; } = - [ - 0x07, 0x00, - 0x40, 0x00, - 0x21, - 0x21, - 0x21 ^ 0x21 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/LogOffCommand.cs b/src/Z21.Client/Core/Command/SystemState/LogOffCommand.cs index 6046c8d..26c3b38 100644 --- a/src/Z21.Client/Core/Command/SystemState/LogOffCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/LogOffCommand.cs @@ -1,18 +1,19 @@ -namespace Z21.Core.Command.SystemState +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState { /// - /// Logging off the client from the Z21. + /// Logging off the client from the Z21. /// public class LogOffCommand : IZ21Command { + public LogOffCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x0030); + } + public string Name => "LAN_LOGOFF"; - public byte[] Data { get; } = - [ - 0x04, - 0x00, - 0x30, - 0x00 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/SetBroadcastFlagsCommand.cs b/src/Z21.Client/Core/Command/SystemState/SetBroadcastFlagsCommand.cs index 3e17ecd..4045a4d 100644 --- a/src/Z21.Client/Core/Command/SystemState/SetBroadcastFlagsCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/SetBroadcastFlagsCommand.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Linq; +using Z21.Core.Framing; using Z21.Core.Model; namespace Z21.Core.Command.SystemState @@ -9,24 +10,15 @@ namespace Z21.Core.Command.SystemState /// public class SetBroadcastFlagsCommand : IZ21Command { - public string Name => "LAN_SET_BROADCASTFLAGS"; - - public SetBroadcastFlagsCommand(params uint[] flags) + public SetBroadcastFlagsCommand(IZ21FrameBuilder frameBuilder, params uint[] flags) { uint flag = flags.Length > 0 ? flags.Aggregate((u, u1) => u | u1) : 0; - byte[] broadcastFlags = BitConverter.GetBytes(flag); - Data = - [ - 0x08, 0x00, - 0x50, 0x00, - broadcastFlags[0], - broadcastFlags[1], - broadcastFlags[2], - broadcastFlags[3], - ]; + Data = frameBuilder.BuildLan(0x0050, broadcastFlags[0], broadcastFlags[1], broadcastFlags[2], broadcastFlags[3]); } + public string Name => "LAN_SET_BROADCASTFLAGS"; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/TrackPower/SetStopCommand.cs b/src/Z21.Client/Core/Command/SystemState/TrackPower/SetStopCommand.cs index e0e8b2c..c4e26b9 100644 --- a/src/Z21.Client/Core/Command/SystemState/TrackPower/SetStopCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/TrackPower/SetStopCommand.cs @@ -1,18 +1,19 @@ -namespace Z21.Core.Command.SystemState.TrackPower +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState.TrackPower { /// /// With this command the emergency stop is activated, i.e. the locomotives are stopped but the track voltage remains switched on. /// public class SetStopCommand : IZ21Command { + public SetStopCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildXBus(0x80); + } + public string Name => "LAN_X_SET_STOP"; - public byte[] Data { get; } = - [ - 0x06, 0x00, - 0x40, 0x00, - 0x80, - 0x0 ^ 0x80 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/TrackPower/SetTrackPowerOffCommand.cs b/src/Z21.Client/Core/Command/SystemState/TrackPower/SetTrackPowerOffCommand.cs index a6616a2..4b3e06a 100644 --- a/src/Z21.Client/Core/Command/SystemState/TrackPower/SetTrackPowerOffCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/TrackPower/SetTrackPowerOffCommand.cs @@ -1,19 +1,19 @@ -namespace Z21.Core.Command.SystemState.TrackPower +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState.TrackPower { /// /// This command switches off the track voltage. /// public class SetTrackPowerOffCommand : IZ21Command { + public SetTrackPowerOffCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildXBus(0x21, 0x80); + } + public string Name => "LAN_X_SET_TRACK_POWER_OFF"; - public byte[] Data { get; } = - [ - 0x07, 0x00, - 0x40, 0x00, - 0x21, - 0x80, - 0x21 ^ 0x80 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/SystemState/TrackPower/SetTrackPowerOnCommand.cs b/src/Z21.Client/Core/Command/SystemState/TrackPower/SetTrackPowerOnCommand.cs index 5334cd8..3359afd 100644 --- a/src/Z21.Client/Core/Command/SystemState/TrackPower/SetTrackPowerOnCommand.cs +++ b/src/Z21.Client/Core/Command/SystemState/TrackPower/SetTrackPowerOnCommand.cs @@ -1,19 +1,19 @@ -namespace Z21.Core.Command.SystemState.TrackPower +using Z21.Core.Framing; + +namespace Z21.Core.Command.SystemState.TrackPower { /// /// This command switches on the track voltage, or terminates either the emergency stop or the programming mode. /// public class SetTrackPowerOnCommand : IZ21Command { + public SetTrackPowerOnCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildXBus(0x21, 0x81); + } + public string Name => "LAN_X_SET_TRACK_POWER_ON"; - public byte[] Data { get; } = - [ - 0x07, 0x00, - 0x40, 0x00, - 0x21, - 0x81, - 0x21 ^ 0x81 - ]; + public byte[] Data { get; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Command/Z21CommandFactory.cs b/src/Z21.Client/Core/Command/Z21CommandFactory.cs new file mode 100644 index 0000000..ecdc3c3 --- /dev/null +++ b/src/Z21.Client/Core/Command/Z21CommandFactory.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Z21.Core.Codecs; +using Z21.Core.Framing; + +namespace Z21.Core.Command +{ + public class Z21CommandFactory : IZ21CommandFactory + { + private readonly IServiceProvider _encodingServices; + + public Z21CommandFactory(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ILocoSpeedCodec locoSpeedCodec) + { + ArgumentNullException.ThrowIfNull(frameBuilder); + ArgumentNullException.ThrowIfNull(addressCodec); + ArgumentNullException.ThrowIfNull(locoSpeedCodec); + _encodingServices = new EncodingServiceProvider(frameBuilder, addressCodec, locoSpeedCodec); + } + + public TCommand Create(params object[] args) where TCommand : IZ21Command => + (TCommand)ActivatorUtilities.CreateInstance(_encodingServices, typeof(TCommand), args); + + private sealed class EncodingServiceProvider(IZ21FrameBuilder frameBuilder, IAddressCodec addressCodec, ILocoSpeedCodec locoSpeedCodec) : IServiceProvider + { + public object? GetService(Type serviceType) + { + if (serviceType == typeof(IZ21FrameBuilder)) + return frameBuilder; + if (serviceType == typeof(IAddressCodec)) + return addressCodec; + if (serviceType == typeof(ILocoSpeedCodec)) + return locoSpeedCodec; + return null; + } + } + } +} diff --git a/src/Z21.Client/Core/Command/ZLink/GetZLinkHardwareInfoCommand.cs b/src/Z21.Client/Core/Command/ZLink/GetZLinkHardwareInfoCommand.cs new file mode 100644 index 0000000..2451a9e --- /dev/null +++ b/src/Z21.Client/Core/Command/ZLink/GetZLinkHardwareInfoCommand.cs @@ -0,0 +1,19 @@ +using Z21.Core.Framing; + +namespace Z21.Core.Command.ZLink +{ + /// + /// Queries the properties of a 10838 Z21 pro LINK adapter (LAN_ZLINK_GET_HWINFO, protocol §11.1.1.1). + /// + public class GetZLinkHardwareInfoCommand : IZ21Command + { + public GetZLinkHardwareInfoCommand(IZ21FrameBuilder frameBuilder) + { + Data = frameBuilder.BuildLan(0x00E8, 0x06); + } + + public string Name => "LAN_ZLINK_GET_HWINFO"; + + public byte[] Data { get; } + } +} diff --git a/src/Z21.Client/Core/Exception/LocoSpeedOutOfRangeException.cs b/src/Z21.Client/Core/Exception/LocoSpeedOutOfRangeException.cs index 1ed550b..c067028 100644 --- a/src/Z21.Client/Core/Exception/LocoSpeedOutOfRangeException.cs +++ b/src/Z21.Client/Core/Exception/LocoSpeedOutOfRangeException.cs @@ -10,11 +10,11 @@ public static void ThrowIfOutOfRange(DccSpeedMode dccSpeedMode, ushort locoSpeed switch (dccSpeedMode) { case DccSpeedMode.Steps14 when locoSpeed > 14: - throw new LocoSpeedOutOfRangeException($"{nameof(DccSpeedMode.Steps14)} allows for a maximum speed of 13 steps.", nameof(locoSpeed)); + throw new LocoSpeedOutOfRangeException($"{nameof(DccSpeedMode.Steps14)} allows for a maximum speed of 14 steps.", nameof(locoSpeed)); case DccSpeedMode.Steps28 when locoSpeed > 28: - throw new LocoSpeedOutOfRangeException($"{nameof(DccSpeedMode.Steps28)} allows for a maximum speed of 25 steps.", nameof(locoSpeed)); + throw new LocoSpeedOutOfRangeException($"{nameof(DccSpeedMode.Steps28)} allows for a maximum speed of 28 steps.", nameof(locoSpeed)); case DccSpeedMode.Steps128 when locoSpeed > 126: - throw new LocoSpeedOutOfRangeException($"{nameof(DccSpeedMode.Steps128)} allows for a maximum speed of 125 steps.", nameof(locoSpeed)); + throw new LocoSpeedOutOfRangeException($"{nameof(DccSpeedMode.Steps128)} allows for a maximum speed of 126 steps.", nameof(locoSpeed)); default: return; } diff --git a/src/Z21.Client/Core/Exception/MtuPayloadLengthExceededException.cs b/src/Z21.Client/Core/Exception/MtuPayloadLengthExceededException.cs index d3c7e03..70b1f9c 100644 --- a/src/Z21.Client/Core/Exception/MtuPayloadLengthExceededException.cs +++ b/src/Z21.Client/Core/Exception/MtuPayloadLengthExceededException.cs @@ -6,8 +6,8 @@ public class MtuPayloadLengthExceededException(string message) : InvalidOperatio { public static void ThrowIfExceeded(byte[] datagram) { - if (datagram.Length > Z21Client.MaxUdpPayload) - throw new MtuPayloadLengthExceededException($"Combined UDP payload length '{datagram.Length}' exceeds MTU size '{Z21Client.MaxUdpPayload}'."); + if (datagram.Length > Z21CommandStation.MaxUdpPayload) + throw new MtuPayloadLengthExceededException($"Combined UDP payload length '{datagram.Length}' exceeds MTU size '{Z21CommandStation.MaxUdpPayload}'."); } } } \ No newline at end of file diff --git a/src/Z21.Client/Core/Exception/NotConnectedException.cs b/src/Z21.Client/Core/Exception/NotConnectedException.cs new file mode 100644 index 0000000..f6c4528 --- /dev/null +++ b/src/Z21.Client/Core/Exception/NotConnectedException.cs @@ -0,0 +1,11 @@ +using System; + +namespace Z21.Core.Exception +{ + /// + /// Thrown when a command is sent before the command station has been connected. + /// + public class NotConnectedException(string message) : InvalidOperationException(message) + { + } +} diff --git a/src/Z21.Client/Core/Framing/IZ21FrameBuilder.cs b/src/Z21.Client/Core/Framing/IZ21FrameBuilder.cs new file mode 100644 index 0000000..24c751e --- /dev/null +++ b/src/Z21.Client/Core/Framing/IZ21FrameBuilder.cs @@ -0,0 +1,26 @@ +namespace Z21.Core.Framing +{ + /// + /// Assembles outbound Z21 LAN frames, prepending the little-endian DataLen prefix and (for X-Bus + /// frames) appending the trailing XOR checksum. + /// + public interface IZ21FrameBuilder + { + /// + /// Builds a plain LAN frame: [DataLen][header][payload], with no checksum. + /// + byte[] BuildLan(ushort header, params byte[] payload); + + /// + /// Builds an X-Bus frame under LAN header 0x40 0x00: [DataLen][0x40 0x00][xHeader][data][XOR], + /// where the XOR runs over the X-header and data bytes. + /// + byte[] BuildXBus(byte xHeader, params byte[] data); + + /// + /// Builds a LAN frame that carries a trailing XOR checksum over its data bytes (used by non X-Bus + /// LAN messages such as LAN_FAST_CLOCK_CONTROL): [DataLen][header][data][XOR]. + /// + byte[] BuildLanChecksummed(ushort header, params byte[] data); + } +} diff --git a/src/Z21.Client/Core/Framing/Z21FrameBuilder.cs b/src/Z21.Client/Core/Framing/Z21FrameBuilder.cs new file mode 100644 index 0000000..172dfd2 --- /dev/null +++ b/src/Z21.Client/Core/Framing/Z21FrameBuilder.cs @@ -0,0 +1,52 @@ +using System; + +namespace Z21.Core.Framing +{ + public class Z21FrameBuilder : IZ21FrameBuilder + { + public byte[] BuildLan(ushort header, params byte[] payload) + { + ArgumentNullException.ThrowIfNull(payload); + + ushort length = (ushort)(4 + payload.Length); + byte[] frame = new byte[length]; + frame[0] = (byte)(length & 0xFF); + frame[1] = (byte)(length >> 8); + frame[2] = (byte)(header & 0xFF); + frame[3] = (byte)(header >> 8); + Array.Copy(payload, 0, frame, 4, payload.Length); + return frame; + } + + public byte[] BuildXBus(byte xHeader, params byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + + byte[] xBusPayload = new byte[data.Length + 2]; + xBusPayload[0] = xHeader; + Array.Copy(data, 0, xBusPayload, 1, data.Length); + + byte xor = xHeader; + foreach (byte value in data) + xor ^= value; + xBusPayload[^1] = xor; + + return BuildLan(0x0040, xBusPayload); + } + + public byte[] BuildLanChecksummed(ushort header, params byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + + byte[] payload = new byte[data.Length + 1]; + Array.Copy(data, 0, payload, 0, data.Length); + + byte xor = 0; + foreach (byte value in data) + xor ^= value; + payload[^1] = xor; + + return BuildLan(header, payload); + } + } +} diff --git a/src/Z21.Client/Core/Framing/Z21FrameReader.cs b/src/Z21.Client/Core/Framing/Z21FrameReader.cs new file mode 100644 index 0000000..2e5a911 --- /dev/null +++ b/src/Z21.Client/Core/Framing/Z21FrameReader.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using CommandStation.Framing; +using Microsoft.Extensions.Logging; + +namespace Z21.Core.Framing +{ + /// + /// Splits the Z21 byte stream into frames using the leading little-endian DataLen prefix, + /// buffering any partial trailing frame until the rest of its bytes arrive. + /// + public class Z21FrameReader : IFrameReader + { + /// + /// Upper bound for a single frame length. Equals the IPv4-safe UDP payload limit; any declared + /// DataLen above this is treated as a corrupt prefix and the buffer is resynchronised. + /// + private const int MaxFrameLength = 1472; + + private readonly ILogger? _logger; + private readonly List _buffer = []; + + public Z21FrameReader(ILogger? logger = null) + { + _logger = logger; + } + + public event EventHandler? OnFrameReceived; + + public void Append(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + _buffer.AddRange(data); + + int offset = 0; + while (offset + 2 <= _buffer.Count) + { + ushort dataLen = (ushort)(_buffer[offset] | (_buffer[offset + 1] << 8)); + + if (dataLen == 0 || dataLen > MaxFrameLength) + { + _logger?.LogError("Z21FrameReader read an out-of-range frame length {dataLen}; discarding buffered bytes.", dataLen); + _buffer.Clear(); + return; + } + + if (offset + dataLen > _buffer.Count) + break; + + byte[] frame = new byte[dataLen]; + _buffer.CopyTo(offset, frame, 0, dataLen); + offset += dataLen; + OnFrameReceived?.Invoke(this, new FrameReceivedEventArgs(frame)); + } + + if (offset > 0) + _buffer.RemoveRange(0, offset); + } + } +} diff --git a/src/Z21.Client/Core/Helper/AddressHelper.cs b/src/Z21.Client/Core/Helper/AddressHelper.cs deleted file mode 100644 index 9875864..0000000 --- a/src/Z21.Client/Core/Helper/AddressHelper.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace Z21.Core.Helper -{ - public static class AddressHelper - { - public static (byte lsb, byte msb) SplitLocoAddress(ushort address) - { - byte lsb = (byte)(address & 0xFF); - byte msb = (byte)((address >> 8) & 0xFF); - - if (address >= 128) - msb |= 0xC0; - - return (lsb, msb); - } - - /// Thrown when > is smaller than 1. - public static (byte lsb, byte msb) SplitAccessoryAddress(ushort address) - { - if (address < 1) - throw new ArgumentOutOfRangeException(nameof(address), address, "Smallest address is 1"); - - ushort dccAddress = (ushort)(address - 1); - byte msb = (byte)((dccAddress >> 8) & 0xFF); - byte lsb = (byte)(dccAddress & 0xFF); - return (lsb, msb); - } - - public static ushort CombineAccessoryAddress(byte lsb, byte msb) - { - return (ushort)((msb << 8) + lsb + 1); - } - } -} \ No newline at end of file diff --git a/src/Z21.Client/Core/Helper/DelayedAction.cs b/src/Z21.Client/Core/Helper/DelayedAction.cs index 39af772..72f1167 100644 --- a/src/Z21.Client/Core/Helper/DelayedAction.cs +++ b/src/Z21.Client/Core/Helper/DelayedAction.cs @@ -4,7 +4,7 @@ namespace Z21.Core.Helper { - public class DelayedAction + public class DelayedAction : IDisposable { private readonly Timer _connectionKeepAlive; @@ -24,5 +24,13 @@ public void Delay() _connectionKeepAlive.Stop(); _connectionKeepAlive.Start(); } + + public void Stop() => _connectionKeepAlive.Stop(); + + public void Dispose() + { + _connectionKeepAlive.Dispose(); + GC.SuppressFinalize(this); + } } } \ No newline at end of file diff --git a/src/Z21.Client/Core/Helper/LocoSpeedHelper.cs b/src/Z21.Client/Core/Helper/LocoSpeedHelper.cs deleted file mode 100644 index 0ed5559..0000000 --- a/src/Z21.Client/Core/Helper/LocoSpeedHelper.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using Z21.Core.Model; - -namespace Z21.Core.Helper -{ - public class LocoSpeedHelper - { - /// - /// Calculates the dcc step that should send to the Z21. - /// - /// - /// - /// - public static ushort CalculateDccSpeed(DccSpeedMode dccSpeedMode, ushort speedStep) => dccSpeedMode switch - { - DccSpeedMode.Steps14 when speedStep > 0 => (ushort)(speedStep + 1), - DccSpeedMode.Steps28 when speedStep > 0 => CalculateDcc28DccSpeed(speedStep + 3), - DccSpeedMode.Steps128 when speedStep > 0 => (ushort)(speedStep + 1), - _ => speedStep - }; - - private static ushort CalculateDcc28DccSpeed(int speedStep) - { - double dcc14Speed = speedStep / 2.0; - int dccSpeed = (int)Math.Floor(dcc14Speed); - - if (dcc14Speed % 1 != 0) - dccSpeed |= 0x10; - return (ushort)dccSpeed; - } - - /// - /// Calculates the dcc speed step that will be sent to event subscribers. - /// - /// - /// - /// - public static ushort CalculateSpeedStep(DccSpeedMode dccSpeedMode, ushort dccSpeed) => dccSpeedMode switch - { - DccSpeedMode.Steps14 when dccSpeed > 1 => (ushort)(dccSpeed - 1), - DccSpeedMode.Steps28 when dccSpeed > 0 => CalculateDcc28SpeedStep(dccSpeed), - DccSpeedMode.Steps128 when dccSpeed > 1 => (ushort)(dccSpeed - 1), - _ => 0 - }; - - private readonly static Dictionary CalculateDcc28SpeedStepLookup = new() - { - { 0, 0 }, { 16, 0 }, - { 1, 0 }, { 17, 0 }, - { 2, 1 }, { 18, 2 }, - { 3, 3 }, { 19, 4 }, - { 4, 5 }, { 20, 6 }, - { 5, 7 }, { 21, 8 }, - { 6, 9 }, { 22, 10 }, - { 7, 11 }, { 23, 12 }, - { 8, 13 }, { 24, 14 }, - { 9, 15 }, { 25, 16 }, - { 10, 17 }, { 26, 18 }, - { 11, 19 }, { 27, 20 }, - { 12, 21 }, { 28, 22 }, - { 13, 23 }, { 29, 24 }, - { 14, 25 }, { 30, 26 }, - { 15, 27 }, { 31, 28 } - }; - - - private static ushort CalculateDcc28SpeedStep(ushort dccSpeed) - { - return CalculateDcc28SpeedStepLookup[dccSpeed]; - } - } -} \ No newline at end of file diff --git a/src/Z21.Client/Core/IZ21Client.cs b/src/Z21.Client/Core/IZ21Client.cs deleted file mode 100644 index ae7e66a..0000000 --- a/src/Z21.Client/Core/IZ21Client.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Threading.Tasks; -using Z21.Core.Command; -using Z21.Core.Exception; -using Z21.Core.Model.EventArgs; - -namespace Z21.Core -{ - public interface IZ21Client - { - event EventHandler? OnConnectionChanged; - - /// - /// Sends to the Z21. - /// - /// Thrown if command length exceeds max udp payload length. - /// Thrown when has not yet been called. - Task SendCommandsAsync(params IZ21Command[] z21Commands); - - Task ConnectAsync(); - - bool IsConnected { get; } - } -} \ No newline at end of file diff --git a/src/Z21.Client/Core/IZ21CommandStation.cs b/src/Z21.Client/Core/IZ21CommandStation.cs new file mode 100644 index 0000000..8ff7e9b --- /dev/null +++ b/src/Z21.Client/Core/IZ21CommandStation.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using CommandStation; +using Z21.Core.Command; + +namespace Z21.Core +{ + /// + /// The Z21 command station: the protocol-agnostic capabilities plus a Z21-specific raw escape hatch + /// for sending hand-built commands. + /// + public interface IZ21CommandStation : ICommandStation, ILocoControl, IAccessoryControl, ITrackPowerControl, ISystemInfoProvider + { + /// + /// Factory for building raw Z21 commands to pass to . + /// + IZ21CommandFactory Commands { get; } + + /// + /// Sends one or more raw commands in a single UDP packet. + /// + Task SendCommandsAsync(params IZ21Command[] commands); + } +} diff --git a/src/Z21.Client/Core/Model/BoosterSystemState.cs b/src/Z21.Client/Core/Model/BoosterSystemState.cs new file mode 100644 index 0000000..51cae2c --- /dev/null +++ b/src/Z21.Client/Core/Model/BoosterSystemState.cs @@ -0,0 +1,21 @@ +namespace Z21.Core.Model +{ + /// + /// zLink booster system state (LAN_BOOSTER_SYSTEMSTATE_DATACHANGED, protocol §11.2.4). Currents + /// are in mA, temperatures in °C, voltages in mV. The four central-state bytes are raw bit masks. + /// + public record BoosterSystemState( + short Booster1MainCurrent, + short Booster2MainCurrent, + short Booster1FilteredMainCurrent, + short Booster2FilteredMainCurrent, + short Booster1Temperature, + short Booster2Temperature, + ushort SupplyVoltage, + ushort Booster1VccVoltage, + ushort Booster2VccVoltage, + byte CentralState, + byte CentralStateEx, + byte CentralStateEx2, + byte CentralStateEx3); +} diff --git a/src/Z21.Client/Core/Model/CanBoosterState.cs b/src/Z21.Client/Core/Model/CanBoosterState.cs new file mode 100644 index 0000000..99f0d5c --- /dev/null +++ b/src/Z21.Client/Core/Model/CanBoosterState.cs @@ -0,0 +1,28 @@ +using System; + +namespace Z21.Core.Model +{ + /// + /// CAN booster state bit mask (LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD Booster_State, protocol §10.2.3). + /// + [Flags] + public enum CanBoosterState : ushort + { + None = 0x0000, + + /// Brake generator active (ZCAN SSP). + BrakeGeneratorActive = 0x0001, + + /// Short circuit at the output stage (ZCAN UES). + ShortCircuit = 0x0020, + + /// Track voltage is switched off. + TrackVoltageOff = 0x0080, + + /// Booster output disabled by the user (from booster FW V1.11). + OutputDisabled = 0x0100, + + /// RailCom cutout active. + RailComActive = 0x0800 + } +} diff --git a/src/Z21.Client/Core/Model/CanBoosterSystemState.cs b/src/Z21.Client/Core/Model/CanBoosterSystemState.cs new file mode 100644 index 0000000..0421537 --- /dev/null +++ b/src/Z21.Client/Core/Model/CanBoosterSystemState.cs @@ -0,0 +1,7 @@ +namespace Z21.Core.Model +{ + /// + /// CAN booster system state (LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD, protocol §10.2.3). + /// + public record CanBoosterSystemState(ushort NetworkId, ushort OutputPort, CanBoosterState State, ushort VccVoltage, ushort Current); +} diff --git a/src/Z21.Client/Core/Model/CanDetectorData.cs b/src/Z21.Client/Core/Model/CanDetectorData.cs new file mode 100644 index 0000000..4d37200 --- /dev/null +++ b/src/Z21.Client/Core/Model/CanDetectorData.cs @@ -0,0 +1,8 @@ +namespace Z21.Core.Model +{ + /// + /// A CAN occupancy detector report (LAN_CAN_DETECTOR, protocol §10.1). The meaning of + /// / depends on . + /// + public record CanDetectorData(ushort NetworkId, ushort ModuleAddress, byte Port, byte Type, ushort Value1, ushort Value2); +} diff --git a/src/Z21.Client/Core/Model/EventArgs/BoosterDescriptionReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/BoosterDescriptionReceivedEventArgs.cs new file mode 100644 index 0000000..5a1875e --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/BoosterDescriptionReceivedEventArgs.cs @@ -0,0 +1,10 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries the description of a zLink booster (LAN_BOOSTER_GET_DESCRIPTION reply). + /// + public class BoosterDescriptionReceivedEventArgs(string name) : System.EventArgs + { + public string Name { get; } = name; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/BoosterSystemStateReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/BoosterSystemStateReceivedEventArgs.cs new file mode 100644 index 0000000..98f3ab8 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/BoosterSystemStateReceivedEventArgs.cs @@ -0,0 +1,12 @@ +using Z21.Core.Model; + +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries a zLink booster system state (LAN_BOOSTER_SYSTEMSTATE_DATACHANGED). + /// + public class BoosterSystemStateReceivedEventArgs(BoosterSystemState state) : System.EventArgs + { + public BoosterSystemState State { get; } = state; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/CanBoosterSystemStateReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/CanBoosterSystemStateReceivedEventArgs.cs new file mode 100644 index 0000000..7c64374 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/CanBoosterSystemStateReceivedEventArgs.cs @@ -0,0 +1,12 @@ +using Z21.Core.Model; + +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries a CAN booster system state (LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD). + /// + public class CanBoosterSystemStateReceivedEventArgs(CanBoosterSystemState state) : System.EventArgs + { + public CanBoosterSystemState State { get; } = state; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/CanDetectorReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/CanDetectorReceivedEventArgs.cs new file mode 100644 index 0000000..3a11f36 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/CanDetectorReceivedEventArgs.cs @@ -0,0 +1,12 @@ +using Z21.Core.Model; + +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries a CAN occupancy detector report (LAN_CAN_DETECTOR). + /// + public class CanDetectorReceivedEventArgs(CanDetectorData data) : System.EventArgs + { + public CanDetectorData Data { get; } = data; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/CanDeviceDescriptionReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/CanDeviceDescriptionReceivedEventArgs.cs new file mode 100644 index 0000000..218e215 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/CanDeviceDescriptionReceivedEventArgs.cs @@ -0,0 +1,12 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries the description of a CAN booster (LAN_CAN_DEVICE_GET_DESCRIPTION reply). + /// + public class CanDeviceDescriptionReceivedEventArgs(ushort networkId, string name) : System.EventArgs + { + public ushort NetworkId { get; } = networkId; + + public string Name { get; } = name; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/ConnectionChangedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/ConnectionChangedEventArgs.cs deleted file mode 100644 index 2052747..0000000 --- a/src/Z21.Client/Core/Model/EventArgs/ConnectionChangedEventArgs.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Z21.Core.Model.EventArgs -{ - public class ConnectionChangedEventArgs(bool isConnected) : System.EventArgs - { - public bool IsConnected { get; } = isConnected; - } -} \ No newline at end of file diff --git a/src/Z21.Client/Core/Model/EventArgs/CvResultReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/CvResultReceivedEventArgs.cs new file mode 100644 index 0000000..8ca713d --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/CvResultReceivedEventArgs.cs @@ -0,0 +1,12 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries a positive CV programming result (LAN_X_CV_RESULT): the CV address (0 = CV1) and its value. + /// + public class CvResultReceivedEventArgs(ushort cvAddress, byte value) : System.EventArgs + { + public ushort CvAddress { get; } = cvAddress; + + public byte Value { get; } = value; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/DecoderDescriptionReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/DecoderDescriptionReceivedEventArgs.cs new file mode 100644 index 0000000..373ccc2 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/DecoderDescriptionReceivedEventArgs.cs @@ -0,0 +1,10 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries the description of a zLink decoder (LAN_DECODER_GET_DESCRIPTION reply). + /// + public class DecoderDescriptionReceivedEventArgs(string name) : System.EventArgs + { + public string Name { get; } = name; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/FastClockDataReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/FastClockDataReceivedEventArgs.cs new file mode 100644 index 0000000..3f37c0b --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/FastClockDataReceivedEventArgs.cs @@ -0,0 +1,12 @@ +using Z21.Core.Model; + +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries the current model time (LAN_FAST_CLOCK_DATA). + /// + public class FastClockDataReceivedEventArgs(FastClockData data) : System.EventArgs + { + public FastClockData Data { get; } = data; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/FastClockSettingsReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/FastClockSettingsReceivedEventArgs.cs new file mode 100644 index 0000000..12415e2 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/FastClockSettingsReceivedEventArgs.cs @@ -0,0 +1,12 @@ +using Z21.Core.Model; + +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries the persistent fast-clock settings (LAN_FAST_CLOCK_SETTINGS_GET reply). + /// + public class FastClockSettingsReceivedEventArgs(FastClockSettingsData settings) : System.EventArgs + { + public FastClockSettingsData Settings { get; } = settings; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/HardwareInfoEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/HardwareInfoEventArgs.cs index 6bd93e6..84da762 100644 --- a/src/Z21.Client/Core/Model/EventArgs/HardwareInfoEventArgs.cs +++ b/src/Z21.Client/Core/Model/EventArgs/HardwareInfoEventArgs.cs @@ -1,7 +1,12 @@ namespace Z21.Core.Model.EventArgs { - public class HardwareInfoEventArgs(int z21HardwareType) : System.EventArgs + public class HardwareInfoEventArgs(int z21HardwareType, int firmwareVersion) : System.EventArgs { public int Z21HardwareType { get; init; } = z21HardwareType; + + /// + /// Raw 32-bit firmware version from the HWINFO reply (BCD; e.g. 0x0143 means firmware 1.43). + /// + public int FirmwareVersion { get; init; } = firmwareVersion; } } \ No newline at end of file diff --git a/src/Z21.Client/Core/Model/EventArgs/LocoNetDetectorReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/LocoNetDetectorReceivedEventArgs.cs new file mode 100644 index 0000000..ee24bad --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/LocoNetDetectorReceivedEventArgs.cs @@ -0,0 +1,15 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries a LocoNet occupancy detector report (LAN_LOCONET_DETECTOR). The meaning of + /// depends on (see protocol §9.5). + /// + public class LocoNetDetectorReceivedEventArgs(byte type, ushort reportAddress, byte[] info) : System.EventArgs + { + public byte Type { get; } = type; + + public ushort ReportAddress { get; } = reportAddress; + + public byte[] Info { get; } = info; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/LocoNetDispatchAddressReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/LocoNetDispatchAddressReceivedEventArgs.cs new file mode 100644 index 0000000..5d8655f --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/LocoNetDispatchAddressReceivedEventArgs.cs @@ -0,0 +1,13 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries the result of a LocoNet dispatch request (LAN_LOCONET_DISPATCH_ADDR). + /// 0 indicates the dispatch failed; a positive value is the assigned LocoNet slot. + /// + public class LocoNetDispatchAddressReceivedEventArgs(ushort locoAddress, byte slot) : System.EventArgs + { + public ushort LocoAddress { get; } = locoAddress; + + public byte Slot { get; } = slot; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/LocoNetMessageReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/LocoNetMessageReceivedEventArgs.cs new file mode 100644 index 0000000..590b502 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/LocoNetMessageReceivedEventArgs.cs @@ -0,0 +1,11 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries a raw LocoNet message (including its checksum) tunneled through the Z21 + /// (LAN_LOCONET_Z21_RX/_TX/_FROM_LAN). + /// + public class LocoNetMessageReceivedEventArgs(byte[] message) : System.EventArgs + { + public byte[] Message { get; } = message; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/RailComDataReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/RailComDataReceivedEventArgs.cs new file mode 100644 index 0000000..155b1ca --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/RailComDataReceivedEventArgs.cs @@ -0,0 +1,10 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries parsed RailCom data (LAN_RAILCOM_DATACHANGED). + /// + public class RailComDataReceivedEventArgs(RailComData data) : System.EventArgs + { + public RailComData Data { get; } = data; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/ResponseReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/ResponseReceivedEventArgs.cs deleted file mode 100644 index 00ac865..0000000 --- a/src/Z21.Client/Core/Model/EventArgs/ResponseReceivedEventArgs.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Z21.Core.Model.EventArgs -{ - public class ResponseReceivedEventArgs(byte[] response) : System.EventArgs - { - public byte[] Response { get; } = response; - } -} \ No newline at end of file diff --git a/src/Z21.Client/Core/Model/EventArgs/RmBusDataReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/RmBusDataReceivedEventArgs.cs new file mode 100644 index 0000000..502f20d --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/RmBusDataReceivedEventArgs.cs @@ -0,0 +1,13 @@ +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries an R-BUS feedback status change (LAN_RMBUS_DATACHANGED): the group index and the ten + /// status bytes (one byte per feedback module, one bit per input). + /// + public class RmBusDataReceivedEventArgs(byte groupIndex, byte[] feedbackStates) : System.EventArgs + { + public byte GroupIndex { get; } = groupIndex; + + public byte[] FeedbackStates { get; } = feedbackStates; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/SignalDecoderSystemStateReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/SignalDecoderSystemStateReceivedEventArgs.cs new file mode 100644 index 0000000..9bf12b8 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/SignalDecoderSystemStateReceivedEventArgs.cs @@ -0,0 +1,12 @@ +using Z21.Core.Model; + +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries a 10837 signal decoder system state (LAN_DECODER_SYSTEMSTATE_DATACHANGED). + /// + public class SignalDecoderSystemStateReceivedEventArgs(SignalDecoderSystemState state) : System.EventArgs + { + public SignalDecoderSystemState State { get; } = state; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/SwitchDecoderSystemStateReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/SwitchDecoderSystemStateReceivedEventArgs.cs new file mode 100644 index 0000000..bc1fb82 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/SwitchDecoderSystemStateReceivedEventArgs.cs @@ -0,0 +1,12 @@ +using Z21.Core.Model; + +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries a 10836 switch decoder system state (LAN_DECODER_SYSTEMSTATE_DATACHANGED). + /// + public class SwitchDecoderSystemStateReceivedEventArgs(SwitchDecoderSystemState state) : System.EventArgs + { + public SwitchDecoderSystemState State { get; } = state; + } +} diff --git a/src/Z21.Client/Core/Model/EventArgs/ZLinkHardwareInfoReceivedEventArgs.cs b/src/Z21.Client/Core/Model/EventArgs/ZLinkHardwareInfoReceivedEventArgs.cs new file mode 100644 index 0000000..af8e842 --- /dev/null +++ b/src/Z21.Client/Core/Model/EventArgs/ZLinkHardwareInfoReceivedEventArgs.cs @@ -0,0 +1,12 @@ +using Z21.Core.Model; + +namespace Z21.Core.Model.EventArgs +{ + /// + /// Carries the hardware information of a Z21 pro LINK (LAN_ZLINK_GET_HWINFO reply). + /// + public class ZLinkHardwareInfoReceivedEventArgs(ZLinkHardwareInfo info) : System.EventArgs + { + public ZLinkHardwareInfo Info { get; } = info; + } +} diff --git a/src/Z21.Client/Core/Model/FastClockAction.cs b/src/Z21.Client/Core/Model/FastClockAction.cs new file mode 100644 index 0000000..f74cb27 --- /dev/null +++ b/src/Z21.Client/Core/Model/FastClockAction.cs @@ -0,0 +1,17 @@ +namespace Z21.Core.Model +{ + /// + /// A parameterless fast-clock control action for LAN_FAST_CLOCK_CONTROL (protocol §12.1). + /// + public enum FastClockAction + { + /// Read the current model time. + Read, + + /// Start (resume) the model clock. + Start, + + /// Stop (pause) the model clock. + Stop + } +} diff --git a/src/Z21.Client/Core/Model/FastClockData.cs b/src/Z21.Client/Core/Model/FastClockData.cs new file mode 100644 index 0000000..f2a90f6 --- /dev/null +++ b/src/Z21.Client/Core/Model/FastClockData.cs @@ -0,0 +1,7 @@ +namespace Z21.Core.Model +{ + /// + /// The current model time reported by the Z21 (LAN_FAST_CLOCK_DATA, protocol §12.2). + /// + public record FastClockData(byte Day, byte Hour, byte Minute, byte Second, byte Rate, bool IsStopped, bool IsHalted, FastClockSettings Settings); +} diff --git a/src/Z21.Client/Core/Model/FastClockSettings.cs b/src/Z21.Client/Core/Model/FastClockSettings.cs new file mode 100644 index 0000000..f891fce --- /dev/null +++ b/src/Z21.Client/Core/Model/FastClockSettings.cs @@ -0,0 +1,31 @@ +using System; + +namespace Z21.Core.Model +{ + /// + /// Persistent fast-clock setting flags (FcSettings, protocol §12.3). + /// + [Flags] + public enum FastClockSettings : byte + { + None = 0x00, + + /// Enable polled output on the LocoNet. + LocoNetEnabled = 0x01, + + /// Enable the broadcast on the X-BUS. + XBusEnabled = 0x02, + + /// Enable the DCC broadcast on the track. + DccEnabled = 0x08, + + /// Enable the multicast to MRclock clients. + MRclockEnabled = 0x10, + + /// Automatically halt the model time on emergency stop. + EmergencyHaltEnabled = 0x40, + + /// The fast clock is enabled. + Enabled = 0x80 + } +} diff --git a/src/Z21.Client/Core/Model/FastClockSettingsData.cs b/src/Z21.Client/Core/Model/FastClockSettingsData.cs new file mode 100644 index 0000000..98efed4 --- /dev/null +++ b/src/Z21.Client/Core/Model/FastClockSettingsData.cs @@ -0,0 +1,7 @@ +namespace Z21.Core.Model +{ + /// + /// The persistent fast-clock settings (LAN_FAST_CLOCK_SETTINGS_GET reply, protocol §12.3). + /// + public record FastClockSettingsData(FastClockSettings Settings, byte Rate, byte StartDayHour, byte StartMinute); +} diff --git a/src/Z21.Client/Core/Model/FirmwareVersion.cs b/src/Z21.Client/Core/Model/FirmwareVersion.cs deleted file mode 100644 index a6c4fba..0000000 --- a/src/Z21.Client/Core/Model/FirmwareVersion.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; - -namespace Z21.Core.Model -{ - public sealed class FirmwareVersion(int major, int minor) : IComparable, IEquatable - { - public int Major { get; } = major; - - public int Minor { get; } = minor; - - public string Firmware { get; } = major + "." + minor; - - override public string ToString() => Firmware; - - public bool Equals(FirmwareVersion? other) => Major == other?.Major && Minor == other.Minor; - - override public bool Equals(object? obj) => obj is FirmwareVersion other && Equals(other); - - override public int GetHashCode() => HashCode.Combine(Major, Minor); - - public int CompareTo(FirmwareVersion? other) - { - int majorCmp = Major.CompareTo(other?.Major); - return majorCmp != 0 ? majorCmp : Minor.CompareTo(other?.Minor); - } - - public static bool operator <(FirmwareVersion left, FirmwareVersion right) => left.CompareTo(right) < 0; - - public static bool operator >(FirmwareVersion left, FirmwareVersion right) => left.CompareTo(right) > 0; - - public static bool operator <=(FirmwareVersion left, FirmwareVersion right) => left.CompareTo(right) <= 0; - - public static bool operator >=(FirmwareVersion left, FirmwareVersion right) => left.CompareTo(right) >= 0; - - public static bool operator ==(FirmwareVersion left, FirmwareVersion right) => left.Equals(right); - - public static bool operator !=(FirmwareVersion left, FirmwareVersion right) => !left.Equals(right); - } -} \ No newline at end of file diff --git a/src/Z21.Client/Core/Model/LocoFunctionGroup.cs b/src/Z21.Client/Core/Model/LocoFunctionGroup.cs new file mode 100644 index 0000000..53ae0a5 --- /dev/null +++ b/src/Z21.Client/Core/Model/LocoFunctionGroup.cs @@ -0,0 +1,39 @@ +namespace Z21.Core.Model +{ + /// + /// Identifies a locomotive function group for LAN_X_SET_LOCO_FUNCTION_GROUP. The enum value is + /// the wire "Group" byte; each group carries up to 8 functions in a single command (see protocol §4.3.2). + /// + public enum LocoFunctionGroup : byte + { + /// F0, F4, F3, F2, F1. + Group1 = 0x20, + + /// F5–F8. + Group2 = 0x21, + + /// F9–F12. + Group3 = 0x22, + + /// F13–F20. + Group4 = 0x23, + + /// F21–F28. + Group5 = 0x28, + + /// F29–F36. + Group6 = 0x29, + + /// F37–F44. + Group7 = 0x2A, + + /// F45–F52. + Group8 = 0x2B, + + /// F53–F60. + Group9 = 0x50, + + /// F61–F68. + Group10 = 0x51 + } +} diff --git a/src/Z21.Client/Core/Model/RailComData.cs b/src/Z21.Client/Core/Model/RailComData.cs new file mode 100644 index 0000000..8a6c9a8 --- /dev/null +++ b/src/Z21.Client/Core/Model/RailComData.cs @@ -0,0 +1,7 @@ +namespace Z21.Core.Model +{ + /// + /// RailCom data reported by the Z21 for a decoder (LAN_RAILCOM_DATACHANGED, protocol §8.1). + /// + public record RailComData(ushort LocoAddress, uint ReceiveCounter, ushort ErrorCounter, RailComOptions Options, byte Speed, byte QualityOfService); +} diff --git a/src/Z21.Client/Core/Model/RailComOptions.cs b/src/Z21.Client/Core/Model/RailComOptions.cs new file mode 100644 index 0000000..7e63fae --- /dev/null +++ b/src/Z21.Client/Core/Model/RailComOptions.cs @@ -0,0 +1,23 @@ +using System; + +namespace Z21.Core.Model +{ + /// + /// RailCom data option flags (LAN_RAILCOM_DATACHANGED DB Options) indicating which optional + /// fields the decoder reported. + /// + [Flags] + public enum RailComOptions : byte + { + None = 0x00, + + /// CH7 subindex 0 speed is present. + Speed1 = 0x01, + + /// CH7 subindex 1 speed is present. + Speed2 = 0x02, + + /// CH7 subindex 7 quality of service is present. + QoS = 0x04 + } +} diff --git a/src/Z21.Client/Core/Model/SignalDecoderSystemState.cs b/src/Z21.Client/Core/Model/SignalDecoderSystemState.cs new file mode 100644 index 0000000..b0c5180 --- /dev/null +++ b/src/Z21.Client/Core/Model/SignalDecoderSystemState.cs @@ -0,0 +1,21 @@ +namespace Z21.Core.Model +{ + /// + /// System state of a 10837 signal decoder (LAN_DECODER_SYSTEMSTATE_DATACHANGED, protocol §11.3.4.2). + /// Voltage is in mV. carries the current DCCext signal aspect per signal. + /// + public record SignalDecoderSystemState( + short Current, + short FilteredCurrent, + ushort Voltage, + byte CentralState, + byte CentralStateEx, + byte[] OutputStates, + byte[] BlinkStates, + byte[] SignalDccExt, + byte[] SignalCurrentAspect, + byte SignalCount, + byte[] SignalConfig, + byte[] SignalInitAspect, + ushort Address); +} diff --git a/src/Z21.Client/Core/Model/SwitchDecoderSystemState.cs b/src/Z21.Client/Core/Model/SwitchDecoderSystemState.cs new file mode 100644 index 0000000..750bf89 --- /dev/null +++ b/src/Z21.Client/Core/Model/SwitchDecoderSystemState.cs @@ -0,0 +1,20 @@ +namespace Z21.Core.Model +{ + /// + /// System state of a 10836 switch decoder (LAN_DECODER_SYSTEMSTATE_DATACHANGED, protocol §11.3.4.1). + /// Currents are in mA, voltage in mV. , and + /// each have one entry per output (8 outputs). + /// + public record SwitchDecoderSystemState( + short Current, + short FilteredCurrent, + ushort Voltage, + byte CentralState, + byte CentralStateEx, + byte[] OutputStates, + byte[] OutputConfig, + byte[] OutputDimm, + ushort Address, + ushort Address2, + byte Dimmed); +} diff --git a/src/Z21.Client/Core/Model/Z21Configuration.cs b/src/Z21.Client/Core/Model/Z21Configuration.cs deleted file mode 100644 index c9e254d..0000000 --- a/src/Z21.Client/Core/Model/Z21Configuration.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Net; - -// ReSharper disable InconsistentNaming - -namespace Z21.Core.Model -{ - public class Z21Configuration - { - private IPEndPoint _clientIpEndPoint = Defaults.IpEndPoint; - private bool _allowNatTraversal = true; - - /// - /// IPEndPoint of the Z21. - /// - public IPEndPoint ClientIPEndPoint - { - get => _clientIpEndPoint; - set - { - ArgumentNullException.ThrowIfNull(value); - if (_clientIpEndPoint.Equals(value)) - return; - - _clientIpEndPoint = value; - ConfigurationUpdated?.Invoke(this, System.EventArgs.Empty); - } - } - - /// - /// Enables or disables Network Address Translation (NAT) traversal on a UdpClient instance. - /// - public bool AllowNatTraversal - { - get => _allowNatTraversal; - set - { - if (_allowNatTraversal.Equals(value)) - return; - _allowNatTraversal = value; - ConfigurationUpdated?.Invoke(this, System.EventArgs.Empty); - } - } - - /// - /// Time it takes between a command being sent and a response being received. This Setting should not need changing! - /// - public TimeSpan ResponseTime { get; set; } = TimeSpan.FromSeconds(2); - - /// - /// Configures the default broadcast flags that should be sent to the Z21 - /// - public uint[] BroadcastFlags { get; set; } = Defaults.BroadcastFlags; - - public event EventHandler? ConfigurationUpdated; - - public static class Defaults - { - public readonly static IPEndPoint IpEndPoint = new(IPAddress.Parse("192.168.0.111"), 21105); - - public readonly static uint[] BroadcastFlags = - [ - Z21BroadcastFlags.DriveAndSwitchingMessages, - Z21BroadcastFlags.LocoInfoChangedMessages - ]; - } - } -} \ No newline at end of file diff --git a/src/Z21.Client/Core/Model/ZLinkHardwareInfo.cs b/src/Z21.Client/Core/Model/ZLinkHardwareInfo.cs new file mode 100644 index 0000000..57d1c14 --- /dev/null +++ b/src/Z21.Client/Core/Model/ZLinkHardwareInfo.cs @@ -0,0 +1,7 @@ +namespace Z21.Core.Model +{ + /// + /// Hardware information of a 10838 Z21 pro LINK adapter (LAN_ZLINK_GET_HWINFO reply, protocol §11.1.1.1). + /// + public record ZLinkHardwareInfo(ushort HardwareId, byte FirmwareMajor, byte FirmwareMinor, ushort FirmwareBuild, string MacAddress, string Name); +} diff --git a/src/Z21.Client/Core/Reflection/Z21ServiceDiscovery.cs b/src/Z21.Client/Core/Reflection/Z21ServiceDiscovery.cs new file mode 100644 index 0000000..d799a0d --- /dev/null +++ b/src/Z21.Client/Core/Reflection/Z21ServiceDiscovery.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Z21.Core.Reflection +{ + /// + /// Discovers concrete implementations of a Z21 service contract (response handlers, parsers) for + /// container registration, so both DI containers register an identical set. + /// + public sealed class Z21ServiceDiscovery + { + /// + /// Returns every concrete, non-abstract class in the Z21 assembly that implements . + /// + public IEnumerable GetImplementations(Type baseInterface) => + baseInterface.Assembly + .GetTypes() + .Where(type => type is { IsClass: true, IsAbstract: false } && baseInterface.IsAssignableFrom(type)); + + /// + /// Returns the contract interfaces an implementation should be registered against. The base contract + /// itself is included only when is true (handlers are resolved + /// as IEnumerable<base> by the dispatcher; parsers are not). + /// + public IEnumerable GetServiceInterfaces(Type implementationType, Type baseInterface, bool includeBaseInterface) => + implementationType.GetInterfaces() + .Where(serviceInterface => baseInterface.IsAssignableFrom(serviceInterface) && (includeBaseInterface || serviceInterface != baseInterface)); + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Booster/BoosterDescriptionResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Booster/BoosterDescriptionResponseHandler.cs new file mode 100644 index 0000000..79f46f4 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Booster/BoosterDescriptionResponseHandler.cs @@ -0,0 +1,45 @@ +using System; +using System.Text; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.Booster +{ + public interface IBoosterDescriptionResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnBoosterDescriptionReceived; + } + + /// + /// Reports the description of a zLink booster (LAN_BOOSTER_GET_DESCRIPTION reply, protocol §11.2.1). + /// A leading 0xFF means no description has ever been stored and is reported as an empty string. + /// + public class BoosterDescriptionResponseHandler : IBoosterDescriptionResponseHandler + { + private const int NameLength = 32; + + public event EventHandler? OnBoosterDescriptionReceived; + + public string Name => "LAN_BOOSTER_GET_DESCRIPTION"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 4 + NameLength && response[2] == 0xB8 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + string name; + if (response[4] == 0xFF) + name = string.Empty; + else + { + name = Encoding.Latin1.GetString(response, 4, NameLength); + int terminator = name.IndexOf('\0'); + if (terminator >= 0) + name = name[..terminator]; + } + OnBoosterDescriptionReceived?.Invoke(this, new(name)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Booster/BoosterSystemStateResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Booster/BoosterSystemStateResponseHandler.cs new file mode 100644 index 0000000..558d53e --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Booster/BoosterSystemStateResponseHandler.cs @@ -0,0 +1,43 @@ +using System; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.Booster +{ + public interface IBoosterSystemStateResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnBoosterSystemStateReceived; + } + + /// + /// Reports a zLink booster system state (LAN_BOOSTER_SYSTEMSTATE_DATACHANGED, protocol §11.2.4). + /// + public class BoosterSystemStateResponseHandler : IBoosterSystemStateResponseHandler + { + public event EventHandler? OnBoosterSystemStateReceived; + + public string Name => "LAN_BOOSTER_SYSTEMSTATE_DATACHANGED"; + + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 28, (2, 0xBA), (3, 0x00)); + + public void Handle(byte[] response) + { + BoosterSystemState state = new( + BitConverter.ToInt16(response, 4), + BitConverter.ToInt16(response, 6), + BitConverter.ToInt16(response, 8), + BitConverter.ToInt16(response, 10), + BitConverter.ToInt16(response, 12), + BitConverter.ToInt16(response, 14), + BitConverter.ToUInt16(response, 16), + BitConverter.ToUInt16(response, 18), + BitConverter.ToUInt16(response, 20), + response[22], + response[23], + response[24], + response[26]); + OnBoosterSystemStateReceived?.Invoke(this, new(state)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Can/CanBoosterSystemStateResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Can/CanBoosterSystemStateResponseHandler.cs new file mode 100644 index 0000000..5d82f67 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Can/CanBoosterSystemStateResponseHandler.cs @@ -0,0 +1,38 @@ +using System; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.Can +{ + public interface ICanBoosterSystemStateResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnCanBoosterSystemStateReceived; + } + + /// + /// From Z21 FW version 1.41, reports a CAN booster system state + /// (LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD, protocol §10.2.3). + /// + public class CanBoosterSystemStateResponseHandler : ICanBoosterSystemStateResponseHandler + { + public event EventHandler? OnCanBoosterSystemStateReceived; + + public string Name => "LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 14 && response[2] == 0xCA && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + ushort networkId = BitConverter.ToUInt16(response, 4); + ushort outputPort = BitConverter.ToUInt16(response, 6); + CanBoosterState state = (CanBoosterState)BitConverter.ToUInt16(response, 8); + ushort vccVoltage = BitConverter.ToUInt16(response, 10); + ushort current = BitConverter.ToUInt16(response, 12); + OnCanBoosterSystemStateReceived?.Invoke(this, new(new CanBoosterSystemState(networkId, outputPort, state, vccVoltage, current))); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Can/CanDetectorResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Can/CanDetectorResponseHandler.cs new file mode 100644 index 0000000..e79c64d --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Can/CanDetectorResponseHandler.cs @@ -0,0 +1,39 @@ +using System; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.Can +{ + public interface ICanDetectorResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnCanDetectorReceived; + } + + /// + /// From Z21 FW version 1.30, reports a CAN occupancy detector status (LAN_CAN_DETECTOR, + /// protocol §10.1). + /// + public class CanDetectorResponseHandler : ICanDetectorResponseHandler + { + public event EventHandler? OnCanDetectorReceived; + + public string Name => "LAN_CAN_DETECTOR"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 14 && response[2] == 0xC4 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + ushort networkId = BitConverter.ToUInt16(response, 4); + ushort moduleAddress = BitConverter.ToUInt16(response, 6); + byte port = response[8]; + byte type = response[9]; + ushort value1 = BitConverter.ToUInt16(response, 10); + ushort value2 = BitConverter.ToUInt16(response, 12); + OnCanDetectorReceived?.Invoke(this, new(new CanDetectorData(networkId, moduleAddress, port, type, value1, value2))); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Can/CanDeviceDescriptionResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Can/CanDeviceDescriptionResponseHandler.cs new file mode 100644 index 0000000..66a52aa --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Can/CanDeviceDescriptionResponseHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Text; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.Can +{ + public interface ICanDeviceDescriptionResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnCanDeviceDescriptionReceived; + } + + /// + /// From Z21 FW version 1.41, reports the description of a CAN booster + /// (LAN_CAN_DEVICE_GET_DESCRIPTION reply, protocol §10.2.1). + /// + public class CanDeviceDescriptionResponseHandler : ICanDeviceDescriptionResponseHandler + { + private const int NameLength = 16; + + public event EventHandler? OnCanDeviceDescriptionReceived; + + public string Name => "LAN_CAN_DEVICE_GET_DESCRIPTION"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 6 + NameLength && response[2] == 0xC8 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + ushort networkId = BitConverter.ToUInt16(response, 4); + string name = Encoding.Latin1.GetString(response, 6, NameLength); + int terminator = name.IndexOf('\0'); + if (terminator >= 0) + name = name[..terminator]; + OnCanDeviceDescriptionReceived?.Invoke(this, new(networkId, name)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Decoder/DecoderDescriptionResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Decoder/DecoderDescriptionResponseHandler.cs new file mode 100644 index 0000000..0b43dc6 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Decoder/DecoderDescriptionResponseHandler.cs @@ -0,0 +1,38 @@ +using System; +using System.Text; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.Decoder +{ + public interface IDecoderDescriptionResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnDecoderDescriptionReceived; + } + + /// + /// Reports the description of a zLink decoder (LAN_DECODER_GET_DESCRIPTION reply, protocol §11.3.1). + /// + public class DecoderDescriptionResponseHandler : IDecoderDescriptionResponseHandler + { + private const int NameLength = 32; + + public event EventHandler? OnDecoderDescriptionReceived; + + public string Name => "LAN_DECODER_GET_DESCRIPTION"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 4 + NameLength && response[2] == 0xD8 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + string name = Encoding.Latin1.GetString(response, 4, NameLength); + int terminator = name.IndexOf('\0'); + if (terminator >= 0) + name = name[..terminator]; + OnDecoderDescriptionReceived?.Invoke(this, new(name)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Decoder/DecoderSystemStateResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Decoder/DecoderSystemStateResponseHandler.cs new file mode 100644 index 0000000..8acbb1d --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Decoder/DecoderSystemStateResponseHandler.cs @@ -0,0 +1,44 @@ +using System; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseParser; + +namespace Z21.Core.ResponseHandler.Decoder +{ + public interface IDecoderSystemStateResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnSwitchDecoderSystemStateReceived; + + event EventHandler? OnSignalDecoderSystemStateReceived; + } + + /// + /// Reports a zLink decoder system state (LAN_DECODER_SYSTEMSTATE_DATACHANGED, protocol §11.3.4). + /// The switch decoder (10836) and signal decoder (10837) layouts are distinguished by the frame length. + /// + public class DecoderSystemStateResponseHandler(ISwitchDecoderSystemStateParser switchParser, ISignalDecoderSystemStateParser signalParser) : IDecoderSystemStateResponseHandler + { + private const int SwitchFrameLength = 48; + private const int SignalFrameLength = 46; + + public event EventHandler? OnSwitchDecoderSystemStateReceived; + + public event EventHandler? OnSignalDecoderSystemStateReceived; + + public string Name => "LAN_DECODER_SYSTEMSTATE_DATACHANGED"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return (response.Length == SwitchFrameLength || response.Length == SignalFrameLength) && response[2] == 0xDA && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + byte[] payload = response[4..]; + if (response.Length == SwitchFrameLength) + OnSwitchDecoderSystemStateReceived?.Invoke(this, new(switchParser.Parse(payload))); + else + OnSignalDecoderSystemStateReceived?.Invoke(this, new(signalParser.Parse(payload))); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Driving/LocoInfoResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Driving/LocoInfoResponseHandler.cs index 90004c1..8d2ff1d 100644 --- a/src/Z21.Client/Core/ResponseHandler/Driving/LocoInfoResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/Driving/LocoInfoResponseHandler.cs @@ -1,10 +1,10 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; +using Microsoft.Extensions.Logging; +using Z21.Core.Codecs; using Z21.Core.Command.Driving; using Z21.Core.Command.SystemState; -using Z21.Core.Helper; using Z21.Core.Model; using Z21.Core.Model.EventArgs; @@ -21,21 +21,21 @@ public interface ILocoInfoResponseHandler : IZ21ResponseHandler /// public class LocoInfoResponseHandler : ILocoInfoResponseHandler { + private readonly ILocoSpeedCodec _locoSpeedCodec; + private readonly ILogger? _logger; + + public LocoInfoResponseHandler(ILocoSpeedCodec locoSpeedCodec, ILogger? logger = null) + { + _locoSpeedCodec = locoSpeedCodec; + _logger = logger; + } + public string Name => "LAN_X_LOCO_INFO"; public event EventHandler? OnLocoInfoReceived; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 && response[3] == 0x00 && response[4] == 0xEF; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 5, (2, 0x40), (3, 0x00), (4, 0xEF)); public void Handle(byte[] response) { @@ -45,17 +45,16 @@ public void Handle(byte[] response) DecoderMode decoderMode = (db2 & 0x10) == 0x10 ? DecoderMode.MM : DecoderMode.DCC; bool locoIsBusy = (db2 & 0x8) == 0x8; - DccSpeedMode speedMode = (DccSpeedMode)999; - if ((db2 & 0x1) == 0x1) - speedMode = DccSpeedMode.Steps14; - if ((db2 & 0x2) == 0x2) - speedMode = DccSpeedMode.Steps28; - if ((db2 & 0x4) == 0x4) - speedMode = DccSpeedMode.Steps128; + DccSpeedMode speedMode = (db2 & 0x07) switch + { + 0x02 => DccSpeedMode.Steps28, + 0x04 => DccSpeedMode.Steps128, + _ => DccSpeedMode.Steps14 + }; byte db3 = response[8]; DrivingDirection drivingDirection = (db3 & 0x80) == 0x80 ? DrivingDirection.Forward : DrivingDirection.Backward; - ushort stepSpeed = LocoSpeedHelper.CalculateSpeedStep(speedMode, (ushort)(db3 & 0x7F)); + ushort stepSpeed = _locoSpeedCodec.CalculateSpeedStep(speedMode, (ushort)(db3 & 0x7F)); byte db4 = response[9]; bool locoContainedInDoubleTraction = (db4 & 0x40) == 0x40; @@ -71,7 +70,7 @@ public void Handle(byte[] response) ]; int functionAddressCount = 5; - for (int index = 10; index < response.Length - 2; index++) + for (int index = 10; index < response.Length - 1; index++) { BitArray functionBits = new(new[] { response[index] }); for (int temp = 0; temp < 8; temp++) @@ -80,8 +79,8 @@ public void Handle(byte[] response) } } - string function = string.Join(", ", infodata.Select(functionData => $"F{functionData.FunctionIndex}: {functionData.FunctionToggleType}")); - Console.WriteLine($"Address: {address}, DecoderMode: {decoderMode}, Busy: {locoIsBusy}, DccSpeedMode: {speedMode}, Direction: {drivingDirection}, Speed: {stepSpeed}, Double Traction: {locoContainedInDoubleTraction}, smartSearch: {smartSearch}\n{function}"); + _logger?.LogDebug("{name} address {address}, decoderMode {decoderMode}, busy {busy}, speedMode {speedMode}, direction {direction}, speed {speed}, doubleTraction {doubleTraction}, smartSearch {smartSearch}.", + Name, address, decoderMode, locoIsBusy, speedMode, drivingDirection, stepSpeed, locoContainedInDoubleTraction, smartSearch); OnLocoInfoReceived?.Invoke(this, new(new() @@ -98,6 +97,6 @@ public void Handle(byte[] response) })); } - private static FunctionToggleType GetFunctionToggleType(bool value) => value ? FunctionToggleType.On : FunctionToggleType.Off; + private FunctionToggleType GetFunctionToggleType(bool value) => value ? FunctionToggleType.On : FunctionToggleType.Off; } } \ No newline at end of file diff --git a/src/Z21.Client/Core/ResponseHandler/FastClock/FastClockDataResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/FastClock/FastClockDataResponseHandler.cs new file mode 100644 index 0000000..13454b8 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/FastClock/FastClockDataResponseHandler.cs @@ -0,0 +1,43 @@ +using System; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.FastClock +{ + public interface IFastClockDataResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnFastClockDataReceived; + } + + /// + /// From Z21 FW version 1.43, reports the current model time (LAN_FAST_CLOCK_DATA, protocol §12.2). + /// + public class FastClockDataResponseHandler : IFastClockDataResponseHandler + { + public event EventHandler? OnFastClockDataReceived; + + public string Name => "LAN_FAST_CLOCK_DATA"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 12 && response[2] == 0xCD && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + byte dayHour = response[6]; + byte day = (byte)((dayHour >> 5) & 0x07); + byte hour = (byte)(dayHour & 0x1F); + byte minute = (byte)(response[7] & 0x3F); + byte secondsByte = response[8]; + byte second = (byte)(secondsByte & 0x3F); + bool isStopped = (secondsByte & 0x80) == 0x80; + bool isHalted = (secondsByte & 0x40) == 0x40; + byte rate = (byte)(response[9] & 0x3F); + FastClockSettings settings = (FastClockSettings)response[10]; + + OnFastClockDataReceived?.Invoke(this, new(new FastClockData(day, hour, minute, second, rate, isStopped, isHalted, settings))); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/FastClock/FastClockSettingsResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/FastClock/FastClockSettingsResponseHandler.cs new file mode 100644 index 0000000..36bdaa5 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/FastClock/FastClockSettingsResponseHandler.cs @@ -0,0 +1,36 @@ +using System; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.FastClock +{ + public interface IFastClockSettingsResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnFastClockSettingsReceived; + } + + /// + /// Reports the persistent fast-clock settings (LAN_FAST_CLOCK_SETTINGS_GET reply, protocol §12.3). + /// + public class FastClockSettingsResponseHandler : IFastClockSettingsResponseHandler + { + public event EventHandler? OnFastClockSettingsReceived; + + public string Name => "LAN_FAST_CLOCK_SETTINGS_GET"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 8 && response[2] == 0xCE && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + FastClockSettings settings = (FastClockSettings)response[4]; + byte rate = response[5]; + byte startDayHour = response[6]; + byte startMinute = response[7]; + OnFastClockSettingsReceived?.Invoke(this, new(new FastClockSettingsData(settings, rate, startDayHour, startMinute))); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Feedback/RmBusDataChangedResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Feedback/RmBusDataChangedResponseHandler.cs new file mode 100644 index 0000000..50fe26b --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Feedback/RmBusDataChangedResponseHandler.cs @@ -0,0 +1,37 @@ +using System; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.Feedback +{ + public interface IRmBusDataChangedResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnRmBusDataReceived; + } + + /// + /// Reports a change on the R-BUS feedback bus (LAN_RMBUS_DATACHANGED, protocol §7.1), either + /// automatically when the corresponding broadcast is set or in response to LAN_RMBUS_GETDATA. + /// + public class RmBusDataChangedResponseHandler : IRmBusDataChangedResponseHandler + { + private const int FeedbackStateCount = 10; + + public event EventHandler? OnRmBusDataReceived; + + public string Name => "LAN_RMBUS_DATACHANGED"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 5 + FeedbackStateCount && response[2] == 0x80 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + byte groupIndex = response[4]; + byte[] feedbackStates = new byte[FeedbackStateCount]; + Buffer.BlockCopy(response, 5, feedbackStates, 0, FeedbackStateCount); + OnRmBusDataReceived?.Invoke(this, new(groupIndex, feedbackStates)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/IZ21ResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/IZ21ResponseHandler.cs index 62662f5..73d7a58 100644 --- a/src/Z21.Client/Core/ResponseHandler/IZ21ResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/IZ21ResponseHandler.cs @@ -17,5 +17,24 @@ public interface IZ21ResponseHandler public bool CanHandle(byte[] response); public void Handle(byte[] response); + + /// + /// Length-safe frame matcher shared by every response handler. Returns true only when + /// is non-null, at least bytes long, + /// and every (index, value) pair in matches. It never throws + /// on a short or null datagram, replacing the per-handler try/catch (IndexOutOfRangeException) + /// guard and the duplicated response.Length >= n checks. + /// + public bool MatchesFrame(byte[] response, int minimumLength, params (int Index, byte Value)[] expected) + { + if (response is null || response.Length < minimumLength) + return false; + + foreach ((int index, byte value) in expected) + if ((uint)index >= (uint)response.Length || response[index] != value) + return false; + + return true; + } } } \ No newline at end of file diff --git a/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetDetectorResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetDetectorResponseHandler.cs new file mode 100644 index 0000000..1e8ff28 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetDetectorResponseHandler.cs @@ -0,0 +1,35 @@ +using System; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.LocoNet +{ + public interface ILocoNetDetectorResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnLocoNetDetectorReceived; + } + + /// + /// From Z21 FW version 1.22, reports the occupancy status of LocoNet track occupancy detectors + /// (LAN_LOCONET_DETECTOR, protocol §9.5). + /// + public class LocoNetDetectorResponseHandler : ILocoNetDetectorResponseHandler + { + public event EventHandler? OnLocoNetDetectorReceived; + + public string Name => "LAN_LOCONET_DETECTOR"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 7 && response[2] == 0xA4 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + byte type = response[4]; + ushort reportAddress = BitConverter.ToUInt16(response, 5); + byte[] info = response[7..]; + OnLocoNetDetectorReceived?.Invoke(this, new(type, reportAddress, info)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetDispatchAddressResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetDispatchAddressResponseHandler.cs new file mode 100644 index 0000000..23554e3 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetDispatchAddressResponseHandler.cs @@ -0,0 +1,34 @@ +using System; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.LocoNet +{ + public interface ILocoNetDispatchAddressResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnLocoNetDispatchAddressReceived; + } + + /// + /// From Z21 FW version 1.22, reports the result of a LocoNet dispatch request + /// (LAN_LOCONET_DISPATCH_ADDR, protocol §9.4). + /// + public class LocoNetDispatchAddressResponseHandler : ILocoNetDispatchAddressResponseHandler + { + public event EventHandler? OnLocoNetDispatchAddressReceived; + + public string Name => "LAN_LOCONET_DISPATCH_ADDR"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 7 && response[2] == 0xA3 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + ushort locoAddress = BitConverter.ToUInt16(response, 4); + byte slot = response[6]; + OnLocoNetDispatchAddressReceived?.Invoke(this, new(locoAddress, slot)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetFromLanResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetFromLanResponseHandler.cs new file mode 100644 index 0000000..79fa6bb --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetFromLanResponseHandler.cs @@ -0,0 +1,32 @@ +using System; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.LocoNet +{ + public interface ILocoNetFromLanResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnLocoNetMessageReceived; + } + + /// + /// From Z21 FW version 1.20, reports a LocoNet message another client wrote onto the bus + /// (LAN_LOCONET_FROM_LAN, protocol §9.3). + /// + public class LocoNetFromLanResponseHandler : ILocoNetFromLanResponseHandler + { + public event EventHandler? OnLocoNetMessageReceived; + + public string Name => "LAN_LOCONET_FROM_LAN"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 4 && response[2] == 0xA2 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + OnLocoNetMessageReceived?.Invoke(this, new(response[4..])); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetReceiveResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetReceiveResponseHandler.cs new file mode 100644 index 0000000..ee31a15 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetReceiveResponseHandler.cs @@ -0,0 +1,32 @@ +using System; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.LocoNet +{ + public interface ILocoNetReceiveResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnLocoNetMessageReceived; + } + + /// + /// From Z21 FW version 1.20, reports a LocoNet message received on the bus (LAN_LOCONET_Z21_RX, + /// protocol §9.1). + /// + public class LocoNetReceiveResponseHandler : ILocoNetReceiveResponseHandler + { + public event EventHandler? OnLocoNetMessageReceived; + + public string Name => "LAN_LOCONET_Z21_RX"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 4 && response[2] == 0xA0 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + OnLocoNetMessageReceived?.Invoke(this, new(response[4..])); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetTransmitResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetTransmitResponseHandler.cs new file mode 100644 index 0000000..5872544 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/LocoNet/LocoNetTransmitResponseHandler.cs @@ -0,0 +1,32 @@ +using System; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.LocoNet +{ + public interface ILocoNetTransmitResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnLocoNetMessageReceived; + } + + /// + /// From Z21 FW version 1.20, reports a LocoNet message the Z21 itself wrote onto the bus + /// (LAN_LOCONET_Z21_TX, protocol §9.2). + /// + public class LocoNetTransmitResponseHandler : ILocoNetTransmitResponseHandler + { + public event EventHandler? OnLocoNetMessageReceived; + + public string Name => "LAN_LOCONET_Z21_TX"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 4 && response[2] == 0xA1 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + OnLocoNetMessageReceived?.Invoke(this, new(response[4..])); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Programming/CvNackResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Programming/CvNackResponseHandler.cs new file mode 100644 index 0000000..c2dff12 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Programming/CvNackResponseHandler.cs @@ -0,0 +1,28 @@ +using System; + +namespace Z21.Core.ResponseHandler.Programming +{ + public interface ICvNackResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnCvNackReceived; + } + + /// + /// Sent when the decoder acknowledgement is missing during CV programming (LAN_X_CV_NACK, + /// protocol §6.4). + /// + public class CvNackResponseHandler : ICvNackResponseHandler + { + public event EventHandler? OnCvNackReceived; + + public string Name => "LAN_X_CV_NACK"; + + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x61), (5, 0x13)); + + public void Handle(byte[] response) + { + OnCvNackReceived?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Programming/CvNackShortCircuitResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Programming/CvNackShortCircuitResponseHandler.cs new file mode 100644 index 0000000..c021f76 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Programming/CvNackShortCircuitResponseHandler.cs @@ -0,0 +1,28 @@ +using System; + +namespace Z21.Core.ResponseHandler.Programming +{ + public interface ICvNackShortCircuitResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnCvNackShortCircuitReceived; + } + + /// + /// Sent when CV programming fails because of a short circuit on the track (LAN_X_CV_NACK_SC, + /// protocol §6.3). + /// + public class CvNackShortCircuitResponseHandler : ICvNackShortCircuitResponseHandler + { + public event EventHandler? OnCvNackShortCircuitReceived; + + public string Name => "LAN_X_CV_NACK_SC"; + + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x61), (5, 0x12)); + + public void Handle(byte[] response) + { + OnCvNackShortCircuitReceived?.Invoke(this, EventArgs.Empty); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Programming/CvResultResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Programming/CvResultResponseHandler.cs new file mode 100644 index 0000000..2ae7a91 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/Programming/CvResultResponseHandler.cs @@ -0,0 +1,32 @@ +using System; +using Z21.Core.Codecs; +using Z21.Core.Model.EventArgs; + +namespace Z21.Core.ResponseHandler.Programming +{ + public interface ICvResultResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnCvResultReceived; + } + + /// + /// Positive acknowledgement of a CV read/write (LAN_X_CV_RESULT, protocol §6.5), sent to the + /// triggering client. + /// + public class CvResultResponseHandler(IAddressCodec addressCodec) : ICvResultResponseHandler + { + public event EventHandler? OnCvResultReceived; + + public string Name => "LAN_X_CV_RESULT"; + + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x64), (5, 0x14)); + + public void Handle(byte[] response) + { + ushort cvAddress = addressCodec.CombineCvAddress(response[6], response[7]); + byte value = response[8]; + OnCvResultReceived?.Invoke(this, new(cvAddress, value)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/RailCom/RailComDataChangedResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/RailCom/RailComDataChangedResponseHandler.cs new file mode 100644 index 0000000..32ed958 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/RailCom/RailComDataChangedResponseHandler.cs @@ -0,0 +1,37 @@ +using System; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseParser; + +namespace Z21.Core.ResponseHandler.RailCom +{ + public interface IRailComDataChangedResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnRailComDataReceived; + } + + /// + /// From Z21 FW version 1.29, reports RailCom data (LAN_RAILCOM_DATACHANGED, protocol §8.1), + /// either in response to LAN_RAILCOM_GETDATA or unsolicited when the broadcast is active. + /// + public class RailComDataChangedResponseHandler(IRailComDataParser railComDataParser) : IRailComDataChangedResponseHandler + { + private const int PayloadLength = 13; + + public event EventHandler? OnRailComDataReceived; + + public string Name => "LAN_RAILCOM_DATACHANGED"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= 4 + PayloadLength && response[2] == 0x88 && response[3] == 0x00; + } + + public void Handle(byte[] response) + { + byte[] data = new byte[PayloadLength]; + Buffer.BlockCopy(response, 4, data, 0, PayloadLength); + OnRailComDataReceived?.Invoke(this, new(railComDataParser.Parse(data))); + } + } +} diff --git a/src/Z21.Client/Core/ResponseHandler/Settings/AccessoryModeResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Settings/AccessoryModeResponseHandler.cs index 1bd4d6a..561f1c4 100644 --- a/src/Z21.Client/Core/ResponseHandler/Settings/AccessoryModeResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/Settings/AccessoryModeResponseHandler.cs @@ -5,7 +5,7 @@ namespace Z21.Core.ResponseHandler.Settings { - public interface IAccessoryModeResponseHandler + public interface IAccessoryModeResponseHandler : IZ21ResponseHandler { event EventHandler? OnAccessoryModeReceived; } @@ -23,17 +23,8 @@ public class AccessoryModeResponseHandler : IAccessoryModeResponseHandler public string Name => "LAN_GET_TURNOUTMODE"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x70 && response[3] == 0x00; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 4, (2, 0x70), (3, 0x00)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/Settings/LocoModeResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Settings/LocoModeResponseHandler.cs index 79bb9c8..527b1c9 100644 --- a/src/Z21.Client/Core/ResponseHandler/Settings/LocoModeResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/Settings/LocoModeResponseHandler.cs @@ -22,17 +22,8 @@ public class LocoModeResponseHandler : ILocoModeResponseHandler public string Name => "LAN_GET_LOCOMODE"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x60 && response[3] == 0x00; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 4, (2, 0x60), (3, 0x00)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/Switching/ExtAccessoryInfoResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Switching/ExtAccessoryInfoResponseHandler.cs index a2e1ced..dbcaf3a 100644 --- a/src/Z21.Client/Core/ResponseHandler/Switching/ExtAccessoryInfoResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/Switching/ExtAccessoryInfoResponseHandler.cs @@ -1,7 +1,7 @@ using System; +using Z21.Core.Codecs; using Z21.Core.Command.Switching; using Z21.Core.Command.SystemState; -using Z21.Core.Helper; using Z21.Core.Model; using Z21.Core.Model.EventArgs; @@ -17,27 +17,25 @@ public interface IExtAccessoryInfoResponseHandler : IZ21ResponseHandler /// public class ExtAccessoryInfoResponseHandler : IExtAccessoryInfoResponseHandler { + private readonly IAddressCodec _addressCodec; + + public ExtAccessoryInfoResponseHandler(IAddressCodec addressCodec) + { + _addressCodec = addressCodec; + } + public event EventHandler? OnExtAccessoryInfoReceived; public string Name => "LAN_X_EXT_ACCESSORY_INFO"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 && response[3] == 0x00 && response[4] == 0x44; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 5, (2, 0x40), (3, 0x00), (4, 0x44)); public void Handle(byte[] response) { byte msb = response[5]; byte lsb = response[6]; - ushort address = AddressHelper.CombineAccessoryAddress(lsb, msb); + ushort address = _addressCodec.CombineExtAccessoryAddress(lsb, msb); byte db2 = response[7]; byte status = response[8]; diff --git a/src/Z21.Client/Core/ResponseHandler/Switching/TurnoutInfoResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/Switching/TurnoutInfoResponseHandler.cs index bde875a..5c5a5cb 100644 --- a/src/Z21.Client/Core/ResponseHandler/Switching/TurnoutInfoResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/Switching/TurnoutInfoResponseHandler.cs @@ -1,7 +1,8 @@ using System; +using Microsoft.Extensions.Logging; +using Z21.Core.Codecs; using Z21.Core.Command.Switching; using Z21.Core.Command.SystemState; -using Z21.Core.Helper; using Z21.Core.Model; using Z21.Core.Model.EventArgs; @@ -18,28 +19,27 @@ public interface ITurnoutInfoResponseHandler : IZ21ResponseHandler /// public class TurnoutInfoResponseHandler : ITurnoutInfoResponseHandler { + private readonly IAddressCodec _addressCodec; + private readonly ILogger? _logger; + + public TurnoutInfoResponseHandler(IAddressCodec addressCodec, ILogger? logger = null) + { + _addressCodec = addressCodec; + _logger = logger; + } public event EventHandler? OnTurnoutInfoReceived; public string Name => "LAN_X_TURNOUT_INFO"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 && response[3] == 0x00 && response[4] == 0x43; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 5, (2, 0x40), (3, 0x00), (4, 0x43)); public void Handle(byte[] response) { byte msb = response[5]; byte lsb = response[6]; - ushort address = AddressHelper.CombineAccessoryAddress(lsb, msb); + ushort address = _addressCodec.CombineAccessoryAddress(lsb, msb); byte db2 = response[7]; AccessoryOutput? accessoryOutput = null; @@ -48,7 +48,7 @@ public void Handle(byte[] response) if (db2 == 0x02) accessoryOutput = AccessoryOutput.Output2; - Console.WriteLine($"Turnout: {address}, State: {accessoryOutput}"); + _logger?.LogDebug("{name} address {address}, output {accessoryOutput}.", Name, address, accessoryOutput); OnTurnoutInfoReceived?.Invoke(this, new(address, accessoryOutput)); } } diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/BroadcastFlagsResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/BroadcastFlagsResponseHandler.cs index 16e69c8..8f78014 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/BroadcastFlagsResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/BroadcastFlagsResponseHandler.cs @@ -17,17 +17,8 @@ public class BroadcastFlagsResponseHandler : IBroadcastFlagsResponseHandler public string Name => "LAN_GET_BROADCASTFLAGS"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x51 - && response[3] == 0x00; - } catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 4, (2, 0x51), (3, 0x00)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/FirmwareVersionResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/FirmwareVersionResponseHandler.cs index df1158e..0df4a70 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/FirmwareVersionResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/FirmwareVersionResponseHandler.cs @@ -18,21 +18,9 @@ public class FirmwareVersionResponseHandler : IFirmwareVersionResponseHandler public string Name => "LAN_X_GET_FIRMWARE_VERSION"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 - && response[3] == 0x00 - && response[4] == 0xF3 - && response[5] == 0x0A - && (response[4] ^ response[5] ^ response[6] ^ response[7]) == response[8]; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 9, (2, 0x40), (3, 0x00), (4, 0xF3), (5, 0x0A)) + && (response[4] ^ response[5] ^ response[6] ^ response[7]) == response[8]; public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/HardwareInfoResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/HardwareInfoResponseHandler.cs index a12287c..bee9443 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/HardwareInfoResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/HardwareInfoResponseHandler.cs @@ -18,23 +18,14 @@ public class HardwareInfoResponseHandler : IHardwareInfoResponseHandler public event EventHandler? OnHardwareInfoReceived; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x1A && response[3] == 0x00; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 4, (2, 0x1A), (3, 0x00)); public void Handle(byte[] response) { - // TODO: calculate FW version and add to HardwareInfoEventArgs int hwType = BitConverter.ToInt32(response, 4); - OnHardwareInfoReceived?.Invoke(this, new(hwType)); + int firmwareVersion = BitConverter.ToInt32(response, 8); + OnHardwareInfoReceived?.Invoke(this, new(hwType, firmwareVersion)); } } diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/SerialNumberResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/SerialNumberResponseHandler.cs index 6172121..084dccf 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/SerialNumberResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/SerialNumberResponseHandler.cs @@ -18,18 +18,8 @@ public class SerialNumberResponseHandler : ISerialNumberResponseHandler public string Name => "LAN_GET_SERIAL_NUMBER"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x10 - && response[3] == 0x00; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 4, (2, 0x10), (3, 0x00)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/SoftwareLockResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/SoftwareLockResponseHandler.cs index 459a378..96719ba 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/SoftwareLockResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/SoftwareLockResponseHandler.cs @@ -18,17 +18,8 @@ public class SoftwareLockResponseHandler : ISoftwareLockResponseHandler public event EventHandler? OnSoftwareLockReceived; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x18 && response[3] == 0x00; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 4, (2, 0x18), (3, 0x00)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/StatusChangedResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/StatusChangedResponseHandler.cs index 95e6773..4088d73 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/StatusChangedResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/StatusChangedResponseHandler.cs @@ -21,20 +21,9 @@ public class StatusChangedResponseHandler(ICentralStateResponseParser centralSta public string Name => "LAN_X_STATUS_CHANGED"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 - && response[3] == 0x00 - && response[4] == 0x62 - && response[5] == 0x22 - && (response[4] ^ response[5] ^ response[6]) == response[7]; - } catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 8, (2, 0x40), (3, 0x00), (4, 0x62), (5, 0x22)) + && (response[4] ^ response[5] ^ response[6]) == response[7]; public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/SystemStateDataChangedResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/SystemStateDataChangedResponseHandler.cs index bc64320..c074ad4 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/SystemStateDataChangedResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/SystemStateDataChangedResponseHandler.cs @@ -20,16 +20,8 @@ public class SystemStateDataChangedResponseHandler(ISystemStateResponseParser sy public event EventHandler? OnSystemStateDataChangedReceived; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x84 && response[3] == 0x00; - } catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 4, (2, 0x84), (3, 0x00)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/ProgrammingModeResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/ProgrammingModeResponseHandler.cs index 00dd29e..4860d68 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/ProgrammingModeResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/ProgrammingModeResponseHandler.cs @@ -16,21 +16,8 @@ public class ProgrammingModeResponseHandler : IProgrammingModeResponseHandler public string Name => "LAN_X_BC_PROGRAMMING_MODE"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 - && response[3] == 0x00 - && response[4] == 0x61 - && response[5] == 0x02 - && (response[4] ^ response[5]) == 0x63; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x61), (5, 0x02)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/StoppedResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/StoppedResponseHandler.cs index 3d4e233..02facf6 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/StoppedResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/StoppedResponseHandler.cs @@ -19,21 +19,8 @@ public class StoppedResponseHandler : IStoppedResponseHandler public string Name => "LAN_X_BC_STOPPED"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 - && response[3] == 0x00 - && response[4] == 0x81 - && response[5] == 0x00 - && (response[4] ^ response[5]) == 0x81; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x81), (5, 0x00)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackPowerOffResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackPowerOffResponseHandler.cs index 4055a94..c901e5d 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackPowerOffResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackPowerOffResponseHandler.cs @@ -19,21 +19,8 @@ public class TrackPowerOffResponseHandler : ITrackPowerOffResponseHandler public string Name => "LAN_X_BC_TRACK_POWER_OFF"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 - && response[3] == 0x00 - && response[4] == 0x61 - && response[5] == 0x00 - && (response[4] ^ response[5]) == 0x61; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x61), (5, 0x00)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackPowerOnResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackPowerOnResponseHandler.cs index 3f62470..f421377 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackPowerOnResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackPowerOnResponseHandler.cs @@ -19,21 +19,8 @@ public class TrackPowerOnResponseHandler : ITrackPowerOnResponseHandler public string Name => "LAN_X_BC_TRACK_POWER_ON"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 - && response[3] == 0x00 - && response[4] == 0x61 - && response[5] == 0x01 - && (response[4] ^ response[5]) == 0x60; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x61), (5, 0x01)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackShortResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackShortResponseHandler.cs index db54b4f..724544f 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackShortResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/TrackPower/TrackShortResponseHandler.cs @@ -18,21 +18,8 @@ public class TrackShortResponseHandler : ITrackShortResponseHandler public string Name => "LAN_X_BC_TRACK_SHORT_CIRCUIT"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 - && response[3] == 0x00 - && response[4] == 0x61 - && response[5] == 0x08 - && (response[4] ^ response[5]) == 0x69; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x61), (5, 0x08)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/UnknownCommandResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/UnknownCommandResponseHandler.cs index a5a5c33..adfd2b1 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/UnknownCommandResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/UnknownCommandResponseHandler.cs @@ -17,21 +17,8 @@ public class UnknownCommandResponseHandler : IUnknownCommandResponseHandler public string Name => "LAN_X_UNKNOWN_COMMAND"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 - && response[3] == 0x00 - && response[4] == 0x61 - && response[5] == 0x82 - && (response[4] ^ response[5]) == 0xE3; - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (3, 0x00), (4, 0x61), (5, 0x82)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/SystemState/VersionResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/SystemState/VersionResponseHandler.cs index e90ce9a..4bf40bd 100644 --- a/src/Z21.Client/Core/ResponseHandler/SystemState/VersionResponseHandler.cs +++ b/src/Z21.Client/Core/ResponseHandler/SystemState/VersionResponseHandler.cs @@ -18,20 +18,8 @@ public class VersionResponseHandler : IVersionResponseHandler public string Name => "LAN_X_GET_VERSION"; - public bool CanHandle(byte[] response) - { - try - { - return response[2] == 0x40 && - response[4] == 0x63 && - response[5] == 0x21; - // TODO handle XOR-Byte - } - catch (IndexOutOfRangeException) - { - return false; - } - } + public bool CanHandle(byte[] response) => + ((IZ21ResponseHandler)this).MatchesFrame(response, 6, (2, 0x40), (4, 0x63), (5, 0x21)); public void Handle(byte[] response) { diff --git a/src/Z21.Client/Core/ResponseHandler/ZLink/ZLinkHardwareInfoResponseHandler.cs b/src/Z21.Client/Core/ResponseHandler/ZLink/ZLinkHardwareInfoResponseHandler.cs new file mode 100644 index 0000000..f811006 --- /dev/null +++ b/src/Z21.Client/Core/ResponseHandler/ZLink/ZLinkHardwareInfoResponseHandler.cs @@ -0,0 +1,34 @@ +using System; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseParser; + +namespace Z21.Core.ResponseHandler.ZLink +{ + public interface IZLinkHardwareInfoResponseHandler : IZ21ResponseHandler + { + event EventHandler? OnZLinkHardwareInfoReceived; + } + + /// + /// Reports the hardware information of a Z21 pro LINK (LAN_ZLINK_GET_HWINFO reply, protocol §11.1.1.1). + /// + public class ZLinkHardwareInfoResponseHandler(IZLinkHardwareInfoParser parser) : IZLinkHardwareInfoResponseHandler + { + private const int FrameLength = 63; + + public event EventHandler? OnZLinkHardwareInfoReceived; + + public string Name => "LAN_ZLINK_GET_HWINFO"; + + public bool CanHandle(byte[] response) + { + ArgumentNullException.ThrowIfNull(response); + return response.Length >= FrameLength && response[2] == 0xE8 && response[3] == 0x00 && response[4] == 0x06; + } + + public void Handle(byte[] response) + { + OnZLinkHardwareInfoReceived?.Invoke(this, new(parser.Parse(response[5..]))); + } + } +} diff --git a/src/Z21.Client/Core/ResponseParser/RailComDataParser.cs b/src/Z21.Client/Core/ResponseParser/RailComDataParser.cs new file mode 100644 index 0000000..87963ab --- /dev/null +++ b/src/Z21.Client/Core/ResponseParser/RailComDataParser.cs @@ -0,0 +1,30 @@ +using System; +using Z21.Core.Model; + +namespace Z21.Core.ResponseParser +{ + public interface IRailComDataParser : IZ21ResponseParser + { + RailComData Parse(byte[] data); + } + + /// + /// Parses the RailCom data payload (the bytes following the LAN_RAILCOM_DATACHANGED header). + /// + public class RailComDataParser : IRailComDataParser + { + public RailComData Parse(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + + ushort locoAddress = BitConverter.ToUInt16(data, 0); + uint receiveCounter = BitConverter.ToUInt32(data, 2); + ushort errorCounter = BitConverter.ToUInt16(data, 6); + RailComOptions options = (RailComOptions)data[9]; + byte speed = data[10]; + byte qos = data[11]; + + return new RailComData(locoAddress, receiveCounter, errorCounter, options, speed, qos); + } + } +} diff --git a/src/Z21.Client/Core/ResponseParser/SignalDecoderSystemStateParser.cs b/src/Z21.Client/Core/ResponseParser/SignalDecoderSystemStateParser.cs new file mode 100644 index 0000000..2573510 --- /dev/null +++ b/src/Z21.Client/Core/ResponseParser/SignalDecoderSystemStateParser.cs @@ -0,0 +1,36 @@ +using System; +using Z21.Core.Model; + +namespace Z21.Core.ResponseParser +{ + public interface ISignalDecoderSystemStateParser : IZ21ResponseParser + { + SignalDecoderSystemState Parse(byte[] data); + } + + /// + /// Parses the 42-byte signal decoder system state payload (protocol §11.3.4.2). + /// + public class SignalDecoderSystemStateParser : ISignalDecoderSystemStateParser + { + public SignalDecoderSystemState Parse(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + + return new SignalDecoderSystemState( + BitConverter.ToInt16(data, 0), + BitConverter.ToInt16(data, 2), + BitConverter.ToUInt16(data, 4), + data[6], + data[7], + data[8..10], + data[10..12], + data[12..16], + data[16..20], + data[23], + data[24..28], + data[28..32], + BitConverter.ToUInt16(data, 32)); + } + } +} diff --git a/src/Z21.Client/Core/ResponseParser/SwitchDecoderSystemStateParser.cs b/src/Z21.Client/Core/ResponseParser/SwitchDecoderSystemStateParser.cs new file mode 100644 index 0000000..c818f7e --- /dev/null +++ b/src/Z21.Client/Core/ResponseParser/SwitchDecoderSystemStateParser.cs @@ -0,0 +1,34 @@ +using System; +using Z21.Core.Model; + +namespace Z21.Core.ResponseParser +{ + public interface ISwitchDecoderSystemStateParser : IZ21ResponseParser + { + SwitchDecoderSystemState Parse(byte[] data); + } + + /// + /// Parses the 44-byte switch decoder system state payload (protocol §11.3.4.1). + /// + public class SwitchDecoderSystemStateParser : ISwitchDecoderSystemStateParser + { + public SwitchDecoderSystemState Parse(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + + return new SwitchDecoderSystemState( + BitConverter.ToInt16(data, 0), + BitConverter.ToInt16(data, 2), + BitConverter.ToUInt16(data, 4), + data[6], + data[7], + data[8..16], + data[16..24], + data[24..32], + BitConverter.ToUInt16(data, 32), + BitConverter.ToUInt16(data, 34), + data[42]); + } + } +} diff --git a/src/Z21.Client/Core/ResponseParser/ZLinkHardwareInfoParser.cs b/src/Z21.Client/Core/ResponseParser/ZLinkHardwareInfoParser.cs new file mode 100644 index 0000000..06e798a --- /dev/null +++ b/src/Z21.Client/Core/ResponseParser/ZLinkHardwareInfoParser.cs @@ -0,0 +1,38 @@ +using System; +using System.Text; +using Z21.Core.Model; + +namespace Z21.Core.ResponseParser +{ + public interface IZLinkHardwareInfoParser : IZ21ResponseParser + { + ZLinkHardwareInfo Parse(byte[] data); + } + + /// + /// Parses the 58-byte Z_Hw_Info payload of a Z21 pro LINK (protocol §11.1.1.1). + /// + public class ZLinkHardwareInfoParser : IZLinkHardwareInfoParser + { + public ZLinkHardwareInfo Parse(byte[] data) + { + ArgumentNullException.ThrowIfNull(data); + + ushort hardwareId = BitConverter.ToUInt16(data, 0); + byte major = data[2]; + byte minor = data[3]; + ushort build = BitConverter.ToUInt16(data, 4); + string mac = ReadString(data, 6, 18); + string name = ReadString(data, 24, 33); + + return new ZLinkHardwareInfo(hardwareId, major, minor, build, mac, name); + } + + private string ReadString(byte[] data, int offset, int length) + { + string value = Encoding.Latin1.GetString(data, offset, length); + int terminator = value.IndexOf('\0'); + return terminator >= 0 ? value[..terminator] : value; + } + } +} diff --git a/src/Z21.Client/Core/Z21Client.cs b/src/Z21.Client/Core/Z21Client.cs deleted file mode 100644 index 34f27c3..0000000 --- a/src/Z21.Client/Core/Z21Client.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Z21.Core.Command; -using Z21.Core.Command.SystemState; -using Z21.Core.Exception; -using Z21.Core.Helper; -using Z21.Core.Model; -using Z21.Core.Model.EventArgs; -using Z21.Transport; -// ReSharper disable ClassWithVirtualMembersNeverInherited.Global - -namespace Z21.Core -{ - - public class Z21Client : IZ21Client - { - private readonly ILogger? _logger; - private readonly Z21Configuration _z21Configuration; - private readonly IZ21Transport _transport; - private readonly DelayedAction _delayedKeepAliveAction; - private readonly Z21Watchdog _z21Watchdog; - - /// - /// IPv4 safe MTU for payload according to specification. - /// - public const int MaxUdpPayload = 1472; - - /// Thrown when system architecture is not little-endian. - public Z21Client(Z21Configuration z21Configuration, IZ21Transport z21Transport, ILogger? logger = null) - { - ArgumentNullException.ThrowIfNull(z21Configuration); - ArgumentNullException.ThrowIfNull(z21Transport); - - if (!BitConverter.IsLittleEndian) - throw new PlatformNotSupportedException("Z21Client requires little-endian architecture."); - - _z21Configuration = z21Configuration; - _transport = z21Transport; - _logger = logger; - _z21Watchdog = new (z21Configuration); - _z21Watchdog.OnReachabilityChanged += async (_, args ) => await Watchdog_OnOnReachabilityChanged(args); - _delayedKeepAliveAction = new (TimeSpan.FromSeconds(45), async () => await SendCommandsAsync(new GetFirmwareVersionCommand())); - } - - public event EventHandler? OnConnectionChanged; - - public bool IsConnected { get; private set; } - - public async Task ConnectAsync() - { - _logger?.LogInformation("Z21Client trying to connect with {ClientIPEndPoint}.", _transport.Z21Configuration.ClientIPEndPoint); - _transport.Connect(); - await LogOnAsync(); - } - - public async Task SendCommandsAsync(params IZ21Command[] z21Commands) - { - ArgumentNullException.ThrowIfNull(z21Commands); - - if (!_transport.IsConnected) - await ConnectAsync(); - - foreach (var z21Command in z21Commands) - _logger?.LogDebug("{commandName} sending {datagram} to Z21.", z21Command.Name, BitConverter.ToString(z21Command.Data)); - - var combinedPayload = z21Commands.SelectMany(z21Command => z21Command.Data).ToArray(); - MtuPayloadLengthExceededException.ThrowIfExceeded(combinedPayload); - - await _transport.SendAsync(combinedPayload); - _delayedKeepAliveAction.Delay(); - } - - protected async virtual Task LogOnAsync() - { - await SendCommandsAsync(new SetBroadcastFlagsCommand(_z21Configuration.BroadcastFlags), new GetFirmwareVersionCommand()); - } - - private async Task Watchdog_OnOnReachabilityChanged(ConnectionChangedEventArgs args) - { - if (args.IsConnected) - await LogOnAsync(); - IsConnected = args.IsConnected; - OnConnectionChanged?.Invoke(this, args); - } - } -} \ No newline at end of file diff --git a/src/Z21.Client/Core/Z21CommandStation.cs b/src/Z21.Client/Core/Z21CommandStation.cs new file mode 100644 index 0000000..4cc2b18 --- /dev/null +++ b/src/Z21.Client/Core/Z21CommandStation.cs @@ -0,0 +1,214 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using CommandStation; +using CommandStation.Transport; +using Microsoft.Extensions.Logging; +using Z21.Core.Command; +using Z21.Core.Command.Driving; +using Z21.Core.Command.FastClock; +using Z21.Core.Command.Feedback; +using Z21.Core.Command.Programming; +using Z21.Core.Command.Switching; +using Z21.Core.Command.SystemState; +using Z21.Core.Command.SystemState.TrackPower; +using Z21.Core.Exception; +using Z21.Core.Helper; +using Z21.Core.Model; +using Z21.Core.ResponseHandler.Driving; +using Z21.Core.ResponseHandler.FastClock; +using Z21.Core.ResponseHandler.Feedback; +using Z21.Core.ResponseHandler.Programming; +using Z21.Core.ResponseHandler.Switching; +using Z21.Core.ResponseHandler.SystemState; +using Z21.Core.ResponseHandler.SystemState.TrackPower; + +namespace Z21.Core +{ + public class Z21CommandStation : IZ21CommandStation, IProgrammingControl, IFeedbackControl, IFastClockControl, IDisposable + { + private readonly ITransport _transport; + private readonly Z21ResponseHandler _dispatcher; + private readonly Z21Options _options; + private readonly DelayedAction _delayedKeepAliveAction; + private readonly ILogger? _logger; + + /// + /// IPv4 safe MTU for payload according to specification. + /// + public const int MaxUdpPayload = 1472; + + /// Thrown when system architecture is not little-endian. + public Z21CommandStation(ITransport transport, + Z21ResponseHandler dispatcher, + IZ21CommandFactory commands, + Z21Options options, + ILocoInfoResponseHandler locoInfoResponseHandler, + ITurnoutInfoResponseHandler turnoutInfoResponseHandler, + IExtAccessoryInfoResponseHandler extAccessoryInfoResponseHandler, + ISystemStateDataChangedResponseHandler systemStateResponseHandler, + IFirmwareVersionResponseHandler firmwareVersionResponseHandler, + IStatusChangedResponseHandler statusChangedResponseHandler, + ITrackPowerOnResponseHandler trackPowerOnResponseHandler, + ITrackPowerOffResponseHandler trackPowerOffResponseHandler, + ICvResultResponseHandler cvResultResponseHandler, + ICvNackResponseHandler cvNackResponseHandler, + ICvNackShortCircuitResponseHandler cvNackShortCircuitResponseHandler, + IRmBusDataChangedResponseHandler rmBusDataChangedResponseHandler, + IFastClockDataResponseHandler fastClockDataResponseHandler, + ILogger? logger = null) + { + ArgumentNullException.ThrowIfNull(transport); + ArgumentNullException.ThrowIfNull(dispatcher); + ArgumentNullException.ThrowIfNull(commands); + ArgumentNullException.ThrowIfNull(options); + + if (!BitConverter.IsLittleEndian) + throw new PlatformNotSupportedException("Z21CommandStation requires little-endian architecture."); + + _transport = transport; + _dispatcher = dispatcher; + Commands = commands; + _options = options; + _logger = logger; + _delayedKeepAliveAction = new(options.KeepAliveInterval, KeepAliveAsync); + + _transport.OnConnectionChanged += (_, args) => + { + if (!args.IsConnected) + _delayedKeepAliveAction.Stop(); + ConnectionChanged?.Invoke(this, args); + }; + + locoInfoResponseHandler.OnLocoInfoReceived += (_, args) => LocoInfoReceived?.Invoke(this, args.Data); + turnoutInfoResponseHandler.OnTurnoutInfoReceived += (_, args) => TurnoutInfoReceived?.Invoke(this, new TurnoutInfo(args.AccessoryAddress, args.AccessoryOutput)); + extAccessoryInfoResponseHandler.OnExtAccessoryInfoReceived += (_, args) => ExtAccessoryInfoReceived?.Invoke(this, new ExtAccessoryInfo(args.AccessoryAddress, args.EncodedState, args.DataValid)); + systemStateResponseHandler.OnSystemStateDataChangedReceived += (_, args) => SystemStateReceived?.Invoke(this, args.SystemState); + firmwareVersionResponseHandler.OnFirmwareVersionReceived += (_, args) => FirmwareVersionReceived?.Invoke(this, args.FirmwareVersion); + statusChangedResponseHandler.OnStatusChangedReceived += (_, args) => StatusChanged?.Invoke(this, args.CentralState); + trackPowerOnResponseHandler.OnTrackPowerOnReceived += (_, _) => TrackPowerChanged?.Invoke(this, true); + trackPowerOffResponseHandler.OnTrackPowerOffReceived += (_, _) => TrackPowerChanged?.Invoke(this, false); + cvResultResponseHandler.OnCvResultReceived += (_, args) => CvReadCompleted?.Invoke(this, new CvValue(args.CvAddress, args.Value)); + cvNackResponseHandler.OnCvNackReceived += (_, _) => CvProgrammingFailed?.Invoke(this, CvProgrammingError.NoAcknowledgement); + cvNackShortCircuitResponseHandler.OnCvNackShortCircuitReceived += (_, _) => CvProgrammingFailed?.Invoke(this, CvProgrammingError.ShortCircuit); + rmBusDataChangedResponseHandler.OnRmBusDataReceived += (_, args) => FeedbackChanged?.Invoke(this, new FeedbackData(args.GroupIndex, args.FeedbackStates)); + fastClockDataResponseHandler.OnFastClockDataReceived += (_, args) => ModelTimeChanged?.Invoke(this, new ModelTime(args.Data.Day, args.Data.Hour, args.Data.Minute, args.Data.Second, args.Data.Rate)); + } + + public IZ21CommandFactory Commands { get; } + + public bool IsConnected => _transport.IsConnected; + + public event EventHandler? ConnectionChanged; + public event EventHandler? LocoInfoReceived; + public event EventHandler? TurnoutInfoReceived; + public event EventHandler? ExtAccessoryInfoReceived; + public event EventHandler? SystemStateReceived; + public event EventHandler? FirmwareVersionReceived; + public event EventHandler? StatusChanged; + public event EventHandler? TrackPowerChanged; + public event EventHandler? CvReadCompleted; + public event EventHandler? CvProgrammingFailed; + public event EventHandler? FeedbackChanged; + public event EventHandler? ModelTimeChanged; + + public async Task ConnectAsync() + { + _logger?.LogInformation("Z21CommandStation connecting."); + await _transport.ConnectAsync(); + await LogOnAsync(); + } + + public Task DisconnectAsync() + { + _delayedKeepAliveAction.Stop(); + return _transport.DisconnectAsync(); + } + + public async Task SendCommandsAsync(params IZ21Command[] commands) + { + ArgumentNullException.ThrowIfNull(commands); + + if (!_transport.IsConnected) + throw new NotConnectedException("Cannot send commands before ConnectAsync has completed."); + + foreach (var command in commands) + _logger?.LogDebug("{commandName} sending {datagram} to Z21.", command.Name, BitConverter.ToString(command.Data)); + + byte[] combinedPayload = commands.SelectMany(command => command.Data).ToArray(); + MtuPayloadLengthExceededException.ThrowIfExceeded(combinedPayload); + + await _transport.SendAsync(combinedPayload); + _delayedKeepAliveAction.Delay(); + } + + public Task DriveAsync(ushort locoAddress, DccSpeedMode speedMode, DrivingDirection direction, ushort speed) => + SendCommandsAsync(Commands.Create(speedMode, locoAddress, direction, speed)); + + public Task EmergencyStopAsync(ushort locoAddress) => SendCommandsAsync(Commands.Create(locoAddress)); + + public Task SetFunctionAsync(ushort locoAddress, ushort functionIndex, FunctionToggleType toggleType) => + SendCommandsAsync(Commands.Create(locoAddress, functionIndex, toggleType)); + + public Task PurgeAsync(ushort locoAddress) => SendCommandsAsync(Commands.Create(locoAddress)); + + public Task RequestLocoInfoAsync(ushort locoAddress) => SendCommandsAsync(Commands.Create(locoAddress)); + + public Task SetTurnoutAsync(ushort accessoryAddress, AccessoryOutput output, AccessoryState state, bool executeImmediately) => + SendCommandsAsync(Commands.Create(accessoryAddress, output, state, executeImmediately)); + + public Task SetExtAccessoryAsync(ushort accessoryAddress, byte payload) => + SendCommandsAsync(Commands.Create(accessoryAddress, payload)); + + public Task RequestTurnoutInfoAsync(ushort accessoryAddress) => SendCommandsAsync(Commands.Create(accessoryAddress)); + + public Task RequestExtAccessoryInfoAsync(ushort accessoryAddress) => SendCommandsAsync(Commands.Create(accessoryAddress)); + + public Task TrackPowerOnAsync() => SendCommandsAsync(Commands.Create()); + + public Task TrackPowerOffAsync() => SendCommandsAsync(Commands.Create()); + + public Task EmergencyStopAllAsync() => SendCommandsAsync(Commands.Create()); + + public Task RequestSystemStateAsync() => SendCommandsAsync(Commands.Create()); + + public Task RequestFirmwareVersionAsync() => SendCommandsAsync(Commands.Create()); + + public Task RequestStatusAsync() => SendCommandsAsync(Commands.Create()); + + public Task ReadCvAsync(ushort cvAddress) => SendCommandsAsync(Commands.Create(cvAddress)); + + public Task WriteCvAsync(ushort cvAddress, byte value) => SendCommandsAsync(Commands.Create(cvAddress, value)); + + public Task RequestFeedbackAsync(byte groupIndex) => SendCommandsAsync(Commands.Create(groupIndex)); + + public Task RequestModelTimeAsync() => SendCommandsAsync(Commands.Create(FastClockAction.Read)); + + public Task SetModelTimeAsync(ModelTime time) => SendCommandsAsync(Commands.Create(time)); + + public Task StartModelTimeAsync() => SendCommandsAsync(Commands.Create(FastClockAction.Start)); + + public Task StopModelTimeAsync() => SendCommandsAsync(Commands.Create(FastClockAction.Stop)); + + protected async virtual Task LogOnAsync() => + await SendCommandsAsync(Commands.Create(_options.BroadcastFlags), Commands.Create()); + + private async Task KeepAliveAsync() + { + try + { + await SendCommandsAsync(Commands.Create()); + } + catch (NotConnectedException exception) + { + _logger?.LogDebug(exception, "Keep-alive skipped because the station is not connected."); + } + } + + public void Dispose() + { + _delayedKeepAliveAction.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Z21.Client/Core/Z21Options.cs b/src/Z21.Client/Core/Z21Options.cs new file mode 100644 index 0000000..a83b083 --- /dev/null +++ b/src/Z21.Client/Core/Z21Options.cs @@ -0,0 +1,26 @@ +using System; +using Z21.Core.Model; + +namespace Z21.Core +{ + /// + /// Protocol-level options for the Z21 command station (transport options are configured separately + /// on the transport itself). + /// + public class Z21Options + { + /// + /// Broadcast flags requested from the Z21 on (re)connect. + /// + public uint[] BroadcastFlags { get; set; } = + [ + Z21BroadcastFlags.DriveAndSwitchingMessages, + Z21BroadcastFlags.LocoInfoChangedMessages + ]; + + /// + /// Interval after the last command before an automatic keep-alive request is sent. + /// + public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(45); + } +} diff --git a/src/Z21.Client/Core/Z21ResponseHandler.cs b/src/Z21.Client/Core/Z21ResponseHandler.cs index cd9bf82..467c706 100644 --- a/src/Z21.Client/Core/Z21ResponseHandler.cs +++ b/src/Z21.Client/Core/Z21ResponseHandler.cs @@ -1,84 +1,56 @@ using System; using System.Collections.Generic; -using System.Linq; +using CommandStation.Framing; +using CommandStation.Transport; using Microsoft.Extensions.Logging; -using Z21.Core.Model.EventArgs; using Z21.Core.ResponseHandler; -using Z21.Transport; namespace Z21.Core { public class Z21ResponseHandler { - private readonly IZ21Transport _transport; + private readonly ITransport _transport; + private readonly IFrameReader _frameReader; private readonly IEnumerable _z21ResponseHandlers; private readonly ILogger? _logger; - public Z21ResponseHandler(IZ21Transport z21Transport, IEnumerable z21ResponseHandlers, ILogger? logger = null) + public Z21ResponseHandler(ITransport transport, IFrameReader frameReader, IEnumerable z21ResponseHandlers, ILogger? logger = null) { - _transport = z21Transport; + _transport = transport; + _frameReader = frameReader; _z21ResponseHandlers = z21ResponseHandlers; _logger = logger; - _transport.OnResponseReceived += Transport_OnResponseReceived; + _frameReader.OnFrameReceived += FrameReader_OnFrameReceived; + _transport.OnBytesReceived += Transport_OnBytesReceived; } - protected virtual void Transport_OnResponseReceived(object? sender, ResponseReceivedEventArgs bytes) + protected virtual void Transport_OnBytesReceived(object? sender, BytesReceivedEventArgs args) { - CutDatagram(bytes.Response).ForEach(HandleDatagram); + _frameReader.Append(args.Data); } - protected virtual void HandleDatagram(byte[] data) + protected virtual void FrameReader_OnFrameReceived(object? sender, FrameReceivedEventArgs args) { - foreach (IZ21ResponseHandler handler in _z21ResponseHandlers.Where(handler => handler.CanHandle(data))) - { - try - { - _logger?.LogDebug("{handlerName} handling datagram {cutDatagram}.", handler.Name, BitConverter.ToString(data)); - handler.Handle(data); - } - catch (System.Exception exception) - { - _logger?.LogError(exception, "{handlerName} failed to handle datagram {cutDatagram}.", handler.Name, BitConverter.ToString(data)); - } - } + HandleDatagram(args.Frame); } - protected virtual List CutDatagram(byte[] datagram) + protected virtual void HandleDatagram(byte[] data) { - List cutDatagrams = []; - int offset = 0; - while (offset < datagram.Length) + foreach (IZ21ResponseHandler handler in _z21ResponseHandlers) { try { - if (offset + 2 > datagram.Length) - { - _logger?.LogError("Incomplete DataLen field — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); - return cutDatagrams; - } - - ushort dataLen = (ushort)(datagram[offset] | (datagram[offset + 1] << 8)); - - if (offset + dataLen > datagram.Length) - { - _logger?.LogError("Incomplete packet — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); - return cutDatagrams; - } + if (!handler.CanHandle(data)) + continue; - byte[] cutDatagram = new byte[dataLen]; - Buffer.BlockCopy(datagram, offset, cutDatagram, 0, dataLen); - _logger?.LogDebug("Received cut datagram: {cutDatagram}", BitConverter.ToString(cutDatagram)); - offset += dataLen; - cutDatagrams.Add(cutDatagram); + _logger?.LogDebug("{handlerName} handling datagram {cutDatagram}.", handler.Name, BitConverter.ToString(data)); + handler.Handle(data); } catch (System.Exception exception) { - _logger?.LogError(exception, "Failed to cut datagram — discarding remainder. Data: {datagram}", BitConverter.ToString(datagram)); - return cutDatagrams; + _logger?.LogError(exception, "{handlerName} failed to handle datagram {cutDatagram}.", handler.Name, BitConverter.ToString(data)); } } - - return cutDatagrams; } } -} \ No newline at end of file +} diff --git a/src/Z21.Client/Core/Z21Watchdog.cs b/src/Z21.Client/Core/Z21Watchdog.cs deleted file mode 100644 index 1b61689..0000000 --- a/src/Z21.Client/Core/Z21Watchdog.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Net.NetworkInformation; -using System.Timers; -using Z21.Core.Model; -using Z21.Core.Model.EventArgs; - -namespace Z21.Core -{ - public sealed class Z21Watchdog - { - private readonly Z21Configuration _configuration; - private readonly Timer _timer; - private bool? _lastReachable; - - public event EventHandler? OnReachabilityChanged; - - public Z21Watchdog(Z21Configuration configuration) - { - _configuration = configuration; - - _timer = new(TimeSpan.FromSeconds(1)) - { - AutoReset = true, - Enabled = true - }; - _timer.Elapsed += (_, _) => CheckState(); - } - - private void CheckState() - { - var reachable = IsReachable(); - - if (_lastReachable == reachable) - return; - - _lastReachable = reachable; - OnReachabilityChanged?.Invoke(this, new (reachable)); - } - - private bool IsReachable() - { - try - { - using var ping = new Ping(); - var reply = ping.Send(_configuration.ClientIPEndPoint.Address, 1000); - return reply.Status == IPStatus.Success; - } - catch - { - return false; - } - } - } -} \ No newline at end of file diff --git a/src/Z21.Client/GlobalUsings.cs b/src/Z21.Client/GlobalUsings.cs new file mode 100644 index 0000000..bffbd65 --- /dev/null +++ b/src/Z21.Client/GlobalUsings.cs @@ -0,0 +1 @@ +global using CommandStation.Model; diff --git a/src/Z21.Client/Transport/IZ21Transport.cs b/src/Z21.Client/Transport/IZ21Transport.cs deleted file mode 100644 index e12ffb7..0000000 --- a/src/Z21.Client/Transport/IZ21Transport.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Threading.Tasks; -using Z21.Core.Model; -using Z21.Core.Model.EventArgs; - -namespace Z21.Transport -{ - public interface IZ21Transport - { - bool IsConnected { get; } - event EventHandler? OnResponseReceived; - - public Z21Configuration Z21Configuration { get; } - - void Connect(); - - Task SendAsync(byte[] datagram); - } -} \ No newline at end of file diff --git a/src/Z21.Client/Transport/Z21Transport.cs b/src/Z21.Client/Transport/Z21Transport.cs deleted file mode 100644 index fca0f0d..0000000 --- a/src/Z21.Client/Transport/Z21Transport.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Threading.Tasks; -using Z21.Core.Model; -using Z21.Core.Model.EventArgs; - -namespace Z21.Transport -{ - public class Z21Transport : IZ21Transport, IDisposable - { - private Lazy _udpClient; - - public Z21Transport(Z21Configuration z21Configuration) - { - ArgumentNullException.ThrowIfNull(z21Configuration); - Z21Configuration = z21Configuration; - Z21Configuration.ConfigurationUpdated += (_, _) => _udpClient = new(UdpClientFactory()); - _udpClient = new(UdpClientFactory()); - } - - private UdpClient UdpClientFactory() - { - if (_udpClient?.IsValueCreated == true) - _udpClient.Value.Dispose(); - - var udpClient = new UdpClient(Z21Configuration.ClientIPEndPoint.Port); - - if (OperatingSystem.IsWindows()) - udpClient.AllowNatTraversal(Z21Configuration.AllowNatTraversal); - return udpClient; - } - - public event EventHandler? OnResponseReceived; - - public bool IsConnected { get; private set; } = false; - - public Z21Configuration Z21Configuration { get; } - - public void Connect() - { - _udpClient.Value.Connect(Z21Configuration.ClientIPEndPoint); - _udpClient.Value.BeginReceive(Receiving, null); - IsConnected = true; - } - - private void Receiving(IAsyncResult res) - { - IPEndPoint? remoteIpEndPoint = null!; - byte[] received = _udpClient.Value.EndReceive(res, ref remoteIpEndPoint); - _udpClient.Value.BeginReceive(Receiving, null); - - if (remoteIpEndPoint is not null - && remoteIpEndPoint.Equals(Z21Configuration.ClientIPEndPoint)) - OnResponseReceived?.Invoke(this, new(received)); - } - - public async Task SendAsync(byte[] datagram) - { - ArgumentNullException.ThrowIfNull(datagram); - await _udpClient.Value.SendAsync(datagram, datagram.GetLength(0)); - } - - public void Dispose() - { - if (_udpClient.IsValueCreated) - _udpClient.Value.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/Z21.Client/Z21.Client.csproj b/src/Z21.Client/Z21.Client.csproj index 42b901f..47b9df8 100644 --- a/src/Z21.Client/Z21.Client.csproj +++ b/src/Z21.Client/Z21.Client.csproj @@ -4,17 +4,17 @@ enable 12 True + true + $(NoWarn);CS1591;CS1573 Z21 Jakob Eichberger Z21Client partially implements the z21 lan protocoll for the digital command center z21/Z21 from Roco/Fleischmann. - https://github.com/Jakob-Eichberger/z21Client + https://github.com/jaak0b/Z21 Z21;Roco;Fleischman Debug - 4.0.0 x64 enable net8.0;net8.0-windows - 6.0.0 true LICENSE Z21 @@ -26,6 +26,11 @@ + + + + + diff --git a/src/Z21.Console/Command/CliGetFirmwareVersionCommand.cs b/src/Z21.Console/Command/CliGetFirmwareVersionCommand.cs index e183c7d..121f7b0 100644 --- a/src/Z21.Console/Command/CliGetFirmwareVersionCommand.cs +++ b/src/Z21.Console/Command/CliGetFirmwareVersionCommand.cs @@ -14,7 +14,7 @@ public class CliGetFirmwareVersionCommand : Command { override public int Execute([NotNull] CommandContext context, [NotNull] GetFirmwareVersionSettings settings) { - Program.Z21Client.SendCommandsAsync(new GetFirmwareVersionCommand()); + Program.Station.RequestFirmwareVersionAsync(); return 0; } } diff --git a/src/Z21.Console/Command/CliSetTrackPowerCommand.cs b/src/Z21.Console/Command/CliSetTrackPowerCommand.cs index 398a575..96a8dff 100644 --- a/src/Z21.Console/Command/CliSetTrackPowerCommand.cs +++ b/src/Z21.Console/Command/CliSetTrackPowerCommand.cs @@ -33,13 +33,13 @@ override public int Execute([NotNull] CommandContext context, [NotNull] SetTrack { if (settings.On) { - Program.Z21Client.SendCommandsAsync(new SetTrackPowerOnCommand()); + Program.Station.TrackPowerOnAsync(); return 0; } if (settings.Off) { - Program.Z21Client.SendCommandsAsync(new SetTrackPowerOffCommand()); + Program.Station.TrackPowerOffAsync(); return 0; } diff --git a/src/Z21.Console/Program.cs b/src/Z21.Console/Program.cs index b7e5a78..1b54261 100644 --- a/src/Z21.Console/Program.cs +++ b/src/Z21.Console/Program.cs @@ -12,7 +12,7 @@ namespace Z21.Console { abstract internal class Program { - internal static IZ21Client Z21Client = null!; + internal static IZ21CommandStation Station = null!; public static void Main(string[] args) { @@ -26,9 +26,9 @@ public static void Main(string[] args) builder.RegisterSerilog(log); var container = builder.Build(); - Z21Client = container.Resolve(); - - Z21Client.ConnectAsync(); + Station = container.Resolve(); + + Station.ConnectAsync(); CommandApp app = new(); diff --git a/src/Z21.DependencyInjection.UnitTest/SpyTransport.cs b/src/Z21.DependencyInjection.UnitTest/SpyTransport.cs new file mode 100644 index 0000000..b1d1812 --- /dev/null +++ b/src/Z21.DependencyInjection.UnitTest/SpyTransport.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using CommandStation.Transport; + +namespace Z21.DependencyInjection.UnitTest +{ + public class SpyTransport : ITransport + { + public bool IsConnected { get; private set; } + + public event EventHandler? OnBytesReceived; + + public event EventHandler? OnConnectionChanged; + + public Task ConnectAsync() + { + IsConnected = true; + OnConnectionChanged?.Invoke(this, new ConnectionChangedEventArgs(true)); + return Task.CompletedTask; + } + + public Task DisconnectAsync() + { + IsConnected = false; + OnConnectionChanged?.Invoke(this, new ConnectionChangedEventArgs(false)); + return Task.CompletedTask; + } + + public Task SendAsync(ReadOnlyMemory data) => Task.CompletedTask; + + public void RaiseBytes(byte[] data) => OnBytesReceived?.Invoke(this, new BytesReceivedEventArgs(data)); + } +} diff --git a/src/Z21.DependencyInjection.UnitTest/Z21DependencyInjectionExtensionTest.cs b/src/Z21.DependencyInjection.UnitTest/Z21DependencyInjectionExtensionTest.cs index d94bc47..c124184 100644 --- a/src/Z21.DependencyInjection.UnitTest/Z21DependencyInjectionExtensionTest.cs +++ b/src/Z21.DependencyInjection.UnitTest/Z21DependencyInjectionExtensionTest.cs @@ -1,11 +1,76 @@ +using CommandStation; +using CommandStation.Model; +using CommandStation.Transport; using Microsoft.Extensions.DependencyInjection; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; using Z21.Core.ResponseHandler; +using Z21.Core.ResponseHandler.Settings; using Z21.Core.ResponseHandler.SystemState; namespace Z21.DependencyInjection.UnitTest { public class Z21DependencyInjectionExtensionTest { + [Test] + public void AddZ21_WithoutHost_ResolvingCommandStation_WiresInboundHandling() + { + ServiceCollection services = new(); + services.AddZ21(); + SpyTransport transport = new(); + services.AddSingleton(transport); + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + ILocoControl station = serviceProvider.GetRequiredService() as ILocoControl + ?? throw new InvalidOperationException("Station does not support loco control."); + LocoInfoData? received = null; + station.LocoInfoReceived += (_, data) => received = data; + + transport.RaiseBytes([0x0F, 0x00, 0x40, 0x00, 0xEF, 0x00, 0x03, 0x02, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69]); + + Assert.That(received, Is.Not.Null); + Assert.That(received!.LocoAddress, Is.EqualTo(3)); + } + + [Test] + public void AddZ21_WithoutHost_NewHandler_IsDiscoveredAndFlowsThroughCapability() + { + ServiceCollection services = new(); + services.AddZ21(); + SpyTransport transport = new(); + services.AddSingleton(transport); + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + IFastClockControl station = serviceProvider.GetRequiredService() as IFastClockControl + ?? throw new InvalidOperationException("Station does not support fast clock control."); + ModelTime? received = null; + station.ModelTimeChanged += (_, time) => received = time; + + // LAN_FAST_CLOCK_DATA: day=0, hour=12, minute=30, second=45, rate=8 + transport.RaiseBytes([0x0C, 0x00, 0xCD, 0x00, 0x66, 0x25, 0x0C, 0x1E, 0x2D, 0x08, 0x80, 0x00]); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.Hour, Is.EqualTo(12)); + Assert.That(received.Minute, Is.EqualTo(30)); + Assert.That(received.Rate, Is.EqualTo(8)); + }); + } + + [Test] + public void AddZ21_ProviderDisposedSynchronously_DoesNotThrow() + { + ServiceCollection services = new(); + services.AddZ21(); + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Resolving the station instantiates the singleton UdpTransport, so the provider tracks it for disposal. + _ = serviceProvider.GetRequiredService(); + + Assert.DoesNotThrow(() => serviceProvider.Dispose()); + } + [Test] public void AddZ21_WithZ21ResponseHandlers_SameInstanceIsRegisteredForAllInterfaces() { @@ -29,5 +94,31 @@ public void AddZ21_WithZ21ResponseHandlers_SameInstanceIsRegisteredForAllInterfa Assert.That(implementationSpecificInterface, Is.SameAs(baseInterface)); }); } + + [Test] + public void AddZ21_WithoutHost_AccessoryModeFrame_IsDiscoveredAndDispatched() + { + ServiceCollection services = new(); + services.AddZ21(); + SpyTransport transport = new(); + services.AddSingleton(transport); + ServiceProvider serviceProvider = services.BuildServiceProvider(); + + // Resolving the station wires the dispatcher to the transport. + _ = serviceProvider.GetRequiredService(); + IAccessoryModeResponseHandler handler = serviceProvider.GetRequiredService(); + DecoderModeReceivedEventArgs? received = null; + handler.OnAccessoryModeReceived += (_, args) => received = args; + + // LAN_GET_TURNOUTMODE: address=12 (0x000C), mode=DCC (0x00) + transport.RaiseBytes([0x07, 0x00, 0x70, 0x00, 0x00, 0x0C, 0x00]); + + Assert.That(received, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(received!.LocoAddress, Is.EqualTo(12)); + Assert.That(received.Mode, Is.EqualTo(DecoderMode.DCC)); + }); + } } } \ No newline at end of file diff --git a/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj b/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj index 74e3789..bcfc4a2 100644 --- a/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj +++ b/src/Z21.DependencyInjection/Z21.DependencyInjection.csproj @@ -5,9 +5,10 @@ enable enable true + true + $(NoWarn);CS1591;CS1573 Jakob Eichberger - https://github.com/Jakob-Eichberger/z21Client - 6.0.0 + https://github.com/jaak0b/Z21 LICENSE true @@ -15,8 +16,6 @@ - - @@ -25,6 +24,7 @@ + diff --git a/src/Z21.DependencyInjection/Z21DependencyInjectionExtension.cs b/src/Z21.DependencyInjection/Z21DependencyInjectionExtension.cs index 43661cc..a6fb7c6 100644 --- a/src/Z21.DependencyInjection/Z21DependencyInjectionExtension.cs +++ b/src/Z21.DependencyInjection/Z21DependencyInjectionExtension.cs @@ -1,22 +1,40 @@ -using Microsoft.Extensions.DependencyInjection; +using CommandStation; +using CommandStation.Framing; +using CommandStation.Transport; +using CommandStation.Transport.Udp; +using Microsoft.Extensions.DependencyInjection; using Z21.Core; -using Z21.Core.Model; +using Z21.Core.Codecs; +using Z21.Core.Command; +using Z21.Core.Framing; +using Z21.Core.Reflection; using Z21.Core.ResponseHandler; using Z21.Core.ResponseParser; -using Z21.Transport; namespace Z21.DependencyInjection { - + public static class Z21DependencyInjectionExtension { - public static IServiceCollection AddZ21(this IServiceCollection services, Action? configurationAction = null) + public static IServiceCollection AddZ21(this IServiceCollection services, Action? transportConfiguration = null, Action? optionsConfiguration = null) { - services.AddSingleton(); - services.AddSingleton(); - services.AddActivatedSingleton(); - - services.ConfigureZ21Client(configurationAction); + services.AddSingleton(_ => + { + UdpTransportOptions options = new(); + transportConfiguration?.Invoke(options); + return options; + }); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(provider => provider.GetRequiredService()); + services.AddSingleton(); + + services.ConfigureZ21Options(optionsConfiguration); services.AddZ21ResponseParser(); services.AddZ21ResponseHandler(); return services; @@ -25,59 +43,37 @@ public static IServiceCollection AddZ21(this IServiceCollection services, Action /// /// Discovers all Z21 response handlers and registers them in the collection. /// - private static IServiceCollection AddZ21ResponseHandler(this IServiceCollection services) - { - ArgumentNullException.ThrowIfNull(services); - - Type baseInterface = typeof(IZ21ResponseHandler); - - IEnumerable handlerTypes = baseInterface.Assembly.GetTypes().Where(type => type is { IsClass: true, IsAbstract: false } && baseInterface.IsAssignableFrom(type)); + private static IServiceCollection AddZ21ResponseHandler(this IServiceCollection services) => + services.AddDiscovered(typeof(IZ21ResponseHandler), includeBaseInterface: true); - foreach (Type handlerType in handlerTypes) - { - // Get all interfaces this class implements that are in the chain to IZ21ResponseHandler - List interfacesToRegister = handlerType.GetInterfaces().Where(baseInterface.IsAssignableFrom).ToList(); - services.AddSingleton(handlerType); - foreach (Type serviceType in interfacesToRegister) - { - services.AddSingleton(serviceType, provider => provider.GetRequiredService(handlerType)); - } - } - - return services; - } + private static IServiceCollection AddZ21ResponseParser(this IServiceCollection services) => + services.AddDiscovered(typeof(IZ21ResponseParser), includeBaseInterface: false); - private static IServiceCollection AddZ21ResponseParser(this IServiceCollection services) + private static IServiceCollection AddDiscovered(this IServiceCollection services, Type baseInterface, bool includeBaseInterface) { ArgumentNullException.ThrowIfNull(services); - Type baseInterface = typeof(IZ21ResponseParser); - - IEnumerable handlerTypes = baseInterface.Assembly.GetTypes().Where(type => type is { IsClass: true, IsAbstract: false } && baseInterface.IsAssignableFrom(type)); + Z21ServiceDiscovery discovery = new(); - foreach (Type handlerType in handlerTypes) + foreach (Type implementationType in discovery.GetImplementations(baseInterface)) { - // Get all interfaces this class implements that are in the chain up to IZ21ResponseParser - List interfacesToRegister = handlerType.GetInterfaces().Where(type => baseInterface.IsAssignableFrom(type) && type != baseInterface).ToList(); - services.AddSingleton(handlerType); - foreach (Type serviceType in interfacesToRegister) - { - services.AddSingleton(serviceType, provider => provider.GetRequiredService(handlerType)); - } + services.AddSingleton(implementationType); + foreach (Type serviceType in discovery.GetServiceInterfaces(implementationType, baseInterface, includeBaseInterface)) + services.AddSingleton(serviceType, provider => provider.GetRequiredService(implementationType)); } return services; } - private static IServiceCollection ConfigureZ21Client(this IServiceCollection services, Action? configurationAction = null) // TODO: Test + private static IServiceCollection ConfigureZ21Options(this IServiceCollection services, Action? optionsConfiguration) { ArgumentNullException.ThrowIfNull(services); - Z21Configuration configuration = new(); - configurationAction?.Invoke(configuration); - services.AddSingleton(configuration); + Z21Options options = new(); + optionsConfiguration?.Invoke(options); + services.AddSingleton(options); return services; } } -} \ No newline at end of file +} diff --git a/src/Z21.SmokeTest/Z21.SmokeTest.csproj b/src/Z21.SmokeTest/Z21.SmokeTest.csproj new file mode 100644 index 0000000..269f5c8 --- /dev/null +++ b/src/Z21.SmokeTest/Z21.SmokeTest.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + false + true + Z21.SmokeTest + + + + + + + + + + + + + + + + + + + + diff --git a/src/Z21.SmokeTest/Z21HardwareTests.cs b/src/Z21.SmokeTest/Z21HardwareTests.cs new file mode 100644 index 0000000..8cbebbf --- /dev/null +++ b/src/Z21.SmokeTest/Z21HardwareTests.cs @@ -0,0 +1,403 @@ +using System; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using CommandStation.Model; +using Microsoft.Extensions.DependencyInjection; +using Z21.Core; +using Z21.Core.Command.SystemState; +using Z21.Core.Model; +using Z21.Core.Model.EventArgs; +using Z21.Core.ResponseHandler.SystemState; +using Z21.DependencyInjection; + +namespace Z21.SmokeTest +{ + /// + /// End-to-end smoke tests that exercise the library against a real Z21 on the network. These are + /// and tagged Hardware, so the normal dotnet test / + /// CI sweep discovers but never runs them. Run them on demand with a live command station: + /// + /// $env:Z21_ENDPOINT="192.168.0.111:21105"; $env:Z21_LOCO="3" + /// dotnet test src/Z21.SmokeTest --filter "TestCategory=Hardware" + /// + /// Set Z21_READONLY=1 to skip the destructive (track power / driving / turnout) tests. + /// + [TestFixture] + [Category("Hardware")] + [Explicit("Requires a live Z21 on the network; set Z21_ENDPOINT to run.")] + public class Z21HardwareTests + { + private readonly TimeSpan _eventTimeout = TimeSpan.FromSeconds(5); + private readonly TimeSpan _settleDelay = TimeSpan.FromMilliseconds(300); + + private ServiceProvider _provider = null!; + private IZ21CommandStation _station = null!; + private ushort _locoAddress; + private bool _readOnly; + + [OneTimeSetUp] + public async Task ConnectToZ21() + { + string? endpoint = Environment.GetEnvironmentVariable("Z21_ENDPOINT"); + if (string.IsNullOrWhiteSpace(endpoint)) + Assert.Ignore("Set Z21_ENDPOINT (e.g. 192.168.0.111:21105) to run the Z21 hardware tests."); + + string[] hostPort = endpoint!.Split(':'); + IPAddress ip = IPAddress.Parse(hostPort[0]); + int port = hostPort.Length > 1 ? int.Parse(hostPort[1]) : 21105; + _locoAddress = ushort.TryParse(Environment.GetEnvironmentVariable("Z21_LOCO"), out ushort loco) ? loco : (ushort)3; + _readOnly = IsTruthy(Environment.GetEnvironmentVariable("Z21_READONLY")); + + var services = new ServiceCollection(); + services.AddZ21( + t => t.RemoteEndPoint = new IPEndPoint(ip, port), + o => o.BroadcastFlags = + [ + Z21BroadcastFlags.DriveAndSwitchingMessages, + Z21BroadcastFlags.LocoInfoChangedMessages, + Z21BroadcastFlags.SystemStateDataChangedMessages, + ]); + + _provider = services.BuildServiceProvider(); + _station = _provider.GetRequiredService(); + + await _station.ConnectAsync(); + + Task firmware = NextEventAsync( + h => _station.FirmwareVersionReceived += h, + h => _station.FirmwareVersionReceived -= h, + _eventTimeout); + await _station.RequestFirmwareVersionAsync(); + await firmware; + + Assert.That(_station.IsConnected, Is.True, "Station did not connect to the Z21."); + } + + [OneTimeTearDown] + public async Task DisconnectFromZ21() + { + if (_station is not null && _station.IsConnected) + { + if (!_readOnly) + { + await _station.DriveAsync(_locoAddress, DccSpeedMode.Steps128, DrivingDirection.Forward, 0); + await _station.TrackPowerOffAsync(); + } + await _station.DisconnectAsync(); + } + + if (_provider is not null) + await _provider.DisposeAsync(); + } + + [Test] + [Order(1)] + public void Connects_AndReportsConnected() + => Assert.That(_station.IsConnected, Is.True); + + [Test] + [Order(2)] + public async Task SerialNumber_IsReported() + { + ISerialNumberResponseHandler handler = _provider.GetRequiredService(); + Task serial = NextEventAsync( + h => handler.OnSerialNumberReceived += h, + h => handler.OnSerialNumberReceived -= h, + _eventTimeout); + + await _station.SendCommandsAsync(_station.Commands.Create()); + + SerialNumberReceivedEventArgs args = await serial; + Assert.That(args.SerialNumber, Is.GreaterThan(0u)); + } + + [Test] + [Order(3)] + public async Task HardwareInfo_IsReported() + { + IHardwareInfoResponseHandler handler = _provider.GetRequiredService(); + Task hardware = NextEventAsync( + h => handler.OnHardwareInfoReceived += h, + h => handler.OnHardwareInfoReceived -= h, + _eventTimeout); + + await _station.SendCommandsAsync(_station.Commands.Create()); + + HardwareInfoEventArgs args = await hardware; + Assert.Multiple(() => + { + Assert.That(args.Z21HardwareType, Is.GreaterThan(0)); + Assert.That(args.FirmwareVersion, Is.GreaterThan(0)); + }); + } + + [Test] + [Order(4)] + public async Task FirmwareVersion_IsReported() + { + Task firmware = NextEventAsync( + h => _station.FirmwareVersionReceived += h, + h => _station.FirmwareVersionReceived -= h, + _eventTimeout); + + await _station.RequestFirmwareVersionAsync(); + + FirmwareVersion version = await firmware; + Assert.That(version.Major, Is.GreaterThan(0)); + } + + [Test] + [Order(5)] + public async Task XBusVersion_IsReported() + { + IVersionResponseHandler handler = _provider.GetRequiredService(); + Task version = NextEventAsync( + h => handler.OnVersionReceived += h, + h => handler.OnVersionReceived -= h, + _eventTimeout); + + await _station.SendCommandsAsync(_station.Commands.Create()); + + VersionReceivedEventArgs args = await version; + Assert.That(args.CommandStationId, Is.GreaterThan(0)); + } + + [Test] + [Order(6)] + public async Task BroadcastFlags_AreReported() + { + IBroadcastFlagsResponseHandler handler = _provider.GetRequiredService(); + Task flags = NextEventAsync( + h => handler.OnBroadcastFlagsReceived += h, + h => handler.OnBroadcastFlagsReceived -= h, + _eventTimeout); + + await _station.SendCommandsAsync(_station.Commands.Create()); + + BroadcastFlagsReceivedEventArgs args = await flags; + Assert.That(args.BroadCastFlag, Is.Not.Zero); + } + + [Test] + [Order(7)] + public async Task SystemState_IsReported() + { + Task state = NextEventAsync( + h => _station.SystemStateReceived += h, + h => _station.SystemStateReceived -= h, + _eventTimeout); + + await _station.RequestSystemStateAsync(); + + SystemState systemState = await state; + Assert.Multiple(() => + { + Assert.That(systemState.CentralState, Is.Not.Null); + Assert.That(systemState.SupplyVoltage, Is.GreaterThan(0)); + }); + } + + [Test] + [Order(8)] + public async Task Status_IsReported() + { + Task status = NextEventAsync( + h => _station.StatusChanged += h, + h => _station.StatusChanged -= h, + _eventTimeout); + + await _station.RequestStatusAsync(); + + CentralState centralState = await status; + Assert.That(centralState, Is.Not.Null); + } + + [Test] + [Order(10)] + public async Task TrackPower_OnOffOn_RaisesTrackPowerChanged() + { + SkipIfReadOnly(); + + await _station.TrackPowerOffAsync(); + await Task.Delay(_settleDelay); + + Assert.That(await ExpectPowerChangeAsync(true), Is.True); + Assert.That(await ExpectPowerChangeAsync(false), Is.False); + Assert.That(await ExpectPowerChangeAsync(true), Is.True); + } + + [Test] + [Order(11)] + public async Task Drive_RampForward_ReportsSpeedAndDirection() + { + SkipIfReadOnly(); + await EnsurePowerOnAsync(); + + await _station.DriveAsync(_locoAddress, DccSpeedMode.Steps128, DrivingDirection.Forward, 10); + await _station.DriveAsync(_locoAddress, DccSpeedMode.Steps128, DrivingDirection.Forward, 40); + await _station.DriveAsync(_locoAddress, DccSpeedMode.Steps128, DrivingDirection.Forward, 80); + + LocoInfoData info = await RequestLocoInfoAsync(); + Assert.Multiple(() => + { + Assert.That(info.LocoAddress, Is.EqualTo(_locoAddress)); + Assert.That(info.DrivingDirection, Is.EqualTo(DrivingDirection.Forward)); + Assert.That(info.LocoSpeed, Is.GreaterThan(0)); + }); + } + + [Test] + [Order(12)] + public async Task Functions_F0F1_Toggle() + { + SkipIfReadOnly(); + await EnsurePowerOnAsync(); + + await _station.SetFunctionAsync(_locoAddress, 0, FunctionToggleType.On); + LocoInfoData onInfo = await RequestLocoInfoAsync(); + Assert.That( + onInfo.LocoFunctionsData.Any(f => f.FunctionIndex == 0 && f.FunctionToggleType == FunctionToggleType.On), + Is.True, + "F0 (lights) should be reported as On."); + + await _station.SetFunctionAsync(_locoAddress, 1, FunctionToggleType.On); + await _station.SetFunctionAsync(_locoAddress, 1, FunctionToggleType.Off); + LocoInfoData offInfo = await RequestLocoInfoAsync(); + Assert.That( + offInfo.LocoFunctionsData.Any(f => f.FunctionIndex == 1 && f.FunctionToggleType == FunctionToggleType.Off), + Is.True, + "F1 should be reported as Off after toggling on then off."); + } + + [Test] + [Order(13)] + public async Task Drive_Reverse_ReportsBackward() + { + SkipIfReadOnly(); + await EnsurePowerOnAsync(); + + await _station.DriveAsync(_locoAddress, DccSpeedMode.Steps128, DrivingDirection.Backward, 30); + + LocoInfoData info = await RequestLocoInfoAsync(); + Assert.That(info.DrivingDirection, Is.EqualTo(DrivingDirection.Backward)); + } + + [Test] + [Order(14)] + public async Task EmergencyStop_StopsLoco() + { + SkipIfReadOnly(); + await EnsurePowerOnAsync(); + + await _station.DriveAsync(_locoAddress, DccSpeedMode.Steps128, DrivingDirection.Forward, 50); + await _station.EmergencyStopAsync(_locoAddress); + + LocoInfoData info = await RequestLocoInfoAsync(); + Assert.That(info.LocoSpeed, Is.Zero); + } + + [Test] + [Order(15)] + public async Task Turnout_ActivateDeactivateRead_RaisesTurnoutInfo() + { + SkipIfReadOnly(); + await EnsurePowerOnAsync(); + + await _station.SetTurnoutAsync(1, AccessoryOutput.Output1, AccessoryState.Activate, true); + await _station.SetTurnoutAsync(1, AccessoryOutput.Output1, AccessoryState.Deactivate, true); + + Task turnout = NextEventAsync( + h => _station.TurnoutInfoReceived += h, + h => _station.TurnoutInfoReceived -= h, + _eventTimeout, + t => t.AccessoryAddress == 1); + await _station.RequestTurnoutInfoAsync(1); + + TurnoutInfo info = await turnout; + Assert.That(info.AccessoryAddress, Is.EqualTo((ushort)1)); + } + + private void SkipIfReadOnly() + { + if (_readOnly) + Assert.Ignore("Z21_READONLY is set; skipping track power, driving and turnout tests."); + } + + private bool IsTruthy(string? value) => + value is not null && (value == "1" + || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) + || string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase)); + + private async Task EnsurePowerOnAsync() + { + await _station.TrackPowerOnAsync(); + await Task.Delay(_settleDelay); + } + + private async Task ExpectPowerChangeAsync(bool on) + { + Task change = NextEventAsync( + h => _station.TrackPowerChanged += h, + h => _station.TrackPowerChanged -= h, + _eventTimeout, + state => state == on); + + if (on) + await _station.TrackPowerOnAsync(); + else + await _station.TrackPowerOffAsync(); + + return await change; + } + + private async Task RequestLocoInfoAsync() + { + Task info = NextEventAsync( + h => _station.LocoInfoReceived += h, + h => _station.LocoInfoReceived -= h, + _eventTimeout, + data => data.LocoAddress == _locoAddress); + + await _station.RequestLocoInfoAsync(_locoAddress); + + return await info; + } + + private async Task NextEventAsync( + Action> subscribe, + Action> unsubscribe, + TimeSpan timeout, + Func? predicate = null) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + EventHandler handler = (_, args) => + { + if (predicate is null || predicate(args)) + completion.TrySetResult(args); + }; + + subscribe(handler); + try + { + using var cancellation = new CancellationTokenSource(timeout); + await using (cancellation.Token.Register(() => completion.TrySetCanceled(cancellation.Token))) + { + try + { + return await completion.Task; + } + catch (OperationCanceledException) + { + throw new TimeoutException($"No matching {typeof(TArgs).Name} event was received within {timeout.TotalSeconds:0.#}s."); + } + } + } + finally + { + unsubscribe(handler); + } + } + } +} diff --git a/src/Z21.sln b/src/Z21.sln index aad54d0..9990883 100644 --- a/src/Z21.sln +++ b/src/Z21.sln @@ -17,70 +17,156 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Z21.Autofac", "Z21.Autofac\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Z21.Autofac.UnitTests", "Z21.Autofac.UnitTests\Z21.Autofac.UnitTests.csproj", "{2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandStation.Abstractions", "CommandStation.Abstractions\CommandStation.Abstractions.csproj", "{FD91D131-D6CF-438C-8AE7-7C39AB743081}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandStation.Transport.Udp", "CommandStation.Transport.Udp\CommandStation.Transport.Udp.csproj", "{94F51955-BE73-449F-9C92-2BA271B99E70}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommandStation.Transport.Udp.UnitTest", "CommandStation.Transport.Udp.UnitTest\CommandStation.Transport.Udp.UnitTest.csproj", "{E4149B7B-47F2-4697-9405-0B770B2AB106}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Z21.SmokeTest", "Z21.SmokeTest\Z21.SmokeTest.csproj", "{1480FB67-F911-4279-BEE7-0F94DFE4D6BC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Debug|Any CPU.ActiveCfg = Debug|x64 + {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Debug|Any CPU.Build.0 = Debug|x64 {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Debug|x64.ActiveCfg = Debug|x64 {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Debug|x64.Build.0 = Debug|x64 + {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Debug|x86.ActiveCfg = Debug|Any CPU + {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Debug|x86.Build.0 = Debug|Any CPU {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Release|Any CPU.ActiveCfg = Debug|x64 {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Release|Any CPU.Build.0 = Debug|x64 {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Release|x64.ActiveCfg = Debug|x64 {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Release|x64.Build.0 = Debug|x64 - {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Debug|Any CPU.Build.0 = Debug|x64 + {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Release|x86.ActiveCfg = Release|Any CPU + {A6BAE63F-5DBE-4AE7-B2C5-5CB89C25BA15}.Release|x86.Build.0 = Release|Any CPU {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Debug|Any CPU.Build.0 = Debug|Any CPU {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Debug|x64.ActiveCfg = Debug|Any CPU {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Debug|x64.Build.0 = Debug|Any CPU + {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Debug|x86.ActiveCfg = Debug|Any CPU + {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Debug|x86.Build.0 = Debug|Any CPU {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Release|Any CPU.ActiveCfg = Release|Any CPU {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Release|Any CPU.Build.0 = Release|Any CPU {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Release|x64.ActiveCfg = Release|Any CPU {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Release|x64.Build.0 = Release|Any CPU + {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Release|x86.ActiveCfg = Release|Any CPU + {A3246DA8-ACBE-435D-B050-CA7595512C1A}.Release|x86.Build.0 = Release|Any CPU {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Debug|Any CPU.Build.0 = Debug|Any CPU {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Debug|x64.ActiveCfg = Debug|Any CPU {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Debug|x64.Build.0 = Debug|Any CPU + {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Debug|x86.ActiveCfg = Debug|Any CPU + {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Debug|x86.Build.0 = Debug|Any CPU {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Release|Any CPU.Build.0 = Release|Any CPU {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Release|x64.ActiveCfg = Release|Any CPU {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Release|x64.Build.0 = Release|Any CPU + {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Release|x86.ActiveCfg = Release|Any CPU + {6B823598-684E-4EAC-BFD5-93BFD43531D8}.Release|x86.Build.0 = Release|Any CPU {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Debug|Any CPU.Build.0 = Debug|Any CPU {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Debug|x64.ActiveCfg = Debug|Any CPU {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Debug|x64.Build.0 = Debug|Any CPU + {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Debug|x86.ActiveCfg = Debug|Any CPU + {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Debug|x86.Build.0 = Debug|Any CPU {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Release|Any CPU.ActiveCfg = Release|Any CPU {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Release|Any CPU.Build.0 = Release|Any CPU {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Release|x64.ActiveCfg = Release|Any CPU {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Release|x64.Build.0 = Release|Any CPU + {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Release|x86.ActiveCfg = Release|Any CPU + {4355685F-8F0B-451A-93AA-68C5752AD2B8}.Release|x86.Build.0 = Release|Any CPU {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Debug|Any CPU.Build.0 = Debug|Any CPU {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Debug|x64.ActiveCfg = Debug|Any CPU {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Debug|x64.Build.0 = Debug|Any CPU + {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Debug|x86.ActiveCfg = Debug|Any CPU + {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Debug|x86.Build.0 = Debug|Any CPU {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Release|Any CPU.ActiveCfg = Release|Any CPU {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Release|Any CPU.Build.0 = Release|Any CPU {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Release|x64.ActiveCfg = Release|Any CPU {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Release|x64.Build.0 = Release|Any CPU + {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Release|x86.ActiveCfg = Release|Any CPU + {B1276BFA-A2F1-4A4A-8081-BA1191833D39}.Release|x86.Build.0 = Release|Any CPU {0D027593-0A9B-4F30-8501-FCA46245A260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0D027593-0A9B-4F30-8501-FCA46245A260}.Debug|Any CPU.Build.0 = Debug|Any CPU {0D027593-0A9B-4F30-8501-FCA46245A260}.Debug|x64.ActiveCfg = Debug|Any CPU {0D027593-0A9B-4F30-8501-FCA46245A260}.Debug|x64.Build.0 = Debug|Any CPU + {0D027593-0A9B-4F30-8501-FCA46245A260}.Debug|x86.ActiveCfg = Debug|Any CPU + {0D027593-0A9B-4F30-8501-FCA46245A260}.Debug|x86.Build.0 = Debug|Any CPU {0D027593-0A9B-4F30-8501-FCA46245A260}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D027593-0A9B-4F30-8501-FCA46245A260}.Release|Any CPU.Build.0 = Release|Any CPU {0D027593-0A9B-4F30-8501-FCA46245A260}.Release|x64.ActiveCfg = Release|Any CPU {0D027593-0A9B-4F30-8501-FCA46245A260}.Release|x64.Build.0 = Release|Any CPU + {0D027593-0A9B-4F30-8501-FCA46245A260}.Release|x86.ActiveCfg = Release|Any CPU + {0D027593-0A9B-4F30-8501-FCA46245A260}.Release|x86.Build.0 = Release|Any CPU {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Debug|x64.ActiveCfg = Debug|Any CPU {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Debug|x64.Build.0 = Debug|Any CPU + {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Debug|x86.Build.0 = Debug|Any CPU {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Release|Any CPU.Build.0 = Release|Any CPU {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Release|x64.ActiveCfg = Release|Any CPU {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Release|x64.Build.0 = Release|Any CPU + {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Release|x86.ActiveCfg = Release|Any CPU + {2088EA8E-4AC5-4CB3-B273-C93A0A3988EF}.Release|x86.Build.0 = Release|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Debug|x64.ActiveCfg = Debug|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Debug|x64.Build.0 = Debug|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Debug|x86.ActiveCfg = Debug|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Debug|x86.Build.0 = Debug|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Release|Any CPU.Build.0 = Release|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Release|x64.ActiveCfg = Release|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Release|x64.Build.0 = Release|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Release|x86.ActiveCfg = Release|Any CPU + {FD91D131-D6CF-438C-8AE7-7C39AB743081}.Release|x86.Build.0 = Release|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Debug|x64.ActiveCfg = Debug|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Debug|x64.Build.0 = Debug|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Debug|x86.ActiveCfg = Debug|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Debug|x86.Build.0 = Debug|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Release|Any CPU.Build.0 = Release|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Release|x64.ActiveCfg = Release|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Release|x64.Build.0 = Release|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Release|x86.ActiveCfg = Release|Any CPU + {94F51955-BE73-449F-9C92-2BA271B99E70}.Release|x86.Build.0 = Release|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Debug|x64.ActiveCfg = Debug|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Debug|x64.Build.0 = Debug|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Debug|x86.ActiveCfg = Debug|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Debug|x86.Build.0 = Debug|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Release|Any CPU.Build.0 = Release|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Release|x64.ActiveCfg = Release|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Release|x64.Build.0 = Release|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Release|x86.ActiveCfg = Release|Any CPU + {E4149B7B-47F2-4697-9405-0B770B2AB106}.Release|x86.Build.0 = Release|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Debug|x64.ActiveCfg = Debug|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Debug|x64.Build.0 = Debug|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Debug|x86.ActiveCfg = Debug|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Debug|x86.Build.0 = Debug|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Release|Any CPU.Build.0 = Release|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Release|x64.ActiveCfg = Release|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Release|x64.Build.0 = Release|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Release|x86.ActiveCfg = Release|Any CPU + {1480FB67-F911-4279-BEE7-0F94DFE4D6BC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/api/z21-lan-protokoll.md b/src/api/z21-lan-protokoll.md new file mode 100644 index 0000000..6d19c60 --- /dev/null +++ b/src/api/z21-lan-protokoll.md @@ -0,0 +1,1589 @@ +# Z21 LAN Protokoll Spezifikation + +**Dokumentenversion 1.13 — 06.11.2023** + +--- + +## Rechtliches, Haftungsausschluss + +Die Firma Modelleisenbahn GmbH erklärt ausdrücklich, in keinem Fall für den Inhalt in diesem Dokument oder für in diesem Dokument angegebene weiterführende Informationen rechtlich haftbar zu sein. + +Die Rechtsverantwortung liegt ausschließlich beim Verwender der angegebenen Daten oder beim Herausgeber der jeweiligen weiterführenden Information. + +Für sämtliche Schäden die durch die Verwendung der angegebenen Informationen oder durch die Nicht-Verwendung der angegebenen Informationen entstehen übernimmt die Modelleisenbahn GmbH, Plainbachstraße 4, A-5101 Bergheim, Austria, ausdrücklich keinerlei Haftung. + +Die Modelleisenbahn GmbH, Plainbachstraße 4, A-5101 Bergheim, Austria, übernimmt keinerlei Gewähr für die Aktualität, Korrektheit, Vollständigkeit oder Qualität der bereitgestellten Informationen. Haftungsansprüche, welche sich auf Schäden materieller, immaterieller oder ideeller Art beziehen, die durch die Nutzung oder Nichtnutzung der dargebotenen Informationen verursacht wurden, sind grundsätzlich ausgeschlossen. + +Die Modelleisenbahn GmbH behält es sich vor, die bereitgestellten Informationen ohne gesonderte Ankündigung zu verändern, zu ergänzen oder zu löschen. + +Alle innerhalb des Dokuments genannten und gegebenenfalls durch Dritte geschützten Marken- und Warenzeichen unterliegen uneingeschränkt den Bestimmungen des jeweils gültigen Kennzeichenrechts und den Besitzrechten der jeweiligen eingetragenen Eigentümer. + +Das Copyright für veröffentlichte, von der Modelleisenbahn GmbH erstellte Informationen, bleibt in jedem Fall allein bei der Modelleisenbahn GmbH. + +Eine Vervielfältigung oder Verwendung der bereitgestellten Informationen in anderen elektronischen oder gedruckten Publikationen ist ohne ausdrückliche Zustimmung nicht gestattet. + +Sollten Teile oder einzelne Formulierungen des Haftungsausschlusses der geltenden Rechtslage nicht, nicht mehr oder nicht vollständig entsprechen, bleiben die übrigen Teile des Haftungsausschlusses in ihrem Inhalt und ihrer Gültigkeit davon unberührt. + +### Impressum + +- Apple, iPad, iPhone, iOS are trademarks of Apple Inc., registered in the U.S. and other countries. +- App Store is a service mark of Apple Inc. +- Android is a trademark of Google Inc. +- Google Play is a service mark of Google Inc. +- RailCom und XpressNet sind eingetragene Warenzeichen der Firma Lenz Elektronik GmbH. +- Motorola is a registered trademark of Motorola Inc., Tempe-Phoenix, USA. +- LocoNet is a registered trademark of Digitrax, Inc. + +Alle Rechte, Änderungen, Irrtümer und Liefermöglichkeiten vorbehalten. Spezifikationen und Abbildungen ohne Gewähr. Änderung vorbehalten. + +*Herausgeber: Modelleisenbahn GmbH, Plainbachstraße 4, A-5101 Bergheim, Austria* + +--- + +## Änderungshistorie + +| Datum | Version | Änderung | +|---|---|---| +| 06.02.2013 | 1.00 | Beschreibung der LAN Schnittstelle für Z21 FW Version 1.10, 1.11 und SmartRail FW Version 1.12 | +| 20.03.2013 | 1.01 | Z21 FW Version 1.20 — `LAN_SET_BROADCASTFLAGS`: neue Flags; `LAN_GET_HWINFO`: neuer Befehl; `LAN_SET_TURNOUTMODE`: MM-Format; LocoNet: Gateway-Funktionalität. SmartRail FW 1.13 — `LAN_GET_HWINFO`: neuer Befehl | +| 29.10.2013 | 1.02 | Z21 FW Version 1.22: Decoder CV Lesen und Schreiben; POM Lesen und Accessory Decoder: neue Befehle; LocoNet Dispatch und Gleisbesetztmelder; `LAN_LOCONET_DISPATCH_ADDR`: neue Antwort; `LAN_SET_BROADCASTFLAGS`: neues Flag; `LAN_LOCONET_DETECTOR`: neuer Befehl | +| 12.02.2014 | 1.03 | Z21 FW Version 1.23 — Korrektur lange Fahrzeugadresse in Kapitel 4; `LAN_X_MM_WRITE_BYTE`; `LAN_LOCONET_DETECTOR`: Erweiterung für LISSY | +| 25.03.2014 | 1.04 | Z21 FW Version 1.24 — `LAN_SET_BROADCASTFLAGS`: Flag 0x00010000; Kapitel 5: Weichenadressierung; `LAN_X_GET_TURNOUT_INFO`: Erweiterung Queue-Bit; `LAN_X_DCC_WRITE_REGISTER` | +| 21.01.2015 | 1.05 | Z21 FW Version 1.25 und 1.26 — Kapitel 4: Fahrstufen und Format; `LAN_X_DCC_READ_REGISTER`; `LAN_X_DCC_WRITE_REGISTER`; `LAN_LOCONET_Z21_TX` Binary State Control Instruction | +| 05.04.2016 | 1.06 | Z21 FW Version 1.28 — Kapitel 2 System Status Versionen: z21start; `LAN_GET_HW_INFO`; `LAN_GET_CODE` | +| 19.04.2017 | 1.07 | Z21 FW Version 1.29 und 1.30 — Kapitel 8 RailCom; Kapitel 10 CAN: Belegtmelder | +| 15.01.2018 | 1.08 | Kapitel 9 LocoNet: Lissy Beispiele | +| 23.05.2019 | 1.09 | Kapitel 4: Codierung der Geschwindigkeitsstufen; Kapitel 7 R-BUS: 10808 und 10819 hinzugefügt; Kapitel 9.3.1: Korrektur Binary State Control Instruction | +| 28.01.2021 | 1.10 | Z21 FW Version 1.40 — Kapitel 2 `LAN_GET_HWINFO`: weitere HW-Typen; Kapitel 5: Erweiterte Zubehördecoder DCCext; Kapitel 11 zLink | +| 11.08.2021 | 1.11 | Z21 FW Version 1.41 — Kapitel 10 CAN: Booster | +| 28.02.2022 | 1.12 | Z21 FW Version 1.42 — Kapitel 2.18 SystemState: cseRCN213, Capabilities; Kapitel 4: DCC Funktionen ≥ F29, Binary States; Kapitel 6: Tippfehler POM Read „111001MM" 0xE4 ausgebessert; Kapitel 10.2 und 11.2: Booster Management | +| 20.06.2023 | 1.13 | Z21 FW Version 1.43 — Kapitel 4: Motorola-Bit in `LAN_X_LOCO_INFO`; Kapitel 4: neue Befehle für Purge und E-STOP; Kapitel 12 Modellzeit | + +--- + +## Inhaltsverzeichnis + +1. **Grundlagen** + - 1.1 Kommunikation + - 1.2 Z21 Datensatz (Aufbau, X-BUS Protokoll Tunnelung, LocoNet Tunnelung) + - 1.3 Kombinieren von Datensätzen in einem UDP-Paket +2. **System, Status, Versionen** (2.1–2.21) +3. **Einstellungen** (3.1–3.4) +4. **Fahren** (4.1–4.6) +5. **Schalten** (5.1–5.6) +6. **Decoder CV Lesen und Schreiben** (6.1–6.14) +7. **Rückmelder – R-BUS** (7.1–7.3) +8. **RailCom** (8.1–8.2) +9. **LocoNet** (9.1–9.5) +10. **CAN** (10.1–10.2) +11. **zLink** (11.1–11.3) +12. **Modellzeit** (12.1–12.4) +- Anhang A – Befehlsübersicht + +--- + +## 1 Grundlagen + +### 1.1 Kommunikation + +Die Kommunikation mit der Z21 erfolgt per UDP über die Ports **21105** oder **21106**. Steuerungsanwendungen am Client (PC, App, ...) sollten in erster Linie den Port 21105 verwenden. + +Die Kommunikation erfolgt immer asynchron, d.h. zwischen einer Anforderung und der entsprechenden Antwort können z.B. Broadcast-Meldungen auftreten. *(Abbildung 1: Beispiel Sequenz Kommunikation)* + +Es wird erwartet, dass jeder Client einmal pro Minute mit der Z21 kommuniziert, da er sonst aus der Liste der aktiven Teilnehmer entfernt wird. Wenn möglich sollte sich ein Client beim Beenden mit dem Befehl `LAN_LOGOFF` bei der Zentrale abmelden. + +### 1.2 Z21 Datensatz + +#### 1.2.1 Aufbau + +Ein Z21-Datensatz (eine Anforderung oder Antwort) ist folgendermaßen aufgebaut: + +| DataLen (2 Byte) | Header (2 Byte) | Data (n Bytes) | +|---|---|---| + +- **DataLen** (little endian): Gesamtlänge über den ganzen Datensatz inklusive DataLen, Header und Data, d.h. `DataLen = 2 + 2 + n`. +- **Header** (little endian): Beschreibt das Kommando bzw. die Protokollgruppe. +- **Data**: Aufbau und Anzahl hängen vom Kommando ab. + +Falls nicht anders angegeben, ist die Byte-Reihenfolge **Little-Endian** (zuerst low byte, danach high byte). + +#### 1.2.2 X-BUS Protokoll Tunnelung + +Mit dem Z21-LAN-Header `0x40` (`LAN_X_xxx`) werden Anforderungen und Antworten übertragen, welche an das X-BUS-Protokoll angelehnt sind. Gemeint ist dabei nur das Protokoll — diese Befehle haben nichts mit dem physikalischen X-BUS der Z21 zu tun, sondern sind ausschließlich an die LAN-Clients bzw. die Z21 gerichtet. + +Der eigentliche X-BUS-Befehl liegt im Feld **Data**. Das letzte Byte ist eine Prüfsumme und wird als XOR über den X-BUS-Befehl berechnet. Beispiel: + +| DataLen | Header | X-Header | DB0 | DB1 | XOR-Byte | +|---|---|---|---|---|---| +| 0x08 0x00 | 0x40 0x00 | h | x | y | h XOR x XOR y | + +#### 1.2.3 LocoNet Tunnelung + +*Ab Z21 FW Version 1.20.* + +Mit den Z21-LAN-Headern `0xA0` und `0xA1` (`LAN_LOCONET_Z21_RX`, `LAN_LOCONET_Z21_TX`) werden Meldungen, die von der Z21 am LocoNet-Bus empfangen bzw. gesendet werden, an den LAN-Client weitergeleitet. Der LAN-Client muss dazu die LocoNet-Meldungen mittels [2.16 LAN_SET_BROADCASTFLAGS](#216-lan_set_broadcastflags) abonniert haben. + +Über den Z21-LAN-Header `0xA2` (`LAN_LOCONET_FROM_LAN`) kann der LAN-Client Meldungen auf den LocoNet-Bus schreiben. + +Damit kann die Z21 als **Ethernet/LocoNet Gateway** verwendet werden, wobei die Z21 gleichzeitig der LocoNet-Master ist, welcher die Refresh-Slots verwaltet und die DCC-Pakete generiert. Die eigentliche LocoNet-Meldung liegt im Feld **Data**. + +Beispiel: LocoNet-Meldung `OPC_MOVE_SLOTS <0><0>` („DISPATCH_GET") von Z21 empfangen: + +| DataLen | Header | OPC | ARG1 | ARG2 | CKSUM | +|---|---|---|---|---|---| +| 0x08 0x00 | 0xA0 0x00 | 0xBA | 0x00 | 0x00 | 0x45 | + +### 1.3 Kombinieren von Datensätzen in einem UDP-Paket + +In den Nutzdaten eines UDP-Pakets können auch mehrere, voneinander unabhängige Z21-Datensätze gemeinsam an einen Empfänger gesendet werden. Jeder Empfänger muss diese kombinierten UDP-Pakete interpretieren können. + +Beispiel: ein kombiniertes UDP-Paket mit drei Datensätzen (`LAN_X_GET_TURNOUT_INFO #4`, `LAN_X_GET_TURNOUT_INFO #5`, `LAN_RMBUS_GETDATA #0`) ist gleichwertig mit den drei einzeln nacheinander gesendeten UDP-Paketen. + +Das UDP-Paket muss in eine Ethernet MTU passen, d.h. abzüglich IPv4- und UDP-Header stehen maximal `1500 - 20 - 8 = 1472` Bytes Nutzdaten zur Verfügung. + +--- + +## 2 System, Status, Versionen + +### 2.1 LAN_GET_SERIAL_NUMBER + +Auslesen der Seriennummer der Z21. + +**Anforderung an Z21:** `DataLen=0x04 0x00`, `Header=0x10 0x00`, kein Data. + +**Antwort von Z21:** `DataLen=0x08 0x00`, `Header=0x10 0x00`, `Data=` Seriennummer 32 Bit (little endian). + +### 2.2 LAN_LOGOFF + +Abmelden des Clients von der Z21. + +**Anforderung an Z21:** `DataLen=0x04 0x00`, `Header=0x30 0x00`, kein Data. **Antwort:** keine. + +Verwenden Sie beim Abmelden die gleiche Portnummer wie beim Anmelden. *Anmerkung:* das Anmelden erfolgt implizit mit dem ersten Befehl des Clients (z.B. `LAN_SYSTEMSTATE_GETDATA`). + +### 2.3 LAN_X_GET_VERSION + +Auslesen der X-Bus Version der Z21. + +**Anforderung an Z21:** + +| DataLen | Header | X-Header | DB0 | XOR-Byte | +|---|---|---|---|---| +| 0x07 0x00 | 0x40 0x00 | 0x21 | 0x21 | 0x00 | + +**Antwort von Z21:** + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | XOR-Byte | +|---|---|---|---|---|---|---| +| 0x09 0x00 | 0x40 0x00 | 0x63 | 0x21 | XBUS_VER | CMDST_ID | 0x60 | + +- **XBUS_VER**: X-Bus Protokoll Version (0x30 = V3.0, 0x36 = V3.6, 0x40 = V4.0, …) +- **CMDST_ID**: Command station ID (0x12 = Z21 Gerätefamilie) + +### 2.4 LAN_X_GET_STATUS + +Anfordern des Zentralenstatus. + +**Anforderung an Z21:** `Header=0x40 0x00`, X-Header `0x21`, DB0 `0x24`, XOR `0x05`. + +**Antwort:** siehe [2.12 LAN_X_STATUS_CHANGED](#212-lan_x_status_changed). Dieser Zentralenstatus ist identisch mit dem CentralState im SystemStatus, siehe [2.18](#218-lan_systemstate_datachanged). + +### 2.5 LAN_X_SET_TRACK_POWER_OFF + +Abschalten der Gleisspannung. + +**Anforderung an Z21:** X-Header `0x21`, DB0 `0x80`, XOR `0xA1`. **Antwort:** siehe [2.7](#27-lan_x_bc_track_power_off). + +### 2.6 LAN_X_SET_TRACK_POWER_ON + +Einschalten der Gleisspannung bzw. Beenden von Notstop oder Programmiermodus. + +**Anforderung an Z21:** X-Header `0x21`, DB0 `0x81`, XOR `0xA0`. **Antwort:** siehe [2.8](#28-lan_x_bc_track_power_on). + +### 2.7 LAN_X_BC_TRACK_POWER_OFF + +Wird von der Z21 an die registrierten Clients versendet, wenn ein Client `LAN_X_SET_TRACK_POWER_OFF` gesendet hat, ein anderes Eingabegerät (multiMaus) die Gleisspannung abgeschaltet hat, und der Client den Broadcast (Flag 0x00000001) aktiviert hat. + +**Z21 an Client:** X-Header `0x61`, DB0 `0x00`, XOR `0x61`. + +### 2.8 LAN_X_BC_TRACK_POWER_ON + +Analog zu 2.7, beim Einschalten der Gleisspannung. **Z21 an Client:** X-Header `0x61`, DB0 `0x01`, XOR `0x60`. + +### 2.9 LAN_X_BC_PROGRAMMING_MODE + +Wird versendet, wenn die Z21 durch `LAN_X_CV_READ` oder `LAN_X_CV_WRITE` in den CV-Programmiermodus versetzt wurde (Broadcast-Flag 0x00000001). **Z21 an Client:** X-Header `0x61`, DB0 `0x02`, XOR `0x63`. + +### 2.10 LAN_X_BC_TRACK_SHORT_CIRCUIT + +Wird bei einem Kurzschluss versendet (Broadcast-Flag 0x00000001). **Z21 an Client:** X-Header `0x61`, DB0 `0x08`, XOR `0x69`. + +### 2.11 LAN_X_UNKNOWN_COMMAND + +Antwort auf eine ungültige Anforderung. **Z21 an Client:** X-Header `0x61`, DB0 `0x82`, XOR `0xE3`. + +### 2.12 LAN_X_STATUS_CHANGED + +Wird versendet, wenn der Client den Status explizit mit [2.4](#24-lan_x_get_status) angefordert hat. + +**Z21 an Client:** `Header=0x40 0x00`, X-Header `0x62`, DB0 `0x22`, DB1 = Status, dann XOR-Byte. + +Bitmasken für Zentralenstatus: + +```c +#define csEmergencyStop 0x01 // Der Nothalt ist eingeschaltet +#define csTrackVoltageOff 0x02 // Die Gleisspannung ist abgeschaltet +#define csShortCircuit 0x04 // Kurzschluss +#define csProgrammingModeActive 0x20 // Der Programmiermodus ist aktiv +``` + +Identisch mit `SystemState.CentralState`, siehe [2.18](#218-lan_systemstate_datachanged). + +### 2.13 LAN_X_SET_STOP + +Aktiviert den Notstop: die Loks werden angehalten, aber die Gleisspannung bleibt eingeschaltet. + +**Anforderung an Z21:** `DataLen=0x06 0x00`, `Header=0x40 0x00`, X-Header `0x80`, XOR `0x80`. **Antwort:** siehe [2.14](#214-lan_x_bc_stopped). + +### 2.14 LAN_X_BC_STOPPED + +Wird versendet, wenn der Notstop ausgelöst wurde (Broadcast-Flag 0x00000001). **Z21 an Client:** X-Header `0x81`, DB0 `0x00`, XOR `0x81`. + +### 2.15 LAN_X_GET_FIRMWARE_VERSION + +Auslesen der Firmware-Version der Z21. + +**Anforderung an Z21:** X-Header `0xF1`, DB0 `0x0A`, XOR `0xFB`. + +**Antwort von Z21:** X-Header `0xF3`, DB0 `0x0A`, DB1 = V_MSB, DB2 = V_LSB, dann XOR. +- DB1: Höherwertiges Byte der Firmware Version +- DB2: Niederwertiges Byte der Firmware Version +- Version im BCD-Format. Beispiel: `... 0xf3 0x0a 0x01 0x23 0xdb` → „Firmware Version 1.23". + +### 2.16 LAN_SET_BROADCASTFLAGS + +Setzen der Broadcast-Flags in der Z21. Diese Flags werden pro Client (IP + Portnummer) eingestellt und müssen beim nächsten Anmelden neu gesetzt werden. + +**Anforderung an Z21:** `Header=0x50 0x00`, `Data=` Broadcast-Flags 32 Bit (little endian). Broadcast-Flags sind eine OR-Verknüpfung folgender Werte: + +| Flag | Bedeutung | +|---|---| +| `0x00000001` | Automatisch generierte Broadcasts/Meldungen zu Fahren und Schalten. Abonniert: 2.7 PowerOff, 2.8 PowerOn, 2.9 ProgrammingMode, 2.10 ShortCircuit, 2.14 Stopped, 4.4 LOCO_INFO (Lok-Adresse muss abonniert sein), 5.3 TURNOUT_INFO | +| `0x00000002` | Änderungen der Rückmelder am R-Bus → 7.1 `LAN_RMBUS_DATACHANGED` | +| `0x00000004` | Änderungen bei RailCom-Daten der abonnierten Loks → 8.1 `LAN_RAILCOM_DATACHANGED` | +| `0x00000100` | Änderungen des Z21-Systemzustands → 2.18 `LAN_SYSTEMSTATE_DATACHANGED` | +| `0x00010000` | *(ab FW 1.20)* Ergänzt Flag 0x00000001; Client bekommt `LAN_X_LOCO_INFO` ohne vorheriges Abonnieren der Lok-Adressen (alle Loks!). Nur für vollwertige PC-Steuerungen, nicht für mobile Handregler. Ab FW V1.20–V1.23: für **alle** Loks; ab FW V1.24: für **alle geänderten** Loks | +| `0x01000000` | Meldungen vom LocoNet-Bus an LAN Client weiterleiten (ohne Loks und Weichen) | +| `0x02000000` | Lok-spezifische LocoNet-Meldungen: OPC_LOCO_SPD, OPC_LOCO_DIRF, OPC_LOCO_SND, OPC_LOCO_F912, OPC_EXP_CMD | +| `0x04000000` | Weichen-spezifische LocoNet-Meldungen: OPC_SW_REQ, OPC_SW_REP, OPC_SW_ACK, OPC_SW_STATE | +| `0x08000000` | *(ab FW 1.22)* Status-Meldungen von Gleisbesetztmeldern am LocoNet-Bus → 9.5 `LAN_LOCONET_DETECTOR` | +| `0x00040000` | *(ab FW 1.29)* RailCom-Daten automatisch, ohne vorheriges Abonnieren (alle Loks). Nur für vollwertige PC-Steuerungen → 8.1 `LAN_RAILCOM_DATACHANGED` | +| `0x00080000` | *(ab FW 1.30)* Status-Meldungen von Gleisbesetztmeldern am CAN-Bus → 10.1 `LAN_CAN_DETECTOR` | +| `0x00020000` | *(ab FW 1.41)* CAN-Bus Booster Status-Meldungen → 10.2.3 `LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD` | +| `0x00000010` | *(ab FW 1.43)* Fastclock Modellzeit Meldungen → 12.2 `LAN_FAST_CLOCK_DATA` | + +**Antwort:** keine. + +Berücksichtigen Sie die Auswirkungen auf die Netzwerkauslastung — besonders bei den Flags `0x00010000`, `0x00040000`, `0x02000000` und `0x04000000`. IP-Pakete dürfen vom Router bei Überlast gelöscht werden, und UDP bietet keine Erkennungsmechanismen. Bei Flag 0x00000100 (Systemzustand) ist abzuwägen, ob nicht 0x00000001 mit den entsprechenden `LAN_X_BC_xxx`-Broadcasts die sinnvollere Alternative ist. + +### 2.17 LAN_GET_BROADCASTFLAGS + +Auslesen der Broadcast-Flags. **Anforderung:** `Header=0x51 0x00`, kein Data. **Antwort:** `Header=0x51 0x00`, Broadcast-Flags 32 Bit (little endian). + +### 2.18 LAN_SYSTEMSTATE_DATACHANGED + +Änderung des Systemzustandes melden. Wird asynchron gemeldet, wenn der Client den Broadcast (Flag 0x00000100) aktiviert hat oder den Systemzustand explizit mit [2.19](#219-lan_systemstate_getdata) angefordert hat. + +**Z21 an Client:** `DataLen=0x14 0x00`, `Header=0x84 0x00`, `Data=` SystemState (16 Bytes). + +SystemState (16-bit Werte little endian): + +| Offset | Typ | Name | Einheit | Bedeutung | +|---|---|---|---|---| +| 0 | INT16 | MainCurrent | mA | Strom am Hauptgleis | +| 2 | INT16 | ProgCurrent | mA | Strom am Programmiergleis | +| 4 | INT16 | FilteredMainCurrent | mA | geglätteter Strom am Hauptgleis | +| 6 | INT16 | Temperature | °C | interne Temperatur in der Zentrale | +| 8 | UINT16 | SupplyVoltage | mV | Versorgungsspannung | +| 10 | UINT16 | VCCVoltage | mV | interne Spannung, identisch mit Gleisspannung | +| 12 | UINT8 | CentralState | bitmask | siehe unten | +| 13 | UINT8 | CentralStateEx | bitmask | siehe unten | +| 14 | UINT8 | reserved | | | +| 15 | UINT8 | Capabilities | bitmask | siehe unten, ab Z21 V1.42 | + +```c +// CentralState +#define csEmergencyStop 0x01 // Der Nothalt ist eingeschaltet +#define csTrackVoltageOff 0x02 // Die Gleisspannung ist abgeschaltet +#define csShortCircuit 0x04 // Kurzschluss +#define csProgrammingModeActive 0x20 // Der Programmiermodus ist aktiv + +// CentralStateEx +#define cseHighTemperature 0x01 // zu hohe Temperatur +#define csePowerLost 0x02 // zu geringe Eingangsspannung +#define cseShortCircuitExternal 0x04 // am externen Booster-Ausgang +#define cseShortCircuitInternal 0x08 // am Hauptgleis oder Programmiergleis +#define cseRCN213 0x20 // Weichenadressierung gem. RCN213 (ab FW 1.42) + +// Capabilities (ab FW 1.42) +#define capDCC 0x01 // beherrscht DCC +#define capMM 0x02 // beherrscht MM +//#define capReserved 0x04 // reserviert +#define capRailCom 0x08 // RailCom ist aktiviert +#define capLocoCmds 0x10 // akzeptiert LAN-Befehle für Lokdecoder +#define capAccessoryCmds 0x20 // akzeptiert LAN-Befehle für Zubehördecoder +#define capDetectorCmds 0x40 // akzeptiert LAN-Befehle für Belegtmelder +#define capNeedsUnlockCode 0x80 // benötigt Freischaltcode (z21start) +``` + +`SystemState.Capabilities` verschafft dem Client einen Überblick über den Feature-Umfang. Ist `Capabilities == 0`, handelt es sich vermutlich um eine ältere Firmware — bei älteren Versionen sollte Capabilities nicht ausgewertet werden. + +### 2.19 LAN_SYSTEMSTATE_GETDATA + +Anfordern des aktuellen Systemzustandes. **Anforderung:** `Header=0x85 0x00`, kein Data. **Antwort:** siehe [2.18](#218-lan_systemstate_datachanged). + +### 2.20 LAN_GET_HWINFO + +*Ab Z21 FW Version 1.20 und SmartRail FW Version V1.13.* Auslesen von Hardware-Typ und Firmware-Version. + +**Anforderung:** `Header=0x1A 0x00`, kein Data. + +**Antwort:** `DataLen=0x0C 0x00`, `Header=0x1A 0x00`, `Data=` HwType 32 Bit + FW Version 32 Bit (beide little endian). + +```c +#define D_HWT_Z21_OLD 0x00000200 // "schwarze Z21" (ab 2012) +#define D_HWT_Z21_NEW 0x00000201 // "schwarze Z21" (ab 2013) +#define D_HWT_SMARTRAIL 0x00000202 // SmartRail (ab 2012) +#define D_HWT_z21_SMALL 0x00000203 // "weiße z21" Starterset (ab 2013) +#define D_HWT_z21_START 0x00000204 // "z21 start" Starterset (ab 2016) +#define D_HWT_SINGLE_BOOSTER 0x00000205 // 10806 "Z21 Single Booster" (zLink) +#define D_HWT_DUAL_BOOSTER 0x00000206 // 10807 "Z21 Dual Booster" (zLink) +#define D_HWT_Z21_XL 0x00000211 // 10870 "Z21 XL Series" (ab 2020) +#define D_HWT_XL_BOOSTER 0x00000212 // 10869 "Z21 XL Booster" (ab 2021, zLink) +#define D_HWT_Z21_SWITCH_DECODER 0x00000301 // 10836 "Z21 SwitchDecoder" (zLink) +#define D_HWT_Z21_SIGNAL_DECODER 0x00000302 // 10836 "Z21 SignalDecoder" (zLink) +``` + +FW Version im BCD-Format. Beispiel: `... 0x00 0x02 0x00 0x00 0x20 0x01 0x00 0x00` → „Hardware Typ 0x200, Firmware Version 1.20". Für ältere Firmware ggf. [2.15](#215-lan_x_get_firmware_version) verwenden (V1.10/V1.11 = Z21 ab 2012, V1.12 = SmartRail ab 2012). + +### 2.21 LAN_GET_CODE + +Prüfen und Auslesen des SW Feature-Umfangs. Besonders bei „z21 start" interessant, um zu prüfen, ob Fahren und Schalten per LAN gesperrt oder erlaubt ist. + +**Anforderung:** `Header=0x18 0x00`, kein Data. **Antwort:** `Header=0x18 0x00`, Code (8 Bit). + +```c +#define Z21_NO_LOCK 0x00 // keine Features gesperrt +#define z21_START_LOCKED 0x01 // "z21 start": Fahren und Schalten per LAN gesperrt +#define z21_START_UNLOCKED 0x02 // "z21 start": alle Feature-Sperren aufgehoben +``` + +--- + +## 3 Einstellungen + +Die hier beschriebenen Einstellungen werden in der Z21 persistent gespeichert. Sie können vom Anwender auf Werkseinstellung zurückgesetzt werden, indem die STOP-Taste gedrückt gehalten wird, bis die LEDs violett blinken. + +### 3.1 LAN_GET_LOCOMODE + +Lesen des Ausgabeformats (DCC, MM) für eine Lok-Adresse. Es können max. 256 verschiedene Lok-Adressen abgelegt werden; jede Adresse ≥ 256 ist automatisch DCC. + +**Anforderung:** `Header=0x60 0x00`, `Data=` Lok-Adresse 16 bit (**big endian**). + +**Antwort:** `Header=0x60 0x00`, Lok-Adresse 16 Bit (big endian) + Modus 8 bit. +- Lok-Adresse: 2 Byte, big endian (zuerst high byte). +- Modus: `0` = DCC, `1` = MM. + +### 3.2 LAN_SET_LOCOMODE + +Setzen des Ausgabeformats (persistent). **Anforderung:** `Header=0x61 0x00`, Lok-Adresse 16 Bit (big endian) + Modus 8 bit. **Antwort:** keine. + +*Anmerkungen:* Jede Lok-Adresse ≥ 256 bleibt automatisch DCC. Die Fahrstufen (14, 28, 128) werden ebenfalls persistent gespeichert (automatisch beim Fahrbefehl, siehe [4.2](#42-lan_x_set_loco_drive)). + +### 3.3 LAN_GET_TURNOUTMODE + +Lesen der Einstellungen für eine Funktionsdecoder-Adresse („Accessory Decoder" RP-9.2.1). Max. 256 Adressen; jede ≥ 256 ist automatisch DCC. + +**Anforderung:** `Header=0x70 0x00`, Funktionsdecoder-Adresse 16 bit (big endian). +**Antwort:** `Header=0x70 0x00`, Funktionsdecoder-Adresse 16 Bit (big endian) + Modus 8 bit (`0`=DCC, `1`=MM). + +An der LAN-Schnittstelle und in der Z21 werden Funktionsdecoder-Adressen ab 0 adressiert, in der Visualisierung der Apps/multiMaus jedoch ab 1. Beispiel: multiMaus Weichenadresse #3 entspricht in der Z21 der Adresse 2. + +### 3.4 LAN_SET_TURNOUTMODE + +Setzen des Ausgabeformats für eine Funktionsdecoder-Adresse (persistent). **Anforderung:** `Header=0x71 0x00`, Funktionsdecoder-Adresse 16 Bit (big endian) + Modus 8 bit. **Antwort:** keine. + +MM-Funktionsdecoder werden ab Z21 FW 1.20 unterstützt; SmartRail unterstützt sie nicht. Jede Adresse ≥ 256 bleibt automatisch DCC. + +--- + +## 4 Fahren + +Ein Client kann Lok-Infos mit [4.1 LAN_X_GET_LOCO_INFO](#41-lan_x_get_loco_info) abonnieren, um über Änderungen (durch andere Clients/Handregler) informiert zu werden. Zusätzlich muss der Broadcast (Flag 0x00000001) aktiviert sein. *(Abbildung 2: Beispiel Sequenz Lok-Steuerung.)* + +Maximal **16 Lok-Adressen pro Client** können abonniert werden (FIFO). Weiteres Pollen ist möglich, sollte aber mit Rücksicht auf die Netzwerkauslastung erfolgen. + +### 4.1 LAN_X_GET_LOCO_INFO + +Anfordern des Status einer Lok (und Abonnieren, nur mit Flag 0x00000001). + +**Anforderung an Z21:** + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | XOR-Byte | +|---|---|---|---|---|---|---| +| 0x09 0x00 | 0x40 0x00 | 0xE3 | 0xF0 | Adr_MSB | Adr_LSB | XOR | + +`Lok-Adresse = (Adr_MSB & 0x3F) << 8 + Adr_LSB`. Bei Lok-Adressen ≥ 128 müssen die beiden höchsten Bits in DB1 auf 1 gesetzt sein: `DB1 = (0xC0 | Adr_MSB)`. + +**Antwort:** siehe [4.4 LAN_X_LOCO_INFO](#44-lan_x_loco_info). + +### 4.2 LAN_X_SET_LOCO_DRIVE + +Verändern der Fahrstufe eines Lok-Decoders. + +**Anforderung an Z21:** + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0xE4 | 0x1S | Adr_MSB | Adr_LSB | RVVVVVVV | XOR | + +`Lok-Adresse = (Adr_MSB & 0x3F) << 8 + Adr_LSB` (≥ 128: `DB1 = 0xC0 | Adr_MSB`). + +`0x1S` = Anzahl der Fahrstufen je nach Schienenformat: +- `S=0`: DCC 14 Fahrstufen bzw. MMI mit 14 Fahrstufen und F0 +- `S=2`: DCC 28 Fahrstufen bzw. MMII mit 14 realen Fahrstufen und F0-F4 +- `S=3`: DCC 128 Fahrstufen (alias „126" ohne Stops) bzw. MMII mit 28 realen Fahrstufen und F0-F4 + +`RVVVVVVV`: R = Richtung (1 = vorwärts), V = Geschwindigkeit (Codierung abhängig von S). Bei MM erfolgt die Umrechnung von DCC- in MM-Fahrstufe automatisch in der Z21. + +**Fahrstufen-Codierung „DCC 14"** (`R000 VVVV`): + +| Code | Speed | Code | Speed | Code | Speed | +|---|---|---|---|---|---| +| R000 0000 | Stop | R000 0110 | Step 5 | R000 1100 | Step 11 | +| R000 0001 | E-Stop | R000 0111 | Step 6 | R000 1101 | Step 12 | +| R000 0010 | Step 1 | R000 1000 | Step 7 | R000 1110 | Step 13 | +| R000 0011 | Step 2 | R000 1001 | Step 8 | R000 1111 | Step 14 (max) | +| R000 0100 | Step 3 | R000 1010 | Step 9 | | | +| R000 0101 | Step 4 | R000 1011 | Step 10 | | | + +**Fahrstufen-Codierung „DCC 28"** (`R00V5 VVVV`, Zwischenschritt im Bit V5): + +| Code | Speed | Code | Speed | +|---|---|---|---| +| R000 0000 | Stop | R000 1000 | Step 13 | +| R001 0000 | Stop¹ | R001 1000 | Step 14 | +| R000 0001 | E-Stop | R000 1001 | Step 15 | +| R001 0001 | E-Stop¹ | … | … | +| R000 0010 | Step 1 | R001 1111 | Step 28 (max) | +| R001 0010 | Step 2 | | | + +¹ Verwendung nicht empfohlen. + +**Fahrstufen-Codierung „DCC 128"** (`RVVV VVVV`): `R000 0000`=Stop, `R000 0001`=E-Stop, `R000 0010`=Step 1, … `R111 1111`=Step 126 (max). + +**Antwort:** keine Standardantwort, [4.4 LAN_X_LOCO_INFO](#44-lan_x_loco_info) an Clients mit Abo. Eine Änderung der Fahrstufenzahl (14/28/128) wird automatisch persistent gespeichert. + +### 4.3 Funktionen für Fahrzeugdecoder + +Funktionsbefehle F0–F12 werden am Gleis (wie Fahrstufe/Richtung) regelmäßig prioritätsgesteuert wiederholt. Befehle ab F13 werden nach einer Änderung dreimal ausgegeben und danach (gem. RCN-212, aus Rücksicht auf die Bandbreite) nicht mehr regelmäßig wiederholt. + +#### 4.3.1 LAN_X_SET_LOCO_FUNCTION + +Schalten einer Einzelfunktion. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0xE4 | 0xF8 | Adr_MSB | Adr_LSB | TTNN NNNN | XOR | + +`Lok-Adresse = (Adr_MSB & 0x3F) << 8 + Adr_LSB` (≥ 128: `DB1 = 0xC0 | Adr_MSB`). +- `TT` Umschalttyp: `00`=aus, `01`=ein, `10`=umschalten, `11`=nicht erlaubt. +- `NNNNNN` Funktionsindex: `0x00`=F0 (Licht), `0x01`=F1 usw. + +Bei MMI nur F0, bei MMII F0–F4. Bei DCC F0–F28, ab FW 1.42 erweitert F0–F31. **Antwort:** keine Standardantwort, 4.4 LOCO_INFO an Clients mit Abo. + +#### 4.3.2 LAN_X_SET_LOCO_FUNCTION_GROUP + +Schaltet eine ganze Funktionsgruppe (bis zu 8 Funktionen) mit einem Befehl. Ab FW 1.42 bis F31, mit Einschränkungen bis F68. Der Client sollte den aktuellen Zustand aller Funktionen mitverfolgen (Befehl eher für PC-Steuerungen geeignet). + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0xE4 | Group | Adr_MSB | Adr_LSB | Functions | XOR | + +Group und Functions: + +| Nr | Group | Bit7 | Bit6 | Bit5 | Bit4 | Bit3 | Bit2 | Bit1 | Bit0 | Anm. | +|---|---|---|---|---|---|---|---|---|---|---| +| 1 | 0x20 | 0 | 0 | 0 | F0 | F4 | F3 | F2 | F1 | (A) | +| 2 | 0x21 | 0 | 0 | 0 | 0 | F8 | F7 | F6 | F5 | | +| 3 | 0x22 | 0 | 0 | 0 | 0 | F12 | F11 | F10 | F9 | | +| 4 | 0x23 | F20 | F19 | F18 | F17 | F16 | F15 | F14 | F13 | (B) | +| 5 | 0x28 | F28 | F27 | F26 | F25 | F24 | F23 | F22 | F21 | (B) | +| 6 | 0x29 | F36 | F35 | F34 | F33 | F32 | F31 | F30 | F29 | (C)(D)(E) | +| 7 | 0x2A | F44 | F43 | F42 | F41 | F40 | F39 | F38 | F37 | (D)(E) | +| 8 | 0x2B | F52 | F51 | F50 | F49 | F48 | F47 | F46 | F45 | (D)(E) | +| 9 | 0x50 | F60 | F59 | F58 | F57 | F56 | F55 | F54 | F53 | (D)(E) | +| 10 | 0x51 | F68 | F67 | F66 | F65 | F64 | F63 | F62 | F61 | (D)(E) | + +- (A) MMI nur F0, MMII bis max. F4. +- (B) DCC F13–F28 mit diesem Befehl erst ab FW V1.24. +- (C) DCC F29–F31 ab FW V1.42, inkl. Rückmeldung an die LAN-Clients. +- (D) DCC F32–F68 ab FW V1.42, **ohne** Rückmeldung; Befehle nur am Gleis ausgegeben. +- (E) Es kann nicht gewährleistet werden, dass DCC-Funktionsbefehle ≥ F29 von allen Decodern verstanden werden (2022: nur sehr wenige Typen, getestet F29–F31 mit „Loksound 5"). + +**Antwort:** keine Standardantwort; für F0–F31 erfolgt Rückmeldung 4.4 LOCO_INFO an Clients mit Abo. + +#### 4.3.3 LAN_X_SET_LOCO_BINARY_STATE + +*Ab Z21 FW Version 1.42.* Sendet ein DCC „Binary State" Kommando an einen Lok-Decoder. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | DB4 | XOR | +|---|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0xE5 | 0x5F | AH | AL | FLLL LLLL | HHHH HHHH | XOR | + +`Lok-Adresse = (AH & 0x3F) << 8 + AL` (≥ 128: `DB1 = 0xC0 | AH`). +- `F`: oberstes Bit legt fest, ob der Binärzustand ein- oder ausgeschaltet ist. +- `LLLLLLL`: niederwertige 7 Bits der Binärzustandsadresse. +- `HHHHHHHH`: höherwertige 8 Bits der Binärzustandsadresse. +- `15-Bit Binärzustandsadresse = (HHHHHHHH << 7) + (LLLLLLL & 0x7F)`. + +Erlaubt: Binärzustandsadressen **29 bis 32767**. Adressen 1–28 sind reserviert, Adresse 0 ist Broadcast. Adressen < 128 (HHHHHHHH == 0) werden gem. RCN-212 als „kurze Form" ausgegeben, ≥ 128 als „lange Form". Befehle werden dreimal am Gleis ausgegeben und danach nicht mehr wiederholt. **Antwort:** keine (auch keine Benachrichtigung an andere Clients). + +### 4.4 LAN_X_LOCO_INFO + +Wird als Antwort auf [4.1](#41-lan_x_get_loco_info) gesendet, aber auch ungefragt, wenn der Lok-Status verändert wurde, der Broadcast (Flag 0x00000001) aktiviert ist und die Lok-Adresse abonniert wurde. + +**Z21 an Client:** `DataLen = 7 + n`, `Header=0x40 0x00`, X-Header `0xEF`, Lok-Information, XOR-Byte. Paketlänge variiert mit `7 ≤ n ≤ 14`. Ab FW 1.42 ist `DataLen ≥ 15 (n ≥ 8)` zur Übertragung von F29–F31. + +| Position | Daten | Bedeutung | +|---|---|---| +| DB0 | Adr_MSB | beide höchsten Bits ignorieren | +| DB1 | Adr_LSB | `Lok-Adresse = (Adr_MSB & 0x3F) << 8 + Adr_LSB` | +| DB2 | `000MBKKK` | M=1: MM-Lok (ab FW 1.43); B=1: Lok von anderem X-BUS Handregler gesteuert („besetzt"); KKK = Fahrstufeninfo (0=14, 2=28, 4=128) | +| DB3 | `RVVVVVVV` | R = Richtung (1=vorwärts), V = Geschwindigkeit (Codierung abh. von KKK) | +| DB4 | `0DSLFGHJ` | D = Doppeltraktion; S = Smartsearch; L = F0 (Licht); F = F4; G = F3; H = F2; J = F1 | +| DB5 | F5–F12 | F5 ist Bit0 (LSB) | +| DB6 | F13–F20 | F13 ist Bit0 (LSB) | +| DB7 | F21–F28 | F21 ist Bit0 (LSB) | +| DB8 | F29–F31 | ab FW 1.42 (falls DataLen ≥ 15); F29 ist Bit0 (LSB) | +| DBn | optional | für zukünftige Erweiterungen | + +### 4.5 LAN_X_SET_LOCO_E_STOP + +*Ab Z21 FW Version 1.43.* Hält eine Lok an. Bei DCC wird die Fahrstufe „E-STOP" (RCN-212) ausgegeben; bei MM die Fahrstufe 0 („Stop"). + +| DataLen | Header | X-Header | DB0 | DB2 | XOR | +|---|---|---|---|---|---| +| 0x08 0x00 | 0x40 0x00 | 0x92 | Adr_MSB | Adr_LSB | XOR | + +`Lok-Adresse = (Adr_MSB & 0x3F) << 8 + Adr_LSB` (≥ 128: `DB1 = 0xC0 | Adr_MSB`). **Antwort:** keine Standardantwort, 4.4 LOCO_INFO an Clients mit Abo. + +### 4.6 LAN_X_PURGE_LOCO + +*Ab Z21 FW Version 1.43.* Nimmt eine Lok aus der Z21 heraus; die Fahrbefehle für diese Lok am Gleis werden beendet (bis ein neuer Fahr-/Funktionsbefehl an dieselbe Adresse kommt). Damit kann z.B. eine PC-Steuerung die Anzahl der Loks und den Datendurchsatz am Gleis beeinflussen. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | XOR | +|---|---|---|---|---|---|---| +| 0x09 0x00 | 0x40 0x00 | 0xE3 | 0x44 | Adr_MSB | Adr_LSB | XOR | + +`Lok-Adresse = (Adr_MSB & 0x3F) << 8 + Adr_LSB` (≥ 128: `DB1 = 0xC0 | Adr_MSB`). Keine Antwort an Aufrufer/andere Clients. + +--- + +## 5 Schalten + +Meldungen zum Schalten von Funktionsdecodern („Accessory Decoder" RP-9.2.1, z.B. Weichendecoder). + +Die Visualisierung der Weichennummer ist bei vielen DCC-Systemen unterschiedlich gelöst. Gemäß DCC gibt es pro Accessorydecoder-Adresse vier Ports mit je zwei Ausgängen. Übliche Visualisierungen: +1. Nummerierung ab 1, DCC-Adresse ab 1, je 4 Ports (ESU, Uhlenbrock): Weiche #1 = Addr 1/Port 0; #5 = Addr 2/Port 0; #6 = Addr 2/Port 1. +2. Nummerierung ab 1, DCC-Adresse ab 0, je 4 Ports (Roco, Lenz): Weiche #1 = Addr 0/Port 0; #5 = Addr 1/Port 0; #6 = Addr 1/Port 1. +3. Virtuelle Weichennummer mit frei konfigurierbarer DCC-Adresse/Port (Twin-Center). +4. Darstellung DCC-Adresse / Port (Zimo). + +Umsetzung der Input-Parameter (FAdr_MSB, FAdr_LSB, A, P) in den DCC Accessory Befehl. DCC Basic Accessory Decoder Packet Format: `{preamble} 0 10AAAAAA 0 1aaaCDDd 0 EEEEEEEE 1` + +```c +UINT16 FAdr = (FAdr_MSB << 8) + FAdr_LSB; +UINT16 Dcc_Addr = FAdr >> 2; +aaaAAAAAA = (~Dcc_Addr & 0x1C0) | (Dcc_Addr & 0x003F); // DCC Adresse +C = A; // Ausgang aktivieren oder deaktivieren +DD = FAdr & 0x03; // Port +d = P; // Weiche nach links oder rechts +``` + +Beispiel: FAdr=0 → DCC-Addr 0/Port 0; FAdr=3 → DCC-Addr 0/Port 3; FAdr=4 → DCC-Addr 1/Port 0. Bei MM gilt: FAdr=0 → MM-Addr 1; FAdr=1 → MM-Addr 2; … + +Ein Client kann Funktions-Infos abonnieren (Broadcast-Flag 0x00000001). Die tatsächliche Stellung der Weiche hängt von Verkabelung/Konfiguration ab; daher wird auf „gerade"/„abzweigend" bewusst verzichtet. + +### 5.1 LAN_X_GET_TURNOUT_INFO + +Anfordern des Status einer Weiche/Schaltfunktion. + +| DataLen | Header | X-Header | DB0 | DB1 | XOR | +|---|---|---|---|---|---| +| 0x08 0x00 | 0x40 0x00 | 0x43 | FAdr_MSB | FAdr_LSB | XOR | + +`Funktions-Adresse = (FAdr_MSB << 8) + FAdr_LSB`. **Antwort:** siehe [5.3](#53-lan_x_turnout_info). + +### 5.2 LAN_X_SET_TURNOUT + +Schalten einer Weiche. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | XOR | +|---|---|---|---|---|---|---| +| 0x09 0x00 | 0x40 0x00 | 0x53 | FAdr_MSB | FAdr_LSB | 10Q0A00P | XOR | + +`Funktions-Adresse = (FAdr_MSB << 8) + FAdr_LSB`. `1000A00P`: +- `A=0` Weichenausgang deaktivieren / `A=1` aktivieren +- `P=0` Ausgang 1 wählen / `P=1` Ausgang 2 wählen +- `Q=0` Kommando sofort ausführen +- `Q=1` (ab FW V1.24) Weichenbefehl in Z21-Queue einfügen und zum nächstmöglichen Zeitpunkt am Gleis ausgeben + +**Antwort:** keine Standardantwort, [5.3](#53-lan_x_turnout_info) an Clients mit Abo. Das Q-Flag wurde ab FW V1.24 eingeführt. + +#### 5.2.1 LAN_X_SET_TURNOUT mit Q=0 + +Bei `Q=0` verhält sich die Z21 kompatibel zu früheren Versionen: der Befehl wird sofort ausgegeben. Das Activate (A=1) wird ausgegeben, bis das entsprechende Deactivate gesendet wird. Es darf zu einem Zeitpunkt nur ein Weichenstellbefehl aktiv sein. Die korrekte Reihenfolge (Activate → Deactivate) und das Timing der Schaltdauer liegen in der Verantwortung des LAN-Clients. + +- **Falsch:** mehrere Weichen gleichzeitig aktivieren, dann gemeinsam deaktivieren. +- **Richtig:** je Weiche: aktivieren → ~100 ms warten → deaktivieren → ~50 ms warten, dann nächste. + +*(Abbildung 3: DCC Sniff am Gleis bei Q=0.)* + +#### 5.2.2 LAN_X_SET_TURNOUT mit Q=1 + +Bei `Q=1` wird der Befehl in einer internen FIFO-Queue eingereiht und beim Generieren des Gleissignals viermal am Gleis ausgegeben. Das befreit den Client von der Serialisierung — Schaltbefehle dürfen gemischt gesendet werden (Fahrstraßen!). Der Client kümmert sich nur noch um das Timing des Deactivate; bei manchen DCC-Decodern kann es entfallen, bei MM jedoch nicht (z.B. k83 ohne Endabschaltung). + +**Vermischen Sie keinesfalls Schaltbefehle mit Q=0 und Q=1.** *(Abbildung 4: DCC Sniff am Gleis bei Q=1.)* + +### 5.3 LAN_X_TURNOUT_INFO + +Antwort auf [5.1](#51-lan_x_get_turnout_info), aber auch ungefragt bei Statusänderung (Broadcast-Flag 0x00000001). + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | XOR | +|---|---|---|---|---|---|---| +| 0x09 0x00 | 0x40 0x00 | 0x43 | FAdr_MSB | FAdr_LSB | 000000ZZ | XOR | + +`Funktions-Adresse = (FAdr_MSB << 8) + FAdr_LSB`. `000000ZZ`: +- `ZZ=00` Weiche noch nicht geschaltet +- `ZZ=01` Weiche steht gemäß „P=0" +- `ZZ=10` Weiche steht gemäß „P=1" +- `ZZ=11` ungültige Kombination + +*(Abbildung 5: Beispiel Sequenz Weiche schalten.)* + +### 5.4 LAN_X_SET_EXT_ACCESSORY + +*Ab Z21 FW V1.40.* Sendet einen DCC-Befehl im „erweiterten Zubehördecoder Paketformat" (DCCext) an einen Erweiterten Zubehördecoder (siehe RCN-213 Abschnitt 2.3). + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0x54 | Adr_MSB | Adr_LSB | DDDDDDDD | 0x00 | XOR | + +`RawAddress = (Adr_MSB << 8) + Adr_LSB`. +- **RawAddress**: Die RawAddress für den ersten erweiterten Zubehördecoder ist gem. RCN-213 die Adresse 4 (in Anwenderdialogen als „Adresse 1" dargestellt). Adressierung strikt nach RCN-213, ohne abweichende Verschiebung. +- **DDDDDDDD**: über Bits 0–7 werden die 256 möglichen Zustände übertragen, im Erweiterten Zubehördecoder-Paketformat gem. RCN-213. + +Hinweis: Der **10836 Z21 switch DECODER** interpretiert DDDDDDDD als `RZZZZZZZ`: +- `ZZZZZZZ` = Einschaltzeit (Auflösung 100 ms). 0 = Ausgang aus; 127 = dauerhaft eingeschaltet (bis zum nächsten Befehl). +- Bit 7 `R` wählt den Ausgang: R=1 „grün" (gerade), R=0 „rot" (abzweigend). + +Der **10837 Z21 signal DECODER** interpretiert DDDDDDDD als einen von 256 Signalbegriffen (Wertebereich abhängig vom Signaltyp). Beispiele: `0`=absoluter Haltebegriff, `4`=Fahrt 40 km/h, `16`=freie Fahrt, `65 (0x41)`=Rangieren erlaubt, `66 (0x42)`=Dunkelschaltung, `69 (0x45)`=Ersatzsignal. Konkrete Werte siehe `https://www.z21.eu/de/produkte/z21-signal-decoder/signaltypen` unter „DCCext". + +**Antwort:** keine Standardantwort, oder [5.6](#56-lan_x_ext_accessory_info) an Clients mit Abo. + +Beispiel: `0x0A 0x00 0x40 0x00 0x54 0x00 0x04 0x05 0x00 0x55` → an Decoder RawAddress=4 (Anwender-Adresse 1) Wert DDDDDDDD=5. Beim 10836 switch DECODER: Ausgang 1 „rot" (Klemme 1A) ein, nach 5×100 ms automatisch aus. + +„Notaus-Befehl für Erweiterte Zubehördecoder" (RCN-213, 2.4) = Wert 0 für RawAddress=2047: `0x0A 0x00 0x40 0x00 0x54 0x07 0xFF 0x00 0x00 0xAC`. + +### 5.5 LAN_X_GET_EXT_ACCESSORY_INFO + +*Ab Z21 FW V1.40.* Abfragen des letzten an einen Erweiterten Zubehördecoder übertragenen Befehls. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | XOR | +|---|---|---|---|---|---|---| +| 0x09 0x00 | 0x40 0x00 | 0x44 | Adr_MSB | Adr_LSB | 0x00 | XOR | + +`RawAddress = (Adr_MSB << 8) + Adr_LSB`. DB2 reserviert (mit 0 initialisieren). **Antwort:** siehe [5.6](#56-lan_x_ext_accessory_info). + +### 5.6 LAN_X_EXT_ACCESSORY_INFO + +Antwort auf [5.5](#55-lan_x_get_ext_accessory_info), aber auch ungefragt, wenn jemand anderes ein Kommando an einen Erweiterten Zubehördecoder sendet (Broadcast-Flag 0x00000001). + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0x44 | Adr_MSB | Adr_LSB | DDDDDDDD | Status | XOR | + +`RawAddress = (Adr_MSB << 8) + Adr_LSB`. DDDDDDDD = Zustand (Erweitertes Zubehördecoder-Paketformat). Status: `0x00` = Data Valid, `0xFF` = Data Unknown. + +--- + +## 6 Decoder CV Lesen und Schreiben + +Meldungen zum Lesen/Schreiben von Decoder-CVs (Configuration Variable, RP-9.2.2, RP-9.2.3). Ob bit- oder byteweiser Zugriff erfolgt, hängt von den Z21-Einstellungen ab. + +### 6.1 LAN_X_CV_READ + +CV im Direct-Mode auslesen. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | XOR | +|---|---|---|---|---|---|---| +| 0x09 0x00 | 0x40 0x00 | 0x23 | 0x11 | CVAdr_MSB | CVAdr_LSB | XOR | + +`CV-Adresse = (CVAdr_MSB << 8) + CVAdr_LSB`, mit 0=CV1, 1=CV2, 255=CV256, usw. **Antwort:** 2.9 ProgrammingMode an Clients mit Abo, sowie Ergebnis [6.3](#63-lan_x_cv_nack_sc)/[6.4](#64-lan_x_cv_nack)/[6.5](#65-lan_x_cv_result). + +### 6.2 LAN_X_CV_WRITE + +CV im Direct-Mode überschreiben. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0x24 | 0x12 | CVAdr_MSB | CVAdr_LSB | Value | XOR | + +`CV-Adresse = (CVAdr_MSB << 8) + CVAdr_LSB`. **Antwort:** wie 6.1. + +### 6.3 LAN_X_CV_NACK_SC + +Wird bei fehlerhafter Programmierung wegen Kurzschluss am Gleis automatisch an den auslösenden Client geschickt. **Z21 an Client:** X-Header `0x61`, DB0 `0x12`, XOR `0x73`. + +### 6.4 LAN_X_CV_NACK + +Wird gesendet, wenn das ACK vom Decoder ausbleibt. Bei byteweisem Zugriff kann das Lesen lange dauern. **Z21 an Client:** X-Header `0x61`, DB0 `0x13`, XOR `0x72`. + +### 6.5 LAN_X_CV_RESULT + +„Positives ACK", an den auslösenden Client. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0x64 | 0x14 | CVAdr_MSB | CVAdr_LSB | Value | XOR | + +`CV-Adresse = (CVAdr_MSB << 8) + CVAdr_LSB`. *(Abbildung 6: Beispiel Sequenz CV Lesen.)* + +### 6.6 LAN_X_CV_POM_WRITE_BYTE + +Schreibt eine CV eines Lokdecoders (NMRA S-9.2.1 Abschnitt C) auf dem Hauptgleis (POM „Programming on the Main"). Normaler Betriebsmodus (Gleisspannung ein, Programmiermodus aus). Keine Rückmeldung. + +| DataLen | Header | X-Header | DB0 | DB1..DB5 | XOR | +|---|---|---|---|---|---| +| 0x0C 0x00 | 0x40 0x00 | 0xE6 | 0x30 | POM-Parameter | XOR | + +POM-Parameter: + +| Pos | Daten | Bedeutung | +|---|---|---| +| DB1 | Adr_MSB | | +| DB2 | Adr_LSB | `Lok-Adresse = (Adr_MSB & 0x3F) << 8 + Adr_LSB` | +| DB3 | `111011MM` | Option `0xEC`; MM = CVAdr_MSB | +| DB4 | CVAdr_LSB | `CV-Adresse = (MM << 8) + CVAdr_LSB` (0=CV1, …) | +| DB5 | Value | neuer CV-Wert | + +**Antwort:** keine. + +### 6.7 LAN_X_CV_POM_WRITE_BIT + +Wie 6.6, aber schreibt ein Bit einer CV (POM). DB3 = `111010MM` (Option `0xE8`), DB5 = `0000VPPP` (PPP = Bit-Position, V = neuer Bit-Wert). **Antwort:** keine. + +### 6.8 LAN_X_CV_POM_READ_BYTE + +*Ab Z21 FW Version 1.22.* Liest eine CV eines Lokdecoders auf dem Hauptgleis (POM). RailCom muss in der Z21 aktiviert, der Decoder RailCom-fähig sein (CV28 Bit 0/1 und CV29 Bit 3 = 1, Zimo). + +POM-Parameter: DB3 = `111001MM` (Option `0xE4`), DB5 = `0`. **Antwort:** [6.4](#64-lan_x_cv_nack) oder [6.5](#65-lan_x_cv_result). + +### 6.9 LAN_X_CV_POM_ACCESSORY_WRITE_BYTE + +*Ab Z21 FW Version 1.22.* Schreibt eine CV eines Accessory Decoders (NMRA S-9.2.1 Abschnitt D) auf dem Hauptgleis (POM). Keine Rückmeldung. `Header X-Header=0xE6`, DB0 `0x31`. + +POM-Parameter: + +| Pos | Daten | Bedeutung | +|---|---|---| +| DB1 | aaaaa | Decoder_Adresse MSB | +| DB2 | AAAACDDD | `aaaaaAAAACDDD = ((Decoder_Addresse & 0x1FF) << 4) \| CDDD`. CDDD=0000 → CV bezieht sich auf ganzen Decoder; C=1 → DDD = Ausgangsnummer | +| DB3 | `111011MM` | Option `0xEC`; MM = CVAdr_MSB | +| DB4 | CVAdr_LSB | `CV-Adresse = (MM << 8) + CVAdr_LSB` | +| DB5 | Value | neuer CV-Wert | + +**Antwort:** keine. + +### 6.10 LAN_X_CV_POM_ACCESSORY_WRITE_BIT + +*Ab Z21 FW Version 1.22.* Wie 6.9, aber Bit-Schreiben. DB3 = `111010MM` (Option `0xE8`), DB5 = `0000VPPP`. **Antwort:** keine. + +### 6.11 LAN_X_CV_POM_ACCESSORY_READ_BYTE + +*Ab Z21 FW Version 1.22.* Liest eine CV eines Accessory Decoders (POM). RailCom muss aktiviert sein, der Decoder RailCom-fähig. DB3 = `111001MM` (Option `0xE4`), DB5 = `0`. **Antwort:** [6.4](#64-lan_x_cv_nack) oder [6.5](#65-lan_x_cv_result). + +### 6.12 LAN_X_MM_WRITE_BYTE + +*Ab Z21 FW Version 1.23.* Überschreibt ein Register eines Motorola-Decoders auf dem Programmiergleis. + +| DataLen | Header | X-Header | DB0 | DB1 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---|---| +| 0x0A 0x00 | 0x40 0x00 | 0x24 | 0xFF | 0 | RegAdr | Value | XOR | + +`RegAdr`: 0=Register1, …, 78=Register79. `0 ≤ Value ≤ 255` (manche Decoder nur 0–80). **Antwort:** 2.9 ProgrammingMode an Clients mit Abo, sowie [6.3](#63-lan_x_cv_nack_sc) oder [6.5](#65-lan_x_cv_result). + +*Anmerkung:* Die Z21 verwendet den „6021-Programmiermodus" für MM-Decoder (nur Schreiben, kein Lesen, keine Erfolgsprüfung außer Kurzschlusserkennung). Funktioniert für viele Decoder von ESU, Zimo, Märklin — nicht zwingend für alle MM-Decoder (z.B. nicht mit DIP-Schaltern). `LAN_X_CV_RESULT` bedeutet hier nur „Programmiervorgang beendet", nicht „erfolgreich". Beispiel: `0x0A 0x00 0x40 0x00 0x24 0xFF 0x00 0x00 0x05 0xDE` → „Ändere Lokdecoder-Adresse (Register1) auf 5". + +### 6.13 LAN_X_DCC_READ_REGISTER + +*Ab Z21 FW Version 1.25.* Liest ein Register eines DCC-Decoders im Registermodus (S-9.2.3) auf dem Programmiergleis. + +| DataLen | Header | X-Header | DB0 | DB1 | XOR | +|---|---|---|---|---|---| +| 0x08 0x00 | 0x40 0x00 | 0x22 | 0x11 | REG | XOR | + +`REG`: 0x01=Register1, …, 0x08=Register8. `0 ≤ Value ≤ 255`. **Antwort:** 2.9 ProgrammingMode an Clients mit Abo, sowie [6.3](#63-lan_x_cv_nack_sc) oder [6.5](#65-lan_x_cv_result). *Registermodus nur für sehr alte DCC-Decoder; Direct CV bevorzugen.* + +### 6.14 LAN_X_DCC_WRITE_REGISTER + +*Ab Z21 FW Version 1.25.* Überschreibt ein Register eines DCC-Decoders im Registermodus (S-9.2.3). + +| DataLen | Header | X-Header | DB0 | DB2 | DB3 | XOR | +|---|---|---|---|---|---|---| +| 0x09 0x00 | 0x40 0x00 | 0x23 | 0x12 | REG | Value | XOR | + +`REG`: 0x01–0x08. `0 ≤ Value ≤ 255`. **Antwort:** wie 6.13. *Direct CV bevorzugen.* + +--- + +## 7 Rückmelder – R-BUS + +Die Rückmeldemodule (Bestellnummer 10787, 10808 und 10819) am R-BUS können mit folgenden Kommandos ausgelesen und konfiguriert werden. + +### 7.1 LAN_RMBUS_DATACHANGED + +Änderung am Rückmeldebus melden. Asynchron, wenn der Broadcast (Flag 0x00000002) aktiviert ist oder der Status explizit angefordert wurde. + +| DataLen | Header | Gruppenindex (1 Byte) | Rückmelder-Status (10 Byte) | +|---|---|---|---| +| 0x0F 0x00 | 0x80 0x00 | … | … | + +- **Gruppenindex:** `0` = Module mit Adressen 1–10, `1` = Module mit Adressen 11–20. +- **Rückmelder-Status:** 1 Byte pro Rückmelder, 1 Bit pro Eingang. Zuordnung statisch aufsteigend. + +Beispiel: GruppenIndex=1, Status `0x01 0x00 0xC5 0x00 ...` → „Rückmelder 11 Kontakt auf Eingang 1; Rückmelder 13 Kontakt auf Eingang 8,7,3 und 1". + +### 7.2 LAN_RMBUS_GETDATA + +Anfordern des aktuellen Status. **Anforderung:** `Header=0x81 0x00`, Gruppenindex (1 Byte). **Antwort:** siehe 7.1. + +### 7.3 LAN_RMBUS_PROGRAMMODULE + +Ändern der Rückmelder-Adresse. **Anforderung:** `Header=0x82 0x00`, Adresse (1 Byte). **Antwort:** keine. + +Adresse = neue Adresse (Wertebereich 0 und 1…20). Der Programmierbefehl wird am R-BUS ausgegeben, bis dieser Befehl erneut mit Adresse=0 gesendet wird. Während der Programmierung darf sich kein anderes Modul am R-BUS befinden. *(Abbildung 7: Beispiel Sequenz Rückmeldemodul programmieren.)* + +--- + +## 8 RailCom + +Die Z21 unterstützt RailCom durch: +- Erzeugung der RailCom-Lücke am Gleissignal. +- Globaler Empfänger in der Z21. +- Lokale Empfänger, z.B. in den Belegtmeldern 10808 (Lokerkennung; Kanal-2-Daten über CAN ab FW V1.29). +- POM-Lesen (siehe [6.8](#68-lan_x_cv_pom_read_byte) ab FW V1.22). +- Lokadressen-Erkennung bei Belegtmeldern (siehe [9.5](#95-lan_loconet_detector) ab V1.22 und [10.1](#101-lan_can_detector) ab V1.30). +- Decoder-Geschwindigkeit und Decoder-QoS ab FW V1.29. + +Voraussetzung: Decoder RailCom-fähig, CV28/CV29 korrekt konfiguriert, Option „RailCom" in der Z21 aktiviert. + +### 8.1 LAN_RAILCOM_DATACHANGED + +Ab FW V1.29. Antwort auf [8.2](#82-lan_railcom_getdata); auch ungefragt, wenn sich RailCom-Daten ändern und der Client den Broadcast (Flag 0x00000004) mit abonnierter Lok-Adresse oder den Broadcast 0x00040000 (alle Loks) aktiviert hat. + +**Z21 an Client:** `DataLen=0x11 0x00`, `Header=0x88 0x00`, RailComDaten. + +| Offset | Typ | Name | Bedeutung | +|---|---|---|---| +| 0 | UINT16 | LocoAddress | Adresse des erkannten Decoders | +| 2 | UINT32 | ReceiveCounter | Empfangszähler in Z21 | +| 6 | UINT16 | ErrorCounter | Empfangsfehlerzähler in Z21 | +| 8 | UINT8 | reserved | | +| 9 | UINT8 | Options | Flags-Bitmaske (siehe unten) | +| 10 | UINT8 | Speed | Geschwindigkeit 1 oder 2 (falls unterstützt) | +| 11 | UINT8 | QoS | Quality of Service (falls unterstützt) | +| 12 | UINT8 | reserved | | + +```c +#define rcoSpeed1 0x01 // CH7 subindex 0 +#define rcoSpeed2 0x02 // CH7 subindex 1 +#define rcoQoS 0x04 // CH7 subindex 7 +``` + +Die Struktur kann in Zukunft vergrößert werden — bei der Auswertung unbedingt DataLen berücksichtigen. + +### 8.2 LAN_RAILCOM_GETDATA + +RailCom-Daten anfordern (ab FW V1.29). **Anforderung:** `Header=0x89 0x00`, Typ 8 bit + LocoAddress 16 bit (little endian). +- Typ `0x01` = RailCom-Daten für gegebene Lokadresse anfordern. +- LocoAddress: Lokadresse; `0` = nächste Lok im Ringbuffer. + +**Antwort:** siehe [8.1](#81-lan_railcom_datachanged). + +--- + +## 9 LocoNet + +*Ab Z21 FW Version 1.20.* Die Z21 kann als Ethernet/LocoNet Gateway verwendet werden, wobei sie gleichzeitig der LocoNet-Master ist. + +Damit der Client Meldungen vom LocoNet bekommt, muss er die entsprechenden Meldungen mittels [2.16](#216-lan_set_broadcastflags) abonniert haben. +- Empfangene Meldungen → Header `LAN_LOCONET_Z21_RX`. +- Selbst gesendete Meldungen → Header `LAN_LOCONET_Z21_TX`. +- Mit `LAN_LOCONET_FROM_LAN` kann der Client selbst Meldungen auf den Bus schreiben (andere Clients mit Abo werden ebenfalls per `LAN_LOCONET_FROM_LAN` benachrichtigt; nur der Absender nicht). + +*(Abbildung 8: Beispiel Sequenz Ethernet/LocoNet Gateway.)* Selbst triviale Vorgänge am Bus können erheblichen Netzwerkverkehr erzeugen. Diese Funktionalität ist primär für PC-Steuerungen gedacht. Wägen Sie die Flags 0x02000000 (Loks) und 0x04000000 (Weichen) genau ab — verwenden Sie zum konventionellen Fahren/Schalten möglichst die Befehle aus den Kapiteln 4, 5 und 6. Das LocoNet-Protokoll selbst wird hier nicht beschrieben (siehe Digitrax bzw. Hardware-Hersteller). + +### 9.1 LAN_LOCONET_Z21_RX + +*Ab FW 1.20.* Asynchron, wenn der Broadcast (Flags 0x01000000/0x02000000/0x04000000) aktiviert ist und eine Meldung am LocoNet-Bus empfangen wurde. + +| DataLen | Header | Data | +|---|---|---| +| 0x04+n 0x00 | 0xA0 0x00 | LocoNet Meldung inkl. CKSUM (n Bytes) | + +### 9.2 LAN_LOCONET_Z21_TX + +*Ab FW 1.20.* Analog zu 9.1, wenn die Z21 eine Meldung auf den Bus geschrieben hat. `Header=0xA1 0x00`, LocoNet Meldung inkl. CKSUM (n Bytes). + +### 9.3 LAN_LOCONET_FROM_LAN + +*Ab FW 1.20.* Ein Client schreibt eine Meldung auf den LocoNet-Bus. Wird auch asynchron an andere Clients gemeldet (Flags 0x01000000/0x02000000/0x04000000), wenn ein anderer Client geschrieben hat. + +| DataLen | Header | Data | +|---|---|---| +| 0x04+n 0x00 | 0xA2 0x00 | LocoNet Meldung inkl. CKSUM (n Bytes) | + +#### 9.3.1 DCC Binary State Control Instruction per LocoNet OPC_IMM_PACKET + +Ab FW V1.42 wird zum Schalten von Binary States das neue Kommando [4.3.3 LAN_X_SET_LOCO_BINARY_STATE](#433-lan_x_set_loco_binary_state) empfohlen. Der folgende (etwas veraltete) Absatz bleibt zur Vollständigkeit: + +Ab FW V1.25 können mittels `LAN_LOCONET_FROM_LAN` und dem LocoNet-Befehl `OPC_IMM_PACKET` beliebige DCC-Pakete am Gleisausgang generiert werden, darunter die Binary State Control Instruction („F29…F32767"). Das gilt auch für die weiße z21 (virtueller LocoNet-Stack). Zum Aufbau siehe LocoNet Spec bzw. NMRA S-9.2.1 (Feature Expansion Instruction). + +### 9.4 LAN_LOCONET_DISPATCH_ADDR + +*Ab FW 1.20.* Eine Lok-Adresse zum LocoNet-Dispatch vorbereiten („DISPATCH_PUT"). + +**Anforderung:** `Header=0xA3 0x00`, Lok-Adresse 16 bit (little endian). + +**Antwort:** +- FW < 1.22: keine. +- FW ≥ 1.22: `Header=0xA3 0x00`, Lok-Adresse 16 bit (little endian) + Ergebnis 8 bit. + - `0` = „DISPATCH_PUT" fehlgeschlagen (z.B. Z21 als Slave, Master hat abgelehnt, Adresse bereits zugeteilt). + - `>0` = erfolgreich; Wert = aktuelle LocoNet Slot-Nummer für die Lok-Adresse. + +*(Abbildung 9: Beispiel Sequenz LocoNet Dispatch per LAN-Client.)* + +### 9.5 LAN_LOCONET_DETECTOR + +*Ab FW 1.22.* Abfragen/Benachrichtigung über Belegtstatus von LocoNet-Gleisbesetztmeldern, ohne das LocoNet-Protokoll selbst verarbeiten zu müssen. + +*Unterschied:* Roco 10787 (R-BUS) basiert auf mechanischen Schaltkontakten; LocoNet-Gleisbesetztmelder basieren üblicherweise auf Strommessung bzw. Transponder/Infrarot/RailCom (im Idealfall nur eine Meldung bei Statusänderung). + +**Anforderung (Status abfragen):** + +| DataLen | Header | Typ 8 bit | Reportadresse 16 bit (little endian) | +|---|---|---|---| +| 0x07 0x00 | 0xA4 0x00 | … | … | + +- `0x80`: „Stationary Interrogate Request" (SIC) gem. Digitrax (auch Blücher-Elektronik). Reportadresse hier 0 (don't care). +- `0x81`: Reportadresse für Uhlenbrock-Besetztmelder (z.B. UB63320 über LNCV 17; Default 1017). Nur zum Abfragen, nicht mit Rückmelderadresse zu verwechseln. Am LocoNet-Bus über Weichenstellbefehle implementiert → Wert um 1 dekrementiert übergeben. Beispiel: `0x07 0x00 0xA4 0x00 0x81 0xF8 0x03` → Status aller Besetztmelder mit Reportadresse 1017 (`= 0x03F8 + 1 = 1016 + 1`). +- `0x82`: Statusabfrage für LISSY (ab FW 1.23). Bei Uhlenbrock LISSY entspricht die Reportadresse der Rückmelderadresse; Rückmeldung abhängig vom LISSY-Betriebsmodus. + +Bei einer Anfrage können mehrere Besetztmelder gleichzeitig angesprochen werden → mehrere Antworten, ggf. mehrfach pro Eingang. + +**Antwort:** + +| DataLen | Header | Typ 8 bit | Rückmelderadresse 16 bit (little endian) | Info[n] | +|---|---|---|---|---| +| 0x07+n 0x00 | 0xA4 0x00 | … | … | … | + +Asynchron, wenn der Broadcast (Flag 0x08000000) aktiviert ist und eine Meldung empfangen wurde (Statusänderung oder explizite Abfrage). **Rückmelderadresse:** jedem Eingang zugeordnet, vom Anwender konfigurierbar (z.B. via LNCV). + +| Typ | Bedeutung | n | Info | +|---|---|---|---| +| `0x01` | Besetzt/Frei (z.B. Uhlenbrock 63320, Blücher GBM16XL; LocoNet OPC_INPUT_REP, X=1) | 1 | Info[0]=0 → frei (LO), =1 → belegt (HI) | +| `0x02` | Transponder Enters Block (z.B. Blücher GBM16XN, OPC_MULTI_SENSE) | 2 | Transponderadresse 16 Bit LE: Info[0]=Low, Info[1]=High | +| `0x03` | Transponder Exits Block | 2 | wie 0x02 | +| `0x10` | LISSY Lokadresse (ab FW 1.23; Uhlenbrock-Übergabeformat, LNCV 15=1) | 3 | Info[0/1]=Lokadresse 16 Bit LE; Info[2]=`0 DIR1 DIR0 0 K3 K2 K1 K0` | +| `0x11` | LISSY Belegtzustand (ab FW 1.23) | 1 | Info[0]=0 → Block frei, =1 → belegt | +| `0x12` | LISSY Geschwindigkeit (ab FW 1.23) | 2 | Info[0/1]=Geschwindigkeit 16 Bit LE | + +Hinweise zu Typ 0x02/0x03 (GBM16XN): Transponderadresse identifiziert das Fahrzeug (Lok-Adresse via RailCom). Zur Rückmelderadresse +1 addieren, um die im GBM16XN konfigurierte Adresse zu erhalten. Das Bit unter Maske 0x1000 (Fahrtrichtung) kollidiert mit dem Adressraum langer Lok-Adressen — diese Konfiguration wird nicht empfohlen. + +Hinweise zu Typ 0x10 (LISSY): Loks 1…9999, Wagen 10000…16382. `DIR1=0` → DIR0 ignorieren; `DIR1=1` → DIR0=0 vorwärts, DIR0=1 rückwärts; K3..K0 = 4-Bit Klasseninformation. *Beispielkonfigurationen für Lissy-Empfänger 68610 (LNCV-Tabellen) siehe Original.* Typ wird künftig um weitere IDs erweitert. + +--- + +## 10 CAN + +### 10.1 LAN_CAN_DETECTOR + +*Ab Z21 FW Version 1.30.* Der Roco CAN-Belegtmelder 10808 wird ab FW 1.30 unterstützt. Vier Verwendungsweisen: +1. **R-BUS-Emulation**: Belegtmelder als R-BUS-Melder (siehe Kapitel 7). +2. **LocoNet-Emulation**: als LocoNet-Melder (siehe 9.5; Typ 0x01 belegt/frei, Typ 0x02/0x03 Transponder). +3. **LISSY-Emulation**: durch LISSY/Marco-Meldungen (siehe 9.5; Typ 0x10 Lokadresse, Typ 0x11 Belegtzustand). +4. **Direkter Zugriff** durch `LAN_CAN_DETECTOR` (siehe unten). + +Emulation konfigurierbar über das Z21 Maintenance Tool. Werkseinstellung: R-BUS=ein, LocoNet=ein, LISSY=aus. Der direkte Zugriff (`0xC4`) ist am schnellsten und ressourcenschonendsten — empfohlen bei vielen CAN-Belegtmeldern. + +**Anforderung:** + +| DataLen | Header | Typ 8 bit | CAN-NetworkID 16 bit (little endian) | +|---|---|---|---| +| 0x07 0x00 | 0xC4 0x00 | 0x00 | … | + +- Typ `0x00`: Abfrage des Belegtmelders mit gegebener CAN-NetworkID. `0xD000` = „alle CAN-Belegtmelder". Beispiel: `0x07 0x00 0xC4 0x00 0x00 0x00 0xD0`. + +**Antwort:** + +| DataLen | Header | NId 16 | Addr 16 | Port 8 | Typ 8 | Value1 16 | Value2 16 | +|---|---|---|---|---|---|---|---| +| 0x0E 0x00 | 0xC4 0x00 | … | … | … | … | … | … | + +Asynchron, wenn der Broadcast (Flag 0x00080000) aktiviert ist und eine Meldung empfangen wurde. Alle 16-bit Werte little endian. +- **NId**: unveränderbare CAN-NetworkID. +- **Addr**: konfigurierbare Moduladresse. +- **Port**: Eingang (0–7). +- **Typ**: `0x01` Belegtstatus; `0x11`–`0x1F` erkannte Lokadressen (0x11 = 1./2., 0x12 = 3./4., …, 0x1F = 29./30.). + +Falls Typ = `0x01` (Belegtstatus), Value1: + +| Wert | Bedeutung | +|---|---| +| 0x0000 | Frei, ohne Spannung | +| 0x0100 | Frei, mit Spannung | +| 0x1000 | Besetzt, ohne Spannung | +| 0x1100 | Besetzt, mit Spannung | +| 0x1201 | Besetzt, Überlast 1 | +| 0x1202 | Besetzt, Überlast 2 | +| 0x1203 | Besetzt, Überlast 3 | + +Falls Typ = `0x11`–`0x1F` (RailCom Lokadressen): Value1/Value2 = erste/zweite erkannte Lokadresse inkl. Richtung. `0` = keine Adresse erkannt bzw. Listenende. In den obersten 2 Bits: `0x` keine Richtung, `10` vorwärts, `11` rückwärts; in den untersten 14 Bits die Lokadresse. + +### 10.2 CAN Booster + +*Ab Z21 FW Version 1.41.* LAN-Befehle für CAN-Booster-Management (Roco 10806, 10807, 10869). Funktionieren nur, wenn die Booster über CAN-Bus (nicht B-BUS) mit der Z21 verbunden sind. + +#### 10.2.1 LAN_CAN_DEVICE_GET_DESCRIPTION + +Bezeichnung (Freitext) aus CAN-Booster auslesen. **Anforderung:** `Header=0xC8 0x00`, NId 16 bit. **Antwort:** `DataLen=0x16 0x00`, `Header=0xC8 0x00`, NId 16 bit + `UINT8 Name[16]`. + +NId = CAN-NetworkID (0xC101–0xC1FF). Name = nullterminierter String, ISO 8859-1 (Latin-1). *Hinweis:* nicht zwei Requests schnell hintereinander senden; zuerst Antwort abwarten. Die NetworkIDs aller Booster liefert [10.2.3](#1023-lan_can_booster_systemstate_chgd). + +#### 10.2.2 LAN_CAN_DEVICE_SET_DESCRIPTION + +Bezeichnung überschreiben. **Anforderung:** `Header=0xC9 0x00`, NId 16 bit + `UINT8 Name[16]`. **Antwort:** keine. Rest von Data mit 0x00 auffüllen; nach 16 Zeichen wird abgeschnitten. Nicht erlaubt: `"` (0x22) und `\` (0x5C). + +#### 10.2.3 LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD + +Systemzustand des CAN-Boosters melden (ca. einmal pro Sekunde, pro Booster und Ausgang). Asynchron, wenn der Broadcast (Flag 0x00020000) aktiviert ist und mind. ein Booster über CAN verbunden ist. + +**Z21 an Client:** `DataLen=0x0E 0x00`, `Header=0xCA 0x00`, CANBoosterSystemState (10 Bytes). + +| Offset | Typ | Name | Wert | +|---|---|---|---| +| 0 | UINT16 | NId | 0xC101…0xC1FF (CAN-NetworkID) | +| 2 | UINT16 | Booster_OutputPort | 1 = erste Endstufe, 2 = zweite (nur 10807) | +| 4 | UINT16 | Booster_State | bitmask (siehe unten) | +| 6 | UINT16 | Booster_VCCVoltage | mV (Spannung an der Endstufe) | +| 8 | UINT16 | Booster_Current | mA (Strom an der Endstufe) | + +```c +#define bsBgActive 0x0001 // Bremsgenerator aktiv (ZCAN SSP) +#define bsShortCircuit 0x0020 // Kurzschluss an Endstufe (ZCAN UES) +#define bsTrackVoltageOff 0x0080 // Gleisspannung ist abgeschaltet (OFF) +#define bsRailComActive 0x0800 // RailCom-Cutout aktiv +#define bsOutputDisabled 0x0100 // Booster Ausgang deaktiviert (by user) — ab Booster FW V1.11 +``` + +#### 10.2.4 LAN_CAN_BOOSTER_SET_TRACKPOWER + +Booster Management: Gleisausgänge deaktivieren/reaktivieren. **Anforderung:** `Header=0xCB 0x00`, NId 16 bit + Power 8 bit. + +- `0x00` alle Ausgänge deaktivieren / `0xFF` reaktivieren +- *(ab FW V1.42 + Booster FW V1.11)* `0x10`/`0x11` erster Ausgang aus/ein, `0x20`/`0x22` zweiter Ausgang aus/ein (Z21 dual BOOSTER) + +Ausgänge können nur eingeschaltet werden, wenn die Zentrale eingeschaltet ist und ein gültiges Gleissignal sendet. Einstellungen nicht persistent. **Antwort:** bei Änderung [10.2.3](#1023-lan_can_booster_systemstate_chgd) an Clients mit Abo. + +--- + +## 11 zLink + +Die zLink-Schnittstelle erlaubt es, Endgeräte mit kleinerem Microcontroller ohne eigenes LAN/WLAN ins Netzwerk zu integrieren. Endgeräte (Stand 06/2021): 10806 single BOOSTER, 10807 dual BOOSTER, 10869 XL BOOSTER, 10836 switch DECODER, 10837 signal DECODER. + +### 11.1 Adapter + +An die zLink-Schnittstelle kann ein Adapter angeschlossen werden — z.B. der **10838 Z21 pro LINK**. + +#### 11.1.1 10838 Z21 pro LINK + +Verbindet als Gateway die zLink-Schnittstelle mit dem WLAN, für: +1. Konfiguration des Endgeräts (Tasten/Display, Z21 App, Maintenance Tool). +2. Firmware Update (Z21 Updater App, Maintenance Tool). +3. Steuerung durch WLAN-Clients über das Z21 LAN Protokoll. + +Im jeweiligen Endgerät ist ein zugeschnittener Z21-Protokoll-Stack implementiert; Kommandos werden wie an eine Zentrale per UDP geschickt (z.B. Boosterausgänge schalten, Systemstatus abfragen, Weichen/Signale direkt schalten, Decoder per CV-Schreibbefehl konfigurieren — sogar ohne Verbindung zum Hauptgleis). Zu beachten: +- Eingeschränkte Bandbreite: effektive Transferrate deutlich unter 1024 Bytes/s halten. +- Zwischen zwei Befehlen mind. 50 ms Pause. +- Z21 pro LINK vorzugsweise im Client Mode verwenden. +- Möglichst nur ein WLAN-Client verbinden, maximal 4 Clients. + +UDP-Broadcasts möglich, aber nur zum Auffinden der Geräte empfohlen. Danach Zuordnung über Hardware-Typ (`LAN_GET_HWINFO`), Seriennummer (`LAN_GET_SERIAL_NUMBER`), IP-Adresse und konfigurierbaren Namen. Ein Befehl, den der Z21 pro LINK selbst beantwortet (nicht durchreicht), ist `LAN_ZLINK_GET_HWINFO`. + +##### 11.1.1.1 LAN_ZLINK_GET_HWINFO + +Abfragen der Eigenschaften des Z21 pro LINK. Als UDP-Broadcast gesendet, lassen sich die im WLAN angemeldeten Z21 pro LINK auffinden. + +**Anforderung an Z21 pro LINK:** `DataLen=0x05 0x00`, `Header=0xE8 0x00`, Data[0]=`0x06` (ZLINK_MSG_TYPE_HW_INFO). + +**Antwort:** `DataLen=0x3F 0x00`, `Header=0xE8 0x00`, Data[0]=`0x06` + Z_Hw_Info (58 Bytes). + +| Offset | Typ | Name | Beispiel | +|---|---|---|---| +| 0 | UINT16 | HwID | 401 (0x191) | +| 2 | UINT8 | FW_Version_Major | 1 | +| 3 | UINT8 | FW_Version_Minor | 1 | +| 4 | UINT16 | FW_Version_Build | 3217 (0xC91) | +| 6 | UINT8[18] | MAC_Address (string) | „EC FA BC 4F 04 C6" | +| 24 | UINT8[33] | Name (string) | „this_is_a_quite_long_device_name" | +| 57 | UINT8 | Reserved | 0x00 | + +- **HwID**: 401 (0x191) = Adapter 10838 Z21 pro LINK. +- **MAC_Address**: nullterminierte Zeichenkette, 8-bit ASCII. +- **Name**: vom Anwender konfigurierbar, max. 32 Zeichen + 0x00, ISO 8859-1 (Latin-1). Alle Zeichen nach dem ersten 0x00 ignorieren. + +### 11.2 Booster 10806, 10807 und 10869 + +Unterstützte Befehle siehe Anhang A. Zusätzlich gibt es boosterspezifische Befehle: + +#### 11.2.1 LAN_BOOSTER_GET_DESCRIPTION + +Bezeichnung auslesen. **Anforderung:** `Header=0xB8 0x00`. **Antwort:** `DataLen=0x24 0x00`, `Header=0xB8 0x00`, `UINT8 Name[32]`. String ISO 8859-1, aus CAN-Kompatibilität ≤ 16 Zeichen. *Sonderfall:* `Name[0]==0xFF` → noch nie eine Bezeichnung abgelegt (als Leerstring interpretieren). + +#### 11.2.2 LAN_BOOSTER_SET_DESCRIPTION + +Bezeichnung überschreiben. **Anforderung:** `Header=0xB9 0x00`, `UINT8 Name[32]`. Rest mit 0x00 auffüllen; nicht erlaubt `"` (0x22) und `\` (0x5C). **Antwort:** keine. + +#### 11.2.3 LAN_BOOSTER_SYSTEMSTATE_GETDATA + +Anfordern des Systemzustandes. **Anforderung:** `Header=0xBB 0x00`. **Antwort:** siehe [11.2.4](#1124-lan_booster_systemstate_datachanged). + +#### 11.2.4 LAN_BOOSTER_SYSTEMSTATE_DATACHANGED + +Asynchron vom Booster, wenn der Broadcast (Flag 0x00000100) aktiviert ist oder der Status explizit angefordert wurde. + +**Booster an Client:** `DataLen=0x1C 0x00`, `Header=0xBA 0x00`, BoosterSystemState (24 Bytes). + +| Offset | Typ | Name | | +|---|---|---|---| +| 0 | INT16 | Booster_1_MainCurrent | mA | +| 2 | INT16 | Booster_2_MainCurrent | mA | +| 4 | INT16 | Booster_1_FilteredMainCurrent | mA | +| 6 | INT16 | Booster_2_FilteredMainCurrent | mA | +| 8 | INT16 | Booster_1_Temperature | °C | +| 10 | INT16 | Booster_2_Temperature | °C | +| 12 | UINT16 | SupplyVoltage | mV | +| 14 | UINT16 | Booster_1_VCCVoltage | mV | +| 16 | UINT16 | Booster_2_VCCVoltage | mV | +| 18 | UINT8 | CentralState | bitmask | +| 19 | UINT8 | CentralStateEx | bitmask | +| 20 | UINT8 | CentralStateEx2 | bitmask | +| 21 | UINT8 | Reserved1 | | +| 22 | UINT8 | CentralStateEx3 | bitmask | +| 23 | UINT8 | Reserved2 | | + +```c +// CentralState +#define csTrackVoltageOff 0x02 // Die Gleisspannung ist abgeschaltet +#define csConfigMode 0x10 // Konfigurationsmodus aktiv +#define csCanConnected 0x20 // CAN Verbindung mit Zentrale Ok + +// CentralStateEx +#define cseHighTemperature 0x01 // zu hohe Temperatur +#define csePowerLost 0x02 // zu geringe Eingangsspannung +#define cseBooster_1_ShortCircuit 0x04 // Kurzschluss an 1. Endstufe +#define cseBooster_2_ShortCircuit 0x08 // Kurzschluss an 2. Endstufe +#define cseRevPol 0x10 // Fehler Versorgungsspannung +#define cseNoDCCInput 0x80 // kein DCC-Eingangssignal vorhanden + +// CentralStateEx2 +#define cse2Booster_1_RailComActive 0x01 // RailCom aktiv 1. Endstufe +#define cse2Booster_2_RailComActive 0x02 // RailCom aktiv 2. Endstufe +#define cse2Booster_1_MasterSettings 0x04 // CAN Autosettings Ok 1. Endstufe +#define cse2Booster_2_MasterSettings 0x08 // CAN Autosettings Ok 2. Endstufe +#define cse2Booster_1_BgActive 0x10 // Bremsgenerator aktiv 1. Endstufe +#define cse2Booster_2_BgActive 0x20 // Bremsgenerator aktiv 2. Endstufe +#define cse2Booster_1_RailComFwd 0x40 // RailCom Forwarding aktiv 1. Endstufe +#define cse2Booster_2_RailComFwd 0x80 // RailCom Forwarding aktiv 2. Endstufe + +// CentralStateEx3 +#define cse3Booster_1_OutputInverted 0x01 // 1. Endstufe invertiert (Autoinvert) +#define cse3Booster_2_OutputInverted 0x02 // 2. Endstufe invertiert (Autoinvert) +#define cse3Booster_1_OutputDisabled 0x10 // 1. Endstufe deaktiviert (by user) — ab Booster FW V1.11 +#define cse3Booster_2_OutputDisabled 0x20 // 2. Endstufe deaktiviert (by user) — ab Booster FW V1.11 +``` + +#### 11.2.5 LAN_BOOSTER_SET_POWER + +*Ab Booster FW V1.11.* Booster Management durch Anwender. Werden alle Ausgänge deaktiviert/reaktiviert, entspricht das `LAN_X_SET_TRACK_POWER_OFF`/`_ON` am Booster. Beim 10807 dual BOOSTER kann auch ein einzelner Ausgang geschaltet werden. + +**Anforderung:** `Header=0xB2 0x00`, BoosterPort 8 bit + BoosterPortState 8 bit. +- BoosterPort: `0x01` erster Ausgang, `0x02` zweiter Ausgang (nur dual), `0x03` alle. +- BoosterPortState: `0x00` deaktivieren, `0x01` reaktivieren. + +Einstellungen nicht persistent. **Antwort:** bei Änderung [11.2.4](#1124-lan_booster_systemstate_datachanged) an Clients mit Abo. + +### 11.3 Decoder 10836 und 10837 + +Unterstützte Befehle siehe Anhang A; einige decoderspezifische Befehle: + +#### 11.3.1 LAN_DECODER_GET_DESCRIPTION + +Bezeichnung auslesen. **Anforderung:** `Header=0xD8 0x00`. **Antwort:** `DataLen=0x24 0x00`, `Header=0xD8 0x00`, `UINT8 Name[32]` (Codierung wie 11.2.1). + +#### 11.3.2 LAN_DECODER_SET_DESCRIPTION + +Bezeichnung überschreiben. **Anforderung:** `Header=0xD9 0x00`, `UINT8 Name[32]` (Codierung wie 11.2.2). **Antwort:** keine. + +#### 11.3.3 LAN_DECODER_SYSTEMSTATE_GETDATA + +Anfordern des Systemzustandes. **Anforderung:** `Header=0xDB 0x00`. **Antwort:** siehe [11.3.4](#1134-lan_decoder_systemstate_datachanged). + +#### 11.3.4 LAN_DECODER_SYSTEMSTATE_DATACHANGED + +Asynchron vom Decoder, wenn der Broadcast (Flag 0x00000100) aktiviert ist oder der Status explizit angefordert wurde. (Meldet sich der Signaldecoder nach 4 s nicht, kann gepollt werden.) Die Antworten von 10836 und 10837 unterscheiden sich in Aufbau/Inhalt und sind anhand von **DataLen** zu unterscheiden. + +##### 11.3.4.1 SwitchDecoderSystemState (10836) + +**An Client:** `DataLen=0x30 0x00`, `Header=0xDA 0x00`, SwitchDecoderSystemState (44 Bytes). + +| Offset | Typ | Name | | +|---|---|---|---| +| 0 | INT16 | Current | mA (Strom) | +| 2 | INT16 | FilteredCurrent | mA (geglättet) | +| 4 | UINT16 | Voltage | mV (interne Spannung 3.3V) | +| 6 | UINT8 | CentralState | bitmask | +| 7 | UINT8 | CentralStateEx | bitmask | +| 8 | UINT8[8] | OutputStates[0..7] | Status pro Ausgang | +| 16 | UINT8[8] | OutputConfig[0..7] | Betriebsmodus pro Ausgang | +| 24 | UINT8[4] | OutputDimm[0..7] | Dimmwert pro Ausgang | +| 32 | UINT16 | Address | Erste Decoderadresse | +| 34 | UINT16 | Address2 | Zweite Decoderadresse | +| 36 | UINT8[6] | Reserved1 | | +| 42 | UINT8 | Dimmed | 1 Bit pro Ausgang | +| 43 | UINT8 | Reserved2 | | + +```c +// CentralState +#define csEmergencyStop 0x01 // Not-Aus für Decoder +#define csTrackVoltageOff 0x02 // Die Gleisspannung ist abgeschaltet +#define csShortCircuit 0x04 // Kurzschluss erkannt +#define csConfigMode 0x10 // Konfigurationsmodus aktiv +// CentralStateEx +#define csePowerLost 0x02 // zu geringe Eingangsspannung +#define cseRCN213 0x20 // Adressierung gem. RCN213 +#define cseNoDCCInput 0x80 // kein DCC-Eingangssignal vorhanden + +// OutputState — Zustand des Ausgangs +#define oUnknown 0x00 +#define oRedActive 0x11 +#define oRedInactive 0x01 +#define oGreenActive 0x12 +#define oGreenInactive 0x02 + +// OutputConfig — Betriebsmodus +#define ocfgNormal 0 // Impulsbetrieb (default) +#define ocfgBlinker 1 // Wechselblinker +#define ocfgBlinkSm 2 // Wechselblinker mit Ein-/Ausblenden +#define ocfg10775 3 // Momentbetrieb wie 10775 +#define ocfgK84 4 // Dauerbetrieb (z.B. Beleuchtung) +#define ocfgK84Sm 5 // Dauerbetrieb mit Ein-/Ausblenden +``` + +- **FilteredCurrent**: Summe interner Stromverbrauch + Verbrauch an den Klemmen. +- **OutputDimm**: 0 = Dimmung deaktiviert (volle Leistung); 1–100 = min. bis max. Leistung. +- **Address**: einer Decoderadresse entsprechen 4 Weichennummern (Addr 1 → #1–4, Addr 2 → #5–8, …). +- **Address2**: =0 → automatisch „Erste Decoderadresse + 1"; sonst analog Address. +- **Dimmed**: 1 Bit pro Ausgangspaar (0 = nicht gedimmt, 1 = gedimmt/Auf-/Abblenden). LSB = Paar 1, MSB = Paar 8. + +##### 11.3.4.2 SignalDecoderSystemState (10837) + +**An Client:** `DataLen=0x2E 0x00`, `Header=0xDA 0x00`, SignalDecoderSystemState (42 Bytes). + +| Offset | Typ | Name | | +|---|---|---|---| +| 0 | INT16 | Current | mA (0 / reserviert) | +| 2 | INT16 | FilteredCurrent | mA (0 / reserviert) | +| 4 | UINT16 | Voltage | mV (Spannung an den Klemmen) | +| 6 | UINT8 | CentralState | bitmask | +| 7 | UINT8 | CentralStateEx | bitmask | +| 8 | UINT8[2] | OutputStates[0..1] | Ein/Aus-Status Ausgänge A1…B8 | +| 10 | UINT8[2] | BlinkStates[0..1] | Blink-Status A1…B8 | +| 12 | UINT8[4] | SignalDccExt[0..3] | DCCext aktueller Signalbegriff 1.–4. Signal | +| 16 | UINT8[4] | SignalCurrAsp[0..3] | Index aktueller Signalbegriff | +| 20 | UINT8[3] | Reserved1 | | +| 23 | UINT8 | SignalCount | Anzahl verwendeter Signale (2/3/4) | +| 24 | UINT8[4] | SignalConfig[0..3] | Signal-ID Konfiguration 1.–4. Signal | +| 28 | UINT8[4] | SignalInitAsp[0..3] | Index Initialisierung | +| 32 | UINT16 | Address | Erste Decoderadresse | +| 34 | UINT16[4] | Reserved2 | | + +```c +// CentralState +#define csEmergencyStop 0x01 // Not-Aus für Decoder +#define csTrackVoltageOff 0x02 // Die Gleisspannung ist abgeschaltet +#define csShortCircuit 0x04 // Kurzschluss erkannt +#define csConfigMode 0x10 // Konfigurationsmodus aktiv +// CentralStateEx +#define csePowerLost 0x02 // zu geringe Eingangsspannung +#define cseEEPromError 0x10 // EEPROM Schreib/Lesefehler +#define cseRCN213 0x20 // Adressierung gem. RCN213 +#define cseNoDCCInput 0x80 // kein DCC-Eingangssignal vorhanden +``` + +- **OutputStates/BlinkStates**: [0] LSB=A1 … MSB=A8; [1] LSB=B1 … MSB=B8. +- **SignalConfig** = Signal-ID (Signaltyp); **SignalDccExt** = DCCext-Wert (aktueller Signalbegriff). Werte siehe `https://www.z21.eu/de/produkte/z21-signal-decoder/signaltypen`. +- **Address**: einer Decoderadresse entsprechen 4 Signaladressen; der Decoder belegt 4 Decoderadressen = 16 Signaladressen (Addr 1 → Signaladressen 1–16, usw.). + +--- + +## 12 Modellzeit + +*Ab Z21 FW Version 1.43.* Die beschleunigte Modellzeit der Z21 steht nun auch Teilnehmern am Gleis, X-BUS und LAN zur Verfügung (Beschleunigungsfaktor ≤ 63). Die Z21 hat keine Echtzeituhr — die Modellzeit beginnt immer bei der einstellbaren Startzeit. + +- DCC-Zeitmeldungen am Gleis: siehe RCN-211. +- LocoNet: Clock Slot `0x7B` ca. alle 70–100 s pollen. +- X-BUS: Zeitmeldung gem. XpressNet™ V4.0 einmal pro Modellminute. +- LAN: optional per „MRclock" Multicast an `239.50.50.20`, Port `2000` (einmal pro Modellminute, mind. dreimal pro echter Minute). + +### 12.1 LAN_FAST_CLOCK_CONTROL + +#### 12.1.1 Modellzeit lesen + +**Anforderung:** `DataLen=0x07 0x00`, `Header=0xCC 0x00`, Data `0x21 0x2A 0x0B`. **Antwort:** siehe [12.2](#122-lan_fast_clock_data). + +#### 12.1.2 Modellzeit setzen + +Setzt Rate und aktuelle Modellzeit. + +| DataLen | Header | Data | +|---|---|---| +| 0x0A 0x00 | 0xCC 0x00 | `0x24 0x2B DDDhhhhh 00mmmmmm 00rrrrrr` XOR-Byte | + +- `DDD`: Wochentag (3 Bits), 0=Montag … 6=Sonntag. +- `hhhhh`: Stunde (5 Bits), 0–23. +- `mmmmmm`: Minute (6 Bits), 0–59. +- `rrrrrr`: Rate (6 Bits), 0–63. 0 = Modellzeit bleibt stehen (nicht empfohlen, besser [12.1.4](#1214-modellzeit-anhalten)); 1 = Echtzeit; 2 = doppelt so schnell; usw. *Die Rate wird persistent gespeichert.* +- `XOR-Byte`: XOR-Prüfsumme über Data. + +**Antwort:** [12.2](#122-lan_fast_clock_data) an Clients mit Abo. + +#### 12.1.3 Modellzeit starten + +Startet (setzt fort) die Modellzeituhr. **Anforderung:** `Header=0xCC 0x00`, Data `0x21 0x2C 0x0D`. **Antwort:** [12.2](#122-lan_fast_clock_data) an Clients mit Abo. *Der Zustand „fcFastClockEnabled" wird persistent gespeichert.* + +#### 12.1.4 Modellzeit anhalten + +Hält die Modellzeituhr an. **Anforderung:** `Header=0xCC 0x00`, Data `0x21 0x2D 0x0C`. **Antwort:** [12.2](#122-lan_fast_clock_data) an Clients mit Abo. *Der Zustand „not fcFastClockEnabled" wird persistent gespeichert.* + +### 12.2 LAN_FAST_CLOCK_DATA + +Aktuelle Modellzeit melden. Asynchron, wenn der Broadcast (Flag 0x00000010) aktiviert ist oder die Modellzeit explizit gelesen wurde. Bei laufender Uhr ca. einmal pro Modellminute, auch bei Start/Stop/Setzen. Übersprungene Zeitmeldungen müssen Clients tolerieren (ggf. anhand des Beschleunigungsfaktors selbst weiterrechnen). + +**Z21 an Client:** `DataLen=0x0C 0x00`, `Header=0xCD 0x00`, FastClockTime (8 Bytes). + +| Offset | Typ | Name | Wert | +|---|---|---|---| +| 0 | UINT8 | — | 0x66 | +| 1 | UINT8 | — | 0x25 | +| 2 | UINT8 | DDDh hhhh | Wochentag und Stunde | +| 3 | UINT8 | 00mm mmmm | Minute | +| 4 | UINT8 | SHss ssss | Sekunde, mit STOP-/HALT-Flag | +| 5 | UINT8 | 00rr rrrr | Rate | +| 6 | UINT8 | FcSettings | Einstellungen-Flags | +| 7 | UINT8 | XOR-Byte | XOR-Prüfsumme über Data | + +- `DDD` Wochentag (0=Montag…6=Sonntag); `hhhhh` Stunde 0–23; `mmmmmm` Minute 0–59; `ssssss` Sekunde 0–59; `rrrrrr` Rate 0–63. +- `S` STOP-Flag: Modellzeit läuft nicht (Fastclock nicht enabled oder Rate=0). +- `H` HALT-Flag: vorübergehend angehalten (Nothalt oder Kurzschluss am Gleis). +- `FcSettings`: persistente Einstellungen, siehe [12.3](#123-lan_fast_clock_settings_get). + +### 12.3 LAN_FAST_CLOCK_SETTINGS_GET + +Auslesen der persistenten Modellzeit-Einstellungen. **Anforderung:** `DataLen=0x05 0x00`, `Header=0xCE 0x00`, Data `0x04`. + +**Antwort:** `DataLen=0x08 0x00`, `Header=0xCE 0x00`, `FcSettings | Rate | StartDDDhhhhh | StartMMMMMM` (je 8 bit). +- **Rate**: 0–63 (0 = kann nicht laufen; 1 = Echtzeit; 2 = doppelt; usw.). +- **StartDDDhhhhh**: Default-Startzeit Wochentag (3 Bits, 0=Montag…6=Sonntag) und Stunde (5 Bits, 0–23). +- **StartMMMMMM**: Default-Startzeit Minute (6 Bits, 0–59). + +```c +#define fcFastClockLocoNetEn 0x01 // Ausgabe am LocoNet (polled) aktivieren +#define fcFastClockXBUSEn 0x02 // Broadcast am XBUS aktivieren +// 0x04 // reserved +#define fcFastClockDCCEn 0x08 // DCC Broadcast am Gleis aktivieren +#define fcFastClockMRclockEn 0x10 // Multicast an MRclock clients aktivieren +// 0x20 // reserved +#define fcFastClockEmergenyHaltEn 0x40 // Modellzeit beim Nothalt autom. anhalten +#define fcFastClockEnabled 0x80 // Fastclock ist aktiviert +``` + +`fcFastClockEmergenyHaltEn` pausiert die Modellzeit bei Nothalt/Kurzschluss. `fcFastClockEnabled` ist das Enable-Flag (wird auch indirekt über `LAN_FAST_CLOCK_CONTROL` durch Start/Stop geändert). **Werkseinstellung:** FcSettings=0x4F, Rate=1, StartDDDhhhhh=0, StartMMMMMM=0. + +### 12.4 LAN_FAST_CLOCK_SETTINGS_SET + +Überschreiben der persistenten Einstellungen (Parameter je 8 bit), `Header=0xCF 0x00`: + +| DataLen | Data | Wirkung | +|---|---|---| +| 0x05 0x00 | FcSettings | nur FcSettings | +| 0x06 0x00 | FcSettings, Rate | FcSettings + Rate | +| 0x08 0x00 | FcSettings, Rate, StartDDDhhhhh, StartMMMMMM | FcSettings + Rate + Default-Startzeit | + +Feldbeschreibung siehe [12.3](#123-lan_fast_clock_settings_get). **Antwort:** keine. + +--- + +## Anhang A – Befehlsübersicht + +### Client an Z21 + +Diese Meldungen können von einem Client an eine Z21 oder an ein zLink-Gerät gesendet werden. Spalten: **Z21/Z21 XL**, **z21/z21start**, **Booster (10806/10807/10869)**, **Decoder (10836/10837)**. + +| Header / X-Hdr / DB0 | Name | Z21/XL | z21/start | Booster | Decoder | +|---|---|---|---|---|---| +| 0x10 | LAN_GET_SERIAL_NUMBER | ✓ | ✓ | ✓ | ✓ | +| 0x18 | LAN_GET_CODE | ✓ | ✓ | | | +| 0x1A | LAN_GET_HWINFO | ✓ | ✓ | ✓ | ✓ | +| 0x30 | LAN_LOGOFF | ✓ | ✓ | ✓ | ✓ | +| 0x40 / 0x21 / 0x21 | LAN_X_GET_VERSION | ✓ | ✓ | ✓ | ✓ | +| 0x40 / 0x21 / 0x24 | LAN_X_GET_STATUS | ✓ | ✓ | ✓ | ✓ | +| 0x40 / 0x21 / 0x80 | LAN_X_SET_TRACK_POWER_OFF | ✓ | ✓ | ✓ | ✓ | +| 0x40 / 0x21 / 0x81 | LAN_X_SET_TRACK_POWER_ON | ✓ | ✓ | ✓ | ✓ (4) | +| 0x40 / 0x22 / 0x11 | LAN_X_DCC_READ_REGISTER | ✓ | ✓ | | | +| 0x40 / 0x23 / 0x11 | LAN_X_CV_READ | ✓ | ✓ | | ✓ | +| 0x40 / 0x23 / 0x12 | LAN_X_DCC_WRITE_REGISTER | ✓ | ✓ | | | +| 0x40 / 0x24 / 0x12 | LAN_X_CV_WRITE | ✓ | ✓ | | ✓ | +| 0x40 / 0x24 / 0xFF | LAN_X_MM_WRITE_BYTE | ✓ | ✓ | | | +| 0x40 / 0x43 | LAN_X_GET_TURNOUT_INFO | ✓ | ✓ | | ✓ | +| 0x40 / 0x44 | LAN_X_GET_EXT_ACCESSORY_INFO | ✓ | ✓ | | ✓ (3) | +| 0x40 / 0x53 | LAN_X_SET_TURNOUT | ✓ | ✓ (1) | | ✓ | +| 0x40 / 0x54 | LAN_X_SET_EXT_ACCESSORY | ✓ | ✓ (1) | | ✓ | +| 0x40 / 0x80 | LAN_X_SET_STOP | ✓ | ✓ | | ✓ (5) | +| 0x40 / 0x92 | LAN_X_SET_LOCO_E_STOP | ✓ | ✓ | | | +| 0x40 / 0xE3 / 0x44 | LAN_X_PURGE_LOCO | ✓ | ✓ | | | +| 0x40 / 0xE3 / 0xF0 | LAN_X_GET_LOCO_INFO | ✓ | ✓ | | | +| 0x40 / 0xE4 / 0x1s | LAN_X_SET_LOCO_DRIVE | ✓ | ✓ (1) | | | +| 0x40 / 0xE4 / 0xF8 | LAN_X_SET_LOCO_FUNCTION | ✓ | ✓ (1) | | | +| 0x40 / 0xE4 / Group | LAN_X_SET_LOCO_FUNCTION_GROUP | ✓ | ✓ (1) | | | +| 0x40 / 0xE5 / 0x5F | LAN_X_SET_LOCO_BINARY_STATE | ✓ | ✓ | | | +| 0x40 / 0xE6 / 0x30 (0xEC) | LAN_X_CV_POM_WRITE_BYTE | ✓ | ✓ | | ✓ | +| 0x40 / 0xE6 / 0x30 (0xE8) | LAN_X_CV_POM_WRITE_BIT | ✓ | ✓ | | | +| 0x40 / 0xE6 / 0x30 (0xE4) | LAN_X_CV_POM_READ_BYTE | ✓ | ✓ | | ✓ | +| 0x40 / 0xE6 / 0x31 (0xEC) | LAN_X_CV_POM_ACCESSORY_WRITE_BYTE | ✓ | ✓ | | ✓ | +| 0x40 / 0xE6 / 0x31 (0xE8) | LAN_X_CV_POM_ACCESSORY_WRITE_BIT | ✓ | ✓ | | | +| 0x40 / 0xE6 / 0x31 (0xE4) | LAN_X_CV_POM_ACCESSORY_READ_BYTE | ✓ | ✓ | | ✓ | +| 0x40 / 0xF1 / 0x0A | LAN_X_GET_FIRMWARE_VERSION | ✓ | ✓ | ✓ | ✓ | +| 0x50 | LAN_SET_BROADCASTFLAGS | ✓ | ✓ | ✓ | ✓ | +| 0x51 | LAN_GET_BROADCASTFLAGS | ✓ | ✓ | ✓ | ✓ | +| 0x60 | LAN_GET_LOCOMODE | ✓ | ✓ | | | +| 0x61 | LAN_SET_LOCOMODE | ✓ | ✓ | | | +| 0x70 | LAN_GET_TURNOUTMODE | ✓ | ✓ | | | +| 0x71 | LAN_SET_TURNOUTMODE | ✓ | ✓ | | | +| 0x81 | LAN_RMBUS_GETDATA | ✓ | ✓ | | | +| 0x82 | LAN_RMBUS_PROGRAMMODULE | ✓ | ✓ | | | +| 0x85 | LAN_SYSTEMSTATE_GETDATA | ✓ | ✓ | | | +| 0x89 | LAN_RAILCOM_GETDATA | ✓ | ✓ | ✓ | | +| 0xA2 | LAN_LOCONET_FROM_LAN | ✓ | ✓ (1)(2) | | | +| 0xA3 | LAN_LOCONET_DISPATCH_ADDR | ✓ | | | | +| 0xA4 | LAN_LOCONET_DETECTOR | ✓ | ✓ (2) | | | +| 0xC4 | LAN_CAN_DETECTOR | ✓ | | | | +| 0xC8 | LAN_CAN_DEVICE_GET_DESCRIPTION | ✓ | | | | +| 0xC9 | LAN_CAN_DEVICE_SET_DESCRIPTION | ✓ | | | | +| 0xCB | LAN_CAN_BOOSTER_SET_TRACKPOWER | ✓ | | | | +| 0xCC | LAN_FAST_CLOCK_CONTROL | ✓ | ✓ | | | +| 0xCE | LAN_FAST_CLOCK_SETTINGS_GET | ✓ | ✓ | | | +| 0xCF | LAN_FAST_CLOCK_SETTINGS_SET | ✓ | ✓ | | | +| 0xB2 | LAN_BOOSTER_SET_POWER | | | ✓ | | +| 0xB8 | LAN_BOOSTER_GET_DESCRIPTION | | | ✓ | | +| 0xB9 | LAN_BOOSTER_SET_DESCRIPTION | | | ✓ | | +| 0xBB | LAN_BOOSTER_SYSTEMSTATE_GETDATA | | | ✓ | | +| 0xD8 | LAN_DECODER_GET_DESCRIPTION | | | | ✓ | +| 0xD9 | LAN_DECODER_SET_DESCRIPTION | | | | ✓ | +| 0xDB | LAN_DECODER_SYSTEMSTATE_GETDATA | | | | ✓ | +| 0xE8 / 0x06 | LAN_ZLINK_GET_HWINFO | | | ✓ (6) | ✓ (6) | + +**Fußnoten:** +1. z21start: nur mit Freischaltcode (Artikelnummer 10814 oder 10818). +2. z21, z21start: virtueller LocoNet-Stack (z.B. bei GBM16XN mit XPN-Interface). +3. ab Decoder FW V1.11. +4. Decoder: Signallampen wieder einschalten (nur 10837). +5. Decoder: zeigt Haltebegriff, wenn in CV38 das zweite Bit (0x02) gesetzt ist (nur 10837). +6. Wird vom 10838 Z21 pro LINK beantwortet, nicht vom Endgerät (Booster oder Decoder). + +### Z21 an Client + +Diese Meldungen können von einer Z21 oder einem zLink-Gerät an einen Client gesendet werden. + +| Header / X-Hdr / DB0 | Name | Z21/XL | z21/start | Booster | Decoder | +|---|---|---|---|---|---| +| 0x10 | Antwort auf LAN_GET_SERIAL_NUMBER | ✓ | ✓ | ✓ | ✓ | +| 0x18 | Antwort auf LAN_GET_CODE | ✓ | ✓ | | | +| 0x1A | Antwort auf LAN_GET_HWINFO | ✓ | ✓ | ✓ | ✓ | +| 0x40 / 0x43 | LAN_X_TURNOUT_INFO | ✓ | ✓ (1) | | ✓ | +| 0x40 / 0x44 | LAN_X_EXT_ACCESSORY_INFO | ✓ | ✓ (1) | | ✓ (3) | +| 0x40 / 0x61 / 0x00 | LAN_X_BC_TRACK_POWER_OFF | ✓ | ✓ | ✓ | | +| 0x40 / 0x61 / 0x01 | LAN_X_BC_TRACK_POWER_ON | ✓ | ✓ | ✓ | | +| 0x40 / 0x61 / 0x02 | LAN_X_BC_PROGRAMMING_MODE | ✓ | ✓ | | | +| 0x40 / 0x61 / 0x08 | LAN_X_BC_TRACK_SHORT_CIRCUIT | ✓ | ✓ | (4) | (4) | +| 0x40 / 0x61 / 0x12 | LAN_X_CV_NACK_SC | ✓ | ✓ | | | +| 0x40 / 0x61 / 0x13 | LAN_X_CV_NACK | ✓ | ✓ | | ✓ | +| 0x40 / 0x61 / 0x82 | LAN_X_UNKNOWN_COMMAND | ✓ | ✓ | ✓ | ✓ | +| 0x40 / 0x62 / 0x22 | LAN_X_STATUS_CHANGED | ✓ | ✓ | ✓ | ✓ | +| 0x40 / 0x63 / 0x21 | Antwort auf LAN_X_GET_VERSION | ✓ | ✓ | ✓ | ✓ | +| 0x40 / 0x64 / 0x14 | LAN_X_CV_RESULT | ✓ | ✓ | | ✓ | +| 0x40 / 0x81 | LAN_X_BC_STOPPED | ✓ | ✓ | | | +| 0x40 / 0xEF | LAN_X_LOCO_INFO | ✓ | ✓ (1) | | | +| 0x40 / 0xF3 / 0x0A | Antwort auf LAN_X_GET_FIRMWARE_VERSION | ✓ | ✓ | ✓ | ✓ | +| 0x51 | Antwort auf LAN_GET_BROADCASTFLAGS | ✓ | ✓ | ✓ | ✓ | +| 0x60 | Antwort auf LAN_GET_LOCOMODE | ✓ | ✓ | | | +| 0x70 | Antwort auf LAN_GET_TURNOUTMODE | ✓ | ✓ | | | +| 0x80 | LAN_RMBUS_DATACHANGED | ✓ | ✓ | | | +| 0x84 | LAN_SYSTEMSTATE_DATACHANGED | ✓ | ✓ | | | +| 0x88 | LAN_RAILCOM_DATACHANGED | ✓ | ✓ | ✓ | | +| 0xA0 | LAN_LOCONET_Z21_RX | ✓ | | | | +| 0xA1 | LAN_LOCONET_Z21_TX | ✓ | ✓ (2) | | | +| 0xA2 | LAN_LOCONET_FROM_LAN | ✓ | ✓ (2) | | | +| 0xA3 | LAN_LOCONET_DISPATCH_ADDR | ✓ | | | | +| 0xA4 | LAN_LOCONET_DETECTOR | ✓ | ✓ (2) | | | +| 0xC4 | LAN_CAN_DETECTOR | ✓ | | | | +| 0xC8 | Antwort LAN_CAN_DEVICE_GET_DESCRIPTION | ✓ | | | | +| 0xCA | LAN_CAN_BOOSTER_SYSTEMSTATE_CHGD | ✓ | | | | +| 0xCD | LAN_FAST_CLOCK_DATA | ✓ | ✓ | | | +| 0xCE | LAN_FAST_CLOCK_SETTINGS_GET | ✓ | ✓ | | | +| 0xB8 | Antwort auf LAN_BOOSTER_GET_DESCRIPTION | | | ✓ | | +| 0xBA | LAN_BOOSTER_SYSTEMSTATE_DATACHANGED | | | ✓ | | +| 0xD8 | Antwort auf LAN_DECODER_GET_DESCRIPTION | | | | ✓ | +| 0xDA | LAN_DECODER_SYSTEMSTATE_DATACHANGED | | | | ✓ | +| 0xE8 / 0x06 | Antwort auf LAN_ZLINK_GET_HWINFO | | | ✓ (5) | ✓ (5) | + +**Fußnoten:** +1. z21start: vollfunktionsfähig nur mit Freischaltcode (Artikelnummer 10814 oder 10818). +2. z21, z21start: virtueller LocoNet-Stack (z.B. bei GBM16XN mit XPN-Interface). +3. ab Decoder FW V1.11. +4. Kurzschluss wird im entsprechenden Booster-/Decoder-SystemState gemeldet. +5. Wird vom 10838 Z21 pro LINK beantwortet, nicht vom Endgerät (Booster oder Decoder). + +--- + +## Abbildungs- und Tabellenverzeichnis + +**Abbildungen:** 1 Sequenz Kommunikation · 2 Sequenz Lok-Steuerung · 3 DCC Sniff bei Q=0 · 4 DCC Sniff bei Q=1 · 5 Sequenz Weiche schalten · 6 Sequenz CV Lesen · 7 Sequenz Rückmeldemodul programmieren · 8 Sequenz Ethernet/LocoNet Gateway · 9 Sequenz LocoNet Dispatch per LAN-Client. + +**Tabellen:** 1 Meldungen vom Client an Z21 · 2 Meldungen von Z21 an Clients. + +--- + +*Konvertiert aus „Z21 LAN Protokoll Spezifikation", Dokumentenversion 1.13 (06.11.2023), Herausgeber Modelleisenbahn GmbH. Diagramm-Abbildungen des Originals sind hier nicht enthalten und im Text als „(Abbildung …)" referenziert.* diff --git a/src/api/z21-lan-protokoll.pdf b/src/api/z21-lan-protokoll.pdf new file mode 100644 index 0000000..235cd09 Binary files /dev/null and b/src/api/z21-lan-protokoll.pdf differ diff --git a/src/docfx.json b/src/docfx.json deleted file mode 100644 index 7d5100d..0000000 --- a/src/docfx.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", - "metadata": [ - { - "src": [ - { - "src": "../src", - "files": [ - "Z21.Client/Z21.Client.csproj", - "Z21.DependencyInjection/Z21.DependencyInjection.csproj" - ] - } - ], - "dest": "api" - } - ], - "globalMetadata": { - "_appTitle": "Z21 API Documentation", - "_enableSearch": true - }, - "build": { - "content": [ - { - "files": [ - "index.md", - "api/**.yml", - "toc.yml" - ] - }, - { - "files": [ - "articles/**.md" - ] - } - ], - "dest": "_site" - } -} \ No newline at end of file diff --git a/src/toc.yml b/src/toc.yml deleted file mode 100644 index 38eceae..0000000 --- a/src/toc.yml +++ /dev/null @@ -1,8 +0,0 @@ -- name: Home - href: index.md - -- name: API Reference - href: api/toc.yml - -- name: Articles - href: articles/ diff --git a/version.json b/version.json new file mode 100644 index 0000000..a59cefc --- /dev/null +++ b/version.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", + "version": "7.0", + "publicReleaseRefs": [ + "^refs/heads/main$" + ] +}