DSOX1204G v2 — Oscilloscope Web UI
Public UnreviewedReal-time web UI + Python/FastAPI backend + CLI for the Keysight DSOX1204G oscilloscope.
name: dsox1204g description: Use when controlling a Keysight DSOX1204G oscilloscope (or another Keysight SCPI-over-LAN scope) via the dsox1204g-v2 backend. Covers running/stopping acquisition, toggling channels, setting vertical/horizontal scale, offsets, trigger source/slope/level/sweep, timebase, raw SCPI passthrough, and CSV export. Trigger phrases include "DSOX", "DSOX1204G", "Keysight scope", "oscilloscope CLI", "scope cli", "scope scpi", "run the scope", "set channel scale", "set trigger", "set timebase", "autoscale", "single capture", "turn channel on", "turn channel off".
DSOX1204G v2 — CLI and backend control
The dsox1204g-v2 project exposes a Python/FastAPI backend (by default on http://127.0.0.1:5174) that speaks SCPI to a Keysight DSOX1204G (or a compatible Keysight scope) over LAN port 5025. It ships with a thin Python CLI at bin/scope that wraps every REST endpoint — every button in the web UI has a matching subcommand.
Use this skill whenever the user asks you to drive the scope programmatically. Prefer the CLI over raw curl — same auth, same host detection via DSOX_HOST, and it handles JSON parsing for you.
When to use
- "turn on channel 2"
- "set timebase to 1 us/div"
- "autoscale the scope"
- "what is the current trigger level"
- "single capture on CH1"
- "set trigger source to external"
- "reset offsets and trigger"
- "download the current waveforms"
- "connect to a different scope at 10.0.3.128"
Prerequisites
- The backend must be running (
python3 backend/server.pyin the dsox1204g-v2 repo, or the corresponding service container's port). - The CLI is at
bin/scopeinside the repo. Eithercdto the repo and run./bin/scope ...or add it toPATH. DSOX_HOSToverrides the defaulthttp://127.0.0.1:5174if the backend is elsewhere.
Commands (copy-paste friendly)
State and acquisition
./bin/scope status # health + IDN
./bin/scope run # start continuous acquisition
./bin/scope stop # halt
./bin/scope single # single-shot
./bin/scope autoscale # :AUToscale (blocks on *OPC?)
./bin/scope connect # re-initialise the SCPI session
Channel control (ch <n> <action> [value])
./bin/scope ch 1 on # enable channel
./bin/scope ch 1 off # disable channel
./bin/scope ch 1 scale 0.5 # volts/div
./bin/scope ch 1 offset 0 # volts
./bin/scope ch 1 coupling AC # AC | DC | GND
Channel numbering is 1–4. Values accept scientific notation (5e-3).
Timebase
./bin/scope tb scale 1e-6 # 1 µs/div
./bin/scope tb position 0 # seconds, negative = pre-trigger
Trigger
./bin/scope trig source CHAN1 # CHAN1..4 | EXT | LINE
./bin/scope trig slope POS # POS | NEG | EITH
./bin/scope trig level 0.5 --source CHAN1
./bin/scope trig sweep AUTO # AUTO | NORM | SING
Raw SCPI passthrough
./bin/scope scpi write ":CHAN1:DISP 1"
./bin/scope scpi query "*IDN?"
./bin/scope scpi query ":SYST:ERR?"
Device address (change target scope at runtime)
# Switch the backend's target scope without restarting
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"ip":"10.0.3.128","port":5025}' \
http://127.0.0.1:5174/config/address
WebSocket contract
The frontend (and anything else that wants live waveforms) connects to ws://host:5174/ws and receives JSON messages:
{ "type": "state", "data": { channels:[...4], timebase, trigger, status, idn } }— initial snapshot on connect{ "type": "waveform", "waveforms": [{ "source": "CHAN1", "voltages": [...] }] }— per enabled channel each capture{ "type": "channels", "channels": [...] }— broadcast after any channel-level set{ "type": "tbtrig", "timebase": {}, "trigger": {}, "status": "running"|"stopped" }— periodic (~every 15 frames) and after timebase/trigger sets{ "type": "measurement", "result": {...} }— if/when the backend pushes rotated measurements
No inbound WS messages are expected — all control flows through REST.
DSOX1204G SCPI gotchas (already handled by the backend)
:CHANnel<n>:DISPlay OFF(keyword form) hangs this scope. Use0/1.:WAVeform:SOURce CHAN<n>implicitly re-enables channel<n>. The capture loop must never query a channel that was just disabled, or the disable is silently reversed. The backend guards this by updating local enable-state inside the same lock as the SCPI write.:RSTate?does not exist on this scope — do not use it to check run/stop.
If you ever add a new SCPI command that interacts with :WAVeform:* or channel display state, re-read these gotchas before debugging a "command doesn't work" bug.
Tips
- Use
./bin/scope statusbefore setting anything, to confirm the backend is reachable and the scope is connected. - After any change you care about, a quick
./bin/scope scpi query ":SYST:ERR?"returns+0,"No error"if the scope accepted the command cleanly. - For bulk scripted control, chain commands with
&&— each one is synchronous and exits non-zero on failure.
---
name: dsox1204g
description: Use when controlling a Keysight DSOX1204G oscilloscope (or another Keysight SCPI-over-LAN scope) via the dsox1204g-v2 backend. Covers running/stopping acquisition, toggling channels, setting vertical/horizontal scale, offsets, trigger source/slope/level/sweep, timebase, raw SCPI passthrough, and CSV export. Trigger phrases include "DSOX", "DSOX1204G", "Keysight scope", "oscilloscope CLI", "scope cli", "scope scpi", "run the scope", "set channel scale", "set trigger", "set timebase", "autoscale", "single capture", "turn channel on", "turn channel off".
---
# DSOX1204G v2 — CLI and backend control
The `dsox1204g-v2` project exposes a Python/FastAPI backend (by default on `http://127.0.0.1:5174`) that speaks SCPI to a Keysight DSOX1204G (or a compatible Keysight scope) over LAN port 5025. It ships with a thin Python CLI at `bin/scope` that wraps every REST endpoint — every button in the web UI has a matching subcommand.
**Use this skill whenever the user asks you to drive the scope programmatically.** Prefer the CLI over raw `curl` — same auth, same host detection via `DSOX_HOST`, and it handles JSON parsing for you.
## When to use
- "turn on channel 2"
- "set timebase to 1 us/div"
- "autoscale the scope"
- "what is the current trigger level"
- "single capture on CH1"
- "set trigger source to external"
- "reset offsets and trigger"
- "download the current waveforms"
- "connect to a different scope at 10.0.3.128"
## Prerequisites
1. The backend must be running (`python3 backend/server.py` in the dsox1204g-v2 repo, or the corresponding service container's port).
2. The CLI is at `bin/scope` inside the repo. Either `cd` to the repo and run `./bin/scope ...` or add it to `PATH`.
3. `DSOX_HOST` overrides the default `http://127.0.0.1:5174` if the backend is elsewhere.
## Commands (copy-paste friendly)
### State and acquisition
```bash
./bin/scope status # health + IDN
./bin/scope run # start continuous acquisition
./bin/scope stop # halt
./bin/scope single # single-shot
./bin/scope autoscale # :AUToscale (blocks on *OPC?)
./bin/scope connect # re-initialise the SCPI session
```
### Channel control (`ch <n> <action> [value]`)
```bash
./bin/scope ch 1 on # enable channel
./bin/scope ch 1 off # disable channel
./bin/scope ch 1 scale 0.5 # volts/div
./bin/scope ch 1 offset 0 # volts
./bin/scope ch 1 coupling AC # AC | DC | GND
```
Channel numbering is 1–4. Values accept scientific notation (`5e-3`).
### Timebase
```bash
./bin/scope tb scale 1e-6 # 1 µs/div
./bin/scope tb position 0 # seconds, negative = pre-trigger
```
### Trigger
```bash
./bin/scope trig source CHAN1 # CHAN1..4 | EXT | LINE
./bin/scope trig slope POS # POS | NEG | EITH
./bin/scope trig level 0.5 --source CHAN1
./bin/scope trig sweep AUTO # AUTO | NORM | SING
```
### Raw SCPI passthrough
```bash
./bin/scope scpi write ":CHAN1:DISP 1"
./bin/scope scpi query "*IDN?"
./bin/scope scpi query ":SYST:ERR?"
```
### Device address (change target scope at runtime)
```bash
# Switch the backend's target scope without restarting
curl -s -X POST -H 'Content-Type: application/json' \
-d '{"ip":"10.0.3.128","port":5025}' \
http://127.0.0.1:5174/config/address
```
## WebSocket contract
The frontend (and anything else that wants live waveforms) connects to `ws://host:5174/ws` and receives JSON messages:
- `{ "type": "state", "data": { channels:[...4], timebase, trigger, status, idn } }` — initial snapshot on connect
- `{ "type": "waveform", "waveforms": [{ "source": "CHAN1", "voltages": [...] }] }` — per enabled channel each capture
- `{ "type": "channels", "channels": [...] }` — broadcast after any channel-level set
- `{ "type": "tbtrig", "timebase": {}, "trigger": {}, "status": "running"|"stopped" }` — periodic (~every 15 frames) and after timebase/trigger sets
- `{ "type": "measurement", "result": {...} }` — if/when the backend pushes rotated measurements
No inbound WS messages are expected — all control flows through REST.
## DSOX1204G SCPI gotchas (already handled by the backend)
1. `:CHANnel<n>:DISPlay OFF` (keyword form) **hangs** this scope. Use `0`/`1`.
2. `:WAVeform:SOURce CHAN<n>` **implicitly re-enables** channel `<n>`. The capture loop must never query a channel that was just disabled, or the disable is silently reversed. The backend guards this by updating local enable-state inside the same lock as the SCPI write.
3. `:RSTate?` does not exist on this scope — do not use it to check run/stop.
If you ever add a new SCPI command that interacts with `:WAVeform:*` or channel display state, re-read these gotchas before debugging a "command doesn't work" bug.
## Tips
- Use `./bin/scope status` before setting anything, to confirm the backend is reachable and the scope is connected.
- After any change you care about, a quick `./bin/scope scpi query ":SYST:ERR?"` returns `+0,"No error"` if the scope accepted the command cleanly.
- For bulk scripted control, chain commands with `&&` — each one is synchronous and exits non-zero on failure.