From 7666fbf9b65137187c4add4a3d9010d827b880ae Mon Sep 17 00:00:00 2001 From: csawatzky Date: Fri, 26 Jun 2026 15:25:18 -0600 Subject: [PATCH] update to the bin player to set the bin bushels in the status using the measurements from the bin measurements --- src/bin/BinPlayer.tsx | 94 +++++++++++++++++++++++++++++++++------- src/bin/binTableView.tsx | 2 +- 2 files changed, 79 insertions(+), 17 deletions(-) diff --git a/src/bin/BinPlayer.tsx b/src/bin/BinPlayer.tsx index 6173596..364d5af 100644 --- a/src/bin/BinPlayer.tsx +++ b/src/bin/BinPlayer.tsx @@ -8,7 +8,7 @@ import { Bin } from "models"; import moment, { Moment } from "moment"; import { pond } from "protobuf-ts/pond"; import { quack } from "protobuf-ts/quack"; -import { useComponentAPI } from "providers"; +import { useBinAPI, useComponentAPI } from "providers"; import React, { useEffect, useRef, useState } from "react"; const CHECKPOINT_COUNT = 10; @@ -16,6 +16,7 @@ const CHECKPOINT_COUNT = 10; interface StatusCheckpoint { time: Moment; timeString: string; + statusBushels: number; cables: pond.GrainCable[]; fans: pond.BinFan[]; heaters: pond.BinHeater[]; @@ -99,11 +100,6 @@ type TopNodeHistoryEntry = { topNode: number; }; -/** - * Converts raw `ComponentHistory` entries for one cable into a sorted (ascending) array of - * `TopNodeHistoryEntry` records, keeping only entries where `grain_filled_to` is defined and - * non-zero so that we never overwrite a known top node with a "not set" value. - */ function buildTopNodeHistory( history: pond.ComponentHistory[] ): TopNodeHistoryEntry[] { @@ -116,11 +112,6 @@ function buildTopNodeHistory( .sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf()); } -/** - * Binary-searches a sorted `TopNodeHistoryEntry[]` for the most recent entry whose timestamp - * is at or before `checkpointTime`. Returns `undefined` when no such entry exists (i.e. the - * checkpoint is before the first recorded top-node change). - */ function topNodeAtTime( history: TopNodeHistoryEntry[], checkpointTime: Moment @@ -142,6 +133,61 @@ function topNodeAtTime( return result; } +/** + * A single recorded bushel estimate with its timestamp, sourced from bin object measurements. + */ +type BushelEntry = { + timestamp: Moment; + bushels: number; +}; + +function buildBushelHistory( + response: pond.ListObjectMeasurementsResponse +): BushelEntry[] { + const entries: BushelEntry[] = []; + + response.measurements.forEach(um => { + const isBushelType = + um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_LIDAR || + um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_CABLE || + um.measurementType === quack.MeasurementType.MEASUREMENT_TYPE_BUSHEL_LIBRACART; + + if (!isBushelType) return; + + um.timestamps.forEach((ts, i) => { + const valueArray = um.values[i]; + if (!ts || !valueArray || valueArray.error || valueArray.values.length === 0) return; + entries.push({ + timestamp: moment(ts), + bushels: valueArray.values[0], + }); + }); + }); + + return entries.sort((a, b) => a.timestamp.valueOf() - b.timestamp.valueOf()); +} + +function bushelsAtTime( + history: BushelEntry[], + checkpointTime: Moment +): number | undefined { + if (history.length === 0) return undefined; + const t = checkpointTime.valueOf(); + let lo = 0; + let hi = history.length - 1; + let result: number | undefined = undefined; + while (lo <= hi) { + const mid = (lo + hi) >>> 1; + if (history[mid].timestamp.valueOf() <= t) { + result = history[mid].bushels; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + return result; +} + /** * Produces `count` wall-clock times evenly spaced from `start` through `end` (inclusive). * These are the playback frames — not derived from measurement data. @@ -414,6 +460,10 @@ function buildStatusCheckpoints( heaterMeasurementResponses: pond.SampleUnitMeasurementsResponse[], /** Sorted top-node history per cable key. Pass an empty Map when not needed. */ topNodeHistories: Map, + /** Sorted bushel history derived from bin object measurements. */ + bushelHistory: BushelEntry[], + /** Fallback bushel count used when no measurement precedes a checkpoint. */ + fallbackBushels: number, start: Moment, end: Moment, count: number = CHECKPOINT_COUNT @@ -503,7 +553,12 @@ function buildStatusCheckpoints( return buildBinHeaterFromState(template, merged); }); - return { time, cables, fans, heaters, timeString: time.toISOString() }; + // Resolve bushels: binary search gives the most recent recorded value at or before this + // checkpoint (carry-forward is implicit in the search). Falls back to the current live + // status bushels when the playback range predates all recorded measurements. + const statusBushels = bushelsAtTime(bushelHistory, time) ?? fallbackBushels; + + return { time, cables, fans, heaters, timeString: time.toISOString(), statusBushels }; }).filter(checkpoint => // Keep checkpoints that have at least some cable data checkpoint.cables.some(cable => @@ -537,6 +592,7 @@ export default function BinPlayer(props: Props) { const { bin, componentDevices, setBin } = props; const isMobile = useMobile(); const componentAPI = useComponentAPI(); + const binAPI = useBinAPI(); const [dateSelect, setDateSelect] = useState(0); const [startDate, setStartDate] = useState(moment().subtract(1, 'week')); const [endDate, setEndDate] = useState(moment); @@ -598,7 +654,6 @@ export default function BinPlayer(props: Props) { // Cable top-node history requests — one per cable, covering the full playback range. const cableHistoryRequests = bin.status.grainCables.map(cable => { const deviceID = componentDevices.get(cable.key) ?? 0; - console.log(deviceID) return componentAPI.listHistoryBetween( deviceID, cable.key, @@ -619,15 +674,16 @@ export default function BinPlayer(props: Props) { return componentAPI.sampleUnitMeasurements(deviceID, heater.key, startDate, endDate, 100, undefined, undefined, undefined, undefined, true); }); - const [cableResponses, fanResponses, heaterResponses, cableHistoryResponses] = await Promise.all([ + const binMeasurementsRequest = binAPI.listBinMeasurements(bin.key(), startDate, endDate, 0, 0, 'asc'); + + const [cableResponses, fanResponses, heaterResponses, cableHistoryResponses, binMeasurementsResponse] = await Promise.all([ Promise.all(cableSampleRequests), Promise.all(fanSampleRequests), Promise.all(heaterSampleRequests), Promise.all(cableHistoryRequests), + binMeasurementsRequest, ]); - console.log(cableHistoryResponses) - // Build a per-cable sorted top-node history map for use during checkpoint construction. const topNodeHistories = new Map(); bin.status.grainCables.forEach((cable, i) => { @@ -636,6 +692,9 @@ export default function BinPlayer(props: Props) { topNodeHistories.set(cable.key, entries); }); + // Build sorted bushel history from bin object measurements. + const bushelHistory = buildBushelHistory(binMeasurementsResponse.data); + const checkpoints = buildStatusCheckpoints( bin.status.grainCables, bin.status.fans, @@ -644,6 +703,8 @@ export default function BinPlayer(props: Props) { fanResponses.map(r => r.data), heaterResponses.map(r => r.data), topNodeHistories, + bushelHistory, + bin.status.grainBushels, startDate, endDate, checkpointCountFromRange(startDate, endDate) @@ -664,6 +725,7 @@ export default function BinPlayer(props: Props) { newBin.status.grainCables = checkpoints[i].cables; newBin.status.fans = checkpoints[i].fans; newBin.status.heaters = checkpoints[i].heaters; + newBin.status.grainBushels = checkpoints[i].statusBushels; setBin(newBin); setCurrentCheckpointTime(checkpoints[i].time); diff --git a/src/bin/binTableView.tsx b/src/bin/binTableView.tsx index 0659335..e11132e 100644 --- a/src/bin/binTableView.tsx +++ b/src/bin/binTableView.tsx @@ -312,7 +312,7 @@ export default function BinTableView(props: Props) {