TIA Portal MCP
by YAlwaysfirst
README.md
# dsh-tia-portal
把 **西门子博图(TIA Portal)** 接到 DeepSeek Harness 上的本地插件。模型可以直接看工程结构、读块、导入导出、编译、改变量表,也能启动/打开/保存工程。
```
模型 ──tool call──► 插件(JS) ──子进程──► tia-bridge.ps1 ──Openness──► 博图
│ │
└── 请求/结果走 UTF-8 文件 ─┘
```
同一套能力也能当 **MCP server** 给 Codex / Claude Code / Cursor 用(`mcp/server.js`),
工具定义和 bridge 都是同一份,两条入口行为一致。配置见「九、当 MCP 用」。
## 一、前置条件
| 项 | 要求 |
|---|---|
| 博图 | TIA Portal V13 以上,本机装好 Openness |
| 用户组 | 当前 Windows 用户属于 **Siemens TIA Openness**(改完要重新登录) |
| 工程 | 博图里**打开**了工程(或用 `tia_project(action=launch)` 让插件启动并打开) |
| DLL | 插件自动找 `*:\...\PublicAPI\V*\Siemens.Engineering.dll`,取版本号最大的一套;也可用环境变量 `TIA_OPENNESS_DLL` 指定 |
> 首次调用会比较慢(要附加到博图进程)。编译、启动博图这类操作本身就要几分钟。
## 二、工具
| 工具 | 作用 |
|---|---|
| `tia_project` | `status` / `launch`(启动博图并按 path 打开工程)/ `open` / `create`(新建,可带 `orderNumber` 顺手加 PLC,`closeFirst` 先关旧工程)/ `add-device` / `archive`(归档成带时间戳的 `.zap17` 备份)/ `save` / `close` |
| `tia_list_blocks` | 设备、PLC、块树(类型、编号、语言、实例 DB 来源) |
| `tia_read_block` | 单块导出成 SimaticML,返回路径给你读 |
| `tia_export_blocks` | 批量导出(`*` 表示整台 PLC) |
| `tia_import_blocks` | 导入 `.scl` / `.xml` 并生成块;SCL 含中文时自动转 GBK 副本 |
| `tia_compile` | 编译 PLC,返回状态 + 每条消息 |
| `tia_delete_blocks` | 按名字删块 |
| `tia_set_value` | 改块/成员属性,最常用来改 DB 起始值、连接参数 |
| `tia_tags` | 变量表:`list` / `create_table` / `add_tag` |
| `tia_read_lad` | 把梯形图块读成文字:每条网络、串并联路径、触点变量、线圈类型、定时器/计数器/比较/MOVE/数学/块调用;块内 SCL 网络还原成源码 |
| `tia_write_lad` | 用「条件 + 指令框 + 线圈」**生成梯形图**并导入,可声明 `interface` 参数,可顺手编译;写 FB 时自动建实例 DB |
| `tia_write_db` | 新建全局数据块(DB):成员、起始值、结构体、数组 |
| `tia_write_scl` | 把完整 SCL 源码写成块并导入(算法类逻辑用;中文注释自动转 GBK) |
改工程的工具**只改内存**,要落盘再 `tia_project(action=save)`。
### 覆盖保护
`tia_write_lad` / `tia_write_scl` / `tia_write_db` / `tia_import_blocks` **默认不覆盖同名块**:目标块已存在时返回 `BLOCK_EXISTS`,报出它的类型、语言和编号。确认要替换时传 `overwrite=true`。
`tia_project(action=archive)` 把当前工程归档成 `工程名_时间戳.zap17`(默认落在工程文件夹下的 `dsh-backup`),改别人的工程前先做。
### 从零新建工程
一个 TIA 实例只能开一个工程,所以新建前当前工程必须先关(`closeFirst=true` 会先保存再关)。
```
tia_project(
action="create",
path="D:\\工控\\新工程\\新工程.ap17",
orderNumber="6ES7 214-1AG40-0XB0",
deviceName="PLC_1"
)
```
不经过 DSH 也能建(同一个桥接,直接跑):
```powershell
cd "D:\DeepSeek Harness\博图\tia-mcp"
.\tools\new-project.ps1 -Path "D:\工控\新工程\新工程.ap17" -OrderNumber "6ES7 214-1AG40-0XB0"
```
> V17 的签名是 `Projects.Create(DirectoryInfo, string)`。**没有 1 参数的重载**,早先版本用 `Create(path)` 会报 `METHOD_NOT_FOUND: Create/1`,已修正。
## 三、典型流程
1. `tia_project(action=status)` 看博图开没开、工程叫啥。
2. `tia_list_blocks` 摸清块结构。
3. 改 SCL → `tia_import_blocks` → `tia_compile` → 有错就改 → `tia_project(action=save)`。
### 梯形图
读:
```
tia_read_lad(name="交通灯")
块 交通灯 (LAD),共 32 条网络
网络 1 IO映射:%I0.0 -> DB.启动I
|--[ ]启动--( )交通灯数据_DB.启动I
网络 6 合并启动:两路均未按下时复位
|--[/]交通灯数据_DB.启动I--[/]交通灯数据_DB.启动HMI--(R)交通灯数据_DB.启动
```
写(`condition` 是能流条件,`boxes` 是要串进去的指令框,`output` 是末尾线圈):
```
tia_write_lad(
blockName="顶升",
blockType="FB",
interface='{"input":[{"name":"启动","dataType":"Bool"},{"name":"顶升到位","dataType":"Bool"}],
"output":[{"name":"顶起阀","dataType":"Bool"}],
"static":[{"name":"步","dataType":"Int","startValue":"0"}],
"temp":[{"name":"临时","dataType":"Int"}]}',
networks='[
{"title":"步1 等待启动","condition":"#启动 AND #步 = 0","boxes":[{"type":"Move","in":"1","out":"#步"}]},
{"title":"步2 顶起","condition":"#步 = 1 AND #顶升到位","boxes":[{"type":"TON","instance":"#T1","pt":"0.2s"},{"type":"Move","in":"2","out":"#步"}],"output":"#顶起阀","outputKind":"set"},
{"title":"复位","condition":"HMI.复位","boxes":[{"type":"Move","in":"0","out":"#步"}],"output":"#顶起阀","outputKind":"reset"}
]'
)
```
要点:
- 比较可以直接写进 `condition`(`#步 = 1`、`#计数 >= 3`),自动变成比较框;也能和触点混写 `#步 = 1 AND 顶升到位`。
- **FB/FC 自己的参数引用必须带 `#`**(`#启动`);写成裸 `启动` 会被当全局变量,编译报 `Tag "启动" not defined`。
- `interface` 的键:`input` / `output` / `inout` / `static` / `temp`;成员可带 `startValue`、`members`(结构体嵌套)。
建数据块:
```
tia_write_db(
blockName="顶升数据",
members='[
{"name":"步","dataType":"Int","startValue":"0"},
{"name":"顶升中","dataType":"Bool","startValue":"FALSE"},
{"name":"限位","dataType":"Struct","members":[{"name":"上限位","dataType":"Bool"},{"name":"下限位","dataType":"Bool"}]},
{"name":"计数","dataType":"Array[1..10] of Int"},
{"name":"延时","dataType":"Time","startValue":"T#2s"}
]'
)
```
支持的指令框:
| 类别 | 类型 | 参数 |
|---|---|---|
| 定时器 | `TON` `TOF` `TONR` `TP` | `instance`、`pt` |
| 计数器 | `CTU` `CTD` `CTUD` | `instance`、`pv`、`r`/`ld`、`cv` |
| 比较 | `Eq` `Ne` `Gt` `Ge` `Lt` `Le` | `in1`、`in2`、`dataType` |
| 传送 | `Move` | `in`、`out` |
| 数学 | `Add` `Sub` `Mul` `Div` `Mod` | `in`(数组)、`out` |
| 沿触发 | `PContact` `NContact` | `operand`、`bit` |
| 块调用 | `Call` | `block`、`blockType`、`instance`、`params` |
`instance` 写 `#T1` 表示局部多实例,会自动写进该 FB 的 Static 段(块类型得是 FB);写 `定时DB.T1` 则是全局实例。
生成的是真正的梯形图(触点/指令框/线圈),不是 SCL 伪装。导入后必须 `tia_compile` 看到 0 错误,再 `tia_read_lad` 复核。
### 选 LAD 还是 SCL
默认 **LAD**,只有算法才用 SCL:
| 逻辑 | 语言 |
|---|---|
| 步序、互锁、起保停、置位复位、延时、计数、比较、传送、模块调用、报警 | LAD |
| 循环、数组遍历、位打包、字符串/协议解析、多步临时计算 | SCL |
用户明确指定时按用户说的。一个块里可以混排(块语言 LAD、个别算法网络 SCL)。
SCL 走 `tia_write_scl`,给完整源码。**FC 没有返回值也要写 `: Void`**,否则报 "设定值 VERSION 无效":
```scl
FUNCTION "字节偏移计算" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
库位号 : Int;
END_VAR
BEGIN
#字节值 := INT_TO_DWORD((#库位号 - 1) / 8); // 中文注释会自动处理
END_FUNCTION
```
> 随插件还带一个 `tia-ladder` skill:把「默认 LAD、只有算法用 SCL」和四代目框架结构(Main 只调用、FB+同名实例 DB、步号步序、轴模板多实例、HMI/数据 DB 分工)写成可加载的规范,新会话会自动带上。
## 四、实现要点
- **每次工具调用起一个 `powershell.exe`**,Openness 的 `GetService<T>()` 通过反射调用(PowerShell 不能直接绑定泛型方法)。
- 结果写 **UTF-8 文件**而不是 stdout:Windows PowerShell 的 stdout 是 ANSI 代码页,中文会变乱码。
- `tia-bridge.ps1` 存成 **UTF-8 with BOM**,否则 PS 5.1 会按 ANSI 读脚本,中文全废。
- SCL 导入前检测编码:合法 UTF-8 且含中文 → 转 GBK 副本再导入(TIA 按系统 ANSI 代码页解析外部源)。
- 一次调用一个进程,TIA 卡死/崩溃不会拖垮 harness;代价是每次几秒的附加开销。
## 五、已知限制
- 梯形图生成覆盖**触点 + 线圈 + 常见指令框**(定时器/计数器/比较/MOVE/数学/沿触发/块调用),实测导入后编译 0 错误;工艺对象(如 `SinaPos`)、PID 等专用指令暂未支持,需要时先手工在博图里放一个再读出来改,或用 `tia_write_scl` 写。
- 并联在 SimaticML 里不是「两条线并进同一个引脚」(博图会拒绝),而是插一个显式的 `O` 部件(`TemplateValue Card=分支数`,引脚 `in1/in2/.../out`)。
- 时间常量必须写成 `Scope="TypedConstant"` + `T#2s`;写 `LiteralConstant + ConstantType=Time` 会被拒绝。
- 读梯形图是**静态结构**:指令框会标出实例和已绑定的引脚,但内部时序不展开。
- 不能在线读写 PLC 实时值、不能下载到 PLC(Openness 的下载接口没接;实时值建议走 Modbus/S7)。
- 一个 TIA 实例同一时间只接受一个 Openness 连接;两个工具并发调用会排队/失败。
- 博图弹窗(如“项目来自旧版本,是否升级”)会卡住调用,直到你手动点掉。
## 六、排错
| 现象 | 处理 |
|---|---|
| `TIA_NOT_RUNNING` | 先启动博图,或 `tia_project(action=launch, path=...)` |
| `TIA_ATTACH_FAILED` | 检查 Openness 用户组、是否同一用户启动、DLL 版本是否与博图一致 |
| `NO_PROJECT_OPEN` | 博图里打开 `.ap17` 工程 |
| `OPENNESS_NOT_FOUND` | 设 `TIA_OPENNESS_DLL` 指向 `PublicAPI\V17\Siemens.Engineering.dll` |
| 中文乱码 | 确认 `tia-bridge.ps1` 仍是 UTF-8 **带 BOM** |
| 超时 | 编译/启动给足时间;看博图是否有弹窗 |
调试时设环境变量 `TIA_DEBUG=1`,错误里会带上出错行位置。
## 七、自测与发布前检查
生成器自测(不依赖博图,纯本地):
```powershell
cd test
node test-lad.mjs # FC 起保停/置位/复位 生成 + 回读
node test-boxes.mjs # TON/CTU/比较/MOVE/数学/沿/块调用 生成
node test-blocks.mjs # DB / FB 接口 / 比较条件 / 算术框 Card 规则
```
输出写到 `test/out/`,不会污染 `lad-ref/` 里的真实导出。
### 发布前自检
```powershell
powershell -File tools\check.ps1
```
检查每个 `.ps1` 是否带 **UTF-8 BOM**(PS 5.1 缺 BOM 会按 ANSI 读,中文注释把脚本解析成乱码报 `Unexpected token`)并做语法解析,再 `node --check` 一遍 `lib\*.js`。
> 编辑 `.ps1` 后务必重跑一次——多数编辑器/工具保存时会悄悄去掉 BOM。
## 八、当 Codex skill 用
工具解决"能操作",skill 解决"知道怎么操作才不出错"。仓库里 `codex-skill/` 就是给 Codex 用的那份:
```
codex-skill\
SKILL.md 8 条硬规则 + 6 步流程 + 7 项交活自检
agents\openai.yaml Codex 的 skill 清单
references\mcp-tools.md 工具完整语法、常见编译错误(写块前必读)
references\lad-scl-selection.md 选型矩阵
references\fourth-gen-framework.md 四代目框架快照
references\simaticml-pitfalls.md 直接改 XML 时的 14 个坑
```
安装就是复制目录:
```powershell
Copy-Item -Recurse ".\codex-skill" "$env:USERPROFILE\.codex\skills\tia-portal-plc"
```
装完在 Codex 里用 `$tia-portal-plc` 引用,或让它自己按描述触发。
### skill 和 MCP 的分工
| | 提供什么 |
|---|---|
| MCP server | 13 个 `tia_*` 工具,能真正读写博图 |
| skill | 什么时候读、什么语言写、什么顺序写、什么算完成 |
两个都要配:只有 MCP,模型会用工具但不守规矩(直接覆盖、跳过编译、乱选语言);只有 skill,规则再全也落不了地。
### 三处副本怎么同步
```powershell
powershell -File tools\sync-skill.ps1
```
真源是 `~\.codex\skills\tia-portal-plc\`(Codex 在读),镜像到:
- `.dsh\skills\tia-ladder\` —— DSH 会话加载,frontmatter 自动加上 `whenToUse`
- `tia-mcp\skills\` —— 随插件打包
没装 Codex 的机器会自动回落到仓库里的 `codex-skill\`,所以 clone 下来就能用。
## 九、当 MCP 用(Codex / Claude Code / Cursor)
`mcp/server.js` 是一个标准 **stdio MCP server**,把同样的 13 个工具暴露出去:
```
mcp/server.js 手写的 JSON-RPC(不依赖 npm 包),每行一条消息
mcp/run.ps1 启动器:自动找 node(Codex 自带的 / DSH 的 / PATH 里的)
```
不用装任何依赖,`node mcp/server.js` 直接跑。
### Codex 配置
`~/.codex/config.toml`:
```toml
[mcp_servers.tia-portal]
command = "powershell.exe"
args = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", "D:/DeepSeek Harness/博图/tia-mcp/mcp/run.ps1"]
startup_timeout_sec = 60
tool_timeout_sec = 900
```
`tool_timeout_sec` 别设小:编译一个大工程要几分钟。
### 为什么要 run.ps1 这层
这台机器上没有独立的 `node.exe`,只有两个候选:
- Codex 应用自带的(`WindowsApps\OpenAI.Codex_<版本>\app\resources\cua_node\bin\node.exe`,路径带版本号,升级会变)
- DSH Desktop 的 `node.cmd`(内部是 `ELECTRON_RUN_AS_NODE=1` 起桌面程序)
把"找 node"放进启动器,客户端配置就固定指向 `run.ps1`,上面两条路径怎么变都不用改 TOML。
也可以用环境变量 `TIA_MCP_NODE` 强制指定。
### 注意
- **别让 DSH 和 Codex 同时改同一个工程**。Openness 允许多客户端 Attach,但同一个 `.ap17` 两边同时写会互相覆盖。
- MCP 客户端多半有自己的沙箱。bridge 要起 `powershell.exe` 子进程、读写临时文件、连 TIA,沙箱太严会失败(Codex 需要 `sandbox_mode = "danger-full-access"` 或等效设置)。
- 工具名在客户端里可能带前缀(`tia-portal__tia_project` 之类),那是客户端自己加的。
### 自测
```powershell
# 发两条消息看 initialize 和工具清单
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}',
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | powershell -File mcp\run.ps1
```This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues