Files
pm3py/docs/plans/2026-03-18-ndef-trace-decode.md
michael 615d4a6115 docs: roadmap index + custom-14a-command plans + backfill plan docs
- Add docs/plans/README.md as the roadmap index cataloguing all design/plan docs.
- Add the custom ISO14443-A command handling design + plan (L3 NTAG I2C
  SECTOR_SELECT / cross-sector reads + native auth; L4 static APDUs + WTX relay),
  produced from a multi-agent design workflow.
- Backfill previously-uncommitted plan docs (trace-formatter, ndef-trace-decode,
  live-sniff, firmware-upstream-rebase, 14a-live-trace) and NTAG5_SECURITY.md.
- Sync CLAUDE.md package structure with the committed transponder models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:58:48 -07:00

23 KiB

NDEF Trace Decode Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Detect and decode NDEF content in ISO 15693 trace output — both sim trace and sniff — showing human-readable NDEF records in the annotation column, with overflow on the next line.

Architecture: Add an decode_ndef() function that parses NDEF TLV + records from block data. Wire it into decode_15693_response() so READ SINGLE BLOCK and READ MULTIPLE BLOCKS responses that contain NDEF data show decoded content. CC (block 0) gets its own annotation. Overflow annotations wrap to a continuation line at the annotation column.

Tech Stack: Python, pytest, existing hf_15.py decoder + sim trace_fmt.py

Working directory: /home/work/pm3py/.worktrees/sim-framework/

Test command: cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_hf_15.py tests/test_sim_trace_fmt.py -v


Context for implementer

The trace formatter produces lines like:

[Sim] Tag → Reader:  00 E1 40 0D 01 C1 C0                                                      OK [4B]
[Sim] Tag → Reader:  00 03 0B D1 01 07 54 02 65 6E 74 65 73 74 FE 00 ... FD 02                  OK [60B]

The response decoder (decode_15693_response) currently sees all success responses as either inventory (9B data), empty, or generic OK [NB]. We want to detect and decode:

  1. CC block (block 0): E1 40 XX YYCC v1.0 MLEN=XX MBREAD=Y
  2. NDEF TLV: 03 LL <message> FE → decode the NDEF records inside
  3. NDEF records: Text → NDEF Text "en" "hello", URI → NDEF URI https://example.com, Media → NDEF MIME text/plain [5B]

When the annotation is longer than the available space, it wraps to a continuation line right-justified at the annotation column (same pattern the hex wrapping already uses).

Tricky parts:

  • The decoder only sees the response payload (after flags byte), with no context about WHICH command produced it. It must detect CC/NDEF purely from the data pattern.
  • READ MULTIPLE BLOCKS responses can contain block 0 (CC) followed by NDEF data. The CC is in the first 4 bytes, NDEF TLV starts at byte 4.
  • READ SINGLE BLOCK #0 response is just 4 bytes of CC data.
  • We need both hf_15.py (sniff decoder) and sim/trace_fmt.py (sim decoder) to share the NDEF decode logic. Put the shared code in hf_15.py since that's where the 15693 decoders live; the sim trace_fmt calls into it via decode_15693.

NFC Forum Type 5 CC format (4 bytes):

  • Byte 0: 0xE1 = NDEF magic
  • Byte 1: version (upper nibble = major, lower = minor) + access bits
  • Byte 2: MLEN (data area size / 8)
  • Byte 3: feature flags (bit 0 = MBREAD)

NDEF TLV format:

  • Type 0x03 = NDEF message, 0xFE = terminator, 0x00 = NULL (skip)
  • Length: 1 byte if < 0xFF, else 0xFF + 2 bytes big-endian
  • Value: raw NDEF message bytes

NDEF record format:

  • Byte 0: flags (MB=0x80, ME=0x40, CF=0x20, SR=0x10, IL=0x08, TNF=0x07)
  • Byte 1: TYPE_LENGTH
  • If SR: byte 2 = PAYLOAD_LENGTH (1 byte), else bytes 2-5 = PAYLOAD_LENGTH (4 bytes BE)
  • If IL: next byte = ID_LENGTH
  • TYPE (TYPE_LENGTH bytes)
  • ID (ID_LENGTH bytes, if IL set)
  • PAYLOAD (PAYLOAD_LENGTH bytes)

