SDK

AO_Net

The network of a Kode Dot: ping, HTTP, UDP and MQTT as jobs on two workers, the download loan, the screen upload and the Kode channel host.

What it is

Once a Kode Dot has joined a Wi-Fi network, AO_Net lets the console and your app reach other computers. It pings a host, fetches a web page over HTTP or HTTPS, sends a UDP datagram and keeps an MQTT client connected to a broker. It also streams a downloaded file to whoever installs it, uploads a picture of the screen to the cloud, and runs the Kode channel, the session through which the account owner sends console commands.

lwIP runs on the P4, so sockets, HTTP, MQTT and ICMP never use the C5's RPC channel. The C5 only carries frames. That is why AO_Net is not part of sdk/kaos/radio: a ping can run while a 38 s Wi-Fi scan runs, and neither waits for the other. A socket is memory in the P4, not a part of the board, so AO_Net owns no resource. It keeps one piece of state, the MQTT client.

Nothing in AO_Net blocks. A name lookup, the start of a TLS connection and a ping of four echoes all run as jobs on a worker task. The Kode channel is described on internals/kode-channel; this page covers only how AO_Net hosts it.

AO_Net also takes part in the board's rest (sdk/kaos/lowpower). Asleep, the channel and an MQTT client of yours stay connected, and AO_Net tells the rest when it next has work, so the board wakes for it.

How it is connected

flowchart LR
req["Console, kode_mqtt_*()<br/>AO_Radio, AO_Display"]
radio["AO_Radio"]
subgraph p4["On the P4"]
  ao["AO_Net<br/>priority 2"] -- "kdrv_net_run()" --> w1["net_worker"]
  ao -- "stream jobs" --> w2["net_stream"]
end
req -- "NET_*, MQTT_*_REQ_SIG" --> ao
radio -- "WIFI_STATE_SIG" --> ao
w1 -- "lwIP, esp-mqtt, TLS" --> net["Network"]
ao -- "MQTT_MSG_SIG,<br/>NET_*_SIG" --> subs["Subscribers"]
class ao,radio ao
class net ext
class subs app
AO_Net, its workers and the network

Priority and options

AO_Net runs at priority 2 (KODE_PRIO_NET), the lowest of any Active Object, below AO_Gui at 3. Network I/O is the slowest work in the system, so at this level it can only delay itself, and its workers can block on a socket as long as the network takes.

That is load-bearing: every kodeOS subscriber to MQTT_MSG_SIG outranks AO_Net, so a subscriber preempts it and handles each message before the next is published, and a chatty broker cannot fill a subscriber's queue.

Priority 1 (KODE_PRIO_USER_LOW) is the one level below, reserved for an app's own AO. An app that puts a subscriber there, or that blocks, needs a queue deep enough for its broker's bursts. The queue is 16 events (KQP_AO_QUEUE_LEN) and the stack 4096 bytes. kode_init() starts it after AO_Radio.

The Kconfig options below decide what is compiled. All default to on.

OptionCompilesDepends on
KODEOS_NETAO_NetKODEOS_WIFI
KODEOS_MQTTThe MQTT half-
KODEOS_CHANNELThe Kode channelKODEOS_MQTT, KODEOS_CONSOLE
KODEOS_SHOT_HTTPThe screen uploadThe channel, KODEOS_MIRROR
KODE_LOWPOWERAO_Net's part in the rest: the real-time waits, the radio wake and the mqtt check-

The channel's renewal time is KODEOS_CHANNEL_RENEW_S, 82800 s.

States

stateDiagram-v2
[*] --> Net_offline: subscribe WIFI_STATE_SIG, start workers, ask for the state
Net_offline --> Net_online: WIFI_STATE_SIG got-ip, ip4 not 0
Net_online --> Net_offline: any other WIFI_STATE_SIG, stop MQTT
AO_Net states

Net_root is the parent. It answers the queries in both states, because "not connected" is a valid answer: NET_INFO_REQ_SIG, MQTT_STATE_REQ_SIG and CHANNEL_STATE_REQ_SIG. It also handles worker outcomes, since a job submitted online can finish after the network has gone.

Start-up

The initial transition takes the job storage from PSRAM, starts net_worker and net_stream with kqp_worker_start() (12288 bytes of stack each, in PSRAM, so the TLS handshake costs no internal RAM) and installs the MQTT callback. If the storage or net_worker cannot be had, the network services stay down; without net_stream, transfers are refused. Then it asks for the Wi-Fi state, since AO_Radio may have had an address for a while.

Offline

Offline refuses every request with an answer. A caller that gets silence cannot tell "no network" from "still working". Most refusals log no network; <verb> needs an address - 'wifi_join' first and publish the terminator the verb closes on:

RequestRefusal
NET_PING_REQ_SIGNET_PING_END_SIG with ok=false
NET_HTTP_REQ_SIGNET_HTTP_DONE_SIG with rc=KODE_DENIED
NET_UDP_REQ_SIGNET_UDP_DONE_SIG with rc=KODE_DENIED, sent=0
NET_TEST_REQ_SIGTEST_RESULT_SIG named net, ok=false
MQTT_UP_REQ_SIGerr = KDRV_MQTT_ERR_NO_NET, then MQTT_STATE_SIG
MQTT_PUB/SUB/UNSUB_REQ_SIGA log line, then MQTT_STATE_SIG, which closes the verb
NET_STREAM_OPEN_REQ_SIGNET_STREAM_OPEN_RSP_SIG to who with KODE_DENIED and medium = KDRV_MEDIUM_COUNT

A stream read, return or close that arrives right after the network went is handled with no harm.

Going online and offline

Entering Net_online sets online, logs online as <ip>, starts the time client and starts the channel. Leaving it closes an open transfer, because a stuck sopen would refuse every later open with a transfer is already open for the rest of the boot. Any WIFI_STATE_SIG other than got-ip logs the network is gone, stops MQTT, ends the channel and goes offline. A got-ip with a non-zero address is the same network and is handled in place.

Jobs and the busy refusal

There are six job types (kdrv_net_job_t): PING, HTTP_GET, HTTP_POST, UDP_SEND, MQTT_START and MQTT_STOP. net_submit() copies a kdrv_net_req_t into storage in PSRAM and hands it to net_worker. me->busy is set at the submit and cleared only by the NET_JOB_SIG whose sequence number matches.

Every online request checks busy before it builds the job, and a refusal publishes its own terminator, so a busy refusal and a no-network refusal look the same to a waiting console.

AO_Net keeps one job in flight on each worker, so the download of a file never waits behind a long POST of the channel, and the POST never waits behind the download. Every job has a one-shot guard, NET_JOB_GUARD_SIG, of 60 s (KODE_NET_GUARD_MS). If it fires the job is still with the worker, busy stays up, and AO_Net answers whoever waits with KODE_TIMEOUT; the late done event answers nobody twice. A MQTT_START or MQTT_STOP waits for nobody.

The worker waits for a pool block when the pool is dry, so a dry pool delays a done event and never loses it.

Publishing answers

The driver keeps the results of the last job and a new job overwrites them, so AO_Net reads them between the done event and the next submit. A ping publishes one NET_PING_ENTRY_SIG per echo (seq, time_ms, ttl, ok) and then NET_PING_END_SIG (ip4, tx, rx, min_ms, avg_ms, max_ms, ok). AO_Net takes the terminator from the pool first, because net_ping and http_get disarm the console watchdog, so a lost terminator would leave the console busy for good; a short pool gives a short listing.

HTTP publishes up to 8 NET_HTTP_DATA_SIG records (KODE_NET_HTTP_CHUNKS) of 192 bytes each (KODE_NET_HTTP_CHUNK: 191 characters and a terminator), with every non-visible byte rewritten to ., then NET_HTTP_DONE_SIG (status, content_length, body_len, kept, truncated, rc).

The self-test sends one echo to the gateway, since a ping outside the building would test someone else's network, and passes on ok && rx > 0. Every field of a pooled event is written explicitly, because a pool block holds whatever the last event left in it.

MQTT

AO_Net tracks its own phase apart from the driver's state, because the driver knows whether a socket is connected and the phase knows whether anyone asked for one.

stateDiagram-v2
MQTT_IDLE --> MQTT_STARTING: MQTT_UP_REQ, job accepted
MQTT_STARTING --> MQTT_LIVE: connected
MQTT_STARTING --> MQTT_IDLE: start failed
MQTT_STARTING --> MQTT_STOPPING: 15 s, down, or network gone
MQTT_LIVE --> MQTT_STOPPING: down, or network gone
MQTT_STOPPING --> MQTT_IDLE: MQTT_STOP_DONE
The MQTT phases

