Install this skill

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

Search the Adom Wiki for the skill "Instrument Viewer" (slug: instrument-viewer) at https://wiki-ufypy5dpx93o.adom.cloud/wiki/skills/instrument-viewer and install it into my local ~/.claude/skills/instrument-viewer/ directory. Fetch the skill_source content from the wiki page and save it as SKILL.md. Then confirm it's installed by showing the first 5 lines.
?
What is a skill? Skills are instructions that teach AI assistants like Claude Code how to perform specific tasks. The description below is loaded into the AI as context when you invoke this skill. Well-written skills make the AI significantly more effective. Like Wikipedia, anyone can improve a skill by clicking Edit AI Skill — or have your AI submit an edit on your behalf.

Description

Edit AI Skill

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

EndpointMethodBodyDescription
/pingGETHealth check (returns 204)
/dataGETLatest waveform data, settings, measurements
/set_acquisitionPOST{"mode": "run|stop|single"}Start/stop/single-shot acquisition
/set_timebasePOST{"value": 0.001}Set horizontal timebase (seconds/div)
/set_time_positionPOST{"value": 0.0}Set horizontal time offset
/set_channelPOST{"channel": 1, ...}Set channel params (see below)
/set_triggerPOST{"source": "1", ...}Set trigger params (see below)
/set_acquirePOST{"type": "NORM|AVER|HRES|PEAK"}Set acquisition type
/set_measurementsPOST[{"id":0,"source":"1","type":"FREQ"}]Set active measurements
/export_csvGETDownload 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

EndpointMethodBodyDescription
/instrumentsGETList all connected instruments
/instrumentsPOST{"ip": "10.0.3.83", "port": 5025}Connect a new instrument
/instruments/<ip>DELETEDisconnect an instrument
/instruments/<ip>/dataGETGet instrument state (readings, relay states, scan config)

DAQ Scan Control

EndpointMethodBodyDescription
/instruments/<ip>/scan/configurePOST{"channels": "220", "mtype": "VOLT:DC", "interval": 0.5, "nplc": 1.0}Configure and start scanning
/instruments/<ip>/scan/startPOST{}Resume a previously configured scan
/instruments/<ip>/scan/stopPOST{}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

EndpointMethodBodyDescription
/instruments/<ip>/relay/togglePOST{"channel": 101, "close": true}Close or open one relay
/instruments/<ip>/relay/open_allPOST{"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)101105109113
Y2 (y=1)102106110114
Y3 (y=2)103107111115
Y4 (y=3)104108112116

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_all to 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

SymptomCauseFix
"Disconnected" in viewerFlask server not running or proxy URL wrongCheck curl http://localhost:5000/ping returns 204
Scope shows flat lineInstrument not reachablepython3 -c "import socket; s=socket.socket(); s.settimeout(3); s.connect(('10.0.3.127', 5025)); print('OK')"
DAQ tab but no readingsScan not startedUse /scan/configure endpoint
Relay matrix doesn't match hardwareUI not syncedThe UI auto-syncs via closed_relays in /data. Refresh the viewer page.
Relay flickering on/off in UIVISA 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 errorsMissing headersEnsure server/__init__.py has the CORS @app.after_request handler
Low FPS in viewerPolling overhead (250ms)Expected. For high-speed, open proxy URL directly in new browser tab

Skill Source

Edit AI Skill
---
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

```bash
curl -s --max-time 2 http://localhost:5000/ping -o /dev/null -w "%{http_code}"
```

If NOT `204`, start it:

```bash
cd /home/adom/pyvisa-testscript && nohup python3 main.py > /tmp/flask.log 2>&1 &
```

### 2. Fetch and Display

```bash
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`

```bash
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`)

```json
{
  "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`)

```json
{
  "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_all` to 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:

```json
{
  "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
```bash
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
```bash
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
```bash
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
```bash
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
```bash
curl -s -X POST http://localhost:5000/set_timebase -H 'Content-Type: application/json' -d '{"value": 0.001}'
```

### Enable channel 2 at 500mV/div
```bash
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
```bash
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
```bash
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`:

```python
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 |

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!

0 revisions · Updated 2026-03-02 17:31:35