-
Notifications
You must be signed in to change notification settings - Fork 2
GHI #112, #119 - Fix API data pipeline & add altitude tape [FV/typescript-rewrite] #116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a1064da
b2b04db
e67b452
5133acb
77fc40b
0f55948
4f9311d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { type FC, useEffect, useState } from "react"; | ||
| import { POLLING_INTERVAL_MS } from "@/hooks/useSensorData"; | ||
| import { ConversionFactors, altitudeHandler, AltitudeMode } from "@/utils/units/units" | ||
|
|
||
| /* -- Constants -- */ | ||
| const DEFAULT_ALTITUDE_MAXIMUM_METERS = 10000 / ConversionFactors.METERS_TO_FEET; | ||
| const GROUND_BAND_REM = 2; // reserved space at the bottom for the reading | ||
| const TOP_CLEARANCE_REM = 3.33; // reserved space at the top so the pill can't breach it | ||
| const TRACK_TRANSITION_MS = POLLING_INTERVAL_MS; // small buffer so transitions finish before the next poll lands | ||
|
|
||
| /* -- Model Functions -- */ | ||
| export const getAltitudeMaximumMeters = (override?: number): number => { | ||
| if (typeof override === "number" && Number.isFinite(override) && override > 0) { | ||
| return override; | ||
| } | ||
|
|
||
| /* Rev 3's barometer has a different max altitude, so we could | ||
| apply scaling based on the platform using this function. This | ||
| is postponed for now, though. */ | ||
|
|
||
| return DEFAULT_ALTITUDE_MAXIMUM_METERS; | ||
| }; | ||
|
|
||
| /* -- View & Presenter -- */ | ||
| export interface AltitudeTapeProps { | ||
| altitudeMeters: number; | ||
| altitudeMaximumMeters?: number; | ||
| } | ||
|
|
||
| export const AltitudeTape: FC<AltitudeTapeProps> = ({ | ||
| altitudeMeters, | ||
| altitudeMaximumMeters, | ||
| }) => { | ||
| const [hasReceivedReading, setHasReceivedReading] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| if (altitudeMeters > 0 && !hasReceivedReading) { | ||
| setHasReceivedReading(true); | ||
| } | ||
| }, [altitudeMeters, hasReceivedReading]); | ||
|
|
||
| // Inline style rather than a Tailwind class, since the duration is | ||
| // computed at runtime from POLLING_INTERVAL_MS and won't survive | ||
| // Tailwind's JIT class scanning as a template string. | ||
| const trackTransitionStyle = hasReceivedReading | ||
| ? { transition: `bottom ${TRACK_TRANSITION_MS}ms` } | ||
| : {}; | ||
|
|
||
| if( altitudeHandler.mode === AltitudeMode.QFE ) { | ||
| altitudeMeters -= altitudeHandler.referenceElevation; | ||
| } | ||
| const maximum = getAltitudeMaximumMeters(altitudeMaximumMeters); | ||
| const safeAltitude = Number.isFinite(altitudeMeters) ? altitudeMeters : 0; | ||
| const clampedAltitude = Math.max(0, Math.min(safeAltitude, maximum)); | ||
| const fillPercent = maximum > 0 ? (clampedAltitude / maximum) * 100 : 0; | ||
| const clampedFillPercent = Math.max(0, Math.min(fillPercent, 100)); | ||
| const fillFraction = clampedFillPercent / 100; | ||
|
|
||
| // Both the bar and the pill sit within the space above the ground band, | ||
| // so 0% lands right at the top of the ground band instead of the very | ||
| // bottom of the container. The top of the container is also reserved, | ||
| // meaning the pill position will latch at maxAltitude and keep from | ||
| // blocking the unit readout. | ||
| const trackPosition = `calc(${fillFraction} * (100% - ${GROUND_BAND_REM}rem - ${TOP_CLEARANCE_REM}rem) + ${GROUND_BAND_REM}rem)`; | ||
|
|
||
| // Widget Layout | ||
| return ( | ||
| <div className="relative flex h-full w-20 flex-col items-center justify-between rounded-l-none rounded-r-lg font-sans text-xs text-white/90 transition-colors duration-700 shadow-xl"> | ||
| <div className="pointer-events-none absolute inset-0 overflow-hidden rounded-l-none rounded-r-lg"> | ||
| <div className="absolute inset-0 bg-gradient-to-b transition-colors duration-700 to-sky-200 from-blue-400 dark:to-sky-800 dark:from-blue-950" /> | ||
| <div | ||
| className="absolute bottom-0 left-0 right-0 bg-amber-800 transition-colors duration-700 dark:bg-amber-950" | ||
| style={{ height: `${GROUND_BAND_REM}rem` }} | ||
| /> | ||
| <div | ||
| className="absolute left-0 right-0 h-[2px] bg-white shadow-[0_0_4px_rgba(255,255,255,0.8)]" | ||
| style={{ bottom: trackPosition, ...trackTransitionStyle }} | ||
| /> | ||
| </div> | ||
|
|
||
| <div className="relative z-10 flex flex-col items-center pt-2 transition-colors duration-700 text-slate-800 dark:text-slate-400"> | ||
| <span className="font-medium leading-tight">Altitude</span> | ||
| <span className="text-[10px] font-normal leading-tight">({altitudeHandler.getReferenceMode()})</span> | ||
| </div> | ||
|
|
||
| <div | ||
| className="absolute left-0 right-0 z-20 flex justify-center" | ||
| style={{ bottom: trackPosition, transform: "translateY(50%)", ...trackTransitionStyle }} | ||
| > | ||
| <div className="rounded-full border border-white/25 bg-slate-200 dark:bg-slate-800 px-2 py-1 font-medium transition-colors duration-700 text-black/95 dark:text-white/95"> | ||
| {altitudeHandler.getDisplayString(altitudeMeters)} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,9 @@ import { api } from "@/utils/api"; | |
| import { MockFlight } from "@/utils/mock"; | ||
| import type { SensorData } from "@/components/widgets/SensorReadingWidget"; | ||
|
|
||
| /** Used for renderers to determine the fastest possible update rate */ | ||
| export const POLLING_INTERVAL_MS = 40; | ||
|
|
||
| /** | ||
| * Raw sensor payload shape coming from the backend / mock flight source. | ||
| * Field names mirror the device's wire format before conversion to the | ||
|
|
@@ -15,24 +18,22 @@ interface RawSensorPacket { | |
| alt?: number; | ||
| long?: number; | ||
| lat?: number; | ||
| acc_z?: number; | ||
| acc_x?: number; | ||
| roll_rate?: number; | ||
| /** Present when the mock source returns an array instead of a single packet. */ | ||
| length?: number; | ||
| } | ||
|
|
||
| const POLLING_INTERVAL_MS = 40; | ||
|
|
||
| const INITIAL_SENSOR_DATA: SensorData = { | ||
| quatW: 1, | ||
| quatX: 0, | ||
| quatY: 0, | ||
| quatZ: 0, | ||
| altitude: 0, | ||
| longitude: 0, | ||
| latitude: 0, | ||
| accelerationZ: 0, | ||
| rollRate: 0, | ||
| w: 1, | ||
| x: 0, | ||
| y: 0, | ||
| z: 0, | ||
| alt: 0, | ||
| long: 0, | ||
| lat: 0, | ||
| acc_x: 0, | ||
| roll_rate: 0, | ||
| }; | ||
|
|
||
| /** Formats a raw numeric value, falling back to the previous value when invalid. */ | ||
|
|
@@ -43,24 +44,22 @@ function toFixedOrPrevious(value: number | undefined, previous: number, digits = | |
| return Number(Number(value).toFixed(digits)); | ||
| } | ||
|
|
||
| function parseSensorData(data: RawSensorPacket | null, prevState: SensorData): SensorData { | ||
| function parseSensorData(data: RawSensorPacket | null | undefined, prevState: SensorData): SensorData { | ||
| if (!data) { | ||
| return prevState; | ||
| } | ||
|
|
||
| return { | ||
| quatW: toFixedOrPrevious(data.quat_w, prevState.quatW, 6), | ||
| quatX: toFixedOrPrevious(data.quat_x, prevState.quatX, 6), | ||
| quatY: toFixedOrPrevious(data.quat_y, prevState.quatY, 6), | ||
| quatZ: toFixedOrPrevious(data.quat_z, prevState.quatZ, 6), | ||
|
|
||
| altitude: toFixedOrPrevious(data.alt, prevState.altitude), | ||
|
|
||
| longitude: data.lat !== 0 || data.long !== 0 ? (data.long ?? prevState.longitude) : prevState.longitude, | ||
| latitude: data.lat !== 0 || data.long !== 0 ? (data.lat ?? prevState.latitude) : prevState.latitude, | ||
|
|
||
| accelerationZ: toFixedOrPrevious(data.acc_z, prevState.accelerationZ), | ||
| rollRate: toFixedOrPrevious(data.roll_rate, prevState.rollRate), | ||
| w: toFixedOrPrevious(data.quat_w, prevState.w, 6), | ||
| x: toFixedOrPrevious(data.quat_x, prevState.x, 6), | ||
| y: toFixedOrPrevious(data.quat_y, prevState.y, 6), | ||
| z: toFixedOrPrevious(data.quat_z, prevState.z, 6), | ||
|
|
||
| alt: toFixedOrPrevious(data.alt, prevState.alt), | ||
| long: data.lat !== 0 || data.long !== 0 ? (data.long ?? prevState.long) : prevState.long, | ||
| lat: data.lat !== 0 || data.long !== 0 ? (data.lat ?? prevState.lat) : prevState.lat, | ||
| acc_x: toFixedOrPrevious(data.acc_x, prevState.acc_x), | ||
| roll_rate: toFixedOrPrevious(data.roll_rate, prevState.roll_rate), | ||
| }; | ||
|
Comment on lines
+62
to
63
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed against One thing for the display side: accel offset compensation is disabled in firmware ( Will note though that the combination of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gravity compensation also in review |
||
| } | ||
|
|
||
|
|
@@ -74,7 +73,7 @@ export const useSensorData = ( | |
|
|
||
| const fetchData = useCallback(async () => { | ||
| try { | ||
| const result: RawSensorPacket = mock | ||
| const result: RawSensorPacket | undefined = mock | ||
| ? await MockFlight.getSensorData(rowCount) | ||
| : (await api.getSensorData()).data; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The needle position (
trackPosition, driven byclampedAltitude/fillFraction) uses rawaltitudeMeters, but this pill rendersaltitudeHandler.getDisplayString(altitudeMeters), which subtractsreferenceElevationin QFE mode. They will disagree by the launch site elevation whenever QFE is active. Both need to go through the exact same conversion.Second issue:
clampedAltitudeclamps to a minimum of 0, butaltitudeUnits.tscomments state negative altitudes are allowed in QFE.