Systemd Service Management and Network Topology
The Portal platform runs on an HP EliteBook Ultra G1q (“Spaceboard”) powered by a Snapdragon X Elite SoC. Every component — from the Wayfire compositor to the on-device LLM — is orchestrated through systemd unit files that encode precise startup ordering, dependency binding, crash-recovery policies, and security sandboxing. This page maps the complete service dependency graph, explains the dual-network topology (Wi-Fi AP + STA), and documents the configuration patterns that keep 15+ daemons coordinated on a single ARM64 device.
Service Dependency Graph
Section titled “Service Dependency Graph”The service architecture follows a layered fan-out pattern: a root compositor service (portal.service) anchors the display layer, and all downstream services either bind to its lifetime or sequence after it. Two sysinit-stage oneshot services run before multi-user.target to fix hardware quirks.
graph TD
subgraph "Sysinit Stage"
BINFMT["portal-binfmt-fix.service<br/>(oneshot)"]
IRIS["portal-iris-fixup.service<br/>(oneshot)"]
end
subgraph "Multi-User Target"
PORTAL["portal.service<br/>Wayfire Compositor<br/>Creates /run/portal"]
HEALTH["portal-health-monitor.service"]
NET["portal-network.service<br/>Wi-Fi AP (uap0)"]
WLO1["wlo1-watchdog.service<br/>STA auto-reconnect"]
FILEWATCH["portal-file-watcherd.service"]
WINEMON["portal-wine-monitor.service"]
end
PORTAL -->|"After"| INPUT["portal-input.service<br/>BindsTo=portal.service"]
PORTAL -->|"After"| WM["portal-wm.service"]
PORTAL -->|"After"| STREAM["portal-stream.service<br/>PartOf=portal.service"]
PORTAL -->|"After"| LLM["portal-llm.service"]
PORTAL -->|"After"| DBUS["portal-dbus.service<br/>PartOf=portal.service"]
DBUS -->|"Before"| PCP["portal-pcp.service"]
LLM -->|"After"| VOICE["portal-voice.service"]
PORTAL -->|"After"| VOICE
PORTAL -->|"After"| LAUNCHER["portal-launcher.service<br/>BindsTo + PartOf=portal.service"]
NET -.->|"uap0 AP"| GLASSES["INMO AIR3<br/>Glasses"]
STREAM -.->|"RTP/UDP"| GLASSES
VOICE -.->|"RTP TTS Audio"| GLASSES
style PORTAL fill:#4a90d9,color:#fff
style NET fill:#e87d3e,color:#fff
style GLASSES fill:#50c878,color:#fff
The diagram reveals two critical dependency chains. First, the PCP chain: portal.service → portal-dbus.service → portal-pcp.service, where the D-Bus session bus must be ready before the capability protocol daemon can register its AT-SPI2 adapters. Second, the voice chain: portal.service → portal-llm.service → portal-voice.service, where the GenieX LLM daemon must initialize before the voice pipeline can route fallback queries to it.
Sources: portal/systemd/portal.service#L1-L29, portal/systemd/portal-pcp.service#L1-L25, portal/systemd/portal-voice.service#L1-L27, docs/x-elite-deployment-state.md#L27-L51
Service Catalog
Section titled “Service Catalog”Every Portal service runs as the portal system user (UID 1000) and shares a common XDG_RUNTIME_DIR=/run/portal for Wayland socket access. The table below summarizes all production services, their startup triggers, and restart semantics.
| Service | Binary | Type | Restart Policy | Group | Key Dependency |
|---|---|---|---|---|---|
portal |
/usr/bin/wayfire |
simple | on-failure (5s) | video | After seatd.service |
portal-input |
/usr/local/bin/portal-keyboardd |
simple | always (3s) | portal | BindsTo portal.service |
portal-wm |
/usr/local/bin/portal-wm |
simple | always (3s) | video | After portal.service |
portal-stream |
/usr/local/bin/portal_stream |
simple | always (3s) | video | PartOf portal.service |
portal-voice |
/usr/local/bin/portal-voiced |
simple | always (5s) | audio | After portal-llm.service |
portal-llm |
/usr/local/bin/portal-llmd |
simple | always (5s) | audio | After portal.service |
portal-pcp |
/usr/local/bin/portal-pcpd |
simple | on-failure (3s) | video | After portal-dbus.service |
portal-dbus |
/usr/bin/dbus-daemon |
forking | on-failure (2s) | video | PartOf portal.service |
portal-launcher |
/usr/local/bin/portal-launcherd |
simple | always (3s) | video | BindsTo + PartOf portal.service |
portal-network |
/usr/local/sbin/hostapd |
forking | on-failure (5s) | — | After NetworkManager-wait-online |
portal-health-monitor |
/usr/local/bin/portal-health-monitor |
simple | always (2s) | — | After multi-user.target |
portal-file-watcherd |
/usr/local/bin/portal-file-watcherd |
simple | on-failure (5s) | portal | After network.target |
portal-wine-monitor |
/usr/local/bin/portal-wine-monitor |
simple | on-failure (5s) | portal | After network.target |
portal-binfmt-fix |
shell oneshot | oneshot | — | — | After systemd-binfmt.service |
portal-iris-fixup |
shell oneshot | oneshot | — | — | After systemd-modules-load.service |
wlo1-watchdog |
/usr/local/bin/wlo1-watchdog |
simple | always (5s) | — | After NetworkManager.service |
Sources: portal/systemd/portal-input.service#L1-L45, portal/systemd/portal-launcher.service#L1-L43, portal/systemd/portal-dbus.service#L1-L18, docs/x-elite-deployment-state.md#L27-L51
Dependency Binding Semantics
Section titled “Dependency Binding Semantics”Portal’s services use three distinct systemd dependency directives, each with different lifecycle implications. Understanding the distinction is essential for debugging startup failures and restart cascades.
After= is purely an ordering constraint. The dependent service will not start until the named unit has reached its activation state, but a failure or stop of the named unit does not propagate. For example, portal-wm.service uses After=portal.service — if Wayfire crashes and restarts, the window manager continues running and reconnects to the new socket.
BindsTo= is the strongest coupling. The dependent unit’s lifetime is entirely controlled by the target. If portal.service stops, portal-input.service (which uses BindsTo=portal.service) is stopped too — systemd treats them as a single lifecycle unit. This is appropriate for services that are meaningless without the compositor.
PartOf= propagates stop and restart events from the target to the dependent, but not start events. portal-stream.service uses PartOf=portal.service, meaning that restarting the compositor restarts the stream daemon, but the stream daemon must be independently enabled for boot. portal-launcher.service combines both BindsTo= and PartOf=, giving it the tightest coupling possible.
graph LR
subgraph "After= (ordering only)"
A1["portal.service"] --> A2["portal-wm.service"]
A2 -.->|"survives<br/>restart of A1"| A2
end
subgraph "BindsTo= (lifetime bound)"
B1["portal.service"] --> B2["portal-input.service"]
B1 -.->|"stops with A1"| B2
end
subgraph "PartOf= (stop/restart propagate)"
C1["portal.service"] --> C2["portal-stream.service"]
C1 -.->|"restarts with A1<br/>but not started by it"| C2
end
Sources: portal/systemd/portal-input.service#L4, portal/systemd/portal-launcher.service#L5-L6, portal/systemd/portal-stream.service#L4, portal/systemd/portal-wm.service#L3
The Wayland Socket Bootstrap Problem
Section titled “The Wayland Socket Bootstrap Problem”A critical architectural decision underpins all Portal service files: the use of /run/portal as the shared XDG_RUNTIME_DIR rather than the conventional /run/user/1000. The deployment logs document why: systemd-logind recreates /run/user/1000 when sessions start, deleting Wayfire’s socket and breaking every downstream service that depends on it.
The portal.service unit creates /run/portal in its ExecStartPre phase with explicit ownership, and a tmpfiles configuration ensures the directory persists across reboots:
d /run/portal 0775 portal video -Every downstream service then uses /run/portal as its XDG_RUNTIME_DIR, and most implement a polling guard in ExecStartPre that waits up to 15 seconds for the Wayland socket to appear before starting. This pattern prevents a race condition where a service starts before Wayfire has bound the socket.
Sources: portal/systemd/portal-tmpfiles.conf#L1-L2, portal/systemd/portal.service#L19-L23, portal/systemd/portal-wm.service#L12, docs/x-elite-deployment-state.md#L115-L116
Network Topology: Dual-Interface Design
Section titled “Network Topology: Dual-Interface Design”The Spaceboard operates a dual-interface Wi-Fi topology using the WCN6855 chipset’s concurrent STA+AP capability. One interface (wlo1) connects to an upstream network for SSH access and updates, while a second virtual interface (uap0) hosts a dedicated access point for the AR glasses.
graph TB
subgraph "Spaceboard (X Elite)"
NM["NetworkManager<br/>manages wlo1"]
UAP0["uap0 (virtual AP)<br/>192.168.50.1/24<br/>Unmanaged by NM"]
HOSTAPD["hostapd 2.10 (patched)<br/>5GHz ch36 SSID=Spaceboard"]
DNSMASQ["dnsmasq<br/>DHCP 192.168.50.100-120"]
STREAM["portal-stream.service<br/>RTP/UDP → 192.168.50.255:5000"]
VOICE["portal-voice.service<br/>TTS RTP → glasses IP"]
end
subgraph "External Network"
ROUTER["Upstream Router<br/>(e.g. MyPissefWifi)"]
end
subgraph "Glasses Network"
GLASSES1["INMO AIR3 #1<br/>DHCP 192.168.50.x"]
GLASSES2["INMO AIR3 #2<br/>DHCP 192.168.50.y"]
end
ROUTER <-->|"wlo1 STA"| NM
NM --- PHY0["phy0 (WCN6855)"]
PHY0 ---|"iw phy phy0 interface add"| UAP0
UAP0 --> HOSTAPD
UAP0 --> DNSMASQ
HOSTAPD -.->|"5GHz AP"| GLASSES1
HOSTAPD -.->|"5GHz AP"| GLASSES2
STREAM -.->|"RTP H.265 video"| GLASSES1
STREAM -.->|"RTP H.265 video"| GLASSES2
VOICE -.->|"RTP TTS audio"| GLASSES1
style UAP0 fill:#e87d3e,color:#fff
style HOSTAPD fill:#4a90d9,color:#fff
style GLASSES1 fill:#50c878,color:#fff
style GLASSES2 fill:#50c878,color:#fff
The network separation is enforced at the NetworkManager level. A dedicated configuration file in conf.d/ explicitly marks uap0 as unmanaged, preventing NM from interfering with the AP interface:
[keyfile]unmanaged-devices=interface-name:uap0Additionally, NetworkManager’s main configuration sets dns=none and managed=false for ifupdown devices, preventing the system dnsmasq instance from conflicting with Portal’s instance on port 53.
Sources: portal/network/10-portal-uap0.conf#L1-L3, portal/network/NetworkManager.conf#L1-L10, docs/x-elite-deployment-state.md#L107-L122
Portal Network Service: AP Bring-up Sequence
Section titled “Portal Network Service: AP Bring-up Sequence”The portal-network.service unit is one of the most operationally complex services in the stack. It must create a virtual AP interface from the physical radio, configure IP addressing, launch hostapd, render a dnsmasq configuration from environment templates, and start the DHCP server — all in a specific sequence within ExecStartPre, ExecStart, and ExecStartPost phases.
| Phase | Command | Purpose |
|---|---|---|
| ExecStartPre | iw dev uap0 del |
Remove stale virtual interface |
| ExecStartPre | iw phy phy0 interface add uap0 type __ap |
Create virtual AP interface on phy0 |
| ExecStartPre | ip addr add ${PORTAL_NETWORK_CIDR:-192.168.50.1/24} dev uap0 |
Assign AP IP address |
| ExecStartPre | ip link set uap0 up |
Bring interface up |
| ExecStart | hostapd -B -P /run/portal-hostapd.pid |
Launch AP daemon (forking) |
| ExecStartPost | sleep 2 |
Wait for AP to stabilize |
| ExecStartPost | envsubst < template > dnsmasq-portal.conf |
Render DHCP config from env |
| ExecStartPost | dnsmasq -C /etc/dnsmasq.d/dnsmasq-portal.conf |
Launch DHCP server |
| ExecStop | Kill dnsmasq + hostapd PIDs | Graceful teardown |
| ExecStopPost | ip link set uap0 down; iw dev uap0 del |
Clean up interface |
A critical operational lesson is documented in the deployment state: the StartLimitBurst=3 directive was originally placed in the [Service] section, where systemd silently ignores it. This caused portal-network to restart in an infinite loop every 7 seconds, which crashed the ath11k firmware and caused a full system lockup. The fix was to move it to the [Unit] section where systemd actually honors rate-limiting.
Sources: portal/network/portal-network.service#L1-L37, docs/x-elite-deployment-state.md#L119
Hostapd Configuration: Patched for ath11k
Section titled “Hostapd Configuration: Patched for ath11k”The access point runs on 5GHz channel 36 (802.11a/n) with WPA2-CCMP encryption. The hostapd binary is a custom-patched build of version 2.10 — the Qualcomm ath11k driver pre-registers management frames in a way that causes stock hostapd to fail with EALREADY/EBUSY errors when trying to register its own frame handlers. The patched build tolerates these conditions.
| Parameter | Value | Significance |
|---|---|---|
interface |
uap0 |
Virtual AP interface |
driver |
nl80211 |
Standard netlink driver |
ssid |
Spaceboard |
Network name seen by glasses |
hw_mode |
a |
5GHz band |
channel |
36 |
Non-DFS, widely compatible |
beacon_int |
120 |
120ms beacon interval |
ieee80211n |
1 |
802.11n (HT) enabled |
wpa |
2 |
WPA2 only |
wpa_key_mgmt |
WPA-PSK |
Pre-shared key authentication |
rsn_pairwise |
CCMP |
AES-based encryption |
wmm_enabled |
1 |
QoS for multimedia traffic |
The WPA passphrase (Spaceboard2026) is baked into the hostapd configuration. For production deployments, this file should be treated as sensitive and protected by filesystem permissions.
Sources: portal/network/hostapd-portal.conf#L1-L13, docs/x-elite-deployment-state.md#L113
DHCP and IP Allocation: Template-Driven Configuration
Section titled “DHCP and IP Allocation: Template-Driven Configuration”Portal uses envsubst to render the dnsmasq configuration at service startup time, pulling values from an environment file. This approach allows network parameters to be changed by editing /etc/portal/portal-network.env without modifying the service unit or rebuilding the template.
| Variable | Default | Used By |
|---|---|---|
PORTAL_NETWORK_CIDR |
192.168.50.1/24 |
ip addr add on uap0 |
PORTAL_DHCP_RANGE |
192.168.50.100,192.168.50.120,255.255.255.0,24h |
dnsmasq dhcp-range |
PORTAL_DHCP_GATEWAY |
192.168.50.1 |
dnsmasq DHCP option 3 |
The dnsmasq template binds only to the uap0 interface (via bind-dynamic) and advertises Google DNS (8.8.8.8, 8.8.4.4) as DHCP option 6 to the glasses clients.
Sources: portal/network/dnsmasq-portal.conf.tmpl#L1-L9, portal/network/portal-network.env.example#L1-L22
Environment File Pattern: Configuration Externalization
Section titled “Environment File Pattern: Configuration Externalization”A consistent pattern across all Portal services is the use of EnvironmentFile=- (with the leading hyphen) to load deployment-specific configuration from /etc/portal/. The hyphen tells systemd to not fail if the file is missing, allowing each service to ship with sensible defaults while still supporting per-deployment overrides.
| Service | Environment File | Key Variables |
|---|---|---|
portal-stream |
/etc/portal/portal-stream.env |
PORTAL_STREAM_TARGET (required), STREAM_PORT |
portal-voice |
/etc/portal/portal-voice.env |
PORTAL_GLASSES_IP, PORTAL_VENV_DIR (required) |
portal-llm |
/etc/portal/portal-voice.env |
PORTAL_VENV_DIR, PORTAL_PYTHON_BIN |
portal-network |
/etc/portal/portal-network.env |
PORTAL_NETWORK_CIDR, DHCP range |
portal-pcp |
/etc/default/portal-pcp |
PORTAL_UID |
The v0.2.1 hardening effort introduced a policy that deployment-specific IPs must never be hardcoded into source code or unit files. The portal-stream.service exemplifies this: PORTAL_STREAM_TARGET is required with no fallback, and if the environment file is missing this variable, the streaming binary exits non-zero with a clear error message.
Sources: portal/systemd/portal-stream.service#L10-L24, portal/systemd/portal-stream.env.example#L1-L22, portal/systemd/portal-voice.env.example#L1-L23, portal/systemd/portal-pcp.env#L1-L3
Security Hardening: Service Sandboxing
Section titled “Security Hardening: Service Sandboxing”Portal applies graduated security hardening across its services, with the most aggressive sandboxing on portal-input.service (which has direct device access) and portal-launcher.service (which processes user input). The hardening directives restrict kernel attack surface by eliminating capabilities, syscall access, and namespace creation.
| Directive | portal-input | portal-launcher | portal-llm |
|---|---|---|---|
NoNewPrivileges |
✅ | ✅ | — |
ProtectSystem |
strict | strict | — |
ProtectHome |
true | read-only | — |
PrivateTmp |
✅ | ✅ | — |
MemoryDenyWriteExecute |
✅ | ✅ | — |
SystemCallFilter |
@system-service + mincore | @system-service | — |
RestrictNamespaces |
✅ | ✅ | — |
LockPersonality |
✅ | ✅ | — |
ProtectKernelTunables |
✅ | ✅ | — |
ProtectKernelModules |
✅ | ✅ | — |
ProtectControlGroups |
✅ | ✅ | — |
RestrictAddressFamilies |
AF_UNIX, AF_INET, AF_INET6 | — | — |
CapabilityBoundingSet |
(empty) | — | — |
MemoryMax |
— | 128M | — |
ReadOnlyPaths |
— | — | /usr/share/portal, /home/portal |
The udev rules file 99-portal-input.rules replaces what was previously an ExecStartPre chmod/chgrp on /dev/uinput inside the service. By giving the portal group standing read/write access at boot time, the service runs with zero capabilities (CapabilityBoundingSet= and AmbientCapabilities= both empty) — a significant security improvement documented as Wave 3.A SEC-02.
Sources: portal/systemd/portal-input.service#L24-L41, portal/systemd/portal-launcher.service#L22-L40, portal/systemd/99-portal-input.rules#L1-L6, portal/systemd/portal-llm.service#L20-L21
Health Monitoring and Crash Loop Prevention
Section titled “Health Monitoring and Crash Loop Prevention”The portal-health-monitor.service runs a bash polling loop that watches seven core services for restart storms. It tracks NRestarts counters per service, logs every restart event with journal context, and force-stops any service that exceeds 3 restarts while still in the activating state. This prevents a failing service from consuming CPU in a tight restart loop — the exact failure mode that previously caused ath11k firmware crashes.
The monitor also watches system load average, logging top-CPU processes and memory state when /proc/loadavg exceeds 20.0. All output goes to /var/log/portal-health.log alongside the wlo1 watchdog.
stateDiagram-v2
[*] --> Monitoring: Service starts (5s interval)
Monitoring --> Monitoring: All services healthy
Monitoring --> Logging: Service restart detected
Logging --> Monitoring: Logged restart + journal context
Monitoring --> CrashLoop: NRestarts ≥ 3 AND state = activating
CrashLoop --> Stopped: systemctl stop <service>
Stopped --> Diagnostics: Dump status + journal
Diagnostics --> Monitoring: Continue watching others
Monitoring --> LoadAlert: loadavg ≥ 20
LoadAlert --> Monitoring: Logged top-CPU + memory
Sources: portal/scripts/portal-health-monitor.sh#L1-L51, portal/systemd/portal-health-monitor.service#L1-L15, docs/x-elite-deployment-state.md#L46
Wi-Fi STA Watchdog: ath11k Resilience
Section titled “Wi-Fi STA Watchdog: ath11k Resilience”The wlo1-watchdog.service addresses a well-documented ath11k driver bug: the WCN6855 chipset drops the STA connection under concurrent STA+AP load. The watchdog polls every 10 seconds using nmcli, and when it detects that wlo1 is not in the connected state, it issues an nmcli device connect wlo1 command, waits 5 seconds, and verifies recovery.
This watchdog is essential for remote management: without SSH access via the upstream network, a dropped STA connection would require physical console access to recover. Both the health monitor and the wlo1 watchdog write to the same log file (/var/log/portal-health.log), providing a unified operational timeline.
Sources: portal/scripts/wlo1-watchdog.sh#L1-L31, portal/systemd/wlo1-watchdog.service#L1-L15, docs/x-elite-deployment-state.md#L121
Boot-Stage Hardware Fixes
Section titled “Boot-Stage Hardware Fixes”Two oneshot services run at sysinit.target — before multi-user.target and before any Portal daemon starts — to work around hardware and firmware quirks specific to the Snapdragon X Elite platform.
portal-binfmt-fix.service disables qemu-x86_64 and qemu-i386 binfmt_misc registrations by writing 0 to their /proc/sys/fs/binfmt_misc/ entries. This ensures that box64 (an ARM64-to-x86-64 dynamic recompiler) handles x86 ELF execution instead of the slower qemu-user emulation. This is critical for Wine compatibility: the portal-wine-monitor.service and portal-file-watcherd.service depend on correct binfmt_misc state to transparently launch Windows applications.
portal-iris-fixup.service rebinds the Qualcomm IRIS video encoder driver (aa00000.video-codec) after a 2-second delay. The initial kernel module load picks up the wrong firmware variant; unbinding and rebinding forces the driver to reload with the correct X1E80100 HP-signed firmware. Without this fixup, portal-stream.service cannot use hardware H.265 encoding via /dev/video1.
Sources: portal/systemd/portal-binfmt-fix.service#L1-L13, portal/systemd/portal-iris-fixup.service#L1-L13, docs/x-elite-deployment-state.md#L43-L48
Boot Prerequisites and Target Configuration
Section titled “Boot Prerequisites and Target Configuration”The Spaceboard is configured for headless operation with multi-user.target as the default. Several system-level preparations are required for the full service stack to boot cleanly:
| Prerequisite | Command | Reason |
|---|---|---|
| Default target | systemctl set-default multi-user.target |
Headless boot (no display manager) |
| Mask getty | systemctl mask getty@tty1 |
Prevent VT conflict with Wayfire DRM |
| Mask system dnsmasq | systemctl mask dnsmasq |
Avoid port 53 conflict with portal-network |
| NM dns=none | /etc/NetworkManager/NetworkManager.conf |
Prevent DNS port conflict |
| NM uap0 unmanaged | /etc/NetworkManager/conf.d/10-portal-uap0.conf |
NM must not control AP interface |
| /run/portal exists | Created by portal.service ExecStartPre + tmpfiles |
Shared XDG_RUNTIME_DIR |
| portal-network starts after NM online | After=NetworkManager-wait-online.service |
PHY must be ready for virtual interface |
Sources: docs/x-elite-deployment-state.md#L179-L186, portal/network/NetworkManager.conf#L1-L10, portal/network/portal-network.service#L3-L4
Configuration File Mapping
Section titled “Configuration File Mapping”The deployment follows a clear separation between repository sources and deployed paths on the Spaceboard. Understanding this mapping is essential for applying configuration changes.
| Repository Path | Deployed Path | Purpose |
|---|---|---|
portal/systemd/*.service |
/etc/systemd/system/ |
Service unit files |
portal/systemd/99-portal-input.rules |
/etc/udev/rules.d/ |
uinput device permissions |
portal/systemd/99-portal-tty.rules |
/etc/udev/rules.d/ |
TTY device permissions |
portal/systemd/portal-tmpfiles.conf |
/etc/tmpfiles.d/ |
Runtime directory creation |
portal/network/hostapd-portal.conf |
/etc/hostapd/hostapd-portal.conf |
AP configuration |
portal/network/dnsmasq-portal.conf.tmpl |
/etc/portal/templates/ |
DHCP template |
portal/network/NetworkManager.conf |
/etc/NetworkManager/NetworkManager.conf |
NM global config |
portal/network/10-portal-uap0.conf |
/etc/NetworkManager/conf.d/ |
NM unmanaged interface |
portal/compositor/wayfire.ini |
/etc/portal/wayfire.ini |
Wayfire compositor config |
portal/systemd/portal-stream.env.example |
/etc/portal/portal-stream.env |
Stream env (copy & edit) |
portal/systemd/portal-voice.env.example |
/etc/portal/portal-voice.env |
Voice env (copy & edit) |
portal/network/portal-network.env.example |
/etc/portal/portal-network.env |
Network env (copy & edit) |
Sources: docs/x-elite-deployment-state.md#L126-L136, portal/systemd/portal-stream.env.example#L1-L22
ALSA-Only Mode: Development Without Glasses
Section titled “ALSA-Only Mode: Development Without Glasses”For development and debugging without the INMO AIR3 glasses, Portal ships an alternative voice service file (portal-voice.service.alsa) that replaces the UDP network audio capture with local ALSA input. Swapping between modes is a two-step process: copy the desired service file into place and restart.
| Mode | Service File | Capture Source | TTS Output | Glasses Dependency |
|---|---|---|---|---|
| Network (production) | portal-voice.service |
--capture-mode udp |
RTP to glasses IP | Required |
| ALSA (development) | portal-voice.service.alsa |
--capture-mode alsa --alsa-device plughw:2,0 |
Local playback | None |
The ALSA variant includes the same security hardening as the production service (NoNewPrivileges, ProtectSystem=strict, MemoryDenyWriteExecute) but restricts ReadWritePaths to /dev/snd only, and adds ProtectHome=read-only since no glasses IP needs to be resolved.
Sources: portal/systemd/portal-voice.service.alsa#L1-L39, portal/systemd/portal-voice.service#L1-L27
Operational Quick Reference
Section titled “Operational Quick Reference”The following commands cover the most common operational tasks when managing Portal services on the Spaceboard:
# Check all Portal services at oncesystemctl status portal* portal-network wlo1-watchdog
# View the dependency tree from portal.servicesystemctl list-dependencies portal.service
# Restart the entire display stack (compositor + bound services)sudo systemctl restart portal.service
# Reload environment files after editingsudo systemctl daemon-reloadsudo systemctl restart portal-stream portal-voice portal-llm
# View health monitor logtail -f /var/log/portal-health.log
# Verify Wayland socket existstest -S /run/portal/wayland-1 && echo "OK" || echo "MISSING"
# Check which services are bound to portal.servicesystemctl show portal-input -p BindsTosystemctl show portal-launcher -p BindsTo,PartOfSources: docs/x-elite-deployment-state.md#L138-L151, portal/scripts/portal-health-monitor.sh#L1-L51
Related Pages
Section titled “Related Pages”- Security Hardening: AppArmor, Stream Authentication, and Supply Chain Audits — Covers the AppArmor profile that restricts x86/.exe execution and the HMAC-SHA256 stream authentication mechanism
- End-to-End Streaming: DMA-BUF to H.265 Hardware Encode to RTP/UDP — Details the streaming pipeline that
portal-stream.serviceorchestrates - Window Manager Daemon: Wayland Toplevel Tracking and IPC Protocol — Describes the
portal-wm.servicedaemon architecture - Portal Capability Protocol (PCP): Architecture, Registry, and Daemon — Explains the
portal-pcp.serviceandportal-dbus.servicedependency chain - ARM64 Sandbox Environment Setup — Setting up a development environment for testing these services locally