From fb9927a1849f8ef10595f3cb13a234ce0b0bce67 Mon Sep 17 00:00:00 2001 From: Bran Date: Thu, 15 Oct 2026 18:45:57 -0700 Subject: [PATCH 5/5] Document the Quectel EP06 port Covers enabling adb through usbcfg, the headless configuration, the AT port being on /dev/smd8 rather than smd7, and the version 101 layout of 0xB114 with the evidence it was derived from and what remains unverified. --- doc/ep06.md | 190 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 doc/ep06.md diff --git a/doc/ep06.md b/doc/ep06.md new file mode 100644 index 0000000..380e3bd --- /dev/null +++ b/doc/ep06.md @@ -0,0 +1,190 @@ +# Quectel EP06 + +Rayhunter runs on the Quectel EP06 LTE module. Not on the host it is +plugged into: inside the module itself, which runs its own Linux and has +its own `/dev/diag`. The host only needs to reach the web UI. + +This was developed on an EP06-A, firmware `EP06ALAR02A03M4G`, inside a +GL.iNet GL-XE300 running OpenWrt 24.10.6. The host architecture does not +matter, because nothing runs there. That is the same arrangement the +PinePhone port uses, and it means any device carrying an EP06 is a +candidate, including routers whose own CPU could never run the daemon. + +## The module runs Linux + +Quectel staff have stated in their own forums that only the EC2x, BG96, +EG91, EG95 and some EC200A/R modules run Linux, and that "the others +should run the RTOS". The EP06 is not in that list. On this firmware that +is wrong: + +``` +# uname -a +Linux mdm9640-perf 3.10.49-g3bd6b7f #1 PREEMPT Tue Jul 17 12:47:32 CST 2018 armv7l GNU/Linux +# ls -l /dev/diag +crw-rw---- 1 root diag 244, 0 /dev/diag +# df -h / +Filesystem Size Used Available Use% Mounted on +ubi0:rootfs 66.9M 13.1M 53.8M 20% / +``` + +Root, a writable ubifs rootfs with about 50 MB free, ARMv7 with NEON and +VFPv4, and `/dev/diag` present with the usual major 244. SysV init, so a +symlink into `/etc/rc5.d` is enough to start at boot. + +Memory is the real constraint: 138 MB total with roughly 20 MB free once +the vendor's own daemons are up. + +## Enabling adb + +adb is a flag in the USB composition and is off by default. Read it +first: + +``` +AT+QCFG="usbcfg" ++QCFG: "usbcfg",0x2C7C,0x0306,1,1,1,1,1,0 +``` + +The fields after the PID are `diag, nmea, at_port, modem, rmnet, adb`. +Set the last to 1 and reset: + +``` +AT+QCFG="usbcfg",0x2C7C,0x0306,1,1,1,1,1,1 +AT+CFUN=1,1 +``` + +The module drops off the USB bus and returns after about a minute. On the +host, `adb devices` should then list it. + +Notes: + +* `AT+QADBKEY` and `AT+QLINUXCMD` both return ERROR on this firmware. + Neither is needed. The key mechanism described for newer modules such + as the RM5xx does not apply here. +* The adb interface is vendor-specific and no kernel driver claims it, so + it produces no `dmesg` line. Its absence from the kernel log does not + mean it failed; check `adb devices`. +* Keep the PID at `0x0306`. The kernel's `option` driver already handles + the EP06's composition variants. +* The port numbering may shift after the reset. Do not assume a `ttyUSB` + index survives a composition change. +* Rolling back is the same command with the last field set to 0. The AT + port survives every composition that keeps `at_port` at 1. + +## Configuration + +Use the headless device profile. The EP06 has no display, no battery and +no buttons, and a daemon trying to drive hardware that is not there is a +plausible crash. + +```toml +device = "ep06" +ui_level = 0 +key_input_mode = 0 + +qmdl_store_path = "/data/rayhunter/qmdl" +port = 8080 + +# ~50 MB rootfs, so leave more headroom than the 1 MB default +min_space_to_start_recording_mb = 5 +min_space_to_continue_recording_mb = 3 + +# The module boots without a clock, so recordings are stamped in 1970 +# until something sets it. Let the web UI push the browser's time. +clock_sync_mode = 1 +``` + +`/data` exists and is writable. `rootshell` is not needed: adb is already +root. + +The module has no IP stack reachable from the host and no route to the +internet, so `ntfy_url` cannot work. Notifications have to be polled from +the host side. + +## The AT command processor is on /dev/smd8 + +Reading the SIM uses `/dev/smd7` on the Tplink and Orbic paths. On the +EP06 `smd7` is held open by a vendor daemon and never answers, so a read +there times out rather than failing. `smd8` is the AT port: + +``` +/dev/smd7 no response +/dev/smd8 [AT][OK] +/dev/smd9 no response +``` + +## 0xB114 ServingCellTiming, version 101 + +The EP06 emits version 101 of this log. The existing struct asserts +version 1, so before this change every occurrence was discarded, and +because the skipped-message path wrote the whole packet into the analysis +output, the `.ndjson` ran thirteen times larger than the capture it +described and filled the filesystem in hours. + +Version 101 is a different shape: + +``` +48-byte header | N x 44-byte record | final record truncated to 8 bytes +``` + +Derived from 205 messages and 2647 records captured on a live Verizon +cell. Every payload fit that split with no remainder: + +``` +364 = 48 + 44x7 + 8 672 = 48 + 44x14 + 8 +540 = 48 + 44x11 + 8 716 = 48 + 44x15 + 8 +584 = 48 + 44x12 + 8 760 = 48 + 44x16 + 8 +628 = 48 + 44x13 + 8 848 = 48 + 44x18 + 8 + 892 = 48 + 44x19 + 8 +``` + +Established: + +* Records are eleven little-endian u32s, 32-bit aligned throughout. +* Word 0 steps by exactly 30720 between consecutive records, which is one + LTE subframe at 30.72 MHz, and repeats unchanged when the radio took no + new measurement that subframe. +* Words 5, 6, 8 and 10 were zero in all 2647 records. +* Words 7 and 9 each hold one u16 duplicated into both halves. +* Words 3 and 4 are a matched pair with the same range. +* Word 1 is signed and small, roughly -18432 to 16385. +* There is no record count anywhere in the header. The array has to be + read to the end of the buffer. +* Every message ends on a record cut short after 8 bytes, whose first + word repeats the last whole record's value. + +Not established: which field is which. In the version 1 layout the +equivalents are the downlink and uplink frame timing offsets and the +timing advance, and word 1 has the right shape for a timing adjustment, +but that is a guess. Confirming it needs a device in connected mode so the +values can be compared against the timing advance the network assigns, +and the capture above came from a module in limited service with no SIM +(`+CPIN?` returning `+CME ERROR: 10`). + +So the parser exposes word 0, which is verified, and leaves the rest as +raw words with accessors describing what is known. Naming them would +produce numbers that look reasonable and are not. + +## What works and what does not without a SIM + +A module with no SIM still decodes everything broadcast: SIB1 through +SIB8, neighbour lists, paging. The broadcast-based heuristics work. + +The analyzers that need an attach do not: `imsi_requested`, +`null_cipher`, `nas_null_cipher`, and +`connection_redirect_2g_downgrade` all require the modem to exchange +signalling with the network. Roughly half the detection is inert until +there is service. + +## Reproducing the layout analysis + +The scripts used are not included here, but the method is simple enough +to repeat: pull the payloads for one log code out of a QMDL file, split +them on the 48/44/8 boundary, read each record as eleven u32s, and +tabulate per-column distinct counts and deltas across every sample. Fields +that never change are padding. Fields that step by a constant are +counters. Fields whose unsigned reading is enormous and whose signed +reading is small are signed. + +A QMDL file is the raw diag stream: HDLC frames delimited by `0x7E` with +`0x7D` escaping, each carrying `0x10, pad, len:u16, len:u16, code:u16, +timestamp:u64, payload`. -- 2.47.3