Well-known RTD types:

  • "T" (0x54): Text record — payload[0] = lang_len, payload[1:1+lang_len] = lang, rest = text
  • "U" (0x55): URI record — payload[0] = prefix code, rest = URI suffix

Task 1: NDEF TLV + record decoder

Files:

  • Modify: pm3py/hf_15.py
  • Modify: tests/test_hf_15.py

Step 1: Write failing tests

Append to tests/test_hf_15.py:

from pm3py.hf_15 import decode_ndef_annotation

# ---- NDEF decode ----

class TestDecodeNdefAnnotation:
    def test_cc_block(self):
        """4-byte CC recognized and decoded."""
        data = bytes([0xE1, 0x40, 0x0D, 0x01])
        ann = decode_ndef_annotation(data)
        assert ann is not None
        assert "CC" in ann
        assert "v1.0" in ann
        assert "MBREAD" in ann

    def test_cc_no_mbread(self):
        data = bytes([0xE1, 0x40, 0x10, 0x00])
        ann = decode_ndef_annotation(data)
        assert "CC" in ann
        assert "MBREAD" not in ann

    def test_ndef_text_record(self):
        """NDEF TLV with Text record."""
        # TLV: 03 0B <ndef> FE
        # Record: D1 01 07 54 02 65 6E 74 65 73 74
        #   MB+ME+SR, TNF=well-known, type="T", lang="en", text="test"
        msg = bytes([0xD1, 0x01, 0x07, 0x54, 0x02, 0x65, 0x6E,
                     0x74, 0x65, 0x73, 0x74])
        tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
        ann = decode_ndef_annotation(tlv)
        assert ann is not None
        assert '"test"' in ann

    def test_ndef_uri_record(self):
        """NDEF TLV with URI record."""
        # URI: https://example.com → prefix 0x04 + "example.com"
        uri_payload = bytes([0x04]) + b"example.com"
        rec = bytes([0xD1, 0x01, len(uri_payload), 0x55]) + uri_payload
        tlv = bytes([0x03, len(rec)]) + rec + bytes([0xFE])
        ann = decode_ndef_annotation(tlv)
        assert "https://example.com" in ann

    def test_ndef_mime_record(self):
        """NDEF TLV with MIME record."""
        mime_type = b"text/plain"
        payload = b"hello"
        rec = bytes([0xD2, len(mime_type), len(payload)]) + mime_type + payload
        tlv = bytes([0x03, len(rec)]) + rec + bytes([0xFE])
        ann = decode_ndef_annotation(tlv)
        assert "text/plain" in ann

    def test_no_ndef_magic(self):
        """Random data without NDEF TLV returns None."""
        ann = decode_ndef_annotation(bytes([0x00, 0x00, 0x00, 0x00]))
        assert ann is None

    def test_cc_plus_ndef(self):
        """Data starting with CC followed by NDEF TLV (READ MULTIPLE from block 0)."""
        cc = bytes([0xE1, 0x40, 0x0D, 0x01])
        msg = bytes([0xD1, 0x01, 0x07, 0x54, 0x02, 0x65, 0x6E,
                     0x74, 0x65, 0x73, 0x74])
        tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
        data = cc + tlv + bytes(50)  # pad like real response
        ann = decode_ndef_annotation(data)
        assert "CC" in ann
        assert '"test"' in ann

    def test_empty_ndef(self):
        """NDEF TLV with zero length."""
        tlv = bytes([0x03, 0x00, 0xFE])
        ann = decode_ndef_annotation(tlv)
        assert ann is not None
        assert "empty" in ann.lower() or "0B" in ann or "NDEF" in ann

    def test_long_text_truncated(self):
        """Long text is truncated in annotation."""
        long_text = "A" * 200
        msg = ndef_text(long_text)
        tlv = bytes([0x03, 0xFF, (len(msg) >> 8) & 0xFF, len(msg) & 0xFF]) + msg + bytes([0xFE])
        ann = decode_ndef_annotation(tlv)
        assert ann is not None
        assert "..." in ann  # truncated

Step 2: Run tests to see them fail

Run: cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_hf_15.py::TestDecodeNdefAnnotation -v Expected: FAIL — ImportError: cannot import name 'decode_ndef_annotation'

Step 3: Implement decode_ndef_annotation

