Summary
Modem.unlock_puk(puk, new_pin) is advertised by the SIM PUK error path, but the normal async with Modem(...) startup aborts before user code receives a connected modem object. That makes PUK recovery difficult to perform through the public API precisely when it is needed.
Evidence
Affected code:
callstack/modem.py:89-100 enters the context manager by calling _connect() before returning self.
callstack/modem.py:148-154 _initialize_modem() checks SIM state during _connect().
callstack/modem.py:197-200 raises SIMPUKRequired("SIM is PUK-locked — use modem.unlock_puk(puk, new_pin) to recover") when AT+CPIN? reports SIM PUK.
callstack/modem.py:322-329 implements unlock_puk(), but it sends through self._executor; there is no documented/public recovery entry point that opens the AT transport while bypassing the failing startup check.
tests/test_sim_pin.py covers command formatting and CPIN parsing but has no test proving a PUK-locked modem can be recovered through public API.
Repository health checks from this scout run:
git diff --check
# exit 0
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
# 291 passed in 3.94s
Duplicate checks:
gh issue list --repo Justinabox/Callstack --state all --search 'SIM PUK unlock_puk in:title,body'
# no matches
gh issue list --repo Justinabox/Callstack --state all --search 'PUK locked connect unlock in:title,body'
# no matches
Expected behavior
A user with a PUK-locked SIM should have a documented, test-covered way to recover without using private internals or raw serial tools. The recovery path should send the PUK/new PIN safely, then allow normal modem initialization to continue or clearly instruct the user to reconnect.
Actual behavior
The startup path raises SIMPUKRequired before the context manager yields. The exception tells users to call modem.unlock_puk(...), but the obvious public pattern never reaches a usable modem inside the async with block.
Suggested fix direction
Pick a small, explicit recovery API and document it. Possible shapes:
- Add
ModemConfig(sim_puk=..., new_sim_pin=...) and handle SIM PUK during _check_sim_pin() similarly to sim_pin, with careful validation and redacted logging.
- Or add a class/helper such as
await Modem.recover_puk(config, puk, new_pin) that opens only the AT transport, sends AT+CPIN="puk","new_pin", verifies +CPIN: READY, and closes cleanly.
- Update the
SIMPUKRequired message to point at the actual supported path.
Avoid logging PUK/PIN values or SIM identifiers.
Acceptance criteria
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/test_sim_pin.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
Summary
Modem.unlock_puk(puk, new_pin)is advertised by theSIM PUKerror path, but the normalasync with Modem(...)startup aborts before user code receives a connected modem object. That makes PUK recovery difficult to perform through the public API precisely when it is needed.Evidence
Affected code:
callstack/modem.py:89-100enters the context manager by calling_connect()before returningself.callstack/modem.py:148-154_initialize_modem()checks SIM state during_connect().callstack/modem.py:197-200raisesSIMPUKRequired("SIM is PUK-locked — use modem.unlock_puk(puk, new_pin) to recover")whenAT+CPIN?reportsSIM PUK.callstack/modem.py:322-329implementsunlock_puk(), but it sends throughself._executor; there is no documented/public recovery entry point that opens the AT transport while bypassing the failing startup check.tests/test_sim_pin.pycovers command formatting and CPIN parsing but has no test proving a PUK-locked modem can be recovered through public API.Repository health checks from this scout run:
Duplicate checks:
Expected behavior
A user with a PUK-locked SIM should have a documented, test-covered way to recover without using private internals or raw serial tools. The recovery path should send the PUK/new PIN safely, then allow normal modem initialization to continue or clearly instruct the user to reconnect.
Actual behavior
The startup path raises
SIMPUKRequiredbefore the context manager yields. The exception tells users to callmodem.unlock_puk(...), but the obvious public pattern never reaches a usablemodeminside theasync withblock.Suggested fix direction
Pick a small, explicit recovery API and document it. Possible shapes:
ModemConfig(sim_puk=..., new_sim_pin=...)and handleSIM PUKduring_check_sim_pin()similarly tosim_pin, with careful validation and redacted logging.await Modem.recover_puk(config, puk, new_pin)that opens only the AT transport, sendsAT+CPIN="puk","new_pin", verifies+CPIN: READY, and closes cleanly.SIMPUKRequiredmessage to point at the actual supported path.Avoid logging PUK/PIN values or SIM identifiers.
Acceptance criteria
SIMPUKRequiredpoints to that API and does not suggest an unreachable call pattern.+CPIN: SIM PUK, successful PUK entry, and subsequentREADYstate with mock transport.Verification gates