use aingle_minimal::{Config, MinimalNode, Result};
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Parser)]
#[command(name = "aingle-minimal")]
#[command(author = "Apilium Technologies OÜ <hello@apilium.com>")]
#[command(version, long_version = concat!(
env!("CARGO_PKG_VERSION"), "\n",
"Copyright 2019-2026 Apilium Technologies OÜ\n",
"License: Apache-2.0 OR Commercial\n",
"https://github.com/ApiliumCode/aingle"
))]
#[command(about = "Ultra-light AIngle node for IoT devices (<1MB RAM)", long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[arg(short, long, default_value = "info", global = true)]
log_level: String,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Run {
#[arg(long)]
iot: bool,
#[arg(long)]
low_power: bool,
#[arg(short, long)]
publish_interval: Option<u64>,
#[arg(short, long)]
memory_limit: Option<usize>,
#[arg(short, long, default_value = "0.0.0.0")]
bind_addr: String,
#[arg(short = 'P', long, default_value = "5683")]
port: u16,
#[arg(long)]
peer: Vec<String>,
#[arg(long)]
mdns: bool,
#[arg(short, long)]
db_path: Option<PathBuf>,
#[arg(long)]
rest_port: Option<u16>,
},
Keygen {
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long, default_value = "hex")]
format: String,
},
Info,
Config {
#[command(subcommand)]
action: ConfigAction,
},
Version,
Bench {
#[arg(short, long, default_value = "100")]
entries: usize,
#[arg(short, long, default_value = "1")]
iterations: usize,
},
}
#[derive(Subcommand)]
enum ConfigAction {
Show,
Iot,
LowPower,
Validate,
}
fn main() -> Result<()> {
let cli = Cli::parse();
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(&cli.log_level))
.format_timestamp_millis()
.init();
match cli.command {
Some(Commands::Run {
iot,
low_power,
publish_interval,
memory_limit,
bind_addr,
port,
peer,
mdns,
db_path,
rest_port,
}) => run_node(
iot,
low_power,
publish_interval,
memory_limit,
bind_addr,
port,
peer,
mdns,
db_path,
rest_port,
),
Some(Commands::Keygen { output, format }) => keygen(output, format),
Some(Commands::Info) => show_info(),
Some(Commands::Config { action }) => config_action(action),
Some(Commands::Version) => show_version(),
Some(Commands::Bench {
entries,
iterations,
}) => run_benchmark(entries, iterations),
None => {
print_banner();
println!("Use 'aingle-minimal --help' for usage information.");
println!("Use 'aingle-minimal run' to start the node.");
Ok(())
}
}
}
fn print_banner() {
println!(
r#"
╔═══════════════════════════════════════════════════════════╗
║ █████╗ ██╗███╗ ██╗ ██████╗ ██╗ ███████╗ ║
║ ██╔══██╗██║████╗ ██║██╔════╝ ██║ ██╔════╝ ║
║ ███████║██║██╔██╗ ██║██║ ███╗██║ █████╗ ║
║ ██╔══██║██║██║╚██╗██║██║ ██║██║ ██╔══╝ ║
║ ██║ ██║██║██║ ╚████║╚██████╔╝███████╗███████╗ ║
║ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ║
║ ║
║ MINIMAL NODE - IoT Edition ║
║ Version {} ║
╚═══════════════════════════════════════════════════════════╝
"#,
aingle_minimal::VERSION
);
}
#[allow(clippy::too_many_arguments)]
fn run_node(
iot: bool,
low_power: bool,
publish_interval: Option<u64>,
memory_limit: Option<usize>,
bind_addr: String,
port: u16,
peers: Vec<String>,
mdns: bool,
db_path: Option<PathBuf>,
rest_port: Option<u16>,
) -> Result<()> {
print_banner();
let mut config = if iot {
println!("Mode: IoT (sub-second confirmation)");
Config::iot_mode()
} else if low_power {
println!("Mode: Low Power");
Config::low_power()
} else {
println!("Mode: Standard");
Config::default()
};
if let Some(interval) = publish_interval {
config.publish_interval = Duration::from_millis(interval);
}
if let Some(limit) = memory_limit {
config.memory_limit = limit * 1024;
}
if mdns {
config.enable_mdns = true;
}
if let Some(path) = db_path {
config.storage.db_path = path.to_string_lossy().to_string();
}
config.transport = aingle_minimal::TransportConfig::Coap { bind_addr, port };
println!("\nConfiguration:");
println!(" Publish interval: {:?}", config.publish_interval);
println!(" Power mode: {:?}", config.power_mode);
println!(" Memory limit: {} KB", config.memory_limit / 1024);
println!(" Gossip delay: {:?}", config.gossip.loop_delay);
println!(" mDNS discovery: {}", config.enable_mdns);
println!(" Storage: {}", config.storage.db_path);
println!();
let mut node = MinimalNode::new(config)?;
for peer_addr in peers {
match peer_addr.parse() {
Ok(addr) => {
node.add_peer(addr);
println!("Added peer: {}", peer_addr);
}
Err(e) => {
eprintln!("Invalid peer address '{}': {}", peer_addr, e);
}
}
}
println!("\nNode public key: {}", node.public_key().to_hex());
println!("\nStarting node...");
println!("Press Ctrl+C to stop\n");
setup_ctrlc_handler();
#[cfg(feature = "rest")]
{
if let Some(rest_port) = rest_port {
use aingle_minimal::rest::{RestConfig, RestServer};
use std::sync::{Arc, Mutex};
let rest_config = RestConfig::with_port(rest_port);
println!("REST API server: http://0.0.0.0:{}", rest_port);
println!(" GET /api/v1/info - Node information");
println!(" POST /api/v1/entries - Create entry");
println!(" GET /api/v1/entries/:hash - Get entry");
println!(" GET /api/v1/peers - List peers");
println!(" GET /api/v1/stats - Statistics");
println!();
let node_arc = Arc::new(Mutex::new(node));
let _server = RestServer::start_with_node(rest_config, Arc::clone(&node_arc))?;
loop {
{
let node_guard = match node_arc.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
if !node_guard.is_running() {
break;
}
}
std::thread::sleep(Duration::from_millis(100));
}
} else {
smol::block_on(node.run())?;
}
}
#[cfg(not(feature = "rest"))]
{
if rest_port.is_some() {
eprintln!("Warning: REST API requested but 'rest' feature not enabled");
eprintln!("Rebuild with: cargo build --features rest");
}
smol::block_on(node.run())?;
}
Ok(())
}
fn keygen(output: Option<PathBuf>, format: String) -> Result<()> {
use aingle_minimal::crypto::Keypair;
let keypair = Keypair::generate();
let pubkey = keypair.public_key();
let pubkey_str = match format.as_str() {
"base64" => base64_encode(pubkey.as_bytes()),
_ => pubkey.to_hex(),
};
let output_text = format!(
"Public Key: {}\nFormat: {}\n\nNote: Private key is not exported for security.",
pubkey_str, format
);
if let Some(path) = output {
std::fs::write(&path, &output_text)?;
println!("Keypair info written to: {}", path.display());
} else {
println!("{}", output_text);
}
Ok(())
}
fn show_info() -> Result<()> {
print_banner();
println!("Build Information:");
println!(" Version: {}", aingle_minimal::VERSION);
println!(" MSRV: {}", aingle_minimal::MSRV);
println!(
" Memory budget: {} KB",
aingle_minimal::MEMORY_BUDGET / 1024
);
println!();
println!("Features:");
#[cfg(feature = "sqlite")]
println!(" [x] SQLite storage");
#[cfg(not(feature = "sqlite"))]
println!(" [ ] SQLite storage");
#[cfg(feature = "rocksdb")]
println!(" [x] RocksDB storage");
#[cfg(not(feature = "rocksdb"))]
println!(" [ ] RocksDB storage");
#[cfg(feature = "coap")]
println!(" [x] CoAP transport");
#[cfg(not(feature = "coap"))]
println!(" [ ] CoAP transport");
#[cfg(feature = "quic")]
println!(" [x] QUIC transport");
#[cfg(not(feature = "quic"))]
println!(" [ ] QUIC transport");
#[cfg(feature = "webrtc")]
println!(" [x] WebRTC transport");
#[cfg(not(feature = "webrtc"))]
println!(" [ ] WebRTC transport");
#[cfg(feature = "ble")]
println!(" [x] Bluetooth LE");
#[cfg(not(feature = "ble"))]
println!(" [ ] Bluetooth LE");
#[cfg(feature = "hw_wallet")]
println!(" [x] Hardware wallet");
#[cfg(not(feature = "hw_wallet"))]
println!(" [ ] Hardware wallet");
#[cfg(feature = "smart_agents")]
println!(" [x] Smart agents (Kaneru)");
#[cfg(not(feature = "smart_agents"))]
println!(" [ ] Smart agents (Kaneru)");
#[cfg(feature = "ai_memory")]
println!(" [x] AI memory (Titans)");
#[cfg(not(feature = "ai_memory"))]
println!(" [ ] AI memory (Titans)");
println!();
Ok(())
}
fn config_action(action: ConfigAction) -> Result<()> {
match action {
ConfigAction::Show => {
let config = Config::default();
print_config(&config);
}
ConfigAction::Iot => {
let config = Config::iot_mode();
println!("IoT Mode Configuration:\n");
print_config(&config);
}
ConfigAction::LowPower => {
let config = Config::low_power();
println!("Low Power Mode Configuration:\n");
print_config(&config);
}
ConfigAction::Validate => {
let config = Config::from_env();
match config.validate() {
Ok(()) => println!("Configuration is valid."),
Err(e) => println!("Configuration error: {}", e),
}
}
}
Ok(())
}
fn print_config(config: &Config) {
println!(" node_id: {:?}", config.node_id);
println!(" publish_interval: {:?}", config.publish_interval);
println!(" power_mode: {:?}", config.power_mode);
println!(" memory_limit: {} KB", config.memory_limit / 1024);
println!(" enable_metrics: {}", config.enable_metrics);
println!(" enable_mdns: {}", config.enable_mdns);
println!(" log_level: {}", config.log_level);
println!("\nGossip:");
println!(" max_peers: {}", config.gossip.max_peers);
println!(" loop_delay: {:?}", config.gossip.loop_delay);
println!(
" output_target_mbps: {}",
config.gossip.output_target_mbps
);
println!("\nStorage:");
println!(" db_path: {}", config.storage.db_path);
println!(" backend: {:?}", config.storage.backend);
println!("\nTransport:");
match &config.transport {
aingle_minimal::TransportConfig::Memory => println!(" type: Memory"),
aingle_minimal::TransportConfig::Coap { bind_addr, port } => {
println!(" type: CoAP");
println!(" bind_addr: {}", bind_addr);
println!(" port: {}", port);
}
aingle_minimal::TransportConfig::Quic { bind_addr, port } => {
println!(" type: QUIC");
println!(" bind_addr: {}", bind_addr);
println!(" port: {}", port);
}
aingle_minimal::TransportConfig::Mesh { mode } => {
println!(" type: Mesh");
println!(" mode: {:?}", mode);
}
#[cfg(feature = "webrtc")]
aingle_minimal::TransportConfig::WebRtc {
stun_server,
signaling_port,
..
} => {
println!(" type: WebRTC");
println!(" stun_server: {}", stun_server);
println!(" signaling_port: {}", signaling_port);
}
#[cfg(feature = "ble")]
aingle_minimal::TransportConfig::Ble {
device_name,
mesh_relay,
tx_power,
} => {
println!(" type: Bluetooth LE");
println!(" device_name: {}", device_name);
println!(" mesh_relay: {}", mesh_relay);
println!(" tx_power: {} dBm", tx_power);
}
}
}
fn show_version() -> Result<()> {
println!("AIngle Minimal v{}", aingle_minimal::VERSION);
println!("Copyright 2019-2026 Apilium Technologies OÜ");
println!("License: Apache-2.0 OR Commercial");
println!("https://github.com/ApiliumCode/aingle");
println!();
println!("Build info:");
println!(" Rust MSRV: {}", aingle_minimal::MSRV);
println!(
" Target memory: {} KB",
aingle_minimal::MEMORY_BUDGET / 1024
);
#[cfg(debug_assertions)]
println!(" Profile: debug");
#[cfg(not(debug_assertions))]
println!(" Profile: release");
Ok(())
}
fn run_benchmark(entries: usize, iterations: usize) -> Result<()> {
println!("Running benchmark...\n");
println!(" Entries per iteration: {}", entries);
println!(" Iterations: {}", iterations);
println!();
let config = Config::test_mode();
let mut node = MinimalNode::new(config)?;
let mut total_duration = Duration::ZERO;
for i in 0..iterations {
let start = std::time::Instant::now();
for j in 0..entries {
let data = serde_json::json!({
"iteration": i,
"entry": j,
"timestamp": chrono::Utc::now().timestamp_millis(),
});
node.create_entry(data)?;
}
let duration = start.elapsed();
total_duration += duration;
let entries_per_sec = entries as f64 / duration.as_secs_f64();
println!(
" Iteration {}: {:?} ({:.0} entries/sec)",
i + 1,
duration,
entries_per_sec
);
}
let total_entries = entries * iterations;
let avg_per_entry = total_duration / total_entries as u32;
let entries_per_sec = total_entries as f64 / total_duration.as_secs_f64();
println!();
println!("Results:");
println!(" Total entries: {}", total_entries);
println!(" Total time: {:?}", total_duration);
println!(" Average per entry: {:?}", avg_per_entry);
println!(" Throughput: {:.0} entries/sec", entries_per_sec);
let stats = node.stats()?;
println!();
println!("Storage:");
println!(" Entries: {}", stats.entries_count);
println!(" Actions: {}", stats.actions_count);
Ok(())
}
fn setup_ctrlc_handler() {
}
fn base64_encode(data: &[u8]) -> String {
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
let mut i = 0;
while i < data.len() {
let b0 = data[i] as usize;
let b1 = if i + 1 < data.len() {
data[i + 1] as usize
} else {
0
};
let b2 = if i + 2 < data.len() {
data[i + 2] as usize
} else {
0
};
result.push(ALPHABET[b0 >> 2] as char);
result.push(ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)] as char);
if i + 1 < data.len() {
result.push(ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] as char);
} else {
result.push('=');
}
if i + 2 < data.len() {
result.push(ALPHABET[b2 & 0x3f] as char);
} else {
result.push('=');
}
i += 3;
}
result
}