Pilot v2: Core implementation + battery telemetry

Major updates:
- Complete Rust rewrite (pilot-v2/) with working MQTT client
- Fixed MQTT event loop deadlock (background task pattern)
- Battery telemetry for Linux (auto-detected via /sys/class/power_supply)
- Home Assistant auto-discovery for all sensors and switches
- Comprehensive documentation (AVANCEMENT.md, CLAUDE.md, roadmap)
- Docker test environment with Mosquitto broker
- Helper scripts for development and testing

Features working:
 MQTT connectivity with LWT
 YAML configuration with validation
 Telemetry: CPU, memory, IP, battery (Linux)
 Commands: shutdown, reboot, sleep, screen (dry-run tested)
 HA discovery and integration
 Allowlist and cooldown protection

Ready for testing on real hardware.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Gilles Soulier
2025-12-30 06:23:00 +01:00
parent b7e67c6501
commit c5381b7112
4453 changed files with 78647 additions and 0 deletions

2206
pilot-v2/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
pilot-v2/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
# Rust package metadata and dependencies for pilot v2.
[package]
name = "pilot-v2"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1"
thiserror = "1"
serde = { version = "1", features = ["derive"] }
serde_yaml = "0.9"
serde_json = "1"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
rumqttc = "0.24"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "signal", "sync"] }
sysinfo = "0.30"
local-ip-address = "0.6"
zbus = "3"

43
pilot-v2/config.yaml Normal file
View File

@@ -0,0 +1,43 @@
# Codex created 2025-12-29_0224
device:
name: pilot-device
identifiers: ["pilot-device"]
mqtt:
host: "127.0.0.1"
port: 1883
username: ""
password: ""
base_topic: "pilot"
discovery_prefix: "homeassistant"
client_id: "pilot-device"
keepalive_s: 60
qos: 0
retain_states: true
features:
telemetry:
enabled: true
interval_s: 10
commands:
enabled: true
cooldown_s: 5
dry_run: true
allowlist: ["shutdown", "reboot", "sleep", "screen"]
power_backend:
linux: "linux_logind_polkit" # or linux_sudoers
windows: "windows_service"
screen_backend:
linux: "gnome_busctl" # or x11_xset
windows: "winapi_session" # or external_tool
publish:
heartbeat_s: 30
availability: true
paths:
linux_config: "/etc/pilot/config.yaml"
windows_config: "C:\\ProgramData\\Pilot\\config.yaml"
# Codex modified 2025-12-29_0224

View File

