This document tracks detailed implementation decisions, debugging sessions, and hardware setup for the FastPay project.
Hardware Components:
- NFC Module: DFRobot DFR0231-H Gravity (PN532 chip, firmware v1.6)
- USB-to-UART: Mayina FT232R (VID:PID 0403:6001)
- Serial Port:
/dev/tty.usbserial-ABSCDY4Z(macOS) - Platform: macOS (Darwin 24.6.0), Python 3.12.x
Wiring Configuration:
FT232 Mayina → DFRobot NFC Module
===================================
VCC (5V) → VCC [RED wire]
GND → GND [BLACK wire]
TXD → C/R [BLUE wire] ✅ TX/RX crossover
RXD → D/T [GREEN wire] ✅ TX/RX crossover
Initial Problem: PN532 module not responding to commands
Debugging Timeline:
-
Serial Port Test (
test-serial.py) - ✅ PASSED- FT232 detected successfully
- Port opens without errors at 115200 baud
-
Basic NFC Communication (
test-nfc.py) - ❌ FAILED- No response from GetFirmwareVersion command
- Module power LED confirmed on (hardware powered)
-
Comprehensive Diagnostics (
test-nfc-debug.py) - ❌ FAILED- Tested multiple frame formats
- Tried different timing sequences
- No ACK or response frames received
-
Baudrate Scan (
test-baudrates.py) - ❌ FAILED- Tested: 9600, 19200, 38400, 57600, 115200
- No response at any baudrate
-
BREAKTHROUGH (
test-wire-quality.py) -⚠️ PARTIAL SUCCESS- Module responded ONCE when DTR/RTS were toggled from HIGH to LOW
- Received firmware response:
00 00 ff 00 ff 00 00 00 ff 06 fa d5 03 32 01 06 07 e8 00 - Decoded: PN532 firmware v1.6, IC type 0x32, revision 7
- Key insight: DTR=False, RTS=False required for this FT232 module
-
ACK Protocol Implementation (
test-nfc-with-ack.py) -⚠️ INCONSISTENT- Implemented proper UART protocol flow:
- Send command → Read ACK (6 bytes) → Read response frame
- Proper flow control disabled (rtscts=False, dsrdtr=False, xonxoff=False)
- DTR/RTS forced LOW
- Still inconsistent responses
- Implemented proper UART protocol flow:
Root Cause Analysis:
The PN532 UART protocol requires:
- Control lines LOW: DTR=False, RTS=False (Python serial defaults to HIGH)
- ACK frame handling: Protocol expects ACK before response frame
- No flow control: All hardware/software flow control must be disabled
- Correct timing: Small delays required for signal stabilization
Solution: Adafruit Library
Instead of raw protocol implementation, switched to battle-tested library:
import serial
from adafruit_pn532.uart import PN532_UART
uart = serial.Serial(PORT, baudrate=115200, timeout=1)
uart.dtr = False # CRITICAL: Force DTR LOW
uart.rts = False # CRITICAL: Force RTS LOW
time.sleep(0.2) # Allow signals to stabilize
pn532 = PN532_UART(uart, debug=False)
ic, ver, rev, support = pn532.firmware_version # ✅ Works immediately!Result: Module detected perfectly on first try with Adafruit library
Time Investment: ~2 hours of systematic debugging
Key Lesson: Use proven libraries for complex protocols instead of raw implementations
Test: detect-phone-tap.py
Successfully detected 5 phone taps with different UIDs:
- Tap #1: 086AF124
- Tap #2: 08349FB5
- Tap #3: 08055B46
- Tap #4: 08BB33FB
- Tap #5: 08C2E70B
Findings:
- PN532 hardware interaction confirmed working
- Phone NFC changes UID on each tap (privacy feature)
- Detection latency: <100ms
- Reliable detection at 3-5cm range
Goal: Write raw JSON payment data to NTAG tag
Payment Payload:
{
"v": 1,
"addr": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
"merchant": "Alice's Coffee",
"amt": "5.00 USDC",
"item": "Latte",
"time": 1728404791
}Approach:
- Write raw JSON bytes starting at page 4 (NTAG user memory)
- 4 bytes per page
- No NDEF formatting
Result: Data written but phone couldn't read (not NDEF formatted)
Goal: Write complete FastPay payment request with all fields
Payload Size: 225 bytes (exceeds NTAG213 144-byte limit)
Result:
- Data truncated to 144 bytes
- Some page writes succeeded (✅)
- Many checksum errors (
⚠️ ) - Inconsistent write reliability
Observation: User was tapping PHONE (reader mode), not physical NFC tags
Goal: Write NDEF-formatted Text Record that phones can parse
NDEF Format:
Header: 0xD1 (MB=1, ME=1, SR=1, TNF=Well-known)
Type: 0x01 (length) + 'T' (Text record)
Payload: 0x02 + 'en' (language) + JSON text
TLV Wrapper:
0x03 - NDEF Message TLV
[length] - Payload size
[NDEF data] - The actual record
0xFE - Terminator TLV
Capability Container (Page 3):
0xE1 - Magic number
0x10 - Version 1.0
0x12 - Memory size (144 bytes for NTAG213)
0x00 - Full read/write access
Result:
- CC page write failed with checksum error 132
- Library rejected invalid checksum
- Could not establish proper NDEF formatting
Goal: Check if tag already NDEF formatted, skip CC if needed
Logic:
- Read page 3 (CC)
- If CC[0] == 0xE1, tag already formatted → skip CC write
- If not formatted, try to write CC
- Write data pages regardless of CC success
Result:
- Only 7/30 page writes succeeded
- Most pages failed with checksum errors
- Inconsistent write behavior
Final Realization: User doesn't have physical NTAG tags yet, was tapping phone in reader mode
Initial Exploration: Testing card emulation as potential approach for NFC communication
Finding: After extensive testing, card emulation is not viable for FastPay production. See Phase 1 conclusion below.
Approach: Use Adafruit library's TgInitAsTarget method
Code:
response = pn532.TgInitAsTarget(
mode=0x05, # PICC only + Passive
mifare_params=mifare_params,
felica_params=felica_params,
nfcid3t=nfcid3t,
timeout=1
)Result: ❌ AttributeError: 'PN532_UART' object has no attribute 'TgInitAsTarget'
Finding: Adafruit CircuitPython PN532 library doesn't implement card emulation methods
Approach: Implement card emulation using raw PN532 protocol commands
PN532 Commands Implemented:
-
TgInitAsTarget (0x8C) - Initialize as NFC target
params = bytearray() params.append(0x05) # Mode: PICC only + Passive params.extend([0x04, 0x00]) # SENS_RES (Type 4 Tag) params.extend([0x12, 0x34, 0x56]) # NFCID1t (UID) params.append(0x40) # SEL_RES (ISO14443-4 compliant) params.extend([...]) # FeliCa params (required but unused) params.extend([...]) # NFCID3t params.append(0x00) # General bytes length params.append(0x00) # Historical bytes length
-
TgGetData (0x86) - Receive data from phone (initiator)
def tg_get_data(ser): return send_command(ser, 0x86, [])
-
TgSetData (0x8E) - Send data to phone
def tg_set_data(ser, data): return send_command(ser, 0x8E, list(data))
ISO-DEP APDU Command Handling:
Phone sends APDU commands to Type 4 Tags:
-
SELECT (0xA4): Select application/file
- Response:
90 00(success)
- Response:
-
READ BINARY (0xB0): Read file contents
- Response:
[NDEF data...] 90 00
- Response:
-
Unknown commands:
- Response:
6A 82(file not found/not supported)
- Response:
Payment Data:
{
"v": 1,
"merchant": "Alice's Coffee",
"amount": "5.00 USDC",
"item": "Latte",
"addr": "0x742d...bEb",
"time": 1728415455
}NDEF Encoding:
def create_ndef_text(text):
text_bytes = text.encode('utf-8')
record = bytearray()
record.append(0xD1) # Header
record.append(0x01) # Type length
record.append(len(text_bytes) + 3) # Payload length
record.append(0x54) # Type: T
record.append(0x02) # Language length
record.extend(b'en')
record.extend(text_bytes)
return bytes(record)Main Loop:
while True:
# Wait for phone tap (blocks until tap or timeout)
response = tg_init_as_target(ser, ndef_msg)
if response:
# Phone detected! Handle ISO-DEP session
while session_active:
# Get command from phone
cmd = tg_get_data(ser)
# Parse APDU
if cmd[1] == 0xA4: # SELECT
tg_set_data(ser, bytes([0x90, 0x00]))
elif cmd[1] == 0xB0: # READ BINARY
tg_set_data(ser, ndef_msg[:50] + bytes([0x90, 0x00]))
else:
tg_set_data(ser, bytes([0x6A, 0x82]))Status: ✅ Implementation complete, ready for testing
Next Step: User needs to run python3 test/card-emulation-raw.py and tap phone
Decision: Use adafruit-circuitpython-pn532 for PN532 communication
Rationale:
- Raw UART protocol is complex (ACK frames, checksums, timing)
- 2 hours of debugging raw implementation vs instant success with library
- Library handles edge cases and module-specific quirks
- Proven reliability across different PN532 modules
- Active maintenance and community support
Trade-off: Library doesn't support card emulation, requiring raw commands for TgInitAsTarget
Solution: Hybrid approach - Use library for initialization, raw commands for card emulation
Decision: PN532 acts as reader to detect phone taps, payment data flows via Coinbase Commerce API
Rationale:
- Customer needs internet to broadcast to Base L2 blockchain anyway
- Coinbase Commerce provides proven hosted checkout
- No card emulation complexity (UART timing issues)
- No physical tags needed
- Tap just provides UX convenience (alternative to QR scan)
- Scalable to high transaction volume
Implementation:
- PN532 in reader mode detects ISO14443A devices
- Extracts UID from tapped phone
- Associates tap with pending Coinbase Commerce charge
- Customer completes payment via hosted checkout
- Webhook confirms payment to terminal
Decision: Always set DTR=False, RTS=False after opening serial port
Rationale:
- Python serial library defaults to DTR=True, RTS=True (HIGH)
- Some FT232 modules (like Mayina) use these for flow control
- PN532 module misinterprets HIGH signals as flow control assertion
- Setting LOW disables hardware flow control, allowing communication
Platform Specificity:
- Required for: USB-to-UART converters (FT232, CP2102, CH340)
- Not required for: Raspberry Pi GPIO UART (direct connection)
- Environment flag:
NFC_DTR_LOW=truefor conditional application
- test-adafruit-pn532.py - Firmware detection and SAM configuration
- detect-phone-tap.py - Phone tap detection and UID reading (PRODUCTION APPROACH)
- test-serial.py - Serial port verification
- test-nfc.py - Raw protocol implementation (reference)
- test-nfc-debug.py - Comprehensive diagnostics
- test-baudrates.py - Baudrate scanning
- test-wire-quality.py - Control signal testing (found DTR/RTS issue)
- test-nfc-with-ack.py - ACK frame handling implementation
- test-nfc-wakeup.py - Wake-up sequence testing
Card Emulation Experiments:
- card-emulation-hybrid.py - nfcpy card emulation attempt
- card-emulation-simple.py - Adafruit library emulation attempt
- card-emulation-raw.py - Raw PN532 command emulation attempt
Tag Writing Experiments:
- write-simple-payment.py - Raw data writing
- write-full-payment.py - Complete payment request
- write-ndef-formatted.py - NDEF format with CC
- write-ndef-smart.py - Smart NDEF detection and writing
- write-payment-request.py - Structured payment data
- write-payment-tag.py - NTAG tag writing (hardware validation)
- write-to-tag.py - Basic tag writing
Note: These scripts proved the hardware works but are not used in production. FastPay uses reader mode, not card emulation or tag writing.
Serial Port: /dev/tty.usbserial-ABSCDY4Z (FT232 detected via VID:PID 0403:6001)
Requirements:
- DTR/RTS must be LOW
- No driver installation needed (macOS built-in FTDI support)
- Permissions: Usually no sudo required
Testing Commands:
ls /dev/tty.usbserial* # List USB serial devices
python3 test/test-adafruit-pn532.py # Quick verificationSerial Port: /dev/ttyAMA0 (GPIO UART)
Requirements:
- Disable serial console in
/boot/config.txt - Enable UART:
enable_uart=1 - DTR/RTS not applicable (direct GPIO connection)
- Add user to dialout group:
sudo usermod -a -G dialout $USER
Config Changes:
# /boot/config.txt
enable_uart=1
dtoverlay=disable-bt # Disable Bluetooth (conflicts with UART)Platform-agnostic NFC bridge:
import os
import serial
from adafruit_pn532.uart import PN532_UART
# Platform detection
if os.path.exists('/dev/ttyAMA0'):
PORT = '/dev/ttyAMA0' # Raspberry Pi
NEEDS_DTR_RTS_FIX = False
elif os.path.exists('/dev/tty.usbserial-ABSCDY4Z'):
PORT = '/dev/tty.usbserial-ABSCDY4Z' # macOS dev
NEEDS_DTR_RTS_FIX = True
else:
PORT = os.getenv('NFC_PORT', '/dev/ttyUSB0') # Linux USB
NEEDS_DTR_RTS_FIX = True
uart = serial.Serial(PORT, 115200, timeout=1)
if NEEDS_DTR_RTS_FIX:
uart.dtr = False
uart.rts = False
time.sleep(0.2)
pn532 = PN532_UART(uart)After extensive testing of three approaches:
- Card Emulation - ❌ Not viable (PN532 over UART too slow for ISO-DEP timing)
- Physical Tag Writing - ✅ Hardware validated but not needed for production
- Reader Mode - ✅ SELECTED for production
Why Reader Mode:
- Customers need internet for blockchain anyway (Base L2 settlement)
- Coinbase Commerce provides proven payment infrastructure
- NFC tap is UX convenience (alternative to QR code)
- No card emulation complexity
- Scalable and reliable
Payment Flow:
1. Terminal creates Coinbase Commerce charge
2. Displays QR code + "Tap to Pay" prompt
3. PN532 reader detects phone tap (extracts UID)
4. Associates tap with pending charge
5. Customer opens wallet, sees charge (via QR or deep link)
6. Customer approves transaction
7. Broadcasts to Base L2
8. Webhook confirms payment to terminal
-
Create terminal/ directory structure
- Node.js business logic
- Python NFC reader bridge
- IPC via stdin/stdout
-
Coinbase Commerce Integration
- Charge creation API
- Webhook endpoint
- Payment confirmation
-
Reader Mode NFC Bridge
- Port
detect-phone-tap.pyto production - Add JSON IPC output
- Implement tap debouncing
- Port
-
Test End-to-End Flow
- Create charge → Display QR → Detect tap → Verify payment
- Escrow Protocol Integration - If bypassing Coinbase hosted checkout
- Direct Blockchain Monitoring - Base L2 event monitoring
- Custom Contract Deployment - Base Sepolia testnet
- Raspberry Pi Migration - Same code, change NFC_PORT
- Merchant Pilot Prep - Packaging, documentation
- Video Demo - Record end-to-end payment
Document Status: Living document, updated as implementation progresses
Last Updated: October 14, 2025 - Architecture finalized: Reader Mode selected