Skip to content

Commit e2a1f73

Browse files
authored
Merge pull request #15 from ZaparooProject/fix/i2c-broken
fix: improve I2C transport reliability with timeout increase and robust frame handling
2 parents 868fca3 + 5cc7733 commit e2a1f73

File tree

3 files changed

+158
-83
lines changed

3 files changed

+158
-83
lines changed

bumpers.yml

Lines changed: 0 additions & 35 deletions
This file was deleted.

polling/device_actor.go

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -94,32 +94,13 @@ func (da *DeviceActor) pollLoop() {
9494
atomic.StoreInt64(&da.running, 0)
9595
}()
9696

97+
// Perform immediate poll before entering ticker loop for responsive startup
98+
da.performPoll()
99+
97100
for {
98101
select {
99102
case <-ticker.C:
100-
if da.device == nil || da.callbacks.OnCardDetected == nil {
101-
continue
102-
}
103-
104-
start := time.Now()
105-
detectedTags, err := da.device.InitiatorListPassiveTargets(1, pn532.TagTypeAny, nil)
106-
pollDuration := time.Since(start)
107-
108-
// Track poll cycle
109-
atomic.AddInt64(&da.pollCycles, 1)
110-
atomic.StoreInt64(&da.lastPollLatency, pollDuration.Nanoseconds())
111-
112-
if err != nil {
113-
// Track poll error
114-
atomic.AddInt64(&da.pollErrors, 1)
115-
}
116-
117-
if err == nil && len(detectedTags) > 0 {
118-
// Track card detected and update timestamp
119-
atomic.AddInt64(&da.cardsDetected, 1)
120-
atomic.StoreInt64(&da.lastCardDetection, start.UnixNano())
121-
_ = da.callbacks.OnCardDetected(detectedTags[0])
122-
}
103+
da.performPoll()
123104

124105
// Adaptive polling: adjust interval based on card presence
125106
da.adjustPollInterval()
@@ -133,6 +114,33 @@ func (da *DeviceActor) pollLoop() {
133114
}
134115
}
135116

117+
// performPoll executes a single polling cycle
118+
func (da *DeviceActor) performPoll() {
119+
if da.device == nil || da.callbacks.OnCardDetected == nil {
120+
return
121+
}
122+
123+
start := time.Now()
124+
detectedTags, err := da.device.InitiatorListPassiveTargets(1, pn532.TagTypeAny, nil)
125+
pollDuration := time.Since(start)
126+
127+
// Track poll cycle
128+
atomic.AddInt64(&da.pollCycles, 1)
129+
atomic.StoreInt64(&da.lastPollLatency, pollDuration.Nanoseconds())
130+
131+
if err != nil {
132+
// Track poll error
133+
atomic.AddInt64(&da.pollErrors, 1)
134+
}
135+
136+
if err == nil && len(detectedTags) > 0 {
137+
// Track card detected and update timestamp
138+
atomic.AddInt64(&da.cardsDetected, 1)
139+
atomic.StoreInt64(&da.lastCardDetection, start.UnixNano())
140+
_ = da.callbacks.OnCardDetected(detectedTags[0])
141+
}
142+
}
143+
136144
// adjustPollInterval implements adaptive polling logic
137145
func (da *DeviceActor) adjustPollInterval() {
138146
now := time.Now().UnixNano()

transport/i2c/i2c.go

Lines changed: 127 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ func New(busName string) (*Transport, error) {
8383
transport := &Transport{
8484
dev: dev,
8585
busName: busName,
86-
timeout: 50 * time.Millisecond,
86+
// Match UART's unified timeout - originally 50ms but increased to 100ms
87+
// for better I2C bus compatibility across different hardware
88+
timeout: 100 * time.Millisecond,
8789
}
8890

8991
return transport, nil
@@ -142,20 +144,41 @@ func (*Transport) Type() pn532.TransportType {
142144
}
143145

144146
// checkReady checks if the PN532 is ready by reading the ready status
147+
// Now includes retry logic with exponential backoff for better hardware compatibility
145148
func (t *Transport) checkReady() error {
146-
// Use buffer pool for ready status check - small optimization
147-
ready := frame.GetSmallBuffer(1)
148-
defer frame.PutBuffer(ready)
149+
const maxRetries = 5
150+
baseDelay := time.Millisecond
149151

150-
if err := t.dev.Tx(nil, ready); err != nil {
151-
return fmt.Errorf("I2C ready check failed: %w", err)
152-
}
152+
var lastErr error
153+
for attempt := 0; attempt < maxRetries; attempt++ {
154+
// Use buffer pool for ready status check - small optimization
155+
ready := frame.GetSmallBuffer(1)
156+
157+
err := t.dev.Tx(nil, ready)
158+
if err != nil {
159+
frame.PutBuffer(ready)
160+
lastErr = fmt.Errorf("I2C ready check failed: %w", err)
161+
// Exponential backoff: 1ms, 2ms, 4ms, 8ms, 16ms
162+
if attempt < maxRetries-1 {
163+
time.Sleep(baseDelay * time.Duration(1<<attempt))
164+
continue
165+
}
166+
return lastErr
167+
}
168+
169+
if ready[0] == pn532Ready {
170+
frame.PutBuffer(ready)
171+
return nil
172+
}
153173

154-
if ready[0] != pn532Ready {
155-
return pn532.NewTransportNotReadyError("waitReady", t.busName)
174+
frame.PutBuffer(ready)
175+
// Device not ready yet, wait with backoff
176+
if attempt < maxRetries-1 {
177+
time.Sleep(baseDelay * time.Duration(1<<attempt))
178+
}
156179
}
157180

158-
return nil
181+
return pn532.NewTransportNotReadyError("checkReady", t.busName)
159182
}
160183

161184
// sendFrame sends a frame to the PN532 via I2C
@@ -295,18 +318,18 @@ func (t *Transport) receiveFrameAttempt() (data []byte, shouldRetry bool, err er
295318
return nil, true, nil
296319
}
297320

298-
buf, err := t.readFrameData()
321+
buf, actualLen, err := t.readFrameData()
299322
if err != nil {
300323
return nil, false, err
301324
}
302325
defer frame.PutBuffer(buf) // Ensure buffer is returned to pool
303326

304-
off, err := t.findI2CFrameStart(buf)
327+
off, err := t.findI2CFrameStart(buf, actualLen)
305328
if err != nil {
306329
return nil, false, err
307330
}
308331

309-
frameLen, shouldRetry, err := t.validateI2CFrameLength(buf, off)
332+
frameLen, shouldRetry, err := t.validateI2CFrameLength(buf, off, actualLen)
310333
if err != nil || shouldRetry {
311334
return nil, shouldRetry, err
312335
}
@@ -319,21 +342,99 @@ func (t *Transport) receiveFrameAttempt() (data []byte, shouldRetry bool, err er
319342
return t.extractI2CFrameData(buf, off, frameLen)
320343
}
321344

322-
// readFrameData reads frame data from I2C using buffer pool
323-
func (t *Transport) readFrameData() ([]byte, error) {
324-
// Use buffer pool for frame reading - major optimization
325-
buf := frame.GetBuffer(255 + 7) // max data + frame overhead
326-
if err := t.dev.Tx(nil, buf); err != nil {
327-
frame.PutBuffer(buf) // Return to pool on error
328-
return nil, fmt.Errorf("I2C frame data read failed: %w", err)
345+
// readFrameData reads frame data from I2C using incremental reads
346+
// This fixes the critical bug where we were reading a fixed-size buffer without
347+
// knowing how much data was actually received from the PN532
348+
func (t *Transport) readFrameData() (buf []byte, actualLen int, err error) {
349+
// PHASE 1: Read frame header to determine frame size
350+
// Frame header structure: [preamble] [0x00] [0xFF] [LEN] [LCS] = 5 bytes minimum
351+
// We read a bit more to get the TFI byte and start of data
352+
headerSize := 32 // Read enough to get header + some data
353+
headerBuf := frame.GetSmallBuffer(headerSize)
354+
355+
if err := t.dev.Tx(nil, headerBuf); err != nil {
356+
frame.PutBuffer(headerBuf)
357+
return nil, 0, fmt.Errorf("I2C frame header read failed: %w", err)
329358
}
330359

331-
return buf, nil
360+
// Find frame start in header
361+
frameStart := -1
362+
for i := 0; i < headerSize-1; i++ {
363+
if headerBuf[i] == 0x00 && headerBuf[i+1] == 0xFF {
364+
frameStart = i + 2 // Point to length byte
365+
break
366+
}
367+
}
368+
369+
if frameStart == -1 || frameStart+1 >= headerSize {
370+
frame.PutBuffer(headerBuf)
371+
return nil, 0, &pn532.TransportError{
372+
Op: "readFrameData",
373+
Port: t.busName,
374+
Err: pn532.ErrFrameCorrupted,
375+
Type: pn532.ErrorTypeTransient,
376+
Retryable: true,
377+
}
378+
}
379+
380+
// Parse frame length
381+
frameLen := int(headerBuf[frameStart])
382+
lengthChecksum := headerBuf[frameStart+1]
383+
384+
// Validate length checksum
385+
if ((frameLen + int(lengthChecksum)) & 0xFF) != 0 {
386+
frame.PutBuffer(headerBuf)
387+
return nil, 0, &pn532.TransportError{
388+
Op: "readFrameData",
389+
Port: t.busName,
390+
Err: pn532.ErrFrameCorrupted,
391+
Type: pn532.ErrorTypeTransient,
392+
Retryable: true,
393+
}
394+
}
395+
396+
// PHASE 2: Calculate total frame size and check if we need more data
397+
// Total frame: [preamble] [0x00] [0xFF] [LEN] [LCS] [TFI] [data...] [DCS] [postamble]
398+
// = frameStart + 2 (LEN+LCS) + frameLen + 1 (DCS) + 1 (postamble)
399+
totalFrameSize := frameStart + 2 + frameLen + 2
400+
401+
// If header buffer has all the data, use it
402+
if totalFrameSize <= headerSize {
403+
return headerBuf, totalFrameSize, nil
404+
}
405+
406+
// PHASE 3: Need to read more data - allocate bigger buffer and copy header
407+
buf = frame.GetBuffer(totalFrameSize)
408+
copy(buf, headerBuf[:headerSize])
409+
frame.PutBuffer(headerBuf) // Return small buffer to pool
410+
411+
// Read remaining data
412+
remainingSize := totalFrameSize - headerSize
413+
remainingBuf := frame.GetSmallBuffer(remainingSize)
414+
defer frame.PutBuffer(remainingBuf)
415+
416+
if err := t.dev.Tx(nil, remainingBuf); err != nil {
417+
frame.PutBuffer(buf)
418+
return nil, 0, fmt.Errorf("I2C remaining frame data read failed: %w", err)
419+
}
420+
421+
// Copy remaining data to main buffer
422+
copy(buf[headerSize:], remainingBuf[:remainingSize])
423+
424+
return buf, totalFrameSize, nil
332425
}
333426

334427
// findI2CFrameStart locates the frame start marker (0x00 0xFF)
335-
func (t *Transport) findI2CFrameStart(buf []byte) (int, error) {
336-
for off := 0; off < len(buf)-1; off++ {
428+
// CRITICAL FIX: Now accepts actualLen to only search through actual received data
429+
// This prevents false positives from searching uninitialized buffer space
430+
func (t *Transport) findI2CFrameStart(buf []byte, actualLen int) (int, error) {
431+
// Only search through actual data received, not entire buffer
432+
searchLen := actualLen
433+
if searchLen > len(buf) {
434+
searchLen = len(buf)
435+
}
436+
437+
for off := 0; off < searchLen-1; off++ {
337438
if buf[off] == 0x00 && buf[off+1] == 0xFF {
338439
return off + 2, nil // Skip to length byte
339440
}
@@ -348,8 +449,9 @@ func (t *Transport) findI2CFrameStart(buf []byte) (int, error) {
348449
}
349450

350451
// validateI2CFrameLength validates the frame length and its checksum
351-
func (t *Transport) validateI2CFrameLength(buf []byte, off int) (frameLen int, shouldRetry bool, err error) {
352-
frameLen, shouldRetry, err = frame.ValidateFrameLength(buf, off-1, len(buf), "receiveFrame", t.busName)
452+
// CRITICAL FIX: Now uses actualLen instead of len(buf) to avoid reading beyond actual data
453+
func (t *Transport) validateI2CFrameLength(buf []byte, off, actualLen int) (frameLen int, shouldRetry bool, err error) {
454+
frameLen, shouldRetry, err = frame.ValidateFrameLength(buf, off-1, actualLen, "receiveFrame", t.busName)
353455
if err != nil {
354456
return frameLen, shouldRetry, fmt.Errorf("I2C frame length validation failed: %w", err)
355457
}

0 commit comments

Comments
 (0)