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,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* smart-net-trace.mjs —— S7-200 SMART .smart 工程「网络级轨迹」提取
|
|
4
|
+
*
|
|
5
|
+
* 与既有工具的分工:
|
|
6
|
+
* smart-ladder-elements.mjs 只给「触点/写元件」(判读/写)
|
|
7
|
+
* smart-pin-dump.mjs 只给「地址形式操作数」(漏符号名形式)
|
|
8
|
+
* smart-block-instseq.mjs 只给「ASCII 指令名 + 符号引用」
|
|
9
|
+
* 本工具把三者合到同一偏移轴:按偏移顺序输出 触点 / 写元件 / 指令盒(引脚名 + 操作数 + 常数),
|
|
10
|
+
* 用于把一段块还原成「网络 = 触点序列 + 盒序列」的可读轨迹(不含串并联拓扑——那是未解盲区)。
|
|
11
|
+
*
|
|
12
|
+
* 记录结构(字节验证):
|
|
13
|
+
* 通用头 9B:01 01 XX 00 00 00 YY 03 01
|
|
14
|
+
* XX=01 触点(读) / XX=02 写元件(线圈、带 OUT 的盒、S-R 线圈) / XX=00 指令盒头
|
|
15
|
+
* YY=01 头后 9B 实例 + 11B 地址操作数;YY=02 头后 9B 实例 + 符号名记录 01 00 01 00 <len> <GBK>
|
|
16
|
+
* 调用盒:头 9B + E9 03 <xx> 01 00 02 09 03 00 + <块名 GBK>\0
|
|
17
|
+
*
|
|
18
|
+
* 用法:
|
|
19
|
+
* node smart-net-trace.mjs <工程.smart> <起hex> <止hex> [--raw]
|
|
20
|
+
* --raw 同时打印每个盒窗口内的原始 hex(核对引脚归属时用)
|
|
21
|
+
* 输出:每行「偏移 类型 内容」
|
|
22
|
+
*/
|
|
23
|
+
import fs from 'node:fs'
|
|
24
|
+
import zlib from 'node:zlib'
|
|
25
|
+
|
|
26
|
+
const file = process.argv[2]
|
|
27
|
+
const A = parseInt(process.argv[3], 16)
|
|
28
|
+
const B = parseInt(process.argv[4], 16)
|
|
29
|
+
const RAW = process.argv.includes('--raw')
|
|
30
|
+
if (!file || !Number.isFinite(A) || !Number.isFinite(B)) {
|
|
31
|
+
console.error('用法: node smart-net-trace.mjs <工程.smart> <起hex> <止hex> [--raw]')
|
|
32
|
+
process.exit(2)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const buf = fs.readFileSync(file)
|
|
36
|
+
if (buf.readUInt32LE(0) !== 0x00334853) throw new Error('不是 SH3 头的 .smart 工程')
|
|
37
|
+
const len = buf.readUInt32LE(0x6c)
|
|
38
|
+
const doc = zlib.inflateSync(buf.subarray(0x70, 0x70 + len + 4096)).subarray(0, len)
|
|
39
|
+
const dec = new TextDecoder('gb18030')
|
|
40
|
+
|
|
41
|
+
const AREA = { 1: 'I', 2: 'Q', 0x10: 'V', 0x20: 'M', 0x40: 'T', 0x80: 'C', 0x200: 'SM', 0x2000: 'L' }
|
|
42
|
+
const TYPE = { 1: 'bit', 2: 'byte', 4: 'word', 8: 'dword' }
|
|
43
|
+
function addrName(t, area, v) {
|
|
44
|
+
const a = AREA[area]
|
|
45
|
+
if (t === 1) return (area === 0x40 || area === 0x80) ? a + v : a + (v >> 3) + '.' + (v & 7)
|
|
46
|
+
return a + ({ 2: 'B', 4: 'W', 8: 'D' })[t] + v
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ---- 符号表:名字 → 地址(用符号名写的元素回查) ----
|
|
50
|
+
const symByName = new Map()
|
|
51
|
+
for (let i = 0; i < doc.length - 24; i++) {
|
|
52
|
+
if (doc[i] !== 2 || doc[i + 1] !== 0) continue
|
|
53
|
+
const ln = doc.readUInt16LE(i + 2)
|
|
54
|
+
if (ln > 0 && ln < 40 && doc[i + 4 + ln] === 0 && doc[i + 5 + ln] === 1 &&
|
|
55
|
+
doc[i + 6 + ln] === 2 && doc[i + 7 + ln] === 0) {
|
|
56
|
+
const t = doc[i + 8 + ln]
|
|
57
|
+
if (TYPE[t]) {
|
|
58
|
+
const area = doc.readUInt32LE(i + 9 + ln), v = doc.readUInt32LE(i + 13 + ln)
|
|
59
|
+
if (AREA[area]) {
|
|
60
|
+
const nm = dec.decode(doc.subarray(i + 4, i + 4 + ln)).replace(/\0+$/, '')
|
|
61
|
+
if (nm && !symByName.has(nm)) symByName.set(nm, addrName(t, area, v))
|
|
62
|
+
i += 17 + ln
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isHead = (p) => doc[p] === 1 && doc[p + 1] === 1 && doc[p + 3] === 0 && doc[p + 4] === 0 &&
|
|
70
|
+
doc[p + 5] === 0 && doc[p + 7] === 3 && doc[p + 8] === 1
|
|
71
|
+
|
|
72
|
+
// 常数记录(8 字节):01 0x <len 01|02|04> 01 <u32>
|
|
73
|
+
function constAt(p) {
|
|
74
|
+
if (doc[p] !== 1) return null
|
|
75
|
+
const ok1 = doc[p + 1] === 2 && [1, 2, 4].includes(doc[p + 2]) && doc[p + 3] === 1
|
|
76
|
+
const ok2 = doc[p + 1] === 1 && [1, 2, 4].includes(doc[p + 2]) && doc[p + 3] === 1
|
|
77
|
+
if (!ok1 && !ok2) return null
|
|
78
|
+
const v = doc.readUInt32LE(p + 4)
|
|
79
|
+
return v < 100000000 ? v : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 元素尾部端点号:同号=并到同一个端点(并联),各异=串联
|
|
83
|
+
function endpointAfter(p, size) {
|
|
84
|
+
const lim = Math.min(p + size + 40, B)
|
|
85
|
+
for (let e = p + size; e < lim; e++) {
|
|
86
|
+
if (doc[e] !== 3 || doc[e + 1] !== 1) continue
|
|
87
|
+
if (doc[e - 1] === 0 && doc[e - 2] === 0 && doc[e - 3] === 0 && doc[e - 4] === 0) return doc[e - 5]
|
|
88
|
+
return doc[e - 3]
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 元素:XX=01 触点 / XX=02 写元件
|
|
94
|
+
function elementAt(p) {
|
|
95
|
+
if (!isHead(p)) return null
|
|
96
|
+
const kind = doc[p + 2]
|
|
97
|
+
if (kind !== 1 && kind !== 2) return null
|
|
98
|
+
const y = doc[p + 6]
|
|
99
|
+
if (y === 1) {
|
|
100
|
+
if (doc[p + 18] !== 2 || doc[p + 19] !== 0) return null
|
|
101
|
+
const t = doc[p + 20], area = doc.readUInt32LE(p + 21)
|
|
102
|
+
if (!TYPE[t] || !AREA[area]) return null
|
|
103
|
+
const name = addrName(t, area, doc.readUInt32LE(p + 25))
|
|
104
|
+
return { off: p, kind, label: name, size: 29, ep: endpointAfter(p, 29) }
|
|
105
|
+
}
|
|
106
|
+
if (y === 2) {
|
|
107
|
+
if (!(doc[p + 18] === 1 && doc[p + 19] === 0 && doc[p + 20] === 1 && doc[p + 21] === 0)) return null
|
|
108
|
+
const l = doc[p + 22]
|
|
109
|
+
if (!(l > 0 && l < 60)) return null
|
|
110
|
+
const nm = dec.decode(doc.subarray(p + 23, p + 23 + l)).replace(/\0+$/, '')
|
|
111
|
+
if (!/^[\u4e00-\u9fffA-Za-z_0-9]+$/.test(nm)) return null
|
|
112
|
+
const a = symByName.get(nm)
|
|
113
|
+
return { off: p, kind, label: a ? `${nm} (${a})` : nm, size: 23 + l, ep: endpointAfter(p, 23 + l) }
|
|
114
|
+
}
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 盒:XX=00;窗口 = 到下一个「记录起点」为止,窗内收集 指令名/引脚名、操作数、常数、内嵌块名
|
|
119
|
+
function boxAt(p) {
|
|
120
|
+
if (!isHead(p) || doc[p + 2] !== 0) return null
|
|
121
|
+
let end = p + 9
|
|
122
|
+
for (let q = p + 9; q < Math.min(p + 0x140, B); q++) {
|
|
123
|
+
if (q > p + 9 && (isHead(q) || (doc[q] === 0xe9 && doc[q + 1] === 3 && doc[q + 3] === 1 && doc[q + 5] === 2))) { end = q; break }
|
|
124
|
+
end = q
|
|
125
|
+
}
|
|
126
|
+
const win = doc.subarray(p, end)
|
|
127
|
+
const dec2 = new TextDecoder('gb18030')
|
|
128
|
+
const names = []
|
|
129
|
+
for (let i = 0; i < win.length; i++) {
|
|
130
|
+
if (win[i] === 0 && /[A-Za-z]/.test(String.fromCharCode(win[i + 1] || 0))) {
|
|
131
|
+
let q = i + 1
|
|
132
|
+
while (q < win.length && /[A-Za-z0-9_]/.test(String.fromCharCode(win[q]))) q++
|
|
133
|
+
const s = win.subarray(i + 1, q).toString('latin1')
|
|
134
|
+
if (s.length >= 2 && win[q] === 0) names.push({ i, s })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// 内嵌被调块名:E9 03 xx 01 00 02 09 03 00 <GBK>\0
|
|
138
|
+
let callee = null
|
|
139
|
+
for (let i = 0; i + 10 < win.length; i++) {
|
|
140
|
+
if (win[i] === 0xe9 && win[i + 1] === 3 && win[i + 3] === 1 && win[i + 5] === 2 && win[i + 6] === 9 && win[i + 7] === 3 && win[i + 8] === 0) {
|
|
141
|
+
let q = i + 9
|
|
142
|
+
while (q < win.length && win[q] !== 0) q++
|
|
143
|
+
if (win[q] === 0) { callee = dec2.decode(win.subarray(i + 9, q)); break }
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const ops = [], consts = []
|
|
147
|
+
for (let i = 0; i + 11 <= win.length; i++) {
|
|
148
|
+
if (win[i] === 2 && win[i + 1] === 0 && TYPE[win[i + 2]] && AREA[win.readUInt32LE(i + 3)]) {
|
|
149
|
+
ops.push({ i, name: addrName(win[i + 2], win.readUInt32LE(i + 3), win.readUInt32LE(i + 7)) })
|
|
150
|
+
i += 10; continue
|
|
151
|
+
}
|
|
152
|
+
if (win[i] === 1 && win[i + 1] === 2 && win[i + 3] === 1) {
|
|
153
|
+
const v = win.readUInt32LE(i + 4)
|
|
154
|
+
if (v < 1000000) { consts.push({ i, name: 'K=' + v }); i += 6 }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const instr = names.length ? names[0].s : '(?)'
|
|
158
|
+
const pins = names.slice(1).map(n => n.s)
|
|
159
|
+
return { off: p, kind: 0, instr, pins, ops, consts, callee, size: end - p, end, raw: RAW ? win : null }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const rows = []
|
|
163
|
+
for (let p = A; p < B - 30;) {
|
|
164
|
+
const e = elementAt(p)
|
|
165
|
+
if (e) { rows.push(e); p += e.size; continue }
|
|
166
|
+
const c = constAt(p)
|
|
167
|
+
if (c !== null) { rows.push({ off: p, kind: 3, value: c }); p += 8; continue }
|
|
168
|
+
const x = boxAt(p)
|
|
169
|
+
if (x) { rows.push(x); p += Math.max(x.size, 9); continue }
|
|
170
|
+
p++
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 相邻重复元素(同一元件的地址形式 + 符号名形式)标记
|
|
174
|
+
const TAG = { 1: '触点', 2: '写', 3: '常数' }
|
|
175
|
+
for (let i = 0; i < rows.length; i++) {
|
|
176
|
+
const r = rows[i]
|
|
177
|
+
const off = '0x' + r.off.toString(16).padStart(6, '0')
|
|
178
|
+
if (r.kind === 3) {
|
|
179
|
+
console.log(`${off} 常数 ${r.value}`)
|
|
180
|
+
} else if (r.kind === 1 || r.kind === 2) {
|
|
181
|
+
const ep = r.ep === null ? '' : ` <端 ${r.ep}>`
|
|
182
|
+
console.log(`${off} ${TAG[r.kind]} ${r.label}${ep}`)
|
|
183
|
+
} else {
|
|
184
|
+
const head = r.callee ? `调用 ${r.callee}` : r.instr
|
|
185
|
+
const pinTxt = r.pins.length ? '[' + r.pins.join('|') + ']' : ''
|
|
186
|
+
const opTxt = r.ops.map(o => o.name).join(' ')
|
|
187
|
+
const kTxt = r.consts.map(c => c.name).join(' ')
|
|
188
|
+
console.log(`${off} 盒 ${head} ${pinTxt} ${opTxt}${kTxt ? ' ' + kTxt : ''}`)
|
|
189
|
+
if (RAW) {
|
|
190
|
+
for (let q = 0; q < r.raw.length; q += 16) {
|
|
191
|
+
const s = r.raw.subarray(q, q + 16)
|
|
192
|
+
const h = Array.from(s).map(b => b.toString(16).padStart(2, '0')).join(' ')
|
|
193
|
+
const as = Array.from(s).map(b => (b >= 32 && b < 127) ? String.fromCharCode(b) : '.').join('')
|
|
194
|
+
console.log(` +0x${q.toString(16).padStart(3, '0')} ${h.padEnd(48)} ${as}`)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
console.log(`\n(0x${A.toString(16)}~0x${B.toString(16)}:元素 ${rows.filter(r => r.kind).length},盒 ${rows.filter(r => r.kind === 0).length})`)
|
package/cordis.patch.yml
ADDED