dsh-plc-decode 0.0.0-stage → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +85 -0
- package/README.md +128 -2
- package/assets/codesys-tools/check_project.py +390 -0
- package/assets/codesys-tools/dvp_decode.py +588 -0
- package/assets/codesys-tools/precompile_extract.py +146 -0
- package/assets/codesys-tools/read_project.py +506 -0
- package/assets/decode-pack.mjs +206 -0
- package/assets/icon-180.png +0 -0
- package/assets/smart-lad.mjs +299 -0
- package/assets/smart-net-trace.mjs +199 -0
- package/cordis.patch.yml +4 -0
- package/index.js +3435 -0
- package/invariant.js +10 -0
- package/package.json +57 -4
- package/scripts/smoke.mjs +157 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// decode-pack.mjs —— 解码产物落盘形态(合并版)
|
|
2
|
+
//
|
|
3
|
+
// 一份数据一处落,按读者分三路:
|
|
4
|
+
// · 目录件:00-目录.md(块表+怎么查+文件清单)——AI 只先读这一份
|
|
5
|
+
// · AI 件:程序.lad(网络级逻辑)、元素记录.tsv(块内序号+指令+操作数)、
|
|
6
|
+
// 符号地址.tsv(用户变量/系统符号/子程序参数合并一张表)、分析.md(算出来的部分)
|
|
7
|
+
// · 人看件:梯形图在对话里按需出,本目录不存图
|
|
8
|
+
// · 机器件:记录.json、字符串全录.txt(兄弟工具按这两个名字认已解码目录)
|
|
9
|
+
//
|
|
10
|
+
// 合并:块级操作数流.tsv 与元素记录.tsv 信息重叠(后者有序号即可);
|
|
11
|
+
// 符号.tsv 与子程序接口.tsv 都是「名字↔地址」,并成 符号地址.tsv;解码报告.md 降为指路页。
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { execFileSync } from 'node:child_process'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
|
|
17
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
const B = (s) => Buffer.byteLength(s, 'utf8')
|
|
19
|
+
const off4 = (o) => String(o || '').replace(/^0x/, '').toUpperCase()
|
|
20
|
+
const num = (n) => Number(n || 0).toLocaleString('en-US')
|
|
21
|
+
|
|
22
|
+
/** LAD 指令序列压成 AI 件:[盒] 那行长注释换成 [盒N],去掉尾部空白 */
|
|
23
|
+
export function compactLad(raw) {
|
|
24
|
+
let t = raw
|
|
25
|
+
.replace(/^# 说明:[\s\S]*?\n# 指令盒[^\n]*\n/m, '')
|
|
26
|
+
.replace(/^## 块:/gm, '## ')
|
|
27
|
+
.replace(/\[盒\]\s+← 位次 (\d+)(指令盒记录,未列元件行)/g, '[盒$1]')
|
|
28
|
+
.replace(/[ \t]+$/gm, '')
|
|
29
|
+
return '# LAD 指令序列:LD/A/O/AN/ON 触点、EU/ED 边沿、LPS/LRD/LPP 并联支路、=/S/R 线圈、比较带第二操作数\n'
|
|
30
|
+
+ '# [盒N] = 该位次是指令盒,盒内实参见 元素记录.tsv 同一块的行\n\n' + t.trimStart()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 从原报告里切出「算出来的」分析节(记录类的 2 与 2.5 不要——那是记录的第二遍) */
|
|
34
|
+
export function sliceAnalysis(reportText) {
|
|
35
|
+
const secs = {}
|
|
36
|
+
let cur = '', buf = []
|
|
37
|
+
for (const ln of reportText.split('\n')) {
|
|
38
|
+
const m = /^## (\S.*)$/.exec(ln)
|
|
39
|
+
if (m) { if (cur) secs[cur] = buf.join('\n'); cur = m[1]; buf = [] } else buf.push(ln)
|
|
40
|
+
}
|
|
41
|
+
if (cur) secs[cur] = buf.join('\n')
|
|
42
|
+
let out = ''
|
|
43
|
+
for (const [k, v] of Object.entries(secs)) {
|
|
44
|
+
if (/^(2\.[6-9]|2\.7b|3\.5|3\.6|3\.7|5\.)/.test(k)) out += `## ${k}\n${v}\n`
|
|
45
|
+
}
|
|
46
|
+
return out
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** CoDeSys/DVP 侧的导航件:人看 ST 源码(pous/)或指令表报告,AI 先读本页与源码 */
|
|
50
|
+
export function writeSourcePack({ outd, kind, projName, projFile }) {
|
|
51
|
+
const list = []
|
|
52
|
+
const pousDir = path.join(outd, 'pous')
|
|
53
|
+
if (fs.existsSync(pousDir)) {
|
|
54
|
+
for (const f of fs.readdirSync(pousDir)) {
|
|
55
|
+
const p = path.join(pousDir, f)
|
|
56
|
+
if (!fs.statSync(p).isFile()) continue
|
|
57
|
+
const t = fs.readFileSync(p, 'utf8')
|
|
58
|
+
list.push({ name: f.replace(/\.txt$/, ''), file: `pous/${f}`, bytes: B(t), lines: t.split('\n').length })
|
|
59
|
+
}
|
|
60
|
+
list.sort((a, b) => b.bytes - a.bytes)
|
|
61
|
+
}
|
|
62
|
+
const sizeOf = (p) => { try { return B(fs.readFileSync(path.join(outd, p), 'utf8')) } catch { return 0 } }
|
|
63
|
+
const has = (p) => fs.existsSync(path.join(outd, p))
|
|
64
|
+
let nav = `# ${projName || path.basename(projFile || outd)} · 解码索引(${kind})\n\n`
|
|
65
|
+
nav += `- 源文件: ${path.basename(projFile || '')}\n- 输出目录: ${outd}\n`
|
|
66
|
+
nav += `- 规模: ${list.length ? `POU/对象 ${list.length} 个` : '(见下方文件清单)'}\n\n## 文件\n\n| 文件 | 大小 | 读者 | 说明 |\n|---|---|---|---|\n`
|
|
67
|
+
if (list.length) nav += `| pous/ | ${num(list.reduce((n, x) => n + x.bytes, 0))} B(${list.length} 个) | 人+AI | **看程序用这个**:每个 POU 一份 ST 源码(含注释与变量声明),按需要的那份读 |\n`
|
|
68
|
+
if (has('工程内容报告.md')) nav += `| 工程内容报告.md | ${num(sizeOf('工程内容报告.md'))} B | 证据 | 对象树逐对象的原始串(含 GUID/属性),人不必通读 |\n`
|
|
69
|
+
if (has('解码报告.md')) nav += `| 解码报告.md | ${num(sizeOf('解码报告.md'))} B | AI | 结构/段落注释/设备注释/指令表反汇编 |\n`
|
|
70
|
+
if (has('程序解读.md')) nav += `| 程序解读.md | ${num(sizeOf('程序解读.md'))} B | 人 | 网络级功能地图+I/O 表+机制链+未确证清单(人工解读,重解码不覆盖) |\n`
|
|
71
|
+
if (has('全部字符串.txt')) nav += `| 全部字符串.txt | ${num(sizeOf('全部字符串.txt'))} B | 检索 | 全工程可读串(按出现顺序编号),查关键词用 |\n`
|
|
72
|
+
if (has('对象表.md')) nav += `| 对象表.md | ${num(sizeOf('对象表.md'))} B | AI | 对象清单 |\n`
|
|
73
|
+
if (has('项目树.md')) nav += `| 项目树.md | ${num(sizeOf('项目树.md'))} B | AI | 项目树 |\n`
|
|
74
|
+
if (kind === 'dvp') {
|
|
75
|
+
nav += `\n## 怎么查(按问题挑一条,别整份读)\n\n`
|
|
76
|
+
nav += `- 某段指令表 → 按下文「网络行号表」取段(行号=解码报告里的行,从 1 起)\n`
|
|
77
|
+
nav += `- 某地址在哪些网络出现 → grep 地址(如 \`grep -n "D2340" 解码报告.md\`),只读命中的那几行\n`
|
|
78
|
+
nav += `- 找中文注释/设备名 → grep 解码报告.md(dvp 只出这一份文本,不生成 全部字符串.txt)\n\n`
|
|
79
|
+
nav += `## 读法\n\n- **看程序**:台达 DVP 没有 pous/ST,程序就是 \`解码报告.md\` 的指令表(按 \`LD M1000\` 分网络,末尾 END)。\n- **AI**:目录里有 \`程序解读.md\` 就先读它(网络级功能地图+I/O 表+机制链+未确证清单),再回指令表逐段核对。报告里 \`?名\`(如 \`?DRVI\`)与 \`?CMP(关系)\` 是**推断**指令名——报告末尾有「推断对照表」(码/推测名/依据/把握),拿它下结论前先核台达指令表;\`??xx\` 与 \`?CMP[六位]\` 是推断不出、保持未定的。\n- 读之前核对源工程文件修改时间是否与本次解码一致(工程一改,导出即过期)。\n`
|
|
80
|
+
|
|
81
|
+
// 网络行号表:让 AI 能按行号取一段,而不是整份读报告
|
|
82
|
+
const rep = path.join(outd, '解码报告.md')
|
|
83
|
+
if (fs.existsSync(rep)) {
|
|
84
|
+
const lines = fs.readFileSync(rep, 'utf8').split('\n')
|
|
85
|
+
const starts = []
|
|
86
|
+
for (let i = 0; i < lines.length; i++) {
|
|
87
|
+
if (/\bLD\s+M1000\b/.test(lines[i]) && /^\s*(行\s*\d+\s*[::])?\s*LD\s+M1000\b/.test(lines[i])) starts.push(i + 1)
|
|
88
|
+
}
|
|
89
|
+
if (starts.length) {
|
|
90
|
+
const CAP = 50
|
|
91
|
+
nav += `\n## 网络定位(共 ${starts.length} 个网络;要哪段用 按行号取段,别整份读)\n\n| # | 起始行 | 首条 |\n|---|---|---|\n`
|
|
92
|
+
for (let k = 0; k < Math.min(starts.length, CAP); k++) {
|
|
93
|
+
const from = starts[k]
|
|
94
|
+
const head = (lines[from - 1] || '').trim().replace(/^行\s*\d+\s*[::]\s*/, '').slice(0, 46).replace(/\|/g, '\\|')
|
|
95
|
+
nav += `| ${k + 1} | ${from} | ${head} |\n`
|
|
96
|
+
}
|
|
97
|
+
if (starts.length > CAP) nav += `\n(其余 ${starts.length - CAP} 个网络按行号顺延,或直接 grep 地址定位)\n`
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
nav += `\n## 读法\n\n- **看程序**:读 pous/<名字>.txt(ST 本体,结构化、最小)。\n- **AI**:先读本页 → 按 POU 读源码(grep 变量名/地址即可跨 POU 定位);要全量检索用 全部字符串.txt。\n- 读源码前先核对源工程文件的修改时间是否与本次解码一致(工程一改,导出即过期)。\n`
|
|
102
|
+
}
|
|
103
|
+
if (list.length) {
|
|
104
|
+
nav += `\n## POU/对象清单\n\n| # | 名称 | 大小 | 行 | 文件 |\n|---|---|---|---|---|\n`
|
|
105
|
+
list.forEach((x, i) => { nav += `| ${i + 1} | ${x.name} | ${num(x.bytes)} | ${x.lines} | ${x.file} |\n` })
|
|
106
|
+
}
|
|
107
|
+
fs.writeFileSync(path.join(outd, '00-索引.md'), nav, 'utf8')
|
|
108
|
+
return { nav: B(nav), pous: list.length }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function writeSmartPack({ out, projName, fingerprint, container, version, records, userSyms, sysSyms, blockStreams, blockIfaces, blockStats, reportText, smartPath }) {
|
|
112
|
+
const blocks = []
|
|
113
|
+
const byBlock = new Map()
|
|
114
|
+
for (const r of records) { if (!byBlock.has(r.pou)) { byBlock.set(r.pou, []); blocks.push(r.pou) } byBlock.get(r.pou).push(r) }
|
|
115
|
+
|
|
116
|
+
// ── AI 件:元素记录(块内序号;偏移不再进表——AI 按块+序号就能定位,偏移留给 记录.json)──
|
|
117
|
+
const recLines = ['块\t序号\t指令\t操作数']
|
|
118
|
+
const ranges = []
|
|
119
|
+
let ln = 1
|
|
120
|
+
for (const b of blocks) {
|
|
121
|
+
const st = ln + 1
|
|
122
|
+
let k = 0
|
|
123
|
+
for (const r of byBlock.get(b)) { recLines.push(`${b}\t${++k}\t${r.instr}\t${(r.operands || []).join(';')}`); ln++ }
|
|
124
|
+
ranges.push({ block: b, from: st, to: ln, n: byBlock.get(b).length })
|
|
125
|
+
}
|
|
126
|
+
const recTxt = recLines.join('\n') + '\n'
|
|
127
|
+
fs.writeFileSync(path.join(out, '元素记录.tsv'), recTxt, 'utf8')
|
|
128
|
+
|
|
129
|
+
// ── AI 件:符号地址(用户变量+系统符号+子程序参数,合并一张表)──
|
|
130
|
+
const symLines = ['种类\t名字\t地址\t类型']
|
|
131
|
+
for (const s of (userSyms || [])) symLines.push(`用户变量\t${s.name}\t${s.addr}\t`)
|
|
132
|
+
for (const s of (sysSyms || [])) symLines.push(`系统符号\t${s.name}\t${s.addr}\t`)
|
|
133
|
+
let ifaceCount = 0
|
|
134
|
+
for (const b of (blockIfaces || [])) {
|
|
135
|
+
for (const i of (b.ifs || [])) { symLines.push(`子程序参数\t${b.name}.${i.name}\t${i.addr || ''}\t${i.type || ''}`); ifaceCount++ }
|
|
136
|
+
}
|
|
137
|
+
const symTxt = symLines.join('\n') + '\n'
|
|
138
|
+
fs.writeFileSync(path.join(out, '符号地址.tsv'), symTxt, 'utf8')
|
|
139
|
+
|
|
140
|
+
// ── AI 件:分析(算出来的部分)──
|
|
141
|
+
const anaTxt = sliceAnalysis(reportText)
|
|
142
|
+
fs.writeFileSync(path.join(out, '分析.md'), anaTxt, 'utf8')
|
|
143
|
+
|
|
144
|
+
// ── AI 件:程序.lad(网络级逻辑)──
|
|
145
|
+
let ladTxt = ''
|
|
146
|
+
try {
|
|
147
|
+
ladTxt = compactLad(execFileSync(process.execPath, [path.join(HERE, 'smart-lad.mjs'), smartPath], { encoding: 'utf8', maxBuffer: 64 << 20, timeout: 180000 }))
|
|
148
|
+
} catch (e) { ladTxt = '' }
|
|
149
|
+
if (ladTxt) fs.writeFileSync(path.join(out, '程序.lad'), ladTxt, 'utf8')
|
|
150
|
+
|
|
151
|
+
// ── 网络数:从 程序.lad 数 ──
|
|
152
|
+
const netOf = {}
|
|
153
|
+
if (ladTxt) {
|
|
154
|
+
let cur = null
|
|
155
|
+
for (const ln of ladTxt.split('\n')) {
|
|
156
|
+
const m = /^## (.+)$/.exec(ln)
|
|
157
|
+
if (m) { cur = m[1].trim(); netOf[cur] = 0; continue }
|
|
158
|
+
if (cur && /^Network \d+/.test(ln)) netOf[cur]++
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const statOf = new Map((blockStats || []).map((s) => [s.name, s]))
|
|
162
|
+
const sizeOf = (p) => { try { return B(fs.readFileSync(path.join(out, p), 'utf8')) } catch { return 0 } }
|
|
163
|
+
|
|
164
|
+
// ── 目录件(AI 先读这一份;目标 2K 以内)──
|
|
165
|
+
let dir = `# ${projName || '(未命名)'} · 目录\n\n`
|
|
166
|
+
dir += `- 源文件: ${fingerprint.file} ${num(fingerprint.size)} B ${fingerprint.mtime}(用前核指纹,不一致=已过期)\n`
|
|
167
|
+
dir += `- 规模: 块 ${blocks.length} / 元素 ${records.length} / 用户变量 ${(userSyms || []).length} / 系统符号 ${(sysSyms || []).length} / 子程序参数 ${ifaceCount}\n`
|
|
168
|
+
if (version) dir += `- 文档版本: ${version}${container ? `;容器: ${container}` : ''}\n`
|
|
169
|
+
dir += `\n## 块\n\n| 块 | 网络 | 元素 | 子程序参数 | 被调用 |\n|---|---|---|---|---|\n`
|
|
170
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
171
|
+
const b = blocks[i]
|
|
172
|
+
const st = statOf.get(b) || {}
|
|
173
|
+
const ifs = ((blockIfaces || []).find((x) => x.name === b)?.ifs || []).length
|
|
174
|
+
const nets = netOf[b] ?? '—'
|
|
175
|
+
dir += `| ${b} | ${nets} | ${ranges[i].n} | ${ifs} | ${st.calls ?? '—'} |\n`
|
|
176
|
+
}
|
|
177
|
+
dir += `\n## 怎么查(按问题挑一条,别整份读文件)\n\n`
|
|
178
|
+
dir += `- 某地址谁写谁读 → \`smart-ladder-elements\` addr=VD20(一批用逗号)\n`
|
|
179
|
+
dir += `- 某块某网络的梯形图 → \`smart-lad\` block=自动 net=3\n`
|
|
180
|
+
dir += `- 子程序内部结构(参数/盒链/调用点)→ \`smart-pou-structure\` pou=<块名>\n`
|
|
181
|
+
dir += `- 地址规划与空闲段 → \`smart-free-space\`;地址冲突体检 → \`smart-conflict\`\n`
|
|
182
|
+
dir += `- 字节级核对(排查用,出偏移)→ \`smart-net-trace\` / \`smart-box-pins\`\n`
|
|
183
|
+
dir += `\n## 文件(要什么读什么)\n\n| 文件 | 大小 | 用途 | 读不读 |\n|---|---|---|---|\n`
|
|
184
|
+
if (ladTxt) dir += `| 程序.lad | ${num(sizeOf('程序.lad'))} B | 按块分段的网络级逻辑 | 要逻辑时读,按 ## 块名定位 |\n`
|
|
185
|
+
dir += `| 元素记录.tsv | ${num(B(recTxt))} B | 块\\t序号\\t指令\\t操作数 | 按块 grep,不要整份读 |\n`
|
|
186
|
+
dir += `| 符号地址.tsv | ${num(B(symTxt))} B | 名字↔地址(含子程序参数) | grep 名字 |\n`
|
|
187
|
+
dir += `| 分析.md | ${num(B(anaTxt))} B | 信号流/空闲段/静态体检等算出来的结论 | 要结论时读 |\n`
|
|
188
|
+
dir += `| 记录.json | ${num(sizeOf('记录.json'))} B | 兄弟工具的输入(含偏移与全量记录) | AI 不读 |\n`
|
|
189
|
+
dir += `| 字符串全录.txt | ${num(sizeOf('字符串全录.txt'))} B | 工程内全部可读串 | 查关键词时才 grep |\n`
|
|
190
|
+
dir += `\n梯形图不落盘:按需用 smart-lad 取某块某网络的指令序列(串并联/边沿/比较)。\n`
|
|
191
|
+
fs.writeFileSync(path.join(out, '00-目录.md'), dir, 'utf8')
|
|
192
|
+
|
|
193
|
+
// ── 指路页:保留 解码报告.md 这个名字(兄弟工具按它认目录),内容只留指路 ──
|
|
194
|
+
let nav = `# ${projName || '(未命名)'} · 解码产物\n\n`
|
|
195
|
+
nav += `- 源文件指纹: ${fingerprint.file} ${num(fingerprint.size)} B / ${fingerprint.mtime}(用前先核对;不一致=已过期)\n`
|
|
196
|
+
nav += `- **先读 00-目录.md**(块表+怎么查+文件清单);本页只是指路,已不再写全量报告。\n`
|
|
197
|
+
nav += `- 文件:程序.lad(逻辑)/元素记录.tsv(盒实参)/符号地址.tsv(名字↔地址)/分析.md(结论)/记录.json(机器件)/字符串全录.txt(检索)\n`
|
|
198
|
+
nav += `- 梯形图不落盘,在对话里出。\n`
|
|
199
|
+
fs.writeFileSync(path.join(out, '解码报告.md'), nav, 'utf8')
|
|
200
|
+
|
|
201
|
+
const sizes = {
|
|
202
|
+
'00-目录.md': B(dir), '程序.lad': B(ladTxt), '元素记录.tsv': B(recTxt), '符号地址.tsv': B(symTxt), '分析.md': B(anaTxt),
|
|
203
|
+
}
|
|
204
|
+
const total = B(dir) + B(ladTxt) + B(recTxt) + B(symTxt) + B(anaTxt)
|
|
205
|
+
return { sizes, total }
|
|
206
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// smart-lad.mjs —— 从 S7-200 SMART .smart 直接还原每个块的「LAD 指令序列」(= 串/并联结构),不依赖软件导出 STL
|
|
2
|
+
//
|
|
3
|
+
// 原理(破解,用导出的 .awl 逐网络回归验证 16/16):
|
|
4
|
+
// · 每个元件记录 = [u32 序号][u16 操作码][9B 头][9B 实例][载荷];序号 = 该指令在网络里的位次
|
|
5
|
+
// · 结构指令(LPS/LRD/LPP/EU/ED)单独成记录:[u32 序号][1B 操作码][02 01 01 00 00]
|
|
6
|
+
// · 比较指令的第二个操作数(常数)紧跟其后:[01 02 <len> 01][u32 值]
|
|
7
|
+
//
|
|
8
|
+
// 用法: node smart-lad.mjs <工程.smart> [块名] [--net=N]
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import zlib from 'node:zlib'
|
|
11
|
+
|
|
12
|
+
const AREA = { 1: 'I', 2: 'Q', 0x10: 'V', 0x20: 'M', 0x40: 'T', 0x80: 'C', 0x200: 'SM', 0x2000: 'L' }
|
|
13
|
+
const TY = { 1: 'bit', 2: 'byte', 4: 'word', 8: 'dword' }
|
|
14
|
+
// 操作码表(验证):元件记录第二个 u16
|
|
15
|
+
const OPID = {
|
|
16
|
+
510: 'LD', 511: 'A', 512: 'O', 513: 'ON', 514: 'AN',
|
|
17
|
+
532: '=', 534: 'S', 536: 'R',
|
|
18
|
+
588: 'LDW=', 589: 'LW=', 590: 'OW=', 591: 'W=',
|
|
19
|
+
592: 'LDW<>', 600: 'LDW>', 601: 'LW>', 602: 'LDW<', 603: 'LW<',
|
|
20
|
+
604: 'OW>', 605: 'OW<', 606: 'OW<>', 607: 'OW>=', 608: 'OW<=',
|
|
21
|
+
700: 'CTU_PV', 730: '*I', 736: 'INCW', 781: 'MOVW', 855: 'TON',
|
|
22
|
+
}
|
|
23
|
+
// 结构指令记录里那一个字节
|
|
24
|
+
const CTRL = { 0x0b: 'EU', 0x0d: 'ED', 0x11: 'LPS', 0x12: 'LRD', 0x13: 'LPP' }
|
|
25
|
+
|
|
26
|
+
const file = process.argv[2]
|
|
27
|
+
const wantBlock = process.argv[3] && !process.argv[3].startsWith('--') ? process.argv[3] : null
|
|
28
|
+
const wantNet = (process.argv.find(a => a.startsWith('--net=')) || '').replace('--net=', '')
|
|
29
|
+
if (!file) { console.error('用法: node smart-lad.mjs <工程.smart> [块名] [--net=N]'); process.exit(1) }
|
|
30
|
+
|
|
31
|
+
const buf = fs.readFileSync(file)
|
|
32
|
+
if (buf.readUInt32LE(0) !== 0x00334853) throw new Error('不是 SH3 头的 .smart 工程')
|
|
33
|
+
const docLen = buf.readUInt32LE(0x6c)
|
|
34
|
+
const d = zlib.inflateSync(buf.subarray(0x70, 0x70 + docLen + 4096)).subarray(0, docLen)
|
|
35
|
+
const gbk = new TextDecoder('gb18030')
|
|
36
|
+
|
|
37
|
+
// 符号表:名字 → 地址(用于把符号名形式的记录解析回地址)
|
|
38
|
+
const sym = new Map()
|
|
39
|
+
for (let i = 0; i < d.length - 24; i++) {
|
|
40
|
+
if (d[i] !== 2 || d[i + 1] !== 0) continue
|
|
41
|
+
const ln = d.readUInt16LE(i + 2)
|
|
42
|
+
if (ln > 0 && ln < 40 && i + 17 + ln < d.length && d[i + 4 + ln] === 0 && d[i + 5 + ln] === 1 && d[i + 6 + ln] === 2 && d[i + 7 + ln] === 0) {
|
|
43
|
+
const t = d[i + 8 + ln]
|
|
44
|
+
if (TY[t] && AREA[d.readUInt32LE(i + 9 + ln)]) {
|
|
45
|
+
const n = gbk.decode(d.subarray(i + 4, i + 4 + ln)).replace(/\0+$/, '')
|
|
46
|
+
if (n && !sym.has(n)) sym.set(n, addrOf(t, d.readUInt32LE(i + 9 + ln), d.readUInt32LE(i + 13 + ln)))
|
|
47
|
+
}
|
|
48
|
+
i += 17 + ln
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function addrOf(t, area, v) {
|
|
54
|
+
const a = AREA[area]
|
|
55
|
+
if (t === 1) return (area === 0x40 || area === 0x80) ? a + v : a + (v >> 3) + '.' + (v & 7)
|
|
56
|
+
return a + ({ 2: 'B', 4: 'W', 8: 'D' })[t] + v
|
|
57
|
+
}
|
|
58
|
+
function operand(p) { // p = 元素头;返回 {name, form, negated?}
|
|
59
|
+
const q = p + 18
|
|
60
|
+
if (d[q] === 2 && d[q + 1] === 0 && TY[d[q + 2]] && AREA[d.readUInt32LE(q + 3)]) {
|
|
61
|
+
return addrOf(d[q + 2], d.readUInt32LE(q + 3), d.readUInt32LE(q + 7))
|
|
62
|
+
}
|
|
63
|
+
if (d[q] === 1 && d[q + 1] === 0 && d[q + 2] === 1 && d[q + 3] === 0) {
|
|
64
|
+
const l = d[q + 4]
|
|
65
|
+
return '"' + gbk.decode(d.subarray(q + 5, q + 5 + l)) + '"'
|
|
66
|
+
}
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
function constAfter(p, limit = 0x40) { // 比较指令的第二个操作数(常数)或地址(跳过它自己的第一个操作数)
|
|
70
|
+
for (let q = p + 18 + 11; q < Math.min(p + 18 + limit, d.length - 10); q++) {
|
|
71
|
+
if (d[q] === 1 && d[q + 1] === 2 && d[q + 3] === 1) {
|
|
72
|
+
const v = d.readUInt32LE(q + 4)
|
|
73
|
+
if (v < 100000000) return (d[q + 2] === 4 ? (v | 0) : v).toString()
|
|
74
|
+
}
|
|
75
|
+
if (d[q] === 2 && d[q + 1] === 0 && TY[d[q + 2]] && AREA[d.readUInt32LE(q + 3)]) {
|
|
76
|
+
return addrOf(d[q + 2], d.readUInt32LE(q + 3), d.readUInt32LE(q + 7))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 槽式网络的标记块:单字节 P/N/S/R,后面跟 ≥16 个 0 */
|
|
83
|
+
function markBefore(p) {
|
|
84
|
+
const hits = []
|
|
85
|
+
for (let q = p - 0x40; q >= Math.max(0, p - 0x160); q--) {
|
|
86
|
+
const c = d[q]
|
|
87
|
+
if ((c === 0x50 || c === 0x4e || c === 0x53 || c === 0x52) && d[q + 1] === 0 && d[q + 2] === 0) {
|
|
88
|
+
let z = 0; for (let t = q + 1; t < q + 40 && d[t] === 0; t++) z++
|
|
89
|
+
if (z >= 16) hits.push({ q, ch: String.fromCharCode(c) })
|
|
90
|
+
if (hits.length >= 2) break
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// 线圈标记距元件头约 0x69,边沿标记约 0x119
|
|
94
|
+
const coil = hits.find(h => p - h.q >= 0x40 && p - h.q <= 0x90 && (h.ch === 'S' || h.ch === 'R'))
|
|
95
|
+
const edge = hits.find(h => p - h.q > 0x90 && p - h.q <= 0x160 && (h.ch === 'P' || h.ch === 'N'))
|
|
96
|
+
return [coil, edge]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 符号名记录里的名字(Y=2 形式) */
|
|
100
|
+
function symbolOf(p) {
|
|
101
|
+
const q = p + 18
|
|
102
|
+
if (d[q] === 1 && d[q + 1] === 0 && d[q + 2] === 1 && d[q + 3] === 0) {
|
|
103
|
+
const l = d[q + 4]
|
|
104
|
+
if (l > 0 && l < 60) {
|
|
105
|
+
const name = gbk.decode(d.subarray(q + 5, q + 5 + l))
|
|
106
|
+
return '"' + name + '"'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 抽一个字节区间里的指令记录 */
|
|
113
|
+
function extract(A, B) {
|
|
114
|
+
const isHeadP = (p) => d[p] === 1 && d[p + 1] === 1 && [1, 2].includes(d[p + 2]) && d[p + 3] === 0 && d[p + 4] === 0 && d[p + 7] === 3 && d[p + 8] === 1
|
|
115
|
+
// 预扫:槽式线圈的位置(符号名形式的触点只在槽式网络里才算指令)
|
|
116
|
+
const coilAt = []
|
|
117
|
+
for (let p = A; p < B; p++) if (isHeadP(p)) { const mk = markBefore(p); if (mk[0]) coilAt.push(p) }
|
|
118
|
+
const recs = []
|
|
119
|
+
for (let p = A; p < B; p++) {
|
|
120
|
+
// 结构指令
|
|
121
|
+
if (d[p + 5] === 0x02 && d[p + 6] === 0x01 && d[p + 7] === 0x01 && d[p + 8] === 0x00 && d[p + 9] === 0x00 && CTRL[d[p + 4]]) {
|
|
122
|
+
recs.push({ p, idx: d.readUInt32LE(p), ins: CTRL[d[p + 4]], arg: '' })
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
// 槽式网络的分支标记(b6=2 记录 = 新支路,等价 LRD)
|
|
126
|
+
if (d[p] === 1 && d[p + 1] === 1 && d[p + 2] === 0 && d[p + 3] === 0 && d[p + 4] === 0 && d[p + 6] === 2 && d[p + 7] === 3 && d[p + 8] === 1) {
|
|
127
|
+
recs.push({ p, idx: -1, ins: 'LRD', arg: '', slot: true, marker: true })
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
// 元件
|
|
131
|
+
if (d[p] === 1 && d[p + 1] === 1 && [1, 2].includes(d[p + 2]) && d[p + 3] === 0 && d[p + 4] === 0 && d[p + 7] === 3 && d[p + 8] === 1) {
|
|
132
|
+
const y = d[p + 6]
|
|
133
|
+
const arg0 = operand(p)
|
|
134
|
+
if (y === 1 && arg0) {
|
|
135
|
+
const opid = d.readUInt16LE(p - 2), idx = d.readUInt32LE(p - 6)
|
|
136
|
+
const ins = OPID[opid]
|
|
137
|
+
if (ins) {
|
|
138
|
+
let extra = ''
|
|
139
|
+
if (/^LDW|^LW|^OW|^W/.test(ins) && /[<>=]/.test(ins)) { const c = constAfter(p); if (c !== null) extra = ', ' + c }
|
|
140
|
+
recs.push({ p, idx, ins, arg: arg0 + extra })
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// 槽式 / 符号名形式:线圈看 P/N/S/R 标记块
|
|
145
|
+
const mk = markBefore(p)
|
|
146
|
+
const coil = mk[0] && mk[0].ch === 'S' ? 'S' : mk[0] && mk[0].ch === 'R' ? 'R' : null
|
|
147
|
+
if (coil) {
|
|
148
|
+
const edge = mk[1] && mk[1].ch === 'P' ? 'EU' : mk[1] && mk[1].ch === 'N' ? 'ED' : null
|
|
149
|
+
const name = arg0 || (y === 2 ? symbolOf(p) : null)
|
|
150
|
+
if (name) {
|
|
151
|
+
if (edge) recs.push({ p, idx: -1, ins: edge, arg: '', slot: true })
|
|
152
|
+
recs.push({ p, idx: -1, ins: coil, arg: name, slot: true, fromMark: true })
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (y === 2) {
|
|
157
|
+
// 符号名形式(触点):只在槽式网络里(附近有槽式线圈)才算指令,否则是副本
|
|
158
|
+
if (!coilAt.some(c => Math.abs(c - p) < 0x400)) continue
|
|
159
|
+
const arg = arg0 || symbolOf(p)
|
|
160
|
+
if (!arg) continue
|
|
161
|
+
const tail = d.subarray(p + 18, p + 96)
|
|
162
|
+
let code = null
|
|
163
|
+
for (let i = 0; i < tail.length - 4; i++) {
|
|
164
|
+
if (tail[i + 1] === 0x03 && tail[i + 2] === 0x01 && tail[i] >= 1 && tail[i] <= 3) { code = tail[i]; break }
|
|
165
|
+
}
|
|
166
|
+
const ins = code === 0x01 ? 'LD' : code === 0x02 ? 'A' : code === 0x03 ? 'O' : null
|
|
167
|
+
if (ins) recs.push({ p, idx: -1, ins, arg, slot: true, sym: true })
|
|
168
|
+
}
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return recs
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const BLOCKMARK = /^[\u4e00-\u9fa5][\u4e00-\u9fa5A-Za-z0-9_]{1,39}$/
|
|
176
|
+
function findBlocks() {
|
|
177
|
+
const marks = []
|
|
178
|
+
for (let i = 0; i < d.length - 6;) {
|
|
179
|
+
if (d[i] === 0x0d && d[i + 1] === 0x00) {
|
|
180
|
+
const ln = d[i + 2] | (d[i + 3] << 8)
|
|
181
|
+
if (ln > 0 && ln < 40 && i + 4 + ln <= d.length) {
|
|
182
|
+
const nm = gbk.decode(d.subarray(i + 4, i + 4 + ln)).trim()
|
|
183
|
+
if (BLOCKMARK.test(nm)) marks.push({ off: i, name: nm })
|
|
184
|
+
i += 4 + ln; continue
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
i += 1
|
|
188
|
+
}
|
|
189
|
+
return marks
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const blocks = findBlocks()
|
|
193
|
+
if (!blocks.length) { console.log('(未识别到块标记)'); process.exit(0) }
|
|
194
|
+
let out = []
|
|
195
|
+
for (let k = 0; k < blocks.length; k++) {
|
|
196
|
+
const b = blocks[k]
|
|
197
|
+
if (wantBlock && b.name !== wantBlock) continue
|
|
198
|
+
const end = k + 1 < blocks.length ? blocks[k + 1].off : d.length
|
|
199
|
+
const recs = extract(b.off, end)
|
|
200
|
+
if (!recs.length) continue
|
|
201
|
+
// 槽式/符号副本去重:与附近(±0x1000)"地址形式指令"完全相同的,视为存档副本
|
|
202
|
+
const realRecs = recs.filter(r => !r.slot)
|
|
203
|
+
const dropSet = new Set()
|
|
204
|
+
recs.forEach((r, i) => {
|
|
205
|
+
if (!r.slot || r.marker) return
|
|
206
|
+
const raw = r.arg.startsWith('"') ? (sym.get(r.arg.slice(1, -1)) || r.arg) : r.arg.split(',')[0].trim()
|
|
207
|
+
const win = /^(S|R|=)$/.test(r.ins) ? 0x3000 : 0x500
|
|
208
|
+
if (realRecs.some(o => o.ins === r.ins && o.arg.split(',')[0].trim() === raw && Math.abs(o.p - r.p) < win)) dropSet.add(i)
|
|
209
|
+
})
|
|
210
|
+
// 线圈被当副本丢掉时,它前面那条边沿也要一起丢
|
|
211
|
+
recs.forEach((r, i) => { if ((r.ins === 'EU' || r.ins === 'ED') && dropSet.has(i + 1)) dropSet.add(i) })
|
|
212
|
+
const recs2 = recs.filter((r, i) => !dropSet.has(i))
|
|
213
|
+
// 按 idx 回退切网络(槽式条目 idx=-1 不参与边界判定,只按文件顺序落在当前位置)
|
|
214
|
+
const nets = []; let cn = [], last = -1
|
|
215
|
+
const keep = []
|
|
216
|
+
for (const r of recs2) {
|
|
217
|
+
// 紧跟的"同操作数 + 同指令"副本(存档第二份)不算指令
|
|
218
|
+
const prev = keep[keep.length - 1]
|
|
219
|
+
if (r.slot && prev && prev.arg === r.arg && prev.ins === r.ins) continue
|
|
220
|
+
keep.push(r)
|
|
221
|
+
}
|
|
222
|
+
for (let i = 0; i < keep.length; i++) {
|
|
223
|
+
const r = keep[i], p = keep[i - 1]
|
|
224
|
+
if (!r.slot) {
|
|
225
|
+
// 打包网络:序号回退,或刚从槽式段出来 → 新网络
|
|
226
|
+
if (r.idx <= last || (p && p.slot)) { nets.push(cn); cn = []; last = -1 }
|
|
227
|
+
last = r.idx
|
|
228
|
+
} else if (p && !p.slot && keep[i + 1] && keep[i + 1].slot && keep[i + 2] && keep[i + 2].slot && keep[i + 3] && keep[i + 3].slot) {
|
|
229
|
+
// 打包 → 槽式(连续三条以上才算一个槽式网络):槽式网络自成一格
|
|
230
|
+
nets.push(cn); cn = []; last = -1
|
|
231
|
+
}
|
|
232
|
+
cn.push(r)
|
|
233
|
+
}
|
|
234
|
+
if (cn.length) nets.push(cn)
|
|
235
|
+
// 组内二次去重:打包网络里残留的槽式副本(同组内已有同指令同地址的真身)
|
|
236
|
+
for (const n of nets) {
|
|
237
|
+
const real = n.filter(r => !r.slot)
|
|
238
|
+
if (!real.length) continue
|
|
239
|
+
for (let i = n.length - 1; i >= 0; i--) {
|
|
240
|
+
const r = n[i]
|
|
241
|
+
if (!r.slot || r.marker) continue
|
|
242
|
+
const raw = r.arg.startsWith('"') ? (sym.get(r.arg.slice(1, -1)) || r.arg) : r.arg.split(',')[0].trim()
|
|
243
|
+
if (real.some(o => o.ins === r.ins && o.arg.split(',')[0].trim() === raw)) n.splice(i, 1)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// 分支标记(LRD)只在"槽式网络"里成图:该网络含标记块定类型的线圈
|
|
247
|
+
for (const n of nets) {
|
|
248
|
+
const isSlot = n.some(r => r.fromMark)
|
|
249
|
+
if (!isSlot) {
|
|
250
|
+
for (let i = n.length - 1; i >= 0; i--) if (n[i].marker) n.splice(i, 1)
|
|
251
|
+
continue
|
|
252
|
+
}
|
|
253
|
+
const firstCoil = n.findIndex(r => r.fromMark)
|
|
254
|
+
// 第一条支路之前的 LRD 无意义,丢掉;再补 LPS / LPP
|
|
255
|
+
for (let i = firstCoil - 1; i >= 0; i--) if (n[i].marker) n.splice(i, 1)
|
|
256
|
+
for (const r of n) if (r.marker) r.ins = 'LRD'
|
|
257
|
+
const idx = n.findIndex(r => r.fromMark)
|
|
258
|
+
const at = idx > 0 && (n[idx - 1].ins === 'EU' || n[idx - 1].ins === 'ED') ? idx - 1 : idx
|
|
259
|
+
n.splice(at, 0, { ins: 'LPS', arg: '', slot: true })
|
|
260
|
+
// LPP 落在最后一条支路之前(末支路不属于并联块)
|
|
261
|
+
const k = n.length
|
|
262
|
+
if (k >= 2 && (n[k - 2].ins === 'EU' || n[k - 2].ins === 'ED') && /^(S|R|=)$/.test(n[k - 1].ins)) {
|
|
263
|
+
if (n[k - 3] && n[k - 3].marker) n.splice(k - 3, 1) // 末支路的标记不是 LRD(它属于 LPP 之后那条)
|
|
264
|
+
const kk = n.length
|
|
265
|
+
n.splice(kk - 2, 0, { ins: 'LPP', arg: '', slot: true })
|
|
266
|
+
} else n.push({ ins: 'LPP', arg: '', slot: true })
|
|
267
|
+
}
|
|
268
|
+
out.push(`## 块:${b.name}`)
|
|
269
|
+
nets.forEach((n, i) => {
|
|
270
|
+
if (wantNet && String(i + 1) !== wantNet) return
|
|
271
|
+
out.push(`Network ${i + 1}`)
|
|
272
|
+
// 注意:齐序号缺口:那些位次是指令盒(盒有独立记录,不产生元件行)
|
|
273
|
+
const idxs = n.filter(r => !r.slot && r.idx >= 0).map(r => r.idx)
|
|
274
|
+
const lo = idxs.length ? Math.min(...idxs) : -1
|
|
275
|
+
const hi = idxs.length ? Math.max(...idxs) : -1
|
|
276
|
+
const have = new Set(idxs)
|
|
277
|
+
let shown = []
|
|
278
|
+
for (const r of n) {
|
|
279
|
+
if (!r.slot && r.idx >= 0) shown.push({ at: r.idx, line: `${r.ins.padEnd(6)} ${r.arg}`.trimEnd() })
|
|
280
|
+
else shown.push({ at: -1, line: `${r.ins.padEnd(6)} ${r.arg}`.trimEnd() })
|
|
281
|
+
}
|
|
282
|
+
// 在同一网络内按位次插入缺口的占位行(槽式行保持原位)
|
|
283
|
+
const outLines = []
|
|
284
|
+
let k = 0
|
|
285
|
+
for (let t = lo; t >= 0 && t <= hi; t++) {
|
|
286
|
+
while (k < shown.length && shown[k].at === -1) { outLines.push(shown[k].line); k++ }
|
|
287
|
+
if (!have.has(t)) { outLines.push(`[盒] ← 位次 ${t}(指令盒记录,未列元件行)`); continue }
|
|
288
|
+
if (k < shown.length && shown[k].at === t) { outLines.push(shown[k].line); k++ }
|
|
289
|
+
}
|
|
290
|
+
while (k < shown.length) { outLines.push(shown[k].line); k++ }
|
|
291
|
+
out.push(...outLines)
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
console.log([
|
|
295
|
+
'# 说明:本表是 LAD 指令序列(= 串/并联结构):LD/A/O/AN/ON 触点、EU/ED 边沿、LPS/LRD/LPP 并联支路、=/S/R 线圈、LDW>/OW< 等比较(带第二操作数)。',
|
|
296
|
+
'# 指令盒(含库调用 CALL)不产生元件行:网络内位次缺号处标 [盒];盒的实参用 smart-box-pins 或软件里的 CALL 行读。',
|
|
297
|
+
'',
|
|
298
|
+
...out,
|
|
299
|
+
].join('\n'))
|