A stop that AO_Net could not submit leaves the phase alone, otherwise mqtt_down would answer "already down" over a running client. mqtt_up with a client existing answers the current state and no error. The connect deadline is NET_TMO_SIG, 15 s (KODE_MQTT_CONNECT_MS).

A publish, a subscribe and an unsubscribe do not go through the worker: they run on AO_Net's own thread, and a publish is written to the socket at once with esp_mqtt_client_publish() and never stored in the outbox, which takes one message per second and drops silently what is older than 30 s.

The error kdrv_mqtt_err_t is NONE, NO_NET, REFUSED, TIMEOUT or BUSY; BUSY is the refusal of mqtt_up and mqtt_down while the Kode channel holds the one client, printed as >err code=4 msg=busy.

Inbound messages

A broker on the internet sets the rate of this stream. esp-mqtt calls net_on_mqtt() on a task it owns, inside its API lock, and AO_Net can be blocked in a publish under the same lock. Nothing on that path may wait for AO_Net, so a message does not travel to AO_Net as an event.

  1. The ring. The driver copies the message into a ring of 48 messages in PSRAM (KDRV_MQTT_RX_RING) and returns at once. The return is also what makes esp-mqtt acknowledge a QoS 1 message. A message that finds the ring full is dropped, with a warning and a count.
  2. The notice. The driver calls net_on_mqtt() when the ring goes from empty to holding a message, and again when a notice is owed: one was refused, or AO_Net asked for one with kdrv_net_mqtt_rx_owe(). The call carries no message. It posts the static event NET_MQTT_RX_SIG with QACTIVE_POST_X and a margin of 4 free slots (NET_MQTT_MARGIN), so a flooding broker cannot fill the queue and QACTIVE_POST never asserts. The call returns whether the post was taken.
  3. The drain. On NET_MQTT_RX_SIG, net_rx_drain() takes messages with kdrv_net_mqtt_rx_take(), at most one ring's worth per step, into a scratch buffer in PSRAM, and handles each whole before it takes the next. When the ring still holds messages AO_Net posts the notice to itself. If that post is refused it calls kdrv_net_mqtt_rx_owe(), and the next message to arrive sends the notice. A refused notice delays messages and does not lose them. chan_on_watch() also empties the ring every 15 s, but only while the client of the channel serves.
  4. Where a message goes. The driver tags a message of the channel's client (kdrv_mqtt_msg_t.channel) when it arrives. Such a message is a command: it joins the channel's queue and AO_Net never republishes it. The tag is used and not the topic, because the session that names the topic is wiped when the channel ends and the ring outlives it. Every other message is copied into an MqttMsgEvt and republished as MQTT_MSG_SIG from AO_Net's own thread, so only AO_Net publishes MQTT messages. A topic or payload longer than the event carries arrives shortened and says so: topic_cut, data_cut, and retained for a broker replay.

Link events (CONNECTED, DISCONNECTED, ERROR) are not in the ring. net_on_mqtt() copies each into NET_MQTT_LINK_SIG and posts it with a margin of 0, because losing one costs a command its answer or leaves the channel believing in a dead session. CONNECTED carries session_present, the broker's flag that it resumed the session.

Hosting the Kode channel

The channel is a sequence inside Net_online, not a state of its own: a phase cp says which answer AO_Net waits for, and one timer ctmo means a guard on the C5, a wait after a failure or the renewal. The setting chan (kset/chan, boolean, default on) is the switch.

AO_Net asks AO_Radio for the unit's certificate and a signature (ID_PART_SIG, ID_END_SIG, ID_SIGN_RSP_SIG), posts to https://api.kode.diy/kodes/challenge and /kodes/connect as HTTP_POST jobs, starts the MQTT client with the broker the answer names, and follows the in topic.

Waits after a failure back off from 5 s, doubling to 10 min, scaled to 75 to 125 % with khal_random(). The token lives 24 h and is renewed after 23 h.

The session asks for MQTT 5, no clean start, the age chan_keep as its expiry and QoS 1 on the in topic, so the broker keeps the commands sent while the board is away. On a connect AO_Net follows the topic again, publishes the presence again, logs channel: session resumed or channel: new session (nothing kept for this Kode) from the broker's session_present, and starts handing the queued commands to the console.

