poe-svelte-ui-lib 1.64.0 → 1.65.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
@@ -59,7 +59,7 @@ npm install poe-svelte-ui-lib
59
59
 
60
60
  **Названия смарт-виджетов** — в словаре библиотеки на всех языках (WiFi, «Об устройстве», «Файловая система», «Стек и задачи», «RGB лента», «Пилотажный дисплей», «Карта»); пустой `label.name` показывает название на языке пользователя, `widgetTitle(type)` отдаёт его сразу на всех языках `{ ru, en, zh }` — так конструктор заполняет заголовок нового виджета.
61
61
 
62
- **Ядро протоколов** — `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` `{ TrgBusID : [RetBusID, HA_Code, Payload, CRC16] }` (CAN, RS485): `encodePacket`/`decodePacket` (разбор с начала буфера — `complete` с длиной, `incomplete` для потока или `garbage`; объекты с одинаковым `TrgBusID` не склеиваются, неверный CRC — флаг `crcOk`), `objectsFor` (свой адрес и broadcast `0xFFFF`), `makeHaCode`/`splitHaCode`, `packetObjectCrc`; контрольные суммы `crc16Kermit` и `crc8Rohc`. Байты совпадают с `poe_cbor_pkt.c` прошивки — проверка `npm run TestProtocols` (`tests/protocols.test.mjs`, эталонные пакеты собраны прошивочным кодом). Рамка POESerial (UART-консоль `system_console`, `[SOH] HDR [US] ARG [STX] VAL [ETX] CRC8 [US] FHS [EOT]`): `encodePoeSerialFrame` — команда устройству, `createPoeSerialParser().push(байты)` — потоковый разбор кадров, строк Teleplot (`plot`) и прочего текста (`raw`), с проверкой CRC `crc8RohcUtf8` по правилу прошивки (`crc8_rohc` в `common.c`). Дальше сюда переезжает разбор CAN/RS485 — своих копий протоколов в приложениях не держим.
62
+ **Ядро протоколов** — `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` `{ TrgBusID : [RetBusID, HA_Code, Payload, CRC16] }` (CAN, RS485): `encodePacket`/`decodePacket` (разбор с начала буфера — `complete` с длиной, `incomplete` для потока или `garbage`; объекты с одинаковым `TrgBusID` не склеиваются, неверный CRC — флаг `crcOk`), `objectsFor` (свой адрес и broadcast `0xFFFF`), `makeHaCode`/`splitHaCode`, `packetObjectCrc`; контрольные суммы `crc16Kermit` и `crc8Rohc`. Байты совпадают с `poe_cbor_pkt.c` прошивки — проверка `npm run TestProtocols` (`tests/protocols.test.mjs`, эталонные пакеты собраны прошивочным кодом). Рамка POESerial (UART-консоль `system_console`, `[SOH] HDR [US] ARG [STX] VAL [ETX] CRC8 [US] FHS [EOT]`): `encodePoeSerialFrame` — команда устройству, `createPoeSerialParser().push(байты)` — потоковый разбор кадров, строк Teleplot (`plot`) и прочего текста (`raw`), с проверкой CRC `crc8RohcUtf8` по правилу прошивки (`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(байты)` (пауза обрывает недособранное, мусор пропускается по байту). Своих копий протоколов в приложениях не держим: терминал читает и пишет порт сырыми байтами, разбор и сборка — здесь.
63
63
 
64
64
  **Шапка смарт-виджета** (`WidgetHeader`): значок, заголовок и сворачивание слева, «?» в правом краю; правая часть (режимы, «Обновить» и т. п.) стоит в той же строке, а если не помещается — уходит второй строкой под заголовок и там переносится.
65
65
 
@@ -0,0 +1,23 @@
1
+ export declare const CAN_FINAL_BIT = 1024;
2
+ export declare const CAN_PACKET_ID_MASK = 1023;
3
+ export interface CanFrame {
4
+ canId: number;
5
+ data: Uint8Array;
6
+ }
7
+ export declare const canFragments: (payload: Uint8Array, fd: boolean, packetId?: number) => CanFrame[];
8
+ export declare const encodeSlcanFrame: ({ canId, data }: CanFrame, fd: boolean) => string;
9
+ export declare const encodeCanPacket: (payload: Uint8Array, fd: boolean, packetId?: number) => string;
10
+ export interface CanMessage {
11
+ canId: number;
12
+ packetId: number;
13
+ data: Uint8Array;
14
+ remote: boolean;
15
+ timestamp: number;
16
+ }
17
+ export declare const createCanParser: ({ sessionTimeoutMs, maxPacketBytes }?: {
18
+ sessionTimeoutMs?: number | undefined;
19
+ maxPacketBytes?: number | undefined;
20
+ }) => {
21
+ push(chunk: Uint8Array, now?: number): CanMessage[];
22
+ reset(): void;
23
+ };
@@ -0,0 +1,104 @@
1
+ /* $lib/protocols/can.ts */
2
+ /* Пакеты по CAN/CAN FD через slcan-адаптер (can_service прошивки): стандартный 11-битный ID — бит 10 «последний/единственный фрагмент»,
3
+ биты 0..9 — packet_id, одинаковый у всех фрагментов пакета; DATA — чистый payload без служебных байт. Адаптер говорит ASCII-кадрами slcan:
4
+ [t|d|b|r]III L DD..DD\r (стандартный ID, 3 hex), [T|D|B|R]IIIIIIII L DD..\r (расширенный, 8 hex); t/T — CAN, d/D, b/B — CAN FD (b — с BRS) */
5
+ import { decodePacket } from "./packet.js";
6
+ export const CAN_FINAL_BIT = 0x400;
7
+ export const CAN_PACKET_ID_MASK = 0x3ff;
8
+ const CAN_CLASSIC_PAYLOAD = 8;
9
+ const CAN_FD_PAYLOAD = 64;
10
+ /* Длины данных CAN FD по коду DLC 9..15 */
11
+ const FD_SIZES = [12, 16, 20, 24, 32, 48, 64];
12
+ /* Нарезка пакета на кадры — как CAN_Send прошивки: один кадр с битом «последний», иначе куски по 8 (CAN FD — 64) байт, бит только у последнего */
13
+ export const canFragments = (payload, fd, packetId = 1 + Math.floor(Math.random() * CAN_PACKET_ID_MASK)) => {
14
+ const max = fd ? CAN_FD_PAYLOAD : CAN_CLASSIC_PAYLOAD;
15
+ const id = packetId & CAN_PACKET_ID_MASK;
16
+ if (payload.length <= max)
17
+ return [{ canId: CAN_FINAL_BIT | id, data: payload }];
18
+ const frames = [];
19
+ for (let offset = 0; offset < payload.length; offset += max) {
20
+ const isLast = offset + max >= payload.length;
21
+ frames.push({ canId: isLast ? CAN_FINAL_BIT | id : id, data: payload.subarray(offset, offset + max) });
22
+ }
23
+ return frames;
24
+ };
25
+ const hex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).toUpperCase().padStart(2, "0")).join("");
26
+ /* Кадр данных slcan (стандартный ID): CAN — «t», CAN FD — «b» (с BRS); данные CAN FD дополняются нулями до ближайшей допустимой длины */
27
+ export const encodeSlcanFrame = ({ canId, data }, fd) => {
28
+ const id = (canId & 0x7ff).toString(16).toUpperCase().padStart(3, "0");
29
+ if (!fd)
30
+ return `t${id}${Math.min(data.length, 8).toString(16).toUpperCase()}${hex(data.subarray(0, 8))}\r`;
31
+ const code = data.length <= 8 ? data.length : 9 + FD_SIZES.findIndex((size) => size >= data.length);
32
+ const size = code <= 8 ? code : FD_SIZES[code - 9];
33
+ const padded = new Uint8Array(size);
34
+ padded.set(data.subarray(0, size));
35
+ return `b${id}${code.toString(16).toUpperCase()}${hex(padded)}\r`;
36
+ };
37
+ /* Пакет целиком в строку кадров slcan для отправки в адаптер */
38
+ export const encodeCanPacket = (payload, fd, packetId) => canFragments(payload, fd, packetId)
39
+ .map((frame) => encodeSlcanFrame(frame, fd))
40
+ .join("");
41
+ const SLCAN_FRAME = /([tdbr])([0-9A-Fa-f]{3})([0-9A-Fa-f])([0-9A-Fa-f]*)\r|([TDBR])([0-9A-Fa-f]{8})([0-9A-Fa-f])([0-9A-Fa-f]*)\r/g;
42
+ const fdLength = (code) => (code <= 8 ? code : (FD_SIZES[code - 9] ?? 0));
43
+ /* CAN FD добивает последний кадр до допустимой длины — лишние байты после пакета poe_cbor_pkt (или нулевой хвост) отрезаем */
44
+ const trimFdPadding = (data) => {
45
+ const parsed = decodePacket(data);
46
+ if (parsed.status === "complete")
47
+ return data.subarray(0, parsed.length);
48
+ let end = data.length;
49
+ while (end > 0 && data[end - 1] === 0)
50
+ end--;
51
+ return data.subarray(0, end);
52
+ };
53
+ const decoder = new TextDecoder();
54
+ /* Потоковый разбор ответа slcan-адаптера: push(байты порта, время) → собранные пакеты; незавершённые сессии живут sessionTimeoutMs */
55
+ export const createCanParser = ({ sessionTimeoutMs = 2000, maxPacketBytes = 8192 } = {}) => {
56
+ let text = "";
57
+ const sessions = new Map();
58
+ return {
59
+ push(chunk, now = Date.now()) {
60
+ text += decoder.decode(chunk, { stream: true });
61
+ for (const [id, session] of sessions)
62
+ if (now - session.timestamp > sessionTimeoutMs)
63
+ sessions.delete(id);
64
+ const messages = [];
65
+ let consumed = 0;
66
+ for (const match of text.matchAll(SLCAN_FRAME)) {
67
+ consumed = (match.index ?? 0) + match[0].length;
68
+ const [type, idHex, dlcHex, dataHex] = match[1] ? match.slice(1, 5) : match.slice(5, 9);
69
+ const lower = type.toLowerCase();
70
+ const canId = parseInt(idHex, 16);
71
+ const dlc = parseInt(dlcHex, 16);
72
+ const size = lower === "t" || lower === "r" ? Math.min(dlc, 8) : fdLength(dlc);
73
+ const bytes = lower === "r" ? [] : Array.from({ length: Math.min(size, dataHex.length >> 1) }, (_, i) => parseInt(dataHex.slice(i * 2, i * 2 + 2), 16));
74
+ const packetId = canId & CAN_PACKET_ID_MASK;
75
+ if (lower === "r") {
76
+ messages.push({ canId, packetId, data: new Uint8Array(), remote: true, timestamp: now });
77
+ continue;
78
+ }
79
+ const session = sessions.get(packetId);
80
+ if (!(canId & CAN_FINAL_BIT)) {
81
+ /* Промежуточный фрагмент: копим; потерян последний и пакет разросся — сбрасываем */
82
+ const data = [...(session?.data ?? []), ...bytes];
83
+ if (data.length > maxPacketBytes)
84
+ sessions.delete(packetId);
85
+ else
86
+ sessions.set(packetId, { data, timestamp: session?.timestamp ?? now });
87
+ continue;
88
+ }
89
+ sessions.delete(packetId);
90
+ const data = Uint8Array.from([...(session?.data ?? []), ...bytes]);
91
+ messages.push({ canId, packetId, data: lower === "t" ? data : trimFdPadding(data), remote: false, timestamp: session?.timestamp ?? now });
92
+ }
93
+ /* Разобранное убираем; хвост без конца кадра ждёт следующего чанка, мусор без \r не копится бесконечно */
94
+ text = text.slice(consumed);
95
+ if (text.length > 4 * maxPacketBytes)
96
+ text = text.slice(text.lastIndexOf("\r") + 1);
97
+ return messages;
98
+ },
99
+ reset() {
100
+ text = "";
101
+ sessions.clear();
102
+ },
103
+ };
104
+ };
@@ -5,5 +5,6 @@ export { bytesToBase64, base64ToBytes } from "./cbor/base64.js";
5
5
  export { CborToValue, ValueToCbor, cborToValue, valueToCbor } from "./cbor/value.js";
