From 70cbc72f74ccd7bcf76430a28ac99d70eae36a10 Mon Sep 17 00:00:00 2001 From: Claas Date: Sun, 30 Aug 2026 23:17:56 +0200 Subject: [PATCH] Rebuild the serial tool on SolidJS 2.0 and pnpm, starting with the Modbus core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Svelte 4 app with a SolidJS 2.0 (RC) one on Vite 8, managed with pnpm, and rewrites the protocol layer from scratch rather than porting it. The UI is a support gate for now; the devices and the interface come in the layers above. The protocol layer is the part worth reviewing: - `crc.ts` — the CRC-16, and the fact that it is the one little-endian field in Modbus. - `pdu.ts` — how long a frame is and what it says, per function code. Request and response are different shapes for the same code and neither announces which it is, so length inference and role are kept separate. - `monitor.ts` — recovering frames from a bus this app is not driving, which the old tool could not do at all. RTU has no start byte, no length field and no terminator; frames are separated by 3.5 characters of silence that neither Web Serial nor the CH340 preserves. So timing is only a hint here. What actually recovers the framing is that a frame is self-checking: the function code narrows the end to one or two candidates and the CRC says which was right, while the single-master alternation of request and response breaks the tie between two 8-byte frames. Tests are built on frames copied out of the manufacturers' own manuals, check bytes included — those agree only if this code agrees with the devices rather than with itself. That paid for itself immediately: both humidity frames in docs/temperature-sensor.md carry wrong check bytes, which the test found. Both are corrected there, with a note saying why and how to recompute them. The monitor also grew a `suspect` state while under test. A frame length read out of a byte whose frame has not been checked can be nonsense, and waiting on it swallowed every good frame behind it in the same burst. It now looks further along for a frame that does check out, but only once the shape it was actually expecting has been checked and disproven — otherwise a long response still arriving would be cut apart on a coincidental checksum. Both halves of that are tested. Co-Authored-By: Claude Opus 5 --- docs/temperature-sensor.md | 15 +- serial/.vscode/extensions.json | 2 +- serial/README.md | 73 +- serial/index.html | 19 +- serial/package-lock.json | 1591 ------------------- serial/package.json | 28 +- serial/pnpm-lock.yaml | 1368 ++++++++++++++++ serial/public/vite.svg | 1 - serial/src/App.svelte | 17 - serial/src/App.tsx | 38 + serial/src/app.css | 75 + serial/src/assets/svelte.svg | 1 - serial/src/index.tsx | 11 + serial/src/lib/BaudRateSelect.svelte | 17 - serial/src/lib/DeviceView.svelte | 53 - serial/src/lib/PortDevices.svelte | 110 -- serial/src/lib/Register.svelte | 72 - serial/src/lib/SerialPortSettings.svelte | 94 -- serial/src/lib/SerialPortsList.svelte | 79 - serial/src/lib/crc.ts | 29 - serial/src/lib/devices/fan.ts | 448 ------ serial/src/lib/devices/temperatureSensor.ts | 135 -- serial/src/lib/modbus.ts | 375 ----- serial/src/lib/supportedDevices.ts | 15 - serial/src/main.ts | 8 - serial/src/modbus/crc.test.ts | 74 + serial/src/modbus/crc.ts | 73 + serial/src/modbus/frames.fixture.ts | 68 + serial/src/modbus/monitor.test.ts | 231 +++ serial/src/modbus/monitor.ts | 269 ++++ serial/src/modbus/pdu.test.ts | 194 +++ serial/src/modbus/pdu.ts | 316 ++++ serial/src/vite-env.d.ts | 2 - serial/svelte.config.js | 7 - serial/tsconfig.json | 33 +- serial/tsconfig.node.json | 9 - serial/vite.config.ts | 19 +- 37 files changed, 2825 insertions(+), 3144 deletions(-) delete mode 100644 serial/package-lock.json create mode 100644 serial/pnpm-lock.yaml delete mode 100644 serial/public/vite.svg delete mode 100644 serial/src/App.svelte create mode 100644 serial/src/App.tsx delete mode 100644 serial/src/assets/svelte.svg create mode 100644 serial/src/index.tsx delete mode 100644 serial/src/lib/BaudRateSelect.svelte delete mode 100644 serial/src/lib/DeviceView.svelte delete mode 100644 serial/src/lib/PortDevices.svelte delete mode 100644 serial/src/lib/Register.svelte delete mode 100644 serial/src/lib/SerialPortSettings.svelte delete mode 100644 serial/src/lib/SerialPortsList.svelte delete mode 100644 serial/src/lib/crc.ts delete mode 100644 serial/src/lib/devices/fan.ts delete mode 100644 serial/src/lib/devices/temperatureSensor.ts delete mode 100644 serial/src/lib/modbus.ts delete mode 100644 serial/src/lib/supportedDevices.ts delete mode 100644 serial/src/main.ts create mode 100644 serial/src/modbus/crc.test.ts create mode 100644 serial/src/modbus/crc.ts create mode 100644 serial/src/modbus/frames.fixture.ts create mode 100644 serial/src/modbus/monitor.test.ts create mode 100644 serial/src/modbus/monitor.ts create mode 100644 serial/src/modbus/pdu.test.ts create mode 100644 serial/src/modbus/pdu.ts delete mode 100644 serial/src/vite-env.d.ts delete mode 100644 serial/svelte.config.js delete mode 100644 serial/tsconfig.node.json diff --git a/docs/temperature-sensor.md b/docs/temperature-sensor.md index 05af4ef..fc71a46 100644 --- a/docs/temperature-sensor.md +++ b/docs/temperature-sensor.md @@ -76,15 +76,26 @@ Temperature value = 0x131, converted to decimal 305, actual temperature value = #### The host reads the humidity command frame (0x04): +> [!WARNING] +> **Both frames in this humidity section had wrong check bytes in the source we transcribed from, +> and both have been corrected below.** The request was printed as `0xC1 0xCA` but `01 04 00 02 00 +> 01` checksums to `0x90 0x0A`; the response was printed as `0xD1 0xBA` but `01 04 02 02 22` +> checksums to `0x38 0x49`. +> +> Every other frame in this file is correct, as is every frame in the relay manual. They were all +> checked against the CRC implementation in +> [serial/src/modbus/crc.ts](../serial/src/modbus/crc.ts) by `serial/src/modbus/crc.test.ts`, which +> is where the mistake surfaced. Do not "fix" these back without recomputing them. + | Slave address | function code | Register address High byte | Register address Low byte | Number of registers High byte | Number of registers Low byte | CRC High byte | CRC Low byte | | ------------- | ------------- | -------------------------- | ------------------------- | ----------------------------- | ---------------------------- | ------------- | ------------ | -| 0x01 | 0x04 | 0x00 | 0x02 | 0x00 | 0x01 | 0xC1 | 0xCA | +| 0x01 | 0x04 | 0x00 | 0x02 | 0x00 | 0x01 | 0x90 | 0x0A | #### The slave responds to the data frame: | Slave address | function code | Number of bytes | humidity High byte | humidity Low byte | CRC High byte | CRC Low byte | | ------------- | ------------- | --------------- | ------------------ | ----------------- | ------------- | ------------ | -| 0x01 | 0x04 | 0x02 | 0x02 | 0x22 | 0xD1 | 0xBA | +| 0x01 | 0x04 | 0x02 | 0x02 | 0x22 | 0x38 | 0x49 | Humidity value=0x222, converted to decimal 546, actual humidity value=546 / 10 = 54.6%; diff --git a/serial/.vscode/extensions.json b/serial/.vscode/extensions.json index bdef820..1b39e50 100644 --- a/serial/.vscode/extensions.json +++ b/serial/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["svelte.svelte-vscode"] + "recommendations": ["solidjs-community.solid-snippets", "vitest.explorer"] } diff --git a/serial/README.md b/serial/README.md index ceeb933..efffbe8 100644 --- a/serial/README.md +++ b/serial/README.md @@ -1,38 +1,47 @@ # serial -A browser tool for talking to the RS-485/Modbus RTU devices on this project's bus, using the -[Web Serial API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API). It opens a USB -serial adapter straight from the page, reads input registers and reads *and writes* holding -registers — which is how a device gets configured before it is wired into the fan controller. +A browser tool for the RS-485/Modbus RTU bus this project's devices sit on, using the +[Web Serial API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API). It does two +things a firmware build cannot: -It was folded in from [SantaClaas/serial](https://github.com/SantaClaas/serial), where it lived as -its own repository. +- **Watches the bus.** Decodes the traffic between the fan controller and the fans without joining + in, the way [debug-listener](../debug-listener) does from the desktop — but framed and decoded + rather than printed as bytes. +- **Talks to a device directly.** Reads input registers and reads *and writes* holding registers, + which is how a device gets its address, baud rate and correction values set before it is wired in. + +Only one of those at a time per port: watching is passive and polling is not, and a half-duplex +RS-485 line has room for one master. ## Running it ```bash -cd serial && npm install && npm run dev +cd serial && pnpm install && pnpm dev ``` +`pnpm test` runs the protocol tests, `pnpm check` type-checks, `pnpm build` does both and produces +`dist/`. + The Web Serial API is Chromium-only (Chrome, Edge, Opera) and needs a secure context, so `localhost` -or HTTPS. The app says so itself when the API is missing rather than failing silently. +or HTTPS. The app says so itself rather than failing silently. + +## Layout -`npm run check` type-checks the Svelte components. +| Path | What it is | +|---|---| +| `src/modbus/crc.ts` | The CRC-16 every RTU frame ends with. The one little-endian field in the protocol. | +| `src/modbus/pdu.ts` | Frame lengths, decoding and building, per function code. | +| `src/modbus/monitor.ts` | Recovering frames from a bus you are not driving. | +| `src/modbus/frames.fixture.ts` | Frames copied out of the device manuals, CRCs and all. | -## What it does today +Everything under `src/modbus` is plain TypeScript over bytes with no DOM in it, so it is tested +directly with `vitest`. The tests are built on frames the *manufacturers* printed, which is the +strongest check available: they agree only if this code agrees with the devices rather than merely +with itself. -- **Add Port** calls `navigator.serial.requestPort()` filtered to USB vendor `0x1A86` / product - `0x7523` — the CH340 adapter. That filter is hardcoded in `src/lib/SerialPortsList.svelte`, so a - different adapter will not show up in the picker. -- Baud rate and parity are chosen per port before opening it. The RadiCal fans want 19 200 baud with - **even** parity; the temperature sensor defaults to 9600 with **no** parity. A port can only speak - one of those at a time. -- Devices are added to an open port by Modbus address. Each device type declares its registers - once — address, length, how to decode the bytes, and for holding registers how to encode and - validate them — and the UI is generated from that declaration - (`src/lib/modbus.ts`, `src/lib/devices/`). -- Two device types are implemented: the ebm-papst RadiCal fan (`devices/fan.ts`) and the temperature - sensor (`devices/temperatureSensor.ts`). +> That is not hypothetical. The humidity frames in +> [docs/temperature-sensor.md](../docs/temperature-sensor.md) had wrong check bytes, and the test +> comparing them against `crc.ts` is what found it. Both are corrected there now. ## Where the device documentation lives @@ -42,18 +51,14 @@ or HTTPS. The app says so itself when the API is missing rather than failing sil - **Temperature sensor** — [docs/temperature-sensor.md](../docs/temperature-sensor.md). No manufacturer PDF exists for it; that file is all there is. -The register names in `devices/fan.ts` are working translations of the German manual made before -that manual was checked in, and several are flagged as guesses in the source (`PhaseControlFactor` -for *Aussteuergrad*, `CurrentDesiredEffect` for *Aktueller Wirksinn*). Prefer the terminology in the -RadiCal documentation over these. +The German terms in the RadiCal manual are the reference for what a register is called — *Aussteuergrad*, +*Wirksinn*, *Sollwert* — rather than any translation invented here. -## Status +## Stack -This is Svelte 4 on Vite 4 and is slated to be rewritten from scratch on SolidJS, adding passive -listening of bus traffic in the manner of [debug-listener](../debug-listener). Treat the current -code as reference for what the devices do, not as the shape the rewrite should take. +SolidJS 2.0 (release candidate) on Vite, with pnpm. Solid 2.0 differs from 1.x in ways that matter +when reading this code: -The GitHub Pages deployment workflow that shipped with the standalone repository was dropped during -the fold-in: GitHub only reads workflows from the repository root, so it would never have run from -`serial/.github/`. It needs to be re-created at the repository root, scoped to this directory, when -the rewrite is ready to deploy. +- `createEffect` takes **two** functions, a compute and an effect: `createEffect(() => signal(), value => …)`. + The single-argument form from 1.x is gone and is typed `never`. +- DOM rendering lives in `@solidjs/web`, not `solid-js/web`, and that is also the `jsxImportSource`. diff --git a/serial/index.html b/serial/index.html index 1de9aa6..1fc8d22 100644 --- a/serial/index.html +++ b/serial/index.html @@ -1,16 +1,13 @@ - - + - - Web Serial API - - - + + Modbus Serial Tool + +
- - - - \ No newline at end of file + + + diff --git a/serial/package-lock.json b/serial/package-lock.json deleted file mode 100644 index b3e0da2..0000000 --- a/serial/package-lock.json +++ /dev/null @@ -1,1591 +0,0 @@ -{ - "name": "web-serial-api", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "web-serial-api", - "version": "0.0.0", - "dependencies": { - "@types/dom-serial": "^1.0.3" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.2", - "@tsconfig/svelte": "^5.0.0", - "svelte": "^4.0.5", - "svelte-check": "^3.4.6", - "tslib": "^2.6.0", - "typescript": "^5.0.2", - "vite": "^4.4.5" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.5.tgz", - "integrity": "sha512-UJKsFNwhzCVuiZd06jM/psscyNJNDwjQC+qIeb7GBJK9iWeQCcIyfcPWDvbCudfcJggY9jtxJeeaZH7uny93FQ==", - "dev": true, - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^1.0.3", - "debug": "^4.3.4", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.2", - "svelte-hmr": "^0.15.3", - "vitefu": "^0.2.4" - }, - "engines": { - "node": "^14.18.0 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.54.0 || ^4.0.0", - "vite": "^4.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz", - "integrity": "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==", - "dev": true, - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": "^14.18.0 || >= 16" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.2.0", - "svelte": "^3.54.0 || ^4.0.0", - "vite": "^4.0.0" - } - }, - "node_modules/@tsconfig/svelte": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.2.tgz", - "integrity": "sha512-BRbo1fOtyVbhfLyuCWw6wAWp+U8UQle+ZXu84MYYWzYSEB28dyfnRBIE99eoG+qdAC0po6L2ScIEivcT07UaMA==", - "dev": true - }, - "node_modules/@types/dom-serial": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/dom-serial/-/dom-serial-1.0.3.tgz", - "integrity": "sha512-iuuXTHSUQfosDJTN3AxTHfYtDZVehzAc07P0d6Y6/D0mNMoAzSQcAB8cf3glzH8MfMpZx8JDQv9C+PBpILVI3g==" - }, - "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", - "dev": true - }, - "node_modules/@types/pug": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.6.tgz", - "integrity": "sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/axobject-query": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", - "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==", - "dev": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/code-red": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", - "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@types/estree": "^1.0.1", - "acorn": "^8.10.0", - "estree-walker": "^3.0.3", - "periscopic": "^3.1.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/es6-promise": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dev": true, - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "dev": true - }, - "node_modules/magic-string": { - "version": "0.30.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.3.tgz", - "integrity": "sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/rollup": { - "version": "3.29.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.1.tgz", - "integrity": "sha512-c+ebvQz0VIH4KhhCpDsI+Bik0eT8ZFEVZEYw0cGMVqIP8zc+gnwl7iXCamTw7vzv2MeuZFZfdx5JJIq+ehzDlg==", - "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "dev": true, - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/sander": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", - "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", - "dev": true, - "dependencies": { - "es6-promise": "^3.1.2", - "graceful-fs": "^4.1.3", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.2" - } - }, - "node_modules/sorcery": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.0.tgz", - "integrity": "sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "buffer-crc32": "^0.2.5", - "minimist": "^1.2.0", - "sander": "^0.5.0" - }, - "bin": { - "sorcery": "bin/sorcery" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "dependencies": { - "min-indent": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/svelte": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.0.tgz", - "integrity": "sha512-kVsdPjDbLrv74SmLSUzAsBGquMs4MPgWGkGLpH+PjOYnFOziAvENVzgJmyOCV2gntxE32aNm8/sqNKD6LbIpeQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.1", - "@jridgewell/sourcemap-codec": "^1.4.15", - "@jridgewell/trace-mapping": "^0.3.18", - "acorn": "^8.9.0", - "aria-query": "^5.3.0", - "axobject-query": "^3.2.1", - "code-red": "^1.0.3", - "css-tree": "^2.3.1", - "estree-walker": "^3.0.3", - "is-reference": "^3.0.1", - "locate-character": "^3.0.0", - "magic-string": "^0.30.0", - "periscopic": "^3.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/svelte-check": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.5.1.tgz", - "integrity": "sha512-+Zb4iHxAhdUtcUg/WJPRjlS1RJalIsWAe9Mz6G1zyznSs7dDkT7VUBdXc3q7Iwg49O/VrZgyJRvOJkjuBfKjFA==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.17", - "chokidar": "^3.4.1", - "fast-glob": "^3.2.7", - "import-fresh": "^3.2.1", - "picocolors": "^1.0.0", - "sade": "^1.7.4", - "svelte-preprocess": "^5.0.4", - "typescript": "^5.0.3" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "peerDependencies": { - "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0" - } - }, - "node_modules/svelte-hmr": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.15.3.tgz", - "integrity": "sha512-41snaPswvSf8TJUhlkoJBekRrABDXDMdpNpT2tfHIv4JuhgvHqLMhEPGtaQn0BmbNSTkuz2Ed20DF2eHw0SmBQ==", - "dev": true, - "engines": { - "node": "^12.20 || ^14.13.1 || >= 16" - }, - "peerDependencies": { - "svelte": "^3.19.0 || ^4.0.0" - } - }, - "node_modules/svelte-preprocess": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.0.4.tgz", - "integrity": "sha512-ABia2QegosxOGsVlsSBJvoWeXy1wUKSfF7SWJdTjLAbx/Y3SrVevvvbFNQqrSJw89+lNSsM58SipmZJ5SRi5iw==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@types/pug": "^2.0.6", - "detect-indent": "^6.1.0", - "magic-string": "^0.27.0", - "sorcery": "^0.11.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">= 14.10.0" - }, - "peerDependencies": { - "@babel/core": "^7.10.2", - "coffeescript": "^2.5.1", - "less": "^3.11.3 || ^4.0.0", - "postcss": "^7 || ^8", - "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0", - "pug": "^3.0.0", - "sass": "^1.26.8", - "stylus": "^0.55.0", - "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0", - "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "coffeescript": { - "optional": true - }, - "less": { - "optional": true - }, - "postcss": { - "optional": true - }, - "postcss-load-config": { - "optional": true - }, - "pug": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/svelte-preprocess/node_modules/magic-string": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", - "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true - }, - "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", - "dev": true, - "dependencies": { - "esbuild": "^0.18.10", - "postcss": "^8.4.27", - "rollup": "^3.27.1" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@types/node": ">= 14", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.4.tgz", - "integrity": "sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==", - "dev": true, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - } - } -} diff --git a/serial/package.json b/serial/package.json index 3f2c7b5..0ccc283 100644 --- a/serial/package.json +++ b/serial/package.json @@ -1,24 +1,26 @@ { - "name": "web-serial-api", + "name": "serial", "private": true, "version": "0.0.0", "type": "module", + "packageManager": "pnpm@11.10.0", "scripts": { "dev": "vite", - "build": "vite build", + "build": "tsc --noEmit && vite build", "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^2.4.2", - "@tsconfig/svelte": "^5.0.0", - "svelte": "^4.0.5", - "svelte-check": "^3.4.6", - "tslib": "^2.6.0", - "typescript": "^5.0.2", - "vite": "^4.4.5" + "check": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { - "@types/dom-serial": "^1.0.3" + "@solidjs/web": "2.0.0-rc.4", + "solid-js": "2.0.0-rc.4" + }, + "devDependencies": { + "@solidjs/vite-plugin": "3.0.0-next.36", + "@types/dom-serial": "^1.0.6", + "typescript": "^5.9.3", + "vite": "^8.2.2", + "vitest": "^4.1.11" } } diff --git a/serial/pnpm-lock.yaml b/serial/pnpm-lock.yaml new file mode 100644 index 0000000..5f42549 --- /dev/null +++ b/serial/pnpm-lock.yaml @@ -0,0 +1,1368 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@solidjs/web': + specifier: 2.0.0-rc.4 + version: 2.0.0-rc.4(solid-js@2.0.0-rc.4) + solid-js: + specifier: 2.0.0-rc.4 + version: 2.0.0-rc.4 + devDependencies: + '@solidjs/vite-plugin': + specifier: 3.0.0-next.36 + version: 3.0.0-next.36(@solidjs/web@2.0.0-rc.4(solid-js@2.0.0-rc.4))(solid-js@2.0.0-rc.4)(vite@8.2.2) + '@types/dom-serial': + specifier: ^1.0.6 + version: 1.0.6 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^8.2.2 + version: 8.2.2 + vitest: + specifier: ^4.1.11 + version: 4.1.11(vite@8.2.2) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.18.6': + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} + + '@rolldown/binding-android-arm-eabi@1.2.6': + resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.6': + resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.6': + resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.6': + resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.6': + resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.6': + resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.6': + resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.6': + resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.6': + resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.6': + resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@solidjs/babel-plugin@2.0.0-rc.4': + resolution: {integrity: sha512-4RYR4PWAlQIz1dmIyPheaUvVb9EnSIw5KIGsEISmHod5qfT1IPVKgtb9bctyQ5FeDAuc7JcpbkFYN2heLrk6rQ==} + peerDependencies: + '@babel/core': ^7.20.12 + + '@solidjs/compiler-darwin-arm64@2.0.0-rc.4': + resolution: {integrity: sha512-YR4T15ucsV1Vb4J6yaHxss2hakdcbZ353ye53AhX+FWNayuEhO/Cyjdtb9uS38Dfzysok/rIEQdaeZcaXt+rPA==} + cpu: [arm64] + os: [darwin] + + '@solidjs/compiler-darwin-x64@2.0.0-rc.4': + resolution: {integrity: sha512-95SkVSyXP2eh7PvPWi+fTeH5aTcTwOUvRV7ubeUrutE4akW3ndq1N8cvg2WZ+LhKrf2pvDO0eSDEZ72I769CLg==} + cpu: [x64] + os: [darwin] + + '@solidjs/compiler-linux-arm64-gnu@2.0.0-rc.4': + resolution: {integrity: sha512-36Ht+EUAVUt8fAxq8zlfPS0+vZ61hfJRJDiX1HDNXvlAwZE1B7FH/ym1wfXrhG1TvrNA63Sjjj5CeccYO2H4Tg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@solidjs/compiler-linux-x64-gnu@2.0.0-rc.4': + resolution: {integrity: sha512-DSFOQEjfYmmRQ70pNLamRXPNQ+aP+5vyh1rMjSKwAzONt0xB2rcy9Alt02GU38gztW9YN6ZyxM4rwkYe+7MfaQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@solidjs/compiler-wasm32-wasi@2.0.0-rc.4': + resolution: {integrity: sha512-zVxXS+01rbv+0yOp2vaGjIu0om5I1UhhPpzNYeE+AmfpVc2eCWxqDsPai3dIxFDGPTjaJ+npwjKJRmOTaoknRw==} + engines: {node: '>=14.0.0'} + + '@solidjs/compiler-win32-x64-msvc@2.0.0-rc.4': + resolution: {integrity: sha512-DkzTHMZXzaiL/KhL76UJWN24pKkkXeLpPYtDxUqX6VaKuOCPDmp/m7GLWSDrqCdF7leuYsig2MuMJTXZRLOkbQ==} + cpu: [x64] + os: [win32] + + '@solidjs/compiler@2.0.0-rc.4': + resolution: {integrity: sha512-lKx6Jp1KbHxqO+v+g7cRbm8I1DHx/10Lj8bKG4vmdGnCfSlFbyhCd8cPTLdLV0YVG7GGSzzF5m8NkC9NlfGN5w==} + + '@solidjs/signals@2.0.0-rc.4': + resolution: {integrity: sha512-l7P0g8+2pnNscaIPOGDMhv0boaidGAsvR8QN66JySIWNMvVnuq2ayv7ZnvP+IR00DJC+RqCNnnTG/UCdxf+h8g==} + + '@solidjs/vite-plugin@3.0.0-next.36': + resolution: {integrity: sha512-PMreKx9IeQwSRt6xt8LQHe58Gk79CVaB57PQMlvXUdbyL5JliaehOSg5a4W6bKoJfeWku7nryEWUvTDKyZ2PBg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@solidjs/start-devtools': ^1.0.0-next.2 + '@solidjs/web': ^2.0.0-rc.0 + '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* + solid-js: ^2.0.0-rc.0 + vite: ^8.0.0 || ^9.0.0 + peerDependenciesMeta: + '@solidjs/start-devtools': + optional: true + '@testing-library/jest-dom': + optional: true + + '@solidjs/web@2.0.0-rc.4': + resolution: {integrity: sha512-acM9W0FByf0aJDMG5kMbHr0BLTjvFIg8lxwLwHzsu3ybWSmsP1FiOOs2RHNItve/AiX+MGR9CQBCazmnfPykEA==} + peerDependencies: + solid-js: ^2.0.0-rc.4 + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/dom-serial@1.0.6': + resolution: {integrity: sha512-eUHKbc6mdMgMm75/oBLocs3wkOQkPQ/oNCT+b5OgUT6mLgIvDTp3wCCE9tYZNvDPPh6Cj9lVg2IguWfS/mDrrQ==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + baseline-browser-mapping@2.11.20: + resolution: {integrity: sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==} + engines: {node: '>=6.0.0'} + hasBin: true + + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + electron-to-chromium@1.5.416: + resolution: {integrity: sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + merge-anything@5.1.7: + resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} + engines: {node: '>=12.13'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} + + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + + rolldown@1.2.6: + resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + seroval-plugins@1.5.6: + resolution: {integrity: sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.6: + resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==} + engines: {node: '>=10'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + solid-js@2.0.0-rc.4: + resolution: {integrity: sha512-hSkDmtduesjFtvzjNyqLphrqiSvhSD5+njkPQUR+9YEoR60MMWtuv36SbLL4OucI0Ronof3Jc+AsUp0oWffwMg==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + validate-html-nesting@1.2.4: + resolution: {integrity: sha512-doQi7e8EJ2OWneSG1aZpJluS6A49aZM0+EICXWKm1i6WvqTLmq0tpUcImc4KTWG50mORO0C4YDBtOCSYvElftw==} + + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.8 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.18.6': + dependencies: + '@babel/types': 7.29.8 + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@emnapi/core@1.11.3': + dependencies: + '@emnapi/wasi-threads': 1.2.3 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@oxc-project/types@0.147.0': {} + + '@rolldown/binding-android-arm-eabi@1.2.6': + optional: true + + '@rolldown/binding-android-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.6': + optional: true + + '@rolldown/binding-darwin-x64@1.2.6': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.6': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.6': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.6': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.6': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.6': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.6': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.6': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@solidjs/babel-plugin@2.0.0-rc.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.8 + html-entities: 2.3.3 + parse5: 7.3.0 + validate-html-nesting: 1.2.4 + + '@solidjs/compiler-darwin-arm64@2.0.0-rc.4': + optional: true + + '@solidjs/compiler-darwin-x64@2.0.0-rc.4': + optional: true + + '@solidjs/compiler-linux-arm64-gnu@2.0.0-rc.4': + optional: true + + '@solidjs/compiler-linux-x64-gnu@2.0.0-rc.4': + optional: true + + '@solidjs/compiler-wasm32-wasi@2.0.0-rc.4': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + optional: true + + '@solidjs/compiler-win32-x64-msvc@2.0.0-rc.4': + optional: true + + '@solidjs/compiler@2.0.0-rc.4': + optionalDependencies: + '@solidjs/compiler-darwin-arm64': 2.0.0-rc.4 + '@solidjs/compiler-darwin-x64': 2.0.0-rc.4 + '@solidjs/compiler-linux-arm64-gnu': 2.0.0-rc.4 + '@solidjs/compiler-linux-x64-gnu': 2.0.0-rc.4 + '@solidjs/compiler-wasm32-wasi': 2.0.0-rc.4 + '@solidjs/compiler-win32-x64-msvc': 2.0.0-rc.4 + + '@solidjs/signals@2.0.0-rc.4': {} + + '@solidjs/vite-plugin@3.0.0-next.36(@solidjs/web@2.0.0-rc.4(solid-js@2.0.0-rc.4))(solid-js@2.0.0-rc.4)(vite@8.2.2)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/core': 7.29.7 + '@solidjs/babel-plugin': 2.0.0-rc.4(@babel/core@7.29.7) + '@solidjs/compiler': 2.0.0-rc.4 + '@solidjs/web': 2.0.0-rc.4(solid-js@2.0.0-rc.4) + '@types/babel__core': 7.20.5 + merge-anything: 5.1.7 + solid-js: 2.0.0-rc.4 + vite: 8.2.2 + vitefu: 1.1.3(vite@8.2.2) + transitivePeerDependencies: + - supports-color + + '@solidjs/web@2.0.0-rc.4(solid-js@2.0.0-rc.4)': + dependencies: + seroval: 1.5.6 + seroval-plugins: 1.5.6(seroval@1.5.6) + solid-js: 2.0.0-rc.4 + + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/dom-serial@1.0.6': {} + + '@types/estree@1.0.9': {} + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.2.2)': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.2 + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + assertion-error@2.0.1: {} + + baseline-browser-mapping@2.11.20: {} + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.416 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.8) + + caniuse-lite@1.0.30001810: {} + + chai@6.2.2: {} + + convert-source-map@2.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: {} + + electron-to-chromium@1.5.416: {} + + entities@6.0.1: {} + + es-module-lexer@2.3.2: {} + + escalade@3.2.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + html-entities@2.3.3: {} + + is-what@4.1.16: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + merge-anything@5.1.7: + dependencies: + is-what: 4.1.16 + + ms@2.1.3: {} + + nanoid@3.3.18: {} + + node-releases@2.0.54: {} + + obug@2.1.4: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rolldown@1.2.6: + dependencies: + '@oxc-project/types': 0.147.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.6 + '@rolldown/binding-android-arm64': 1.2.6 + '@rolldown/binding-darwin-arm64': 1.2.6 + '@rolldown/binding-darwin-x64': 1.2.6 + '@rolldown/binding-freebsd-x64': 1.2.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 + '@rolldown/binding-linux-arm64-gnu': 1.2.6 + '@rolldown/binding-linux-arm64-musl': 1.2.6 + '@rolldown/binding-linux-ppc64-gnu': 1.2.6 + '@rolldown/binding-linux-s390x-gnu': 1.2.6 + '@rolldown/binding-linux-x64-gnu': 1.2.6 + '@rolldown/binding-linux-x64-musl': 1.2.6 + '@rolldown/binding-openharmony-arm64': 1.2.6 + '@rolldown/binding-win32-arm64-msvc': 1.2.6 + '@rolldown/binding-win32-x64-msvc': 1.2.6 + + semver@6.3.1: {} + + seroval-plugins@1.5.6(seroval@1.5.6): + dependencies: + seroval: 1.5.6 + + seroval@1.5.6: {} + + siginfo@2.0.0: {} + + solid-js@2.0.0-rc.4: + dependencies: + '@solidjs/signals': 2.0.0-rc.4 + csstype: 3.2.3 + seroval: 1.5.6 + seroval-plugins: 1.5.6(seroval@1.5.6) + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + tinybench@2.9.0: {} + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + tslib@2.8.1: + optional: true + + typescript@5.9.3: {} + + update-browserslist-db@1.3.2(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + validate-html-nesting@1.2.4: {} + + vite@8.2.2: + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.26 + rolldown: 1.2.6 + tinyglobby: 0.2.17 + optionalDependencies: + fsevents: 2.3.3 + + vitefu@1.1.3(vite@8.2.2): + optionalDependencies: + vite: 8.2.2 + + vitest@4.1.11(vite@8.2.2): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.2.2) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.2 + why-is-node-running: 2.3.0 + transitivePeerDependencies: + - msw + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + yallist@3.1.1: {} diff --git a/serial/public/vite.svg b/serial/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/serial/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/serial/src/App.svelte b/serial/src/App.svelte deleted file mode 100644 index 6fc53f3..0000000 --- a/serial/src/App.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -{#if "serial" in navigator} - -{:else} -