Replies leave through a 32 KB PSRAM ring (KCHAN_OUT_BYTES) in bursts of 8 lines (CHAN_OUT_BURST). While the channel is active, mqtt_up, mqtt_down, mqtt_pub, mqtt_sub and mqtt_unsub answer busy and touch nothing.

The setting chan_keep

chan_keep is AO_Net's second setting: how long the broker keeps the channel's session while the board is away. The words are 1h, 6h, 1d, 3d, 7d and never, the default is 1d, and the store is kset/chan_keep, a blob of one uint32_t in seconds (never is 0xFFFFFFFF, which a _Static_assert keeps equal to KDRV_MQTT_EXPIRY_NEVER, the MQTT 5 value for no expiry). AO_Net reads it once in its initial transition. A value that is none of the six, or a blob of another size, reads as the default. A set writes the blob, logs one notice and does nothing else: the next connect puts the age into the session, and a renewal is a connect, so a new age reaches the broker within 23 h and sooner after a drop. channel_info shows the age of the live session as keep=.

The queue of commands

The console takes one command at a time, and a Kode Dot that was away comes back to a burst. A command (a message tagged as the channel's) is checked first: a payload that is retained, cut, empty, 240 bytes or longer, or that holds a CR, an LF or a NUL is refused with >err code=3 msg=line (msg=retained for a retained one). A good one joins a ring of 32 lines in PSRAM (KODE_CHAN_QUEUE_MAX), and a line that finds it full is answered >err code=4 msg=busy at once.

chan_q_pump() gives the console the oldest line when none is out and the channel is up, so the answer can leave. It posts a ConsoleLineEvt with source NET and a sequence number, with a margin of 2 slots (CHAN_LINE_MARGIN). The line stays in the ring until it ends.

The channel's own states (CHAN_OFF, CHAN_NO_NET, CHAN_NO_IDENTITY, CHAN_JOINING, CHAN_UP, CHAN_WAITING, CHAN_REFUSED), its failure words, the fast mode, the renewal and the presence message are on internals/kode-channel. net_leave_request() makes AO_Net publish the offline presence, stop the client and answer AO_Boot exactly once with BOOT_NET_LEFT_SIG, so a deliberate restart does not wait for a keepalive.

The board's rest

With CONFIG_KODE_LOWPOWER, AO_Net joins the rounds of AO_LowPower as net (lowpower_join_radio(), because it takes part in radio wakes) and tells AO_Radio from its initial transition that it does (radio_lp_expect_net()). The full state machine is on sdk/kaos/lowpower. This is AO_Net's part.

CaseWhat AO_Net does
ASLEEPThe Kode channel and an MQTT client of yours stay connected on the C5 link, which AO_Radio keeps up. A channel command that arrives is a console line like any other and wakes the board. A message of your client, republished as MQTT_MSG_SIG, wakes it too
Nothing new startsA request that would start a job is refused with its own answer, as offline
Round for ASLEEP refused as busyA job or transfer in flight (net job, net transfer), a screen upload (screen upload), a leave for a jump (net leaving), a join between steps (net joining, because its guards are QTimeEvts that would freeze), the wake time having no event to go in (net wake), or the check's dwell (check dwell)
OFFCloses the channel and any MQTT session, tells AO_Radio with radio_net_left(), and only then answers ready. An OFF round that does not happen leaves the channel to join anew at the next AWAKE. A client of yours is not brought back

The long waits are real time

A QTimeEvt does not count while ASLEEP, because the QP tick is stopped. A renewal due in 23 h would wait for 23 h of awake time, the token would lapse and the broker would drop the session. So the two long waits, the renewal (phase Up) and the back-off (phase Wait), carry a deadline in cdue_us, in khal_time_us(), which runs through light sleep. chan_wait() sets it and chan_due_arm() puts the wait where the board is:

AO_LowPower wakes the board at the deadline with the cause timer, as a radio wake that does not light the glass. On LOWPOWER_STATE_SIG with state RADIO, or with AWAKE after a person used the board, chan_lp_resume() arms ctmo for the real time left and at least 2 s (CHAN_LP_SETTLE_MS), because AO_Radio takes the C5 link out of its rest on the same event. A deadline that passed in the rest acts 2 s after the wake and not a whole renewal later.

Nothing else starts in the rest. An address that arrives while ASLEEP sets lp_rejoin, and the join begins 2 s after the wake. A long timer that fires as the board goes to sleep gives the deadline again and does not begin the renewal. A deadline left registered after the channel ended costs one radio wake that finds nothing to do.

The radio wake

In a radio wake the tick runs and the lock is held, but the glass, the touch panel and every other owner stay at rest. net_lp_state() treats RADIO like AWAKE. AO_Net says it is finished with lowpower_radio_done(), once per radio wake, decided by net_lp_radio_check() at the start of the wake and at the end of each step of the channel. It is done when nothing is in the way of resting (no job, transfer, screen upload or leave) and the channel is not in use or waits for a time further off than 2 s. A renewal that is due, a back-off that has run out and a join between steps are work. A renewal that succeeds is done at once, and so is a try that fails and waits: a back-off starts at about 5 s. A broker that cannot be reached costs one radio wake per try while the back-off grows to 10 min, and the glass never lights. If the work takes longer than KODE_LOWPOWER_RADIO_MAX_MS (60 s), AO_LowPower asks AO_Net to rest anyway.

The check of the mqtt entry

AO_Scheduler has a built-in entry, mqtt, whose work is a look at the sessions in a radio wake. It is the net under the interrupt of the C5: if the interrupt that wakes the P4 for a message is ever lost, the message arrives at the next check. The scheduler tells AO_Net the next time with net_check_at(due_wall_s) (NET_CHECK_AT_SIG, Unix seconds in UTC, -1 for none), and AO_Net keeps one time at a time. AO_Net knows whether the C5 link is up from RADIO_STATE_SIG.

The screen upload

screenshot dst=http token="…" uploads a picture of the screen. The channel carries lines of 240 bytes, which a picture does not fit, so MQTT carries the order and one HTTPS PUT carries the picture.

AO_Console checks the token and posts NET_SHOT_REQ_SIG. AO_Net holds the session's shots prefix and the JWT, asks AO_Display for a JPEG (display_shot_request(), with a 2 s deadline, KSHOT_DISPLAY_MS), copies it into the driver's buffer and releases the loan before it submits KDRV_NET_JOB_HTTP_PUT. A capture answers one NET_SHOT_RSP_SIG result.

An order with fps or secs is a stream: AO_Net answers at once, runs it alone, marks a frame due on each tick of a one-shot 1000 / fps ms timer, and starts it only when the last PUT is done and the worker is free, so a slow link lowers the rate and builds no backlog. Three network errors in a row end a stream (KSHOT_STREAM_ERR_MAX), a 429 drops that frame, and the channel always gets the worker first.

A stream ends with one of the words time, expired, refused, channel, network, replaced, display or stopped. The URL never comes from the order, only the token, whose characters cannot leave the last path segment.

Neither AO_Net nor kdrv_net logs the token, the link or the JWT, and kdrv_net holds the HTTP_CLIENT log tag at INFO during a PUT.

The download loan

A URL can stand in for a file name when an image is installed, the same conversation as sdk/kaos/storage. AO_Net has two 32 KB PSRAM buffers (NET_STREAM_BUFS, NET_STREAM_CHUNK) and lends them by index, 0 or 1, never by address. The reads are jobs on net_stream: the worker fills one buffer while the reader reads the other. A return frees the buffer and asks for the next chunk in the same step.

A close always wins: it forgets the transfer at once, answers a staged open with a refusal and owes the driver a close job, and a new open waits behind it so the driver never sees two transfers. Each job has a guard above the driver's own cap: 20 s plus 5 s of slack for an open or a read, 30 s for a close.

The callers are AO_AppMgr (install src="http…"), AO_Radio (a C5 image over HTTP) and AO_Gui (the files of Kody's pack). None has a deadline of its own, which is why the offline state answers an open and does not ignore it.

The clock

Entering Net_online starts the time client, not boot, because without a network it would retry and never reach a server. When a server answers, the callback sets the wall clock and logs one line, the moment where every timestamp in the session changes from milliseconds since boot to dates.

Specifications

Signals in

What AO_Net receives: requests, answers from other parts, and its own timers.

SignalEventDirectionMeaning
WIFI_STATE_SIGWifiStateEvtin, subscribedGot-ip with a non-zero ip4 is online
NET_PING_REQ_SIGNetPingReqEvtin, postednet_ping; 1 to 10 echoes
NET_HTTP_REQ_SIGNetHttpReqEvtin, postedhttp_get. HTTP_POST is submitted only by the channel
NET_UDP_REQ_SIGNetUdpReqEvtin, postedudp_send; payload up to 64 bytes from an event
NET_INFO_REQ_SIG, NET_TEST_REQ_SIGnonein, postednet_info; test component=net
MQTT_UP_REQ_SIGMqttUpEvtin, postedStart the client
MQTT_DOWN_REQ_SIG, MQTT_STATE_REQ_SIGnonein, postedStop; republish
MQTT_PUB_REQ_SIG, MQTT_SUB_REQ_SIG, MQTT_UNSUB_REQ_SIGMqttPubEvt, MqttTopicEvtin, postedSend, follow, unfollow
CHANNEL_STATE_REQ_SIGnonein, postedchannel_info; AO_Gui when the settings screen opens
CONFIG_REQ_SIGKodeConfigReqEvtin, subscribedThe keys chan and chan_keep
RADIO_STATE_SIGRadioStateEvtin, subscribedWhether the C5 link is up, for the mqtt check
LOWPOWER_ROUND_SIG, LOWPOWER_STATE_SIGLowPowerRoundEvt, LowPowerStateEvtin, subscribed, with KODE_LOWPOWERA round to rest or to close the sessions, and the state the board went to
NET_LINE_DONE_SIGNetLineDoneEvtin, posted by AO_Consolenet_line_done(): the line of source NET ended, with the sequence of the line
NET_LINE_BUSY_SIGnonein, posted by AO_Consolenet_line_busy(): the console was busy and did not take the line
NET_CHECK_AT_SIGnonein, posted by AO_Scheduler, with KODE_LOWPOWERnet_check_at(): the next time of the mqtt check
ID_PART_SIG, ID_END_SIG, ID_SIGN_RSP_SIGin, posted by AO_RadioThe channel's certificate and signature
NET_STREAM_OPEN/READ/NEXT/CLOSE_REQ_SIGNetStreamOpenEvt for the openin, postedThe download loan
NET_SHOT_REQ_SIG, NET_SHOT_STOP_SIGNetShotReqEvt, NetShotStopEvtin, postedscreenshot dst=http and its stop
DISPLAY_SHOT_RSP_SIGDisplayShotRspEvtin, posted by AO_DisplayThe JPEG, lent
NET_LEAVE_REQ_SIGnonein, postednet_leave_request(), before a restart
NET_JOB_SIG, NET_STREAM_JOB_SIGKqpWorkerDoneEvtin, from the workersJob outcomes
NET_JOB_GUARD_SIG, NET_STREAM_GUARD_SIG, NET_TMO_SIG, NET_CHAN_TMO_SIG, NET_CHAN_FAST_SIG, NET_CHAN_WATCH_SIG, NET_CHAN_LINE_SIG, NET_CHECK_TMO_SIG, NET_SHOT_TICK_SIG, NET_SHOT_WAIT_SIGnoneself, one-shotGuards, the connect deadline, the channel's wait (real time for the renewal and the back-off), fast-mode hold (60 s), client look (15 s, which also empties the receive ring), the bound on the line the console runs (30 s) or its retry (100 ms), the dwell of the mqtt check (3 s), stream pace, display deadline
NET_MQTT_LINK_SIG (NetMqttLinkEvt), NET_MQTT_RX_SIG (a static event), NET_CHAN_OUT_SIGin, privateFrom esp-mqtt's task (a link event, or the notice that the receive ring holds messages) and from the ring's writer

Signals out

What AO_Net publishes or posts back.

SignalEventDirectionMeaning
NET_PING_ENTRY_SIG, NET_PING_END_SIGNetPingEntryEvt, NetPingEndEvtout, publishedThe ping listing
NET_HTTP_DATA_SIG, NET_HTTP_DONE_SIGNetHttpDataEvt, NetHttpDoneEvtout, publishedThe GET
NET_UDP_DONE_SIG, NET_INFO_SIGNetUdpDoneEvt, NetInfoEvtout, publishedNetInfoEvt: ip4, gw4, mask4, dns4 in network byte order, the MQTT state, online and rx_dropped, the messages the driver dropped since boot because its receive ring was full; answered in every state
MQTT_STATE_SIGMqttStateEvtout, publishedWhere the client stands
MQTT_MSG_SIGMqttMsgEvt: topic, len, topic_cut, data_cut, retained, dataout, publishedA message from the broker
CHANNEL_STATE_SIGChannelStateEvtout, publishedState, reason, wait, fast mode, the upload counters, and the session (keep_s, the age the connect asked for, and session, what the broker said)
CONSOLE_LINE_SIGConsoleLineEvtout, posted to AO_ConsoleA command of the channel, source NET, one at a time from the queue, with a sequence number
NET_SHOT_RSP_SIGNetShotRspEvtout, posted to the askerResult of a capture or a stream
NET_STREAM_OPEN_RSP_SIG, NET_STREAM_DATA_SIGStorageOpenRspEvt, StorageDataEvt: idx, len, eof, statusout, posted to the readerThe loan
BOOT_NET_LEFT_SIGnoneout, posted to AO_BootThe channel has left; once per NET_LEAVE_REQ_SIG
TEST_RESULT_SIGnamed netout, publishedThe verdict

Calls, limits and verbs

ThingValue
Blocking callskode_net_lookup(host, &ip4), kode_net_ping(host, count, &stats) (count 1 to 10), kode_http_get(url, buf, cap, &result), kode_udp_send(host, port, data, len). Never from an AO; reentrant, each call opens its own socket. https:// is checked against the Mozilla bundle. A 404 returns KODE_OK and result.status says 404
Posting callskode_mqtt_connect(), kode_mqtt_disconnect(), kode_mqtt_publish(), kode_mqtt_subscribe(), kode_mqtt_unsubscribe() and kode_mqtt_connected(). Safe anywhere. There is deliberately no polling accessor: subscribe an AO to MQTT_MSG_SIG
Request helpersnet_ping_request(), net_http_get_request(), net_udp_send_request(), net_info_request(), net_test_request(), mqtt_*_request(), net_leave_request(), net_shot_request(), net_shot_stop_request(), net_stream_*
Size limitsHost 64, URL 80, datagram 64, URI 96, user 33, password 65, topic 80, payload 240 (KODE_MQTT_PAYLOAD_MAX, one console line). Receive ring 48 messages (KDRV_MQTT_RX_RING), command queue 32 lines (KODE_CHAN_QUEUE_MAX). _Static_asserts check each against the driver's copy, including 8 x 191 = 1528 (KDRV_NET_BODY_MAX)
GuardsJob 60 s; connect 15 s; stream open or read 20 s + 5 s; stream close 30 s; screenshot display 2 s; a line out to the console 30 s
Stored settingskset/chan, boolean, default on. kset/chan_keep, one uint32_t of seconds, default 86400
Consolenet_ping host= count=, net_info, http_get url=, udp_send host= port= data=, mqtt_up uri= user= pass=, mqtt_down, mqtt_pub, mqtt_sub, mqtt_unsub, mqtt_info, channel_info, config key="chan", config key="chan_keep", screenshot dst=http, test component=net. The ICMP verb is net_ping; plain ping is the console liveness check

Gotchas

Example code

This fetches a page over HTTPS and logs the status and the size, once the Kode Dot is online. Join a network first (wifi_join on the console). The log level must be i: config key="log" value=i.

/*
 * http_status.c - 2026-10-28 - from Kode with love
 *
 * Fetches https://api.kode.diy/ with AO_Net's blocking GET and logs the HTTP
 * status and how many body bytes arrived. The Kode Dot must be online. With
 * no network the log shows the failure code instead of a status.
 */
#include "kode.h"
 
/* Bring the system up, then GET one page every 10 s and report it. */
void app_main(void)
{
    kode_init();
 
    while (1) {
        char body[128];
        kode_http_result_t result;
        kode_status_t status = kode_http_get("https://api.kode.diy/", body,
                                             sizeof(body), &result);
 
        if (status == KODE_OK) {
            KLOG_I("http_status", "status %u, %u body bytes",
                   (unsigned)result.status, (unsigned)result.body_len);
        } else {
            KLOG_I("http_status", "no answer, status %d", (int)status);
        }
        kode_delay_ms(10000);
    }
}

Compiled against the SDK with ESP-IDF v6.1 on 2026-10-28. TBD: a run on a Kode Dot.

In the SDK