5a8527e96e
The bridge becomes pure transport: it opens the port, writes a poll string it is given at runtime, and streams raw lines. Protocol definitions live in Django and parsing happens in the browser, so a new scale type never means a new binary on the station PCs. The poll loop stays in the bridge because a browser cannot be the heartbeat - hidden tabs get throttled and a closed tab would stop the scale. Every payload now carries a hex field. Toledo Continuous sends STX and bit-packed status, which UTF-8 decoding silently corrupts. Fixed-column protocols need the opposite care: a space can mean balanced, so the trailing whitespace must survive. Parsers for the simulator, fixed-column, Toledo Continuous and SICS return an explicit error rather than a guess on a frame that does not match, since a silently wrong weight on a weighbridge is worse than no weight. Toledo and SICS are written from documented structure and have not been validated against real hardware. The admin monitor shows the raw stream as text and hex, sends one-off probe commands, and builds fixed-column protocols against captured lines. Spaces render as middots because in some protocols a space is the stability flag. The test writer now simulates a weighing cycle - idle, ramp, balanced, drop - alternating empty and loaded trucks, quantised to the 20kg and 50kg divisions real truck scales use. Bridge logging rotates and per-reading lines drop to DEBUG; the old config had grown two log files totalling 368MB. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
3.5 KiB
React
101 lines
3.5 KiB
React
import { createContext, useContext, useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
import { useNomenclatures } from './NomenclatureContext';
|
|
import api from '../services/api';
|
|
|
|
const ScaleProtocolContext = createContext(null);
|
|
|
|
// Which protocol this station uses. Deployment is flexible (one central Django
|
|
// at the port, single-machine elsewhere), so the station is identified by the
|
|
// bridge it talks to on localhost - this only records the operator's choice.
|
|
const SELECTED_KEY = 'scalesapp.selectedProtocolId';
|
|
|
|
export const useScaleProtocols = () => {
|
|
const ctx = useContext(ScaleProtocolContext);
|
|
if (!ctx) throw new Error('useScaleProtocols must be used within ScaleProtocolProvider');
|
|
return ctx;
|
|
};
|
|
|
|
export function ScaleProtocolProvider({ children }) {
|
|
const { subscribeToUpdates } = useNomenclatures();
|
|
|
|
const [protocols, setProtocols] = useState([]);
|
|
const [selectedId, setSelectedId] = useState(() => {
|
|
try {
|
|
const stored = localStorage.getItem(SELECTED_KEY);
|
|
return stored ? parseInt(stored, 10) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
});
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
const [error, setError] = useState(null);
|
|
|
|
// subscribeToUpdates is not memoized in NomenclatureContext - hold it in a
|
|
// ref so the SSE effect runs once instead of on every provider render.
|
|
const subscribeRef = useRef(subscribeToUpdates);
|
|
subscribeRef.current = subscribeToUpdates;
|
|
|
|
const load = useCallback(async () => {
|
|
try {
|
|
const res = await api.get('/api/scale-protocols/');
|
|
setProtocols(res.data.results || res.data);
|
|
setError(null);
|
|
} catch {
|
|
setError('Failed to load scale protocols');
|
|
} finally {
|
|
setIsLoaded(true);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = subscribeRef.current((message) => {
|
|
if (message.object !== 'scale_protocol') return;
|
|
const { operation, data } = message;
|
|
setProtocols(prev => {
|
|
if (operation === 'delete') return prev.filter(p => p.id !== data.id);
|
|
const without = prev.filter(p => p.id !== data.id);
|
|
return [...without, data].sort((a, b) => a.name.localeCompare(b.name));
|
|
});
|
|
});
|
|
return unsubscribe;
|
|
}, []);
|
|
|
|
const selectProtocol = useCallback((id) => {
|
|
setSelectedId(id);
|
|
try {
|
|
if (id === null) localStorage.removeItem(SELECTED_KEY);
|
|
else localStorage.setItem(SELECTED_KEY, String(id));
|
|
} catch {
|
|
// Storage unavailable (private window, blocked site data) - the
|
|
// selection still applies for this session.
|
|
}
|
|
}, []);
|
|
|
|
// Fall back to the simulator so a fresh install shows a weight rather than
|
|
// an error before anyone has chosen a protocol.
|
|
const selectedProtocol = useMemo(() => {
|
|
if (!protocols.length) return null;
|
|
return protocols.find(p => p.id === selectedId)
|
|
|| protocols.find(p => p.kind === 'simulator')
|
|
|| protocols[0];
|
|
}, [protocols, selectedId]);
|
|
|
|
const value = {
|
|
protocols,
|
|
selectedProtocol,
|
|
selectedId,
|
|
selectProtocol,
|
|
isLoaded,
|
|
error,
|
|
refresh: load,
|
|
};
|
|
|
|
return (
|
|
<ScaleProtocolContext.Provider value={value}>
|
|
{children}
|
|
</ScaleProtocolContext.Provider>
|
|
);
|
|
}
|