Compare commits

..

3 commits

Author SHA1 Message Date
4acf07f389 Refactors brain to event-driven lifecycle
Introduces a multi-threaded architecture where the brain operates in its own thread.

Establishes explicit MPSC channels for communication, enabling the main application (God) to send `LifecycleCommand` messages to the brain and receive status updates. This replaces the previous polling-based `update` loop with an event-driven model.

Defines a comprehensive `LifeState` enum for more granular and robust state management. The `LedPump` component is removed as its functionality is superseded by the new command-driven state transitions.

This architectural shift improves separation of concerns and lays the groundwork for more complex and responsive robot control.
2026-03-01 15:19:53 +00:00
92c2ffa9cd Makes build number increment conditional
Introduces an `increment` Cargo feature to control the automatic incrementing of the build number during the build process.

This change prevents the build number from incrementing on every build, providing more control for local development and specific release scenarios. The build number will now only increment when the `increment` feature is explicitly enabled.

Adds `cargo:rerun-if-changed=src/` to ensure the build script re-runs when source files change.
2026-02-28 08:07:07 +00:00
76d1311440 Bumps build number to 102
Reflects the latest successful build iteration for the project.
2026-02-28 07:40:26 +00:00
7 changed files with 140 additions and 87 deletions

View file

@ -8,3 +8,6 @@ build = "build.rs"
ctrlc = "3.5.2"
tracing = "0.1"
tracing-subscriber = { version = "0.3.22", features = ["env-filter", "fmt"] }
[features]
increment = []

View file

@ -1,14 +1,17 @@
use std::fs;
use std::env;
fn main() {
println!("cargo:rerun-if-changed=src/");
let pkg_version = env!("CARGO_PKG_VERSION");
let mut content = fs::read_to_string("build_number.txt").unwrap_or("0".to_string());
let content = fs::read_to_string("build_number.txt").unwrap_or_else(|_| "0".to_string());
let mut build_num: u32 = content.trim().parse().unwrap_or(0);
build_num += 1;
let full_version = format!("{}.{}", pkg_version, build_num);
if env::var("CARGO_FEATURE_INCREMENT").is_ok() {
build_num += 1;
let _ = fs::write("build_number.txt", build_num.to_string());
}
fs::write("build_number.txt", build_num.to_string()).unwrap();
println!("cargo:rustc-env=FULL_VERSION={}", full_version);
println!("cargo:rustc-env=FULL_VERSION={}.{}", pkg_version, build_num);
}

View file

@ -1 +1 @@
98
133

View file

@ -1,49 +1,75 @@
pub mod state;
pub mod led_pump;
pub use self::state::State;
use tracing::info;
use std::thread::JoinHandle;
use self::led_pump::LedPump;
use std::sync::mpsc::{Receiver, Sender};
use crate::lifecycle::{LifeState, LifecycleCommand};
use crate::lifecycle::LifeState::{Buried, Genisys};
pub struct Brain {
pub state: State,
device_threads: Vec<JoinHandle<()>>,
led_pump: LedPump
state:LifeState,
divine_rx : Receiver<LifecycleCommand>,
divine_tx: Sender<String>
}
impl Brain {
pub fn new() -> Self {
pub fn new(divine_rx: Receiver<LifecycleCommand>, divine_tx: Sender<String>) -> Self {
Self {
state: State::Starting,
device_threads: Vec::new(),
led_pump: LedPump::new()
state: LifeState::Dead,
divine_rx,
divine_tx,
}
}
pub fn request_shutdown(&mut self) {
if self.state == State::Running {
self.state = State::Stopping;
pub fn run(&mut self) {
loop {
while let Ok(command) = self.divine_rx.try_recv() {
self.handle_divine_command(command)
}
if self.state == LifeState::Buried {
break;
}
// TODO: Drain the Organ Feedback Mailbox
// (We will add this once we define the Organ channels)
Self::rest()
}
}
pub fn update(&mut self) {
match self.state {
State::Starting => {
// For now, we just instantly transition
self.state = State::Running;
}
State::Running => {
self.led_pump.beat();
}
State::Stopping => {
info!("Brain: Waiting for all threads to join...");
while let Some(handle) = self.device_threads.pop() {
let _ = handle.join();
}
self.state = State::Stopped;
}
State::Stopped => {}
}
fn rest() {
std::thread::sleep(std::time::Duration::from_millis(1))
}
}
fn handle_divine_command(&mut self, command: LifecycleCommand) {
info!("God has commanded {:?}", command);
if self.can_transition_lifecycle(&command) {
self.set_lifecycle_state(&command);
return;
}
self.report_to_god("REFUSED")
}
fn can_transition_lifecycle(&self, command: &LifecycleCommand) -> bool
{
if (command.required_state == Buried) {
return true
}
if self.state == LifeState::Dead && command.required_state == LifeState::Genisys {
return true
}
false
}
fn set_lifecycle_state(&mut self, state: &LifecycleCommand) {
self.state = state.required_state;
self.report_to_god("OK");
}
fn report_to_god(&self, msg: &str) {
info!("Reporting to God {}", msg);
let _ = self.divine_tx.send(msg.to_string());
}
}

View file

@ -1,28 +0,0 @@
use std::time::{Duration, Instant};
use tracing::debug;
pub struct LedPump {
last_beat: Option<Instant>,
interval: Duration,
}
impl LedPump {
pub fn new() -> LedPump {
Self{
last_beat: None,
interval: Duration::from_millis(500),
}
}
pub fn beat(&mut self) {
let now = Instant::now();
let should_beat = self.last_beat.map_or(
true, |last_beat| now.duration_since(last_beat) > self.interval);
if should_beat {
self.last_beat = Some(now);
debug!("LED Pump beat.");
}
}
}

View file

@ -0,0 +1,22 @@
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LifeState {
Dead,
Genisys,
Sleeping,
Wakening,
Awake,
DeepThought,
ActionFocused,
Flight,
Panic,
Dying,
Buried
}
#[derive(Debug)]
pub struct LifecycleCommand {
pub required_state: LifeState,
pub command_time: Instant
}

View file

@ -1,13 +1,18 @@
mod brain;
use brain::{Brain, State};
pub mod lifecycle;
pub mod brain;
use tracing::info;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, Sender};
use std::{thread, time::Duration};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use crate::brain::Brain;
use crate::lifecycle::{LifeState, LifecycleCommand};
fn main() {
let semantic_version = env!("CARGO_PKG_VERSION").to_string();
let _semantic_version = env!("CARGO_PKG_VERSION").to_string();
let shutdown_requested = Arc::new(AtomicBool::new(false));
let s = shutdown_requested.clone();
@ -19,23 +24,45 @@ fn main() {
s.store(true, Ordering::SeqCst);
}).expect("Error setting signal handler");
let mut robot_brain = Brain::new();
// 1. Setup the Divine Nerves
let (to_brain_tx, to_brain_rx) = mpsc::channel::<LifecycleCommand>();
let (from_brain_tx, from_brain_rx) = mpsc::channel::<String>();
while robot_brain.state != State::Stopped {
// 2. Spawn the Brain thread
thread::spawn(move || {
let mut brain = Brain::new(to_brain_rx, from_brain_tx);
brain.run();
});
// If the signal handler tripped the flag, tell the brain to stop
if shutdown_requested.load(Ordering::SeqCst) && robot_brain.state == State::Running {
robot_brain.request_shutdown();
}
// 3. Command: Genisys
info!("God: Commanding Genisys...");
let _ = to_brain_tx.send(LifecycleCommand {
required_state: LifeState::Genisys,
command_time: Instant::now(),
});
// Run one "tick" of brain logic
robot_brain.update();
info!("Main Loop: I'm alive! State: {:?}", robot_brain.state);
thread::sleep(Duration::from_millis(500));
// Wait for Brain's "OK"
if let Ok(reply) = from_brain_rx.recv() {
info!("Brain: {}", reply);
}
info!("Main Loop: Brain has Stopped. Exiting cleanly.");
// 4. Wait two seconds
info!("God: Resting for 2 seconds...");
thread::sleep(Duration::from_secs(2));
// 5. Command: Buried
info!("God: Commanding Burial...");
let _ = to_brain_tx.send(LifecycleCommand {
required_state: LifeState::Buried,
command_time: Instant::now(),
});
// Wait for Brain's final "OK"
if let Ok(reply) = from_brain_rx.recv() {
info!("Brain: {}", reply);
}
info!("God: Brain has been buried. Shutting down.");
}
fn setup_logging() {