这是 Studio 1.1.0 的手册,不是最新版本。 最新版本的手册
文档 1.1.0 接口参考
文件格式
_草稿(第 14 轮):正文在写,各节还会补。_
程序
程序是 G 代码文本文件,用 Grbl 1.1 的写法,同匠的控制器认这种写法。这样的文件 Studio 都能读。它的 CAM 写出的程序是这样的:
(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
| 部分 | 是什么 |
|---|---|
| 程序头 | 注释:作业的标题;第一把刀、材料和主要切削参数;程序零点在哪里 |
| 模式 | G21 G90 G17 G94 G54:毫米、绝对坐标、XY 平面、每分钟进给、工件坐标 G54 |
G53 G0 Z… | 按机床坐标移到 Z 的最高处:开头、每次换刀和结尾。这个高度是回零后 Z 停的地方。 |
T… M6 | 换刀。控制器在对刀仪上量新刀。 |
M3 S…、G4 P… | 开主轴,再暂停一会儿等它转到转速。水冷主轴先开它的水泵(M8)。 |
| 特征的注释 | 每个特征开头有一行注释,说明切什么 |
| 移动 | G0、G1,圆弧 G2/G3 带 I 和 J。钻孔用 G81,啄钻用 G83,R 平面在顶面上方 1 mm。 |
| 结尾 | M5、M9、移到最高处、M30 |
注释写在括号里。行不编号。
Studio 把程序的每个版本存在程序目录(文档/Twinwright Studio/programs)里,以程序名为名的文件夹中:
| 文件 | 存什么 |
|---|---|
v0001.nc | 程序的文本,按存的时候原样 |
v0001.json | 它的版本号、时间(UTC)、谁出的、为什么、从哪个版本来、哈希和行数 |
v0001.ops.json | 每个特征变成了什么(工序、策略、刀具、行号、转速、进给):装夹单读它 |
作业和装夹
作业是 CAM 出程序所依据的零件描述,是一个 JSON 对象:
| 键 | 意思 |
|---|---|
name | 程序的名字,比如 bracket.nc |
title、description | 写进程序头和装夹单的文字 |
material | 材料,比如 6061-T6;别的标准的牌号也认得 |
tool | 主轴上的刀,按规格写(flat 6 n3 L20) |
tools | 刀具表:{"1": "flat 6 n3 L20", "2": "drill 3.4 118 L20"}。不写时用机床的。 |
cutting | 覆盖自动选定值的参数:rpm、feed、ap、ae、finish_ap;corner_steps(true 或 false:壁的内角先分步切) |
safe_z | 横移的高度,从工件零点往上算,单位毫米 |
zero | 工件零点在哪里:front-left-top(默认)或 center-top |
features | 各个特征,按切的顺序。见出程序。 |
setups | 代替 features:每次装夹一个对象 |
nest | 代替 features:在板料上排料的零件 |
每次装夹有自己的 features,并写明零件从上一次装夹怎么过来:
| 键 | 意思 |
|---|---|
turn | u:绕 X 翻面(前后对调);v:绕 Y 翻面(左右对调) |
fixture | 从这次装夹起换一种夹法(vise、clamps、tape …) |
zero | 重新找的工件零点:front-left-top 或 center-top。翻面或换了夹具又没写时,取 front-left-top;否则沿用上一次装夹的零点。 |
name、tool、tools、cutting、safe_z | 这次装夹自己的,覆盖作业的 |
{"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}]}
作业的每个版本都存在它出的程序旁边,叫 job-v0001.json:版本号、时间(UTC)、谁出的(operator 或 ai:<客户端>)、备注、它出的程序版本,以及作业本身。分几次装夹的作业还列出每次装夹的程序、出的版本和它的哈希。
录制(MCAP)
--mcap 文件 把一次运行录成 MCAP 文件(mcap.dev,格式版本 0):每类数据一个通道,消息是 JSON,每个通道带它的 JSON Schema,不分块、不压缩,文件末尾有统计和 CRC 的摘要。Studio 的测试按 MCAP 规范把文件读回来核对过;PlotJuggler、Foxglove 这类读 MCAP 的工具应当打得开,但还没实际试过。
--rec-period 毫秒设采样通道多久写一次(不给就是 1 ms)。- 录制不改变运行:录着的一次和没录的同一次逐位相同。
--play 文件.mcap在 Studio 里放一段录制,带时间轴:播放、暂停、拖动定位、0.25–10 倍速。放的时候什么都不仿真。
时间
每条消息的记录时间是孪生的虚拟时间,从上电起算的纳秒。多数通道还带 t,是同一个时间,单位秒。采样通道只在内容变了的时候才写一条。
文件头
一条名为 mc1sim 的元数据记录,说这是一次什么样的运行:
| 键 | 意思 |
|---|---|
machine | 机床的名字 |
drive_model | 这次运行的驱动模型:L0、L1、L2、L2/3(三质量)或 L3 |
f_cpu | 控制器的时钟,Hz |
nvs | 运行开始时控制器所存设置的散列,16 位十六进制 |
sample_period_ms | 采样周期 |
time | virtual, ns from power-on |
通道
| 通道 | 什么时候写 | 字段 | 类型 | 单位 | 意思 |
|---|---|---|---|---|---|
/machine | 采样 | pos | number[3] | mm | 各轴在哪里:机械本身,不是控制器以为的 |
mpos | number[3] 或 null | mm | 控制器的机床位置(MPos);控制器没开时为 null | ||
lost | number[3] | 各轴丢了多少步 | |||
feed | number | mm/min | 控制器此刻的进给速度 | ||
tip | number[3] | mm | 刀尖相对工件:孪生的误差模型开着时按它的误差移过,关着时和 pos 一样 | ||
/drives | 采样,驱动模型从 L1 起 | ferr_um | number[3] | µm | 各驱动器的跟随误差 |
state | string[3] | 各驱动器的状态 | |||
km1 | boolean | 接触器 KM1 吸合 | |||
bus48 | boolean | 驱动器的 48 V 电源有电 | |||
/spindle | 采样,有变频器时 | f_out | number | Hz | 变频器的输出频率 |
rpm | number | rpm | 主轴的转速 | ||
amps | number | A | 变频器的输出电流 | ||
load_pct | number | % | 变频器报的负载 | ||
torque_nm | number | N·m | 电机的转矩 | ||
bus_v | number | V | 变频器的直流母线电压 | ||
cmd_rpm | number | rpm | 控制器要的转速 | ||
run、pump | boolean | 变频器在运行;水泵开着 | |||
power | string | 变频器自己的供电:ready、boot 或 off | |||
fault | string | 变频器的故障,照它面板上显示的 | |||
/controller | 变化时 | state | string | 控制器的状态(没开时为 Off) | |
alarm | integer | 当前的报警(0:没有) | |||
homed | integer | 已回零的轴,每轴一位:X 1、Y 2、Z 4 | |||
limits | integer | 有效的限位输入,每路一位 | |||
probe、estop、motor_fault、door | boolean | 探针、急停、电机故障、门的输入(MC-1 没有门) | |||
on | boolean | 控制器有电 | |||
/power | 变化时 | mains、v24、mcu、km1、bus48 | boolean | 市电;24 V 电源;控制器在运行;KM1;48 V 电源 | |
vfd_mains、vfd、pump | boolean、string、boolean | 变频器的市电;它的状态(none、ready、boot、off);水泵 | |||
/serial/in | 来的时候 | text | string | 控制器收到的字节;控制字符写成 \u 转义 | |
/serial/out | 来的时候 | line | string | 控制器写出的每一行 | |
/events | 发生的时候 | text | string | 事件日志的每一条:标记、供电、驱动器、变频器、碰撞、断线 | |
/cut | 采样,有毛坯时 | cutting | boolean | 正在切下材料;不切时下面的数除 rpm 和 removed_mm3 外都是 0 | |
mrr_mm3_s | number | mm³/s | 材料去除率 | ||
ap_mm、ae_mm | number | mm | 轴向切深和径向切宽 | ||
fz_mm | number | mm | 每齿进给 | ||
rpm | number | rpm | 主轴的转速 | ||
fx_n、fy_n、fz_n | number | N | 沿机床各轴的切削力 | ||
torque_nm、power_w | number | N·m、W | 切削的转矩和功率 | ||
kc_j_mm3 | number | J/mm³ | 比切削能 | ||
removed_mm3 | number | mm³ | 到此刻一共去掉的材料 | ||
/input | 来的时候 | line | string | 会话的输入,一行一行,照 --record 写的(见会话日志) | |
/shadow | 来的时候,影子模式下 | host_s | number | s | 电脑的时间 |
dir | string | t 发往真机,r 从真机来 | |||
data | string | 那些字节,每个字节一个字符 U+0000–U+00FF,一条消息最多 1 KB | |||
/ui/state、/sound | 给播放用 | 界面的状态帧(变化时每秒 30 次)和声音的帧(变化时每 10 ms 虚拟时间一次)。是 Studio 自己用的:字段随版本可能会变。 |
引脚波形(VCD)
--vcd 文件 把控制器的引脚写成 VCD 文件(IEEE 1364),GTKWave、PulseView 读得了:作用域 mcu 是控制器的引脚,每个变化记在它发生的那一拍(步进脚画成 $0 微秒宽的脉冲);作用域 machine 是控制器外面的线:各路电源、KM1、变频器、水泵、急停按钮、对刀仪、五只开关。时间刻度 1 ns。
会话日志
--record 文件 把一次会话的输入连同各自的虚拟时间写下来:发送软件发的、实时命令、界面的命令。--replay 文件 把这次会话再跑一遍,逐位相同,前提是同一版本的 Studio、同样的机床文件。--replay 文件 --branch 秒 重放到那一秒,再从那里实时接着走;加 --record 时分叉有自己的日志,从父日志开始(# branch <父日志> <拍数>)。日志是文本;它的行是 Studio 自己用的,随版本可能会变。
世界存档
孪生在两次会话之间留着的东西,就像真机关了再开:各轴停在哪里、毛坯和切过的地方、主轴上的刀、控制器的存储和板上时钟,还有热——关机这段时间里慢慢凉下来。控制器是冷启动:机床要重新回零。
窗口把每台机床的世界放在状态文件夹里(Windows 上是 %LOCALAPPDATA%\Twinwright\Studio):第一台的在 world,其他的在 machines\<编号>\world。--world 目录 用别的文件夹;--world-reset 从出厂状态开始。批量运行、测试、重放、放录制、AI 的后台机器都不留世界,除非给了 --world。同一个世界同时只有一个程序能打开;第二个从出厂状态开始,什么也不存。
| 文件 | 存的是什么 |
|---|---|
world.json | 会话结束时的样子(见下),先写成 world.json.tmp 再改名 |
stock.snap | 毛坯的格子,切过的和没切的(二进制,TWSTOCK1) |
eeprom.bin | 控制器的存储:它的设置、偏置和刀具数据,一变就写进去 |
world.json:
| 键 | 意思 |
|---|---|
world | 格式的版本:1 |
saved_at、saved_epoch | 什么时候存的:UTC,以及 1970 年以来的秒数 |
version | 存它的 Studio 的版本 |
pos | 各轴停在哪里,mm(X、Y、Z) |
tool | 主轴上的刀,写成刀具规格(flat 6 n3 L20) |
tool_table | 作业的刀具表:{"1": "flat 6 n3 L20", ...} |
tool_auto | 换刀时孪生替操作员做:按刀具表装上刀、按循环启动 |
setup | 装夹和毛坯,和窗口的“保存装夹”写的一样(null:没有) |
cells | stock.snap 里存着毛坯的格子 |
thermal | 热:screws(各丝杠高于机架)、frame(高于 20 °C)、ambient(室温高于 20 °C)、part_heat 和 part_offset(工件高于机架:切削带进去的,以及给定的偏置),单位都是 K;spindle_um 是主轴的伸长,单位 µm |
rtc | 板上时钟的读数,s |
probe_cal | 测头最近一次标定的结果:球的直径、有效半径、三瓣和它的相位、环规的直径、最大偏差、什么时候、谁标的(标定过才有) |
odometer | 各轴的 travel_mm 和 reversals,spindle_h(主轴转过的小时数)和 on_h(程序开着的小时数),跨会话累计 |
下次开机时,各轴还在原处;同一块毛坯带着它的格子回来,刀和刀具表照旧;热按关机的时间凉下来,各部分按各自的时间常数回到静止的状态;板上时钟走过了这段时间。--world-off 秒 指定关了多久(测试用;不给就是存档以来的时间)。启动时给了 --pos、--tool、--stock、--setup、--part,就用它们,不用存档里的。
授权文件
授权文件(.twlic)是一个 UTF-8 的 JSON 对象,用 Ed25519 签名。Studio 在本机核验,不用联网。文件放在哪儿、怎么用:见授权、席位和激活。
下面这个例子取自共用的测试向量,用测试密钥签(kid 是 test),正式发布的版本不认它:
{"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"}
字段
| 字段 | 类型 | 意思 |
|---|---|---|
v | 整数 | 格式版本,是 1。别的版本的文件,Studio 按无效处理。 |
kid | 字符串 | 用同匠哪一把密钥签的 |
id | 字符串 | 授权编号:TWL- 加 10 位字母和数字。续期时发的就是它。 |
edition | 字符串 | maker 或 pro |
kind | 字符串 | subscription(订阅)、perpetual(买断)或 owner(MC-1 机主) |
seats | 整数 | 席位数 |
holder | 字符串 | 授权给谁:个人或单位的名字,从不放电子邮箱 |
issued | 日期时间 | 签发时间,UTC,YYYY-MM-DDTHH:MM:SSZ |
valid_until | 日期时间或 null | 订阅:已付费那一期的结束。买断:第一个文件里是退款期的结束,永久文件里是 null。 |
renew_after | 日期时间或 null | Studio 从什么时候开始试着续期 |
updates_until | 日期或 null | 买断、MC-1 机主:这一天以前发布的版本能用付费功能 |
fp | 字符串数组 | 绑定的电脑:它们指纹的散列,每个席位最多三个 |
mc1_serial | 字符串或 null | MC-1 机主:机器的序列号。Studio 运行时不查机器。 |
sig | 字符串 | 其余字段的 Ed25519 签名,base64url,不补 = |
Studio 不认识的字段也在签名里;Studio 不管它的意思。
签名的字节
签名覆盖去掉 sig 以后的文件,按规范的写法写出来:
- 键按 Unicode 码点排序;
- 没有空白;
- 字符串用 JSON 最短的转义;
- 整数不带小数点;不许有别的数,也不许同一个键出现两次。
Studio 核验签名之前,先把文件按这个写法写一遍。所以文件里的空格、换行和键的顺序都不要紧;改了任何一个值,核验就不过。
指纹的散列
散列是对这些字节算 SHA-256 后取前 32 位十六进制:twinwright-fp-v1、一个零字节、电脑的 ID。
| 系统 | 电脑的 ID |
|---|---|
| Windows | HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography 下的 MachineGuid |
| Linux | /etc/machine-id |
两样都没有时,Studio 生成一个随机 ID,存在授权文件夹里(machine.id);删了它就等于换了一台电脑。ID 本身从不离开电脑。
例如 ID 8b1c3a0e-5f2d-4c7a-9e61-2d0f4b7a1c55 的散列是 0a2d31bb82176516415efa7b3a340562。
请求码
离线激活用的请求码,是规范写法的 UTF-8 JSON {"fp": <散列>, "version": <Studio 的版本>} 做 base64url,不补 =。例如散列 0123456789abcdef0123456789abcdef、版本 1.0.0 得到:
eyJmcCI6IjAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmIiwidmVyc2lvbiI6IjEuMC4wIn0
TwinwrightStudio.exe --licence-request 打出这台电脑的请求码。
试用文件
试用就是一份授权文件,和别的授权文件一样由同匠的授权服务器签名:"kind": "trial",fp 里是这台电脑的散列,valid_until 是开始试用后 30 天。试用文件从不续期。手改过的试用文件不再有效。
机床档案
机床档案的每一项,带 MC-1 的值:机床档案格式。