Add to pm3py/hf_15.py, after the existing URI_PREFIXES dict (copy it from type5.py or import it), before the _15693_CMDS dict:

# ---- NDEF decode (for trace annotation) ----

# NFC Type 5 CC magic
_NDEF_MAGIC = 0xE1

# URI prefix codes (NFC Forum RTD URI)
_URI_PREFIXES = {
    0x00: "",
    0x01: "http://www.",
    0x02: "https://www.",
    0x03: "http://",
    0x04: "https://",
    0x05: "tel:",
    0x06: "mailto:",
}

# TNF values
_TNF_EMPTY = 0x00
_TNF_WELL_KNOWN = 0x01
_TNF_MEDIA = 0x02
_TNF_URI = 0x03
_TNF_EXTERNAL = 0x04

_MAX_ANN_TEXT = 40  # truncate decoded text in annotations


def decode_ndef_annotation(data: bytes) -> str | None:
    """Decode NDEF content from block data for trace annotation.

    Handles:
    - CC block (4 bytes starting with 0xE1)
    - NDEF TLV (starting with 0x03)
    - CC + NDEF TLV (READ MULTIPLE from block 0)

    Returns annotation string or None if not NDEF.
    """
    if len(data) < 4:
        return None

    parts = []
    offset = 0

    # Check for CC at start
    if data[0] == _NDEF_MAGIC:
        ver_major = (data[1] >> 6) & 0x03
        ver_minor = (data[1] >> 4) & 0x03
        mlen = data[2]
        features = data[3]
        cc_str = f"CC v{ver_major}.{ver_minor} MLEN={mlen}"
        if features & 0x01:
            cc_str += " MBREAD"
        parts.append(cc_str)
        offset = 4
        if offset >= len(data):
            return cc_str

    # Scan for NDEF TLV
    while offset < len(data):
        tlv_type = data[offset]
        offset += 1

        if tlv_type == 0x00:  # NULL TLV — skip
            continue
        if tlv_type == 0xFE:  # Terminator
            break
        if tlv_type != 0x03:  # Not NDEF — stop
            break

        # Parse TLV length
        if offset >= len(data):
            break
        tlv_len = data[offset]
        offset += 1
        if tlv_len == 0xFF:
            if offset + 2 > len(data):
                break
            tlv_len = (data[offset] << 8) | data[offset + 1]
            offset += 2

        if tlv_len == 0:
            parts.append("NDEF empty")
            continue

        # Parse NDEF message (one or more records)
        msg_end = min(offset + tlv_len, len(data))
        records = _parse_ndef_records(data[offset:msg_end])
        for rec in records:
            parts.append(rec)
        offset = msg_end

    return " | ".join(parts) if parts else None


def _parse_ndef_records(msg: bytes) -> list[str]:
    """Parse NDEF records from raw message bytes. Returns list of annotation strings."""
    records = []
    offset = 0

    while offset < len(msg):
        if offset + 3 > len(msg):
            break

        flags = msg[offset]
        tnf = flags & 0x07
        sr = bool(flags & 0x10)
        il = bool(flags & 0x08)
        type_len = msg[offset + 1]
        offset += 2

        # Payload length
        if sr:
            if offset >= len(msg):
                break
            payload_len = msg[offset]
            offset += 1
        else:
            if offset + 4 > len(msg):
                break
            payload_len = int.from_bytes(msg[offset:offset + 4], "big")
            offset += 4

        # ID length
        id_len = 0
        if il:
            if offset >= len(msg):
                break
            id_len = msg[offset]
            offset += 1

        # Type
        if offset + type_len > len(msg):
            break
        rec_type = msg[offset:offset + type_len]
        offset += type_len

        # ID (skip)
        offset += id_len

        # Payload
        if offset + payload_len > len(msg):
            payload = msg[offset:]
        else:
            payload = msg[offset:offset + payload_len]
        offset += payload_len

        # Decode based on TNF + type
        ann = _decode_ndef_record(tnf, rec_type, payload)
        records.append(ann)

    return records


