BP6 + SIM/IC card adapter: reading a SIM over HDUART — ATR captures fine, but the card issues a T=0 repeat/NAK on every byte I transmit. Is a full APDU session over HDUART bridge supported?

BP6 + SIM/IC card adapter: reading a SIM over HDUART — ATR captures fine, but the card issues a T=0 repeat/NAK on every byte I transmit. Is a full APDU session over HDUART bridge supported?

TL;DR

On a Bus Pirate 6 (firmware d821f13) with the official SIM/IC-card adapter, ATR capture over HDUART is rock-solid, but the first T=0 command after the ATR always fails. I isolated the cause and it is not pySim, the port, reset polarity, echo mode, parity content, read timing, or clock speed.

The lone 0xFF that comes back after a SELECT header is the card’s own T=0 error/character-repeat signal (I/O pulled low for ~1–2 etu during the guard time, sampled as a spurious 0xFF). The card asserts it once per byte we transmit. In other words: the card rejects every character the HDUART transmits at the bit level and asks for retransmission, which the generic half-duplex UART does not do — so the exchange stalls immediately.

The question for the maintainers: is driving a full T=0 APDU session over HDUART bridge a supported/known-good path on BP6, and if so, is there a config that makes the open-drain TX characters acceptable to the card (extra guard time / a smart-card TX mode)? Or is HDUART intended for ATR capture + passive monitoring only?

Setup

  • Hardware: Bus Pirate 6, official SIM and IC card adapter.
  • Firmware: main @ d821f13, built 2026-05-27 14:21:18, RP2350B.
  • Host: Linux, pySim from Osmocom master.
  • Cards: the two lab test SIMs shipped with the adapter (referred to as SIM-test-1 and SIM-test-2). Failure reproduces on both; all logs below are SIM-test-2 at 1.8 V.
  • Wiring: Vout→VCC, IO0→I/O, IO1→CLK, IO2→RST, GND→GND (no VPP).

HDUART config used (matches the official SIM demo):

m hduart -b 9600 -d 8 -p even -s 2 -l listen   # 9600 8E2, Listen (passive, open drain)
W 1.8        # class C card; SIM-test-1 uses 3.3 V
P            # pull-ups ON (10k)
G → IO1, 3.5712 MHz, 50%   # 9600 * 372 = 3.5712 MHz

What works: ATR

