首页
社区
课程
招聘
[原创]基于 .NET 4 实现从欧姆龙 Sysmac Studio工程文件解析全局符号变量表
发表于: 2026-9-24 16:53 229

[原创]基于 .NET 4 实现从欧姆龙 Sysmac Studio工程文件解析全局符号变量表

2026-9-24 16:53
229

基于 .NET 4 实现从欧姆龙 Sysmac Studio 工程文件解析全局符号变量表

摘要:欧姆龙 NJ/NX 系列 PLC 的 EtherNet/IP 通讯(CIP 符号寻址)需要提前知道每个变量的符号名、完整路径与 PLC 数据类型。本文介绍如何在不依赖任何第三方库、目标框架仅为 .NET Framework 4.0 的前提下,直接解析 Sysmac Studio 的 .smc2 工程文件,从中还原出完整的全局符号变量表——包括 ZIP 压缩包的手工解析、SLWD 变量表格式、DUT 结构体展开、变量树构建与"完整路径名 → 数据类型"映射的生成。


一、背景与需求

一个实际工程(焊机生产线,NX502-1600)的全局变量表包含 3,042 个顶层变量、44 万多个叶子变量。上位机驱动与模拟器都需要这样一张表:

  • 驱动侧:浏览变量树、按符号名读写变量;
  • 模拟器侧:按 VAR://名称 解析内存地址,模拟批量读写。

变量表有三个来源:运行时从 PLC 上传、解析 txt 导出文件、直接解析 .smc2 工程文件。前两者依赖 PLC 在线或人工导出步骤,而 .smc2 是 Sysmac Studio 的工程存档本身——离线、完整、无需额外操作,是实现"打开工程即能通讯"的最佳数据源。

但有两个现实约束:

  1. 格式未公开——.smc2 的内部结构只能靠逆向分析;
  2. 目标框架是 .NET 4.0——没有 System.IO.Compression.ZipArchive(.NET 4.5 才有),必须手工解析 ZIP。

二、.smc2 工程文件的内部结构

.smc2 本质上是一个 ZIP 压缩包,内部按工程 GUID 组织:

<项目GUID>/
  <GUID>.xml              变量表 (SLWD 格式) / 数据类型 (DUT) / PLC 配置
  <GUID>.oem              工程实体清单 (变量表/POUs/任务等)
  <GUID>.manifest         CPU 型号信息
  <GUID>.NexBuildResults  编译版本
  • 变量表以 SLWD 文本格式嵌在 xml 条目中(文件头 30 字符内出现 [SLWD 即判定);
  • 结构体(DUT)与枚举定义也是 xml(<DataType BaseType="STRUCT"> / BaseType="ENUM");
  • 哪个 GUID 是全局变量表、属于哪个 POU,由 .oem 实体清单回答。

三、第一步:手写极简 ZIP 读取器

.NET 4.0 没有 ZipArchive,好在 .smc2 只用 Deflate 压缩(方法 8),按 ZIP 规范解析三种结构即可:

[本地文件头] [数据] ... [中央目录] ... [EOCD]
internal class Smc2ZipArchive : IDisposable
{
    // 1. EOCD 签名 0x06054b50: 从文件尾部回扫定位 (末尾注释可能长达 64KB)
    //    读出入口数量 / 中央目录偏移与长度 (不支持 ZIP64)
    // 2. 中央目录签名 0x02014b50: 逐条目读 Method / 压缩前后大小 /
    //    本地头偏移 / 条目名 (UTF-8)
    // 3. 读取: 按本地头偏移定位, 跳过 30 字节固定字段 + 文件名 + 扩展区,
    //    方法 8 用 DeflateStream 解压 (RFC1951 裸 deflate 流, 与 ZIP 方法 8 一致)
}

要点:

  • 回扫 EOCD:ZIP 末尾注释最长 64KB,从尾部向前扫描签名最稳妥;
  • 循环读满:FileStream.Read 不保证一次读满,封装 ReadExactly 处理短读;
  • 全部条目名用 StringComparer.OrdinalIgnoreCase 存字典,读取时按名字索引。

整个读取器不到 150 行,零依赖。

四、SLWD 变量表格式

SLWD 文本每行一条变量记录,列以 TAB 分隔、每列为 K=V:

++D=N=HMI_Button<TAB>D=ARRAY[0..9,0..4] OF BOOL<TAB>AT=%W0.0<TAB>
    Com=&amp;comment<TAB>G=VAR_GLOBAL<TAB>NTP=PublicationOnly<TAB>R=1...

解析规则:

键 含义 键 含义
N 变量名 R 保持属性 (=1)
D 数据类型 (IEC 写法原样保留) RO 只读 (=1)
AT 硬件地址原文 Ord 排序号
Com 注释 (XML 实体编码) G 作用域 (VAR_GLOBAL/VAR/...)
IV 初始值 NTP 网络公开属性

实现上的三个细节:

  1. 键前缀剥离:K 前面的 + 是行/列标记,参与匹配前剥掉;
  2. 后键覆盖:同一行重复出现的键,后出现的生效(data[key] = value 即可);
  3. +GN= 行跳过:只是作用域标记,记录自身的 G= 值更精确。

Com= 里的 &amp;、&lt; 等需要做 XML 实体解码——注意 &amp; 必须放在 &gt;/&lt; 之后替换,避免双重解码。

五、OEM 实体清单:回答"变量表属于谁"

一个工程含全局变量表 + 各 POU(Program/FB/Function)的局部变量表,它们的归属记录在 .oem 中:

<Entity type="Program"   name="ST10" id="{guid-A}" />
<Entity type="Variables" subtype="Local" id="{guid-B}" />   <!-- 属于 ST10 -->
<Entity type="Variables" subtype="Global" />                <!-- 全局表 -->
<Entity type="DataType"  name="Recipe_Data" id="{guid-C}" />
<Entity type="NexTask"   name="PrimaryTask" id="{guid-D}" />

解析器按文档顺序扫描 <Entity .../>,维护"当前 POU"上下文:遇到 Program/Function/FunctionBlock 进入上下文,Variables/PouBody 等实体挂在当前 POU 名下。最后得到 变量表 GUID → POU 名 的映射,全局表命名 Global,局部表用 POU 名。

六、DUT 结构体与枚举

结构体成员的类型信息不在 SLWD 里,而在 DUT 定义中:

<DataType Name="Recipe_Data" BaseType="STRUCT" OffsetType="NJ">
  <DataType Name="WeldTime" BaseType="UINT" OffsetChannel="0" OffsetBit="0"/>
  <DataType Name="WeldPressure" BaseType="ARRAY[0..3] OF REAL" .../>
</DataType>
<DataType Name="State_ENUM" BaseType="ENUM"> ... </DataType>
  • 成员是自闭合的 <DataType ... /> 引用,正则平铺收集即可;
  • 跳过无名/无类型成员和内联 STRUCT 声明;
  • IEC 类型名大小写不敏感(工程里 ST_Robot_01IN 被引用为 ST_Robot_01In 很常见),DUT 字典必须用 OrdinalIgnoreCase;
  • 枚举定义可能在另一个 xml 里,与结构体分开扫描(BaseType="ENUM" 收集类型名,供模拟器把枚举按 4 字节整数读写)。

七、变量树构建:递归展开 + 数组延迟展开

每张变量表构建为顶层节点列表,BuildTypedNode 按类型名分派:

类型名 结果
STRING[n] STRING 叶子
ARRAY[a..b,c..d] OF X ARRAY 节点(存维度/上下界/元素类型元数据)
已知 DUT 名 STRUCT 节点 + 成员子树(递归)
Sysmac 库类型(如 _sAXIS_REF) 内置成员表构建的 STRUCT 子树
其它 标量叶子(类型名原样保留)

两个关键设计:

1. 数组延迟展开。一个 ARRAY[0..99,0..99] OF REAL 有 1 万个元素,全部预展开会让 44 万叶子的工程再膨胀一个数量级。所以数组节点只存边界元数据,GUI 浏览展开树节点时再按需生成 Name[x,y] 元素;例外是结构体数组——元素类型是结构体时预展开(浏览体验与性能的平衡点)。

2. 结构体子树模板缓存。同一 DUT 类型会被大量重复展开(结构体数组每个元素都重建一遍完整成员子树),首次构建后把成员节点字典缓存起来复用节点引用——树节点仅供只读浏览,共享子树是安全的。配合"类型名不含 [ 先短路、不跑正则"的检查,44 万叶子的工程导入从几十秒降到 3.7 秒。

递归保护:activeDuts 集合防止结构体递归自引用(含自身数组的 DUT)导致死循环;模板缓存"先登记空模板、再递归填充"处理自引用。

八、完整路径 → 类型映射:为通讯服务

驱动树浏览与模拟器通讯共用同一棵树。模拟器按地址读写需要的是平铺映射:

"H.PLC_Mode"              -> "INT"
"CCD[0].Out_STRING"       -> "STRING"
"H.PLCData.XXX"           -> "REAL"          (结构体成员)
"_EC_CommErrTbl[5]"       -> "BOOL"          (数组元素)
"RecipeType"              -> "ARRAY[0..9] OF Recipe_Data"   (裸数组名, 支持整数组读写)

loadVariableTypeMap 递归枚举节点子树:结构体节点递归成员(路径加 .成员名),数组节点先登记裸数组名、再按边界枚举元素路径(结构体元素在树上直接递归,标量元素直接登记)。

网络公开过滤是通讯正确性的关键:实体 PLC 上只有设置过"网络公开"的变量才能被 EtherNet/IP 访问(SLWD 的 NTP 字段非空)。同一份解析结果,传 onlyPublishedGlobals 即过滤未公开变量——实测 44 万叶子的工程,过滤后只剩 306,345 个网络公开变量(顶层 3,042 → 355)。

九、控制器信息提取

CPU 型号/固件/IP/任务周期等散落在不同文件中(.oem/.manifest/.NexBuildResults/配置 xml),用一组窄正则逐个文件累积提取:

信息 来源片段
型号 + 固件 SetUnit "NX502-1600 Ver.1.66"
IP / 掩码 / 网关 NexCpuEipTcpIp 配置块(NX5xx 双网口有 Port1/Port2 两段)
任务周期/优先级 TaskExecutionCondition / ExecutionPriority
作者/语言/时间 <SolutionModel> 元数据

一个值得记录的坑:CompileVersion 在 NX 工程里第一个匹配往往是占位的 0.0,0.0——取第一个非占位的值才是真实编译器版本。

十、小结

整套解析器 约 1,500 行、零第三方依赖、纯 .NET 4.0,拆解下来的技术点:

  1. ZIP 三种结构的手工解析(EOCD 回扫 / 中央目录 / 本地头)+ Deflate 解压;
  2. SLWD 文本格式的键值解析与 XML 实体解码顺序;
  3. OEM 实体流式扫描维护 POU 上下文;
  4. DUT 递归展开 + 模板缓存 + 数组延迟展开的组合拳;
  5. 完整路径 → 类型映射与网络公开过滤。

同一份实现同时服务两个程序集:驱动侧(变量树浏览)与模拟器侧(批量读写通讯),实际工程 44 万叶子导入 3.7 秒,验证与 Sysmac Studio 变量监视器导出的清单完全一致。

本文对应的实现代码见 OmronNjNxEipDriver 解决方案中的 OmronSmc2File.cs(驱动与模拟器两个工程保持同步的双份拷贝)。


[招生]科锐逆向工程师培训(2026年11月13日实地,远程教学同时开班, 第57期)

收藏
点赞 ・0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回