Skip to content

Commit d3faf42

Browse files
committed
adding DMM ranges
1 parent 292ac25 commit d3faf42

7 files changed

Lines changed: 307 additions & 6 deletions

File tree

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

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React, { useEffect, useMemo, useRef, useState } from 'react'
22
import { useMeasurement } from '../../MeasurementContext'
3+
import { GenericRange } from './GenericRange'
4+
import { GenericTemp } from './GenericTemp'
35

46
// Generic DMM component styled similarly to GenericPSU
57
// - Before rendering, fetch GET /instruments/DMM/{device_id} to obtain features (modes list)
@@ -19,6 +21,7 @@ export function GenericDMM({ channelPath, registry }: { channelPath?: string, re
1921
const [currentSymbol, setCurrentSymbol] = useState('U')
2022
const [currentUnit, setCurrentUnit] = useState('V')
2123
const [currentNote, setCurrentNote] = useState<string | undefined>(undefined)
24+
const [fullConfig, setFullConfig] = useState<any>(null)
2225

2326
// Extract measurement value and prefix from registry
2427
const { measurementValue, unitPrefix } = useMemo(() => {
@@ -76,7 +79,7 @@ export function GenericDMM({ channelPath, registry }: { channelPath?: string, re
7679
})
7780
}, [channelPath, deviceId, channel, currentSymbol, currentUnit, unitPrefix, registerSource])
7881

79-
// Fetch class features (modes) before rendering content
82+
// Fetch class features (modes and full config) before rendering content
8083
useEffect(() => {
8184
let cancelled = false
8285
async function loadFeatures() {
@@ -86,6 +89,9 @@ export function GenericDMM({ channelPath, registry }: { channelPath?: string, re
8689
if (!r.ok) return
8790
const j = await r.json().catch(() => ({} as any))
8891
if (!cancelled) {
92+
// Store full config
93+
setFullConfig(j)
94+
8995
let mm: string[] = []
9096
let mData: Record<string, any> = {}
9197

@@ -153,6 +159,7 @@ export function GenericDMM({ channelPath, registry }: { channelPath?: string, re
153159
onChange={handleModeChange}
154160
disabled={busy || modes.length === 0}
155161
title={`POST ${endpointTemplate}`}
162+
modesData={modesData}
156163
/>
157164
<button
158165
className="psu-set"
@@ -165,6 +172,21 @@ export function GenericDMM({ channelPath, registry }: { channelPath?: string, re
165172
</button>
166173
<span className="psu-api" title={`GET /instruments/${klass || 'DMM'}/${deviceId || '{id}'}`}>API</span>
167174
</div>
175+
176+
{/* Render mode-specific component */}
177+
{mode && modesData[mode]?.ui_component === 'GenericRange' && fullConfig?.RANGe?.[mode] && (
178+
<GenericRange
179+
mode={mode}
180+
ranges={fullConfig.RANGe[mode]}
181+
channelPath={channelPath}
182+
klass={klass}
183+
deviceId={deviceId}
184+
channel={channel}
185+
/>
186+
)}
187+
{mode && modesData[mode]?.ui_component === 'GenericTemp' && (
188+
<GenericTemp mode={mode} channelPath={channelPath} />
189+
)}
168190
</div>
169191
<div className="psu-section">
170192
<div className="psu-section-title">Readings</div>
@@ -187,13 +209,15 @@ function CustomDropdown({
187209
value,
188210
onChange,
189211
disabled,
190-
title
212+
title,
213+
modesData
191214
}: {
192215
options: string[]
193216
value: string
194217
onChange: (value: string) => void
195218
disabled?: boolean
196219
title?: string
220+
modesData?: Record<string, any>
197221
}) {
198222
const [isOpen, setIsOpen] = useState(false)
199223
const dropdownRef = useRef<HTMLDivElement>(null)
@@ -214,6 +238,11 @@ function CustomDropdown({
214238
setIsOpen(false)
215239
}
216240

241+
// Get display label for a mode
242+
const getLabel = (modeKey: string) => {
243+
return modesData?.[modeKey]?.ui_name || modeKey
244+
}
245+
217246
return (
218247
<div ref={dropdownRef} style={{ position: 'relative', width: '100%' }}>
219248
<button
@@ -242,7 +271,7 @@ function CustomDropdown({
242271
fontSize: '16px',
243272
color: '#c26a1a'
244273
}}>
245-
{value || (options.length === 0 ? 'No modes' : 'Select...')}
274+
{value ? getLabel(value) : (options.length === 0 ? 'No modes' : 'Select...')}
246275
</span>
247276
<span style={{
248277
fontSize: '12px',
@@ -292,7 +321,7 @@ function CustomDropdown({
292321
}
293322
}}
294323
>
295-
{option}
324+
{getLabel(option)}
296325
</div>
297326
))}
298327
</div>
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import React, { useState, useEffect, useRef } from 'react'
2+
3+
interface RangeOption {
4+
value: string
5+
display: string
6+
}
7+
8+
interface GenericRangeProps {
9+
mode: string
10+
ranges?: RangeOption[]
11+
channelPath?: string
12+
klass?: string
13+
deviceId?: string
14+
channel?: string
15+
}
16+
17+
export function GenericRange({ mode, ranges, channelPath, klass, deviceId, channel }: GenericRangeProps) {
18+
const apiBase = `${window.location.protocol}//${window.location.hostname}:57666`
19+
const [selectedRange, setSelectedRange] = useState<string>('AUTO')
20+
const [isOpen, setIsOpen] = useState(false)
21+
const [busy, setBusy] = useState(false)
22+
const dropdownRef = useRef<HTMLDivElement>(null)
23+
24+
// Close dropdown when clicking outside
25+
useEffect(() => {
26+
const handleClickOutside = (event: MouseEvent) => {
27+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
28+
setIsOpen(false)
29+
}
30+
}
31+
document.addEventListener('mousedown', handleClickOutside)
32+
return () => document.removeEventListener('mousedown', handleClickOutside)
33+
}, [])
34+
35+
// Set default to AUTO when ranges change
36+
useEffect(() => {
37+
if (ranges && ranges.length > 0) {
38+
const autoOption = ranges.find(r => r.value === 'AUTO')
39+
if (autoOption) {
40+
setSelectedRange('AUTO')
41+
} else {
42+
setSelectedRange(ranges[0].value)
43+
}
44+
}
45+
}, [ranges])
46+
47+
const handleRangeChange = async (newRange: string) => {
48+
setSelectedRange(newRange)
49+
setIsOpen(false)
50+
51+
if (!klass || !deviceId || !channelPath) return
52+
53+
setBusy(true)
54+
try {
55+
const ch = channel || '1'
56+
const endpoint = `${apiBase}/instruments/${klass}/${deviceId}/${ch}/set_range/${encodeURIComponent(newRange)}`
57+
await fetch(endpoint, { method: 'POST' })
58+
} catch (err) {
59+
console.debug('Range change failed', err)
60+
} finally {
61+
setBusy(false)
62+
}
63+
}
64+
65+
const getDisplayValue = () => {
66+
const option = ranges?.find(r => r.value === selectedRange)
67+
return option?.display || selectedRange
68+
}
69+
70+
if (!ranges || ranges.length === 0) {
71+
return null
72+
}
73+
74+
const endpointTemplate = `/instruments/${klass || 'DMM'}/${deviceId || '{id}'}/${channel || '1'}/set_range/{value}`
75+
76+
return (
77+
<div className="psu-block" style={{ gridTemplateColumns: 'auto 1fr auto auto', width: '100%' }}>
78+
<div className="psu-label">
79+
<span className="psu-symbol">Range</span>
80+
</div>
81+
82+
<div ref={dropdownRef} style={{ position: 'relative', width: '100%' }}>
83+
<button
84+
type="button"
85+
className="psu-number editable"
86+
disabled={busy}
87+
onClick={() => !busy && setIsOpen(!isOpen)}
88+
title={`POST ${endpointTemplate}`}
89+
style={{
90+
width: '100%',
91+
padding: '4px 8px',
92+
cursor: busy ? 'not-allowed' : 'pointer',
93+
background: 'rgba(255,255,255,.03)',
94+
border: '1px solid rgba(255,255,255,.25)',
95+
borderRadius: '4px',
96+
display: 'flex',
97+
justifyContent: 'space-between',
98+
alignItems: 'center',
99+
textAlign: 'left',
100+
opacity: busy ? 0.5 : 1
101+
}}
102+
>
103+
<span style={{
104+
fontVariantNumeric: 'tabular-nums',
105+
fontWeight: 700,
106+
fontSize: '16px',
107+
color: '#c26a1a'
108+
}}>
109+
{getDisplayValue()}
110+
</span>
111+
<span style={{
112+
fontSize: '12px',
113+
color: 'rgba(255,255,255,.5)',
114+
marginLeft: '8px'
115+
}}>
116+
{isOpen ? '▲' : '▼'}
117+
</span>
118+
</button>
119+
120+
{isOpen && (
121+
<div style={{
122+
position: 'absolute',
123+
top: '100%',
124+
left: 0,
125+
right: 0,
126+
marginTop: '4px',
127+
background: '#161d2a',
128+
border: '1px solid #202737',
129+
borderRadius: '6px',
130+
boxShadow: '0 4px 12px rgba(0,0,0,.4)',
131+
zIndex: 1000,
132+
maxHeight: '200px',
133+
overflowY: 'auto'
134+
}}>
135+
{ranges.map((option) => (
136+
<div
137+
key={option.value}
138+
onClick={() => handleRangeChange(option.value)}
139+
style={{
140+
padding: '8px 12px',
141+
cursor: 'pointer',
142+
background: option.value === selectedRange ? 'rgba(96,165,250,.15)' : 'transparent',
143+
color: option.value === selectedRange ? '#bcd9ff' : '#b7c0d1',
144+
fontSize: '14px',
145+
fontWeight: option.value === selectedRange ? 600 : 400,
146+
borderBottom: '1px solid rgba(255,255,255,.05)',
147+
transition: 'background .15s ease'
148+
}}
149+
onMouseEnter={(e) => {
150+
if (option.value !== selectedRange) {
151+
e.currentTarget.style.background = 'rgba(255,255,255,.05)'
152+
}
153+
}}
154+
onMouseLeave={(e) => {
155+
if (option.value !== selectedRange) {
156+
e.currentTarget.style.background = 'transparent'
157+
}
158+
}}
159+
>
160+
{option.display}
161+
</div>
162+
))}
163+
</div>
164+
)}
165+
</div>
166+
167+
<button
168+
className="psu-set"
169+
type="button"
170+
disabled={busy}
171+
title={`POST ${endpointTemplate.replace('{value}', selectedRange)}`}
172+
onClick={() => handleRangeChange(selectedRange)}
173+
>
174+
{busy ? (<><span className="spinner"/>SET</>) : 'SET'}
175+
</button>
176+
<span className="psu-api" title={`GET /instruments/${klass || 'DMM'}/${deviceId || '{id}'}`}>API</span>
177+
</div>
178+
)
179+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react'
2+
3+
interface GenericTempProps {
4+
mode: string
5+
channelPath?: string
6+
}
7+
8+
// Placeholder component for temperature mode
9+
export function GenericTemp({ mode, channelPath }: GenericTempProps) {
10+
return (
11+
<div style={{ padding: '8px', color: 'var(--text-2)', fontSize: '12px', fontStyle: 'italic' }}>
12+
Temperature mode configuration (placeholder)
13+
</div>
14+
)
15+
}

benchmesh-serial-service/src/benchmesh_service/drivers/owon_spm/driver.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from ...transport import SerialTransport
22
from ...utils.si import format_scientific_to_si
3+
from ...utils.si import trim_digits_to
34

45
class OWONSPM:
56
def __init__(self, port, baudrate=115200, serial_mode='8N1', seol='\r', reol='\r'):
@@ -63,7 +64,7 @@ def poll_status_dmm(self, channel: int):
6364
if not raw:
6465
return None
6566
print(num_str)
66-
return {"measurement1_si": parts[1], "measurement1_num": num_str, "measurement1_symbol": sym, "measurement1_function": function}
67+
return {"measurement1_si": parts[1], "measurement1_num": trim_digits_to(num_str, 5), "measurement1_symbol": sym, "measurement1_function": function}
6768

6869

6970
def set_output(self, channel: int, value): # ON / OFF

benchmesh-serial-service/src/benchmesh_service/drivers/owon_spm/manifest.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@
115115
"delta": true,
116116
"RANGe": {
117117
"CAPacitance": [
118+
{
119+
"value": "AUTO",
120+
"display": "AUTO"
121+
},
118122
{
119123
"value": "2E-9",
120124
"display": "2nF"
@@ -145,6 +149,10 @@
145149
}
146150
],
147151
"RESistance": [
152+
{
153+
"value": "AUTO",
154+
"display": "AUTO"
155+
},
148156
{
149157
"value": "MIN",
150158
"display": "MINimum"
@@ -183,6 +191,10 @@
183191
}
184192
],
185193
"CURRent_AC": [
194+
{
195+
"value": "AUTO",
196+
"display": "AUTO"
197+
},
186198
{
187199
"value": "MIN",
188200
"display": "MINimum"
@@ -201,6 +213,10 @@
201213
}
202214
],
203215
"CURRent_DC": [
216+
{
217+
"value": "AUTO",
218+
"display": "AUTO"
219+
},
204220
{
205221
"value": "MIN",
206222
"display": "MINimum"
@@ -219,6 +235,10 @@
219235
}
220236
],
221237
"VOLTage_AC": [
238+
{
239+
"value": "AUTO",
240+
"display": "AUTO"
241+
},
222242
{
223243
"value": "MIN",
224244
"display": "MINimum"
@@ -249,6 +269,10 @@
249269
}
250270
],
251271
"VOLTage_DC": [
272+
{
273+
"value": "AUTO",
274+
"display": "AUTO"
275+
},
252276
{
253277
"value": "MIN",
254278
"display": "MINimum"

0 commit comments

Comments
 (0)