@@ -0,0 +1,129 @@
// Ce module declare les interfaces des commandes systeme et le parsing basique.
use anyhow::{bail, Result};
use std::time::{Duration, Instant};
use tracing::info;
// Actions d'alimentation supportees (shutdown/reboot/sleep).
pub trait PowerControl {
fn shutdown(&self) -> Result<()>;
fn reboot(&self) -> Result<()>;
fn sleep(&self) -> Result<()>;
}
// Actions d'ecran supportees (on/off).
pub trait ScreenControl {
fn screen_on(&self) -> Result<()>;
fn screen_off(&self) -> Result<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CommandAction {
Shutdown,
Reboot,
Sleep,
Screen,
}
#[derive(Debug, Clone, Copy)]
pub enum CommandValue {
On,
Off,
}
// Decode une action depuis le topic cmd/<action>/set.
pub fn parse_action(topic: &str) -> Result<CommandAction> {
let parts: Vec<&str> = topic.split('/').collect();
if parts.len() < 2 {
bail!("topic too short");
}
let action = parts[parts.len() - 2];
match action {
"shutdown" => Ok(CommandAction::Shutdown),
"reboot" => Ok(CommandAction::Reboot),
"sleep" => Ok(CommandAction::Sleep),
"screen" => Ok(CommandAction::Screen),
_ => bail!("unknown action"),
}
}
// Decode une valeur ON/OFF (insensible a la casse).
pub fn parse_value(payload: &[u8]) -> Result<CommandValue> {
let raw = String::from_utf8_lossy(payload).trim().to_uppercase();
match raw.as_str() {
"ON" => Ok(CommandValue::On),
"OFF" => Ok(CommandValue::Off),
_ => bail!("invalid payload"),
}
}
// Verifie si l'action est autorisee par l'allowlist (vide = tout autoriser).
pub fn allowlist_allows(allowlist: &[String], action: CommandAction) -> bool {
if allowlist.is_empty() {
return true;
}
let name = action_name(action);
allowlist.iter().any(|item| item == name)
}
// Verifie le cooldown et renvoie true si l'action est autorisee.
pub fn allow_command(
last_exec: &mut std::collections::HashMap<CommandAction, Instant>,
cooldown_s: u64,
action: CommandAction,
) -> bool {
let now = Instant::now();
if let Some(prev) = last_exec.get(&action) {
if now.duration_since(*prev) < Duration::from_secs(cooldown_s) {
return false;
}
}
last_exec.insert(action, now);
true
}
// Execute une commande en mode dry-run (journalise seulement).
pub fn execute_dry_run(action: CommandAction, value: CommandValue) -> Result<()> {
info!(?action, ?value, "dry-run command");
Ok(())
}
// Convertit une action en nom utilise par la config.
pub fn action_name(action: CommandAction) -> &'static str {
match action {
CommandAction::Shutdown => "shutdown",
CommandAction::Reboot => "reboot",
CommandAction::Sleep => "sleep",
CommandAction::Screen => "screen",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_action_ok() {
let topic = "pilot/device/cmd/shutdown/set";
assert_eq!(parse_action(topic).unwrap(), CommandAction::Shutdown);
}
#[test]
fn parse_value_ok() {
assert!(matches!(parse_value(b"ON").unwrap(), CommandValue::On));
assert!(matches!(parse_value(b"off").unwrap(), CommandValue::Off));
}
#[test]
fn allowlist_checks() {
let list = vec!["shutdown".to_string(), "screen".to_string()];
assert!(allowlist_allows(&list, CommandAction::Shutdown));
assert!(!allowlist_allows(&list, CommandAction::Reboot));
}
#[test]
fn cooldown_blocks_second_call() {
let mut last_exec = std::collections::HashMap::new();
assert!(allow_command(&mut last_exec, 60, CommandAction::Shutdown));
assert!(!allow_command(&mut last_exec, 60, CommandAction::Shutdown));
}
}

183
pilot-v2/src/config/mod.rs Normal file
View File

@@ -0,0 +1,183 @@
// Ce module charge et valide la configuration YAML du projet.
// Il expose des structures de donnees simples pour le reste du code.
use anyhow::{bail, Context, Result};
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
pub device: Device,
pub mqtt: Mqtt,
pub features: Features,
pub power_backend: PowerBackend,
pub screen_backend: ScreenBackend,
pub publish: Publish,
pub paths: Option<Paths>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Device {
pub name: String,
pub identifiers: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Mqtt {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
pub base_topic: String,
pub discovery_prefix: String,
pub client_id: String,
pub keepalive_s: u64,
pub qos: u8,
pub retain_states: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Features {
pub telemetry: Telemetry,
pub commands: Commands,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Telemetry {
pub enabled: bool,
pub interval_s: u64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Commands {
pub enabled: bool,
pub cooldown_s: u64,
pub dry_run: bool,
pub allowlist: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PowerBackend {
pub linux: String,
pub windows: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ScreenBackend {
pub linux: String,
pub windows: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Publish {
pub heartbeat_s: u64,
pub availability: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Paths {
pub linux_config: String,
pub windows_config: String,
}
// Charge la config depuis les chemins par defaut (OS + fallback).
pub fn load() -> Result<Config> {
let candidates = candidate_paths();
for path in candidates {
if path.exists() {
let raw = fs::read_to_string(&path)
.with_context(|| format!("failed reading config {}", path.display()))?;
let cfg: Config = serde_yaml::from_str(&raw)
.with_context(|| format!("failed parsing config {}", path.display()))?;
validate(&cfg)?;
return Ok(cfg);
}
}
bail!("no config file found in default locations");
}
// Liste les chemins de config a tester en premier.
pub fn candidate_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if cfg!(target_os = "windows") {
paths.push(PathBuf::from(r"C:\ProgramData\Pilot\config.yaml"));
} else {
paths.push(PathBuf::from("/etc/pilot/config.yaml"));
}
paths.push(PathBuf::from("./config.yaml"));
paths
}
// Construit la racine des topics MQTT pour le device.
pub fn base_device_topic(cfg: &Config) -> String {
let base = cfg.mqtt.base_topic.trim_end_matches('/');
format!("{}/{}", base, cfg.device.name)
}
// Verifie les champs minimum pour eviter les erreurs au demarrage.
fn validate(cfg: &Config) -> Result<()> {
if cfg.device.name.trim().is_empty() {
bail!("device.name must not be empty");
}
if cfg.mqtt.host.trim().is_empty() {
bail!("mqtt.host must not be empty");
}
if cfg.mqtt.base_topic.trim().is_empty() {
bail!("mqtt.base_topic must not be empty");
}
if cfg.mqtt.discovery_prefix.trim().is_empty() {
bail!("mqtt.discovery_prefix must not be empty");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_config_from_yaml() {
let raw = r#"
device:
name: "test"
identifiers: ["test"]
mqtt:
host: "127.0.0.1"
port: 1883
username: ""
password: ""
base_topic: "pilot"
discovery_prefix: "homeassistant"
client_id: "test"
keepalive_s: 60
qos: 0
retain_states: true
features:
telemetry:
enabled: true
interval_s: 5
commands:
enabled: true
cooldown_s: 2
dry_run: true
allowlist: ["shutdown"]
power_backend:
linux: "linux_logind_polkit"
windows: "windows_service"
screen_backend:
linux: "gnome_busctl"
windows: "winapi_session"
publish:
heartbeat_s: 10
availability: true
"#;
let cfg: Config = serde_yaml::from_str(raw).unwrap();
validate(&cfg).unwrap();
assert_eq!(cfg.device.name, "test");
assert_eq!(cfg.mqtt.port, 1883);
assert!(cfg.features.commands.dry_run);
}
}

115
pilot-v2/src/ha/mod.rs Normal file
View File

@@ -0,0 +1,115 @@
// Ce module regroupe la publication Home Assistant discovery.
use anyhow::{Context, Result};
use rumqttc::AsyncClient;
use serde::Serialize;
use crate::config::{base_device_topic, Config};
#[derive(Clone, Serialize)]
struct DeviceInfo {
identifiers: Vec<String>,
name: String,
manufacturer: String,
model: String,
sw_version: String,
}
#[derive(Serialize)]
struct EntityConfig<'a> {
name: &'a str,
unique_id: String,
state_topic: String,
availability_topic: String,
device: DeviceInfo,
#[serde(skip_serializing_if = "Option::is_none")]
command_topic: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
payload_on: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
payload_off: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
unit_of_measurement: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
device_class: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
icon: Option<&'a str>,
}
// Publie les entites HA discovery pour les capteurs et commandes standard.
pub async fn publish_all(client: &AsyncClient, cfg: &Config) -> Result<()> {
let base = base_device_topic(cfg);
let prefix = cfg.mqtt.discovery_prefix.trim_end_matches('/');
let device = DeviceInfo {
identifiers: cfg.device.identifiers.clone(),
name: cfg.device.name.clone(),
manufacturer: "Pilot".to_string(),
model: "v2".to_string(),
sw_version: "2.0.0".to_string(),
};
let availability = format!("{}/availability", base);
let sensors = vec![
("cpu_usage", "CPU Usage", Some("%"), Some("power"), Some("mdi:chip")),
("memory_used_mb", "Memory Used", Some("MB"), None, Some("mdi:memory")),
("memory_total_mb", "Memory Total", Some("MB"), None, Some("mdi:memory")),
("ip_address", "IP Address", None, None, Some("mdi:ip")),
("power_state", "Power State", None, None, Some("mdi:power")),
("battery_level", "Battery Level", Some("%"), Some("battery"), Some("mdi:battery")),
("battery_state", "Battery State", None, None, Some("mdi:battery-charging")),
];
for (key, name, unit, class, icon) in sensors {
let entity = EntityConfig {
name,
unique_id: format!("{}_{}", cfg.device.name, key),
state_topic: format!("{}/state/{}", base, key),
availability_topic: availability.clone(),
device: DeviceInfo { ..device.clone() },
command_topic: None,
payload_on: None,
payload_off: None,
unit_of_measurement: unit,
device_class: class,
icon,
};
let topic = format!("{}/sensor/{}/{}_{}", prefix, cfg.device.name, cfg.device.name, key);
publish_discovery(client, &topic, &entity).await?;
}
let switches = vec![
("shutdown", "Shutdown", "cmd/shutdown/set"),
("reboot", "Reboot", "cmd/reboot/set"),
("sleep", "Sleep", "cmd/sleep/set"),
("screen", "Screen", "cmd/screen/set"),
];
for (key, name, cmd) in switches {
let entity = EntityConfig {
name,
unique_id: format!("{}_{}", cfg.device.name, key),
state_topic: format!("{}/state/{}", base, key),
availability_topic: availability.clone(),
device: DeviceInfo { ..device.clone() },
command_topic: Some(format!("{}/{}", base, cmd)),
payload_on: Some("ON"),
payload_off: Some("OFF"),
unit_of_measurement: None,
device_class: Some("switch"),
icon: Some("mdi:power"),
};
let topic = format!("{}/switch/{}/{}_{}", prefix, cfg.device.name, cfg.device.name, key);
publish_discovery(client, &topic, &entity).await?;
}
Ok(())
}
async fn publish_discovery<T: Serialize>(client: &AsyncClient, topic: &str, payload: &T) -> Result<()> {
let data = serde_json::to_vec(payload).context("serialize discovery")?;
client
.publish(topic, rumqttc::QoS::AtLeastOnce, true, data)
.await
.context("publish discovery")?;
Ok(())
}

9
pilot-v2/src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
// Modules de base exposes au binaire principal.
pub mod config;
pub mod mqtt;
pub mod ha;
pub mod telemetry;
pub mod commands;
pub mod platform;
pub mod runtime;
pub mod security;

19
pilot-v2/src/main.rs Normal file
View File

@@ -0,0 +1,19 @@
// Point d'entree principal de l'application.
use anyhow::Result;
use tracing::info;
use pilot_v2::config;
use pilot_v2::runtime::Runtime;
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter("pilot_v2=info")
.init();
let config = config::load()?;
info!("config loaded");
let runtime = Runtime::new(config);
runtime.run().await
}

132
pilot-v2/src/mqtt/mod.rs Normal file
View File

@@ -0,0 +1,132 @@
// Ce module gere la connexion MQTT et les publications de base.
use anyhow::{Context, Result};
use rumqttc::{AsyncClient, EventLoop, LastWill, MqttOptions, QoS};
use serde::Serialize;
use std::time::Duration;
use crate::config::{base_device_topic, Config};
pub struct MqttHandle {
pub client: AsyncClient,
pub event_loop: EventLoop,
}
#[derive(Debug, Serialize)]
pub struct Status {
pub version: String,
pub os: String,
pub uptime_s: u64,
pub last_error: String,
pub backends: Backends,
}
#[derive(Debug, Serialize)]
pub struct Backends {
pub power: String,
pub screen: String,
}
#[derive(Debug, Serialize)]
pub struct Capabilities {
pub telemetry: Vec<String>,
pub commands: Vec<String>,
pub gpu: bool,
}
// Cree un client MQTT configure selon le YAML.
pub fn connect(cfg: &Config) -> Result<MqttHandle> {
let client_id = if cfg.mqtt.client_id.trim().is_empty() {
cfg.device.name.clone()
} else {
cfg.mqtt.client_id.clone()
};
let mut options = MqttOptions::new(client_id, cfg.mqtt.host.clone(), cfg.mqtt.port);
options.set_keep_alive(Duration::from_secs(cfg.mqtt.keepalive_s));
if !cfg.mqtt.username.trim().is_empty() || !cfg.mqtt.password.trim().is_empty() {
options.set_credentials(cfg.mqtt.username.clone(), cfg.mqtt.password.clone());
}
let will_topic = format!("{}/availability", base_device_topic(cfg));
let will = LastWill::new(will_topic, "offline", qos(cfg), true);
options.set_last_will(will);
let (client, event_loop) = AsyncClient::new(options, 10);
Ok(MqttHandle { client, event_loop })
}
// Publie availability en retained pour indiquer online/offline.
pub async fn publish_availability(client: &AsyncClient, cfg: &Config, online: bool) -> Result<()> {
let topic = format!("{}/availability", base_device_topic(cfg));
let payload = if online { "online" } else { "offline" };
client
.publish(topic, qos(cfg), true, payload)
.await
.context("publish availability")?;
Ok(())
}
// Publie un status JSON (version, OS, backends, etc.).
pub async fn publish_status(
client: &AsyncClient,
cfg: &Config,
status: &Status,
) -> Result<()> {
let topic = format!("{}/status", base_device_topic(cfg));
let payload = serde_json::to_vec(status).context("serialize status")?;
client
.publish(topic, qos(cfg), true, payload)
.await
.context("publish status")?;
Ok(())
}
// Publie les capacites actives (telemetrie/commandes).
pub async fn publish_capabilities(
client: &AsyncClient,
cfg: &Config,
capabilities: &Capabilities,
) -> Result<()> {
let topic = format!("{}/capabilities", base_device_topic(cfg));
let payload = serde_json::to_vec(capabilities).context("serialize capabilities")?;
client
.publish(topic, qos(cfg), true, payload)
.await
.context("publish capabilities")?;
Ok(())
}
// Publie une valeur de capteur ou d'etat systeme.
pub async fn publish_state(
client: &AsyncClient,
cfg: &Config,
name: &str,
value: &str,
) -> Result<()> {
let topic = format!("{}/state/{}", base_device_topic(cfg), name);
client
.publish(topic, qos(cfg), cfg.mqtt.retain_states, value)
.await
.context("publish state")?;
Ok(())
}
// S'abonne aux commandes standard (cmd/<action>/set).
pub async fn subscribe_commands(client: &AsyncClient, cfg: &Config) -> Result<()> {
let topic = format!("{}/cmd/+/set", base_device_topic(cfg));
client
.subscribe(topic, qos(cfg))
.await
.context("subscribe commands")?;
Ok(())
}
// Convertit le QoS configure (0/1/2) en enum rumqttc.
fn qos(cfg: &Config) -> QoS {
match cfg.mqtt.qos {
1 => QoS::AtLeastOnce,
2 => QoS::ExactlyOnce,
_ => QoS::AtMostOnce,
}
}

View File

@@ -0,0 +1,75 @@
// Implementations Linux (logind, sudoers, gnome busctl, x11 xset).
use anyhow::{bail, Context, Result};
use std::process::Command;
use crate::commands::{CommandAction, CommandValue};
// Execute une commande d'alimentation selon le backend choisi.
pub fn execute_power(backend: &str, action: CommandAction) -> Result<()> {
match backend {
"linux_logind_polkit" => match action {
CommandAction::Shutdown => run("systemctl", &["poweroff"]),
CommandAction::Reboot => run("systemctl", &["reboot"]),
CommandAction::Sleep => run("systemctl", &["suspend"]),
CommandAction::Screen => bail!("screen action not supported in power backend"),
},
"linux_sudoers" => match action {
CommandAction::Shutdown => run("shutdown", &["-h", "now"]),
CommandAction::Reboot => run("reboot", &[]),
CommandAction::Sleep => run("systemctl", &["suspend"]),
CommandAction::Screen => bail!("screen action not supported in power backend"),
},
_ => bail!("unknown linux power backend"),
}
}
// Execute une commande d'ecran selon le backend choisi.
pub fn execute_screen(backend: &str, value: CommandValue) -> Result<()> {
match backend {
"gnome_busctl" => match value {
CommandValue::Off => run(
"busctl",
&[
"--user",
"set-property",
"org.gnome.Mutter.DisplayConfig",
"/org/gnome/Mutter/DisplayConfig",
"org.gnome.Mutter.DisplayConfig",
"PowerSaveMode",
"i",
"1",
],
),
CommandValue::On => run(
"busctl",
&[
"--user",
"set-property",
"org.gnome.Mutter.DisplayConfig",
"/org/gnome/Mutter/DisplayConfig",
"org.gnome.Mutter.DisplayConfig",
"PowerSaveMode",
"i",
"0",
],
),
},
"x11_xset" => match value {
CommandValue::Off => run("xset", &["dpms", "force", "off"]),
CommandValue::On => run("xset", &["dpms", "force", "on"]),
},
_ => bail!("unknown linux screen backend"),
}
}
fn run(cmd: &str, args: &[&str]) -> Result<()> {
let status = Command::new(cmd)
.args(args)
.status()
.with_context(|| format!("failed to run {cmd}"))?;
if status.success() {
Ok(())
} else {
bail!("command failed: {cmd}")
}
}

View File

@@ -0,0 +1,26 @@
// Ce module selectionne les backends selon l'OS.
// Les sous-modules linux/windows implementent les commandes concretes.
use anyhow::Result;
use crate::commands::{CommandAction, CommandValue};
pub mod linux;
pub mod windows;
// Execute une commande d'alimentation (shutdown/reboot/sleep).
pub fn execute_power(backend: &str, action: CommandAction) -> Result<()> {
if cfg!(target_os = "windows") {
windows::execute_power(backend, action)
} else {
linux::execute_power(backend, action)
}
}
// Execute une commande d'ecran (on/off).
pub fn execute_screen(backend: &str, value: CommandValue) -> Result<()> {
if cfg!(target_os = "windows") {
windows::execute_screen(backend, value)
} else {
linux::execute_screen(backend, value)
}
}

View File

@@ -0,0 +1,27 @@
// Implementations Windows (winapi_session ou external_tool).
use anyhow::{bail, Result};
use tracing::info;
use crate::commands::{CommandAction, CommandValue};
// Stub Windows pour les commandes power (a completer).
pub fn execute_power(backend: &str, action: CommandAction) -> Result<()> {
match backend {
"windows_service" | "winapi_session" | "external_tool" => {
info!(?action, "windows power backend stub");
Ok(())
}
_ => bail!("unknown windows power backend"),
}
}
// Stub Windows pour l'ecran (a completer).
pub fn execute_screen(backend: &str, value: CommandValue) -> Result<()> {
match backend {
"winapi_session" | "external_tool" => {
info!(?value, "windows screen backend stub");
Ok(())
}
_ => bail!("unknown windows screen backend"),
}
}

332
pilot-v2/src/runtime/mod.rs Normal file
View File

@@ -0,0 +1,332 @@
// Ce module orchestre le cycle de vie de l'application.
use anyhow::Result;
use std::time::Instant;
use std::collections::HashMap;
use std::process::Command;
use tokio::time::{interval, sleep, Duration};
use tracing::warn;
use crate::config::Config;
use crate::commands::{self, CommandAction, CommandValue};
use crate::ha;
use crate::mqtt::{self, Backends, Capabilities, Status};
use crate::platform;
use crate::telemetry::{BasicTelemetry, TelemetryProvider};
pub struct Runtime {
config: Config,
start: Instant,
}
impl Runtime {
// Cree un runtime avec la configuration chargee.
pub fn new(config: Config) -> Self {
Self {
config,
start: Instant::now(),
}
}
// Demarre la connexion MQTT et boucle sur l'eventloop.
pub async fn run(self) -> Result<()> {
let handle = mqtt::connect(&self.config)?;
let mut event_loop = handle.event_loop;
let client = handle.client;
// Wait for MQTT connection to be established
loop {
match event_loop.poll().await {
Ok(rumqttc::Event::Incoming(rumqttc::Packet::ConnAck(_))) => {
tracing::info!("mqtt connected");
break;
}
Ok(_) => continue,
Err(err) => {
tracing::warn!(error = %err, "mqtt connection error, retrying...");
sleep(Duration::from_secs(2)).await;
}
}
}
// Spawn event loop handler in background to process messages
let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel();
tokio::spawn(async move {
loop {
match event_loop.poll().await {
Ok(rumqttc::Event::Incoming(rumqttc::Packet::Publish(publish))) => {
let _ = cmd_tx.send((publish.topic.to_string(), publish.payload.to_vec()));
}
Ok(_) => {}
Err(err) => {
tracing::warn!(error = %err, "mqtt eventloop error");
tokio::time::sleep(Duration::from_secs(2)).await;
}
}
}
});
// Send initial messages
if self.config.publish.availability {
mqtt::publish_availability(&client, &self.config, true).await?;
}
let status = build_status(&self.config, self.start.elapsed().as_secs());
mqtt::publish_status(&client, &self.config, &status).await?;
mqtt::publish_capabilities(&client, &self.config, &capabilities(&self.config)).await?;
if let Err(err) = ha::publish_all(&client, &self.config).await {
warn!(error = %err, "ha discovery publish failed");
}
publish_initial_command_states(&client, &self.config).await;
let initial_power_state = detect_power_state();
if let Err(err) = mqtt::publish_state(&client, &self.config, "power_state", &initial_power_state).await {
warn!(error = %err, "publish power_state failed");
}
if self.config.features.commands.enabled {
mqtt::subscribe_commands(&client, &self.config).await?;
}
tracing::info!("entering main event loop");
let mut telemetry = if self.config.features.telemetry.enabled {
Some(BasicTelemetry::new())
} else {
None
};
let mut telemetry_tick = interval(Duration::from_secs(
self.config.features.telemetry.interval_s,
));
let mut heartbeat_tick = interval(Duration::from_secs(
self.config.publish.heartbeat_s,
));
let mut last_exec: HashMap<CommandAction, std::time::Instant> = HashMap::new();
let shutdown = tokio::signal::ctrl_c();
tokio::pin!(shutdown);
loop {
tokio::select! {
_ = telemetry_tick.tick(), if telemetry.is_some() => {
let metrics = telemetry.as_mut().unwrap().read();
for (name, value) in metrics {
if let Err(err) = mqtt::publish_state(&client, &self.config, &name, &value).await {
warn!(error = %err, "publish state failed");
}
}
}
_ = heartbeat_tick.tick() => {
let current = detect_power_state();
if let Err(err) = mqtt::publish_state(&client, &self.config, "power_state", &current).await {
warn!(error = %err, "publish power_state failed");
}
let status = build_status(&self.config, self.start.elapsed().as_secs());
if let Err(err) = mqtt::publish_status(&client, &self.config, &status).await {
warn!(error = %err, "publish status failed");
}
}
Some((topic, payload)) = cmd_rx.recv() => {
if let Err(err) = handle_command(
&client,
&self.config,
&mut last_exec,
&topic,
&payload,
).await {
warn!(error = %err, "command handling failed");
}
}
_ = &mut shutdown => {
if let Err(err) = mqtt::publish_availability(&client, &self.config, false).await {
warn!(error = %err, "publish availability offline failed");
}
if let Err(err) = client.disconnect().await {
warn!(error = %err, "mqtt disconnect failed");
}
break Ok(());
}
}
}
}
}
// Genere les capacites declarees par le programme.
fn capabilities(cfg: &Config) -> Capabilities {
let mut telemetry = Vec::new();
if cfg.features.telemetry.enabled {
telemetry.push("cpu_usage".to_string());
telemetry.push("cpu_temp".to_string());
telemetry.push("memory".to_string());
}
let mut commands = Vec::new();
if cfg.features.commands.enabled {
commands.push("shutdown".to_string());
commands.push("reboot".to_string());
commands.push("sleep".to_string());
commands.push("screen".to_string());
}
Capabilities {
telemetry,
commands,
gpu: false,
}
}
// Retourne le backend power actif selon l'OS.
fn backend_power(cfg: &Config) -> String {
if cfg!(target_os = "windows") {
cfg.power_backend.windows.clone()
} else {
cfg.power_backend.linux.clone()
}
}
// Retourne le backend screen actif selon l'OS.
fn backend_screen(cfg: &Config) -> String {
if cfg!(target_os = "windows") {
cfg.screen_backend.windows.clone()
} else {
cfg.screen_backend.linux.clone()
}
}
// Construit un status stable (version, OS, uptime, backends).
fn build_status(cfg: &Config, uptime_s: u64) -> Status {
Status {
version: "2.0.0".to_string(),
os: std::env::consts::OS.to_string(),
uptime_s,
last_error: String::new(),
backends: Backends {
power: backend_power(cfg),
screen: backend_screen(cfg),
},
}
}
// Essaie de determiner l'etat d'alimentation sur Linux via systemctl.
fn detect_power_state() -> String {
if cfg!(target_os = "windows") {
return "on".to_string();
}
if let Some(state) = detect_power_state_logind() {
return state;
}
match Command::new("systemctl").arg("is-system-running").output() {
Ok(output) => {
let raw = String::from_utf8_lossy(&output.stdout).trim().to_string();
match raw.as_str() {
"running" | "degraded" => "on".to_string(),
"stopping" | "starting" => "unknown".to_string(),
_ => "unknown".to_string(),
}
}
Err(_) => "unknown".to_string(),
}
}
// Essaie de lire l'etat logind (Active + IdleHint).
fn detect_power_state_logind() -> Option<String> {
if let Ok(connection) = zbus::blocking::Connection::system() {
if let Ok(proxy) = zbus::blocking::Proxy::new(
&connection,
"org.freedesktop.login1",
"/org/freedesktop/login1",
"org.freedesktop.login1.Manager",
) {
let active: Result<bool, _> = proxy.get_property("IdleHint").map(|v| v);
if let Ok(idle_hint) = active {
if idle_hint {
return Some("idle".to_string());
}
return Some("on".to_string());
}
}
}
None
}
// Traite une commande entrante (topic + payload) avec cooldown et dry-run.
async fn handle_command(
client: &rumqttc::AsyncClient,
cfg: &Config,
last_exec: &mut HashMap<CommandAction, std::time::Instant>,
topic: &str,
payload: &[u8],
) -> anyhow::Result<()> {
let action = commands::parse_action(topic)?;
let value = commands::parse_value(payload)?;
if !commands::allowlist_allows(&cfg.features.commands.allowlist, action) {
return Ok(());
}
if !commands::allow_command(last_exec, cfg.features.commands.cooldown_s, action) {
return Ok(());
}
if cfg.features.commands.dry_run {
commands::execute_dry_run(action, value)?;
publish_command_state(client, cfg, action, value).await?;
return Ok(());
}
match action {
CommandAction::Shutdown => {
if matches!(value, CommandValue::Off) {
platform::execute_power(&backend_power(cfg), action)?;
mqtt::publish_state(client, cfg, "power_state", "off").await?;
publish_command_state(client, cfg, action, value).await?;
}
}
CommandAction::Reboot => {
if matches!(value, CommandValue::Off) {
platform::execute_power(&backend_power(cfg), action)?;
mqtt::publish_state(client, cfg, "power_state", "on").await?;
publish_command_state(client, cfg, action, value).await?;
}
}
CommandAction::Sleep => {
if matches!(value, CommandValue::Off) {
platform::execute_power(&backend_power(cfg), action)?;
mqtt::publish_state(client, cfg, "power_state", "sleep").await?;
publish_command_state(client, cfg, action, value).await?;
}
}
CommandAction::Screen => {
platform::execute_screen(&backend_screen(cfg), value)?;
publish_command_state(client, cfg, action, value).await?;
}
}
Ok(())
}
// Publie l'etat initial des switches HA (par defaut ON).
async fn publish_initial_command_states(client: &rumqttc::AsyncClient, cfg: &Config) {
let _ = mqtt::publish_state(client, cfg, "shutdown", "ON").await;
let _ = mqtt::publish_state(client, cfg, "reboot", "ON").await;
let _ = mqtt::publish_state(client, cfg, "sleep", "ON").await;
let _ = mqtt::publish_state(client, cfg, "screen", "ON").await;
}
// Publie l'etat d'une commande pour Home Assistant.
async fn publish_command_state(
client: &rumqttc::AsyncClient,
cfg: &Config,
action: CommandAction,
value: CommandValue,
) -> anyhow::Result<()> {
let state = match value {
CommandValue::On => "ON",
CommandValue::Off => "OFF",
};
let name = commands::action_name(action);
mqtt::publish_state(client, cfg, name, state).await
}

View File

@@ -0,0 +1 @@
// Fonctions de securite futures (HMAC, signatures, ACL).

View File

@@ -0,0 +1,132 @@
// Ce module declare l'interface de telemetrie et une implementation basique.
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use local_ip_address::local_ip;
use sysinfo::System;
// Retourne un dictionnaire simple {nom -> valeur} pour les capteurs.
pub trait TelemetryProvider {
fn read(&mut self) -> HashMap<String, String>;
}
// Telemetrie basique cross-platform (cpu, memoire, ip, batterie).
pub struct BasicTelemetry {
system: System,
}
impl BasicTelemetry {
// Initialise le collecteur systeme.
pub fn new() -> Self {
let mut system = System::new();
system.refresh_all();
Self { system }
}
}
impl TelemetryProvider for BasicTelemetry {
fn read(&mut self) -> HashMap<String, String> {
self.system.refresh_cpu();
self.system.refresh_memory();
let mut values = HashMap::new();
let cpu = self.system.global_cpu_info().cpu_usage();
let mem_used_mb = self.system.used_memory() / 1024;
let mem_total_mb = self.system.total_memory() / 1024;
values.insert("cpu_usage".to_string(), format!("{:.1}", cpu));
values.insert("memory_used_mb".to_string(), mem_used_mb.to_string());
values.insert("memory_total_mb".to_string(), mem_total_mb.to_string());
if let Ok(ip) = local_ip() {
values.insert("ip_address".to_string(), ip.to_string());
}
// Add battery info if available
if let Some(battery) = read_battery_info() {
values.insert("battery_level".to_string(), battery.level.to_string());
values.insert("battery_state".to_string(), battery.state);
}
values
}
}
#[derive(Debug)]
struct BatteryInfo {
level: u8, // 0-100
state: String, // "charging", "discharging", "full", "unknown"
}
// Lit les informations de batterie depuis /sys/class/power_supply (Linux)
fn read_battery_info() -> Option<BatteryInfo> {
#[cfg(target_os = "linux")]
{
read_battery_linux()
}
#[cfg(target_os = "windows")]
{
read_battery_windows()
}
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
{
None
}
}
#[cfg(target_os = "linux")]
fn read_battery_linux() -> Option<BatteryInfo> {
let power_supply_path = Path::new("/sys/class/power_supply");
if !power_supply_path.exists() {
return None;
}
// Find first battery device (BAT0, BAT1, or battery)
let battery_dirs = fs::read_dir(power_supply_path).ok()?;
for entry in battery_dirs.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with("BAT") || name_str == "battery" {
let bat_path = entry.path();
// Read capacity (0-100)
let capacity = fs::read_to_string(bat_path.join("capacity"))
.ok()?
.trim()
.parse::<u8>()
.ok()?;
// Read status
let status = fs::read_to_string(bat_path.join("status"))
.ok()?
.trim()
.to_lowercase();
let state = match status.as_str() {
"charging" => "charging".to_string(),
"discharging" => "discharging".to_string(),
"full" => "full".to_string(),
"not charging" => "not_charging".to_string(),
_ => "unknown".to_string(),
};
return Some(BatteryInfo {
level: capacity,
state,
});
}
}
None
}
#[cfg(target_os = "windows")]
fn read_battery_windows() -> Option<BatteryInfo> {
// TODO: Implement Windows battery reading via GetSystemPowerStatus
// For now, return None
None
}

View File

@@ -0,0 +1 @@
{"rustc_fingerprint":14304282315022827685,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.92.0 (ded5c06cf 2025-12-08)\nbinary: rustc\ncommit-hash: ded5c06cf21d2b93bffd5d884aa6e96934ee4234\ncommit-date: 2025-12-08\nhost: x86_64-unknown-linux-gnu\nrelease: 1.92.0\nLLVM version: 21.1.3\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/gilles/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}

View File

@@ -0,0 +1 @@
{"rustc_vv":"rustc 1.92.0 (ded5c06cf 2025-12-08)\nbinary: rustc\ncommit-hash: ded5c06cf21d2b93bffd5d884aa6e96934ee4234\ncommit-date: 2025-12-08\nhost: x86_64-unknown-linux-gnu\nrelease: 1.92.0\nLLVM version: 21.1.3\n"}

View File

@@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

View File

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
d0c54af552649cc6

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":2225463790103693989,"path":2697853566079511613,"deps":[[198136567835728122,"memchr",false,14442475621410761341]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aho-corasick-f56eb701d1d265c8/dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[1852463361802237065,"build_script_build",false,2620827986237482161]],"local":[{"RerunIfChanged":{"output":"debug/build/anyhow-7dffd08ca73f1c01/output","paths":["src/nightly.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":[],"config":0,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
71cddc83508d9fb9

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":16100955855663461252,"profile":2241668132362809309,"path":15748188269771136802,"deps":[[1852463361802237065,"build_script_build",false,6040597542066670943]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-a8e29080dfa88f6a/dep-lib-anyhow","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":17883862002600103897,"profile":2225463790103693989,"path":11298574807048988049,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-f0f8ac34947eb6de/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
58ae41729d808c2a

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":16100955855663461252,"profile":15657897354478470176,"path":15748188269771136802,"deps":[[1852463361802237065,"build_script_build",false,6040597542066670943]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/anyhow-f83b8d3e8e6fcc5b/dep-lib-anyhow","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
4483bcdbbe950895

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":14946317168266388427,"profile":15657897354478470176,"path":4414353296763948497,"deps":[[1464803193346256239,"event_listener",false,15480104919079411986],[7620660491849607393,"futures_core",false,14806556747044414787]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-broadcast-96cd5d92478f62ce/dep-lib-async_broadcast","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
ebf9ddc1043cc9b6

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":14946317168266388427,"profile":2241668132362809309,"path":4414353296763948497,"deps":[[1464803193346256239,"event_listener",false,6817172485043742015],[7620660491849607393,"futures_core",false,8459390063132964111]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-broadcast-ed4fb2f1bd311559/dep-lib-async_broadcast","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
e9346414c52f9df2

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"portable-atomic\", \"std\"]","target":2348331682808714104,"profile":15657897354478470176,"path":11422452653132114092,"deps":[[1906322745568073236,"pin_project_lite",false,8991253654115275928],[7620660491849607393,"futures_core",false,14806556747044414787],[12100481297174703255,"concurrent_queue",false,16848644008424325506],[17148897597675491682,"event_listener_strategy",false,17280328617240379150]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-channel-86c3c96ca9a3e4c4/dep-lib-async_channel","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
dd86fdc7c107195d

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"portable-atomic\", \"std\"]","target":2348331682808714104,"profile":2241668132362809309,"path":11422452653132114092,"deps":[[1906322745568073236,"pin_project_lite",false,3550369563450963358],[7620660491849607393,"futures_core",false,8459390063132964111],[12100481297174703255,"concurrent_queue",false,4102932450197136801],[17148897597675491682,"event_listener_strategy",false,5354340854549327226]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-channel-e91f434cc5bde120/dep-lib-async_channel","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
2c11f4d5bc76d548

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[\"static\"]","target":7483652822946339806,"profile":2241668132362809309,"path":3112205183626101043,"deps":[[867502981669738401,"async_task",false,16343854254514468088],[1906322745568073236,"pin_project_lite",false,3550369563450963358],[9090520973410485560,"futures_lite",false,11145987570875990860],[12100481297174703255,"concurrent_queue",false,4102932450197136801],[12285238697122577036,"fastrand",false,8347313827937685988],[14767213526276824509,"slab",false,10354343093452352864]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-executor-901f60802cc7cf0d/dep-lib-async_executor","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
6c5f9465a8f5c627

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[\"static\"]","target":7483652822946339806,"profile":15657897354478470176,"path":3112205183626101043,"deps":[[867502981669738401,"async_task",false,1082350877060955153],[1906322745568073236,"pin_project_lite",false,8991253654115275928],[9090520973410485560,"futures_lite",false,7501930813355833590],[12100481297174703255,"concurrent_queue",false,16848644008424325506],[12285238697122577036,"fastrand",false,16856789532386053895],[14767213526276824509,"slab",false,9338198606256010886]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-executor-f15b8cf4d2656fdf/dep-lib-async_executor","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":17883862002600103897,"profile":2225463790103693989,"path":16311120052012549081,"deps":[[13927012481677012980,"autocfg",false,4491350997734096854]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-fs-0fc57937ef2ca20d/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
73aaca22067421c5

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":13530298058224660176,"profile":2241668132362809309,"path":7810507604688103693,"deps":[[7208080732687383809,"async_lock",false,3120658934024652719],[9570980159325712564,"futures_lite",false,2801693992710442923],[11099682918945173275,"blocking",false,1558806162020186034],[17415156283097623665,"build_script_build",false,5620434700617653819]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-fs-226cd1aa13a861ef/dep-lib-async_fs","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
257328d007fa2ac5

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":13530298058224660176,"profile":15657897354478470176,"path":7810507604688103693,"deps":[[7208080732687383809,"async_lock",false,9875250485109216967],[9570980159325712564,"futures_lite",false,12762884056246454962],[11099682918945173275,"blocking",false,8648931100631883446],[17415156283097623665,"build_script_build",false,5620434700617653819]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-fs-3ea0bd7facca6ae4/dep-lib-async_fs","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[17415156283097623665,"build_script_build",false,17662751843603552224]],"local":[{"Precalculated":"1.6.0"}],"rustflags":[],"config":0,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
c1e6c7b1680a3102

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":13601420042805913294,"profile":2241668132362809309,"path":15080696096402417374,"deps":[[189982446159473706,"parking",false,4296536885862228987],[1211321333142909612,"socket2",false,15328149775485923740],[6246679968272628950,"rustix",false,4306342092927746802],[7208080732687383809,"async_lock",false,3120658934024652719],[7667230146095136825,"cfg_if",false,2379222946250249096],[8864093321401338808,"waker_fn",false,6754656470642932160],[9570980159325712564,"futures_lite",false,2801693992710442923],[10166384453965283024,"polling",false,16092961559541005391],[10630857666389190470,"log",false,5080065308326131370],[12100481297174703255,"concurrent_queue",false,4102932450197136801],[12914622799526586510,"build_script_build",false,11966104318495890957],[14767213526276824509,"slab",false,10354343093452352864]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-io-5b17f2e9cedef537/dep-lib-async_io","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[12914622799526586510,"build_script_build",false,6751964766134238648]],"local":[{"Precalculated":"1.13.0"}],"rustflags":[],"config":0,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
0237cc00cdb079f0

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":13601420042805913294,"profile":15657897354478470176,"path":15080696096402417374,"deps":[[189982446159473706,"parking",false,12450303803023399652],[1211321333142909612,"socket2",false,3755417887275652736],[6246679968272628950,"rustix",false,18147481592832568662],[7208080732687383809,"async_lock",false,9875250485109216967],[7667230146095136825,"cfg_if",false,4127000677558031520],[8864093321401338808,"waker_fn",false,7128209248171288251],[9570980159325712564,"futures_lite",false,12762884056246454962],[10166384453965283024,"polling",false,9970362139234296328],[10630857666389190470,"log",false,12461827721875662956],[12100481297174703255,"concurrent_queue",false,16848644008424325506],[12914622799526586510,"build_script_build",false,11966104318495890957],[14767213526276824509,"slab",false,9338198606256010886]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-io-c29a4c449a4f1a74/dep-lib-async_io","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":17883862002600103897,"profile":2225463790103693989,"path":10704244322782859744,"deps":[[13927012481677012980,"autocfg",false,4491350997734096854]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-io-efada4b51bbaa2f4/dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
c756f0b0feef0b89

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":4213861256432978679,"profile":15657897354478470176,"path":2858335931736234219,"deps":[[1464803193346256239,"event_listener",false,15480104919079411986]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-lock-4d7615b53c137535/dep-lib-async_lock","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
af47b03bc4ce4e2b

View File

@@ -0,0 +1 @@
{"rustc":4758242423518056681,"features":"[]","declared_features":"[]","target":4213861256432978679,"profile":2241668132362809309,"path":2858335931736234219,"deps":[[1464803193346256239,"event_listener",false,6817172485043742015]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/async-lock-b68da8ab013ff246/dep-lib-async_lock","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}

View File

@@ -0,0 +1 @@
This file has an mtime of when this was started.

View File

@@ -0,0 +1 @@
138119a117138926

Some files were not shown because too many files have changed in this diff Show More