Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion src/components/ConnectModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ export function ConnectModal({
<option value="odd">Odd</option>
</Select>
</div>


<div className="col-span-2">
<div>
<label className="block text-sm font-medium mb-2">Flow Control</label>
<Select
value={serialConfig.flowControl}
Expand All @@ -164,6 +165,17 @@ export function ConnectModal({
<option value="hardware">Hardware (RTS/CTS)</option>
</Select>
</div>

<div>
<label className="block text-sm font-medium mb-2">Encoding</label>
<Select
value={serialConfig.encoding}
onChange={(e) => setSerialConfig(prev => ({ ...prev, encoding: e.target.value as 'ascii' | 'cobs-f32' }))}
>
<option value="ascii">Space-separated base-10</option>
<option value="cobs-f32">COBS float32</option>
</Select>
</div>
</div>

<div className="pt-4 border-t border-gray-200 dark:border-neutral-700">
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/useDataConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
150 changes: 110 additions & 40 deletions src/hooks/useSerial.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }))
Expand Down
32 changes: 32 additions & 0 deletions src/utils/cobsDecoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export class COBSDecoderError extends Error {
constructor(message?: string) {
super(message);
this.name = "COBSDecoderError";
}
}


export function decodeCOBS(data: Array<number>) {
const output: Array<number> = []
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)
}