<!-- SPDX-License-Identifier: MIT -->
<!-- Copyright (c) 2026 Olympian LED -->

# current_probe_kit: a current probe on a scope channel, end to end, over SCPI

`current_probe_kit.py` checks that an oscilloscope channel set for a current probe reads in amperes. It covers the
channel's mean, the trigger level, the cursors, a math channel in watts, a current step and a trigger in amperes, the
probe's zero over time, and whether the unit and the ratio survive a saved setup and a power cycle. It is one
Python file (3.8 or newer, standard library only). It talks SCPI over the raw socket, TCP 5555 by default, and sends
only commands that the Rigol DHO800/DHO900 and MHO900 programming guides document. The same run therefore works on
Rigol's own firmware, on a modified firmware and on NovaOS, and the results compare like for like.

It is written for the **Micsig CP503B**, the version with 5 A and 30 A ranges and a 12 V adapter. [METHOD.md](METHOD.md)
gives the arithmetic and the source of every figure.

## What it measures

| test | command | what passes |
|---|---|---|
| 1. amps | `amps --range 5A` / `--range 30A` | at 0, 0.1, 0.5, 1, 2 and 2.9 A: the channel's reading minus its zero, taken at the same setting, within 1 % + 5 mA of the reference (5 A range) or 1 % + 20 mA (30 A range). The trigger level set to 1 must read back 1 (A), and cursor A placed at the reading must read the same value. |
| 2. power | `power` | a second channel with a 10x probe on the supply's output, math CH1 x CH2: its mean, in W, within 2 % of the supply's own V x I at 0.5, 1, 2 and 2.9 A |
| 3. step | `step` | the load stepping 0.5 <-> 1.5 A at 1 kHz; an edge trigger at 1 A in NORMal sweep fires on all 20 singles; the step's two levels within the band plus the load's own setting accuracy; the rise and the fall in A |
| 4. drift | `drift --minutes 30` | nothing in the jaws: the mean every minute, and the drift from the first reading (no pass mark: the datasheet gives none) |
| persistence | `persist-setup`, `persist-arm` + `persist-check` | the unit and the ratio come back from `:LOAD:SETup` of a file saved with `:SAVE:SETup`, and after a power cycle |

When a firmware does not take `:CHANnel<n>:UNITs AMPere`, a ratio or a custom ratio, the run records a **finding** and
carries on. That is a result about the firmware, not a failure of the kit. The kit also explains a reading that is off
by a whole factor: x0.01 means the firmware multiplies by the ratio where the guide says it divides by a V/A
sensitivity; x10 or x0.1 means the probe is on its other range. For x0.01 the kit measures once more with the
inverse ratio, to show which value makes that firmware read amperes.

## What you need

- The scope on your network: DHO800, DHO900 or MHO900.
- The current probe, powered from **its own 12 V adapter**. Never power it from the scope's USB port (user manual,
  PDF page 13).
- A DC current source that holds 0 to 2.9 A, and a reference ammeter. Our bench uses a Rigol DP2031 supply (CH1,
  12 V, current limit 3.0 A, OCP 3.2 A) as the reference. A Rigol DL3031 electronic load in constant-current mode sets
  the current. Without those, `--source manual` asks you to set each step and type your ammeter's reading.
- For test 2: a 10x passive probe on a second channel.
- Python 3.8 or newer.

## Wiring (once)

1. Check the probe's version: its range buttons read **5 A** and **30 A**, and a 12 V adapter is in the box. A probe
   with 6 A / 30 A buttons, or USB power, is the older model, which is sold under the same number.
2. Power the probe from its 12 V adapter. Let it warm up for **20 minutes** (user manual, PDF pages 13 and 25).
3. Close the jaw on nothing until it clicks. Then press **Zero** (degauss and zero). The probe also does this by
   itself at power-on. If the Zero button flashes 5 times, the zero failed (PDF page 12).
4. Plug the probe's BNC into the scope channel (CH1 by default). The kit sets that input to 1 MOhm itself; the probe
   needs 1 MOhm (PDF pages 14 and 24).
5. Wire the supply's output to the load's input. Clamp the probe around the + lead, with the arrow on the jaw
   pointing with the current, from the supply to the load (PDF page 14). The lead must be 5 mm across at most.
