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"], + }, +});