Motivation
Callstack still assumes fixed Raspberry Pi modem ports (/dev/ttyUSB2 for AT commands and /dev/ttyUSB4 for audio) in ModemConfig. That is workable for one SIMCOM setup but fragile for unattended deployments and confusing for developers trying Quectel, Huawei, Sierra, or a different USB enumeration order.
This decomposes the roadmap modem auto-detection item from #9 into one safe PR: produce an explicit discovery report and capability profile without sending SMS, placing calls, unlocking SIMs, or mutating modem state.
User journey
A developer plugs in a USB LTE modem and runs a Callstack discovery helper or CLI/library function. It scans candidate serial ports, identifies which ports respond like AT command ports, records the modem identity, and reports a recommended ModemConfig plus known/unknown capabilities. If detection is uncertain, the report explains why instead of guessing silently.
API / UX sketch
Minimal library-first shape; a later CLI can wrap it:
from callstack.hardware import discover_modems
reports = await discover_modems(patterns=("/dev/ttyUSB*", "/dev/ttyACM*"), baudrate=115200)
for report in reports:
print(report.at_port, report.identity.vendor, report.identity.model, report.capabilities)
Suggested dataclasses:
@dataclass(frozen=True)
class ModemIdentity:
port: str
manufacturer: str = ""
model: str = ""
revision: str = ""
imei_present: bool = False # never expose/log the actual IMEI by default
@dataclass(frozen=True)
class ModemCapabilities:
sms_text_mode: str = "unknown" # supported | unsupported | unknown
sms_pdu_mode: str = "unknown"
delivery_reports: str = "unknown"
ussd: str = "unknown"
voice_calls: str = "unknown"
dtmf_send: str = "unknown"
pcm_audio: str = "unknown"
gnss: str = "unknown"
@dataclass(frozen=True)
class ModemDiscoveryReport:
at_port: str
audio_port: str | None
identity: ModemIdentity
capabilities: ModemCapabilities
confidence: str # high | medium | low
notes: tuple[str, ...] = ()
ModemConfig should remain explicit/overridable. Auto-detection must not break existing users who pass at_port= and audio_port= manually.
Technical approach
- Add a small
callstack/hardware/ module with pure dataclasses plus an async discovery function.
- Discover candidate device paths via injectable glob/list function so tests do not touch real
/dev.
- Probe each candidate with safe read-only/basic commands only, such as
AT, ATE0, ATI, AT+CGMM, and AT+CGMR, with short timeouts and no SIM PIN, SMS, USSD, or call commands.
- Classify AT-capable ports separately from audio/unknown ports. If audio role cannot be proven safely, leave
audio_port=None or confidence="low" with a note; do not hard-code /dev/ttyUSB4 as detected fact.
- Add simple profile matching from identity strings for SIMCOM/Quectel/Huawei/Sierra families, using
unknown for unverified capabilities.
- Add a convenience
ModemConfig.from_discovery(report) or documented helper only if it can remain explicit and reviewable.
Affected modules
- Create:
callstack/hardware/__init__.py
- Create:
callstack/hardware/discovery.py
- Create:
callstack/hardware/profiles.py if profile tables would keep discovery readable
- Modify:
callstack/config.py only if adding an explicit helper such as ModemConfig.from_discovery(...)
- Modify:
callstack/__init__.py only if exporting the discovery API is desired
- Create:
tests/test_hardware_discovery.py
- README/ROADMAP docs can be a follow-up unless the PR adds public API that needs a short usage note
Hardware / modem caveats
ATI/identity commands are common across SIMCOM and Quectel-class AT command sets, but exact responses vary by family and firmware.
- USB enumeration order can change after reconnect; detection should be safe to rerun and should not treat a previous
/dev/ttyUSBx number as proof.
- IMEI/IMSI/SIM numbers are sensitive. Discovery should never print or persist those values by default; if a command proves an IMEI exists, expose only
imei_present=True or a redacted value in debug-only contexts.
- Voice/audio capabilities vary widely. A profile may say
unknown until real hardware evidence exists.
Research links:
Acceptance criteria
Exact gates
git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_hardware_discovery.py tests/test_config.py tests/test_modem.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
Non-goals for this PR
- Building the CLI command itself (
callstack detect/callstack status).
- Real-hardware certification for every modem family.
- Automatic SIM PIN entry, SMS send tests, USSD, or phone calls.
- Multi-modem orchestration or failover.
Motivation
Callstack still assumes fixed Raspberry Pi modem ports (
/dev/ttyUSB2for AT commands and/dev/ttyUSB4for audio) inModemConfig. That is workable for one SIMCOM setup but fragile for unattended deployments and confusing for developers trying Quectel, Huawei, Sierra, or a different USB enumeration order.This decomposes the roadmap modem auto-detection item from #9 into one safe PR: produce an explicit discovery report and capability profile without sending SMS, placing calls, unlocking SIMs, or mutating modem state.
User journey
A developer plugs in a USB LTE modem and runs a Callstack discovery helper or CLI/library function. It scans candidate serial ports, identifies which ports respond like AT command ports, records the modem identity, and reports a recommended
ModemConfigplus known/unknown capabilities. If detection is uncertain, the report explains why instead of guessing silently.API / UX sketch
Minimal library-first shape; a later CLI can wrap it:
Suggested dataclasses:
ModemConfigshould remain explicit/overridable. Auto-detection must not break existing users who passat_port=andaudio_port=manually.Technical approach
callstack/hardware/module with pure dataclasses plus an async discovery function./dev.AT,ATE0,ATI,AT+CGMM, andAT+CGMR, with short timeouts and no SIM PIN, SMS, USSD, or call commands.audio_port=Noneorconfidence="low"with a note; do not hard-code/dev/ttyUSB4as detected fact.unknownfor unverified capabilities.ModemConfig.from_discovery(report)or documented helper only if it can remain explicit and reviewable.Affected modules
callstack/hardware/__init__.pycallstack/hardware/discovery.pycallstack/hardware/profiles.pyif profile tables would keep discovery readablecallstack/config.pyonly if adding an explicit helper such asModemConfig.from_discovery(...)callstack/__init__.pyonly if exporting the discovery API is desiredtests/test_hardware_discovery.pyHardware / modem caveats
ATI/identity commands are common across SIMCOM and Quectel-class AT command sets, but exact responses vary by family and firmware./dev/ttyUSBxnumber as proof.imei_present=Trueor a redacted value in debug-only contexts.unknownuntil real hardware evidence exists.Research links:
ATIand SMS command families: https://simcom.ee/documents/SIM7000x/SIM7000%20Series_AT%20Command%20Manual_V1.04.pdfAcceptance criteria
confidenceand human-actionablenoteswhen AT/audio role assignment is ambiguous.ModemConfig(at_port=..., audio_port=...)behavior remains unchanged.Exact gates
Non-goals for this PR
callstack detect/callstack status).