从那智机器人备份中解析下挂设备的IP
Nachi OP10R1 备份程序 IP 信息解析结论
结论
可以从本次提供的 Nachi 机器人备份中找到 IP图例.pic 里的主要网络信息:
| 图片信息 | 是否能在备份中找到 | 备份中的证据 |
|---|---|---|
机器人名称 OP10R1 | 可以 | WORK/F00FIELD.CON 第 72、103 行为 op10r1;PLCEngine/config1.nxd offset 0x2eb8 也有 op10r1 |
机器人本体 IP 190.201.2.1 | 可以 | PLCEngine/nwid1.nxd offset 0x000000de,4 字节 01 02 c9 be,按小端 IPv4 解码为 190.201.2.1 |
子网掩码 255.255.0.0 | 可以 | PLCEngine/nwid1.nxd offset 0x000000e2,4 字节 00 00 ff ff,按小端 IPv4 解码为 255.255.0.0;WORK/F00FIELD.CON 第 64 行 4294901760 也等于 255.255.0.0 |
从站 Timer1 的 IP 190.201.2.5 | 可以 | PLCEngine/config1.nxd 中 station name 为 op10r1timer1,名称 offset 0x0000197e,IP offset 0x00001a76 |
从站 RIP1 的 IP 190.201.2.6 | 可以 | PLCEngine/config1.nxd 中 station name 为 op10r1rip1,名称 offset 0x00001ae6,IP offset 0x00001bde |
安全码 201 | 不能作为独立字段可靠提取 | 备份中没有找到带有明确语义的 安全码=201、Safety Code=201、F-Address=201 等字段。201 大量出现,且 190.201.2.x 的第二段本身就是 201,不能把裸值 201 当作安全码证据。若你们现场规则是 安全码 = 200 + 机器人 IP 最后一段,则可由 190.201.2.1 推导为 201,但这属于业务规则推导,不是备份字段直接提取。 |
输入文件定位
用户消息中写的是相对路径 测试/OP10R1 和 测试/IP图例.pic。当前 Codex 工作目录下没有这些文件,实际在本机找到:
- 备份目录:
/Users/liuke/PycharmProjects/PythonProject/测试/OP10R1 - 图片:
/Users/liuke/PycharmProjects/PythonProject/测试/IP图例.pic
图片内容确认:OP10R1 本体 IP 为 190.201.2.1,子网掩码 255.255.0.0,安全码 201;从站为 Timer1=190.201.2.5、RIP1=190.201.2.6。
关键文件和位置
1. 机器人本体 IP:PLCEngine/nwid1.nxd
文件:/Users/liuke/PycharmProjects/PythonProject/测试/OP10R1/PLCEngine/nwid1.nxd
这个文件是二进制 .NXD 文件,不是普通 INI/XML。它包含可读字段名:
ipIpAddressipNetMaskipGatewayIp
实际值在文件前部的二进制数据区:
| 字段 | offset | 原始字节 | 解码方式 | 值 |
|---|---|---|---|---|
| robot IP | 0x000000de / 222 | 01 02 c9 be | 4 字节小端 IPv4 | 190.201.2.1 |
| netmask | 0x000000e2 / 226 | 00 00 ff ff | 4 字节小端 IPv4 | 255.255.0.0 |
| gateway | 0x000000e6 / 230 | 00 00 00 00 | 4 字节小端 IPv4 | 0.0.0.0 |
解码规则说明:小端 IPv4 的字节顺序与常见网络序相反。01 02 c9 be 作为小端 32 位整数还原后是十六进制 be c9 02 01,即十进制 190.201.2.1。
2. 机器人本体的文本佐证:WORK/F00FIELD.CON
文件:/Users/liuke/PycharmProjects/PythonProject/测试/OP10R1/WORK/F00FIELD.CON
该文件是可读文本,包含 Fieldbus 通道配置:
64:CHANNEL_2_SUBNET_MASK=4294901760
65:CHANNEL_2_DEF_GATEWAY=3200844289
72:CHANNEL_2_STATION_NAME=op10r1
其中:
4294901760按大端 IPv4 整数解释为255.255.0.03200844289按大端 IPv4 整数解释为190.201.2.1CHANNEL_2_STATION_NAME=op10r1与机器人名OP10R1对应
注意:第 65 行字段名是 DEF_GATEWAY,所以我不把它作为本体 IP 的唯一证据;它更适合作为 nwid1.nxd 的交叉验证。
3. 下挂设备名字和 IP:PLCEngine/config1.nxd
文件:/Users/liuke/PycharmProjects/PythonProject/测试/OP10R1/PLCEngine/config1.nxd
这个文件同样是二进制 .NXD 文件。它包含 PROFINET/PNIOC 相关可读字段名:
PNIOC_IODabNameOfStationipIPAddressipNetworkMaskipGatewayAddr
它还包含两个从站 station name:
| 图片中的设备名 | 备份中的 station name | name offset | IP offset | IP 原始字节 | 解码 IP | mask offset | mask |
|---|---|---|---|---|---|---|---|
Timer1 | op10r1timer1 | 0x0000197e / 6526 | 0x00001a76 / 6774 | 05 02 c9 be | 190.201.2.5 | 0x00001a7a | 255.255.0.0 |
RIP1 | op10r1rip1 | 0x00001ae6 / 6886 | 0x00001bde / 7134 | 06 02 c9 be | 190.201.2.6 | 0x00001be2 | 255.255.0.0 |
这里的 Timer1 和 RIP1 不是以图片里的大小写形式直接存储的。备份里存的是 station name:小写、带机器人名前缀,即 op10r1timer1 和 op10r1rip1。因此程序解析时建议保留完整 station name,同时可按前缀 op10r1 去掉后得到业务显示名 timer1、rip1。
查找方案
步骤 1:先做文本精确搜索
在备份目录执行:
rg -n -a "190\.201\.2\.(1|5|6)|255\.255\.0\.0|Timer1|RIP1|OP10R1" /Users/liuke/PycharmProjects/PythonProject/测试/OP10R1
本次结果:没有找到 190.201.2.x、255.255.0.0、Timer1、RIP1 这些图片中的原样文本。说明不能只靠普通文本搜索。
步骤 2:查看候选二进制文件的可读字符串
strings -a -t d /Users/liuke/PycharmProjects/PythonProject/测试/OP10R1/PLCEngine/nwid1.nxd
strings -a -t d /Users/liuke/PycharmProjects/PythonProject/测试/OP10R1/PLCEngine/config1.nxd
关键发现:
nwid1.nxd有ipIpAddress、ipNetMask、ipGatewayIpconfig1.nxd有op10r1timer1、op10r1rip1、ipIPAddress、ipNetworkMask
步骤 3:按小端 IPv4 搜索已知 IP
图片中的 IP 需要转成小端字节后搜索:
| IP | 小端字节 |
|---|---|
190.201.2.1 | 01 02 c9 be |
190.201.2.5 | 05 02 c9 be |
190.201.2.6 | 06 02 c9 be |
255.255.0.0 | 00 00 ff ff |
验证脚本:
from pathlib import Path
from ipaddress import IPv4Address
base = Path("/Users/liuke/PycharmProjects/PythonProject/测试/OP10R1")
patterns = {
"le_190.201.2.1": bytes([1, 2, 201, 190]),
"le_190.201.2.5": bytes([5, 2, 201, 190]),
"le_190.201.2.6": bytes([6, 2, 201, 190]),
}
for path in sorted(base.rglob("*")):
if not path.is_file():
continue
data = path.read_bytes()
for label, pattern in patterns.items():
start = 0
while True:
offset = data.find(pattern, start)
if offset == -1:
break
print(path.relative_to(base), label, hex(offset), offset)
start = offset + 1
本次关键输出:
PLCEngine/nwid1.nxd le_190.201.2.1 0x000000de 222
PLCEngine/config1.nxd le_190.201.2.5 0x00001a76 6774
PLCEngine/config1.nxd le_190.201.2.6 0x00001bde 7134
步骤 4:从 station name 定位从站 IP
对 config1.nxd,本次两个从站记录的规律是:
- 找到 station name 字符串 offset
- IP offset = station name offset +
0xf8 - IP 后 4 字节是 mask
- mask 后 4 字节是 gateway
验证脚本:
from pathlib import Path
from ipaddress import IPv4Address
base = Path("/Users/liuke/PycharmProjects/PythonProject/测试/OP10R1")
data = (base / "PLCEngine/config1.nxd").read_bytes()
def ip_le(offset: int) -> str:
return str(IPv4Address(int.from_bytes(data[offset:offset + 4], "little")))
for station in [b"op10r1timer1", b"op10r1rip1"]:
name_offset = data.find(station)
ip_offset = name_offset + 0xF8
mask_offset = ip_offset + 4
print(station.decode(), hex(name_offset), ip_le(ip_offset), ip_le(mask_offset))
本次输出:
op10r1timer1 0x197e 190.201.2.5 255.255.0.0
op10r1rip1 0x1ae6 190.201.2.6 255.255.0.0
自动化解析可行性
可以用 Java 或 Python 自动化解析。建议分两层实现:
文本配置层:解析
WORK/F00FIELD.CON- 读取
CHANNEL_n_STATION_NAME - 读取
CHANNEL_n_SUBNET_MASK、CHANNEL_n_DEF_GATEWAY - 将十进制无符号 32 位整数按大端 IPv4 转成点分十进制
- 这层适合做通道和 station name 的佐证
- 读取
二进制配置层:解析
PLCEngine/nwid1.nxd与PLCEngine/config1.nxd- 对
nwid1.nxd,扫描本体网络记录头17 00 00 00 78 00 00 00,再按记录内相对位置读取 IP、mask、gateway。 - 对
config1.nxd,扫描下挂设备记录头17 00 00 00 50 01 00 00,再按同一条记录内的 station name 和 IP 字段建立对应关系。 - 所有 NXD 内的 IP 字段按小端 4 字节 IPv4 解码。
- 对
Python 解码函数:
from ipaddress import IPv4Address
def decode_ipv4_le(data: bytes, offset: int) -> str:
value = int.from_bytes(data[offset:offset + 4], "little")
return str(IPv4Address(value))
def decode_ipv4_be_int(value: int) -> str:
return str(IPv4Address(value))
Java 解码思路:
static String ipv4LittleEndian(byte[] data, int offset) {
int b0 = Byte.toUnsignedInt(data[offset]);
int b1 = Byte.toUnsignedInt(data[offset + 1]);
int b2 = Byte.toUnsignedInt(data[offset + 2]);
int b3 = Byte.toUnsignedInt(data[offset + 3]);
return b3 + "." + b2 + "." + b1 + "." + b0;
}
static String ipv4FromUnsignedDecimal(long value) {
return ((value >>> 24) & 0xff) + "."
+ ((value >>> 16) & 0xff) + "."
+ ((value >>> 8) & 0xff) + "."
+ (value & 0xff);
}
实际产品化时不要只硬编码绝对 offset。更稳妥的策略是:
F00FIELD.CON只作为 channel 元数据和人工复核依据,不用于伪造缺失的本体 IP 或下挂设备 IP。nwid1.nxd必须存在且能扫描到有效本体网络记录,才输出robot。config1.nxd必须存在且能扫描到有效下挂设备记录,才输出downstream_devices。- 如果缺少关键二进制文件,程序输出
status=warning和warnings报警;robot=null,downstream_devices=[]。 - 对未知版本备份,应先看是否还能匹配记录头;如果记录头也变化,需要重新分析该版本的 NXD 结构,不能直接套用当前 offset。
安全码 201 的论证
我没有把备份中的任意 201 视为安全码,原因如下:
- 文本搜索
SAFE、SAFETY、F_DEST、F_SOURCE、CODE、ADDR等关键字,只找到与安全 CRC 长度、机器人 fail-safe、日志错误码等无关配置,没有明确安全码=201字段。 - 二进制搜索
201的 16 位、32 位、小端、大端形式命中很多文件,数量很大,无法从上下文证明它们代表图片中的安全码。 - IP
190.201.2.1本身包含十进制字节201,因此裸201具有高误报风险。 - 图片里
OP10R1=201、OP10R2=221、OP20R1=241、OP20R2=261看起来像200 + IP 最后一段的业务编码规则;但该规则没有在备份文件中以字段形式出现。
因此:本备份可以提取 IP、mask、station name;安全码只能在你确认业务规则后由 IP 推导,不能宣称是从备份中直接解析得到。
2026-05-19 补充:更可靠的通用解析方案
前面针对 OP10R1 的分析曾提到若已知 station name,则可以用 name offset + 0xf8 找到下挂设备 IP。这个规律在 OP10R1 上成立,但它不应该作为最终程序的唯一依据。更可靠的做法是:先找到二进制记录的记录头,再在同一条记录内部读取名字、IP、mask 和 gateway。这样不会依赖某一个备份文件里的绝对 offset,也不依赖设备名是否叫 timer、rip、grip。
当前代码实现位于:
/Users/liuke/Documents/Codex/2026-05-19/goal-superpowers-using-superpowers-users-liuke/tools/nachi_nxd_ip_parser.py
1. 为什么不建议硬编码绝对 offset
跨 5 个正常样本对比后可以确认:
| 信息 | OP10R1 | APR15R1 | CDPL15R1 | CDPL35R1 | FCP15-R1 |
|---|---|---|---|---|---|
| 本体 IP 记录起点 | 0x0c8 | 0x0c8 | 0x0c8 | 0x0c8 | 0x0c8 |
| 第 1 个下挂设备记录起点 | 0x1968 | 0x32b4 | 0x313c | 0x313c | 0x1d14 |
| 下挂设备数量 | 2 | 3 | 3 | 3 | 2 |
本体 IP 在这几个样本中的绝对 offset 都是 0x0de,但这只是当前样本集的现象。下挂设备的绝对 offset 已经明显不同,所以程序不应该写死“第一个设备在某个固定位置”。正确方法是扫描记录头,并按记录内部的相对位置读取。
2. 机器人本体 IP 记录映射
文件:PLCEngine/nwid1.nxd
代码常量对应关系:
| 含义 | 代码常量 | 值 |
|---|---|---|
| 本体网络记录头 | NWID_RECORD_HEADER | 17 00 00 00 78 00 00 00 |
| IP 相对记录起点 | NWID_IP_REL | 0x16 |
| mask 相对记录起点 | NWID_MASK_REL | 0x1a |
| gateway 相对记录起点 | NWID_GATEWAY_REL | 0x1e |
解析逻辑:
- 在
nwid1.nxd中扫描所有17 00 00 00 78 00 00 00。 - 对每个命中的
record_start,读取:ip_offset = record_start + 0x16mask_offset = record_start + 0x1agateway_offset = record_start + 0x1e
- 每个字段均按 4 字节小端 IPv4 解码。
- 只有当 IP 不是
0.0.0.0、不是广播/组播地址,且 mask 是合法连续掩码时,才输出为高置信度结果。
以 OP10R1 为例:
| 字段 | 位置 | 值 |
|---|---|---|
record_start | 0x000000c8 | 记录头 17 00 00 00 78 00 00 00 |
ip_offset | 0x000000de | 190.201.2.1 |
mask_offset | 0x000000e2 | 255.255.0.0 |
gateway_offset | 0x000000e6 | 0.0.0.0 |
3. 下挂设备记录映射
文件:PLCEngine/config1.nxd
代码常量对应关系:
| 含义 | 代码常量 | 值 |
|---|---|---|
| 下挂设备记录头 | CONFIG_DEVICE_RECORD_HEADER | 17 00 00 00 50 01 00 00 |
| station name 长度相对记录起点 | DEVICE_NAME_LENGTH_REL | 0x14 |
| station name 内容相对记录起点 | DEVICE_NAME_REL | 0x16 |
| IP 相对记录起点 | DEVICE_IP_REL | 0x10e |
| mask 相对记录起点 | DEVICE_MASK_REL | 0x112 |
| gateway 相对记录起点 | DEVICE_GATEWAY_REL | 0x116 |
解析逻辑:
- 在
config1.nxd中扫描所有17 00 00 00 50 01 00 00。 - 每命中一次,就认为找到了一个候选下挂设备记录。
- 读取
record_start + 0x14的 2 字节小端整数作为 station name 长度。 - 读取
record_start + 0x16开始的 station name 字节,按 ASCII 解码。 - 读取
record_start + 0x10e的 IP、+0x112的 mask、+0x116的 gateway,均按 4 字节小端 IPv4 解码。 - 只有 station name 可解码、IP 合理、mask 合法时,才输出该设备。
这解释了旧结论里的 name offset + 0xf8:因为 name_offset = record_start + 0x16,ip_offset = record_start + 0x10e,所以 ip_offset - name_offset = 0xf8。程序中保留的是更基础的记录映射,不是只依赖 +0xf8 这个局部现象。
4. 多个或任意名称下挂设备的处理
下挂设备数量不是由程序预设的。程序扫描到多少条有效设备记录,就输出多少条。因此以下情况都可以处理:
- 只有 1 个下挂设备。
- 有 2 个或更多下挂设备。
- 名字是
rip1、rip2、rip3。 - 名字是
timer1、timer2。 - 名字是
grip1、grip3。 - 名字是其他站名,例如本次样本里的
weldsaver。
程序不会按 rip/timer/grip 关键词筛选设备。设备名来自记录内部 station name 字段,IP 来自同一条记录内部的 IP 字段,所以名字和 IP 的对应关系是文件结构给出的,不是靠字符串规则推断的。
5. 目前样本的验证结果
| 备份目录 | 本体 IP | 下挂设备 |
|---|---|---|
OP10R1 | 190.201.2.1 | op10r1timer1=190.201.2.5; op10r1rip1=190.201.2.6 |
APR15R1 | 190.64.6.89 | apr15r1timer1=190.64.6.80; apr15r1rip=190.64.6.81; apr15r1grip1=190.64.6.85 |
CDPL15R1_20260501_9 | 190.102.6.39 | cdpl015l1timer1=190.102.6.30; cdpl015l1grip1=190.102.6.35; cdpl015l1rip1=190.102.6.31 |
CDPL35R1_20260501_9 | 190.102.6.99 | cdpl035l1timer1=190.102.6.90; cdpl035l1grip1=190.102.6.95; cdpl035l1rip1=190.102.6.91 |
FCP15-R1 | 190.65.6.5 | weldsaver=190.65.6.6; fcp15l1grip=190.65.6.57 |
CDPL25R1_20260501_9 | 不输出 | PLCEngine 是 0 字节文件,缺少 nwid1.nxd 和 config1.nxd,程序只输出报警,不伪造结果 |
6. Python 程序用法
命令格式:
python3 /Users/liuke/Documents/Codex/2026-05-19/goal-superpowers-using-superpowers-users-liuke/tools/nachi_nxd_ip_parser.py \
/Users/liuke/PycharmProjects/PythonProject/测试/OP10R1 \
--output /Users/liuke/Documents/Codex/2026-05-19/goal-superpowers-using-superpowers-users-liuke/output/OP10R1_nachi_ip.json
输出 JSON 的主要结构:
{
"backup_folder": "/path/to/OP10R1",
"status": "ok",
"robot": {
"ip": "190.201.2.1",
"mask": "255.255.0.0",
"gateway": "0.0.0.0",
"source_file": "PLCEngine/nwid1.nxd",
"record_start": "0x000000c8",
"ip_offset": "0x000000de",
"mask_offset": "0x000000e2",
"gateway_offset": "0x000000e6",
"confidence": "high"
},
"downstream_devices": [
{
"station_name": "op10r1timer1",
"ip": "190.201.2.5",
"mask": "255.255.0.0",
"gateway": "0.0.0.0",
"source_file": "PLCEngine/config1.nxd",
"record_start": "0x00001968",
"name_offset": "0x0000197e",
"ip_offset": "0x00001a76",
"confidence": "high"
}
],
"fieldbus_channels": [],
"warnings": []
}
fieldbus_channels 来自 WORK/F00FIELD.CON,只作为辅助信息输出。缺少关键二进制文件时,程序不会用 F00FIELD.CON 推断本体 IP 或下挂设备 IP。
7. 缺少关键文件时的输出原则
对于 CDPL25R1_20260501_9 这类目录:
- 如果缺少
PLCEngine/nwid1.nxd,则robot输出null,并在warnings中写入报警。 - 如果缺少
PLCEngine/config1.nxd,则downstream_devices输出空数组,并在warnings中写入报警。 - 只要存在报警,顶层
status输出warning;无报警时输出ok。 - 即使
WORK/F00FIELD.CON中存在站名或 gateway 候选,也不会把它伪造成高置信度的本体 IP 或下挂设备 IP。
参考资料
- Nachi FD/CFD 控制器资料说明控制器支持多种 fieldbus/网络选项:NACHI FD/CFD Controller PDF
- Nachi FD Controller 操作手册中描述备份/保存控制器数据的入口:Nachi FD Controller Instructions on ManualsLib
- PROFINET 设备寻址通常涉及 station/device name、IP、subnet、gateway 等参数:PROFINET Addressing Parameters
最终判断
对本次 OP10R1 备份:
- 可以可靠获取机器人本体 IP:
190.201.2.1 - 可以可靠获取机器人子网掩码:
255.255.0.0 - 可以可靠获取下挂设备名和 IP:
op10r1timer1=190.201.2.5、op10r1rip1=190.201.2.6 - 不能可靠地从备份中直接获取“安全码 201”;只能在确认业务规则后从 IP 派生