diff --git a/README.md b/README.md index 866d3b9..868e467 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ Firefox and Safari do not support Web Serial at this time. On desktop, use Chrom **Potential Future Enhancements:** - Session save/load (plot configurations and data) -- Binary protocol support (CBOR/SLIP/COBS) +- More binary protocol support (CBOR/SLIP) (already supports COBS) - Advanced data processing filters (moving average, FFT) - Cursor/crosshair tools for precise measurements - Keyboard navigation and accessibility improvements diff --git a/src/components/ConnectModal.tsx b/src/components/ConnectModal.tsx index 65ab0ee..ffadb11 100644 --- a/src/components/ConnectModal.tsx +++ b/src/components/ConnectModal.tsx @@ -153,8 +153,9 @@ export function ConnectModal({ + -
+
+ +
+ + +
diff --git a/src/hooks/useDataConnection.ts b/src/hooks/useDataConnection.ts index 3526c69..32a1804 100644 --- a/src/hooks/useDataConnection.ts +++ b/src/hooks/useDataConnection.ts @@ -8,6 +8,7 @@ export interface SerialConfig { stopBits: 1 | 2 parity: 'none' | 'even' | 'odd' flowControl: 'none' | 'hardware' + encoding: 'ascii' | 'cobs-f32' } export type ConnectionType = 'serial' | 'generator' @@ -35,7 +36,8 @@ const DEFAULT_SERIAL_CONFIG: SerialConfig = { dataBits: 8, stopBits: 1, parity: 'none', - flowControl: 'none' + flowControl: 'none', + encoding: 'ascii' } export function useDataConnection(onLine: (line: string) => void): UseDataConnection { @@ -65,12 +67,10 @@ export function useDataConnection(onLine: (line: string) => void): UseDataConnec setError(null) try { - // Convert our config to the format useSerial expects // Note: Web Serial API has limited configuration options - await serial.connect(config.baudRate) + await serial.connect(config); setConnectionType('serial') // The actual port configuration would need to be done at the port.open() level - // For now, we'll just use baudRate as useSerial currently does } catch (err) { const message = err instanceof Error ? err.message : 'Failed to connect to serial port' setError(message) diff --git a/src/hooks/useSerial.ts b/src/hooks/useSerial.ts index cc56291..c17f346 100644 --- a/src/hooks/useSerial.ts +++ b/src/hooks/useSerial.ts @@ -1,5 +1,10 @@ import { useCallback, useRef, useState } from 'react' +import type { SerialConfig } from './useDataConnection' + +import { decodeCOBS, COBSDecoderError } from '../utils/cobsDecoder.ts' + + export type BaudRate = 300 | 600 | 1200 | 2400 | 4800 | 9600 | 19200 | 38400 | 57600 | 115200 | 230400 | 460800 | 921600 export interface SerialState { @@ -79,7 +84,7 @@ export function useSerial(): UseSerial { } }, []) // No dependencies - use refs for everything - const connect = useCallback(async (baudRate: number) => { + const connect = useCallback(async (config: SerialConfig) => { if (!state.isSupported) { setState((s) => ({ ...s, error: 'Web Serial not supported in this browser.' })) return @@ -108,54 +113,119 @@ export function useSerial(): UseSerial { } } - await port.open({ baudRate }) + await port.open({ baudRate: config.baudRate }) - const textDecoder = new TextDecoderStream() - const readableClosed = port.readable.pipeTo(textDecoder.writable) - const reader = textDecoder.readable.getReader() - readerRef.current = reader - - const writer = port.writable.getWriter() - writerRef.current = writer - portRef.current = port - - setState((s) => ({ ...s, port, isConnected: true })) + if (config.encoding === 'ascii') { + const textDecoder = new TextDecoderStream() + const readableClosed = port.readable.pipeTo(textDecoder.writable) + const reader = textDecoder.readable.getReader() + readerRef.current = reader - const abort = new AbortController() - abortControllerRef.current = abort + const writer = port.writable.getWriter() + writerRef.current = writer + portRef.current = port - let buffer = '' - ;(async () => { - try { - while (true) { - const { value, done } = await reader.read() - if (done) break - if (value) { - buffer += value - let index - while ((index = buffer.indexOf('\n')) >= 0) { - const line = buffer.slice(0, index).replace(/\r$/, '') - buffer = buffer.slice(index + 1) - lineHandlerRef.current?.(line) + setState((s) => ({ ...s, port, isConnected: true })) + + const abort = new AbortController() + abortControllerRef.current = abort + + let buffer = '' + ;(async () => { + try { + while (true) { + const { value, done } = await reader.read() + if (done) break + if (value) { + buffer += value + let index + while ((index = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, index).replace(/\r$/, '') + buffer = buffer.slice(index + 1) + lineHandlerRef.current?.(line) + } } } - } - } catch { - // ignore if aborted - } finally { - try { - reader.releaseLock() } catch { - // ignore + // ignore if aborted + } finally { + try { + reader.releaseLock() + } catch { + // ignore + } + try { + await readableClosed.catch(() => {}) + } catch { + // ignore + } + setState((s) => ({ ...s, readerLocked: false })) } + })() + } else if (config.encoding === 'cobs-f32') { + const reader = port.readable.getReader() + readerRef.current = reader + + const writer = port.writable.getWriter() + writerRef.current = writer + portRef.current = port + + setState((s) => ({ ...s, port, isConnected: true })) + + const abort = new AbortController() + abortControllerRef.current = abort + + let buffer = []; + (async () => { try { - await readableClosed.catch(() => {}) - } catch { - // ignore + while (true) { + const { value, done } = await reader.read() + if (done) break + if (value) { + for (const byte of value) { + if (byte === 0x00) { + // End of COBS packet + if (buffer.length > 0) { + try { + const decoded = decodeCOBS(buffer) + const converted = new Float32Array(decoded.buffer, 0, Math.floor(decoded.byteLength/4)); + + // disgusting hack because I don't feel like refactoring stuff right now + const line = converted.map((x: number)=>x.toString()).join(' '); + lineHandlerRef.current?.(line); + + } catch (err) { // catch COBS decoding errors + if (err instanceof COBSDecoderError) { + console.log(err); + } else { + throw err; + } + } + + buffer = [] // reset for next packet + } + } else { + buffer.push(byte) + } + } + } + } + } catch (e) { + if (e instanceof Error && e.name === "AbortError") { + // ignore if aborted + } else { + throw e; + } + } finally { + try { + reader.releaseLock() + } catch { + // ignore + } + setState((s) => ({ ...s, readerLocked: false })) } - setState((s) => ({ ...s, readerLocked: false })) - } - })() + })() + } } catch (err) { const message = err instanceof Error ? err.message : 'Failed to connect.' setState((s) => ({ ...s, error: message })) diff --git a/src/utils/cobsDecoder.ts b/src/utils/cobsDecoder.ts new file mode 100644 index 0000000..de19d0c --- /dev/null +++ b/src/utils/cobsDecoder.ts @@ -0,0 +1,32 @@ +export class COBSDecoderError extends Error { + constructor(message?: string) { + super(message); + this.name = "COBSDecoderError"; + } +} + + +export function decodeCOBS(data: Array) { + const output: Array = [] + let i = 0 + + while (i < data.length) { + const code = data[i] + if (code === 0 || i + code > data.length + 1) { + throw new COBSDecoderError("Invalid COBS data") + } + + const nextBlockEnd = i + code + for (let j = i + 1; j < nextBlockEnd && j < data.length; j++) { + output.push(data[j]) + } + + if (code < 0xFF && nextBlockEnd < data.length) { + output.push(0x00) + } + + i = nextBlockEnd + } + + return Uint8Array.from(output) +} \ No newline at end of file