Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 50 additions & 12 deletions lox-sim/src/blocks/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +47,56 @@ macro_rules! passthrough_io_block {

/// Input reference — proxy that forwards named inputs to the block graph.
///
/// A ref is fed on exactly one side (I or AI) but the Miniserver mirrors the
/// signal on BOTH outputs: consumers routinely read Q from an AI-fed ref
/// (r50 corpus: `ref.AI <- mem.AQ` with `monoflop.InputTrigger: ref.Q`).
/// Q is the digital view (non-zero → 1), AQ the analog value; the unfed
/// side idles at 0, so combining the two inputs is lossless.
/// The Miniserver mirrors the fed input on BOTH outputs: consumers routinely
/// read Q from an AI-fed ref (r50 corpus: `ref.AI <- mem.AQ` with
/// `monoflop.InputTrigger: ref.Q`). Q is the digital view (non-zero -> 1), AQ
/// the analog value. When both inputs are wired, AI is authoritative.
#[derive(Clone, Copy)]
pub struct InputRef;
pub struct InputRef {
source: InputRefSource,
}

#[derive(Clone, Copy)]
enum InputRefSource {
Digital,
Analog,
}

impl InputRef {
pub fn new() -> Self {
Self {
source: InputRefSource::Digital,
}
}
}

impl Default for InputRef {
fn default() -> Self {
Self::new()
}
}

impl Block for InputRef {
fn configure_input_connections(&mut self, connected: &[bool]) {
self.source = if connected.get(1).copied().unwrap_or(false) {
InputRefSource::Analog
} else {
InputRefSource::Digital
};
}

fn eval(
&mut self,
inputs: &[Signal],
_params: &[Signal],
_dt: f64,
_prev: &[Signal],
) -> Vec<Signal> {
let i = inputs.first().copied().unwrap_or(0.0);
let ai = inputs.get(1).copied().unwrap_or(0.0);
let q = if i != 0.0 || ai != 0.0 { 1.0 } else { 0.0 };
let aq = if ai != 0.0 { ai } else { i };
vec![q, aq]
let value = match self.source {
InputRefSource::Digital => inputs.first().copied().unwrap_or(0.0),
InputRefSource::Analog => inputs.get(1).copied().unwrap_or(0.0),
};
vec![if value != 0.0 { 1.0 } else { 0.0 }, value]
}

fn block_type(&self) -> &str {
Expand Down Expand Up @@ -268,13 +297,22 @@ mod tests {
use crate::blocks::create_block;
#[test]
fn input_ref_mirrors_fed_side_to_both_outputs() {
let mut block = InputRef;
let mut block = InputRef::new();
// I=42, AI unfed → Q=1 (digital view), AQ=42
assert_eq!(block.eval(&[42.0], &[], 0.0, &[]), vec![1.0, 42.0]);
block.configure_input_connections(&[false, true]);
// I unfed, AI=99 → Q=1, AQ=99 (AI-fed refs serve Q consumers)
assert_eq!(block.eval(&[0.0, 99.0], &[], 0.0, &[]), vec![1.0, 99.0]);
// Both present, AI=0 → the analog zero remains authoritative.
assert_eq!(block.eval(&[1.0, 0.0], &[], 0.0, &[]), vec![0.0, 0.0]);
// empty → Q=0, AQ=0
assert_eq!(block.eval(&[], &[], 0.0, &[]), vec![0.0, 0.0]);

block.configure_input_connections(&[true, false]);
assert_eq!(block.eval(&[42.0, 99.0], &[], 0.0, &[]), vec![1.0, 42.0]);

block.configure_input_connections(&[true, true]);
assert_eq!(block.eval(&[1.0, 0.0], &[], 0.0, &[]), vec![0.0, 0.0]);
}

#[test]
Expand Down
12 changes: 9 additions & 3 deletions lox-sim/src/blocks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ pub(crate) fn deserialize_f64s(bytes: &[u8], count: usize) -> Option<Vec<f64>> {
return None;
}
let mut values = Vec::with_capacity(count);
for chunk in bytes[..count * 8].chunks_exact(8) {
values.push(f64::from_le_bytes(chunk.try_into().ok()?));
for chunk in bytes[..count * 8].as_chunks::<8>().0 {
values.push(f64::from_le_bytes(*chunk));
}
Some(values)
}
Expand Down Expand Up @@ -221,6 +221,12 @@ impl Clone for Box<dyn Block> {

/// Core block trait — all simulation blocks implement this.
pub trait Block: Send + Sync + BlockClone {
/// Configure which input connectors are wired before the first tick.
///
/// Most blocks only depend on input values. Blocks whose semantics depend
/// on connector presence can override this hook.
fn configure_input_connections(&mut self, _connected: &[bool]) {}

/// Evaluate the block for one tick.
fn eval(
&mut self,
Expand Down Expand Up @@ -591,7 +597,7 @@ pub fn create_block(block_type: &str) -> Box<dyn Block> {
"JoinWindowSensor" => Box::new(JoinWindowSensor),

// Group D — I/O
"InputRef" => Box::new(InputRef),
"InputRef" => Box::new(InputRef::new()),
"OutputRef" => Box::new(OutputRef),
"OutputRefLM" => Box::new(OutputRefLM),
"EIBPush" => Box::new(EIBPush),
Expand Down
87 changes: 71 additions & 16 deletions lox-sim/src/blocks/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,14 +257,16 @@ impl Block for PushButton {
let prev_trigger = prev_inputs.first().copied().unwrap_or(0.0);
let previous = self.is_on;

// WARNING: Assumed behavior — not validated against Miniserver.
// Assumption: Reset dominates On; InputDisable gates only the trigger.
if is_high(reset) {
self.is_on = false;
} else if is_high(force_on) {
self.is_on = true;
} else if !is_high(disable) && !is_high(prev_trigger) && is_high(trigger) {
self.is_on = !self.is_on;
// InputDisable disables every peripheral input; Reset dominates On
// when peripheral control is enabled.
if !is_high(disable) {
if is_high(reset) {
self.is_on = false;
} else if is_high(force_on) {
self.is_on = true;
} else if !is_high(prev_trigger) && is_high(trigger) {
self.is_on = !self.is_on;
}
}

let qon = !previous && self.is_on;
Expand Down Expand Up @@ -335,20 +337,23 @@ impl Block for PushButton2 {
prev_inputs: &[Signal],
) -> Vec<Signal> {
let trigger = inputs.first().copied().unwrap_or(0.0);
let force_on = inputs.get(1).copied().unwrap_or(0.0);
let reset = inputs.get(2).copied().unwrap_or(0.0);
let disable = inputs.get(3).copied().unwrap_or(0.0);
let prev_trigger = prev_inputs.first().copied().unwrap_or(0.0);
let dc_window = params.first().copied().unwrap_or(0.4).max(0.0);
let previous = self.is_on;
// WARNING: Assumed behavior — not validated against Miniserver.
// Assumption: Reset dominates and cancels a pending double-click;
// InputDisable gates only the trigger.
if is_high(reset) {
self.is_on = false;
self.awaiting_second = false;
let mut rising = false;
if !is_high(disable) {
if is_high(reset) {
self.is_on = false;
self.awaiting_second = false;
} else if is_high(force_on) {
self.is_on = true;
} else {
rising = !is_high(prev_trigger) && is_high(trigger);
}
}
let rising =
!is_high(reset) && !is_high(disable) && !is_high(prev_trigger) && is_high(trigger);
let mut double_click = false;

if self.awaiting_second {
Expand Down Expand Up @@ -940,6 +945,56 @@ mod tests {
);
}

#[test]
fn pushbutton_reset_dominates_on_when_enabled() {
let mut block = PushButton::new();
assert_eq!(
block.eval(&[1.0, 1.0, 1.0, 0.0], &[], 0.0, &[0.0, 0.0, 0.0, 0.0]),
vec![0.0, 0.0, 0.0]
);
}

#[test]
fn pushbutton_disable_blocks_all_inputs() {
let mut block = PushButton::new();
block.eval(&[1.0, 0.0, 0.0, 0.0], &[], 0.0, &[0.0, 0.0, 0.0, 0.0]);

assert_eq!(
block.eval(&[0.0, 0.0, 1.0, 1.0], &[], 0.0, &[1.0, 0.0, 0.0, 0.0]),
vec![1.0, 0.0, 0.0],
"disabled Reset must not change the state"
);

let mut off = PushButton::new();
assert_eq!(
off.eval(&[1.0, 1.0, 0.0, 1.0], &[], 0.0, &[0.0, 0.0, 0.0, 1.0]),
vec![0.0, 0.0, 0.0],
"disabled Trigger and On must not change the state"
);
}

#[test]
fn pushbutton2_disable_blocks_reset_and_trigger() {
let mut block = PushButton2::new();
block.eval(&[1.0, 0.0, 0.0, 0.0], &[0.5], 0.0, &[0.0, 0.0, 0.0, 0.0]);
let out = block.eval(&[0.0, 0.0, 1.0, 1.0], &[0.5], 0.1, &[1.0, 0.0, 0.0, 0.0]);
assert_eq!(out[0], 1.0);
assert_eq!(out[1], 0.0);
}

#[test]
fn pushbutton2_on_forces_output_unless_disabled() {
let mut block = PushButton2::new();
let out = block.eval(&[0.0, 1.0, 0.0, 0.0], &[0.5], 0.1, &[0.0; 4]);
assert_eq!(out[0], 1.0);
assert_eq!(out[2], 1.0);

let mut disabled = PushButton2::new();
let out = disabled.eval(&[0.0, 1.0, 0.0, 1.0], &[0.5], 0.1, &[0.0; 4]);
assert_eq!(out[0], 0.0);
assert_eq!(out[2], 0.0);
}

#[test]
fn pushbutton2_detects_double_click() {
let mut block = PushButton2::new();
Expand Down
17 changes: 17 additions & 0 deletions lox-sim/src/blocks/timers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,23 @@ mod tests {
assert_eq!(block.eval(&[0.0], &[1.0], 0.25, &[0.0]), vec![0.0]);
}

#[test]
fn monoflop_reset_aborts_and_blocks_retrigger() {
let mut block = Monoflop::new();
assert_eq!(
block.eval(&[1.0, 0.0], &[1.0], 0.25, &[0.0, 0.0]),
vec![1.0]
);
assert_eq!(
block.eval(&[1.0, 1.0], &[1.0], 0.25, &[1.0, 0.0]),
vec![0.0]
);
assert_eq!(
block.eval(&[1.0, 1.0], &[1.0], 0.25, &[0.0, 1.0]),
vec![0.0]
);
}

#[test]
fn on_pulse_delay_emits_delayed_pulse() {
let mut block = OnPulseDelay::new();
Expand Down
Loading
Loading