6. For test 2: a 10x probe on CH2, its tip on the supply's + terminal, its ground on the supply's - terminal.
7. Press the probe's range button to match `--range` (5A or 30A) before each run.

## Running it

```sh
python3 current_probe_kit.py amps  --host SCOPE --range 5A  --source manual     # any scope, a typed reference
python3 current_probe_kit.py amps  --host SCOPE --range 30A --source manual
python3 current_probe_kit.py power --host SCOPE --range 5A --vch 2 --source manual
python3 current_probe_kit.py step  --host SCOPE --range 5A --source manual      # your source steps 0.5 <-> 1.5 A
python3 current_probe_kit.py drift --host SCOPE --range 5A --minutes 30 --interval 60
python3 current_probe_kit.py persist-setup --host SCOPE --range 30A
python3 current_probe_kit.py persist-arm   --host SCOPE --range 30A --state s.json   # then power-cycle the scope
python3 current_probe_kit.py persist-check --host SCOPE --range 30A --state s.json
python3 current_probe_kit.py report run1/*.json run2/*.json                         # one table over runs
python3 current_probe_kit.py amps --host SCOPE --dry-run                            # every command, sent to nobody
python3 -I test_current_probe_kit.py                                                # the tests, offline
```

`SCOPE` is the scope's host name or address. `--dry-run` runs the whole test against a simulated scope and bench
built into the kit. It prints the plan, then every command as it would be sent, and touches no instrument.

