Skip to content

Commit cf2faef

Browse files
committed
connecting web sockets readings
1 parent c08ce0c commit cf2faef

12 files changed

Lines changed: 270 additions & 104 deletions

File tree

benchmesh-serial-service/frontend/src/ui/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export default function App() {
101101
const waitingForApi = (!instruments || instruments.length === 0) && !!error
102102

103103
return (
104-
<MeasurementProvider>
104+
<MeasurementProvider registry={registry}>
105105
<div style={{ paddingBottom: '48px' }}>
106106
<div className="topbar">
107107
<div className="brand">BenchMesh</div>

benchmesh-serial-service/frontend/src/ui/ClassPods.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ import { GenericLCR } from './classes/LCR/GenericLCR'
99
import { GenericSAL } from './classes/SAL/GenericSAL'
1010
import { UnknownInstrument } from './classes/Unknown/UnknownInstrument'
1111

12-
export function ChannelPod({ path, klass, uiComponent }: { path: string, klass?: string, uiComponent?: string }) {
12+
export function ChannelPod({ path, klass, uiComponent, registry }: { path: string, klass?: string, uiComponent?: string, registry?: any }) {
1313
const upper = (klass || '').toUpperCase()
1414
return (
1515
<div className="channel-card">
1616
<code className="channel-path">{path}</code>
1717
{/* Prefer explicit ui_component from API if provided, otherwise fallback by klass */}
1818
<div className="channel-extra">
19-
{uiComponent === 'GenericPSU' || (uiComponent == null && upper === 'PSU') ? <GenericPSU channelPath={path} /> : null}
20-
{uiComponent === 'GenericDMM' || (uiComponent == null && upper === 'DMM') ? <GenericDMM channelPath={path} /> : null}
21-
{uiComponent === 'GenericELL' || (uiComponent == null && upper === 'ELL') ? <GenericELL channelPath={path} /> : null}
19+
{uiComponent === 'GenericPSU' || (uiComponent == null && upper === 'PSU') ? <GenericPSU channelPath={path} registry={registry} /> : null}
20+
{uiComponent === 'GenericDMM' || (uiComponent == null && upper === 'DMM') ? <GenericDMM channelPath={path} registry={registry} /> : null}
21+
{uiComponent === 'GenericELL' || (uiComponent == null && upper === 'ELL') ? <GenericELL channelPath={path} registry={registry} /> : null}
2222
{uiComponent === 'GenericAWG' || (uiComponent == null && upper === 'AWG') ? <GenericAWG channelPath={path} /> : null}
2323
{uiComponent === 'GenericOSC' || (uiComponent == null && upper === 'OSC') ? <GenericOSC channelPath={path} /> : null}
2424
{uiComponent === 'GenericLCR' || (uiComponent == null && upper === 'LCR') ? <GenericLCR channelPath={path} /> : null}
@@ -29,13 +29,13 @@ export function ChannelPod({ path, klass, uiComponent }: { path: string, klass?:
2929
)
3030
}
3131

32-
function BaseClassPod({ title, channels, klass, uiComponent }: { title: string, channels: string[], klass: string, uiComponent?: string }) {
32+
function BaseClassPod({ title, channels, klass, uiComponent, registry }: { title: string, channels: string[], klass: string, uiComponent?: string, registry?: any }) {
3333
return (
3434
<div className="subcard">
3535
<div className="subcard-title">{title}</div>
3636
<div className="channels">
3737
{channels.map((p) => (
38-
<ChannelPod key={p} path={p} klass={klass} uiComponent={uiComponent} />
38+
<ChannelPod key={p} path={p} klass={klass} uiComponent={uiComponent} registry={registry} />
3939
))}
4040
</div>
4141
</div>
@@ -51,7 +51,7 @@ export const OSCClassPod = ({ channels, uiComponent }: { channels: string[], u
5151
export const LCRClassPod = ({ channels, uiComponent }: { channels: string[], uiComponent?: string }) => <BaseClassPod title={getClassDescription('LCR')} channels={channels} klass={'LCR'} uiComponent={uiComponent} />
5252
export const SALClassPod = ({ channels, uiComponent }: { channels: string[], uiComponent?: string }) => <BaseClassPod title={getClassDescription('SAL')} channels={channels} klass={'SAL'} uiComponent={uiComponent} />
5353

54-
export function ClassPodResolver({ klass, channels, uiComponent }: { klass: string, channels: string[], uiComponent?: string }) {
54+
export function ClassPodResolver({ klass, channels, uiComponent, registry }: { klass: string, channels: string[], uiComponent?: string, registry?: any }) {
5555
const k = (klass || '').toUpperCase()
56-
return <BaseClassPod title={getClassDescription(k)} channels={channels} klass={k} uiComponent={uiComponent} />
56+
return <BaseClassPod title={getClassDescription(k)} channels={channels} klass={k} uiComponent={uiComponent} registry={registry} />
5757
}

benchmesh-serial-service/frontend/src/ui/InstrumentPod.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function InstrumentPod({ instrument, registry }: { instrument: Instrument
3636
<div key={c.class}>
3737
<div>
3838
{/* Render dedicated nested class pod, honoring ui_component from API */}
39-
<ClassPodResolver klass={c.class} channels={c.channels} uiComponent={c.ui_component} />
39+
<ClassPodResolver klass={c.class} channels={c.channels} uiComponent={c.ui_component} registry={registry} />
4040
</div>
4141
</div>
4242
))}

benchmesh-serial-service/frontend/src/ui/MeasurementContext.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ interface MeasurementContextType {
1616
toggleGraph: (sourceId: string) => void
1717
registerSource: (source: MeasurementSource) => void
1818
sources: Map<string, MeasurementSource>
19+
registry: any
1920
}
2021

2122
const MeasurementContext = createContext<MeasurementContextType | undefined>(undefined)
2223

23-
export function MeasurementProvider({ children }: { children: ReactNode }) {
24+
export function MeasurementProvider({ children, registry }: { children: ReactNode, registry?: any }) {
2425
const [selectedForRecord, setSelectedForRecord] = useState<Set<string>>(new Set())
2526
const [selectedForGraph, setSelectedForGraph] = useState<Set<string>>(new Set())
2627
const [sources, setSources] = useState<Map<string, MeasurementSource>>(new Map())
@@ -74,7 +75,8 @@ export function MeasurementProvider({ children }: { children: ReactNode }) {
7475
toggleRecord,
7576
toggleGraph,
7677
registerSource,
77-
sources
78+
sources,
79+
registry: registry || {}
7880
}}>
7981
{children}
8082
</MeasurementContext.Provider>

benchmesh-serial-service/frontend/src/ui/MeasurementStatusBar.tsx

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
import React, { useState, useEffect, useRef } from 'react'
2-
import { useMeasurement } from './MeasurementContext'
2+
import { useMeasurement, MeasurementSource } from './MeasurementContext'
33

44
interface MeasurementRecord {
55
timestamp: Date
66
values: Record<string, number | null>
77
}
88

9+
// Helper function to extract measurement value from registry
10+
function getValueFromRegistry(registry: any, source: MeasurementSource): number | null {
11+
if (!registry || !source) return null
12+
13+
// Parse channelPath like /instruments/DMM/{deviceId}/{channel}
14+
const parts = source.channelPath.split('/').filter(Boolean)
15+
if (parts.length < 4) return null
16+
17+
const klass = parts[1]
18+
const deviceId = parts[2]
19+
const channel = parts[3]
20+
const statusKey = `status_ch${channel}`
21+
22+
const channelData = registry[deviceId]?.[klass]?.[statusKey]
23+
if (!channelData) return null
24+
25+
const rawValue = channelData.measurement1_num
26+
if (rawValue === undefined || rawValue === null) return null
27+
28+
return parseFloat(String(rawValue))
29+
}
30+
931
const FREQUENCIES = [
1032
{ label: '0.5s', value: 500 },
1133
{ label: '1s', value: 1000 },
@@ -26,40 +48,27 @@ export function MeasurementStatusBar() {
2648
const [isRecording, setIsRecording] = useState(false)
2749
const [isGraphing, setIsGraphing] = useState(false)
2850

29-
const { selectedForRecord, selectedForGraph, sources } = useMeasurement()
51+
const { selectedForRecord, selectedForGraph, sources, registry } = useMeasurement()
3052

31-
const apiBase = `${window.location.protocol}//${window.location.hostname}:57666`
32-
33-
// Recording logic
53+
// Recording logic using WebSocket registry data
3454
useEffect(() => {
3555
if (!isRecording || selectedForRecord.size === 0) return
3656

37-
const interval = setInterval(async () => {
57+
const interval = setInterval(() => {
3858
const values: Record<string, number | null> = {}
3959

4060
for (const sourceId of selectedForRecord) {
4161
const source = sources.get(sourceId)
4262
if (!source) continue
4363

44-
try {
45-
const endpoint = `${apiBase}${source.channelPath}/query_output_${source.parameter.toLowerCase()}`
46-
const res = await fetch(endpoint)
47-
if (res.ok) {
48-
const data = await res.json()
49-
values[sourceId] = parseFloat(data.value) || null
50-
} else {
51-
values[sourceId] = null
52-
}
53-
} catch {
54-
values[sourceId] = null
55-
}
64+
values[sourceId] = getValueFromRegistry(registry, source)
5665
}
5766

5867
setRecords(prev => [...prev, { timestamp: new Date(), values }])
5968
}, recordFrequency)
6069

6170
return () => clearInterval(interval)
62-
}, [isRecording, recordFrequency, selectedForRecord, sources, apiBase])
71+
}, [isRecording, recordFrequency, selectedForRecord, sources, registry])
6372

6473
const exportCSV = () => {
6574
if (records.length === 0) return
@@ -390,8 +399,7 @@ function GraphPanel({
390399
const canvasRef = useRef<HTMLCanvasElement>(null)
391400
const [graphData, setGraphData] = useState<Map<string, Array<{ time: number, value: number }>>>(new Map())
392401

393-
const apiBase = `${window.location.protocol}//${window.location.hostname}:57666`
394-
const { sources } = useMeasurement()
402+
const { sources, registry } = useMeasurement()
395403

396404
useEffect(() => {
397405
if (!isDragging) return
@@ -412,34 +420,28 @@ function GraphPanel({
412420
}
413421
}, [isDragging, onHeightChange])
414422

415-
// Fetch data for graphing
423+
// Fetch data for graphing using WebSocket registry data
416424
useEffect(() => {
417425
if (!isGraphing || selectedSources.length === 0) return
418426

419-
const interval = setInterval(async () => {
427+
const interval = setInterval(() => {
420428
const now = Date.now()
421429

422430
for (const source of selectedSources) {
423-
try {
424-
const endpoint = `${apiBase}${source.channelPath}/query_output_${source.parameter.toLowerCase()}`
425-
const res = await fetch(endpoint)
426-
if (res.ok) {
427-
const data = await res.json()
428-
const value = parseFloat(data.value) || 0
429-
430-
setGraphData(prev => {
431-
const next = new Map(prev)
432-
const points = next.get(source.id) || []
433-
next.set(source.id, [...points, { time: now, value }].slice(-100))
434-
return next
435-
})
436-
}
437-
} catch {}
431+
const value = getValueFromRegistry(registry, source)
432+
if (value === null) continue
433+
434+
setGraphData(prev => {
435+
const next = new Map(prev)
436+
const points = next.get(source.id) || []
437+
next.set(source.id, [...points, { time: now, value }].slice(-100))
438+
return next
439+
})
438440
}
439441
}, frequency)
440442

441443
return () => clearInterval(interval)
442-
}, [isGraphing, frequency, selectedSources, apiBase])
444+
}, [isGraphing, frequency, selectedSources, registry])
443445

444446
// Draw graph
445447
useEffect(() => {

benchmesh-serial-service/frontend/src/ui/classes/DMM/GenericDMM.tsx

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useMeasurement } from '../../MeasurementContext'
55
// - Before rendering, fetch GET /instruments/DMM/{device_id} to obtain features (modes list)
66
// - Settings: full-width dropdown to select mode; hover tooltip shows POST endpoint template
77
// - Readings: placeholder big-number display U[V] with 5-digit readonly value
8-
export function GenericDMM({ channelPath }: { channelPath?: string }) {
8+
export function GenericDMM({ channelPath, registry }: { channelPath?: string, registry?: any }) {
99
const apiBase = `${window.location.protocol}//${window.location.hostname}:57666`
1010
const { registerSource } = useMeasurement()
1111

@@ -15,20 +15,66 @@ export function GenericDMM({ channelPath }: { channelPath?: string }) {
1515
const [modes, setModes] = useState<string[]>([])
1616
const [mode, setMode] = useState<string>('')
1717
const [busy, setBusy] = useState(false)
18+
const [modesData, setModesData] = useState<Record<string, any>>({})
19+
const [currentSymbol, setCurrentSymbol] = useState('U')
20+
const [currentUnit, setCurrentUnit] = useState('V')
21+
const [currentNote, setCurrentNote] = useState<string | undefined>(undefined)
22+
23+
// Extract measurement value and prefix from registry
24+
const { measurementValue, unitPrefix } = useMemo(() => {
25+
if (!registry || !deviceId || !klass || !channel) {
26+
return { measurementValue: '00000', unitPrefix: '' }
27+
}
28+
29+
const statusKey = `status_ch${channel}`
30+
const channelData = registry[deviceId]?.[klass]?.[statusKey]
31+
32+
if (!channelData) {
33+
return { measurementValue: '00000', unitPrefix: '' }
34+
}
35+
36+
const rawValue = String(channelData.measurement1_num || '0')
37+
// Ensure 5 numerical digits (excluding decimal point and sign)
38+
const isNegative = rawValue.startsWith('-')
39+
const absoluteValue = isNegative ? rawValue.slice(1) : rawValue
40+
const parts = absoluteValue.split('.')
41+
42+
// Count total digits needed (excluding decimal point)
43+
const totalDigits = parts.join('').length
44+
const zerosNeeded = Math.max(0, 5 - totalDigits)
45+
46+
// Add leading zeros and reconstruct with decimal point if present
47+
const paddedInteger = '0'.repeat(zerosNeeded) + parts[0]
48+
const formattedValue = parts.length > 1 ? `${paddedInteger}.${parts[1]}` : paddedInteger
49+
const value = isNegative ? `-${formattedValue}` : formattedValue
50+
const prefix = channelData.measurement1_symbol || ''
51+
52+
return { measurementValue: value, unitPrefix: prefix }
53+
}, [registry, deviceId, klass, channel])
54+
55+
// Update symbol and unit when mode changes
56+
useEffect(() => {
57+
if (mode && modesData[mode]) {
58+
const modeInfo = modesData[mode]
59+
setCurrentSymbol(modeInfo.symbol || 'U')
60+
setCurrentUnit(modeInfo.unit || 'V')
61+
setCurrentNote(modeInfo.note)
62+
}
63+
}, [mode, modesData])
1864

1965
// Register measurement sources
2066
useEffect(() => {
2167
if (!channelPath || !deviceId) return
2268

2369
registerSource({
24-
id: `${deviceId}-${channel}-U`,
70+
id: `${deviceId}-${channel}-${currentSymbol}`,
2571
deviceId,
2672
channelPath,
2773
parameter: 'voltage',
28-
label: `${deviceId} Ch${channel} U`,
29-
unit: 'V'
74+
label: `${deviceId} Ch${channel} ${currentSymbol}`,
75+
unit: currentUnit
3076
})
31-
}, [channelPath, deviceId, channel, registerSource])
77+
}, [channelPath, deviceId, channel, currentSymbol, currentUnit, registerSource])
3278

3379
// Fetch class features (modes) before rendering content
3480
useEffect(() => {
@@ -40,15 +86,36 @@ export function GenericDMM({ channelPath }: { channelPath?: string }) {
4086
if (!r.ok) return
4187
const j = await r.json().catch(() => ({} as any))
4288
if (!cancelled) {
43-
const mm = Array.isArray(j?.modes) ? (j.modes as any[]).map(String) : []
89+
let mm: string[] = []
90+
let mData: Record<string, any> = {}
91+
92+
if (Array.isArray(j?.modes)) {
93+
// Old format: array of mode names
94+
mm = (j.modes as any[]).map(String)
95+
} else if (j?.modes && typeof j.modes === 'object') {
96+
// New format: object with mode names as keys and details as values
97+
mm = Object.keys(j.modes)
98+
mData = j.modes
99+
}
100+
44101
setModes(mm)
45-
if (!mode && mm.length) setMode(String(mm[0]))
102+
setModesData(mData)
103+
if (!mode && mm.length) {
104+
const firstMode = String(mm[0])
105+
setMode(firstMode)
106+
// Set initial symbol and unit from first mode
107+
if (mData[firstMode]) {
108+
setCurrentSymbol(mData[firstMode].symbol || 'U')
109+
setCurrentUnit(mData[firstMode].unit || 'V')
110+
setCurrentNote(mData[firstMode].note)
111+
}
112+
}
46113
}
47114
} catch {}
48115
}
49116
loadFeatures()
50117
return () => { cancelled = true }
51-
}, [apiBase, deviceId, klass])
118+
}, [apiBase, deviceId, klass, mode])
52119

53120
const endpointTemplate = useMemo(() => {
54121
const k = klass || 'DMM'
@@ -101,7 +168,13 @@ export function GenericDMM({ channelPath }: { channelPath?: string }) {
101168
</div>
102169
<div className="psu-section">
103170
<div className="psu-section-title">Readings</div>
104-
<ReadonlyBigNumber kind="U" label={<Label symbol="U" unit="V"/>} value={"00000"} channelPath={channelPath} parameter="voltage" />
171+
<ReadonlyBigNumber
172+
kind={currentSymbol as 'U' | 'I' | 'P'}
173+
label={<Label symbol={currentSymbol} unit={`${unitPrefix}${currentUnit}`} note={currentNote} />}
174+
value={measurementValue}
175+
channelPath={channelPath}
176+
parameter="voltage"
177+
/>
105178
</div>
106179
<hr className="sep"/>
107180
</div>
@@ -291,10 +364,14 @@ function ReadonlyBigNumber({ kind, label, value, channelPath, parameter }: { kin
291364
)
292365
}
293366

294-
function Label({ symbol, unit }: { symbol: string, unit: string }) {
367+
function Label({ symbol, unit, note }: { symbol: string, unit: string, note?: string }) {
295368
return (
296369
<>
297-
<span className="psu-symbol">{symbol}</span><span className="psu-unit">[{unit}]</span>
370+
<span className="psu-symbol" style={{ fontSize: '24px' }}>{symbol}</span>
371+
<span className="psu-unit" style={{ fontSize: '20px' }}>
372+
[{unit}]
373+
{note && <span style={{ fontSize: '14px', marginLeft: '4px', color: 'var(--text-2)' }}>{note}</span>}
374+
</span>
298375
</>
299376
)
300377
}

0 commit comments

Comments
 (0)