From 7d2bdb94cfd674e7668137b8cec855fa094755cd Mon Sep 17 00:00:00 2001 From: Dan Keenan Date: Mon, 16 Sep 2024 10:18:07 -0400 Subject: [PATCH] Start UI. --- messages/ReceiveLevelsReq.fbs | 5 + messages/ReceiveLevelsResp.fbs | 11 ++ mobile_sacn_webui/src/common/colorForCID.ts | 2 +- .../src/receive/levels/Levels.tsx | 133 ++++++++++++++---- 4 files changed, 125 insertions(+), 26 deletions(-) diff --git a/messages/ReceiveLevelsReq.fbs b/messages/ReceiveLevelsReq.fbs index 86437eb..90e7741 100644 --- a/messages/ReceiveLevelsReq.fbs +++ b/messages/ReceiveLevelsReq.fbs @@ -2,8 +2,13 @@ include "Universe.fbs"; namespace mobilesacn.message; +table FlickerFinder { + flicker_finder:bool; +} + union ReceiveLevelsReqVal { universe:Universe, + flicker_finder:FlickerFinder, } table ReceiveLevelsReq { diff --git a/messages/ReceiveLevelsResp.fbs b/messages/ReceiveLevelsResp.fbs index b942964..368c004 100644 --- a/messages/ReceiveLevelsResp.fbs +++ b/messages/ReceiveLevelsResp.fbs @@ -8,6 +8,16 @@ table LevelsChanged { owners:[string] (required); } +struct LevelChange { + address: uint16; + new_level: uint8; + change: int8; +} + +table Flicker { + changes: [LevelChange] (required); +} + table SourceUpdated { cid:string (required); name:string (required); @@ -27,6 +37,7 @@ table SystemTime { union ReceiveLevelsRespVal { levelsChanged:LevelsChanged, + flicker:Flicker, sourceUpdated:SourceUpdated, sourceExpired:SourceExpired, systemTime:SystemTime, diff --git a/mobile_sacn_webui/src/common/colorForCID.ts b/mobile_sacn_webui/src/common/colorForCID.ts index ab27774..e00569a 100644 --- a/mobile_sacn_webui/src/common/colorForCID.ts +++ b/mobile_sacn_webui/src/common/colorForCID.ts @@ -2,7 +2,7 @@ import CRC32 from "crc-32"; import Color from "colorjs.io"; import {each, every} from "lodash"; -interface CidColor { +export interface CidColor { light: Color; dark: Color; } diff --git a/mobile_sacn_webui/src/receive/levels/Levels.tsx b/mobile_sacn_webui/src/receive/levels/Levels.tsx index 540d033..8ab4a88 100644 --- a/mobile_sacn_webui/src/receive/levels/Levels.tsx +++ b/mobile_sacn_webui/src/receive/levels/Levels.tsx @@ -11,7 +11,7 @@ import useWebsocket from "../../common/useWebsocket.ts"; import {ReceiveLevelsResp} from "../../messages/receive-levels-resp.ts"; import {ReceiveLevelsRespVal} from "../../messages/receive-levels-resp-val.ts"; import {SourceUpdated} from "../../messages/source-updated.ts"; -import colorForCID from "../../common/colorForCID.ts"; +import colorForCID, {CidColor} from "../../common/colorForCID.ts"; import {SourceExpired} from "../../messages/source-expired.ts"; import {ByteBuffer} from "flatbuffers"; import {LevelsChanged} from "../../messages/levels-changed.ts"; @@ -27,6 +27,8 @@ import LevelDisplay, {PriorityDisplay} from "../../common/components/LevelDispla import {LevelBar} from "../../common/components/LevelBar.tsx"; import bigIntAbs from "../../common/bigIntAbs.ts"; import AppContext from "../../common/Context.ts"; +import {FlickerFinder} from "../../messages/flicker-finder.ts"; +import {Flicker} from "../../messages/flicker.ts"; enum ViewMode { GRID = "grid", @@ -35,8 +37,7 @@ enum ViewMode { interface Source { cid: string; - lightColor: Color; - darkColor: Color; + color: CidColor; name: string; ipAddr: string; hasPap: boolean; @@ -47,8 +48,10 @@ interface Source { // Used for addresses that have no owner. const DEFAULT_SOURCE: Source = { cid: "00000000-0000-0000-0000-000000000000", - lightColor: new Color("transparent"), - darkColor: new Color("transparent"), + color: { + light: new Color("transparent"), + dark: new Color("transparent"), + }, name: "No Source", ipAddr: "", hasPap: false, @@ -56,8 +59,23 @@ const DEFAULT_SOURCE: Source = { universes: new Uint16Array(), }; +const RAISE_COLOR: CidColor = { + light: new Color("aqua"), + dark: new Color("blue"), +} +const LOWER_COLOR: CidColor = { + light: new Color("lime"), + dark: new Color("blue") +} + +const SAME_COLOR: CidColor = { + light: new Color("silver"), + dark: new Color("gray"), +} + const emptyLevelBuffer = () => Array.from(times(DMX_MAX, constant(0))); const emptyOwnerBuffer = () => Array.from(times(DMX_MAX, constant(""))); +const emptyFlickerBuffer = () => Array.from(times(DMX_MAX, constant(null))); const emptySourceMap = () => new Map(); function* getSourceListUniverses(sources: Iterable): Generator { @@ -86,9 +104,31 @@ export function Component() { }, [sourceMap]); const [viewMode, setViewMode] = useState(ViewMode.GRID); const [showPriorities, setShowPriorities] = useState(true); + const [flickerFinder, setFlickerFinder] = useState(false); + const [flickers, setFlickers] = useState<(number | null)[]>(emptyFlickerBuffer()); const [showUnivDialog, setShowUnivDialog] = useState(false); const openUnivDialog = useCallback(() => setShowUnivDialog(true), [setShowUnivDialog]); const closeUnivDialog = useCallback(() => setShowUnivDialog(false), [setShowUnivDialog]); + const addressColors = useMemo(() => { + if (flickerFinder) { + return flickers.map(change => { + if (change === null) { + return DEFAULT_SOURCE.color; + } else if (change < 0) { + return LOWER_COLOR; + } else if (change > 0) { + return RAISE_COLOR; + } else { + return SAME_COLOR; + } + }); + } else { + return owners.map(cid => { + const source = sourceMap.get(cid) ?? DEFAULT_SOURCE; + return source.color; + }); + } + }, [flickerFinder, flickers, owners, sourceMap]); // RPC Receivers const onSourceUpdated = useCallback((msg: SourceUpdated) => { @@ -101,7 +141,6 @@ export function Component() { } else { universes = msg.universesArray() ?? Uint16Array.from([universe]); } - const color = colorForCID(cid); const newSource: Source = { cid: cid, name: msg.name() as string, @@ -109,8 +148,7 @@ export function Component() { hasPap: msg.hasPap() ?? oldSource?.hasPap ?? DEFAULT_SOURCE.hasPap, priority: msg.priority() ?? oldSource?.priority ?? DEFAULT_SOURCE.priority, universes: universes, - lightColor: color.light, - darkColor: color.dark, + color: colorForCID(cid) }; newSourceMap.set(cid, newSource); setSourceMap(newSourceMap); @@ -130,9 +168,20 @@ export function Component() { const newPriorities = Array.from({length: LevelBuffer.sizeOf()}, (v, i) => msgPriorities.levels(i)) as number[]; setPriorities(newPriorities); - const newOwners = Array.from({length: msg.ownersLength()}, (v, i) => msg.owners(i)) as string[]; + const newOwners = Array.from({length: msg.ownersLength()}, (v, i) => msg.owners(i)); setOwners(newOwners); }, [setLevels, setPriorities, setOwners]); + const onFlicker = useCallback((msg: Flicker) => { + const newFlickers = flickers.slice(); + const newLevels = levels.slice(); + for (let ix = 0; ix < msg.changesLength(); ++ix) { + const change = msg.changes(ix)!; + newFlickers[change.address()] = change.change(); + newLevels[change.address()] = change.newLevel(); + } + setLevels(newLevels); + setFlickers(newFlickers); + }, [flickers, setFlickers, levels, setLevels]); const onSystemTime = useCallback((timestamp: bigint) => { setServerTimeOffset(timestamp - BigInt(Date.now())); }, [setServerTimeOffset]); @@ -156,10 +205,13 @@ export function Component() { } const msgLevelsChanged = msg.val(new LevelsChanged()) as LevelsChanged; onLevelsChanged(msgLevelsChanged); + } else if (msg.valType() === ReceiveLevelsRespVal.flicker) { + const msgFlicker = msg.val(new Flicker()) as Flicker; + onFlicker(msgFlicker); } else if (msg.valType() == ReceiveLevelsRespVal.systemTime) { onSystemTime(msg.timestamp()); } - }, [serverTimeOffset, onSourceUpdated, onSourceExpired, onLevelsChanged, onSystemTime]); + }, [serverTimeOffset, onSourceUpdated, onSourceExpired, onLevelsChanged, onFlicker, onSystemTime]); const onOpen = useCallback((e: WebSocketEventMap["open"]) => { const ws = e.currentTarget; if (ws instanceof WebSocket) { @@ -176,12 +228,12 @@ export function Component() { // RPC Setters const sendUniverse = useCallback((val: typeof universe) => { - let builder = new fbsBuilder(); - let msgUniverse = Universe.createUniverse(builder, val); + const builder = new fbsBuilder(); + const msgUniverse = Universe.createUniverse(builder, val); ReceiveLevelsReq.startReceiveLevelsReq(builder); ReceiveLevelsReq.addValType(builder, ReceiveLevelsReqVal.universe); ReceiveLevelsReq.addVal(builder, msgUniverse); - let msgReceiveLevelsReq = ReceiveLevelsReq.endReceiveLevelsReq(builder); + const msgReceiveLevelsReq = ReceiveLevelsReq.endReceiveLevelsReq(builder); builder.finish(msgReceiveLevelsReq); const data = builder.asUint8Array(); sendMessage(data); @@ -194,6 +246,21 @@ export function Component() { setOwners(emptyOwnerBuffer()); setSourceMap(emptySourceMap()); }, [universe, sendUniverse, setLevels, setPriorities, setOwners, setSourceMap]); + const sendFlickerFinder = useCallback((val: typeof flickerFinder) => { + const builder = new fbsBuilder(); + const msgFlickerFinder = FlickerFinder.createFlickerFinder(builder, val); + ReceiveLevelsReq.startReceiveLevelsReq(builder); + ReceiveLevelsReq.addValType(builder, ReceiveLevelsReqVal.flicker_finder); + ReceiveLevelsReq.addVal(builder, msgFlickerFinder); + const msgReceiveLevelsReq = ReceiveLevelsReq.endReceiveLevelsReq(builder); + builder.finish(msgReceiveLevelsReq); + const data = builder.asUint8Array(); + sendMessage(data); + }, [sendMessage]); + useEffect(() => { + sendFlickerFinder(flickerFinder); + setFlickers(emptyFlickerBuffer()); + }, [flickerFinder, sendFlickerFinder, setFlickers]); return ( <> @@ -228,6 +295,19 @@ export function Component() {

Universe {universe}

source.universes.includes(universe))}/> + + setShowPriorities(!showPriorities)} + /> + setFlickerFinder(!flickerFinder)} + /> + + @@ -248,6 +329,7 @@ export function Component() { levels={levels} priorities={priorities} owners={owners} + colors={addressColors} showPriorities={showPriorities} /> @@ -343,7 +425,7 @@ function SourceList(props: SourceListProps) { {sources.map(source => ( + style={{backgroundColor: darkMode ? source.color.dark.display() : source.color.light.display()}}> {source.name} {source.ipAddr} {source.hasPap && "*"}{source.priority} @@ -376,13 +458,14 @@ interface LevelsViewProps { levels: number[]; priorities: number[]; owners: string[]; + colors: CidColor[]; showPriorities: boolean; } const DEFAULT_VIEW_GRID_COLS = 4; function ViewGrid(props: LevelsViewProps) { - const {sourceMap, levels, priorities, owners} = props; + const {levels, priorities, colors} = props; const {darkMode} = useContext(AppContext); const [recalcCols, setRecalcCols] = useState(true); const [cols, setCols] = useState(DEFAULT_VIEW_GRID_COLS); @@ -425,13 +508,13 @@ function ViewGrid(props: LevelsViewProps) { setPreferredCellHeight(tdHeight); setCols(cols + 1); } - }, [recalcCols, cols]); + }, [recalcCols, cols, preferredCellHeight]); useEffect(() => { window.addEventListener("resize", forceRecalcCols); return () => { window.removeEventListener("resize", forceRecalcCols); }; - }, []); + }, [forceRecalcCols]); // Setup level rows. const rows = []; @@ -444,14 +527,14 @@ function ViewGrid(props: LevelsViewProps) { // Add cells for each address. for (let colIx = 0; colIx < cols; ++colIx, ++levelIx) { const level = levels[levelIx]; - const ownerCid = owners[levelIx]; - const owner = sourceMap.get(ownerCid) ?? DEFAULT_SOURCE; + const cidColor = colors[levelIx] ?? DEFAULT_SOURCE.color; + const color = darkMode ? cidColor.dark : cidColor.light; const priority = priorities[levelIx]; row.push( @@ -489,13 +572,13 @@ function ViewBarsTitle() { } function ViewBars(props: LevelsViewProps) { - const {sourceMap, levels, priorities, owners} = props; + const {levels, priorities, colors} = props; const fgColors = useMemo(() => { - return owners.map(owner => sourceMap.get(owner)?.lightColor ?? DEFAULT_SOURCE.lightColor); - }, [sourceMap, owners]); + return colors.map(color => color.light ?? DEFAULT_SOURCE.color.light); + }, [colors]); const bgColors = useMemo(() => { - return owners.map(owner => sourceMap.get(owner)?.darkColor ?? DEFAULT_SOURCE.darkColor); - }, [sourceMap, owners]); + return colors.map(color => color.dark ?? DEFAULT_SOURCE.color.dark); + }, [colors]); return (