name: instrument-viewer description: Use when the user wants to view instruments in the Gallia Viewer, display their oscilloscope or DAQ in the GV, show live waveforms in the viewer, or open the instrument monitor panel. Also use when the user wants to control instruments — toggle relays, configure scans, change scope settings, connect/disconnect instruments. Trigger phrases include "show scope in viewer", "instrument viewer", "show scope in gv", "display DAQ in viewer", "live waveform in gv", "open instrument monitor", "toggle relay", "close relay", "open relay", "start scan", "set timebase", "set channel", "connect instrument", "isolated mode".
Instrument Viewer — Display & Control Instruments in Gallia Viewer
Displays and controls the pyvisa-testscript live instrument monitor (oscilloscope waveforms, DAQ readings, relay matrices) inside the Gallia Viewer panel.
Source code: /home/adom/pyvisa-testscript
Server: Flask on port 5000
Config: /home/adom/pyvisa-testscript/config.py
Display in Viewer (Quick Start)
1. Ensure the Flask Server is Running
curl -s --max-time 2 http://localhost:5000/ping -o /dev/null -w "%{http_code}"
If NOT 204, start it:
cd /home/adom/pyvisa-testscript && nohup python3 main.py > /tmp/flask.log 2>&1 &
2. Fetch and Display
PROXY_URL="https://coder.$(cat /etc/hostname)-containers.adom.inc/proxy/5000"
Note: The workspace ID is found from the Coder URL pattern. Currently it is:
https://coder.noah-gallia-799ae51bab88845f.containers.adom.inc/proxy/5000
PROXY_URL="https://coder.noah-gallia-799ae51bab88845f.containers.adom.inc/proxy/5000"
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${PROXY_URL}', safe=''))")
curl -s "http://localhost:5000/viewer?proxy=${ENCODED}" > /tmp/instrument-viewer.html
Then call: gv_clear followed by gv_display_file(file_path='/tmp/instrument-viewer.html', title='Scope')
REST API Reference
All endpoints are at http://localhost:5000. Use curl from within the container.
Scope Control
| Endpoint | Method | Body | Description |
|---|---|---|---|
/ping | GET | — | Health check (returns 204) |
/data | GET | — | Latest waveform data, settings, measurements |
/set_acquisition | POST | {"mode": "run|stop|single"} | Start/stop/single-shot acquisition |
/set_timebase | POST | {"value": 0.001} | Set horizontal timebase (seconds/div) |
/set_time_position | POST | {"value": 0.0} | Set horizontal time offset |
/set_channel | POST | {"channel": 1, ...} | Set channel params (see below) |
/set_trigger | POST | {"source": "1", ...} | Set trigger params (see below) |
/set_acquire | POST | {"type": "NORM|AVER|HRES|PEAK"} | Set acquisition type |
/set_measurements | POST | [{"id":0,"source":"1","type":"FREQ"}] | Set active measurements |
/export_csv | GET | — | Download waveform data as CSV |
Channel Parameters (/set_channel)
{
"channel": 1,
"enabled": true,
"scale": 1.0,
"offset": 0.0,
"coupling": "DC",
"probe": 10.0,
"bwlimit": false,
"invert": false
}
All fields except channel are optional — include only what you want to change.
Trigger Parameters (/set_trigger)
{
"source": "1",
"slope": "POS",
"level": 1.5,
"mode": "AUTO"
}
All fields optional — include only what you want to change.
Instrument Management
| Endpoint | Method | Body | Description |
|---|---|---|---|
/instruments | GET | — | List all connected instruments |
/instruments | POST | {"ip": "10.0.3.83", "port": 5025} | Connect a new instrument |
/instruments/<ip> | DELETE | — | Disconnect an instrument |
/instruments/<ip>/data | GET | — | Get instrument state (readings, relay states, scan config) |
DAQ Scan Control
| Endpoint | Method | Body | Description |
|---|---|---|---|
/instruments/<ip>/scan/configure | POST | {"channels": "220", "mtype": "VOLT:DC", "interval": 0.5, "nplc": 1.0} | Configure and start scanning |
/instruments/<ip>/scan/start | POST | {} | Resume a previously configured scan |
/instruments/<ip>/scan/stop | POST | {} | Pause the scan loop |
Scan Parameters
- channels: Channel string —
"220","201-220","201,205,210", or mixed"201-205,210,220" - mtype: Measurement type —
"VOLT:DC","VOLT:AC","RES","FRES","TEMP:TC","TEMP:RTD","FREQ","DIOD","CURR:DC","CURR:AC" - interval: Seconds between scans (min 0.1)
- nplc: Integration time in power-line cycles —
0.02(fast),0.2,1(normal),10,100(accurate)
Relay / Actuator Control
| Endpoint | Method | Body | Description |
|---|---|---|---|
/instruments/<ip>/relay/toggle | POST | {"channel": 101, "close": true} | Close or open one relay |
/instruments/<ip>/relay/open_all | POST | {"slot": 1} | Open all relays in a slot |
4×4 Relay Matrix Channel Mapping
The relay matrix uses the formula: channel = base + x*4 + y
Where base = (slot - 1) * 100 + 101, x = column index (0–3), y = row index (0–3).
Slot 1 (base=101):
| X1 (x=0) | X2 (x=1) | X3 (x=2) | X4 (x=3) | |
|---|---|---|---|---|
| Y1 (y=0) | 101 | 105 | 109 | 113 |
| Y2 (y=1) | 102 | 106 | 110 | 114 |
| Y3 (y=2) | 103 | 107 | 111 | 115 |
| Y4 (y=3) | 104 | 108 | 112 | 116 |
Examples:
- Y1 → X1: channel 101 (
close: true) - Y2 → X4: channel 114 (
close: true) - Y3 → X2: channel 107 (
close: true)
Isolated Mode
Isolated mode is ON by default in the UI (one connection per row, one per column). When controlling relays via API, YOU must enforce isolation logic:
- Before closing a relay at (X, Y), open any existing relay in column X and row Y first
- Or use
/relay/open_allto clear the slot, then close only the desired relays
Relay State Sync
The /instruments/<ip>/data endpoint includes closed_relays showing which relays are physically closed on the instrument:
{
"closed_relays": {
"1": [101, 114]
}
}
The UI automatically syncs to match this hardware state on every poll cycle. Relay states are queried by the background DAQ loop thread (not the HTTP handler) to avoid VISA resource contention. This means relay toggle commands issued via the API will be reflected in the UI within one poll cycle (~0.25s when idle, or after the next scan cycle).
Common Commands (Copy-Paste Examples)
Connect the DAQ
curl -s -X POST http://localhost:5000/instruments -H 'Content-Type: application/json' -d '{"ip": "10.0.3.83", "port": 5025}'
Open all relays in slot 1, then close specific ones
curl -s -X POST http://localhost:5000/instruments/10.0.3.83/relay/open_all -H 'Content-Type: application/json' -d '{"slot": 1}'
curl -s -X POST http://localhost:5000/instruments/10.0.3.83/relay/toggle -H 'Content-Type: application/json' -d '{"channel": 101, "close": true}'
curl -s -X POST http://localhost:5000/instruments/10.0.3.83/relay/toggle -H 'Content-Type: application/json' -d '{"channel": 114, "close": true}'
Start scanning channel 220 at 0.5s
curl -s -X POST http://localhost:5000/instruments/10.0.3.83/scan/configure -H 'Content-Type: application/json' -d '{"channels": "220", "mtype": "VOLT:DC", "interval": 0.5}'
Stop scanning
curl -s -X POST http://localhost:5000/instruments/10.0.3.83/scan/stop -H 'Content-Type: application/json' -d '{}'
Set scope timebase to 1ms/div
curl -s -X POST http://localhost:5000/set_timebase -H 'Content-Type: application/json' -d '{"value": 0.001}'
Enable channel 2 at 500mV/div
curl -s -X POST http://localhost:5000/set_channel -H 'Content-Type: application/json' -d '{"channel": 2, "enabled": true, "scale": 0.5}'
Set trigger to channel 1, rising edge, 1.5V
curl -s -X POST http://localhost:5000/set_trigger -H 'Content-Type: application/json' -d '{"source": "1", "slope": "POS", "level": 1.5}'
Single-shot acquisition
curl -s -X POST http://localhost:5000/set_acquisition -H 'Content-Type: application/json' -d '{"mode": "single"}'
Instrument Configuration (config.py)
Edit /home/adom/pyvisa-testscript/config.py:
IP_ADDRESS = "10.0.3.127" # Primary oscilloscope IP
VISA_ADDRESS = f"TCPIP0::{IP_ADDRESS}::5025::SOCKET"
DRIVER_NAME = "keysight_dsox1204g" # Must match drivers/__init__.py REGISTRY
STARTUP_INSTRUMENTS: list[dict] = [
{"ip": "10.0.3.83", "port": 5025}, # Keysight DAQ970A
]
After editing config.py, restart the Flask server.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| "Disconnected" in viewer | Flask server not running or proxy URL wrong | Check curl http://localhost:5000/ping returns 204 |
| Scope shows flat line | Instrument not reachable | python3 -c "import socket; s=socket.socket(); s.settimeout(3); s.connect(('10.0.3.127', 5025)); print('OK')" |
| DAQ tab but no readings | Scan not started | Use /scan/configure endpoint |
| Relay matrix doesn't match hardware | UI not synced | The UI auto-syncs via closed_relays in /data. Refresh the viewer page. |
| Relay flickering on/off in UI | VISA contention (fixed) | Relay queries now run in the DAQ background thread only. If still seen, ensure routes.py does NOT query relay states in the HTTP handler. |
| CORS errors | Missing headers | Ensure server/__init__.py has the CORS @app.after_request handler |
| Low FPS in viewer | Polling overhead (250ms) | Expected. For high-speed, open proxy URL directly in new browser tab |