Skip to content

Commit df2e849

Browse files
add PICC module, PiccTransceiver trait, and loopback example
PICC (card emulation) module mirroring the PCD side: - PiccTransceiver trait with separate receive()/send() - Picc struct with wait_for_activation(), wait_for_rats(), receive_command(), send_response() - PiccConfig with Uid enum (Single/Double/Triple) enforcing valid UID sizes, and enable_14443_4() for ISO14443-4 tags - Transparent PPS handling in receive_command() - DESELECT transitions to HALT state per ISO14443-3 Supporting changes: - activate() + wakeup() for REQA vs WUPA activation - Serializers: AtqA::to_bytes(), Sak::to_byte(), Ats::to_bytes()/new() - Fix R(ACK) block number in process_iblock (build before toggle) - Fix BitFrameAntiCollistion typo → BitFrameAntiCollision - New re-exports: Uid, Fsci, Ta, Tb, Tc, Fwi, Sfgi, Dxi, UidSize Loopback example exercising the full protocol: - Triple UID (3 cascade levels) - RATS/ATS + PPS negotiation - Short APDU exchange + long APDU with I-Block chaining - DESELECT → HALT → WUPA re-activation → second session
1 parent bd2c373 commit df2e849

File tree

11 files changed

+1163
-28
lines changed

11 files changed

