diff --git a/scripts/demo_oscilloscope.py b/scripts/demo_oscilloscope.py new file mode 100644 index 0000000..ade7409 --- /dev/null +++ b/scripts/demo_oscilloscope.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +Oscilloscope Demo - Real-time waveform visualization + +This demonstrates a real oscilloscope-style display where: +1. A complete waveform is drawn on the canvas +2. The camera scrolls horizontally (time axis) +3. The "pen" traces the waveform vertically at the center + +Think of it as: +- Canvas: Contains the waveform pattern (like a stamp) +- Camera: Moves left-to-right, revealing different parts of the waveform +- Pen: Always at center X, moves vertically with the signal value + +Usage: + uv run python scripts/demo_oscilloscope.py --frequency 1.0 --speed 10 +""" + +import argparse +import math +import time +import sys +from pathlib import Path + +# Add mainline to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from engine.sensors.oscillator import OscillatorSensor, register_oscillator_sensor + + +def render_oscilloscope( + width: int, + height: int, + osc: OscillatorSensor, + frame: int, +) -> str: + """Render an oscilloscope-style display.""" + # Get current reading (0.0 to 1.0) + reading = osc.read() + current_value = reading.value if reading else 0.5 + phase = osc._phase + frequency = osc.frequency + + # Build visualization + lines = [] + + # Header with sensor info + header = ( + f"Oscilloscope: {osc.name} | Wave: {osc.waveform} | " + f"Freq: {osc.frequency}Hz | Phase: {phase:.2f}" + ) + lines.append(header) + lines.append("─" * width) + + # Center line (zero reference) + center_row = height // 2 + + # Draw oscilloscope trace + waveform_fn = osc.WAVEFORMS[osc._waveform] + + # Calculate time offset for scrolling + # The trace scrolls based on phase - this creates the time axis movement + # At frequency 1.0, the trace completes one full sweep per frequency cycle + time_offset = phase * frequency * 2.0 + + # Pre-calculate all sample values for this frame + # Each column represents a time point on the X axis + samples = [] + for col in range(width): + # Time position for this column (0.0 to 1.0 across width) + col_fraction = col / width + # Combine with time offset for scrolling effect + time_pos = time_offset + col_fraction + + # Sample the waveform at this time point + # Multiply by frequency to get correct number of cycles shown + sample_value = waveform_fn(time_pos * frequency * 2) + samples.append(sample_value) + + # Draw the trace + # For each row, check which columns have their sample value in this row + for row in range(height - 3): # Reserve 3 lines for header/footer + # Calculate vertical position (0.0 at bottom, 1.0 at top) + row_pos = 1.0 - (row / (height - 4)) + + line_chars = [] + for col in range(width): + sample = samples[col] + + # Check if this sample falls in this row + tolerance = 1.0 / (height - 4) + if abs(sample - row_pos) < tolerance: + line_chars.append("█") + else: + line_chars.append(" ") + lines.append("".join(line_chars)) + + # Draw center indicator line + center_line = list(" " * width) + # Position the indicator based on current value + indicator_x = int((current_value) * (width - 1)) + if 0 <= indicator_x < width: + center_line[indicator_x] = "◎" + lines.append("".join(center_line)) + + # Footer with current value + footer = f"Value: {current_value:.3f} | Frame: {frame} | Phase: {phase:.2f}" + lines.append(footer) + + return "\n".join(lines) + + +def demo_oscilloscope( + waveform: str = "sine", + frequency: float = 1.0, + frames: int = 0, +): + """Run oscilloscope demo.""" + print(f"Oscilloscope demo: {waveform} wave at {frequency}Hz") + if frames > 0: + print(f"Running for {frames} frames") + else: + print("Press Ctrl+C to stop") + print() + + # Create oscillator sensor + register_oscillator_sensor( + name="oscilloscope_osc", waveform=waveform, frequency=frequency + ) + osc = OscillatorSensor( + name="oscilloscope_osc", waveform=waveform, frequency=frequency + ) + osc.start() + + # Run demo loop + try: + frame = 0 + while frames == 0 or frame < frames: + # Render oscilloscope display + visualization = render_oscilloscope(80, 22, osc, frame) + + # Print with ANSI escape codes to clear screen and move cursor + print("\033[H\033[J" + visualization) + + time.sleep(1.0 / 60.0) # 60 FPS + frame += 1 + + except KeyboardInterrupt: + print("\n\nDemo stopped by user") + + finally: + osc.stop() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Oscilloscope demo") + parser.add_argument( + "--waveform", + choices=["sine", "square", "sawtooth", "triangle", "noise"], + default="sine", + help="Waveform type", + ) + parser.add_argument( + "--frequency", + type=float, + default=1.0, + help="Oscillator frequency in Hz", + ) + parser.add_argument( + "--frames", + type=int, + default=0, + help="Number of frames to render (0 = infinite until Ctrl+C)", + ) + + args = parser.parse_args() + demo_oscilloscope( + waveform=args.waveform, + frequency=args.frequency, + frames=args.frames, + )