后处理改造 · 修订 14 —— 坐标里加变量号(X[1.2+#646])防忘文章

对象:mill_intergrex_300_2022 后处理(.tcl / .def / .cdl / .pui)
机床:Mazak INTEGREX 300(车铣复合 · 5 轴)· 系统 FANUC · NX 2606
需求编号:需求③(坐标宏 / Add # Variable)
本文用途:防忘。忘了从头看第 0 章;要改代码看第 3、4 章;要查现场问题看第 7 章。
行号约定:文中行号一律指明文母本 mill_intergrex_300_2022_bf.tcl(559,160 字节,
即"已降级 ni、加密前的最后一份明文")。加密后的 .tcl 是字节码,里面没有行号。


目录

章 内容
0 结论速查(30 秒回忆版)
1 为什么要加这个变量号
2 加变量号的方法是什么
3 具体代码(4 段,含位置)
4 主要代码逐行解释(起什么作用、有什么用)
5 这个实现的逻辑是什么
6 真 Tcl 8.6 实测数据
7 维护、排错、跨机纪律
8 附录:术语与变量速查

第 0 章 · 结论速查

一句话:给某道工序在 NX 里加一个 UDE 开关(add_variable = Active),
这道工序输出的每个 X / Y 就从 X1.2 变成 X[1.2+#646];
工序一结束开关自动关掉,后面没加事件的工序逐字不变。

问题 答案
加在哪 工序的开始事件里加 UDE add_variable,状态 Active
变成什么样 X1.2 → X[1.2+#646];Y3.4 → Y[3.4+#647](Z、I/J/K、R、F 一律不动)
变量号怎么来 默认 X = 600+刀号*5-4、Y = 600+刀号*5-3(T10 → #646/#647)
代码在哪 .tcl 里 4 段:事件入口 / 开关函数 / 输出前改写 / 工序结束复位
要不要动 .def 不要。本方案一个字节都不碰 .def
什么时候失效 每道工序结束时由 PB_CMD_collect_cutting_time 自动复位
怎么验收 加事件的工序有方括号,没加的工序与改动前逐字一致

四处改动位置一览:

# 文件 proc 明文行号 作用
1 .tcl MOM_add_variable 782–785 事件入口(NX 一遇到这个 UDE 就调它)
2 .tcl PB_CMD_add_variable_flag 5802–5840 读事件状态 → 打开本工序的开关
3 .tcl PB_CMD_before_output 的 B 段 5973–6008 每行落盘前改写 X/Y
4 .tcl PB_CMD_collect_cutting_time 末尾 6382–6384 工序结束 → 关开关
附 .pui Control Functions 表 777 把 add_variable 事件登记给 proc(不做 = 事件被静默丢弃)

第 1 章 · 为什么要加这个变量号

1.1 问题的起点:坐标是"死数"

后处理吐出来的坐标,原本都是写死的数值:

G0 X111.5 Y0.0 Z62.
G1 X84.9138 Y15.7205 F141.48

这个 111.5 是出程序那一刻从模型上算出来的。以后只要工件在机床上的位置和出程序时不一样
(夹具换了一套、工件在转台上重新装夹、找正后差了几丝、托盘/工位换了),
坐标就得重新算、程序就得重新出、工件号就得重新对。

1.2 加变量号之后:数值 + 宏变量

FANUC 允许在地址后面直接用表达式([...] 是宏程序的表达式括号),于是可以写成:

G0 X[111.5+#646] Y[0.0+#647] Z62.
G1 X[84.9138+#646] Y[15.7205+#647] F141.48

机床执行时,实际落点 = 后处理算出来的基准值 + #646 里当时的值。
#646 是公共宏变量(#500 以上断电保持),可以在机床侧随时改:

结果:偏移量变了,NC 程序一个字都不用改、不用重出。
这是这套写法唯一的目的 —— 把"必须重新出程序才能改的量",变成"机床上改一个变量就能改的量"。

1.3 为什么是"每把刀一组"

这台后处理的 #6xx 区早就按"每把刀占 5 格"分配好了,不是为需求③新开的区:

寿命变量编号 = 600 + 刀号*5(.tcl 第 9194 行注释,寿命段代码)

以 T10 为例,5 格是 #646 ~ #650:

槽位 T10 的变量 用途 谁写的
5N-4 #646 X 坐标偏移 需求③(本文)
5N-3 #647 Y 坐标偏移 需求③(本文)
5N-2 #648 H 调正量(长度方向) PB_CMD_custom_command_10,注释原文 (H TIAO ZHENG BIAN LIANG:#648)
5N-1 #649 D 调正量(半径方向) 同上,(D TIAO ZHENG BIAN LIANG:#649)
5N #650 寿命计数(GOTO 判断用) 寿命段 #650=#650+1

也就是说:5N-2 / 5N-1 原来是"换刀临时变量",已经被挪走了,5N-4 / 5N-3 是空出来的两格,
坐标偏移正好落在这一组的头上。所以公式才是 600 + 刀号*5 - 4(X)和 - 3(Y)。

一句话:不是"随便找个变量",而是"塞进这套后处理已经约定好的、每刀一组的槽位里"。
好处是:每把刀可以有自己的 X/Y 偏移(不同刀具装夹位置/主轴不同,偏差本来就不一样),
而且和 H/D 调正量、寿命计数互不打架。

1.4 为什么要"逐工序开关"

同样的 X 字,在这个后处理里还兼职干别的活(.def 里 90 个块含 X/Y):

行 X/Y 是 能不能加变量号
G53 G0 X0. Y0. 机床坐标 ❌ 加了就撞
G28 / G30 回参考点中间点 ❌
G52 / G92 / G10 局部坐标 / 坐标设定 / 数据输入 ❌
G4 X1.0 X 是时间(秒) ❌
G68 / G68.2 旋转中心 / 倾斜面 ❌
G0/G1/G2/G3、G8x 工件坐标 ✅ 只有这些该加

所以做法必须是"按需开":
需要偏移的工序加事件,其它工序一个字不动,改造零回归风险。

1.5 适用 / 不适用

适用 不适用(本方案的跳过名单,实测均不被改写)
直线/快速/圆弧:G0 G1 G2 G3 G53 / G53.1 机床坐标
全部循环:G8x(钻/镗/攻/铰) G28 / G30 回参考点
只有 X / 只有 Y 的行 G52 / G92 / G10 坐标类
模态省略 G 的裸坐标行(X1.2 Y3.4) G4 / G04 延时(X 是时间)
G68 / G68.2 / G68.5 旋转/倾斜
含 ( ) 的注释行、操作者信息行
#650=#650+1、IF [#650 GT 37] GOTO 101 等宏变量行(本来就不含裸 X/Y,天然不动)

已知未拦:G65 P.. X.. Y..(宏调用传参)不在跳过名单里 → 实测会被改写。
本机程序若用到 G65 传参,需要在第 3.3 节的跳过规则里补 |65。目前没用,先记着。


第 2 章 · 加变量号的方法是什么

2.1 整体手法:两件事咬合

   ①  UDE 事件  →  打开本工序的"开关"(一个全局变量)
   ②  输出钩子  →  开关开着,就把即将落盘的那一行文本改写

关键在于改哪一层。PB 在每一行 NC 落盘之前留了一个官方钩子:

也就是说:每一个 NC 块都会经过它,不用挂任何事件,拿到的就是"已经拼好的整行文本"
(G0 X1.2 Y3.4 的字面内容,小数点、前导零都是后处理算好的写法)。

于是做法变成:不动 .def 的块定义,只改这一行的文本。

2.2 数据流(一次工序的完整时序)

工序开始序列
   └─ UDE: add_variable = Active
         └─ MOM_add_variable          (.tcl 782)
               └─ PB_CMD_add_variable_flag   (.tcl 5802)
                     ├─ 读 mom_command_status → "ACTIVE"
                     ├─ set ::nc_addvar_on 1        ← 开关打开
                     ├─ 读 mom_x_var / mom_y_var → ::nc_x_var / ::nc_y_var(可选)
                     └─ 只影响本道工序

工序内每一个 NC 块(快速/直线/圆弧/循环…)
   └─ 拼好一行 → MOM_before_output → PB_CMD_before_output(.tcl 5931)
         ├─ A 段:循环 R 值跟踪(需求②,与本需求无关)
         └─ B 段:开关 == 1 ? 改写 X/Y : return   ← 本文主角
               X1.2 → X[1.2+#646]

工序结束序列(Operation End Sequence)
   └─ {End of Path} 的第一项 = PB_CMD_collect_cutting_time(.tcl 6334)
         └─ 末尾:set ::nc_addvar_on 0        ← 开关关闭

下一道工序(若没加事件)→ 开关是 0 → 输出逐字不变

2.3 要动哪些文件(清单)

文件 动不动 动什么
.tcl 要动 4 段代码(第 3 章全文)。这是唯一必须改的文件
.pui 已登记 777 行:{add_variable} {PB_CMD_add_variable_flag} {add_variable} {UDE}。不登记,事件会被静默丢弃
.cdl(后处理自己的) 现状够用 EVENT add_variable(第 3–15 行)只有 command_status。想让事件里能手填变量号,才需要加 x_var / y_var 两个 PARAM
NX 全局 ude.cdl 现状够用 同上(.../MACH/resource/user_def_event/ude.cdl 第 53–65 行)。改完必须重启 NX,且出程序那台机器也要一致
.def 不动 本方案一个字节都不碰。桌面的 mill_intergrex_300_2022_EXPR.def 是旧方案(修订4)的产物,别用

⚠ 当前状态(文章落笔时):.cdl 与 NX 全局 ude.cdl 里的 add_variable 都只有 command_status,
所以事件框里看不到"变量号"输入格 —— 现在走的是默认公式。
要启用手填,按第 4.5 节/第 7.4 节补两个 PARAM 并重启 NX。

2.4 操作步骤(NX 里怎么用)

  1. 打开工序(操作导航器 → 双击工序);
  2. 找到开始事件那一栏 —— 与加 set_new_tool、set_tool_life 的位置完全一样;
  3. 添加 UDE → 选 add_variable → 状态选 Active → 确定;
  4. 出程序,检查这道工序的 G0/G1/G2/G3 和 G8x 是不是 X[..+#646] Y[..+#647];
  5. 换一道没加事件的工序,检查它的输出必须与改动前逐字一致。

2.5 为什么不采用另外两种做法(都试过)

做法 结果
A. .def 里每条坐标拆成两个字(X_wrap 表达式 + X 数值),用 \opt 互斥 能用,但要在 .def 里改 140 行、新增 2 个地址;且"模态省略重复坐标"会变成按字符串比较,容易出新问题。→ 保留作参考,不采用(这就是修订 4 + _EXPR.def)
B. 块里同放两个 X,用 MOM_suppress once X 屏蔽一个 不成立。① once 只管一个块,循环事件一次出好几个块 → 会出两份坐标;② MOM_disable_address X 是全局开关,会把 G53/G28/G30/G4 里的 X 一起干掉(回零段变 G28 G91)。
C. 改即将落盘的那一行文本(本文采用) 只改1 个 proc;循环多块天然覆盖;不株连;数字格式沿用后处理已算好的写法,逐字一致;最坏失败模式也只是"没改写",程序照样能跑

一句话对比:

对比项 A/B C(本方案)
要改的地方 .def 140 行 + 多个事件表 1 个 proc 的 B 段
循环多块 覆盖不到 / 出两份 天然覆盖
G53/G28 株连 会 按 G 代码规则跳过
数字格式 要自己 format,容易不一致 沿用原文本,零差异
最坏结果 X 整字消失 最多"没改写"

第 3 章 · 具体代码

以下 4 段就是全部改动。位置见每段标题。改的是明文母本,改完再加密(见修订13)。

3.1 事件入口(.tcl 第 782–785 行)

proc MOM_add_variable { } {
#=============================================================
   PB_CMD_add_variable_flag
}

说明:MOM_<事件名> 是 PB 的约定 —— .pui 里登记了 add_variable → PB_CMD_add_variable_flag,
PB 保存时就会把 MOM_add_variable 生成为"调用该命令"。这段其实不用手写,
在 PB 里登记好自己就会出现;手改 .pui 的话才需要补。

3.2 开关函数(.tcl 第 5802–5840 行,函数名 PB_CMD_add_variable_flag)

proc PB_CMD_add_variable_flag { } {
#=============================================================
#=============================================================
# 【需求③】工序"开始事件"里加了 UDE "add_variable" 且状态为 Active 时,
#           只对【本道工序】打开坐标宏开关;工序结束时由
#           PB_CMD_collect_cutting_time 自动关闭。
#   注意:NX 送进来的状态值是全大写 ACTIVE,所以做大小写无关判断。
#   事件里可填 x_var / y_var 指定宏变量号;填 0 或不填则用默认公式:
#       X -> #600 + 刀号*5 - 4        Y -> #600 + 刀号*5 - 3
#=============================================================
   global mom_command_status
   global mom_x_var mom_y_var

   set st ""
   if { [info exists mom_command_status] } {
      set st [string tolower [string trim $mom_command_status]]
   }

   set ::nc_addvar_on 0
   set ::nc_x_var 0
   set ::nc_y_var 0

   if { [lsearch -exact {"" inactive off no false disable disabled none} $st] < 0 } {

      set ::nc_addvar_on 1

      set v 0
      if { [info exists mom_x_var] } { regexp {[0-9]+} $mom_x_var v }
      if { $v > 0 } { set ::nc_x_var $v }

      set v 0
      if { [info exists mom_y_var] } { regexp {[0-9]+} $mom_y_var v }
      if { $v > 0 } { set ::nc_y_var $v }
   }

   if { [info exists ::nc_dbg] && $::nc_dbg == 1 } {
      MOM_output_literal "(DBGADD on=$::nc_addvar_on x=$::nc_x_var y=$::nc_y_var)"
   }
}

3.3 输出前改写 —— PB_CMD_before_output 的 B 段(.tcl 第 5973–6008 行)

该 proc 从第 5931 行开始,A 段(5940–5971)是循环 R 值跟踪(需求②),一字未动。
下面是 B 段,紧跟在 A 段之后、} 之前。

   # ============ B. coord macro (UNCHANGED - rev6) ============
   if { ![info exists ::nc_addvar_on] || $::nc_addvar_on != 1 } { return }
   if { ![info exists mom_o_buffer] || [string length $mom_o_buffer] == 0 } { return }

   set buf $mom_o_buffer

   if { [string first $mom_sys_control_out $buf] >= 0 } { return }
   if { [string first ")" $buf] >= 0 } { return }
   if { [regexp {G(53|28|30|52|92|10|04|4|68)\M} $buf] } { return }

   set tn 0
   if { [info exists mom_tool_number] } {
      set tn $mom_tool_number
      if { $tn > 100 } { set tn [expr {int(double($tn) / 100.0 + 0.5)}] }
   }
   set xn [expr {600 + $tn * 5 - 4}]
   set yn [expr {600 + $tn * 5 - 3}]
   if { [info exists ::nc_x_var] && $::nc_x_var > 0 } { set xn $::nc_x_var }
   if { [info exists ::nc_y_var] && $::nc_y_var > 0 } { set yn $::nc_y_var }

   set lx $mom_sys_leader(X)
   set ly $mom_sys_leader(Y)
   set n 0

   if { [string length $lx] > 0 } {
      set pat [format {%s(-?[0-9.]+)} $lx]
      set rep [format {%s[%s+#%s]} $lx "\\1" $xn]
      incr n [regsub -all $pat $buf $rep buf]
   }
   if { [string length $ly] > 0 } {
      set pat [format {%s(-?[0-9.]+)} $ly]
      set rep [format {%s[%s+#%s]} $ly "\\1" $yn]
      incr n [regsub -all $pat $buf $rep buf]
   }

   if { $n > 0 } { set mom_o_buffer $buf }

同时,proc 开头的 global 清单(第 5933–5936 行)需要这几项:

   global mom_o_buffer
   global mom_sys_leader
   global mom_sys_control_out
   global mom_tool_number

3.4 工序结束复位 —— PB_CMD_collect_cutting_time 末尾(.tcl 第 6382–6384 行)

# 【需求③】本道工序结束,关闭坐标宏开关,
   #            防止污染后面没加 add_variable 事件的工序。
   if { [info exists ::nc_addvar_on] } { set ::nc_addvar_on 0 }

⚠ 这里有一个真实缺陷,见第 5.7 节 —— 该函数第 6347 行有一句
if { ![info exists mom_tool_number] } { return },在复位之前。
一旦某道工序没有 mom_tool_number,就会提前 return,复位被跳过、开关残留。
建议把复位语句挪到第 6345 行(那句 ::nc_new_tool_request 清标记)旁边,即移到 return 之前。


第 4 章 · 主要代码逐行解释

4.1 MOM_add_variable(事件入口)

proc MOM_add_variable { } {          ;# NX 在工序里遇到 add_variable 这个 UDE 时调用的入口
   PB_CMD_add_variable_flag          ;# 唯一动作:转交给开关函数
}
行 起什么作用 有什么用
proc MOM_add_variable { } 按 PB 命名规则生成的事件入口(MOM_ + 事件名) NX 处理到"工序开始序列里的这个 UDE"时,会自动调到它。入口不写,事件就是空转
PB_CMD_add_variable_flag 转调开关函数 把"事件识别"和"具体逻辑"分开:入口永远只有一行,逻辑全在 PB_CMD_* 里,好维护

4.2 PB_CMD_add_variable_flag(读状态 → 开开关)

行号 代码 起什么作用 有什么用 / 为什么这么写
5812 global mom_command_status 声明要读 NX 送进来的事件状态 这是 UDE 里那个下拉框(Active/Inactive)的值
5813 global mom_x_var mom_y_var 声明可选的手填变量号 事件里填了才会有;没填这两个变量根本不存在,后面靠 info exists 兜
5815–5818 set st "" / if {[info exists mom_command_status]} / set st [string tolower [string trim $mom_command_status]] 把状态取成小写、去空格的字符串 ① 必须先判存在:没加事件时这个变量不存在,直接读会报错;② string tolower 是因为 NX 真实送进来的是全大写 ACTIVE(这是当初踩过的坑,写 == "Active" 会永远判错);③ string trim 防手改 .cdl 时带进空格
5820–5822 set ::nc_addvar_on 0 / set ::nc_x_var 0 / set ::nc_y_var 0 先无条件清零(默认不生效) 关键设计:每次事件触发都先复位,避免上一次的残留(例如上一道工序手填过 656)漏到这一次
5824 if { [lsearch -exact {"" inactive off no false disable disabled none} $st] < 0 } { 判断状态是否**"不是关闭类"** 用"黑名单"而不是"白名单 == "active"",好处:将来 NX 的取值多一个变体(例如 on、true)也不会误判成关闭。-exact 必须加:否则 -gl 模糊匹配会把 off 当成……更重要的原因是精确匹配才可靠(lsearch 默认允许通配符)
5826 set ::nc_addvar_on 1 打开本工序的开关 用一个全局(:: 前缀)变量跨 proc 传递状态:事件函数设、输出钩子读、结束函数清。用 :: 前缀避免和 proc 内的同名局部变量撞车
5828–5830 set v 0 / if { [info exists mom_x_var] } { regexp {[0-9]+} $mom_x_var v } / if { $v > 0 } { set ::nc_x_var $v } 从事件里抠出 X 的变量号 ① info exists 兜住"事件里没这一格";② 用 regexp {[0-9]+} 只取数字——用户可能填 #656、656、656,regexp 都能抠出 656(填 #656 直接拿字符串会出错);③ > 0 才算数:填 0 / 空 = "走默认公式"的约定
5832–5834 同样处理 mom_y_var 抠出 Y 的变量号 与 X 对称,独立判定(可以只填 X 不填 Y)
5837–5839 if { [info exists ::nc_dbg] && $::nc_dbg == 1 } { MOM_output_literal "(DBGADD on=... )" } 调试开关 打开总开关 ::nc_dbg(.tcl 第 6880 行)后,NC 里会多一行 (DBGADD on=1 x=0 y=0),用来确认"事件到底有没有送达"。确认完记得把 ::nc_dbg 改回 0

这一段解决了什么问题:把"NX 里点了个下拉框"翻译成"本工序的一个 Tcl 开关",
并且把"变量号"这个可选项也一并准备好(填了用手填值,没填用 0 表示"走默认")。

4.3 PB_CMD_before_output 的 B 段(逐行)

PB_CMD_before_output 由 MOM_before_output 对每一个 NC 块自动调用,
参数就是"已经拼好、等着落盘的那一行文本",放在全局变量 mom_o_buffer 里。
本段只读它、必要时改写它,绝不调用任何 MOM_ 输出命令(否则递归)。

行号 代码 起什么作用 有什么用 / 为什么这么写
5973 # ============ B. coord macro (UNCHANGED - rev6) ============ 段落标记 说明这段与"修订6 版"一致,方便以后比对
5974 `if { ![info exists ::nc_addvar_on] $::nc_addvar_on != 1 } { return }`
5975 if { ![info exists mom_o_buffer] || [string length $mom_o_buffer] == 0 } { return } 空行/没有缓冲时不处理 有些内部调用可能没有缓冲内容,防 regsub 拿空串做出意外结果
5977 set buf $mom_o_buffer 把待处理文本取到局部变量 后面要对 buf 做多次 regsub(改写两遍:先 X 后 Y),用局部变量更清晰;最后再一次性写回
5979 if { [string first $mom_sys_control_out $buf] >= 0 } { return } 注释/控制行不碰 mom_sys_control_out 在本后处理里 = "("(.tcl 第 160 行)。(T10 X Y) 这类行里的 X/Y 是文字,不是坐标
5980 if { [string first ")" $buf] >= 0 } { return } 含右括号的行也不碰 双保险:MOM_output_literal "(...)" 出来的消息行、操作者信息行
5981 if { [regexp {G(53|28|30|52|92|10|04|4|68)\M} $buf] } { return } 不是工件坐标的行不碰 这是最重要的一条安全规则。\M 是 Tcl 正则的"词尾"锚:G53、G28、G30、G52、G92、G10、G04/G4、G68(含 G68.2/G68.5)都会命中。这些行里的 X/Y 分别是机床坐标、回零中间点、局部坐标、时间、旋转中心 —— 加了变量号就撞机。用 \M 而不是 \> 或 $,是因为 G 码后面可能跟 G53.1 之类的小数
5983–5987 set tn 0 / if { [info exists mom_tool_number] } { set tn $mom_tool_number; if { $tn > 100 } { set tn [expr {int(double($tn) / 100.0 + 0.5)}] } } 取本行所属刀具的刀号 ① info exists 兜底:取不到就用 0(对应 #596/#597,不报错);② > 100 时是"刀号×100+补偿号"的编码(例如 104 表示 1 号刀+4 号补偿),必须还原成整数刀号,否则变量号会飞到 #1116 那种非法区。这条规则与 PB_CMD_custom_command_10 完全一致(那里也做同样的还原),必须一致,否则同一把刀在换刀段和坐标宏里会对不上号
5988–5989 set xn [expr {600 + $tn * 5 - 4}] / set yn [expr {600 + $tn * 5 - 3}] 默认变量号公式 这就是"每刀 5 格"的槽位公式:5N-4 给 X、5N-3 给 Y(T10 → #646/#647)。和 #(600+5N) 的寿命计数、5N-2/5N-1 的 H/D 调正量共用同一套分配,互不冲突
5990–5991 if { [info exists ::nc_x_var] && $::nc_x_var > 0 } { set xn $::nc_x_var }(Y 同理) 手填优先 事件里填了变量号就用填的;填 0 / 没填(::nc_x_var = 0)就走默认公式。覆盖方向是"手填 > 默认"
5993–5994 set lx $mom_sys_leader(X) / set ly $mom_sys_leader(Y) 取 X/Y 的字头 不写死 "X",而是问后处理自己:.def 里地址的 LEADER 值就是 X。这样以后有人改了字头(例如车床上加前导字符),这段代码不用改
5995 set n 0 计数器 累计"这一行总共被替换了几处",用来判断要不要把改过的文本写回
5997–6001 if { [string length $lx] > 0 } { ... } 里三行:
set pat [format {%s(-?[0-9.]+)} $lx]
set rep [format {%s[%s+#%s]} $lx "\\1" $xn]
incr n [regsub -all $pat $buf $rep buf]
把这一行里的 X 数字套上 [..+#..] 三行的分工:
① pat = 搜索模式 X(-?[0-9.]+):找字头 X,后面接可选负号 + 一串数字/小数点,并用 ( ) 捕获这段数字。-? 是为了 X-1.25 这种负值;[0-9.]+ 里的 . 是"小数点也是数字的一部分"
② rep = 替换串 X[\1+#646]:用 format 拼出来,%s 依次塞进字头、\1(regsub 的第 1 个捕获组)、变量号。为什么用 format 而不用双引号直写:双引号里 \1、\[、\] 的转义规则容易写错,format 把"格式"和"数据"分开,所见即所得
③ regsub -all ... buf ... buf = 全局替换,结果写回 buf;regsub 的返回值是"替换了几处",所以 incr n [...] 能顺带计数。一行的 X 只会有一处(模态也只会写一次),但这个写法对"意外多写"也安全
④ 数字本体被原样搬进 \1 —— 小数点位数、前导零、正负号全都是后处理算好的原样,这是本方案最大的优点(对比"自己 format 一个数字"会有格式差异)
6002–6006 Y 的同样三行($ly / $yn) 同上,处理 Y 与 X 完全对称。只处理 X、Y 两个轴 —— Z / I / J / K / R / F 一律不动(这是需求口径)
6008 if { $n > 0 } { set mom_o_buffer $buf } 只在真改过时才写回 没改过就不动 mom_o_buffer —— 形式上也是"零差异"的保证;同时避免无谓赋值

这段解决了什么问题:在不碰 .def、不碰任何事件表的前提下,
把"即将落盘的那一行"里的工件坐标改写成"基准值 + 宏变量"的形式,
并且靠三条跳过规则(注释行 / 右括号行 / G 码黑名单)做到只改该改的。

4.4 复位那两行

   if { [info exists ::nc_addvar_on] } { set ::nc_addvar_on 0 }
行 起什么作用 有什么用
if { [info exists ::nc_addvar_on] } 判存在 万一这个 proc 在别的场合被调用(例如程序里没有坐标宏相关代码),也不会报错
set ::nc_addvar_on 0 关掉开关 ::nc_addvar_on 是全局的,不复位就会污染下一道工序:下一道没加事件的工序也会被套上 [..+#..]。所以"开"和"关"必须成对 —— 开在事件函数里,关在工序结束

4.5 关于"手填变量号"(可选功能)

代码里已经支持 ::nc_x_var / ::nc_y_var,但要让 NX 界面上出现输入格,
需要在两个 .cdl 里各加两个 PARAM(两个文件必须一致):

(a) 后处理自己的 mill_intergrex_300_2022.cdl,EVENT add_variable(第 3–15 行)整段换成:

EVENT add_variable
{
   POST_EVENT "add_variable"
   UI_LABEL "add_variable"
   CATEGORY MILL DRILL
   PARAM command_status
   {
      TYPE o
      DEFVAL "Active"
      OPTIONS "Active","Inactive"
      UI_LABEL "Add # Variable"
   }
   PARAM x_var
   {
      TYPE i
      DEFVAL "0"
      UI_LABEL "X Variable No"
   }
   PARAM y_var
   {
      TYPE i
      DEFVAL "0"
      UI_LABEL "Y Variable No"
   }
}

(b) NX 全局 ...\Designcenter2606\MACH\resource\user_def_event\ude.cdl(第 53–65 行那一段,
在 PARAM command_status 的 } 之后、最外层 } 之前)插入同样的两个 PARAM。

改完重启 NX(UDE 库是启动时读的)。已经加进工序的旧事件不会自动长出输入格,
要在工序里删掉重新加一次。

用法:两个格都填 0 或不填 → 走默认公式;填 656 / 657 → 就用 #656 / #657。
未做这一步时,功能照常工作(走默认公式),只是界面上看不见那两个格。


第 5 章 · 这个实现的逻辑是什么

5.1 状态机:只有三个状态

        ┌──────────────────────────────┐
        │  其它工序:::nc_addvar_on = 0 │  ← 什么也不做(输出逐字不变)
        └──────────────────────────────┘
   工序开始事件(UDE add_variable = Active)
        ↓  PB_CMD_add_variable_flag
        ┌──────────────────────────────┐
        │  本工序:::nc_addvar_on = 1   │  ← 每个 NC 块的 X/Y 被改写
        └──────────────────────────────┘
   工序结束({End of Path} → PB_CMD_collect_cutting_time 末尾)
        ↓  set ::nc_addvar_on 0
        └──────── 回到状态 0 ──────────┘

为什么用"全局变量 + 两道保险":
Tcl 里 proc 之间不共享局部变量,而"事件"和"每一行输出"分别在两个 proc 里,
只能用全局变量(:: 前缀)传状态;工序结束再复位,形成闭环。

5.2 判断链:为什么是"黑名单"而不是"白名单"

改写发生在文本层,看到的只是"一行字符串",所以只能靠行内容特征判断该不该改:

要不要改这一行?
  开关没开?                → 不改(return)
  行是空的?                → 不改
  含 "(" 或 ")"(注释/消息)?→ 不改
  含 G53/G28/G30/G52/G92/G10/G4/G04/G68? → 不改
  否则                      → 改(把 X/Y 套上 [..+#..])

用黑名单的理由:

5.3 改写算法:为什么用 regsub 而不是拆字符串

一行 NC 里,"X" 可能出现多次且含义不同(G53 G0 X0. 里的 X 是机床坐标),
用 split 按空格拆再拼回去,会破坏原有的空格/前导零格式。
regsub 是原地正则替换:

而"哪些行不处理"的问题,用前置的 G 码黑名单解决,
不在正则里堆条件 —— 这样规则一目了然,也好维护。

5.4 变量号算法:两级(默认公式 + 手填覆盖)

变量号 = 默认公式(按刀号定位到"每刀 5 格"里的那一格)
         ↓ 若事件里手填了 (>0)
         手填值

5.5 为什么必须"只改文本、不调输出命令"

PB_CMD_before_output 是被 MOM_before_output 调的 —— 也就是正处在
"输出一个 NC 块"的过程当中。如果在这里再调 MOM_output_literal 之类,
就会递归地再触发一次输出流程,轻则多出行、重则死循环。
所以这一段只做两件事:读 mom_o_buffer、写 mom_o_buffer。
(这也是为什么调试信息放在 A 段之外的事件函数里打印,而不是在这里打。)

5.6 与"需求②(循环 R 值)"共存

同一个 PB_CMD_before_output 里,A 段负责循环 R 值跟踪(需求②),
B 段负责坐标宏(需求③)。两段的先后是确定的:

  1. A 段先跑:它读 mom_o_buffer 里原始的 Z 值来跟踪刀位
    (如果 B 段先改写了 X/Y,虽然不影响 Z,但保持"先跟踪、后改写"的顺序更清晰);
  2. B 段后跑:改写 X/Y。

A、B 两段互不引用对方的变量(A 用 ::nc_cycR*,B 用 ::nc_addvar_on),所以顺序其实不敏感,
但不要重新排列它们,也不要往 B 段里塞 A 段的逻辑。

5.7 ⚠ 已知缺陷:复位语句可能被跳过(建议修)

PB_CMD_collect_cutting_time 的结构是:

proc PB_CMD_collect_cutting_time { } {
   ...
   if { [info exists ::nc_new_tool_request] } { set ::nc_new_tool_request 0 }   ;# ← 6345
   if { ![info exists mom_tool_number] } { return }                              ;# ← 6347 ★ 提前返回
   ... 累加切削时间 / 记刀号 / 记文本 ...
   if { [info exists ::nc_addvar_on] } { set ::nc_addvar_on 0 }                   ;# ← 6384 复位
}

问题:一旦"本道工序没有 mom_tool_number",第 6347 行就 return,
第 6384 行的复位永远不会执行 → 开关残留 → 下一道没加事件的工序也会被套上方括号。

风险高不高:实际出程序时 End of Path 几乎总是有刀号(工序都用刀),所以平时看不出来;
但这是一个"迟早会咬人"的隐患。

建议改法(把复位提到 return 之前):

   # 【需求③】工序结束先关坐标宏开关(必须放在下面那句 return 之前!)
   if { [info exists ::nc_addvar_on] } { set ::nc_addvar_on 0 }

   # 需求①保险:本道工序结束,把"本工序的 set_new_tool 标记"清掉
   if { [info exists ::nc_new_tool_request] } { set ::nc_new_tool_request 0 }

   if { ![info exists mom_tool_number] } { return }

需要我改的话说一声 —— 改完要重新走一遍"降 ni → 加密 → Deploy-TBC"的流程。

5.8 其它已知边界

边界 说明 处理
G65 P.. X.. Y.. 宏调用传参也会被改写 用到就在第 3.3 节跳过规则里补 |65
增量输出 G91 G91 G1 X.. Y.. 会被改写;增量模式下加宏偏移的物理含义与绝对模式不同 目前后处理按绝对输出用;若某道工序会出 G91,在跳过规则里补 |91(注意 G91 G30 现已被 G30 规则拦下)
G43/G44(刀长补偿)行 不在黑名单里,如果这一行同时带 X/Y 会被改写 本后处理里 G43 行只带 Z/H,实测无坐标;若以后出现带 X/Y 的补偿行,补规则
只有 X 没有 Y 的行 正常处理(实测 G0 X0. → G0 X[0.+#646]) 无需处理
0 值坐标 正常处理:Y0. → Y[0.+#647](数字原样保留) 无需处理
加密后的 .tcl 字节码,行号和文本都不可读 只能改明文母本再加密(修订13)

第 6 章 · 真 Tcl 8.6 实测数据

方法:用 NX 自带的真解释器 ...\Designcenter2606\NXBIN\python\tcl86t.dll(ctypes 调 Tcl_Eval),
把桌面 mill_intergrex_300_2022_bf.tcl 里的 PB_CMD_add_variable_flag 和
PB_CMD_before_output 原文按大括号配对抽出来,摆好 mom_o_buffer / mom_sys_leader /
mom_tool_number 后逐条跑。

复现命令:

python Test-CoordMacro.py "C:\Users\Administrator\Desktop\mill_intergrex_300_2022_bf.tcl"

(脚本:桌面 Test-CoordMacro.py,也有一份在工作区 tclenc_verify\coordmacro_test.py)

6.1 事件状态 → 开关(7 种取值)

事件状态 mom_command_status ::nc_addvar_on ::nc_x_var ::nc_y_var 说明
ACTIVE 1 0 0 NX 真实送进来的值(全大写)
Active 1 0 0 大小写变体也认
Inactive 0 0 0 不开
Off 0 0 0 不开
ACTIVE + 填 656/657 1 656 657 手填生效
ACTIVE + 填 0/0 1 0 0 填 0 = 走默认公式
变量根本不存在 0 0 0 没加事件

6.2 逐行改写对照(20 条,全部 OK)

输入行 刀号 开关 输出行 结果
G0 X1.2 Y3.4 10 开 G0 X[1.2+#646] Y[3.4+#647] ✅
N100 G1 X-1.25 Y0. Z-5. F200. 10 开 N100 G1 X[-1.25+#646] Y[0.+#647] Z-5. F200. ✅ 负值/零/其它字不动
G81 X10. Y20. Z-3. R2. F100. 10 开 G81 X[10.+#646] Y[20.+#647] Z-3. R2. F100. ✅ 循环也套
G2 X10. Y20. I5. J0. 7 开 G2 X[10.+#631] Y[20.+#632] I5. J0. ✅ I/J 不动
G0 X1.2 Y3.4 104 开 G0 X[1.2+#601] Y[3.4+#602] ✅ 按 T1 还原
G0 X1.2 Y3.4 10 开 G0 X[1.2+#656] Y[3.4+#657] ✅ 手填 656/657
G0 X0. 10 开 G0 X[0.+#646] ✅ 只有 X 的行
G0 X1.2 Y3.4 10 关 G0 X1.2 Y3.4 ✅ 逐字不变
G53 G0 X0. Y0. 10 开 G53 G0 X0. Y0. ✅ 跳过
G28 G91 X0 10 开 G28 G91 X0 ✅ 跳过
G91 G30 X0 10 开 G91 G30 X0 ✅ 跳过
G52 X10. Y10. 10 开 G52 X10. Y10. ✅ 跳过
G68.2 X1. Y2. I0. J0. K0. 10 开 G68.2 X1. Y2. I0. J0. K0. ✅ 跳过
G4 X1.0 10 开 G4 X1.0 ✅ 跳过(X 是时间)
(T10 X Y) 10 开 (T10 X Y) ✅ 跳过(注释)
T01.00T02.00M06 10 开 T01.00T02.00M06 ✅ 换刀行不动
#650=#650+1 10 开 #650=#650+1 ✅ 宏变量行不动
IF [#650 GT 37] GOTO 101 10 开 IF [#650 GT 37] GOTO 101 ✅ 不动
X1.2 Y3.4 Z5. 10 开 X[1.2+#646] Y[3.4+#647] Z5. ✅ 裸坐标行(模态省略 G)
G65 P9001 X1. Y2. 10 开 G65 P9001 X[1.+#646] Y[2.+#647] ⚠ 已知边界(会被改)

异常项:0。

6.3 复位

::nc_addvar_on 从 1 → 执行 if { [info exists ::nc_addvar_on] } { set ::nc_addvar_on 0 } → 0 ✅


第 7 章 · 维护、排错、跨机纪律

7.1 三条纪律(老规矩)

  1. 明文母本必须留着(_bf.tcl / .bak84)。加密版 .tcl 绝不能再让 Post Builder 打开并保存
    —— PB 会按 .pui 重写 .tcl,加密当场作废。
  2. 流程固定:PB 里改完 →(如需)降 ni → 加密 → 双击 Deploy-TBC.cmd → 出程序核对 NC。
  3. 跨机四件套一致:.tcl / .def / .cdl / .pui,以及 NX 全局 ude.cdl
    (.cdl 决定"事件有哪些参数",.pui 决定"事件挂给哪个 proc")。

7.2 PB 保存后被重写怎么办

在 Post Builder 里改过东西并保存了 → .tcl 里这 4 段会被 PB 重写掉 →
直接再跑一次 Apply-CoordMacro.ps1(桌面,幂等,自动备份)。

powershell -ExecutionPolicy Bypass -File "%USERPROFILE%\Desktop\Apply-CoordMacro.ps1"

跑完看同目录 Apply-CoordMacro.log,最后一行 === DONE === 即成功。

7.3 现场排错表

现象 原因 / 处理
该出方括号的工序,一个都没有 ① 事件没挂上 → 查 .pui 第 777 行登记;② 状态不是 Active → 打开 ::nc_dbg 看 (DBGADD on=..);③ .cdl 里没有 add_variable 事件定义;④ 没重启 NX 就加事件
方括号有,但号不对(例如出现 +#596) 刀号没取到 → mom_tool_number 在该块不存在,走了 tn=0 的兜底。把那段 NC 发出来核对;或直接在该工序事件里手填变量号
本该没有方括号的工序出现了方括号 复位没执行 → 见第 5.7 节(return 提前返回的缺陷);或 PB 保存后代码被重写,需重灌
某行 G28/G53 被套了方括号 跳过规则被改动 → 核对第 3.3 节那三行是否与原版一致(尤其 \M 不能丢)
坐标整字消失 说明改写发生在"块定义层"而不是"文本层" → 请确认没有把 _EXPR.def 混进来用
改了 .cdl 后事件框里还是没有变量号格 ① 只改了一个 .cdl(后处理 + NX 全局两个都要改,且要一致);② 没重启 NX;③ 旧事件要删掉重加

7.4 验收判据(每次改完必查)

① 加事件的工序:G0/G1/G2/G3 + G8x 的 X/Y 都带 [..+#..],Z/I/J/K/R/F 原样
② 没加事件的工序:与改动前【逐字一致】(这一条是硬指标)
③ G53/G28/G30/G52/G92/G10/G4/G68 行:一个方括号都没有
④ .def 文件的字节数不变(本方案不碰 .def)
⑤ 变量号与刀号对得上:T10 → #646/#647;T7 → #631/#632;T1 → #601/#602

第 8 章 · 附录:术语与变量速查

8.1 用到的全局变量

变量 谁设 谁读 含义
::nc_addvar_on PB_CMD_add_variable_flag / PB_CMD_collect_cutting_time PB_CMD_before_output B 段 本工序的坐标宏开关(0/1)
::nc_x_var 同上(来自事件手填) B 段 手填的 X 变量号;0 = 用默认公式
::nc_y_var 同上 B 段 手填的 Y 变量号;0 = 用默认公式
::nc_dbg .tcl 第 6880 行(手工开关) 各处调试分支 1 = 在 NC 里打印诊断行

8.2 NX 送进来的变量

变量 含义 注意
mom_command_status UDE 的状态 NX 送进来是全大写 ACTIVE
mom_x_var / mom_y_var UDE 里手填的变量号 需要在两个 .cdl 里加 PARAM 才会有
mom_tool_number 当前刀号 >100 = 刀号*100+补偿号,要还原
mom_o_buffer 即将落盘的那一行 NC 文本 只读/改写,别在这里调 MOM_ 输出命令
mom_sys_leader(X/Y) X/Y 的字头(值就是 X/Y) 别写死
mom_sys_control_out 注释起始符(本后处理 = () 用于识别注释行

8.3 关键常量与公式

项 值
默认变量号 X = 600 + 刀号*5 - 4;Y = 600 + 刀号*5 - 3
每刀 5 格 5N-4 X偏移 / 5N-3 Y偏移 / 5N-2 H调正 / 5N-1 D调正 / 5N 寿命计数
跳过名单(G 码) 53 / 28 / 30 / 52 / 92 / 10 / 04 / 4 / 68
输出形态 X[基准值+#变量号](FANUC 宏表达式;[ ] 是表达式括号)

8.4 相关文件

文件 说明
mill_intergrex_300_2022_bf.tcl 明文母本(本文行号基准,559,160 字节)
mill_intergrex_300_2022.tcl 加密后的字节码(NX 实际加载的就是它)
Apply-CoordMacro.ps1 本需求的 .tcl 重灌脚本(PB 保存后跑一次)
Test-CoordMacro.py 本需求的真 Tcl 8.6 离线测试脚本(第 6 章数据就是它跑的)
后处理改造_修订4_坐标宏表达式.md 旧方案(.def 双地址版)实现与取舍,保留参考
后处理改造_修订6_坐标宏_缓冲改写版.md 本方案(缓冲行改写)的完整说明
后处理改造_修订12_循环R值_防忘总纲.md 需求②(同一个 proc 的 A 段)
后处理改造_修订13_Tcl官方加密后处理_方案与落地.md 加密与部署(Deploy-TBC.cmd)

本文基于 2026-10-16 的桌面文件实测撰写。第 3 章代码与 mill_intergrex_300_2022_bf.tcl 逐字一致;
第 6 章数据由真 Tcl 8.6.15(NX 自带 tcl86t.dll)跑出。