def _decode_ndef_record(tnf: int, rec_type: bytes, payload: bytes) -> str:
    """Decode a single NDEF record into an annotation string."""
    if tnf == _TNF_WELL_KNOWN:
        if rec_type == b"T" and len(payload) >= 1:
            lang_len = payload[0] & 0x3F
            lang = payload[1:1 + lang_len].decode("ascii", errors="replace")
            text = payload[1 + lang_len:].decode("utf-8", errors="replace")
            if len(text) > _MAX_ANN_TEXT:
                text = text[:_MAX_ANN_TEXT] + "..."
            return f'NDEF Text "{lang}" "{text}"'
        if rec_type == b"U" and len(payload) >= 1:
            prefix = _URI_PREFIXES.get(payload[0], "")
            suffix = payload[1:].decode("utf-8", errors="replace")
            uri = prefix + suffix
            if len(uri) > _MAX_ANN_TEXT:
                uri = uri[:_MAX_ANN_TEXT] + "..."
            return f"NDEF URI {uri}"
        return f"NDEF WK type={rec_type!r} [{len(payload)}B]"

    if tnf == _TNF_MEDIA:
        mime = rec_type.decode("ascii", errors="replace")
        if len(mime) > 30:
            mime = mime[:30] + "..."
        return f"NDEF MIME {mime} [{len(payload)}B]"

    if tnf == _TNF_EXTERNAL:
        ext_type = rec_type.decode("ascii", errors="replace")
        return f"NDEF EXT {ext_type} [{len(payload)}B]"

    if tnf == _TNF_EMPTY:
        return "NDEF empty"

    return f"NDEF TNF={tnf} [{len(payload)}B]"

Step 4: Run tests

Run: cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_hf_15.py::TestDecodeNdefAnnotation -v Expected: All pass

Step 5: Commit

cd /home/work/pm3py/.worktrees/sim-framework
git add pm3py/hf_15.py tests/test_hf_15.py
git commit --no-gpg-sign -m "feat: NDEF TLV + record decoder for trace annotations"

Task 2: Wire NDEF decode into response annotations

Files:

  • Modify: pm3py/hf_15.py
  • Modify: tests/test_hf_15.py

Step 1: Write failing tests

Append to tests/test_hf_15.py:

class TestResponseNdefDecode:
    def test_read_single_block_0_cc(self):
        """READ SINGLE BLOCK #0 response shows CC annotation."""
        # Response: flags(1) + block_data(4)
        data = bytes([0x00, 0xE1, 0x40, 0x0D, 0x01])
        ann = decode_15693_response(data)
        assert "CC" in ann
        assert "MBREAD" in ann

    def test_read_multiple_blocks_ndef(self):
        """READ MULTIPLE BLOCKS response with NDEF shows decoded text."""
        # Response: flags(1) + blocks data
        msg = bytes([0xD1, 0x01, 0x07, 0x54, 0x02, 0x65, 0x6E,
                     0x74, 0x65, 0x73, 0x74])
        tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
        data = bytes([0x00]) + tlv + bytes(50)
        ann = decode_15693_response(data)
        assert '"test"' in ann

    def test_read_multiple_with_cc_and_ndef(self):
        """READ MULTIPLE from block 0: CC + NDEF both decoded."""
        cc = bytes([0xE1, 0x40, 0x0D, 0x01])
        msg = bytes([0xD1, 0x01, 0x07, 0x54, 0x02, 0x65, 0x6E,
                     0x74, 0x65, 0x73, 0x74])
        tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
        data = bytes([0x00]) + cc + tlv + bytes(40)
        ann = decode_15693_response(data)
        assert "CC" in ann
        assert '"test"' in ann

    def test_generic_data_no_ndef(self):
        """Non-NDEF data still shows OK [NB]."""
        data = bytes([0x00, 0xDE, 0xAD, 0xBE, 0xEF])
        ann = decode_15693_response(data)
        assert "OK [4B]" in ann

    def test_inventory_not_affected(self):
        """Inventory response still decoded as inventory, not NDEF."""
        data = bytes([0x00, 0x00]) + bytes(8)
        ann = decode_15693_response(data)
        assert "INVENTORY" in ann

Step 2: Run tests to see them fail

Run: cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_hf_15.py::TestResponseNdefDecode -v Expected: FAIL — "CC" not in "OK [4B]"

Step 3: Update decode_15693_response

Replace the generic response handler section in decode_15693_response (the part after error check, starting at data = payload[1:]):