6
6
  export { CborToJson, JsonToCbor, cborToJson, jsonToCbor } from "./cbor/json.js";
7
7
  export { crc16Kermit, crc8Rohc, crc8RohcUtf8 } from "./crc.js";
8
- export { BROADCAST_BUS_ID, makeHaCode, splitHaCode, packetObjectCrc, encodePacket, decodePacket, objectsFor, type PacketObject, type DecodedPacketObject, type PacketParse, } from "./packet.js";
8
+ export { BROADCAST_BUS_ID, makeHaCode, splitHaCode, packetObjectCrc, encodePacket, decodePacket, objectsFor, createPacketStreamParser, type PacketObject, type DecodedPacketObject, type PacketParse, } from "./packet.js";
9
9
  export { encodePoeSerialFrame, createPoeSerialParser, type PoeSerialItem } from "./poeSerial.js";
10
+ export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser, type CanFrame, type CanMessage } from "./can.js";
@@ -11,6 +11,8 @@ export { CborToValue, ValueToCbor, cborToValue, valueToCbor } from "./cbor/value
11
11
  export { CborToJson, JsonToCbor, cborToJson, jsonToCbor } from "./cbor/json.js";
12
12
  /* Контрольные суммы и пакет poe_cbor_pkt { TrgBusID : [RetBusID, HA_Code, Payload, CRC16] } (CAN, RS485) */
