poe-svelte-ui-lib 1.64.0 → 1.66.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/can.d.ts +24 -0
- package/dist/protocols/can.js +107 -0
- package/dist/protocols/dictionary.d.ts +4 -0
- package/dist/protocols/dictionary.js +68 -0
- package/dist/protocols/index.d.ts +4 -2
- package/dist/protocols/index.js +6 -2
- package/dist/protocols/packet.d.ts +10 -0
- package/dist/protocols/packet.js +35 -0
- package/dist/protocols/poeSerial.d.ts +1 -0
- package/dist/protocols/poeSerial.js +18 -0
- package/package.json +1 -1
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`).
|
|
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(байты)` (пауза обрывает недособранное, мусор пропускается по байту). Открытие slcan-адаптера — `slcanOpenCommands(скорость, скоростьДанных?)`. Поле Value команды POESerial из ввода — `poeSerialValueFromText` (hex как есть, JSON → CBOR). Словарь протокола — `HEADER_CODES`, `DEFAULT_ARGUMENT_CODES` (из `.framework/Dictionary.yaml` прошивок), `parseArgumentDictionary` (строки `- [Строка, Enum, Число]`), `nameByCode`. Своих копий протоколов и словаря в приложениях не держим: терминал и отладка облака читают и пишут порт сырыми байтами, разбор и сборка — здесь.
|
|
63
63
|
|
|
64
64
|
**Шапка смарт-виджета** (`WidgetHeader`): значок, заголовок и сворачивание слева, «?» в правом краю; правая часть (режимы, «Обновить» и т. п.) стоит в той же строке, а если не помещается — уходит второй строкой под заголовок и там переносится.
|
|
65
65
|
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
};
|
|
24
|
+
export declare const slcanOpenCommands: (bitrate: string, dataBitrate?: string) => string[];
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
};
|
|
105
|
+
/* Команды открытия slcan-адаптера (CANable и совместимые): закрыть канал, скорость арбитража (S0..S8, SA..SD), для CAN FD — скорость данных
|
|
106
|
+
(Y1..Y5), нормальный режим, без автоповтора, открыть канал — каждая строка заканчивается \r */
|
|
107
|
+
export const slcanOpenCommands = (bitrate, dataBitrate) => ["C", bitrate, ...(dataBitrate ? [dataBitrate] : []), "M0", "A0", "O"].map((command) => `${command}\r`);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const HEADER_CODES: Record<string, number>;
|
|
2
|
+
export declare const DEFAULT_ARGUMENT_CODES: Record<string, number>;
|
|
3
|
+
export declare const parseArgumentDictionary: (text: string) => Record<string, number>;
|
|
4
|
+
export declare const nameByCode: (codes: Record<string, number>, code: number) => string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* $lib/protocols/dictionary.ts */
|
|
2
|
+
/* Словарь заголовков и аргументов протокола HA_Code — заголовок в 2 старших битах, аргумент в 14 младших (makeHaCode). Аргументы по умолчанию —
|
|
3
|
+
из .framework/Dictionary.yaml прошивок (ProdFactory-ESP и ProdFactory-STM, одинаковые); у устройства словарь может быть свой — его загружают
|
|
4
|
+
из файла (parseArgumentDictionary), своих копий словаря в приложениях не держим */
|
|
5
|
+
export const HEADER_CODES = {
|
|
6
|
+
GET: 0,
|
|
7
|
+
SET: 1,
|
|
8
|
+
"OK!": 2,
|
|
9
|
+
"ER!": 3,
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_ARGUMENT_CODES = {
|
|
12
|
+
Unknown: 0,
|
|
13
|
+
Restart: 1,
|
|
14
|
+
DefCfg: 2,
|
|
15
|
+
DlFW: 3,
|
|
16
|
+
RstNVS: 5,
|
|
17
|
+
LocTime: 6,
|
|
18
|
+
Update: 7,
|
|
19
|
+
StackInfo: 8,
|
|
20
|
+
BackUp: 9,
|
|
21
|
+
BindMKey: 10,
|
|
22
|
+
DataStream: 15,
|
|
23
|
+
APsList: 21,
|
|
24
|
+
FS: 22,
|
|
25
|
+
JoinDev: 23,
|
|
26
|
+
JoinUser: 24,
|
|
27
|
+
LeaveUser: 25,
|
|
28
|
+
ModFind: 31,
|
|
29
|
+
ModList: 32,
|
|
30
|
+
ModCap: 33,
|
|
31
|
+
ModInfo: 34,
|
|
32
|
+
ModMode: 35,
|
|
33
|
+
ModCfg: 36,
|
|
34
|
+
ModT1: 37,
|
|
35
|
+
ModT2: 38,
|
|
36
|
+
ModT3: 39,
|
|
37
|
+
ModT4: 40,
|
|
38
|
+
ModT5: 41,
|
|
39
|
+
ModS1: 42,
|
|
40
|
+
ModS2: 43,
|
|
41
|
+
ModS3: 44,
|
|
42
|
+
ModS4: 45,
|
|
43
|
+
ModS5: 46,
|
|
44
|
+
FwUpgS: 51,
|
|
45
|
+
FwUpgP: 52,
|
|
46
|
+
FwUpgD: 53,
|
|
47
|
+
FwUpgE: 54,
|
|
48
|
+
FDwS: 65,
|
|
49
|
+
FDwP: 66,
|
|
50
|
+
FDwD: 67,
|
|
51
|
+
FDwE: 68,
|
|
52
|
+
TestReq: 91,
|
|
53
|
+
MotInit: 111,
|
|
54
|
+
MotMode: 112,
|
|
55
|
+
MotPos: 113,
|
|
56
|
+
MotPID: 114,
|
|
57
|
+
MotInfo: 115,
|
|
58
|
+
MotCfg: 116,
|
|
59
|
+
MotCal: 117,
|
|
60
|
+
OrData: 141,
|
|
61
|
+
OrOfs: 142,
|
|
62
|
+
OrAccCal: 143,
|
|
63
|
+
OrMagCal: 144,
|
|
64
|
+
};
|
|
65
|
+
/* Аргументы из текста Dictionary.yaml (или его фрагмента): строки «- [Строка, Enum, Число]», комментарии и прочее пропускаются */
|
|
66
|
+
export const parseArgumentDictionary = (text) => Object.fromEntries([...text.matchAll(/^\s*-\s*\[\s*([^,\]]+?)\s*,\s*[^,\]]+?\s*,\s*(\d+)\s*\]/gm)].map(([, name, code]) => [name, Number(code)]));
|
|
67
|
+
/* Имя по коду (обратный поиск) — для показа принятых пакетов; неизвестный код — числом */
|
|
68
|
+
export const nameByCode = (codes, code) => Object.entries(codes).find(([, value]) => value === code)?.[0] ?? String(code);
|
|
@@ -5,5 +5,7 @@ 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";
|
|
9
|
-
export { encodePoeSerialFrame, createPoeSerialParser, type PoeSerialItem } from "./poeSerial.js";
|
|
8
|
+
export { BROADCAST_BUS_ID, makeHaCode, splitHaCode, packetObjectCrc, encodePacket, decodePacket, objectsFor, createPacketStreamParser, type PacketObject, type DecodedPacketObject, type PacketParse, } from "./packet.js";
|
|
9
|
+
export { encodePoeSerialFrame, createPoeSerialParser, poeSerialValueFromText, type PoeSerialItem } from "./poeSerial.js";
|
|
10
|
+
export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser, slcanOpenCommands, type CanFrame, type CanMessage, } from "./can.js";
|
|
11
|
+
export { HEADER_CODES, DEFAULT_ARGUMENT_CODES, parseArgumentDictionary, nameByCode } from "./dictionary.js";
|
package/dist/protocols/index.js
CHANGED
|
@@ -11,6 +11,10 @@ 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
|
-
export { encodePoeSerialFrame, createPoeSerialParser } from "./poeSerial.js";
|
|
16
|
+
export { encodePoeSerialFrame, createPoeSerialParser, poeSerialValueFromText } from "./poeSerial.js";
|
|
17
|
+
/* CAN/CAN FD через slcan-адаптер: нарезка пакета на кадры как в can_service прошивки, кадры slcan, потоковый разбор со сборкой фрагментов */
|
|
18
|
+
export { CAN_FINAL_BIT, CAN_PACKET_ID_MASK, canFragments, encodeSlcanFrame, encodeCanPacket, createCanParser, slcanOpenCommands, } from "./can.js";
|
|
19
|
+
/* Словарь заголовков и аргументов HA_Code — по умолчанию из Dictionary.yaml прошивок, разбор файла словаря */
|
|
20
|
+
export { HEADER_CODES, DEFAULT_ARGUMENT_CODES, parseArgumentDictionary, nameByCode } from "./dictionary.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
|
+
};
|
package/dist/protocols/packet.js
CHANGED
|
@@ -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
|
+
};
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
по HEADER+ARGUMENT+VALUE (crc8RohcUtf8), FHS (свободная куча) только у кадров устройства. Устройство шлёт VALUE текстом (лог или hex-дамп CBOR),
|
|
4
4
|
устройству VALUE уходит сырыми байтами CBOR. Между кадрами — текст: строки Teleplot «>Имя:Значение» (PLOT) и прочий вывод (RAW) */
|
|
5
5
|
import { crc8RohcUtf8 } from "./crc.js";
|
|
6
|
+
import { jsonToCbor } from "./cbor/json.js";
|
|
6
7
|
export const SOH = 0x01;
|
|
7
8
|
export const STX = 0x02;
|
|
8
9
|
export const ETX = 0x03;
|
|
@@ -110,3 +111,20 @@ export const createPoeSerialParser = () => {
|
|
|
110
111
|
},
|
|
111
112
|
};
|
|
112
113
|
};
|
|
114
|
+
/* Поле Value команды из ввода пользователя: пусто — без тела, hex-строка (с пробелами или без) — готовые байты как есть, иначе JSON → CBOR
|
|
115
|
+
(двоичные данные — base64, ключи с BIN — байты, см. jsonToCbor); не hex и не JSON — ошибка */
|
|
116
|
+
export const poeSerialValueFromText = (text) => {
|
|
117
|
+
const trimmed = (text ?? "").trim();
|
|
118
|
+
if (!trimmed)
|
|
119
|
+
return new Uint8Array();
|
|
120
|
+
if (/^([0-9A-Fa-f]{2}\s?)+$/.test(trimmed))
|
|
121
|
+
return Uint8Array.from(trimmed.replace(/\s/g, "").match(/../g) ?? [], (pair) => parseInt(pair, 16));
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = JSON.parse(trimmed);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new Error(`Value не является ни hex, ни JSON: ${trimmed}`);
|
|
128
|
+
}
|
|
129
|
+
return jsonToCbor(parsed);
|
|
130
|
+
};
|