forked from genewildish/Mainline
feat: Add oscilloscope-style waveform visualization
- demo_oscilloscope.py: Real-time oscilloscope display with continuous trace - Shows waveform scrolling across the screen at correct time rate - Supports all waveforms: sine, square, sawtooth, triangle, noise - Frequency-based scrolling speed - Single continuous trace instead of multiple copies Related to #46
This commit is contained in:
181
scripts/demo_oscilloscope.py
Normal file
181
scripts/demo_oscilloscope.py
Normal file
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user