def decode_15693_response(payload: bytes) -> str | None:
    """Decode an ISO 15693 response frame into annotation."""
    if len(payload) < 1:
        return None

    flags = payload[0]

    if flags & 0x01:  # Error
        if len(payload) >= 2:
            code = payload[1]
            desc = _15693_ERRORS.get(code, "")
            return f"ERROR 0x{code:02X} {desc}" if desc else f"ERROR 0x{code:02X}"
        return "ERROR"

    data = payload[1:]
    if len(data) == 0:
        return "OK"

    # Inventory response: dsfid(1) + uid(8) = 9 bytes
    if len(data) == 9:
        uid_msb = bytes(reversed(data[1:9]))
        return f"OK INVENTORY UID={uid_msb.hex().upper()}"

    # Try NDEF decode (CC block, NDEF TLV, or both)
    ndef_ann = decode_ndef_annotation(data)
    if ndef_ann:
        return f"OK {ndef_ann}"

    return f"OK [{len(data)}B]"

Step 4: Run tests

Run: cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_hf_15.py -v Expected: All pass (including existing tests — verify test_decode_response_ok_data still passes since it uses non-NDEF data)

Step 5: Commit

cd /home/work/pm3py/.worktrees/sim-framework
git add pm3py/hf_15.py tests/test_hf_15.py
git commit --no-gpg-sign -m "feat: wire NDEF decode into ISO 15693 response annotations"

Task 3: Annotation overflow wrapping

Files:

  • Modify: pm3py/hf_15.py (sniff formatter)
  • Modify: pm3py/sim/trace_fmt.py (sim formatter)
  • Modify: tests/test_hf_15.py
  • Modify: tests/test_sim_trace_fmt.py

The current formatters already wrap hex to multiple lines when it's too wide. But annotations are either right-justified on the last hex line (short) or on their own line (wrapped hex). NDEF annotations can be long (e.g. OK CC v1.0 MLEN=13 MBREAD | NDEF Text "en" "test"). When the annotation overflows the available width, it should wrap to the next line at the annotation column.

Step 1: Write failing tests

In tests/test_hf_15.py, append:

class TestSniffLineAnnotationOverflow:
    def test_long_annotation_wraps(self):
        """Long NDEF annotation wraps to continuation line."""
        # Build a response with CC + NDEF text that produces a long annotation
        cc = bytes([0xE1, 0x40, 0x0D, 0x01])
        long_text = "A" * 30
        from pm3py.sim.type5 import ndef_text
        msg = ndef_text(long_text)
        tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
        payload = bytes([0x00]) + cc + tlv
        # Add CRC
        payload += bytes([0xAA, 0xBB])

        entry = {"is_response": True, "data": payload,
                 "timestamp": 0, "duration": 0, "parity": b""}
        line = format_sniff_line(entry, width=80, is_tty=False)
        lines = line.split("\n")
        # Annotation should be on a separate continuation line
        ann_lines = [l for l in lines if "NDEF" in l]
        assert len(ann_lines) >= 1

    def test_short_annotation_no_wrap(self):
        """Short annotation stays on same line as hex."""
        data = bytes([0x00, 0xE1, 0x40, 0x0D, 0x01, 0xAA, 0xBB])
        entry = {"is_response": True, "data": data,
                 "timestamp": 0, "duration": 0, "parity": b""}
        line = format_sniff_line(entry, width=120, is_tty=False)
        assert "CC" in line
        # Should be single-line (plus possible hex wrap)
        non_empty = [l for l in line.split("\n") if l.strip()]
        assert len(non_empty) <= 2  # hex + annotation on same or next line

In tests/test_sim_trace_fmt.py, append to TestTraceFormatterWrapping:

    def test_long_annotation_wraps_to_next_line(self):
        """Long NDEF annotation wraps to continuation line."""
        # Build response with CC + NDEF that gives a long annotation
        cc = bytes([0xE1, 0x40, 0x0D, 0x01])
        from pm3py.sim.type5 import ndef_text
        msg = ndef_text("A" * 30)
        tlv = bytes([0x03, len(msg)]) + msg + bytes([0xFE])
        payload = bytes([0x00]) + cc + tlv
        fmt = TraceFormatter(mode="sim", decoder=decode_15693, width=80)
        result = self._strip_ansi(fmt.format(1, payload))
        lines = result.strip().split("\n")
        ann_lines = [l for l in lines if "NDEF" in l]
        assert len(ann_lines) >= 1

