This repository hosts a Unity XR application (Meta Quest 3S target) that displays real-time oscilloscope waveforms from a Keysight MSOX604A in VR. The project features draggable, world-space waveform panels with oscilloscope-style grids and scale indicators, streaming live data over TCP from a Python server connected to the physical oscilloscope.
- Stream real oscilloscope data (CH1 & CH2) from a Keysight MSOX604A to Meta Quest headset in real-time
- Display waveforms in XR with oscilloscope-style visualization (grid, voltage/time scales)
- Support hand/pinch grab interactions for moving and positioning panels in 3D space
- Provide smooth, low-latency data streaming (~20 Hz update rate) over TCP
- Waveform visuals:
WaveformPanelcomponent drives a LineRenderer with configurable amplitude scaling, line thickness, and color - Four draggable panels (CH1-CH4) spawned via
WaveformManagerwith Meta hand/pinch grab support - Runtime injection and compatibility code for Meta Interaction SDK (reflection-based fallbacks for InjectColliders, InjectRigidbody, etc.)
- Prefab-based workflow with inspector-configured HandGrab/MovementProvider rules preserved across instantiation
- TCP Client (
TcpOscopeClient.cs): Connects to Python server, parses JSON waveform packets, dispatches events on main thread- Auto-reconnect with configurable interval
- Supports multiple channels, FFT requests, freeze commands
- Verbose logging for diagnostics
- Panel Connector (
WaveformPanelConnector.cs): Routes incoming waveform data to appropriate channel panels- Waits for TCP connection before sending stream commands (fixes race condition)
- Auto-starts CH1 & CH2 streaming on connection
- Python Server (
tcp_streaming_server.py): Streams oscilloscope data over TCP using pyvisa- Real scope mode: Reads from Keysight MSOX604A via VISA (USB/LAN)
- Mock mode fallback: Generates synthetic sine/cosine waves for testing
- Per-channel streaming threads, line-delimited JSON protocol
- Configurable sample rate, normalization to [-1, 1] range
- Grid Overlay (
OscilloscopeGrid.cs): Draws graticule with configurable divisions (10 horizontal, 8 vertical)- Individual LineRenderer per grid line for reliability
- Auto-updating voltage/time scale labels (V/div, µs/ms/s)
- Adjustable color, thickness, visibility
- Scale Updater (
ScopeScaleUpdater.cs): Auto-calculates and updates grid scales from incoming data or manual override - Amplitude Scaling:
WaveformPanel.amplitudeScale(0.1-1.0) keeps waveforms within panel bounds - External Data Mode: Disables synthetic generation when real data arrives via
SetSamples()
WaveformManager.cs- spawns panels, logs diagnostics, handles Meta SDK injectionWaveformPanel.cs- waveform rendering with LineRenderer, amplitude scaling, external data modeTcpOscopeClient.cs- TCP client for oscilloscope data streaming, event-driven architectureWaveformPanelConnector.cs- routes waveform events to correct channel panelsOscilloscopeGrid.cs- draws oscilloscope graticule grid with individual line renderersScopeScaleUpdater.cs- updates V/div and time/div labels based on data or manual settingsTestGreenCubeSpawner.cs- test spawner for debugging grab interactionsPanelGrabVerifier.cs- verifies grab component injection after spawn
tcp_streaming_server.py- main TCP server with real scope integration via pyvisaKeysightScopeclass: VISA connection, waveform acquisition, preamble parsingMockScopeclass: Synthetic data generator for testing- Line-delimited JSON protocol with command parsing (stream, stop_stream, fft, freeze)
oscope.py- Basic VISA connection test and channel enumerationquick_channel_test.py- Quick 2-channel test with voltage/sample rate readoutrequirements.txt- Python dependencies (pyvisa, numpy)
Assets/Prefabs/Panel_CH.prefab- fully configured panel with Interaction SDK componentsAssets/Panel_Background.mat- URP Unlit material for panel backgroundAssets/Panel_Frame.mat,Assets/Panel_Handle.mat- frame/handle materialsAssets/Editor/SaveSelectedAsWaveformPrefab.cs- Editor utility for creating prefabs
- Scope IP: Set in
tcp_streaming_server.py(SCOPE_IP = "169.254.208.205") - Server IP: Set in Unity Inspector on
TcpOscopeClientcomponent (serverIP = "192.168.1.156") - Channels: CH1 & CH2 enabled by default in
WaveformPanelConnector - Grid scales: Adjustable per-panel in
OscilloscopeGridor auto-updated viaScopeScaleUpdater
Panel_CH.prefab(inAssets/Prefabs/) contains:- Root GameObject
Panel_CHwith a Rigidbody and BoxCollider sized to the panel. WaveformPanelcomponent with default dummy waveform parameters (resolution, frequency, amplitude, color).LineRendererused for waveform visualization.- Child GameObjects:
Background(Quad, MeshRenderer, materialPanel_Background), frame edges (Edge_Top,Edge_Left, etc.), and aGrab_Handlechild used as the grab handle. - Interaction SDK components wired on the prefab:
Grabbable(parent),GrabInteractable,HandGrabInteractable(installation child) andMoveTowardsTargetProvider.
- Root GameObject
- Materials used:
Assets/Panel_Background.mat- URP-compatible material (Unlit/URP) to avoid magenta artifacts on URP builds.Assets/Panel_Frame.mat,Assets/Panel_Handle.mat- small materials for frame/handle.
The prefab was intentionally assembled in Editor so the HandGrabInteractable serialized properties (Pinch/Palm grabbing rules, MovementProvider references) are preserved and don't need fragile runtime reflection to set.
- Runtime-only wiring by creating panels procedurally and using reflection to set private backing fields and call injector methods. This worked in the Editor in many cases but was fragile due to Start() ordering races and un-serialized runtime changes not persisting into build-time assets.
- Prefab-first workflow: create a fully-configured prefab in Editor (with Inspector-set HandGrab rules, MovementProvider, and materials) and then instantiate that prefab at runtime. This is more robust and recommended.
- Added a one-frame re-injection coroutine (
ReinjectHandGrabRulesNextFrame) to try to address Start() ordering races when reflection is unavoidable.
- Navigate to
OscopeScripts/directory - Activate virtual environment:
cd c:\Users\robot\Documents\XREE-LAB\OscopeScripts venv\Scripts\activate.bat - Install dependencies (if needed):
pip install -r requirements.txt
- Update scope IP in
tcp_streaming_server.pyif changed (default:169.254.208.205) - Ensure oscilloscope is powered on and CH1/CH2 are displayed
- Start server:
Look for:
python tcp_streaming_server.py
[SCOPE] Connected: KEYSIGHT TECHNOLOGIES...and[SERVER] Using REAL scope data
- Open project in Unity 2022.3+ with URP
- Select
OscopeManagerGameObject in Hierarchy - In
TcpOscopeClientcomponent, setServer IPto your PC's LAN IP (e.g.,192.168.1.156) - In
WaveformPanelConnector, ensureStream Channel 1andStream Channel 2are checked - On each panel GameObject:
- Add
OscilloscopeGridcomponent (if not present) - Add
ScopeScaleUpdatercomponent and assign references:Tcp Client: OscopeManagerGrid: OscilloscopeGrid on same panelChannel Number: 1 or 2
- Adjust
Amplitude Scale(0.5-0.8 recommended) inWaveformPanelto fit waveforms in bounds
- Add
- Build Settings → Android → Switch Platform
- Build And Run to Meta Quest 3S
- Firewall: Allow Python and Unity through Windows Firewall (both inbound/outbound)
- IP Addresses:
- Scope: Link-local (169.254.x.x) or LAN IP
- PC Server: LAN IP (192.168.x.x), NOT 127.0.0.1 for device builds
- Quest: Check in Meta Quest Developer Hub or via
adb shell ip addr
- Testing: Use
telnet <server_ip> 8765to verify TCP connectivity before building
- Unity Logs (Device):
adb logcat -s Unity adb logcat | findstr TcpOscopeClient
- Meta Quest Developer Hub: Device → Logs tab → filter "TcpOscopeClient" or "WaveformPanelConnector"
- Server Logs: Watch Python console for connection events, stream commands, packet counts
- Verbose Mode: Enable
verboseLogginginTcpOscopeClientfor detailed packet statistics
- Real-time streaming of oscilloscope data from Keysight MSOX604A to Meta Quest over TCP
- Smooth waveform updates at ~20 Hz with automatic reconnection on disconnect
- Oscilloscope-style grid overlay with auto-updating voltage and time scale labels
- Hand/pinch grab interactions for moving panels in 3D space
- Prefab-based panel instantiation preserves Interaction SDK configuration
- Dual-channel display (CH1 & CH2) with independent waveform routing
- Automatic fallback to mock data if scope connection fails
- Amplitude scaling keeps waveforms within panel bounds
- If you assign a scene object instance (a Hierarchy object) to
WaveformManager.panelPrefabduring Play instead of the prefab asset from the Project view, that scene reference does not persist to builds - panels will not appear in the built player. Always assign the prefab asset (Project → Assets/Prefabs/Panel_CH). - Some earlier edits introduced duplicate/fragmented code in
WaveformManager.cswhich caused compile errors; those were cleaned up. - Runtime reflection is brittle across SDK versions and can miss private fields with different names or signature changes; prefer the prefab workflow.
- Shader/material pitfalls: if a material uses a shader that isn't available on the Android/Quest build (or not included in the build), objects may appear invisible or magenta on device. Use URP-compatible Unlit shaders for stable results and verify shaders are included.