reboot command beside poweroff, one polkit grant; forward yukart/gnss/+; mock uses current names
--allow-poweroff becomes --allow-power. deploy/50-yukart-gw-power.pkla adds logind's reboot actions and the deploy removes the old poweroff-only file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
# polkit (0.105, local authority): lets the svc-gw user power the host off through
|
# polkit (0.105, local authority): lets the svc-gw user power the host off or reboot it
|
||||||
# logind, with no sudo. svc-gw runs `systemctl poweroff` for the display's
|
# through logind, with no sudo. svc-gw runs `systemctl poweroff` / `systemctl reboot`
|
||||||
# `poweroff` command (contracts BOUNDARY.md). Nothing else is granted.
|
# for the display's `poweroff` and `reboot` commands (contracts BOUNDARY.md). Nothing
|
||||||
|
# else is granted.
|
||||||
# Installed to /etc/polkit-1/localauthority/50-local.d/ by scripts/deploy.sh.
|
# Installed to /etc/polkit-1/localauthority/50-local.d/ by scripts/deploy.sh.
|
||||||
# multiple-sessions: an ssh login is another session; ignore-inhibit: no inhibitor lock
|
# multiple-sessions: an ssh login is another session; ignore-inhibit: no inhibitor lock
|
||||||
# (none are taken on this host today) should leave the button silently doing nothing.
|
# (none are taken on this host today) should leave the button silently doing nothing.
|
||||||
[yukart-gw: power off]
|
[yukart-gw: power off and reboot]
|
||||||
Identity=unix-user:mikkeli
|
Identity=unix-user:mikkeli
|
||||||
Action=org.freedesktop.login1.power-off;org.freedesktop.login1.power-off-multiple-sessions;org.freedesktop.login1.power-off-ignore-inhibit
|
Action=org.freedesktop.login1.power-off;org.freedesktop.login1.power-off-multiple-sessions;org.freedesktop.login1.power-off-ignore-inhibit;org.freedesktop.login1.reboot;org.freedesktop.login1.reboot-multiple-sessions;org.freedesktop.login1.reboot-ignore-inhibit
|
||||||
ResultAny=yes
|
ResultAny=yes
|
||||||
ResultInactive=yes
|
ResultInactive=yes
|
||||||
ResultActive=yes
|
ResultActive=yes
|
||||||
@@ -8,8 +8,8 @@ StartLimitIntervalSec=0
|
|||||||
[Service]
|
[Service]
|
||||||
User=mikkeli
|
User=mikkeli
|
||||||
# Add --mock only on a bench unit: every message then says so, and displays must show it.
|
# Add --mock only on a bench unit: every message then says so, and displays must show it.
|
||||||
# --allow-poweroff: the display may shut this host down (contracts BOUNDARY.md, poweroff). Only on the Orin.
|
# --allow-power: the display may power this host off or reboot it (contracts BOUNDARY.md). Only on the Orin.
|
||||||
ExecStart=/usr/local/bin/yukart-gw --listen 0.0.0.0:7700 --allow-poweroff
|
ExecStart=/usr/local/bin/yukart-gw --listen 0.0.0.0:7700 --allow-power
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=2
|
RestartSec=2
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ directly.
|
|||||||
0. **Boundary v0** — in place: WebSocket on `:7700`, hello, heartbeats both ways,
|
0. **Boundary v0** — in place: WebSocket on `:7700`, hello, heartbeats both ways,
|
||||||
`mark` forwarded to the recorder's control socket, mock state behind `--mock`.
|
`mark` forwarded to the recorder's control socket, mock state behind `--mock`.
|
||||||
Protocol in contracts [`docs/BOUNDARY.md`](gitea:yukart/contracts).
|
Protocol in contracts [`docs/BOUNDARY.md`](gitea:yukart/contracts).
|
||||||
**Bus input** — in place (2026-10-12): subscribes to `yukart/vehicle/+` and
|
**Bus input** — in place (2026-10-12): subscribes to `yukart/vehicle/+`,
|
||||||
`yukart/host/+`, keeps the newest envelope per signal, marks stale after 5 s
|
`yukart/host/+` and `yukart/gnss/+`, keeps the newest envelope per signal, marks stale after 5 s
|
||||||
or when known only from a retained copy. Bus or mock, never both.
|
or when known only from a retained copy. Bus or mock, never both.
|
||||||
1. Static file serving for the basemap — smallest useful thing, unblocks the map.
|
1. Static file serving for the basemap — smallest useful thing, unblocks the map.
|
||||||
2. Nav HTTP proxy to [svc-nav](gitea:yukart/svc-nav).
|
2. Nav HTTP proxy to [svc-nav](gitea:yukart/svc-nav).
|
||||||
|
|||||||
+7
-5
@@ -6,8 +6,8 @@
|
|||||||
# env: BUILD_HOST (default: mikkeli-orin-nano-2)
|
# env: BUILD_HOST (default: mikkeli-orin-nano-2)
|
||||||
#
|
#
|
||||||
# Builds what is pushed, not the working tree: the build host pulls master of
|
# Builds what is pushed, not the working tree: the build host pulls master of
|
||||||
# svc-gw and contracts under ~/dev/yukart/. Native on purpose: the poweroff command
|
# svc-gw and contracts under ~/dev/yukart/. Native on purpose: the poweroff and reboot
|
||||||
# runs systemctl, and std::process::Command cross-compiled against the workstation's
|
# commands run systemctl, and std::process::Command cross-compiled against the workstation's
|
||||||
# newer glibc links GLIBC_2.39 symbols (pidfd_spawnp) the car cannot load.
|
# newer glibc links GLIBC_2.39 symbols (pidfd_spawnp) the car cannot load.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -31,13 +31,15 @@ scripts/check-glibc.sh "$BIN"
|
|||||||
ssh "$HOST" 'mkdir -p ~/infra/svc-gw'
|
ssh "$HOST" 'mkdir -p ~/infra/svc-gw'
|
||||||
scp -q "$BIN" "$HOST:infra/svc-gw/svc-gw.new"
|
scp -q "$BIN" "$HOST:infra/svc-gw/svc-gw.new"
|
||||||
scp -q deploy/yukart-gw.service "$HOST:infra/svc-gw/yukart-gw.service"
|
scp -q deploy/yukart-gw.service "$HOST:infra/svc-gw/yukart-gw.service"
|
||||||
scp -q deploy/50-yukart-gw-poweroff.pkla "$HOST:infra/svc-gw/50-yukart-gw-poweroff.pkla"
|
scp -q deploy/50-yukart-gw-power.pkla "$HOST:infra/svc-gw/50-yukart-gw-power.pkla"
|
||||||
ssh "$HOST" 'set -e
|
ssh "$HOST" 'set -e
|
||||||
mv ~/infra/svc-gw/svc-gw.new ~/infra/svc-gw/svc-gw
|
mv ~/infra/svc-gw/svc-gw.new ~/infra/svc-gw/svc-gw
|
||||||
sudo ln -sfn "$HOME/infra/svc-gw/svc-gw" /usr/local/bin/yukart-gw
|
sudo ln -sfn "$HOME/infra/svc-gw/svc-gw" /usr/local/bin/yukart-gw
|
||||||
sudo install -m 0644 ~/infra/svc-gw/yukart-gw.service /etc/systemd/system/yukart-gw.service
|
sudo install -m 0644 ~/infra/svc-gw/yukart-gw.service /etc/systemd/system/yukart-gw.service
|
||||||
# polkit, not sudo, lets the service power the host off (the poweroff command).
|
# polkit, not sudo, lets the service power the host off or reboot it (the poweroff and
|
||||||
sudo install -m 0644 ~/infra/svc-gw/50-yukart-gw-poweroff.pkla /etc/polkit-1/localauthority/50-local.d/50-yukart-gw-poweroff.pkla
|
# reboot commands). The file was named 50-yukart-gw-poweroff.pkla before reboot joined.
|
||||||
|
sudo install -m 0644 ~/infra/svc-gw/50-yukart-gw-power.pkla /etc/polkit-1/localauthority/50-local.d/50-yukart-gw-power.pkla
|
||||||
|
sudo rm -f /etc/polkit-1/localauthority/50-local.d/50-yukart-gw-poweroff.pkla ~/infra/svc-gw/50-yukart-gw-poweroff.pkla
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable yukart-gw.service >/dev/null
|
sudo systemctl enable yukart-gw.service >/dev/null
|
||||||
sudo systemctl restart yukart-gw.service
|
sudo systemctl restart yukart-gw.service
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ use std::time::{Duration, Instant};
|
|||||||
use rumqttc::{Client, Event, MqttOptions, Packet, QoS};
|
use rumqttc::{Client, Event, MqttOptions, Packet, QoS};
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
|
|
||||||
const TOPICS: [&str; 2] = ["yukart/vehicle/+", "yukart/host/+"];
|
const TOPICS: [&str; 3] = ["yukart/vehicle/+", "yukart/host/+", "yukart/gnss/+"];
|
||||||
const STALE: Duration = Duration::from_secs(5);
|
const STALE: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
struct Entry {
|
struct Entry {
|
||||||
|
|||||||
+19
-17
@@ -26,8 +26,8 @@ pub struct Ctx {
|
|||||||
pub hub: Hub,
|
pub hub: Hub,
|
||||||
pub hello: Value,
|
pub hello: Value,
|
||||||
pub recorder_ctl: String,
|
pub recorder_ctl: String,
|
||||||
/// `--allow-poweroff`: off by default, so a gateway on a workstation can never shut it down.
|
/// `--allow-power`: off by default, so a gateway on a workstation can never shut it down or reboot it.
|
||||||
pub allow_poweroff: bool,
|
pub allow_power: bool,
|
||||||
/// Acks by command id, shared across connections so a retry after reconnect is answered, not re-run.
|
/// Acks by command id, shared across connections so a retry after reconnect is answered, not re-run.
|
||||||
pub acks: Mutex<VecDeque<(String, Value)>>,
|
pub acks: Mutex<VecDeque<(String, Value)>>,
|
||||||
}
|
}
|
||||||
@@ -130,39 +130,41 @@ fn run(ctx: &Ctx, msg: &Value) -> (bool, String) {
|
|||||||
Err(e) => (false, format!("recorder unreachable: {e}")),
|
Err(e) => (false, format!("recorder unreachable: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"poweroff" => poweroff(ctx, &msg["args"]),
|
"poweroff" => power(ctx, &msg["args"], "poweroff"),
|
||||||
|
"reboot" => power(ctx, &msg["args"], "reboot"),
|
||||||
other => (false, format!("unknown command '{other}'")),
|
other => (false, format!("unknown command '{other}'")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The host's boot id: a poweroff command must carry the current one, so a resend
|
/// The host's boot id: a poweroff or reboot command must carry the current one, so a resend
|
||||||
/// from before a reboot cannot shut the next boot down.
|
/// from before a reboot cannot shut the next boot down.
|
||||||
pub fn boot_id() -> String {
|
pub fn boot_id() -> String {
|
||||||
std::fs::read_to_string("/proc/sys/kernel/random/boot_id").unwrap_or_default().trim().to_string()
|
std::fs::read_to_string("/proc/sys/kernel/random/boot_id").unwrap_or_default().trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poweroff(ctx: &Ctx, args: &Value) -> (bool, String) {
|
/// `poweroff` or `reboot`: the same guards, the same grant, a different systemctl verb.
|
||||||
if !ctx.allow_poweroff {
|
fn power(ctx: &Ctx, args: &Value, verb: &'static str) -> (bool, String) {
|
||||||
return (false, "poweroff not enabled on this gateway (--allow-poweroff)".into());
|
if !ctx.allow_power {
|
||||||
|
return (false, format!("{verb} not enabled on this gateway (--allow-power)"));
|
||||||
}
|
}
|
||||||
if args["confirm"].as_str() != Some("poweroff") {
|
if args["confirm"].as_str() != Some(verb) {
|
||||||
return (false, "poweroff needs args.confirm = \"poweroff\"".into());
|
return (false, format!("{verb} needs args.confirm = \"{verb}\""));
|
||||||
}
|
}
|
||||||
if args["boot_id"].as_str() != Some(boot_id().as_str()) {
|
if args["boot_id"].as_str() != Some(boot_id().as_str()) {
|
||||||
return (false, "poweroff refused: boot_id is not this boot's".into());
|
return (false, format!("{verb} refused: boot_id is not this boot's"));
|
||||||
}
|
}
|
||||||
eprintln!("svc-gw: poweroff requested by a client");
|
eprintln!("svc-gw: {verb} requested by a client");
|
||||||
// Ack first: the reply has to reach the client before the services stop.
|
// Ack first: the reply has to reach the client before the services stop.
|
||||||
thread::spawn(|| {
|
thread::spawn(move || {
|
||||||
thread::sleep(Duration::from_secs(2));
|
thread::sleep(Duration::from_secs(2));
|
||||||
// Unprivileged: polkit grants this user logind's power-off (deploy/50-yukart-gw-poweroff.pkla).
|
// Unprivileged: polkit grants this user logind's power-off and reboot (deploy/50-yukart-gw-power.pkla).
|
||||||
match Command::new("systemctl").args(["poweroff", "--no-ask-password"]).status() {
|
match Command::new("systemctl").args([verb, "--no-ask-password"]).status() {
|
||||||
Ok(s) if s.success() => {}
|
Ok(s) if s.success() => {}
|
||||||
Ok(s) => eprintln!("svc-gw: systemctl poweroff exited {s}"),
|
Ok(s) => eprintln!("svc-gw: systemctl {verb} exited {s}"),
|
||||||
Err(e) => eprintln!("svc-gw: systemctl poweroff: {e}"),
|
Err(e) => eprintln!("svc-gw: systemctl {verb}: {e}"),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
(true, "shutting down in 2 s".into())
|
(true, if verb == "reboot" { "rebooting in 2 s" } else { "shutting down in 2 s" }.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recorder(path: &str, line: &str) -> std::io::Result<String> {
|
fn recorder(path: &str, line: &str) -> std::io::Result<String> {
|
||||||
|
|||||||
+4
-4
@@ -25,7 +25,7 @@ usage: svc-gw [options]
|
|||||||
--mock synthesise vehicle state, flagged as mock in every message (no bus)
|
--mock synthesise vehicle state, flagged as mock in every message (no bus)
|
||||||
--broker HOST:PORT MQTT broker; '' for none (127.0.0.1:1883)
|
--broker HOST:PORT MQTT broker; '' for none (127.0.0.1:1883)
|
||||||
--recorder-ctl PATH recorder control socket, for 'mark' (/run/yukart-recorder/ctl.sock)
|
--recorder-ctl PATH recorder control socket, for 'mark' (/run/yukart-recorder/ctl.sock)
|
||||||
--allow-poweroff accept the 'poweroff' command (the Orin's unit only; never on a workstation)";
|
--allow-power accept the 'poweroff' and 'reboot' commands (the Orin's unit only; never on a workstation)";
|
||||||
|
|
||||||
pub fn now_us() -> i64 {
|
pub fn now_us() -> i64 {
|
||||||
let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
|
let d = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
|
||||||
@@ -54,13 +54,13 @@ fn main() -> ExitCode {
|
|||||||
let mut listen = "0.0.0.0:7700".to_string();
|
let mut listen = "0.0.0.0:7700".to_string();
|
||||||
let mut mock = false;
|
let mut mock = false;
|
||||||
let mut recorder_ctl = "/run/yukart-recorder/ctl.sock".to_string();
|
let mut recorder_ctl = "/run/yukart-recorder/ctl.sock".to_string();
|
||||||
let mut allow_poweroff = false;
|
let mut allow_power = false;
|
||||||
let mut broker = "127.0.0.1:1883".to_string();
|
let mut broker = "127.0.0.1:1883".to_string();
|
||||||
let mut args = std::env::args().skip(1);
|
let mut args = std::env::args().skip(1);
|
||||||
while let Some(a) = args.next() {
|
while let Some(a) = args.next() {
|
||||||
match a.as_str() {
|
match a.as_str() {
|
||||||
"--mock" => mock = true,
|
"--mock" => mock = true,
|
||||||
"--allow-poweroff" => allow_poweroff = true,
|
"--allow-power" => allow_power = true,
|
||||||
"--listen" => listen = args.next().unwrap_or_default(),
|
"--listen" => listen = args.next().unwrap_or_default(),
|
||||||
"--recorder-ctl" => recorder_ctl = args.next().unwrap_or_default(),
|
"--recorder-ctl" => recorder_ctl = args.next().unwrap_or_default(),
|
||||||
"--broker" => broker = args.next().unwrap_or_default(),
|
"--broker" => broker = args.next().unwrap_or_default(),
|
||||||
@@ -99,7 +99,7 @@ fn main() -> ExitCode {
|
|||||||
"timeout_ms": client::TIMEOUT.as_millis() as u64,
|
"timeout_ms": client::TIMEOUT.as_millis() as u64,
|
||||||
}),
|
}),
|
||||||
recorder_ctl,
|
recorder_ctl,
|
||||||
allow_poweroff,
|
allow_power,
|
||||||
acks: Mutex::new(Default::default()),
|
acks: Mutex::new(Default::default()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -40,20 +40,20 @@ impl Mock {
|
|||||||
s.insert(name.to_string(), reg.envelope(name, v, q, true));
|
s.insert(name.to_string(), reg.envelope(name, v, q, true));
|
||||||
};
|
};
|
||||||
put("engine_rpm", json!(rpm.round()), None);
|
put("engine_rpm", json!(rpm.round()), None);
|
||||||
put("vehicle_speed_kph", json!((speed * 10.0_f64).round() / 10.0), None);
|
put("gnss_speed_kph", json!((speed * 10.0_f64).round() / 10.0), None);
|
||||||
put("coolant_temp_c", json!(self.coolant.round()), None);
|
put("obd_coolant_temp_c", json!(self.coolant.round()), None);
|
||||||
// Throttle drops out for 5 s in every 40: a signal that disappears.
|
// The pedal drops out for 5 s in every 40: a signal that disappears.
|
||||||
if t % 40.0 >= 5.0 {
|
if t % 40.0 >= 5.0 {
|
||||||
put("throttle_pct", json!(throttle), None);
|
put("accel_pedal_pct", json!(throttle), None);
|
||||||
}
|
}
|
||||||
// Intake temperature is stuck at its last value and marked stale for 10 s in every 30.
|
// Intake temperature is stuck at its last value and marked stale for 10 s in every 30.
|
||||||
if t % 30.0 < 10.0 {
|
if t % 30.0 < 10.0 {
|
||||||
put("intake_air_temp_c", json!(31.0), Some("stale"));
|
put("obd_intake_air_temp_c", json!(31.0), Some("stale"));
|
||||||
} else {
|
} else {
|
||||||
put("intake_air_temp_c", json!((31.0 + (t * 0.05).sin()).round()), None);
|
put("obd_intake_air_temp_c", json!((31.0 + (t * 0.05).sin()).round()), None);
|
||||||
}
|
}
|
||||||
// Fuel level is known but has no value: the registry has it, the car never sent it.
|
// Fuel level is known but has no value: the registry has it, the car never sent it.
|
||||||
put("fuel_level_pct", Value::Null, None);
|
put("obd_fuel_level_pct", Value::Null, None);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user