Step 2: Run tests to see them fail

Run: cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_hf_15.py::TestSniffLineAnnotationOverflow tests/test_sim_trace_fmt.py::TestTraceFormatterWrapping::test_long_annotation_wraps_to_next_line -v Expected: Might pass or fail depending on current wrapping behavior — the annotation might already go on its own line if hex is wrapped. Run to check.

Step 3: Update annotation wrapping in both formatters

In format_sniff_line() in hf_15.py, replace the annotation handling in the multi-line section (the if annotation: block near the end):

    if annotation:
        # Wrap annotation if it exceeds available width
        ann_lines = _wrap_annotation(annotation, avail)
        for al in ann_lines:
            ann_pad = max(avail - len(al), 0)
            parts.append(f"{pad}{' ' * ann_pad}{_color(ann_color, al, is_tty)}")

And add the helper (shared between both formatters):

def _wrap_annotation(annotation: str, avail: int) -> list[str]:
    """Wrap annotation text to fit within avail columns.

    Splits on ' | ' boundaries first, then by word if still too long.
    """
    if len(annotation) <= avail:
        return [annotation]

    # Try splitting on pipe separator (NDEF uses " | " between parts)
    if " | " in annotation:
        parts = annotation.split(" | ")
        lines = []
        current = parts[0]
        for part in parts[1:]:
            candidate = f"{current} | {part}"
            if len(candidate) <= avail:
                current = candidate
            else:
                lines.append(current)
                current = part
        lines.append(current)
        return lines

    # Fall back to truncation
    return [annotation[:avail - 3] + "..."]

In trace_fmt.py, apply the same pattern to the TraceFormatter.format() method's multi-line annotation section. Import _wrap_annotation from hf_15 or duplicate the logic inline:

In the multi-line section of TraceFormatter.format(), replace:

        if annotation:
            ann_pad = avail - len(annotation)
            ann_pad = max(ann_pad, 0)
            parts.append(f"{pad}{' ' * ann_pad}{self._color(ann_color, annotation)}")

With:

        if annotation:
            ann_lines = self._wrap_annotation(annotation, avail)
            for al in ann_lines:
                ann_pad = max(avail - len(al), 0)
                parts.append(f"{pad}{' ' * ann_pad}{self._color(ann_color, al)}")

And add to TraceFormatter:

    def _wrap_annotation(self, annotation: str, avail: int) -> list[str]:
        """Wrap annotation to fit within avail columns."""
        if len(annotation) <= avail:
            return [annotation]
        if " | " in annotation:
            parts = annotation.split(" | ")
            lines = []
            current = parts[0]
            for part in parts[1:]:
                candidate = f"{current} | {part}"
                if len(candidate) <= avail:
                    current = candidate
                else:
                    lines.append(current)
                    current = part
            lines.append(current)
            return lines
        return [annotation[:avail - 3] + "..."]

Step 4: Run all tests

Run: cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/test_hf_15.py tests/test_sim_trace_fmt.py -v Expected: All pass

Step 5: Commit

cd /home/work/pm3py/.worktrees/sim-framework
git add pm3py/hf_15.py pm3py/sim/trace_fmt.py tests/test_hf_15.py tests/test_sim_trace_fmt.py
git commit --no-gpg-sign -m "feat: annotation overflow wrapping for NDEF decode in trace output"

Task 4: Copy hf_15.py to main branch + final test

Files:

  • Copy: pm3py/hf_15.py/home/work/pm3py/pm3py/hf_15.py
  • Copy: tests/test_hf_15.py/home/work/pm3py/tests/test_hf_15.py

Step 1: Copy files

cp /home/work/pm3py/.worktrees/sim-framework/pm3py/hf_15.py /home/work/pm3py/pm3py/hf_15.py
cp /home/work/pm3py/.worktrees/sim-framework/tests/test_hf_15.py /home/work/pm3py/tests/test_hf_15.py

Step 2: Run main branch tests

Run: cd /home/work/pm3py && python -m pytest tests/ -v Expected: All pass

Step 3: Run worktree full test suite

Run: cd /home/work/pm3py/.worktrees/sim-framework && python -m pytest tests/ -v Expected: All pass