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:
2206
pilot-v2/Cargo.lock
generated
Normal file
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
19
pilot-v2/Cargo.toml
Normal 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
43
pilot-v2/config.yaml
Normal 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
|
||||
129
pilot-v2/src/commands/mod.rs
Normal file
129
pilot-v2/src/commands/mod.rs
Normal 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
183
pilot-v2/src/config/mod.rs
Normal 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
115
pilot-v2/src/ha/mod.rs
Normal 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
9
pilot-v2/src/lib.rs
Normal 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
19
pilot-v2/src/main.rs
Normal 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
132
pilot-v2/src/mqtt/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
75
pilot-v2/src/platform/linux/mod.rs
Normal file
75
pilot-v2/src/platform/linux/mod.rs
Normal 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}")
|
||||
}
|
||||
}
|
||||
26
pilot-v2/src/platform/mod.rs
Normal file
26
pilot-v2/src/platform/mod.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
27
pilot-v2/src/platform/windows/mod.rs
Normal file
27
pilot-v2/src/platform/windows/mod.rs
Normal 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
332
pilot-v2/src/runtime/mod.rs
Normal 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", ¤t).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
|
||||
}
|
||||
1
pilot-v2/src/security/mod.rs
Normal file
1
pilot-v2/src/security/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
// Fonctions de securite futures (HMAC, signatures, ACL).
|
||||
132
pilot-v2/src/telemetry/mod.rs
Normal file
132
pilot-v2/src/telemetry/mod.rs
Normal 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
|
||||
}
|
||||
1
pilot-v2/target/.rustc_info.json
Normal file
1
pilot-v2/target/.rustc_info.json
Normal 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":{}}
|
||||
1
pilot-v2/target/.rustdoc_fingerprint.json
Normal file
1
pilot-v2/target/.rustdoc_fingerprint.json
Normal 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"}
|
||||
3
pilot-v2/target/CACHEDIR.TAG
Normal file
3
pilot-v2/target/CACHEDIR.TAG
Normal 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/
|
||||
0
pilot-v2/target/debug/.cargo-lock
Normal file
0
pilot-v2/target/debug/.cargo-lock
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
d0c54af552649cc6
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
5f4dfbc57583d453
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
71cddc83508d9fb9
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
b16416f12a0d5f24
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
58ae41729d808c2a
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
4483bcdbbe950895
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
ebf9ddc1043cc9b6
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
e9346414c52f9df2
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
dd86fdc7c107195d
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
2c11f4d5bc76d548
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
6c5f9465a8f5c627
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
e097dd7138b31ef5
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
73aaca22067421c5
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
257328d007fa2ac5
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
3b1e98f594cbff4d
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
c1e6c7b1680a3102
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
0d1e37817a2410a6
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
0237cc00cdb079f0
|
||||
@@ -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}
|
||||
@@ -0,0 +1 @@
|
||||
b87d626d2eccb35d
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
c756f0b0feef0b89
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
af47b03bc4ce4e2b
|
||||
@@ -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}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
138119a117138926
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user