diff --git a/lox-sim/src/blocks/io.rs b/lox-sim/src/blocks/io.rs index c83736a..a622c04 100644 --- a/lox-sim/src/blocks/io.rs +++ b/lox-sim/src/blocks/io.rs @@ -46,7 +46,12 @@ macro_rules! passthrough_io_block { // --------------------------------------------------------------------------- /// Input reference — proxy that forwards named inputs to the block graph. -/// I→Q (digital) and AI→AQ (analog). +/// +/// 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. #[derive(Clone, Copy)] pub struct InputRef; @@ -60,7 +65,9 @@ impl Block for InputRef { ) -> Vec { let i = inputs.first().copied().unwrap_or(0.0); let ai = inputs.get(1).copied().unwrap_or(0.0); - vec![i, ai] + 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] } fn block_type(&self) -> &str { @@ -260,12 +267,12 @@ mod tests { use super::*; use crate::blocks::create_block; #[test] - fn input_ref_passthrough() { + fn input_ref_mirrors_fed_side_to_both_outputs() { let mut block = InputRef; - // I=42, AI=0 → Q=42, AQ=0 - assert_eq!(block.eval(&[42.0], &[], 0.0, &[]), vec![42.0, 0.0]); - // I=0, AI=99 → Q=0, AQ=99 - assert_eq!(block.eval(&[0.0, 99.0], &[], 0.0, &[]), vec![0.0, 99.0]); + // I=42, AI unfed → Q=1 (digital view), AQ=42 + assert_eq!(block.eval(&[42.0], &[], 0.0, &[]), vec![1.0, 42.0]); + // 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]); // empty → Q=0, AQ=0 assert_eq!(block.eval(&[], &[], 0.0, &[]), vec![0.0, 0.0]); } diff --git a/lox-sim/src/blocks/state.rs b/lox-sim/src/blocks/state.rs index 113c92e..3a4eadd 100644 --- a/lox-sim/src/blocks/state.rs +++ b/lox-sim/src/blocks/state.rs @@ -252,12 +252,18 @@ impl Block for PushButton { ) -> Vec { 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 previous = self.is_on; - if is_high(force_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(prev_trigger) && is_high(trigger) { + } else if !is_high(disable) && !is_high(prev_trigger) && is_high(trigger) { self.is_on = !self.is_on; } @@ -329,10 +335,20 @@ impl Block for PushButton2 { prev_inputs: &[Signal], ) -> Vec { let trigger = inputs.first().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; - let rising = !is_high(prev_trigger) && is_high(trigger); + // 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 rising = + !is_high(reset) && !is_high(disable) && !is_high(prev_trigger) && is_high(trigger); let mut double_click = false; if self.awaiting_second { diff --git a/lox-sim/src/blocks/timers.rs b/lox-sim/src/blocks/timers.rs index 0e3dbee..bacc3e5 100644 --- a/lox-sim/src/blocks/timers.rs +++ b/lox-sim/src/blocks/timers.rs @@ -30,10 +30,16 @@ impl Block for Monoflop { prev_inputs: &[Signal], ) -> Vec { let trigger = inputs.first().copied().unwrap_or(0.0); + let reset = inputs.get(1).copied().unwrap_or(0.0); let prev_trigger = prev_inputs.first().copied().unwrap_or(0.0); let duration = params.first().copied().unwrap_or(1.0).max(0.0); - if !is_high(prev_trigger) && is_high(trigger) { + // WARNING: Assumed behavior — not validated against Miniserver. + // Assumption: Reset aborts the running pulse and blocks retriggering + // while held. + if is_high(reset) { + self.remaining = 0.0; + } else if !is_high(prev_trigger) && is_high(trigger) { self.remaining = duration.max(dt); } diff --git a/lox-sim/src/compiler.rs b/lox-sim/src/compiler.rs index 973058e..fe4e74f 100644 --- a/lox-sim/src/compiler.rs +++ b/lox-sim/src/compiler.rs @@ -137,6 +137,8 @@ pub enum EvalStep { Monoflop { trigger: usize, prev_trigger: usize, + /// Reset signal slot; `usize::MAX` when the block has no Reset wire. + reset: usize, param_duration: usize, output: usize, state_idx: usize, @@ -217,6 +219,10 @@ pub enum EvalStep { trigger: usize, prev_trigger: usize, force_on: usize, + /// Reset/InputDisable signal slots; `usize::MAX` when the block has + /// no such connector (PushButtonSel layouts vary). + reset: usize, + disable: usize, /// outputs: [Q, Qoff, Qon] outputs: [usize; 3], state_idx: usize, @@ -520,6 +526,7 @@ impl CompiledGraph { EvalStep::Monoflop { trigger: resolved_inputs.first().copied().unwrap_or(0), prev_trigger: prev_inputs.first().copied().unwrap_or(0), + reset: resolved_inputs.get(1).copied().unwrap_or(usize::MAX), param_duration: params.first().copied().unwrap_or(0), output: outputs[0], state_idx: si, @@ -662,6 +669,8 @@ impl CompiledGraph { trigger: resolved_inputs.first().copied().unwrap_or(0), prev_trigger: prev_inputs.first().copied().unwrap_or(0), force_on: resolved_inputs.get(1).copied().unwrap_or(0), + reset: resolved_inputs.get(2).copied().unwrap_or(usize::MAX), + disable: resolved_inputs.get(3).copied().unwrap_or(usize::MAX), outputs: [ *outputs.first().unwrap_or(&0), *outputs.get(1).unwrap_or(&0), @@ -969,18 +978,22 @@ impl CompiledGraph { EvalStep::Monoflop { trigger, prev_trigger, + reset, param_duration, output, state_idx, } => { let trig = self.signals[*trigger]; let prev_trig = self.prev_signals[*prev_trigger]; + let rst = self.signals.get(*reset).copied().unwrap_or(0.0); let duration = self.signals[*param_duration].max(0.0); let out = *output; let si = *state_idx; if let BlockState::Timer { remaining, .. } = &mut self.state[si] { - if prev_trig < 0.5 && trig >= 0.5 { + if rst >= 0.5 { + *remaining = 0.0; + } else if prev_trig < 0.5 && trig >= 0.5 { *remaining = duration.max(dt); } let q = *remaining > 0.0; @@ -1315,20 +1328,26 @@ impl CompiledGraph { trigger, prev_trigger, force_on, + reset, + disable, outputs, state_idx, } => { let trig = self.signals[*trigger]; let prev_trig = self.prev_signals[*prev_trigger]; let force = self.signals[*force_on]; + let rst = self.signals.get(*reset).copied().unwrap_or(0.0); + let dis = self.signals.get(*disable).copied().unwrap_or(0.0); let outs = *outputs; let si = *state_idx; if let BlockState::PushButton { is_on } = &mut self.state[si] { let previous = *is_on; - if force >= 0.5 { + if rst >= 0.5 { + *is_on = false; + } else if force >= 0.5 { *is_on = true; - } else if prev_trig < 0.5 && trig >= 0.5 { + } else if dis < 0.5 && prev_trig < 0.5 && trig >= 0.5 { *is_on = !*is_on; } let qon = !previous && *is_on; diff --git a/lox-sim/src/parser.rs b/lox-sim/src/parser.rs index e46bd6c..2fe776b 100644 --- a/lox-sim/src/parser.rs +++ b/lox-sim/src/parser.rs @@ -574,7 +574,7 @@ fn block_signature( &["Q1", "Q2", "Q3", "Q4", "AQ"], &["Time", "V1", "V2", "V3", "V4"], ), - "Monoflop" => (&["InputTrigger"], &["Q"], &["Time"]), + "Monoflop" => (&["InputTrigger", "Reset"], &["Q"], &["Time"]), "Minmax" => ( &["Input1", "Input2", "Input3", "Input4"], &["AQmin", "AQmax"], @@ -605,12 +605,12 @@ fn block_signature( &["TimeHigh", "TimeLow"], ), "PushButton" | "PushButton2" | "PushButton2Sel" => ( - &["InputTrigger", "On"], + &["InputTrigger", "On", "Reset", "InputDisable"], &["Q", "Qoff", "Qon", "AQ"], &["Min", "Max"], ), "PushButtonSel" => ( - &["InputTrigger", "InputPos", "Reset"], + &["InputTrigger", "InputPos", "Reset", "InputDisable"], &["AQ"], &["Min", "Max", "Step", "Repeat", "Def"], ),