Skip to content

Commit b902766

Browse files
Implement Server-Side Calorie Estimation and Display (#1531)
* feat: Implement server-side calorie estimation and display This commit introduces a new feature to calculate and display real-time calorie expenditure based on user heart rate and age. Key changes include: - **Server-Side Calculation:** Added stateful calorie accumulation logic to `utils/socketManager.ts`. The calculation runs on the server to ensure consistency and uses a standard metabolic formula with default weight and age values. - **Updated Data Structures:** Added a `calories` field to the `HrmData` interface in `types/websocket.ts` and `types/index.ts`. - **UI Integration:** Modified the `HrTile.tsx` component to display the new calorie data (KCAL) alongside the BPM. The parent `HrmTiles.tsx` component now passes the `calories` prop. - **Bug Fix:** Corrected a bug in the `WebSocketContext.tsx` reducer where a calorie value of `0` was being improperly handled, preventing the UI from resetting correctly. Also added a default `calories` prop to the `HrTile` component to prevent `NaN` from being displayed. * feat: implement server-side calorie estimation Implements server-side calorie estimation based on heart rate and age. - Adds a `calories` field to the `HrmData` interface. - Defines metabolic constants for calorie calculation. - Implements `calculateCalories` logic in `utils/socketManager.ts`. - Updates the `HrTile` component to display the accumulated calories. - Fixes a bug in the WebSocket context reducer that was dropping the calories field. - Addresses all PR feedback, including linting, styling, and type safety improvements. * fix: resolve build failure by making calories prop optional The previous commit introduced a build failure because the `HrTile` component was used in `app/client/connect/ConnectView.tsx` without the newly required `calories` prop. This commit resolves the issue by making the `calories` prop optional in the `HrTileProps` interface. The component already provides a default value of 0, so this change is safe and prevents build failures in places where the calorie data is not relevant. * chore: update visual regression snapshots Updates the visual regression snapshots to match the UI changes introduced by the new calorie estimation feature. The `HrTile` component now displays the calorie count, which required updating the corresponding snapshot. * chore: finalize calorie estimation feature This marks the completion of the server-side calorie estimation feature. - Implemented calorie calculation logic on the server. - Updated frontend components to display calories. - Fixed build errors and client-side state management bugs. - Updated visual regression snapshots to reflect UI changes. - All pull request feedback has been addressed. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent de84a93 commit b902766

8 files changed

Lines changed: 132 additions & 110 deletions

File tree

components/HrTile.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const HrTile = ({
3232
name,
3333
bpm,
3434
percentMax,
35+
calories = 0, // Default to 0 to prevent NaN
3536
isConnected = true, // Default to connected
3637
isAlerting = false,
3738
alertMessage = 'Checking signal...',
@@ -43,7 +44,7 @@ const HrTile = ({
4344
? alertMessage
4445
: !isConnected
4546
? 'Disconnected - Showing last known value'
46-
: `Name: ${name}, BPM: ${bpm}, % Max HR: ${percentMax}%`
47+
: `Name: ${name}, BPM: ${bpm}, Kcal: ${calories}, % Max HR: ${percentMax}%`
4748

4849
return (
4950
<Tooltip title={tooltipTitle} arrow>
@@ -113,18 +114,38 @@ const HrTile = ({
113114
>
114115
{percentMax}%
115116
</Typography>
116-
<Typography
117-
data-testid="live-hr-value"
118-
variant="h6"
117+
<Box
119118
sx={{
120-
fontWeight: 600,
121-
fontSize: { xs: '1.2rem', sm: '1.4rem', md: '1.6rem' },
122-
transition:
123-
'font-size 0.3s ease-in-out, color 0.3s ease-in-out',
119+
display: 'flex',
120+
justifyContent: 'space-around',
121+
alignItems: 'center',
122+
mt: 1,
124123
}}
125124
>
126-
{bpm} BPM
127-
</Typography>
125+
{/* BPM Display */}
126+
<Typography variant="h6" sx={{ fontWeight: 600 }}>
127+
{bpm}{' '}
128+
<Typography
129+
variant="caption"
130+
component="span"
131+
sx={{ opacity: 0.8 }}
132+
>
133+
BPM
134+
</Typography>
135+
</Typography>
136+
137+
{/* Calorie Display */}
138+
<Typography variant="h6" sx={{ fontWeight: 600 }}>
139+
{Math.floor(calories)}{' '}
140+
<Typography
141+
variant="caption"
142+
component="span"
143+
sx={{ opacity: 0.8 }}
144+
>
145+
KCAL
146+
</Typography>
147+
</Typography>
148+
</Box>
128149
{name && !/^(user|new user)$/i.test(name) && (
129150
<Typography
130151
variant="subtitle1"
@@ -154,6 +175,7 @@ const arePropsEqual = (prevProps: HrTileProps, nextProps: HrTileProps) => {
154175
prevProps.name === nextProps.name &&
155176
prevProps.bpm === nextProps.bpm &&
156177
prevProps.percentMax === nextProps.percentMax &&
178+
prevProps.calories === nextProps.calories &&
157179
prevProps.isConnected === nextProps.isConnected &&
158180
prevProps.isAlerting === nextProps.isAlerting &&
159181
prevProps.alertMessage === nextProps.alertMessage

components/HrmTiles.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ const HrmTiles = () => {
4242
name={user.name || ''}
4343
bpm={user.value}
4444
percentMax={hrZoneProps.percentage}
45+
calories={user.calories || 0} // Pass calories
4546
isAlerting={!!matchingAlert}
4647
// Conditionally add alertMessage to avoid passing `undefined`
4748
{...(matchingAlert && { alertMessage: matchingAlert.message })}

context/WebSocketContext.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,19 +110,18 @@ export const WebSocketProvider = ({
110110

111111
// Create a new state array by merging existing and new data
112112
const mergedHrmData = state.hrmData.map((existingUser) => {
113-
// If the user is in the new payload, update their data and mark as connected
114113
if (incomingClients.has(existingUser.clientId)) {
115114
const updatedUser = payload.find(
116115
(newUser) => newUser.clientId === existingUser.clientId
117116
)
118-
// If updatedUser is found, always use its data and mark as connected.
119-
// The 'value > 0' check is not relevant for determining if a *connected* device's data should be used.
120117
return updatedUser
121-
? { ...updatedUser, isConnected: true }
122-
: { ...existingUser, isConnected: true } // Fallback, though updatedUser should always exist if incomingClients.has(clientId)
118+
? {
119+
...existingUser,
120+
...updatedUser,
121+
isConnected: true,
122+
}
123+
: { ...existingUser, isConnected: true }
123124
}
124-
// If the user is NOT in the new payload, they have disconnected.
125-
// Keep their last known data but mark as disconnected.
126125
return { ...existingUser, isConnected: false }
127126
})
128127

1.79 KB
Loading

types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface HrTileProps {
55
name: string
66
bpm: number
77
percentMax: number // 0-100
8+
calories?: number
89
isConnected?: boolean
910

1011
// NEW: Flag to trigger the visual diagnostic state

types/websocket.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface HrmData {
1212
maxHr: number
1313
name?: string
1414
age?: number
15+
calories: number // Added field
1516
}
1617

1718
export type TimerMode = 'STOPWATCH' | 'TABATA'

utils/constants.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,16 @@ export const HEART_RATE_ZONES: HeartRateZoneConfig[] = [
5050
{ name: 'Zone 2', minPercent: 60, maxPercent: 70, color: '#2196F3' },
5151
{ name: 'Zone 1', minPercent: 50, maxPercent: 60, color: '#9E9E9E' },
5252
]
53+
54+
// --- Calorie Calculation Constants ---
55+
// Based on standard metabolic formulas (e.g., Keytel)
56+
export const CALORIE_DEFAULTS = {
57+
WEIGHT_KG: 75, // Default weight if not provided
58+
// Simplified Factors (Male/Female average or specific)
59+
// Formula: Calories/min = (-55.0969 + 0.6309 x HR + 0.1988 x Weight + 0.2017 x Age) / 4.184
60+
FACTOR_HR: 0.6309,
61+
FACTOR_WEIGHT: 0.1988,
62+
FACTOR_AGE: 0.2017,
63+
INTERCEPT: 55.0969,
64+
JOULE_CONVERSION: 4.184,
65+
}

0 commit comments

Comments
 (0)