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.
This commit is contained in:
Russell Gilbert 2026-03-01 15:19:53 +00:00
parent 92c2ffa9cd
commit 4acf07f389
5 changed files with 126 additions and 81 deletions

View file

@ -1 +1 @@
127 133

View file

@ -1,51 +1,75 @@
pub mod state;
pub mod led_pump;
pub use self::state::State;
use tracing::info; use tracing::info;
use std::thread::JoinHandle; use std::sync::mpsc::{Receiver, Sender};
use self::led_pump::LedPump; use crate::lifecycle::{LifeState, LifecycleCommand};
use crate::lifecycle::LifeState::{Buried, Genisys};
pub struct Brain { pub struct Brain {
pub state: State, state:LifeState,
device_threads: Vec<JoinHandle<()>>, divine_rx : Receiver<LifecycleCommand>,
led_pump: LedPump divine_tx: Sender<String>
} }
impl Brain { impl Brain {
pub fn new() -> Self { pub fn new(divine_rx: Receiver<LifecycleCommand>, divine_tx: Sender<String>) -> Self {
Self { Self {
state: State::Starting, state: LifeState::Dead,
device_threads: Vec::new(), divine_rx,
led_pump: LedPump::new() divine_tx,
} }
} }
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;
}
pub fn request_shutdown(&mut self) { // TODO: Drain the Organ Feedback Mailbox
if self.state == State::Running { // (We will add this once we define the Organ channels)
self.state = State::Stopping;
Self::rest()
} }
} }
pub fn update(&mut self) { fn rest() {
match self.state { std::thread::sleep(std::time::Duration::from_millis(1))
State::Starting => {
// For now, we just instantly transition
self.state = State::Running;
} }
State::Running => {
self.led_pump.beat(); 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;
} }
State::Stopping => {
info!("Brain: Waiting for all threads to join..."); self.report_to_god("REFUSED")
while let Some(handle) = self.device_threads.pop() {
let _ = handle.join();
} }
self.state = State::Stopped;
fn can_transition_lifecycle(&self, command: &LifecycleCommand) -> bool
{
if (command.required_state == Buried) {
return true
} }
State::Stopped => {}
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; pub mod lifecycle;
use brain::{Brain, State}; pub mod brain;
use tracing::info; use tracing::info;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::mpsc;
use std::{thread, time::Duration}; use std::thread;
use std::time::{Duration, Instant};
use crate::brain::Brain;
use crate::lifecycle::{LifeState, LifecycleCommand};
fn main() { 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 shutdown_requested = Arc::new(AtomicBool::new(false));
let s = shutdown_requested.clone(); let s = shutdown_requested.clone();
@ -19,23 +24,45 @@ fn main() {
s.store(true, Ordering::SeqCst); s.store(true, Ordering::SeqCst);
}).expect("Error setting signal handler"); }).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 // 3. Command: Genisys
if shutdown_requested.load(Ordering::SeqCst) && robot_brain.state == State::Running { info!("God: Commanding Genisys...");
robot_brain.request_shutdown(); let _ = to_brain_tx.send(LifecycleCommand {
required_state: LifeState::Genisys,
command_time: Instant::now(),
});
// Wait for Brain's "OK"
if let Ok(reply) = from_brain_rx.recv() {
info!("Brain: {}", reply);
} }
// Run one "tick" of brain logic // 4. Wait two seconds
robot_brain.update(); info!("God: Resting for 2 seconds...");
thread::sleep(Duration::from_secs(2));
info!("Main Loop: I'm alive! State: {:?}", robot_brain.state); // 5. Command: Buried
thread::sleep(Duration::from_millis(500)); 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!("Main Loop: Brain has Stopped. Exiting cleanly."); info!("God: Brain has been buried. Shutting down.");
} }
fn setup_logging() { fn setup_logging() {