From 68d0b32bf9f9e80d15c95d0caa10047fbed0204d Mon Sep 17 00:00:00 2001 From: Sander Date: Thu, 8 Jan 2026 15:10:53 +0100 Subject: [PATCH] Fixes pixel shift and MQTT connection handling - Ensures pixel shift uses rounded and parsed number values for accurate shifts. - Modifies MQTT connection to ensure the clientId is properly set. - Introduces utilities for transforming unknown values to boolean and number types. - Adds `pins` data to the board connection state. --- .../electron-app/src/main/board-connection.ts | 8 +++- packages/mqtt-provider/src/stores/mqtt.ts | 25 ++++++------ .../src/_utils/transformUnknownValues.ts | 39 ++++++++++++------- packages/runtime/src/pixel/pixel.ts | 11 +++--- 4 files changed, 49 insertions(+), 34 deletions(-) diff --git a/apps/electron-app/src/main/board-connection.ts b/apps/electron-app/src/main/board-connection.ts index df26643a..57634709 100644 --- a/apps/electron-app/src/main/board-connection.ts +++ b/apps/electron-app/src/main/board-connection.ts @@ -9,7 +9,7 @@ import { import type { Edge, Node } from '@xyflow/react'; import { fork, ChildProcess } from 'child_process'; import { sendMessageToRenderer } from './window'; -import { Board, IpcResponse, UploadedCodeMessage } from '../common/types'; +import { Board, IpcResponse, UploadedCodeMessage, Pin } from '../common/types'; import { getRandomMessage } from '../common/messages'; import log from 'electron-log/node'; import { existsSync } from 'fs'; @@ -30,6 +30,7 @@ const ipRegex = new RegExp( let runnerProcess: ChildProcess | undefined; let lastUsedPinsHash: string | null = null; let lastFlow: { nodes: Node[]; edges: Edge[]; ip?: string } | null = null; +let currentPins: Pin[] | undefined = undefined; /** * Gets the current runner process @@ -45,6 +46,7 @@ export async function killRunnerProcess() { runnerProcess?.kill('SIGKILL'); runnerProcess = undefined; setConnectedPort(undefined); + currentPins = undefined; await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for the process to die } @@ -146,11 +148,13 @@ export function getCurrentConnectionState(): Board | null { type: 'ready', port: connectedPort.path, message: 'Board connected', + pins: currentPins, }; } else { // Runner process died, clear the connection log.debug('[STATE] ', 'Runner process died, clearing connection'); setConnectedPort(undefined); + currentPins = undefined; return null; } } else if (connectedPort && !runnerProcess) { @@ -160,6 +164,7 @@ export function getCurrentConnectionState(): Board | null { type: 'ready', port: connectedPort.path, message: 'Reconnecting to board', + pins: currentPins, }; } else { // No connection state @@ -428,6 +433,7 @@ async function checkBoardOnPort(port: Pick, board: BoardName) break; case 'ready': log.debug(`[RUNNER] <${data.type}>`, runnerProcess?.pid, timer.duration); + currentPins = data.pins; sendMessageToRenderer('ipc-board', { success: true, data: { type: 'ready', port: port.path, pins: data.pins }, diff --git a/packages/mqtt-provider/src/stores/mqtt.ts b/packages/mqtt-provider/src/stores/mqtt.ts index 91cf34a4..4effc6e9 100644 --- a/packages/mqtt-provider/src/stores/mqtt.ts +++ b/packages/mqtt-provider/src/stores/mqtt.ts @@ -290,7 +290,7 @@ export const useMqttStore = create((set, get) => { protocol: (protocol ?? 'wss') as 'ws' | 'wss', host, port: port ? parseInt(String(port), 10) : (protocol ?? 'wss') === 'wss' ? 8883 : 1883, - path: path ?? '/mqtt', + path: path ? (path.startsWith('/') ? path : `/${path}`) : '/mqtt', }; } catch (error) { throw new Error( @@ -312,31 +312,31 @@ export const useMqttStore = create((set, get) => { const { protocol, host, port, path } = parseMqttUrl(config.url); const clientId = `microflow_${appName}_${config.uniqueId}_${Date.now().toString(36)}`; - // Construct the full URL for WebSocket connections - // mqtt.js requires the URL as the first argument for proper clientId handling - const url = `${protocol}://${host}:${port}${path}`; - console.debug('[MQTT] Parsed URL:', { input: config.url, protocol, host, port, path, - constructedUrl: url, clientId, }); - // Build connection options (without protocol/host/port/path when using URL) + // Build connection options + // For WebSocket connections, use object form to ensure clientId is properly set const connectionOptions: mqtt.IClientOptions = { - username: config.username, - password: config.password, + protocol: protocol === 'wss' ? 'wss' : protocol === 'ws' ? 'ws' : 'wss', + hostname: host, + port: port, + path: path, + ...(config.username && { username: config.username }), + ...(config.password && { password: config.password }), clientId, // connectTimeout: 30000, // 30 seconds // keepalive: 60, // 60 seconds // clean: true, // Start with a clean session // reconnectPeriod: 1000, // Reconnect after 1 second // For WSS connections, ensure proper SSL handling - ...(protocol === 'wss' + ...(protocol === 'wss' || protocol === 'ws' ? { // Allow self-signed certificates (common for public brokers) rejectUnauthorized: false, @@ -355,8 +355,9 @@ export const useMqttStore = create((set, get) => { }, }; - console.debug('[MQTT] ', config, appName, url, connectionOptions); - client = mqtt.connect(url, connectionOptions); + console.debug('[MQTT] ', config, appName, connectionOptions); + // Use object form instead of URL string to ensure clientId is properly set + client = mqtt.connect(connectionOptions); // Handle status messages from other clients const statusHandler = (topic: string, payload: Buffer) => { diff --git a/packages/runtime/src/_utils/transformUnknownValues.ts b/packages/runtime/src/_utils/transformUnknownValues.ts index 26c9f7a9..be497aa5 100644 --- a/packages/runtime/src/_utils/transformUnknownValues.ts +++ b/packages/runtime/src/_utils/transformUnknownValues.ts @@ -1,20 +1,29 @@ export function transformValueToBoolean(value: unknown) { - if (typeof value === 'boolean') return value; - - if (typeof value === 'number') return value > 0; - - const isTruthy = ['1', 'true', 'on', 'yes'].includes(String(value).toLowerCase()); - const isSecretlyPositiveNumber = !Number.isNaN(Number(value)) && Number(value) > 0; - - return isTruthy || isSecretlyPositiveNumber; + switch (typeof value) { + case 'boolean': + return value; + case 'number': + return value > 0; + case 'string': + const isTruthy = ['1', 'true', 'on', 'yes'].includes(String(value).toLowerCase()); + const isSecretlyPositiveNumber = !Number.isNaN(Number(value)) && Number(value) > 0; + return isTruthy || isSecretlyPositiveNumber; + default: + return false; + } } export function transformValueToNumber(value: unknown) { - if (typeof value === 'number') return value; - - if (typeof value === 'boolean') return value ? 1 : 0; - - const parsed = parseFloat(String(value)); - - return Number.isNaN(parsed) ? 0 : parsed; + switch (typeof value) { + case 'number': + return value; + case 'boolean': + return value ? 1 : 0; + case 'string': + const parsed = parseFloat(value); + if (Number.isNaN(parsed)) return transformValueToBoolean(value) ? 1 : 0; + return parsed; + default: + return 0; + } } diff --git a/packages/runtime/src/pixel/pixel.ts b/packages/runtime/src/pixel/pixel.ts index 079795f2..fe302bb0 100644 --- a/packages/runtime/src/pixel/pixel.ts +++ b/packages/runtime/src/pixel/pixel.ts @@ -46,11 +46,13 @@ export class Pixel extends Hardware { } private forward(amount: number = 1) { + const parsedAmount = transformValueToNumber(amount); + const normalizedAmount = Math.round(parsedAmount); const newValue = this.value.map((_color, index) => { - const newIndex = (index - amount + this.data.length) % this.data.length; + const newIndex = (index - normalizedAmount + this.data.length) % this.data.length; return this.value[newIndex]; }); - this.component?.shift(amount, pixel.FORWARD, true); + this.component?.shift(normalizedAmount, pixel.FORWARD, true); this.flush(newValue); } @@ -82,10 +84,7 @@ export class Pixel extends Hardware { if (this.flushTimeout) clearTimeout(this.flushTimeout); this.flushTimeout = setTimeout( () => { - if (!this.component) { - console.warn('[PIXEL] flushing too early'); - return; - } + if (!this.component) return; this.lastFlushTime = Date.now(); this.value = color; this.component?.show();