Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/requirements/SRS-CanKit.Pro.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ Ist-Zustand (09.10.2026): ausgeliefert als `CanKit.Pro.CANopen`. Der Zuschnitt d
| FR-CO-026 | Das System MUSS jeden Eintrag einer Gerätebeschreibung, den es nicht wie beschrieben umsetzen kann, degradieren, die Degradation im OD korrigieren und in `ICanOpenNode.DeviceDescription` (`DeviceDescriptionReport`) je Eintrag mit Index, Sub-Index, Ergebnis, Begründung, beschriebenem Wert und — wo eine SDO-Regel entschieden hat — dem Abort-Code melden; nichts wird still übergangen. Ergebnisse: *Omitted* (nicht anlegbar: Datentyp ohne Entsprechung, fünftes PDO, unparsbarer Wert, nicht implementierter Sub-Index eines festen Records), *Corrected* (der Wert wurde von derselben Regel wie ein SDO-Download abgewiesen, die CiA-301-Vorgabe bleibt), *PdoDisabled* (das PDO bleibt zerstört, weil Kommunikationsrecord oder Mapping nicht anwendbar ist), *NotImplemented* (der Eintrag existiert als Datum ohne Verhalten, etwa 1012h oder ein abweichendes 1200h), *SuppliedDefault* (ein fehlendes Pflichtobjekt 1000h/1001h/1018h behält den Platzhalter). `IsExact` ist genau dann wahr, wenn es keine Meldung gibt. | Must | Unit-Test (`CanOpenDeviceDescriptionTests`): `quirky.eds` erzeugt je Fall die Meldung mit dem erwarteten Ergebnis und Abort-Code — 1018h fehlt (SuppliedDefault), 1800h:02 = F5h (Corrected, 0609 0030h, FEh bleibt), 1801h:01 mit Bit 29 (PdoDisabled), 1A02h:01 auf ein nicht existierendes Objekt (PdoDisabled, 0602 0000h), 1804h/1A04h (Omitted), 1012h (NotImplemented), INTEGER24 (Omitted), doppelte Node-ID in 1016h (Corrected, 0604 0043h), abweichendes 1200h:01 (NotImplemented), 1010h:02 (Omitted) — und das OD trägt jeweils den korrigierten Wert. | Architekturentscheidung (Scope-Dokument 2026-09-15, Posten 2); Abort-Codes nach CiA 301 §7.2.4.3.17; #132 |
| FR-CO-027 | Das System MUSS die Zugriffsrechte, die eine Gerätebeschreibung für die PDO-Records 1400h–1403h, 1600h–1603h, 1800h–1803h und 1A00h–1A03h angibt, auf dem Bus durchsetzen — ein als `rw` beschriebenes Record ist per SDO schreibbar, auch ohne die Option `WritableCommunicationParameters`, ein als `ro` beschriebenes Objekt bleibt `ro` (0601 0002h) — und das `PDOMapping`-Attribut der Datei als Mappbarkeit des Objekts übernehmen, so dass ein Objekt mit `PDOMapping=0` beim Mapping nach FR-CO-020 mit 0604 0041h abgewiesen wird. | Must | Virtual-Loopback-Integrationstest (`CanOpenDeviceDescriptionTests`): mit `device.eds` und ohne die Option schreibt ein Master 1800h:01 und 1A00h:00/01 per Re-Mapping-Prozedur und liest 1A00h:01 zurück; 2002h (`ro`) → 0601 0002h; `PDOMapping=0` von 2002h steht als `PdoMappable = false` im OD, `PDOMapping=1` von 2000h als `true`. | Architekturentscheidung (Scope-Dokument 2026-09-15, Posten 13, 26); CiA 301 §7.5.2.36; #132 |
| FR-CO-028 | Mit einer Gerätebeschreibung MUSS der Knoten deren Werte als Einschaltwerte führen: „load“ (1011h:01, `RestoreDefaultParameters`) und ein NMT Reset Communication stellen 1000h–1FFFh auf die beschriebenen — oder die seit dem letzten „save“ gespeicherten — Werte zurück, ein NMT Reset Node zusätzlich die Applikationsobjekte der Datei; Reset Communication lässt die Applikationsobjekte unverändert. | Must | Virtual-Loopback-Integrationstest (`CanOpenDeviceDescriptionTests`): nach lokalem Schreiben von 2000h und 1017h liefert Reset Communication 1017h = 200 (Datei) und 2000h unverändert, Reset Node 2000h = 1234h (Datei); „save“ nach 1017h = 300, dann „load“ und Reset Communication → 1017h = 200. | CiA 301 §7.3.2.2.1 (Einschaltwerte), §7.5.2.14 („load“), §7.2.8.2.1.5/§7.2.8.2.1.6 (Reset Node / Reset Communication); Architekturentscheidung (Scope-Dokument 2026-09-15, Posten 1); #132 |
| FR-CO-029 | Das System MUSS jeden SDO-Client-Up- und -Download vor dem Senden gegen die Gerätebeschreibung prüfen, die für den Server gebunden ist (`BindPeerDeviceDescription`, EDS oder DCF): mit gebundener Datei nur (Index, Sub-Index)-Paare, die sie führt — 1000h, 1001h und 1018h eingeschlossen —, ohne Datei nur 1000h:00, 1001h:00 und 1018h:00–04h. Einzige Ausnahme (CiA DSP 304 §8.4.2.2, 13FFh:00 = Anzahl der SRDOs): führt die Datei SRDO-Records bis höchstens N, gelten Sub-Index 00h–06h von 1301h–(1300h + N) und 00h–10h von 1381h–(1380h + N) als geführt, so wie ein Gerät sie beim Laden der Datei bereitstellt; sonst nichts, und ohne SRDO-Record in der Datei keine. Eine Verweigerung MUSS als `PeerSdoAccessException` vor jedem SDO-Frame gemeldet werden. Eine DCF, die für eine andere Node-ID kommissioniert ist, MUSS mit `ArgumentException` abgewiesen werden und eine bestehende Bindung stehen lassen. | Must | Virtual-Loopback-Integrationstest (`CanOpenPeerSdoGateTests`): ein gelistetes Objekt wird übertragen, ein nicht gelistetes vor dem ersten Frame abgewiesen; ohne Datei gehen die drei Basisobjekte durch, 1003h und 1018h:05 nicht; eine Datei ohne 1000h verweigert 1000h; eine DCF für eine andere Node-ID wirft und ersetzt die Bindung nicht; die SRDO-Records bis zum höchsten der Datei gehen durch, darüber, jenseits Sub-Index 6/16 und ohne SRDO-Record in der Datei nicht (`CanOpenPeerSafetyTests.The_Gate_Implies_The_Srdo_Records_Up_To_The_Highest_Declared_One`, `Without_An_Srdo_Record_In_The_File_The_Gate_Implies_None`), und ein Gerät mit einer Lücke in den SRDO-Nummern wird konfiguriert, verifiziert und im Boot-up gestartet (`A_Device_Whose_File_Leaves_An_Srdo_Number_Out_Is_Configured_And_Verified`, `CanOpenSafetyBootUpTests.A_Slave_Whose_File_Leaves_An_Srdo_Number_Out_Is_Verified_And_Started`). | Entscheidung 1 (Scope-Dokument 2026-09-26); #174 |
| FR-CO-029 | Das System MUSS jeden SDO-Client-Up- und -Download vor dem Senden gegen die Gerätebeschreibung prüfen, die für den Server gebunden ist (`BindPeerDeviceDescription`, EDS oder DCF): mit gebundener Datei nur (Index, Sub-Index)-Paare, die sie führt — 1000h, 1001h und 1018h eingeschlossen —, ohne Datei nur 1000h:00, 1001h:00 und 1018h:00–04h. Einzige Ausnahme (CiA DSP 304 §8.4.2.2, 13FFh:00 = Anzahl der SRDOs): führt die Datei SRDO-Records bis höchstens N, gelten Sub-Index 00h–06h von 1301h–(1300h + N) und 00h–10h von 1381h–(1380h + N) als geführt, so wie ein Gerät sie beim Laden der Datei bereitstellt; sonst nichts, und ohne SRDO-Record in der Datei keine. Eine Verweigerung MUSS als `PeerSdoAccessException` vor jedem SDO-Frame gemeldet werden. Eine DCF, die für eine andere Node-ID kommissioniert ist, MUSS mit `ArgumentException` abgewiesen werden und eine bestehende Bindung stehen lassen. Die gebundene Beschreibung ist veränderlich (`Objects`, das EdsDcfNet-Modell) und wird von jedem Leser — dem Tor bei jedem Aufruf, dem Loader, den Beobachtern, einer Safety-Transaktion bei ihrer Lesung zu Beginn — ohne Synchronisation gelesen: ein Ändern an Ort und Stelle wirkt ab dem nächsten Aufruf; ein Ändern während eines lesenden Aufrufs wird nicht unterstützt, und was dieser Aufruf dann sieht, ist unbestimmt — die EdsDcfNet-Dictionaries sind für ein Lesen parallel zum Schreiben nicht sicher: ein enumerierender Leser (Loader, Lesung einer Safety-Transaktion) kann mit der `InvalidOperationException` des Dictionaries scheitern, gemeldet als Fehler dieses Aufrufs, ein Leser mit Einzelzugriffen um eigene Awaits (Datei-Rückfall der Beobachter, `Contains` des Tors) kann aus einer teils alten, teils neuen Beschreibung antworten oder keinen Treffer melden, ohne Fehler; eine gebundene Beschreibung wird daher nur geändert, solange kein lesender Aufruf läuft (dokumentierte Einschränkung, #296; unveränderliche Beschreibung beim nächsten Major, #299). | Must | Virtual-Loopback-Integrationstest (`CanOpenPeerSdoGateTests`): ein gelistetes Objekt wird übertragen, ein nicht gelistetes vor dem ersten Frame abgewiesen; ohne Datei gehen die drei Basisobjekte durch, 1003h und 1018h:05 nicht; eine Datei ohne 1000h verweigert 1000h; eine DCF für eine andere Node-ID wirft und ersetzt die Bindung nicht; die SRDO-Records bis zum höchsten der Datei gehen durch, darüber, jenseits Sub-Index 6/16 und ohne SRDO-Record in der Datei nicht (`CanOpenPeerSafetyTests.The_Gate_Implies_The_Srdo_Records_Up_To_The_Highest_Declared_One`, `Without_An_Srdo_Record_In_The_File_The_Gate_Implies_None`), und ein Gerät mit einer Lücke in den SRDO-Nummern wird konfiguriert, verifiziert und im Boot-up gestartet (`A_Device_Whose_File_Leaves_An_Srdo_Number_Out_Is_Configured_And_Verified`, `CanOpenSafetyBootUpTests.A_Slave_Whose_File_Leaves_An_Srdo_Number_Out_Is_Verified_And_Started`). | Entscheidung 1 (Scope-Dokument 2026-09-26); #174 |
| FR-CO-030 | Das System MUSS mit `ObserveForeignPdoAsync` die Nutzlast eines PDO eines anderen Knotens in eine Senke des Aufrufers zerlegen und dabei das eigene OD nicht schreiben. COB-ID (1400h:01 / 1800h:01) und Mapping (1600h–1603h / 1A00h–1A03h) MÜSSEN live über den SDO-Client gelesen werden, also durch das Tor aus FR-CO-029; bricht das Live-Lesen ab, läuft es in die Zeit, verweigert das Tor das Paar, ist der SDO-Kanal des Knotens zu Beginn der Beobachtung belegt oder liefert es kein verwendbares Wort — beim Mapping auch einen Wert anderer Breite als sein Typ: Sub-Index 00h genau ein Byte (UNSIGNED8), ein Eintrag genau vier (UNSIGNED32), nicht auf die niedrigen Bytes gekürzt —, MUSS der Eintrag der an den Aufruf übergebenen EDS/DCF gelten (bei einer DCF der `ParameterValue`). Eine live gelesene COB-ID mit gesetztem Bit 31 wird nicht durch die Datei ersetzt, ein abgebrochener Aufruf fällt nicht auf die Datei zurück; eine unbekannte COB-ID, ein ungültiges PDO und eine eingeschränkte CAN-ID werden nicht zerlegt; eine Nutzlast kürzer als das Mapping schreibt nichts. Die Live-Lesungen einer Beobachtung — COB-IDs, Mapping — sind eine Momentaufnahme des Peers: der Beobachter nimmt den SDO-Kanal des Peers ohne zu warten und hält ihn bis zum Ende, sodass eine währenddessen startende Safety-Transaktion auf ihn wartet (wie auf jeden laufenden Aufruf) und ein anderer SDO-Aufruf dieses Knotens an den Peer als „already in flight“ abgewiesen wird; ein zu Beginn belegter Kanal macht jede Live-Lesung dieser Beobachtung zu „already in flight“, die ganze Momentaufnahme kommt dann aus der Datei — eine live gelesene COB-ID wird nie mit dem Datei-Mapping kombiniert, weil ein Aufrufer dazwischenkam (#295). | Must | Virtual-Loopback-Integrationstest (`CanOpenForeignPdoObserveTests`): Live-Mapping zerlegt ein TPDO ohne OD-Schreibzugriff; SDO-Abbruch, Zeitüberschreitung, Tor-Verweigerung und laufender Transfer nehmen die Datei, ebenso ein Zähler aus zwei Bytes und ein Eintrag aus drei oder fünf Bytes (`A_live_read_that_is_not_a_usable_COB_ID_or_mapping_falls_back_to_the_file`); eine live ungültige COB-ID bleibt ungültig; kurze Nutzlast, unbekannte und eingeschränkte COB-ID liefern kein Signal; ein Aufruf, der zwischen COB-ID- und Mapping-Lesung dazwischenkommt, wird als „already in flight“ abgewiesen und die Momentaufnahme bleibt live (`A_call_cutting_in_mid_snapshot_is_refused_and_the_snapshot_stays_live`). | Entscheidung 2 und Frage 3 (Scope-Dokument 2026-09-26); #163 |
| FR-CO-031 | Das System MUSS die Wahl des aktiven NMT-Masters nach CiA 302-2 Version 4.1.0 (NMT flying master, 1F90h) ausführen: Teilnahme über 1F80h Bit 0 und Bit 5 bzw. `StartFlyingMaster`, Anfrage auf 073h, Antwort und Anspruch auf 071h (Priorität, Node-ID), Zeitschlitz-Wettlauf auf 072h mit `Priorität × 1F90h:04 + Node-ID × 1F90h:05`, Neuwahl auf 076h. Eine bessere oder gleiche Priorität setzt diesen Knoten auf Standby, ohne einen aktiven gleichrangigen Master abzusetzen; bei gleicher Priorität gewinnt die kleinere Node-ID; nach der ersten, kalten Wahl ohne Antwort MUSS Reset Communication an alle gehen und warm neu gewählt werden; der Verlust des Heartbeats des aktiven Masters MUSS eine warme Wahl auslösen. Priorität über 2, Device-Zeitschlitz 0 und ein Zeitschlitzpaar, das die Prioritäten nicht trennt (1F90h:04 ≤ 127 × 1F90h:05), MÜSSEN mit 0609 0030h abgewiesen werden. | Must | Virtual-Loopback-Integrationstest mit virtueller Uhr (`CanOpenFlyingMasterTests`, `CanOpenFlyingMasterGapTests`): allein am Bus gewinnt der Knoten nach dem kalten Reset; schlechtere Priorität weicht, bessere erzwingt den Neustart und gewinnt; gleicher Anspruch setzt den aktiven Master nicht ab; gleiche Priorität → kleinere Node-ID; Heartbeat-Verlust startet eine warme Wahl; ungültige 1F90h-Werte → 0609 0030h. | Entscheidung 4 und Frage 4 (Scope-Dokument 2026-09-26); CiA 302-2 v4.1.0 (öffentliche Beschreibung, Mitgliedstext nicht im Repository); #164 |
| FR-CO-032 | Als aktiver Master MUSS das System die Slaves aus 1F81h hochfahren: Reset Communication je Slave an dessen Node-ID (nicht bei Bit 4 keep-alive), NMT Start nach seinem ersten Heartbeat oder Boot-up (Bit 2), Pflicht-Slaves (Bit 3) vor dem eigenen Operational und vor einem gemeinsamen Start (1F80h Bit 1); 1F80h Bit 2 hält den eigenen Zustand, Bit 3 unterdrückt das Hochfahren, Bit 4 und Bit 6 wählen die Reaktion auf die Boot-Zeitüberschreitung 1F89h (Reset Node bzw. Stop an alle, sonst Reset Node an den fehlenden Slave). 1F82h MUSS den zuletzt gesehenen Zustand je Node-ID führen; ein Schreibzugriff darauf fordert als aktiver Master das NMT-Kommando an und wird sonst mit 0800 0022h abgewiesen. Ein Tool-Profil beginnt mit gesetzten Bits 2 und 3. | Must | Virtual-Loopback-Integrationstest mit virtueller Uhr (`CanOpenFlyingMasterTests`, `CanOpenFlyingMasterGapTests`): zugewiesener Slave wird zurückgesetzt, dann gestartet und sein Zustand in 1F82h geführt; Bit 3 lässt Slaves unberührt; ein nie antwortender Pflicht-Slave läuft in die Zeit, Bit 4 und Bit 6 wirken; 1F82h-Anforderung nur als aktiver Master; ein Tool startet weder sich noch Slaves, bis die Bits gelöscht sind. | Frage 5 (Scope-Dokument 2026-09-26); CiA 302-2 v4.1.0 (1F80h, 1F81h, 1F82h, 1F89h); #164 |
Expand Down
11 changes: 10 additions & 1 deletion src/CanKit.Pro.CANopen/CanOpenDeviceDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,16 @@ private CanOpenDeviceDescription(ElectronicDataSheet? eds, DeviceConfigurationFi
public DeviceConfigurationFile? Dcf { get; }

/// <summary>The object dictionary the description declares (the EdsDcfNet model, not the
/// node's <see cref="CanKit.Pro.CANopen.ObjectDictionary"/>).</summary>
/// node's <see cref="CanKit.Pro.CANopen.ObjectDictionary"/>). Mutable: an edit in place is
/// seen from the next call that reads the description. Nothing synchronizes those readers —
/// the peer-SDO gate on every SDO call to the peer this description is bound for, the loader,
/// the foreign PDO and SRDO observers, a safety transaction's reading at its start — with an
/// edit, so edit only while no such call is running. An edit during one is not supported
/// and what that call sees is not specified: the EdsDcfNet dictionaries are not safe for a
/// read concurrent with a write, so a reader that enumerates may fail with the dictionary's
/// <see cref="InvalidOperationException"/>, reported as that call's failure, and a reader
/// that looks objects up one at a time around its own awaits may answer from a description
/// that is part old and part new, or find no match, with no error.</summary>
public EdsDcfNet.Models.ObjectDictionary Objects => Eds?.ObjectDictionary ?? Dcf!.ObjectDictionary;

/// <summary>The device information section of the description.</summary>
Expand Down
5 changes: 4 additions & 1 deletion src/CanKit.Pro.CANopen/ICanOpenNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,10 @@ Task SendEmcyAsync(ushort errorCode, byte errorRegister,
/// Binds the EDS or DCF of remote node <paramref name="nodeId"/>. Client SDO transfers to
/// that node then proceed only for (index, sub-index) pairs the description declares
/// (<see cref="CanOpenDeviceDescription.Contains"/>). Replaces a description already bound
/// for the same node-id. The description is read at the start of each transfer.
/// for the same node-id. The description is read at the start of each transfer, without
/// synchronization against an edit of the bound instance in place
/// (<see cref="CanOpenDeviceDescription.Objects"/>): edit it only while no call that reads
/// it is running, or bind another one.
/// </summary>
/// <exception cref="ArgumentException">
/// <paramref name="description"/> is a DCF whose commissioned node-id is not
Expand Down
Loading
Loading