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]