-
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathdevice_context.go
More file actions
1152 lines (978 loc) · 38.3 KB
/
Copy pathdevice_context.go
File metadata and controls
1152 lines (978 loc) · 38.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2026 The Zaparoo Project Contributors.
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pn532
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
"time"
)
// Init initializes the PN532 device with context support
func (d *Device) Init(ctx context.Context) error {
skipFirmwareVersion := d.shouldSkipFirmwareVersion()
// Test with GetFirmwareVersion first to see if any PN532 commands work via PC/SC
if !skipFirmwareVersion {
d.tryFirmwareVersionCheck(ctx)
}
skipSAM := d.shouldSkipSAMConfiguration()
if !skipSAM {
if err := d.handleSAMConfiguration(ctx); err != nil {
return err
}
}
// Configure finite passive activation retries to prevent infinite wait lockups
// Each retry is ~100ms per PN532 datasheet, so 10 retries = ~1 second
if err := d.SetPassiveActivationRetries(ctx, DefaultPassiveActivationRetries); err != nil {
// Log but don't fail initialization - this is an optimization, not critical
// Some older firmware versions might not support this configuration
_ = err
}
// Get firmware version (if supported by transport)
if !skipFirmwareVersion {
if err := d.setupFirmwareVersion(ctx); err != nil {
return err
}
} else {
d.setDefaultFirmwareVersion()
}
return nil
}
// Reset reinitializes the device connection after a power loss, sleep/wake cycle,
// or communication failure. This clears internal state and re-runs the initialization
// sequence (GetFirmwareVersion + SAMConfiguration).
//
// Use this when:
// - Host device wakes from sleep
// - PN532 module was power cycled
// - Communication becomes unreliable
//
// If Reset fails, consider closing and reopening the transport entirely.
func (d *Device) Reset(ctx context.Context) error {
// Clear internal state
d.firmwareVersion = nil
// Reinitialize the device
return d.Init(ctx)
}
// HardReset performs a "nuclear" recovery by closing and reopening the transport
// connection, then reinitializing the PN532. This is the last resort when the PN532
// enters a complete firmware lockup (stops ACKing any commands).
//
// This recovery is more disruptive than Reset() but handles cases where the PN532
// firmware is completely unresponsive. It requires the transport to implement the
// Reconnecter interface.
//
// Returns an error if the transport doesn't support reconnection or if recovery fails.
func (d *Device) HardReset(ctx context.Context) error {
Debugln("HardReset: attempting nuclear recovery")
// Check if transport supports reconnection
reconnecter, ok := d.transport.(Reconnecter)
if !ok {
return errors.New("transport does not support reconnection")
}
// Close and reopen the transport
if err := reconnecter.Reconnect(); err != nil {
return fmt.Errorf("HardReset reconnect failed: %w", err)
}
// Allow PN532 to boot after reconnection
time.Sleep(50 * time.Millisecond)
// Clear internal state
d.firmwareVersion = nil
// Reinitialize with SAMConfiguration
if err := d.SAMConfiguration(ctx, SAMModeNormal, 0, 0); err != nil {
return fmt.Errorf("HardReset SAMConfiguration failed: %w", err)
}
Debugln("HardReset: recovery successful")
return nil
}
// shouldSkipFirmwareVersion checks if transport supports firmware version retrieval
func (*Device) shouldSkipFirmwareVersion() bool {
// All transports now support firmware version retrieval
return false
}
// tryFirmwareVersionCheck attempts to get firmware version for early validation
func (d *Device) tryFirmwareVersionCheck(ctx context.Context) {
_, err := d.GetFirmwareVersion(ctx)
if err != nil {
// Continue with initialization even if GetFirmwareVersion fails
// This is expected for some transports/clone devices
_ = err // Explicitly ignore error
}
}
// shouldSkipSAMConfiguration determines if SAM configuration should be skipped
func (*Device) shouldSkipSAMConfiguration() bool {
// All transports now require SAM configuration
return false
}
// handleSAMConfiguration performs SAM configuration with clone device error handling
func (d *Device) handleSAMConfiguration(ctx context.Context) error {
err := d.setupSAMConfiguration(ctx)
if err == nil {
return nil
}
// Check if this looks like a clone device returning wrong response
errStr := err.Error()
if strings.Contains(errStr, "unexpected SAM configuration response code: 03") ||
strings.Contains(errStr, "response too short") ||
strings.Contains(errStr, "clone device returned empty response") {
// Clone device returned wrong response format - this is common with some clones
// Continue without SAM config as these devices often don't support it properly
Debugf("Warning: Clone device detected (SAM config issue: %s), continuing without SAM configuration", errStr)
return nil
}
return fmt.Errorf("SAM configuration failed: %w", err)
}
// setupFirmwareVersion retrieves and sets the firmware version
func (d *Device) setupFirmwareVersion(ctx context.Context) error {
fw, err := d.GetFirmwareVersion(ctx)
if err != nil {
return fmt.Errorf("failed to get firmware version: %w", err)
}
d.firmwareVersion = fw
return nil
}
// setDefaultFirmwareVersion creates a default firmware version for unsupported transports
func (d *Device) setDefaultFirmwareVersion() {
d.firmwareVersion = &FirmwareVersion{
Version: "1.6", // Generic version for PC/SC mode
SupportIso14443a: true, // Assume basic NFC-A support
SupportIso14443b: false, // Conservative defaults
SupportIso18092: false,
}
}
// GetFirmwareVersion returns the PN532 firmware version
func (d *Device) GetFirmwareVersion(ctx context.Context) (*FirmwareVersion, error) {
res, err := d.transport.SendCommand(ctx, cmdGetFirmwareVersion, []byte{})
if err != nil {
return nil, fmt.Errorf("failed to send GetFirmwareVersion command: %w", err)
}
d.debugFirmwareResponse(res)
if len(res) < 5 {
return nil, errors.New("unexpected firmware version response")
}
return d.parseFirmwareResponse(res)
}
// debugFirmwareResponse logs the firmware response for debugging
func (*Device) debugFirmwareResponse(res []byte) {
Debugf("GetFirmwareVersion response: [%s] (len=%d)",
strings.Join(func() []string {
strs := make([]string, len(res))
for i, b := range res {
strs[i] = fmt.Sprintf("0x%02X", b)
}
return strs
}(), " "), len(res))
}
// parseFirmwareResponse parses the firmware version response from various device types
func (d *Device) parseFirmwareResponse(res []byte) (*FirmwareVersion, error) {
// Check for standard PN532 response format first
if res[0] == 0x03 {
return d.parseStandardFirmwareResponse(res)
}
// Handle unexpected response format validation
if len(res) == 5 && res[0] != 0x03 {
return nil, errors.New("unexpected firmware version response")
}
// Handle clone device variations
return d.parseCloneFirmwareResponse(res)
}
// parseStandardFirmwareResponse parses standard PN532 firmware response
func (*Device) parseStandardFirmwareResponse(res []byte) (*FirmwareVersion, error) {
if res[1] != 0x32 {
return nil, fmt.Errorf("unexpected IC: %x", res[1])
}
return &FirmwareVersion{
Version: fmt.Sprintf("%d.%d", res[2], res[3]),
SupportIso14443a: res[4]&0x01 == 0x01,
SupportIso14443b: res[4]&0x02 == 0x02,
SupportIso18092: res[4]&0x04 == 0x04,
}, nil
}
// parseCloneFirmwareResponse handles clone device firmware response variations
func (d *Device) parseCloneFirmwareResponse(res []byte) (*FirmwareVersion, error) {
// Clone device returned SAM configuration response (0x15)
if len(res) == 1 && res[0] == 0x15 {
Debugln("Clone device returned SAM config response (0x15) for GetFirmwareVersion")
return d.createDefaultFirmwareVersion(), nil
}
if len(res) >= 3 {
// Try to extract version information from different positions
if version := d.parseCloneD5Format(res); version != nil {
return version, nil
}
// Fallback: Create a generic firmware version for compatibility
Debugln("Using fallback firmware version for clone device")
return d.createDefaultFirmwareVersion(), nil
}
return nil, fmt.Errorf("unexpected firmware version response: got %d bytes: %v", len(res), res)
}
// parseCloneD5Format parses clone devices with 0xD5 prefix
func (*Device) parseCloneD5Format(res []byte) *FirmwareVersion {
if len(res) >= 5 && res[0] == 0xD5 && res[1] == 0x03 {
// Some clones prefix with 0xD5 (response command byte)
Debugln("Detected clone format with 0xD5 prefix")
if len(res) >= 7 && res[2] == 0x32 {
return &FirmwareVersion{
Version: fmt.Sprintf("%d.%d", res[3], res[4]),
SupportIso14443a: res[5]&0x01 == 0x01,
SupportIso14443b: res[5]&0x02 == 0x02,
SupportIso18092: res[5]&0x04 == 0x04,
}
}
}
return nil
}
// createDefaultFirmwareVersion creates a default firmware version for clone devices
func (*Device) createDefaultFirmwareVersion() *FirmwareVersion {
return &FirmwareVersion{
Version: "1.6", // Generic version for clones
SupportIso14443a: true, // Assume basic NFC-A support
SupportIso14443b: false, // Conservative defaults
SupportIso18092: false,
}
}
// GetGeneralStatus returns the PN532 general status with context support
func (d *Device) GetGeneralStatus(ctx context.Context) (*GeneralStatus, error) {
res, err := d.transport.SendCommand(ctx, cmdGetGeneralStatus, []byte{})
if err != nil {
return nil, fmt.Errorf("failed to send GetGeneralStatus command: %w", err)
}
if len(res) < 4 || res[0] != 0x05 {
return nil, errors.New("unexpected general status response")
}
return &GeneralStatus{
LastError: res[1],
FieldPresent: res[2] == 0x01,
Targets: res[3],
}, nil
}
// Diagnose performs a self-diagnosis test with context support
func (d *Device) Diagnose(ctx context.Context, testNumber byte, data []byte) (*DiagnoseResult, error) {
// Build command: TestNumber + optional data
cmdPayload := append([]byte{testNumber}, data...)
res, err := d.transport.SendCommand(ctx, cmdDiagnose, cmdPayload)
if err != nil {
return nil, fmt.Errorf("diagnose command failed: %w", err)
}
// Check response format
if len(res) < 1 {
return nil, errors.New("empty diagnose response")
}
result := &DiagnoseResult{
TestNumber: testNumber,
}
// Special handling for ROM/RAM tests which return status byte wrapped by transport
if testNumber == DiagnoseROMTest || testNumber == DiagnoseRAMTest {
if len(res) != 2 || res[0] != 0x01 {
return nil, fmt.Errorf("unexpected ROM/RAM diagnose response format: %v", res)
}
result.Data = res[1:] // The single status byte
result.Success = res[1] == 0x00 // 0x00 = OK, 0xFF = Not Good
return result, nil
}
// Standard response should start with 0x01
if res[0] != 0x01 {
return nil, fmt.Errorf("unexpected diagnose response header: 0x%02X", res[0])
}
result.Data = res[1:]
// Set Success flag based on test type
switch testNumber {
case DiagnoseCommunicationTest:
// Communication test echoes back the entire command (test number + data)
result.Success = bytes.Equal(result.Data, cmdPayload)
case DiagnosePollingTest:
// Returns number of failures (0 = all succeeded)
if len(result.Data) == 0 {
return nil, errors.New("empty data for polling test")
}
result.Success = result.Data[0] == 0
case DiagnoseEchoBackTest:
// Echo back test runs infinitely, so no response expected
// If we get here, it means the test setup was successful
result.Success = true
case DiagnoseAttentionTest, DiagnoseSelfAntennaTest:
// For these tests, if no error, assume success
result.Success = true
default:
// Unknown test number, but got valid response
result.Success = true
}
return result, nil
}
// setupSAMConfiguration configures the SAM with context support
func (d *Device) setupSAMConfiguration(ctx context.Context) error {
return d.SAMConfiguration(ctx, SAMModeNormal, 0x00, 0x00)
}
// SAMConfiguration configures the SAM with context support
func (d *Device) SAMConfiguration(ctx context.Context, mode SAMMode, timeout, irq byte) error {
res, err := d.transport.SendCommand(ctx, cmdSamConfiguration, []byte{byte(mode), timeout, irq})
if err != nil {
return fmt.Errorf("SAM configuration command failed: %w", err)
}
// Validate SAM configuration response
if len(res) == 0 {
return errors.New("empty SAM configuration response")
}
// Expected response: 0x15 (command response code)
// Some transports may return additional data (e.g., PC/SC status words)
if res[0] != 0x15 {
return fmt.Errorf("unexpected SAM configuration response code: %02X, expected 0x15 (full response: %v)",
res[0], res)
}
return nil
}
// DetectTag detects a single tag in the field with context support
func (d *Device) DetectTag(ctx context.Context) (*DetectedTag, error) {
Debugln("Using InListPassiveTarget strategy")
// Apply transport-specific optimizations and timing
if err := d.prepareTransportForInListPassiveTarget(ctx); err != nil {
Debugf("Transport preparation failed: %v", err)
return nil, fmt.Errorf("transport preparation failed: %w", err)
}
// Release any previously selected targets to clear HALT states
// This addresses intermittent "empty valid tag" issues where tags get stuck
if err := d.InRelease(ctx); err != nil {
Debugf("InRelease failed, continuing anyway: %v", err)
// Don't fail the operation if InRelease fails - it's an optimization
}
// Small delay to allow RF field and tags to stabilize after release
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(10 * time.Millisecond):
}
return d.InListPassiveTarget(ctx, 0x00)
}
// prepareTransportForInListPassiveTarget applies transport-specific preparations for InListPassiveTarget
func (d *Device) prepareTransportForInListPassiveTarget(_ context.Context) error {
// Apply transport-specific timing and RF field management
switch d.transport.Type() {
case TransportUART:
// UART typically doesn't need special preparation
return nil
case TransportI2C, TransportSPI, TransportMock:
// Other transports typically don't need special preparation
return nil
default:
return nil
}
}
// convertAutoPollResult converts a single InAutoPoll result to DetectedTag format
func (d *Device) convertAutoPollResult(result *AutoPollResult) *DetectedTag {
if result == nil {
return nil
}
// Parse the target data to extract UID, ATQ, SAK
uid, atq, sak := d.parseTargetData(result.Type, result.TargetData)
// Determine tag type based on AutoPoll target type first, then ATQ/SAK
tagType := d.identifyTagTypeFromTarget(result.Type, atq, sak)
return &DetectedTag{
Type: tagType,
UID: fmt.Sprintf("%x", uid),
UIDBytes: uid,
ATQ: atq,
SAK: sak,
DetectedAt: time.Now(),
TargetData: result.TargetData, // Store full target data for FeliCa
}
}
// identifyTagTypeFromTarget identifies tag type from AutoPollTarget type and ATQ/SAK
func (d *Device) identifyTagTypeFromTarget(targetType AutoPollTarget, atq []byte, sak byte) TagType {
// For FeliCa targets, we can determine the type directly from the AutoPollTarget
switch targetType {
case AutoPollGeneric212kbps, AutoPollGeneric424kbps, AutoPollFeliCa212, AutoPollFeliCa424:
return TagTypeFeliCa
case AutoPollGeneric106kbps, AutoPollMifare, AutoPollISO14443A:
// For Type A targets, use ATQ/SAK identification
return d.identifyTagType(atq, sak)
case AutoPollJewel:
// Jewel tags not yet fully supported
return TagTypeUnknown
case AutoPollISO14443B, AutoPollISO14443B4:
// Type B tags not yet fully supported
return TagTypeUnknown
default:
return TagTypeUnknown
}
}
// parseTargetData extracts UID, ATQ, and SAK from InAutoPoll target data
// The format depends on the target type:
// - Type A (ISO14443A): [SENS_RES(2), SEL_RES(1), NFCID1_LEN(1), NFCID1...]
// - Type B (ISO14443B): [ATQB(11), ATTRIB_RES_LEN(1), ATTRIB_RES...]
// - FeliCa: [POL_RES(18) or POL_RES(20)]
func (d *Device) parseTargetData(targetType AutoPollTarget, targetData []byte) (uid, atq []byte, sak byte) {
// Default values for unsupported formats
uid = []byte{0x00, 0x00, 0x00, 0x00}
atq = []byte{0x00, 0x00}
sak = 0x00
switch targetType {
case AutoPollGeneric106kbps, AutoPollMifare, AutoPollISO14443A:
uid, atq, sak = d.parseISO14443AData(targetData)
case AutoPollJewel:
uid = d.parseJewelData(targetData)
case AutoPollGeneric212kbps, AutoPollGeneric424kbps, AutoPollFeliCa212, AutoPollFeliCa424:
uid = d.parseFeliCaData(targetData)
case AutoPollISO14443B, AutoPollISO14443B4:
uid = d.parseISO14443BData(targetData)
}
return uid, atq, sak
}
// parseISO14443AData parses ISO14443 Type A target data
func (*Device) parseISO14443AData(targetData []byte) (uid, atq []byte, sak byte) {
uid = []byte{0x00, 0x00, 0x00, 0x00}
atq = []byte{0x00, 0x00}
sak = 0x00
if len(targetData) < 4 {
return uid, atq, sak
}
// Parse ATQ and SAK from the first 3 bytes
atq = targetData[0:2] // SENS_RES (ATQ)
sak = targetData[2] // SEL_RES (SAK)
// Try parsing UID length at offset 3 first (test/mock format)
// Format: ATQ(2) + SAK(1) + UID_LENGTH(1) + UID(n)
if len(targetData) > 3 {
uidLen := targetData[3]
if uidLen > 0 && len(targetData) >= 4+int(uidLen) {
uid = targetData[4 : 4+int(uidLen)]
return uid, atq, sak
}
}
// Try parsing UID length at offset 4 (real hardware format)
// Format: ATQ(2) + SAK(1) + UNKNOWN(1) + UID_LENGTH(1) + UID(n)
if len(targetData) > 4 {
uidLen := targetData[4]
if uidLen > 0 && len(targetData) >= 5+int(uidLen) {
uid = targetData[5 : 5+int(uidLen)]
return uid, atq, sak
}
}
return uid, atq, sak
}
// parseJewelData parses Jewel target data
func (*Device) parseJewelData(targetData []byte) []byte {
if len(targetData) >= 6 {
return targetData[2:6] // UID (4 bytes)
}
return []byte{0x00, 0x00, 0x00, 0x00}
}
// parseFeliCaData parses FeliCa target data
func (*Device) parseFeliCaData(targetData []byte) []byte {
if len(targetData) >= 18 {
return targetData[2:10] // NFCID2 (8 bytes)
}
return []byte{0x00, 0x00, 0x00, 0x00}
}
// parseISO14443BData parses ISO14443 Type B target data
func (*Device) parseISO14443BData(targetData []byte) []byte {
if len(targetData) >= 11 && len(targetData) >= 5 {
return targetData[1:5] // PUPI acts as UID for Type B
}
return []byte{0x00, 0x00, 0x00, 0x00}
}
// SendDataExchange sends a data exchange command with context support
func (d *Device) SendDataExchange(ctx context.Context, data []byte) ([]byte, error) {
const targetNum byte = 1
if len(data) > 0 {
Debugf("SendDataExchange: target=%d, data[0]=0x%02X, len=%d", targetNum, data[0], len(data))
} else {
Debugf("SendDataExchange: target=%d, data=(empty), len=0", targetNum)
}
res, err := d.transport.SendCommand(ctx, cmdInDataExchange, append([]byte{targetNum}, data...))
if err != nil {
return nil, fmt.Errorf("failed to send data exchange command: %w", err)
}
// Check for error frame (TFI = 0x7F)
if len(res) >= 2 && res[0] == 0x7F {
errorCode := res[1]
return nil, NewPN532ErrorWithDetails(errorCode, "InDataExchange", len(data), targetNum)
}
if len(res) < 2 || res[0] != 0x41 {
return nil, errors.New("unexpected data exchange response")
}
if res[1] != 0x00 {
return nil, NewPN532ErrorWithDetails(res[1], "InDataExchange", len(data), targetNum)
}
return res[2:], nil
}
// isRetryableRFError checks if an error is an RF-related PN532 error that should be retried.
// This includes timeout (0x01) and RF communication errors (CRC, parity, framing, etc.)
// that commonly occur during card sliding into a reader slot.
func isRetryableRFError(err error) bool {
var pn532Err *PN532Error
if errors.As(err, &pn532Err) {
return pn532Err.IsTimeoutError() || pn532Err.IsRFError()
}
return false
}
// SendDataExchangeWithRetry sends a data exchange command with automatic retry on RF errors.
// RF errors (timeout, CRC, parity, framing) indicate transient communication failures that
// commonly occur during card sliding into a reader slot. A small delay between retries
// allows the RF field to stabilize.
//
// Configuration:
// - 3 attempts total (1 initial + 2 retries)
// - 15ms delay between retries (allows RF stabilization)
// - Retries on timeout (0x01) and RF errors (CRC, parity, framing, etc.)
func (d *Device) SendDataExchangeWithRetry(ctx context.Context, data []byte) ([]byte, error) {
const maxAttempts = 3
const retryDelay = 15 * time.Millisecond
var lastErr error
for attempt := range maxAttempts {
if ctxErr := ctx.Err(); ctxErr != nil {
if lastErr != nil {
return nil, lastErr
}
return nil, ctxErr
}
result, err := d.SendDataExchange(ctx, data)
if err == nil {
if attempt > 0 {
Debugf("SendDataExchangeWithRetry: succeeded on attempt %d", attempt+1)
}
return result, nil
}
if !isRetryableRFError(err) {
return nil, err
}
// RF error - save it and retry after delay
lastErr = err
if attempt < maxAttempts-1 {
Debugf("SendDataExchangeWithRetry: RF error on attempt %d, retrying after %v", attempt+1, retryDelay)
time.Sleep(retryDelay)
}
}
return nil, lastErr
}
// SendRawCommand sends a raw command with context support
func (d *Device) SendRawCommand(ctx context.Context, data []byte) ([]byte, error) {
const targetNum byte = 1
res, err := d.transport.SendCommand(ctx, cmdInCommunicateThru, data)
if err != nil {
return nil, fmt.Errorf("failed to send communicate through command: %w", err)
}
// Check for error frame (TFI = 0x7F)
if len(res) >= 2 && res[0] == 0x7F {
errorCode := res[1]
return nil, NewPN532ErrorWithDetails(errorCode, "InCommunicateThru", len(data), targetNum)
}
if len(res) < 2 || res[0] != 0x43 {
return nil, errors.New("unexpected InCommunicateThru response")
}
if res[1] != 0x00 {
return nil, NewPN532ErrorWithDetails(res[1], "InCommunicateThru", len(data), targetNum)
}
return res[2:], nil
}
// InRelease releases all selected targets with context support
func (d *Device) InRelease(ctx context.Context) error {
// Always release all targets (target 0 = release all)
res, err := d.transport.SendCommand(ctx, cmdInRelease, []byte{0x00})
if err != nil {
return fmt.Errorf("InRelease command failed: %w", err)
}
if len(res) != 2 || res[0] != 0x53 {
return errors.New("unexpected InRelease response")
}
// Check status byte
if res[1] != 0x00 {
return fmt.Errorf("InRelease failed with status: %02x", res[1])
}
return nil
}
// InDeselect deselects all targets while keeping target information in PN532 memory.
// Unlike InRelease, this allows InSelect to re-select the target later.
// For MIFARE cards, this sends HLTA which puts the tag in HALT state.
// Use InSelect after InDeselect to wake up HALTed tags with WUPA.
func (d *Device) InDeselect(ctx context.Context) error {
// Deselect all targets (target 0 = all targets)
res, err := d.transport.SendCommand(ctx, cmdInDeselect, []byte{0x00})
if err != nil {
return fmt.Errorf("InDeselect command failed: %w", err)
}
if len(res) != 2 || res[0] != 0x45 {
return errors.New("unexpected InDeselect response")
}
// Check status byte
if res[1] != 0x00 {
return fmt.Errorf("InDeselect failed with status: %02x", res[1])
}
return nil
}
// InSelect selects target 1 for communication with context support
func (d *Device) InSelect(ctx context.Context) error {
const targetNumber byte = 1
Debugf("InSelect: selecting target %d", targetNumber)
res, err := d.transport.SendCommand(ctx, cmdInSelect, []byte{targetNumber})
if err != nil {
Debugf("InSelect: command failed: %v", err)
return fmt.Errorf("InSelect command failed: %w", err)
}
if len(res) != 2 || res[0] != 0x55 {
return errors.New("unexpected InSelect response")
}
// Check status byte
if res[1] == 0x27 {
// 0x27 = Wrong Context - target likely already selected by InListPassiveTarget
Debugf("InSelect returned 0x27 for target %d - assuming already selected", targetNumber)
return nil
}
if res[1] != 0x00 {
Debugf("InSelect: failed with status: %02x", res[1])
return fmt.Errorf("InSelect failed with status: %02x", res[1])
}
Debugf("InSelect: success for target %d", targetNumber)
return nil
}
// InAutoPoll polls for a single target with context support
func (d *Device) InAutoPoll(
ctx context.Context, pollCount, pollPeriod byte, targetTypes []AutoPollTarget,
) (*AutoPollResult, error) {
if pollPeriod < 1 || pollPeriod > 15 {
return nil, errors.New("poll period must be between 1 and 15")
}
if len(targetTypes) == 0 || len(targetTypes) > 15 {
return nil, errors.New("must specify 1-15 target types")
}
// Build command data
data := []byte{pollCount, pollPeriod}
for _, tt := range targetTypes {
data = append(data, byte(tt))
}
res, err := d.transport.SendCommand(ctx, cmdInAutoPoll, data)
if err != nil {
return nil, fmt.Errorf("InAutoPoll command failed: %w", err)
}
if len(res) < 2 || res[0] != 0x61 {
return nil, errors.New("unexpected InAutoPoll response")
}
numTargets := res[1]
if numTargets == 0 {
return nil, nil //nolint:nilnil // nil result, nil error is valid "no tag detected" response
}
// Parse first result only
offset := 2
if offset+2 > len(res) {
return nil, fmt.Errorf("%w: response truncated when expecting target header", ErrInvalidResponse)
}
targetType := AutoPollTarget(res[offset])
dataLen := res[offset+1]
offset += 2
if offset+int(dataLen) > len(res) {
return nil, errors.New("invalid response data length")
}
targetData := res[offset : offset+int(dataLen)]
return &AutoPollResult{
Type: targetType,
TargetData: targetData,
}, nil
}
// InListPassiveTarget detects a single passive target using InListPassiveTarget command
func (d *Device) InListPassiveTarget(ctx context.Context, brTy byte) (*DetectedTag, error) {
const maxTg byte = 1
data := []byte{maxTg, brTy}
Debugf("InListPassiveTarget - maxTg=%d, brTy=0x%02X, transport=%s", maxTg, brTy, d.transport.Type())
res, err := d.executeInListPassiveTarget(ctx, data)
if err != nil {
return d.handleInListPassiveTargetError(ctx, err, brTy)
}
Debugf("InListPassiveTarget response (%d bytes): %X", len(res), res)
if err := d.validateInListPassiveTargetResponse(res); err != nil {
return nil, err
}
return d.parseInListPassiveTargetResponse(res)
}
// InListPassiveTargetWithTimeout detects a single passive target with timeout support.
// The mxRtyATR parameter controls the maximum retry count for target detection:
// - 0x00: Try once, no retry
// - 0x01-0xFE: Retry count (each retry is ~150ms according to PN532 datasheet)
// - 0xFF: Retry infinitely (blocking mode - use with caution)
//
// For continuous polling with card removal detection, use low values (0x01-0x10) to ensure
// the command returns quickly when no card is present.
func (d *Device) InListPassiveTargetWithTimeout(
ctx context.Context, brTy, mxRtyATR byte,
) (*DetectedTag, error) {
const maxTg byte = 1
data := []byte{maxTg, brTy, mxRtyATR}
Debugf("InListPassiveTargetWithTimeout - maxTg=%d, brTy=0x%02X, mxRtyATR=0x%02X, transport=%s",
maxTg, brTy, mxRtyATR, d.transport.Type())
res, err := d.executeInListPassiveTarget(ctx, data)
if err != nil {
return d.handleInListPassiveTargetError(ctx, err, brTy)
}
Debugf("InListPassiveTargetWithTimeout response (%d bytes): %X", len(res), res)
if err := d.validateInListPassiveTargetResponse(res); err != nil {
return nil, err
}
return d.parseInListPassiveTargetResponse(res)
}
// executeInListPassiveTarget sends the InListPassiveTarget command
func (d *Device) executeInListPassiveTarget(ctx context.Context, data []byte) ([]byte, error) {
// Dynamically adjust transport timeout based on mxRtyATR when provided
// mxRtyATR is the 3rd byte of the InListPassiveTarget payload
var prevTimeout time.Duration
if d.config != nil {
prevTimeout = d.config.Timeout
}
computed := d.computeInListHostTimeout(ctx, data)
Debugf("executeInListPassiveTarget: computed timeout=%v, prevTimeout=%v", computed, prevTimeout)
// Best-effort set; if it fails we'll proceed with previous timeout
if err := d.transport.SetTimeout(computed); err != nil {
Debugf("executeInListPassiveTarget: SetTimeout failed: %v", err)
}
// Restore previous timeout after the command completes
defer func() { _ = d.transport.SetTimeout(prevTimeout) }()
Debugln("executeInListPassiveTarget: calling SendCommand")
result, err := d.transport.SendCommand(ctx, cmdInListPassiveTarget, data)
if err != nil {
Debugf("executeInListPassiveTarget: SendCommand failed: %v", err)
return nil, fmt.Errorf("failed to send InListPassiveTarget command: %w", err)
}
Debugf("executeInListPassiveTarget: SendCommand succeeded, result len=%d", len(result))
return result, nil
}
// computeInListHostTimeout derives a host-side timeout from mxRtyATR and context deadline
// According to PN532 docs, each retry is ~150ms. We add baseline slack and bound by context if present.
func (d *Device) computeInListHostTimeout(ctx context.Context, data []byte) time.Duration {
var fallback time.Duration
if d.config != nil {
fallback = d.config.Timeout
}
// When mxRtyATR (3rd byte) is provided, derive a timeout from it.
if len(data) >= 3 {
return d.computeTimeoutFromRetryCount(ctx, data[2])
}
// No mxRtyATR provided; use context deadline if present or fallback
return d.getContextTimeoutOrFallback(ctx, fallback)
}
// computeTimeoutFromRetryCount calculates timeout based on mxRtyATR retry count
func (d *Device) computeTimeoutFromRetryCount(ctx context.Context, mx byte) time.Duration {
// 0xFF means infinite retry on hardware; rely on context or cap to a safe upper bound
if mx == 0xFF {
return d.handleInfiniteRetry(ctx)
}
// Each retry ~150ms; add small baseline slack for host/driver overhead
expected := time.Duration(int(mx))*150*time.Millisecond + 300*time.Millisecond
// When mx is explicitly set (not 0xFF), honor the calculated timeout directly
// without enforcing the device default minimum. This allows quick re-selects
// with low retry counts (e.g., mx=0x02 for ~600ms timeout).
// Only cap to reasonable maximum and respect context deadline.
expected = min(expected, 8*time.Second)
// Respect context deadline if sooner
return d.getContextTimeoutOrFallback(ctx, expected)
}
// handleInfiniteRetry handles the special case of infinite retry (0xFF)
func (*Device) handleInfiniteRetry(ctx context.Context) time.Duration {
if deadline, ok := ctx.Deadline(); ok {
if rem := time.Until(deadline); rem > 0 {
return rem
}
}
// No deadline; choose a conservative cap
return 10 * time.Second
}
// getContextTimeoutOrFallback returns context deadline if present and positive, otherwise fallback
func (*Device) getContextTimeoutOrFallback(ctx context.Context, fallback time.Duration) time.Duration {
if deadline, ok := ctx.Deadline(); ok {
if rem := time.Until(deadline); rem > 0 && rem < fallback {
return rem
}
}
return fallback
}
// handleInListPassiveTargetError handles command errors with clone device fallback
func (d *Device) handleInListPassiveTargetError(
ctx context.Context, err error, brTy byte,
) (*DetectedTag, error) {
Debugf("InListPassiveTarget command failed: %v", err)
// Check if this looks like a clone device compatibility issue
if strings.Contains(err.Error(), "clone device returned empty response") ||
strings.Contains(err.Error(), "need at least 2 bytes for status") {
Debugln("Clone device detected - InListPassiveTarget not supported, falling back to InAutoPoll")
return d.fallbackToInAutoPoll(ctx, brTy)
}
return nil, fmt.Errorf("InListPassiveTarget command failed: %w", err)
}
// validateInListPassiveTargetResponse validates the response format
func (*Device) validateInListPassiveTargetResponse(res []byte) error {
if res == nil || len(res) < 2 { //nolint:staticcheck // explicit nil check for nilaway
Debugf("Response too short (%d bytes) - may indicate clone device timing issue", len(res))
return fmt.Errorf("InListPassiveTarget response too short: got %d bytes, expected at least 2", len(res))
}
// Check response format: should start with 0x4B (InListPassiveTarget response)
if res[0] != 0x4B {
Debugf("Invalid response format - expected 0x4B response code, got: %X", res)
// Some clone devices may return wrapped responses - try to extract the actual PN532 response
if len(res) <= 2 || res[1] != 0x4B {
return fmt.Errorf("unexpected InListPassiveTarget response: expected 0x4B, got %v", res)
}
Debugln("Detected wrapped response, adjusting offset")
// Modify res in place to skip the wrapper byte
copy(res, res[1:])
}
return nil
}
// parseInListPassiveTargetResponse parses the response and creates a DetectedTag
func (d *Device) parseInListPassiveTargetResponse(res []byte) (*DetectedTag, error) {
if len(res) < 2 {
return nil, fmt.Errorf("response too short: %d bytes", len(res))
}
numTargets := res[1]
Debugf("InListPassiveTarget found %d targets", numTargets)
if numTargets == 0 {
Debugln("No targets detected - this may indicate clone device needs different timing or initialization")
return nil, nil //nolint:nilnil // nil tag, nil error is valid "no tag detected" response
}
// Parse first target only
tag, _, err := d.parseTargetAtOffset(res, 2, 1)
if err != nil {
return nil, err
}
return tag, nil
}
// parseTargetAtOffset parses a single target from the response at the given offset
func (d *Device) parseTargetAtOffset(res []byte, offset, targetIndex int) (*DetectedTag, int, error) {
Debugf("Parsing target %d at offset %d", targetIndex, offset)