Skip to content

Cellular Links

Detecting a 4G module, dialling and keeping up its PPP link, and recovering a module that has stopped answering. Source: internal/wwan.

Detection and profiles

detect.go finds modules from usbdevs -v and the serial drivers OpenBSD attaches (umsm, uark, uslcom, uftdi, umodem, ucom), and their /dev/cuaU* callout devices. profiles.go maps USB ids to a profile with its candidate AT ports and quirks:

Profile USB ids
sierra-mdm9200 (MC7700 family) 1199:68a3, 1199:68a2
huawei-generic 12d1:1506, 12d1:1436, 12d1:14ac
quectel-ec25 2c7c:0125, 2c7c:0121
generic-ppp anything else

A device has at most one cellular interface (type wwan). The link is ppp0.

Every AT port access takes the UUCP lock /var/spool/lock/LCK..cuaUN, the same one pppd uses, and takes over a stale lock left by a dead process.

edge-agent modem status and edge-agent modem at <cmd>... give a shell view of the same module.

Applying the config

Apply writes /etc/digilan/wwan.chat (0600) and /etc/ppp/pap-secrets.digilan, can read the APN from the module when the config asks for automatic APN, and stops the link when settings change. It never dials itself; the supervisor does.

pppd runs with 115200 noauth nodefaultroute noipdefault ipcp-accept-local lcp-echo-interval 20 lcp-echo-failure 3, plus local and a peer address for modules whose profile needs them. There is no persist, maxfail or usepeerdns: the default route belongs to uplink failover, and the agent owns the retry. pppd reopening the exclusive AT port on every retry would interrupt a module that is still searching for a network.

The supervision loop

Supervise runs in its own goroutine, independent of any connection to the platform, and calls superviseStep every 30 s (not in dry-run).

State What happens
Link up, probe verdict usable Nothing. Reading the module would take the AT port from the running link
Link up, verdict down for 90 s Rebuild the link, subject to the restart backoff
pppd running, no address after 2 min Stop it
Link down, within 45 s of a stop Wait (quiet period)
Link down Read the module and dial if it has a network

Restart backoff: 5 minutes, doubling to a 1 hour cap, forgotten after 30 minutes of healthy link.

An address is not reachability

linkUp only says ppp0 holds an address, meaning IPCP finished. Whether anything crosses it is the failover probe's verdict, read through wanfailover.ProbeVerdict. A link can hold an address, answer pppd's echoes and carry nothing.

Dialling

dialWhenReady dials only when the module reports registration home or roaming. Dialling a module that is still searching cannot succeed and costs it the port it needs to register. Before dialling it programs the APN with AT+CGDCONT when the module's APN differs (and dials on the next pass), and runs ifconfig ppp0 create.

The pre-dial read is the only regular module reading: once the link is up, pppd holds the AT port and status queries fail. The reading is cached in /var/db/digilan/modems.json, keyed by module serial, and reported with the current error attached, so the portal shows "this is what it last said" instead of a blank card. The state-report path reads the module at most every 2 minutes and never waits for a busy port.

Remedies

Each remedy has its own trigger and rate limit; they are not one sequence.

Remedy Commands When Limit
Data-mode escape +++ then ATH, written blind with 1.2 s guards Dial path, no AT port answers, link not up backoff 2 → 15 min
Operator re-select AT+COPS=2, AT+COPS=0 SIM ready, registration denied every 10 min
Radio cycle AT+CFUN=0, 3 s, AT+CFUN=1 SIM ready, searching or not registered for 5 min every 15 min
Reboot the device reboot See below once per 6 h

Re-select wins over a radio cycle when both apply. Re-select and radio cycle are decided by escalationFor, which both the dial path and the state-report path use, so they share one rate limiter. A remedy that could not be delivered (no port answered) does not consume its interval in the report path, and says so in the log.

The escape log line carries opened=N total=N. Fewer opened than total means something else holds the other ports; equal means the module is not speaking at all, which is the case that needs power.

When nothing reaches the module

Every remedy except the escape is an AT command, so none reaches a module that answers on no port. A device reboot does recover such a module. lastresort.go makes that the last remedy, narrowly:

  • only when the cellular link is the device's sole uplink (no other interface has WANPriority > 0 in the config). With a wired uplink present it does nothing: rebooting would drop every client to rescue a backup nobody is using;
  • after 45 minutes with no port answering, at most once per 6 hours.

The pacing is on disk

The remedy restarts the process, so in-memory pacing would be erased by the act it paces. The marker /var/db/digilan/wwan-reboot.json is written (temporary file plus rename) before the reboot, and the reboot is skipped if it cannot be written. A test discards all in-memory state as a reboot would and asserts the remedy does not fire twice.

The self-heal ladder is separate: it acts when the whole device has no internet, whatever the uplink.