Skip to content

Commit 4f87688

Browse files
authored
Merge pull request #461 from dborgards/fix/srdo-disabled-emcy-434
fix(canopen): preserve SRDO errors while EMCY is disabled
2 parents d74230b + 4204086 commit 4f87688

7 files changed

Lines changed: 95 additions & 2 deletions

File tree

‎src/CanKit.Pro.CANopen/CanOpenNode.Safety.cs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,8 @@ private async Task SendSrdoPairAsync(SrdoPair pair, CancellationToken period)
756756
}
757757
}
758758

759+
bool ISrdoEngineHost.EmcyEnabled => _emcyValid;
760+
759761
void ISrdoEngineHost.EmitEmcy(int srdoNumber, ushort errorCode)
760762
=> RaiseCommunicationError(CommunicationError.SrdoLength, srdoNumber, errorCode);
761763

‎src/CanKit.Pro.CANopen/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,9 @@ while a communication error is active still carries bit 4. An EMCY held in Stopp
658658
register of the moment it is sent. Reset Node and Reset Communication end the communication
659659
errors without an EMCY (transitions 5 and 0) and leave the application's bits in `1001h`. With
660660
the EMCY switched off (bit 31 of `1014h`) no error is raised, and an error that ends meanwhile
661-
still clears the bits. Heartbeat, node-guarding and life-guarding timeouts and the CAN
661+
still clears the bits. A run of short RPDO frames or SRDO pairs is not consumed while disabled:
662+
if it persists after enabling, the next short frame/pair reports `8210h` once. Toggling the
663+
producer after that report does not start a new error event (#434). Heartbeat, node-guarding and life-guarding timeouts and the CAN
662664
controller state raise no EMCY (`8110h`–`8140h`); they are events only.
663665

664666
**Heartbeat.** The producer and the consumer are separate modules. `HeartbeatProducer` sends

‎src/CanKit.Pro.CANopen/Safety/ISrdoEngineHost.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ namespace CanKit.Pro.CANopen.Safety;
66
/// events. All calls arrive on the actor loop. The node implements it; tests record it.</summary>
77
internal interface ISrdoEngineHost
88
{
9+
/// <summary>Whether the host's EMCY producer is enabled. An error run is not consumed
10+
/// while disabled, matching the node's RPDO reporting rule (#434). Actor loop only.</summary>
11+
bool EmcyEnabled { get; }
12+
913
/// <summary>A single frame, sent at once: the global failsafe command.</summary>
1014
void Send(uint cobId, byte[] payload);
1115

‎src/CanKit.Pro.CANopen/Safety/SrdoEngine.Consumer.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ private void OnSecondFrame(SrdoRuntime rt, byte[] data)
150150
// Emergency message with error code 8210h shall be produced" — once per run, like RPDOs.
151151
if (first.Length < rt.TotalBytes)
152152
{
153-
if (!rt.ShortFrameReported)
153+
// The node does not consume an RPDO error event while EMCY is disabled.
154+
// Keep the SRDO event available too, so enabling mid-run reports the next pair.
155+
if (!rt.ShortFrameReported && _host.EmcyEnabled)
154156
{
155157
rt.ShortFrameReported = true;
156158
_host.EmitEmcy(rt.Number, 0x8210);

‎tests/CanKit.Pro.Tests/TestCases/CANopen/CanOpenErrorRegisterTests.cs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,59 @@ public async Task Srdo_Length_Error_Sets_The_Communication_Bit_Until_The_Engine_
173173
await ExpectEmcyAsync(wire, 3, 0xFF00, 0x00);
174174
}
175175

176+
// #434: the SRDO engine must not latch a disabled error before the node can report it.
177+
// RPDO is the existing contract; application EMCYs witness the exact wire sequence.
178+
[Theory]
179+
[InlineData(false)]
180+
[InlineData(true)]
181+
public async Task A_Length_Error_While_Emcy_Is_Disabled_Is_Reported_After_Enabling_For_Pdo_And_Srdo(bool srdo)
182+
{
183+
var session = NewSession();
184+
using var bus = Open(session, 0);
185+
using var wire = new Wire(session, 2);
186+
using var device = OpenClocked(bus, Device, new ManualTimeSource(),
187+
new CanOpenNodeOptions { SrdoCount = 1, WritableCommunicationParameters = true });
188+
await WaitForStateAsync(device, NmtState.PreOperational);
189+
var od = device.ObjectDictionary;
190+
od.AddU16(0x2000, 0, 0); od.AddU16(0x2001, 0, 0);
191+
if (srdo)
192+
{
193+
device.Safety().ConfigureSrdoConsumer(1, new SrdoMapping().Add(0x2000, 0, 16).Add(0x2001, 0, 16),
194+
TimeSpan.FromMilliseconds(100), TimeSpan.FromMilliseconds(20), 0x123, 0x124);
195+
device.Safety().CommitSafetyConfiguration();
196+
}
197+
else device.ConfigureRpdo(1, new PdoMapping().Add(0x2000, 0, 16).Add(0x2001, 0, 16));
198+
await StartAsync(wire, device);
199+
async Task Receive(bool shortFrame)
200+
{
201+
byte[] normal = shortFrame ? new byte[] { 1, 2 } : new byte[] { 1, 2, 3, 4 };
202+
await Inject(device, srdo ? 0x123 : Rpdo1, normal);
203+
if (srdo) await Inject(device, 0x124, SrdoFrames.Invert(normal));
204+
}
205+
od.WriteUnsigned(0x1014, 0, EmcyId | CanOpenCobId.InvalidBit);
206+
await Receive(shortFrame: true);
207+
await Receive(shortFrame: true);
208+
od.WriteUnsigned(0x1014, 0, EmcyId);
209+
await device.SendEmcyAsync(0xFF00, 0);
210+
await ExpectEmcyAsync(wire, 1, 0xFF00, 0);
211+
await Receive(shortFrame: true);
212+
await device.SendEmcyAsync(0xFF01, 0);
213+
await ExpectEmcyAsync(wire, 2, 0x8210, Communication);
214+
await ExpectEmcyAsync(wire, 3, 0xFF01, Communication);
215+
od.ReadUnsigned(0x2000, 0).Should().Be(0, "short frames remain unprocessed");
216+
od.WriteUnsigned(0x1014, 0, EmcyId | CanOpenCobId.InvalidBit);
217+
await Receive(shortFrame: true);
218+
od.WriteUnsigned(0x1014, 0, EmcyId);
219+
await Receive(shortFrame: true);
220+
await device.SendEmcyAsync(0xFF02, 0);
221+
await ExpectEmcyAsync(wire, 4, 0xFF02, Communication);
222+
await Receive(shortFrame: false);
223+
await device.SendEmcyAsync(0xFF03, 0);
224+
await ExpectEmcyAsync(wire, 5, ErrorReset, 0);
225+
await ExpectEmcyAsync(wire, 6, 0xFF03, 0);
226+
wire.Count(EmcyId).Should().Be(6, "enabling an already reported run does not report it twice");
227+
}
228+
176229
// The rule across sources and the application: each new error is its own EMCY, the register
177230
// keeps bit 4 while any one is active, the application's bits are ORed in and never cleared by
178231
// the node, and the 0000h comes when the last communication error ends — with the

‎tests/CanKit.Pro.Tests/TestCases/CANopen/Safety/SrdoEngineConsumerTests.cs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,35 @@ public void A_Short_Pair_Raises_Emcy_8210h_Once_And_Is_Not_Processed()
386386
_host.EmcyEnded.Should().Equal(new[] { 1, 1 }, "a rebuild ends the run too");
387387
}
388388

389+
[Fact]
390+
public void Disabled_Emcy_Does_Not_Consume_A_Short_Run_And_Enabling_Reports_Exactly_Once()
391+
{
392+
StartConsumer();
393+
_host.EmcyEnabled = false;
394+
Frame(0x123, 0x78, 0x56); Frame(0x124, 0x87, 0xA9);
395+
Frame(0x123, 0x78, 0x56); Frame(0x124, 0x87, 0xA9);
396+
_host.Emcys.Should().BeEmpty();
397+
_od.ReadUnsigned(0x2000, 0).Should().Be(0x1234u, "a disabled diagnostic never actuates a short pair");
398+
_engine.GetState(1).IsValid.Should().BeFalse();
399+
_host.EmcyEnabled = true;
400+
Frame(0x123, 0x78, 0x56); Frame(0x124, 0x87, 0xA9);
401+
_host.Emcys.Should().Equal((ushort)0x8210);
402+
_host.EmcyEnabled = false;
403+
Frame(0x123, 0x78, 0x56); Frame(0x124, 0x87, 0xA9);
404+
_host.EmcyEnabled = true;
405+
Frame(0x123, 0x78, 0x56); Frame(0x124, 0x87, 0xA9);
406+
_host.Emcys.Should().HaveCount(1, "toggling EMCY does not start a new already-reported error run");
407+
Frame(0x123, Plain); Frame(0x124, Inverse);
408+
_host.EmcyEnded.Should().Equal(1);
409+
_host.EmcyEnabled = false;
410+
Frame(0x123, 0x78, 0x56); Frame(0x124, 0x87, 0xA9);
411+
Frame(0x123, Plain); Frame(0x124, Inverse);
412+
_host.EmcyEnded.Should().Equal(new[] { 1 }, "an unreported disabled run has no reported cause to end");
413+
_host.EmcyEnabled = true;
414+
Frame(0x123, 0x78, 0x56); Frame(0x124, 0x87, 0xA9);
415+
_host.Emcys.Should().HaveCount(2, "the completed run allows a fresh error");
416+
}
417+
389418
/// <summary>Bugbot on #417: entering Operational re-reads every SRDO without a change of its
390419
/// own, so it does not end a run of short pairs — the error goes on until a pair fits, as an
391420
/// RPDO's does — and a short pair after it is the same run.</summary>

‎tests/CanKit.Pro.Tests/TestCases/CANopen/Safety/SrdoEngineProducerTests.cs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ private void Record(uint cobId, byte[] payload)
7979
Sent.Add((cobId, payload));
8080
Log.Add($"send:{cobId:X3}");
8181
}
82+
public bool EmcyEnabled { get; set; } = true;
8283
public void EmitEmcy(int srdoNumber, ushort errorCode) { Emcys.Add(errorCode); Log.Add($"emcy:{errorCode:X4}"); }
8384
/// <summary>The SRDOs whose reported error ended, in order (<c>emcy-ended:1</c> in <see cref="Log"/>).</summary>
8485
public readonly List<int> EmcyEnded = new();

0 commit comments

Comments
 (0)