poe-svelte-ui-lib 1.72.0 → 1.74.0
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/README.md +1 -1
- package/dist/protocols/crc.js +1 -1
- package/dist/protocols/dictionary.yaml +2 -1
- package/dist/protocols/dictionaryCodes.js +2 -1
- package/dist/protocols/index.d.ts +2 -1
- package/dist/protocols/index.js +4 -2
- package/dist/protocols/xPoeDebug.d.ts +4 -6
- package/dist/protocols/xPoeDebug.js +99 -81
- package/dist/protocols/xPoeDebugView.d.ts +4 -0
- package/dist/protocols/xPoeDebugView.js +107 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ npm install poe-svelte-ui-lib
|
|
|
61
61
|
|
|
62
62
|
**Битовые поля конфигурации** — `bitFieldGet(word, start, end)`/`bitFieldSet(word, start, end, value)`: поле `start..end` 32-битного слова `{GROUP}.Cfg` (Select/Input с `bitMode`) обычной арифметикой, без знаковых 32-битных операций JS — бит 31 разбирается и собирается корректно, результат всегда 0..2³²−1; тест против эталона на BigInt — `tests/bitField.test.mjs`.
|
|
63
63
|
|
|
64
|
-
**Ядро протоколов** — `poe-svelte-ui-lib/protocols` (без Svelte, работает и под Node без Vite): общий для DevCloud, WebApp и SYP-DevTerminal CBOR-кодек (RFC 8949) — `encodeCbor`/`decodeCbor`, `CborEncoder`/`CborDecoder`; `valueToCbor`/`cborToValue` — обычные значения, двоичные данные как `Uint8Array` (WebSocket, хранилище, телеметрия); `jsonToCbor`/`cborToJson` — JSON-совместимые значения, двоичные данные в base64, строки под ключами с `BIN` — байты (поля ввода терминала и отладки). Пакет `poe_cbor_pkt` — indefinite-массив объектов `[[TrgBusID, SrcBusID, Cmd, Payload, CRC16], …]` (CAN, RS485): `encodePacket`/`decodePacket` (разбор с начала буфера — `complete` с длиной, `incomplete` для потока или `garbage`; объекты одному адресату не склеиваются, порядок сохраняется, неверный CRC — флаг `crcOk`), `objectsFor` (свой адрес и broadcast `BROADCAST_BUS_ID = 0xFFFF`), `EXTERNAL_BUS_ID = 0x0000` — `SrcBusID` терминала и отладки, `makeCmd(header, argument, reply = true)`/`splitCmd` — `Cmd`: заголовок в битах 14-15, аргумент в битах 1-13 (0..8191), флаг ответа в бите 0 (как `CMD`/`CMD_NR` прошивки), `packetObjectCrc`; контрольные суммы `crc16Kermit` и `crc8Rohc` — строго побайтные, как в `common.c` прошивок. Байты совпадают с `poe_cbor_pkt.c` прошивки — проверка `npm run TestProtocols` (`tests/protocols.test.mjs`, эталонные пакеты собраны прошивочным кодом).
|
|
64
|
+
**Ядро протоколов** — `poe-svelte-ui-lib/protocols` (без Svelte, работает и под Node без Vite): общий для DevCloud, WebApp и SYP-DevTerminal CBOR-кодек (RFC 8949) — `encodeCbor`/`decodeCbor`, `CborEncoder`/`CborDecoder`; `valueToCbor`/`cborToValue` — обычные значения, двоичные данные как `Uint8Array` (WebSocket, хранилище, телеметрия); `jsonToCbor`/`cborToJson` — JSON-совместимые значения, двоичные данные в base64, строки под ключами с `BIN` — байты (поля ввода терминала и отладки). Пакет `poe_cbor_pkt` — indefinite-массив объектов `[[TrgBusID, SrcBusID, Cmd, Payload, CRC16], …]` (CAN, RS485): `encodePacket`/`decodePacket` (разбор с начала буфера — `complete` с длиной, `incomplete` для потока или `garbage`; объекты одному адресату не склеиваются, порядок сохраняется, неверный CRC — флаг `crcOk`), `objectsFor` (свой адрес и broadcast `BROADCAST_BUS_ID = 0xFFFF`), `EXTERNAL_BUS_ID = 0x0000` — `SrcBusID` терминала и отладки, `makeCmd(header, argument, reply = true)`/`splitCmd` — `Cmd`: заголовок в битах 14-15, аргумент в битах 1-13 (0..8191), флаг ответа в бите 0 (как `CMD`/`CMD_NR` прошивки), `packetObjectCrc`; контрольные суммы `crc16Kermit` и `crc8Rohc` — строго побайтные, как в `common.c` прошивок. Байты совпадают с `poe_cbor_pkt.c` прошивки — проверка `npm run TestProtocols` (`tests/protocols.test.mjs`, эталонные пакеты собраны прошивочным кодом). Протокол xPOE-Debug (UART-консоль `system_console`): definite CBOR-массив `[SrcID, HdrArg, Value, FHS, CRC16]`; `HdrArg` — общее 16-битное `cmd_t.word`, `Value` — один CBOR item, FHS — свободная куча (или `null` в запросе), CRC-16/KERMIT покрывает сырые CBOR-байты первых четырёх элементов. `encodeXPoeDebugFrame` кодирует запрос, `createXPoeDebugParser().push(байты)` разбирает поток кадров и внешний текст (`raw`). CAN/CAN FD через slcan-адаптер (`can_service` прошивки): `canFragments`/`encodeCanPacket` — нарезка пакета на кадры как `CAN_Send` (бит 10 ID — «последний», биты 0..9 — `packetId`, по 8 / 64 байта), `encodeSlcanFrame`, `createCanParser().push(байты)` — разбор кадров slcan со сборкой фрагментов и обрезкой паддинга CAN FD. RS485 — сырой поток пакетов: `createPacketStreamParser().push(байты)` (пауза обрывает недособранное, мусор пропускается по байту). Открытие slcan-адаптера — `slcanOpenCommands(скорость, скоростьДанных?)`. Поле Value команды xPOE-Debug из ввода — `xPoeDebugValueFromText` (hex как есть, JSON → CBOR). Графики отладки — кадр `OK! DebPlot` (`SC_PLOT` прошивки, `DEB_PLOT_ARGUMENT`): `debPlotValues(value)` — пары `[имя, число]` из hex CBOR-карты `VALUE`, `createPlotGroups().update(пары)` раскладывает по группам (префикс до точки) в порядке карты и возвращает обновлённые группы, `data(группа)` — последние значения. Словарь протокола — `HEADER_CODES`, `DEFAULT_ARGUMENT_CODES` (генерируется из мастер-словаря `src/lib/protocols/dictionary.yaml` — `npm run Dictionary`, перед упаковкой автоматически; сам файл поставляется как `poe-svelte-ui-lib/protocols/dictionary.yaml`, `.framework/Dictionary.yaml` прошивок — его справочные копии, сборка прошивок сверяет с ними `api_cmd.h`), `parseArgumentDictionary` (строки `- [Строка, Enum, Число]`), `nameByCode`. Своих копий протоколов и словаря в приложениях не держим: терминал и отладка облака читают и пишут порт сырыми байтами, разбор и сборка — здесь.
|
|
65
65
|
|
|
66
66
|
**Шапка смарт-виджета** (`WidgetHeader`): значок, заголовок и сворачивание слева, «?» в правом краю; правая часть (режимы, «Обновить» и т. п.) стоит в той же строке, а если не помещается — уходит второй строкой под заголовок и там переносится.
|
|
67
67
|
|
package/dist/protocols/crc.js
CHANGED
|
@@ -9,7 +9,7 @@ export const crc16Kermit = (crc, bytes) => {
|
|
|
9
9
|
}
|
|
10
10
|
return crc & 0xffff;
|
|
11
11
|
};
|
|
12
|
-
/* CRC-8/ROHC
|
|
12
|
+
/* CRC-8/ROHC remains available for legacy consumers. System Console frames use CRC-16/KERMIT over CBOR bytes. */
|
|
13
13
|
export const crc8Rohc = (crc, bytes) => {
|
|
14
14
|
for (const byte of bytes) {
|
|
15
15
|
crc ^= byte & 0xff;
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
- [Restart, A_RESTART, 1] # SET: перезагрузка устройства
|
|
10
10
|
- [DefCfg, A_DEF_CONFIG, 2] # SET: сброс конфигурации к заводской
|
|
11
11
|
- [DlFW, A_DOWNLOAD_FIRMWARE, 3] # SET: скачивание прошивки с сервера (http_client)
|
|
12
|
-
- [DebPlot, A_DEB_PLOT, 4] # OK!: значения графиков отладки — CBOR-карта {"Имя": float32} (SC_PLOT)
|
|
13
12
|
- [RstNVS, A_RESET_NVS, 5] # SET: очистка NVS и перезагрузка
|
|
14
13
|
- [LocTime, A_LOCAL_TIME, 6] # GET: локальное время устройства
|
|
15
14
|
- [Update, A_UPDATE, 7] # OK!/ER!: обновление данных в интерфейсе (DevCloud ретранслирует DataStream)
|
|
16
15
|
- [StackInfo, A_STACK_INFO, 8] # GET: снимок куч и стеков задач
|
|
17
16
|
- [BackUp, A_BACKUP, 9] # Резервная копия конфигурации
|
|
18
17
|
- [BindMKey, A_BIND_MOD_KEY, 10] # SET: привязка ключа модуля (ModBindKey)
|
|
18
|
+
- [DebugLog, A_DEBUG_LOG, 11] # OK!: системное сообщение отладки; VALUE — CBOR-массив [уровень, тег, текст]
|
|
19
|
+
- [DebPlot, A_DEBUG_PLOT, 12] # OK!: значения графиков отладки — CBOR-карта {"Имя": float32} (SC_PLOT)
|
|
19
20
|
- [DataStream, A_DATA_STREAM, 15] # OK!: поток данных групп конфигурации (CS_PushGroupStream)
|
|
20
21
|
|
|
21
22
|
# Локальная сеть
|
|
@@ -4,13 +4,14 @@ export const DEFAULT_ARGUMENT_CODES = {
|
|
|
4
4
|
Restart: 1,
|
|
5
5
|
DefCfg: 2,
|
|
6
6
|
DlFW: 3,
|
|
7
|
-
DebPlot: 4,
|
|
8
7
|
RstNVS: 5,
|
|
9
8
|
LocTime: 6,
|
|
10
9
|
Update: 7,
|
|
11
10
|
StackInfo: 8,
|
|
12
11
|
BackUp: 9,
|
|
13
12
|
BindMKey: 10,
|
|
13
|
+
DebugLog: 11,
|
|
14
|
+
DebPlot: 12,
|
|
14
15
|
DataStream: 15,
|
|
15
16
|
APsList: 21,
|
|
16
17
|
FS: 22,
|
|
@@ -7,6 +7,7 @@ export { CborToJson, JsonToCbor, cborToJson, jsonToCbor } from "./cbor/json.js";
|
|
|
7
7
|
export { crc16Kermit, crc8Rohc } from "./crc.js";
|
|
8
8
|
export { BROADCAST_BUS_ID, makeCmd, splitCmd, EXTERNAL_BUS_ID, packetObjectCrc, encodePacket, decodePacket, objectsFor, createPacketStreamParser, type PacketObject, type DecodedPacketObject, type PacketParse, } from "./packet.js";
|
|
9
9
|
export { createPlotGroups, debPlotValues, DEB_PLOT_ARGUMENT, type PlotPoint } from "./plotGroups.js";
|
|
10
|
-
export { encodeXPoeDebugFrame, createXPoeDebugParser, xPoeDebugValueFromText, type XPoeDebugItem } from "./xPoeDebug.js";
|
|
10
|
+
export { encodeXPoeDebugFrame, createXPoeDebugParser, xPoeDebugCmd, xPoeDebugValueFromText, type XPoeDebugItem } from "./xPoeDebug.js";
|
|
11
|
+
export { formatXPoeDebugValue, formatXPoeDebugValueForTable, formatXPoeDebugModal, bindXPoeDebugCopyButtons } from "./xPoeDebugView.js";
|
|
11
12
|
export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser, slcanOpenCommands, type CanFrame, type CanMessage, } from "./can.js";
|
|
12
13
|
export { HEADER_CODES, DEFAULT_ARGUMENT_CODES, parseArgumentDictionary, nameByCode } from "./dictionary.js";
|
package/dist/protocols/index.js
CHANGED
|
@@ -14,8 +14,10 @@ export { crc16Kermit, crc8Rohc } from "./crc.js";
|
|
|
14
14
|
export { BROADCAST_BUS_ID, makeCmd, splitCmd, EXTERNAL_BUS_ID, packetObjectCrc, encodePacket, decodePacket, objectsFor, createPacketStreamParser, } from "./packet.js";
|
|
15
15
|
/* Графики отладки: кадр OK! DebPlot (SC_PLOT прошивки) — значения из CBOR-карты, график на группу, серии в порядке устройства */
|
|
16
16
|
export { createPlotGroups, debPlotValues, DEB_PLOT_ARGUMENT } from "./plotGroups.js";
|
|
17
|
-
/*
|
|
18
|
-
export { encodeXPoeDebugFrame, createXPoeDebugParser, xPoeDebugValueFromText } from "./xPoeDebug.js";
|
|
17
|
+
/* System Console over UART: CBOR [SrcID, HdrArg, Value, FHS, CRC16], plus stream parsing */
|
|
18
|
+
export { encodeXPoeDebugFrame, createXPoeDebugParser, xPoeDebugCmd, xPoeDebugValueFromText } from "./xPoeDebug.js";
|
|
19
|
+
/* Отображение Value xPOE-Debug в таблице/модалке и копирование Value/HEX */
|
|
20
|
+
export { formatXPoeDebugValue, formatXPoeDebugValueForTable, formatXPoeDebugModal, bindXPoeDebugCopyButtons } from "./xPoeDebugView.js";
|
|
19
21
|
/* CAN/CAN FD через slcan-адаптер: нарезка пакета на кадры как в can_service прошивки, кадры slcan, потоковый разбор со сборкой фрагментов */
|
|
20
22
|
export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser, slcanOpenCommands, } from "./can.js";
|
|
21
23
|
/* Словарь заголовков и аргументов Cmd — по умолчанию из мастер-словаря dictionary.yaml библиотеки, разбор файла словаря */
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
export declare const SOH = 1;
|
|
2
|
-
export declare const STX = 2;
|
|
3
|
-
export declare const ETX = 3;
|
|
4
|
-
export declare const EOT = 4;
|
|
5
|
-
export declare const US = 31;
|
|
6
1
|
export type XPoeDebugItem = {
|
|
7
2
|
type: "frame";
|
|
3
|
+
srcId: number;
|
|
4
|
+
cmd: number;
|
|
8
5
|
header: string;
|
|
9
6
|
argument: string;
|
|
10
7
|
value: string;
|
|
@@ -16,7 +13,8 @@ export type XPoeDebugItem = {
|
|
|
16
13
|
type: "raw";
|
|
17
14
|
text: string;
|
|
18
15
|
};
|
|
19
|
-
export declare const
|
|
16
|
+
export declare const xPoeDebugCmd: (header: string, argument: string) => number;
|
|
17
|
+
export declare const encodeXPoeDebugFrame: (header: string, argument: string, value: Uint8Array, srcId?: number, freeHeap?: number | null) => Uint8Array;
|
|
20
18
|
export declare const createXPoeDebugParser: () => {
|
|
21
19
|
push(chunk: Uint8Array): XPoeDebugItem[];
|
|
22
20
|
reset(): void;
|
|
@@ -1,59 +1,83 @@
|
|
|
1
|
-
/*
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
1
|
+
/* System Console: CBOR array [SrcID, HdrArg, Value, FHS, CRC16]. CRC-16/KERMIT covers the encoded
|
|
2
|
+
first four array items, including the array header. UART is a stream; the definite array is self-delimiting. */
|
|
3
|
+
import { CborDecoder } from "./cbor/decoder.js";
|
|
4
|
+
import { CborEndOfDataError } from "./cbor/types.js";
|
|
5
|
+
import { crc16Kermit } from "./crc.js";
|
|
6
6
|
import { jsonToCbor } from "./cbor/json.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export const ETX = 0x03;
|
|
10
|
-
export const EOT = 0x04;
|
|
11
|
-
export const US = 0x1f;
|
|
12
|
-
const LF = 0x0a;
|
|
13
|
-
/* Предел текста без конца строки и незавершённого кадра — дальше отдаётся/сбрасывается, чтобы буфер не рос бесконечно */
|
|
7
|
+
import { DEFAULT_ARGUMENT_CODES, HEADER_CODES, nameByCode } from "./dictionary.js";
|
|
8
|
+
import { makeCmd, splitCmd } from "./packet.js";
|
|
14
9
|
const MAX_TEXT_TAIL = 4096;
|
|
15
10
|
const MAX_FRAME = 8192;
|
|
16
|
-
const
|
|
11
|
+
const ARRAY5 = 0x85;
|
|
17
12
|
const decoder = new TextDecoder();
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
const encodeUInt = (input) => {
|
|
14
|
+
const value = BigInt(input);
|
|
15
|
+
if (value < 24n)
|
|
16
|
+
return [Number(value)];
|
|
17
|
+
if (value <= 0xffn)
|
|
18
|
+
return [0x18, Number(value)];
|
|
19
|
+
if (value <= 0xffffn)
|
|
20
|
+
return [0x19, Number(value >> 8n), Number(value & 0xffn)];
|
|
21
|
+
if (value <= 0xffffffffn)
|
|
22
|
+
return [0x1a, Number(value >> 24n), Number(value >> 16n & 0xffn), Number(value >> 8n & 0xffn), Number(value & 0xffn)];
|
|
23
|
+
throw new Error("xPOE-Debug integer is out of range");
|
|
25
24
|
};
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/*
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
25
|
+
export const xPoeDebugCmd = (header, argument) => {
|
|
26
|
+
const hdr = HEADER_CODES[header];
|
|
27
|
+
const arg = DEFAULT_ARGUMENT_CODES[argument];
|
|
28
|
+
if (hdr === undefined || arg === undefined)
|
|
29
|
+
throw new Error(`Hdr/Arg отсутствует в общем словаре: ${header} ${argument}`);
|
|
30
|
+
return makeCmd(hdr, arg);
|
|
31
|
+
};
|
|
32
|
+
/* Команда терминала: CRC считается по тем CBOR-байтам, которые реально отправляются. */
|
|
33
|
+
export const encodeXPoeDebugFrame = (header, argument, value, srcId = 0, freeHeap = null) => {
|
|
34
|
+
const cmd = xPoeDebugCmd(header, argument);
|
|
35
|
+
if (!Number.isInteger(srcId) || srcId < 0 || srcId > 0xffff || !Number.isInteger(cmd) || cmd < 0 || cmd > 0xffff)
|
|
36
|
+
throw new Error("SrcID и HdrArg должны быть uint16");
|
|
37
|
+
const valueParser = new CborDecoder(value);
|
|
38
|
+
if (!valueParser.skipValue() || valueParser.offset !== value.length)
|
|
39
|
+
throw new Error("Value должен содержать ровно один CBOR item");
|
|
40
|
+
const prefix = new Uint8Array([ARRAY5, ...encodeUInt(srcId), ...encodeUInt(cmd), ...value, ...(freeHeap === null ? [0xf6] : encodeUInt(freeHeap))]);
|
|
41
|
+
const crc = crc16Kermit(0, prefix);
|
|
42
|
+
return new Uint8Array([...prefix, ...encodeUInt(crc)]);
|
|
43
|
+
};
|
|
44
|
+
const textItems = (bytes) => decoder.decode(bytes).split("\n").map((line) => line.trim()).filter((line) => line && ![...line].some((char) => char.charCodeAt(0) < 0x20 && char !== "\t")).map((text) => ({ type: "raw", text }));
|
|
45
|
+
const readUInt = (value) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff ? value : null;
|
|
46
|
+
/* Декодирует одну definite array из начала буфера; null означает неполный или невалидный кадр. */
|
|
47
|
+
const frameItem = (buffer) => {
|
|
48
|
+
if (buffer[0] !== ARRAY5)
|
|
49
|
+
return { status: "garbage" };
|
|
50
|
+
try {
|
|
51
|
+
const cbor = new CborDecoder(buffer);
|
|
52
|
+
cbor.decode(); /* Проверка структуры и границы всего массива */
|
|
53
|
+
const length = cbor.offset;
|
|
54
|
+
const fields = new CborDecoder(buffer.subarray(1, length));
|
|
55
|
+
const srcId = readUInt(fields.decode());
|
|
56
|
+
const cmd = readUInt(fields.decode());
|
|
57
|
+
if (srcId === null || cmd === null)
|
|
58
|
+
return { status: "garbage" };
|
|
59
|
+
const valueStart = 1 + fields.offset;
|
|
60
|
+
if (!fields.skipValue())
|
|
61
|
+
return { status: "garbage" };
|
|
62
|
+
const valueEnd = 1 + fields.offset;
|
|
63
|
+
const freeHeapValue = fields.decode();
|
|
64
|
+
const crcStart = 1 + fields.offset;
|
|
65
|
+
const crc = readUInt(fields.decode());
|
|
66
|
+
if (fields.offset !== length - 1 || crc === null || (freeHeapValue !== null && (typeof freeHeapValue !== "number" || !Number.isInteger(freeHeapValue) || freeHeapValue < 0)))
|
|
67
|
+
return { status: "garbage" };
|
|
68
|
+
const valueBytes = buffer.slice(valueStart, valueEnd);
|
|
69
|
+
const { header, argument } = splitCmd(cmd);
|
|
70
|
+
return {
|
|
71
|
+
status: "complete",
|
|
72
|
+
length,
|
|
73
|
+
item: { type: "frame", srcId, cmd, header: Object.entries(HEADER_CODES).find(([, code]) => code === header)?.[0] ?? String(header), argument: nameByCode(DEFAULT_ARGUMENT_CODES, argument), value: Array.from(valueBytes, (byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join(""), valueBytes, crc: crc.toString(16).toUpperCase().padStart(4, "0"), crcOk: crc16Kermit(0, buffer.subarray(0, crcStart)) === crc, freeHeap: typeof freeHeapValue === "number" ? String(freeHeapValue) : "" },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof CborEndOfDataError)
|
|
78
|
+
return { status: "incomplete" };
|
|
79
|
+
return { status: "garbage" };
|
|
80
|
+
}
|
|
57
81
|
};
|
|
58
82
|
const concat = (a, b) => {
|
|
59
83
|
const out = new Uint8Array(a.length + b.length);
|
|
@@ -61,59 +85,53 @@ const concat = (a, b) => {
|
|
|
61
85
|
out.set(b, a.length);
|
|
62
86
|
return out;
|
|
63
87
|
};
|
|
64
|
-
/* Потоковый разбор: push(байты из порта) → готовые кадры и строки; незавершённое ждёт следующего чанка */
|
|
65
88
|
export const createXPoeDebugParser = () => {
|
|
66
89
|
let buffer = new Uint8Array();
|
|
67
90
|
return {
|
|
68
91
|
push(chunk) {
|
|
69
92
|
buffer = concat(buffer, chunk);
|
|
70
93
|
const items = [];
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
94
|
+
while (buffer.length) {
|
|
95
|
+
const start = buffer.indexOf(ARRAY5);
|
|
96
|
+
if (start < 0) {
|
|
97
|
+
const cut = buffer.length > MAX_TEXT_TAIL ? buffer.length : buffer.lastIndexOf(0x0a);
|
|
98
|
+
if (cut >= 0) {
|
|
99
|
+
items.push(...textItems(buffer.subarray(0, cut)));
|
|
100
|
+
buffer = buffer.slice(Math.min(cut + 1, buffer.length));
|
|
101
|
+
}
|
|
102
|
+
else if (buffer.length > MAX_TEXT_TAIL)
|
|
103
|
+
buffer = new Uint8Array();
|
|
74
104
|
break;
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
buffer = buffer.slice(soh);
|
|
105
|
+
}
|
|
106
|
+
if (start > 0) {
|
|
107
|
+
items.push(...textItems(buffer.subarray(0, start)));
|
|
108
|
+
buffer = buffer.slice(start);
|
|
80
109
|
continue;
|
|
81
110
|
}
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
/* Кадр без конца слишком длинный — потерян EOT: пропускаем его SOH и ищем следующий кадр, иначе ждём ещё байт */
|
|
111
|
+
const parsed = frameItem(buffer);
|
|
112
|
+
if (parsed.status === "incomplete") {
|
|
85
113
|
if (buffer.length <= MAX_FRAME)
|
|
86
114
|
break;
|
|
87
115
|
buffer = buffer.slice(1);
|
|
88
116
|
continue;
|
|
89
117
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
buffer = buffer.slice(eot + 1);
|
|
94
|
-
}
|
|
95
|
-
/* Хвост без начала кадра: целые строки — наружу, незавершённая строка ждёт следующего чанка */
|
|
96
|
-
if (buffer.indexOf(SOH) < 0) {
|
|
97
|
-
const cut = buffer.length > MAX_TEXT_TAIL ? buffer.length : buffer.lastIndexOf(LF);
|
|
98
|
-
if (cut >= 0) {
|
|
99
|
-
items.push(...textItems(buffer.subarray(0, cut)));
|
|
100
|
-
buffer = buffer.slice(Math.min(cut + 1, buffer.length));
|
|
118
|
+
if (parsed.status === "garbage") {
|
|
119
|
+
buffer = buffer.slice(1);
|
|
120
|
+
continue;
|
|
101
121
|
}
|
|
122
|
+
items.push(parsed.item);
|
|
123
|
+
buffer = buffer.slice(parsed.length);
|
|
102
124
|
}
|
|
103
125
|
return items;
|
|
104
126
|
},
|
|
105
|
-
|
|
106
|
-
reset() {
|
|
107
|
-
buffer = new Uint8Array();
|
|
108
|
-
},
|
|
127
|
+
reset() { buffer = new Uint8Array(); },
|
|
109
128
|
};
|
|
110
129
|
};
|
|
111
|
-
/*
|
|
112
|
-
(двоичные данные — base64, ключи с BIN — байты, см. jsonToCbor); не hex и не JSON — ошибка */
|
|
130
|
+
/* Value form input: ready CBOR hex passes through; JSON is converted to CBOR. */
|
|
113
131
|
export const xPoeDebugValueFromText = (text) => {
|
|
114
132
|
const trimmed = (text ?? "").trim();
|
|
115
133
|
if (!trimmed)
|
|
116
|
-
return new Uint8Array();
|
|
134
|
+
return new Uint8Array([0xf6]);
|
|
117
135
|
if (/^([0-9A-Fa-f]{2}\s?)+$/.test(trimmed))
|
|
118
136
|
return Uint8Array.from(trimmed.replace(/\s/g, "").match(/../g) ?? [], (pair) => parseInt(pair, 16));
|
|
119
137
|
let parsed;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function formatXPoeDebugValue(rawValue: string): string;
|
|
2
|
+
export declare function formatXPoeDebugValueForTable(rawValue: string, isCbor?: boolean): string;
|
|
3
|
+
export declare function bindXPoeDebugCopyButtons(): () => void;
|
|
4
|
+
export declare function formatXPoeDebugModal(text: string): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { cborToValue } from "./cbor/value.js";
|
|
2
|
+
const LOG_LEVELS = { 1: "🔴", 2: "🟡", 3: "🟢", 4: "🔵" };
|
|
3
|
+
const HTML_ESCAPES = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
4
|
+
const escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => HTML_ESCAPES[char]);
|
|
5
|
+
const bytesToHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join(" ");
|
|
6
|
+
const normalizeCborValue = (value) => {
|
|
7
|
+
if (value instanceof Uint8Array)
|
|
8
|
+
return `h'${bytesToHex(value)}'`;
|
|
9
|
+
if (Array.isArray(value))
|
|
10
|
+
return value.map(normalizeCborValue);
|
|
11
|
+
if (value && typeof value === "object")
|
|
12
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeCborValue(item)]));
|
|
13
|
+
return value;
|
|
14
|
+
};
|
|
15
|
+
const compactCborValue = (value) => {
|
|
16
|
+
if (typeof value === "string")
|
|
17
|
+
return escapeHtml(value);
|
|
18
|
+
if (value instanceof Uint8Array)
|
|
19
|
+
return `bytes(${value.length}): ${escapeHtml(bytesToHex(value))}`;
|
|
20
|
+
return escapeHtml(JSON.stringify(normalizeCborValue(value)) ?? String(value));
|
|
21
|
+
};
|
|
22
|
+
/* Декодирует CBOR Value для таблицы; системный лог имеет вид [уровень, тег, текст]. */
|
|
23
|
+
export function formatXPoeDebugValue(rawValue) {
|
|
24
|
+
const hex = rawValue.trim().replace(/\s/g, "");
|
|
25
|
+
if (!hex || hex.length % 2 !== 0 || !/^[0-9A-Fa-f]+$/.test(hex))
|
|
26
|
+
return escapeHtml(rawValue);
|
|
27
|
+
try {
|
|
28
|
+
const value = cborToValue(Uint8Array.from(hex.match(/.{2}/g), (byte) => parseInt(byte, 16)));
|
|
29
|
+
if (Array.isArray(value) &&
|
|
30
|
+
value.length === 3 &&
|
|
31
|
+
typeof value[0] === "number" &&
|
|
32
|
+
LOG_LEVELS[value[0]] &&
|
|
33
|
+
typeof value[1] === "string" &&
|
|
34
|
+
(typeof value[2] === "string" || value[2] instanceof Uint8Array)) {
|
|
35
|
+
const level = LOG_LEVELS[value[0]];
|
|
36
|
+
const detail = compactCborValue(value[2]);
|
|
37
|
+
return `<span class="font-semibold">${level}</span> · ${escapeHtml(value[1])}${detail ? ` · ${detail}` : ""}`;
|
|
38
|
+
}
|
|
39
|
+
return compactCborValue(value);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return escapeHtml(rawValue);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/* Сохраняет исходный HEX скрытым до раскрытия строки в общей таблице. */
|
|
46
|
+
export function formatXPoeDebugValueForTable(rawValue, isCbor = true) {
|
|
47
|
+
if (!isCbor)
|
|
48
|
+
return escapeHtml(rawValue);
|
|
49
|
+
const formatted = formatXPoeDebugValue(rawValue);
|
|
50
|
+
return formatted === escapeHtml(rawValue) ? formatted : `${formatted}<span class="hidden">\nHEX\n${escapeHtml(rawValue)}</span>`;
|
|
51
|
+
}
|
|
52
|
+
const COPY_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 11c0-2.828 0-4.243.879-5.121C7.757 5 9.172 5 12 5h3c2.828 0 4.243 0 5.121.879C21 6.757 21 8.172 21 11v5c0 2.828 0 4.243-.879 5.121C19.243 22 17.828 22 15 22h-3c-2.828 0-4.243 0-5.121-.879C6 20.243 6 18.828 6 16z"/><path d="M6 19a3 3 0 0 1-3-3v-6c0-3.771 0-5.657 1.172-6.828S7.229 2 11 2h4a3 3 0 0 1 3 3"/></g></svg>`;
|
|
53
|
+
const COPIED_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" aria-hidden="true"><path d="m5 12 4 4L19 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
54
|
+
let copyBindings = 0;
|
|
55
|
+
const handleCopyClick = (event) => {
|
|
56
|
+
if (!(event.target instanceof Element))
|
|
57
|
+
return;
|
|
58
|
+
const button = event.target.closest("button[data-xpoe-copy]");
|
|
59
|
+
if (!button)
|
|
60
|
+
return;
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
event.stopPropagation();
|
|
63
|
+
const value = button.closest("section")?.querySelector("pre")?.textContent ?? "";
|
|
64
|
+
const label = button.getAttribute("aria-label") ?? "Копировать";
|
|
65
|
+
navigator.clipboard
|
|
66
|
+
.writeText(value)
|
|
67
|
+
.then(() => {
|
|
68
|
+
button.innerHTML = COPIED_ICON;
|
|
69
|
+
button.setAttribute("aria-label", "Скопировано");
|
|
70
|
+
window.setTimeout(() => {
|
|
71
|
+
button.innerHTML = COPY_ICON;
|
|
72
|
+
button.setAttribute("aria-label", label);
|
|
73
|
+
}, 1300);
|
|
74
|
+
})
|
|
75
|
+
.catch(() => {
|
|
76
|
+
button.setAttribute("aria-label", "Не удалось скопировать");
|
|
77
|
+
window.setTimeout(() => button.setAttribute("aria-label", label), 1300);
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
/* Делегированный обработчик кнопок модального окна; возвращённую функцию вызывают при уничтожении страницы. */
|
|
81
|
+
export function bindXPoeDebugCopyButtons() {
|
|
82
|
+
if (copyBindings++ === 0)
|
|
83
|
+
document.addEventListener("click", handleCopyClick);
|
|
84
|
+
let isBound = true;
|
|
85
|
+
return () => {
|
|
86
|
+
if (!isBound)
|
|
87
|
+
return;
|
|
88
|
+
isBound = false;
|
|
89
|
+
if (--copyBindings === 0)
|
|
90
|
+
document.removeEventListener("click", handleCopyClick);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/* Две прокручиваемые секции модалки; HEX-раздел появляется, если форматтер таблицы добавил исходные байты. */
|
|
94
|
+
export function formatXPoeDebugModal(text) {
|
|
95
|
+
const separator = "\nHEX\n";
|
|
96
|
+
const hexIndex = text.lastIndexOf(separator);
|
|
97
|
+
const decoded = hexIndex < 0 ? text : text.slice(0, hexIndex);
|
|
98
|
+
const hex = hexIndex < 0 ? "" : text.slice(hexIndex + separator.length);
|
|
99
|
+
const modalStyle = "display:flex;flex-direction:column;gap:.75rem;max-height:80vh;min-width:0;overflow-y:auto;overflow-x:hidden";
|
|
100
|
+
const sectionStyle = "min-width:0;padding:.25rem 0 .75rem";
|
|
101
|
+
const headingStyle = "display:flex;align-items:center;justify-content:space-between;gap:.75rem;margin:0 0 .5rem;font-weight:600";
|
|
102
|
+
const buttonStyle = "display:flex;flex:0 0 28px;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:.5rem;background:transparent;color:var(--faint-color);cursor:pointer";
|
|
103
|
+
const buttonClass = "flex size-7 cursor-pointer items-center justify-center rounded-lg border-none bg-transparent text-(--faint-color) transition-colors duration-150 hover:bg-(--container-color) hover:text-(--font-color)";
|
|
104
|
+
const textStyle = "display:block;min-width:0;max-width:100%;margin:0;white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font:inherit";
|
|
105
|
+
const pane = (title, target, value) => `<section style="${sectionStyle}"><div style="${headingStyle}"><span>${title}</span><button type="button" class="${buttonClass}" data-xpoe-copy="${target}" aria-label="Копировать ${title}" style="${buttonStyle}">${COPY_ICON}</button></div><pre style="${textStyle}">${value}</pre></section>`;
|
|
106
|
+
return `<style>[data-modal]:has([data-xpoe-debug-modal])>div:last-child{display:none!important}</style><div data-xpoe-debug-modal style="${modalStyle}">${pane("Value", "value", decoded)}${hexIndex < 0 ? "" : pane("HEX", "hex", hex)}</div>`;
|
|
107
|
+
}
|