Documentation 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
PartWhat it is
HeaderComments: the job's title; the first tool, the material and the main cutting data; where the program's zero is
ModesG21 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… M6A 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 commentsEach feature starts with a comment that says what it cuts
MovesG0, G1, and arcs G2/G3 with I and J. Drilling uses G81, or G83 with pecks, its R plane 1 mm over the top.
EndM5, 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:

FileWhat it holds
v0001.ncThe program's text, as it was saved
v0001.jsonIts version, the time (UTC), who made it, why, the version it came from, its hash and its lines
v0001.ops.jsonWhat 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:

KeyMeaning
nameThe program's name, for example bracket.nc
title, descriptionWords for the program's header and the setup sheet
materialThe material, for example 6061-T6; other standards' names are understood
toolThe tool in the spindle, as a spec (flat 6 n3 L20)
toolsThe tool table: {"1": "flat 6 n3 L20", "2": "drill 3.4 118 L20"}. Left out: the machine's.
cuttingValues 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_zThe height the moves across go at, in millimetres over the work zero
zeroWhere the work zero is: front-left-top (the default) or center-top
featuresThe features, in the order they are cut. See Making programs.
setupsInstead of features: one object for each setup
nestInstead 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:

KeyMeaning
turnu: turned over about X (front and back change places); v: about Y (left and right)
fixtureHeld another way from this setup on (vise, clamps, tape …)
zeroThe 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_zThe 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 MS sets 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.mcap plays 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:

KeyMeaning
machineThe machine's name
drive_modelThe drive model of the run: L0, L1, L2, L2/3 (three masses) or L3
f_cpuThe controller's clock, Hz
nvsA hash of the controller's stored settings when the run began, 16 hexadecimal digits
sample_period_msThe sampling period
timevirtual, ns from power-on

Channels

ChannelWrittenFieldTypeUnitMeaning
/machinesampledposnumber[3]mmWhere each axis is: the mechanics, not the controller's idea of it
mposnumber[3] or nullmmThe controller's machine position (MPos); null while the controller is off
lostnumber[3]The steps each axis has lost
feednumbermm/minThe controller's feed rate now
tipnumber[3]mmThe tool tip against the work: with the twin's error model on, moved by its errors; off, the same as pos
/drivessampled, with a drive model from L1ferr_umnumber[3]µmEach drive's following error
statestring[3]Each drive's state
km1booleanThe contactor KM1 closed
bus48booleanThe drives' 48 V supply on
/spindlesampled, with a spindle drivef_outnumberHzThe drive's output frequency
rpmnumberrpmThe spindle's speed
ampsnumberAThe drive's output current
load_pctnumber%The load as the drive reports it
torque_nmnumberN·mThe motor's torque
bus_vnumberVThe drive's DC bus voltage
cmd_rpmnumberrpmThe speed the controller asks for
run, pumpbooleanThe drive running; the pump on
powerstringThe drive's own supply: ready, boot or off
faultstringThe drive's fault, as its display shows it
/controlleron changestatestringThe controller's state (Off while it is off)
alarmintegerThe alarm it is in (0: none)
homedintegerThe axes homed, a bit each: X 1, Y 2, Z 4
limitsintegerThe limit inputs that are active, a bit each
probe, estop, motor_fault, doorbooleanThe probe, E-stop, motor-fault and door inputs (the MC-1 has no door)
onbooleanThe controller powered
/poweron changemains, v24, mcu, km1, bus48booleanMains; the 24 V supply; the controller running; KM1; the 48 V supply
vfd_mains, vfd, pumpboolean, string, booleanThe spindle drive's mains; its state (none, ready, boot, off); the pump
/serial/inas it comestextstringThe bytes the controller received; control characters as \u escapes
/serial/outas it comeslinestringEach line the controller wrote
/eventsas they happentextstringEach entry of the event log: marks, the supply, the drives, the spindle drive, collisions, broken wires
/cutsampled, with a blankcuttingbooleanMaterial coming off; while not, the figures below are 0 but rpm and removed_mm3
mrr_mm3_snumbermm³/sThe material removal rate
ap_mm, ae_mmnumbermmThe axial depth and the radial width of the cut
fz_mmnumbermmThe feed per tooth
rpmnumberrpmThe spindle's speed
fx_n, fy_n, fz_nnumberNThe cutting force along the machine's axes
torque_nm, power_wnumberN·m, WThe cutting torque and power
kc_j_mm3numberJ/mm³The specific cutting energy
removed_mm3numbermm³The material removed so far
/inputas it comeslinestringThe session's inputs, line by line as --record writes them (see Session logs)
/shadowas it comes, in shadow modehost_snumbersThe computer's time
dirstringt to the real machine, r from it
datastringThe bytes, each as one character U+0000–U+00FF, up to 1 KB a message
/ui/state, /soundfor the playerThe 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.

FileWhat it holds
world.jsonThe session's end (below), written to world.json.tmp first, then renamed
stock.snapThe blank's cells, cut or not (binary, TWSTOCK1)
eeprom.binThe controller's stored memory: its settings, offsets and tool data, written through as it changes

world.json:

KeyMeaning
worldThe format's version: 1
saved_at, saved_epochWhen it was saved: UTC, and seconds since 1970
versionThe version of Studio that saved it
posWhere the axes stood, mm (X, Y, Z)
toolThe tool in the spindle, as a spec (flat 6 n3 L20)
tool_tableThe job's tool table: {"1": "flat 6 n3 L20", ...}
tool_autoThe twin plays the operator at a tool change: it puts in the tool from the table and presses cycle start
setupThe workholding and the blank, as the window's Save setup writes them (null: none)
cellsstock.snap holds the blank's cells
thermalThe 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
rtcThe board clock's reading, s
probe_calThe 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)
odometertravel_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

FieldTypeMeaning
vintegerThe format's version: 1. Studio treats a file of another version as not valid.
kidstringWhich of Twinwright's keys signed the file
idstringThe licence's number: TWL- and 10 letters and digits. Renewal sends it.
editionstringmaker or pro
kindstringsubscription, perpetual (bought outright) or owner (MC-1 owner)
seatsintegerThe number of seats
holderstringWho the licence is for: a person or an organisation. Never an e-mail address.
issueddate and timeWhen the file was signed, UTC, YYYY-MM-DDTHH:MM:SSZ
valid_untildate and time, or nullA 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_afterdate and time, or nullFrom when Studio tries to renew the file
updates_untildate, or nullBought outright, MC-1 owner: the versions released up to this day have the paid features
fparray of stringsThe computers the licence is bound to: the hashes of their fingerprints, up to three a seat
mc1_serialstring, or nullMC-1 owner: the machine's serial number. Studio does not check the machine when it runs.
sigstringThe 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:

SystemThe computer's ID
WindowsThe 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.