From a98397a241d2c5ee16b15806e9dc063fa0351cca Mon Sep 17 00:00:00 2001 From: ordinarthur Date: Wed, 8 Apr 2026 18:37:08 +0200 Subject: [PATCH] add code --- .gitignore | 10 + _tmp_945_290d62cdc9142d0226f30816d854ae86 | 0 _tmp_945_eb782ccbcda1bcca02bbc45f41d4c1b8 | 0 .../inbound/rest/pairing/dto/pairing.dto.ts | 16 + .../rest/pairing/pairing.controller.ts | 57 + apps/backend/src/app.module.ts | 5 +- .../src/core/services/pairing.service.ts | 143 ++ apps/robot-client/.env.example | 20 + apps/robot-client/package.json | 6 +- apps/robot-client/scripts/hardware-demo.ts | 69 + apps/robot-client/scripts/verify-protocol.ts | 64 + .../src/config/hardware.config.ts | 21 + apps/robot-client/src/config/robot.config.ts | 19 +- .../src/hardware/hardware.service.ts | 211 +++ apps/robot-client/src/hardware/index.ts | 13 + apps/robot-client/src/hardware/protocol.ts | 212 +++ apps/robot-client/src/main.ts | 64 +- apps/robot-client/src/services/index.ts | 1 + .../src/services/local-store.service.ts | 6 +- .../src/services/pairing.service.ts | 177 ++ .../src/services/wake-word.service.ts | 3 +- .../robot-client/src/services/wifi.service.ts | 11 +- apps/robot-client/src/setup/captive-portal.ts | 35 +- .../src/setup/pairing-app.html.ts | 333 ++++ .../tests/hardware/protocol.test.ts | 124 ++ apps/robot-hardware/.gitignore | 8 + apps/robot-hardware/README.md | 115 ++ apps/robot-hardware/include/protocol_types.h | 91 + apps/robot-hardware/legacy/README.md | 14 + .../legacy/robot-emotion/robot-emotion.ino | 253 +++ apps/robot-hardware/legacy/robot-hardware.ino | 81 + apps/robot-hardware/lib/Eyes/library.json | 10 + apps/robot-hardware/lib/Eyes/src/Eyes.cpp | 203 +++ apps/robot-hardware/lib/Eyes/src/Eyes.h | 60 + apps/robot-hardware/lib/Protocol/library.json | 7 + .../lib/Protocol/src/Protocol.cpp | 122 ++ .../lib/Protocol/src/Protocol.h | 98 + apps/robot-hardware/platformio.ini | 48 + apps/robot-hardware/src/main.cpp | 147 ++ docs/STATUS.md | 104 ++ .../fusion-libraries/INMP441_Breakout.lbr | 146 ++ pnpm-lock.yaml | 1609 +++++++++++++++++ 42 files changed, 4691 insertions(+), 45 deletions(-) delete mode 100644 _tmp_945_290d62cdc9142d0226f30816d854ae86 delete mode 100644 _tmp_945_eb782ccbcda1bcca02bbc45f41d4c1b8 create mode 100644 apps/backend/src/adapters/inbound/rest/pairing/dto/pairing.dto.ts create mode 100644 apps/backend/src/adapters/inbound/rest/pairing/pairing.controller.ts create mode 100644 apps/backend/src/core/services/pairing.service.ts create mode 100644 apps/robot-client/scripts/hardware-demo.ts create mode 100644 apps/robot-client/scripts/verify-protocol.ts create mode 100644 apps/robot-client/src/hardware/hardware.service.ts create mode 100644 apps/robot-client/src/hardware/index.ts create mode 100644 apps/robot-client/src/hardware/protocol.ts create mode 100644 apps/robot-client/src/services/pairing.service.ts create mode 100644 apps/robot-client/src/setup/pairing-app.html.ts create mode 100644 apps/robot-client/tests/hardware/protocol.test.ts create mode 100644 apps/robot-hardware/.gitignore create mode 100644 apps/robot-hardware/README.md create mode 100644 apps/robot-hardware/include/protocol_types.h create mode 100644 apps/robot-hardware/legacy/README.md create mode 100644 apps/robot-hardware/legacy/robot-emotion/robot-emotion.ino create mode 100644 apps/robot-hardware/legacy/robot-hardware.ino create mode 100644 apps/robot-hardware/lib/Eyes/library.json create mode 100644 apps/robot-hardware/lib/Eyes/src/Eyes.cpp create mode 100644 apps/robot-hardware/lib/Eyes/src/Eyes.h create mode 100644 apps/robot-hardware/lib/Protocol/library.json create mode 100644 apps/robot-hardware/lib/Protocol/src/Protocol.cpp create mode 100644 apps/robot-hardware/lib/Protocol/src/Protocol.h create mode 100644 apps/robot-hardware/platformio.ini create mode 100644 apps/robot-hardware/src/main.cpp create mode 100644 docs/STATUS.md create mode 100644 hardware/fusion-libraries/INMP441_Breakout.lbr diff --git a/.gitignore b/.gitignore index 6ae1c4b..8c93aa0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,13 @@ docker-compose.override.yml # TypeORM *.sqlite + +#desktop +apps/robot-desktop/ +apps/robot-desktop/src-tauri/target +apps/robot-desktop/src-tauri/target/ +apps/robot-desktop/dist +apps/robot-client/node_modules + + +.pio/ \ No newline at end of file diff --git a/_tmp_945_290d62cdc9142d0226f30816d854ae86 b/_tmp_945_290d62cdc9142d0226f30816d854ae86 deleted file mode 100644 index e69de29..0000000 diff --git a/_tmp_945_eb782ccbcda1bcca02bbc45f41d4c1b8 b/_tmp_945_eb782ccbcda1bcca02bbc45f41d4c1b8 deleted file mode 100644 index e69de29..0000000 diff --git a/apps/backend/src/adapters/inbound/rest/pairing/dto/pairing.dto.ts b/apps/backend/src/adapters/inbound/rest/pairing/dto/pairing.dto.ts new file mode 100644 index 0000000..5993035 --- /dev/null +++ b/apps/backend/src/adapters/inbound/rest/pairing/dto/pairing.dto.ts @@ -0,0 +1,16 @@ +import { IsString, IsNotEmpty, Length, Matches } from 'class-validator'; + +export class RequestPairingDto { + @IsString() + @IsNotEmpty() + @Length(1, 100) + deviceName!: string; +} + +export class ConfirmPairingDto { + @IsString() + @IsNotEmpty() + @Length(6, 6) + @Matches(/^\d{6}$/, { message: 'Pairing code must be 6 digits' }) + code!: string; +} diff --git a/apps/backend/src/adapters/inbound/rest/pairing/pairing.controller.ts b/apps/backend/src/adapters/inbound/rest/pairing/pairing.controller.ts new file mode 100644 index 0000000..15a9aab --- /dev/null +++ b/apps/backend/src/adapters/inbound/rest/pairing/pairing.controller.ts @@ -0,0 +1,57 @@ +import { Controller, Post, Get, Body, Param, UseGuards, Request } from '@nestjs/common'; +import { PairingService } from '../../../../core/services/pairing.service'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { RequestPairingDto, ConfirmPairingDto } from './dto/pairing.dto'; + +@Controller('pairing') +export class PairingController { + constructor( + private readonly pairingService: PairingService, + ) {} + + /** + * POST /pairing/request + * Called by the robot on first boot. No auth required. + * Returns a requestId + 6-digit code for the robot to display. + */ + @Post('request') + async requestPairing(@Body() dto: RequestPairingDto) { + return this.pairingService.requestPairing(dto.deviceName); + } + + /** + * GET /pairing/status/:requestId + * Called by the robot to poll for confirmation. No auth required. + * Returns { status: 'pending' } or { status: 'confirmed', deviceId, deviceToken }. + */ + @Get('status/:requestId') + async getPairingStatus(@Param('requestId') requestId: string) { + const request = await this.pairingService.getPairingStatus(requestId); + + if (request.status === 'pending') { + return { status: 'pending', code: request.code }; + } + + // Confirmed — return credentials to the robot + return { + status: 'confirmed', + deviceId: request.deviceId, + deviceToken: request.deviceToken, + homeId: request.homeId, + }; + } + + /** + * POST /pairing/confirm + * Called by the user's app with their JWT + the 6-digit code. + * Associates the robot to the user's home. + */ + @Post('confirm') + @UseGuards(JwtAuthGuard) + async confirmPairing( + @Body() dto: ConfirmPairingDto, + @Request() req: { user: { homeId: string } }, + ) { + return this.pairingService.confirmPairing(dto.code, req.user.homeId); + } +} diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 7545866..9f9af55 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -19,6 +19,8 @@ import { ConversationService } from './core/services/conversation.service'; import { JwtStrategy } from './adapters/inbound/rest/auth/strategies/jwt.strategy'; import { AuthController } from './adapters/inbound/rest/auth/auth.controller'; import { DeviceController } from './adapters/inbound/rest/device/device.controller'; +import { PairingController } from './adapters/inbound/rest/pairing/pairing.controller'; +import { PairingService } from './core/services/pairing.service'; import { RobotGateway } from './adapters/inbound/websocket/robot.gateway'; import { DeepgramAdapter } from './adapters/outbound/stt/deepgram.adapter'; import { AnthropicAdapter } from './adapters/outbound/llm/anthropic.adapter'; @@ -54,12 +56,13 @@ import { CACHE_PORT } from './core/ports/outbound/cache.port'; }), }), ], - controllers: [AuthController, DeviceController], + controllers: [AuthController, DeviceController, PairingController], providers: [ AuthService, UserService, HomeService, DeviceService, + PairingService, JwtStrategy, RobotGateway, { diff --git a/apps/backend/src/core/services/pairing.service.ts b/apps/backend/src/core/services/pairing.service.ts new file mode 100644 index 0000000..42c8168 --- /dev/null +++ b/apps/backend/src/core/services/pairing.service.ts @@ -0,0 +1,143 @@ +import { Injectable, Inject, BadRequestException, NotFoundException } from '@nestjs/common'; +import { ICachePort, CACHE_PORT } from '../ports/outbound/cache.port'; +import { AuthService } from './auth.service'; +import * as crypto from 'crypto'; + +export interface PairingRequest { + requestId: string; + code: string; + deviceName: string; + status: 'pending' | 'confirmed'; + deviceId?: string; + deviceToken?: string; + homeId?: string; + createdAt: number; +} + +const PAIRING_TTL = 600; // 10 minutes +const PAIRING_CODE_PREFIX = 'pairing:code:'; +const PAIRING_REQUEST_PREFIX = 'pairing:request:'; + +@Injectable() +export class PairingService { + constructor( + @Inject(CACHE_PORT) private readonly cache: ICachePort, + private readonly authService: AuthService, + ) {} + + /** + * Step 1: Robot requests pairing. + * Generates a 6-digit code and stores the request in Redis. + * Returns the requestId + code for the robot to display. + */ + async requestPairing(deviceName: string): Promise<{ requestId: string; code: string }> { + const requestId = crypto.randomUUID(); + const code = this.generateCode(); + + const pairingRequest: PairingRequest = { + requestId, + code, + deviceName, + status: 'pending', + createdAt: Date.now(), + }; + + // Store by requestId (for robot polling) + await this.cache.set( + `${PAIRING_REQUEST_PREFIX}${requestId}`, + pairingRequest, + PAIRING_TTL, + ); + + // Store code → requestId mapping (for user confirmation lookup) + await this.cache.set( + `${PAIRING_CODE_PREFIX}${code}`, + requestId, + PAIRING_TTL, + ); + + return { requestId, code }; + } + + /** + * Step 2: Robot polls for pairing status. + * Returns current status; once confirmed, includes deviceId + token. + */ + async getPairingStatus(requestId: string): Promise { + const request = await this.cache.get( + `${PAIRING_REQUEST_PREFIX}${requestId}`, + ); + + if (!request) { + throw new NotFoundException('Pairing request not found or expired'); + } + + return request; + } + + /** + * Step 3: User confirms pairing with the 6-digit code. + * Associates the device to the user's home, generates credentials. + */ + async confirmPairing(code: string, homeId: string): Promise<{ deviceId: string; deviceName: string }> { + // Look up requestId from code + const requestId = await this.cache.get( + `${PAIRING_CODE_PREFIX}${code}`, + ); + + if (!requestId) { + throw new BadRequestException('Invalid or expired pairing code'); + } + + // Get the pairing request + const request = await this.cache.get( + `${PAIRING_REQUEST_PREFIX}${requestId}`, + ); + + if (!request) { + throw new BadRequestException('Pairing request expired'); + } + + if (request.status === 'confirmed') { + throw new BadRequestException('Pairing code already used'); + } + + // Register the device on the user's home + const { deviceId, token } = await this.authService.registerDevice( + homeId, + request.deviceName, + ); + + // Update the pairing request with credentials + const confirmedRequest: PairingRequest = { + ...request, + status: 'confirmed', + deviceId, + deviceToken: token, + homeId, + }; + + await this.cache.set( + `${PAIRING_REQUEST_PREFIX}${requestId}`, + confirmedRequest, + PAIRING_TTL, + ); + + // Clean up the code mapping + await this.cache.del(`${PAIRING_CODE_PREFIX}${code}`); + + return { deviceId, deviceName: request.deviceName }; + } + + /** + * Generate a random 6-digit numeric code. + * Avoids ambiguous patterns (000000, 111111, etc.) + */ + private generateCode(): string { + let code: string; + do { + code = String(crypto.randomInt(0, 1000000)).padStart(6, '0'); + } while (/^(\d)\1{5}$/.test(code)); // Avoid 000000, 111111, etc. + return code; + } +} diff --git a/apps/robot-client/.env.example b/apps/robot-client/.env.example index cbc6200..375533c 100644 --- a/apps/robot-client/.env.example +++ b/apps/robot-client/.env.example @@ -53,3 +53,23 @@ WAKEWORD_MODEL=hey_jarvis # Wake word detection threshold (0.0 to 1.0, higher = less false positives) WAKEWORD_THRESHOLD=0.5 + +# ── Hardware bridge (ESP32 firmware over UART) ── + +# Enable the serial link to the ESP32 running apps/robot-hardware. +# When false, the robot-client runs headless (no eyes / no face). +HARDWARE_SERIAL_ENABLED=false + +# Serial device path. +# Dev (ESP32 plugged into laptop): /dev/ttyUSB0 or /dev/ttyACM0 (Linux), +# /dev/tty.usbserial-XXXX (macOS), +# COM3 (Windows). +# Prod (Pi → ESP32 UART): /dev/serial0 or /dev/ttyAMA0 +HARDWARE_SERIAL_PORT=/dev/ttyUSB0 + +# Must match HW_SERIAL_BAUD in apps/robot-hardware/platformio.ini +HARDWARE_SERIAL_BAUD=921600 + +# Heartbeat interval (ms). The firmware falls back to a SLEEPY +# animation if it stops hearing from us for ~5s (HW_HEARTBEAT_TIMEOUT_MS). +HARDWARE_HEARTBEAT_MS=1000 diff --git a/apps/robot-client/package.json b/apps/robot-client/package.json index b1d8c9f..19d467c 100644 --- a/apps/robot-client/package.json +++ b/apps/robot-client/package.json @@ -11,13 +11,15 @@ "lint": "eslint \"src/**/*.ts\" --fix", "format": "prettier --write \"src/**/*.ts\"", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "hw:demo": "tsx scripts/hardware-demo.ts" }, "dependencies": { "socket.io-client": "^4.8.3", "dotenv": "^17.3.1", "pino": "^9.6.0", - "pino-pretty": "^13.0.0" + "pino-pretty": "^13.0.0", + "serialport": "^12.0.0" }, "devDependencies": { "typescript": "^5.8.3", diff --git a/apps/robot-client/scripts/hardware-demo.ts b/apps/robot-client/scripts/hardware-demo.ts new file mode 100644 index 0000000..a353a26 --- /dev/null +++ b/apps/robot-client/scripts/hardware-demo.ts @@ -0,0 +1,69 @@ +/** + * Ti-Pote — Hardware bring-up demo. + * + * Run with: + * HARDWARE_SERIAL_PORT=/dev/ttyUSB0 pnpm --filter @ti-pote/robot-client hw:demo + * + * What it does: + * 1. Opens the serial link to the ESP32 firmware. + * 2. Pings it and prints the round-trip time. + * 3. Cycles through every emotion with a 1.2 s pause so you can + * watch the OLED eyes react. + * 4. Finishes on NEUTRAL and disconnects cleanly. + * + * Use this as a smoke test after flashing new firmware, before + * wiring the full robot-client/cloud pipeline. + */ + +import { HardwareService, Emotion } from '../src/hardware/index.js'; + +const path = process.env.HARDWARE_SERIAL_PORT ?? '/dev/ttyUSB0'; +const baudRate = parseInt(process.env.HARDWARE_SERIAL_BAUD ?? '921600', 10); + +const EMOTION_SEQUENCE: Emotion[] = [ + Emotion.NEUTRAL, + Emotion.HAPPY, + Emotion.SAD, + Emotion.ANGRY, + Emotion.SURPRISED, + Emotion.SLEEPY, + Emotion.WINK, + Emotion.LOVE, + Emotion.DIZZY, + Emotion.DEAD, + Emotion.NEUTRAL, +]; + +async function sleep(ms: number): Promise { + return new Promise((r) => setTimeout(r, ms)); +} + +async function main(): Promise { + const hw = new HardwareService({ path, baudRate, heartbeatIntervalMs: 1000 }); + hw.on('log', (line) => console.log(`[firmware] ${line}`)); + hw.on('error', (err) => console.error(`[firmware error] ${err.message}`)); + + console.log(`→ opening ${path} @ ${baudRate} baud`); + await hw.connect(); + + try { + const rtt = await hw.ping(Buffer.from('hello')); + console.log(`← pong (rtt ${rtt.toFixed(1)} ms)`); + } catch (err) { + console.warn(`ping failed: ${(err as Error).message}`); + } + + for (const emotion of EMOTION_SEQUENCE) { + console.log(`→ emotion ${Emotion[emotion]}`); + hw.sendEmotion(emotion); + await sleep(1200); + } + + await hw.disconnect(); + console.log('done.'); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/robot-client/scripts/verify-protocol.ts b/apps/robot-client/scripts/verify-protocol.ts new file mode 100644 index 0000000..1fa3b95 --- /dev/null +++ b/apps/robot-client/scripts/verify-protocol.ts @@ -0,0 +1,64 @@ +// One-off verification script used during development to ensure the +// TS protocol codec round-trips correctly and matches the C++ side. +// Not intended to ship — kept because vitest couldn't run in the +// sandbox where this was bootstrapped, and a plain tsx script lets +// us re-run it quickly on any machine. + +import { + crc8, + encodeFrame, + FrameDecoder, + MsgType, + Emotion, + type DecodedFrame, +} from '../src/hardware/protocol.js'; + +function assert(cond: unknown, msg: string): void { + if (!cond) { + console.error(`FAIL: ${msg}`); + process.exitCode = 1; + } else { + console.log(`ok: ${msg}`); + } +} + +// CRC reference values (also validated against Python implementation). +assert(crc8(Buffer.from('123456789', 'ascii')) === 0xf4, "crc8('123456789') === 0xF4"); +assert(crc8(Buffer.from([0x00])) === 0x00, 'crc8([0x00]) === 0x00'); +assert(crc8(Buffer.from([0x01])) === 0x07, 'crc8([0x01]) === 0x07'); +assert(crc8(Buffer.from([0xaa])) === 0x5f, 'crc8([0xAA]) === 0x5F'); + +// DISPLAY_EMOTION HAPPY must produce a 6-byte frame with CRC 0xDC +// (matches Python reference computed in verify script). +const happy = encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.HAPPY])); +assert(happy.length === 6, 'DISPLAY_EMOTION frame length === 6'); +assert(happy[0] === 0xaa, 'start byte 0xAA'); +assert(happy[1] === 0x20, 'type 0x20'); +assert(happy[2] === 0x00 && happy[3] === 0x01, 'length = 1'); +assert(happy[4] === 0x01, 'payload = HAPPY (1)'); +assert(happy[5] === 0xdc, 'CRC === 0xDC'); + +// Round-trip through the decoder. +let received: DecodedFrame | null = null; +const decoder = new FrameDecoder((f) => { + received = f; +}); +decoder.feed(happy); +assert(received !== null, 'frame emitted'); +assert(received!.type === MsgType.DISPLAY_EMOTION, 'type preserved'); +assert(received!.payload[0] === Emotion.HAPPY, 'payload preserved'); + +// Resync after corrupted CRC. +received = null; +const bad = Buffer.from(happy); +bad[bad.length - 1] = (bad[bad.length - 1]! ^ 0xff) & 0xff; +decoder.feed(bad); +assert(received === null, 'bad CRC not emitted'); +assert(decoder.framesDropped === 1, 'drop counter === 1'); + +const again = encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.WINK])); +decoder.feed(again); +assert(received !== null, 'decoder resynced'); +assert((received as unknown as DecodedFrame).payload[0] === Emotion.WINK, 'resync payload ok'); + +console.log('\nall protocol checks passed'); diff --git a/apps/robot-client/src/config/hardware.config.ts b/apps/robot-client/src/config/hardware.config.ts index 7fd4934..c071238 100644 --- a/apps/robot-client/src/config/hardware.config.ts +++ b/apps/robot-client/src/config/hardware.config.ts @@ -32,9 +32,24 @@ export interface WakeWordConfig { threshold: number; } +export interface SerialHardwareConfig { + /** Enable the serial link to the ESP32 firmware. */ + enabled: boolean; + + /** Serial device path. Linux/macOS: /dev/ttyUSB0, /dev/ttyACM0. Windows: COM3. */ + path: string; + + /** Baud rate. Must match HW_SERIAL_BAUD in apps/robot-hardware/platformio.ini. */ + baudRate: number; + + /** Interval between STATUS heartbeats to the firmware (ms). 0 disables. */ + heartbeatIntervalMs: number; +} + export interface HardwareConfig { audio: AudioConfig; wakeWord: WakeWordConfig; + serial: SerialHardwareConfig; } export function loadHardwareConfig(): HardwareConfig { @@ -53,5 +68,11 @@ export function loadHardwareConfig(): HardwareConfig { modelName: process.env.WAKEWORD_MODEL || 'hey_ti_pote', threshold: parseFloat(process.env.WAKEWORD_THRESHOLD || '0.5'), }, + serial: { + enabled: (process.env.HARDWARE_SERIAL_ENABLED || 'false').toLowerCase() === 'true', + path: process.env.HARDWARE_SERIAL_PORT || '/dev/ttyUSB0', + baudRate: parseInt(process.env.HARDWARE_SERIAL_BAUD || '921600', 10), + heartbeatIntervalMs: parseInt(process.env.HARDWARE_HEARTBEAT_MS || '1000', 10), + }, }; } diff --git a/apps/robot-client/src/config/robot.config.ts b/apps/robot-client/src/config/robot.config.ts index 35c6dc5..746fd04 100644 --- a/apps/robot-client/src/config/robot.config.ts +++ b/apps/robot-client/src/config/robot.config.ts @@ -23,11 +23,11 @@ export interface RobotConfig { /** How conversations are triggered */ triggerMode: TriggerMode; - /** Unique device identifier (generated at first setup or from env) */ - deviceId: string; + /** Unique device identifier (from env, store, or pairing) */ + deviceId?: string; - /** JWT device token for cloud authentication */ - deviceToken: string; + /** JWT device token for cloud authentication (from env, store, or pairing) */ + deviceToken?: string; /** Cloud backend WebSocket URL */ cloudUrl: string; @@ -58,18 +58,11 @@ export function loadRobotConfig(): RobotConfig { return { mode, triggerMode, - deviceId: requireEnv('DEVICE_ID'), - deviceToken: requireEnv('DEVICE_TOKEN'), + deviceId: process.env.DEVICE_ID || undefined, + deviceToken: process.env.DEVICE_TOKEN || undefined, cloudUrl: process.env.CLOUD_URL || 'ws://localhost:3000', robotName: process.env.ROBOT_NAME || 'Ti-Pote', logLevel: process.env.LOG_LEVEL || 'info', }; } -function requireEnv(key: string): string { - const value = process.env[key]; - if (!value) { - throw new Error(`Missing required environment variable: ${key}`); - } - return value; -} diff --git a/apps/robot-client/src/hardware/hardware.service.ts b/apps/robot-client/src/hardware/hardware.service.ts new file mode 100644 index 0000000..f243dfc --- /dev/null +++ b/apps/robot-client/src/hardware/hardware.service.ts @@ -0,0 +1,211 @@ +import { EventEmitter } from 'node:events'; +import { SerialPort } from 'serialport'; +import { createLogger } from '../utils/index.js'; +import { + encodeFrame, + FrameDecoder, + MsgType, + Emotion, + type DecodedFrame, +} from './protocol.js'; + +export interface HardwareServiceOptions { + /** Serial device path. Examples: '/dev/ttyUSB0', '/dev/ttyACM0', 'COM3'. */ + path: string; + /** Baud rate. Must match HW_SERIAL_BAUD in apps/robot-hardware/platformio.ini. */ + baudRate: number; + /** Interval between STATUS heartbeats sent to the firmware, in ms. 0 disables. */ + heartbeatIntervalMs?: number; + /** PING round-trip timeout when calling `ping()`, in ms. */ + pingTimeoutMs?: number; +} + +export interface HardwareServiceEvents { + open: () => void; + close: () => void; + error: (err: Error) => void; + log: (message: string) => void; + frame: (frame: DecodedFrame) => void; + ack: (payload: Buffer) => void; +} + +/** + * HardwareService — the robot-client's only direct link to the ESP32. + * + * Responsibilities: + * - Own the `SerialPort` instance. + * - Reassemble binary frames via `FrameDecoder`. + * - Expose a small typed command surface (`sendEmotion`, `clearDisplay`, + * `ping`, …) that the rest of the client can use without caring + * about the wire format. + * - Send periodic STATUS heartbeats so the firmware does not fall + * back to its sleepy idle animation. + * + * The service is resilient to the ESP32 not being present: if the + * port cannot be opened, `connect()` throws and the caller decides + * whether that is fatal. For the MVP we treat it as non-fatal — + * the robot can still talk to the cloud even without a face. + */ +export class HardwareService extends EventEmitter { + private readonly log = createLogger('hardware', 'info'); + private readonly decoder: FrameDecoder; + private port: SerialPort | null = null; + private heartbeatTimer: NodeJS.Timeout | null = null; + + constructor(private readonly options: HardwareServiceOptions) { + super(); + this.decoder = new FrameDecoder((frame) => this.onDecodedFrame(frame)); + } + + /** + * Open the serial port and wire up event handlers. + * Rejects if the port cannot be opened within ~2 s. + */ + connect(): Promise { + return new Promise((resolve, reject) => { + if (this.port?.isOpen) { + resolve(); + return; + } + + this.log.info( + { path: this.options.path, baudRate: this.options.baudRate }, + 'Opening hardware serial port', + ); + + this.port = new SerialPort( + { + path: this.options.path, + baudRate: this.options.baudRate, + autoOpen: false, + }, + (err: Error | null) => { + // constructor callback is only called with autoOpen: true, + // so this is a no-op in practice. + if (err) reject(err); + }, + ); + + this.port.on('data', (chunk: Buffer) => this.decoder.feed(chunk)); + this.port.on('error', (err: Error) => { + this.log.error({ err }, 'Hardware serial error'); + this.emit('error', err); + }); + this.port.on('close', () => { + this.log.warn('Hardware serial port closed'); + this.stopHeartbeat(); + this.emit('close'); + }); + + this.port.open((err: Error | null) => { + if (err) { + this.log.error({ err, path: this.options.path }, 'Failed to open serial port'); + reject(err); + return; + } + this.log.info('Hardware serial port open'); + this.startHeartbeat(); + this.emit('open'); + resolve(); + }); + }); + } + + async disconnect(): Promise { + this.stopHeartbeat(); + if (!this.port?.isOpen) return; + await new Promise((resolve) => this.port!.close(() => resolve())); + this.port = null; + } + + isConnected(): boolean { + return this.port?.isOpen === true; + } + + // ────────────────────────────────────────────────────────── + // Typed command API + // ────────────────────────────────────────────────────────── + + /** Display a named emotion on the OLED eyes. */ + sendEmotion(emotion: Emotion): void { + this.writeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([emotion])); + } + + /** Blank the OLED. */ + clearDisplay(): void { + this.writeFrame(MsgType.DISPLAY_CLEAR); + } + + /** + * Round-trip PING → PONG used for bring-up and latency checks. + * Resolves with the measured RTT in ms. + */ + ping(payload: Buffer = Buffer.alloc(0)): Promise { + const timeout = this.options.pingTimeoutMs ?? 1000; + return new Promise((resolve, reject) => { + const started = performance.now(); + const timer = setTimeout(() => { + this.off('frame', handler); + reject(new Error(`ping timed out after ${timeout}ms`)); + }, timeout); + + const handler = (frame: DecodedFrame) => { + if (frame.type !== MsgType.PONG) return; + clearTimeout(timer); + this.off('frame', handler); + resolve(performance.now() - started); + }; + + this.on('frame', handler); + this.writeFrame(MsgType.PING, payload); + }); + } + + // ────────────────────────────────────────────────────────── + // Internals + // ────────────────────────────────────────────────────────── + + private writeFrame(type: MsgType, payload: Buffer = Buffer.alloc(0)): void { + if (!this.port?.isOpen) { + this.log.warn({ type }, 'Dropping frame — serial port not open'); + return; + } + this.port.write(encodeFrame(type, payload)); + } + + private onDecodedFrame(frame: DecodedFrame): void { + this.emit('frame', frame); + + switch (frame.type) { + case MsgType.LOG: + this.emit('log', frame.payload.toString('utf8')); + this.log.debug({ line: frame.payload.toString('utf8') }, 'firmware log'); + return; + case MsgType.ACK: + this.emit('ack', frame.payload); + return; + case MsgType.ERROR: + this.log.error({ payload: frame.payload.toString('utf8') }, 'firmware error'); + return; + default: + return; + } + } + + private startHeartbeat(): void { + const interval = this.options.heartbeatIntervalMs ?? 1000; + if (interval <= 0) return; + this.heartbeatTimer = setInterval(() => { + // Empty STATUS payload — the firmware only cares that *something* + // arrived recently. + this.writeFrame(MsgType.STATUS); + }, interval); + } + + private stopHeartbeat(): void { + if (this.heartbeatTimer) { + clearInterval(this.heartbeatTimer); + this.heartbeatTimer = null; + } + } +} diff --git a/apps/robot-client/src/hardware/index.ts b/apps/robot-client/src/hardware/index.ts new file mode 100644 index 0000000..da959bf --- /dev/null +++ b/apps/robot-client/src/hardware/index.ts @@ -0,0 +1,13 @@ +export { HardwareService } from './hardware.service.js'; +export type { HardwareServiceOptions } from './hardware.service.js'; +export { + Emotion, + MsgType, + FrameDecoder, + encodeFrame, + crc8, + FRAME_START, + FRAME_OVERHEAD, + MAX_PAYLOAD_SIZE, + type DecodedFrame, +} from './protocol.js'; diff --git a/apps/robot-client/src/hardware/protocol.ts b/apps/robot-client/src/hardware/protocol.ts new file mode 100644 index 0000000..4638caa --- /dev/null +++ b/apps/robot-client/src/hardware/protocol.ts @@ -0,0 +1,212 @@ +/** + * Ti-Pote — Binary UART protocol, TypeScript side. + * + * This file MUST stay byte-for-byte compatible with the C++ reference + * at `apps/robot-hardware/include/protocol_types.h` and the decoder + * implementation at `apps/robot-hardware/lib/Protocol/`. + * + * Frame layout: + * + * ┌────────┬──────┬──────────┬──────────┬─────────────┬──────┐ + * │ START │ TYPE │ LENGTH_H │ LENGTH_L │ PAYLOAD │ CRC8 │ + * │ 0xAA │ 1B │ 1B │ 1B │ 0..65535 B │ 1B │ + * └────────┴──────┴──────────┴──────────┴─────────────┴──────┘ + * + * CRC8: poly=0x07, init=0x00, no reflection, no final XOR. + * Computed over TYPE + LENGTH_H + LENGTH_L + PAYLOAD. + */ + +export const FRAME_START = 0xaa; +export const FRAME_HEADER_SIZE = 4; // START + TYPE + LEN_H + LEN_L +export const FRAME_OVERHEAD = FRAME_HEADER_SIZE + 1; // + CRC +export const MAX_PAYLOAD_SIZE = 1024; + +/** + * Message type codes — keep in sync with `MsgType` in protocol_types.h. + */ +export enum MsgType { + // Reserved for Phase 2 (not yet implemented) + AUDIO_UP = 0x01, + AUDIO_DOWN = 0x02, + SERVO_CMD = 0x03, + LED_CMD = 0x04, + STATUS = 0x05, + SENSOR_DATA = 0x06, + CONFIG = 0x07, + ACK = 0x08, + IDLE_MODE = 0x09, + + // v0 — display / eyes + DISPLAY_EMOTION = 0x20, + DISPLAY_CLEAR = 0x21, + + // v0 — bring-up / diagnostics + PING = 0xf0, + PONG = 0xf1, + LOG = 0xfd, + ERROR = 0xfe, +} + +/** + * Emotion catalogue — keep in sync with `Emotion` in protocol_types.h + * and the switch in `lib/Eyes/src/Eyes.cpp`. + */ +export enum Emotion { + NEUTRAL = 0, + HAPPY = 1, + SAD = 2, + ANGRY = 3, + SURPRISED = 4, + SLEEPY = 5, + WINK = 6, + LOVE = 7, + DIZZY = 8, + DEAD = 9, +} + +export interface DecodedFrame { + type: MsgType; + payload: Buffer; +} + +/** + * CRC-8 with polynomial 0x07, init 0x00, no reflection, no final XOR. + */ +export function crc8(data: Uint8Array | Buffer, start = 0, end = data.length): number { + let crc = 0x00; + for (let i = start; i < end; i++) { + crc ^= data[i]!; + for (let b = 0; b < 8; b++) { + crc = (crc & 0x80) !== 0 ? ((crc << 1) ^ 0x07) & 0xff : (crc << 1) & 0xff; + } + } + return crc; +} + +/** + * Build a complete framed message ready to write to the serial port. + */ +export function encodeFrame(type: MsgType, payload: Buffer = Buffer.alloc(0)): Buffer { + if (payload.length > MAX_PAYLOAD_SIZE) { + throw new Error( + `Payload too large: ${payload.length} > MAX_PAYLOAD_SIZE (${MAX_PAYLOAD_SIZE})`, + ); + } + const total = FRAME_OVERHEAD + payload.length; + const out = Buffer.alloc(total); + out[0] = FRAME_START; + out[1] = type; + out[2] = (payload.length >> 8) & 0xff; + out[3] = payload.length & 0xff; + payload.copy(out, FRAME_HEADER_SIZE); + // CRC over TYPE + LEN + PAYLOAD (skip the START byte). + out[FRAME_HEADER_SIZE + payload.length] = crc8(out, 1, FRAME_HEADER_SIZE + payload.length); + return out; +} + +/** + * Streaming decoder: feed bytes as they come off the wire and the + * decoder will emit complete, CRC-validated frames via the handler. + * + * Matches the state machine in `apps/robot-hardware/lib/Protocol/src/Protocol.cpp`. + */ +export class FrameDecoder { + private state: + | 'WAIT_START' + | 'READ_TYPE' + | 'READ_LEN_H' + | 'READ_LEN_L' + | 'READ_PAYLOAD' + | 'READ_CRC' = 'WAIT_START'; + + private type = 0; + private length = 0; + private payload: Buffer = Buffer.alloc(0); + private payloadIdx = 0; + + private _ok = 0; + private _dropped = 0; + + constructor(private readonly onFrame: (frame: DecodedFrame) => void) {} + + get framesOk(): number { + return this._ok; + } + get framesDropped(): number { + return this._dropped; + } + + feed(chunk: Buffer): void { + for (const byte of chunk) { + this.feedByte(byte); + } + } + + private reset(): void { + this.state = 'WAIT_START'; + this.type = 0; + this.length = 0; + this.payloadIdx = 0; + } + + private feedByte(byte: number): void { + switch (this.state) { + case 'WAIT_START': + if (byte === FRAME_START) this.state = 'READ_TYPE'; + return; + + case 'READ_TYPE': + this.type = byte; + this.state = 'READ_LEN_H'; + return; + + case 'READ_LEN_H': + this.length = (byte & 0xff) << 8; + this.state = 'READ_LEN_L'; + return; + + case 'READ_LEN_L': + this.length |= byte & 0xff; + if (this.length > MAX_PAYLOAD_SIZE) { + this._dropped++; + this.reset(); + return; + } + this.payload = Buffer.alloc(this.length); + this.payloadIdx = 0; + this.state = this.length === 0 ? 'READ_CRC' : 'READ_PAYLOAD'; + return; + + case 'READ_PAYLOAD': + this.payload[this.payloadIdx++] = byte; + if (this.payloadIdx === this.length) this.state = 'READ_CRC'; + return; + + case 'READ_CRC': { + // Re-materialise the header to CRC over it. + const header = Buffer.from([ + this.type, + (this.length >> 8) & 0xff, + this.length & 0xff, + ]); + let crc = crc8(header); + // Fold the payload in (matches the C++ impl exactly). + for (let i = 0; i < this.length; i++) { + crc ^= this.payload[i]!; + for (let b = 0; b < 8; b++) { + crc = (crc & 0x80) !== 0 ? ((crc << 1) ^ 0x07) & 0xff : (crc << 1) & 0xff; + } + } + + if (crc === byte) { + this._ok++; + this.onFrame({ type: this.type as MsgType, payload: this.payload }); + } else { + this._dropped++; + } + this.reset(); + return; + } + } + } +} diff --git a/apps/robot-client/src/main.ts b/apps/robot-client/src/main.ts index 1836163..e271a3b 100644 --- a/apps/robot-client/src/main.ts +++ b/apps/robot-client/src/main.ts @@ -8,9 +8,11 @@ import { OrchestratorService, LocalStore, WifiService, + PairingService, } from './services/index.js'; import { type ITriggerService } from './services/trigger.interface.js'; import { SetupFlow } from './setup/index.js'; +import { HardwareService, Emotion } from './hardware/index.js'; import { createLogger } from './utils/index.js'; const logger = createLogger('main', 'info'); @@ -33,8 +35,6 @@ async function main(): Promise { // ── Step 1: Ensure WiFi connectivity ── // Only run the setup flow (captive portal) in physical/production mode. - // In dev mode, the Pi is already on the network (configured manually). - // In simulator mode, the dev machine is already on the network. if (robotConfig.mode === 'physical') { const wifiService = new WifiService(); @@ -50,33 +50,57 @@ async function main(): Promise { } // ── Step 2: Resolve device credentials ── - // Use stored device credentials if available, fall back to env vars. + // Priority: LocalStore → env vars → auto-pairing - const deviceId = store.device?.id || robotConfig.deviceId; - const deviceToken = store.device?.token || robotConfig.deviceToken; + let deviceId = store.device?.id || robotConfig.deviceId; + let deviceToken = store.device?.token || robotConfig.deviceToken; if (!deviceId || !deviceToken) { - logger.fatal( - 'No device credentials found. Register this device on the backend first, ' + - 'then set DEVICE_ID and DEVICE_TOKEN in your .env file.', - ); - process.exit(1); + logger.info('🔗 No device credentials found — starting pairing flow...'); + + const pairingService = new PairingService(robotConfig.cloudUrl, store); + const credentials = await pairingService.pair(robotConfig.robotName); + + deviceId = credentials.deviceId; + deviceToken = credentials.deviceToken; } - // Override config with resolved credentials - const resolvedConfig = { ...robotConfig, deviceId, deviceToken }; - - logger.info({ deviceId }, 'Device credentials resolved'); + logger.info({ deviceId }, '✅ Device credentials resolved'); // ── Step 3: Initialize services ── - const cloudSocket = new CloudSocket(resolvedConfig); + const resolvedConfig = { ...robotConfig, deviceId, deviceToken }; + + const cloudSocket = new CloudSocket(resolvedConfig as Required); const audioService = new AudioService(hardwareConfig.audio); const healthService = new HealthService(cloudSocket); - // Choose trigger based on TRIGGER_MODE: - // wakeword → OpenWakeWord subprocess (requires Python + openwakeword) - // keyboard → Press Enter in terminal to talk + // ── Optional: hardware bridge (ESP32 firmware) ── + // The serial link is opt-in via HARDWARE_SERIAL_ENABLED=true. We + // treat failures here as non-fatal: even without a face, the + // robot can still converse with the cloud. + + let hardwareService: HardwareService | null = null; + if (hardwareConfig.serial.enabled) { + hardwareService = new HardwareService({ + path: hardwareConfig.serial.path, + baudRate: hardwareConfig.serial.baudRate, + heartbeatIntervalMs: hardwareConfig.serial.heartbeatIntervalMs, + }); + hardwareService.on('log', (line) => logger.debug({ line }, 'firmware log')); + try { + await hardwareService.connect(); + hardwareService.sendEmotion(Emotion.HAPPY); + logger.info('Hardware bridge connected'); + } catch (err) { + logger.warn({ err }, 'Hardware bridge unavailable — continuing without face'); + hardwareService = null; + } + } else { + logger.info('Hardware bridge disabled (set HARDWARE_SERIAL_ENABLED=true to enable)'); + } + + // Choose trigger based on TRIGGER_MODE let trigger: ITriggerService; if (resolvedConfig.triggerMode === 'wakeword') { @@ -125,6 +149,10 @@ async function main(): Promise { await orchestrator.stop(); healthService.stop(); await audioService.destroy(); + if (hardwareService) { + hardwareService.sendEmotion(Emotion.SLEEPY); + await hardwareService.disconnect(); + } await cloudSocket.disconnect(); logger.info('Goodbye!'); diff --git a/apps/robot-client/src/services/index.ts b/apps/robot-client/src/services/index.ts index 4390a33..0a25fd7 100644 --- a/apps/robot-client/src/services/index.ts +++ b/apps/robot-client/src/services/index.ts @@ -5,4 +5,5 @@ export { HealthService } from './health.service.js'; export { OrchestratorService } from './orchestrator.service.js'; export { LocalStore } from './local-store.service.js'; export { WifiService } from './wifi.service.js'; +export { PairingService } from './pairing.service.js'; export { type ITriggerService } from './trigger.interface.js'; diff --git a/apps/robot-client/src/services/local-store.service.ts b/apps/robot-client/src/services/local-store.service.ts index 6357704..de17a5b 100644 --- a/apps/robot-client/src/services/local-store.service.ts +++ b/apps/robot-client/src/services/local-store.service.ts @@ -1,5 +1,6 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; -import { dirname } from 'node:path'; +import { dirname, join } from 'node:path'; +import { homedir } from 'node:os'; import { createLogger, type Logger } from '../utils/index.js'; /** @@ -31,7 +32,8 @@ export interface LocalStoreData { setupComplete?: boolean; } -const DEFAULT_STORE_PATH = '/home/pi/.tipote/config.json'; +const DEFAULT_STORE_PATH = process.env.TIPOTE_STORE_PATH + || join(homedir(), '.tipote', 'config.json'); /** * Simple JSON file store for persisting robot config on the SD card. diff --git a/apps/robot-client/src/services/pairing.service.ts b/apps/robot-client/src/services/pairing.service.ts new file mode 100644 index 0000000..bd0750d --- /dev/null +++ b/apps/robot-client/src/services/pairing.service.ts @@ -0,0 +1,177 @@ +import { createLogger, type Logger } from '../utils/index.js'; +import { type LocalStore } from './local-store.service.js'; + +interface PairingRequestResponse { + requestId: string; + code: string; +} + +interface PairingStatusResponse { + status: 'pending' | 'confirmed'; + code?: string; + deviceId?: string; + deviceToken?: string; + homeId?: string; +} + +/** + * Pairing service — handles automatic device registration. + * + * Flow: + * 1. Call backend POST /pairing/request → get a 6-digit code + * 2. Display the code on screen (HDMI / captive portal) + * 3. Poll GET /pairing/status/:requestId until confirmed + * 4. Store credentials in LocalStore + */ +export class PairingService { + private readonly logger: Logger; + + constructor( + private readonly cloudUrl: string, + private readonly store: LocalStore, + ) { + this.logger = createLogger('pairing', 'info'); + } + + /** + * Run the full pairing flow. + * Returns device credentials once the user confirms on the app. + */ + async pair(deviceName: string): Promise<{ deviceId: string; deviceToken: string }> { + // Step 1: Request a pairing code from the backend + const backendUrl = this.cloudUrl.replace(/^ws/, 'http'); + this.logger.info({ backendUrl }, 'Requesting pairing code from backend...'); + + const { requestId, code } = await this.requestPairing(backendUrl, deviceName); + + // Step 2: Display the code + this.displayCode(code); + + // Step 3: Poll for confirmation + this.logger.info('Waiting for user to confirm pairing on the app...'); + const credentials = await this.pollForConfirmation(backendUrl, requestId); + + // Step 4: Store credentials + this.store.setDevice(credentials.deviceId, credentials.deviceToken, credentials.homeId); + this.store.markSetupComplete(); + + this.logger.info({ deviceId: credentials.deviceId }, '✅ Device paired successfully!'); + + return { + deviceId: credentials.deviceId, + deviceToken: credentials.deviceToken, + }; + } + + private async requestPairing(backendUrl: string, deviceName: string): Promise { + const res = await fetch(`${backendUrl}/api/pairing/request`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ deviceName }), + }); + + if (!res.ok) { + const body = await res.text(); + throw new Error(`Pairing request failed (${res.status}): ${body}`); + } + + return res.json() as Promise; + } + + private async pollForConfirmation( + backendUrl: string, + requestId: string, + ): Promise<{ deviceId: string; deviceToken: string; homeId: string }> { + const pollIntervalMs = 3000; + const maxAttempts = 200; // 10 minutes at 3s intervals + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + await this.sleep(pollIntervalMs); + + try { + const res = await fetch(`${backendUrl}/api/pairing/status/${requestId}`); + + if (!res.ok) { + if (res.status === 404) { + throw new Error('Pairing request expired. Please restart the robot to try again.'); + } + continue; + } + + const data = (await res.json()) as PairingStatusResponse; + + if (data.status === 'confirmed' && data.deviceId && data.deviceToken && data.homeId) { + return { + deviceId: data.deviceId, + deviceToken: data.deviceToken, + homeId: data.homeId, + }; + } + } catch (err) { + if (err instanceof Error && err.message.includes('expired')) { + throw err; + } + this.logger.debug({ err }, 'Poll error (will retry)'); + } + } + + throw new Error('Pairing timed out. Please restart the robot to try again.'); + } + + /** + * Display the pairing code prominently. + * For now: big ASCII art in the console (visible on HDMI). + * Future: display on an OLED/LCD screen. + */ + private displayCode(code: string): void { + const digits = code.split(''); + const big = digits.map((d) => this.bigDigit(d)); + + // Build 5 lines of ASCII art + const lines: string[] = []; + for (let row = 0; row < 5; row++) { + lines.push(big.map((d) => d[row]).join(' ')); + } + + console.log(''); + console.log(`╔${'═'.repeat(58)}╗`); + console.log(`║${'PAIRING CODE'.padStart(35).padEnd(58)}║`); + console.log(`╠${'═'.repeat(58)}╣`); + console.log(`║${' '.repeat(58)}║`); + for (const line of lines) { + console.log(`║ ${line.padEnd(56)}║`); + } + console.log(`║${' '.repeat(58)}║`); + console.log(`║${'Enter this code in the Ti-Pote app'.padStart(46).padEnd(58)}║`); + console.log(`║${'to pair this robot to your account'.padStart(46).padEnd(58)}║`); + console.log(`║${' '.repeat(58)}║`); + console.log(`║${'Code expires in 10 minutes'.padStart(42).padEnd(58)}║`); + console.log(`╚${'═'.repeat(58)}╝`); + console.log(''); + + this.logger.info({ code }, '📱 Pairing code displayed — enter it in the Ti-Pote app'); + } + + /** + * 5-line ASCII art for a single digit (3 chars wide). + */ + private bigDigit(d: string): string[] { + const digits: Record = { + '0': [' ██ ', '█ █', '█ █', '█ █', ' ██ '], + '1': [' █ ', ' ██ ', ' █ ', ' █ ', ' ███'], + '2': [' ██ ', '█ █', ' █ ', ' █ ', '████'], + '3': ['████', ' █', ' ██ ', ' █', '████'], + '4': ['█ █', '█ █', '████', ' █', ' █'], + '5': ['████', '█ ', '███ ', ' █', '███ '], + '6': [' ██ ', '█ ', '███ ', '█ █', ' ██ '], + '7': ['████', ' █', ' █ ', ' █ ', ' █ '], + '8': [' ██ ', '█ █', ' ██ ', '█ █', ' ██ '], + '9': [' ██ ', '█ █', ' ███', ' █', ' ██ '], + }; + return digits[d] || [' ', ' ', ' ', ' ', ' ']; + } + + private sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/apps/robot-client/src/services/wake-word.service.ts b/apps/robot-client/src/services/wake-word.service.ts index 9e6076a..228fb42 100644 --- a/apps/robot-client/src/services/wake-word.service.ts +++ b/apps/robot-client/src/services/wake-word.service.ts @@ -110,7 +110,8 @@ export class WakeWordService extends EventEmitter { } else if (msg.startsWith('Matched device') || msg.startsWith('Using device')) { this.logger.info(`🔊 ${msg}`); } else { - this.logger.debug({ msg }, 'Wake word stderr'); + // Log unknown stderr messages at warn level to catch errors + this.logger.warn({ msg }, 'Wake word stderr'); } } }); diff --git a/apps/robot-client/src/services/wifi.service.ts b/apps/robot-client/src/services/wifi.service.ts index 9a9da77..39c370b 100644 --- a/apps/robot-client/src/services/wifi.service.ts +++ b/apps/robot-client/src/services/wifi.service.ts @@ -165,11 +165,16 @@ export class WifiService { await execAsync('nmcli connection delete Hotspot').catch(() => { /* ignore */ }); // Create and start an open hotspot (no password — easier for setup) + // Run commands sequentially with delays — chaining with && causes activation failures await execAsync( - `nmcli connection add type wifi ifname wlan0 con-name Hotspot autoconnect no ssid "${this.escapeShell(name)}" && ` + - `nmcli connection modify Hotspot 802-11-wireless.mode ap ipv4.method shared ipv4.addresses 192.168.4.1/24 && ` + - `nmcli connection up Hotspot`, + `nmcli connection add type wifi ifname wlan0 con-name Hotspot autoconnect no ssid "${this.escapeShell(name)}"`, ); + await new Promise((r) => setTimeout(r, 1000)); + await execAsync( + `nmcli connection modify Hotspot 802-11-wireless.mode ap ipv4.method shared ipv4.addresses 192.168.4.1/24`, + ); + await new Promise((r) => setTimeout(r, 1000)); + await execAsync('nmcli connection up Hotspot'); this.logger.info({ apName: name }, 'Access Point started'); return true; diff --git a/apps/robot-client/src/setup/captive-portal.ts b/apps/robot-client/src/setup/captive-portal.ts index 9d32a4c..8b4133b 100644 --- a/apps/robot-client/src/setup/captive-portal.ts +++ b/apps/robot-client/src/setup/captive-portal.ts @@ -82,6 +82,17 @@ export class CaptivePortal { this.logger.debug({ method, url }, 'Request'); try { + // ── CORS preflight for Tauri app ── + if (method === 'OPTIONS') { + res.writeHead(204, { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }); + res.end(); + return; + } + // ── Captive portal detection URLs ── // iOS, Android, Windows, etc. check these URLs to detect captive portals. // Redirecting them triggers the captive portal popup on the user's device. @@ -93,6 +104,20 @@ export class CaptivePortal { // ── API routes ── + if (url === '/api/status' && method === 'GET') { + res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); + res.end(JSON.stringify({ ready: true, robotName: this.robotName })); + return; + } + + if (url === '/api/wifi/status' && method === 'GET') { + const connected = await this.wifiService.isConnected(); + const ssid = connected ? await this.wifiService.currentSSID() : null; + res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); + res.end(JSON.stringify({ connected, ssid })); + return; + } + if (url === '/api/wifi/scan' && method === 'GET') { await this.handleScan(res); return; @@ -141,7 +166,7 @@ export class CaptivePortal { private async handleScan(res: ServerResponse): Promise { const networks = await this.wifiService.scan(); - res.writeHead(200, { 'Content-Type': 'application/json' }); + res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify(networks)); } @@ -159,13 +184,13 @@ export class CaptivePortal { ssid = parsed.ssid; password = parsed.password; } catch { - res.writeHead(400, { 'Content-Type': 'application/json' }); + res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, error: 'Invalid request body' })); return; } if (!ssid) { - res.writeHead(400, { 'Content-Type': 'application/json' }); + res.writeHead(400, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: false, error: 'SSID is required' })); return; } @@ -186,7 +211,7 @@ export class CaptivePortal { // Respond with success (we may lose the connection since AP is down, // but the page JavaScript handles this gracefully) - res.writeHead(200, { 'Content-Type': 'application/json' }); + res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end(JSON.stringify({ success: true })); // Notify the setup flow that WiFi is configured @@ -198,7 +223,7 @@ export class CaptivePortal { // Connection failed — restart AP so user can retry await this.wifiService.startAP(`Ti-Pote`); - res.writeHead(200, { 'Content-Type': 'application/json' }); + res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }); res.end( JSON.stringify({ success: false, diff --git a/apps/robot-client/src/setup/pairing-app.html.ts b/apps/robot-client/src/setup/pairing-app.html.ts new file mode 100644 index 0000000..4369479 --- /dev/null +++ b/apps/robot-client/src/setup/pairing-app.html.ts @@ -0,0 +1,333 @@ +/** + * Generates the HTML page for the desktop pairing app. + * This is served by the backend or opened locally. + * Allows the user to log in and enter the 6-digit pairing code. + */ +export function pairingAppHTML(backendUrl: string): string { + return ` + + + + + Ti-Pote — Appairer mon robot + + + +
+ + +
+
+
+
+
+
+ + +
+