13
13
  export { crc16Kermit, crc8Rohc, crc8RohcUtf8 } from "./crc.js";
14
- export { BROADCAST_BUS_ID, makeHaCode, splitHaCode, packetObjectCrc, encodePacket, decodePacket, objectsFor, } from "./packet.js";
14
+ export { BROADCAST_BUS_ID, makeHaCode, splitHaCode, packetObjectCrc, encodePacket, decodePacket, objectsFor, createPacketStreamParser, } from "./packet.js";
15
15
  /* Рамка POESerial (UART-консоль system_console): сборка команды и потоковый разбор кадров, строк Teleplot и текста */
16
16
  export { encodePoeSerialFrame, createPoeSerialParser } from "./poeSerial.js";
17
+ /* CAN/CAN FD через slcan-адаптер: нарезка пакета на кадры как в can_service прошивки, кадры slcan, потоковый разбор со сборкой фрагментов */
18
+ export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser } from "./can.js";
@@ -29,3 +29,13 @@ export type PacketParse = {
29
29
  };
30
30
  export declare const decodePacket: (bytes: Uint8Array) => PacketParse;
31
31
  export declare const objectsFor: (objects: DecodedPacketObject[], busID: number) => DecodedPacketObject[];
32
+ export declare const createPacketStreamParser: ({ gapMs, maxBytes }?: {
33
+ gapMs?: number | undefined;
34
+ maxBytes?: number | undefined;
35
+ }) => {
36
+ push(chunk: Uint8Array, now?: number): {
37
+ bytes: Uint8Array;
38
+ objects: DecodedPacketObject[];
39
+ }[];
40
+ reset(): void;
41
+ };
@@ -103,3 +103,38 @@ export const decodePacket = (bytes) => {
103
103
  };
