This is the manual of Studio 1.1.0, an earlier version. The manual of the latest version
Documentation 1.1.0 Reference
File formats
_Draft (round 14): the text is being written; the sections may still grow._
Programs
A program is a G-code text file in the dialect of Grbl 1.1, which the Twinwright controller speaks. Studio reads any such file. The programs its CAM writes look like this:
(Sensor bracket) (tool T1 flat 6 n3 L20, 6061-T6: S13263, F836, plunge F279, depth of cut 1.20 mm, stepover 2.40 mm) (program zero: on the blank's top; the machine's top first; T1 is the length reference) G21 G90 G17 G94 G54 G53 G0 Z0 T1 M6 G53 G0 Z0 M3 S13263 G4 P3 (up to speed) (face X-2..62 Y-2..42, 0.3 mm deep: 1 layer of 12 passes) ... M5 M9 G53 G0 Z0 M30
| Part | What it is |
|---|---|
| Header | Comments: the job's title; the first tool, the material and the main cutting data; where the program's zero is |
| Modes | G21 G90 G17 G94 G54: millimetres, absolute, the XY plane, feed per minute, work offset G54 |
G53 G0 Z… | A move to the top of Z, in machine coordinates: at the start, at each tool change and at the end. The height is where homing leaves Z. |
T… M6 | A tool change. The controller measures the new tool on the tool setter. |
M3 S…, G4 P… | The spindle on, then a dwell while it comes up to speed. A water-cooled spindle's pump (M8) comes first. |
| Feature comments | Each feature starts with a comment that says what it cuts |
| Moves | G0, G1, and arcs G2/G3 with I and J. Drilling uses G81, or G83 with pecks, its R plane 1 mm over the top. |
| End | M5, M9, the move to the top, M30 |
Comments are in parentheses. Lines have no numbers.
Studio keeps each version of a program in the programs folder (Documents/Twinwright Studio/programs), in a folder of the program's name:
| File | What it holds |
|---|---|
v0001.nc | The program's text, as it was saved |
v0001.json | Its version, the time (UTC), who made it, why, the version it came from, its hash and its lines |
v0001.ops.json | What each feature became (operation, strategy, tool, lines, speed, feed): a setup sheet reads it |
Jobs and setups
A job is the description of a part that the CAM makes programs from. It is a JSON object:
| Key | Meaning |
|---|---|
name | The program's name, for example bracket.nc |
title, description | Words for the program's header and the setup sheet |
material | The material, for example 6061-T6; other standards' names are understood |
tool | The tool in the spindle, as a spec (flat 6 n3 L20) |
tools | The tool table: {"1": "flat 6 n3 L20", "2": "drill 3.4 118 L20"}. Left out: the machine's. |
cutting | Values that override the chosen ones: rpm, feed, ap, ae, finish_ap; corner_steps (true or false: a wall's inner corners cut in steps first) |
safe_z | The height the moves across go at, in millimetres over the work zero |
zero | Where the work zero is: front-left-top (the default) or center-top |
features | The features, in the order they are cut. See Making programs. |
setups | Instead of features: one object for each setup |
nest | Instead of features: parts nested on a plate |
A setup has its own features, and says how the part comes to it from the setup before:
| Key | Meaning |
|---|---|
turn | u: turned over about X (front and back change places); v: about Y (left and right) |
fixture | Held another way from this setup on (vise, clamps, tape …) |
zero | The work zero found again: front-left-top or center-top. Left out after a turn or a new fixture: front-left-top; otherwise the zero of the setup before. |
name, tool, tools, cutting, safe_z | The setup's own, over the job's |
{"name": "bracket.nc", "title": "Sensor bracket", "material": "6061-T6",
"tools": {"1": "flat 6 n3 L20", "2": "drill 3.4 118 n2 L20"},
"features": [
{"op": "face", "x0": -2, "y0": -2, "x1": 62, "y1": 42, "depth": 0.3, "tool": 1},
{"op": "pocket", "shape": "rect", "x0": 6, "y0": 6, "x1": 28, "y1": 34, "r": 3, "depth": 4, "tool": 1},
{"op": "pocket", "shape": "circle", "cx": 44, "cy": 20, "d": "Ø12 H7", "depth": 6, "tool": 1},
{"op": "drill", "points": [[44, 6], [44, 34]], "depth": 8, "tool": 2}]}
Each version of a job is kept beside the program it made, as job-v0001.json: its version, the time (UTC), who made it (operator or ai:<client>), a note, the program version it made, and the job itself. A job in setups also lists each setup's program, the version made and its hash.
Recordings (MCAP)
--mcap FILE records a run into an MCAP file (mcap.dev, format version 0): a channel for each kind of data, JSON messages, each channel with its JSON schema, not chunked and not compressed, and a summary at the end with the statistics and a CRC. Studio's tests read the files back against the MCAP specification; tools that read MCAP, such as PlotJuggler and Foxglove, are meant to open them, but that has not been tried yet.
--rec-period MSsets how often the sampled channels are written (1 ms unless given).- A recording changes nothing in the run: a run recorded and the same run not recorded are the same bit for bit.
--play FILE.mcapplays a recording back in Studio, with a timeline: play, pause, seek, 0.25–10 times the speed. Nothing is simulated while it plays.
Time
Each message's log time is the twin's virtual time, in nanoseconds from power-on. Most channels also carry t, the same time in seconds. A sampled channel writes a message only when something in it changed.
The header
A metadata record named mc1sim says what the run was:
| Key | Meaning |
|---|---|
machine | The machine's name |
drive_model | The drive model of the run: L0, L1, L2, L2/3 (three masses) or L3 |
f_cpu | The controller's clock, Hz |
nvs | A hash of the controller's stored settings when the run began, 16 hexadecimal digits |
sample_period_ms | The sampling period |
time | virtual, ns from power-on |
Channels
| Channel | Written | Field | Type | Unit | Meaning |
|---|---|---|---|---|---|
/machine | sampled | pos | number[3] | mm | Where each axis is: the mechanics, not the controller's idea of it |
mpos | number[3] or null | mm | The controller's machine position (MPos); null while the controller is off | ||
lost | number[3] | The steps each axis has lost | |||
feed | number | mm/min | The controller's feed rate now | ||
tip | number[3] | mm | The tool tip against the work: with the twin's error model on, moved by its errors; off, the same as pos | ||
/drives | sampled, with a drive model from L1 | ferr_um | number[3] | µm | Each drive's following error |
state | string[3] | Each drive's state | |||
km1 | boolean | The contactor KM1 closed | |||
bus48 | boolean | The drives' 48 V supply on | |||
/spindle | sampled, with a spindle drive | f_out | number | Hz | The drive's output frequency |
rpm | number | rpm | The spindle's speed | ||
amps | number | A | The drive's output current | ||
load_pct | number | % | The load as the drive reports it | ||
torque_nm | number | N·m | The motor's torque | ||
bus_v | number | V | The drive's DC bus voltage | ||
cmd_rpm | number | rpm | The speed the controller asks for | ||
run, pump | boolean | The drive running; the pump on | |||
power | string | The drive's own supply: ready, boot or off | |||
fault | string | The drive's fault, as its display shows it | |||
/controller | on change | state | string | The controller's state (Off while it is off) | |
alarm | integer | The alarm it is in (0: none) | |||
homed | integer | The axes homed, a bit each: X 1, Y 2, Z 4 | |||
limits | integer | The limit inputs that are active, a bit each | |||
probe, estop, motor_fault, door | boolean | The probe, E-stop, motor-fault and door inputs (the MC-1 has no door) | |||
on | boolean | The controller powered | |||
/power | on change | mains, v24, mcu, km1, bus48 | boolean | Mains; the 24 V supply; the controller running; KM1; the 48 V supply | |
vfd_mains, vfd, pump | boolean, string, boolean | The spindle drive's mains; its state (none, ready, boot, off); the pump | |||
/serial/in | as it comes | text | string | The bytes the controller received; control characters as \u escapes | |
/serial/out | as it comes | line | string | Each line the controller wrote | |
/events | as they happen | text | string | Each entry of the event log: marks, the supply, the drives, the spindle drive, collisions, broken wires | |
/cut | sampled, with a blank | cutting | boolean | Material coming off; while not, the figures below are 0 but rpm and removed_mm3 | |
mrr_mm3_s | number | mm³/s | The material removal rate | ||
ap_mm, ae_mm | number | mm | The axial depth and the radial width of the cut | ||
fz_mm | number | mm | The feed per tooth | ||
rpm | number | rpm | The spindle's speed | ||
fx_n, fy_n, fz_n | number | N | The cutting force along the machine's axes | ||
torque_nm, power_w | number | N·m, W | The cutting torque and power | ||
kc_j_mm3 | number | J/mm³ | The specific cutting energy | ||
removed_mm3 | number | mm³ | The material removed so far | ||
/input | as it comes | line | string | The session's inputs, line by line as --record writes them (see Session logs) | |
/shadow | as it comes, in shadow mode | host_s | number | s | The computer's time |
dir | string | t to the real machine, r from it | |||
data | string | The bytes, each as one character U+0000–U+00FF, up to 1 KB a message | |||
/ui/state, /sound | for the player | The views' state frame (30 a second while it changes) and the sound's frame (every 10 ms of virtual time when it changes). Studio's own: their fields may change from version to version. |
Pin traces (VCD)
--vcd FILE writes the controller's pins as a VCD file (IEEE 1364), which GTKWave and PulseView read: the scope mcu has the controller's pins, each change at the clock tick it happens (the step pins as pulses of $0 microseconds); the scope machine has the lines outside the controller: the supplies, KM1, the spindle drive, the pump, the E-stop button, the tool setter, the five switches. The timescale is 1 ns.
Session logs
--record FILE writes a session's inputs, each with its virtual time: what the senders sent, the real-time commands, the views' commands. --replay FILE runs the session again, bit for bit, with the same version of Studio and the same machine files. --replay FILE --branch S replays it to S seconds and goes on live from there; with --record the branch has a log of its own, which starts from its parent's (# branch <parent> <ticks>). A log is text; its lines are Studio's own and may change from version to version.
The world archive
What the twin keeps between sessions, as a real machine does when it is switched off and on again: where the axes stand, the blank with its cuts, the tool in the spindle, the controller's memory and the board's clock, and the heat, which cools down while the machine is off. The controller starts cold: the machine is homed again.
The window keeps each machine's world in the state folder (on Windows %LOCALAPPDATA%\Twinwright\Studio): the first machine's in world, the others' in machines\<id>\world. --world DIR uses another folder; --world-reset starts from the factory state. Batch runs, tests, replays, recordings played back and the AI's background machines keep no world unless given --world. One program at a time opens a world; a second one starts from the factory state and saves nothing.
| File | What it holds |
|---|---|
world.json | The session's end (below), written to world.json.tmp first, then renamed |
stock.snap | The blank's cells, cut or not (binary, TWSTOCK1) |
eeprom.bin | The controller's stored memory: its settings, offsets and tool data, written through as it changes |
world.json:
| Key | Meaning |
|---|---|
world | The format's version: 1 |
saved_at, saved_epoch | When it was saved: UTC, and seconds since 1970 |
version | The version of Studio that saved it |
pos | Where the axes stood, mm (X, Y, Z) |
tool | The tool in the spindle, as a spec (flat 6 n3 L20) |
tool_table | The job's tool table: {"1": "flat 6 n3 L20", ...} |
tool_auto | The twin plays the operator at a tool change: it puts in the tool from the table and presses cycle start |
setup | The workholding and the blank, as the window's Save setup writes them (null: none) |
cells | stock.snap holds the blank's cells |
thermal | The heat: screws (each screw over the frame), frame (over 20 °C), ambient (the room over 20 °C), part_heat and part_offset (the workpiece over the frame: what the cuts put in, and an offset given), all in K; spindle_um, the spindle's growth in µm |
rtc | The board clock's reading, s |
probe_cal | The touch probe as last calibrated: the ball's diameter, its effective radius, the lobing and its phase, the ring gauge's diameter, the largest deviation, when and by whom (only once it has been calibrated) |
odometer | travel_mm and reversals of each axis, spindle_h (the spindle's hours) and on_h (the program's hours), added up over the sessions |
At the next start the axes are where they stood; the same blank comes back with its cells, and the tool and the table; the heat has cooled for the time the machine was off, each part towards its rest by its own time constant; the board clock has run on. --world-off S says how long the machine was off (for tests; otherwise the time since it was saved). --pos, --tool, --stock, --setup and --part given at the start win over the archive.
Licence files
A licence file (.twlic) is a JSON object in UTF-8, signed with Ed25519. Studio checks it on the computer, without the network. Where the files live and how they are used: Licences, seats and activation.
This example is from the shared test vectors. It is signed with the test key (kid test), which release builds do not accept:
{"v": 1, "kid": "test", "id": "TWL-TEST000001", "edition": "pro", "kind": "subscription", "seats": 1,
"holder": "Weber Lab, TU Example", "issued": "2026-11-02T10:00:00Z", "valid_until": "2027-11-02T10:00:00Z",
"renew_after": "2027-10-28T10:00:00Z", "updates_until": null, "fp": ["0123456789abcdef0123456789abcdef"],
"mc1_serial": null,
"sig": "h3FTqZnDYtq8Q7_0ZuELLeDrTfyZnkTPgvDWSywTyl7BCJJxGIyhrBQUjYHJiE1_WOWaFarqbveFiMLrEfUyCA"}
Fields
| Field | Type | Meaning |
|---|---|---|
v | integer | The format's version: 1. Studio treats a file of another version as not valid. |
kid | string | Which of Twinwright's keys signed the file |
id | string | The licence's number: TWL- and 10 letters and digits. Renewal sends it. |
edition | string | maker or pro |
kind | string | subscription, perpetual (bought outright) or owner (MC-1 owner) |
seats | integer | The number of seats |
holder | string | Who the licence is for: a person or an organisation. Never an e-mail address. |
issued | date and time | When the file was signed, UTC, YYYY-MM-DDTHH:MM:SSZ |
valid_until | date and time, or null | A subscription: the end of the paid period. Bought outright: the end of the refund period in the first file, null in the permanent one. |
renew_after | date and time, or null | From when Studio tries to renew the file |
updates_until | date, or null | Bought outright, MC-1 owner: the versions released up to this day have the paid features |
fp | array of strings | The computers the licence is bound to: the hashes of their fingerprints, up to three a seat |
mc1_serial | string, or null | MC-1 owner: the machine's serial number. Studio does not check the machine when it runs. |
sig | string | The Ed25519 signature of the other fields, base64url without padding |
A field Studio does not know is part of the signed bytes, and Studio ignores what it means.
The signed bytes
The signature covers the file without sig, written in a canonical form:
- keys in Unicode code point order;
- no whitespace;
- strings with JSON's shortest escapes;
- integers without a decimal point; no other numbers, and no key twice.
Studio writes the file in this form before it checks the signature. Spaces, line breaks and the order of keys in the file therefore do not matter. A changed value makes the check fail.
The fingerprint's hash
The hash is the first 32 hexadecimal digits of SHA-256 over the bytes twinwright-fp-v1, a zero byte, and the computer's ID:
| System | The computer's ID |
|---|---|
| Windows | The value MachineGuid under HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography |
| Linux | /etc/machine-id |
Without either, Studio makes a random ID and keeps it in the licence folder (machine.id); deleting it makes the computer a different one. The ID itself never leaves the computer.
For example, the ID 8b1c3a0e-5f2d-4c7a-9e61-2d0f4b7a1c55 gives the hash 0a2d31bb82176516415efa7b3a340562.
The request code
The request code for offline activation is the canonical UTF-8 JSON {"fp": <the hash>, "version": <Studio's version>}, in base64url without = padding. For example, the hash 0123456789abcdef0123456789abcdef and version 1.0.0 give:
eyJmcCI6IjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmIiwidmVyc2lvbiI6IjEuMC4wIn0
TwinwrightStudio.exe --licence-request prints this computer's request code.
Trial files
A trial is a licence file like the others, signed by Twinwright's licence server: "kind": "trial", this computer's hash in fp, and valid_until 30 days after the trial began. It is never renewed. A trial file changed by hand no longer holds.
The machine archive
Every key of a machine's archive, with the MC-1's values: Machine profile schema.