Connexion

+ +
+ + +
+
+ + +
+ +
+ + + + + + +
+
+ + + +`; +} diff --git a/apps/robot-client/tests/hardware/protocol.test.ts b/apps/robot-client/tests/hardware/protocol.test.ts new file mode 100644 index 0000000..d4d47ab --- /dev/null +++ b/apps/robot-client/tests/hardware/protocol.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect } from 'vitest'; +import { + crc8, + encodeFrame, + FrameDecoder, + FRAME_START, + MsgType, + Emotion, + type DecodedFrame, +} from '../../src/hardware/protocol.js'; + +describe('hardware/protocol', () => { + describe('crc8', () => { + it('returns 0 for an empty buffer', () => { + expect(crc8(Buffer.alloc(0))).toBe(0); + }); + + it('matches the known-good value for a single byte', () => { + // CRC-8/SMBus (poly 0x07, init 0x00) of [0x00] is 0x00. + expect(crc8(Buffer.from([0x00]))).toBe(0x00); + // CRC-8 of [0x01] = 0x07. + expect(crc8(Buffer.from([0x01]))).toBe(0x07); + // CRC-8 of [0xAA] = 0x5F. + expect(crc8(Buffer.from([0xaa]))).toBe(0x5f); + }); + + it('is stable across reference inputs', () => { + // The string "123456789" is the canonical CRC test vector — + // CRC-8 (poly 0x07, init 0x00) = 0xF4. + expect(crc8(Buffer.from('123456789', 'ascii'))).toBe(0xf4); + }); + }); + + describe('encodeFrame', () => { + it('builds a well-formed DISPLAY_EMOTION frame', () => { + const frame = encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.HAPPY])); + expect(frame[0]).toBe(FRAME_START); + expect(frame[1]).toBe(MsgType.DISPLAY_EMOTION); + expect(frame[2]).toBe(0x00); // length high byte + expect(frame[3]).toBe(0x01); // length low byte + expect(frame[4]).toBe(Emotion.HAPPY); + // The last byte is CRC8 over bytes [1..5). + expect(frame[5]).toBe(crc8(frame, 1, 5)); + expect(frame.length).toBe(6); + }); + + it('handles zero-length payloads (DISPLAY_CLEAR, STATUS)', () => { + const frame = encodeFrame(MsgType.DISPLAY_CLEAR); + expect(frame.length).toBe(5); + expect(frame[0]).toBe(FRAME_START); + expect(frame[1]).toBe(MsgType.DISPLAY_CLEAR); + expect(frame[2]).toBe(0x00); + expect(frame[3]).toBe(0x00); + // CRC over just [type, 0, 0]. + expect(frame[4]).toBe(crc8(Buffer.from([MsgType.DISPLAY_CLEAR, 0, 0]))); + }); + + it('rejects oversized payloads', () => { + expect(() => encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.alloc(2048))).toThrow(); + }); + }); + + describe('FrameDecoder', () => { + it('decodes a frame fed in one chunk', () => { + const received: DecodedFrame[] = []; + const decoder = new FrameDecoder((f) => received.push(f)); + decoder.feed(encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.LOVE]))); + expect(received).toHaveLength(1); + expect(received[0]!.type).toBe(MsgType.DISPLAY_EMOTION); + expect(received[0]!.payload[0]).toBe(Emotion.LOVE); + expect(decoder.framesOk).toBe(1); + expect(decoder.framesDropped).toBe(0); + }); + + it('decodes a frame fed byte-by-byte', () => { + const received: DecodedFrame[] = []; + const decoder = new FrameDecoder((f) => received.push(f)); + const frame = encodeFrame(MsgType.PONG, Buffer.from('hello')); + for (const b of frame) decoder.feed(Buffer.from([b])); + expect(received).toHaveLength(1); + expect(received[0]!.type).toBe(MsgType.PONG); + expect(received[0]!.payload.toString('utf8')).toBe('hello'); + }); + + it('resyncs after a corrupted CRC', () => { + const received: DecodedFrame[] = []; + const decoder = new FrameDecoder((f) => received.push(f)); + + const bad = encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.SAD])); + bad[bad.length - 1] = (bad[bad.length - 1]! ^ 0xff) & 0xff; // flip CRC + decoder.feed(bad); + + const good = encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.HAPPY])); + decoder.feed(good); + + expect(received).toHaveLength(1); + expect(received[0]!.payload[0]).toBe(Emotion.HAPPY); + expect(decoder.framesDropped).toBe(1); + expect(decoder.framesOk).toBe(1); + }); + + it('ignores noise before the start byte', () => { + const received: DecodedFrame[] = []; + const decoder = new FrameDecoder((f) => received.push(f)); + decoder.feed(Buffer.from([0x00, 0x12, 0x34, 0xff])); // garbage + decoder.feed(encodeFrame(MsgType.PING)); + expect(received).toHaveLength(1); + expect(received[0]!.type).toBe(MsgType.PING); + }); + + it('handles two back-to-back frames in one chunk', () => { + const received: DecodedFrame[] = []; + const decoder = new FrameDecoder((f) => received.push(f)); + const buf = Buffer.concat([ + encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.HAPPY])), + encodeFrame(MsgType.DISPLAY_EMOTION, Buffer.from([Emotion.WINK])), + ]); + decoder.feed(buf); + expect(received).toHaveLength(2); + expect(received[0]!.payload[0]).toBe(Emotion.HAPPY); + expect(received[1]!.payload[0]).toBe(Emotion.WINK); + }); + }); +}); diff --git a/apps/robot-hardware/.gitignore b/apps/robot-hardware/.gitignore new file mode 100644 index 0000000..69ea882 --- /dev/null +++ b/apps/robot-hardware/.gitignore @@ -0,0 +1,8 @@ +.pio/ +.vscode/ +.cache/ +*.bin +*.elf +*.map +*.hex +*.log diff --git a/apps/robot-hardware/README.md b/apps/robot-hardware/README.md new file mode 100644 index 0000000..dcc7773 --- /dev/null +++ b/apps/robot-hardware/README.md @@ -0,0 +1,115 @@ +# robot-hardware + +Firmware ESP32 de Ti-Pote. Gère l'OLED (yeux animatroniques), et +à terme l'audio I2S, les servos et les LEDs. Communique avec le +`robot-client` (Pi Zero / laptop en dev) via un protocole binaire +sur UART. + +## Stack + +- **Framework** : Arduino via [PlatformIO](https://platformio.org/) +- **Cibles** : `esp32dev` (prototype actuel) et `esp32-s3` (cible finale, cf. `docs/hardware.md`) +- **Libs** : `U8g2` pour l'OLED SSD1309 + +## Layout + +``` +apps/robot-hardware/ +├── platformio.ini # envs esp32dev + esp32-s3 +├── include/ +│ └── protocol_types.h # Référence du protocole (C++) — mirroir TS côté robot-client +├── src/ +│ └── main.cpp # setup/loop : RX frames → dispatch → Eyes +├── lib/ +│ ├── Protocol/ # Encoder/decoder binaire + CRC8 +│ └── Eyes/ # Rendu des 10 émotions sur SSD1309 +└── legacy/ # Prototypes .ino originaux (référence) +``` + +## Protocole UART (v0) + +``` +┌────────┬──────┬──────────┬──────────┬─────────────┬──────┐ +│ START │ TYPE │ LENGTH_H │ LENGTH_L │ PAYLOAD │ CRC8 │ +│ 0xAA │ 1B │ 1B │ 1B │ 0..65535 B │ 1B │ +└────────┴──────┴──────────┴──────────┴─────────────┴──────┘ +``` + +CRC8 : polynôme `0x07`, init `0x00`, calculé sur +`TYPE + LENGTH + PAYLOAD`. + +Types de messages implémentés dans cette itération : + +| Code | Nom | Direction | Payload | +|---------|--------------------|-------------|----------------------------| +| `0x05` | `STATUS` | host → fw | vide (heartbeat) | +| `0x08` | `ACK` | fw → host | opaque (contexte) | +| `0x20` | `DISPLAY_EMOTION` | host → fw | 1 byte (code émotion 0..9) | +| `0x21` | `DISPLAY_CLEAR` | host → fw | vide | +| `0xF0` | `PING` | host → fw | opaque | +| `0xF1` | `PONG` | fw → host | echo du PING | +| `0xFD` | `LOG` | fw → host | texte UTF-8 | +| `0xFE` | `ERROR` | fw → host | texte UTF-8 | + +Les codes `0x01..0x09` sont réservés pour la Phase 2 (audio, servos, +LEDs) — voir `docs/hardware.md`. + +## Build & flash + +Installe [PlatformIO Core](https://docs.platformio.org/en/latest/core/installation/index.html) +(extension VSCode ou `pipx install platformio`), puis : + +```bash +cd apps/robot-hardware + +# Compile +pio run + +# Flash (auto-détection du port) +pio run -t upload + +# Terminal série — tu verras les LOG frames en binaire + les +# bytes bruts. Pour un monitor plus lisible pendant le bring-up, +# préfère la demo TypeScript (voir ci-dessous). +pio device monitor +``` + +Pour cibler l'ESP32-S3 : + +```bash +pio run -e esp32-s3 -t upload +``` + +## Câblage OLED (SSD1309, 4-wire HW SPI) + +| OLED | ESP32 (VSPI) | +|------|--------------| +| VCC | 3.3V | +| GND | GND | +| SCK | GPIO18 | +| SDA | GPIO23 | +| CS | GPIO5 | +| DC | GPIO16 | +| RES | GPIO17 | + +Si tu changes ces pins, modifie `EyesPins` dans `src/main.cpp` (ou +passe des pins custom à `Eyes{EyesPins{...}}`). + +## Tester depuis le robot-client + +Avec l'ESP32 branché en USB sur ton laptop : + +```bash +export HARDWARE_SERIAL_PORT=/dev/ttyUSB0 # ou /dev/ttyACM0, /dev/tty.usbserial-XXXX… +pnpm --filter @ti-pote/robot-client hw:demo +``` + +La demo fait un `ping` puis cycle sur les 10 émotions. Tu dois +voir les yeux changer toutes les 1.2 s sur l'OLED. + +## Phase 2 (à venir) + +- `AUDIO_UP` / `AUDIO_DOWN` : streaming I2S 16 kHz via buffer DMA +- `SERVO_CMD` : contrôle PWM des servos de tête +- `LED_CMD` : animations NeoPixel +- Bascule de `Serial` vers `Serial2` pour la liaison Pi ↔ ESP32 diff --git a/apps/robot-hardware/include/protocol_types.h b/apps/robot-hardware/include/protocol_types.h new file mode 100644 index 0000000..29dee7f --- /dev/null +++ b/apps/robot-hardware/include/protocol_types.h @@ -0,0 +1,91 @@ +// Ti-Pote — Shared protocol definitions (firmware side) +// +// This header is the C++ reference for the binary UART protocol between +// the robot-client (Pi Zero 2W, Node.js) and the robot-hardware (ESP32). +// +// The TypeScript mirror lives at: +// apps/robot-client/src/hardware/protocol.ts +// +// Any change here MUST be mirrored there — the byte layout and CRC must +// match exactly or frames will be rejected. +// +// Frame layout (see docs/hardware.md): +// +// ┌────────┬──────┬──────────┬──────────┬─────────────┬──────┐ +// │ START │ TYPE │ LENGTH_H │ LENGTH_L │ PAYLOAD │ CRC8 │ +// │ 0xAA │ 1B │ 1B │ 1B │ 0..65535 B │ 1B │ +// └────────┴──────┴──────────┴──────────┴─────────────┴──────┘ +// +// CRC8 is computed over TYPE + LENGTH_H + LENGTH_L + PAYLOAD, +// polynomial 0x07, init 0x00, no reflection, no final XOR. + +#pragma once + +#include + +namespace tipote { + +static constexpr uint8_t FRAME_START = 0xAA; +static constexpr uint16_t MAX_PAYLOAD_SIZE = 1024; // plenty for v0; audio will bump this later +static constexpr size_t FRAME_HEADER_SIZE = 4; // START + TYPE + LEN_H + LEN_L +static constexpr size_t FRAME_OVERHEAD = FRAME_HEADER_SIZE + 1; // + CRC + +// Message type codes. +// +// The 0x01..0x09 range is reserved for the full protocol defined in +// docs/hardware.md (AUDIO_UP, AUDIO_DOWN, SERVO_CMD, LED_CMD, STATUS, +// SENSOR_DATA, CONFIG, ACK, IDLE_MODE). We implement a subset for v0 +// (just what's needed to drive the OLED eyes) and start the display +// specific codes at 0x20 to leave room for the rest. +enum class MsgType : uint8_t { + // Phase 1 reserved (not yet implemented) + AUDIO_UP = 0x01, + AUDIO_DOWN = 0x02, + SERVO_CMD = 0x03, + LED_CMD = 0x04, + STATUS = 0x05, + SENSOR_DATA = 0x06, + CONFIG = 0x07, + ACK = 0x08, + IDLE_MODE = 0x09, + + // v0 — display / eyes + DISPLAY_EMOTION = 0x20, // payload: 1 byte (Emotion code) + DISPLAY_CLEAR = 0x21, // payload: 0 bytes + + // v0 — bring-up / diagnostics + PING = 0xF0, // payload: optional opaque bytes, echoed back + PONG = 0xF1, // reply to PING + LOG = 0xFD, // firmware → host human-readable log line + ERROR = 0xFE, // firmware → host error code + message +}; + +// Emotion catalogue — must match the TS side AND the Eyes library. +enum class Emotion : uint8_t { + NEUTRAL = 0, + HAPPY = 1, + SAD = 2, + ANGRY = 3, + SURPRISED = 4, + SLEEPY = 5, + WINK = 6, + LOVE = 7, + DIZZY = 8, + DEAD = 9, + COUNT +}; + +// CRC-8, poly=0x07, init=0x00. Small and matches what the TS side computes. +inline uint8_t crc8(const uint8_t* data, size_t len) { + uint8_t crc = 0x00; + for (size_t i = 0; i < len; ++i) { + crc ^= data[i]; + for (uint8_t b = 0; b < 8; ++b) { + crc = (crc & 0x80) ? static_cast((crc << 1) ^ 0x07) + : static_cast(crc << 1); + } + } + return crc; +} + +} // namespace tipote diff --git a/apps/robot-hardware/legacy/README.md b/apps/robot-hardware/legacy/README.md new file mode 100644 index 0000000..26ae12d --- /dev/null +++ b/apps/robot-hardware/legacy/README.md @@ -0,0 +1,14 @@ +# Legacy Arduino sketches + +Les fichiers de ce dossier sont les **prototypes originaux** en `.ino` +d'avril 2026, avant la restructuration du projet en PlatformIO. + +Ils sont conservés comme **référence visuelle** : la géométrie de +`robot-emotion.ino` a été portée 1:1 dans `lib/Eyes/src/Eyes.cpp`, +donc à pixel près le rendu du firmware PlatformIO doit être identique. + +- `robot-hardware.ino` — premier test d'affichage (neutre statique). +- `robot-emotion/robot-emotion.ino` — cycle des 10 émotions. + +Ne pas les utiliser pour de nouveaux développements : le vrai point +d'entrée est désormais `src/main.cpp`. diff --git a/apps/robot-hardware/legacy/robot-emotion/robot-emotion.ino b/apps/robot-hardware/legacy/robot-emotion/robot-emotion.ino new file mode 100644 index 0000000..1ba4f37 --- /dev/null +++ b/apps/robot-hardware/legacy/robot-emotion/robot-emotion.ino @@ -0,0 +1,253 @@ +#include +#include +#include + +#define CS 5 +#define DC 16 +#define RESET 17 + +U8G2_SSD1309_128X64_NONAME0_F_4W_HW_SPI u8g2(U8G2_R2, CS, DC, RESET); + +// ---- Géométrie commune ---- +const int EYE_R = 29; +const int BAR_THICK = 5; +const int BAR_PITCH = 6; +const int BAR_ROUND = 2; +const int N_BARS = 10; + +const int EYE_L_CX = 32; +const int EYE_R_CX = 96; +const int EYE_CY = 32; + +const int PUPIL_R = 11; + +enum Emotion { + NEUTRAL = 0, HAPPY, SAD, ANGRY, SURPRISED, + SLEEPY, WINK, LOVE, DIZZY, DEAD, N_EMOTIONS +}; + +// ---- Utilitaire : trace une barre horizontale arrondie dont la largeur +// suit le profil d'un cercle de rayon eyeR, à l'offset vertical dy ---- +void drawBar(int cx, int cy, int dy, int eyeR) { + int dy2 = dy * dy; + int r2 = eyeR * eyeR; + if (dy2 >= r2) return; + int halfW = (int)sqrtf((float)(r2 - dy2)); + int topY = cy + dy - BAR_THICK / 2; + int w = 2 * halfW + 1; + if (w > 2 * BAR_ROUND && BAR_THICK > 2 * BAR_ROUND) { + u8g2.drawRBox(cx - halfW, topY, w, BAR_THICK, BAR_ROUND); + } else { + u8g2.drawBox(cx - halfW, topY, w, BAR_THICK); + } +} + +// ========================================================================= +// 1) NEUTRAL : état "par défaut", 10 barres, 4e supprimée, pupille haute, +// 3e barre redessinée par-dessus la pupille. +// ========================================================================= +void drawNeutral(int cx, int cy) { + const int SKIP_BAR = 3; + const int OVER_BAR = 2; + const int PUPIL_DY = -3; + + for (int i = 0; i < N_BARS; i++) { + if (i == SKIP_BAR) continue; + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar(cx, cy, (int)roundf(off), EYE_R); + } + u8g2.setDrawColor(0); + u8g2.drawDisc(cx, cy + PUPIL_DY, PUPIL_R); + u8g2.setDrawColor(1); + float off = (OVER_BAR - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar(cx, cy, (int)roundf(off), EYE_R); +} + +// ========================================================================= +// 2) HAPPY : arc courbé vers le haut (^_^) — croissant fait avec 2 discs +// ========================================================================= +void drawHappy(int cx, int cy) { + u8g2.drawDisc(cx, cy + 8, EYE_R - 2); + u8g2.setDrawColor(0); + u8g2.drawDisc(cx, cy + 16, EYE_R - 2); + u8g2.setDrawColor(1); +} + +// ========================================================================= +// 3) SAD : arc inversé (⌣) + petite larme sur l'œil gauche +// ========================================================================= +void drawSad(int cx, int cy) { + u8g2.drawDisc(cx, cy - 8, EYE_R - 2); + u8g2.setDrawColor(0); + u8g2.drawDisc(cx, cy - 16, EYE_R - 2); + u8g2.setDrawColor(1); + // larme sous l'œil (côté extérieur) + bool isLeft = (cx < 64); + int tx = isLeft ? (cx - 14) : (cx + 14); + u8g2.drawDisc(tx, cy + 20, 3); + u8g2.drawTriangle(tx - 3, cy + 20, tx + 3, cy + 20, tx, cy + 14); +} + +// ========================================================================= +// 4) ANGRY : œil écrasé + sourcil diagonal froncé +// ========================================================================= +void drawAngry(int cx, int cy) { + int cyShift = cy + 6; + int smallR = EYE_R - 6; + // 4 barres centrales + for (int i = 3; i <= 6; i++) { + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar(cx, cyShift, (int)roundf(off), smallR); + } + // petite pupille + u8g2.setDrawColor(0); + u8g2.drawDisc(cx, cyShift, 5); + u8g2.setDrawColor(1); + + // sourcil diagonal épais + bool isLeft = (cx < 64); + int x1, y1, x2, y2; + if (isLeft) { + x1 = cx - smallR - 2; y1 = cy - smallR - 4; + x2 = cx + smallR + 2; y2 = cy - smallR + 6; + } else { + x1 = cx - smallR - 2; y1 = cy - smallR + 6; + x2 = cx + smallR + 2; y2 = cy - smallR - 4; + } + for (int k = -2; k <= 2; k++) { + u8g2.drawLine(x1, y1 + k, x2, y2 + k); + } +} + +// ========================================================================= +// 5) SURPRISED : œil plus grand, toutes les barres présentes, petite pupille +// ========================================================================= +void drawSurprised(int cx, int cy) { + for (int i = 0; i < N_BARS; i++) { + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar(cx, cy, (int)roundf(off), EYE_R); + } + u8g2.setDrawColor(0); + u8g2.drawDisc(cx, cy, 5); + u8g2.setDrawColor(1); +} + +// ========================================================================= +// 6) SLEEPY : seulement les barres du bas + paupière épaisse en haut +// ========================================================================= +void drawSleepy(int cx, int cy) { + for (int i = 5; i < N_BARS; i++) { + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar(cx, cy, (int)roundf(off), EYE_R); + } + u8g2.drawRBox(cx - EYE_R + 2, cy - 2, 2 * EYE_R - 3, 4, 2); +} + +// ========================================================================= +// 7) WINK : œil gauche normal, œil droit fermé (barre horizontale) +// ========================================================================= +void drawWinkClosed(int cx, int cy) { + u8g2.drawRBox(cx - EYE_R + 4, cy - 3, 2 * EYE_R - 7, 6, 3); +} + +// ========================================================================= +// 8) LOVE : cœur plein (2 discs + triangle) +// ========================================================================= +void drawHeart(int cx, int cy) { + const int r = 11; + int topY = cy - 6; + u8g2.drawDisc(cx - 7, topY, r); + u8g2.drawDisc(cx + 7, topY, r); + u8g2.drawTriangle(cx - 17, topY + 2, cx + 17, topY + 2, cx, cy + 20); +} + +// ========================================================================= +// 9) DIZZY : cercles concentriques + petite pupille décalée +// ========================================================================= +void drawDizzy(int cx, int cy) { + u8g2.drawCircle(cx, cy, EYE_R - 2); + u8g2.drawCircle(cx, cy, EYE_R - 8); + u8g2.drawCircle(cx, cy, EYE_R - 14); + u8g2.drawCircle(cx, cy, EYE_R - 20); + u8g2.drawDisc(cx + 4, cy - 2, 3); +} + +// ========================================================================= +// 10) DEAD : deux lignes épaisses en X +// ========================================================================= +void drawDead(int cx, int cy) { + int a = EYE_R - 4; + for (int k = -2; k <= 2; k++) { + u8g2.drawLine(cx - a + k, cy - a, cx + a + k, cy + a); + u8g2.drawLine(cx - a + k, cy + a, cx + a + k, cy - a); + } +} + +// ========================================================================= +// Cycle des émotions +// ========================================================================= +void drawEmotion(Emotion e) { + u8g2.clearBuffer(); + switch (e) { + case NEUTRAL: + drawNeutral(EYE_L_CX, EYE_CY); + drawNeutral(EYE_R_CX, EYE_CY); + break; + case HAPPY: + drawHappy(EYE_L_CX, EYE_CY); + drawHappy(EYE_R_CX, EYE_CY); + break; + case SAD: + drawSad(EYE_L_CX, EYE_CY); + drawSad(EYE_R_CX, EYE_CY); + break; + case ANGRY: + drawAngry(EYE_L_CX, EYE_CY); + drawAngry(EYE_R_CX, EYE_CY); + break; + case SURPRISED: + drawSurprised(EYE_L_CX, EYE_CY); + drawSurprised(EYE_R_CX, EYE_CY); + break; + case SLEEPY: + drawSleepy(EYE_L_CX, EYE_CY); + drawSleepy(EYE_R_CX, EYE_CY); + break; + case WINK: + drawNeutral(EYE_L_CX, EYE_CY); + drawWinkClosed(EYE_R_CX, EYE_CY); + break; + case LOVE: + drawHeart(EYE_L_CX, EYE_CY); + drawHeart(EYE_R_CX, EYE_CY); + break; + case DIZZY: + drawDizzy(EYE_L_CX, EYE_CY); + drawDizzy(EYE_R_CX, EYE_CY); + break; + case DEAD: + drawDead(EYE_L_CX, EYE_CY); + drawDead(EYE_R_CX, EYE_CY); + break; + default: break; + } + u8g2.sendBuffer(); +} + +const unsigned long EMOTION_INTERVAL = 3000; +unsigned long lastSwitch = 0; +int current = 0; + +void setup() { + u8g2.begin(); + drawEmotion((Emotion)current); + lastSwitch = millis(); +} + +void loop() { + if (millis() - lastSwitch >= EMOTION_INTERVAL) { + current = (current + 1) % N_EMOTIONS; + drawEmotion((Emotion)current); + lastSwitch = millis(); + } +} diff --git a/apps/robot-hardware/legacy/robot-hardware.ino b/apps/robot-hardware/legacy/robot-hardware.ino new file mode 100644 index 0000000..dff44ef --- /dev/null +++ b/apps/robot-hardware/legacy/robot-hardware.ino @@ -0,0 +1,81 @@ +#include +#include +#include + +#define CS 5 +#define DC 16 +#define RESET 17 + +U8G2_SSD1309_128X64_NONAME0_F_4W_HW_SPI u8g2(U8G2_R2, CS, DC, RESET); + +// Yeux ronds formés de 10 barres horizontales, la 4e depuis le haut supprimée +const int EYE_R = 29; // rayon du cercle +const int BAR_THICK = 5; // épaisseur +const int BAR_PITCH = 6; // espacement centre-à-centre +const int BAR_ROUND = 2; // rayon coin arrondi des barres +const int N_BARS = 10; +const int SKIP_BAR = 3; // 4e en partant du haut (index 0 = top) +const int OVER_BAR = 2; // 3e en partant du haut, redessinée par-dessus la pupille + +const int EYE_L_CX = 32; +const int EYE_R_CX = 96; +const int EYE_CY = 32; + +const int PUPIL_R = 11; +const int PUPIL_DY = -3; + +void drawEye(int cx, int cy) { + for (int i = 0; i < N_BARS; i++) { + if (i == SKIP_BAR) continue; + // offset vertical centré autour de cy (10 barres symétriques) + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + int dy = (int)roundf(off); + int dy2 = dy * dy; + int r2 = EYE_R * EYE_R; + if (dy2 >= r2) continue; + int halfW = (int)sqrtf((float)(r2 - dy2)); + int topY = cy + dy - BAR_THICK / 2; + int w = 2 * halfW + 1; + // garde-fou pour drawRBox (w et h doivent être > 2*r) + if (w > 2 * BAR_ROUND && BAR_THICK > 2 * BAR_ROUND) { + u8g2.drawRBox(cx - halfW, topY, w, BAR_THICK, BAR_ROUND); + } else { + u8g2.drawBox(cx - halfW, topY, w, BAR_THICK); + } + } + + // pupille : disque sombre creusé + u8g2.setDrawColor(0); + u8g2.drawDisc(cx, cy + PUPIL_DY, PUPIL_R); + u8g2.setDrawColor(1); + + // redessine le 3e trait par-dessus la pupille + { + float off = (OVER_BAR - (N_BARS - 1) / 2.0f) * BAR_PITCH; + int dy = (int)roundf(off); + int dy2 = dy * dy; + int r2 = EYE_R * EYE_R; + if (dy2 < r2) { + int halfW = (int)sqrtf((float)(r2 - dy2)); + int topY = cy + dy - BAR_THICK / 2; + int w = 2 * halfW + 1; + if (w > 2 * BAR_ROUND && BAR_THICK > 2 * BAR_ROUND) { + u8g2.drawRBox(cx - halfW, topY, w, BAR_THICK, BAR_ROUND); + } else { + u8g2.drawBox(cx - halfW, topY, w, BAR_THICK); + } + } + } +} + +void setup() { + u8g2.begin(); + u8g2.clearBuffer(); + drawEye(EYE_L_CX, EYE_CY); + drawEye(EYE_R_CX, EYE_CY); + u8g2.sendBuffer(); +} + +void loop() { + // image statique +} diff --git a/apps/robot-hardware/lib/Eyes/library.json b/apps/robot-hardware/lib/Eyes/library.json new file mode 100644 index 0000000..cb91b9b --- /dev/null +++ b/apps/robot-hardware/lib/Eyes/library.json @@ -0,0 +1,10 @@ +{ + "name": "Eyes", + "version": "0.1.0", + "description": "Ti-Pote animatronic eyes renderer on SSD1309 (U8g2).", + "frameworks": "arduino", + "platforms": "espressif32", + "dependencies": { + "olikraus/U8g2": "^2.35.30" + } +} diff --git a/apps/robot-hardware/lib/Eyes/src/Eyes.cpp b/apps/robot-hardware/lib/Eyes/src/Eyes.cpp new file mode 100644 index 0000000..5dd1756 --- /dev/null +++ b/apps/robot-hardware/lib/Eyes/src/Eyes.cpp @@ -0,0 +1,203 @@ +#include "Eyes.h" +#include + +namespace tipote { + +// Geometry constants — ported from legacy/robot-emotion/robot-emotion.ino +static constexpr int EYE_R = 29; +static constexpr int BAR_THICK = 5; +static constexpr int BAR_PITCH = 6; +static constexpr int BAR_ROUND = 2; +static constexpr int N_BARS = 10; +static constexpr int EYE_L_CX = 32; +static constexpr int EYE_R_CX = 96; +static constexpr int EYE_CY = 32; +static constexpr int PUPIL_R = 11; + +Eyes::Eyes(const EyesPins& pins) + : display_(U8G2_R2, pins.cs, pins.dc, pins.reset) {} + +void Eyes::begin() { + display_.begin(); + show(Emotion::NEUTRAL); +} + +void Eyes::clear() { + display_.clearBuffer(); + display_.sendBuffer(); +} + +void Eyes::drawBar_(int cx, int cy, int dy, int eyeR) { + int dy2 = dy * dy; + int r2 = eyeR * eyeR; + if (dy2 >= r2) return; + int halfW = (int)sqrtf((float)(r2 - dy2)); + int topY = cy + dy - BAR_THICK / 2; + int w = 2 * halfW + 1; + if (w > 2 * BAR_ROUND && BAR_THICK > 2 * BAR_ROUND) { + display_.drawRBox(cx - halfW, topY, w, BAR_THICK, BAR_ROUND); + } else { + display_.drawBox(cx - halfW, topY, w, BAR_THICK); + } +} + +void Eyes::drawNeutral_(int cx, int cy) { + const int SKIP_BAR = 3; + const int OVER_BAR = 2; + const int PUPIL_DY = -3; + + for (int i = 0; i < N_BARS; i++) { + if (i == SKIP_BAR) continue; + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar_(cx, cy, (int)roundf(off), EYE_R); + } + display_.setDrawColor(0); + display_.drawDisc(cx, cy + PUPIL_DY, PUPIL_R); + display_.setDrawColor(1); + float off = (OVER_BAR - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar_(cx, cy, (int)roundf(off), EYE_R); +} + +void Eyes::drawHappy_(int cx, int cy) { + display_.drawDisc(cx, cy + 8, EYE_R - 2); + display_.setDrawColor(0); + display_.drawDisc(cx, cy + 16, EYE_R - 2); + display_.setDrawColor(1); +} + +void Eyes::drawSad_(int cx, int cy) { + display_.drawDisc(cx, cy - 8, EYE_R - 2); + display_.setDrawColor(0); + display_.drawDisc(cx, cy - 16, EYE_R - 2); + display_.setDrawColor(1); + bool isLeft = (cx < 64); + int tx = isLeft ? (cx - 14) : (cx + 14); + display_.drawDisc(tx, cy + 20, 3); + display_.drawTriangle(tx - 3, cy + 20, tx + 3, cy + 20, tx, cy + 14); +} + +void Eyes::drawAngry_(int cx, int cy) { + int cyShift = cy + 6; + int smallR = EYE_R - 6; + for (int i = 3; i <= 6; i++) { + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar_(cx, cyShift, (int)roundf(off), smallR); + } + display_.setDrawColor(0); + display_.drawDisc(cx, cyShift, 5); + display_.setDrawColor(1); + + bool isLeft = (cx < 64); + int x1, y1, x2, y2; + if (isLeft) { + x1 = cx - smallR - 2; y1 = cy - smallR - 4; + x2 = cx + smallR + 2; y2 = cy - smallR + 6; + } else { + x1 = cx - smallR - 2; y1 = cy - smallR + 6; + x2 = cx + smallR + 2; y2 = cy - smallR - 4; + } + for (int k = -2; k <= 2; k++) { + display_.drawLine(x1, y1 + k, x2, y2 + k); + } +} + +void Eyes::drawSurprised_(int cx, int cy) { + for (int i = 0; i < N_BARS; i++) { + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar_(cx, cy, (int)roundf(off), EYE_R); + } + display_.setDrawColor(0); + display_.drawDisc(cx, cy, 5); + display_.setDrawColor(1); +} + +void Eyes::drawSleepy_(int cx, int cy) { + for (int i = 5; i < N_BARS; i++) { + float off = (i - (N_BARS - 1) / 2.0f) * BAR_PITCH; + drawBar_(cx, cy, (int)roundf(off), EYE_R); + } + display_.drawRBox(cx - EYE_R + 2, cy - 2, 2 * EYE_R - 3, 4, 2); +} + +void Eyes::drawWinkClosed_(int cx, int cy) { + display_.drawRBox(cx - EYE_R + 4, cy - 3, 2 * EYE_R - 7, 6, 3); +} + +void Eyes::drawHeart_(int cx, int cy) { + const int r = 11; + int topY = cy - 6; + display_.drawDisc(cx - 7, topY, r); + display_.drawDisc(cx + 7, topY, r); + display_.drawTriangle(cx - 17, topY + 2, cx + 17, topY + 2, cx, cy + 20); +} + +void Eyes::drawDizzy_(int cx, int cy) { + display_.drawCircle(cx, cy, EYE_R - 2); + display_.drawCircle(cx, cy, EYE_R - 8); + display_.drawCircle(cx, cy, EYE_R - 14); + display_.drawCircle(cx, cy, EYE_R - 20); + display_.drawDisc(cx + 4, cy - 2, 3); +} + +void Eyes::drawDead_(int cx, int cy) { + int a = EYE_R - 4; + for (int k = -2; k <= 2; k++) { + display_.drawLine(cx - a + k, cy - a, cx + a + k, cy + a); + display_.drawLine(cx - a + k, cy + a, cx + a + k, cy - a); + } +} + +void Eyes::show(Emotion emotion) { + current_ = emotion; + display_.clearBuffer(); + switch (emotion) { + case Emotion::NEUTRAL: + drawNeutral_(EYE_L_CX, EYE_CY); + drawNeutral_(EYE_R_CX, EYE_CY); + break; + case Emotion::HAPPY: + drawHappy_(EYE_L_CX, EYE_CY); + drawHappy_(EYE_R_CX, EYE_CY); + break; + case Emotion::SAD: + drawSad_(EYE_L_CX, EYE_CY); + drawSad_(EYE_R_CX, EYE_CY); + break; + case Emotion::ANGRY: + drawAngry_(EYE_L_CX, EYE_CY); + drawAngry_(EYE_R_CX, EYE_CY); + break; + case Emotion::SURPRISED: + drawSurprised_(EYE_L_CX, EYE_CY); + drawSurprised_(EYE_R_CX, EYE_CY); + break; + case Emotion::SLEEPY: + drawSleepy_(EYE_L_CX, EYE_CY); + drawSleepy_(EYE_R_CX, EYE_CY); + break; + case Emotion::WINK: + drawNeutral_(EYE_L_CX, EYE_CY); + drawWinkClosed_(EYE_R_CX, EYE_CY); + break; + case Emotion::LOVE: + drawHeart_(EYE_L_CX, EYE_CY); + drawHeart_(EYE_R_CX, EYE_CY); + break; + case Emotion::DIZZY: + drawDizzy_(EYE_L_CX, EYE_CY); + drawDizzy_(EYE_R_CX, EYE_CY); + break; + case Emotion::DEAD: + drawDead_(EYE_L_CX, EYE_CY); + drawDead_(EYE_R_CX, EYE_CY); + break; + default: + // Unknown emotion: render NEUTRAL as a safe fallback. + drawNeutral_(EYE_L_CX, EYE_CY); + drawNeutral_(EYE_R_CX, EYE_CY); + break; + } + display_.sendBuffer(); +} + +} // namespace tipote diff --git a/apps/robot-hardware/lib/Eyes/src/Eyes.h b/apps/robot-hardware/lib/Eyes/src/Eyes.h new file mode 100644 index 0000000..15bdea2 --- /dev/null +++ b/apps/robot-hardware/lib/Eyes/src/Eyes.h @@ -0,0 +1,60 @@ +// Ti-Pote — Animatronic eyes on an SSD1309 128x64 OLED. +// +// This is a refactor of the original Arduino sketches that used to +// live at apps/robot-hardware/robot-emotion/robot-emotion.ino. The +// drawing primitives are unchanged — only the organisation is new, +// so emotions can be driven from the main loop in response to +// incoming UART commands instead of cycling on a timer. + +#pragma once + +#include +#include "../../../include/protocol_types.h" + +namespace tipote { + +// Default SPI wiring matches the working prototype from the legacy +// sketches. Change these at construction time if the PCB uses +// different pins. +struct EyesPins { + uint8_t cs = 5; + uint8_t dc = 16; + uint8_t reset = 17; +}; + +class Eyes { +public: + explicit Eyes(const EyesPins& pins = EyesPins{}); + + // Must be called from setup() once. + void begin(); + + // Render a full-screen emotion. Safe to call from any context. + void show(Emotion emotion); + + // Clear the screen (dark). Useful for "sleeping" or before + // switching to a custom animation. + void clear(); + + Emotion current() const { return current_; } + +private: + U8G2_SSD1309_128X64_NONAME0_F_4W_HW_SPI display_; + Emotion current_ = Emotion::NEUTRAL; + + // Primitives ported 1:1 from robot-emotion.ino so the visual + // output is byte-identical to the prototype. + void drawBar_(int cx, int cy, int dy, int eyeR); + void drawNeutral_(int cx, int cy); + void drawHappy_(int cx, int cy); + void drawSad_(int cx, int cy); + void drawAngry_(int cx, int cy); + void drawSurprised_(int cx, int cy); + void drawSleepy_(int cx, int cy); + void drawWinkClosed_(int cx, int cy); + void drawHeart_(int cx, int cy); + void drawDizzy_(int cx, int cy); + void drawDead_(int cx, int cy); +}; + +} // namespace tipote diff --git a/apps/robot-hardware/lib/Protocol/library.json b/apps/robot-hardware/lib/Protocol/library.json new file mode 100644 index 0000000..843e82e --- /dev/null +++ b/apps/robot-hardware/lib/Protocol/library.json @@ -0,0 +1,7 @@ +{ + "name": "Protocol", + "version": "0.1.0", + "description": "Ti-Pote binary UART protocol: framing, CRC, stream decoder.", + "frameworks": "arduino", + "platforms": "espressif32" +} diff --git a/apps/robot-hardware/lib/Protocol/src/Protocol.cpp b/apps/robot-hardware/lib/Protocol/src/Protocol.cpp new file mode 100644 index 0000000..6325284 --- /dev/null +++ b/apps/robot-hardware/lib/Protocol/src/Protocol.cpp @@ -0,0 +1,122 @@ +#include "Protocol.h" + +namespace tipote { + +bool FrameDecoder::feed(uint8_t byte) { + switch (state_) { + case State::WAIT_START: + if (byte == FRAME_START) { + state_ = State::READ_TYPE; + } + return false; + + case State::READ_TYPE: + type_ = byte; + state_ = State::READ_LEN_H; + return false; + + case State::READ_LEN_H: + length_ = static_cast(byte) << 8; + state_ = State::READ_LEN_L; + return false; + + case State::READ_LEN_L: + length_ |= byte; + if (length_ > MAX_PAYLOAD_SIZE) { + // Oversized frame: drop and re-sync. + framesDropped_++; + reset_(); + return false; + } + payloadIdx_ = 0; + state_ = (length_ == 0) ? State::READ_CRC : State::READ_PAYLOAD; + return false; + + case State::READ_PAYLOAD: + payload_[payloadIdx_++] = byte; + if (payloadIdx_ == length_) { + state_ = State::READ_CRC; + } + return false; + + case State::READ_CRC: { + // Compute expected CRC over TYPE + LEN_H + LEN_L + PAYLOAD. + uint8_t header[3] = { + type_, + static_cast((length_ >> 8) & 0xFF), + static_cast(length_ & 0xFF), + }; + uint8_t crc = crc8(header, 3); + // Extend over payload. We can't easily continue crc8 with a + // different pointer in one call, so fold payload in manually. + for (uint16_t i = 0; i < length_; ++i) { + crc ^= payload_[i]; + for (uint8_t b = 0; b < 8; ++b) { + crc = (crc & 0x80) ? static_cast((crc << 1) ^ 0x07) + : static_cast(crc << 1); + } + } + + bool emitted = false; + if (crc == byte) { + framesOk_++; + if (handler_) { + Frame frame{ + static_cast(type_), + length_, + payload_, + }; + handler_(frame, userData_); + } + emitted = true; + } else { + framesDropped_++; + } + reset_(); + return emitted; + } + } + return false; +} + +size_t FrameDecoder::feed(const uint8_t* data, size_t len) { + size_t emitted = 0; + for (size_t i = 0; i < len; ++i) { + if (feed(data[i])) emitted++; + } + return emitted; +} + +size_t FrameEncoder::encode(MsgType type, + const uint8_t* payload, + uint16_t length, + uint8_t* out, + size_t outCapacity) { + if (length > MAX_PAYLOAD_SIZE) return 0; + const size_t total = FRAME_OVERHEAD + length; + if (outCapacity < total) return 0; + + out[0] = FRAME_START; + out[1] = static_cast(type); + out[2] = static_cast((length >> 8) & 0xFF); + out[3] = static_cast(length & 0xFF); + for (uint16_t i = 0; i < length; ++i) { + out[FRAME_HEADER_SIZE + i] = payload[i]; + } + + // CRC over TYPE + LEN + PAYLOAD (skip START). + out[FRAME_HEADER_SIZE + length] = crc8(out + 1, 3 + length); + return total; +} + +bool FrameEncoder::writeTo(Stream& stream, + MsgType type, + const uint8_t* payload, + uint16_t length) { + uint8_t buffer[FRAME_OVERHEAD + MAX_PAYLOAD_SIZE]; + size_t written = encode(type, payload, length, buffer, sizeof(buffer)); + if (written == 0) return false; + return stream.write(buffer, written) == written; +} + +} // namespace tipote diff --git a/apps/robot-hardware/lib/Protocol/src/Protocol.h b/apps/robot-hardware/lib/Protocol/src/Protocol.h new file mode 100644 index 0000000..dba523e --- /dev/null +++ b/apps/robot-hardware/lib/Protocol/src/Protocol.h @@ -0,0 +1,98 @@ +// Ti-Pote — Streaming frame decoder & encoder. +// +// FrameDecoder implements a small state machine that consumes bytes +// coming from a UART one at a time and emits complete frames once +// the CRC matches. Invalid frames are dropped silently (the decoder +// re-syncs on the next 0xAA start byte), and optional counters are +// exposed for diagnostics. +// +// FrameEncoder is a tiny helper that writes a fully framed message +// into a caller-provided buffer. + +#pragma once + +#include +#include +#include "../../../include/protocol_types.h" + +namespace tipote { + +struct Frame { + MsgType type; + uint16_t length; + const uint8_t* payload; // points into the decoder's internal buffer — copy if you need to keep it +}; + +class FrameDecoder { +public: + // Called when a full, CRC-valid frame has been decoded. + using FrameHandler = void (*)(const Frame& frame, void* userData); + + FrameDecoder() = default; + + void onFrame(FrameHandler handler, void* userData = nullptr) { + handler_ = handler; + userData_ = userData; + } + + // Feed one byte from the serial stream. Returns true if a full + // frame was emitted as a result of this byte. + bool feed(uint8_t byte); + + // Feed a batch. Returns the number of frames emitted. + size_t feed(const uint8_t* data, size_t len); + + uint32_t framesOk() const { return framesOk_; } + uint32_t framesDropped() const { return framesDropped_; } + +private: + enum class State : uint8_t { + WAIT_START, + READ_TYPE, + READ_LEN_H, + READ_LEN_L, + READ_PAYLOAD, + READ_CRC, + }; + + State state_ = State::WAIT_START; + uint8_t type_ = 0; + uint16_t length_ = 0; + uint16_t payloadIdx_ = 0; + uint8_t payload_[MAX_PAYLOAD_SIZE] = {0}; + + FrameHandler handler_ = nullptr; + void* userData_ = nullptr; + + uint32_t framesOk_ = 0; + uint32_t framesDropped_ = 0; + + void reset_() { + state_ = State::WAIT_START; + type_ = 0; + length_ = 0; + payloadIdx_ = 0; + } +}; + +class FrameEncoder { +public: + // Encode a frame into `out`. Returns the total number of bytes + // written, or 0 if `out` is too small. + // + // Required capacity = FRAME_OVERHEAD + length. + static size_t encode(MsgType type, + const uint8_t* payload, + uint16_t length, + uint8_t* out, + size_t outCapacity); + + // Convenience: encode + write to an Arduino Stream in one shot. + // Returns true on success. + static bool writeTo(Stream& stream, + MsgType type, + const uint8_t* payload = nullptr, + uint16_t length = 0); +}; + +} // namespace tipote diff --git a/apps/robot-hardware/platformio.ini b/apps/robot-hardware/platformio.ini new file mode 100644 index 0000000..3af807a --- /dev/null +++ b/apps/robot-hardware/platformio.ini @@ -0,0 +1,48 @@ +; Ti-Pote — Robot Hardware (ESP32 firmware) +; PlatformIO configuration +; +; Two envs are provided: +; - esp32dev : classic ESP32 (what the current OLED prototype uses) +; - esp32-s3 : target board per docs/hardware.md (Phase 2) +; +; Default is esp32dev because the current prototype wiring (VSPI pins +; CS=5, DC=16, RESET=17) targets the classic ESP32 module that Arthur +; currently has plugged into his laptop. + +[platformio] +default_envs = esp32dev +src_dir = src +include_dir = include +lib_dir = lib +test_dir = test + +[env] +framework = arduino +monitor_speed = 921600 +upload_speed = 921600 +lib_deps = + olikraus/U8g2@^2.35.30 +build_flags = + -std=gnu++17 + -Wall + -Wextra + ; Protocol baud rate (matches docs/hardware.md) + -DHW_SERIAL_BAUD=921600 + ; Idle timeout before the eyes fall back to the default animation (ms) + -DHW_HEARTBEAT_TIMEOUT_MS=5000 +build_unflags = + -std=gnu++11 + +[env:esp32dev] +platform = espressif32 +board = esp32dev +board_build.f_cpu = 240000000L + +[env:esp32-s3] +platform = espressif32 +board = esp32-s3-devkitc-1 +board_build.f_cpu = 240000000L +build_flags = + ${env.build_flags} + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/apps/robot-hardware/src/main.cpp b/apps/robot-hardware/src/main.cpp new file mode 100644 index 0000000..c347343 --- /dev/null +++ b/apps/robot-hardware/src/main.cpp @@ -0,0 +1,147 @@ +// Ti-Pote — Robot Hardware firmware (ESP32) +// +// Responsibilities for v0: +// - Listen on UART0 (the USB-connected serial port while the ESP32 +// is plugged into Arthur's laptop; on the real robot this will +// eventually be Serial2 wired to the Raspberry Pi). +// - Decode incoming binary frames (see include/protocol_types.h). +// - Dispatch commands to the Eyes renderer. +// - Reply to PING with PONG. +// - Fall back to a sleepy animation if no heartbeat is received +// for HW_HEARTBEAT_TIMEOUT_MS (set in platformio.ini). +// +// Intentionally NOT yet implemented (Phase 2): +// - I2S audio up/down streaming +// - Servo / LED commands +// +// The hook points for those are marked with TODO(phase2). + +#include +#include "Protocol.h" +#include "Eyes.h" + +#ifndef HW_SERIAL_BAUD +#define HW_SERIAL_BAUD 921600 +#endif + +#ifndef HW_HEARTBEAT_TIMEOUT_MS +#define HW_HEARTBEAT_TIMEOUT_MS 5000 +#endif + +// The communication stream. When the ESP32 is plugged into a +// computer, UART0 (Serial) is the USB-CDC port, which is exactly +// what the robot-client will talk to during development. Later, +// for the Pi wiring, change this to Serial2 and call +// `Serial2.begin(HW_SERIAL_BAUD, SERIAL_8N1, RX_PIN, TX_PIN)`. +#define HW_COMM Serial + +using namespace tipote; + +static Eyes eyes; +static FrameDecoder decoder; + +static uint32_t lastHeartbeatMs = 0; +static bool idleMode = false; + +// Forward decl +static void handleFrame(const Frame& frame, void* userData); +static void logLine(const char* line); + +void setup() { + HW_COMM.begin(HW_SERIAL_BAUD); + // Give the host a beat to open the port after auto-reset. + delay(50); + + eyes.begin(); + + decoder.onFrame(handleFrame); + + lastHeartbeatMs = millis(); + logLine("robot-hardware ready"); +} + +void loop() { + // Drain whatever the host has sent since the last tick. + while (HW_COMM.available() > 0) { + int b = HW_COMM.read(); + if (b < 0) break; + decoder.feed(static_cast(b)); + } + + // Heartbeat watchdog: if we haven't heard from the host in a + // while, slip into a sleepy animation so the robot doesn't + // look frozen. Any incoming frame resets this. + const uint32_t now = millis(); + if (!idleMode && (now - lastHeartbeatMs) > HW_HEARTBEAT_TIMEOUT_MS) { + idleMode = true; + eyes.show(Emotion::SLEEPY); + } +} + +// --------------------------------------------------------------- +// Frame dispatcher +// --------------------------------------------------------------- + +static void handleFrame(const Frame& frame, void* /*userData*/) { + lastHeartbeatMs = millis(); + if (idleMode) { + idleMode = false; + } + + switch (frame.type) { + case MsgType::DISPLAY_EMOTION: { + if (frame.length < 1) { + logLine("DISPLAY_EMOTION: empty payload"); + return; + } + const uint8_t code = frame.payload[0]; + if (code >= static_cast(Emotion::COUNT)) { + logLine("DISPLAY_EMOTION: out-of-range code"); + return; + } + eyes.show(static_cast(code)); + + // ACK back so the host knows it was applied. + uint8_t ackPayload[1] = {code}; + FrameEncoder::writeTo(HW_COMM, MsgType::ACK, ackPayload, 1); + return; + } + + case MsgType::DISPLAY_CLEAR: { + eyes.clear(); + FrameEncoder::writeTo(HW_COMM, MsgType::ACK); + return; + } + + case MsgType::PING: { + // Echo the payload back as PONG. Useful for latency + // measurements and proving the link is symmetric. + FrameEncoder::writeTo(HW_COMM, MsgType::PONG, + frame.payload, frame.length); + return; + } + + case MsgType::STATUS: { + // Heartbeat from host — lastHeartbeatMs was already + // bumped above. Nothing else to do for v0. + return; + } + + // TODO(phase2): AUDIO_UP / AUDIO_DOWN / SERVO_CMD / LED_CMD + default: + logLine("unknown frame type"); + return; + } +} + +// --------------------------------------------------------------- +// Diagnostic logging — wraps text in a LOG frame so the host +// can parse it without getting confused by free text on the wire. +// --------------------------------------------------------------- + +static void logLine(const char* line) { + const size_t len = strnlen(line, MAX_PAYLOAD_SIZE); + FrameEncoder::writeTo(HW_COMM, MsgType::LOG, + reinterpret_cast(line), + static_cast(len)); +} diff --git a/docs/STATUS.md b/docs/STATUS.md new file mode 100644 index 0000000..2d59994 --- /dev/null +++ b/docs/STATUS.md @@ -0,0 +1,104 @@ +# Ti-Pote — État du projet & prochaines missions + +> Dernière mise à jour : 2 avril 2026 + +--- + +## Ce qui fonctionne + +### Voice conversation (end-to-end) +- Wake word ("Hey Jarvis") → capture audio (arecord) → STT Deepgram → LLM → TTS ElevenLabs → playback aplay +- Conversation continue : après chaque réponse, le robot réécoute automatiquement sans re-trigger +- Grace period 3s + silence detection (RMS 200, timeout 2s) pour savoir quand l'utilisateur a fini de parler +- Audio buffering : les chunks sont bufferisés côté robot-client jusqu'à ce que le backend confirme que le stream STT est prêt (évite de couper le premier mot) + +### Wake word (OpenWakeWord) +- Script Python long-lived avec PAUSE/RESUME/QUIT via stdin +- Modèle chargé une seule fois au démarrage (pas de reload 8s à chaque cycle) +- PAUSE ferme le stream PyAudio (libère le device pour arecord), RESUME le rouvre +- stdin lu via `sys.stdin.readline()` (pas `for line in sys.stdin` qui bufferise) + +### Auto-pairing +- Robot-client : si pas de credentials → appelle `POST /api/pairing/request` → affiche code 6 chiffres en ASCII art sur HDMI → poll `GET /api/pairing/status/:requestId` toutes les 3s +- Backend : Redis TTL 10min, `POST /api/pairing/confirm` (JWT required) associe le robot au home de l'utilisateur +- Credentials persistées dans `~/.tipote/config.json` via LocalStore (path dynamique via `os.homedir()`) +- App desktop Tauri v2 créée dans `apps/desktop/` (login → code 6 chiffres → succès) + +### Infrastructure +- Backend NestJS avec archi hexagonale (ports & adapters) +- WebSocket socket.io entre robot-client et backend +- Docker Compose (PostgreSQL + Redis) en dev +- Robot-client TypeScript sur Raspberry Pi + +--- + +## Bugs connus à fixer + +### 1. Wake word crash en boucle (PRIORITÉ HAUTE) +**Symptôme** : Le modèle charge OK, puis le process Python exit avec code 1 ~2s après. Boucle de restart infinie. +**Cause probable** : Erreur à l'ouverture du stream audio PyAudio (device busy, mauvais index, ou erreur ALSA). Les messages d'erreur stderr étaient loggés en `debug` donc invisibles. +**Fix appliqué** : Les messages stderr inconnus sont maintenant loggés en `warn`. Relancer et lire les logs pour voir l'erreur exacte. +**Fichiers** : +- `apps/robot-client/scripts/wake_word.py` +- `apps/robot-client/src/services/wake-word.service.ts` (ligne ~113) + +### 2. .env vs .env.dev loading +**Symptôme** : `ROBOT_MODE` n'est pas set avant que dotenv charge, donc `.env` est toujours chargé au lieu de `.env.dev`. +**Workaround** : Préfixer la commande avec `ROBOT_MODE=dev` ou éditer `.env` directement sur le Pi. + +--- + +## Prochaines missions + +### Court terme (sprint en cours) + +1. **Fixer le crash wake word** — Lire les logs stderr (maintenant en `warn`), identifier l'erreur PyAudio, corriger +2. **Tester le pairing end-to-end avec l'app Tauri** — L'app est créée mais pas encore testée avec un vrai pairing flow +3. **Tester la persistence des credentials** — Vérifier qu'après reboot du Pi, le robot-client retrouve ses credentials dans `~/.tipote/config.json` et skip le pairing + +### Moyen terme + +4. **ESP32 + wake word embarqué** — Déléguer l'audio à l'ESP32 (I2S mic + speaker). Le wake word pourrait tourner sur l'ESP32 via un modèle TFLite léger ou rester sur le Pi +5. **Frontend Next.js** (`apps/frontend`) — Dashboard web pour gérer ses robots, voir les conversations, configurer les préférences +6. **Multi-robot / Multi-home** — Le backend supporte déjà le concept de `home`, mais le flow complet n'est pas testé +7. **OTA updates** — Mécanisme de mise à jour du robot-client sur le Pi (rsync pour le dev, mais il faudra un vrai système pour la prod) + +### Long terme + +8. **Personnalité configurable** — Chaque robot peut avoir un system prompt custom via l'app/dashboard +9. **Mémoire contextuelle** — Le robot se souvient des conversations passées (pgvector déjà dans le stack) +10. **Animations servo** — Synchroniser les mouvements du robot avec le TTS (lipsync, expressions) +11. **Mode offline** — Wake word + réponses basiques sans connexion cloud + +--- + +## Structure du repo + +``` +apps/ +├── backend/ # NestJS — API REST + WebSocket + LLM/STT/TTS orchestration +├── robot-client/ # TypeScript — tourne sur le Raspberry Pi +├── desktop/ # Tauri v2 — app native de pairing (NEW) +├── frontend/ # Next.js — dashboard web (à venir) +└── simulator/ # Simulateur (existant) +docs/ # Architecture, features, data model, roadmap +``` + +## Commandes utiles + +```bash +# Backend (sur le Mac) +cd apps/backend && pnpm dev +docker compose up -d # PostgreSQL + Redis + +# Robot-client (sur le Pi) +cd ~/robot-client && npx tsx src/main.ts +# ou avec mode dev explicite : +ROBOT_MODE=dev npx tsx src/main.ts + +# Desktop app (sur le Mac) +cd apps/desktop && pnpm install && pnpm dev + +# Rsync vers le Pi +rsync -avz --exclude node_modules --exclude .git apps/robot-client/ pi@192.168.1.XX:~/robot-client/ +``` diff --git a/hardware/fusion-libraries/INMP441_Breakout.lbr b/hardware/fusion-libraries/INMP441_Breakout.lbr new file mode 100644 index 0000000..5594a49 --- /dev/null +++ b/hardware/fusion-libraries/INMP441_Breakout.lbr @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +INMP441 I2S MEMS microphone breakout module (1x6 2.54 mm header). Generated for Ti-pote project. + + +INMP441 breakout module, 15.0 x 10.5 mm, 1x6 pin header, 2.54 mm pitch. +Pins numbered 1..6 = SCK, SD, WS, L/R, GND, VDD. + + + + + + + + + + + + + + + + + + + + + + + + +SCK +SD +WS +L/R +GND +VDD + +>NAME +>VALUE + + + + +INMP441 I2S MEMS microphone (breakout module, 1 gate). + + + + + + + + + + + + + +>NAME +>VALUE + +INMP441 +I2S MEMS Mic + + + + +INMP441 I2S MEMS microphone breakout module. +Omnidirectional, 24-bit I2S output, 1.8-3.3 V supply. +Footprint: 1x6 through-hole header, 2.54 mm pitch. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fefcbd1..957e317 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,59 @@ importers: specifier: ^5.8.3 version: 5.8.3 + apps/desktop: + dependencies: + '@tauri-apps/api': + specifier: ^2 + version: 2.10.1 + devDependencies: + '@tauri-apps/cli': + specifier: ^2 + version: 2.10.1 + serve: + specifier: ^14 + version: 14.2.6 + + apps/robot-client: + dependencies: + dotenv: + specifier: ^17.3.1 + version: 17.3.1 + pino: + specifier: ^9.6.0 + version: 9.14.0 + pino-pretty: + specifier: ^13.0.0 + version: 13.1.3 + serialport: + specifier: ^12.0.0 + version: 12.0.0 + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + devDependencies: + '@types/node': + specifier: ^22.15.0 + version: 22.19.15 + eslint: + specifier: ^10.1.0 + version: 10.1.0 + prettier: + specifier: ^3.8.1 + version: 3.8.1 + tsup: + specifier: ^8.5.0 + version: 8.5.1(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3) + tsx: + specifier: ^4.21.0 + version: 4.21.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^3.2.1 + version: 3.2.4(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0) + apps/simulator: dependencies: react: @@ -1088,6 +1141,9 @@ packages: '@oxc-project/types@0.122.0': resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1197,9 +1253,211 @@ packages: '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@rollup/rollup-android-arm-eabi@4.60.1': + resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.1': + resolution: {integrity: sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.1': + resolution: {integrity: sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.1': + resolution: {integrity: sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.1': + resolution: {integrity: sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.1': + resolution: {integrity: sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.1': + resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.1': + resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.1': + resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.1': + resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.1': + resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.1': + resolution: {integrity: sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + resolution: {integrity: sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + resolution: {integrity: sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.1': + resolution: {integrity: sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.1': + resolution: {integrity: sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==} + cpu: [x64] + os: [win32] + '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@serialport/binding-mock@10.2.2': + resolution: {integrity: sha512-HAFzGhk9OuFMpuor7aT5G1ChPgn5qSsklTFOTUX72Rl6p0xwcSVsRtG/xaGp6bxpN7fI9D/S8THLBWbBgS6ldw==} + engines: {node: '>=12.0.0'} + + '@serialport/bindings-cpp@12.0.1': + resolution: {integrity: sha512-r2XOwY2dDvbW7dKqSPIk2gzsr6M6Qpe9+/Ngs94fNaNlcTRCV02PfaoDmRgcubpNVVcLATlxSxPTIDw12dbKOg==} + engines: {node: '>=16.0.0'} + + '@serialport/bindings-interface@1.2.2': + resolution: {integrity: sha512-CJaUd5bLvtM9c5dmO9rPBHPXTa9R2UwpkJ0wdh9JCYcbrPWsKz+ErvR0hBLeo7NPeiFdjFO4sonRljiw4d2XiA==} + engines: {node: ^12.22 || ^14.13 || >=16} + + '@serialport/parser-byte-length@12.0.0': + resolution: {integrity: sha512-0ei0txFAj+s6FTiCJFBJ1T2hpKkX8Md0Pu6dqMrYoirjPskDLJRgZGLqoy3/lnU1bkvHpnJO+9oJ3PB9v8rNlg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-cctalk@12.0.0': + resolution: {integrity: sha512-0PfLzO9t2X5ufKuBO34DQKLXrCCqS9xz2D0pfuaLNeTkyGUBv426zxoMf3rsMRodDOZNbFblu3Ae84MOQXjnZw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@11.0.0': + resolution: {integrity: sha512-aZLJhlRTjSmEwllLG7S4J8s8ctRAS0cbvCpO87smLvl3e4BgzbVgF6Z6zaJd3Aji2uSiYgfedCdNc4L6W+1E2g==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-delimiter@12.0.0': + resolution: {integrity: sha512-gu26tVt5lQoybhorLTPsH2j2LnX3AOP2x/34+DUSTNaUTzu2fBXw+isVjQJpUBFWu6aeQRZw5bJol5X9Gxjblw==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-inter-byte-timeout@12.0.0': + resolution: {integrity: sha512-GnCh8K0NAESfhCuXAt+FfBRz1Cf9CzIgXfp7SdMgXwrtuUnCC/yuRTUFWRvuzhYKoAo1TL0hhUo77SFHUH1T/w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-packet-length@12.0.0': + resolution: {integrity: sha512-p1hiCRqvGHHLCN/8ZiPUY/G0zrxd7gtZs251n+cfNTn+87rwcdUeu9Dps3Aadx30/sOGGFL6brIRGK4l/t7MuQ==} + engines: {node: '>=8.6.0'} + + '@serialport/parser-readline@11.0.0': + resolution: {integrity: sha512-rRAivhRkT3YO28WjmmG4FQX6L+KMb5/ikhyylRfzWPw0nSXy97+u07peS9CbHqaNvJkMhH1locp2H36aGMOEIA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-readline@12.0.0': + resolution: {integrity: sha512-O7cywCWC8PiOMvo/gglEBfAkLjp/SENEML46BXDykfKP5mTPM46XMaX1L0waWU6DXJpBgjaL7+yX6VriVPbN4w==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-ready@12.0.0': + resolution: {integrity: sha512-ygDwj3O4SDpZlbrRUraoXIoIqb8sM7aMKryGjYTIF0JRnKeB1ys8+wIp0RFMdFbO62YriUDextHB5Um5cKFSWg==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-regex@12.0.0': + resolution: {integrity: sha512-dCAVh4P/pZrLcPv9NJ2mvPRBg64L5jXuiRxIlyxxdZGH4WubwXVXY/kBTihQmiAMPxbT3yshSX8f2+feqWsxqA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-slip-encoder@12.0.0': + resolution: {integrity: sha512-0APxDGR9YvJXTRfY+uRGhzOhTpU5akSH183RUcwzN7QXh8/1jwFsFLCu0grmAUfi+fItCkR+Xr1TcNJLR13VNA==} + engines: {node: '>=12.0.0'} + + '@serialport/parser-spacepacket@12.0.0': + resolution: {integrity: sha512-dozONxhPC/78pntuxpz/NOtVps8qIc/UZzdc/LuPvVsqCoJXiRxOg6ZtCP/W58iibJDKPZPAWPGYeZt9DJxI+Q==} + engines: {node: '>=12.0.0'} + + '@serialport/stream@12.0.0': + resolution: {integrity: sha512-9On64rhzuqKdOQyiYLYv2lQOh3TZU/D3+IWCR5gk0alPel2nwpp4YwDEGiUBfrQZEdQ6xww0PWkzqth4wqwX3Q==} + engines: {node: '>=12.0.0'} + '@sinclair/typebox@0.34.48': resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} @@ -1291,6 +1549,85 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tauri-apps/api@2.10.1': + resolution: {integrity: sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==} + + '@tauri-apps/cli-darwin-arm64@2.10.1': + resolution: {integrity: sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.10.1': + resolution: {integrity: sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.10.1': + resolution: {integrity: sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.10.1': + resolution: {integrity: sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.10.1': + resolution: {integrity: sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': + resolution: {integrity: sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.10.1': + resolution: {integrity: sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.10.1': + resolution: {integrity: sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.10.1': + resolution: {integrity: sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.10.1': + resolution: {integrity: sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.10.1': + resolution: {integrity: sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.10.1': + resolution: {integrity: sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==} + engines: {node: '>= 10'} + hasBin: true + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -1331,12 +1668,18 @@ packages: '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} '@types/cors@2.8.19': resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -1388,6 +1731,9 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@22.19.15': + resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} + '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} @@ -1619,6 +1965,35 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1670,6 +2045,9 @@ packages: '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + '@zeit/schemas@2.36.0': + resolution: {integrity: sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -1737,6 +2115,9 @@ packages: ajv@8.18.0: resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -1769,6 +2150,9 @@ packages: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -1780,9 +2164,15 @@ packages: append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1795,9 +2185,17 @@ packages: array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -1861,6 +2259,10 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + boxen@7.0.0: + resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==} + engines: {node: '>=14.16'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1895,14 +2297,28 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1927,13 +2343,29 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + caniuse-lite@1.0.30001781: resolution: {integrity: sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk-template@0.4.0: + resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} + engines: {node: '>=12'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.0.1: + resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -1941,6 +2373,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1962,6 +2398,10 @@ packages: class-validator@0.15.1: resolution: {integrity: sha512-LqoS80HBBSCVhz/3KloUly0ovokxpdOLR++Al3J3+dHXWt9sTKlKd4eYtoxhxyUjoe5+UcIM+5k9MIxyBWnRTw==} + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -1978,6 +2418,10 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + clipboardy@3.0.0: + resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2004,6 +2448,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -2022,6 +2469,14 @@ packages: resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} engines: {node: '>= 6'} + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.8.1: + resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} + engines: {node: '>= 0.8.0'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2029,10 +2484,17 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@0.5.2: + resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==} + engines: {node: '>= 0.6'} + content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -2085,6 +2547,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dayjs@1.11.20: resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} @@ -2096,6 +2561,15 @@ packages: supports-color: optional: true + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2113,6 +2587,14 @@ packages: babel-plugin-macros: optional: true + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2205,6 +2687,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + engine.io-client@6.6.4: resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} @@ -2231,6 +2716,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -2337,6 +2825,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2373,6 +2864,10 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expect@30.3.0: resolution: {integrity: sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2395,6 +2890,9 @@ packages: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} + fast-copy@4.0.2: + resolution: {integrity: sha512-ybA6PDXIXOXivLJK/z9e+Otk7ve13I4ckBvGO5I2RRmBU1gMHLVDJYEuJYhGwez7YNlYji2M2DvVU+a9mSFDlw==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2453,6 +2951,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -2603,6 +3104,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + hono-openapi@1.3.0: resolution: {integrity: sha512-xDvCWpWEIv0weEmnl3EjRQzqbHIO8LnfzMuYOCmbuyE5aes6aXxLg4vM3ybnoZD5TiTUkA6PuRQPJs3R7WRBig==} peerDependencies: @@ -2676,6 +3180,9 @@ packages: inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ioredis@5.10.1: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} @@ -2695,6 +3202,11 @@ packages: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -2727,6 +3239,10 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-port-reachable@4.0.0: + resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -2750,6 +3266,10 @@ packages: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -2918,12 +3438,19 @@ packages: jose@6.2.2: resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tiktoken@1.0.21: resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.2: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true @@ -3079,6 +3606,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -3086,6 +3617,10 @@ packages: resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} engines: {node: '>=13.2.0'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + loader-runner@4.3.1: resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==} engines: {node: '>=6.11.5'} @@ -3135,6 +3670,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3188,6 +3726,10 @@ packages: resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} engines: {node: '>= 0.6'} + mime-db@1.33.0: + resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==} + engines: {node: '>= 0.6'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -3196,6 +3738,10 @@ packages: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.18: + resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} @@ -3231,9 +3777,15 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -3245,6 +3797,9 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3262,6 +3817,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@0.6.4: + resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} + engines: {node: '>= 0.6'} + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -3272,6 +3831,9 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-addon-api@7.0.0: + resolution: {integrity: sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==} + node-addon-api@8.7.0: resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} engines: {node: ^18 || ^20 || >= 21} @@ -3288,6 +3850,10 @@ packages: encoding: optional: true + node-gyp-build@4.6.0: + resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} + hasBin: true + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -3322,10 +3888,18 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} + on-headers@1.1.0: + resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} + engines: {node: '>= 0.8'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -3422,6 +3996,9 @@ packages: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} + path-is-inside@1.0.2: + resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -3441,6 +4018,9 @@ packages: path-to-regexp@0.1.13: resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + path-to-regexp@3.3.0: + resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@8.3.0: resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} @@ -3448,6 +4028,13 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pause@0.0.1: resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} @@ -3500,6 +4087,23 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.14.0: + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -3512,6 +4116,9 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -3520,6 +4127,24 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -3561,6 +4186,9 @@ packages: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -3569,6 +4197,9 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -3587,10 +4218,17 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + radash@12.1.1: resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==} engines: {node: '>=14.18.0'} + range-parser@1.2.0: + resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==} + engines: {node: '>= 0.6'} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -3603,6 +4241,10 @@ packages: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -3627,6 +4269,10 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -3638,6 +4284,13 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + registry-auth-token@3.3.2: + resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==} + + registry-url@3.1.0: + resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} + engines: {node: '>=0.10.0'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -3670,6 +4323,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.60.1: + resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} @@ -3683,6 +4341,10 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -3704,6 +4366,9 @@ packages: secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3721,6 +4386,13 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + serialport@12.0.0: + resolution: {integrity: sha512-AmH3D9hHPFmnF/oq/rvigfiAouAKyK/TjnrkwZRYSFZxNggJxwvbAbfYrLeuvq7ktUdhuHdVdSjj852Z55R+uA==} + engines: {node: '>=16.0.0'} + + serve-handler@6.1.7: + resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==} + serve-static@1.16.3: resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} engines: {node: '>= 0.8.0'} @@ -3729,6 +4401,11 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + serve@14.2.6: + resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==} + engines: {node: '>= 14'} + hasBin: true + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3765,6 +4442,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3791,6 +4471,9 @@ packages: resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} engines: {node: '>=10.2.0'} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3809,6 +4492,10 @@ packages: resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} engines: {node: '>= 8'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -3824,6 +4511,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} @@ -3831,6 +4521,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -3878,14 +4571,30 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3931,10 +4640,38 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -3953,6 +4690,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} @@ -3962,6 +4703,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.4.6: resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -4014,6 +4758,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsx@4.21.0: resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} engines: {node: '>=18.0.0'} @@ -4031,6 +4794,10 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -4115,6 +4882,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} @@ -4128,6 +4898,9 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} @@ -4152,6 +4925,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-check@1.5.4: + resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -4184,6 +4960,51 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@8.0.3: resolution: {integrity: sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4227,6 +5048,34 @@ packages: yaml: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -4270,6 +5119,15 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -5458,6 +6316,8 @@ snapshots: '@oxc-project/types@0.122.0': {} + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5514,8 +6374,137 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.7': {} + '@rollup/rollup-android-arm-eabi@4.60.1': + optional: true + + '@rollup/rollup-android-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.1': + optional: true + + '@rollup/rollup-darwin-x64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.1': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.1': + optional: true + '@sec-ant/readable-stream@0.4.1': {} + '@serialport/binding-mock@10.2.2': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-cpp@12.0.1': + dependencies: + '@serialport/bindings-interface': 1.2.2 + '@serialport/parser-readline': 11.0.0 + debug: 4.3.4 + node-addon-api: 7.0.0 + node-gyp-build: 4.6.0 + transitivePeerDependencies: + - supports-color + + '@serialport/bindings-interface@1.2.2': {} + + '@serialport/parser-byte-length@12.0.0': {} + + '@serialport/parser-cctalk@12.0.0': {} + + '@serialport/parser-delimiter@11.0.0': {} + + '@serialport/parser-delimiter@12.0.0': {} + + '@serialport/parser-inter-byte-timeout@12.0.0': {} + + '@serialport/parser-packet-length@12.0.0': {} + + '@serialport/parser-readline@11.0.0': + dependencies: + '@serialport/parser-delimiter': 11.0.0 + + '@serialport/parser-readline@12.0.0': + dependencies: + '@serialport/parser-delimiter': 12.0.0 + + '@serialport/parser-ready@12.0.0': {} + + '@serialport/parser-regex@12.0.0': {} + + '@serialport/parser-slip-encoder@12.0.0': {} + + '@serialport/parser-spacepacket@12.0.0': {} + + '@serialport/stream@12.0.0': + dependencies: + '@serialport/bindings-interface': 1.2.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + '@sinclair/typebox@0.34.48': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -5560,6 +6549,55 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@tauri-apps/api@2.10.1': {} + + '@tauri-apps/cli-darwin-arm64@2.10.1': + optional: true + + '@tauri-apps/cli-darwin-x64@2.10.1': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.10.1': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.10.1': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.10.1': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.10.1': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.10.1': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.10.1': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.10.1': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.10.1': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.10.1': + optional: true + + '@tauri-apps/cli@2.10.1': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.10.1 + '@tauri-apps/cli-darwin-x64': 2.10.1 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.10.1 + '@tauri-apps/cli-linux-arm64-gnu': 2.10.1 + '@tauri-apps/cli-linux-arm64-musl': 2.10.1 + '@tauri-apps/cli-linux-riscv64-gnu': 2.10.1 + '@tauri-apps/cli-linux-x64-gnu': 2.10.1 + '@tauri-apps/cli-linux-x64-musl': 2.10.1 + '@tauri-apps/cli-win32-arm64-msvc': 2.10.1 + '@tauri-apps/cli-win32-ia32-msvc': 2.10.1 + '@tauri-apps/cli-win32-x64-msvc': 2.10.1 + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -5612,6 +6650,11 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 25.5.0 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/connect@3.4.38': dependencies: '@types/node': 25.5.0 @@ -5620,6 +6663,8 @@ snapshots: dependencies: '@types/node': 25.5.0 + '@types/deep-eql@4.0.2': {} + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -5689,6 +6734,10 @@ snapshots: '@types/ms@2.1.0': {} + '@types/node@22.19.15': + dependencies: + undici-types: 6.21.0 + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 @@ -5910,6 +6959,48 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: 8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(terser@5.46.1)(tsx@4.21.0) + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 7.3.1(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -5990,6 +7081,8 @@ snapshots: '@xtuc/long@4.2.2': {} + '@zeit/schemas@2.36.0': {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -6060,6 +7153,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + ansi-colors@4.1.3: {} ansi-escapes@4.3.2: @@ -6080,6 +7177,8 @@ snapshots: ansis@4.2.0: {} + any-promise@1.3.0: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -6089,8 +7188,12 @@ snapshots: append-field@1.0.0: {} + arch@2.2.0: {} + arg@4.1.3: {} + arg@5.0.2: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -6101,8 +7204,12 @@ snapshots: array-timsort@1.0.3: {} + assertion-error@2.0.1: {} + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -6211,6 +7318,17 @@ snapshots: transitivePeerDependencies: - supports-color + boxen@7.0.0: + dependencies: + ansi-align: 3.0.1 + camelcase: 7.0.1 + chalk: 5.0.1 + cli-boxes: 3.0.0 + string-width: 5.1.2 + type-fest: 2.19.0 + widest-line: 4.0.1 + wrap-ansi: 8.1.0 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6254,12 +7372,21 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bundle-require@5.1.0(esbuild@0.27.4): + dependencies: + esbuild: 0.27.4 + load-tsconfig: 0.2.5 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 + bytes@3.0.0: {} + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6283,17 +7410,35 @@ snapshots: camelcase@6.3.0: {} + camelcase@7.0.1: {} + caniuse-lite@1.0.30001781: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk-template@0.4.0: + dependencies: + chalk: 4.1.2 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.0.1: {} + char-regex@1.0.2: {} chardet@2.1.1: {} + check-error@2.1.3: {} + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -6312,6 +7457,8 @@ snapshots: libphonenumber-js: 1.12.40 validator: 13.15.26 + cli-boxes@3.0.0: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -6326,6 +7473,12 @@ snapshots: cli-width@4.1.0: {} + clipboardy@3.0.0: + dependencies: + arch: 2.2.0 + execa: 5.1.1 + is-wsl: 2.2.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -6346,6 +7499,8 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -6362,6 +7517,22 @@ snapshots: core-util-is: 1.0.3 esprima: 4.0.1 + compressible@2.0.18: + dependencies: + mime-db: 1.54.0 + + compression@1.8.1: + dependencies: + bytes: 3.1.2 + compressible: 2.0.18 + debug: 2.6.9 + negotiator: 0.6.4 + on-headers: 1.1.0 + safe-buffer: 5.2.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -6371,8 +7542,12 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + confbox@0.1.8: {} + consola@3.4.2: {} + content-disposition@0.5.2: {} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 @@ -6415,18 +7590,28 @@ snapshots: csstype@3.2.3: {} + dateformat@4.6.3: {} + dayjs@1.11.20: {} debug@2.6.9: dependencies: ms: 2.0.0 + debug@4.3.4: + dependencies: + ms: 2.1.2 + debug@4.4.3: dependencies: ms: 2.1.3 dedent@1.7.2: {} + deep-eql@5.0.2: {} + + deep-extend@0.6.0: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -6503,6 +7688,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + engine.io-client@6.6.4: dependencies: '@socket.io/component-emitter': 3.1.2 @@ -6547,6 +7736,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: @@ -6684,6 +7875,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} @@ -6727,6 +7922,8 @@ snapshots: exit-x@0.2.2: {} + expect-type@1.3.0: {} + expect@30.3.0: dependencies: '@jest/expect-utils': 30.3.0 @@ -6814,6 +8011,8 @@ snapshots: dependencies: is-extendable: 0.1.1 + fast-copy@4.0.2: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -6884,6 +8083,12 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.17 + mlly: 1.8.2 + rollup: 4.60.1 + flat-cache@4.0.1: dependencies: flatted: 3.4.2 @@ -7051,6 +8256,8 @@ snapshots: dependencies: function-bind: 1.1.2 + help-me@5.0.0: {} + hono-openapi@1.3.0(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-community/standard-openapi@0.2.9(@standard-community/standard-json@0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6))(@standard-schema/spec@1.1.0)(openapi-types@12.1.3)(zod@4.3.6))(@types/json-schema@7.0.15)(hono@4.12.9)(openapi-types@12.1.3): dependencies: '@standard-community/standard-json': 0.3.5(@standard-schema/spec@1.1.0)(@types/json-schema@7.0.15)(quansync@0.2.11)(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6) @@ -7109,6 +8316,8 @@ snapshots: inherits@2.0.4: {} + ini@1.3.8: {} + ioredis@5.10.1: dependencies: '@ioredis/commands': 1.5.1 @@ -7131,6 +8340,8 @@ snapshots: is-callable@1.2.7: {} + is-docker@2.2.1: {} + is-extendable@0.1.1: {} is-extglob@2.1.1: {} @@ -7149,6 +8360,8 @@ snapshots: is-plain-obj@4.1.0: {} + is-port-reachable@4.0.0: {} + is-promise@4.0.0: {} is-stream@2.0.1: {} @@ -7163,6 +8376,10 @@ snapshots: is-unicode-supported@2.1.0: {} + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + isarray@2.0.5: {} isexe@2.0.0: {} @@ -7525,12 +8742,16 @@ snapshots: jose@6.2.2: {} + joycon@3.1.1: {} + js-tiktoken@1.0.21: dependencies: base64-js: 1.5.1 js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.2: dependencies: argparse: 1.0.10 @@ -7661,10 +8882,14 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lilconfig@3.1.3: {} + lines-and-columns@1.2.4: {} load-esm@1.0.3: {} + load-tsconfig@0.2.5: {} + loader-runner@4.3.1: {} locate-path@5.0.0: @@ -7702,6 +8927,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + loupe@3.2.1: {} + lru-cache@10.4.3: {} lru-cache@11.2.7: {} @@ -7742,10 +8969,16 @@ snapshots: methods@1.1.2: {} + mime-db@1.33.0: {} + mime-db@1.52.0: {} mime-db@1.54.0: {} + mime-types@2.1.18: + dependencies: + mime-db: 1.33.0 + mime-types@2.1.35: dependencies: mime-db: 1.52.0 @@ -7774,8 +9007,17 @@ snapshots: minipass@7.1.3: {} + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + ms@2.0.0: {} + ms@2.1.2: {} + ms@2.1.3: {} multer@2.1.1: @@ -7787,6 +9029,12 @@ snapshots: mute-stream@2.0.0: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.11: {} napi-postinstall@0.3.4: {} @@ -7795,12 +9043,16 @@ snapshots: negotiator@0.6.3: {} + negotiator@0.6.4: {} + negotiator@1.0.0: {} neo-async@2.6.2: {} node-abort-controller@3.1.1: {} + node-addon-api@7.0.0: {} + node-addon-api@8.7.0: {} node-emoji@1.11.0: @@ -7811,6 +9063,8 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-gyp-build@4.6.0: {} + node-gyp-build@4.8.4: {} node-int64@0.4.0: {} @@ -7834,10 +9088,14 @@ snapshots: object-inspect@1.13.4: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 + on-headers@1.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -7932,6 +9190,8 @@ snapshots: path-is-absolute@1.0.1: {} + path-is-inside@1.0.2: {} + path-key@3.1.1: {} path-key@4.0.0: {} @@ -7948,10 +9208,16 @@ snapshots: path-to-regexp@0.1.13: {} + path-to-regexp@3.3.0: {} + path-to-regexp@8.3.0: {} path-type@4.0.0: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + pause@0.0.1: {} pg-cloudflare@1.3.0: @@ -7997,6 +9263,46 @@ snapshots: picomatch@4.0.4: {} + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@9.14.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 3.1.0 + pirates@4.0.7: {} pkce-challenge@5.0.1: {} @@ -8005,10 +9311,23 @@ snapshots: dependencies: find-up: 4.1.0 + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} + postcss-load-config@6.0.1(postcss@8.5.8)(tsx@4.21.0): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.8 + tsx: 4.21.0 + postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -8043,6 +9362,8 @@ snapshots: dependencies: parse-ms: 4.0.0 + process-warning@5.0.0: {} + process@0.11.10: {} proxy-addr@2.0.7: @@ -8050,6 +9371,11 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode@2.3.1: {} pure-rand@7.0.1: {} @@ -8064,8 +9390,12 @@ snapshots: quansync@0.2.11: {} + quick-format-unescaped@4.0.4: {} + radash@12.1.1: {} + range-parser@1.2.0: {} + range-parser@1.2.1: {} raw-body@2.5.3: @@ -8082,6 +9412,13 @@ snapshots: iconv-lite: 0.7.2 unpipe: 1.0.0 + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -8107,6 +9444,8 @@ snapshots: readdirp@4.1.2: {} + real-require@0.2.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -8115,6 +9454,15 @@ snapshots: reflect-metadata@0.2.2: {} + registry-auth-token@3.3.2: + dependencies: + rc: 1.2.8 + safe-buffer: 5.2.1 + + registry-url@3.1.0: + dependencies: + rc: 1.2.8 + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -8155,6 +9503,37 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.12 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.12 + rollup@4.60.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.1 + '@rollup/rollup-android-arm64': 4.60.1 + '@rollup/rollup-darwin-arm64': 4.60.1 + '@rollup/rollup-darwin-x64': 4.60.1 + '@rollup/rollup-freebsd-arm64': 4.60.1 + '@rollup/rollup-freebsd-x64': 4.60.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.1 + '@rollup/rollup-linux-arm-musleabihf': 4.60.1 + '@rollup/rollup-linux-arm64-gnu': 4.60.1 + '@rollup/rollup-linux-arm64-musl': 4.60.1 + '@rollup/rollup-linux-loong64-gnu': 4.60.1 + '@rollup/rollup-linux-loong64-musl': 4.60.1 + '@rollup/rollup-linux-ppc64-gnu': 4.60.1 + '@rollup/rollup-linux-ppc64-musl': 4.60.1 + '@rollup/rollup-linux-riscv64-gnu': 4.60.1 + '@rollup/rollup-linux-riscv64-musl': 4.60.1 + '@rollup/rollup-linux-s390x-gnu': 4.60.1 + '@rollup/rollup-linux-x64-gnu': 4.60.1 + '@rollup/rollup-linux-x64-musl': 4.60.1 + '@rollup/rollup-openbsd-x64': 4.60.1 + '@rollup/rollup-openharmony-arm64': 4.60.1 + '@rollup/rollup-win32-arm64-msvc': 4.60.1 + '@rollup/rollup-win32-ia32-msvc': 4.60.1 + '@rollup/rollup-win32-x64-gnu': 4.60.1 + '@rollup/rollup-win32-x64-msvc': 4.60.1 + fsevents: 2.3.3 + router@2.2.0: dependencies: debug: 4.4.3 @@ -8175,6 +9554,8 @@ snapshots: safe-buffer@5.2.1: {} + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} scheduler@0.27.0: {} @@ -8199,6 +9580,8 @@ snapshots: secure-json-parse@2.7.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.4: {} @@ -8237,6 +9620,35 @@ snapshots: transitivePeerDependencies: - supports-color + serialport@12.0.0: + dependencies: + '@serialport/binding-mock': 10.2.2 + '@serialport/bindings-cpp': 12.0.1 + '@serialport/parser-byte-length': 12.0.0 + '@serialport/parser-cctalk': 12.0.0 + '@serialport/parser-delimiter': 12.0.0 + '@serialport/parser-inter-byte-timeout': 12.0.0 + '@serialport/parser-packet-length': 12.0.0 + '@serialport/parser-readline': 12.0.0 + '@serialport/parser-ready': 12.0.0 + '@serialport/parser-regex': 12.0.0 + '@serialport/parser-slip-encoder': 12.0.0 + '@serialport/parser-spacepacket': 12.0.0 + '@serialport/stream': 12.0.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + + serve-handler@6.1.7: + dependencies: + bytes: 3.0.0 + content-disposition: 0.5.2 + mime-types: 2.1.18 + minimatch: 3.1.5 + path-is-inside: 1.0.2 + path-to-regexp: 3.3.0 + range-parser: 1.2.0 + serve-static@1.16.3: dependencies: encodeurl: 2.0.0 @@ -8255,6 +9667,22 @@ snapshots: transitivePeerDependencies: - supports-color + serve@14.2.6: + dependencies: + '@zeit/schemas': 2.36.0 + ajv: 8.18.0 + arg: 5.0.2 + boxen: 7.0.0 + chalk: 5.0.1 + chalk-template: 0.4.0 + clipboardy: 3.0.0 + compression: 1.8.1 + is-port-reachable: 4.0.0 + serve-handler: 6.1.7 + update-check: 1.5.4 + transitivePeerDependencies: + - supports-color + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -8306,6 +9734,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -8353,6 +9783,10 @@ snapshots: - supports-color - utf-8-validate + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -8369,6 +9803,8 @@ snapshots: source-map@0.7.4: {} + source-map@0.7.6: {} + split2@4.2.0: {} sprintf-js@1.0.3: {} @@ -8379,10 +9815,14 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + standard-as-callback@2.1.0: {} statuses@2.0.2: {} + std-env@3.10.0: {} + streamsearch@1.1.0: {} string-length@4.0.2: @@ -8424,12 +9864,30 @@ snapshots: strip-final-newline@4.0.0: {} + strip-json-comments@2.0.1: {} + strip-json-comments@3.1.1: {} + strip-json-comments@5.0.3: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -8467,11 +9925,33 @@ snapshots: glob: 7.2.3 minimatch: 3.1.5 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + tmpl@1.0.5: {} to-buffer@1.2.2: @@ -8490,12 +9970,16 @@ snapshots: tr46@0.0.3: {} + tree-kill@1.2.2: {} + ts-algebra@2.0.0: {} ts-api-utils@2.5.0(typescript@5.8.3): dependencies: typescript: 5.8.3 + ts-interface-checker@0.1.13: {} + ts-jest@29.4.6(@babel/core@7.29.0)(@jest/transform@30.3.0)(@jest/types@30.3.0)(babel-jest@30.3.0(@babel/core@7.29.0))(jest-util@30.3.0)(jest@30.3.0(@types/node@25.5.0)(ts-node@10.9.2(@types/node@25.5.0)(typescript@5.8.3)))(typescript@5.8.3): dependencies: bs-logger: 0.2.6 @@ -8549,6 +10033,34 @@ snapshots: tslib@2.8.1: {} + tsup@8.5.1(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.4) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.4 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.8)(tsx@4.21.0) + resolve-from: 5.0.0 + rollup: 4.60.1 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.8 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.21.0: dependencies: esbuild: 0.27.4 @@ -8564,6 +10076,8 @@ snapshots: type-fest@0.21.3: {} + type-fest@2.19.0: {} + type-fest@4.41.0: {} type-is@1.6.18: @@ -8614,6 +10128,8 @@ snapshots: typescript@5.9.3: {} + ufo@1.6.3: {} + uglify-js@3.19.3: optional: true @@ -8623,6 +10139,8 @@ snapshots: uint8array-extras@1.5.0: {} + undici-types@6.21.0: {} + undici-types@7.18.2: {} unicorn-magic@0.3.0: {} @@ -8661,6 +10179,11 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + update-check@1.5.4: + dependencies: + registry-auth-token: 3.3.2 + registry-url: 3.1.0 + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -8685,6 +10208,42 @@ snapshots: vary@1.1.2: {} + vite-node@3.2.4(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.1(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0): + dependencies: + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.8 + rollup: 4.60.1 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.19.15 + fsevents: 2.3.3 + lightningcss: 1.32.0 + terser: 5.46.1 + tsx: 4.21.0 + vite@8.0.3(@types/node@25.5.0)(esbuild@0.27.4)(terser@5.46.1)(tsx@4.21.0): dependencies: lightningcss: 1.32.0 @@ -8699,6 +10258,47 @@ snapshots: terser: 5.46.1 tsx: 4.21.0 + vitest@3.2.4(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@22.19.15)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.15 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -8769,6 +10369,15 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + widest-line@4.0.1: + dependencies: + string-width: 5.1.2 + word-wrap@1.2.5: {} wordwrap@1.0.0: {}