poe-svelte-ui-lib 1.74.0 → 1.74.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.
@@ -16,6 +16,7 @@ export declare class CborEncoder {
16
16
  encodeFloat16(value: number): this;
17
17
  encodeString(value: string): this;
18
18
  encodeBytes(value: Uint8Array | number[]): this;
19
+ encodeRaw(value: Uint8Array): this;
19
20
  encodeArrayStart(count: number | null): this;
20
21
  encodeMapStart(count: number | null): this;
21
22
  encodeBreak(): this;
@@ -211,6 +211,11 @@ export class CborEncoder {
211
211
  this.writeBytes(bytes);
212
212
  return this;
213
213
  }
214
+ /* Добавить заранее проверенный CBOR item без повторного кодирования. */
215
+ encodeRaw(value) {
216
+ this.writeBytes(Array.from(value));
217
+ return this;
218
+ }
214
219
  /* Начать кодирование массива - count элементов или null для неопределённой длины */
215
220
  encodeArrayStart(count) {
216
221
  if (count === null)
@@ -14,7 +14,7 @@ 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
- /* System Console over UART: CBOR [SrcID, HdrArg, Value, FHS, CRC16], plus stream parsing */
17
+ /* System Console over UART: single-record poe_cbor_pkt with debug-only FHS */
18
18
  export { encodeXPoeDebugFrame, createXPoeDebugParser, xPoeDebugCmd, xPoeDebugValueFromText } from "./xPoeDebug.js";
19
19
  /* Отображение Value xPOE-Debug в таблице/модалке и копирование Value/HEX */
20
20
  export { formatXPoeDebugValue, formatXPoeDebugValueForTable, formatXPoeDebugModal, bindXPoeDebugCopyButtons } from "./xPoeDebugView.js";
@@ -19,7 +19,8 @@ export declare const splitCmd: (cmd: number) => {
19
19
  argument: number;
20
20
  reply: boolean;
21
21
  };
22
- export declare const packetObjectCrc: (trgBusID: number, srcBusID: number, cmd: number, payload: Uint8Array) => number;
22
+ export declare const packetObjectCrc: (encodedRecordPrefix: Uint8Array) => number;
23
+ export declare const encodePacketBytesPayload: (bytes: Uint8Array) => Uint8Array;
23
24
  export declare const encodePacket: (objects: PacketObject[]) => Uint8Array;
24
25
  export type PacketParse = {
25
26
  status: "complete";
@@ -1,7 +1,6 @@
1
1
  /* $lib/protocols/packet.ts */
2
- /* Пакет poe_cbor_pkt (README poe_cbor прошивки, ProdFactory-ESP/STM): indefinite ARRAY объектов [[TrgBusID, SrcBusID, Cmd, Payload, CRC16], …],
3
- объектов в пакете сколько угодно (в том числе одному адресату), порядок сохраняется; у каждого свой CRC-16/KERMIT по TrgBusID|SrcBusID|Cmd
4
- (по 2 байта big-endian) и Payload — не зависит от того, как числа записаны в CBOR. Общая точка сборки и разбора для терминала (CAN, RS485) и облака */
2
+ /* Общий пакет poe_cbor_pkt: indefinite ARRAY записей [TrgBusID, SrcBusID, Cmd, Payload, CRC16].
3
+ Payload — ровно один CBOR item. CRC-16/KERMIT считается по сырым CBOR-байтам записи от её заголовка до конца Payload. */
5
4
  import { CborDecoder } from "./cbor/decoder.js";
6
5
  import { CborEncoder } from "./cbor/encoder.js";
7
6
  import { CborEndOfDataError } from "./cbor/types.js";
@@ -17,22 +16,31 @@ export const splitCmd = (cmd) => ({
17
16
  argument: (cmd >> 1) & 0x1fff,
18
17
  reply: (cmd & 1) === 1,
19
18
  });
20
- /* CRC объекта: TrgBusID | SrcBusID | Cmd по 2 байта big-endian, затем Payload */
21
- export const packetObjectCrc = (trgBusID, srcBusID, cmd, payload) => {
22
- const head = [trgBusID >> 8, trgBusID, srcBusID >> 8, srcBusID, cmd >> 8, cmd].map((byte) => byte & 0xff);
23
- return crc16Kermit(crc16Kermit(0, head), payload);
19
+ export const packetObjectCrc = (encodedRecordPrefix) => crc16Kermit(0, encodedRecordPrefix);
20
+ /* Кодирует сырые байты как один CBOR byte string для Payload. */
21
+ export const encodePacketBytesPayload = (bytes) => {
22
+ const encoder = new CborEncoder();
23
+ encoder.encodeBytes(bytes);
24
+ return encoder.getResult();
24
25
  };
25
26
  /* Пакет из одного или нескольких объектов — те же байты, что CBOR_PACK/CBOR_PktEncFinish прошивки */
26
27
  export const encodePacket = (objects) => {
27
28
  const encoder = new CborEncoder();
28
29
  encoder.encodeArrayStart(null);
29
30
  for (const { trgBusID, srcBusID, cmd, payload } of objects) {
31
+ const recordStart = encoder.getResult().length;
30
32
  encoder.encodeArrayStart(5);
31
33
  encoder.encodeUnsigned(trgBusID);
32
34
  encoder.encodeUnsigned(srcBusID);
33
35
  encoder.encodeUnsigned(cmd);
34
- encoder.encodeBytes(payload);
35
- encoder.encodeUnsigned(packetObjectCrc(trgBusID, srcBusID, cmd, payload));
36
+ const prefixStart = encoder.getResult().length;
37
+ /* Payload is already CBOR-encoded; reject malformed or concatenated values. */
38
+ const payloadDecoder = new CborDecoder(payload);
39
+ if (!payloadDecoder.skipValue() || payloadDecoder.offset !== payload.length)
40
+ throw new Error("Payload должен содержать ровно один CBOR item");
41
+ encoder.encodeRaw(payload);
42
+ const prefix = encoder.getResult().slice(recordStart, prefixStart + payload.length);
43
+ encoder.encodeUnsigned(packetObjectCrc(prefix));
36
44
  }
37
45
  encoder.encodeBreak();
38
46
  return encoder.getResult();
@@ -52,13 +60,27 @@ const readValue = (bytes, offset) => {
52
60
  };
53
61
  const toNumber = (value) => (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff ? value : null);
54
62
  /* Объект пакета; не по формату (не 5 элементов, не те типы) — null, как CBOR_PktDecNext прошивки его пропускает */
55
- const toObject = (value) => {
63
+ const toObject = (value, rawRecord) => {
56
64
  if (!Array.isArray(value) || value.length !== 5)
57
65
  return null;
58
- const [trgBusID, srcBusID, cmd, payload, crc] = [toNumber(value[0]), toNumber(value[1]), toNumber(value[2]), value[3], toNumber(value[4])];
59
- if (trgBusID === null || srcBusID === null || cmd === null || !(payload instanceof Uint8Array) || crc === null)
66
+ const [trgBusID, srcBusID, cmd, crc] = [toNumber(value[0]), toNumber(value[1]), toNumber(value[2]), toNumber(value[4])];
67
+ if (trgBusID === null || srcBusID === null || cmd === null || crc === null)
68
+ return null;
69
+ const info = rawRecord[0] & 0x1f;
70
+ const headerLength = info < 24 ? 1 : info === 24 ? 2 : info === 25 ? 3 : info === 26 ? 5 : info === 27 ? 9 : 0;
71
+ if ((rawRecord[0] >> 5) !== 4 || headerLength === 0)
72
+ return null;
73
+ const fields = new CborDecoder(rawRecord.subarray(headerLength));
74
+ for (let i = 0; i < 3; i++)
75
+ if (!fields.skipValue())
76
+ return null;
77
+ const payloadStart = headerLength + fields.offset;
78
+ if (!fields.skipValue())
60
79
  return null;
61
- return { trgBusID, srcBusID, cmd, payload, crc, crcOk: packetObjectCrc(trgBusID, srcBusID, cmd, payload) === crc, ...splitCmd(cmd) };
80
+ const payloadEnd = headerLength + fields.offset;
81
+ const prefix = rawRecord.subarray(0, payloadEnd);
82
+ const payloadBytes = rawRecord.subarray(payloadStart, payloadEnd);
83
+ return { trgBusID, srcBusID, cmd, payload: payloadBytes, crc, crcOk: packetObjectCrc(prefix) === crc, ...splitCmd(cmd) };
62
84
  };
63
85
  /* Разбор пакета с начала буфера — и для целого кадра (CAN), и для потока (RS485: «incomplete» — ждать ещё байт, «garbage» — сдвинуться на байт) */
64
86
  export const decodePacket = (bytes) => {
@@ -93,7 +115,7 @@ export const decodePacket = (bytes) => {
93
115
  if (!value)
94
116
  return { status: "incomplete" };
95
117
  offset += value.length;
96
- const object = toObject(value.value);
118
+ const object = toObject(value.value, bytes.subarray(offset - value.length, offset));
97
119
  if (object)
98
120
  objects.push(object);
99
121
  }
@@ -1,5 +1,6 @@
1
1
  export type XPoeDebugItem = {
2
2
  type: "frame";
3
+ trgId: number;
3
4
  srcId: number;
4
5
  cmd: number;
5
6
  header: string;
@@ -14,7 +15,7 @@ export type XPoeDebugItem = {
14
15
  text: string;
15
16
  };
16
17
  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;
18
+ export declare const encodeXPoeDebugFrame: (header: string, argument: string, value: Uint8Array, srcId?: number, freeHeap?: number | null, trgId?: number) => Uint8Array;
18
19
  export declare const createXPoeDebugParser: () => {
19
20
  push(chunk: Uint8Array): XPoeDebugItem[];
20
21
  reset(): void;
@@ -1,5 +1,4 @@
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. */
1
+ /* Debug is a one-record poe_cbor_pkt. Its record adds FHS to the common bus fields. */
3
2
  import { CborDecoder } from "./cbor/decoder.js";
4
3
  import { CborEndOfDataError } from "./cbor/types.js";
5
4
  import { crc16Kermit } from "./crc.js";
@@ -8,7 +7,8 @@ import { DEFAULT_ARGUMENT_CODES, HEADER_CODES, nameByCode } from "./dictionary.j
8
7
  import { makeCmd, splitCmd } from "./packet.js";
9
8
  const MAX_TEXT_TAIL = 4096;
10
9
  const MAX_FRAME = 8192;
11
- const ARRAY5 = 0x85;
10
+ const PACKET_START = 0x9f;
11
+ const RECORD6 = 0x86;
12
12
  const decoder = new TextDecoder();
13
13
  const encodeUInt = (input) => {
14
14
  const value = BigInt(input);
@@ -19,7 +19,7 @@ const encodeUInt = (input) => {
19
19
  if (value <= 0xffffn)
20
20
  return [0x19, Number(value >> 8n), Number(value & 0xffn)];
21
21
  if (value <= 0xffffffffn)
22
- return [0x1a, Number(value >> 24n), Number(value >> 16n & 0xffn), Number(value >> 8n & 0xffn), Number(value & 0xffn)];
22
+ return [0x1a, Number(value >> 24n), Number((value >> 16n) & 0xffn), Number((value >> 8n) & 0xffn), Number(value & 0xffn)];
23
23
  throw new Error("xPOE-Debug integer is out of range");
24
24
  };
25
25
  export const xPoeDebugCmd = (header, argument) => {
@@ -30,47 +30,67 @@ export const xPoeDebugCmd = (header, argument) => {
30
30
  return makeCmd(hdr, arg);
31
31
  };
32
32
  /* Команда терминала: CRC считается по тем CBOR-байтам, которые реально отправляются. */
33
- export const encodeXPoeDebugFrame = (header, argument, value, srcId = 0, freeHeap = null) => {
33
+ export const encodeXPoeDebugFrame = (header, argument, value, srcId = 0, freeHeap = null, trgId = 0) => {
34
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");
35
+ if (![srcId, trgId, cmd].every((n) => Number.isInteger(n) && n >= 0 && n <= 0xffff))
36
+ throw new Error("TrgBusID, SrcBusID и Cmd должны быть uint16");
37
37
  const valueParser = new CborDecoder(value);
38
38
  if (!valueParser.skipValue() || valueParser.offset !== value.length)
39
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)]);
40
+ const recordPrefix = new Uint8Array([RECORD6, ...encodeUInt(trgId), ...encodeUInt(srcId), ...encodeUInt(cmd), ...value, ...(freeHeap === null ? [0xf6] : encodeUInt(freeHeap))]);
41
+ const crc = crc16Kermit(0, recordPrefix);
42
+ return new Uint8Array([PACKET_START, ...recordPrefix, ...encodeUInt(crc), 0xff]);
43
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 означает неполный или невалидный кадр. */
44
+ const textItems = (bytes) => decoder
45
+ .decode(bytes)
46
+ .split("\n")
47
+ .map((line) => line.trim())
48
+ .filter((line) => line && ![...line].some((char) => char.charCodeAt(0) < 0x20 && char !== "\t"))
49
+ .map((text) => ({ type: "raw", text }));
50
+ const readUInt = (value) => (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff ? value : null);
51
+ /* Декодирует пакет из одной записи с FHS; null означает неполный или невалидный кадр. */
47
52
  const frameItem = (buffer) => {
48
- if (buffer[0] !== ARRAY5)
53
+ if (buffer[0] !== PACKET_START || buffer[1] !== RECORD6)
49
54
  return { status: "garbage" };
50
55
  try {
51
56
  const cbor = new CborDecoder(buffer);
52
- cbor.decode(); /* Проверка структуры и границы всего массива */
57
+ const packet = cbor.decode(); /* Проверка структуры и границы всего пакета */
58
+ if (!Array.isArray(packet) || packet.length !== 1 || !Array.isArray(packet[0]) || packet[0].length !== 6)
59
+ return { status: "garbage" };
53
60
  const length = cbor.offset;
54
- const fields = new CborDecoder(buffer.subarray(1, length));
61
+ const fields = new CborDecoder(buffer.subarray(2, length));
62
+ const trgId = readUInt(fields.decode());
55
63
  const srcId = readUInt(fields.decode());
56
64
  const cmd = readUInt(fields.decode());
57
- if (srcId === null || cmd === null)
65
+ if (trgId === null || srcId === null || cmd === null)
58
66
  return { status: "garbage" };
59
- const valueStart = 1 + fields.offset;
67
+ const valueStart = 2 + fields.offset;
60
68
  if (!fields.skipValue())
61
69
  return { status: "garbage" };
62
- const valueEnd = 1 + fields.offset;
70
+ const valueEnd = 2 + fields.offset;
63
71
  const freeHeapValue = fields.decode();
64
- const crcStart = 1 + fields.offset;
72
+ const crcStart = 2 + fields.offset;
65
73
  const crc = readUInt(fields.decode());
66
- if (fields.offset !== length - 1 || crc === null || (freeHeapValue !== null && (typeof freeHeapValue !== "number" || !Number.isInteger(freeHeapValue) || freeHeapValue < 0)))
74
+ if (fields.offset !== length - 3 || crc === null || (freeHeapValue !== null && (typeof freeHeapValue !== "number" || !Number.isInteger(freeHeapValue) || freeHeapValue < 0)))
67
75
  return { status: "garbage" };
68
76
  const valueBytes = buffer.slice(valueStart, valueEnd);
69
77
  const { header, argument } = splitCmd(cmd);
70
78
  return {
71
79
  status: "complete",
72
80
  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) : "" },
81
+ item: {
82
+ type: "frame",
83
+ trgId,
84
+ srcId,
85
+ cmd,
86
+ header: Object.entries(HEADER_CODES).find(([, code]) => code === header)?.[0] ?? String(header),
87
+ argument: nameByCode(DEFAULT_ARGUMENT_CODES, argument),
88
+ value: Array.from(valueBytes, (byte) => byte.toString(16).padStart(2, "0").toUpperCase()).join(""),
89
+ valueBytes,
90
+ crc: crc.toString(16).toUpperCase().padStart(4, "0"),
91
+ crcOk: crc16Kermit(0, buffer.subarray(1, crcStart)) === crc,
92
+ freeHeap: typeof freeHeapValue === "number" ? String(freeHeapValue) : "",
93
+ },
74
94
  };
75
95
  }
76
96
  catch (error) {
@@ -92,7 +112,7 @@ export const createXPoeDebugParser = () => {
92
112
  buffer = concat(buffer, chunk);
93
113
  const items = [];
94
114
  while (buffer.length) {
95
- const start = buffer.indexOf(ARRAY5);
115
+ const start = buffer.indexOf(PACKET_START);
96
116
  if (start < 0) {
97
117
  const cut = buffer.length > MAX_TEXT_TAIL ? buffer.length : buffer.lastIndexOf(0x0a);
98
118
  if (cut >= 0) {
@@ -124,7 +144,9 @@ export const createXPoeDebugParser = () => {
124
144
  }
125
145
  return items;
126
146
  },
127
- reset() { buffer = new Uint8Array(); },
147
+ reset() {
148
+ buffer = new Uint8Array();
149
+ },
128
150
  };
129
151
  };
130
152
  /* Value form input: ready CBOR hex passes through; JSON is converted to CBOR. */
@@ -52,6 +52,17 @@ export function formatXPoeDebugValueForTable(rawValue, isCbor = true) {
52
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
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
54
  let copyBindings = 0;
55
+ let copyObserver = null;
56
+ const removeXPoeDebugModalFooters = () => {
57
+ document.querySelectorAll("[data-modal]").forEach((modal) => {
58
+ if (!modal.querySelector("[data-xpoe-debug-modal]"))
59
+ return;
60
+ const footer = modal.lastElementChild;
61
+ if (!footer || modal.children.length < 3)
62
+ return;
63
+ footer.remove();
64
+ });
65
+ };
55
66
  const handleCopyClick = (event) => {
56
67
  if (!(event.target instanceof Element))
57
68
  return;
@@ -79,15 +90,22 @@ const handleCopyClick = (event) => {
79
90
  };
80
91
  /* Делегированный обработчик кнопок модального окна; возвращённую функцию вызывают при уничтожении страницы. */
81
92
  export function bindXPoeDebugCopyButtons() {
82
- if (copyBindings++ === 0)
93
+ if (copyBindings++ === 0) {
83
94
  document.addEventListener("click", handleCopyClick);
95
+ removeXPoeDebugModalFooters();
96
+ copyObserver = new MutationObserver(removeXPoeDebugModalFooters);
97
+ copyObserver.observe(document.documentElement, { childList: true, subtree: true });
98
+ }
84
99
  let isBound = true;
85
100
  return () => {
86
101
  if (!isBound)
87
102
  return;
88
103
  isBound = false;
89
- if (--copyBindings === 0)
104
+ if (--copyBindings === 0) {
90
105
  document.removeEventListener("click", handleCopyClick);
106
+ copyObserver?.disconnect();
107
+ copyObserver = null;
108
+ }
91
109
  };
92
110
  }
93
111
  /* Две прокручиваемые секции модалки; HEX-раздел появляется, если форматтер таблицы добавил исходные байты. */
@@ -103,5 +121,5 @@ export function formatXPoeDebugModal(text) {
103
121
  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
122
  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
123
  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>`;
124
+ return `<div data-xpoe-debug-modal style="${modalStyle}">${pane("Value", "value", decoded)}${hexIndex < 0 ? "" : pane("HEX", "hex", hex)}</div>`;
107
125
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.74.0",
3
+ "version": "1.74.2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Oleg-Perevyshin/ElementsUI.git"