Your Browser is not supported

-

- This app requires the Web Serial API to interact with serial devices which - not supported by your Browser. - You can check if your browser is supported here. -

-

As of writing Chrome, Edge and Opera support the API.

-{/if} diff --git a/serial/src/App.tsx b/serial/src/App.tsx new file mode 100644 index 0000000..d9a0d2d --- /dev/null +++ b/serial/src/App.tsx @@ -0,0 +1,38 @@ +import { Show } from "solid-js"; + +/** + * Whether this browser can talk to a serial port at all. + * + * Read once rather than reactively: the Web Serial API either is or is not in the browser, and it + * does not appear part-way through a session. It is also gated on a secure context, so a page + * served over plain HTTP from anything but localhost lands here too + */ +const isSupported = "serial" in navigator; + +export default function App() { + return ( +
+

Modbus Serial Tool

+ + }> +

Web Serial is available.

+
+
+ ); +} + +function Unsupported() { + return ( +
+

This browser cannot open a serial port

+

+ The tool needs the Web Serial API, + which is only in Chromium-based browsers — Chrome, Edge and Opera — and only on a secure origin. +

+

+ If you are on one of those and still see this, check that the page is served over HTTPS or from{" "} + localhost. +

+
+ ); +} diff --git a/serial/src/app.css b/serial/src/app.css index e69de29..2bd1c6f 100644 --- a/serial/src/app.css +++ b/serial/src/app.css @@ -0,0 +1,75 @@ +/* + * Both schemes are defined as tokens on :root and swapped under prefers-color-scheme, so the page + * follows the operating system without any script and without a flash on load. + */ +:root { + --surface: #ffffff; + --surface-sunken: #f4f5f7; + --surface-raised: #ffffff; + --border: #d8dbe0; + --text: #16181d; + --text-dim: #5c6270; + --accent: #1c5fd6; + --danger: #b3261e; + + color-scheme: light dark; + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; + line-height: 1.5; +} + +@media (prefers-color-scheme: dark) { + :root { + --surface: #16181d; + --surface-sunken: #101216; + --surface-raised: #1e2128; + --border: #333944; + --text: #e8eaee; + --text-dim: #9aa2b1; + --accent: #79a9ff; + --danger: #f2837c; + } +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--surface-sunken); + color: var(--text); +} + +main { + max-width: 70rem; + margin-inline: auto; + padding: 1.5rem 1rem 4rem; +} + +h1 { + font-size: 1.5rem; + margin-block: 0 1.5rem; +} + +a { + color: var(--accent); +} + +code { + font-family: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, monospace; + font-size: 0.9em; +} + +.notice { + background: var(--surface-raised); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 1rem 1.25rem; +} + +.notice h2 { + font-size: 1.1rem; + margin-block-start: 0; +} diff --git a/serial/src/assets/svelte.svg b/serial/src/assets/svelte.svg deleted file mode 100644 index c5e0848..0000000 --- a/serial/src/assets/svelte.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/serial/src/index.tsx b/serial/src/index.tsx new file mode 100644 index 0000000..53e8739 --- /dev/null +++ b/serial/src/index.tsx @@ -0,0 +1,11 @@ +import { render } from "@solidjs/web"; +import App from "./App"; +import "./app.css"; + +const mount = document.getElementById("app"); + +// Thrown rather than narrowed away, because a missing mount point means index.html and this file +// have drifted apart — a build mistake, not a runtime condition to handle +if (!mount) throw new Error("index.html is missing the #app element"); + +render(() => , mount); diff --git a/serial/src/lib/BaudRateSelect.svelte b/serial/src/lib/BaudRateSelect.svelte deleted file mode 100644 index c0d2d18..0000000 --- a/serial/src/lib/BaudRateSelect.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/serial/src/lib/DeviceView.svelte b/serial/src/lib/DeviceView.svelte deleted file mode 100644 index 6ef8fd0..0000000 --- a/serial/src/lib/DeviceView.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - -{#if countInputRegisters} -
- Input registers - {#each Object.entries(device.inputRegisters) as [label, register]} - - {/each} -
-{/if} - -{#if countHoldingRegisters} -
- Holding registers - {#each Object.entries(device.holdingRegisters) as [label, register]} - - {/each} -
-{/if} - -{#if !countInputRegisters && !countInputRegisters} -

No registers defined for this device

-{/if} - - diff --git a/serial/src/lib/PortDevices.svelte b/serial/src/lib/PortDevices.svelte deleted file mode 100644 index 6e3b6a1..0000000 --- a/serial/src/lib/PortDevices.svelte +++ /dev/null @@ -1,110 +0,0 @@ - - -

Modbus

-
-
- Add Device - - - - - - - - -
-
- -{#each devices as [name, device]} -
-

{name}

-

Address: {device.address}

-
- -{/each} - - diff --git a/serial/src/lib/Register.svelte b/serial/src/lib/Register.svelte deleted file mode 100644 index ad0c9b0..0000000 --- a/serial/src/lib/Register.svelte +++ /dev/null @@ -1,72 +0,0 @@ - - - - -{#if register instanceof HoldingRegister} - {#if register.input} - {#if "options" in register.input} - - {:else if "min" in register.input} - - {/if} - {:else} - - {/if} -{:else} - -{/if} - - -{#if register instanceof HoldingRegister} - -{/if} diff --git a/serial/src/lib/SerialPortSettings.svelte b/serial/src/lib/SerialPortSettings.svelte deleted file mode 100644 index 0c922a5..0000000 --- a/serial/src/lib/SerialPortSettings.svelte +++ /dev/null @@ -1,94 +0,0 @@ - - -
- Serial Port Settings - - - - - - - - - - -
- - -
-
- -{#if isOpen} - -{/if} - - diff --git a/serial/src/lib/SerialPortsList.svelte b/serial/src/lib/SerialPortsList.svelte deleted file mode 100644 index 0b7b730..0000000 --- a/serial/src/lib/SerialPortsList.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - -
-

Serial Ports

-
    - {#each availablePorts as [port, info]} - {#key port} -
  • - -
  • - {/key} - {/each} -
- - -
- - diff --git a/serial/src/lib/crc.ts b/serial/src/lib/crc.ts deleted file mode 100644 index 518b558..0000000 --- a/serial/src/lib/crc.ts +++ /dev/null @@ -1,29 +0,0 @@ -function swapEndianness16(number: number) { - // Source: https://stackoverflow.com/questions/5320439/how-do-i-swap-endian-ness-byte-order-of-a-variable-in-javascript - return ((number & 0xff) << 8) | ((number >> 8) & 0xff); -} - -/** - * Creates the cyclic redundancy check for the given data - * @param data - * @returns big endian crc - */ -export function crc16(data: Uint8Array): number { - let crc = 0xffff; - let polynomial = 0xa001; - - for (let i = 0; i < data.length; i++) { - crc ^= data[i]; - - for (let j = 0; j < 8; j++) { - if ((crc & 1) === 1) { - crc >>= 1; - crc ^= polynomial; - } else { - crc >>= 1; - } - } - } - - return swapEndianness16(crc); -} diff --git a/serial/src/lib/devices/fan.ts b/serial/src/lib/devices/fan.ts deleted file mode 100644 index 6ba2b1d..0000000 --- a/serial/src/lib/devices/fan.ts +++ /dev/null @@ -1,448 +0,0 @@ -import { Device } from "../modbus"; - -function identity(value: T) { - return value; -} -enum InputRegisterAddress { - Identification = 0xd000, - MaxMessageLength = 0xd001, - SoftwareNameBusController = 0xd002, - SoftwareVersionBusController = 0xd003, - SoftwareNameCommutationController = 0xd004, - SoftwareVersionCommutationController = 0xd005, - CurrentRpm = 0xd010, - MotorStatus = 0xd011, - Warning = 0xd012, - /** - * Also known as DC link voltage - * German: Zwischenkreisspannung - */ - VoltageIntermediateCircuit = 0xd013, - /** - * Also known as DC link current - * German: Zwischenkreisstrom - */ - CurrentIntermediateCircuit = 0xd014, - MotorTemperature = 0xd016, - ElectronicInteriorTemperature = 0xd017, - /** - * German: Aussteuergrad - * Note: Not sure on translation as I lack the technical knowledge - */ - PhaseControlFactor = 0xd019, - /** - * Set point? - * German: Aktueller Sollwert - */ - CurrentTargetValue = 0xd01a, - /** - * German: Sensoristwert - */ - CurrentSensorValue = 0xd01b, - /** - * German: Sensoristwert 1 - */ - CurrentSensorValue1 = 0xd023, - - TemperatureSensor1 = 0xd02e, - HumiditySensor1 = 0xd02f, - TemperatureSensor2 = 0xd030, - HumiditySensor2 = 0xd031, - - /** - * German: Flügelradanemometer - */ - RpmVaneAnemometer, - - VolumeFlowAbsolute = 0xd033, - VolumeFlowRelative = 0xd035, - - /** - * Mass flow kg/h - */ - MassFlowAbsolute = 0xd034, - MassFlowRelative = 0xd036, - Pt1000TemperatureSensor1 = 0xd038, - Pt1000TemperatureSensor2 = 0xd039, - StateEnableInput = 0xd01c, - /** - * German: Aktueller Wirksinn - */ - CurrentDesiredEffect = 0xd01e, - CurrentPowerRelative = 0xd021, - /** - * Absolute in Watt - */ - CurrentPowerAbsolute = 0xd027, - CurrentTargetValueSource = 0xd028, - /** - * Energt consumption meter LSB (Least significant byte (or bit?)) - */ - EnergyConsumptionMeterLsb = 0xd029, - - /** - * Energt consumption meter MSB (most significant byte (or bit?)) - */ - EnergyConsumptionMeterMsb = 0xd029, - Heartbeat = 0xd037, - MirroredInputRegister1 = 0xd100, - MirroredInputRegister2 = 0xd101, - MirroredInputRegister3 = 0xd102, - MirroredInputRegister4 = 0xd103, - MirroredInputRegister5 = 0xd104, - MirroredInputRegister6 = 0xd105, - MirroredInputRegister7 = 0xd106, - MirroredInputRegister8 = 0xd107, - MirroredInputRegister9 = 0xd108, - MirroredInputRegister10 = 0xd109, - MirroredInputRegister11 = 0xd10a, - MirroredInputRegister12 = 0xd10b, - MirroredInputRegister13 = 0xd10c, - MirroredInputRegister14 = 0xd10d, - MirroredInputRegister15 = 0xd10e, - MirroredInputRegister16 = 0xd1f, -} - -enum HoldingRegisterAddress { - Reset = 0xd000, - DefaultTargetValue = 0xd001, - PasswordRegister1 = 0xd002, - PasswordRegister2 = 0xd003, - PasswordRegister3 = 0xd004, - FactorySettingControl = 0xd005, - CustomerSettingControl = 0xd006, - OperatingHoursCounter = 0xd009, - OperatingMinutesCounter = 0xd00a, - AddressingFunctionOnOff = 0xd00c, - SavedTargetValue = 0xd00d, - EnableRs485 = 0xd00f, - FanAddress = 0xd100, - TargetValueSource = 0xd101, - PreferredRunningDirection = 0xd102, - TargetValueSaving = 0xd103, - OperatingMode = 0xd106, - /** - * German: Wirksinn - */ - DesiredEffect = 0xd108, - ControlParameterPFactor = 0xd10a, - ControlParameterKFactor = 0xd10c, - /** - * German: Maximaler Aussteuergrad - */ - MaximumPhaseControlFactor = 0xd10e, - /** - * German: Minimum Aussteuergrad - */ - MinimumPhaseControlFactor = 0xd110, - MotorStopEnable = 0xd112, - StartPhaseControlFactor = 0xd116, - MaximumAllowedPhaseControlFactor = 0xd117, - MinimumAllowedPhaseControlFactor = 0xd118, - MaximumRpm = 0xd119, - MaximumAllowedRpm = 0xd11a, - /** - * German: Hochlauframpe - */ - AccelerationRamp = 0xd11f, - /** - * German: Auslauframpe - */ - DeccelarationRamp = 0xd120, - RpmLimit = 0xd128, - InputCharacteristicCurvePoint1X = 0xd12a, - InputCharacteristicCurvePoint1Y = 0xd12b, - InputCharacteristicCurvePoint2X = 0xd12c, - InputCharacteristicCurvePoint2Y = 0xd12d, - MaximumPower = 0xd155, - MaximumAllowedPower = 0xd135, - RpmLimitActionMonitoring = 0xd145, - SensorCurrentValueSource = 0xd147, - BaudRate = 0xd149, - Parity = 0xd14a, - EmergencyRunningDirection = 0xd15b, - EmergencyTargetValueSource = 0xd15c, - EmergencyTargetValue = 0xd15d, - EmergencyDelay = 0xd15e, - InputCharacteristicLimitCableBreak = 0xd15f, - MinimumSensorValue1 = 0xd160, - MinimumSensorValue2 = 0xd161, - MaximumSensorValue1 = 0xd161, - MaximumSensorValue2 = 0xd161, - SensorAddress1 = 0xd165, - SensorAddress2 = 0xd165, - SensorAddress3 = 0xd166, - SensorAddress4 = 0xd167, - SensorAddress5 = 0xd168, - SensorAddress6 = 0xd169, - EnableSource = 0xd16a, - SavedEnableRs485 = 0xd16b, - CustomerData1 = 0xd170, - CustomerData2 = 0xd171, - CustomerData3 = 0xd172, - CustomerData4 = 0xd173, - CustomerData5 = 0xd174, - CustomerData6 = 0xd175, - CustomerData7 = 0xd176, - CustomerData8 = 0xd177, - CustomerData9 = 0xd178, - CustomerData10 = 0xd179, - CustomerData11 = 0xd17a, - CustomerData12 = 0xd17b, - CustomerData13 = 0xd17c, - CustomerData14 = 0xd17d, - CustomerData15 = 0xd17e, - CustomerData16 = 0xd17f, - - ErrorPointer = 0xd182, - - Error1 = 0xd184, - Error1Time = 0xd185, - - ErrorHistory1 = 0xd186, - ErrorHistory1Time = 0xd187, - - ErrorHistory2 = 0xd188, - ErrorHistory2Time = 0xd189, - - ErrorHistory3 = 0xd18a, - ErrorHistory3Time = 0xd18b, - - ErrorHistory4 = 0xd18c, - ErrorHistory4Time = 0xd18d, - - ErrorHistory5 = 0xd18e, - ErrorHistory5Time = 0xd18f, - - ErrorHistory6 = 0xd190, - ErrorHistory6Time = 0xd191, - - ErrorHistory7 = 0xd192, - ErrorHistory7Time = 0xd193, - - ErrorHistory8 = 0xd194, - ErrorHistory8Time = 0xd195, - - ErrorHistory9 = 0xd196, - ErrorHistory9Time = 0xd197, - - ErrorHistory10 = 0xd198, - ErrorHistory10Time = 0xd199, - - ErrorHistory11 = 0xd19a, - ErrorHistory11Time = 0xd19b, - - ErrorHistory12 = 0xd19c, - ErrorHistory12Time = 0xd19d, - - ErrorHistory13 = 0xd19e, - ErrorHistory13Time = 0xd19f, - - VoltageIntermediateCircuitReferenceValue = 0xd1a0, - CurrentIntermediateCircuitReferenceValue = 0xd1a1, - SerialNumber1 = 0xd1a2, - SerialNumber2 = 0xd1a3, - ProductionDate = 0xd1a4, - VolumeFlowReferenceValue = 0xd1ed, - MassFlowReferenceValue = 0xd1ee, - - /** - * The original address register stores the register address of the register that should be mirrored. - * Mirrored registers help sequencially reading registers that might not be next to each other - */ - MirroredHoldingRegister1OriginalAddress = 0xd480, - MirroredHoldingRegister2OriginalAddress = 0xd481, - MirroredHoldingRegister3OriginalAddress = 0xd482, - MirroredHoldingRegister4OriginalAddress = 0xd483, - MirroredHoldingRegister5OriginalAddress = 0xd484, - MirroredHoldingRegister6OriginalAddress = 0xd485, - MirroredHoldingRegister7OriginalAddress = 0xd486, - MirroredHoldingRegister8OriginalAddress = 0xd487, - MirroredHoldingRegister9OriginalAddress = 0xd488, - MirroredHoldingRegister10OriginalAddress = 0xd489, - MirroredHoldingRegister11OriginalAddress = 0xd48a, - MirroredHoldingRegister12OriginalAddress = 0xd48b, - MirroredHoldingRegister13OriginalAddress = 0xd48c, - MirroredHoldingRegister14OriginalAddress = 0xd48d, - MirroredHoldingRegister15OriginalAddress = 0xd48e, - MirroredHoldingRegister16OriginalAddress = 0xd48f, - MirroredHoldingRegister17OriginalAddress = 0xd490, - MirroredHoldingRegister18OriginalAddress = 0xd491, - MirroredHoldingRegister19OriginalAddress = 0xd492, - MirroredHoldingRegister20OriginalAddress = 0xd493, - MirroredHoldingRegister21OriginalAddress = 0xd494, - MirroredHoldingRegister22OriginalAddress = 0xd495, - MirroredHoldingRegister23OriginalAddress = 0xd496, - MirroredHoldingRegister24OriginalAddress = 0xd497, - MirroredHoldingRegister25OriginalAddress = 0xd498, - MirroredHoldingRegister26OriginalAddress = 0xd499, - MirroredHoldingRegister27OriginalAddress = 0xd49a, - MirroredHoldingRegister28OriginalAddress = 0xd49b, - MirroredHoldingRegister29OriginalAddress = 0xd49c, - MirroredHoldingRegister30OriginalAddress = 0xd49d, - MirroredHoldingRegister31OriginalAddress = 0xd49e, - MirroredHoldingRegister32OriginalAddress = 0xd49f, - - /** - * The registers where the values from the orginal address register can be read - */ - MirroredHoldingRegister1 = 0xd380, - MirroredHoldingRegister2 = 0xd381, - MirroredHoldingRegister3 = 0xd382, - MirroredHoldingRegister4 = 0xd383, - MirroredHoldingRegister5 = 0xd384, - MirroredHoldingRegister6 = 0xd385, - MirroredHoldingRegister7 = 0xd386, - MirroredHoldingRegister8 = 0xd387, - MirroredHoldingRegister9 = 0xd388, - MirroredHoldingRegister10 = 0xd389, - MirroredHoldingRegister11 = 0xd38a, - MirroredHoldingRegister12 = 0xd38b, - MirroredHoldingRegister13 = 0xd38c, - MirroredHoldingRegister14 = 0xd38d, - MirroredHoldingRegister15 = 0xd38e, - MirroredHoldingRegister16 = 0xd38f, - MirroredHoldingRegister17 = 0xd390, - MirroredHoldingRegister18 = 0xd391, - MirroredHoldingRegister19 = 0xd392, - MirroredHoldingRegister20 = 0xd393, - MirroredHoldingRegister21 = 0xd394, - MirroredHoldingRegister22 = 0xd395, - MirroredHoldingRegister23 = 0xd396, - MirroredHoldingRegister24 = 0xd397, - MirroredHoldingRegister25 = 0xd398, - MirroredHoldingRegister26 = 0xd399, - MirroredHoldingRegister27 = 0xd39a, - MirroredHoldingRegister28 = 0xd39b, - MirroredHoldingRegister29 = 0xd39c, - MirroredHoldingRegister30 = 0xd39d, - MirroredHoldingRegister31 = 0xd39e, - MirroredHoldingRegister32 = 0xd39f, - - MirroredInputRegister1OriginalAddress = 0xd400, - MirroredInputRegister2OriginalAddress = 0xd401, - MirroredInputRegister3OriginalAddress = 0xd402, - MirroredInputRegister4OriginalAddress = 0xd403, - MirroredInputRegister5OriginalAddress = 0xd404, - MirroredInputRegister6OriginalAddress = 0xd405, - MirroredInputRegister7OriginalAddress = 0xd406, - MirroredInputRegister8OriginalAddress = 0xd407, - MirroredInputRegister9OriginalAddress = 0xd408, - MirroredInputRegister10OriginalAddress = 0xd409, - MirroredInputRegister11OriginalAddress = 0xd40a, - MirroredInputRegister12OriginalAddress = 0xd40b, - MirroredInputRegister13OriginalAddress = 0xd40c, - MirroredInputRegister14OriginalAddress = 0xd40d, - MirroredInputRegister15OriginalAddress = 0xd40e, - MirroredInputRegister16OriginalAddress = 0xd40f, - - MassFlowHeightAboveSeaLevel = 0xd602, -} - -function toUint16(view: DataView) { - return view.getUint16(0); -} - -export class Fan extends Device { - inputRegisters = { - /** - * The maximum length in bytes a message can have - */ - maxMessageLength: this.createInputRegister({ - label: "Maximum Byte Count", - address: InputRegisterAddress.MaxMessageLength, - length: 2, - deserialize: toUint16, - }), - heartbeat: this.createInputRegister({ - label: "Heartbeat", - address: InputRegisterAddress.Heartbeat, - length: 2, - deserialize: toUint16, - }), - phaseFactor: this.createInputRegister({ - label: "Phase Factor", - address: InputRegisterAddress.PhaseControlFactor, - length: 2, - //TODO interpretation - deserialize: toUint16, - }), - temperatureSensor1: this.createInputRegister({ - label: "Temperature Sensor 1", - address: InputRegisterAddress.TemperatureSensor1, - length: 2, - //TODO interpretation - deserialize: toUint16, - }), - temperatureSensor2: this.createInputRegister({ - label: "Temperature Sensor 2", - address: InputRegisterAddress.TemperatureSensor2, - length: 2, - //TODO interpretation - deserialize: toUint16, - }), - humiditySensor1: this.createInputRegister({ - label: "Humidity Sensor 1", - address: InputRegisterAddress.HumiditySensor1, - length: 2, - deserialize: (view) => { - const value = toUint16(view); - return (value / 65536) * 100; - }, - }), - humiditySensor2: this.createInputRegister({ - label: "Humidity Sensor 2", - address: InputRegisterAddress.HumiditySensor2, - length: 2, - deserialize: (view) => { - const value = toUint16(view); - return (value / 65536) * 100; - }, - }), - volumeFlowAbsolute: this.createInputRegister({ - label: "Volume Flow Absolute", - address: InputRegisterAddress.VolumeFlowAbsolute, - length: 2, - deserialize: toUint16, - }), - }; - - holdingRegisters = { - targetValue: this.createHoldingRegister({ - label: "Target Value (m³/h currently)", - address: HoldingRegisterAddress.DefaultTargetValue, - length: 2, - deserialize: toUint16, - serialize: identity, - }), - volumeFlowReferenceValue: this.createHoldingRegister({ - label: "Volume Flow Reference Value", - address: HoldingRegisterAddress.VolumeFlowReferenceValue, - length: 2, - deserialize: toUint16, - serialize: (value) => { - throw new Error("Not allowed to write to this register"); - }, - }), - operatingMode: this.createHoldingRegister({ - label: "Operating Mode", - address: HoldingRegisterAddress.OperatingMode, - length: 2, - deserialize: toUint16, - serialize: (value) => { - //TODO valid values: 0, 1, 2 - throw new Error("Not implemented"); - }, - }), - sensorCurrentValueSource: this.createHoldingRegister({ - label: "Sensor Current Value Source", - address: HoldingRegisterAddress.SensorCurrentValueSource, - length: 2, - deserialize: toUint16, - serialize: (value) => { - //TODO valid values: 0, 1, 2 - throw new Error("Not implemented"); - }, - }), - }; -} diff --git a/serial/src/lib/devices/temperatureSensor.ts b/serial/src/lib/devices/temperatureSensor.ts deleted file mode 100644 index 5750a23..0000000 --- a/serial/src/lib/devices/temperatureSensor.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Device } from "../modbus"; - -export function isValidAddress(value: any): value is number { - // Device address (1~247) - return Number.isInteger(value) && value < 248 && value > 0; -} - -/** - * Input register addresses for the temperature sensor - */ -enum InputRegisterAddress { - Temperature = 0x0001, - Humidity = 0x0002, -} - -/** - * Holding register addresses for the temperature sensor - */ -enum HoldingRegisterAddress { - DeviceAddress = 0x0101, - BaudRate = 0x0102, - TemperatureCorrectionValue = 0x0103, - HumidityCorrectionValue = 0x0104, -} - -export const supportedBaudRates = { - 9600: "9600 Bit/s", - 14400: "14400 Bit/s", - 19200: "19200 Bit/s", -} as const; - -type TemperatureSensorBaudRate = keyof typeof supportedBaudRates; - -export function isValidBaudRate( - baudRate: any -): baudRate is TemperatureSensorBaudRate { - return baudRate in supportedBaudRates; -} - -export function isValidCorrectionValue(value: any): value is number { - // Value can only have 1 decimal place - return Number.isInteger(value * 10) && value > -10 && value < 10; -} -function toUint16(view: DataView) { - return view.getUint16(0); -} -function toBaudRate(view: DataView) { - return view.getUint16(0) as TemperatureSensorBaudRate; -} - -function toTemperature(view: DataView) { - return view.getInt16(0) / 10; -} - -function fromTemperature(temperature: number) { - return temperature * 10; -} - -function identity(value: T) { - return value; -} - -export class TemperatureSensor extends Device { - inputRegisters = { - temperature: this.createInputRegister({ - label: "Temperature (℃)", - address: InputRegisterAddress.Temperature, - length: 2, - deserialize: toTemperature, - }), - - humidity: this.createInputRegister({ - label: "Humidity (%)", - address: InputRegisterAddress.Humidity, - length: 2, - deserialize: toTemperature, - }), - }; - - holdingRegisters = { - deviceAddress: this.createHoldingRegister({ - label: "Address", - address: HoldingRegisterAddress.DeviceAddress, - length: 2, - deserialize: toUint16, - serialize: identity, - isValid: isValidAddress, - input: { - min: 1, - max: 247, - step: 1, - }, - }), - - baudRate: this.createHoldingRegister({ - label: "Baud Rate", - address: HoldingRegisterAddress.BaudRate, - length: 2, - deserialize: toBaudRate, - serialize: identity, - isValid: isValidBaudRate, - input: { - options: supportedBaudRates, - }, - }), - - temperatureCorrection: this.createHoldingRegister({ - label: "Temperature Correction Value", - address: HoldingRegisterAddress.TemperatureCorrectionValue, - length: 2, - deserialize: toTemperature, - serialize: fromTemperature, - isValid: isValidCorrectionValue, - input: { - min: -10, - max: 10, - step: 0.1, - }, - }), - - humidityCorrection: this.createHoldingRegister({ - label: "Humidity Correction Value", - address: HoldingRegisterAddress.HumidityCorrectionValue, - length: 2, - deserialize: toTemperature, - serialize: fromTemperature, - isValid: isValidCorrectionValue, - input: { - min: -10, - max: 10, - step: 0.1, - }, - }), - }; -} diff --git a/serial/src/lib/modbus.ts b/serial/src/lib/modbus.ts deleted file mode 100644 index 31b19fb..0000000 --- a/serial/src/lib/modbus.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { crc16 } from "./crc"; - -export enum FunctionCode { - ReadHoldingRegister = 0x03, - ReadInputRegister = 0x04, - WriteSingleHoldingRegister = 0x06, - Diagnostics = 0x08, - WriteMultipleHoldingRegisters = 0x10, -} - -/** - * Validates that the given value is a valid device address between 1 and 247 - */ -function isValidAddress(value: any): value is number { - // Device address (1-247), 0 is broadcast - return Number.isInteger(value) && value < 248 && value > 0; -} - -/** - * Validates a response to determine that the response is intended for us as it is a shared bus medium - */ -function isReadResponseValid( - response: { view: DataView; frame: Uint8Array }, - expectedAddress: number, - expectedFunctionCode: FunctionCode, - expectedDataLength: number -) { - const crcStartIndex = response.frame.byteLength - 2; - const crc = crc16(response.frame.subarray(0, crcStartIndex)); - const expectedCrc = response.view.getUint16(5); - - if (crc !== expectedCrc) return false; - - const slaveAddress = response.view.getUint8(0); - if (expectedAddress !== slaveAddress) return false; - - const functionCode = response.view.getUint8(1); - if (functionCode !== expectedFunctionCode) return false; - - const dataLength = response.view.getUint8(2); - - return dataLength === expectedDataLength && 2 + dataLength < crcStartIndex; -} - -/** - * Validates that the response to a write is valid, that is the response mirrors the command - */ -function isWriteResponseValid(frame: Uint8Array, response: Uint8Array) { - if (frame.length !== response.length) return false; - - for (let index = 0; index < frame.length; index++) { - if (frame[index] !== response[index]) return false; - } - - return true; -} - -type SerializeFunction = (value: T) => number; -type DeserializeFunction = (view: DataView) => T; - -type RegisterDefinition = { - /** - * An optional human readable label or name for the register - */ - label?: string; - - /** - * A valid register address is a unsigned 16 bit number - */ - address: number; - - /** - * The length in bytes of the data contained in the register - */ - length: number; - - /** - * A deserializing function that allows customization how the value is read and allows interpretation of the binary - */ - deserialize: DeserializeFunction; -}; - -type InputRegisterDefinition = RegisterDefinition; - -/** - * Input options define what values can be put in a holding register before serialization. - * This is first and foremost intended to validate values that should be written to the holding register but can be used - * to defining HTML input elements - */ -export type InputOptions = - | { - options: { [key: number]: string }; - } - | { min: number; max: number; step: number }; -type HoldingRegisterDefinition = RegisterDefinition & { - serialize: SerializeFunction; - isValid?(value: T): value is T; - input?: InputOptions; -}; - -export class InputRegister { - #device: Device; - #definition: InputRegisterDefinition; - - get label(): string | undefined { - return this.#definition.label; - } - - constructor(device: Device, definition: InputRegisterDefinition) { - this.#device = device; - this.#definition = definition; - } - - async read(signal?: AbortSignal): Promise { - if (signal?.aborted) return; - - const length = 8; - const crcOffset = length - 2; - const frame = new ArrayBuffer(length); - - const view = new DataView(frame); - // Address - view.setUint8(0, this.#device.address); - // Function - view.setUint8(1, FunctionCode.ReadInputRegister); - // Register address - view.setUint16(2, this.#definition.address); - // Number of registers - view.setUint16(4, 1); - // CRC - const crc = crc16(new Uint8Array(frame.slice(0, crcOffset))); - view.setUint16(crcOffset, crc); - - try { - await this.#device.write(frame, signal); - } catch (error: unknown) { - console.warn("Non-fatal write error:", error); - return; - } - - let value; - try { - value = await this.#device.read(signal); - } catch (error: unknown) { - console.warn("Non-fatal read error:", error); - return; - } - - if (!value) return; - - const response = { view: new DataView(value.buffer), frame: value }; - - if ( - !isReadResponseValid( - response, - this.#device.address, - FunctionCode.ReadInputRegister, - this.#definition.length - ) - ) - return; - - const deserializationView = new DataView( - value.buffer, - 3, - this.#definition.length - ); - return this.#definition.deserialize(deserializationView); - } -} - -export class HoldingRegister { - #device: Device; - #definition: HoldingRegisterDefinition; - - get input(): InputOptions | undefined { - return this.#definition.input; - } - - get label(): string | undefined { - return this.#definition.label; - } - - constructor(device: Device, definition: HoldingRegisterDefinition) { - this.#device = device; - this.#definition = definition; - } - - async read(signal?: AbortSignal): Promise { - if (signal?.aborted) return; - const length = 8; - const crcOffset = length - 2; - const frame = new ArrayBuffer(length); - - const view = new DataView(frame); - // Address - view.setUint8(0, this.#device.address); - // Function - view.setUint8(1, FunctionCode.ReadHoldingRegister); - // Register address - view.setUint16(2, this.#definition.address); - // Number of registers - view.setUint16(4, 1); - // CRC - const crc = crc16(new Uint8Array(frame.slice(0, crcOffset))); - view.setUint16(crcOffset, crc); - - try { - await this.#device.write(frame, signal); - } catch (error: unknown) { - console.warn("Non-fatal write error:", error); - return; - } - - let value; - try { - value = await this.#device.read(signal); - } catch (error: unknown) { - console.warn("Non-fatal read error:", error); - return; - } - - if (!value) return; - - const response = { view: new DataView(value.buffer), frame: value }; - if ( - !isReadResponseValid( - response, - this.#device.address, - FunctionCode.ReadHoldingRegister, - this.#definition.length - ) - ) - return; - - const deserializationView = new DataView( - value.buffer, - 3, - this.#definition.length - ); - - return this.#definition.deserialize(deserializationView); - } - - async write(value: T, signal?: AbortSignal) { - if ( - signal?.aborted || - (this.#definition.isValid && !this.#definition.isValid(value)) - ) - return false; - const length = 8; - const crcOffset = length - 2; - const frame = new Uint8Array(length); - const view = new DataView(frame.buffer); - // Address - view.setUint8(0, this.#device.address); - // Function - view.setUint8(1, FunctionCode.WriteSingleHoldingRegister); - // Register address - view.setUint16(2, this.#definition.address); - // Register value - view.setInt16(4, this.#definition.serialize(value)); - // CRC - const crc = crc16(new Uint8Array(frame.slice(0, crcOffset))); - view.setUint16(crcOffset, crc); - - try { - await this.#device.write(frame, signal); - } catch (error: unknown) { - console.warn("Non-fatal write error:", error); - return false; - } - - let responseFrame; - try { - responseFrame = await this.#device.read(signal); - } catch (error: unknown) { - console.warn("Non-fatal read error:", error); - return false; - } - - if (!responseFrame) return false; - - return isWriteResponseValid(frame, responseFrame); - } -} - -export abstract class Device { - address: number; - #port: SerialPort; - - static name: string; - abstract inputRegisters: { readonly [name: string]: InputRegister }; - abstract holdingRegisters: { - readonly [name: string]: HoldingRegister; - }; - - /** - * - * @param port The serial port used to write to write the holding register on the modbus device - * @param address the 8 bit modbus device address - */ - constructor(port: SerialPort, address: number) { - this.address = address; - this.#port = port; - } - - /** - * - * @param definition the object describing the register - */ - protected createInputRegister( - definition: InputRegisterDefinition - ): InputRegister { - return new InputRegister(this, definition); - } - - /** - * - * @param definition the object describing the register - */ - protected createHoldingRegister(definition: HoldingRegisterDefinition) { - return new HoldingRegister(this, definition); - } - - async write(frame: Uint8Array | ArrayBuffer, signal?: AbortSignal) { - const writer = this.#port.writable.getWriter(); - function cancel() { - writer.abort(signal?.reason); - } - - signal?.addEventListener("abort", cancel, { once: true }); - - try { - await writer.write(frame); - } finally { - signal?.removeEventListener("abort", cancel); - writer.releaseLock(); - } - } - - async read(signal?: AbortSignal): Promise { - if (signal?.aborted) return; - - const reader = this.#port.readable.getReader(); - function cancel() { - reader.cancel(signal?.reason); - } - - signal?.addEventListener("abort", cancel, { once: true }); - - try { - const { value, done } = await reader.read(); - - if (done) { - console.log("Done reading"); - return; - } - - if (!value) { - console.log("No value"); - return; - } - - if (!(value instanceof Uint8Array)) - throw new Error("Expected value read to be binary"); - - console.log("Read value: ", value); - return value; - } finally { - signal?.removeEventListener("abort", cancel); - reader.releaseLock(); - } - } -} diff --git a/serial/src/lib/supportedDevices.ts b/serial/src/lib/supportedDevices.ts deleted file mode 100644 index fc5cdaa..0000000 --- a/serial/src/lib/supportedDevices.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Fan } from "./devices/fan"; -import { TemperatureSensor } from "./devices/temperatureSensor"; -import type { Device } from "./modbus"; - -// export interface DeviceConstructor { -// new (port: SerialPort, address: number): Device; -// } - -export const supportedDevices = { - "Temperature Sensor": TemperatureSensor, - Fan: Fan, -}; - -type ValueOf = T[keyof T]; -export type DeviceConstructor = ValueOf; diff --git a/serial/src/main.ts b/serial/src/main.ts deleted file mode 100644 index 8a909a1..0000000 --- a/serial/src/main.ts +++ /dev/null @@ -1,8 +0,0 @@ -import './app.css' -import App from './App.svelte' - -const app = new App({ - target: document.getElementById('app'), -}) - -export default app diff --git a/serial/src/modbus/crc.test.ts b/serial/src/modbus/crc.test.ts new file mode 100644 index 0000000..0faa4c2 --- /dev/null +++ b/serial/src/modbus/crc.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; +import { append, crc16, isValid, read } from "./crc"; +import { bytes, relay, temperatureSensor } from "./frames.fixture"; + +describe("crc16", () => { + /** + * The canonical Modbus check value: the CRC of the ASCII bytes "123456789". If this is wrong, + * nothing else here is worth checking + */ + it("matches the standard check value", () => { + expect(crc16(new TextEncoder().encode("123456789"))).toBe(0x4b37); + }); + + it("seeds with 0xFFFF, so an empty range is the seed itself", () => { + expect(crc16(new Uint8Array(0))).toBe(0xffff); + }); +}); + +describe("isValid", () => { + /** + * Every frame the two manuals print, checked against the check bytes their authors published. + * These agreeing is the real evidence the implementation is right + */ + it.each([ + ...Object.entries(relay), + ...Object.entries(temperatureSensor), + ])("accepts the manufacturer's frame %s", (_name, frame) => { + expect(isValid(frame)).toBe(true); + }); + + it("rejects a frame with a corrupted payload", () => { + const corrupted = Uint8Array.from(relay.readAddressResponse); + corrupted[4] = corrupted[4]! ^ 0x01; + + expect(isValid(corrupted)).toBe(false); + }); + + it("rejects a frame with a corrupted check byte", () => { + const corrupted = Uint8Array.from(relay.readAddressResponse); + corrupted[corrupted.length - 1] = corrupted[corrupted.length - 1]! ^ 0x01; + + expect(isValid(corrupted)).toBe(false); + }); + + it("rejects anything too short to be a frame", () => { + expect(isValid(bytes("FF 03 02"))).toBe(false); + expect(isValid(new Uint8Array(0))).toBe(false); + }); +}); + +describe("byte order", () => { + /** + * The one part of Modbus that is little-endian. Everything else — register addresses, counts, + * values — goes high byte first, and only the CRC is reversed + */ + it("puts the CRC on the wire low byte first", () => { + // The manual prints the check code for this frame as 0x99E4, sent as 99 then E4 + const frame = relay.closeRelayRequest; + + expect(read(frame, frame.length - 2)).toBe(0xe499); + expect(frame[frame.length - 2]).toBe(0x99); + expect(frame[frame.length - 1]).toBe(0xe4); + }); + + it("append fills the last two bytes so the frame validates", () => { + const frame = Uint8Array.from(relay.readAddressRequest); + frame[frame.length - 2] = 0; + frame[frame.length - 1] = 0; + + append(frame); + + expect(frame).toEqual(relay.readAddressRequest); + }); +}); diff --git a/serial/src/modbus/crc.ts b/serial/src/modbus/crc.ts new file mode 100644 index 0000000..95c2074 --- /dev/null +++ b/serial/src/modbus/crc.ts @@ -0,0 +1,73 @@ +/** + * The CRC-16 every Modbus RTU frame ends with. + * + * It is the reflected algorithm: polynomial 0xA001 (0x8005 bit-reversed), seeded with 0xFFFF, no + * final inversion. On the wire the two bytes go out **low byte first**, which is the opposite + * order from every other 16-bit field in the protocol — see `append` and `read`. + */ + +/** + * The table costs 512 bytes and turns the per-byte inner loop into one lookup. Built once at + * module load rather than written out as a literal, so there is nothing to mistype + */ +const TABLE = /* @__PURE__ */ (() => { + const table = new Uint16Array(256); + + for (let byte = 0; byte < 256; byte++) { + let crc = byte; + + for (let bit = 0; bit < 8; bit++) { + crc = crc & 1 ? (crc >> 1) ^ 0xa001 : crc >> 1; + } + + table[byte] = crc; + } + + return table; +})(); + +/** The CRC of `bytes`, or of the slice `[start, end)` of it */ +export function crc16(bytes: Uint8Array, start = 0, end = bytes.length): number { + let crc = 0xffff; + + for (let index = start; index < end; index++) { + // The loop bound is the array's own length, so the index is always in range. `!` rather than a + // branch that can never be taken + const byte = bytes[index]!; + crc = (crc >> 8) ^ TABLE[(crc ^ byte) & 0xff]!; + } + + return crc; +} + +/** + * Whether a whole frame — address, PDU and the two CRC bytes — checks out. + * + * A frame shorter than four bytes cannot be one: an address, a function code and a CRC is already + * four, and there is no function code without at least one more byte after it + */ +export function isValid(frame: Uint8Array): boolean { + if (frame.length < 4) return false; + + const expected = crc16(frame, 0, frame.length - 2); + return expected === read(frame, frame.length - 2); +} + +/** Reads a CRC from `frame` at `offset`, low byte first */ +export function read(frame: Uint8Array, offset: number): number { + return frame[offset]! | (frame[offset + 1]! << 8); +} + +/** + * Writes the CRC of everything before `offset` into `frame` at `offset`, low byte first. + * + * Takes the frame with its last two bytes already reserved rather than returning a new array, so + * that building a frame is one allocation + */ +export function append(frame: Uint8Array): void { + const offset = frame.length - 2; + const crc = crc16(frame, 0, offset); + + frame[offset] = crc & 0xff; + frame[offset + 1] = crc >> 8; +} diff --git a/serial/src/modbus/frames.fixture.ts b/serial/src/modbus/frames.fixture.ts new file mode 100644 index 0000000..3a3f451 --- /dev/null +++ b/serial/src/modbus/frames.fixture.ts @@ -0,0 +1,68 @@ +/** + * Frames copied out of the device manuals, CRCs and all. + * + * These are worth more than frames computed here would be: the manufacturer wrote the bytes *and* + * the check bytes, so a test built on them fails if this implementation disagrees with the device + * rather than merely with itself. + * + * Relay frames: LC-Modbus-1R-D7 manual, "Modbus RTU instruction introduction". + * Temperature sensor frames: docs/temperature-sensor.md, "MODBUS command frame". + */ + +/** `"FF 05 00 00"` → bytes. Whitespace is ignored, so frames can be written as the manual prints them */ +export function bytes(hex: string): Uint8Array { + const digits = hex.replace(/\s+/g, ""); + const out = new Uint8Array(digits.length / 2); + + for (let index = 0; index < out.length; index++) { + out[index] = Number.parseInt(digits.slice(index * 2, index * 2 + 2), 16); + } + + return out; +} + +export const relay = { + /** Close relay 1, manual mode */ + closeRelayRequest: bytes("FF 05 00 00 FF 00 99 E4"), + closeRelayResponse: bytes("FF 05 00 00 FF 00 99 E4"), + openRelayRequest: bytes("FF 05 00 00 00 00 D8 14"), + + /** Close all eight relays */ + closeAllRequest: bytes("FF 0F 00 00 00 08 01 FF 30 1D"), + openAllRequest: bytes("FF 0F 00 00 00 08 01 00 70 5D"), + writeCoilsResponse: bytes("FF 0F 00 00 00 08 41 D3"), + + /** Read the device address out of holding register 0 */ + readAddressRequest: bytes("00 03 00 00 00 01 85 DB"), + readAddressResponse: bytes("00 03 02 00 FF C5 C4"), + + readRelayStateRequest: bytes("FF 01 00 00 00 08 28 12"), + readRelayStateResponse: bytes("FF 01 01 01 A1 A0"), + + readInputStateRequest: bytes("FF 02 00 00 00 08 6C 12"), + readInputStateResponse: bytes("FF 02 01 01 51 A0"), + + /** Set the baud rate to 19 200 */ + setBaudRequest: bytes("FF 10 03 E9 00 01 02 00 04 CA 0E"), + setBaudResponse: bytes("FF 10 03 E9 00 01 C5 A7"), + + readBaudRequest: bytes("FF 03 03 E8 00 01 11 A4"), + readBaudResponse: bytes("FF 03 02 00 04 90 53"), +} as const; + +export const temperatureSensor = { + readTemperatureRequest: bytes("01 04 00 01 00 01 60 0A"), + /** 0x0131 → 30.5 °C */ + readTemperatureResponse: bytes("01 04 02 01 31 79 74"), + + // The two humidity frames are the only ones in either manual whose printed check bytes were + // wrong: the request was given as `C1 CA` and the response as `D1 BA`. These are the recomputed + // values, and docs/temperature-sensor.md now carries the same correction and says why + readHumidityRequest: bytes("01 04 00 02 00 01 90 0A"), + /** 0x0222 → 54.6 % */ + readHumidityResponse: bytes("01 04 02 02 22 38 49"), + + /** Both values in one read, which is what the app actually issues */ + readBothRequest: bytes("01 04 00 01 00 02 20 0B"), + readBothResponse: bytes("01 04 04 01 31 02 22 2A CE"), +} as const; diff --git a/serial/src/modbus/monitor.test.ts b/serial/src/modbus/monitor.test.ts new file mode 100644 index 0000000..dbeafc4 --- /dev/null +++ b/serial/src/modbus/monitor.test.ts @@ -0,0 +1,231 @@ +import { describe, expect, it } from "vitest"; +import { append } from "./crc"; +import { bytes, relay, temperatureSensor } from "./frames.fixture"; +import { readInputRegisters } from "./pdu"; +import { BusMonitor, type MonitorEvent, type Observed } from "./monitor"; + +/** Joins frames into one buffer, the way they arrive when the adapter batches a burst */ +function run(chunks: Uint8Array[], options?: { gapMs?: number; at?: (index: number) => number }) { + const monitor = new BusMonitor(options); + const events: MonitorEvent[] = []; + + chunks.forEach((chunk, index) => { + events.push(...monitor.push(chunk, options?.at?.(index) ?? index)); + }); + + return events; +} + +function frames(events: MonitorEvent[]): Observed[] { + return events.filter((event): event is MonitorEvent & Observed => event.type === "frame"); +} + +describe("BusMonitor", () => { + it("recovers a request and its response from one chunk", () => { + const events = run([concat(relay.readBaudRequest, relay.readBaudResponse)]); + const seen = frames(events); + + expect(seen).toHaveLength(2); + expect(seen[0]).toMatchObject({ role: "request", address: 0xff, functionCode: 0x03 }); + expect(seen[1]).toMatchObject({ role: "response", address: 0xff, functionCode: 0x03 }); + expect(seen[1]!.decoded).toEqual({ kind: "registers", of: "holdingRegisters", values: [4] }); + }); + + it("recovers a frame split across chunks", () => { + const whole = concat(relay.readBaudRequest, relay.readBaudResponse); + + // Split in the middle of the request, then again in the middle of the response + const events = run([whole.subarray(0, 3), whole.subarray(3, 11), whole.subarray(11)]); + + expect(frames(events)).toHaveLength(2); + }); + + it("recovers frames arriving one byte at a time", () => { + const whole = concat(relay.readBaudRequest, relay.readBaudResponse); + const events = run(Array.from(whole, (byte) => Uint8Array.of(byte))); + + expect(frames(events)).toHaveLength(2); + }); + + /** + * The reason the monitor tracks whose turn it is. A read request and the echo of a single write + * are both eight bytes, so length alone cannot tell them apart — only the alternation can + */ + it("does not mistake a request for a response of the same length", () => { + const events = run([concat(relay.closeRelayRequest, relay.closeRelayResponse)]); + const seen = frames(events); + + expect(seen.map((frame) => frame.role)).toEqual(["request", "response"]); + }); + + /** + * A read response carries values but never the addresses they came from. Pairing is the only + * way the UI can say which register a number belongs to + */ + it("pairs a response with the request it answers", () => { + const events = run([concat(temperatureSensor.readBothRequest, temperatureSensor.readBothResponse)]); + const seen = frames(events); + + expect(seen[1]!.inReplyTo?.decoded).toEqual({ + kind: "read", + of: "inputRegisters", + start: 0x0001, + quantity: 2, + }); + }); + + it("does not pair a response from a different device", () => { + const events = run([concat(temperatureSensor.readBothRequest, relay.readBaudResponse)]); + const seen = frames(events); + + expect(seen[1]!.inReplyTo).toBeUndefined(); + }); + + /** + * Joining a live bus lands in the middle of a frame, which is the normal case rather than the + * exceptional one: the app connects whenever the user clicks, not between frames + */ + it("resynchronises after joining mid-frame", () => { + const whole = concat(relay.readBaudRequest, relay.readBaudResponse); + // Start three bytes into the request, so the first frame is unrecoverable + const events = run([whole.subarray(3)]); + const seen = frames(events); + + expect(seen).toHaveLength(1); + expect(seen[0]).toMatchObject({ role: "response", functionCode: 0x03 }); + }); + + /** + * The guard on the scan-ahead. A long read response arrives over several chunks, and while it is + * in flight the *request* interpretation of its head is both long enough to check and wrong. If + * that alone were taken as grounds to go looking further along, a legitimate frame would be cut + * apart on nothing more than a coincidental checksum + */ + it("does not cut apart a long response that is still arriving", () => { + // 20 input registers, the shape of a RadiCal status poll + const values = Array.from({ length: 20 }, (_, index) => 0x1000 + index); + const response = registerResponse(0x02, values); + const monitor = new BusMonitor(); + + monitor.push(readRequest(0x02, 0xd010, values.length), 0); + + // Delivered a few bytes at a time, well inside the gap + for (let offset = 0; offset < response.length; offset += 5) { + monitor.push(response.subarray(offset, offset + 5), 1 + offset); + } + + const events = monitor.push(new Uint8Array(0), 100); + expect(events.filter((event) => event.type === "noise")).toHaveLength(0); + + // Re-run in one piece to confirm the frame itself is what we expected all along + const whole = frames(run([concat(readRequest(0x02, 0xd010, values.length), response)])); + expect(whole).toHaveLength(2); + expect(whole[1]!.decoded).toMatchObject({ kind: "registers", values }); + }); + + it("reports the bytes it could not account for instead of dropping them", () => { + const events = run([concat(bytes("DE AD BE EF"), relay.readAddressRequest)]); + + const noise = events.filter((event) => event.type === "noise"); + expect(noise).toHaveLength(1); + expect(frames(events)).toHaveLength(1); + // Reported before the frame that followed it, so the order matches the wire + expect(events[0]!.type).toBe("noise"); + }); + + it("abandons a partial frame after a silence rather than letting it swallow the next one", () => { + const monitor = new BusMonitor({ gapMs: 10 }); + + // Half a request, then nothing for long enough that it can never complete + expect(frames(monitor.push(relay.readBaudRequest.subarray(0, 4), 0))).toHaveLength(0); + + const after = monitor.push(relay.readAddressRequest, 100); + + expect(frames(after)).toHaveLength(1); + expect(frames(after)[0]).toMatchObject({ address: 0x00, functionCode: 0x03 }); + }); + + it("keeps a frame that merely straddles a chunk boundary within the gap", () => { + const monitor = new BusMonitor({ gapMs: 50 }); + + monitor.push(relay.readBaudRequest.subarray(0, 4), 0); + const after = monitor.push(relay.readBaudRequest.subarray(4), 5); + + expect(frames(after)).toHaveLength(1); + }); + + it("decodes a whole conversation with several devices", () => { + const events = run([ + concat( + relay.readRelayStateRequest, + relay.readRelayStateResponse, + temperatureSensor.readBothRequest, + temperatureSensor.readBothResponse, + relay.closeRelayRequest, + relay.closeRelayResponse, + ), + ]); + const seen = frames(events); + + expect(seen).toHaveLength(6); + expect(seen.map((frame) => frame.role)).toEqual([ + "request", + "response", + "request", + "response", + "request", + "response", + ]); + expect(seen.map((frame) => frame.address)).toEqual([0xff, 0xff, 0x01, 0x01, 0xff, 0xff]); + }); + + it("hands back what is left when the port closes", () => { + const monitor = new BusMonitor(); + monitor.push(relay.readBaudRequest.subarray(0, 4), 0); + + const remaining = monitor.flush(1); + + expect(remaining).toHaveLength(1); + expect(remaining[0]).toMatchObject({ type: "noise" }); + }); + + it("copies frames out of the working buffer", () => { + const source = concat(relay.readAddressRequest, relay.readAddressResponse); + const seen = frames(run([source])); + + // Scribbling on the source must not change what was already reported + source.fill(0); + + expect(Array.from(seen[0]!.bytes)).toEqual(Array.from(relay.readAddressRequest)); + }); +}); + +/** A read-input-registers response carrying `values`, built the way a device would */ +function registerResponse(address: number, values: readonly number[]): Uint8Array { + const frame = new Uint8Array(5 + values.length * 2); + const view = new DataView(frame.buffer); + + frame[0] = address; + frame[1] = 0x04; + frame[2] = values.length * 2; + values.forEach((value, index) => view.setUint16(3 + index * 2, value)); + append(frame); + + return frame; +} + +function readRequest(address: number, start: number, quantity: number): Uint8Array { + return readInputRegisters(address, start, quantity); +} + +function concat(...parts: Uint8Array[]): Uint8Array { + const joined = new Uint8Array(parts.reduce((total, part) => total + part.length, 0)); + let offset = 0; + + for (const part of parts) { + joined.set(part, offset); + offset += part.length; + } + + return joined; +} diff --git a/serial/src/modbus/monitor.ts b/serial/src/modbus/monitor.ts new file mode 100644 index 0000000..df27127 --- /dev/null +++ b/serial/src/modbus/monitor.ts @@ -0,0 +1,269 @@ +/** + * Reading a Modbus RTU bus you are not driving. + * + * This is the part `debug-listener` does not attempt — it prints bytes. Turning those bytes back + * into frames is awkward, because RTU has no start byte, no length field and no terminator. Frames + * are separated only by 3.5 characters of silence, which at 19 200 baud is about 2 ms, and neither + * the Web Serial API nor the USB adapter behind it preserves gaps that short: bytes arrive in + * chunks whose boundaries have more to do with USB polling than with the wire. + * + * So timing is a hint here, not the mechanism. What actually recovers the framing is that a frame + * is *self-checking*: guess where it ends, and the CRC tells you whether the guess was right. The + * function code narrows the guessing to one or two candidate lengths (see `pdu.ts`), and a bus has + * exactly one master, so requests and responses alternate. Try the expected one first, fall back + * to the other, and if neither checks out, drop a byte and try again from there. + * + * The result is best-effort by nature. It resynchronises within a frame or two of joining a bus + * mid-conversation, and it says plainly which bytes it could not account for rather than hiding + * them. + */ + +import { isValid } from "./crc"; +import { + decodeRequest, + decodeResponse, + requestLength, + responseLength, + type FrameLength, + type Request, + type Response, +} from "./pdu"; + +export type Role = "request" | "response"; + +/** The longest an RTU frame can be, so any inferred length beyond it is a misread */ +const MAX_FRAME = 256; + +/** + * Silence after which the next byte is assumed to start a fresh request. Far longer than the 3.5 + * character times the specification asks for, because the adapter's own buffering routinely + * stretches the real gap; this is only used to abandon a partial frame that will never complete + */ +const DEFAULT_GAP_MS = 50; + +export type Observed = { + /** When the chunk that completed the frame arrived, not when the frame went out on the wire */ + at: number; + role: Role; + address: number; + functionCode: number; + /** The whole frame including its CRC, copied out of the working buffer */ + bytes: Uint8Array; + /** `null` when the function code is outside what `pdu.ts` understands */ + decoded: Request | Response | null; + /** + * For a response, the request it answers, when one was seen. A read response carries values but + * not the addresses they came from — only the request says that — so without this a register + * dump cannot be labelled + */ + inReplyTo?: Observed; +}; + +/** Bytes the monitor could not fit into any frame, kept rather than silently dropped */ +export type Noise = { + at: number; + bytes: Uint8Array; +}; + +export type MonitorEvent = ({ type: "frame" } & Observed) | ({ type: "noise" } & Noise); + +export class BusMonitor { + // Annotated rather than inferred: chunks handed in by the Web Serial reader are + // `Uint8Array`, and inferring `Uint8Array` from the initialiser + // would make them unassignable here + #buffer: Uint8Array = new Uint8Array(0); + #expecting: Role = "request"; + #lastRequest: Observed | undefined; + #lastArrival: number | undefined; + #pendingNoise: number[] = []; + readonly #gapMs: number; + + constructor(options: { gapMs?: number } = {}) { + this.#gapMs = options.gapMs ?? DEFAULT_GAP_MS; + } + + /** + * Feeds one chunk in and takes out whatever became decidable. + * + * `at` is the arrival time, defaulting to now. It is passed in so tests can drive the clock and + * so a replay can use recorded timestamps + */ + push(chunk: Uint8Array, at: number = performance.now()): MonitorEvent[] { + const events: MonitorEvent[] = []; + + // A long silence means whatever is still in the buffer never completed. Give up on it before + // adding to it, or its bogus inferred length will keep swallowing good frames behind it + const since = this.#lastArrival; + if (since !== undefined && at - since > this.#gapMs && this.#buffer.length > 0) { + this.#discardAll(); + this.#expecting = "request"; + } + this.#lastArrival = at; + + this.#buffer = concat(this.#buffer, chunk); + + while (this.#buffer.length > 0) { + const frame = this.#takeFrame(at); + + if (frame === "wait") break; + + if (frame === "suspect") { + // The shape we were expecting was long enough to check and its CRC failed, so the byte at + // the head is probably not the start of anything — the length we are now waiting on came + // out of a byte we have no reason to trust. Rather than block on it, look for a frame that + // does check out further along. Gated this narrowly on purpose: while a frame we *are* + // expecting is legitimately still arriving, nothing has been disproven and this never runs + if (!this.#resyncAhead()) break; + continue; + } + + if (frame === "resync") { + // The head byte belongs to no frame we can recognise. Set it aside and try again one byte + // along — this is how the monitor recovers from joining mid-frame + this.#pendingNoise.push(this.#buffer[0]!); + this.#buffer = this.#buffer.subarray(1); + continue; + } + + // Any noise accumulated before this frame is reported first, so the order the caller sees + // matches the order the bytes arrived in + this.#flushNoise(at, events); + events.push({ type: "frame", ...frame }); + } + + return events; + } + + /** Everything still held back, for when the port closes and there will be no more bytes */ + flush(at: number = performance.now()): MonitorEvent[] { + const events: MonitorEvent[] = []; + this.#discardAll(); + this.#flushNoise(at, events); + return events; + } + + #takeFrame(at: number): Observed | "wait" | "resync" | "suspect" { + const request = requestLength(this.#buffer); + const response = responseLength(this.#buffer); + + // One master, so the two alternate. Trying the expected shape first is what keeps an 8-byte + // request from being mistaken for the equally 8-byte echo of a write + const candidates: ReadonlyArray = + this.#expecting === "request" + ? [ + [request, "request"], + [response, "response"], + ] + : [ + [response, "response"], + [request, "request"], + ]; + + let mayYetComplete = false; + let expectedWasDisproven = false; + + for (const [length, role] of candidates) { + if (length === "incomplete") { + mayYetComplete = true; + continue; + } + + if (length === "unknown" || length > MAX_FRAME) continue; + + if (this.#buffer.length < length) { + mayYetComplete = true; + continue; + } + + const frame = this.#buffer.subarray(0, length); + if (!isValid(frame)) { + // Long enough to judge, and it failed. Worth remembering only for the shape we expected: + // the other shape failing says little, since we did not expect it in the first place + if (role === this.#expecting) expectedWasDisproven = true; + continue; + } + + this.#buffer = this.#buffer.subarray(length); + return this.#observe(frame, role, at); + } + + if (!mayYetComplete) return "resync"; + + return expectedWasDisproven ? "suspect" : "wait"; + } + + /** + * Looks for a frame that checks out somewhere after the head, for when the head itself is not + * trustworthy. Everything skipped over is reported as noise. + * + * A CRC-16 agrees by chance about once in 65 536 tries, so scanning is not free of false + * positives. It only ever runs while the monitor is already lost, though, where the alternative + * is staying lost until the next silence — and a wrong guess there costs one mislabelled frame, + * which is what staying lost costs anyway + */ + #resyncAhead(): boolean { + for (let offset = 1; offset + 4 <= this.#buffer.length; offset++) { + const ahead = this.#buffer.subarray(offset); + + for (const length of [requestLength(ahead), responseLength(ahead)]) { + if (typeof length !== "number" || length > MAX_FRAME || ahead.length < length) continue; + if (!isValid(ahead.subarray(0, length))) continue; + + for (const byte of this.#buffer.subarray(0, offset)) this.#pendingNoise.push(byte); + this.#buffer = ahead; + return true; + } + } + + return false; + } + + #observe(frame: Uint8Array, role: Role, at: number): Observed { + // Copied, because the working buffer is a view over memory that later chunks will reuse + const bytes = new Uint8Array(frame); + + const observed: Observed = { + at, + role, + address: bytes[0]!, + functionCode: bytes[1]!, + bytes, + decoded: role === "request" ? decodeRequest(bytes) : decodeResponse(bytes), + }; + + if (role === "request") { + this.#lastRequest = observed; + this.#expecting = "response"; + } else { + // Only pair with a request from the same device; a response that does not match one is + // reported unpaired rather than mislabelled + if (this.#lastRequest?.address === observed.address) observed.inReplyTo = this.#lastRequest; + this.#lastRequest = undefined; + this.#expecting = "request"; + } + + return observed; + } + + #discardAll(): void { + for (const byte of this.#buffer) this.#pendingNoise.push(byte); + this.#buffer = new Uint8Array(0); + } + + #flushNoise(at: number, events: MonitorEvent[]): void { + if (this.#pendingNoise.length === 0) return; + + events.push({ type: "noise", at, bytes: Uint8Array.from(this.#pendingNoise) }); + this.#pendingNoise = []; + } +} + +function concat(left: Uint8Array, right: Uint8Array): Uint8Array { + if (left.length === 0) return right; + if (right.length === 0) return left; + + const joined = new Uint8Array(left.length + right.length); + joined.set(left); + joined.set(right, left.length); + return joined; +} diff --git a/serial/src/modbus/pdu.test.ts b/serial/src/modbus/pdu.test.ts new file mode 100644 index 0000000..9ac9e53 --- /dev/null +++ b/serial/src/modbus/pdu.test.ts @@ -0,0 +1,194 @@ +import { describe, expect, it } from "vitest"; +import { isValid } from "./crc"; +import { relay, temperatureSensor } from "./frames.fixture"; +import { + decodeRequest, + decodeResponse, + readHoldingRegisters, + readInputRegisters, + requestLength, + responseLength, + writeMultipleRegisters, + writeSingleCoil, + writeSingleRegister, +} from "./pdu"; + +describe("requestLength", () => { + it("sizes a read from its function code alone", () => { + expect(requestLength(relay.readAddressRequest)).toBe(8); + expect(requestLength(temperatureSensor.readBothRequest)).toBe(8); + }); + + it("sizes a single write from its function code alone", () => { + expect(requestLength(relay.closeRelayRequest)).toBe(8); + }); + + it("sizes a multiple write from its byte count", () => { + // 7 header + 1 payload + 2 CRC + expect(requestLength(relay.closeAllRequest)).toBe(10); + // 7 header + 2 payload + 2 CRC + expect(requestLength(relay.setBaudRequest)).toBe(11); + }); + + it("asks for more bytes when the count has not arrived", () => { + expect(requestLength(relay.closeAllRequest.subarray(0, 1))).toBe("incomplete"); + expect(requestLength(relay.closeAllRequest.subarray(0, 6))).toBe("incomplete"); + }); + + it("gives up on a function code it cannot size", () => { + // 0x43 is the RadiCal's read-addressed-by-serial-number, which this module does not model + expect(requestLength(Uint8Array.of(0x02, 0x43, 0x00))).toBe("unknown"); + }); +}); + +describe("responseLength", () => { + it("sizes a read response from its byte count", () => { + expect(responseLength(relay.readAddressResponse)).toBe(7); + expect(responseLength(relay.readRelayStateResponse)).toBe(6); + expect(responseLength(temperatureSensor.readBothResponse)).toBe(9); + }); + + it("sizes a write acknowledgement as the echoed header", () => { + expect(responseLength(relay.writeCoilsResponse)).toBe(8); + expect(responseLength(relay.setBaudResponse)).toBe(8); + }); + + it("sizes an exception response", () => { + expect(responseLength(Uint8Array.of(0x02, 0x83, 0x02))).toBe(5); + }); + + it("asks for more bytes when the count has not arrived", () => { + expect(responseLength(relay.readAddressResponse.subarray(0, 2))).toBe("incomplete"); + }); +}); + +describe("decodeRequest", () => { + it("reads the register range out of a read", () => { + expect(decodeRequest(relay.readBaudRequest)).toEqual({ + kind: "read", + of: "holdingRegisters", + start: 0x03e8, + quantity: 1, + }); + }); + + it("distinguishes input registers from holding registers", () => { + expect(decodeRequest(temperatureSensor.readBothRequest)).toEqual({ + kind: "read", + of: "inputRegisters", + start: 0x0001, + quantity: 2, + }); + }); + + it("reads a coil write as on or off, not as the raw 0xFF00", () => { + expect(decodeRequest(relay.closeRelayRequest)).toEqual({ kind: "writeSingleCoil", address: 0, on: true }); + expect(decodeRequest(relay.openRelayRequest)).toEqual({ kind: "writeSingleCoil", address: 0, on: false }); + }); + + it("unpacks a multiple coil write low bit first", () => { + const decoded = decodeRequest(relay.closeAllRequest); + + expect(decoded).toEqual({ + kind: "writeMultipleCoils", + start: 0, + quantity: 8, + values: Array.from({ length: 8 }, () => true), + }); + }); + + it("unpacks a multiple register write", () => { + // Baud rate 19 200 written to holding register 0x03E9 as the value 4 + expect(decodeRequest(relay.setBaudRequest)).toEqual({ + kind: "writeMultipleRegisters", + start: 0x03e9, + values: [4], + }); + }); +}); + +describe("decodeResponse", () => { + it("reads register values", () => { + expect(decodeResponse(relay.readAddressResponse)).toEqual({ + kind: "registers", + of: "holdingRegisters", + values: [0x00ff], + }); + }); + + it("reads the temperature and humidity out of one response", () => { + expect(decodeResponse(temperatureSensor.readBothResponse)).toEqual({ + kind: "registers", + of: "inputRegisters", + values: [0x0131, 0x0222], + }); + }); + + it("reads coil bits", () => { + const decoded = decodeResponse(relay.readRelayStateResponse); + + // The manual: bit 0 is relay 1, and 1 means closed + expect(decoded).toEqual({ + kind: "bits", + of: "coils", + values: [true, false, false, false, false, false, false, false], + }); + }); + + it("reads an exception and names it", () => { + // The RadiCal answers 0x02 for a register outside D000…D614 + expect(decodeResponse(Uint8Array.of(0x02, 0x83, 0x02, 0, 0))).toEqual({ + kind: "exception", + functionCode: 0x03, + code: 0x02, + text: "Illegal data address", + }); + }); + + it("names an exception code it does not know rather than dropping it", () => { + const decoded = decodeResponse(Uint8Array.of(0x02, 0x84, 0x7f, 0, 0)); + + expect(decoded).toMatchObject({ kind: "exception", code: 0x7f, text: "Unknown exception 0x7f" }); + }); +}); + +describe("building frames", () => { + /** + * The strongest check available: build the frame from scratch and compare it, CRC and all, to + * the bytes the manufacturer printed + */ + it("builds the relay's own read-address request", () => { + expect(readHoldingRegisters(0x00, 0x0000, 1)).toEqual(relay.readAddressRequest); + }); + + it("builds the relay's own read-baud request", () => { + expect(readHoldingRegisters(0xff, 0x03e8, 1)).toEqual(relay.readBaudRequest); + }); + + it("builds the relay's own coil writes", () => { + expect(writeSingleCoil(0xff, 0x0000, true)).toEqual(relay.closeRelayRequest); + expect(writeSingleCoil(0xff, 0x0000, false)).toEqual(relay.openRelayRequest); + }); + + it("builds the relay's own set-baud request", () => { + expect(writeMultipleRegisters(0xff, 0x03e9, [4])).toEqual(relay.setBaudRequest); + }); + + it("builds the temperature sensor's own read requests", () => { + expect(readInputRegisters(0x01, 0x0001, 1)).toEqual(temperatureSensor.readTemperatureRequest); + expect(readInputRegisters(0x01, 0x0002, 1)).toEqual(temperatureSensor.readHumidityRequest); + expect(readInputRegisters(0x01, 0x0001, 2)).toEqual(temperatureSensor.readBothRequest); + }); + + it("builds frames that check out", () => { + expect(isValid(writeSingleRegister(0x02, 0xd001, 32_000))).toBe(true); + expect(isValid(writeMultipleRegisters(0x02, 0xd001, [1, 2, 3]))).toBe(true); + }); + + /** The set point the firmware writes, so the shape is worth pinning down */ + it("builds a RadiCal set point write", () => { + const frame = writeSingleRegister(0x02, 0xd001, 32_000); + + expect(Array.from(frame.subarray(0, 6))).toEqual([0x02, 0x06, 0xd0, 0x01, 0x7d, 0x00]); + }); +}); diff --git a/serial/src/modbus/pdu.ts b/serial/src/modbus/pdu.ts new file mode 100644 index 0000000..d5d9286 --- /dev/null +++ b/serial/src/modbus/pdu.ts @@ -0,0 +1,316 @@ +/** + * Modbus RTU frames: how long one is, and what it says. + * + * A frame is `address · function code · payload · CRC-16`. Nothing in it records its own length, + * which is the whole difficulty of reading a bus you are not driving — see `monitor.ts`. What the + * frame does carry is a function code, and for every code the length is either fixed or written + * into one byte near the front. That is what `requestLength` and `responseLength` exploit. + * + * Request and response are *different shapes for the same function code*, and neither says which + * it is. A read-holding-registers request is always 8 bytes; the response to it is 5 plus however + * many bytes it carries. Telling them apart is the monitor's job, not this module's. + */ + +import { crc16, read as readCrc } from "./crc"; + +export const FunctionCode = { + ReadCoils: 0x01, + ReadDiscreteInputs: 0x02, + ReadHoldingRegisters: 0x03, + ReadInputRegisters: 0x04, + WriteSingleCoil: 0x05, + WriteSingleRegister: 0x06, + WriteMultipleCoils: 0x0f, + WriteMultipleRegisters: 0x10, +} as const; + +export type FunctionCode = (typeof FunctionCode)[keyof typeof FunctionCode]; + +/** Set in the function code of a response that is reporting a refusal rather than an answer */ +export const EXCEPTION_FLAG = 0x80; + +/** The value written to a coil to close it. Anything else, in practice 0x0000, opens it */ +export const COIL_ON = 0xff00; +export const COIL_OFF = 0x0000; + +/** + * Modbus' own exception codes, plus the two the RadiCal manual documents beyond the standard + * meanings (section 1.3.1). The fan answers 0x02 for a register outside D000…D614 and for a read + * of more than 37 registers, and 0x04 when the electronics cannot read a register at all + */ +export const exceptionText: Readonly> = { + 0x01: "Illegal function", + 0x02: "Illegal data address", + 0x03: "Illegal data value", + 0x04: "Device failure", + 0x05: "Acknowledge", + 0x06: "Device busy", + 0x08: "Memory parity error", + 0x0a: "Gateway path unavailable", + 0x0b: "Gateway target failed to respond", +}; + +/** + * How long a frame is, once its function code and any count byte have been seen. + * + * `"incomplete"` means the bytes that carry the length have not arrived yet and the caller should + * ask again with more. `"unknown"` means the function code is not one this module can size — the + * RadiCal's serial-number-addressed commands (0x43, 0x44, 0x46, 0x50) are the ones that turn up in + * practice — and the caller cannot do better than resynchronise + */ +export type FrameLength = number | "incomplete" | "unknown"; + +/** A read or a single write, in either direction, is always this long */ +const FIXED_LENGTH = 8; + +export function requestLength(buffer: Uint8Array): FrameLength { + if (buffer.length < 2) return "incomplete"; + + switch (buffer[1]) { + case FunctionCode.ReadCoils: + case FunctionCode.ReadDiscreteInputs: + case FunctionCode.ReadHoldingRegisters: + case FunctionCode.ReadInputRegisters: + case FunctionCode.WriteSingleCoil: + case FunctionCode.WriteSingleRegister: + return FIXED_LENGTH; + + // address · code · start(2) · quantity(2) · byte count · payload · CRC(2) + case FunctionCode.WriteMultipleCoils: + case FunctionCode.WriteMultipleRegisters: { + if (buffer.length < 7) return "incomplete"; + return 7 + buffer[6]! + 2; + } + + default: + return "unknown"; + } +} + +export function responseLength(buffer: Uint8Array): FrameLength { + if (buffer.length < 2) return "incomplete"; + + const code = buffer[1]!; + + // address · code · exception · CRC(2). Checked before the switch because the flag makes the + // function code an entirely different number + if (code & EXCEPTION_FLAG) return 5; + + switch (code) { + // address · code · byte count · payload · CRC(2) + case FunctionCode.ReadCoils: + case FunctionCode.ReadDiscreteInputs: + case FunctionCode.ReadHoldingRegisters: + case FunctionCode.ReadInputRegisters: { + if (buffer.length < 3) return "incomplete"; + return 3 + buffer[2]! + 2; + } + + // A write is answered by echoing the header back + case FunctionCode.WriteSingleCoil: + case FunctionCode.WriteSingleRegister: + case FunctionCode.WriteMultipleCoils: + case FunctionCode.WriteMultipleRegisters: + return FIXED_LENGTH; + + default: + return "unknown"; + } +} + +export type Request = + | { kind: "read"; of: "coils" | "discreteInputs" | "holdingRegisters" | "inputRegisters"; start: number; quantity: number } + | { kind: "writeSingleCoil"; address: number; on: boolean } + | { kind: "writeSingleRegister"; address: number; value: number } + | { kind: "writeMultipleCoils"; start: number; quantity: number; values: boolean[] } + | { kind: "writeMultipleRegisters"; start: number; values: number[] }; + +export type Response = + | { kind: "bits"; of: "coils" | "discreteInputs"; values: boolean[] } + | { kind: "registers"; of: "holdingRegisters" | "inputRegisters"; values: number[] } + | { kind: "writeSingleCoil"; address: number; on: boolean } + | { kind: "writeSingleRegister"; address: number; value: number } + | { kind: "writeAcknowledged"; start: number; quantity: number } + | { kind: "exception"; functionCode: number; code: number; text: string }; + +const readTarget: Readonly> = { + [FunctionCode.ReadCoils]: "coils", + [FunctionCode.ReadDiscreteInputs]: "discreteInputs", + [FunctionCode.ReadHoldingRegisters]: "holdingRegisters", + [FunctionCode.ReadInputRegisters]: "inputRegisters", +}; + +/** + * Decodes the payload of a frame already known to be complete and CRC-valid. + * + * Returns `null` for a function code it does not understand rather than throwing, because the + * monitor shows unparseable traffic as raw bytes instead of dropping it + */ +export function decodeRequest(frame: Uint8Array): Request | null { + const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength); + const code = frame[1]!; + + const target = readTarget[code]; + if (target !== undefined) { + return { kind: "read", of: target, start: view.getUint16(2), quantity: view.getUint16(4) }; + } + + switch (code) { + case FunctionCode.WriteSingleCoil: + return { kind: "writeSingleCoil", address: view.getUint16(2), on: view.getUint16(4) === COIL_ON }; + + case FunctionCode.WriteSingleRegister: + return { kind: "writeSingleRegister", address: view.getUint16(2), value: view.getUint16(4) }; + + case FunctionCode.WriteMultipleCoils: { + const quantity = view.getUint16(4); + return { kind: "writeMultipleCoils", start: view.getUint16(2), quantity, values: unpackBits(frame, 7, quantity) }; + } + + case FunctionCode.WriteMultipleRegisters: { + const count = frame[6]!; + const values: number[] = []; + for (let offset = 0; offset + 1 < count; offset += 2) values.push(view.getUint16(7 + offset)); + return { kind: "writeMultipleRegisters", start: view.getUint16(2), values }; + } + + default: + return null; + } +} + +export function decodeResponse(frame: Uint8Array): Response | null { + const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength); + const code = frame[1]!; + + if (code & EXCEPTION_FLAG) { + const exception = frame[2]!; + return { + kind: "exception", + functionCode: code & ~EXCEPTION_FLAG, + code: exception, + text: exceptionText[exception] ?? `Unknown exception 0x${exception.toString(16).padStart(2, "0")}`, + }; + } + + switch (code) { + case FunctionCode.ReadCoils: + case FunctionCode.ReadDiscreteInputs: { + const count = frame[2]!; + return { + kind: "bits", + of: code === FunctionCode.ReadCoils ? "coils" : "discreteInputs", + // Without the request there is no way to know how many of the trailing bits are padding, + // so every bit of every byte is reported and the caller takes the ones it asked for + values: unpackBits(frame, 3, count * 8), + }; + } + + case FunctionCode.ReadHoldingRegisters: + case FunctionCode.ReadInputRegisters: { + const count = frame[2]!; + const values: number[] = []; + for (let offset = 0; offset + 1 < count; offset += 2) values.push(view.getUint16(3 + offset)); + return { + kind: "registers", + of: code === FunctionCode.ReadHoldingRegisters ? "holdingRegisters" : "inputRegisters", + values, + }; + } + + case FunctionCode.WriteSingleCoil: + return { kind: "writeSingleCoil", address: view.getUint16(2), on: view.getUint16(4) === COIL_ON }; + + case FunctionCode.WriteSingleRegister: + return { kind: "writeSingleRegister", address: view.getUint16(2), value: view.getUint16(4) }; + + case FunctionCode.WriteMultipleCoils: + case FunctionCode.WriteMultipleRegisters: + return { kind: "writeAcknowledged", start: view.getUint16(2), quantity: view.getUint16(4) }; + + default: + return null; + } +} + +/** Modbus packs bits low-bit-first within each byte, starting at the lowest address */ +function unpackBits(frame: Uint8Array, offset: number, quantity: number): boolean[] { + const values: boolean[] = []; + + for (let index = 0; index < quantity; index++) { + const byte = frame[offset + (index >> 3)]; + if (byte === undefined) break; + values.push((byte & (1 << (index & 7))) !== 0); + } + + return values; +} + +/* -------------------------------------------------------------------------- */ +/* Building frames */ +/* -------------------------------------------------------------------------- */ + +/** `address · code · start · quantity · CRC`, the shape of every read and single write */ +function fixedFrame(address: number, code: number, first: number, second: number): Uint8Array { + const frame = new Uint8Array(FIXED_LENGTH); + const view = new DataView(frame.buffer); + + frame[0] = address; + frame[1] = code; + view.setUint16(2, first); + view.setUint16(4, second); + writeCrc(frame); + + return frame; +} + +export function readCoils(address: number, start: number, quantity: number): Uint8Array { + return fixedFrame(address, FunctionCode.ReadCoils, start, quantity); +} + +export function readDiscreteInputs(address: number, start: number, quantity: number): Uint8Array { + return fixedFrame(address, FunctionCode.ReadDiscreteInputs, start, quantity); +} + +export function readHoldingRegisters(address: number, start: number, quantity: number): Uint8Array { + return fixedFrame(address, FunctionCode.ReadHoldingRegisters, start, quantity); +} + +export function readInputRegisters(address: number, start: number, quantity: number): Uint8Array { + return fixedFrame(address, FunctionCode.ReadInputRegisters, start, quantity); +} + +export function writeSingleCoil(address: number, coil: number, on: boolean): Uint8Array { + return fixedFrame(address, FunctionCode.WriteSingleCoil, coil, on ? COIL_ON : COIL_OFF); +} + +export function writeSingleRegister(address: number, register: number, value: number): Uint8Array { + return fixedFrame(address, FunctionCode.WriteSingleRegister, register, value); +} + +export function writeMultipleRegisters(address: number, start: number, values: readonly number[]): Uint8Array { + const count = values.length * 2; + const frame = new Uint8Array(9 + count); + const view = new DataView(frame.buffer); + + frame[0] = address; + frame[1] = FunctionCode.WriteMultipleRegisters; + view.setUint16(2, start); + view.setUint16(4, values.length); + frame[6] = count; + values.forEach((value, index) => view.setUint16(7 + index * 2, value)); + writeCrc(frame); + + return frame; +} + +/** Local to keep `crc.append`'s name from colliding with the array method of the same idea */ +function writeCrc(frame: Uint8Array): void { + const offset = frame.length - 2; + const crc = crc16(frame, 0, offset); + frame[offset] = crc & 0xff; + frame[offset + 1] = crc >> 8; +} + +/** Re-exported so callers checking a frame do not need to reach into `crc.ts` for the byte order */ +export { readCrc }; diff --git a/serial/src/vite-env.d.ts b/serial/src/vite-env.d.ts deleted file mode 100644 index 4078e74..0000000 --- a/serial/src/vite-env.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -/// -/// diff --git a/serial/svelte.config.js b/serial/svelte.config.js deleted file mode 100644 index b0683fd..0000000 --- a/serial/svelte.config.js +++ /dev/null @@ -1,7 +0,0 @@ -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' - -export default { - // Consult https://svelte.dev/docs#compile-time-svelte-preprocess - // for more information about preprocessors - preprocess: vitePreprocess(), -} diff --git a/serial/tsconfig.json b/serial/tsconfig.json index c4e1c5f..f27c0b5 100644 --- a/serial/tsconfig.json +++ b/serial/tsconfig.json @@ -1,20 +1,23 @@ { - "extends": "@tsconfig/svelte/tsconfig.json", "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", - "resolveJsonModule": true, - /** - * Typecheck JS in `.svelte` and `.js` files by default. - * Disable checkJs if you'd like to use dynamic types in JS. - * Note that setting allowJs false does not prevent the use - * of JS in `.svelte` files. - */ - "allowJs": true, - "checkJs": true, - "isolatedModules": true + "moduleResolution": "bundler", + "jsx": "preserve", + "jsxImportSource": "@solidjs/web", + "types": ["vite/client", "dom-serial"], + + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + "noEmit": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true }, - "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], - "references": [{ "path": "./tsconfig.node.json" }] + "include": ["src", "vite.config.ts"] } diff --git a/serial/tsconfig.node.json b/serial/tsconfig.node.json deleted file mode 100644 index 494bfe0..0000000 --- a/serial/tsconfig.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler" - }, - "include": ["vite.config.ts"] -} diff --git a/serial/vite.config.ts b/serial/vite.config.ts index d701969..01c2464 100644 --- a/serial/vite.config.ts +++ b/serial/vite.config.ts @@ -1,7 +1,16 @@ -import { defineConfig } from 'vite' -import { svelte } from '@sveltejs/vite-plugin-svelte' +import { defineConfig } from "vitest/config"; +import solid from "@solidjs/vite-plugin"; -// https://vitejs.dev/config/ export default defineConfig({ - plugins: [svelte()], -}) + // Relative, so the built app works both at a domain root and under the + // // path GitHub Pages serves a project site from + base: "./", + plugins: [solid()], + test: { + // Everything under src/modbus and src/devices is plain TypeScript over + // bytes, with no DOM in sight. The components are verified by `tsc` and by + // running the thing against a real fan + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); -- 2.51.2