What a device is, what varies between one and the next, and who owns that variation.
Status: the Pixel 3a is the phone (2026-09-15).
moarchy-sargo-0.2.2-20260915 was built by image/build.sh, verified (132
checks), flashed over fastboot, and booted to the shell from a clean rootfs
with nothing of postmarketOS's anywhere in it. Our kernel, our device and
firmware packages, systemd, autologin, sway. There is no initramfs at all
(D24), and the bootloader confirms the slot is marked successful (D26).
On that image, measured rather than reported: Wi-Fi, Bluetooth, sound,
camera, vibration, NFC, calls with voice on them, and SMS in both
directions — D27 through D32. All of it from packages, with nothing
installed by hand. pacman -S works too, which it did not before
(SECURITY_LANDLOCK).
What is open on this device is quality rather than absence: a matrix-only camera profile with no HueSatMap or look table, and telephony on an operator with no circuit-switched fallback, which needs a SIM nobody here has.
The PinePhone was dropped on 2026-09-19. It was the only device that could not clear Hyprland's GLES 3.0 floor, and it had not been booted since D1–D6. build-log.md has the account. Every criterion below that named it is amended or deleted rather than left standing with its own obituary attached.
The acceptance criteria are the contract to argue with; where one is my reading rather than your decision it is marked ?.
Companion to structure.md, which decides what a package is and where packages come from. This file decides what a device is. It amends one of that file's non-goals, and §2 says so out loud rather than in a PKGBUILD.
The target list is Pixel 3a (sargo, shipping) and Fairphone later. The
question is not "can moarchy run on a phone" — §9 says it does — but what
varies per device, where that variation lives, and who owns it.
Get that wrong and the next device costs as much as this one. Get it right and it costs a package and two pins.
Every device this project targets is the same shape: Qualcomm, fastboot, an
Android boot.img, A/B slots, AVB to defeat, non-removable storage, Adreno.
The Pixel 3a and the Fairphone 4/5 differ in their pins, not in their pipeline.
That is a decision rather than an observation, because it was once the other way round. moarchy shipped first on a PinePhone, which booted from raw sectors (the Allwinner BROM reads u-boot SPL at byte 131072), shipped on removable media so the deliverable was a whole-disk image, had its kernel, u-boot and firmware already packaged for pacman by DanctNIX, and had neither A/B slots nor verified boot. Generalising outward from that device would have made every one of those the default shape of something. D0 is the decision not to, and it outlived the device that prompted it: what is left is the general case, and the next phone is an Android phone.
structure.md §2 says, and meant:
A distribution. We are not forking Arch Linux ARM or DanctNIX. Their kernel, u-boot, firmware, modem stack and ALSA UCM profiles are consumed as packages from their repos, never rebuilt here.
It held while DanctNIX packaged a whole device stack for us. It cannot hold for an Android phone, and pretending otherwise is how this turns into a surprise.
postmarketOS has done the SDM670 bring-up and maintains it well — the kernel
tree at gitlab.com/sdm670-mainline/linux was tagged sdm670-v7.2.3_beta2 on
2026-09-03, and device-google-sargo sits in their community tier. But all
of it is Alpine .apk. There is no pacman repo anywhere that carries an
SDM670 kernel or the sargo firmware.
So for every device it ships, moarchy builds and publishes:
- a kernel package, from someone else's mainline fork, at a pinned tag
- a firmware package, from publicly-downloadable vendor blobs
- a device package tying them together
That is a distribution-shaped commitment: when the kernel tree moves, we move; when it stops being maintained, the device is dead and we are the ones who notice. It is the price of an Android phone and it does not get cheaper for the next one.
The amendment is narrow and stays narrow: we package what upstream has already brought up, at a pin, for devices we ship. We do not do bring-up, we do not carry patches of our own against a kernel, and we do not package for devices we do not ship. If a device needs us to write kernel code, it is out of scope and the answer is no.
We still consume from
[danctnix]—libdngandlibmegapixels, whichpkgbuilds/megapixelslinks against and Arch Linux ARM does not carry. The repo outlived the phone it was added for.
- Not a device-support matrix. One device shipping and a second chosen deliberately. A half-working third helps nobody.
- Not runtime device detection. An image is built for a device and says which. Nothing probes the SoC at boot to decide what it is (D3 is about hardware values, which is a different thing).
- Not an Android app compatibility layer. Amended by android.md §1: Halium stays out, Waydroid comes in as an optional package.
┌─────────────────────────────┐
│ rootfs (device-independent)│
moarchy-meta ──────►│ pacstrap + configure.sh │
moarchy-device-X ──►│ identical for every device │
└──────────────┬──────────────┘
│
▼
boot/android-bootimg.sh
mkbootimg + AVB
→ boot.img + rootfs.img
+ flash.sh
One rootfs builder. One boot backend today, chosen by name. N device packages.
The indirection stays with a single backend in it. It was introduced with two
and has been exercised by both, which is the only reason it is known to be a
seam and not a guess; collapsing it back into build.sh now would throw away
the one part of this pipeline that has been demonstrated rather than assumed.
Audited against the tree at d258680, not guessed. This is the whole list.
| # | coupling | where it is today | verdict |
|---|---|---|---|
| 1 | kernel, bootloader, firmware | was image/build.sh:203, now the device package's depends |
device package depends — done |
| 2 | output scale, gaps, orientation | was a file in default/sway/, now pkgbuilds/moarchy-device-sargo/sway.conf |
device package file — done |
| 3 | modem daemon | was bin/moarchy-firstboot:71 — eg25-manager, now DEVICE_SERVICES in device.conf |
device package — done |
| 4 | boot artifact + partitioning | image/build.sh:287–end |
boot backend |
| 5 | battery sysfs path | moarchy.device/Device.qml:166-167 — axp20x-battery |
probe, no key |
| 6 | output name | moarchy.control-center/ControlCenter.qml:825 — DSI-1 |
probe, no key |
| 7 | typeable-keyboard test | bin/moarchy-has-keyboard |
already generic — fix comment only |
Everything else in default/ and bin/ is already device-independent: the QML
reads screen.height with a 720 fallback rather than assuming, and
omarchy-brightness-display already takes the first entry in
/sys/class/backlight.
Seven items, of which two are already solved and two more dissolve into probes. The genuine per-device surface is four things.
Rows 5 and 6 could each be a device.conf key. They should not be.
A key is a cost paid per device, forever: every new device must supply it, a wrong one fails at runtime on hardware you may not have, and nothing checks it. A probe is written once and is right on hardware nobody has tested yet.
- Battery — scan
/sys/class/power_supply/*/fortype == "Battery"and take the first. Correct on sargo, and on a device with a differently-named PMIC that nobody has plugged in yet. - Output — ask sway.
swaymsg -t get_outputsnames the panel; the shell wants "the one output this phone has", not the stringDSI-1.
D3 states the rule: a device.conf key is justified only when the value
cannot be discovered at runtime. Scale (row 2) qualifies — nothing in sysfs
knows that 440 ppi wants scale 3, because that is a judgement about thumbs and
not a fact about hardware. The battery path does not qualify.
D1 Each supported device has exactly one package, moarchy-device-<codename>,
built from pkgbuilds/moarchy-device-<codename>/. Codenames are the upstream
ones: sargo, FP4.
D2 It is the only place a device's hardware is named. It carries:
| path | content |
|---|---|
depends |
kernel, firmware, bootloader, modem stack for this device |
/usr/share/moarchy/device/sway.conf |
the output scale and any hardware sway needs told |
/usr/share/moarchy/device/device.conf |
shell-sourceable keys that survive D3 |
/usr/share/moarchy/device/build-info |
codename, so a running phone can say what it is |
| udev rules, systemd units | only where the hardware needs them |
D3 device.conf holds only values that cannot be discovered at runtime
(§4.1). Every proposed key must come with the sentence explaining why a probe
cannot answer it. Initially this is expected to be scale and little else; a
key list that grows past ~5 is a sign the probes are not being written.
D4 Every device package provides=('moarchy-device') and
conflicts=('moarchy-device'), so pacman enforces exactly one installed.
D5 moarchy-meta gains moarchy-device in depends. It stays otherwise
device-independent and names no hardware. A rootfs with no device package is a
pacman error, not a phone that boots wrong.
D6 config/sway/config includes /usr/share/moarchy/device/sway.conf, a
fixed path owned by whichever moarchy-device-* package is installed. The
moarchy package owns nothing under device/ — two packages cannot own one
path, and that is the boundary which makes a mistake here a build error rather
than a decision.
D8 image/build.sh sources its backend from image/boot/$BACKEND.sh. The
device-specific work interleaves with the device-independent work rather
than sitting in a tail that could simply be cut off — provenance,
configure.sh and the rootfs trim all run between the kernel step and the
image step. So a backend is three hooks, not one tail:
backend_kernel— after pacstrap: whatever this device needs doing to the kernel. On sargo, checks only, because this backend ships no initramfs (D24)backend_fstab— what/etc/fstabshould say; the disk layout is the backend's business, and sargo has no separate/bootpartition to mountbackend_image— after the trim: assemble and compress the artifact
The hook order is load-bearing. Moving the kernel step to sit after
configure.sh would be a behaviour change smuggled in as a refactor.
D9 One backend today:
android-bootimg—mkbootimgwith the DTB appended, a rootfs ext4, an AVB-disablingvbmeta. Output: a directory ofboot.img,rootfs.img,vbmeta.imgand aflash.sh, tarred and compressed.
Amended 2026-09-19. There were two; sunxi-gpt went with the PinePhone.
D10 The artifact is a directory, not an image file, and that is not
papered over. An Android image is three files and a script you run with the
phone in fastboot; there is nothing to dd. Every script that consumes an
artifact takes that shape, and none of them infer it from an extension.
D11 The backend is chosen by DEVICE=<codename>, defaulting to sargo.
scripts/build-image.sh passes it through and refuses a codename with no
pkgbuilds/moarchy-device-<codename>/.
D24 Added 2026-09-14. The android-bootimg backend ships no
initramfs. The kernel mounts root itself: root=PARTLABEL= is resolved out
of the GPT by early_lookup_bdev(), and EXT4_FS, MMC_BLOCK,
MMC_SDHCI_MSM, EFI_PARTITION and DEVTMPFS_MOUNT are all =y in the
pinned config, so nothing is left for an early userspace to do.
backend_kernel asserts the first three against modules.builtin rather than
trusting a config file in another package, because the failure is otherwise a
phone that shows two penguins and stops.
The initramfs was only ever resolving
root=LABEL=through udev, and it resolved it into a night: eighteen megabytes of pre-userspace code on the one device in the project that cannot print (D23), where every failure looks exactly like every other failure.PARTLABEL=needs no udev, so the dependency and the debugging surface go together.
Amended 2026-10-17. Still no initramfs, but the boot image now carries a 124-byte empty cpio as its ramdisk (android-image.py --empty-ramdisk), because a ramdisk_size of 0 is not "no initrd" to sargo's ABL. Given none, it still passes the kernel a stale one: a handset that looped on 0.5.0 logged Trying to unpack rootfs image as initramfs and Freeing initrd memory: 13924K from an image with no ramdisk in it. The kernel then runs that region's /init instead of mounting root, and the phone shows a penguin and lands in fastboot with Enter reason: reboot bootloader, nothing in the journal. That reason is what Android's init does on a fatal error, so the region is most likely the Android boot ramdisk the handset last ran. Whether a handset has one depends on what it booted before, which is why the same image boots on one 3a and loops on another.
Found by elimination, all with
fastboot bootso nothing was written: the release kernel booted with any ramdisk at all (one that waited for a USB console, one that continued at once, and finally an empty one) and looped without one, including on 7.2.3 with the bootloader's cmdline ignored (CMDLINE_FORCE).boot_delay=40slowed the kernel log enough to film, and the initrd lines were in it. The empty cpio has no/init, so the kernel falls through toroot=andinit=exactly as before; nothing above changes.
D12 image/verify.sh splits the same way. The backend asserts its own
artifact facts (boot.img magic, the DTB appended, vbmeta flags = 2, the empty
ramdisk, and the cmdline read back out of the header), and the behavioural
section — the first-boot scripts run in a chroot — stays shared because it is
about the rootfs.
Amended 2026-09-15. verify_artifact and verify_grow are required;
verify_rootfs is a third and optional hook, run against the mounted
rootfs, for facts true of one device's rootfs and meaningless for another's.
sargo uses it for D26, which is invisible in every shared check because an
image missing it is otherwise perfect. Optional rather than required because a
backend with no such facts should not have to define an empty function to say
so.
D13 manifest.toml gains one section per device, and it stays the only
file in the project that says what version of anything is (V1):
[device.sargo]
kernel-url = "https://gitlab.com/sdm670-mainline/linux"
kernel-ref = "sdm670-v7.1.3" # a tag; makepkg can verify it
firmware-url = "https://github.com/TheMuppets/proprietary_vendor_google_sargo"
firmware-ref = "c631f0f2aa24ea60cfb505d327cd4ae56ca27f16"D14 A device pin is a hard error when unread, exactly as every other pin is
— scripts/manifest.sh already enforces this and needs no change.
Measured on the device (serial 987AY139XT), not read off a wiki.
| codename / SoC | sargo / SDM670, 4 GB LPDDR4X, 64 GB eMMC |
| panel | 1080×2220, density 440 → scale 3 gives 360×740 logical |
| bootloader | b4s4-0.4-8048689, secure-boot: PRODUCTION, now unlocked |
| slots | A/B, current-slot: a; bootable is a countdown, not a state — D26 |
| dynamic partitions | retrofit — no super; system_a=p68, system_b=p69 |
max-download-size |
0x10000000 (256 MiB) → rootfs must flash sparse |
| flash targets | kernel→boot, rootfs→userdata, plus vbmeta |
| GPU | Adreno 615, freedreno — GLES 3.2 and Vulkan |
The scale number is the whole reason row 2 of §4 is a key and not a probe: 1080×2220 at scale 3 is 360×740 logical, and 360 logical pixels wide is what every layout constant in the shell was tuned against — the bar height, the app drawer grid, the keyboard's exclusive zone, the home strip. Nothing in sysfs could have worked that out; it is a judgement about thumbs.
A postmarketOS image was booted with fastboot boot — RAM only, nothing
written to flash — to settle the hardware questions before any of this was
built around them. All of it held:
| question | result |
|---|---|
| mainline SDM670 kernel boots | yes — 7.0.10-sdm670, Device: Google Pixel 3a (google-sargo) |
| boot image format | yes — the header reverse-engineered in D15 was accepted and jumped to |
| display / DRM / KMS | yes — msm_dpu ae01000.display-controller drove the full 1080×2220 panel with a rendered UI |
| touch | yes — a tap on the on-screen keyboard registered at the shell |
| iteration loop | yes — fastboot boot writes nothing, so a bad kernel costs a power cycle. It still spends a slot retry (D26) |
Three things this changed, each of which was a stated risk:
The kernel config needs no changes. pmOS runs OpenRC and elogind, so the
worry was that their config omits what systemd requires. Audited: DEVTMPFS,
DEVTMPFS_MOUNT, CGROUPS, FHANDLE, INOTIFY_USER, SIGNALFD, TIMERFD,
EPOLL, all four namespaces, SECCOMP+SECCOMP_FILTER, TMPFS_POSIX_ACL,
TMPFS_XATTR, AUTOFS_FS, EXT4_FS — every one set. FHANDLE and
DEVTMPFS_MOUNT are the two that usually bite. No fragment of ours is needed.
The GPU firmware is mostly free. The boot showed
[drm:adreno_request_fw] *ERROR* failed to load qcom/a630_sqe.fw, which reads
like the vendor-blob problem and is not: a630_sqe.fw and a630_gmu.bin are
both in upstream linux-firmware, which image/build.sh already pacstraps.
They were missing only because a pmOS initramfs is minimal. Of the Adreno
firmware only a615_zap.mbn needs the proprietary blob and pil-squasher.
D19 Any USB network gadget moarchy raises on this device is CDC-ECM or
NCM, never RNDIS. pmOS's initramfs comes up as RNDIS (idProduct 0x4EE3,
serial "postmarketOS"), macOS binds no driver, and a phone offering a debug
network that the only machine on the desk cannot speak to is a debug channel
that does not exist. The pinned config already sets USB_CONFIGFS_ECM=y and
USB_CONFIGFS_NCM=y, so this costs nothing but choosing correctly.
Nothing raises one today — the rootfs presents no gadget, so a running phone is invisible over the cable and the shell in D23 is postmarketOS's rather than ours. That is the gap worth closing next, and it belongs in the rootfs as a systemd unit now that there is no initramfs to put it in. ?
Amended 2026-10-19: a debug image built with MOARCHY_SSH_KEY raises one, NCM, as moarchy-usbnet.service (structure.md I5). A published image still raises none.
D15 AVB must be defeated or the Android 12 bootloader rejects an unsigned
kernel. The backend generates an empty vbmeta with the verification-disabled
flag — avbtool make_vbmeta_image --flags 2 --padding_size 4096 — and flashes
it to vbmeta. Verified: a 4096-byte image, Algorithm: NONE, Flags: 2.
D16 The rootfs goes to userdata (p72, the bulk of the 64 GB), not
into the retrofit dynamic partitions. pmbootstrap's default for a device that
overrides neither flash_fastboot_partition_rootfs nor _system is
userdata, and sargo overrides neither. This avoids make-dynpart-mappings
and the logical-partition machinery entirely. ? — it also gives up the
system_a/system_b space, which is a real cost if 64 GB ever gets tight.
D17 Amended 2026-09-14. We flash the current slot and leave the other
alone, so a bad flash is recoverable by switching slots in the bootloader.
Seamless updates are explicitly not a goal; pacman -Syu is the update path.
This AC used to open "A/B slots are not used", and that was wrong in a way that costs a phone. The slots are not optional machinery you can decline to operate — see D26.
D25 The boot image's cmdline ends with init=/sbin/init, and removing
it stops the phone booting. ABL does not pass our cmdline through; it builds
one, putting its own androidboot.* parameters first — init=/init among them
— then ours, then console=null. init= is last-wins in the kernel
(init/main.c, init_setup), so ours must be present to win. An Arch rootfs
has no /init, and a failed init= is a panic() with no fallback to
/sbin/init (init/main.c:1633-1637).
This is what D23 was hiding. Root mounted correctly and the kernel died one
execvelater, on every image tried, looking identical to a kernel that never found its disk. The same rule applies toroot=: ABL passes aroot=PARTUUID=of its own for the Android system partition, and ours wins only by coming after it.
D26 Something must mark the boot successful on every boot, or the phone
stops booting. moarchy-device-sargo depends on qbootctl and ships
qbootctl-mark-successful.service already enabled, by the symlink rather
than through DEVICE_SERVICES, so it is true from the moment the package is
installed rather than from the moment a first-boot script succeeds.
An A/B bootloader decrements a retry counter every time it hands off to a slot and marks the slot unbootable when it reaches zero with no callback. On 2026-09-14 this handset read
slot-retry-count:a:0,slot-unbootable:a:yes, and in that state it booted nothing — a postmarketOS image that had worked an hour earlier failed exactly as ours did, which is how the counter was found rather than the image blamed. About three reboots of headroom, then a phone that needs a computer withfastbootto revive.
flash.shrunsfastboot --set-activefor the same reason: it is the only thing that clears the unbootable flag, and without it a freshly flashed phone can refuse the image just written to it.Observed working 2026-09-15, which is a different claim from "the package is installed": after one boot of an image carrying the unit, the bootloader reports
slot-successful:a:yesandslot-retry-count:a:3. It had readnoat every check before that, and had been counting down — 3 to 1 across the boots of one afternoon.
D27 Added 2026-09-15. Wi-Fi on this SoC is a modem feature. The chain
is rmtfs -s → modem DSP → wlanmdsp.mbn → QMI → ath10k_snoc → wlan0, and
every link of it is shipped: firmware-moarchy-sargo carries mba.mbn and
modem.mbn, and moarchy-device-sargo depends on moarchy-qcom-modem (qrtr,
rmtfs, tqftpserv), whose two units ship enabled by their own symlinks.
The five facts that make this non-obvious, each read out of a source rather than a wiki:
- The Wi-Fi firmware does not run on the Wi-Fi chip. WCN3990's
wlanmdsp.mbnexecutes on the modem DSP as a protection domain;ath10k_snocreaches it over QMI. This package already shipped that blob, which is why the situation looked like a driver problem.- Nothing in the kernel boots that DSP.
qcom_q6v5_mss.csetsrproc->auto_boot = false. The remoteproc sits idle until userspace writesstartto itsstatefile.rmtfsis what writes it. Its-sflag callsrproc_init(), which hunts/sys/class/remoteproc/*for the-mss-pilmodalias. The daemon named for the remote filesystem service is also the ignition — the name tells you nothing about the job that matters here.mba.mbnandmodem.mbnare not in TheMuppets tree. That is a dump of/vendor/firmware; the modem images live in sargo's ownmodempartition. They come from a third upstream, the one postmarketOS uses, pinned inmanifest.tomlasmodem-url/modem-ref.- The protection-domain mapper is the kernel's, and userspace must not run one.
CONFIG_QCOM_PD_MAPPERcompiles ansdm670_domains[]table intodrivers/soc/qcom/qcom_pd_mapper.c— includingmpss_wlan_pd, the exact domainath10klooks up — and binds as an auxiliary driver to a devicepdm_notify_prepare()creates whenever a DSP starts. Shippinglinux-msm/pd-mapperbeside it would put a secondtms/servregserver on QRTR. Alpine stopped packaging it; so did we, after packaging it first. The kernel announces the case for the daemon when it applies: "PDM: no support for the platform, userspace daemon might be required."The decision this reverses is
firmware-moarchy-sargo's own: "the modem pair is NOT shipped … it goes in when the daemons that drive it do." The dependency was right and the reason was wrong — 66 MB ofmodem.mbnwas written off as telephony that nobody had built, when it is what Wi-Fi runs on. Telephony turned out to needq6voiced(D32) and nothing else on this SIM;hexagonrpcdis the sensors daemon and was never part of it.Bluetooth is not part of this chain and never was. WCN3990's BT is a UART controller on
&uart6driven byhci_qca, wantingqca/crbtfw21.tlvandqca/crnv21.bin— both already in the image, inlinux-firmware-atheros, which plainlinux-firmwaredoes pull in (checked withpacman -Fx, not read off the upstream git tree).bluezis installed andmoarchy-firstbootenablesbluetooth. So nothing identifiable is missing, and the next step is a measurement rather than a package.The one named suspect, so the measurement knows what it is looking for:
sdm670-google-common.dtsi'sbluetoothnode has nolocal-bd-address, and the vendor keeps the real BD address outside the filesystem. postmarketOS covers this withbootmac, which every Qualcomm device gets throughsoc-qcom: a shell script on a udev rule that derives a stable locally-administered address (prefix0200) fromandroidboot.serialnoin/proc/cmdline, then applies it withbtmgmt public-addrforhci0andip link set addressforwlan0. Roughlyqbootctl-sized to package.So the measurement is two questions in order: does
hci0exist at all (bluetoothctl list,dmesg | grep -i qca), and if it does, what address does it have —00:00:00:00:00:00or a shared vendor default means bootmac, and nohci0at all means something earlier and unguessed. Writing bootmac before asking would be packaging on a hunch.
D28 Added 2026-09-15, measured on the handset. Bluetooth needs an
address, and nothing else. moarchy-device-sargo depends on bootmac, whose
udev rule sets hci0's public address from the bootloader's
androidboot.serialno when the controller appears.
The failure this closes points nowhere. Every component reported success:
/sys/class/bluetooth/hci0 exists dmesg: QCA Downloading qca/crbtfw21.tlv firmware loads dmesg: QCA setup on UART is completed controller is up rfkill: hci0 Bluetooth soft no hard no not blocked systemctl is-active bluetooth: active daemon running btmgmt info: Index list with 0 items ...and no adapterWCN3990 has no BD address Linux can see and the DT has no
local-bd-address, so bluez classes the adapter unconfigured — and an unconfigured adapter is not in the index listbluetoothctlreads. Nothing in that picture contains the word "address".
btmgmt --index 0 public-addr 02:00:ff:14:0f:1bturned it into a powered Primary controller that discovered ten devices, and after packaging it the same address came back by itself across a reboot. Verified fix, not a plausible one.
bootmac's Wi-Fi half is deliberately not shipped. It runs, logsWLAN MAC address configured successfully, andip -br linkthen shows a different address: NetworkManager applies its own cloned MAC when it activates the connection, after udev. Sopackage()deletes that udev rule. Making the Wi-Fi MAC stable is a NetworkManager setting, not a bootmac one — and it is worth doing, because a phone that takes a new address every boot is a phone you have to hunt for on the LAN. ?
D29 Added 2026-09-15, corrected the same day. Sound needed two files,
not a kernel fix. firmware-moarchy-sargo carries Global_cal.acdb and
alsa-ucm-conf-moarchy-sdm670 carries the use-case profile; with both, the card
registers and PipeWire exposes an earpiece, a speaker and a microphone.
This entry first said the opposite, and the mistake is the useful part. The symptom was four errors deep:
qcom-q6afe aprsvc:service:4:4: AFE set params failed -110 msm8916-wcd-digital-codec 62ec0000.audio-codec: failed to enable mclk -110 platform sound: deferred probe pending: snd-sm8250: ... codec dai not found /proc/asound/cards -> --- no soundcards ---All three of those name a DSP that will not answer, so this file recorded a bring-up bug, "above this project's line (§2)", and moved on. A rebind forced 14 minutes after boot failed identically, which ruled out a boot race and seemed to confirm it.
The actual first line was further up the log and had been filtered out of every search, because none of the greps included the word that mattered:
qcom-q6core aprsvc:service:4:3: Direct firmware load for qcom/sdm670/sargo/Global_cal.acdb failed with error -2 qcom-q6core ...: probe with driver qcom-q6core failed with error -2
q6afesits onq6core, the codec asksq6afefor its MCLK, and the card's DAI link waits for the codec. One missing 23 KB file, four errors of distance, and not one of the downstream three mentions a filename. The lesson is to read the first error rather than the loudest, and to be slower to call something upstream's problem: "the DSP is not answering" was a true description and a false diagnosis.
Global_cal.acdbis audio calibration and lives in LineageOS's device config rather than TheMuppets'/vendor/firmwaredump, under a directory named for the card this phone has —sdm670-intcodec-s4-snd-card. A fourth upstream for one file, pinned inmanifest.tomlasacdb-url/acdb-ref.The second file is the ALSA use-case profile, and without it the card exists and the phone is still silent:
wpctl statusshows no sink and no source, because WirePlumber does not expose a card it has no routing for. Arch'salsa-ucm-confhasQualcomm/sdm845andsc7180and no sdm670. Ours installs only the three sargo files, where postmarketOS's replaces the wholeucm2tree — on Arch that would be two packages owning several hundred identical paths. ALSA finds it throughconf.d/sdm660/Google Pixel 3a.conf, whose name must be the card's name exactly.Measured after both:
0 [G3a]: sdm660 - Google Pixel 3a, sink "Built-in Audio Earpiece (L) and Speaker (R)", source "Built-in Audio Built-in Microphone", a tone played throughpw-playand a 3 s capture that came back 48 kHz stereo at full scale rather than silence.Closed by D32: routing a call through the profile's VoiceCall verb also wants
q6voiced. Calls connect, carry audio, and are what exposed all of this.
D32 Added 2026-09-15. Fixing sound broke calling, and that is the
correct order of events. moarchy-device-sargo depends on q6voiced and
ships the PCM numbers it needs.
Calls connected before D29 and carried no audio. After D29 they went
[modem0/call0] call state changed: unknown -> terminated (unknown)immediately, on a modem that was
registeredono2 - de+at signal 92 withCS: 'attached'. Nothing about the network had changed.The reason is that a voice call on this SoC is not carried by the modem alone. The ADSP exposes a voice PCM —
VoiceMMode1, card 0 device 4 here, visible in/proc/asound/pcm— and something has to hold it open for the duration of a call. Whileqcom-q6corewas failing to probe, the whole q6 stack was dead and the modem did the call by itself: it connected, and there was nowhere for the audio to go. With q6core probing,q6voice,q6mvm,q6cvsandq6cvpare live, the voice path expects a driver, and a dial with nothing on the PCM is torn down at once.So the sequence reads as "the audio change broke calls", and the truth is that it revealed the missing half.
q6voicedis postmarketOS's daemon; it watches ModemManager over D-Bus and drives the PCM.The card and device numbers live in the device package, not in
q6voiced: they are a hardware value (§4). q6voiced's own unit is written for that — itConditionPathExistson the config file, so a device that ships none skips the unit rather than failing it.Also fixed here:
docker/Dockerfile.buildergainedalsa-lib. D20 says the builder carries build tools because--nodepsnever installs makedepends; this is the same rule one step further, because--nodepsdoes not installdependseither, so a library a package links against has to be in the container too.
D33 Added 2026-09-17, measured on the handset. Mobile data is one
packaged file, and NetworkManager refuses it at 0644. The moarchy package
ships /usr/lib/NetworkManager/system-connections/moarchy-mobile-data.nmconnection
at mode 0600, carrying [gsm] auto-config=true and nothing else.
docs/control-center.md S29 is the tile that switches it.
Nothing was missing below the profile, which is why this is a file and not a stack. Read off the phone before anything was written: NetworkManager 1.58.1 already had the modem as a gsm device (
qrtr0, portsqrtr0 (qmi)andrmnet_ipa0 (net)),nmcli radioalready reportedWWAN-HW enabledandWWAN enabled, andmobile-broadband-provider-info 20251101was already in the image — ModemManager pulls it in. So the operator's APN, username and password are all resolvable from the SIM, and there is nothing per-operator to maintain and no APN in the file.The mode is the whole difficulty, and it fails silently. The keyfile reader refuses any profile the group or the world can read, wherever it lives — /usr/lib included, secrets or none:
keyfile: load: ".../moarchy-mobile-data.nmconnection": failed to load connection: File permissions (100644) are insecureThat is one
<warn>in a journal nobody is tailing, and the phone is then indistinguishable from one with no profile at all: a SIM in the tray, bars on the bar, and no data. The first install of this file shipped 0644 and did exactly nothing.image/verify.shasserts the mode for that reason, not the file./usr/lib and not /etc, for three reasons that agree: a package's files belong in a system path (
docs/structure.mdP8, which is also why this is not a line inmoarchy-firstboot),image/verify.shcounts the profiles in/etcto catch a baked-in credential, and NetworkManager copy-on-writes a read-only profile into/etcthe moment anything edits it — same uuid, 0600 root:root — which is what makes the tile'soffoutlive a reboot (S29c).Device-independent on purpose: every phone moarchy targets has a modem that NetworkManager presents as a gsm device, so this sits beside ModemManager's own placement in
moarchy-firstbootrather than in a device package. Only sargo has run it.What is measured, and what is not. Loaded at 0600, the profile appears in
nmcli c show, NetworkManager derives a uuid from the filename, auto-activates it unprompted, and parks the device atconnecting (need authentication)— which is it waiting for the SIM PIN.bin/moarchy-datareads and writes all of that correctly against a locked SIM, and the tile draws it.It carries data, measured end to end 2026-09-17 on a Telefónica Germany SIM (MCC-MNC
26203,o2 - de+), minutes after the PIN was entered and with nothing else done by hand:modem state connected, reg home, PS attached, LTE, signal 81% bearer connected, multiplexed, apn internet.eplus.de, user eplus IPv4 10.132.10.120/28 gw 10.132.10.121 dns 62.109.121.17,.18 IPv6 2a02:3032:1b:3a68::/64, dns 2a02:3018:0:40ff::aaaa,::bbbb route default via 10.132.10.121 dev qmapmux1.0 metric 700 (see below) fetch http=200 in 0.44s bound to the cellular netdev public ip 176.0.20.10 over cellular, against the Wi-Fi v6 address over wlan0The APN was never configured anywhere.
internet.eplus.de,eplusandgprscame out ofmobile-broadband-provider-infoviaauto-config, which is the whole case for shipping one profile with no operator in it.Wi-Fi keeps the default route at metric 600 against cellular's 700, so a phone on both prefers Wi-Fi without anything having to say so.
The netdev is not
rmnet_ipa0, and it has no fixed name. The bearer comes up multiplexed, so the traffic is on a QMAP channel —qmapmuxN.0@rmnet_ipa0— whilermnet_ipa0holds a link-local address and nothing else. Acurl --interface rmnet_ipa0against a working connection fails, which is a good hour to lose.N changes. This phone was
qmapmux1.0on the first connect andqmapmux0.0after one off/on twenty minutes later, with a new address and gateway each time (10.132.10.120/28, then10.132.110.212/29). Anything that needs the interface reads it from the default route or frommmcli -b <bearer>; a hardcodedqmapmux1.0is a check that passes once. Both connects answeredhttp=200over the channel actually in use.The modem re-enumerates on reconnect. One
moarchy-data off; ontook it fromModem/1toModem/0, and NetworkManager had no gsm device at all for several seconds around it — long enough that the tile, bound to "is there a modem now", hid itself immediately after being switched on.docs/control-center.mdS29 latches it;moarchy-data statusanswerssim=rather thansim=missingin that window, because a device NetworkManager has lost says nothing about the tray.
D34 Added 2026-09-19, measured on the handset. The speakers were quiet
in three places at once, and only one of them was a volume control.
alsa-ucm-conf-moarchy-sdm670 enables both CS35L36 boost converters,
moarchy ships moarchy-loudness.service, and moarchy-device-sargo depends
on swh-plugins and carries the filter graph it runs.
Reported as "Spotify is very quiet" while playing through Waydroid. Every stage that has a number was already at its maximum: the PipeWire sink at 100%, the
Waydroidsink-input at 100% and uncorked,Top/Bottom Analog PCM Volume19/19,Digital PCM Volume816/816. A phone at full volume, quiet.1. Android has its own volume, and nothing on this phone can reach it.
STREAM_MUSICwas at 13 of 15 — about -6.5 dB on Android's curve. The volume rocker (default/sway/bindings.conf) and the control center's slider both drive the PipeWire sink, which had no headroom left, so turning the volume up was a no-op that looked like a broken rocker.cmd media_session volume --stream 3 --set 15sets it;media volumeis not a command in this image. This one is not packaged and cannot be: it is state inside the Android container, and it is the first thing to check when Waydroid is quiet (docs/android.md).2. Both speaker amplifiers had their boost converters bypassed. The amps are Cirrus CS35L36s (
cs35l36.2-0040top/earpiece,2-0041bottom), and the device tree provisions a 10 V rail for them —cirrus,boost-ctl-millivolt = 10000, 1 uH, 1800 mA peak. BothBOOST Enable Switchcontrols readoff, so the amps were swinging against VBAT. This was the only hardware gain the phone had left, and it is audible.The cset goes in the Speakers EnableSequence, not
BootSequencewhere a volume default would naturally live, because this device's boot sequences do not take:BootSequencesetsTop Analog PCM Volumeto 17 and it reads 19;FixedBootSequencesets four ramp and zero-cross switches to 1 and all four readoff.EnableSequencedoes take —SEC_TDM_RX_0 Voice Mixer VoiceMMode1readsonand nothing else sets it — and it re-runs on every profile switch.WirePlumber parses the UCM once and caches it. Editing the profile and cycling the card profile re-runs the old sequence, which is indistinguishable from a patch that does not work. Verified the way that could fail instead: forced both controls
off, restartedwireplumber, and watched them come backonby themselves.3. The headroom was going unused, which is what "quiet" actually meant. With the first two fixed, music still reached the speaker at -17.0 dBFS RMS with peaks at -3.6. That gap is the loudness: a small driver only ever sees the peaks, and spends the rest of its time far from the excursion the amplifier could give it. An SC4 compressor into a fast lookahead limiter closes it.
RMS peak as reported -23.7 dBFS -10.0 dBFS STREAM_MUSIC15/15-17.0 dBFS -3.6 dBFS + compressor and limiter -10.8 dBFS -0.5 dBFS, zero clipped Measure the monitor, not the room. Every layer here reports a healthy number while the sound is quiet, so the only honest instrument is
parecordon the sink's.monitorand RMS in dBFS. Two approaches were tried and abandoned first: recording the built-in microphone clips atADC1 Capture Volume63/63, and A/B-ing against whatever music happens to be playing is worthless — the same setting drifted 8.7 dB between two runs, which is larger than the effect being chased. (audioopis gone in Python 3.13; compute RMS fromarrayby hand.)The filter runs as a client, and that is not a style choice. A drop-in on the daemon's own config would mean restarting PipeWire, and a
pipewire-pulserestart drops Waydroid's audio HAL connection — the HAL does not reconnect, so Android goes silent until the container restarts.pipewire -c filter-chain.confin its own unit builds the sink against the running daemon.
priority.session = 2000is load-bearing. The filter sink has to outrank the ALSA sink or a freshly flashed phone, with no stored WirePlumber state, elects the hardware sink and routes nothing through the graph — a chain that is built, correct, and inaudible. Verified by deletingdefault.configured.audio.sinkfrom the WirePlumber state and restarting it.Not measured: loudness in SPL, battery cost of the 10 V rail, and whether the compressor settings hold up on speech or podcasts — they were tuned by ear against music, and they are live-adjustable with
pw-cli s <id> Propsfor exactly that reason.
D30 Added 2026-09-15, fixed the same day. The camera's colour needed a
profile and a patched megapixels. moarchy-device-sargo generates
google,b4s4-sdm670,{Rear,Front}.dcp; pkgbuilds/megapixels carries a
three-line patch without which megapixels cannot load a profile at all.
Why green.
process_pipeline.cfalls back to IDENTITY colour matrices when it finds no profile, and a Bayer mosaic has twice as many green photosites as red or blue, so an uncorrected debayer is green.The profile.
make-dcp.pybuilds both from Google's ownColorMatrix1/2, read out of a Pixel 3a DNG published on raw.pixls.us under CC0 — every DNG the stock camera wrote carries those tags, so this is the vendor's measurement rather than ours. That DNG has noForwardMatrixand megapixels reads them, so both are derived per DNG 1.4 (FM = CA(W→D50) · CM⁻¹ · diag(CM·W), Bradford) and the script asserts the spec's own property —FM · [1,1,1]must be the XYZ of D50 — before writing. Generated, not committed: a.dcpis a TIFF whose payload is nine numbers twice over, and as a binary the only reviewable part would be unreadable.Three bugs in one upstream function,
find_calibration_by_modelinsrc/dcp.c, in 2.1.0 and on master:
- It looks for
<model>.confunderXDG_CONFIG_HOMEwhile hunting for a profile — wrong extension, and it drops the sensor from the name.- That name is libmegapixels' device config. Putting a profile there, the obvious workaround for (1), shadows the file defining the sensors and the media pipeline, and the camera stops starting. Tried here; it cost the camera until the file was deleted.
for (const char *fmt = paths[0]; fmt; fmt++)walks the bytes of the first format string instead of the array, matches.config— a directory in$HOME— and returns it as a calibration file. The tell isFound calibration file at .config.The names came from the fixed binary, not from inference. The PinePhone's shipped profiles are lowercase and had suggested the camera name was lowercased; a patched megapixels says
No calibration found Front, naming the libmegapixels section verbatim. Upstream's own PinePhone profiles are evidently misnamed too. Measuring beat inferring, and inferring cost a round trip.Measured:
Found calibration file at /usr/share/megapixels/config/google,b4s4-sdm670,Front.dcp.The cost, stated plainly.
pkgbuilds/megapixelsis a fork of an Arch package, which §2's amendment says we do not do. It is meant to be temporary: the patch goes upstream, and when it lands this package is deleted. It also obligeddocker/Dockerfile.builderto gain[danctnix], becauselibdngandlibmegapixelslive there and Arch Linux ARM has neither — the builder only; a flashed phone's ownpacman.confstill carries no such stanza (R8a). The image's[moarchy]is first in itspacman.conf, so our megapixels wins over danctnix's deterministically rather than by luck.
D23 There is no console on this device, and there cannot be one. ABL
strips any console= from the boot image and appends its own console=null.
Verified from a shell on the handset: with console=tty0 in the boot image,
grep -o "console=[^ ]*" /proc/cmdline returns console=null alone and
/proc/consoles lists only ttynull0. There is no pstore either — /dev/pmsg0,
/proc/last_kmsg and /sys/fs/pstore/console-ramoops* are all absent — so a
previous boot's log cannot be recovered after the fact.
Nothing printed before userspace is ever visible, so a failing image and a working one look identical: fbcon's penguins, then silence. Do not debug this device by changing something and watching the screen. What works instead:
- USB networking. postmarketOS's initramfs raises a gadget macOS binds; the
phone answers on 172.16.42.1 with a telnet shell on port 23, host at
172.16.42.2.
fastboot boottheir image, mount the rootfs, and read it from there. This is how D25 and D26 were both found. - A getty on tty1 after boot. Once the real root is running its getty writes to the VT and is visible.
- The assertions in
image/verify/android-bootimg.sh, which read the cmdline back out of the artifact. On a device that cannot tell you what went wrong, a check before the flash is worth more than any amount of looking.
D18 Amended 2026-09-19. The GLES 2.0 ceiling left with the device that imposed it. It was never a moarchy fact; it was an Allwinner A64 fact. Every device on the target list has an Adreno and clears GLES 3.2, which is what makes upstream Omarchy's own compositor reachable again.
What the shell targets is therefore a choice from here on, and
docs/style.md owns it: it must state the floor out loud rather than leave it
as an assumption inherited from hardware nobody here still has. A motion budget
sized for a Mali-400 is not wrong on an Adreno, but it is no longer forced, and
the difference has to be written down or the next device silently violates it.
D0 The Android-phone case is the general one. A change that makes
removable media, raw-sector boot, or a whole-disk .img.xz the default shape
of anything is wrong: no device on the target list works that way, and the one
that did is gone.
Restated as a checklist, in build order. Each carries its state.
-
D1/D2/D3 — DONE.
moarchy-device-sargois the only package naming hardware, anddevice.confcarries the codename, the modem services and the grow policy. Nothing else in the tree names a phone. -
D6 — DONE.
config/sway/configincludes/usr/share/moarchy/device/sway.conf;pacman -Qoon the installed path namesmoarchy-device-sargo, and themoarchypackage owns nothing underdevice/.image/verify.sh's existing "every absolute sway include resolves" check covers this in the image without modification. -
D4/D5 — DONE, demonstrated. Against a throwaway second device package: two at once gives
unresolvable package conflicts detected ... are in conflict;moarchy-metawith none givesunable to satisfy dependency 'moarchy-device'. Neither was assumed. -
§4 rows 5-7 — NOT STARTED. Battery and output are probed;
axp20x-batteryandDSI-1appear nowhere in the tree.moarchy-has-keyboard's comment is corrected. -
D8-D12 — DONE, run.
image/build.shsourcesimage/boot/$BACKEND.shand callsbackend_kernel,backend_fstabandbackend_image; it refuses aDEVICEwith no device package and a device with no backend.image/verify.shsplits the same way intoimage/verify/$BACKEND.sh(verify_artifact,verify_grow, and the optionalverify_rootfs), inferring the device from the artifact name. Run on sargo since 2026-09-14. The contract is exercised byimage/boot/test-backends.sh, which is what keeps it a contract now that one backend implements it. -
D13 — DONE.
manifest.tomlcarries[device.sargo]with the kernel tag, real SHA256s and the config's provenance;manifest_getreads all five keys and the existingmanifest_components/manifest_aur_packagesscans are unaffected.linux-moarchy-sdm670andmoarchy-device-sargoboth build. -
D15-D17, D23-D26 — DONE, booted. On 2026-09-14 moarchy came up on the handset from a boot image this repo produced:
linux-moarchy-sdm6707.1.3, no ramdisk,root=PARTLABEL=userdata ro rootwait rootfstype=ext4 init=/sbin/init, throughfastboot bootso that nothing was written while it was still a question. The panel, touch, autologin, sway and the shell all ran; the Adreno needslinux-firmware-qcom, which is whymoarchy-device-sargodepends on it by name (Arch splitslinux-firmwareper vendor and the plain package carries every vendor but Qualcomm).Three of those criteria were found by booting and could not have been found any other way. D25 (
init=/sbin/init) is the one that cost a night: root was mounting correctly the whole time and the kernel panicked oneexecvelater, which on a device with no console (D23) is the same picture. D26 (the A/B retry counter) invalidated part of the measurement that preceded it — onceslot-unbootable:a:yesis set the bootloader boots nothing, so a run of images tested after that point were all "failing" identically for a reason that had nothing to do with any of them. D24 (no initramfs) removed the component all of it was being blamed on.The lesson worth keeping: on this device, a boot that produces no output is not evidence about the thing you changed. Check
fastboot getvar slot-unbootable:afirst, and get a shell — pmOS's initramfs at 172.16.42.1:23 — before forming a theory.Two tools were written rather than depended on, and both are checked against artifacts instead of trusted:
image/boot/android-image.pyreproduces postmarketOS's own boot image byte-for-byte, andpkgbuilds/firmware-moarchy-sargo/pil-squash.pyreproduces the vendor's own unsplita615_zap.elfbyte-for-byte. Neithermkbootimg,avbtoolnorpil-squasheris packaged for Arch; each replacement is ~40 lines with a test that fails when broken.D22 Growth is a per-device policy, not a probe.
DEVICE_GROW=filesystemon sargo, where the rootfs sits inuserdatainside a vendor GPT that also holdsxbl,abl,tzand the A/B slots. Runningsfdiskthere would rewrite a vendor partition table on a phone with no removable storage and no recovery image — the one irreversible thing this project could do to a device.DEVICE_GROW=partitionexisted for a device with removable media and went with it; the key stays because the next device's answer is not knowable from sysfs either. The key meets §4.1's bar because "is it safe to rewrite this table" is a policy about the hardware, not a fact readable from it, andmoarchy-grow-rootfsdefaults to the safe value so a device package that forgets the key costs storage rather than a partition table.D21 The kernel is built on a case-sensitive filesystem, never in a macOS bind mount.
net/netfilter/holds bothxt_TCPMSS.candxt_tcpmss.c; APFS keeps one. The build then dies twelve minutes in withNo rule to make target 'net/netfilter/xt_TCPMSS.o', which reads like a corrupt tarball and is not —tarexited 0 and the sha256 matched, because the file was overwritten rather than dropped.docker/build-packages.shis already safe (it copies/repointo the container first); hand-rolled-v $PWD:/workiteration is not.prepare()now checks for both files and fails in one sentence.D20 The builder image carries the kernel's build tools (
bc,libelf), notlinux-moarchy-sdm670'smakedepends.docker/build-packages.shbuilds every in-repo package with--nodeps, so makedepends are declared and never installed. Omittingbccost a build and presented asinclude/generated/timeconst.h ... Error 127— make's code for "command not found", about a header, four directories from the missing tool. -
D27 — BUILT, NOT MEASURED. The radios. What is true off the device:
moarchy-qcom-modembuilds and installsqrtr,rmtfsandtqftpserv; both daemons linklibqrtr.so.1(readelf -d, asserted inbuild()so a silent unlinked build fails rather than ships); their units name/usr/bin/…rather than/usr/local/bin/…and land inmulti-user.target.wants;firmware-moarchy-sargo0.2.2-2 carries thirteen files includingmba.mbn,modem.mbnand five.jsnmaps, with the two modem blobs checked for\x7fELF+EM_QDSP6so a truncated download fails the build rather than the boot.image/verify/android-bootimg.shasserts every link of the chain in the image, and the Bluetooth firmware beside it.Not yet done, and it is the only claim that matters: no image has been built from this and nothing has run on the handset. "wlan0 exists" and "it associates" are both still unmeasured, and this file should not say otherwise until they are. The thing to read first on the device is
systemctl status rmtfs, thenjournalctl -b | grep -iE 'q6v5|mss|ath10k'— a skippedConditionPathExistsand a failed firmware load look nothing alike and both end as "no Wi-Fi". -
D27–D30 — MEASURED ON THE HANDSET 2026-09-15. The flashed image was booted and every claim below was read off the device rather than inferred.
Works: Wi-Fi (associated, −55 dBm,
rmtfs/tqftpservactive, all three remoteprocsrunning); Bluetooth (hci0a powered Primary controller afterbootmac, ten devices discovered, survives a reboot); camera (live preview, both sensors); vibration (drv2624:haptics, confirmed by hand); NFC (nfc0); battery and charger (qcom-battery,pm660-charger); the Venus decoder (visible to PipeWire); touch, display and GPU.Also works, found after the first pass: the modem registers and calls connect in both directions (
o2 - de+, LTE,CS: 'attached'as well asPS, so this network still offers circuit-switched fallback and needs no IMS); and audio, once D29's two missing files were added — card, earpiece, speaker and microphone all measured.Calls carry audio (D32), measured on a real call after
q6voicedlanded. So on this handset the phone is a phone: Wi-Fi, Bluetooth, camera, vibration, sound, and calls in both directions with voice on them.Built and verified as an image, 2026-09-15:
moarchy-sargo-0.2.2-20260915, 132 checks passing and one failing — the dirty-tree provenance flag, from another session's uncommitted docs. The verify suite now asserts every link of the Wi-Fi chain, the audio chain, bootmac, and both halves of the camera fix.Flashed and booted 2026-09-15.
moarchy-sargo-0.2.2-20260915went onto the handset over fastboot (166 s, slot marked active), left the bootloader and came up. This is the first artifact carrying the packaged form of everything from D27 onward — until now every one of those fixes had only ever been hand-installed onto a running phone — and it was reported working on the device.Measured on that fresh rootfs, after re-authorising a key by hand — a published image has no sshd and no credentials, so getting back in costs a person at the screen:
kernel 7.1.3-sdm670, LSMs lockdown,capability,landlock,yama,bpf pacman `pacman -S tree` installs over the network, sandbox on, no DisableSandbox in pacman.conf Wi-Fi associated, -48 dBm modem rmtfs / tqftpserv / q6voiced all active; cdsp, adsp and 4080000.remoteproc all running Bluetooth Controller 02:00:FF:14:0F:1B — the same serial-derived address as before, unattended, with nobody running btmgmt sound 0 [G3a]: sdm660 — Google Pixel 3a, sink "Earpiece (L) and Speaker (R)" camera Found calibration file at /usr/share/megapixels/config/google,b4s4-sdm670,Front.dcpEvery one of those was a hand-installed file this morning. The Bluetooth address and the camera profile are the two worth singling out: both prove the packaging rather than the fix, because both appear with nothing run by hand at all.
SMS works, both directions, on the flashed image. Measured rather than assumed:
chatty-history.dbholds three rows within three seconds of each other, twodirection -1and onedirection 1, and ModemManager's journal shows/SMS/1consumed and deleted — the app took the message off the modem. So the whole receive path ran: modem → ModemManager → chatty → store. No81voltdand no IMS, because this operator still offers circuit-switched fallback (CS: 'attached'); a VoLTE-only SIM would need §10's items 3 and 4. Message bodies were not read; lengths were enough.Nothing on the sargo feature list is untested any more. The remaining open items are quality rather than absence: camera colour beyond a matrix-only profile (no HueSatMap, no look table), and telephony on a VoLTE-only operator.
One expected behaviour worth not mistaking for a fault: the SIM re-locks on every boot, so the modem reads
lockeduntil someone enters the PIN. That is whatmoarchy.simis for (D28).The Bluetooth AC that the previous revision of this file declined to write is now written, because the measurement it was waiting for has been taken.
-
D34 — MEASURED ON THE HANDSET 2026-09-19; PACKAGED, NOT YET BUILT OR FLASHED. The speaker loudness work. What was measured on the running phone, with the tuned chain live: music reaching the speaker went from -23.7 dBFS RMS / -10.0 peak as reported, to -10.8 dBFS RMS / -0.5 peak with zero clipped samples, across three independent causes — Android's own
STREAM_MUSICat 13/15, both CS35L36 boost converters bypassed, and the crest factor nothing was reclaiming.Each half of the persistence was verified against the case that would fail, not the case that would pass: the boost cset by forcing both controls
offand watching awireplumberrestart bring them backon, and the default-sink election by deletingdefault.configured.audio.sinkfrom the WirePlumber state and confirming the filter sink still won onpriority.session. The packaged fragment was moved to/usr/share/pipewire/filter-chain.conf.d/and the hand-placed copy under~/.configremoved before that test, so the path the package ships to is the path that was measured.Not done: no package has been rebuilt from these PKGBUILDs, so the
sedthat patchesVoiceCall.confhas been proved only against the pristine upstream file outside makepkg, and nothing has been flashed. Until an image carries it, the claim is that the fix works and not that the packaging does — the same distinction AC 9 draws, and the one this project keeps having to redraw.The handset carries hand-placed copies that will collide, and they have to go before these packages are installed on it. Whoever installs them first should remove all three, because each one wins against the package rather than losing to it:
/usr/share/pipewire/filter-chain.conf.d/99-loudness.conf # -> two graphs ~/.config/systemd/user/moarchy-loudness.service # -> shadows /usr/lib ~/.config/systemd/user/pipewire.service.wants/moarchy-loudness.serviceThe first is the worst of them: PipeWire reads every fragment in that directory, so the hand-placed one and the packaged
99-moarchy-loudness.confbuild two chains, both claimingpriority.session = 2000. A user unit also takes precedence over/usr/lib/systemd/user, so the old copy would keep running after an upgrade changed the packaged one.pacmanwill additionally refuse the unowned/usr/sharepath with "exists in filesystem" — compare first, then--overwritescoped to it. The device also still carriesalsa-utils, installed by hand foramixerand deliberately not packaged.
AC 7 was the one that could fail for reasons none of the others predicted, which is why it was last and why nothing above it depended on owning a Pixel 3a. It did fail that way, three times over, and D24–D26 are what came back.
AC 1 is the one most likely to be declared done without being done. The packages building and the metadata being right is not the same as a phone booting, and this file should not say otherwise until one has.
-
Who builds the SDM670 kernel. Answered in part: it builds in the existing
moarchy-builderon the same cached image as everything else, and natively — the build host is aarch64, so there is no emulation penalty. What is still open is whether a kernel belongs in the sameprovision.sh buildas a 3 KB meta package, given it dwarfs everything else in the loop. -
Whether
[moarchy]publishes kernels at all. It could; the repo is ours. But a kernel in the same repo as the shell means a phone'spacman -Syucan replace its kernel, which on a device with no recovery is a different risk profile than replacing a QML file. ? -
Fairphone codename and tier. FP4 and FP5 are both pmOS community, both fastboot; which one, and whether the
android-bootimgbackend covers it unchanged, is unverified. -
Calls, Wi-Fi and Bluetooth — one decision, not three. All three answered and measured on the handset 2026-09-15; see D27, D28, D29, D32. The modem stack is built and shipped:
mba.mbnandmodem.mbnfrom a third upstream, andmoarchy-qcom-modemcarrying qrtr, rmtfs and tqftpserv at three pins.qbootctlwas the precedent and it held — upstream C projects, pinned and packaged, at the price of being the ones who notice when they move. It was four until reading the kernel showed the fourth,pd-mapper, had been replaced byCONFIG_QCOM_PD_MAPPER— which is the cheapest kind of scope cut and the reason D27 quotes sources rather than wikis.It works. Wi-Fi associates, Bluetooth pairs, and a call connects in both directions with voice on it — all read off the device rather than inferred.
Telephony was far closer than this file claimed, and the order it actually went in is worth keeping, because almost none of it was the order predicted.
hexagonrpcdwas never involved — it is the sensors/FastRPC daemon. What it took, in the sequence it took:- A SIM, and its PIN — which is why
moarchy.simexists (D28). — done (D32). It is what holdsq6voicedVoiceMMode1open for the duration of a call; without it a dial is torn down immediately.81voltd— not needed on this SIM, and that was luck rather than design.qmicli --nas-get-serving-systemreportsCS: 'attached'as well asPS, capabilitycs-ps: o2's network still offers circuit-switched fallback, so the modem carries voice without IMS at all. A VoLTE-only operator would need this and the two below, and the next SIM is the thing that decides. (gitlab.com/flamingradian/81voltd, GPL-2.0, in pmaports astemp/81voltd) — a host-side implementation of the QMI IMS Data service. LTE carries no circuit-switched voice, so on a network with no 2G/3G fallback the modem must register with IMS, and it will not until something answers its request for an IMS PDN. Its only deps aremm-glibandlibqrtr— andmoarchy-qcom-modemalready shipslibqrtr, so this is a small package rather than a new stack. Same upstream author as the sargo modem firmware this project pins.- The IMS bearer's netdev, which ModemManager creates and does not
configure. Bringing it up with the bearer's own address is reportedly what
turns an outbound SMS from a 25 s
WmsMessageDeliveryFailureinto a one-second success.
Points 3 and 4 are read from a sibling project's notes on a OnePlus 6T (sdm845), not measured here — recorded as a map, not as fact. Its author reports 81voltd working on a Pixel 3a, which is encouraging and still second-hand. The userspace above all this is already in the image: ModemManager 1.24.2, libqmi 1.38, gnome-calls, chatty, mmsd-tng, callaudiod.
Bluetooth was never part of it, and that is now a finding rather than a guess. Nothing identifiable is missing: the kernel has
BT_HCIUART_QCAandBT_QCA, the DT enables&uart6with aqcom,wcn3990-btchild, the firmware is inlinux-firmware-atheros(whichlinux-firmwarepulls in),bluezis inmoarchy-metaandmoarchy-firstbootenables it. So the next step is to look at the device rather than to write a package — and D27 names the suspect (nolocal-bd-addressin the DT) and the package that would answer it (bootmac, which pmOS gives every Qualcomm device) precisely so that the measurement has something to confirm or rule out. ?The same
bootmacquestion applies to Wi-Fi once it works, and is not a blocker either way: awlan0on a firmware-default MAC associates fine and only becomes a problem when two of these phones meet one network. - A SIM, and its PIN — which is why