Skip to content

ModemAsync

Module: modem_uasync
Source: modules/modem_uasync.py
Depends on: modem, uasyncio

ModemAsync extends Modem and replaces its blocking I/O layer with a uasyncio-based one. Every business method and every I/O method is a coroutine — it must be awaited to execute. Use this class whenever the modem must coexist with other concurrent tasks (timers, sensors, display updates, etc.).

ModemAsync keeps a separate singleton from Modem. Do not instantiate both in the same application — they would both try to own the nrf9151 hardware.

from modem_uasync import ModemAsync
import uasyncio
async def main():
m = ModemAsync()
await m.start() # mandatory: launches the background reader task
...
uasyncio.run(main())

These constraints come from MicroPython’s uasyncio event loop model and apply throughout the entire application, not just to modem calls.

1. Every coroutine must be awaited.
Without await, the call returns a coroutine object — no AT command is sent, no value is returned.

# correct
result = await m.mqtt_publish("topic", "payload")
# wrong — AT command is never sent
result = m.mqtt_publish("topic", "payload")

2. await m.start() must be called before anything else.
It creates the _reader() background task that reads incoming bytes continuously. Without it, every wait_response() will time out.

3. Never use blocking sleep inside a coroutine.
time.sleep() and time.sleep_ms() freeze the entire event loop, preventing _reader() from running and causing spurious timeouts.

# correct
await uasyncio.sleep_ms(1000)
# wrong — blocks the event loop
import time
time.sleep_ms(1000)

4. Long-running loops must be separate tasks.
Use uasyncio.create_task(coro()) to run a coroutine concurrently. Calling await coro() inline blocks main() until that coroutine returns.

# correct — runs concurrently with the rest of main()
uasyncio.create_task(on_message(m))
# wrong — main() never advances past this line
await on_message(m)

5. recv() suspends the calling coroutine until a message arrives.
Always run it inside a dedicated task so it does not block the rest of the program.


Returns the ModemAsync singleton. On the first call, delegates hardware initialisation to Modem.__init__() (powers on the nRF9151, flushes the RX buffer), then sets up the async state (_cmd_event, _msg_queue, _msg_event). Subsequent calls return the same instance without re-initialising.


await m.start()

Launch the background _reader() task. Must be called once before any other method.


send() and read() are inherited from Modem unchanged — they write/read raw bytes synchronously and do not need await. Only wait_response() and send_cmd() are replaced by async versions.

await m.wait_response(expected="OK", timeout_ms=1000)

Wait for a command response without blocking the event loop. The _reader() task accumulates incoming bytes; this method waits on an internal uasyncio.Event instead of polling.

ParameterTypeDefaultDescription
expectedstr"OK"String to wait for.
timeout_msint1000Timeout in milliseconds.

Returns the accumulated response str, "ERROR", or a timeout message.


await m.send_cmd(command, expected="OK", timeout_ms=1000, is_bool=True)

Send an AT command and await the response.

ParameterTypeDefaultDescription
commandstrAT command string — \r\n is appended automatically.
expectedstr"OK"Substring to look for in the response.
timeout_msint1000Timeout in milliseconds.
is_boolboolTrueTrue → returns bool; False → returns raw response str.

topic, payload = await m.recv()

Suspend the calling coroutine until an incoming MQTT message arrives on any subscribed topic.

Returns a (topic, payload) tuple of strings. This method is exclusive to ModemAsync — the sync Modem class has no equivalent.

Run recv() inside a dedicated task to avoid blocking the rest of the program:

async def on_message(m):
while True:
topic, payload = await m.recv()
print(topic, payload)
uasyncio.create_task(on_message(m))

await m.CFUN(mode)

Set the radio functionality level. Same parameters and return value as Modem.CFUN().

ParameterTypeDescription
modeintOne of: 0, 1, 2, 4, 20, 21, 30, 31, 40, 41, 44.

Returns True on success, False otherwise. Uses a 5-second timeout internally.


All methods share the same parameters and return values as their sync equivalents in Modem — refer to that page for full parameter tables.

await m.mqtt_cfg(client_id, keep_alive=60, clean_session=0)

await m.get_mqtt_cfg()

await m.mqtt_conn(op, username, password, url, port, sec_tag=None)

await m.is_mqtt_conn()

await m.mqtt_publish(topic, msg, qos=0, retain=0)

await m.mqtt_subscribe(topic, qos=0)

Same parameters and return values as Modem.

await m.write_certificate(sec_tag, cert_type, content, psw=None, sha256=None)

await m.list_certificate(sec_tag, cert_type=None)

await m.read_certificate(sec_tag, cert_type)

await m.delete_certificate(sec_tag, cert_type)

from modem_uasync import ModemAsync
import uasyncio, ujson
from led import Led
l = Led()
async def on_message(m):
while True:
topic, payload = await m.recv() # suspends until a message arrives
print(topic, payload)
if "v1/devices/me/attributes" in topic:
s = ujson.loads(payload)
led_value = s.get("led")
if led_value == 1:
l.red()
else:
l.off()
async def main():
m = ModemAsync()
await m.start() # mandatory: launches _reader()
uasyncio.create_task(on_message(m)) # run receiver concurrently
await m.mqtt_cfg("nxpico")
await m.CFUN(4)
await uasyncio.sleep_ms(500) # never use time.sleep_ms() here
await m.CFUN(1)
while not await m.is_mqtt_conn():
await m.mqtt_conn(1, "user", "user", "broker.example.com", 1883)
await uasyncio.sleep_ms(1000)
print("connected")
await m.mqtt_subscribe("v1/devices/me/attributes")
while True:
await m.mqtt_publish("v1/devices/me/telemetry", "{'temp': 25}")
await uasyncio.sleep_ms(1000)
uasyncio.run(main())