From a6d43eb3c56d8c94b12b0d983ced4af199d8ac1b Mon Sep 17 00:00:00 2001 From: Dietmar Borgards <2646931+dborgards@users.noreply.github.com> Date: Wed, 9 Sep 2026 15:05:03 +0200 Subject: [PATCH] feat: drop HAWE (moving to a separate internal repo), publish the rest of L3/L4 CanKit.Pro.Hawe -- the generic extension framework for a customer's confidential private protocol -- is moving out of this public repository into its own internal one. Removed entirely: the project, its tests, solution/project-reference entries, and every mention across README and docs (architecture, SRS requirements incl. the FR-HAWE-* section, assumption A-6, constraint CON-006 -- IDs renumbered where a gap would otherwise appear, migration and release-process notes). Nothing here ever held more than a generic SPI extension point, but even that is confidential enough not to belong in a public source history. With HAWE out of the way, the remaining five L3/L4 protocol packages (IsoTp, J1939Tp, CANopen, J1939, Uds) now publish too: drop each project's IsPackable=false, track them in PublicApiSurfaceTests, and check in their initial API-approval baselines. All nine CanKit.Pro.* packages now ship from this repo; none are held back. Also fixed while touching these docs: SRS CON-005 claimed this project uses Apache-2.0 licensing -- it's MIT (docs/licensing.md already explains why, correctly, elsewhere). Verified: full build + 403/403 tests green on net10.0, `dotnet pack` produces exactly the 9 expected packages, no remaining "Hawe" mentions anywhere in the repo outside two explanatory notes (migration-from- legacy.md, release-process.md) about why it's gone. --- CanKit.Pro.sln | 7 - README.md | 11 +- docs/architecture/arc42-CanKit.Pro.md | 15 +- docs/migration-from-legacy.md | 18 +- docs/release-process.md | 35 +- docs/requirements/SRS-CanKit.Pro.md | 35 +- .../CanKit.Pro.CANopen.csproj | 6 - src/CanKit.Pro.Hawe/CanKit.Pro.Hawe.csproj | 21 - src/CanKit.Pro.Hawe/HaweChannel.cs | 341 -------- src/CanKit.Pro.Hawe/HaweChannelOptions.cs | 44 - src/CanKit.Pro.Hawe/HaweCodecRegistry.cs | 81 -- src/CanKit.Pro.Hawe/HaweFramePattern.cs | 76 -- src/CanKit.Pro.Hawe/HaweSessionState.cs | 34 - src/CanKit.Pro.Hawe/IHaweChannel.cs | 34 - src/CanKit.Pro.Hawe/IHaweCodec.cs | 87 -- src/CanKit.Pro.Hawe/IHaweCodecHost.cs | 93 --- src/CanKit.Pro.Hawe/IHaweCodecRegistry.cs | 63 -- src/CanKit.Pro.Hawe/README.md | 97 --- src/CanKit.Pro.IsoTp/CanKit.Pro.IsoTp.csproj | 6 - src/CanKit.Pro.J1939/CanKit.Pro.J1939.csproj | 6 - .../CanKit.Pro.J1939Tp.csproj | 6 - src/CanKit.Pro.Uds/CanKit.Pro.Uds.csproj | 6 - .../CanKit.Pro.CANopen.approved.txt | 217 +++++ .../CanKit.Pro.IsoTp.approved.txt | 135 +++ .../CanKit.Pro.J1939.approved.txt | 84 ++ .../CanKit.Pro.J1939Tp.approved.txt | 53 ++ .../ApiApprovals/CanKit.Pro.Uds.approved.txt | 108 +++ .../CanKit.Pro.Tests/CanKit.Pro.Tests.csproj | 1 - .../TestCases/HaweFrameworkTests.cs | 785 ------------------ .../TestCases/PublicApiSurfaceTests.cs | 5 + 30 files changed, 652 insertions(+), 1858 deletions(-) delete mode 100644 src/CanKit.Pro.Hawe/CanKit.Pro.Hawe.csproj delete mode 100644 src/CanKit.Pro.Hawe/HaweChannel.cs delete mode 100644 src/CanKit.Pro.Hawe/HaweChannelOptions.cs delete mode 100644 src/CanKit.Pro.Hawe/HaweCodecRegistry.cs delete mode 100644 src/CanKit.Pro.Hawe/HaweFramePattern.cs delete mode 100644 src/CanKit.Pro.Hawe/HaweSessionState.cs delete mode 100644 src/CanKit.Pro.Hawe/IHaweChannel.cs delete mode 100644 src/CanKit.Pro.Hawe/IHaweCodec.cs delete mode 100644 src/CanKit.Pro.Hawe/IHaweCodecHost.cs delete mode 100644 src/CanKit.Pro.Hawe/IHaweCodecRegistry.cs delete mode 100644 src/CanKit.Pro.Hawe/README.md create mode 100644 tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.CANopen.approved.txt create mode 100644 tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.IsoTp.approved.txt create mode 100644 tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939.approved.txt create mode 100644 tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939Tp.approved.txt create mode 100644 tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.Uds.approved.txt delete mode 100644 tests/CanKit.Pro.Tests/TestCases/HaweFrameworkTests.cs diff --git a/CanKit.Pro.sln b/CanKit.Pro.sln index d850003..a706893 100644 --- a/CanKit.Pro.sln +++ b/CanKit.Pro.sln @@ -38,8 +38,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanKit.Pro.Addressing", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanKit.Pro.CANopen", "src\CanKit.Pro.CANopen\CanKit.Pro.CANopen.csproj", "{40B6A67F-A5F7-5F9B-BE82-D6573563021F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanKit.Pro.Hawe", "src\CanKit.Pro.Hawe\CanKit.Pro.Hawe.csproj", "{B8C4D4AA-1C87-50EA-ADB6-6D511D416E1E}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanKit.Pro.IsoTp", "src\CanKit.Pro.IsoTp\CanKit.Pro.IsoTp.csproj", "{24D148F4-45F3-5E3E-83E6-05FD963051FB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CanKit.Pro.J1939", "src\CanKit.Pro.J1939\CanKit.Pro.J1939.csproj", "{2CFA3474-2C5F-56A0-963E-7A16D9FF21A5}" @@ -84,10 +82,6 @@ Global {40B6A67F-A5F7-5F9B-BE82-D6573563021F}.Debug|Any CPU.Build.0 = Debug|Any CPU {40B6A67F-A5F7-5F9B-BE82-D6573563021F}.Release|Any CPU.ActiveCfg = Release|Any CPU {40B6A67F-A5F7-5F9B-BE82-D6573563021F}.Release|Any CPU.Build.0 = Release|Any CPU - {B8C4D4AA-1C87-50EA-ADB6-6D511D416E1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B8C4D4AA-1C87-50EA-ADB6-6D511D416E1E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B8C4D4AA-1C87-50EA-ADB6-6D511D416E1E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B8C4D4AA-1C87-50EA-ADB6-6D511D416E1E}.Release|Any CPU.Build.0 = Release|Any CPU {24D148F4-45F3-5E3E-83E6-05FD963051FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {24D148F4-45F3-5E3E-83E6-05FD963051FB}.Debug|Any CPU.Build.0 = Debug|Any CPU {24D148F4-45F3-5E3E-83E6-05FD963051FB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -148,7 +142,6 @@ Global {EE488B24-01E3-5C1E-B2AF-BB0A9255C5F0} = {5368F703-B5EE-5524-8E24-C0CAA76683E4} {521A33A4-20B4-5353-ABCF-8C7F092AE769} = {5368F703-B5EE-5524-8E24-C0CAA76683E4} {40B6A67F-A5F7-5F9B-BE82-D6573563021F} = {5368F703-B5EE-5524-8E24-C0CAA76683E4} - {B8C4D4AA-1C87-50EA-ADB6-6D511D416E1E} = {5368F703-B5EE-5524-8E24-C0CAA76683E4} {24D148F4-45F3-5E3E-83E6-05FD963051FB} = {5368F703-B5EE-5524-8E24-C0CAA76683E4} {2CFA3474-2C5F-56A0-963E-7A16D9FF21A5} = {5368F703-B5EE-5524-8E24-C0CAA76683E4} {0281D090-907B-5820-8CB7-CDE83B209501} = {5368F703-B5EE-5524-8E24-C0CAA76683E4} diff --git a/README.md b/README.md index b6ea9f4..a89afa1 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ needs and that people otherwise rebuild, slightly differently and slightly wrong ## Packages -**L2 — the raw-CAN service layer.** Published to nuget.org, versioned and released together. +Published to nuget.org, versioned and released together. + +**L2 — the raw-CAN service layer.** | Package | What it gives you | Depends on | | --- | --- | --- | @@ -30,9 +32,7 @@ needs and that people otherwise rebuild, slightly differently and slightly wrong | [`CanKit.Pro.Addressing`](src/CanKit.Pro.Addressing) | Validated 11/29-bit CAN IDs, J1939 PGN/priority/PDU/source-address composition, J1939 NAME and PGN catalogues. | — | | [`CanKit.Pro.Reliability`](src/CanKit.Pro.Reliability) | Deadlines whose expiry is guaranteed to be checked, and a `BusStateMonitor` that pushes `ErrWarning`/`ErrPassive`/`BusOff` transitions and recovery. | `CanKit.Abstractions`, `CanKit.Pro.Actor` | -**L3/L4 — transports and application protocols.** Built and tested on every CI run, **not yet -published**: the APIs are still settling, and shipping them now would freeze decisions that -should stay open. Reference the projects from a clone until they are ready. +**L3/L4 — transports and application protocols.** | Package | What it gives you | | --- | --- | @@ -41,9 +41,8 @@ should stay open. Reference the projects from a clone until they are ready. | [`CanKit.Pro.CANopen`](src/CanKit.Pro.CANopen) | CiA 301: SDO client/server incl. block transfer, static and dynamic PDO mapping, NMT, heartbeat and node guarding, EMCY, object dictionary. | | [`CanKit.Pro.J1939`](src/CanKit.Pro.J1939) | J1939 node: address claim with arbitrary-address fallback, fixed-rate periodic send, SPN catalogue over J1939-71. | | [`CanKit.Pro.Uds`](src/CanKit.Pro.Uds) | ISO 14229-1 client over ISO-TP: session control, security access, read/write by identifier, routine control, upload/download, P2/P2\* timing and 0x78 response-pending. | -| [`CanKit.Pro.Hawe`](src/CanKit.Pro.Hawe) | Framework for the HAWE private protocol: codec registry and typed message dispatch. | -Everything targets `netstandard2.0` and `net8.0`. +Everything targets `netstandard2.0` and `net10.0`. ## Install diff --git a/docs/architecture/arc42-CanKit.Pro.md b/docs/architecture/arc42-CanKit.Pro.md index abd2f88..cf36184 100644 --- a/docs/architecture/arc42-CanKit.Pro.md +++ b/docs/architecture/arc42-CanKit.Pro.md @@ -33,7 +33,7 @@ > voraussetzen, nicht durchsetzen (siehe Abschnitt 11). > > Requirement-IDs verweisen auf das SRS (`docs/requirements/SRS-CanKit.Pro.md`); Schema: -> `FR-RAW-*`, `FR-TP-*`, `FR-UDS-*`, `FR-CO-*`, `FR-J1939-*`, `FR-HAWE-*`, `NFR-*`, `CON-*`. Die +> `FR-RAW-*`, `FR-TP-*`, `FR-UDS-*`, `FR-CO-*`, `FR-J1939-*`, `NFR-*`, `CON-*`. Die > technischen Schulden in Abschnitt 11 sind aus dem Deep-Code-Review > (`docs/reviews/2026-07-14-deep-code-review.md`) abgeleitet; dessen Abschnittsnummern werden als > „Review §x.y" referenziert. @@ -50,7 +50,7 @@ Diese Nomenklatur ist identisch zur SRS und wird im gesamten Dokument verwendet: | **L1** | Raw-CAN-Kern | vorhanden | `ICanBus`, `CanFrame`, Registry, Utilities, Diagnostics | | **L2** | Raw-CAN-Dienstebene | **NEU / Ziel** | Multi-Consumer-Demux, Ownership-Vertrag, TX-Confirm, Adressierung, Aktor-Modell, Fehler-/Timeout-Infrastruktur | | **L3** | Transport-Ebene | Prototyp (ISO-TP) / Ziel (J1939-TP) | ISO-TP (ISO 15765-2), J1939-TP (BAM/CM) | -| **L4** | Anwendungsprotokoll-Ebene | **NEU / Ziel** | UDS, CANopen, J1939-App, HAWE-Privatprotokoll | +| **L4** | Anwendungsprotokoll-Ebene | **NEU / Ziel** | UDS, CANopen, J1939-App | ### Auflösung der Requirement-Referenzen (arc42 ↔ SRS) @@ -85,7 +85,7 @@ CanKit ist eine **herstellerneutrale .NET-Bibliothek für den CAN-Bus** (Classic und CAN FD). Sie kapselt heterogene Vendor-Treiber (SocketCAN, ZLG, PCAN, Kvaser, Vector, ControlCAN) hinter einer einheitlichen API (`ICanBus`) und stellt darüber hinaus eine Erweiterungsplattform (SPI + Registry) bereit, auf der Transport- und -Anwendungsprotokolle (ISO-TP, UDS, J1939, CANopen, HAWE) aufsetzen können. +Anwendungsprotokolle (ISO-TP, UDS, J1939, CANopen) aufsetzen können. Kernanliegen: @@ -119,7 +119,7 @@ Zero-Copy) erhöhen den Aufwand für Q4 und für einen sicheren **Frame-Ownershi |-------|------------------------------| | **Anwendungsentwickler** (Diagnose/Steuerung) | Stabile, hardwareunabhängige API; einfache Endpoint-Öffnung; async-Streaming. | | **Adapter-Autor** (Vendor-Integration) | Klares Adapter-Muster (Bus/Transceiver/Options/Native), SPI-Verträge, Fake-Schicht als Vorlage. | -| **Protokoll-Autor** (ISO-TP/UDS/J1939/CANopen/HAWE) | Verbindlicher Ownership-Vertrag, Multi-Consumer-Demux, TX-Confirm, definiertes Threading-Modell (→ L2). | +| **Protokoll-Autor** (ISO-TP/UDS/J1939/CANopen) | Verbindlicher Ownership-Vertrag, Multi-Consumer-Demux, TX-Confirm, definiertes Threading-Modell (→ L2). | | **Maintainer / Architekt** | Geringe Kopplung, testbare Kerne, dokumentierte Entscheidungen (ADRs), Kontrolle über Release-Reife. | | **CI/Release-Engineer** | Adapterweise Pfadfilter, `-c Fake`-Builds, NuGet-Packaging, reproduzierbare Matrix. | | **Endnutzer/Betreiber** | Zuverlässiges Verhalten unter Last, kein Ressourcenleck über lange Laufzeiten. | @@ -181,7 +181,7 @@ flowchart TB subgraph App["Anwendungsprozess (.NET)"] UserApp["Anwendungscode
Diagnose / Steuerung / Telemetrie"] subgraph CanKitLib["CanKit (Bibliothek, in-process)"] - L4["L4 Anwendungsprotokolle
UDS / CANopen / J1939 / HAWE (NEU)"] + L4["L4 Anwendungsprotokolle
UDS / CANopen / J1939 (NEU)"] L3["L3 Transport
ISO-TP (Prototyp) / J1939-TP (Ziel)"] L2["L2 Raw-CAN-Dienste
Demux / Ownership / TX-Confirm (NEU)"] L1["L1 Raw-CAN-Kern
ICanBus / CanFrame / Registry"] @@ -254,7 +254,6 @@ flowchart TB UDS["UDS (ISO 14229)"] CANopen["CANopen (SDO/PDO/NMT/EMCY)"] J1939App["J1939 (Applikation)"] - HAWE["HAWE-Privatprotokoll"] end subgraph L3["L3 Transport"] @@ -293,7 +292,6 @@ flowchart TB UDS --> IsoTp CANopen --> Demux J1939App --> J1939Tp - HAWE --> Demux IsoTp --> Demux J1939Tp --> Demux Demux --> ICanBus @@ -322,7 +320,7 @@ flowchart TB | L1 Raw-CAN-Kern | vorhanden | Herstellerneutraler Frame-Zugriff, Discovery, Utilities, Diagnostics. | `ICanBus`, `CanBus.Open`, `CanRegistry` | `FR-RAW-*` | | L2 Raw-CAN-Dienste | NEU | Ein RX-Strom → N unabhängige gefilterte Consumer; Ownership-Vertrag; TX-Confirm; Aktor-Modell. | (neu) `ICanBusService` / `ISubscription` | `FR-RAW-DEMUX-*`, `FR-RAW-OWN-*`, `FR-RAW-TXC-*` | | L3 Transport | Prototyp/Ziel | Segmentierung/Reassemblierung (ISO-TP), Sessions (J1939-TP). | `IIsoTpChannel`, `IIsoTpScheduler` | `FR-TP-*` | -| L4 Anwendungsprotokolle | NEU | Diagnose-/Applikationssemantik auf L3/L2. | (neu) protokollspezifisch | `FR-UDS-*`, `FR-CO-*`, `FR-J1939-*`, `FR-HAWE-*` | +| L4 Anwendungsprotokolle | NEU | Diagnose-/Applikationssemantik auf L3/L2. | (neu) protokollspezifisch | `FR-UDS-*`, `FR-CO-*`, `FR-J1939-*` | ## 5.2 Ebene 2 – Zoom L1 (Raw-CAN-Kern, vorhanden) @@ -1308,7 +1306,6 @@ Ziel-Architektur und Voraussetzung für belastbare L3/L4-Stacks. | **CANopen** | Höheres Protokoll (CiA 301) auf CAN. | | **SDO / PDO** | Service / Process Data Object (CANopen). | | **NMT / EMCY** | Network Management / Emergency Object (CANopen). | -| **HAWE** | Hier: herstellerspezifisches Privatprotokoll (Ziel-L4). | | **SPI (hier)** | Service Provider Interface; interne Erweiterungspunkte (`CanKit.Abstractions.SPI.*`), nicht der Hardware-SPI-Bus. | | **Fake-Native** | `*.Fake.cs`-Spiegel der P/Invoke-Schicht für hardwarelose Builds (`-c Fake`). | | **Virtual-Hub** | In-Memory-Loopback-Adapter (`VirtualBusHub`) für Tests. | diff --git a/docs/migration-from-legacy.md b/docs/migration-from-legacy.md index c439b30..241aa99 100644 --- a/docs/migration-from-legacy.md +++ b/docs/migration-from-legacy.md @@ -38,7 +38,7 @@ four L2 packages, while the entire transport and application stack is on `develo | --- | --- | | `src/core/CanKit.Pro.{Actor,Addressing,RawCan,Reliability}/` | → `src/CanKit.Pro.*/` — **source unchanged** apart from comments referring to fork-internal files | | `src/transports/CanKit.Pro.{IsoTp,J1939Tp}/` | → `src/CanKit.Pro.*/` | -| `src/protocols/CanKit.Pro.{CANopen,J1939,Uds,Hawe}/` | → `src/CanKit.Pro.*/` | +| `src/protocols/CanKit.Pro.{CANopen,J1939,Uds}/` | → `src/CanKit.Pro.*/` | | The Pro test suites in `tests/CanKit.Tests/TestCases/` | → `tests/CanKit.Pro.Tests/TestCases/`, adapted (below) | | The five Pro quickstart samples | → `samples/CanKit.Pro.Sample.*/` | | `docs/architecture/`, `docs/requirements/`, `docs/reviews/` | → `docs/`, with headers that mark CanKit as external context | @@ -47,12 +47,20 @@ The flat `src/CanKit.Pro./` layout replaces the legacy `core/transports/pr a repository whose every project is a `CanKit.Pro.*` package, the extra directory level encoded a layer that the package name already states. +`CanKit.Pro.Hawe` (the generic extension framework for a customer's confidential private +protocol) moved over with everything else initially, but was later removed from this public +repository entirely and continues as a separate, internal-only repo — its presence here was never +more than the generic SPI extension point, and even that is confidential enough not to belong in +a public source history. + ### Publishing -Only the four L2 packages publish. `CanKit.Pro.{IsoTp,J1939Tp,CANopen,J1939,Uds,Hawe}` keep -`IsPackable=false`, which is the legacy repository's own assessment (`publish: false` in its -`eng/packages.json`) — they are pre-release, and this migration is not the moment to overrule -that. They are still built and tested on every CI run, so they cannot rot silently. +All `CanKit.Pro.*` packages built from this repo publish to nuget.org. The four L2 packages +(`Actor`, `Addressing`, `RawCan`, `Reliability`) were first; `CanKit.Pro.{IsoTp,J1939Tp,CANopen, +J1939,Uds}` initially kept `IsPackable=false` — the legacy repository's own assessment +(`publish: false` in its `eng/packages.json`), since they were pre-release and this migration was +not the moment to overrule that — until their APIs settled enough to ship. They were built and +tested on every CI run throughout, so they could not rot silently while unpublished. ## What did not move diff --git a/docs/release-process.md b/docs/release-process.md index f58523c..e307fd1 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -67,8 +67,10 @@ dotnet gitversion ## Which packages the release publishes -Ten projects build; four of them ship. The split is expressed once, in the project files, and -everything else follows from it: +Nine projects build; all nine ship. The split used to hold four of them back is expressed once, +in the project files, so a project that becomes packable is always a one-line change: drop its +`IsPackable` property, and add it to `PublicApiSurfaceTests.Tracked` so its surface is tracked +from its first release. | Package | Layer | Ships | | --- | --- | --- | @@ -76,23 +78,22 @@ everything else follows from it: | `CanKit.Pro.Addressing` | L2 — CAN-ID addressing and filters | yes | | `CanKit.Pro.RawCan` | L2 — raw-CAN demultiplex and TX confirmation | yes | | `CanKit.Pro.Reliability` | L2 — deadlines, retries, bus-state monitoring | yes | -| `CanKit.Pro.IsoTp` | L3 — ISO 15765-2 | not yet | -| `CanKit.Pro.J1939Tp` | L3 — SAE J1939-21 transport | not yet | -| `CanKit.Pro.CANopen` | L4 — CiA 301 | not yet | -| `CanKit.Pro.J1939` | L4 — J1939 node and address claim | not yet | -| `CanKit.Pro.Uds` | L4 — ISO 14229-1 | not yet | -| `CanKit.Pro.Hawe` | L4 — HAWE private protocol framework | not yet | - -The six L3/L4 projects carry `false`. `dotnet pack CanKit.Pro.sln` -therefore skips them without needing a list of project names anywhere, which means the CI pack job -and the `@semantic-release/exec` `prepareCmd` cannot drift apart — they run the same command over -the same solution. The CI `pack` job prints `ls -l artifacts/nuget`, so a project that silently +| `CanKit.Pro.IsoTp` | L3 — ISO 15765-2 | yes | +| `CanKit.Pro.J1939Tp` | L3 — SAE J1939-21 transport | yes | +| `CanKit.Pro.CANopen` | L4 — CiA 301 | yes | +| `CanKit.Pro.J1939` | L4 — J1939 node and address claim | yes | +| `CanKit.Pro.Uds` | L4 — ISO 14229-1 | yes | + +`dotnet pack CanKit.Pro.sln` packs whichever projects don't set `IsPackable=false`, without +needing a list of project names anywhere, which means the CI pack job and the +`@semantic-release/exec` `prepareCmd` cannot drift apart — they run the same command over the +same solution. The CI `pack` job prints `ls -l artifacts/nuget`, so a project that silently becomes packable shows up as a new file in that listing on the pull request that did it. -They still build and test on every run, so the unshipped layers cannot rot; they are held back -only until their public API has settled. Publishing one is a one-line change: drop its -`IsPackable` property, and add it to `PublicApiSurfaceTests.Tracked` so its surface is tracked -from the first release like the four L2 packages are. +`CanKit.Pro.Hawe`, the generic extension framework for a customer's confidential private +protocol, used to be the tenth project and stayed unpublished for a different reason: not +API-immaturity but confidentiality. It has since moved out of this public repository entirely +into a separate, internal-only one. ## What a release run does diff --git a/docs/requirements/SRS-CanKit.Pro.md b/docs/requirements/SRS-CanKit.Pro.md index 27dd742..4c89996 100644 --- a/docs/requirements/SRS-CanKit.Pro.md +++ b/docs/requirements/SRS-CanKit.Pro.md @@ -39,7 +39,7 @@ Das Dokument spezifiziert: - **L2 Raw-CAN-Dienstebene** (neu zu bauen) – die Härtung und Erweiterung von `ICanBus`/`CanFrame` um Multi-Consumer-Demultiplexing, einen verbindlichen Frame-Ownership-Vertrag, TX-Bestätigung, Adressierungs-Helfer und ein einheitliches Threading-Modell je Protokollinstanz. - **L3 Transport-Ebene** – Fertigstellung von ISO-TP (ISO 15765-2) sowie Neubau von J1939-TP (TP.BAM/TP.CM). -- **L4 Anwendungsprotokoll-Ebene** – UDS (ISO 14229 über ISO-TP), CANopen (CiA 301), J1939-Applikation (SAE J1939/ISO 11783), sowie ein generischer Rahmen für ein proprietäres HAWE-Privatprotokoll. +- **L4 Anwendungsprotokoll-Ebene** – UDS (ISO 14229 über ISO-TP), CANopen (CiA 301), J1939-Applikation (SAE J1939/ISO 11783). Nicht im Scope: Neuimplementierung von L0 (Vendor-Adapter) und der bereits vorhandenen L1-Kernfunktionalität (`ICanBus`, `CanFrame`, `CanRegistry`) – diese werden als gegeben vorausgesetzt und nur dort referenziert, wo L2 auf ihnen aufsetzt oder bestehende Defekte (siehe Review) die L2-Anforderungen begründen. @@ -82,7 +82,6 @@ Nicht im Scope: Neuimplementierung von L0 (Vendor-Adapter) und der bereits vorha - SAE J1939 / ISO 11783: Serial Control and Communications Vehicle Network (Nutzfahrzeuge/Agrartechnik) - CiA 301: CANopen Application Layer and Communication Profile - CiA 302: CANopen Additional Application Layer Functions (Netzwerkmanagement, Boot-up) -- HAWE-Privatprotokoll: proprietär, vertraulich, extern verwaltet – in dieser SRS nur als generischer Rahmen mit Erweiterungspunkten spezifiziert (siehe Annahme A-6) - `docs/reviews/2026-07-14-deep-code-review.md` – Ist-Zustands-Review, Quelle der L2-Architekturlücken - ISO/IEC/IEEE 29148:2018 – Requirements Engineering - IREB CPRE-Lehrplan (Satzschablonen, MoSCoW) @@ -101,7 +100,7 @@ CanKit wird in fünf Ebenen strukturiert. Diese Nomenklatur ist verbindlich und | L1 | Raw-CAN-Kern | `ICanBus`, `CanFrame`/`CanFrameView`, `ITransceiver`, `ICanDevice`, `IPeriodicTx`, `CanRegistry`, Utilities (`AsyncFramePipe`, `QueuedTxCanBus`, `SoftwarePeriodicTx`, `PreciseDelay`, `BitTimingSolver`) | **vorhanden**, mit bekannten Defekten (siehe Review) | | L2 | Raw-CAN-Dienstebene | Multi-Consumer-Demultiplexing, Frame-Ownership-Vertrag, TX-Bestätigung, Adressierungs-Helfer, Threading-Modell je Protokollinstanz, Fehler-/Timeout-Infrastruktur | **neu zu bauen** – Gegenstand dieser SRS | | L3 | Transport-Ebene | ISO-TP (ISO 15765-2), J1939-TP (TP.BAM/TP.CM) | ISO-TP: **unfertiger, funktional defekter Prototyp** (`CanKit.Transport.IsoTp`); J1939-TP: **nicht vorhanden** | -| L4 | Anwendungsprotokoll-Ebene | UDS (auf ISO-TP), CANopen, J1939 (Applikation), HAWE-Privatprotokoll | **nicht vorhanden** | +| L4 | Anwendungsprotokoll-Ebene | UDS (auf ISO-TP), CANopen, J1939 (Applikation) | **nicht vorhanden** | L2 ist die vom Auftraggeber geforderte zusätzliche „raw-CAN“-Schicht: Sie kapselt alles, was mehrere Protokoll-Stacks gemeinsam benötigen und was heute nicht (oder nicht korrekt) in L1 existiert. @@ -113,7 +112,6 @@ L2 ist die vom Auftraggeber geforderte zusätzliche „raw-CAN“-Schicht: Sie k - UDS-Client-Funktionalität für Diagnosewerkzeuge/Testautomatisierung. - CANopen-Basisdienste (SDO, PDO, NMT, Heartbeat, EMCY) für Automatisierungsanwendungen. - J1939-Applikationsschicht (PGN/SPN-Zugriff, Address Claiming, Request-PGN). -- Erweiterungsrahmen für das HAWE-Privatprotokoll. ### 2.3 Nutzer-/Stakeholder-Charakteristik @@ -132,8 +130,7 @@ Siehe Abschnitt 6 (`CON-xxx`). Zentral: Multi-Targeting (netstandard2.0, net8.0, | A-3 | J1939-TP wird als eigenständiger Transport nach demselben SPI-Muster wie ISO-TP (`IIsoTpRegister`-Analogon) realisiert. | | A-4 | Vendor-SDK-Lizenzen (Peak PCANBasic, Kvaser CANlib, Vector XL-Driver) bleiben proprietär und werden nicht Teil dieser Spezifikation; nur die Integrationspunkte werden betrachtet. | | A-5 | HIL-Testinfrastruktur mit realer Hardware wird für Abnahmetests der L3/L4-Ebenen benötigt, ist aber nicht Gegenstand dieser SRS (siehe Abschnitt 7). | -| A-6 | Das HAWE-Protokoll ist vertraulich und dem Requirements-Team nicht im Detail bekannt; entsprechende Anforderungen (`FR-HAWE-xxx`) sind bewusst generisch als Rahmen mit Erweiterungspunkten formuliert und müssen bei Verfügbarkeit der Protokollspezifikation verfeinert werden. | -| A-7 | `netstandard2.0` bleibt Ziel-TFM für Kernbibliothek und L2/L3, um .NET-Framework-Konsumenten (Windows-Altsysteme mit Kvaser/Vector/PCAN) zu unterstützen. | +| A-6 | `netstandard2.0` bleibt Ziel-TFM für Kernbibliothek und L2/L3, um .NET-Framework-Konsumenten (Windows-Altsysteme mit Kvaser/Vector/PCAN) zu unterstützen. | --- @@ -146,7 +143,6 @@ Siehe Abschnitt 6 (`CON-xxx`). Zentral: Multi-Targeting (netstandard2.0, net8.0, | **Steuergerät (ECU, extern)** | Kommunikationspartner auf dem Bus; kein CanKit-Nutzer, aber Quelle von Antworten/Fehlerbedingungen, gegen die CanKit robust sein muss. | | **CANopen-Master/-Node-Anwendung** | Nutzt L4-CANopen-API für NMT/PDO/SDO-Interaktion. | | **J1939-Anwendung** | Nutzt L4-J1939-API für PGN/SPN-Zugriff und Address Claiming. | -| **HAWE-Integrationsanwendung** | Nutzt den generischen HAWE-Rahmen für ein proprietäres Frame-Protokoll. | | **Bibliotheks-Maintainer** | Verantwortlich für Architektur, Code-Qualität, SPI-Erweiterbarkeit, Release-Prozess. | | **CI/Testautomatisierung** | Führt Unit-, Virtual-Loopback- und (wo verfügbar) HIL-Tests automatisiert aus; Konsument der Fake-Native-Schicht. | @@ -315,18 +311,6 @@ Ist-Zustand: nicht vorhanden, Neubau; setzt FR-TP-030ff. voraus. | FR-J1939-006 | Das System MUSS für PGN-Nachrichten > 8 Byte automatisch das J1939-TP (FR-TP-030ff.) verwenden, für ≤8 Byte den direkten Single-Frame-Pfad. | Must | Integrationstest: PGN mit 20-Byte-Payload nutzt TP.BAM/TP.CM, PGN mit 6-Byte-Payload nutzt Single Frame. | SAE J1939-21 | | FR-J1939-007 | Das System SOLLTE periodisches Senden von PGNs mit spezifikationsgemäßer Standardrate unterstützen (Nutzung von `IPeriodicTx`/L2-Scheduling). | Should | Integrationstest: konfigurierte PGN wird mit korrekter Periodenrate gesendet. | SAE J1939-71; `IPeriodicTx` | -#### 4.3.4 HAWE-Privatprotokoll (generischer Rahmen) - -Ist-Zustand: nicht vorhanden; Protokolldetails vertraulich/extern (Annahme A-6). Anforderungen sind bewusst generisch gehalten und definieren Erweiterungspunkte statt konkreter Serviceinhalte. - -| ID | Anforderung | Priorität | Verifikation | Quelle | -|---|---|---|---|---| -| FR-HAWE-001 | Das System MUSS dem Applikationsentwickler einen Erweiterungspunkt (SPI, analog `IIsoTpRegister`) bieten, über den ein HAWE-spezifisches Frame-Codec-Modul (Kodierung/Dekodierung proprietärer Nachrichtenformate) eingebunden werden kann, ohne den L2/L3-Kern zu verändern. | Must | Architekturtest: Referenz-Dummy-Codec wird über SPI registriert und im Registry-Auto-Discovery gefunden. | Auftrag, Annahme A-6 | -| FR-HAWE-002 | Das System MUSS dem Applikationsentwickler die Möglichkeit bieten, beliebige rohe CAN-Frame-Payloads gemäß einem konfigurierbaren, frame-basierten Muster (ID-Bereich, Payload-Layout) zu senden und zu empfangen, ohne dass CanKit-Kern das konkrete HAWE-Format kennen muss. | Must | Integrationstest: generisches Frame-Muster wird über Virtual-Loopback korrekt gesendet/empfangen. | Auftrag | -| FR-HAWE-003 | Das System SOLLTE dem HAWE-Erweiterungsmodul Zugriff auf die L2-Dienste (Demultiplexing, TX-Confirm, Threading-Modell) auf gleicher Grundlage wie ISO-TP/CANopen/J1939 gewähren. | Should | Architekturreview: HAWE-Referenzmodul nutzt dieselben L2-SPI-Schnittstellen wie ISO-TP. | Konsistenzanforderung | -| FR-HAWE-004 | Das System KANN einen Platzhalter-Zustandsautomaten (Session/Handshake) als Vorlage für die spätere Umsetzung der tatsächlichen HAWE-Protokolllogik bereitstellen, sobald die Spezifikation verfügbar ist. | Could | Nicht verifizierbar vor Vorliegen der HAWE-Spezifikation; Platzhalter-Vorhandensein per Codereview. | Annahme A-6 | -| FR-HAWE-005 | Das System MUSS bei Verfügbarkeit der HAWE-Spezifikation eine Nachführung dieser Anforderungen (Verfeinerung von FR-HAWE-001..004) vorsehen; bis dahin gilt dieser Abschnitt als vorläufig. | Must | Dokumentationsprozess: SRS-Änderungsprotokoll. | Annahme A-6 | - --- ## 5. Nicht-funktionale Anforderungen @@ -356,10 +340,9 @@ Ist-Zustand: nicht vorhanden; Protokolldetails vertraulich/extern (Annahme A-6). | CON-002 | Vendor-Adapter-Integrationen (PCAN, Kvaser, Vector, ControlCAN) basieren auf P/Invoke gegen proprietäre native SDKs/DLLs; L2/L3-Komponenten dürfen diese Abhängigkeiten nicht direkt referenzieren, sondern ausschließlich über `ICanBus`/`ITransceiver`-Abstraktionen nutzen. | technisch | Review §1.1 Punkt 16 (Negativbeispiel: ISO-TP referenziert `Peak.PCANBasic.NET` grundlos) | | CON-003 | Lizenzbedingungen der Vendor-SDKs (Peak PCANBasic, Kvaser CANlib, Vector XL-Driver) sind proprietär; Distribution/Verwendung dieser SDKs unterliegt Drittanbieter-Lizenzen, die außerhalb der Kontrolle dieses Projekts liegen. | rechtlich | `CanKit.Adapter.PCAN.csproj` (`Peak.PCANBasic.NET`-Referenz) | | CON-004 | Neue Pakete (L3/L4) MÜSSEN, solange sie funktional unvollständig sind, klar als experimentell gekennzeichnet oder von der Release-Pipeline ausgeschlossen werden (`IsPackable=false`), analog der Review-Empfehlung für den aktuellen ISO-TP-Stand. | organisatorisch | Review §1.1, Empfehlung Pkt. 1 | -| CON-005 | Das Projekt verwendet Apache-2.0-Lizenzierung (`PackageLicenseExpression` in `Directory.Build.props`); neue L3/L4-Pakete MÜSSEN dieselbe Lizenz führen, sofern keine abweichende vertragliche Regelung (z. B. HAWE-Vertraulichkeit) entgegensteht. | rechtlich/organisatorisch | `src/Directory.Build.props` | -| CON-006 | Das HAWE-Protokoll ist vertraulich; jegliche konkrete Protokolldetails DÜRFEN NICHT in öffentlichen CanKit-Repositories oder NuGet-Paketen offengelegt werden. Nur der generische Rahmen (FR-HAWE-xxx) ist öffentlich. | rechtlich | Auftrag, Annahme A-6 | -| CON-007 | CI-Workflows testen aktuell projektweise über `.slnf`-Filterdateien (z. B. `CanKitAdapters.slnf`, `CanKitTransports.slnf`); neue L3/L4-Pakete MÜSSEN in eine passende `.slnf`-Datei mit eigenem CI-Workflow aufgenommen werden. | organisatorisch | Review §4 („ISO-TP-Projekt hat keinen eigenen Workflow“) | -| CON-008 | Der Standard-Git-Branch ist `master`; Release-/Paket-Pipelines MÜSSEN konsistent auf diesen Branch referenzieren. | organisatorisch | Review §3, §5 Pkt. 9 (`nuget-pipeline.yml` Branch-Trigger-Fehler) | +| CON-005 | Das Projekt verwendet MIT-Lizenzierung (`PackageLicenseExpression` in `Directory.Build.props`); neue L3/L4-Pakete MÜSSEN dieselbe Lizenz führen, sofern keine abweichende vertragliche Regelung entgegensteht. | rechtlich/organisatorisch | `src/Directory.Build.props` | +| CON-006 | CI-Workflows testen aktuell projektweise über `.slnf`-Filterdateien (z. B. `CanKitAdapters.slnf`, `CanKitTransports.slnf`); neue L3/L4-Pakete MÜSSEN in eine passende `.slnf`-Datei mit eigenem CI-Workflow aufgenommen werden. | organisatorisch | Review §4 („ISO-TP-Projekt hat keinen eigenen Workflow“) | +| CON-007 | Der Standard-Git-Branch ist `master`; Release-/Paket-Pipelines MÜSSEN konsistent auf diesen Branch referenzieren. | organisatorisch | Review §3, §5 Pkt. 9 (`nuget-pipeline.yml` Branch-Trigger-Fehler) | --- @@ -396,7 +379,6 @@ Verweise auf Architektur-Bausteine nutzen die in Abschnitt 2.1 definierten Schic | FR-UDS-001..012 | L4 – *UDS-Client* (geplant, neues Paket auf `IIsoTpChannel`) | Virtual-Loopback-Integrationstest, HIL-Stichprobe | | FR-CO-001..012 | L4 – *CANopen-Stack* (geplant, neues Paket auf L2-Demultiplexing + L1 `ICanBus`) | Virtual-Loopback-Integrationstest, HIL-Stichprobe | | FR-J1939-001..007 | L4 – *J1939-Applikationsschicht* (geplant, aufbauend auf L3 J1939-TP) | Virtual-Loopback-Integrationstest, HIL-Stichprobe | -| FR-HAWE-001..005 | L4 – *HAWE-Erweiterungsrahmen* (geplant, SPI-Erweiterungspunkt analog `IIsoTpRegister`, `src/core/CanKit.Abstractions/SPI/Registry/Transports/IIsoTpRegister.cs`) | Architekturreview, Virtual-Loopback-Integrationstest (generischer Referenzcodec) | | NFR-001..003 | L2/L3 Timing-Infrastruktur, L1 `SoftwarePeriodicTx`/`PreciseDelay` (`src/core/CanKit.Core/Utils/SoftwarePeriodicTx.cs`) | Performance-/Timing-Test | | NFR-004, CON-001 | Alle Ebenen – Multi-Targeting (`src/Directory.Build.props`) | CI-Testmatrix | | NFR-005, CON-002, CON-003 | L0 – Vendor-Adapter (`src/adapters/*`) | Buildmatrix, Codereview | @@ -406,7 +388,7 @@ Verweise auf Architektur-Bausteine nutzen die in Abschnitt 2.1 definierten Schic | NFR-009 | Alle Ebenen – Testinfrastruktur, L0 `CanKit.Adapter.Virtual`, Fake-Native-Muster | CI-Lauf | | NFR-010 | L2/L3/L4 – SPI-Registrierungsmuster, L1 `CanRegistryEntryAttribute`/`ICanRegistryEntry` (`src/core/CanKit.Abstractions/Attributes/CanRegistryEntryAttribute.cs`) | Architekturreview | | NFR-011, NFR-012 | Alle neuen Ebenen – API-Namenskonventionen | API-Review | -| CON-004..008 | Release-/CI-Prozess (`eng/`, `.github/workflows`) | Prozessreview | +| CON-004..007 | Release-/CI-Prozess (`eng/`, `.github/workflows`) | Prozessreview | --- @@ -414,5 +396,4 @@ Verweise auf Architektur-Bausteine nutzen die in Abschnitt 2.1 definierten Schic 1. Konkrete Zielwerte für Jitter/Durchsatz (NFR-001) sind projektspezifisch festzulegen (aktuell als Platzhalter markiert) – abhängig von Zielanwendungen (Diagnose vs. Steuerungs-Echtzeit). 2. J1939-TP- und CANopen-Pakete existieren im Repository noch nicht; Anforderungen in Abschnitt 4.2.2/4.3.2/4.3.3 sind Neubau-Spezifikationen ohne Ist-Code-Referenz. -3. HAWE-Anforderungen (Abschnitt 4.3.4) sind bewusst als Rahmen gehalten und müssen bei Vorliegen der vertraulichen Spezifikation verfeinert werden (Annahme A-6). -4. Die Traceability-Matrix referenziert `docs/architecture/arc42-CanKit.Pro.md`, das zum Zeitpunkt dieser SRS noch nicht vorliegt; Bausteinnamen sind als *geplant* markiert und beim Erscheinen des Architekturdokuments gegenzuprüfen. +3. Die Traceability-Matrix referenziert `docs/architecture/arc42-CanKit.Pro.md`, das zum Zeitpunkt dieser SRS noch nicht vorliegt; Bausteinnamen sind als *geplant* markiert und beim Erscheinen des Architekturdokuments gegenzuprüfen. diff --git a/src/CanKit.Pro.CANopen/CanKit.Pro.CANopen.csproj b/src/CanKit.Pro.CANopen/CanKit.Pro.CANopen.csproj index 6a00862..787c08d 100644 --- a/src/CanKit.Pro.CANopen/CanKit.Pro.CANopen.csproj +++ b/src/CanKit.Pro.CANopen/CanKit.Pro.CANopen.csproj @@ -3,12 +3,6 @@ Experimental CANopen (CiA 301) node implementation for CanKit.Pro. Provides an in-process ICanOpenNode with a local Object Dictionary (FR-CO-001), SDO expedited/segmented/block transfers (FR-CO-002/003/004), static TPDO/RPDO mapping with event/timer/SYNC triggers (FR-CO-005/006), an NMT master with heartbeat producer/consumer (FR-CO-007/008), node-guarding consumer/producer (FR-CO-009), SYNC producer/consumer (FR-CO-010) and EMCY encode/decode (FR-CO-011), all composed on the L2 ICanBusService demux (FR-CO-012). $(PackageTags);CANopen;CiA 301 - - false diff --git a/src/CanKit.Pro.Hawe/CanKit.Pro.Hawe.csproj b/src/CanKit.Pro.Hawe/CanKit.Pro.Hawe.csproj deleted file mode 100644 index 1831a78..0000000 --- a/src/CanKit.Pro.Hawe/CanKit.Pro.Hawe.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - Generic HAWE extension framework for CanKit (CanKit.Pro): a public SPI (IHaweCodec, IHaweCodecRegistry, IHaweChannel) and reference host that lets a proprietary, out-of-repository HAWE codec module plug into CanKit's L2 raw-CAN service (subscriptions, TX-confirm, actor loop, deadline scheduler) without disclosing any HAWE protocol detail. This assembly contains only the generic framework surface required by SRS FR-HAWE-001..005; no HAWE service IDs, frame layouts, session logic, or secrets are shipped (SRS CON-006 / A-6). - $(PackageTags);HAWE;framework;SPI - - false - - - - - - - - - - diff --git a/src/CanKit.Pro.Hawe/HaweChannel.cs b/src/CanKit.Pro.Hawe/HaweChannel.cs deleted file mode 100644 index 75bdb52..0000000 --- a/src/CanKit.Pro.Hawe/HaweChannel.cs +++ /dev/null @@ -1,341 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using CanKit.Abstractions.API.Can.Definitions; -using CanKit.Pro.Actor; -using CanKit.Pro.RawCan; -using CanKit.Pro.Reliability; - -namespace CanKit.Pro.Hawe -{ - /// - /// Default implementation: wires one to - /// one using the same L2 building blocks every other L3/L4 stack - /// in CanKit uses -- a filtered demultiplexer subscription (SRS FR-RAW-010..013), a - /// single-mailbox (SRS FR-RAW-020..024) and a - /// (SRS FR-RAW-050). The channel itself is deliberately thin: - /// all HAWE-specific decisions live behind in a private module. - /// - /// - /// - /// The channel does not construct or take ownership of the underlying - /// : several codecs may share one service (each with its own - /// disjoint ), and the caller normally owns the service - /// lifetime already. Disposing the channel therefore only unwinds this channel's own - /// resources: subscription, actor, deadline scheduler, plus one - /// call on the actor loop. - /// - /// - /// Every codec callback (, - /// , , - /// ) runs on the actor's single-writer loop. Frames are - /// pushed onto that loop by the demux subscription (a background task drains the - /// subscription's async enumerable and posts each frame). This preserves the same - /// single-writer guarantee ISO-TP relies on so the codec's internal state needs no locking. - /// - /// - public sealed class HaweChannel : IHaweChannel - { - private readonly IHaweCodec _codec; - private readonly ICanBusService _busService; - private readonly ProtocolActor _actor; - private readonly DeadlineScheduler _deadlines; - private readonly ISubscription _subscription; - private readonly CancellationTokenSource _pumpCts = new(); - private readonly Task _pumpTask; - private readonly Host _host; - - // Non-null only in ActorExecutionMode.SynchronizationContext. Used to detect the classic - // "sync-over-async on the dispatcher thread" deadlock: ProtocolActor marshals with - // SynchronizationContext.Send, so a PostAsync().GetAwaiter().GetResult() call made *from* - // that same context blocks the pump Send needs and hangs forever. See RunOnActorLoop. - private readonly SynchronizationContext? _actorSyncContext; - - // Guarded via Interlocked; the channel's own state, distinct from the codec-driven session - // state, so a Dispose racing with a codec callback is fully deterministic. - private int _disposed; - - // Set by Dispose *before* it starts awaiting the pump: read by PumpAsync and by Post-time - // helpers to stop enqueuing new codec callbacks once teardown is in progress. Ordering rule - // is "no OnFrameReceived after OnDetached"; Dispose enforces that by (1) cancelling the - // pump, (2) awaiting the pump task so no further OnFrameReceived can be posted, and only - // then (3) posting OnDetached. This flag is the belt-and-braces defense against a frame - // slipping through between (1) and (2) -- the pump checks it before each Post. - private int _detachStarted; - - // Codec-driven generic session state (FR-HAWE-004). Kept as an int for lock-free reads - // from any thread via Volatile.Read; writes are serialized on the actor loop by - // Host.SetSessionState, which is the only mutator. - private int _sessionState; - - // True for the duration of a codec callback that this channel itself dispatched to the - // actor loop (OnAttached, OnFrameReceived, OnSessionStateChanged, OnDetached, ArmDeadline - // fires, Host.Post work). Lets Host.SetSessionState detect reentrant calls from those - // callbacks and apply the state transition synchronously instead of - // PostAsync().GetAwaiter().GetResult() -- which would deadlock, because the actor loop - // that would service that posted work is the very loop currently blocked inside the - // callback that just called SetSessionState. AsyncLocal (rather than [ThreadStatic]) so - // the flag correctly follows ExecutionContext through any await boundary a callback might - // introduce, and is scoped to this channel instance so nested channels don't interfere. - private readonly AsyncLocal _isOnActorLoop = new(); - - /// - /// Attaches to and starts pumping - /// matching frames onto the codec's actor loop. Invokes - /// once, on that loop, before returning control -- so - /// callers can call -facing helpers immediately after construction. - /// - /// The shared L2 demux service to attach onto. Not owned by the channel. - /// The private codec implementation. Owned exclusively by this channel. - /// Optional tuning; null uses defaults. - public HaweChannel(ICanBusService busService, IHaweCodec codec, HaweChannelOptions? options = null) - { - _busService = busService ?? throw new ArgumentNullException(nameof(busService)); - _codec = codec ?? throw new ArgumentNullException(nameof(codec)); - var opts = options ?? new HaweChannelOptions(); - - // Locals until construction succeeds: if anything below throws after Subscribe / actor - // start, the catch path disposes those partial resources. Calling Dispose() is not - // safe here -- _pumpTask may not be assigned yet, and OnAttached may never have run. - ISubscription? subscription = null; - ProtocolActor? actor = null; - try - { - subscription = _busService.Subscribe(codec.FramePattern.Filter, opts.SubscriptionBufferCapacity); - - // SynchronizationContext mode needs a non-null context (ProtocolActor ctor). Prefer - // an explicit options value; otherwise fall back to SynchronizationContext.Current - // so UI / ASP.NET-style callers can select the mode without plumbing the context. - var syncContext = opts.ActorMode == ActorExecutionMode.SynchronizationContext - ? opts.SynchronizationContext ?? SynchronizationContext.Current - : null; - actor = new ProtocolActor(opts.ActorMode, syncContext); - - _subscription = subscription; - _actor = actor; - _actorSyncContext = syncContext; - _deadlines = new DeadlineScheduler(_actor); - _host = new Host(this); - - // Attach on the actor loop before starting the pump: the OnAttached callback is - // guaranteed to see zero frames delivered, matching every other protocol instance's - // "attach first, then receive" ordering. RunOnActorLoop waits synchronously so the - // constructor's contract ("codec attached before this returns") is a hard guarantee, - // not a race -- and avoids PostAsync().GetResult() when called from the actor's - // own SynchronizationContext (UI/dispatcher), which would deadlock on Send. - RunOnActorLoop(() => _codec.OnAttached(_host)); - - _pumpTask = Task.Run(PumpAsync); - } - catch - { - try { subscription?.Dispose(); } catch { /* best-effort teardown */ } - try { actor?.Dispose(); } catch { /* best-effort teardown */ } - _pumpCts.Dispose(); - throw; - } - } - - /// - public IHaweCodec Codec => _codec; - - /// - public HaweSessionState SessionState => (HaweSessionState)Volatile.Read(ref _sessionState); - - private async Task PumpAsync() - { - try - { - await foreach (var frame in _subscription.Frames.WithCancellation(_pumpCts.Token).ConfigureAwait(false)) - { - // Stop enqueuing new frames the instant Dispose signals detach: keeps the - // "no OnFrameReceived after OnDetached" ordering intact even against the - // narrow window between _pumpCts.Cancel() and the enumerator actually - // observing the cancellation on its next MoveNext. - if (Volatile.Read(ref _detachStarted) != 0) break; - - // Capture-by-value into the closure: CanFrameView is a struct, and the reference - // captured here is the buffered copy the subscription already owns (see - // Subscription.TryDeliver's payload-copy comment), so posting it to the actor - // loop is safe even after the subscription's async enumerable has moved on. - var f = frame; - _actor.Post(WrapOnLoop(() => _codec.OnFrameReceived(in f))); - } - } - catch (OperationCanceledException) - { - // Expected on Dispose(): _pumpCts fires and the enumeration ends. - } - catch (Exception) - { - // The subscription's async enumerable does not surface a fault channel other than - // completing early; if anything unexpected escapes we treat it as end-of-stream and - // let Dispose finish the teardown normally. A codec that needs to observe pump - // failures can surface that via its own OnDetached-time bookkeeping. - } - } - - /// - public void Dispose() - { - if (Interlocked.Exchange(ref _disposed, 1) != 0) return; - - // Signal detach-in-progress to the pump *before* cancelling: the pump checks this - // flag between iterations and stops enqueuing further OnFrameReceived posts, so a - // frame that arrived just before Cancel() cannot slip past into the mailbox behind - // the OnDetached we post below. - Volatile.Write(ref _detachStarted, 1); - _pumpCts.Cancel(); - - // Wait for the pump task to end BEFORE posting OnDetached. This is the core of the - // "no OnFrameReceived after OnDetached" guarantee: once _pumpTask has completed no - // further _actor.Post(() => OnFrameReceived(...)) can ever be issued, so posting - // OnDetached here places it strictly after every OnFrameReceived that will ever run - // on the single-writer actor loop. Doing this in the opposite order (post OnDetached - // first, then await pump) let the pump enqueue late frames behind OnDetached and - // fire them after the codec had already been told the channel was detached. - try { _pumpTask.Wait(TimeSpan.FromSeconds(5)); } - catch { /* pump exits on cancel; ignore any residual AggregateException */ } - - // Now, and only now, hand OnDetached to the actor loop. The codec is guaranteed - // exactly-once OnDetached on the same single-writer loop as every other callback, - // closing out any codec-owned resources safely. If the actor is already torn down - // for some reason, swallow the exception and continue -- Dispose must not throw for - // an already-broken channel. Inline when already on the actor SyncContext so we do - // not PostAsync().Wait on the dispatcher thread (Send deadlock); otherwise wait with - // a bounded timeout so a stuck actor cannot hang Dispose forever. - try - { - if (_isOnActorLoop.Value || IsOnActorSyncContext()) - RunOnActorLoop(() => _codec.OnDetached()); - else - _actor.PostAsync(WrapOnLoop(() => _codec.OnDetached())).Wait(TimeSpan.FromSeconds(5)); - } - catch - { - // See remark above: teardown is best-effort past this point. - } - - _subscription.Dispose(); - _actor.Dispose(); - _pumpCts.Dispose(); - } - - // Every codec callback the channel dispatches to the actor loop goes through this wrapper - // so _isOnActorLoop is true for the duration of the callback. Host.SetSessionState reads - // that flag to route reentrant calls onto the synchronous fast path instead of - // PostAsync().GetAwaiter().GetResult() (see the field's own remark for the full rationale). - private Action WrapOnLoop(Action work) - { - return () => - { - var previous = _isOnActorLoop.Value; - _isOnActorLoop.Value = true; - try { work(); } - finally { _isOnActorLoop.Value = previous; } - }; - } - - private Func WrapOnLoop(Func work) - { - return () => - { - var previous = _isOnActorLoop.Value; - _isOnActorLoop.Value = true; - try { return work(); } - finally { _isOnActorLoop.Value = previous; } - }; - } - - // True when the caller is already executing on the SynchronizationContext that - // ProtocolActor.Send marshals onto. Reference equality matches how UI dispatchers install - // a single context instance as Current on their thread. - private bool IsOnActorSyncContext() - => _actorSyncContext is not null - && ReferenceEquals(SynchronizationContext.Current, _actorSyncContext); - - /// - /// Runs under the channel's single-writer discipline and waits for - /// it to finish. Three paths, in priority order: - /// - /// Already inside a WrapOnLoop callback → invoke inline (reentrancy; avoids - /// self-deadlock on the actor mailbox). - /// Calling from the actor's SynchronizationContext → invoke inline (avoids the - /// SyncContext UI deadlock: PostAsync+GetResult blocks the dispatcher that - /// ProtocolActor.Send needs to deliver the posted work). - /// Otherwise → PostAsync and block for the result (DedicatedThread / ThreadPool / - /// SyncContext called from a non-dispatcher thread). - /// - /// Inline paths preserve single-writer: while this thread runs the callback, a concurrent - /// ProtocolActor.Send onto the same context cannot proceed until we return to the pump. - /// - private void RunOnActorLoop(Action work) - { - if (_isOnActorLoop.Value) - { - work(); - return; - } - - if (IsOnActorSyncContext()) - { - WrapOnLoop(work)(); - return; - } - - _actor.PostAsync(WrapOnLoop(work)).GetAwaiter().GetResult(); - } - - private T RunOnActorLoop(Func work) - { - if (_isOnActorLoop.Value) - return work(); - - if (IsOnActorSyncContext()) - return WrapOnLoop(work)(); - - return _actor.PostAsync(WrapOnLoop(work)).GetAwaiter().GetResult(); - } - - // The IHaweCodecHost surface. Kept as a private nested class so the framework's own - // instance state (actor, deadlines, session state) is never exposed to the codec beyond - // the documented interface. - private sealed class Host : IHaweCodecHost - { - private readonly HaweChannel _channel; - - internal Host(HaweChannel channel) => _channel = channel; - - public ICanBusService BusService => _channel._busService; - - public HaweSessionState SessionState => _channel.SessionState; - - public Task SendConfirmedAsync(CanFrame frame, TimeSpan? timeout = null, CancellationToken cancellationToken = default) - => _channel._busService.SendConfirmed(frame, timeout, cancellationToken); - - public bool SetSessionState(HaweSessionState state) - { - // RunOnActorLoop covers both deadlock classes: (1) reentrant calls from inside a - // codec callback already on the actor loop, and (2) off-loop calls made from the - // actor's own SynchronizationContext / UI thread, where PostAsync().GetResult() - // would block the dispatcher ProtocolActor.Send needs. Nested SetSessionState - // from inside OnSessionStateChanged takes the reentrant path via WrapOnLoop. - return _channel.RunOnActorLoop(() => ApplyStateChange(state)); - } - - private bool ApplyStateChange(HaweSessionState state) - { - var previous = (HaweSessionState)Volatile.Read(ref _channel._sessionState); - if (previous == state) return false; - Volatile.Write(ref _channel._sessionState, (int)state); - _channel._codec.OnSessionStateChanged(previous, state); - return true; - } - - public IDisposable ArmDeadline(TimeSpan timeout, Action onExpired) - => _channel._deadlines.Arm(timeout, _channel.WrapOnLoop(onExpired)); - - public void Post(Action work) => _channel._actor.Post(_channel.WrapOnLoop(work)); - } - } -} diff --git a/src/CanKit.Pro.Hawe/HaweChannelOptions.cs b/src/CanKit.Pro.Hawe/HaweChannelOptions.cs deleted file mode 100644 index d7fd8f2..0000000 --- a/src/CanKit.Pro.Hawe/HaweChannelOptions.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Threading; -using CanKit.Pro.Actor; - -namespace CanKit.Pro.Hawe -{ - /// - /// Optional tuning for a . Every field has a safe default so a codec - /// caller normally does not have to fill this in. - /// - public sealed class HaweChannelOptions - { - /// - /// Bounded buffer capacity for the channel's demultiplexer subscription (drop-oldest when - /// full, exactly like every other subscription -- see - /// CanKit.Pro.RawCan.CanBusService.DefaultBufferCapacity). Null uses the service - /// default. - /// - public int? SubscriptionBufferCapacity { get; set; } - - /// - /// Execution model for the actor loop that drives this channel's codec callbacks. Default - /// is , matching every other protocol - /// instance in CanKit. - /// - /// - /// When set to , the channel - /// constructs its with - /// (or - /// when that property is null). Both must resolve to a non-null context; otherwise - /// construction fails with . Construction, - /// SetSessionState, and detach are safe to invoke from that same context thread: - /// the channel runs the work inline instead of sync-waiting on a Send marshal. - /// - public ActorExecutionMode ActorMode { get; set; } = ActorExecutionMode.DedicatedThread; - - /// - /// Optional used when - /// is . - /// When null in that mode, - /// is used instead. Ignored for every other . - /// - public SynchronizationContext? SynchronizationContext { get; set; } - } -} diff --git a/src/CanKit.Pro.Hawe/HaweCodecRegistry.cs b/src/CanKit.Pro.Hawe/HaweCodecRegistry.cs deleted file mode 100644 index b7d3f99..0000000 --- a/src/CanKit.Pro.Hawe/HaweCodecRegistry.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace CanKit.Pro.Hawe -{ - /// - /// Default in-process : a name-keyed dictionary of codec - /// factories, protected by a single lock. Not a static singleton -- each application (and - /// each unit test) owns its own instance so that codec registrations never leak between - /// tests or across independent HAWE modules. - /// - public sealed class HaweCodecRegistry : IHaweCodecRegistry - { - private readonly object _gate = new(); - private readonly Dictionary> _factories = new(StringComparer.Ordinal); - - /// - public void Register(string name, Func factory) - { - if (string.IsNullOrEmpty(name)) throw new ArgumentException("Codec name must be non-empty.", nameof(name)); - if (factory is null) throw new ArgumentNullException(nameof(factory)); - - lock (_gate) - { - _factories[name] = factory; - } - } - - /// - public bool Unregister(string name) - { - if (string.IsNullOrEmpty(name)) return false; - lock (_gate) - { - return _factories.Remove(name); - } - } - - /// - public IHaweCodec Create(string name) - { - if (string.IsNullOrEmpty(name)) throw new ArgumentException("Codec name must be non-empty.", nameof(name)); - - Func? factory; - lock (_gate) - { - if (!_factories.TryGetValue(name, out factory)) - throw new KeyNotFoundException($"No HAWE codec registered under name '{name}'."); - } - - var codec = factory(); - if (codec is null) - throw new InvalidOperationException($"HAWE codec factory for '{name}' returned null."); - - return codec; - } - - /// - public bool IsRegistered(string name) - { - if (string.IsNullOrEmpty(name)) return false; - lock (_gate) - { - return _factories.ContainsKey(name); - } - } - - /// - public IReadOnlyList RegisteredNames - { - get - { - lock (_gate) - { - return _factories.Keys.ToArray(); - } - } - } - } -} diff --git a/src/CanKit.Pro.Hawe/HaweFramePattern.cs b/src/CanKit.Pro.Hawe/HaweFramePattern.cs deleted file mode 100644 index 938be32..0000000 --- a/src/CanKit.Pro.Hawe/HaweFramePattern.cs +++ /dev/null @@ -1,76 +0,0 @@ -using CanKit.Abstractions.API.Common.Definitions; -using CanKit.Pro.RawCan; - -namespace CanKit.Pro.Hawe -{ - /// - /// A generic, protocol-agnostic frame-selection pattern that tells a - /// which CAN frames belong to a proprietary HAWE codec instance - /// (SRS FR-HAWE-002). This is the only frame-shape information the public framework holds: - /// the actual codec is free to interpret matched frames however it wants -- the framework - /// merely delivers them. - /// - /// - /// - /// A pattern is expressed as a (ID range or acceptance code/mask): - /// the same allocation-free filter primitive used by every other L2/L3 protocol instance - /// (ISO-TP, J1939-TP, CANopen, ...), routed through the shared - /// demultiplexer so this codec's frames are delivered on its own - /// bounded, filtered subscription without the codec having to fight over - /// ICanBus.ReceiveAsync with other stacks. - /// - /// - /// The pattern deliberately carries only which frames matter, not what they mean: - /// no payload layout, no service-ID lookup table, no frame-shape assumptions beyond a CAN-ID - /// selector. This is what keeps the framework free of any HAWE proprietary detail (SRS CON-006 / - /// A-6): the concrete decode/encode logic lives entirely inside the private - /// implementation shipped in a separate, non-public repository. - /// - /// - public readonly struct HaweFramePattern - { - /// - /// Creates a pattern that accepts every frame matching . - /// - /// - /// The ID-range or acceptance-code/mask filter that selects this codec's frames on the - /// shared bus. Evaluated on the demultiplexer's per-frame fast path; see - /// for the exact matching semantics. - /// - public HaweFramePattern(CanIdFilter filter) - { - Filter = filter; - } - - /// - /// The CAN-ID filter that decides which frames on the shared bus belong to this codec - /// instance. Deliberately opaque about payload semantics: the framework never inspects - /// frame data itself. - /// - public CanIdFilter Filter { get; } - - /// - /// Convenience factory for an inclusive ID-range pattern. - /// - /// Minimum ID, inclusive. - /// Maximum ID, inclusive. - /// Standard 11-bit or extended 29-bit ID space. - public static HaweFramePattern Range( - uint from, - uint to, - CanFilterIDType idType = CanFilterIDType.Standard) - => new(CanIdFilter.Range(from, to, idType)); - - /// - /// Convenience factory for an acceptance-code/mask pattern. - /// - /// Acceptance code. - /// Acceptance mask; only the set bits are compared. - /// Standard 11-bit or extended 29-bit ID space. - public static HaweFramePattern Mask( - uint accCode, - uint accMask, - CanFilterIDType idType = CanFilterIDType.Standard) - => new(CanIdFilter.Mask(accCode, accMask, idType)); - } -} diff --git a/src/CanKit.Pro.Hawe/HaweSessionState.cs b/src/CanKit.Pro.Hawe/HaweSessionState.cs deleted file mode 100644 index 1db0782..0000000 --- a/src/CanKit.Pro.Hawe/HaweSessionState.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace CanKit.Pro.Hawe -{ - /// - /// The generic, protocol-agnostic session states surfaced by the framework's placeholder - /// session skeleton (SRS FR-HAWE-004). The framework deliberately defines no HAWE-specific - /// transitions, guards, or actions -- those live entirely inside a private codec once the - /// (confidential) protocol specification becomes available (SRS A-6 / FR-HAWE-005). Until then - /// this three-state alphabet is a Vorlage / placeholder only: a codec is free to keep the - /// channel in forever, or to model its own richer state machine internally - /// and drive this enum purely as a public health signal for the caller. - /// - public enum HaweSessionState - { - /// - /// The channel is attached and configured but no session-level activity is in progress. - /// The initial state of every freshly-opened channel. - /// - Idle = 0, - - /// - /// The codec has declared that a session-level exchange is currently in progress. The - /// framework attaches no further meaning to this state; it does not, for example, gate - /// frame delivery on it. - /// - Active = 1, - - /// - /// The codec has declared a session-level fault that is not, by itself, a bus fault. The - /// framework does not automatically recover from this state; a codec transitions back to - /// (or ) when it considers the fault handled. - /// - Fault = 2, - } -} diff --git a/src/CanKit.Pro.Hawe/IHaweChannel.cs b/src/CanKit.Pro.Hawe/IHaweChannel.cs deleted file mode 100644 index 75a83af..0000000 --- a/src/CanKit.Pro.Hawe/IHaweChannel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -namespace CanKit.Pro.Hawe -{ - /// - /// A single, running attachment of one onto one - /// (SRS FR-HAWE-002/003): the framework side - /// of the running "HAWE stack instance". Owns the demultiplexer subscription, the actor loop, - /// the deadline scheduler, and the generic session-skeleton state -- all shared out to the - /// codec via . - /// - /// - /// Disposing the channel is the exclusive way to shut a codec down: it stops delivering - /// frames, invokes once on the actor loop, and releases - /// the subscription, the actor and the deadline scheduler. Dispose is idempotent. - /// - public interface IHaweChannel : IDisposable - { - /// - /// The codec plugged into this channel. Exposed so callers can look up - /// / for logging or - /// diagnostics; the framework itself does not require this handle after construction. - /// - IHaweCodec Codec { get; } - - /// - /// The current generic session-skeleton state (SRS FR-HAWE-004). Reads are lock-free but - /// may race with a concurrent codec-initiated transition; callers that need a coherent - /// before/after view should observe via - /// . - /// - HaweSessionState SessionState { get; } - } -} diff --git a/src/CanKit.Pro.Hawe/IHaweCodec.cs b/src/CanKit.Pro.Hawe/IHaweCodec.cs deleted file mode 100644 index d829497..0000000 --- a/src/CanKit.Pro.Hawe/IHaweCodec.cs +++ /dev/null @@ -1,87 +0,0 @@ -using CanKit.Abstractions.API.Can.Definitions; - -namespace CanKit.Pro.Hawe -{ - /// - /// The public extension point (SPI) through which a private, proprietary HAWE codec module - /// plugs into the generic framework (SRS FR-HAWE-001). All HAWE-specific knowledge -- payload - /// layout, service catalogue, session/handshake state machine, cryptographic material, error - /// vocabulary -- lives behind this interface, in an implementation shipped from a separate, - /// non-public repository (SRS CON-006 / A-6). The framework itself only ever sees raw CAN - /// frames and opaque codec callbacks. - /// - /// - /// - /// A codec instance is owned by exactly one for the lifetime of - /// that channel: is invoked once, before any frame is delivered, and - /// is invoked once, after which no further callbacks fire. Both - /// and run on the channel's - /// single actor loop, so the codec implementation is guaranteed single-writer against its own - /// internal state and does not need to take its own locks (mirrors the - /// CanKit.Pro.Actor contract used by every other L3/L4 stack). - /// - /// - /// The framework never inspects a codec's identity beyond (used for - /// diagnostics/registry lookup) and (used to size the - /// demultiplexer subscription). It ships no reference/default implementation of this - /// interface: any codec sitting behind it is deliberately out of scope for this open-source - /// package. - /// - /// - public interface IHaweCodec - { - /// - /// Short, human-readable name used in registry lookups () - /// and diagnostic logs. Must be unique within a single registry instance. Deliberately not - /// versioned or namespaced -- the framework does not care whether two codecs are compatible - /// with each other, only that a caller can find the codec it registered. - /// - string Name { get; } - - /// - /// The generic frame-selection pattern that tells the channel which frames on the shared - /// bus belong to this codec (SRS FR-HAWE-002). Read once during - /// construction; must not change afterwards. - /// - HaweFramePattern FramePattern { get; } - - /// - /// Called exactly once, on the channel's actor loop, before any other callback. The codec - /// captures the handle if it needs to send frames or read session - /// state later; it must not use the handle from any other thread than the actor loop. - /// - /// - /// The framework side of the codec/host contract: the codec's transmit / state-change / - /// deadline surface. Only valid until is invoked. - /// - void OnAttached(IHaweCodecHost host); - - /// - /// Invoked on the channel's actor loop for every CAN frame that matches - /// . The framework has done no HAWE-specific decoding; the - /// codec receives the same read-only the demultiplexer would - /// have delivered to any other subscription. - /// - /// The matching frame. Non-owning; do not retain past this callback. - void OnFrameReceived(in CanFrameView frame); - - /// - /// Invoked on the channel's actor loop whenever the framework's generic session-skeleton - /// state () transitions. The framework itself never triggers - /// a transition; every change originates from a codec call to - /// . Codecs that do not - /// use the session skeleton can leave this as a no-op. - /// - /// The state the channel was in before the transition. - /// The state the channel is now in. - void OnSessionStateChanged(HaweSessionState previous, HaweSessionState current); - - /// - /// Invoked exactly once, on the channel's actor loop, when the channel is being torn down - /// (either the caller disposed it, or the underlying bus became irrecoverable). Any - /// codec-owned resources (buffers, timers, files) must be released here; the codec must - /// not touch after this returns. - /// - void OnDetached(); - } -} diff --git a/src/CanKit.Pro.Hawe/IHaweCodecHost.cs b/src/CanKit.Pro.Hawe/IHaweCodecHost.cs deleted file mode 100644 index bf9ddb7..0000000 --- a/src/CanKit.Pro.Hawe/IHaweCodecHost.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using CanKit.Abstractions.API.Can.Definitions; -using CanKit.Pro.RawCan; - -namespace CanKit.Pro.Hawe -{ - /// - /// The framework-side of the codec/host contract (SRS FR-HAWE-003): the set of L2 services a - /// private may call from its callbacks in order to send frames, - /// schedule timers, and drive the generic session-skeleton state. Handed to the codec exactly - /// once via . - /// - /// - /// - /// Every method on this host is safe to call only from the channel's actor loop -- i.e. from - /// inside a codec callback or from a scheduled deadline callback. This is the same - /// single-writer discipline every ISO-TP / J1939-TP / CANopen instance already relies on, so - /// the codec does not have to marshal frames onto its own thread or take its own locks. - /// - /// - /// The framework surfaces raw CAN transmit through , backed - /// by the same primitive that ISO-TP already uses; - /// this keeps HAWE on parity with every other L3/L4 stack and makes the confirmation - /// semantics (hardware-echo vs. driver-acceptance approximation) identical. - /// - /// - public interface IHaweCodecHost - { - /// - /// The underlying the channel is demultiplexing. Exposed so - /// specialised codecs that need a secondary filtered subscription (e.g. a debug tap) can - /// register one directly, using the same primitive as the primary - /// . Codecs that only need the primary flow can - /// ignore this. - /// - ICanBusService BusService { get; } - - /// - /// The current generic session-skeleton state - /// (SRS FR-HAWE-004). See . Reads from the actor loop only. - /// - HaweSessionState SessionState { get; } - - /// - /// Sends on the shared bus and asynchronously reports whether - /// it was actually sent, using the same TX-confirm semantics as every other L3/L4 stack - /// (SRS FR-RAW-030..034). The returned task completes on some pool thread; if the codec - /// needs to react on the actor loop it must post the result back explicitly. The - /// framework does not itself dispose ; the caller keeps ownership - /// per the CanKit TX-lease contract. - /// - /// The frame to transmit. - /// - /// Maximum time to wait for a hardware echo (when the bus has echo enabled). Null uses the - /// service's default. Ignored on the driver-acceptance approximation path. - /// - /// Caller-supplied cancellation. - Task SendConfirmedAsync(CanFrame frame, TimeSpan? timeout = null, CancellationToken cancellationToken = default); - - /// - /// Transitions the channel's generic session-skeleton state and, if it actually changes, - /// invokes on the actor loop before this - /// call returns. The framework attaches no meaning to any specific transition; the codec - /// is the sole authority on session semantics (SRS FR-HAWE-004). - /// - /// The new state. - /// True if the state changed, false if it was already . - bool SetSessionState(HaweSessionState state); - - /// - /// Arms a one-shot deadline that fires on the actor loop after - /// unless the returned handle is completed or disposed first. - /// Composed on top of the same DeadlineScheduler every other protocol instance - /// uses (SRS FR-RAW-050), so a codec that models its own timing (P2, N_As, session - /// keep-alive) gets deadlines that are guaranteed to be checked. - /// - /// Time until expiry. Must be >= . - /// Callback invoked at most once, on the actor loop. - /// A handle used to complete or cancel the deadline. Disposing cancels it. - IDisposable ArmDeadline(TimeSpan timeout, Action onExpired); - - /// - /// Schedules to run on the actor loop as a fire-and-forget item. - /// The codec uses this to defer work triggered outside a callback (e.g. from an - /// application thread) onto the single-writer loop. Exceptions surface via the actor's - /// documented background-exception channel, exactly like every other post. - /// - /// The work to run on the actor loop. - void Post(Action work); - } -} diff --git a/src/CanKit.Pro.Hawe/IHaweCodecRegistry.cs b/src/CanKit.Pro.Hawe/IHaweCodecRegistry.cs deleted file mode 100644 index 13ec825..0000000 --- a/src/CanKit.Pro.Hawe/IHaweCodecRegistry.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace CanKit.Pro.Hawe -{ - /// - /// A registry of factories keyed by codec - /// (SRS FR-HAWE-001). Its role is analogous to the former ISO-TP register SPI: a - /// public discovery/registration surface that lets an application (or a private HAWE module - /// assembly) plug in a proprietary codec without touching the framework itself. - /// - /// - /// - /// Registrations hold codec factories, not codec instances, because a channel opens on - /// exactly one bus and must not share codec state across buses. A factory is invoked once per - /// and hands back a fresh codec bound to that channel's lifetime. - /// - /// - /// The registry is deliberately in-process and dependency-free: it does not scan assemblies - /// or use MEF-style attributes. A private HAWE module registers its codec by calling - /// once at application startup, which keeps - /// every reference to HAWE-proprietary detail inside the private module. - /// - /// - public interface IHaweCodecRegistry - { - /// - /// Registers a codec factory under . Overwrites any previous - /// factory registered under the same name -- this mirrors the "last writer wins" behaviour - /// of the other registries in CanKit and lets tests substitute a codec without having to - /// unregister an earlier one first. - /// - /// Codec name; must be non-null/non-empty and unique per registry. - /// Factory producing a fresh codec instance per invocation. - void Register(string name, Func factory); - - /// - /// Removes the factory previously registered under , if any. - /// Returns true when a registration was removed, false when there was none. Never throws. - /// - bool Unregister(string name); - - /// - /// Looks up the factory registered under and invokes it, returning - /// a fresh codec instance. Throws when no such - /// registration exists -- callers that want a "try" pattern should first check - /// . - /// - IHaweCodec Create(string name); - - /// - /// True when a factory is registered under . - /// - bool IsRegistered(string name); - - /// - /// Snapshot of every currently-registered codec name, in unspecified order. Primarily for - /// diagnostics and tests; the framework itself never enumerates the registry on the hot - /// path. - /// - IReadOnlyList RegisteredNames { get; } - } -} diff --git a/src/CanKit.Pro.Hawe/README.md b/src/CanKit.Pro.Hawe/README.md deleted file mode 100644 index a073f09..0000000 --- a/src/CanKit.Pro.Hawe/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# CanKit.Pro.Hawe - -Generic HAWE extension framework for [CanKit](https://github.com/pkuyo/CanKit): a public SPI and -reference host that lets a proprietary HAWE codec module attach onto CanKit's L2 raw-CAN service -(subscriptions, TX-confirm, actor loop, deadline scheduler) without the framework itself having -any knowledge of the HAWE protocol (SRS FR-HAWE-001..005, arc42 §5 L4). - -> **Legal note (SRS CON-006 / assumption A-6).** The HAWE protocol is confidential and its -> specification is not available to this project. **No** service IDs, frame layouts, session -> transitions, keys, or any other proprietary HAWE detail are shipped in this repository, this -> assembly, or any NuGet package produced from it. Only the generic extension surface required by -> `FR-HAWE-001..005` is public. Any concrete HAWE codec must be implemented in a separate, -> non-public repository against the SPI defined here. - -## Status - -- Pre-release (0.1.x). -- `IsPackable=false`: this framework is not published to NuGet until a private HAWE module exists - to consume it (SRS CON-004). Consumers depend on it via `ProjectReference`. -- No HAWE-specific code included. `FR-HAWE-004`'s session skeleton is deliberately generic - (`Idle` / `Active` / `Fault`) and applies no protocol logic. - -## Public SPI - -- `IHaweCodec` — the plug-in surface a private module implements. Lifetime callbacks - (`OnAttached`, `OnFrameReceived`, `OnSessionStateChanged`, `OnDetached`) all run on the - channel's single-writer actor loop. -- `IHaweCodecHost` — the framework side of the same contract: `SendConfirmedAsync`, - `SetSessionState`, `ArmDeadline`, `Post`, plus direct access to the shared `ICanBusService` - for secondary subscriptions. -- `HaweFramePattern` — a `CanIdFilter`-based frame selector (`FR-HAWE-002`). Carries no payload - layout, no service catalogue, no HAWE-specific semantics. -- `HaweSessionState` — placeholder three-state alphabet for `FR-HAWE-004`. -- `IHaweCodecRegistry` / `HaweCodecRegistry` — in-process, name-keyed factory registry - for pluggable codecs (`FR-HAWE-001`). -- `IHaweChannel` / `HaweChannel` — the running attachment of one codec to one bus service. - -## Example - -```csharp -using CanKit.Core; -using CanKit.Pro.RawCan; -using CanKit.Pro.Hawe; - -// Register a codec once, at startup. In production, `MyPrivateHaweCodec` lives in a -// non-public repository and is not part of CanKit. -var registry = new HaweCodecRegistry(); -registry.Register("acme-hawe-v1", () => new MyPrivateHaweCodec()); - -// Open a bus and share one CanBusService across every protocol instance on it (same pattern -// ISO-TP and J1939-TP already use). -using var bus = CanBus.Open("virtual://demo/0", cfg => - cfg.SetProtocolMode(CanProtocolMode.Can20).Baud(500_000)); -using var service = new CanBusService(bus); - -// The framework never sees inside `MyPrivateHaweCodec`; it just delivers matching frames to it -// and offers back the L2 services via IHaweCodecHost. -using var channel = new HaweChannel(service, registry.Create("acme-hawe-v1")); -``` - -## Testing - -The framework ships with a `FakePatternCodec` in the test project only. It is deliberately -generic (echo one CAN ID pattern, count callbacks, drive the session skeleton) and does not -implement any HAWE-specific behaviour -- it exists solely to verify the framework's plumbing on -the Virtual adapter (`FR-HAWE-001`/`FR-HAWE-002` verification criteria). - -Run the framework test suite: - -``` -dotnet test CanKitProHawe.slnf -c Release -f net8.0 -``` - -## References - -- SRS: `docs/requirements/SRS-CanKit.md`, §4.3.4 (`FR-HAWE-001..005`), §5 `CON-006`, - §6 `A-6`. -- arc42: `docs/architecture/arc42-CanKit.md`, §3 L4 building blocks / §5 zoom L2. -- SPI pattern: name-keyed in-process factory registry (see `HaweCodecRegistry`). - -## Install - -`CanKit.Pro.Hawe` is **not published to nuget.org yet** — its API is still settling. It is built -and tested on every CI run, so it does not rot; to use it today, reference the project from a -clone of [CanKit.Pro](https://github.com/dborgards/CanKit.Pro). - -Dependencies: `CanKit.Abstractions`, `CanKit.Pro.Actor`, `CanKit.Pro.RawCan`, `CanKit.Pro.Reliability`. - -Part of [CanKit.Pro](https://github.com/dborgards/CanKit.Pro) — higher CAN protocol layers -built **on top of** [CanKit](https://github.com/pkuyo/CanKit), which is consumed as a NuGet -package rather than forked. - -## License - -MIT — see [LICENSE](https://github.com/dborgards/CanKit.Pro/blob/main/LICENSE). -CanKit itself is a separate project licensed under Apache-2.0; see -[THIRD-PARTY-NOTICES.md](https://github.com/dborgards/CanKit.Pro/blob/main/THIRD-PARTY-NOTICES.md). diff --git a/src/CanKit.Pro.IsoTp/CanKit.Pro.IsoTp.csproj b/src/CanKit.Pro.IsoTp/CanKit.Pro.IsoTp.csproj index 0788ae3..5b399c1 100644 --- a/src/CanKit.Pro.IsoTp/CanKit.Pro.IsoTp.csproj +++ b/src/CanKit.Pro.IsoTp/CanKit.Pro.IsoTp.csproj @@ -3,12 +3,6 @@ Experimental, specification-compliant ISO 15765-2 (ISO-TP) implementation for CanKit.Pro: deterministic Single-/First-/Consecutive-/Flow-Control-frame codec, bounds-checked PCI parser and STmin helpers, plus an actor-driven runtime (IIsoTpChannel) that composes on top of the CanKit.Pro L2 services (RawCan demux, TX-confirm, Actor, Reliability deadlines) — no vendor-SDK dependency. $(PackageTags);ISO-TP;ISO 15765-2;codec;channel - - false diff --git a/src/CanKit.Pro.J1939/CanKit.Pro.J1939.csproj b/src/CanKit.Pro.J1939/CanKit.Pro.J1939.csproj index e37306d..5d9e45b 100644 --- a/src/CanKit.Pro.J1939/CanKit.Pro.J1939.csproj +++ b/src/CanKit.Pro.J1939/CanKit.Pro.J1939.csproj @@ -3,12 +3,6 @@ SAE J1939 application-layer node for CanKit.Pro: PGN send/receive with 29-bit Priority/PF/PS/SA encode/decode, SPN scale/offset extraction, PGN 0xEE00 Address Claiming with NAME arbitration (including Cannot-Claim), PGN 0xEA00 Request-PGN, and automatic multi-frame routing through CanKit.Pro.J1939Tp for payloads > 8 bytes. Composes on the CanKit.Pro L2 services (RawCan demux, TX-confirm, Actor, Reliability deadlines) with no vendor-SDK dependency. $(PackageTags);J1939;J1939-21;J1939-81;PGN;SPN;address claim - - false diff --git a/src/CanKit.Pro.J1939Tp/CanKit.Pro.J1939Tp.csproj b/src/CanKit.Pro.J1939Tp/CanKit.Pro.J1939Tp.csproj index 5da4e36..79b2b28 100644 --- a/src/CanKit.Pro.J1939Tp/CanKit.Pro.J1939Tp.csproj +++ b/src/CanKit.Pro.J1939Tp/CanKit.Pro.J1939Tp.csproj @@ -3,12 +3,6 @@ SAE J1939-21 Transport Protocol (TP.BAM broadcast + TP.CM connection-mode RTS/CTS/EndOfMsgAck) for CanKit.Pro: an actor-driven multi-session channel that composes on top of the CanKit.Pro L2 services (RawCan demux, TX-confirm, Actor, Reliability deadlines) and the CanKit.Pro.Addressing J1939 PGN helpers -- no vendor-SDK dependency. $(PackageTags);J1939;J1939-21;TP.BAM;TP.CM;transport - - false diff --git a/src/CanKit.Pro.Uds/CanKit.Pro.Uds.csproj b/src/CanKit.Pro.Uds/CanKit.Pro.Uds.csproj index d32f67c..15d72ad 100644 --- a/src/CanKit.Pro.Uds/CanKit.Pro.Uds.csproj +++ b/src/CanKit.Pro.Uds/CanKit.Pro.Uds.csproj @@ -3,12 +3,6 @@ Experimental Unified Diagnostic Services (ISO 14229-1) client for CanKit.Pro. Provides an async IUdsClient over IIsoTpChannel with the MVP service set (0x10 DiagnosticSessionControl, 0x11 ECUReset, 0x22 ReadDataByIdentifier, 0x27 SecurityAccess, 0x2E WriteDataByIdentifier, 0x31 RoutineControl, 0x3E TesterPresent), P2/P2* timing, NRC 0x78 responsePending handling and structured negative-response reporting. $(PackageTags);UDS;ISO 14229;diagnostics - - false diff --git a/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.CANopen.approved.txt b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.CANopen.approved.txt new file mode 100644 index 0000000..25bcc7a --- /dev/null +++ b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.CANopen.approved.txt @@ -0,0 +1,217 @@ +class CanKit.Pro.CANopen.CanOpen + method CanKit.Pro.CANopen.ICanOpenNode OpenNode(CanKit.Abstractions.API.Can.ICanBus bus, System.Byte nodeId, CanKit.Pro.CANopen.CanOpenNodeOptions options) + method CanKit.Pro.CANopen.ICanOpenNode OpenNode(CanKit.Pro.RawCan.ICanBusService service, System.Byte nodeId, CanKit.Pro.CANopen.CanOpenNodeOptions options, System.Boolean leaveOpen) +class CanKit.Pro.CANopen.CanOpenCobId + field System.Byte MaxNodeId + field System.Byte MinNodeId + field System.UInt32 EmcyBase + field System.UInt32 HeartbeatBase + field System.UInt32 NmtCommand + field System.UInt32 Rpdo1Base + field System.UInt32 Rpdo2Base + field System.UInt32 Rpdo3Base + field System.UInt32 Rpdo4Base + field System.UInt32 SdoRxBase + field System.UInt32 SdoTxBase + field System.UInt32 Sync + field System.UInt32 Tpdo1Base + field System.UInt32 Tpdo2Base + field System.UInt32 Tpdo3Base + field System.UInt32 Tpdo4Base + method System.UInt32 Emcy(System.Byte nodeId) + method System.UInt32 Heartbeat(System.Byte nodeId) + method System.UInt32 RpdoDefault(System.Byte nodeId, System.Int32 pdoIndex) + method System.UInt32 SdoRx(System.Byte nodeId) + method System.UInt32 SdoTx(System.Byte nodeId) + method System.UInt32 TpdoDefault(System.Byte nodeId, System.Int32 pdoIndex) + method System.Void ValidateNodeId(System.Byte nodeId) +class CanKit.Pro.CANopen.CanOpenNodeOptions + ctor () + method CanKit.Pro.CANopen.CanOpenNodeOptions With(System.Nullable sdoTimeout, System.Nullable sdoServerTimeout, System.Nullable defaultTpdoEventTimerInterval, System.Nullable eventQueueCapacity, System.Nullable maxSdoTransferBytes, System.Nullable sdoBlockThresholdBytes, System.Nullable sdoBlockSize, System.Nullable sdoBlockCrcSupported, System.Nullable sdoBlockMaxRetransmissions, System.Nullable respondToNodeGuardingRtr, System.Nullable enableChangeOfStateTpdo) + prop System.Boolean EnableChangeOfStateTpdo {get/set} + prop System.Boolean RespondToNodeGuardingRtr {get/set} + prop System.Boolean SdoBlockCrcSupported {get/set} + prop System.Byte SdoBlockSize {get/set} + prop System.Int32 EventQueueCapacity {get/set} + prop System.Int32 MaxSdoTransferBytes {get/set} + prop System.Int32 SdoBlockMaxRetransmissions {get/set} + prop System.Int32 SdoBlockThresholdBytes {get/set} + prop System.TimeSpan DefaultTpdoEventTimerInterval {get/set} + prop System.TimeSpan SdoServerTimeout {get/set} + prop System.TimeSpan SdoTimeout {get/set} +class CanKit.Pro.CANopen.CanOpenTransportException + ctor (System.String message) + ctor (System.String message, System.Exception inner) +class CanKit.Pro.CANopen.Emcy.EmcyMessage + ctor (System.Byte producerNodeId, System.UInt16 errorCode, System.Byte errorRegister, System.ReadOnlySpan manufacturerSpecific) + field System.Int32 ManufacturerFieldLength + field System.Int32 WireSize + method CanKit.Pro.CANopen.Emcy.EmcyMessage Decode(System.Byte producerNodeId, System.ReadOnlySpan data) + method System.Byte[] Encode() + prop System.Byte ErrorRegister {get} + prop System.Byte ProducerNodeId {get} + prop System.Byte[] ManufacturerSpecific {get} + prop System.UInt16 ErrorCode {get} +class CanKit.Pro.CANopen.EmcyReceivedEventArgs + ctor (CanKit.Pro.CANopen.Emcy.EmcyMessage message, System.DateTime timestamp) + prop CanKit.Pro.CANopen.Emcy.EmcyMessage Message {get} + prop System.DateTime Timestamp {get} +class CanKit.Pro.CANopen.HeartbeatReceivedEventArgs + ctor (System.Byte producerNodeId, CanKit.Pro.CANopen.Nmt.NmtState state, System.DateTime timestamp) + prop CanKit.Pro.CANopen.Nmt.NmtState State {get} + prop System.Byte ProducerNodeId {get} + prop System.DateTime Timestamp {get} +class CanKit.Pro.CANopen.HeartbeatTimeoutEventArgs + ctor (System.Byte producerNodeId, System.TimeSpan timeout) + prop System.Byte ProducerNodeId {get} + prop System.TimeSpan Timeout {get} +interface CanKit.Pro.CANopen.ICanOpenNode + event System.EventHandler EmcyReceived + event System.EventHandler HeartbeatReceived + event System.EventHandler HeartbeatTimeout + event System.EventHandler NmtCommandReceived + event System.EventHandler NodeGuardingReceived + event System.EventHandler NodeGuardingTimeout + event System.EventHandler RpdoReceived + event System.EventHandler SyncReceived + event System.EventHandler BackgroundExceptionOccurred + method System.Threading.Tasks.Task SdoDownloadAsync(System.Byte serverNodeId, System.UInt16 index, System.Byte subindex, System.ReadOnlyMemory data, CanKit.Pro.CANopen.Sdo.SdoTransferMode mode, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SdoDownloadAsync(System.Byte serverNodeId, System.UInt16 index, System.Byte subindex, System.ReadOnlyMemory data, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SendEmcyAsync(System.UInt16 errorCode, System.Byte errorRegister, System.ReadOnlyMemory manufacturerSpecific, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SendNmtCommandAsync(CanKit.Pro.CANopen.Nmt.NmtCommand command, System.Byte targetNodeId, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SendSyncAsync(System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task TriggerTpdoAsync(System.Int32 pdoIndex, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SdoUploadAsync(System.Byte serverNodeId, System.UInt16 index, System.Byte subindex, CanKit.Pro.CANopen.Sdo.SdoTransferMode mode, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SdoUploadAsync(System.Byte serverNodeId, System.UInt16 index, System.Byte subindex, System.Threading.CancellationToken cancellationToken) + method System.Void AddHeartbeatConsumer(System.Byte producerNodeId, System.TimeSpan timeout) + method System.Void ConfigureRpdo(System.Int32 pdoIndex, CanKit.Pro.CANopen.Pdo.PdoMapping mapping, System.Nullable cobId) + method System.Void ConfigureTpdo(System.Int32 pdoIndex, CanKit.Pro.CANopen.Pdo.PdoMapping mapping, CanKit.Pro.CANopen.Pdo.TpdoTransmission transmission, System.Nullable cobId, System.Nullable eventTimerInterval) + method System.Void RemoveHeartbeatConsumer(System.Byte producerNodeId) + method System.Void StartHeartbeatProducer(System.TimeSpan interval) + method System.Void StartNodeGuardingConsumer(System.Byte producerNodeId, System.TimeSpan guardTime, System.Byte lifeTimeFactor) + method System.Void StartSyncProducer(System.TimeSpan interval) + method System.Void StopHeartbeatProducer() + method System.Void StopNodeGuardingConsumer(System.Byte producerNodeId) + method System.Void StopSyncProducer() + prop CanKit.Pro.CANopen.CanOpenNodeOptions Options {get} + prop CanKit.Pro.CANopen.Nmt.NmtState State {get} + prop CanKit.Pro.CANopen.ObjectDictionary ObjectDictionary {get} + prop System.Byte NodeId {get} +enum CanKit.Pro.CANopen.Nmt.NmtCommand : Byte + field CanKit.Pro.CANopen.Nmt.NmtCommand EnterPreOperational + field CanKit.Pro.CANopen.Nmt.NmtCommand ResetCommunication + field CanKit.Pro.CANopen.Nmt.NmtCommand ResetNode + field CanKit.Pro.CANopen.Nmt.NmtCommand Start + field CanKit.Pro.CANopen.Nmt.NmtCommand Stop +enum CanKit.Pro.CANopen.Nmt.NmtState : Byte + field CanKit.Pro.CANopen.Nmt.NmtState Initializing + field CanKit.Pro.CANopen.Nmt.NmtState Operational + field CanKit.Pro.CANopen.Nmt.NmtState PreOperational + field CanKit.Pro.CANopen.Nmt.NmtState Stopped +class CanKit.Pro.CANopen.NmtCommandReceivedEventArgs + ctor (CanKit.Pro.CANopen.Nmt.NmtCommand command, System.Byte targetNodeId) + prop CanKit.Pro.CANopen.Nmt.NmtCommand Command {get} + prop System.Byte TargetNodeId {get} +class CanKit.Pro.CANopen.NodeGuardingReceivedEventArgs + ctor (System.Byte producerNodeId, CanKit.Pro.CANopen.Nmt.NmtState state, System.Boolean toggle, System.DateTime timestamp) + prop CanKit.Pro.CANopen.Nmt.NmtState State {get} + prop System.Boolean Toggle {get} + prop System.Byte ProducerNodeId {get} + prop System.DateTime Timestamp {get} +class CanKit.Pro.CANopen.NodeGuardingTimeoutEventArgs + ctor (System.Byte producerNodeId, System.TimeSpan guardTime, System.Byte lifeTimeFactor) + prop System.Byte LifeTimeFactor {get} + prop System.Byte ProducerNodeId {get} + prop System.TimeSpan GuardTime {get} +class CanKit.Pro.CANopen.ObjectDictionary + ctor () + method CanKit.Pro.CANopen.OdEntry AddDomain(System.UInt16 index, System.Byte subindex, System.Byte[] value, CanKit.Pro.CANopen.OdAccess access) + method CanKit.Pro.CANopen.OdEntry AddI16(System.UInt16 index, System.Byte subindex, System.Int16 value, CanKit.Pro.CANopen.OdAccess access) + method CanKit.Pro.CANopen.OdEntry AddI32(System.UInt16 index, System.Byte subindex, System.Int32 value, CanKit.Pro.CANopen.OdAccess access) + method CanKit.Pro.CANopen.OdEntry AddI8(System.UInt16 index, System.Byte subindex, System.SByte value, CanKit.Pro.CANopen.OdAccess access) + method CanKit.Pro.CANopen.OdEntry AddU16(System.UInt16 index, System.Byte subindex, System.UInt16 value, CanKit.Pro.CANopen.OdAccess access) + method CanKit.Pro.CANopen.OdEntry AddU32(System.UInt16 index, System.Byte subindex, System.UInt32 value, CanKit.Pro.CANopen.OdAccess access) + method CanKit.Pro.CANopen.OdEntry AddU8(System.UInt16 index, System.Byte subindex, System.Byte value, CanKit.Pro.CANopen.OdAccess access) + method System.Boolean TryGet(System.UInt16 index, System.Byte subindex, CanKit.Pro.CANopen.OdEntry& entry) + method System.Boolean TryReadRaw(System.UInt16 index, System.Byte subindex, System.Byte[]& value) + method System.Byte[] ReadRaw(System.UInt16 index, System.Byte subindex) + method System.Int32 ReadSigned(System.UInt16 index, System.Byte subindex) + method System.UInt32 ReadUnsigned(System.UInt16 index, System.Byte subindex) + method System.Void WriteRaw(System.UInt16 index, System.Byte subindex, System.Byte[] value) + method System.Void WriteUnsigned(System.UInt16 index, System.Byte subindex, System.UInt32 value) + prop System.Int32 Count {get} +enum CanKit.Pro.CANopen.OdAccess : Byte + field CanKit.Pro.CANopen.OdAccess ReadOnly + field CanKit.Pro.CANopen.OdAccess ReadWrite + field CanKit.Pro.CANopen.OdAccess WriteOnly +enum CanKit.Pro.CANopen.OdDataType : Byte + field CanKit.Pro.CANopen.OdDataType Domain + field CanKit.Pro.CANopen.OdDataType Integer16 + field CanKit.Pro.CANopen.OdDataType Integer32 + field CanKit.Pro.CANopen.OdDataType Integer8 + field CanKit.Pro.CANopen.OdDataType Unsigned16 + field CanKit.Pro.CANopen.OdDataType Unsigned32 + field CanKit.Pro.CANopen.OdDataType Unsigned8 +class CanKit.Pro.CANopen.OdEntry + method System.Byte[] GetRawValue() + prop CanKit.Pro.CANopen.OdAccess Access {get} + prop CanKit.Pro.CANopen.OdDataType DataType {get} + prop System.Int32 Size {get} +class CanKit.Pro.CANopen.Pdo.PdoMapping + ctor () + ctor (System.Collections.Generic.IEnumerable entries) + method CanKit.Pro.CANopen.Pdo.PdoMapping Add(CanKit.Pro.CANopen.Pdo.PdoMappingEntry entry) + method CanKit.Pro.CANopen.Pdo.PdoMapping Add(System.UInt16 index, System.Byte subindex, System.Byte bitLength) + method System.Void Clear() + prop System.Collections.Generic.IReadOnlyList Entries {get} + prop System.Int32 TotalBytes {get} +struct CanKit.Pro.CANopen.Pdo.PdoMappingEntry + ctor (System.UInt16 index, System.Byte subindex, System.Byte bitLength) + method System.Boolean Equals(CanKit.Pro.CANopen.Pdo.PdoMappingEntry other) + method System.Boolean Equals(System.Object obj) + method System.Int32 GetHashCode() + prop System.Byte BitLength {get} + prop System.Byte Subindex {get} + prop System.Int32 ByteLength {get} + prop System.UInt16 Index {get} +enum CanKit.Pro.CANopen.Pdo.TpdoTransmission : Byte + field CanKit.Pro.CANopen.Pdo.TpdoTransmission EventDriven + field CanKit.Pro.CANopen.Pdo.TpdoTransmission EventTimer + field CanKit.Pro.CANopen.Pdo.TpdoTransmission Synchronous +class CanKit.Pro.CANopen.RpdoReceivedEventArgs + ctor (System.Int32 pdoIndex, System.UInt32 cobId, System.Byte[] payload) + prop System.Byte[] Payload {get} + prop System.Int32 PdoIndex {get} + prop System.UInt32 CobId {get} +enum CanKit.Pro.CANopen.Sdo.SdoAbortCode : UInt32 + field CanKit.Pro.CANopen.Sdo.SdoAbortCode AttemptReadWriteOnly + field CanKit.Pro.CANopen.Sdo.SdoAbortCode AttemptWriteReadOnly + field CanKit.Pro.CANopen.Sdo.SdoAbortCode CommandSpecifierInvalid + field CanKit.Pro.CANopen.Sdo.SdoAbortCode CrcError + field CanKit.Pro.CANopen.Sdo.SdoAbortCode DataTypeLengthMismatch + field CanKit.Pro.CANopen.Sdo.SdoAbortCode General + field CanKit.Pro.CANopen.Sdo.SdoAbortCode InvalidBlockSize + field CanKit.Pro.CANopen.Sdo.SdoAbortCode LengthTooHigh + field CanKit.Pro.CANopen.Sdo.SdoAbortCode LengthTooLow + field CanKit.Pro.CANopen.Sdo.SdoAbortCode ObjectCannotBeMapped + field CanKit.Pro.CANopen.Sdo.SdoAbortCode ObjectDoesNotExist + field CanKit.Pro.CANopen.Sdo.SdoAbortCode OutOfMemory + field CanKit.Pro.CANopen.Sdo.SdoAbortCode PdoMappingLengthExceeded + field CanKit.Pro.CANopen.Sdo.SdoAbortCode SdoProtocolTimedOut + field CanKit.Pro.CANopen.Sdo.SdoAbortCode SubIndexDoesNotExist + field CanKit.Pro.CANopen.Sdo.SdoAbortCode ToggleBitNotAlternated + field CanKit.Pro.CANopen.Sdo.SdoAbortCode UnsupportedAccess +class CanKit.Pro.CANopen.Sdo.SdoAbortException + ctor (System.UInt16 index, System.Byte subindex, CanKit.Pro.CANopen.Sdo.SdoAbortCode abortCode) + ctor (System.UInt16 index, System.Byte subindex, System.UInt32 abortCode, System.String message) + prop System.Byte Subindex {get} + prop System.UInt16 Index {get} + prop System.UInt32 AbortCode {get} +enum CanKit.Pro.CANopen.Sdo.SdoTransferMode : Int32 + field CanKit.Pro.CANopen.Sdo.SdoTransferMode Auto + field CanKit.Pro.CANopen.Sdo.SdoTransferMode Block + field CanKit.Pro.CANopen.Sdo.SdoTransferMode Expedited + field CanKit.Pro.CANopen.Sdo.SdoTransferMode Segmented +class CanKit.Pro.CANopen.SyncReceivedEventArgs + ctor (System.DateTime timestamp) + prop System.DateTime Timestamp {get} diff --git a/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.IsoTp.approved.txt b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.IsoTp.approved.txt new file mode 100644 index 0000000..4346679 --- /dev/null +++ b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.IsoTp.approved.txt @@ -0,0 +1,135 @@ +enum CanKit.Pro.IsoTp.FlowStatus : Byte + field CanKit.Pro.IsoTp.FlowStatus ClearToSend + field CanKit.Pro.IsoTp.FlowStatus Overflow + field CanKit.Pro.IsoTp.FlowStatus Wait +interface CanKit.Pro.IsoTp.IIsoTpChannel + event System.EventHandler DatagramReceived + event System.EventHandler BackgroundExceptionOccurred + method System.Collections.Generic.IAsyncEnumerable ReceiveAllAsync(System.Threading.CancellationToken cancellationToken) + method System.Int32 DiscardPendingPdus() + method System.Threading.Tasks.Task SendAsync(System.ReadOnlyMemory pdu, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task ReceiveAsync(System.Threading.CancellationToken cancellationToken) + prop CanKit.Pro.IsoTp.IsoTpChannelOptions Options {get} + prop CanKit.Pro.IsoTp.IsoTpEndpoint Endpoint {get} +class CanKit.Pro.IsoTp.IsoTp + method CanKit.Pro.IsoTp.IIsoTpChannel Open(CanKit.Abstractions.API.Can.ICanBus bus, CanKit.Pro.IsoTp.IsoTpEndpoint endpoint, CanKit.Pro.IsoTp.IsoTpChannelOptions options) + method CanKit.Pro.IsoTp.IIsoTpChannel Open(CanKit.Pro.RawCan.ICanBusService service, CanKit.Pro.IsoTp.IsoTpEndpoint endpoint, CanKit.Pro.IsoTp.IsoTpChannelOptions options, System.Boolean leaveOpen) + method CanKit.Pro.IsoTp.IsoTpFunctionalClient OpenFunctional(CanKit.Abstractions.API.Can.ICanBus bus, System.UInt32 functionalTxCanId, System.UInt32 responseRxCanIdRangeStart, System.UInt32 responseRxCanIdRangeEnd, CanKit.Pro.IsoTp.IsoTpFunctionalOptions options) + method CanKit.Pro.IsoTp.IsoTpFunctionalClient OpenFunctional(CanKit.Pro.RawCan.ICanBusService service, System.UInt32 functionalTxCanId, System.UInt32 responseRxCanIdRangeStart, System.UInt32 responseRxCanIdRangeEnd, CanKit.Pro.IsoTp.IsoTpFunctionalOptions options, System.Boolean leaveOpen) +enum CanKit.Pro.IsoTp.IsoTpAddressingMode : Int32 + field CanKit.Pro.IsoTp.IsoTpAddressingMode Extended + field CanKit.Pro.IsoTp.IsoTpAddressingMode Mixed + field CanKit.Pro.IsoTp.IsoTpAddressingMode Normal + field CanKit.Pro.IsoTp.IsoTpAddressingMode NormalFixed +class CanKit.Pro.IsoTp.IsoTpChannelOptions + ctor () + field System.TimeSpan DefaultTimeout + method CanKit.Pro.IsoTp.IsoTpChannelOptions With(System.Nullable useCanFd, System.Nullable usePadding, System.Nullable paddingByte, System.Nullable localBlockSize, System.Nullable localStMin, System.Nullable nAs, System.Nullable nBs, System.Nullable nCr, System.Nullable wftMax, System.Nullable receiveBufferCapacity) + prop System.Boolean UseCanFd {get/set} + prop System.Boolean UsePadding {get/set} + prop System.Byte LocalBlockSize {get/set} + prop System.Byte PaddingByte {get/set} + prop System.Int32 ReceiveBufferCapacity {get/set} + prop System.Int32 WftMax {get/set} + prop System.TimeSpan LocalStMin {get/set} + prop System.TimeSpan NAs {get/set} + prop System.TimeSpan NBs {get/set} + prop System.TimeSpan NCr {get/set} +class CanKit.Pro.IsoTp.IsoTpDatagramReceivedEventArgs + ctor (CanKit.Pro.IsoTp.IsoTpEndpoint endpoint, System.Byte[] data) + prop CanKit.Pro.IsoTp.IsoTpEndpoint Endpoint {get} + prop System.Byte[] Data {get} +struct CanKit.Pro.IsoTp.IsoTpEndpoint + method CanKit.Pro.IsoTp.IsoTpEndpoint Extended(System.UInt32 txCanId, System.UInt32 rxCanId, System.Byte sourceAddress, System.Byte targetAddress, System.Boolean isExtendedCanId) + method CanKit.Pro.IsoTp.IsoTpEndpoint Mixed(System.UInt32 txCanId, System.UInt32 rxCanId, System.Byte addressExtension, System.Boolean isExtendedCanId) + method CanKit.Pro.IsoTp.IsoTpEndpoint Normal(System.UInt32 txCanId, System.UInt32 rxCanId, System.Boolean isExtendedCanId) + method CanKit.Pro.IsoTp.IsoTpEndpoint NormalFixed(System.UInt32 txCanId, System.UInt32 rxCanId) + method System.Boolean Equals(CanKit.Pro.IsoTp.IsoTpEndpoint other) + method System.Boolean Equals(System.Object obj) + method System.Int32 GetHashCode() + prop CanKit.Pro.IsoTp.IsoTpAddressingMode AddressingMode {get} + prop System.Boolean IsExtendedCanId {get} + prop System.Boolean UsesAddressExtension {get} + prop System.Byte AddressExtension {get} + prop System.Byte RxAddressExtension {get} + prop System.Int32 AddressExtensionSize {get} + prop System.UInt32 RxCanId {get} + prop System.UInt32 TxCanId {get} +class CanKit.Pro.IsoTp.IsoTpException + ctor (System.String message) + ctor (System.String message, System.Exception innerException) +class CanKit.Pro.IsoTp.IsoTpFrameCodec + field System.Byte DefaultPaddingByte + field System.Byte FirstConsecutiveSequenceNumber + field System.Int32 CanFdMaxData + field System.Int32 ClassicCanMaxData + field System.Int32 MaxClassicFirstFrameLength + field System.Int32 SequenceNumberModulus + field System.UInt32 MaxFdFirstFrameLength + method System.Boolean TryParsePci(System.ReadOnlySpan canPayload, CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, System.Boolean isCanFd, CanKit.Pro.IsoTp.Pci& pci) + method System.Byte EncodeStMin(System.TimeSpan value) + method System.Byte NextConsecutiveSequenceNumber(System.Byte current) + method System.Byte[] BuildConsecutiveFrame(CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, System.Byte sequenceNumber, System.ReadOnlySpan chunk, System.Boolean isCanFd, System.Boolean padding, System.Byte paddingByte) + method System.Byte[] BuildFirstFrame(CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, System.Int32 totalLength, System.ReadOnlySpan firstChunk, System.Boolean isCanFd) + method System.Byte[] BuildFlowControl(CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, CanKit.Pro.IsoTp.FlowStatus flowStatus, System.Byte blockSize, System.Byte stMinRaw, System.Boolean isCanFd, System.Boolean padding, System.Byte paddingByte) + method System.Byte[] BuildSingleFrame(CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, System.ReadOnlySpan userData, System.Boolean isCanFd, System.Boolean padding, System.Byte paddingByte) + method System.Int32 BuildConsecutiveFrame(System.Span destination, CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, System.Byte sequenceNumber, System.ReadOnlySpan chunk, System.Boolean isCanFd, System.Boolean padding, System.Byte paddingByte) + method System.Int32 BuildFirstFrame(System.Span destination, CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, System.Int32 totalLength, System.ReadOnlySpan firstChunk, System.Boolean isCanFd) + method System.Int32 BuildFlowControl(System.Span destination, CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, CanKit.Pro.IsoTp.FlowStatus flowStatus, System.Byte blockSize, System.Byte stMinRaw, System.Boolean isCanFd, System.Boolean padding, System.Byte paddingByte) + method System.Int32 BuildSingleFrame(System.Span destination, CanKit.Pro.IsoTp.IsoTpEndpoint& endpoint, System.ReadOnlySpan userData, System.Boolean isCanFd, System.Boolean padding, System.Byte paddingByte) + method System.Int32 ConsecutiveFrameMaxDataLength(System.Boolean isCanFd, System.Boolean usesAddressExtension) + method System.Int32 FirstFrameMaxDataLength(System.Boolean isCanFd, System.Boolean usesAddressExtension, System.Boolean useLongLength) + method System.Int32 NextValidFrameLength(System.Int32 dataLength, System.Boolean isCanFd) + method System.Int32 SingleFrameMaxDataLength(System.Boolean isCanFd, System.Boolean usesAddressExtension) + method System.Int32 SingleFrameShortFormMaxDataLength(System.Boolean usesAddressExtension) + method System.TimeSpan DecodeStMin(System.Byte raw) +class CanKit.Pro.IsoTp.IsoTpFunctionalClient + method System.Threading.Tasks.Task SendAsync(System.ReadOnlyMemory pdu, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task> CollectResponsesAsync(System.TimeSpan window, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task> SendAndCollectAsync(System.ReadOnlyMemory pdu, System.TimeSpan window, System.Threading.CancellationToken cancellationToken) + method System.Void Dispose() + prop CanKit.Pro.IsoTp.IsoTpFunctionalOptions Options {get} + prop System.UInt32 FunctionalTxCanId {get} +class CanKit.Pro.IsoTp.IsoTpFunctionalOptions + ctor () + field System.TimeSpan DefaultTimeout + prop System.Boolean IsExtendedCanId {get/set} + prop System.Boolean UseCanFd {get/set} + prop System.Boolean UsePadding {get/set} + prop System.Byte PaddingByte {get/set} + prop System.TimeSpan NAs {get/set} +class CanKit.Pro.IsoTp.IsoTpFunctionalResponse + prop System.Byte[] Data {get} + prop System.UInt32 SourceCanId {get} +class CanKit.Pro.IsoTp.IsoTpOverflowException + ctor (System.String message) +class CanKit.Pro.IsoTp.IsoTpSendRejectedException + ctor (System.String message) +class CanKit.Pro.IsoTp.IsoTpTimeoutException + ctor (CanKit.Pro.IsoTp.IsoTpTimer timer, System.String message) + prop CanKit.Pro.IsoTp.IsoTpTimer Timer {get} +enum CanKit.Pro.IsoTp.IsoTpTimer : Int32 + field CanKit.Pro.IsoTp.IsoTpTimer NAs + field CanKit.Pro.IsoTp.IsoTpTimer NBs + field CanKit.Pro.IsoTp.IsoTpTimer NCr +class CanKit.Pro.IsoTp.IsoTpWaitFrameLimitExceededException + ctor (System.Int32 received, System.Int32 limit) + prop System.Int32 Limit {get} + prop System.Int32 WaitFramesReceived {get} +struct CanKit.Pro.IsoTp.Pci + method System.Boolean Equals(CanKit.Pro.IsoTp.Pci other) + method System.Boolean Equals(System.Object obj) + method System.Int32 GetHashCode() + prop CanKit.Pro.IsoTp.FlowStatus FlowStatus {get} + prop CanKit.Pro.IsoTp.PciType Type {get} + prop System.Byte BlockSize {get} + prop System.Byte SequenceNumber {get} + prop System.Byte StMinRaw {get} + prop System.Int32 DataOffset {get} + prop System.Int32 Length {get} + prop System.TimeSpan StMin {get} +enum CanKit.Pro.IsoTp.PciType : Byte + field CanKit.Pro.IsoTp.PciType ConsecutiveFrame + field CanKit.Pro.IsoTp.PciType FirstFrame + field CanKit.Pro.IsoTp.PciType FlowControl + field CanKit.Pro.IsoTp.PciType SingleFrame diff --git a/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939.approved.txt b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939.approved.txt new file mode 100644 index 0000000..4de366c --- /dev/null +++ b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939.approved.txt @@ -0,0 +1,84 @@ +interface CanKit.Pro.J1939.IJ1939Node + event System.EventHandler AddressClaimChanged + event System.EventHandler MessageReceived + event System.EventHandler BackgroundExceptionOccurred + method System.IDisposable StartPeriodicSend(CanKit.Pro.J1939.J1939Message message, System.TimeSpan period) + method System.Threading.Tasks.Task ClaimAddressAsync(System.Byte preferredAddress, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task RequestPgnAsync(System.UInt32 requestedPgn, System.Byte destinationAddress, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SendAsync(CanKit.Pro.J1939.J1939Message message, System.Threading.CancellationToken cancellationToken) + prop CanKit.Pro.Addressing.J1939Name Name {get} + prop CanKit.Pro.J1939.J1939ClaimState ClaimState {get} + prop CanKit.Pro.J1939.J1939NodeOptions Options {get} + prop System.Nullable Address {get} +class CanKit.Pro.J1939.J1939CannotClaimException + ctor (System.Byte preferredAddress) + prop System.Byte PreferredAddress {get} +class CanKit.Pro.J1939.J1939ClaimEventArgs + ctor (CanKit.Pro.J1939.J1939ClaimState state, System.Nullable address, System.Nullable contendingSourceAddress, System.Nullable contendingName) + prop CanKit.Pro.J1939.J1939ClaimState State {get} + prop System.Nullable ContendingName {get} + prop System.Nullable Address {get} + prop System.Nullable ContendingSourceAddress {get} +enum CanKit.Pro.J1939.J1939ClaimState : Int32 + field CanKit.Pro.J1939.J1939ClaimState CannotClaim + field CanKit.Pro.J1939.J1939ClaimState Claimed + field CanKit.Pro.J1939.J1939ClaimState Claiming + field CanKit.Pro.J1939.J1939ClaimState NotClaimed +struct CanKit.Pro.J1939.J1939Message + ctor (System.UInt32 pgn, System.ReadOnlyMemory payload, System.Byte priority, System.Byte sourceAddress, System.Byte destinationAddress) + method System.Boolean Equals(CanKit.Pro.J1939.J1939Message other) + method System.Boolean Equals(System.Object obj) + method System.Int32 GetHashCode() + prop System.Boolean WasMultiFrame {get} + prop System.Byte DestinationAddress {get} + prop System.Byte Priority {get} + prop System.Byte SourceAddress {get} + prop System.ReadOnlyMemory Payload {get} + prop System.UInt32 Pgn {get} +class CanKit.Pro.J1939.J1939NoAddressException + ctor () +class CanKit.Pro.J1939.J1939Node + method CanKit.Pro.J1939.IJ1939Node Open(CanKit.Abstractions.API.Can.ICanBus bus, CanKit.Pro.J1939.J1939NodeOptions options) + method CanKit.Pro.J1939.IJ1939Node Open(CanKit.Pro.RawCan.ICanBusService service, CanKit.Pro.J1939.J1939NodeOptions options, System.Boolean leaveOpen) +class CanKit.Pro.J1939.J1939NodeException + ctor (System.String message) + ctor (System.String message, System.Exception innerException) +class CanKit.Pro.J1939.J1939NodeOptions + ctor (CanKit.Pro.Addressing.J1939Name name) + prop CanKit.Pro.Addressing.J1939Name Name {get} + prop CanKit.Pro.J1939Tp.J1939TpOptions TransportOptions {get/set} + prop System.Byte ClaimPriority {get/set} + prop System.Byte DefaultPriority {get/set} + prop System.Int32 ReceiveBufferCapacity {get/set} + prop System.Nullable EnableArbitraryAddressClaiming {get/set} + prop System.TimeSpan ClaimAnnounceTimeout {get/set} +class CanKit.Pro.J1939.J1939Spn + method System.Double Extract(System.ReadOnlySpan payload, System.Int32 byteOffset, System.Int32 bitLength, System.Double resolution, System.Double offset) + method System.Double Extract(System.ReadOnlySpan payload, System.Int32 byteOffset, System.Int32 startBit, System.Int32 bitLength, System.Double resolution, System.Double offset) + method System.UInt64 ExtractRaw(System.ReadOnlySpan payload, System.Int32 byteOffset, System.Int32 startBit, System.Int32 bitLength) + method System.Void WriteRaw(System.Span payload, System.Int32 byteOffset, System.Int32 startBit, System.Int32 bitLength, System.UInt64 rawValue) +class CanKit.Pro.J1939.J1939SpnCatalog + ctor () + method CanKit.Pro.J1939.J1939SpnCatalog Register(CanKit.Pro.J1939.J1939SpnDefinition definition) + method System.Boolean TryGet(System.Int32 spn, CanKit.Pro.J1939.J1939SpnDefinition& definition) + method System.Collections.Generic.IEnumerator GetEnumerator() + method System.Double Extract(System.ReadOnlySpan payload, System.Int32 spn) + prop CanKit.Pro.J1939.J1939SpnCatalog Default {get} +class CanKit.Pro.J1939.J1939SpnDefinition + ctor (System.Int32 Spn, System.String Name, System.UInt32 Pgn, System.Int32 ByteOffset, System.Int32 StartBit, System.Int32 BitLength, System.Double Resolution, System.Double Offset, System.String Unit) + method CanKit.Pro.J1939.J1939SpnDefinition $() + method System.Boolean Equals(CanKit.Pro.J1939.J1939SpnDefinition other) + method System.Boolean Equals(System.Object obj) + method System.Double Extract(System.ReadOnlySpan payload) + method System.Int32 GetHashCode() + method System.String ToString() + method System.Void Deconstruct(System.Int32& Spn, System.String& Name, System.UInt32& Pgn, System.Int32& ByteOffset, System.Int32& StartBit, System.Int32& BitLength, System.Double& Resolution, System.Double& Offset, System.String& Unit) + prop System.Double Offset {get/set} + prop System.Double Resolution {get/set} + prop System.Int32 BitLength {get/set} + prop System.Int32 ByteOffset {get/set} + prop System.Int32 Spn {get/set} + prop System.Int32 StartBit {get/set} + prop System.String Name {get/set} + prop System.String Unit {get/set} + prop System.UInt32 Pgn {get/set} diff --git a/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939Tp.approved.txt b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939Tp.approved.txt new file mode 100644 index 0000000..92bdb28 --- /dev/null +++ b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.J1939Tp.approved.txt @@ -0,0 +1,53 @@ +interface CanKit.Pro.J1939Tp.IJ1939TpChannel + event System.EventHandler DatagramReceived + event System.EventHandler BackgroundExceptionOccurred + method System.Collections.Generic.IAsyncEnumerable ReceiveAllAsync(System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SendBamAsync(System.UInt32 pgn, System.ReadOnlyMemory payload, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SendCmAsync(System.UInt32 pgn, System.Byte destinationAddress, System.ReadOnlyMemory payload, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task ReceiveAsync(System.Threading.CancellationToken cancellationToken) + prop CanKit.Pro.J1939Tp.J1939TpOptions Options {get} + prop System.Byte SourceAddress {get} +class CanKit.Pro.J1939Tp.J1939Tp + method CanKit.Pro.J1939Tp.IJ1939TpChannel Open(CanKit.Abstractions.API.Can.ICanBus bus, System.Byte sourceAddress, CanKit.Pro.J1939Tp.J1939TpOptions options) + method CanKit.Pro.J1939Tp.IJ1939TpChannel Open(CanKit.Pro.RawCan.ICanBusService service, System.Byte sourceAddress, CanKit.Pro.J1939Tp.J1939TpOptions options, System.Boolean leaveOpen) +class CanKit.Pro.J1939Tp.J1939TpAbortException + ctor (CanKit.Pro.J1939Tp.J1939TpAbortReason reason, System.UInt32 pgn, System.String message) + prop CanKit.Pro.J1939Tp.J1939TpAbortReason Reason {get} + prop System.UInt32 Pgn {get} +enum CanKit.Pro.J1939Tp.J1939TpAbortReason : Byte + field CanKit.Pro.J1939Tp.J1939TpAbortReason NoResourcesAvailable + field CanKit.Pro.J1939Tp.J1939TpAbortReason ReceiverAbort + field CanKit.Pro.J1939Tp.J1939TpAbortReason ResourceNeededForHigherPriorityProcess + field CanKit.Pro.J1939Tp.J1939TpAbortReason RetransmitNotSupported + field CanKit.Pro.J1939Tp.J1939TpAbortReason SessionAlreadyOpen + field CanKit.Pro.J1939Tp.J1939TpAbortReason Timeout + field CanKit.Pro.J1939Tp.J1939TpAbortReason UnexpectedCtsNumPackets + field CanKit.Pro.J1939Tp.J1939TpAbortReason UnexpectedCtsSequenceNumber + field CanKit.Pro.J1939Tp.J1939TpAbortReason Unknown +struct CanKit.Pro.J1939Tp.J1939TpDatagram + ctor (System.UInt32 pgn, System.Byte sourceAddress, System.Byte destinationAddress, CanKit.Pro.J1939Tp.J1939TpKind kind, System.Byte[] payload) + prop CanKit.Pro.J1939Tp.J1939TpKind Kind {get} + prop System.Byte DestinationAddress {get} + prop System.Byte SourceAddress {get} + prop System.Byte[] Payload {get} + prop System.UInt32 Pgn {get} +class CanKit.Pro.J1939Tp.J1939TpException + ctor (System.String message) + ctor (System.String message, System.Exception innerException) +enum CanKit.Pro.J1939Tp.J1939TpKind : Byte + field CanKit.Pro.J1939Tp.J1939TpKind Bam + field CanKit.Pro.J1939Tp.J1939TpKind Cm +class CanKit.Pro.J1939Tp.J1939TpOptions + ctor () + method CanKit.Pro.J1939Tp.J1939TpOptions With(System.Nullable t1, System.Nullable t2, System.Nullable t3, System.Nullable t4, System.Nullable tr, System.Nullable th, System.Nullable priority, System.Nullable maxPacketsPerCts, System.Nullable receiveBufferCapacity) + prop System.Byte MaxPacketsPerCts {get/set} + prop System.Byte Priority {get/set} + prop System.Int32 ReceiveBufferCapacity {get/set} + prop System.TimeSpan T1 {get/set} + prop System.TimeSpan T2 {get/set} + prop System.TimeSpan T3 {get/set} + prop System.TimeSpan T4 {get/set} + prop System.TimeSpan Th {get/set} + prop System.TimeSpan Tr {get/set} +class CanKit.Pro.J1939Tp.J1939TpSendRejectedException + ctor (System.String message) diff --git a/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.Uds.approved.txt b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.Uds.approved.txt new file mode 100644 index 0000000..ab07157 --- /dev/null +++ b/tests/CanKit.Pro.Tests/ApiApprovals/CanKit.Pro.Uds.approved.txt @@ -0,0 +1,108 @@ +interface CanKit.Pro.Uds.IUdsClient + method System.IDisposable StartTesterPresentKeepAlive(System.Nullable period) + method System.Threading.Tasks.Task DownloadAsync(System.Byte dataFormatIdentifier, System.Byte addressAndLengthFormatIdentifier, System.ReadOnlyMemory memoryAddress, System.ReadOnlyMemory memorySize, System.ReadOnlyMemory data, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task RequestTransferExitAsync(System.ReadOnlyMemory transferRequestParameterRecord, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SecurityAccessAsync(System.Byte requestSeedLevel, System.Func computeKey, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task TesterPresentAsync(System.Boolean suppressPositiveResponse, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task WriteDataByIdentifierAsync(System.UInt16 dataIdentifier, System.ReadOnlyMemory data, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task RequestDownloadAsync(System.Byte dataFormatIdentifier, System.Byte addressAndLengthFormatIdentifier, System.ReadOnlyMemory memoryAddress, System.ReadOnlyMemory memorySize, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task RequestUploadAsync(System.Byte dataFormatIdentifier, System.Byte addressAndLengthFormatIdentifier, System.ReadOnlyMemory memoryAddress, System.ReadOnlyMemory memorySize, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task DiagnosticSessionControlAsync(CanKit.Pro.Uds.UdsSessionType session, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task DiagnosticSessionControlAsync(System.Byte sessionType, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task EcuResetAsync(CanKit.Pro.Uds.UdsEcuResetType resetType, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task ReadDataByIdentifierAsync(System.UInt16 dataIdentifier, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task RoutineControlAsync(CanKit.Pro.Uds.UdsRoutineControlType routineType, System.UInt16 routineIdentifier, System.ReadOnlyMemory routineControlOptionRecord, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task SendRawAsync(System.ReadOnlyMemory request, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task TransferDataAsync(System.Byte blockSequenceCounter, System.ReadOnlyMemory data, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task UploadAsync(System.Byte dataFormatIdentifier, System.Byte addressAndLengthFormatIdentifier, System.ReadOnlyMemory memoryAddress, System.ReadOnlyMemory memorySize, System.Threading.CancellationToken cancellationToken) + method System.Threading.Tasks.Task> ReadDataByIdentifierAsync(System.Collections.Generic.IReadOnlyList dataIdentifiers, System.Collections.Generic.IReadOnlyDictionary dataRecordLengths, System.Threading.CancellationToken cancellationToken) + prop CanKit.Pro.IsoTp.IIsoTpChannel Channel {get} + prop CanKit.Pro.Uds.UdsClientOptions Options {get} + prop System.Byte CurrentSession {get} +class CanKit.Pro.Uds.UdsClient + method CanKit.Pro.Uds.IUdsClient Create(CanKit.Pro.IsoTp.IIsoTpChannel channel, CanKit.Pro.Uds.UdsClientOptions options, System.Boolean leaveOpen) +class CanKit.Pro.Uds.UdsClientOptions + ctor () + field System.TimeSpan DefaultP2 + field System.TimeSpan DefaultP2Star + field System.TimeSpan DefaultTesterPresentPeriod + method CanKit.Pro.Uds.UdsClientOptions With(System.Nullable p2ClientMax, System.Nullable p2StarClientMax, System.Nullable maxResponsePendingCount, System.Nullable testerPresentPeriod, System.Nullable keepAliveSuppressPositiveResponse) + prop System.Boolean KeepAliveSuppressPositiveResponse {get/set} + prop System.Int32 MaxResponsePendingCount {get/set} + prop System.TimeSpan P2ClientMax {get/set} + prop System.TimeSpan P2StarClientMax {get/set} + prop System.TimeSpan TesterPresentPeriod {get/set} +struct CanKit.Pro.Uds.UdsDownloadResponse + ctor (System.Byte lengthFormatIdentifier, System.UInt64 maxNumberOfBlockLength) + prop System.Byte LengthFormatIdentifier {get} + prop System.UInt64 MaxNumberOfBlockLength {get} +enum CanKit.Pro.Uds.UdsEcuResetType : Byte + field CanKit.Pro.Uds.UdsEcuResetType DisableRapidPowerShutDown + field CanKit.Pro.Uds.UdsEcuResetType EnableRapidPowerShutDown + field CanKit.Pro.Uds.UdsEcuResetType HardReset + field CanKit.Pro.Uds.UdsEcuResetType KeyOffOnReset + field CanKit.Pro.Uds.UdsEcuResetType SoftReset +class CanKit.Pro.Uds.UdsException + ctor (System.String message) + ctor (System.String message, System.Exception innerException) +enum CanKit.Pro.Uds.UdsNegativeResponseCode : Byte + field CanKit.Pro.Uds.UdsNegativeResponseCode BusyRepeatRequest + field CanKit.Pro.Uds.UdsNegativeResponseCode ConditionsNotCorrect + field CanKit.Pro.Uds.UdsNegativeResponseCode ExceedNumberOfAttempts + field CanKit.Pro.Uds.UdsNegativeResponseCode GeneralProgrammingFailure + field CanKit.Pro.Uds.UdsNegativeResponseCode GeneralReject + field CanKit.Pro.Uds.UdsNegativeResponseCode IncorrectMessageLengthOrInvalidFormat + field CanKit.Pro.Uds.UdsNegativeResponseCode InvalidKey + field CanKit.Pro.Uds.UdsNegativeResponseCode RequestCorrectlyReceivedResponsePending + field CanKit.Pro.Uds.UdsNegativeResponseCode RequestOutOfRange + field CanKit.Pro.Uds.UdsNegativeResponseCode RequestSequenceError + field CanKit.Pro.Uds.UdsNegativeResponseCode RequiredTimeDelayNotExpired + field CanKit.Pro.Uds.UdsNegativeResponseCode SecurityAccessDenied + field CanKit.Pro.Uds.UdsNegativeResponseCode ServiceNotSupported + field CanKit.Pro.Uds.UdsNegativeResponseCode ServiceNotSupportedInActiveSession + field CanKit.Pro.Uds.UdsNegativeResponseCode SubFunctionNotSupported + field CanKit.Pro.Uds.UdsNegativeResponseCode SubFunctionNotSupportedInActiveSession + field CanKit.Pro.Uds.UdsNegativeResponseCode TransferDataSuspended + field CanKit.Pro.Uds.UdsNegativeResponseCode UploadDownloadNotAccepted + field CanKit.Pro.Uds.UdsNegativeResponseCode WrongBlockSequenceCounter +class CanKit.Pro.Uds.UdsNegativeResponseException + ctor (CanKit.Pro.Uds.UdsServiceId requestedService, System.Byte code) + prop CanKit.Pro.Uds.UdsServiceId RequestedService {get} + prop System.Byte Code {get} + prop System.Nullable CodeAsEnum {get} + prop System.String CodeName {get} +class CanKit.Pro.Uds.UdsProtocolException + ctor (System.String message) +enum CanKit.Pro.Uds.UdsRoutineControlType : Byte + field CanKit.Pro.Uds.UdsRoutineControlType RequestRoutineResults + field CanKit.Pro.Uds.UdsRoutineControlType StartRoutine + field CanKit.Pro.Uds.UdsRoutineControlType StopRoutine +enum CanKit.Pro.Uds.UdsServiceId : Byte + field CanKit.Pro.Uds.UdsServiceId DiagnosticSessionControl + field CanKit.Pro.Uds.UdsServiceId EcuReset + field CanKit.Pro.Uds.UdsServiceId ReadDataByIdentifier + field CanKit.Pro.Uds.UdsServiceId RequestDownload + field CanKit.Pro.Uds.UdsServiceId RequestTransferExit + field CanKit.Pro.Uds.UdsServiceId RequestUpload + field CanKit.Pro.Uds.UdsServiceId RoutineControl + field CanKit.Pro.Uds.UdsServiceId SecurityAccess + field CanKit.Pro.Uds.UdsServiceId TesterPresent + field CanKit.Pro.Uds.UdsServiceId TransferData + field CanKit.Pro.Uds.UdsServiceId WriteDataByIdentifier +enum CanKit.Pro.Uds.UdsSessionType : Byte + field CanKit.Pro.Uds.UdsSessionType Default + field CanKit.Pro.Uds.UdsSessionType Extended + field CanKit.Pro.Uds.UdsSessionType Programming + field CanKit.Pro.Uds.UdsSessionType SafetySystem +class CanKit.Pro.Uds.UdsTimeoutException + ctor (CanKit.Pro.Uds.UdsServiceId requestedService, CanKit.Pro.Uds.UdsTimeoutTimer timer, System.TimeSpan elapsed) + prop CanKit.Pro.Uds.UdsServiceId RequestedService {get} + prop CanKit.Pro.Uds.UdsTimeoutTimer Timer {get} + prop System.TimeSpan Elapsed {get} +enum CanKit.Pro.Uds.UdsTimeoutTimer : Int32 + field CanKit.Pro.Uds.UdsTimeoutTimer P2 + field CanKit.Pro.Uds.UdsTimeoutTimer P2Star +struct CanKit.Pro.Uds.UdsUploadResponse + ctor (System.Byte lengthFormatIdentifier, System.UInt64 maxNumberOfBlockLength) + prop System.Byte LengthFormatIdentifier {get} + prop System.UInt64 MaxNumberOfBlockLength {get} diff --git a/tests/CanKit.Pro.Tests/CanKit.Pro.Tests.csproj b/tests/CanKit.Pro.Tests/CanKit.Pro.Tests.csproj index 6d21fe4..fed7978 100644 --- a/tests/CanKit.Pro.Tests/CanKit.Pro.Tests.csproj +++ b/tests/CanKit.Pro.Tests/CanKit.Pro.Tests.csproj @@ -21,7 +21,6 @@ - diff --git a/tests/CanKit.Pro.Tests/TestCases/HaweFrameworkTests.cs b/tests/CanKit.Pro.Tests/TestCases/HaweFrameworkTests.cs deleted file mode 100644 index 9fb591d..0000000 --- a/tests/CanKit.Pro.Tests/TestCases/HaweFrameworkTests.cs +++ /dev/null @@ -1,785 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using CanKit.Abstractions.API.Can; -using CanKit.Abstractions.API.Can.Definitions; -using CanKit.Abstractions.API.Common.Definitions; -using CanKit.Core; -using CanKit.Core.Definitions; -using CanKit.Pro.Actor; -using CanKit.Pro.Hawe; -using CanKit.Pro.RawCan; -using CanKit.Pro.Tests.Infrastructure; -using FluentAssertions; -using Xunit; - -namespace CanKit.Pro.Tests.TestCases; - -/// -/// Verifies the generic public HAWE extension framework (CanKit.Pro.Hawe, -/// SRS FR-HAWE-001..005) against the Virtual adapter. Uses a deliberately generic -/// that carries no HAWE-specific behaviour -- the framework -/// itself must never require, or expose, proprietary HAWE protocol details (SRS CON-006 / A-6). -/// -public class HaweFrameworkTests : IClassFixture -{ - private static string NewSession() => $"hawe-{Guid.NewGuid():N}"; - - private static ICanBus Open(string session, int channel) => CanBus.Open( - $"virtual://{session}/{channel}", - cfg => cfg.SetProtocolMode(CanProtocolMode.Can20).Baud(VirtualAdapterFixture.Bitrate)); - - private static readonly TimeSpan ShortTimeout = TimeSpan.FromSeconds(2); - - // A deliberately generic codec used only in the test project: it selects one CAN ID range, - // counts callbacks, echoes back a fixed byte, and lets the test drive the session skeleton - // through IHaweCodecHost.SetSessionState. It contains no HAWE-specific frame layout, service - // id, or state machine -- the framework itself must not require any of those (SRS CON-006). - private sealed class FakePatternCodec : IHaweCodec - { - private readonly TaskCompletionSource _attachedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); - public FakePatternCodec(HaweFramePattern pattern) => FramePattern = pattern; - - public string Name => "fake-pattern-codec"; - public HaweFramePattern FramePattern { get; } - public IHaweCodecHost? Host { get; private set; } - - public ConcurrentQueue Received { get; } = new(); - public int DetachedCount => Volatile.Read(ref _detached); - public List<(HaweSessionState prev, HaweSessionState curr)> Transitions { get; } = new(); - public Task Attached => _attachedTcs.Task; - - private int _detached; - - public void OnAttached(IHaweCodecHost host) - { - Host = host; - _attachedTcs.TrySetResult(true); - } - - public void OnFrameReceived(in CanFrameView frame) => Received.Enqueue(frame); - - public void OnSessionStateChanged(HaweSessionState previous, HaweSessionState current) - => Transitions.Add((previous, current)); - - public void OnDetached() => Interlocked.Increment(ref _detached); - } - - // Convenience helper: waits (bounded) until the codec has captured `count` frames. - private static async Task WaitForFrames(FakePatternCodec codec, int count, TimeSpan timeout) - { - var deadline = DateTime.UtcNow + timeout; - while (codec.Received.Count < count && DateTime.UtcNow < deadline) - await Task.Delay(10); - } - - // FR-HAWE-001 verification criterion: a codec registered via the public SPI is discoverable - // by name and the registry hands back a fresh instance per Create call. - [Fact] - public void Registry_Registers_And_Creates_Codec_By_Name() - { - var registry = new HaweCodecRegistry(); - var pattern = HaweFramePattern.Range(0x700, 0x7FF); - - registry.IsRegistered("fake-pattern-codec").Should().BeFalse(); - registry.Register("fake-pattern-codec", () => new FakePatternCodec(pattern)); - - registry.IsRegistered("fake-pattern-codec").Should().BeTrue(); - registry.RegisteredNames.Should().Contain("fake-pattern-codec"); - - var codec1 = registry.Create("fake-pattern-codec"); - var codec2 = registry.Create("fake-pattern-codec"); - codec1.Should().NotBeNull(); - codec2.Should().NotBeNull(); - codec1.Should().NotBeSameAs(codec2); - codec1.Name.Should().Be("fake-pattern-codec"); - } - - [Fact] - public void Registry_Throws_KeyNotFound_For_Unknown_Codec() - { - var registry = new HaweCodecRegistry(); - Action act = () => registry.Create("does-not-exist"); - act.Should().Throw(); - } - - [Fact] - public void Registry_Unregister_Removes_Factory() - { - var registry = new HaweCodecRegistry(); - registry.Register("x", () => new FakePatternCodec(HaweFramePattern.Range(0x100, 0x1FF))); - registry.Unregister("x").Should().BeTrue(); - registry.Unregister("x").Should().BeFalse(); - registry.IsRegistered("x").Should().BeFalse(); - } - - [Fact] - public void Registry_Rejects_Null_Or_Empty_Name_On_Register() - { - var registry = new HaweCodecRegistry(); - Action nullName = () => registry.Register(null!, () => new FakePatternCodec(HaweFramePattern.Range(0, 0))); - Action emptyName = () => registry.Register(string.Empty, () => new FakePatternCodec(HaweFramePattern.Range(0, 0))); - nullName.Should().Throw(); - emptyName.Should().Throw(); - } - - // FR-HAWE-002 verification criterion: a generic frame pattern is sent/received end-to-end - // over the Virtual adapter without the framework knowing anything about payload semantics. - [Fact] - public async Task Frame_Pattern_Delivers_Matching_Frames_And_Ignores_Others() - { - var session = NewSession(); - using var sender = Open(session, 0); - using var receiver = Open(session, 1); - using var service = new CanBusService(receiver); - - var codec = new FakePatternCodec(HaweFramePattern.Range(0x300, 0x3FF)); - using var channel = new HaweChannel(service, codec); - - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - channel.Codec.Should().BeSameAs(codec); - channel.SessionState.Should().Be(HaweSessionState.Idle); - - sender.Transmit(CanFrame.Classic(0x300, new byte[] { 0xA })); - sender.Transmit(CanFrame.Classic(0x3FF, new byte[] { 0xB })); - // Deliberately outside the pattern; must be ignored by this codec. - sender.Transmit(CanFrame.Classic(0x400, new byte[] { 0xC })); - - await WaitForFrames(codec, 2, ShortTimeout); - - codec.Received.Count.Should().Be(2); - codec.Received.Select(f => f.ID).Should().BeEquivalentTo(new[] { 0x300, 0x3FF }); - } - - // FR-HAWE-002 / FR-HAWE-003 verification: a codec can send frames back on the same bus via - // IHaweCodecHost.SendConfirmedAsync (built on ICanBusService's TX-confirm), i.e. the codec - // reaches the shared L2 services on the same terms as ISO-TP/J1939-TP. - [Fact] - public async Task Codec_Can_Send_Frames_Through_Host_On_Same_Bus() - { - var session = NewSession(); - using var busA = Open(session, 0); - using var busB = Open(session, 1); - using var service = new CanBusService(busA); - using var mirror = new CanBusService(busB); - - // A vanilla subscription on the *other* bus receives whatever the codec sends via the - // host: it lets the test observe the transmit end-to-end without inspecting internal - // service state. - using var observer = mirror.Subscribe(CanIdFilter.Range(0x555, 0x555)); - - var codec = new FakePatternCodec(HaweFramePattern.Range(0x100, 0x1FF)); - using var channel = new HaweChannel(service, codec); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - var frame = CanFrame.Classic(0x555, new byte[] { 1, 2, 3, 4 }); - var confirmation = await codec.Host!.SendConfirmedAsync(frame); - confirmation.Confirmed.Should().BeTrue(); - - using var cts = new CancellationTokenSource(ShortTimeout); - var received = new List(); - try - { - await foreach (var f in observer.Frames.WithCancellation(cts.Token)) - { - received.Add(f); - break; - } - } - catch (OperationCanceledException) { } - - received.Should().HaveCount(1); - received[0].ID.Should().Be(0x555); - received[0].Data.ToArray().Should().Equal(new byte[] { 1, 2, 3, 4 }); - } - - // FR-HAWE-004 verification: the session skeleton exists as a placeholder and can be driven - // by the codec through IHaweCodecHost.SetSessionState; the framework surfaces every - // transition to OnSessionStateChanged in order. - [Fact] - public async Task Session_Skeleton_Transitions_Are_Observed_By_Codec() - { - var session = NewSession(); - using var receiver = Open(session, 0); - using var service = new CanBusService(receiver); - - var codec = new FakePatternCodec(HaweFramePattern.Range(0x600, 0x6FF)); - using var channel = new HaweChannel(service, codec); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - channel.SessionState.Should().Be(HaweSessionState.Idle); - - codec.Host!.SetSessionState(HaweSessionState.Active).Should().BeTrue(); - codec.Host!.SetSessionState(HaweSessionState.Active).Should().BeFalse(); // no-op - codec.Host!.SetSessionState(HaweSessionState.Fault).Should().BeTrue(); - codec.Host!.SetSessionState(HaweSessionState.Idle).Should().BeTrue(); - - // Give the actor loop a beat to drain any posted callbacks (they are synchronous under - // PostAsync().GetAwaiter().GetResult(), but the visibility of `Transitions` mutations to - // this thread is what we wait for here). - await Task.Delay(50); - - codec.Transitions.Should().Equal(new[] - { - (HaweSessionState.Idle, HaweSessionState.Active), - (HaweSessionState.Active, HaweSessionState.Fault), - (HaweSessionState.Fault, HaweSessionState.Idle), - }); - channel.SessionState.Should().Be(HaweSessionState.Idle); - } - - // FR-HAWE-003 verification: the actor-driven deadline surface is reachable from a codec via - // the host and fires on the actor loop. - [Fact] - public async Task Codec_Can_Arm_Deadline_Through_Host() - { - var session = NewSession(); - using var receiver = Open(session, 0); - using var service = new CanBusService(receiver); - - var codec = new FakePatternCodec(HaweFramePattern.Range(0x200, 0x2FF)); - using var channel = new HaweChannel(service, codec); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - var fired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using var handle = codec.Host!.ArmDeadline(TimeSpan.FromMilliseconds(50), () => fired.TrySetResult(true)); - - // Bounded wait: if the actor-driven deadline never fires within ShortTimeout something - // is wrong with the framework's wiring to Reliability's DeadlineScheduler. - var completed = await Task.WhenAny(fired.Task, Task.Delay(ShortTimeout)); - completed.Should().BeSameAs(fired.Task); - } - - // Disposing the channel must invoke OnDetached exactly once and stop delivering frames to - // the codec afterwards, even if the underlying bus keeps carrying traffic. - [Fact] - public async Task Dispose_Detaches_Codec_And_Stops_Delivery() - { - var session = NewSession(); - using var sender = Open(session, 0); - using var receiver = Open(session, 1); - using var service = new CanBusService(receiver); - - var codec = new FakePatternCodec(HaweFramePattern.Range(0x100, 0x1FF)); - var channel = new HaweChannel(service, codec); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - sender.Transmit(CanFrame.Classic(0x100, new byte[] { 1 })); - await WaitForFrames(codec, 1, ShortTimeout); - codec.Received.Count.Should().Be(1); - - channel.Dispose(); - codec.DetachedCount.Should().Be(1); - - // Traffic on the shared bus continues; the codec must not see any of it now. - sender.Transmit(CanFrame.Classic(0x101, new byte[] { 2 })); - await Task.Delay(200); - codec.Received.Count.Should().Be(1); - - // Double-dispose is safe and must not increment OnDetached a second time. - channel.Dispose(); - codec.DetachedCount.Should().Be(1); - } - - // The framework attaches purely on the shared bus service and does not take ownership of it: - // two independent codecs on disjoint patterns coexist on one service and each see only their - // own traffic (mirrors the multi-protocol scenario FR-HAWE-003 is aligned with). - [Fact] - public async Task Two_Codecs_Share_One_Bus_Service_Without_Cross_Talk() - { - var session = NewSession(); - using var sender = Open(session, 0); - using var receiver = Open(session, 1); - using var service = new CanBusService(receiver); - - var codecA = new FakePatternCodec(HaweFramePattern.Range(0x100, 0x1FF)); - var codecB = new FakePatternCodec(HaweFramePattern.Range(0x200, 0x2FF)); - - using var channelA = new HaweChannel(service, codecA); - using var channelB = new HaweChannel(service, codecB); - - codecA.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - codecB.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - sender.Transmit(CanFrame.Classic(0x100, new byte[] { 1 })); - sender.Transmit(CanFrame.Classic(0x150, new byte[] { 2 })); - sender.Transmit(CanFrame.Classic(0x200, new byte[] { 3 })); - sender.Transmit(CanFrame.Classic(0x2FF, new byte[] { 4 })); - - await WaitForFrames(codecA, 2, ShortTimeout); - await WaitForFrames(codecB, 2, ShortTimeout); - - codecA.Received.Select(f => f.ID).Should().BeEquivalentTo(new[] { 0x100, 0x150 }); - codecB.Received.Select(f => f.ID).Should().BeEquivalentTo(new[] { 0x200, 0x2FF }); - } - - // FR-HAWE-002: the CanIdFilter-based pattern works with the acceptance-code/mask shape too, - // not just inclusive ranges. - [Fact] - public async Task Frame_Pattern_Accepts_Mask_Filter() - { - var session = NewSession(); - using var sender = Open(session, 0); - using var receiver = Open(session, 1); - using var service = new CanBusService(receiver); - - // Match all IDs whose lower nibble is 0x1 (0x001, 0x011, 0x021, ...). - var codec = new FakePatternCodec(HaweFramePattern.Mask(0x001, 0x00F)); - using var channel = new HaweChannel(service, codec); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - sender.Transmit(CanFrame.Classic(0x001, new byte[] { 1 })); // match - sender.Transmit(CanFrame.Classic(0x011, new byte[] { 2 })); // match - sender.Transmit(CanFrame.Classic(0x002, new byte[] { 3 })); // no match - - await WaitForFrames(codec, 2, ShortTimeout); - codec.Received.Select(f => f.ID).Should().BeEquivalentTo(new[] { 0x001, 0x011 }); - } - - // Regression for the SetSessionState reentrancy deadlock: prior to the fix, - // Host.SetSessionState used PostAsync().GetAwaiter().GetResult() unconditionally, which - // deadlocked when called from any codec callback already executing on the actor loop -- - // the very loop the pending PostAsync work needs in order to complete. The fix must detect - // the reentrant call and apply the state transition synchronously. - [Fact] - public async Task SetSessionState_From_Codec_Callback_Does_Not_Deadlock() - { - var session = NewSession(); - using var sender = Open(session, 0); - using var receiver = Open(session, 1); - using var service = new CanBusService(receiver); - - var codec = new ReentrantStateCodec(HaweFramePattern.Range(0x100, 0x1FF)); - using var channel = new HaweChannel(service, codec); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - sender.Transmit(CanFrame.Classic(0x100, new byte[] { 1 })); - - // Bounded wait: pre-fix the callback deadlocks and this never completes; the test times - // out and fails with a clear "reentrancy deadlock" message. - var completed = await Task.WhenAny(codec.CallbackDone.Task, Task.Delay(ShortTimeout)); - completed.Should().BeSameAs(codec.CallbackDone.Task, - "SetSessionState from inside a codec callback must not deadlock the actor loop"); - - codec.SetStateReturn.Should().BeTrue("first Idle->Active transition returns true"); - channel.SessionState.Should().Be(HaweSessionState.Active); - - // OnSessionStateChanged must have fired synchronously on the same loop invocation -- - // before the reentrant SetSessionState call returned -- preserving in-order single-writer - // semantics for state transitions. - codec.Transitions.Should().ContainSingle() - .Which.Should().Be((HaweSessionState.Idle, HaweSessionState.Active)); - } - - // Also cover the nested-reentrant case: a codec's OnSessionStateChanged handler itself - // calls SetSessionState. Because we're still on the actor loop when OnSessionStateChanged - // fires, this nested call must also take the synchronous path. - [Fact] - public async Task Nested_SetSessionState_From_OnSessionStateChanged_Does_Not_Deadlock() - { - var session = NewSession(); - using var receiver = Open(session, 0); - using var service = new CanBusService(receiver); - - var codec = new NestedStateCodec(HaweFramePattern.Range(0x600, 0x6FF)); - using var channel = new HaweChannel(service, codec); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - // The call originates off-loop, hops onto the actor loop, and its OnSessionStateChanged - // callback then calls SetSessionState(Fault) reentrantly on the same loop invocation. - var setTask = Task.Run(() => codec.Host!.SetSessionState(HaweSessionState.Active)); - var completed = await Task.WhenAny(setTask, Task.Delay(ShortTimeout)); - completed.Should().BeSameAs(setTask, "nested SetSessionState must not deadlock"); - (await setTask).Should().BeTrue(); - - // Both transitions observed, in order. - await Task.Delay(50); - codec.Transitions.Should().Equal(new[] - { - (HaweSessionState.Idle, HaweSessionState.Active), - (HaweSessionState.Active, HaweSessionState.Fault), - }); - channel.SessionState.Should().Be(HaweSessionState.Fault); - } - - // Regression for the Dispose ordering race: prior to the fix, Dispose posted OnDetached - // BEFORE awaiting the pump task, so the pump could still Post OnFrameReceived items behind - // OnDetached in the mailbox and fire them after the codec had been told the channel was - // detached. The fix must guarantee no OnFrameReceived fires after OnDetached, even under - // constant traffic during Dispose. We give the race the widest possible window: a slow codec - // (so the actor mailbox visibly accumulates work behind OnDetached) plus a large burst of - // frames still resident in the subscription buffer at the moment Dispose runs. - [Fact] - public async Task No_OnFrameReceived_Fires_After_OnDetached_During_Dispose() - { - var session = NewSession(); - using var sender = Open(session, 0); - using var receiver = Open(session, 1); - using var service = new CanBusService(receiver); - - // A modest per-frame delay in the codec keeps enough work in the actor mailbox at any - // given instant that the OnDetached post lands somewhere in the middle rather than at - // the very tail -- so any pre-fix late post from the pump is a genuine post-OnDetached - // fire, not a timing artifact. Small enough that the whole test drains well under the - // 5 s Dispose timeout. - var codec = new OrderCapturingCodec(HaweFramePattern.Range(0x100, 0x1FF), perFrameDelayMs: 1); - var channel = new HaweChannel(service, codec, new HaweChannelOptions { SubscriptionBufferCapacity = 4096 }); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - - // Continuous background flood: keeps the subscription buffer refilling so the pump is - // repeatedly inside its "TryRead + Post" hot loop rather than parked at - // WaitToReadAsync (which would cancel cleanly with no late posts). Bounded by both - // the flood cancellation and the total frame budget -- we want the race window open - // during Dispose, not runaway traffic for the whole test. - using var floodCts = new CancellationTokenSource(); - var flooder = Task.Run(async () => - { - for (var i = 0; i < 200 && !floodCts.IsCancellationRequested; i++) - { - try { sender.Transmit(CanFrame.Classic(0x100, new byte[] { (byte)(i & 0xFF) })); } - catch { break; } - // Yield often enough that the pump can interleave TryRead/Post iterations with - // new frames arriving; without this the sender bursts to completion before the - // pump ever starts and Dispose sees an empty subscription. - if ((i % 20) == 19) await Task.Delay(1).ConfigureAwait(false); - } - }); - - // Give the pump time to actually start moving those frames onto the actor loop, so at - // least a few OnFrameReceived have fired before Dispose runs (proves the pump is - // active and the subscription buffer is populated). - var deadline = DateTime.UtcNow + ShortTimeout; - while (codec.ReceivedCount < 5 && DateTime.UtcNow < deadline) - await Task.Delay(1); - codec.ReceivedCount.Should().BeGreaterThan(0, "the pump must be actively delivering frames before Dispose"); - - channel.Dispose(); - floodCts.Cancel(); - try { await flooder; } catch { /* transmit may throw once the bus goes away; that is fine */ } - - // The channel is disposed. Give the actor loop one more moment to publish any late - // post to _framesAfterDetach the pre-fix ordering would have queued behind OnDetached. - await Task.Delay(50); - - codec.DetachedCount.Should().Be(1); - codec.FramesAfterDetach.Should().Be(0, - "OnFrameReceived must never be dispatched after OnDetached on the single-writer loop"); - } - - // Constructor guards. - [Fact] - public void HaweChannel_Rejects_Null_Args() - { - var codec = new FakePatternCodec(HaweFramePattern.Range(0, 0)); - Action nullService = () => new HaweChannel(null!, codec); - Action nullCodec = () => - { - var session = NewSession(); - using var bus = Open(session, 0); - using var service = new CanBusService(bus); - _ = new HaweChannel(service, null!); - }; - nullService.Should().Throw(); - nullCodec.Should().Throw(); - } - - // ActorExecutionMode.SynchronizationContext is exposed on HaweChannelOptions; the channel must - // pass a real context through to ProtocolActor (options value, else SynchronizationContext.Current). - [Fact] - public void HaweChannel_SynchronizationContext_Mode_Uses_Options_Context() - { - var session = NewSession(); - using var bus = Open(session, 0); - using var service = new CanBusService(bus); - var codec = new FakePatternCodec(HaweFramePattern.Range(0x100, 0x100)); - var context = new InlineSynchronizationContext(); - - using var channel = new HaweChannel(service, codec, new HaweChannelOptions - { - ActorMode = ActorExecutionMode.SynchronizationContext, - SynchronizationContext = context, - }); - - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - codec.Host.Should().NotBeNull(); - context.SendCount.Should().BeGreaterThan(0, - "OnAttached must be marshaled through the SynchronizationContext supplied via options"); - } - - // Regression for Bugbot 3596169080: ProtocolActor marshals SyncContext-mode work with blocking - // Send. Constructing HaweChannel (or calling SetSessionState) from that same dispatcher thread - // used to PostAsync().GetResult(), which blocks the pump Send needs → permanent deadlock. - // The channel must detect "caller is on the actor SyncContext" and run the work inline. - [Fact] - public void HaweChannel_SyncContext_Mode_From_Dispatcher_Thread_Does_Not_Deadlock() - { - using var dispatcher = new QueuingDispatcherSynchronizationContext(); - dispatcher.Start(); - - var session = NewSession(); - using var bus = Open(session, 0); - using var service = new CanBusService(bus); - var codec = new FakePatternCodec(HaweFramePattern.Range(0x100, 0x100)); - - Exception? error = null; - HaweSessionState? observed = null; - using var done = new ManualResetEventSlim(false); - - dispatcher.Post(_ => - { - try - { - using var channel = new HaweChannel(service, codec, new HaweChannelOptions - { - ActorMode = ActorExecutionMode.SynchronizationContext, - SynchronizationContext = dispatcher, - }); - - codec.Host.Should().NotBeNull(); - codec.Host!.SetSessionState(HaweSessionState.Active).Should().BeTrue(); - observed = channel.SessionState; - } - catch (Exception ex) - { - error = ex; - } - finally - { - done.Set(); - } - }, null); - - done.Wait(ShortTimeout).Should().BeTrue( - "constructing HaweChannel / SetSessionState on the SyncContext thread must not deadlock"); - error.Should().BeNull(); - observed.Should().Be(HaweSessionState.Active); - codec.Attached.Status.Should().Be(TaskStatus.RanToCompletion); - codec.Transitions.Should().ContainSingle() - .Which.Should().Be((HaweSessionState.Idle, HaweSessionState.Active)); - } - - // If OnAttached fails after Subscribe + actor start, the demux subscription must not leak. - [Fact] - public void HaweChannel_Failed_Construction_Disposes_Partial_Resources() - { - var session = NewSession(); - using var bus = Open(session, 0); - using var service = new CanBusService(bus); - var codec = new ThrowingOnAttachCodec(HaweFramePattern.Range(0x200, 0x200)); - - Action act = () => _ = new HaweChannel(service, codec); - act.Should().Throw().WithMessage("attach failed"); - - service.SubscriptionCount.Should().Be(0, - "a failed HaweChannel constructor must dispose the demux subscription it already created"); - } - - private sealed class ThrowingOnAttachCodec : IHaweCodec - { - public ThrowingOnAttachCodec(HaweFramePattern pattern) => FramePattern = pattern; - public string Name => "throwing-on-attach-codec"; - public HaweFramePattern FramePattern { get; } - public void OnAttached(IHaweCodecHost host) => throw new InvalidOperationException("attach failed"); - public void OnFrameReceived(in CanFrameView frame) { } - public void OnSessionStateChanged(HaweSessionState previous, HaweSessionState current) { } - public void OnDetached() { } - } - - /// - /// Runs inline (like many test / console contexts) and counts invocations - /// so we can assert marshaling happened. - /// - private sealed class InlineSynchronizationContext : SynchronizationContext - { - private int _sendCount; - public int SendCount => Volatile.Read(ref _sendCount); - - public override void Send(SendOrPostCallback d, object? state) - { - Interlocked.Increment(ref _sendCount); - d(state); - } - - public override void Post(SendOrPostCallback d, object? state) => Send(d, state); - } - - /// - /// Minimal UI-style dispatcher: a dedicated thread pumps a queue; from - /// other threads blocks until that pump runs the callback; from the pump - /// thread itself runs inline. Reproduces the HaweChannel SyncContext deadlock when the - /// channel sync-waits on PostAsync from the dispatcher thread. - /// - private sealed class QueuingDispatcherSynchronizationContext : SynchronizationContext, IDisposable - { - private readonly BlockingCollection<(SendOrPostCallback Callback, object? State, ManualResetEventSlim? Done, Exception?[]? Error)> _queue = new(); - private Thread? _thread; - - public void Start() - { - _thread = new Thread(() => - { - SetSynchronizationContext(this); - foreach (var item in _queue.GetConsumingEnumerable()) - { - try - { - item.Callback(item.State); - } - catch (Exception ex) - { - if (item.Error is not null) item.Error[0] = ex; - else throw; - } - finally - { - item.Done?.Set(); - } - } - }) - { - IsBackground = true, - Name = "HaweTest.Dispatcher", - }; - _thread.Start(); - } - - public override void Send(SendOrPostCallback d, object? state) - { - if (ReferenceEquals(Current, this)) - { - d(state); - return; - } - - using var done = new ManualResetEventSlim(false); - var error = new Exception?[1]; - _queue.Add((d, state, done, error)); - done.Wait(); - if (error[0] is not null) - throw error[0]!; - } - - public override void Post(SendOrPostCallback d, object? state) - => _queue.Add((d, state, null, null)); - - public void Dispose() - { - _queue.CompleteAdding(); - _thread?.Join(TimeSpan.FromSeconds(5)); - _queue.Dispose(); - } - } - - // Calls Host.SetSessionState from inside OnFrameReceived (i.e. from the actor loop). - // Pre-fix, HaweChannel.Host.SetSessionState always posted+blocked, which deadlocks in this - // scenario. Post-fix, the reentrant call must apply the transition synchronously. - private sealed class ReentrantStateCodec : IHaweCodec - { - private readonly TaskCompletionSource _attachedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); - public ReentrantStateCodec(HaweFramePattern pattern) => FramePattern = pattern; - public string Name => "reentrant-state-codec"; - public HaweFramePattern FramePattern { get; } - public IHaweCodecHost? Host { get; private set; } - public Task Attached => _attachedTcs.Task; - public TaskCompletionSource CallbackDone { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); - public bool SetStateReturn { get; private set; } - public List<(HaweSessionState prev, HaweSessionState curr)> Transitions { get; } = new(); - - public void OnAttached(IHaweCodecHost host) - { - Host = host; - _attachedTcs.TrySetResult(true); - } - - public void OnFrameReceived(in CanFrameView frame) - { - // Reentrant SetSessionState from the actor loop -- pre-fix, this deadlocks. - SetStateReturn = Host!.SetSessionState(HaweSessionState.Active); - CallbackDone.TrySetResult(true); - } - - public void OnSessionStateChanged(HaweSessionState previous, HaweSessionState current) - => Transitions.Add((previous, current)); - - public void OnDetached() { } - } - - // A codec whose OnSessionStateChanged handler itself calls SetSessionState. Verifies the - // nested reentrant case is also handled synchronously and does not deadlock. - private sealed class NestedStateCodec : IHaweCodec - { - private readonly TaskCompletionSource _attachedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); - private bool _nested; - public NestedStateCodec(HaweFramePattern pattern) => FramePattern = pattern; - public string Name => "nested-state-codec"; - public HaweFramePattern FramePattern { get; } - public IHaweCodecHost? Host { get; private set; } - public Task Attached => _attachedTcs.Task; - public List<(HaweSessionState prev, HaweSessionState curr)> Transitions { get; } = new(); - - public void OnAttached(IHaweCodecHost host) - { - Host = host; - _attachedTcs.TrySetResult(true); - } - - public void OnFrameReceived(in CanFrameView frame) { } - - public void OnSessionStateChanged(HaweSessionState previous, HaweSessionState current) - { - Transitions.Add((previous, current)); - if (!_nested && current == HaweSessionState.Active) - { - _nested = true; - // Nested reentrant call from within OnSessionStateChanged -- must not deadlock. - Host!.SetSessionState(HaweSessionState.Fault); - } - } - - public void OnDetached() { } - } - - // Records the order of callbacks: any OnFrameReceived that runs after OnDetached is a - // regression of the Dispose ordering fix. All bookkeeping happens on the single-writer actor - // loop, so plain (non-volatile) counters are fine. - private sealed class OrderCapturingCodec : IHaweCodec - { - private readonly TaskCompletionSource _attachedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly int _perFrameDelayMs; - private int _received; - private int _detached; - private int _framesAfterDetach; - public OrderCapturingCodec(HaweFramePattern pattern, int perFrameDelayMs = 0) - { - FramePattern = pattern; - _perFrameDelayMs = perFrameDelayMs; - } - public string Name => "order-capturing-codec"; - public HaweFramePattern FramePattern { get; } - public Task Attached => _attachedTcs.Task; - public int ReceivedCount => Volatile.Read(ref _received); - public int DetachedCount => Volatile.Read(ref _detached); - public int FramesAfterDetach => Volatile.Read(ref _framesAfterDetach); - - public void OnAttached(IHaweCodecHost host) => _attachedTcs.TrySetResult(true); - - public void OnFrameReceived(in CanFrameView frame) - { - // Ordering check is on the actor loop, so a plain read of _detached is authoritative: - // any nonzero value here means OnDetached already ran and this OnFrameReceived is a - // regression. - if (_detached != 0) Interlocked.Increment(ref _framesAfterDetach); - Interlocked.Increment(ref _received); - // Slow the actor loop's frame consumption so the mailbox visibly backs up behind any - // work Dispose posts, giving the pre-fix race the widest possible window to lose. - if (_perFrameDelayMs > 0) Thread.Sleep(_perFrameDelayMs); - } - - public void OnSessionStateChanged(HaweSessionState previous, HaweSessionState current) { } - - public void OnDetached() => Interlocked.Increment(ref _detached); - } -} diff --git a/tests/CanKit.Pro.Tests/TestCases/PublicApiSurfaceTests.cs b/tests/CanKit.Pro.Tests/TestCases/PublicApiSurfaceTests.cs index 5400c8e..a948343 100644 --- a/tests/CanKit.Pro.Tests/TestCases/PublicApiSurfaceTests.cs +++ b/tests/CanKit.Pro.Tests/TestCases/PublicApiSurfaceTests.cs @@ -25,6 +25,11 @@ private static readonly (string PackageId, string AssemblyName)[] Tracked = ("CanKit.Pro.Addressing", "CanKit.Pro.Addressing"), ("CanKit.Pro.RawCan", "CanKit.Pro.RawCan"), ("CanKit.Pro.Reliability", "CanKit.Pro.Reliability"), + ("CanKit.Pro.IsoTp", "CanKit.Pro.IsoTp"), + ("CanKit.Pro.J1939Tp", "CanKit.Pro.J1939Tp"), + ("CanKit.Pro.CANopen", "CanKit.Pro.CANopen"), + ("CanKit.Pro.J1939", "CanKit.Pro.J1939"), + ("CanKit.Pro.Uds", "CanKit.Pro.Uds"), }; [Theory]