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
1 change: 1 addition & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ function App() {
<Header
connectionState={dataConnection.state}
onConnectSerial={dataConnection.connectSerial}
onConnectHttp={dataConnection.connectHttp}
onConnectGenerator={dataConnection.connectGenerator}
onDisconnect={dataConnection.disconnect}
generatorConfig={dataConnection.generatorConfig}
Expand Down
64 changes: 63 additions & 1 deletion src/components/ConnectModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { XMarkIcon, WifiIcon, SignalIcon } from '@heroicons/react/24/outline'
import { XMarkIcon, WifiIcon, SignalIcon, GlobeAltIcon } from '@heroicons/react/24/outline'
import Button from './ui/Button'
import Input from './ui/Input'
import Select from './ui/Select'
Expand All @@ -12,6 +12,7 @@ interface Props {
isOpen: boolean
onClose: () => void
onConnectSerial: (config: SerialConfig) => Promise<void>
onConnectHttp: (address: string) => Promise<void>
onConnectGenerator: (config: GeneratorConfig) => Promise<void>
isConnecting: boolean
isSupported: boolean
Expand All @@ -24,6 +25,7 @@ export function ConnectModal({
isOpen,
onClose,
onConnectSerial,
onConnectHttp,
onConnectGenerator,
isConnecting,
isSupported,
Expand All @@ -32,6 +34,7 @@ export function ConnectModal({
const [activeTab, setActiveTab] = useState<ConnectionType>('serial')
const [serialConfig, setSerialConfig] = useState<SerialConfig>(DEFAULT_SERIAL_CONFIG)
const [localGeneratorConfig, setLocalGeneratorConfig] = useState<GeneratorConfig>(generatorConfig)
const [httpConfig, setHttpConfig] = useState<{ address: string }>({ address: '' })

if (!isOpen) return null

Expand All @@ -44,6 +47,16 @@ export function ConnectModal({
}
}

const handleHttpConnect = async () => {
try {
await onConnectHttp(httpConfig.address)
onClose()
} catch (e) {
// Keep modal open on error so user can try again
console.log(e)
}
}

const handleGeneratorConnect = async () => {
try {
await onConnectGenerator(localGeneratorConfig)
Expand Down Expand Up @@ -80,6 +93,17 @@ export function ConnectModal({
<WifiIcon className="w-4 h-4" />
Serial Port
</button>
<button
onClick={() => setActiveTab('http')}
className={`flex items-center gap-2 px-6 py-3 font-medium transition-colors ${
activeTab === 'http'
? 'border-b-2 border-blue-500 text-blue-600 dark:text-blue-400'
: 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200'
}`}
>
<GlobeAltIcon className="w-4 h-4" />
HTTP/WS Stream
</button>
<button
onClick={() => setActiveTab('generator')}
className={`flex items-center gap-2 px-6 py-3 font-medium transition-colors ${
Expand Down Expand Up @@ -179,6 +203,44 @@ export function ConnectModal({
</div>
)}

{activeTab === 'http' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2">HTTP Stream or WebSocket URL</label>
<Input
type="url"
className="w-full"
placeholder="http://microcontroller.local:1234"
value={httpConfig.address}
onChange={(e) => setHttpConfig(prev => ({
...prev,
address: e.target.value
}))}
/>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Enter URL for HTTP streaming endpoint or WebSocket server
</p>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Response must have Access-Control-Allow-Origin header set to * or this domain
</p>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{document.location.protocol == 'https:' ? 'Server must use HTTPS/WSS due to browser restrictions as this is an HTTPS page' : ''}
</p>
</div>

<div className="pt-4 border-t border-gray-200 dark:border-neutral-700">
<Button
variant="primary"
onClick={handleHttpConnect}
disabled={!httpConfig.address || isConnecting}
className="w-full"
>
{isConnecting ? 'Connecting...' : 'Connect to HTTP or WebSocket server'}
</Button>
</div>
</div>
)}

{activeTab === 'generator' && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
Expand Down
15 changes: 9 additions & 6 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useState } from 'react'
import { PlayIcon, StopIcon, XCircleIcon, CircleStackIcon } from '@heroicons/react/24/outline'
import Button from './ui/Button'
import { PlayIcon, StopIcon, XCircleIcon } from '@heroicons/react/24/outline'
import ConnectModal from './ConnectModal'
import type { ConnectionState, ConnectionType, SerialConfig } from '../hooks/useDataConnection'
import type { GeneratorConfig } from '../hooks/useSignalGenerator'

interface Props {
connectionState: ConnectionState
onConnectSerial: (config: SerialConfig) => Promise<void>
onConnectHttp: (address: string) => Promise<void>
onConnectGenerator: (config: GeneratorConfig) => Promise<void>
onDisconnect: () => Promise<void>
generatorConfig: GeneratorConfig
Expand Down Expand Up @@ -41,25 +42,26 @@ function getButtonVariant(state: ConnectionState) {
export function Header({
connectionState,
onConnectSerial,
onConnectHttp,
onConnectGenerator,
onDisconnect,
generatorConfig
}: Props) {
const [showModal, setShowModal] = useState(false)

const [showConnectModal, setShowConnectModal] = useState(false)

const handleButtonClick = () => {
if (connectionState.isConnected) {
onDisconnect()
} else {
setShowModal(true)
setShowConnectModal(true)
}
}

return (
<>
<header className="flex items-center justify-between gap-4 py-3 px-4 border-b border-neutral-800">
<div className="flex items-center gap-3">
<CircleStackIcon className="w-6 h-6" />
<div className="text-lg font-semibold tracking-tight">Web Serial Plotter</div>
</div>
<div className="flex items-center gap-4">
Expand All @@ -85,9 +87,10 @@ export function Header({
</header>

<ConnectModal
isOpen={showModal}
onClose={() => setShowModal(false)}
isOpen={showConnectModal}
onClose={() => setShowConnectModal(false)}
onConnectSerial={onConnectSerial}
onConnectHttp={onConnectHttp}
onConnectGenerator={onConnectGenerator}
isConnecting={connectionState.isConnecting}
isSupported={connectionState.isSupported}
Expand Down
75 changes: 59 additions & 16 deletions src/hooks/useDataConnection.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useState, useEffect } from 'react'
import { useSerial } from './useSerial'
import { useSignalGenerator, type GeneratorConfig } from './useSignalGenerator'
import { useHttp } from './useHttp'

export interface SerialConfig {
baudRate: number
Expand All @@ -10,7 +11,7 @@ export interface SerialConfig {
flowControl: 'none' | 'hardware'
}

export type ConnectionType = 'serial' | 'generator'
export type ConnectionType = 'serial' | 'http' | 'generator'

export interface ConnectionState {
type: ConnectionType | null
Expand All @@ -23,6 +24,7 @@ export interface ConnectionState {
export interface UseDataConnection {
state: ConnectionState
connectSerial: (config: SerialConfig) => Promise<void>
connectHttp: (address: string) => Promise<void>
connectGenerator: (config: GeneratorConfig) => Promise<void>
disconnect: () => Promise<void>
write: (data: string) => Promise<void>
Expand All @@ -43,23 +45,26 @@ export function useDataConnection(onLine: (line: string) => void): UseDataConnec
const [isConnecting, setIsConnecting] = useState(false)
const [error, setError] = useState<string | null>(null)


const serial = useSerial()
const generator = useSignalGenerator(onLine)
const http = useHttp()

const state: ConnectionState = {
type: connectionType,
isConnecting: isConnecting || serial.state.isConnecting,
isConnected: serial.state.isConnected || generator.isRunning,
isSupported: serial.state.isSupported,
error: error || serial.state.error
isConnecting: isConnecting || serial.state.isConnecting || http.state.isConnecting,
isConnected: serial.state.isConnected || generator.isRunning || http.state.isConnected,
isSupported: serial.state.isSupported && http.state.isSupported,
error: error || serial.state.error || http.state.error
}


const connectSerial = useCallback(async (config: SerialConfig) => {
if (generator.isRunning) {
generator.stop()
}

if (http.state.isConnected) {
await http.disconnect()
}

setIsConnecting(true)
setError(null)
Expand All @@ -79,12 +84,41 @@ export function useDataConnection(onLine: (line: string) => void): UseDataConnec
} finally {
setIsConnecting(false)
}
}, [serial, generator])
}, [serial, generator, http])

const connectHttp = useCallback(async (address: string) => {
if (serial.state.isConnected) {
await serial.disconnect()
}

if (generator.isRunning) {
generator.stop()
}

setIsConnecting(true)
setError(null)

try {
await http.connect(address)
setConnectionType('http')
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to connect to HTTP stream'
setError(message)
setConnectionType(null)
throw err
} finally {
setIsConnecting(false)
}
}, [serial, generator, http])

const connectGenerator = useCallback(async (config: GeneratorConfig) => {
if (serial.state.isConnected) {
await serial.disconnect()
}

if (http.state.isConnected) {
await http.disconnect()
}

setError(null)
setConnectionType('generator')
Expand All @@ -97,7 +131,7 @@ export function useDataConnection(onLine: (line: string) => void): UseDataConnec
setError(message)
setConnectionType(null)
}
}, [serial, generator])
}, [serial, generator, http])

const disconnect = useCallback(async () => {
setError(null)
Expand All @@ -109,25 +143,34 @@ export function useDataConnection(onLine: (line: string) => void): UseDataConnec
if (generator.isRunning) {
generator.stop()
}

if (http.state.isConnected) {
await http.disconnect()
}

setConnectionType(null)
}, [serial, generator])
}, [serial, generator, http])

// Set up serial line handler
// Set up serial and http line handlers
useEffect(() => {
serial.onLine(onLine)
}, [serial, onLine])
http.onLine(onLine)
}, [serial, http, onLine])

const write = useCallback(async (data: string) => {
if (connectionType !== 'serial' || !serial.state.isConnected) {
throw new Error('Serial port not connected')
if (connectionType === 'serial' && serial.state.isConnected) {
await serial.write(data)
} else if (connectionType === 'http' && http.state.isConnected) {
await http.write(data)
} else {
throw new Error('Not connected')
}
await serial.write(data)
}, [connectionType, serial])
}, [connectionType, serial, http])

return {
state,
connectSerial,
connectHttp,
connectGenerator,
disconnect,
write,
Expand Down
Loading