Skip to content

Commit 868fca3

Browse files
authored
Merge pull request #14 from ZaparooProject/fix/windows-uart-fastread-lockup
fix: prevent PN532 firmware lockup on Windows UART with large FastRead operations
2 parents a3adaa8 + 0893066 commit 868fca3

File tree

6 files changed

+183
-3
lines changed

6 files changed

+183
-3
lines changed

device.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,18 @@ type DeviceConfig struct {
4444
RetryConfig *RetryConfig
4545
// Timeout is the default timeout for operations
4646
Timeout time.Duration
47+
// MaxFastReadPages limits the number of pages in a single FastRead operation
48+
// Set to 0 to use platform-specific defaults (16 pages on Windows UART, unlimited elsewhere)
49+
// This helps avoid PN532 firmware lockups with large InCommunicateThru payloads
50+
MaxFastReadPages int
4751
}
4852

4953
// DefaultDeviceConfig returns default device configuration
5054
func DefaultDeviceConfig() *DeviceConfig {
5155
return &DeviceConfig{
52-
RetryConfig: DefaultRetryConfig(),
53-
Timeout: 1 * time.Second,
56+
RetryConfig: DefaultRetryConfig(),
57+
Timeout: 1 * time.Second,
58+
MaxFastReadPages: 0, // Use platform-specific defaults
5459
}
5560
}
5661

device_context.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,3 +1067,17 @@ func (d *Device) PowerDownContext(ctx context.Context, wakeupEnable, irqEnable b
10671067

10681068
return nil
10691069
}
1070+
1071+
// ClearTransportState clears corrupted transport state to prevent firmware lockup
1072+
// This is critical when switching between InCommunicateThru and InDataExchange
1073+
// operations after frame reception failures
1074+
func (d *Device) ClearTransportState() error {
1075+
// Check if the transport supports state clearing
1076+
if clearer, ok := d.transport.(interface{ ClearTransportState() error }); ok {
1077+
if err := clearer.ClearTransportState(); err != nil {
1078+
return fmt.Errorf("failed to clear transport state: %w", err)
1079+
}
1080+
}
1081+
// If transport doesn't support clearing, that's ok (some transports may not need it)
1082+
return nil
1083+
}

