first commit
This commit is contained in:
24
linux-hello-cli/Cargo.toml
Normal file
24
linux-hello-cli/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "linux-hello-cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Linux Hello command-line interface"
|
||||
|
||||
[[bin]]
|
||||
name = "linux-hello"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
linux-hello-common = { path = "../linux-hello-common" }
|
||||
linux-hello-daemon = { path = "../linux-hello-daemon" }
|
||||
|
||||
thiserror.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
toml.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tokio.workspace = true
|
||||
clap.workspace = true
|
||||
image.workspace = true
|
||||
373
linux-hello-cli/src/main.rs
Normal file
373
linux-hello-cli/src/main.rs
Normal file
@@ -0,0 +1,373 @@
|
||||
//! Linux Hello CLI
|
||||
//!
|
||||
//! Command-line interface for enrollment, testing, and diagnostics.
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use linux_hello_common::{Config, Result};
|
||||
use tracing::{info, Level};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "linux-hello")]
|
||||
#[command(author, version, about = "Linux Hello facial authentication", long_about = None)]
|
||||
struct Cli {
|
||||
/// Enable verbose output
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Capture frames from IR camera (for testing)
|
||||
Capture {
|
||||
/// Output directory for captured frames
|
||||
#[arg(short, long, default_value = ".")]
|
||||
output: String,
|
||||
|
||||
/// Number of frames to capture
|
||||
#[arg(short, long, default_value = "5")]
|
||||
count: u32,
|
||||
|
||||
/// Camera device path (auto-detect if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
},
|
||||
|
||||
/// Test face detection on camera or image
|
||||
Detect {
|
||||
/// Image file to process (use camera if not specified)
|
||||
#[arg(short, long)]
|
||||
image: Option<String>,
|
||||
|
||||
/// Show detection confidence scores
|
||||
#[arg(long)]
|
||||
scores: bool,
|
||||
|
||||
/// Save annotated output image
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
},
|
||||
|
||||
/// Show system status
|
||||
Status {
|
||||
/// Include camera diagnostics
|
||||
#[arg(long)]
|
||||
camera: bool,
|
||||
|
||||
/// Include daemon status
|
||||
#[arg(long)]
|
||||
daemon: bool,
|
||||
},
|
||||
|
||||
/// Enroll a face model
|
||||
Enroll {
|
||||
/// Label for this enrollment
|
||||
#[arg(short, long, default_value = "default")]
|
||||
label: String,
|
||||
},
|
||||
|
||||
/// List enrolled face models
|
||||
List,
|
||||
|
||||
/// Remove enrolled face models
|
||||
Remove {
|
||||
/// Label to remove (or --all for all)
|
||||
label: Option<String>,
|
||||
|
||||
/// Remove all enrolled models
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
},
|
||||
|
||||
/// Test authentication
|
||||
Test {
|
||||
/// Show detailed confidence scores
|
||||
#[arg(long)]
|
||||
verbose: bool,
|
||||
|
||||
/// Save debug information
|
||||
#[arg(long)]
|
||||
debug: bool,
|
||||
},
|
||||
|
||||
/// Show or modify configuration
|
||||
Config {
|
||||
/// Show config as JSON
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
|
||||
/// Set a configuration value (key=value)
|
||||
#[arg(long)]
|
||||
set: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Initialize logging
|
||||
let log_level = if cli.verbose { Level::DEBUG } else { Level::INFO };
|
||||
FmtSubscriber::builder()
|
||||
.with_max_level(log_level)
|
||||
.with_target(false)
|
||||
.init();
|
||||
|
||||
// Load configuration
|
||||
let config = Config::load_or_default();
|
||||
|
||||
match cli.command {
|
||||
Commands::Capture { output, count, device } => {
|
||||
cmd_capture(&config, &output, count, device.as_deref()).await
|
||||
}
|
||||
Commands::Detect { image, scores, output } => {
|
||||
cmd_detect(&config, image.as_deref(), scores, output.as_deref()).await
|
||||
}
|
||||
Commands::Status { camera, daemon } => {
|
||||
cmd_status(&config, camera, daemon).await
|
||||
}
|
||||
Commands::Enroll { label } => {
|
||||
cmd_enroll(&config, &label).await
|
||||
}
|
||||
Commands::List => {
|
||||
cmd_list(&config).await
|
||||
}
|
||||
Commands::Remove { label, all } => {
|
||||
cmd_remove(&config, label.as_deref(), all).await
|
||||
}
|
||||
Commands::Test { verbose, debug } => {
|
||||
cmd_test(&config, verbose, debug).await
|
||||
}
|
||||
Commands::Config { json, set } => {
|
||||
cmd_config(&config, json, set.as_deref()).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cmd_capture(
|
||||
_config: &Config,
|
||||
output: &str,
|
||||
count: u32,
|
||||
device: Option<&str>,
|
||||
) -> Result<()> {
|
||||
info!("Capturing {} frames to {}", count, output);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
use linux_hello_daemon::camera::{enumerate_cameras, Camera};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use linux_hello_daemon::camera::{enumerate_cameras, Camera};
|
||||
|
||||
use linux_hello_daemon::PixelFormat;
|
||||
|
||||
// Find camera
|
||||
let device_path = match device {
|
||||
Some(d) => d.to_string(),
|
||||
None => {
|
||||
let cameras = enumerate_cameras()?;
|
||||
let ir_cam = cameras.iter().find(|c| c.is_ir);
|
||||
let cam = ir_cam.or(cameras.first());
|
||||
|
||||
match cam {
|
||||
Some(c) => {
|
||||
info!("Using camera: {}", c);
|
||||
c.device_path.clone()
|
||||
}
|
||||
None => {
|
||||
return Err(linux_hello_common::Error::NoCameraFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Open camera and capture
|
||||
let mut camera = Camera::open(&device_path)?;
|
||||
info!("Camera opened, resolution: {:?}", camera.resolution());
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
std::fs::create_dir_all(output)?;
|
||||
|
||||
for i in 0..count {
|
||||
let frame = camera.capture_frame()?;
|
||||
|
||||
match frame.format {
|
||||
PixelFormat::Grey => {
|
||||
let filename = format!("{}/frame_{:03}.png", output, i);
|
||||
if let Some(img) = image::GrayImage::from_raw(frame.width, frame.height, frame.data) {
|
||||
img.save(&filename).map_err(|e| linux_hello_common::Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e)))?;
|
||||
info!("Saved frame {} (Permissions: Grey)", filename);
|
||||
}
|
||||
}
|
||||
PixelFormat::Yuyv => {
|
||||
// TODO: Convert YUYV to RGB/Gray
|
||||
let filename = format!("{}/frame_{:03}.raw", output, i);
|
||||
std::fs::write(&filename, &frame.data)?;
|
||||
info!("Saved frame {} ({} bytes - Raw YUYV)", filename, frame.data.len());
|
||||
}
|
||||
_ => {
|
||||
let filename = format!("{}/frame_{:03}.raw", output, i);
|
||||
std::fs::write(&filename, &frame.data)?;
|
||||
info!("Saved frame {} ({} bytes - Unknown format)", filename, frame.data.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Capture complete");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_detect(
|
||||
_config: &Config,
|
||||
image: Option<&str>,
|
||||
show_scores: bool,
|
||||
output: Option<&str>,
|
||||
) -> Result<()> {
|
||||
match image {
|
||||
Some(path) => {
|
||||
info!("Detecting faces in: {}", path);
|
||||
|
||||
// Load image
|
||||
let img = image::open(path)
|
||||
.map_err(|e| linux_hello_common::Error::Detection(e.to_string()))?;
|
||||
|
||||
let gray = img.to_luma8();
|
||||
let (width, height) = gray.dimensions();
|
||||
|
||||
// Run simple detection (placeholder)
|
||||
use linux_hello_daemon::detection::detect_face_simple;
|
||||
let detection = detect_face_simple(gray.as_raw(), width, height);
|
||||
|
||||
match detection {
|
||||
Some(det) => {
|
||||
let (x, y, w, h) = det.to_pixels(width, height);
|
||||
println!("Face detected:");
|
||||
println!(" Position: ({}, {})", x, y);
|
||||
println!(" Size: {}x{}", w, h);
|
||||
if show_scores {
|
||||
println!(" Confidence: {:.2}%", det.confidence * 100.0);
|
||||
}
|
||||
|
||||
if let Some(out_path) = output {
|
||||
info!("Saving annotated image to: {}", out_path);
|
||||
// TODO: Draw bounding box and save
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("No face detected");
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
println!("Live camera detection not yet implemented");
|
||||
println!("Use --image to detect in an image file");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_status(config: &Config, show_camera: bool, show_daemon: bool) -> Result<()> {
|
||||
println!("Linux Hello Status");
|
||||
println!("==================");
|
||||
|
||||
if show_camera || (!show_camera && !show_daemon) {
|
||||
println!("\nCamera:");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use linux_hello_daemon::camera::enumerate_cameras;
|
||||
match enumerate_cameras() {
|
||||
Ok(cameras) => {
|
||||
if cameras.is_empty() {
|
||||
println!(" No cameras found");
|
||||
} else {
|
||||
for cam in cameras {
|
||||
println!(" - {}", cam);
|
||||
if !cam.resolutions.is_empty() {
|
||||
println!(
|
||||
" Resolutions: {:?}",
|
||||
cam.resolutions.iter().take(3).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
println!(" Camera detection requires Linux");
|
||||
}
|
||||
|
||||
if show_daemon || (!show_camera && !show_daemon) {
|
||||
println!("\nDaemon:");
|
||||
// TODO: Check daemon via D-Bus
|
||||
println!(" Status: Unknown (D-Bus not implemented)");
|
||||
}
|
||||
|
||||
println!("\nConfiguration:");
|
||||
println!(" Detection model: {}", config.detection.model);
|
||||
println!(" Anti-spoofing: {}", if config.anti_spoofing.enabled { "enabled" } else { "disabled" });
|
||||
println!(" TPM: {}", if config.tpm.enabled { "enabled" } else { "disabled" });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_enroll(_config: &Config, label: &str) -> Result<()> {
|
||||
println!("Enrollment not yet implemented");
|
||||
println!("Would enroll with label: {}", label);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_list(_config: &Config) -> Result<()> {
|
||||
println!("Enrolled models:");
|
||||
println!(" (none - enrollment not yet implemented)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_remove(_config: &Config, label: Option<&str>, all: bool) -> Result<()> {
|
||||
if all {
|
||||
println!("Would remove ALL enrolled models");
|
||||
} else if let Some(l) = label {
|
||||
println!("Would remove model: {}", l);
|
||||
} else {
|
||||
println!("Specify a label or --all");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_test(_config: &Config, verbose: bool, debug: bool) -> Result<()> {
|
||||
println!("Authentication test not yet implemented");
|
||||
if verbose {
|
||||
println!(" (would show confidence scores)");
|
||||
}
|
||||
if debug {
|
||||
println!(" (would save debug frames)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cmd_config(config: &Config, json: bool, set: Option<&str>) -> Result<()> {
|
||||
if let Some(kv) = set {
|
||||
println!("Setting config values not yet implemented: {}", kv);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if json {
|
||||
let json = serde_json::to_string_pretty(config)
|
||||
.map_err(|e| linux_hello_common::Error::Serialization(e.to_string()))?;
|
||||
println!("{}", json);
|
||||
} else {
|
||||
let toml = toml::to_string_pretty(config)
|
||||
.map_err(|e| linux_hello_common::Error::Serialization(e.to_string()))?;
|
||||
println!("{}", toml);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user