| option | default | meaning |
|---|---|---|
| `--host H[:port]` | port 5555 | the scope |
| `--range 5A\|30A` | `5A` | the probe's range button: 1 V/A or 0.1 V/A |
| `--ch N` | 1 | the channel the probe is on |
| `--turns N` | 1 | turns of the lead through the jaws. The ratio sent is N x the sensitivity; a value off the guide's list tests a custom ratio (3 turns: 3 V/A) |
| `--ratio-convention guide\|multiplier` | `guide` | in amperes the MHO900 guide (3.6.9) takes `:CHANnel<n>:PROBe` as V/A; `multiplier` sends its inverse |
| `--source manual\|none` | `manual` for tests 1-3 | who sets the current and reads the reference (a bench module adds its own) |
| `--ref-acc REL,ABS` | `0.0015,0.005` | the reference ammeter's accuracy, for the datasheet budget |
| `--records N` | 5 | single acquisitions averaged for one reading |
| `--settle S` | 1.5 | seconds after each current change |
| `--top A` | 2.9 | the highest current step |
| `--vch N` | 2 | test 2: the channel with the 10x probe |
| `--minutes`, `--interval` | 30, 60 | test 4 |
| `--setup-path` | `C:/cpkit.stp` | persist-setup's file on the scope |
| `--state FILE` | `current-probe-persist-state.json` | persist-arm writes it, persist-check reads it |
| `--out-dir DIR` | `current-probe-<unit>-<test>-<UTC>` | where the JSON and the CSV go |
| `--firmware`, `--label` | | labels kept in the record (`stock`, `novaos`, a mod's name) |
| `--echo` | off | print every command as it is sent |

## What a run does to the scope

1. `*IDN?` and `*OPC?`. It refuses a model it is not written for (exit 2; `--untested-model` to try anyway).
2. It reads every setting it will change: each channel's display; the test channel's unit, ratio, impedance (MHO900),
   coupling, band limit, scale and offset; the acquisition type and the time base; the trigger mode, source, slope,
   sweep and the edge level on each source it touches; the waveform source, mode, format, start and stop; for test 2
   the second channel and Math1; for test 1 the manual cursors; for persist-setup `:SAVE:OVERlap`. It also reads the
   run state.
3. It sets the channel: `:CHANnel<n>:UNITs AMPere`, `:CHANnel<n>:PROBe <V/A>`, 1 MOhm, DC, the 20 MHz band limit
   (the probe's manual for small currents, PDF page 26).
4. It reads each value as `:SINGle` and `:WAVeform:DATA?` of the screen record (`NORMal`, `WORD`). The mean is computed
   in the kit, in the channel's own unit from the preamble. A reading at the screen's edge is taken again at ten
   times the scale, at most twice.
5. It puts back every setting that differs, in an order the scope accepts: a channel's unit before its ratio, the
   ratio before its scale and offset, the channel shown while those go back, the math and cursor sources before
   their switches, each trigger source's own level, the channels' display and the run state last. It does this twice
   if needed, then reads everything again and lists what still differs. A scope found stopped keeps the kit's last
   acquisition on screen.

The kit **never** sends `*RST`, a calibration, `:AUToscale`, a screenshot (`:DISPlay:DATA?`), any `:MEASure`
command, `:SYSTem:SETup`, a restart or a file command. `:MEASure:ITEM?` adds a live measurement item that stays, and
`:MEASure:DELete` would clear the user's own items, so the kit reads the waveform and does its own arithmetic. The one
exception is `persist-setup`: it saves and loads the setup file it names, `C:/cpkit.stp` on the scope's internal disk.
No documented command deletes that file, so it stays on the scope; delete it in the scope's file manager.
`persist-arm` leaves the channel in amperes on purpose, and `persist-check` puts it back.

Every read has a timeout. After a timeout the kit opens a new connection and sends `*IDN?` before it goes on.

## Output

Per run, in `--out-dir`: `<test>-<range>.json` and `<test>-<range>.csv`. The JSON holds:

- `unit` (the `*IDN?` model and firmware; the serial is never recorded, in the trace either), `firmware_label`,
  `range`, `turns`, `ratio_convention`.
- `stamp_start`, `stamp_end`: this PC's UTC on either side, the scope's own date and time over SCPI and, from our
  bench module that can read it, the unit's own `date -u`.
- `snapshot`, `restore`: every setting as found, what was written back, anything that still differs.
- `bench`: what the source reported about itself (with a bench module: the supply's and the load's identity and the
  supply channel's set points).
- `points`: per step: the set current, the scale, the zero, the reading, the corrected reading, the reference, the
  error, the band, the datasheet budget, the factor, pass / fail, the supply's CV / CC state and the load's readback.
- `results`: the channel as set and as read back, the trigger level, the cursor check, the factor and what it means,
  the step's levels and edges, the drift, and the persistence checks.
- `findings`: what a firmware did not take or did differently, each with its evidence.
- `verdict`: pass, fail or none (drift has no pass mark).
- `trace`: every command sent to the scope, the load and the supply, with its UTC time, its answer and the scope's
  latency.

Exit codes: 0 ran and restored; 1 refused or failed, restored; 2 usage error or an untested model; 3 the restore is
incomplete; 4 the scope stopped answering; 130 Ctrl-C, restored.

## A bench's own instruments

`--source manual` works with any source and any reference ammeter. A bench that drives its load and supply from a
program can add its own source as a module beside the kit, `current_probe_bench.py`. The kit loads it when it is
there, adds its options and its `--source` name, and calls it to set each current and read the reference. The
published kit runs without it.

## Known limits

- **The reference sets the floor.** The DP2031's current readback is 0.15 % + 5 mA (data sheet page 9), so at 0.1 A
  the reference alone uses most of the 5 A range's 6 mA band. The JSON gives the datasheet budget beside each band.
- **The zero is the scope's and the probe's.** Each step subtracts the reading with the load off at the same
  setting, which removes the scope's offset error (up to 0.1 div + 2 mV, data sheets) and the probe's residual zero.
  The uncorrected error is in the JSON as `raw_error`.
- **The scope's own measurement readout is not read.** That needs `:MEASure:ITEM?`, which adds an item that stays.
  The kit computes the mean from the waveform, and a person checks the measurement cards on screen.
- **The units on screen are not read.** No documented query returns the cursor's or the axis's unit text. The kit
  checks the values in amperes; the channel card and the axis are checked by eye.
- **The top step is 2.9 A,** below the DP2031's 3 A limit, so that the supply stays in CV. A point taken with the
  supply in CC is marked invalid.
- **One probe, one channel.** Another channel, another probe or the other range can differ. Run each one you use.

## Files

- `current_probe_kit.py`: the kit.
- `test_current_probe_kit.py`: the tests, against the simulated scope on 127.0.0.1. Run them with
  `python3 -I test_current_probe_kit.py`.
- `METHOD.md`: the arithmetic, and the source of every figure.
- `LICENSE`: MIT.

MIT licence (the SPDX line at the top of each file). Copyright (c) 2026 Olympian LED. No third-party code is included.