+1163
-28
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@ hex = "0.4"
3030
[[example]]
3131
name = "cli_parser"
3232
path = "example/cli_parser.rs"
33+
34+
[[example]]
35+
name = "loopback"
36+
path = "example/loopback.rs"
37+
required-features = ["std"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl PcdTransceiver for MyTransceiver {
4747
todo!("implement for your hardware")
4848
}
4949

50-
fn enable_hw_crc(&mut self) -> Result<(), MyError> {
50+
fn try_enable_hw_crc(&mut self) -> Result<(), MyError> {
5151
// Enable hardware CRC if the chip supports it.
5252
// Return Err to fall back to software CRC.
5353
Err(MyError)

example/loopback.rs

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
//! Loopback example: PCD and PICC communicating over in-memory channels.
5+
//!
6+
//! Demonstrates the full ISO14443 protocol flow:
7+
//! 1. Activation (REQA → ATQA → anticollision → SELECT → SAK)
8+
//! 2. RATS/ATS negotiation
9+
//! 3. APDU exchange
10+
//! 4. DESELECT
11+
//!
12+
//! Run with: `cargo run --example loopback --features std`
13+
14+
use std::sync::mpsc::{self, Receiver, Sender};
15+
use std::thread;
16+
17+
use iso14443::type_a::{
18+
Ats, Cid, Dxi, Frame, Fsci, Fsdi, PcdTransceiver, PiccTransceiver, Ta, Tb, Tc,
19+
activation::{activate, wakeup},
20+
pcd::Pcd,
21+
picc::{Picc, PiccConfig},
22+
};
23+
24+
// ── Channel-based transceivers with frame logging ───────────────────────
25+
26+
#[derive(Debug)]
27+
struct ChannelError;
28+
29+
/// PCD-side transceiver: sends a frame, waits for the PICC response.
30+
struct ChannelPcd {
31+
tx: Sender<Vec<u8>>,
32+
rx: Receiver<Vec<u8>>,
33+
}
34+
35+
impl PcdTransceiver for ChannelPcd {
36+
type Error = ChannelError;
37+
38+
fn transceive(&mut self, frame: &Frame) -> Result<Vec<u8>, ChannelError> {
39+
let data = frame.data().to_vec();
40+
println!(" PCD → PICC [{:02x?}]", data);
41+
self.tx.send(data).map_err(|_| ChannelError)?;
42+
let resp = self.rx.recv().map_err(|_| ChannelError)?;
43+
println!(" PCD ← PICC [{:02x?}]", resp);
44+
Ok(resp)
45+
}
46+
47+
fn try_enable_hw_crc(&mut self) -> Result<(), ChannelError> {
48+
Err(ChannelError) // no HW CRC in loopback
49+
}
50+
}
51+
52+
/// PICC-side transceiver: waits for a frame from the PCD, sends response.
53+
struct ChannelPicc {
54+
tx: Sender<Vec<u8>>,
55+
rx: Receiver<Vec<u8>>,
56+
}
57+
58+
impl PiccTransceiver for ChannelPicc {
59+
type Error = ChannelError;
60+
61+
fn receive(&mut self) -> Result<Vec<u8>, ChannelError> {
62+
self.rx.recv().map_err(|_| ChannelError)
63+
}
64+
65+
fn send(&mut self, frame: &Frame) -> Result<(), ChannelError> {
66+
self.tx
67+
.send(frame.data().to_vec())
68+
.map_err(|_| ChannelError)
69+
}
70+
71+
fn try_enable_hw_crc(&mut self) -> Result<(), ChannelError> {
72+
Err(ChannelError)
73+
}
74+
}
75+
76+
/// Create a linked pair of PCD and PICC channel transceivers.
77+
fn channel_pair() -> (ChannelPcd, ChannelPicc) {
78+
let (pcd_tx, picc_rx) = mpsc::channel();
79+
let (picc_tx, pcd_rx) = mpsc::channel();
80+
(
81+
ChannelPcd {
82+
tx: pcd_tx,
83+
rx: pcd_rx,
84+
},
85+
ChannelPicc {
86+
tx: picc_tx,
87+
rx: picc_rx,
88+
},
89+
)
90+
}
91+
92+
// ── Main ────────────────────────────────────────────────────────────────
93+
94+
fn main() {
95+
let (mut pcd_side, mut picc_side) = channel_pair();
96+
97+
// PICC thread: card emulation
98+
let picc_handle = thread::spawn(move || {
99+
let mut config = PiccConfig::new(iso14443::type_a::Uid::Triple([
100+
0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE, 0xBA, 0xBE, 0x13, 0x37,
101+
]));
102+
// Small FSC (16 bytes) to demonstrate I-Block chaining
103+
config.enable_14443_4(Ats::new(
104+
Fsci::Fsc16,
105+
Ta::SAME_D_SUPP,
106+
Tb::default(),
107+
Tc::CID_SUPP,
108+
));
109+
110+
let mut picc = Picc::new(&mut picc_side, config);
111+
112+
// Serve multiple sessions (activation → exchange → deselect → re-activation)
113+
for session in 1..=2 {
114+
picc.wait_for_activation().unwrap();
115+
println!("[PICC] Activation complete (session {session})");
116+
117+
picc.wait_for_rats().unwrap();
118+
println!("[PICC] RATS/ATS complete");
119+
120+
loop {
121+
match picc.receive_command() {
122+
Ok(apdu) => {
123+
println!(
124+
"[PICC] APDU received ({} bytes): {:02x?}",
125+
apdu.len(),
126+
apdu.as_slice()
127+
);
128+
let mut resp: Vec<u8> = apdu.as_slice().to_vec();
129+
resp.extend_from_slice(&[0x90, 0x00]);
130+
picc.send_response(&resp).unwrap();
131+
}
132+
Err(iso14443::type_a::picc::PiccError::Deselected) => {
133+
println!("[PICC] Deselected (now in HALT state)");
134+
break;
135+
}
136+
Err(e) => {
137+
println!("[PICC] Error: {e:?}");
138+
return;
139+
}
140+
}
141+
}
142+
}
143+
});
144+
145+
// ── Session 1: REQA activation ─────────────────────────────────────
146+
147+
println!("── Session 1: ISO14443-3A Activation (REQA) ────────────\n");
148+
149+
let activation = activate(&mut pcd_side).unwrap();
150+
println!(
151+
"\n[PCD] Activation complete — UID: {:02x?}, SAK: {:02x?}",
152+
activation.uid.as_slice(),
153+
activation.sak
154+
);
155+
156+
println!("\n── ISO14443-4 RATS/ATS ─────────────────────────────────\n");
157+
158+
let cid = Cid::new(0).unwrap();
159+
let (mut pcd, ats) = Pcd::connect(&mut pcd_side, Fsdi::Fsd16, cid).unwrap();
160+
println!(
161+
"\n[PCD] Session established — FSC: {} bytes, FWI: {:?}",
162+
ats.format.fsci.fsc(),
163+
ats.tb
164+
);
165+
166+
println!("\n── ISO14443-4 PPS (optional bit rate negotiation) ──────\n");
167+
168+
pcd.pps(Dxi::Dx2, Dxi::Dx2).unwrap();
169+
println!("[PCD] PPS complete — DR=2x, DS=2x");
170+
171+
println!("\n── APDU Exchange (short, no chaining) ──────────────────\n");
172+
173+
let short_apdu = [0x00, 0xA4, 0x04, 0x00];
174+
println!(
175+
"[PCD] Sending APDU ({} bytes): {:02x?}",
176+
short_apdu.len(),
177+
short_apdu
178+
);
179+
let response = pcd.exchange(&short_apdu).unwrap();
180+
println!("[PCD] Response APDU: {:02x?}", response.as_slice());
181+
182+
println!("\n── APDU Exchange (long, with I-Block chaining) ────────\n");
183+
184+
let long_apdu: Vec<u8> = (0x00..0x1E).collect();
185+
println!(
186+
"[PCD] Sending APDU ({} bytes): {:02x?}",
187+
long_apdu.len(),
188+
long_apdu
189+
);
190+
let response = pcd.exchange(&long_apdu).unwrap();
191+
println!("[PCD] Response APDU: {:02x?}", response.as_slice());
192+
193+
println!("\n── DESELECT (PICC → HALT state) ────────────────────────\n");
194+
195+
pcd.deselect().unwrap();
196+
println!("[PCD] Session 1 closed, tag is now halted");
197+
198+
// ── Session 2: WUPA re-activation ───────────────────────────────
199+
200+
println!("\n── Session 2: ISO14443-3A Re-activation (WUPA) ────────\n");
201+
202+
let activation = wakeup(&mut pcd_side).unwrap();
203+
println!(
204+
"\n[PCD] Re-activated — UID: {:02x?}",
205+
activation.uid.as_slice(),
206+
);
207+
208+
println!("\n── ISO14443-4 RATS/ATS ─────────────────────────────────\n");
209+
210+
let (mut pcd, _ats) = Pcd::connect(&mut pcd_side, Fsdi::Fsd16, cid).unwrap();
211+
println!("\n[PCD] Session 2 established");
212+
213+
println!("\n── APDU Exchange (session 2) ────────────────────────────\n");
214+
215+
let apdu = [0x00, 0xB0, 0x00, 0x00, 0x04];
216+
println!("[PCD] Sending APDU: {:02x?}", apdu);
217+
let response = pcd.exchange(&apdu).unwrap();
218+
println!("[PCD] Response APDU: {:02x?}", response.as_slice());
219+
220+
println!("\n── DESELECT (session 2) ────────────────────────────────\n");
221+
222+
pcd.deselect().unwrap();
223+
println!("[PCD] Session 2 closed");
224+
225+
picc_handle.join().unwrap();
226+
println!("\n── Done ────────────────────────────────────────────────");
227+
}

src/type_a/activation.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,26 @@ pub struct Activation {
4242
/// Sends REQA, then resolves the full UID through up to 3 cascade levels
4343
/// (supporting 4, 7, and 10-byte UIDs per ISO14443-3).
4444
pub fn activate<T: PcdTransceiver>(t: &mut T) -> Result<Activation, ActivationError<T::Error>> {
45-
let hw_crc = t.enable_hw_crc().is_ok();
45+
do_activate(t, Command::ReqA)
46+
}
47+
48+
/// Re-activate a halted tag using WUPA.
49+
///
50+
/// Same as [`activate`] but sends WUPA (0x52) instead of REQA (0x26),
51+
/// which wakes tags in the HALT state. Use after DESELECT or HLTA.
52+
pub fn wakeup<T: PcdTransceiver>(t: &mut T) -> Result<Activation, ActivationError<T::Error>> {
53+
do_activate(t, Command::WupA)
54+
}
55+
56+
fn do_activate<T: PcdTransceiver>(
57+
t: &mut T,
58+
req: Command,
59+
) -> Result<Activation, ActivationError<T::Error>> {
60+
let hw_crc = t.try_enable_hw_crc().is_ok();
4661

47-
// REQA
48-
let reqa = Command::ReqA.to_frame()?;
62+
let frame = req.to_frame()?;
4963
let resp = t
50-
.transceive(&reqa)
64+
.transceive(&frame)
5165
.map_err(ActivationError::PcdTransceiver)?;
5266
let atqa = AtqA::try_from(resp.as_slice())?;
5367

src/type_a/atqa.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,42 @@ pub enum UidSize {
1212
Triple,
1313
}
1414

15-
/// Table 5 - Coding of b1-b5 for bit frame anticollistion
15+
/// Number of anticollision time slots supported by the PICC.
16+
///
17+
/// ISO14443-3 Table 5 — Coding of b1-b5 for bit frame anticollision.
18+
/// Most tags support a single slot.
1619
#[derive(Debug, Clone, IntoPrimitive, TryFromPrimitive)]
1720
#[repr(u8)]
18-
pub enum BitFrameAntiCollistion {
19-
B1 = 1,
20-
B2 = 2,
21-
B3 = 4,
22-
B4 = 8,
23-
B5 = 16,
21+
pub enum BitFrameAntiCollision {
22+
/// 1 time slot.
23+
Slot1 = 1,
24+
/// 2 time slots.
25+
Slot2 = 2,
26+
/// 4 time slots.
27+
Slot4 = 4,
28+
/// 8 time slots.
29+
Slot8 = 8,
30+
/// 16 time slots.
31+
Slot16 = 16,
2432
}
2533

2634
/// Table 3 - Coding of ATQA
2735
#[derive(Debug, Clone)]
2836
pub struct AtqA {
2937
pub uid_size: UidSize,
30-
pub bit_frame_ac: BitFrameAntiCollistion,
38+
pub bit_frame_ac: BitFrameAntiCollision,
3139
pub proprietary_coding: u8,
3240
}
3341

42+
impl AtqA {
43+
/// Serialize ATQA to its 2-byte wire representation.
44+
pub fn to_bytes(&self) -> [u8; 2] {
45+
let byte0 = ((self.uid_size.clone() as u8) << 6) | (self.bit_frame_ac.clone() as u8);
46+
let byte1 = self.proprietary_coding & 0b1111;
47+
[byte0, byte1]
48+
}
49+
}
50+
3451
impl TryFrom<&[u8]> for AtqA {
3552
type Error = TypeAError;
3653

@@ -39,7 +56,7 @@ impl TryFrom<&[u8]> for AtqA {
3956
Ok(Self {
4057
uid_size: UidSize::try_from((value[0] >> 6) & 0b11)
4158
.map_err(|_| TypeAError::UnknownOpcode(value[0] >> 6))?,
42-
bit_frame_ac: BitFrameAntiCollistion::try_from(value[0] & 0b11111)
59+
bit_frame_ac: BitFrameAntiCollision::try_from(value[0] & 0b11111)
4360
.map_err(|_| TypeAError::UnknownOpcode(value[0] & 0b11111))?,
4461
proprietary_coding: value[1] & 0b1111,
4562
})

0 commit comments

Comments
 (0)