Reset with [ open + a 2; @ 2 yields a stable, reproducible ATR (bp_atr_probe.py --cold-start --voltage 1.8 --attempts 10 → 10/10):

SIM-test-2 ATR: 3B 9F 96 80 1F C7 80 31 E0 73 FE 21 13 67 98 08 01 09 03 01 04 46
SIM-test-1 ATR: 3B 9E 97 80 1F C6 80 31 E0 73 FE 21 1B 66 D0 02 40 1B 15 00 4C   (at 3.3 V)

Both decode cleanly (direct convention, T=0, valid TCK). RX is flawless.

What fails: the first T=0 command

Sending the UICC SELECT MF header 00 A4 00 04 02 (or classic GSM A0 A4 00 00 02) and reading the procedure byte:

TX:  00 A4 00 04 02
RX:  FF        <-- then nothing

For T=0 we’d expect A4 (ACK), 60 (NULL/wait), or 6x/9x (status). FF is none of these. Same result via raw bridge (with the expected 5-byte local echo, then FF) and via pySim (ProtocolError, b = b'\xff').

Evidence: the 0xFF is the card, and it fires once per transmitted byte

All five experiments below are one clean session in HDUART command mode (no bridge, no button), using a probe script that gets a fresh ATR before each send. Raw bytes shown; (nd) = Bus Pirate’s 0x00 (No data to read) placeholder.

1. Control — transmit to a muted card. Card powered + clocked but held in reset (a 2, so it cannot drive I/O), then send the header:

RST low, TX 00 A4 00 04 02  ->  RX: (nd)(nd)(nd)...   # NOTHING comes back

So a powered-but-muted card returns no byte. The 0xFF therefore requires a live card — it is not idle-line noise nor a host-side artifact.

2. Guard-time sweep — the count scales with inter-byte boundaries, not wait time. Same header, varying the delay inserted between the header bytes:

no gap                :  FF               (1 pulse)
1 ms between bytes    :  FB FF            (2 pulses)
5 ms between bytes    :  FF FF FF         (3 pulses)
D:1000 after last byte:  FF               (1 pulse)   # long total wait, one boundary

The number of FF/FB-type pulses tracks the number of byte boundaries, not the total wait. Spacing the bytes out simply lets each per-byte pulse be sampled separately (their exact value FF/FB/… varies because the card’s low pulse is asynchronous to our byte framing). This is the ISO 7816-3 T=0 error/character-repeat signal: the card pulls I/O low during the guard time to request retransmission of the character it just received badly — one request per byte.

3. Parity sweep — even is correct, and the card still NAKs.

-p even :  ATR OK, header -> NAK per byte (as above)
-p odd  :  NO ATR at all (BP rejects the even-parity ATR bytes -> BP *does* validate RX parity, so even is the right convention)
-p none :  ATR OK; with 5 ms gaps -> FF FF FF FF FF (five pulses, one per header byte)

With the correct even parity the card still rejects every byte. So this is not a parity-content problem — it’s the physical integrity of the transmitted bits.

4. Speed sweep — identical at half speed.

9600 baud / 3.5712 MHz :  NAK
4800 baud / 1.7856 MHz :  ATR OK, identical NAK (FF, FF FF FF)
2400 baud / 0.8928 MHz :  no ATR (clock < 1 MHz)

Identical at 4800 → the corruption is not a fixed-duration glitch; it scales with the bit period.

Interpretation / root-cause hypothesis

  • ATR works because there the card drives the (open-drain, pulled-up) I/O line.
  • The failure appears only when the Bus Pirate drives I/O. hwuart_pio_write() temporarily claims IO0/RXTX, transmits, and releases it (even in Listen). The characters produced by that open-drain TX are not accepted at the bit level by these cards — each one triggers a T=0 parity/framing error, and the card requests retransmission by holding I/O low during the guard time (our lone 0xFF).
  • The Bus Pirate HDUART is a generic half-duplex UART, not a T=0 protocol engine: it does not implement ISO 7816-3 character repetition, so a single rejected byte deadlocks the exchange. There is never a valid procedure byte, so pySim (and any client) fails on the very first SELECT.

Ruled out (with evidence)

pySim install/port (raw bridge reproduces it) · missing ATR (stable, decoded) · human pause (timed scripts fail identically) · pySim post-ATR read timeout (--bp-fast-atr reads exact ATR, still fails) · Phoenix echo model (bridge -s + no-echo fails identically; plain bridge shows correct echo) · RTS reset polarity (+rts/-rts tried) · host flow control (stty shows -crtscts -ixon -ixoff) · no TX on IO0 (logic follow-along shows IO0 toggling on TX) · parity content · read timing · line speed · Listen vs Master. Firmware is already the latest: the last hwuart_pio.c commits are March 2026 (bb04958, d3c12ff “fix garbled RX data”, e1073c9 “add listen mode”), all predating d821f13.

How to reproduce

Bus Pirate configured as above, then in HDUART command mode after a clean ATR:

]                                         # close async print
>0x00 0xA4 0x00 0x04 0x02 D:20 r:8        # -> RX: 0xFF then No data
>0x00 D:5 0xA4 D:5 0x00 D:5 0x04 D:5 0x02 D:50 r:8   # -> RX: 0xFF 0xFF 0xFF (one per byte)

Control (should return nothing):

a 2                                       # hold RST low, card muted
>0x00 0xA4 0x00 0x04 0x02 D:20 r:8        # -> RX: No data

Questions for the maintainers / community

  1. Is a full T=0 APDU session over HDUART bridge a supported path on BP6, or is HDUART meant for ATR capture + passive monitoring only? The official SIM doc only demonstrates ATR.
  2. Has anyone captured a working pySim + HDUART bridge T=0 session on a BP6 (not BP5)? On which firmware build?
  3. Is the open-drain TX turnaround / guard-bit handling a known limitation, and is there a config (extra guard time, stop-bit count, a dedicated smart-card TX mode) that makes the transmitted characters acceptable to the card so it stops requesting retransmission?

