This is where I measure how a servo behaves, and where the firmware in open-servo-core gets its reasons from. A loop, a limit or a default over there should have a notebook in here showing the data behind it.
The work is split into stages. Each stage reads the results of the stages before it, and each stage is also one bench setup, so the reading order and the bench order are the same.
Folder stages/0-instruments/. Targets the rulers. The DMM, the power resistors, the camera tag, the
thermocouple and the clock that lines their logs up.
The later stages compare their results against these, so each one gets its own error bar first. No board and no servo on the bench.
Folder stages/1-board/. Targets the controller board, one type at a time (dev board, swap board),
latest revision only.
Known loads only, power resistors and an RL dummy, with the rail swept by the DPS. This gives the conversions from ADC counts to volts and amps, the sample timing, the noise floor and the board NTC curve. Shares a sitting with stage 0.
Folder stages/2-motor/. Targets the motor on its own, by type. Today that is a small brushed DC
motor.
The bare motor sits in the printed motor rig, a base that seats the can with the leads under it and a wrench bar that holds the rotor by its pinion at one of twelve angles, with the DMM across its tabs. Resistance, inductance, the back-EMF constant and the no-load current are measured here with nothing else in the way, and the teardown at the end reads the copper on its own. Stage 3 is graded against these numbers. Same sitting as stage 1.
Folder stages/3-servo/. Targets the assembled servo, meaning the motor plus a gear train, a pot and
a case.
Open loop throughout, with the camera tag on the horn and the horn lock for
the held-rotor routes. Two groups of experiments live here. Electrical
repeats the stage 2 measurements in situ, the way osc-cal has to do them,
in one pass with one supply and no meter. Mechanical covers friction,
inertia, the gear train, the pot and the servo under a known load, down to
a crawl against the camera tag. The winding temperature constants are
earlier work and are not re-measured; the winding's own resistance against
its rest value is the thermometer. This is the long sitting, and the last
one that captures open loop.
Folder stages/4-estimators/. Targets the real-time estimators the firmware runs. Speed, position,
current zero and winding temperature.
No new captures. Each estimator is designed offline over the stage 3 data and graded against the stage 0 instruments.
Folder stages/5-control/. Targets the control loops. Current, velocity, position and trajectory,
including how a hold behaves.
The loops are designed from the measured plant and the estimators' noise and delay, over the stage 3 data. Each notebook ends with the loop the data supports, which is what the firmware carries, and the acceptance it has to meet in stage 7.
Folder stages/6-safety/. Targets the limits and the faults. The current ceiling, the stall and
position-error faults, the thermal derate and cutoff, the stop-hold limits.
Derived from stages 3 to 5, with the acceptance each limit and fault has to meet in stage 7, including on a sagging supply.
Folder stages/7-validation/. Targets the firmware as built from stages 4
to 6, mode by mode, on the servo of stage 3 and then on servos it was not
designed on.
The closed-loop image runs each mode against the stage 0 instruments and the acceptance the design stages pre-registered: position, with its step, hold and tracking error against the tag; velocity, from a crawl to full speed against the tag, smooth at the crawl; torque and the current ceiling against the DMM; the thermal estimate and derate against the winding's own resistance read at rest; the faults and the stop-hold limits, and all of it on a sagging supply. A mode passes or fails against its number, and a failure goes back to the stage that designed it. This is the bench gate for a release, and it is re-run on each new servo type.
One folder with three files. A capture that drives the bench and writes a
dataset under telemetry/, one notebook that reads the dataset and writes
its result, and the results.json it wrote. In stages 0 and 1 the capture
is a capture.py run on its own. In stage 2 the bench holds one state for
several experiments at once, so each declares its capture as plans in a
plan.py and python -m oscnb.sitting <state> runs every plan of a state
in one sitting, with a manifest of what ran under telemetry/.../sittings/;
the teardown's hand routes keep a capture.py. A notebook with no capture
of its own may keep a module beside it that only it imports. Each stage
folder has a README listing its experiments and how they chain.
Every notebook has the same sections, in the order a reader asks the questions. Purpose, why the number is needed and who uses it. Procedure, what the capture script does, step by step. Data, each file of the dataset shown on its own with its columns. Method, how the number is pulled out of the data, with the code. Results, the numbers in words and the write to the result file. Limitations, what the experiment does not cover. Open questions, each with the measurement that would settle it. Conclusion, the highlight: under which conditions the thing measured can be relied on, and what later experiments should take from it. References last, when a method leans on published work, numbered and cited in the text as [1]. Every code cell has a sentence above saying what it does, and every figure a sentence below saying what it shows.
Which loops exist and how they are shaped, which limits and faults exist, and the defaults shared by all servos come out of the measurements here. The board constants are the only numbers copied into the firmware. Servo numbers like resistance, back-EMF constant, friction and thermal gains are reproduced by the tools on the user's own servo.
The characterization, identification and calibration steps that osc-cal and
the osc-web calibration flow run on a user's servo start here
as experiments, in long form, with the model and its limits worked out on
real data. An experiment is written for a type of thing, like a brushed DC
motor or a pot, not for one servo model. The MG90 goes through first, the
SG90 second to check the methods carry over.
The notebooks hold the outside truth, a DMM, a camera, a thermocouple. The tools are scored against it, and the range where an experiment holds becomes the range where the tool stops.
A hobbyist without an EE degree should be able to follow along. The notebooks are committed with their outputs baked in, so they read straight from GitHub.
What the experiments use, by the names the stage READMEs refer to. The
scripts that drive the controlled ones live in equipment/.
An OWON B41T+ handheld multimeter, 4.5 digits, true RMS, with Bluetooth LE streaming and a K thermocouple input. It reads DC volts, ohms at about a milliamp, DC amps and temperature. equipment/b41t.py connects and logs every packet with the host time of arrival. Its Bluetooth address sits in equipment/registry.json so the script connects without a scan. Two bluez quirks the script handles on its own: a meter left connected stops advertising, so the script always disconnects on exit, and a stale discovery makes bluez refuse a new scan, which an adapter power cycle clears. A review with the measured accuracy is at lygte-info.dk. Amazon link: to add.
A FNIRSI DPS-150 bench supply, 0 to 30 V, 0 to 5 A, 150 W, fed from a 100 W USB-C PD adapter, scripted over USB serial through the fnirsi-dps150 package. equipment/dps150.py reads it, sets a voltage and a current limit, and switches the output. The voltage readback agrees with the DMM within millivolts. The current readback reads well under the true current and is not used as a measurement. Product page at Elektor. Amazon link: to add.
Three uxcell aluminium-housed wirewound power resistors, 5 ohm 50 W 5 %. One is used bare, one with a capacitor across it, and the three in series make about 15 ohm. Their values come from the power-resistors experiment in stage 0, not from the label. A power inductor of about 1 mH rated 2 A or more, in series with one resistor, makes the RL dummy for the bridge-state experiment. Amazon links: to add.
A welded K-type thermocouple on twisted leads, read on the DMM's temperature input. Its offset, slope and time constant come from the thermocouple experiment in stage 0. Amazon link: to add.
A phone filming two printed ArUco tags, one on the horn and one fixed
beside it, plus a printed ChArUco board for the lens calibration. The
pipeline lives in oscnb.fiducial. The phone model goes in the camera's
registry entry, since the lens calibration belongs to it.
- equipment/camera/tags.md: generating the tag and board sheets, and printing and mounting them.
- equipment/camera/iphone.md: setting an iPhone up for the tag videos and getting the clips onto the host.
- equipment/camera/extract.py: runs the pipeline over a dataset's clips once and writes what the notebooks read.
The original clips are not committed: a slow-motion phone clip runs to gigabytes, and the tag pipeline takes only the corners of two squares from each frame. A dataset holds the lens calibration, a corners table per clip, and a manifest with each original's size and hash. Each clip also has a 960 px real-time preview, a few megabytes, committed so that anyone replicating the experiment can see what was filmed and how. The originals stay on the host that captured them. The pipeline can be tried on the previews in place of the originals; the camera-tag notebook shows what that costs (a noise floor several times higher, the geometric error about the same).
Printed fixtures under rigs/, parametric CAD in build123d with the measured parts as their inputs. The motor rig of stage 2 seats the bare motor and holds its rotor at indexed angles. The servo rig of stage 3 holds the assembled servo and the board under the camera, with a tag mount on the stock horn, a bolted horn lock at mid travel, and a load arm, on one base that lies flat or stands on edge.
The servo board's STAT lamp, flashed from the host through the
stat_flash_ticks register (osc clapper). Like a film clapper, every
recording sees the same flash, so the camera's clip and the host's log can
be lined up afterwards.
The computer running osc and osc-lab over the osc adapter, with the capture scripts in
this repo. Python comes through uv, see Setup. On Linux, turn USB
autosuspend off for the Bluetooth dongle (options btusb enable_autosuspend=0
in modprobe.d). A dongle that suspends between the DMM's packets resets on
USB, comes back as a new adapter, and bluez forgets the meter.
Each notebook writes one file next to itself, results.json, with the
value, its bounds, the unit and the capture it came from. An experiment run
on a servo and board pair writes results/<servo>__<board>.json in its
folder instead, one file per pair. A later stage reads these files and
nothing else from the stages before it.
Once a stage settles, its README opens with a TL;DR. It holds the
constants a later stage reads, with their bounds, unit, confidence and
notebook, then the key findings, the open questions and what the next stage
takes. The constants table is not typed by hand. The stage's tldr.toml
holds the hand-written parts of each row, the label, the result and key, the
figures, what the bounds are and the confidence, and the numbers come from
the result files. After a re-record or a spec change,
uv run python -m oscnb.tldr rewrites the table between the
tldr:constants markers in the README, and uv run pytest fails while a
README is stale. A stage with no tldr.toml is skipped.
osc-lab is the little Rust tool in osc-lab/ that every capture
records with. It has two commands. osc-lab sweep drives open-loop duty
rungs and streams the raw TEL rows into sweep.csv. osc-lab burst takes
the high-rate shunt bursts from rest into burst-K.csv. Both write a
meta.json beside the data with the servo's constants at the time.
It is the only capture instrument. Nothing in it fits, identifies or
calibrates. It is a raw instrument on purpose, so a dataset never goes
through identification code on its way to disk. Everything else a capture
script does to the servo, like get, set, status or baud, still goes
through osc from the firmware repo.
To install it, you will need a nightly Rust toolchain (the
rust-toolchain.toml in the folder picks it for you). Then run this in
this folder:
cargo install --locked --path osc-lab
oscnb.osc finds it on the PATH, or at the path in the OSC_LAB
environment variable.
You will need uv. Then uv sync in this folder
installs Python 3.12 and the dependencies, uv run python equipment/dps150.py read
checks the DPS, and uv run jupyter lab opens the notebooks.
The principles and rules this repo follows are in PRINCIPLES.md.