From a176c791740405089a1818574bd514e94c70c54b Mon Sep 17 00:00:00 2001 From: Callan Barrett Date: Sat, 17 Jan 2026 22:42:33 +0800 Subject: [PATCH] fix: don't fire OnCardRemoved on fatal communication errors When the PN532 device is unplugged or experiences a fatal error, the library was incorrectly firing OnCardRemoved before OnDeviceDisconnected. This gives callers false information - the card wasn't removed, the reader disconnected. Now handlePollingError() checks for fatal errors first and returns early after calling OnDeviceDisconnected, skipping handleCardRemoval(). --- polling/session.go | 9 +++++---- polling/session_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/polling/session.go b/polling/session.go index b3f0b10..669de42 100644 --- a/polling/session.go +++ b/polling/session.go @@ -636,11 +636,8 @@ func (s *Session) handlePollingError(err error) { return } - // For serious device errors, trigger immediate card removal - // This handles cases like device disconnection - s.handleCardRemoval() - // If this is a fatal error (device disconnected), notify via callback + // but do NOT trigger card removal - the card wasn't removed, the reader was if pn532.IsFatal(err) { s.stateMutex.RLock() onDisconnected := s.OnDeviceDisconnected @@ -649,7 +646,11 @@ func (s *Session) handlePollingError(err error) { if onDisconnected != nil { onDisconnected(err) } + return } + + // For non-fatal errors, trigger card removal (transient comm issues) + s.handleCardRemoval() } // handleCardRemoval handles card removal state changes diff --git a/polling/session_test.go b/polling/session_test.go index e5f9d22..001dc2a 100644 --- a/polling/session_test.go +++ b/polling/session_test.go @@ -1281,6 +1281,42 @@ func TestSession_OnDeviceDisconnected(t *testing.T) { }) } +// TestSession_FatalErrorDoesNotTriggerCardRemoved verifies that fatal errors +// (device disconnection) don't incorrectly fire OnCardRemoved before OnDeviceDisconnected. +func TestSession_FatalErrorDoesNotTriggerCardRemoved(t *testing.T) { + t.Parallel() + device, _ := createMockDeviceWithTransport(t) + session := NewSession(device, nil) + + // Simulate card was present + session.stateMutex.Lock() + session.state.Present = true + session.state.LastUID = "04123456789ABC" + session.stateMutex.Unlock() + + var cardRemovedCalled bool + var disconnectedCalled bool + session.SetOnCardRemoved(func() { + cardRemovedCalled = true + }) + session.SetOnDeviceDisconnected(func(_ error) { + disconnectedCalled = true + }) + + // Create a fatal error (device disconnected) + fatalErr := &pn532.TransportError{ + Op: "test", + Port: "test", + Err: pn532.ErrDeviceNotFound, + Type: pn532.ErrorTypePermanent, + } + + session.handlePollingError(fatalErr) + + assert.True(t, disconnectedCalled, "OnDeviceDisconnected should be called") + assert.False(t, cardRemovedCalled, "OnCardRemoved should NOT be called for fatal errors") +} + func TestSession_HandleCardRemoval(t *testing.T) { t.Parallel()