@@ -45,6 +45,14 @@ func (d *Device) InitContext(ctx context.Context) error {
4545 }
4646 }
4747
48+ // Configure finite passive activation retries to prevent infinite wait lockups
49+ // Use 10 retries (~1 second) instead of default 0xFF (infinite)
50+ if err := d .SetPassiveActivationRetries (0x0A ); err != nil {
51+ // Log but don't fail initialization - this is an optimization, not critical
52+ // Some older firmware versions might not support this configuration
53+ _ = err
54+ }
55+
4856 // Get firmware version (if supported by transport)
4957 if ! skipFirmwareVersion {
5058 if err := d .setupFirmwareVersion (ctx ); err != nil {
@@ -370,6 +378,20 @@ func (d *Device) detectTagsWithInListPassiveTarget(
370378 return nil , fmt .Errorf ("transport preparation failed: %w" , err )
371379 }
372380
381+ // Release any previously selected targets to clear HALT states
382+ // This addresses intermittent "empty valid tag" issues where tags get stuck
383+ if err := d .InReleaseContext (ctx , 0 ); err != nil {
384+ debugf ("InRelease failed, continuing anyway: %v" , err )
385+ // Don't fail the operation if InRelease fails - it's an optimization
386+ }
387+
388+ // Small delay to allow RF field and tags to stabilize after release
389+ select {
390+ case <- ctx .Done ():
391+ return nil , ctx .Err ()
392+ case <- time .After (10 * time .Millisecond ):
393+ }
394+
373395 // Use InListPassiveTarget for legacy compatibility
374396 return d .InListPassiveTargetContext (ctx , maxTags , baudRate )
375397}
@@ -549,7 +571,7 @@ func (d *Device) SendDataExchangeContext(ctx context.Context, data []byte) ([]by
549571 // Check for error frame (TFI = 0x7F)
550572 if len (res ) >= 2 && res [0 ] == 0x7F {
551573 errorCode := res [1 ]
552- return nil , fmt . Errorf ( "PN532 error: 0x%02X" , errorCode )
574+ return nil , NewPN532Error ( errorCode , "InDataExchange" , "" )
553575 }
554576
555577 if len (res ) < 2 || res [0 ] != 0x41 {
@@ -571,7 +593,7 @@ func (d *Device) SendRawCommandContext(ctx context.Context, data []byte) ([]byte
571593 // Check for error frame (TFI = 0x7F)
572594 if len (res ) >= 2 && res [0 ] == 0x7F {
573595 errorCode := res [1 ]
574- return nil , fmt . Errorf ( "PN532 error: 0x%02X" , errorCode )
596+ return nil , NewPN532Error ( errorCode , "InCommunicateThru" , "" )
575597 }
576598
577599 if len (res ) < 2 || res [0 ] != 0x43 {
@@ -750,13 +772,93 @@ func (*Device) normalizeMaxTargets(maxTg byte) byte {
750772
751773// executeInListPassiveTarget sends the InListPassiveTarget command
752774func (d * Device ) executeInListPassiveTarget (ctx context.Context , data []byte ) ([]byte , error ) {
775+ // Dynamically adjust transport timeout based on mxRtyATR when provided
776+ // mxRtyATR is the 3rd byte of the InListPassiveTarget payload
777+ var prevTimeout time.Duration
778+ if d .config != nil {
779+ prevTimeout = d .config .Timeout
780+ }
781+ computed := d .computeInListHostTimeout (ctx , data )
782+ // Best-effort set; if it fails we'll proceed with previous timeout
783+ _ = d .transport .SetTimeout (computed )
784+ // Restore previous timeout after the command completes
785+ defer func () { _ = d .transport .SetTimeout (prevTimeout ) }()
786+
753787 result , err := d .transport .SendCommandWithContext (ctx , cmdInListPassiveTarget , data )
754788 if err != nil {
755789 return nil , fmt .Errorf ("failed to send InListPassiveTarget command: %w" , err )
756790 }
757791 return result , nil
758792}
759793
794+ // computeInListHostTimeout derives a host-side timeout from mxRtyATR and context deadline
795+ // According to PN532 docs, each retry is ~150ms. We add baseline slack and bound by context if present.
796+ func (d * Device ) computeInListHostTimeout (ctx context.Context , data []byte ) time.Duration {
797+ var fallback time.Duration
798+ if d .config != nil {
799+ fallback = d .config .Timeout
800+ }
801+
802+ // When mxRtyATR (3rd byte) is provided, derive a timeout from it.
803+ if len (data ) >= 3 {
804+ return d .computeTimeoutFromRetryCount (ctx , data [2 ], fallback )
805+ }
806+
807+ // No mxRtyATR provided; use context deadline if present or fallback
808+ return d .getContextTimeoutOrFallback (ctx , fallback )
809+ }
810+
811+ // computeTimeoutFromRetryCount calculates timeout based on mxRtyATR retry count
812+ func (d * Device ) computeTimeoutFromRetryCount (ctx context.Context , mx byte , fallback time.Duration ) time.Duration {
813+ // 0xFF means infinite retry on hardware; rely on context or cap to a safe upper bound
814+ if mx == 0xFF {
815+ return d .handleInfiniteRetry (ctx )
816+ }
817+
818+ // Each retry ~150ms; add small baseline slack for host/driver overhead
819+ expected := time .Duration (int (mx ))* 150 * time .Millisecond + 300 * time .Millisecond
820+
821+ // Apply bounds and respect context deadline
822+ return d .applyTimeoutBounds (ctx , expected , fallback )
823+ }
824+
825+ // handleInfiniteRetry handles the special case of infinite retry (0xFF)
826+ func (* Device ) handleInfiniteRetry (ctx context.Context ) time.Duration {
827+ if deadline , ok := ctx .Deadline (); ok {
828+ if rem := time .Until (deadline ); rem > 0 {
829+ return rem
830+ }
831+ }
832+ // No deadline; choose a conservative cap
833+ return 10 * time .Second
834+ }
835+
836+ // applyTimeoutBounds ensures timeout is within reasonable bounds and respects context
837+ func (d * Device ) applyTimeoutBounds (ctx context.Context , expected , fallback time.Duration ) time.Duration {
838+ // Ensure we don't go below device default
839+ if expected < fallback {
840+ expected = fallback
841+ }
842+
843+ // Cap to a reasonable maximum to avoid excessive blocking when mx is large
844+ if expected > 8 * time .Second {
845+ expected = 8 * time .Second
846+ }
847+
848+ // Respect context deadline if sooner
849+ return d .getContextTimeoutOrFallback (ctx , expected )
850+ }
851+
852+ // getContextTimeoutOrFallback returns context deadline if present and positive, otherwise fallback
853+ func (* Device ) getContextTimeoutOrFallback (ctx context.Context , fallback time.Duration ) time.Duration {
854+ if deadline , ok := ctx .Deadline (); ok {
855+ if rem := time .Until (deadline ); rem > 0 && rem < fallback {
856+ return rem
857+ }
858+ }
859+ return fallback
860+ }
861+
760862// handleInListPassiveTargetError handles command errors with clone device fallback
761863func (d * Device ) handleInListPassiveTargetError (
762864 ctx context.Context , err error , maxTg , brTy byte ,
0 commit comments