ntag.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"context"
2626
"errors"
2727
"fmt"
28+
"runtime"
2829
"strings"
2930
"time"
3031
)
@@ -245,6 +246,16 @@ func (t *NTAGTag) ReadNDEF() (*NDEFMessage, error) {
245246

246247
data, err := t.readNDEFDataWithFastRead(header, totalBytes)
247248
if err != nil {
249+
// CRITICAL: Clear transport state after failed InCommunicateThru operation
250+
// This prevents firmware lockup when switching to InDataExchange protocol
251+
fallbackReason := "FastRead failed"
252+
if runtime.GOOS == "windows" && t.isUARTTransport() {
253+
fallbackReason = "FastRead failed (Windows UART has size limits to prevent PN532 firmware lockup)"
254+
}
255+
debugf("NTAG %s, clearing transport state before fallback: %v", fallbackReason, err)
256+
if clearErr := t.device.ClearTransportState(); clearErr != nil {
257+
debugf("NTAG transport state clearing failed: %v", clearErr)
258+
}
248259
return t.readNDEFBlockByBlock()
249260
}
250261

@@ -339,6 +350,49 @@ func (t *NTAGTag) ensureTagTypeDetected() error {
339350
return nil
340351
}
341352

353+
// getMaxFastReadPages returns the maximum number of pages to read in a single FastRead operation
354+
// This is platform-specific to avoid PN532 firmware lockups with large InCommunicateThru payloads
355+
func (t *NTAGTag) getMaxFastReadPages() uint8 {
356+
// Check if user has explicitly configured a limit
357+
if t.device.config.MaxFastReadPages > 0 {
358+
// Bounds check to prevent integer overflow and ensure reasonable limits
359+
switch {
360+
case t.device.config.MaxFastReadPages > 255:
361+
debugf("NTAG MaxFastReadPages %d exceeds uint8 limit, capping to 255", t.device.config.MaxFastReadPages)
362+
return 255
363+
default:
364+
// #nosec G115 - bounds checked above
365+
return uint8(t.device.config.MaxFastReadPages)
366+
}
367+
}
368+
369+
// Apply platform-specific defaults
370+
if runtime.GOOS == "windows" {
371+
// Check if using UART transport which is most prone to the firmware lockup
372+
if t.isUARTTransport() {
373+
// 16 pages = 64 bytes - safe limit that avoids PN532 firmware lockups on Windows UART
374+
// Based on research showing InCommunicateThru becomes unreliable beyond 64 bytes
375+
debugf("NTAG applying Windows UART FastRead limit: 16 pages (64 bytes) to prevent firmware lockup")
376+
debugf("NTAG tip: set config.MaxFastReadPages to override this Windows-specific safety limit")
377+
return 16
378+
}
379+
debugf("NTAG Windows detected but non-UART transport, using default FastRead limits")
380+
}
381+
382+
// Default: no limit (use original behavior)
383+
return 60
384+
}
385+
386+
// isUARTTransport checks if the current transport is UART-based
387+
func (t *NTAGTag) isUARTTransport() bool {
388+
// Check if transport has UART capability
389+
if checker, ok := t.device.transport.(TransportCapabilityChecker); ok {
390+
return checker.HasCapability(CapabilityUART)
391+
}
392+
// Fallback: assume it might be UART to be safe
393+
return true
394+
}
395+
342396
func (t *NTAGTag) readNDEFDataWithFastRead(_ *ndefHeader, totalBytes int) ([]byte, error) {
343397
if t.fastReadSupported != nil && !*t.fastReadSupported {
344398
debugf("NTAG FastRead disabled - using block-by-block fallback")
@@ -348,7 +402,7 @@ func (t *NTAGTag) readNDEFDataWithFastRead(_ *ndefHeader, totalBytes int) ([]byt
348402
debugf("NTAG attempting FastRead for NDEF data (%d bytes)", totalBytes)
349403
readRange := t.calculateReadRange(totalBytes)
350404

351-
maxPagesPerRead := uint8(60)
405+
maxPagesPerRead := t.getMaxFastReadPages()
352406

353407
if readRange.endPage-readRange.startPage+1 <= maxPagesPerRead {
354408
debugf("NTAG using single FastRead for pages %d-%d", readRange.startPage, readRange.endPage)

ntag_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,4 +539,83 @@ func TestNTAG215BlockByBlockBufferOverflow(t *testing.T) {
539539
}
540540
}
541541

542+
// TestNTAGFastReadConfigLimits tests the FastRead configuration limits
543+
func TestNTAGFastReadConfigLimits(t *testing.T) {
544+
t.Parallel()
545+
tests := []struct {
546+
name string
547+
maxFastReadPages int
548+
expectedResult uint8
549+
}{
550+
{
551+
name: "Custom config normal value",
552+
maxFastReadPages: 32,
553+
expectedResult: 32,
554+
},
555+
{
556+
name: "Custom config bounds check - exceeds uint8",
557+
maxFastReadPages: 300,
558+
expectedResult: 255, // Capped to uint8 max
559+
},
560+
{
561+
name: "Custom config at uint8 boundary",
562+
maxFastReadPages: 255,
563+
expectedResult: 255,
564+
},
565+
{
566+
name: "Custom config small value",
567+
maxFastReadPages: 1,
568+
expectedResult: 1,
569+
},
570+
}
571+
572+
for _, tt := range tests {
573+
t.Run(tt.name, func(t *testing.T) {
574+
t.Parallel()
575+
// Create device with test config
576+
config := &DeviceConfig{
577+
MaxFastReadPages: tt.maxFastReadPages,
578+
}
579+
device := &Device{
580+
transport: &MockTransport{},
581+
config: config,
582+
}
583+
584+
// Create NTAG tag
585+
tag := NewNTAGTag(device, []byte{0x04, 0x12, 0x34, 0x56}, 0x00)
586+
587+
// Test the getMaxFastReadPages method
588+
maxPages := tag.getMaxFastReadPages()
589+
590+
// Verify the result matches expected bounds checking
591+
assert.Equal(t, tt.expectedResult, maxPages,
592+
"MaxFastReadPages should respect config with proper bounds checking")
593+
})
594+
}
595+
}
596+
597+
// TestNTAGFastReadDefaultLimits tests that default limits are reasonable
598+
func TestNTAGFastReadDefaultLimits(t *testing.T) {
599+
t.Parallel()
600+
// Create device with default config (0 = use platform defaults)
601+
config := &DeviceConfig{
602+
MaxFastReadPages: 0,
603+
}
604+
device := &Device{
605+
transport: &MockTransport{},
606+
config: config,
607+
}
608+
609+
// Create NTAG tag
610+
tag := NewNTAGTag(device, []byte{0x04, 0x12, 0x34, 0x56}, 0x00)
611+
612+
// Test the getMaxFastReadPages method
613+
maxPages := tag.getMaxFastReadPages()
614+
615+
// Verify the result is reasonable for any platform
616+
// (should be either Windows UART limit of 16 or default of 60)
617+
assert.True(t, maxPages == 16 || maxPages == 60,
618+
"Default MaxFastReadPages should be either Windows UART limit (16) or default (60), got %d", maxPages)
619+
}
620+
542621
// NDEF Message Tests

transport.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ const (
7474
// CapabilityAutoPollNative indicates the transport supports native InAutoPoll
7575
// with full command set and reliable operation (e.g., UART, I2C, SPI)
7676
CapabilityAutoPollNative TransportCapability = "autopoll_native"
77+
78+
// CapabilityUART indicates the transport uses UART communication
79+
// UART transport is prone to PN532 firmware lockups with large InCommunicateThru payloads
80+
CapabilityUART TransportCapability = "uart"
7781
)
7882

7983
// TransportCapabilityChecker defines an interface for querying transport capabilities

transport/uart/uart.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,27 @@ func (*Transport) Type() pn532.TransportType {
228228
return pn532.TransportUART
229229
}
230230

231+
// ClearTransportState resets transport state after protocol failures
232+
// This is critical for preventing firmware lockup when switching between
233+
// InCommunicateThru and InDataExchange operations after frame corruption
234+
func (t *Transport) ClearTransportState() error {
235+
t.mu.Lock()
236+
defer t.mu.Unlock()
237+
238+
if t.port != nil {
239+
// Reset input buffer to clear any stale/corrupted data
240+
_ = t.port.ResetInputBuffer()
241+
242+
// Add delay for Windows USB-UART drivers to process buffer reset
243+
if isWindows() {
244+
time.Sleep(15 * time.Millisecond)
245+
} else {
246+
time.Sleep(10 * time.Millisecond)
247+
}
248+
}
249+
return nil
250+
}
251+
231252
// isInterruptedSystemCall checks if an error is caused by an interrupted system call
232253
func isInterruptedSystemCall(err error) bool {
233254
if err == nil {
@@ -782,6 +803,9 @@ func (*Transport) HasCapability(capability pn532.TransportCapability) bool {
782803
case pn532.CapabilityRequiresInSelect:
783804
// UART requires InSelect after InListPassiveTarget for proper target selection
784805
return true
806+
case pn532.CapabilityUART:
807+
// This is the UART transport
808+
return true
785809
default:
786810
return false
787811
}

0 commit comments

Comments
 (0)