diff --git a/.githooks/_/pre-commit b/.githooks/_/pre-commit deleted file mode 100755 index 387f0eb46..000000000 --- a/.githooks/_/pre-commit +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -if [ "$SKIP_SIMPLE_GIT_HOOKS" = "1" ]; then - echo "[INFO] SKIP_SIMPLE_GIT_HOOKS is set to 1, skipping hook." - exit 0 -fi - -if [ -f "$SIMPLE_GIT_HOOKS_RC" ]; then - . "$SIMPLE_GIT_HOOKS_RC" -fi - -# cd packages/web && bun run lint && bun run format \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7c03ebef4..6dbab3df4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ npm/ packages/protobufs/packages/ts/dist .pnpm-store/ +.claude +.gemini # Local dev certs *.pem diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..cb2c84d5c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm lint-staged diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..ff601bd11 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxfmt/configuration_schema.json", + "useTabs": false, + "tabWidth": 2, + "printWidth": 80, + "ignorePatterns": [ + "node_modules", + "dist", + "protobufs", + "**/locales/*-*/*.json", + "**/packages/ui/" + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..2792d77ee --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,26 @@ +{ + "$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxlint/configuration_schema.json", + "plugins": ["react", "react-perf", "jsx-a11y", "typescript", "import"], + "rules": { + "no-debugger": "error", + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unused-vars": "error", + "import/no-unused-modules": "error" + }, + "ignorePatterns": [ + "node_modules", + "dist", + "protobufs", + "**/locales/*-*/*.json", + "**/packages/ui/" + ], + "overrides": [ + { + "files": ["**/*.test.ts", "**/*.test.tsx", "**/__tests__/**/*.{ts,tsx}"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off" + } + } + ] +} diff --git a/biome.json b/biome.json deleted file mode 100644 index eb286f6cb..000000000 --- a/biome.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "files": { - "includes": [ - "**/*.ts", - "**/*.tsx", - "!npm_modules", - "!**/dist", - "!**/protobufs", - "!**/.*", - "!npm", - "**/*.json", - "!**/locales/*-*/*.json", - "!**/packages/ui/" - ], - "ignoreUnknown": false - }, - "formatter": { - "enabled": true, - "formatWithErrors": false, - "indentStyle": "space", - "indentWidth": 2, - "lineWidth": 80, - "attributePosition": "auto" - }, - "linter": { - "enabled": true, - "includes": ["**", "!test/**", "!**/dist/**"], - "rules": { - "recommended": true, - "suspicious": { - "noExplicitAny": "error", - "noDebugger": "error" - }, - "style": { - "useBlockStatements": "error", - "useSingleVarDeclarator": "off" - }, - "correctness": { - "noUnusedVariables": "error", - "noUnusedImports": "error", - "useImportExtensions": "error" - } - } - }, - "javascript": { - "formatter": { - "quoteStyle": "double", - "semicolons": "always" - } - }, - "json": { - "formatter": { - "enabled": true - } - }, - "overrides": [ - { - "includes": [ - "**/*.test.ts", - "**/*.test.tsx", - "**/__tests__/**/*.{ts,tsx}" - ], - "linter": { - "rules": { - "suspicious": { - "noExplicitAny": "off" - }, - "style": { - "noNonNullAssertion": "off" - } - } - } - } - ] -} diff --git a/package.json b/package.json index 6f0e13b17..e591ad462 100644 --- a/package.json +++ b/package.json @@ -12,31 +12,38 @@ "url": "https://github.com/meshtastic/web/issues" }, "homepage": "https://meshtastic.org", - "simple-git-hooks": { - "pre-commit": "pnpm run check:fix" - }, "scripts": { "preinstall": "npx only-allow pnpm", - "lint": "biome lint", - "lint:fix": "biome lint --write", - "format": "biome format", - "format:fix": "biome format . --write", - "check": "biome check", - "check:fix": "biome check --write", + "lint": "oxlint", + "lint:fix": "oxlint --fix", + "format": "oxfmt --check .", + "format:fix": "oxfmt .", + "check": "pnpm lint && pnpm format", + "check:fix": "pnpm lint:fix && pnpm format:fix", "build:all": "pnpm run --filter '*' build", "clean:all": "pnpm run --filter '*' clean", "publish:packages": "pnpm run build --filter 'packages/transport-*'", - "test": "vitest" + "test": "vitest", + "prepare": "husky" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "oxlint", + "oxfmt" + ] }, "dependencies": { "@bufbuild/protobuf": "^2.9.0", - "@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.7.12-1", + "@meshtastic/protobufs": "npm:@jsr/meshtastic__protobufs@^2.7.18", "ste-simple-events": "^3.0.11", "tslog": "^4.9.3" }, "devDependencies": { - "@biomejs/biome": "2.2.4", "@types/node": "^24.3.1", + "husky": "^9.1.0", + "lint-staged": "^16.0.0", + "oxfmt": "^0.16.0", + "oxlint": "^1.41.0", "tsdown": "^0.15.0", "typescript": "^5.9.2", "vitest": "^3.2.4" @@ -46,8 +53,7 @@ "@serialport/bindings-cpp", "@tailwindcss/oxide", "core-js", - "esbuild", - "simple-git-hooks" + "esbuild" ] } } diff --git a/packages/core/src/meshDevice.ts b/packages/core/src/meshDevice.ts index c1367815f..d3efae93c 100755 --- a/packages/core/src/meshDevice.ts +++ b/packages/core/src/meshDevice.ts @@ -80,7 +80,20 @@ export class MeshDevice { this.pendingSettingsChanges = state; }); - this.transport.fromDevice.pipeTo(decodePacket(this)); + this.transport.fromDevice + .pipeTo(decodePacket(this)) + .then(() => { + this.log.info( + Emitter[Emitter.HandleFromRadio], + "📡 Packet pipeline completed", + ); + }) + .catch((err) => { + this.log.error( + Emitter[Emitter.HandleFromRadio], + `⚠️ Packet pipeline error: ${err?.message ?? err}`, + ); + }); } /** Abstract method that connects to the radio */ @@ -759,7 +772,7 @@ export class MeshDevice { } /** - * Triggers the device configure process + * Triggers the device configure process (single-stage) */ public configure(): Promise { this.log.debug( @@ -785,6 +798,128 @@ export class MeshDevice { ); } + /** + * Two-stage config flow + * Stage 1: Config-only (device config, module config, channels) + * Stage 2: Node-info only (node database) + */ + public async configureTwoStage(): Promise { + // Nonces from Android implementation + const CONFIG_ONLY_NONCE = 69420; + const NODE_INFO_NONCE = 69421; + const WANT_CONFIG_DELAY = 100; // ms + const STAGE_TIMEOUT = 30000; // 30 second timeout + + this.log.debug( + Emitter[Emitter.Configure], + "⚙️ Starting two-stage configuration flow", + ); + this.updateDeviceStatus(DeviceStatusEnum.DeviceConfiguring); + + // Helper to wait for a specific config complete nonce + const waitForConfigComplete = ( + nonce: number, + stageName: string, + ): Promise => { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + unsubscribe(); + reject(new Error(`${stageName} timeout`)); + }, STAGE_TIMEOUT); + + const unsubscribe = this.events.onConfigComplete.subscribe( + (configCompleteId) => { + if (configCompleteId === nonce) { + clearTimeout(timeout); + unsubscribe(); + this.log.debug( + Emitter[Emitter.Configure], + `✅ ${stageName} complete (nonce: ${configCompleteId})`, + ); + resolve(configCompleteId); + } + }, + ); + }); + }; + + // Stage 1: Config-only + this.log.debug( + Emitter[Emitter.Configure], + `⚙️ Stage 1: Requesting config-only (nonce: ${CONFIG_ONLY_NONCE})`, + ); + + // Set up listener FIRST, then send request + const stage1Promise = waitForConfigComplete( + CONFIG_ONLY_NONCE, + "Config-only stage", + ); + + // Send request and wait for it to be transmitted + await this.sendConfigRequest(CONFIG_ONLY_NONCE); + + // Now wait for the config complete response + await stage1Promise; + + // Send heartbeat between stages + this.log.debug( + Emitter[Emitter.Configure], + "❤️ Sending heartbeat between config stages", + ); + await new Promise((resolve) => setTimeout(resolve, WANT_CONFIG_DELAY)); + await this.heartbeat(); + await new Promise((resolve) => setTimeout(resolve, WANT_CONFIG_DELAY)); + + // Stage 2: Node-info only + this.log.debug( + Emitter[Emitter.Configure], + `⚙️ Stage 2: Requesting node-info (nonce: ${NODE_INFO_NONCE})`, + ); + + const stage2Promise = waitForConfigComplete( + NODE_INFO_NONCE, + "Node-info stage", + ); + + // Send request and wait for it to be transmitted + await this.sendConfigRequest(NODE_INFO_NONCE); + + // Now wait for the node info complete response + await stage2Promise; + + this.log.debug( + Emitter[Emitter.Configure], + "✅ Two-stage configuration complete", + ); + this.updateDeviceStatus(DeviceStatusEnum.DeviceConfigured); + } + + /** + * Sends a config request and waits for it to be sent + */ + private async sendConfigRequest(nonce: number): Promise { + this.log.debug( + Emitter[Emitter.Configure], + `⚙️ Sending config request with nonce: ${nonce}`, + ); + + const toRadio = create(Protobuf.Mesh.ToRadioSchema, { + payloadVariant: { + case: "wantConfigId", + value: nonce, + }, + }); + + return this.sendRaw(toBinary(Protobuf.Mesh.ToRadioSchema, toRadio)).catch( + (e) => { + if (this.deviceStatus === DeviceStatusEnum.DeviceDisconnected) { + throw new Error("Device connection lost"); + } + throw e; + }, + ); + } + /** * Serial connection requires a heartbeat ping to stay connected, otherwise times out after 15 minutes */ @@ -935,6 +1070,8 @@ export class MeshDevice { rxRssi: meshPacket.rxRssi, rxSnr: meshPacket.rxSnr, viaMqtt: meshPacket.viaMqtt, + replyId: dataPacket.replyId, + emoji: dataPacket.emoji, }; this.log.trace( @@ -1001,6 +1138,15 @@ export class MeshDevice { }); } + // Update message store with ACK information + this.updateMessageACK(dataPacket.requestId, { + receivedACK: true, + ackError: routingPacket.variant.value, + ackTimestamp: meshPacket.rxTime || Date.now(), + ackSNR: meshPacket.rxSnr || 0, + realACK: meshPacket.to === this.myNodeInfo.myNodeNum, + }); + break; } case "routeReply": { @@ -1245,4 +1391,44 @@ export class MeshDevice { throw new Error(`Unhandled case ${dataPacket.portnum}`); } } + + /** + * Update message store with ACK information from routing packet + */ + private updateMessageACK( + messageId: number, + ackInfo: { + receivedACK: boolean; + ackError: number; + ackTimestamp: number; + ackSNR: number; + realACK: boolean; + }, + ): void { + // This will be handled by the web layer through event system + // For now, just log the ACK information + this.log.debug( + Emitter[Emitter.HandleMeshPacket], + `📨 ACK received for message ${messageId}:`, + ackInfo, + ); + + // Dispatch event for web layer to handle + this.events.onRoutingPacket.dispatch({ + id: messageId, + rxTime: new Date(ackInfo.ackTimestamp * 1000), + type: "direct", + from: 0, // Not applicable for ACK + to: this.myNodeInfo.myNodeNum, + channel: 0, + hops: 0, + rxRssi: 0, + rxSnr: ackInfo.ackSNR, + viaMqtt: false, + data: { + messageId, + ...ackInfo, + }, + }); + } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d2614d5f2..f0720c0d0 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -29,6 +29,7 @@ export interface QueueItem { sent: boolean; added: Date; promise: Promise; + resolveSend?: (id: number) => void; } export interface HttpRetryConfig { @@ -64,6 +65,10 @@ export interface PacketMetadata { rxRssi: number; rxSnr: number; viaMqtt: boolean; + replyId?: number; + emoji?: number; + hopsLimit: number; + hopsStart: number; data: T; } diff --git a/packages/core/src/utils/decodePayload.ts b/packages/core/src/utils/decodePayload.ts new file mode 100644 index 000000000..ae175f33a --- /dev/null +++ b/packages/core/src/utils/decodePayload.ts @@ -0,0 +1,180 @@ +import { fromBinary, toJson } from "@bufbuild/protobuf"; +import * as Protobuf from "@meshtastic/protobufs"; + +/** + * Type for valid PortNum string names (excludes reverse numeric mappings) + */ +export type PortNumName = { + [K in keyof typeof Protobuf.Portnums.PortNum]: typeof Protobuf.Portnums.PortNum[K] extends number + ? K + : never; +}[keyof typeof Protobuf.Portnums.PortNum]; + +/** + * Strongly typed lookup map for O(1) string-to-number PortNum enum conversion. + * Allows bracket notation: portNumMap["TEXT_MESSAGE_APP"] => 1 + */ +export const portNumMap = Object.fromEntries( + Object.entries(Protobuf.Portnums.PortNum).filter( + (entry): entry is [PortNumName, number] => typeof entry[1] === "number", + ), +) as { readonly [K in PortNumName]: typeof Protobuf.Portnums.PortNum[K] }; + +/** + * Decode a mesh packet payload based on its port number. + * Returns the decoded protobuf message or string, or null if decoding fails. + */ +export function decodePayload( + portnum: number, + payload: Uint8Array, +): + | Protobuf.Mesh.Routing + | Protobuf.Admin.AdminMessage + | Protobuf.Mesh.Position + | Protobuf.Mesh.User + | Protobuf.Telemetry.Telemetry + | Protobuf.Mesh.Waypoint + | Protobuf.Mesh.RouteDiscovery + | Protobuf.Mesh.NeighborInfo + | Protobuf.StoreForward.StoreAndForward + | Protobuf.PaxCount.Paxcount + | Protobuf.RemoteHardware.HardwareMessage + | string + | null { + try { + switch (portnum) { + case Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP: + case Protobuf.Portnums.PortNum.TEXT_MESSAGE_COMPRESSED_APP: + return new TextDecoder().decode(payload); + + case Protobuf.Portnums.PortNum.ROUTING_APP: + return fromBinary(Protobuf.Mesh.RoutingSchema, payload); + + case Protobuf.Portnums.PortNum.ADMIN_APP: + return fromBinary(Protobuf.Admin.AdminMessageSchema, payload); + + case Protobuf.Portnums.PortNum.POSITION_APP: + return fromBinary(Protobuf.Mesh.PositionSchema, payload); + + case Protobuf.Portnums.PortNum.NODEINFO_APP: + return fromBinary(Protobuf.Mesh.UserSchema, payload); + + case Protobuf.Portnums.PortNum.TELEMETRY_APP: + return fromBinary(Protobuf.Telemetry.TelemetrySchema, payload); + + case Protobuf.Portnums.PortNum.WAYPOINT_APP: + return fromBinary(Protobuf.Mesh.WaypointSchema, payload); + + case Protobuf.Portnums.PortNum.TRACEROUTE_APP: + return fromBinary(Protobuf.Mesh.RouteDiscoverySchema, payload); + + case Protobuf.Portnums.PortNum.NEIGHBORINFO_APP: + return fromBinary(Protobuf.Mesh.NeighborInfoSchema, payload); + + case Protobuf.Portnums.PortNum.STORE_FORWARD_APP: + return fromBinary(Protobuf.StoreForward.StoreAndForwardSchema, payload); + + case Protobuf.Portnums.PortNum.PAXCOUNTER_APP: + return fromBinary(Protobuf.PaxCount.PaxcountSchema, payload); + + case Protobuf.Portnums.PortNum.REMOTE_HARDWARE_APP: + return fromBinary( + Protobuf.RemoteHardware.HardwareMessageSchema, + payload, + ); + + default: + return null; + } + } catch { + return null; + } +} + +/** + * Decode a mesh packet payload and return as JSON for display. + * Returns a JSON object or null if decoding fails or port is unsupported. + */ +export function decodePayloadToJson( + portnum: number, + payload: Uint8Array, +): Record | null { + try { + switch (portnum) { + case Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP: + case Protobuf.Portnums.PortNum.TEXT_MESSAGE_COMPRESSED_APP: + return { message: new TextDecoder().decode(payload) }; + + case Protobuf.Portnums.PortNum.ROUTING_APP: + return toJson( + Protobuf.Mesh.RoutingSchema, + fromBinary(Protobuf.Mesh.RoutingSchema, payload), + ); + + case Protobuf.Portnums.PortNum.ADMIN_APP: + return toJson( + Protobuf.Admin.AdminMessageSchema, + fromBinary(Protobuf.Admin.AdminMessageSchema, payload), + ); + + case Protobuf.Portnums.PortNum.POSITION_APP: + return toJson( + Protobuf.Mesh.PositionSchema, + fromBinary(Protobuf.Mesh.PositionSchema, payload), + ); + + case Protobuf.Portnums.PortNum.NODEINFO_APP: + return toJson( + Protobuf.Mesh.UserSchema, + fromBinary(Protobuf.Mesh.UserSchema, payload), + ); + + case Protobuf.Portnums.PortNum.TELEMETRY_APP: + return toJson( + Protobuf.Telemetry.TelemetrySchema, + fromBinary(Protobuf.Telemetry.TelemetrySchema, payload), + ); + + case Protobuf.Portnums.PortNum.WAYPOINT_APP: + return toJson( + Protobuf.Mesh.WaypointSchema, + fromBinary(Protobuf.Mesh.WaypointSchema, payload), + ); + + case Protobuf.Portnums.PortNum.TRACEROUTE_APP: + return toJson( + Protobuf.Mesh.RouteDiscoverySchema, + fromBinary(Protobuf.Mesh.RouteDiscoverySchema, payload), + ); + + case Protobuf.Portnums.PortNum.NEIGHBORINFO_APP: + return toJson( + Protobuf.Mesh.NeighborInfoSchema, + fromBinary(Protobuf.Mesh.NeighborInfoSchema, payload), + ); + + case Protobuf.Portnums.PortNum.STORE_FORWARD_APP: + return toJson( + Protobuf.StoreForward.StoreAndForwardSchema, + fromBinary(Protobuf.StoreForward.StoreAndForwardSchema, payload), + ); + + case Protobuf.Portnums.PortNum.PAXCOUNTER_APP: + return toJson( + Protobuf.PaxCount.PaxcountSchema, + fromBinary(Protobuf.PaxCount.PaxcountSchema, payload), + ); + + case Protobuf.Portnums.PortNum.REMOTE_HARDWARE_APP: + return toJson( + Protobuf.RemoteHardware.HardwareMessageSchema, + fromBinary(Protobuf.RemoteHardware.HardwareMessageSchema, payload), + ); + + default: + return null; + } + } catch { + return null; + } +} diff --git a/packages/core/src/utils/eventSystem.ts b/packages/core/src/utils/eventSystem.ts index f139c0d94..2eb87cf83 100644 --- a/packages/core/src/utils/eventSystem.ts +++ b/packages/core/src/utils/eventSystem.ts @@ -369,8 +369,8 @@ export class EventSystem { * * @event onDeviceDebugLog */ - public readonly onDeviceDebugLog: SimpleEventDispatcher = - new SimpleEventDispatcher(); + public readonly onDeviceDebugLog: SimpleEventDispatcher = + new SimpleEventDispatcher(); /** * Outputs status of pending settings changes diff --git a/packages/core/src/utils/mod.ts b/packages/core/src/utils/mod.ts index b4717d8f6..ad85ded32 100644 --- a/packages/core/src/utils/mod.ts +++ b/packages/core/src/utils/mod.ts @@ -1,3 +1,9 @@ +export { + decodePayload, + decodePayloadToJson, + portNumMap, + type PortNumName, +} from "./decodePayload.ts"; export { EventSystem } from "./eventSystem.ts"; export { Queue } from "./queue.ts"; export { fromDeviceStream } from "./transform/fromDevice.ts"; diff --git a/packages/core/src/utils/queue.ts b/packages/core/src/utils/queue.ts index 65bff5691..2600644de 100644 --- a/packages/core/src/utils/queue.ts +++ b/packages/core/src/utils/queue.ts @@ -40,20 +40,42 @@ export class Queue { reject(e); } }); - setTimeout(() => { - if (this.queue.findIndex((qi) => qi.id === item.id) !== -1) { - this.remove(item.id); - const decoded = fromBinary(Protobuf.Mesh.ToRadioSchema, item.data); - - if ( - decoded.payloadVariant.case === "heartbeat" || - decoded.payloadVariant.case === "wantConfigId" - ) { - // heartbeat and wantConfigId packets are not acknowledged by the device, assume success after timeout + // Check if this is a fire-and-forget packet type + const decoded = fromBinary(Protobuf.Mesh.ToRadioSchema, item.data); + console.debug(`[Queue] Queued packet id=${item.id} type=${decoded.payloadVariant.case}`); + if ( + decoded.payloadVariant.case === "heartbeat" || + decoded.payloadVariant.case === "wantConfigId" + ) { + // heartbeat and wantConfigId packets are not acknowledged by the device + // For these packets, resolve once they have actually been sent + const start = Date.now(); + const intervalId = setInterval(() => { + // Resolve when processQueue() (or equivalent) marks the item as sent + if (queueItem.sent) { + clearInterval(intervalId); + this.remove(item.id); resolve(item.id); return; } + // Guard against waiting forever if the item is never sent + if (Date.now() - start >= this.timeout) { + clearInterval(intervalId); + if (this.queue.findIndex((qi) => qi.id === item.id) !== -1) { + this.remove(item.id); + } + reject({ + id: item.id, + error: Protobuf.Mesh.Routing_Error.TIMEOUT, + }); + } + }, 50); + return; + } + setTimeout(() => { + if (this.queue.findIndex((qi) => qi.id === item.id) !== -1) { + this.remove(item.id); console.warn( `Packet ${item.id} of type ${decoded.payloadVariant.case} timed out`, ); @@ -114,8 +136,10 @@ export class Queue { if (item) { await new Promise((resolve) => setTimeout(resolve, 200)); try { + console.debug(`[Queue] Writing packet id=${item.id} size=${item.data.length} bytes`); await writer.write(item.data); item.sent = true; + console.debug(`[Queue] Packet id=${item.id} sent successfully`); } catch (error) { if ( error?.code === "ECONNRESET" || diff --git a/packages/core/src/utils/transform/decodePacket.ts b/packages/core/src/utils/transform/decodePacket.ts index a90a37f6f..00a319601 100644 --- a/packages/core/src/utils/transform/decodePacket.ts +++ b/packages/core/src/utils/transform/decodePacket.ts @@ -21,6 +21,7 @@ export const decodePacket = (device: MeshDevice) => break; } case "debug": { + device.events.onDeviceDebugLog.dispatch(chunk.data); break; } case "packet": { @@ -40,6 +41,11 @@ export const decodePacket = (device: MeshDevice) => } device.events.onFromRadio.dispatch(decodedMessage); + // device.log.info( + // Types.Emitter[Types.Emitter.HandleFromRadio], + // `📨 FromRadio: ${decodedMessage.payloadVariant.case}`, + // ); + /** @todo Add map here when `all=true` gets fixed. */ switch (decodedMessage.payloadVariant.case) { case "packet": { @@ -67,9 +73,9 @@ export const decodePacket = (device: MeshDevice) => } case "nodeInfo": { - device.log.info( + device.log.debug( Types.Emitter[Types.Emitter.HandleFromRadio], - `📱 Received Node Info packet for node: ${decodedMessage.payloadVariant.value.num}`, + `Received Node Info packet for node: ${decodedMessage.payloadVariant.value.num}`, ); device.events.onNodeInfoPacket.dispatch( @@ -114,7 +120,7 @@ export const decodePacket = (device: MeshDevice) => case "config": { if (decodedMessage.payloadVariant.value.payloadVariant.case) { - device.log.trace( + device.log.info( Types.Emitter[Types.Emitter.HandleFromRadio], `💾 Received Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`, ); @@ -148,13 +154,10 @@ export const decodePacket = (device: MeshDevice) => `⚙️ Received config complete id: ${decodedMessage.payloadVariant.value}`, ); - // Emit the configCompleteId event for MeshService to handle two-stage flow device.events.onConfigComplete.dispatch( decodedMessage.payloadVariant.value, ); - // For backward compatibility: if configId matches, update device status - // MeshService will override this behavior for two-stage flow if (decodedMessage.payloadVariant.value === device.configId) { device.log.info( Types.Emitter[Types.Emitter.HandleFromRadio], @@ -176,7 +179,7 @@ export const decodePacket = (device: MeshDevice) => case "moduleConfig": { if (decodedMessage.payloadVariant.value.payloadVariant.case) { - device.log.trace( + device.log.info( Types.Emitter[Types.Emitter.HandleFromRadio], `💾 Received Module Config packet of variant: ${decodedMessage.payloadVariant.value.payloadVariant.case}`, ); @@ -194,7 +197,7 @@ export const decodePacket = (device: MeshDevice) => } case "channel": { - device.log.trace( + device.log.info( Types.Emitter[Types.Emitter.HandleFromRadio], `🔐 Received Channel: ${decodedMessage.payloadVariant.value.index}`, ); diff --git a/packages/core/src/utils/transform/fromDevice.test.ts b/packages/core/src/utils/transform/fromDevice.test.ts new file mode 100644 index 000000000..2b7f243ca --- /dev/null +++ b/packages/core/src/utils/transform/fromDevice.test.ts @@ -0,0 +1,44 @@ + +import { describe, it, expect } from 'vitest'; +import { fromDeviceStream } from './fromDevice'; + +describe('fromDeviceStream', () => { + it('should recover from garbage data and broken headers', async () => { + const stream = fromDeviceStream(); + const writer = stream.writable.getWriter(); + const reader = stream.readable.getReader(); + + const received: any[] = []; + + // Start reading in background + (async () => { + while (true) { + const { value, done } = await reader.read(); + if (done) break; + received.push(value); + } + })(); + + // 1. Send garbage data (no 0x94) + await writer.write(new Uint8Array([0x01, 0x02, 0x03])); + + // 2. Send broken header (0x94 but not 0xc3) + await writer.write(new Uint8Array([0x94, 0x00, 0x04])); + + // 3. Send valid packet (0x94 0xc3 0x00 0x01 0xAA) (len 1) + // Header: 94 C3 00 01. Payload: AA. + await writer.write(new Uint8Array([0x94, 0xc3, 0x00, 0x01, 0xAA])); + + // Give it a moment to process + await new Promise(resolve => setTimeout(resolve, 100)); + + await writer.close(); + + // Expect at least one packet + const packet = received.find(r => r.type === 'packet'); + expect(packet).toBeDefined(); + expect(packet.data).toEqual(new Uint8Array([0xAA])); + + // Check debug data if possible, but mainly concerned about the packet. + }); +}); diff --git a/packages/core/src/utils/transform/fromDevice.ts b/packages/core/src/utils/transform/fromDevice.ts index a21cf6ccf..73b916318 100644 --- a/packages/core/src/utils/transform/fromDevice.ts +++ b/packages/core/src/utils/transform/fromDevice.ts @@ -1,23 +1,45 @@ import type { DeviceOutput } from "../../types.ts"; export const fromDeviceStream: () => TransformStream = - ( - // onReleaseEvent: SimpleEventDispatcher, - ) => { + () => { let byteBuffer = new Uint8Array([]); const textDecoder = new TextDecoder(); return new TransformStream({ transform(chunk: Uint8Array, controller): void { - // onReleaseEvent.subscribe(() => { - // controller.terminate(); - // }); byteBuffer = new Uint8Array([...byteBuffer, ...chunk]); let processingExhausted = false; while (byteBuffer.length !== 0 && !processingExhausted) { const framingIndex = byteBuffer.indexOf(0x94); + + // Case 1: No 0x94 found + if (framingIndex === -1) { + controller.enqueue({ + type: "debug", + data: textDecoder.decode(byteBuffer), + }); + byteBuffer = new Uint8Array([]); + processingExhausted = true; + continue; + } + + // Case 2: 0x94 found. Check if we have enough data for framingByte2 + if (framingIndex + 1 >= byteBuffer.length) { + // Dump preceeding as debug if any + if (framingIndex > 0) { + controller.enqueue({ + type: "debug", + data: textDecoder.decode(byteBuffer.subarray(0, framingIndex)), + }); + byteBuffer = byteBuffer.subarray(framingIndex); + } + // Buffer is just [0x94] + processingExhausted = true; + continue; + } + const framingByte2 = byteBuffer[framingIndex + 1]; if (framingByte2 === 0xc3) { - if (byteBuffer.subarray(0, framingIndex).length) { + if (framingIndex > 0) { controller.enqueue({ type: "debug", data: textDecoder.decode(byteBuffer.subarray(0, framingIndex)), @@ -25,30 +47,34 @@ export const fromDeviceStream: () => TransformStream = byteBuffer = byteBuffer.subarray(framingIndex); } + // 2. Check length bytes + if (byteBuffer.length < 4) { + processingExhausted = true; + continue; + } + const msb = byteBuffer[2]; const lsb = byteBuffer[3]; + const packetLen = (msb << 8) + lsb; - if ( - msb !== undefined && - lsb !== undefined && - byteBuffer.length >= 4 + (msb << 8) + lsb - ) { - const packet = byteBuffer.subarray(4, 4 + (msb << 8) + lsb); + if (byteBuffer.length >= 4 + packetLen) { + const packet = byteBuffer.subarray(4, 4 + packetLen); const malformedDetectorIndex = packet.indexOf(0x94); if ( malformedDetectorIndex !== -1 && + malformedDetectorIndex + 1 < packet.length && packet[malformedDetectorIndex + 1] === 0xc3 ) { console.warn( `⚠️ Malformed packet found, discarding: ${byteBuffer - .subarray(0, malformedDetectorIndex - 1) + .subarray(0, 4 + malformedDetectorIndex - 1) .toString()}`, ); - byteBuffer = byteBuffer.subarray(malformedDetectorIndex); + byteBuffer = byteBuffer.subarray(4 + malformedDetectorIndex); } else { - byteBuffer = byteBuffer.subarray(3 + (msb << 8) + lsb + 1); + byteBuffer = byteBuffer.subarray(4 + packetLen); controller.enqueue({ type: "packet", @@ -56,12 +82,16 @@ export const fromDeviceStream: () => TransformStream = }); } } else { - /** Only partioal message in buffer, wait for the rest */ processingExhausted = true; } } else { - /** Message not complete, only 1 byte in buffer */ - processingExhausted = true; + // 0x94 found but followed by !0xc3. + const discardLen = framingIndex + 1; + controller.enqueue({ + type: "debug", + data: textDecoder.decode(byteBuffer.subarray(0, discardLen)), + }); + byteBuffer = byteBuffer.subarray(discardLen); } } }, diff --git a/packages/packages/protobufs b/packages/packages/protobufs deleted file mode 160000 index a1b8c3d17..000000000 --- a/packages/packages/protobufs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a1b8c3d171445b2eebfd4b5bd1e4876f3bbed605 diff --git a/packages/protobufs/.gitattributes b/packages/protobufs/.gitattributes deleted file mode 100644 index 314766e91..000000000 --- a/packages/protobufs/.gitattributes +++ /dev/null @@ -1,3 +0,0 @@ -* text=auto eol=lf -*.{cmd,[cC][mM][dD]} text eol=crlf -*.{bat,[bB][aA][tT]} text eol=crlf diff --git a/packages/protobufs/.github/pull_request_template.md b/packages/protobufs/.github/pull_request_template.md deleted file mode 100644 index 8db5e3057..000000000 --- a/packages/protobufs/.github/pull_request_template.md +++ /dev/null @@ -1,30 +0,0 @@ - - -# What does this PR do? - - - -> [Related Issue](https://github.com/meshtastic/protobufs/issues/0) - -## Checklist before merging - -- [ ] All top level messages commented -- [ ] All enum members have unique descriptions - - -### New Hardware Model Acceptance Policy - -Due to limited availability and ongoing support, new Hardware Models will only be accepted from [Meshtastic Backers and Partners](https://meshtastic.com/). The Meshtastic team reserves the right to make exceptions to this policy. - -#### Alternative for Community Contributors - -You are welcome to use one of the existing DIY hardware models in your PlatformIO environment and create a pull request in the firmware project. Please note the following conditions: - -- The device will **not** be officially supported by the core Meshtastic team. -- The device will **not** appear in the [Web Flasher](https://flasher.meshtastic.org/) or Github release assets. -- You will be responsible for ongoing maintenance and support. -- Community-contributed / DIY hardware models are considered experimental and will likely have limited or no testing. - -#### Getting Official Support - -To have your hardware model officially supported and included in the Meshtastic ecosystem, consider becoming a Meshtastic Backer or Partner. Visit [meshtastic.com](https://meshtastic.com/) for more information about partnership opportunities. diff --git a/packages/protobufs/.github/workflows/ci.yml b/packages/protobufs/.github/workflows/ci.yml deleted file mode 100644 index f38ad9f16..000000000 --- a/packages/protobufs/.github/workflows/ci.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Push commit to schema registry - -permissions: - contents: read - -on: - push: - branches: - - master - -jobs: - push_to_registry: - name: Push to schema registry - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Push to schema registry - uses: bufbuild/buf-action@v1.2.0 - with: - github_token: ${{ github.token }} - token: ${{ secrets.BUF_TOKEN }} - push: true diff --git a/packages/protobufs/.github/workflows/create_tag.yml b/packages/protobufs/.github/workflows/create_tag.yml deleted file mode 100644 index a1e976848..000000000 --- a/packages/protobufs/.github/workflows/create_tag.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Create tag - -permissions: - contents: write - -on: - workflow_dispatch: - inputs: - increment_type: - type: choice - description: Select the type of version increment - required: true - options: - - patch - - minor - - major - -jobs: - increment_version: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - id: version - name: Get current version - run: | - VERSION=$(git describe --abbrev=0 --tags) - - # Split version into major, minor, and patch - MAJOR=$(echo $VERSION | awk -F '.' '{print $1}' | cut -c 2-) - MINOR=$(echo $VERSION | awk -F '.' '{print $2}') - PATCH=$(echo $VERSION | awk -F '.' '{print $3}') - - # Increment the appropriate part of the version - if [[ ${{ inputs.increment_type }} == "patch" ]]; then - PATCH=$((PATCH + 1)) - elif [[ ${{ inputs.increment_type }} == "minor" ]]; then - MINOR=$((MINOR + 1)) - PATCH=0 - elif [[ ${{ inputs.increment_type }} == "major" ]]; then - MAJOR=$((MAJOR + 1)) - MINOR=0 - PATCH=0 - fi - - # Update the version - echo "NEW_VERSION=v$MAJOR.$MINOR.$PATCH" >> $GITHUB_OUTPUT - - - name: Create release - uses: ncipollo/release-action@v1 - with: - name: Meshtastic Protobufs ${{ steps.version.outputs.NEW_VERSION }} - tag: ${{ steps.version.outputs.NEW_VERSION }} - generateReleaseNotes: true - token: ${{ github.token }} - - - name: Setup Buf - uses: bufbuild/buf-action@v1.2.0 - with: - github_token: ${{ github.token }} - token: ${{ secrets.BUF_TOKEN }} - setup_only: true - - - name: Push to schema registry - env: - BUF_TOKEN: ${{ secrets.BUF_TOKEN }} - run: | - buf push --tag ${{ steps.version.outputs.NEW_VERSION }} diff --git a/packages/protobufs/.github/workflows/publish.yml b/packages/protobufs/.github/workflows/publish.yml deleted file mode 100644 index 70c229ecf..000000000 --- a/packages/protobufs/.github/workflows/publish.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Push new version to schema registry - -permissions: - contents: read - -on: - push: - tags: - - "**" - -jobs: - push_to_registry: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Buf - uses: bufbuild/buf-action@v1.2.0 - with: - github_token: ${{ github.token }} - token: ${{ secrets.BUF_TOKEN }} - setup_only: true - - - name: Push to schema registry - env: - BUF_TOKEN: ${{ secrets.BUF_TOKEN }} - run: | - buf push --tag ${{ github.ref_name }} diff --git a/packages/protobufs/.github/workflows/pull_request.yml b/packages/protobufs/.github/workflows/pull_request.yml deleted file mode 100644 index 6df3b323d..000000000 --- a/packages/protobufs/.github/workflows/pull_request.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: pull-request - -permissions: - contents: read - pull-requests: write - -on: pull_request -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Buf PR Checks - uses: bufbuild/buf-action@v1.2.0 - with: - github_token: ${{ github.token }} - token: ${{ secrets.BUF_TOKEN }} - format: true - lint: true - breaking: true diff --git a/packages/protobufs/.gitignore b/packages/protobufs/.gitignore deleted file mode 100644 index e43b0f988..000000000 --- a/packages/protobufs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.DS_Store diff --git a/packages/protobufs/.gitmodules b/packages/protobufs/.gitmodules deleted file mode 100644 index 3654e8120..000000000 --- a/packages/protobufs/.gitmodules +++ /dev/null @@ -1,2 +0,0 @@ -[submodule "packages/protobufs"] - branch = master diff --git a/packages/protobufs/.vscode/extensions.json b/packages/protobufs/.vscode/extensions.json deleted file mode 100644 index 157b29198..000000000 --- a/packages/protobufs/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["pbkit.vscode-pbkit", "bufbuild.vscode-buf"] -} diff --git a/packages/protobufs/.vscode/settings.json b/packages/protobufs/.vscode/settings.json deleted file mode 100644 index 60415c3ec..000000000 --- a/packages/protobufs/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "editor.formatOnSave": true, - "editor.defaultFormatter": "pbkit.vscode-pbkit" -} diff --git a/packages/protobufs/README.md b/packages/protobufs/README.md deleted file mode 100644 index 9e3619778..000000000 --- a/packages/protobufs/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Meshtastic Protobuf Definitions - -[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/protobufs/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/protobufs/actions/workflows/ci.yml) -[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/protobufs)](https://cla-assistant.io/meshtastic/protobufs) -[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) -[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) - -## Overview - -The [Protobuf](https://developers.google.com/protocol-buffers) message definitions for the Meshtastic project (used by apps and the device firmware). - -**[Documentation/API Reference](https://buf.build/meshtastic/protobufs)** - -## Stats - -![Alt](https://repobeats.axiom.co/api/embed/47e9ee1d81d9c0fdd2b4b5b4c673adb1756f6db5.svg "Repobeats analytics image") diff --git a/packages/protobufs/buf.gen.yaml b/packages/protobufs/buf.gen.yaml deleted file mode 100644 index 1effc836b..000000000 --- a/packages/protobufs/buf.gen.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: v2 -plugins: - - remote: buf.build/bufbuild/es - out: packages/ts/dist - opt: target=ts diff --git a/packages/protobufs/buf.yaml b/packages/protobufs/buf.yaml deleted file mode 100644 index 6e15c142f..000000000 --- a/packages/protobufs/buf.yaml +++ /dev/null @@ -1,15 +0,0 @@ -version: v1 -name: buf.build/meshtastic/protobufs -deps: [] -build: - excludes: - - node_modules -breaking: - use: - - FILE -lint: - ignore_only: - PACKAGE_DEFINED: - - nanopb.proto - use: - - MINIMAL diff --git a/packages/protobufs/deno.json b/packages/protobufs/deno.json deleted file mode 100644 index b5f91df02..000000000 --- a/packages/protobufs/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@meshtastic/protobufs", - "version": "__PACKAGE_VERSION__", - "exports": { - ".": "./mod.ts" - }, - "imports": { - "@bufbuild/protobuf": "npm:@bufbuild/protobuf@^2.9.0" - }, - "publish": { - "exclude": ["!dist"] - } -} diff --git a/packages/protobufs/meshtastic/admin.options b/packages/protobufs/meshtastic/admin.options deleted file mode 100644 index 4a28ff2dd..000000000 --- a/packages/protobufs/meshtastic/admin.options +++ /dev/null @@ -1,19 +0,0 @@ -*AdminMessage.payload_variant anonymous_oneof:true - -*AdminMessage.session_passkey max_size:8 - -*AdminMessage.InputEvent.event_code int_size:8 -*AdminMessage.InputEvent.kb_char int_size:8 -*AdminMessage.InputEvent.touch_x int_size:16 -*AdminMessage.InputEvent.touch_y int_size:16 - -*AdminMessage.set_canned_message_module_messages max_size:201 -*AdminMessage.get_canned_message_module_messages_response max_size:201 -*AdminMessage.delete_file_request max_size:201 - -*AdminMessage.set_ringtone_message max_size:231 -*AdminMessage.get_ringtone_response max_size:231 - -*HamParameters.call_sign max_size:8 -*HamParameters.short_name max_size:5 -*NodeRemoteHardwarePinsResponse.node_remote_hardware_pins max_count:16 diff --git a/packages/protobufs/meshtastic/admin.proto b/packages/protobufs/meshtastic/admin.proto deleted file mode 100644 index 8dc1dc295..000000000 --- a/packages/protobufs/meshtastic/admin.proto +++ /dev/null @@ -1,582 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -import "meshtastic/channel.proto"; -import "meshtastic/config.proto"; -import "meshtastic/connection_status.proto"; -import "meshtastic/device_ui.proto"; -import "meshtastic/mesh.proto"; -import "meshtastic/module_config.proto"; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "AdminProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * This message is handled by the Admin module and is responsible for all settings/channel read/write operations. - * This message is used to do settings operations to both remote AND local nodes. - * (Prior to 1.2 these operations were done via special ToRadio operations) - */ -message AdminMessage { - /* - * The node generates this key and sends it with any get_x_response packets. - * The client MUST include the same key with any set_x commands. Key expires after 300 seconds. - * Prevents replay attacks for admin messages. - */ - bytes session_passkey = 101; - - /* - * TODO: REPLACE - */ - enum ConfigType { - /* - * TODO: REPLACE - */ - DEVICE_CONFIG = 0; - - /* - * TODO: REPLACE - */ - POSITION_CONFIG = 1; - - /* - * TODO: REPLACE - */ - POWER_CONFIG = 2; - - /* - * TODO: REPLACE - */ - NETWORK_CONFIG = 3; - - /* - * TODO: REPLACE - */ - DISPLAY_CONFIG = 4; - - /* - * TODO: REPLACE - */ - LORA_CONFIG = 5; - - /* - * TODO: REPLACE - */ - BLUETOOTH_CONFIG = 6; - - /* - * TODO: REPLACE - */ - SECURITY_CONFIG = 7; - - /* - * Session key config - */ - SESSIONKEY_CONFIG = 8; - - /* - * device-ui config - */ - DEVICEUI_CONFIG = 9; - } - - /* - * TODO: REPLACE - */ - enum ModuleConfigType { - /* - * TODO: REPLACE - */ - MQTT_CONFIG = 0; - - /* - * TODO: REPLACE - */ - SERIAL_CONFIG = 1; - - /* - * TODO: REPLACE - */ - EXTNOTIF_CONFIG = 2; - - /* - * TODO: REPLACE - */ - STOREFORWARD_CONFIG = 3; - - /* - * TODO: REPLACE - */ - RANGETEST_CONFIG = 4; - - /* - * TODO: REPLACE - */ - TELEMETRY_CONFIG = 5; - - /* - * TODO: REPLACE - */ - CANNEDMSG_CONFIG = 6; - - /* - * TODO: REPLACE - */ - AUDIO_CONFIG = 7; - - /* - * TODO: REPLACE - */ - REMOTEHARDWARE_CONFIG = 8; - - /* - * TODO: REPLACE - */ - NEIGHBORINFO_CONFIG = 9; - - /* - * TODO: REPLACE - */ - AMBIENTLIGHTING_CONFIG = 10; - - /* - * TODO: REPLACE - */ - DETECTIONSENSOR_CONFIG = 11; - - /* - * TODO: REPLACE - */ - PAXCOUNTER_CONFIG = 12; - } - - enum BackupLocation { - /* - * Backup to the internal flash - */ - FLASH = 0; - - /* - * Backup to the SD card - */ - SD = 1; - } - - /* - * Input event message to be sent to the node. - */ - message InputEvent { - /* - * The input event code - */ - uint32 event_code = 1; - /* - * Keyboard character code - */ - uint32 kb_char = 2; - /* - * The touch X coordinate - */ - uint32 touch_x = 3; - /* - * The touch Y coordinate - */ - uint32 touch_y = 4; - } - - /* - * TODO: REPLACE - */ - oneof payload_variant { - /* - * Send the specified channel in the response to this message - * NOTE: This field is sent with the channel index + 1 (to ensure we never try to send 'zero' - which protobufs treats as not present) - */ - uint32 get_channel_request = 1; - - /* - * TODO: REPLACE - */ - Channel get_channel_response = 2; - - /* - * Send the current owner data in the response to this message. - */ - bool get_owner_request = 3; - - /* - * TODO: REPLACE - */ - User get_owner_response = 4; - - /* - * Ask for the following config data to be sent - */ - ConfigType get_config_request = 5; - - /* - * Send the current Config in the response to this message. - */ - Config get_config_response = 6; - - /* - * Ask for the following config data to be sent - */ - ModuleConfigType get_module_config_request = 7; - - /* - * Send the current Config in the response to this message. - */ - ModuleConfig get_module_config_response = 8; - - /* - * Get the Canned Message Module messages in the response to this message. - */ - bool get_canned_message_module_messages_request = 10; - - /* - * Get the Canned Message Module messages in the response to this message. - */ - string get_canned_message_module_messages_response = 11; - - /* - * Request the node to send device metadata (firmware, protobuf version, etc) - */ - bool get_device_metadata_request = 12; - - /* - * Device metadata response - */ - DeviceMetadata get_device_metadata_response = 13; - - /* - * Get the Ringtone in the response to this message. - */ - bool get_ringtone_request = 14; - - /* - * Get the Ringtone in the response to this message. - */ - string get_ringtone_response = 15; - - /* - * Request the node to send it's connection status - */ - bool get_device_connection_status_request = 16; - - /* - * Device connection status response - */ - DeviceConnectionStatus get_device_connection_status_response = 17; - - /* - * Setup a node for licensed amateur (ham) radio operation - */ - HamParameters set_ham_mode = 18; - - /* - * Get the mesh's nodes with their available gpio pins for RemoteHardware module use - */ - bool get_node_remote_hardware_pins_request = 19; - - /* - * Respond with the mesh's nodes with their available gpio pins for RemoteHardware module use - */ - NodeRemoteHardwarePinsResponse get_node_remote_hardware_pins_response = 20; - - /* - * Enter (UF2) DFU mode - * Only implemented on NRF52 currently - */ - bool enter_dfu_mode_request = 21; - - /* - * Delete the file by the specified path from the device - */ - string delete_file_request = 22; - - /* - * Set zero and offset for scale chips - */ - uint32 set_scale = 23; - - /* - * Backup the node's preferences - */ - BackupLocation backup_preferences = 24; - - /* - * Restore the node's preferences - */ - BackupLocation restore_preferences = 25; - - /* - * Remove backups of the node's preferences - */ - BackupLocation remove_backup_preferences = 26; - - /* - * Send an input event to the node. - * This is used to trigger physical input events like button presses, touch events, etc. - */ - InputEvent send_input_event = 27; - - /* - * Set the owner for this node - */ - User set_owner = 32; - - /* - * Set channels (using the new API). - * A special channel is the "primary channel". - * The other records are secondary channels. - * Note: only one channel can be marked as primary. - * If the client sets a particular channel to be primary, the previous channel will be set to SECONDARY automatically. - */ - Channel set_channel = 33; - - /* - * Set the current Config - */ - Config set_config = 34; - - /* - * Set the current Config - */ - ModuleConfig set_module_config = 35; - - /* - * Set the Canned Message Module messages text. - */ - string set_canned_message_module_messages = 36; - - /* - * Set the ringtone for ExternalNotification. - */ - string set_ringtone_message = 37; - - /* - * Remove the node by the specified node-num from the NodeDB on the device - */ - uint32 remove_by_nodenum = 38; - - /* - * Set specified node-num to be favorited on the NodeDB on the device - */ - uint32 set_favorite_node = 39; - - /* - * Set specified node-num to be un-favorited on the NodeDB on the device - */ - uint32 remove_favorite_node = 40; - - /* - * Set fixed position data on the node and then set the position.fixed_position = true - */ - Position set_fixed_position = 41; - - /* - * Clear fixed position coordinates and then set position.fixed_position = false - */ - bool remove_fixed_position = 42; - - /* - * Set time only on the node - * Convenience method to set the time on the node (as Net quality) without any other position data - */ - fixed32 set_time_only = 43; - - /* - * Tell the node to send the stored ui data. - */ - bool get_ui_config_request = 44; - - /* - * Reply stored device ui data. - */ - DeviceUIConfig get_ui_config_response = 45; - - /* - * Tell the node to store UI data persistently. - */ - DeviceUIConfig store_ui_config = 46; - - /* - * Set specified node-num to be ignored on the NodeDB on the device - */ - uint32 set_ignored_node = 47; - - /* - * Set specified node-num to be un-ignored on the NodeDB on the device - */ - uint32 remove_ignored_node = 48; - - /* - * Begins an edit transaction for config, module config, owner, and channel settings changes - * This will delay the standard *implicit* save to the file system and subsequent reboot behavior until committed (commit_edit_settings) - */ - bool begin_edit_settings = 64; - - /* - * Commits an open transaction for any edits made to config, module config, owner, and channel settings - */ - bool commit_edit_settings = 65; - - /* - * Add a contact (User) to the nodedb - */ - SharedContact add_contact = 66; - - /* - * Initiate or respond to a key verification request - */ - KeyVerificationAdmin key_verification = 67; - - /* - * Tell the node to factory reset config everything; all device state and configuration will be returned to factory defaults and BLE bonds will be cleared. - */ - int32 factory_reset_device = 94; - - /* - * Tell the node to reboot into the OTA Firmware in this many seconds (or <0 to cancel reboot) - * Only Implemented for ESP32 Devices. This needs to be issued to send a new main firmware via bluetooth. - */ - int32 reboot_ota_seconds = 95; - - /* - * This message is only supported for the simulator Portduino build. - * If received the simulator will exit successfully. - */ - bool exit_simulator = 96; - - /* - * Tell the node to reboot in this many seconds (or <0 to cancel reboot) - */ - int32 reboot_seconds = 97; - - /* - * Tell the node to shutdown in this many seconds (or <0 to cancel shutdown) - */ - int32 shutdown_seconds = 98; - - /* - * Tell the node to factory reset config; all device state and configuration will be returned to factory defaults; BLE bonds will be preserved. - */ - int32 factory_reset_config = 99; - - /* - * Tell the node to reset the nodedb. - */ - int32 nodedb_reset = 100; - } -} - -/* - * Parameters for setting up Meshtastic for ameteur radio usage - */ -message HamParameters { - /* - * Amateur radio call sign, eg. KD2ABC - */ - string call_sign = 1; - - /* - * Transmit power in dBm at the LoRA transceiver, not including any amplification - */ - int32 tx_power = 2; - - /* - * The selected frequency of LoRA operation - * Please respect your local laws, regulations, and band plans. - * Ensure your radio is capable of operating of the selected frequency before setting this. - */ - float frequency = 3; - - /* - * Optional short name of user - */ - string short_name = 4; -} - -/* - * Response envelope for node_remote_hardware_pins - */ -message NodeRemoteHardwarePinsResponse { - /* - * Nodes and their respective remote hardware GPIO pins - */ - repeated NodeRemoteHardwarePin node_remote_hardware_pins = 1; -} - -message SharedContact { - /* - * The node number of the contact - */ - uint32 node_num = 1; - - /* - * The User of the contact - */ - User user = 2; - - /* - * Add this contact to the blocked / ignored list - */ - bool should_ignore = 3; - - /* - * Set the IS_KEY_MANUALLY_VERIFIED bit - */ - bool manually_verified = 4; -} - -/* - * This message is used by a client to initiate or complete a key verification - */ -message KeyVerificationAdmin { - /* - * Three stages of this request. - */ - enum MessageType { - /* - * This is the first stage, where a client initiates - */ - INITIATE_VERIFICATION = 0; - - /* - * After the nonce has been returned over the mesh, the client prompts for the security number - * And uses this message to provide it to the node. - */ - PROVIDE_SECURITY_NUMBER = 1; - - /* - * Once the user has compared the verification message, this message notifies the node. - */ - DO_VERIFY = 2; - - /* - * This is the cancel path, can be taken at any point - */ - DO_NOT_VERIFY = 3; - } - - MessageType message_type = 1; - - /* - * The nodenum we're requesting - */ - uint32 remote_nodenum = 2; - - /* - * The nonce is used to track the connection - */ - uint64 nonce = 3; - - /* - * The 4 digit code generated by the remote node, and communicated outside the mesh - */ - optional uint32 security_number = 4; -} diff --git a/packages/protobufs/meshtastic/apponly.options b/packages/protobufs/meshtastic/apponly.options deleted file mode 100644 index 28244de05..000000000 --- a/packages/protobufs/meshtastic/apponly.options +++ /dev/null @@ -1 +0,0 @@ -*ChannelSet.settings max_count:8 diff --git a/packages/protobufs/meshtastic/apponly.proto b/packages/protobufs/meshtastic/apponly.proto deleted file mode 100644 index 100833f43..000000000 --- a/packages/protobufs/meshtastic/apponly.proto +++ /dev/null @@ -1,31 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -import "meshtastic/channel.proto"; -import "meshtastic/config.proto"; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "AppOnlyProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * This is the most compact possible representation for a set of channels. - * It includes only one PRIMARY channel (which must be first) and - * any SECONDARY channels. - * No DISABLED channels are included. - * This abstraction is used only on the the 'app side' of the world (ie python, javascript and android etc) to show a group of Channels as a (long) URL - */ -message ChannelSet { - /* - * Channel list with settings - */ - repeated ChannelSettings settings = 1; - - /* - * LoRa config - */ - Config.LoRaConfig lora_config = 2; -} diff --git a/packages/protobufs/meshtastic/atak.options b/packages/protobufs/meshtastic/atak.options deleted file mode 100644 index cec1ca32b..000000000 --- a/packages/protobufs/meshtastic/atak.options +++ /dev/null @@ -1,8 +0,0 @@ -*Contact.callsign max_size:120 -*Contact.device_callsign max_size:120 -*Status.battery int_size:8 -*PLI.course int_size:16 -*GeoChat.message max_size:200 -*GeoChat.to max_size:120 -*GeoChat.to_callsign max_size:120 -*TAKPacket.detail max_size:220 \ No newline at end of file diff --git a/packages/protobufs/meshtastic/atak.proto b/packages/protobufs/meshtastic/atak.proto deleted file mode 100644 index 5dc08c9ad..000000000 --- a/packages/protobufs/meshtastic/atak.proto +++ /dev/null @@ -1,263 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "ATAKProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * Packets for the official ATAK Plugin - */ -message TAKPacket { - /* - * Are the payloads strings compressed for LoRA transport? - */ - bool is_compressed = 1; - /* - * The contact / callsign for ATAK user - */ - Contact contact = 2; - /* - * The group for ATAK user - */ - Group group = 3; - /* - * The status of the ATAK EUD - */ - Status status = 4; - /* - * The payload of the packet - */ - oneof payload_variant { - /* - * TAK position report - */ - PLI pli = 5; - /* - * ATAK GeoChat message - */ - GeoChat chat = 6; - - /* - * Generic CoT detail XML - * May be compressed / truncated by the sender (EUD) - */ - bytes detail = 7; - } -} - -/* - * ATAK GeoChat message - */ -message GeoChat { - /* - * The text message - */ - string message = 1; - - /* - * Uid recipient of the message - */ - optional string to = 2; - - /* - * Callsign of the recipient for the message - */ - optional string to_callsign = 3; -} - -/* - * ATAK Group - * <__group role='Team Member' name='Cyan'/> - */ -message Group { - /* - * Role of the group member - */ - MemberRole role = 1; - /* - * Team (color) - * Default Cyan - */ - Team team = 2; -} - -enum Team { - /* - * Unspecifed - */ - Unspecifed_Color = 0; - /* - * White - */ - White = 1; - /* - * Yellow - */ - Yellow = 2; - /* - * Orange - */ - Orange = 3; - /* - * Magenta - */ - Magenta = 4; - /* - * Red - */ - Red = 5; - /* - * Maroon - */ - Maroon = 6; - /* - * Purple - */ - Purple = 7; - /* - * Dark Blue - */ - Dark_Blue = 8; - /* - * Blue - */ - Blue = 9; - /* - * Cyan - */ - Cyan = 10; - /* - * Teal - */ - Teal = 11; - /* - * Green - */ - Green = 12; - /* - * Dark Green - */ - Dark_Green = 13; - /* - * Brown - */ - Brown = 14; -} - -/* - * Role of the group member - */ -enum MemberRole { - /* - * Unspecifed - */ - Unspecifed = 0; - /* - * Team Member - */ - TeamMember = 1; - /* - * Team Lead - */ - TeamLead = 2; - /* - * Headquarters - */ - HQ = 3; - /* - * Airsoft enthusiast - */ - Sniper = 4; - /* - * Medic - */ - Medic = 5; - /* - * ForwardObserver - */ - ForwardObserver = 6; - /* - * Radio Telephone Operator - */ - RTO = 7; - /* - * Doggo - */ - K9 = 8; -} - -/* - * ATAK EUD Status - * - */ -message Status { - /* - * Battery level - */ - uint32 battery = 1; -} - -/* - * ATAK Contact - * - */ -message Contact { - /* - * Callsign - */ - string callsign = 1; - - /* - * Device callsign - */ - string device_callsign = 2; - /* - * IP address of endpoint in integer form (0.0.0.0 default) - */ - // fixed32 enpoint_address = 3; - /* - * Port of endpoint (4242 default) - */ - // uint32 endpoint_port = 4; - /* - * Phone represented as integer - * Terrible practice, but we really need the wire savings - */ - // uint32 phone = 4; -} - -/* - * Position Location Information from ATAK - */ -message PLI { - /* - * The new preferred location encoding, multiply by 1e-7 to get degrees - * in floating point - */ - sfixed32 latitude_i = 1; - - /* - * The new preferred location encoding, multiply by 1e-7 to get degrees - * in floating point - */ - sfixed32 longitude_i = 2; - - /* - * Altitude (ATAK prefers HAE) - */ - int32 altitude = 3; - - /* - * Speed - */ - uint32 speed = 4; - - /* - * Course in degrees - */ - uint32 course = 5; -} diff --git a/packages/protobufs/meshtastic/cannedmessages.options b/packages/protobufs/meshtastic/cannedmessages.options deleted file mode 100644 index c1d537bba..000000000 --- a/packages/protobufs/meshtastic/cannedmessages.options +++ /dev/null @@ -1 +0,0 @@ -*CannedMessageModuleConfig.messages max_size:201 diff --git a/packages/protobufs/meshtastic/cannedmessages.proto b/packages/protobufs/meshtastic/cannedmessages.proto deleted file mode 100644 index baa513436..000000000 --- a/packages/protobufs/meshtastic/cannedmessages.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "CannedMessageConfigProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * Canned message module configuration. - */ -message CannedMessageModuleConfig { - /* - * Predefined messages for canned message module separated by '|' characters. - */ - string messages = 1; -} diff --git a/packages/protobufs/meshtastic/channel.options b/packages/protobufs/meshtastic/channel.options deleted file mode 100644 index d0bdcbc95..000000000 --- a/packages/protobufs/meshtastic/channel.options +++ /dev/null @@ -1,5 +0,0 @@ -*Channel.index int_size:8 - -# 256 bit or 128 bit psk key -*ChannelSettings.psk max_size:32 -*ChannelSettings.name max_size:12 diff --git a/packages/protobufs/meshtastic/channel.proto b/packages/protobufs/meshtastic/channel.proto deleted file mode 100644 index 4b7724215..000000000 --- a/packages/protobufs/meshtastic/channel.proto +++ /dev/null @@ -1,161 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "ChannelProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * This information can be encoded as a QRcode/url so that other users can configure - * their radio to join the same channel. - * A note about how channel names are shown to users: channelname-X - * poundsymbol is a prefix used to indicate this is a channel name (idea from @professr). - * Where X is a letter from A-Z (base 26) representing a hash of the PSK for this - * channel - so that if the user changes anything about the channel (which does - * force a new PSK) this letter will also change. Thus preventing user confusion if - * two friends try to type in a channel name of "BobsChan" and then can't talk - * because their PSKs will be different. - * The PSK is hashed into this letter by "0x41 + [xor all bytes of the psk ] modulo 26" - * This also allows the option of someday if people have the PSK off (zero), the - * users COULD type in a channel name and be able to talk. - * FIXME: Add description of multi-channel support and how primary vs secondary channels are used. - * FIXME: explain how apps use channels for security. - * explain how remote settings and remote gpio are managed as an example - */ -message ChannelSettings { - /* - * Deprecated in favor of LoraConfig.channel_num - */ - uint32 channel_num = 1 [deprecated = true]; - - /* - * A simple pre-shared key for now for crypto. - * Must be either 0 bytes (no crypto), 16 bytes (AES128), or 32 bytes (AES256). - * A special shorthand is used for 1 byte long psks. - * These psks should be treated as only minimally secure, - * because they are listed in this source code. - * Those bytes are mapped using the following scheme: - * `0` = No crypto - * `1` = The special "default" channel key: {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0x01} - * `2` through 10 = The default channel key, except with 1 through 9 added to the last byte. - * Shown to user as simple1 through 10 - */ - bytes psk = 2; - - /* - * A SHORT name that will be packed into the URL. - * Less than 12 bytes. - * Something for end users to call the channel - * If this is the empty string it is assumed that this channel - * is the special (minimally secure) "Default"channel. - * In user interfaces it should be rendered as a local language translation of "X". - * For channel_num hashing empty string will be treated as "X". - * Where "X" is selected based on the English words listed above for ModemPreset - */ - string name = 3; - - /* - * Used to construct a globally unique channel ID. - * The full globally unique ID will be: "name.id" where ID is shown as base36. - * Assuming that the number of meshtastic users is below 20K (true for a long time) - * the chance of this 64 bit random number colliding with anyone else is super low. - * And the penalty for collision is low as well, it just means that anyone trying to decrypt channel messages might need to - * try multiple candidate channels. - * Any time a non wire compatible change is made to a channel, this field should be regenerated. - * There are a small number of 'special' globally known (and fairly) insecure standard channels. - * Those channels do not have a numeric id included in the settings, but instead it is pulled from - * a table of well known IDs. - * (see Well Known Channels FIXME) - */ - fixed32 id = 4; - - /* - * If true, messages on the mesh will be sent to the *public* internet by any gateway ndoe - */ - bool uplink_enabled = 5; - - /* - * If true, messages seen on the internet will be forwarded to the local mesh. - */ - bool downlink_enabled = 6; - - /* - * Per-channel module settings. - */ - ModuleSettings module_settings = 7; - - /* - * Whether or not we should receive notifactions / alerts through this channel - */ - bool mute = 8; -} - -/* - * This message is specifically for modules to store per-channel configuration data. - */ -message ModuleSettings { - /* - * Bits of precision for the location sent in position packets. - */ - uint32 position_precision = 1; - - /* - * Controls whether or not the phone / clients should mute the current channel - * Useful for noisy public channels you don't necessarily want to disable - */ - bool is_client_muted = 2; -} - -/* - * A pair of a channel number, mode and the (sharable) settings for that channel - */ -message Channel { - /* - * How this channel is being used (or not). - * Note: this field is an enum to give us options for the future. - * In particular, someday we might make a 'SCANNING' option. - * SCANNING channels could have different frequencies and the radio would - * occasionally check that freq to see if anything is being transmitted. - * For devices that have multiple physical radios attached, we could keep multiple PRIMARY/SCANNING channels active at once to allow - * cross band routing as needed. - * If a device has only a single radio (the common case) only one channel can be PRIMARY at a time - * (but any number of SECONDARY channels can't be sent received on that common frequency) - */ - enum Role { - /* - * This channel is not in use right now - */ - DISABLED = 0; - - /* - * This channel is used to set the frequency for the radio - all other enabled channels must be SECONDARY - */ - PRIMARY = 1; - - /* - * Secondary channels are only used for encryption/decryption/authentication purposes. - * Their radio settings (freq etc) are ignored, only psk is used. - */ - SECONDARY = 2; - } - - /* - * The index of this channel in the channel table (from 0 to MAX_NUM_CHANNELS-1) - * (Someday - not currently implemented) An index of -1 could be used to mean "set by name", - * in which case the target node will find and set the channel by settings.name. - */ - int32 index = 1; - - /* - * The new settings, or NULL to disable that channel - */ - ChannelSettings settings = 2; - - /* - * TODO: REPLACE - */ - Role role = 3; -} diff --git a/packages/protobufs/meshtastic/clientonly.options b/packages/protobufs/meshtastic/clientonly.options deleted file mode 100644 index c47944a09..000000000 --- a/packages/protobufs/meshtastic/clientonly.options +++ /dev/null @@ -1,4 +0,0 @@ -*DeviceProfile.long_name max_size:40 -*DeviceProfile.short_name max_size:5 -*DeviceProfile.ringtone max_size:231 -*DeviceProfile.canned_messages max_size:201 \ No newline at end of file diff --git a/packages/protobufs/meshtastic/clientonly.proto b/packages/protobufs/meshtastic/clientonly.proto deleted file mode 100644 index 2b919ef7a..000000000 --- a/packages/protobufs/meshtastic/clientonly.proto +++ /dev/null @@ -1,58 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -import "meshtastic/localonly.proto"; -import "meshtastic/mesh.proto"; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "ClientOnlyProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * This abstraction is used to contain any configuration for provisioning a node on any client. - * It is useful for importing and exporting configurations. - */ -message DeviceProfile { - /* - * Long name for the node - */ - optional string long_name = 1; - - /* - * Short name of the node - */ - optional string short_name = 2; - - /* - * The url of the channels from our node - */ - optional string channel_url = 3; - - /* - * The Config of the node - */ - optional LocalConfig config = 4; - - /* - * The ModuleConfig of the node - */ - optional LocalModuleConfig module_config = 5; - - /* - * Fixed position data - */ - optional Position fixed_position = 6; - - /* - * Ringtone for ExternalNotification - */ - optional string ringtone = 7; - - /* - * Predefined messages for CannedMessage - */ - optional string canned_messages = 8; -} diff --git a/packages/protobufs/meshtastic/config.options b/packages/protobufs/meshtastic/config.options deleted file mode 100644 index 3f6d81c9c..000000000 --- a/packages/protobufs/meshtastic/config.options +++ /dev/null @@ -1,24 +0,0 @@ -# longest current is 45 chars, plan with a bit of buffer -*DeviceConfig.tzdef max_size:65 -*DeviceConfig.buzzer_mode int_size:8 - - -*NetworkConfig.wifi_ssid max_size:33 -*NetworkConfig.wifi_psk max_size:65 -*NetworkConfig.ntp_server max_size:33 -*NetworkConfig.rsyslog_server max_size:33 - -# Max of three ignored nodes for our testing -*LoRaConfig.ignore_incoming max_count:3 - -*LoRaConfig.tx_power int_size:8 -*LoRaConfig.bandwidth int_size:16 -*LoRaConfig.coding_rate int_size:8 -*LoRaConfig.channel_num int_size:16 - -*PowerConfig.device_battery_ina_address int_size:8 - -*SecurityConfig.public_key max_size:32 -*SecurityConfig.private_key max_size:32 -*SecurityConfig.admin_key max_size:32 -*SecurityConfig.admin_key max_count:3 diff --git a/packages/protobufs/meshtastic/config.proto b/packages/protobufs/meshtastic/config.proto deleted file mode 100644 index f17a07854..000000000 --- a/packages/protobufs/meshtastic/config.proto +++ /dev/null @@ -1,1200 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -import "meshtastic/device_ui.proto"; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "ConfigProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -message Config { - /* - * Configuration - */ - message DeviceConfig { - /* - * Defines the device's role on the Mesh network - */ - enum Role { - /* - * Description: App connected or stand alone messaging device. - * Technical Details: Default Role - */ - CLIENT = 0; - /* - * Description: Device that does not forward packets from other devices. - */ - CLIENT_MUTE = 1; - - /* - * Description: Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list. - * Technical Details: Mesh packets will prefer to be routed over this node. This node will not be used by client apps. - * The wifi radio and the oled screen will be put to sleep. - * This mode may still potentially have higher power usage due to it's preference in message rebroadcasting on the mesh. - */ - ROUTER = 2; - - /* - * Description: Combination of both ROUTER and CLIENT. Not for mobile devices. - * Deprecated in v2.3.15 because improper usage is impacting public meshes: Use ROUTER or CLIENT instead. - */ - - ROUTER_CLIENT = 3 [deprecated = true]; - - /* - * Description: Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list. - * Technical Details: Mesh packets will simply be rebroadcasted over this node. Nodes configured with this role will not originate NodeInfo, Position, Telemetry - * or any other packet type. They will simply rebroadcast any mesh packets on the same frequency, channel num, spread factor, and coding rate. - * Deprecated in v2.7.11 because it creates "holes" in the mesh rebroadcast chain. - */ - REPEATER = 4 [deprecated = true]; - - /* - * Description: Broadcasts GPS position packets as priority. - * Technical Details: Position Mesh packets will be prioritized higher and sent more frequently by default. - * When used in conjunction with power.is_power_saving = true, nodes will wake up, - * send position, and then sleep for position.position_broadcast_secs seconds. - */ - TRACKER = 5; - - /* - * Description: Broadcasts telemetry packets as priority. - * Technical Details: Telemetry Mesh packets will be prioritized higher and sent more frequently by default. - * When used in conjunction with power.is_power_saving = true, nodes will wake up, - * send environment telemetry, and then sleep for telemetry.environment_update_interval seconds. - */ - SENSOR = 6; - - /* - * Description: Optimized for ATAK system communication and reduces routine broadcasts. - * Technical Details: Used for nodes dedicated for connection to an ATAK EUD. - * Turns off many of the routine broadcasts to favor CoT packet stream - * from the Meshtastic ATAK plugin -> IMeshService -> Node - */ - TAK = 7; - - /* - * Description: Device that only broadcasts as needed for stealth or power savings. - * Technical Details: Used for nodes that "only speak when spoken to" - * Turns all of the routine broadcasts but allows for ad-hoc communication - * Still rebroadcasts, but with local only rebroadcast mode (known meshes only) - * Can be used for clandestine operation or to dramatically reduce airtime / power consumption - */ - CLIENT_HIDDEN = 8; - - /* - * Description: Broadcasts location as message to default channel regularly for to assist with device recovery. - * Technical Details: Used to automatically send a text message to the mesh - * with the current position of the device on a frequent interval: - * "I'm lost! Position: lat / long" - */ - LOST_AND_FOUND = 9; - - /* - * Description: Enables automatic TAK PLI broadcasts and reduces routine broadcasts. - * Technical Details: Turns off many of the routine broadcasts to favor ATAK CoT packet stream - * and automatic TAK PLI (position location information) broadcasts. - * Uses position module configuration to determine TAK PLI broadcast interval. - */ - TAK_TRACKER = 10; - - /* - * Description: Will always rebroadcast packets, but will do so after all other modes. - * Technical Details: Used for router nodes that are intended to provide additional coverage - * in areas not already covered by other routers, or to bridge around problematic terrain, - * but should not be given priority over other routers in order to avoid unnecessaraily - * consuming hops. - */ - ROUTER_LATE = 11; - - /* - * Description: Treats packets from or to favorited nodes as ROUTER, and all other packets as CLIENT. - * Technical Details: Used for stronger attic/roof nodes to distribute messages more widely - * from weaker, indoor, or less-well-positioned nodes. Recommended for users with multiple nodes - * where one CLIENT_BASE acts as a more powerful base station, such as an attic/roof node. - */ - CLIENT_BASE = 12; - } - - /* - * Defines the device's behavior for how messages are rebroadcast - */ - enum RebroadcastMode { - /* - * Default behavior. - * Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params. - */ - ALL = 0; - - /* - * Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. - * Only available in Repeater role. Setting this on any other roles will result in ALL behavior. - */ - ALL_SKIP_DECODING = 1; - - /* - * Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. - * Only rebroadcasts message on the nodes local primary / secondary channels. - */ - LOCAL_ONLY = 2; - - /* - * Ignores observed messages from foreign meshes like LOCAL_ONLY, - * but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB) - */ - KNOWN_ONLY = 3; - - /* - * Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role. - */ - NONE = 4; - - /* - * Ignores packets from non-standard portnums such as: TAK, RangeTest, PaxCounter, etc. - * Only rebroadcasts packets with standard portnums: NodeInfo, Text, Position, Telemetry, and Routing. - */ - CORE_PORTNUMS_ONLY = 5; - } - - /* - * Defines buzzer behavior for audio feedback - */ - enum BuzzerMode { - /* - * Default behavior. - * Buzzer is enabled for all audio feedback including button presses and alerts. - */ - ALL_ENABLED = 0; - - /* - * Disabled. - * All buzzer audio feedback is disabled. - */ - DISABLED = 1; - - /* - * Notifications Only. - * Buzzer is enabled only for notifications and alerts, but not for button presses. - * External notification config determines the specifics of the notification behavior. - */ - NOTIFICATIONS_ONLY = 2; - - /* - * Non-notification system buzzer tones only. - * Buzzer is enabled only for non-notification tones such as button presses, startup, shutdown, but not for alerts. - */ - SYSTEM_ONLY = 3; - - /* - * Direct Message notifications only. - * Buzzer is enabled only for direct messages and alerts, but not for button presses. - * External notification config determines the specifics of the notification behavior. - */ - DIRECT_MSG_ONLY = 4; - } - - /* - * Sets the role of node - */ - Role role = 1; - - /* - * Disabling this will disable the SerialConsole by not initilizing the StreamAPI - * Moved to SecurityConfig - */ - bool serial_enabled = 2 [deprecated = true]; - - /* - * For boards without a hard wired button, this is the pin number that will be used - * Boards that have more than one button can swap the function with this one. defaults to BUTTON_PIN if defined. - */ - uint32 button_gpio = 4; - - /* - * For boards without a PWM buzzer, this is the pin number that will be used - * Defaults to PIN_BUZZER if defined. - */ - uint32 buzzer_gpio = 5; - - /* - * Sets the role of node - */ - RebroadcastMode rebroadcast_mode = 6; - - /* - * Send our nodeinfo this often - * Defaults to 900 Seconds (15 minutes) - */ - uint32 node_info_broadcast_secs = 7; - - /* - * Treat double tap interrupt on supported accelerometers as a button press if set to true - */ - bool double_tap_as_button_press = 8; - - /* - * If true, device is considered to be "managed" by a mesh administrator - * Clients should then limit available configuration and administrative options inside the user interface - * Moved to SecurityConfig - */ - bool is_managed = 9 [deprecated = true]; - - /* - * Disables the triple-press of user button to enable or disable GPS - */ - bool disable_triple_click = 10; - - /* - * POSIX Timezone definition string from https://github.com/nayarsystems/posix_tz_db/blob/master/zones.csv. - */ - string tzdef = 11; - - /* - * If true, disable the default blinking LED (LED_PIN) behavior on the device - */ - bool led_heartbeat_disabled = 12; - - /* - * Controls buzzer behavior for audio feedback - * Defaults to ENABLED - */ - BuzzerMode buzzer_mode = 13; - } - - /* - * Position Config - */ - message PositionConfig { - /* - * Bit field of boolean configuration options, indicating which optional - * fields to include when assembling POSITION messages. - * Longitude, latitude, altitude, speed, heading, and DOP - * are always included (also time if GPS-synced) - * NOTE: the more fields are included, the larger the message will be - - * leading to longer airtime and a higher risk of packet loss - */ - enum PositionFlags { - /* - * Required for compilation - */ - UNSET = 0x0000; - - /* - * Include an altitude value (if available) - */ - ALTITUDE = 0x0001; - - /* - * Altitude value is MSL - */ - ALTITUDE_MSL = 0x0002; - - /* - * Include geoidal separation - */ - GEOIDAL_SEPARATION = 0x0004; - - /* - * Include the DOP value ; PDOP used by default, see below - */ - DOP = 0x0008; - - /* - * If POS_DOP set, send separate HDOP / VDOP values instead of PDOP - */ - HVDOP = 0x0010; - - /* - * Include number of "satellites in view" - */ - SATINVIEW = 0x0020; - - /* - * Include a sequence number incremented per packet - */ - SEQ_NO = 0x0040; - - /* - * Include positional timestamp (from GPS solution) - */ - TIMESTAMP = 0x0080; - - /* - * Include positional heading - * Intended for use with vehicle not walking speeds - * walking speeds are likely to be error prone like the compass - */ - HEADING = 0x0100; - - /* - * Include positional speed - * Intended for use with vehicle not walking speeds - * walking speeds are likely to be error prone like the compass - */ - SPEED = 0x0200; - } - - enum GpsMode { - /* - * GPS is present but disabled - */ - DISABLED = 0; - - /* - * GPS is present and enabled - */ - ENABLED = 1; - - /* - * GPS is not present on the device - */ - NOT_PRESENT = 2; - } - - /* - * We should send our position this often (but only if it has changed significantly) - * Defaults to 15 minutes - */ - uint32 position_broadcast_secs = 1; - - /* - * Adaptive position braoadcast, which is now the default. - */ - bool position_broadcast_smart_enabled = 2; - - /* - * If set, this node is at a fixed position. - * We will generate GPS position updates at the regular interval, but use whatever the last lat/lon/alt we have for the node. - * The lat/lon/alt can be set by an internal GPS or with the help of the app. - */ - bool fixed_position = 3; - - /* - * Is GPS enabled for this node? - */ - bool gps_enabled = 4 [deprecated = true]; - - /* - * How often should we try to get GPS position (in seconds) - * or zero for the default of once every 30 seconds - * or a very large value (maxint) to update only once at boot. - */ - uint32 gps_update_interval = 5; - - /* - * Deprecated in favor of using smart / regular broadcast intervals as implicit attempt time - */ - uint32 gps_attempt_time = 6 [deprecated = true]; - - /* - * Bit field of boolean configuration options for POSITION messages - * (bitwise OR of PositionFlags) - */ - uint32 position_flags = 7; - - /* - * (Re)define GPS_RX_PIN for your board. - */ - uint32 rx_gpio = 8; - - /* - * (Re)define GPS_TX_PIN for your board. - */ - uint32 tx_gpio = 9; - - /* - * The minimum distance in meters traveled (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled - */ - uint32 broadcast_smart_minimum_distance = 10; - - /* - * The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled - */ - uint32 broadcast_smart_minimum_interval_secs = 11; - - /* - * (Re)define PIN_GPS_EN for your board. - */ - uint32 gps_en_gpio = 12; - - /* - * Set where GPS is enabled, disabled, or not present - */ - GpsMode gps_mode = 13; - } - - /* - * Power Config\ - * See [Power Config](/docs/settings/config/power) for additional power config details. - */ - message PowerConfig { - /* - * Description: Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. - * Don't use this setting if you want to use your device with the phone apps or are using a device without a user button. - * Technical Details: Works for ESP32 devices and NRF52 devices in the Sensor or Tracker roles - */ - bool is_power_saving = 1; - - /* - * Description: If non-zero, the device will fully power off this many seconds after external power is removed. - */ - uint32 on_battery_shutdown_after_secs = 2; - - /* - * Ratio of voltage divider for battery pin eg. 3.20 (R1=100k, R2=220k) - * Overrides the ADC_MULTIPLIER defined in variant for battery voltage calculation. - * https://meshtastic.org/docs/configuration/radio/power/#adc-multiplier-override - * Should be set to floating point value between 2 and 6 - */ - float adc_multiplier_override = 3; - - /* - * Description: The number of seconds for to wait before turning off BLE in No Bluetooth states - * Technical Details: ESP32 Only 0 for default of 1 minute - */ - uint32 wait_bluetooth_secs = 4; - - /* - * Super Deep Sleep Seconds - * While in Light Sleep if mesh_sds_timeout_secs is exceeded we will lower into super deep sleep - * for this value (default 1 year) or a button press - * 0 for default of one year - */ - uint32 sds_secs = 6; - - /* - * Description: In light sleep the CPU is suspended, LoRa radio is on, BLE is off an GPS is on - * Technical Details: ESP32 Only 0 for default of 300 - */ - uint32 ls_secs = 7; - - /* - * Description: While in light sleep when we receive packets on the LoRa radio we will wake and handle them and stay awake in no BLE mode for this value - * Technical Details: ESP32 Only 0 for default of 10 seconds - */ - uint32 min_wake_secs = 8; - - /* - * I2C address of INA_2XX to use for reading device battery voltage - */ - uint32 device_battery_ina_address = 9; - - /* - * If non-zero, we want powermon log outputs. With the particular (bitfield) sources enabled. - * Note: we picked an ID of 32 so that lower more efficient IDs can be used for more frequently used options. - */ - uint64 powermon_enables = 32; - } - - /* - * Network Config - */ - message NetworkConfig { - enum AddressMode { - /* - * obtain ip address via DHCP - */ - DHCP = 0; - - /* - * use static ip address - */ - STATIC = 1; - } - - message IpV4Config { - /* - * Static IP address - */ - fixed32 ip = 1; - - /* - * Static gateway address - */ - fixed32 gateway = 2; - - /* - * Static subnet mask - */ - fixed32 subnet = 3; - - /* - * Static DNS server address - */ - fixed32 dns = 4; - } - - /* - * Enable WiFi (disables Bluetooth) - */ - bool wifi_enabled = 1; - - /* - * If set, this node will try to join the specified wifi network and - * acquire an address via DHCP - */ - string wifi_ssid = 3; - - /* - * If set, will be use to authenticate to the named wifi - */ - string wifi_psk = 4; - - /* - * NTP server to use if WiFi is conneced, defaults to `meshtastic.pool.ntp.org` - */ - string ntp_server = 5; - - /* - * Enable Ethernet - */ - bool eth_enabled = 6; - - /* - * acquire an address via DHCP or assign static - */ - AddressMode address_mode = 7; - - /* - * struct to keep static address - */ - IpV4Config ipv4_config = 8; - - /* - * rsyslog Server and Port - */ - string rsyslog_server = 9; - - /* - * Flags for enabling/disabling network protocols - */ - uint32 enabled_protocols = 10; - - /* - * Enable/Disable ipv6 support - */ - bool ipv6_enabled = 11; - - /* - * Available flags auxiliary network protocols - */ - enum ProtocolFlags { - /* - * Do not broadcast packets over any network protocol - */ - NO_BROADCAST = 0x0000; - - /* - * Enable broadcasting packets via UDP over the local network - */ - UDP_BROADCAST = 0x0001; - } - } - - /* - * Display Config - */ - message DisplayConfig { - /* - * Deprecated in 2.7.4: Unused - */ - enum DeprecatedGpsCoordinateFormat { - UNUSED = 0; - } - - /* - * Unit display preference - */ - enum DisplayUnits { - /* - * Metric (Default) - */ - METRIC = 0; - - /* - * Imperial - */ - IMPERIAL = 1; - } - - /* - * Override OLED outo detect with this if it fails. - */ - enum OledType { - /* - * Default / Autodetect - */ - OLED_AUTO = 0; - - /* - * Default / Autodetect - */ - OLED_SSD1306 = 1; - - /* - * Default / Autodetect - */ - OLED_SH1106 = 2; - - /* - * Can not be auto detected but set by proto. Used for 128x64 screens - */ - OLED_SH1107 = 3; - - /* - * Can not be auto detected but set by proto. Used for 128x128 screens - */ - OLED_SH1107_128_128 = 4; - } - - /* - * Number of seconds the screen stays on after pressing the user button or receiving a message - * 0 for default of one minute MAXUINT for always on - */ - uint32 screen_on_secs = 1; - - /* - * Deprecated in 2.7.4: Unused - * How the GPS coordinates are formatted on the OLED screen. - */ - DeprecatedGpsCoordinateFormat gps_format = 2 [deprecated = true]; - - /* - * Automatically toggles to the next page on the screen like a carousel, based the specified interval in seconds. - * Potentially useful for devices without user buttons. - */ - uint32 auto_screen_carousel_secs = 3; - - /* - * If this is set, the displayed compass will always point north. if unset, the old behaviour - * (top of display is heading direction) is used. - */ - bool compass_north_top = 4 [deprecated = true]; - - /* - * Flip screen vertically, for cases that mount the screen upside down - */ - bool flip_screen = 5; - - /* - * Perferred display units - */ - DisplayUnits units = 6; - - /* - * Override auto-detect in screen - */ - OledType oled = 7; - - enum DisplayMode { - /* - * Default. The old style for the 128x64 OLED screen - */ - DEFAULT = 0; - - /* - * Rearrange display elements to cater for bicolor OLED displays - */ - TWOCOLOR = 1; - - /* - * Same as TwoColor, but with inverted top bar. Not so good for Epaper displays - */ - INVERTED = 2; - - /* - * TFT Full Color Displays (not implemented yet) - */ - COLOR = 3; - } - /* - * Display Mode - */ - DisplayMode displaymode = 8; - - /* - * Print first line in pseudo-bold? FALSE is original style, TRUE is bold - */ - bool heading_bold = 9; - - /* - * Should we wake the screen up on accelerometer detected motion or tap - */ - bool wake_on_tap_or_motion = 10; - - enum CompassOrientation { - /* - * The compass and the display are in the same orientation. - */ - DEGREES_0 = 0; - - /* - * Rotate the compass by 90 degrees. - */ - DEGREES_90 = 1; - - /* - * Rotate the compass by 180 degrees. - */ - DEGREES_180 = 2; - - /* - * Rotate the compass by 270 degrees. - */ - DEGREES_270 = 3; - - /* - * Don't rotate the compass, but invert the result. - */ - DEGREES_0_INVERTED = 4; - - /* - * Rotate the compass by 90 degrees and invert. - */ - DEGREES_90_INVERTED = 5; - - /* - * Rotate the compass by 180 degrees and invert. - */ - DEGREES_180_INVERTED = 6; - - /* - * Rotate the compass by 270 degrees and invert. - */ - DEGREES_270_INVERTED = 7; - } - - /* - * Indicates how to rotate or invert the compass output to accurate display on the display. - */ - CompassOrientation compass_orientation = 11; - - /* - * If false (default), the device will display the time in 24-hour format on screen. - * If true, the device will display the time in 12-hour format on screen. - */ - bool use_12h_clock = 12; - - /* - * If false (default), the device will use short names for various display screens. - * If true, node names will show in long format - */ - bool use_long_node_name = 13; - } - - /* - * Lora Config - */ - message LoRaConfig { - enum RegionCode { - /* - * Region is not set - */ - UNSET = 0; - - /* - * United States - */ - US = 1; - - /* - * European Union 433mhz - */ - EU_433 = 2; - - /* - * European Union 868mhz - */ - EU_868 = 3; - - /* - * China - */ - CN = 4; - - /* - * Japan - */ - JP = 5; - - /* - * Australia / New Zealand - */ - ANZ = 6; - - /* - * Korea - */ - KR = 7; - - /* - * Taiwan - */ - TW = 8; - - /* - * Russia - */ - RU = 9; - - /* - * India - */ - IN = 10; - - /* - * New Zealand 865mhz - */ - NZ_865 = 11; - - /* - * Thailand - */ - TH = 12; - - /* - * WLAN Band - */ - LORA_24 = 13; - - /* - * Ukraine 433mhz - */ - UA_433 = 14; - - /* - * Ukraine 868mhz - */ - UA_868 = 15; - - /* - * Malaysia 433mhz - */ - MY_433 = 16; - - /* - * Malaysia 919mhz - */ - MY_919 = 17; - - /* - * Singapore 923mhz - */ - SG_923 = 18; - - /* - * Philippines 433mhz - */ - PH_433 = 19; - - /* - * Philippines 868mhz - */ - PH_868 = 20; - - /* - * Philippines 915mhz - */ - PH_915 = 21; - - /* - * Australia / New Zealand 433MHz - */ - ANZ_433 = 22; - - /* - * Kazakhstan 433MHz - */ - KZ_433 = 23; - - /* - * Kazakhstan 863MHz - */ - KZ_863 = 24; - - /* - * Nepal 865MHz - */ - NP_865 = 25; - - /* - * Brazil 902MHz - */ - BR_902 = 26; - } - - /* - * Standard predefined channel settings - * Note: these mappings must match ModemPreset Choice in the device code. - */ - enum ModemPreset { - /* - * Long Range - Fast - */ - LONG_FAST = 0; - - /* - * Long Range - Slow - */ - LONG_SLOW = 1; - - /* - * Very Long Range - Slow - * Deprecated in 2.5: Works only with txco and is unusably slow - */ - VERY_LONG_SLOW = 2 [deprecated = true]; - - /* - * Medium Range - Slow - */ - MEDIUM_SLOW = 3; - - /* - * Medium Range - Fast - */ - MEDIUM_FAST = 4; - - /* - * Short Range - Slow - */ - SHORT_SLOW = 5; - - /* - * Short Range - Fast - */ - SHORT_FAST = 6; - - /* - * Long Range - Moderately Fast - */ - LONG_MODERATE = 7; - - /* - * Short Range - Turbo - * This is the fastest preset and the only one with 500kHz bandwidth. - * It is not legal to use in all regions due to this wider bandwidth. - */ - SHORT_TURBO = 8; - } - - /* - * When enabled, the `modem_preset` fields will be adhered to, else the `bandwidth`/`spread_factor`/`coding_rate` - * will be taked from their respective manually defined fields - */ - bool use_preset = 1; - - /* - * Either modem_config or bandwidth/spreading/coding will be specified - NOT BOTH. - * As a heuristic: If bandwidth is specified, do not use modem_config. - * Because protobufs take ZERO space when the value is zero this works out nicely. - * This value is replaced by bandwidth/spread_factor/coding_rate. - * If you'd like to experiment with other options add them to MeshRadio.cpp in the device code. - */ - ModemPreset modem_preset = 2; - - /* - * Bandwidth in MHz - * Certain bandwidth numbers are 'special' and will be converted to the - * appropriate floating point value: 31 -> 31.25MHz - */ - uint32 bandwidth = 3; - - /* - * A number from 7 to 12. - * Indicates number of chirps per symbol as 1< 7 results in the default - */ - uint32 hop_limit = 8; - - /* - * Disable TX from the LoRa radio. Useful for hot-swapping antennas and other tests. - * Defaults to false - */ - bool tx_enabled = 9; - - /* - * If zero, then use default max legal continuous power (ie. something that won't - * burn out the radio hardware) - * In most cases you should use zero here. - * Units are in dBm. - */ - int32 tx_power = 10; - - /* - * This controls the actual hardware frequency the radio transmits on. - * Most users should never need to be exposed to this field/concept. - * A channel number between 1 and NUM_CHANNELS (whatever the max is in the current region). - * If ZERO then the rule is "use the old channel name hash based - * algorithm to derive the channel number") - * If using the hash algorithm the channel number will be: hash(channel_name) % - * NUM_CHANNELS (Where num channels depends on the regulatory region). - */ - uint32 channel_num = 11; - - /* - * If true, duty cycle limits will be exceeded and thus you're possibly not following - * the local regulations if you're not a HAM. - * Has no effect if the duty cycle of the used region is 100%. - */ - bool override_duty_cycle = 12; - - /* - * If true, sets RX boosted gain mode on SX126X based radios - */ - bool sx126x_rx_boosted_gain = 13; - - /* - * This parameter is for advanced users and licensed HAM radio operators. - * Ignore Channel Calculation and use this frequency instead. The frequency_offset - * will still be applied. This will allow you to use out-of-band frequencies. - * Please respect your local laws and regulations. If you are a HAM, make sure you - * enable HAM mode and turn off encryption. - */ - float override_frequency = 14; - - /* - * If true, disable the build-in PA FAN using pin define in RF95_FAN_EN. - */ - bool pa_fan_disabled = 15; - - /* - * For testing it is useful sometimes to force a node to never listen to - * particular other nodes (simulating radio out of range). All nodenums listed - * in ignore_incoming will have packets they send dropped on receive (by router.cpp) - */ - repeated uint32 ignore_incoming = 103; - - /* - * If true, the device will not process any packets received via LoRa that passed via MQTT anywhere on the path towards it. - */ - bool ignore_mqtt = 104; - - /* - * Sets the ok_to_mqtt bit on outgoing packets - */ - bool config_ok_to_mqtt = 105; - } - - message BluetoothConfig { - enum PairingMode { - /* - * Device generates a random PIN that will be shown on the screen of the device for pairing - */ - RANDOM_PIN = 0; - - /* - * Device requires a specified fixed PIN for pairing - */ - FIXED_PIN = 1; - - /* - * Device requires no PIN for pairing - */ - NO_PIN = 2; - } - - /* - * Enable Bluetooth on the device - */ - bool enabled = 1; - - /* - * Determines the pairing strategy for the device - */ - PairingMode mode = 2; - - /* - * Specified PIN for PairingMode.FixedPin - */ - uint32 fixed_pin = 3; - } - - message SecurityConfig { - /* - * The public key of the user's device. - * Sent out to other nodes on the mesh to allow them to compute a shared secret key. - */ - bytes public_key = 1; - - /* - * The private key of the device. - * Used to create a shared key with a remote device. - */ - bytes private_key = 2; - - /* - * The public key authorized to send admin messages to this node. - */ - repeated bytes admin_key = 3; - - /* - * If true, device is considered to be "managed" by a mesh administrator via admin messages - * Device is managed by a mesh administrator. - */ - bool is_managed = 4; - - /* - * Serial Console over the Stream API." - */ - bool serial_enabled = 5; - - /* - * By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). - * Output live debug logging over serial or bluetooth is set to true. - */ - bool debug_log_api_enabled = 6; - - /* - * Allow incoming device control over the insecure legacy admin channel. - */ - bool admin_channel_enabled = 8; - } - - /* - * Blank config request, strictly for getting the session key - */ - message SessionkeyConfig {} - - /* - * Payload Variant - */ - oneof payload_variant { - DeviceConfig device = 1; - PositionConfig position = 2; - PowerConfig power = 3; - NetworkConfig network = 4; - DisplayConfig display = 5; - LoRaConfig lora = 6; - BluetoothConfig bluetooth = 7; - SecurityConfig security = 8; - SessionkeyConfig sessionkey = 9; - DeviceUIConfig device_ui = 10; - } -} diff --git a/packages/protobufs/meshtastic/connection_status.options b/packages/protobufs/meshtastic/connection_status.options deleted file mode 100644 index d4901ddc8..000000000 --- a/packages/protobufs/meshtastic/connection_status.options +++ /dev/null @@ -1 +0,0 @@ -*WifiConnectionStatus.ssid max_size:33 diff --git a/packages/protobufs/meshtastic/connection_status.proto b/packages/protobufs/meshtastic/connection_status.proto deleted file mode 100644 index 75515965c..000000000 --- a/packages/protobufs/meshtastic/connection_status.proto +++ /dev/null @@ -1,120 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "ConnStatusProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -message DeviceConnectionStatus { - /* - * WiFi Status - */ - optional WifiConnectionStatus wifi = 1; - /* - * WiFi Status - */ - optional EthernetConnectionStatus ethernet = 2; - - /* - * Bluetooth Status - */ - optional BluetoothConnectionStatus bluetooth = 3; - - /* - * Serial Status - */ - optional SerialConnectionStatus serial = 4; -} - -/* - * WiFi connection status - */ -message WifiConnectionStatus { - /* - * Connection status - */ - NetworkConnectionStatus status = 1; - - /* - * WiFi access point SSID - */ - string ssid = 2; - - /* - * RSSI of wireless connection - */ - int32 rssi = 3; -} - -/* - * Ethernet connection status - */ -message EthernetConnectionStatus { - /* - * Connection status - */ - NetworkConnectionStatus status = 1; -} - -/* - * Ethernet or WiFi connection status - */ -message NetworkConnectionStatus { - /* - * IP address of device - */ - fixed32 ip_address = 1; - - /* - * Whether the device has an active connection or not - */ - bool is_connected = 2; - - /* - * Whether the device has an active connection to an MQTT broker or not - */ - bool is_mqtt_connected = 3; - - /* - * Whether the device is actively remote syslogging or not - */ - bool is_syslog_connected = 4; -} - -/* - * Bluetooth connection status - */ -message BluetoothConnectionStatus { - /* - * The pairing PIN for bluetooth - */ - uint32 pin = 1; - - /* - * RSSI of bluetooth connection - */ - int32 rssi = 2; - - /* - * Whether the device has an active connection or not - */ - bool is_connected = 3; -} - -/* - * Serial connection status - */ -message SerialConnectionStatus { - /* - * Serial baud rate - */ - uint32 baud = 1; - - /* - * Whether the device has an active connection or not - */ - bool is_connected = 2; -} diff --git a/packages/protobufs/meshtastic/device_ui.options b/packages/protobufs/meshtastic/device_ui.options deleted file mode 100644 index a8fab4662..000000000 --- a/packages/protobufs/meshtastic/device_ui.options +++ /dev/null @@ -1,12 +0,0 @@ -*DeviceUIConfig.screen_brightness int_size:8 -*DeviceUIConfig.screen_timeout int_size:16 -*DeviceUIConfig.ring_tone_id int_size:8 -*DeviceUIConfig.calibration_data max_size:16 -*DeviceUIConfig.compass_mode int_size:8 -*DeviceUIConfig.gps_format int_size:8 -*NodeFilter.node_name max_size:16 -*NodeFilter.hops_away int_size:8 -*NodeFilter.channel int_size:8 -*NodeHighlight.node_name max_size:16 -*GeoPoint.zoom int_size:8 -*Map.style max_size:20 diff --git a/packages/protobufs/meshtastic/device_ui.proto b/packages/protobufs/meshtastic/device_ui.proto deleted file mode 100644 index 5b6bb5ddb..000000000 --- a/packages/protobufs/meshtastic/device_ui.proto +++ /dev/null @@ -1,389 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "DeviceUIProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * Protobuf structures for device-ui persistency - */ - -message DeviceUIConfig { - /* - * A version integer used to invalidate saved files when we make incompatible changes. - */ - uint32 version = 1; - - /* - * TFT display brightness 1..255 - */ - uint32 screen_brightness = 2; - - /* - * Screen timeout 0..900 - */ - uint32 screen_timeout = 3; - - /* - * Screen/Settings lock enabled - */ - bool screen_lock = 4; - bool settings_lock = 5; - uint32 pin_code = 6; - - /* - * Color theme - */ - Theme theme = 7; - - /* - * Audible message, banner and ring tone - */ - bool alert_enabled = 8; - bool banner_enabled = 9; - uint32 ring_tone_id = 10; - - /* - * Localization - */ - Language language = 11; - - /* - * Node list filter - */ - NodeFilter node_filter = 12; - - /* - * Node list highlightening - */ - NodeHighlight node_highlight = 13; - - /* - * 8 integers for screen calibration data - */ - bytes calibration_data = 14; - - /* - * Map related data - */ - Map map_data = 15; - - /* - * Compass mode - */ - CompassMode compass_mode = 16; - - /* - * RGB color for BaseUI - * 0xRRGGBB format, e.g. 0xFF0000 for red - */ - uint32 screen_rgb_color = 17; - - /* - * Clockface analog style - * true for analog clockface, false for digital clockface - */ - bool is_clockface_analog = 18; - - /* - * How the GPS coordinates are formatted on the OLED screen. - */ - GpsCoordinateFormat gps_format = 19; - - /* - * How the GPS coordinates are displayed on the OLED screen. - */ - enum GpsCoordinateFormat { - /* - * GPS coordinates are displayed in the normal decimal degrees format: - * DD.DDDDDD DDD.DDDDDD - */ - DEC = 0; - - /* - * GPS coordinates are displayed in the degrees minutes seconds format: - * DD°MM'SS"C DDD°MM'SS"C, where C is the compass point representing the locations quadrant - */ - DMS = 1; - - /* - * Universal Transverse Mercator format: - * ZZB EEEEEE NNNNNNN, where Z is zone, B is band, E is easting, N is northing - */ - UTM = 2; - - /* - * Military Grid Reference System format: - * ZZB CD EEEEE NNNNN, where Z is zone, B is band, C is the east 100k square, D is the north 100k square, - * E is easting, N is northing - */ - MGRS = 3; - - /* - * Open Location Code (aka Plus Codes). - */ - OLC = 4; - - /* - * Ordnance Survey Grid Reference (the National Grid System of the UK). - * Format: AB EEEEE NNNNN, where A is the east 100k square, B is the north 100k square, - * E is the easting, N is the northing - */ - OSGR = 5; - - /* - * Maidenhead Locator System - * Described here: https://en.wikipedia.org/wiki/Maidenhead_Locator_System - */ - MLS = 6; - } -} - -message NodeFilter { - /* - * Filter unknown nodes - */ - bool unknown_switch = 1; - - /* - * Filter offline nodes - */ - bool offline_switch = 2; - - /* - * Filter nodes w/o public key - */ - bool public_key_switch = 3; - - /* - * Filter based on hops away - */ - int32 hops_away = 4; - - /* - * Filter nodes w/o position - */ - bool position_switch = 5; - - /* - * Filter nodes by matching name string - */ - string node_name = 6; - - /* - * Filter based on channel - */ - int32 channel = 7; -} - -message NodeHighlight { - /* - * Hightlight nodes w/ active chat - */ - bool chat_switch = 1; - - /* - * Highlight nodes w/ position - */ - bool position_switch = 2; - - /* - * Highlight nodes w/ telemetry data - */ - bool telemetry_switch = 3; - - /* - * Highlight nodes w/ iaq data - */ - bool iaq_switch = 4; - - /* - * Highlight nodes by matching name string - */ - string node_name = 5; -} - -message GeoPoint { - /* - * Zoom level - */ - int32 zoom = 1; - - /* - * Coordinate: latitude - */ - int32 latitude = 2; - - /* - * Coordinate: longitude - */ - int32 longitude = 3; -} - -message Map { - /* - * Home coordinates - */ - GeoPoint home = 1; - - /* - * Map tile style - */ - string style = 2; - - /* - * Map scroll follows GPS - */ - bool follow_gps = 3; -} - -enum CompassMode { - /* - * Compass with dynamic ring and heading - */ - DYNAMIC = 0; - - /* - * Compass with fixed ring and heading - */ - FIXED_RING = 1; - - /* - * Compass with heading and freeze option - */ - FREEZE_HEADING = 2; -} - -enum Theme { - /* - * Dark - */ - DARK = 0; - /* - * Light - */ - LIGHT = 1; - /* - * Red - */ - RED = 2; -} - -/* - * Localization - */ -enum Language { - /* - * English - */ - ENGLISH = 0; - - /* - * French - */ - FRENCH = 1; - - /* - * German - */ - GERMAN = 2; - - /* - * Italian - */ - ITALIAN = 3; - - /* - * Portuguese - */ - PORTUGUESE = 4; - - /* - * Spanish - */ - SPANISH = 5; - - /* - * Swedish - */ - SWEDISH = 6; - - /* - * Finnish - */ - FINNISH = 7; - - /* - * Polish - */ - POLISH = 8; - - /* - * Turkish - */ - TURKISH = 9; - - /* - * Serbian - */ - SERBIAN = 10; - - /* - * Russian - */ - RUSSIAN = 11; - - /* - * Dutch - */ - DUTCH = 12; - - /* - * Greek - */ - GREEK = 13; - - /* - * Norwegian - */ - NORWEGIAN = 14; - - /* - * Slovenian - */ - SLOVENIAN = 15; - - /* - * Ukrainian - */ - UKRAINIAN = 16; - - /* - * Bulgarian - */ - BULGARIAN = 17; - - /* - * Czech - */ - CZECH = 18; - - /* - * Danish - */ - DANISH = 19; - - /* - * Simplified Chinese (experimental) - */ - SIMPLIFIED_CHINESE = 30; - - /* - * Traditional Chinese (experimental) - */ - TRADITIONAL_CHINESE = 31; -} diff --git a/packages/protobufs/meshtastic/deviceonly.options b/packages/protobufs/meshtastic/deviceonly.options deleted file mode 100644 index d6aae0c4d..000000000 --- a/packages/protobufs/meshtastic/deviceonly.options +++ /dev/null @@ -1,18 +0,0 @@ -# options for nanopb -# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options - -# FIXME - max_count is actually 32 but we save/load this as one long string of preencoded MeshPacket bytes - not a big array in RAM -*DeviceState.receive_queue max_count:1 - -*ChannelFile.channels max_count:8 - -*DeviceState.node_remote_hardware_pins max_count:12 - -*NodeInfoLite.channel int_size:8 -*NodeInfoLite.hops_away int_size:8 -*NodeInfoLite.next_hop int_size:8 - -*UserLite.long_name max_size:40 -*UserLite.short_name max_size:5 -*UserLite.public_key max_size:32 # public key -*UserLite.macaddr max_size:6 fixed_length:true \ No newline at end of file diff --git a/packages/protobufs/meshtastic/deviceonly.proto b/packages/protobufs/meshtastic/deviceonly.proto deleted file mode 100644 index d449373d2..000000000 --- a/packages/protobufs/meshtastic/deviceonly.proto +++ /dev/null @@ -1,301 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -import "meshtastic/channel.proto"; -import "meshtastic/config.proto"; -import "meshtastic/localonly.proto"; -import "meshtastic/mesh.proto"; -import "meshtastic/telemetry.proto"; -import "nanopb.proto"; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "DeviceOnly"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; -option (nanopb_fileopt).include = ""; - -/* - * Position with static location information only for NodeDBLite - */ -message PositionLite { - /* - * The new preferred location encoding, multiply by 1e-7 to get degrees - * in floating point - */ - sfixed32 latitude_i = 1; - - /* - * TODO: REPLACE - */ - sfixed32 longitude_i = 2; - - /* - * In meters above MSL (but see issue #359) - */ - int32 altitude = 3; - - /* - * This is usually not sent over the mesh (to save space), but it is sent - * from the phone so that the local device can set its RTC If it is sent over - * the mesh (because there are devices on the mesh without GPS), it will only - * be sent by devices which has a hardware GPS clock. - * seconds since 1970 - */ - fixed32 time = 4; - - /* - * TODO: REPLACE - */ - Position.LocSource location_source = 5; -} - -message UserLite { - /* - * This is the addr of the radio. - */ - bytes macaddr = 1 [deprecated = true]; - - /* - * A full name for this user, i.e. "Kevin Hester" - */ - string long_name = 2; - - /* - * A VERY short name, ideally two characters. - * Suitable for a tiny OLED screen - */ - string short_name = 3; - - /* - * TBEAM, HELTEC, etc... - * Starting in 1.2.11 moved to hw_model enum in the NodeInfo object. - * Apps will still need the string here for older builds - * (so OTA update can find the right image), but if the enum is available it will be used instead. - */ - HardwareModel hw_model = 4; - - /* - * In some regions Ham radio operators have different bandwidth limitations than others. - * If this user is a licensed operator, set this flag. - * Also, "long_name" should be their licence number. - */ - bool is_licensed = 5; - - /* - * Indicates that the user's role in the mesh - */ - Config.DeviceConfig.Role role = 6; - - /* - * The public key of the user's device. - * This is sent out to other nodes on the mesh to allow them to compute a shared secret key. - */ - bytes public_key = 7; - - /* - * Whether or not the node can be messaged - */ - optional bool is_unmessagable = 9; -} - -message NodeInfoLite { - /* - * The node number - */ - uint32 num = 1; - - /* - * The user info for this node - */ - UserLite user = 2; - - /* - * This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. - * Position.time now indicates the last time we received a POSITION from that node. - */ - PositionLite position = 3; - - /* - * Returns the Signal-to-noise ratio (SNR) of the last received message, - * as measured by the receiver. Return SNR of the last received message in dB - */ - float snr = 4; - - /* - * Set to indicate the last time we received a packet from this node - */ - fixed32 last_heard = 5; - /* - * The latest device metrics for the node. - */ - DeviceMetrics device_metrics = 6; - - /* - * local channel index we heard that node on. Only populated if its not the default channel. - */ - uint32 channel = 7; - - /* - * True if we witnessed the node over MQTT instead of LoRA transport - */ - bool via_mqtt = 8; - - /* - * Number of hops away from us this node is (0 if direct neighbor) - */ - optional uint32 hops_away = 9; - - /* - * True if node is in our favorites list - * Persists between NodeDB internal clean ups - */ - bool is_favorite = 10; - - /* - * True if node is in our ignored list - * Persists between NodeDB internal clean ups - */ - bool is_ignored = 11; - - /* - * Last byte of the node number of the node that should be used as the next hop to reach this node. - */ - uint32 next_hop = 12; - - /* - * Bitfield for storing booleans. - * LSB 0 is_key_manually_verified - */ - uint32 bitfield = 13; -} - -/* - * This message is never sent over the wire, but it is used for serializing DB - * state to flash in the device code - * FIXME, since we write this each time we enter deep sleep (and have infinite - * flash) it would be better to use some sort of append only data structure for - * the receive queue and use the preferences store for the other stuff - */ -message DeviceState { - /* - * Read only settings/info about this node - */ - MyNodeInfo my_node = 2; - - /* - * My owner info - */ - User owner = 3; - - /* - * Received packets saved for delivery to the phone - */ - repeated MeshPacket receive_queue = 5; - - /* - * A version integer used to invalidate old save files when we make - * incompatible changes This integer is set at build time and is private to - * NodeDB.cpp in the device code. - */ - uint32 version = 8; - - /* - * We keep the last received text message (only) stored in the device flash, - * so we can show it on the screen. - * Might be null - */ - MeshPacket rx_text_message = 7; - - /* - * Used only during development. - * Indicates developer is testing and changes should never be saved to flash. - * Deprecated in 2.3.1 - */ - bool no_save = 9 [deprecated = true]; - - /* - * Previously used to manage GPS factory resets. - * Deprecated in 2.5.23 - */ - bool did_gps_reset = 11 [deprecated = true]; - - /* - * We keep the last received waypoint stored in the device flash, - * so we can show it on the screen. - * Might be null - */ - MeshPacket rx_waypoint = 12; - - /* - * The mesh's nodes with their available gpio pins for RemoteHardware module - */ - repeated NodeRemoteHardwarePin node_remote_hardware_pins = 13; -} - -message NodeDatabase { - /* - * A version integer used to invalidate old save files when we make - * incompatible changes This integer is set at build time and is private to - * NodeDB.cpp in the device code. - */ - uint32 version = 1; - - /* - * New lite version of NodeDB to decrease memory footprint - */ - repeated NodeInfoLite nodes = 2 [(nanopb).callback_datatype = "std::vector"]; -} - -/* - * The on-disk saved channels - */ -message ChannelFile { - /* - * The channels our node knows about - */ - repeated Channel channels = 1; - - /* - * A version integer used to invalidate old save files when we make - * incompatible changes This integer is set at build time and is private to - * NodeDB.cpp in the device code. - */ - uint32 version = 2; -} - -/* - * The on-disk backup of the node's preferences - */ -message BackupPreferences { - /* - * The version of the backup - */ - uint32 version = 1; - - /* - * The timestamp of the backup (if node has time) - */ - fixed32 timestamp = 2; - - /* - * The node's configuration - */ - LocalConfig config = 3; - - /* - * The node's module configuration - */ - LocalModuleConfig module_config = 4; - - /* - * The node's channels - */ - ChannelFile channels = 5; - - /* - * The node's user (owner) information - */ - User owner = 6; -} diff --git a/packages/protobufs/meshtastic/interdevice.options b/packages/protobufs/meshtastic/interdevice.options deleted file mode 100644 index 97df282f7..000000000 --- a/packages/protobufs/meshtastic/interdevice.options +++ /dev/null @@ -1 +0,0 @@ -*InterdeviceMessage.nmea max_size:1024 diff --git a/packages/protobufs/meshtastic/interdevice.proto b/packages/protobufs/meshtastic/interdevice.proto deleted file mode 100644 index 4616c0871..000000000 --- a/packages/protobufs/meshtastic/interdevice.proto +++ /dev/null @@ -1,44 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "InterdeviceProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -// encapsulate up to 1k of NMEA string data - -enum MessageType { - ACK = 0; - COLLECT_INTERVAL = 160; // in ms - BEEP_ON = 161; // duration ms - BEEP_OFF = 162; // cancel prematurely - SHUTDOWN = 163; - POWER_ON = 164; - SCD41_TEMP = 176; - SCD41_HUMIDITY = 177; - SCD41_CO2 = 178; - AHT20_TEMP = 179; - AHT20_HUMIDITY = 180; - TVOC_INDEX = 181; -} - -message SensorData { - // The message type - MessageType type = 1; - // The sensor data, either as a float or an uint32 - oneof data { - float float_value = 2; - uint32 uint32_value = 3; - } -} - -message InterdeviceMessage { - // The message data - oneof data { - string nmea = 1; - SensorData sensor = 2; - } -} diff --git a/packages/protobufs/meshtastic/localonly.proto b/packages/protobufs/meshtastic/localonly.proto deleted file mode 100644 index bcb279643..000000000 --- a/packages/protobufs/meshtastic/localonly.proto +++ /dev/null @@ -1,140 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -import "meshtastic/config.proto"; -import "meshtastic/module_config.proto"; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "LocalOnlyProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * Protobuf structures common to apponly.proto and deviceonly.proto - * This is never sent over the wire, only for local use - */ - -message LocalConfig { - /* - * The part of the config that is specific to the Device - */ - Config.DeviceConfig device = 1; - - /* - * The part of the config that is specific to the GPS Position - */ - Config.PositionConfig position = 2; - - /* - * The part of the config that is specific to the Power settings - */ - Config.PowerConfig power = 3; - - /* - * The part of the config that is specific to the Wifi Settings - */ - Config.NetworkConfig network = 4; - - /* - * The part of the config that is specific to the Display - */ - Config.DisplayConfig display = 5; - - /* - * The part of the config that is specific to the Lora Radio - */ - Config.LoRaConfig lora = 6; - - /* - * The part of the config that is specific to the Bluetooth settings - */ - Config.BluetoothConfig bluetooth = 7; - - /* - * A version integer used to invalidate old save files when we make - * incompatible changes This integer is set at build time and is private to - * NodeDB.cpp in the device code. - */ - uint32 version = 8; - - /* - * The part of the config that is specific to Security settings - */ - Config.SecurityConfig security = 9; -} - -message LocalModuleConfig { - /* - * The part of the config that is specific to the MQTT module - */ - ModuleConfig.MQTTConfig mqtt = 1; - - /* - * The part of the config that is specific to the Serial module - */ - ModuleConfig.SerialConfig serial = 2; - - /* - * The part of the config that is specific to the ExternalNotification module - */ - ModuleConfig.ExternalNotificationConfig external_notification = 3; - - /* - * The part of the config that is specific to the Store & Forward module - */ - ModuleConfig.StoreForwardConfig store_forward = 4; - - /* - * The part of the config that is specific to the RangeTest module - */ - ModuleConfig.RangeTestConfig range_test = 5; - - /* - * The part of the config that is specific to the Telemetry module - */ - ModuleConfig.TelemetryConfig telemetry = 6; - - /* - * The part of the config that is specific to the Canned Message module - */ - ModuleConfig.CannedMessageConfig canned_message = 7; - - /* - * The part of the config that is specific to the Audio module - */ - ModuleConfig.AudioConfig audio = 9; - - /* - * The part of the config that is specific to the Remote Hardware module - */ - ModuleConfig.RemoteHardwareConfig remote_hardware = 10; - - /* - * The part of the config that is specific to the Neighbor Info module - */ - ModuleConfig.NeighborInfoConfig neighbor_info = 11; - - /* - * The part of the config that is specific to the Ambient Lighting module - */ - ModuleConfig.AmbientLightingConfig ambient_lighting = 12; - - /* - * The part of the config that is specific to the Detection Sensor module - */ - ModuleConfig.DetectionSensorConfig detection_sensor = 13; - - /* - * Paxcounter Config - */ - ModuleConfig.PaxcounterConfig paxcounter = 14; - - /* - * A version integer used to invalidate old save files when we make - * incompatible changes This integer is set at build time and is private to - * NodeDB.cpp in the device code. - */ - uint32 version = 8; -} diff --git a/packages/protobufs/meshtastic/mesh.options b/packages/protobufs/meshtastic/mesh.options deleted file mode 100644 index 37c934165..000000000 --- a/packages/protobufs/meshtastic/mesh.options +++ /dev/null @@ -1,92 +0,0 @@ -# options for nanopb -# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options - -*macaddr max_size:6 fixed_length:true # macaddrs -*id max_size:16 # node id strings -*public_key max_size:32 # public key - -*User.long_name max_size:40 -*User.short_name max_size:5 - -*RouteDiscovery.route max_count:8 -*RouteDiscovery.snr_towards max_count:8 -*RouteDiscovery.snr_towards int_size:8 -*RouteDiscovery.route_back max_count:8 -*RouteDiscovery.snr_back max_count:8 -*RouteDiscovery.snr_back int_size:8 - -# note: this payload length is ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is -# outside of this envelope -*Data.payload max_size:233 -*Data.bitfield int_size:8 - -*NodeInfo.channel int_size:8 -*NodeInfo.hops_away int_size:8 - -# Big enough for 1.2.28.568032c-d -*MyNodeInfo.firmware_version max_size:18 -*MyNodeInfo.device_id max_size:16 -*MyNodeInfo.pio_env max_size:40 - -*MyNodeInfo.air_period_tx max_count:8 -*MyNodeInfo.air_period_rx max_count:8 - -*MyNodeInfo.firmware_edition int_size:8 -*MyNodeInfo.nodedb_count int_size:16 - -# Note: the actual limit (because of header bytes) on the size of encrypted payloads is 251 bytes, but I use 256 -# here because we might need to fill with zeros for padding to encryption block size (16 bytes per block) -*MeshPacket.encrypted max_size:256 -*MeshPacket.payload_variant anonymous_oneof:true -*MeshPacket.hop_limit int_size:8 -*MeshPacket.hop_start int_size:8 -*MeshPacket.channel int_size:8 -*MeshPacket.next_hop int_size:8 -*MeshPacket.relay_node int_size:8 - -*QueueStatus.res int_size:8 -*QueueStatus.free int_size:8 -*QueueStatus.maxlen int_size:8 - -*ToRadio.payload_variant anonymous_oneof:true - -*FromRadio.payload_variant anonymous_oneof:true - -*Routing.variant anonymous_oneof:true - -*LogRecord.message max_size:384 -*LogRecord.source max_size:32 - -*FileInfo.file_name max_size:228 - -*ClientNotification.message max_size:400 - -*KeyVerificationNumberInform.remote_longname max_size:40 -*KeyVerificationNumberRequest.remote_longname max_size:40 -*KeyVerificationFinal.remote_longname max_size:40 -*KeyVerificationFinal.verification_characters max_size:10 - -*KeyVerification.hash1 max_size:32 -*KeyVerification.hash2 max_size:32 - - -# MyMessage.name max_size:40 -# or fixed_length or fixed_count, or max_count - -#This value may want to be a few bytes smaller to compensate for the parent fields. -*Compressed.data max_size:233 - -*Waypoint.name max_size:30 -*Waypoint.description max_size:100 - -*NeighborInfo.neighbors max_count:10 - -*DeviceMetadata.firmware_version max_size:18 - -*MqttClientProxyMessage.topic max_size:60 -*MqttClientProxyMessage.data max_size:435 -*MqttClientProxyMessage.text max_size:435 - -*ChunkedPayload.chunk_count int_size:16 -*ChunkedPayload.chunk_index int_size:16 -*ChunkedPayload.payload_chunk max_size:228 \ No newline at end of file diff --git a/packages/protobufs/meshtastic/mesh.proto b/packages/protobufs/meshtastic/mesh.proto deleted file mode 100644 index 59f8c1016..000000000 --- a/packages/protobufs/meshtastic/mesh.proto +++ /dev/null @@ -1,2409 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -import "meshtastic/channel.proto"; -import "meshtastic/config.proto"; -import "meshtastic/device_ui.proto"; -import "meshtastic/module_config.proto"; -import "meshtastic/portnums.proto"; -import "meshtastic/telemetry.proto"; -import "meshtastic/xmodem.proto"; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "MeshProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * A GPS Position - */ -message Position { - /* - * The new preferred location encoding, multiply by 1e-7 to get degrees - * in floating point - */ - optional sfixed32 latitude_i = 1; - - /* - * TODO: REPLACE - */ - optional sfixed32 longitude_i = 2; - - /* - * In meters above MSL (but see issue #359) - */ - optional int32 altitude = 3; - - /* - * This is usually not sent over the mesh (to save space), but it is sent - * from the phone so that the local device can set its time if it is sent over - * the mesh (because there are devices on the mesh without GPS or RTC). - * seconds since 1970 - */ - fixed32 time = 4; - - /* - * How the location was acquired: manual, onboard GPS, external (EUD) GPS - */ - enum LocSource { - /* - * TODO: REPLACE - */ - LOC_UNSET = 0; - - /* - * TODO: REPLACE - */ - LOC_MANUAL = 1; - - /* - * TODO: REPLACE - */ - LOC_INTERNAL = 2; - - /* - * TODO: REPLACE - */ - LOC_EXTERNAL = 3; - } - - /* - * TODO: REPLACE - */ - LocSource location_source = 5; - - /* - * How the altitude was acquired: manual, GPS int/ext, etc - * Default: same as location_source if present - */ - enum AltSource { - /* - * TODO: REPLACE - */ - ALT_UNSET = 0; - - /* - * TODO: REPLACE - */ - ALT_MANUAL = 1; - - /* - * TODO: REPLACE - */ - ALT_INTERNAL = 2; - - /* - * TODO: REPLACE - */ - ALT_EXTERNAL = 3; - - /* - * TODO: REPLACE - */ - ALT_BAROMETRIC = 4; - } - - /* - * TODO: REPLACE - */ - AltSource altitude_source = 6; - - /* - * Positional timestamp (actual timestamp of GPS solution) in integer epoch seconds - */ - fixed32 timestamp = 7; - - /* - * Pos. timestamp milliseconds adjustment (rarely available or required) - */ - int32 timestamp_millis_adjust = 8; - - /* - * HAE altitude in meters - can be used instead of MSL altitude - */ - optional sint32 altitude_hae = 9; - - /* - * Geoidal separation in meters - */ - optional sint32 altitude_geoidal_separation = 10; - - /* - * Horizontal, Vertical and Position Dilution of Precision, in 1/100 units - * - PDOP is sufficient for most cases - * - for higher precision scenarios, HDOP and VDOP can be used instead, - * in which case PDOP becomes redundant (PDOP=sqrt(HDOP^2 + VDOP^2)) - * TODO: REMOVE/INTEGRATE - */ - uint32 PDOP = 11; - - /* - * TODO: REPLACE - */ - uint32 HDOP = 12; - - /* - * TODO: REPLACE - */ - uint32 VDOP = 13; - - /* - * GPS accuracy (a hardware specific constant) in mm - * multiplied with DOP to calculate positional accuracy - * Default: "'bout three meters-ish" :) - */ - uint32 gps_accuracy = 14; - - /* - * Ground speed in m/s and True North TRACK in 1/100 degrees - * Clarification of terms: - * - "track" is the direction of motion (measured in horizontal plane) - * - "heading" is where the fuselage points (measured in horizontal plane) - * - "yaw" indicates a relative rotation about the vertical axis - * TODO: REMOVE/INTEGRATE - */ - optional uint32 ground_speed = 15; - - /* - * TODO: REPLACE - */ - optional uint32 ground_track = 16; - - /* - * GPS fix quality (from NMEA GxGGA statement or similar) - */ - uint32 fix_quality = 17; - - /* - * GPS fix type 2D/3D (from NMEA GxGSA statement) - */ - uint32 fix_type = 18; - - /* - * GPS "Satellites in View" number - */ - uint32 sats_in_view = 19; - - /* - * Sensor ID - in case multiple positioning sensors are being used - */ - uint32 sensor_id = 20; - - /* - * Estimated/expected time (in seconds) until next update: - * - if we update at fixed intervals of X seconds, use X - * - if we update at dynamic intervals (based on relative movement etc), - * but "AT LEAST every Y seconds", use Y - */ - uint32 next_update = 21; - - /* - * A sequence number, incremented with each Position message to help - * detect lost updates if needed - */ - uint32 seq_number = 22; - - /* - * Indicates the bits of precision set by the sending node - */ - uint32 precision_bits = 23; -} - -/* - * Note: these enum names must EXACTLY match the string used in the device - * bin/build-all.sh script. - * Because they will be used to find firmware filenames in the android app for OTA updates. - * To match the old style filenames, _ is converted to -, p is converted to . - */ -enum HardwareModel { - /* - * TODO: REPLACE - */ - UNSET = 0; - - /* - * TODO: REPLACE - */ - TLORA_V2 = 1; - - /* - * TODO: REPLACE - */ - TLORA_V1 = 2; - - /* - * TODO: REPLACE - */ - TLORA_V2_1_1P6 = 3; - - /* - * TODO: REPLACE - */ - TBEAM = 4; - - /* - * The original heltec WiFi_Lora_32_V2, which had battery voltage sensing hooked to GPIO 13 - * (see HELTEC_V2 for the new version). - */ - HELTEC_V2_0 = 5; - - /* - * TODO: REPLACE - */ - TBEAM_V0P7 = 6; - - /* - * TODO: REPLACE - */ - T_ECHO = 7; - - /* - * TODO: REPLACE - */ - TLORA_V1_1P3 = 8; - - /* - * TODO: REPLACE - */ - RAK4631 = 9; - - /* - * The new version of the heltec WiFi_Lora_32_V2 board that has battery sensing hooked to GPIO 37. - * Sadly they did not update anything on the silkscreen to identify this board - */ - HELTEC_V2_1 = 10; - - /* - * Ancient heltec WiFi_Lora_32 board - */ - HELTEC_V1 = 11; - - /* - * New T-BEAM with ESP32-S3 CPU - */ - LILYGO_TBEAM_S3_CORE = 12; - - /* - * RAK WisBlock ESP32 core: https://docs.rakwireless.com/Product-Categories/WisBlock/RAK11200/Overview/ - */ - RAK11200 = 13; - - /* - * B&Q Consulting Nano Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:nano - */ - NANO_G1 = 14; - - /* - * TODO: REPLACE - */ - TLORA_V2_1_1P8 = 15; - - /* - * TODO: REPLACE - */ - TLORA_T3_S3 = 16; - - /* - * B&Q Consulting Nano G1 Explorer: https://wiki.uniteng.com/en/meshtastic/nano-g1-explorer - */ - NANO_G1_EXPLORER = 17; - - /* - * B&Q Consulting Nano G2 Ultra: https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra - */ - NANO_G2_ULTRA = 18; - - /* - * LoRAType device: https://loratype.org/ - */ - LORA_TYPE = 19; - - /* - * wiphone https://www.wiphone.io/ - */ - WIPHONE = 20; - - /* - * WIO Tracker WM1110 family from Seeed Studio. Includes wio-1110-tracker and wio-1110-sdk - */ - WIO_WM1110 = 21; - - /* - * RAK2560 Solar base station based on RAK4630 - */ - RAK2560 = 22; - - /* - * Heltec HRU-3601: https://heltec.org/project/hru-3601/ - */ - HELTEC_HRU_3601 = 23; - - /* - * Heltec Wireless Bridge - */ - HELTEC_WIRELESS_BRIDGE = 24; - - /* - * B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station - */ - STATION_G1 = 25; - - /* - * RAK11310 (RP2040 + SX1262) - */ - RAK11310 = 26; - - /* - * Makerfabs SenseLoRA Receiver (RP2040 + RFM96) - */ - SENSELORA_RP2040 = 27; - - /* - * Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96) - */ - SENSELORA_S3 = 28; - - /* - * Canary Radio Company - CanaryOne: https://canaryradio.io/products/canaryone - */ - CANARYONE = 29; - - /* - * Waveshare RP2040 LoRa - https://www.waveshare.com/rp2040-lora.htm - */ - RP2040_LORA = 30; - - /* - * B&Q Consulting Station G2: https://wiki.uniteng.com/en/meshtastic/station-g2 - */ - STATION_G2 = 31; - - /* - * --------------------------------------------------------------------------- - * Less common/prototype boards listed here (needs one more byte over the air) - * --------------------------------------------------------------------------- - */ - LORA_RELAY_V1 = 32; - - /* - * TODO: REPLACE - */ - NRF52840DK = 33; - - /* - * TODO: REPLACE - */ - PPR = 34; - - /* - * TODO: REPLACE - */ - GENIEBLOCKS = 35; - - /* - * TODO: REPLACE - */ - NRF52_UNKNOWN = 36; - - /* - * TODO: REPLACE - */ - PORTDUINO = 37; - - /* - * The simulator built into the android app - */ - ANDROID_SIM = 38; - - /* - * Custom DIY device based on @NanoVHF schematics: https://github.com/NanoVHF/Meshtastic-DIY/tree/main/Schematics - */ - DIY_V1 = 39; - - /* - * nRF52840 Dongle : https://www.nordicsemi.com/Products/Development-hardware/nrf52840-dongle/ - */ - NRF52840_PCA10059 = 40; - - /* - * Custom Disaster Radio esp32 v3 device https://github.com/sudomesh/disaster-radio/tree/master/hardware/board_esp32_v3 - */ - DR_DEV = 41; - - /* - * M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ - */ - M5STACK = 42; - - /* - * New Heltec LoRA32 with ESP32-S3 CPU - */ - HELTEC_V3 = 43; - - /* - * New Heltec Wireless Stick Lite with ESP32-S3 CPU - */ - HELTEC_WSL_V3 = 44; - - /* - * New BETAFPV ELRS Micro TX Module 2.4G with ESP32 CPU - */ - BETAFPV_2400_TX = 45; - - /* - * BetaFPV ExpressLRS "Nano" TX Module 900MHz with ESP32 CPU - */ - BETAFPV_900_NANO_TX = 46; - - /* - * Raspberry Pi Pico (W) with Waveshare SX1262 LoRa Node Module - */ - RPI_PICO = 47; - - /* - * Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT - * Newer V1.1, version is written on the PCB near the display. - */ - HELTEC_WIRELESS_TRACKER = 48; - - /* - * Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display - */ - HELTEC_WIRELESS_PAPER = 49; - - /* - * LilyGo T-Deck with ESP32-S3 CPU, Keyboard and IPS display - */ - T_DECK = 50; - - /* - * LilyGo T-Watch S3 with ESP32-S3 CPU and IPS display - */ - T_WATCH_S3 = 51; - - /* - * Bobricius Picomputer with ESP32-S3 CPU, Keyboard and IPS display - */ - PICOMPUTER_S3 = 52; - - /* - * Heltec HT-CT62 with ESP32-C3 CPU and SX1262 LoRa - */ - HELTEC_HT62 = 53; - - /* - * EBYTE SPI LoRa module and ESP32-S3 - */ - EBYTE_ESP32_S3 = 54; - - /* - * Waveshare ESP32-S3-PICO with PICO LoRa HAT and 2.9inch e-Ink - */ - ESP32_S3_PICO = 55; - - /* - * CircuitMess Chatter 2 LLCC68 Lora Module and ESP32 Wroom - * Lora module can be swapped out for a Heltec RA-62 which is "almost" pin compatible - * with one cut and one jumper Meshtastic works - */ - CHATTER_2 = 56; - - /* - * Heltec Wireless Paper, With ESP32-S3 CPU and E-Ink display - * Older "V1.0" Variant, has no "version sticker" - * E-Ink model is DEPG0213BNS800 - * Tab on the screen protector is RED - * Flex connector marking is FPC-7528B - */ - HELTEC_WIRELESS_PAPER_V1_0 = 57; - - /* - * Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT - * Older "V1.0" Variant - */ - HELTEC_WIRELESS_TRACKER_V1_0 = 58; - - /* - * unPhone with ESP32-S3, TFT touchscreen, LSM6DS3TR-C accelerometer and gyroscope - */ - UNPHONE = 59; - - /* - * Teledatics TD-LORAC NRF52840 based M.2 LoRA module - * Compatible with the TD-WRLS development board - */ - TD_LORAC = 60; - - /* - * CDEBYTE EoRa-S3 board using their own MM modules, clone of LILYGO T3S3 - */ - CDEBYTE_EORA_S3 = 61; - - /* - * TWC_MESH_V4 - * Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS - */ - TWC_MESH_V4 = 62; - - /* - * NRF52_PROMICRO_DIY - * Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS - */ - NRF52_PROMICRO_DIY = 63; - - /* - * RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module - * ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS - */ - RADIOMASTER_900_BANDIT_NANO = 64; - - /* - * Heltec Capsule Sensor V3 with ESP32-S3 CPU, Portable LoRa device that can replace GNSS modules or sensors - */ - HELTEC_CAPSULE_SENSOR_V3 = 65; - - /* - * Heltec Vision Master T190 with ESP32-S3 CPU, and a 1.90 inch TFT display - */ - HELTEC_VISION_MASTER_T190 = 66; - - /* - * Heltec Vision Master E213 with ESP32-S3 CPU, and a 2.13 inch E-Ink display - */ - HELTEC_VISION_MASTER_E213 = 67; - - /* - * Heltec Vision Master E290 with ESP32-S3 CPU, and a 2.9 inch E-Ink display - */ - HELTEC_VISION_MASTER_E290 = 68; - - /* - * Heltec Mesh Node T114 board with nRF52840 CPU, and a 1.14 inch TFT display, Ultimate low-power design, - * specifically adapted for the Meshtatic project - */ - HELTEC_MESH_NODE_T114 = 69; - - /* - * Sensecap Indicator from Seeed Studio. ESP32-S3 device with TFT and RP2040 coprocessor - */ - SENSECAP_INDICATOR = 70; - - /* - * Seeed studio T1000-E tracker card. NRF52840 w/ LR1110 radio, GPS, button, buzzer, and sensors. - */ - TRACKER_T1000_E = 71; - - /* - * RAK3172 STM32WLE5 Module (https://store.rakwireless.com/products/wisduo-lpwan-module-rak3172) - */ - RAK3172 = 72; - - /* - * Seeed Studio Wio-E5 (either mini or Dev kit) using STM32WL chip. - */ - WIO_E5 = 73; - - /* - * RadioMaster 900 Bandit, https://www.radiomasterrc.com/products/bandit-expresslrs-rf-module - * SSD1306 OLED and No GPS - */ - RADIOMASTER_900_BANDIT = 74; - - /* - * Minewsemi ME25LS01 (ME25LE01_V1.0). NRF52840 w/ LR1110 radio, buttons and leds and pins. - */ - ME25LS01_4Y10TD = 75; - - /* - * RP2040_FEATHER_RFM95 - * Adafruit Feather RP2040 with RFM95 LoRa Radio RFM95 with SX1272, SSD1306 OLED - * https://www.adafruit.com/product/5714 - * https://www.adafruit.com/product/326 - * https://www.adafruit.com/product/938 - * ^^^ short A0 to switch to I2C address 0x3C - * - */ - RP2040_FEATHER_RFM95 = 76; - - /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */ - M5STACK_COREBASIC = 77; - M5STACK_CORE2 = 78; - - /* Pico2 with Waveshare Hat, same as Pico */ - RPI_PICO2 = 79; - - /* M5 esp32 based MCU modules with enclosure, TFT and LORA Shields. All Variants (Basic, Core, Fire, Core2, CoreS3, Paper) https://m5stack.com/ */ - M5STACK_CORES3 = 80; - - /* Seeed XIAO S3 DK*/ - SEEED_XIAO_S3 = 81; - - /* - * Nordic nRF52840+Semtech SX1262 LoRa BLE Combo Module. nRF52840+SX1262 MS24SF1 - */ - MS24SF1 = 82; - - /* - * Lilygo TLora-C6 with the new ESP32-C6 MCU - */ - TLORA_C6 = 83; - - /* - * WisMesh Tap - * RAK-4631 w/ TFT in injection modled case - */ - WISMESH_TAP = 84; - - /* - * Similar to PORTDUINO but used by Routastic devices, this is not any - * particular device and does not run Meshtastic's code but supports - * the same frame format. - * Runs on linux, see https://github.com/Jorropo/routastic - */ - ROUTASTIC = 85; - - /* - * Mesh-Tab, esp32 based - * https://github.com/valzzu/Mesh-Tab - */ - MESH_TAB = 86; - - /* - * MeshLink board developed by LoraItalia. NRF52840, eByte E22900M22S (Will also come with other frequencies), 25w MPPT solar charger (5v,12v,18v selectable), support for gps, buzzer, oled or e-ink display, 10 gpios, hardware watchdog - * https://www.loraitalia.it - */ - MESHLINK = 87; - - /* - * Seeed XIAO nRF52840 + Wio SX1262 kit - */ - XIAO_NRF52_KIT = 88; - - /* - * Elecrow ThinkNode M1 & M2 - * https://www.elecrow.com/wiki/ThinkNode-M1_Transceiver_Device(Meshtastic)_Power_By_nRF52840.html - * https://www.elecrow.com/wiki/ThinkNode-M2_Transceiver_Device(Meshtastic)_Power_By_NRF52840.html (this actually uses ESP32-S3) - */ - THINKNODE_M1 = 89; - THINKNODE_M2 = 90; - - /* - * Lilygo T-ETH-Elite - */ - T_ETH_ELITE = 91; - - /* - * Heltec HRI-3621 industrial probe - */ - HELTEC_SENSOR_HUB = 92; - - /* - * Reserved Fried Chicken ID for future use - */ - RESERVED_FRIED_CHICKEN = 93; - - /* - * Heltec Magnetic Power Bank with Meshtastic compatible - */ - HELTEC_MESH_POCKET = 94; - - /* - * Seeed Solar Node - */ - SEEED_SOLAR_NODE = 95; - - /* - * NomadStar Meteor Pro https://nomadstar.ch/ - */ - NOMADSTAR_METEOR_PRO = 96; - - /* - * Elecrow CrowPanel Advance models, ESP32-S3 and TFT with SX1262 radio plugin - */ - CROWPANEL = 97; - - /* - * Lilygo LINK32 board with sensors - */ - LINK_32 = 98; - - /* - * Seeed Tracker L1 - */ - SEEED_WIO_TRACKER_L1 = 99; - - /* - * Seeed Tracker L1 EINK driver - */ - SEEED_WIO_TRACKER_L1_EINK = 100; - - /* - * Muzi Works R1 Neo - */ - MUZI_R1_NEO = 101; - - /* - * Lilygo T-Deck Pro - */ - T_DECK_PRO = 102; - - /* - * Lilygo TLora Pager - */ - T_LORA_PAGER = 103; - - /* - * M5Stack Reserved - */ - M5STACK_RESERVED = 104; // 0x68 - - /* - * RAKwireless WisMesh Tag - */ - WISMESH_TAG = 105; - - /* - * RAKwireless WisBlock Core RAK3312 https://docs.rakwireless.com/product-categories/wisduo/rak3112-module/overview/ - */ - RAK3312 = 106; - - /* - * Elecrow ThinkNode M5 https://www.elecrow.com/wiki/ThinkNode_M5_Meshtastic_LoRa_Signal_Transceiver_ESP32-S3.html - */ - THINKNODE_M5 = 107; - - /* - * MeshSolar is an integrated power management and communication solution designed for outdoor low-power devices. - * https://heltec.org/project/meshsolar/ - */ - HELTEC_MESH_SOLAR = 108; - - /* - * Lilygo T-Echo Lite - */ - T_ECHO_LITE = 109; - - /* - * New Heltec LoRA32 with ESP32-S3 CPU - */ - HELTEC_V4 = 110; - - /* - * M5Stack C6L - */ - M5STACK_C6L = 111; - - /* - * M5Stack Cardputer Adv - */ - M5STACK_CARDPUTER_ADV = 112; - - /* - * ESP32S3 main controller with GPS and TFT screen. - */ - HELTEC_WIRELESS_TRACKER_V2 = 113; - - /* - * LilyGo T-Watch Ultra - */ - T_WATCH_ULTRA = 114; - - /* - * ------------------------------------------------------------------------------------------------------------------------------------------ - * Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. - * ------------------------------------------------------------------------------------------------------------------------------------------ - */ - PRIVATE_HW = 255; -} - -/* - * Broadcast when a newly powered mesh node wants to find a node num it can use - * Sent from the phone over bluetooth to set the user id for the owner of this node. - * Also sent from nodes to each other when a new node signs on (so all clients can have this info) - * The algorithm is as follows: - * when a node starts up, it broadcasts their user and the normal flow is for all - * other nodes to reply with their User as well (so the new node can build its nodedb) - * If a node ever receives a User (not just the first broadcast) message where - * the sender node number equals our node number, that indicates a collision has - * occurred and the following steps should happen: - * If the receiving node (that was already in the mesh)'s macaddr is LOWER than the - * new User who just tried to sign in: it gets to keep its nodenum. - * We send a broadcast message of OUR User (we use a broadcast so that the other node can - * receive our message, considering we have the same id - it also serves to let - * observers correct their nodedb) - this case is rare so it should be okay. - * If any node receives a User where the macaddr is GTE than their local macaddr, - * they have been vetoed and should pick a new random nodenum (filtering against - * whatever it knows about the nodedb) and rebroadcast their User. - * A few nodenums are reserved and will never be requested: - * 0xff - broadcast - * 0 through 3 - for future use - */ -message User { - /* - * A globally unique ID string for this user. - * In the case of Signal that would mean +16504442323, for the default macaddr derived id it would be !<8 hexidecimal bytes>. - * Note: app developers are encouraged to also use the following standard - * node IDs "^all" (for broadcast), "^local" (for the locally connected node) - */ - string id = 1; - - /* - * A full name for this user, i.e. "Kevin Hester" - */ - string long_name = 2; - - /* - * A VERY short name, ideally two characters. - * Suitable for a tiny OLED screen - */ - string short_name = 3; - - /* - * Deprecated in Meshtastic 2.1.x - * This is the addr of the radio. - * Not populated by the phone, but added by the esp32 when broadcasting - */ - bytes macaddr = 4 [deprecated = true]; - - /* - * TBEAM, HELTEC, etc... - * Starting in 1.2.11 moved to hw_model enum in the NodeInfo object. - * Apps will still need the string here for older builds - * (so OTA update can find the right image), but if the enum is available it will be used instead. - */ - HardwareModel hw_model = 5; - - /* - * In some regions Ham radio operators have different bandwidth limitations than others. - * If this user is a licensed operator, set this flag. - * Also, "long_name" should be their licence number. - */ - bool is_licensed = 6; - - /* - * Indicates that the user's role in the mesh - */ - Config.DeviceConfig.Role role = 7; - - /* - * The public key of the user's device. - * This is sent out to other nodes on the mesh to allow them to compute a shared secret key. - */ - bytes public_key = 8; - - /* - * Whether or not the node can be messaged - */ - optional bool is_unmessagable = 9; -} - -/* - * A message used in a traceroute - */ -message RouteDiscovery { - /* - * The list of nodenums this packet has visited so far to the destination. - */ - repeated fixed32 route = 1; - - /* - * The list of SNRs (in dB, scaled by 4) in the route towards the destination. - */ - repeated int32 snr_towards = 2; - - /* - * The list of nodenums the packet has visited on the way back from the destination. - */ - repeated fixed32 route_back = 3; - - /* - * The list of SNRs (in dB, scaled by 4) in the route back from the destination. - */ - repeated int32 snr_back = 4; -} - -/* - * A Routing control Data packet handled by the routing module - */ -message Routing { - /* - * A failure in delivering a message (usually used for routing control messages, but might be provided in addition to ack.fail_id to provide - * details on the type of failure). - */ - enum Error { - /* - * This message is not a failure - */ - NONE = 0; - - /* - * Our node doesn't have a route to the requested destination anymore. - */ - NO_ROUTE = 1; - - /* - * We received a nak while trying to forward on your behalf - */ - GOT_NAK = 2; - - /* - * TODO: REPLACE - */ - TIMEOUT = 3; - - /* - * No suitable interface could be found for delivering this packet - */ - NO_INTERFACE = 4; - - /* - * We reached the max retransmission count (typically for naive flood routing) - */ - MAX_RETRANSMIT = 5; - - /* - * No suitable channel was found for sending this packet (i.e. was requested channel index disabled?) - */ - NO_CHANNEL = 6; - - /* - * The packet was too big for sending (exceeds interface MTU after encoding) - */ - TOO_LARGE = 7; - - /* - * The request had want_response set, the request reached the destination node, but no service on that node wants to send a response - * (possibly due to bad channel permissions) - */ - NO_RESPONSE = 8; - - /* - * Cannot send currently because duty cycle regulations will be violated. - */ - DUTY_CYCLE_LIMIT = 9; - - /* - * The application layer service on the remote node received your request, but considered your request somehow invalid - */ - BAD_REQUEST = 32; - - /* - * The application layer service on the remote node received your request, but considered your request not authorized - * (i.e you did not send the request on the required bound channel) - */ - NOT_AUTHORIZED = 33; - - /* - * The client specified a PKI transport, but the node was unable to send the packet using PKI (and did not send the message at all) - */ - PKI_FAILED = 34; - - /* - * The receiving node does not have a Public Key to decode with - */ - PKI_UNKNOWN_PUBKEY = 35; - - /* - * Admin packet otherwise checks out, but uses a bogus or expired session key - */ - ADMIN_BAD_SESSION_KEY = 36; - - /* - * Admin packet sent using PKC, but not from a public key on the admin key list - */ - ADMIN_PUBLIC_KEY_UNAUTHORIZED = 37; - - /* - * Airtime fairness rate limit exceeded for a packet - * This typically enforced per portnum and is used to prevent a single node from monopolizing airtime - */ - RATE_LIMIT_EXCEEDED = 38; - } - - oneof variant { - /* - * A route request going from the requester - */ - RouteDiscovery route_request = 1; - - /* - * A route reply - */ - RouteDiscovery route_reply = 2; - - /* - * A failure in delivering a message (usually used for routing control messages, but might be provided - * in addition to ack.fail_id to provide details on the type of failure). - */ - Error error_reason = 3; - } -} - -/* - * (Formerly called SubPacket) - * The payload portion fo a packet, this is the actual bytes that are sent - * inside a radio packet (because from/to are broken out by the comms library) - */ -message Data { - /* - * Formerly named typ and of type Type - */ - PortNum portnum = 1; - - /* - * TODO: REPLACE - */ - bytes payload = 2; - - /* - * Not normally used, but for testing a sender can request that recipient - * responds in kind (i.e. if it received a position, it should unicast back it's position). - * Note: that if you set this on a broadcast you will receive many replies. - */ - bool want_response = 3; - - /* - * The address of the destination node. - * This field is is filled in by the mesh radio device software, application - * layer software should never need it. - * RouteDiscovery messages _must_ populate this. - * Other message types might need to if they are doing multihop routing. - */ - fixed32 dest = 4; - - /* - * The address of the original sender for this message. - * This field should _only_ be populated for reliable multihop packets (to keep - * packets small). - */ - fixed32 source = 5; - - /* - * Only used in routing or response messages. - * Indicates the original message ID that this message is reporting failure on. (formerly called original_id) - */ - fixed32 request_id = 6; - - /* - * If set, this message is intened to be a reply to a previously sent message with the defined id. - */ - fixed32 reply_id = 7; - - /* - * Defaults to false. If true, then what is in the payload should be treated as an emoji like giving - * a message a heart or poop emoji. - */ - fixed32 emoji = 8; - - /* - * Bitfield for extra flags. First use is to indicate that user approves the packet being uploaded to MQTT. - */ - optional uint32 bitfield = 9; -} - -/* - * The actual over-the-mesh message doing KeyVerification - */ -message KeyVerification { - /* - * random value Selected by the requesting node - */ - uint64 nonce = 1; - - /* - * The final authoritative hash, only to be sent by NodeA at the end of the handshake - */ - bytes hash1 = 2; - - /* - * The intermediary hash (actually derived from hash1), - * sent from NodeB to NodeA in response to the initial message. - */ - bytes hash2 = 3; -} - -/* - * Waypoint message, used to share arbitrary locations across the mesh - */ -message Waypoint { - /* - * Id of the waypoint - */ - uint32 id = 1; - - /* - * latitude_i - */ - optional sfixed32 latitude_i = 2; - - /* - * longitude_i - */ - optional sfixed32 longitude_i = 3; - - /* - * Time the waypoint is to expire (epoch) - */ - uint32 expire = 4; - - /* - * If greater than zero, treat the value as a nodenum only allowing them to update the waypoint. - * If zero, the waypoint is open to be edited by any member of the mesh. - */ - uint32 locked_to = 5; - - /* - * Name of the waypoint - max 30 chars - */ - string name = 6; - - /* - * Description of the waypoint - max 100 chars - */ - string description = 7; - - /* - * Designator icon for the waypoint in the form of a unicode emoji - */ - fixed32 icon = 8; -} - -/* - * This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server - */ -message MqttClientProxyMessage { - /* - * The MQTT topic this message will be sent /received on - */ - string topic = 1; - - /* - * The actual service envelope payload or text for mqtt pub / sub - */ - oneof payload_variant { - /* - * Bytes - */ - bytes data = 2; - - /* - * Text - */ - string text = 3; - } - - /* - * Whether the message should be retained (or not) - */ - bool retained = 4; -} - -/* - * A packet envelope sent/received over the mesh - * only payload_variant is sent in the payload portion of the LORA packet. - * The other fields are either not sent at all, or sent in the special 16 byte LORA header. - */ -message MeshPacket { - /* - * The priority of this message for sending. - * Higher priorities are sent first (when managing the transmit queue). - * This field is never sent over the air, it is only used internally inside of a local device node. - * API clients (either on the local node or connected directly to the node) - * can set this parameter if necessary. - * (values must be <= 127 to keep protobuf field to one byte in size. - * Detailed background on this field: - * I noticed a funny side effect of lora being so slow: Usually when making - * a protocol there isn’t much need to use message priority to change the order - * of transmission (because interfaces are fairly fast). - * But for lora where packets can take a few seconds each, it is very important - * to make sure that critical packets are sent ASAP. - * In the case of meshtastic that means we want to send protocol acks as soon as possible - * (to prevent unneeded retransmissions), we want routing messages to be sent next, - * then messages marked as reliable and finally 'background' packets like periodic position updates. - * So I bit the bullet and implemented a new (internal - not sent over the air) - * field in MeshPacket called 'priority'. - * And the transmission queue in the router object is now a priority queue. - */ - enum Priority { - /* - * Treated as Priority.DEFAULT - */ - UNSET = 0; - - /* - * TODO: REPLACE - */ - MIN = 1; - - /* - * Background position updates are sent with very low priority - - * if the link is super congested they might not go out at all - */ - BACKGROUND = 10; - - /* - * This priority is used for most messages that don't have a priority set - */ - DEFAULT = 64; - - /* - * If priority is unset but the message is marked as want_ack, - * assume it is important and use a slightly higher priority - */ - RELIABLE = 70; - - /* - * If priority is unset but the packet is a response to a request, we want it to get there relatively quickly. - * Furthermore, responses stop relaying packets directed to a node early. - */ - RESPONSE = 80; - - /* - * Higher priority for specific message types (portnums) to distinguish between other reliable packets. - */ - HIGH = 100; - - /* - * Higher priority alert message used for critical alerts which take priority over other reliable packets. - */ - ALERT = 110; - - /* - * Ack/naks are sent with very high priority to ensure that retransmission - * stops as soon as possible - */ - ACK = 120; - - /* - * TODO: REPLACE - */ - MAX = 127; - } - - /* - * Identify if this is a delayed packet - */ - enum Delayed { - /* - * If unset, the message is being sent in real time. - */ - NO_DELAY = 0; - - /* - * The message is delayed and was originally a broadcast - */ - DELAYED_BROADCAST = 1; - - /* - * The message is delayed and was originally a direct message - */ - DELAYED_DIRECT = 2; - } - - /* - * Enum to identify which transport mechanism this packet arrived over - */ - enum TransportMechanism { - /* - * The default case is that the node generated a packet itself - */ - TRANSPORT_INTERNAL = 0; - - /* - * Arrived via the primary LoRa radio - */ - TRANSPORT_LORA = 1; - - /* - * Arrived via a secondary LoRa radio - */ - TRANSPORT_LORA_ALT1 = 2; - - /* - * Arrived via a tertiary LoRa radio - */ - TRANSPORT_LORA_ALT2 = 3; - - /* - * Arrived via a quaternary LoRa radio - */ - TRANSPORT_LORA_ALT3 = 4; - - /* - * Arrived via an MQTT connection - */ - TRANSPORT_MQTT = 5; - - /* - * Arrived via Multicast UDP - */ - TRANSPORT_MULTICAST_UDP = 6; - - /* - * Arrived via API connection - */ - TRANSPORT_API = 7; - } - - /* - * The sending node number. - * Note: Our crypto implementation uses this field as well. - * See [crypto](/docs/overview/encryption) for details. - */ - fixed32 from = 1; - - /* - * The (immediate) destination for this packet - */ - fixed32 to = 2; - - /* - * (Usually) If set, this indicates the index in the secondary_channels table that this packet was sent/received on. - * If unset, packet was on the primary channel. - * A particular node might know only a subset of channels in use on the mesh. - * Therefore channel_index is inherently a local concept and meaningless to send between nodes. - * Very briefly, while sending and receiving deep inside the device Router code, this field instead - * contains the 'channel hash' instead of the index. - * This 'trick' is only used while the payload_variant is an 'encrypted'. - */ - uint32 channel = 3; - - /* - * Internally to the mesh radios we will route SubPackets encrypted per [this](docs/developers/firmware/encryption). - * However, when a particular node has the correct - * key to decode a particular packet, it will decode the payload into a SubPacket protobuf structure. - * Software outside of the device nodes will never encounter a packet where - * "decoded" is not populated (i.e. any encryption/decryption happens before reaching the applications) - * The numeric IDs for these fields were selected to keep backwards compatibility with old applications. - */ - - oneof payload_variant { - /* - * TODO: REPLACE - */ - Data decoded = 4; - - /* - * TODO: REPLACE - */ - bytes encrypted = 5; - } - - /* - * A unique ID for this packet. - * Always 0 for no-ack packets or non broadcast packets (and therefore take zero bytes of space). - * Otherwise a unique ID for this packet, useful for flooding algorithms. - * ID only needs to be unique on a _per sender_ basis, and it only - * needs to be unique for a few minutes (long enough to last for the length of - * any ACK or the completion of a mesh broadcast flood). - * Note: Our crypto implementation uses this id as well. - * See [crypto](/docs/overview/encryption) for details. - */ - fixed32 id = 6; - - /* - * The time this message was received by the esp32 (secs since 1970). - * Note: this field is _never_ sent on the radio link itself (to save space) Times - * are typically not sent over the mesh, but they will be added to any Packet - * (chain of SubPacket) sent to the phone (so the phone can know exact time of reception) - */ - fixed32 rx_time = 7; - - /* - * *Never* sent over the radio links. - * Set during reception to indicate the SNR of this packet. - * Used to collect statistics on current link quality. - */ - float rx_snr = 8; - - /* - * If unset treated as zero (no forwarding, send to direct neighbor nodes only) - * if 1, allow hopping through one node, etc... - * For our usecase real world topologies probably have a max of about 3. - * This field is normally placed into a few of bits in the header. - */ - uint32 hop_limit = 9; - - /* - * This packet is being sent as a reliable message, we would prefer it to arrive at the destination. - * We would like to receive a ack packet in response. - * Broadcasts messages treat this flag specially: Since acks for broadcasts would - * rapidly flood the channel, the normal ack behavior is suppressed. - * Instead, the original sender listens to see if at least one node is rebroadcasting this packet (because naive flooding algorithm). - * If it hears that the odds (given typical LoRa topologies) the odds are very high that every node should eventually receive the message. - * So FloodingRouter.cpp generates an implicit ack which is delivered to the original sender. - * If after some time we don't hear anyone rebroadcast our packet, we will timeout and retransmit, using the regular resend logic. - * Note: This flag is normally sent in a flag bit in the header when sent over the wire - */ - bool want_ack = 10; - - /* - * The priority of this message for sending. - * See MeshPacket.Priority description for more details. - */ - Priority priority = 11; - - /* - * rssi of received packet. Only sent to phone for dispay purposes. - */ - int32 rx_rssi = 12; - - /* - * Describe if this message is delayed - */ - Delayed delayed = 13 [deprecated = true]; - - /* - * Describes whether this packet passed via MQTT somewhere along the path it currently took. - */ - bool via_mqtt = 14; - - /* - * Hop limit with which the original packet started. Sent via LoRa using three bits in the unencrypted header. - * When receiving a packet, the difference between hop_start and hop_limit gives how many hops it traveled. - */ - uint32 hop_start = 15; - - /* - * Records the public key the packet was encrypted with, if applicable. - */ - bytes public_key = 16; - - /* - * Indicates whether the packet was en/decrypted using PKI - */ - bool pki_encrypted = 17; - - /* - * Last byte of the node number of the node that should be used as the next hop in routing. - * Set by the firmware internally, clients are not supposed to set this. - */ - uint32 next_hop = 18; - - /* - * Last byte of the node number of the node that will relay/relayed this packet. - * Set by the firmware internally, clients are not supposed to set this. - */ - uint32 relay_node = 19; - - /* - * *Never* sent over the radio links. - * Timestamp after which this packet may be sent. - * Set by the firmware internally, clients are not supposed to set this. - */ - uint32 tx_after = 20; - - /* - * Indicates which transport mechanism this packet arrived over - */ - TransportMechanism transport_mechanism = 21; -} - -/* - * Shared constants between device and phone - */ -enum Constants { - /* - * First enum must be zero, and we are just using this enum to - * pass int constants between two very different environments - */ - ZERO = 0; - - /* - * From mesh.options - * note: this payload length is ONLY the bytes that are sent inside of the Data protobuf (excluding protobuf overhead). The 16 byte header is - * outside of this envelope - */ - DATA_PAYLOAD_LEN = 233; -} - -/* - * The bluetooth to device link: - * Old BTLE protocol docs from TODO, merge in above and make real docs... - * use protocol buffers, and NanoPB - * messages from device to phone: - * POSITION_UPDATE (..., time) - * TEXT_RECEIVED(from, text, time) - * OPAQUE_RECEIVED(from, payload, time) (for signal messages or other applications) - * messages from phone to device: - * SET_MYID(id, human readable long, human readable short) (send down the unique ID - * string used for this node, a human readable string shown for that id, and a very - * short human readable string suitable for oled screen) SEND_OPAQUE(dest, payload) - * (for signal messages or other applications) SEND_TEXT(dest, text) Get all - * nodes() (returns list of nodes, with full info, last time seen, loc, battery - * level etc) SET_CONFIG (switches device to a new set of radio params and - * preshared key, drops all existing nodes, force our node to rejoin this new group) - * Full information about a node on the mesh - */ -message NodeInfo { - /* - * The node number - */ - uint32 num = 1; - - /* - * The user info for this node - */ - User user = 2; - - /* - * This position data. Note: before 1.2.14 we would also store the last time we've heard from this node in position.time, that is no longer true. - * Position.time now indicates the last time we received a POSITION from that node. - */ - Position position = 3; - - /* - * Returns the Signal-to-noise ratio (SNR) of the last received message, - * as measured by the receiver. Return SNR of the last received message in dB - */ - float snr = 4; - - /* - * TODO: REMOVE/INTEGRATE - * Returns the last measured frequency error. - * The LoRa receiver estimates the frequency offset between the receiver - * center frequency and that of the received LoRa signal. This function - * returns the estimates offset (in Hz) of the last received message. - * Caution: this measurement is not absolute, but is measured relative to the - * local receiver's oscillator. Apparent errors may be due to the - * transmitter, the receiver or both. \return The estimated center frequency - * offset in Hz of the last received message. - * int32 frequency_error = 6; - * enum RouteState { - * Invalid = 0; - * Discovering = 1; - * Valid = 2; - * } - * Not needed? - * RouteState route = 4; - */ - - /* - * TODO: REMOVE/INTEGRATE - * Not currently used (till full DSR deployment?) Our current preferred node node for routing - might be the same as num if - * we are direct neighbor or zero if we don't yet know a route to this node. - * fixed32 next_hop = 5; - */ - - /* - * Set to indicate the last time we received a packet from this node - */ - fixed32 last_heard = 5; - /* - * The latest device metrics for the node. - */ - DeviceMetrics device_metrics = 6; - - /* - * local channel index we heard that node on. Only populated if its not the default channel. - */ - uint32 channel = 7; - - /* - * True if we witnessed the node over MQTT instead of LoRA transport - */ - bool via_mqtt = 8; - - /* - * Number of hops away from us this node is (0 if direct neighbor) - */ - optional uint32 hops_away = 9; - - /* - * True if node is in our favorites list - * Persists between NodeDB internal clean ups - */ - bool is_favorite = 10; - - /* - * True if node is in our ignored list - * Persists between NodeDB internal clean ups - */ - bool is_ignored = 11; - - /* - * True if node public key has been verified. - * Persists between NodeDB internal clean ups - * LSB 0 of the bitfield - */ - bool is_key_manually_verified = 12; -} - -/* - * Error codes for critical errors - * The device might report these fault codes on the screen. - * If you encounter a fault code, please post on the meshtastic.discourse.group - * and we'll try to help. - */ -enum CriticalErrorCode { - /* - * TODO: REPLACE - */ - NONE = 0; - - /* - * A software bug was detected while trying to send lora - */ - TX_WATCHDOG = 1; - - /* - * A software bug was detected on entry to sleep - */ - SLEEP_ENTER_WAIT = 2; - - /* - * No Lora radio hardware could be found - */ - NO_RADIO = 3; - - /* - * Not normally used - */ - UNSPECIFIED = 4; - - /* - * We failed while configuring a UBlox GPS - */ - UBLOX_UNIT_FAILED = 5; - - /* - * This board was expected to have a power management chip and it is missing or broken - */ - NO_AXP192 = 6; - - /* - * The channel tried to set a radio setting which is not supported by this chipset, - * radio comms settings are now undefined. - */ - INVALID_RADIO_SETTING = 7; - - /* - * Radio transmit hardware failure. We sent data to the radio chip, but it didn't - * reply with an interrupt. - */ - TRANSMIT_FAILED = 8; - - /* - * We detected that the main CPU voltage dropped below the minimum acceptable value - */ - BROWNOUT = 9; - - /* Selftest of SX1262 radio chip failed */ - SX1262_FAILURE = 10; - - /* - * A (likely software but possibly hardware) failure was detected while trying to send packets. - * If this occurs on your board, please post in the forum so that we can ask you to collect some information to allow fixing this bug - */ - RADIO_SPI_BUG = 11; - - /* - * Corruption was detected on the flash filesystem but we were able to repair things. - * If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. - */ - FLASH_CORRUPTION_RECOVERABLE = 12; - - /* - * Corruption was detected on the flash filesystem but we were unable to repair things. - * NOTE: Your node will probably need to be reconfigured the next time it reboots (it will lose the region code etc...) - * If you see this failure in the field please post in the forum because we are interested in seeing if this is occurring in the field. - */ - FLASH_CORRUPTION_UNRECOVERABLE = 13; -} - -/* - * Enum to indicate to clients whether this firmware is a special firmware build, like an event. - * The first 16 values are reserved for non-event special firmwares, like the Smart Citizen use case. - */ -enum FirmwareEdition { - /* - * Vanilla firmware - */ - VANILLA = 0; - - /* - * Firmware for use in the Smart Citizen environmental monitoring network - */ - SMART_CITIZEN = 1; - - /* - * Open Sauce, the maker conference held yearly in CA - */ - OPEN_SAUCE = 16; - - /* - * DEFCON, the yearly hacker conference - */ - DEFCON = 17; - - /* - * Burning Man, the yearly hippie gathering in the desert - */ - BURNING_MAN = 18; - - /* - * Hamvention, the Dayton amateur radio convention - */ - HAMVENTION = 19; - - /* - * Placeholder for DIY and unofficial events - */ - DIY_EDITION = 127; -} - -/* - * Unique local debugging info for this node - * Note: we don't include position or the user info, because that will come in the - * Sent to the phone in response to WantNodes. - */ -message MyNodeInfo { - /* - * Tells the phone what our node number is, default starting value is - * lowbyte of macaddr, but it will be fixed if that is already in use - */ - uint32 my_node_num = 1; - - /* - * The total number of reboots this node has ever encountered - * (well - since the last time we discarded preferences) - */ - uint32 reboot_count = 8; - - /* - * The minimum app version that can talk to this device. - * Phone/PC apps should compare this to their build number and if too low tell the user they must update their app - */ - uint32 min_app_version = 11; - - /* - * Unique hardware identifier for this device - */ - bytes device_id = 12; - - /* - * The PlatformIO environment used to build this firmware - */ - string pio_env = 13; - - /* - * The indicator for whether this device is running event firmware and which - */ - FirmwareEdition firmware_edition = 14; - - /* - * The number of nodes in the nodedb. - * This is used by the phone to know how many NodeInfo packets to expect on want_config - */ - uint32 nodedb_count = 15; -} - -/* - * Debug output from the device. - * To minimize the size of records inside the device code, if a time/source/level is not set - * on the message it is assumed to be a continuation of the previously sent message. - * This allows the device code to use fixed maxlen 64 byte strings for messages, - * and then extend as needed by emitting multiple records. - */ -message LogRecord { - /* - * Log levels, chosen to match python logging conventions. - */ - enum Level { - /* - * Log levels, chosen to match python logging conventions. - */ - UNSET = 0; - - /* - * Log levels, chosen to match python logging conventions. - */ - CRITICAL = 50; - - /* - * Log levels, chosen to match python logging conventions. - */ - ERROR = 40; - - /* - * Log levels, chosen to match python logging conventions. - */ - WARNING = 30; - - /* - * Log levels, chosen to match python logging conventions. - */ - INFO = 20; - - /* - * Log levels, chosen to match python logging conventions. - */ - DEBUG = 10; - - /* - * Log levels, chosen to match python logging conventions. - */ - TRACE = 5; - } - - /* - * Log levels, chosen to match python logging conventions. - */ - string message = 1; - - /* - * Seconds since 1970 - or 0 for unknown/unset - */ - fixed32 time = 2; - - /* - * Usually based on thread name - if known - */ - string source = 3; - - /* - * Not yet set - */ - Level level = 4; -} - -message QueueStatus { - /* Last attempt to queue status, ErrorCode */ - int32 res = 1; - - /* Free entries in the outgoing queue */ - uint32 free = 2; - - /* Maximum entries in the outgoing queue */ - uint32 maxlen = 3; - - /* What was mesh packet id that generated this response? */ - uint32 mesh_packet_id = 4; -} - -/* - * Packets from the radio to the phone will appear on the fromRadio characteristic. - * It will support READ and NOTIFY. When a new packet arrives the device will BLE notify? - * It will sit in that descriptor until consumed by the phone, - * at which point the next item in the FIFO will be populated. - */ -message FromRadio { - /* - * The packet id, used to allow the phone to request missing read packets from the FIFO, - * see our bluetooth docs - */ - uint32 id = 1; - - /* - * Log levels, chosen to match python logging conventions. - */ - oneof payload_variant { - /* - * Log levels, chosen to match python logging conventions. - */ - MeshPacket packet = 2; - - /* - * Tells the phone what our node number is, can be -1 if we've not yet joined a mesh. - * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. - */ - MyNodeInfo my_info = 3; - - /* - * One packet is sent for each node in the on radio DB - * starts over with the first node in our DB - */ - NodeInfo node_info = 4; - - /* - * Include a part of the config (was: RadioConfig radio) - */ - Config config = 5; - - /* - * Set to send debug console output over our protobuf stream - */ - LogRecord log_record = 6; - - /* - * Sent as true once the device has finished sending all of the responses to want_config - * recipient should check if this ID matches our original request nonce, if - * not, it means your config responses haven't started yet. - * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. - */ - uint32 config_complete_id = 7; - - /* - * Sent to tell clients the radio has just rebooted. - * Set to true if present. - * Not used on all transports, currently just used for the serial console. - * NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps. - */ - bool rebooted = 8; - - /* - * Include module config - */ - ModuleConfig moduleConfig = 9; - - /* - * One packet is sent for each channel - */ - Channel channel = 10; - - /* - * Queue status info - */ - QueueStatus queueStatus = 11; - - /* - * File Transfer Chunk - */ - XModem xmodemPacket = 12; - - /* - * Device metadata message - */ - DeviceMetadata metadata = 13; - - /* - * MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT) - */ - MqttClientProxyMessage mqttClientProxyMessage = 14; - - /* - * File system manifest messages - */ - FileInfo fileInfo = 15; - - /* - * Notification message to the client - */ - ClientNotification clientNotification = 16; - - /* - * Persistent data for device-ui - */ - DeviceUIConfig deviceuiConfig = 17; - } -} - -/* - * A notification message from the device to the client - * To be used for important messages that should to be displayed to the user - * in the form of push notifications or validation messages when saving - * invalid configuration. - */ -message ClientNotification { - /* - * The id of the packet we're notifying in response to - */ - optional uint32 reply_id = 1; - - /* - * Seconds since 1970 - or 0 for unknown/unset - */ - fixed32 time = 2; - - /* - * The level type of notification - */ - LogRecord.Level level = 3; - /* - * The message body of the notification - */ - string message = 4; - - oneof payload_variant { - KeyVerificationNumberInform key_verification_number_inform = 11; - KeyVerificationNumberRequest key_verification_number_request = 12; - KeyVerificationFinal key_verification_final = 13; - DuplicatedPublicKey duplicated_public_key = 14; - LowEntropyKey low_entropy_key = 15; - } -} - -message KeyVerificationNumberInform { - uint64 nonce = 1; - string remote_longname = 2; - uint32 security_number = 3; -} -message KeyVerificationNumberRequest { - uint64 nonce = 1; - string remote_longname = 2; -} -message KeyVerificationFinal { - uint64 nonce = 1; - string remote_longname = 2; - bool isSender = 3; - string verification_characters = 4; -} -message DuplicatedPublicKey {} -message LowEntropyKey {} - -/* - * Individual File info for the device - */ -message FileInfo { - /* - * The fully qualified path of the file - */ - string file_name = 1; - - /* - * The size of the file in bytes - */ - uint32 size_bytes = 2; -} - -/* - * Packets/commands to the radio will be written (reliably) to the toRadio characteristic. - * Once the write completes the phone can assume it is handled. - */ -message ToRadio { - /* - * Log levels, chosen to match python logging conventions. - */ - oneof payload_variant { - /* - * Send this packet on the mesh - */ - MeshPacket packet = 1; - - /* - * Phone wants radio to send full node db to the phone, This is - * typically the first packet sent to the radio when the phone gets a - * bluetooth connection. The radio will respond by sending back a - * MyNodeInfo, a owner, a radio config and a series of - * FromRadio.node_infos, and config_complete - * the integer you write into this field will be reported back in the - * config_complete_id response this allows clients to never be confused by - * a stale old partially sent config. - */ - uint32 want_config_id = 3; - - /* - * Tell API server we are disconnecting now. - * This is useful for serial links where there is no hardware/protocol based notification that the client has dropped the link. - * (Sending this message is optional for clients) - */ - bool disconnect = 4; - - /* - * File Transfer Chunk - */ - - XModem xmodemPacket = 5; - - /* - * MQTT Client Proxy Message (for client / phone subscribed to MQTT sending to device) - */ - MqttClientProxyMessage mqttClientProxyMessage = 6; - - /* - * Heartbeat message (used to keep the device connection awake on serial) - */ - Heartbeat heartbeat = 7; - } -} - -/* - * Compressed message payload - */ -message Compressed { - /* - * PortNum to determine the how to handle the compressed payload. - */ - PortNum portnum = 1; - - /* - * Compressed data. - */ - bytes data = 2; -} - -/* - * Full info on edges for a single node - */ -message NeighborInfo { - /* - * The node ID of the node sending info on its neighbors - */ - uint32 node_id = 1; - /* - * Field to pass neighbor info for the next sending cycle - */ - uint32 last_sent_by_id = 2; - - /* - * Broadcast interval of the represented node (in seconds) - */ - uint32 node_broadcast_interval_secs = 3; - /* - * The list of out edges from this node - */ - repeated Neighbor neighbors = 4; -} - -/* - * A single edge in the mesh - */ -message Neighbor { - /* - * Node ID of neighbor - */ - uint32 node_id = 1; - - /* - * SNR of last heard message - */ - float snr = 2; - - /* - * Reception time (in secs since 1970) of last message that was last sent by this ID. - * Note: this is for local storage only and will not be sent out over the mesh. - */ - fixed32 last_rx_time = 3; - - /* - * Broadcast interval of this neighbor (in seconds). - * Note: this is for local storage only and will not be sent out over the mesh. - */ - uint32 node_broadcast_interval_secs = 4; -} - -/* - * Device metadata response - */ -message DeviceMetadata { - /* - * Device firmware version string - */ - string firmware_version = 1; - - /* - * Device state version - */ - uint32 device_state_version = 2; - - /* - * Indicates whether the device can shutdown CPU natively or via power management chip - */ - bool canShutdown = 3; - - /* - * Indicates that the device has native wifi capability - */ - bool hasWifi = 4; - - /* - * Indicates that the device has native bluetooth capability - */ - bool hasBluetooth = 5; - - /* - * Indicates that the device has an ethernet peripheral - */ - bool hasEthernet = 6; - - /* - * Indicates that the device's role in the mesh - */ - Config.DeviceConfig.Role role = 7; - - /* - * Indicates the device's current enabled position flags - */ - uint32 position_flags = 8; - - /* - * Device hardware model - */ - HardwareModel hw_model = 9; - - /* - * Has Remote Hardware enabled - */ - bool hasRemoteHardware = 10; - - /* - * Has PKC capabilities - */ - bool hasPKC = 11; - - /* - * Bit field of boolean for excluded modules - * (bitwise OR of ExcludedModules) - */ - uint32 excluded_modules = 12; -} - -/* - * Enum for modules excluded from a device's configuration. - * Each value represents a ModuleConfigType that can be toggled as excluded - * by setting its corresponding bit in the `excluded_modules` bitmask field. - */ -enum ExcludedModules { - /* - * Default value of 0 indicates no modules are excluded. - */ - EXCLUDED_NONE = 0x0000; - - /* - * MQTT module - */ - MQTT_CONFIG = 0x0001; - - /* - * Serial module - */ - SERIAL_CONFIG = 0x0002; - - /* - * External Notification module - */ - EXTNOTIF_CONFIG = 0x0004; - - /* - * Store and Forward module - */ - STOREFORWARD_CONFIG = 0x0008; - - /* - * Range Test module - */ - RANGETEST_CONFIG = 0x0010; - - /* - * Telemetry module - */ - TELEMETRY_CONFIG = 0x0020; - - /* - * Canned Message module - */ - CANNEDMSG_CONFIG = 0x0040; - - /* - * Audio module - */ - AUDIO_CONFIG = 0x0080; - - /* - * Remote Hardware module - */ - REMOTEHARDWARE_CONFIG = 0x0100; - - /* - * Neighbor Info module - */ - NEIGHBORINFO_CONFIG = 0x0200; - - /* - * Ambient Lighting module - */ - AMBIENTLIGHTING_CONFIG = 0x0400; - - /* - * Detection Sensor module - */ - DETECTIONSENSOR_CONFIG = 0x0800; - - /* - * Paxcounter module - */ - PAXCOUNTER_CONFIG = 0x1000; - - /* - * Bluetooth config (not technically a module, but used to indicate bluetooth capabilities) - */ - BLUETOOTH_CONFIG = 0x2000; - - /* - * Network config (not technically a module, but used to indicate network capabilities) - */ - NETWORK_CONFIG = 0x4000; -} - -/* - * A heartbeat message is sent to the node from the client to keep the connection alive. - * This is currently only needed to keep serial connections alive, but can be used by any PhoneAPI. - */ -message Heartbeat { - /* - * The nonce of the heartbeat message - */ - uint32 nonce = 1; -} - -/* - * RemoteHardwarePins associated with a node - */ -message NodeRemoteHardwarePin { - /* - * The node_num exposing the available gpio pin - */ - uint32 node_num = 1; - - /* - * The the available gpio pin for usage with RemoteHardware module - */ - RemoteHardwarePin pin = 2; -} - -message ChunkedPayload { - /* - * The ID of the entire payload - */ - uint32 payload_id = 1; - - /* - * The total number of chunks in the payload - */ - uint32 chunk_count = 2; - - /* - * The current chunk index in the total - */ - uint32 chunk_index = 3; - - /* - * The binary data of the current chunk - */ - bytes payload_chunk = 4; -} - -/* - * Wrapper message for broken repeated oneof support - */ -message resend_chunks { - repeated uint32 chunks = 1; -} - -/* - * Responses to a ChunkedPayload request - */ -message ChunkedPayloadResponse { - /* - * The ID of the entire payload - */ - uint32 payload_id = 1; - - oneof payload_variant { - /* - * Request to transfer chunked payload - */ - bool request_transfer = 2; - - /* - * Accept the transfer chunked payload - */ - bool accept_transfer = 3; - /* - * Request missing indexes in the chunked payload - */ - resend_chunks resend_chunks = 4; - } -} diff --git a/packages/protobufs/meshtastic/module_config.options b/packages/protobufs/meshtastic/module_config.options deleted file mode 100644 index bf2a5f4b6..000000000 --- a/packages/protobufs/meshtastic/module_config.options +++ /dev/null @@ -1,29 +0,0 @@ -*CannedMessageConfig.allow_input_source max_size:16 - -*MQTTConfig.address max_size:64 -*MQTTConfig.username max_size:64 -*MQTTConfig.password max_size:32 -*MQTTConfig.root max_size:32 - -*AudioConfig.ptt_pin int_size:8 -*AudioConfig.i2s_ws int_size:8 -*AudioConfig.i2s_sd int_size:8 -*AudioConfig.i2s_din int_size:8 -*AudioConfig.i2s_sck int_size:8 - -*ExternalNotificationConfig.output_vibra int_size:8 -*ExternalNotificationConfig.output_buzzer int_size:8 -*ExternalNotificationConfig.nag_timeout int_size:16 - -*RemoteHardwareConfig.available_pins max_count:4 -*RemoteHardwarePin.name max_size:15 -*RemoteHardwarePin.gpio_pin int_size:8 - -*AmbientLightingConfig.current int_size:8 -*AmbientLightingConfig.red int_size:8 -*AmbientLightingConfig.green int_size:8 -*AmbientLightingConfig.blue int_size:8 - -*DetectionSensorConfig.monitor_pin int_size:8 -*DetectionSensorConfig.name max_size:20 -*DetectionSensorConfig.detection_trigger_type max_size:8 diff --git a/packages/protobufs/meshtastic/module_config.proto b/packages/protobufs/meshtastic/module_config.proto deleted file mode 100644 index be8a69372..000000000 --- a/packages/protobufs/meshtastic/module_config.proto +++ /dev/null @@ -1,870 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "ModuleConfigProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * Module Config - */ -message ModuleConfig { - /* - * MQTT Client Config - */ - message MQTTConfig { - /* - * If a meshtastic node is able to reach the internet it will normally attempt to gateway any channels that are marked as - * is_uplink_enabled or is_downlink_enabled. - */ - bool enabled = 1; - - /* - * The server to use for our MQTT global message gateway feature. - * If not set, the default server will be used - */ - string address = 2; - - /* - * MQTT username to use (most useful for a custom MQTT server). - * If using a custom server, this will be honoured even if empty. - * If using the default server, this will only be honoured if set, otherwise the device will use the default username - */ - string username = 3; - - /* - * MQTT password to use (most useful for a custom MQTT server). - * If using a custom server, this will be honoured even if empty. - * If using the default server, this will only be honoured if set, otherwise the device will use the default password - */ - string password = 4; - - /* - * Whether to send encrypted or decrypted packets to MQTT. - * This parameter is only honoured if you also set server - * (the default official mqtt.meshtastic.org server can handle encrypted packets) - * Decrypted packets may be useful for external systems that want to consume meshtastic packets - */ - bool encryption_enabled = 5; - - /* - * Whether to send / consume json packets on MQTT - */ - bool json_enabled = 6; - - /* - * If true, we attempt to establish a secure connection using TLS - */ - bool tls_enabled = 7; - - /* - * The root topic to use for MQTT messages. Default is "msh". - * This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs - */ - string root = 8; - - /* - * If true, we can use the connected phone / client to proxy messages to MQTT instead of a direct connection - */ - bool proxy_to_client_enabled = 9; - - /* - * If true, we will periodically report unencrypted information about our node to a map via MQTT - */ - bool map_reporting_enabled = 10; - - /* - * Settings for reporting information about our node to a map via MQTT - */ - MapReportSettings map_report_settings = 11; - } - - /* - * Settings for reporting unencrypted information about our node to a map via MQTT - */ - message MapReportSettings { - /* - * How often we should report our info to the map (in seconds) - */ - uint32 publish_interval_secs = 1; - - /* - * Bits of precision for the location sent (default of 32 is full precision). - */ - uint32 position_precision = 2; - - /* - * Whether we have opted-in to report our location to the map - */ - bool should_report_location = 3; - } - - /* - * RemoteHardwareModule Config - */ - message RemoteHardwareConfig { - /* - * Whether the Module is enabled - */ - bool enabled = 1; - - /* - * Whether the Module allows consumers to read / write to pins not defined in available_pins - */ - bool allow_undefined_pin_access = 2; - - /* - * Exposes the available pins to the mesh for reading and writing - */ - repeated RemoteHardwarePin available_pins = 3; - } - - /* - * NeighborInfoModule Config - */ - message NeighborInfoConfig { - /* - * Whether the Module is enabled - */ - bool enabled = 1; - - /* - * Interval in seconds of how often we should try to send our - * Neighbor Info (minimum is 14400, i.e., 4 hours) - */ - uint32 update_interval = 2; - - /* - * Whether in addition to sending it to MQTT and the PhoneAPI, our NeighborInfo should be transmitted over LoRa. - * Note that this is not available on a channel with default key and name. - */ - bool transmit_over_lora = 3; - } - - /* - * Detection Sensor Module Config - */ - message DetectionSensorConfig { - enum TriggerType { - // Event is triggered if pin is low - LOGIC_LOW = 0; - // Event is triggered if pin is high - LOGIC_HIGH = 1; - // Event is triggered when pin goes high to low - FALLING_EDGE = 2; - // Event is triggered when pin goes low to high - RISING_EDGE = 3; - // Event is triggered on every pin state change, low is considered to be - // "active" - EITHER_EDGE_ACTIVE_LOW = 4; - // Event is triggered on every pin state change, high is considered to be - // "active" - EITHER_EDGE_ACTIVE_HIGH = 5; - } - /* - * Whether the Module is enabled - */ - bool enabled = 1; - - /* - * Interval in seconds of how often we can send a message to the mesh when a - * trigger event is detected - */ - uint32 minimum_broadcast_secs = 2; - - /* - * Interval in seconds of how often we should send a message to the mesh - * with the current state regardless of trigger events When set to 0, only - * trigger events will be broadcasted Works as a sort of status heartbeat - * for peace of mind - */ - uint32 state_broadcast_secs = 3; - - /* - * Send ASCII bell with alert message - * Useful for triggering ext. notification on bell - */ - bool send_bell = 4; - - /* - * Friendly name used to format message sent to mesh - * Example: A name "Motion" would result in a message "Motion detected" - * Maximum length of 20 characters - */ - string name = 5; - - /* - * GPIO pin to monitor for state changes - */ - uint32 monitor_pin = 6; - - /* - * The type of trigger event to be used - */ - TriggerType detection_trigger_type = 7; - - /* - * Whether or not use INPUT_PULLUP mode for GPIO pin - * Only applicable if the board uses pull-up resistors on the pin - */ - bool use_pullup = 8; - } - - /* - * Audio Config for codec2 voice - */ - message AudioConfig { - /* - * Baudrate for codec2 voice - */ - enum Audio_Baud { - CODEC2_DEFAULT = 0; - CODEC2_3200 = 1; - CODEC2_2400 = 2; - CODEC2_1600 = 3; - CODEC2_1400 = 4; - CODEC2_1300 = 5; - CODEC2_1200 = 6; - CODEC2_700 = 7; - CODEC2_700B = 8; - } - - /* - * Whether Audio is enabled - */ - bool codec2_enabled = 1; - - /* - * PTT Pin - */ - uint32 ptt_pin = 2; - - /* - * The audio sample rate to use for codec2 - */ - Audio_Baud bitrate = 3; - - /* - * I2S Word Select - */ - uint32 i2s_ws = 4; - - /* - * I2S Data IN - */ - uint32 i2s_sd = 5; - - /* - * I2S Data OUT - */ - uint32 i2s_din = 6; - - /* - * I2S Clock - */ - uint32 i2s_sck = 7; - } - - /* - * Config for the Paxcounter Module - */ - message PaxcounterConfig { - /* - * Enable the Paxcounter Module - */ - bool enabled = 1; - - /* - * Interval in seconds of how often we should try to send our - * metrics to the mesh - */ - - uint32 paxcounter_update_interval = 2; - - /* - * WiFi RSSI threshold. Defaults to -80 - */ - int32 wifi_threshold = 3; - - /* - * BLE RSSI threshold. Defaults to -80 - */ - int32 ble_threshold = 4; - } - - /* - * Serial Config - */ - message SerialConfig { - /* - * TODO: REPLACE - */ - enum Serial_Baud { - BAUD_DEFAULT = 0; - BAUD_110 = 1; - BAUD_300 = 2; - BAUD_600 = 3; - BAUD_1200 = 4; - BAUD_2400 = 5; - BAUD_4800 = 6; - BAUD_9600 = 7; - BAUD_19200 = 8; - BAUD_38400 = 9; - BAUD_57600 = 10; - BAUD_115200 = 11; - BAUD_230400 = 12; - BAUD_460800 = 13; - BAUD_576000 = 14; - BAUD_921600 = 15; - } - - /* - * TODO: REPLACE - */ - enum Serial_Mode { - DEFAULT = 0; - SIMPLE = 1; - PROTO = 2; - TEXTMSG = 3; - NMEA = 4; - // NMEA messages specifically tailored for CalTopo - CALTOPO = 5; - // Ecowitt WS85 weather station - WS85 = 6; - // VE.Direct is a serial protocol used by Victron Energy products - // https://beta.ivc.no/wiki/index.php/Victron_VE_Direct_DIY_Cable - VE_DIRECT = 7; - //Used to configure and view some parameters of MeshSolar. - //https://heltec.org/project/meshsolar/ - MS_CONFIG = 8; - } - - /* - * Preferences for the SerialModule - */ - bool enabled = 1; - - /* - * TODO: REPLACE - */ - bool echo = 2; - - /* - * RX pin (should match Arduino gpio pin number) - */ - uint32 rxd = 3; - - /* - * TX pin (should match Arduino gpio pin number) - */ - uint32 txd = 4; - - /* - * Serial baud rate - */ - Serial_Baud baud = 5; - - /* - * TODO: REPLACE - */ - uint32 timeout = 6; - - /* - * Mode for serial module operation - */ - Serial_Mode mode = 7; - - /* - * Overrides the platform's defacto Serial port instance to use with Serial module config settings - * This is currently only usable in output modes like NMEA / CalTopo and may behave strangely or not work at all in other modes - * Existing logging over the Serial Console will still be present - */ - bool override_console_serial_port = 8; - } - - /* - * External Notifications Config - */ - message ExternalNotificationConfig { - /* - * Enable the ExternalNotificationModule - */ - bool enabled = 1; - - /* - * When using in On/Off mode, keep the output on for this many - * milliseconds. Default 1000ms (1 second). - */ - uint32 output_ms = 2; - - /* - * Define the output pin GPIO setting Defaults to - * EXT_NOTIFY_OUT if set for the board. - * In standalone devices this pin should drive the LED to match the UI. - */ - uint32 output = 3; - - /* - * Optional: Define a secondary output pin for a vibra motor - * This is used in standalone devices to match the UI. - */ - uint32 output_vibra = 8; - - /* - * Optional: Define a tertiary output pin for an active buzzer - * This is used in standalone devices to to match the UI. - */ - uint32 output_buzzer = 9; - - /* - * IF this is true, the 'output' Pin will be pulled active high, false - * means active low. - */ - bool active = 4; - - /* - * True: Alert when a text message arrives (output) - */ - bool alert_message = 5; - - /* - * True: Alert when a text message arrives (output_vibra) - */ - bool alert_message_vibra = 10; - - /* - * True: Alert when a text message arrives (output_buzzer) - */ - bool alert_message_buzzer = 11; - - /* - * True: Alert when the bell character is received (output) - */ - bool alert_bell = 6; - - /* - * True: Alert when the bell character is received (output_vibra) - */ - bool alert_bell_vibra = 12; - - /* - * True: Alert when the bell character is received (output_buzzer) - */ - bool alert_bell_buzzer = 13; - - /* - * use a PWM output instead of a simple on/off output. This will ignore - * the 'output', 'output_ms' and 'active' settings and use the - * device.buzzer_gpio instead. - */ - bool use_pwm = 7; - - /* - * The notification will toggle with 'output_ms' for this time of seconds. - * Default is 0 which means don't repeat at all. 60 would mean blink - * and/or beep for 60 seconds - */ - uint32 nag_timeout = 14; - - /* - * When true, enables devices with native I2S audio output to use the RTTTL over speaker like a buzzer - * T-Watch S3 and T-Deck for example have this capability - */ - bool use_i2s_as_buzzer = 15; - } - - /* - * Store and Forward Module Config - */ - message StoreForwardConfig { - /* - * Enable the Store and Forward Module - */ - bool enabled = 1; - - /* - * TODO: REPLACE - */ - bool heartbeat = 2; - - /* - * TODO: REPLACE - */ - uint32 records = 3; - - /* - * TODO: REPLACE - */ - uint32 history_return_max = 4; - - /* - * TODO: REPLACE - */ - uint32 history_return_window = 5; - - /* - * Set to true to let this node act as a server that stores received messages and resends them upon request. - */ - bool is_server = 6; - } - - /* - * Preferences for the RangeTestModule - */ - message RangeTestConfig { - /* - * Enable the Range Test Module - */ - bool enabled = 1; - - /* - * Send out range test messages from this node - */ - uint32 sender = 2; - - /* - * Bool value indicating that this node should save a RangeTest.csv file. - * ESP32 Only - */ - bool save = 3; - - /* - * Bool indicating that the node should cleanup / destroy it's RangeTest.csv file. - * ESP32 Only - */ - bool clear_on_reboot = 4; - } - - /* - * Configuration for both device and environment metrics - */ - message TelemetryConfig { - /* - * Interval in seconds of how often we should try to send our - * device metrics to the mesh - */ - uint32 device_update_interval = 1; - - /* - * Interval in seconds of how often we should try to send our - * environment measurements to the mesh - */ - - uint32 environment_update_interval = 2; - - /* - * Preferences for the Telemetry Module (Environment) - * Enable/Disable the telemetry measurement module measurement collection - */ - bool environment_measurement_enabled = 3; - - /* - * Enable/Disable the telemetry measurement module on-device display - */ - bool environment_screen_enabled = 4; - - /* - * We'll always read the sensor in Celsius, but sometimes we might want to - * display the results in Fahrenheit as a "user preference". - */ - bool environment_display_fahrenheit = 5; - - /* - * Enable/Disable the air quality metrics - */ - bool air_quality_enabled = 6; - - /* - * Interval in seconds of how often we should try to send our - * air quality metrics to the mesh - */ - uint32 air_quality_interval = 7; - - /* - * Enable/disable Power metrics - */ - bool power_measurement_enabled = 8; - - /* - * Interval in seconds of how often we should try to send our - * power metrics to the mesh - */ - uint32 power_update_interval = 9; - - /* - * Enable/Disable the power measurement module on-device display - */ - bool power_screen_enabled = 10; - - /* - * Preferences for the (Health) Telemetry Module - * Enable/Disable the telemetry measurement module measurement collection - */ - bool health_measurement_enabled = 11; - - /* - * Interval in seconds of how often we should try to send our - * health metrics to the mesh - */ - uint32 health_update_interval = 12; - - /* - * Enable/Disable the health telemetry module on-device display - */ - bool health_screen_enabled = 13; - - /* - * Enable/Disable the device telemetry module to send metrics to the mesh - * Note: We will still send telemtry to the connected phone / client every minute over the API - */ - bool device_telemetry_enabled = 14; - } - - /* - * Canned Messages Module Config - */ - message CannedMessageConfig { - /* - * TODO: REPLACE - */ - enum InputEventChar { - /* - * TODO: REPLACE - */ - NONE = 0; - - /* - * TODO: REPLACE - */ - UP = 17; - - /* - * TODO: REPLACE - */ - DOWN = 18; - - /* - * TODO: REPLACE - */ - LEFT = 19; - - /* - * TODO: REPLACE - */ - RIGHT = 20; - - /* - * '\n' - */ - SELECT = 10; - - /* - * TODO: REPLACE - */ - BACK = 27; - - /* - * TODO: REPLACE - */ - CANCEL = 24; - } - - /* - * Enable the rotary encoder #1. This is a 'dumb' encoder sending pulses on both A and B pins while rotating. - */ - bool rotary1_enabled = 1; - - /* - * GPIO pin for rotary encoder A port. - */ - uint32 inputbroker_pin_a = 2; - - /* - * GPIO pin for rotary encoder B port. - */ - uint32 inputbroker_pin_b = 3; - - /* - * GPIO pin for rotary encoder Press port. - */ - uint32 inputbroker_pin_press = 4; - - /* - * Generate input event on CW of this kind. - */ - InputEventChar inputbroker_event_cw = 5; - - /* - * Generate input event on CCW of this kind. - */ - InputEventChar inputbroker_event_ccw = 6; - - /* - * Generate input event on Press of this kind. - */ - InputEventChar inputbroker_event_press = 7; - - /* - * Enable the Up/Down/Select input device. Can be RAK rotary encoder or 3 buttons. Uses the a/b/press definitions from inputbroker. - */ - bool updown1_enabled = 8; - - /* - * Enable/disable CannedMessageModule. - */ - bool enabled = 9 [deprecated = true]; - - /* - * Input event origin accepted by the canned message module. - * Can be e.g. "rotEnc1", "upDownEnc1", "scanAndSelect", "cardkb", "serialkb", or keyword "_any" - */ - string allow_input_source = 10 [deprecated = true]; - - /* - * CannedMessageModule also sends a bell character with the messages. - * ExternalNotificationModule can benefit from this feature. - */ - bool send_bell = 11; - } - - /* - Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. - Initially created for the RAK14001 RGB LED module. - */ - message AmbientLightingConfig { - /* - * Sets LED to on or off. - */ - bool led_state = 1; - - /* - * Sets the current for the LED output. Default is 10. - */ - uint32 current = 2; - - /* - * Sets the red LED level. Values are 0-255. - */ - uint32 red = 3; - - /* - * Sets the green LED level. Values are 0-255. - */ - uint32 green = 4; - - /* - * Sets the blue LED level. Values are 0-255. - */ - uint32 blue = 5; - } - - /* - * TODO: REPLACE - */ - oneof payload_variant { - /* - * TODO: REPLACE - */ - MQTTConfig mqtt = 1; - - /* - * TODO: REPLACE - */ - SerialConfig serial = 2; - - /* - * TODO: REPLACE - */ - ExternalNotificationConfig external_notification = 3; - - /* - * TODO: REPLACE - */ - StoreForwardConfig store_forward = 4; - - /* - * TODO: REPLACE - */ - RangeTestConfig range_test = 5; - - /* - * TODO: REPLACE - */ - TelemetryConfig telemetry = 6; - - /* - * TODO: REPLACE - */ - CannedMessageConfig canned_message = 7; - - /* - * TODO: REPLACE - */ - AudioConfig audio = 8; - - /* - * TODO: REPLACE - */ - RemoteHardwareConfig remote_hardware = 9; - - /* - * TODO: REPLACE - */ - NeighborInfoConfig neighbor_info = 10; - - /* - * TODO: REPLACE - */ - AmbientLightingConfig ambient_lighting = 11; - - /* - * TODO: REPLACE - */ - DetectionSensorConfig detection_sensor = 12; - - /* - * TODO: REPLACE - */ - PaxcounterConfig paxcounter = 13; - } -} - -/* - * A GPIO pin definition for remote hardware module - */ -message RemoteHardwarePin { - /* - * GPIO Pin number (must match Arduino) - */ - uint32 gpio_pin = 1; - - /* - * Name for the GPIO pin (i.e. Front gate, mailbox, etc) - */ - string name = 2; - - /* - * Type of GPIO access available to consumers on the mesh - */ - RemoteHardwarePinType type = 3; -} - -enum RemoteHardwarePinType { - /* - * Unset/unused - */ - UNKNOWN = 0; - - /* - * GPIO pin can be read (if it is high / low) - */ - DIGITAL_READ = 1; - - /* - * GPIO pin can be written to (high / low) - */ - DIGITAL_WRITE = 2; -} diff --git a/packages/protobufs/meshtastic/mqtt.options b/packages/protobufs/meshtastic/mqtt.options deleted file mode 100644 index 591e898e3..000000000 --- a/packages/protobufs/meshtastic/mqtt.options +++ /dev/null @@ -1,8 +0,0 @@ -*ServiceEnvelope.packet type:FT_POINTER -*ServiceEnvelope.channel_id type:FT_POINTER -*ServiceEnvelope.gateway_id type:FT_POINTER - -*MapReport.long_name max_size:40 -*MapReport.short_name max_size:5 -*MapReport.firmware_version max_size:18 -*MapReport.num_online_local_nodes int_size:16 \ No newline at end of file diff --git a/packages/protobufs/meshtastic/mqtt.proto b/packages/protobufs/meshtastic/mqtt.proto deleted file mode 100644 index 4edf0c431..000000000 --- a/packages/protobufs/meshtastic/mqtt.proto +++ /dev/null @@ -1,112 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -import "meshtastic/config.proto"; -import "meshtastic/mesh.proto"; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "MQTTProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * This message wraps a MeshPacket with extra metadata about the sender and how it arrived. - */ -message ServiceEnvelope { - /* - * The (probably encrypted) packet - */ - MeshPacket packet = 1; - - /* - * The global channel ID it was sent on - */ - string channel_id = 2; - - /* - * The sending gateway node ID. Can we use this to authenticate/prevent fake - * nodeid impersonation for senders? - i.e. use gateway/mesh id (which is authenticated) + local node id as - * the globally trusted nodenum - */ - string gateway_id = 3; -} - -/* - * Information about a node intended to be reported unencrypted to a map using MQTT. - */ -message MapReport { - /* - * A full name for this user, i.e. "Kevin Hester" - */ - string long_name = 1; - - /* - * A VERY short name, ideally two characters. - * Suitable for a tiny OLED screen - */ - string short_name = 2; - - /* - * Role of the node that applies specific settings for a particular use-case - */ - Config.DeviceConfig.Role role = 3; - - /* - * Hardware model of the node, i.e. T-Beam, Heltec V3, etc... - */ - HardwareModel hw_model = 4; - - /* - * Device firmware version string - */ - string firmware_version = 5; - - /* - * The region code for the radio (US, CN, EU433, etc...) - */ - Config.LoRaConfig.RegionCode region = 6; - - /* - * Modem preset used by the radio (LongFast, MediumSlow, etc...) - */ - Config.LoRaConfig.ModemPreset modem_preset = 7; - - /* - * Whether the node has a channel with default PSK and name (LongFast, MediumSlow, etc...) - * and it uses the default frequency slot given the region and modem preset. - */ - bool has_default_channel = 8; - - /* - * Latitude: multiply by 1e-7 to get degrees in floating point - */ - sfixed32 latitude_i = 9; - - /* - * Longitude: multiply by 1e-7 to get degrees in floating point - */ - sfixed32 longitude_i = 10; - - /* - * Altitude in meters above MSL - */ - int32 altitude = 11; - - /* - * Indicates the bits of precision for latitude and longitude set by the sending node - */ - uint32 position_precision = 12; - - /* - * Number of online nodes (heard in the last 2 hours) this node has in its list that were received locally (not via MQTT) - */ - uint32 num_online_local_nodes = 13; - - /* - * User has opted in to share their location (map report) with the mqtt server - * Controlled by map_report.should_report_location - */ - bool has_opted_report_location = 14; -} diff --git a/packages/protobufs/meshtastic/paxcount.proto b/packages/protobufs/meshtastic/paxcount.proto deleted file mode 100644 index 47b26398a..000000000 --- a/packages/protobufs/meshtastic/paxcount.proto +++ /dev/null @@ -1,29 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "PaxcountProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * TODO: REPLACE - */ -message Paxcount { - /* - * seen Wifi devices - */ - uint32 wifi = 1; - - /* - * Seen BLE devices - */ - uint32 ble = 2; - - /* - * Uptime in seconds - */ - uint32 uptime = 3; -} diff --git a/packages/protobufs/meshtastic/portnums.proto b/packages/protobufs/meshtastic/portnums.proto deleted file mode 100644 index e388a6f2e..000000000 --- a/packages/protobufs/meshtastic/portnums.proto +++ /dev/null @@ -1,244 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "Portnums"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * For any new 'apps' that run on the device or via sister apps on phones/PCs they should pick and use a - * unique 'portnum' for their application. - * If you are making a new app using meshtastic, please send in a pull request to add your 'portnum' to this - * master table. - * PortNums should be assigned in the following range: - * 0-63 Core Meshtastic use, do not use for third party apps - * 64-127 Registered 3rd party apps, send in a pull request that adds a new entry to portnums.proto to register your application - * 256-511 Use one of these portnums for your private applications that you don't want to register publically - * All other values are reserved. - * Note: This was formerly a Type enum named 'typ' with the same id # - * We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. - * This change is backwards compatible by treating the legacy OPAQUE/CLEAR_TEXT values identically. - */ -enum PortNum { - /* - * Deprecated: do not use in new code (formerly called OPAQUE) - * A message sent from a device outside of the mesh, in a form the mesh does not understand - * NOTE: This must be 0, because it is documented in IMeshService.aidl to be so - * ENCODING: binary undefined - */ - UNKNOWN_APP = 0; - - /* - * A simple UTF-8 text message, which even the little micros in the mesh - * can understand and show on their screen eventually in some circumstances - * even signal might send messages in this form (see below) - * ENCODING: UTF-8 Plaintext (?) - */ - TEXT_MESSAGE_APP = 1; - - /* - * Reserved for built-in GPIO/example app. - * See remote_hardware.proto/HardwareMessage for details on the message sent/received to this port number - * ENCODING: Protobuf - */ - REMOTE_HARDWARE_APP = 2; - - /* - * The built-in position messaging app. - * Payload is a Position message. - * ENCODING: Protobuf - */ - POSITION_APP = 3; - - /* - * The built-in user info app. - * Payload is a User message. - * ENCODING: Protobuf - */ - NODEINFO_APP = 4; - - /* - * Protocol control packets for mesh protocol use. - * Payload is a Routing message. - * ENCODING: Protobuf - */ - ROUTING_APP = 5; - - /* - * Admin control packets. - * Payload is a AdminMessage message. - * ENCODING: Protobuf - */ - ADMIN_APP = 6; - - /* - * Compressed TEXT_MESSAGE payloads. - * ENCODING: UTF-8 Plaintext (?) with Unishox2 Compression - * NOTE: The Device Firmware converts a TEXT_MESSAGE_APP to TEXT_MESSAGE_COMPRESSED_APP if the compressed - * payload is shorter. There's no need for app developers to do this themselves. Also the firmware will decompress - * any incoming TEXT_MESSAGE_COMPRESSED_APP payload and convert to TEXT_MESSAGE_APP. - */ - TEXT_MESSAGE_COMPRESSED_APP = 7; - - /* - * Waypoint payloads. - * Payload is a Waypoint message. - * ENCODING: Protobuf - */ - WAYPOINT_APP = 8; - - /* - * Audio Payloads. - * Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now - * ENCODING: codec2 audio frames - * NOTE: audio frames contain a 3 byte header (0xc0 0xde 0xc2) and a one byte marker for the decompressed bitrate. - * This marker comes from the 'moduleConfig.audio.bitrate' enum minus one. - */ - AUDIO_APP = 9; - - /* - * Same as Text Message but originating from Detection Sensor Module. - * NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9 - */ - DETECTION_SENSOR_APP = 10; - - /* - * Same as Text Message but used for critical alerts. - */ - ALERT_APP = 11; - - /* - * Module/port for handling key verification requests. - */ - KEY_VERIFICATION_APP = 12; - - /* - * Provides a 'ping' service that replies to any packet it receives. - * Also serves as a small example module. - * ENCODING: ASCII Plaintext - */ - REPLY_APP = 32; - - /* - * Used for the python IP tunnel feature - * ENCODING: IP Packet. Handled by the python API, firmware ignores this one and pases on. - */ - IP_TUNNEL_APP = 33; - - /* - * Paxcounter lib included in the firmware - * ENCODING: protobuf - */ - PAXCOUNTER_APP = 34; - - /* - * Provides a hardware serial interface to send and receive from the Meshtastic network. - * Connect to the RX/TX pins of a device with 38400 8N1. Packets received from the Meshtastic - * network is forwarded to the RX pin while sending a packet to TX will go out to the Mesh network. - * Maximum packet size of 240 bytes. - * Module is disabled by default can be turned on by setting SERIAL_MODULE_ENABLED = 1 in SerialPlugh.cpp. - * ENCODING: binary undefined - */ - SERIAL_APP = 64; - - /* - * STORE_FORWARD_APP (Work in Progress) - * Maintained by Jm Casler (MC Hamster) : jm@casler.org - * ENCODING: Protobuf - */ - STORE_FORWARD_APP = 65; - - /* - * Optional port for messages for the range test module. - * ENCODING: ASCII Plaintext - * NOTE: This portnum traffic is not sent to the public MQTT starting at firmware version 2.2.9 - */ - RANGE_TEST_APP = 66; - - /* - * Provides a format to send and receive telemetry data from the Meshtastic network. - * Maintained by Charles Crossan (crossan007) : crossan007@gmail.com - * ENCODING: Protobuf - */ - TELEMETRY_APP = 67; - - /* - * Experimental tools for estimating node position without a GPS - * Maintained by Github user a-f-G-U-C (a Meshtastic contributor) - * Project files at https://github.com/a-f-G-U-C/Meshtastic-ZPS - * ENCODING: arrays of int64 fields - */ - ZPS_APP = 68; - - /* - * Used to let multiple instances of Linux native applications communicate - * as if they did using their LoRa chip. - * Maintained by GitHub user GUVWAF. - * Project files at https://github.com/GUVWAF/Meshtasticator - * ENCODING: Protobuf (?) - */ - SIMULATOR_APP = 69; - - /* - * Provides a traceroute functionality to show the route a packet towards - * a certain destination would take on the mesh. Contains a RouteDiscovery message as payload. - * ENCODING: Protobuf - */ - TRACEROUTE_APP = 70; - - /* - * Aggregates edge info for the network by sending out a list of each node's neighbors - * ENCODING: Protobuf - */ - NEIGHBORINFO_APP = 71; - - /* - * ATAK Plugin - * Portnum for payloads from the official Meshtastic ATAK plugin - */ - ATAK_PLUGIN = 72; - - /* - * Provides unencrypted information about a node for consumption by a map via MQTT - */ - MAP_REPORT_APP = 73; - - /* - * PowerStress based monitoring support (for automated power consumption testing) - */ - POWERSTRESS_APP = 74; - - /* - * Reticulum Network Stack Tunnel App - * ENCODING: Fragmented RNS Packet. Handled by Meshtastic RNS interface - */ - RETICULUM_TUNNEL_APP = 76; - - /* - * App for transporting Cayenne Low Power Payload, popular for LoRaWAN sensor nodes. Offers ability to send - * arbitrary telemetry over meshtastic that is not covered by telemetry.proto - * ENCODING: CayenneLLP - */ - CAYENNE_APP = 77; - - /* - * Private applications should use portnums >= 256. - * To simplify initial development and testing you can use "PRIVATE_APP" - * in your code without needing to rebuild protobuf files (via [regen-protos.sh](https://github.com/meshtastic/firmware/blob/master/bin/regen-protos.sh)) - */ - PRIVATE_APP = 256; - - /* - * ATAK Forwarder Module https://github.com/paulmandal/atak-forwarder - * ENCODING: libcotshrink - */ - ATAK_FORWARDER = 257; - - /* - * Currently we limit port nums to no higher than this value - */ - MAX = 511; -} diff --git a/packages/protobufs/meshtastic/powermon.proto b/packages/protobufs/meshtastic/powermon.proto deleted file mode 100644 index 77206f4c8..000000000 --- a/packages/protobufs/meshtastic/powermon.proto +++ /dev/null @@ -1,103 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "PowerMonProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* Note: There are no 'PowerMon' messages normally in use (PowerMons are sent only as structured logs - slogs). - * But we wrap our State enum in this message to effectively nest a namespace (without our linter yelling at us) - */ -message PowerMon { - /* Any significant power changing event in meshtastic should be tagged with a powermon state transition. - * If you are making new meshtastic features feel free to add new entries at the end of this definition. - */ - enum State { - None = 0; - - CPU_DeepSleep = 0x01; - CPU_LightSleep = 0x02; - - /* - The external Vext1 power is on. Many boards have auxillary power rails that the CPU turns on only - occasionally. In cases where that rail has multiple devices on it we usually want to have logging on - the state of that rail as an independent record. - For instance on the Heltec Tracker 1.1 board, this rail is the power source for the GPS and screen. - - The log messages will be short and complete (see PowerMon.Event in the protobufs for details). - something like "S:PM:C,0x00001234,REASON" where the hex number is the bitmask of all current states. - (We use a bitmask for states so that if a log message gets lost it won't be fatal) - */ - Vext1_On = 0x04; - - Lora_RXOn = 0x08; - Lora_TXOn = 0x10; - Lora_RXActive = 0x20; - BT_On = 0x40; - LED_On = 0x80; - - Screen_On = 0x100; - Screen_Drawing = 0x200; - Wifi_On = 0x400; - - /* - * GPS is actively trying to find our location - * See GPSPowerState for more details - */ - GPS_Active = 0x800; - } -} - -/* - * PowerStress testing support via the C++ PowerStress module - */ -message PowerStressMessage { - /* - * What operation would we like the UUT to perform. - * note: senders should probably set want_response in their request packets, so that they can know when the state - * machine has started processing their request - */ - enum Opcode { - /* - * Unset/unused - */ - UNSET = 0; - - PRINT_INFO = 1; // Print board version slog and send an ack that we are alive and ready to process commands - FORCE_QUIET = 2; // Try to turn off all automatic processing of packets, screen, sleeping, etc (to make it easier to measure in isolation) - END_QUIET = 3; // Stop powerstress processing - probably by just rebooting the board - - SCREEN_ON = 16; // Turn the screen on - SCREEN_OFF = 17; // Turn the screen off - - CPU_IDLE = 32; // Let the CPU run but we assume mostly idling for num_seconds - CPU_DEEPSLEEP = 33; // Force deep sleep for FIXME seconds - CPU_FULLON = 34; // Spin the CPU as fast as possible for num_seconds - - LED_ON = 48; // Turn the LED on for num_seconds (and leave it on - for baseline power measurement purposes) - LED_OFF = 49; // Force the LED off for num_seconds - - LORA_OFF = 64; // Completely turn off the LORA radio for num_seconds - LORA_TX = 65; // Send Lora packets for num_seconds - LORA_RX = 66; // Receive Lora packets for num_seconds (node will be mostly just listening, unless an external agent is helping stress this by sending packets on the current channel) - - BT_OFF = 80; // Turn off the BT radio for num_seconds - BT_ON = 81; // Turn on the BT radio for num_seconds - - WIFI_OFF = 96; // Turn off the WIFI radio for num_seconds - WIFI_ON = 97; // Turn on the WIFI radio for num_seconds - - GPS_OFF = 112; // Turn off the GPS radio for num_seconds - GPS_ON = 113; // Turn on the GPS radio for num_seconds - } - - /* - * What type of HardwareMessage is this? - */ - Opcode cmd = 1; - - float num_seconds = 2; -} diff --git a/packages/protobufs/meshtastic/remote_hardware.proto b/packages/protobufs/meshtastic/remote_hardware.proto deleted file mode 100644 index ba4a69308..000000000 --- a/packages/protobufs/meshtastic/remote_hardware.proto +++ /dev/null @@ -1,75 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "RemoteHardware"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * An example app to show off the module system. This message is used for - * REMOTE_HARDWARE_APP PortNums. - * Also provides easy remote access to any GPIO. - * In the future other remote hardware operations can be added based on user interest - * (i.e. serial output, spi/i2c input/output). - * FIXME - currently this feature is turned on by default which is dangerous - * because no security yet (beyond the channel mechanism). - * It should be off by default and then protected based on some TBD mechanism - * (a special channel once multichannel support is included?) - */ -message HardwareMessage { - /* - * TODO: REPLACE - */ - enum Type { - /* - * Unset/unused - */ - UNSET = 0; - - /* - * Set gpio gpios based on gpio_mask/gpio_value - */ - WRITE_GPIOS = 1; - - /* - * We are now interested in watching the gpio_mask gpios. - * If the selected gpios change, please broadcast GPIOS_CHANGED. - * Will implicitly change the gpios requested to be INPUT gpios. - */ - WATCH_GPIOS = 2; - - /* - * The gpios listed in gpio_mask have changed, the new values are listed in gpio_value - */ - GPIOS_CHANGED = 3; - - /* - * Read the gpios specified in gpio_mask, send back a READ_GPIOS_REPLY reply with gpio_value populated - */ - READ_GPIOS = 4; - - /* - * A reply to READ_GPIOS. gpio_mask and gpio_value will be populated - */ - READ_GPIOS_REPLY = 5; - } - - /* - * What type of HardwareMessage is this? - */ - Type type = 1; - - /* - * What gpios are we changing. Not used for all MessageTypes, see MessageType for details - */ - uint64 gpio_mask = 2; - - /* - * For gpios that were listed in gpio_mask as valid, what are the signal levels for those gpios. - * Not used for all MessageTypes, see MessageType for details - */ - uint64 gpio_value = 3; -} diff --git a/packages/protobufs/meshtastic/rtttl.options b/packages/protobufs/meshtastic/rtttl.options deleted file mode 100644 index 171e426d2..000000000 --- a/packages/protobufs/meshtastic/rtttl.options +++ /dev/null @@ -1 +0,0 @@ -*RTTTLConfig.ringtone max_size:231 diff --git a/packages/protobufs/meshtastic/rtttl.proto b/packages/protobufs/meshtastic/rtttl.proto deleted file mode 100644 index 11c8b9256..000000000 --- a/packages/protobufs/meshtastic/rtttl.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "RTTTLConfigProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * Canned message module configuration. - */ -message RTTTLConfig { - /* - * Ringtone for PWM Buzzer in RTTTL Format. - */ - string ringtone = 1; -} diff --git a/packages/protobufs/meshtastic/storeforward.options b/packages/protobufs/meshtastic/storeforward.options deleted file mode 100644 index 57a122cf5..000000000 --- a/packages/protobufs/meshtastic/storeforward.options +++ /dev/null @@ -1 +0,0 @@ -*StoreAndForward.text max_size:233 \ No newline at end of file diff --git a/packages/protobufs/meshtastic/storeforward.proto b/packages/protobufs/meshtastic/storeforward.proto deleted file mode 100644 index 651eae570..000000000 --- a/packages/protobufs/meshtastic/storeforward.proto +++ /dev/null @@ -1,218 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "StoreAndForwardProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * TODO: REPLACE - */ -message StoreAndForward { - /* - * 001 - 063 = From Router - * 064 - 127 = From Client - */ - enum RequestResponse { - /* - * Unset/unused - */ - UNSET = 0; - - /* - * Router is an in error state. - */ - ROUTER_ERROR = 1; - - /* - * Router heartbeat - */ - ROUTER_HEARTBEAT = 2; - - /* - * Router has requested the client respond. This can work as a - * "are you there" message. - */ - ROUTER_PING = 3; - - /* - * The response to a "Ping" - */ - ROUTER_PONG = 4; - - /* - * Router is currently busy. Please try again later. - */ - ROUTER_BUSY = 5; - - /* - * Router is responding to a request for history. - */ - ROUTER_HISTORY = 6; - - /* - * Router is responding to a request for stats. - */ - ROUTER_STATS = 7; - - /* - * Router sends a text message from its history that was a direct message. - */ - ROUTER_TEXT_DIRECT = 8; - - /* - * Router sends a text message from its history that was a broadcast. - */ - ROUTER_TEXT_BROADCAST = 9; - - /* - * Client is an in error state. - */ - CLIENT_ERROR = 64; - - /* - * Client has requested a replay from the router. - */ - CLIENT_HISTORY = 65; - - /* - * Client has requested stats from the router. - */ - CLIENT_STATS = 66; - - /* - * Client has requested the router respond. This can work as a - * "are you there" message. - */ - CLIENT_PING = 67; - - /* - * The response to a "Ping" - */ - CLIENT_PONG = 68; - - /* - * Client has requested that the router abort processing the client's request - */ - CLIENT_ABORT = 106; - } - - /* - * TODO: REPLACE - */ - message Statistics { - /* - * Number of messages we have ever seen - */ - uint32 messages_total = 1; - - /* - * Number of messages we have currently saved our history. - */ - uint32 messages_saved = 2; - - /* - * Maximum number of messages we will save - */ - uint32 messages_max = 3; - - /* - * Router uptime in seconds - */ - uint32 up_time = 4; - - /* - * Number of times any client sent a request to the S&F. - */ - uint32 requests = 5; - - /* - * Number of times the history was requested. - */ - uint32 requests_history = 6; - - /* - * Is the heartbeat enabled on the server? - */ - bool heartbeat = 7; - - /* - * Maximum number of messages the server will return. - */ - uint32 return_max = 8; - - /* - * Maximum history window in minutes the server will return messages from. - */ - uint32 return_window = 9; - } - - /* - * TODO: REPLACE - */ - message History { - /* - * Number of that will be sent to the client - */ - uint32 history_messages = 1; - - /* - * The window of messages that was used to filter the history client requested - */ - uint32 window = 2; - - /* - * Index in the packet history of the last message sent in a previous request to the server. - * Will be sent to the client before sending the history and can be set in a subsequent request to avoid getting packets the server already sent to the client. - */ - uint32 last_request = 3; - } - - /* - * TODO: REPLACE - */ - message Heartbeat { - /* - * Period in seconds that the heartbeat is sent out that will be sent to the client - */ - uint32 period = 1; - - /* - * If set, this is not the primary Store & Forward router on the mesh - */ - uint32 secondary = 2; - } - - /* - * TODO: REPLACE - */ - RequestResponse rr = 1; - - /* - * TODO: REPLACE - */ - oneof variant { - /* - * TODO: REPLACE - */ - Statistics stats = 2; - - /* - * TODO: REPLACE - */ - History history = 3; - - /* - * TODO: REPLACE - */ - Heartbeat heartbeat = 4; - - /* - * Text from history message. - */ - bytes text = 5; - } -} diff --git a/packages/protobufs/meshtastic/telemetry.options b/packages/protobufs/meshtastic/telemetry.options deleted file mode 100644 index 81d2aa821..000000000 --- a/packages/protobufs/meshtastic/telemetry.options +++ /dev/null @@ -1,18 +0,0 @@ -# options for nanopb -# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options - -*EnvironmentMetrics.iaq int_size:16 -*EnvironmentMetrics.wind_direction int_size:16 -*EnvironmentMetrics.soil_moisture int_size:8 - -*LocalStats.num_online_nodes int_size:16 -*LocalStats.num_total_nodes int_size:16 -*LocalStats.num_tx_dropped int_size:16 - -*HealthMetrics.heart_bpm int_size:8 -*HealthMetrics.spO2 int_size:8 - -*HostMetrics.load1 int_size:16 -*HostMetrics.load5 int_size:16 -*HostMetrics.load15 int_size:16 -*HostMetrics.user_string max_size:200 diff --git a/packages/protobufs/meshtastic/telemetry.proto b/packages/protobufs/meshtastic/telemetry.proto deleted file mode 100644 index 448adf328..000000000 --- a/packages/protobufs/meshtastic/telemetry.proto +++ /dev/null @@ -1,808 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "TelemetryProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -/* - * Key native device metrics such as battery level - */ -message DeviceMetrics { - /* - * 0-100 (>100 means powered) - */ - optional uint32 battery_level = 1; - - /* - * Voltage measured - */ - optional float voltage = 2; - - /* - * Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). - */ - optional float channel_utilization = 3; - - /* - * Percent of airtime for transmission used within the last hour. - */ - optional float air_util_tx = 4; - - /* - * How long the device has been running since the last reboot (in seconds) - */ - optional uint32 uptime_seconds = 5; -} - -/* - * Weather station or other environmental metrics - */ -message EnvironmentMetrics { - /* - * Temperature measured - */ - optional float temperature = 1; - - /* - * Relative humidity percent measured - */ - optional float relative_humidity = 2; - - /* - * Barometric pressure in hPA measured - */ - optional float barometric_pressure = 3; - - /* - * Gas resistance in MOhm measured - */ - optional float gas_resistance = 4; - - /* - * Voltage measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) - */ - optional float voltage = 5; - - /* - * Current measured (To be depreciated in favor of PowerMetrics in Meshtastic 3.x) - */ - optional float current = 6; - - /* - * relative scale IAQ value as measured by Bosch BME680 . value 0-500. - * Belongs to Air Quality but is not particle but VOC measurement. Other VOC values can also be put in here. - */ - optional uint32 iaq = 7; - - /* - * RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. - */ - optional float distance = 8; - - /* - * VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. - */ - optional float lux = 9; - - /* - * VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor. - */ - optional float white_lux = 10; - - /* - * Infrared lux - */ - optional float ir_lux = 11; - - /* - * Ultraviolet lux - */ - optional float uv_lux = 12; - - /* - * Wind direction in degrees - * 0 degrees = North, 90 = East, etc... - */ - optional uint32 wind_direction = 13; - - /* - * Wind speed in m/s - */ - optional float wind_speed = 14; - - /* - * Weight in KG - */ - optional float weight = 15; - - /* - * Wind gust in m/s - */ - optional float wind_gust = 16; - - /* - * Wind lull in m/s - */ - optional float wind_lull = 17; - - /* - * Radiation in µR/h - */ - optional float radiation = 18; - - /* - * Rainfall in the last hour in mm - */ - optional float rainfall_1h = 19; - - /* - * Rainfall in the last 24 hours in mm - */ - optional float rainfall_24h = 20; - - /* - * Soil moisture measured (% 1-100) - */ - optional uint32 soil_moisture = 21; - - /* - * Soil temperature measured (*C) - */ - optional float soil_temperature = 22; -} - -/* - * Power Metrics (voltage / current / etc) - */ -message PowerMetrics { - /* - * Voltage (Ch1) - */ - optional float ch1_voltage = 1; - - /* - * Current (Ch1) - */ - optional float ch1_current = 2; - - /* - * Voltage (Ch2) - */ - optional float ch2_voltage = 3; - - /* - * Current (Ch2) - */ - optional float ch2_current = 4; - - /* - * Voltage (Ch3) - */ - optional float ch3_voltage = 5; - - /* - * Current (Ch3) - */ - optional float ch3_current = 6; - - /* - * Voltage (Ch4) - */ - optional float ch4_voltage = 7; - - /* - * Current (Ch4) - */ - optional float ch4_current = 8; - - /* - * Voltage (Ch5) - */ - optional float ch5_voltage = 9; - - /* - * Current (Ch5) - */ - optional float ch5_current = 10; - - /* - * Voltage (Ch6) - */ - optional float ch6_voltage = 11; - - /* - * Current (Ch6) - */ - optional float ch6_current = 12; - - /* - * Voltage (Ch7) - */ - optional float ch7_voltage = 13; - - /* - * Current (Ch7) - */ - optional float ch7_current = 14; - - /* - * Voltage (Ch8) - */ - optional float ch8_voltage = 15; - - /* - * Current (Ch8) - */ - optional float ch8_current = 16; -} - -/* - * Air quality metrics - */ -message AirQualityMetrics { - /* - * Concentration Units Standard PM1.0 in ug/m3 - */ - optional uint32 pm10_standard = 1; - - /* - * Concentration Units Standard PM2.5 in ug/m3 - */ - optional uint32 pm25_standard = 2; - - /* - * Concentration Units Standard PM10.0 in ug/m3 - */ - optional uint32 pm100_standard = 3; - - /* - * Concentration Units Environmental PM1.0 in ug/m3 - */ - optional uint32 pm10_environmental = 4; - - /* - * Concentration Units Environmental PM2.5 in ug/m3 - */ - optional uint32 pm25_environmental = 5; - - /* - * Concentration Units Environmental PM10.0 in ug/m3 - */ - optional uint32 pm100_environmental = 6; - - /* - * 0.3um Particle Count in #/0.1l - */ - optional uint32 particles_03um = 7; - - /* - * 0.5um Particle Count in #/0.1l - */ - optional uint32 particles_05um = 8; - - /* - * 1.0um Particle Count in #/0.1l - */ - optional uint32 particles_10um = 9; - - /* - * 2.5um Particle Count in #/0.1l - */ - optional uint32 particles_25um = 10; - - /* - * 5.0um Particle Count in #/0.1l - */ - optional uint32 particles_50um = 11; - - /* - * 10.0um Particle Count in #/0.1l - */ - optional uint32 particles_100um = 12; - - /* - * CO2 concentration in ppm - */ - optional uint32 co2 = 13; - - /* - * CO2 sensor temperature in degC - */ - optional float co2_temperature = 14; - - /* - * CO2 sensor relative humidity in % - */ - optional float co2_humidity = 15; - - /* - * Formaldehyde sensor formaldehyde concentration in ppb - */ - optional float form_formaldehyde = 16; - - /* - * Formaldehyde sensor relative humidity in %RH - */ - optional float form_humidity = 17; - - /* - * Formaldehyde sensor temperature in degrees Celsius - */ - optional float form_temperature = 18; - - /* - * Concentration Units Standard PM4.0 in ug/m3 - */ - optional uint32 pm40_standard = 19; - - /* - * 4.0um Particle Count in #/0.1l - */ - optional uint32 particles_40um = 20; - - /* - * PM Sensor Temperature - */ - optional float pm_temperature = 21; - - /* - * PM Sensor humidity - */ - optional float pm_humidity = 22; - - /* - * PM Sensor VOC Index - */ - optional float pm_voc_idx = 23; - - /* - * PM Sensor NOx Index - */ - optional float pm_nox_idx = 24; - - /* - * Typical Particle Size in um - */ - optional float particles_tps = 25; -} - -/* - * Local device mesh statistics - */ -message LocalStats { - /* - * How long the device has been running since the last reboot (in seconds) - */ - uint32 uptime_seconds = 1; - /* - * Utilization for the current channel, including well formed TX, RX and malformed RX (aka noise). - */ - float channel_utilization = 2; - /* - * Percent of airtime for transmission used within the last hour. - */ - float air_util_tx = 3; - - /* - * Number of packets sent - */ - uint32 num_packets_tx = 4; - - /* - * Number of packets received (both good and bad) - */ - uint32 num_packets_rx = 5; - - /* - * Number of packets received that are malformed or violate the protocol - */ - uint32 num_packets_rx_bad = 6; - - /* - * Number of nodes online (in the past 2 hours) - */ - uint32 num_online_nodes = 7; - - /* - * Number of nodes total - */ - uint32 num_total_nodes = 8; - - /* - * Number of received packets that were duplicates (due to multiple nodes relaying). - * If this number is high, there are nodes in the mesh relaying packets when it's unnecessary, for example due to the ROUTER/REPEATER role. - */ - uint32 num_rx_dupe = 9; - - /* - * Number of packets we transmitted that were a relay for others (not originating from ourselves). - */ - uint32 num_tx_relay = 10; - - /* - * Number of times we canceled a packet to be relayed, because someone else did it before us. - * This will always be zero for ROUTERs/REPEATERs. If this number is high, some other node(s) is/are relaying faster than you. - */ - uint32 num_tx_relay_canceled = 11; - - /* - * Number of bytes used in the heap - */ - uint32 heap_total_bytes = 12; - - /* - * Number of bytes free in the heap - */ - uint32 heap_free_bytes = 13; - - /* - * Number of packets that were dropped because the transmit queue was full. - */ - uint32 num_tx_dropped = 14; -} - -/* - * Health telemetry metrics - */ -message HealthMetrics { - /* - * Heart rate (beats per minute) - */ - optional uint32 heart_bpm = 1; - - /* - * SpO2 (blood oxygen saturation) level - */ - optional uint32 spO2 = 2; - - /* - * Body temperature in degrees Celsius - */ - optional float temperature = 3; -} - -/* - * Linux host metrics - */ -message HostMetrics { - /* - * Host system uptime - */ - uint32 uptime_seconds = 1; - - /* - * Host system free memory - */ - uint64 freemem_bytes = 2; - - /* - * Host system disk space free for / - */ - uint64 diskfree1_bytes = 3; - - /* - * Secondary system disk space free - */ - optional uint64 diskfree2_bytes = 4; - - /* - * Tertiary disk space free - */ - optional uint64 diskfree3_bytes = 5; - - /* - * Host system one minute load in 1/100ths - */ - uint32 load1 = 6; - - /* - * Host system five minute load in 1/100ths - */ - uint32 load5 = 7; - - /* - * Host system fifteen minute load in 1/100ths - */ - uint32 load15 = 8; - - /* - * Optional User-provided string for arbitrary host system information - * that doesn't make sense as a dedicated entry. - */ - optional string user_string = 9; -} - -/* - * Types of Measurements the telemetry module is equipped to handle - */ -message Telemetry { - /* - * Seconds since 1970 - or 0 for unknown/unset - */ - fixed32 time = 1; - - oneof variant { - /* - * Key native device metrics such as battery level - */ - DeviceMetrics device_metrics = 2; - - /* - * Weather station or other environmental metrics - */ - EnvironmentMetrics environment_metrics = 3; - - /* - * Air quality metrics - */ - AirQualityMetrics air_quality_metrics = 4; - - /* - * Power Metrics - */ - PowerMetrics power_metrics = 5; - - /* - * Local device mesh statistics - */ - LocalStats local_stats = 6; - - /* - * Health telemetry metrics - */ - HealthMetrics health_metrics = 7; - - /* - * Linux host metrics - */ - HostMetrics host_metrics = 8; - } -} - -/* - * Supported I2C Sensors for telemetry in Meshtastic - */ -enum TelemetrySensorType { - /* - * No external telemetry sensor explicitly set - */ - SENSOR_UNSET = 0; - - /* - * High accuracy temperature, pressure, humidity - */ - BME280 = 1; - - /* - * High accuracy temperature, pressure, humidity, and air resistance - */ - BME680 = 2; - - /* - * Very high accuracy temperature - */ - MCP9808 = 3; - - /* - * Moderate accuracy current and voltage - */ - INA260 = 4; - - /* - * Moderate accuracy current and voltage - */ - INA219 = 5; - - /* - * High accuracy temperature and pressure - */ - BMP280 = 6; - - /* - * High accuracy temperature and humidity - */ - SHTC3 = 7; - - /* - * High accuracy pressure - */ - LPS22 = 8; - - /* - * 3-Axis magnetic sensor - */ - QMC6310 = 9; - - /* - * 6-Axis inertial measurement sensor - */ - QMI8658 = 10; - - /* - * 3-Axis magnetic sensor - */ - QMC5883L = 11; - - /* - * High accuracy temperature and humidity - */ - SHT31 = 12; - - /* - * PM2.5 air quality sensor - */ - PMSA003I = 13; - - /* - * INA3221 3 Channel Voltage / Current Sensor - */ - INA3221 = 14; - - /* - * BMP085/BMP180 High accuracy temperature and pressure (older Version of BMP280) - */ - BMP085 = 15; - - /* - * RCWL-9620 Doppler Radar Distance Sensor, used for water level detection - */ - RCWL9620 = 16; - - /* - * Sensirion High accuracy temperature and humidity - */ - SHT4X = 17; - - /* - * VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. - */ - VEML7700 = 18; - - /* - * MLX90632 non-contact IR temperature sensor. - */ - MLX90632 = 19; - - /* - * TI OPT3001 Ambient Light Sensor - */ - OPT3001 = 20; - - /* - * Lite On LTR-390UV-01 UV Light Sensor - */ - LTR390UV = 21; - - /* - * AMS TSL25911FN RGB Light Sensor - */ - TSL25911FN = 22; - - /* - * AHT10 Integrated temperature and humidity sensor - */ - AHT10 = 23; - - /* - * DFRobot Lark Weather station (temperature, humidity, pressure, wind speed and direction) - */ - DFROBOT_LARK = 24; - - /* - * NAU7802 Scale Chip or compatible - */ - NAU7802 = 25; - - /* - * BMP3XX High accuracy temperature and pressure - */ - BMP3XX = 26; - - /* - * ICM-20948 9-Axis digital motion processor - */ - ICM20948 = 27; - - /* - * MAX17048 1S lipo battery sensor (voltage, state of charge, time to go) - */ - MAX17048 = 28; - - /* - * Custom I2C sensor implementation based on https://github.com/meshtastic/i2c-sensor - */ - CUSTOM_SENSOR = 29; - - /* - * MAX30102 Pulse Oximeter and Heart-Rate Sensor - */ - MAX30102 = 30; - - /* - * MLX90614 non-contact IR temperature sensor - */ - MLX90614 = 31; - - /* - * SCD40/SCD41 CO2, humidity, temperature sensor - */ - SCD4X = 32; - - /* - * ClimateGuard RadSens, radiation, Geiger-Muller Tube - */ - RADSENS = 33; - - /* - * High accuracy current and voltage - */ - INA226 = 34; - - /* - * DFRobot Gravity tipping bucket rain gauge - */ - DFROBOT_RAIN = 35; - - /* - * Infineon DPS310 High accuracy pressure and temperature - */ - DPS310 = 36; - - /* - * RAKWireless RAK12035 Soil Moisture Sensor Module - */ - RAK12035 = 37; - - /* - * MAX17261 lipo battery gauge - */ - MAX17261 = 38; - - /* - * PCT2075 Temperature Sensor - */ - PCT2075 = 39; - - /* - * ADS1X15 ADC - */ - ADS1X15 = 40; - - /* - * ADS1X15 ADC_ALT - */ - ADS1X15_ALT = 41; - - /* - * Sensirion SFA30 Formaldehyde sensor - */ - SFA30 = 42; - - /* - * SEN5X PM SENSORS - */ - SEN5X = 43; - /* - * TSL2561 light sensor - */ - TSL2561 = 44; -} - -/* - * NAU7802 Telemetry configuration, for saving to flash - */ -message Nau7802Config { - /* - * The offset setting for the NAU7802 - */ - int32 zeroOffset = 1; - - /* - * The calibration factor for the NAU7802 - */ - float calibrationFactor = 2; -} diff --git a/packages/protobufs/meshtastic/xmodem.options b/packages/protobufs/meshtastic/xmodem.options deleted file mode 100644 index 3af6125a9..000000000 --- a/packages/protobufs/meshtastic/xmodem.options +++ /dev/null @@ -1,6 +0,0 @@ -# options for nanopb -# https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options - -*XModem.buffer max_size:128 -*XModem.seq int_size:16 -*XModem.crc16 int_size:16 diff --git a/packages/protobufs/meshtastic/xmodem.proto b/packages/protobufs/meshtastic/xmodem.proto deleted file mode 100644 index 732780a3f..000000000 --- a/packages/protobufs/meshtastic/xmodem.proto +++ /dev/null @@ -1,27 +0,0 @@ -syntax = "proto3"; - -package meshtastic; - -option csharp_namespace = "Meshtastic.Protobufs"; -option go_package = "github.com/meshtastic/go/generated"; -option java_outer_classname = "XmodemProtos"; -option java_package = "com.geeksville.mesh"; -option swift_prefix = ""; - -message XModem { - enum Control { - NUL = 0; - SOH = 1; - STX = 2; - EOT = 4; - ACK = 6; - NAK = 21; - CAN = 24; - CTRLZ = 26; - } - - Control control = 1; - uint32 seq = 2; - uint32 crc16 = 3; - bytes buffer = 4; -} diff --git a/packages/protobufs/nanopb.proto b/packages/protobufs/nanopb.proto deleted file mode 100644 index 1c107c16b..000000000 --- a/packages/protobufs/nanopb.proto +++ /dev/null @@ -1,185 +0,0 @@ -// Custom options for defining: -// - Maximum size of string/bytes -// - Maximum number of elements in array -// -// These are used by nanopb to generate statically allocable structures -// for memory-limited environments. - -syntax = "proto2"; - -import "google/protobuf/descriptor.proto"; - -option go_package = "github.com/meshtastic/go/generated"; -option java_package = "fi.kapsi.koti.jpa.nanopb"; - -enum FieldType { - FT_DEFAULT = 0; // Automatically decide field type, generate static field if possible. - FT_CALLBACK = 1; // Always generate a callback field. - FT_POINTER = 4; // Always generate a dynamically allocated field. - FT_STATIC = 2; // Generate a static field or raise an exception if not possible. - FT_IGNORE = 3; // Ignore the field completely. - FT_INLINE = 5; // Legacy option, use the separate 'fixed_length' option instead -} - -enum IntSize { - IS_DEFAULT = 0; // Default, 32/64bit based on type in .proto - IS_8 = 8; - IS_16 = 16; - IS_32 = 32; - IS_64 = 64; -} - -enum TypenameMangling { - M_NONE = 0; // Default, no typename mangling - M_STRIP_PACKAGE = 1; // Strip current package name - M_FLATTEN = 2; // Only use last path component - M_PACKAGE_INITIALS = 3; // Replace the package name by the initials -} - -enum DescriptorSize { - DS_AUTO = 0; // Select minimal size based on field type - DS_1 = 1; // 1 word; up to 15 byte fields, no arrays - DS_2 = 2; // 2 words; up to 4095 byte fields, 4095 entry arrays - DS_4 = 4; // 4 words; up to 2^32-1 byte fields, 2^16-1 entry arrays - DS_8 = 8; // 8 words; up to 2^32-1 entry arrays -} - -// This is the inner options message, which basically defines options for -// a field. When it is used in message or file scope, it applies to all -// fields. -message NanoPBOptions { - // Allocated size for 'bytes' and 'string' fields. - // For string fields, this should include the space for null terminator. - optional int32 max_size = 1; - - // Maximum length for 'string' fields. Setting this is equivalent - // to setting max_size to a value of length+1. - optional int32 max_length = 14; - - // Allocated number of entries in arrays ('repeated' fields) - optional int32 max_count = 2; - - // Size of integer fields. Can save some memory if you don't need - // full 32 bits for the value. - optional IntSize int_size = 7 [default = IS_DEFAULT]; - - // Force type of field (callback or static allocation) - optional FieldType type = 3 [default = FT_DEFAULT]; - - // Use long names for enums, i.e. EnumName_EnumValue. - optional bool long_names = 4 [default = true]; - - // Add 'packed' attribute to generated structs. - // Note: this cannot be used on CPUs that break on unaligned - // accesses to variables. - optional bool packed_struct = 5 [default = false]; - - // Add 'packed' attribute to generated enums. - optional bool packed_enum = 10 [default = false]; - - // Skip this message - optional bool skip_message = 6 [default = false]; - - // Generate oneof fields as normal optional fields instead of union. - optional bool no_unions = 8 [default = false]; - - // integer type tag for a message - optional uint32 msgid = 9; - - // decode oneof as anonymous union - optional bool anonymous_oneof = 11 [default = false]; - - // Proto3 singular field does not generate a "has_" flag - optional bool proto3 = 12 [default = false]; - - // Force proto3 messages to have no "has_" flag. - // This was default behavior until nanopb-0.4.0. - optional bool proto3_singular_msgs = 21 [default = false]; - - // Generate an enum->string mapping function (can take up lots of space). - optional bool enum_to_string = 13 [default = false]; - - // Generate bytes arrays with fixed length - optional bool fixed_length = 15 [default = false]; - - // Generate repeated field with fixed count - optional bool fixed_count = 16 [default = false]; - - // Generate message-level callback that is called before decoding submessages. - // This can be used to set callback fields for submsgs inside oneofs. - optional bool submsg_callback = 22 [default = false]; - - // Shorten or remove package names from type names. - // This option applies only on the file level. - optional TypenameMangling mangle_names = 17 [default = M_NONE]; - - // Data type for storage associated with callback fields. - optional string callback_datatype = 18 [default = "pb_callback_t"]; - - // Callback function used for encoding and decoding. - // Prior to nanopb-0.4.0, the callback was specified in per-field pb_callback_t - // structure. This is still supported, but does not work inside e.g. oneof or pointer - // fields. Instead, a new method allows specifying a per-message callback that - // will be called for all callback fields in a message type. - optional string callback_function = 19 [default = "pb_default_field_callback"]; - - // Select the size of field descriptors. This option has to be defined - // for the whole message, not per-field. Usually automatic selection is - // ok, but if it results in compilation errors you can increase the field - // size here. - optional DescriptorSize descriptorsize = 20 [default = DS_AUTO]; - - // Set default value for has_ fields. - optional bool default_has = 23 [default = false]; - - // Extra files to include in generated `.pb.h` - repeated string include = 24; - - // Automatic includes to exclude from generated `.pb.h` - // Same as nanopb_generator.py command line flag -x. - repeated string exclude = 26; - - // Package name that applies only for nanopb. - optional string package = 25; - - // Override type of the field in generated C code. Only to be used with related field types - optional google.protobuf.FieldDescriptorProto.Type type_override = 27; - - // Due to historical reasons, nanopb orders fields in structs by their tag number - // instead of the order in .proto. Set this to false to keep the .proto order. - // The default value will probably change to false in nanopb-0.5.0. - optional bool sort_by_tag = 28 [default = true]; - - // Set the FT_DEFAULT field conversion strategy. - // A field that can become a static member of a c struct (e.g. int, bool, etc) - // will be a a static field. - // Fields with dynamic length are converted to either a pointer or a callback. - optional FieldType fallback_type = 29 [default = FT_CALLBACK]; -} - -// Extensions to protoc 'Descriptor' type in order to define options -// inside a .proto file. -// -// Protocol Buffers extension number registry -// -------------------------------- -// Project: Nanopb -// Contact: Petteri Aimonen -// Web site: http://kapsi.fi/~jpa/nanopb -// Extensions: 1010 (all types) -// -------------------------------- - -extend google.protobuf.FileOptions { - optional NanoPBOptions nanopb_fileopt = 1010; -} - -extend google.protobuf.MessageOptions { - optional NanoPBOptions nanopb_msgopt = 1010; -} - -extend google.protobuf.EnumOptions { - optional NanoPBOptions nanopb_enumopt = 1010; -} - -extend google.protobuf.FieldOptions { - optional NanoPBOptions nanopb = 1010; -} diff --git a/packages/protobufs/package.json b/packages/protobufs/package.json deleted file mode 100644 index ee31ed5ed..000000000 --- a/packages/protobufs/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@meshtastic/protobufs-ws", - "private": true, - "version": "0.0.0", - "type": "module", - "description": "Workspace package for Meshtastic protobuf stubs (local dev only). This package is published to the JSR registry separately.", - "license": "GPL-3.0-only", - "files": [ - "./packages/ts/dist/" - ], - "exports": { - ".": { - "types": "./packages/ts/dist/mod.d.ts", - "default": "./packages/ts/dist/mod.js" - }, - "./*": "./packages/ts/dist/*" - }, - "types": "./packages/ts/dist/mod.d.ts", - "sideEffects": false, - "scripts": { - "gen": "buf generate", - "clean": "rimraf dist", - "build": "pnpm run clean && pnpm run gen" - }, - "dependencies": {}, - "devDependencies": { - "@bufbuild/protoc-gen-es": "^1.9.0", - "rimraf": "^6.0.0" - } -} diff --git a/packages/protobufs/packages/ts/README.md b/packages/protobufs/packages/ts/README.md deleted file mode 100644 index 9e3619778..000000000 --- a/packages/protobufs/packages/ts/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Meshtastic Protobuf Definitions - -[![CI](https://img.shields.io/github/actions/workflow/status/meshtastic/protobufs/ci.yml?branch=master&label=actions&logo=github&color=yellow)](https://github.com/meshtastic/protobufs/actions/workflows/ci.yml) -[![CLA assistant](https://cla-assistant.io/readme/badge/meshtastic/protobufs)](https://cla-assistant.io/meshtastic/protobufs) -[![Fiscal Contributors](https://opencollective.com/meshtastic/tiers/badge.svg?label=Fiscal%20Contributors&color=deeppink)](https://opencollective.com/meshtastic/) -[![Vercel](https://img.shields.io/static/v1?label=Powered%20by&message=Vercel&style=flat&logo=vercel&color=000000)](https://vercel.com?utm_source=meshtastic&utm_campaign=oss) - -## Overview - -The [Protobuf](https://developers.google.com/protocol-buffers) message definitions for the Meshtastic project (used by apps and the device firmware). - -**[Documentation/API Reference](https://buf.build/meshtastic/protobufs)** - -## Stats - -![Alt](https://repobeats.axiom.co/api/embed/47e9ee1d81d9c0fdd2b4b5b4c673adb1756f6db5.svg "Repobeats analytics image") diff --git a/packages/protobufs/packages/ts/deno.json b/packages/protobufs/packages/ts/deno.json deleted file mode 100644 index b5f91df02..000000000 --- a/packages/protobufs/packages/ts/deno.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "@meshtastic/protobufs", - "version": "__PACKAGE_VERSION__", - "exports": { - ".": "./mod.ts" - }, - "imports": { - "@bufbuild/protobuf": "npm:@bufbuild/protobuf@^2.9.0" - }, - "publish": { - "exclude": ["!dist"] - } -} diff --git a/packages/protobufs/packages/ts/deno.lock b/packages/protobufs/packages/ts/deno.lock deleted file mode 100644 index 4f69667ec..000000000 --- a/packages/protobufs/packages/ts/deno.lock +++ /dev/null @@ -1,384 +0,0 @@ -{ - "version": "5", - "specifiers": { - "npm:@bufbuild/protobuf@^2.9.0": "2.9.0", - "npm:tsdown@~0.15.6": "0.15.6_typescript@5.9.3_rolldown@1.0.0-beta.42", - "npm:typescript@^5.9.3": "5.9.3" - }, - "npm": { - "@babel/generator@7.28.3": { - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dependencies": [ - "@babel/parser", - "@babel/types", - "@jridgewell/gen-mapping", - "@jridgewell/trace-mapping", - "jsesc" - ] - }, - "@babel/helper-string-parser@7.27.1": { - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" - }, - "@babel/helper-validator-identifier@7.27.1": { - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" - }, - "@babel/parser@7.28.4": { - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dependencies": [ - "@babel/types" - ], - "bin": true - }, - "@babel/types@7.28.4": { - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dependencies": [ - "@babel/helper-string-parser", - "@babel/helper-validator-identifier" - ] - }, - "@bufbuild/protobuf@2.9.0": { - "integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==" - }, - "@emnapi/core@1.5.0": { - "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", - "dependencies": [ - "@emnapi/wasi-threads", - "tslib" - ] - }, - "@emnapi/runtime@1.5.0": { - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", - "dependencies": [ - "tslib" - ] - }, - "@emnapi/wasi-threads@1.1.0": { - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dependencies": [ - "tslib" - ] - }, - "@jridgewell/gen-mapping@0.3.13": { - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dependencies": [ - "@jridgewell/sourcemap-codec", - "@jridgewell/trace-mapping" - ] - }, - "@jridgewell/resolve-uri@3.1.2": { - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" - }, - "@jridgewell/sourcemap-codec@1.5.5": { - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" - }, - "@jridgewell/trace-mapping@0.3.31": { - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dependencies": [ - "@jridgewell/resolve-uri", - "@jridgewell/sourcemap-codec" - ] - }, - "@napi-rs/wasm-runtime@1.0.7": { - "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", - "dependencies": [ - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util" - ] - }, - "@oxc-project/types@0.94.0": { - "integrity": "sha512-+UgQT/4o59cZfH6Cp7G0hwmqEQ0wE+AdIwhikdwnhWI9Dp8CgSY081+Q3O67/wq3VJu8mgUEB93J9EHHn70fOw==" - }, - "@quansync/fs@0.1.5": { - "integrity": "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA==", - "dependencies": [ - "quansync" - ] - }, - "@rolldown/binding-android-arm64@1.0.0-beta.42": { - "integrity": "sha512-W5ZKF3TP3bOWuBfotAGp+UGjxOkGV7jRmIRbBA7NFjggx7Oi6vOmGDqpHEIX7kDCiry1cnIsWQaxNvWbMdkvzQ==", - "os": ["android"], - "cpu": ["arm64"] - }, - "@rolldown/binding-darwin-arm64@1.0.0-beta.42": { - "integrity": "sha512-abw/wtgJA8OCgaTlL+xJxnN/Z01BwV1rfzIp5Hh9x+IIO6xOBfPsQ0nzi0+rWx3TyZ9FZXyC7bbC+5NpQ9EaXQ==", - "os": ["darwin"], - "cpu": ["arm64"] - }, - "@rolldown/binding-darwin-x64@1.0.0-beta.42": { - "integrity": "sha512-Y/UrZIRVr8CvXVEB88t6PeC46r1K9/QdPEo2ASE/b/KBEyXIx+QbM6kv9QfQVWU2Atly2+SVsQzxQsIvuk3lZQ==", - "os": ["darwin"], - "cpu": ["x64"] - }, - "@rolldown/binding-freebsd-x64@1.0.0-beta.42": { - "integrity": "sha512-zRM0oOk7BZiy6DoWBvdV4hyEg+j6+WcBZIMHVirMEZRu8hd18kZdJkg+bjVMfCEhwpWeFUfBfZ1qcaZ5UdYzlQ==", - "os": ["freebsd"], - "cpu": ["x64"] - }, - "@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.42": { - "integrity": "sha512-6RjFaC52QNwo7ilU8C5H7swbGlgfTkG9pudXwzr3VYyT18s0C9gLg3mvc7OMPIGqNxnQ0M5lU8j6aQCk2DTRVg==", - "os": ["linux"], - "cpu": ["arm"] - }, - "@rolldown/binding-linux-arm64-gnu@1.0.0-beta.42": { - "integrity": "sha512-LMYHM5Sf6ROq+VUwHMDVX2IAuEsWTv4SnlFEedBnMGpvRuQ14lCmD4m5Q8sjyAQCgyha9oghdGoK8AEg1sXZKg==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@rolldown/binding-linux-arm64-musl@1.0.0-beta.42": { - "integrity": "sha512-/bNTYb9aKNhzdbPn3O4MK2aLv55AlrkUKPE4KNfBYjkoZUfDr4jWp7gsSlvTc5A/99V1RCm9axvt616ZzeXGyA==", - "os": ["linux"], - "cpu": ["arm64"] - }, - "@rolldown/binding-linux-x64-gnu@1.0.0-beta.42": { - "integrity": "sha512-n/SLa4h342oyeGykZdch7Y3GNCNliRPL4k5wkeZ/5eQZs+c6/ZG1SHCJQoy7bZcmxiMyaXs9HoFmv1PEKrZgWg==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@rolldown/binding-linux-x64-musl@1.0.0-beta.42": { - "integrity": "sha512-4PSd46sFzqpLHSGdaSViAb1mk55sCUMpJg+X8ittXaVocQsV3QLG/uydSH8RyL0ngHX5fy3D70LcCzlB15AgHw==", - "os": ["linux"], - "cpu": ["x64"] - }, - "@rolldown/binding-openharmony-arm64@1.0.0-beta.42": { - "integrity": "sha512-BmWoeJJyeZXmZBcfoxG6J9+rl2G7eO47qdTkAzEegj4n3aC6CBIHOuDcbE8BvhZaEjQR0nh0nJrtEDlt65Q7Sw==", - "os": ["openharmony"], - "cpu": ["arm64"] - }, - "@rolldown/binding-wasm32-wasi@1.0.0-beta.42": { - "integrity": "sha512-2Ft32F7uiDTrGZUKws6CLNTlvTWHC33l4vpXrzUucf9rYtUThAdPCOt89Pmn13tNX6AulxjGEP2R0nZjTSW3eQ==", - "dependencies": [ - "@napi-rs/wasm-runtime" - ], - "cpu": ["wasm32"] - }, - "@rolldown/binding-win32-arm64-msvc@1.0.0-beta.42": { - "integrity": "sha512-hC1kShXW/z221eG+WzQMN06KepvPbMBknF0iGR3VMYJLOe9gwnSTfGxFT5hf8XrPv7CEZqTWRd0GQpkSHRbGsw==", - "os": ["win32"], - "cpu": ["arm64"] - }, - "@rolldown/binding-win32-ia32-msvc@1.0.0-beta.42": { - "integrity": "sha512-AICBYromawouGjj+GS33369E8Vwhy6UwhQEhQ5evfS8jPCsyVvoICJatbDGDGH01dwtVGLD5eDFzPicUOVpe4g==", - "os": ["win32"], - "cpu": ["ia32"] - }, - "@rolldown/binding-win32-x64-msvc@1.0.0-beta.42": { - "integrity": "sha512-XpZ0M+tjoEiSc9c+uZR7FCnOI0uxDRNs1elGOMjeB0pUP1QmvVbZGYNsyLbLoP4u7e3VQN8rie1OQ8/mB6rcJg==", - "os": ["win32"], - "cpu": ["x64"] - }, - "@rolldown/pluginutils@1.0.0-beta.42": { - "integrity": "sha512-N7pQzk9CyE7q0bBN/q0J8s6Db279r5kUZc6d7/wWRe9/zXqC52HQovVyu6iXPIDY4BEzzgbVLhVFXrOuGJ22ZQ==" - }, - "@tybys/wasm-util@0.10.1": { - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dependencies": [ - "tslib" - ] - }, - "ansis@4.2.0": { - "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==" - }, - "ast-kit@2.1.3": { - "integrity": "sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==", - "dependencies": [ - "@babel/parser", - "pathe" - ] - }, - "birpc@2.6.1": { - "integrity": "sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==" - }, - "cac@6.7.14": { - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==" - }, - "chokidar@4.0.3": { - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dependencies": [ - "readdirp" - ] - }, - "debug@4.4.3": { - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": [ - "ms" - ] - }, - "defu@6.1.4": { - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==" - }, - "diff@8.0.2": { - "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==" - }, - "dts-resolver@2.1.2": { - "integrity": "sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg==" - }, - "empathic@2.0.0": { - "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==" - }, - "fdir@6.5.0_picomatch@4.0.3": { - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dependencies": [ - "picomatch" - ], - "optionalPeers": [ - "picomatch" - ] - }, - "get-tsconfig@4.12.0": { - "integrity": "sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==", - "dependencies": [ - "resolve-pkg-maps" - ] - }, - "hookable@5.5.3": { - "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==" - }, - "jiti@2.6.1": { - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "bin": true - }, - "jsesc@3.1.0": { - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "bin": true - }, - "magic-string@0.30.19": { - "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", - "dependencies": [ - "@jridgewell/sourcemap-codec" - ] - }, - "ms@2.1.3": { - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "pathe@2.0.3": { - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" - }, - "picomatch@4.0.3": { - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" - }, - "quansync@0.2.11": { - "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==" - }, - "readdirp@4.1.2": { - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" - }, - "resolve-pkg-maps@1.0.0": { - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==" - }, - "rolldown-plugin-dts@0.16.11_rolldown@1.0.0-beta.42_typescript@5.9.3": { - "integrity": "sha512-9IQDaPvPqTx3RjG2eQCK5GYZITo203BxKunGI80AGYicu1ySFTUyugicAaTZWRzFWh9DSnzkgNeMNbDWBbSs0w==", - "dependencies": [ - "@babel/generator", - "@babel/parser", - "@babel/types", - "ast-kit", - "birpc", - "debug", - "dts-resolver", - "get-tsconfig", - "magic-string", - "rolldown", - "typescript" - ], - "optionalPeers": [ - "typescript" - ] - }, - "rolldown@1.0.0-beta.42": { - "integrity": "sha512-xaPcckj+BbJhYLsv8gOqezc8EdMcKKe/gk8v47B0KPvgABDrQ0qmNPAiT/gh9n9Foe0bUkEv2qzj42uU5q1WRg==", - "dependencies": [ - "@oxc-project/types", - "@rolldown/pluginutils", - "ansis" - ], - "optionalDependencies": [ - "@rolldown/binding-android-arm64", - "@rolldown/binding-darwin-arm64", - "@rolldown/binding-darwin-x64", - "@rolldown/binding-freebsd-x64", - "@rolldown/binding-linux-arm-gnueabihf", - "@rolldown/binding-linux-arm64-gnu", - "@rolldown/binding-linux-arm64-musl", - "@rolldown/binding-linux-x64-gnu", - "@rolldown/binding-linux-x64-musl", - "@rolldown/binding-openharmony-arm64", - "@rolldown/binding-wasm32-wasi", - "@rolldown/binding-win32-arm64-msvc", - "@rolldown/binding-win32-ia32-msvc", - "@rolldown/binding-win32-x64-msvc" - ], - "bin": true - }, - "semver@7.7.3": { - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "bin": true - }, - "tinyexec@1.0.1": { - "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==" - }, - "tinyglobby@0.2.15_picomatch@4.0.3": { - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dependencies": [ - "fdir", - "picomatch" - ] - }, - "tree-kill@1.2.2": { - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "bin": true - }, - "tsdown@0.15.6_typescript@5.9.3_rolldown@1.0.0-beta.42": { - "integrity": "sha512-W6++O3JeV9gm3JY6P/vLiC7zzTcJbZhQxXb+p3AvRMpDOPBIg82yXULyZCcwjsihY/bFG+Qw37HkezZbP7fzUg==", - "dependencies": [ - "ansis", - "cac", - "chokidar", - "debug", - "diff", - "empathic", - "hookable", - "rolldown", - "rolldown-plugin-dts", - "semver", - "tinyexec", - "tinyglobby", - "tree-kill", - "typescript", - "unconfig" - ], - "optionalPeers": [ - "typescript" - ], - "bin": true - }, - "tslib@2.8.1": { - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "typescript@5.9.3": { - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "bin": true - }, - "unconfig@7.3.3": { - "integrity": "sha512-QCkQoOnJF8L107gxfHL0uavn7WD9b3dpBcFX6HtfQYmjw2YzWxGuFQ0N0J6tE9oguCBJn9KOvfqYDCMPHIZrBA==", - "dependencies": [ - "@quansync/fs", - "defu", - "jiti", - "quansync" - ] - } - }, - "workspace": { - "dependencies": [ - "npm:@bufbuild/protobuf@^2.9.0" - ], - "packageJson": { - "dependencies": [ - "npm:@bufbuild/protobuf@^2.9.0", - "npm:tsdown@~0.15.6", - "npm:typescript@^5.9.3" - ] - } - } -} diff --git a/packages/protobufs/packages/ts/mod.ts b/packages/protobufs/packages/ts/mod.ts deleted file mode 100644 index a3f7f2df1..000000000 --- a/packages/protobufs/packages/ts/mod.ts +++ /dev/null @@ -1,20 +0,0 @@ -export * as Admin from "./dist/admin_pb.ts"; -export * as AppOnly from "./dist/apponly_pb.ts"; -export * as ATAK from "./dist/atak_pb.ts"; -export * as CannedMessages from "./dist/cannedmessages_pb.ts"; -export * as Channel from "./dist/channel_pb.ts"; -export * as ClientOnly from "./dist/clientonly_pb.ts"; -export * as Config from "./dist/config_pb.ts"; -export * as ConnectionStatus from "./dist/connection_status_pb.ts"; -export * as LocalOnly from "./dist/localonly_pb.ts"; -export * as Mesh from "./dist/mesh_pb.ts"; -export * as ModuleConfig from "./dist/module_config_pb.ts"; -export * as Mqtt from "./dist/mqtt_pb.ts"; -export * as PaxCount from "./dist/paxcount_pb.ts"; -export * as Portnums from "./dist/portnums_pb.ts"; -export * as PowerMon from "./dist/powermon_pb.ts"; -export * as RemoteHardware from "./dist/remote_hardware_pb.ts"; -export * as Rtttl from "./dist/rtttl_pb.ts"; -export * as StoreForward from "./dist/storeforward_pb.ts"; -export * as Telemetry from "./dist/telemetry_pb.ts"; -export * as Xmodem from "./dist/xmodem_pb.ts"; diff --git a/packages/transport-mock/README.md b/packages/transport-mock/README.md new file mode 100644 index 000000000..7ed5fba6f --- /dev/null +++ b/packages/transport-mock/README.md @@ -0,0 +1,127 @@ +# @meshtastic/transport-mock + +A mock transport layer for Meshtastic applications, enabling development and testing without a physical device. + +## Installation + +```bash +pnpm add @meshtastic/transport-mock +``` + +## Usage + +### Basic Usage + +```typescript +import { MeshDevice } from "@meshtastic/core"; +import { TransportMock } from "@meshtastic/transport-mock"; + +// Create mock transport with default scenario (5 nodes in SF) +const transport = TransportMock.create(); + +// Use exactly like a real transport +const device = new MeshDevice(transport, "mock-device"); +await device.configureTwoStage(); +``` + +### With Scenarios + +```typescript +import { TransportMock, scenarios } from "@meshtastic/transport-mock"; + +// Use a predefined scenario +const transport = TransportMock.create({ + scenario: "large", // 25 nodes for stress testing +}); + +// Or use minimal for just local node +const minimal = TransportMock.create({ + scenario: "minimal", +}); +``` + +### Custom Scenario + +```typescript +import { TransportMock, createScenario, generateMeshNodes } from "@meshtastic/transport-mock"; + +const customScenario = createScenario({ + name: "my-test", + description: "Custom test mesh", + myNodeNum: 0xDEADBEEF, + centerLat: 51.5074, + centerLon: -0.1278, + nodes: generateMeshNodes(10, 51.5074, -0.1278, 5), + simulateMessages: true, + simulatePositions: true, + simulateTelemetry: true, + activityIntervalMs: 10000, +}); + +const transport = TransportMock.create({ + scenario: customScenario, +}); +``` + +### Available Scenarios + +| Scenario | Nodes | Description | +|----------|-------|-------------| +| `default` | 5 | Small mesh in San Francisco | +| `large` | 25 | Stress test with 25 nodes | +| `minimal` | 0 | Just the local device | +| `dense` | 15 | Dense urban mesh (NYC, 1km radius) | + +### Configuration Options + +```typescript +interface MockTransportOptions { + /** Scenario name or custom scenario object */ + scenario?: string | MockScenario; + + /** Override the node number for the local device */ + nodeNum?: number; + + /** Delay before sending config packets (default: 100ms) */ + configDelayMs?: number; + + /** Delay between individual packets (default: 20ms) */ + packetDelayMs?: number; + + /** Enable debug logging to console (default: false) */ + debug?: boolean; +} +``` + +## Features + +- **Full Transport Interface**: Implements `Types.Transport` from `@meshtastic/core` +- **Realistic Config Sequence**: Simulates the two-stage configuration flow +- **Mesh Simulation**: Generates realistic node info, positions, and telemetry +- **Activity Simulation**: Optionally generates periodic messages, positions, and telemetry +- **Customizable Scenarios**: Use predefined scenarios or create your own +- **Zero Hardware**: Perfect for UI development and automated testing + +## Simulated Data + +The mock transport generates realistic: + +- **Device Config**: Device, position, power, network, display, LoRa, Bluetooth, security +- **Module Config**: MQTT, serial, telemetry, canned messages, etc. +- **Channels**: Primary channel with default PSK + 7 disabled channels +- **Node Info**: User details, hardware model, positions, device metrics +- **Mesh Packets**: Text messages, position updates, telemetry data + +## Development + +```bash +# Build the package +pnpm build:npm + +# Run in debug mode +const transport = TransportMock.create({ debug: true }); +``` + +## License + +GPL-3.0-only diff --git a/packages/transport-mock/mod.ts b/packages/transport-mock/mod.ts new file mode 100644 index 000000000..a4d47745e --- /dev/null +++ b/packages/transport-mock/mod.ts @@ -0,0 +1,3 @@ +export { TransportMock, type MockTransportOptions } from "./src/transport.ts"; +export { type MockScenario, scenarios } from "./src/scenarios.ts"; +export * from "./src/generators/index.ts"; diff --git a/packages/transport-mock/package.json b/packages/transport-mock/package.json new file mode 100644 index 000000000..2c86520df --- /dev/null +++ b/packages/transport-mock/package.json @@ -0,0 +1,50 @@ +{ + "name": "@meshtastic/transport-mock", + "version": "0.1.0", + "description": "A mock transport layer for Meshtastic applications for development and testing.", + "exports": { + ".": "./mod.ts" + }, + "type": "module", + "files": [ + "package.json", + "README.md", + "LICENSE", + "dist" + ], + "main": "./dist/mod.js", + "module": "./dist/mod.js", + "types": "./dist/mod.d.ts", + "license": "GPL-3.0-only", + "tsdown": { + "entry": "mod.ts", + "dts": true, + "format": [ + "esm" + ], + "splitting": false, + "clean": true + }, + "jsrInclude": [ + "mod.ts", + "src", + "README.md", + "LICENSE" + ], + "jsrExclude": [ + "src/**/*.test.ts" + ], + "scripts": { + "preinstall": "npx only-allow pnpm", + "prepack": "cp ../../LICENSE ./LICENSE", + "clean": "rm -rf dist LICENSE", + "build:npm": "tsdown", + "publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks", + "prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr", + "publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check" + }, + "dependencies": { + "@bufbuild/protobuf": "^2.0.0", + "@meshtastic/core": "workspace:*" + } +} diff --git a/packages/transport-mock/src/generators/config.ts b/packages/transport-mock/src/generators/config.ts new file mode 100644 index 000000000..a62fa7695 --- /dev/null +++ b/packages/transport-mock/src/generators/config.ts @@ -0,0 +1,538 @@ +import { create, toBinary } from "@bufbuild/protobuf"; +import { Protobuf } from "@meshtastic/core"; + +/** + * Generate a MyNodeInfo packet for the mock device + */ +export function generateMyNodeInfo(nodeNum: number): Protobuf.Mesh.MyNodeInfo { + return create(Protobuf.Mesh.MyNodeInfoSchema, { + myNodeNum: nodeNum, + rebootCount: 1, + minAppVersion: 30200, + }); +} + +/** + * Generate device config packets + */ +export function generateDeviceConfigs(): Protobuf.Config.Config[] { + return [ + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "device", + value: create(Protobuf.Config.Config_DeviceConfigSchema, { + role: Protobuf.Config.Config_DeviceConfig_Role.CLIENT, + serialEnabled: true, + buttonGpio: 0, + buzzerGpio: 0, + rebroadcastMode: + Protobuf.Config.Config_DeviceConfig_RebroadcastMode.ALL, + nodeInfoBroadcastSecs: 10800, + doubleTapAsButtonPress: false, + isManaged: false, + disableTripleClick: false, + }), + }, + }), + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "position", + value: create(Protobuf.Config.Config_PositionConfigSchema, { + positionBroadcastSecs: 900, + positionBroadcastSmartEnabled: true, + gpsEnabled: true, + gpsUpdateInterval: 120, + gpsAttemptTime: 900, + positionFlags: 811, + rxGpio: 0, + txGpio: 0, + broadcastSmartMinimumDistance: 100, + broadcastSmartMinimumIntervalSecs: 30, + }), + }, + }), + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "power", + value: create(Protobuf.Config.Config_PowerConfigSchema, { + isPowerSaving: false, + onBatteryShutdownAfterSecs: 0, + adcMultiplierOverride: 0, + waitBluetoothSecs: 60, + sdsSecs: 4294967295, + lsSecs: 300, + minWakeSecs: 10, + deviceBatteryInaAddress: 0, + }), + }, + }), + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "network", + value: create(Protobuf.Config.Config_NetworkConfigSchema, { + wifiEnabled: false, + wifiSsid: "", + wifiPsk: "", + ntpServer: "0.pool.ntp.org", + ethEnabled: false, + addressMode: Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP, + }), + }, + }), + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "display", + value: create(Protobuf.Config.Config_DisplayConfigSchema, { + screenOnSecs: 60, + gpsFormat: + Protobuf.Config.Config_DisplayConfig_DeprecatedGpsCoordinateFormat + .UNUSED, + autoScreenCarouselSecs: 0, + compassNorthTop: false, + flipScreen: false, + units: Protobuf.Config.Config_DisplayConfig_DisplayUnits.METRIC, + oled: Protobuf.Config.Config_DisplayConfig_OledType.OLED_AUTO, + displaymode: Protobuf.Config.Config_DisplayConfig_DisplayMode.DEFAULT, + headingBold: false, + wakeOnTapOrMotion: false, + }), + }, + }), + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "lora", + value: create(Protobuf.Config.Config_LoRaConfigSchema, { + usePreset: true, + modemPreset: Protobuf.Config.Config_LoRaConfig_ModemPreset.LONG_FAST, + bandwidth: 0, + spreadFactor: 0, + codingRate: 0, + frequencyOffset: 0, + region: Protobuf.Config.Config_LoRaConfig_RegionCode.US, + hopLimit: 3, + txEnabled: true, + txPower: 30, + channelNum: 0, + sx126xRxBoostedGain: false, + }), + }, + }), + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "bluetooth", + value: create(Protobuf.Config.Config_BluetoothConfigSchema, { + enabled: true, + mode: Protobuf.Config.Config_BluetoothConfig_PairingMode.RANDOM_PIN, + fixedPin: 123456, + }), + }, + }), + create(Protobuf.Config.ConfigSchema, { + payloadVariant: { + case: "security", + value: create(Protobuf.Config.Config_SecurityConfigSchema, { + publicKey: new Uint8Array(32), + privateKey: new Uint8Array(32), + adminKey: [], + isManaged: false, + serialEnabled: true, + debugLogApiEnabled: false, + adminChannelEnabled: false, + }), + }, + }), + ]; +} + +/** + * Generate module config packets + */ +export function generateModuleConfigs(): Protobuf.ModuleConfig.ModuleConfig[] { + return [ + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "mqtt", + value: create(Protobuf.ModuleConfig.ModuleConfig_MQTTConfigSchema, { + enabled: false, + address: "", + username: "", + password: "", + encryptionEnabled: false, + jsonEnabled: false, + tlsEnabled: false, + root: "msh", + proxyToClientEnabled: false, + mapReportingEnabled: false, + }), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "serial", + value: create(Protobuf.ModuleConfig.ModuleConfig_SerialConfigSchema, { + enabled: false, + echo: false, + rxd: 0, + txd: 0, + baud: Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Baud + .BAUD_DEFAULT, + timeout: 0, + mode: Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Mode + .DEFAULT, + }), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "externalNotification", + value: create( + Protobuf.ModuleConfig.ModuleConfig_ExternalNotificationConfigSchema, + { + enabled: false, + outputMs: 1000, + output: 0, + outputVibra: 0, + outputBuzzer: 0, + active: true, + alertMessage: true, + alertMessageVibra: false, + alertMessageBuzzer: false, + alertBell: true, + alertBellVibra: false, + alertBellBuzzer: false, + usePwm: false, + nagTimeout: 0, + useI2sAsBuzzer: false, + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "storeForward", + value: create( + Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfigSchema, + { + enabled: false, + heartbeat: false, + records: 0, + historyReturnMax: 0, + historyReturnWindow: 0, + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "rangeTest", + value: create( + Protobuf.ModuleConfig.ModuleConfig_RangeTestConfigSchema, + { + enabled: false, + sender: 0, + save: false, + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "telemetry", + value: create( + Protobuf.ModuleConfig.ModuleConfig_TelemetryConfigSchema, + { + deviceUpdateInterval: 900, + environmentUpdateInterval: 900, + environmentMeasurementEnabled: false, + environmentScreenEnabled: false, + environmentDisplayFahrenheit: false, + airQualityEnabled: false, + airQualityInterval: 900, + powerMeasurementEnabled: false, + powerUpdateInterval: 900, + powerScreenEnabled: false, + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "cannedMessage", + value: create( + Protobuf.ModuleConfig.ModuleConfig_CannedMessageConfigSchema, + { + rotary1Enabled: false, + inputbrokerPinA: 0, + inputbrokerPinB: 0, + inputbrokerPinPress: 0, + inputbrokerEventCw: + Protobuf.ModuleConfig + .ModuleConfig_CannedMessageConfig_InputEventChar.NONE, + inputbrokerEventCcw: + Protobuf.ModuleConfig + .ModuleConfig_CannedMessageConfig_InputEventChar.NONE, + inputbrokerEventPress: + Protobuf.ModuleConfig + .ModuleConfig_CannedMessageConfig_InputEventChar.NONE, + updown1Enabled: false, + enabled: false, + allowInputSource: "", + sendBell: false, + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "audio", + value: create(Protobuf.ModuleConfig.ModuleConfig_AudioConfigSchema, { + codec2Enabled: false, + pttPin: 0, + bitrate: + Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud + .CODEC2_DEFAULT, + i2sWs: 0, + i2sSd: 0, + i2sDin: 0, + i2sSck: 0, + }), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "remoteHardware", + value: create( + Protobuf.ModuleConfig.ModuleConfig_RemoteHardwareConfigSchema, + { + enabled: false, + allowUndefinedPinAccess: false, + availablePins: [], + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "neighborInfo", + value: create( + Protobuf.ModuleConfig.ModuleConfig_NeighborInfoConfigSchema, + { + enabled: false, + updateInterval: 900, + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "ambientLighting", + value: create( + Protobuf.ModuleConfig.ModuleConfig_AmbientLightingConfigSchema, + { + ledState: false, + current: 10, + red: 0, + green: 0, + blue: 0, + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "detectionSensor", + value: create( + Protobuf.ModuleConfig.ModuleConfig_DetectionSensorConfigSchema, + { + enabled: false, + minimumBroadcastSecs: 0, + stateBroadcastSecs: 0, + sendBell: false, + name: "", + monitorPin: 0, + detectionTriggerType: + Protobuf.ModuleConfig + .ModuleConfig_DetectionSensorConfig_TriggerType.LOGIC_LOW, + usePullup: false, + }, + ), + }, + }), + create(Protobuf.ModuleConfig.ModuleConfigSchema, { + payloadVariant: { + case: "paxcounter", + value: create( + Protobuf.ModuleConfig.ModuleConfig_PaxcounterConfigSchema, + { + enabled: false, + paxcounterUpdateInterval: 0, + }, + ), + }, + }), + ]; +} + +/** + * Generate default channels (Primary + 7 disabled) + */ +export function generateChannels(): Protobuf.Channel.Channel[] { + const channels: Protobuf.Channel.Channel[] = []; + + // Primary channel + channels.push( + create(Protobuf.Channel.ChannelSchema, { + index: 0, + role: Protobuf.Channel.Channel_Role.PRIMARY, + settings: create(Protobuf.Channel.ChannelSettingsSchema, { + name: "", + psk: new Uint8Array([1]), // Default key + uplinkEnabled: false, + downlinkEnabled: false, + moduleSettings: create(Protobuf.Channel.ModuleSettingsSchema, { + positionPrecision: 32, + }), + }), + }), + ); + + // Secondary channels (disabled) + for (let i = 1; i < 8; i++) { + channels.push( + create(Protobuf.Channel.ChannelSchema, { + index: i, + role: Protobuf.Channel.Channel_Role.DISABLED, + settings: create(Protobuf.Channel.ChannelSettingsSchema, { + name: "", + psk: new Uint8Array([]), + uplinkEnabled: false, + downlinkEnabled: false, + }), + }), + ); + } + + return channels; +} + +/** + * Generate device metadata + */ +export function generateMetadata(): Protobuf.Mesh.DeviceMetadata { + return create(Protobuf.Mesh.DeviceMetadataSchema, { + firmwareVersion: "2.5.6.abc1234", + deviceStateVersion: 23, + canShutdown: true, + hasWifi: true, + hasBluetooth: true, + hasEthernet: false, + role: Protobuf.Config.Config_DeviceConfig_Role.CLIENT, + positionFlags: 811, + hwModel: Protobuf.Mesh.HardwareModel.TBEAM, + hasRemoteHardware: false, + }); +} + +/** + * Serialize a FromRadio message to bytes + */ +export function serializeFromRadio( + fromRadio: Protobuf.Mesh.FromRadio, +): Uint8Array { + return toBinary(Protobuf.Mesh.FromRadioSchema, fromRadio); +} + +/** + * Create a FromRadio wrapper for myInfo + */ +export function createMyInfoPacket( + myNodeInfo: Protobuf.Mesh.MyNodeInfo, +): Protobuf.Mesh.FromRadio { + return create(Protobuf.Mesh.FromRadioSchema, { + id: 1, + payloadVariant: { + case: "myInfo", + value: myNodeInfo, + }, + }); +} + +/** + * Create a FromRadio wrapper for config + */ +export function createConfigPacket( + config: Protobuf.Config.Config, + id: number, +): Protobuf.Mesh.FromRadio { + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "config", + value: config, + }, + }); +} + +/** + * Create a FromRadio wrapper for moduleConfig + */ +export function createModuleConfigPacket( + moduleConfig: Protobuf.ModuleConfig.ModuleConfig, + id: number, +): Protobuf.Mesh.FromRadio { + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "moduleConfig", + value: moduleConfig, + }, + }); +} + +/** + * Create a FromRadio wrapper for channel + */ +export function createChannelPacket( + channel: Protobuf.Channel.Channel, + id: number, +): Protobuf.Mesh.FromRadio { + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "channel", + value: channel, + }, + }); +} + +/** + * Create a FromRadio wrapper for configCompleteId + */ +export function createConfigCompletePacket( + configCompleteId: number, + id: number, +): Protobuf.Mesh.FromRadio { + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "configCompleteId", + value: configCompleteId, + }, + }); +} + +/** + * Create a FromRadio wrapper for metadata + */ +export function createMetadataPacket( + metadata: Protobuf.Mesh.DeviceMetadata, + id: number, +): Protobuf.Mesh.FromRadio { + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "metadata", + value: metadata, + }, + }); +} diff --git a/packages/transport-mock/src/generators/index.ts b/packages/transport-mock/src/generators/index.ts new file mode 100644 index 000000000..336d641ee --- /dev/null +++ b/packages/transport-mock/src/generators/index.ts @@ -0,0 +1,3 @@ +export * from "./config.ts"; +export * from "./nodes.ts"; +export * from "./packets.ts"; diff --git a/packages/transport-mock/src/generators/nodes.ts b/packages/transport-mock/src/generators/nodes.ts new file mode 100644 index 000000000..084d84443 --- /dev/null +++ b/packages/transport-mock/src/generators/nodes.ts @@ -0,0 +1,164 @@ +import { create } from "@bufbuild/protobuf"; +import { Protobuf } from "@meshtastic/core"; + +export interface MockNodeOptions { + nodeNum: number; + shortName?: string; + longName?: string; + hwModel?: Protobuf.Mesh.HardwareModel; + latitude?: number; + longitude?: number; + altitude?: number; + batteryLevel?: number; + voltage?: number; + channelUtilization?: number; + airUtilTx?: number; + snr?: number; + lastHeard?: number; + hopsAway?: number; +} + +/** + * Generate a mock node info packet + */ +export function generateNodeInfo( + options: MockNodeOptions, +): Protobuf.Mesh.NodeInfo { + const nodeNumHex = options.nodeNum + .toString(16) + .toUpperCase() + .padStart(8, "0"); + const last4 = nodeNumHex.slice(-4); + + return create(Protobuf.Mesh.NodeInfoSchema, { + num: options.nodeNum, + snr: options.snr ?? Math.random() * 10 - 2, // -2 to 8 dB + lastHeard: + options.lastHeard ?? + Math.floor(Date.now() / 1000) - Math.floor(Math.random() * 600), + hopsAway: options.hopsAway ?? Math.floor(Math.random() * 4), + user: create(Protobuf.Mesh.UserSchema, { + id: `!${nodeNumHex.toLowerCase()}`, + shortName: options.shortName ?? last4, + longName: options.longName ?? `Meshtastic ${last4}`, + hwModel: options.hwModel ?? Protobuf.Mesh.HardwareModel.TBEAM, + isLicensed: false, + role: Protobuf.Config.Config_DeviceConfig_Role.CLIENT, + publicKey: new Uint8Array(32), + }), + position: + options.latitude !== undefined + ? create(Protobuf.Mesh.PositionSchema, { + latitudeI: Math.round((options.latitude ?? 0) * 1e7), + longitudeI: Math.round((options.longitude ?? 0) * 1e7), + altitude: options.altitude ?? 0, + time: Math.floor(Date.now() / 1000), + locationSource: Protobuf.Mesh.Position_LocSource.LOC_INTERNAL, + altitudeSource: Protobuf.Mesh.Position_AltSource.ALT_INTERNAL, + satsInView: 8, + precisionBits: 32, + }) + : undefined, + deviceMetrics: create(Protobuf.Telemetry.DeviceMetricsSchema, { + batteryLevel: options.batteryLevel ?? Math.floor(Math.random() * 100), + voltage: options.voltage ?? 3.7 + Math.random() * 0.5, + channelUtilization: options.channelUtilization ?? Math.random() * 30, + airUtilTx: options.airUtilTx ?? Math.random() * 10, + uptimeSeconds: Math.floor(Math.random() * 86400), + }), + }); +} + +/** + * Create a FromRadio wrapper for nodeInfo + */ +export function createNodeInfoPacket( + nodeInfo: Protobuf.Mesh.NodeInfo, + id: number, +): Protobuf.Mesh.FromRadio { + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "nodeInfo", + value: nodeInfo, + }, + }); +} + +/** + * Generate a set of mock nodes for a mesh network + */ +export function generateMeshNodes( + count: number, + centerLat = 37.7749, + centerLon = -122.4194, + radiusKm = 5, +): MockNodeOptions[] { + const nodes: MockNodeOptions[] = []; + const names = [ + "Alpha", + "Bravo", + "Charlie", + "Delta", + "Echo", + "Foxtrot", + "Golf", + "Hotel", + "India", + "Juliet", + "Kilo", + "Lima", + "Mike", + "November", + "Oscar", + "Papa", + "Quebec", + "Romeo", + "Sierra", + "Tango", + "Uniform", + "Victor", + "Whiskey", + "X-ray", + "Yankee", + "Zulu", + ]; + + const hwModels = [ + Protobuf.Mesh.HardwareModel.TBEAM, + Protobuf.Mesh.HardwareModel.TLORA_V2, + Protobuf.Mesh.HardwareModel.HELTEC_V3, + Protobuf.Mesh.HardwareModel.RAK4631, + Protobuf.Mesh.HardwareModel.STATION_G1, + ]; + + for (let i = 0; i < count; i++) { + // Random position within radius + const angle = Math.random() * 2 * Math.PI; + const distance = Math.random() * radiusKm; + const latOffset = (distance / 111) * Math.cos(angle); + const lonOffset = + (distance / (111 * Math.cos((centerLat * Math.PI) / 180))) * + Math.sin(angle); + + const nodeNum = 0x12340000 + i + 1; + const name = names[i % names.length]; + + nodes.push({ + nodeNum, + shortName: name?.slice(0, 4) ?? `N${i}`, + longName: `${name} Node`, + hwModel: hwModels[i % hwModels.length], + latitude: centerLat + latOffset, + longitude: centerLon + lonOffset, + altitude: Math.floor(Math.random() * 500) + 10, + batteryLevel: Math.floor(Math.random() * 100), + voltage: 3.5 + Math.random() * 0.7, + channelUtilization: Math.random() * 25, + airUtilTx: Math.random() * 8, + hopsAway: Math.floor(Math.random() * 4), + }); + } + + return nodes; +} diff --git a/packages/transport-mock/src/generators/packets.ts b/packages/transport-mock/src/generators/packets.ts new file mode 100644 index 000000000..c4e016aae --- /dev/null +++ b/packages/transport-mock/src/generators/packets.ts @@ -0,0 +1,196 @@ +import { create, toBinary } from "@bufbuild/protobuf"; +import { Protobuf } from "@meshtastic/core"; + +/** + * Create a text message packet + */ +export function createTextMessagePacket( + from: number, + to: number, + message: string, + channel: number, + id: number, +): Protobuf.Mesh.FromRadio { + const dataPacket = create(Protobuf.Mesh.DataSchema, { + portnum: Protobuf.Portnums.PortNum.TEXT_MESSAGE_APP, + payload: new TextEncoder().encode(message), + }); + + const meshPacket = create(Protobuf.Mesh.MeshPacketSchema, { + from, + to, + channel, + id, + rxTime: Math.floor(Date.now() / 1000), + rxSnr: Math.random() * 10 - 2, + rxRssi: Math.floor(-50 - Math.random() * 70), + hopLimit: 3, + hopStart: 3, + payloadVariant: { + case: "decoded", + value: dataPacket, + }, + }); + + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "packet", + value: meshPacket, + }, + }); +} + +/** + * Create a position update packet + */ +export function createPositionPacket( + from: number, + latitude: number, + longitude: number, + altitude: number, + id: number, +): Protobuf.Mesh.FromRadio { + const position = create(Protobuf.Mesh.PositionSchema, { + latitudeI: Math.round(latitude * 1e7), + longitudeI: Math.round(longitude * 1e7), + altitude, + time: Math.floor(Date.now() / 1000), + locationSource: Protobuf.Mesh.Position_LocSource.LOC_INTERNAL, + satsInView: 8, + }); + + const dataPacket = create(Protobuf.Mesh.DataSchema, { + portnum: Protobuf.Portnums.PortNum.POSITION_APP, + payload: toBinary(Protobuf.Mesh.PositionSchema, position), + }); + + const meshPacket = create(Protobuf.Mesh.MeshPacketSchema, { + from, + to: 0xffffffff, // broadcast + channel: 0, + id, + rxTime: Math.floor(Date.now() / 1000), + rxSnr: Math.random() * 10 - 2, + rxRssi: Math.floor(-50 - Math.random() * 70), + hopLimit: 3, + hopStart: 3, + payloadVariant: { + case: "decoded", + value: dataPacket, + }, + }); + + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "packet", + value: meshPacket, + }, + }); +} + +/** + * Create a telemetry packet with device metrics + */ +export function createTelemetryPacket( + from: number, + batteryLevel: number, + voltage: number, + channelUtilization: number, + airUtilTx: number, + id: number, +): Protobuf.Mesh.FromRadio { + const telemetry = create(Protobuf.Telemetry.TelemetrySchema, { + time: Math.floor(Date.now() / 1000), + variant: { + case: "deviceMetrics", + value: create(Protobuf.Telemetry.DeviceMetricsSchema, { + batteryLevel, + voltage, + channelUtilization, + airUtilTx, + uptimeSeconds: Math.floor(Math.random() * 86400), + }), + }, + }); + + const dataPacket = create(Protobuf.Mesh.DataSchema, { + portnum: Protobuf.Portnums.PortNum.TELEMETRY_APP, + payload: toBinary(Protobuf.Telemetry.TelemetrySchema, telemetry), + }); + + const meshPacket = create(Protobuf.Mesh.MeshPacketSchema, { + from, + to: 0xffffffff, // broadcast + channel: 0, + id, + rxTime: Math.floor(Date.now() / 1000), + rxSnr: Math.random() * 10 - 2, + rxRssi: Math.floor(-50 - Math.random() * 70), + hopLimit: 3, + hopStart: 3, + payloadVariant: { + case: "decoded", + value: dataPacket, + }, + }); + + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "packet", + value: meshPacket, + }, + }); +} + +/** + * Create a neighbor info packet + */ +export function createNeighborInfoPacket( + from: number, + neighbors: Array<{ nodeId: number; snr: number }>, + id: number, +): Protobuf.Mesh.FromRadio { + const neighborInfo = create(Protobuf.Mesh.NeighborInfoSchema, { + nodeId: from, + lastSentById: from, + nodeBroadcastIntervalSecs: 900, + neighbors: neighbors.map((n) => + create(Protobuf.Mesh.NeighborSchema, { + nodeId: n.nodeId, + snr: n.snr, + }), + ), + }); + + const dataPacket = create(Protobuf.Mesh.DataSchema, { + portnum: Protobuf.Portnums.PortNum.NEIGHBORINFO_APP, + payload: toBinary(Protobuf.Mesh.NeighborInfoSchema, neighborInfo), + }); + + const meshPacket = create(Protobuf.Mesh.MeshPacketSchema, { + from, + to: 0xffffffff, // broadcast + channel: 0, + id, + rxTime: Math.floor(Date.now() / 1000), + rxSnr: Math.random() * 10 - 2, + rxRssi: Math.floor(-50 - Math.random() * 70), + hopLimit: 3, + hopStart: 3, + payloadVariant: { + case: "decoded", + value: dataPacket, + }, + }); + + return create(Protobuf.Mesh.FromRadioSchema, { + id, + payloadVariant: { + case: "packet", + value: meshPacket, + }, + }); +} diff --git a/packages/transport-mock/src/scenarios.ts b/packages/transport-mock/src/scenarios.ts new file mode 100644 index 000000000..a1657e479 --- /dev/null +++ b/packages/transport-mock/src/scenarios.ts @@ -0,0 +1,110 @@ +import type { MockNodeOptions } from "./generators/nodes.ts"; +import { generateMeshNodes } from "./generators/nodes.ts"; + +export interface MockScenario { + /** Name of the scenario */ + name: string; + /** Description of what this scenario tests */ + description: string; + /** Node number for the "local" device */ + myNodeNum: number; + /** Nodes in the mesh (excluding the local device) */ + nodes: MockNodeOptions[]; + /** Center latitude for the mesh */ + centerLat: number; + /** Center longitude for the mesh */ + centerLon: number; + /** Whether to simulate periodic messages */ + simulateMessages?: boolean; + /** Whether to simulate position updates */ + simulatePositions?: boolean; + /** Whether to simulate telemetry updates */ + simulateTelemetry?: boolean; + /** Interval in ms for simulated activity */ + activityIntervalMs?: number; +} + +/** + * Default scenario: Small mesh with 5 nodes in San Francisco + */ +const defaultScenario: MockScenario = { + name: "default", + description: "Small 5-node mesh in San Francisco", + myNodeNum: 0x12345678, + centerLat: 37.7749, + centerLon: -122.4194, + nodes: generateMeshNodes(5, 37.7749, -122.4194, 3), + simulateMessages: true, + simulatePositions: true, + simulateTelemetry: true, + activityIntervalMs: 30000, // 30 seconds +}; + +/** + * Large mesh scenario: 25 nodes for stress testing + */ +const largeScenario: MockScenario = { + name: "large", + description: "Large 25-node mesh for stress testing", + myNodeNum: 0x12345678, + centerLat: 37.7749, + centerLon: -122.4194, + nodes: generateMeshNodes(25, 37.7749, -122.4194, 10), + simulateMessages: true, + simulatePositions: true, + simulateTelemetry: true, + activityIntervalMs: 15000, // 15 seconds +}; + +/** + * Minimal scenario: Just the local node, no mesh + */ +const minimalScenario: MockScenario = { + name: "minimal", + description: "Minimal setup with just local node", + myNodeNum: 0x12345678, + centerLat: 37.7749, + centerLon: -122.4194, + nodes: [], + simulateMessages: false, + simulatePositions: false, + simulateTelemetry: false, +}; + +/** + * Dense urban scenario: Many nodes in close proximity + */ +const denseScenario: MockScenario = { + name: "dense", + description: "Dense urban mesh with 15 nodes in 1km radius", + myNodeNum: 0x12345678, + centerLat: 40.7128, + centerLon: -74.006, + nodes: generateMeshNodes(15, 40.7128, -74.006, 1), + simulateMessages: true, + simulatePositions: true, + simulateTelemetry: true, + activityIntervalMs: 20000, +}; + +/** + * Available scenarios + */ +export const scenarios: Record = { + default: defaultScenario, + large: largeScenario, + minimal: minimalScenario, + dense: denseScenario, +}; + +/** + * Create a custom scenario + */ +export function createScenario( + options: Partial & { name: string }, +): MockScenario { + return { + ...defaultScenario, + ...options, + }; +} diff --git a/packages/transport-mock/src/transport.ts b/packages/transport-mock/src/transport.ts new file mode 100644 index 000000000..337c12d24 --- /dev/null +++ b/packages/transport-mock/src/transport.ts @@ -0,0 +1,867 @@ +import { create, fromBinary, toBinary } from "@bufbuild/protobuf"; +import { Protobuf, Types } from "@meshtastic/core"; +import { + createChannelPacket, + createConfigCompletePacket, + createConfigPacket, + createMetadataPacket, + createModuleConfigPacket, + createMyInfoPacket, + generateChannels, + generateDeviceConfigs, + generateMetadata, + generateModuleConfigs, + generateMyNodeInfo, + serializeFromRadio, +} from "./generators/config.ts"; +import { createNodeInfoPacket, generateNodeInfo } from "./generators/nodes.ts"; +import { + createPositionPacket, + createTelemetryPacket, + createTextMessagePacket, +} from "./generators/packets.ts"; +import { type MockScenario, scenarios } from "./scenarios.ts"; + +const CONFIG_COMPLETE_STAGE1 = 69420; +const CONFIG_COMPLETE_STAGE2 = 69421; + +export interface MockTransportOptions { + /** Scenario name or custom scenario */ + scenario?: string | MockScenario; + /** Override the node number */ + nodeNum?: number; + /** Delay in ms before sending config packets (simulates real device) */ + configDelayMs?: number; + /** Delay in ms between individual packets */ + packetDelayMs?: number; + /** Enable debug logging */ + debug?: boolean; +} + +/** Internal device state for config persistence */ +interface DeviceState { + configs: Map; + moduleConfigs: Map; + channels: Map; + user: Protobuf.Mesh.User | null; +} + +/** + * Mock transport for development and testing without a real Meshtastic device. + * + * Implements the Transport interface from @meshtastic/core and simulates + * a device connection with configurable scenarios. + */ +export class TransportMock implements Types.Transport { + private _toDevice: WritableStream; + private _fromDevice: ReadableStream; + private fromDeviceController?: ReadableStreamDefaultController; + + private scenario: MockScenario; + private options: Required; + private packetId = 1; + private activityInterval?: ReturnType; + private isDisconnected = false; + + /** Device state - stores config changes made by the client */ + private deviceState: DeviceState = { + configs: new Map(), + moduleConfigs: new Map(), + channels: new Map(), + user: null, + }; + + /** + * Create a new mock transport with the specified options + */ + public static create(options?: MockTransportOptions): TransportMock { + return new TransportMock(options); + } + + constructor(options?: MockTransportOptions) { + // Resolve scenario + if (typeof options?.scenario === "string") { + this.scenario = scenarios[options.scenario] ?? scenarios.default!; + } else if (options?.scenario) { + this.scenario = options.scenario; + } else { + this.scenario = scenarios.default!; + } + + // Apply node number override + if (options?.nodeNum) { + this.scenario = { ...this.scenario, myNodeNum: options.nodeNum }; + } + + this.options = { + scenario: this.scenario, + nodeNum: this.scenario.myNodeNum, + configDelayMs: options?.configDelayMs ?? 100, + packetDelayMs: options?.packetDelayMs ?? 20, + debug: options?.debug ?? false, + }; + + // Initialize device state with generated configs + this.initializeDeviceState(); + + // Create toDevice stream (receives commands from MeshDevice) + this._toDevice = new WritableStream({ + write: (chunk) => { + this.handleToDevicePacket(chunk); + }, + }); + + // Create fromDevice stream (sends data to MeshDevice) + this._fromDevice = new ReadableStream({ + start: (controller) => { + this.fromDeviceController = controller; + // Start the connection sequence + this.startConnectionSequence(); + }, + cancel: () => { + this.cleanup(); + }, + }); + } + + /** Initialize device state from generators */ + private initializeDeviceState(): void { + // Store generated configs + for (const config of generateDeviceConfigs()) { + if (config.payloadVariant.case) { + this.deviceState.configs.set(config.payloadVariant.case, config); + } + } + + // Store generated module configs + for (const moduleConfig of generateModuleConfigs()) { + if (moduleConfig.payloadVariant.case) { + this.deviceState.moduleConfigs.set( + moduleConfig.payloadVariant.case, + moduleConfig, + ); + } + } + + // Store generated channels + for (const channel of generateChannels()) { + this.deviceState.channels.set(channel.index, channel); + } + + // Initialize user + const nodeNumHex = this.scenario.myNodeNum + .toString(16) + .toUpperCase() + .padStart(8, "0"); + this.deviceState.user = create(Protobuf.Mesh.UserSchema, { + id: `!${nodeNumHex.toLowerCase()}`, + shortName: nodeNumHex.slice(-4), + longName: `Demo ${nodeNumHex.slice(-4)}`, + hwModel: Protobuf.Mesh.HardwareModel.TBEAM, + isLicensed: false, + role: Protobuf.Config.Config_DeviceConfig_Role.CLIENT, + }); + } + + /** Writable stream of bytes to the device */ + get toDevice(): WritableStream { + return this._toDevice; + } + + /** Readable stream from the device */ + get fromDevice(): ReadableStream { + return this._fromDevice; + } + + /** Disconnect the mock transport */ + async disconnect(): Promise { + this.isDisconnected = true; + this.cleanup(); + this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "user"); + } + + private cleanup(): void { + if (this.activityInterval) { + clearTimeout(this.activityInterval); + this.activityInterval = undefined; + } + } + + private log(message: string): void { + if (this.options.debug) { + console.log(`[TransportMock] ${message}`); + } + } + + private emitStatus(status: Types.DeviceStatusEnum, reason?: string): void { + this.fromDeviceController?.enqueue({ + type: "status", + data: { status, reason }, + }); + } + + private emitPacket(data: Uint8Array): void { + if (this.isDisconnected) return; + this.fromDeviceController?.enqueue({ + type: "packet", + data, + }); + } + + private nextPacketId(): number { + return this.packetId++; + } + + /** + * Start the connection and config sequence + */ + private startConnectionSequence(): void { + this.log("Starting connection sequence"); + this.emitStatus(Types.DeviceStatusEnum.DeviceConnecting); + + // Simulate connection delay + setTimeout(() => { + this.emitStatus(Types.DeviceStatusEnum.DeviceConnected); + }, 50); + } + + /** + * Handle incoming packets from MeshDevice (commands to the device) + */ + private handleToDevicePacket(data: Uint8Array): void { + try { + const toRadio = fromBinary(Protobuf.Mesh.ToRadioSchema, data); + this.log(`Received ToRadio: ${toRadio.payloadVariant.case}`); + + switch (toRadio.payloadVariant.case) { + case "wantConfigId": + this.handleWantConfig(toRadio.payloadVariant.value); + break; + case "heartbeat": + this.log("Received heartbeat"); + // No response needed for heartbeat + break; + case "packet": + this.handleOutgoingPacket(toRadio.payloadVariant.value); + break; + default: + this.log(`Unhandled ToRadio variant: ${toRadio.payloadVariant.case}`); + } + } catch (e) { + this.log(`Error decoding ToRadio: ${e}`); + } + } + + /** + * Handle config request from MeshDevice + */ + private handleWantConfig(nonce: number): void { + this.log(`Config requested with nonce: ${nonce}`); + + // Run config sequence async + this.sendConfigSequence(nonce).catch((e) => { + this.log(`Error in config sequence: ${e}`); + }); + } + + /** + * Send the full configuration sequence + */ + private async sendConfigSequence(nonce: number): Promise { + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + await delay(this.options.configDelayMs); + + // Stage 1: Send device info and config + if (nonce === CONFIG_COMPLETE_STAGE1) { + // 1. Send MyNodeInfo + const myNodeInfo = generateMyNodeInfo(this.scenario.myNodeNum); + this.emitPacket(serializeFromRadio(createMyInfoPacket(myNodeInfo))); + // Extra delay after MyNodeInfo to ensure it's processed before config complete + // This prevents a race condition where configComplete fires before myNodeNum is set + await delay(Math.max(this.options.packetDelayMs, 100)); + + // 2. Send metadata + const metadata = generateMetadata(); + this.emitPacket( + serializeFromRadio(createMetadataPacket(metadata, this.nextPacketId())), + ); + await delay(this.options.packetDelayMs); + + // 3. Send device configs from state + for (const config of this.deviceState.configs.values()) { + this.emitPacket( + serializeFromRadio(createConfigPacket(config, this.nextPacketId())), + ); + await delay(this.options.packetDelayMs); + } + + // 4. Send module configs from state + for (const moduleConfig of this.deviceState.moduleConfigs.values()) { + this.emitPacket( + serializeFromRadio( + createModuleConfigPacket(moduleConfig, this.nextPacketId()), + ), + ); + await delay(this.options.packetDelayMs); + } + + // 5. Send channels from state + for (const channel of this.deviceState.channels.values()) { + this.emitPacket( + serializeFromRadio(createChannelPacket(channel, this.nextPacketId())), + ); + await delay(this.options.packetDelayMs); + } + + // 6. Send config complete + this.emitPacket( + serializeFromRadio( + createConfigCompletePacket( + CONFIG_COMPLETE_STAGE1, + this.nextPacketId(), + ), + ), + ); + this.log("Stage 1 config complete sent"); + } + + // Stage 2: Send node info + if (nonce === CONFIG_COMPLETE_STAGE2) { + // Send local device's own NodeInfo first + const myNodeInfo = generateNodeInfo({ + nodeNum: this.scenario.myNodeNum, + shortName: this.deviceState.user?.shortName, + longName: this.deviceState.user?.longName, + hwModel: this.deviceState.user?.hwModel, + latitude: this.scenario.centerLat, + longitude: this.scenario.centerLon, + }); + this.emitPacket( + serializeFromRadio( + createNodeInfoPacket(myNodeInfo, this.nextPacketId()), + ), + ); + await delay(this.options.packetDelayMs); + + // Send node info for each mesh node + for (const nodeOptions of this.scenario.nodes) { + const nodeInfo = generateNodeInfo(nodeOptions); + this.emitPacket( + serializeFromRadio( + createNodeInfoPacket(nodeInfo, this.nextPacketId()), + ), + ); + await delay(this.options.packetDelayMs); + } + + // Send config complete + this.emitPacket( + serializeFromRadio( + createConfigCompletePacket( + CONFIG_COMPLETE_STAGE2, + this.nextPacketId(), + ), + ), + ); + this.log("Stage 2 config complete sent"); + + // Start activity simulation after config is complete + this.startActivitySimulation(); + } + } + + /** + * Handle outgoing mesh packets (messages, admin commands, etc.) + */ + private handleOutgoingPacket(packet: Protobuf.Mesh.MeshPacket): void { + if (packet.payloadVariant.case !== "decoded") { + this.log(`Ignoring non-decoded packet`); + return; + } + + const decoded = packet.payloadVariant.value; + this.log(`Outgoing packet: portnum=${decoded.portnum}, to=${packet.to}`); + + // Handle admin messages + if (decoded.portnum === Protobuf.Portnums.PortNum.ADMIN_APP) { + this.handleAdminMessage(packet, decoded.payload); + return; + } + + // For other packets, just send a routing ACK + this.sendRoutingAck(packet.id, packet.from); + } + + /** + * Handle admin messages (config changes, requests, etc.) + */ + private handleAdminMessage( + packet: Protobuf.Mesh.MeshPacket, + payload: Uint8Array, + ): void { + try { + const adminMessage = fromBinary( + Protobuf.Admin.AdminMessageSchema, + payload, + ); + this.log(`Admin message: ${adminMessage.payloadVariant.case}`); + + switch (adminMessage.payloadVariant.case) { + // Config setters + case "setConfig": + this.handleSetConfig(adminMessage.payloadVariant.value); + this.sendRoutingAck(packet.id, packet.from); + break; + + case "setModuleConfig": + this.handleSetModuleConfig(adminMessage.payloadVariant.value); + this.sendRoutingAck(packet.id, packet.from); + break; + + case "setOwner": + this.handleSetOwner(adminMessage.payloadVariant.value); + this.sendRoutingAck(packet.id, packet.from); + break; + + case "setChannel": + this.handleSetChannel(adminMessage.payloadVariant.value); + this.sendRoutingAck(packet.id, packet.from); + break; + + // Config getters + case "getConfigRequest": + this.handleGetConfigRequest( + adminMessage.payloadVariant.value, + packet.from, + ); + break; + + case "getModuleConfigRequest": + this.handleGetModuleConfigRequest( + adminMessage.payloadVariant.value, + packet.from, + ); + break; + + case "getOwnerRequest": + this.handleGetOwnerRequest(packet.from); + break; + + case "getChannelRequest": + this.handleGetChannelRequest( + adminMessage.payloadVariant.value, + packet.from, + ); + break; + + // Other admin commands - just ACK them + case "beginEditSettings": + case "commitEditSettings": + case "rebootSeconds": + case "rebootOtaSeconds": + case "factoryResetDevice": + this.log(`Received ${adminMessage.payloadVariant.case}, sending ACK`); + this.sendRoutingAck(packet.id, packet.from); + break; + + default: + this.log( + `Unhandled admin message: ${adminMessage.payloadVariant.case}`, + ); + this.sendRoutingAck(packet.id, packet.from); + } + } catch (e) { + this.log(`Error handling admin message: ${e}`); + this.sendRoutingError( + packet.id, + packet.from, + Protobuf.Mesh.Routing_Error.BAD_REQUEST, + ); + } + } + + /** + * Handle setConfig admin message + */ + private handleSetConfig(config: Protobuf.Config.Config): void { + if (!config.payloadVariant.case) { + this.log("setConfig: no variant case"); + return; + } + + this.log(`setConfig: ${config.payloadVariant.case}`); + this.deviceState.configs.set(config.payloadVariant.case, config); + } + + /** + * Handle setModuleConfig admin message + */ + private handleSetModuleConfig( + moduleConfig: Protobuf.ModuleConfig.ModuleConfig, + ): void { + if (!moduleConfig.payloadVariant.case) { + this.log("setModuleConfig: no variant case"); + return; + } + + this.log(`setModuleConfig: ${moduleConfig.payloadVariant.case}`); + this.deviceState.moduleConfigs.set( + moduleConfig.payloadVariant.case, + moduleConfig, + ); + } + + /** + * Handle setOwner admin message + */ + private handleSetOwner(user: Protobuf.Mesh.User): void { + this.log(`setOwner: ${user.longName}`); + this.deviceState.user = user; + } + + /** + * Handle setChannel admin message + */ + private handleSetChannel(channel: Protobuf.Channel.Channel): void { + this.log(`setChannel: index=${channel.index}`); + this.deviceState.channels.set(channel.index, channel); + } + + /** + * Handle getConfigRequest - send back the requested config + */ + private handleGetConfigRequest( + configType: Protobuf.Admin.AdminMessage_ConfigType, + requestFrom: number, + ): void { + const configTypeMap: Record = { + [Protobuf.Admin.AdminMessage_ConfigType.DEVICE_CONFIG]: "device", + [Protobuf.Admin.AdminMessage_ConfigType.POSITION_CONFIG]: "position", + [Protobuf.Admin.AdminMessage_ConfigType.POWER_CONFIG]: "power", + [Protobuf.Admin.AdminMessage_ConfigType.NETWORK_CONFIG]: "network", + [Protobuf.Admin.AdminMessage_ConfigType.DISPLAY_CONFIG]: "display", + [Protobuf.Admin.AdminMessage_ConfigType.LORA_CONFIG]: "lora", + [Protobuf.Admin.AdminMessage_ConfigType.BLUETOOTH_CONFIG]: "bluetooth", + [Protobuf.Admin.AdminMessage_ConfigType.SECURITY_CONFIG]: "security", + }; + + const configKey = configTypeMap[configType]; + if (!configKey) { + this.log(`getConfigRequest: unknown config type ${configType}`); + return; + } + + const config = this.deviceState.configs.get(configKey); + if (!config) { + this.log(`getConfigRequest: config ${configKey} not found`); + return; + } + + this.log(`getConfigRequest: sending ${configKey}`); + this.sendAdminResponse(requestFrom, { + case: "getConfigResponse", + value: config, + }); + } + + /** + * Handle getModuleConfigRequest - send back the requested module config + */ + private handleGetModuleConfigRequest( + moduleConfigType: Protobuf.Admin.AdminMessage_ModuleConfigType, + requestFrom: number, + ): void { + const moduleConfigTypeMap: Record = { + [Protobuf.Admin.AdminMessage_ModuleConfigType.MQTT_CONFIG]: "mqtt", + [Protobuf.Admin.AdminMessage_ModuleConfigType.SERIAL_CONFIG]: "serial", + [Protobuf.Admin.AdminMessage_ModuleConfigType.EXTNOTIF_CONFIG]: + "externalNotification", + [Protobuf.Admin.AdminMessage_ModuleConfigType.STOREFORWARD_CONFIG]: + "storeForward", + [Protobuf.Admin.AdminMessage_ModuleConfigType.RANGETEST_CONFIG]: + "rangeTest", + [Protobuf.Admin.AdminMessage_ModuleConfigType.TELEMETRY_CONFIG]: + "telemetry", + [Protobuf.Admin.AdminMessage_ModuleConfigType.CANNEDMSG_CONFIG]: + "cannedMessage", + [Protobuf.Admin.AdminMessage_ModuleConfigType.AUDIO_CONFIG]: "audio", + [Protobuf.Admin.AdminMessage_ModuleConfigType.REMOTEHARDWARE_CONFIG]: + "remoteHardware", + [Protobuf.Admin.AdminMessage_ModuleConfigType.NEIGHBORINFO_CONFIG]: + "neighborInfo", + [Protobuf.Admin.AdminMessage_ModuleConfigType.AMBIENTLIGHTING_CONFIG]: + "ambientLighting", + [Protobuf.Admin.AdminMessage_ModuleConfigType.DETECTIONSENSOR_CONFIG]: + "detectionSensor", + [Protobuf.Admin.AdminMessage_ModuleConfigType.PAXCOUNTER_CONFIG]: + "paxcounter", + }; + + const configKey = moduleConfigTypeMap[moduleConfigType]; + if (!configKey) { + this.log( + `getModuleConfigRequest: unknown module config type ${moduleConfigType}`, + ); + return; + } + + const moduleConfig = this.deviceState.moduleConfigs.get(configKey); + if (!moduleConfig) { + this.log(`getModuleConfigRequest: module config ${configKey} not found`); + return; + } + + this.log(`getModuleConfigRequest: sending ${configKey}`); + this.sendAdminResponse(requestFrom, { + case: "getModuleConfigResponse", + value: moduleConfig, + }); + } + + /** + * Handle getOwnerRequest - send back the user info + */ + private handleGetOwnerRequest(requestFrom: number): void { + if (!this.deviceState.user) { + this.log("getOwnerRequest: no user set"); + return; + } + + this.log("getOwnerRequest: sending user"); + this.sendAdminResponse(requestFrom, { + case: "getOwnerResponse", + value: this.deviceState.user, + }); + } + + /** + * Handle getChannelRequest - send back the requested channel + */ + private handleGetChannelRequest( + channelIndex: number, + requestFrom: number, + ): void { + const channel = this.deviceState.channels.get(channelIndex); + if (!channel) { + this.log(`getChannelRequest: channel ${channelIndex} not found`); + return; + } + + this.log(`getChannelRequest: sending channel ${channelIndex}`); + this.sendAdminResponse(requestFrom, { + case: "getChannelResponse", + value: channel, + }); + } + + /** + * Send an admin response packet + */ + private sendAdminResponse( + to: number, + payloadVariant: Protobuf.Admin.AdminMessage["payloadVariant"], + ): void { + const adminMessage = create(Protobuf.Admin.AdminMessageSchema, { + payloadVariant, + }); + + const dataPacket = create(Protobuf.Mesh.DataSchema, { + portnum: Protobuf.Portnums.PortNum.ADMIN_APP, + payload: toBinary(Protobuf.Admin.AdminMessageSchema, adminMessage), + }); + + const meshPacket = create(Protobuf.Mesh.MeshPacketSchema, { + from: this.scenario.myNodeNum, + to, + id: this.nextPacketId(), + rxTime: Math.floor(Date.now() / 1000), + hopLimit: 3, + hopStart: 3, + payloadVariant: { + case: "decoded", + value: dataPacket, + }, + }); + + const fromRadio = create(Protobuf.Mesh.FromRadioSchema, { + id: this.nextPacketId(), + payloadVariant: { + case: "packet", + value: meshPacket, + }, + }); + + this.emitPacket(serializeFromRadio(fromRadio)); + } + + /** + * Send a routing ACK for a packet + */ + private sendRoutingAck(requestId: number, to: number): void { + this.sendRoutingResponse(requestId, to, Protobuf.Mesh.Routing_Error.NONE); + } + + /** + * Send a routing error for a packet + */ + private sendRoutingError( + requestId: number, + to: number, + error: Protobuf.Mesh.Routing_Error, + ): void { + this.sendRoutingResponse(requestId, to, error); + } + + /** + * Send a routing response (ACK or error) + */ + private sendRoutingResponse( + requestId: number, + to: number, + error: Protobuf.Mesh.Routing_Error, + ): void { + const routing = create(Protobuf.Mesh.RoutingSchema, { + variant: { + case: "errorReason", + value: error, + }, + }); + + const dataPacket = create(Protobuf.Mesh.DataSchema, { + portnum: Protobuf.Portnums.PortNum.ROUTING_APP, + payload: toBinary(Protobuf.Mesh.RoutingSchema, routing), + requestId, + }); + + const meshPacket = create(Protobuf.Mesh.MeshPacketSchema, { + from: this.scenario.myNodeNum, + to, + id: this.nextPacketId(), + rxTime: Math.floor(Date.now() / 1000), + hopLimit: 3, + hopStart: 3, + payloadVariant: { + case: "decoded", + value: dataPacket, + }, + }); + + const fromRadio = create(Protobuf.Mesh.FromRadioSchema, { + id: this.nextPacketId(), + payloadVariant: { + case: "packet", + value: meshPacket, + }, + }); + + this.emitPacket(serializeFromRadio(fromRadio)); + } + + /** + * Schedule the next random activity tick with a random 1–10 minute delay + */ + private scheduleNextActivity(): void { + const minMs = 60_000; + const maxMs = 600_000; + const delay = Math.floor(Math.random() * (maxMs - minMs + 1)) + minMs; + + this.activityInterval = setTimeout(() => { + this.simulateRandomActivity(); + this.scheduleNextActivity(); + }, delay); + + this.log(`Next activity in ${Math.round(delay / 1000)}s`); + } + + /** + * Start simulating mesh activity (messages, positions, telemetry) + */ + private startActivitySimulation(): void { + if (this.scenario.nodes.length === 0) return; + + this.log("Starting activity simulation (1–10 min random intervals)"); + this.scheduleNextActivity(); + } + + /** + * Simulate random mesh activity + */ + private simulateRandomActivity(): void { + if (this.isDisconnected) return; + + const activityType = Math.random(); + const randomNode = + this.scenario.nodes[ + Math.floor(Math.random() * this.scenario.nodes.length) + ]; + + if (!randomNode) return; + + if (activityType < 0.4 && this.scenario.simulateMessages) { + // Simulate incoming message + const messages = [ + "Hello from the mesh!", + "Testing 1 2 3", + "Anyone copy?", + "Good signal today", + "Check out this view!", + "Heading home", + "What's the weather like?", + ]; + const message = + messages[Math.floor(Math.random() * messages.length)] ?? "Hello"; + + this.emitPacket( + serializeFromRadio( + createTextMessagePacket( + randomNode.nodeNum, + 0xffffffff, // broadcast + message, + 0, + this.nextPacketId(), + ), + ), + ); + this.log(`Simulated message from ${randomNode.shortName}: "${message}"`); + } else if (activityType < 0.7 && this.scenario.simulatePositions) { + // Simulate position update + const lat = randomNode.latitude ?? this.scenario.centerLat; + const lon = randomNode.longitude ?? this.scenario.centerLon; + const alt = randomNode.altitude ?? 100; + + // Add small random movement + const newLat = lat + (Math.random() - 0.5) * 0.001; + const newLon = lon + (Math.random() - 0.5) * 0.001; + + this.emitPacket( + serializeFromRadio( + createPositionPacket( + randomNode.nodeNum, + newLat, + newLon, + alt, + this.nextPacketId(), + ), + ), + ); + this.log(`Simulated position from ${randomNode.shortName}`); + } else if (this.scenario.simulateTelemetry) { + // Simulate telemetry update + this.emitPacket( + serializeFromRadio( + createTelemetryPacket( + randomNode.nodeNum, + randomNode.batteryLevel ?? Math.floor(Math.random() * 100), + randomNode.voltage ?? 3.7 + Math.random() * 0.5, + Math.random() * 30, + Math.random() * 10, + this.nextPacketId(), + ), + ), + ); + this.log(`Simulated telemetry from ${randomNode.shortName}`); + } + } +} diff --git a/packages/transport-mock/tsconfig.json b/packages/transport-mock/tsconfig.json new file mode 100644 index 000000000..590adbed5 --- /dev/null +++ b/packages/transport-mock/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "target": "ES2020", + "declaration": true, + "outDir": "./dist", + "moduleResolution": "bundler", + "emitDeclarationOnly": false, + "esModuleInterop": true + }, + "include": ["src", "mod.ts"] +} diff --git a/packages/transport-web-bluetooth/src/transport.test.ts b/packages/transport-web-bluetooth/src/transport.test.ts index c7825ecab..5a7d52d9d 100644 --- a/packages/transport-web-bluetooth/src/transport.test.ts +++ b/packages/transport-web-bluetooth/src/transport.test.ts @@ -54,7 +54,8 @@ function stubWebBluetooth() { } as unknown as BluetoothRemoteGATTCharacteristic; const toRadioCharacteristic: BluetoothRemoteGATTCharacteristic = { - async writeValue(bufferSource: BufferSource) { + properties: { writeWithoutResponse: false }, + async writeValueWithResponse(bufferSource: BufferSource) { const u8 = bufferSource instanceof ArrayBuffer ? new Uint8Array(bufferSource) @@ -181,6 +182,9 @@ describe("TransportWebBluetooth (contract)", () => { return await TransportWebBluetooth.create(); }, pushIncoming: async (bytes) => { + // Yield to let the ReadableStream start() callback complete + // (registers the characteristicvaluechanged listener) + await new Promise((r) => setTimeout(r, 10)); ( globalThis as unknown as { __ble: ReturnType } ).__ble.pushIncoming(bytes); diff --git a/packages/transport-web-bluetooth/src/transport.ts b/packages/transport-web-bluetooth/src/transport.ts index 00c0e0c4c..0e98d8533 100644 --- a/packages/transport-web-bluetooth/src/transport.ts +++ b/packages/transport-web-bluetooth/src/transport.ts @@ -26,6 +26,7 @@ export class TransportWebBluetooth implements Types.Transport { private toRadioCharacteristic: BluetoothRemoteGATTCharacteristic; private fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic; private fromNumCharacteristic: BluetoothRemoteGATTCharacteristic; + private logRadioCharacteristic: BluetoothRemoteGATTCharacteristic | null; private gattServer: BluetoothRemoteGATTServer; private lastStatus: Types.DeviceStatusEnum = @@ -39,6 +40,8 @@ export class TransportWebBluetooth implements Types.Transport { static FromRadioUuid = "2c55e69e-4993-11ed-b878-0242ac120002"; /** UUID for the "fromNum" notification characteristic. */ static FromNumUuid = "ed9da18c-a800-4f66-a670-aa7547e34453"; + /** UUID for the "logRadio" notification characteristic (optional). */ + static LogRadioUuid = "5a3d6e49-06e6-4423-9944-e9de8cdf9547"; /** UUID for the Meshtastic GATT service. */ static ServiceUuid = "6ba1b218-15a8-461f-9fa8-5dcae273eafd"; @@ -54,6 +57,16 @@ export class TransportWebBluetooth implements Types.Transport { private onFromNumChanged = () => { void this.readFromRadio(); }; + private onLogRadioChanged = (event: Event) => { + const target = event.target as BluetoothRemoteGATTCharacteristic; + const value = target.value; + if (value && value.byteLength > 0) { + this.enqueue({ + type: "packet", + data: new Uint8Array(value.buffer), + }); + } + }; /** * Prompts the user to select a Bluetooth device, connects it, and returns a transport. @@ -109,11 +122,21 @@ export class TransportWebBluetooth implements Types.Transport { throw new Error("Failed to find required characteristics"); } + let logRadioCharacteristic: BluetoothRemoteGATTCharacteristic | null = null; + try { + logRadioCharacteristic = await service.getCharacteristic( + TransportWebBluetooth.LogRadioUuid, + ); + } catch { + // Optional — older firmware may not expose this characteristic + } + return new TransportWebBluetooth( toRadioCharacteristic, fromRadioCharacteristic, fromNumCharacteristic, gattServer, + logRadioCharacteristic, ); } @@ -126,10 +149,12 @@ export class TransportWebBluetooth implements Types.Transport { fromRadioCharacteristic: BluetoothRemoteGATTCharacteristic, fromNumCharacteristic: BluetoothRemoteGATTCharacteristic, gattServer: BluetoothRemoteGATTServer, + logRadioCharacteristic: BluetoothRemoteGATTCharacteristic | null = null, ) { this.toRadioCharacteristic = toRadioCharacteristic; this.fromRadioCharacteristic = fromRadioCharacteristic; this.fromNumCharacteristic = fromNumCharacteristic; + this.logRadioCharacteristic = logRadioCharacteristic; this.gattServer = gattServer; this._fromDevice = new ReadableStream({ @@ -143,11 +168,29 @@ export class TransportWebBluetooth implements Types.Transport { ); try { - await this.fromNumCharacteristic.startNotifications(); + await this.retryCall(() => + this.fromNumCharacteristic.startNotifications(), + ); this.fromNumCharacteristic.addEventListener( "characteristicvaluechanged", this.onFromNumChanged, ); + + if (this.logRadioCharacteristic) { + try { + await this.retryCall(() => + this.logRadioCharacteristic!.startNotifications(), + ); + this.logRadioCharacteristic.addEventListener( + "characteristicvaluechanged", + this.onLogRadioChanged, + ); + } catch { + // Non-fatal — logRadio is optional + this.logRadioCharacteristic = null; + } + } + this.emitStatus(Types.DeviceStatusEnum.DeviceConnected); // prime once in case data already queued void this.readFromRadio(); @@ -168,7 +211,12 @@ export class TransportWebBluetooth implements Types.Transport { write: async (chunk) => { try { const ab = toArrayBuffer(chunk); - await this.toRadioCharacteristic.writeValue(ab); + await this.retryCall(() => { + if (this.toRadioCharacteristic.properties.writeWithoutResponse) { + return this.toRadioCharacteristic.writeValueWithoutResponse(ab); + } + return this.toRadioCharacteristic.writeValueWithResponse(ab); + }); void this.readFromRadio(); // ensure we read any response } catch (error) { this.emitStatus( @@ -205,6 +253,17 @@ export class TransportWebBluetooth implements Types.Transport { "characteristicvaluechanged", this.onFromNumChanged, ); + + if (this.logRadioCharacteristic) { + try { + this.logRadioCharacteristic.stopNotifications?.(); + } catch {} + this.logRadioCharacteristic.removeEventListener( + "characteristicvaluechanged", + this.onLogRadioChanged, + ); + } + this.gattServer.device.removeEventListener( "gattserverdisconnected", this.onGattDisconnected, @@ -226,7 +285,9 @@ export class TransportWebBluetooth implements Types.Transport { try { let hasMoreData = true; while (hasMoreData && this.fromRadioCharacteristic) { - const value = await this.fromRadioCharacteristic.readValue(); + const value = await this.retryCall(() => + this.fromRadioCharacteristic.readValue(), + ); if (value.byteLength === 0) { hasMoreData = false; continue; @@ -249,6 +310,22 @@ export class TransportWebBluetooth implements Types.Transport { } } + private async retryCall( + fn: () => Promise, + attempts = 3, + delayMs = 500, + ): Promise { + for (let i = 0; i < attempts; i++) { + try { + return await fn(); + } catch (error) { + if (i === attempts - 1) throw error; + await new Promise((r) => setTimeout(r, delayMs)); + } + } + throw new Error("retryCall exhausted"); + } + private emitStatus(next: Types.DeviceStatusEnum, reason?: string): void { if (next === this.lastStatus) { return; diff --git a/packages/transport-web-serial/src/transport.test.ts b/packages/transport-web-serial/src/transport.test.ts index 403a3f38f..74274c524 100644 --- a/packages/transport-web-serial/src/transport.test.ts +++ b/packages/transport-web-serial/src/transport.test.ts @@ -115,6 +115,8 @@ class FakeSerialPort { readable: ReadableStream; writable: WritableStream; lastWritten?: Uint8Array; + setSignalsCalled = false; + setSignalsArgs?: { dataTerminalReady?: boolean; requestToSend?: boolean }; private _readController!: ReadableStreamDefaultController; @@ -143,6 +145,15 @@ class FakeSerialPort { return Promise.resolve(); } + setSignals(signals: { + dataTerminalReady?: boolean; + requestToSend?: boolean; + }): Promise { + this.setSignalsCalled = true; + this.setSignalsArgs = signals; + return Promise.resolve(); + } + pushIncoming(bytes: Uint8Array) { this._readController.enqueue(bytes); } @@ -243,4 +254,33 @@ describe("TransportWebSerial (extras)", () => { reader.releaseLock(); await transport.disconnect(); }); + + /** + * REGRESSION TEST: setSignals() must NOT be called during connection. + * + * Calling setSignals({ dataTerminalReady: false, requestToSend: false }) + * prevents the device from responding to config requests. This was + * discovered when the connection would hang during configureTwoStage(). + * + * See: https://github.com/meshtastic/meshtastic-web/issues/XXX + */ + it("does NOT call setSignals during connection (regression)", async () => { + const fake = new FakeSerialPort(); + const transport = await TransportWebSerial.createFromPort(fake as any); + + // Wait for connection to be established + const reader = transport.fromDevice.getReader(); + for (let i = 0; i < 3; i++) { + const { value } = await reader.read(); + if (value?.type === "status" && value.data.status === Types.DeviceStatusEnum.DeviceConnected) { + break; + } + } + + // Verify setSignals was never called + expect(fake.setSignalsCalled).toBe(false); + + reader.releaseLock(); + await transport.disconnect(); + }); }); diff --git a/packages/transport-web-serial/src/transport.ts b/packages/transport-web-serial/src/transport.ts index ff81aee13..52a67bb90 100644 --- a/packages/transport-web-serial/src/transport.ts +++ b/packages/transport-web-serial/src/transport.ts @@ -26,6 +26,7 @@ export class TransportWebSerial implements Types.Transport { public static async create(baudRate?: number): Promise { const port = await navigator.serial.requestPort(); await port.open({ baudRate: baudRate || 115200 }); + await TransportWebSerial.sendWakeBytes(port); return new TransportWebSerial(port); } @@ -37,12 +38,39 @@ export class TransportWebSerial implements Types.Transport { port: SerialPort, baudRate?: number, ): Promise { + const streamsLocked = port.readable?.locked || port.writable?.locked; + + if (streamsLocked) { + // Streams locked from a previous connection — close and reopen + try { + await port.close(); + } catch { + /* port.close() releases locks internally */ + } + } + if (!port.readable || !port.writable) { await port.open({ baudRate: baudRate || 115200 }); } + + await TransportWebSerial.sendWakeBytes(port); return new TransportWebSerial(port); } + /** + * Sends wake bytes to rouse a sleeping device before establishing streams. + * Must be called before the constructor, which locks the writable stream via pipeTo(). + */ + private static async sendWakeBytes(port: SerialPort): Promise { + if (!port.writable || port.writable.locked) return; + const writer = port.writable.getWriter(); + try { + await writer.write(new Uint8Array([0x94, 0x94, 0x94, 0x94])); + } finally { + writer.releaseLock(); + } + } + /** * Constructs a transport around a given {@link SerialPort}. * @throws If the port lacks readable or writable streams. @@ -57,16 +85,16 @@ export class TransportWebSerial implements Types.Transport { this.abortController = new AbortController(); const abortController = this.abortController; - // Set up the pipe with abort signal for clean cancellation + console.debug("[Serial] Setting up transport streams"); + const toDeviceTransform = Utils.toDeviceStream(); this.pipePromise = toDeviceTransform.readable .pipeTo(connection.writable, { signal: this.abortController.signal }) .catch((err) => { - // Ignore expected rejection when we cancel it via the AbortController. if (abortController.signal.aborted) { return; } - console.error("Error piping data to serial port:", err); + console.error("[Serial] Error piping data to serial port:", err); this.connection.close().catch(() => {}); this.emitStatus( Types.DeviceStatusEnum.DeviceDisconnected, @@ -76,11 +104,11 @@ export class TransportWebSerial implements Types.Transport { this._toDevice = toDeviceTransform.writable; - // Wrap + capture controller to inject status packets this._fromDevice = new ReadableStream({ start: async (ctrl) => { this.fromDeviceController = ctrl; + console.debug("[Serial] Starting read loop"); this.emitStatus(Types.DeviceStatusEnum.DeviceConnecting); const transformed = this.portReadable.pipeThrough( @@ -100,17 +128,20 @@ export class TransportWebSerial implements Types.Transport { navigator.serial.addEventListener("disconnect", onOsDisconnect); this.emitStatus(Types.DeviceStatusEnum.DeviceConnected); + console.debug("[Serial] Connected, waiting for data..."); try { while (true) { const { value, done } = await reader.read(); if (done) { + console.debug("[Serial] Read stream done"); break; } ctrl.enqueue(value); } ctrl.close(); } catch (error) { + console.error("[Serial] Read error:", error); if (!this.closingByUser) { this.emitStatus( Types.DeviceStatusEnum.DeviceDisconnected, @@ -157,73 +188,23 @@ export class TransportWebSerial implements Types.Transport { try { this.closingByUser = true; - // Stop outbound piping this.abortController.abort(); if (this.pipePromise) { await this.pipePromise; } - // Cancel any remaining streams if (this._fromDevice?.locked) { try { await this._fromDevice.cancel(); - } catch { - // Stream cancellation might fail if already cancelled - } + } catch {} } await this.connection.close(); } catch (error) { - // If we can't close cleanly, let the browser handle cleanup console.warn("Could not cleanly disconnect serial port:", error); } finally { this.emitStatus(Types.DeviceStatusEnum.DeviceDisconnected, "user"); this.closingByUser = false; } } - - /** - * Reconnects the transport by creating a new AbortController and re-establishing - * the pipe connection. Only call this after disconnect() or if the connection failed. - */ - public async reconnect() { - this.emitStatus(Types.DeviceStatusEnum.DeviceConnecting, "reconnect"); - - try { - if (!this.connection.readable || !this.connection.writable) { - throw new Error("Stream not accessible"); - } - this.portReadable = this.connection.readable; - - // Create a new AbortController for the new connection - this.abortController = new AbortController(); - const abortController = this.abortController; - - // Re-establish the pipe connection - const toDeviceTransform = Utils.toDeviceStream(); - this.pipePromise = toDeviceTransform.readable - .pipeTo(this.connection.writable, { - signal: this.abortController.signal, - }) - .catch((error) => { - if (abortController.signal.aborted) { - return; - } - console.error("Error piping data to serial port (reconnect):", error); - this.emitStatus( - Types.DeviceStatusEnum.DeviceDisconnected, - "write-error", - ); - }); - - this.emitStatus(Types.DeviceStatusEnum.DeviceConnected, "reconnected"); - } catch (error) { - // Couldn’t re-pipe - this.emitStatus( - Types.DeviceStatusEnum.DeviceDisconnected, - "reconnect-failed", - ); - throw error; - } - } } diff --git a/packages/ui/components.json b/packages/ui/components.json deleted file mode 100644 index 3e90654ec..000000000 --- a/packages/ui/components.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/base.css", - "baseColor": "slate", - "cssVariables": true, - "prefix": "" - }, - "iconLibrary": "lucide", - "aliases": { - "components": "@/components", - "utils": "@/lib/utils", - "ui": "@/components/ui", - "lib": "@/lib", - "hooks": "@/hooks" - }, - "registries": {} -} diff --git a/packages/ui/package.json b/packages/ui/package.json deleted file mode 100644 index f12f10e1a..000000000 --- a/packages/ui/package.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "name": "@meshtastic/ui", - "version": "0.1.0", - "license": "GPL-3.0-only", - "repository": { - "type": "git", - "url": "git+https://github.com/meshtastic/web.git" - }, - "type": "module", - "files": [ - "dist", - "!dist/**/*.test.*" - ], - "sideEffects": [ - "**/*.css" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./theme/default.css": "./dist/theme/default.css" - }, - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/index.d.ts", - "scripts": { - "start": "npm run dev", - "dev": "vite dev", - "watch": "vite build --watch", - "build": "vite build && publint", - "typecheck": "tsc -p tsconfig.json --noEmit", - "preview": "vite preview", - "lint": "eslint . --max-warnings 0", - "lint:fix": "npm run lint -- --fix", - "format": "prettier --check .", - "format:fix": "prettier --write .", - "test": "vitest" - }, - "peerDependencies": { - "@radix-ui/react-slot": ">=1.0.2", - "class-variance-authority": ">=0.7.0", - "react": ">=19", - "react-dom": ">=19", - "tailwind-merge": ">=2.5.0", - "tailwindcss": "^4.1.7" - }, - "dependencies": { - "@radix-ui/react-collapsible": "^1.1.12", - "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-dropdown-menu": "^2.1.16", - "@radix-ui/react-separator": "^1.1.7", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-tooltip": "^1.2.8", - "@tanstack/react-router": "^1.132.47", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.545.0", - "tailwind-merge": "^2.6.0" - }, - "devDependencies": { - "@tailwindcss/postcss": "^4.1.7", - "@tailwindcss/vite": "^4.1.14", - "@types/react": "^18.3.5", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.4", - "publint": "^0.3.14", - "tailwindcss": "^4.1.14", - "tw-animate-css": "^1.4.0", - "typescript": "^5.6.3", - "vite": "^7.0.0", - "vite-plugin-dts": "^4.5.4", - "vite-plugin-static-copy": "^3.1.4", - "vitest": "^3.0.0" - } -} diff --git a/packages/ui/src/app.css b/packages/ui/src/app.css deleted file mode 100644 index 33a766b1a..000000000 --- a/packages/ui/src/app.css +++ /dev/null @@ -1,7 +0,0 @@ -body.dark { - color-scheme: dark; -} - -body:not(.dark) { - color-scheme: light; -} \ No newline at end of file diff --git a/packages/ui/src/components/theme-provider.tsx b/packages/ui/src/components/theme-provider.tsx deleted file mode 100644 index c63f43c20..000000000 --- a/packages/ui/src/components/theme-provider.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { createContext, useContext, useEffect, useState } from "react"; - -type Theme = "dark" | "light" | "system"; - -type ThemeProviderProps = { - children: React.ReactNode; - defaultTheme?: Theme; - storageKey?: string; -}; - -type ThemeProviderState = { - theme: Theme; - setTheme: (theme: Theme) => void; -}; - -const initialState: ThemeProviderState = { - theme: "system", - setTheme: () => null, -}; - -const ThemeProviderContext = createContext(initialState); - -export function ThemeProvider({ - children, - defaultTheme = "system", - storageKey = "vite-ui-theme", - ...props -}: ThemeProviderProps) { - const [theme, setTheme] = useState( - () => (localStorage.getItem(storageKey) as Theme) || defaultTheme, - ); - - useEffect(() => { - const root = window.document.documentElement; - - root.classList.remove("light", "dark"); - - if (theme === "system") { - const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") - .matches - ? "dark" - : "light"; - - root.classList.add(systemTheme); - return; - } - - root.classList.add(theme); - }, [theme]); - - const value = { - theme, - setTheme: (theme: Theme) => { - localStorage.setItem(storageKey, theme); - setTheme(theme); - }, - }; - - return ( - - {children} - - ); -} - -export const useTheme = () => { - const context = useContext(ThemeProviderContext); - - // If the provider is missing, context will be initialState (setTheme is a no-op) - if (context.setTheme === initialState.setTheme) { - throw new Error( - "useTheme must be used within a ThemeProvider: provider is missing", - ); - } - - return context; -}; diff --git a/packages/ui/src/components/ui/badge.tsx b/packages/ui/src/components/ui/badge.tsx deleted file mode 100644 index d9ebd4afc..000000000 --- a/packages/ui/src/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span"; - - return ( - - ); -} - -export { Badge, badgeVariants }; diff --git a/packages/ui/src/components/ui/button.tsx b/packages/ui/src/components/ui/button.tsx deleted file mode 100644 index c6f2d89e2..000000000 --- a/packages/ui/src/components/ui/button.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-sm": "size-8", - "icon-lg": "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean; - }) { - const Comp = asChild ? Slot : "button"; - - return ( - - ); -} - -export { Button, buttonVariants }; diff --git a/packages/ui/src/components/ui/collapsible.tsx b/packages/ui/src/components/ui/collapsible.tsx deleted file mode 100644 index 849e7b66f..000000000 --- a/packages/ui/src/components/ui/collapsible.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"; - -function Collapsible({ - ...props -}: React.ComponentProps) { - return ; -} - -function CollapsibleTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function CollapsibleContent({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/packages/ui/src/components/ui/dropdown-menu.tsx b/packages/ui/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index a7a316565..000000000 --- a/packages/ui/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function DropdownMenu({ - ...props -}: React.ComponentProps) { - return ; -} - -function DropdownMenuPortal({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuContent({ - className, - sideOffset = 4, - ...props -}: React.ComponentProps) { - return ( - - - - ); -} - -function DropdownMenuGroup({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuItem({ - className, - inset, - variant = "default", - ...props -}: React.ComponentProps & { - inset?: boolean; - variant?: "default" | "destructive"; -}) { - return ( - - ); -} - -function DropdownMenuCheckboxItem({ - className, - children, - checked, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function DropdownMenuRadioGroup({ - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuRadioItem({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - - - - - {children} - - ); -} - -function DropdownMenuLabel({ - className, - inset, - ...props -}: React.ComponentProps & { - inset?: boolean; -}) { - return ( - - ); -} - -function DropdownMenuSeparator({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function DropdownMenuShortcut({ - className, - ...props -}: React.ComponentProps<"span">) { - return ( - - ); -} - -function DropdownMenuSub({ - ...props -}: React.ComponentProps) { - return ; -} - -function DropdownMenuSubTrigger({ - className, - inset, - children, - ...props -}: React.ComponentProps & { - inset?: boolean; -}) { - return ( - - {children} - - - ); -} - -function DropdownMenuSubContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { - DropdownMenu, - DropdownMenuPortal, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuLabel, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubTrigger, - DropdownMenuSubContent, -}; diff --git a/packages/ui/src/components/ui/input.tsx b/packages/ui/src/components/ui/input.tsx deleted file mode 100644 index 73ea8679a..000000000 --- a/packages/ui/src/components/ui/input.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ); -} - -export { Input }; diff --git a/packages/ui/src/components/ui/separator.tsx b/packages/ui/src/components/ui/separator.tsx deleted file mode 100644 index 434a10e83..000000000 --- a/packages/ui/src/components/ui/separator.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Separator({ - className, - orientation = "horizontal", - decorative = true, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { Separator }; diff --git a/packages/ui/src/components/ui/sheet.tsx b/packages/ui/src/components/ui/sheet.tsx deleted file mode 100644 index 577a724e2..000000000 --- a/packages/ui/src/components/ui/sheet.tsx +++ /dev/null @@ -1,139 +0,0 @@ -"use client"; - -import * as SheetPrimitive from "@radix-ui/react-dialog"; -import { XIcon } from "lucide-react"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Sheet({ ...props }: React.ComponentProps) { - return ; -} - -function SheetTrigger({ - ...props -}: React.ComponentProps) { - return ; -} - -function SheetClose({ - ...props -}: React.ComponentProps) { - return ; -} - -function SheetPortal({ - ...props -}: React.ComponentProps) { - return ; -} - -function SheetOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SheetContent({ - className, - children, - side = "right", - ...props -}: React.ComponentProps & { - side?: "top" | "right" | "bottom" | "left"; -}) { - return ( - - - - {children} - - - Close - - - - ); -} - -function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -function SheetTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -function SheetDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ); -} - -export { - Sheet, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -}; diff --git a/packages/ui/src/components/ui/sidebar.tsx b/packages/ui/src/components/ui/sidebar.tsx deleted file mode 100644 index bf103684a..000000000 --- a/packages/ui/src/components/ui/sidebar.tsx +++ /dev/null @@ -1,726 +0,0 @@ -"use client"; - -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; -import { PanelLeftIcon } from "lucide-react"; -import * as React from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Separator } from "@/components/ui/separator"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; -import { Skeleton } from "@/components/ui/skeleton"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { useIsMobile } from "@/hooks/use-mobile"; -import { cn } from "@/lib/utils"; - -const SIDEBAR_COOKIE_NAME = "sidebar_state"; -const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; -const SIDEBAR_WIDTH = "16rem"; -const SIDEBAR_WIDTH_MOBILE = "18rem"; -const SIDEBAR_WIDTH_ICON = "3rem"; -const SIDEBAR_KEYBOARD_SHORTCUT = "b"; - -type SidebarContextProps = { - state: "expanded" | "collapsed"; - open: boolean; - setOpen: (open: boolean) => void; - openMobile: boolean; - setOpenMobile: (open: boolean) => void; - isMobile: boolean; - toggleSidebar: () => void; -}; - -const SidebarContext = React.createContext(null); - -function useSidebar() { - const context = React.useContext(SidebarContext); - if (!context) { - throw new Error("useSidebar must be used within a SidebarProvider."); - } - - return context; -} - -function SidebarProvider({ - defaultOpen = true, - open: openProp, - onOpenChange: setOpenProp, - className, - style, - children, - ...props -}: React.ComponentProps<"div"> & { - defaultOpen?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; -}) { - const isMobile = useIsMobile(); - const [openMobile, setOpenMobile] = React.useState(false); - - // This is the internal state of the sidebar. - // We use openProp and setOpenProp for control from outside the component. - const [_open, _setOpen] = React.useState(defaultOpen); - const open = openProp ?? _open; - const setOpen = React.useCallback( - (value: boolean | ((value: boolean) => boolean)) => { - const openState = typeof value === "function" ? value(open) : value; - if (setOpenProp) { - setOpenProp(openState); - } else { - _setOpen(openState); - } - - // This sets the cookie to keep the sidebar state. - // biome-ignore lint/suspicious/noDocumentCookie: this was from a shadcn template - document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`; - }, - [setOpenProp, open], - ); - - // Helper to toggle the sidebar. - const toggleSidebar = React.useCallback(() => { - return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open); - }, [isMobile, setOpen]); - - // Adds a keyboard shortcut to toggle the sidebar. - React.useEffect(() => { - const handleKeyDown = (event: KeyboardEvent) => { - if ( - event.key === SIDEBAR_KEYBOARD_SHORTCUT && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault(); - toggleSidebar(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [toggleSidebar]); - - // We add a state so that we can do data-state="expanded" or "collapsed". - // This makes it easier to style the sidebar with Tailwind classes. - const state = open ? "expanded" : "collapsed"; - - const contextValue = React.useMemo( - () => ({ - state, - open, - setOpen, - isMobile, - openMobile, - setOpenMobile, - toggleSidebar, - }), - [state, open, setOpen, isMobile, openMobile, toggleSidebar], - ); - - return ( - - -
- {children} -
-
-
- ); -} - -function Sidebar({ - side = "left", - variant = "sidebar", - collapsible = "offcanvas", - className, - children, - ...props -}: React.ComponentProps<"div"> & { - side?: "left" | "right"; - variant?: "sidebar" | "floating" | "inset"; - collapsible?: "offcanvas" | "icon" | "none"; -}) { - const { isMobile, state, openMobile, setOpenMobile } = useSidebar(); - - if (collapsible === "none") { - return ( -
- {children} -
- ); - } - - if (isMobile) { - return ( - - - - Sidebar - Displays the mobile sidebar. - -
{children}
-
-
- ); - } - - return ( -
- {/* This is what handles the sidebar gap on desktop */} -
- -
- ); -} - -function SidebarTrigger({ - className, - onClick, - ...props -}: React.ComponentProps) { - const { toggleSidebar } = useSidebar(); - - return ( - - ); -} - -function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { - const { toggleSidebar } = useSidebar(); - - return ( - - - - setTheme("light")}> - Light - - setTheme("dark")}> - Dark - - setTheme("system")}> - System - - - - ); -} diff --git a/packages/ui/src/lib/theme/default.css b/packages/ui/src/lib/theme/default.css deleted file mode 100644 index 4bc2c75f0..000000000 --- a/packages/ui/src/lib/theme/default.css +++ /dev/null @@ -1,124 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; - -@custom-variant dark (&:is(.dark *)); - -:root { - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --destructive-foreground: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --radius: 0.625rem; - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.439 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -@layer base { - * { - @apply border-border outline-ring/50; - } - - body { - @apply bg-background text-foreground; - } -} \ No newline at end of file diff --git a/packages/ui/src/lib/utils.ts b/packages/ui/src/lib/utils.ts deleted file mode 100644 index 365058ceb..000000000 --- a/packages/ui/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json deleted file mode 100644 index 1ab3219aa..000000000 --- a/packages/ui/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "target": "ES2021", - "lib": ["ES2021", "DOM"], - "jsx": "react-jsx", - "module": "ESNext", - "moduleResolution": "Bundler", - "skipLibCheck": true, - "declaration": true, - "declarationMap": true, - "emitDeclarationOnly": false, - "noUncheckedIndexedAccess": true, - "erasableSyntaxOnly": true, - "outDir": "dist", - "rootDir": "src", - "strict": true, - "types": ["react", "react-dom"], - "baseUrl": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src"] -} diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts deleted file mode 100644 index e7e4bcdb0..000000000 --- a/packages/ui/vite.config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import path from "node:path"; -import tailwindcss from "@tailwindcss/vite"; -import react from "@vitejs/plugin-react"; -import { defineConfig } from "vite"; -import dts from "vite-plugin-dts"; -import { viteStaticCopy } from "vite-plugin-static-copy"; - -export default defineConfig({ - plugins: [ - react(), - tailwindcss(), - dts({ - entryRoot: "src", - outDir: "dist", - insertTypesEntry: true, - copyDtsFiles: true, - }), - viteStaticCopy({ - targets: [ - { - src: "src/lib/theme/default.css", - dest: "theme", - }, - ], - }), - ], - resolve: { - alias: { - "@": path.resolve(__dirname, "./src"), - }, - }, - build: { - emptyOutDir: true, - lib: { - entry: "src/index.ts", - name: "MeshtasticUI", - formats: ["es"], - fileName: () => "index.js", - }, - rollupOptions: { - external: [ - "react", - "react-dom", - "tailwindcss", - "class-variance-authority", - "tailwind-merge", - "@radix-ui/react-slot", - ], - }, - sourcemap: true, - target: "es2021", - }, -}); diff --git a/packages/web/ARCHITECTURE.md b/packages/web/ARCHITECTURE.md new file mode 100644 index 000000000..9c39d38ce --- /dev/null +++ b/packages/web/ARCHITECTURE.md @@ -0,0 +1,793 @@ +# Meshtastic Web: Architecture Documentation + +## Overview + +Meshtastic Web is the official web client for [Meshtastic](https://meshtastic.org) mesh radio networks. This document covers the architecture of approximately 46,000 lines of TypeScript/React code across 270+ files. The client enables users to configure devices, send messages, view nodes on a map, and monitor mesh network health. + +**Live instances:** +- Production: [client.meshtastic.org](https://client.meshtastic.org) +- Staging: [client-test.meshtastic.org](https://client-test.meshtastic.org) + +## Core Architecture + +The application follows a feature-based architecture with clear separation of concerns: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ UI Layer │ +│ React 19 + TanStack Router + Tailwind CSS + shadcn/ui │ +├─────────────────────────────────────────────────────────────┤ +│ State Layer │ +│ Zustand (deviceStore, uiStore) + React Context │ +├─────────────────────────────────────────────────────────────┤ +│ Data Layer │ +│ Drizzle ORM + SQLite (via sqlocal) + Repositories │ +├─────────────────────────────────────────────────────────────┤ +│ Transport Layer │ +│ @meshtastic/core + Web Bluetooth/Serial/HTTP transports │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Technology Stack + +| Category | Technologies | +|----------|-------------| +| **Framework** | React 19, TypeScript 5.9, Vite 7 | +| **Routing** | TanStack Router with type-safe routes | +| **State** | Zustand with subscribeWithSelector middleware | +| **Database** | SQLite via sqlocal, Drizzle ORM | +| **Styling** | Tailwind CSS 4, shadcn/ui components | +| **Forms** | React Hook Form + Zod validation | +| **Maps** | MapLibre GL + react-map-gl | +| **i18n** | i18next with 20+ language support | +| **Testing** | Vitest + Testing Library | + +## Directory Structure + +``` +src/ +├── app/ # App shell +│ ├── App.tsx # Root application component +│ ├── routes.tsx # Route definitions with guards +│ └── layouts/ # Layout components +│ ├── AppLayout.tsx # Main layout wrapper +│ └── AppSidebar.tsx # Navigation sidebar +│ +├── features/ # Feature modules (colocated code) +│ ├── connections/ # Device connection management +│ │ ├── pages/ # ConnectionsPage +│ │ ├── components/ # AddConnectionDialog, SupportedBadge, etc. +│ │ ├── hooks/ # useConnections +│ │ └── utils.ts # Connection utilities +│ │ +│ ├── messages/ # Messaging feature +│ │ ├── pages/ # MessagesPage +│ │ ├── components/ # ChatPanel, MessageBubble, MessageInput +│ │ └── hooks/ # useMessages, useMessageDraft, useUnreadCount +│ │ +│ ├── nodes/ # Node management feature +│ │ ├── pages/ # NodesPage +│ │ ├── components/ # Node-specific components +│ │ ├── hooks/ # useNodes +│ │ └── utils/ # nodeSort, signalColor +│ │ +│ ├── map/ # Map feature +│ │ ├── pages/ # MapPage +│ │ └── components/ # Map, Layers/, Markers/, Popups/, Tools/ +│ │ +│ ├── preferences/ # App preferences feature +│ │ └── pages/ # PreferencesPage +│ │ +│ └── settings/ # Settings feature +│ ├── pages/ # SettingsPage, RadioConfig, DeviceConfig, ModuleConfig +│ ├── hooks/ # useChannelForm, useConfigForm, useDeviceForm, etc. +│ ├── components/ +│ │ ├── panels/ # Config panels +│ │ │ ├── Channels/ # Channel configuration (Channel.tsx, Channels.tsx, validation.ts) +│ │ │ ├── Device/ # Device settings +│ │ │ ├── Security/ # Security settings +│ │ │ └── *.tsx # Bluetooth, Display, LoRa, Network, Position, Power +│ │ ├── modules/ # MQTT, Telemetry, CannedMessage, etc. +│ │ ├── form/ # ConfigFormFields, FormInput, FormSelect +│ │ └── activity/ # ActivityPanel, ActivityItem +│ └── validation/ +│ ├── config/ # Zod schemas for device config +│ └── moduleConfig/ # Zod schemas for module config +│ +├── shared/ # Truly shared code +│ ├── components/ +│ │ ├── ui/ # shadcn/ui primitives (button, card, dialog, etc.) +│ │ ├── Badge/ # ConnectionStatusBadge +│ │ ├── Dialog/ # Modal dialogs (RemoveNodeDialog, UnsafeRolesDialog, etc.) +│ │ ├── Filter/ # Filter components +│ │ ├── Table/ # Table component +│ │ ├── BatteryStatus.tsx # Battery status indicator +│ │ ├── DeviceImage.tsx # Device image component +│ │ ├── LanguageSwitcher.tsx +│ │ ├── MeshNetwork.tsx # Mesh network visualization +│ │ ├── Mono.tsx # Monospace text component +│ │ ├── NodeAvatar.tsx # Node avatar component +│ │ ├── OnlineIndicator.tsx # Online status indicator +│ │ ├── SignalIndicator.tsx # Signal strength indicator +│ │ ├── TimeAgo.tsx # Relative time display +│ │ ├── Toaster.tsx # Toast notifications +│ │ └── WelcomeSplash.tsx # Welcome screen +│ │ +│ ├── hooks/ # Shared hooks +│ │ ├── useBrowserFeatureDetection.ts +│ │ ├── useCopyToClipboard.ts +│ │ ├── useDebounce.ts +│ │ ├── useDeleteMessages.ts +│ │ ├── useDeviceCommands.ts # Device command interface +│ │ ├── useFavoriteNode.ts +│ │ ├── useFilter.ts +│ │ ├── useIgnoreNode.ts +│ │ ├── useIsMobile.ts +│ │ ├── useLanguage.ts +│ │ ├── useLocalStorage.ts +│ │ ├── useMapFitting.ts +│ │ ├── useMyNode.ts # Device context hooks (useMyNode, useNodeNum, useNodeNumSafe) +│ │ ├── usePasswordVisibilityToggle.ts +│ │ ├── usePinnedItems.ts +│ │ ├── usePositionFlags.ts +│ │ ├── useTheme.ts +│ │ ├── useToast.ts +│ │ ├── useTraceroute.ts +│ │ └── useWindowFocus.ts +│ │ +│ └── utils/ # Shared utilities +│ ├── bitwise.ts +│ ├── cn.ts # className utility +│ ├── color.ts +│ ├── debounce.ts +│ ├── deepCompareConfig.ts +│ ├── dotPath.ts +│ ├── eventBus.ts +│ ├── geo.ts +│ ├── github.ts +│ ├── ip.ts +│ ├── messagePipelineHandlers.ts +│ ├── pskSchema.ts +│ ├── randId.ts +│ ├── sort.ts +│ ├── string.ts +│ ├── typeGuards.ts +│ └── x25519.ts +│ +├── state/ # Global state management +│ ├── device/ # Device store (Zustand) +│ │ ├── store.ts # Device state, connection tracking +│ │ └── types.ts # Device-related types +│ └── ui/ # UI preferences store +│ ├── index.ts # Re-exports +│ └── store.ts # Theme, language, column visibility +│ +├── data/ # Data layer +│ ├── schema.ts # Drizzle ORM schema definitions +│ ├── client.ts # SQLite client initialization +│ ├── types.ts # Data layer types +│ ├── errors.ts # Data error types +│ ├── packetBatcher.ts # Packet batching utility +│ ├── subscriptionService.ts # Data subscription service +│ ├── migrationService.ts # Database migration service +│ ├── repositories/ # Data access layer +│ │ ├── ChannelRepository.ts +│ │ ├── ConnectionRepository.ts +│ │ ├── DeviceRepository.ts +│ │ ├── MessageRepository.ts +│ │ ├── NodeRepository.ts +│ │ ├── PacketLogRepository.ts +│ │ ├── PendingChangesRepository.ts # Pending config changes only +│ │ ├── PreferencesRepository.ts +│ │ └── TracerouteRepository.ts +│ ├── hooks/ # Database hooks +│ │ ├── useChannels.ts +│ │ ├── useConfig.ts # Config from Zustand store +│ │ ├── useDevicePreference.ts +│ │ ├── useDisplayUnits.ts # Display unit preferences +│ │ ├── useLoraConfig.ts # LoRa config hook +│ │ ├── useNodes.ts +│ │ ├── usePacketLogs.ts +│ │ ├── usePendingChanges.ts # Pending config changes CRUD +│ │ ├── usePreferences.ts +│ │ └── useSignalLogs.ts +│ └── migrations/ # SQL migration files +│ +├── core/ # Core services +│ ├── dto/ # Data transfer objects +│ │ ├── NodeNumToNodeInfoDTO.ts +│ │ └── PacketToMessageDTO.ts +│ └── services/ # Business logic services +│ ├── adminMessageService.ts +│ ├── configBackupService.ts +│ ├── maintenanceService.ts +│ └── logger.ts +│ +├── tests/ # Test configuration +│ ├── setup.ts +│ └── test-utils.tsx +│ +├── i18n-config.ts # Internationalization config +├── index.tsx # Application entry point +└── index.css # Global styles +``` + +## Path Aliases + +The project uses TypeScript path aliases for clean imports: + +| Alias | Path | +|-------|------| +| `@app/*` | `./src/*` | +| `@features/*` | `./src/features/*` | +| `@shared/*` | `./src/shared/*` | +| `@state/*` | `./src/state/*` | +| `@data/*` | `./src/data/*` | +| `@core/*` | `./src/core/*` | +| `@public/*` | `./public/*` | + +## State Management + +### Device Store (`src/state/device/store.ts`) +- Manages the active Meshtastic device connection +- Tracks connection phase: `disconnected → connecting → configuring → connected → configured` +- Stores **ephemeral state** (not persisted): + - `connection` - MeshDevice instance for sending packets + - `hardware` - MyNodeInfo from device + - `configProgress` - Config loading progress during connection + - `remoteAdminTargetNode` - Node being remotely administered + - `queuedAdminMessages` - Admin messages waiting to be sent + - `configConflicts` - Detected conflicts between local and remote config +- **Note:** Device config is stored in Zustand (`device.config`, `device.moduleConfig`). Pending changes are tracked in SQLite via `usePendingChanges()`. + +### Accessing the Current Device + +Use the `useDevice()` hook from `@state/index.ts` to access the device connection and config: + +```typescript +import { useDevice } from "@state/index.ts"; + +function MyComponent() { + const device = useDevice(); + // device.connection - MeshDevice for sending packets + // device.hardware - MyNodeInfo from device + // device.config - LocalConfig (device, lora, network, etc.) + // device.moduleConfig - LocalModuleConfig (mqtt, telemetry, etc.) +} +``` + +**For config data**, use the config hooks: + +```typescript +import { useConfig, useConfigVariant, useModuleConfigVariant } from "@data/hooks"; + +function MyComponent() { + const { config, moduleConfig, isLoading } = useConfig(); + const loraConfig = useConfigVariant("lora"); + const mqttConfig = useModuleConfigVariant("mqtt"); +} +``` + +**Note:** For database queries that need the device's node number, use `useMyNode()`: + +```typescript +import { useMyNode } from "@shared/hooks"; + +function MyComponent() { + const { myNodeNum } = useMyNode(); + const { nodes } = useNodes(myNodeNum); // Query database by nodeNum +} +``` + +### UI Store (`src/state/ui/store.ts`) +- **Ephemeral state only** (not persisted): + - Modal/dialog visibility via `dialogs` object and `setDialogOpen()`/`getDialogOpen()` methods + - Command palette state + - Message tab state (`messageTabs`, `activeMessageTabId`, `secondaryMessageTabId`, `messageSplitMode`) + - Connect dialog state (`connectDialogOpen`) + - Node details state (`nodeNumDetails`, `tracerouteNodeNum`) +- Exports `DEFAULT_PREFERENCES` for use with the preferences hook +- Exports `Dialogs` and `DialogVariant` types + +### Preferences System + +User preferences are persisted to SQLite using reactive queries: + +``` +Component ──> usePreference(key, default) ──> useReactiveQuery ──> PreferencesRepository (SQLite) + ↑ + (auto-updates on DB changes) +``` + +**Key files:** +- `src/data/hooks/usePreferences.ts` - Preferences hook using `useReactiveQuery` +- `src/data/repositories/PreferencesRepository.ts` - Database access with query builders +- `src/state/ui/store.ts` - `DEFAULT_PREFERENCES` constants + +**Usage:** +```typescript +import { usePreference } from "@data/hooks"; +import { DEFAULT_PREFERENCES } from "@state/ui"; + +const [theme, setTheme] = usePreference("theme", DEFAULT_PREFERENCES.theme); +``` + +The `usePreference` hook uses `preferencesRepo.buildPreferenceQuery(key)` with `useReactiveQuery` for automatic updates when preferences change. + +**Available preferences:** +- `theme` - Light/dark/system theme +- `compactMode` - Compact UI mode +- `showNodeAvatars` - Show node avatars +- `language` - UI language +- `timeFormat` - 12h/24h time format +- `distanceUnits` - Imperial/metric +- `coordinateFormat` - DD/DMS/UTM +- `mapStyle` - Map tile style +- `showNodeLabels` - Show labels on map +- `showConnectionLines` - Show connection lines on map +- `autoCenterOnPosition` - Auto-center map on position updates +- `masterVolume` - Audio volume (0-100) +- `messageSoundEnabled` - Message notification sounds +- `alertSoundEnabled` - Alert sounds +- `packetBatchSize` - Packet batching size +- `nodesTableColumnVisibility` - Visible columns in nodes table +- `nodesTableColumnOrder` - Column ordering in nodes table +- `rasterSources` - Custom map raster sources + +## Device-Scoped Architecture + +The application supports multiple Meshtastic devices. All data is scoped to a specific device using `ownerNodeNum` (the device's node number) as a foreign key. The current device context is determined by the URL. + +### URL-Based Device Context + +After connecting to a device, the user is navigated to `/$nodeNum/messages?channel=0`. The `nodeNum` URL parameter identifies which device's data to display. + +``` +URL: /2662173639/messages?channel=0 + ↑ + └── nodeNum identifies the connected device +``` + +### Accessing the Current Device's nodeNum + +Use the `useMyNode` hook to get the current device context: + +```typescript +import { useMyNode } from "@shared/hooks"; + +function MyComponent() { + const { myNodeNum, myNode } = useMyNode(); + + // myNodeNum: number | undefined - extracted from URL params + // myNode: Node | undefined - the device's node data from database + + if (!myNodeNum) { + return
Not connected
; + } + + // Use myNodeNum for queries + const { messages } = useChannelMessages(myNodeNum, channelId, 100); +} +``` + +**Implementation details:** +- `useMyNode()` internally calls `useNodeNumSafe()` which extracts `nodeNum` from TanStack Router's `useParams()` +- Returns `undefined` when not in a connected route (e.g., `/connect` page) +- The `connectedLayoutRoute` validates the nodeNum and checks device existence before rendering + +### Database Scoping with ownerNodeNum + +All database tables use `ownerNodeNum` as a scoping key: + +```typescript +// Saving a message - use myNodeNum as ownerNodeNum +const newMessage: NewMessage = { + ownerNodeNum: myNodeNum, // Critical: scope to current device + messageId: packetId, + type: "channel", + channelId: 0, + // ... other fields +}; +await messageRepo.saveMessage(newMessage); + +// Querying messages - filter by ownerNodeNum +const query = messageRepo.buildBroadcastMessagesQuery(myNodeNum, channelId, limit); +// This generates: SELECT * FROM messages WHERE ownerNodeNum = ? AND ... +``` + +**Key tables with ownerNodeNum:** +- `messages` - Scopes messages to the device that received/sent them +- `nodes` - Scopes discovered nodes to the device that saw them +- `channels` - Scopes channel configs to the device they belong to +- `positionLogs`, `telemetryLogs`, `packetLogs` - Scopes logs to the receiving device + +This architecture enables: +- Multiple devices to store data without conflicts +- Switching between devices without data mixing +- Per-device message history and node discovery + +## Database Layer + +Uses Drizzle ORM with SQLite (sqlocal) for client-side persistence: + +| Table | Purpose | +|-------|---------| +| `messages` | Direct and channel messages with delivery state | +| `nodes` | Node metadata, position, telemetry (deduplicated by deviceId+nodeNum) | +| `channels` | Channel configuration per device | +| `connections` | Saved HTTP/Bluetooth/Serial connections | +| `positionLogs` | Historical position data | +| `telemetryLogs` | Historical battery/environment metrics | +| `packetLogs` | Raw packet metadata for debugging | +| `tracerouteLogs` | Route discovery results | +| `preferences` | Key-value user preferences | +| `configChanges` | Pending local config changes not yet saved to device | + +**Note:** Device config is stored in Zustand (not SQLite) and received fresh from the device on each connection. + +### Repository Pattern + +Data access is abstracted through repositories in `src/data/repositories/`: +- `MessageRepository` - Message CRUD with conversation queries +- `NodeRepository` - Node upsert with position/telemetry updates +- `ChannelRepository` - Channel configuration management +- `ConnectionRepository` - Saved connection management +- `DeviceRepository` - Device metadata management +- `PacketLogRepository` - Raw packet logging for debugging +- `PendingChangesRepository` - Pending config changes (not yet saved to device) +- `PreferencesRepository` - Key-value user preferences +- `TracerouteRepository` - Route discovery results + +#### Query Builder Pattern with Reactive Updates + +Repositories expose two types of methods: + +1. **Mutation methods** - Execute database operations directly: + ```typescript + await nodeRepo.upsertNode(node); + await messageRepo.saveMessage(message); + ``` + +2. **Query builder methods** - Return Drizzle query objects (not executed) for use with `useReactiveQuery`: + ```typescript + // Repository method returns query object + buildNodesQuery(ownerNodeNum: number) { + return this.db.select().from(nodes).where(eq(nodes.ownerNodeNum, ownerNodeNum)); + } + ``` + +This pattern enables automatic reactivity via sqlocal's `useReactiveQuery`, which subscribes to database changes and re-runs queries when relevant tables are modified: + +```typescript +// Hook using repository query builder +export function useNodes(deviceId: number) { + const query = useMemo(() => nodeRepo.buildNodesQuery(deviceId), [deviceId]); + const { data, status } = useReactiveQuery(nodeRepo.getClient(), query); + + // data automatically updates when nodes table changes + return { nodes: data ?? [], isLoading: status === "pending" }; +} +``` + +**Repository methods:** + +| Repository | Query Builders | +|------------|----------------| +| `NodeRepository` | `buildNodesQuery()`, `buildOnlineNodesQuery()`, `buildPositionHistoryQuery()`, `buildTelemetryHistoryQuery()` | +| `MessageRepository` | `buildDirectMessagesQuery()`, `buildBroadcastMessagesQuery()`, `buildAllMessagesQuery()`, `buildPendingMessagesQuery()`, `buildAllDirectMessagesQuery()`, `buildAllChannelMessagesQuery()`, `buildLastReadQuery()` | +| `ConnectionRepository` | `buildConnectionsQuery()`, `buildConnectionQuery()`, `buildDefaultConnectionQuery()` | +| `ChannelRepository` | `buildChannelsQuery()`, `buildChannelQuery()`, `buildPrimaryChannelQuery()` | +| `PacketLogRepository` | `buildPacketLogsQuery()`, `buildSignalLogsQuery()` | +| `PreferencesRepository` | `buildPreferenceQuery()`, `buildAllPreferencesQuery()` | +| `PendingChangesRepository` | `buildChangesQuery()` | + +**Dependency injection:** Each repository exposes `getClient(client?: SQLocalDrizzle)` for testing: +```typescript +// Production - uses default client +const { data } = useReactiveQuery(nodeRepo.getClient(), query); + +// Testing - inject mock client +const { data } = useReactiveQuery(nodeRepo.getClient(mockClient), query); +``` + +## Config Management Architecture + +The application uses a **Zustand-centric architecture** for device configuration: +- Config is stored in the Zustand device store, received fresh from the device on each connection +- Pending changes (edits not yet saved to device) are tracked in SQLite +- Single source of truth: Zustand for current config, SQLite for pending changes + +### Data Flow + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Config Data Flow │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Device ──(packets)──▶ Zustand device store ──▶ useConfig() ──▶ UI │ +│ (device.config, device.moduleConfig) │ +│ │ +│ Form edits ──▶ config_changes table ──▶ usePendingChanges() ──▶ UI │ +│ (pending changes) │ +│ │ +│ Save button ──▶ adminCommands.saveAllPendingChanges() ──▶ Device │ +│ (builds protobuf from Zustand base + pending changes) │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Config Storage + +| Location | What's Stored | +|----------|---------------| +| Zustand `device.config` | Current LocalConfig from device (8 variants) | +| Zustand `device.moduleConfig` | Current LocalModuleConfig from device (13 variants) | +| SQLite `config_changes` | Field-level pending changes (not yet saved to device) | + +### Config Hooks + +| Hook | File | Purpose | +|------|------|---------| +| `useConfig` | `useConfig.ts` | Read config from Zustand store | +| `useConfigVariant` | `useConfig.ts` | Read specific config variant (e.g., "lora") | +| `useModuleConfigVariant` | `useConfig.ts` | Read specific module config variant | +| `usePendingChanges` | `usePendingChanges.ts` | CRUD for pending config changes in SQLite | + +### Form Integration + +Settings forms read from Zustand and track changes in SQLite: + +```typescript +// Reading config for forms +const device = useDevice(); +const baseConfig = device?.config?.lora ?? null; +const hasReceivedConfig = device?.configProgress?.receivedConfigs?.has("config:lora"); + +// Saving field changes +const { saveChange, clearChange } = usePendingChanges(myNodeNum); +await saveChange({ + changeType: "config", + variant: "lora", + fieldPath: "region", + value: newRegion, + originalValue: baseConfig?.region, +}); +``` + +### Activity Panel Undo Mechanism + +The Activity Panel (`src/features/settings/components/activity/`) shows pending config changes and allows users to undo individual changes. When a change is removed, the corresponding form input must reset to its original value. + +**Problem:** Form hooks sync to `effectiveValues` (base + pending changes) on each keystroke. Simply removing a change from the database doesn't immediately reset the input because the form's internal state still holds the changed value. + +**Solution:** Explicit undo via Zustand store action with stored original value. + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Activity Undo Flow │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ActivityItem.originalValue ──▶ useActivityChanges.removeChange() │ +│ │ │ +│ ▼ │ +│ useUIStore.resetField({ changeType, variant, │ +│ fieldPath, value }) │ +│ │ │ +│ ▼ │ +│ Form hook subscribes to pendingFieldReset │ +│ (useUserForm, useConfigForm, etc.) │ +│ │ │ +│ ▼ │ +│ form.setValue(fieldPath, originalValue) │ +│ useUIStore.clearPendingReset() │ +│ │ │ +│ ▼ │ +│ clearChange() removes from config_changes table │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Key components:** + +| Component | File | Role | +|-----------|------|------| +| `ActivityItem.originalValue` | `activity/types.ts` | Stores the value to restore when undoing | +| `PendingFieldReset` | `ui/store.ts` | Interface for reset action payload | +| `resetField()` | `ui/store.ts` | Zustand action to dispatch reset | +| `clearPendingReset()` | `ui/store.ts` | Clears the pending reset after handling | +| Form hooks | `useUserForm.ts`, `useConfigForm.ts`, etc. | Subscribe to `pendingFieldReset` and call `form.setValue()` | + +**Form hook subscription pattern:** + +```typescript +// In each form hook (useUserForm, useConfigForm, useModuleConfigForm, etc.) +const pendingReset = useUIStore((s) => s.pendingFieldReset); + +useEffect(() => { + if ( + pendingReset?.changeType === "config" && + pendingReset.variant === configType && + pendingReset.fieldPath + ) { + form.setValue(pendingReset.fieldPath, pendingReset.value); + useUIStore.getState().clearPendingReset(); + } +}, [pendingReset, form, configType]); +``` + +## Feature Modules + +### Connections (`src/features/connections/`) +- Device connection management (Bluetooth, Serial, HTTP) +- Connection status tracking +- Saved connections persistence + +#### Connection Flow + +The connections feature demonstrates the **Service + Hook** pattern used throughout the app for managing imperative, stateful operations. + +**ConnectionService** (`services/ConnectionService.ts`) - A singleton class handling all **imperative work**: +- **Transport lifecycle** - Creating/destroying serial, bluetooth, and HTTP transports +- **MeshDevice management** - Creating MeshDevice instances, subscribing to device events +- **Connection state machine** - Managing status transitions (connecting → configuring → connected) +- **Event subscriptions** - Subscribing to `onMyNodeInfo`, `onConfigPacket`, `onConfigComplete`, etc. +- **Heartbeat management** - Starting/stopping heartbeat intervals +- **Cache logic** - Checking for cached config/nodes for fast reconnection +- **Database updates** - Updating connection status via repositories + +**useConnections** (`hooks/useConnections.ts`) - A React hook providing a **reactive interface**: +- **React state synchronization** - Using `useSyncExternalStore` to subscribe to changes +- **Exposing actions** - Wrapping ConnectionService methods in React-friendly callbacks +- **Data fetching** - Refreshing connection list from the database +- **Cache management** - Maintaining a local cache for synchronous React rendering + +``` +┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────┐ +│ React Component │────▶│ useConnections │────▶│ ConnectionService│ +│ (ConnectionsPage) │ │ (hook) │ │ (singleton) │ +└─────────────────────┘ └──────────────────────┘ └─────────────────┘ + ▲ │ │ + │ │ ▼ + │ │ ┌─────────────────┐ + │ │ │ MeshDevice │ + │ │ │ Transport │ + └───────────────────────────┘ │ Database │ + (state updates via └─────────────────┘ + useSyncExternalStore) +``` + +**Why this split:** +- **Testability** - ConnectionService can be tested without React +- **Separation of concerns** - Imperative I/O vs reactive UI +- **Lifecycle management** - Service persists across component mounts/unmounts +- **Singleton pattern** - One connection manager, many observing components + +### Messages (`src/features/messages/`) +- Chat interface for channel and direct messages +- Message drafts and delivery status +- Unread message tracking + +### Nodes (`src/features/nodes/`) +- Node list with sorting and filtering +- Node details drawer with telemetry charts +- Signal metrics visualization +- Signal grading algorithm (matches Meshtastic firmware) + +### Map (`src/features/map/`) +- Full map view with node markers +- Position trails and precision circles +- SNR visualization layer +- Waypoint management + +### Preferences (`src/features/preferences/`) +- App-level user preferences +- Theme and language settings + +### Settings (`src/features/settings/`) +- Device configuration forms +- Module configuration +- Validation schemas (Zod) +- Field change tracking + +## Signal Grading Algorithm + +The signal grading logic (`src/features/nodes/utils/signalColor.ts`) matches Meshtastic firmware: + +```typescript +function getSignalGrade(snr: number, rssi: number, snrLimit: number): SignalGradeResult { + if (snr > snrLimit && rssi > -10) return { grade: "Good", bars: 5 }; + if (snr > snrLimit && rssi > -20) return { grade: "Good", bars: 4 }; + if (snr > 0 && rssi > -50) return { grade: "Good", bars: 3 }; + if (snr > -10 && rssi > -100) return { grade: "Fair", bars: 2 }; + return { grade: "Bad", bars: 1 }; +} +``` + +SNR limits vary by modem preset: +- Long range presets: -6.0 dB +- Medium range presets: -5.5 dB +- Short range presets: -4.5 dB + +## Routes + +Routes are organized around the connected device's `nodeNum`: + +| Path | Component | Description | +|------|-----------|-------------| +| `/connect` | `ConnectPage` | Device connection management | +| `/$nodeNum/messages` | `MessagesPage` | Chat interface (channel/direct) | +| `/$nodeNum/map` | `MapPage` | Full map view with nodes | +| `/$nodeNum/map/$long/$lat/$zoom` | `MapPage` | Map with specific coordinates | +| `/$nodeNum/nodes` | `NodesPage` | Node list with signal/telemetry | +| `/$nodeNum/settings` | `SettingsPage` | Device configuration | +| `/$nodeNum/settings/radio` | `RadioConfig` | LoRa settings | +| `/$nodeNum/settings/device` | `DeviceConfig` | Device settings | +| `/$nodeNum/settings/module` | `ModuleConfig` | Module configuration | + +Routes under `/$nodeNum/*` require a valid device and redirect to `/connect` if the device doesn't exist. + +## Development + +```bash +# Install dependencies +pnpm install + +# Start dev server +pnpm dev + +# Type checking +pnpm typecheck + +# Run tests +pnpm test + +# Build for production +pnpm build + +# Database migrations +pnpm db:generate # Generate migrations from schema +pnpm db:check # Validate migrations +``` + +## Design Patterns + +| Pattern | Usage | +|---------|-------| +| **Feature-based Organization** | Colocated code by feature domain | +| **Repository with Query Builders** | Database access abstraction with reactive query support | +| **Factory** | Device store creation | +| **State Machine** | Drawer navigation, connection phases | +| **Observer** | Zustand subscriptions, event bus | +| **Lazy Loading** | Route-based code splitting with Suspense | +| **Reactive Queries** | `useReactiveQuery` for automatic UI updates on database changes | + +## Code Standards + +- No `any` types +- No non-null assertion operator (`!`) +- No type assertions (`as Type`) +- Use `isDefined()` type guard for null checks +- Prefer `flatMap` over `filter().map()` for type narrowing +- State machines over simple boolean state for complex UI + +## Key Files Reference + +| File | Purpose | +|------|---------| +| `src/app/routes.tsx` | Route definitions with guards | +| `src/data/schema.ts` | Database schema definitions | +| `src/state/device/store.ts` | Device connection state (ephemeral) | +| `src/state/ui/store.ts` | UI state, dialogs, and DEFAULT_PREFERENCES | +| `src/data/hooks/useConfig.ts` | Base config from database | +| `src/data/hooks/usePendingChanges.ts` | Pending config changes CRUD + effective config (base + changes) | +| `src/data/hooks/useWorkingHashes.ts` | Hash-based change detection | +| `src/data/hooks/usePreferences.ts` | User preferences hook | +| `src/data/repositories/ConfigCacheRepository.ts` | Config and changes persistence | +| `src/data/repositories/PreferencesRepository.ts` | Preferences persistence | +| `src/shared/hooks/useMyNode.ts` | Device context hooks (useMyNode, useNodeNum, useNodeNumSafe) | +| `src/features/nodes/utils/signalColor.ts` | Signal grading algorithm | +| `src/shared/utils/typeGuards.ts` | Type guard utilities | +| `src/core/utils/merkleConfig.ts` | Config hashing utilities | +| `src/core/services/adminMessageService.ts` | Admin message handling | +| `src/data/repositories/NodeRepository.ts` | Node data access | +| `src/shared/hooks/useFavoriteNode.ts` | Favorite node management | diff --git a/packages/web/components.json b/packages/web/components.json new file mode 100644 index 000000000..3b9b7cc29 --- /dev/null +++ b/packages/web/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} diff --git a/packages/web/drizzle.config.ts b/packages/web/drizzle.config.ts new file mode 100644 index 000000000..3ce21892e --- /dev/null +++ b/packages/web/drizzle.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/data/schema.ts", + out: "./src/data/migrations", + dialect: "sqlite", + driver: "expo", +}); diff --git a/packages/web/index.html b/packages/web/index.html index 30ad400c1..2bd077bfc 100644 --- a/packages/web/index.html +++ b/packages/web/index.html @@ -5,12 +5,11 @@ <%- cookieYesScript %> - + - - + This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/bg-BG/ui.json b/packages/web/public/i18n/locales/bg-BG/ui.json index a58b34aed..8e1a23403 100644 --- a/packages/web/public/i18n/locales/bg-BG/ui.json +++ b/packages/web/public/i18n/locales/bg-BG/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "Това е малко смущаващо...", - "description1": "Наистина съжаляваме, но възникна грешка в web клиента, която доведе до срив.
Това не би трябвало да се случва и работим усилено, за да го поправим.", + "description1": "Наистина съжаляваме, но възникна грешка в web клиента, която доведе до срив. Това не би трябвало да се случва и работим усилено, за да го поправим.", "description2": "Най-добрият начин да предотвратите това да се случи отново с вас или с някой друг е да ни съобщите за проблема.", "reportInstructions": "Моля, включете следната информация в доклада си:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/cs-CZ/ui.json b/packages/web/public/i18n/locales/cs-CZ/ui.json index bc4862d89..eb1e726b1 100644 --- a/packages/web/public/i18n/locales/cs-CZ/ui.json +++ b/packages/web/public/i18n/locales/cs-CZ/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/de-DE/ui.json b/packages/web/public/i18n/locales/de-DE/ui.json index 43670beeb..b6cfd9088 100644 --- a/packages/web/public/i18n/locales/de-DE/ui.json +++ b/packages/web/public/i18n/locales/de-DE/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "Das ist ein wenig peinlich...", - "description1": "Es tut uns wirklich leid, aber im Webclient ist ein Fehler aufgetreten, der es zum Absturz gebracht hat.
Das soll nicht passieren, und wir arbeiten hart daran, es zu beheben.", + "description1": "Es tut uns wirklich leid, aber im Webclient ist ein Fehler aufgetreten, der es zum Absturz gebracht hat. Das soll nicht passieren, und wir arbeiten hart daran, es zu beheben.", "description2": "Der beste Weg, um zu verhindern, dass sich dies Ihnen oder irgendjemand anderem wiederholt, besteht darin, uns über dieses Problem zu berichten.", "reportInstructions": "Bitte fügen Sie folgende Informationen in Ihren Bericht ein:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/en/channels.json b/packages/web/public/i18n/locales/en/channels.json index 8f2b390d6..d27384f9a 100644 --- a/packages/web/public/i18n/locales/en/channels.json +++ b/packages/web/public/i18n/locales/en/channels.json @@ -1,4 +1,18 @@ { + "user": { + "longName": { + "validation": { + "min": "Long name must be at least 1 character", + "max": "Long name must be at most 39 characters" + } + }, + "shortName": { + "validation": { + "min": "Short name must be at least 1 character", + "max": "Short name must be at most 4 characters" + } + } + }, "page": { "sectionLabel": "Channels", "channelName": "Channel: {{channelName}}", diff --git a/packages/web/public/i18n/locales/en/commandPalette.json b/packages/web/public/i18n/locales/en/commandPalette.json index 665adbf39..bc8d6ebe8 100644 --- a/packages/web/public/i18n/locales/en/commandPalette.json +++ b/packages/web/public/i18n/locales/en/commandPalette.json @@ -44,8 +44,7 @@ "label": "Debug", "command": { "reconfigure": "Reconfigure", - "clearAllStoredMessages": "Clear All Stored Messages", - "clearAllStores": "Clear All Local Storage" + "clearAllStoredMessages": "Clear All Stored Messages" } } } diff --git a/packages/web/public/i18n/locales/en/common.json b/packages/web/public/i18n/locales/en/common.json index db09d144c..caf735bf0 100644 --- a/packages/web/public/i18n/locales/en/common.json +++ b/packages/web/public/i18n/locales/en/common.json @@ -27,24 +27,28 @@ "reset": "Reset", "retry": "Retry", "save": "Save", + "loading": "Loading", + "saving": "Saving", "setDefault": "Set as default", "unsetDefault": "Unset default", "scanQr": "Scan QR Code", "traceRoute": "Trace Route", - "submit": "Submit" + "submit": "Submit", + "MQTT": "MQTT" }, "app": { "title": "Meshtastic", "fullTitle": "Meshtastic Web Client" }, - "loading": "Loading...", "unit": { "cps": "CPS", "dbm": "dBm", + "db": "dB", "hertz": "Hz", + "snr": "SNR", "hop": { - "one": "Hop", - "plural": "Hops" + "one": "hop", + "plural": "hops" }, "hopsAway": { "one": "{{count}} hop away", @@ -69,9 +73,9 @@ "today": "Today", "yesterday": "Yesterday" }, + "change": { "one": "change", "plural": "changes" }, "month": { "one": "Month", "plural": "Months" }, "year": { "one": "Year", "plural": "Years" }, - "snr": "SNR", "volt": { "one": "Volt", "plural": "Volts", "suffix": "V" }, "record": { "one": "Records", "plural": "Records" }, "degree": { "one": "Degree", "plural": "Degrees", "suffix": "°" } diff --git a/packages/web/public/i18n/locales/en/config.json b/packages/web/public/i18n/locales/en/config.json index 5c62b99b0..36816f0c3 100644 --- a/packages/web/public/i18n/locales/en/config.json +++ b/packages/web/public/i18n/locales/en/config.json @@ -1,4 +1,92 @@ { + "settings": { + "activityList": { + "noPendingChanges": "No pending changes", + "pendingChanges": "Pending Changes", + "emptyPage": "Changes will appear here as you modify settings", + "discardChanges": "Are you sure you want to discard all pending changes?", + "changesWaitingToSave": "{{num}} waiting to be saved", + "clearAllChanges": "Clear all changes", + "categories": { + "User": "User", + "Device": "Device", + "Lora": "LoRa", + "Security": "Security", + "Position": "Position", + "Power": "Power", + "Network": "Network", + "Display": "Display", + "Bluetooth": "Bluetooth", + "MQTT": "MQTT", + "Serial": "Serial", + "Telemetry": "Telemetry", + "Canned Message": "Canned Message", + "External Notification": "External Notification", + "Store Forward": "Store Forward", + "Range Test": "Range Test", + "Audio": "Audio", + "Neighbor Info": "Neighbor Info", + "Ambient Lighting": "Ambient Lighting", + "Detection Sensor": "Detection Sensor", + "Paxcounter": "Paxcounter", + "Channels": "Channels", + "Settings": "Settings" + } + }, + "advanced": { + "administration": { + "title": "Administration", + "description": "Device management actions including reboot, shutdown, and reset options", + "reboot": { + "title": "Reboot", + "description": "Restart the connected device", + "button": "Reboot" + }, + "shutdown": { + "title": "Shutdown", + "description": "Turn off the connected device", + "button": "Shutdown" + }, + "resetNodeDb": { + "title": "Reset Node Database", + "description": "Clear all nodes from the device's node database", + "button": "Reset" + }, + "factoryReset": { + "title": "Factory Reset", + "description": "Reset the device to factory defaults, erasing all configurations and data", + "button": "Factory Reset" + } + }, + "database": { + "title": "Database", + "description": "Manage local database storage and cleanup settings", + "cleanNodes": { + "title": "Clean Node Database", + "description": "Automatically remove nodes from the local database that haven't been heard from recently. Runs weekly.", + "daysLabel": "Clean up nodes last seen older than {{days}} days", + "unknownOnly": "Clean up only unknown nodes", + "queuedCount": "{{count}} nodes queued for deletion", + "lastRun": "Last run: {{- time}}", + "cleanNow": "Clean Now" + } + }, + "debugLog": { + "title": "Debug Log", + "description": "View raw packet log for debugging and troubleshooting", + "button": { "open": "View Debug Log" }, + "packetCount": "{{count}} packets", + "refresh": "Refresh", + "noPackets": "No packets logged" + } + } + }, + "database": { + "cleanNodes": "Clean Node Database", + "noResults": "No seetings found matching {{search}}", + "title": "Database", + "description": "Manage local database storage and cleanup settings" + }, "page": { "title": "Settings", "tabUser": "User", @@ -10,7 +98,47 @@ "tabNetwork": "Network", "tabPosition": "Position", "tabPower": "Power", - "tabSecurity": "Security" + "tabSecurity": "Security", + "lora": { + "title": "LoRa", + "description": "Configure LoRa radio settings" + }, + "channels": { + "title": "Channels", + "description": "Manage mesh network channels" + }, + "security": { + "title": "Security", + "description": "Configure security and encryption settings" + }, + "user": { + "title": "User", + "description": "Configure user profile settings" + }, + "device": { + "title": "Device", + "description": "Configure device hardware settings" + }, + "position": { + "title": "Position", + "description": "Configure GPS and position settings" + }, + "power": { + "title": "Power", + "description": "Configure power management settings" + }, + "network": { + "title": "Network", + "description": "Configure network and WiFi settings" + }, + "display": { + "title": "Display", + "description": "Configure display settings" + }, + "bluetooth": { + "title": "Bluetooth", + "description": "Configure Bluetooth settings" + } }, "sidebar": { "label": "Configuration" diff --git a/packages/web/public/i18n/locales/en/connections.json b/packages/web/public/i18n/locales/en/connections.json index de1b6b067..c1756e058 100644 --- a/packages/web/public/i18n/locales/en/connections.json +++ b/packages/web/public/i18n/locales/en/connections.json @@ -3,6 +3,30 @@ "title": "Connect to a Meshtastic device", "description": "Add a device connection via HTTP, Bluetooth, or Serial. Your saved connections will be saved in your browser." }, + "status": { + "disconnected": "Disconnected", + "connecting": "Connecting...", + "configuring": "Configuring...", + "configured": "Connected", + "connected": "Connected", + "reconnecting": "Reconnecting...", + "error": "Connection error", + "notAuthorized": "Not authorized" + }, + "button": { + "addConnection": "Add Connection", + "connect": "Connect", + "connecting": "Connecting...", + "disconnect": "Disconnect", + "retry": "Retry", + "delete": "Delete", + "cancel": "Cancel", + "setDefault": "Set as default", + "unsetDefault": "Remove default", + "enableAutoReconnect": "Enable auto-reconnect", + "disableAutoReconnect": "Disable auto-reconnect" + }, + "default": "Default", "connectionType_ble": "BLE", "connectionType_serial": "Serial", "connectionType_network": "Network", @@ -11,10 +35,39 @@ "moreActions": "More actions", "noConnections": { "title": "No connections yet.", - "description": "Create your first connection. It will connect immediately and be saved for later." + "description": "Create your first connection. It will be saved for later use." }, "lastConnectedAt": "Last connected: {{date}}", "neverConnected": "Never connected", + "connectionProgress": { + "title": "Connecting", + "initializing": "Initializing connection...", + "waitingForDevice": "Waiting for device response...", + "receivingConfig": "Receiving configuration ({{received}}/{{total}})...", + "syncingNetwork": "Syncing network data...", + "connected": "Connected", + "device": "Device config", + "position": "Position config", + "power": "Power config", + "network": "Network config", + "display": "Display config", + "lora": "LoRa config", + "bluetooth": "Bluetooth config", + "security": "Security config", + "mqtt": "MQTT config", + "serial": "Serial config", + "externalNotification": "External notification config", + "storeForward": "Store and forward config", + "rangeTest": "Range test config", + "telemetry": "Telemetry config", + "cannedMessage": "Canned message config", + "audio": "Audio config", + "remoteHardware": "Remote hardware config", + "neighborInfo": "Neighbor info config", + "ambientLighting": "Ambient lighting config", + "detectionSensor": "Detection sensor config", + "paxcounter": "PAX counter config" + }, "toasts": { "connected": "Connected", "nowConnected": "{{name}} is now connected", diff --git a/packages/web/public/i18n/locales/en/dialog.json b/packages/web/public/i18n/locales/en/dialog.json index 12d3bb1a3..76e731d38 100644 --- a/packages/web/public/i18n/locales/en/dialog.json +++ b/packages/web/public/i18n/locales/en/dialog.json @@ -3,14 +3,20 @@ "description": "This action will clear all message history. This cannot be undone. Are you sure you want to continue?", "title": "Clear All Messages" }, + "deviceShare": { + "title": "Share {{name}}", + "description": "Scan this QR code to add this device as a contact.", + "shareableUrl": "Shareable URL", + "copyUrl": "Copy URL" + }, "import": { - "description": "Import a Channel Set from a Meshtastic URL.
Valid Meshtasic URLs start with \"https://meshtastic.org/e/...\"", + "description": "Import a Channel Set from a Meshtastic URL. Valid Meshtasic URLs start with \"https://meshtastic.org/e/...\"", "error": { "invalidUrl": "Invalid Meshtastic URL" }, "channelPrefix": "Channel ", "primary": "Primary ", - "doNotImport": "No not import", + "doNotImport": "Don't import", "channelName": "Name", "channelSlot": "Slot", "channelSetUrl": "Channel Set/QR Code URL", @@ -102,34 +108,6 @@ } } }, - "nodeDetails": { - "message": "Message", - "requestPosition": "Request Position", - "traceRoute": "Trace Route", - "airTxUtilization": "Air TX utilization", - "allRawMetrics": "All Raw Metrics:", - "batteryLevel": "Battery level", - "channelUtilization": "Channel utilization", - "details": "Details:", - "deviceMetrics": "Device Metrics:", - "hardware": "Hardware: ", - "lastHeard": "Last Heard: ", - "nodeHexPrefix": "Node Hex: ", - "nodeNumber": "Node Number: ", - "position": "Position:", - "role": "Role: ", - "uptime": "Uptime: ", - "voltage": "Voltage", - "title": "Node Details for {{identifier}}", - "ignoreNode": "Ignore node", - "removeNode": "Remove node", - "unignoreNode": "Unignore node", - "security": "Security:", - "publicKey": "Public Key: ", - "messageable": "Messageable: ", - "KeyManuallyVerifiedTrue": "Public Key has been manually verified", - "KeyManuallyVerifiedFalse": "Public Key is not manually verified" - }, "pkiBackup": { "loseKeysWarning": "If you lose your keys, you will need to reset your device.", "secureBackup": "Its important to backup your public and private keys and store your backup securely!", @@ -181,6 +159,18 @@ "description": "Are you sure you want to remove this Node?", "title": "Remove Node?" }, + "removeDevice": { + "title": "Remove Device?", + "description": "This will permanently remove all data associated with this device.", + "confirm": "Remove Device", + "nodeNum": "Node #{{nodeNum}}", + "warningTitle": "This action will permanently delete:", + "warningMessages": "All messages", + "warningNodes": "Node database", + "warningChannels": "Channel settings", + "warningPositions": "Position history", + "warningSettings": "All device settings and configuration" + }, "shutdown": { "title": "Schedule Shutdown", "description": "Turn off the connected node after x minutes." @@ -217,12 +207,6 @@ "confirm": "Reset Node Database", "failedTitle": "There was an error resetting the Node DB. Please try again." }, - "clearAllStores": { - "title": "Clear All Local Storage", - "description": "This will clear all locally stored data, including message history and node databases for all previously connected devices. This will require you to reconnect to your node once complete and cannot be undone. Are you sure you want to continue?", - "confirm": "Clear all local storage", - "failedTitle": "There was an error clearing local storage. Please try again." - }, "factoryResetDevice": { "title": "Factory Reset Device", "description": "This will factory reset the connected device, erasing all configurations and data on the device as well as all nodes and messages saved in the client. This cannot be undone. Are you sure you want to continue?", @@ -234,5 +218,28 @@ "description": "This will factory reset the configuration on the connected device, erasing all configurations on the device. This cannot be undone. Are you sure you want to continue?", "confirm": "Factory Reset Config", "failedTitle": "There was an error performing the factory reset. Please try again." + }, + "deviceReboot": { + "title": "Device Rebooting", + "rebooting": "The device is rebooting to apply configuration changes. This may take a few seconds.", + "disconnected": "The device has disconnected. Click Reconnect when the device has finished rebooting.", + "reconnecting": "Reconnecting to device...", + "reconnected": "Successfully reconnected to the device.", + "reconnectFailed": "Unable to reconnect. The device may still be rebooting. Try again in a few seconds.", + "reconnect": "Reconnect", + "reconnectingButton": "Reconnecting...", + "cancel": "Cancel", + "close": "Close" + }, + "deviceDisconnect": { + "title": "Device Disconnected", + "description": "The connection to your device has been lost.", + "reconnecting": "Reconnecting to device...", + "reconnectFailed": "Unable to reconnect. Please check your device and try again.", + "reconnect": "Reconnect", + "reconnectingButton": "Reconnecting...", + "cancel": "Cancel", + "goToConnections": "Go to Connections", + "close": "Close" } } diff --git a/packages/web/public/i18n/locales/en/map.json b/packages/web/public/i18n/locales/en/map.json index c17f38b25..ccb67ef98 100644 --- a/packages/web/public/i18n/locales/en/map.json +++ b/packages/web/public/i18n/locales/en/map.json @@ -12,6 +12,7 @@ "directNeighbors": "Show direct connections", "remoteNeighbors": "Show remote connections", "positionPrecision": "Show position precision", + "positionTrails": "Show position trails", "traceroutes": "Show traceroutes", "waypoints": "Show waypoints" }, diff --git a/packages/web/public/i18n/locales/en/messages.json b/packages/web/public/i18n/locales/en/messages.json index 07d60e570..b0b4cd344 100644 --- a/packages/web/public/i18n/locales/en/messages.json +++ b/packages/web/public/i18n/locales/en/messages.json @@ -10,31 +10,29 @@ "selectChatPrompt": { "text": "Select a channel or node to start messaging." }, - "sendMessage": { + "input": { "placeholder": "Enter your message here...", - "sendButton": "Send" + "tooltip": "Send message", + "cancelReply": "Cancel reply", + "replyingTo": "Replying to", + "notConnected": "Not connected - go to the Connections page to reconnect", + "emojiButton": "Send emoji" }, + "emojiPicker": { "search": "Search emoji" }, "actionsMenu": { - "addReactionLabel": "Add Reaction", - "replyLabel": "Reply" + "addReaction": "Add Reaction", + "reply": "Reply" }, - "deliveryStatus": { - "delivered": { - "label": "Message delivered", - "displayText": "Message delivered" + "delivered": "Delivered", + "ack": { + "delivered": "Delivered", + "acknowledged": "Acknowledged" }, - "failed": { - "label": "Message delivery failed", - "displayText": "Delivery failed" - }, - "unknown": { - "label": "Message status unknown", - "displayText": "Unknown state" - }, - "waiting": { - "label": "Sending message", - "displayText": "Waiting for delivery" - } + "waiting": "Waiting", + "sending": "Sending", + "sent": "Sent", + "failed": "Failed {{error}}", + "unknown": "Unknown" } } diff --git a/packages/web/public/i18n/locales/en/moduleConfig.json b/packages/web/public/i18n/locales/en/moduleConfig.json index f35995984..f13f28fe2 100644 --- a/packages/web/public/i18n/locales/en/moduleConfig.json +++ b/packages/web/public/i18n/locales/en/moduleConfig.json @@ -1,17 +1,17 @@ { "page": { - "tabAmbientLighting": "Ambient Lighting", - "tabAudio": "Audio", - "tabCannedMessage": "Canned", - "tabDetectionSensor": "Detection Sensor", - "tabExternalNotification": "Ext Notif", - "tabMqtt": "MQTT", - "tabNeighborInfo": "Neighbor Info", - "tabPaxcounter": "Paxcounter", - "tabRangeTest": "Range Test", - "tabSerial": "Serial", - "tabStoreAndForward": "S&F", - "tabTelemetry": "Telemetry" + "ambientLighting": "Ambient Lighting", + "audio": "Audio", + "cannedMessage": "Canned", + "detectionSensor": "Detection Sensor", + "externalNotification": "Ext Notif", + "mqtt": "MQTT", + "neighborInfo": "Neighbor Info", + "paxcounter": "Paxcounter", + "rangeTest": "Range Test", + "serial": "Serial", + "storeAndForward": "S&F", + "telemetry": "Telemetry" }, "ambientLighting": { "title": "Ambient Lighting Settings", diff --git a/packages/web/public/i18n/locales/en/nodes.json b/packages/web/public/i18n/locales/en/nodes.json index 16db3f953..19cfff8f7 100644 --- a/packages/web/public/i18n/locales/en/nodes.json +++ b/packages/web/public/i18n/locales/en/nodes.json @@ -1,4 +1,13 @@ { + "nodesPage": { + "title": "Nodes", + "description": "View and manage all nodes in your mesh network", + "tableTitle": "Network Nodes", + "nodeCount": "{{count}} nodes", + "nodeCount_one": "{{count}} node", + "nodeCount_other": "{{count}} nodes", + "noNodes": "No nodes found" + }, "nodeDetail": { "publicKeyEnabled": { "label": "Public Key Enabled" @@ -20,6 +29,9 @@ "label": "Error", "text": "An error occurred while fetching node details. Please try again later." }, + "online": { + "label": "Online" + }, "status": { "heard": "Heard", "mqtt": "MQTT" @@ -35,13 +47,21 @@ } }, "nodesTable": { + "columns": "Columns", + "toggleColumns": "Toggle Columns", "headings": { - "longName": "Long Name", - "connection": "Connection", + "longName": "Name", + "encryption": "Enc", "lastHeard": "Last Heard", - "encryption": "Encryption", + "signal": "Signal", + "battery": "Battery", + "altitude": "Altitude", + "hopsAway": "Hops", + "temp": "Temp", + "chUtil": "Ch/Air Util", "model": "Model", - "macAddress": "MAC Address" + "role": "Role", + "nodeId": "Node ID" }, "connectionStatus": { "direct": "Direct", diff --git a/packages/web/public/i18n/locales/en/ui.json b/packages/web/public/i18n/locales/en/ui.json index 721050dde..89e06c068 100644 --- a/packages/web/public/i18n/locales/en/ui.json +++ b/packages/web/public/i18n/locales/en/ui.json @@ -1,36 +1,29 @@ { "navigation": { "title": "Navigation", + "dashboard": "Dashboard", "messages": "Messages", "map": "Map", + "nodes": "Nodes", "settings": "Settings", - "channels": "Channels", - "radioConfig": "Radio Config", - "deviceConfig": "Device Config", - "moduleConfig": "Module Config", - "manageConnections": "Manage Connections", - "nodes": "Nodes" + "preferences": "Preferences", + "configuration": "Configuration", + "networkStatus": "Network Status", + "connections": "Connections" }, "app": { "title": "Meshtastic", + "subtitle": "Web Client", "logo": "Meshtastic Logo" }, "sidebar": { - "collapseToggle": { - "button": { - "open": "Open sidebar", - "close": "Close sidebar" - } - }, - "deviceInfo": { - "volts": "{{voltage}} volts", - "firmware": { - "title": "Firmware", - "version": "v{{version}}", - "buildDate": "Build date: {{date}}" - } - } + "collapse": "Collapse" + }, + "nodes": { + "onlineTotal": "{{ online }} of {{ total }}", + "text": "nodes online" }, + "remoteAdmin": {}, "batteryStatus": { "charging": "{{level}}% charging", "pluggedIn": "Plugged in", @@ -106,6 +99,10 @@ "unknown": "Unknown" } }, + + "clearInput": { + "label": "Clear input" + }, "general": { "label": "General" }, @@ -124,9 +121,6 @@ "advanced": { "label": "Advanced" }, - "clearInput": { - "label": "Clear input" - }, "resetFilters": { "label": "Reset Filters" }, @@ -185,15 +179,9 @@ "label": "Language", "changeLanguage": "Change Language" }, - "theme": { - "dark": "Dark", - "light": "Light", - "system": "Automatic", - "changeTheme": "Change Color Scheme" - }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { @@ -212,5 +200,113 @@ "footer": { "text": "Powered by <0>▲ Vercel | Meshtastic® is a registered trademark of Meshtastic LLC. | <1>Legal Information", "commitSha": "Commit SHA: {{sha}}" + }, + "messages": { + "actionsMenu": { + "more": "More actions", + "copy": "Copy text", + "delete": "Delete" + }, + "MQTT": "MQTT" + }, + "preferences": { + "title": "Preferences", + "description": "Customize your application experience", + "resetToDefaults": "Reset to Defaults", + "appearance": { + "title": "Appearance", + "description": "Customize how Meshtastic looks", + "theme": "Theme", + "light": "Light", + "dark": "Dark", + "system": "System", + "compactMode": { + "label": "Compact Mode", + "description": "Reduce spacing and padding for more content" + }, + "showNodeAvatars": { + "label": "Show Node Avatars", + "description": "Display avatars in the node list" + } + }, + "localization": { + "title": "Localization", + "description": "Language and regional settings", + "language": "Language", + "dateFormat": "Date Format", + "dateFormat.none": "Time only", + "dateFormat.short": "Short date", + "dateFormat.long": "Long date", + "timeFormat": "Time Format", + "12hour": "12 Hour (AM/PM)", + "24hour": "24 Hour", + "distanceUnits": "Distance Units", + "imperial": "Imperial (mi, ft)", + "metric": "Metric (km, m)", + "coordinateFormat": "Coordinate Format", + "decimalDegrees": "Decimal Degrees", + "dms": "Degrees Minutes Seconds", + "utm": "UTM" + }, + "map": { + "title": "Map Preferences", + "description": "Configure map display settings", + "mapStyle": "Map Style", + "dark": "Dark", + "light": "Light", + "satellite": "Satellite", + "terrain": "Terrain", + "streets": "Streets", + "showNodeLabels": { + "label": "Show Node Labels", + "description": "Display names on map markers" + }, + "showConnectionLines": { + "label": "Show Connection Lines", + "description": "Draw lines between connected nodes" + }, + "autoCenterOnPosition": { + "label": "Auto-center on Position", + "description": "Keep map centered on your location" + } + }, + "audio": { + "title": "Audio", + "description": "Sound and audio settings. MP3 uploads are limited to 512 KB.", + "messageSound": { + "label": "Message Sound", + "description": "Play sound for incoming messages" + }, + "alertSound": { + "label": "Alert Sound", + "description": "Play sound for network alerts" + }, + "upload": "Upload", + "preview": "Preview", + "remove": "Remove", + "currentSound": "Current: {{name}}", + "noSound": "No sound uploaded", + "fileTooLarge": "File exceeds 512 KB limit", + "invalidFormat": "Only MP3 files are supported" + }, + "performance": { + "title": "Performance", + "description": "Database and performance settings", + "packetBatchSize": { + "label": "Packet Batch Size", + "description": "Number of packets to batch before writing to database" + } + }, + "devices": { + "title": "Devices", + "description": "Manage saved devices and their data", + "noDevices": "No devices found", + "lastSeen": "Last seen", + "remove": "Remove device", + "database": "Database", + "databaseDescription": "Download a backup or delete all stored data", + "downloadDatabase": "Download", + "deleteDatabase": "Delete" + } } } diff --git a/packages/web/public/i18n/locales/es-ES/ui.json b/packages/web/public/i18n/locales/es-ES/ui.json index bcf133989..1e150a65d 100644 --- a/packages/web/public/i18n/locales/es-ES/ui.json +++ b/packages/web/public/i18n/locales/es-ES/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/fi-FI/ui.json b/packages/web/public/i18n/locales/fi-FI/ui.json index 9c59a3d91..20ec222ca 100644 --- a/packages/web/public/i18n/locales/fi-FI/ui.json +++ b/packages/web/public/i18n/locales/fi-FI/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "Tämä on hieman noloa...", - "description1": "Pahoittelemme, mutta verkkosovelluksessa tapahtui virhe, joka aiheutti kaatumisen.
\nTällaista ei pitäisi tapahtua ja me työskentelemme ahkerasti ongelman korjaamiseksi.", + "description1": "Pahoittelemme, mutta verkkosovelluksessa tapahtui virhe, joka aiheutti kaatumisen. \nTällaista ei pitäisi tapahtua ja me työskentelemme ahkerasti ongelman korjaamiseksi.", "description2": "Paras tapa estää tämän tapahtuminen uudelleen sinulle tai kenellekään muulle, on ilmoittaa meille ongelmasta.", "reportInstructions": "Lisääthän raporttiisi seuraavat tiedot:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/fr-FR/ui.json b/packages/web/public/i18n/locales/fr-FR/ui.json index 4d6f16225..52c8a9de9 100644 --- a/packages/web/public/i18n/locales/fr-FR/ui.json +++ b/packages/web/public/i18n/locales/fr-FR/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "C'est un peu embarrassant...", - "description1": "Nous sommes vraiment désolés mais une erreur est survenue dans le client web qui l'a fait planter.
Ce n'est pas censé se produire, et nous travaillons dur pour le corriger.", + "description1": "Nous sommes vraiment désolés mais une erreur est survenue dans le client web qui l'a fait planter. Ce n'est pas censé se produire, et nous travaillons dur pour le corriger.", "description2": "La meilleure façon d'éviter que cela ne se reproduise à vous ou à qui que ce soit d'autre consiste à nous signaler le problème.", "reportInstructions": "Veuillez inclure les informations suivantes dans votre rapport :", "reportSteps": { diff --git a/packages/web/public/i18n/locales/hu-HU/ui.json b/packages/web/public/i18n/locales/hu-HU/ui.json index 8ea0a0ee3..4e10aa34b 100644 --- a/packages/web/public/i18n/locales/hu-HU/ui.json +++ b/packages/web/public/i18n/locales/hu-HU/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/it-IT/ui.json b/packages/web/public/i18n/locales/it-IT/ui.json index 2f8960ee5..363c277ab 100644 --- a/packages/web/public/i18n/locales/it-IT/ui.json +++ b/packages/web/public/i18n/locales/it-IT/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "Questo è un po' imbarazzante...", - "description1": "Siamo davvero spiacenti, ma si è verificato un errore nel client web che ha causato il crash.
Questo non dovrebbe accadere e stiamo lavorando duramente per risolverlo.", + "description1": "Siamo davvero spiacenti, ma si è verificato un errore nel client web che ha causato il crash. Questo non dovrebbe accadere e stiamo lavorando duramente per risolverlo.", "description2": "Il modo migliore per evitare che ciò accada di nuovo a voi o a chiunque altro è quello di riferire la questione a noi.", "reportInstructions": "Per favore includi le seguenti informazioni nel tuo report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/ja-JP/ui.json b/packages/web/public/i18n/locales/ja-JP/ui.json index 6afb22e05..d2642ac0b 100644 --- a/packages/web/public/i18n/locales/ja-JP/ui.json +++ b/packages/web/public/i18n/locales/ja-JP/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/ko-KR/ui.json b/packages/web/public/i18n/locales/ko-KR/ui.json index e1b193219..a5ae19d0c 100644 --- a/packages/web/public/i18n/locales/ko-KR/ui.json +++ b/packages/web/public/i18n/locales/ko-KR/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "이건 좀 부끄러운 일이에요...", - "description1": "정말 죄송합니다. 웹 클라이언트에서 오류가 발생하여 강제 종료되었습니다.
이 문제는 발생하지 않아야 하는 것이며, 현재 이를 해결하기 위해 최선을 다하고 있습니다.", + "description1": "정말 죄송합니다. 웹 클라이언트에서 오류가 발생하여 강제 종료되었습니다. 이 문제는 발생하지 않아야 하는 것이며, 현재 이를 해결하기 위해 최선을 다하고 있습니다.", "description2": "이 문제가 다시 발생하지 않도록 하는 가장 좋은 방법은 해당 문제를 저희에게 신고해 주시는 것입니다.", "reportInstructions": "보고서에 다음 정보를 포함해 주시기 바랍니다:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/nl-NL/ui.json b/packages/web/public/i18n/locales/nl-NL/ui.json index ca37de0a6..53ec7dd4f 100644 --- a/packages/web/public/i18n/locales/nl-NL/ui.json +++ b/packages/web/public/i18n/locales/nl-NL/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/pl-PL/ui.json b/packages/web/public/i18n/locales/pl-PL/ui.json index 206a8bb0b..4a5768b66 100644 --- a/packages/web/public/i18n/locales/pl-PL/ui.json +++ b/packages/web/public/i18n/locales/pl-PL/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/pt-BR/ui.json b/packages/web/public/i18n/locales/pt-BR/ui.json index ad8410678..607b592a5 100644 --- a/packages/web/public/i18n/locales/pt-BR/ui.json +++ b/packages/web/public/i18n/locales/pt-BR/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/pt-PT/ui.json b/packages/web/public/i18n/locales/pt-PT/ui.json index 6763befed..c705a5ea4 100644 --- a/packages/web/public/i18n/locales/pt-PT/ui.json +++ b/packages/web/public/i18n/locales/pt-PT/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/ru-RU/dialog.json b/packages/web/public/i18n/locales/ru-RU/dialog.json index 4ee529972..a08ff637b 100644 --- a/packages/web/public/i18n/locales/ru-RU/dialog.json +++ b/packages/web/public/i18n/locales/ru-RU/dialog.json @@ -4,7 +4,7 @@ "title": "Clear All Messages" }, "import": { - "description": "Import a Channel Set from a Meshtastic URL.
Valid Meshtasic URLs start with \"https://meshtastic.org/e/...\"", + "description": "Import a Channel Set from a Meshtastic URL. Valid Meshtasic URLs start with \"https://meshtastic.org/e/...\"", "error": { "invalidUrl": "Invalid Meshtastic URL" }, diff --git a/packages/web/public/i18n/locales/ru-RU/ui.json b/packages/web/public/i18n/locales/ru-RU/ui.json index 492d459ec..4185b52a0 100644 --- a/packages/web/public/i18n/locales/ru-RU/ui.json +++ b/packages/web/public/i18n/locales/ru-RU/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/sv-SE/ui.json b/packages/web/public/i18n/locales/sv-SE/ui.json index 44e25cb9c..4317f9e6d 100644 --- a/packages/web/public/i18n/locales/sv-SE/ui.json +++ b/packages/web/public/i18n/locales/sv-SE/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "Det här är lite pinsamt...", - "description1": "Vi är verkligen ledsna men ett fel inträffade i webbklienten som fick den att krascha.
Detta var inte meningen, men vi arbetar hårt för att åtgärda det.", + "description1": "Vi är verkligen ledsna men ett fel inträffade i webbklienten som fick den att krascha. Detta var inte meningen, men vi arbetar hårt för att åtgärda det.", "description2": "Det bästa sättet att förhindra att detta händer igen för dig eller någon annan är att rapportera problemet till oss.", "reportInstructions": "Vänligen inkludera följande information i din rapport:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/tr-TR/ui.json b/packages/web/public/i18n/locales/tr-TR/ui.json index 31c79f483..cbe5ef681 100644 --- a/packages/web/public/i18n/locales/tr-TR/ui.json +++ b/packages/web/public/i18n/locales/tr-TR/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/uk-UA/ui.json b/packages/web/public/i18n/locales/uk-UA/ui.json index 8ad2d7242..6fdcbb30f 100644 --- a/packages/web/public/i18n/locales/uk-UA/ui.json +++ b/packages/web/public/i18n/locales/uk-UA/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/zh-CN/ui.json b/packages/web/public/i18n/locales/zh-CN/ui.json index 7269d6f5d..832fb41e5 100644 --- a/packages/web/public/i18n/locales/zh-CN/ui.json +++ b/packages/web/public/i18n/locales/zh-CN/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "This is a little embarrassing...", - "description1": "We are really sorry but an error occurred in the web client that caused it to crash.
This is not supposed to happen, and we are working hard to fix it.", + "description1": "We are really sorry but an error occurred in the web client that caused it to crash. This is not supposed to happen, and we are working hard to fix it.", "description2": "The best way to prevent this from happening again to you or anyone else is to report the issue to us.", "reportInstructions": "Please include the following information in your report:", "reportSteps": { diff --git a/packages/web/public/i18n/locales/zh-TW/dialog.json b/packages/web/public/i18n/locales/zh-TW/dialog.json index f2ef748d7..2f12c13de 100644 --- a/packages/web/public/i18n/locales/zh-TW/dialog.json +++ b/packages/web/public/i18n/locales/zh-TW/dialog.json @@ -4,7 +4,7 @@ "title": "Clear All Messages" }, "import": { - "description": "Import a Channel Set from a Meshtastic URL.
Valid Meshtasic URLs start with \"https://meshtastic.org/e/...\"", + "description": "Import a Channel Set from a Meshtastic URL. Valid Meshtasic URLs start with \"https://meshtastic.org/e/...\"", "error": { "invalidUrl": "Invalid Meshtastic URL" }, diff --git a/packages/web/public/i18n/locales/zh-TW/ui.json b/packages/web/public/i18n/locales/zh-TW/ui.json index ac98cfdc9..a2c26f540 100644 --- a/packages/web/public/i18n/locales/zh-TW/ui.json +++ b/packages/web/public/i18n/locales/zh-TW/ui.json @@ -207,7 +207,7 @@ }, "errorPage": { "title": "這有點尷尬……", - "description1": "我們深感抱歉,網頁客戶端發生錯誤並導致程式崩潰。
這是不應該發生的情況,我們正積極努力修復此問題。", + "description1": "我們深感抱歉,網頁客戶端發生錯誤並導致程式崩潰。 這是不應該發生的情況,我們正積極努力修復此問題。", "description2": "幫助我們防止此問題再次發生,請將問題回報給我們。", "reportInstructions": "請在您的回報中附上下列資訊:", "reportSteps": { diff --git a/packages/web/public/icon.svg b/packages/web/public/logo.svg similarity index 100% rename from packages/web/public/icon.svg rename to packages/web/public/logo.svg diff --git a/packages/web/src/App.tsx b/packages/web/src/App.tsx deleted file mode 100644 index 5fc115c6f..000000000 --- a/packages/web/src/App.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; -import { CommandPalette } from "@components/CommandPalette/index.tsx"; -import { DialogManager } from "@components/Dialog/DialogManager.tsx"; -import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx"; -import { Toaster } from "@components/Toaster.tsx"; -import { ErrorPage } from "@components/UI/ErrorPage.tsx"; -import Footer from "@components/UI/Footer.tsx"; -import { useTheme } from "@core/hooks/useTheme.ts"; -import { SidebarProvider, useAppStore, useDeviceStore } from "@core/stores"; -import { Connections } from "@pages/Connections/index.tsx"; -import { Outlet } from "@tanstack/react-router"; -import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; -import { ErrorBoundary } from "react-error-boundary"; -import { MapProvider } from "react-map-gl/maplibre"; - -export function App() { - useTheme(); - - const { getDevice } = useDeviceStore(); - const { selectedDeviceId } = useAppStore(); - - const device = getDevice(selectedDeviceId); - - return ( - // - - {/* { - setConnectDialogOpen(open); - }} - /> */} - - - -
- -
- {device ? ( -
- - - - - - -
- ) : ( - <> - -
- - )} -
-
-
-
-
- //
- ); -} diff --git a/packages/web/src/DeviceWrapper.tsx b/packages/web/src/DeviceWrapper.tsx deleted file mode 100644 index d9ee3def3..000000000 --- a/packages/web/src/DeviceWrapper.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { CurrentDeviceContext } from "@core/stores"; -import type { ReactNode } from "react"; - -export interface DeviceWrapperProps { - children: ReactNode; - deviceId: number; -} - -export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => { - return ( - - {children} - - ); -}; diff --git a/packages/web/src/__mocks__/README.md b/packages/web/src/__mocks__/README.md index 6f69ec3aa..d3ac9f8c5 100644 --- a/packages/web/src/__mocks__/README.md +++ b/packages/web/src/__mocks__/README.md @@ -24,7 +24,7 @@ __mocks__/ Vitest will automatically use the mock files in this directory when the corresponding module is imported in tests. For example, when a test imports -`@components/UI/Dialog.tsx`, Vitest will use +`@shared/components/UI/Dialog.tsx`, Vitest will use `__mocks__/components/UI/Dialog.tsx` instead. ## Creating New Mocks diff --git a/packages/web/src/__mocks__/components/UI/Button.tsx b/packages/web/src/__mocks__/components/UI/Button.tsx index d8e4586fb..3e6b2bddf 100644 --- a/packages/web/src/__mocks__/components/UI/Button.tsx +++ b/packages/web/src/__mocks__/components/UI/Button.tsx @@ -1,6 +1,6 @@ import { vi } from "vitest"; -vi.mock("@components/UI/Button.tsx", () => ({ +vi.mock("@shared/components/ui/button", () => ({ Button: ({ children, name, diff --git a/packages/web/src/__mocks__/components/UI/Checkbox.tsx b/packages/web/src/__mocks__/components/UI/Checkbox.tsx index a194124dd..1fbe53986 100644 --- a/packages/web/src/__mocks__/components/UI/Checkbox.tsx +++ b/packages/web/src/__mocks__/components/UI/Checkbox.tsx @@ -1,6 +1,6 @@ import { vi } from "vitest"; -vi.mock("@components/UI/Checkbox.tsx", () => ({ +vi.mock("@shared/components/ui/checkbox", () => ({ Checkbox: ({ id, checked, diff --git a/packages/web/src/__mocks__/components/UI/Label.tsx b/packages/web/src/__mocks__/components/UI/Label.tsx index 504321dcc..8d7235688 100644 --- a/packages/web/src/__mocks__/components/UI/Label.tsx +++ b/packages/web/src/__mocks__/components/UI/Label.tsx @@ -1,6 +1,6 @@ import { vi } from "vitest"; -vi.mock("@components/UI/Label.tsx", () => ({ +vi.mock("@shared/components/ui/label", () => ({ Label: ({ children, htmlFor, diff --git a/packages/web/src/__mocks__/components/UI/Link.tsx b/packages/web/src/__mocks__/components/UI/Link.tsx index 01f7cf501..acaf96be5 100644 --- a/packages/web/src/__mocks__/components/UI/Link.tsx +++ b/packages/web/src/__mocks__/components/UI/Link.tsx @@ -1,6 +1,6 @@ import { vi } from "vitest"; -vi.mock("@components/UI/Typography/Link.tsx", () => ({ +vi.mock("@shared/components/ui/link", () => ({ Link: ({ children, href, diff --git a/packages/web/src/app/App.tsx b/packages/web/src/app/App.tsx new file mode 100644 index 000000000..35757945b --- /dev/null +++ b/packages/web/src/app/App.tsx @@ -0,0 +1,15 @@ +import { ThemeProvider } from "@app/shared/components/ui/theme-provider.tsx"; +import { Toaster } from "@shared/components/Toaster"; +import { SidebarProvider } from "@shared/components/ui/sidebar"; +import { Outlet } from "@tanstack/react-router"; + +export function App() { + return ( + + + + + + + ); +} diff --git a/packages/web/src/app/index.ts b/packages/web/src/app/index.ts new file mode 100644 index 000000000..a607247dc --- /dev/null +++ b/packages/web/src/app/index.ts @@ -0,0 +1,3 @@ +export { App } from "./App.tsx"; +export * from "./layouts/index.ts"; +export { router } from "./router.ts"; diff --git a/packages/web/src/app/layouts/AppLayout.tsx b/packages/web/src/app/layouts/AppLayout.tsx new file mode 100644 index 000000000..1838399e3 --- /dev/null +++ b/packages/web/src/app/layouts/AppLayout.tsx @@ -0,0 +1,48 @@ +import { DialogManager } from "@app/shared/components/Dialog/DialogManager.tsx"; +import { Separator } from "@shared/components/ui/separator"; +import { SidebarInset, SidebarTrigger } from "@shared/components/ui/sidebar"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@shared/components/ui/tooltip"; +import { cn } from "@shared/utils/cn"; +import { useDevice } from "@state/index.ts"; +import type React from "react"; +import { useTranslation } from "react-i18next"; +import { AppSidebar } from "./AppSidebar.tsx"; + +export function AppLayout({ children }: { children: React.ReactNode }) { + const { t } = useTranslation("ui"); + const device = useDevice(); + const isRemoteAdmin = device.remoteAdminTargetNode !== null; + + return ( +
+ + + +
+ + + + + + +

{t("sidebar.collapse")}

+
+
+
+ +
+
{children}
+
+
+ ); +} diff --git a/packages/web/src/app/layouts/AppSidebar.tsx b/packages/web/src/app/layouts/AppSidebar.tsx new file mode 100644 index 000000000..05273999f --- /dev/null +++ b/packages/web/src/app/layouts/AppSidebar.tsx @@ -0,0 +1,477 @@ +import { + useConnect, + useConversations, + useNodes, + useOnlineCount, +} from "@data/hooks"; +import { ConnectionService } from "@features/connect/services"; +import { + getEffectiveStatus, + getStatusColor, + getStatusTranslationKey, +} from "@features/connect/utils/connectionStatus"; +import { NodeAvatar } from "@shared/components/NodeAvatar"; +import { Badge } from "@shared/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from "@shared/components/ui/dropdown-menu"; +import { Link } from "@shared/components/ui/link"; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from "@shared/components/ui/sidebar"; +import { Skeleton } from "@shared/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@shared/components/ui/tooltip"; +import { useMyNode } from "@shared/hooks"; +import { useDevice } from "@state/index.ts"; +import { useLocation, useNavigate, useParams } from "@tanstack/react-router"; +import { + CableIcon, + CheckIcon, + ChevronUp, + LayoutDashboard, + LogOutIcon, + MapIcon, + MessageSquare, + Settings, + Users, +} from "lucide-react"; +import { Suspense, useMemo } from "react"; +import { useTranslation } from "react-i18next"; + +const getMainNavItems = (nodeNum: number, unreadCount: number) => { + const basePath = `/${nodeNum}`; + return [ + { + title: "Dashboard", + url: `${basePath}/dashboard`, + icon: LayoutDashboard, + }, + { + title: "Messages", + url: `${basePath}/messages`, + icon: MessageSquare, + badge: unreadCount > 0 ? unreadCount : undefined, + }, + { + title: "Map", + url: `${basePath}/map`, + icon: MapIcon, + }, + { + title: "Nodes", + url: `${basePath}/nodes`, + icon: Users, + }, + ]; +}; + +const getConfigNavItems = (nodeNum: number) => { + const basePath = `/${nodeNum}`; + return [ + { + title: "Settings", + url: `${basePath}/settings`, + icon: Settings, + }, + ]; +}; + +function SidebarSkeleton() { + return ( + <> + + + + + + +
+ + + + +
+
+
+
+ + + + + ); +} + +function ConnectedSidebarContent() { + const pathname = useLocation().pathname; + const navigate = useNavigate(); + + const device = useDevice(); + const { t } = useTranslation(); + const { myNodeNum, myNode } = useMyNode(); + + const { nodes: allNodes, nodeMap } = useNodes(myNodeNum); + const { count: onlineCount } = useOnlineCount(myNodeNum); + const { connections, autoReconnectStatus } = useConnect(); + + // Find connection for current device by nodeNum + const activeConnection = connections.find((c) => c.nodeNum === myNodeNum); + + const remoteAdminTarget = device.remoteAdminTargetNode; + const isRemoteAdmin = remoteAdminTarget !== null; + const isAuthorized = device.remoteAdminAuthorized; + const remoteNode = remoteAdminTarget ? nodeMap.get(remoteAdminTarget) : null; + + const effectiveStatus = getEffectiveStatus( + activeConnection?.status, + autoReconnectStatus, + ); + + const statusColor = + isRemoteAdmin && !isAuthorized + ? "bg-red-500" + : getStatusColor(effectiveStatus); + + const statusText = (() => { + if (isRemoteAdmin && !isAuthorized) { + return t("status.notAuthorized", { ns: "connections" }); + } + if (activeConnection?.error && effectiveStatus === "error") { + return activeConnection.error; + } + return t(getStatusTranslationKey(effectiveStatus), { ns: "connections" }); + })(); + + const { conversations } = useConversations(myNodeNum); + + const displayNode = isRemoteAdmin ? remoteNode : myNode; + const displayNodeNum = isRemoteAdmin ? remoteAdminTarget : myNodeNum; + const displayName = isRemoteAdmin + ? `[Remote] ${ + remoteNode?.longName ?? remoteNode?.shortName ?? t("unknown.longName") + }` + : (displayNode?.longName ?? + displayNode?.shortName ?? + t("unknown.longName")); + + const totalUnread = useMemo(() => { + return conversations.reduce((acc, conv) => acc + conv.unreadCount, 0); + }, [conversations]); + + const mainNavItems = useMemo( + () => getMainNavItems(myNodeNum, totalUnread), + [myNodeNum, totalUnread], + ); + + const configNavItems = useMemo( + () => getConfigNavItems(myNodeNum), + [myNodeNum], + ); + + const nodeStats = useMemo( + () => ({ + total: allNodes.length, + online: onlineCount, + }), + [allNodes.length, onlineCount], + ); + + return ( + <> + + + {t("navigation.title")} + + + {mainNavItems.map((item) => ( + + + + + + {item.title} + + {item.badge && ( + + {item.badge} + + )} + + + + ))} + + + + + + {t("navigation.configuration")} + + + {configNavItems.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + {t("navigation.networkStatus")} + +
+
+ +
+
+ {t("nodes.onlineTotal", { + online: nodeStats.online, + total: nodeStats.total, + })} +
+
{t("nodes.text")}
+
+
+
+
+
+
+ + + + {displayNode && ( + <> + + + + + + + {import.meta.env.VITE_VERSION} ( + {import.meta.env.VITE_COMMIT_HASH}) + + + + + + Recently Connected Nodes + + + {/* Local node - always first */} + {myNode && ( + device.setRemoteAdminTarget(null)} + > + {!isRemoteAdmin && ( + + )} + + {myNode.longName ?? myNode.shortName} + + + Local + + + )} + {/* Remote nodes from history */} + {device.recentlyConnectedNodes + .filter((nodeNum) => nodeNum !== myNodeNum) + .map((nodeNum) => { + const node = nodeMap.get(nodeNum); + const isSelected = remoteAdminTarget === nodeNum; + return ( + + device.setRemoteAdminTarget( + nodeNum, + node?.publicKey ?? undefined, + ) + } + > + {isSelected && ( + + )} + + {node?.longName ?? + node?.shortName ?? + `!${nodeNum.toString(16)}`} + + + ); + })} + + + navigate({ to: "/connect" })} + > + + Connections + + + { + if (activeConnection) { + ConnectionService.disconnect(activeConnection); + } + }} + className="text-destructive focus:text-destructive" + > + + Disconnect + + + + {isRemoteAdmin && ( +
+
+
+ Authorized - can configure +
+
+
+ Not authorized - read only +
+
+ )} + + )} + + + + ); +} + +function DisconnectedSidebarContent() { + const { t } = useTranslation(); + + return ( + <> + + + {t("navigation.title")} + +
+ Connect to a device to access navigation +
+
+
+
+ +
+ No device connected +
+
+ + ); +} + +export function AppSidebar() { + const { t } = useTranslation(); + const params = useParams({ strict: false }); + const isConnectedRoute = "nodeNum" in params; + + return ( + + + +
+ Meshtastic Logo +
+
+ + {t("app.title")} + + + {t("app.subtitle")} + +
+ +
+ + {isConnectedRoute ? ( + }> + + + ) : ( + + )} +
+ ); +} diff --git a/packages/web/src/app/layouts/index.ts b/packages/web/src/app/layouts/index.ts new file mode 100644 index 000000000..258916eed --- /dev/null +++ b/packages/web/src/app/layouts/index.ts @@ -0,0 +1,2 @@ +export { AppLayout } from "./AppLayout.tsx"; +export { AppSidebar } from "./AppSidebar.tsx"; diff --git a/packages/web/src/app/router.ts b/packages/web/src/app/router.ts new file mode 100644 index 000000000..67acac2a3 --- /dev/null +++ b/packages/web/src/app/router.ts @@ -0,0 +1,7 @@ +import { createRouter } from "@tanstack/react-router"; +import { routeTree } from "./routes.tsx"; + +export const router = createRouter({ + routeTree, + context: undefined!, +}); diff --git a/packages/web/src/app/routerContext.ts b/packages/web/src/app/routerContext.ts new file mode 100644 index 000000000..0cf8272e3 --- /dev/null +++ b/packages/web/src/app/routerContext.ts @@ -0,0 +1,37 @@ +import type { dbClient } from "@data/client"; +import type { + ChannelRepository, + ConnectionRepository, + DeviceRepository, + MessageRepository, + NodeRepository, + PacketLogRepository, + PendingChangesRepository, + PreferencesRepository, + TracerouteRepository, +} from "@data/repositories"; +import type { useDeviceStore } from "@state/device"; +import type { useUIStore } from "@state/ui"; +import type { Logger } from "tslog"; + +export interface RouterContext { + services: { + db: typeof dbClient; + logger: Logger; + }; + repositories: { + channel: ChannelRepository; + pendingChanges: PendingChangesRepository; + connection: ConnectionRepository; + device: DeviceRepository; + message: MessageRepository; + node: NodeRepository; + packetLog: PacketLogRepository; + preferences: PreferencesRepository; + traceroute: TracerouteRepository; + }; + stores: { + device: typeof useDeviceStore; + ui: typeof useUIStore; + }; +} diff --git a/packages/web/src/app/routes.tsx b/packages/web/src/app/routes.tsx new file mode 100644 index 000000000..abc4229b5 --- /dev/null +++ b/packages/web/src/app/routes.tsx @@ -0,0 +1,322 @@ +import { SidebarInset } from "@app/shared/components/ui/sidebar.tsx"; +import { + ConnectPage, + useConnect, + useDeviceStatusEvents, +} from "@features/connect"; +import { MessagesPage } from "@features/messages"; +import { ErrorPage } from "@shared/components/ui/error-page"; +import { Spinner } from "@shared/components/ui/spinner"; +import { + Outlet, + createRootRouteWithContext, + createRoute, + redirect, + useLocation, + useNavigate, +} from "@tanstack/react-router"; +import { Activity, Suspense, lazy, useCallback, useEffect } from "react"; +import { z } from "zod/v4"; +import { App } from "./App.tsx"; +import { AppLayout } from "./layouts/AppLayout.tsx"; +import { AppSidebar } from "./layouts/AppSidebar.tsx"; +import type { RouterContext } from "./routerContext.ts"; + +const DashboardPage = lazy(() => + import("@features/dashboard/pages/DashboardPage").then((m) => ({ + default: m.DashboardPage, + })), +); +const MapPage = lazy(() => + import("@features/map/pages/MapPage").then((m) => ({ + default: m.MapPage, + })), +); +const NodesPage = lazy(() => + import("@features/nodes/pages/NodesPage").then((m) => ({ + default: m.default, + })), +); +const SettingsPage = lazy(() => + import("@features/settings/pages/SettingsPage").then((m) => ({ + default: m.default, + })), +); + +function RouteLoader() { + return ( +
+ +
+ ); +} + +/** + * Handles auto-reconnect, navigation intent subscriptions, and device status events + */ +function ConnectedLayout() { + const navigate = useNavigate(); + const location = useLocation(); + + // Subscribe to device status events (disconnect detection) + useDeviceStatusEvents(); + + const handleNavigationIntent = useCallback( + (intent: { nodeNum: number }) => { + const prefix = `/${intent.nodeNum}`; + const currentPath = location.pathname; + + // If already on a valid connected route for this device, don't redirect + const validSegments = [ + "dashboard", + "messages", + "map", + "settings", + "nodes", + ]; + const isOnValidRoute = validSegments.some((seg) => + currentPath.startsWith(`${prefix}/${seg}`), + ); + + if (isOnValidRoute) { + return; + } + + navigate({ + to: "/$nodeNum/messages", + params: { nodeNum: String(intent.nodeNum) }, + }); + }, + [navigate, location.pathname], + ); + + const { autoReconnectStatus } = useConnect({ + autoReconnect: true, + onNavigationIntent: handleNavigationIntent, + }); + + // Redirect to connect page if auto-reconnect fails + // (e.g., Web Serial permissions have timed out) + useEffect(() => { + if (autoReconnectStatus === "failed") { + navigate({ to: "/connect", replace: true }); + } + }, [autoReconnectStatus, navigate]); + + // Show loading state while attempting to reconnect + // This prevents the jarring flash of content before redirect + if (autoReconnectStatus === "connecting") { + return ( + +
+
+ +

Reconnecting...

+
+
+
+ ); + } + + return ( + + + + ); +} + +export const rootRoute = createRootRouteWithContext()({ + component: () => , + errorComponent: ErrorPage, + validateSearch: z.object({ + // Traceroute dialog: ?traceroute=123456789 (target node number) + traceroute: z.coerce.number().int().min(0).max(4294967294).optional(), + }), +}); + +const indexRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/", + // component: ConnectPage, + beforeLoad: async ({ context }) => { + const lastConnection = + await context.repositories.connection.getLastConnectedConnection(); + + if (lastConnection?.nodeNum) { + // Verify the device still exists in the database + const deviceExists = await context.repositories.device.deviceExists( + lastConnection.nodeNum, + ); + + if (deviceExists) { + // Device exists - redirect to messages + throw redirect({ + to: "/$nodeNum/messages", + params: { nodeNum: String(lastConnection.nodeNum) }, + replace: true, + }); + } + } + + // No valid previous connection - go to connect page + throw redirect({ to: "/connect", replace: true }); + }, +}); + +/** + * Layout for the connect page - shows sidebar in disconnected state + */ +function ConnectLayout() { + return ( + <> + + + + + + ); +} + +const connectRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/connect", + component: ConnectLayout, + errorComponent: ErrorPage, +}); + +// Connected layout route - validates nodeNum and provides it to children +const connectedLayoutRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/$nodeNum", + component: ConnectedLayout, + errorComponent: ErrorPage, + beforeLoad: async ({ params, context }) => { + const nodeNum = Number(params.nodeNum); + + // Validate nodeNum is a valid number + if (Number.isNaN(nodeNum) || nodeNum <= 0) { + throw redirect({ to: "/connect", replace: true }); + } + + const deviceExists = + await context.repositories.device.deviceExists(nodeNum); + + if (!deviceExists) { + throw redirect({ to: "/connect", replace: true }); + } + + return { nodeNum }; + }, +}); + +const dashboardRoute = createRoute({ + getParentRoute: () => connectedLayoutRoute, + path: "/dashboard", + errorComponent: ErrorPage, + component: () => ( + }> + + + ), +}); + +const messagesRoute = createRoute({ + getParentRoute: () => connectedLayoutRoute, + path: "/messages", + component: () => ( + }> + + + ), + errorComponent: ErrorPage, +}); + +const mapRoute = createRoute({ + getParentRoute: () => connectedLayoutRoute, + path: "/map", + errorComponent: ErrorPage, + component: () => ( + }> + + + + + ), +}); + +const coordParamsSchema = z.object({ + long: z.coerce + .number() + .refine( + (n) => Number.isFinite(n) && n >= -180 && n <= 180, + "Invalid longitude (-180..180).", + ), + lat: z.coerce + .number() + .refine( + (n) => Number.isFinite(n) && n >= -90 && n <= 90, + "Invalid latitude (-90..90).", + ), + // map zoom levels ~0..22 + zoom: z.coerce + .number() + .int() + .min(0, "Zoom too small.") + .max(22, "Zoom too large."), +}); + +export const mapWithParamsRoute = createRoute({ + getParentRoute: () => connectedLayoutRoute, + path: "/map/$long/$lat/$zoom", + errorComponent: ErrorPage, + component: () => ( + }> + + + + + ), + parseParams: (raw) => coordParamsSchema.parse(raw), + stringifyParams: (p) => ({ + long: String(p.long), + lat: String(p.lat), + zoom: String(p.zoom), + }), +}); + +export const settingsRoute = createRoute({ + getParentRoute: () => connectedLayoutRoute, + path: "/settings", + component: () => ( + }> + + + ), + errorComponent: ErrorPage, +}); + +const nodesRoute = createRoute({ + getParentRoute: () => connectedLayoutRoute, + path: "/nodes", + errorComponent: ErrorPage, + component: () => ( + + }> + + + + ), +}); + +export const routeTree = rootRoute.addChildren([ + indexRoute, + connectRoute, + connectedLayoutRoute.addChildren([ + dashboardRoute, + messagesRoute, + mapRoute, + mapWithParamsRoute, + settingsRoute, + nodesRoute, + ]), +]); diff --git a/packages/web/src/components/Badge/ConnectionStatusBadge.tsx b/packages/web/src/components/Badge/ConnectionStatusBadge.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/web/src/components/CommandPalette/index.tsx b/packages/web/src/components/CommandPalette/index.tsx deleted file mode 100644 index afda6b5c3..000000000 --- a/packages/web/src/components/CommandPalette/index.tsx +++ /dev/null @@ -1,356 +0,0 @@ -import { Avatar } from "@components/UI/Avatar.tsx"; -import { - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@components/UI/Command.tsx"; -import { usePinnedItems } from "@core/hooks/usePinnedItems.ts"; -import { - useAppStore, - useDevice, - useDeviceStore, - useNodeDB, -} from "@core/stores"; -import { cn } from "@core/utils/cn.ts"; -import { useNavigate } from "@tanstack/react-router"; -import { useCommandState } from "cmdk"; -import { - ArrowLeftRightIcon, - BoxSelectIcon, - BugIcon, - CloudOff, - EraserIcon, - FactoryIcon, - HardDriveUpload, - LinkIcon, - type LucideIcon, - MapIcon, - MessageSquareIcon, - Pin, - PlusIcon, - PowerIcon, - QrCodeIcon, - RefreshCwIcon, - SettingsIcon, - SmartphoneIcon, - TrashIcon, - UsersIcon, -} from "lucide-react"; -import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; - -export interface Group { - id: string; - label: string; - icon: LucideIcon; - commands: Command[]; -} -export interface Command { - label: string; - icon: LucideIcon; - action?: () => void; - subItems?: SubItem[]; - tags?: string[]; -} -export interface SubItem { - label: string; - icon: React.ReactNode; - action: () => void; -} - -export const CommandPalette = () => { - const { - commandPaletteOpen, - setCommandPaletteOpen, - setConnectDialogOpen, - setSelectedDevice, - } = useAppStore(); - const { getDevices } = useDeviceStore(); - const { setDialogOpen, connection } = useDevice(); - const { getNode } = useNodeDB(); - const { pinnedItems, togglePinnedItem } = usePinnedItems({ - storageName: "pinnedCommandMenuGroups", - }); - const { t } = useTranslation("commandPalette"); - const navigate = useNavigate({ from: "/" }); - - const groups: Group[] = [ - { - id: "gotoGroup", - label: t("goto.label"), - icon: LinkIcon, - commands: [ - { - label: t("goto.command.messages"), - icon: MessageSquareIcon, - action() { - navigate({ to: "/messages" }); - }, - }, - { - label: t("goto.command.map"), - icon: MapIcon, - action() { - navigate({ to: "/map" }); - }, - }, - { - label: t("goto.command.config"), - icon: SettingsIcon, - action() { - navigate({ to: "/config" }); - }, - tags: ["settings"], - }, - { - label: t("goto.command.nodes"), - icon: UsersIcon, - action() { - navigate({ to: "/nodes" }); - }, - }, - ], - }, - { - id: "manageGroup", - label: t("manage.label"), - icon: SmartphoneIcon, - commands: [ - { - label: t("manage.command.switchNode"), - icon: ArrowLeftRightIcon, - subItems: getDevices().map((device) => ({ - label: - getNode(device.hardware.myNodeNum)?.user?.longName ?? - t("unknown.shortName"), - icon: , - action() { - setSelectedDevice(device.id); - }, - })), - }, - { - label: t("manage.command.connectNewNode"), - icon: PlusIcon, - action() { - setConnectDialogOpen(true); - }, - }, - ], - }, - { - id: "contextualGroup", - label: t("contextual.label"), - icon: BoxSelectIcon, - commands: [ - { - label: t("contextual.command.qrCode"), - icon: QrCodeIcon, - subItems: [ - { - label: t("contextual.command.qrGenerator"), - icon: , - action() { - setDialogOpen("QR", true); - }, - }, - { - label: t("contextual.command.qrImport"), - icon: , - action() { - setDialogOpen("import", true); - }, - }, - ], - }, - { - label: t("contextual.command.scheduleShutdown"), - icon: PowerIcon, - action() { - setDialogOpen("shutdown", true); - }, - }, - { - label: t("contextual.command.scheduleReboot"), - icon: RefreshCwIcon, - action() { - setDialogOpen("reboot", true); - }, - }, - { - label: t("contextual.command.dfuMode"), - icon: HardDriveUpload, - action() { - connection?.enterDfuMode(); - }, - }, - { - label: t("contextual.command.resetNodeDb"), - icon: TrashIcon, - action() { - setDialogOpen("resetNodeDb", true); - }, - }, - { - label: t("contextual.command.disconnect"), - icon: CloudOff, - action() { - connection?.disconnect().catch((error) => { - console.error("Failed to disconnect:", error); - }); - }, - }, - { - label: t("contextual.command.factoryResetDevice"), - icon: FactoryIcon, - action() { - setDialogOpen("factoryResetDevice", true); - }, - }, - { - label: t("contextual.command.factoryResetConfig"), - icon: FactoryIcon, - action() { - setDialogOpen("factoryResetConfig", true); - }, - }, - ], - }, - { - id: "debugGroup", - label: t("debug.label"), - icon: BugIcon, - commands: [ - { - label: t("debug.command.reconfigure"), - icon: RefreshCwIcon, - action() { - void connection?.configure(); - }, - }, - { - label: t("debug.command.clearAllStoredMessages"), - icon: EraserIcon, - action() { - setDialogOpen("deleteMessages", true); - }, - }, - { - label: t("debug.command.clearAllStores"), - icon: EraserIcon, - action() { - setDialogOpen("clearAllStores", true); - }, - }, - ], - }, - ]; - - const sortedGroups = [...groups].sort((a, b) => { - const aPinned = pinnedItems.includes(a.id) ? 1 : 0; - const bPinned = pinnedItems.includes(b.id) ? 1 : 0; - return bPinned - aPinned; - }); - - useEffect(() => { - const handleKeydown = (e: KeyboardEvent) => { - if (e.key === "k" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - setCommandPaletteOpen(true); - } - }; - - globalThis.addEventListener("keydown", handleKeydown); - return () => globalThis.removeEventListener("keydown", handleKeydown); - }, [setCommandPaletteOpen]); - - return ( - - - - {t("emptyState")} - {sortedGroups.map((group) => ( - - {group.label} - -
- } - > - {group.commands.map((command) => ( -
- { - command.action?.(); - setCommandPaletteOpen(false); - }} - > - - {command.label} - - {command.subItems?.map((subItem) => ( - - ))} -
- ))} - - ))} - - - ); -}; - -const SubItem = ({ - label, - icon, - action, -}: { - label: string; - icon: React.ReactNode; - action: () => void; -}) => { - const search = useCommandState((state) => state.search); - if (!search) { - return null; - } - - return ( - - {icon} - {label} - - ); -}; diff --git a/packages/web/src/components/DeviceInfoPanel.tsx b/packages/web/src/components/DeviceInfoPanel.tsx deleted file mode 100644 index f962a382b..000000000 --- a/packages/web/src/components/DeviceInfoPanel.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import type { ConnectionStatus } from "@app/core/stores/deviceStore/types.ts"; -import { cn } from "@core/utils/cn.ts"; -import type { Protobuf } from "@meshtastic/core"; -import { useNavigate } from "@tanstack/react-router"; -import { - ChevronRight, - CpuIcon, - Languages, - type LucideIcon, - Palette, - Search as SearchIcon, - ZapIcon, -} from "lucide-react"; -import type React from "react"; -import { Fragment } from "react"; -import { useTranslation } from "react-i18next"; -import BatteryStatus from "./BatteryStatus.tsx"; -import LanguageSwitcher from "./LanguageSwitcher.tsx"; -import ThemeSwitcher from "./ThemeSwitcher.tsx"; -import type { DeviceMetrics } from "./types.ts"; -import { Avatar } from "./UI/Avatar.tsx"; -import { Button } from "./UI/Button.tsx"; -import { Subtle } from "./UI/Typography/Subtle.tsx"; - -interface DeviceInfoPanelProps { - isCollapsed: boolean; - deviceMetrics: DeviceMetrics; - firmwareVersion: string; - user?: Protobuf.Mesh.User; - setDialogOpen: () => void; - setCommandPaletteOpen: () => void; - disableHover?: boolean; - connectionStatus?: ConnectionStatus; - connectionName?: string; -} - -interface InfoDisplayItem { - id: string; - label: string; - icon?: LucideIcon; - customComponent?: React.ReactNode; - value?: string | number | null; -} - -interface ActionButtonConfig { - id: string; - label: string; - icon: LucideIcon; - onClick?: () => void; - render?: () => React.ReactNode; -} - -export const DeviceInfoPanel = ({ - deviceMetrics, - firmwareVersion, - user, - isCollapsed, - setCommandPaletteOpen, - disableHover = false, - connectionStatus, - connectionName, -}: DeviceInfoPanelProps) => { - const { t } = useTranslation(); - const navigate = useNavigate({ from: "/" }); - const { batteryLevel, voltage } = deviceMetrics; - - const getStatusColor = (status?: ConnectionStatus): string => { - if (!status) { - return "bg-gray-400"; - } - switch (status) { - case "connected": - case "configured": - case "online": - return "bg-emerald-500"; - case "connecting": - case "configuring": - case "disconnecting": - return "bg-amber-500"; - case "error": - return "bg-red-500"; - default: - return "bg-gray-400"; - } - }; - - const getStatusLabel = (status?: ConnectionStatus): string => { - if (!status) { - return t("unknown.notAvailable", "N/A"); - } - // Show "connected" for configured state - if (status === "configured") { - return t("toasts.connected", { ns: "connections" }); - } - return status; - }; - - const deviceInfoItems: InfoDisplayItem[] = [ - { - id: "battery", - label: t("batteryStatus.title"), - customComponent: , - value: batteryLevel !== undefined ? `${batteryLevel}%` : "N/A", - }, - { - id: "voltage", - label: t("batteryVoltage.title"), - icon: ZapIcon, - value: - voltage !== undefined - ? `${voltage?.toPrecision(3)} V` - : t("unknown.notAvailable", "N/A"), - }, - { - id: "firmware", - label: t("sidebar.deviceInfo.firmware.title"), - icon: CpuIcon, - value: firmwareVersion ?? t("unknown.notAvailable", "N/A"), - }, - ]; - - const actionButtons: ActionButtonConfig[] = [ - { - id: "theme", - label: t("theme.changeTheme"), - icon: Palette, - render: () => , - }, - { - id: "commandMenu", - label: t("page.title", { ns: "commandPalette" }), - icon: SearchIcon, - onClick: setCommandPaletteOpen, - }, - - { - id: "language", - label: t("languagePicker.label"), - icon: Languages, - render: () => , - }, - ]; - - return ( - <> - {user && ( -
- - {!isCollapsed && ( -

- {user.longName} -

- )} -
- )} - - {connectionStatus && ( - - )} - - {!isCollapsed && ( -
- )} - -
- {deviceInfoItems.map((item) => { - const IconComponent = item.icon; - return ( -
- {IconComponent && ( - - )} - {item.customComponent} - {item.id !== "battery" && ( - - {item.label}: {item.value} - - )} -
- ); - })} -
- - {!isCollapsed && ( -
- )} - -
- {actionButtons.map((buttonItem) => { - const Icon = buttonItem.icon; - if (buttonItem.render) { - return ( - {buttonItem.render()} - ); - } - return ( - - ); - })} -
- - ); -}; diff --git a/packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.test.tsx b/packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.test.tsx deleted file mode 100644 index 50e6af3d4..000000000 --- a/packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { fireEvent, render, screen } from "@testing-library/react"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ClearAllStoresDialog } from "./ClearAllStoresDialog.tsx"; - -const mockClearAllStores = vi.fn(); - -vi.mock("@core/stores", () => ({ - CurrentDeviceContext: { - _currentValue: { deviceId: 1234 }, - }, - clearAllStores: () => mockClearAllStores(), -})); - -describe("ClearAllStoresDialog", () => { - const mockOnOpenChange = vi.fn(); - - // Capture window.location.href assignment without triggering real navigation - const originalLocation = window.location; - let assignedHref: string | undefined; - - beforeEach(() => { - mockOnOpenChange.mockClear(); - mockClearAllStores.mockClear(); - assignedHref = undefined; - - Object.defineProperty(window, "location", { - configurable: true, - value: { - ...originalLocation, - get href() { - return originalLocation.href; - }, - set href(val: string) { - assignedHref = val; - }, - }, - }); - }); - - // restore the real location object after each test - afterEach(() => { - Object.defineProperty(window, "location", { - configurable: true, - value: originalLocation, - }); - }); - - it("calls clearAllStores and navigates to '/' when confirm is clicked", () => { - render(); - fireEvent.click( - screen.getByRole("button", { name: "Clear all local storage" }), - ); - - expect(mockClearAllStores).toHaveBeenCalledTimes(1); - expect(assignedHref).toBe("/"); // forced reload target - // We reload instead of toggling the dialog, so ensure we didn't call onOpenChange - expect(mockOnOpenChange).not.toHaveBeenCalled(); - }); - - it("calls onOpenChange with false when cancel is clicked", () => { - render(); - fireEvent.click(screen.getByRole("button", { name: "Cancel" })); - - expect(mockClearAllStores).not.toHaveBeenCalled(); - expect(assignedHref).toBeUndefined(); // no navigation - expect(mockOnOpenChange).toHaveBeenCalledTimes(1); - expect(mockOnOpenChange).toHaveBeenCalledWith(false); - }); -}); diff --git a/packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.tsx b/packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.tsx deleted file mode 100644 index 731e0e8cb..000000000 --- a/packages/web/src/components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { clearAllStores } from "@core/stores"; -import { useTranslation } from "react-i18next"; -import { DialogWrapper } from "../DialogWrapper.tsx"; - -export interface ClearAllStoresDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const ClearAllStoresDialog = ({ - open, - onOpenChange, -}: ClearAllStoresDialogProps) => { - const { t } = useTranslation("dialog"); - - const handleClearAllStores = () => { - clearAllStores(); - - // Reload the app to ensure all state is cleared - window.location.href = "/"; - }; - - return ( - - ); -}; diff --git a/packages/web/src/components/Dialog/DialogManager.tsx b/packages/web/src/components/Dialog/DialogManager.tsx deleted file mode 100644 index 811e35a77..000000000 --- a/packages/web/src/components/Dialog/DialogManager.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { FactoryResetConfigDialog } from "@app/components/Dialog/FactoryResetConfigDialog/FactoryResetConfigDialog"; -import { FactoryResetDeviceDialog } from "@app/components/Dialog/FactoryResetDeviceDialog/FactoryResetDeviceDialog"; -import { ClearAllStoresDialog } from "@components/Dialog/ClearAllStoresDialog/ClearAllStoresDialog.tsx"; -import { ClientNotificationDialog } from "@components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx"; -import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx"; -import { ImportDialog } from "@components/Dialog/ImportDialog.tsx"; -import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx"; -import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx"; -import { QRDialog } from "@components/Dialog/QRDialog.tsx"; -import { RebootDialog } from "@components/Dialog/RebootDialog.tsx"; -import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx"; -import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx"; -import { ResetNodeDbDialog } from "@components/Dialog/ResetNodeDbDialog/ResetNodeDbDialog.tsx"; -import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx"; -import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx"; -import { useDevice } from "@core/stores"; - -export const DialogManager = () => { - const { channels, config, dialog, setDialogOpen } = useDevice(); - return ( - <> - { - setDialogOpen("QR", open); - }} - channels={channels} - loraConfig={config.lora} - /> - { - setDialogOpen("import", open); - }} - loraConfig={config.lora} - /> - { - setDialogOpen("shutdown", false); - }} - /> - { - setDialogOpen("reboot", false); - }} - /> - { - setDialogOpen("nodeRemoval", open); - }} - /> - { - setDialogOpen("pkiBackup", open); - }} - /> - { - setDialogOpen("nodeDetails", open); - }} - /> - { - setDialogOpen("unsafeRoles", open); - }} - /> - { - setDialogOpen("refreshKeys", open); - }} - /> - { - setDialogOpen("deleteMessages", open); - }} - /> - { - setDialogOpen("clientNotification", open); - }} - /> - { - setDialogOpen("resetNodeDb", open); - }} - /> - { - setDialogOpen("clearAllStores", open); - }} - /> - { - setDialogOpen("factoryResetDevice", open); - }} - /> - { - setDialogOpen("factoryResetConfig", open); - }} - /> - - ); -}; diff --git a/packages/web/src/components/Dialog/ImportDialog.tsx b/packages/web/src/components/Dialog/ImportDialog.tsx deleted file mode 100644 index aea9bf74f..000000000 --- a/packages/web/src/components/Dialog/ImportDialog.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { create, fromBinary } from "@bufbuild/protobuf"; -import { Button } from "@components/UI/Button.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; -import { Input } from "@components/UI/Input.tsx"; -import { Label } from "@components/UI/Label.tsx"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@components/UI/Select.tsx"; -import { Switch } from "@components/UI/Switch.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { toByteArray } from "base64-js"; -import { useEffect, useState } from "react"; -import { Trans, useTranslation } from "react-i18next"; - -export interface ImportDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; - loraConfig?: Protobuf.Config.Config_LoRaConfig; -} - -export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => { - const { setChange, channels, config } = useDevice(); - const { t } = useTranslation("dialog"); - const [importDialogInput, setImportDialogInput] = useState(""); - const [channelSet, setChannelSet] = useState(); - const [validUrl, setValidUrl] = useState(false); - const [updateConfig, setUpdateConfig] = useState(true); - const [importIndex, setImportIndex] = useState([]); - - useEffect(() => { - // the channel information is contained in the URL's fragment, which will be present after a - // non-URL encoded `#`. - try { - const channelsUrl = new URL(importDialogInput); - if ( - (channelsUrl.hostname !== "meshtastic.org" && - channelsUrl.pathname !== "/e/") || - !channelsUrl.hash - ) { - throw t("import.error.invalidUrl"); - } - - const encodedChannelConfig = channelsUrl.hash.substring(1); - const paddedString = encodedChannelConfig - .padEnd( - encodedChannelConfig.length + - ((4 - (encodedChannelConfig.length % 4)) % 4), - "=", - ) - .replace(/-/g, "+") - .replace(/_/g, "/"); - - const newChannelSet = fromBinary( - Protobuf.AppOnly.ChannelSetSchema, - toByteArray(paddedString), - ); - - const newImportChannelArray = newChannelSet.settings.map((_, idx) => idx); - - setChannelSet(newChannelSet); - setImportIndex(newImportChannelArray); - setUpdateConfig(newChannelSet?.loraConfig !== undefined); - setValidUrl(true); - } catch (_error) { - setValidUrl(false); - setChannelSet(undefined); - } - }, [importDialogInput, t]); - - const apply = () => { - channelSet?.settings.forEach( - (ch: Protobuf.Channel.ChannelSettings, index: number) => { - if (importIndex[index] === -1) { - return; - } - - const payload = create(Protobuf.Channel.ChannelSchema, { - index: importIndex[index], - role: - importIndex[index] === 0 - ? Protobuf.Channel.Channel_Role.PRIMARY - : Protobuf.Channel.Channel_Role.SECONDARY, - settings: ch, - }); - - if ( - !deepCompareConfig( - channels.get(importIndex[index] ?? 0), - payload, - true, - ) - ) { - setChange( - { type: "channels", index: importIndex[index] ?? 0 }, - payload, - channels.get(importIndex[index] ?? 0), - ); - } - }, - ); - - if (channelSet?.loraConfig && updateConfig) { - const payload = { - ...config.lora, - ...channelSet.loraConfig, - }; - - if (!deepCompareConfig(config.lora, payload, true)) { - setChange({ type: "config", variant: "lora" }, payload, config.lora); - } - } - // Reset state after import - setImportDialogInput(""); - setChannelSet(undefined); - setValidUrl(false); - setImportIndex([]); - setUpdateConfig(true); - - onOpenChange(false); - }; - - const onSelectChange = (value: string, index: number) => { - const newImportIndex = [...importIndex]; - newImportIndex[index] = Number.parseInt(value, 10); - setImportIndex(newImportIndex); - }; - - return ( - - - - - {t("import.title")} - - , br:
}} - /> -
-
-
- - { - setImportDialogInput(e.target.value); - }} - /> - {validUrl && ( -
-
-
- setUpdateConfig(next)} - /> - -
-
- -
-
- {t("import.channelName")} - {t("import.channelSlot")} -
- {channelSet?.settings.map((channel, index) => ( -
- - -
- ))} -
-
- )} -
- - - -
-
- ); -}; diff --git a/packages/web/src/components/Dialog/LocationResponseDialog.tsx b/packages/web/src/components/Dialog/LocationResponseDialog.tsx deleted file mode 100644 index 0fa9aff91..000000000 --- a/packages/web/src/components/Dialog/LocationResponseDialog.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useNodeDB } from "@core/stores"; -import type { Protobuf, Types } from "@meshtastic/core"; -import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; -import { useTranslation } from "react-i18next"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "../UI/Dialog.tsx"; - -export interface LocationResponseDialogProps { - location: Types.PacketMetadata | undefined; - open: boolean; - onOpenChange: () => void; -} - -export const LocationResponseDialog = ({ - location, - open, - onOpenChange, -}: LocationResponseDialogProps) => { - const { t } = useTranslation("dialog"); - const { getNode } = useNodeDB(); - - const from = getNode(location?.from ?? 0); - const longName = - from?.user?.longName ?? - (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName")); - const shortName = - from?.user?.shortName ?? - (from - ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` - : t("unknown.shortName")); - - const position = location?.data; - - const hasCoordinates = - position && - typeof position.latitudeI === "number" && - typeof position.longitudeI === "number" && - typeof position.altitude === "number"; - - return ( - - - - - - {t("locationResponse.title", { - interpolation: { escapeValue: false }, - identifier: `${longName} (${shortName})`, - })} - - - - {hasCoordinates ? ( -
- -

- {t("locationResponse.coordinates")} - - {" "} - {position.latitudeI ?? 0 / 1e7},{" "} - {position.longitudeI ?? 0 / 1e7} - -

-

- {t("locationResponse.altitude")} {position.altitude} - {(position.altitude ?? 0) < 1 - ? t("unit.meter.one") - : t("unit.meter.plural")} -

-
-
- ) : ( - // Optional: Show a message if coordinates are not available -

- {t("locationResponse.noCoordinates")} -

- )} -
-
-
- ); -}; diff --git a/packages/web/src/components/Dialog/NewDeviceDialog.tsx b/packages/web/src/components/Dialog/NewDeviceDialog.tsx deleted file mode 100644 index e6170123a..000000000 --- a/packages/web/src/components/Dialog/NewDeviceDialog.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { BLE } from "@components/PageComponents/Connect/BLE.tsx"; -import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx"; -import { Serial } from "@components/PageComponents/Connect/Serial.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@components/UI/Tabs.tsx"; -import { - type BrowserFeature, - useBrowserFeatureDetection, -} from "@core/hooks/useBrowserFeatureDetection.ts"; -import { AlertCircle } from "lucide-react"; -import { Trans, useTranslation } from "react-i18next"; -import { Link } from "../UI/Typography/Link.tsx"; - -export interface TabElementProps { - closeDialog: () => void; -} - -export interface TabManifest { - id: "HTTP" | "BLE" | "Serial"; - label: string; - element: React.FC; - isDisabled: boolean; -} - -export interface NewDeviceProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -interface FeatureErrorProps { - missingFeatures: BrowserFeature[]; - tabId: "HTTP" | "BLE" | "Serial"; -} - -const errors: Record = { - "Web Bluetooth": { - href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", - i18nKey: "newDeviceDialog.validation.requiresWebBluetooth", - }, - "Web Serial": { - href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", - i18nKey: "newDeviceDialog.validation.requiresWebSerial", - }, - "Secure Context": { - href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", - i18nKey: "newDeviceDialog.validation.requiresSecureContext", - }, -}; - -const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => { - if (missingFeatures.length === 0) { - return null; - } - - const browserFeatures = missingFeatures.filter( - (feature) => feature !== "Secure Context", - ); - const needsSecureContext = missingFeatures.includes("Secure Context"); - - const needsFeature = - tabId === "BLE" && browserFeatures.includes("Web Bluetooth") - ? "Web Bluetooth" - : tabId === "Serial" && browserFeatures.includes("Web Serial") - ? "Web Serial" - : undefined; - - return ( -
-
- -
-
- {needsFeature && ( - , - ]} - /> - )} - {needsFeature && needsSecureContext && " "} - {needsSecureContext && ( - 0 - ? "newDeviceDialog.validation.additionallyRequiresSecureContext" - : "newDeviceDialog.validation.requiresSecureContext" - } - components={{ - "0": ( - - ), - }} - /> - )} -
-
-
-
- ); -}; - -export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => { - const { t } = useTranslation("dialog"); - const { unsupported } = useBrowserFeatureDetection(); - - const tabs: TabManifest[] = [ - { - id: "HTTP", - label: t("newDeviceDialog.tabHttp"), - element: HTTP, - isDisabled: false, - }, - { - id: "BLE", - label: t("newDeviceDialog.tabBluetooth"), - element: BLE, - isDisabled: - unsupported.includes("Web Bluetooth") || - unsupported.includes("Secure Context"), - }, - { - id: "Serial", - label: t("newDeviceDialog.tabSerial"), - element: Serial, - isDisabled: - unsupported.includes("Web Serial") || - unsupported.includes("Secure Context"), - }, - ]; - - return ( - - - - - {t("newDeviceDialog.title")} - - - - {tabs.map((tab) => ( - - {tab.label} - - ))} - - {tabs.map((tab) => ( - -
- {tab.id !== "HTTP" && tab.isDisabled ? ( - - ) : ( - onOpenChange(false)} /> - )} -
-
- ))} -
-
-
- ); -}; diff --git a/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx b/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx deleted file mode 100644 index e2eaa0a7d..000000000 --- a/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx +++ /dev/null @@ -1,472 +0,0 @@ -import { DeviceImage } from "@components/generic/DeviceImage.tsx"; -import { TimeAgo } from "@components/generic/TimeAgo.tsx"; -import { Uptime } from "@components/generic/Uptime.tsx"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@components/UI/Accordion.tsx"; -import { Button } from "@components/UI/Button.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; -import { Separator } from "@components/UI/Separator.tsx"; -import { - Tooltip, - TooltipArrow, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@components/UI/Tooltip.tsx"; -import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts"; -import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts"; -import { toast } from "@core/hooks/useToast.ts"; -import { useAppStore, useDevice, useNodeDB } from "@core/stores"; -import { cn } from "@core/utils/cn.ts"; -import { Protobuf } from "@meshtastic/core"; -import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; -import { useNavigate } from "@tanstack/react-router"; -import { fromByteArray } from "base64-js"; -import { - BellIcon, - BellOffIcon, - MapPinnedIcon, - MessageSquareIcon, - StarIcon, - TrashIcon, - WaypointsIcon, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; - -export interface NodeDetailsDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const NodeDetailsDialog = ({ - open, - onOpenChange, -}: NodeDetailsDialogProps) => { - const { t } = useTranslation("dialog"); - const { setDialogOpen, connection } = useDevice(); - const { getNode } = useNodeDB(); - const navigate = useNavigate(); - const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore(); - const { updateFavorite } = useFavoriteNode(); - const { updateIgnored } = useIgnoreNode(); - - const node = getNode(nodeNumDetails); - - const [isFavoriteState, setIsFavoriteState] = useState( - node?.isFavorite ?? false, - ); - const [isIgnoredState, setIsIgnoredState] = useState( - node?.isIgnored ?? false, - ); - - useEffect(() => { - if (!node) { - return; - } - setIsFavoriteState(node?.isFavorite); - setIsIgnoredState(node?.isIgnored); - }, [node]); - - if (!node) { - return; - } - - function handleDirectMessage() { - if (!node) { - return; - } - navigate({ to: `/messages/direct/${node.num}` }); - setDialogOpen("nodeDetails", false); - } - - function handleRequestPosition() { - if (!node) { - return; - } - - toast({ - title: t("toast.requestingPosition.title", { ns: "ui" }), - }); - connection?.requestPosition(node.num).then(() => - toast({ - title: t("toast.positionRequestSent.title", { ns: "ui" }), - }), - ); - onOpenChange(false); - } - - function handleTraceroute() { - if (!node) { - return; - } - - toast({ - title: t("toast.sendingTraceroute.title", { ns: "ui" }), - }); - connection?.traceRoute(node.num).then(() => - toast({ - title: t("toast.tracerouteSent.title", { ns: "ui" }), - }), - ); - onOpenChange(false); - } - - function handleNodeRemove() { - if (!node) { - return; - } - - setNodeNumToBeRemoved(node?.num); - setDialogOpen("nodeRemoval", true); - onOpenChange(false); - } - - function handleToggleFavorite() { - if (!node) { - return; - } - - updateFavorite({ nodeNum: node.num, isFavorite: !isFavoriteState }); - setIsFavoriteState(!isFavoriteState); - } - - function handleToggleIgnored() { - if (!node) { - return; - } - - updateIgnored({ nodeNum: node.num, isIgnored: !isIgnoredState }); - setIsIgnoredState(!isIgnoredState); - } - - const deviceMetricsMap = [ - { - key: "airUtilTx", - label: t("nodeDetails.airTxUtilization"), - value: node.deviceMetrics?.airUtilTx, - format: (val: number) => `${val.toFixed(2)}%`, - }, - { - key: "channelUtilization", - label: t("nodeDetails.channelUtilization"), - value: node.deviceMetrics?.channelUtilization, - format: (val: number) => `${val.toFixed(2)}%`, - }, - { - key: "batteryLevel", - label: t("nodeDetails.batteryLevel"), - value: node.deviceMetrics?.batteryLevel, - format: (val: number) => - val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`, - }, - { - key: "voltage", - label: t("nodeDetails.voltage"), - value: - typeof node.deviceMetrics?.voltage === "number" - ? Math.abs(node.deviceMetrics?.voltage) - : undefined, - format: (val: number) => `${val.toFixed(2)}V`, - }, - ]; - - const sectionClassName = - "text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-4 rounded-lg mt-3"; - - return ( - - - - - - {t("nodeDetails.title", { - interpolation: { escapeValue: false }, - identifier: `${node.user?.longName ?? t("unknown.shortName")} (${ - node.user?.shortName ?? t("unknown.shortName") - })`, - })} - - - -
-
- - - -
- - - - - - - - {isIgnoredState - ? t("nodeDetails.unignoreNode") - : t("nodeDetails.ignoreNode")} - - - - - - - - - - - - {t("nodeDetails.removeNode")} - - - - -
- - - -
-
-
-

- {t("nodeDetails.details")} -

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{t("nodeDetails.nodeNumber")}{node.num}
{t("nodeDetails.nodeHexPrefix")}!{numberToHexUnpadded(node.num)}
{t("nodeDetails.role")} - {Protobuf.Config.Config_DeviceConfig_Role[ - node.user?.role ?? 0 - ]?.replace(/_/g, " ")} -
{t("nodeDetails.lastHeard")} - {node.lastHeard === 0 ? ( - t("nodesTable.lastHeardStatus.never", { - ns: "nodes", - }) - ) : ( - - )} -
{t("nodeDetails.hardware")} - {( - Protobuf.Mesh.HardwareModel[ - node.user?.hwModel ?? 0 - ] ?? t("unknown.shortName") - ).replace(/_/g, " ")} -
{t("nodeDetails.messageable")} - {node.user?.isUnmessagable ? t("no") : t("yes")} -
-
- -
-
- -
-
-

- {t("nodeDetails.security")} -

- - - - - - - - - - - -
{t("nodeDetails.publicKey")} -
-                          {node.user?.publicKey &&
-                          node.user?.publicKey.length > 0
-                            ? fromByteArray(node.user.publicKey)
-                            : t("unknown.longName")}
-                        
-
- {node.isKeyManuallyVerified - ? t("nodeDetails.KeyManuallyVerifiedTrue") - : t("nodeDetails.KeyManuallyVerifiedFalse")} -
-
- -
-

- {t("nodeDetails.position")} -

- - {node.position ? ( - - - {node.position.latitudeI && node.position.longitudeI && ( - - - - - )} - {node.position.altitude && ( - - - - - )} - -
{t("locationResponse.coordinates")} - - {node.position.latitudeI / 1e7},{" "} - {node.position.longitudeI / 1e7} - -
{t("locationResponse.altitude")} - {node.position.altitude} - {t("unit.meter.suffix")} -
- ) : ( -

{t("unknown.longName")}

- )} - -
- - {node.deviceMetrics && ( -
-

- {t("nodeDetails.deviceMetrics")} -

- - - {deviceMetricsMap - .filter((metric) => metric.value !== undefined) - .map((metric) => ( - - - - - ))} - {node.deviceMetrics.uptimeSeconds && ( - - - - - )} - -
{metric.label}: {metric.format(metric?.value ?? 0)}
{t("nodeDetails.uptime")} - -
-
- )} -
- -
- - - -

- {t("nodeDetails.allRawMetrics")} -

-
- -
-                      {JSON.stringify(node, null, 2)}
-                    
-
-
-
-
-
- - -
- ); -}; diff --git a/packages/web/src/components/Dialog/PKIBackupDialog.tsx b/packages/web/src/components/Dialog/PKIBackupDialog.tsx deleted file mode 100644 index 2c19721e2..000000000 --- a/packages/web/src/components/Dialog/PKIBackupDialog.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { Button } from "@components/UI/Button.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; -import { useDevice, useNodeDB } from "@core/stores"; -import { fromByteArray } from "base64-js"; -import { DownloadIcon, PrinterIcon } from "lucide-react"; -import React from "react"; -import { useTranslation } from "react-i18next"; - -export interface PkiBackupDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const PkiBackupDialog = ({ - open, - onOpenChange, -}: PkiBackupDialogProps) => { - const { t } = useTranslation("dialog"); - const { config, setDialogOpen } = useDevice(); - const { getMyNode } = useNodeDB(); - const privateKey = config.security?.privateKey; - const publicKey = config.security?.publicKey; - - const decodeKeyData = React.useCallback( - (key: Uint8Array) => { - if (!key) { - return ""; - } - return fromByteArray(key ?? new Uint8Array(0)); - }, - [], - ); - - const closeDialog = React.useCallback(() => { - setDialogOpen("pkiBackup", false); - }, [setDialogOpen]); - - const renderPrintWindow = React.useCallback(() => { - if (!privateKey || !publicKey) { - return; - } - - const printWindow = globalThis.open("", "_blank"); - if (printWindow) { - printWindow.document.write(` - - - ${t("pkiBackup.header", { - interpolation: { escapeValue: false }, - shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"), - longName: getMyNode()?.user?.longName ?? t("unknown.longName"), - })} - - - -

${t("pkiBackup.header", { - interpolation: { escapeValue: false }, - shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"), - longName: getMyNode()?.user?.longName ?? t("unknown.longName"), - })}

-

${t("pkiBackup.secureBackup")}

-

${t("pkiBackup.publicKey")}

-

${decodeKeyData(publicKey)}

-

${t("pkiBackup.privateKey")}

-

${decodeKeyData(privateKey)}

-

${t("pkiBackup.footer")}

- - - `); - printWindow.document.close(); - printWindow.print(); - closeDialog(); - } - }, [decodeKeyData, privateKey, publicKey, closeDialog, t, getMyNode]); - - const createDownloadKeyFile = React.useCallback(() => { - if (!privateKey || !publicKey) { - return; - } - - const decodedPrivateKey = decodeKeyData(privateKey); - const decodedPublicKey = decodeKeyData(publicKey); - - const formattedContent = [ - `${t("pkiBackup.header", { - interpolation: { escapeValue: false }, - shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"), - longName: getMyNode()?.user?.longName ?? t("unknown.longName"), - })}\n\n`, - `${t("pkiBackup.privateKey")}\n`, - decodedPrivateKey, - `\n\n${t("pkiBackup.publicKey")}\n`, - decodedPublicKey, - `\n\n${t("pkiBackup.footer")}`, - ].join(""); - - const blob = new Blob([formattedContent], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = t("pkiBackup.fileName", { - interpolation: { escapeValue: false }, - shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"), - longName: getMyNode()?.user?.longName ?? t("unknown.longName"), - }); - - link.style.display = "none"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - closeDialog(); - URL.revokeObjectURL(url); - }, [decodeKeyData, privateKey, publicKey, closeDialog, t, getMyNode]); - - return ( - - - - - {t("pkiBackup.title")} - {t("pkiBackup.secureBackup")} - - - {t("pkiBackup.loseKeysWarning")} - - - - - - - - - - ); -}; diff --git a/packages/web/src/components/Dialog/PkiRegenerateDialog.tsx b/packages/web/src/components/Dialog/PkiRegenerateDialog.tsx deleted file mode 100644 index 8a07969de..000000000 --- a/packages/web/src/components/Dialog/PkiRegenerateDialog.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Button } from "@components/UI/Button.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; -import { useTranslation } from "react-i18next"; - -export interface PkiRegenerateDialogProps { - text: { - title: string; - description: string; - button: string; - }; - open: boolean; - onOpenChange: () => void; - onSubmit: () => void; -} - -export const PkiRegenerateDialog = ({ - text = { - title: "", - description: "", - button: "", - }, - open, - onOpenChange, - onSubmit, -}: PkiRegenerateDialogProps) => { - const { t } = useTranslation("dialog"); - const dialogText = { - title: text.title || t("pkiRegenerate.title"), - description: text.description || t("pkiRegenerate.description"), - button: text.button || t("button.regenerate"), - }; - return ( - - - - - {dialogText.title} - {dialogText.description} - - - - - - - ); -}; diff --git a/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx b/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx deleted file mode 100644 index f7ba3ec77..000000000 --- a/packages/web/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Button } from "@components/UI/Button.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; -import { useMessages, useNodeDB } from "@core/stores"; -import { LockKeyholeOpenIcon } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; - -export interface RefreshKeysDialogProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export const RefreshKeysDialog = ({ - open, - onOpenChange, -}: RefreshKeysDialogProps) => { - const { t } = useTranslation("dialog"); - const { activeChat } = useMessages(); - const { nodeErrors, getNode } = useNodeDB(); - - const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog(); - - const nodeErrorNum = nodeErrors.get(activeChat); - - if (!nodeErrorNum) { - return null; - } - - const nodeWithError = getNode(nodeErrorNum.node); - - const text = { - title: t("refreshKeys.title", { - interpolation: { escapeValue: false }, - identifier: nodeWithError?.user?.longName ?? "", - }), - description: `${t("refreshKeys.description.unableToSendDmPrefix")}${ - nodeWithError?.user?.longName ?? "" - } (${nodeWithError?.user?.shortName ?? ""})${t( - "refreshKeys.description.keyMismatchReasonSuffix", - )}`, - }; - - return ( - - - - - {text.title} - - {text.description} -
    -
  • -
    - -
    -
    -
    -

    - {t("refreshKeys.label.acceptNewKeys")} -

    -

    {t("refreshKeys.description.acceptNewKeys")}

    -
    - - -
    -
  • -
-
-
- ); -}; diff --git a/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts b/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts deleted file mode 100644 index 40156a95e..000000000 --- a/packages/web/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useDevice, useMessages, useNodeDB } from "@core/stores"; -import { useCallback } from "react"; - -export function useRefreshKeysDialog() { - const { setDialogOpen } = useDevice(); - const { removeNode, clearNodeError, getNodeError } = useNodeDB(); - const { activeChat } = useMessages(); - - const handleCloseDialog = useCallback(() => { - setDialogOpen("refreshKeys", false); - }, [setDialogOpen]); - - const handleNodeRemove = useCallback(() => { - const nodeWithError = getNodeError(activeChat); - if (!nodeWithError) { - return; - } - clearNodeError(activeChat); - handleCloseDialog(); - return removeNode(nodeWithError?.node); - }, [activeChat, clearNodeError, getNodeError, removeNode, handleCloseDialog]); - - return { - handleCloseDialog, - handleNodeRemove, - }; -} diff --git a/packages/web/src/components/Dialog/TracerouteResponseDialog.tsx b/packages/web/src/components/Dialog/TracerouteResponseDialog.tsx deleted file mode 100644 index cf27fe2d2..000000000 --- a/packages/web/src/components/Dialog/TracerouteResponseDialog.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useNodeDB } from "@core/stores"; -import type { Protobuf, Types } from "@meshtastic/core"; -import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; -import { useTranslation } from "react-i18next"; - -import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "../UI/Dialog.tsx"; - -export interface TracerouteResponseDialogProps { - traceroute: Types.PacketMetadata | undefined; - open: boolean; - onOpenChange: () => void; -} - -export const TracerouteResponseDialog = ({ - traceroute, - open, - onOpenChange, -}: TracerouteResponseDialogProps) => { - const { t } = useTranslation("dialog"); - const { getNode } = useNodeDB(); - const route: number[] = traceroute?.data.route ?? []; - const routeBack: number[] = traceroute?.data.routeBack ?? []; - const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4); - const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4); - const from = getNode(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet - const fromLongName = - from?.user?.longName ?? - (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName")); - const fromShortName = - from?.user?.shortName ?? - (from - ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` - : t("unknown.shortName")); - - const toUser = getNode(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet - - if (!toUser || !from) { - return null; - } - - return ( - - - - - - {t("tracerouteResponse.title", { - identifier: `${fromLongName} (${fromShortName})`, - })} - - - - - - - - ); -}; diff --git a/packages/web/src/components/Form/DynamicForm.tsx b/packages/web/src/components/Form/DynamicForm.tsx deleted file mode 100644 index f94a55a22..000000000 --- a/packages/web/src/components/Form/DynamicForm.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import { createZodResolver } from "@components/Form/createZodResolver.ts"; -import { - DynamicFormField, - type FieldProps, -} from "@components/Form/DynamicFormField.tsx"; -import { FieldWrapper } from "@components/Form/FormWrapper.tsx"; -import { Button } from "@components/UI/Button.tsx"; -import { Heading } from "@components/UI/Typography/Heading.tsx"; -import { Subtle } from "@components/UI/Typography/Subtle.tsx"; -import { useEffect } from "react"; -import { - type Control, - type DefaultValues, - type FieldValues, - FormProvider, - get, - type Path, - type SubmitHandler, - type UseFormReturn, - useForm, -} from "react-hook-form"; -import { useTranslation } from "react-i18next"; -import type { ZodType } from "zod/v4"; - -interface DisabledBy { - fieldName: Path; - selector?: number; - invert?: boolean; -} - -export interface BaseFormBuilderProps { - name: Path; - disabled?: boolean; - disabledBy?: DisabledBy[]; - label: string; - description?: string; - notes?: string; - validationText?: string; - properties?: Record; -} - -export interface GenericFormElementProps { - control: Control; - disabled?: boolean; - field: Y; - isDirty?: boolean; - invalid?: boolean; -} - -export interface DynamicFormProps { - propMethods?: UseFormReturn; - onSubmit: SubmitHandler; - onFormInit?: DynamicFormFormInit; - submitType?: "onChange" | "onSubmit"; - hasSubmitButton?: boolean; - defaultValues?: DefaultValues; - values?: T; - fieldGroups: { - label: string; - description: string; - notes?: string; - valid?: boolean; - validationText?: string; - fields: FieldProps[]; - }[]; - validationSchema?: ZodType; -} - -export type DynamicFormFormInit = ( - methods: UseFormReturn, -) => void; - -export function DynamicForm({ - propMethods, - onSubmit, - onFormInit, - submitType = "onChange", - hasSubmitButton, - defaultValues, - values, - fieldGroups, - validationSchema, -}: DynamicFormProps) { - const { t } = useTranslation(); - - const internalMethods = useForm({ - mode: "onChange", - defaultValues: defaultValues, - resolver: validationSchema - ? createZodResolver(validationSchema) - : undefined, - shouldFocusError: false, - resetOptions: { keepDefaultValues: true }, - values, - }); - - const methods = propMethods ?? internalMethods; - - const { handleSubmit, control, getValues, formState, getFieldState } = - methods; - - useEffect(() => { - if (!propMethods) { - onFormInit?.(internalMethods); - } - }, [onFormInit, propMethods, internalMethods]); - - const isDisabled = ( - disabledBy?: DisabledBy[], - disabled?: boolean, - ): boolean => { - if (disabled) { - return true; - } - if (!disabledBy) { - return false; - } - - return disabledBy.some((field) => { - const value = getValues(field.fieldName); - if (value === "always") { - return true; - } - if (typeof value === "boolean") { - return field.invert ? value : !value; - } - if (typeof value === "number") { - return field.invert - ? field.selector !== value - : field.selector === value; - } - return false; - }); - }; - - return ( - -
- {fieldGroups.map((fieldGroup) => ( -
-
- - {fieldGroup.label} - - {fieldGroup.description} - {fieldGroup?.notes} -
- - {fieldGroup.fields.map((field) => { - const error = get(formState.errors, field.name as string); - return ( - - - - ); - })} -
- ))} - {hasSubmitButton && ( - - )} -
-
- ); -} diff --git a/packages/web/src/components/Form/DynamicFormField.tsx b/packages/web/src/components/Form/DynamicFormField.tsx deleted file mode 100644 index 9452d30c4..000000000 --- a/packages/web/src/components/Form/DynamicFormField.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { - GenericInput, - type InputFieldProps, -} from "@components/Form/FormInput.tsx"; -import { - PasswordGenerator, - type PasswordGeneratorProps, -} from "@components/Form/FormPasswordGenerator.tsx"; -import { - type SelectFieldProps, - SelectInput, -} from "@components/Form/FormSelect.tsx"; -import { - type ToggleFieldProps, - ToggleInput, -} from "@components/Form/FormToggle.tsx"; -import type { Control, FieldValues } from "react-hook-form"; -import { - type MultiSelectFieldProps, - MultiSelectInput, -} from "./FormMultiSelect.tsx"; - -export type FieldProps = - | InputFieldProps - | SelectFieldProps - | MultiSelectFieldProps - | ToggleFieldProps - | PasswordGeneratorProps; - -export interface DynamicFormFieldProps { - field: FieldProps; - control: Control; - disabled?: boolean; - isDirty?: boolean; - invalid?: boolean; -} - -export function DynamicFormField({ - field, - control, - disabled, - isDirty, - invalid, -}: DynamicFormFieldProps) { - switch (field.type) { - case "text": - case "password": - case "number": - return ( - - ); - - case "toggle": - return ( - - ); - case "select": - return ( - - ); - case "passwordGenerator": - return ( - - ); - case "multiSelect": - return ( - - ); - } -} diff --git a/packages/web/src/components/Form/FormInput.tsx b/packages/web/src/components/Form/FormInput.tsx deleted file mode 100644 index 487f96569..000000000 --- a/packages/web/src/components/Form/FormInput.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import type { - BaseFormBuilderProps, - GenericFormElementProps, -} from "@components/Form/DynamicForm.tsx"; -import { Input } from "@components/UI/Input.tsx"; -import type { ChangeEventHandler } from "react"; -import { type FieldValues, useController } from "react-hook-form"; - -export interface InputFieldProps extends BaseFormBuilderProps { - type: "text" | "number" | "password"; - inputChange?: ChangeEventHandler; - prefix?: string; - properties?: { - id?: string; - suffix?: string; - step?: number; - className?: string; - fieldLength?: { - min?: number; - max?: number; - currentValueLength?: number; - showCharacterCount?: boolean; - }; - showPasswordToggle?: boolean; - showCopyButton?: boolean; - }; -} - -export function GenericInput({ - control, - disabled, - field, -}: GenericFormElementProps>) { - const { fieldLength, ...restProperties } = field.properties || {}; - - const { - field: controllerField, - fieldState: { error, isDirty }, - } = useController({ - name: field.name, - control, - rules: { - minLength: field.properties?.fieldLength?.min, - maxLength: field.properties?.fieldLength?.max, - }, - }); - - const handleInputChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - - if ( - field.properties?.fieldLength?.max && - newValue.length > field.properties.fieldLength.max - ) { - return; - } - - if (field.inputChange) { - field.inputChange(e); - } - - controllerField.onChange( - field.type === "number" - ? Number.parseFloat(newValue).toString() - : newValue, - ); - }; - - const currentLength = controllerField.value - ? String(controllerField.value).length - : 0; - - return ( -
- - - {fieldLength?.showCharacterCount && fieldLength.max && ( -
- {currentLength}/{fieldLength.max} -
- )} -
- ); -} diff --git a/packages/web/src/components/KeyBackupReminder.tsx b/packages/web/src/components/KeyBackupReminder.tsx deleted file mode 100644 index 6a5561451..000000000 --- a/packages/web/src/components/KeyBackupReminder.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { useBackupReminder } from "@core/hooks/useKeyBackupReminder.tsx"; -import { useDevice } from "@core/stores"; -import { useTranslation } from "react-i18next"; - -export const KeyBackupReminder = () => { - const { setDialogOpen } = useDevice(); - const { t } = useTranslation("dialog"); - - useBackupReminder({ - message: t("pkiBackupReminder.description"), - onAccept: () => setDialogOpen("pkiBackup", true), - enabled: true, - }); - return null; -}; diff --git a/packages/web/src/components/Map.tsx b/packages/web/src/components/Map.tsx deleted file mode 100644 index 0300a96c3..000000000 --- a/packages/web/src/components/Map.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useTheme } from "@core/hooks/useTheme.ts"; -import { useEffect, useMemo, useRef } from "react"; -import { useTranslation } from "react-i18next"; -import MapGl, { - AttributionControl, - type MapLayerMouseEvent, - type MapRef, - NavigationControl, - ScaleControl, -} from "react-map-gl/maplibre"; - -interface MapProps { - children?: React.ReactNode; - onLoad?: (map: MapRef) => void; - onMouseMove?: (event: MapLayerMouseEvent) => void; - onClick?: (event: MapLayerMouseEvent) => void; - interactiveLayerIds?: string[]; - initialViewState?: { - latitude?: number; - longitude?: number; - zoom?: number; - }; -} - -export const BaseMap = ({ - children, - onLoad, - onClick, - onMouseMove, - interactiveLayerIds, - initialViewState, -}: MapProps) => { - const { theme } = useTheme(); - const { t } = useTranslation("map"); - - const darkMode = theme === "dark"; - const mapRef = useRef(null); - - useEffect(() => { - const map = mapRef.current; - if (map && onLoad) { - onLoad(map); - } - }, [onLoad]); - - const locale = useMemo(() => { - return { - "GeolocateControl.FindMyLocation": t( - "maplibre.GeolocateControl.FindMyLocation", - ), - "NavigationControl.ZoomIn": t("maplibre.NavigationControl.ZoomIn"), - "NavigationControl.ZoomOut": t("maplibre.NavigationControl.ZoomOut"), - "ScaleControl.Meters": t("unit.meter.suffix"), - "ScaleControl.Kilometers": t("unit.kilometer.suffix"), - "CooperativeGesturesHandler.WindowsHelpText": t( - "maplibre.CooperativeGesturesHandler.WindowsHelpText", - ), - "CooperativeGesturesHandler.MacHelpText": t( - "maplibre.CooperativeGesturesHandler.MacHelpText", - ), - "CooperativeGesturesHandler.MobileHelpText": t( - "maplibre.CooperativeGesturesHandler.MobileHelpText", - ), - }; - }, [t]); - - return ( - - - {/* { Disabled for now until we can use i18n for the geolocate control} */} - {/* */} - - - {children} - - ); -}; diff --git a/packages/web/src/components/PageComponents/Channels/Channel.tsx b/packages/web/src/components/PageComponents/Channels/Channel.tsx deleted file mode 100644 index deb81d5de..000000000 --- a/packages/web/src/components/PageComponents/Channels/Channel.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import { - type ChannelValidation, - makeChannelSchema, -} from "@app/validation/channel.ts"; -import { create } from "@bufbuild/protobuf"; -import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx"; -import { createZodResolver } from "@components/Form/createZodResolver.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { fromByteArray, toByteArray } from "base64-js"; -import cryptoRandomString from "crypto-random-string"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { type DefaultValues, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; - -export interface SettingsPanelProps { - onFormInit: DynamicFormFormInit; - channel: Protobuf.Channel.Channel; -} - -export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => { - const { config, setChange, getChange, removeChange } = useDevice(); - const { t } = useTranslation(["channels", "ui", "dialog"]); - - const defaultConfig = channel; - const defaultValues = { - ...defaultConfig, - ...{ - settings: { - ...defaultConfig?.settings, - psk: fromByteArray(defaultConfig?.settings?.psk ?? new Uint8Array(0)), - moduleSettings: { - ...defaultConfig?.settings?.moduleSettings, - positionPrecision: - defaultConfig?.settings?.moduleSettings?.positionPrecision === - undefined - ? 10 - : defaultConfig?.settings?.moduleSettings?.positionPrecision, - }, - }, - }, - }; - - const workingChannel = getChange({ - type: "channels", - index: channel.index, - }) as Protobuf.Channel.Channel | undefined; - const effectiveConfig = workingChannel ?? channel; - const formValues = { - ...effectiveConfig, - ...{ - settings: { - ...effectiveConfig?.settings, - psk: fromByteArray(effectiveConfig?.settings?.psk ?? new Uint8Array(0)), - moduleSettings: { - ...effectiveConfig?.settings?.moduleSettings, - positionPrecision: - effectiveConfig?.settings?.moduleSettings?.positionPrecision === - undefined - ? 10 - : effectiveConfig?.settings?.moduleSettings?.positionPrecision, - }, - }, - }, - }; - - const [preSharedDialogOpen, setPreSharedDialogOpen] = - useState(false); - const [byteCount, setBytes] = useState( - effectiveConfig?.settings?.psk.length ?? 16, - ); - const ChannelValidationSchema = useMemo(() => { - return makeChannelSchema(byteCount); - }, [byteCount]); - - const formMethods = useForm({ - mode: "onChange", - defaultValues: defaultValues as DefaultValues, - resolver: createZodResolver(ChannelValidationSchema), - shouldFocusError: false, - resetOptions: { keepDefaultValues: true }, - values: formValues as ChannelValidation, - }); - const { setValue, trigger, handleSubmit, formState } = formMethods; - - useEffect(() => { - onFormInit?.(formMethods); - }, [onFormInit, formMethods]); - - // Since byteCount is an independent state, we need to use the effective value - // from the channel config to ensure the form updates when the setting changes - const effectiveByteCount = effectiveConfig.settings?.psk.length ?? 16; - const lastEffectiveRef = useRef(effectiveByteCount); - useEffect(() => { - if (effectiveByteCount !== lastEffectiveRef.current) { - lastEffectiveRef.current = effectiveByteCount; - - setBytes(effectiveByteCount); - trigger("settings.psk"); - } - }, [effectiveByteCount, trigger]); - - const onSubmit = (data: ChannelValidation) => { - if (!formState.isReady) { - return; - } - - const payload = create(Protobuf.Channel.ChannelSchema, { - ...data, - settings: { - ...data.settings, - psk: toByteArray(data.settings.psk), - moduleSettings: create(Protobuf.Channel.ModuleSettingsSchema, { - ...data.settings.moduleSettings, - positionPrecision: data.settings.moduleSettings.positionPrecision, - }), - }, - }); - - if (deepCompareConfig(channel, payload, true)) { - removeChange({ type: "channel", index: channel.index }); - return; - } - - setChange({ type: "channel", index: channel.index }, payload, channel); - }; - - const preSharedKeyRegenerate = async () => { - const newPsk = btoa( - cryptoRandomString({ - length: byteCount ?? 16, - type: "alphanumeric", - }), - ); - setValue("settings.psk", newPsk, { shouldDirty: true }); - setPreSharedDialogOpen(false); - - const valid = await trigger("settings.psk"); - if (valid) { - handleSubmit(onSubmit)(); // manually invoke form submit - } - }; - - const selectChangeEvent = (e: string) => { - const count = Number.parseInt(e, 10); - if (!Number.isNaN(count)) { - setBytes(count); - trigger("settings.psk"); - } - }; - - return ( - <> - - propMethods={formMethods} - onSubmit={onSubmit} - fieldGroups={[ - { - label: t("settings.label"), - description: t("settings.description"), - fields: [ - { - type: "select", - name: "role", - label: t("role.label"), - disabled: channel.index === 0, - description: t("role.description"), - properties: { - enumValue: - channel.index === 0 - ? { [t("role.options.primary")]: 1 } - : { - [t("role.options.disabled")]: 0, - [t("role.options.secondary")]: 2, - }, - }, - }, - { - type: "passwordGenerator", - name: "settings.psk", - id: "channel-psk", - label: t("psk.label"), - description: t("psk.description"), - devicePSKBitCount: byteCount ?? 16, - selectChange: selectChangeEvent, - actionButtons: [ - { - text: t("psk.generate"), - variant: "success", - onClick: () => setPreSharedDialogOpen(true), - }, - ], - hide: true, - properties: { - showPasswordToggle: true, - showCopyButton: true, - }, - }, - { - type: "text", - name: "settings.name", - label: t("name.label"), - description: t("name.description"), - }, - { - type: "toggle", - name: "settings.uplinkEnabled", - label: t("uplinkEnabled.label"), - description: t("uplinkEnabled.description"), - }, - { - type: "toggle", - name: "settings.downlinkEnabled", - label: t("downlinkEnabled.label"), - description: t("downlinkEnabled.description"), - }, - { - type: "select", - name: "settings.moduleSettings.positionPrecision", - label: t("positionPrecision.label"), - description: t("positionPrecision.description"), - properties: { - enumValue: - config.display?.units === 0 - ? { - [t("positionPrecision.options.none")]: 0, - [t("positionPrecision.options.metric_km23")]: 10, - [t("positionPrecision.options.metric_km12")]: 11, - [t("positionPrecision.options.metric_km5_8")]: 12, - [t("positionPrecision.options.metric_km2_9")]: 13, - [t("positionPrecision.options.metric_km1_5")]: 14, - [t("positionPrecision.options.metric_m700")]: 15, - [t("positionPrecision.options.metric_m350")]: 16, - [t("positionPrecision.options.metric_m200")]: 17, - [t("positionPrecision.options.metric_m90")]: 18, - [t("positionPrecision.options.metric_m50")]: 19, - [t("positionPrecision.options.precise")]: 32, - } - : { - [t("positionPrecision.options.none")]: 0, - [t("positionPrecision.options.imperial_mi15")]: 10, - [t("positionPrecision.options.imperial_mi7_3")]: 11, - [t("positionPrecision.options.imperial_mi3_6")]: 12, - [t("positionPrecision.options.imperial_mi1_8")]: 13, - [t("positionPrecision.options.imperial_mi0_9")]: 14, - [t("positionPrecision.options.imperial_mi0_5")]: 15, - [t("positionPrecision.options.imperial_mi0_2")]: 16, - [t("positionPrecision.options.imperial_ft600")]: 17, - [t("positionPrecision.options.imperial_ft300")]: 18, - [t("positionPrecision.options.imperial_ft150")]: 19, - [t("positionPrecision.options.precise")]: 32, - }, - }, - }, - ], - }, - ]} - /> - setPreSharedDialogOpen(false)} - onSubmit={() => preSharedKeyRegenerate()} - /> - - ); -}; diff --git a/packages/web/src/components/PageComponents/Channels/Channels.tsx b/packages/web/src/components/PageComponents/Channels/Channels.tsx deleted file mode 100644 index ad8edfd1b..000000000 --- a/packages/web/src/components/PageComponents/Channels/Channels.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Channel } from "@app/components/PageComponents/Channels/Channel"; -import { Button } from "@components/UI/Button.tsx"; -import { Spinner } from "@components/UI/Spinner.tsx"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@components/UI/Tabs.tsx"; -import { useDevice } from "@core/stores"; -import type { Protobuf } from "@meshtastic/core"; -import i18next from "i18next"; -import { QrCodeIcon, UploadIcon } from "lucide-react"; -import { Suspense, useMemo } from "react"; -import type { UseFormReturn } from "react-hook-form"; -import { useTranslation } from "react-i18next"; - -interface ConfigProps { - onFormInit: (methods: UseFormReturn) => void; -} - -export const getChannelName = (channel: Protobuf.Channel.Channel) => { - return channel.settings?.name.length - ? channel.settings?.name - : channel.index === 0 - ? i18next.t("page.broadcastLabel") - : i18next.t("page.channelIndex", { - ns: "channels", - index: channel.index, - }); -}; - -export const Channels = ({ onFormInit }: ConfigProps) => { - const { channels, hasChannelChange, setDialogOpen } = useDevice(); - const { t } = useTranslation("channels"); - - const allChannels = Array.from(channels.values()); - const flags = useMemo( - () => - new Map( - allChannels.map((channel) => [ - channel.index, - hasChannelChange(channel.index), - ]), - ), - [allChannels, hasChannelChange], - ); - - return ( - - - {allChannels.map((channel) => ( - - {getChannelName(channel)} - {flags.get(channel.index) && ( - - - - - )} - - ))} - - - - {allChannels.map((channel) => ( - - }> - - - - ))} - - ); -}; diff --git a/packages/web/src/components/PageComponents/Map/Popups/NodeDetail.tsx b/packages/web/src/components/PageComponents/Map/Popups/NodeDetail.tsx deleted file mode 100644 index 35f016cf8..000000000 --- a/packages/web/src/components/PageComponents/Map/Popups/NodeDetail.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import BatteryStatus from "@components/BatteryStatus.tsx"; -import { Mono } from "@components/generic/Mono.tsx"; -import { TimeAgo } from "@components/generic/TimeAgo.tsx"; -import { Avatar } from "@components/UI/Avatar.tsx"; -import { Separator } from "@components/UI/Separator.tsx"; -import { Heading } from "@components/UI/Typography/Heading.tsx"; -import { Subtle } from "@components/UI/Typography/Subtle.tsx"; -import { formatQuantity } from "@core/utils/string.ts"; -import type { Protobuf as ProtobufType } from "@meshtastic/core"; -import { Protobuf } from "@meshtastic/core"; -import { - Tooltip, - TooltipContent, - TooltipPortal, - TooltipProvider, - TooltipTrigger, -} from "@radix-ui/react-tooltip"; -import { useNavigate } from "@tanstack/react-router"; -import { - Dot, - LockIcon, - LockOpenIcon, - MessageSquareIcon, - MountainSnow, - Star, -} from "lucide-react"; -import { useTranslation } from "react-i18next"; - -export interface NodeDetailProps { - node: ProtobufType.Mesh.NodeInfo; -} - -export const NodeDetail = ({ node }: NodeDetailProps) => { - const navigate = useNavigate(); - const { t } = useTranslation("nodes"); - const name = node.user?.longName ?? t("unknown.shortName"); - const shortName = node.user?.shortName ?? t("unknown.shortName"); - const hwModel = node.user?.hwModel ?? 0; - const rawHardwareType = Protobuf.Mesh.HardwareModel[hwModel] as - | keyof typeof Protobuf.Mesh.HardwareModel - | undefined; - const hardwareType = rawHardwareType - ? rawHardwareType === "UNSET" - ? t("unset") - : rawHardwareType.replaceAll("_", " ") - : `${hwModel}`; - function handleDirectMessage() { - navigate({ to: `/messages/direct/${node.num}` }); - } - - return ( -
-
-
- - -
{ - // Required to prevent DM tooltip auto-appearing on creation - e.stopPropagation(); - }} - > - {node.user?.publicKey && node.user?.publicKey.length > 0 ? ( - - ) : ( - - )} - - - - - - - - - {t("nodeDetail.directMessage.label", { - shortName, - })} - - - - - - -
-
- -
- {name} - {hardwareType !== t("unset") && {hardwareType}} - - {!!node.deviceMetrics?.batteryLevel && ( - - )} - -
- {node.user?.shortName &&
"{node.user?.shortName}"
} - {node.user?.id &&
{node.user?.id}
} -
- -
-
- {node.lastHeard > 0 && ( -
- {t("nodeDetail.status.heard")}{" "} - -
- )} -
- {node.viaMqtt && ( -
- {t("nodeDetail.status.mqtt")} -
- )} -
-
-
- - - -
-
-
- {Number.isNaN(node.hopsAway) - ? t("unit.hopsAway.unknown") - : node.hopsAway} -
-
- {node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")} -
-
- {node.position?.altitude && ( -
- -
- {formatQuantity(node.position?.altitude, { - one: t("unit.meter.one"), - other: t("unit.meter.plural"), - })} -
-
- )} -
- -
- {!!node.deviceMetrics?.channelUtilization && ( -
-
{t("channelUtilization.short")}
- - {node.deviceMetrics?.channelUtilization.toPrecision(3)}% - -
- )} - {!!node.deviceMetrics?.airUtilTx && ( -
-
{t("airtimeUtilization.short")}
- - {node.deviceMetrics?.airUtilTx.toPrecision(3)}% - -
- )} -
- - {node.snr !== 0 && ( -
-
{t("unit.snr")}
- - {node.snr} - {t("unit.dbm")} - - {Math.min(Math.max((node.snr + 10) * 5, 0), 100)}% - - {(node.snr + 10) * 5} - {t("unit.raw")} - -
- )} -
- ); -}; diff --git a/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx b/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx deleted file mode 100644 index f44cb626d..000000000 --- a/packages/web/src/components/PageComponents/Messages/ChannelChat.tsx +++ /dev/null @@ -1,164 +0,0 @@ -import { MessageItem } from "@components/PageComponents/Messages/MessageItem.tsx"; -import { Separator } from "@components/UI/Separator"; -import { Skeleton } from "@components/UI/Skeleton.tsx"; -import type { Message } from "@core/stores/messageStore/types.ts"; -import type { TFunction } from "i18next"; -import { InboxIcon } from "lucide-react"; -import { Fragment, Suspense, useMemo } from "react"; -import { useTranslation } from "react-i18next"; - -export interface ChannelChatProps { - messages?: Message[]; -} - -function toTs(d: Message["date"]): number { - return typeof d === "number" ? d : Date.parse(String(d)); -} - -function startOfLocalDay(ts: number): number { - const d = new Date(ts); - d.setHours(0, 0, 0, 0); - return d.getTime(); -} - -function formatDateLabelFromDayKey( - dayKey: number, - t: TFunction<"common", undefined>, - fmt: Intl.DateTimeFormat, -): string { - const todayKey = startOfLocalDay(Date.now()); - const yestKey = todayKey - 24 * 60 * 60 * 1000; - - if (dayKey === todayKey) { - return t("unit.day.today"); // "Today" from common.json - } - if (dayKey === yestKey) { - return t("unit.day.yesterday"); // "Yesterday" from common.json - } - return fmt.format(new Date(dayKey)); -} - -type DayGroup = { dayKey: number; label: string; items: Message[] }; - -function groupMessagesByDay( - messages: Message[], - t: TFunction<"common", undefined>, - fmt: Intl.DateTimeFormat, -): DayGroup[] { - const out: DayGroup[] = []; - - for (const msg of messages) { - const ts = toTs(msg.date); - const dayKey = startOfLocalDay(ts); - const last = out[out.length - 1]; - if (last && last.dayKey === dayKey) { - last.items.push(msg); - } else { - out.push({ - dayKey, - label: formatDateLabelFromDayKey(dayKey, t, fmt), - items: [msg], - }); - } - } - return out; -} - -const DateDelimiter = ({ label }: { label: string }) => ( -
  • -
    - -
    - {label} -
    - -
    -
  • -); - -const MessageSkeleton = () => { - console.log("[ChannelChat] Showing MessageSkeleton (Suspense fallback)"); - return ( -
  • -
    - -
    -
    - - -
    - -
    -
    -
  • - ); -}; - -const EmptyState = () => { - const { t } = useTranslation("messages"); - return ( -
    - - {t("emptyState.text")} -
    - ); -}; - -export const ChannelChat = ({ messages = [] }: ChannelChatProps) => { - const { i18n, t } = useTranslation(); - - const locale = useMemo( - () => - i18n.language || - (typeof navigator !== "undefined" ? navigator.language : "en-US"), - [i18n.language], - ); - - const dayLabelFmt = useMemo( - () => - new Intl.DateTimeFormat(locale, { - year: "numeric", - month: "long", - day: "numeric", - }), - [locale], - ); - - // Sort messages by date in case they are stored out of order - const sorted = useMemo( - () => [...messages].sort((a, b) => toTs(b.date) - toTs(a.date)), - [messages], - ); - - const groups = useMemo( - () => groupMessagesByDay(sorted, t, dayLabelFmt), - [sorted, dayLabelFmt, t], - ); - - if (!messages.length) { - return ( -
    - -
    - ); - } - - return ( -
      - {groups.map(({ dayKey, label, items }) => ( - - {/* Render messages first, then delimiter — with flex-col-reverse this shows the delimiter above that day's messages */} - {items.map((message) => ( - } - > - - - ))} - - - ))} -
    - ); -}; diff --git a/packages/web/src/components/PageComponents/Messages/MessageInput.test.tsx b/packages/web/src/components/PageComponents/Messages/MessageInput.test.tsx deleted file mode 100644 index f22c01305..000000000 --- a/packages/web/src/components/PageComponents/Messages/MessageInput.test.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import type { Types } from "@meshtastic/core"; -import { - act, - fireEvent, - render, - screen, - waitFor, -} from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { MessageInput, type MessageInputProps } from "./MessageInput.tsx"; - -vi.mock("@components/UI/Button.tsx", () => ({ - Button: vi.fn(({ type, className, children, onClick, onSubmit, ...rest }) => ( - - )), -})); - -vi.mock("@components/UI/Input.tsx", () => ({ - Input: vi.fn(({ minLength, name, placeholder, value, onChange }) => ( - - )), -})); - -const mockSetDraft = vi.fn(); -const mockGetDraft = vi.fn(); -const mockClearDraft = vi.fn(); - -vi.mock("@core/stores", () => ({ - CurrentDeviceContext: { - _currentValue: { deviceId: 1234 }, - }, - useMessages: vi.fn(() => ({ - setDraft: mockSetDraft, - getDraft: mockGetDraft, - clearDraft: mockClearDraft, - })), - MessageState: { - Ack: "ack", - Waiting: "waiting", - Failed: "failed", - }, - MessageType: { - Direct: "direct", - Broadcast: "broadcast", - }, -})); - -vi.mock("lucide-react", () => ({ - SendIcon: vi.fn(() => ), -})); - -describe("MessageInput", () => { - const mockOnSend = vi.fn(); - const defaultProps: MessageInputProps = { - onSend: mockOnSend, - to: 123, - maxBytes: 256, - }; - - beforeEach(() => { - vi.clearAllMocks(); - - mockGetDraft.mockReturnValue(""); - }); - - const renderComponent = (props: Partial = {}) => { - render(); - }; - - it("should render the input field, byte counter, and send button", () => { - renderComponent(); - expect(screen.getByTestId("message-input-field")).toBeInTheDocument(); - expect(screen.getByTestId("byte-counter")).toBeInTheDocument(); - expect(screen.getByRole("button")).toBeInTheDocument(); - expect(screen.getByTestId("send-icon")).toBeInTheDocument(); - }); - - it("should initialize with the draft from the store", () => { - const initialDraft = "Existing draft message"; - mockGetDraft.mockImplementation((key) => { - return key === defaultProps.to ? initialDraft : ""; - }); - - renderComponent(); - - expect(mockGetDraft).toHaveBeenCalledWith(defaultProps.to); - const expectedBytes = new Blob([initialDraft]).size; - expect(screen.getByTestId("byte-counter")).toHaveTextContent( - `${expectedBytes}/${defaultProps.maxBytes}`, - ); - }); - - it("should update input value, byte counter, and call setDraft on change within limits", () => { - renderComponent(); - const inputElement = screen.getByTestId("message-input-field"); - const testMessage = "Hello there!"; - const expectedBytes = new Blob([testMessage]).size; - - fireEvent.change(inputElement, { target: { value: testMessage } }); - - expect((inputElement as HTMLInputElement).value).toBe(testMessage); - expect(screen.getByTestId("byte-counter")).toHaveTextContent( - `${expectedBytes}/${defaultProps.maxBytes}`, - ); - expect(mockSetDraft).toHaveBeenCalledTimes(1); - expect(mockSetDraft).toHaveBeenCalledWith(defaultProps.to, testMessage); - }); - - it("should NOT update input value or call setDraft if maxBytes is exceeded", () => { - const smallMaxBytes = 5; - renderComponent({ maxBytes: smallMaxBytes }); - const inputElement = screen.getByTestId("message-input-field"); - const initialValue = "12345"; - const excessiveValue = "123456"; - - fireEvent.change(inputElement, { target: { value: initialValue } }); - expect((inputElement as HTMLInputElement).value).toBe(initialValue); - expect(mockSetDraft).toHaveBeenCalledWith(defaultProps.to, initialValue); - mockSetDraft.mockClear(); - - fireEvent.change(inputElement, { target: { value: excessiveValue } }); - - expect((inputElement as HTMLInputElement).value).toBe(initialValue); - expect(screen.getByTestId("byte-counter")).toHaveTextContent( - `${smallMaxBytes}/${smallMaxBytes}`, - ); - expect(mockSetDraft).not.toHaveBeenCalled(); - }); - - it("should call onSend, clear input, reset byte counter, and call clearDraft on valid submit", async () => { - renderComponent(); - const inputElement = screen.getByTestId("message-input-field"); - const formElement = screen.getByRole("form"); - const testMessage = "Send this message"; - - fireEvent.change(inputElement, { target: { value: testMessage } }); - fireEvent.submit(formElement); - - await waitFor(() => { - expect(mockOnSend).toHaveBeenCalledTimes(1); - expect(mockOnSend).toHaveBeenCalledWith(testMessage); - expect((inputElement as HTMLInputElement).value).toBe(""); - expect(screen.getByTestId("byte-counter")).toHaveTextContent( - `0/${defaultProps.maxBytes}`, - ); - expect(mockClearDraft).toHaveBeenCalledTimes(1); - expect(mockClearDraft).toHaveBeenCalledWith(defaultProps.to); - }); - }); - - it("should trim whitespace before calling onSend", async () => { - renderComponent(); - const inputElement = screen.getByTestId("message-input-field"); - const formElement = screen.getByRole("form"); - const testMessageWithWhitespace = " Trim me! "; - const expectedTrimmedMessage = "Trim me!"; - - fireEvent.change(inputElement, { - target: { value: testMessageWithWhitespace }, - }); - fireEvent.submit(formElement); - - await waitFor(() => { - expect(mockOnSend).toHaveBeenCalledTimes(1); - expect(mockOnSend).toHaveBeenCalledWith(expectedTrimmedMessage); - expect(mockClearDraft).toHaveBeenCalledWith(defaultProps.to); - }); - }); - - it("should not call onSend or clearDraft if input is empty on submit", async () => { - renderComponent(); - const inputElement = screen.getByTestId("message-input-field"); - const formElement = screen.getByRole("form"); - - expect((inputElement as HTMLInputElement).value).toBe(""); - - fireEvent.submit(formElement); - - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 50)); - }); - - expect(mockOnSend).not.toHaveBeenCalled(); - expect(mockClearDraft).not.toHaveBeenCalled(); - }); - - it("should not call onSend or clearDraft if input contains only whitespace on submit", async () => { - renderComponent(); - const inputElement = screen.getByTestId("message-input-field"); - const formElement = screen.getByRole("form"); - const whitespaceMessage = " \t "; - - fireEvent.change(inputElement, { target: { value: whitespaceMessage } }); - expect((inputElement as HTMLInputElement).value).toBe(whitespaceMessage); - - fireEvent.submit(formElement); - - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 50)); - }); - - expect(mockOnSend).not.toHaveBeenCalled(); - expect(mockClearDraft).not.toHaveBeenCalled(); - - expect((inputElement as HTMLInputElement).value).toBe(whitespaceMessage); - }); - - it("should work with broadcast destination for drafts", () => { - const broadcastDest: Types.Destination = "broadcast"; - mockGetDraft.mockImplementation((key) => - key === broadcastDest ? "Broadcast draft" : "", - ); - - renderComponent({ to: broadcastDest }); - - expect(mockGetDraft).toHaveBeenCalledWith(broadcastDest); - expect( - (screen.getByTestId("message-input-field") as HTMLInputElement).value, - ).toBe("Broadcast draft"); - - const inputElement = screen.getByTestId( - "message-input-field", - ) as HTMLInputElement; - const formElement = screen.getByRole("form"); - const newMessage = "New broadcast msg"; - - fireEvent.change(inputElement, { target: { value: newMessage } }); - expect(mockSetDraft).toHaveBeenCalledWith(broadcastDest, newMessage); - - fireEvent.submit(formElement); - - expect(mockOnSend).toHaveBeenCalledWith(newMessage); - expect(mockClearDraft).toHaveBeenCalledWith(broadcastDest); - }); -}); diff --git a/packages/web/src/components/PageComponents/Messages/MessageInput.tsx b/packages/web/src/components/PageComponents/Messages/MessageInput.tsx deleted file mode 100644 index 4a78d0683..000000000 --- a/packages/web/src/components/PageComponents/Messages/MessageInput.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Button } from "@components/UI/Button.tsx"; -import { Input } from "@components/UI/Input.tsx"; -import { useMessages } from "@core/stores"; -import type { Types } from "@meshtastic/core"; -import { SendIcon } from "lucide-react"; -import { startTransition, useState } from "react"; -import { useTranslation } from "react-i18next"; - -export interface MessageInputProps { - onSend: (message: string) => void; - to: Types.Destination; - maxBytes: number; -} - -export const MessageInput = ({ onSend, to, maxBytes }: MessageInputProps) => { - const { setDraft, getDraft, clearDraft } = useMessages(); - const { t } = useTranslation("messages"); - - const calculateBytes = (text: string) => new Blob([text]).size; - - const initialDraft = getDraft(to); - const [localDraft, setLocalDraft] = useState(initialDraft); - const [messageBytes, setMessageBytes] = useState(() => - calculateBytes(initialDraft), - ); - - const handleInputChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - const byteLength = calculateBytes(newValue); - - if (byteLength <= maxBytes) { - setLocalDraft(newValue); - setMessageBytes(byteLength); - setDraft(to, newValue); - } - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!localDraft.trim()) { - return; - } - // Reset bytes *before* sending (consider if onSend failure needs different handling) - setMessageBytes(0); - - startTransition(() => { - onSend(localDraft.trim()); - setLocalDraft(""); - clearDraft(to); - }); - }; - - return ( -
    -
    -
    - - - - - -
    -
    -
    - ); -}; diff --git a/packages/web/src/components/PageComponents/Messages/MessageItem.tsx b/packages/web/src/components/PageComponents/Messages/MessageItem.tsx deleted file mode 100644 index 54e589e38..000000000 --- a/packages/web/src/components/PageComponents/Messages/MessageItem.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { Avatar } from "@components/UI/Avatar.tsx"; -import { - Tooltip, - TooltipArrow, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "@components/UI/Tooltip.tsx"; -import { MessageState, useAppStore, useDevice, useNodeDB } from "@core/stores"; -import type { Message } from "@core/stores/messageStore/types.ts"; -import { cn } from "@core/utils/cn.ts"; -import { type Protobuf, Types } from "@meshtastic/core"; -import type { LucideIcon } from "lucide-react"; -import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react"; -import { type ReactNode, useMemo } from "react"; -import { useTranslation } from "react-i18next"; - -// Cache for pending promises -const myNodePromises = new Map>(); - -// Hook that suspends when myNode is not available -function useSuspendingMyNode() { - const { getMyNode } = useNodeDB(); - const selectedDeviceId = useAppStore((s) => s.selectedDeviceId); - const myNode = getMyNode(); - - if (!myNode) { - // Use the selected device ID to cache promises per device - const deviceKey = `device-${selectedDeviceId}`; - - if (!myNodePromises.has(deviceKey)) { - const promise = new Promise((resolve) => { - // Poll for myNode to become available - const checkInterval = setInterval(() => { - const node = getMyNode(); - if (node) { - console.log( - "[MessageItem] myNode now available, resolving promise", - ); - clearInterval(checkInterval); - myNodePromises.delete(deviceKey); - resolve(node); - } - }, 100); - - setTimeout(() => { - clearInterval(checkInterval); - myNodePromises.delete(deviceKey); - }, 10000); - }); - - myNodePromises.set(deviceKey, promise); - } - - // Throw the promise to trigger Suspense - throw myNodePromises.get(deviceKey); - } - - return myNode; -} - -// import { MessageActionsMenu } from "@components/PageComponents/Messages/MessageActionsMenu.tsx"; // TODO: Uncomment when actions menu is implemented - -interface MessageStatusInfo { - displayText: string; - icon: LucideIcon; - ariaLabel: string; - iconClassName?: string; -} - -const StatusTooltip = ({ - statusInfo, - children, -}: { - statusInfo: MessageStatusInfo; - children: ReactNode; -}) => ( - - - {children} - - {statusInfo.displayText} - - - - -); - -interface MessageItemProps { - message: Message; -} - -export const MessageItem = ({ message }: MessageItemProps) => { - const { config } = useDevice(); - const { getNode } = useNodeDB(); - const { t, i18n } = useTranslation("messages"); - - // This will suspend if myNode is not available yet - const myNode = useSuspendingMyNode(); - const myNodeNum = myNode.num; - - const MESSAGE_STATUS_MAP = useMemo( - (): Record => ({ - [MessageState.Ack]: { - displayText: t("deliveryStatus.delivered.displayText"), - icon: CheckCircle2, - ariaLabel: t("deliveryStatus.delivered.label"), - iconClassName: "text-green-500", - }, - [MessageState.Waiting]: { - displayText: t("deliveryStatus.waiting.displayText"), - icon: CircleEllipsis, - ariaLabel: t("deliveryStatus.waiting.label"), - iconClassName: "text-slate-400", - }, - [MessageState.Failed]: { - displayText: t("deliveryStatus.failed.displayText"), - icon: AlertCircle, - ariaLabel: t("deliveryStatus.failed.label"), - iconClassName: "text-red-500 dark:text-red-400", - }, - }), - [t], - ); - - const UNKNOWN_STATUS = useMemo( - (): MessageStatusInfo => ({ - displayText: t("deliveryStatus.unknown.displayText"), - icon: AlertCircle, - ariaLabel: t("deliveryStatus.unknown.label"), - iconClassName: "text-red-500 dark:text-red-400", - }), - [t], - ); - - const getMessageStatusInfo = useMemo( - () => - (state: MessageState): MessageStatusInfo => - MESSAGE_STATUS_MAP[state] ?? UNKNOWN_STATUS, - [MESSAGE_STATUS_MAP, UNKNOWN_STATUS], - ); - - const messageUser: Protobuf.Mesh.NodeInfo | null | undefined = useMemo(() => { - return message.from != null ? getNode(message.from) : null; - }, [getNode, message.from]); - - const { displayName, isFavorite, nodeNum } = useMemo(() => { - const userIdHex = message.from.toString(16).toUpperCase().padStart(2, "0"); - const last4 = userIdHex.slice(-4); - const fallbackName = t("fallbackName", { last4 }); - const longName = messageUser?.user?.longName; - const derivedShortName = messageUser?.user?.shortName || fallbackName; - const derivedDisplayName = longName || derivedShortName; - const isFavorite = - messageUser?.num !== myNodeNum && messageUser?.isFavorite; - return { - displayName: derivedDisplayName, - shortName: derivedShortName, - isFavorite: isFavorite, - nodeNum: message.from, - }; - }, [messageUser, message.from, t, myNodeNum]); - - const messageStatusInfo = getMessageStatusInfo(message.state); - const StatusIconComponent = messageStatusInfo.icon; - - const messageDate = useMemo( - () => (message.date ? new Date(message.date) : null), - [message.date], - ); - const locale = i18n.language; - - const formattedTime = useMemo( - () => - messageDate?.toLocaleTimeString(locale, { - hour: "numeric", - minute: "2-digit", - hour12: config?.display?.use12hClock ?? true, - }) ?? "", - [messageDate, locale, config?.display?.use12hClock], - ); - - const fullDateTime = useMemo( - () => - messageDate?.toLocaleString(locale, { - dateStyle: "medium", - timeStyle: "short", - }) ?? "", - [messageDate, locale], - ); - - const isSender = myNodeNum !== undefined && message.from === myNodeNum; - const isOnPrimaryChannel = message.channel === Types.ChannelNumber.Primary; // Use the enum - const shouldShowStatusIcon = isSender && isOnPrimaryChannel; - - const messageItemWrapperClass = cn( - "group w-full py-2 relative list-none", - "rounded-md", - "hover:bg-slate-300/15 dark:hover:bg-slate-600/20", - "transition-colors duration-100 ease-in-out", - ); - const dateTextStyle = "text-xs text-slate-500 dark:text-slate-400"; - - return ( -
  • -
    - - -
    -
    - - {displayName} - - {message.viaMqtt && ( - - - - - ☁️ - - - - MQTT - - - - - )} - {messageDate && ( - - )} - {shouldShowStatusIcon && ( - - - - - )} -
    - - {message?.message && ( -
    - {message.message} -
    - )} - {(message.hops && ( -
    - {t("hops.text", { value: message.hops })} -
    - )) || - (message.rxSnr && message.rxRssi && ( -
    - SNR: {message.rxSnr}, RSSI: {message.rxRssi} -
    - ))} -
    -
    - {/* Actions Menu Placeholder */} - {/*
    - console.log("Reply")} /> -
    */} -
  • - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/AmbientLighting.tsx b/packages/web/src/components/PageComponents/ModuleConfig/AmbientLighting.tsx deleted file mode 100644 index 108b3796e..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/AmbientLighting.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type AmbientLightingValidation, - AmbientLightingValidationSchema, -} from "@app/validation/moduleConfig/ambientLighting.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { useTranslation } from "react-i18next"; - -interface AmbientLightingModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const AmbientLighting = ({ - onFormInit, -}: AmbientLightingModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "ambientLighting" }); - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: AmbientLightingValidation) => { - if (deepCompareConfig(moduleConfig.ambientLighting, data, true)) { - removeChange({ type: "moduleConfig", variant: "ambientLighting" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "ambientLighting" }, - data, - moduleConfig.ambientLighting, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={AmbientLightingValidationSchema} - defaultValues={moduleConfig.ambientLighting} - values={getEffectiveModuleConfig("ambientLighting")} - fieldGroups={[ - { - label: t("ambientLighting.title"), - description: t("ambientLighting.description"), - fields: [ - { - type: "toggle", - name: "ledState", - label: t("ambientLighting.ledState.label"), - description: t("ambientLighting.ledState.description"), - }, - { - type: "number", - name: "current", - label: t("ambientLighting.current.label"), - description: t("ambientLighting.current.description"), - }, - { - type: "number", - name: "red", - label: t("ambientLighting.red.label"), - description: t("ambientLighting.red.description"), - }, - { - type: "number", - name: "green", - label: t("ambientLighting.green.label"), - description: t("ambientLighting.green.description"), - }, - { - type: "number", - name: "blue", - label: t("ambientLighting.blue.label"), - description: t("ambientLighting.blue.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/Audio.tsx b/packages/web/src/components/PageComponents/ModuleConfig/Audio.tsx deleted file mode 100644 index 067383236..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/Audio.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type AudioValidation, - AudioValidationSchema, -} from "@app/validation/moduleConfig/audio.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface AudioModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const Audio = ({ onFormInit }: AudioModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "audio" }); - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: AudioValidation) => { - if (deepCompareConfig(moduleConfig.audio, data, true)) { - removeChange({ type: "moduleConfig", variant: "audio" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "audio" }, - data, - moduleConfig.audio, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={AudioValidationSchema} - defaultValues={moduleConfig.audio} - values={getEffectiveModuleConfig("audio")} - fieldGroups={[ - { - label: t("audio.title"), - description: t("audio.description"), - fields: [ - { - type: "toggle", - name: "codec2Enabled", - label: t("audio.codec2Enabled.label"), - description: t("audio.codec2Enabled.description"), - }, - { - type: "number", - name: "pttPin", - label: t("audio.pttPin.label"), - description: t("audio.pttPin.description"), - }, - { - type: "select", - name: "bitrate", - label: t("audio.bitrate.label"), - description: t("audio.bitrate.description"), - properties: { - enumValue: - Protobuf.ModuleConfig.ModuleConfig_AudioConfig_Audio_Baud, - }, - }, - { - type: "number", - name: "i2sWs", - label: t("audio.i2sWs.label"), - description: t("audio.i2sWs.description"), - }, - { - type: "number", - name: "i2sSd", - label: t("audio.i2sSd.label"), - description: t("audio.i2sSd.description"), - }, - { - type: "number", - name: "i2sDin", - label: t("audio.i2sDin.label"), - description: t("audio.i2sDin.description"), - }, - { - type: "number", - name: "i2sSck", - label: t("audio.i2sSck.label"), - description: t("audio.i2sSck.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/CannedMessage.tsx b/packages/web/src/components/PageComponents/ModuleConfig/CannedMessage.tsx deleted file mode 100644 index 402fbdfe1..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/CannedMessage.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type CannedMessageValidation, - CannedMessageValidationSchema, -} from "@app/validation/moduleConfig/cannedMessage.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface CannedMessageModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const CannedMessage = ({ - onFormInit, -}: CannedMessageModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "cannedMessage" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: CannedMessageValidation) => { - if (deepCompareConfig(moduleConfig.cannedMessage, data, true)) { - removeChange({ type: "moduleConfig", variant: "cannedMessage" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "cannedMessage" }, - data, - moduleConfig.cannedMessage, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={CannedMessageValidationSchema} - defaultValues={moduleConfig.cannedMessage} - values={getEffectiveModuleConfig("cannedMessage")} - fieldGroups={[ - { - label: t("cannedMessage.title"), - description: t("cannedMessage.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("cannedMessage.moduleEnabled.label"), - description: t("cannedMessage.moduleEnabled.description"), - }, - { - type: "toggle", - name: "rotary1Enabled", - label: t("cannedMessage.rotary1Enabled.label"), - description: t("cannedMessage.rotary1Enabled.description"), - }, - { - type: "number", - name: "inputbrokerPinA", - label: t("cannedMessage.inputbrokerPinA.label"), - description: t("cannedMessage.inputbrokerPinA.description"), - }, - { - type: "number", - name: "inputbrokerPinB", - label: t("cannedMessage.inputbrokerPinB.label"), - description: t("cannedMessage.inputbrokerPinB.description"), - }, - { - type: "number", - name: "inputbrokerPinPress", - label: t("cannedMessage.inputbrokerPinPress.label"), - description: t("cannedMessage.inputbrokerPinPress.description"), - }, - { - type: "select", - name: "inputbrokerEventCw", - label: t("cannedMessage.inputbrokerEventCw.label"), - description: t("cannedMessage.inputbrokerEventCw.description"), - properties: { - enumValue: - Protobuf.ModuleConfig - .ModuleConfig_CannedMessageConfig_InputEventChar, - }, - }, - { - type: "select", - name: "inputbrokerEventCcw", - label: t("cannedMessage.inputbrokerEventCcw.label"), - description: t("cannedMessage.inputbrokerEventCcw.description"), - properties: { - enumValue: - Protobuf.ModuleConfig - .ModuleConfig_CannedMessageConfig_InputEventChar, - }, - }, - { - type: "select", - name: "inputbrokerEventPress", - label: t("cannedMessage.inputbrokerEventPress.label"), - description: t("cannedMessage.inputbrokerEventPress.description"), - properties: { - enumValue: - Protobuf.ModuleConfig - .ModuleConfig_CannedMessageConfig_InputEventChar, - }, - }, - { - type: "toggle", - name: "updown1Enabled", - label: t("cannedMessage.updown1Enabled.label"), - description: t("cannedMessage.updown1Enabled.description"), - }, - { - type: "text", - name: "allowInputSource", - label: t("cannedMessage.allowInputSource.label"), - description: t("cannedMessage.allowInputSource.description"), - }, - { - type: "toggle", - name: "sendBell", - label: t("cannedMessage.sendBell.label"), - description: t("cannedMessage.sendBell.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/DetectionSensor.tsx b/packages/web/src/components/PageComponents/ModuleConfig/DetectionSensor.tsx deleted file mode 100644 index 79d188479..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/DetectionSensor.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type DetectionSensorValidation, - DetectionSensorValidationSchema, -} from "@app/validation/moduleConfig/detectionSensor.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface DetectionSensorModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const DetectionSensor = ({ - onFormInit, -}: DetectionSensorModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "detectionSensor" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: DetectionSensorValidation) => { - if (deepCompareConfig(moduleConfig.detectionSensor, data, true)) { - removeChange({ type: "moduleConfig", variant: "detectionSensor" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "detectionSensor" }, - data, - moduleConfig.detectionSensor, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={DetectionSensorValidationSchema} - defaultValues={moduleConfig.detectionSensor} - values={getEffectiveModuleConfig("detectionSensor")} - fieldGroups={[ - { - label: t("detectionSensor.title"), - description: t("detectionSensor.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("detectionSensor.enabled.label"), - description: t("detectionSensor.enabled.description"), - }, - { - type: "number", - name: "minimumBroadcastSecs", - label: t("detectionSensor.minimumBroadcastSecs.label"), - description: t( - "detectionSensor.minimumBroadcastSecs.description", - ), - properties: { - suffix: t("unit.second.plural"), - }, - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "stateBroadcastSecs", - label: t("detectionSensor.stateBroadcastSecs.label"), - description: t("detectionSensor.stateBroadcastSecs.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "sendBell", - label: t("detectionSensor.sendBell.label"), - description: t("detectionSensor.sendBell.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "text", - name: "name", - label: t("detectionSensor.name.label"), - description: t("detectionSensor.name.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "monitorPin", - label: t("detectionSensor.monitorPin.label"), - description: t("detectionSensor.monitorPin.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "select", - name: "detectionTriggerType", - label: t("detectionSensor.detectionTriggerType.label"), - description: t( - "detectionSensor.detectionTriggerType.description", - ), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - properties: { - enumValue: - Protobuf.ModuleConfig - .ModuleConfig_DetectionSensorConfig_TriggerType, - }, - }, - { - type: "toggle", - name: "usePullup", - label: t("detectionSensor.usePullup.label"), - description: t("detectionSensor.usePullup.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx b/packages/web/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx deleted file mode 100644 index e316ac78a..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/ExternalNotification.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type ExternalNotificationValidation, - ExternalNotificationValidationSchema, -} from "@app/validation/moduleConfig/externalNotification.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { useTranslation } from "react-i18next"; - -interface ExternalNotificationModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const ExternalNotification = ({ - onFormInit, -}: ExternalNotificationModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "externalNotification" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: ExternalNotificationValidation) => { - if (deepCompareConfig(moduleConfig.externalNotification, data, true)) { - removeChange({ type: "moduleConfig", variant: "externalNotification" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "externalNotification" }, - data, - moduleConfig.externalNotification, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={ExternalNotificationValidationSchema} - defaultValues={moduleConfig.externalNotification} - values={getEffectiveModuleConfig("externalNotification")} - fieldGroups={[ - { - label: t("externalNotification.title"), - description: t("externalNotification.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("externalNotification.enabled.label"), - description: t("externalNotification.enabled.description"), - }, - { - type: "number", - name: "outputMs", - label: t("externalNotification.outputMs.label"), - description: t("externalNotification.outputMs.description"), - - disabledBy: [ - { - fieldName: "enabled", - }, - ], - properties: { - suffix: t("unit.millisecond.suffix"), - }, - }, - { - type: "number", - name: "output", - label: t("externalNotification.output.label"), - description: t("externalNotification.output.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "outputVibra", - label: t("externalNotification.outputVibra.label"), - description: t("externalNotification.outputVibra.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "outputBuzzer", - label: t("externalNotification.outputBuzzer.label"), - description: t("externalNotification.outputBuzzer.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "active", - label: t("externalNotification.active.label"), - description: t("externalNotification.active.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "alertMessage", - label: t("externalNotification.alertMessage.label"), - description: t("externalNotification.alertMessage.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "alertMessageVibra", - label: t("externalNotification.alertMessageVibra.label"), - description: t( - "externalNotification.alertMessageVibra.description", - ), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "alertMessageBuzzer", - label: t("externalNotification.alertMessageBuzzer.label"), - description: t( - "externalNotification.alertMessageBuzzer.description", - ), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "alertBell", - label: t("externalNotification.alertBell.label"), - description: t("externalNotification.alertBell.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "alertBellVibra", - label: t("externalNotification.alertBellVibra.label"), - description: t("externalNotification.alertBellVibra.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "alertBellBuzzer", - label: t("externalNotification.alertBellBuzzer.label"), - description: t( - "externalNotification.alertBellBuzzer.description", - ), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "usePwm", - label: t("externalNotification.usePwm.label"), - description: t("externalNotification.usePwm.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "nagTimeout", - label: t("externalNotification.nagTimeout.label"), - description: t("externalNotification.nagTimeout.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "useI2sAsBuzzer", - label: t("externalNotification.useI2sAsBuzzer.label"), - description: t("externalNotification.useI2sAsBuzzer.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/MQTT.tsx b/packages/web/src/components/PageComponents/ModuleConfig/MQTT.tsx deleted file mode 100644 index c65c7aa81..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/MQTT.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type MqttValidation, - MqttValidationSchema, -} from "@app/validation/moduleConfig/mqtt.ts"; -import { create } from "@bufbuild/protobuf"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface MqttModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const MQTT = ({ onFormInit }: MqttModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "mqtt" }); - - const { - config, - moduleConfig, - setChange, - getEffectiveModuleConfig, - removeChange, - } = useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: MqttValidation) => { - const payload = { - ...data, - mapReportSettings: create( - Protobuf.ModuleConfig.ModuleConfig_MapReportSettingsSchema, - data.mapReportSettings, - ), - }; - - if (deepCompareConfig(moduleConfig.mqtt, payload, true)) { - removeChange({ type: "moduleConfig", variant: "mqtt" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "mqtt" }, - payload, - moduleConfig.mqtt, - ); - }; - - const populateDefaultValues = ( - cfg: Protobuf.ModuleConfig.ModuleConfig_MQTTConfig | undefined, - ) => { - return cfg - ? { - ...cfg, - mapReportSettings: cfg.mapReportSettings ?? { - publishIntervalSecs: 0, - positionPrecision: 10, - }, - } - : undefined; - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={MqttValidationSchema} - defaultValues={populateDefaultValues(moduleConfig.mqtt)} - values={populateDefaultValues(getEffectiveModuleConfig("mqtt"))} - fieldGroups={[ - { - label: t("mqtt.title"), - description: t("mqtt.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("mqtt.enabled.label"), - description: t("mqtt.enabled.description"), - }, - { - type: "text", - name: "address", - label: t("mqtt.address.label"), - description: t("mqtt.address.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "text", - name: "username", - label: t("mqtt.username.label"), - description: t("mqtt.username.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "password", - name: "password", - label: t("mqtt.password.label"), - description: t("mqtt.password.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "encryptionEnabled", - label: t("mqtt.encryptionEnabled.label"), - description: t("mqtt.encryptionEnabled.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "jsonEnabled", - label: t("mqtt.jsonEnabled.label"), - description: t("mqtt.jsonEnabled.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "tlsEnabled", - label: t("mqtt.tlsEnabled.label"), - description: t("mqtt.tlsEnabled.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "text", - name: "root", - label: t("mqtt.root.label"), - description: t("mqtt.root.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "proxyToClientEnabled", - label: t("mqtt.proxyToClientEnabled.label"), - description: t("mqtt.proxyToClientEnabled.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "mapReportingEnabled", - label: t("mqtt.mapReportingEnabled.label"), - description: t("mqtt.mapReportingEnabled.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "mapReportSettings.publishIntervalSecs", - label: t("mqtt.mapReportSettings.publishIntervalSecs.label"), - description: t( - "mqtt.mapReportSettings.publishIntervalSecs.description", - ), - properties: { - suffix: t("unit.second.plural"), - }, - disabledBy: [ - { - fieldName: "enabled", - }, - { - fieldName: "mapReportingEnabled", - }, - ], - }, - { - type: "select", - name: "mapReportSettings.positionPrecision", - label: t("mqtt.mapReportSettings.positionPrecision.label"), - description: t( - "mqtt.mapReportSettings.positionPrecision.description", - ), - properties: { - enumValue: - config.display?.units === 0 - ? { - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_km23", - )]: 10, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_km12", - )]: 11, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_km5_8", - )]: 12, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_km2_9", - )]: 13, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_km1_5", - )]: 14, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_m700", - )]: 15, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_m350", - )]: 16, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_m200", - )]: 17, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_m90", - )]: 18, - [t( - "mqtt.mapReportSettings.positionPrecision.options.metric_m50", - )]: 19, - } - : { - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_mi15", - )]: 10, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_mi7_3", - )]: 11, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_mi3_6", - )]: 12, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_mi1_8", - )]: 13, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_9", - )]: 14, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_5", - )]: 15, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_mi0_2", - )]: 16, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_ft600", - )]: 17, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_ft300", - )]: 18, - [t( - "mqtt.mapReportSettings.positionPrecision.options.imperial_ft150", - )]: 19, - }, - }, - disabledBy: [ - { - fieldName: "enabled", - }, - { - fieldName: "mapReportingEnabled", - }, - ], - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/NeighborInfo.tsx b/packages/web/src/components/PageComponents/ModuleConfig/NeighborInfo.tsx deleted file mode 100644 index 1b134cbc9..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/NeighborInfo.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type NeighborInfoValidation, - NeighborInfoValidationSchema, -} from "@app/validation/moduleConfig/neighborInfo.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { useTranslation } from "react-i18next"; - -interface NeighborInfoModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const NeighborInfo = ({ onFormInit }: NeighborInfoModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "neighborInfo" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: NeighborInfoValidation) => { - if (deepCompareConfig(moduleConfig.neighborInfo, data, true)) { - removeChange({ type: "moduleConfig", variant: "neighborInfo" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "neighborInfo" }, - data, - moduleConfig.neighborInfo, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={NeighborInfoValidationSchema} - defaultValues={moduleConfig.neighborInfo} - values={getEffectiveModuleConfig("neighborInfo")} - fieldGroups={[ - { - label: t("neighborInfo.title"), - description: t("neighborInfo.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("neighborInfo.enabled.label"), - description: t("neighborInfo.enabled.description"), - }, - { - type: "number", - name: "updateInterval", - label: t("neighborInfo.updateInterval.label"), - description: t("neighborInfo.updateInterval.description"), - properties: { - suffix: t("unit.second.plural"), - }, - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/Paxcounter.tsx b/packages/web/src/components/PageComponents/ModuleConfig/Paxcounter.tsx deleted file mode 100644 index 13d636b04..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/Paxcounter.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type PaxcounterValidation, - PaxcounterValidationSchema, -} from "@app/validation/moduleConfig/paxcounter.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { useTranslation } from "react-i18next"; - -interface PaxcounterModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const Paxcounter = ({ onFormInit }: PaxcounterModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "paxcounter" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: PaxcounterValidation) => { - if (deepCompareConfig(moduleConfig.paxcounter, data, true)) { - removeChange({ type: "moduleConfig", variant: "paxcounter" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "paxcounter" }, - data, - moduleConfig.paxcounter, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={PaxcounterValidationSchema} - defaultValues={moduleConfig.paxcounter} - values={getEffectiveModuleConfig("paxcounter")} - fieldGroups={[ - { - label: t("paxcounter.title"), - description: t("paxcounter.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("paxcounter.enabled.label"), - description: t("paxcounter.enabled.description"), - }, - { - type: "number", - name: "paxcounterUpdateInterval", - label: t("paxcounter.paxcounterUpdateInterval.label"), - description: t("paxcounter.paxcounterUpdateInterval.description"), - properties: { - suffix: t("unit.second.plural"), - }, - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "wifiThreshold", - label: t("paxcounter.wifiThreshold.label"), - description: t("paxcounter.wifiThreshold.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "bleThreshold", - label: t("paxcounter.bleThreshold.label"), - description: t("paxcounter.bleThreshold.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/RangeTest.tsx b/packages/web/src/components/PageComponents/ModuleConfig/RangeTest.tsx deleted file mode 100644 index a360492e6..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/RangeTest.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type RangeTestValidation, - RangeTestValidationSchema, -} from "@app/validation/moduleConfig/rangeTest.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { useTranslation } from "react-i18next"; - -interface RangeTestModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const RangeTest = ({ onFormInit }: RangeTestModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "rangeTest" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: RangeTestValidation) => { - if (deepCompareConfig(moduleConfig.rangeTest, data, true)) { - removeChange({ type: "moduleConfig", variant: "rangeTest" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "rangeTest" }, - data, - moduleConfig.rangeTest, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={RangeTestValidationSchema} - defaultValues={moduleConfig.rangeTest} - values={getEffectiveModuleConfig("rangeTest")} - fieldGroups={[ - { - label: t("rangeTest.title"), - description: t("rangeTest.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("rangeTest.enabled.label"), - description: t("rangeTest.enabled.description"), - }, - { - type: "number", - name: "sender", - label: t("rangeTest.sender.label"), - description: t("rangeTest.sender.description"), - properties: { - suffix: t("unit.second.plural"), - }, - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "toggle", - name: "save", - label: t("rangeTest.save.label"), - description: t("rangeTest.save.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/Serial.tsx b/packages/web/src/components/PageComponents/ModuleConfig/Serial.tsx deleted file mode 100644 index 96de4bc65..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/Serial.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type SerialValidation, - SerialValidationSchema, -} from "@app/validation/moduleConfig/serial.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface SerialModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const Serial = ({ onFormInit }: SerialModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "serial" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: SerialValidation) => { - if (deepCompareConfig(moduleConfig.serial, data, true)) { - removeChange({ type: "moduleConfig", variant: "serial" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "serial" }, - data, - moduleConfig.serial, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={SerialValidationSchema} - defaultValues={moduleConfig.serial} - values={getEffectiveModuleConfig("serial")} - fieldGroups={[ - { - label: t("serial.title"), - description: t("serial.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("serial.enabled.label"), - description: t("serial.enabled.description"), - }, - { - type: "toggle", - name: "echo", - label: t("serial.echo.label"), - description: t("serial.echo.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "rxd", - label: t("serial.rxd.label"), - description: t("serial.rxd.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "txd", - label: t("serial.txd.label"), - description: t("serial.txd.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "select", - name: "baud", - label: t("serial.baud.label"), - description: t("serial.baud.description"), - - disabledBy: [ - { - fieldName: "enabled", - }, - ], - properties: { - enumValue: - Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Baud, - }, - }, - { - type: "number", - name: "timeout", - label: t("serial.timeout.label"), - description: t("serial.timeout.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "select", - name: "mode", - label: t("serial.mode.label"), - description: t("serial.mode.description"), - - disabledBy: [ - { - fieldName: "enabled", - }, - ], - properties: { - enumValue: - Protobuf.ModuleConfig.ModuleConfig_SerialConfig_Serial_Mode, - formatEnumName: true, - }, - }, - { - type: "toggle", - name: "overrideConsoleSerialPort", - label: t("serial.overrideConsoleSerialPort.label"), - description: t("serial.overrideConsoleSerialPort.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/StoreForward.tsx b/packages/web/src/components/PageComponents/ModuleConfig/StoreForward.tsx deleted file mode 100644 index 658362df0..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/StoreForward.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type StoreForwardValidation, - StoreForwardValidationSchema, -} from "@app/validation/moduleConfig/storeForward.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { useTranslation } from "react-i18next"; - -interface StoreForwardModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const StoreForward = ({ onFormInit }: StoreForwardModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "storeForward" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: StoreForwardValidation) => { - if (deepCompareConfig(moduleConfig.storeForward, data, true)) { - removeChange({ type: "moduleConfig", variant: "storeForward" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "storeForward" }, - data, - moduleConfig.storeForward, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={StoreForwardValidationSchema} - defaultValues={moduleConfig.storeForward} - values={getEffectiveModuleConfig("storeForward")} - fieldGroups={[ - { - label: t("storeForward.title"), - description: t("storeForward.description"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("storeForward.enabled.label"), - description: t("storeForward.enabled.description"), - }, - { - type: "toggle", - name: "heartbeat", - label: t("storeForward.heartbeat.label"), - description: t("storeForward.heartbeat.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "records", - label: t("storeForward.records.label"), - description: t("storeForward.records.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - properties: { - suffix: t("unit.record.plural"), - }, - }, - { - type: "number", - name: "historyReturnMax", - label: t("storeForward.historyReturnMax.label"), - description: t("storeForward.historyReturnMax.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - { - type: "number", - name: "historyReturnWindow", - label: t("storeForward.historyReturnWindow.label"), - description: t("storeForward.historyReturnWindow.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx b/packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx deleted file mode 100644 index c6d1384a1..000000000 --- a/packages/web/src/components/PageComponents/ModuleConfig/Telemetry.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type TelemetryValidation, - TelemetryValidationSchema, -} from "@app/validation/moduleConfig/telemetry.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { useTranslation } from "react-i18next"; - -interface TelemetryModuleConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const Telemetry = ({ onFormInit }: TelemetryModuleConfigProps) => { - useWaitForConfig({ moduleConfigCase: "telemetry" }); - - const { moduleConfig, setChange, getEffectiveModuleConfig, removeChange } = - useDevice(); - const { t } = useTranslation("moduleConfig"); - - const onSubmit = (data: TelemetryValidation) => { - if (deepCompareConfig(moduleConfig.telemetry, data, true)) { - removeChange({ type: "moduleConfig", variant: "telemetry" }); - return; - } - - setChange( - { type: "moduleConfig", variant: "telemetry" }, - data, - moduleConfig.telemetry, - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={TelemetryValidationSchema} - defaultValues={moduleConfig.telemetry} - values={getEffectiveModuleConfig("telemetry")} - fieldGroups={[ - { - label: t("telemetry.title"), - description: t("telemetry.description"), - fields: [ - { - type: "number", - name: "deviceUpdateInterval", - label: t("telemetry.deviceUpdateInterval.label"), - description: t("telemetry.deviceUpdateInterval.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "number", - name: "environmentUpdateInterval", - label: t("telemetry.environmentUpdateInterval.label"), - description: t("telemetry.environmentUpdateInterval.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "toggle", - name: "environmentMeasurementEnabled", - label: t("telemetry.environmentMeasurementEnabled.label"), - description: t( - "telemetry.environmentMeasurementEnabled.description", - ), - }, - { - type: "toggle", - name: "environmentScreenEnabled", - label: t("telemetry.environmentScreenEnabled.label"), - description: t("telemetry.environmentScreenEnabled.description"), - }, - { - type: "toggle", - name: "environmentDisplayFahrenheit", - label: t("telemetry.environmentDisplayFahrenheit.label"), - description: t( - "telemetry.environmentDisplayFahrenheit.description", - ), - }, - { - type: "toggle", - name: "airQualityEnabled", - label: t("telemetry.airQualityEnabled.label"), - description: t("telemetry.airQualityEnabled.description"), - }, - { - type: "number", - name: "airQualityInterval", - label: t("telemetry.airQualityInterval.label"), - description: t("telemetry.airQualityInterval.description"), - }, - { - type: "toggle", - name: "powerMeasurementEnabled", - label: t("telemetry.powerMeasurementEnabled.label"), - description: t("telemetry.powerMeasurementEnabled.description"), - }, - { - type: "number", - name: "powerUpdateInterval", - label: t("telemetry.powerUpdateInterval.label"), - description: t("telemetry.powerUpdateInterval.description"), - }, - { - type: "toggle", - name: "powerScreenEnabled", - label: t("telemetry.powerScreenEnabled.label"), - description: t("telemetry.powerScreenEnabled.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/Bluetooth.tsx b/packages/web/src/components/PageComponents/Settings/Bluetooth.tsx deleted file mode 100644 index 423197c95..000000000 --- a/packages/web/src/components/PageComponents/Settings/Bluetooth.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type BluetoothValidation, - BluetoothValidationSchema, -} from "@app/validation/config/bluetooth.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface BluetoothConfigProps { - onFormInit: DynamicFormFormInit; -} -export const Bluetooth = ({ onFormInit }: BluetoothConfigProps) => { - useWaitForConfig({ configCase: "bluetooth" }); - - const { config, setChange, getEffectiveConfig, removeChange } = useDevice(); - const { t } = useTranslation("config"); - - const onSubmit = (data: BluetoothValidation) => { - if (deepCompareConfig(config.bluetooth, data, true)) { - removeChange({ type: "config", variant: "bluetooth" }); - return; - } - - setChange({ type: "config", variant: "bluetooth" }, data, config.bluetooth); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={BluetoothValidationSchema} - defaultValues={config.bluetooth} - values={getEffectiveConfig("bluetooth")} - fieldGroups={[ - { - label: t("bluetooth.title"), - description: t("bluetooth.description"), - notes: t("bluetooth.note"), - fields: [ - { - type: "toggle", - name: "enabled", - label: t("bluetooth.enabled.label"), - description: t("bluetooth.enabled.description"), - }, - { - type: "select", - name: "mode", - label: t("bluetooth.pairingMode.label"), - description: t("bluetooth.pairingMode.description"), - disabledBy: [ - { - fieldName: "enabled", - }, - ], - properties: { - enumValue: Protobuf.Config.Config_BluetoothConfig_PairingMode, - formatEnumName: true, - }, - }, - { - type: "number", - name: "fixedPin", - label: t("bluetooth.pin.label"), - description: t("bluetooth.pin.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/Device/index.tsx b/packages/web/src/components/PageComponents/Settings/Device/index.tsx deleted file mode 100644 index 063eb682f..000000000 --- a/packages/web/src/components/PageComponents/Settings/Device/index.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type DeviceValidation, - DeviceValidationSchema, -} from "@app/validation/config/device.ts"; -import { useUnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface DeviceConfigProps { - onFormInit: DynamicFormFormInit; -} -export const Device = ({ onFormInit }: DeviceConfigProps) => { - useWaitForConfig({ configCase: "device" }); - - const { config, setChange, getEffectiveConfig, removeChange } = useDevice(); - const { t } = useTranslation("config"); - const { validateRoleSelection } = useUnsafeRolesDialog(); - - const onSubmit = (data: DeviceValidation) => { - if (deepCompareConfig(config.device, data, true)) { - removeChange({ type: "config", variant: "device" }); - return; - } - - setChange({ type: "config", variant: "device" }, data, config.device); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={DeviceValidationSchema} - defaultValues={config.device} - values={getEffectiveConfig("device")} - fieldGroups={[ - { - label: t("device.title"), - description: t("device.description"), - fields: [ - { - type: "select", - name: "role", - label: t("device.role.label"), - description: t("device.role.description"), - validate: validateRoleSelection, - properties: { - enumValue: Protobuf.Config.Config_DeviceConfig_Role, - formatEnumName: true, - }, - }, - { - type: "number", - name: "buttonGpio", - label: t("device.buttonPin.label"), - description: t("device.buttonPin.description"), - }, - { - type: "number", - name: "buzzerGpio", - label: t("device.buzzerPin.label"), - description: t("device.buzzerPin.description"), - }, - { - type: "select", - name: "rebroadcastMode", - label: t("device.rebroadcastMode.label"), - description: t("device.rebroadcastMode.description"), - properties: { - enumValue: Protobuf.Config.Config_DeviceConfig_RebroadcastMode, - formatEnumName: true, - }, - }, - { - type: "number", - name: "nodeInfoBroadcastSecs", - label: t("device.nodeInfoBroadcastInterval.label"), - description: t("device.nodeInfoBroadcastInterval.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "toggle", - name: "doubleTapAsButtonPress", - label: t("device.doubleTapAsButtonPress.label"), - description: t("device.doubleTapAsButtonPress.description"), - }, - { - type: "toggle", - name: "disableTripleClick", - label: t("device.disableTripleClick.label"), - description: t("device.disableTripleClick.description"), - }, - { - type: "text", - name: "tzdef", - label: t("device.posixTimezone.label"), - description: t("device.posixTimezone.description"), - properties: { - fieldLength: { - max: 64, - currentValueLength: - getEffectiveConfig("device")?.tzdef?.length, - showCharacterCount: true, - }, - }, - }, - { - type: "toggle", - name: "ledHeartbeatDisabled", - label: t("device.ledHeartbeatDisabled.label"), - description: t("device.ledHeartbeatDisabled.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/Display.tsx b/packages/web/src/components/PageComponents/Settings/Display.tsx deleted file mode 100644 index 963e7ea72..000000000 --- a/packages/web/src/components/PageComponents/Settings/Display.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type DisplayValidation, - DisplayValidationSchema, -} from "@app/validation/config/display.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface DisplayConfigProps { - onFormInit: DynamicFormFormInit; -} -export const Display = ({ onFormInit }: DisplayConfigProps) => { - useWaitForConfig({ configCase: "display" }); - const { config, setChange, getEffectiveConfig, removeChange } = useDevice(); - const { t } = useTranslation("config"); - - const onSubmit = (data: DisplayValidation) => { - if (deepCompareConfig(config.display, data, true)) { - removeChange({ type: "config", variant: "display" }); - return; - } - - setChange({ type: "config", variant: "display" }, data, config.display); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={DisplayValidationSchema} - defaultValues={config.display} - values={getEffectiveConfig("display")} - fieldGroups={[ - { - label: t("display.title"), - description: t("display.description"), - fields: [ - { - type: "number", - name: "screenOnSecs", - label: t("display.screenTimeout.label"), - description: t("display.screenTimeout.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - // TODO: This field is deprecated since protobufs 2.7.4 and only has UNUSED=0 value. - // GPS format has been moved to DeviceUIConfig.gps_format with proper enum values (DEC, DMS, UTM, MGRS, OLC, OSGR, MLS). - // This should be removed once DeviceUI settings are implemented. - // See: packages/protobufs/meshtastic/device_ui.proto - { - type: "select", - name: "gpsFormat", - label: t("display.gpsDisplayUnits.label"), - description: t("display.gpsDisplayUnits.description"), - properties: { - enumValue: - Protobuf.Config - .Config_DisplayConfig_DeprecatedGpsCoordinateFormat, - }, - }, - { - type: "number", - name: "autoScreenCarouselSecs", - label: t("display.carouselDelay.label"), - description: t("display.carouselDelay.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "toggle", - name: "compassNorthTop", - label: t("display.compassNorthTop.label"), - description: t("display.compassNorthTop.description"), - }, - { - type: "toggle", - name: "use12hClock", - label: t("display.twelveHourClock.label"), - description: t("display.twelveHourClock.description"), - }, - { - type: "toggle", - name: "flipScreen", - label: t("display.flipScreen.label"), - description: t("display.flipScreen.description"), - }, - { - type: "select", - name: "units", - label: t("display.displayUnits.label"), - description: t("display.displayUnits.description"), - properties: { - enumValue: Protobuf.Config.Config_DisplayConfig_DisplayUnits, - formatEnumName: true, - }, - }, - { - type: "select", - name: "oled", - label: t("display.oledType.label"), - description: t("display.oledType.description"), - properties: { - enumValue: Protobuf.Config.Config_DisplayConfig_OledType, - }, - }, - { - type: "select", - name: "displaymode", - label: t("display.displayMode.label"), - description: t("display.displayMode.description"), - properties: { - enumValue: Protobuf.Config.Config_DisplayConfig_DisplayMode, - formatEnumName: true, - }, - }, - { - type: "toggle", - name: "headingBold", - label: t("display.headingBold.label"), - description: t("display.headingBold.description"), - }, - { - type: "toggle", - name: "wakeOnTapOrMotion", - label: t("display.wakeOnTapOrMotion.label"), - description: t("display.wakeOnTapOrMotion.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/LoRa.tsx b/packages/web/src/components/PageComponents/Settings/LoRa.tsx deleted file mode 100644 index 844085dfd..000000000 --- a/packages/web/src/components/PageComponents/Settings/LoRa.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type LoRaValidation, - LoRaValidationSchema, -} from "@app/validation/config/lora.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface LoRaConfigProps { - onFormInit: DynamicFormFormInit; -} -export const LoRa = ({ onFormInit }: LoRaConfigProps) => { - useWaitForConfig({ configCase: "lora" }); - - const { config, setChange, getEffectiveConfig, removeChange } = useDevice(); - const { t } = useTranslation("config"); - - const onSubmit = (data: LoRaValidation) => { - if (deepCompareConfig(config.lora, data, true)) { - removeChange({ type: "config", variant: "lora" }); - return; - } - - setChange({ type: "config", variant: "lora" }, data, config.lora); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={LoRaValidationSchema} - defaultValues={config.lora} - values={getEffectiveConfig("lora")} - fieldGroups={[ - { - label: t("lora.title"), - description: t("lora.description"), - fields: [ - { - type: "select", - name: "region", - label: t("lora.region.label"), - description: t("lora.region.description"), - properties: { - enumValue: Protobuf.Config.Config_LoRaConfig_RegionCode, - }, - }, - { - type: "select", - name: "hopLimit", - label: t("lora.hopLimit.label"), - description: t("lora.hopLimit.description"), - properties: { - enumValue: { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7 }, - }, - }, - { - type: "number", - name: "channelNum", - label: t("lora.frequencySlot.label"), - description: t("lora.frequencySlot.description"), - }, - { - type: "toggle", - name: "ignoreMqtt", - label: t("lora.ignoreMqtt.label"), - description: t("lora.ignoreMqtt.description"), - }, - { - type: "toggle", - name: "configOkToMqtt", - label: t("lora.okToMqtt.label"), - description: t("lora.okToMqtt.description"), - }, - ], - }, - { - label: t("lora.waveformSettings.label"), - description: t("lora.waveformSettings.description"), - fields: [ - { - type: "toggle", - name: "usePreset", - label: t("lora.usePreset.label"), - description: t("lora.usePreset.description"), - }, - { - type: "select", - name: "modemPreset", - label: t("lora.modemPreset.label"), - description: t("lora.modemPreset.description"), - disabledBy: [ - { - fieldName: "usePreset", - }, - ], - properties: { - enumValue: Protobuf.Config.Config_LoRaConfig_ModemPreset, - formatEnumName: true, - }, - }, - { - type: "number", - name: "bandwidth", - label: t("lora.bandwidth.label"), - description: t("lora.bandwidth.description"), - disabledBy: [ - { - fieldName: "usePreset", - invert: true, - }, - ], - properties: { - suffix: t("unit.megahertz"), - }, - }, - { - type: "number", - name: "spreadFactor", - label: t("lora.spreadingFactor.label"), - description: t("lora.spreadingFactor.description"), - - disabledBy: [ - { - fieldName: "usePreset", - invert: true, - }, - ], - properties: { - suffix: t("unit.cps"), - }, - }, - { - type: "number", - name: "codingRate", - label: t("lora.codingRate.label"), - description: t("lora.codingRate.description"), - disabledBy: [ - { - fieldName: "usePreset", - invert: true, - }, - ], - }, - ], - }, - { - label: t("lora.radioSettings.label"), - description: t("lora.radioSettings.description"), - fields: [ - { - type: "toggle", - name: "txEnabled", - label: t("lora.transmitEnabled.label"), - description: t("lora.transmitEnabled.description"), - }, - { - type: "number", - name: "txPower", - label: t("lora.transmitPower.label"), - description: t("lora.transmitPower.description"), - properties: { - suffix: t("unit.dbm"), - }, - }, - { - type: "toggle", - name: "overrideDutyCycle", - label: t("lora.overrideDutyCycle.label"), - description: t("lora.overrideDutyCycle.description"), - }, - { - type: "number", - name: "frequencyOffset", - label: t("lora.frequencyOffset.label"), - description: t("lora.frequencyOffset.description"), - properties: { - suffix: t("unit.hertz"), - }, - }, - { - type: "toggle", - name: "sx126xRxBoostedGain", - label: t("lora.boostedRxGain.label"), - description: t("lora.boostedRxGain.description"), - }, - { - type: "number", - name: "overrideFrequency", - label: t("lora.overrideFrequency.label"), - description: t("lora.overrideFrequency.description"), - properties: { - suffix: t("unit.megahertz"), - step: 0.001, - }, - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/Network/index.tsx b/packages/web/src/components/PageComponents/Settings/Network/index.tsx deleted file mode 100644 index 6b5bb63ec..000000000 --- a/packages/web/src/components/PageComponents/Settings/Network/index.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type NetworkValidation, - NetworkValidationSchema, -} from "@app/validation/config/network.ts"; -import { create } from "@bufbuild/protobuf"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { - convertIntToIpAddress, - convertIpAddressToInt, -} from "@core/utils/ip.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface NetworkConfigProps { - onFormInit: DynamicFormFormInit; -} -export const Network = ({ onFormInit }: NetworkConfigProps) => { - useWaitForConfig({ configCase: "network" }); - - const { config, setChange, getEffectiveConfig, removeChange } = useDevice(); - const { t } = useTranslation("config"); - - const networkConfig = getEffectiveConfig("network"); - - const onSubmit = (data: NetworkValidation) => { - const payload = { - ...data, - ipv4Config: create( - Protobuf.Config.Config_NetworkConfig_IpV4ConfigSchema, - { - ip: convertIpAddressToInt(data.ipv4Config?.ip ?? ""), - gateway: convertIpAddressToInt(data.ipv4Config?.gateway ?? ""), - subnet: convertIpAddressToInt(data.ipv4Config?.subnet ?? ""), - dns: convertIpAddressToInt(data.ipv4Config?.dns ?? ""), - }, - ), - }; - - if (deepCompareConfig(config.network, payload, true)) { - removeChange({ type: "config", variant: "network" }); - return; - } - - setChange({ type: "config", variant: "network" }, payload, config.network); - }; - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={NetworkValidationSchema} - defaultValues={{ - ...config.network, - ipv4Config: { - ip: convertIntToIpAddress(config.network?.ipv4Config?.ip ?? 0), - gateway: convertIntToIpAddress( - config.network?.ipv4Config?.gateway ?? 0, - ), - subnet: convertIntToIpAddress( - config.network?.ipv4Config?.subnet ?? 0, - ), - dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0), - }, - enabledProtocols: - config.network?.enabledProtocols ?? - Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST, - }} - values={ - { - ...networkConfig, - ipv4Config: { - ip: convertIntToIpAddress(networkConfig?.ipv4Config?.ip ?? 0), - gateway: convertIntToIpAddress( - networkConfig?.ipv4Config?.gateway ?? 0, - ), - subnet: convertIntToIpAddress( - networkConfig?.ipv4Config?.subnet ?? 0, - ), - dns: convertIntToIpAddress(networkConfig?.ipv4Config?.dns ?? 0), - }, - enabledProtocols: - networkConfig?.enabledProtocols ?? - Protobuf.Config.Config_NetworkConfig_ProtocolFlags.NO_BROADCAST, - } as NetworkValidation - } - fieldGroups={[ - { - label: t("network.title"), - description: t("network.description"), - notes: t("network.note"), - fields: [ - { - type: "toggle", - name: "wifiEnabled", - label: t("network.wifiEnabled.label"), - description: t("network.wifiEnabled.description"), - }, - { - type: "text", - name: "wifiSsid", - label: t("network.ssid.label"), - description: t("network.ssid.label"), - disabledBy: [ - { - fieldName: "wifiEnabled", - }, - ], - }, - { - type: "password", - name: "wifiPsk", - label: t("network.psk.label"), - description: t("network.psk.description"), - disabledBy: [ - { - fieldName: "wifiEnabled", - }, - ], - }, - ], - }, - { - label: t("network.ethernetConfigSettings.label"), - description: t("network.ethernetConfigSettings.description"), - fields: [ - { - type: "toggle", - name: "ethEnabled", - label: t("network.ethernetEnabled.label"), - description: t("network.ethernetEnabled.description"), - }, - ], - }, - { - label: t("network.ipConfigSettings.label"), - description: t("network.ipConfigSettings.description"), - fields: [ - { - type: "select", - name: "addressMode", - label: t("network.addressMode.label"), - description: t("network.addressMode.description"), - properties: { - enumValue: Protobuf.Config.Config_NetworkConfig_AddressMode, - }, - }, - { - type: "text", - name: "ipv4Config.ip", - label: t("network.ip.label"), - description: t("network.ip.description"), - disabledBy: [ - { - fieldName: "addressMode", - selector: - Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP, - }, - ], - }, - { - type: "text", - name: "ipv4Config.gateway", - label: t("network.gateway.label"), - description: t("network.gateway.description"), - disabledBy: [ - { - fieldName: "addressMode", - selector: - Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP, - }, - ], - }, - { - type: "text", - name: "ipv4Config.subnet", - label: t("network.subnet.label"), - description: t("network.subnet.description"), - disabledBy: [ - { - fieldName: "addressMode", - selector: - Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP, - }, - ], - }, - { - type: "text", - name: "ipv4Config.dns", - label: t("network.dns.label"), - description: t("network.dns.description"), - disabledBy: [ - { - fieldName: "addressMode", - selector: - Protobuf.Config.Config_NetworkConfig_AddressMode.DHCP, - }, - ], - }, - ], - }, - { - label: t("network.udpConfigSettings.label"), - description: t("network.udpConfigSettings.description"), - fields: [ - { - type: "select", - name: "enabledProtocols", - label: t("network.meshViaUdp.label"), - properties: { - enumValue: Protobuf.Config.Config_NetworkConfig_ProtocolFlags, - formatEnumName: true, - }, - }, - ], - }, - { - label: t("network.ntpConfigSettings.label"), - description: t("network.ntpConfigSettings.description"), - fields: [ - { - type: "text", - name: "ntpServer", - label: t("network.ntpServer.label"), - }, - ], - }, - { - label: t("network.rsyslogConfigSettings.label"), - description: t("network.rsyslogConfigSettings.description"), - fields: [ - { - type: "text", - name: "rsyslogServer", - label: t("network.rsyslogServer.label"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/Position.tsx b/packages/web/src/components/PageComponents/Settings/Position.tsx deleted file mode 100644 index f8ab0ab2e..000000000 --- a/packages/web/src/components/PageComponents/Settings/Position.tsx +++ /dev/null @@ -1,303 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type PositionValidation, - PositionValidationSchema, -} from "@app/validation/config/position.ts"; -import { create } from "@bufbuild/protobuf"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { - type FlagName, - usePositionFlags, -} from "@core/hooks/usePositionFlags.ts"; -import { useDevice, useNodeDB } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { Protobuf } from "@meshtastic/core"; -import { useCallback, useMemo } from "react"; -import { useTranslation } from "react-i18next"; - -interface PositionConfigProps { - onFormInit: DynamicFormFormInit; -} -export const Position = ({ onFormInit }: PositionConfigProps) => { - useWaitForConfig({ configCase: "position" }); - - const { - setChange, - config, - getEffectiveConfig, - removeChange, - queueAdminMessage, - } = useDevice(); - const { getMyNode } = useNodeDB(); - const { flagsValue, activeFlags, toggleFlag, getAllFlags } = usePositionFlags( - getEffectiveConfig("position")?.positionFlags ?? 0, - ); - const { t } = useTranslation("config"); - - const myNode = getMyNode(); - const currentPosition = myNode?.position; - - const effectiveConfig = getEffectiveConfig("position"); - const displayUnits = getEffectiveConfig("display")?.units; - - const formValues = useMemo(() => { - return { - ...config.position, - ...effectiveConfig, - // Include current position coordinates if available - latitude: currentPosition?.latitudeI - ? currentPosition.latitudeI / 1e7 - : undefined, - longitude: currentPosition?.longitudeI - ? currentPosition.longitudeI / 1e7 - : undefined, - altitude: currentPosition?.altitude ?? 0, - } as PositionValidation; - }, [config.position, effectiveConfig, currentPosition]); - - const onSubmit = (data: PositionValidation) => { - // Exclude position coordinates from config payload (they're handled via admin message) - const { - latitude: _latitude, - longitude: _longitude, - altitude: _altitude, - ...configData - } = data; - const payload = { ...configData, positionFlags: flagsValue }; - - // Save config first - let configResult: ReturnType | undefined; - if (deepCompareConfig(config.position, payload, true)) { - removeChange({ type: "config", variant: "position" }); - configResult = undefined; - } else { - configResult = setChange( - { type: "config", variant: "position" }, - payload, - config.position, - ); - } - - // Then handle position coordinates via admin message if fixedPosition is enabled - if ( - data.fixedPosition && - data.latitude !== undefined && - data.longitude !== undefined - ) { - const message = create(Protobuf.Admin.AdminMessageSchema, { - payloadVariant: { - case: "setFixedPosition", - value: create(Protobuf.Mesh.PositionSchema, { - latitudeI: Math.round(data.latitude * 1e7), - longitudeI: Math.round(data.longitude * 1e7), - altitude: data.altitude || 0, - time: Math.floor(Date.now() / 1000), - }), - }, - }); - - queueAdminMessage(message); - } - - return configResult; - }; - - const onPositonFlagChange = useCallback( - (name: string) => { - return toggleFlag(name as FlagName); - }, - [toggleFlag], - ); - - return ( - - onSubmit={(data) => { - data.positionFlags = flagsValue; - return onSubmit(data); - }} - onFormInit={onFormInit} - validationSchema={PositionValidationSchema} - defaultValues={config.position} - values={formValues} - fieldGroups={[ - { - label: t("position.title"), - description: t("position.description"), - fields: [ - { - type: "toggle", - name: "positionBroadcastSmartEnabled", - label: t("position.smartPositionEnabled.label"), - description: t("position.smartPositionEnabled.description"), - }, - { - type: "select", - name: "gpsMode", - label: t("position.gpsMode.label"), - description: t("position.gpsMode.description"), - properties: { - enumValue: Protobuf.Config.Config_PositionConfig_GpsMode, - }, - }, - { - type: "toggle", - name: "fixedPosition", - label: t("position.fixedPosition.label"), - description: t("position.fixedPosition.description"), - disabledBy: [ - { - fieldName: "gpsMode", - selector: - Protobuf.Config.Config_PositionConfig_GpsMode.ENABLED, - }, - ], - }, - // Position coordinate fields (only shown when fixedPosition is enabled) - { - type: "number", - name: "latitude", - label: t("position.fixedPosition.latitude.label"), - description: `${t("position.fixedPosition.latitude.description")} (Max 7 decimal precision)`, - properties: { - step: 0.0000001, - suffix: "Degrees", - fieldLength: { - max: 10, - }, - }, - disabledBy: [ - { - fieldName: "fixedPosition", - }, - ], - }, - { - type: "number", - name: "longitude", - label: t("position.fixedPosition.longitude.label"), - description: `${t("position.fixedPosition.longitude.description")} (Max 7 decimal precision)`, - properties: { - step: 0.0000001, - suffix: "Degrees", - fieldLength: { - max: 10, - }, - }, - disabledBy: [ - { - fieldName: "fixedPosition", - }, - ], - }, - { - type: "number", - name: "altitude", - label: t("position.fixedPosition.altitude.label"), - description: t("position.fixedPosition.altitude.description", { - unit: - displayUnits === - Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL - ? "Feet" - : "Meters", - }), - properties: { - step: 0.0000001, - suffix: - displayUnits === - Protobuf.Config.Config_DisplayConfig_DisplayUnits.IMPERIAL - ? "Feet" - : "Meters", - }, - disabledBy: [ - { - fieldName: "fixedPosition", - }, - ], - }, - { - type: "multiSelect", - name: "positionFlags", - value: activeFlags, - isChecked: (name: string) => - activeFlags?.includes(name as FlagName) ?? false, - onValueChange: onPositonFlagChange, - label: t("position.positionFlags.label"), - placeholder: t("position.flags.placeholder"), - description: t("position.positionFlags.description"), - properties: { - enumValue: getAllFlags(), - }, - }, - { - type: "number", - name: "rxGpio", - label: t("position.receivePin.label"), - description: t("position.receivePin.description"), - }, - { - type: "number", - name: "txGpio", - label: t("position.transmitPin.label"), - description: t("position.transmitPin.description"), - }, - { - type: "number", - name: "gpsEnGpio", - label: t("position.enablePin.label"), - description: t("position.enablePin.description"), - }, - ], - }, - { - label: t("position.intervalsSettings.label"), - description: t("position.intervalsSettings.description"), - fields: [ - { - type: "number", - name: "positionBroadcastSecs", - label: t("position.broadcastInterval.label"), - description: t("position.broadcastInterval.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "number", - name: "gpsUpdateInterval", - label: t("position.gpsUpdateInterval.label"), - description: t("position.gpsUpdateInterval.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "number", - name: "broadcastSmartMinimumDistance", - label: t("position.smartPositionMinDistance.label"), - description: t("position.smartPositionMinDistance.description"), - disabledBy: [ - { - fieldName: "positionBroadcastSmartEnabled", - }, - ], - }, - { - type: "number", - name: "broadcastSmartMinimumIntervalSecs", - label: t("position.smartPositionMinInterval.label"), - description: t("position.smartPositionMinInterval.description"), - disabledBy: [ - { - fieldName: "positionBroadcastSmartEnabled", - }, - ], - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/Power.tsx b/packages/web/src/components/PageComponents/Settings/Power.tsx deleted file mode 100644 index dcad23d91..000000000 --- a/packages/web/src/components/PageComponents/Settings/Power.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type PowerValidation, - PowerValidationSchema, -} from "@app/validation/config/power.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { useTranslation } from "react-i18next"; - -interface PowerConfigProps { - onFormInit: DynamicFormFormInit; -} -export const Power = ({ onFormInit }: PowerConfigProps) => { - useWaitForConfig({ configCase: "power" }); - - const { setChange, config, getEffectiveConfig, removeChange } = useDevice(); - const { t } = useTranslation("config"); - - const onSubmit = (data: PowerValidation) => { - if (deepCompareConfig(config.power, data, true)) { - removeChange({ type: "config", variant: "power" }); - return; - } - - setChange({ type: "config", variant: "power" }, data, config.power); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={PowerValidationSchema} - defaultValues={config.power} - values={getEffectiveConfig("power")} - fieldGroups={[ - { - label: t("power.powerConfigSettings.label"), - description: t("power.powerConfigSettings.description"), - fields: [ - { - type: "toggle", - name: "isPowerSaving", - label: t("power.powerSavingEnabled.label"), - description: t("power.powerSavingEnabled.description"), - }, - { - type: "number", - name: "onBatteryShutdownAfterSecs", - label: t("power.shutdownOnBatteryDelay.label"), - description: t("power.shutdownOnBatteryDelay.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "number", - name: "adcMultiplierOverride", - label: t("power.adcMultiplierOverride.label"), - description: t("power.adcMultiplierOverride.description"), - properties: { - step: 0.0001, - }, - }, - { - type: "number", - name: "waitBluetoothSecs", - label: t("power.noConnectionBluetoothDisabled.label"), - description: t("power.noConnectionBluetoothDisabled.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "number", - name: "deviceBatteryInaAddress", - label: t("power.ina219Address.label"), - description: t("power.ina219Address.description"), - }, - ], - }, - { - label: t("power.sleepSettings.label"), - description: t("power.sleepSettings.description"), - fields: [ - { - type: "number", - name: "sdsSecs", - label: t("power.superDeepSleepDuration.label"), - description: t("power.superDeepSleepDuration.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "number", - name: "lsSecs", - label: t("power.lightSleepDuration.label"), - description: t("power.lightSleepDuration.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - { - type: "number", - name: "minWakeSecs", - label: t("power.minimumWakeTime.label"), - description: t("power.minimumWakeTime.description"), - properties: { - suffix: t("unit.second.plural"), - }, - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/Security/Security.tsx b/packages/web/src/components/PageComponents/Settings/Security/Security.tsx deleted file mode 100644 index a3e08e2cb..000000000 --- a/packages/web/src/components/PageComponents/Settings/Security/Security.tsx +++ /dev/null @@ -1,306 +0,0 @@ -import { useWaitForConfig } from "@app/core/hooks/useWaitForConfig"; -import { - type ParsedSecurity, - type RawSecurity, - RawSecuritySchema, -} from "@app/validation/config/security.ts"; -import { ManagedModeDialog } from "@components/Dialog/ManagedModeDialog.tsx"; -import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx"; -import { createZodResolver } from "@components/Form/createZodResolver.ts"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice } from "@core/stores"; -import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; -import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts"; -import { fromByteArray, toByteArray } from "base64-js"; -import { useEffect, useState } from "react"; -import { type DefaultValues, useForm } from "react-hook-form"; -import { useTranslation } from "react-i18next"; - -interface SecurityConfigProps { - onFormInit: DynamicFormFormInit; -} -export const Security = ({ onFormInit }: SecurityConfigProps) => { - useWaitForConfig({ configCase: "security" }); - - const { config, setChange, setDialogOpen, getEffectiveConfig, removeChange } = - useDevice(); - - const { t } = useTranslation("config"); - - const defaultConfig = config.security; - const defaultValues = { - ...defaultConfig, - ...{ - privateKey: fromByteArray(defaultConfig?.privateKey ?? new Uint8Array(0)), - publicKey: fromByteArray(defaultConfig?.publicKey ?? new Uint8Array(0)), - adminKey: [ - fromByteArray(defaultConfig?.adminKey?.at(0) ?? new Uint8Array(0)), - fromByteArray(defaultConfig?.adminKey?.at(1) ?? new Uint8Array(0)), - fromByteArray(defaultConfig?.adminKey?.at(2) ?? new Uint8Array(0)), - ], - }, - }; - - const effectiveConfig = getEffectiveConfig("security"); - const formValues = { - ...effectiveConfig, - ...{ - privateKey: fromByteArray( - effectiveConfig?.privateKey ?? new Uint8Array(0), - ), - publicKey: fromByteArray(effectiveConfig?.publicKey ?? new Uint8Array(0)), - adminKey: [ - fromByteArray(effectiveConfig?.adminKey?.at(0) ?? new Uint8Array(0)), - fromByteArray(effectiveConfig?.adminKey?.at(1) ?? new Uint8Array(0)), - fromByteArray(effectiveConfig?.adminKey?.at(2) ?? new Uint8Array(0)), - ], - }, - }; - - const formMethods = useForm({ - mode: "onChange", - defaultValues: defaultValues as DefaultValues, - resolver: createZodResolver(RawSecuritySchema), - shouldFocusError: false, - resetOptions: { keepDefaultValues: true }, - values: formValues as RawSecurity, - }); - const { setValue, trigger, handleSubmit, formState } = formMethods; - - useEffect(() => { - onFormInit?.(formMethods); - }, [onFormInit, formMethods]); - - const [privateKeyDialogOpen, setPrivateKeyDialogOpen] = - useState(false); - const [managedModeDialogOpen, setManagedModeDialogOpen] = - useState(false); - - const onSubmit = (data: RawSecurity) => { - if (!formState.isReady) { - return; - } - - const payload: ParsedSecurity = { - ...data, - privateKey: toByteArray(data.privateKey), - publicKey: toByteArray(data.publicKey), - adminKey: [ - toByteArray(data.adminKey.at(0) ?? ""), - toByteArray(data.adminKey.at(1) ?? ""), - toByteArray(data.adminKey.at(2) ?? ""), - ], - }; - - if (deepCompareConfig(config.security, payload, true)) { - removeChange({ type: "config", variant: "security" }); - return; - } - - setChange( - { type: "config", variant: "security" }, - payload, - config.security, - ); - }; - - const pkiRegenerate = () => { - const privateKey = getX25519PrivateKey(); - updatePublicKey(fromByteArray(privateKey)); - }; - - const updatePublicKey = async (privateKey: string) => { - try { - const publicKey = fromByteArray( - getX25519PublicKey(toByteArray(privateKey)), - ); - setValue("privateKey", privateKey, { shouldDirty: true }); - setValue("publicKey", publicKey, { shouldDirty: true }); - - setPrivateKeyDialogOpen(false); - } catch (_e) { - setValue("privateKey", privateKey, { shouldDirty: true }); - } - const valid = await trigger(["privateKey", "publicKey"]); - if (valid) { - handleSubmit(onSubmit)(); // manually invoke form submit - } - }; - - const bits = [ - { - text: t("security.256bit"), - value: "32", - key: "bit256", - }, - ]; - - return ( - <> - - propMethods={formMethods} - onSubmit={onSubmit} - fieldGroups={[ - { - label: t("security.title"), - description: t("security.description"), - fields: [ - { - type: "passwordGenerator", - id: "pskInput", - name: "privateKey", - label: t("security.privateKey.label"), - description: t("security.privateKey.description"), - bits, - devicePSKBitCount: 32, - hide: true, - inputChange: (e: React.ChangeEvent) => { - updatePublicKey(e.target.value); - }, - actionButtons: [ - { - text: t("button.generate"), - onClick: () => setPrivateKeyDialogOpen(true), - variant: "success", - }, - { - text: t("button.backupKey"), - onClick: () => setDialogOpen("pkiBackup", true), - variant: "subtle", - }, - ], - properties: { - showCopyButton: true, - showPasswordToggle: true, - }, - }, - { - type: "text", - name: "publicKey", - label: t("security.publicKey.label"), - disabled: true, - description: t("security.publicKey.description"), - properties: { - showCopyButton: true, - }, - }, - ], - }, - { - label: t("security.adminSettings.label"), - description: t("security.adminSettings.description"), - fields: [ - { - type: "passwordGenerator", - name: "adminKey.0", - id: "adminKey0Input", - label: t("security.primaryAdminKey.label"), - description: t("security.primaryAdminKey.description"), - bits, - devicePSKBitCount: 32, - actionButtons: [], - disabledBy: [ - { fieldName: "adminChannelEnabled", invert: true }, - ], - properties: { - showCopyButton: true, - }, - }, - { - type: "passwordGenerator", - name: "adminKey.1", - id: "adminKey1Input", - label: t("security.secondaryAdminKey.label"), - description: t("security.secondaryAdminKey.description"), - bits, - devicePSKBitCount: 32, - actionButtons: [], - disabledBy: [ - { fieldName: "adminChannelEnabled", invert: true }, - ], - properties: { - showCopyButton: true, - }, - }, - { - type: "passwordGenerator", - name: "adminKey.2", - id: "adminKey2Input", - label: t("security.tertiaryAdminKey.label"), - description: t("security.tertiaryAdminKey.description"), - bits, - devicePSKBitCount: 32, - actionButtons: [], - disabledBy: [ - { fieldName: "adminChannelEnabled", invert: true }, - ], - properties: { - showCopyButton: true, - }, - }, - { - type: "toggle", - name: "isManaged", - label: t("security.managed.label"), - description: t("security.managed.description"), - inputChange: (checked) => { - if (checked) { - setManagedModeDialogOpen(true); - } - - setValue("isManaged", false); - }, - }, - { - type: "toggle", - name: "adminChannelEnabled", - label: t("security.adminChannelEnabled.label"), - description: t("security.adminChannelEnabled.description"), - }, - ], - }, - { - label: t("security.loggingSettings.label"), - description: t("security.loggingSettings.description"), - fields: [ - { - type: "toggle", - name: "debugLogApiEnabled", - label: t("security.enableDebugLogApi.label"), - description: t("security.enableDebugLogApi.description"), - }, - { - type: "toggle", - name: "serialEnabled", - label: t("security.serialOutputEnabled.label"), - description: t("security.serialOutputEnabled.description"), - }, - ], - }, - ]} - /> - setPrivateKeyDialogOpen((prev) => !prev)} - onSubmit={pkiRegenerate} - /> - - setManagedModeDialogOpen((prev) => !prev)} - onSubmit={() => { - setValue("isManaged", true); - setManagedModeDialogOpen(false); - }} - /> - - ); -}; diff --git a/packages/web/src/components/PageComponents/Settings/User.tsx b/packages/web/src/components/PageComponents/Settings/User.tsx deleted file mode 100644 index c4f094f10..000000000 --- a/packages/web/src/components/PageComponents/Settings/User.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { - type UserValidation, - UserValidationSchema, -} from "@app/validation/config/user.ts"; -import { create } from "@bufbuild/protobuf"; -import { - DynamicForm, - type DynamicFormFormInit, -} from "@components/Form/DynamicForm.tsx"; -import { useDevice, useNodeDB } from "@core/stores"; -import { Protobuf } from "@meshtastic/core"; -import { useTranslation } from "react-i18next"; - -interface UserConfigProps { - onFormInit: DynamicFormFormInit; -} - -export const User = ({ onFormInit }: UserConfigProps) => { - const { hardware, getChange, connection } = useDevice(); - const { getNode } = useNodeDB(); - const { t } = useTranslation("config"); - - const myNode = getNode(hardware.myNodeNum); - const defaultUser = myNode?.user ?? { - id: "", - longName: "", - shortName: "", - isLicensed: false, - }; - - // Get working copy from change registry - const workingUser = getChange({ type: "user" }) as - | Protobuf.Mesh.User - | undefined; - - const effectiveUser = workingUser ?? defaultUser; - - const onSubmit = (data: UserValidation) => { - connection?.setOwner( - create(Protobuf.Mesh.UserSchema, { - ...data, - }), - ); - }; - - return ( - - onSubmit={onSubmit} - onFormInit={onFormInit} - validationSchema={UserValidationSchema} - defaultValues={{ - longName: defaultUser.longName, - shortName: defaultUser.shortName, - isLicensed: defaultUser.isLicensed, - isUnmessageable: false, - }} - values={{ - longName: effectiveUser.longName, - shortName: effectiveUser.shortName, - isLicensed: effectiveUser.isLicensed, - isUnmessageable: false, - }} - fieldGroups={[ - { - label: t("user.title"), - description: t("user.description"), - fields: [ - { - type: "text", - name: "longName", - label: t("user.longName.label"), - description: t("user.longName.description"), - properties: { - fieldLength: { - min: 1, - max: 40, - showCharacterCount: true, - }, - }, - }, - { - type: "text", - name: "shortName", - label: t("user.shortName.label"), - description: t("user.shortName.description"), - properties: { - fieldLength: { - min: 2, - max: 4, - showCharacterCount: true, - }, - }, - }, - { - type: "toggle", - name: "isUnmessageable", - label: t("user.isUnmessageable.label"), - description: t("user.isUnmessageable.description"), - }, - { - type: "toggle", - name: "isLicensed", - label: t("user.isLicensed.label"), - description: t("user.isLicensed.description"), - }, - ], - }, - ]} - /> - ); -}; diff --git a/packages/web/src/components/PageLayout.tsx b/packages/web/src/components/PageLayout.tsx deleted file mode 100644 index b2bc76c25..000000000 --- a/packages/web/src/components/PageLayout.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { ErrorPage } from "@components/UI/ErrorPage.tsx"; -import Footer from "@components/UI/Footer.tsx"; -import { Spinner } from "@components/UI/Spinner.tsx"; -import { cn } from "@core/utils/cn.ts"; -import type { LucideIcon } from "lucide-react"; -import type React from "react"; -import { ErrorBoundary } from "react-error-boundary"; - -export interface ActionItem { - key: string; - icon?: LucideIcon; - iconClasses?: string; - onClick: () => void; - disabled?: boolean; - isLoading?: boolean; - ariaLabel?: string; - label?: string; - className?: string; -} - -export interface PageLayoutProps { - label: string; - actions?: ActionItem[]; - children: React.ReactNode; - leftBar?: React.ReactNode; - rightBar?: React.ReactNode; - noPadding?: boolean; - leftBarClassName?: string; - rightBarClassName?: string; - topBarClassName?: string; - contentClassName?: string; -} - -export const PageLayout = ({ - label, - actions, - children, - leftBar, - rightBar, - noPadding, - leftBarClassName, - rightBarClassName, - topBarClassName, - contentClassName, -}: PageLayoutProps) => { - return ( - -
    - {/* Left Sidebar */} - {leftBar && ( - - )} - -
    - {/* Header */} -
    - {/* Header Content */} -
    - - {label} - -
    - {actions?.map((action) => { - return ( - - ); - })} -
    -
    -
    - -
    - {children} -
    -
    -
    - - {/* Right Sidebar */} - {rightBar && ( - - )} -
    -
    - ); -}; diff --git a/packages/web/src/components/Sidebar.tsx b/packages/web/src/components/Sidebar.tsx deleted file mode 100644 index a5cf71a09..000000000 --- a/packages/web/src/components/Sidebar.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import { useFirstSavedConnection } from "@app/core/stores/deviceStore/selectors.ts"; -import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx"; -import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx"; -import { Spinner } from "@components/UI/Spinner.tsx"; -import { Subtle } from "@components/UI/Typography/Subtle.tsx"; -import { - type Page, - useActiveConnection, - useAppStore, - useDefaultConnection, - useDevice, - useNodeDB, - useSidebar, -} from "@core/stores"; -import { cn } from "@core/utils/cn.ts"; -import { useLocation, useNavigate } from "@tanstack/react-router"; -import { - CircleChevronLeft, - type LucideIcon, - MapIcon, - MessageSquareIcon, - SettingsIcon, - UsersIcon, -} from "lucide-react"; -import type React from "react"; -import { useEffect, useState, useTransition } from "react"; -import { useTranslation } from "react-i18next"; -import { DeviceInfoPanel } from "./DeviceInfoPanel.tsx"; - -export interface SidebarProps { - children?: React.ReactNode; -} - -interface NavLink { - name: string; - icon: LucideIcon; - page: Page; - count?: number; -} - -const CollapseToggleButton = () => { - const { isCollapsed, toggleSidebar } = useSidebar(); - const { t } = useTranslation("ui"); - const buttonLabel = isCollapsed - ? t("sidebar.collapseToggle.button.open") - : t("sidebar.collapseToggle.button.close"); - - return ( - - ); -}; - -export const Sidebar = ({ children }: SidebarProps) => { - const { hardware, metadata, unreadCounts, setDialogOpen } = useDevice(); - const { getNode, getNodesLength } = useNodeDB(); - const { setCommandPaletteOpen } = useAppStore(); - const myNode = getNode(hardware.myNodeNum); - const { isCollapsed } = useSidebar(); - const { t } = useTranslation("ui"); - const navigate = useNavigate({ from: "/" }); - - // Get the active connection from selector (connected > default > first) - const activeConnection = - useActiveConnection() || - // biome-ignore lint/correctness/useHookAtTopLevel: not a react hook - useDefaultConnection() || - // biome-ignore lint/correctness/useHookAtTopLevel: not a hook - useFirstSavedConnection(); - - const pathname = useLocation({ - select: (location) => location.pathname.replace(/^\//, ""), - }); - - const myMetadata = metadata.get(0); - - const numUnread = [...unreadCounts.values()].reduce((sum, v) => sum + v, 0); - - const [displayedNodeCount, setDisplayedNodeCount] = useState(() => - Math.max(getNodesLength() - 1, 0), - ); - - const [_, startNodeCountTransition] = useTransition(); - - const currentNodeCountValue = Math.max(getNodesLength() - 1, 0); - - useEffect(() => { - if (currentNodeCountValue !== displayedNodeCount) { - startNodeCountTransition(() => { - setDisplayedNodeCount(currentNodeCountValue); - }); - } - }, [currentNodeCountValue, displayedNodeCount]); - - const pages: NavLink[] = [ - { - name: t("navigation.messages"), - icon: MessageSquareIcon, - page: "messages", - count: numUnread ? numUnread : undefined, - }, - { name: t("navigation.map"), icon: MapIcon, page: "map" }, - { - name: t("navigation.settings"), - icon: SettingsIcon, - page: "settings", - }, - { - name: `${t("navigation.nodes")} (${displayedNodeCount})`, - icon: UsersIcon, - page: "nodes", - }, - ]; - - return ( -
    - - -
    - {t("app.logo")} -

    - {t("app.title")} -

    -
    - - - {pages.map((link) => { - return ( - { - if (myNode !== undefined) { - navigate({ to: `/${link.page}` }); - } - }} - active={link.page === pathname} - disabled={myNode === undefined} - /> - ); - })} - - -
    - {children} -
    - -
    - {myNode === undefined ? ( -
    - - - {t("loading")} - -
    - ) : ( - setCommandPaletteOpen(true)} - setDialogOpen={() => setDialogOpen("deviceName", true)} - user={myNode.user} - firmwareVersion={ - myMetadata?.firmwareVersion ?? t("unknown.notAvailable") - } - deviceMetrics={{ - batteryLevel: myNode.deviceMetrics?.batteryLevel, - voltage: - typeof myNode.deviceMetrics?.voltage === "number" - ? Math.abs(myNode.deviceMetrics?.voltage) - : undefined, - }} - connectionStatus={activeConnection?.status} - connectionName={activeConnection?.name} - /> - )} -
    -
    - ); -}; diff --git a/packages/web/src/components/ThemeSwitcher.tsx b/packages/web/src/components/ThemeSwitcher.tsx deleted file mode 100644 index e71533d54..000000000 --- a/packages/web/src/components/ThemeSwitcher.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Monitor, Moon, Sun } from "lucide-react"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "../core/hooks/useTheme.ts"; -import { useToggleVisibility } from "../core/hooks/useToggleVisiblility.ts"; -import { cn } from "../core/utils/cn.ts"; -import { Button } from "./UI/Button.tsx"; -import { Subtle } from "./UI/Typography/Subtle.tsx"; - -type ThemePreference = "light" | "dark" | "system"; - -interface ThemeSwitcherProps { - className?: string; - disableHover?: boolean; -} - -const TOOLTIP_TIMEOUT = 2000; // 2 seconds - -export default function ThemeSwitcher({ - className: passedClassName = "", - disableHover = false, -}: ThemeSwitcherProps) { - const [showTooltip, toggleShowTooltip] = useToggleVisibility({ - timeout: TOOLTIP_TIMEOUT, - }); - - const { preference, setPreference } = useTheme(); - const { t } = useTranslation("ui"); - - const iconBaseClass = - "size-4 flex-shrink-0 text-gray-500 dark:text-gray-400 transition-colors duration-150"; - const iconHoverClass = !disableHover - ? "group-hover:text-gray-700 dark:group-hover:text-gray-200" - : ""; - const combinedIconClass = cn(iconBaseClass, iconHoverClass); - - const themeIcons = { - light: , - dark: , - system: , - }; - - const toggleTheme = () => { - const preferences: ThemePreference[] = ["light", "dark", "system"]; - const currentIndex = preferences.indexOf(preference); - const nextPreference = - preferences[(currentIndex + 1) % preferences.length] ?? "system"; - setPreference(nextPreference); - toggleShowTooltip(); - }; - - const preferenceDisplayMap: Record = { - light: t("theme.light"), - dark: t("theme.dark"), - system: t("theme.system"), - }; - - const currentDisplayPreference = preferenceDisplayMap[preference]; - - return ( - - ); -} diff --git a/packages/web/src/components/UI/Accordion.tsx b/packages/web/src/components/UI/Accordion.tsx deleted file mode 100644 index e4d54cec1..000000000 --- a/packages/web/src/components/UI/Accordion.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; -import * as AccordionPrimitive from "@radix-ui/react-accordion"; -import { ChevronDownIcon } from "lucide-react"; -import { type ComponentRef, forwardRef } from "react"; - -export const Accordion = AccordionPrimitive.Root; - -export const AccordionHeader = AccordionPrimitive.Header; - -export const AccordionItem = AccordionPrimitive.Item; - -export const AccordionTrigger = forwardRef< - ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - {props.children} - - -)); - -export const AccordionContent = forwardRef< - ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); diff --git a/packages/web/src/components/UI/Avatar.tsx b/packages/web/src/components/UI/Avatar.tsx deleted file mode 100644 index 209a69eef..000000000 --- a/packages/web/src/components/UI/Avatar.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { useNodeDB } from "@app/core/stores"; -import { getColorFromNodeNum, isLightColor } from "@app/core/utils/color"; -import { - Tooltip, - TooltipArrow, - TooltipContent, - TooltipPortal, - TooltipProvider, - TooltipTrigger, -} from "@components/UI/Tooltip.tsx"; -import { cn } from "@core/utils/cn.ts"; -import { LockKeyholeOpenIcon, StarIcon } from "lucide-react"; -import { useTranslation } from "react-i18next"; - -interface AvatarProps { - nodeNum: number; - size?: "sm" | "lg"; - className?: string; - showError?: boolean; - showFavorite?: boolean; -} - -export const Avatar = ({ - nodeNum, - size = "sm", - showError = false, - showFavorite = false, - className, -}: AvatarProps) => { - const { t } = useTranslation(); - const { getNode } = useNodeDB(); - const node = getNode(nodeNum); - - if (!nodeNum) { - return null; - } - - const sizes = { - sm: "size-10 text-xs font-light", - lg: "size-16 text-lg", - }; - - const shortName = node?.user?.shortName ?? ""; - const longName = node?.user?.longName ?? ""; - const displayName = shortName || longName; - - const bgColor = getColorFromNodeNum(nodeNum); - const isLight = isLightColor(bgColor); - const textColor = isLight ? "#000000" : "#FFFFFF"; - const initials = displayName.slice(0, 4) || t("unknown.shortName"); - - return ( -
    - {showFavorite ? ( - - - - - - - {t("nodeDetail.favorite.label", { ns: "nodes" })} - - - - - - ) : null} - {showError ? ( - - - - - - - {t("nodeDetail.error.label", { ns: "nodes" })} - - - - - - ) : null} -

    {initials}

    -
    - ); -}; diff --git a/packages/web/src/components/UI/Badge.tsx b/packages/web/src/components/UI/Badge.tsx deleted file mode 100644 index df7ee9321..000000000 --- a/packages/web/src/components/UI/Badge.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; -import { cva, type VariantProps } from "class-variance-authority"; -import type * as React from "react"; - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-slate-900 text-white hover:bg-slate-900/80 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/80", - secondary: - "border-transparent bg-slate-100 text-slate-900 hover:bg-slate-100/80 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-800/80", - destructive: - "border-transparent bg-red-500 text-white hover:bg-red-500/80 dark:bg-red-900 dark:text-red-50 dark:hover:bg-red-900/80", - outline: "text-slate-900 dark:text-slate-100", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
    - ); -} - -export { Badge, badgeVariants }; diff --git a/packages/web/src/components/UI/Label.tsx b/packages/web/src/components/UI/Label.tsx deleted file mode 100644 index 3751a0aef..000000000 --- a/packages/web/src/components/UI/Label.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import * as React from "react"; - -const Label = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -Label.displayName = LabelPrimitive.Root.displayName; - -export { Label }; diff --git a/packages/web/src/components/UI/Select.tsx b/packages/web/src/components/UI/Select.tsx deleted file mode 100644 index 6afec979f..000000000 --- a/packages/web/src/components/UI/Select.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; -import * as SelectPrimitive from "@radix-ui/react-select"; -import { Check, ChevronDown } from "lucide-react"; -import * as React from "react"; - -const Select = SelectPrimitive.Root; - -const SelectGroup = SelectPrimitive.Group; - -const SelectValue = SelectPrimitive.Value; - -const SelectTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - {children} - - -)); -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; - -const SelectContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - -)); -SelectContent.displayName = SelectPrimitive.Content.displayName; - -const SelectLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SelectLabel.displayName = SelectPrimitive.Label.displayName; - -const SelectItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - - {children} - -)); -SelectItem.displayName = SelectPrimitive.Item.displayName; - -const SelectSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -SelectSeparator.displayName = SelectPrimitive.Separator.displayName; - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectSeparator, - SelectTrigger, - SelectValue, -}; diff --git a/packages/web/src/components/UI/Separator.tsx b/packages/web/src/components/UI/Separator.tsx deleted file mode 100644 index 503d5133e..000000000 --- a/packages/web/src/components/UI/Separator.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; -import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import * as React from "react"; - -const Separator = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef ->( - ( - { className, orientation = "horizontal", decorative = true, ...props }, - ref, - ) => ( - - ), -); -Separator.displayName = SeparatorPrimitive.Root.displayName; - -export { Separator }; diff --git a/packages/web/src/components/UI/Sidebar/SidebarButton.tsx b/packages/web/src/components/UI/Sidebar/SidebarButton.tsx deleted file mode 100644 index ea1f8d1e9..000000000 --- a/packages/web/src/components/UI/Sidebar/SidebarButton.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Button } from "@components/UI/Button.tsx"; -import { useSidebar } from "@core/stores"; -import { cn } from "@core/utils/cn.ts"; -import type { LucideIcon } from "lucide-react"; -import type React from "react"; - -export interface SidebarButtonProps { - label: string; - count?: number; - active?: boolean; - Icon?: LucideIcon; - children?: React.ReactNode; - onClick?: () => void; - disabled?: boolean; - preventCollapse?: boolean; - isDirty?: boolean; -} - -export const SidebarButton = ({ - label, - active, - Icon, - count, - children, - onClick, - disabled = false, - preventCollapse = false, - isDirty, -}: SidebarButtonProps) => { - const { isCollapsed: isSidebarCollapsed } = useSidebar(); - const isButtonCollapsed = isSidebarCollapsed && !preventCollapse; - - return ( - - ); -}; diff --git a/packages/web/src/components/UI/Sidebar/SidebarSection.tsx b/packages/web/src/components/UI/Sidebar/SidebarSection.tsx deleted file mode 100644 index e43c46bb3..000000000 --- a/packages/web/src/components/UI/Sidebar/SidebarSection.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Heading } from "@components/UI/Typography/Heading.tsx"; -import { useSidebar } from "@core/stores"; -import { cn } from "@core/utils/cn.ts"; -import type React from "react"; - -interface SidebarSectionProps { - label: string; - children: React.ReactNode; - className?: string; -} - -export const SidebarSection = ({ - label, - children, - className, -}: SidebarSectionProps) => { - const { isCollapsed } = useSidebar(); - return ( -
    - - {label} - - -
    {children}
    -
    - ); -}; diff --git a/packages/web/src/components/UI/Spinner.tsx b/packages/web/src/components/UI/Spinner.tsx deleted file mode 100644 index 84e1f6833..000000000 --- a/packages/web/src/components/UI/Spinner.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { cn } from "../../core/utils/cn.ts"; - -interface SpinnerProps extends React.HTMLAttributes { - size?: "sm" | "md" | "lg"; -} - -const sizeClasses = { - sm: "h-4 w-4", - md: "h-8 w-8", - lg: "h-12 w-12", -}; - -export function Spinner({ className, size = "md", ...props }: SpinnerProps) { - return ( -
    - - - - - - - - - - -
    - ); -} diff --git a/packages/web/src/components/UI/Switch.tsx b/packages/web/src/components/UI/Switch.tsx deleted file mode 100644 index d99e2166b..000000000 --- a/packages/web/src/components/UI/Switch.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; -import * as SwitchPrimitives from "@radix-ui/react-switch"; -import * as React from "react"; - -const Switch = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)); - -Switch.displayName = SwitchPrimitives.Root.displayName; - -export { Switch }; diff --git a/packages/web/src/components/UI/ToggleGroup.tsx b/packages/web/src/components/UI/ToggleGroup.tsx deleted file mode 100644 index d35d41a45..000000000 --- a/packages/web/src/components/UI/ToggleGroup.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; -import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; -import * as React from "react"; - -const toggleGroupItemClasses = [ - "flex flex-1 h-10 items-center justify-center first:rounded-l last:rounded-r ", - "bg-slate-100", - "dark:bg-slate-800", - "data-[state=on]:bg-slate-600 data-[state=on]:text-white", - "data-[state=on]:dark:bg-slate-950 data-[state=on]:text-white data-[state=on]:dark:text-slate-200", - "data-[state=on]:hover:bg-slate-700 hover:bg-slate-700 hover:text-white hover:z-10 hover:shadow-[0_0_1px_2px] hover:outline-1 hover:outline-slate-700 hover:shadow-white/10", - "data-[state=on]:dark:hover:bg-slate-700 dark:hover:bg-slate-700 dark:hover:text-slate-200 dark:hover:outline-slate-700 dark:hover:shadow-black/20", - "disabled:text-slate-300 disabled:hover:bg-slate-100 disabled:hover:outline-none hover:shadow-none disabled:dark:text-slate-600 disabled:dark:hover:bg-slate-800 disabled:dark:hover:outline-none disabled:shadow-none", -]; - -const ToggleGroup = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; - -const ToggleGroupItem = React.forwardRef< - React.ComponentRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - {children} - -)); -ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; - -export { ToggleGroup, ToggleGroupItem }; diff --git a/packages/web/src/components/UI/Typography/Blockquote.tsx b/packages/web/src/components/UI/Typography/Blockquote.tsx deleted file mode 100644 index cdac7ca83..000000000 --- a/packages/web/src/components/UI/Typography/Blockquote.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export interface BlockquoteProps { - children: React.ReactNode; -} - -export const BlockQuote = ({ children }: BlockquoteProps) => ( -
    - {children} -
    -); diff --git a/packages/web/src/components/UI/Typography/Code.tsx b/packages/web/src/components/UI/Typography/Code.tsx deleted file mode 100644 index b6102b7af..000000000 --- a/packages/web/src/components/UI/Typography/Code.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export interface CodeProps { - children: React.ReactNode; -} - -export const Code = ({ children }: CodeProps) => ( - - {children} - -); diff --git a/packages/web/src/components/UI/Typography/Heading.tsx b/packages/web/src/components/UI/Typography/Heading.tsx deleted file mode 100644 index fb4dad8ba..000000000 --- a/packages/web/src/components/UI/Typography/Heading.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import type React from "react"; - -const headingStyles = { - h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl", - h2: "scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700", - h3: "scroll-m-20 text-lg font-semibold tracking-tight", - h4: "scroll-m-20 text-xl font-semibold tracking-tight", - h5: "scroll-m-20 text-lg font-medium tracking-tight", -}; - -interface HeadingProps { - as?: "h1" | "h2" | "h3" | "h4" | "h5"; - children: React.ReactNode; - className?: string; -} - -export const Heading = ({ - as: Component = "h1", - children, - className = "", - ...props -}: HeadingProps) => { - const baseStyles = headingStyles[Component] || headingStyles.h1; - - return ( - - {children} - - ); -}; diff --git a/packages/web/src/components/UI/Typography/Link.tsx b/packages/web/src/components/UI/Typography/Link.tsx deleted file mode 100644 index da447f539..000000000 --- a/packages/web/src/components/UI/Typography/Link.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; -import { - Link as RouterLink, - type LinkProps as RouterLinkProps, -} from "@tanstack/react-router"; - -export interface LinkProps extends RouterLinkProps { - href: string; - children?: - | React.ReactNode - | ((state: { - isActive: boolean; - isTransitioning: boolean; - }) => React.ReactNode); - className?: string; -} - -export const Link = ({ href, children, className }: LinkProps) => ( - - {children} - -); diff --git a/packages/web/src/components/UI/Typography/P.tsx b/packages/web/src/components/UI/Typography/P.tsx deleted file mode 100644 index 5e492f60c..000000000 --- a/packages/web/src/components/UI/Typography/P.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; - -export interface PProps { - children: React.ReactNode; - className?: string; -} - -export const P = ({ children, className }: PProps) => ( -

    {children}

    -); diff --git a/packages/web/src/components/UI/Typography/Subtle.tsx b/packages/web/src/components/UI/Typography/Subtle.tsx deleted file mode 100644 index 6a2f75689..000000000 --- a/packages/web/src/components/UI/Typography/Subtle.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { cn } from "@core/utils/cn.ts"; - -export interface SubtleProps { - className?: string; - children: React.ReactNode; -} - -export const Subtle = ({ className, children }: SubtleProps) => ( -

    - {children} -

    -); diff --git a/packages/web/src/components/generic/Blur.tsx b/packages/web/src/components/generic/Blur.tsx deleted file mode 100644 index fa6f9b814..000000000 --- a/packages/web/src/components/generic/Blur.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export const Blur = () => { - return ( -