Happy to provide raw logs, a self-contained repro script, or run any suggested test — I have a clean reproduction on hand.

1 Like

Welcome to the community!

This is a bit beyond my ken, but it “sounds” reasonable(+). The explanation is detailed, refers to specs, explains what the code does, interprets it as intended actions, explains what the chip is likely doing, … all in a very clear manner.

Presuming you validated everything there, I look forward to more of your contributions!

(+) It also sounds AI-generated. In this case, that’s not necessarily bad, so long as it’s not hallucinating. (I don’t know enough about this area to have a clue.)

Thanks for the welcome!

It was AI-generated, apologies for that. My intention with the post isn’t to get any credit, it’s honestly just to ask for help. This isn’t a topic I know in depth either, i’m only getting started with all of this. When I saw posts from people doing on a BP5 what I couldn’t get working on my BP6 i leaned on AI to help me debug the issue and put together a clear report so i could ask the real experts here whether anyone has run into this before, or flag it as a bug if that’s what it turns out to be.

To be honest, the post probably reads more expert than i am xD i guess that is a problem. but I’m very much still learning.

That said, all the tests and checks are mine, run on real hardware. The AI helped me organize and interpret the results, but none of it is made up. Happy to run any test you suggest!

2 Likes

** In the setup i mention two lab test SIMs shipped with the adapter: that is a mistake, those were not sim cards. the test sims were of my own

1 Like

Thanks for confirming the AI usage, and confirming that you did the work. I never thought about “credit”, so I apologize if I came across that way.

As an aside, for myself, I find it helpful to mention what portions of AI-generated output I could not or did not independently confirm, when sharing with others. In the past, this has helped me save face, when hallucinations existed in those parts. (As you know, AI is just really good at presenting non-facts as facts…)

Your post made me curious, so I’m reading the docs, and trying it first on a BP5, and then will try the same on a BP6 … but using the smart card provided with the official adapter.

@jalruiz - Do you have the self-contained repro script, which should “just work” correctly on the BP5, and will fail as indicated on the BP6?

I’m new to this adapter, and having trouble getting any response from the card.

Sequence I’m trying, based on the docs:

m hduart -b 9600 -d 8 -p even -s 2 -l listen
W 3.3
P
G 1 # 3.5712mhz 50%
[
a 2; @ 2

No response yet from a BP5 with firmware from commit d821f1344fa561a015362b5499ef9606cc16df69 (May 27, 2026) … not even the 0xFF you see.

Maybe I’ve configured something wrongly?

Your config is the same as mine working on regular sims, I just tried the card from the adapter and it fails on getting the ATR as well.

This is the script in python (AI-generated, manually tested). The output is ugly, but it basically captures the card’s ATR (same method you were using) and then sends a “SELECT MF” and looks at the card’s response. If the T=0 exchange works, the byte sent by the card should be a valid procedure/status byte (like 0xA4 ACK), and if it doesn’t work you get a 0xFF (or nothing). The veredict on successes may be wrong since the “WORKS” path is untested on a working card, but the output is always shown.

This is basically what tools like pysim do to read the files of a sim, and I have seen articles like thisusing it on the BP5. (If I use pysim on my setup with BP6 the script does not read/detect the card).

Thank you for taking time trying to help :slight_smile:

Since I’m new I can´t upload files so here it goes xD:

from __future__ import annotations

import argparse
import re
import sys
import time
from pathlib import Path

import serial


DEFAULT_PORT = "/dev/ttyACM0"
HEX_RE = re.compile(rb"0x([0-9a-fA-F]{2})")
PROMPT_RE = re.compile(rb"(HiZ|HDUART|HDPLXUART)>")
ANSI_RE = re.compile(rb"\x1b\[[0-9;:?]*[ -/]*[@-~]|\x1b[78]")
RX_LINE_RE = re.compile(rb"RX:(.*?)(?:(?:\r?\n)+(?:\S+>)|$)", re.DOTALL)
NODATA_PLACEHOLDER_RE = re.compile(rb"0x00\s*\(\s*No\s+data\s+to\s+read\s*\)", re.DOTALL)


def read_for(ser: serial.Serial, seconds: float) -> bytes:
    end = time.monotonic() + seconds
    chunks: list[bytes] = []
    while time.monotonic() < end:
        waiting = ser.in_waiting
        if waiting:
            chunks.append(ser.read(waiting))
        else:
            time.sleep(0.005)
    waiting = ser.in_waiting
    if waiting:
        chunks.append(ser.read(waiting))
    return b"".join(chunks)


def read_until_quiet(ser: serial.Serial, timeout: float, quiet: float = 0.2, minimum: float = 0.1) -> bytes:
    end = time.monotonic() + timeout
    min_end = time.monotonic() + minimum
    last_data = time.monotonic()
    data = bytearray()
    while time.monotonic() < end:
        waiting = ser.in_waiting
        if waiting:
            data.extend(ser.read(waiting))
            last_data = time.monotonic()
        elif time.monotonic() >= min_end and time.monotonic() - last_data >= quiet:
            break
        else:
            time.sleep(0.005)
    waiting = ser.in_waiting
    if waiting:
        data.extend(ser.read(waiting))
    return bytes(data)


def read_until_prompt(ser: serial.Serial, timeout: float) -> bytes:
    end = time.monotonic() + timeout
    data = bytearray()
    while time.monotonic() < end:
        waiting = ser.in_waiting
        if waiting:
            data.extend(ser.read(waiting))
            if PROMPT_RE.search(strip_ansi(bytes(data))):
                break
        else:
            time.sleep(0.005)
    waiting = ser.in_waiting
    if waiting:
        data.extend(ser.read(waiting))
    return bytes(data)


def send_line(ser: serial.Serial, line: str) -> None:
    ser.write(line.encode("ascii") + b"\r")
    ser.flush()


def emit(data: bytes, log_file: Path) -> None:
    with log_file.open("ab") as f:
        f.write(data)
    text = data.decode("utf-8", errors="replace")
    if text:
        print(text, end="")


def note(log_file: Path, text: str) -> None:
    emit(f"\n>>> {text}\n".encode("ascii"), log_file)


def strip_ansi(data: bytes) -> bytes:
    return ANSI_RE.sub(b"", data).replace(b"\r", b"\n")


def command_and_log(ser: serial.Serial, log_file: Path, line: str, timeout: float = 1.0,
                    quiet: float = 0.2, minimum: float = 0.1) -> bytes:
    note(log_file, f"cmd: {line}")
    send_line(ser, line)
    data = read_until_quiet(ser, timeout, quiet=quiet, minimum=minimum)
    emit(data, log_file)
    return data


def bus_command_and_log(ser: serial.Serial, log_file: Path, line: str, read_seconds: float) -> bytes:
    note(log_file, f"cmd: {line}")
    send_line(ser, line)
    data = read_for(ser, read_seconds)
    emit(data, log_file)
    return data


def configure_clock(ser: serial.Serial, log_file: Path, pin: str, clock: str, duty: str) -> None:
    note(log_file, f"enable clock: IO{pin} {clock} {duty}")
    send_line(ser, "G")
    emit(read_until_quiet(ser, 1.0), log_file)
    send_line(ser, pin)
    emit(read_until_quiet(ser, 1.0), log_file)
    send_line(ser, clock)
    emit(read_until_quiet(ser, 1.0), log_file)
    send_line(ser, duty)
    emit(read_until_quiet(ser, 2.0, quiet=0.25, minimum=0.3), log_file)


def configure_hduart_mode(ser: serial.Serial, log_file: Path, bus: str,
                          parity: str = "even", stops: int = 2, baud: int = 9600) -> None:
    note(log_file, f"configure HDUART mode: {baud} 8-{parity}-{stops} {bus}")
    command_and_log(ser, log_file, f"m hduart -b {baud} -d 8 -p {parity} -s {stops} -l {bus}",
                    timeout=2.0, quiet=0.25, minimum=0.4)


def extract_hex_bytes(data: bytes) -> list[int]:
    return [int(m.group(1), 16) for m in HEX_RE.finditer(data)]


def parse_atr_from(values: list[int], start: int) -> list[int] | None:
    if start + 2 > len(values) or values[start] not in (0x3B, 0x3F):
        return None
    pos = start + 1
    t0 = values[pos]
    pos += 1
    y = t0 >> 4
    historical_len = t0 & 0x0F
    tck_present = False
    while y:
        td = None
        for bit in range(4):
            if y & (1 << bit):
                if pos >= len(values):
                    return None
                value = values[pos]
                pos += 1
                if bit == 3:
                    td = value
                    if value & 0x0F:
                        tck_present = True
        y = (td >> 4) if td is not None else 0
    pos += historical_len
    if tck_present:
        pos += 1
    if pos > len(values):
        return None
    atr = values[start:pos]
    if tck_present:                     # reject a garbled/doubled capture: TCK must
        x = 0                           # make the XOR of T0..TCK zero
        for b in atr[1:]:
            x ^= b
        if x != 0:
            return None
    return atr


def find_atr(values: list[int]) -> list[int] | None:
    for idx, value in enumerate(values):
        if value in (0x3B, 0x3F):
            atr = parse_atr_from(values, idx)
            if atr:
                return atr
    return None


def extract_rx_text(data: bytes) -> list[str]:
    """Return the raw text of each `RX:` line, ANSI stripped, for exact reading."""
    data = strip_ansi(data)
    out: list[str] = []
    for m in RX_LINE_RE.finditer(data):
        chunk = m.group(1)
        text = chunk.decode("utf-8", errors="replace")
        out.append(" ".join(text.split()))
    return out


def extract_rx_real_bytes(data: bytes) -> list[int]:
    """Parse only real received bytes from RX: lines, dropping 'No data' 0x00s.

    Bus Pirate prints `0x00 (No data to read)` for empty reads. We drop those so
    the first *real* byte the card produced is not hidden behind placeholders.
    """
    data = strip_ansi(data)
    values: list[int] = []
    for m in RX_LINE_RE.finditer(data):
        chunk = m.group(1)
        # Bus Pirate can wrap the placeholder over two terminal lines; remove
        # the whole marker before extracting hex bytes.
        chunk = NODATA_PLACEHOLDER_RE.sub(b"", chunk)
        values.extend(int(x.group(1), 16) for x in HEX_RE.finditer(chunk))
    return values


def fmt(values: list[int] | None) -> str:
    if not values:
        return "(none)"
    return " ".join(f"{v:02X}" for v in values)


def cold_start(ser: serial.Serial, log_file: Path, args: argparse.Namespace, voltage: str) -> None:
    if args.configure_mode:
        configure_hduart_mode(ser, log_file, args.hduart_bus, args.parity, args.stops, args.sim_baud)
    note(log_file, "cold-start cleanup")
    for cmd in ("logic stop", "]", "a 2", "w", "p", "g", "l"):
        command_and_log(ser, log_file, cmd, timeout=1.0)
    note(log_file, f"wait with Vout off: {args.power_off_delay:.2f}s")
    time.sleep(args.power_off_delay)
    command_and_log(ser, log_file, f"W {voltage}", timeout=2.0)
    command_and_log(ser, log_file, "P", timeout=1.0)
    configure_clock(ser, log_file, args.clock_pin, args.clock, args.duty)


def read_atr_after_reset(ser: serial.Serial, log_file: Path, args: argparse.Namespace) -> list[int] | None:
    command_and_log(ser, log_file, "[", timeout=1.0)
    command_and_log(ser, log_file, "a 2", timeout=1.0)
    time.sleep(args.reset_low)
    note(log_file, "cmd: @ 2 ; read ATR window")
    send_line(ser, "@ 2")
    data = read_for(ser, args.atr_window)
    emit(data, log_file)
    atr = find_atr(extract_hex_bytes(data))
    note(log_file, f"ATR parsed: {fmt(atr)}")
    command_and_log(ser, log_file, "]", timeout=1.0)
    return atr


def read_atr_with_retries(
    ser: serial.Serial,
    log_file: Path,
    args: argparse.Namespace,
    context: str,
) -> list[int] | None:
    for attempt in range(1, args.atr_retries + 1):
        if args.atr_retries > 1:
            note(log_file, f"{context}: ATR attempt {attempt}/{args.atr_retries}")
        atr = read_atr_after_reset(ser, log_file, args)
        if atr:
            return atr
    return None


def sum_delays_ms(command: str) -> int:
    return sum(int(m) for m in re.findall(r"D:(\d+)", command))


def run_variation(ser: serial.Serial, log_file: Path, args: argparse.Namespace,
                  label: str, command: str) -> tuple[list[int] | None, list[int], list[str]]:
    note(log_file, f"variation: {label}")
    atr = read_atr_with_retries(ser, log_file, args, label)
    if not atr:
        return None, [], []
    read_seconds = max(1.0, sum_delays_ms(command) / 1000.0 + 1.0)
    raw = bus_command_and_log(ser, log_file, command, read_seconds=read_seconds)
    real = extract_rx_real_bytes(raw)
    rx_lines = extract_rx_text(raw)
    note(log_file, f"{label} RX lines: {rx_lines}")
    note(log_file, f"{label} first real card byte(s): {fmt(real)}")
    return atr, real, rx_lines


def classify_first_byte(b: int, ins: int) -> tuple[str, bool]:
    """Interpret the first byte the card returns after a T=0 command header.
    Returns (human-readable meaning, works?)."""
    if b == ins:
        return (f"ACK (procedure byte = INS {ins:02X}): card accepts, wants command data", True)
    if b == 0x60:
        return ("NULL (0x60): card requests more time - valid T=0 procedure byte", True)
    if 0x61 <= b <= 0x6F or 0x90 <= b <= 0x9F:
        return (f"status word start SW1={b:02X}: card returned a T=0 status", True)
    if b == 0xFF:
        return ("0xFF: not a valid T=0 procedure/status byte (idle line / NAK)", False)
    return (f"{b:02X}: not a standard T=0 procedure byte", False)


def run_verdict_test(ser: serial.Serial, log_file: Path, args: argparse.Namespace,
                     label: str, header: list[int], data: list[int]) -> str:
    """Reset for a fresh ATR, send a SELECT header, read the T=0 procedure byte,
    and if the card ACKs, send the command data and read the status word. Returns
    one human-readable RESULT line (WORKS / FAIL / INCONCLUSIVE)."""
    note(log_file, f"verdict test: {label}")
    atr = read_atr_with_retries(ser, log_file, args, label)
    if not atr:
        return f"RESULT [{label}]: INCONCLUSIVE - no ATR (check card/socket/voltage)"
    ins = header[1]
    hdr_cmd = ">" + " ".join(f"0x{b:02x}" for b in header) + " D:20 r:8"
    raw = bus_command_and_log(ser, log_file, hdr_cmd, read_seconds=1.5)
    real = extract_rx_real_bytes(raw)
    note(log_file, f"{label} header response bytes: {fmt(real)}")
    if not real:
        return (f"RESULT [{label}]: FAIL - no procedure byte after the SELECT header "
                f"(the BP6 issue: turnaround produces nothing)")
    b = real[0]
    meaning, works = classify_first_byte(b, ins)
    if not works:
        return f"RESULT [{label}]: FAIL - first byte {meaning}"
    detail = f"procedure byte {meaning}"
    if b == ins and data:
        data_cmd = ">" + " ".join(f"0x{x:02x}" for x in data) + " D:20 r:8"
        raw2 = bus_command_and_log(ser, log_file, data_cmd, read_seconds=1.5)
        sw = extract_rx_real_bytes(raw2)
        note(log_file, f"{label} after-data bytes: {fmt(sw)}")
        detail += f"; after data {' '.join(f'{x:02X}' for x in data)} -> {fmt(sw)}"
    return f"RESULT [{label}]: WORKS - T=0 turnaround functional. {detail}"


# (label, command).  UICC SELECT MF header 00 A4 00 04 02 (+data 3F 00), and
# the classic GSM header A0 A4 00 00 02, sent with varied guard time / windows.
VARIATIONS = [
    ("uicc_baseline_D20",      ">0x00 0xA4 0x00 0x04 0x02 D:20 r:8"),
    ("uicc_immediate",         ">0x00 0xA4 0x00 0x04 0x02 r:8"),
    ("uicc_guard_1ms",         ">0x00 D:1 0xA4 D:1 0x00 D:1 0x04 D:1 0x02 D:20 r:8"),
    ("uicc_guard_5ms",         ">0x00 D:5 0xA4 D:5 0x00 D:5 0x04 D:5 0x02 D:50 r:8"),
    ("uicc_longwait_D1000",    ">0x00 0xA4 0x00 0x04 0x02 D:1000 r:8"),
    ("gsm_baseline_D20",       ">0xA0 0xA4 0x00 0x00 0x02 D:20 r:8"),
    ("gsm_guard_5ms",          ">0xA0 D:5 0xA4 D:5 0x00 D:5 0x00 D:5 0x02 D:50 r:8"),
]


def prepare_prompt(ser: serial.Serial, log_file: Path) -> None:
    note(log_file, "wake prompt")
    emit(read_for(ser, 0.3), log_file)
    send_line(ser, "")
    emit(read_until_quiet(ser, 1.0), log_file)


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--port", default=DEFAULT_PORT)
    parser.add_argument("--baud", type=int, default=115200)
    parser.add_argument("--voltage", default="3.3")
    parser.add_argument("--auto-voltage", action="store_true",
                        help="try 3.3V then 1.8V and keep whichever yields an ATR")
    parser.add_argument("--sim-baud", type=int, default=9600,
                        help="HDUART baud toward the SIM; keep = clock/372")
    parser.add_argument("--clock-pin", default="1")
    parser.add_argument("--clock", default="3.5712mhz")
    parser.add_argument("--duty", default="50%")
    parser.add_argument("--hduart-bus", choices=["listen", "master"], default="listen")
    parser.add_argument("--parity", choices=["even", "odd", "none"], default="even")
    parser.add_argument("--stops", type=int, choices=[1, 2], default=2)
    parser.add_argument("--quick", action="store_true",
                        help="run only baseline + guard_5ms variations")
    parser.add_argument("--configure-mode", action=argparse.BooleanOptionalAction, default=True)
    parser.add_argument("--power-off-delay", type=float, default=1.0)
    parser.add_argument("--reset-low", type=float, default=0.1)
    parser.add_argument("--atr-window", type=float, default=1.0)
    parser.add_argument("--atr-retries", type=int, default=3)
    parser.add_argument("--inspect-only", action="store_true",
                        help="just report i/v and one ATR, then exit")
    parser.add_argument("--rst-low-control", action="store_true",
                        help="control: power the card but hold RST low (muted), then "
                             "transmit the SELECT header and read. Expect NO return byte, "
                             "proving the 0xFF only appears with a live card.")
    parser.add_argument("--log", default="bp6_hduart_sim_t0_repro.log")
    args = parser.parse_args()

    log_file = Path(args.log)
    log_file.parent.mkdir(parents=True, exist_ok=True)
    log_file.write_bytes(b"")

    voltages = ["3.3", "1.8"] if args.auto_voltage else [args.voltage]

    try:
        with serial.Serial(args.port, args.baud, timeout=0.02, write_timeout=1, exclusive=True) as ser:
            note(log_file, f"opened {args.port} at {args.baud}")
            prepare_prompt(ser, log_file)

            summaries: list[str] = []

            if args.rst_low_control:
                voltage = voltages[0]
                note(log_file, f"=== RST-low control at {voltage}V ===")
                cold_start(ser, log_file, args, voltage)
                command_and_log(ser, log_file, "v", timeout=2.0, quiet=0.25, minimum=0.3)
                note(log_file, "hold RST low: a 2 (card powered+clocked but muted)")
                command_and_log(ser, log_file, "a 2", timeout=1.0)
                cmd = ">0x00 0xA4 0x00 0x04 0x02 D:20 r:8"
                raw = bus_command_and_log(ser, log_file, cmd, read_seconds=1.5)
                real = extract_rx_real_bytes(raw)
                rx_lines = extract_rx_text(raw)
                summaries.append(f"RST-low control: first_real={fmt(real)} | RX={rx_lines}")
                summaries.append("(expected: no card byte; proves 0xFF needs a live card)")
                command_and_log(ser, log_file, "@ 2", timeout=1.0)
                note(log_file, "summary")
                for line in summaries:
                    note(log_file, line)
                print("\n==== SUMMARY ====")
                for line in summaries:
                    print(f"  {line}")
                print(f"Raw log: {log_file}")
                return 0

            atr = None
            used_voltage = None
            for voltage in voltages:
                note(log_file, f"=== try voltage {voltage}V ===")
                cold_start(ser, log_file, args, voltage)
                command_and_log(ser, log_file, "i", timeout=3.0, quiet=0.25, minimum=0.5)
                command_and_log(ser, log_file, "v", timeout=2.0, quiet=0.25, minimum=0.3)
                atr = read_atr_with_retries(ser, log_file, args, f"voltage {voltage}V")
                if atr:
                    used_voltage = voltage
                    break

            verdicts: list[str] = []
            if not atr:
                summaries.append("no ATR at any tried voltage; check inserted card / socket")
                verdicts.append("RESULT: INCONCLUSIVE - no ATR (card not answering; check "
                                "card/socket/voltage before judging T=0)")
            else:
                summaries.append(f"ATR at {used_voltage}V: {fmt(atr)}")
                if not args.inspect_only:
                    # Headline: self-verifying pass/fail T=0 verdict.
                    verdicts.append(run_verdict_test(
                        ser, log_file, args, "UICC SELECT MF",
                        [0x00, 0xA4, 0x00, 0x04, 0x02], [0x3F, 0x00]))
                    verdicts.append(run_verdict_test(
                        ser, log_file, args, "GSM SELECT MF",
                        [0xA0, 0xA4, 0x00, 0x00, 0x02], [0x3F, 0x00]))
                    # Supporting evidence: guard-time / parity / speed sweeps.
                    variations = VARIATIONS
                    if args.quick:
                        variations = [v for v in VARIATIONS
                                      if v[0] in ("uicc_baseline_D20", "uicc_guard_5ms")]
                    for label, command in variations:
                        _atr, real, rx_lines = run_variation(ser, log_file, args, label, command)
                        if _atr is None:
                            summaries.append(f"{label}: LOST ATR before send")
                            continue
                        summaries.append(f"{label}: first_real={fmt(real)} | RX={rx_lines}")

            note(log_file, "verdict")
            for line in verdicts:
                note(log_file, line)
            note(log_file, "summary")
            for line in summaries:
                note(log_file, line)
            if verdicts:
                print("\n==== RESULT (does T=0 work on this setup?) ====")
                for line in verdicts:
                    print(f"  {line}")
            print("\n==== DETAIL ====")
            for line in summaries:
                print(f"  {line}")
            print(f"Raw log: {log_file}")

    except serial.SerialException as exc:
        print(f"serial error: {exc}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

I was looking for the minimal set of commands. See my last post. I listed six commands. Something similar, where you list maybe ten commands, at most, where you get a response from the card.

I don’t have time to try to review AI-generated stuff. It tends to be overly verbose, sloppy, wrong, and combinations of all three.

Copying ten individual commands one at a time? That’s a repro scenario … (unless it doesn’t work when done by hand, and even then … that’s a very different issue). Running a 100+ line of AI-generated script? Not for me, sorry.

Here’s what I am trying. Do you get a response from this sequence, or not?

m hduart -b 9600 -d 8 -p even -s 2 -l listen
W 3.3
P
G 1 # 3.5712mhz 50%
[
a 2; @ 2

Yes, i do get a response from your sequence in other sims, but not in the blank card that comes with the adapter. I made some tests with that one and I can’t get a response from it either.

The script does the same thing as yours, just followed with the instruction:

>0xa0 0xa4 0x00 0x00 0x02

, which is the SELECT MF instruction, with different stops and variations. It’s only that it does it for different combinations of power and baud rates. (and parsing the output, it is indeed very verbose).

You are right, the probe case you were asking for would be:

m hduart -b 9600 -d 8 -p even -s 2 -l listen
W 3.3
P
G 1 # 3.5712mhz 50%
[
a 2; @ 2
>0xa0 0xa4 0x00 0x00 0x02 D:2 r:3

This shoud receive a response from the SIM, but I receive none (or FF) from any of them.

But if you are not getting the ATR with the instruction a 2; @ 2 this won’t work either.

I’m now trying with an hybrid mount with the BP6 and the ESP32 bit pirate project on a ESP32 S3 dev kit, using the I/O of the ESP32. I can get the ATR fine but same problem as before.