Device power control¶
Parking a catalogued device tells the kernel to drop its interfaces and lets its USB port suspend. It is the reversible equivalent of unplugging something you are not using right now — a GPS receiver that draws power on battery whether anything is reading it or not, a device you want off until the next session. This page covers what it does to a machine, per CLAUDE.md's rule that every system modification says what changes, why, how to inspect it afterwards, and how to reverse it.
What follows is a mix of two kinds of claim, and they are kept visibly
apart. What the engine writes to sysfs, and reads back to confirm, is
measured — it is exercised by the test suite on every run. One device has
also been run on hardware: the field laptop's USB GPS receiver was parked
and woken from the tray on 2026-09-27 (bench session 10 in
docs/reference/bench-verification-5430.md), and what that run saw is stated
below as measured. Nothing else has: no modem, Bluetooth controller or
camera has been parked, no park has been run from the CLI or a menu entry on
hardware, and no reboot or replug with a device kept off has been run. For
every claim below about a device other than that GPS receiver, and for the
claims the run did not cover, treat it as an expectation from reading the
kernel's own sysfs documentation, not a measurement, the same way each
class's power_control.note hedges it.
What parking is, and what it is not¶
Parking writes 0 to a device's own authorized file under
/sys/bus/usb/devices/<address>/ and auto to its power/control. The
kernel drops every interface the device presented, the same as an ordinary
unplug — but the sysfs node itself stays: authorized is a file on the
device, not on any one interface, so it is still there to read and to
write back to 1, which is the whole mechanism wake and
hammunition hardware state depend on. If the node disappeared along with
the interfaces, state could never report a device as parked and wake
would have nothing to write to — parking a device does not remove it from
the bus, it deauthorizes it on the bus.
What a parked device looks like elsewhere, measured on the GPS receiver
(session 10): lsusb still lists it — deauthorising drops its
interfaces, not its USB entry — while gpsd's gpsdctl@ttyACM0 removed it
from the running gpsd within a second and /dev/ttyACM0 was gone. Waking
writes 1 back to authorized; the kernel logged authorized to connect
and re-created ttyACM0 and /dev/gps0 within a second, and gpsd took the
receiver back with no action from the operator. How a modem, a Bluetooth
controller or a camera looks while parked is not measured.
power/control is left alone on wake, deliberately — restoring it to on
would undo a runtime power-management setting a udev rule or the operator
already owns.
It is not a low-power mode the device itself enters and not a driver
unload — a park is a statement about the port, not the hardware. /sys
itself is still the only record of whether a device is parked right now:
hammunition hardware state reads the live answer back from the bus on
every call rather than trusting a cache that could go stale (D-056).
What changed since D-056 was first decided: park now also records, by
default, that the operator asked for this device to stay off — one udev
rule per device, in a file the engine owns — so that when the device is next
added, at boot or replugged into the same port, udev parks it again instead
of it waking on its own. That is the design; neither the reboot nor the
replug has been measured on hardware yet. That record is intent, never a
second copy of the live answer: it does not override what state reports,
and a device authorised by hand while its rule still exists shows up as
awake, kept rather than the file winning the argument. hammunition
hardware park --until-reboot NAME skips writing the rule and gets you the
original behaviour above — parked now, and a reboot wakes it — and removes
any kept entry the device already had, so an earlier park does not re-park
it at boot. See "Kept off
across reboots" below, and the 2026-09-28 amendment to D-056 for why the
design changed and what it does not yet answer.
Two things the catalog can schema-validate but that are refused at runtime until hardware proves them:
pci_runtime— the power-control method for an MHI/PCIe card such as a WWAN modem, as opposed tousb_deauthorize's USB path. One manifest uses it,dell-dw5930e, so the gap, its reason and its route (the radio switch) are written down; the hardware report reads the USB bus only, so that entry is never offered for parking. When a card that needs it is bench-verified, it ships.networkmanager_autoconnect— a "quiet verb" meant to stop NetworkManager racing to reconnect a WWAN modem's interface the instant it reappears on wake. It ships refused because the honest implementation binds it to the device's own interface, andParkablecarries no interface — a USB GPS receiver has none to bind to in the first place. The only device that would actually need this verb is a WWAN modem, and that method is itself refused above. An earlier draft implemented it by listing every NetworkManager profile on the machine and forcingautoconnect yeson all of them on wake, including ones the operator had deliberately set tono— a park/wake cycle would silently undo an unrelated setting. That is worse than doing nothing, so nothing is what it does until the filter can be built honestly.
Neither refusal blocks anything else: a device that declares pci_runtime or
a non-empty quiet list is simply refused with a reason the moment you try
to park it, the same way any other unbuilt capability in this project is
carried as a documented gap rather than dropped.
Which devices are parkable, and why the catalog decides that¶
A device is parkable when, and only when, its catalog entry — a device or a
device class — carries a power_control block. There is no flag that
makes an arbitrary attached device parkable and no "parkable by default":
parking a rig cable mid-QSO because it happened to match a generic USB-serial
identifier is not a thing this project wants anyone to discover by accident.
The manifest author has to state, per device, that parking is safe and what an
operator should expect while it is parked.
Four classes are parkable, all by usb_deauthorize:
gps-receiver (the one measured on the bench),
and, added with the 2026-10-02 amendment to D-056 and not yet run on any
device, wwan-modem,
bluetooth-controller and
camera. The field laptop's own entries for them are
dell-dw5821e,
intel-ax210-bluetooth and
sunplus-integrated-webcam-fhd, their
identifiers read-only from lsusb and udevadm and none of them marked
maintainer-verified. The DW5930e, a PCIe/MHI card, is carried as the
documented gap: dell-dw5930e.
What a park of any of the three new classes is, and is not. A parked USB
device is unconfigured, not unpowered at the port: the kernel drops its
interfaces and the port may suspend, and the supply is not cut. For the
radios there is a lighter switch that detaches nothing and needs no root in the
active session: nmcli radio wwan off and bluetoothctl power off, which
the helper's planned radio off wwan and radio off bluetooth verbs and the
tray's Radios toggles wrap (hammunition-tray 0.5.0; exercised against fakes only, never against a real NetworkManager or BlueZ). A camera has no such switch, and a park of it is
the kernel's view and not a hardware privacy guarantee.
The gps-receiver power_control.note is the model for how an unmeasured
expectation is written into a manifest — it says plainly that gpsd's own
hot-unplug handling means nothing further needs quieting. Every claim about
what the new classes look like while parked is an expectation, not a
measurement, until docs/reference/bench-verification-5430.md records a run.
A device is only ever offered for parking while it is actually attached.
hammunition hardware state, the CLI verbs, and the generated menu entries
all read the USB bus fresh on every call — never a cached list — because
between one check and the next a device can be unplugged, or a USB address
reused by something else entirely.
The two files hardware apply installs¶
Parking and waking write to sysfs as root, and everything that can ask for
that write is unprivileged: the CLI, a generated menu entry, and the Plasma
applet in hammunition-tray. hammunition hardware apply installs the one
thing that bridges the two, behind one polkit action, so there is a single
privileged path to review rather than three.
| File | Path | Mode | What it is |
|---|---|---|---|
| The helper wrapper | /usr/local/libexec/hammunition-devctl |
0755, owned by root |
A small POSIX shell script |
| The polkit action | /usr/share/polkit-1/actions/com.chiefgyk3d.hammunition.devctl.policy |
0644, owned by root |
XML, one <action> |
The helper wrapper is a fixed path under the shared prefix — polkit annotates an absolute executable path, and a venv's own path changes across an upgrade, so the wrapper is the stable thing the policy names. Its content is short enough to read in full:
#!/bin/sh
# Installed by `hammunition hardware apply` (D-056). Do not edit: the
# polkit action at /usr/share/polkit-1/actions/com.chiefgyk3d.hammunition.devctl.policy
# authorises this exact path, and the next apply rewrites this file.
cd /
exec /path/to/your/interpreter -I -m hammunition.cli.devctl "$@"
The interpreter path is whichever Python ran hardware apply — normally the
venv's own .venv/bin/python, since that is the documented install
(docs/getting-started/install.md). It is disclosed in the plan before it is
ever written, and re-baked every time apply runs, so reinstalling the
engine into a new venv updates what root actually executes.
The -I is load-bearing, not tidiness. python -m <pkg> inserts
os.getcwd() at sys.path[0]. pkexec normally masks that by chdir()-ing
to the target user's home before it execs the authorised program — but
pkexec --keep-cwd does not, and the polkit action above pins an executable
path, not an argument list, so nothing stops a caller from adding that flag.
Without -I, a local user with an active session could cd to a directory
holding their own src/hammunition/cli/devctl.py, run pkexec --keep-cwd
/usr/local/libexec/hammunition-devctl state, authenticate with their own
password (auth_self_keep), and have their module imported and run as root
instead of the real one. -I drops sys.path[0], PYTHONPATH, PYTHONHOME
and user site-packages while still resolving hammunition from the
interpreter's own venv, so the hijack import fails instead of succeeding. The
cd / above it is defence in depth on top of that.
The polkit action authorises exactly that one path, for exactly the
com.chiefgyk3d.hammunition.devctl action id, nothing wider:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<vendor>Hammunition</vendor>
<vendor_url>https://github.com/Renegade-Penguin/Hammunition</vendor_url>
<action id="com.chiefgyk3d.hammunition.devctl">
<description>Park or wake a radio device, set the clock's time source, control a system service Hammunition manages, or keep your services running after you log out</description>
<message>Authentication is required to change a radio device's power state, the clock's time source, a system service Hammunition manages, or whether your services keep running after you log out</message>
<icon_name>preferences-system-power</icon_name>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_self_keep</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/libexec/hammunition-devctl</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
</action>
</policyconfig>
The same action authorises hammunition-devctl time mode (D-058), the linger
verb and, since the helper moved to hammunition-tray, services changes to a
system service; the wording above is hammunition-tray's, byte for byte, and the
next hammunition hardware apply reinstalls it where no tray helper answers.
Parking a GPS receiver also turns GPS time off, whatever the time mode: ntpd
stops hearing the receiver and follows the network, or holds over, until it
is woken, with no configuration rewritten (inferred from ntpd's reachability
rules; not yet watched on the bench). See docs/guides/gps-time.md.
allow_active=auth_self_keep means an active local session authenticates
once and stays authorised for a few minutes afterwards — polkit's own manual
page documents the interval as "a brief period (e.g. five minutes)" without
committing to an exact number, so read it as "a few minutes", not as "the
session". A park followed by a wake a few minutes later is one password; one
at breakfast and the next at lunch is two. It is still the shape a tray
switch needs, because one that demands a password on every flip is a
switch nobody uses — it just does not remove the prompt for good. A remote
or inactive session (SSH, a login on another virtual terminal) always needs
auth_admin: parking someone else's device over SSH is not a thing a single
password prompt should make easy.
hardware apply is idempotent here exactly as it is for the udev rules: if
both files already match what it would write, it reports that and does
nothing. Before writing either one, it checks who could tamper with what
root is about to run — the interpreter path and the hammunition package
directory it imports from, both as given and resolved through any symlink.
Two things make it refuse outright, before anything is written: either
component being writable by more than its own owner (world-writable, or
group-writable by a group someone else can hold, so another account could
then replace what root runs), or either
component failing to stat at all, which is treated as unsafe rather than
assumed safe. A component that is merely owned by one non-root account — the
ordinary shape of a venv under $HOME — is not refused; apply discloses it
and asks Proceed? [yes/no]: once, a question --yes cannot answer. Group
write by your own user-private group — the group named after you that nobody
else is in, which a 0002 umask makes the default on Parrot 7 — counts as
owner-only and gets the same question. See D-056 and its 2026-09-27
amendments for the reasoning behind the distinction.
The verbs and their exit codes¶
hammunition hardware park NAME [--until-reboot] [--dry-run]¶
Detaches the named device and lets its port suspend, and — by default —
keeps it parked across reboots (see "Kept off across reboots" below).
NAME is the catalog name (gps-receiver), or NAME@ADDRESS
(gps-receiver@1-4) when two of the same kind are attached and the plain
name would be a guess. --until-reboot parks the device without adding the
kept entry, and removes the device's kept entry if an earlier park wrote
one, so a reboot alone wakes it. --dry-run prints the writes,
whether a kept entry is added, and the pkexec call it would make, then
stops — nothing is executed.
| Exit code | Meaning |
|---|---|
| 0 | Parked and verified, or a --dry-run that printed the plan |
| 1 | The helper ran but a write did not verify, or the command otherwise failed to run |
| 2 | Unplannable: the helper is not installed, pkexec is not on PATH, or NAME does not resolve to a parkable attached device |
| 3 | The authentication prompt was declined or denied; nothing was changed |
hammunition hardware wake NAME [--dry-run]¶
The reverse of park, same NAME syntax, same flags, same exit codes. Also
removes the device's kept-off entry, if it has one, so a reboot after wake
does not park it again. wake NAME@ADDRESS also works for a device that is
not currently attached, to clear a stale kept entry for something already
unplugged and put away — see "Kept off across reboots" below. A device
parked with --until-reboot has no kept entry to remove; a reboot alone
already wakes it.
hammunition hardware state¶
Read-only, needs no privilege: lists every catalogued device that is both
attached now and parkable, and whether each one is parked, plus any device
kept parked whose entry names a port nothing is attached to right now.
Always exits 0 — it is a report, and an empty report ("no parkable device
is attached") is not a failure.
hammunition hardware unapply [--dry-run] [--yes] [--user NAME]¶
Removes the helper wrapper and the polkit action, and — since D-056's
2026-09-28 amendment — the kept-off rules file too, if it exists. It is not
part of uninstall and is invoked separately for a reason recorded in
D-056: uninstall resolves the names it is given against the package
and profile catalogs, and there is no unit named hardware to give it.
unapply removes exactly what the transaction log records hardware apply
put there for the given operator — never a path it merely expects to exist,
and never anything the log names that is not the helper or the policy path,
even if a hand-edited log claimed otherwise — plus the kept-off rules file,
which is not something apply installs but park writes; removing it
reloads udev, so every device it was holding parked wakes from the next boot
on. It deliberately never touches the device-access udev rules file.
Those rules (65-hammunition.rules) are declarative, harmless for a device
that is not attached, and removing them would take away device access you
are still using — power control is the reversible part of this feature;
device permissions are not.
It also takes back GeoClue's two files for the GPS tether's socket
(D-069, written by hardware apply where GeoClue is installed), by
content: each file only when it starts with Hammunition's header, then the
socket, rmdir /run/hammunition-gps and systemctl try-restart geoclue.
docs/guides/offline-navigation.md §17 describes them.
| Exit code | Meaning |
|---|---|
| 0 | Removed and verified, nothing recorded to remove, everything already absent, a --dry-run, or the operator declined the confirmation prompt |
| 1 | The operator could not be determined, a removal command failed, or a file the run tried to remove is still present afterwards |
Kept off across reboots¶
By default, park does more than the sysfs write above: it also adds two
lines to /etc/udev/rules.d/66-hammunition-kept.rules — a # kept: NAME
comment, then a rule naming the device's exact port and its vendor/product
pair. udev applies that rule when the device is added — at boot, and on an
unplug and replug into the same port — so udev itself writes
authorized=0, with nothing of Hammunition's running to do it. A
suspend/resume is not claimed: a resume is normally not a udev add event,
so the rule has no reason to fire then, and nothing here has measured it.
Nor has the reboot been measured yet. hammunition hardware park
--until-reboot NAME skips that: the device parks now, exactly as described
above, any kept entry an earlier park wrote for it is removed, and a reboot
wakes it, because nothing is left to reapply.
Whether the rule actually beats every consumer to the device — whether a tty
node like /dev/ttyACM0 never appears at all across a reboot, or appears
briefly before the rule reasserts authorized=0 — has not been measured
against real hardware yet; that is docs/reference/bench-verification-5430.md's
job (this design's Task 7), not a claim this page makes ahead of it. See the
2026-09-28 amendment to D-056.
Inspecting it.
$ cat /etc/udev/rules.d/66-hammunition-kept.rules
# Written by hammunition-devctl (D-056): devices kept parked across reboots.
# Change it with `hammunition hardware park` and `wake`, not by hand.
# kept: gps-receiver
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", KERNEL=="3-5.1", ATTR{idVendor}=="1546", ATTR{idProduct}=="01a9", ATTR{authorized}="0"
hammunition hardware state reports the same thing alongside the live sysfs
answer — a kept field next to parked — including a kept device that is
currently unplugged, shown with attached: false.
Removing one. hammunition hardware wake NAME removes the kept entry for
an attached device and wakes it in the same step. wake NAME@ADDRESS also
works for a kept device that is not attached — resolved from the port and
address the rules file itself names — so a stale line for something already
unplugged and put away can be cleared without plugging it back in first.
Removing all of them. hammunition hardware unapply deletes
66-hammunition-kept.rules along with the power-control helper and its
polkit action, and reloads udev, so every device it was holding parked wakes
from the next boot on (see above). The device-access rules file
(65-hammunition.rules) is untouched, as always.
Moving a kept device to another port brings it back on. The rule names
the port (KERNEL=="3-5.1") and the model together; the same device plugged
into a different port is, as far as the rule is concerned, a line that does
not exist yet, so it comes up awake there. This errs toward not losing access
to a device over a cable swap, at the cost of the kept state not following
the device — re-park it in its new port if you still want it off, and clear
the old line with wake NAME@<old-address>.
A line the engine did not write makes it refuse the whole file. The file
is rewritten whole on every change, never appended to. If it finds a line
that is not exactly a # kept: NAME comment followed by the fixed rule shape
park/wake generate, it refuses to touch the file at all, naming the line
and the file, rather than discard whatever put that line there. Move the
foreign line to a file of its own and try again.
The helper is moving to hammunition-tray (D-056, amended 2026-10-02)¶
The helper's code is leaving this engine for
hammunition-tray, which
owns everything device-shaped. Three things follow, all already in this
engine:
- Two more root files. The helper no longer imports the engine's catalog:
hardware applywrites/etc/hammunition/devctl-devices.yaml(the devices with apower_controlblock) and/etc/hammunition/devctl-services.yaml(gpsd,time,gps-resume), root-owned0644, disclosed whole in the plan, read back afterwards and removed byunapplyby header. Their shapes, and why they exist, are in the helper's lists. Inspect them withcat, and what the helper makes of them withhammunition servicesandhammunition hardware state. - A hand-over. Where the helper installed at
/usr/local/libexec/hammunition-devctlanswers--version, it is the tray's, andhardware applywrites neither its wrapper nor an existing polkit action, andhardware unapplyleaves both. Where nothing answers, the engine's own copy, described above, is still written. It is removed in a later release. - The tray units install it.
hammunition install hammunition-tray(orhammunition-tray-qt) pins hammunition-tray v0.5.0's source archive, which carries the helper and publishes no.debyet, and installs the helper from it, as root, exactly as that repository'sinstall.sh --helper-only --interpreterdoes: the code under/usr/local/lib/hammunition-devctl(copied; root never runs the unpacked tree), the wrapper at/usr/local/libexec/hammunition-devctlrunning the engine's own venv Python as root, and the polkit action. The plan prints every one of those files and the interpreter before anything is written; it asks oneyesthat--yescannot answer when that venv belongs to a single account, and refuses when any account can write it. A helper already answering--version(thehammunition-devctl.deb, the tray's owninstall.sh) is left alone and the plan says whose it is; one this engine installed earlier is refreshed to this pin; a file a package owns is never written over (the plan refuses and names the package, so a machine with the 0.4.0.debrunssudo apt-get remove hammunition-trayonce first). Check it afterwards with/usr/local/libexec/hammunition-devctl --version, which printshammunition-devctl contract 1.hammunition uninstall hammunition-trayremoves the helper only if this engine installed it and no other tray unit is still installed. Not yet measured: an install as root on a real machine.
How to inspect it afterwards¶
cat /sys/bus/usb/devices/<address>/authorized— the ground truth for one device:0means parked,1means not. This is the exact filepark/wakewrite and read back to confirm their own effect (D-031), so in the moment either command ran it agrees with what it reported — asked again later, the address may now name a different device entirely if something was unplugged and replugged in between, which is exactly why the verbs re-resolveNAMEfresh from the bus on every call rather than trusting an address from a previous run.hammunition hardware state— the read-only, no-privilege summary of what is parkable and what is parked right now, reading the same file above for every catalogued, attached device in one pass.pkaction --action-id com.chiefgyk3d.hammunition.devctl --verbose— prints the polkit action as the system currently sees it: theallow_active/allow_inactive/allow_anydefaults above, and confirms the action is actually registered (anapplythat was never run, or one whose policy file failed verification, leaves this command reporting nothing for that action id).lsusb— a parked device is still listed (measured on the GPS receiver, bench session 10: deauthorising drops its interfaces, not its USB entry), solsusbcannot tell you whether something is parked. Readauthorizedfor that, the file the engine itself trusts; the missing/dev/ttyACM0or/dev/gps0is the visible sign for a serial device.
How to reverse it¶
hammunition hardware wake NAMEbrings one parked device back, and removes its kept entry (if any) so a later reboot does not park it again.- A reboot wakes a device parked with
--until-reboot. A device kept parked (the default since D-056's 2026-09-28 amendment) is meant to come back parked instead — that is the point of keeping it, and it has not yet been measured on hardware — sowake NAMEorhammunition hardware unapplyfirst if you want everything awake before rebooting. See "Kept off across reboots" above. hammunition hardware unapplyremoves the helper and the polkit action themselves, and also deletes the kept-off rules file so every device it was holding parked wakes from the next boot on. No CLI verb, menu entry or tray switch can park or wake anything on this machine untilhammunition hardware applyreinstalls the helper. It does not wake a device that is currently parked in sysfs right now — do that withwakeor a reboot first if you want a clean state immediately. It also removes the GPS receiver's resume step ("After suspend" below), and the helper's two lists (above). A helper that has become hammunition-tray's is left alone, with its polkit action, and the run names the logged paths it left.
Removing the authentication prompt for the active session (optional, never installed by us)¶
allow_active=auth_self_keep already means one password covers a park and a
wake done a few minutes apart, but the grant lapses well before a session
does — a flip at breakfast and another at lunch are two separate prompts. If
even the recurring prompt is unwelcome — for a single-user field laptop
where the active session is the operator — polkit supports a local
authorization rule that grants the action to the active session with no
prompt at all, for as long as that session stays active. Hammunition never
installs this file. It is root-owned
policy that widens what an unattended process can do without a password, and
this project's own rule is that a security posture like that is the
operator's decision alone, made in full, not something an install script
nudges toward with a default. If you want it, create it yourself:
// /etc/polkit-1/rules.d/49-hammunition-devctl.rules
polkit.addRule(function(action, subject) {
if (action.id == "com.chiefgyk3d.hammunition.devctl" &&
subject.active && subject.local) {
return polkit.Result.YES;
}
});
Save it at /etc/polkit-1/rules.d/49-hammunition-devctl.rules, mode 0644,
owned by root; polkitd picks up rules files automatically, no reload
command needed. subject.active && subject.local is the same pair of
conditions the shipped action's auth_self_keep already narrows to a local,
active session — this rule only removes the password on top of that, it does
not widen who qualifies. Deleting the file (or reverting to the packaged
default, since nothing under /etc/polkit-1/rules.d/ is ours to manage) puts
the prompt back.
After suspend¶
The symptom. The laptop sleeps and wakes, and the GPS has no fix:
cgps or xgps shows satellites stop updating or never come back, and
the map tether shows no position. Parking and waking the receiver brings it
back.
Why (issue #177, measured read-only on the field laptop, 2026-10-01).
Across 19 suspends in one boot the USB receiver was never re-enumerated: the
same USB device number throughout, and its connected_duration equal to the
time the machine was awake. Nothing unplugs it, so gpsd's USB hot-plug
(USBAUTO, gpsdctl@) never fires, and gpsd can keep a tty that has gone
quiet. Every recovery that worked gave gpsd a fresh open of the receiver.
The fault needs something watching across the suspend: cgps, xgps, the
map tether, or gpsd running with -n for GPS time.
Without a watcher, gpsd closes the receiver before the sleep and opens it
fresh afterwards. Power control plays no part: the kept-off rule was not
present and nothing writes power/control for this device.
What hardware apply installs for it. The gps-receiver class asks for
a resume step (resume: {step: gpsd_reopen}), and where gpsd is installed
hammunition hardware apply adds three root-owned files, each printed whole
in the plan before anything runs:
/usr/local/libexec/hammunition-gps-resume, mode0755, a short Python script (standard library only, run by/usr/bin/python3 -I, which gpsd's own package depends on). It logs one line per action:- no
/dev/gpsN(gpsd's own udev rule makes them): nothing. A parked or unplugged receiver is never woken; - no gpsd control socket (
/run/gpsd.sock): nothing, becausegpsdctl addwould otherwise start a gpsd of its own outside systemd; - for each receiver,
gpsdctl removethengpsdctl add, by the path gpsd reports for it (the tty, asgpsdctl@registers it, or/dev/gpsNwhere gpsd was configured with that name); ?DEVICES;to gpsd on127.0.0.1:2947, two seconds at most. No device or no answer:systemctl try-restart gpsd.service, which restarts gpsd only if it is running. Clients then have to reconnect;- a data check: gpsd's socket is watched for up to 20 s for a
SKYorTPVreport from the receiver (or any report withmode1 or more). Satellites without a fix are alive; silence is the fault; - only on silence, one USB power cycle. The script walks up from
/sys/class/tty/<tty>/deviceto the first directory withidVendor/idProductwhose child is the tty's own interface (a hub is never taken for the receiver), checks the path lexically ashammunition hardware parkdoes, writes0to itsauthorized, waits three seconds, writes1, waits for the tty to return, runsgpsdctl addif gpsd does not list it within 5 s, and repeats the data check once. The receiver loses its warm start: a 3D fix returned 74 s after a wake on the bench. With noauthorizedfile it restarts gpsd instead. It never loops.
The unit succeeds only when data was seen. Otherwise it fails and the last
journal line names the manual steps (systemctl status
hammunition-gps-resume). Read it all with sudo journalctl -b -u
hammunition-gps-resume.service -u gpsd.service --since "-1h".
- /etc/systemd/system/hammunition-gps-resume.service, a oneshot ordered
After= and WantedBy= suspend.target, hibernate.target,
hybrid-sleep.target and suspend-then-hibernate.target. It is enabled,
never started: it runs after each resume and at no other time.
- /etc/tmpfiles.d/hammunition-gps-resume.conf, mode 0644, one line making
/run/hammunition (0755, root); systemd-tmpfiles --create makes it at
apply time. The script writes the lines of its last run to
/run/hammunition/gps-resume.log (0644, replaced each run, gone at boot),
so you can read them without systemd-journal:
cat /run/hammunition/gps-resume.log, or
hammunition hardware gps-resume-report, which also checks the installed
files, the unit's last result and whether gpsd delivers data now. Both are
read-only.
A file at any of the three paths that does not start with Hammunition's header refuses
the plan, and is never overwritten. hammunition hardware apply
--no-gps-resume leaves the step out.
When the step fails. The step power-cycles a silent receiver once
itself, through the same authorized switch as park and wake. If it
still fails, restart gpsd (session 12's measured recovery, a fix within
1 s), and if that does not do it, park and wake the receiver by hand (a 3D
fix took 74 s from a wake in bench session 10):
sudo systemctl restart gpsd.socket gpsd
hammunition hardware park gps-receiver
hammunition hardware wake gps-receiver
Inspect it. systemctl cat hammunition-gps-resume shows the unit,
systemctl status hammunition-gps-resume the last run, and
journalctl -u hammunition-gps-resume every run's lines (reading the
system journal may need sudo or membership of systemd-journal).
hammunition doctor reports whether the step is installed when a GPS
receiver is attached and gpsd is installed.
Reverse it. hammunition hardware unapply runs systemctl disable on
the unit and removes the files (and the log and its directory when empty), each only when it starts with Hammunition's
header, then reloads systemd and checks that the files and the four
.wants links are gone.
Not yet measured. Whether gpsdctl remove and add bring the fix back
after a real suspend is what the bench steps on issue #177 measure. If they
do not, a gpsd that still lists the receiver is not restarted
automatically, and the manual steps above are what recovers it. Until the
bench steps are run this is the design the measurement points to, not a
measured recovery.
The tray applet and its Controls panel¶
A Plasma system-tray applet, and for Xfce, LXQt, LXDE, MATE and Cinnamon a Qt
tray icon, live in a separate repository,
hammunition-tray, not in
this one. Each is a client of the engine exactly as the CLI and the generated
menu entries are: it calls the same pkexec
/usr/local/libexec/hammunition-devctl park|wake|state through the same one
polkit action. Since its 0.5.0 it also has a Controls panel with three
groups: the devices on this page, the services the helper controls, and the
machine's radios. The hammunition-tray and hammunition-tray-qt units
install the applet or tray and the helper together. How to use the panel, and
which switch asks for a password, is
the tray's Controls panel; the same switches as
commands are hammunition hardware park and wake here and
hammunition services in the command reference.
Troubleshooting¶
pkexec exiting 126 or 127 does not only mean the dialog was dismissed.
hammunition hardware park/wake report "the authentication prompt was
dismissed; nothing was changed" for any pkexec exit of 126 or 127. Per
pkexec's own manual page, that pair of codes is not only "you clicked
Cancel": 126 is specifically the dialog being dismissed, and 127 covers
every other way authorisation did not happen.
The likeliest 127 on a real ham's machine is not a denial at all — it is
no authentication agent being available to show a dialog in the first
place. That is the ordinary state of a bare SSH session or a plain TTY
with no desktop session behind it, and it is the first thing anyone running
this headless will hit. pkexec falls back to registering its own textual
agent only when nothing else offers one, and on some setups that fallback
still isn't enough. If park/wake reports the prompt was "dismissed" and
you were never on a graphical session (or a terminal a graphical session's
own polkit agent is attached to) to dismiss anything, install policykit-1
— the same package hammunition hardware park/wake already names in
their own error message when pkexec is missing outright — and make sure a
polkit authentication agent is actually running for that session.
127 also covers the calling process being outright denied (a policy
decision, not merely "not yet given") and authentication failing, and folds
in any other error, which is the bucket a target that exists but is not
marked executable falls into — the state /usr/local/libexec/hammunition-devctl
would be in if its permissions were altered by hand after apply wrote it
0755. Nothing is written to the device in any of these cases, so this is a
message-accuracy issue rather than a correctness one — but if the dialog
never appeared at all and an authentication agent genuinely is running,
check ls -l /usr/local/libexec/hammunition-devctl before assuming you
dismissed a prompt you never saw.