Skip to content

Normalize quoted DTMF URC digits before IVR dispatch #37

Description

@Justinabox

Summary

URCDispatcher emits quoted DTMF payloads literally when a modem reports +DTMF: "5". That makes IVR code see the digit as "5" instead of 5, so DTMFCollector, terminator handling, and menu routing can miss otherwise valid keypresses on modems that quote DTMF URCs.

Affected source

  • callstack/protocol/urc.py:89-93 splits the URC after : and uses .strip() only:
    • digit = parts[1].strip() if len(parts) > 1 else ""
    • then emits DTMFEvent(digit=digit).
  • tests/test_urc.py covers unquoted DTMF examples but not quoted modem output.

Minimal reproduction

Command run on clean main (23c53805bb63c0aaa7676033b92d108f32de7d81):

PYTHONPATH=. uv run --no-project --with pyserial-asyncio python - <<'PY'
import asyncio
from callstack.events.bus import EventBus
from callstack.events.types import DTMFEvent
from callstack.protocol.urc import URCDispatcher

async def main():
    bus = EventBus(); urc = URCDispatcher(bus)
    async with bus.stream(DTMFEvent) as stream:
        await urc.dispatch('+DTMF: "5"')
        event = await stream.next(timeout=1.0)
        print({'digit': event.digit, 'len': len(event.digit), 'equals_5': event.digit == '5'})
asyncio.run(main())
PY

Observed output:

{'digit': '"5"', 'len': 3, 'equals_5': False}

Expected behavior

Quoted and unquoted DTMF URCs should emit the same normalized single-character digit (0-9, *, #, A-D). Invalid/multicharacter values should be ignored or logged rather than delivered as public DTMF digits.

Actual behavior

The quotes are preserved in the public DTMFEvent, so downstream code receives a three-character string and comparisons against "5", # terminators, and menu options fail.

Suggested fix direction

Normalize DTMF payloads in one place in URCDispatcher (or a small parser helper): trim whitespace, strip one matching quote pair, validate against the existing ATCommand._VALID_DTMF alphabet or an equivalent public constant, and add tests for quoted +DTMF, unquoted +DTMF, and RXDTMF forms.

Acceptance criteria

  • +DTMF: "5" emits DTMFEvent(digit="5").
  • +DTMF: 5 and RXDTMF: 5 continue to emit "5".
  • Invalid quoted/multicharacter payloads do not reach IVR collectors as valid digits.
  • A regression test proves CallSession.collect_dtmf() / DTMFCollector handles a quoted digit and quoted terminator.

Verification gates

git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q

Duplicate search performed

No existing issue/PR was found with:

  • DTMF quoted digit URC in:title,body
  • +DTMF quotes IVR collect digit in:title,body
  • PR search DTMF quoted digit URC

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions