/************************************************************************** * * Copyright (c) 2025-2026 Diality Inc. - All Rights Reserved. * * THIS CODE MAY NOT BE COPIED OR REPRODUCED IN ANY FORM, IN PART OR IN * WHOLE, WITHOUT THE EXPLICIT PERMISSION OF THE COPYRIGHT OWNER. * * @file FluidBolus.c * * @author (last) Praneeth Bunne * @date (last) 01-May-2026 * * @author (original) Praneeth Bunne * @date (original) 01-May-2026 * ***************************************************************************/ #include "AlarmMgmt.h" #include "AlarmMgmtTD.h" #include "BloodFlow.h" #include "DDInterface.h" #include "FluidBolus.h" #include "Messaging.h" #include "Pressures.h" #include "ModeTreatment.h" #include "StateTxBloodPrime.h" #include "StateTxDialysis.h" #include "StateTxPaused.h" #include "TaskGeneral.h" #include "Timers.h" #include "TxParams.h" #include "Valves.h" /** * @addtogroup FluidBolus * @{ */ // ********** private definitions ********** #define NUM_OF_FLUID_BOLUS_PERMITTED_ALARMS 6U ///< Number of permitted alarms for fluid bolus from paused state. static const U32 FLUID_BOLUS_DATA_PUB_INTERVAL = ( MS_PER_SECOND / TASK_GENERAL_INTERVAL); ///< Saline bolus data broadcast interval (ms/task time) count. // ********** private data ********** static FLUID_BOLUS_STATE_T currentFluidBolusState; ///< Current fluid bolus state static FLUID_BOLUS_MEDIUM_T currentFluidBolusMedium; ///< Current fluid bolus medium static U32 fluidBolusBroadCastTimerCtr; ///< Broadcast Timer counter static BOOL fluidBolusStartRequested; ///< Flag indicates a fluid bolus start has been requested by user. static BOOL fluidBolusAbortRequested; ///< Flag indicates a fluid bolus abort has been requested by user. static BOOL pubBolusPermitted; ///< Flag indicates a bolus is permitted or not to UI (used to broadcast). static U32 targetBloodFlowMLPM; ///< Blood pump flow rate (mL/min) to use for current bolus delivery. static F32 totalFluidVolumeDelivered_mL; ///< Volume (mL) in total of fluid delivered so far (cumulative for all boluses including current one). static F32 bolusFluidVolumeDelivered_mL; ///< Volume (mL) of current bolus delivered so far (calculated from measured blood flow rate). static U32 bolusVolumeLastUpdateTimeStamp; ///< Time stamp for last bolus volume update. // TODO: replace TRUE with getDialysateGoodToDeliverStatus() when we are ready BOOL isDialysateGoodToDeliver = TRUE; ///< Flag indicating dialysate is good to deliver to the patient. ///< Permitted alarm list-bolus allowed from paused state static const ALARM_ID_T FLUID_BOLUS_PERMITTED_PAUSED_ALARMS[ NUM_OF_FLUID_BOLUS_PERMITTED_ALARMS ] = { ALARM_ID_TD_TREATMENT_STOPPED_BY_USER, ALARM_ID_TD_ARTERIAL_PRESSURE_HIGH, ALARM_ID_TD_ARTERIAL_PRESSURE_LOW, ALARM_ID_TD_VENOUS_PRESSURE_HIGH, ALARM_ID_TD_TMP_PRESSURE_HIGH, ALARM_ID_TD_TMP_PRESSURE_LOW }; // ********** private function prototypes ********** static void updateFluidBolusVolumeDelivered( void ); static void completeBolusToCumulative( void ); static FLUID_BOLUS_STATE_T handleFluidBolusIdleState( void ); static FLUID_BOLUS_STATE_T handleFluidBolusWait4Pumps2Stop( void ); static FLUID_BOLUS_STATE_T handleFluidBolusSalineInProgressState( void ); static FLUID_BOLUS_STATE_T handleFluidBolusSubstituteInProgressState( void ); /*********************************************************************//** * @brief * The initFluidBolus function initializes the Fluid Bolus module. * Calling this function resets all bolus state and cumulative volume, * and therefore should only be called when a new treatment is due to begin. * @details \b Inputs: none * @details \b Outputs: currentFluidBolusState, currentFluidBolusMedium, * fluidBolusBroadCastTimerCtr, targetBloodFlowMLPM, totalFluidVolumeDelivered_mL, * bolusFluidVolumeDelivered_mL, fluidBolusStartRequested, * fluidBolusAbortRequested, bolusVolumeLastUpdateTimeStamp, pubBolusPermitted * @return none *************************************************************************/ void initFluidBolus( void ) { currentFluidBolusState = FLUID_BOLUS_IDLE_STATE; currentFluidBolusMedium = getFluidBolusMedium(); fluidBolusBroadCastTimerCtr = FLUID_BOLUS_DATA_PUB_INTERVAL - 10; // setup to stagger publish from other broadcasters targetBloodFlowMLPM = 0; totalFluidVolumeDelivered_mL = 0.0F; bolusFluidVolumeDelivered_mL = 0.0F; fluidBolusStartRequested = FALSE; fluidBolusAbortRequested = FALSE; bolusVolumeLastUpdateTimeStamp = getMSTimerCount(); pubBolusPermitted = TRUE; } /*********************************************************************//** * @brief * The execFluidBolus function executes the fluid bolus state machine. * @details \b Inputs: currentFluidBolusState * @details \b Outputs: currentFluidBolusState * @return Current fluid bolus state machine. *************************************************************************/ FLUID_BOLUS_STATE_T execFluidBolus( void ) { FLUID_BOLUS_STATE_T priorState = currentFluidBolusState; switch ( currentFluidBolusState ) { case FLUID_BOLUS_IDLE_STATE: currentFluidBolusState = handleFluidBolusIdleState(); break; case FLUID_BOLUS_WAIT_FOR_PUMPS_STOP_STATE: currentFluidBolusState = handleFluidBolusWait4Pumps2Stop(); break; case FLUID_BOLUS_SALINE_IN_PROGRESS_STATE: currentFluidBolusState = handleFluidBolusSalineInProgressState(); break; case FLUID_BOLUS_SUBSITUTE_IN_PROGRESS_STATE: currentFluidBolusState = handleFluidBolusSubstituteInProgressState(); break; default: SET_ALARM_WITH_2_U32_DATA( ALARM_ID_TD_SOFTWARE_FAULT, SW_FAULT_ID_INVALID_FLUID_BOLUS_STATE, currentFluidBolusState ) currentFluidBolusState = FLUID_BOLUS_IDLE_STATE; break; } if ( priorState != currentFluidBolusState ) { SEND_EVENT_WITH_2_U32_DATA( TD_EVENT_SUB_STATE_CHANGE, priorState, currentFluidBolusState ); } // Publish fluid bolus data at set interval (whether we are delivering one or not) publishFluidBolusData(); return currentFluidBolusState; } /*********************************************************************//** * @brief * The getFluidBolusMedium function determines the fluid bolus medium based * on the current treatment modality and online fluid configuration. * Substitution fluid applies to HDF treatment, or HD treatment with online * fluid enabled. Saline applies to standard HD without online fluid. * @details \b Inputs: none * @details \b Outputs: none * @return FLUID_BOLUS_MEDIUM_SUBSTITUTE for HDF or HD-online, FLUID_BOLUS_MEDIUM_SALINE otherwise. *************************************************************************/ FLUID_BOLUS_MEDIUM_T getFluidBolusMedium( void ) { FLUID_BOLUS_MEDIUM_T medium = FLUID_BOLUS_MEDIUM_SALINE; U32 modality = getTreatmentParameterU32( TREATMENT_PARAM_TREATMENT_MODALITY ); if ( ( modality == TREATMENT_MODALITY_HDF ) ) // || ( modality == TREATMENT_MODALITY_HD ) && ( check online fluid enabled ) { medium = FLUID_BOLUS_MEDIUM_SUBSTITUTE; } return medium; } /*********************************************************************//** * @brief * The isFluidBolusActive function determines whether a fluid bolus is * currently being delivered. * @details \b Inputs: currentFluidBolusState * @details \b Outputs: none * @return TRUE if bolus state is not IDLE, FALSE otherwise. *************************************************************************/ BOOL isFluidBolusActive( void ) { return ( FLUID_BOLUS_IDLE_STATE != currentFluidBolusState ) ? TRUE : FALSE; } /*********************************************************************//** * @brief * The getTotalFluidBolusVolumeDelivered function gets the current total * fluid volume delivered. * @details \b Inputs: totalFluidVolumeDelivered * @details \b Outputs: none * @return totalFluidVolumeDelivered *************************************************************************/ F32 getTotalFluidBolusVolumeDelivered( void ) { return totalFluidVolumeDelivered_mL; } /*********************************************************************//** * @brief * The signalStartFluidBolus function is called by a parent treatment state * to initiate a fluid bolus. * @details \b Inputs: currentFluidBolusState * @details \b Outputs: fluidBolusStartRequested, targetBloodFlowMLPM * @return none *************************************************************************/ BOOL signalStartFluidBolus( U32 flowRate ) { BOOL result = FALSE; if ( ( FLUID_BOLUS_IDLE_STATE == currentFluidBolusState ) && ( FALSE == fluidBolusStartRequested ) ) { fluidBolusStartRequested = TRUE; targetBloodFlowMLPM = flowRate; result = TRUE; } return result; } /*********************************************************************//** * @brief * The signalAbortFluidBolus function signals an external abort to the * fluid bolus service. Called by user-stop or the global alarm-stop hook. * @details \b Inputs: currentFluidBolusState * @details \b Outputs: fluidBolusAbortRequested * @return none *************************************************************************/ void signalAbortFluidBolus( void ) { if ( FLUID_BOLUS_IDLE_STATE != currentFluidBolusState ) { fluidBolusAbortRequested = TRUE; } } /*********************************************************************//** * @brief * The setBolusPermitted function sets the fluid bolus permitted flag, * which is broadcast to the UI to enable or disable the fluid bolus * button on screen. * @details \b Inputs: none * @details \b Outputs: pubBolusPermitted * @param permitted TRUE if a fluid bolus is currently permitted, FALSE otherwise. * @return none *************************************************************************/ void setBolusPermitted( BOOL permitted ) { pubBolusPermitted = permitted; } /*********************************************************************//** * @brief * The publishFluidBolusData function publishes the fluid bolus data * at the set time interval. * @details \b Inputs: fluidBolusBroadCastTimerCtr, currentFluidBolusState, * bolusFluidVolumeDelivered_mL, totalFluidVolumeDelivered_mL, pubBolusPermitted * @details \b Outputs: fluidBolusBroadCastTimerCtr * @return none *************************************************************************/ void publishFluidBolusData( void ) { if ( ++fluidBolusBroadCastTimerCtr >= FLUID_BOLUS_DATA_PUB_INTERVAL ) { FLUID_BOLUS_DATA_PAYLOAD_T data; data.tgtFluidVolumeMl = getTreatmentParameterU32( TREATMENT_PARAM_FLUID_BOLUS_VOLUME ); data.bolFluidVolumeMl = bolusFluidVolumeDelivered_mL; data.cumFluidVolumeMl = totalFluidVolumeDelivered_mL; data.fluidBolusState = currentFluidBolusState; data.bolusPermitted = pubBolusPermitted; broadcastData( MSG_ID_TD_FLUID_BOLUS_DATA, COMM_BUFFER_OUT_CAN_TD_BROADCAST, (U08*)&data, sizeof( FLUID_BOLUS_DATA_PAYLOAD_T ) ); fluidBolusBroadCastTimerCtr = 0; } } /*********************************************************************//** * @brief * The handleFluidBolusIdleState function handles the idle state of the * fluid bolus state machine. * @details \b Inputs: fluidBolusStartRequested * @details \b Outputs: fluidBolusStartRequested, bolusVolumeLastUpdateTimeStamp * @return next fluid bolus state *************************************************************************/ static FLUID_BOLUS_STATE_T handleFluidBolusIdleState( void ) { FLUID_BOLUS_STATE_T state = FLUID_BOLUS_IDLE_STATE; // Handle fluid bolus start request if ( TRUE == fluidBolusStartRequested ) { fluidBolusStartRequested = FALSE; // Stop blood pump signalBloodPumpHardStop(); // Stop substitution pump cmdSubstitutionRate( 0.0F ); bolusVolumeLastUpdateTimeStamp = getMSTimerCount(); state = FLUID_BOLUS_WAIT_FOR_PUMPS_STOP_STATE; } return state; } /*********************************************************************//** * @brief * The handleFluidBolusWait4Pumps2Stop function handles the wait for pumps * to stop state of the fluid bolus state machine. * @details \b Inputs: fluidBolusAbortRequested, currentFluidBolusMedium * @details \b Outputs: fluidBolusAbortRequested, bolusFluidVolumeDelivered_mL, * bolusVolumeLastUpdateTimeStamp * @return next fluid bolus state *************************************************************************/ static FLUID_BOLUS_STATE_T handleFluidBolusWait4Pumps2Stop( void ) { FLUID_BOLUS_STATE_T state = FLUID_BOLUS_WAIT_FOR_PUMPS_STOP_STATE; if( TRUE == fluidBolusAbortRequested ) { fluidBolusAbortRequested = FALSE; signalBloodPumpHardStop(); state = FLUID_BOLUS_IDLE_STATE; } else if ( FALSE == isBloodPumpRunning() ) // TODO: Write condition to check D92 when HDF is implemented { // Reset bolus data before we start bolusFluidVolumeDelivered_mL = 0.0; bolusVolumeLastUpdateTimeStamp = getMSTimerCount(); // Set arterial & venous valves setValvePosition( H1_VALV, VALVE_POSITION_C_CLOSE ); setValvePosition( H19_VALV, VALVE_POSITION_B_OPEN ); if ( FLUID_BOLUS_MEDIUM_SALINE == currentFluidBolusMedium ) { setBloodPumpTargetFlowRate( targetBloodFlowMLPM, MOTOR_DIR_FORWARD, PUMP_CONTROL_MODE_OPEN_LOOP ); state = FLUID_BOLUS_SALINE_IN_PROGRESS_STATE; } else if ( TRUE == isDialysateGoodToDeliver ) { // set D92 flow rate cmdSubstitutionRate( (F32)targetBloodFlowMLPM ); state = FLUID_BOLUS_SUBSITUTE_IN_PROGRESS_STATE; } else { state = FLUID_BOLUS_IDLE_STATE; } } return state; } /*********************************************************************//** * @brief * The handleFluidBolusSalineInProgressState function handles the saline * in-progress state of the fluid bolus state machine. Integrates delivered * volume from measured blood flow, fires an alarm if the saline bag is empty, * and completes the bolus when the target volume is reached or an abort is requested. * @details \b Inputs: bolusFluidVolumeDelivered_mL, fluidBolusAbortRequested * @details \b Outputs: fluidBolusAbortRequested * @return next fluid bolus state *************************************************************************/ static FLUID_BOLUS_STATE_T handleFluidBolusSalineInProgressState( void ) { FLUID_BOLUS_STATE_T state = FLUID_BOLUS_SALINE_IN_PROGRESS_STATE; F32 bolusTargetVolume = (F32)getTreatmentParameterU32( TREATMENT_PARAM_FLUID_BOLUS_VOLUME ); updateFluidBolusVolumeDelivered(); // Check for empty saline bag per arterial line pressure if ( TRUE == isSalineBagEmpty() ) { SET_ALARM_WITH_1_F32_DATA( ALARM_ID_TD_EMPTY_SALINE_BAG, getFilteredArterialPressure() ); state = FLUID_BOLUS_IDLE_STATE; } // Determine if bolus is complete or stopped by user if ( ( bolusFluidVolumeDelivered_mL >= bolusTargetVolume ) || ( TRUE == fluidBolusAbortRequested ) ) { fluidBolusAbortRequested = FALSE; state = FLUID_BOLUS_IDLE_STATE; } else { // No action required } // complete or abort or alarm if ( state != FLUID_BOLUS_SALINE_IN_PROGRESS_STATE ) { // Hard stop blood signalBloodPumpHardStop(); completeBolusToCumulative(); } return state; } /*********************************************************************//** * @brief * The handleFluidBolusSubstituteInProgressState function handles the * substitute in-progress state state of the fluid bolus state machine. * Integrates delivered volume from measured blood flow. Monitors dialysate * readiness every tick, aborts if dialysate is no longer good to deliver. * Completes the bolus when target is reached or an abort is requested. * @details \b Inputs: bolusFluidVolumeDelivered_mL, fluidBolusAbortRequested * @details \b Outputs: fluidBolusAbortRequested, * totalFluidVolumeDelivered_mL via completeBolusToCumulative() * @return next fluid bolus state. *************************************************************************/ static FLUID_BOLUS_STATE_T handleFluidBolusSubstituteInProgressState( void ) { FLUID_BOLUS_STATE_T state = FLUID_BOLUS_SUBSITUTE_IN_PROGRESS_STATE; F32 bolusTargetVolume = (F32)getTreatmentParameterU32( TREATMENT_PARAM_FLUID_BOLUS_VOLUME ); updateFluidBolusVolumeDelivered(); // Check for dialysate, target volume delivered or abort from user if ( ( FALSE == isDialysateGoodToDeliver ) || ( bolusFluidVolumeDelivered_mL >= bolusTargetVolume ) || ( TRUE == fluidBolusAbortRequested ) ) { fluidBolusAbortRequested = FALSE; cmdSubstitutionRate( 0.0F ); completeBolusToCumulative(); state = FLUID_BOLUS_IDLE_STATE; } return state; } /*********************************************************************//** * @brief * The updateFluidBolusVolumeDelivered function integrates the volume * delivered for the active bolus. * @details \b Inputs: bolusVolumeLastUpdateTimeStamp, targetBloodFlowMLPM * @details \b Outputs: bolusFluidVolumeDelivered_mL, bolusVolumeLastUpdateTimeStamp * @return none *************************************************************************/ static void updateFluidBolusVolumeDelivered( void ) { F32 timeSinceLastVolumeUpdateMin = (F32)calcTimeSince( bolusVolumeLastUpdateTimeStamp ) / (F32)( MS_PER_SECOND * SEC_PER_MIN ); bolusFluidVolumeDelivered_mL += (F32)targetBloodFlowMLPM * timeSinceLastVolumeUpdateMin; bolusVolumeLastUpdateTimeStamp = getMSTimerCount(); } /*********************************************************************//** * @brief * The completeBolusToCumulative function rolls the per-bolus delivered * volume into the cumulative counter and emits a treatment log event. * @details \b Inputs: bolusFluidVolumeDelivered_mL * @details \b Outputs: totalFluidVolumeDelivered_mL, bolusFluidVolumeDelivered_mL, * @return none *************************************************************************/ static void completeBolusToCumulative( void ) { totalFluidVolumeDelivered_mL += bolusFluidVolumeDelivered_mL; SEND_EVENT_WITH_2_F32_DATA( FLUID_BOLUSES_CHANGE_EVENT, bolusFluidVolumeDelivered_mL, totalFluidVolumeDelivered_mL ); bolusFluidVolumeDelivered_mL = 0.0F; } /*********************************************************************//** * @brief * The isBolusAllowedByActiveAlarms function checks whether all * currently active alarms permit a fluid bolus from the paused state. * For saline medium, a non-permitted alarm blocks the bolus only if it is * TD source. For substitute medium, any non-permitted alarm blocks the bolus. * @details \b Inputs: FLUID_BOLUS_PERMITTED_PAUSED_ALARMS[], currentFluidBolusMedium * @details \b Outputs: none * @return TRUE if all active alarms permit the bolus, FALSE otherwise. *************************************************************************/ BOOL isBolusAllowedByActiveAlarms( void ) { U32 alarm; U32 permittedIndex; BOOL permitted = FALSE; BOOL result = TRUE; for ( alarm = 0; alarm < NUM_OF_ALARM_IDS; alarm++ ) { if ( TRUE == isAlarmActive( alarm ) ) { for ( permittedIndex = 0; permittedIndex < NUM_OF_FLUID_BOLUS_PERMITTED_ALARMS; permittedIndex++ ) { if ( FLUID_BOLUS_PERMITTED_PAUSED_ALARMS[ permittedIndex ] == alarm ) { permitted = TRUE; break; } } if ( FALSE == permitted ) { if ( FLUID_BOLUS_MEDIUM_SALINE == currentFluidBolusMedium ) { if ( ALM_SRC_TD == getAlarmSource( alarm ) ) { result = FALSE; break; } } else { result = FALSE; break; } } } } return result; } /*********************************************************************//** * @brief * The handleFluidBolusRequest function handles the UI fluid bolus request. * @details \b Message \b Sent: MSG_ID_TD_FLUID_BOLUS_RESPONSE * @details \b Inputs: none * @details \b Outputs: none * @return TRUE if request is accepted, FALSE if rejected. *************************************************************************/ BOOL handleFluidBolusRequest( MESSAGE_T *message ) { BOOL result = FALSE; U32 cmd = 0; REQUEST_REJECT_REASON_CODE_T rejReason = REQUEST_REJECT_REASON_NONE; FLUID_BOLUS_RESPONSE_PAYLOAD_T response; if ( sizeof(U32) == message->hdr.payloadLen ) { memcpy( &cmd, message->payload, sizeof(U32) ); if ( FLUID_BOLUS_CMD_START == cmd ) { if ( TRUE == isFluidBolusActive() ) { // TD Fluid Bolus Additional Bolus Prevention rejReason = REQUEST_REJECT_REASON_FLUID_BOLUS_IN_PROGRESS; } else if ( TREATMENT_PAUSED_STATE == getTreatmentState() ) { if ( ( TRUE == isBolusAllowedByActiveAlarms() ) ) { result = signalPauseTreatFluidBolusRequest(); rejReason = ( result == TRUE ) ? REQUEST_REJECT_REASON_NONE : REQUEST_REJECT_REASON_INVALID_TREATMENT_SUB_STATE; } else { rejReason = REQUEST_REJECT_REASON_ALARM_IS_ACTIVE; } } else { // Route to whichever calling state is currently active switch ( getTreatmentState() ) { case TREATMENT_BLOOD_PRIME_STATE: result = signalBloodPrimeFluidBolusRequest(); rejReason = ( result == TRUE ) ? REQUEST_REJECT_REASON_NONE : REQUEST_REJECT_REASON_INVALID_TREATMENT_SUB_STATE; break; case TREATMENT_DIALYSIS_STATE: result = signalDialysisFluidBolusRequest(); rejReason = ( result == TRUE ) ? REQUEST_REJECT_REASON_NONE : REQUEST_REJECT_REASON_INVALID_TREATMENT_SUB_STATE; break; // case TREATMENT_HDF_STATE: // result = signalHdfFluidBolusRequest(); // rejReason = ( result == TRUE ) ? REQUEST_REJECT_REASON_NONE : REQUEST_REJECT_REASON_INVALID_TREATMENT_SUB_STATE; // break; // // case TREATMENT_ISO_UF_STATE: // result = signalIsoUFFluidBolusRequest(); // rejReason = ( result == TRUE ) ? REQUEST_REJECT_REASON_NONE : REQUEST_REJECT_REASON_INVALID_TREATMENT_SUB_STATE; // break; // // case TREATMENT_END_STATE: // result = signalTreatmentEndFluidBolusRequest(); // rejReason = ( result == TRUE ) ? REQUEST_REJECT_REASON_NONE : REQUEST_REJECT_REASON_INVALID_TREATMENT_SUB_STATE; // break; default: rejReason = REQUEST_REJECT_REASON_INVALID_TREATMENT_STATE; break; } } } else if ( FLUID_BOLUS_CMD_STOP == cmd ) { if ( TRUE == isFluidBolusActive() ) { signalAbortFluidBolus(); result = TRUE; rejReason = REQUEST_REJECT_REASON_NONE; } else { rejReason = REQUEST_REJECT_REASON_FLUID_BOLUS_NOT_IN_PROGRESS; } } else { rejReason = REQUEST_REJECT_REASON_INVALID_COMMAND; } } else { SET_ALARM_WITH_2_U32_DATA( ALARM_ID_TD_SOFTWARE_FAULT, SW_FAULT_INVALID_PAYLOAD_LENGTH, (U32)message->hdr.payloadLen ); } response.accepted = result; response.rejectionReason = rejReason; response.targetVolumeMl = getTreatmentParameterU32( TREATMENT_PARAM_FLUID_BOLUS_VOLUME ); sendMessage( MSG_ID_TD_FLUID_BOLUS_RESPONSE, COMM_BUFFER_OUT_CAN_TD_2_UI, (U08*)&response, sizeof( FLUID_BOLUS_RESPONSE_PAYLOAD_T ) ); return result; } /**@}*/