@tsplc/debug 0.2.17 → 0.2.19
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/dist/format.js +36 -18
- package/dist/format.js.map +4 -4
- package/dist/index.js +768 -516
- package/dist/index.js.map +20 -17
- package/dist/manifest.js +6 -2
- package/dist/manifest.js.map +3 -3
- package/dist/paths.js +60 -28
- package/dist/paths.js.map +3 -3
- package/package.json +47 -46
- package/src/adapter.ts +43 -36
- package/src/api.ts +126 -123
- package/src/debugger.ts +584 -467
- package/src/errors.ts +3 -0
- package/src/hardware.ts +127 -96
- package/src/http.ts +147 -116
- package/src/index.ts +34 -4
- package/src/jobs.ts +102 -77
- package/src/login-value.ts +165 -0
- package/src/login.ts +233 -253
- package/src/manifest.ts +200 -194
- package/src/opcua.ts +55 -55
- package/src/paths.ts +230 -157
- package/src/requests.ts +86 -0
- package/src/telemetry.ts +104 -90
- package/src/timeline.ts +96 -61
- package/src/trace.ts +41 -31
- package/src/ui/bits.ts +45 -29
- package/src/ui/client.ts +33 -26
- package/src/ui/dom.ts +189 -108
- package/src/ui/format.ts +127 -72
- package/src/ui/hardware.ts +416 -343
- package/src/ui/header.ts +186 -164
- package/src/ui/index.ts +16 -9
- package/src/ui/jobs.ts +263 -214
- package/src/ui/main.ts +52 -48
- package/src/ui/overview.ts +152 -108
- package/src/ui/recent.ts +329 -276
- package/src/ui/requests.ts +69 -73
- package/src/ui/store.ts +236 -198
- package/src/ui/tree.ts +162 -130
- package/src/ui/variables.ts +557 -431
- package/src/watch.ts +22 -17
- package/src/ui/dev.ts +0 -579
- package/src/ui/fixture.ts +0 -548
package/src/jobs.ts
CHANGED
|
@@ -6,22 +6,25 @@ const scalar = (iec: string): DebugType => ({ kind: "scalar", iec });
|
|
|
6
6
|
|
|
7
7
|
/** What the trace said of a job: whether it is running, and when it last started. */
|
|
8
8
|
export interface TracedJob {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
busy: boolean;
|
|
10
|
+
startedAt?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/** The job's `busy` flag, read while a page shows the job and the trace says nothing of it. */
|
|
14
14
|
export const busyRead = (job: DebugJob): TargetRead => ({
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
target: `${job.target}.busy`,
|
|
16
|
+
type: scalar("BOOL"),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
/** What a busy job shows: where it waits, since when, and its latest request. */
|
|
20
20
|
export const detailReads = (job: DebugJob): TargetRead[] => [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
{ target: `${job.target}.step`, type: scalar("UINT") },
|
|
22
|
+
{ target: `${job.target}.cancelling`, type: scalar("BOOL") },
|
|
23
|
+
...(job.startedAt ? [{ target: job.startedAt, type: scalar("UDINT") }] : []),
|
|
24
|
+
...job.requests.map(({ sequence }) => ({
|
|
25
|
+
target: sequence,
|
|
26
|
+
type: scalar("UDINT"),
|
|
27
|
+
})),
|
|
25
28
|
];
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -29,16 +32,20 @@ export const detailReads = (job: DebugJob): TargetRead[] => [
|
|
|
29
32
|
* stops running it once it finishes, so the trace can miss the finish.
|
|
30
33
|
*/
|
|
31
34
|
export const busyOf = (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
job: DebugJob,
|
|
36
|
+
value: (target: string) => TargetSample | undefined,
|
|
37
|
+
traced: TracedJob | undefined,
|
|
35
38
|
) => {
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
const read = value(busyRead(job).target);
|
|
40
|
+
return read?.quality === "good"
|
|
41
|
+
? read.value === true
|
|
42
|
+
: (traced?.busy ?? null);
|
|
38
43
|
};
|
|
39
44
|
|
|
40
45
|
const numberOf = (sample: TargetSample | undefined) =>
|
|
41
|
-
|
|
46
|
+
sample?.quality === "good" && typeof sample.value === "number"
|
|
47
|
+
? sample.value
|
|
48
|
+
: undefined;
|
|
42
49
|
|
|
43
50
|
/**
|
|
44
51
|
* A job as the debugger shows it at host time `now`. Details are shown only while it is busy;
|
|
@@ -46,70 +53,88 @@ const numberOf = (sample: TargetSample | undefined) =>
|
|
|
46
53
|
* clock estimate there is no start time from the PLC's own `startedAt`.
|
|
47
54
|
*/
|
|
48
55
|
export const jobState = (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
job: DebugJob,
|
|
57
|
+
inputs: {
|
|
58
|
+
value: (target: string) => TargetSample | undefined;
|
|
59
|
+
traced: TracedJob | undefined;
|
|
60
|
+
failure: JobState["failure"];
|
|
61
|
+
/** Busy, but its details aren't read for want of room in the poll. */
|
|
62
|
+
unread: boolean;
|
|
63
|
+
clock: Session["clock"];
|
|
64
|
+
now: number;
|
|
65
|
+
/** The server's trace of a call, which the PLC tags with its job and the call site's count. */
|
|
66
|
+
request: (call: {
|
|
67
|
+
job: number;
|
|
68
|
+
service: string;
|
|
69
|
+
sequence: number;
|
|
70
|
+
}) => RequestTrace | undefined;
|
|
71
|
+
},
|
|
61
72
|
): JobState => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
73
|
+
const { value, traced, clock, now } = inputs;
|
|
74
|
+
const busy = busyOf(job, value, traced);
|
|
75
|
+
const startedPlc =
|
|
76
|
+
job.startedAt === undefined ? undefined : numberOf(value(job.startedAt));
|
|
77
|
+
const startedAt =
|
|
78
|
+
traced?.startedAt ??
|
|
79
|
+
(clock && startedPlc !== undefined
|
|
80
|
+
? new Date(startedPlc - clock.offsetMs).toISOString()
|
|
81
|
+
: undefined);
|
|
82
|
+
const base: JobState = {
|
|
83
|
+
id: job.id,
|
|
84
|
+
quality:
|
|
85
|
+
value(busyRead(job).target)?.quality ?? (traced ? "good" : "unavailable"),
|
|
86
|
+
busy,
|
|
87
|
+
...(startedAt && { startedAt }),
|
|
88
|
+
...(inputs.failure && { failure: inputs.failure }),
|
|
89
|
+
};
|
|
90
|
+
if (!busy) {
|
|
91
|
+
return base;
|
|
92
|
+
}
|
|
93
|
+
if (inputs.unread) {
|
|
94
|
+
return { ...base, unread: true };
|
|
95
|
+
}
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
const step = numberOf(value(`${job.target}.step`));
|
|
98
|
+
const current = job.steps.find((candidate) => candidate.step === step);
|
|
99
|
+
// Each call site counts its own requests, so the latest is the one the server saw start last,
|
|
100
|
+
// or the one counted furthest while the server has yet to report them.
|
|
101
|
+
const calls = job.requests
|
|
102
|
+
.map((call) => {
|
|
103
|
+
const sequence = numberOf(value(call.sequence)) ?? 0;
|
|
104
|
+
const trace = inputs.request({
|
|
105
|
+
job: call.job ?? job.id,
|
|
106
|
+
service: call.service,
|
|
107
|
+
sequence,
|
|
108
|
+
});
|
|
109
|
+
return { service: call.service, sequence, trace };
|
|
110
|
+
})
|
|
111
|
+
.filter(({ sequence }) => sequence > 0);
|
|
112
|
+
const later = (call: (typeof calls)[number], than: (typeof calls)[number]) =>
|
|
113
|
+
call.trace && than.trace
|
|
114
|
+
? call.trace.startedAt > than.trace.startedAt
|
|
115
|
+
: call.sequence > than.sequence;
|
|
116
|
+
const latest = calls.reduce<(typeof calls)[number] | undefined>(
|
|
117
|
+
(latest, call) => (latest && !later(call, latest) ? latest : call),
|
|
118
|
+
undefined,
|
|
119
|
+
);
|
|
120
|
+
const cancelling = value(`${job.target}.cancelling`);
|
|
100
121
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
...(cancelling?.quality === "good" && {
|
|
125
|
+
cancelling: cancelling.value === true,
|
|
126
|
+
}),
|
|
127
|
+
...(step !== undefined && { step }),
|
|
128
|
+
...(current && {
|
|
129
|
+
current: { kind: current.kind, range: current.range, text: current.text },
|
|
130
|
+
}),
|
|
131
|
+
...(startedAt && { elapsedMs: Math.max(0, now - Date.parse(startedAt)) }),
|
|
132
|
+
...(latest && {
|
|
133
|
+
request: {
|
|
134
|
+
service: latest.service,
|
|
135
|
+
sequence: latest.sequence,
|
|
136
|
+
...(latest.trace && { trace: latest.trace }),
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
115
140
|
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { elementaryType } from "@tsplc/iec";
|
|
2
|
+
import type { DebugType } from "./manifest.ts";
|
|
3
|
+
|
|
4
|
+
/** CODESYS's `TRUE`/`FALSE`, and the digits a BOOL may also read as. */
|
|
5
|
+
const booleans: Record<string, boolean> = {
|
|
6
|
+
TRUE: true,
|
|
7
|
+
FALSE: false,
|
|
8
|
+
"1": true,
|
|
9
|
+
"0": false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const radixPrefixes: Record<string, string> = { 2: "0b", 8: "0o", 16: "0x" };
|
|
13
|
+
|
|
14
|
+
const timeUnits: Record<string, number> = {
|
|
15
|
+
d: 86_400_000,
|
|
16
|
+
h: 3_600_000,
|
|
17
|
+
m: 60_000,
|
|
18
|
+
s: 1000,
|
|
19
|
+
ms: 1,
|
|
20
|
+
us: 0.001,
|
|
21
|
+
ns: 0.000_001,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** IEC's `$` escapes by letter; `$$`, `$'` and `$"` stand for themselves. */
|
|
25
|
+
const stringEscapes: Record<string, string> = {
|
|
26
|
+
l: "\n",
|
|
27
|
+
n: "\n",
|
|
28
|
+
p: "\f",
|
|
29
|
+
r: "\r",
|
|
30
|
+
t: "\t",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Typed literals such as `INT#5`, without their type; TIME's own `T#` prefix is kept. */
|
|
34
|
+
const withoutTypePrefix = (text: string) =>
|
|
35
|
+
text.replace(/^(?!L?T(?:IME)?#)[a-z_]+#(?=.)/i, "");
|
|
36
|
+
|
|
37
|
+
/** `16#FF`, `2#1010`, `-42`, with IEC's digit separators, as a BigInt literal. */
|
|
38
|
+
const bigIntLiteral = (text: string) => {
|
|
39
|
+
const [, radix, digits] = /^(2|8|16)#([0-9a-f_]+)$/i.exec(text) ?? [];
|
|
40
|
+
if (radix !== undefined && digits !== undefined) {
|
|
41
|
+
return `${radixPrefixes[radix]}${digits.replaceAll("_", "")}`;
|
|
42
|
+
}
|
|
43
|
+
const decimal = text.replaceAll("_", "");
|
|
44
|
+
return /^[+-]?\d+$/.test(decimal) ? decimal : undefined;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** An integer literal; beyond doubles' exact range, a string. */
|
|
48
|
+
const integer = (text: string) => {
|
|
49
|
+
const literal = bigIntLiteral(text);
|
|
50
|
+
if (literal === undefined) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const value = BigInt(literal);
|
|
54
|
+
return Number.isSafeInteger(Number(value)) ? Number(value) : String(value);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const real = (text: string) => {
|
|
58
|
+
const number = Number(text);
|
|
59
|
+
return text !== "" && !Number.isNaN(number) ? number : undefined;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** `T#1h2m3s4ms`, `TIME#-1.5s`, `LTIME#5us`, in milliseconds. */
|
|
63
|
+
const duration = (text: string) => {
|
|
64
|
+
const [, sign, body] = /^L?T(?:IME)?#(-?)(.+)$/i.exec(text) ?? [];
|
|
65
|
+
if (body === undefined) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const units = body.replaceAll("_", "");
|
|
69
|
+
const parts = [...units.matchAll(/(\d+(?:\.\d+)?)(ms|us|ns|d|h|m|s)/gi)];
|
|
70
|
+
if (parts.map(([part]) => part).join("") !== units) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const ms = parts.reduce(
|
|
74
|
+
(total, [, amount, unit]) =>
|
|
75
|
+
total + Number(amount) * timeUnits[unit!.toLowerCase()]!,
|
|
76
|
+
0,
|
|
77
|
+
);
|
|
78
|
+
return sign ? -ms : ms;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** `'it$'s'` or `"wide"`, with IEC's `$` escapes. */
|
|
82
|
+
const quoted = (text: string) => {
|
|
83
|
+
const [, , body] = /^(['"])(.*)\1$/s.exec(text) ?? [];
|
|
84
|
+
return body?.replace(/\$([0-9a-f]{2}|[$'"lnprt])/gi, (_, code: string) =>
|
|
85
|
+
code.length === 2
|
|
86
|
+
? String.fromCharCode(parseInt(code, 16))
|
|
87
|
+
: (stringEscapes[code.toLowerCase()] ?? code),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** A member's number, or the name the login gave when the manifest doesn't list it. */
|
|
92
|
+
const enumValue = (
|
|
93
|
+
type: Extract<DebugType, { kind: "enum" }>,
|
|
94
|
+
text: string,
|
|
95
|
+
) => {
|
|
96
|
+
const number = integer(text);
|
|
97
|
+
if (typeof number === "number") {
|
|
98
|
+
return number;
|
|
99
|
+
}
|
|
100
|
+
// `E_Bin.Present`, `Present`, or a member the manifest lacks with its number: `Missing (7)`.
|
|
101
|
+
const [, name, value] =
|
|
102
|
+
/^(?:\w+\.)*(\w+)(?:\s*\((-?\d+)\))?$/.exec(text) ?? [];
|
|
103
|
+
if (name === undefined) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return type.members[name] ?? (value === undefined ? name : Number(value));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Only raw assembly bytes are read whole; their elements are scalars named by IEC type. */
|
|
110
|
+
const arrayValue = (
|
|
111
|
+
type: Extract<DebugType, { kind: "array" }>,
|
|
112
|
+
text: string,
|
|
113
|
+
) => {
|
|
114
|
+
const items = text.replace(/^\[/, "").replace(/\]$/, "").trim();
|
|
115
|
+
if (!items) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const values = items
|
|
119
|
+
.split(/\s*,\s*/)
|
|
120
|
+
.map((item) => scalarValue(type.element, item));
|
|
121
|
+
return values.length === type.length &&
|
|
122
|
+
values.every((value) => value !== undefined)
|
|
123
|
+
? values
|
|
124
|
+
: undefined;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const scalarValue = (iec: string, text: string): unknown => {
|
|
128
|
+
const untyped = withoutTypePrefix(text);
|
|
129
|
+
const name = iec.toUpperCase();
|
|
130
|
+
switch (elementaryType(name)?.family) {
|
|
131
|
+
case "bool":
|
|
132
|
+
return booleans[untyped.toUpperCase()];
|
|
133
|
+
case "integer":
|
|
134
|
+
case "bits":
|
|
135
|
+
return integer(untyped);
|
|
136
|
+
case "real":
|
|
137
|
+
return real(untyped);
|
|
138
|
+
case "duration":
|
|
139
|
+
return duration(text);
|
|
140
|
+
}
|
|
141
|
+
if (/^W?STRING\b/.test(name)) {
|
|
142
|
+
return quoted(text) ?? text;
|
|
143
|
+
}
|
|
144
|
+
return text;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A value as `read_value` returns it, in the form the debugger decodes: BOOL as a boolean,
|
|
149
|
+
* numbers and TIME (in ms) as numbers, enums as their member's number (or its name when the
|
|
150
|
+
* manifest doesn't list it), STRING unquoted, and arrays read whole as arrays. Undefined when the
|
|
151
|
+
* text doesn't fit the type.
|
|
152
|
+
*/
|
|
153
|
+
export const parseLoginValue = (type: DebugType, text: string): unknown => {
|
|
154
|
+
const trimmed = text.trim();
|
|
155
|
+
switch (type.kind) {
|
|
156
|
+
case "enum":
|
|
157
|
+
return enumValue(type, withoutTypePrefix(trimmed));
|
|
158
|
+
case "array":
|
|
159
|
+
return arrayValue(type, withoutTypePrefix(trimmed));
|
|
160
|
+
case "record":
|
|
161
|
+
return undefined;
|
|
162
|
+
case "scalar":
|
|
163
|
+
return scalarValue(type.iec, trimmed);
|
|
164
|
+
}
|
|
165
|
+
};
|