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 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`, эталонные пакеты собраны прошивочным кодом). Рамка xPOE-Debug (UART-консоль `system_console`, `[SOH] HDR [US] ARG [STX] VAL [ETX] CRC8 [US] FHS [EOT]`): `encodeXPoeDebugFrame` — команда устройству, `createXPoeDebugParser().push(байты)` — потоковый разбор кадров и текста вне кадров (`raw`), с проверкой CRC `crc8Rohc` строго побайтно, как `crc8_rohc` в `common.c` прошивок. 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`. Своих копий протоколов и словаря в приложениях не держим: терминал и отладка облака читают и пишут порт сырыми байтами, разбор и сборка — здесь.
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
 
@@ -9,7 +9,7 @@ export const crc16Kermit = (crc, bytes) => {
9
9
  }
10
10
  return crc & 0xffff;
11
11
  };
12
- /* CRC-8/ROHC: poly 0x07 reflected (0xE0), init 0xFF, xorout 0x00, check("123456789") = 0xD0 — рамка xPOE-Debug (system_console): HEADER, ARGUMENT, VALUE */
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";
@@ -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
- /* Рамка xPOE-Debug (UART-консоль system_console): сборка команды и потоковый разбор кадров и текста */
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 encodeXPoeDebugFrame: (header: string, argument: string, value: Uint8Array) => Uint8Array;
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
- /* $lib/protocols/xPoeDebug.ts */
2
- /* Рамка xPOE-Debug (system_console прошивки): [SOH] HEADER [US] ARGUMENT [STX] VALUE [ETX] CRC8 [US] FHS [EOT] \n — CRC8 двумя hex-символами
3
- по HEADER+ARGUMENT+VALUE (crc8Rohc), FHS (свободная куча) только у кадров устройства. Устройство шлёт VALUE текстом (лог или hex-дамп CBOR),
4
- устройству VALUE уходит сырыми байтами CBOR. Значения графиков — кадр OK! DebPlot (createPlotGroups). Текст между кадрами — RAW (вывод загрузчика и т.п.) */
5
- import { crc8Rohc } from "./crc.js";
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
- export const SOH = 0x01;
8
- export const STX = 0x02;
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 encoder = new TextEncoder();
11
+ const ARRAY5 = 0x85;
17
12
  const decoder = new TextDecoder();
18
- const hex2 = (value) => value.toString(16).toUpperCase().padStart(2, "0");
19
- /* Кадр команды устройству — те же байты, что разбирает system_console (VALUE — сырой CBOR, FHS не передаётся) */
20
- export const encodeXPoeDebugFrame = (header, argument, value) => {
21
- const headerBytes = encoder.encode(header);
22
- const argumentBytes = encoder.encode(argument);
23
- const crc = crc8Rohc(crc8Rohc(crc8Rohc(0xff, headerBytes), argumentBytes), value);
24
- return new Uint8Array([SOH, ...headerBytes, US, ...argumentBytes, STX, ...value, ETX, ...encoder.encode(hex2(crc)), EOT]);
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
- /* Строки текста вне кадров → raw; пустые и с управляющими символами пропускаются */
27
- const textItems = (bytes) => decoder
28
- .decode(bytes)
29
- .split("\n")
30
- .map((line) => line.trim())
31
- .filter((line) => line && ![...line].some((char) => char.charCodeAt(0) < 0x20 && char !== "\t"))
32
- .map((line) => ({ type: "raw", text: line }));
33
- /* Кадр от SOH до EOT включительно; без разделителей — null (пропускается, как в прошивке и Rust-терминале) */
34
- const frameItem = (frame) => {
35
- const us = frame.indexOf(US);
36
- const stx = us < 0 ? -1 : frame.indexOf(STX, us + 1);
37
- const etx = stx < 0 ? -1 : frame.indexOf(ETX, stx + 1);
38
- if (etx < 0)
39
- return null;
40
- const header = frame.subarray(1, us);
41
- const argument = frame.subarray(us + 1, stx);
42
- const valueBytes = frame.subarray(stx + 1, etx);
43
- const tailUs = frame.indexOf(US, etx + 1);
44
- const crc = decoder.decode(frame.subarray(etx + 1, tailUs < 0 ? frame.length - 1 : tailUs)).trim();
45
- const freeHeap = tailUs < 0 ? "" : decoder.decode(frame.subarray(tailUs + 1, frame.length - 1)).trim();
46
- const crcCalc = crc8Rohc(crc8Rohc(crc8Rohc(0xff, header), argument), valueBytes);
47
- return {
48
- type: "frame",
49
- header: decoder.decode(header),
50
- argument: decoder.decode(argument),
51
- value: decoder.decode(valueBytes),
52
- valueBytes: valueBytes.slice(),
53
- crc,
54
- crcOk: /^[0-9A-Fa-f]{2}$/.test(crc) && parseInt(crc, 16) === crcCalc,
55
- freeHeap,
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
- for (;;) {
72
- const soh = buffer.indexOf(SOH);
73
- if (soh < 0)
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
- /* Текст перед кадром не теряем; обрывок прошлого кадра (до последнего EOT) отбрасываем */
76
- if (soh > 0) {
77
- const prefix = buffer.subarray(0, soh);
78
- items.push(...textItems(prefix.subarray(prefix.lastIndexOf(EOT) + 1)));
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 eot = buffer.indexOf(EOT);
83
- if (eot < 0) {
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
- const item = frameItem(buffer.subarray(0, eot + 1));
91
- if (item)
92
- items.push(item);
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
- /* Поле Value команды из ввода пользователя: пусто — без тела, hex-строка (с пробелами или без) — готовые байты как есть, иначе JSON → CBOR
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 = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.72.0",
3
+ "version": "1.74.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Oleg-Perevyshin/ElementsUI.git"