104
104
  /* Объекты пакета для своего адреса (и broadcast) с верным CRC — как CBOR_PktDecGet прошивки, только все, а не первый */
105
105
  export const objectsFor = (objects, busID) => objects.filter((object) => object.crcOk && (object.trgBusID === busID || object.trgBusID === BROADCAST_BUS_ID));
106
+ /* Потоковый разбор пакетов из сырого потока (RS485): пауза дольше gapMs обрывает недособранный пакет, мусор сдвигается по байту,
107
+ без единого целого пакета буфер не растёт дальше maxBytes */
108
+ export const createPacketStreamParser = ({ gapMs = 100, maxBytes = 4096 } = {}) => {
109
+ let buffer = new Uint8Array();
110
+ let last = 0;
111
+ return {
112
+ push(chunk, now = Date.now()) {
113
+ if (buffer.length && now - last > gapMs)
114
+ buffer = new Uint8Array();
115
+ last = now;
116
+ const merged = new Uint8Array(buffer.length + chunk.length);
117
+ merged.set(buffer);
118
+ merged.set(chunk, buffer.length);
119
+ buffer = merged;
120
+ const packets = [];
121
+ while (buffer.length) {
122
+ const parsed = decodePacket(buffer);
123
+ if (parsed.status === "incomplete")
124
+ break;
125
+ if (parsed.status === "garbage") {
126
+ buffer = buffer.slice(1);
127
+ continue;
128
+ }
129
+ packets.push({ bytes: buffer.slice(0, parsed.length), objects: parsed.objects });
130
+ buffer = buffer.slice(parsed.length);
131
+ }
132
+ if (buffer.length > maxBytes)
133
+ buffer = new Uint8Array();
134
+ return packets;
135
+ },
136
+ reset() {
137
+ buffer = new Uint8Array();
138
+ },
139
+ };
140
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-svelte-ui-lib",
3
- "version": "1.64.0",
3
+ "version": "1.65.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Oleg-Perevyshin/ElementsUI.git"