Skip to content

Commit 18329a9

Browse files
frontend: src: components: BasicSettings: Coalesce actuator updates
Prevent snap-back from 1Hz polling by coalescing setActuatorsState requests (latest-wins) and ignoring polled actuator state while a desired value is pending.
1 parent f79c96d commit 18329a9

File tree

1 file changed

+75
-10
lines changed

1 file changed

+75
-10
lines changed

frontend/src/components/BasicSettings.vue

Lines changed: 75 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -649,7 +649,6 @@ import Loading from './Loading.vue'
649649
import { VideoChannelValue, type BaseParameterSetting, type VideoParameterSettings, type VideoResolutionValue, BaseAutoWhiteBalanceModeValue, BaseAutoWhiteBalanceSceneValue, type AdvancedParameterSetting, type CameraControl } from '@/bindings/radcam'
650650
import axios from 'axios'
651651
import type { ActuatorsConfig, ActuatorsControl, ActuatorsParametersConfig, ActuatorsState, CameraID, MountType, ScriptFunction, ServoChannel } from '@/bindings/autopilot'
652-
import { applyNonNull } from '@/utils/jsonUtils'
653652
import ErrorDialog from './ErrorDialog.vue'
654653
import WelcomeDialog from './WelcomeDialog.vue'
655654
import { OneMoreTime } from '@/utils/oneMoreTime'
@@ -744,6 +743,30 @@ const actuatorsState = ref<ActuatorsState>({
744743
zoom: 0,
745744
tilt: 0,
746745
})
746+
type ActuatorKey = keyof ActuatorsState
747+
748+
const approxEqual = (a: number, b: number): boolean => Math.abs(a - b) <= 1e-3
749+
750+
// Tracks the last user-requested value per actuator. While a key is pending, we do not let
751+
// periodic polling overwrite the UI with intermediate/stale values.
752+
const desiredActuatorsState = ref<Record<ActuatorKey, number | null>>({
753+
focus: null,
754+
zoom: null,
755+
tilt: null,
756+
})
757+
758+
// Coalesce actuator set requests so only one request per actuator can be in-flight, always
759+
// sending the most recent value (prevents request pile-up).
760+
const actuatorsSetInFlight = ref<Record<ActuatorKey, boolean>>({
761+
focus: false,
762+
zoom: false,
763+
tilt: false,
764+
})
765+
const actuatorsSetQueued = ref<Record<ActuatorKey, number | null>>({
766+
focus: null,
767+
zoom: null,
768+
tilt: null,
769+
})
747770
const isConfigured = ref<boolean>(true)
748771
const showWelcomeDialog = ref<boolean>(true)
749772
const isLoading = ref<boolean>(false)
@@ -1103,7 +1126,23 @@ const getActuatorsState = () => {
11031126
.then((response) => {
11041127
const state = response.data as ActuatorsState
11051128
1106-
applyNonNull(actuatorsState.value, state)
1129+
;(['focus', 'zoom', 'tilt'] as const).forEach((key) => {
1130+
const desired = desiredActuatorsState.value[key]
1131+
const received = state[key]
1132+
1133+
// If we have a desired value in flight, only accept feedback once it converges.
1134+
if (desired !== null) {
1135+
if (received !== null && received !== undefined && approxEqual(received, desired)) {
1136+
desiredActuatorsState.value[key] = null
1137+
actuatorsState.value[key] = received
1138+
}
1139+
return
1140+
}
1141+
1142+
if (received !== null && received !== undefined) {
1143+
actuatorsState.value[key] = received
1144+
}
1145+
})
11071146
console.log(state)
11081147
isConfigured.value = true
11091148
})
@@ -1114,28 +1153,54 @@ const getActuatorsState = () => {
11141153
})
11151154
}
11161155
1117-
const updateActuatorsState = (param: keyof ActuatorsState, value: number) => {
1156+
const sendQueuedActuatorState = (param: ActuatorKey): void => {
11181157
if (!props.selectedCameraUuid || isLoading.value) return
1158+
if (actuatorsSetInFlight.value[param]) return
1159+
1160+
const value = actuatorsSetQueued.value[param]
1161+
if (value === null) return
1162+
1163+
actuatorsSetQueued.value[param] = null
1164+
actuatorsSetInFlight.value[param] = true
11191165
11201166
const payload: ActuatorsControl = {
11211167
camera_uuid: props.selectedCameraUuid,
11221168
action: "setActuatorsState",
1123-
json: { [param]: value } as ActuatorsState
1169+
json: { [param]: value } as ActuatorsState,
11241170
}
11251171
11261172
axios
11271173
.post(`${props.backendApi}/autopilot/control`, payload)
1128-
.then((response) => {
1129-
const state = response.data as ActuatorsState;
1130-
1131-
applyNonNull(actuatorsState.value, state)
1132-
console.log(state)
1133-
})
11341174
.catch((error) => {
11351175
const message = `Error updating ${param}`
11361176
console.log(message, error.message)
11371177
showWarningToast(message, error)
11381178
})
1179+
.finally(() => {
1180+
actuatorsSetInFlight.value[param] = false
1181+
1182+
// If a newer value was queued while this request was in-flight, send it now.
1183+
if (actuatorsSetQueued.value[param] !== null) {
1184+
sendQueuedActuatorState(param)
1185+
}
1186+
})
1187+
}
1188+
1189+
const updateActuatorsState = (param: keyof ActuatorsState, value: number) => {
1190+
if (!props.selectedCameraUuid || isLoading.value) return
1191+
1192+
const key = param as ActuatorKey
1193+
1194+
// Optimistic UI update: do not wait for feedback to reflect user input.
1195+
actuatorsState.value[key] = value
1196+
desiredActuatorsState.value[key] = value
1197+
1198+
// Coalesce requests per actuator to prevent backlog.
1199+
const existingQueued = actuatorsSetQueued.value[key]
1200+
if (existingQueued === null || !approxEqual(existingQueued, value)) {
1201+
actuatorsSetQueued.value[key] = value
1202+
}
1203+
sendQueuedActuatorState(key)
11391204
}
11401205
11411206
// eslint-disable-next-line @typescript-eslint/no-explicit-any

0 commit comments

Comments
 (0)