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