APP

DSOX1204G v2 — Oscilloscope Web UI

Real-time web UI + Python/FastAPI backend + CLI for the Keysight DSOX1204G oscilloscope.

~60s tour: channel toggles, timebase, V/div, offset, trigger level, measurements — every visual change is a real CLI call against a live DSOX1204G

Install this skill

Paste this into Claude Code (VS Code panel, Adom editor, or terminal) to install:

Fetch the Adom Wiki app "DSOX1204G v2 — Oscilloscope Web UI" (slug: dsox1204g-v2) at https://wiki-ufypy5dpx93o.adom.cloud/wiki/apps/dsox1204g-v2. This is a knowledge-only app — no binary. Call GET https://wiki-ufypy5dpx93o.adom.cloud/api/v1/pages/dsox1204g-v2, extract the .page.skill_source field, and save it to ~/.claude/skills/dsox1204g-v2/SKILL.md (create the directory). Then confirm by showing the first 10 lines of the saved file.

DSOX1204G v2

A real-time, browser-based oscilloscope UI for the Keysight DSOX1204G (and compatible Keysight scopes over SCPI/LAN). Python/FastAPI backend talks raw SCPI-over-TCP to the hardware and streams waveform frames over a WebSocket; a Svelte 4 SPA renders them on a canvas at 60 fps with click-drag "knob" interactions, persistence mode, live measurements, and a matching CLI for scripting / AI control.

Hardware requirement (the "backend caveat")

This app is two pieces that need to run together:

  1. The frontend + control UI — runs in any browser. Lightweight, no hardware required to look at.
  2. The Python backend — must run on a container or machine that has a direct LAN path to the physical oscilloscope (port 5025, SCPI raw socket). The scope itself can't be put on the public internet without someone's LAN, and the backend needs to actually be reachable from the scope to stream waveforms.

If you don't have a DSOX1204G (or compatible Keysight scope) reachable on your LAN, you can still read the code, run the frontend in demo mode, or follow the patterns here to build your own scope UI. If you do have one, the backend can be pointed at its IP at runtime via the Device sidebar or POST /config/address.

A reference deployment is running at https://dsox1204g-ax3spi0ds2dc.adom.cloud/ against a DSOX1204G on the Adom office LAN. Access is gated by ownership of the underlying hardware.

Features

  • Canvas-rendered live waveforms with 4 channels, DPR-aware, 60 fps
  • Click-drag knobs — ground triangle = channel offset, T marker = trigger level, drag plot body = horizontal pan (timebase position), mouse wheel = H/div, shift+wheel = V/div of active channel
  • Live cursor readout — crosshair with tooltip showing time + per-channel voltage under the mouse
  • Measurements panel — per-channel Vpp, Vrms, Vavg, Freq, Period computed from streamed samples
  • Persistence mode — cycle 4/8/16/32 frames of intensity-graded trace history
  • Trigger control — source, slope, level, sweep, show/hide visual
  • Keyboard shortcuts — Space run/stop · 1–4 toggle channels · T trigger line · A autoscale · S single · R reset · ? help
  • CSV export — download current captures with a time column
  • Runtime scope address change — point the backend at a different scope without restarting
  • Matching CLI./bin/scope wraps every REST endpoint so every UI button has a shell command
  • Adom skill included so Claude can drive the CLI in an agent loop

Performance notes

  • Uses :WAVeform:FORMat BYTE (the DSOX1204G's ADC is 8-bit native; WORD was wasting half the bandwidth)
  • Only enabled channels are polled each frame
  • Waveform preamble cached per channel, invalidated when scale/offset/coupling change — saves a round-trip
  • Single compound SCPI command :WAV:SOUR CHAN<n>;:WAV:DATA? instead of two
  • Typical live throughput: ~20 Hz on 1 channel, halved per additional enabled channel. The scope's LAN firmware is the wall beyond that

DSOX1204G SCPI gotchas (discovered the hard way)

Both are documented inline in backend/server.py:

  1. :CHANnel<n>:DISPlay OFF (keyword form) hangs this scope. The numeric 0/1 form is used everywhere.
  2. :WAVeform:SOURce CHAN<n> implicitly re-enables channel <n>. If the capture loop iterates a stale local "enabled" list and queries a channel that was just disabled, the disable is silently reversed. Fix: update local enable-state inside the same lock as the SCPI write.

Repo

github.com/adom-inc/dsox1204g-v2 (private; contact kcknox for access)

Install the skill on a user container

The repo ships a SKILL.md under skills/dsox1204g/ that teaches Claude how to drive the CLI. To install it locally:

git clone [email protected]:adom-inc/dsox1204g-v2.git
ln -sfn "$PWD/dsox1204g-v2/skills/dsox1204g" ~/.claude/skills/dsox1204g

Then any prompt mentioning DSOX / Keysight scope / trigger / autoscale / channel scale will pick it up.

CLI quick reference

./bin/scope status
./bin/scope run | stop | single | autoscale
./bin/scope ch 1 on | off | scale 0.5 | offset 0 | coupling AC
./bin/scope tb scale 1e-6 | position 0
./bin/scope trig source CHAN1 | slope POS | level 0.5 | sweep AUTO
./bin/scope scpi query "*IDN?"

Point at a different backend: DSOX_HOST=http://host:5174 ./bin/scope ...

AI Skill — how Claude uses this app

Edit AI Skill

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

./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)

  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.

Sub-Skills
?
What are Sub-Skills?

Sub-skills are community-contributed AI skill extensions for this component. They teach AI assistants about specific tools, configurators, or workflows.

Examples:

  • A manufacturer’s configuration tool for a motor controller
  • A community-written design guide for an amplifier circuit
  • An automated test/validation script for a sensor module

How to add one: Click Add Sub-Skill, provide the URL to your skill and a brief description. Submissions are reviewed by the Adom team before going live.

No sub-skills yet. Be the first to contribute one!